feat(game): 게임 시스템 전면 개편 및 다국어 지원 확장

## 스킬 시스템 개선
- skill_data.dart: 스킬 데이터 구조 전면 개편 (+1176 라인)
- skill_service.dart: 스킬 발동 로직 확장 및 버프 시스템 연동
- skill.dart: 스킬 모델 개선, 쿨다운/효과 타입 추가

## Canvas 애니메이션 리팩토링
- battle_composer.dart 삭제 (레거시 위젯 기반 렌더러)
- monster_colors.dart 삭제 (AsciiCell 색상 시스템으로 통합)
- canvas_battle_composer.dart: z-index 정렬 (몬스터 z=1, 캐릭터 z=2, 이펙트 z=3)
- ascii_cell.dart, ascii_layer.dart: 코드 정리

## UI/UX 개선
- hp_mp_bar.dart: l10n 적용, 몬스터 HP 바 컴팩트화
- death_overlay.dart: 사망 화면 개선
- equipment_stats_panel.dart: 장비 스탯 표시 확장
- active_buff_panel.dart: 버프 패널 개선
- notification_overlay.dart: 알림 시스템 개선

## 다국어 지원 확장
- game_text_l10n.dart: 게임 텍스트 통합 (+758 라인)
- 한국어/일본어/영어/중국어 번역 업데이트
- ARB 파일 동기화

## 게임 로직 개선
- progress_service.dart: 진행 로직 리팩토링
- combat_calculator.dart: 전투 계산 로직 개선
- stat_calculator.dart: 스탯 계산 시스템 개선
- story_service.dart: 스토리 진행 로직 개선

## 기타
- theme_preferences.dart 삭제 (미사용)
- 테스트 파일 업데이트
- class_data.dart: 클래스 데이터 정리
This commit is contained in:
JiWoong Sul
2025-12-22 19:00:58 +09:00
parent f606fca063
commit 99f5b74802
63 changed files with 3403 additions and 2740 deletions

View File

@@ -1,5 +1,7 @@
import 'package:flutter/material.dart';
import 'package:askiineverdie/data/game_text_l10n.dart' as game_l10n;
import 'package:askiineverdie/data/skill_data.dart';
import 'package:askiineverdie/data/story_data.dart';
import 'package:askiineverdie/l10n/app_localizations.dart';
import 'package:askiineverdie/src/core/animation/ascii_animation_type.dart';
@@ -8,6 +10,7 @@ import 'package:askiineverdie/src/core/model/combat_event.dart';
import 'package:askiineverdie/src/core/l10n/game_data_l10n.dart';
import 'package:askiineverdie/src/core/model/game_state.dart';
import 'package:askiineverdie/src/core/model/hall_of_fame.dart';
import 'package:askiineverdie/src/core/model/skill.dart';
import 'package:askiineverdie/src/core/notification/notification_service.dart';
import 'package:askiineverdie/src/core/storage/hall_of_fame_storage.dart';
import 'package:askiineverdie/src/core/util/pq_logic.dart' as pq_logic;
@@ -18,7 +21,6 @@ import 'package:askiineverdie/src/features/game/widgets/combat_log.dart';
import 'package:askiineverdie/src/features/game/widgets/death_overlay.dart';
import 'package:askiineverdie/src/features/game/widgets/hp_mp_bar.dart';
import 'package:askiineverdie/src/features/game/widgets/notification_overlay.dart';
import 'package:askiineverdie/src/features/game/widgets/skill_panel.dart';
import 'package:askiineverdie/src/features/game/widgets/stats_panel.dart';
import 'package:askiineverdie/src/features/game/widgets/equipment_stats_panel.dart';
import 'package:askiineverdie/src/features/game/widgets/potion_inventory_panel.dart';
@@ -78,7 +80,10 @@ class _GamePlayScreenState extends State<GamePlayScreen>
if (state.traits.level > _lastLevel && _lastLevel > 0) {
_specialAnimation = AsciiAnimationType.levelUp;
_notificationService.showLevelUp(state.traits.level);
_addCombatLog('Level Up! Now level ${state.traits.level}', CombatLogType.levelUp);
_addCombatLog(
'${game_l10n.uiLevelUp} Lv.${state.traits.level}',
CombatLogType.levelUp,
);
_resetSpecialAnimationAfterFrame();
// Phase 9: Act 변경 감지 (레벨 기반)
@@ -111,7 +116,10 @@ class _GamePlayScreenState extends State<GamePlayScreen>
.lastOrNull;
if (completedQuest != null) {
_notificationService.showQuestComplete(completedQuest.caption);
_addCombatLog('Quest Complete: ${completedQuest.caption}', CombatLogType.questComplete);
_addCombatLog(
game_l10n.uiQuestComplete(completedQuest.caption),
CombatLogType.questComplete,
);
}
_resetSpecialAnimationAfterFrame();
}
@@ -131,11 +139,9 @@ class _GamePlayScreenState extends State<GamePlayScreen>
/// Phase 8: 전투 로그 추가 (Add Combat Log Entry)
void _addCombatLog(String message, CombatLogType type) {
_combatLogEntries.add(CombatLogEntry(
message: message,
timestamp: DateTime.now(),
type: type,
));
_combatLogEntries.add(
CombatLogEntry(message: message, timestamp: DateTime.now(), type: type),
);
// 최대 50개 유지
if (_combatLogEntries.length > 50) {
_combatLogEntries.removeAt(0);
@@ -167,53 +173,78 @@ class _GamePlayScreenState extends State<GamePlayScreen>
/// 전투 이벤트를 메시지와 타입으로 변환
(String, CombatLogType) _formatCombatEvent(CombatEvent event) {
final target = event.targetName ?? '';
return switch (event.type) {
CombatEventType.playerAttack => event.isCritical
? ('CRITICAL! ${event.damage} damage to ${event.targetName}!', CombatLogType.critical)
: ('You hit ${event.targetName} for ${event.damage} damage', CombatLogType.damage),
CombatEventType.playerAttack =>
event.isCritical
? (
game_l10n.combatCritical(event.damage, target),
CombatLogType.critical,
)
: (
game_l10n.combatYouHit(target, event.damage),
CombatLogType.damage,
),
CombatEventType.monsterAttack => (
'${event.targetName} hits you for ${event.damage} damage',
CombatLogType.monsterAttack,
),
game_l10n.combatMonsterHitsYou(target, event.damage),
CombatLogType.monsterAttack,
),
CombatEventType.playerEvade => (
'You evaded ${event.targetName}\'s attack!',
CombatLogType.evade,
),
game_l10n.combatYouEvaded(target),
CombatLogType.evade,
),
CombatEventType.monsterEvade => (
'${event.targetName} evaded your attack!',
CombatLogType.evade,
),
game_l10n.combatMonsterEvaded(target),
CombatLogType.evade,
),
CombatEventType.playerBlock => (
'Blocked! Reduced to ${event.damage} damage',
CombatLogType.block,
),
game_l10n.combatBlocked(event.damage),
CombatLogType.block,
),
CombatEventType.playerParry => (
'Parried! Reduced to ${event.damage} damage',
CombatLogType.parry,
),
CombatEventType.playerSkill => event.isCritical
? ('CRITICAL ${event.skillName}! ${event.damage} damage!', CombatLogType.critical)
: ('${event.skillName}: ${event.damage} damage', CombatLogType.spell),
game_l10n.combatParried(event.damage),
CombatLogType.parry,
),
CombatEventType.playerSkill =>
event.isCritical
? (
game_l10n.combatSkillCritical(
event.skillName ?? '',
event.damage,
),
CombatLogType.critical,
)
: (
game_l10n.combatSkillDamage(event.skillName ?? '', event.damage),
CombatLogType.spell,
),
CombatEventType.playerHeal => (
'${event.skillName ?? "Heal"}: +${event.healAmount} HP',
CombatLogType.heal,
game_l10n.combatSkillHeal(
event.skillName ?? game_l10n.uiHeal,
event.healAmount,
),
CombatLogType.heal,
),
CombatEventType.playerBuff => (
'${event.skillName} activated!',
CombatLogType.buff,
),
game_l10n.combatBuffActivated(event.skillName ?? ''),
CombatLogType.buff,
),
CombatEventType.dotTick => (
'${event.skillName} ticks for ${event.damage} damage',
CombatLogType.dotTick,
),
game_l10n.combatDotTick(event.skillName ?? '', event.damage),
CombatLogType.dotTick,
),
CombatEventType.playerPotion => (
'${event.skillName}: +${event.healAmount} ${event.targetName}',
CombatLogType.potion,
game_l10n.combatPotionUsed(
event.skillName ?? '',
event.healAmount,
target,
),
CombatLogType.potion,
),
CombatEventType.potionDrop => (
'Dropped: ${event.skillName}',
CombatLogType.potionDrop,
),
game_l10n.combatPotionDrop(event.skillName ?? ''),
CombatLogType.potionDrop,
),
};
}
@@ -394,105 +425,107 @@ class _GamePlayScreenState extends State<GamePlayScreen>
}
},
child: Scaffold(
appBar: AppBar(
title: Text(L10n.of(context).progressQuestTitle(state.traits.name)),
actions: [
// 치트 버튼 (디버그용)
if (widget.controller.cheatsEnabled) ...[
IconButton(
icon: const Text('L+1'),
tooltip: L10n.of(context).levelUp,
onPressed: () => widget.controller.loop?.cheatCompleteTask(),
),
IconButton(
icon: const Text('Q!'),
tooltip: L10n.of(context).completeQuest,
onPressed: () => widget.controller.loop?.cheatCompleteQuest(),
),
IconButton(
icon: const Text('P!'),
tooltip: L10n.of(context).completePlot,
onPressed: () => widget.controller.loop?.cheatCompletePlot(),
),
],
],
),
body: Stack(
children: [
// 메인 게임 UI
Column(
children: [
// 상단: ASCII 애니메이션 + Task Progress (Phase 7: 고정 4색 팔레트)
TaskProgressPanel(
progress: state.progress,
speedMultiplier: widget.controller.loop?.speedMultiplier ?? 1,
onSpeedCycle: () {
widget.controller.loop?.cycleSpeed();
setState(() {});
},
isPaused: !widget.controller.isRunning,
onPauseToggle: () async {
await widget.controller.togglePause();
setState(() {});
},
specialAnimation: _specialAnimation,
weaponName: state.equipment.weapon,
shieldName: state.equipment.shield,
characterLevel: state.traits.level,
monsterLevel: state.progress.currentTask.monsterLevel,
latestCombatEvent: state.progress.currentCombat?.recentEvents.lastOrNull,
appBar: AppBar(
title: Text(L10n.of(context).progressQuestTitle(state.traits.name)),
actions: [
// 치트 버튼 (디버그용)
if (widget.controller.cheatsEnabled) ...[
IconButton(
icon: const Text('L+1'),
tooltip: L10n.of(context).levelUp,
onPressed: () => widget.controller.loop?.cheatCompleteTask(),
),
// 메인 3패널 영역
Expanded(
child: Row(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// 좌측 패널: Character Sheet
Expanded(flex: 2, child: _buildCharacterPanel(state)),
// 중앙 패널: Equipment/Inventory
Expanded(flex: 3, child: _buildEquipmentPanel(state)),
// 우측 패널: Plot/Quest
Expanded(flex: 2, child: _buildQuestPanel(state)),
],
),
IconButton(
icon: const Text('Q!'),
tooltip: L10n.of(context).completeQuest,
onPressed: () => widget.controller.loop?.cheatCompleteQuest(),
),
IconButton(
icon: const Text('P!'),
tooltip: L10n.of(context).completePlot,
onPressed: () => widget.controller.loop?.cheatCompletePlot(),
),
],
),
],
),
body: Stack(
children: [
// 메인 게임 UI
Column(
children: [
// 상단: ASCII 애니메이션 + Task Progress (Phase 7: 고정 4색 팔레트)
TaskProgressPanel(
progress: state.progress,
speedMultiplier:
widget.controller.loop?.speedMultiplier ?? 1,
onSpeedCycle: () {
widget.controller.loop?.cycleSpeed();
setState(() {});
},
isPaused: !widget.controller.isRunning,
onPauseToggle: () async {
await widget.controller.togglePause();
setState(() {});
},
specialAnimation: _specialAnimation,
weaponName: state.equipment.weapon,
shieldName: state.equipment.shield,
characterLevel: state.traits.level,
monsterLevel: state.progress.currentTask.monsterLevel,
latestCombatEvent:
state.progress.currentCombat?.recentEvents.lastOrNull,
),
// Phase 4: 사망 오버레이 (Death Overlay)
if (state.isDead && state.deathInfo != null)
DeathOverlay(
deathInfo: state.deathInfo!,
traits: state.traits,
onResurrect: () async {
// 1. 부활 처리 (HP/MP 회복, 장비 구매 - 게임 재개 없음)
await widget.controller.resurrect();
// 메인 3패널 영역
Expanded(
child: Row(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// 좌측 패널: Character Sheet
Expanded(flex: 2, child: _buildCharacterPanel(state)),
// 2. 부활 애니메이션 재생
setState(() {
_specialAnimation = AsciiAnimationType.resurrection;
});
// 중앙 패널: Equipment/Inventory
Expanded(flex: 3, child: _buildEquipmentPanel(state)),
// 3. 애니메이션 종료 후 게임 재개
final duration = getSpecialAnimationDuration(
AsciiAnimationType.resurrection,
);
Future.delayed(Duration(milliseconds: duration), () async {
if (mounted) {
setState(() {
_specialAnimation = null;
});
// 부활 후 게임 재개 (새 루프 시작)
await widget.controller.resumeAfterResurrection();
}
});
},
// 우측 패널: Plot/Quest
Expanded(flex: 2, child: _buildQuestPanel(state)),
],
),
),
],
),
],
),
// Phase 4: 사망 오버레이 (Death Overlay)
if (state.isDead && state.deathInfo != null)
DeathOverlay(
deathInfo: state.deathInfo!,
traits: state.traits,
onResurrect: () async {
// 1. 부활 처리 (HP/MP 회복, 장비 구매 - 게임 재개 없음)
await widget.controller.resurrect();
// 2. 부활 애니메이션 재생
setState(() {
_specialAnimation = AsciiAnimationType.resurrection;
});
// 3. 애니메이션 종료 후 게임 재개
final duration = getSpecialAnimationDuration(
AsciiAnimationType.resurrection,
);
Future.delayed(Duration(milliseconds: duration), () async {
if (mounted) {
setState(() {
_specialAnimation = null;
});
// 부활 후 게임 재개 (새 루프 시작)
await widget.controller.resumeAfterResurrection();
}
});
},
),
],
),
),
),
);
@@ -519,13 +552,17 @@ class _GamePlayScreenState extends State<GamePlayScreen>
// Phase 8: HP/MP 바 (사망 위험 시 깜빡임, 전투 중 몬스터 HP 표시)
// 전투 중에는 전투 스탯의 HP/MP 사용, 비전투 시 기본 스탯 사용
HpMpBar(
hpCurrent: state.progress.currentCombat?.playerStats.hpCurrent ??
hpCurrent:
state.progress.currentCombat?.playerStats.hpCurrent ??
state.stats.hp,
hpMax: state.progress.currentCombat?.playerStats.hpMax ??
hpMax:
state.progress.currentCombat?.playerStats.hpMax ??
state.stats.hpMax,
mpCurrent: state.progress.currentCombat?.playerStats.mpCurrent ??
mpCurrent:
state.progress.currentCombat?.playerStats.mpCurrent ??
state.stats.mp,
mpMax: state.progress.currentCombat?.playerStats.mpMax ??
mpMax:
state.progress.currentCombat?.playerStats.mpMax ??
state.stats.mpMax,
// 전투 중일 때 몬스터 HP 정보 전달
monsterHpCurrent:
@@ -545,16 +582,12 @@ class _GamePlayScreenState extends State<GamePlayScreen>
'${l10n.xpNeededForNextLevel}',
),
// Spell Book
// 스킬 (Skills - SpellBook 기반)
_buildSectionHeader(l10n.spellBook),
Expanded(flex: 2, child: _buildSpellsList(state)),
// Phase 8: 스킬 (Skills with cooldown glow)
_buildSectionHeader('Skills'),
Expanded(flex: 2, child: SkillPanel(skillSystem: state.skillSystem)),
Expanded(flex: 3, child: _buildSkillsList(state)),
// 활성 버프 (Active Buffs)
_buildSectionHeader('Buffs'),
_buildSectionHeader(game_l10n.uiBuffs),
Expanded(
child: ActiveBuffPanel(
activeBuffs: state.skillSystem.activeBuffs,
@@ -587,7 +620,7 @@ class _GamePlayScreenState extends State<GamePlayScreen>
Expanded(child: _buildInventoryList(state)),
// Potions (물약 인벤토리)
_buildSectionHeader('Potions'),
_buildSectionHeader(game_l10n.uiPotions),
Expanded(
child: PotionInventoryPanel(
inventory: state.potionInventory,
@@ -647,7 +680,8 @@ class _GamePlayScreenState extends State<GamePlayScreen>
Colors.green,
tooltip: state.progress.quest.max > 0
? l10n.percentComplete(
100 * state.progress.quest.position ~/
100 *
state.progress.quest.position ~/
state.progress.quest.max,
)
: null,
@@ -737,10 +771,16 @@ class _GamePlayScreenState extends State<GamePlayScreen>
);
}
Widget _buildSpellsList(GameState state) {
/// 통합 스킬 목록 (SpellBook 기반)
///
/// 스펠 이름, 랭크, 스킬 타입, 쿨타임 표시
Widget _buildSkillsList(GameState state) {
if (state.spellBook.spells.isEmpty) {
return Center(
child: Text(L10n.of(context).noSpellsYet, style: const TextStyle(fontSize: 11)),
child: Text(
L10n.of(context).noSpellsYet,
style: const TextStyle(fontSize: 11),
),
);
}
@@ -749,21 +789,22 @@ class _GamePlayScreenState extends State<GamePlayScreen>
padding: const EdgeInsets.symmetric(horizontal: 8),
itemBuilder: (context, index) {
final spell = state.spellBook.spells[index];
final skill = SkillData.getSkillBySpellName(spell.name);
final spellName = GameDataL10n.getSpellName(context, spell.name);
return Row(
children: [
Expanded(
child: Text(
spellName,
style: const TextStyle(fontSize: 11),
overflow: TextOverflow.ellipsis,
),
),
Text(
spell.rank,
style: const TextStyle(fontSize: 11, fontWeight: FontWeight.bold),
),
],
// 쿨타임 상태 확인
final skillState = skill != null
? state.skillSystem.getSkillState(skill.id)
: null;
final isOnCooldown =
skillState != null &&
!skillState.isReady(state.skillSystem.elapsedMs, skill!.cooldownMs);
return _SkillRow(
spellName: spellName,
rank: spell.rank,
skill: skill,
isOnCooldown: isOnCooldown,
);
},
);
@@ -880,7 +921,8 @@ class _GamePlayScreenState extends State<GamePlayScreen>
padding: const EdgeInsets.symmetric(horizontal: 8),
itemBuilder: (context, index) {
final quest = questHistory[index];
final isCurrentQuest = index == questHistory.length - 1 && !quest.isComplete;
final isCurrentQuest =
index == questHistory.length - 1 && !quest.isComplete;
return Row(
children: [
@@ -942,3 +984,70 @@ class _GamePlayScreenState extends State<GamePlayScreen>
return result;
}
}
/// 스킬 행 위젯
///
/// 스펠 이름, 랭크, 스킬 타입 아이콘, 쿨타임 상태 표시
class _SkillRow extends StatelessWidget {
const _SkillRow({
required this.spellName,
required this.rank,
required this.skill,
required this.isOnCooldown,
});
final String spellName;
final String rank;
final Skill? skill;
final bool isOnCooldown;
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 1),
child: Row(
children: [
// 스킬 타입 아이콘
_buildTypeIcon(),
const SizedBox(width: 4),
// 스킬 이름
Expanded(
child: Text(
spellName,
style: TextStyle(
fontSize: 11,
color: isOnCooldown ? Colors.grey : null,
),
overflow: TextOverflow.ellipsis,
),
),
// 쿨타임 표시
if (isOnCooldown)
const Icon(Icons.hourglass_empty, size: 10, color: Colors.orange),
const SizedBox(width: 4),
// 랭크
Text(
rank,
style: const TextStyle(fontSize: 11, fontWeight: FontWeight.bold),
),
],
),
);
}
/// 스킬 타입별 아이콘
Widget _buildTypeIcon() {
if (skill == null) {
return const SizedBox(width: 12);
}
final (IconData icon, Color color) = switch (skill!.type) {
SkillType.attack => (Icons.flash_on, Colors.red),
SkillType.heal => (Icons.favorite, Colors.green),
SkillType.buff => (Icons.arrow_upward, Colors.blue),
SkillType.debuff => (Icons.arrow_downward, Colors.purple),
};
return Icon(icon, size: 12, color: color);
}
}