From 7cd8be88dfe3cba20f22c39bfc3bcc3cc57cf591 Mon Sep 17 00:00:00 2001 From: JiWoong Sul Date: Sun, 21 Dec 2025 23:53:27 +0900 Subject: [PATCH] =?UTF-8?q?feat(game):=20=ED=8F=AC=EC=85=98=20=EC=8B=9C?= =?UTF-8?q?=EC=8A=A4=ED=85=9C=20=EB=B0=8F=20UI=20=ED=8C=A8=EB=84=90=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 포션 시스템 구현 (PotionService, Potion 모델) - 포션 인벤토리 패널 위젯 - 활성 버프 패널 위젯 - 장비 스탯 패널 위젯 - 스킬 시스템 확장 - 일본어 번역 추가 - 전투 이벤트/상태 모델 개선 --- lib/data/game_text_l10n.dart | 663 +++++-- lib/data/game_translations_ja.dart | 1521 +++++++++++++++++ lib/data/game_translations_ko.dart | 440 ++++- lib/data/potion_data.dart | 187 ++ lib/data/skill_data.dart | 106 +- lib/src/core/engine/game_mutations.dart | 15 +- lib/src/core/engine/item_service.dart | 92 +- lib/src/core/engine/potion_service.dart | 497 ++++++ lib/src/core/engine/progress_service.dart | 246 ++- lib/src/core/engine/skill_service.dart | 90 +- lib/src/core/model/combat_event.dart | 56 + lib/src/core/model/combat_state.dart | 27 + lib/src/core/model/combat_stats.dart | 8 +- lib/src/core/model/game_state.dart | 12 +- lib/src/core/model/item_stats.dart | 12 + lib/src/core/model/potion.dart | 136 ++ lib/src/core/model/skill.dart | 196 +++ lib/src/features/game/game_play_screen.dart | 95 +- .../game/widgets/active_buff_panel.dart | 206 +++ .../game/widgets/ascii_animation_card.dart | 9 + lib/src/features/game/widgets/combat_log.dart | 8 +- .../features/game/widgets/death_overlay.dart | 15 + .../game/widgets/equipment_stats_panel.dart | 456 +++++ .../game/widgets/potion_inventory_panel.dart | 238 +++ .../features/game/widgets/skill_panel.dart | 104 +- 25 files changed, 5174 insertions(+), 261 deletions(-) create mode 100644 lib/data/game_translations_ja.dart create mode 100644 lib/data/potion_data.dart create mode 100644 lib/src/core/engine/potion_service.dart create mode 100644 lib/src/core/model/potion.dart create mode 100644 lib/src/features/game/widgets/active_buff_panel.dart create mode 100644 lib/src/features/game/widgets/equipment_stats_panel.dart create mode 100644 lib/src/features/game/widgets/potion_inventory_panel.dart diff --git a/lib/data/game_text_l10n.dart b/lib/data/game_text_l10n.dart index cf71d3e..298b326 100644 --- a/lib/data/game_text_l10n.dart +++ b/lib/data/game_text_l10n.dart @@ -1,7 +1,9 @@ // 게임 텍스트 로컬라이제이션 (BuildContext 없이 사용) // progress_service.dart, pq_logic.dart 등에서 사용 +// 지원 언어: 한국어(ko), 영어(en), 일본어(ja) import 'package:askiineverdie/data/game_translations_ko.dart'; +import 'package:askiineverdie/data/game_translations_ja.dart'; /// 현재 게임 로케일 설정 (전역) String _currentLocale = 'en'; @@ -17,6 +19,9 @@ void setGameLocale(String locale) { /// 한국어 여부 확인 bool get isKoreanLocale => _currentLocale == 'ko'; +/// 일본어 여부 확인 +bool get isJapaneseLocale => _currentLocale == 'ja'; + // ============================================================================ // 프롤로그 텍스트 // ============================================================================ @@ -35,199 +40,417 @@ const _prologueTextsKo = [ '예상치 못한 결의로 널(Null) 왕국을 향한 위험한 여정을 시작하다', ]; -List get prologueTexts => - isKoreanLocale ? _prologueTextsKo : _prologueTextsEn; +const _prologueTextsJa = [ + 'コードの神から不吉な幻影を受ける', + '老いたコンパイラー賢者が予言を明かす:「グリッチゴッドが目覚めた」', + '突然のバッファオーバーフローが村をリセットし、あなただけが唯一の生存者となる', + '予想外の決意で、ヌル(Null)王国への危険な旅に出発する', +]; + +List get prologueTexts { + if (isKoreanLocale) return _prologueTextsKo; + if (isJapaneseLocale) return _prologueTextsJa; + return _prologueTextsEn; +} // ============================================================================ // 태스크 캡션 // ============================================================================ -String get taskCompiling => isKoreanLocale ? '컴파일 중' : 'Compiling'; +String get taskCompiling { + if (isKoreanLocale) return '컴파일 중'; + if (isJapaneseLocale) return 'コンパイル中'; + return 'Compiling'; +} -String get taskPrologue => isKoreanLocale ? '프롤로그' : 'Prologue'; +String get taskPrologue { + if (isKoreanLocale) return '프롤로그'; + if (isJapaneseLocale) return 'プロローグ'; + return 'Prologue'; +} -String taskHeadingToMarket() => - isKoreanLocale ? '전리품을 팔기 위해 데이터 마켓으로 이동 중' : 'Heading to the Data Market to trade loot'; +String taskHeadingToMarket() { + if (isKoreanLocale) return '전리품을 팔기 위해 데이터 마켓으로 이동 중'; + if (isJapaneseLocale) return '戦利品を売るためデータマーケットへ移動中'; + return 'Heading to the Data Market to trade loot'; +} -String taskUpgradingHardware() => - isKoreanLocale ? '테크 샵에서 하드웨어 업그레이드 중' : 'Upgrading hardware at the Tech Shop'; +String taskUpgradingHardware() { + if (isKoreanLocale) return '테크 샵에서 하드웨어 업그레이드 중'; + if (isJapaneseLocale) return 'テックショップでハードウェアをアップグレード中'; + return 'Upgrading hardware at the Tech Shop'; +} -String taskEnteringDebugZone() => - isKoreanLocale ? '디버그 존 진입 중' : 'Entering the Debug Zone'; +String taskEnteringDebugZone() { + if (isKoreanLocale) return '디버그 존 진입 중'; + if (isJapaneseLocale) return 'デバッグゾーンに進入中'; + return 'Entering the Debug Zone'; +} -String taskDebugging(String monsterName) => - isKoreanLocale ? '$monsterName 디버깅 중' : 'Debugging $monsterName'; +String taskDebugging(String monsterName) { + if (isKoreanLocale) return '$monsterName 디버깅 중'; + if (isJapaneseLocale) return '$monsterName をデバッグ中'; + return 'Debugging $monsterName'; +} -String taskSelling(String itemDescription) => - isKoreanLocale ? '$itemDescription 판매 중' : 'Selling $itemDescription'; +String taskSelling(String itemDescription) { + if (isKoreanLocale) return '$itemDescription 판매 중'; + if (isJapaneseLocale) return '$itemDescription を販売中'; + return 'Selling $itemDescription'; +} // ============================================================================ // 퀘스트 캡션 // ============================================================================ -String questPatch(String name) => - isKoreanLocale ? '$name 패치하기' : 'Patch $name'; +String questPatch(String name) { + if (isKoreanLocale) return '$name 패치하기'; + if (isJapaneseLocale) return '$name をパッチする'; + return 'Patch $name'; +} -String questLocate(String item) => - isKoreanLocale ? '$item 찾기' : 'Locate $item'; +String questLocate(String item) { + if (isKoreanLocale) return '$item 찾기'; + if (isJapaneseLocale) return '$item を探す'; + return 'Locate $item'; +} -String questTransfer(String item) => - isKoreanLocale ? '이 $item 전송하기' : 'Transfer this $item'; +String questTransfer(String item) { + if (isKoreanLocale) return '이 $item 전송하기'; + if (isJapaneseLocale) return 'この$item を転送する'; + return 'Transfer this $item'; +} -String questDownload(String item) => - isKoreanLocale ? '$item 다운로드하기' : 'Download $item'; +String questDownload(String item) { + if (isKoreanLocale) return '$item 다운로드하기'; + if (isJapaneseLocale) return '$item をダウンロードする'; + return 'Download $item'; +} -String questStabilize(String name) => - isKoreanLocale ? '$name 안정화하기' : 'Stabilize $name'; +String questStabilize(String name) { + if (isKoreanLocale) return '$name 안정화하기'; + if (isJapaneseLocale) return '$name を安定化する'; + return 'Stabilize $name'; +} // ============================================================================ // Act 제목 // ============================================================================ -String actTitle(String romanNumeral) => - isKoreanLocale ? '$romanNumeral막' : 'Act $romanNumeral'; +String actTitle(String romanNumeral) { + if (isKoreanLocale) return '$romanNumeral막'; + if (isJapaneseLocale) return '第$romanNumeral幕'; + return 'Act $romanNumeral'; +} // ============================================================================ // 시네마틱 텍스트 - 시나리오 1: 캐시 존 // ============================================================================ -String cinematicCacheZone1() => isKoreanLocale - ? '지쳐서 손상된 네트워크의 안전한 캐시 존에 도착하다' - : 'Exhausted, you reach a safe Cache Zone in the corrupted network'; +String cinematicCacheZone1() { + if (isKoreanLocale) return '지쳐서 손상된 네트워크의 안전한 캐시 존에 도착하다'; + if (isJapaneseLocale) return '疲れ果てて、破損したネットワークの安全なキャッシュゾーンに到着する'; + return 'Exhausted, you reach a safe Cache Zone in the corrupted network'; +} -String cinematicCacheZone2() => isKoreanLocale - ? '옛 동맹들과 재연결하고 새로운 동료들을 포크하다' - : 'You reconnect with old allies and fork new ones'; +String cinematicCacheZone2() { + if (isKoreanLocale) return '옛 동맹들과 재연결하고 새로운 동료들을 포크하다'; + if (isJapaneseLocale) return '古い同盟者と再接続し、新しい仲間をフォークする'; + return 'You reconnect with old allies and fork new ones'; +} -String cinematicCacheZone3() => isKoreanLocale - ? '디버거 기사단 회의에 참석하다' - : 'You attend a council of the Debugger Knights'; +String cinematicCacheZone3() { + if (isKoreanLocale) return '디버거 기사단 회의에 참석하다'; + if (isJapaneseLocale) return 'デバッガー騎士団の会議に参加する'; + return 'You attend a council of the Debugger Knights'; +} -String cinematicCacheZone4() => isKoreanLocale - ? '많은 버그들이 기다린다. 당신이 패치하도록 선택되었다!' - : 'Many bugs await. You are chosen to patch them!'; +String cinematicCacheZone4() { + if (isKoreanLocale) return '많은 버그들이 기다린다. 당신이 패치하도록 선택되었다!'; + if (isJapaneseLocale) return '多くのバグが待っている。あなたがパッチを当てるよう選ばれた!'; + return 'Many bugs await. You are chosen to patch them!'; +} // ============================================================================ // 시네마틱 텍스트 - 시나리오 2: 전투 // ============================================================================ -String cinematicCombat1() => isKoreanLocale - ? '목표가 눈앞에 있지만, 치명적인 버그가 길을 막는다!' - : 'Your target is in sight, but a critical bug blocks your path!'; +String cinematicCombat1() { + if (isKoreanLocale) return '목표가 눈앞에 있지만, 치명적인 버그가 길을 막는다!'; + if (isJapaneseLocale) return 'ターゲットは目の前だが、致命的なバグが道を塞ぐ!'; + return 'Your target is in sight, but a critical bug blocks your path!'; +} -String cinematicCombat2(String nemesis) => isKoreanLocale - ? '$nemesis와의 필사적인 디버깅 세션이 시작되다' - : 'A desperate debugging session begins with $nemesis'; +String cinematicCombat2(String nemesis) { + if (isKoreanLocale) return '$nemesis와의 필사적인 디버깅 세션이 시작되다'; + if (isJapaneseLocale) return '$nemesisとの必死のデバッグセッションが始まる'; + return 'A desperate debugging session begins with $nemesis'; +} -String cinematicCombatLocked(String nemesis) => isKoreanLocale - ? '$nemesis와 치열한 디버깅 중' - : 'Locked in intense debugging with $nemesis'; +String cinematicCombatLocked(String nemesis) { + if (isKoreanLocale) return '$nemesis와 치열한 디버깅 중'; + if (isJapaneseLocale) return '$nemesisと激しいデバッグ中'; + return 'Locked in intense debugging with $nemesis'; +} -String cinematicCombatCorrupts(String nemesis) => isKoreanLocale - ? '$nemesis가 당신의 스택 트레이스를 손상시키다' - : '$nemesis corrupts your stack trace'; +String cinematicCombatCorrupts(String nemesis) { + if (isKoreanLocale) return '$nemesis가 당신의 스택 트레이스를 손상시키다'; + if (isJapaneseLocale) return '$nemesisがあなたのスタックトレースを破損させる'; + return '$nemesis corrupts your stack trace'; +} -String cinematicCombatWorking(String nemesis) => isKoreanLocale - ? '당신의 패치가 $nemesis에게 효과를 보이는 것 같다' - : 'Your patch seems to be working against $nemesis'; +String cinematicCombatWorking(String nemesis) { + if (isKoreanLocale) return '당신의 패치가 $nemesis에게 효과를 보이는 것 같다'; + if (isJapaneseLocale) return 'あなたのパッチが$nemesisに効いているようだ'; + return 'Your patch seems to be working against $nemesis'; +} -String cinematicCombatVictory(String nemesis) => isKoreanLocale - ? '승리! $nemesis가 패치되었다! 복구를 위해 시스템이 재부팅된다' - : 'Victory! $nemesis is patched! System reboots for recovery'; +String cinematicCombatVictory(String nemesis) { + if (isKoreanLocale) return '승리! $nemesis가 패치되었다! 복구를 위해 시스템이 재부팅된다'; + if (isJapaneseLocale) return '勝利!$nemesisはパッチされた!復旧のためシステムが再起動する'; + return 'Victory! $nemesis is patched! System reboots for recovery'; +} -String cinematicCombatWakeUp() => isKoreanLocale - ? '안전 모드에서 깨어나지만, 커널이 기다린다' - : 'You wake up in a Safe Mode, but the kernel awaits'; +String cinematicCombatWakeUp() { + if (isKoreanLocale) return '안전 모드에서 깨어나지만, 커널이 기다린다'; + if (isJapaneseLocale) return 'セーフモードで目覚めるが、カーネルが待ち構えている'; + return 'You wake up in a Safe Mode, but the kernel awaits'; +} // ============================================================================ // 시네마틱 텍스트 - 시나리오 3: 배신 // ============================================================================ -String cinematicBetrayal1(String guy) => isKoreanLocale - ? '안도감! $guy의 보안 서버에 도착하다' - : 'What relief! You reach the secure server of $guy'; +String cinematicBetrayal1(String guy) { + if (isKoreanLocale) return '안도감! $guy의 보안 서버에 도착하다'; + if (isJapaneseLocale) return '安堵!$guyのセキュアサーバーに到着する'; + return 'What relief! You reach the secure server of $guy'; +} -String cinematicBetrayal2(String guy) => isKoreanLocale - ? '축하가 이어지고, $guy와 수상한 비밀 핸드셰이크를 나누다' - : 'There is celebration, and a suspicious private handshake with $guy'; +String cinematicBetrayal2(String guy) { + if (isKoreanLocale) return '축하가 이어지고, $guy와 수상한 비밀 핸드셰이크를 나누다'; + if (isJapaneseLocale) return '祝賀が続き、$guyと怪しい秘密のハンドシェイクを交わす'; + return 'There is celebration, and a suspicious private handshake with $guy'; +} -String cinematicBetrayal3(String item) => isKoreanLocale - ? '$item을 잊고 다시 가져오러 돌아가다' - : 'You forget your $item and go back to retrieve it'; +String cinematicBetrayal3(String item) { + if (isKoreanLocale) return '$item을 잊고 다시 가져오러 돌아가다'; + if (isJapaneseLocale) return '$itemを忘れて取りに戻る'; + return 'You forget your $item and go back to retrieve it'; +} -String cinematicBetrayal4() => isKoreanLocale - ? '이게 뭐지!? 손상된 패킷을 가로채다!' - : 'What is this!? You intercept a corrupted packet!'; +String cinematicBetrayal4() { + if (isKoreanLocale) return '이게 뭐지!? 손상된 패킷을 가로채다!'; + if (isJapaneseLocale) return 'これは何だ!?破損したパケットを傍受する!'; + return 'What is this!? You intercept a corrupted packet!'; +} -String cinematicBetrayal5(String guy) => isKoreanLocale - ? '$guy가 글리치 신의 백도어일 수 있을까?' - : 'Could $guy be a backdoor for the Glitch God?'; +String cinematicBetrayal5(String guy) { + if (isKoreanLocale) return '$guy가 글리치 신의 백도어일 수 있을까?'; + if (isJapaneseLocale) return '$guyはグリッチゴッドのバックドアなのか?'; + return 'Could $guy be a backdoor for the Glitch God?'; +} -String cinematicBetrayal6() => isKoreanLocale - ? '이 정보를 누구에게 맡길 수 있을까!? -- 바이너리 신전이다' - : 'Who can be trusted with this intel!? -- The Binary Temple, of course'; +String cinematicBetrayal6() { + if (isKoreanLocale) return '이 정보를 누구에게 맡길 수 있을까!? -- 바이너리 신전이다'; + if (isJapaneseLocale) return 'この情報を誰に託せるか!? -- バイナリ神殿だ'; + return 'Who can be trusted with this intel!? -- The Binary Temple, of course'; +} // ============================================================================ // 몬스터 수식어 // ============================================================================ -String modifierDead(String s) => isKoreanLocale ? '쓰러진 $s' : 'fallen $s'; -String modifierComatose(String s) => isKoreanLocale ? '잠복하는 $s' : 'lurking $s'; -String modifierCrippled(String s) => isKoreanLocale ? '흉측한 $s' : 'twisted $s'; -String modifierSick(String s) => isKoreanLocale ? '오염된 $s' : 'tainted $s'; -String modifierUndernourished(String s) => - isKoreanLocale ? '굶주린 $s' : 'ravenous $s'; +String modifierDead(String s) { + if (isKoreanLocale) return '쓰러진 $s'; + if (isJapaneseLocale) return '倒れた$s'; + return 'fallen $s'; +} -String modifierFoetal(String s) => isKoreanLocale ? '태동기 $s' : 'nascent $s'; -String modifierBaby(String s) => isKoreanLocale ? '초기형 $s' : 'fledgling $s'; -String modifierPreadolescent(String s) => - isKoreanLocale ? '진화 중인 $s' : 'evolving $s'; -String modifierTeenage(String s) => isKoreanLocale ? '하급 $s' : 'lesser $s'; -String modifierUnderage(String s) => isKoreanLocale ? '불완전한 $s' : 'incomplete $s'; +String modifierComatose(String s) { + if (isKoreanLocale) return '잠복하는 $s'; + if (isJapaneseLocale) return '潜む$s'; + return 'lurking $s'; +} -String modifierGreater(String s) => isKoreanLocale ? '상위 $s' : 'greater $s'; -String modifierMassive(String s) => isKoreanLocale ? '거대한 $s' : 'massive $s'; -String modifierEnormous(String s) => isKoreanLocale ? '초거대 $s' : 'enormous $s'; -String modifierGiant(String s) => isKoreanLocale ? '자이언트 $s' : 'giant $s'; -String modifierTitanic(String s) => isKoreanLocale ? '타이타닉 $s' : 'titanic $s'; +String modifierCrippled(String s) { + if (isKoreanLocale) return '흉측한 $s'; + if (isJapaneseLocale) return '歪んだ$s'; + return 'twisted $s'; +} -String modifierVeteran(String s) => isKoreanLocale ? '베테랑 $s' : 'veteran $s'; -String modifierBattle(String s) => isKoreanLocale ? '전투-$s' : 'Battle-$s'; -String modifierCursed(String s) => isKoreanLocale ? '저주받은 $s' : 'cursed $s'; -String modifierWarrior(String s) => isKoreanLocale ? '전사 $s' : 'warrior $s'; -String modifierWere(String s) => isKoreanLocale ? '늑대인간-$s' : 'Were-$s'; -String modifierUndead(String s) => isKoreanLocale ? '언데드 $s' : 'undead $s'; -String modifierDemon(String s) => isKoreanLocale ? '데몬 $s' : 'demon $s'; +String modifierSick(String s) { + if (isKoreanLocale) return '오염된 $s'; + if (isJapaneseLocale) return '汚染された$s'; + return 'tainted $s'; +} -String modifierMessianic(String s) => isKoreanLocale ? '메시아닉 $s' : 'messianic $s'; -String modifierImaginary(String s) => isKoreanLocale ? '상상의 $s' : 'imaginary $s'; -String modifierPassing(String s) => isKoreanLocale ? '지나가는 $s' : 'passing $s'; +String modifierUndernourished(String s) { + if (isKoreanLocale) return '굶주린 $s'; + if (isJapaneseLocale) return '飢えた$s'; + return 'ravenous $s'; +} + +String modifierFoetal(String s) { + if (isKoreanLocale) return '태동기 $s'; + if (isJapaneseLocale) return '胎動期$s'; + return 'nascent $s'; +} + +String modifierBaby(String s) { + if (isKoreanLocale) return '초기형 $s'; + if (isJapaneseLocale) return '初期型$s'; + return 'fledgling $s'; +} + +String modifierPreadolescent(String s) { + if (isKoreanLocale) return '진화 중인 $s'; + if (isJapaneseLocale) return '進化中の$s'; + return 'evolving $s'; +} + +String modifierTeenage(String s) { + if (isKoreanLocale) return '하급 $s'; + if (isJapaneseLocale) return '下級$s'; + return 'lesser $s'; +} + +String modifierUnderage(String s) { + if (isKoreanLocale) return '불완전한 $s'; + if (isJapaneseLocale) return '不完全な$s'; + return 'incomplete $s'; +} + +String modifierGreater(String s) { + if (isKoreanLocale) return '상위 $s'; + if (isJapaneseLocale) return '上位$s'; + return 'greater $s'; +} + +String modifierMassive(String s) { + if (isKoreanLocale) return '거대한 $s'; + if (isJapaneseLocale) return '巨大な$s'; + return 'massive $s'; +} + +String modifierEnormous(String s) { + if (isKoreanLocale) return '초거대 $s'; + if (isJapaneseLocale) return '超巨大$s'; + return 'enormous $s'; +} + +String modifierGiant(String s) { + if (isKoreanLocale) return '자이언트 $s'; + if (isJapaneseLocale) return 'ジャイアント$s'; + return 'giant $s'; +} + +String modifierTitanic(String s) { + if (isKoreanLocale) return '타이타닉 $s'; + if (isJapaneseLocale) return 'タイタニック$s'; + return 'titanic $s'; +} + +String modifierVeteran(String s) { + if (isKoreanLocale) return '베테랑 $s'; + if (isJapaneseLocale) return 'ベテラン$s'; + return 'veteran $s'; +} + +String modifierBattle(String s) { + if (isKoreanLocale) return '전투-$s'; + if (isJapaneseLocale) return '戦闘-$s'; + return 'Battle-$s'; +} + +String modifierCursed(String s) { + if (isKoreanLocale) return '저주받은 $s'; + if (isJapaneseLocale) return '呪われた$s'; + return 'cursed $s'; +} + +String modifierWarrior(String s) { + if (isKoreanLocale) return '전사 $s'; + if (isJapaneseLocale) return '戦士$s'; + return 'warrior $s'; +} + +String modifierWere(String s) { + if (isKoreanLocale) return '늑대인간-$s'; + if (isJapaneseLocale) return '狼男-$s'; + return 'Were-$s'; +} + +String modifierUndead(String s) { + if (isKoreanLocale) return '언데드 $s'; + if (isJapaneseLocale) return 'アンデッド$s'; + return 'undead $s'; +} + +String modifierDemon(String s) { + if (isKoreanLocale) return '데몬 $s'; + if (isJapaneseLocale) return 'デーモン$s'; + return 'demon $s'; +} + +String modifierMessianic(String s) { + if (isKoreanLocale) return '메시아닉 $s'; + if (isJapaneseLocale) return 'メシアニック$s'; + return 'messianic $s'; +} + +String modifierImaginary(String s) { + if (isKoreanLocale) return '상상의 $s'; + if (isJapaneseLocale) return '想像上の$s'; + return 'imaginary $s'; +} + +String modifierPassing(String s) { + if (isKoreanLocale) return '지나가는 $s'; + if (isJapaneseLocale) return '通りすがりの$s'; + return 'passing $s'; +} // ============================================================================ // 시간 표시 // ============================================================================ -String roughTimeSeconds(int seconds) => - isKoreanLocale ? '$seconds초' : '$seconds seconds'; +String roughTimeSeconds(int seconds) { + if (isKoreanLocale) return '$seconds초'; + if (isJapaneseLocale) return '$seconds秒'; + return '$seconds seconds'; +} -String roughTimeMinutes(int minutes) => - isKoreanLocale ? '$minutes분' : '$minutes minutes'; +String roughTimeMinutes(int minutes) { + if (isKoreanLocale) return '$minutes분'; + if (isJapaneseLocale) return '$minutes分'; + return '$minutes minutes'; +} -String roughTimeHours(int hours) => - isKoreanLocale ? '$hours시간' : '$hours hours'; +String roughTimeHours(int hours) { + if (isKoreanLocale) return '$hours시간'; + if (isJapaneseLocale) return '$hours時間'; + return '$hours hours'; +} -String roughTimeDays(int days) => - isKoreanLocale ? '$days일' : '$days days'; +String roughTimeDays(int days) { + if (isKoreanLocale) return '$days일'; + if (isJapaneseLocale) return '$days日'; + return '$days days'; +} // ============================================================================ -// 영어 문법 함수 (한국어에서는 단순화) +// 영어 문법 함수 (한국어/일본어에서는 단순화) // ============================================================================ -/// 관사 + 명사 (한국어: 수량만 표시) +/// 관사 + 명사 (한국어/일본어: 수량만 표시) String indefiniteL10n(String s, int qty) { if (isKoreanLocale) { return qty == 1 ? s : '$qty $s'; } + if (isJapaneseLocale) { + return qty == 1 ? s : '$qty $s'; + } // 영어 로직 if (qty == 1) { const vowels = 'AEIOUÜaeiouü'; @@ -238,9 +461,9 @@ String indefiniteL10n(String s, int qty) { return '$qty ${_pluralize(s)}'; } -/// the + 명사 (한국어: 그냥 명사) +/// the + 명사 (한국어/일본어: 그냥 명사) String definiteL10n(String s, int qty) { - if (isKoreanLocale) { + if (isKoreanLocale || isJapaneseLocale) { return s; } // 영어 로직 @@ -266,70 +489,202 @@ String _pluralize(String s) { // impressiveGuy 관련 // ============================================================================ -String impressiveGuyPattern1(String title, String race) => isKoreanLocale +String impressiveGuyPattern1(String title, String race) { + if (isKoreanLocale) { // ignore: unnecessary_brace_in_string_interps - ? '${race}들의 $title' // 한국어 조사 연결을 위해 중괄호 필요 - : 'the $title of the ${_pluralize(race)}'; + return '${race}들의 $title'; // 한국어 조사 연결을 위해 중괄호 필요 + } + if (isJapaneseLocale) { + // ignore: unnecessary_brace_in_string_interps + return '${race}たちの$title'; // 일본어 연결을 위해 중괄호 필요 + } + return 'the $title of the ${_pluralize(race)}'; +} -String impressiveGuyPattern2(String title, String name1, String name2) => - isKoreanLocale - ? '$name2의 $title $name1' - : '$title $name1 of $name2'; +String impressiveGuyPattern2(String title, String name1, String name2) { + if (isKoreanLocale) return '$name2의 $title $name1'; + if (isJapaneseLocale) return '$name2の$title $name1'; + return '$title $name1 of $name2'; +} // ============================================================================ // namedMonster 관련 // ============================================================================ -String namedMonsterFormat(String generatedName, String monsterType) => - isKoreanLocale - ? '$monsterType $generatedName' - : '$generatedName the $monsterType'; +String namedMonsterFormat(String generatedName, String monsterType) { + if (isKoreanLocale) return '$monsterType $generatedName'; + if (isJapaneseLocale) return '$monsterType $generatedName'; + return '$generatedName the $monsterType'; +} // ============================================================================ // 게임 데이터 번역 함수 (BuildContext 없이 사용) +// 지원 언어: 한국어(ko), 영어(en), 일본어(ja) // ============================================================================ -/// 몬스터 이름 번역 -String translateMonster(String englishName) => - isKoreanLocale ? (monsterTranslationsKo[englishName] ?? englishName) : englishName; +/// 몬스터 이름 번역 (기본 + 고급 몬스터 포함) +String translateMonster(String englishName) { + if (isKoreanLocale) { + return monsterTranslationsKo[englishName] ?? + advancedMonsterTranslationsKo[englishName] ?? + englishName; + } + if (isJapaneseLocale) { + return monsterTranslationsJa[englishName] ?? + advancedMonsterTranslationsJa[englishName] ?? + englishName; + } + return englishName; +} /// 종족 이름 번역 -String translateRace(String englishName) => - isKoreanLocale ? (raceTranslationsKo[englishName] ?? englishName) : englishName; +String translateRace(String englishName) { + if (isKoreanLocale) return raceTranslationsKo[englishName] ?? englishName; + if (isJapaneseLocale) return raceTranslationsJa[englishName] ?? englishName; + return englishName; +} /// 직업 이름 번역 -String translateKlass(String englishName) => - isKoreanLocale ? (klassTranslationsKo[englishName] ?? englishName) : englishName; +String translateKlass(String englishName) { + if (isKoreanLocale) return klassTranslationsKo[englishName] ?? englishName; + if (isJapaneseLocale) return klassTranslationsJa[englishName] ?? englishName; + return englishName; +} /// 칭호 이름 번역 -String translateTitle(String englishName) => - isKoreanLocale ? (titleTranslationsKo[englishName] ?? englishName) : englishName; +String translateTitle(String englishName) { + if (isKoreanLocale) return titleTranslationsKo[englishName] ?? englishName; + if (isJapaneseLocale) return titleTranslationsJa[englishName] ?? englishName; + return englishName; +} /// 인상적인 칭호 번역 (impressiveTitles용) -String translateImpressiveTitle(String englishName) => - isKoreanLocale ? (impressiveTitleTranslationsKo[englishName] ?? englishName) : englishName; +String translateImpressiveTitle(String englishName) { + if (isKoreanLocale) { + return impressiveTitleTranslationsKo[englishName] ?? englishName; + } + if (isJapaneseLocale) { + return impressiveTitleTranslationsJa[englishName] ?? englishName; + } + return englishName; +} /// 특수 아이템 이름 번역 -String translateSpecial(String englishName) => - isKoreanLocale ? (specialTranslationsKo[englishName] ?? englishName) : englishName; +String translateSpecial(String englishName) { + if (isKoreanLocale) return specialTranslationsKo[englishName] ?? englishName; + if (isJapaneseLocale) return specialTranslationsJa[englishName] ?? englishName; + return englishName; +} -/// 아이템 속성 이름 번역 -String translateItemAttrib(String englishName) => - isKoreanLocale ? (itemAttribTranslationsKo[englishName] ?? englishName) : englishName; +/// 아이템 속성 이름 번역 (기본 + 추가 속성 포함) +String translateItemAttrib(String englishName) { + if (isKoreanLocale) { + return itemAttribTranslationsKo[englishName] ?? + additionalItemAttribTranslationsKo[englishName] ?? + englishName; + } + if (isJapaneseLocale) { + return itemAttribTranslationsJa[englishName] ?? + additionalItemAttribTranslationsJa[englishName] ?? + englishName; + } + return englishName; +} -/// 아이템 "~의" 접미사 번역 -String translateItemOf(String englishName) => - isKoreanLocale ? (itemOfsTranslationsKo[englishName] ?? englishName) : englishName; +/// 아이템 "~의" 접미사 번역 (기본 + 추가 포함) +String translateItemOf(String englishName) { + if (isKoreanLocale) { + return itemOfsTranslationsKo[englishName] ?? + additionalItemOfsTranslationsKo[englishName] ?? + englishName; + } + if (isJapaneseLocale) { + return itemOfsTranslationsJa[englishName] ?? + additionalItemOfsTranslationsJa[englishName] ?? + englishName; + } + return englishName; +} -/// 단순 아이템 번역 -String translateBoringItem(String englishName) => - isKoreanLocale ? (boringItemTranslationsKo[englishName] ?? englishName) : englishName; +/// 단순 아이템 번역 (기본 + 추가 드롭 포함) +String translateBoringItem(String englishName) { + if (isKoreanLocale) { + return boringItemTranslationsKo[englishName] ?? + dropItemTranslationsKo[englishName] ?? + additionalDropTranslationsKo[englishName] ?? + englishName; + } + if (isJapaneseLocale) { + return boringItemTranslationsJa[englishName] ?? + dropItemTranslationsJa[englishName] ?? + additionalDropTranslationsJa[englishName] ?? + englishName; + } + return englishName; +} /// interestingItem 번역 (attrib + special 조합) -/// 예: "Golden Iterator" → "황금 이터레이터" +/// 예: "Golden Iterator" → "황금 이터레이터" / "黄金のイテレーター" String translateInterestingItem(String attrib, String special) { - if (!isKoreanLocale) return '$attrib $special'; - final translatedAttrib = itemAttribTranslationsKo[attrib] ?? attrib; - final translatedSpecial = specialTranslationsKo[special] ?? special; - return '$translatedAttrib $translatedSpecial'; + if (isKoreanLocale) { + final translatedAttrib = itemAttribTranslationsKo[attrib] ?? + additionalItemAttribTranslationsKo[attrib] ?? + attrib; + final translatedSpecial = specialTranslationsKo[special] ?? special; + return '$translatedAttrib $translatedSpecial'; + } + if (isJapaneseLocale) { + final translatedAttrib = itemAttribTranslationsJa[attrib] ?? + additionalItemAttribTranslationsJa[attrib] ?? + attrib; + final translatedSpecial = specialTranslationsJa[special] ?? special; + return '$translatedAttrib$translatedSpecial'; + } + return '$attrib $special'; +} + +// ============================================================================ +// 스토리/시네마틱 번역 함수 (Story/Cinematic Translation Functions) +// ============================================================================ + +/// Act 제목 번역 +String translateActTitle(String 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; + return englishBoss; +} + +/// Act 퀘스트 번역 +String translateActQuest(String 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; + return englishText; +} + +/// 지역 이름 번역 +String translateLocation(String 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; + return englishFaction; } diff --git a/lib/data/game_translations_ja.dart b/lib/data/game_translations_ja.dart new file mode 100644 index 0000000..d332e59 --- /dev/null +++ b/lib/data/game_translations_ja.dart @@ -0,0 +1,1521 @@ +// ============================================================================ +// ASCII NEVER DIE 日本語翻訳データ +// ============================================================================ +// +// このファイルはゲームのすべての日本語翻訳を含みます。 +// 英語原文は pq_config_data.dart に定義されています。 +// +// ## 世界観 (World Setting) +// アスキーナラ(ASCII-Nara): コードの神が創造したデジタルファンタジー世界 +// - ヌル(Null)王国: 冒険者たちが始まる中央都市 (64ビット王統治) +// - コアの深淵: 伝説的装備が発見される古代ダンジョン +// - グリッチ領域: バグとエラーで現実が歪んだ地域 +// - バイナリ神殿: コードの神を崇拝する神聖な場所 +// +// ## 翻訳構造 +// - 基本翻訳Maps: raceTranslationsJa, klassTranslationsJa, monsterTranslationsJa など +// - 追加翻訳Maps: advancedMonsterTranslationsJa, additionalDropTranslationsJa など +// - ストーリー翻訳Maps: actTitleTranslationsJa, cinematicTranslationsJa など +// - 世界観翻訳Maps: locationTranslationsJa, factionTranslationsJa +// +// ## 翻訳関数使用 (game_text_l10n.dart) +// - translateMonster(name): モンスター名翻訳 +// - translateRace(name): 種族名翻訳 +// - translateItemAttrib(name): アイテム属性翻訳 +// - translateCinematic(text): シネマティックテキスト翻訳 +// - など... +// +// ============================================================================ + +/// 種族名日本語翻訳 +const Map raceTranslationsJa = { + 'Byte Human': 'バイトヒューマン', + 'Null Elf': 'ヌルエルフ', + 'Buffer Dwarf': 'バッファドワーフ', + 'Bit Halfling': 'ビットハーフリング', + 'Array Orc': 'アレイオーク', + 'Stack Goblin': 'スタックゴブリン', + 'Heap Troll': 'ヒープトロール', + 'Pointer Fairy': 'ポインターフェアリー', + 'Register Gnome': 'レジスターノーム', + 'Cache Imp': 'キャッシュインプ', + 'Kernel Giant': 'カーネルジャイアント', + 'Thread Spirit': 'スレッドスピリット', + 'Coredump Undead': 'コアダンプアンデッド', + 'Flag Knight': 'フラグナイト', + 'Loop Wizard': 'ループウィザード', + 'Recursive Sage': 'リカーシブセイジ', + 'Iterator Rogue': 'イテレーターローグ', + 'Callback Priest': 'コールバックプリースト', + 'Lambda Druid': 'ラムダドルイド', + 'Protocol Paladin': 'プロトコルパラディン', + 'Index Ranger': 'インデックスレンジャー', +}; + +/// 職業名日本語翻訳 +const Map klassTranslationsJa = { + 'Bug Hunter': 'バグハンター', + 'Debugger Paladin': 'デバッガーパラディン', + 'Compiler Mage': 'コンパイラメイジ', + 'Refactor Monk': 'リファクターモンク', + 'Overflow Warrior': 'オーバーフローウォリアー', + 'Exception Handler': '例外ハンドラー', + 'Loop Breaker': 'ループブレイカー', + 'Recursion Master': 'リカージョンマスター', + 'Callback Samurai': 'コールバックサムライ', + 'Pointer Assassin': 'ポインターアサシン', + 'Garbage Collector': 'ガベージコレクター', + 'Memory Leaker': 'メモリリーカー', + 'Stack Crusher': 'スタッククラッシャー', + 'Null Checker': 'ヌルチェッカー', + 'Type Caster': 'タイプキャスター', + 'Assertion Knight': 'アサーションナイト', + 'Tester Jester': 'テスタージェスター', + 'DevOps Shaman': 'デブオプスシャーマン', +}; + +/// 呪文名日本語翻訳 +const Map spellTranslationsJa = { + 'Garbage Collection': 'ガベージコレクション', + 'Memory Optimization': 'メモリ最適化', + 'Debug Mode': 'デバッグモード', + 'Breakpoint': 'ブレークポイント', + 'Step Over': 'ステップオーバー', + 'Step Into': 'ステップイントゥ', + 'Watch Variable': '変数ウォッチ', + 'Hot Reload': 'ホットリロード', + 'Cold Boot': 'コールドブート', + 'Safe Mode': 'セーフモード', + 'Kernel Panic': 'カーネルパニック', + 'Blue Screen': 'ブルースクリーン', + 'Stack Trace': 'スタックトレース', + 'Core Dump': 'コアダンプ', + 'Memory Dump': 'メモリダンプ', + 'Heap Analysis': 'ヒープ分析', + 'Profile Run': 'プロファイル実行', + 'Benchmark': 'ベンチマーク', + 'Unit Test': 'ユニットテスト', + 'Integration Test': '統合テスト', + 'Fuzzing': 'ファジング', + 'Sanitizer': 'サニタイザー', + 'Static Analysis': '静的解析', + 'Dynamic Analysis': '動的解析', + 'Reverse Engineer': 'リバースエンジニアリング', + 'Decompile': 'デコンパイル', + 'Disassemble': 'ディスアセンブル', + 'Patch Binary': 'バイナリパッチ', + 'Hook Function': '関数フック', + 'Inject Code': 'コードインジェクション', + 'Elevate Privilege': '権限昇格', + 'Spawn Shell': 'シェル生成', + 'Pivot Network': 'ネットワークピボット', + 'Exfiltrate Data': 'データ抽出', + 'Cover Tracks': '痕跡隠蔽', + 'Git Commit': 'Gitコミット', + 'Git Push': 'Gitプッシュ', + 'Git Merge': 'Gitマージ', + 'Rollback': 'ロールバック', + 'Hotfix': 'ホットフィックス', + 'Deploy': 'デプロイ', + 'Scale Up': 'スケールアップ', + 'Failover': 'フェイルオーバー', +}; + +/// モンスター名日本語翻訳 (主要モンスター) +const Map monsterTranslationsJa = { + // レベル 0-5: 初級バグ + 'Syntax Error': '構文エラー', + 'Typo Bug': 'タイポバグ', + 'Missing Bracket': '括弧抜け', + 'Lint Warning': 'リント警告', + 'Tab-Space Conflict': 'タブスペース衝突', + 'Trailing Comma': '末尾カンマ', + 'Undefined Variable': '未定義変数', + 'Type Mismatch': '型不一致', + 'Import Error': 'インポートエラー', + 'Compile Warning': 'コンパイル警告', + 'Null Reference': 'ヌル参照', + 'Index Out of Bounds': 'インデックス超過', + 'Division by Zero': 'ゼロ除算', + 'Stack Trace': 'スタックトレース', + 'Assertion Failure': 'アサーション失敗', + 'Parse Error': 'パースエラー', + 'Encoding Bug': 'エンコーディングバグ', + 'Off-by-One Error': 'オフバイワンエラー', + 'Logic Bug': 'ロジックバグ', + 'Infinite Loop Larva': '無限ループ幼虫', + 'Memory Leak Sprout': 'メモリリーク芽', + 'Buffer Overflow Seed': 'バッファオーバーフロー種', + 'Race Condition Pup': 'レースコンディション子犬', + 'Null Pointer Imp': 'ヌルポインターインプ', + 'Segfault Sprite': 'セグフォルトスプライト', + 'Exception Goblin': '例外ゴブリン', + 'Timeout Gremlin': 'タイムアウトグレムリン', + 'Connection Drop': '接続切断', + 'File Not Found': 'ファイル未検出', + 'Permission Denied': '権限拒否', + + // レベル 6-10: 中級バグ + 'Memory Leak': 'メモリリーク', + 'Buffer Overflow': 'バッファオーバーフロー', + 'Null Pointer Exception': 'ヌルポインター例外', + 'Array Index Bug': '配列インデックスバグ', + 'Type Confusion': '型混乱', + 'Use After Free': '解放後使用', + 'Double Free': '二重解放', + 'Integer Overflow': '整数オーバーフロー', + 'Format String Bug': 'フォーマット文字列バグ', + 'SQL Injection': 'SQLインジェクション', + 'XSS Worm': 'XSSワーム', + 'CSRF Token': 'CSRFトークン', + 'Path Traversal': 'パストラバーサル', + 'Command Injection': 'コマンドインジェクション', + 'Race Condition': 'レースコンディション', + 'Deadlock': 'デッドロック', + 'Livelock': 'ライブロック', + 'Priority Inversion': '優先度逆転', + 'Thread Starvation': 'スレッド飢餓', + 'Heap Corruption': 'ヒープ破損', + 'Stack Smash': 'スタックスマッシュ', + 'Memory Corruption': 'メモリ破損', + 'Data Race': 'データレース', + 'Dangling Pointer': 'ダングリングポインター', + + // レベル 11-20: 高級バグ + 'Kernel Panic': 'カーネルパニック', + 'Blue Screen Beast': 'ブルースクリーンビースト', + 'Core Dump': 'コアダンプ', + 'Segmentation Fault': 'セグメンテーションフォルト', + 'Bus Error': 'バスエラー', + 'Page Fault Phantom': 'ページフォルトファントム', + 'Cache Corruption': 'キャッシュ破損', + 'TLB Miss Monster': 'TLBミスモンスター', + 'DMA Error': 'DMAエラー', + 'Interrupt Storm': '割り込みストーム', + 'Watchdog Timeout': 'ウォッチドッグタイムアウト', + 'Firmware Bug': 'ファームウェアバグ', + 'BIOS Corruption': 'BIOS破損', + 'EFI Malware': 'EFIマルウェア', + 'Bootkit': 'ブートキット', + 'Rootkit': 'ルートキット', + 'Ring Zero Bug': 'リングゼロバグ', + 'Hypervisor Escape': 'ハイパーバイザー脱出', + 'SMM Bug': 'SMMバグ', + 'Microcode Error': 'マイクロコードエラー', + 'Spectre Variant': 'スペクター変種', + 'Meltdown Ghost': 'メルトダウンゴースト', + 'Rowhammer': 'ローハンマー', + 'Cold Boot Specter': 'コールドブートスペクター', + 'DMA Attack': 'DMA攻撃', + 'Side Channel Leak': 'サイドチャネルリーク', + 'Firmware Rootkit': 'ファームウェアルートキット', + 'UEFI Implant': 'UEFIインプラント', + 'Intel ME Bug': 'Intel MEバグ', + + // レベル 21-30: エリート脅威 + 'Advanced Persistent Threat': '高度持続的脅威', + 'Nation State Actor': '国家アクター', + 'Zero Day Exploit': 'ゼロデイエクスプロイト', + 'Supply Chain Attack': 'サプライチェーン攻撃', + 'Watering Hole': 'ウォータリングホール', + 'Spear Phishing': 'スピアフィッシング', + 'Living Off the Land': '環境依存攻撃', + 'Fileless Malware': 'ファイルレスマルウェア', + 'Polymorphic Virus': 'ポリモーフィックウイルス', + 'Metamorphic Engine': 'メタモーフィックエンジン', + 'Code Cave Lurker': 'コードケイブ潜伏者', + + // レベル 31-40: ボス級脅威 + 'Worm Cluster': 'ワームクラスター', + 'Botnet Commander': 'ボットネット司令官', + 'Ransomware King': 'ランサムウェア王', + 'Cryptominer Hive': 'クリプトマイナーハイブ', + 'Data Exfiltrator': 'データ抽出者', + 'Credential Harvester': '認証情報収穫者', + 'Keylogger Phantom': 'キーロガーファントム', + 'Screen Scraper': 'スクリーンスクレイパー', + 'Clipboard Hijacker': 'クリップボードハイジャッカー', + 'DNS Poisoner': 'DNSポイズナー', + + // レベル 41-45: 伝説級脅威 + 'Stuxnet Legacy': 'スタックスネットレガシー', + 'NotPetya Remnant': 'NotPetya残骸', + 'WannaCry Echo': 'WannaCryエコー', + 'Equation Group Tool': 'イクエーショングループツール', + 'Shadow Broker Cache': 'シャドウブローカーキャッシュ', + + // レベル 46-53: 神話級ボス + 'Overflow Dragon': 'オーバーフロードラゴン', + 'Memory Corruptor': 'メモリコラプター', + 'Kernel Destroyer': 'カーネルデストロイヤー', + 'Stack Annihilator': 'スタックアナイアレイター', + 'Heap Devastator': 'ヒープデバステイター', + 'Glitch Archon': 'グリッチアルコン', + 'Bug God Minion': 'バグゴッドミニオン', + 'The Glitch God': 'グリッチゴッド', + + // コード品質関連 + 'Deprecated Function': '非推奨関数', + 'Legacy Code Zombie': 'レガシーコードゾンビ', + 'Spaghetti Code': 'スパゲッティコード', + 'God Object': 'ゴッドオブジェクト', + 'Circular Dependency': '循環依存', + 'Magic Number': 'マジックナンバー', + 'Hardcoded Path': 'ハードコードパス', + 'Global Variable': 'グローバル変数', + 'Copy-Paste Error': 'コピペエラー', + 'Cargo Cult Code': 'カーゴカルトコード', + 'Dead Code Walker': 'デッドコードウォーカー', + 'Zombie Process': 'ゾンビプロセス', + 'Orphan Thread': 'オーファンスレッド', + 'Phantom Pointer': 'ファントムポインター', + 'Heisenbug': 'ハイゼンバグ', + 'Schrodinger Bug': 'シュレディンガーバグ', + 'Bohrbug': 'ボーアバグ', + 'Mandelbug': 'マンデルバグ', + 'Hindenbug': 'ヒンデンバグ', + + // 追加バグと回避技法 + 'Schroedinbug': 'シュレーディンバグ', + 'Antivirus Evasion': 'アンチウイルス回避', + 'Packer': 'パッカー', + 'Crypter': 'クリプター', + 'Dropper': 'ドロッパー', + 'Loader': 'ローダー', + 'Payload Carrier': 'ペイロードキャリア', + 'Persistence Mechanism': '永続化メカニズム', + 'Privilege Escalation': '権限昇格', + 'Lateral Movement': '横方向移動', + 'Exfil Channel': '抽出チャネル', + 'C2 Beacon': 'C2ビーコン', + 'DNS Tunnel': 'DNSトンネル', + 'ICMP Shell': 'ICMPシェル', + 'HTTP Backdoor': 'HTTPバックドア', + 'Reverse Shell': 'リバースシェル', + 'Bind Shell': 'バインドシェル', + 'Web Shell': 'ウェブシェル', + 'Cron Job Malware': 'Cronジョブマルウェア', + 'Init Script Virus': '初期化スクリプトウイルス', + + // コードインジェクション技法 + 'Library Injection': 'ライブラリインジェクション', + 'Process Hollowing': 'プロセスホローイング', + 'Thread Injection': 'スレッドインジェクション', + 'APC Injection': 'APCインジェクション', + 'Atom Bombing': 'アトムボミング', + 'Process Doppelganging': 'プロセスドッペルゲンギング', + 'Ghostwriting': 'ゴーストライティング', + 'Module Stomping': 'モジュールストンピング', + 'Reflective Loading': 'リフレクティブローディング', + 'Manual Mapping': 'マニュアルマッピング', + 'Syscall Stub': 'システムコールスタブ', + 'Heaven Gate': 'ヘブンズゲート', + + // 非同期とメモリ関連 + 'Callback Hell': 'コールバック地獄', + 'Promise Rejection': 'プロミス拒否', + 'Event Loop Block': 'イベントループブロック', + 'Memory Pressure': 'メモリプレッシャー', + 'Garbage Storm': 'ガベージストーム', + 'Finalizer Bug': 'ファイナライザーバグ', + 'Weak Reference Leak': '弱参照リーク', + 'String Interning Bug': '文字列インターニングバグ', + 'Classloader Leak': 'クラスローダーリーク', + 'Native Memory Leak': 'ネイティブメモリリーク', + 'Direct Buffer Leak': 'ダイレクトバッファリーク', + 'Thread Local Leak': 'スレッドローカルリーク', + 'Connection Pool Leak': 'コネクションプールリーク', + 'File Handle Leak': 'ファイルハンドルリーク', + 'Timer Leak': 'タイマーリーク', + 'Listener Leak': 'リスナーリーク', + 'Observable Leak': 'オブザーバブルリーク', + 'Async Leak': '非同期リーク', + 'Context Leak': 'コンテキストリーク', + 'Bitmap Leak': 'ビットマップリーク', + 'Cursor Leak': 'カーソルリーク', + 'Stream Leak': 'ストリームリーク', + 'Transaction Leak': 'トランザクションリーク', + 'Session Leak': 'セッションリーク', + 'Cache Bloat': 'キャッシュ肥大化', + 'Queue Overflow': 'キューオーバーフロー', + 'Ring Buffer Bug': 'リングバッファバグ', + + // 同時性とロック関連 + 'Lock Contention': 'ロック競合', + 'Spin Lock Burn': 'スピンロックバーン', + 'False Sharing': 'フォールスシェアリング', + 'NUMA Bug': 'NUMAバグ', + 'Affinity Bug': 'アフィニティバグ', + 'Context Switch Storm': 'コンテキストスイッチストーム', + 'TLB Shootdown': 'TLBシュートダウン', + 'IPI Storm': 'IPIストーム', + 'Interrupt Disable Bug': '割り込み無効化バグ', + 'Preemption Bug': 'プリエンプションバグ', + 'RCU Bug': 'RCUバグ', + 'Seqlock Bug': 'シーケンスロックバグ', + 'RWLock Bug': '読み書きロックバグ', + 'Futex Bug': 'Futexバグ', + 'Spinlock Bug': 'スピンロックバグ', + 'Barrier Bug': 'バリアバグ', + 'Condition Variable Bug': '条件変数バグ', + 'Semaphore Leak': 'セマフォリーク', + 'Message Queue Bug': 'メッセージキューバグ', + 'Shared Memory Bug': '共有メモリバグ', + 'Pipe Deadlock': 'パイプデッドロック', + 'Socket Leak': 'ソケットリーク', + 'Epoll Bug': 'Epollバグ', + 'Kqueue Bug': 'Kqueueバグ', + 'IOCP Bug': 'IOCPバグ', + 'AIO Bug': 'AIOバグ', + 'Scatter Gather Bug': 'スキャッターギャザーバグ', + 'Zero Copy Bug': 'ゼロコピーバグ', + 'Splice Bug': 'スプライスバグ', + 'Vmsplice Bug': 'Vmspliceバグ', + 'Tee Bug': 'Teeバグ', + 'Fallocate Bug': 'Fallocateバグ', + 'Punch Hole Bug': 'パンチホールバグ', + 'Direct IO Bug': 'ダイレクトIOバグ', + 'Sync Bug': '同期バグ', + 'Datasync Bug': 'データ同期バグ', + 'Journal Bug': 'ジャーナルバグ', + 'Inode Leak': 'Inodeリーク', + 'Dentry Cache Bug': 'Dentryキャッシュバグ', + 'Buffer Head Bug': 'バッファヘッドバグ', + 'Page Cache Bug': 'ページキャッシュバグ', + 'Slab Leak': 'スラブリーク', + 'Kmalloc Bug': 'Kmallocバグ', + 'Vmalloc Bug': 'Vmallocバグ', + 'Highmem Bug': 'ハイメモリバグ', + 'Lowmem Bug': 'ローメモリバグ', + 'OOM Killer': 'OOMキラー', + + // 爆弾とDoS関連 + 'Fork Bomb': 'フォーク爆弾', + 'Zip Bomb': 'Zip爆弾', + 'Xml Bomb': 'XML爆弾', + 'Regex Bomb': '正規表現爆弾', + 'Hash Collision Attack': 'ハッシュ衝突攻撃', + 'Algorithmic Complexity': 'アルゴリズム複雑度攻撃', + 'Slowloris': 'スローロリス', + 'RUDY': 'RUDY', + 'Apache Killer': 'アパッチキラー', + 'HashDoS': 'ハッシュDoS', + 'SYN Flood Spirit': 'SYNフラッドスピリット', + 'UDP Flood': 'UDPフラッド', + 'ICMP Flood': 'ICMPフラッド', + 'Smurf Attack': 'スマーフ攻撃', + 'Fraggle Attack': 'フラッグル攻撃', + 'DNS Amplification': 'DNS増幅', + 'NTP Amplification': 'NTP増幅', + 'SSDP Amplification': 'SSDP増幅', + 'Memcached Amplification': 'Memcached増幅', + 'CLDAP Amplification': 'CLDAP増幅', + 'Reflection Attack': 'リフレクション攻撃', + 'Carpet Bombing': 'カーペットボミング', + 'Pulse Wave': 'パルスウェーブ', + 'Low and Slow': '低速攻撃', + 'Application Layer': 'アプリケーション層攻撃', + 'SSL Exhaustion': 'SSL枯渇', + 'Renegotiation Attack': '再ネゴシエーション攻撃', + + // 有名な脆弱性 + 'BEAST': 'BEAST', + 'CRIME': 'CRIME', + 'BREACH': 'BREACH', + 'POODLE': 'POODLE', + 'Heartbleed Ghost': 'ハートブリードゴースト', + 'Shellshock': 'シェルショック', + 'Dirty COW': 'ダーティCOW', + 'VENOM': 'VENOM', + 'Cloudbleed': 'クラウドブリード', + 'Krack Attack': 'KRACK攻撃', + 'Dragonblood': 'ドラゴンブラッド', + 'Frag Attack': 'フラグ攻撃', + 'Kr00k': 'Kr00k', + 'PMKID Attack': 'PMKID攻撃', + + // 無線ネットワーク攻撃 + 'Evil Twin': 'イービルツイン', + 'Karma Attack': 'カルマ攻撃', + 'Deauth Attack': '認証解除攻撃', + 'Beacon Flood': 'ビーコンフラッド', + 'Bluetooth Bug': 'Bluetoothバグ', + 'Blueborne': 'ブルーボーン', + 'Sweyntooth': 'スウェイントゥース', + 'Braktooth': 'ブラクトゥース', + 'Knob Attack': 'KNOBアタック', + 'Bias Attack': 'BIASアタック', + + // ネットワーク機器とプロトコルバグ + 'Cable Haunt': 'ケーブルホーント', + 'CallStranger': 'コールストレンジャー', + 'Ripple20': 'Ripple20', + 'Amnesia33': 'Amnesia33', + 'Number Jack': 'ナンバージャック', + 'NAME:WRECK': 'NAME:WRECK', + 'BadAlloc': 'BadAlloc', + 'PwnKit': 'PwnKit', + 'Sudo Bug': 'Sudoバグ', + 'Baron Samedit': 'バロンサメディット', +}; + +/// 武器名日本語翻訳 +const Map weaponTranslationsJa = { + 'Keyboard': 'キーボード', + 'USB Cable': 'USBケーブル', + 'Ethernet Cord': 'イーサネットケーブル', + 'Power Cable': '電源ケーブル', + 'Mouse': 'マウス', + 'Trackpad': 'トラックパッド', + 'Monitor Stand': 'モニタースタンド', + 'Laptop Charger': 'ノートPC充電器', + 'Docking Station': 'ドッキングステーション', + 'Server Rack': 'サーバーラック', + 'Network Switch': 'ネットワークスイッチ', + 'Router': 'ルーター', + 'Fiber Optic': '光ファイバー', + 'SSD': 'SSD', + 'NVMe Drive': 'NVMeドライブ', + 'RAID Array': 'RAIDアレイ', + 'RAM Stick': 'RAMスティック', + 'CPU Cooler': 'CPUクーラー', + 'Heat Sink': 'ヒートシンク', + 'Water Cooler': '水冷クーラー', + 'GPU': 'グラフィックスカード', + 'Tensor Core': 'テンソルコア', + 'TPU': 'TPU', + 'FPGA': 'FPGA', + 'ASIC': 'ASIC', + 'Quantum Bit': '量子ビット', + 'Photonic Chip': 'フォトニックチップ', + 'Neural Processor': 'ニューラルプロセッサー', + 'Mainframe Terminal': 'メインフレームターミナル', + 'Supercomputer Node': 'スーパーコンピューターノード', + 'Cluster Blade': 'クラスターブレード', + 'Data Center Rack': 'データセンターラック', + 'Submarine Cable': '海底ケーブル', + 'Satellite Link': '衛星リンク', + 'Quantum Entangler': '量子もつれ装置', + 'Dyson Sphere Core': 'ダイソン球コア', + 'Black Hole Computer': 'ブラックホールコンピューター', + 'Universe Simulator': '宇宙シミュレーター', +}; + +/// 鎧名日本語翻訳 +const Map armorTranslationsJa = { + 'Firewall': 'ファイアウォール', + 'Spam Filter': 'スパムフィルター', + 'Antivirus': 'アンチウイルス', + 'VPN Cloak': 'VPNクローク', + 'SSL Certificate': 'SSL証明書', + 'Encryption Layer': '暗号化レイヤー', + 'Hash Armor': 'ハッシュアーマー', + 'Binary Coat': 'バイナリコート', + 'Packet Shield': 'パケットシールド', + 'Protocol Suit': 'プロトコルスーツ', + 'Kernel Guard': 'カーネルガード', + 'Memory Barrier': 'メモリバリア', + 'Stack Protector': 'スタックプロテクター', + 'Heap Guard': 'ヒープガード', + 'ASLR Armor': 'ASLRアーマー', + 'Sandbox Shell': 'サンドボックスシェル', + 'Container Suit': 'コンテナスーツ', + 'Virtualization Mail': '仮想化メイル', + 'Quantum Encryption': '量子暗号化', + 'Zero-Day Aegis': 'ゼロデイイージス', +}; + +/// 盾名日本語翻訳 +const Map shieldTranslationsJa = { + 'CAPTCHA': 'CAPTCHA', + 'Rate Limiter': 'レートリミッター', + 'WAF Shield': 'WAFシールド', + 'Load Balancer': 'ロードバランサー', + 'CDN Shield': 'CDNシールド', + 'DDoS Protection': 'DDoS防御', + 'Firewall Shield': 'ファイアウォールシールド', + 'IDS Shield': 'IDSシールド', + 'IPS Shield': 'IPSシールド', + 'SIEM Barrier': 'SIEMバリア', + 'SOC Shield': 'SOCシールド', + 'Honeypot Decoy': 'ハニーポットデコイ', + 'Sandbox Barrier': 'サンドボックスバリア', + 'Air Gap Shield': 'エアギャップシールド', + 'Faraday Shield': 'ファラデーシールド', + 'Quantum Firewall': '量子ファイアウォール', +}; + +/// 称号日本語翻訳 +const Map titleTranslationsJa = { + 'Dev': 'デベロッパー', + 'Senior': 'シニア', + 'Lead': 'リード', + 'Staff': 'スタッフ', + 'Principal': 'プリンシパル', + 'Architect': 'アーキテクト', + 'Fellow': 'フェロー', + 'Distinguished': 'ディスティングイッシュド', + 'Chief': 'チーフ', +}; + +/// 印象的な称号日本語翻訳 +const Map impressiveTitleTranslationsJa = { + 'Root': 'ルート', + 'Admin': '管理者', + 'Superuser': 'スーパーユーザー', + 'Kernel Lord': 'カーネルロード', + 'Arch-Compiler': 'アーチコンパイラー', + 'Prime Debugger': 'プライムデバッガー', + 'Code Sovereign': 'コードソブリン', + 'Data Emperor': 'データエンペラー', + 'Stack Overlord': 'スタックオーバーロード', + 'Memory Master': 'メモリマスター', + 'Thread King': 'スレッドキング', + 'Process Monarch': 'プロセスモナーク', + 'System Architect': 'システムアーキテクト', +}; + +/// アイテム属性日本語翻訳 +const Map itemAttribTranslationsJa = { + 'Golden': '黄金の', + 'Binary': 'バイナリ', + 'Hexadecimal': '16進法の', + 'Quantum': '量子', + 'Recursive': '再帰的', + 'Polymorphic': 'ポリモーフィック', + 'Encrypted': '暗号化された', + 'Compiled': 'コンパイル済み', + 'Optimized': '最適化された', + 'Debugged': 'デバッグ済み', + 'Refactored': 'リファクタリング済み', + 'Blessed': '祝福された', + 'Sacred': '神聖な', + 'Legendary': '伝説の', + 'Mythic': '神話の', + 'Crystalline': '水晶の', + 'Holographic': 'ホログラフィック', + 'Virtual': '仮想の', + 'Augmented': '拡張された', + 'Pristine': '原始の', + 'Ancient': '古代の', + 'Primordial': '太古の', + 'Transcendent': '超越的', + 'Ethereal': 'エーテル的', + 'Magnificent': '壮大な', + 'Immutable': '不変の', + 'Atomic': 'アトミック', + 'Distributed': '分散された', + 'Replicated': '複製された', + 'Synchronized': '同期された', + 'Hashed': 'ハッシュ化された', + 'Signed': '署名済み', + 'Verified': '検証済み', +}; + +/// アイテム「~の」接尾辞日本語翻訳 +const Map itemOfsTranslationsJa = { + 'Compilation': 'コンパイル', + 'Execution': '実行', + 'Iteration': '反復', + 'Recursion': '再帰', + 'Optimization': '最適化', + 'Debugging': 'デバッグ', + 'Refactoring': 'リファクタリング', + 'Deployment': 'デプロイ', + 'Integration': '統合', + 'Testing': 'テスト', + 'Validation': '検証', + 'Verification': '確認', + 'Authentication': '認証', + 'Authorization': '権限付与', + 'Encryption': '暗号化', + 'Decryption': '復号化', + 'Compression': '圧縮', + 'Decompression': '解凍', + 'Serialization': 'シリアライゼーション', + 'Parsing': 'パース', + 'Rendering': 'レンダリング', + 'Processing': '処理', + 'Computing': '演算', + 'Calculating': '計算', + 'Analyzing': '分析', + 'Monitoring': 'モニタリング', + 'Logging': 'ロギング', + 'Caching': 'キャッシング', + 'Buffering': 'バッファリング', + 'Streaming': 'ストリーミング', + 'Threading': 'スレッディング', + 'Forking': 'フォーキング', + 'Spawning': 'スポーニング', + 'Termination': '終了', + 'Resurrection': '復活', + 'Null': 'ヌル', + 'Infinity': '無限', + 'the Stack': 'スタック', + 'the Heap': 'ヒープ', + 'the Core': 'コア', + 'Binary': 'バイナリ', + 'Hexadecimal': '16進法', + 'the Algorithm': 'アルゴリズム', + 'the Protocol': 'プロトコル', + 'the API': 'API', + 'the Framework': 'フレームワーク', + 'the Runtime': 'ランタイム', + 'the Compiler': 'コンパイラー', + 'the Kernel': 'カーネル', +}; + +/// 攻撃属性日本語翻訳 +const Map offenseAttribTranslationsJa = { + 'Compiled': 'コンパイル済み', + 'Optimized': '最適化された', + 'JIT-Enhanced': 'JIT強化', + 'Parallel': '並列', + 'Multithreaded': 'マルチスレッド', + 'SIMD-Accelerated': 'SIMD加速', + 'GPU-Powered': 'GPU駆動', + 'Quantum-Enhanced': '量子強化', + 'AI-Augmented': 'AI拡張', + 'Neural': 'ニューラル', + 'Transcendent': '超越的', +}; + +/// 悪い攻撃属性日本語翻訳 +const Map offenseBadTranslationsJa = { + 'Interpreted': 'インタープリタ', + 'Unoptimized': '非最適化', + 'Buggy': 'バグだらけ', + 'Deprecated': '非推奨', + 'Legacy': 'レガシー', + 'Bloated': '肥大化した', + 'Slow': '遅い', + 'Crashing': 'クラッシュする', + 'Unstable': '不安定な', +}; + +/// 防御属性日本語翻訳 +const Map defenseAttribTranslationsJa = { + 'Patched': 'パッチ済み', + 'Hardened': '強化された', + 'Encrypted': '暗号化された', + 'Certified': '認定された', + 'Blessed by Code God': 'コードの神に祝福された', + 'Sandboxed': 'サンドボックス化', + 'Containerized': 'コンテナ化', + 'Air-gapped': 'エアギャップ', + 'Quantum-safe': '量子安全', +}; + +/// 悪い防御属性日本語翻訳 +const Map defenseBadTranslationsJa = { + 'Deprecated': '非推奨', + 'Unpatched': '未パッチ', + 'Vulnerable': '脆弱な', + 'Exploited': '悪用された', + 'Backdoored': 'バックドア付き', + 'Infected': '感染した', + 'Compromised': '侵害された', + 'Breached': '突破された', + 'Pwned': '征服された', + 'Cursed by Glitch': 'グリッチに呪われた', + 'Legacy': 'レガシー', + 'End-of-life': 'サポート終了', + 'Unsupported': '非サポート', + 'Buggy': 'バグだらけ', +}; + +/// 特殊アイテム日本語翻訳 +const Map specialTranslationsJa = { + 'Algorithm': 'アルゴリズム', + 'Data Structure': 'データ構造', + 'Design Pattern': 'デザインパターン', + 'Framework': 'フレームワーク', + 'Library': 'ライブラリ', + 'Module': 'モジュール', + 'Package': 'パッケージ', + 'Component': 'コンポーネント', + 'Service': 'サービス', + 'API': 'API', + 'Protocol': 'プロトコル', + 'Schema': 'スキーマ', + 'Model': 'モデル', + 'Interface': 'インターフェース', + 'Abstract Class': '抽象クラス', + 'Singleton': 'シングルトン', + 'Factory': 'ファクトリー', + 'Observer': 'オブザーバー', + 'Strategy': 'ストラテジー', + 'Decorator': 'デコレーター', + 'Adapter': 'アダプター', + 'Proxy': 'プロキシ', + 'Facade': 'ファサード', + 'Bridge': 'ブリッジ', + 'Composite': 'コンポジット', + 'Iterator': 'イテレーター', + 'Mediator': 'メディエーター', + 'Memento': 'メメント', + 'State': 'ステート', + 'Visitor': 'ビジター', + 'Chain': 'チェーン', + 'Command': 'コマンド', + 'Template': 'テンプレート', + 'Interpreter': 'インタープリター', + 'Flyweight': 'フライウェイト', + 'Prototype': 'プロトタイプ', + 'Builder': 'ビルダー', +}; + +/// 雑アイテム(BoringItems)日本語翻訳 +const Map boringItemTranslationsJa = { + 'semicolon': 'セミコロン', + 'curly brace': '波括弧', + 'null pointer': 'ヌルポインター', + 'empty string': '空文字列', + 'deprecated token': '非推奨トークン', + 'legacy code': 'レガシーコード', + 'tab character': 'タブ文字', + 'whitespace': '空白文字', + 'comment block': 'コメントブロック', + 'todo marker': 'TODOマーカー', + 'fixme note': 'FIXMEノート', + 'readme fragment': 'READMEフラグメント', + 'config shard': '設定シャード', + 'log entry': 'ログエントリ', + 'stack trace': 'スタックトレース', + 'core dump': 'コアダンプ', + 'crash report': 'クラッシュレポート', + 'error message': 'エラーメッセージ', + 'warning flag': '警告フラグ', + 'lint error': 'リントエラー', + 'syntax fragment': '構文フラグメント', + 'broken link': '壊れたリンク', + 'orphan process': 'オーファンプロセス', + 'zombie thread': 'ゾンビスレッド', + 'dangling pointer': 'ダングリングポインター', + 'memory leak': 'メモリリーク', + 'buffer scrap': 'バッファスクラップ', + 'bit bucket': 'ビットバケツ', + 'dev null': '/dev/null', + '/dev/random': '/dev/random', + 'entropy pool': 'エントロピープール', + 'hash collision': 'ハッシュ衝突', + 'race condition': 'レースコンディション', + 'deadlock key': 'デッドロックキー', + 'mutex token': 'ミューテックストークン', + 'semaphore': 'セマフォ', + 'signal handler': 'シグナルハンドラー', + 'interrupt vector': '割り込みベクター', + 'return value': '戻り値', + 'exit code': '終了コード', + 'errno': 'errno', +}; + +/// モンスタードロップアイテム日本語翻訳 +const Map dropItemTranslationsJa = { + // レベル 0-5 ドロップ + 'misspelling': 'スペルミス', + 'yellow flag': '黄色フラグ', + 'punctuation': '句読点', + 'question mark': '疑問符', + 'red squiggle': '赤い波線', + 'amber light': '黄色信号', + 'empty pointer': '空ポインター', + 'array fragment': '配列フラグメント', + 'infinity shard': '無限の破片', + 'failed check': '失敗した検査', + 'malformed token': '不正なトークン', + 'garbled text': '文字化け', + 'fence post': 'フェンスポスト', + 'twisted gate': '歪んだゲート', + 'spinning wheel': '回転ホイール', + 'dripping byte': '漏れるバイト', + 'overflowing cup': '溢れるカップ', + 'tangled thread': '絡まったスレッド', + 'void fragment': 'voidフラグメント', + 'crash crystal': 'クラッシュクリスタル', + 'thrown object': 'スローされたオブジェクト', + 'hourglass': '砂時計', + 'severed cable': '切断されたケーブル', + 'missing icon': '見つからないアイコン', + 'locked door': 'ロックされたドア', + + // レベル 6-10 ドロップ + 'leaked byte': 'リークしたバイト', + 'overflow data': 'オーバーフローデータ', + 'null crystal': 'ヌルクリスタル', + 'broken index': '壊れたインデックス', + 'morphed type': '変形した型', + 'dangling reference': 'ダングリングリファレンス', + 'duplicate key': '重複キー', + 'wrapped number': 'ラップされた数値', + 'format specifier': 'フォーマット指定子', + 'malicious query': '悪意あるクエリ', + 'script tag': 'スクリプトタグ', + 'forged request': '偽造リクエスト', + 'escaped path': 'エスケープパス', + 'shell command': 'シェルコマンド', + 'tangled threads': '絡まったスレッド群', + 'locked mutex': 'ロックされたミューテックス', + 'spinning lock': '回転ロック', + 'inverted queue': '逆転キュー', + 'hungry process': '貪欲なプロセス', + 'corrupted block': '破損ブロック', + 'crushed frame': '破壊されたフレーム', + 'garbled bytes': '文字化けバイト', + 'racing bits': 'レーシングビット', + 'floating reference': '浮遊リファレンス', + + // レベル 11-20 ドロップ + 'panic message': 'パニックメッセージ', + 'blue fragment': 'ブルーフラグメント', + 'dumped core': 'ダンプされたコア', + 'segment piece': 'セグメントピース', + 'bus token': 'バストークン', + 'missing page': '見つからないページ', + 'invalid cache': '無効キャッシュ', + 'translation fail': '変換失敗', + 'transfer error': '転送エラー', + 'signal flood': 'シグナルフラッド', + 'expired timer': '期限切れタイマー', + 'rom error': 'ROMエラー', + 'boot failure': '起動失敗', + 'boot sector': 'ブートセクター', + 'infected mbr': '感染MBR', + 'hidden process': '隠しプロセス', + 'kernel exploit': 'カーネルエクスプロイト', + 'vm breach': 'VM侵害', + 'management mode': '管理モード', + 'cpu patch': 'CPUパッチ', + 'speculative exec': '投機的実行', + 'kernel leak': 'カーネルリーク', + 'bit flip': 'ビットフリップ', + 'frozen memory': '凍結メモリ', + 'direct access': '直接アクセス', + 'timing info': 'タイミング情報', + 'persistent threat': '持続的脅威', + 'boot implant': 'ブートインプラント', + 'management engine': '管理エンジン', + + // レベル 21-30 ドロップ + 'apt sample': 'APTサンプル', + 'classified doc': '機密文書', + 'undisclosed vuln': '未公開脆弱性', + 'compromised package': '侵害パッケージ', + 'poisoned source': '汚染ソース', + 'crafted email': '細工されたメール', + 'system tool': 'システムツール', + 'memory only': 'メモリ専用', + 'mutating code': '変異コード', + 'self-modifying': '自己変更', + 'hidden section': '隠しセクション', + + // レベル 31-40 ドロップ + 'propagating mass': '伝播する塊', + 'c2 beacon': 'C2ビーコン', + 'encrypted key': '暗号化キー', + 'mining rig': 'マイニングリグ', + 'stolen data': '盗まれたデータ', + 'password hash': 'パスワードハッシュ', + 'keystroke log': 'キーストロークログ', + 'captured frame': 'キャプチャフレーム', + 'clipboard data': 'クリップボードデータ', + 'forged record': '偽造レコード', + + // レベル 41-45 ドロップ + 'plc payload': 'PLCペイロード', + 'wiper code': 'ワイパーコード', + 'smb exploit': 'SMBエクスプロイト', + 'nsa implant': 'NSAインプラント', + 'leaked tool': '流出ツール', + + // レベル 46-53 ドロップ + 'corrupted scale': '破損した鱗', + 'garbled essence': '文字化けエッセンス', + 'system fragment': 'システムフラグメント', + 'shattered block': '砕けたブロック', + 'reality tear': '現実の裂け目', + 'divine error': '神聖なエラー', + 'primordial bug': '太古のバグ', + + // 追加モンスタードロップ + 'old signature': '古代の署名', + 'outdated syntax': '古代の構文', + 'tangled logic': '絡まったロジック', + 'monolithic blob': 'モノリシックブロブ', + 'loop reference': 'ループ参照', + 'unexplained constant': '説明できない定数', + 'fixed string': '固定文字列', + 'shared state': '共有状態', + 'duplicate bug': '重複バグ', + 'mysterious ritual': '神秘的な儀式', + 'unreachable block': '到達不能ブロック', + 'undead thread': 'アンデッドスレッド', + 'parentless process': '親なしプロセス', + 'ghost reference': 'ゴーストリファレンス', + 'observer effect': 'オブザーバー効果', + 'quantum state': '量子状態', + 'deterministic flaw': '決定論的欠陥', + 'fractal complexity': 'フラクタル複雑度', + 'catastrophic fail': '壊滅的失敗', + 'documentation bug': 'ドキュメントバグ', + 'stealth code': 'ステルスコード', + 'compressed threat': '圧縮された脅威', + 'encrypted payload': '暗号化ペイロード', + 'delivery mechanism': '配信メカニズム', + 'stage one': 'ステージ1', + 'stage two': 'ステージ2', + 'startup entry': 'スタートアップエントリ', + 'elevated token': '昇格トークン', + 'network hop': 'ネットワークホップ', + 'covert comm': '秘密通信', + 'command callback': 'コマンドコールバック', + 'hidden channel': '隠しチャネル', + 'ping payload': 'Pingペイロード', + 'web shell': 'ウェブシェル', + 'callback conn': 'コールバック接続', + 'listening port': 'リスニングポート', + 'uploaded script': 'アップロードスクリプト', + 'scheduled task': 'スケジュールタスク', + 'boot persistence': 'ブート永続化', + 'dll implant': 'DLLインプラント', + 'memory injection': 'メモリインジェクション', + 'code injection': 'コードインジェクション', + 'async payload': '非同期ペイロード', + 'global table': 'グローバルテーブル', + 'transaction ntfs': 'トランザクションNTFS', + 'mapped memory': 'マップドメモリ', + 'overwritten dll': '上書きDLL', + 'fileless dll': 'ファイルレスDLL', + 'custom loader': 'カスタムローダー', + 'direct invoke': '直接呼び出し', + 'wow64 transition': 'WoW64遷移', + 'nested async': 'ネスト非同期', + 'unhandled await': '未処理await', + 'main thread': 'メインスレッド', + 'gc stress': 'GCストレス', + 'allocation spike': 'アロケーションスパイク', + 'destructor fail': 'デストラクタ失敗', + 'soft memory': 'ソフトメモリ', + 'pool overflow': 'プールオーバーフロー', + 'permgen fill': 'PermGen充満', + 'off-heap grow': 'オフヒープ増加', + 'nio overflow': 'NIOオーバーフロー', + 'tls accumulate': 'TLS蓄積', + 'socket drain': 'ソケット枯渇', + 'descriptor exhaust': 'ディスクリプタ枯渇', + 'event handler': 'イベントハンドラー', + 'subscription miss': 'サブスクリプション漏れ', + 'pending promise': '保留中Promise', + 'activity ref': 'Activityリファレンス', + 'image buffer': '画像バッファ', + 'db resource': 'DBリソース', + 'unclosed io': '未クローズIO', + 'uncommitted tx': '未コミットTX', + 'orphan session': 'オーファンセッション', + 'unlimited cache': '無制限キャッシュ', + 'unbounded queue': '無制限キュー', + 'circular fail': '循環失敗', + 'mutex fight': 'ミューテックス競争', + 'cpu spin': 'CPUスピン', + 'cache line': 'キャッシュライン', + 'memory locality': 'メモリローカリティ', + 'core binding': 'コアバインディング', + 'thread thrash': 'スレッドスラッシング', + 'page table': 'ページテーブル', + 'inter-processor': 'プロセッサ間', + 'cli hang': 'CLIハング', + 'scheduler race': 'スケジューラーレース', + 'read-copy-update': 'RCU', + 'sequence lock': 'シーケンスロック', + 'reader-writer': 'リーダーライター', + 'fast mutex': 'ファストミューテックス', + 'atomic spin': 'アトミックスピン', + 'sync point': '同期ポイント', + 'signal wait': 'シグナルウェイト', + 'count error': 'カウントエラー', + 'ipc fail': 'IPC失敗', + 'shm corrupt': 'SHM破損', + 'fd block': 'FDブロック', + 'network fd': 'ネットワークFD', + 'event poll': 'イベントポール', + 'kernel queue': 'カーネルキュー', + 'completion port': '完了ポート', + 'async io': '非同期IO', + 'vectored io': 'ベクターIO', + 'sendfile fail': 'sendfile失敗', + 'pipe transfer': 'パイプ転送', + 'vm splice': 'VMスプライス', + 'pipe duplicate': 'パイプ複製', + 'preallocate': '事前割り当て', + 'sparse file': 'スパースファイル', + 'o_direct': 'O_DIRECT', + 'fsync fail': 'fsync失敗', + 'fdatasync': 'fdatasync', + 'write barrier': '書き込みバリア', + 'filesystem log': 'ファイルシステムログ', + 'metadata exhaust': 'メタデータ枯渇', + 'dcache corrupt': 'dcache破損', + 'block buffer': 'ブロックバッファ', + 'file cache': 'ファイルキャッシュ', + 'kernel alloc': 'カーネル割り当て', + 'kernel malloc': 'カーネルmalloc', + 'virtual alloc': '仮想割り当て', + 'high memory': 'ハイメモリ', + 'low memory': 'ローメモリ', + 'out of memory': 'メモリ不足', + 'process flood': 'プロセスフラッド', + 'decompression': '解凍', + 'entity expand': 'エンティティ展開', + 'backtrack': 'バックトラック', + 'hashtable dos': 'ハッシュテーブルDoS', + 'o(n^2) attack': 'O(n²)攻撃', + 'slow http': 'スローHTTP', + 'slow post': 'スローPOST', + 'range header': 'Rangeヘッダー', + 'hash flood': 'ハッシュフラッド', + 'tcp handshake': 'TCPハンドシェイク', + 'datagram storm': 'データグラムストーム', + 'ping storm': 'Pingストーム', + 'broadcast amp': 'ブロードキャスト増幅', + 'udp amp': 'UDP増幅', + 'resolver abuse': 'リゾルバ悪用', + 'monlist abuse': 'monlist悪用', + 'upnp abuse': 'UPnP悪用', + 'cache abuse': 'キャッシュ悪用', + 'ldap abuse': 'LDAP悪用', + 'spoofed source': 'スプーフィングソース', + 'distributed target': '分散ターゲット', + 'burst attack': 'バースト攻撃', + 'evasive dos': '回避型DoS', + 'l7 attack': 'L7攻撃', + 'handshake abuse': 'ハンドシェイク悪用', + 'ssl reneg': 'SSL再ネゴ', + 'ssl downgrade': 'SSLダウングレード', + 'compression leak': '圧縮リーク', + 'http compression': 'HTTP圧縮', + 'ssl3 fallback': 'SSL3フォールバック', + 'openssl leak': 'OpenSSLリーク', + 'bash bug': 'Bashバグ', + 'copy on write': 'Copy-on-Write', + 'vm escape': 'VM脱出', + 'buffer overread': 'バッファオーバーリード', + 'wifi handshake': 'WiFiハンドシェイク', + 'wpa3 attack': 'WPA3攻撃', + 'wifi frag': 'WiFiフラグメント', + 'wifi encryption': 'WiFi暗号化', + 'wifi pmk': 'WiFi PMK', + 'rogue ap': '不正AP', + 'probe response': 'プローブレスポンス', + 'wifi disassoc': 'WiFi切断', + 'ssid spam': 'SSIDスパム', + 'bt exploit': 'BTエクスプロイト', + 'bt remote': 'BTリモート', + 'ble bug': 'BLEバグ', + 'bt classic': 'BTクラシック', + 'bt key': 'BTキー', + 'bt pairing': 'BTペアリング', + 'docsis bug': 'DOCSISバグ', + 'upnp vuln': 'UPnP脆弱性', + 'tcp/ip bug': 'TCP/IPバグ', + 'tcpip stack': 'TCP/IPスタック', + 'tcp random': 'TCPランダム', + 'dns bug': 'DNSバグ', + 'memory bug': 'メモリバグ', + 'polkit priv': 'Polkit権限', + 'privilege escape': '権限脱出', + 'heap overflow': 'ヒープオーバーフロー', +}; + +// ============================================================================ +// ストーリー/シネマティック日本語翻訳 (Story/Cinematic Translations) +// ============================================================================ + +/// Act タイトル日本語翻訳 +const Map actTitleTranslationsJa = { + 'Prologue': 'プロローグ', + 'Act I: Awakening': '第1幕: 覚醒', + 'Act II: Growth': '第2幕: 成長', + 'Act III: Trials': '第3幕: 試練', + 'Act IV: Confrontation': '第4幕: 対決', + 'Act V: Endgame': '第5幕: 終末', + 'The End': '完結', +}; + +/// Act別ボスモンスター名日本語翻訳 +const Map actBossTranslationsJa = { + 'BOSS: Stack Overflow Dragon': 'ボス: スタックオーバーフロードラゴン', + 'BOSS: Heap Corruption Hydra': 'ボス: ヒープ破損ヒドラ', + 'BOSS: Kernel Panic Titan': 'ボス: カーネルパニックタイタン', + 'BOSS: Zero Day Leviathan': 'ボス: ゼロデイリヴァイアサン', + 'BOSS: The Primordial Glitch': 'ボス: 太古のグリッチ', +}; + +/// Act別開始クエスト日本語翻訳 +const Map actQuestTranslationsJa = { + 'Exterminate the Bug Infestation': 'バグ侵入を駆除せよ', + 'Purge the Bug Nest': 'バグの巣を浄化せよ', + 'Cleanse the Corrupted Network': '破損したネットワークを浄化せよ', + 'Pass the Trials of the Ancient Compiler': '古代コンパイラーの試練を通過せよ', + "Infiltrate the Glitch God's Citadel": 'グリッチゴッドの城塞に潜入せよ', + 'Defeat the Glitch God': 'グリッチゴッドを打倒せよ', +}; + +/// シネマティックテキスト日本語翻訳 +const Map cinematicTranslationsJa = { + // プロローグ + '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."': 'コードの神は言われた:「関数あれ。」', + 'And so the Digital Realm was born...': 'かくしてデジタル世界が誕生した…', + 'But from the shadows emerged the Glitch.': 'しかし影からグリッチが出現した。', + 'Now, a new hero awakens to defend the Code.': '今、コードを守る新たな英雄が目覚める。', + 'Your journey begins...': 'あなたの旅が始まる…', + + // Act I: 覚醒 + '=== ACT I: AWAKENING ===': '=== 第1幕: 覚醒 ===', + 'You have proven yourself against the lesser bugs.': '下級バグとの戦いで実力を証明した。', + 'The Debugger Knights take notice of your potential.': 'デバッガー騎士団があなたの可能性に注目する。', + 'But a greater threat lurks in the Bug Nest...': 'しかしより大きな脅威がバグの巣に潜んでいる…', + 'The Syntax Error Dragon awaits.': '構文エラードラゴンが待ち構えている。', + + // Act II: 成長 + '=== ACT II: GROWTH ===': '=== 第2幕: 成長 ===', + 'With the Dragon slain, you join the Debugger Knights.': + 'ドラゴンを倒し、デバッガー騎士団に入団する。', + 'The Corrupted Network spreads its infection...': '破損したネットワークが感染を広げている…', + 'A traitor among the Knights is revealed!': '騎士団内の裏切り者が明らかになった!', + 'The Memory Leak Hydra threatens all data.': 'メモリリークヒドラがすべてのデータを脅かしている。', + 'You must stop the corruption before it consumes everything.': + 'すべてを飲み込む前に破損を止めなければならない。', + + // Act III: 試練 + '=== ACT III: TRIALS ===': '=== 第3幕: 試練 ===', + 'The path leads to the Null Kingdom...': '道はヌル(Null)王国へと続く…', + 'The Ancient Compiler challenges you to its trials.': '古代コンパイラーがあなたに試練を課す。', + 'A companion falls... their sacrifice not in vain.': '仲間が倒れる…その犠牲は無駄ではない。', + '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 Final Alliance is forged.': '最後の同盟が結成された。', + 'The Kernel Panic Archon blocks your path.': 'カーネルパニックアルコンが道を阻む。', + 'One final battle before the end...': '終末前の最後の戦い…', + + // Act V: 終末 + '=== ACT V: ENDGAME ===': '=== 第5幕: 終末 ===', + '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 END ===': '=== 完結 ===', + '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.': 'あなたの伝説は永遠のログにコンパイルされるだろう。', + 'THE END': '完', + '...or is it?': '…本当に?', +}; + +// ============================================================================ +// 世界観関連用語翻訳 (World-Building Terms) +// ============================================================================ + +/// 主要地域日本語翻訳 +const Map locationTranslationsJa = { + 'Null Kingdom': 'ヌル(Null)王国', + 'Core Abyss': 'コアの深淵', + 'Glitch Zone': 'グリッチ領域', + 'Binary Temple': 'バイナリ神殿', + 'Debug Zone': 'デバッグゾーン', + 'Data Market': 'データマーケット', + 'Tech Shop': 'テックショップ', + 'Cache Zone': 'キャッシュゾーン', + 'Bug Nest': 'バグの巣', + 'Corrupted Network': '破損したネットワーク', + "Glitch God's Citadel": 'グリッチゴッドの城塞', + 'Safe Mode': 'セーフモード', +}; + +/// 組織/勢力日本語翻訳 +const Map factionTranslationsJa = { + 'Debugger Knights': 'デバッガー騎士団', + 'Code God': 'コードの神', + 'Glitch God': 'グリッチゴッド', + 'Bug God': 'バグゴッド', + 'Ancient Compiler': '古代コンパイラー', + 'Final Alliance': '最後の同盟', + '64-bit King': '64ビット王', + 'Compiler Sage': 'コンパイラー賢者', + 'Debugger Saint': 'デバッガー聖人', +}; + +// ============================================================================ +// 追加モンスター翻訳 (レベル 54-100) +// ============================================================================ + +/// 高級/エンドゲームモンスター日本語翻訳 +const Map advancedMonsterTranslationsJa = { + // 高級システム脅威 (レベル 54-65) + 'Kernel Exploiter': 'カーネルエクスプロイター', + 'Ring -1 Phantom': 'リング-1ファントム', + 'TPM Bypasser': 'TPMバイパサー', + 'Secure Boot Breaker': 'セキュアブートブレイカー', + 'IOMMU Escape': 'IOMMU脱出', + 'SGX Enclave Bug': 'SGXエンクレーブバグ', + 'TrustZone Breach': 'TrustZone侵害', + 'Platform Security Bug': 'プラットフォームセキュリティバグ', + 'Hardware Backdoor': 'ハードウェアバックドア', + 'Supply Chain Implant': 'サプライチェーンインプラント', + 'BMC Rootkit': 'BMCルートキット', + 'IPMI Ghost': 'IPMIゴースト', + + // エンタープライズ級脅威 (レベル 66-80) + 'Active Directory Worm': 'Active Directoryワーム', + 'Kerberos Golden': 'ケルベロスゴールデン', + 'NTLM Relay Beast': 'NTLMリレービースト', + 'DCSync Phantom': 'DCSyncファントム', + 'Exchange Exploit': 'Exchangeエクスプロイト', + 'SharePoint Bug': 'SharePointバグ', + 'Teams Vulnerability': 'Teams脆弱性', + 'Azure AD Breach': 'Azure AD侵害', + 'AWS IAM Bug': 'AWS IAMバグ', + 'GCP Exploit': 'GCPエクスプロイト', + 'Kubernetes Escape': 'Kubernetes脱出', + 'Docker Breakout': 'Dockerブレイクアウト', + 'Service Mesh Bug': 'サービスメッシュバグ', + 'Terraform State Bug': 'Terraformステートバグ', + 'CI/CD Pipeline Poison': 'CI/CDパイプラインポイズン', + + // エンドゲームモンスター (レベル 81-90) + 'Quantum Decoherence': '量子デコヒーレンス', + 'Neural Network Poison': 'ニューラルネットワークポイズン', + 'AI Hallucination': 'AI幻覚', + 'Deep Fake Engine': 'ディープフェイクエンジン', + 'Adversarial Noise': '敵対的ノイズ', + 'Model Extraction': 'モデル抽出', + 'Prompt Injection': 'プロンプトインジェクション', + 'Training Data Poison': '学習データポイズン', + 'Federated Learning Bug': '連合学習バグ', + 'Differential Privacy Leak': '差分プライバシーリーク', + + // 最終エンドゲーム (レベル 91-100) + 'Post-Quantum Threat': 'ポスト量子脅威', + 'Homomorphic Crack': '準同型暗号クラック', + 'Zero Knowledge Flaw': 'ゼロ知識欠陥', + 'Blockchain Fork': 'ブロックチェーンフォーク', + 'Smart Contract Bug': 'スマートコントラクトバグ', + 'MEV Extractor': 'MEV抽出者', + 'Cross-Chain Bridge Bug': 'クロスチェーンブリッジバグ', + 'Oracle Manipulation': 'オラクル操作', + 'Flash Loan Attack': 'フラッシュローン攻撃', + 'The Final Bug': '最後のバグ', + + // ミニボス + 'Elite Syntax Overlord': 'エリート構文オーバーロード', + 'Champion Buffer Crusher': 'チャンピオンバッファクラッシャー', + 'Veteran Memory Lord': 'ベテランメモリロード', + 'Master Race Conductor': 'マスターレースコンダクター', + 'Arch Kernel Breaker': 'アーチカーネルブレイカー', + 'High Protocol Corruptor': 'ハイプロトコルコラプター', + 'Grand Firmware Defiler': 'グランドファームウェアデファイラー', + 'Supreme Cloud Invader': 'スプリームクラウドインベーダー', + 'Legendary Container Escapist': 'レジェンダリーコンテナエスケーピスト', + 'Ancient Pipeline Poisoner': 'エンシェントパイプラインポイズナー', + + // ボスモンスター + 'BOSS: APT Colossus': 'ボス: APTコロッサス', + 'BOSS: Ransomware Emperor': 'ボス: ランサムウェアエンペラー', + 'BOSS: AI Singularity': 'ボス: AIシンギュラリティ', +}; + +/// 追加ドロップアイテム翻訳 +const Map additionalDropTranslationsJa = { + // レベル 54-65 ドロップ + 'privilege token': '権限トークン', + 'hypervisor breach': 'ハイパーバイザー侵害', + 'trusted module': '信頼モジュール', + 'boot chain': 'ブートチェーン', + 'memory isolation': 'メモリ隔離', + 'secure enclave': 'セキュアエンクレーブ', + 'arm security': 'ARMセキュリティ', + 'firmware key': 'ファームウェアキー', + 'silicon implant': 'シリコンインプラント', + 'factory malware': 'ファクトリーマルウェア', + 'baseboard mgmt': 'ベースボード管理', + 'remote mgmt': 'リモート管理', + + // レベル 66-80 ドロップ + 'domain token': 'ドメイントークン', + 'ticket forgery': 'チケット偽造', + 'auth bypass': '認証バイパス', + 'replication attack': 'レプリケーション攻撃', + 'mail server': 'メールサーバー', + 'collab breach': 'コラボ侵害', + 'comm exploit': '通信エクスプロイト', + 'cloud identity': 'クラウドID', + 'cloud permission': 'クラウド権限', + 'google cloud': 'Google Cloud', + 'container breach': 'コンテナ侵害', + 'namespace escape': '名前空間脱出', + 'istio envoy': 'Istio Envoy', + 'infra code': 'インフラコード', + 'build compromise': 'ビルド侵害', + + // レベル 81-90 ドロップ + 'qubit collapse': 'キュービット崩壊', + 'model corrupt': 'モデル破損', + 'false output': '偽出力', + 'synthetic media': '合成メディア', + 'ml attack': 'ML攻撃', + 'stolen weights': '盗まれた重み', + 'llm exploit': 'LLMエクスプロイト', + 'dataset corrupt': 'データセット破損', + 'distributed ml': '分散ML', + 'anonymity breach': '匿名性侵害', + + // レベル 91-100 ドロップ + 'lattice attack': '格子攻撃', + 'encrypted compute': '暗号化計算', + 'proof bypass': '証明バイパス', + 'consensus break': 'コンセンサス破壊', + 'solidity exploit': 'Solidityエクスプロイト', + 'transaction reorder': 'トランザクション並び替え', + 'bridge exploit': 'ブリッジエクスプロイト', + 'price feed': '価格フィード', + 'defi exploit': 'DeFiエクスプロイト', + 'ultimate error': '究極のエラー', + + // ミニボス/ボスドロップ + 'syntax crown': '構文の王冠', + 'overflow gem': 'オーバーフロージェム', + 'leak artifact': 'リークアーティファクト', + 'thread scepter': 'スレッドセプター', + 'ring zero': 'リングゼロ', + 'packet throne': 'パケット玉座', + 'boot artifact': 'ブートアーティファクト', + 'cloud crown': 'クラウドクラウン', + 'namespace key': '名前空間キー', + 'build shard': 'ビルドシャード', + 'legendary stack': '伝説のスタック', + 'multi-head leak': 'マルチヘッドリーク', + 'system crash': 'システムクラッシュ', + 'unknown vuln': '未知の脆弱性', + 'state actor': '国家アクター', + 'encrypted realm': '暗号化領域', + 'machine god': '機械神', + 'genesis bug': '創世バグ', +}; + +/// 追加盾翻訳 +const Map additionalShieldTranslationsJa = { + 'Neural Defense Grid': 'ニューラル防御グリッド', + 'Singularity Absorber': 'シンギュラリティアブソーバー', + 'Time Dilation Field': '時間拡張フィールド', + 'Reality Anchor': '現実アンカー', + 'Multiverse Barrier': 'マルチバースバリア', + 'Cosmic Dampener': 'コズミックダンプナー', + 'Entropy Shield': 'エントロピーシールド', +}; + +/// 追加鎧翻訳 +const Map additionalArmorTranslationsJa = { + 'Blockchain Platemail': 'ブロックチェーンプレートメイル', + 'Neural Network Mesh': 'ニューラルネットワークメッシュ', + 'AI Firewall': 'AIファイアウォール', + 'Quantum Shield Matrix': '量子シールドマトリックス', + 'Singularity Barrier': 'シンギュラリティバリア', + 'Multiverse Armor': 'マルチバースアーマー', +}; + +/// 追加アイテム属性翻訳 +const Map additionalItemAttribTranslationsJa = { + 'Containerized': 'コンテナ化された', + 'Orchestrated': 'オーケストレーション済み', + 'Scalable': 'スケーラブル', + 'Resilient': 'レジリエント', + 'Fault-Tolerant': 'フォールトトレラント', + 'Self-Healing': '自己修復', + 'Auto-Scaling': 'オートスケーリング', + 'Load-Balanced': 'ロードバランス済み', + 'Cached': 'キャッシュ済み', + 'Indexed': 'インデックス済み', + 'Sharded': 'シャーディング済み', + 'Partitioned': 'パーティション済み', + 'Compressed': '圧縮済み', + 'Tokenized': 'トークン化済み', + 'Anonymized': '匿名化済み', + 'Sanitized': 'サニタイズ済み', + 'Validated': '検証済み', +}; + +/// 追加ItemOfs翻訳 +const Map additionalItemOfsTranslationsJa = { + 'Microservices': 'マイクロサービス', + 'Serverless': 'サーバーレス', + 'Edge Computing': 'エッジコンピューティング', + 'Fog Computing': 'フォグコンピューティング', + 'Cloud Native': 'クラウドネイティブ', + 'DevOps': 'DevOps', + 'Site Reliability': 'サイト信頼性', + 'Platform Engineering': 'プラットフォームエンジニアリング', + 'Infrastructure': 'インフラストラクチャ', + 'Observability': 'オブザーバビリティ', + 'Telemetry': 'テレメトリー', + 'Tracing': 'トレーシング', + 'Metrics': 'メトリクス', + 'Alerting': 'アラート', + 'Incident Response': 'インシデント対応', + 'Chaos Engineering': 'カオスエンジニアリング', + 'Resilience': 'レジリエンス', + 'Availability': '可用性', + 'Durability': '耐久性', + 'Consistency': '一貫性', + 'Partition Tolerance': '分断耐性', +}; + +// ============================================================================ +// 統合翻訳Getter (Unified Translation Getters) +// ============================================================================ + +/// すべてのモンスター翻訳を統合して返す +Map get allMonsterTranslationsJa => { + ...monsterTranslationsJa, + ...advancedMonsterTranslationsJa, + }; + +/// すべてのアイテム属性翻訳を統合して返す +Map get allItemAttribTranslationsJa => { + ...itemAttribTranslationsJa, + ...additionalItemAttribTranslationsJa, + }; + +/// すべてのアイテム接尾辞(~の)翻訳を統合して返す +Map get allItemOfsTranslationsJa => { + ...itemOfsTranslationsJa, + ...additionalItemOfsTranslationsJa, + }; + +/// すべてのドロップアイテム翻訳を統合して返す +Map get allDropTranslationsJa => { + ...boringItemTranslationsJa, + ...dropItemTranslationsJa, + ...additionalDropTranslationsJa, + }; + +/// すべての鎧翻訳を統合して返す +Map get allArmorTranslationsJa => { + ...armorTranslationsJa, + ...additionalArmorTranslationsJa, + }; + +/// すべての盾翻訳を統合して返す +Map get allShieldTranslationsJa => { + ...shieldTranslationsJa, + ...additionalShieldTranslationsJa, + }; diff --git a/lib/data/game_translations_ko.dart b/lib/data/game_translations_ko.dart index 5fffdcc..3841aeb 100644 --- a/lib/data/game_translations_ko.dart +++ b/lib/data/game_translations_ko.dart @@ -1,5 +1,31 @@ +// ============================================================================ // ASCII NEVER DIE 한국어 번역 데이터 -// 게임 데이터의 한국어 번역을 제공합니다. +// ============================================================================ +// +// 이 파일은 게임의 모든 한국어 번역을 포함합니다. +// 영문 원본은 pq_config_data.dart에 정의되어 있습니다. +// +// ## 세계관 (World Setting) +// 아스키나라(ASCII-Nara): 코드의 신이 창조한 디지털 판타지 세계 +// - 널(Null) 왕국: 모험자들이 시작하는 중앙 도시 (64비트 왕 통치) +// - 코어의 심연: 전설적 장비가 발견되는 고대 던전 +// - 글리치 영역: 버그와 오류로 현실이 뒤틀린 지역 +// - 바이너리 신전: 코드의 신을 숭배하는 신성한 장소 +// +// ## 번역 구조 +// - 기본 번역 Maps: raceTranslationsKo, klassTranslationsKo, monsterTranslationsKo 등 +// - 추가 번역 Maps: advancedMonsterTranslationsKo, additionalDropTranslationsKo 등 +// - 스토리 번역 Maps: actTitleTranslationsKo, cinematicTranslationsKo 등 +// - 세계관 번역 Maps: locationTranslationsKo, factionTranslationsKo +// +// ## 번역 함수 사용 (game_text_l10n.dart) +// - translateMonster(name): 몬스터 이름 번역 +// - translateRace(name): 종족 이름 번역 +// - translateItemAttrib(name): 아이템 속성 번역 +// - translateCinematic(text): 시네마틱 텍스트 번역 +// - 등등... +// +// ============================================================================ /// 종족 이름 한국어 번역 const Map raceTranslationsKo = { @@ -1101,3 +1127,415 @@ const Map dropItemTranslationsKo = { 'privilege escape': '권한 탈출', 'heap overflow': '힙 오버플로우', }; + +// ============================================================================ +// 스토리/시네마틱 한국어 번역 (Story/Cinematic Translations) +// ============================================================================ + +/// Act 제목 한국어 번역 +const Map actTitleTranslationsKo = { + 'Prologue': '프롤로그', + 'Act I: Awakening': '제1막: 각성', + 'Act II: Growth': '제2막: 성장', + 'Act III: Trials': '제3막: 시련', + 'Act IV: Confrontation': '제4막: 결전', + 'Act V: Endgame': '제5막: 종말', + 'The End': '완결', +}; + +/// Act별 보스 몬스터 이름 한국어 번역 +const Map actBossTranslationsKo = { + 'BOSS: Stack Overflow Dragon': '보스: 스택 오버플로우 드래곤', + 'BOSS: Heap Corruption Hydra': '보스: 힙 손상 히드라', + 'BOSS: Kernel Panic Titan': '보스: 커널 패닉 타이탄', + 'BOSS: Zero Day Leviathan': '보스: 제로데이 리바이어던', + 'BOSS: The Primordial Glitch': '보스: 태초의 글리치', +}; + +/// Act별 시작 퀘스트 한국어 번역 +const Map actQuestTranslationsKo = { + 'Exterminate the Bug Infestation': '버그 침입 소탕', + 'Purge the Bug Nest': '버그 둥지 정화', + 'Cleanse the Corrupted Network': '손상된 네트워크 정화', + 'Pass the Trials of the Ancient Compiler': '고대 컴파일러의 시련 통과', + "Infiltrate the Glitch God's Citadel": '글리치 신의 성채 침투', + 'Defeat the Glitch God': '글리치 신 처치', +}; + +/// 시네마틱 텍스트 한국어 번역 +const Map cinematicTranslationsKo = { + // 프롤로그 + '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."': + '코드의 신이 말씀하셨다: "함수가 있으라."', + 'And so the Digital Realm was born...': '그리하여 디지털 세계가 탄생하였다...', + 'But from the shadows emerged the Glitch.': '그러나 어둠 속에서 글리치가 출현했다.', + 'Now, a new hero awakens to defend the Code.': + '이제, 코드를 수호할 새로운 영웅이 깨어난다.', + 'Your journey begins...': '당신의 여정이 시작된다...', + + // Act I: 각성 + '=== ACT I: AWAKENING ===': '=== 제1막: 각성 ===', + 'You have proven yourself against the lesser bugs.': + '당신은 하급 버그들을 상대로 실력을 증명했다.', + 'The Debugger Knights take notice of your potential.': + '디버거 기사단이 당신의 잠재력을 주목한다.', + 'But a greater threat lurks in the Bug Nest...': + '하지만 더 큰 위협이 버그 둥지에 도사리고 있다...', + 'The Syntax Error Dragon awaits.': '문법 오류 드래곤이 기다린다.', + + // Act II: 성장 + '=== ACT II: GROWTH ===': '=== 제2막: 성장 ===', + 'With the Dragon slain, you join the Debugger Knights.': + '드래곤을 처치하고, 당신은 디버거 기사단에 입단한다.', + 'The Corrupted Network spreads its infection...': + '손상된 네트워크가 감염을 퍼뜨리고 있다...', + 'A traitor among the Knights is revealed!': '기사단 내 배신자가 드러났다!', + 'The Memory Leak Hydra threatens all data.': + '메모리 누수 히드라가 모든 데이터를 위협한다.', + 'You must stop the corruption before it consumes everything.': + '모든 것을 삼키기 전에 손상을 멈춰야 한다.', + + // Act III: 시련 + '=== ACT III: TRIALS ===': '=== 제3막: 시련 ===', + 'The path leads to the Null Kingdom...': '길은 널(Null) 왕국으로 이어진다...', + 'The Ancient Compiler challenges you to its trials.': + '고대 컴파일러가 당신에게 시련을 건넨다.', + 'A companion falls... their sacrifice not in vain.': + '동료가 쓰러진다... 그들의 희생은 헛되지 않으리.', + '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 Final Alliance is forged.': '최후의 동맹이 결성되었다.', + 'The Kernel Panic Archon blocks your path.': + '커널 패닉 아르콘이 당신의 길을 막는다.', + 'One final battle before the end...': '종말 전의 마지막 전투...', + + // Act V: 종말 + '=== ACT V: ENDGAME ===': '=== 제5막: 종말 ===', + '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 END ===': '=== 완결 ===', + '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.': + '당신의 전설은 영원한 로그에 컴파일될 것이다.', + 'THE END': '완', + '...or is it?': '...정말 그럴까?', +}; + +// ============================================================================ +// 세계관 관련 용어 번역 (World-Building Terms) +// ============================================================================ + +/// 주요 지역 한국어 번역 +const Map locationTranslationsKo = { + 'Null Kingdom': '널(Null) 왕국', + 'Core Abyss': '코어의 심연', + 'Glitch Zone': '글리치 영역', + 'Binary Temple': '바이너리 신전', + 'Debug Zone': '디버그 존', + 'Data Market': '데이터 마켓', + 'Tech Shop': '테크 샵', + 'Cache Zone': '캐시 존', + 'Bug Nest': '버그 둥지', + 'Corrupted Network': '손상된 네트워크', + "Glitch God's Citadel": '글리치 신의 성채', + 'Safe Mode': '안전 모드', +}; + +/// 조직/세력 한국어 번역 +const Map factionTranslationsKo = { + 'Debugger Knights': '디버거 기사단', + 'Code God': '코드의 신', + 'Glitch God': '글리치 신', + 'Bug God': '버그 신', + 'Ancient Compiler': '고대 컴파일러', + 'Final Alliance': '최후의 동맹', + '64-bit King': '64비트 왕', + 'Compiler Sage': '컴파일러 현자', + 'Debugger Saint': '디버거 성인', +}; + +// ============================================================================ +// 추가 몬스터 번역 (레벨 54-100) +// ============================================================================ + +/// 고급/엔드게임 몬스터 한국어 번역 +const Map advancedMonsterTranslationsKo = { + // 고급 시스템 위협 (레벨 54-65) + 'Kernel Exploiter': '커널 익스플로이터', + 'Ring -1 Phantom': '링 -1 팬텀', + 'TPM Bypasser': 'TPM 우회자', + 'Secure Boot Breaker': '시큐어 부트 파괴자', + 'IOMMU Escape': 'IOMMU 탈출', + 'SGX Enclave Bug': 'SGX 엔클레이브 버그', + 'TrustZone Breach': '트러스트존 침해', + 'Platform Security Bug': '플랫폼 보안 버그', + 'Hardware Backdoor': '하드웨어 백도어', + 'Supply Chain Implant': '공급망 임플란트', + 'BMC Rootkit': 'BMC 루트킷', + 'IPMI Ghost': 'IPMI 유령', + + // 엔터프라이즈급 위협 (레벨 66-80) + 'Active Directory Worm': '액티브 디렉토리 웜', + 'Kerberos Golden': '케르베로스 골든', + 'NTLM Relay Beast': 'NTLM 릴레이 야수', + 'DCSync Phantom': 'DCSync 팬텀', + 'Exchange Exploit': '익스체인지 익스플로잇', + 'SharePoint Bug': '셰어포인트 버그', + 'Teams Vulnerability': '팀즈 취약점', + 'Azure AD Breach': '애저 AD 침해', + 'AWS IAM Bug': 'AWS IAM 버그', + 'GCP Exploit': 'GCP 익스플로잇', + 'Kubernetes Escape': '쿠버네티스 탈출', + 'Docker Breakout': '도커 브레이크아웃', + 'Service Mesh Bug': '서비스 메시 버그', + 'Terraform State Bug': '테라폼 상태 버그', + 'CI/CD Pipeline Poison': 'CI/CD 파이프라인 오염', + + // 엔드게임 몬스터 (레벨 81-90) + 'Quantum Decoherence': '양자 결어긋남', + 'Neural Network Poison': '신경망 오염', + 'AI Hallucination': 'AI 환각', + 'Deep Fake Engine': '딥페이크 엔진', + 'Adversarial Noise': '적대적 노이즈', + 'Model Extraction': '모델 추출', + 'Prompt Injection': '프롬프트 인젝션', + 'Training Data Poison': '학습 데이터 오염', + 'Federated Learning Bug': '연합 학습 버그', + 'Differential Privacy Leak': '차등 프라이버시 누출', + + // 최종 엔드게임 (레벨 91-100) + 'Post-Quantum Threat': '포스트 양자 위협', + 'Homomorphic Crack': '동형 암호 균열', + 'Zero Knowledge Flaw': '영지식 결함', + 'Blockchain Fork': '블록체인 포크', + 'Smart Contract Bug': '스마트 컨트랙트 버그', + 'MEV Extractor': 'MEV 추출자', + 'Cross-Chain Bridge Bug': '크로스체인 브릿지 버그', + 'Oracle Manipulation': '오라클 조작', + 'Flash Loan Attack': '플래시 론 공격', + 'The Final Bug': '최후의 버그', + + // 미니보스 + 'Elite Syntax Overlord': '엘리트 문법 군주', + 'Champion Buffer Crusher': '챔피언 버퍼 파괴자', + 'Veteran Memory Lord': '베테랑 메모리 군주', + 'Master Race Conductor': '마스터 레이스 지휘자', + 'Arch Kernel Breaker': '대 커널 파괴자', + 'High Protocol Corruptor': '상위 프로토콜 오염자', + 'Grand Firmware Defiler': '대 펌웨어 훼손자', + 'Supreme Cloud Invader': '최고 클라우드 침략자', + 'Legendary Container Escapist': '전설의 컨테이너 탈출자', + 'Ancient Pipeline Poisoner': '고대 파이프라인 오염자', + + // 보스 몬스터 + 'BOSS: APT Colossus': '보스: APT 거신', + 'BOSS: Ransomware Emperor': '보스: 랜섬웨어 황제', + 'BOSS: AI Singularity': '보스: AI 싱귤래리티', +}; + +/// 추가 드롭 아이템 번역 +const Map additionalDropTranslationsKo = { + // 레벨 54-65 드롭 + 'privilege token': '권한 토큰', + 'hypervisor breach': '하이퍼바이저 침해', + 'trusted module': '신뢰 모듈', + 'boot chain': '부트 체인', + 'memory isolation': '메모리 격리', + 'secure enclave': '보안 엔클레이브', + 'arm security': 'ARM 보안', + 'firmware key': '펌웨어 키', + 'silicon implant': '실리콘 임플란트', + 'factory malware': '공장 악성코드', + 'baseboard mgmt': '베이스보드 관리', + 'remote mgmt': '원격 관리', + + // 레벨 66-80 드롭 + 'domain token': '도메인 토큰', + 'ticket forgery': '티켓 위조', + 'auth bypass': '인증 우회', + 'replication attack': '복제 공격', + 'mail server': '메일 서버', + 'collab breach': '협업 침해', + 'comm exploit': '통신 익스플로잇', + 'cloud identity': '클라우드 ID', + 'cloud permission': '클라우드 권한', + 'google cloud': '구글 클라우드', + 'container breach': '컨테이너 침해', + 'namespace escape': '네임스페이스 탈출', + 'istio envoy': 'Istio 엔보이', + 'infra code': '인프라 코드', + 'build compromise': '빌드 침해', + + // 레벨 81-90 드롭 + 'qubit collapse': '큐비트 붕괴', + 'model corrupt': '모델 손상', + 'false output': '거짓 출력', + 'synthetic media': '합성 미디어', + 'ml attack': 'ML 공격', + 'stolen weights': '탈취된 가중치', + 'llm exploit': 'LLM 익스플로잇', + 'dataset corrupt': '데이터셋 손상', + 'distributed ml': '분산 ML', + 'anonymity breach': '익명성 침해', + + // 레벨 91-100 드롭 + 'lattice attack': '격자 공격', + 'encrypted compute': '암호화 연산', + 'proof bypass': '증명 우회', + 'consensus break': '합의 파괴', + 'solidity exploit': '솔리디티 익스플로잇', + 'transaction reorder': '트랜잭션 재정렬', + 'bridge exploit': '브릿지 익스플로잇', + 'price feed': '가격 피드', + 'defi exploit': 'DeFi 익스플로잇', + 'ultimate error': '궁극의 오류', + + // 미니보스/보스 드롭 + 'syntax crown': '문법의 왕관', + 'overflow gem': '오버플로우 보석', + 'leak artifact': '누수 유물', + 'thread scepter': '스레드 홀', + 'ring zero': '링 제로', + 'packet throne': '패킷 왕좌', + 'boot artifact': '부트 유물', + 'cloud crown': '클라우드 왕관', + 'namespace key': '네임스페이스 열쇠', + 'build shard': '빌드 파편', + 'legendary stack': '전설의 스택', + 'multi-head leak': '다중 머리 누수', + 'system crash': '시스템 붕괴', + 'unknown vuln': '미지의 취약점', + 'state actor': '국가급 행위자', + 'encrypted realm': '암호화된 영역', + 'machine god': '기계 신', + 'genesis bug': '시초 버그', +}; + +/// 추가 방패 번역 +const Map additionalShieldTranslationsKo = { + 'Neural Defense Grid': '신경 방어 그리드', + 'Singularity Absorber': '특이점 흡수기', + 'Time Dilation Field': '시간 확장 필드', + 'Reality Anchor': '현실 닻', + 'Multiverse Barrier': '다중우주 장벽', + 'Cosmic Dampener': '우주 완충기', + 'Entropy Shield': '엔트로피 실드', +}; + +/// 추가 갑옷 번역 +const Map additionalArmorTranslationsKo = { + 'Blockchain Platemail': '블록체인 판금갑옷', + 'Neural Network Mesh': '신경망 메시', + 'AI Firewall': 'AI 방화벽', + 'Quantum Shield Matrix': '양자 실드 매트릭스', + 'Singularity Barrier': '특이점 장벽', + 'Multiverse Armor': '다중우주 갑옷', +}; + +/// 추가 아이템 속성 번역 +const Map additionalItemAttribTranslationsKo = { + 'Containerized': '컨테이너화된', + 'Orchestrated': '오케스트레이션된', + 'Scalable': '확장 가능한', + 'Resilient': '복원력 있는', + 'Fault-Tolerant': '장애 허용', + 'Self-Healing': '자가 치유', + 'Auto-Scaling': '자동 확장', + 'Load-Balanced': '로드 밸런싱된', + 'Cached': '캐시된', + 'Indexed': '인덱싱된', + 'Sharded': '샤딩된', + 'Partitioned': '파티션된', + 'Compressed': '압축된', + 'Tokenized': '토큰화된', + 'Anonymized': '익명화된', + 'Sanitized': '새니타이즈된', + 'Validated': '검증된', +}; + +/// 추가 ItemOfs 번역 +const Map additionalItemOfsTranslationsKo = { + 'Microservices': '마이크로서비스', + 'Serverless': '서버리스', + 'Edge Computing': '엣지 컴퓨팅', + 'Fog Computing': '포그 컴퓨팅', + 'Cloud Native': '클라우드 네이티브', + 'DevOps': '데브옵스', + 'Site Reliability': '사이트 신뢰성', + 'Platform Engineering': '플랫폼 엔지니어링', + 'Infrastructure': '인프라스트럭처', + 'Observability': '관측 가능성', + 'Telemetry': '텔레메트리', + 'Tracing': '트레이싱', + 'Metrics': '메트릭', + 'Alerting': '알림', + 'Incident Response': '인시던트 대응', + 'Chaos Engineering': '카오스 엔지니어링', + 'Resilience': '복원력', + 'Availability': '가용성', + 'Durability': '내구성', + 'Consistency': '일관성', + 'Partition Tolerance': '분할 허용', +}; + +// ============================================================================ +// 통합 번역 Getter (Unified Translation Getters) +// ============================================================================ + +/// 모든 몬스터 번역을 통합하여 반환 +Map get allMonsterTranslationsKo => { + ...monsterTranslationsKo, + ...advancedMonsterTranslationsKo, + }; + +/// 모든 아이템 속성 번역을 통합하여 반환 +Map get allItemAttribTranslationsKo => { + ...itemAttribTranslationsKo, + ...additionalItemAttribTranslationsKo, + }; + +/// 모든 아이템 접미사("~의") 번역을 통합하여 반환 +Map get allItemOfsTranslationsKo => { + ...itemOfsTranslationsKo, + ...additionalItemOfsTranslationsKo, + }; + +/// 모든 드롭 아이템 번역을 통합하여 반환 +Map get allDropTranslationsKo => { + ...boringItemTranslationsKo, + ...dropItemTranslationsKo, + ...additionalDropTranslationsKo, + }; + +/// 모든 갑옷 번역을 통합하여 반환 +Map get allArmorTranslationsKo => { + ...armorTranslationsKo, + ...additionalArmorTranslationsKo, + }; + +/// 모든 방패 번역을 통합하여 반환 +Map get allShieldTranslationsKo => { + ...shieldTranslationsKo, + ...additionalShieldTranslationsKo, + }; diff --git a/lib/data/potion_data.dart b/lib/data/potion_data.dart new file mode 100644 index 0000000..975e4eb --- /dev/null +++ b/lib/data/potion_data.dart @@ -0,0 +1,187 @@ +import 'package:askiineverdie/src/core/model/potion.dart'; + +/// 게임 내 물약 정의 +/// +/// HP/MP 물약 데이터 (티어 1~5) +class PotionData { + PotionData._(); + + // ============================================================================ + // HP 물약 + // ============================================================================ + + /// Minor Health Patch - 소형 HP 물약 + static const minorHealthPatch = Potion( + id: 'minor_health_patch', + name: 'Minor Health Patch', + type: PotionType.hp, + tier: 1, + healAmount: 30, + healPercent: 0.0, + price: 25, + ); + + /// Health Patch - 일반 HP 물약 + static const healthPatch = Potion( + id: 'health_patch', + name: 'Health Patch', + type: PotionType.hp, + tier: 2, + healAmount: 50, + healPercent: 0.10, + price: 75, + ); + + /// Major Health Patch - 대형 HP 물약 + static const majorHealthPatch = Potion( + id: 'major_health_patch', + name: 'Major Health Patch', + type: PotionType.hp, + tier: 3, + healAmount: 80, + healPercent: 0.20, + price: 200, + ); + + /// Super Health Patch - 초대형 HP 물약 + static const superHealthPatch = Potion( + id: 'super_health_patch', + name: 'Super Health Patch', + type: PotionType.hp, + tier: 4, + healAmount: 120, + healPercent: 0.30, + price: 500, + ); + + /// Ultra Health Patch - 최고급 HP 물약 + static const ultraHealthPatch = Potion( + id: 'ultra_health_patch', + name: 'Ultra Health Patch', + type: PotionType.hp, + tier: 5, + healAmount: 200, + healPercent: 0.40, + price: 1200, + ); + + // ============================================================================ + // MP 물약 + // ============================================================================ + + /// Minor Mana Cache - 소형 MP 물약 + static const minorManaCache = Potion( + id: 'minor_mana_cache', + name: 'Minor Mana Cache', + type: PotionType.mp, + tier: 1, + healAmount: 20, + healPercent: 0.0, + price: 20, + ); + + /// Mana Cache - 일반 MP 물약 + static const manaCache = Potion( + id: 'mana_cache', + name: 'Mana Cache', + type: PotionType.mp, + tier: 2, + healAmount: 40, + healPercent: 0.10, + price: 60, + ); + + /// Major Mana Cache - 대형 MP 물약 + static const majorManaCache = Potion( + id: 'major_mana_cache', + name: 'Major Mana Cache', + type: PotionType.mp, + tier: 3, + healAmount: 60, + healPercent: 0.20, + price: 160, + ); + + /// Super Mana Cache - 초대형 MP 물약 + static const superManaCache = Potion( + id: 'super_mana_cache', + name: 'Super Mana Cache', + type: PotionType.mp, + tier: 4, + healAmount: 90, + healPercent: 0.30, + price: 400, + ); + + /// Ultra Mana Cache - 최고급 MP 물약 + static const ultraManaCache = Potion( + id: 'ultra_mana_cache', + name: 'Ultra Mana Cache', + type: PotionType.mp, + tier: 5, + healAmount: 150, + healPercent: 0.40, + price: 1000, + ); + + // ============================================================================ + // 물약 목록 + // ============================================================================ + + /// 모든 물약 목록 + static const List all = [ + // HP 물약 + minorHealthPatch, + healthPatch, + majorHealthPatch, + superHealthPatch, + ultraHealthPatch, + // MP 물약 + minorManaCache, + manaCache, + majorManaCache, + superManaCache, + ultraManaCache, + ]; + + /// HP 물약 목록 + static List get hpPotions => + all.where((p) => p.type == PotionType.hp).toList(); + + /// MP 물약 목록 + static List get mpPotions => + all.where((p) => p.type == PotionType.mp).toList(); + + /// ID로 물약 찾기 + static Potion? getById(String id) { + for (final potion in all) { + if (potion.id == id) return potion; + } + return null; + } + + /// 티어별 HP 물약 + static Potion? getHpPotionByTier(int tier) { + for (final potion in hpPotions) { + if (potion.tier == tier) return potion; + } + return null; + } + + /// 티어별 MP 물약 + static Potion? getMpPotionByTier(int tier) { + for (final potion in mpPotions) { + if (potion.tier == tier) return potion; + } + return null; + } + + /// 레벨에 맞는 HP 물약 티어 + static int tierForLevel(int level) { + if (level < 10) return 1; + if (level < 25) return 2; + if (level < 45) return 3; + if (level < 70) return 4; + return 5; + } +} diff --git a/lib/data/skill_data.dart b/lib/data/skill_data.dart index cc384ab..f3a1d24 100644 --- a/lib/data/skill_data.dart +++ b/lib/data/skill_data.dart @@ -78,6 +78,100 @@ class SkillData { 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', + type: SkillType.attack, + mpCost: 30, + cooldownMs: 12000, // 12초 + power: 0, + damageMultiplier: 0, + element: SkillElement.fire, + attackMode: AttackMode.dot, + baseDotDamage: 15, // 틱당 15 데미지 + baseDotDurationMs: 4000, // 4초 지속 + baseDotTickMs: 500, // 0.5초마다 틱 + ); + + /// Race Condition - 빠른 DOT + /// + /// 매우 빠른 틱의 번개 DOT + static const raceCondition = Skill( + id: 'race_condition', + name: 'Race Condition', + 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초마다 틱 + ); + // ============================================================================ // 회복 스킬 // ============================================================================ @@ -175,13 +269,19 @@ class SkillData { /// 모든 스킬 목록 static const List allSkills = [ - // 공격 스킬 + // 공격 스킬 (단발성) debugStrike, nullPointer, memoryLeak, stackOverflow, coreDump, kernelPanic, + // DOT 스킬 + memoryCorruption, + infiniteLoop, + thermalThrottle, + raceCondition, + system32Delete, // 회복 스킬 quickFix, hotReload, @@ -192,6 +292,10 @@ class SkillData { firewall, ]; + /// DOT 스킬 목록 + static List get dotSkills => + allSkills.where((s) => s.isDot).toList(); + /// ID로 스킬 찾기 static Skill? getSkillById(String id) { for (final skill in allSkills) { diff --git a/lib/src/core/engine/game_mutations.dart b/lib/src/core/engine/game_mutations.dart index 78ddceb..81b8452 100644 --- a/lib/src/core/engine/game_mutations.dart +++ b/lib/src/core/engine/game_mutations.dart @@ -1,3 +1,4 @@ +import 'package:askiineverdie/src/core/engine/item_service.dart'; import 'package:askiineverdie/src/core/model/equipment_slot.dart'; import 'package:askiineverdie/src/core/model/game_state.dart'; import 'package:askiineverdie/src/core/model/pq_config.dart'; @@ -10,12 +11,24 @@ class GameMutations { final PqConfig config; /// 장비 획득 (원본 Main.pas:791-830 WinEquip) + /// /// [slotIndex]: 0-10 (원본 Equips.Items.Count = 11) + /// ItemService를 사용하여 스탯과 공속을 가진 장비 생성 GameState winEquipByIndex(GameState state, int level, int slotIndex) { final rng = state.rng; final name = pq_logic.winEquip(config, rng, level, slotIndex); + final slot = EquipmentSlot.values[slotIndex]; + + // ItemService로 스탯이 있는 장비 생성 + final itemService = ItemService(rng: rng); + final newItem = itemService.generateEquipment( + name: name, + slot: slot, + level: level, + ); + final updatedEquip = state.equipment - .setByIndex(slotIndex, name) + .setItemByIndex(slotIndex, newItem) .copyWith(bestIndex: slotIndex); return state.copyWith(rng: rng, equipment: updatedEquip); } diff --git a/lib/src/core/engine/item_service.dart b/lib/src/core/engine/item_service.dart index e131348..769ee90 100644 --- a/lib/src/core/engine/item_service.dart +++ b/lib/src/core/engine/item_service.dart @@ -97,14 +97,35 @@ class ItemService { } /// 무기 스탯 생성 + /// + /// 공속(attackSpeed)과 공격력(atk)은 역비례 관계: + /// - 느린 무기 (1500ms): atk × 1.4 + /// - 기본 무기 (1000ms): atk × 1.0 + /// - 빠른 무기 (600ms): atk × 0.7 ItemStats _generateWeaponStats(int baseValue, ItemRarity rarity) { - final criBonus = rarity.index >= ItemRarity.rare.index ? 0.02 + rarity.index * 0.01 : 0.0; - final parryBonus = rarity.index >= ItemRarity.uncommon.index ? 0.01 + rarity.index * 0.005 : 0.0; + final criBonus = rarity.index >= ItemRarity.rare.index + ? 0.02 + rarity.index * 0.01 + : 0.0; + final parryBonus = rarity.index >= ItemRarity.uncommon.index + ? 0.01 + rarity.index * 0.005 + : 0.0; + + // 공속 결정 (600ms ~ 1500ms 범위) + // 희귀도가 높을수록 공속 변동 폭 증가 + final speedVariance = 300 + rarity.index * 100; // Common: 300, Legendary: 700 + final speedOffset = rng.nextInt(speedVariance * 2) - speedVariance; + final attackSpeed = (1000 + speedOffset).clamp(600, 1500); + + // 공속-데미지 역비례 계산 + // 기준: 1000ms = 1.0x, 600ms = 0.7x, 1500ms = 1.4x + final speedMultiplier = 0.3 + (attackSpeed / 1000) * 0.7; + final adjustedAtk = (baseValue * speedMultiplier).round(); return ItemStats( - atk: baseValue, + atk: adjustedAtk, criRate: criBonus, parryRate: parryBonus, + attackSpeed: attackSpeed, ); } @@ -201,13 +222,67 @@ class ItemService { ); } + // ============================================================================ + // 장비 점수 계산 + // ============================================================================ + + /// 장비 점수 계산 (Equipment Score) + /// + /// 슬롯별 스탯 가중치를 적용하여 점수 산출. + /// - 무기: 공격력, 크리티컬, 공속 역비례 반영 + /// - 방어구: 방어력, HP 보너스 + /// - 액세서리: 스탯 보너스 + static int calculateEquipmentScore(EquipmentItem item) { + var score = 0; + final stats = item.stats; + + // 공격 스탯 (무기 중심) + score += stats.atk * 2; + score += stats.magAtk * 2; + score += (stats.criRate * 200).round(); + score += (stats.parryRate * 150).round(); + + // 방어 스탯 + score += (stats.def * 1.5).round(); + score += (stats.magDef * 1.5).round(); + score += (stats.blockRate * 150).round(); + score += (stats.evasion * 150).round(); + + // 자원 스탯 + score += stats.hpBonus; + score += stats.mpBonus; + + // 능력치 보너스 (가중치 5배) + score += (stats.strBonus + + stats.conBonus + + stats.dexBonus + + stats.intBonus + + stats.wisBonus + + stats.chaBonus) * + 5; + + // 무기 공속 보정 (느린 무기 = 높은 데미지 → 높은 점수) + // 기준 1000ms, 느린 무기(1500ms)는 +25점, 빠른 무기(600ms)는 -20점 + if (item.slot == EquipmentSlot.weapon && stats.attackSpeed > 0) { + score += ((stats.attackSpeed - 1000) * 0.05).round(); + } + + // 희귀도 배율 적용 + score = (score * item.rarity.multiplier).round(); + + // 레벨 기본 점수 + score += item.level * 5; + + return score; + } + // ============================================================================ // 자동 장착 // ============================================================================ - /// 새 아이템이 현재 장비보다 좋은지 비교 + /// 새 아이템이 현재 장비보다 좋은지 비교 (점수 기반) /// - /// 가중치 기준으로 비교하며, 무게 제한도 고려 + /// 장비 점수 기준으로 비교하며, 무게 제한도 고려 bool shouldEquip({ required EquipmentItem newItem, required EquipmentItem currentItem, @@ -224,8 +299,11 @@ class ItemService { ); } - // 새 아이템이 더 좋은지 확인 - if (newItem.itemWeight <= currentItem.itemWeight) { + // 점수 비교 (새 아이템이 더 높아야 함) + final newScore = calculateEquipmentScore(newItem); + final currentScore = calculateEquipmentScore(currentItem); + + if (newScore <= currentScore) { return false; } diff --git a/lib/src/core/engine/potion_service.dart b/lib/src/core/engine/potion_service.dart new file mode 100644 index 0000000..a9a66d0 --- /dev/null +++ b/lib/src/core/engine/potion_service.dart @@ -0,0 +1,497 @@ +import 'package:askiineverdie/data/potion_data.dart'; +import 'package:askiineverdie/src/core/model/potion.dart'; + +/// 물약 서비스 +/// +/// 물약 사용, 자동 사용 알고리즘, 인벤토리 관리 +class PotionService { + const PotionService(); + + /// 긴급 물약 사용 HP 임계치 (30%) + static const double emergencyHpThreshold = 0.30; + + /// 긴급 물약 사용 MP 임계치 (20%) + static const double emergencyMpThreshold = 0.20; + + // ============================================================================ + // 물약 사용 가능 여부 + // ============================================================================ + + /// 물약 사용 가능 여부 체크 + /// + /// [potionId] 물약 ID + /// [inventory] 물약 인벤토리 + /// Returns: (사용 가능 여부, 실패 사유) + (bool, PotionUseFailReason?) canUsePotion( + String potionId, + PotionInventory inventory, + ) { + // 물약 데이터 존재 체크 + final potion = PotionData.getById(potionId); + if (potion == null) { + return (false, PotionUseFailReason.potionNotFound); + } + + // 보유 수량 체크 + if (!inventory.hasPotion(potionId)) { + return (false, PotionUseFailReason.outOfStock); + } + + // 전투당 종류별 1회 제한 체크 + if (!inventory.canUseType(potion.type)) { + return (false, PotionUseFailReason.alreadyUsedThisBattle); + } + + return (true, null); + } + + // ============================================================================ + // 물약 사용 + // ============================================================================ + + /// 물약 사용 + /// + /// [potionId] 물약 ID + /// [inventory] 물약 인벤토리 + /// [currentHp] 현재 HP + /// [maxHp] 최대 HP + /// [currentMp] 현재 MP + /// [maxMp] 최대 MP + PotionUseResult usePotion({ + required String potionId, + required PotionInventory inventory, + required int currentHp, + required int maxHp, + required int currentMp, + required int maxMp, + }) { + final (canUse, failReason) = canUsePotion(potionId, inventory); + if (!canUse) { + return PotionUseResult.failed(failReason!); + } + + final potion = PotionData.getById(potionId)!; + int healedAmount = 0; + int newHp = currentHp; + int newMp = currentMp; + + if (potion.isHpPotion) { + healedAmount = potion.calculateHeal(maxHp); + newHp = (currentHp + healedAmount).clamp(0, maxHp); + healedAmount = newHp - currentHp; // 실제 회복량 + } else if (potion.isMpPotion) { + healedAmount = potion.calculateHeal(maxMp); + newMp = (currentMp + healedAmount).clamp(0, maxMp); + healedAmount = newMp - currentMp; // 실제 회복량 + } + + final newInventory = inventory.usePotion(potionId, potion.type); + + return PotionUseResult( + success: true, + potion: potion, + healedAmount: healedAmount, + newHp: newHp, + newMp: newMp, + newInventory: newInventory, + ); + } + + // ============================================================================ + // 긴급 물약 자동 사용 + // ============================================================================ + + /// 긴급 HP 물약 선택 + /// + /// HP가 임계치 이하일 때 사용할 최적의 물약 선택 + /// [currentHp] 현재 HP + /// [maxHp] 최대 HP + /// [inventory] 물약 인벤토리 + /// [playerLevel] 플레이어 레벨 (적정 티어 판단용) + Potion? selectEmergencyHpPotion({ + required int currentHp, + required int maxHp, + required PotionInventory inventory, + required int playerLevel, + }) { + // 임계치 체크 + final hpRatio = currentHp / maxHp; + if (hpRatio > emergencyHpThreshold) return null; + + // 전투 중 이미 HP 물약 사용했으면 불가 + if (!inventory.canUseType(PotionType.hp)) return null; + + // 적정 티어 계산 + final targetTier = PotionData.tierForLevel(playerLevel); + + // 적정 티어부터 낮은 티어 순으로 검색 + for (var tier = targetTier; tier >= 1; tier--) { + final potion = PotionData.getHpPotionByTier(tier); + if (potion != null && inventory.hasPotion(potion.id)) { + return potion; + } + } + + // 적정 티어 이상도 검색 + for (var tier = targetTier + 1; tier <= 5; tier++) { + final potion = PotionData.getHpPotionByTier(tier); + if (potion != null && inventory.hasPotion(potion.id)) { + return potion; + } + } + + return null; + } + + /// 긴급 MP 물약 선택 + /// + /// MP가 임계치 이하일 때 사용할 최적의 물약 선택 + Potion? selectEmergencyMpPotion({ + required int currentMp, + required int maxMp, + required PotionInventory inventory, + required int playerLevel, + }) { + // 임계치 체크 + final mpRatio = currentMp / maxMp; + if (mpRatio > emergencyMpThreshold) return null; + + // 전투 중 이미 MP 물약 사용했으면 불가 + if (!inventory.canUseType(PotionType.mp)) return null; + + // 적정 티어 계산 + final targetTier = PotionData.tierForLevel(playerLevel); + + // 적정 티어부터 낮은 티어 순으로 검색 + for (var tier = targetTier; tier >= 1; tier--) { + final potion = PotionData.getMpPotionByTier(tier); + if (potion != null && inventory.hasPotion(potion.id)) { + return potion; + } + } + + // 적정 티어 이상도 검색 + for (var tier = targetTier + 1; tier <= 5; tier++) { + final potion = PotionData.getMpPotionByTier(tier); + if (potion != null && inventory.hasPotion(potion.id)) { + return potion; + } + } + + return null; + } + + // ============================================================================ + // 인벤토리 관리 + // ============================================================================ + + /// 전투 종료 시 사용 기록 초기화 + PotionInventory resetBattleUsage(PotionInventory inventory) { + return inventory.resetBattleUsage(); + } + + /// 물약 드랍 추가 + PotionInventory addPotionDrop( + PotionInventory inventory, + String potionId, [ + int count = 1, + ]) { + return inventory.addPotion(potionId, count); + } + + // ============================================================================ + // 물약 구매 시스템 + // ============================================================================ + + /// 물약 구매 가능 여부 체크 + /// + /// [potionId] 물약 ID + /// [gold] 보유 골드 + /// Returns: (구매 가능 여부, 실패 사유) + (bool, PotionPurchaseFailReason?) canPurchasePotion( + String potionId, + int gold, + ) { + final potion = PotionData.getById(potionId); + if (potion == null) { + return (false, PotionPurchaseFailReason.potionNotFound); + } + + if (gold < potion.price) { + return (false, PotionPurchaseFailReason.insufficientGold); + } + + return (true, null); + } + + /// 물약 구매 + /// + /// [potionId] 물약 ID + /// [inventory] 현재 물약 인벤토리 + /// [gold] 보유 골드 + /// [count] 구매 수량 (기본 1) + PotionPurchaseResult purchasePotion({ + required String potionId, + required PotionInventory inventory, + required int gold, + int count = 1, + }) { + final potion = PotionData.getById(potionId); + if (potion == null) { + return PotionPurchaseResult.failed(PotionPurchaseFailReason.potionNotFound); + } + + final totalCost = potion.price * count; + if (gold < totalCost) { + return PotionPurchaseResult.failed(PotionPurchaseFailReason.insufficientGold); + } + + final newInventory = inventory.addPotion(potionId, count); + final newGold = gold - totalCost; + + return PotionPurchaseResult( + success: true, + potion: potion, + quantity: count, + totalCost: totalCost, + newGold: newGold, + newInventory: newInventory, + ); + } + + /// 레벨에 맞는 물약 자동 구매 + /// + /// 골드의 일정 비율을 물약 구매에 사용 + /// [playerLevel] 플레이어 레벨 + /// [inventory] 현재 물약 인벤토리 + /// [gold] 보유 골드 + /// [spendRatio] 골드 사용 비율 (기본 20%) + PotionPurchaseResult autoPurchasePotions({ + required int playerLevel, + required PotionInventory inventory, + required int gold, + double spendRatio = 0.20, + }) { + final tier = PotionData.tierForLevel(playerLevel); + final hpPotion = PotionData.getHpPotionByTier(tier); + final mpPotion = PotionData.getMpPotionByTier(tier); + + if (hpPotion == null && mpPotion == null) { + return PotionPurchaseResult.failed(PotionPurchaseFailReason.potionNotFound); + } + + // 사용 가능 골드 + final spendableGold = (gold * spendRatio).floor(); + if (spendableGold <= 0) { + return PotionPurchaseResult.failed(PotionPurchaseFailReason.insufficientGold); + } + + var currentInventory = inventory; + var currentGold = gold; + var totalSpent = 0; + var hpPurchased = 0; + var mpPurchased = 0; + + // HP 물약 우선 구매 (60%), MP 물약 (40%) + final hpBudget = (spendableGold * 0.6).floor(); + final mpBudget = spendableGold - hpBudget; + + // HP 물약 구매 + if (hpPotion != null && hpBudget >= hpPotion.price) { + final count = hpBudget ~/ hpPotion.price; + final cost = count * hpPotion.price; + currentInventory = currentInventory.addPotion(hpPotion.id, count); + currentGold -= cost; + totalSpent += cost; + hpPurchased = count; + } + + // MP 물약 구매 + if (mpPotion != null && mpBudget >= mpPotion.price) { + final count = mpBudget ~/ mpPotion.price; + final cost = count * mpPotion.price; + currentInventory = currentInventory.addPotion(mpPotion.id, count); + currentGold -= cost; + totalSpent += cost; + mpPurchased = count; + } + + if (totalSpent == 0) { + return PotionPurchaseResult.failed(PotionPurchaseFailReason.insufficientGold); + } + + return PotionPurchaseResult( + success: true, + potion: hpPotion ?? mpPotion, + quantity: hpPurchased + mpPurchased, + totalCost: totalSpent, + newGold: currentGold, + newInventory: currentInventory, + ); + } + + // ============================================================================ + // 물약 드랍 시스템 + // ============================================================================ + + /// 기본 물약 드랍 확률 (15%) + static const double baseDropChance = 0.15; + + /// 레벨당 드랍 확률 증가 (0.5%씩) + static const double dropChancePerLevel = 0.005; + + /// 최대 드랍 확률 (35%) + static const double maxDropChance = 0.35; + + /// 물약 드랍 시도 + /// + /// 전투 승리 시 물약 드랍 여부 결정 및 물약 획득 + /// [playerLevel] 플레이어 레벨 (드랍 확률 및 티어 결정) + /// [inventory] 현재 물약 인벤토리 + /// [roll] 0~99 범위의 난수 (드랍 확률 판정) + /// [typeRoll] 0~99 범위의 난수 (HP/MP 결정) + /// Returns: (업데이트된 인벤토리, 드랍된 물약 또는 null) + (PotionInventory, Potion?) tryPotionDrop({ + required int playerLevel, + required PotionInventory inventory, + required int roll, + required int typeRoll, + }) { + // 드랍 확률 계산 + final dropChance = (baseDropChance + playerLevel * dropChancePerLevel) + .clamp(baseDropChance, maxDropChance); + final dropThreshold = (dropChance * 100).round(); + + // 드랍 실패 + if (roll >= dropThreshold) { + return (inventory, null); + } + + // 물약 타입 결정 (60% HP, 40% MP) + final isHpPotion = typeRoll < 60; + + // 레벨 기반 티어 결정 + final tier = PotionData.tierForLevel(playerLevel); + + // 물약 선택 + final Potion? potion; + if (isHpPotion) { + potion = PotionData.getHpPotionByTier(tier); + } else { + potion = PotionData.getMpPotionByTier(tier); + } + + if (potion == null) { + return (inventory, null); + } + + // 인벤토리에 추가 + final updatedInventory = inventory.addPotion(potion.id); + return (updatedInventory, potion); + } +} + +/// 물약 사용 결과 +class PotionUseResult { + const PotionUseResult({ + required this.success, + this.potion, + this.healedAmount = 0, + this.newHp = 0, + this.newMp = 0, + this.newInventory, + this.failReason, + }); + + /// 성공 여부 + final bool success; + + /// 사용한 물약 + final Potion? potion; + + /// 실제 회복량 + final int healedAmount; + + /// 사용 후 HP + final int newHp; + + /// 사용 후 MP + final int newMp; + + /// 업데이트된 인벤토리 + final PotionInventory? newInventory; + + /// 실패 사유 + final PotionUseFailReason? failReason; + + /// 실패 결과 생성 + factory PotionUseResult.failed(PotionUseFailReason reason) { + return PotionUseResult( + success: false, + failReason: reason, + ); + } +} + +/// 물약 사용 실패 사유 +enum PotionUseFailReason { + /// 물약 없음 (데이터 없음) + potionNotFound, + + /// 보유 물약 없음 (재고 부족) + outOfStock, + + /// 이번 전투에서 이미 해당 종류 물약 사용 + alreadyUsedThisBattle, +} + +/// 물약 구매 결과 +class PotionPurchaseResult { + const PotionPurchaseResult({ + required this.success, + this.potion, + this.quantity = 0, + this.totalCost = 0, + this.newGold = 0, + this.newInventory, + this.failReason, + }); + + /// 성공 여부 + final bool success; + + /// 구매한 물약 + final Potion? potion; + + /// 구매 수량 + final int quantity; + + /// 총 비용 + final int totalCost; + + /// 구매 후 골드 + final int newGold; + + /// 업데이트된 인벤토리 + final PotionInventory? newInventory; + + /// 실패 사유 + final PotionPurchaseFailReason? failReason; + + /// 실패 결과 생성 + factory PotionPurchaseResult.failed(PotionPurchaseFailReason reason) { + return PotionPurchaseResult( + success: false, + failReason: reason, + ); + } +} + +/// 물약 구매 실패 사유 +enum PotionPurchaseFailReason { + /// 물약 없음 (데이터 없음) + potionNotFound, + + /// 골드 부족 + insufficientGold, +} diff --git a/lib/src/core/engine/progress_service.dart b/lib/src/core/engine/progress_service.dart index 9cb15a7..98995ce 100644 --- a/lib/src/core/engine/progress_service.dart +++ b/lib/src/core/engine/progress_service.dart @@ -4,6 +4,7 @@ import 'package:askiineverdie/data/game_text_l10n.dart' as l10n; import 'package:askiineverdie/data/skill_data.dart'; import 'package:askiineverdie/src/core/engine/combat_calculator.dart'; import 'package:askiineverdie/src/core/engine/game_mutations.dart'; +import 'package:askiineverdie/src/core/engine/potion_service.dart'; import 'package:askiineverdie/src/core/engine/reward_service.dart'; import 'package:askiineverdie/src/core/engine/skill_service.dart'; import 'package:askiineverdie/src/core/model/combat_event.dart'; @@ -13,7 +14,9 @@ import 'package:askiineverdie/src/core/model/equipment_item.dart'; import 'package:askiineverdie/src/core/model/equipment_slot.dart'; 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/potion.dart'; import 'package:askiineverdie/src/core/model/pq_config.dart'; +import 'package:askiineverdie/src/core/model/skill.dart'; import 'package:askiineverdie/src/core/util/pq_logic.dart' as pq_logic; class ProgressTickResult { @@ -186,9 +189,10 @@ class ProgressService { ? progress.task.max : uncapped; - // 킬 태스크 중 전투 진행 (스킬 자동 사용 포함) + // 킬 태스크 중 전투 진행 (스킬 자동 사용, DOT, 물약 포함) var updatedCombat = progress.currentCombat; var updatedSkillSystem = nextState.skillSystem; + var updatedPotionInventory = nextState.potionInventory; if (progress.currentTask.type == TaskType.kill && updatedCombat != null && updatedCombat.isActive) { final combatResult = _processCombatTickWithSkills( nextState, @@ -198,6 +202,9 @@ class ProgressService { ); updatedCombat = combatResult.combat; updatedSkillSystem = combatResult.skillSystem; + if (combatResult.potionInventory != null) { + updatedPotionInventory = combatResult.potionInventory!; + } // Phase 4: 플레이어 사망 체크 if (!updatedCombat.playerStats.isAlive) { @@ -216,7 +223,11 @@ class ProgressService { currentCombat: updatedCombat, ); nextState = _recalculateEncumbrance( - nextState.copyWith(progress: progress, skillSystem: updatedSkillSystem), + nextState.copyWith( + progress: progress, + skillSystem: updatedSkillSystem, + potionInventory: updatedPotionInventory, + ), ); return ProgressTickResult(state: nextState); } @@ -245,11 +256,33 @@ class ProgressService { } // 전리품 획득 (원본 Main.pas:625-630) - nextState = _winLoot(nextState); + final lootResult = _winLoot(nextState); + nextState = lootResult.state; - // 전투 상태 초기화 - progress = nextState.progress.copyWith(currentCombat: null); - nextState = nextState.copyWith(progress: progress); + // 물약 드랍 시 전투 로그에 이벤트 추가 + var combatForReset = progress.currentCombat; + if (lootResult.droppedPotion != null && combatForReset != null) { + final potionDropEvent = CombatEvent.potionDrop( + timestamp: nextState.skillSystem.elapsedMs, + potionName: lootResult.droppedPotion!.name, + isHp: lootResult.droppedPotion!.isHpPotion, + ); + final updatedEvents = [...combatForReset.recentEvents, potionDropEvent]; + combatForReset = combatForReset.copyWith( + recentEvents: updatedEvents.length > 10 + ? updatedEvents.sublist(updatedEvents.length - 10) + : updatedEvents, + ); + progress = progress.copyWith(currentCombat: combatForReset); + } + + // 전투 상태 초기화 및 물약 사용 기록 초기화 + progress = progress.copyWith(currentCombat: null); + final resetPotionInventory = nextState.potionInventory.resetBattleUsage(); + nextState = nextState.copyWith( + progress: progress, + potionInventory: resetPotionInventory, + ); } // 시장/판매/구매 태스크 완료 시 처리 (원본 Main.pas:631-649) @@ -728,39 +761,60 @@ class ProgressService { } /// 킬 태스크 완료 시 전리품 획득 (원본 Main.pas:625-630) - GameState _winLoot(GameState state) { + /// 전리품 획득 결과 + /// + /// [state] 업데이트된 게임 상태 + /// [droppedPotion] 드랍된 물약 (없으면 null) + ({GameState state, Potion? droppedPotion}) _winLoot(GameState state) { final taskInfo = state.progress.currentTask; final monsterPart = taskInfo.monsterPart ?? ''; final monsterBaseName = taskInfo.monsterBaseName ?? ''; + var resultState = state; + // 부위가 '*'이면 WinItem 호출 (특수 아이템) if (monsterPart == '*') { - return mutations.winItem(state); - } + resultState = mutations.winItem(resultState); + } else if (monsterPart.isNotEmpty && monsterBaseName.isNotEmpty) { + // 원본: Add(Inventory, LowerCase(Split(fTask.Caption,1) + ' ' + + // ProperCase(Split(fTask.Caption,3))), 1); + // 예: "goblin Claw" 형태로 인벤토리 추가 + final itemName = + '${monsterBaseName.toLowerCase()} ${_properCase(monsterPart)}'; - // 부위가 비어있으면 전리품 없음 - if (monsterPart.isEmpty || monsterBaseName.isEmpty) { - return state; - } + // 인벤토리에 추가 + final items = [...resultState.inventory.items]; + final existing = items.indexWhere((e) => e.name == itemName); + if (existing >= 0) { + items[existing] = items[existing].copyWith( + count: items[existing].count + 1, + ); + } else { + items.add(InventoryEntry(name: itemName, count: 1)); + } - // 원본: Add(Inventory, LowerCase(Split(fTask.Caption,1) + ' ' + - // ProperCase(Split(fTask.Caption,3))), 1); - // 예: "goblin Claw" 형태로 인벤토리 추가 - final itemName = - '${monsterBaseName.toLowerCase()} ${_properCase(monsterPart)}'; - - // 인벤토리에 추가 - final items = [...state.inventory.items]; - final existing = items.indexWhere((e) => e.name == itemName); - if (existing >= 0) { - items[existing] = items[existing].copyWith( - count: items[existing].count + 1, + resultState = resultState.copyWith( + inventory: resultState.inventory.copyWith(items: items), ); - } else { - items.add(InventoryEntry(name: itemName, count: 1)); } - return state.copyWith(inventory: state.inventory.copyWith(items: items)); + // 물약 드랍 시도 + final potionService = const PotionService(); + final rng = resultState.rng; + final (updatedPotionInventory, droppedPotion) = potionService.tryPotionDrop( + playerLevel: resultState.traits.level, + inventory: resultState.potionInventory, + roll: rng.nextInt(100), + typeRoll: rng.nextInt(100), + ); + + return ( + state: resultState.copyWith( + rng: rng, + potionInventory: updatedPotionInventory, + ), + droppedPotion: droppedPotion, + ); } /// 첫 글자만 대문자로 변환 (원본 ProperCase) @@ -796,6 +850,22 @@ class ProgressService { final slotIndex = nextState.rng.nextInt(Equipment.slotCount); nextState = mutations.winEquipByIndex(nextState, level, slotIndex); + // 물약 자동 구매 (남은 골드의 20% 사용) + final potionService = const PotionService(); + final purchaseResult = potionService.autoPurchasePotions( + playerLevel: level, + inventory: nextState.potionInventory, + gold: nextState.inventory.gold, + spendRatio: 0.20, + ); + + if (purchaseResult.success && purchaseResult.newInventory != null) { + nextState = nextState.copyWith( + inventory: nextState.inventory.copyWith(gold: purchaseResult.newGold), + potionInventory: purchaseResult.newInventory, + ); + } + return nextState; } @@ -864,25 +934,30 @@ class ProgressService { ); } - /// 전투 틱 처리 (스킬 자동 사용 포함) + /// 전투 틱 처리 (스킬 자동 사용, DOT, 물약 포함) /// /// [state] 현재 게임 상태 /// [combat] 현재 전투 상태 /// [skillSystem] 스킬 시스템 상태 /// [elapsedMs] 경과 시간 (밀리초) - /// Returns: 업데이트된 전투 상태 및 스킬 시스템 상태 - ({CombatState combat, SkillSystemState skillSystem}) _processCombatTickWithSkills( + /// Returns: 업데이트된 전투 상태, 스킬 시스템 상태, 물약 인벤토리 + ({ + CombatState combat, + SkillSystemState skillSystem, + PotionInventory? potionInventory, + }) _processCombatTickWithSkills( GameState state, CombatState combat, SkillSystemState skillSystem, int elapsedMs, ) { if (!combat.isActive || combat.isCombatOver) { - return (combat: combat, skillSystem: skillSystem); + return (combat: combat, skillSystem: skillSystem, potionInventory: null); } final calculator = CombatCalculator(rng: state.rng); final skillService = SkillService(rng: state.rng); + final potionService = const PotionService(); var playerStats = combat.playerStats; var monsterStats = combat.monsterStats; var playerAccumulator = combat.playerAttackAccumulatorMs + elapsedMs; @@ -891,11 +966,90 @@ class ProgressService { var totalDamageTaken = combat.totalDamageTaken; var turnsElapsed = combat.turnsElapsed; var updatedSkillSystem = skillSystem; + var activeDoTs = [...combat.activeDoTs]; + var usedPotionTypes = {...combat.usedPotionTypes}; + PotionInventory? updatedPotionInventory; // 새 전투 이벤트 수집 final newEvents = []; final timestamp = updatedSkillSystem.elapsedMs; + // ========================================================================= + // DOT 틱 처리 + // ========================================================================= + var dotDamageThisTick = 0; + final updatedDoTs = []; + + for (final dot in activeDoTs) { + final (updatedDot, ticksTriggered) = dot.tick(elapsedMs); + + if (ticksTriggered > 0) { + final damage = dot.damagePerTick * ticksTriggered; + dotDamageThisTick += damage; + + // DOT 데미지 이벤트 생성 + newEvents.add(CombatEvent.dotTick( + timestamp: timestamp, + skillName: dot.skillId, + damage: damage, + targetName: monsterStats.name, + )); + } + + // 만료되지 않은 DOT만 유지 + if (updatedDot.isActive) { + updatedDoTs.add(updatedDot); + } + } + + // DOT 데미지 적용 + if (dotDamageThisTick > 0 && monsterStats.isAlive) { + final newMonsterHp = (monsterStats.hpCurrent - dotDamageThisTick) + .clamp(0, monsterStats.hpMax); + monsterStats = monsterStats.copyWith(hpCurrent: newMonsterHp); + totalDamageDealt += dotDamageThisTick; + } + + activeDoTs = updatedDoTs; + + // ========================================================================= + // 긴급 물약 자동 사용 (HP < 30%) + // ========================================================================= + final hpRatio = playerStats.hpCurrent / playerStats.hpMax; + if (hpRatio <= PotionService.emergencyHpThreshold) { + final emergencyPotion = potionService.selectEmergencyHpPotion( + currentHp: playerStats.hpCurrent, + maxHp: playerStats.hpMax, + inventory: state.potionInventory, + playerLevel: state.traits.level, + ); + + if (emergencyPotion != null && + !usedPotionTypes.contains(PotionType.hp)) { + final result = potionService.usePotion( + potionId: emergencyPotion.id, + inventory: state.potionInventory, + currentHp: playerStats.hpCurrent, + maxHp: playerStats.hpMax, + currentMp: playerStats.mpCurrent, + maxMp: playerStats.mpMax, + ); + + if (result.success) { + playerStats = playerStats.copyWith(hpCurrent: result.newHp); + usedPotionTypes = {...usedPotionTypes, PotionType.hp}; + updatedPotionInventory = result.newInventory; + + newEvents.add(CombatEvent.playerPotion( + timestamp: timestamp, + potionName: emergencyPotion.name, + healAmount: result.healedAmount, + isHp: true, + )); + } + } + } + // 플레이어 공격 체크 if (playerAccumulator >= playerStats.attackDelayMs) { // 스킬 자동 선택 @@ -912,6 +1066,7 @@ class ProgressService { monster: monsterStats, skillSystem: updatedSkillSystem, availableSkillIds: availableSkillIds, + activeDoTs: activeDoTs, ); if (selectedSkill != null && selectedSkill.isAttack) { @@ -934,6 +1089,30 @@ class ProgressService { damage: skillResult.result.damage, targetName: monsterStats.name, )); + } else if (selectedSkill != null && selectedSkill.isDot) { + // DOT 스킬 사용 + final skillResult = skillService.useDotSkill( + skill: selectedSkill, + player: playerStats, + skillSystem: updatedSkillSystem, + playerInt: state.stats.intelligence, + playerWis: state.stats.wis, + ); + playerStats = skillResult.updatedPlayer; + updatedSkillSystem = skillResult.updatedSkillSystem; + + // DOT 효과 추가 + if (skillResult.dotEffect != null) { + activeDoTs.add(skillResult.dotEffect!); + } + + // DOT 스킬 사용 이벤트 생성 + 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( @@ -1053,8 +1232,11 @@ class ProgressService { turnsElapsed: turnsElapsed, isActive: isActive, recentEvents: recentEvents, + activeDoTs: activeDoTs, + usedPotionTypes: usedPotionTypes, ), skillSystem: updatedSkillSystem, + potionInventory: updatedPotionInventory, ); } diff --git a/lib/src/core/engine/skill_service.dart b/lib/src/core/engine/skill_service.dart index 90478da..de54ba3 100644 --- a/lib/src/core/engine/skill_service.dart +++ b/lib/src/core/engine/skill_service.dart @@ -181,6 +181,60 @@ class SkillService { ); } + /// DOT 스킬 사용 + /// + /// DOT 효과를 생성하여 반환. 호출자가 전투 상태의 activeDoTs에 추가해야 함. + /// INT → 틱당 데미지 보정, WIS → 틱 간격 보정 + ({ + SkillUseResult result, + CombatStats updatedPlayer, + SkillSystemState updatedSkillSystem, + DotEffect? dotEffect, + }) useDotSkill({ + required Skill skill, + required CombatStats player, + required SkillSystemState skillSystem, + required int playerInt, + required int playerWis, + }) { + if (!skill.isDot) { + return ( + result: SkillUseResult.failed(skill, SkillFailReason.invalidState), + updatedPlayer: player, + updatedSkillSystem: skillSystem, + dotEffect: null, + ); + } + + // DOT 효과 생성 (INT/WIS 보정 적용) + final dotEffect = DotEffect.fromSkill( + skill, + playerInt: playerInt, + playerWis: playerWis, + ); + + // MP 소모 + var updatedPlayer = player.withMp(player.mpCurrent - skill.mpCost); + + // 스킬 상태 업데이트 (쿨타임 시작) + final updatedSkillSystem = _updateSkillCooldown(skillSystem, skill.id); + + // 예상 총 데미지 계산 (틱 수 × 틱당 데미지) + final expectedTicks = dotEffect.totalDurationMs ~/ dotEffect.tickIntervalMs; + final expectedDamage = expectedTicks * dotEffect.damagePerTick; + + return ( + result: SkillUseResult( + skill: skill, + success: true, + damage: expectedDamage, + ), + updatedPlayer: updatedPlayer, + updatedSkillSystem: updatedSkillSystem, + dotEffect: dotEffect, + ); + } + // ============================================================================ // 자동 스킬 선택 // ============================================================================ @@ -189,14 +243,16 @@ class SkillService { /// /// 우선순위: /// 1. HP < 30% → 회복 스킬 - /// 2. 보스전 (레벨 차이 10 이상) → 가장 강력한 공격 스킬 - /// 3. 일반 전투 → MP 효율이 좋은 스킬 - /// 4. MP < 20% → null (일반 공격) + /// 2. 몬스터 HP > 50% & DOT 없음 → DOT 스킬 (장기전 유리) + /// 3. 보스전 (레벨 차이 10 이상) → 가장 강력한 공격 스킬 + /// 4. 일반 전투 → MP 효율이 좋은 스킬 + /// 5. MP < 20% → null (일반 공격) Skill? selectAutoSkill({ required CombatStats player, required MonsterCombatStats monster, required SkillSystemState skillSystem, required List availableSkillIds, + List activeDoTs = const [], }) { final currentMp = player.mpCurrent; final mpRatio = player.mpRatio; @@ -225,6 +281,12 @@ class SkillService { if (healSkill != null) return healSkill; } + // 몬스터 HP > 50% & 활성 DOT 없음 → DOT 스킬 사용 + if (monster.hpRatio > 0.5 && activeDoTs.isEmpty) { + final dotSkill = _findBestDotSkill(availableSkills, currentMp); + if (dotSkill != null) return dotSkill; + } + // 보스전 판단 (몬스터 레벨이 높음) final isBossFight = monster.level >= 10 && monster.hpRatio > 0.5; @@ -237,6 +299,28 @@ class SkillService { return _findEfficientAttackSkill(availableSkills); } + /// 가장 좋은 DOT 스킬 찾기 + /// + /// 예상 총 데미지 (틱 × 데미지) 기준으로 선택 + Skill? _findBestDotSkill(List skills, int currentMp) { + final dotSkills = skills + .where((s) => s.isDot && s.mpCost <= currentMp) + .toList(); + + if (dotSkills.isEmpty) return null; + + // 예상 총 데미지 기준 정렬 + dotSkills.sort((a, b) { + final aTotal = (a.baseDotDamage ?? 0) * + ((a.baseDotDurationMs ?? 0) ~/ (a.baseDotTickMs ?? 1000)); + final bTotal = (b.baseDotDamage ?? 0) * + ((b.baseDotDurationMs ?? 0) ~/ (b.baseDotTickMs ?? 1000)); + return bTotal.compareTo(aTotal); + }); + + return dotSkills.first; + } + /// 가장 좋은 회복 스킬 찾기 Skill? _findBestHealSkill(List skills, int currentMp) { final healSkills = skills diff --git a/lib/src/core/model/combat_event.dart b/lib/src/core/model/combat_event.dart index a1eecb2..d36ffd8 100644 --- a/lib/src/core/model/combat_event.dart +++ b/lib/src/core/model/combat_event.dart @@ -26,6 +26,15 @@ enum CombatEventType { /// 플레이어 버프 playerBuff, + + /// DOT 틱 데미지 + dotTick, + + /// 물약 사용 + playerPotion, + + /// 물약 드랍 + potionDrop, } /// 전투 이벤트 (Combat Event) @@ -188,4 +197,51 @@ class CombatEvent { skillName: skillName, ); } + + /// DOT 틱 이벤트 생성 + factory CombatEvent.dotTick({ + required int timestamp, + required String skillName, + required int damage, + required String targetName, + }) { + return CombatEvent( + type: CombatEventType.dotTick, + timestamp: timestamp, + skillName: skillName, + damage: damage, + targetName: targetName, + ); + } + + /// 물약 사용 이벤트 생성 + factory CombatEvent.playerPotion({ + required int timestamp, + required String potionName, + required int healAmount, + required bool isHp, + }) { + return CombatEvent( + type: CombatEventType.playerPotion, + timestamp: timestamp, + skillName: potionName, + healAmount: healAmount, + // isHp를 구분하기 위해 targetName 사용 (HP/MP) + targetName: isHp ? 'HP' : 'MP', + ); + } + + /// 물약 드랍 이벤트 생성 + factory CombatEvent.potionDrop({ + required int timestamp, + required String potionName, + required bool isHp, + }) { + return CombatEvent( + type: CombatEventType.potionDrop, + timestamp: timestamp, + skillName: potionName, + targetName: isHp ? 'HP' : 'MP', + ); + } } diff --git a/lib/src/core/model/combat_state.dart b/lib/src/core/model/combat_state.dart index eae7e17..4a7b470 100644 --- a/lib/src/core/model/combat_state.dart +++ b/lib/src/core/model/combat_state.dart @@ -1,6 +1,8 @@ import 'package:askiineverdie/src/core/model/combat_event.dart'; import 'package:askiineverdie/src/core/model/combat_stats.dart'; import 'package:askiineverdie/src/core/model/monster_combat_stats.dart'; +import 'package:askiineverdie/src/core/model/potion.dart'; +import 'package:askiineverdie/src/core/model/skill.dart'; /// 현재 전투 상태 /// @@ -17,6 +19,8 @@ class CombatState { required this.turnsElapsed, required this.isActive, this.recentEvents = const [], + this.activeDoTs = const [], + this.usedPotionTypes = const {}, }); /// 플레이어 전투 스탯 @@ -46,6 +50,12 @@ class CombatState { /// 최근 전투 이벤트 목록 (최대 10개) final List recentEvents; + /// 활성 DOT 효과 목록 + final List activeDoTs; + + /// 이번 전투에서 사용한 물약 종류 (종류별 1회 제한) + final Set usedPotionTypes; + // ============================================================================ // 유틸리티 // ============================================================================ @@ -65,6 +75,19 @@ class CombatState { /// 몬스터 HP 비율 double get monsterHpRatio => monsterStats.hpRatio; + /// 특정 종류 물약 사용 가능 여부 + bool canUsePotionType(PotionType type) => !usedPotionTypes.contains(type); + + /// 활성 DOT 존재 여부 + bool get hasActiveDoTs => activeDoTs.isNotEmpty; + + /// DOT 총 예상 데미지 + int get totalDotDamageRemaining { + return activeDoTs.fold(0, (sum, dot) { + return sum + (dot.damagePerTick * dot.remainingTicks); + }); + } + CombatState copyWith({ CombatStats? playerStats, MonsterCombatStats? monsterStats, @@ -75,6 +98,8 @@ class CombatState { int? turnsElapsed, bool? isActive, List? recentEvents, + List? activeDoTs, + Set? usedPotionTypes, }) { return CombatState( playerStats: playerStats ?? this.playerStats, @@ -88,6 +113,8 @@ class CombatState { turnsElapsed: turnsElapsed ?? this.turnsElapsed, isActive: isActive ?? this.isActive, recentEvents: recentEvents ?? this.recentEvents, + activeDoTs: activeDoTs ?? this.activeDoTs, + usedPotionTypes: usedPotionTypes ?? this.usedPotionTypes, ); } diff --git a/lib/src/core/model/combat_stats.dart b/lib/src/core/model/combat_stats.dart index 069852c..edb258b 100644 --- a/lib/src/core/model/combat_stats.dart +++ b/lib/src/core/model/combat_stats.dart @@ -270,9 +270,13 @@ class CombatStats { final baseParryRate = (effectiveDex + effectiveStr) * 0.002; final parryRate = (baseParryRate + equipStats.parryRate).clamp(0.0, 0.4); - // 공격 속도: DEX 기반 (기본 1000ms, 최소 357ms) + // 공격 속도: 무기 기본 공속 + DEX 보정 + // 무기 attackSpeed가 0이면 기본값 1000ms 사용 + final weaponItem = equipment.items[0]; // 무기 슬롯 + final weaponSpeed = weaponItem.stats.attackSpeed; + final baseAttackSpeed = weaponSpeed > 0 ? weaponSpeed : 1000; final speedModifier = 1.0 + (effectiveDex - 10) * 0.02; - final attackDelayMs = (1000 / speedModifier).round().clamp(357, 1500); + final attackDelayMs = (baseAttackSpeed / speedModifier).round().clamp(300, 2000); // HP/MP: 기본 + 장비 보너스 var totalHpMax = stats.hpMax + equipStats.hpBonus; diff --git a/lib/src/core/model/game_state.dart b/lib/src/core/model/game_state.dart index 69b98c2..829436b 100644 --- a/lib/src/core/model/game_state.dart +++ b/lib/src/core/model/game_state.dart @@ -5,6 +5,7 @@ import 'package:askiineverdie/src/core/model/combat_state.dart'; import 'package:askiineverdie/src/core/model/equipment_item.dart'; import 'package:askiineverdie/src/core/model/equipment_slot.dart'; import 'package:askiineverdie/src/core/model/item_stats.dart'; +import 'package:askiineverdie/src/core/model/potion.dart'; import 'package:askiineverdie/src/core/model/skill.dart'; import 'package:askiineverdie/src/core/util/deterministic_random.dart'; @@ -23,6 +24,7 @@ class GameState { ProgressState? progress, QueueState? queue, SkillSystemState? skillSystem, + PotionInventory? potionInventory, this.deathInfo, }) : rng = DeterministicRandom.clone(rng), traits = traits ?? Traits.empty(), @@ -32,7 +34,8 @@ class GameState { spellBook = spellBook ?? SpellBook.empty(), progress = progress ?? ProgressState.empty(), queue = queue ?? QueueState.empty(), - skillSystem = skillSystem ?? SkillSystemState.empty(); + skillSystem = skillSystem ?? SkillSystemState.empty(), + potionInventory = potionInventory ?? const PotionInventory(); factory GameState.withSeed({ required int seed, @@ -44,6 +47,7 @@ class GameState { ProgressState? progress, QueueState? queue, SkillSystemState? skillSystem, + PotionInventory? potionInventory, DeathInfo? deathInfo, }) { return GameState( @@ -56,6 +60,7 @@ class GameState { progress: progress, queue: queue, skillSystem: skillSystem, + potionInventory: potionInventory, deathInfo: deathInfo, ); } @@ -72,6 +77,9 @@ class GameState { /// 스킬 시스템 상태 (Phase 3) final SkillSystemState skillSystem; + /// 물약 인벤토리 + final PotionInventory potionInventory; + /// 사망 정보 (Phase 4, null이면 생존 중) final DeathInfo? deathInfo; @@ -88,6 +96,7 @@ class GameState { ProgressState? progress, QueueState? queue, SkillSystemState? skillSystem, + PotionInventory? potionInventory, DeathInfo? deathInfo, bool clearDeathInfo = false, }) { @@ -101,6 +110,7 @@ class GameState { progress: progress ?? this.progress, queue: queue ?? this.queue, skillSystem: skillSystem ?? this.skillSystem, + potionInventory: potionInventory ?? this.potionInventory, deathInfo: clearDeathInfo ? null : (deathInfo ?? this.deathInfo), ); } diff --git a/lib/src/core/model/item_stats.dart b/lib/src/core/model/item_stats.dart index 06b7ca4..f31a6b1 100644 --- a/lib/src/core/model/item_stats.dart +++ b/lib/src/core/model/item_stats.dart @@ -47,6 +47,7 @@ class ItemStats { this.intBonus = 0, this.wisBonus = 0, this.chaBonus = 0, + this.attackSpeed = 0, }); /// 물리 공격력 보정 @@ -97,6 +98,12 @@ class ItemStats { /// CHA 보너스 final int chaBonus; + /// 무기 공격속도 (밀리초, 무기 전용) + /// + /// 0이면 기본값(1000ms) 사용, 값이 클수록 느린 공격. + /// 느린 무기는 높은 기본 데미지를 가짐. + final int attackSpeed; + /// 스탯 합계 (가중치 계산용) int get totalStatValue { return atk + @@ -121,6 +128,8 @@ class ItemStats { static const empty = ItemStats(); /// 두 스탯 합산 + /// + /// attackSpeed는 합산 대상 아님 (무기 슬롯 단일 값) ItemStats operator +(ItemStats other) { return ItemStats( atk: atk + other.atk, @@ -139,6 +148,7 @@ class ItemStats { intBonus: intBonus + other.intBonus, wisBonus: wisBonus + other.wisBonus, chaBonus: chaBonus + other.chaBonus, + // attackSpeed는 무기에서만 직접 참조 ); } @@ -159,6 +169,7 @@ class ItemStats { int? intBonus, int? wisBonus, int? chaBonus, + int? attackSpeed, }) { return ItemStats( atk: atk ?? this.atk, @@ -177,6 +188,7 @@ class ItemStats { intBonus: intBonus ?? this.intBonus, wisBonus: wisBonus ?? this.wisBonus, chaBonus: chaBonus ?? this.chaBonus, + attackSpeed: attackSpeed ?? this.attackSpeed, ); } } diff --git a/lib/src/core/model/potion.dart b/lib/src/core/model/potion.dart new file mode 100644 index 0000000..71ad85c --- /dev/null +++ b/lib/src/core/model/potion.dart @@ -0,0 +1,136 @@ +/// 물약 종류 +enum PotionType { + /// HP 회복 물약 + hp, + + /// MP 회복 물약 + mp, +} + +/// 물약 아이템 +/// +/// 전투 중 사용 가능한 소모품. +/// 전투당 종류별 1회만 사용 가능. +class Potion { + const Potion({ + required this.id, + required this.name, + required this.type, + required this.tier, + this.healAmount = 0, + this.healPercent = 0.0, + this.price = 0, + }); + + /// 물약 ID + final String id; + + /// 물약 이름 + final String name; + + /// 물약 종류 (hp / mp) + final PotionType type; + + /// 물약 티어 (1~5, 높을수록 강력) + final int tier; + + /// 고정 회복량 + final int healAmount; + + /// 비율 회복량 (0.0 ~ 1.0) + final double healPercent; + + /// 구매 가격 (골드) + final int price; + + /// HP 물약 여부 + bool get isHpPotion => type == PotionType.hp; + + /// MP 물약 여부 + bool get isMpPotion => type == PotionType.mp; + + /// 실제 회복량 계산 + /// + /// [maxValue] 최대 HP 또는 MP + int calculateHeal(int maxValue) { + final percentHeal = (maxValue * healPercent).round(); + return healAmount + percentHeal; + } +} + +/// 물약 인벤토리 상태 +/// +/// 보유 물약 수량 및 전투 중 사용 기록 관리 +class PotionInventory { + const PotionInventory({ + this.potions = const {}, + this.usedInBattle = const {}, + }); + + /// 보유 물약 (물약 ID → 수량) + final Map potions; + + /// 현재 전투에서 사용한 물약 종류 + final Set usedInBattle; + + /// 물약 보유 여부 + bool hasPotion(String potionId) => (potions[potionId] ?? 0) > 0; + + /// 물약 수량 조회 + int getQuantity(String potionId) => potions[potionId] ?? 0; + + /// 특정 종류 물약 사용 가능 여부 + /// + /// 전투당 종류별 1회 제한 체크 + bool canUseType(PotionType type) => !usedInBattle.contains(type); + + /// 물약 추가 + PotionInventory addPotion(String potionId, [int count = 1]) { + final newPotions = Map.from(potions); + newPotions[potionId] = (newPotions[potionId] ?? 0) + count; + return PotionInventory( + potions: newPotions, + usedInBattle: usedInBattle, + ); + } + + /// 물약 사용 (수량 감소) + PotionInventory usePotion(String potionId, PotionType type) { + final currentQty = potions[potionId] ?? 0; + if (currentQty <= 0) return this; + + final newPotions = Map.from(potions); + newPotions[potionId] = currentQty - 1; + if (newPotions[potionId] == 0) { + newPotions.remove(potionId); + } + + final newUsed = Set.from(usedInBattle)..add(type); + + return PotionInventory( + potions: newPotions, + usedInBattle: newUsed, + ); + } + + /// 전투 종료 시 사용 기록 초기화 + PotionInventory resetBattleUsage() { + return PotionInventory( + potions: potions, + usedInBattle: const {}, + ); + } + + /// 빈 인벤토리 + static const empty = PotionInventory(); + + PotionInventory copyWith({ + Map? potions, + Set? usedInBattle, + }) { + return PotionInventory( + potions: potions ?? this.potions, + usedInBattle: usedInBattle ?? this.usedInBattle, + ); + } +} diff --git a/lib/src/core/model/skill.dart b/lib/src/core/model/skill.dart index ae2d182..9e3aa2d 100644 --- a/lib/src/core/model/skill.dart +++ b/lib/src/core/model/skill.dart @@ -13,6 +13,42 @@ enum SkillType { debuff, } +/// 스킬 속성 (하이브리드: 코드 + 시스템) +enum SkillElement { + /// 논리 (Logic) - 순수 데미지 + logic, + + /// 메모리 (Memory) - DoT 특화 + memory, + + /// 네트워크 (Network) - 다중 타격 + network, + + /// 화염 (Overheat) - 높은 순간 데미지 + fire, + + /// 빙결 (Freeze) - 슬로우 효과 + ice, + + /// 전기 (Surge) - 빠른 연속 타격 + lightning, + + /// 공허 (Null) - 방어 무시 + voidElement, + + /// 혼돈 (Glitch) - 랜덤 효과 + chaos, +} + +/// 공격 방식 +enum AttackMode { + /// 단발성 - 즉시 데미지 + instant, + + /// 지속 피해 - N초간 틱당 데미지 + dot, +} + /// 버프 효과 class BuffEffect { const BuffEffect({ @@ -62,6 +98,11 @@ class Skill { this.buff, this.selfDamagePercent = 0.0, this.targetDefReduction = 0.0, + this.element, + this.attackMode = AttackMode.instant, + this.baseDotDamage, + this.baseDotDurationMs, + this.baseDotTickMs, }); /// 스킬 ID @@ -100,6 +141,21 @@ class Skill { /// 적 방어력 감소 % (일부 공격 스킬) final double targetDefReduction; + /// 스킬 속성 (element) - 하이브리드 시스템 + final SkillElement? element; + + /// 공격 방식 (instant: 단발성, dot: 지속 피해) + final AttackMode attackMode; + + /// DOT 기본 틱당 데미지 (스킬 레벨로 결정) + final int? baseDotDamage; + + /// DOT 기본 지속시간 (밀리초, 스킬 레벨로 결정) + final int? baseDotDurationMs; + + /// DOT 기본 틱 간격 (밀리초, 스킬 레벨로 결정) + final int? baseDotTickMs; + /// 공격 스킬 여부 bool get isAttack => type == SkillType.attack; @@ -112,6 +168,9 @@ class Skill { /// 디버프 스킬 여부 bool get isDebuff => type == SkillType.debuff; + /// DOT 스킬 여부 + bool get isDot => attackMode == AttackMode.dot; + /// MP 효율 (데미지 당 MP 비용) double get mpEfficiency { if (type != SkillType.attack || damageMultiplier <= 0) return 0; @@ -265,3 +324,140 @@ enum SkillFailReason { /// 사용 불가 상태 invalidState, } + +/// DOT (지속 피해) 효과 +/// +/// 스킬 사용 시 생성되어 전투 중 틱마다 데미지를 적용. +/// - INT: 틱당 데미지 증가 +/// - WIS: 틱 간격 감소 (더 빠른 피해) +/// - 스킬 레벨: 기본 데미지, 지속시간, 틱 간격 결정 +class DotEffect { + const DotEffect({ + required this.skillId, + required this.baseDamage, + required this.damagePerTick, + required this.tickIntervalMs, + required this.totalDurationMs, + this.remainingDurationMs = 0, + this.tickAccumulatorMs = 0, + this.element, + }); + + /// 원본 스킬 ID + final String skillId; + + /// 스킬 기본 데미지 (스킬 레벨로 결정) + final int baseDamage; + + /// INT 보정 적용된 틱당 실제 데미지 + final int damagePerTick; + + /// WIS 보정 적용된 틱 간격 (밀리초) + final int tickIntervalMs; + + /// 총 지속시간 (밀리초, 스킬 레벨로 결정) + final int totalDurationMs; + + /// 남은 지속시간 (밀리초) + final int remainingDurationMs; + + /// 다음 틱까지 누적 시간 (밀리초) + final int tickAccumulatorMs; + + /// 속성 (선택) + final SkillElement? element; + + /// DOT 만료 여부 + bool get isExpired => remainingDurationMs <= 0; + + /// DOT 활성 여부 + bool get isActive => remainingDurationMs > 0; + + /// 예상 남은 틱 수 + int get remainingTicks { + if (tickIntervalMs <= 0) return 0; + return (remainingDurationMs / tickIntervalMs).ceil(); + } + + /// 스킬과 플레이어 스탯으로 DotEffect 생성 + /// + /// [skill] DOT 스킬 + /// [playerInt] 플레이어 INT (틱당 데미지 보정) + /// [playerWis] 플레이어 WIS (틱 간격 보정) + 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 필수'); + assert(skill.baseDotTickMs != null, 'baseDotTickMs 필수'); + + // INT → 데미지 보정 (INT 10 기준, ±3%/포인트) + final intMod = 1.0 + (playerInt - 10) * 0.03; + final actualDamage = (skill.baseDotDamage! * intMod).round(); + + // WIS → 틱 간격 보정 (WIS 10 기준, ±2%/포인트, 빨라짐) + final wisMod = 1.0 + (playerWis - 10) * 0.02; + final actualTickMs = (skill.baseDotTickMs! / wisMod).clamp(200, 2000).round(); + + return DotEffect( + skillId: skill.id, + baseDamage: skill.baseDotDamage!, + damagePerTick: actualDamage.clamp(1, 9999), + tickIntervalMs: actualTickMs, + totalDurationMs: skill.baseDotDurationMs!, + remainingDurationMs: skill.baseDotDurationMs!, + tickAccumulatorMs: 0, + element: skill.element, + ); + } + + /// 시간 경과 후 새 DotEffect 반환 + /// + /// [elapsedMs] 경과 시간 (밀리초) + /// Returns: (새 DotEffect, 이번에 발생한 틱 수) + (DotEffect, int) tick(int elapsedMs) { + var newAccumulator = tickAccumulatorMs + elapsedMs; + var newRemaining = remainingDurationMs - elapsedMs; + var ticksTriggered = 0; + + // 틱 발생 체크 + while (newAccumulator >= tickIntervalMs && newRemaining > 0) { + newAccumulator -= tickIntervalMs; + ticksTriggered++; + } + + final updated = DotEffect( + skillId: skillId, + baseDamage: baseDamage, + damagePerTick: damagePerTick, + tickIntervalMs: tickIntervalMs, + totalDurationMs: totalDurationMs, + remainingDurationMs: newRemaining.clamp(0, totalDurationMs), + tickAccumulatorMs: newRemaining > 0 ? newAccumulator : 0, + element: element, + ); + + return (updated, ticksTriggered); + } + + DotEffect copyWith({ + String? skillId, + int? baseDamage, + int? damagePerTick, + int? tickIntervalMs, + int? totalDurationMs, + int? remainingDurationMs, + int? tickAccumulatorMs, + SkillElement? element, + }) { + return DotEffect( + skillId: skillId ?? this.skillId, + baseDamage: baseDamage ?? this.baseDamage, + damagePerTick: damagePerTick ?? this.damagePerTick, + tickIntervalMs: tickIntervalMs ?? this.tickIntervalMs, + totalDurationMs: totalDurationMs ?? this.totalDurationMs, + remainingDurationMs: remainingDurationMs ?? this.remainingDurationMs, + tickAccumulatorMs: tickAccumulatorMs ?? this.tickAccumulatorMs, + element: element ?? this.element, + ); + } +} diff --git a/lib/src/features/game/game_play_screen.dart b/lib/src/features/game/game_play_screen.dart index f23553a..b23ab37 100644 --- a/lib/src/features/game/game_play_screen.dart +++ b/lib/src/features/game/game_play_screen.dart @@ -20,7 +20,10 @@ 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'; import 'package:askiineverdie/src/features/game/widgets/task_progress_panel.dart'; +import 'package:askiineverdie/src/features/game/widgets/active_buff_panel.dart'; /// 게임 진행 화면 (Main.dfm 기반 3패널 레이아웃) /// @@ -199,6 +202,18 @@ class _GamePlayScreenState extends State '${event.skillName} activated!', CombatLogType.buff, ), + CombatEventType.dotTick => ( + '${event.skillName} ticks for ${event.damage} damage', + CombatLogType.dotTick, + ), + CombatEventType.playerPotion => ( + '${event.skillName}: +${event.healAmount} ${event.targetName}', + CombatLogType.potion, + ), + CombatEventType.potionDrop => ( + 'Dropped: ${event.skillName}', + CombatLogType.potionDrop, + ), }; } @@ -534,6 +549,15 @@ class _GamePlayScreenState extends State // Phase 8: 스킬 (Skills with cooldown glow) _buildSectionHeader('Skills'), Expanded(flex: 2, child: SkillPanel(skillSystem: state.skillSystem)), + + // 활성 버프 (Active Buffs) + _buildSectionHeader('Buffs'), + Expanded( + child: ActiveBuffPanel( + activeBuffs: state.skillSystem.activeBuffs, + currentMs: state.skillSystem.elapsedMs, + ), + ), ], ), ); @@ -549,12 +573,25 @@ class _GamePlayScreenState extends State children: [ _buildPanelHeader(l10n.equipment), - // Equipment 목록 - Expanded(flex: 2, child: _buildEquipmentList(state)), + // Equipment 목록 (확장 가능 스탯 패널) + Expanded( + flex: 2, + child: EquipmentStatsPanel(equipment: state.equipment), + ), // Inventory _buildPanelHeader(l10n.inventory), - Expanded(flex: 2, child: _buildInventoryList(state)), + Expanded(child: _buildInventoryList(state)), + + // Potions (물약 인벤토리) + _buildSectionHeader('Potions'), + Expanded( + child: PotionInventoryPanel( + inventory: state.potionInventory, + usedInBattle: + state.progress.currentCombat?.usedPotionTypes ?? const {}, + ), + ), // Encumbrance 바 _buildSectionHeader(l10n.encumbrance), @@ -729,58 +766,6 @@ class _GamePlayScreenState extends State ); } - Widget _buildEquipmentList(GameState state) { - // 원본 Main.dfm Equips ListView - 11개 슬롯 - // (슬롯 레이블, 장비 이름, 슬롯 인덱스) 튜플 - final l10n = L10n.of(context); - final equipment = [ - (l10n.equipWeapon, state.equipment.weapon, 0), - (l10n.equipShield, state.equipment.shield, 1), - (l10n.equipHelm, state.equipment.helm, 2), - (l10n.equipHauberk, state.equipment.hauberk, 3), - (l10n.equipBrassairts, state.equipment.brassairts, 4), - (l10n.equipVambraces, state.equipment.vambraces, 5), - (l10n.equipGauntlets, state.equipment.gauntlets, 6), - (l10n.equipGambeson, state.equipment.gambeson, 7), - (l10n.equipCuisses, state.equipment.cuisses, 8), - (l10n.equipGreaves, state.equipment.greaves, 9), - (l10n.equipSollerets, state.equipment.sollerets, 10), - ]; - - return ListView.builder( - itemCount: equipment.length, - padding: const EdgeInsets.symmetric(horizontal: 8), - itemBuilder: (context, index) { - final equip = equipment[index]; - // 장비 이름 번역 (슬롯 인덱스 사용) - final translatedName = equip.$2.isNotEmpty - ? GameDataL10n.translateEquipString(context, equip.$2, equip.$3) - : '-'; - return Padding( - padding: const EdgeInsets.symmetric(vertical: 2), - child: Row( - children: [ - SizedBox( - width: 60, - child: Text(equip.$1, style: const TextStyle(fontSize: 11)), - ), - Expanded( - child: Text( - translatedName, - style: const TextStyle( - fontSize: 11, - fontWeight: FontWeight.bold, - ), - overflow: TextOverflow.ellipsis, - ), - ), - ], - ), - ); - }, - ); - } - Widget _buildInventoryList(GameState state) { final l10n = L10n.of(context); if (state.inventory.items.isEmpty) { diff --git a/lib/src/features/game/widgets/active_buff_panel.dart b/lib/src/features/game/widgets/active_buff_panel.dart new file mode 100644 index 0000000..029e0c2 --- /dev/null +++ b/lib/src/features/game/widgets/active_buff_panel.dart @@ -0,0 +1,206 @@ +import 'package:flutter/material.dart'; + +import 'package:askiineverdie/src/core/model/skill.dart'; + +/// 활성 버프 패널 위젯 +/// +/// 현재 적용 중인 버프 목록과 남은 시간을 표시. +class ActiveBuffPanel extends StatelessWidget { + const ActiveBuffPanel({ + super.key, + required this.activeBuffs, + required this.currentMs, + }); + + final List activeBuffs; + final int currentMs; + + @override + Widget build(BuildContext context) { + if (activeBuffs.isEmpty) { + return const Center( + child: Text( + 'No active buffs', + style: TextStyle( + fontSize: 11, + color: Colors.grey, + fontStyle: FontStyle.italic, + ), + ), + ); + } + + return ListView.builder( + itemCount: activeBuffs.length, + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + itemBuilder: (context, index) { + final buff = activeBuffs[index]; + return _BuffRow(buff: buff, currentMs: currentMs); + }, + ); + } +} + +/// 개별 버프 행 위젯 +class _BuffRow extends StatelessWidget { + const _BuffRow({ + required this.buff, + required this.currentMs, + }); + + final ActiveBuff buff; + final int currentMs; + + @override + Widget build(BuildContext context) { + final remainingMs = buff.remainingDuration(currentMs); + final remainingSec = (remainingMs / 1000).toStringAsFixed(1); + final progress = remainingMs / buff.effect.durationMs; + final modifiers = _buildModifierList(); + + return Padding( + padding: const EdgeInsets.symmetric(vertical: 2), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + // 버프 아이콘 + const Icon( + Icons.trending_up, + size: 14, + color: Colors.lightBlue, + ), + const SizedBox(width: 4), + + // 버프 이름 + Expanded( + child: Text( + buff.effect.name, + style: const TextStyle( + fontSize: 11, + fontWeight: FontWeight.w500, + color: Colors.lightBlue, + ), + overflow: TextOverflow.ellipsis, + ), + ), + + // 남은 시간 + Text( + '${remainingSec}s', + style: TextStyle( + fontSize: 10, + color: remainingMs < 3000 ? Colors.orange : Colors.grey, + fontWeight: + remainingMs < 3000 ? FontWeight.bold : FontWeight.normal, + ), + ), + ], + ), + + const SizedBox(height: 2), + + // 남은 시간 프로그레스 바 + ClipRRect( + borderRadius: BorderRadius.circular(2), + child: LinearProgressIndicator( + value: progress.clamp(0.0, 1.0), + minHeight: 3, + backgroundColor: Colors.grey.shade800, + valueColor: AlwaysStoppedAnimation( + progress > 0.3 ? Colors.lightBlue : Colors.orange, + ), + ), + ), + + // 효과 목록 + if (modifiers.isNotEmpty) ...[ + const SizedBox(height: 2), + Wrap( + spacing: 6, + runSpacing: 2, + children: modifiers, + ), + ], + ], + ), + ); + } + + /// 버프 효과 목록 생성 + List _buildModifierList() { + final modifiers = []; + final effect = buff.effect; + + if (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, + )); + } + + if (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, + )); + } + + return modifiers; + } +} + +/// 효과 칩 위젯 +class _ModifierChip extends StatelessWidget { + const _ModifierChip({ + required this.label, + required this.value, + required this.isPositive, + }); + + final String label; + final double value; + final bool isPositive; + + @override + Widget build(BuildContext context) { + final color = isPositive ? Colors.green : Colors.red; + final sign = isPositive ? '+' : ''; + final percent = (value * 100).round(); + + return Container( + padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 1), + decoration: BoxDecoration( + color: color.withValues(alpha: 0.2), + borderRadius: BorderRadius.circular(3), + ), + child: Text( + '$label: $sign$percent%', + style: TextStyle( + fontSize: 8, + color: color, + fontWeight: FontWeight.w500, + ), + ), + ); + } +} diff --git a/lib/src/features/game/widgets/ascii_animation_card.dart b/lib/src/features/game/widgets/ascii_animation_card.dart index 804aa67..fae3293 100644 --- a/lib/src/features/game/widgets/ascii_animation_card.dart +++ b/lib/src/features/game/widgets/ascii_animation_card.dart @@ -200,6 +200,15 @@ class _AsciiAnimationCardState extends State { // 회복/버프 → idle 페이즈 유지 CombatEventType.playerHeal => (BattlePhase.idle, false), CombatEventType.playerBuff => (BattlePhase.idle, false), + + // DOT 틱 → attack 페이즈 (지속 피해) + CombatEventType.dotTick => (BattlePhase.attack, false), + + // 물약 사용 → idle 페이즈 유지 + CombatEventType.playerPotion => (BattlePhase.idle, false), + + // 물약 드랍 → idle 페이즈 유지 + CombatEventType.potionDrop => (BattlePhase.idle, false), }; setState(() { diff --git a/lib/src/features/game/widgets/combat_log.dart b/lib/src/features/game/widgets/combat_log.dart index df6e1f1..9a0dd17 100644 --- a/lib/src/features/game/widgets/combat_log.dart +++ b/lib/src/features/game/widgets/combat_log.dart @@ -21,13 +21,16 @@ enum CombatLogType { levelUp, // 레벨업 questComplete, // 퀘스트 완료 loot, // 전리품 획득 - spell, // 주문 습득 + spell, // 스킬 사용 critical, // 크리티컬 히트 evade, // 회피 block, // 방패 방어 parry, // 무기 쳐내기 monsterAttack, // 몬스터 공격 buff, // 버프 활성화 + dotTick, // DOT 틱 데미지 + potion, // 물약 사용 + potionDrop, // 물약 드랍 } /// 전투 로그 위젯 (Phase 8: 실시간 전투 이벤트 표시) @@ -157,6 +160,9 @@ class _LogEntryTile extends StatelessWidget { CombatLogType.parry => (Colors.teal.shade300, Icons.sports_kabaddi), 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), + CombatLogType.potionDrop => (Colors.lime.shade300, Icons.card_giftcard), }; } } diff --git a/lib/src/features/game/widgets/death_overlay.dart b/lib/src/features/game/widgets/death_overlay.dart index 8a6642a..ed6700e 100644 --- a/lib/src/features/game/widgets/death_overlay.dart +++ b/lib/src/features/game/widgets/death_overlay.dart @@ -426,6 +426,21 @@ class DeathOverlay extends StatelessWidget { Colors.lightBlue.shade300, '${event.skillName} activated', ), + CombatEventType.dotTick => ( + Icons.whatshot, + Colors.deepOrange.shade300, + '${event.skillName} ticks for ${event.damage} damage', + ), + CombatEventType.playerPotion => ( + Icons.local_drink, + Colors.lightGreen.shade300, + '${event.skillName}: +${event.healAmount} ${event.targetName}', + ), + CombatEventType.potionDrop => ( + Icons.card_giftcard, + Colors.lime.shade300, + 'Dropped: ${event.skillName}', + ), }; } } diff --git a/lib/src/features/game/widgets/equipment_stats_panel.dart b/lib/src/features/game/widgets/equipment_stats_panel.dart new file mode 100644 index 0000000..9f7f6d4 --- /dev/null +++ b/lib/src/features/game/widgets/equipment_stats_panel.dart @@ -0,0 +1,456 @@ +import 'package:flutter/material.dart'; + +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'; +import 'package:askiineverdie/src/core/model/game_state.dart'; +import 'package:askiineverdie/src/core/model/item_stats.dart'; + +/// 장비 스탯 표시 패널 +/// +/// 각 장비 슬롯의 아이템과 스탯을 확장 가능한 형태로 표시. +/// 접힌 상태: 슬롯명 + 아이템명 +/// 펼친 상태: 전체 스탯 및 점수 +class EquipmentStatsPanel extends StatelessWidget { + const EquipmentStatsPanel({ + super.key, + required this.equipment, + this.initiallyExpanded = false, + }); + + final Equipment equipment; + final bool initiallyExpanded; + + @override + Widget build(BuildContext context) { + final totalScore = _calculateTotalScore(); + final equippedCount = equipment.items.where((e) => e.isNotEmpty).length; + + return ListView.builder( + // +1 for header + itemCount: equipment.items.length + 1, + padding: const EdgeInsets.all(4), + itemBuilder: (context, index) { + // 첫 번째 아이템은 총합 헤더 + if (index == 0) { + return _TotalScoreHeader( + totalScore: totalScore, + equippedCount: equippedCount, + totalSlots: equipment.items.length, + ); + } + + final item = equipment.items[index - 1]; + return _EquipmentSlotTile( + item: item, + initiallyExpanded: initiallyExpanded, + ); + }, + ); + } + + /// 모든 장비의 점수 합산 + int _calculateTotalScore() { + var total = 0; + for (final item in equipment.items) { + if (item.isNotEmpty) { + total += ItemService.calculateEquipmentScore(item); + } + } + return total; + } +} + +/// 개별 장비 슬롯 타일 +class _EquipmentSlotTile extends StatelessWidget { + const _EquipmentSlotTile({ + required this.item, + this.initiallyExpanded = false, + }); + + final EquipmentItem item; + final bool initiallyExpanded; + + @override + Widget build(BuildContext context) { + if (item.isEmpty) { + return _EmptySlotTile(slot: item.slot); + } + + final score = ItemService.calculateEquipmentScore(item); + final rarityColor = _getRarityColor(item.rarity); + + return ExpansionTile( + initiallyExpanded: initiallyExpanded, + tilePadding: const EdgeInsets.symmetric(horizontal: 8), + childrenPadding: const EdgeInsets.only(left: 16, right: 8, bottom: 8), + dense: true, + title: Row( + children: [ + _SlotIcon(slot: item.slot), + const SizedBox(width: 4), + Expanded( + child: Text( + item.name, + style: TextStyle( + fontSize: 11, + color: rarityColor, + fontWeight: FontWeight.w500, + ), + overflow: TextOverflow.ellipsis, + ), + ), + _ScoreBadge(score: score), + ], + ), + children: [ + _StatsGrid(stats: item.stats, slot: item.slot), + const SizedBox(height: 4), + _ItemMetaRow(item: item), + ], + ); + } + + Color _getRarityColor(ItemRarity rarity) { + return switch (rarity) { + ItemRarity.common => Colors.grey, + ItemRarity.uncommon => Colors.green, + ItemRarity.rare => Colors.blue, + ItemRarity.epic => Colors.purple, + ItemRarity.legendary => Colors.orange, + }; + } +} + +/// 빈 슬롯 타일 +class _EmptySlotTile extends StatelessWidget { + const _EmptySlotTile({required this.slot}); + + final EquipmentSlot slot; + + @override + Widget build(BuildContext context) { + return ListTile( + dense: true, + contentPadding: const EdgeInsets.symmetric(horizontal: 8), + leading: _SlotIcon(slot: slot, isEmpty: true), + title: Text( + '[${_getSlotName(slot)}] (empty)', + style: TextStyle( + fontSize: 11, + color: Colors.grey.shade600, + fontStyle: FontStyle.italic, + ), + ), + ); + } +} + +/// 슬롯 아이콘 +class _SlotIcon extends StatelessWidget { + const _SlotIcon({required this.slot, this.isEmpty = false}); + + final EquipmentSlot slot; + final bool isEmpty; + + @override + Widget build(BuildContext context) { + final icon = switch (slot) { + EquipmentSlot.weapon => Icons.gavel, + EquipmentSlot.shield => Icons.shield, + EquipmentSlot.helm => Icons.sports_martial_arts, + EquipmentSlot.hauberk => Icons.checkroom, + EquipmentSlot.brassairts => Icons.back_hand, + EquipmentSlot.vambraces => Icons.front_hand, + EquipmentSlot.gauntlets => Icons.pan_tool, + EquipmentSlot.gambeson => Icons.dry_cleaning, + EquipmentSlot.cuisses => Icons.airline_seat_legroom_normal, + EquipmentSlot.greaves => Icons.snowshoeing, + EquipmentSlot.sollerets => Icons.do_not_step, + }; + + return Icon( + icon, + size: 16, + color: isEmpty ? Colors.grey.shade400 : Colors.grey.shade700, + ); + } +} + +/// 점수 배지 +class _ScoreBadge extends StatelessWidget { + const _ScoreBadge({required this.score}); + + final int score; + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), + decoration: BoxDecoration( + color: Colors.blueGrey.shade100, + borderRadius: BorderRadius.circular(8), + ), + child: Text( + '$score', + style: TextStyle( + fontSize: 10, + fontWeight: FontWeight.bold, + color: Colors.blueGrey.shade700, + ), + ), + ); + } +} + +/// 장비 점수 총합 헤더 +class _TotalScoreHeader extends StatelessWidget { + const _TotalScoreHeader({ + required this.totalScore, + required this.equippedCount, + required this.totalSlots, + }); + + final int totalScore; + final int equippedCount; + final int totalSlots; + + @override + Widget build(BuildContext context) { + return Container( + margin: const EdgeInsets.only(bottom: 8), + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [ + Colors.blueGrey.shade700, + Colors.blueGrey.shade600, + ], + ), + borderRadius: BorderRadius.circular(8), + boxShadow: [ + BoxShadow( + color: Colors.black.withValues(alpha: 0.2), + blurRadius: 4, + offset: const Offset(0, 2), + ), + ], + ), + child: Row( + children: [ + // 장비 아이콘 + const Icon( + Icons.shield, + size: 20, + color: Colors.white70, + ), + const SizedBox(width: 8), + + // 총합 점수 + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'Equipment Score', + style: TextStyle( + fontSize: 10, + color: Colors.white70, + ), + ), + Text( + '$totalScore', + style: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + color: Colors.white, + ), + ), + ], + ), + ), + + // 장착 현황 + Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + decoration: BoxDecoration( + color: Colors.white.withValues(alpha: 0.2), + borderRadius: BorderRadius.circular(12), + ), + child: Text( + '$equippedCount / $totalSlots', + style: const TextStyle( + fontSize: 11, + fontWeight: FontWeight.w500, + color: Colors.white, + ), + ), + ), + ], + ), + ); + } +} + +/// 스탯 그리드 +class _StatsGrid extends StatelessWidget { + const _StatsGrid({required this.stats, required this.slot}); + + final ItemStats stats; + final EquipmentSlot slot; + + @override + Widget build(BuildContext context) { + 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.criRate > 0) { + entries.add(_StatEntry('CRI', '${(stats.criRate * 100).toStringAsFixed(1)}%')); + } + if (stats.parryRate > 0) { + entries.add(_StatEntry('PARRY', '${(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.blockRate > 0) { + entries.add(_StatEntry('BLOCK', '${(stats.blockRate * 100).toStringAsFixed(1)}%')); + } + if (stats.evasion > 0) { + entries.add(_StatEntry('EVA', '${(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.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 (slot == EquipmentSlot.weapon && stats.attackSpeed > 0) { + entries.add(_StatEntry('SPEED', '${stats.attackSpeed}ms')); + } + + if (entries.isEmpty) { + return const Text( + 'No bonus stats', + style: TextStyle(fontSize: 10, color: Colors.grey), + ); + } + + return Wrap( + spacing: 8, + runSpacing: 4, + children: entries.map((e) => _StatChip(entry: e)).toList(), + ); + } +} + +/// 스탯 엔트리 +class _StatEntry { + const _StatEntry(this.label, this.value); + final String label; + final String value; +} + +/// 스탯 칩 +class _StatChip extends StatelessWidget { + const _StatChip({required this.entry}); + + final _StatEntry entry; + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 2), + decoration: BoxDecoration( + color: Colors.grey.shade200, + borderRadius: BorderRadius.circular(4), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + '${entry.label}: ', + style: TextStyle(fontSize: 9, color: Colors.grey.shade600), + ), + Text( + entry.value, + style: const TextStyle(fontSize: 9, fontWeight: FontWeight.bold), + ), + ], + ), + ); + } +} + +/// 아이템 메타 정보 행 +class _ItemMetaRow extends StatelessWidget { + const _ItemMetaRow({required this.item}); + + final EquipmentItem item; + + @override + Widget build(BuildContext context) { + final rarityName = item.rarity.name.toUpperCase(); + + return Row( + children: [ + Text( + 'Lv.${item.level}', + style: const TextStyle(fontSize: 9, color: Colors.grey), + ), + const SizedBox(width: 8), + Text( + rarityName, + style: TextStyle( + fontSize: 9, + color: _getRarityColor(item.rarity), + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(width: 8), + Text( + 'Wt.${item.weight}', + style: const TextStyle(fontSize: 9, color: Colors.grey), + ), + ], + ); + } + + Color _getRarityColor(ItemRarity rarity) { + return switch (rarity) { + ItemRarity.common => Colors.grey, + ItemRarity.uncommon => Colors.green, + ItemRarity.rare => Colors.blue, + ItemRarity.epic => Colors.purple, + ItemRarity.legendary => Colors.orange, + }; + } +} + +/// 슬롯 이름 반환 +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', + }; +} diff --git a/lib/src/features/game/widgets/potion_inventory_panel.dart b/lib/src/features/game/widgets/potion_inventory_panel.dart new file mode 100644 index 0000000..bf81d0d --- /dev/null +++ b/lib/src/features/game/widgets/potion_inventory_panel.dart @@ -0,0 +1,238 @@ +import 'package:flutter/material.dart'; + +import 'package:askiineverdie/data/potion_data.dart'; +import 'package:askiineverdie/src/core/model/potion.dart'; + +/// 물약 인벤토리 패널 +/// +/// 보유 중인 물약 목록과 수량을 표시. +/// HP 물약은 빨간색, MP 물약은 파란색으로 구분. +class PotionInventoryPanel extends StatelessWidget { + const PotionInventoryPanel({ + super.key, + required this.inventory, + this.usedInBattle = const {}, + }); + + final PotionInventory inventory; + final Set usedInBattle; + + @override + Widget build(BuildContext context) { + final potionEntries = _buildPotionEntries(); + + if (potionEntries.isEmpty) { + return const Center( + child: Text( + 'No potions', + style: TextStyle( + fontSize: 11, + color: Colors.grey, + fontStyle: FontStyle.italic, + ), + ), + ); + } + + return ListView.builder( + itemCount: potionEntries.length, + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + itemBuilder: (context, index) { + final entry = potionEntries[index]; + return _PotionRow( + potion: entry.potion, + quantity: entry.quantity, + isUsedThisBattle: usedInBattle.contains(entry.potion.type), + ); + }, + ); + } + + /// 물약 엔트리 목록 생성 + /// + /// HP 물약 먼저, MP 물약 나중에 정렬 + List<_PotionEntry> _buildPotionEntries() { + final entries = <_PotionEntry>[]; + + for (final potionId in inventory.potions.keys) { + final quantity = inventory.potions[potionId] ?? 0; + if (quantity <= 0) continue; + + final potion = PotionData.getById(potionId); + if (potion == null) continue; + + entries.add(_PotionEntry(potion: potion, quantity: quantity)); + } + + // HP 물약 우선, 같은 타입 내에서는 티어순 + entries.sort((a, b) { + final typeCompare = a.potion.type.index.compareTo(b.potion.type.index); + if (typeCompare != 0) return typeCompare; + return a.potion.tier.compareTo(b.potion.tier); + }); + + return entries; + } +} + +/// 물약 엔트리 +class _PotionEntry { + const _PotionEntry({required this.potion, required this.quantity}); + final Potion potion; + final int quantity; +} + +/// 물약 행 위젯 +class _PotionRow extends StatelessWidget { + const _PotionRow({ + required this.potion, + required this.quantity, + this.isUsedThisBattle = false, + }); + + final Potion potion; + final int quantity; + final bool isUsedThisBattle; + + @override + Widget build(BuildContext context) { + final color = _getPotionColor(); + final opacity = isUsedThisBattle ? 0.5 : 1.0; + + return Padding( + padding: const EdgeInsets.symmetric(vertical: 2), + child: Opacity( + opacity: opacity, + child: Row( + children: [ + // 물약 아이콘 + _PotionIcon(type: potion.type, tier: potion.tier), + const SizedBox(width: 4), + + // 물약 이름 + Expanded( + child: Text( + potion.name, + style: TextStyle( + fontSize: 11, + color: color, + fontWeight: FontWeight.w500, + ), + overflow: TextOverflow.ellipsis, + ), + ), + + // 회복량 표시 + _HealBadge(potion: potion), + const SizedBox(width: 4), + + // 수량 + Container( + padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), + decoration: BoxDecoration( + color: color.withValues(alpha: 0.2), + borderRadius: BorderRadius.circular(8), + ), + child: Text( + 'x$quantity', + style: TextStyle( + fontSize: 10, + fontWeight: FontWeight.bold, + color: color, + ), + ), + ), + + // 전투 중 사용 불가 표시 + if (isUsedThisBattle) ...[ + const SizedBox(width: 4), + const Icon( + Icons.block, + size: 12, + color: Colors.grey, + ), + ], + ], + ), + ), + ); + } + + Color _getPotionColor() { + return switch (potion.type) { + PotionType.hp => Colors.red.shade700, + PotionType.mp => Colors.blue.shade700, + }; + } +} + +/// 물약 아이콘 +class _PotionIcon extends StatelessWidget { + const _PotionIcon({required this.type, required this.tier}); + + final PotionType type; + final int tier; + + @override + Widget build(BuildContext context) { + final color = type == PotionType.hp + ? Colors.red.shade400 + : Colors.blue.shade400; + + // 티어에 따른 아이콘 크기 조절 + final size = 12.0 + tier * 1.0; + + return Container( + width: 18, + height: 18, + alignment: Alignment.center, + child: Icon( + type == PotionType.hp ? Icons.favorite : Icons.bolt, + size: size.clamp(12, 18), + color: color, + ), + ); + } +} + +/// 회복량 배지 +class _HealBadge extends StatelessWidget { + const _HealBadge({required this.potion}); + + final Potion potion; + + @override + Widget build(BuildContext context) { + final healText = _buildHealText(); + + return Container( + padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 1), + decoration: BoxDecoration( + color: Colors.grey.shade200, + borderRadius: BorderRadius.circular(4), + ), + child: Text( + healText, + style: TextStyle( + fontSize: 9, + color: Colors.grey.shade700, + ), + ), + ); + } + + String _buildHealText() { + final parts = []; + + if (potion.healAmount > 0) { + parts.add('+${potion.healAmount}'); + } + + if (potion.healPercent > 0) { + final percent = (potion.healPercent * 100).round(); + parts.add('+$percent%'); + } + + return parts.join(' '); + } +} diff --git a/lib/src/features/game/widgets/skill_panel.dart b/lib/src/features/game/widgets/skill_panel.dart index 1e86d67..ce8b518 100644 --- a/lib/src/features/game/widgets/skill_panel.dart +++ b/lib/src/features/game/widgets/skill_panel.dart @@ -148,25 +148,58 @@ class _SkillRow extends StatelessWidget { final skillIcon = _getSkillIcon(skill.type); final skillColor = _getSkillColor(skill.type); + final elementColor = _getElementColor(skill.element); + final elementIcon = _getElementIcon(skill.element); Widget row = Container( padding: const EdgeInsets.symmetric(vertical: 2), child: Row( children: [ - // 스킬 아이콘 + // 스킬 타입 아이콘 Icon(skillIcon, size: 14, color: skillColor), + + // 속성 아이콘 (있는 경우) + if (skill.element != null) ...[ + const SizedBox(width: 2), + _ElementBadge( + icon: elementIcon, + color: elementColor, + isDot: skill.isDot, + ), + ], + const SizedBox(width: 4), - // 스킬 이름 + + // 스킬 이름 (속성 색상 적용) Expanded( child: Text( skill.name, style: TextStyle( fontSize: 10, - color: isReady ? Colors.white : Colors.grey, + color: isReady + ? (skill.element != null ? elementColor : Colors.white) + : Colors.grey, ), overflow: TextOverflow.ellipsis, ), ), + + // DOT 표시 + if (skill.isDot) ...[ + Container( + padding: const EdgeInsets.symmetric(horizontal: 3, vertical: 1), + decoration: BoxDecoration( + color: elementColor.withValues(alpha: 0.3), + borderRadius: BorderRadius.circular(3), + ), + child: const Text( + 'DOT', + style: TextStyle(fontSize: 7, color: Colors.white70), + ), + ), + const SizedBox(width: 4), + ], + // 랭크 Text( 'Lv.$rank', @@ -241,4 +274,69 @@ class _SkillRow extends StatelessWidget { return Colors.purple; } } + + /// 속성별 색상 + Color _getElementColor(SkillElement? element) { + if (element == null) return Colors.grey; + + return switch (element) { + SkillElement.logic => Colors.cyan, + SkillElement.memory => Colors.purple.shade300, + SkillElement.network => Colors.teal, + SkillElement.fire => Colors.orange, + SkillElement.ice => Colors.lightBlue.shade200, + SkillElement.lightning => Colors.yellow.shade600, + SkillElement.voidElement => Colors.deepPurple, + SkillElement.chaos => Colors.pink, + }; + } + + /// 속성별 아이콘 + IconData _getElementIcon(SkillElement? element) { + if (element == null) return Icons.circle; + + return switch (element) { + SkillElement.logic => Icons.code, + SkillElement.memory => Icons.memory, + SkillElement.network => Icons.lan, + SkillElement.fire => Icons.local_fire_department, + SkillElement.ice => Icons.ac_unit, + SkillElement.lightning => Icons.bolt, + SkillElement.voidElement => Icons.remove_circle_outline, + SkillElement.chaos => Icons.shuffle, + }; + } +} + +/// 속성 배지 위젯 +class _ElementBadge extends StatelessWidget { + const _ElementBadge({ + required this.icon, + required this.color, + this.isDot = false, + }); + + final IconData icon; + final Color color; + final bool isDot; + + @override + Widget build(BuildContext context) { + return Container( + width: 14, + height: 14, + decoration: BoxDecoration( + color: color.withValues(alpha: 0.3), + borderRadius: BorderRadius.circular(3), + border: isDot + ? Border.all(color: color.withValues(alpha: 0.7), width: 1) + : null, + ), + child: Icon( + icon, + size: 10, + color: color, + ), + ); + } }