From 99f5b748021ec335e152642c75ab62821b38783d Mon Sep 17 00:00:00 2001 From: JiWoong Sul Date: Mon, 22 Dec 2025 19:00:58 +0900 Subject: [PATCH] =?UTF-8?q?feat(game):=20=EA=B2=8C=EC=9E=84=20=EC=8B=9C?= =?UTF-8?q?=EC=8A=A4=ED=85=9C=20=EC=A0=84=EB=A9=B4=20=EA=B0=9C=ED=8E=B8=20?= =?UTF-8?q?=EB=B0=8F=20=EB=8B=A4=EA=B5=AD=EC=96=B4=20=EC=A7=80=EC=9B=90=20?= =?UTF-8?q?=ED=99=95=EC=9E=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 스킬 시스템 개선 - 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: 클래스 데이터 정리 --- lib/data/class_data.dart | 146 +- lib/data/game_text_l10n.dart | 758 +++++++++- lib/data/game_translations_ja.dart | 44 +- lib/data/game_translations_ko.dart | 71 +- lib/data/skill_data.dart | 1216 ++++++++++++++--- lib/data/story_data.dart | 66 +- lib/l10n/app_en.arb | 8 +- lib/l10n/app_ja.arb | 4 +- lib/l10n/app_ko.arb | 2 +- lib/l10n/app_localizations.dart | 8 +- lib/l10n/app_localizations_en.dart | 4 +- lib/l10n/app_localizations_ja.dart | 4 +- lib/l10n/app_localizations_ko.dart | 2 +- lib/l10n/app_localizations_zh.dart | 4 +- lib/l10n/app_zh.arb | 4 +- lib/src/app.dart | 10 +- lib/src/core/animation/battle_composer.dart | 815 ----------- lib/src/core/animation/canvas/ascii_cell.dart | 10 +- .../core/animation/canvas/ascii_layer.dart | 6 +- .../canvas/canvas_battle_composer.dart | 158 +-- .../canvas/canvas_special_composer.dart | 166 +-- .../canvas/canvas_town_composer.dart | 38 +- lib/src/core/animation/character_frames.dart | 108 +- lib/src/core/animation/monster_colors.dart | 192 --- lib/src/core/animation/weapon_category.dart | 7 +- lib/src/core/animation/weapon_effects.dart | 2 +- lib/src/core/engine/combat_calculator.dart | 18 +- lib/src/core/engine/item_service.dart | 41 +- lib/src/core/engine/potion_service.dart | 30 +- lib/src/core/engine/progress_service.dart | 223 +-- lib/src/core/engine/resurrection_service.dart | 26 +- lib/src/core/engine/shop_service.dart | 17 +- lib/src/core/engine/skill_service.dart | 225 ++- lib/src/core/engine/stat_calculator.dart | 23 +- lib/src/core/engine/story_service.dart | 25 +- lib/src/core/l10n/game_data_l10n.dart | 43 +- lib/src/core/model/combat_stats.dart | 32 +- lib/src/core/model/game_state.dart | 33 +- lib/src/core/model/hall_of_fame.dart | 4 +- lib/src/core/model/potion.dart | 15 +- lib/src/core/model/race_traits.dart | 9 +- lib/src/core/model/skill.dart | 59 +- .../notification/notification_service.dart | 92 +- lib/src/core/storage/theme_preferences.dart | 24 - lib/src/core/util/pq_logic.dart | 18 +- lib/src/features/front/front_screen.dart | 17 +- lib/src/features/game/game_play_screen.dart | 437 +++--- .../game/game_session_controller.dart | 3 +- .../game/widgets/active_buff_panel.dart | 77 +- .../game/widgets/ascii_animation_card.dart | 180 ++- .../features/game/widgets/cinematic_view.dart | 16 +- lib/src/features/game/widgets/combat_log.dart | 10 +- .../features/game/widgets/death_overlay.dart | 125 +- .../game/widgets/equipment_stats_panel.dart | 123 +- lib/src/features/game/widgets/hp_mp_bar.dart | 27 +- .../game/widgets/notification_overlay.dart | 67 +- .../game/widgets/potion_inventory_panel.dart | 18 +- .../features/game/widgets/skill_panel.dart | 25 +- .../game/widgets/task_progress_panel.dart | 10 +- .../hall_of_fame/hall_of_fame_screen.dart | 115 +- .../new_character/new_character_screen.dart | 72 +- test/core/util/pq_logic_test.dart | 5 +- test/regression/deterministic_game_test.dart | 6 +- 63 files changed, 3403 insertions(+), 2740 deletions(-) delete mode 100644 lib/src/core/animation/battle_composer.dart delete mode 100644 lib/src/core/animation/monster_colors.dart delete mode 100644 lib/src/core/storage/theme_preferences.dart diff --git a/lib/data/class_data.dart b/lib/data/class_data.dart index 862a52d..bc18c03 100644 --- a/lib/data/class_data.dart +++ b/lib/data/class_data.dart @@ -16,10 +16,7 @@ class ClassData { static const bugHunter = ClassTraits( classId: 'bug_hunter', name: 'Bug Hunter', - statModifiers: { - StatType.str: 2, - StatType.intelligence: 1, - }, + statModifiers: {StatType.str: 2, StatType.intelligence: 1}, startingSkills: ['power_strike'], classSkills: ['power_strike', 'execute', 'bug_smash'], passives: [ @@ -36,10 +33,7 @@ class ClassData { static const overflowWarrior = ClassTraits( classId: 'overflow_warrior', name: 'Overflow Warrior', - statModifiers: { - StatType.str: 2, - StatType.con: 1, - }, + statModifiers: {StatType.str: 2, StatType.con: 1}, startingSkills: ['power_strike'], classSkills: ['power_strike', 'overflow_slash', 'buffer_break'], passives: [ @@ -56,10 +50,7 @@ class ClassData { static const stackCrusher = ClassTraits( classId: 'stack_crusher', name: 'Stack Crusher', - statModifiers: { - StatType.str: 2, - StatType.con: 1, - }, + statModifiers: {StatType.str: 2, StatType.con: 1}, startingSkills: ['power_strike'], classSkills: ['power_strike', 'stack_smash', 'heap_slam'], passives: [ @@ -81,10 +72,7 @@ class ClassData { static const assertionKnight = ClassTraits( classId: 'assertion_knight', name: 'Assertion Knight', - statModifiers: { - StatType.str: 2, - StatType.wis: 1, - }, + statModifiers: {StatType.str: 2, StatType.wis: 1}, startingSkills: ['shield_bash'], classSkills: ['shield_bash', 'assert_strike', 'validation_guard'], passives: [ @@ -94,9 +82,7 @@ class ClassData { description: '방어력 +10%', ), ], - restriction: EquipmentRestriction( - armorWeight: ArmorWeight.heavy, - ), + restriction: EquipmentRestriction(armorWeight: ArmorWeight.heavy), ); // ========================================================================== @@ -107,10 +93,7 @@ class ClassData { static const debuggerPaladin = ClassTraits( classId: 'debugger_paladin', name: 'Debugger Paladin', - statModifiers: { - StatType.wis: 2, - StatType.con: 1, - }, + statModifiers: {StatType.wis: 2, StatType.con: 1}, startingSkills: ['shield_bash'], classSkills: ['shield_bash', 'debug_heal', 'breakpoint_guard'], passives: [ @@ -125,19 +108,14 @@ class ClassData { description: '회복력 +10%', ), ], - restriction: EquipmentRestriction( - armorWeight: ArmorWeight.heavy, - ), + restriction: EquipmentRestriction(armorWeight: ArmorWeight.heavy), ); /// Loop Breaker: CON + STR (스탯 합계: +3) static const loopBreaker = ClassTraits( classId: 'loop_breaker', name: 'Loop Breaker', - statModifiers: { - StatType.con: 2, - StatType.str: 1, - }, + statModifiers: {StatType.con: 2, StatType.str: 1}, startingSkills: ['shield_bash'], classSkills: ['shield_bash', 'infinite_guard', 'break_stance'], passives: [ @@ -147,19 +125,14 @@ class ClassData { description: 'HP +15%', ), ], - restriction: EquipmentRestriction( - armorWeight: ArmorWeight.heavy, - ), + restriction: EquipmentRestriction(armorWeight: ArmorWeight.heavy), ); /// Garbage Collector: CON + STR (스탯 합계: +3) static const garbageCollector = ClassTraits( classId: 'garbage_collector', name: 'Garbage Collector', - statModifiers: { - StatType.con: 2, - StatType.str: 1, - }, + statModifiers: {StatType.con: 2, StatType.str: 1}, startingSkills: ['absorb'], classSkills: ['absorb', 'recycle', 'memory_sweep'], passives: [ @@ -174,9 +147,7 @@ class ClassData { description: '전투 후 HP 5% 회복', ), ], - restriction: EquipmentRestriction( - armorWeight: ArmorWeight.heavy, - ), + restriction: EquipmentRestriction(armorWeight: ArmorWeight.heavy), ); // ========================================================================== @@ -187,10 +158,7 @@ class ClassData { static const compilerMage = ClassTraits( classId: 'compiler_mage', name: 'Compiler Mage', - statModifiers: { - StatType.intelligence: 2, - StatType.wis: 1, - }, + statModifiers: {StatType.intelligence: 2, StatType.wis: 1}, startingSkills: ['fireball'], classSkills: ['fireball', 'compile_blast', 'syntax_storm'], passives: [ @@ -200,19 +168,14 @@ class ClassData { description: '마법 데미지 +15%', ), ], - restriction: EquipmentRestriction( - armorWeight: ArmorWeight.light, - ), + restriction: EquipmentRestriction(armorWeight: ArmorWeight.light), ); /// Recursion Master: INT + DEX (스탯 합계: +3) static const recursionMaster = ClassTraits( classId: 'recursion_master', name: 'Recursion Master', - statModifiers: { - StatType.intelligence: 2, - StatType.dex: 1, - }, + statModifiers: {StatType.intelligence: 2, StatType.dex: 1}, startingSkills: ['fireball'], classSkills: ['fireball', 'recursive_bolt', 'stack_overflow'], passives: [ @@ -222,19 +185,14 @@ class ClassData { description: '마법 데미지 +20%', ), ], - restriction: EquipmentRestriction( - armorWeight: ArmorWeight.light, - ), + restriction: EquipmentRestriction(armorWeight: ArmorWeight.light), ); /// Memory Leaker: INT + WIS (스탯 합계: +3) static const memoryLeaker = ClassTraits( classId: 'memory_leaker', name: 'Memory Leaker', - statModifiers: { - StatType.intelligence: 2, - StatType.wis: 1, - }, + statModifiers: {StatType.intelligence: 2, StatType.wis: 1}, startingSkills: ['fireball'], classSkills: ['fireball', 'leak_drain', 'memory_corrupt'], passives: [ @@ -244,19 +202,14 @@ class ClassData { description: '마법 데미지 +10%', ), ], - restriction: EquipmentRestriction( - armorWeight: ArmorWeight.light, - ), + restriction: EquipmentRestriction(armorWeight: ArmorWeight.light), ); /// Type Caster: INT + CHA (스탯 합계: +3) static const typeCaster = ClassTraits( classId: 'type_caster', name: 'Type Caster', - statModifiers: { - StatType.intelligence: 2, - StatType.cha: 1, - }, + statModifiers: {StatType.intelligence: 2, StatType.cha: 1}, startingSkills: ['fireball'], classSkills: ['fireball', 'type_coercion', 'cast_spell'], passives: [ @@ -266,19 +219,14 @@ class ClassData { description: '마법 데미지 +10%', ), ], - restriction: EquipmentRestriction( - armorWeight: ArmorWeight.light, - ), + restriction: EquipmentRestriction(armorWeight: ArmorWeight.light), ); /// DevOps Shaman: CON + INT (스탯 합계: +3) static const devOpsShaman = ClassTraits( classId: 'devops_shaman', name: 'DevOps Shaman', - statModifiers: { - StatType.con: 1, - StatType.intelligence: 2, - }, + statModifiers: {StatType.con: 1, StatType.intelligence: 2}, startingSkills: ['fireball'], classSkills: ['fireball', 'deploy_strike', 'ci_cd_flow'], passives: [ @@ -293,9 +241,7 @@ class ClassData { description: 'HP +10%', ), ], - restriction: EquipmentRestriction( - armorWeight: ArmorWeight.light, - ), + restriction: EquipmentRestriction(armorWeight: ArmorWeight.light), ); // ========================================================================== @@ -306,10 +252,7 @@ class ClassData { static const refactorMonk = ClassTraits( classId: 'refactor_monk', name: 'Refactor Monk', - statModifiers: { - StatType.dex: 2, - StatType.con: 1, - }, + statModifiers: {StatType.dex: 2, StatType.con: 1}, startingSkills: ['flurry'], classSkills: ['flurry', 'clean_code_strike', 'refactor_combo'], passives: [ @@ -324,19 +267,14 @@ class ClassData { description: '연속 공격', ), ], - restriction: EquipmentRestriction( - armorWeight: ArmorWeight.light, - ), + restriction: EquipmentRestriction(armorWeight: ArmorWeight.light), ); /// Pointer Assassin: DEX + STR (스탯 합계: +3) static const pointerAssassin = ClassTraits( classId: 'pointer_assassin', name: 'Pointer Assassin', - statModifiers: { - StatType.dex: 2, - StatType.str: 1, - }, + statModifiers: {StatType.dex: 2, StatType.str: 1}, startingSkills: ['backstab'], classSkills: ['backstab', 'null_strike', 'dereference_kill'], passives: [ @@ -351,19 +289,14 @@ class ClassData { description: '첫 공격 1.5배', ), ], - restriction: EquipmentRestriction( - armorWeight: ArmorWeight.light, - ), + restriction: EquipmentRestriction(armorWeight: ArmorWeight.light), ); /// Callback Samurai: DEX + STR (스탯 합계: +3) static const callbackSamurai = ClassTraits( classId: 'callback_samurai', name: 'Callback Samurai', - statModifiers: { - StatType.dex: 2, - StatType.str: 1, - }, + statModifiers: {StatType.dex: 2, StatType.str: 1}, startingSkills: ['power_strike'], classSkills: ['power_strike', 'async_slash', 'promise_blade'], passives: [ @@ -385,10 +318,7 @@ class ClassData { static const testerJester = ClassTraits( classId: 'tester_jester', name: 'Tester Jester', - statModifiers: { - StatType.dex: 2, - StatType.cha: 1, - }, + statModifiers: {StatType.dex: 2, StatType.cha: 1}, startingSkills: ['flurry'], classSkills: ['flurry', 'mock_strike', 'assert_fail'], passives: [ @@ -403,9 +333,7 @@ class ClassData { description: '크리티컬 +5%', ), ], - restriction: EquipmentRestriction( - armorWeight: ArmorWeight.light, - ), + restriction: EquipmentRestriction(armorWeight: ArmorWeight.light), ); // ========================================================================== @@ -416,10 +344,7 @@ class ClassData { static const exceptionHandler = ClassTraits( classId: 'exception_handler', name: 'Exception Handler', - statModifiers: { - StatType.wis: 2, - StatType.intelligence: 1, - }, + statModifiers: {StatType.wis: 2, StatType.intelligence: 1}, startingSkills: ['heal'], classSkills: ['heal', 'try_catch', 'finally_heal'], passives: [ @@ -429,19 +354,14 @@ class ClassData { description: '회복력 +15%', ), ], - restriction: EquipmentRestriction( - armorWeight: ArmorWeight.light, - ), + restriction: EquipmentRestriction(armorWeight: ArmorWeight.light), ); /// Null Checker: WIS + INT (스탯 합계: +3) static const nullChecker = ClassTraits( classId: 'null_checker', name: 'Null Checker', - statModifiers: { - StatType.wis: 2, - StatType.intelligence: 1, - }, + statModifiers: {StatType.wis: 2, StatType.intelligence: 1}, startingSkills: ['heal'], classSkills: ['heal', 'null_guard', 'safe_call'], passives: [ @@ -456,9 +376,7 @@ class ClassData { description: '방어력 +5%', ), ], - restriction: EquipmentRestriction( - armorWeight: ArmorWeight.light, - ), + restriction: EquipmentRestriction(armorWeight: ArmorWeight.light), ); /// 모든 클래스 목록 (18개) diff --git a/lib/data/game_text_l10n.dart b/lib/data/game_text_l10n.dart index 298b326..f30d2a9 100644 --- a/lib/data/game_text_l10n.dart +++ b/lib/data/game_text_l10n.dart @@ -99,6 +99,362 @@ String taskSelling(String itemDescription) { return 'Selling $itemDescription'; } +// ============================================================================ +// 부활 시퀀스 메시지 +// ============================================================================ + +String get taskReturningToTown { + if (isKoreanLocale) return '마을로 귀환 중...'; + if (isJapaneseLocale) return '町に戻っている...'; + return 'Returning to town...'; +} + +String get taskRestockingAtShop { + if (isKoreanLocale) return '상점에서 장비 정비 중...'; + if (isJapaneseLocale) return 'ショップで装備を整備中...'; + return 'Restocking at shop...'; +} + +String get taskHeadingToHuntingGrounds { + if (isKoreanLocale) return '사냥터로 이동 중...'; + if (isJapaneseLocale) return '狩り場へ向かっている...'; + return 'Heading to hunting grounds...'; +} + +// ============================================================================ +// 사망 화면 메시지 +// ============================================================================ + +String get deathYouDied { + if (isKoreanLocale) return '사망'; + if (isJapaneseLocale) return '死亡'; + return 'YOU DIED'; +} + +String get deathSacrificedToResurrect { + if (isKoreanLocale) return '부활 대가로 희생됨'; + if (isJapaneseLocale) return '復活のために犠牲'; + return 'Sacrificed to Resurrect'; +} + +String get deathEquipment { + if (isKoreanLocale) return '장비'; + if (isJapaneseLocale) return '装備'; + return 'Equipment'; +} + +String get deathNoSacrificeNeeded { + if (isKoreanLocale) return '희생 없이 부활'; + if (isJapaneseLocale) return '犠牲なしで復活'; + return 'No sacrifice needed'; +} + +String get deathGoldRemaining { + if (isKoreanLocale) return '남은 골드'; + if (isJapaneseLocale) return '残りゴールド'; + return 'Gold Remaining'; +} + +String get deathResurrect { + if (isKoreanLocale) return '부활'; + if (isJapaneseLocale) return '復活'; + return 'Resurrect'; +} + +String get deathCombatLog { + if (isKoreanLocale) return '전투 기록'; + if (isJapaneseLocale) return '戦闘ログ'; + return 'Combat Log'; +} + +String deathKilledBy(String killerName) { + if (isKoreanLocale) return '$killerName에게 사망'; + if (isJapaneseLocale) return '$killerNameに倒された'; + return 'Killed by $killerName'; +} + +String get deathEnvironmentalHazard { + if (isKoreanLocale) return '환경 피해로 사망'; + if (isJapaneseLocale) return '環境ダメージで死亡'; + return 'Environmental hazard'; +} + +// ============================================================================ +// UI 일반 메시지 +// ============================================================================ + +String get uiNoPotions { + if (isKoreanLocale) return '포션 없음'; + if (isJapaneseLocale) return 'ポーションなし'; + return 'No potions'; +} + +String get uiTapToContinue { + if (isKoreanLocale) return '탭하여 계속'; + if (isJapaneseLocale) return 'タップして続行'; + return 'Tap to continue'; +} + +String get uiNoSkills { + if (isKoreanLocale) return '습득한 스킬이 없습니다'; + if (isJapaneseLocale) return 'スキルなし'; + return 'No skills'; +} + +String get uiNoBonusStats { + if (isKoreanLocale) return '추가 스탯 없음'; + if (isJapaneseLocale) return 'ボーナスステータスなし'; + return 'No bonus stats'; +} + +String get uiNoActiveBuffs { + if (isKoreanLocale) return '활성 버프 없음'; + if (isJapaneseLocale) return 'アクティブバフなし'; + return 'No active buffs'; +} + +String get uiReady { + if (isKoreanLocale) return '준비'; + if (isJapaneseLocale) return '準備完了'; + return 'Ready'; +} + +String get uiPotions { + if (isKoreanLocale) return '포션'; + if (isJapaneseLocale) return 'ポーション'; + return 'Potions'; +} + +String get uiBuffs { + if (isKoreanLocale) return '버프'; + if (isJapaneseLocale) return 'バフ'; + return 'Buffs'; +} + +// ============================================================================ +// 스탯 약어 +// ============================================================================ + +String get statStr { + if (isKoreanLocale) return '힘'; + if (isJapaneseLocale) return '筋力'; + return 'STR'; +} + +String get statCon { + if (isKoreanLocale) return '체력'; + if (isJapaneseLocale) return '耐久'; + return 'CON'; +} + +String get statDex { + if (isKoreanLocale) return '민첩'; + if (isJapaneseLocale) return '敏捷'; + return 'DEX'; +} + +String get statInt { + if (isKoreanLocale) return '지능'; + if (isJapaneseLocale) return '知力'; + return 'INT'; +} + +String get statWis { + if (isKoreanLocale) return '지혜'; + if (isJapaneseLocale) return '精神'; + return 'WIS'; +} + +String get statCha { + if (isKoreanLocale) return '매력'; + if (isJapaneseLocale) return '魅力'; + return 'CHA'; +} + +// ============================================================================ +// 패시브 능력 설명 +// ============================================================================ + +String passiveHpBonus(int percent) { + if (isKoreanLocale) return 'HP +$percent%'; + if (isJapaneseLocale) return 'HP +$percent%'; + return 'HP +$percent%'; +} + +String passiveMpBonus(int percent) { + if (isKoreanLocale) return 'MP +$percent%'; + if (isJapaneseLocale) return 'MP +$percent%'; + return 'MP +$percent%'; +} + +String passivePhysicalBonus(int percent) { + if (isKoreanLocale) return '물리 공격 +$percent%'; + if (isJapaneseLocale) return '物理攻撃 +$percent%'; + return 'Physical +$percent%'; +} + +String passiveDefenseBonus(int percent) { + if (isKoreanLocale) return '방어력 +$percent%'; + if (isJapaneseLocale) return '防御力 +$percent%'; + return 'Defense +$percent%'; +} + +String passiveMagicBonus(int percent) { + if (isKoreanLocale) return '마법 공격 +$percent%'; + if (isJapaneseLocale) return '魔法攻撃 +$percent%'; + return 'Magic +$percent%'; +} + +String passiveEvasionBonus(int percent) { + if (isKoreanLocale) return '회피율 +$percent%'; + if (isJapaneseLocale) return '回避率 +$percent%'; + return 'Evasion +$percent%'; +} + +String passiveCritBonus(int percent) { + if (isKoreanLocale) return '크리티컬 +$percent%'; + if (isJapaneseLocale) return 'クリティカル +$percent%'; + return 'Critical +$percent%'; +} + +String passiveHpRegen(int percent) { + if (isKoreanLocale) return '전투 후 HP $percent% 회복'; + if (isJapaneseLocale) return '戦闘後HP $percent%回復'; + return 'Recover $percent% HP after combat'; +} + +String passiveMpRegen(int percent) { + if (isKoreanLocale) return '전투 후 MP $percent% 회복'; + if (isJapaneseLocale) return '戦闘後MP $percent%回復'; + return 'Recover $percent% MP after combat'; +} + +// ============================================================================ +// 전투 로그 메시지 +// ============================================================================ + +String combatYouHit(String targetName, int damage) { + if (isKoreanLocale) return '$targetName에게 $damage 데미지'; + if (isJapaneseLocale) return '$targetNameに$damageダメージ'; + return 'You hit $targetName for $damage damage'; +} + +String combatYouEvaded(String targetName) { + if (isKoreanLocale) return '$targetName의 공격 회피!'; + if (isJapaneseLocale) return '$targetNameの攻撃を回避!'; + return 'You evaded $targetName\'s attack!'; +} + +String combatEvadedAttackFrom(String targetName) { + if (isKoreanLocale) return '$targetName의 공격 회피'; + if (isJapaneseLocale) return '$targetNameの攻撃を回避'; + return 'Evaded attack from $targetName'; +} + +String combatHealedFor(int amount) { + if (isKoreanLocale) return 'HP $amount 회복'; + if (isJapaneseLocale) return 'HP $amount回復'; + return 'Healed for $amount HP'; +} + +String combatCritical(int damage, String targetName) { + if (isKoreanLocale) return '크리티컬! $targetName에게 $damage 데미지!'; + if (isJapaneseLocale) return 'クリティカル! $targetNameに$damageダメージ!'; + return 'CRITICAL! $damage damage to $targetName!'; +} + +String combatMonsterHitsYou(String monsterName, int damage) { + if (isKoreanLocale) return '$monsterName이(가) $damage 데미지'; + if (isJapaneseLocale) return '$monsterNameが$damageダメージを与えた'; + return '$monsterName hits you for $damage damage'; +} + +String combatMonsterEvaded(String monsterName) { + if (isKoreanLocale) return '$monsterName이(가) 공격 회피!'; + if (isJapaneseLocale) return '$monsterNameが攻撃を回避!'; + return '$monsterName evaded your attack!'; +} + +String combatBlocked(int damage) { + if (isKoreanLocale) return '방어! $damage 데미지로 감소'; + if (isJapaneseLocale) return 'ブロック! $damageダメージに軽減'; + return 'Blocked! Reduced to $damage damage'; +} + +String combatParried(int damage) { + if (isKoreanLocale) return '패리! $damage 데미지로 감소'; + if (isJapaneseLocale) return 'パリィ! $damageダメージに軽減'; + return 'Parried! Reduced to $damage damage'; +} + +// 스킬 관련 전투 메시지 +String combatSkillCritical(String skillName, int damage) { + if (isKoreanLocale) return '크리티컬 $skillName! $damage 데미지!'; + if (isJapaneseLocale) return 'クリティカル$skillName! $damageダメージ!'; + return 'CRITICAL $skillName! $damage damage!'; +} + +String combatSkillDamage(String skillName, int damage) { + if (isKoreanLocale) return '$skillName: $damage 데미지'; + if (isJapaneseLocale) return '$skillName: $damageダメージ'; + return '$skillName: $damage damage'; +} + +String combatSkillHeal(String skillName, int amount) { + if (isKoreanLocale) return '$skillName: +$amount HP'; + if (isJapaneseLocale) return '$skillName: +$amount HP'; + return '$skillName: +$amount HP'; +} + +String get uiHeal { + if (isKoreanLocale) return '힐'; + if (isJapaneseLocale) return 'ヒール'; + return 'Heal'; +} + +String combatBuffActivated(String skillName) { + if (isKoreanLocale) return '$skillName 발동!'; + if (isJapaneseLocale) return '$skillName 発動!'; + return '$skillName activated!'; +} + +String combatDotTick(String skillName, int damage) { + if (isKoreanLocale) return '$skillName: $damage 지속 데미지'; + if (isJapaneseLocale) return '$skillName: $damage 継続ダメージ'; + return '$skillName ticks for $damage damage'; +} + +String combatPotionUsed(String potionName, int amount, String statName) { + if (isKoreanLocale) return '$potionName: +$amount $statName'; + if (isJapaneseLocale) return '$potionName: +$amount $statName'; + return '$potionName: +$amount $statName'; +} + +String combatPotionDrop(String potionName) { + if (isKoreanLocale) return '획득: $potionName'; + if (isJapaneseLocale) return '獲得: $potionName'; + return 'Dropped: $potionName'; +} + +// 사망 화면 전투 로그 (death overlay) +String combatBlockedAttack(String monsterName, int reducedDamage) { + if (isKoreanLocale) return '$monsterName의 공격 방어 ($reducedDamage 감소)'; + if (isJapaneseLocale) return '$monsterNameの攻撃を防御 ($reducedDamage軽減)'; + return 'Blocked $monsterName\'s attack ($reducedDamage reduced)'; +} + +String combatParriedAttack(String monsterName, int reducedDamage) { + if (isKoreanLocale) return '$monsterName의 공격 패리 ($reducedDamage 감소)'; + if (isJapaneseLocale) return '$monsterNameの攻撃をパリィ ($reducedDamage軽減)'; + return 'Parried $monsterName\'s attack ($reducedDamage reduced)'; +} + +String get deathSelfInflicted { + if (isKoreanLocale) return '자해 데미지로 사망'; + if (isJapaneseLocale) return '自傷ダメージで死亡'; + return 'Self-inflicted damage'; +} + // ============================================================================ // 퀘스트 캡션 // ============================================================================ @@ -572,7 +928,8 @@ String translateImpressiveTitle(String englishName) { /// 특수 아이템 이름 번역 String translateSpecial(String englishName) { if (isKoreanLocale) return specialTranslationsKo[englishName] ?? englishName; - if (isJapaneseLocale) return specialTranslationsJa[englishName] ?? englishName; + if (isJapaneseLocale) + return specialTranslationsJa[englishName] ?? englishName; return englishName; } @@ -627,14 +984,16 @@ String translateBoringItem(String englishName) { /// 예: "Golden Iterator" → "황금 이터레이터" / "黄金のイテレーター" String translateInterestingItem(String attrib, String special) { if (isKoreanLocale) { - final translatedAttrib = itemAttribTranslationsKo[attrib] ?? + final translatedAttrib = + itemAttribTranslationsKo[attrib] ?? additionalItemAttribTranslationsKo[attrib] ?? attrib; final translatedSpecial = specialTranslationsKo[special] ?? special; return '$translatedAttrib $translatedSpecial'; } if (isJapaneseLocale) { - final translatedAttrib = itemAttribTranslationsJa[attrib] ?? + final translatedAttrib = + itemAttribTranslationsJa[attrib] ?? additionalItemAttribTranslationsJa[attrib] ?? attrib; final translatedSpecial = specialTranslationsJa[special] ?? special; @@ -643,48 +1002,419 @@ String translateInterestingItem(String attrib, String special) { return '$attrib $special'; } +/// 아이템 이름 문자열 전체 번역 (판매, 로그 등에 사용) +/// 예: "Golden Iterator of Compilation" → "컴파일의 황금 이터레이터" +/// 예: "Syntax Error fragment" → "구문 오류 조각" +String translateItemNameL10n(String itemString) { + if (!isKoreanLocale && !isJapaneseLocale) return itemString; + if (itemString.isEmpty) return itemString; + + // 1. specialItem 형식: "Attrib Special of ItemOf" + final ofMatch = RegExp(r'^(.+)\s+of\s+(.+)$').firstMatch(itemString); + if (ofMatch != null) { + final beforeOf = ofMatch.group(1)!; + final afterOf = ofMatch.group(2)!; + + // afterOf가 itemOfs 목록에 있는지 확인 + final itemOfKo = + itemOfsTranslationsKo[afterOf] ?? + additionalItemOfsTranslationsKo[afterOf]; + final itemOfJa = + itemOfsTranslationsJa[afterOf] ?? + additionalItemOfsTranslationsJa[afterOf]; + + if (itemOfKo != null || itemOfJa != null) { + // beforeOf를 interestingItem으로 분리 (attrib + special) + final beforeWords = beforeOf.split(' '); + if (beforeWords.length >= 2) { + final attrib = beforeWords[0]; + final special = beforeWords.sublist(1).join(' '); + final translatedBefore = translateInterestingItem(attrib, special); + if (isKoreanLocale) { + return '$itemOfKo의 $translatedBefore'; + } else if (isJapaneseLocale) { + return '$itemOfJaの$translatedBefore'; + } + } + } + } + + // 2. 몬스터 드롭 형식: "{monster} {drop}" (예: "syntax error fragment") + final words = itemString.split(' '); + if (words.length >= 2) { + // 마지막 단어가 드롭 아이템인지 확인 + final lastWord = words.last.toLowerCase(); + final dropKo = + dropItemTranslationsKo[lastWord] ?? + additionalDropTranslationsKo[lastWord]; + final dropJa = + dropItemTranslationsJa[lastWord] ?? + additionalDropTranslationsJa[lastWord]; + + if (dropKo != null || dropJa != null) { + // 앞 부분은 몬스터 이름 + final monsterPart = words.sublist(0, words.length - 1).join(' '); + final translatedMonster = translateMonster(monsterPart); + if (isKoreanLocale && dropKo != null) { + return '$translatedMonster $dropKo'; + } else if (isJapaneseLocale && dropJa != null) { + return '$translatedMonsterの$dropJa'; + } + } + + // 3. interestingItem 형식: "Attrib Special" (2단어) + if (words.length == 2) { + return translateInterestingItem(words[0], words[1]); + } + } + + // 4. 단일 단어 - boringItem 번역 시도 + return translateBoringItem(itemString); +} + // ============================================================================ // 스토리/시네마틱 번역 함수 (Story/Cinematic Translation Functions) // ============================================================================ /// Act 제목 번역 String translateActTitle(String englishTitle) { - if (isKoreanLocale) return actTitleTranslationsKo[englishTitle] ?? englishTitle; - if (isJapaneseLocale) return actTitleTranslationsJa[englishTitle] ?? englishTitle; + if (isKoreanLocale) + return actTitleTranslationsKo[englishTitle] ?? englishTitle; + if (isJapaneseLocale) + return actTitleTranslationsJa[englishTitle] ?? englishTitle; return englishTitle; } /// Act 보스 이름 번역 String translateActBoss(String englishBoss) { if (isKoreanLocale) return actBossTranslationsKo[englishBoss] ?? englishBoss; - if (isJapaneseLocale) return actBossTranslationsJa[englishBoss] ?? englishBoss; + if (isJapaneseLocale) + return actBossTranslationsJa[englishBoss] ?? englishBoss; return englishBoss; } /// Act 퀘스트 번역 String translateActQuest(String englishQuest) { - if (isKoreanLocale) return actQuestTranslationsKo[englishQuest] ?? englishQuest; - if (isJapaneseLocale) return actQuestTranslationsJa[englishQuest] ?? englishQuest; + if (isKoreanLocale) + return actQuestTranslationsKo[englishQuest] ?? englishQuest; + if (isJapaneseLocale) + return actQuestTranslationsJa[englishQuest] ?? englishQuest; return englishQuest; } /// 시네마틱 텍스트 번역 String translateCinematic(String englishText) { - if (isKoreanLocale) return cinematicTranslationsKo[englishText] ?? englishText; - if (isJapaneseLocale) return cinematicTranslationsJa[englishText] ?? englishText; + if (isKoreanLocale) + return cinematicTranslationsKo[englishText] ?? englishText; + if (isJapaneseLocale) + return cinematicTranslationsJa[englishText] ?? englishText; return englishText; } /// 지역 이름 번역 String translateLocation(String englishLocation) { - if (isKoreanLocale) return locationTranslationsKo[englishLocation] ?? englishLocation; - if (isJapaneseLocale) return locationTranslationsJa[englishLocation] ?? englishLocation; + if (isKoreanLocale) + return locationTranslationsKo[englishLocation] ?? englishLocation; + if (isJapaneseLocale) + return locationTranslationsJa[englishLocation] ?? englishLocation; return englishLocation; } /// 세력/조직 이름 번역 String translateFaction(String englishFaction) { - if (isKoreanLocale) return factionTranslationsKo[englishFaction] ?? englishFaction; - if (isJapaneseLocale) return factionTranslationsJa[englishFaction] ?? englishFaction; + if (isKoreanLocale) + return factionTranslationsKo[englishFaction] ?? englishFaction; + if (isJapaneseLocale) + return factionTranslationsJa[englishFaction] ?? englishFaction; return englishFaction; } + +// ============================================================================ +// 프론트 화면 텍스트 +// ============================================================================ + +String get uiHallOfFame { + if (isKoreanLocale) return '명예의 전당'; + if (isJapaneseLocale) return '栄誉の殿堂'; + return 'Hall of Fame'; +} + +String get frontDescription { + if (isKoreanLocale) return 'Flutter로 재구축된 오프라인 Progress Quest (PQ 6.4)'; + if (isJapaneseLocale) return 'Flutterで再構築されたオフラインProgress Quest (PQ 6.4)'; + return 'Offline Progress Quest (PQ 6.4) rebuilt with Flutter.'; +} + +String get frontTodayFocus { + if (isKoreanLocale) return '오늘의 중점'; + if (isJapaneseLocale) return '今日のフォーカス'; + return "Today's focus"; +} + +// ============================================================================ +// 명예의 전당 화면 텍스트 +// ============================================================================ + +String get hofNoHeroes { + if (isKoreanLocale) return '영웅이 아직 없습니다'; + if (isJapaneseLocale) return 'まだ英雄がいません'; + return 'No heroes yet'; +} + +String get hofDefeatGlitchGod { + if (isKoreanLocale) return '글리치 신을 처치하여 전설을 남기세요!'; + if (isJapaneseLocale) return 'グリッチゴッドを倒して伝説を刻もう!'; + return 'Defeat the Glitch God to enshrine your legend!'; +} + +String get hofVictory { + if (isKoreanLocale) return '승리!'; + if (isJapaneseLocale) return '勝利!'; + return 'VICTORY!'; +} + +String get hofDefeatedGlitchGod { + if (isKoreanLocale) return '글리치 신을 처치했습니다!'; + if (isJapaneseLocale) return 'グリッチゴッドを倒しました!'; + return 'You have defeated the Glitch God!'; +} + +String get hofLegendEnshrined { + if (isKoreanLocale) return '당신의 전설이 명예의 전당에 기록되었습니다!'; + if (isJapaneseLocale) return 'あなたの伝説が栄誉の殿堂に刻まれました!'; + return 'Your legend has been enshrined in the Hall of Fame!'; +} + +String get hofViewHallOfFame { + if (isKoreanLocale) return '명예의 전당 보기'; + if (isJapaneseLocale) return '栄誉の殿堂を見る'; + return 'View Hall of Fame'; +} + +String get hofNewGame { + if (isKoreanLocale) return '새 게임'; + if (isJapaneseLocale) return '新しいゲーム'; + return 'New Game'; +} + +String get hofLevel { + if (isKoreanLocale) return '레벨'; + if (isJapaneseLocale) return 'レベル'; + return 'Level'; +} + +String get hofTime { + if (isKoreanLocale) return '시간'; + if (isJapaneseLocale) return '時間'; + return 'Time'; +} + +String get hofDeaths { + if (isKoreanLocale) return '사망'; + if (isJapaneseLocale) return '死亡'; + return 'Deaths'; +} + +String get hofQuests { + if (isKoreanLocale) return '퀘스트'; + if (isJapaneseLocale) return 'クエスト'; + return 'Quests'; +} + +String uiLevel(int level) { + if (isKoreanLocale) return 'Lv.$level'; + if (isJapaneseLocale) return 'Lv.$level'; + return 'Lv.$level'; +} + +// ============================================================================ +// 시네마틱 뷰 텍스트 +// ============================================================================ + +String get uiSkip { + if (isKoreanLocale) return '건너뛰기'; + if (isJapaneseLocale) return 'スキップ'; + return 'SKIP'; +} + +// ============================================================================ +// 게임 플레이 화면 텍스트 +// ============================================================================ + +String get uiLevelUp { + if (isKoreanLocale) return '레벨 업!'; + if (isJapaneseLocale) return 'レベルアップ!'; + return 'Level Up!'; +} + +String uiQuestComplete(String questName) { + if (isKoreanLocale) return '퀘스트 완료: $questName'; + if (isJapaneseLocale) return 'クエスト完了: $questName'; + return 'Quest Complete: $questName'; +} + +// ============================================================================ +// 장비 패널 텍스트 +// ============================================================================ + +String get uiEquipmentScore { + if (isKoreanLocale) return '장비 점수'; + if (isJapaneseLocale) return '装備スコア'; + return 'Equipment Score'; +} + +String get uiEmpty { + if (isKoreanLocale) return '(비어있음)'; + if (isJapaneseLocale) return '(空)'; + return '(empty)'; +} + +String uiWeight(int weight) { + if (isKoreanLocale) return '무게 $weight'; + if (isJapaneseLocale) return '重量 $weight'; + return 'Wt.$weight'; +} + +// 장비 슬롯 이름 +String get slotWeapon { + if (isKoreanLocale) return '무기'; + if (isJapaneseLocale) return '武器'; + return 'Weapon'; +} + +String get slotShield { + if (isKoreanLocale) return '방패'; + if (isJapaneseLocale) return '盾'; + return 'Shield'; +} + +String get slotHelm { + if (isKoreanLocale) return '투구'; + if (isJapaneseLocale) return '兜'; + return 'Helm'; +} + +String get slotHauberk { + if (isKoreanLocale) return '갑옷'; + if (isJapaneseLocale) return '鎧'; + return 'Hauberk'; +} + +String get slotBrassairts { + if (isKoreanLocale) return '상완갑'; + if (isJapaneseLocale) return '上腕甲'; + return 'Brassairts'; +} + +String get slotVambraces { + if (isKoreanLocale) return '전완갑'; + if (isJapaneseLocale) return '前腕甲'; + return 'Vambraces'; +} + +String get slotGauntlets { + if (isKoreanLocale) return '건틀릿'; + if (isJapaneseLocale) return 'ガントレット'; + return 'Gauntlets'; +} + +String get slotGambeson { + if (isKoreanLocale) return '패딩'; + if (isJapaneseLocale) return 'パッデッドアーマー'; + return 'Gambeson'; +} + +String get slotCuisses { + if (isKoreanLocale) return '허벅지갑'; + if (isJapaneseLocale) return '腿当て'; + return 'Cuisses'; +} + +String get slotGreaves { + if (isKoreanLocale) return '정강이갑'; + if (isJapaneseLocale) return '脛当て'; + return 'Greaves'; +} + +String get slotSollerets { + if (isKoreanLocale) return '철제부츠'; + if (isJapaneseLocale) return '鉄靴'; + return 'Sollerets'; +} + +// 스탯 약어 (장비용) +String get statAtk { + if (isKoreanLocale) return '공격'; + if (isJapaneseLocale) return '攻撃'; + return 'ATK'; +} + +String get statMAtk { + if (isKoreanLocale) return '마공'; + if (isJapaneseLocale) return '魔攻'; + return 'MATK'; +} + +String get statCri { + if (isKoreanLocale) return '치명'; + if (isJapaneseLocale) return 'クリ'; + return 'CRI'; +} + +String get statParry { + if (isKoreanLocale) return '패리'; + if (isJapaneseLocale) return 'パリィ'; + return 'PARRY'; +} + +String get statDef { + if (isKoreanLocale) return '방어'; + if (isJapaneseLocale) return '防御'; + return 'DEF'; +} + +String get statMDef { + if (isKoreanLocale) return '마방'; + if (isJapaneseLocale) return '魔防'; + return 'MDEF'; +} + +String get statBlock { + if (isKoreanLocale) return '블록'; + if (isJapaneseLocale) return 'ブロック'; + return 'BLOCK'; +} + +String get statEva { + if (isKoreanLocale) return '회피'; + if (isJapaneseLocale) return '回避'; + return 'EVA'; +} + +String get statHp { + if (isKoreanLocale) return 'HP'; + if (isJapaneseLocale) return 'HP'; + return 'HP'; +} + +String get statMp { + if (isKoreanLocale) return 'MP'; + if (isJapaneseLocale) return 'MP'; + return 'MP'; +} + +String get statSpeed { + if (isKoreanLocale) return '속도'; + if (isJapaneseLocale) return '速度'; + return 'SPEED'; +} + +// ============================================================================ +// 스킬 패널 텍스트 +// ============================================================================ + +String get uiDot { + if (isKoreanLocale) return '지속'; + if (isJapaneseLocale) return 'DOT'; + return 'DOT'; +} diff --git a/lib/data/game_translations_ja.dart b/lib/data/game_translations_ja.dart index d332e59..cbffd5e 100644 --- a/lib/data/game_translations_ja.dart +++ b/lib/data/game_translations_ja.dart @@ -1175,7 +1175,8 @@ const Map cinematicTranslationsJa = { // Act I: 覚醒 '=== ACT I: AWAKENING ===': '=== 第1幕: 覚醒 ===', 'You have proven yourself against the lesser bugs.': '下級バグとの戦いで実力を証明した。', - 'The Debugger Knights take notice of your potential.': 'デバッガー騎士団があなたの可能性に注目する。', + 'The Debugger Knights take notice of your potential.': + 'デバッガー騎士団があなたの可能性に注目する。', 'But a greater threat lurks in the Bug Nest...': 'しかしより大きな脅威がバグの巣に潜んでいる…', 'The Syntax Error Dragon awaits.': '構文エラードラゴンが待ち構えている。', @@ -1217,7 +1218,8 @@ const Map cinematicTranslationsJa = { 'The Glitch God falls. The corruption fades.': 'グリッチゴッドが倒れた。破損が消えていく。', 'System Reboot initiated...': 'システム再起動開始…', 'Peace returns to the Digital Realm.': 'デジタル世界に平和が戻った。', - 'Your legend will be compiled into the eternal logs.': 'あなたの伝説は永遠のログにコンパイルされるだろう。', + 'Your legend will be compiled into the eternal logs.': + 'あなたの伝説は永遠のログにコンパイルされるだろう。', 'THE END': '完', '...or is it?': '…本当に?', }; @@ -1485,37 +1487,37 @@ const Map additionalItemOfsTranslationsJa = { /// すべてのモンスター翻訳を統合して返す Map get allMonsterTranslationsJa => { - ...monsterTranslationsJa, - ...advancedMonsterTranslationsJa, - }; + ...monsterTranslationsJa, + ...advancedMonsterTranslationsJa, +}; /// すべてのアイテム属性翻訳を統合して返す Map get allItemAttribTranslationsJa => { - ...itemAttribTranslationsJa, - ...additionalItemAttribTranslationsJa, - }; + ...itemAttribTranslationsJa, + ...additionalItemAttribTranslationsJa, +}; /// すべてのアイテム接尾辞(~の)翻訳を統合して返す Map get allItemOfsTranslationsJa => { - ...itemOfsTranslationsJa, - ...additionalItemOfsTranslationsJa, - }; + ...itemOfsTranslationsJa, + ...additionalItemOfsTranslationsJa, +}; /// すべてのドロップアイテム翻訳を統合して返す Map get allDropTranslationsJa => { - ...boringItemTranslationsJa, - ...dropItemTranslationsJa, - ...additionalDropTranslationsJa, - }; + ...boringItemTranslationsJa, + ...dropItemTranslationsJa, + ...additionalDropTranslationsJa, +}; /// すべての鎧翻訳を統合して返す Map get allArmorTranslationsJa => { - ...armorTranslationsJa, - ...additionalArmorTranslationsJa, - }; + ...armorTranslationsJa, + ...additionalArmorTranslationsJa, +}; /// すべての盾翻訳を統合して返す Map get allShieldTranslationsJa => { - ...shieldTranslationsJa, - ...additionalShieldTranslationsJa, - }; + ...shieldTranslationsJa, + ...additionalShieldTranslationsJa, +}; diff --git a/lib/data/game_translations_ko.dart b/lib/data/game_translations_ko.dart index 3841aeb..1ae0d90 100644 --- a/lib/data/game_translations_ko.dart +++ b/lib/data/game_translations_ko.dart @@ -1165,16 +1165,13 @@ const Map actQuestTranslationsKo = { /// 시네마틱 텍스트 한국어 번역 const Map cinematicTranslationsKo = { // 프롤로그 - 'In the beginning, there was only the Void...': - '태초에, 오직 공허(Void)만이 존재했다...', + 'In the beginning, there was only the Void...': '태초에, 오직 공허(Void)만이 존재했다...', 'Then came the First Commit, and Light filled the Codebase.': '그리고 첫 번째 커밋이 도래하여, 빛이 코드베이스를 가득 채웠다.', - 'The Code God spoke: "Let there be Functions."': - '코드의 신이 말씀하셨다: "함수가 있으라."', + 'The Code God spoke: "Let there be Functions."': '코드의 신이 말씀하셨다: "함수가 있으라."', 'And so the Digital Realm was born...': '그리하여 디지털 세계가 탄생하였다...', 'But from the shadows emerged the Glitch.': '그러나 어둠 속에서 글리치가 출현했다.', - 'Now, a new hero awakens to defend the Code.': - '이제, 코드를 수호할 새로운 영웅이 깨어난다.', + 'Now, a new hero awakens to defend the Code.': '이제, 코드를 수호할 새로운 영웅이 깨어난다.', 'Your journey begins...': '당신의 여정이 시작된다...', // Act I: 각성 @@ -1191,11 +1188,9 @@ const Map cinematicTranslationsKo = { '=== ACT II: GROWTH ===': '=== 제2막: 성장 ===', 'With the Dragon slain, you join the Debugger Knights.': '드래곤을 처치하고, 당신은 디버거 기사단에 입단한다.', - 'The Corrupted Network spreads its infection...': - '손상된 네트워크가 감염을 퍼뜨리고 있다...', + 'The Corrupted Network spreads its infection...': '손상된 네트워크가 감염을 퍼뜨리고 있다...', 'A traitor among the Knights is revealed!': '기사단 내 배신자가 드러났다!', - 'The Memory Leak Hydra threatens all data.': - '메모리 누수 히드라가 모든 데이터를 위협한다.', + 'The Memory Leak Hydra threatens all data.': '메모리 누수 히드라가 모든 데이터를 위협한다.', 'You must stop the corruption before it consumes everything.': '모든 것을 삼키기 전에 손상을 멈춰야 한다.', @@ -1206,20 +1201,16 @@ const Map cinematicTranslationsKo = { '고대 컴파일러가 당신에게 시련을 건넨다.', 'A companion falls... their sacrifice not in vain.': '동료가 쓰러진다... 그들의 희생은 헛되지 않으리.', - 'The Buffer Overflow Titan guards the gate.': - '버퍼 오버플로우 타이탄이 문을 지키고 있다.', + 'The Buffer Overflow Titan guards the gate.': '버퍼 오버플로우 타이탄이 문을 지키고 있다.', 'Only through great sacrifice can you proceed.': '오직 큰 희생을 통해서만 앞으로 나아갈 수 있다.', // Act IV: 결전 '=== ACT IV: CONFRONTATION ===': '=== 제4막: 결전 ===', - "The Glitch God's Citadel looms before you.": - '글리치 신의 성채가 눈앞에 어렴풋이 보인다.', - 'Former enemies unite against the common threat.': - '이전의 적들이 공동의 위협에 맞서 연합한다.', + "The Glitch God's Citadel looms before you.": '글리치 신의 성채가 눈앞에 어렴풋이 보인다.', + 'Former enemies unite against the common threat.': '이전의 적들이 공동의 위협에 맞서 연합한다.', 'The Final Alliance is forged.': '최후의 동맹이 결성되었다.', - 'The Kernel Panic Archon blocks your path.': - '커널 패닉 아르콘이 당신의 길을 막는다.', + 'The Kernel Panic Archon blocks your path.': '커널 패닉 아르콘이 당신의 길을 막는다.', 'One final battle before the end...': '종말 전의 마지막 전투...', // Act V: 종말 @@ -1227,13 +1218,11 @@ const Map cinematicTranslationsKo = { 'The Glitch God reveals its true form.': '글리치 신이 진정한 모습을 드러낸다.', 'Reality itself begins to corrupt.': '현실 그 자체가 손상되기 시작한다.', 'All hope rests upon your shoulders.': '모든 희망이 당신의 어깨에 달려 있다.', - 'The final battle for the Codebase begins!': - '코드베이스를 위한 최후의 전투가 시작된다!', + 'The final battle for the Codebase begins!': '코드베이스를 위한 최후의 전투가 시작된다!', // 엔딩 '=== THE END ===': '=== 완결 ===', - 'The Glitch God falls. The corruption fades.': - '글리치 신이 쓰러진다. 손상이 사라진다.', + 'The Glitch God falls. The corruption fades.': '글리치 신이 쓰러진다. 손상이 사라진다.', 'System Reboot initiated...': '시스템 재부팅 시작...', 'Peace returns to the Digital Realm.': '디지털 세계에 평화가 돌아온다.', 'Your legend will be compiled into the eternal logs.': @@ -1505,37 +1494,37 @@ const Map additionalItemOfsTranslationsKo = { /// 모든 몬스터 번역을 통합하여 반환 Map get allMonsterTranslationsKo => { - ...monsterTranslationsKo, - ...advancedMonsterTranslationsKo, - }; + ...monsterTranslationsKo, + ...advancedMonsterTranslationsKo, +}; /// 모든 아이템 속성 번역을 통합하여 반환 Map get allItemAttribTranslationsKo => { - ...itemAttribTranslationsKo, - ...additionalItemAttribTranslationsKo, - }; + ...itemAttribTranslationsKo, + ...additionalItemAttribTranslationsKo, +}; /// 모든 아이템 접미사("~의") 번역을 통합하여 반환 Map get allItemOfsTranslationsKo => { - ...itemOfsTranslationsKo, - ...additionalItemOfsTranslationsKo, - }; + ...itemOfsTranslationsKo, + ...additionalItemOfsTranslationsKo, +}; /// 모든 드롭 아이템 번역을 통합하여 반환 Map get allDropTranslationsKo => { - ...boringItemTranslationsKo, - ...dropItemTranslationsKo, - ...additionalDropTranslationsKo, - }; + ...boringItemTranslationsKo, + ...dropItemTranslationsKo, + ...additionalDropTranslationsKo, +}; /// 모든 갑옷 번역을 통합하여 반환 Map get allArmorTranslationsKo => { - ...armorTranslationsKo, - ...additionalArmorTranslationsKo, - }; + ...armorTranslationsKo, + ...additionalArmorTranslationsKo, +}; /// 모든 방패 번역을 통합하여 반환 Map get allShieldTranslationsKo => { - ...shieldTranslationsKo, - ...additionalShieldTranslationsKo, - }; + ...shieldTranslationsKo, + ...additionalShieldTranslationsKo, +}; diff --git a/lib/data/skill_data.dart b/lib/data/skill_data.dart index f3a1d24..f3b2613 100644 --- a/lib/data/skill_data.dart +++ b/lib/data/skill_data.dart @@ -2,46 +2,50 @@ import 'package:askiineverdie/src/core/model/skill.dart'; /// 게임 내 스킬 정의 /// -/// 프로그래밍 테마에 맞춘 스킬 목록 +/// PQ 스펠 70개를 전투 스킬로 매핑 +/// 스펠 이름(영문)으로 스킬 조회 가능 class SkillData { SkillData._(); // ============================================================================ - // 공격 스킬 + // 공격 스킬 (Attack) - 18개 // ============================================================================ - /// Debug Strike - 기본 공격 스킬 - static const debugStrike = Skill( - id: 'debug_strike', - name: 'Debug Strike', + /// Stack Trace - 기본 공격 (빠른 쿨타임) + static const stackTrace = Skill( + id: 'stack_trace', + name: 'Stack Trace', type: SkillType.attack, mpCost: 10, - cooldownMs: 3000, // 3초 + cooldownMs: 3000, power: 15, damageMultiplier: 1.5, ); - /// Memory Leak - 방어력 감소 효과 - static const memoryLeak = Skill( - id: 'memory_leak', - name: 'Memory Leak', - type: SkillType.attack, - mpCost: 25, - cooldownMs: 8000, // 8초 - power: 25, - damageMultiplier: 2.5, - targetDefReduction: 0.2, // 적 방어력 -20% - ); - - /// Core Dump - 강력한 공격 + /// Core Dump - 중급 공격 static const coreDump = Skill( id: 'core_dump', name: 'Core Dump', type: SkillType.attack, - mpCost: 50, - cooldownMs: 20000, // 20초 - power: 40, - damageMultiplier: 4.0, + mpCost: 35, + cooldownMs: 12000, + power: 30, + damageMultiplier: 3.0, + ); + + /// Memory Dump - DOT 공격 + static const memoryDump = Skill( + id: 'memory_dump', + name: 'Memory Dump', + type: SkillType.attack, + mpCost: 25, + cooldownMs: 15000, + power: 0, + element: SkillElement.memory, + attackMode: AttackMode.dot, + baseDotDamage: 10, + baseDotDurationMs: 6000, + baseDotTickMs: 1000, ); /// Kernel Panic - 최강 공격 (자해 데미지) @@ -49,252 +53,1040 @@ class SkillData { id: 'kernel_panic', name: 'Kernel Panic', type: SkillType.attack, - mpCost: 100, - cooldownMs: 60000, // 60초 - power: 80, - damageMultiplier: 8.0, - selfDamagePercent: 0.1, // 자신 HP -10% + mpCost: 80, + cooldownMs: 45000, + power: 60, + damageMultiplier: 6.0, + selfDamagePercent: 0.1, ); - /// Stack Overflow - 중급 공격 - static const stackOverflow = Skill( - id: 'stack_overflow', - name: 'Stack Overflow', + /// Blue Screen - 강력 공격 (긴 쿨타임) + static const blueScreen = Skill( + id: 'blue_screen', + name: 'Blue Screen', + type: SkillType.attack, + mpCost: 60, + cooldownMs: 30000, + power: 50, + damageMultiplier: 5.0, + ); + + /// Inject Code - 방어 무시 공격 + static const injectCode = Skill( + id: 'inject_code', + name: 'Inject Code', + type: SkillType.attack, + mpCost: 40, + cooldownMs: 18000, + power: 35, + damageMultiplier: 2.5, + targetDefReduction: 0.5, + ); + + /// Spawn Shell - 3연타 공격 + static const spawnShell = Skill( + id: 'spawn_shell', + name: 'Spawn Shell', + type: SkillType.attack, + mpCost: 30, + cooldownMs: 10000, + power: 12, + damageMultiplier: 1.2, + hitCount: 3, + ); + + /// Thread Pool - 5연타 공격 + static const threadPool = Skill( + id: 'thread_pool', + name: 'Thread Pool', + type: SkillType.attack, + mpCost: 45, + cooldownMs: 15000, + power: 10, + damageMultiplier: 1.0, + hitCount: 5, + ); + + /// Exfiltrate Data - HP 흡수 공격 + static const exfiltrateData = Skill( + id: 'exfiltrate_data', + name: 'Exfiltrate Data', type: SkillType.attack, mpCost: 35, - cooldownMs: 12000, // 12초 + cooldownMs: 12000, + power: 25, + damageMultiplier: 2.0, + lifestealPercent: 0.3, + ); + + /// Fuzzing - 랜덤 데미지 공격 + static const fuzzing = Skill( + id: 'fuzzing', + name: 'Fuzzing', + type: SkillType.attack, + mpCost: 20, + cooldownMs: 8000, + power: 20, + damageMultiplier: 2.0, + element: SkillElement.chaos, + ); + + /// Chaos Monkey - 랜덤 효과 공격 + static const chaosMonkey = Skill( + id: 'chaos_monkey', + name: 'Chaos Monkey', + type: SkillType.attack, + mpCost: 50, + cooldownMs: 25000, + power: 40, + damageMultiplier: 3.5, + element: SkillElement.chaos, + ); + + /// Saga Pattern - 3회 연속 공격 + static const sagaPattern = Skill( + id: 'saga_pattern', + name: 'Saga Pattern', + type: SkillType.attack, + mpCost: 55, + cooldownMs: 20000, + power: 18, + damageMultiplier: 1.8, + hitCount: 3, + ); + + /// Event Store - 차지 공격 (DOT로 표현) + static const eventStore = Skill( + id: 'event_store', + name: 'Event Store', + type: SkillType.attack, + mpCost: 40, + cooldownMs: 18000, + power: 0, + element: SkillElement.logic, + attackMode: AttackMode.dot, + baseDotDamage: 20, + baseDotDurationMs: 3000, + baseDotTickMs: 3000, + ); + + /// Auto Scale - HP비례 공격 + static const autoScale = Skill( + id: 'auto_scale', + name: 'Auto Scale', + type: SkillType.attack, + mpCost: 45, + cooldownMs: 20000, power: 30, + damageMultiplier: 2.5, + ); + + /// Disassemble - 방어감소+공격 + static const disassemble = Skill( + id: 'disassemble', + name: 'Disassemble', + type: SkillType.attack, + mpCost: 30, + cooldownMs: 12000, + power: 22, + damageMultiplier: 2.0, + targetDefReduction: 0.3, + ); + + /// Decompile - 약점 공격 (높은 크리) + static const decompile = Skill( + id: 'decompile', + name: 'Decompile', + type: SkillType.attack, + mpCost: 25, + cooldownMs: 10000, + power: 20, + damageMultiplier: 2.2, + ); + + /// Canary Release - 테스트 공격 + static const canaryRelease = Skill( + id: 'canary_release', + name: 'Canary Release', + type: SkillType.attack, + mpCost: 15, + cooldownMs: 6000, + power: 12, + damageMultiplier: 1.3, + ); + + /// A/B Test - 이중 공격 + static const abTest = Skill( + id: 'ab_test', + name: 'A/B Test', + type: SkillType.attack, + mpCost: 35, + cooldownMs: 12000, + power: 15, + damageMultiplier: 1.5, + hitCount: 2, + ); + + /// Pivot Network - 네트워크 공격 + static const pivotNetwork = Skill( + id: 'pivot_network', + name: 'Pivot Network', + type: SkillType.attack, + mpCost: 30, + cooldownMs: 10000, + power: 25, + damageMultiplier: 2.2, + element: SkillElement.network, + ); + + /// Async Await - 딜레이 공격 + static const asyncAwait = Skill( + id: 'async_await', + name: 'Async Await', + type: SkillType.attack, + mpCost: 35, + cooldownMs: 14000, + power: 35, damageMultiplier: 3.0, ); - /// Null Pointer - 빠른 공격 - static const nullPointer = Skill( - id: 'null_pointer', - name: 'Null Pointer', - type: SkillType.attack, - mpCost: 15, - cooldownMs: 4000, // 4초 - power: 18, - damageMultiplier: 1.8, - ); - - // ============================================================================ - // DOT (지속 피해) 스킬 - // ============================================================================ - - /// Memory Corruption - 기본 DOT 스킬 - /// - /// INT → 틱당 데미지 보정, WIS → 틱 간격 보정 - static const memoryCorruption = Skill( - id: 'memory_corruption', - name: 'Memory Corruption', - type: SkillType.attack, - mpCost: 20, - cooldownMs: 10000, // 10초 - power: 0, - damageMultiplier: 0, - element: SkillElement.memory, - attackMode: AttackMode.dot, - baseDotDamage: 8, // 틱당 8 데미지 (INT 보정 전) - baseDotDurationMs: 6000, // 6초 지속 - baseDotTickMs: 1000, // 1초마다 틱 - ); - - /// Infinite Loop - 장시간 DOT - /// - /// 오래 지속되는 중급 DOT - static const infiniteLoop = Skill( - id: 'infinite_loop', - name: 'Infinite Loop', - type: SkillType.attack, - mpCost: 35, - cooldownMs: 18000, // 18초 - power: 0, - damageMultiplier: 0, - element: SkillElement.memory, - attackMode: AttackMode.dot, - baseDotDamage: 12, // 틱당 12 데미지 - baseDotDurationMs: 10000, // 10초 지속 - baseDotTickMs: 1000, // 1초마다 틱 - ); - - /// Thermal Throttle - 화염 DOT - /// - /// 빠른 틱, 짧은 지속시간 - static const thermalThrottle = Skill( - id: 'thermal_throttle', - name: 'Thermal Throttle', + /// Event Source - DOT 공격 + static const eventSource = Skill( + id: 'event_source', + name: 'Event Source', type: SkillType.attack, mpCost: 30, - cooldownMs: 12000, // 12초 + cooldownMs: 12000, power: 0, - damageMultiplier: 0, - element: SkillElement.fire, + element: SkillElement.memory, attackMode: AttackMode.dot, - baseDotDamage: 15, // 틱당 15 데미지 - baseDotDurationMs: 4000, // 4초 지속 - baseDotTickMs: 500, // 0.5초마다 틱 + baseDotDamage: 8, + baseDotDurationMs: 8000, + baseDotTickMs: 800, ); - /// Race Condition - 빠른 DOT - /// - /// 매우 빠른 틱의 번개 DOT - static const raceCondition = Skill( - id: 'race_condition', - name: 'Race Condition', + /// CQRS Split - 분리 공격 + static const cqrsSplit = Skill( + id: 'cqrs_split', + name: 'CQRS Split', type: SkillType.attack, - mpCost: 45, - cooldownMs: 15000, // 15초 - power: 0, - damageMultiplier: 0, - element: SkillElement.lightning, - attackMode: AttackMode.dot, - baseDotDamage: 6, // 틱당 6 데미지 - baseDotDurationMs: 5000, // 5초 지속 - baseDotTickMs: 300, // 0.3초마다 틱 - ); - - /// System32 Delete - 강력한 DOT - /// - /// 높은 틱 데미지의 공허 DOT - static const system32Delete = Skill( - id: 'system32_delete', - name: 'System32 Delete', - type: SkillType.attack, - mpCost: 70, - cooldownMs: 30000, // 30초 - power: 0, - damageMultiplier: 0, - element: SkillElement.voidElement, - attackMode: AttackMode.dot, - baseDotDamage: 25, // 틱당 25 데미지 - baseDotDurationMs: 8000, // 8초 지속 - baseDotTickMs: 1000, // 1초마다 틱 + mpCost: 40, + cooldownMs: 15000, + power: 20, + damageMultiplier: 2.0, + hitCount: 2, ); // ============================================================================ - // 회복 스킬 + // 회복 스킬 (Heal) - 12개 // ============================================================================ - /// Hot Reload - HP 회복 - static const hotReload = Skill( - id: 'hot_reload', - name: 'Hot Reload', - type: SkillType.heal, - mpCost: 20, - cooldownMs: 10000, // 10초 - power: 0, - healPercent: 0.3, // HP 30% 회복 - ); - - /// Garbage Collection - 대량 회복 + /// Garbage Collection - 기본 HP 회복 static const garbageCollection = Skill( id: 'garbage_collection', name: 'Garbage Collection', type: SkillType.heal, - mpCost: 45, - cooldownMs: 25000, // 25초 + mpCost: 25, + cooldownMs: 15000, power: 0, - healPercent: 0.5, // HP 50% 회복 + healPercent: 0.3, ); - /// Quick Fix - 빠른 소량 회복 - static const quickFix = Skill( - id: 'quick_fix', - name: 'Quick Fix', + /// Hot Reload - 즉시 회복 (짧은 쿨타임) + static const hotReload = Skill( + id: 'hot_reload', + name: 'Hot Reload', + type: SkillType.heal, + mpCost: 15, + cooldownMs: 8000, + power: 0, + healAmount: 30, + ); + + /// Rollback - HP 회복 + static const rollback = Skill( + id: 'rollback', + name: 'Rollback', + type: SkillType.heal, + mpCost: 30, + cooldownMs: 18000, + power: 0, + healPercent: 0.35, + ); + + /// Hotfix - 긴급 회복 (낮은 MP) + static const hotfix = Skill( + id: 'hotfix', + name: 'Hotfix', type: SkillType.heal, mpCost: 10, - cooldownMs: 5000, // 5초 + cooldownMs: 6000, power: 0, - healAmount: 20, // 고정 20 회복 + healAmount: 20, + ); + + /// Snapshot Restore - 대량 회복 (50% HP) + static const snapshotRestore = Skill( + id: 'snapshot_restore', + name: 'Snapshot Restore', + type: SkillType.heal, + mpCost: 50, + cooldownMs: 30000, + power: 0, + healPercent: 0.5, + ); + + /// Patch Binary - 회복+버프 (HP + DEF) + static const patchBinary = Skill( + id: 'patch_binary', + name: 'Patch Binary', + type: SkillType.heal, + mpCost: 35, + cooldownMs: 20000, + power: 0, + healPercent: 0.25, + buff: BuffEffect( + id: 'patch_binary_buff', + name: 'Patched', + durationMs: 8000, + defModifier: 0.2, + ), + ); + + /// Git Commit - HP 저장/회복 + static const gitCommit = Skill( + id: 'git_commit', + name: 'Git Commit', + type: SkillType.heal, + mpCost: 20, + cooldownMs: 12000, + power: 0, + healPercent: 0.2, + ); + + /// Git Push - HP 복원 + static const gitPush = Skill( + id: 'git_push', + name: 'Git Push', + type: SkillType.heal, + mpCost: 25, + cooldownMs: 15000, + power: 0, + healPercent: 0.25, + ); + + /// Connection Pool - MP 회복 + static const connectionPool = Skill( + id: 'connection_pool', + name: 'Connection Pool', + type: SkillType.heal, + mpCost: 0, + cooldownMs: 20000, + power: 0, + mpHealAmount: 30, + ); + + /// Load Balance - HP/MP 균등화 (HP 회복) + static const loadBalance = Skill( + id: 'load_balance', + name: 'Load Balance', + type: SkillType.heal, + mpCost: 20, + cooldownMs: 15000, + power: 0, + healPercent: 0.2, + mpHealAmount: 15, + ); + + /// Blue Green Deploy - HP/MP 스왑 + static const blueGreenDeploy = Skill( + id: 'blue_green_deploy', + name: 'Blue Green Deploy', + type: SkillType.heal, + mpCost: 30, + cooldownMs: 25000, + power: 0, + healPercent: 0.3, + ); + + /// Cache Invalidate - 디버프 해제 (클렌즈) + static const cacheInvalidate = Skill( + id: 'cache_invalidate', + name: 'Cache Invalidate', + type: SkillType.heal, + mpCost: 25, + cooldownMs: 18000, + power: 0, + healAmount: 15, ); // ============================================================================ - // 버프 스킬 + // 버프 스킬 (Buff) - 20개 // ============================================================================ - /// Safe Mode - 방어 버프 + /// Debug Mode - ATK +25% + static const debugMode = Skill( + id: 'debug_mode', + name: 'Debug Mode', + type: SkillType.buff, + mpCost: 20, + cooldownMs: 20000, + power: 0, + buff: BuffEffect( + id: 'debug_mode_buff', + name: 'Debug Mode', + durationMs: 10000, + atkModifier: 0.25, + ), + ); + + /// Safe Mode - DEF +30% static const safeMode = Skill( id: 'safe_mode', name: 'Safe Mode', type: SkillType.buff, - mpCost: 30, - cooldownMs: 30000, // 30초 + mpCost: 25, + cooldownMs: 25000, power: 0, buff: BuffEffect( id: 'safe_mode_buff', name: 'Safe Mode', - durationMs: 10000, // 10초 지속 - defModifier: 0.5, // 방어력 +50% + durationMs: 10000, + defModifier: 0.3, ), ); - /// Overclock - 공격 버프 - static const overclock = Skill( - id: 'overclock', - name: 'Overclock', + /// Memory Optimization - 전스탯 +10% + static const memoryOptimization = Skill( + id: 'memory_optimization', + name: 'Memory Optimization', type: SkillType.buff, - mpCost: 25, - cooldownMs: 25000, // 25초 + mpCost: 30, + cooldownMs: 30000, power: 0, buff: BuffEffect( - id: 'overclock_buff', - name: 'Overclock', - durationMs: 8000, // 8초 지속 - atkModifier: 0.4, // 공격력 +40% - criRateModifier: 0.1, // 크리티컬 +10% + id: 'memory_optimization_buff', + name: 'Optimized', + durationMs: 15000, + atkModifier: 0.1, + defModifier: 0.1, + criRateModifier: 0.05, + evasionModifier: 0.05, ), ); - /// Firewall - 회피 버프 - static const firewall = Skill( - id: 'firewall', - name: 'Firewall', + /// Breakpoint - 다음 공격 크리티컬 + static const breakpoint = Skill( + id: 'breakpoint', + name: 'Breakpoint', + type: SkillType.buff, + mpCost: 15, + cooldownMs: 12000, + power: 0, + buff: BuffEffect( + id: 'breakpoint_buff', + name: 'Breakpoint', + durationMs: 5000, + criRateModifier: 0.5, + ), + ); + + /// Watch Variable - 회피율 +20% + static const watchVariable = Skill( + id: 'watch_variable', + name: 'Watch Variable', + type: SkillType.buff, + mpCost: 18, + cooldownMs: 15000, + power: 0, + buff: BuffEffect( + id: 'watch_variable_buff', + name: 'Watching', + durationMs: 8000, + evasionModifier: 0.2, + ), + ); + + /// Step Into - 공격속도 +30% (ATK 버프로 표현) + static const stepInto = Skill( + id: 'step_into', + name: 'Step Into', + type: SkillType.buff, + mpCost: 15, + cooldownMs: 12000, + power: 0, + buff: BuffEffect( + id: 'step_into_buff', + name: 'Step Into', + durationMs: 6000, + atkModifier: 0.2, + ), + ); + + /// Profile Run - 크리율 +30% + static const profileRun = Skill( + id: 'profile_run', + name: 'Profile Run', type: SkillType.buff, mpCost: 20, - cooldownMs: 20000, // 20초 + cooldownMs: 18000, power: 0, buff: BuffEffect( - id: 'firewall_buff', - name: 'Firewall', - durationMs: 12000, // 12초 지속 - evasionModifier: 0.15, // 회피율 +15% - defModifier: 0.2, // 방어력 +20% + id: 'profile_run_buff', + name: 'Profiling', + durationMs: 8000, + criRateModifier: 0.3, ), ); + /// Benchmark - 데미지 +40% + static const benchmark = Skill( + id: 'benchmark', + name: 'Benchmark', + type: SkillType.buff, + mpCost: 25, + cooldownMs: 20000, + power: 0, + buff: BuffEffect( + id: 'benchmark_buff', + name: 'Benchmarking', + durationMs: 5000, + atkModifier: 0.4, + ), + ); + + /// Elevate Privilege - 전스탯 +20% + static const elevatePrivilege = Skill( + id: 'elevate_privilege', + name: 'Elevate Privilege', + type: SkillType.buff, + mpCost: 40, + cooldownMs: 35000, + power: 0, + buff: BuffEffect( + id: 'elevate_privilege_buff', + name: 'Elevated', + durationMs: 8000, + atkModifier: 0.2, + defModifier: 0.2, + criRateModifier: 0.1, + evasionModifier: 0.1, + ), + ); + + /// Scale Up - ATK +50% + static const scaleUp = Skill( + id: 'scale_up', + name: 'Scale Up', + type: SkillType.buff, + mpCost: 35, + cooldownMs: 30000, + power: 0, + buff: BuffEffect( + id: 'scale_up_buff', + name: 'Scaled Up', + durationMs: 6000, + atkModifier: 0.5, + ), + ); + + /// Failover - 치명타 방지 (DEF 증가로 표현) + static const failover = Skill( + id: 'failover', + name: 'Failover', + type: SkillType.buff, + mpCost: 30, + cooldownMs: 45000, + power: 0, + buff: BuffEffect( + id: 'failover_buff', + name: 'Failover Ready', + durationMs: 30000, + defModifier: 0.4, + ), + ); + + /// Containerize - 방어막 (DEF 증가) + static const containerize = Skill( + id: 'containerize', + name: 'Containerize', + type: SkillType.buff, + mpCost: 25, + cooldownMs: 20000, + power: 0, + buff: BuffEffect( + id: 'containerize_buff', + name: 'Containerized', + durationMs: 12000, + defModifier: 0.35, + ), + ); + + /// Orchestrate - 복합 버프 + static const orchestrate = Skill( + id: 'orchestrate', + name: 'Orchestrate', + type: SkillType.buff, + mpCost: 45, + cooldownMs: 40000, + power: 0, + buff: BuffEffect( + id: 'orchestrate_buff', + name: 'Orchestrated', + durationMs: 10000, + atkModifier: 0.15, + defModifier: 0.15, + criRateModifier: 0.1, + ), + ); + + /// Promise Resolve - 버프 즉시 발동 + static const promiseResolve = Skill( + id: 'promise_resolve', + name: 'Promise Resolve', + type: SkillType.buff, + mpCost: 20, + cooldownMs: 15000, + power: 0, + buff: BuffEffect( + id: 'promise_resolve_buff', + name: 'Resolved', + durationMs: 8000, + atkModifier: 0.25, + ), + ); + + /// Feature Toggle - 버프 on/off + static const featureToggle = Skill( + id: 'feature_toggle', + name: 'Feature Toggle', + type: SkillType.buff, + mpCost: 15, + cooldownMs: 10000, + power: 0, + buff: BuffEffect( + id: 'feature_toggle_buff', + name: 'Toggled', + durationMs: 12000, + atkModifier: 0.15, + defModifier: 0.15, + ), + ); + + /// Dark Launch - 은신 (회피+100% → 50%로 조정) + static const darkLaunch = Skill( + id: 'dark_launch', + name: 'Dark Launch', + type: SkillType.buff, + mpCost: 35, + cooldownMs: 30000, + power: 0, + buff: BuffEffect( + id: 'dark_launch_buff', + name: 'Hidden', + durationMs: 3000, + evasionModifier: 0.5, + ), + ); + + /// Static Analysis - 적 스탯 확인 (크리 증가) + static const staticAnalysis = Skill( + id: 'static_analysis', + name: 'Static Analysis', + type: SkillType.buff, + mpCost: 15, + cooldownMs: 12000, + power: 0, + buff: BuffEffect( + id: 'static_analysis_buff', + name: 'Analyzed', + durationMs: 10000, + criRateModifier: 0.15, + ), + ); + + /// Dynamic Analysis - 크리+회피 + static const dynamicAnalysis = Skill( + id: 'dynamic_analysis', + name: 'Dynamic Analysis', + type: SkillType.buff, + mpCost: 25, + cooldownMs: 18000, + power: 0, + buff: BuffEffect( + id: 'dynamic_analysis_buff', + name: 'Dynamic', + durationMs: 8000, + criRateModifier: 0.15, + evasionModifier: 0.15, + ), + ); + + /// Reverse Engineer - ATK 증가 + static const reverseEngineer = Skill( + id: 'reverse_engineer', + name: 'Reverse Engineer', + type: SkillType.buff, + mpCost: 30, + cooldownMs: 25000, + power: 0, + buff: BuffEffect( + id: 'reverse_engineer_buff', + name: 'Reversed', + durationMs: 10000, + atkModifier: 0.3, + ), + ); + + /// Cover Tracks - 회피 증가 + static const coverTracks = Skill( + id: 'cover_tracks', + name: 'Cover Tracks', + type: SkillType.buff, + mpCost: 20, + cooldownMs: 15000, + power: 0, + buff: BuffEffect( + id: 'cover_tracks_buff', + name: 'Covered', + durationMs: 5000, + evasionModifier: 0.25, + ), + ); + + /// Deploy - 분신 소환 (ATK 증가로 표현) + static const deploy = Skill( + id: 'deploy', + name: 'Deploy', + type: SkillType.buff, + mpCost: 35, + cooldownMs: 30000, + power: 0, + buff: BuffEffect( + id: 'deploy_buff', + name: 'Deployed', + durationMs: 12000, + atkModifier: 0.35, + ), + ); + + /// Retry Logic - 미스 방지 (크리 증가로 표현) + static const retryLogic = Skill( + id: 'retry_logic', + name: 'Retry Logic', + type: SkillType.buff, + mpCost: 20, + cooldownMs: 15000, + power: 0, + buff: BuffEffect( + id: 'retry_logic_buff', + name: 'Retrying', + durationMs: 10000, + criRateModifier: 0.2, + ), + ); + + /// State Machine - 상태 전이 (복합 버프) + static const stateMachine = Skill( + id: 'state_machine', + name: 'State Machine', + type: SkillType.buff, + mpCost: 30, + cooldownMs: 25000, + power: 0, + buff: BuffEffect( + id: 'state_machine_buff', + name: 'State Active', + durationMs: 15000, + atkModifier: 0.1, + defModifier: 0.1, + ), + ); + + // ============================================================================ + // 디버프 스킬 (Debuff) - 12개 + // ============================================================================ + + /// Step Over - 적 속도 -30% (적 ATK 감소로 표현) + static const stepOver = Skill( + id: 'step_over', + name: 'Step Over', + type: SkillType.debuff, + mpCost: 20, + cooldownMs: 15000, + power: 0, + buff: BuffEffect( + id: 'step_over_debuff', + name: 'Slowed', + durationMs: 5000, + atkModifier: -0.3, + ), + ); + + /// Cold Boot - 적 동결 (2초 스턴 → DEF 감소로 표현) + static const coldBoot = Skill( + id: 'cold_boot', + name: 'Cold Boot', + type: SkillType.debuff, + mpCost: 30, + cooldownMs: 25000, + power: 0, + buff: BuffEffect( + id: 'cold_boot_debuff', + name: 'Frozen', + durationMs: 3000, + defModifier: -0.5, + atkModifier: -0.5, + ), + ); + + /// Heap Analysis - 적 DEF -20% + static const heapAnalysis = Skill( + id: 'heap_analysis', + name: 'Heap Analysis', + type: SkillType.debuff, + mpCost: 25, + cooldownMs: 18000, + power: 0, + buff: BuffEffect( + id: 'heap_analysis_debuff', + name: 'Analyzed', + durationMs: 8000, + defModifier: -0.2, + ), + ); + + /// Unit Test - 버그 발견 (취약) + static const unitTest = Skill( + id: 'unit_test', + name: 'Unit Test', + type: SkillType.debuff, + mpCost: 20, + cooldownMs: 15000, + power: 0, + buff: BuffEffect( + id: 'unit_test_debuff', + name: 'Bug Found', + durationMs: 10000, + defModifier: -0.15, + ), + ); + + /// Integration Test - 연계 취약점 + static const integrationTest = Skill( + id: 'integration_test', + name: 'Integration Test', + type: SkillType.debuff, + mpCost: 25, + cooldownMs: 18000, + power: 0, + buff: BuffEffect( + id: 'integration_test_debuff', + name: 'Integration Bug', + durationMs: 8000, + defModifier: -0.2, + atkModifier: -0.1, + ), + ); + + /// Sanitizer - 적 버프 해제 (ATK/DEF 감소로 표현) + static const sanitizer = Skill( + id: 'sanitizer', + name: 'Sanitizer', + type: SkillType.debuff, + mpCost: 30, + cooldownMs: 20000, + power: 0, + buff: BuffEffect( + id: 'sanitizer_debuff', + name: 'Sanitized', + durationMs: 6000, + atkModifier: -0.25, + defModifier: -0.25, + ), + ); + + /// Hook Function - 다음 공격 반사 (DEF 증가로 표현) + static const hookFunction = Skill( + id: 'hook_function', + name: 'Hook Function', + type: SkillType.debuff, + mpCost: 25, + cooldownMs: 22000, + power: 0, + buff: BuffEffect( + id: 'hook_function_debuff', + name: 'Hooked', + durationMs: 5000, + atkModifier: -0.3, + ), + ); + + /// Rate Limit - 적 공격속도 -50% + static const rateLimit = Skill( + id: 'rate_limit', + name: 'Rate Limit', + type: SkillType.debuff, + mpCost: 30, + cooldownMs: 25000, + power: 0, + buff: BuffEffect( + id: 'rate_limit_debuff', + name: 'Rate Limited', + durationMs: 6000, + atkModifier: -0.4, + ), + ); + + /// Circuit Break - 적 스킬 봉인 (ATK 대폭 감소) + static const circuitBreak = Skill( + id: 'circuit_break', + name: 'Circuit Break', + type: SkillType.debuff, + mpCost: 35, + cooldownMs: 30000, + power: 0, + buff: BuffEffect( + id: 'circuit_break_debuff', + name: 'Circuit Broken', + durationMs: 5000, + atkModifier: -0.5, + ), + ); + + /// Backpressure - 데미지 반사 30% (적 ATK 감소로 표현) + static const backpressure = Skill( + id: 'backpressure', + name: 'Backpressure', + type: SkillType.debuff, + mpCost: 25, + cooldownMs: 20000, + power: 0, + buff: BuffEffect( + id: 'backpressure_debuff', + name: 'Pressured', + durationMs: 10000, + atkModifier: -0.2, + ), + ); + + /// Git Merge - 충돌 유발 (혼란) + static const gitMerge = Skill( + id: 'git_merge', + name: 'Git Merge', + type: SkillType.debuff, + mpCost: 20, + cooldownMs: 15000, + power: 0, + buff: BuffEffect( + id: 'git_merge_debuff', + name: 'Merge Conflict', + durationMs: 3000, + atkModifier: -0.3, + defModifier: -0.2, + ), + ); + + // ============================================================================ + // 스펠 이름 → 스킬 매핑 + // ============================================================================ + + /// PQ 스펠 이름으로 스킬 조회 + /// + /// 스펠 이름(영문)을 키로 사용하여 해당 전투 스킬을 반환 + static const Map spellNameToSkill = { + // 공격 스킬 + 'Stack Trace': stackTrace, + 'Core Dump': coreDump, + 'Memory Dump': memoryDump, + 'Kernel Panic': kernelPanic, + 'Blue Screen': blueScreen, + 'Inject Code': injectCode, + 'Spawn Shell': spawnShell, + 'Thread Pool': threadPool, + 'Exfiltrate Data': exfiltrateData, + 'Fuzzing': fuzzing, + 'Chaos Monkey': chaosMonkey, + 'Saga Pattern': sagaPattern, + 'Event Store': eventStore, + 'Auto Scale': autoScale, + 'Disassemble': disassemble, + 'Decompile': decompile, + 'Canary Release': canaryRelease, + 'A/B Test': abTest, + 'Pivot Network': pivotNetwork, + 'Async Await': asyncAwait, + 'Event Source': eventSource, + 'CQRS Split': cqrsSplit, + + // 회복 스킬 + 'Garbage Collection': garbageCollection, + 'Hot Reload': hotReload, + 'Rollback': rollback, + 'Hotfix': hotfix, + 'Snapshot Restore': snapshotRestore, + 'Patch Binary': patchBinary, + 'Git Commit': gitCommit, + 'Git Push': gitPush, + 'Connection Pool': connectionPool, + 'Load Balance': loadBalance, + 'Blue Green Deploy': blueGreenDeploy, + 'Cache Invalidate': cacheInvalidate, + + // 버프 스킬 + 'Debug Mode': debugMode, + 'Safe Mode': safeMode, + 'Memory Optimization': memoryOptimization, + 'Breakpoint': breakpoint, + 'Watch Variable': watchVariable, + 'Step Into': stepInto, + 'Profile Run': profileRun, + 'Benchmark': benchmark, + 'Elevate Privilege': elevatePrivilege, + 'Scale Up': scaleUp, + 'Failover': failover, + 'Containerize': containerize, + 'Orchestrate': orchestrate, + 'Promise Resolve': promiseResolve, + 'Feature Toggle': featureToggle, + 'Dark Launch': darkLaunch, + 'Static Analysis': staticAnalysis, + 'Dynamic Analysis': dynamicAnalysis, + 'Reverse Engineer': reverseEngineer, + 'Cover Tracks': coverTracks, + 'Deploy': deploy, + 'Retry Logic': retryLogic, + 'State Machine': stateMachine, + + // 디버프 스킬 + 'Step Over': stepOver, + 'Cold Boot': coldBoot, + 'Heap Analysis': heapAnalysis, + 'Unit Test': unitTest, + 'Integration Test': integrationTest, + 'Sanitizer': sanitizer, + 'Hook Function': hookFunction, + 'Rate Limit': rateLimit, + 'Circuit Break': circuitBreak, + 'Backpressure': backpressure, + 'Git Merge': gitMerge, + }; + // ============================================================================ // 스킬 목록 // ============================================================================ /// 모든 스킬 목록 - static const List allSkills = [ - // 공격 스킬 (단발성) - debugStrike, - nullPointer, - memoryLeak, - stackOverflow, - coreDump, - kernelPanic, - // DOT 스킬 - memoryCorruption, - infiniteLoop, - thermalThrottle, - raceCondition, - system32Delete, - // 회복 스킬 - quickFix, - hotReload, - garbageCollection, - // 버프 스킬 - overclock, - safeMode, - firewall, + static List get allSkills => spellNameToSkill.values.toList(); + + /// 기본 스킬 ID 목록 (SpellBook이 비어있을 때 사용) + static List get defaultSkillIds => const [ + 'stack_trace', // 기본 공격 + 'garbage_collection', // 기본 회복 ]; /// DOT 스킬 목록 - static List get dotSkills => - allSkills.where((s) => s.isDot).toList(); + static List get dotSkills => allSkills.where((s) => s.isDot).toList(); /// ID로 스킬 찾기 static Skill? getSkillById(String id) { @@ -304,6 +1096,11 @@ class SkillData { return null; } + /// 스펠 이름으로 스킬 찾기 + static Skill? getSkillBySpellName(String spellName) { + return spellNameToSkill[spellName]; + } + /// 타입별 스킬 목록 static List getSkillsByType(SkillType type) { return allSkills.where((s) => s.type == type).toList(); @@ -322,16 +1119,13 @@ class SkillData { /// 버프 스킬 목록 static List get buffSkills => getSkillsByType(SkillType.buff); - /// 기본 스킬 세트 (새 캐릭터용) - static List get defaultSkillIds => [ - debugStrike.id, - quickFix.id, - ]; + /// 디버프 스킬 목록 + static List get debuffSkills => getSkillsByType(SkillType.debuff); /// MP 비용 이하의 사용 가능한 공격 스킬 static List getAffordableAttackSkills(int currentMp) { - return getSkillsByType(SkillType.attack) - .where((s) => s.mpCost <= currentMp) - .toList(); + return getSkillsByType( + SkillType.attack, + ).where((s) => s.mpCost <= currentMp).toList(); } } diff --git a/lib/data/story_data.dart b/lib/data/story_data.dart index 828c843..33140c1 100644 --- a/lib/data/story_data.dart +++ b/lib/data/story_data.dart @@ -80,18 +80,12 @@ const Map> cinematicData = { text: 'Now, a new hero awakens to defend the Code.', durationMs: 3500, ), - CinematicStep( - text: 'Your journey begins...', - durationMs: 2500, - ), + CinematicStep(text: 'Your journey begins...', durationMs: 2500), ], // Act I: 각성 (레벨 1-20) StoryAct.act1: [ - CinematicStep( - text: '=== ACT I: AWAKENING ===', - durationMs: 3000, - ), + CinematicStep(text: '=== ACT I: AWAKENING ===', durationMs: 3000), CinematicStep( text: 'You have proven yourself against the lesser bugs.', durationMs: 3000, @@ -114,10 +108,7 @@ const Map> cinematicData = { // Act II: 성장 (레벨 21-40) StoryAct.act2: [ - CinematicStep( - text: '=== ACT II: GROWTH ===', - durationMs: 3000, - ), + CinematicStep(text: '=== ACT II: GROWTH ===', durationMs: 3000), CinematicStep( text: 'With the Dragon slain, you join the Debugger Knights.', durationMs: 3500, @@ -144,10 +135,7 @@ const Map> cinematicData = { // Act III: 시련 (레벨 41-60) StoryAct.act3: [ - CinematicStep( - text: '=== ACT III: TRIALS ===', - durationMs: 3000, - ), + CinematicStep(text: '=== ACT III: TRIALS ===', durationMs: 3000), CinematicStep( text: 'The path leads to the Null Kingdom...', asciiArt: _asciiNullKingdom, @@ -174,10 +162,7 @@ const Map> cinematicData = { // Act IV: 결전 (레벨 61-80) StoryAct.act4: [ - CinematicStep( - text: '=== ACT IV: CONFRONTATION ===', - durationMs: 3000, - ), + CinematicStep(text: '=== ACT IV: CONFRONTATION ===', durationMs: 3000), CinematicStep( text: "The Glitch God's Citadel looms before you.", asciiArt: _asciiCitadel, @@ -187,36 +172,24 @@ const Map> cinematicData = { text: 'Former enemies unite against the common threat.', durationMs: 3500, ), - CinematicStep( - text: 'The Final Alliance is forged.', - durationMs: 3000, - ), + CinematicStep(text: 'The Final Alliance is forged.', durationMs: 3000), CinematicStep( text: 'The Kernel Panic Archon blocks your path.', asciiArt: _asciiArchon, durationMs: 4000, ), - CinematicStep( - text: 'One final battle before the end...', - durationMs: 3500, - ), + CinematicStep(text: 'One final battle before the end...', durationMs: 3500), ], // Act V: 종말 (레벨 81-100) StoryAct.act5: [ - CinematicStep( - text: '=== ACT V: ENDGAME ===', - durationMs: 3000, - ), + CinematicStep(text: '=== ACT V: ENDGAME ===', durationMs: 3000), CinematicStep( text: 'The Glitch God reveals its true form.', asciiArt: _asciiGlitchGod, durationMs: 4000, ), - CinematicStep( - text: 'Reality itself begins to corrupt.', - durationMs: 3500, - ), + CinematicStep(text: 'Reality itself begins to corrupt.', durationMs: 3500), CinematicStep( text: 'All hope rests upon your shoulders.', durationMs: 3000, @@ -229,19 +202,13 @@ const Map> cinematicData = { // 엔딩: 시스템 재부팅, 평화 회복 StoryAct.ending: [ - CinematicStep( - text: '=== THE END ===', - durationMs: 3000, - ), + CinematicStep(text: '=== THE END ===', durationMs: 3000), CinematicStep( text: 'The Glitch God falls. The corruption fades.', asciiArt: _asciiVictory, durationMs: 4000, ), - CinematicStep( - text: 'System Reboot initiated...', - durationMs: 3000, - ), + CinematicStep(text: 'System Reboot initiated...', durationMs: 3000), CinematicStep( text: 'Peace returns to the Digital Realm.', durationMs: 3500, @@ -250,15 +217,8 @@ const Map> cinematicData = { text: 'Your legend will be compiled into the eternal logs.', durationMs: 4000, ), - CinematicStep( - text: 'THE END', - asciiArt: _asciiTheEnd, - durationMs: 5000, - ), - CinematicStep( - text: '...or is it?', - durationMs: 3000, - ), + CinematicStep(text: 'THE END', asciiArt: _asciiTheEnd, durationMs: 5000), + CinematicStep(text: '...or is it?', durationMs: 3000), ], }; diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 4e5bbd1..d56f93d 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -78,11 +78,11 @@ "xpNeededForNextLevel": "XP needed for next level", "@xpNeededForNextLevel": { "description": "XP needed tooltip" }, - "spellBook": "Spell Book", - "@spellBook": { "description": "Spell book section title" }, + "spellBook": "Skills", + "@spellBook": { "description": "Skills section title (unified spellbook + skills)" }, - "noSpellsYet": "No spells yet", - "@noSpellsYet": { "description": "Empty spell book message" }, + "noSpellsYet": "No skills yet", + "@noSpellsYet": { "description": "Empty skills message" }, "equipment": "Equipment", "@equipment": { "description": "Equipment panel title" }, diff --git a/lib/l10n/app_ja.arb b/lib/l10n/app_ja.arb index 7a9b921..7f81edf 100644 --- a/lib/l10n/app_ja.arb +++ b/lib/l10n/app_ja.arb @@ -25,8 +25,8 @@ "stats": "Stats", "experience": "Experience", "xpNeededForNextLevel": "XP needed for next level", - "spellBook": "Spell Book", - "noSpellsYet": "No spells yet", + "spellBook": "スキル", + "noSpellsYet": "習得したスキルがありません", "equipment": "Equipment", "inventory": "Inventory", "encumbrance": "Encumbrance", diff --git a/lib/l10n/app_ko.arb b/lib/l10n/app_ko.arb index 2ae2a7b..5384b3e 100644 --- a/lib/l10n/app_ko.arb +++ b/lib/l10n/app_ko.arb @@ -25,7 +25,7 @@ "stats": "능력치", "experience": "경험치", "xpNeededForNextLevel": "다음 레벨까지 필요한 XP", - "spellBook": "스킬북", + "spellBook": "스킬", "noSpellsYet": "습득한 스킬이 없습니다", "equipment": "장비", "inventory": "인벤토리", diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index 0c1f9d5..e019a86 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -245,16 +245,16 @@ abstract class L10n { /// **'XP needed for next level'** String get xpNeededForNextLevel; - /// Spell book section title + /// Skills section title (unified spellbook + skills) /// /// In en, this message translates to: - /// **'Spell Book'** + /// **'Skills'** String get spellBook; - /// Empty spell book message + /// Empty skills message /// /// In en, this message translates to: - /// **'No spells yet'** + /// **'No skills yet'** String get noSpellsYet; /// Equipment panel title diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index 973fdc0..643e583 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -83,10 +83,10 @@ class L10nEn extends L10n { String get xpNeededForNextLevel => 'XP needed for next level'; @override - String get spellBook => 'Spell Book'; + String get spellBook => 'Skills'; @override - String get noSpellsYet => 'No spells yet'; + String get noSpellsYet => 'No skills yet'; @override String get equipment => 'Equipment'; diff --git a/lib/l10n/app_localizations_ja.dart b/lib/l10n/app_localizations_ja.dart index 90a1353..273e0ec 100644 --- a/lib/l10n/app_localizations_ja.dart +++ b/lib/l10n/app_localizations_ja.dart @@ -83,10 +83,10 @@ class L10nJa extends L10n { String get xpNeededForNextLevel => 'XP needed for next level'; @override - String get spellBook => 'Spell Book'; + String get spellBook => 'スキル'; @override - String get noSpellsYet => 'No spells yet'; + String get noSpellsYet => '習得したスキルがありません'; @override String get equipment => 'Equipment'; diff --git a/lib/l10n/app_localizations_ko.dart b/lib/l10n/app_localizations_ko.dart index 3f22f17..f294fad 100644 --- a/lib/l10n/app_localizations_ko.dart +++ b/lib/l10n/app_localizations_ko.dart @@ -83,7 +83,7 @@ class L10nKo extends L10n { String get xpNeededForNextLevel => '다음 레벨까지 필요한 XP'; @override - String get spellBook => '스킬북'; + String get spellBook => '스킬'; @override String get noSpellsYet => '습득한 스킬이 없습니다'; diff --git a/lib/l10n/app_localizations_zh.dart b/lib/l10n/app_localizations_zh.dart index 776a0e4..e8470c6 100644 --- a/lib/l10n/app_localizations_zh.dart +++ b/lib/l10n/app_localizations_zh.dart @@ -83,10 +83,10 @@ class L10nZh extends L10n { String get xpNeededForNextLevel => 'XP needed for next level'; @override - String get spellBook => 'Spell Book'; + String get spellBook => '技能'; @override - String get noSpellsYet => 'No spells yet'; + String get noSpellsYet => '暂无技能'; @override String get equipment => 'Equipment'; diff --git a/lib/l10n/app_zh.arb b/lib/l10n/app_zh.arb index 7330804..feeee41 100644 --- a/lib/l10n/app_zh.arb +++ b/lib/l10n/app_zh.arb @@ -25,8 +25,8 @@ "stats": "Stats", "experience": "Experience", "xpNeededForNextLevel": "XP needed for next level", - "spellBook": "Spell Book", - "noSpellsYet": "No spells yet", + "spellBook": "技能", + "noSpellsYet": "暂无技能", "equipment": "Equipment", "inventory": "Inventory", "encumbrance": "Encumbrance", diff --git a/lib/src/app.dart b/lib/src/app.dart index e2a5e77..3d85aa2 100644 --- a/lib/src/app.dart +++ b/lib/src/app.dart @@ -97,9 +97,9 @@ class _AskiiNeverDieAppState extends State { if (saves.isEmpty) { // 저장 파일이 없으면 안내 메시지 - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(L10n.of(context).noSavedGames)), - ); + ScaffoldMessenger.of( + context, + ).showSnackBar(SnackBar(content: Text(L10n.of(context).noSavedGames))); return; } else if (saves.length == 1) { // 파일이 하나면 바로 선택 @@ -158,9 +158,7 @@ class _AskiiNeverDieAppState extends State { /// Phase 10: 명예의 전당 화면으로 이동 void _navigateToHallOfFame(BuildContext context) { Navigator.of(context).push( - MaterialPageRoute( - builder: (context) => const HallOfFameScreen(), - ), + MaterialPageRoute(builder: (context) => const HallOfFameScreen()), ); } } diff --git a/lib/src/core/animation/battle_composer.dart b/lib/src/core/animation/battle_composer.dart deleted file mode 100644 index 6d4309e..0000000 --- a/lib/src/core/animation/battle_composer.dart +++ /dev/null @@ -1,815 +0,0 @@ -// BattleComposer - 전투 프레임 실시간 합성 -// Stone Story RPG 스타일 참고 - 8줄 캐릭터/몬스터, 60자 폭 -// ASCII Patrol 스타일 패럴렉스 배경 - -import 'package:askiineverdie/src/core/animation/ascii_animation_data.dart'; -import 'package:askiineverdie/src/core/animation/background_data.dart'; -import 'package:askiineverdie/src/core/animation/background_layer.dart'; -import 'package:askiineverdie/src/core/animation/character_frames.dart'; -import 'package:askiineverdie/src/core/animation/monster_size.dart'; -import 'package:askiineverdie/src/core/animation/weapon_category.dart'; -import 'package:askiineverdie/src/core/animation/weapon_effects.dart'; - -/// 전투 프레임 합성기 -class BattleComposer { - const BattleComposer({ - required this.weaponCategory, - required this.hasShield, - required this.monsterCategory, - required this.monsterSize, - }); - - final WeaponCategory weaponCategory; - final bool hasShield; - final MonsterCategory monsterCategory; - final MonsterSize monsterSize; - - /// 전체 프레임 폭 (문자 수) - static const int frameWidth = 60; - - /// 프레임 높이 (줄 수) - static const int frameHeight = 8; - - /// 영역 분할 - static const int characterWidth = 18; - static const int effectWidth = 24; - static const int monsterWidth = 18; - - /// 전투 프레임 생성 (배경 없음) - String composeFrame(BattlePhase phase, int subFrame, String? monsterBaseName) { - // 캐릭터 프레임 - var charFrame = getCharacterFrame(phase, subFrame); - if (hasShield) { - charFrame = charFrame.withShield(); - } - - // 몬스터 프레임 (애니메이션 포함) - final monsterFrames = - _getAnimatedMonsterFrames(monsterCategory, monsterSize, phase); - final monsterFrame = monsterFrames[subFrame % monsterFrames.length]; - - // 무기 이펙트 (단일 라인) - final effect = getWeaponEffect(weaponCategory); - final effectLine = _getEffectLine(effect, phase, subFrame); - - // 프레임 합성 - return _compose(charFrame.lines, monsterFrame, effectLine, phase); - } - - /// 전투 프레임 생성 (배경 포함, ASCII Patrol 스타일) - String composeFrameWithBackground( - BattlePhase phase, - int subFrame, - String? monsterBaseName, - EnvironmentType environment, - int globalTick, - ) { - // 1. 8x60 캔버스 생성 (공백으로 초기화) - final canvas = - List.generate(frameHeight, (_) => List.filled(frameWidth, ' ')); - - // 2. 배경 레이어 그리기 (뒤에서 앞으로) - final layers = getBackgroundLayers(environment); - for (final layer in layers) { - _drawBackgroundLayer(canvas, layer, globalTick); - } - - // 3. 캐릭터 프레임 (페이즈에 따라 X 위치 변경 - 근접 전투) - var charFrame = getCharacterFrame(phase, subFrame); - if (hasShield) { - charFrame = charFrame.withShield(); - } - final normalizedChar = _normalizeSprite(charFrame.lines, characterWidth); - // 바닥 레이어(Y=7) 위에 서있도록 -1 - final charY = frameHeight - normalizedChar.length - 1; - // 페이즈별 캐릭터 X 위치 (몬스터에게 접근) - final charX = switch (phase) { - BattlePhase.idle => 0, - BattlePhase.prepare => 12, - BattlePhase.attack => 24, - BattlePhase.hit => 28, - BattlePhase.recover => 8, - }; - _overlaySpriteWithSpaces(canvas, normalizedChar, charX, charY); - - // 4. 몬스터 프레임 (정규화하여 오른쪽 정렬) - // idle 프레임 기준 너비로 정렬하여 hit/alert 시 위치 이동 방지 - final monsterRefWidth = _getMonsterReferenceWidth(monsterCategory, monsterSize); - final monsterFrames = - _getAnimatedMonsterFrames(monsterCategory, monsterSize, phase); - final monsterFrame = monsterFrames[subFrame % monsterFrames.length]; - final normalizedMonster = _normalizeSpriteRight( - monsterFrame, - monsterWidth, - referenceWidth: monsterRefWidth, - ); - final monsterX = frameWidth - monsterWidth; - // 바닥 레이어(Y=7) 위에 서있도록 -1 - final monsterY = frameHeight - normalizedMonster.length - 1; - // 몬스터는 경계 내 완전 렌더링 (내부 공백에 배경이 비치지 않도록) - _overlaySpriteWithBounds(canvas, normalizedMonster, monsterX, monsterY); - - // 5. 멀티라인 이펙트 오버레이 (공격/히트 페이즈) - if (phase == BattlePhase.attack || phase == BattlePhase.hit) { - final effect = getWeaponEffect(weaponCategory); - final effectLines = _getEffectLines(effect, phase, subFrame); - if (effectLines.isNotEmpty) { - // 이펙트 Y 위치: 캐릭터 머리 높이 (1번째 줄) 기준 - 수정됨 - final effectY = charY; - // 이펙트 X 위치: 캐릭터 오른쪽에 붙여서 표시 - final effectX = charX + 6; - for (var i = 0; i < effectLines.length; i++) { - final y = effectY + i; - if (y >= 0 && y < frameHeight && effectLines[i].isNotEmpty) { - _overlayText(canvas, effectLines[i], effectX, y); - } - } - } - } - - // 6. 문자열로 변환 - return canvas.map((row) => row.join()).join('\n'); - } - - /// 스프라이트를 지정 폭으로 정규화 (왼쪽 정렬) - List _normalizeSprite(List sprite, int width) { - return sprite.map((line) => line.padRight(width).substring(0, width)).toList(); - } - - /// 스프라이트를 지정 폭으로 정규화 (오른쪽 정렬, 전체 스프라이트 기준) - /// - /// 모든 줄을 동일한 기준점에서 오른쪽 정렬하여 - /// 머리와 몸통이 분리되지 않도록 함 - /// - /// [referenceWidth] 지정 시 해당 너비를 기준으로 정렬 (idle/hit 프레임 일관성용) - List _normalizeSpriteRight( - List sprite, - int width, { - int? referenceWidth, - }) { - // 1. 각 줄의 실제 너비(오른쪽 공백 제외) 계산 - final trimmedLines = sprite.map((line) => line.trimRight()).toList(); - - // 2. 기준 너비 결정 (referenceWidth 있으면 사용, 없으면 현재 스프라이트 기준) - int maxLineWidth; - if (referenceWidth != null) { - maxLineWidth = referenceWidth; - } else { - maxLineWidth = 0; - for (final line in trimmedLines) { - if (line.length > maxLineWidth) { - maxLineWidth = line.length; - } - } - } - - // 3. 전체 스프라이트를 오른쪽 정렬 (width 기준) - // 모든 줄에 동일한 왼쪽 패딩 적용 - final leftPadding = width - maxLineWidth; - final paddingStr = leftPadding > 0 ? ' ' * leftPadding : ''; - - return trimmedLines.map((line) { - // 각 줄을 왼쪽에 공통 패딩 추가 후 width로 자르기 - final paddedLine = paddingStr + line; - if (paddedLine.length > width) { - return paddedLine.substring(paddedLine.length - width); - } - return paddedLine.padRight(width); - }).toList(); - } - - /// 몬스터 스프라이트의 기준 너비 계산 (idle 프레임 기준) - int _getMonsterReferenceWidth(MonsterCategory category, MonsterSize size) { - final idleFrames = _getMonsterIdleFrames(category, size); - int maxWidth = 0; - for (final frame in idleFrames) { - for (final line in frame) { - final trimmedLength = line.trimRight().length; - if (trimmedLength > maxWidth) { - maxWidth = trimmedLength; - } - } - } - return maxWidth; - } - - /// 스프라이트를 캔버스에 오버레이 (공백은 투명 처리) - void _overlaySpriteWithSpaces( - List> canvas, - List sprite, - int startX, - int startY, - ) { - for (var i = 0; i < sprite.length; i++) { - final y = startY + i; - if (y < 0 || y >= frameHeight) continue; - - final line = sprite[i]; - for (var j = 0; j < line.length; j++) { - final x = startX + j; - if (x < 0 || x >= frameWidth) continue; - - final char = line[j]; - // 공백이 아닌 문자만 덮어쓰기 (투명 배경 효과) - if (char != ' ') { - canvas[y][x] = char; - } - } - } - } - - /// 스프라이트를 캔버스에 오버레이 (라인별 경계 내 완전 렌더링) - /// - /// 각 라인에서 첫 번째와 마지막 비공백 문자 사이의 모든 문자를 그림. - /// 내부 공백도 그려져서 스크롤링 배경이 비치지 않음. - void _overlaySpriteWithBounds( - List> canvas, - List sprite, - int startX, - int startY, - ) { - for (var i = 0; i < sprite.length; i++) { - final y = startY + i; - if (y < 0 || y >= frameHeight) continue; - - final line = sprite[i]; - - // 각 라인에서 첫/마지막 비공백 문자 위치 찾기 - int firstNonSpace = -1; - int lastNonSpace = -1; - for (var j = 0; j < line.length; j++) { - if (line[j] != ' ') { - if (firstNonSpace == -1) firstNonSpace = j; - lastNonSpace = j; - } - } - - if (firstNonSpace == -1) continue; // 빈 라인 - - // 경계 내 모든 문자 그리기 (공백 포함) - for (var j = firstNonSpace; j <= lastNonSpace; j++) { - final x = startX + j; - if (x < 0 || x >= frameWidth) continue; - canvas[y][x] = line[j]; - } - } - } - - /// 배경 레이어를 캔버스에 그리기 - void _drawBackgroundLayer( - List> canvas, - BackgroundLayer layer, - int globalTick, - ) { - for (var i = 0; i < layer.lines.length; i++) { - final y = layer.yStart + i; - if (y >= frameHeight) break; - - final pattern = layer.lines[i]; - if (pattern.isEmpty) continue; - - // 스크롤 오프셋 계산 - final offset = (globalTick * layer.scrollSpeed).toInt() % pattern.length; - - // 패턴을 스크롤하며 그리기 - for (var x = 0; x < frameWidth; x++) { - final patternIdx = (x + offset) % pattern.length; - final char = pattern[patternIdx]; - if (char != ' ') { - canvas[y][x] = char; - } - } - } - } - - /// 텍스트를 캔버스에 오버레이 - void _overlayText( - List> canvas, - String text, - int startX, - int y, - ) { - if (y < 0 || y >= frameHeight) return; - - for (var i = 0; i < text.length; i++) { - final x = startX + i; - if (x < 0 || x >= frameWidth) continue; - - final char = text[i]; - if (char != ' ') { - canvas[y][x] = char; - } - } - } - - /// 멀티라인 이펙트 프레임 반환 - List _getEffectLines( - WeaponEffect effect, BattlePhase phase, int subFrame) { - final frames = switch (phase) { - BattlePhase.idle => >[], - BattlePhase.prepare => effect.prepareFrames, - BattlePhase.attack => effect.attackFrames, - BattlePhase.hit => effect.hitFrames, - BattlePhase.recover => >[], - }; - if (frames.isEmpty) return []; - return frames[subFrame % frames.length]; - } - - /// 단일 라인 이펙트 (하위 호환용) - String _getEffectLine(WeaponEffect effect, BattlePhase phase, int subFrame) { - final lines = _getEffectLines(effect, phase, subFrame); - if (lines.isEmpty) return ''; - // 멀티라인 중 중간 라인 반환 (메인 이펙트) - final midIndex = lines.length ~/ 2; - return lines.length > midIndex ? lines[midIndex] : lines.first; - } - - String _compose( - List charLines, - List monsterLines, - String effectLine, - BattlePhase phase, - ) { - final result = []; - - // 캐릭터와 몬스터를 하단 정렬 (8줄 기준) - final charOffset = frameHeight - charLines.length; - final monsterOffset = frameHeight - monsterLines.length; - - // 이펙트 Y 위치: 캐릭터 body/arm 줄 (charOffset + 1) - final effectRow = charOffset + 1; - - for (var i = 0; i < frameHeight; i++) { - // 캐릭터 파트 (왼쪽 18자) - final charIdx = i - charOffset; - final charPart = - (charIdx >= 0 && charIdx < charLines.length ? charLines[charIdx] : '') - .padRight(characterWidth); - - // 이펙트 파트 (중앙 24자) - 캐릭터 팔 높이에 표시 - String effectPart = ''; - if (i == effectRow && - (phase == BattlePhase.attack || phase == BattlePhase.hit)) { - effectPart = effectLine; - } - effectPart = effectPart.padRight(effectWidth); - - // 몬스터 파트 (오른쪽 18자) - final monsterIdx = i - monsterOffset; - final monsterPart = (monsterIdx >= 0 && monsterIdx < monsterLines.length - ? monsterLines[monsterIdx] - : '') - .padLeft(monsterWidth); - - result.add('$charPart$effectPart$monsterPart'); - } - - return result.join('\n'); - } -} - -// ============================================================================ -// 몬스터 애니메이션 프레임 -// ============================================================================ - -/// 몬스터 애니메이션 프레임 반환 (페이즈별 다른 동작) -List> _getAnimatedMonsterFrames( - MonsterCategory category, - MonsterSize size, - BattlePhase phase, -) { - // 피격 상태 - if (phase == BattlePhase.hit) { - return _getMonsterHitFrames(category, size); - } - // 경계 상태 (prepare, attack) - if (phase == BattlePhase.prepare || phase == BattlePhase.attack) { - return _getMonsterAlertFrames(category, size); - } - // 일반 상태 (idle, recover) - return _getMonsterIdleFrames(category, size); -} - -/// 일반 상태 몬스터 프레임 -List> _getMonsterIdleFrames(MonsterCategory category, MonsterSize size) { - return switch (size) { - MonsterSize.tiny => _tinyIdleFrames(category), - MonsterSize.small => _smallIdleFrames(category), - MonsterSize.medium => _mediumIdleFrames(category), - MonsterSize.large => _largeIdleFrames(category), - _ => _hugeIdleFrames(category), // huge 이상은 같은 프레임 사용 - }; -} - -/// 피격 상태 몬스터 프레임 -List> _getMonsterHitFrames(MonsterCategory category, MonsterSize size) { - return switch (size) { - MonsterSize.tiny => _tinyHitFrames(category), - MonsterSize.small => _smallHitFrames(category), - MonsterSize.medium => _mediumHitFrames(category), - MonsterSize.large => _largeHitFrames(category), - _ => _hugeHitFrames(category), - }; -} - -/// 경계 상태 몬스터 프레임 (prepare/attack 시) -List> _getMonsterAlertFrames(MonsterCategory category, MonsterSize size) { - return switch (size) { - MonsterSize.tiny => _tinyAlertFrames(category), - MonsterSize.small => _smallAlertFrames(category), - MonsterSize.medium => _mediumAlertFrames(category), - MonsterSize.large => _largeAlertFrames(category), - _ => _hugeAlertFrames(category), - }; -} - -// ============================================================================ -// Tiny 몬스터 (2줄, 8줄 캔버스 하단 정렬) -// ============================================================================ - -List> _tinyIdleFrames(MonsterCategory category) { - return switch (category) { - MonsterCategory.bug => [ - [r'*', r'/\'], - [r'o', r'\/'], - ], - MonsterCategory.malware => [ - [r'><', r'\/'], - [r'<>', r'/\'], - ], - MonsterCategory.network => [ - [r'o', r'|'], - [r'O', r'|'], - ], - MonsterCategory.system => [ - [r'+', r'|'], - [r'x', r'|'], - ], - MonsterCategory.crypto => [ - [r'~<', r'>>'], - [r'<~', r'<<'], - ], - MonsterCategory.ai => [ - [r'()', r''], - [r'{}', r''], - ], - MonsterCategory.boss => [ - [r'^v', r'\/'], - [r'v^', r'/\'], - ], - }; -} - -List> _tinyHitFrames(MonsterCategory category) { - return [ - [r'*!', r'><'], - [r'!*', r'<>'], - ]; -} - -List> _tinyAlertFrames(MonsterCategory category) { - return switch (category) { - MonsterCategory.bug => [ - [r'!!', r'/\'], - [r'OO', r'><'], - ], - MonsterCategory.malware => [ - [r'!!', r'\/'], - [r'@@', r'/\'], - ], - MonsterCategory.network => [ - [r'O!', r'|'], - [r'!O', r'X'], - ], - MonsterCategory.system => [ - [r'!!', r'X'], - [r'@@', r'|'], - ], - MonsterCategory.crypto => [ - [r'!<', r'>>'], - [r'>!', r'<<'], - ], - MonsterCategory.ai => [ - [r'(!)', r''], - [r'{!}', r''], - ], - MonsterCategory.boss => [ - [r'^!', r'><'], - [r'!^', r'<>'], - ], - }; -} - -// ============================================================================ -// Small 몬스터 (4줄) -// ============================================================================ - -List> _smallIdleFrames(MonsterCategory category) { - return switch (category) { - MonsterCategory.bug => [ - [r' /\_/\', r'( o.o )', r' > ^ <', r' /| |\'], - [r' /\_/\', r'( o o )', r' > v <', r' \| |/'], - ], - MonsterCategory.malware => [ - [r' /\/\', r' (O O)', r' / \', r' \/ \/'], - [r' \/\/\', r' (O O)', r' \ /', r' /\ /\'], - ], - MonsterCategory.network => [ - [r' O', r' /|\', r' / \', r' _| |_'], - [r' O', r' \|/', r' | |', r' _/ \_'], - ], - MonsterCategory.system => [ - [r' _+_', r' (x_x)', r' /|\', r' _/ \_'], - [r' _+_', r' (X_X)', r' \|/', r' _| |_'], - ], - MonsterCategory.crypto => [ - [r' __', r' <(oo)~', r' / \', r' <_ _>'], - [r' __', r' (oo)>', r' \ /', r' <_ _>'], - ], - MonsterCategory.ai => [ - [r' ___', r' ( )', r' ( )', r' \_/'], - [r' _', r' / \', r' { }', r' \_/'], - ], - MonsterCategory.boss => [ - [r' ^w^', r' (|o|)', r' /|\', r' V V'], - [r' ^W^', r' (|O|)', r' \|/', r' v v'], - ], - }; -} - -List> _smallHitFrames(MonsterCategory category) { - return [ - [r' *!*', r' (>_<)', r' \X/', r' _/_\_'], - [r' !*!', r' (@_@)', r' /X\', r' _\_/_'], - ]; -} - -List> _smallAlertFrames(MonsterCategory category) { - return switch (category) { - MonsterCategory.bug => [ - [r' /\_/\', r'( O!O )', r' > ! <', r' /| |\'], - [r' /\_/\', r'( !O! )', r' > ! <', r' \| |/'], - ], - MonsterCategory.malware => [ - [r' /\/\', r' (! !)', r' / \', r' \/ \/'], - [r' \/\/\', r' (! !)', r' \ /', r' /\ /\'], - ], - MonsterCategory.network => [ - [r' O!', r' /|\', r' / \', r' _| |_'], - [r' !O', r' \|/', r' | |', r' _/ \_'], - ], - MonsterCategory.system => [ - [r' _!_', r' (!_!)', r' /|\', r' _/ \_'], - [r' _!_', r' (!_!)', r' \|/', r' _| |_'], - ], - MonsterCategory.crypto => [ - [r' __', r' <(!!)~', r' / \', r' <_ _>'], - [r' __', r' (!!)>', r' \ /', r' <_ _>'], - ], - MonsterCategory.ai => [ - [r' ___', r' ( ! )', r' ( ! )', r' \_/'], - [r' _', r' /!\', r' { ! }', r' \_/'], - ], - MonsterCategory.boss => [ - [r' ^!^', r' (|!|)', r' /|\', r' V V'], - [r' ^!^', r' (|!|)', r' \|/', r' v v'], - ], - }; -} - -// ============================================================================ -// Medium 몬스터 (6줄) -// ============================================================================ - -List> _mediumIdleFrames(MonsterCategory category) { - return switch (category) { - MonsterCategory.bug => [ - [r' /\_/\', r' ( O.O )', r' > ^ <', r' /| |\', r' | | | |', r'_|_| |_|_'], - [r' /\_/\', r' ( O O )', r' > v <', r' \| |/', r' | | | |', r'_|_| |_|_'], - ], - MonsterCategory.malware => [ - [r' /\/\', r' /O O\', r' \ /', r' / \', r' \/ \/', r' _/ \_'], - [r' \/\/\', r' \O O/', r' / \', r' \ /', r' /\ /\', r' _\ /_'], - ], - MonsterCategory.network => [ - [r' O', r' /|\', r' / \', r' | |', r' | |', r' _| |_'], - [r' O', r' \|/', r' | |', r' | |', r' | |', r' _/ \_'], - ], - MonsterCategory.system => [ - [r' _+_', r' (X_X)', r' /|\', r' / | \', r' | | |', r'_/ | \_'], - [r' _x_', r' (x_x)', r' \|/', r' \ | /', r' | | |', r'_\ | /_'], - ], - MonsterCategory.crypto => [ - [r' __', r' <(OO)~', r' / \', r' / \', r' | |', r'<__ __>'], - [r' __', r' (OO)>', r' \ /', r' \ /', r' | |', r'<__ __>'], - ], - MonsterCategory.ai => [ - [r' ____', r' / \', r' ( )', r' ( )', r' \ /', r' \__/'], - [r' __', r' / \', r' / \', r' { }', r' \ /', r' \__/'], - ], - MonsterCategory.boss => [ - [r' ^W^', r' (|O|)', r' /|\', r' / | \', r' V V', r' _/ \_'], - [r' ^w^', r' (|o|)', r' \|/', r' \ | /', r' v v', r' _\ /_'], - ], - }; -} - -List> _mediumHitFrames(MonsterCategory category) { - return [ - [r' *!*', r' (>.<)', r' \X/', r' / \', r' | |', r'_/_ \_\'], - [r' !*!', r' (@_@)', r' /X\', r' \ /', r' | |', r'_\_ /_/'], - ]; -} - -List> _mediumAlertFrames(MonsterCategory category) { - return switch (category) { - MonsterCategory.bug => [ - [r' /\_/\', r' ( O!O )', r' > ! <', r' /| |\', r' | | | |', r'_|_| |_|_'], - [r' /\_/\', r' ( !O! )', r' > ! <', r' \| |/', r' | | | |', r'_|_| |_|_'], - ], - MonsterCategory.malware => [ - [r' /\/\', r' /! !\', r' \ /', r' / \', r' \/ \/', r' _/ \_'], - [r' \/\/\', r' \! !/', r' / \', r' \ /', r' /\ /\', r' _\ /_'], - ], - MonsterCategory.network => [ - [r' O!', r' /|\', r' / \', r' | |', r' | |', r' _| |_'], - [r' !O', r' \|/', r' | |', r' | |', r' | |', r' _/ \_'], - ], - MonsterCategory.system => [ - [r' _!_', r' (!_!)', r' /|\', r' / | \', r' | | |', r'_/ | \_'], - [r' _!_', r' (!_!)', r' \|/', r' \ | /', r' | | |', r'_\ | /_'], - ], - MonsterCategory.crypto => [ - [r' __', r' <(!!)~', r' / \', r' / \', r' | |', r'<__ __>'], - [r' __', r' (!!)>', r' \ /', r' \ /', r' | |', r'<__ __>'], - ], - MonsterCategory.ai => [ - [r' ____', r' / ! \', r' ( ! )', r' ( ! )', r' \ /', r' \__/'], - [r' __', r' / !\', r' / ! \', r' { ! }', r' \ /', r' \__/'], - ], - MonsterCategory.boss => [ - [r' ^!^', r' (|!|)', r' /|\', r' / | \', r' V V', r' _/ \_'], - [r' ^!^', r' (|!|)', r' \|/', r' \ | /', r' v v', r' _\ /_'], - ], - }; -} - -// ============================================================================ -// Large 몬스터 (8줄) -// ============================================================================ - -List> _largeIdleFrames(MonsterCategory category) { - return switch (category) { - MonsterCategory.bug => [ - [r' /\__/\', r' ( O O )', r' > ^^ <', r' /| |\', r' | | | |', r' | | | |', r'_| | | |_', r'|__|____|__|'], - [r' /\__/\', r' ( O O )', r' > vv <', r' \| |/', r' | | | |', r' | | | |', r'_| | | |_', r'|__|____|__|'], - ], - MonsterCategory.malware => [ - [r' /\/\/\', r' /O O\', r' \ /', r' / \', r' \/ \/', r' | \ / |', r' _| \/ |_', r'|___________|'], - [r' \/\/\/\', r' \O O/', r' / \', r' \ /', r' /\ /\', r' | / \ |', r' _| /\ |_', r'|___________|'], - ], - MonsterCategory.network => [ - [r' O', r' /|\', r' / \', r' | |', r' | |', r' | |', r' _| |_', r'|_________|'], - [r' O', r' \|/', r' | |', r' | |', r' | |', r' | |', r' _/ \_', r'|_________|'], - ], - MonsterCategory.system => [ - [r' _/+\_', r' (X___X)', r' /|||\', r' / ||| \', r' | ||| |', r' | / \ |', r' _|/ \|_', r'|_/ \_|'], - [r' _\+/_', r' (x___x)', r' \|||/', r' \ ||| /', r' | ||| |', r' | \ / |', r' _|\ /|_', r'|_\ /_|'], - ], - MonsterCategory.crypto => [ - [r' ___', r' <<(O O)~~', r' / || \', r' / || \', r' | || |', r' | || |', r' _| || |_', r'<___________|>'], - [r' ___', r' (O O)>>', r' \ || /', r' \ || /', r' | || |', r' | || |', r' _| || |_', r'<___________|>'], - ], - MonsterCategory.ai => [ - [r' _____', r' / \', r' / \', r' ( )', r' ( )', r' \ /', r' \_____/', r' \___/'], - [r' ___', r' / \', r' / \', r' { }', r' { }', r' \ /', r' \___/', r' \_/'], - ], - MonsterCategory.boss => [ - [r' ^W^', r' /|O|\', r' /|\', r' / | \', r' | | |', r' V | V', r' _/ | \_', r'|____|____|'], - [r' ^w^', r' \|o|/', r' \|/', r' \ | /', r' | | |', r' v | v', r' _\ | /_', r'|____|____|'], - ], - }; -} - -List> _largeHitFrames(MonsterCategory category) { - return [ - [r' *!*!*', r' (>___<)', r' \\X//', r' / \\// \', r' | \\/ |', r' | / \ |', r' _|/ \|_', r'|___/\\___|'], - [r' !*!*!', r' (@___@)', r' //X\\', r' \ /\\/ /', r' | //\\ |', r' | \ / |', r' _|\ /|_', r'|___\\/__|'], - ]; -} - -List> _largeAlertFrames(MonsterCategory category) { - return switch (category) { - MonsterCategory.bug => [ - [r' /\__/\', r' ( O!!O )', r' > !! <', r' /| |\', r' | | | |', r' | | | |', r'_| | | |_', r'|__|____|__|'], - [r' /\__/\', r' ( !!O! )', r' > !! <', r' \| |/', r' | | | |', r' | | | |', r'_| | | |_', r'|__|____|__|'], - ], - MonsterCategory.malware => [ - [r' /\/\/\', r' /! !\', r' \ /', r' / \', r' \/ \/', r' | \ / |', r' _| \/ |_', r'|___________|'], - [r' \/\/\/\', r' \! !/', r' / \', r' \ /', r' /\ /\', r' | / \ |', r' _| /\ |_', r'|___________|'], - ], - MonsterCategory.network => [ - [r' O!', r' /|\', r' / \', r' | |', r' | |', r' | |', r' _| |_', r'|_________|'], - [r' !O', r' \|/', r' | |', r' | |', r' | |', r' | |', r' _/ \_', r'|_________|'], - ], - MonsterCategory.system => [ - [r' _/!\_', r' (!___!)', r' /|||\', r' / ||| \', r' | ||| |', r' | / \ |', r' _|/ \|_', r'|_/ \_|'], - [r' _\!/_', r' (!___!)', r' \|||/', r' \ ||| /', r' | ||| |', r' | \ / |', r' _|\ /|_', r'|_\ /_|'], - ], - MonsterCategory.crypto => [ - [r' ___', r' <<(! !)~~', r' / || \', r' / || \', r' | || |', r' | || |', r' _| || |_', r'<___________|>'], - [r' ___', r' (! !)>>', r' \ || /', r' \ || /', r' | || |', r' | || |', r' _| || |_', r'<___________|>'], - ], - MonsterCategory.ai => [ - [r' _____', r' / ! \', r' / ! \', r' ( ! )', r' ( ! )', r' \ /', r' \_____/', r' \___/'], - [r' ___', r' / ! \', r' / ! \', r' { ! }', r' { ! }', r' \ /', r' \___/', r' \_/'], - ], - MonsterCategory.boss => [ - [r' ^!^', r' /|!|\', r' /|\', r' / | \', r' | | |', r' V | V', r' _/ | \_', r'|____|____|'], - [r' ^!^', r' \|!|/', r' \|/', r' \ | /', r' | | |', r' v | v', r' _\ | /_', r'|____|____|'], - ], - }; -} - -// ============================================================================ -// Huge+ 몬스터 (8줄, 더 넓게) -// ============================================================================ - -List> _hugeIdleFrames(MonsterCategory category) { - return switch (category) { - MonsterCategory.bug => [ - [r' /\____/\', r' ( O O )', r' > ^^^^ <', r' /| |\', r' | | | |', r' | | | |', r' _| | | |_', r'|___|______|___|'], - [r' /\____/\', r' ( O O )', r' > vvvv <', r' \| |/', r' | | | |', r' | | | |', r' _| | | |_', r'|___|______|___|'], - ], - MonsterCategory.malware => [ - [r' /\/\/\/\', r' /O O\', r' \ /', r' / \', r' \/ \/', r' | \ / |', r' _| \ / |_', r'|_______________|'], - [r' \/\/\/\/\', r' \O O/', r' / \', r' \ /', r' /\ /\', r' | / \ |', r' _| / \ |_', r'|_______________|'], - ], - MonsterCategory.network => [ - [r' O', r' _/|\\_', r' / | \\', r' | |', r' | |', r' | |', r' _| |_', r'|___________|'], - [r' O', r' \\_|_/', r' \\|/', r' | |', r' | |', r' | |', r' _/ \\_', r'|___________|'], - ], - MonsterCategory.system => [ - [r' _/+\\_', r' (X_____X)', r' /|||||\', r' / ||||| \\', r' | ||||| |', r' | / \\ |', r' _|/ \\|_', r'|_/ \\_|'], - [r' _\\+/_', r' (x_____x)', r' \\|||||/', r' \\ ||||| /', r' | ||||| |', r' | \\ / |', r' _|\\ /|_', r'|_\\ /_|'], - ], - MonsterCategory.crypto => [ - [r' ____', r' <<<(O O)~~~', r' / |||| \\', r' / |||| \\', r' | |||| |', r' | |||| |', r' _| |||| |_', r'<_______________|>'], - [r' ____', r' (O O)>>>', r' \\ |||| /', r' \\ |||| /', r' | |||| |', r' | |||| |', r' _| |||| |_', r'<_______________|>'], - ], - MonsterCategory.ai => [ - [r' ______', r' / \\', r' / \\', r' ( )', r' ( )', r' \\ /', r' \\______/', r' \\____/'], - [r' ____', r' / \\', r' / \\', r' { }', r' { }', r' \\ /', r' \\____/', r' \\__/'], - ], - MonsterCategory.boss => [ - [r' ^W^', r' /|O|\\ ', r' /|\\', r' / | \\', r' | | |', r' V | V', r' _/ | \\_', r'|_____|_____|'], - [r' ^w^', r' \\|o|/', r' \\|/', r' \\ | /', r' | | |', r' v | v', r' _\\ | /_', r'|_____|_____|'], - ], - }; -} - -List> _hugeHitFrames(MonsterCategory category) { - return [ - [r' *!*!*!*', r' (>_____<)', r' \\\\X////', r' / \\\\// \\', r' | \\\\/ |', r' | / \\ |', r' _|/ \\|_', r'|____/\\\\___|'], - [r' !*!*!*!', r' (@_____@)', r' ////X\\\\', r' \\ /\\\\/ /', r' | ////\\\\ |', r' | \\ / |', r' _|\\ /|_', r'|____\\\\/___|'], - ]; -} - -List> _hugeAlertFrames(MonsterCategory category) { - return switch (category) { - MonsterCategory.bug => [ - [r' /\____/\', r' ( ! ! )', r' > !!!! <', r' /| |\', r' | | | |', r' | | | |', r' _| | | |_', r'|___|______|___|'], - [r' /\____/\', r' ( ! ! )', r' > !!!! <', r' \| |/', r' | | | |', r' | | | |', r' _| | | |_', r'|___|______|___|'], - ], - MonsterCategory.malware => [ - [r' /\/\/\/\', r' /! !\', r' \ /', r' / \', r' \/ \/', r' | \ / |', r' _| \ / |_', r'|_______________|'], - [r' \/\/\/\/\', r' \! !/', r' / \', r' \ /', r' /\ /\', r' | / \ |', r' _| / \ |_', r'|_______________|'], - ], - MonsterCategory.network => [ - [r' O!', r' _/|\\__', r' / | \\', r' | |', r' | |', r' | |', r' _| |_', r'|___________|'], - [r' !O', r' \\_|_/', r' \\|/', r' | |', r' | |', r' | |', r' _/ \\_', r'|___________|'], - ], - MonsterCategory.system => [ - [r' _/!\\__', r' (!_____!)', r' /|||||\', r' / ||||| \\', r' | ||||| |', r' | / \\ |', r' _|/ \\|_', r'|_/ \\_|'], - [r' _\\!/_', r' (!_____!)', r' \\|||||/', r' \\ ||||| /', r' | ||||| |', r' | \\ / |', r' _|\\ /|_', r'|_\\ /_|'], - ], - MonsterCategory.crypto => [ - [r' ____', r' <<<(! !)~~~', r' / |||| \\', r' / |||| \\', r' | |||| |', r' | |||| |', r' _| |||| |_', r'<_______________|>'], - [r' ____', r' (! !)>>>', r' \\ |||| /', r' \\ |||| /', r' | |||| |', r' | |||| |', r' _| |||| |_', r'<_______________|>'], - ], - MonsterCategory.ai => [ - [r' ______', r' / ! \\', r' / ! \\', r' ( ! )', r' ( ! )', r' \\ /', r' \\______/', r' \\____/'], - [r' ____', r' / ! \\', r' / ! \\', r' { ! }', r' { ! }', r' \\ /', r' \\____/', r' \\__/'], - ], - MonsterCategory.boss => [ - [r' ^!^', r' /|!|\\ ', r' /|\\', r' / | \\', r' | | |', r' V | V', r' _/ | \\_', r'|_____|_____|'], - [r' ^!^', r' \\|!|/', r' \\|/', r' \\ | /', r' | | |', r' v | v', r' _\\ | /_', r'|_____|_____|'], - ], - }; -} - -// 레거시 호환용 함수 -List> getMonsterFrames(MonsterCategory category, MonsterSize size) { - return _getMonsterIdleFrames(category, size); -} diff --git a/lib/src/core/animation/canvas/ascii_cell.dart b/lib/src/core/animation/canvas/ascii_cell.dart index 38e9e06..0a977d2 100644 --- a/lib/src/core/animation/canvas/ascii_cell.dart +++ b/lib/src/core/animation/canvas/ascii_cell.dart @@ -15,10 +15,7 @@ enum AsciiCellColor { /// 단일 ASCII 셀 데이터 class AsciiCell { - const AsciiCell({ - required this.char, - this.color = AsciiCellColor.object, - }); + const AsciiCell({required this.char, this.color = AsciiCellColor.object}); /// 표시할 문자 (단일 문자) final String char; @@ -45,10 +42,7 @@ class AsciiCell { /// 문자열에서 AsciiCell 생성 (자동 색상) factory AsciiCell.fromChar(String char) { if (char.isEmpty || char == ' ') return empty; - return AsciiCell( - char: char, - color: colorFromChar(char), - ); + return AsciiCell(char: char, color: colorFromChar(char)); } @override diff --git a/lib/src/core/animation/canvas/ascii_layer.dart b/lib/src/core/animation/canvas/ascii_layer.dart index aca71c3..22b08c6 100644 --- a/lib/src/core/animation/canvas/ascii_layer.dart +++ b/lib/src/core/animation/canvas/ascii_layer.dart @@ -37,11 +37,7 @@ class AsciiLayer { } /// 빈 레이어 생성 - factory AsciiLayer.empty({ - int width = 60, - int height = 8, - int zIndex = 0, - }) { + factory AsciiLayer.empty({int width = 60, int height = 8, int zIndex = 0}) { final cells = List.generate( height, (_) => List.filled(width, AsciiCell.empty), diff --git a/lib/src/core/animation/canvas/canvas_battle_composer.dart b/lib/src/core/animation/canvas/canvas_battle_composer.dart index 26d605b..57bd91e 100644 --- a/lib/src/core/animation/canvas/canvas_battle_composer.dart +++ b/lib/src/core/animation/canvas/canvas_battle_composer.dart @@ -279,8 +279,7 @@ List> _getMonsterIdleFrames( MonsterSize.large || MonsterSize.huge || MonsterSize.giant || - MonsterSize.titanic => - _largeIdleFrames(category), + MonsterSize.titanic => _largeIdleFrames(category), }; } @@ -295,8 +294,7 @@ List> _getMonsterHitFrames( MonsterSize.large || MonsterSize.huge || MonsterSize.giant || - MonsterSize.titanic => - _largeHitFrames(category), + MonsterSize.titanic => _largeHitFrames(category), }; } @@ -311,8 +309,7 @@ List> _getMonsterAlertFrames( MonsterSize.large || MonsterSize.huge || MonsterSize.giant || - MonsterSize.titanic => - _largeAlertFrames(category), + MonsterSize.titanic => _largeAlertFrames(category), }; } @@ -483,7 +480,7 @@ List> _mediumIdleFrames(MonsterCategory category) { r' > ^ <', r' /| |\', r' | | | |', - r'_|_| |_|_' + r'_|_| |_|_', ], [ r' /\_/\', @@ -491,7 +488,7 @@ List> _mediumIdleFrames(MonsterCategory category) { r' > v <', r' \| |/', r' | | | |', - r'_|_| |_|_' + r'_|_| |_|_', ], ], MonsterCategory.malware => [ @@ -501,7 +498,7 @@ List> _mediumIdleFrames(MonsterCategory category) { r' \ /', r' / \', r' \/ \/', - r' _/ \_' + r' _/ \_', ], [ r' \/\/\', @@ -509,7 +506,7 @@ List> _mediumIdleFrames(MonsterCategory category) { r' / \', r' \ /', r' /\ /\', - r' _\ /_' + r' _\ /_', ], ], MonsterCategory.network => [ @@ -517,22 +514,8 @@ List> _mediumIdleFrames(MonsterCategory category) { [r' O', r' \|/', r' | |', r' | |', r' | |', r' _/ \_'], ], MonsterCategory.system => [ - [ - r' _+_', - r' (X_X)', - r' /|\', - r' / | \', - r' | | |', - r'_/ | \_' - ], - [ - r' _x_', - r' (x_x)', - r' \|/', - r' \ | /', - r' | | |', - r'_\ | /_' - ], + [r' _+_', r' (X_X)', r' /|\', r' / | \', r' | | |', r'_/ | \_'], + [r' _x_', r' (x_x)', r' \|/', r' \ | /', r' | | |', r'_\ | /_'], ], MonsterCategory.crypto => [ [ @@ -541,7 +524,7 @@ List> _mediumIdleFrames(MonsterCategory category) { r' / \', r' / \', r' | |', - r'<__ __>' + r'<__ __>', ], [ r' __', @@ -549,12 +532,26 @@ List> _mediumIdleFrames(MonsterCategory category) { r' \ /', r' \ /', r' | |', - r'<__ __>' + r'<__ __>', ], ], MonsterCategory.ai => [ - [r' ____', r' / \', r' ( )', r' ( )', r' \ /', r' \__/'], - [r' __', r' / \', r' / \', r' { }', r' \ /', r' \__/'], + [ + r' ____', + r' / \', + r' ( )', + r' ( )', + r' \ /', + r' \__/', + ], + [ + r' __', + r' / \', + r' / \', + r' { }', + r' \ /', + r' \__/', + ], ], MonsterCategory.boss => [ [r' ^W^', r' (|O|)', r' /|\', r' / | \', r' V V', r' _/ \_'], @@ -579,7 +576,7 @@ List> _mediumAlertFrames(MonsterCategory category) { r' > ! <', r' /| |\', r' | | | |', - r'_|_| |_|_' + r'_|_| |_|_', ], [ r' /\_/\', @@ -587,7 +584,7 @@ List> _mediumAlertFrames(MonsterCategory category) { r' > ! <', r' \| |/', r' | | | |', - r'_|_| |_|_' + r'_|_| |_|_', ], ], MonsterCategory.malware => [ @@ -597,7 +594,7 @@ List> _mediumAlertFrames(MonsterCategory category) { r' \ /', r' / \', r' \/ \/', - r' _/ \_' + r' _/ \_', ], [ r' \/\/\', @@ -605,7 +602,7 @@ List> _mediumAlertFrames(MonsterCategory category) { r' / \', r' \ /', r' /\ /\', - r' _\ /_' + r' _\ /_', ], ], MonsterCategory.network => [ @@ -613,22 +610,8 @@ List> _mediumAlertFrames(MonsterCategory category) { [r' !O', r' \|/', r' | |', r' | |', r' | |', r' _/ \_'], ], MonsterCategory.system => [ - [ - r' _!_', - r' (!_!)', - r' /|\', - r' / | \', - r' | | |', - r'_/ | \_' - ], - [ - r' _!_', - r' (!_!)', - r' \|/', - r' \ | /', - r' | | |', - r'_\ | /_' - ], + [r' _!_', r' (!_!)', r' /|\', r' / | \', r' | | |', r'_/ | \_'], + [r' _!_', r' (!_!)', r' \|/', r' \ | /', r' | | |', r'_\ | /_'], ], MonsterCategory.crypto => [ [ @@ -637,7 +620,7 @@ List> _mediumAlertFrames(MonsterCategory category) { r' / \', r' / \', r' | |', - r'<__ __>' + r'<__ __>', ], [ r' __', @@ -645,7 +628,7 @@ List> _mediumAlertFrames(MonsterCategory category) { r' \ /', r' \ /', r' | |', - r'<__ __>' + r'<__ __>', ], ], MonsterCategory.ai => [ @@ -655,9 +638,16 @@ List> _mediumAlertFrames(MonsterCategory category) { r' ( ! )', r' ( ! )', r' \ /', - r' \__/' + r' \__/', + ], + [ + r' __', + r' / !\', + r' / ! \', + r' { ! }', + r' \ /', + r' \__/', ], - [r' __', r' / !\', r' / ! \', r' { ! }', r' \ /', r' \__/'], ], MonsterCategory.boss => [ [r' ^!^', r' (|!|)', r' /|\', r' / | \', r' V V', r' _/ \_'], @@ -681,7 +671,7 @@ List> _largeIdleFrames(MonsterCategory category) { r' | | | |', r' | | | |', r'_| | | |_', - r'|__|____|__|' + r'|__|____|__|', ], [ r' /\__/\', @@ -691,7 +681,7 @@ List> _largeIdleFrames(MonsterCategory category) { r' | | | |', r' | | | |', r'_| | | |_', - r'|__|____|__|' + r'|__|____|__|', ], ], MonsterCategory.malware => [ @@ -703,7 +693,7 @@ List> _largeIdleFrames(MonsterCategory category) { r' / \', r' \/ \/', r' _/ \_', - r'/__ __\\' + r'/__ __\\', ], [ r' \/\/\', @@ -713,7 +703,7 @@ List> _largeIdleFrames(MonsterCategory category) { r' \ /', r' /\ /\', r' _\ /_', - r'\__ __/' + r'\__ __/', ], ], MonsterCategory.network => [ @@ -725,7 +715,7 @@ List> _largeIdleFrames(MonsterCategory category) { r' | |', r' | |', r' _| |_', - r'|__ __|' + r'|__ __|', ], [ r' O', @@ -735,7 +725,7 @@ List> _largeIdleFrames(MonsterCategory category) { r' | |', r' | |', r' _/ \_', - r'/__ __\\' + r'/__ __\\', ], ], MonsterCategory.system => [ @@ -747,7 +737,7 @@ List> _largeIdleFrames(MonsterCategory category) { r' | | |', r' | | |', r' _/ | \_', - r'|____|____|' + r'|____|____|', ], [ r' _x_', @@ -757,7 +747,7 @@ List> _largeIdleFrames(MonsterCategory category) { r' | | |', r' | | |', r' _\ | /_', - r'|____|____|' + r'|____|____|', ], ], MonsterCategory.crypto => [ @@ -769,7 +759,7 @@ List> _largeIdleFrames(MonsterCategory category) { r' | |', r' | |', r' <__ __>', - r'|___ ___|' + r'|___ ___|', ], [ r' ___', @@ -779,7 +769,7 @@ List> _largeIdleFrames(MonsterCategory category) { r' | |', r' | |', r' <__ __>', - r'|___ ___|' + r'|___ ___|', ], ], MonsterCategory.ai => [ @@ -791,7 +781,7 @@ List> _largeIdleFrames(MonsterCategory category) { r' ( )', r' \ /', r' \ /', - r' \_/' + r' \_/', ], [ r' ___', @@ -801,7 +791,7 @@ List> _largeIdleFrames(MonsterCategory category) { r' { }', r' \ /', r' \ /', - r' \_/' + r' \_/', ], ], MonsterCategory.boss => [ @@ -813,7 +803,7 @@ List> _largeIdleFrames(MonsterCategory category) { r' | | |', r' V | V', r' _/ | \_', - r'|_____|_____|' + r'|_____|_____|', ], [ r' ^w^', @@ -823,7 +813,7 @@ List> _largeIdleFrames(MonsterCategory category) { r' | | |', r' v | v', r' _\ | /_', - r'|_____|_____|' + r'|_____|_____|', ], ], }; @@ -839,7 +829,7 @@ List> _largeHitFrames(MonsterCategory category) { r' | | |', r' X | X', r' _/ | \_', - r'|_____|_____|' + r'|_____|_____|', ], [ r' !*!', @@ -849,7 +839,7 @@ List> _largeHitFrames(MonsterCategory category) { r' | | |', r' x | x', r' _\ | /_', - r'|_____|_____|' + r'|_____|_____|', ], ]; } @@ -865,7 +855,7 @@ List> _largeAlertFrames(MonsterCategory category) { r' | | | |', r' | | | |', r'_| | | |_', - r'|__|____|__|' + r'|__|____|__|', ], [ r' /\__/\', @@ -875,7 +865,7 @@ List> _largeAlertFrames(MonsterCategory category) { r' | | | |', r' | | | |', r'_| | | |_', - r'|__|____|__|' + r'|__|____|__|', ], ], MonsterCategory.malware => [ @@ -887,7 +877,7 @@ List> _largeAlertFrames(MonsterCategory category) { r' / \', r' \/ \/', r' _/ \_', - r'/__ __\\' + r'/__ __\\', ], [ r' \/\/\', @@ -897,7 +887,7 @@ List> _largeAlertFrames(MonsterCategory category) { r' \ /', r' /\ /\', r' _\ /_', - r'\__ __/' + r'\__ __/', ], ], MonsterCategory.network => [ @@ -909,7 +899,7 @@ List> _largeAlertFrames(MonsterCategory category) { r' | |', r' | |', r' _| |_', - r'|__ __|' + r'|__ __|', ], [ r' !O', @@ -919,7 +909,7 @@ List> _largeAlertFrames(MonsterCategory category) { r' | |', r' | |', r' _/ \_', - r'/__ __\\' + r'/__ __\\', ], ], MonsterCategory.system => [ @@ -931,7 +921,7 @@ List> _largeAlertFrames(MonsterCategory category) { r' | | |', r' | | |', r' _/ | \_', - r'|____|____|' + r'|____|____|', ], [ r' _!_', @@ -941,7 +931,7 @@ List> _largeAlertFrames(MonsterCategory category) { r' | | |', r' | | |', r' _\ | /_', - r'|____|____|' + r'|____|____|', ], ], MonsterCategory.crypto => [ @@ -953,7 +943,7 @@ List> _largeAlertFrames(MonsterCategory category) { r' | |', r' | |', r' <__ __>', - r'|___ ___|' + r'|___ ___|', ], [ r' ___', @@ -963,7 +953,7 @@ List> _largeAlertFrames(MonsterCategory category) { r' | |', r' | |', r' <__ __>', - r'|___ ___|' + r'|___ ___|', ], ], MonsterCategory.ai => [ @@ -975,7 +965,7 @@ List> _largeAlertFrames(MonsterCategory category) { r' ( ! )', r' \ /', r' \ /', - r' \_/' + r' \_/', ], [ r' ___', @@ -985,7 +975,7 @@ List> _largeAlertFrames(MonsterCategory category) { r' { ! }', r' \ /', r' \ /', - r' \_/' + r' \_/', ], ], MonsterCategory.boss => [ @@ -997,7 +987,7 @@ List> _largeAlertFrames(MonsterCategory category) { r' | | |', r' V | V', r' _/ | \_', - r'|_____|_____|' + r'|_____|_____|', ], [ r' ^!^', @@ -1007,7 +997,7 @@ List> _largeAlertFrames(MonsterCategory category) { r' | | |', r' v | v', r' _\ | /_', - r'|_____|_____|' + r'|_____|_____|', ], ], }; diff --git a/lib/src/core/animation/canvas/canvas_special_composer.dart b/lib/src/core/animation/canvas/canvas_special_composer.dart index afdd1b8..f5b15d7 100644 --- a/lib/src/core/animation/canvas/canvas_special_composer.dart +++ b/lib/src/core/animation/canvas/canvas_special_composer.dart @@ -20,12 +20,18 @@ class CanvasSpecialComposer { ) { return switch (type) { AsciiAnimationType.levelUp => _composeLevelUp(frameIndex, globalTick), - AsciiAnimationType.questComplete => - _composeQuestComplete(frameIndex, globalTick), - AsciiAnimationType.actComplete => - _composeActComplete(frameIndex, globalTick), - AsciiAnimationType.resurrection => - _composeResurrection(frameIndex, globalTick), + AsciiAnimationType.questComplete => _composeQuestComplete( + frameIndex, + globalTick, + ), + AsciiAnimationType.actComplete => _composeActComplete( + frameIndex, + globalTick, + ), + AsciiAnimationType.resurrection => _composeResurrection( + frameIndex, + globalTick, + ), _ => [AsciiLayer.empty()], }; } @@ -44,7 +50,8 @@ class CanvasSpecialComposer { final layers = [ _createEffectBackground(globalTick, '+'), _createCenteredSprite( - _questCompleteFrames[frameIndex % _questCompleteFrames.length]), + _questCompleteFrames[frameIndex % _questCompleteFrames.length], + ), ]; return layers; } @@ -54,7 +61,8 @@ class CanvasSpecialComposer { final layers = [ _createEffectBackground(globalTick, '~'), _createCenteredSprite( - _actCompleteFrames[frameIndex % _actCompleteFrames.length]), + _actCompleteFrames[frameIndex % _actCompleteFrames.length], + ), ]; return layers; } @@ -64,7 +72,8 @@ class CanvasSpecialComposer { final layers = [ _createEffectBackground(globalTick, '.'), _createCenteredSprite( - _resurrectionFrames[frameIndex % _resurrectionFrames.length]), + _resurrectionFrames[frameIndex % _resurrectionFrames.length], + ), ]; return layers; } @@ -119,41 +128,11 @@ class CanvasSpecialComposer { // ============================================================================ const _levelUpFrames = [ - [ - r' * ', - r' \|/ ', - r' o ', - r' /|\ ', - r' / \ ', - ], - [ - r' * * ', - r' \|/ ', - r' O ', - r' ', - r' / \ ', - ], - [ - r' * * * ', - r' \|/ ', - r' O ', - r' <\|/> ', - r' / \ ', - ], - [ - r' * * * * ', - r' LEVEL ', - r' UP! ', - r' \O/ ', - r' / \ ', - ], - [ - r'* * * * *', - r' LEVEL ', - r' UP! ', - r' \O/ ', - r' | | ', - ], + [r' * ', r' \|/ ', r' o ', r' /|\ ', r' / \ '], + [r' * * ', r' \|/ ', r' O ', r' ', r' / \ '], + [r' * * * ', r' \|/ ', r' O ', r' <\|/> ', r' / \ '], + [r' * * * * ', r' LEVEL ', r' UP! ', r' \O/ ', r' / \ '], + [r'* * * * *', r' LEVEL ', r' UP! ', r' \O/ ', r' | | '], ]; // ============================================================================ @@ -161,34 +140,10 @@ const _levelUpFrames = [ // ============================================================================ const _questCompleteFrames = [ - [ - r' [?] ', - r' | ', - r' o ', - r' /|\ ', - r' / \ ', - ], - [ - r' [???] ', - r' | ', - r' o! ', - r' /|\ ', - r' / \ ', - ], - [ - r' [DONE] ', - r' ! ', - r' \o/ ', - r' | ', - r' / \ ', - ], - [ - r' +[DONE]+', - r' \!/ ', - r' \o/ ', - r' | ', - r' / \ ', - ], + [r' [?] ', r' | ', r' o ', r' /|\ ', r' / \ '], + [r' [???] ', r' | ', r' o! ', r' /|\ ', r' / \ '], + [r' [DONE] ', r' ! ', r' \o/ ', r' | ', r' / \ '], + [r' +[DONE]+', r' \!/ ', r' \o/ ', r' | ', r' / \ '], ]; // ============================================================================ @@ -196,34 +151,10 @@ const _questCompleteFrames = [ // ============================================================================ const _actCompleteFrames = [ - [ - r'=========', - r' ACT ', - r' CLEAR ', - r' o ', - r' /|\ ', - ], - [ - r'~~~~~~~~~', - r' ACT ', - r' CLEAR! ', - r' \o/ ', - r' | ', - ], - [ - r'*~*~*~*~*', - r' ACT ', - r' CLEAR!! ', - r' \O/ ', - r' / \ ', - ], - [ - r'*********', - r' ACT ', - r' CLEAR!! ', - r' \O/ ', - r' | | ', - ], + [r'=========', r' ACT ', r' CLEAR ', r' o ', r' /|\ '], + [r'~~~~~~~~~', r' ACT ', r' CLEAR! ', r' \o/ ', r' | '], + [r'*~*~*~*~*', r' ACT ', r' CLEAR!! ', r' \O/ ', r' / \ '], + [r'*********', r' ACT ', r' CLEAR!! ', r' \O/ ', r' | | '], ]; // ============================================================================ @@ -232,38 +163,13 @@ const _actCompleteFrames = [ const _resurrectionFrames = [ // 프레임 1: R.I.P 묘비 - [ - r' ___ ', - r' |RIP| ', - r' | | ', - r'__|___|__', - ], + [r' ___ ', r' |RIP| ', r' | | ', r'__|___|__'], // 프레임 2: 빛 내림 - [ - r' \|/ ', - r' -|R|- ', - r' | | ', - r'__|___|__', - ], + [r' \|/ ', r' -|R|- ', r' | | ', r'__|___|__'], // 프레임 3: 일어남 - [ - r' \o/ ', - r' --|-- ', - r' | | ', - r'__|___|__', - ], + [r' \o/ ', r' --|-- ', r' | | ', r'__|___|__'], // 프레임 4: 서있음 - [ - r' o ', - r' /|\ ', - r' / \ ', - r'_________', - ], + [r' o ', r' /|\ ', r' / \ ', r'_________'], // 프레임 5: 부활 완료 - [ - r' REVIVED ', - r' \o/ ', - r' | ', - r'___/ \___', - ], + [r' REVIVED ', r' \o/ ', r' | ', r'___/ \___'], ]; diff --git a/lib/src/core/animation/canvas/canvas_town_composer.dart b/lib/src/core/animation/canvas/canvas_town_composer.dart index 7ffb22b..b0edd68 100644 --- a/lib/src/core/animation/canvas/canvas_town_composer.dart +++ b/lib/src/core/animation/canvas/canvas_town_composer.dart @@ -63,12 +63,7 @@ class CanvasTownComposer { const shopX = 32; final shopY = frameHeight - cells.length - 1; - return AsciiLayer( - cells: cells, - zIndex: 1, - offsetX: shopX, - offsetY: shopY, - ); + return AsciiLayer(cells: cells, zIndex: 1, offsetX: shopX, offsetY: shopY); } /// 캐릭터 레이어 생성 (z=2) @@ -82,12 +77,7 @@ class CanvasTownComposer { const charX = 25; final charY = frameHeight - cells.length - 1; - return AsciiLayer( - cells: cells, - zIndex: 2, - offsetX: charX, - offsetY: charY, - ); + return AsciiLayer(cells: cells, zIndex: 2, offsetX: charX, offsetY: charY); } /// 문자열 스프라이트를 AsciiCell 2D 배열로 변환 @@ -104,27 +94,11 @@ class CanvasTownComposer { const _shopIdleFrames = [ // 프레임 1: 기본 - [ - r' o ', - r' /|\ ', - r' / \ ', - ], + [r' o ', r' /|\ ', r' / \ '], // 프레임 2: 머리 숙임 - [ - r' o ', - r' /|~ ', - r' / \ ', - ], + [r' o ', r' /|~ ', r' / \ '], // 프레임 3: 물건 보기 - [ - r' o? ', - r' /| ', - r' / \ ', - ], + [r' o? ', r' /| ', r' / \ '], // 프레임 4: 고개 끄덕 - [ - r' o! ', - r' /|\ ', - r' / \ ', - ], + [r' o! ', r' /|\ ', r' / \ '], ]; diff --git a/lib/src/core/animation/character_frames.dart b/lib/src/core/animation/character_frames.dart index 607c9fc..ecfadde 100644 --- a/lib/src/core/animation/character_frames.dart +++ b/lib/src/core/animation/character_frames.dart @@ -62,26 +62,10 @@ CharacterFrame getCharacterFrame(BattlePhase phase, int subFrame) { // 구조: [머리, 몸통+팔, 다리] // ============================================================================ const _idleFrames = [ - CharacterFrame([ - r' o ', - r' /|\ ', - r' / \ ', - ]), - CharacterFrame([ - r' o ', - r' /|\ ', - r' | | ', - ]), - CharacterFrame([ - r' o ', - r' /|\ ', - r' / \ ', - ]), - CharacterFrame([ - r' O ', - r' /|\ ', - r' / \ ', - ]), + CharacterFrame([r' o ', r' /|\ ', r' / \ ']), + CharacterFrame([r' o ', r' /|\ ', r' | | ']), + CharacterFrame([r' o ', r' /|\ ', r' / \ ']), + CharacterFrame([r' O ', r' /|\ ', r' / \ ']), ]; // ============================================================================ @@ -89,21 +73,9 @@ const _idleFrames = [ // 구조: [머리, 몸통+팔, 다리] // ============================================================================ const _prepareFrames = [ - CharacterFrame([ - r' o ', - r' \|\ ', - r' / \ ', - ]), - CharacterFrame([ - r' o_ ', - r' \| ', - r' / \ ', - ]), - CharacterFrame([ - r' o/ ', - r' \| ', - r' / \ ', - ]), + CharacterFrame([r' o ', r' \|\ ', r' / \ ']), + CharacterFrame([r' o_ ', r' \| ', r' / \ ']), + CharacterFrame([r' o/ ', r' \| ', r' / \ ']), ]; // ============================================================================ @@ -112,31 +84,11 @@ const _prepareFrames = [ // 수정: 공격 이펙트를 머리 줄로 통일 (1칸 위로) // ============================================================================ const _attackFrames = [ - CharacterFrame([ - r' o\ ', - r' /| ', - r' / \ ', - ]), - CharacterFrame([ - r' o- ', - r' /| ', - r' / \ ', - ]), - CharacterFrame([ - r' o-- ', - r' /| ', - r' / \ ', - ]), - CharacterFrame([ - r' o-=>', - r' /| ', - r' / \ ', - ]), - CharacterFrame([ - r' o ', - r' /|\ ', - r' / \ ', - ]), + CharacterFrame([r' o\ ', r' /| ', r' / \ ']), + CharacterFrame([r' o- ', r' /| ', r' / \ ']), + CharacterFrame([r' o-- ', r' /| ', r' / \ ']), + CharacterFrame([r' o-=>', r' /| ', r' / \ ']), + CharacterFrame([r' o ', r' /|\ ', r' / \ ']), ]; // ============================================================================ @@ -145,21 +97,9 @@ const _attackFrames = [ // 수정: 히트 이펙트를 머리 줄로 통일 (1칸 위로) // ============================================================================ const _hitFrames = [ - CharacterFrame([ - r' o-* ', - r' /| ', - r' / \ ', - ]), - CharacterFrame([ - r' o=* ', - r' /| ', - r' / \ ', - ]), - CharacterFrame([ - r' o~* ', - r' /| ', - r' / \ ', - ]), + CharacterFrame([r' o-* ', r' /| ', r' / \ ']), + CharacterFrame([r' o=* ', r' /| ', r' / \ ']), + CharacterFrame([r' o~* ', r' /| ', r' / \ ']), ]; // ============================================================================ @@ -167,19 +107,7 @@ const _hitFrames = [ // 구조: [머리, 몸통+팔, 다리] // ============================================================================ const _recoverFrames = [ - CharacterFrame([ - r' o ', - r' /|\ ', - r' | ', - ]), - CharacterFrame([ - r' o ', - r' /|\ ', - r' / \ ', - ]), - CharacterFrame([ - r' o ', - r' /|\ ', - r' / \ ', - ]), + CharacterFrame([r' o ', r' /|\ ', r' | ']), + CharacterFrame([r' o ', r' /|\ ', r' / \ ']), + CharacterFrame([r' o ', r' /|\ ', r' / \ ']), ]; diff --git a/lib/src/core/animation/monster_colors.dart b/lib/src/core/animation/monster_colors.dart deleted file mode 100644 index 046b659..0000000 --- a/lib/src/core/animation/monster_colors.dart +++ /dev/null @@ -1,192 +0,0 @@ -// 몬스터 카테고리별 색상 시스템 -// 각 몬스터 카테고리에 따라 다른 색상 적용 - -import 'dart:ui'; - -/// 몬스터 카테고리 (ascii_animation_data.dart의 MonsterCategory와 매칭) -enum MonsterColorCategory { - beast, - insect, - humanoid, - undead, - dragon, - slime, - demon, -} - -/// 몬스터 색상 정보 -class MonsterColors { - const MonsterColors({ - required this.normal, - required this.hit, - }); - - /// 일반 상태 색상 - final Color normal; - - /// 피격 상태 색상 - final Color hit; -} - -/// 카테고리별 몬스터 색상 반환 -MonsterColors getMonsterColors(MonsterColorCategory category) { - return switch (category) { - MonsterColorCategory.beast => const MonsterColors( - normal: Color(0xFF00FF00), // 녹색 - hit: Color(0xFFFF0000), // 빨강 - ), - MonsterColorCategory.insect => const MonsterColors( - normal: Color(0xFFFFFF00), // 노랑 - hit: Color(0xFFFF6600), // 주황 - ), - MonsterColorCategory.humanoid => const MonsterColors( - normal: Color(0xFF00FFFF), // 시안 - hit: Color(0xFFFF00FF), // 마젠타 - ), - MonsterColorCategory.undead => const MonsterColors( - normal: Color(0xFF9966FF), // 보라 - hit: Color(0xFFCCCCCC), // 회색 - ), - MonsterColorCategory.dragon => const MonsterColors( - normal: Color(0xFFFF6600), // 주황 - hit: Color(0xFFFFFF00), // 노랑 - ), - MonsterColorCategory.slime => const MonsterColors( - normal: Color(0xFF66FF66), // 연녹색 - hit: Color(0xFF00CC00), // 진녹색 - ), - MonsterColorCategory.demon => const MonsterColors( - normal: Color(0xFFFF0066), // 핑크 - hit: Color(0xFFFFFFFF), // 흰색 - ), - }; -} - -/// 몬스터 기본 이름에서 색상 카테고리 추론 -/// -/// ascii_animation_data.dart의 getMonsterCategory 결과를 변환 -MonsterColorCategory getMonsterColorCategory(String? baseName) { - if (baseName == null || baseName.isEmpty) { - return MonsterColorCategory.beast; - } - - final lower = baseName.toLowerCase(); - - // insect (곤충류) - if (_matchesAny(lower, _insectKeywords)) { - return MonsterColorCategory.insect; - } - - // undead (언데드) - if (_matchesAny(lower, _undeadKeywords)) { - return MonsterColorCategory.undead; - } - - // dragon (드래곤류) - if (_matchesAny(lower, _dragonKeywords)) { - return MonsterColorCategory.dragon; - } - - // slime (슬라임류) - if (_matchesAny(lower, _slimeKeywords)) { - return MonsterColorCategory.slime; - } - - // demon (악마류) - if (_matchesAny(lower, _demonKeywords)) { - return MonsterColorCategory.demon; - } - - // humanoid (인간형) - if (_matchesAny(lower, _humanoidKeywords)) { - return MonsterColorCategory.humanoid; - } - - // 기본은 beast - return MonsterColorCategory.beast; -} - -bool _matchesAny(String text, List keywords) { - return keywords.any((kw) => text.contains(kw)); -} - -const _insectKeywords = [ - 'bug', - 'beetle', - 'spider', - 'ant', - 'bee', - 'wasp', - 'moth', - 'worm', - 'larva', - 'crawler', - 'centipede', - 'scorpion', -]; - -const _undeadKeywords = [ - 'zombie', - 'skeleton', - 'ghost', - 'wraith', - 'vampire', - 'lich', - 'specter', - 'phantom', - 'revenant', - 'undead', - 'corpse', - 'bone', -]; - -const _dragonKeywords = [ - 'dragon', - 'drake', - 'wyrm', - 'wyvern', - 'serpent', - 'hydra', - 'basilisk', -]; - -const _slimeKeywords = [ - 'slime', - 'ooze', - 'blob', - 'jelly', - 'pudding', - 'gel', - 'goo', -]; - -const _demonKeywords = [ - 'demon', - 'devil', - 'imp', - 'fiend', - 'daemon', - 'succubus', - 'incubus', - 'hell', - 'infernal', -]; - -const _humanoidKeywords = [ - 'goblin', - 'orc', - 'troll', - 'ogre', - 'giant', - 'bandit', - 'knight', - 'mage', - 'wizard', - 'warrior', - 'guard', - 'soldier', - 'cultist', - 'hacker', - 'admin', - 'user', -]; diff --git a/lib/src/core/animation/weapon_category.dart b/lib/src/core/animation/weapon_category.dart index ccf886e..0b5ebe7 100644 --- a/lib/src/core/animation/weapon_category.dart +++ b/lib/src/core/animation/weapon_category.dart @@ -65,12 +65,7 @@ bool _matchesAny(String text, List keywords) { // 카테고리별 키워드 목록 -const _cosmicKeywords = [ - 'dyson', - 'black hole', - 'universe', - 'singularity', -]; +const _cosmicKeywords = ['dyson', 'black hole', 'universe', 'singularity']; const _cableKeywords = [ 'cable', diff --git a/lib/src/core/animation/weapon_effects.dart b/lib/src/core/animation/weapon_effects.dart index 8556e52..ad9578d 100644 --- a/lib/src/core/animation/weapon_effects.dart +++ b/lib/src/core/animation/weapon_effects.dart @@ -58,7 +58,7 @@ const _bluntEffect = WeaponEffect( [r' _/ ', r' / ', r'/ '], [r' /__ ', r'/ ', r' '], [r'/__ ', r' ', r' '], - [r'/__=>', r' ', r' '], + [r'/__=>', r' ', r' '], ], hitFrames: [ [r' *BASH* ', r'/__=> ', r' '], diff --git a/lib/src/core/engine/combat_calculator.dart b/lib/src/core/engine/combat_calculator.dart index ee34680..2ccfa3d 100644 --- a/lib/src/core/engine/combat_calculator.dart +++ b/lib/src/core/engine/combat_calculator.dart @@ -24,7 +24,8 @@ class CombatCalculator { /// [attacker] 공격자 (플레이어) 스탯 /// [defender] 방어자 (몬스터) 스탯 /// Returns: 공격 결과 및 업데이트된 몬스터 스탯 - ({AttackResult result, MonsterCombatStats updatedDefender}) playerAttackMonster({ + ({AttackResult result, MonsterCombatStats updatedDefender}) + playerAttackMonster({ required CombatStats attacker, required MonsterCombatStats defender, }) { @@ -178,7 +179,8 @@ class CombatCalculator { } // 전투 종료 체크 - final isCombatOver = currentPlayerStats.isDead || currentMonsterStats.isDead; + final isCombatOver = + currentPlayerStats.isDead || currentMonsterStats.isDead; final isPlayerVictory = isCombatOver && currentMonsterStats.isDead; return CombatTurnResult( @@ -206,7 +208,8 @@ class CombatCalculator { // 플레이어 DPS (초당 데미지) final playerDamagePerHit = math.max(1, player.atk - monster.def * 0.5); final playerHitsPerSecond = 1000 / player.attackDelayMs; - final playerDps = playerDamagePerHit * playerHitsPerSecond * player.accuracy; + final playerDps = + playerDamagePerHit * playerHitsPerSecond * player.accuracy; // 몬스터를 처치하는 데 필요한 시간 (밀리초) final timeToKillMonster = (monster.hpMax / playerDps * 1000).round(); @@ -225,17 +228,20 @@ class CombatCalculator { // 플레이어 예상 생존 시간 final monsterDamagePerHit = math.max(1, monster.atk - player.def * 0.5); final monsterHitsPerSecond = 1000 / monster.attackDelayMs; - final monsterDps = monsterDamagePerHit * monsterHitsPerSecond * monster.accuracy; + final monsterDps = + monsterDamagePerHit * monsterHitsPerSecond * monster.accuracy; final playerSurvivalTime = player.hpCurrent / monsterDps; // 몬스터 예상 생존 시간 final playerDamagePerHit = math.max(1, player.atk - monster.def * 0.5); final playerHitsPerSecond = 1000 / player.attackDelayMs; - final playerDps = playerDamagePerHit * playerHitsPerSecond * player.accuracy; + final playerDps = + playerDamagePerHit * playerHitsPerSecond * player.accuracy; final monsterSurvivalTime = monster.hpCurrent / playerDps; // 난이도 = 몬스터 생존시간 / (플레이어 생존시간 + 몬스터 생존시간) - final difficulty = monsterSurvivalTime / (playerSurvivalTime + monsterSurvivalTime); + final difficulty = + monsterSurvivalTime / (playerSurvivalTime + monsterSurvivalTime); return difficulty.clamp(0.0, 1.0); } diff --git a/lib/src/core/engine/item_service.dart b/lib/src/core/engine/item_service.dart index 769ee90..94ce1fa 100644 --- a/lib/src/core/engine/item_service.dart +++ b/lib/src/core/engine/item_service.dart @@ -45,7 +45,9 @@ class ItemService { // 교체할 슬롯이 있으면 해당 아이템 무게 제외 if (replacingSlot != null) { - final existingItem = currentItems.where((i) => i.slot == replacingSlot).firstOrNull; + final existingItem = currentItems + .where((i) => i.slot == replacingSlot) + .firstOrNull; if (existingItem != null) { currentWeight -= existingItem.weight; } @@ -70,7 +72,8 @@ class ItemService { if (roll < legendaryChance) return ItemRarity.legendary; if (roll < legendaryChance + epicChance) return ItemRarity.epic; - if (roll < legendaryChance + epicChance + rareChance) return ItemRarity.rare; + if (roll < legendaryChance + epicChance + rareChance) + return ItemRarity.rare; if (roll < legendaryChance + epicChance + rareChance + uncommonChance) { return ItemRarity.uncommon; } @@ -112,7 +115,8 @@ class ItemService { // 공속 결정 (600ms ~ 1500ms 범위) // 희귀도가 높을수록 공속 변동 폭 증가 - final speedVariance = 300 + rarity.index * 100; // Common: 300, Legendary: 700 + final speedVariance = + 300 + rarity.index * 100; // Common: 300, Legendary: 700 final speedOffset = rng.nextInt(speedVariance * 2) - speedVariance; final attackSpeed = (1000 + speedOffset).clamp(600, 1500); @@ -133,14 +137,15 @@ class ItemService { ItemStats _generateShieldStats(int baseValue, ItemRarity rarity) { final blockBonus = 0.05 + rarity.index * 0.02; - return ItemStats( - def: baseValue ~/ 2, - blockRate: blockBonus, - ); + return ItemStats(def: baseValue ~/ 2, blockRate: blockBonus); } /// 방어구 스탯 생성 - ItemStats _generateArmorStats(int baseValue, ItemRarity rarity, EquipmentSlot slot) { + ItemStats _generateArmorStats( + int baseValue, + ItemRarity rarity, + EquipmentSlot slot, + ) { // 슬롯별 방어력 가중치 final defMultiplier = switch (slot) { EquipmentSlot.hauberk => 1.5, // 갑옷류 최고 @@ -161,11 +166,7 @@ class ItemService { final hpBonus = rarity.index >= ItemRarity.rare.index ? baseValue ~/ 2 : 0; final evasionBonus = rarity.index >= ItemRarity.epic.index ? 0.01 : 0.0; - return ItemStats( - def: def, - hpBonus: hpBonus, - evasion: evasionBonus, - ); + return ItemStats(def: def, hpBonus: hpBonus, evasion: evasionBonus); } // ============================================================================ @@ -173,10 +174,7 @@ class ItemService { // ============================================================================ /// 무게 계산 (레벨/슬롯 기반) - int calculateWeight({ - required int level, - required EquipmentSlot slot, - }) { + int calculateWeight({required int level, required EquipmentSlot slot}) { // 슬롯별 기본 무게 final baseWeight = switch (slot) { EquipmentSlot.weapon => 10, @@ -209,7 +207,11 @@ class ItemService { ItemRarity? rarity, }) { final itemRarity = rarity ?? determineRarity(level); - final stats = generateItemStats(level: level, rarity: itemRarity, slot: slot); + final stats = generateItemStats( + level: level, + rarity: itemRarity, + slot: slot, + ); final weight = calculateWeight(level: level, slot: slot); return EquipmentItem( @@ -253,7 +255,8 @@ class ItemService { score += stats.mpBonus; // 능력치 보너스 (가중치 5배) - score += (stats.strBonus + + score += + (stats.strBonus + stats.conBonus + stats.dexBonus + stats.intBonus + diff --git a/lib/src/core/engine/potion_service.dart b/lib/src/core/engine/potion_service.dart index a9a66d0..454678b 100644 --- a/lib/src/core/engine/potion_service.dart +++ b/lib/src/core/engine/potion_service.dart @@ -238,12 +238,16 @@ class PotionService { }) { final potion = PotionData.getById(potionId); if (potion == null) { - return PotionPurchaseResult.failed(PotionPurchaseFailReason.potionNotFound); + return PotionPurchaseResult.failed( + PotionPurchaseFailReason.potionNotFound, + ); } final totalCost = potion.price * count; if (gold < totalCost) { - return PotionPurchaseResult.failed(PotionPurchaseFailReason.insufficientGold); + return PotionPurchaseResult.failed( + PotionPurchaseFailReason.insufficientGold, + ); } final newInventory = inventory.addPotion(potionId, count); @@ -277,13 +281,17 @@ class PotionService { final mpPotion = PotionData.getMpPotionByTier(tier); if (hpPotion == null && mpPotion == null) { - return PotionPurchaseResult.failed(PotionPurchaseFailReason.potionNotFound); + return PotionPurchaseResult.failed( + PotionPurchaseFailReason.potionNotFound, + ); } // 사용 가능 골드 final spendableGold = (gold * spendRatio).floor(); if (spendableGold <= 0) { - return PotionPurchaseResult.failed(PotionPurchaseFailReason.insufficientGold); + return PotionPurchaseResult.failed( + PotionPurchaseFailReason.insufficientGold, + ); } var currentInventory = inventory; @@ -317,7 +325,9 @@ class PotionService { } if (totalSpent == 0) { - return PotionPurchaseResult.failed(PotionPurchaseFailReason.insufficientGold); + return PotionPurchaseResult.failed( + PotionPurchaseFailReason.insufficientGold, + ); } return PotionPurchaseResult( @@ -426,10 +436,7 @@ class PotionUseResult { /// 실패 결과 생성 factory PotionUseResult.failed(PotionUseFailReason reason) { - return PotionUseResult( - success: false, - failReason: reason, - ); + return PotionUseResult(success: false, failReason: reason); } } @@ -480,10 +487,7 @@ class PotionPurchaseResult { /// 실패 결과 생성 factory PotionPurchaseResult.failed(PotionPurchaseFailReason reason) { - return PotionPurchaseResult( - success: false, - failReason: reason, - ); + return PotionPurchaseResult(success: false, failReason: reason); } } diff --git a/lib/src/core/engine/progress_service.dart b/lib/src/core/engine/progress_service.dart index 98995ce..78f9df4 100644 --- a/lib/src/core/engine/progress_service.dart +++ b/lib/src/core/engine/progress_service.dart @@ -112,7 +112,9 @@ class ProgressService { ), plotStageCount: 1, // Prologue questCount: 0, - plotHistory: [HistoryEntry(caption: l10n.taskPrologue, isComplete: false)], + plotHistory: [ + HistoryEntry(caption: l10n.taskPrologue, isComplete: false), + ], questHistory: const [], ); @@ -156,13 +158,17 @@ class ProgressService { // 스킬 시스템 시간 업데이트 (Phase 3) final skillService = SkillService(rng: state.rng); - var skillSystem = skillService.updateElapsedTime(state.skillSystem, clamped); + var skillSystem = skillService.updateElapsedTime( + state.skillSystem, + clamped, + ); // 만료된 버프 정리 skillSystem = skillService.cleanupExpiredBuffs(skillSystem); // 비전투 시 MP 회복 - final isInCombat = progress.currentTask.type == TaskType.kill && + final isInCombat = + progress.currentTask.type == TaskType.kill && progress.currentCombat != null && progress.currentCombat!.isActive; @@ -173,7 +179,10 @@ class ProgressService { wis: nextState.stats.wis, ); if (mpRegen > 0) { - final newMp = (nextState.stats.mp + mpRegen).clamp(0, nextState.stats.mpMax); + final newMp = (nextState.stats.mp + mpRegen).clamp( + 0, + nextState.stats.mpMax, + ); nextState = nextState.copyWith( stats: nextState.stats.copyWith(mpCurrent: newMp), ); @@ -193,7 +202,9 @@ class ProgressService { var updatedCombat = progress.currentCombat; var updatedSkillSystem = nextState.skillSystem; var updatedPotionInventory = nextState.potionInventory; - if (progress.currentTask.type == TaskType.kill && updatedCombat != null && updatedCombat.isActive) { + if (progress.currentTask.type == TaskType.kill && + updatedCombat != null && + updatedCombat.isActive) { final combatResult = _processCombatTickWithSkills( nextState, updatedCombat, @@ -480,7 +491,8 @@ class ProgressService { final questMonster = state.progress.currentQuestMonster; final questMonsterData = questMonster?.monsterData; final questLevel = questMonsterData != null - ? int.tryParse(questMonsterData.split('|').elementAtOrNull(1) ?? '') ?? 0 + ? int.tryParse(questMonsterData.split('|').elementAtOrNull(1) ?? '') ?? + 0 : null; final monsterResult = pq_logic.monsterTask( @@ -501,10 +513,9 @@ class ProgressService { // 전투용 몬스터 레벨 조정 (밸런스) // config의 raw 레벨이 플레이어보다 너무 높으면 전투가 불가능 // 플레이어 레벨 ±3 범위로 제한 (최소 1) - final effectiveMonsterLevel = monsterResult.level.clamp( - math.max(1, level - 3), - level + 3, - ).toInt(); + final effectiveMonsterLevel = monsterResult.level + .clamp(math.max(1, level - 3), level + 3) + .toInt(); final monsterCombatStats = MonsterCombatStats.fromLevel( name: monsterResult.displayName, @@ -907,7 +918,8 @@ class ProgressService { if (hasItemsToSell) { // 다음 아이템 판매 태스크 시작 final nextItem = items.first; - final itemDesc = l10n.indefiniteL10n(nextItem.name, nextItem.count); + final translatedName = l10n.translateItemNameL10n(nextItem.name); + final itemDesc = l10n.indefiniteL10n(translatedName, nextItem.count); final taskResult = pq_logic.startTask( state.progress, l10n.taskSelling(itemDesc), @@ -945,7 +957,8 @@ class ProgressService { CombatState combat, SkillSystemState skillSystem, PotionInventory? potionInventory, - }) _processCombatTickWithSkills( + }) + _processCombatTickWithSkills( GameState state, CombatState combat, SkillSystemState skillSystem, @@ -988,12 +1001,14 @@ class ProgressService { dotDamageThisTick += damage; // DOT 데미지 이벤트 생성 - newEvents.add(CombatEvent.dotTick( - timestamp: timestamp, - skillName: dot.skillId, - damage: damage, - targetName: monsterStats.name, - )); + newEvents.add( + CombatEvent.dotTick( + timestamp: timestamp, + skillName: dot.skillId, + damage: damage, + targetName: monsterStats.name, + ), + ); } // 만료되지 않은 DOT만 유지 @@ -1004,8 +1019,10 @@ class ProgressService { // DOT 데미지 적용 if (dotDamageThisTick > 0 && monsterStats.isAlive) { - final newMonsterHp = (monsterStats.hpCurrent - dotDamageThisTick) - .clamp(0, monsterStats.hpMax); + final newMonsterHp = (monsterStats.hpCurrent - dotDamageThisTick).clamp( + 0, + monsterStats.hpMax, + ); monsterStats = monsterStats.copyWith(hpCurrent: newMonsterHp); totalDamageDealt += dotDamageThisTick; } @@ -1024,8 +1041,7 @@ class ProgressService { playerLevel: state.traits.level, ); - if (emergencyPotion != null && - !usedPotionTypes.contains(PotionType.hp)) { + if (emergencyPotion != null && !usedPotionTypes.contains(PotionType.hp)) { final result = potionService.usePotion( potionId: emergencyPotion.id, inventory: state.potionInventory, @@ -1040,25 +1056,27 @@ class ProgressService { usedPotionTypes = {...usedPotionTypes, PotionType.hp}; updatedPotionInventory = result.newInventory; - newEvents.add(CombatEvent.playerPotion( - timestamp: timestamp, - potionName: emergencyPotion.name, - healAmount: result.healedAmount, - isHp: true, - )); + newEvents.add( + CombatEvent.playerPotion( + timestamp: timestamp, + potionName: emergencyPotion.name, + healAmount: result.healedAmount, + isHp: true, + ), + ); } } } // 플레이어 공격 체크 if (playerAccumulator >= playerStats.attackDelayMs) { - // 스킬 자동 선택 - final availableSkillIds = updatedSkillSystem.skillStates - .map((s) => s.skillId) - .toList(); - // 기본 스킬이 없으면 기본 스킬 추가 + // SpellBook에서 사용 가능한 스킬 ID 목록 조회 + var availableSkillIds = skillService.getAvailableSkillIdsFromSpellBook( + state.spellBook, + ); + // SpellBook에 스킬이 없으면 기본 스킬 사용 if (availableSkillIds.isEmpty) { - availableSkillIds.addAll(SkillData.defaultSkillIds); + availableSkillIds = SkillData.defaultSkillIds; } final selectedSkill = skillService.selectAutoSkill( @@ -1070,12 +1088,18 @@ class ProgressService { ); if (selectedSkill != null && selectedSkill.isAttack) { - // 공격 스킬 사용 - final skillResult = skillService.useAttackSkill( + // 스펠 랭크 조회 (SpellBook 기반) + final spellRank = skillService.getSkillRankFromSpellBook( + state.spellBook, + selectedSkill.id, + ); + // 랭크 스케일링 적용된 공격 스킬 사용 + final skillResult = skillService.useAttackSkillWithRank( skill: selectedSkill, player: playerStats, monster: monsterStats, skillSystem: updatedSkillSystem, + rank: spellRank, ); playerStats = skillResult.updatedPlayer; monsterStats = skillResult.updatedMonster; @@ -1083,12 +1107,14 @@ class ProgressService { updatedSkillSystem = skillResult.updatedSkillSystem; // 스킬 공격 이벤트 생성 - newEvents.add(CombatEvent.playerSkill( - timestamp: timestamp, - skillName: selectedSkill.name, - damage: skillResult.result.damage, - targetName: monsterStats.name, - )); + newEvents.add( + CombatEvent.playerSkill( + timestamp: timestamp, + skillName: selectedSkill.name, + damage: skillResult.result.damage, + targetName: monsterStats.name, + ), + ); } else if (selectedSkill != null && selectedSkill.isDot) { // DOT 스킬 사용 final skillResult = skillService.useDotSkill( @@ -1107,12 +1133,14 @@ class ProgressService { } // DOT 스킬 사용 이벤트 생성 - newEvents.add(CombatEvent.playerSkill( - timestamp: timestamp, - skillName: selectedSkill.name, - damage: skillResult.result.damage, - targetName: monsterStats.name, - )); + newEvents.add( + CombatEvent.playerSkill( + timestamp: timestamp, + skillName: selectedSkill.name, + damage: skillResult.result.damage, + targetName: monsterStats.name, + ), + ); } else if (selectedSkill != null && selectedSkill.isHeal) { // 회복 스킬 사용 final skillResult = skillService.useHealSkill( @@ -1124,11 +1152,13 @@ class ProgressService { updatedSkillSystem = skillResult.updatedSkillSystem; // 회복 이벤트 생성 - newEvents.add(CombatEvent.playerHeal( - timestamp: timestamp, - healAmount: skillResult.result.healedAmount, - skillName: selectedSkill.name, - )); + newEvents.add( + CombatEvent.playerHeal( + timestamp: timestamp, + healAmount: skillResult.result.healedAmount, + skillName: selectedSkill.name, + ), + ); } else if (selectedSkill != null && selectedSkill.isBuff) { // 버프 스킬 사용 final skillResult = skillService.useBuffSkill( @@ -1140,10 +1170,12 @@ class ProgressService { updatedSkillSystem = skillResult.updatedSkillSystem; // 버프 이벤트 생성 - newEvents.add(CombatEvent.playerBuff( - timestamp: timestamp, - skillName: selectedSkill.name, - )); + newEvents.add( + CombatEvent.playerBuff( + timestamp: timestamp, + skillName: selectedSkill.name, + ), + ); } else { // 일반 공격 final attackResult = calculator.playerAttackMonster( @@ -1156,17 +1188,21 @@ class ProgressService { // 일반 공격 이벤트 생성 final result = attackResult.result; if (result.isEvaded) { - newEvents.add(CombatEvent.monsterEvade( - timestamp: timestamp, - targetName: monsterStats.name, - )); + newEvents.add( + CombatEvent.monsterEvade( + timestamp: timestamp, + targetName: monsterStats.name, + ), + ); } else { - newEvents.add(CombatEvent.playerAttack( - timestamp: timestamp, - damage: result.damage, - targetName: monsterStats.name, - isCritical: result.isCritical, - )); + newEvents.add( + CombatEvent.playerAttack( + timestamp: timestamp, + damage: result.damage, + targetName: monsterStats.name, + isCritical: result.isCritical, + ), + ); } } @@ -1175,7 +1211,8 @@ class ProgressService { } // 몬스터가 살아있으면 반격 - if (monsterStats.isAlive && monsterAccumulator >= monsterStats.attackDelayMs) { + if (monsterStats.isAlive && + monsterAccumulator >= monsterStats.attackDelayMs) { final attackResult = calculator.monsterAttackPlayer( attacker: monsterStats, defender: playerStats, @@ -1187,28 +1224,36 @@ class ProgressService { // 몬스터 공격 이벤트 생성 final result = attackResult.result; if (result.isEvaded) { - newEvents.add(CombatEvent.playerEvade( - timestamp: timestamp, - attackerName: monsterStats.name, - )); + newEvents.add( + CombatEvent.playerEvade( + timestamp: timestamp, + attackerName: monsterStats.name, + ), + ); } else if (result.isBlocked) { - newEvents.add(CombatEvent.playerBlock( - timestamp: timestamp, - reducedDamage: result.damage, - attackerName: monsterStats.name, - )); + newEvents.add( + CombatEvent.playerBlock( + timestamp: timestamp, + reducedDamage: result.damage, + attackerName: monsterStats.name, + ), + ); } else if (result.isParried) { - newEvents.add(CombatEvent.playerParry( - timestamp: timestamp, - reducedDamage: result.damage, - attackerName: monsterStats.name, - )); + newEvents.add( + CombatEvent.playerParry( + timestamp: timestamp, + reducedDamage: result.damage, + attackerName: monsterStats.name, + ), + ); } else { - newEvents.add(CombatEvent.monsterAttack( - timestamp: timestamp, - damage: result.damage, - attackerName: monsterStats.name, - )); + newEvents.add( + CombatEvent.monsterAttack( + timestamp: timestamp, + damage: result.damage, + attackerName: monsterStats.name, + ), + ); } } @@ -1285,9 +1330,7 @@ class ProgressService { ); // 전투 상태 초기화 - final progress = state.progress.copyWith( - currentCombat: null, - ); + final progress = state.progress.copyWith(currentCombat: null); return state.copyWith( equipment: emptyEquipment, diff --git a/lib/src/core/engine/resurrection_service.dart b/lib/src/core/engine/resurrection_service.dart index 64133ce..02b29ae 100644 --- a/lib/src/core/engine/resurrection_service.dart +++ b/lib/src/core/engine/resurrection_service.dart @@ -1,6 +1,7 @@ import 'dart:math'; import 'package:askiineverdie/data/class_data.dart'; +import 'package:askiineverdie/data/game_text_l10n.dart' as l10n; import 'package:askiineverdie/data/race_data.dart'; import 'package:askiineverdie/src/core/engine/shop_service.dart'; import 'package:askiineverdie/src/core/model/class_traits.dart'; @@ -75,9 +76,7 @@ class ResurrectionService { ); // 전투 상태 초기화 - final progress = state.progress.copyWith( - currentCombat: null, - ); + final progress = state.progress.copyWith(currentCombat: null); return state.copyWith( equipment: newEquipment, @@ -109,9 +108,7 @@ class ResurrectionService { // 장비 적용 var nextState = state.copyWith( equipment: autoBuyResult.updatedEquipment, - inventory: state.inventory.copyWith( - gold: autoBuyResult.remainingGold, - ), + inventory: state.inventory.copyWith(gold: autoBuyResult.remainingGold), ); // 2. 전체 HP/MP 계산 (장비 + 종족 + 클래스 보너스 포함) @@ -137,22 +134,22 @@ class ResurrectionService { // 4. 부활 후 태스크 시퀀스 설정 (큐에 추가) // 순서: 마을 귀환 → 샵 정비 → 사냥터 이동 → 전투 final resurrectionQueue = [ - const QueueEntry( + QueueEntry( kind: QueueKind.task, durationMillis: 3000, // 3초 - caption: 'Returning to town...', + caption: l10n.taskReturningToTown, taskType: TaskType.neutral, // 걷기 애니메이션 ), - const QueueEntry( + QueueEntry( kind: QueueKind.task, durationMillis: 3000, // 3초 - caption: 'Restocking at shop...', + caption: l10n.taskRestockingAtShop, taskType: TaskType.market, // town 애니메이션 ), - const QueueEntry( + QueueEntry( kind: QueueKind.task, durationMillis: 2000, // 2초 - caption: 'Heading to hunting grounds...', + caption: l10n.taskHeadingToHuntingGrounds, taskType: TaskType.neutral, // 걷기 애니메이션 ), ]; @@ -164,10 +161,7 @@ class ResurrectionService { ), // 현재 태스크를 빈 상태로 설정하여 큐에서 다음 태스크를 가져오도록 함 progress: nextState.progress.copyWith( - currentTask: const TaskInfo( - caption: '', - type: TaskType.neutral, - ), + currentTask: const TaskInfo(caption: '', type: TaskType.neutral), task: const ProgressBarState( position: 0, max: 1, // 즉시 완료되어 큐에서 다음 태스크 가져옴 diff --git a/lib/src/core/engine/shop_service.dart b/lib/src/core/engine/shop_service.dart index 557ff55..5bdadf1 100644 --- a/lib/src/core/engine/shop_service.dart +++ b/lib/src/core/engine/shop_service.dart @@ -126,7 +126,11 @@ class ShopService { } /// 슬롯과 레벨에 따른 스탯 생성 - ItemStats _generateItemStats(EquipmentSlot slot, int level, ItemRarity rarity) { + ItemStats _generateItemStats( + EquipmentSlot slot, + int level, + ItemRarity rarity, + ) { final multiplier = rarity.multiplier; final baseValue = (level * multiplier).round(); @@ -145,10 +149,7 @@ class ShopService { magDef: baseValue ~/ 2, intBonus: level ~/ 10, ), - EquipmentSlot.hauberk => ItemStats( - def: baseValue, - hpBonus: level * 2, - ), + EquipmentSlot.hauberk => ItemStats(def: baseValue, hpBonus: level * 2), EquipmentSlot.brassairts => ItemStats( def: baseValue ~/ 2, strBonus: level ~/ 15, @@ -273,11 +274,7 @@ class ShopService { /// 장비 판매 SellResult sellItem(EquipmentItem item, int currentGold) { final price = calculateSellPrice(item); - return SellResult( - item: item, - price: price, - newGold: currentGold + price, - ); + return SellResult(item: item, price: price, newGold: currentGold + price); } } diff --git a/lib/src/core/engine/skill_service.dart b/lib/src/core/engine/skill_service.dart index de54ba3..399ac41 100644 --- a/lib/src/core/engine/skill_service.dart +++ b/lib/src/core/engine/skill_service.dart @@ -4,6 +4,7 @@ import 'package:askiineverdie/src/core/model/game_state.dart'; import 'package:askiineverdie/src/core/model/monster_combat_stats.dart'; import 'package:askiineverdie/src/core/model/skill.dart'; import 'package:askiineverdie/src/core/util/deterministic_random.dart'; +import 'package:askiineverdie/src/core/util/roman.dart'; /// 스킬 시스템 서비스 /// @@ -30,7 +31,8 @@ class SkillService { // 쿨타임 체크 final skillState = skillSystem.getSkillState(skill.id); - if (skillState != null && !skillState.isReady(skillSystem.elapsedMs, skill.cooldownMs)) { + if (skillState != null && + !skillState.isReady(skillSystem.elapsedMs, skill.cooldownMs)) { return SkillFailReason.onCooldown; } @@ -49,7 +51,8 @@ class SkillService { CombatStats updatedPlayer, MonsterCombatStats updatedMonster, SkillSystemState updatedSkillSystem, - }) useAttackSkill({ + }) + useAttackSkill({ required Skill skill, required CombatStats player, required MonsterCombatStats monster, @@ -66,7 +69,9 @@ class SkillService { final effectiveMonsterDef = monster.def * (1 - skill.targetDefReduction); // 최종 데미지 계산 (방어력 감산) - final finalDamage = (buffedDamage - effectiveMonsterDef * 0.5).round().clamp(1, 9999); + final finalDamage = (buffedDamage - effectiveMonsterDef * 0.5) + .round() + .clamp(1, 9999); // 몬스터에 데미지 적용 var updatedMonster = monster.applyDamage(finalDamage); @@ -79,17 +84,15 @@ class SkillService { } // MP 소모 - updatedPlayer = updatedPlayer.withMp(updatedPlayer.mpCurrent - skill.mpCost); + updatedPlayer = updatedPlayer.withMp( + updatedPlayer.mpCurrent - skill.mpCost, + ); // 스킬 상태 업데이트 (쿨타임 시작) final updatedSkillSystem = _updateSkillCooldown(skillSystem, skill.id); return ( - result: SkillUseResult( - skill: skill, - success: true, - damage: finalDamage, - ), + result: SkillUseResult(skill: skill, success: true, damage: finalDamage), updatedPlayer: updatedPlayer, updatedMonster: updatedMonster, updatedSkillSystem: updatedSkillSystem, @@ -101,7 +104,8 @@ class SkillService { SkillUseResult result, CombatStats updatedPlayer, SkillSystemState updatedSkillSystem, - }) useHealSkill({ + }) + useHealSkill({ required Skill skill, required CombatStats player, required SkillSystemState skillSystem, @@ -116,7 +120,9 @@ class SkillService { var updatedPlayer = player.applyHeal(healAmount); // MP 소모 - updatedPlayer = updatedPlayer.withMp(updatedPlayer.mpCurrent - skill.mpCost); + updatedPlayer = updatedPlayer.withMp( + updatedPlayer.mpCurrent - skill.mpCost, + ); // 스킬 상태 업데이트 final updatedSkillSystem = _updateSkillCooldown(skillSystem, skill.id); @@ -137,7 +143,8 @@ class SkillService { SkillUseResult result, CombatStats updatedPlayer, SkillSystemState updatedSkillSystem, - }) useBuffSkill({ + }) + useBuffSkill({ required Skill skill, required CombatStats player, required SkillSystemState skillSystem, @@ -158,10 +165,11 @@ class SkillService { ); // 기존 같은 버프 제거 후 새 버프 추가 - final updatedBuffs = skillSystem.activeBuffs - .where((b) => b.effect.id != skill.buff!.id) - .toList() - ..add(newBuff); + final updatedBuffs = + skillSystem.activeBuffs + .where((b) => b.effect.id != skill.buff!.id) + .toList() + ..add(newBuff); // MP 소모 var updatedPlayer = player.withMp(player.mpCurrent - skill.mpCost); @@ -171,11 +179,7 @@ class SkillService { updatedSkillSystem = updatedSkillSystem.copyWith(activeBuffs: updatedBuffs); return ( - result: SkillUseResult( - skill: skill, - success: true, - appliedBuff: newBuff, - ), + result: SkillUseResult(skill: skill, success: true, appliedBuff: newBuff), updatedPlayer: updatedPlayer, updatedSkillSystem: updatedSkillSystem, ); @@ -190,7 +194,8 @@ class SkillService { CombatStats updatedPlayer, SkillSystemState updatedSkillSystem, DotEffect? dotEffect, - }) useDotSkill({ + }) + useDotSkill({ required Skill skill, required CombatStats player, required SkillSystemState skillSystem, @@ -265,12 +270,15 @@ class SkillService { final availableSkills = availableSkillIds .map((id) => SkillData.getSkillById(id)) .whereType() - .where((skill) => canUseSkill( - skill: skill, - currentMp: currentMp, - skillSystem: skillSystem, - ) == - null) + .where( + (skill) => + canUseSkill( + skill: skill, + currentMp: currentMp, + skillSystem: skillSystem, + ) == + null, + ) .toList(); if (availableSkills.isEmpty) return null; @@ -311,9 +319,11 @@ class SkillService { // 예상 총 데미지 기준 정렬 dotSkills.sort((a, b) { - final aTotal = (a.baseDotDamage ?? 0) * + final aTotal = + (a.baseDotDamage ?? 0) * ((a.baseDotDurationMs ?? 0) ~/ (a.baseDotTickMs ?? 1000)); - final bTotal = (b.baseDotDamage ?? 0) * + final bTotal = + (b.baseDotDamage ?? 0) * ((b.baseDotDurationMs ?? 0) ~/ (b.baseDotTickMs ?? 1000)); return bTotal.compareTo(aTotal); }); @@ -344,7 +354,9 @@ class SkillService { final attackSkills = skills.where((s) => s.isAttack).toList(); if (attackSkills.isEmpty) return null; - attackSkills.sort((a, b) => b.damageMultiplier.compareTo(a.damageMultiplier)); + attackSkills.sort( + (a, b) => b.damageMultiplier.compareTo(a.damageMultiplier), + ); return attackSkills.first; } @@ -399,7 +411,10 @@ class SkillService { // ============================================================================ /// 스킬 쿨타임 업데이트 - SkillSystemState _updateSkillCooldown(SkillSystemState state, String skillId) { + SkillSystemState _updateSkillCooldown( + SkillSystemState state, + String skillId, + ) { final skillStates = List.from(state.skillStates); // 기존 상태 찾기 @@ -412,11 +427,9 @@ class SkillService { ); } else { // 새 상태 추가 - skillStates.add(SkillState( - skillId: skillId, - lastUsedMs: state.elapsedMs, - rank: 1, - )); + skillStates.add( + SkillState(skillId: skillId, lastUsedMs: state.elapsedMs, rank: 1), + ); } return state.copyWith(skillStates: skillStates); @@ -426,4 +439,142 @@ class SkillService { SkillSystemState updateElapsedTime(SkillSystemState state, int deltaMs) { return state.copyWith(elapsedMs: state.elapsedMs + deltaMs); } + + // ============================================================================ + // SpellBook 연동 + // ============================================================================ + + /// SpellBook에서 사용 가능한 스킬 목록 조회 + /// + /// SpellEntry 이름을 Skill로 매핑하여 반환 + List getAvailableSkillsFromSpellBook(SpellBook spellBook) { + return spellBook.spells + .map((spell) => SkillData.getSkillBySpellName(spell.name)) + .whereType() + .toList(); + } + + /// SpellBook에서 스킬의 랭크(레벨) 조회 + /// + /// 로마숫자 랭크(I, II, III)를 정수로 변환하여 반환 + /// 스펠이 없으면 1 반환 + int getSkillRankFromSpellBook(SpellBook spellBook, String skillId) { + // skillId로 스킬 찾기 + final skill = SkillData.getSkillById(skillId); + if (skill == null) return 1; + + // 스킬 이름으로 SpellEntry 찾기 + for (final spell in spellBook.spells) { + if (spell.name == skill.name) { + return romanToInt(spell.rank); + } + } + + return 1; // 기본 랭크 + } + + /// SpellBook에서 스킬 ID 목록 조회 + /// + /// 전투 시스템에서 사용 가능한 스킬 ID 목록 반환 + List getAvailableSkillIdsFromSpellBook(SpellBook spellBook) { + return getAvailableSkillsFromSpellBook( + spellBook, + ).map((skill) => skill.id).toList(); + } + + /// 랭크 스케일링이 적용된 공격 스킬 사용 + /// + /// [rank] 스펠 랭크 (SpellBook에서 조회) + ({ + SkillUseResult result, + CombatStats updatedPlayer, + MonsterCombatStats updatedMonster, + SkillSystemState updatedSkillSystem, + }) + useAttackSkillWithRank({ + required Skill skill, + required CombatStats player, + required MonsterCombatStats monster, + required SkillSystemState skillSystem, + required int rank, + }) { + // 랭크 스케일링 적용 + final rankMult = getRankMultiplier(rank); + final mpMult = getRankMpMultiplier(rank); + + // 실제 MP 비용 계산 + final actualMpCost = (skill.mpCost * mpMult).round(); + + // 기본 데미지 계산 (랭크 배율 적용) + final baseDamage = player.atk * skill.damageMultiplier * rankMult; + + // 버프 효과 적용 + final buffMods = skillSystem.totalBuffModifiers; + final buffedDamage = baseDamage * (1 + buffMods.atkMod); + + // 적 방어력 감소 적용 + final effectiveMonsterDef = monster.def * (1 - skill.targetDefReduction); + + // 최종 데미지 계산 (방어력 감산) + final finalDamage = (buffedDamage - effectiveMonsterDef * 0.5) + .round() + .clamp(1, 9999); + + // 몬스터에 데미지 적용 + var updatedMonster = monster.applyDamage(finalDamage); + + // 자해 데미지 적용 + var updatedPlayer = player; + if (skill.selfDamagePercent > 0) { + final selfDamage = (player.hpMax * skill.selfDamagePercent).round(); + updatedPlayer = player.applyDamage(selfDamage); + } + + // MP 소모 (랭크 스케일링 적용) + updatedPlayer = updatedPlayer.withMp( + updatedPlayer.mpCurrent - actualMpCost, + ); + + // 스킬 상태 업데이트 (쿨타임 시작, 랭크 저장) + // 쿨타임 스케일링은 isReady 체크 시 적용됨 + final updatedSkillSystem = _updateSkillCooldownWithRank( + skillSystem, + skill.id, + rank, + ); + + return ( + result: SkillUseResult(skill: skill, success: true, damage: finalDamage), + updatedPlayer: updatedPlayer, + updatedMonster: updatedMonster, + updatedSkillSystem: updatedSkillSystem, + ); + } + + /// 랭크 정보를 포함한 스킬 쿨타임 업데이트 + SkillSystemState _updateSkillCooldownWithRank( + SkillSystemState state, + String skillId, + int rank, + ) { + final skillStates = List.from(state.skillStates); + + // 기존 상태 찾기 + final existingIndex = skillStates.indexWhere((s) => s.skillId == skillId); + + if (existingIndex >= 0) { + // 기존 상태 업데이트 + skillStates[existingIndex] = skillStates[existingIndex].copyWith( + lastUsedMs: state.elapsedMs, + rank: rank, + ); + } else { + // 새 상태 추가 + skillStates.add( + SkillState(skillId: skillId, lastUsedMs: state.elapsedMs, rank: rank), + ); + } + + return state.copyWith(skillStates: skillStates); + } } diff --git a/lib/src/core/engine/stat_calculator.dart b/lib/src/core/engine/stat_calculator.dart index e4adb02..401cd8b 100644 --- a/lib/src/core/engine/stat_calculator.dart +++ b/lib/src/core/engine/stat_calculator.dart @@ -23,7 +23,8 @@ class StatCalculator { var str = baseStats.str + race.getModifier(StatType.str); var con = baseStats.con + race.getModifier(StatType.con); var dex = baseStats.dex + race.getModifier(StatType.dex); - var intel = baseStats.intelligence + race.getModifier(StatType.intelligence); + var intel = + baseStats.intelligence + race.getModifier(StatType.intelligence); var wis = baseStats.wis + race.getModifier(StatType.wis); var cha = baseStats.cha + race.getModifier(StatType.cha); @@ -108,31 +109,41 @@ class StatCalculator { // 클래스 패시브 적용 // 물리 공격력 보너스 (Bug Hunter: +20%) - final classPhysicalBonus = klass.getPassiveValue(ClassPassiveType.physicalDamageBonus); + final classPhysicalBonus = klass.getPassiveValue( + ClassPassiveType.physicalDamageBonus, + ); if (classPhysicalBonus > 0) { atk = (atk * (1 + classPhysicalBonus)).round(); } // 방어력 보너스 (Debugger Paladin: +15%) - final classDefenseBonus = klass.getPassiveValue(ClassPassiveType.defenseBonus); + final classDefenseBonus = klass.getPassiveValue( + ClassPassiveType.defenseBonus, + ); if (classDefenseBonus > 0) { def = (def * (1 + classDefenseBonus)).round(); } // 마법 데미지 보너스 (Compiler Mage: +25%) - final classMagicBonus = klass.getPassiveValue(ClassPassiveType.magicDamageBonus); + final classMagicBonus = klass.getPassiveValue( + ClassPassiveType.magicDamageBonus, + ); if (classMagicBonus > 0) { magAtk = (magAtk * (1 + classMagicBonus)).round(); } // 회피율 보너스 (Refactor Monk: +15%) - final classEvasionBonus = klass.getPassiveValue(ClassPassiveType.evasionBonus); + final classEvasionBonus = klass.getPassiveValue( + ClassPassiveType.evasionBonus, + ); if (classEvasionBonus > 0) { evasion = (evasion + classEvasionBonus).clamp(0.0, 0.6); } // 크리티컬 보너스 (Pointer Assassin: +20%) - final classCritBonus = klass.getPassiveValue(ClassPassiveType.criticalBonus); + final classCritBonus = klass.getPassiveValue( + ClassPassiveType.criticalBonus, + ); if (classCritBonus > 0) { criRate = (criRate + classCritBonus).clamp(0.0, 0.8); } diff --git a/lib/src/core/engine/story_service.dart b/lib/src/core/engine/story_service.dart index e647691..cd5feca 100644 --- a/lib/src/core/engine/story_service.dart +++ b/lib/src/core/engine/story_service.dart @@ -13,11 +13,7 @@ enum StoryEventType { /// 스토리 이벤트 (Story Event) class StoryEvent { - const StoryEvent({ - required this.type, - required this.act, - this.data, - }); + const StoryEvent({required this.type, required this.act, this.data}); final StoryEventType type; final StoryAct act; @@ -73,18 +69,14 @@ class StoryService { // 이전 Act 완료 처리 if (_currentAct != StoryAct.prologue) { _completedActs.add(_currentAct); - _eventController.add(StoryEvent( - type: StoryEventType.actComplete, - act: _currentAct, - )); + _eventController.add( + StoryEvent(type: StoryEventType.actComplete, act: _currentAct), + ); } // 새 Act 시작 _currentAct = newAct; - final event = StoryEvent( - type: StoryEventType.actStart, - act: newAct, - ); + final event = StoryEvent(type: StoryEventType.actStart, act: newAct); _eventController.add(event); return event; } @@ -126,10 +118,9 @@ class StoryService { void _triggerEnding() { _completedActs.add(StoryAct.act5); _currentAct = StoryAct.ending; - _eventController.add(StoryEvent( - type: StoryEventType.ending, - act: StoryAct.ending, - )); + _eventController.add( + StoryEvent(type: StoryEventType.ending, act: StoryAct.ending), + ); } /// 시네마틱 데이터 가져오기 (Get Cinematic Data) diff --git a/lib/src/core/l10n/game_data_l10n.dart b/lib/src/core/l10n/game_data_l10n.dart index bcda297..6bf6846 100644 --- a/lib/src/core/l10n/game_data_l10n.dart +++ b/lib/src/core/l10n/game_data_l10n.dart @@ -1,14 +1,19 @@ +import 'package:askiineverdie/data/game_text_l10n.dart' as l10n; import 'package:askiineverdie/data/game_translations_ko.dart'; import 'package:askiineverdie/src/core/util/pq_logic.dart'; import 'package:flutter/widgets.dart'; /// 게임 데이터 번역을 위한 헬퍼 클래스 /// 현재 로케일에 따라 게임 데이터의 번역을 제공합니다. +/// +/// 글로벌 로케일 시스템(game_text_l10n.dart)을 사용하여 일관성 보장 class GameDataL10n { GameDataL10n._(); - /// 현재 로케일이 한국어인지 확인 + /// 현재 로케일이 한국어인지 확인 (글로벌 로케일 사용) static bool _isKorean(BuildContext context) { + // 글로벌 로케일 우선, 폴백으로 context 로케일 사용 + if (l10n.isKoreanLocale) return true; final locale = Localizations.localeOf(context); return locale.languageCode == 'ko'; } @@ -307,17 +312,19 @@ class GameDataL10n { for (final entry in baseMap.entries) { if (remaining.endsWith(entry.key)) { baseTranslated = entry.value; - modifierPart = remaining.substring( - 0, - remaining.length - entry.key.length, - ).trim(); + modifierPart = remaining + .substring(0, remaining.length - entry.key.length) + .trim(); break; } } // 3. 수식어 번역 final isWeapon = slotIndex == 0; - final modWords = modifierPart.split(' ').where((s) => s.isNotEmpty).toList(); + final modWords = modifierPart + .split(' ') + .where((s) => s.isNotEmpty) + .toList(); final translatedMods = modWords.map((mod) { if (isWeapon) { return offenseAttribTranslationsKo[mod] ?? @@ -423,19 +430,20 @@ class GameDataL10n { // 드롭 아이템 앞 부분이 몬스터 이름 String monsterPart; if (itemString.endsWith(dropItemProperCase)) { - monsterPart = - itemString.substring(0, itemString.length - dropItemProperCase.length).trim(); + monsterPart = itemString + .substring(0, itemString.length - dropItemProperCase.length) + .trim(); } else { - monsterPart = - itemString.substring(0, itemString.length - dropItem.length).trim(); + monsterPart = itemString + .substring(0, itemString.length - dropItem.length) + .trim(); } if (monsterPart.isEmpty) continue; // 몬스터 이름 번역 (소문자를 원래 형태로 변환하여 찾기) final monsterNameKey = _toTitleCase(monsterPart); - final monsterKo = - monsterTranslationsKo[monsterNameKey] ?? monsterPart; + final monsterKo = monsterTranslationsKo[monsterNameKey] ?? monsterPart; final dropKo = entry.value; return '$monsterKo의 $dropKo'; @@ -452,9 +460,12 @@ class GameDataL10n { /// 각 단어의 첫 글자를 대문자로 (Title Case) static String _toTitleCase(String s) { - return s.split(' ').map((word) { - if (word.isEmpty) return word; - return word[0].toUpperCase() + word.substring(1); - }).join(' '); + return s + .split(' ') + .map((word) { + if (word.isEmpty) return word; + return word[0].toUpperCase() + word.substring(1); + }) + .join(' '); } } diff --git a/lib/src/core/model/combat_stats.dart b/lib/src/core/model/combat_stats.dart index edb258b..bb8b602 100644 --- a/lib/src/core/model/combat_stats.dart +++ b/lib/src/core/model/combat_stats.dart @@ -233,7 +233,8 @@ class CombatStats { final effectiveStr = stats.str + equipStats.strBonus + raceStr + classStr; final effectiveCon = stats.con + equipStats.conBonus + raceCon + classCon; final effectiveDex = stats.dex + equipStats.dexBonus + raceDex + classDex; - final effectiveInt = stats.intelligence + equipStats.intBonus + raceInt + classInt; + final effectiveInt = + stats.intelligence + equipStats.intBonus + raceInt + classInt; final effectiveWis = stats.wis + equipStats.wisBonus + raceWis + classWis; final effectiveCha = stats.cha + equipStats.chaBonus + raceCha + classCha; @@ -276,7 +277,10 @@ class CombatStats { final weaponSpeed = weaponItem.stats.attackSpeed; final baseAttackSpeed = weaponSpeed > 0 ? weaponSpeed : 1000; final speedModifier = 1.0 + (effectiveDex - 10) * 0.02; - final attackDelayMs = (baseAttackSpeed / speedModifier).round().clamp(300, 2000); + final attackDelayMs = (baseAttackSpeed / speedModifier).round().clamp( + 300, + 2000, + ); // HP/MP: 기본 + 장비 보너스 var totalHpMax = stats.hpMax + equipStats.hpBonus; @@ -299,7 +303,8 @@ class CombatStats { } // 마법 데미지 보너스 (Null Elf: +15%) - final raceMagicBonus = race?.getPassiveValue(PassiveType.magicDamageBonus) ?? 0.0; + final raceMagicBonus = + race?.getPassiveValue(PassiveType.magicDamageBonus) ?? 0.0; if (raceMagicBonus > 0) { baseMagAtk = (baseMagAtk * (1 + raceMagicBonus)).round(); } @@ -311,7 +316,8 @@ class CombatStats { } // 크리티컬 보너스 (Stack Goblin: +5%) - final raceCritBonus = race?.getPassiveValue(PassiveType.criticalBonus) ?? 0.0; + final raceCritBonus = + race?.getPassiveValue(PassiveType.criticalBonus) ?? 0.0; criRate += raceCritBonus; // ======================================================================== @@ -319,35 +325,41 @@ class CombatStats { // ======================================================================== // HP 보너스 (Garbage Collector: +30%) - final classHpBonus = klass?.getPassiveValue(ClassPassiveType.hpBonus) ?? 0.0; + final classHpBonus = + klass?.getPassiveValue(ClassPassiveType.hpBonus) ?? 0.0; if (classHpBonus > 0) { totalHpMax = (totalHpMax * (1 + classHpBonus)).round(); } // 물리 공격력 보너스 (Bug Hunter: +20%) - final classPhysBonus = klass?.getPassiveValue(ClassPassiveType.physicalDamageBonus) ?? 0.0; + final classPhysBonus = + klass?.getPassiveValue(ClassPassiveType.physicalDamageBonus) ?? 0.0; if (classPhysBonus > 0) { baseAtk = (baseAtk * (1 + classPhysBonus)).round(); } // 방어력 보너스 (Debugger Paladin: +15%) - final classDefBonus = klass?.getPassiveValue(ClassPassiveType.defenseBonus) ?? 0.0; + final classDefBonus = + klass?.getPassiveValue(ClassPassiveType.defenseBonus) ?? 0.0; if (classDefBonus > 0) { baseDef = (baseDef * (1 + classDefBonus)).round(); } // 마법 데미지 보너스 (Compiler Mage: +25%) - final classMagBonus = klass?.getPassiveValue(ClassPassiveType.magicDamageBonus) ?? 0.0; + final classMagBonus = + klass?.getPassiveValue(ClassPassiveType.magicDamageBonus) ?? 0.0; if (classMagBonus > 0) { baseMagAtk = (baseMagAtk * (1 + classMagBonus)).round(); } // 회피율 보너스 (Refactor Monk: +15%) - final classEvasionBonus = klass?.getPassiveValue(ClassPassiveType.evasionBonus) ?? 0.0; + final classEvasionBonus = + klass?.getPassiveValue(ClassPassiveType.evasionBonus) ?? 0.0; evasion += classEvasionBonus; // 크리티컬 보너스 (Pointer Assassin: +20%) - final classCritBonus = klass?.getPassiveValue(ClassPassiveType.criticalBonus) ?? 0.0; + final classCritBonus = + klass?.getPassiveValue(ClassPassiveType.criticalBonus) ?? 0.0; criRate += classCritBonus; // 최종 클램핑 diff --git a/lib/src/core/model/game_state.dart b/lib/src/core/model/game_state.dart index 829436b..cf258c1 100644 --- a/lib/src/core/model/game_state.dart +++ b/lib/src/core/model/game_state.dart @@ -209,11 +209,8 @@ class SkillSystemState { /// 게임 진행 경과 시간 (밀리초, 스킬 쿨타임 계산용) final int elapsedMs; - factory SkillSystemState.empty() => const SkillSystemState( - skillStates: [], - activeBuffs: [], - elapsedMs: 0, - ); + factory SkillSystemState.empty() => + const SkillSystemState(skillStates: [], activeBuffs: [], elapsedMs: 0); /// 특정 스킬 상태 가져오기 SkillState? getSkillState(String skillId) { @@ -224,7 +221,8 @@ class SkillSystemState { } /// 버프 효과 합산 (동일 버프는 중복 적용 안 됨) - ({double atkMod, double defMod, double criMod, double evasionMod}) get totalBuffModifiers { + ({double atkMod, double defMod, double criMod, double evasionMod}) + get totalBuffModifiers { double atkMod = 0; double defMod = 0; double criMod = 0; @@ -243,7 +241,12 @@ class SkillSystemState { } } - return (atkMod: atkMod, defMod: defMod, criMod: criMod, evasionMod: evasionMod); + return ( + atkMod: atkMod, + defMod: defMod, + criMod: criMod, + evasionMod: evasionMod, + ); } SkillSystemState copyWith({ @@ -477,10 +480,8 @@ class Inventory { /// Phase 2에서 EquipmentItem 기반으로 확장됨. /// 기존 문자열 API(weapon, shield 등)는 호환성을 위해 유지. class Equipment { - Equipment({ - required this.items, - required this.bestIndex, - }) : assert(items.length == slotCount, 'Equipment must have $slotCount items'); + Equipment({required this.items, required this.bestIndex}) + : assert(items.length == slotCount, 'Equipment must have $slotCount items'); /// 장비 아이템 목록 (11개 슬롯) final List items; @@ -525,10 +526,7 @@ class Equipment { /// 모든 장비 스탯 합산 ItemStats get totalStats { - return items.fold( - ItemStats.empty, - (sum, item) => sum + item.stats, - ); + return items.fold(ItemStats.empty, (sum, item) => sum + item.stats); } /// 모든 장비 무게 합산 @@ -647,10 +645,7 @@ class Equipment { return Equipment(items: newItems, bestIndex: bestIndex); } - Equipment copyWith({ - List? items, - int? bestIndex, - }) { + Equipment copyWith({List? items, int? bestIndex}) { return Equipment( items: items ?? List.from(this.items), bestIndex: bestIndex ?? this.bestIndex, diff --git a/lib/src/core/model/hall_of_fame.dart b/lib/src/core/model/hall_of_fame.dart index 7d97323..61257c6 100644 --- a/lib/src/core/model/hall_of_fame.dart +++ b/lib/src/core/model/hall_of_fame.dart @@ -175,9 +175,7 @@ class HallOfFame { /// JSON으로 직렬화 Map toJson() { - return { - 'entries': entries.map((e) => e.toJson()).toList(), - }; + return {'entries': entries.map((e) => e.toJson()).toList()}; } /// JSON에서 역직렬화 diff --git a/lib/src/core/model/potion.dart b/lib/src/core/model/potion.dart index 71ad85c..156a7bf 100644 --- a/lib/src/core/model/potion.dart +++ b/lib/src/core/model/potion.dart @@ -88,10 +88,7 @@ class PotionInventory { PotionInventory addPotion(String potionId, [int count = 1]) { final newPotions = Map.from(potions); newPotions[potionId] = (newPotions[potionId] ?? 0) + count; - return PotionInventory( - potions: newPotions, - usedInBattle: usedInBattle, - ); + return PotionInventory(potions: newPotions, usedInBattle: usedInBattle); } /// 물약 사용 (수량 감소) @@ -107,18 +104,12 @@ class PotionInventory { final newUsed = Set.from(usedInBattle)..add(type); - return PotionInventory( - potions: newPotions, - usedInBattle: newUsed, - ); + return PotionInventory(potions: newPotions, usedInBattle: newUsed); } /// 전투 종료 시 사용 기록 초기화 PotionInventory resetBattleUsage() { - return PotionInventory( - potions: potions, - usedInBattle: const {}, - ); + return PotionInventory(potions: potions, usedInBattle: const {}); } /// 빈 인벤토리 diff --git a/lib/src/core/model/race_traits.dart b/lib/src/core/model/race_traits.dart index 5290d12..1960e50 100644 --- a/lib/src/core/model/race_traits.dart +++ b/lib/src/core/model/race_traits.dart @@ -1,12 +1,5 @@ /// 스탯 타입 열거형 (stat type) -enum StatType { - str, - con, - dex, - intelligence, - wis, - cha, -} +enum StatType { str, con, dex, intelligence, wis, cha } /// 패시브 능력 타입 (passive ability type) enum PassiveType { diff --git a/lib/src/core/model/skill.dart b/lib/src/core/model/skill.dart index 9e3aa2d..ef0c2a8 100644 --- a/lib/src/core/model/skill.dart +++ b/lib/src/core/model/skill.dart @@ -1,3 +1,28 @@ +// ============================================================================ +// 랭크 스케일링 (Rank Scaling) +// ============================================================================ + +/// 스펠 랭크에 따른 스킬 배율 계산 +/// +/// 랭크 1: 1.0x, 랭크 2: 1.15x, 랭크 3: 1.30x, ... +double getRankMultiplier(int rank) => 1.0 + (rank - 1) * 0.15; + +/// 랭크에 따른 쿨타임 감소율 계산 +/// +/// 랭크당 5% 감소 (최대 50% 감소) +double getRankCooldownMultiplier(int rank) => + (1.0 - (rank - 1) * 0.05).clamp(0.5, 1.0); + +/// 랭크에 따른 MP 비용 감소율 계산 +/// +/// 랭크당 3% 감소 (최대 30% 감소) +double getRankMpMultiplier(int rank) => + (1.0 - (rank - 1) * 0.03).clamp(0.7, 1.0); + +// ============================================================================ +// 스킬 타입 (Skill Types) +// ============================================================================ + /// 스킬 타입 enum SkillType { /// 공격 스킬 @@ -103,6 +128,9 @@ class Skill { this.baseDotDamage, this.baseDotDurationMs, this.baseDotTickMs, + this.hitCount = 1, + this.lifestealPercent = 0.0, + this.mpHealAmount = 0, }); /// 스킬 ID @@ -156,6 +184,15 @@ class Skill { /// DOT 기본 틱 간격 (밀리초, 스킬 레벨로 결정) final int? baseDotTickMs; + /// 다중 타격 횟수 (기본 1) + final int hitCount; + + /// HP 흡수율 (0.0 ~ 1.0, 데미지의 N% 회복) + final double lifestealPercent; + + /// MP 회복량 (MP 회복 스킬용) + final int mpHealAmount; + /// 공격 스킬 여부 bool get isAttack => type == SkillType.attack; @@ -207,11 +244,7 @@ class SkillState { return cooldownMs - elapsed; } - SkillState copyWith({ - String? skillId, - int? lastUsedMs, - int? rank, - }) { + SkillState copyWith({String? skillId, int? lastUsedMs, int? rank}) { return SkillState( skillId: skillId ?? this.skillId, lastUsedMs: lastUsedMs ?? this.lastUsedMs, @@ -302,11 +335,7 @@ class SkillUseResult { /// 실패 결과 생성 factory SkillUseResult.failed(Skill skill, SkillFailReason reason) { - return SkillUseResult( - skill: skill, - success: false, - failReason: reason, - ); + return SkillUseResult(skill: skill, success: false, failReason: reason); } } @@ -384,7 +413,11 @@ class DotEffect { /// [skill] DOT 스킬 /// [playerInt] 플레이어 INT (틱당 데미지 보정) /// [playerWis] 플레이어 WIS (틱 간격 보정) - factory DotEffect.fromSkill(Skill skill, {int playerInt = 10, int playerWis = 10}) { + factory DotEffect.fromSkill( + Skill skill, { + int playerInt = 10, + int playerWis = 10, + }) { assert(skill.isDot, 'DOT 스킬만 DotEffect 생성 가능'); assert(skill.baseDotDamage != null, 'baseDotDamage 필수'); assert(skill.baseDotDurationMs != null, 'baseDotDurationMs 필수'); @@ -396,7 +429,9 @@ class DotEffect { // WIS → 틱 간격 보정 (WIS 10 기준, ±2%/포인트, 빨라짐) final wisMod = 1.0 + (playerWis - 10) * 0.02; - final actualTickMs = (skill.baseDotTickMs! / wisMod).clamp(200, 2000).round(); + final actualTickMs = (skill.baseDotTickMs! / wisMod) + .clamp(200, 2000) + .round(); return DotEffect( skillId: skill.id, diff --git a/lib/src/core/notification/notification_service.dart b/lib/src/core/notification/notification_service.dart index 8ca9bf7..10d47aa 100644 --- a/lib/src/core/notification/notification_service.dart +++ b/lib/src/core/notification/notification_service.dart @@ -56,24 +56,28 @@ class NotificationService { /// 레벨업 알림 (Level Up Notification) void showLevelUp(int newLevel) { - show(GameNotification( - type: NotificationType.levelUp, - title: 'LEVEL UP!', - subtitle: 'Level $newLevel', - data: {'level': newLevel}, - duration: const Duration(seconds: 2), - )); + show( + GameNotification( + type: NotificationType.levelUp, + title: 'LEVEL UP!', + subtitle: 'Level $newLevel', + data: {'level': newLevel}, + duration: const Duration(seconds: 2), + ), + ); } /// 퀘스트 완료 알림 void showQuestComplete(String questName) { - show(GameNotification( - type: NotificationType.questComplete, - title: 'QUEST COMPLETE!', - subtitle: questName, - data: {'quest': questName}, - duration: const Duration(seconds: 2), - )); + show( + GameNotification( + type: NotificationType.questComplete, + title: 'QUEST COMPLETE!', + subtitle: questName, + data: {'quest': questName}, + duration: const Duration(seconds: 2), + ), + ); } /// 막 완료 알림 (Act Complete) @@ -82,44 +86,52 @@ class NotificationService { final title = actNumber == 0 ? 'PROLOGUE COMPLETE!' : 'ACT $actNumber COMPLETE!'; - show(GameNotification( - type: NotificationType.actComplete, - title: title, - duration: const Duration(seconds: 3), - )); + show( + GameNotification( + type: NotificationType.actComplete, + title: title, + duration: const Duration(seconds: 3), + ), + ); } /// 새 주문 알림 void showNewSpell(String spellName) { - show(GameNotification( - type: NotificationType.newSpell, - title: 'NEW SPELL!', - subtitle: spellName, - data: {'spell': spellName}, - duration: const Duration(seconds: 2), - )); + show( + GameNotification( + type: NotificationType.newSpell, + title: 'NEW SPELL!', + subtitle: spellName, + data: {'spell': spellName}, + duration: const Duration(seconds: 2), + ), + ); } /// 새 장비 알림 void showNewEquipment(String equipmentName, String slot) { - show(GameNotification( - type: NotificationType.newEquipment, - title: 'NEW EQUIPMENT!', - subtitle: equipmentName, - data: {'equipment': equipmentName, 'slot': slot}, - duration: const Duration(seconds: 2), - )); + show( + GameNotification( + type: NotificationType.newEquipment, + title: 'NEW EQUIPMENT!', + subtitle: equipmentName, + data: {'equipment': equipmentName, 'slot': slot}, + duration: const Duration(seconds: 2), + ), + ); } /// 보스 처치 알림 void showBossDefeat(String bossName) { - show(GameNotification( - type: NotificationType.bossDefeat, - title: 'BOSS DEFEATED!', - subtitle: bossName, - data: {'boss': bossName}, - duration: const Duration(seconds: 3), - )); + show( + GameNotification( + type: NotificationType.bossDefeat, + title: 'BOSS DEFEATED!', + subtitle: bossName, + data: {'boss': bossName}, + duration: const Duration(seconds: 3), + ), + ); } /// 큐 처리 (Process Queue) diff --git a/lib/src/core/storage/theme_preferences.dart b/lib/src/core/storage/theme_preferences.dart deleted file mode 100644 index 4431d8a..0000000 --- a/lib/src/core/storage/theme_preferences.dart +++ /dev/null @@ -1,24 +0,0 @@ -import 'package:shared_preferences/shared_preferences.dart'; - -import 'package:askiineverdie/src/core/animation/ascii_animation_data.dart'; - -/// 테마 설정 저장/로드 서비스 -class ThemePreferences { - static const _keyColorTheme = 'ascii_color_theme'; - - /// 테마 설정 저장 - static Future saveColorTheme(AsciiColorTheme theme) async { - final prefs = await SharedPreferences.getInstance(); - await prefs.setInt(_keyColorTheme, theme.index); - } - - /// 테마 설정 로드 (기본값: green) - static Future loadColorTheme() async { - final prefs = await SharedPreferences.getInstance(); - final index = prefs.getInt(_keyColorTheme); - if (index == null || index < 0 || index >= AsciiColorTheme.values.length) { - return AsciiColorTheme.green; - } - return AsciiColorTheme.values[index]; - } -} diff --git a/lib/src/core/util/pq_logic.dart b/lib/src/core/util/pq_logic.dart index 7d21852..4c803ff 100644 --- a/lib/src/core/util/pq_logic.dart +++ b/lib/src/core/util/pq_logic.dart @@ -42,12 +42,7 @@ class EquipResult { /// 아이템 생성 결과 (구조화된 데이터로 l10n 지원) class ItemResult { - const ItemResult({ - this.attrib, - this.special, - this.itemOf, - this.boringItem, - }); + const ItemResult({this.attrib, this.special, this.itemOf, this.boringItem}); /// 아이템 속성 (예: "Golden") final String? attrib; @@ -592,7 +587,8 @@ MonsterTaskResult monsterTask( } if (!definite) { - name = indefinite(name, qty); + // l10n 지원: 한국어/일본어에서는 관사 불필요 + name = l10n.indefiniteL10n(name, qty); } return MonsterTaskResult( @@ -638,10 +634,13 @@ class QuestResult { final String caption; final RewardKind reward; + /// 몬스터 전체 데이터 (예: "Rat|5|tail") - 원본 fQuest.Caption final String? monsterName; + /// 몬스터 레벨 (파싱된 값) final int? monsterLevel; + /// 몬스터 인덱스 (config.monsters에서의 위치) - 원본 fQuest.Tag final int? monsterIndex; } @@ -694,10 +693,7 @@ QuestResult completeQuest(PqConfig config, DeterministicRandom rng, int level) { case 2: final itemEn = boringItem(config, rng); final item = l10n.translateBoringItem(itemEn); - return QuestResult( - caption: l10n.questTransfer(item), - reward: reward, - ); + return QuestResult(caption: l10n.questTransfer(item), reward: reward); case 3: final itemEn = boringItem(config, rng); final item = l10n.translateBoringItem(itemEn); diff --git a/lib/src/features/front/front_screen.dart b/lib/src/features/front/front_screen.dart index 9a1af8b..1831647 100644 --- a/lib/src/features/front/front_screen.dart +++ b/lib/src/features/front/front_screen.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; +import 'package:askiineverdie/data/game_text_l10n.dart' as game_l10n; import 'package:askiineverdie/l10n/app_localizations.dart'; class FrontScreen extends StatelessWidget { @@ -128,7 +129,7 @@ class _HeroHeader extends StatelessWidget { ), const SizedBox(height: 6), Text( - 'Offline Progress Quest (PQ 6.4) rebuilt with Flutter.', + game_l10n.frontDescription, style: theme.textTheme.titleMedium?.copyWith( color: colorScheme.onPrimary.withValues(alpha: 0.9), ), @@ -146,9 +147,15 @@ class _HeroHeader extends StatelessWidget { spacing: 8, runSpacing: 8, children: [ - _Tag(icon: Icons.cloud_off_outlined, label: l10n.tagNoNetwork), + _Tag( + icon: Icons.cloud_off_outlined, + label: l10n.tagNoNetwork, + ), _Tag(icon: Icons.timer_outlined, label: l10n.tagIdleRpg), - _Tag(icon: Icons.storage_rounded, label: l10n.tagLocalSaves), + _Tag( + icon: Icons.storage_rounded, + label: l10n.tagLocalSaves, + ), ], ); }, @@ -208,7 +215,7 @@ class _ActionRow extends StatelessWidget { TextButton.icon( onPressed: onHallOfFame, icon: const Icon(Icons.emoji_events_outlined), - label: const Text('Hall of Fame'), + label: Text(game_l10n.uiHallOfFame), ), ], ); @@ -245,7 +252,7 @@ class _StatusCards extends StatelessWidget { SizedBox(height: 16), _InfoCard( icon: Icons.checklist_rtl, - title: 'Today’s focus', + title: game_l10n.frontTodayFocus, points: [ 'Set up scaffold + lints.', 'Wire seed theme and initial navigation shell.', diff --git a/lib/src/features/game/game_play_screen.dart b/lib/src/features/game/game_play_screen.dart index 27d6ead..ac04125 100644 --- a/lib/src/features/game/game_play_screen.dart +++ b/lib/src/features/game/game_play_screen.dart @@ -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 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 .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 /// 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 /// 전투 이벤트를 메시지와 타입으로 변환 (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 } }, 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 // 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 '${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 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 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 ); } - 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 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 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 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); + } +} diff --git a/lib/src/features/game/game_session_controller.dart b/lib/src/features/game/game_session_controller.dart index 56d1a8c..4f7fcdb 100644 --- a/lib/src/features/game/game_session_controller.dart +++ b/lib/src/features/game/game_session_controller.dart @@ -186,5 +186,6 @@ class GameSessionController extends ChangeNotifier { } /// 사망 상태 여부 - bool get isDead => _status == GameSessionStatus.dead || (_state?.isDead ?? false); + bool get isDead => + _status == GameSessionStatus.dead || (_state?.isDead ?? false); } diff --git a/lib/src/features/game/widgets/active_buff_panel.dart b/lib/src/features/game/widgets/active_buff_panel.dart index 029e0c2..77582f3 100644 --- a/lib/src/features/game/widgets/active_buff_panel.dart +++ b/lib/src/features/game/widgets/active_buff_panel.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; +import 'package:askiineverdie/data/game_text_l10n.dart' as l10n; import 'package:askiineverdie/src/core/model/skill.dart'; /// 활성 버프 패널 위젯 @@ -18,10 +19,10 @@ class ActiveBuffPanel extends StatelessWidget { @override Widget build(BuildContext context) { if (activeBuffs.isEmpty) { - return const Center( + return Center( child: Text( - 'No active buffs', - style: TextStyle( + l10n.uiNoActiveBuffs, + style: const TextStyle( fontSize: 11, color: Colors.grey, fontStyle: FontStyle.italic, @@ -43,10 +44,7 @@ class ActiveBuffPanel extends StatelessWidget { /// 개별 버프 행 위젯 class _BuffRow extends StatelessWidget { - const _BuffRow({ - required this.buff, - required this.currentMs, - }); + const _BuffRow({required this.buff, required this.currentMs}); final ActiveBuff buff; final int currentMs; @@ -66,11 +64,7 @@ class _BuffRow extends StatelessWidget { Row( children: [ // 버프 아이콘 - const Icon( - Icons.trending_up, - size: 14, - color: Colors.lightBlue, - ), + const Icon(Icons.trending_up, size: 14, color: Colors.lightBlue), const SizedBox(width: 4), // 버프 이름 @@ -92,8 +86,9 @@ class _BuffRow extends StatelessWidget { style: TextStyle( fontSize: 10, color: remainingMs < 3000 ? Colors.orange : Colors.grey, - fontWeight: - remainingMs < 3000 ? FontWeight.bold : FontWeight.normal, + fontWeight: remainingMs < 3000 + ? FontWeight.bold + : FontWeight.normal, ), ), ], @@ -117,11 +112,7 @@ class _BuffRow extends StatelessWidget { // 효과 목록 if (modifiers.isNotEmpty) ...[ const SizedBox(height: 2), - Wrap( - spacing: 6, - runSpacing: 2, - children: modifiers, - ), + Wrap(spacing: 6, runSpacing: 2, children: modifiers), ], ], ), @@ -134,35 +125,43 @@ class _BuffRow extends StatelessWidget { final effect = buff.effect; if (effect.atkModifier != 0) { - modifiers.add(_ModifierChip( - label: 'ATK', - value: effect.atkModifier, - isPositive: effect.atkModifier > 0, - )); + modifiers.add( + _ModifierChip( + label: 'ATK', + value: effect.atkModifier, + isPositive: effect.atkModifier > 0, + ), + ); } if (effect.defModifier != 0) { - modifiers.add(_ModifierChip( - label: 'DEF', - value: effect.defModifier, - isPositive: effect.defModifier > 0, - )); + modifiers.add( + _ModifierChip( + label: 'DEF', + value: effect.defModifier, + isPositive: effect.defModifier > 0, + ), + ); } if (effect.criRateModifier != 0) { - modifiers.add(_ModifierChip( - label: 'CRI', - value: effect.criRateModifier, - isPositive: effect.criRateModifier > 0, - )); + modifiers.add( + _ModifierChip( + label: 'CRI', + value: effect.criRateModifier, + isPositive: effect.criRateModifier > 0, + ), + ); } if (effect.evasionModifier != 0) { - modifiers.add(_ModifierChip( - label: 'EVA', - value: effect.evasionModifier, - isPositive: effect.evasionModifier > 0, - )); + modifiers.add( + _ModifierChip( + label: 'EVA', + value: effect.evasionModifier, + isPositive: effect.evasionModifier > 0, + ), + ); } return modifiers; diff --git a/lib/src/features/game/widgets/ascii_animation_card.dart b/lib/src/features/game/widgets/ascii_animation_card.dart index 4ac91f5..7b5f1e6 100644 --- a/lib/src/features/game/widgets/ascii_animation_card.dart +++ b/lib/src/features/game/widgets/ascii_animation_card.dart @@ -117,6 +117,9 @@ class _AsciiAnimationCardState extends State { // 전투 이벤트 동기화용 (Phase 5) int? _lastEventTimestamp; bool _showCriticalEffect = false; + bool _showBlockEffect = false; + bool _showParryEffect = false; + bool _showSkillEffect = false; // 특수 애니메이션 프레임 수는 ascii_animation_type.dart의 // specialAnimationFrameCounts 상수 사용 @@ -177,33 +180,114 @@ class _AsciiAnimationCardState extends State { // 전투 모드가 아니면 무시 if (_animationMode != AnimationMode.battle) return; - // 이벤트 타입에 따라 페이즈 강제 전환 - final (targetPhase, isCritical) = switch (event.type) { + // 이벤트 타입에 따라 페이즈 및 효과 결정 + final ( + targetPhase, + isCritical, + isBlock, + isParry, + isSkill, + ) = switch (event.type) { // 플레이어 공격 → attack 페이즈 - CombatEventType.playerAttack => (BattlePhase.attack, event.isCritical), - CombatEventType.playerSkill => (BattlePhase.attack, event.isCritical), + CombatEventType.playerAttack => ( + BattlePhase.attack, + event.isCritical, + false, + false, + false, + ), + // 스킬 사용 → attack 페이즈 + 스킬 이펙트 + CombatEventType.playerSkill => ( + BattlePhase.attack, + event.isCritical, + false, + false, + true, + ), - // 몬스터 공격/플레이어 피격 → hit 페이즈 - CombatEventType.monsterAttack => (BattlePhase.hit, false), - CombatEventType.playerBlock => (BattlePhase.hit, false), - CombatEventType.playerParry => (BattlePhase.hit, false), + // 몬스터 공격 → hit 페이즈 + CombatEventType.monsterAttack => ( + BattlePhase.hit, + false, + false, + false, + false, + ), + // 블록 → hit 페이즈 + 블록 이펙트 + CombatEventType.playerBlock => ( + BattlePhase.hit, + false, + true, + false, + false, + ), + // 패리 → hit 페이즈 + 패리 이펙트 + CombatEventType.playerParry => ( + BattlePhase.hit, + false, + false, + true, + false, + ), // 회피 → recover 페이즈 (빠른 회피 동작) - CombatEventType.playerEvade => (BattlePhase.recover, false), - CombatEventType.monsterEvade => (BattlePhase.idle, false), + CombatEventType.playerEvade => ( + BattlePhase.recover, + false, + false, + false, + false, + ), + CombatEventType.monsterEvade => ( + BattlePhase.idle, + false, + false, + false, + false, + ), // 회복/버프 → idle 페이즈 유지 - CombatEventType.playerHeal => (BattlePhase.idle, false), - CombatEventType.playerBuff => (BattlePhase.idle, false), + CombatEventType.playerHeal => ( + BattlePhase.idle, + false, + false, + false, + false, + ), + CombatEventType.playerBuff => ( + BattlePhase.idle, + false, + false, + false, + false, + ), // DOT 틱 → attack 페이즈 (지속 피해) - CombatEventType.dotTick => (BattlePhase.attack, false), + CombatEventType.dotTick => ( + BattlePhase.attack, + false, + false, + false, + false, + ), // 물약 사용 → idle 페이즈 유지 - CombatEventType.playerPotion => (BattlePhase.idle, false), + CombatEventType.playerPotion => ( + BattlePhase.idle, + false, + false, + false, + false, + ), // 물약 드랍 → idle 페이즈 유지 - CombatEventType.potionDrop => (BattlePhase.idle, false), + CombatEventType.potionDrop => ( + BattlePhase.idle, + false, + false, + false, + false, + ), }; setState(() { @@ -211,6 +295,9 @@ class _AsciiAnimationCardState extends State { _battleSubFrame = 0; _phaseFrameCount = 0; _showCriticalEffect = isCritical; + _showBlockEffect = isBlock; + _showParryEffect = isParry; + _showSkillEffect = isSkill; // 페이즈 인덱스 동기화 _phaseIndex = _battlePhaseSequence.indexWhere((p) => p.$1 == targetPhase); @@ -322,8 +409,11 @@ class _AsciiAnimationCardState extends State { _phaseIndex = (_phaseIndex + 1) % _battlePhaseSequence.length; _phaseFrameCount = 0; _battleSubFrame = 0; - // 크리티컬 이펙트 리셋 (페이즈 전환 시) + // 이펙트 리셋 (페이즈 전환 시) _showCriticalEffect = false; + _showBlockEffect = false; + _showParryEffect = false; + _showSkillEffect = false; } else { _battleSubFrame++; } @@ -340,21 +430,22 @@ class _AsciiAnimationCardState extends State { /// 현재 애니메이션 레이어 생성 List _composeLayers() { return switch (_animationMode) { - AnimationMode.battle => _battleComposer?.composeLayers( - _battlePhase, - _battleSubFrame, - widget.monsterBaseName, - _environment, - _globalTick, - ) ?? - [AsciiLayer.empty()], + AnimationMode.battle => + _battleComposer?.composeLayers( + _battlePhase, + _battleSubFrame, + widget.monsterBaseName, + _environment, + _globalTick, + ) ?? + [AsciiLayer.empty()], AnimationMode.walking => _walkingComposer.composeLayers(_globalTick), AnimationMode.town => _townComposer.composeLayers(_globalTick), AnimationMode.special => _specialComposer.composeLayers( - _currentSpecialAnimation ?? AsciiAnimationType.levelUp, - _currentFrame, - _globalTick, - ), + _currentSpecialAnimation ?? AsciiAnimationType.levelUp, + _currentFrame, + _globalTick, + ), }; } @@ -363,17 +454,38 @@ class _AsciiAnimationCardState extends State { // Phase 7: 고정 4색 팔레트 사용 (colorTheme 무시) const bgColor = AsciiColors.background; - // 테두리 효과 결정 (특수 애니메이션 또는 크리티컬 히트) + // 테두리 효과 결정 (전투 이벤트 또는 특수 애니메이션) final isSpecial = _currentSpecialAnimation != null; Border? borderEffect; if (_showCriticalEffect) { - // 크리티컬 히트: 노란색 테두리 (Phase 5) - borderEffect = - Border.all(color: Colors.yellow.withValues(alpha: 0.8), width: 2); + // 크리티컬 히트: 노란색 테두리 + borderEffect = Border.all( + color: Colors.yellow.withValues(alpha: 0.8), + width: 2, + ); + } else if (_showBlockEffect) { + // 블록 (방패 방어): 파란색 테두리 + borderEffect = Border.all( + color: Colors.blue.withValues(alpha: 0.8), + width: 2, + ); + } else if (_showParryEffect) { + // 패리 (무기 쳐내기): 주황색 테두리 + borderEffect = Border.all( + color: Colors.orange.withValues(alpha: 0.8), + width: 2, + ); + } else if (_showSkillEffect) { + // 스킬 사용: 마젠타 테두리 + borderEffect = Border.all( + color: Colors.purple.withValues(alpha: 0.8), + width: 2, + ); } else if (isSpecial) { // 특수 애니메이션: 시안 테두리 - borderEffect = - Border.all(color: AsciiColors.positive.withValues(alpha: 0.5)); + borderEffect = Border.all( + color: AsciiColors.positive.withValues(alpha: 0.5), + ); } return Container( diff --git a/lib/src/features/game/widgets/cinematic_view.dart b/lib/src/features/game/widgets/cinematic_view.dart index 626bf91..d3a7d2d 100644 --- a/lib/src/features/game/widgets/cinematic_view.dart +++ b/lib/src/features/game/widgets/cinematic_view.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'package:flutter/material.dart'; +import 'package:askiineverdie/data/game_text_l10n.dart' as l10n; import 'package:askiineverdie/data/story_data.dart'; /// 시네마틱 뷰 위젯 (Phase 9: Cinematic UI) @@ -162,12 +163,9 @@ class _CinematicViewState extends State right: 16, child: TextButton( onPressed: _skip, - child: const Text( - 'SKIP', - style: TextStyle( - color: Colors.white54, - fontSize: 14, - ), + child: Text( + l10n.uiSkip, + style: const TextStyle(color: Colors.white54, fontSize: 14), ), ), ), @@ -178,7 +176,7 @@ class _CinematicViewState extends State left: 0, right: 0, child: Text( - 'Tap to continue', + l10n.uiTapToContinue, style: TextStyle( color: Colors.white.withValues(alpha: 0.3), fontSize: 12, @@ -246,8 +244,8 @@ class _ProgressDots extends StatelessWidget { color: isActive ? Colors.cyan : isPast - ? Colors.cyan.withValues(alpha: 0.5) - : Colors.white.withValues(alpha: 0.2), + ? Colors.cyan.withValues(alpha: 0.5) + : Colors.white.withValues(alpha: 0.2), ), ); }), diff --git a/lib/src/features/game/widgets/combat_log.dart b/lib/src/features/game/widgets/combat_log.dart index 9a0dd17..2fce2ca 100644 --- a/lib/src/features/game/widgets/combat_log.dart +++ b/lib/src/features/game/widgets/combat_log.dart @@ -148,7 +148,10 @@ class _LogEntryTile extends StatelessWidget { (Color?, IconData?) _getStyleForType(CombatLogType type) { return switch (type) { CombatLogType.normal => (null, null), - CombatLogType.damage => (Colors.red.shade300, Icons.local_fire_department), + CombatLogType.damage => ( + Colors.red.shade300, + Icons.local_fire_department, + ), CombatLogType.heal => (Colors.green.shade300, Icons.healing), CombatLogType.levelUp => (Colors.amber, Icons.arrow_upward), CombatLogType.questComplete => (Colors.blue.shade300, Icons.check_circle), @@ -158,7 +161,10 @@ class _LogEntryTile extends StatelessWidget { CombatLogType.evade => (Colors.cyan.shade300, Icons.directions_run), CombatLogType.block => (Colors.blueGrey.shade300, Icons.shield), CombatLogType.parry => (Colors.teal.shade300, Icons.sports_kabaddi), - CombatLogType.monsterAttack => (Colors.deepOrange.shade300, Icons.dangerous), + CombatLogType.monsterAttack => ( + Colors.deepOrange.shade300, + Icons.dangerous, + ), CombatLogType.buff => (Colors.lightBlue.shade300, Icons.trending_up), CombatLogType.dotTick => (Colors.deepPurple.shade300, Icons.whatshot), CombatLogType.potion => (Colors.pink.shade300, Icons.local_drink), diff --git a/lib/src/features/game/widgets/death_overlay.dart b/lib/src/features/game/widgets/death_overlay.dart index ed6700e..09b92a1 100644 --- a/lib/src/features/game/widgets/death_overlay.dart +++ b/lib/src/features/game/widgets/death_overlay.dart @@ -1,5 +1,7 @@ import 'package:flutter/material.dart'; +import 'package:askiineverdie/data/game_text_l10n.dart' as l10n; +import 'package:askiineverdie/src/core/l10n/game_data_l10n.dart'; import 'package:askiineverdie/src/core/model/combat_event.dart'; import 'package:askiineverdie/src/core/model/game_state.dart'; @@ -109,7 +111,7 @@ class DeathOverlay extends StatelessWidget { ), const SizedBox(height: 16), Text( - 'YOU DIED', + l10n.deathYouDied, style: TextStyle( fontSize: 32, fontWeight: FontWeight.bold, @@ -133,7 +135,7 @@ class DeathOverlay extends StatelessWidget { ), const SizedBox(height: 4), Text( - 'Level ${deathInfo.levelAtDeath} ${traits.klass}', + 'Level ${deathInfo.levelAtDeath} ${GameDataL10n.getKlassName(context, traits.klass)}', style: theme.textTheme.bodyLarge?.copyWith( color: theme.colorScheme.onSurfaceVariant, ), @@ -174,9 +176,9 @@ class DeathOverlay extends StatelessWidget { String _getDeathCauseText() { return switch (deathInfo.cause) { - DeathCause.monster => 'Killed by ${deathInfo.killerName}', - DeathCause.selfDamage => 'Self-inflicted damage', - DeathCause.environment => 'Environmental hazard', + DeathCause.monster => l10n.deathKilledBy(deathInfo.killerName), + DeathCause.selfDamage => l10n.deathSelfInflicted, + DeathCause.environment => l10n.deathEnvironmentalHazard, }; } @@ -210,7 +212,7 @@ class DeathOverlay extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - 'Sacrificed to Resurrect', + l10n.deathSacrificedToResurrect, style: theme.textTheme.labelSmall?.copyWith( color: theme.colorScheme.onSurfaceVariant, ), @@ -234,8 +236,8 @@ class DeathOverlay extends StatelessWidget { _buildInfoRow( context, icon: Icons.check_circle_outline, - label: 'Equipment', - value: 'No sacrifice needed', + label: l10n.deathEquipment, + value: l10n.deathNoSacrificeNeeded, isNegative: false, ), const SizedBox(height: 8), @@ -243,7 +245,7 @@ class DeathOverlay extends StatelessWidget { _buildInfoRow( context, icon: Icons.monetization_on_outlined, - label: 'Gold Remaining', + label: l10n.deathGoldRemaining, value: _formatGold(deathInfo.goldAtDeath), isNegative: false, ), @@ -306,7 +308,7 @@ class DeathOverlay extends StatelessWidget { child: FilledButton.icon( onPressed: onResurrect, icon: const Icon(Icons.replay), - label: const Text('Resurrect'), + label: Text(l10n.deathResurrect), style: FilledButton.styleFrom( backgroundColor: theme.colorScheme.primary, padding: const EdgeInsets.symmetric(vertical: 16), @@ -324,7 +326,7 @@ class DeathOverlay extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - 'Combat Log', + l10n.deathCombatLog, style: theme.textTheme.labelMedium?.copyWith( color: theme.colorScheme.onSurfaceVariant, fontWeight: FontWeight.bold, @@ -378,69 +380,70 @@ class DeathOverlay extends StatelessWidget { /// 전투 이벤트를 아이콘, 색상, 메시지로 포맷 (IconData, Color, String) _formatCombatEvent(CombatEvent event) { + final target = event.targetName ?? ''; return switch (event.type) { CombatEventType.playerAttack => ( - event.isCritical ? Icons.flash_on : Icons.local_fire_department, - event.isCritical ? Colors.yellow.shade300 : Colors.green.shade300, - event.isCritical - ? 'CRITICAL! ${event.damage} damage to ${event.targetName}' - : 'Hit ${event.targetName} for ${event.damage} damage', - ), + event.isCritical ? Icons.flash_on : Icons.local_fire_department, + event.isCritical ? Colors.yellow.shade300 : Colors.green.shade300, + event.isCritical + ? l10n.combatCritical(event.damage, target) + : l10n.combatYouHit(target, event.damage), + ), CombatEventType.monsterAttack => ( - Icons.dangerous, - Colors.red.shade300, - '${event.targetName} hits you for ${event.damage} damage', - ), + Icons.dangerous, + Colors.red.shade300, + l10n.combatMonsterHitsYou(target, event.damage), + ), CombatEventType.playerEvade => ( - Icons.directions_run, - Colors.cyan.shade300, - 'Evaded attack from ${event.targetName}', - ), + Icons.directions_run, + Colors.cyan.shade300, + l10n.combatEvadedAttackFrom(target), + ), CombatEventType.monsterEvade => ( - Icons.directions_run, - Colors.orange.shade300, - '${event.targetName} evaded your attack', - ), + Icons.directions_run, + Colors.orange.shade300, + l10n.combatMonsterEvaded(target), + ), CombatEventType.playerBlock => ( - Icons.shield, - Colors.blueGrey.shade300, - 'Blocked ${event.targetName}\'s attack (${event.damage} reduced)', - ), + Icons.shield, + Colors.blueGrey.shade300, + l10n.combatBlockedAttack(target, event.damage), + ), CombatEventType.playerParry => ( - Icons.sports_kabaddi, - Colors.teal.shade300, - 'Parried ${event.targetName}\'s attack (${event.damage} reduced)', - ), + Icons.sports_kabaddi, + Colors.teal.shade300, + l10n.combatParriedAttack(target, event.damage), + ), CombatEventType.playerSkill => ( - Icons.auto_fix_high, - Colors.purple.shade300, - '${event.skillName} deals ${event.damage} damage', - ), + Icons.auto_fix_high, + Colors.purple.shade300, + l10n.combatSkillDamage(event.skillName ?? '', event.damage), + ), CombatEventType.playerHeal => ( - Icons.healing, - Colors.green.shade300, - 'Healed for ${event.healAmount} HP', - ), + Icons.healing, + Colors.green.shade300, + l10n.combatHealedFor(event.healAmount), + ), CombatEventType.playerBuff => ( - Icons.trending_up, - Colors.lightBlue.shade300, - '${event.skillName} activated', - ), + Icons.trending_up, + Colors.lightBlue.shade300, + l10n.combatBuffActivated(event.skillName ?? ''), + ), CombatEventType.dotTick => ( - Icons.whatshot, - Colors.deepOrange.shade300, - '${event.skillName} ticks for ${event.damage} damage', - ), + Icons.whatshot, + Colors.deepOrange.shade300, + l10n.combatDotTick(event.skillName ?? '', event.damage), + ), CombatEventType.playerPotion => ( - Icons.local_drink, - Colors.lightGreen.shade300, - '${event.skillName}: +${event.healAmount} ${event.targetName}', - ), + Icons.local_drink, + Colors.lightGreen.shade300, + l10n.combatPotionUsed(event.skillName ?? '', event.healAmount, target), + ), CombatEventType.potionDrop => ( - Icons.card_giftcard, - Colors.lime.shade300, - 'Dropped: ${event.skillName}', - ), + Icons.card_giftcard, + Colors.lime.shade300, + l10n.combatPotionDrop(event.skillName ?? ''), + ), }; } } diff --git a/lib/src/features/game/widgets/equipment_stats_panel.dart b/lib/src/features/game/widgets/equipment_stats_panel.dart index 9f7f6d4..15e61cb 100644 --- a/lib/src/features/game/widgets/equipment_stats_panel.dart +++ b/lib/src/features/game/widgets/equipment_stats_panel.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; +import 'package:askiineverdie/data/game_text_l10n.dart' as l10n; import 'package:askiineverdie/src/core/engine/item_service.dart'; import 'package:askiineverdie/src/core/model/equipment_item.dart'; import 'package:askiineverdie/src/core/model/equipment_slot.dart'; @@ -135,7 +136,7 @@ class _EmptySlotTile extends StatelessWidget { contentPadding: const EdgeInsets.symmetric(horizontal: 8), leading: _SlotIcon(slot: slot, isEmpty: true), title: Text( - '[${_getSlotName(slot)}] (empty)', + '[${_getSlotName(slot)}] ${l10n.uiEmpty}', style: TextStyle( fontSize: 11, color: Colors.grey.shade600, @@ -222,10 +223,7 @@ class _TotalScoreHeader extends StatelessWidget { padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), decoration: BoxDecoration( gradient: LinearGradient( - colors: [ - Colors.blueGrey.shade700, - Colors.blueGrey.shade600, - ], + colors: [Colors.blueGrey.shade700, Colors.blueGrey.shade600], ), borderRadius: BorderRadius.circular(8), boxShadow: [ @@ -239,11 +237,7 @@ class _TotalScoreHeader extends StatelessWidget { child: Row( children: [ // 장비 아이콘 - const Icon( - Icons.shield, - size: 20, - color: Colors.white70, - ), + const Icon(Icons.shield, size: 20, color: Colors.white70), const SizedBox(width: 8), // 총합 점수 @@ -251,12 +245,9 @@ class _TotalScoreHeader extends StatelessWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - const Text( - 'Equipment Score', - style: TextStyle( - fontSize: 10, - color: Colors.white70, - ), + Text( + l10n.uiEquipmentScore, + style: const TextStyle(fontSize: 10, color: Colors.white70), ), Text( '$totalScore', @@ -304,46 +295,80 @@ class _StatsGrid extends StatelessWidget { final entries = <_StatEntry>[]; // 공격 스탯 - if (stats.atk > 0) entries.add(_StatEntry('ATK', '+${stats.atk}')); - if (stats.magAtk > 0) entries.add(_StatEntry('MATK', '+${stats.magAtk}')); + if (stats.atk > 0) entries.add(_StatEntry(l10n.statAtk, '+${stats.atk}')); + if (stats.magAtk > 0) { + entries.add(_StatEntry(l10n.statMAtk, '+${stats.magAtk}')); + } if (stats.criRate > 0) { - entries.add(_StatEntry('CRI', '${(stats.criRate * 100).toStringAsFixed(1)}%')); + entries.add( + _StatEntry(l10n.statCri, '${(stats.criRate * 100).toStringAsFixed(1)}%'), + ); } if (stats.parryRate > 0) { - entries.add(_StatEntry('PARRY', '${(stats.parryRate * 100).toStringAsFixed(1)}%')); + entries.add( + _StatEntry( + l10n.statParry, + '${(stats.parryRate * 100).toStringAsFixed(1)}%', + ), + ); } // 방어 스탯 - if (stats.def > 0) entries.add(_StatEntry('DEF', '+${stats.def}')); - if (stats.magDef > 0) entries.add(_StatEntry('MDEF', '+${stats.magDef}')); + if (stats.def > 0) entries.add(_StatEntry(l10n.statDef, '+${stats.def}')); + if (stats.magDef > 0) { + entries.add(_StatEntry(l10n.statMDef, '+${stats.magDef}')); + } if (stats.blockRate > 0) { - entries.add(_StatEntry('BLOCK', '${(stats.blockRate * 100).toStringAsFixed(1)}%')); + entries.add( + _StatEntry( + l10n.statBlock, + '${(stats.blockRate * 100).toStringAsFixed(1)}%', + ), + ); } if (stats.evasion > 0) { - entries.add(_StatEntry('EVA', '${(stats.evasion * 100).toStringAsFixed(1)}%')); + entries.add( + _StatEntry(l10n.statEva, '${(stats.evasion * 100).toStringAsFixed(1)}%'), + ); } // 자원 스탯 - if (stats.hpBonus > 0) entries.add(_StatEntry('HP', '+${stats.hpBonus}')); - if (stats.mpBonus > 0) entries.add(_StatEntry('MP', '+${stats.mpBonus}')); + if (stats.hpBonus > 0) { + entries.add(_StatEntry(l10n.statHp, '+${stats.hpBonus}')); + } + if (stats.mpBonus > 0) { + entries.add(_StatEntry(l10n.statMp, '+${stats.mpBonus}')); + } // 능력치 보너스 - if (stats.strBonus > 0) entries.add(_StatEntry('STR', '+${stats.strBonus}')); - if (stats.conBonus > 0) entries.add(_StatEntry('CON', '+${stats.conBonus}')); - if (stats.dexBonus > 0) entries.add(_StatEntry('DEX', '+${stats.dexBonus}')); - if (stats.intBonus > 0) entries.add(_StatEntry('INT', '+${stats.intBonus}')); - if (stats.wisBonus > 0) entries.add(_StatEntry('WIS', '+${stats.wisBonus}')); - if (stats.chaBonus > 0) entries.add(_StatEntry('CHA', '+${stats.chaBonus}')); + if (stats.strBonus > 0) { + entries.add(_StatEntry(l10n.statStr, '+${stats.strBonus}')); + } + if (stats.conBonus > 0) { + entries.add(_StatEntry(l10n.statCon, '+${stats.conBonus}')); + } + if (stats.dexBonus > 0) { + entries.add(_StatEntry(l10n.statDex, '+${stats.dexBonus}')); + } + if (stats.intBonus > 0) { + entries.add(_StatEntry(l10n.statInt, '+${stats.intBonus}')); + } + if (stats.wisBonus > 0) { + entries.add(_StatEntry(l10n.statWis, '+${stats.wisBonus}')); + } + if (stats.chaBonus > 0) { + entries.add(_StatEntry(l10n.statCha, '+${stats.chaBonus}')); + } // 무기 공속 if (slot == EquipmentSlot.weapon && stats.attackSpeed > 0) { - entries.add(_StatEntry('SPEED', '${stats.attackSpeed}ms')); + entries.add(_StatEntry(l10n.statSpeed, '${stats.attackSpeed}ms')); } if (entries.isEmpty) { - return const Text( - 'No bonus stats', - style: TextStyle(fontSize: 10, color: Colors.grey), + return Text( + l10n.uiNoBonusStats, + style: const TextStyle(fontSize: 10, color: Colors.grey), ); } @@ -406,7 +431,7 @@ class _ItemMetaRow extends StatelessWidget { return Row( children: [ Text( - 'Lv.${item.level}', + l10n.uiLevel(item.level), style: const TextStyle(fontSize: 9, color: Colors.grey), ), const SizedBox(width: 8), @@ -420,7 +445,7 @@ class _ItemMetaRow extends StatelessWidget { ), const SizedBox(width: 8), Text( - 'Wt.${item.weight}', + l10n.uiWeight(item.weight), style: const TextStyle(fontSize: 9, color: Colors.grey), ), ], @@ -441,16 +466,16 @@ class _ItemMetaRow extends StatelessWidget { /// 슬롯 이름 반환 String _getSlotName(EquipmentSlot slot) { return switch (slot) { - EquipmentSlot.weapon => 'Weapon', - EquipmentSlot.shield => 'Shield', - EquipmentSlot.helm => 'Helm', - EquipmentSlot.hauberk => 'Hauberk', - EquipmentSlot.brassairts => 'Brassairts', - EquipmentSlot.vambraces => 'Vambraces', - EquipmentSlot.gauntlets => 'Gauntlets', - EquipmentSlot.gambeson => 'Gambeson', - EquipmentSlot.cuisses => 'Cuisses', - EquipmentSlot.greaves => 'Greaves', - EquipmentSlot.sollerets => 'Sollerets', + EquipmentSlot.weapon => l10n.slotWeapon, + EquipmentSlot.shield => l10n.slotShield, + EquipmentSlot.helm => l10n.slotHelm, + EquipmentSlot.hauberk => l10n.slotHauberk, + EquipmentSlot.brassairts => l10n.slotBrassairts, + EquipmentSlot.vambraces => l10n.slotVambraces, + EquipmentSlot.gauntlets => l10n.slotGauntlets, + EquipmentSlot.gambeson => l10n.slotGambeson, + EquipmentSlot.cuisses => l10n.slotCuisses, + EquipmentSlot.greaves => l10n.slotGreaves, + EquipmentSlot.sollerets => l10n.slotSollerets, }; } diff --git a/lib/src/features/game/widgets/hp_mp_bar.dart b/lib/src/features/game/widgets/hp_mp_bar.dart index daf2a31..75d8c1b 100644 --- a/lib/src/features/game/widgets/hp_mp_bar.dart +++ b/lib/src/features/game/widgets/hp_mp_bar.dart @@ -1,5 +1,7 @@ import 'package:flutter/material.dart'; +import 'package:askiineverdie/data/game_text_l10n.dart' as l10n; + /// HP/MP 바 위젯 (Phase 8: 변화 시 시각 효과) /// /// - HP가 20% 미만일 때 빨간색 깜빡임 @@ -151,7 +153,8 @@ class _HpMpBarState extends State with TickerProviderStateMixin { final hpRatio = widget.hpMax > 0 ? widget.hpCurrent / widget.hpMax : 0.0; final mpRatio = widget.mpMax > 0 ? widget.mpCurrent / widget.mpMax : 0.0; - final hasMonster = widget.monsterHpCurrent != null && + final hasMonster = + widget.monsterHpCurrent != null && widget.monsterHpMax != null && widget.monsterHpMax! > 0; @@ -162,7 +165,7 @@ class _HpMpBarState extends State with TickerProviderStateMixin { children: [ // HP 바 (플래시 효과 포함) _buildAnimatedBar( - label: 'HP', + label: l10n.statHp, current: widget.hpCurrent, max: widget.hpMax, ratio: hpRatio, @@ -176,7 +179,7 @@ class _HpMpBarState extends State with TickerProviderStateMixin { // MP 바 (플래시 효과 포함) _buildAnimatedBar( - label: 'MP', + label: l10n.statMp, current: widget.mpCurrent, max: widget.mpMax, ratio: mpRatio, @@ -188,10 +191,7 @@ class _HpMpBarState extends State with TickerProviderStateMixin { ), // 몬스터 HP 바 (전투 중일 때만) - if (hasMonster) ...[ - const SizedBox(height: 8), - _buildMonsterBar(), - ], + if (hasMonster) ...[const SizedBox(height: 8), _buildMonsterBar()], ], ), ); @@ -228,7 +228,13 @@ class _HpMpBarState extends State with TickerProviderStateMixin { ), child: Stack( children: [ - _buildBar(label: label, current: current, max: max, ratio: ratio, color: color), + _buildBar( + label: label, + current: current, + max: max, + ratio: ratio, + color: color, + ), // 플로팅 변화량 텍스트 (위로 떠오르며 사라짐) if (change != 0 && flashController.value > 0.05) @@ -340,8 +346,9 @@ class _HpMpBarState extends State with TickerProviderStateMixin { child: LinearProgressIndicator( value: ratio.clamp(0.0, 1.0), backgroundColor: Colors.orange.withValues(alpha: 0.2), - valueColor: - const AlwaysStoppedAnimation(Colors.orange), + valueColor: const AlwaysStoppedAnimation( + Colors.orange, + ), minHeight: 8, ), ), diff --git a/lib/src/features/game/widgets/notification_overlay.dart b/lib/src/features/game/widgets/notification_overlay.dart index c43c35b..a73a85f 100644 --- a/lib/src/features/game/widgets/notification_overlay.dart +++ b/lib/src/features/game/widgets/notification_overlay.dart @@ -40,20 +40,21 @@ class _NotificationOverlayState extends State vsync: this, ); - _slideAnimation = Tween( - begin: const Offset(0, -1), - end: Offset.zero, - ).animate(CurvedAnimation( - parent: _animationController, - curve: Curves.easeOutBack, - )); + _slideAnimation = + Tween(begin: const Offset(0, -1), end: Offset.zero).animate( + CurvedAnimation( + parent: _animationController, + curve: Curves.easeOutBack, + ), + ); _fadeAnimation = Tween(begin: 0, end: 1).animate( CurvedAnimation(parent: _animationController, curve: Curves.easeIn), ); - _notificationSub = - widget.notificationService.notifications.listen(_onNotification); + _notificationSub = widget.notificationService.notifications.listen( + _onNotification, + ); _dismissSub = widget.notificationService.dismissals.listen(_onDismiss); } @@ -184,35 +185,35 @@ class _NotificationCard extends StatelessWidget { (Color, IconData, Color) _getStyleForType(NotificationType type) { return switch (type) { NotificationType.levelUp => ( - const Color(0xFF1565C0), - Icons.trending_up, - Colors.amber, - ), + const Color(0xFF1565C0), + Icons.trending_up, + Colors.amber, + ), NotificationType.questComplete => ( - const Color(0xFF2E7D32), - Icons.check_circle, - Colors.lightGreen, - ), + const Color(0xFF2E7D32), + Icons.check_circle, + Colors.lightGreen, + ), NotificationType.actComplete => ( - const Color(0xFF6A1B9A), - Icons.flag, - Colors.purpleAccent, - ), + const Color(0xFF6A1B9A), + Icons.flag, + Colors.purpleAccent, + ), NotificationType.newSpell => ( - const Color(0xFF4527A0), - Icons.auto_fix_high, - Colors.deepPurpleAccent, - ), + const Color(0xFF4527A0), + Icons.auto_fix_high, + Colors.deepPurpleAccent, + ), NotificationType.newEquipment => ( - const Color(0xFFE65100), - Icons.shield, - Colors.orange, - ), + const Color(0xFFE65100), + Icons.shield, + Colors.orange, + ), NotificationType.bossDefeat => ( - const Color(0xFFC62828), - Icons.whatshot, - Colors.redAccent, - ), + const Color(0xFFC62828), + Icons.whatshot, + Colors.redAccent, + ), }; } } diff --git a/lib/src/features/game/widgets/potion_inventory_panel.dart b/lib/src/features/game/widgets/potion_inventory_panel.dart index bf81d0d..036ff4c 100644 --- a/lib/src/features/game/widgets/potion_inventory_panel.dart +++ b/lib/src/features/game/widgets/potion_inventory_panel.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; +import 'package:askiineverdie/data/game_text_l10n.dart' as l10n; import 'package:askiineverdie/data/potion_data.dart'; import 'package:askiineverdie/src/core/model/potion.dart'; @@ -22,10 +23,10 @@ class PotionInventoryPanel extends StatelessWidget { final potionEntries = _buildPotionEntries(); if (potionEntries.isEmpty) { - return const Center( + return Center( child: Text( - 'No potions', - style: TextStyle( + l10n.uiNoPotions, + style: const TextStyle( fontSize: 11, color: Colors.grey, fontStyle: FontStyle.italic, @@ -146,11 +147,7 @@ class _PotionRow extends StatelessWidget { // 전투 중 사용 불가 표시 if (isUsedThisBattle) ...[ const SizedBox(width: 4), - const Icon( - Icons.block, - size: 12, - color: Colors.grey, - ), + const Icon(Icons.block, size: 12, color: Colors.grey), ], ], ), @@ -213,10 +210,7 @@ class _HealBadge extends StatelessWidget { ), child: Text( healText, - style: TextStyle( - fontSize: 9, - color: Colors.grey.shade700, - ), + style: TextStyle(fontSize: 9, color: Colors.grey.shade700), ), ); } diff --git a/lib/src/features/game/widgets/skill_panel.dart b/lib/src/features/game/widgets/skill_panel.dart index ce8b518..298b122 100644 --- a/lib/src/features/game/widgets/skill_panel.dart +++ b/lib/src/features/game/widgets/skill_panel.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; +import 'package:askiineverdie/data/game_text_l10n.dart' as l10n; import 'package:askiineverdie/data/skill_data.dart'; import 'package:askiineverdie/src/core/model/game_state.dart'; import 'package:askiineverdie/src/core/model/skill.dart'; @@ -90,8 +91,8 @@ class _SkillPanelState extends State final skillStates = widget.skillSystem.skillStates; if (skillStates.isEmpty) { - return const Center( - child: Text('No skills', style: TextStyle(fontSize: 11)), + return Center( + child: Text(l10n.uiNoSkills, style: const TextStyle(fontSize: 11)), ); } @@ -143,7 +144,7 @@ class _SkillRow extends StatelessWidget { @override Widget build(BuildContext context) { final cooldownText = isReady - ? 'Ready' + ? l10n.uiReady : '${(remainingMs / 1000).toStringAsFixed(1)}s'; final skillIcon = _getSkillIcon(skill.type); @@ -192,9 +193,9 @@ class _SkillRow extends StatelessWidget { color: elementColor.withValues(alpha: 0.3), borderRadius: BorderRadius.circular(3), ), - child: const Text( - 'DOT', - style: TextStyle(fontSize: 7, color: Colors.white70), + child: Text( + l10n.uiDot, + style: const TextStyle(fontSize: 7, color: Colors.white70), ), ), const SizedBox(width: 4), @@ -202,7 +203,7 @@ class _SkillRow extends StatelessWidget { // 랭크 Text( - 'Lv.$rank', + l10n.uiLevel(rank), style: const TextStyle(fontSize: 9, color: Colors.grey), ), const SizedBox(width: 4), @@ -233,7 +234,9 @@ class _SkillRow extends StatelessWidget { borderRadius: BorderRadius.circular(4), boxShadow: [ BoxShadow( - color: skillColor.withValues(alpha: glowAnimation.value * 0.5), + color: skillColor.withValues( + alpha: glowAnimation.value * 0.5, + ), blurRadius: 8 * glowAnimation.value, spreadRadius: 2 * glowAnimation.value, ), @@ -332,11 +335,7 @@ class _ElementBadge extends StatelessWidget { ? Border.all(color: color.withValues(alpha: 0.7), width: 1) : null, ), - child: Icon( - icon, - size: 10, - color: color, - ), + child: Icon(icon, size: 10, color: color), ); } } diff --git a/lib/src/features/game/widgets/task_progress_panel.dart b/lib/src/features/game/widgets/task_progress_panel.dart index 53d19b4..13ce42d 100644 --- a/lib/src/features/game/widgets/task_progress_panel.dart +++ b/lib/src/features/game/widgets/task_progress_panel.dart @@ -137,8 +137,9 @@ class TaskProgressPanel extends StatelessWidget { child: Text( '${speedMultiplier}x', style: TextStyle( - fontWeight: - speedMultiplier > 1 ? FontWeight.bold : FontWeight.normal, + fontWeight: speedMultiplier > 1 + ? FontWeight.bold + : FontWeight.normal, color: speedMultiplier > 1 ? Theme.of(context).colorScheme.primary : null, @@ -157,8 +158,9 @@ class TaskProgressPanel extends StatelessWidget { padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), child: LinearProgressIndicator( value: progressValue, - backgroundColor: - Theme.of(context).colorScheme.primary.withValues(alpha: 0.2), + backgroundColor: Theme.of( + context, + ).colorScheme.primary.withValues(alpha: 0.2), valueColor: AlwaysStoppedAnimation( Theme.of(context).colorScheme.primary, ), diff --git a/lib/src/features/hall_of_fame/hall_of_fame_screen.dart b/lib/src/features/hall_of_fame/hall_of_fame_screen.dart index afd4553..f5e25b8 100644 --- a/lib/src/features/hall_of_fame/hall_of_fame_screen.dart +++ b/lib/src/features/hall_of_fame/hall_of_fame_screen.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; +import 'package:askiineverdie/data/game_text_l10n.dart' as l10n; import 'package:askiineverdie/src/core/l10n/game_data_l10n.dart'; import 'package:askiineverdie/src/core/model/hall_of_fame.dart'; import 'package:askiineverdie/src/core/storage/hall_of_fame_storage.dart'; @@ -36,10 +37,7 @@ class _HallOfFameScreenState extends State { @override Widget build(BuildContext context) { return Scaffold( - appBar: AppBar( - title: const Text('Hall of Fame'), - centerTitle: true, - ), + appBar: AppBar(title: Text(l10n.uiHallOfFame), centerTitle: true), body: _isLoading ? const Center(child: CircularProgressIndicator()) : _buildContent(), @@ -67,19 +65,13 @@ class _HallOfFameScreenState extends State { ), const SizedBox(height: 16), Text( - 'No heroes yet', - style: TextStyle( - fontSize: 20, - color: Colors.grey.shade600, - ), + l10n.hofNoHeroes, + style: TextStyle(fontSize: 20, color: Colors.grey.shade600), ), const SizedBox(height: 8), Text( - 'Defeat the Glitch God to enshrine your legend!', - style: TextStyle( - fontSize: 14, - color: Colors.grey.shade500, - ), + l10n.hofDefeatGlitchGod, + style: TextStyle(fontSize: 14, color: Colors.grey.shade500), textAlign: TextAlign.center, ), ], @@ -106,22 +98,22 @@ class _HallOfFameScreenState extends State { topRight: Radius.circular(6), ), ), - child: const Row( + child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ - Icon(Icons.emoji_events, color: Colors.white), - SizedBox(width: 8), + const Icon(Icons.emoji_events, color: Colors.white), + const SizedBox(width: 8), Text( - 'HALL OF FAME', - style: TextStyle( + l10n.uiHallOfFame.toUpperCase(), + style: const TextStyle( color: Colors.white, fontSize: 18, fontWeight: FontWeight.bold, letterSpacing: 2, ), ), - SizedBox(width: 8), - Icon(Icons.emoji_events, color: Colors.white), + const SizedBox(width: 8), + const Icon(Icons.emoji_events, color: Colors.white), ], ), ), @@ -132,10 +124,7 @@ class _HallOfFameScreenState extends State { itemCount: hallOfFame.entries.length, itemBuilder: (context, index) { final entry = hallOfFame.entries[index]; - return _HallOfFameEntryCard( - entry: entry, - rank: index + 1, - ); + return _HallOfFameEntryCard(entry: entry, rank: index + 1); }, ), ), @@ -147,10 +136,7 @@ class _HallOfFameScreenState extends State { /// 명예의 전당 엔트리 카드 class _HallOfFameEntryCard extends StatelessWidget { - const _HallOfFameEntryCard({ - required this.entry, - required this.rank, - }); + const _HallOfFameEntryCard({required this.entry, required this.rank}); final HallOfFameEntry entry; final int rank; @@ -217,7 +203,7 @@ class _HallOfFameEntryCard extends StatelessWidget { borderRadius: BorderRadius.circular(4), ), child: Text( - 'Lv.${entry.level}', + l10n.uiLevel(entry.level), style: TextStyle( color: Colors.blue.shade800, fontWeight: FontWeight.bold, @@ -232,10 +218,7 @@ class _HallOfFameEntryCard extends StatelessWidget { Text( '${GameDataL10n.getRaceName(context, entry.race)} ' '${GameDataL10n.getKlassName(context, entry.klass)}', - style: TextStyle( - fontSize: 13, - color: Colors.grey.shade700, - ), + style: TextStyle(fontSize: 13, color: Colors.grey.shade700), ), const SizedBox(height: 4), // 통계 @@ -267,14 +250,15 @@ class _HallOfFameEntryCard extends StatelessWidget { Column( crossAxisAlignment: CrossAxisAlignment.end, children: [ - Icon(Icons.calendar_today, size: 14, color: Colors.grey.shade500), + Icon( + Icons.calendar_today, + size: 14, + color: Colors.grey.shade500, + ), const SizedBox(height: 4), Text( entry.formattedClearedDate, - style: TextStyle( - fontSize: 11, - color: Colors.grey.shade600, - ), + style: TextStyle(fontSize: 11, color: Colors.grey.shade600), ), ], ), @@ -362,22 +346,22 @@ class _GameClearDialog extends StatelessWidget { @override Widget build(BuildContext context) { return AlertDialog( - title: const Row( + title: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ - Icon(Icons.emoji_events, color: Colors.amber, size: 32), - SizedBox(width: 8), - Text('VICTORY!'), - SizedBox(width: 8), - Icon(Icons.emoji_events, color: Colors.amber, size: 32), + const Icon(Icons.emoji_events, color: Colors.amber, size: 32), + const SizedBox(width: 8), + Text(l10n.hofVictory), + const SizedBox(width: 8), + const Icon(Icons.emoji_events, color: Colors.amber, size: 32), ], ), content: Column( mainAxisSize: MainAxisSize.min, children: [ - const Text( - 'You have defeated the Glitch God!', - style: TextStyle(fontSize: 16), + Text( + l10n.hofDefeatedGlitchGod, + style: const TextStyle(fontSize: 16), textAlign: TextAlign.center, ), const SizedBox(height: 16), @@ -386,14 +370,11 @@ class _GameClearDialog extends StatelessWidget { // 캐릭터 정보 Text( '"${entry.characterName}"', - style: const TextStyle( - fontSize: 18, - fontWeight: FontWeight.bold, - ), + style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold), ), const SizedBox(height: 4), Text( - '${entry.race} ${entry.klass}', + '${GameDataL10n.getRaceName(context, entry.race)} ${GameDataL10n.getKlassName(context, entry.klass)}', style: TextStyle(color: Colors.grey.shade600), ), const SizedBox(height: 16), @@ -401,16 +382,16 @@ class _GameClearDialog extends StatelessWidget { Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ - _buildStat('Level', '${entry.level}'), - _buildStat('Time', entry.formattedPlayTime), - _buildStat('Deaths', '${entry.totalDeaths}'), - _buildStat('Quests', '${entry.questsCompleted}'), + _buildStat(l10n.hofLevel, '${entry.level}'), + _buildStat(l10n.hofTime, entry.formattedPlayTime), + _buildStat(l10n.hofDeaths, '${entry.totalDeaths}'), + _buildStat(l10n.hofQuests, '${entry.questsCompleted}'), ], ), const SizedBox(height: 16), - const Text( - 'Your legend has been enshrined in the Hall of Fame!', - style: TextStyle( + Text( + l10n.hofLegendEnshrined, + style: const TextStyle( fontStyle: FontStyle.italic, color: Colors.amber, ), @@ -424,14 +405,14 @@ class _GameClearDialog extends StatelessWidget { Navigator.of(context).pop(); onViewHallOfFame(); }, - child: const Text('View Hall of Fame'), + child: Text(l10n.hofViewHallOfFame), ), FilledButton( onPressed: () { Navigator.of(context).pop(); onNewGame(); }, - child: const Text('New Game'), + child: Text(l10n.hofNewGame), ), ], ); @@ -442,17 +423,11 @@ class _GameClearDialog extends StatelessWidget { children: [ Text( value, - style: const TextStyle( - fontSize: 18, - fontWeight: FontWeight.bold, - ), + style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold), ), Text( label, - style: TextStyle( - fontSize: 11, - color: Colors.grey.shade600, - ), + style: TextStyle(fontSize: 11, color: Colors.grey.shade600), ), ], ); diff --git a/lib/src/features/new_character/new_character_screen.dart b/lib/src/features/new_character/new_character_screen.dart index 8664935..aac8f00 100644 --- a/lib/src/features/new_character/new_character_screen.dart +++ b/lib/src/features/new_character/new_character_screen.dart @@ -3,12 +3,14 @@ import 'dart:math' as math; import 'package:flutter/material.dart'; import 'package:askiineverdie/data/class_data.dart'; +import 'package:askiineverdie/data/game_text_l10n.dart' as game_l10n; import 'package:askiineverdie/data/race_data.dart'; import 'package:askiineverdie/l10n/app_localizations.dart'; import 'package:askiineverdie/src/core/model/class_traits.dart'; import 'package:askiineverdie/src/core/model/game_state.dart'; import 'package:askiineverdie/src/core/model/race_traits.dart'; import 'package:askiineverdie/src/core/util/deterministic_random.dart'; +import 'package:askiineverdie/src/core/l10n/game_data_l10n.dart'; import 'package:askiineverdie/src/core/util/pq_logic.dart'; /// 캐릭터 생성 화면 (NewGuy.pas 포팅) @@ -396,7 +398,10 @@ class _NewCharacterScreenState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text(L10n.of(context).race, style: Theme.of(context).textTheme.titleMedium), + Text( + L10n.of(context).race, + style: Theme.of(context).textTheme.titleMedium, + ), const SizedBox(height: 8), SizedBox( height: 300, @@ -415,7 +420,7 @@ class _NewCharacterScreenState extends State { : null, ), title: Text( - race.name, + GameDataL10n.getRaceName(context, race.name), style: TextStyle( fontWeight: isSelected ? FontWeight.bold @@ -445,7 +450,7 @@ class _NewCharacterScreenState extends State { } final passiveDesc = race.passives.isNotEmpty - ? race.passives.map((p) => p.description).join(', ') + ? race.passives.map((p) => _translateRacePassive(p)).join(', ') : ''; return Column( @@ -460,21 +465,35 @@ class _NewCharacterScreenState extends State { Text( passiveDesc, style: Theme.of(context).textTheme.bodySmall?.copyWith( - color: Theme.of(context).colorScheme.primary, - ), + color: Theme.of(context).colorScheme.primary, + ), ), ], ); } + /// 종족 패시브 설명 번역 + String _translateRacePassive(PassiveAbility passive) { + final percent = (passive.value * 100).round(); + return switch (passive.type) { + PassiveType.hpBonus => game_l10n.passiveHpBonus(percent), + PassiveType.mpBonus => game_l10n.passiveMpBonus(percent), + PassiveType.defenseBonus => game_l10n.passiveDefenseBonus(percent), + PassiveType.magicDamageBonus => game_l10n.passiveMagicBonus(percent), + PassiveType.criticalBonus => game_l10n.passiveCritBonus(percent), + PassiveType.expBonus => passive.description, + PassiveType.deathEquipmentPreserve => passive.description, + }; + } + String _statName(StatType type) { return switch (type) { - StatType.str => 'STR', - StatType.con => 'CON', - StatType.dex => 'DEX', - StatType.intelligence => 'INT', - StatType.wis => 'WIS', - StatType.cha => 'CHA', + StatType.str => game_l10n.statStr, + StatType.con => game_l10n.statCon, + StatType.dex => game_l10n.statDex, + StatType.intelligence => game_l10n.statInt, + StatType.wis => game_l10n.statWis, + StatType.cha => game_l10n.statCha, }; } @@ -485,7 +504,10 @@ class _NewCharacterScreenState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text(L10n.of(context).classTitle, style: Theme.of(context).textTheme.titleMedium), + Text( + L10n.of(context).classTitle, + style: Theme.of(context).textTheme.titleMedium, + ), const SizedBox(height: 8), SizedBox( height: 300, @@ -504,7 +526,7 @@ class _NewCharacterScreenState extends State { : null, ), title: Text( - klass.name, + GameDataL10n.getKlassName(context, klass.name), style: TextStyle( fontWeight: isSelected ? FontWeight.bold @@ -534,7 +556,7 @@ class _NewCharacterScreenState extends State { } final passiveDesc = klass.passives.isNotEmpty - ? klass.passives.map((p) => p.description).join(', ') + ? klass.passives.map((p) => _translateClassPassive(p)).join(', ') : ''; return Column( @@ -549,10 +571,28 @@ class _NewCharacterScreenState extends State { Text( passiveDesc, style: Theme.of(context).textTheme.bodySmall?.copyWith( - color: Theme.of(context).colorScheme.secondary, - ), + color: Theme.of(context).colorScheme.secondary, + ), ), ], ); } + + /// 클래스 패시브 설명 번역 + String _translateClassPassive(ClassPassive passive) { + final percent = (passive.value * 100).round(); + return switch (passive.type) { + ClassPassiveType.hpBonus => game_l10n.passiveHpBonus(percent), + ClassPassiveType.physicalDamageBonus => + game_l10n.passivePhysicalBonus(percent), + ClassPassiveType.defenseBonus => game_l10n.passiveDefenseBonus(percent), + ClassPassiveType.magicDamageBonus => game_l10n.passiveMagicBonus(percent), + ClassPassiveType.evasionBonus => game_l10n.passiveEvasionBonus(percent), + ClassPassiveType.criticalBonus => game_l10n.passiveCritBonus(percent), + ClassPassiveType.postCombatHeal => game_l10n.passiveHpRegen(percent), + ClassPassiveType.healingBonus => passive.description, + ClassPassiveType.multiAttack => passive.description, + ClassPassiveType.firstStrikeBonus => passive.description, + }; + } } diff --git a/test/core/util/pq_logic_test.dart b/test/core/util/pq_logic_test.dart index 1d8a842..4418b3a 100644 --- a/test/core/util/pq_logic_test.dart +++ b/test/core/util/pq_logic_test.dart @@ -48,7 +48,10 @@ void main() { // 아스키나라(ASCII-Nara) 세계관 데이터로 업데이트 // 결정론적 결과가 일관되게 생성되는지 확인 (비어있지 않음) expect(pq_logic.boringItem(config, DeterministicRandom(12)), isNotEmpty); - expect(pq_logic.interestingItem(config, DeterministicRandom(12)), isNotEmpty); + expect( + pq_logic.interestingItem(config, DeterministicRandom(12)), + isNotEmpty, + ); expect(pq_logic.specialItem(config, DeterministicRandom(12)), isNotEmpty); // 원본 Main.pas:770-774 RandomLow 방식으로 수정됨 final spell = pq_logic.winSpell(config, DeterministicRandom(22), 7, 4); diff --git a/test/regression/deterministic_game_test.dart b/test/regression/deterministic_game_test.dart index ac4ea59..7627799 100644 --- a/test/regression/deterministic_game_test.dart +++ b/test/regression/deterministic_game_test.dart @@ -113,11 +113,7 @@ void main() { test('winItem produces consistent items', () { // 아스키나라(ASCII-Nara) 세계관 데이터로 업데이트 // 시드 42에서 아이템 획득 - final item1 = pq_logic.winItem( - config, - DeterministicRandom(testSeed), - 5, - ); + final item1 = pq_logic.winItem(config, DeterministicRandom(testSeed), 5); expect(item1, isNotEmpty); expect(item1, contains(' of '));