feat(game): 포션 시스템 및 UI 패널 추가
- 포션 시스템 구현 (PotionService, Potion 모델) - 포션 인벤토리 패널 위젯 - 활성 버프 패널 위젯 - 장비 스탯 패널 위젯 - 스킬 시스템 확장 - 일본어 번역 추가 - 전투 이벤트/상태 모델 개선
This commit is contained in:
@@ -1,7 +1,9 @@
|
|||||||
// 게임 텍스트 로컬라이제이션 (BuildContext 없이 사용)
|
// 게임 텍스트 로컬라이제이션 (BuildContext 없이 사용)
|
||||||
// progress_service.dart, pq_logic.dart 등에서 사용
|
// progress_service.dart, pq_logic.dart 등에서 사용
|
||||||
|
// 지원 언어: 한국어(ko), 영어(en), 일본어(ja)
|
||||||
|
|
||||||
import 'package:askiineverdie/data/game_translations_ko.dart';
|
import 'package:askiineverdie/data/game_translations_ko.dart';
|
||||||
|
import 'package:askiineverdie/data/game_translations_ja.dart';
|
||||||
|
|
||||||
/// 현재 게임 로케일 설정 (전역)
|
/// 현재 게임 로케일 설정 (전역)
|
||||||
String _currentLocale = 'en';
|
String _currentLocale = 'en';
|
||||||
@@ -17,6 +19,9 @@ void setGameLocale(String locale) {
|
|||||||
/// 한국어 여부 확인
|
/// 한국어 여부 확인
|
||||||
bool get isKoreanLocale => _currentLocale == 'ko';
|
bool get isKoreanLocale => _currentLocale == 'ko';
|
||||||
|
|
||||||
|
/// 일본어 여부 확인
|
||||||
|
bool get isJapaneseLocale => _currentLocale == 'ja';
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// 프롤로그 텍스트
|
// 프롤로그 텍스트
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
@@ -35,199 +40,417 @@ const _prologueTextsKo = [
|
|||||||
'예상치 못한 결의로 널(Null) 왕국을 향한 위험한 여정을 시작하다',
|
'예상치 못한 결의로 널(Null) 왕국을 향한 위험한 여정을 시작하다',
|
||||||
];
|
];
|
||||||
|
|
||||||
List<String> get prologueTexts =>
|
const _prologueTextsJa = [
|
||||||
isKoreanLocale ? _prologueTextsKo : _prologueTextsEn;
|
'コードの神から不吉な幻影を受ける',
|
||||||
|
'老いたコンパイラー賢者が予言を明かす:「グリッチゴッドが目覚めた」',
|
||||||
|
'突然のバッファオーバーフローが村をリセットし、あなただけが唯一の生存者となる',
|
||||||
|
'予想外の決意で、ヌル(Null)王国への危険な旅に出発する',
|
||||||
|
];
|
||||||
|
|
||||||
|
List<String> 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() =>
|
String taskHeadingToMarket() {
|
||||||
isKoreanLocale ? '전리품을 팔기 위해 데이터 마켓으로 이동 중' : 'Heading to the Data Market to trade loot';
|
if (isKoreanLocale) return '전리품을 팔기 위해 데이터 마켓으로 이동 중';
|
||||||
|
if (isJapaneseLocale) return '戦利品を売るためデータマーケットへ移動中';
|
||||||
|
return 'Heading to the Data Market to trade loot';
|
||||||
|
}
|
||||||
|
|
||||||
String taskUpgradingHardware() =>
|
String taskUpgradingHardware() {
|
||||||
isKoreanLocale ? '테크 샵에서 하드웨어 업그레이드 중' : 'Upgrading hardware at the Tech Shop';
|
if (isKoreanLocale) return '테크 샵에서 하드웨어 업그레이드 중';
|
||||||
|
if (isJapaneseLocale) return 'テックショップでハードウェアをアップグレード中';
|
||||||
|
return 'Upgrading hardware at the Tech Shop';
|
||||||
|
}
|
||||||
|
|
||||||
String taskEnteringDebugZone() =>
|
String taskEnteringDebugZone() {
|
||||||
isKoreanLocale ? '디버그 존 진입 중' : 'Entering the Debug Zone';
|
if (isKoreanLocale) return '디버그 존 진입 중';
|
||||||
|
if (isJapaneseLocale) return 'デバッグゾーンに進入中';
|
||||||
|
return 'Entering the Debug Zone';
|
||||||
|
}
|
||||||
|
|
||||||
String taskDebugging(String monsterName) =>
|
String taskDebugging(String monsterName) {
|
||||||
isKoreanLocale ? '$monsterName 디버깅 중' : 'Debugging $monsterName';
|
if (isKoreanLocale) return '$monsterName 디버깅 중';
|
||||||
|
if (isJapaneseLocale) return '$monsterName をデバッグ中';
|
||||||
|
return 'Debugging $monsterName';
|
||||||
|
}
|
||||||
|
|
||||||
String taskSelling(String itemDescription) =>
|
String taskSelling(String itemDescription) {
|
||||||
isKoreanLocale ? '$itemDescription 판매 중' : 'Selling $itemDescription';
|
if (isKoreanLocale) return '$itemDescription 판매 중';
|
||||||
|
if (isJapaneseLocale) return '$itemDescription を販売中';
|
||||||
|
return 'Selling $itemDescription';
|
||||||
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// 퀘스트 캡션
|
// 퀘스트 캡션
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
String questPatch(String name) =>
|
String questPatch(String name) {
|
||||||
isKoreanLocale ? '$name 패치하기' : 'Patch $name';
|
if (isKoreanLocale) return '$name 패치하기';
|
||||||
|
if (isJapaneseLocale) return '$name をパッチする';
|
||||||
|
return 'Patch $name';
|
||||||
|
}
|
||||||
|
|
||||||
String questLocate(String item) =>
|
String questLocate(String item) {
|
||||||
isKoreanLocale ? '$item 찾기' : 'Locate $item';
|
if (isKoreanLocale) return '$item 찾기';
|
||||||
|
if (isJapaneseLocale) return '$item を探す';
|
||||||
|
return 'Locate $item';
|
||||||
|
}
|
||||||
|
|
||||||
String questTransfer(String item) =>
|
String questTransfer(String item) {
|
||||||
isKoreanLocale ? '이 $item 전송하기' : 'Transfer this $item';
|
if (isKoreanLocale) return '이 $item 전송하기';
|
||||||
|
if (isJapaneseLocale) return 'この$item を転送する';
|
||||||
|
return 'Transfer this $item';
|
||||||
|
}
|
||||||
|
|
||||||
String questDownload(String item) =>
|
String questDownload(String item) {
|
||||||
isKoreanLocale ? '$item 다운로드하기' : 'Download $item';
|
if (isKoreanLocale) return '$item 다운로드하기';
|
||||||
|
if (isJapaneseLocale) return '$item をダウンロードする';
|
||||||
|
return 'Download $item';
|
||||||
|
}
|
||||||
|
|
||||||
String questStabilize(String name) =>
|
String questStabilize(String name) {
|
||||||
isKoreanLocale ? '$name 안정화하기' : 'Stabilize $name';
|
if (isKoreanLocale) return '$name 안정화하기';
|
||||||
|
if (isJapaneseLocale) return '$name を安定化する';
|
||||||
|
return 'Stabilize $name';
|
||||||
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// Act 제목
|
// Act 제목
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
String actTitle(String romanNumeral) =>
|
String actTitle(String romanNumeral) {
|
||||||
isKoreanLocale ? '$romanNumeral막' : 'Act $romanNumeral';
|
if (isKoreanLocale) return '$romanNumeral막';
|
||||||
|
if (isJapaneseLocale) return '第$romanNumeral幕';
|
||||||
|
return 'Act $romanNumeral';
|
||||||
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// 시네마틱 텍스트 - 시나리오 1: 캐시 존
|
// 시네마틱 텍스트 - 시나리오 1: 캐시 존
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
String cinematicCacheZone1() => isKoreanLocale
|
String cinematicCacheZone1() {
|
||||||
? '지쳐서 손상된 네트워크의 안전한 캐시 존에 도착하다'
|
if (isKoreanLocale) return '지쳐서 손상된 네트워크의 안전한 캐시 존에 도착하다';
|
||||||
: 'Exhausted, you reach a safe Cache Zone in the corrupted network';
|
if (isJapaneseLocale) return '疲れ果てて、破損したネットワークの安全なキャッシュゾーンに到着する';
|
||||||
|
return 'Exhausted, you reach a safe Cache Zone in the corrupted network';
|
||||||
|
}
|
||||||
|
|
||||||
String cinematicCacheZone2() => isKoreanLocale
|
String cinematicCacheZone2() {
|
||||||
? '옛 동맹들과 재연결하고 새로운 동료들을 포크하다'
|
if (isKoreanLocale) return '옛 동맹들과 재연결하고 새로운 동료들을 포크하다';
|
||||||
: 'You reconnect with old allies and fork new ones';
|
if (isJapaneseLocale) return '古い同盟者と再接続し、新しい仲間をフォークする';
|
||||||
|
return 'You reconnect with old allies and fork new ones';
|
||||||
|
}
|
||||||
|
|
||||||
String cinematicCacheZone3() => isKoreanLocale
|
String cinematicCacheZone3() {
|
||||||
? '디버거 기사단 회의에 참석하다'
|
if (isKoreanLocale) return '디버거 기사단 회의에 참석하다';
|
||||||
: 'You attend a council of the Debugger Knights';
|
if (isJapaneseLocale) return 'デバッガー騎士団の会議に参加する';
|
||||||
|
return 'You attend a council of the Debugger Knights';
|
||||||
|
}
|
||||||
|
|
||||||
String cinematicCacheZone4() => isKoreanLocale
|
String cinematicCacheZone4() {
|
||||||
? '많은 버그들이 기다린다. 당신이 패치하도록 선택되었다!'
|
if (isKoreanLocale) return '많은 버그들이 기다린다. 당신이 패치하도록 선택되었다!';
|
||||||
: 'Many bugs await. You are chosen to patch them!';
|
if (isJapaneseLocale) return '多くのバグが待っている。あなたがパッチを当てるよう選ばれた!';
|
||||||
|
return 'Many bugs await. You are chosen to patch them!';
|
||||||
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// 시네마틱 텍스트 - 시나리오 2: 전투
|
// 시네마틱 텍스트 - 시나리오 2: 전투
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
String cinematicCombat1() => isKoreanLocale
|
String cinematicCombat1() {
|
||||||
? '목표가 눈앞에 있지만, 치명적인 버그가 길을 막는다!'
|
if (isKoreanLocale) return '목표가 눈앞에 있지만, 치명적인 버그가 길을 막는다!';
|
||||||
: 'Your target is in sight, but a critical bug blocks your path!';
|
if (isJapaneseLocale) return 'ターゲットは目の前だが、致命的なバグが道を塞ぐ!';
|
||||||
|
return 'Your target is in sight, but a critical bug blocks your path!';
|
||||||
|
}
|
||||||
|
|
||||||
String cinematicCombat2(String nemesis) => isKoreanLocale
|
String cinematicCombat2(String nemesis) {
|
||||||
? '$nemesis와의 필사적인 디버깅 세션이 시작되다'
|
if (isKoreanLocale) return '$nemesis와의 필사적인 디버깅 세션이 시작되다';
|
||||||
: 'A desperate debugging session begins with $nemesis';
|
if (isJapaneseLocale) return '$nemesisとの必死のデバッグセッションが始まる';
|
||||||
|
return 'A desperate debugging session begins with $nemesis';
|
||||||
|
}
|
||||||
|
|
||||||
String cinematicCombatLocked(String nemesis) => isKoreanLocale
|
String cinematicCombatLocked(String nemesis) {
|
||||||
? '$nemesis와 치열한 디버깅 중'
|
if (isKoreanLocale) return '$nemesis와 치열한 디버깅 중';
|
||||||
: 'Locked in intense debugging with $nemesis';
|
if (isJapaneseLocale) return '$nemesisと激しいデバッグ中';
|
||||||
|
return 'Locked in intense debugging with $nemesis';
|
||||||
|
}
|
||||||
|
|
||||||
String cinematicCombatCorrupts(String nemesis) => isKoreanLocale
|
String cinematicCombatCorrupts(String nemesis) {
|
||||||
? '$nemesis가 당신의 스택 트레이스를 손상시키다'
|
if (isKoreanLocale) return '$nemesis가 당신의 스택 트레이스를 손상시키다';
|
||||||
: '$nemesis corrupts your stack trace';
|
if (isJapaneseLocale) return '$nemesisがあなたのスタックトレースを破損させる';
|
||||||
|
return '$nemesis corrupts your stack trace';
|
||||||
|
}
|
||||||
|
|
||||||
String cinematicCombatWorking(String nemesis) => isKoreanLocale
|
String cinematicCombatWorking(String nemesis) {
|
||||||
? '당신의 패치가 $nemesis에게 효과를 보이는 것 같다'
|
if (isKoreanLocale) return '당신의 패치가 $nemesis에게 효과를 보이는 것 같다';
|
||||||
: 'Your patch seems to be working against $nemesis';
|
if (isJapaneseLocale) return 'あなたのパッチが$nemesisに効いているようだ';
|
||||||
|
return 'Your patch seems to be working against $nemesis';
|
||||||
|
}
|
||||||
|
|
||||||
String cinematicCombatVictory(String nemesis) => isKoreanLocale
|
String cinematicCombatVictory(String nemesis) {
|
||||||
? '승리! $nemesis가 패치되었다! 복구를 위해 시스템이 재부팅된다'
|
if (isKoreanLocale) return '승리! $nemesis가 패치되었다! 복구를 위해 시스템이 재부팅된다';
|
||||||
: 'Victory! $nemesis is patched! System reboots for recovery';
|
if (isJapaneseLocale) return '勝利!$nemesisはパッチされた!復旧のためシステムが再起動する';
|
||||||
|
return 'Victory! $nemesis is patched! System reboots for recovery';
|
||||||
|
}
|
||||||
|
|
||||||
String cinematicCombatWakeUp() => isKoreanLocale
|
String cinematicCombatWakeUp() {
|
||||||
? '안전 모드에서 깨어나지만, 커널이 기다린다'
|
if (isKoreanLocale) return '안전 모드에서 깨어나지만, 커널이 기다린다';
|
||||||
: 'You wake up in a Safe Mode, but the kernel awaits';
|
if (isJapaneseLocale) return 'セーフモードで目覚めるが、カーネルが待ち構えている';
|
||||||
|
return 'You wake up in a Safe Mode, but the kernel awaits';
|
||||||
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// 시네마틱 텍스트 - 시나리오 3: 배신
|
// 시네마틱 텍스트 - 시나리오 3: 배신
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
String cinematicBetrayal1(String guy) => isKoreanLocale
|
String cinematicBetrayal1(String guy) {
|
||||||
? '안도감! $guy의 보안 서버에 도착하다'
|
if (isKoreanLocale) return '안도감! $guy의 보안 서버에 도착하다';
|
||||||
: 'What relief! You reach the secure server of $guy';
|
if (isJapaneseLocale) return '安堵!$guyのセキュアサーバーに到着する';
|
||||||
|
return 'What relief! You reach the secure server of $guy';
|
||||||
|
}
|
||||||
|
|
||||||
String cinematicBetrayal2(String guy) => isKoreanLocale
|
String cinematicBetrayal2(String guy) {
|
||||||
? '축하가 이어지고, $guy와 수상한 비밀 핸드셰이크를 나누다'
|
if (isKoreanLocale) return '축하가 이어지고, $guy와 수상한 비밀 핸드셰이크를 나누다';
|
||||||
: 'There is celebration, and a suspicious private handshake with $guy';
|
if (isJapaneseLocale) return '祝賀が続き、$guyと怪しい秘密のハンドシェイクを交わす';
|
||||||
|
return 'There is celebration, and a suspicious private handshake with $guy';
|
||||||
|
}
|
||||||
|
|
||||||
String cinematicBetrayal3(String item) => isKoreanLocale
|
String cinematicBetrayal3(String item) {
|
||||||
? '$item을 잊고 다시 가져오러 돌아가다'
|
if (isKoreanLocale) return '$item을 잊고 다시 가져오러 돌아가다';
|
||||||
: 'You forget your $item and go back to retrieve it';
|
if (isJapaneseLocale) return '$itemを忘れて取りに戻る';
|
||||||
|
return 'You forget your $item and go back to retrieve it';
|
||||||
|
}
|
||||||
|
|
||||||
String cinematicBetrayal4() => isKoreanLocale
|
String cinematicBetrayal4() {
|
||||||
? '이게 뭐지!? 손상된 패킷을 가로채다!'
|
if (isKoreanLocale) return '이게 뭐지!? 손상된 패킷을 가로채다!';
|
||||||
: 'What is this!? You intercept a corrupted packet!';
|
if (isJapaneseLocale) return 'これは何だ!?破損したパケットを傍受する!';
|
||||||
|
return 'What is this!? You intercept a corrupted packet!';
|
||||||
|
}
|
||||||
|
|
||||||
String cinematicBetrayal5(String guy) => isKoreanLocale
|
String cinematicBetrayal5(String guy) {
|
||||||
? '$guy가 글리치 신의 백도어일 수 있을까?'
|
if (isKoreanLocale) return '$guy가 글리치 신의 백도어일 수 있을까?';
|
||||||
: 'Could $guy be a backdoor for the Glitch God?';
|
if (isJapaneseLocale) return '$guyはグリッチゴッドのバックドアなのか?';
|
||||||
|
return 'Could $guy be a backdoor for the Glitch God?';
|
||||||
|
}
|
||||||
|
|
||||||
String cinematicBetrayal6() => isKoreanLocale
|
String cinematicBetrayal6() {
|
||||||
? '이 정보를 누구에게 맡길 수 있을까!? -- 바이너리 신전이다'
|
if (isKoreanLocale) return '이 정보를 누구에게 맡길 수 있을까!? -- 바이너리 신전이다';
|
||||||
: 'Who can be trusted with this intel!? -- The Binary Temple, of course';
|
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 modifierDead(String s) {
|
||||||
String modifierComatose(String s) => isKoreanLocale ? '잠복하는 $s' : 'lurking $s';
|
if (isKoreanLocale) return '쓰러진 $s';
|
||||||
String modifierCrippled(String s) => isKoreanLocale ? '흉측한 $s' : 'twisted $s';
|
if (isJapaneseLocale) return '倒れた$s';
|
||||||
String modifierSick(String s) => isKoreanLocale ? '오염된 $s' : 'tainted $s';
|
return 'fallen $s';
|
||||||
String modifierUndernourished(String s) =>
|
}
|
||||||
isKoreanLocale ? '굶주린 $s' : 'ravenous $s';
|
|
||||||
|
|
||||||
String modifierFoetal(String s) => isKoreanLocale ? '태동기 $s' : 'nascent $s';
|
String modifierComatose(String s) {
|
||||||
String modifierBaby(String s) => isKoreanLocale ? '초기형 $s' : 'fledgling $s';
|
if (isKoreanLocale) return '잠복하는 $s';
|
||||||
String modifierPreadolescent(String s) =>
|
if (isJapaneseLocale) return '潜む$s';
|
||||||
isKoreanLocale ? '진화 중인 $s' : 'evolving $s';
|
return 'lurking $s';
|
||||||
String modifierTeenage(String s) => isKoreanLocale ? '하급 $s' : 'lesser $s';
|
}
|
||||||
String modifierUnderage(String s) => isKoreanLocale ? '불완전한 $s' : 'incomplete $s';
|
|
||||||
|
|
||||||
String modifierGreater(String s) => isKoreanLocale ? '상위 $s' : 'greater $s';
|
String modifierCrippled(String s) {
|
||||||
String modifierMassive(String s) => isKoreanLocale ? '거대한 $s' : 'massive $s';
|
if (isKoreanLocale) return '흉측한 $s';
|
||||||
String modifierEnormous(String s) => isKoreanLocale ? '초거대 $s' : 'enormous $s';
|
if (isJapaneseLocale) return '歪んだ$s';
|
||||||
String modifierGiant(String s) => isKoreanLocale ? '자이언트 $s' : 'giant $s';
|
return 'twisted $s';
|
||||||
String modifierTitanic(String s) => isKoreanLocale ? '타이타닉 $s' : 'titanic $s';
|
}
|
||||||
|
|
||||||
String modifierVeteran(String s) => isKoreanLocale ? '베테랑 $s' : 'veteran $s';
|
String modifierSick(String s) {
|
||||||
String modifierBattle(String s) => isKoreanLocale ? '전투-$s' : 'Battle-$s';
|
if (isKoreanLocale) return '오염된 $s';
|
||||||
String modifierCursed(String s) => isKoreanLocale ? '저주받은 $s' : 'cursed $s';
|
if (isJapaneseLocale) return '汚染された$s';
|
||||||
String modifierWarrior(String s) => isKoreanLocale ? '전사 $s' : 'warrior $s';
|
return 'tainted $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 modifierMessianic(String s) => isKoreanLocale ? '메시아닉 $s' : 'messianic $s';
|
String modifierUndernourished(String s) {
|
||||||
String modifierImaginary(String s) => isKoreanLocale ? '상상의 $s' : 'imaginary $s';
|
if (isKoreanLocale) return '굶주린 $s';
|
||||||
String modifierPassing(String s) => isKoreanLocale ? '지나가는 $s' : 'passing $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) =>
|
String roughTimeSeconds(int seconds) {
|
||||||
isKoreanLocale ? '$seconds초' : '$seconds seconds';
|
if (isKoreanLocale) return '$seconds초';
|
||||||
|
if (isJapaneseLocale) return '$seconds秒';
|
||||||
|
return '$seconds seconds';
|
||||||
|
}
|
||||||
|
|
||||||
String roughTimeMinutes(int minutes) =>
|
String roughTimeMinutes(int minutes) {
|
||||||
isKoreanLocale ? '$minutes분' : '$minutes minutes';
|
if (isKoreanLocale) return '$minutes분';
|
||||||
|
if (isJapaneseLocale) return '$minutes分';
|
||||||
|
return '$minutes minutes';
|
||||||
|
}
|
||||||
|
|
||||||
String roughTimeHours(int hours) =>
|
String roughTimeHours(int hours) {
|
||||||
isKoreanLocale ? '$hours시간' : '$hours hours';
|
if (isKoreanLocale) return '$hours시간';
|
||||||
|
if (isJapaneseLocale) return '$hours時間';
|
||||||
|
return '$hours hours';
|
||||||
|
}
|
||||||
|
|
||||||
String roughTimeDays(int days) =>
|
String roughTimeDays(int days) {
|
||||||
isKoreanLocale ? '$days일' : '$days days';
|
if (isKoreanLocale) return '$days일';
|
||||||
|
if (isJapaneseLocale) return '$days日';
|
||||||
|
return '$days days';
|
||||||
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// 영어 문법 함수 (한국어에서는 단순화)
|
// 영어 문법 함수 (한국어/일본어에서는 단순화)
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
/// 관사 + 명사 (한국어: 수량만 표시)
|
/// 관사 + 명사 (한국어/일본어: 수량만 표시)
|
||||||
String indefiniteL10n(String s, int qty) {
|
String indefiniteL10n(String s, int qty) {
|
||||||
if (isKoreanLocale) {
|
if (isKoreanLocale) {
|
||||||
return qty == 1 ? s : '$qty $s';
|
return qty == 1 ? s : '$qty $s';
|
||||||
}
|
}
|
||||||
|
if (isJapaneseLocale) {
|
||||||
|
return qty == 1 ? s : '$qty $s';
|
||||||
|
}
|
||||||
// 영어 로직
|
// 영어 로직
|
||||||
if (qty == 1) {
|
if (qty == 1) {
|
||||||
const vowels = 'AEIOUÜaeiouü';
|
const vowels = 'AEIOUÜaeiouü';
|
||||||
@@ -238,9 +461,9 @@ String indefiniteL10n(String s, int qty) {
|
|||||||
return '$qty ${_pluralize(s)}';
|
return '$qty ${_pluralize(s)}';
|
||||||
}
|
}
|
||||||
|
|
||||||
/// the + 명사 (한국어: 그냥 명사)
|
/// the + 명사 (한국어/일본어: 그냥 명사)
|
||||||
String definiteL10n(String s, int qty) {
|
String definiteL10n(String s, int qty) {
|
||||||
if (isKoreanLocale) {
|
if (isKoreanLocale || isJapaneseLocale) {
|
||||||
return s;
|
return s;
|
||||||
}
|
}
|
||||||
// 영어 로직
|
// 영어 로직
|
||||||
@@ -266,70 +489,202 @@ String _pluralize(String s) {
|
|||||||
// impressiveGuy 관련
|
// impressiveGuy 관련
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
String impressiveGuyPattern1(String title, String race) => isKoreanLocale
|
String impressiveGuyPattern1(String title, String race) {
|
||||||
|
if (isKoreanLocale) {
|
||||||
// ignore: unnecessary_brace_in_string_interps
|
// ignore: unnecessary_brace_in_string_interps
|
||||||
? '${race}들의 $title' // 한국어 조사 연결을 위해 중괄호 필요
|
return '${race}들의 $title'; // 한국어 조사 연결을 위해 중괄호 필요
|
||||||
: 'the $title of the ${_pluralize(race)}';
|
}
|
||||||
|
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) =>
|
String impressiveGuyPattern2(String title, String name1, String name2) {
|
||||||
isKoreanLocale
|
if (isKoreanLocale) return '$name2의 $title $name1';
|
||||||
? '$name2의 $title $name1'
|
if (isJapaneseLocale) return '$name2の$title $name1';
|
||||||
: '$title $name1 of $name2';
|
return '$title $name1 of $name2';
|
||||||
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// namedMonster 관련
|
// namedMonster 관련
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
String namedMonsterFormat(String generatedName, String monsterType) =>
|
String namedMonsterFormat(String generatedName, String monsterType) {
|
||||||
isKoreanLocale
|
if (isKoreanLocale) return '$monsterType $generatedName';
|
||||||
? '$monsterType $generatedName'
|
if (isJapaneseLocale) return '$monsterType $generatedName';
|
||||||
: '$generatedName the $monsterType';
|
return '$generatedName the $monsterType';
|
||||||
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// 게임 데이터 번역 함수 (BuildContext 없이 사용)
|
// 게임 데이터 번역 함수 (BuildContext 없이 사용)
|
||||||
|
// 지원 언어: 한국어(ko), 영어(en), 일본어(ja)
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
/// 몬스터 이름 번역
|
/// 몬스터 이름 번역 (기본 + 고급 몬스터 포함)
|
||||||
String translateMonster(String englishName) =>
|
String translateMonster(String englishName) {
|
||||||
isKoreanLocale ? (monsterTranslationsKo[englishName] ?? englishName) : englishName;
|
if (isKoreanLocale) {
|
||||||
|
return monsterTranslationsKo[englishName] ??
|
||||||
|
advancedMonsterTranslationsKo[englishName] ??
|
||||||
|
englishName;
|
||||||
|
}
|
||||||
|
if (isJapaneseLocale) {
|
||||||
|
return monsterTranslationsJa[englishName] ??
|
||||||
|
advancedMonsterTranslationsJa[englishName] ??
|
||||||
|
englishName;
|
||||||
|
}
|
||||||
|
return englishName;
|
||||||
|
}
|
||||||
|
|
||||||
/// 종족 이름 번역
|
/// 종족 이름 번역
|
||||||
String translateRace(String englishName) =>
|
String translateRace(String englishName) {
|
||||||
isKoreanLocale ? (raceTranslationsKo[englishName] ?? englishName) : englishName;
|
if (isKoreanLocale) return raceTranslationsKo[englishName] ?? englishName;
|
||||||
|
if (isJapaneseLocale) return raceTranslationsJa[englishName] ?? englishName;
|
||||||
|
return englishName;
|
||||||
|
}
|
||||||
|
|
||||||
/// 직업 이름 번역
|
/// 직업 이름 번역
|
||||||
String translateKlass(String englishName) =>
|
String translateKlass(String englishName) {
|
||||||
isKoreanLocale ? (klassTranslationsKo[englishName] ?? englishName) : englishName;
|
if (isKoreanLocale) return klassTranslationsKo[englishName] ?? englishName;
|
||||||
|
if (isJapaneseLocale) return klassTranslationsJa[englishName] ?? englishName;
|
||||||
|
return englishName;
|
||||||
|
}
|
||||||
|
|
||||||
/// 칭호 이름 번역
|
/// 칭호 이름 번역
|
||||||
String translateTitle(String englishName) =>
|
String translateTitle(String englishName) {
|
||||||
isKoreanLocale ? (titleTranslationsKo[englishName] ?? englishName) : englishName;
|
if (isKoreanLocale) return titleTranslationsKo[englishName] ?? englishName;
|
||||||
|
if (isJapaneseLocale) return titleTranslationsJa[englishName] ?? englishName;
|
||||||
|
return englishName;
|
||||||
|
}
|
||||||
|
|
||||||
/// 인상적인 칭호 번역 (impressiveTitles용)
|
/// 인상적인 칭호 번역 (impressiveTitles용)
|
||||||
String translateImpressiveTitle(String englishName) =>
|
String translateImpressiveTitle(String englishName) {
|
||||||
isKoreanLocale ? (impressiveTitleTranslationsKo[englishName] ?? englishName) : englishName;
|
if (isKoreanLocale) {
|
||||||
|
return impressiveTitleTranslationsKo[englishName] ?? englishName;
|
||||||
|
}
|
||||||
|
if (isJapaneseLocale) {
|
||||||
|
return impressiveTitleTranslationsJa[englishName] ?? englishName;
|
||||||
|
}
|
||||||
|
return englishName;
|
||||||
|
}
|
||||||
|
|
||||||
/// 특수 아이템 이름 번역
|
/// 특수 아이템 이름 번역
|
||||||
String translateSpecial(String englishName) =>
|
String translateSpecial(String englishName) {
|
||||||
isKoreanLocale ? (specialTranslationsKo[englishName] ?? englishName) : englishName;
|
if (isKoreanLocale) return specialTranslationsKo[englishName] ?? englishName;
|
||||||
|
if (isJapaneseLocale) return specialTranslationsJa[englishName] ?? englishName;
|
||||||
|
return englishName;
|
||||||
|
}
|
||||||
|
|
||||||
/// 아이템 속성 이름 번역
|
/// 아이템 속성 이름 번역 (기본 + 추가 속성 포함)
|
||||||
String translateItemAttrib(String englishName) =>
|
String translateItemAttrib(String englishName) {
|
||||||
isKoreanLocale ? (itemAttribTranslationsKo[englishName] ?? englishName) : englishName;
|
if (isKoreanLocale) {
|
||||||
|
return itemAttribTranslationsKo[englishName] ??
|
||||||
|
additionalItemAttribTranslationsKo[englishName] ??
|
||||||
|
englishName;
|
||||||
|
}
|
||||||
|
if (isJapaneseLocale) {
|
||||||
|
return itemAttribTranslationsJa[englishName] ??
|
||||||
|
additionalItemAttribTranslationsJa[englishName] ??
|
||||||
|
englishName;
|
||||||
|
}
|
||||||
|
return englishName;
|
||||||
|
}
|
||||||
|
|
||||||
/// 아이템 "~의" 접미사 번역
|
/// 아이템 "~의" 접미사 번역 (기본 + 추가 포함)
|
||||||
String translateItemOf(String englishName) =>
|
String translateItemOf(String englishName) {
|
||||||
isKoreanLocale ? (itemOfsTranslationsKo[englishName] ?? englishName) : englishName;
|
if (isKoreanLocale) {
|
||||||
|
return itemOfsTranslationsKo[englishName] ??
|
||||||
|
additionalItemOfsTranslationsKo[englishName] ??
|
||||||
|
englishName;
|
||||||
|
}
|
||||||
|
if (isJapaneseLocale) {
|
||||||
|
return itemOfsTranslationsJa[englishName] ??
|
||||||
|
additionalItemOfsTranslationsJa[englishName] ??
|
||||||
|
englishName;
|
||||||
|
}
|
||||||
|
return englishName;
|
||||||
|
}
|
||||||
|
|
||||||
/// 단순 아이템 번역
|
/// 단순 아이템 번역 (기본 + 추가 드롭 포함)
|
||||||
String translateBoringItem(String englishName) =>
|
String translateBoringItem(String englishName) {
|
||||||
isKoreanLocale ? (boringItemTranslationsKo[englishName] ?? englishName) : 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 조합)
|
/// interestingItem 번역 (attrib + special 조합)
|
||||||
/// 예: "Golden Iterator" → "황금 이터레이터"
|
/// 예: "Golden Iterator" → "황금 이터레이터" / "黄金のイテレーター"
|
||||||
String translateInterestingItem(String attrib, String special) {
|
String translateInterestingItem(String attrib, String special) {
|
||||||
if (!isKoreanLocale) return '$attrib $special';
|
if (isKoreanLocale) {
|
||||||
final translatedAttrib = itemAttribTranslationsKo[attrib] ?? attrib;
|
final translatedAttrib = itemAttribTranslationsKo[attrib] ??
|
||||||
final translatedSpecial = specialTranslationsKo[special] ?? special;
|
additionalItemAttribTranslationsKo[attrib] ??
|
||||||
return '$translatedAttrib $translatedSpecial';
|
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;
|
||||||
}
|
}
|
||||||
|
|||||||
1521
lib/data/game_translations_ja.dart
Normal file
1521
lib/data/game_translations_ja.dart
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,31 @@
|
|||||||
|
// ============================================================================
|
||||||
// ASCII NEVER DIE 한국어 번역 데이터
|
// 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<String, String> raceTranslationsKo = {
|
const Map<String, String> raceTranslationsKo = {
|
||||||
@@ -1101,3 +1127,415 @@ const Map<String, String> dropItemTranslationsKo = {
|
|||||||
'privilege escape': '권한 탈출',
|
'privilege escape': '권한 탈출',
|
||||||
'heap overflow': '힙 오버플로우',
|
'heap overflow': '힙 오버플로우',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// 스토리/시네마틱 한국어 번역 (Story/Cinematic Translations)
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
/// Act 제목 한국어 번역
|
||||||
|
const Map<String, String> 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<String, String> actBossTranslationsKo = {
|
||||||
|
'BOSS: Stack Overflow Dragon': '보스: 스택 오버플로우 드래곤',
|
||||||
|
'BOSS: Heap Corruption Hydra': '보스: 힙 손상 히드라',
|
||||||
|
'BOSS: Kernel Panic Titan': '보스: 커널 패닉 타이탄',
|
||||||
|
'BOSS: Zero Day Leviathan': '보스: 제로데이 리바이어던',
|
||||||
|
'BOSS: The Primordial Glitch': '보스: 태초의 글리치',
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Act별 시작 퀘스트 한국어 번역
|
||||||
|
const Map<String, String> 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<String, String> 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<String, String> 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<String, String> 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<String, String> 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<String, String> 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<String, String> additionalShieldTranslationsKo = {
|
||||||
|
'Neural Defense Grid': '신경 방어 그리드',
|
||||||
|
'Singularity Absorber': '특이점 흡수기',
|
||||||
|
'Time Dilation Field': '시간 확장 필드',
|
||||||
|
'Reality Anchor': '현실 닻',
|
||||||
|
'Multiverse Barrier': '다중우주 장벽',
|
||||||
|
'Cosmic Dampener': '우주 완충기',
|
||||||
|
'Entropy Shield': '엔트로피 실드',
|
||||||
|
};
|
||||||
|
|
||||||
|
/// 추가 갑옷 번역
|
||||||
|
const Map<String, String> additionalArmorTranslationsKo = {
|
||||||
|
'Blockchain Platemail': '블록체인 판금갑옷',
|
||||||
|
'Neural Network Mesh': '신경망 메시',
|
||||||
|
'AI Firewall': 'AI 방화벽',
|
||||||
|
'Quantum Shield Matrix': '양자 실드 매트릭스',
|
||||||
|
'Singularity Barrier': '특이점 장벽',
|
||||||
|
'Multiverse Armor': '다중우주 갑옷',
|
||||||
|
};
|
||||||
|
|
||||||
|
/// 추가 아이템 속성 번역
|
||||||
|
const Map<String, String> 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<String, String> 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<String, String> get allMonsterTranslationsKo => {
|
||||||
|
...monsterTranslationsKo,
|
||||||
|
...advancedMonsterTranslationsKo,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// 모든 아이템 속성 번역을 통합하여 반환
|
||||||
|
Map<String, String> get allItemAttribTranslationsKo => {
|
||||||
|
...itemAttribTranslationsKo,
|
||||||
|
...additionalItemAttribTranslationsKo,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// 모든 아이템 접미사("~의") 번역을 통합하여 반환
|
||||||
|
Map<String, String> get allItemOfsTranslationsKo => {
|
||||||
|
...itemOfsTranslationsKo,
|
||||||
|
...additionalItemOfsTranslationsKo,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// 모든 드롭 아이템 번역을 통합하여 반환
|
||||||
|
Map<String, String> get allDropTranslationsKo => {
|
||||||
|
...boringItemTranslationsKo,
|
||||||
|
...dropItemTranslationsKo,
|
||||||
|
...additionalDropTranslationsKo,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// 모든 갑옷 번역을 통합하여 반환
|
||||||
|
Map<String, String> get allArmorTranslationsKo => {
|
||||||
|
...armorTranslationsKo,
|
||||||
|
...additionalArmorTranslationsKo,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// 모든 방패 번역을 통합하여 반환
|
||||||
|
Map<String, String> get allShieldTranslationsKo => {
|
||||||
|
...shieldTranslationsKo,
|
||||||
|
...additionalShieldTranslationsKo,
|
||||||
|
};
|
||||||
|
|||||||
187
lib/data/potion_data.dart
Normal file
187
lib/data/potion_data.dart
Normal file
@@ -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<Potion> all = [
|
||||||
|
// HP 물약
|
||||||
|
minorHealthPatch,
|
||||||
|
healthPatch,
|
||||||
|
majorHealthPatch,
|
||||||
|
superHealthPatch,
|
||||||
|
ultraHealthPatch,
|
||||||
|
// MP 물약
|
||||||
|
minorManaCache,
|
||||||
|
manaCache,
|
||||||
|
majorManaCache,
|
||||||
|
superManaCache,
|
||||||
|
ultraManaCache,
|
||||||
|
];
|
||||||
|
|
||||||
|
/// HP 물약 목록
|
||||||
|
static List<Potion> get hpPotions =>
|
||||||
|
all.where((p) => p.type == PotionType.hp).toList();
|
||||||
|
|
||||||
|
/// MP 물약 목록
|
||||||
|
static List<Potion> 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -78,6 +78,100 @@ class SkillData {
|
|||||||
damageMultiplier: 1.8,
|
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<Skill> allSkills = [
|
static const List<Skill> allSkills = [
|
||||||
// 공격 스킬
|
// 공격 스킬 (단발성)
|
||||||
debugStrike,
|
debugStrike,
|
||||||
nullPointer,
|
nullPointer,
|
||||||
memoryLeak,
|
memoryLeak,
|
||||||
stackOverflow,
|
stackOverflow,
|
||||||
coreDump,
|
coreDump,
|
||||||
kernelPanic,
|
kernelPanic,
|
||||||
|
// DOT 스킬
|
||||||
|
memoryCorruption,
|
||||||
|
infiniteLoop,
|
||||||
|
thermalThrottle,
|
||||||
|
raceCondition,
|
||||||
|
system32Delete,
|
||||||
// 회복 스킬
|
// 회복 스킬
|
||||||
quickFix,
|
quickFix,
|
||||||
hotReload,
|
hotReload,
|
||||||
@@ -192,6 +292,10 @@ class SkillData {
|
|||||||
firewall,
|
firewall,
|
||||||
];
|
];
|
||||||
|
|
||||||
|
/// DOT 스킬 목록
|
||||||
|
static List<Skill> get dotSkills =>
|
||||||
|
allSkills.where((s) => s.isDot).toList();
|
||||||
|
|
||||||
/// ID로 스킬 찾기
|
/// ID로 스킬 찾기
|
||||||
static Skill? getSkillById(String id) {
|
static Skill? getSkillById(String id) {
|
||||||
for (final skill in allSkills) {
|
for (final skill in allSkills) {
|
||||||
|
|||||||
@@ -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/equipment_slot.dart';
|
||||||
import 'package:askiineverdie/src/core/model/game_state.dart';
|
import 'package:askiineverdie/src/core/model/game_state.dart';
|
||||||
import 'package:askiineverdie/src/core/model/pq_config.dart';
|
import 'package:askiineverdie/src/core/model/pq_config.dart';
|
||||||
@@ -10,12 +11,24 @@ class GameMutations {
|
|||||||
final PqConfig config;
|
final PqConfig config;
|
||||||
|
|
||||||
/// 장비 획득 (원본 Main.pas:791-830 WinEquip)
|
/// 장비 획득 (원본 Main.pas:791-830 WinEquip)
|
||||||
|
///
|
||||||
/// [slotIndex]: 0-10 (원본 Equips.Items.Count = 11)
|
/// [slotIndex]: 0-10 (원본 Equips.Items.Count = 11)
|
||||||
|
/// ItemService를 사용하여 스탯과 공속을 가진 장비 생성
|
||||||
GameState winEquipByIndex(GameState state, int level, int slotIndex) {
|
GameState winEquipByIndex(GameState state, int level, int slotIndex) {
|
||||||
final rng = state.rng;
|
final rng = state.rng;
|
||||||
final name = pq_logic.winEquip(config, rng, level, slotIndex);
|
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
|
final updatedEquip = state.equipment
|
||||||
.setByIndex(slotIndex, name)
|
.setItemByIndex(slotIndex, newItem)
|
||||||
.copyWith(bestIndex: slotIndex);
|
.copyWith(bestIndex: slotIndex);
|
||||||
return state.copyWith(rng: rng, equipment: updatedEquip);
|
return state.copyWith(rng: rng, equipment: updatedEquip);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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) {
|
ItemStats _generateWeaponStats(int baseValue, ItemRarity rarity) {
|
||||||
final criBonus = rarity.index >= ItemRarity.rare.index ? 0.02 + rarity.index * 0.01 : 0.0;
|
final criBonus = rarity.index >= ItemRarity.rare.index
|
||||||
final parryBonus = rarity.index >= ItemRarity.uncommon.index ? 0.01 + rarity.index * 0.005 : 0.0;
|
? 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(
|
return ItemStats(
|
||||||
atk: baseValue,
|
atk: adjustedAtk,
|
||||||
criRate: criBonus,
|
criRate: criBonus,
|
||||||
parryRate: parryBonus,
|
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({
|
bool shouldEquip({
|
||||||
required EquipmentItem newItem,
|
required EquipmentItem newItem,
|
||||||
required EquipmentItem currentItem,
|
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;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
497
lib/src/core/engine/potion_service.dart
Normal file
497
lib/src/core/engine/potion_service.dart
Normal file
@@ -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,
|
||||||
|
}
|
||||||
@@ -4,6 +4,7 @@ import 'package:askiineverdie/data/game_text_l10n.dart' as l10n;
|
|||||||
import 'package:askiineverdie/data/skill_data.dart';
|
import 'package:askiineverdie/data/skill_data.dart';
|
||||||
import 'package:askiineverdie/src/core/engine/combat_calculator.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/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/reward_service.dart';
|
||||||
import 'package:askiineverdie/src/core/engine/skill_service.dart';
|
import 'package:askiineverdie/src/core/engine/skill_service.dart';
|
||||||
import 'package:askiineverdie/src/core/model/combat_event.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/equipment_slot.dart';
|
||||||
import 'package:askiineverdie/src/core/model/game_state.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/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/pq_config.dart';
|
||||||
|
import 'package:askiineverdie/src/core/model/skill.dart';
|
||||||
import 'package:askiineverdie/src/core/util/pq_logic.dart' as pq_logic;
|
import 'package:askiineverdie/src/core/util/pq_logic.dart' as pq_logic;
|
||||||
|
|
||||||
class ProgressTickResult {
|
class ProgressTickResult {
|
||||||
@@ -186,9 +189,10 @@ class ProgressService {
|
|||||||
? progress.task.max
|
? progress.task.max
|
||||||
: uncapped;
|
: uncapped;
|
||||||
|
|
||||||
// 킬 태스크 중 전투 진행 (스킬 자동 사용 포함)
|
// 킬 태스크 중 전투 진행 (스킬 자동 사용, DOT, 물약 포함)
|
||||||
var updatedCombat = progress.currentCombat;
|
var updatedCombat = progress.currentCombat;
|
||||||
var updatedSkillSystem = nextState.skillSystem;
|
var updatedSkillSystem = nextState.skillSystem;
|
||||||
|
var updatedPotionInventory = nextState.potionInventory;
|
||||||
if (progress.currentTask.type == TaskType.kill && updatedCombat != null && updatedCombat.isActive) {
|
if (progress.currentTask.type == TaskType.kill && updatedCombat != null && updatedCombat.isActive) {
|
||||||
final combatResult = _processCombatTickWithSkills(
|
final combatResult = _processCombatTickWithSkills(
|
||||||
nextState,
|
nextState,
|
||||||
@@ -198,6 +202,9 @@ class ProgressService {
|
|||||||
);
|
);
|
||||||
updatedCombat = combatResult.combat;
|
updatedCombat = combatResult.combat;
|
||||||
updatedSkillSystem = combatResult.skillSystem;
|
updatedSkillSystem = combatResult.skillSystem;
|
||||||
|
if (combatResult.potionInventory != null) {
|
||||||
|
updatedPotionInventory = combatResult.potionInventory!;
|
||||||
|
}
|
||||||
|
|
||||||
// Phase 4: 플레이어 사망 체크
|
// Phase 4: 플레이어 사망 체크
|
||||||
if (!updatedCombat.playerStats.isAlive) {
|
if (!updatedCombat.playerStats.isAlive) {
|
||||||
@@ -216,7 +223,11 @@ class ProgressService {
|
|||||||
currentCombat: updatedCombat,
|
currentCombat: updatedCombat,
|
||||||
);
|
);
|
||||||
nextState = _recalculateEncumbrance(
|
nextState = _recalculateEncumbrance(
|
||||||
nextState.copyWith(progress: progress, skillSystem: updatedSkillSystem),
|
nextState.copyWith(
|
||||||
|
progress: progress,
|
||||||
|
skillSystem: updatedSkillSystem,
|
||||||
|
potionInventory: updatedPotionInventory,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
return ProgressTickResult(state: nextState);
|
return ProgressTickResult(state: nextState);
|
||||||
}
|
}
|
||||||
@@ -245,11 +256,33 @@ class ProgressService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 전리품 획득 (원본 Main.pas:625-630)
|
// 전리품 획득 (원본 Main.pas:625-630)
|
||||||
nextState = _winLoot(nextState);
|
final lootResult = _winLoot(nextState);
|
||||||
|
nextState = lootResult.state;
|
||||||
|
|
||||||
// 전투 상태 초기화
|
// 물약 드랍 시 전투 로그에 이벤트 추가
|
||||||
progress = nextState.progress.copyWith(currentCombat: null);
|
var combatForReset = progress.currentCombat;
|
||||||
nextState = nextState.copyWith(progress: progress);
|
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)
|
// 시장/판매/구매 태스크 완료 시 처리 (원본 Main.pas:631-649)
|
||||||
@@ -728,39 +761,60 @@ class ProgressService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// 킬 태스크 완료 시 전리품 획득 (원본 Main.pas:625-630)
|
/// 킬 태스크 완료 시 전리품 획득 (원본 Main.pas:625-630)
|
||||||
GameState _winLoot(GameState state) {
|
/// 전리품 획득 결과
|
||||||
|
///
|
||||||
|
/// [state] 업데이트된 게임 상태
|
||||||
|
/// [droppedPotion] 드랍된 물약 (없으면 null)
|
||||||
|
({GameState state, Potion? droppedPotion}) _winLoot(GameState state) {
|
||||||
final taskInfo = state.progress.currentTask;
|
final taskInfo = state.progress.currentTask;
|
||||||
final monsterPart = taskInfo.monsterPart ?? '';
|
final monsterPart = taskInfo.monsterPart ?? '';
|
||||||
final monsterBaseName = taskInfo.monsterBaseName ?? '';
|
final monsterBaseName = taskInfo.monsterBaseName ?? '';
|
||||||
|
|
||||||
|
var resultState = state;
|
||||||
|
|
||||||
// 부위가 '*'이면 WinItem 호출 (특수 아이템)
|
// 부위가 '*'이면 WinItem 호출 (특수 아이템)
|
||||||
if (monsterPart == '*') {
|
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) {
|
final items = [...resultState.inventory.items];
|
||||||
return state;
|
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) + ' ' +
|
resultState = resultState.copyWith(
|
||||||
// ProperCase(Split(fTask.Caption,3))), 1);
|
inventory: resultState.inventory.copyWith(items: items),
|
||||||
// 예: "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,
|
|
||||||
);
|
);
|
||||||
} 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)
|
/// 첫 글자만 대문자로 변환 (원본 ProperCase)
|
||||||
@@ -796,6 +850,22 @@ class ProgressService {
|
|||||||
final slotIndex = nextState.rng.nextInt(Equipment.slotCount);
|
final slotIndex = nextState.rng.nextInt(Equipment.slotCount);
|
||||||
nextState = mutations.winEquipByIndex(nextState, level, slotIndex);
|
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;
|
return nextState;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -864,25 +934,30 @@ class ProgressService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 전투 틱 처리 (스킬 자동 사용 포함)
|
/// 전투 틱 처리 (스킬 자동 사용, DOT, 물약 포함)
|
||||||
///
|
///
|
||||||
/// [state] 현재 게임 상태
|
/// [state] 현재 게임 상태
|
||||||
/// [combat] 현재 전투 상태
|
/// [combat] 현재 전투 상태
|
||||||
/// [skillSystem] 스킬 시스템 상태
|
/// [skillSystem] 스킬 시스템 상태
|
||||||
/// [elapsedMs] 경과 시간 (밀리초)
|
/// [elapsedMs] 경과 시간 (밀리초)
|
||||||
/// Returns: 업데이트된 전투 상태 및 스킬 시스템 상태
|
/// Returns: 업데이트된 전투 상태, 스킬 시스템 상태, 물약 인벤토리
|
||||||
({CombatState combat, SkillSystemState skillSystem}) _processCombatTickWithSkills(
|
({
|
||||||
|
CombatState combat,
|
||||||
|
SkillSystemState skillSystem,
|
||||||
|
PotionInventory? potionInventory,
|
||||||
|
}) _processCombatTickWithSkills(
|
||||||
GameState state,
|
GameState state,
|
||||||
CombatState combat,
|
CombatState combat,
|
||||||
SkillSystemState skillSystem,
|
SkillSystemState skillSystem,
|
||||||
int elapsedMs,
|
int elapsedMs,
|
||||||
) {
|
) {
|
||||||
if (!combat.isActive || combat.isCombatOver) {
|
if (!combat.isActive || combat.isCombatOver) {
|
||||||
return (combat: combat, skillSystem: skillSystem);
|
return (combat: combat, skillSystem: skillSystem, potionInventory: null);
|
||||||
}
|
}
|
||||||
|
|
||||||
final calculator = CombatCalculator(rng: state.rng);
|
final calculator = CombatCalculator(rng: state.rng);
|
||||||
final skillService = SkillService(rng: state.rng);
|
final skillService = SkillService(rng: state.rng);
|
||||||
|
final potionService = const PotionService();
|
||||||
var playerStats = combat.playerStats;
|
var playerStats = combat.playerStats;
|
||||||
var monsterStats = combat.monsterStats;
|
var monsterStats = combat.monsterStats;
|
||||||
var playerAccumulator = combat.playerAttackAccumulatorMs + elapsedMs;
|
var playerAccumulator = combat.playerAttackAccumulatorMs + elapsedMs;
|
||||||
@@ -891,11 +966,90 @@ class ProgressService {
|
|||||||
var totalDamageTaken = combat.totalDamageTaken;
|
var totalDamageTaken = combat.totalDamageTaken;
|
||||||
var turnsElapsed = combat.turnsElapsed;
|
var turnsElapsed = combat.turnsElapsed;
|
||||||
var updatedSkillSystem = skillSystem;
|
var updatedSkillSystem = skillSystem;
|
||||||
|
var activeDoTs = [...combat.activeDoTs];
|
||||||
|
var usedPotionTypes = {...combat.usedPotionTypes};
|
||||||
|
PotionInventory? updatedPotionInventory;
|
||||||
|
|
||||||
// 새 전투 이벤트 수집
|
// 새 전투 이벤트 수집
|
||||||
final newEvents = <CombatEvent>[];
|
final newEvents = <CombatEvent>[];
|
||||||
final timestamp = updatedSkillSystem.elapsedMs;
|
final timestamp = updatedSkillSystem.elapsedMs;
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// DOT 틱 처리
|
||||||
|
// =========================================================================
|
||||||
|
var dotDamageThisTick = 0;
|
||||||
|
final updatedDoTs = <DotEffect>[];
|
||||||
|
|
||||||
|
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) {
|
if (playerAccumulator >= playerStats.attackDelayMs) {
|
||||||
// 스킬 자동 선택
|
// 스킬 자동 선택
|
||||||
@@ -912,6 +1066,7 @@ class ProgressService {
|
|||||||
monster: monsterStats,
|
monster: monsterStats,
|
||||||
skillSystem: updatedSkillSystem,
|
skillSystem: updatedSkillSystem,
|
||||||
availableSkillIds: availableSkillIds,
|
availableSkillIds: availableSkillIds,
|
||||||
|
activeDoTs: activeDoTs,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (selectedSkill != null && selectedSkill.isAttack) {
|
if (selectedSkill != null && selectedSkill.isAttack) {
|
||||||
@@ -934,6 +1089,30 @@ class ProgressService {
|
|||||||
damage: skillResult.result.damage,
|
damage: skillResult.result.damage,
|
||||||
targetName: monsterStats.name,
|
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) {
|
} else if (selectedSkill != null && selectedSkill.isHeal) {
|
||||||
// 회복 스킬 사용
|
// 회복 스킬 사용
|
||||||
final skillResult = skillService.useHealSkill(
|
final skillResult = skillService.useHealSkill(
|
||||||
@@ -1053,8 +1232,11 @@ class ProgressService {
|
|||||||
turnsElapsed: turnsElapsed,
|
turnsElapsed: turnsElapsed,
|
||||||
isActive: isActive,
|
isActive: isActive,
|
||||||
recentEvents: recentEvents,
|
recentEvents: recentEvents,
|
||||||
|
activeDoTs: activeDoTs,
|
||||||
|
usedPotionTypes: usedPotionTypes,
|
||||||
),
|
),
|
||||||
skillSystem: updatedSkillSystem,
|
skillSystem: updatedSkillSystem,
|
||||||
|
potionInventory: updatedPotionInventory,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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% → 회복 스킬
|
/// 1. HP < 30% → 회복 스킬
|
||||||
/// 2. 보스전 (레벨 차이 10 이상) → 가장 강력한 공격 스킬
|
/// 2. 몬스터 HP > 50% & DOT 없음 → DOT 스킬 (장기전 유리)
|
||||||
/// 3. 일반 전투 → MP 효율이 좋은 스킬
|
/// 3. 보스전 (레벨 차이 10 이상) → 가장 강력한 공격 스킬
|
||||||
/// 4. MP < 20% → null (일반 공격)
|
/// 4. 일반 전투 → MP 효율이 좋은 스킬
|
||||||
|
/// 5. MP < 20% → null (일반 공격)
|
||||||
Skill? selectAutoSkill({
|
Skill? selectAutoSkill({
|
||||||
required CombatStats player,
|
required CombatStats player,
|
||||||
required MonsterCombatStats monster,
|
required MonsterCombatStats monster,
|
||||||
required SkillSystemState skillSystem,
|
required SkillSystemState skillSystem,
|
||||||
required List<String> availableSkillIds,
|
required List<String> availableSkillIds,
|
||||||
|
List<DotEffect> activeDoTs = const [],
|
||||||
}) {
|
}) {
|
||||||
final currentMp = player.mpCurrent;
|
final currentMp = player.mpCurrent;
|
||||||
final mpRatio = player.mpRatio;
|
final mpRatio = player.mpRatio;
|
||||||
@@ -225,6 +281,12 @@ class SkillService {
|
|||||||
if (healSkill != null) return healSkill;
|
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;
|
final isBossFight = monster.level >= 10 && monster.hpRatio > 0.5;
|
||||||
|
|
||||||
@@ -237,6 +299,28 @@ class SkillService {
|
|||||||
return _findEfficientAttackSkill(availableSkills);
|
return _findEfficientAttackSkill(availableSkills);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 가장 좋은 DOT 스킬 찾기
|
||||||
|
///
|
||||||
|
/// 예상 총 데미지 (틱 × 데미지) 기준으로 선택
|
||||||
|
Skill? _findBestDotSkill(List<Skill> 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<Skill> skills, int currentMp) {
|
Skill? _findBestHealSkill(List<Skill> skills, int currentMp) {
|
||||||
final healSkills = skills
|
final healSkills = skills
|
||||||
|
|||||||
@@ -26,6 +26,15 @@ enum CombatEventType {
|
|||||||
|
|
||||||
/// 플레이어 버프
|
/// 플레이어 버프
|
||||||
playerBuff,
|
playerBuff,
|
||||||
|
|
||||||
|
/// DOT 틱 데미지
|
||||||
|
dotTick,
|
||||||
|
|
||||||
|
/// 물약 사용
|
||||||
|
playerPotion,
|
||||||
|
|
||||||
|
/// 물약 드랍
|
||||||
|
potionDrop,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 전투 이벤트 (Combat Event)
|
/// 전투 이벤트 (Combat Event)
|
||||||
@@ -188,4 +197,51 @@ class CombatEvent {
|
|||||||
skillName: skillName,
|
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',
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import 'package:askiineverdie/src/core/model/combat_event.dart';
|
import 'package:askiineverdie/src/core/model/combat_event.dart';
|
||||||
import 'package:askiineverdie/src/core/model/combat_stats.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/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.turnsElapsed,
|
||||||
required this.isActive,
|
required this.isActive,
|
||||||
this.recentEvents = const [],
|
this.recentEvents = const [],
|
||||||
|
this.activeDoTs = const [],
|
||||||
|
this.usedPotionTypes = const {},
|
||||||
});
|
});
|
||||||
|
|
||||||
/// 플레이어 전투 스탯
|
/// 플레이어 전투 스탯
|
||||||
@@ -46,6 +50,12 @@ class CombatState {
|
|||||||
/// 최근 전투 이벤트 목록 (최대 10개)
|
/// 최근 전투 이벤트 목록 (최대 10개)
|
||||||
final List<CombatEvent> recentEvents;
|
final List<CombatEvent> recentEvents;
|
||||||
|
|
||||||
|
/// 활성 DOT 효과 목록
|
||||||
|
final List<DotEffect> activeDoTs;
|
||||||
|
|
||||||
|
/// 이번 전투에서 사용한 물약 종류 (종류별 1회 제한)
|
||||||
|
final Set<PotionType> usedPotionTypes;
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// 유틸리티
|
// 유틸리티
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
@@ -65,6 +75,19 @@ class CombatState {
|
|||||||
/// 몬스터 HP 비율
|
/// 몬스터 HP 비율
|
||||||
double get monsterHpRatio => monsterStats.hpRatio;
|
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({
|
CombatState copyWith({
|
||||||
CombatStats? playerStats,
|
CombatStats? playerStats,
|
||||||
MonsterCombatStats? monsterStats,
|
MonsterCombatStats? monsterStats,
|
||||||
@@ -75,6 +98,8 @@ class CombatState {
|
|||||||
int? turnsElapsed,
|
int? turnsElapsed,
|
||||||
bool? isActive,
|
bool? isActive,
|
||||||
List<CombatEvent>? recentEvents,
|
List<CombatEvent>? recentEvents,
|
||||||
|
List<DotEffect>? activeDoTs,
|
||||||
|
Set<PotionType>? usedPotionTypes,
|
||||||
}) {
|
}) {
|
||||||
return CombatState(
|
return CombatState(
|
||||||
playerStats: playerStats ?? this.playerStats,
|
playerStats: playerStats ?? this.playerStats,
|
||||||
@@ -88,6 +113,8 @@ class CombatState {
|
|||||||
turnsElapsed: turnsElapsed ?? this.turnsElapsed,
|
turnsElapsed: turnsElapsed ?? this.turnsElapsed,
|
||||||
isActive: isActive ?? this.isActive,
|
isActive: isActive ?? this.isActive,
|
||||||
recentEvents: recentEvents ?? this.recentEvents,
|
recentEvents: recentEvents ?? this.recentEvents,
|
||||||
|
activeDoTs: activeDoTs ?? this.activeDoTs,
|
||||||
|
usedPotionTypes: usedPotionTypes ?? this.usedPotionTypes,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -270,9 +270,13 @@ class CombatStats {
|
|||||||
final baseParryRate = (effectiveDex + effectiveStr) * 0.002;
|
final baseParryRate = (effectiveDex + effectiveStr) * 0.002;
|
||||||
final parryRate = (baseParryRate + equipStats.parryRate).clamp(0.0, 0.4);
|
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 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: 기본 + 장비 보너스
|
// HP/MP: 기본 + 장비 보너스
|
||||||
var totalHpMax = stats.hpMax + equipStats.hpBonus;
|
var totalHpMax = stats.hpMax + equipStats.hpBonus;
|
||||||
|
|||||||
@@ -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_item.dart';
|
||||||
import 'package:askiineverdie/src/core/model/equipment_slot.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/item_stats.dart';
|
||||||
|
import 'package:askiineverdie/src/core/model/potion.dart';
|
||||||
import 'package:askiineverdie/src/core/model/skill.dart';
|
import 'package:askiineverdie/src/core/model/skill.dart';
|
||||||
import 'package:askiineverdie/src/core/util/deterministic_random.dart';
|
import 'package:askiineverdie/src/core/util/deterministic_random.dart';
|
||||||
|
|
||||||
@@ -23,6 +24,7 @@ class GameState {
|
|||||||
ProgressState? progress,
|
ProgressState? progress,
|
||||||
QueueState? queue,
|
QueueState? queue,
|
||||||
SkillSystemState? skillSystem,
|
SkillSystemState? skillSystem,
|
||||||
|
PotionInventory? potionInventory,
|
||||||
this.deathInfo,
|
this.deathInfo,
|
||||||
}) : rng = DeterministicRandom.clone(rng),
|
}) : rng = DeterministicRandom.clone(rng),
|
||||||
traits = traits ?? Traits.empty(),
|
traits = traits ?? Traits.empty(),
|
||||||
@@ -32,7 +34,8 @@ class GameState {
|
|||||||
spellBook = spellBook ?? SpellBook.empty(),
|
spellBook = spellBook ?? SpellBook.empty(),
|
||||||
progress = progress ?? ProgressState.empty(),
|
progress = progress ?? ProgressState.empty(),
|
||||||
queue = queue ?? QueueState.empty(),
|
queue = queue ?? QueueState.empty(),
|
||||||
skillSystem = skillSystem ?? SkillSystemState.empty();
|
skillSystem = skillSystem ?? SkillSystemState.empty(),
|
||||||
|
potionInventory = potionInventory ?? const PotionInventory();
|
||||||
|
|
||||||
factory GameState.withSeed({
|
factory GameState.withSeed({
|
||||||
required int seed,
|
required int seed,
|
||||||
@@ -44,6 +47,7 @@ class GameState {
|
|||||||
ProgressState? progress,
|
ProgressState? progress,
|
||||||
QueueState? queue,
|
QueueState? queue,
|
||||||
SkillSystemState? skillSystem,
|
SkillSystemState? skillSystem,
|
||||||
|
PotionInventory? potionInventory,
|
||||||
DeathInfo? deathInfo,
|
DeathInfo? deathInfo,
|
||||||
}) {
|
}) {
|
||||||
return GameState(
|
return GameState(
|
||||||
@@ -56,6 +60,7 @@ class GameState {
|
|||||||
progress: progress,
|
progress: progress,
|
||||||
queue: queue,
|
queue: queue,
|
||||||
skillSystem: skillSystem,
|
skillSystem: skillSystem,
|
||||||
|
potionInventory: potionInventory,
|
||||||
deathInfo: deathInfo,
|
deathInfo: deathInfo,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -72,6 +77,9 @@ class GameState {
|
|||||||
/// 스킬 시스템 상태 (Phase 3)
|
/// 스킬 시스템 상태 (Phase 3)
|
||||||
final SkillSystemState skillSystem;
|
final SkillSystemState skillSystem;
|
||||||
|
|
||||||
|
/// 물약 인벤토리
|
||||||
|
final PotionInventory potionInventory;
|
||||||
|
|
||||||
/// 사망 정보 (Phase 4, null이면 생존 중)
|
/// 사망 정보 (Phase 4, null이면 생존 중)
|
||||||
final DeathInfo? deathInfo;
|
final DeathInfo? deathInfo;
|
||||||
|
|
||||||
@@ -88,6 +96,7 @@ class GameState {
|
|||||||
ProgressState? progress,
|
ProgressState? progress,
|
||||||
QueueState? queue,
|
QueueState? queue,
|
||||||
SkillSystemState? skillSystem,
|
SkillSystemState? skillSystem,
|
||||||
|
PotionInventory? potionInventory,
|
||||||
DeathInfo? deathInfo,
|
DeathInfo? deathInfo,
|
||||||
bool clearDeathInfo = false,
|
bool clearDeathInfo = false,
|
||||||
}) {
|
}) {
|
||||||
@@ -101,6 +110,7 @@ class GameState {
|
|||||||
progress: progress ?? this.progress,
|
progress: progress ?? this.progress,
|
||||||
queue: queue ?? this.queue,
|
queue: queue ?? this.queue,
|
||||||
skillSystem: skillSystem ?? this.skillSystem,
|
skillSystem: skillSystem ?? this.skillSystem,
|
||||||
|
potionInventory: potionInventory ?? this.potionInventory,
|
||||||
deathInfo: clearDeathInfo ? null : (deathInfo ?? this.deathInfo),
|
deathInfo: clearDeathInfo ? null : (deathInfo ?? this.deathInfo),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ class ItemStats {
|
|||||||
this.intBonus = 0,
|
this.intBonus = 0,
|
||||||
this.wisBonus = 0,
|
this.wisBonus = 0,
|
||||||
this.chaBonus = 0,
|
this.chaBonus = 0,
|
||||||
|
this.attackSpeed = 0,
|
||||||
});
|
});
|
||||||
|
|
||||||
/// 물리 공격력 보정
|
/// 물리 공격력 보정
|
||||||
@@ -97,6 +98,12 @@ class ItemStats {
|
|||||||
/// CHA 보너스
|
/// CHA 보너스
|
||||||
final int chaBonus;
|
final int chaBonus;
|
||||||
|
|
||||||
|
/// 무기 공격속도 (밀리초, 무기 전용)
|
||||||
|
///
|
||||||
|
/// 0이면 기본값(1000ms) 사용, 값이 클수록 느린 공격.
|
||||||
|
/// 느린 무기는 높은 기본 데미지를 가짐.
|
||||||
|
final int attackSpeed;
|
||||||
|
|
||||||
/// 스탯 합계 (가중치 계산용)
|
/// 스탯 합계 (가중치 계산용)
|
||||||
int get totalStatValue {
|
int get totalStatValue {
|
||||||
return atk +
|
return atk +
|
||||||
@@ -121,6 +128,8 @@ class ItemStats {
|
|||||||
static const empty = ItemStats();
|
static const empty = ItemStats();
|
||||||
|
|
||||||
/// 두 스탯 합산
|
/// 두 스탯 합산
|
||||||
|
///
|
||||||
|
/// attackSpeed는 합산 대상 아님 (무기 슬롯 단일 값)
|
||||||
ItemStats operator +(ItemStats other) {
|
ItemStats operator +(ItemStats other) {
|
||||||
return ItemStats(
|
return ItemStats(
|
||||||
atk: atk + other.atk,
|
atk: atk + other.atk,
|
||||||
@@ -139,6 +148,7 @@ class ItemStats {
|
|||||||
intBonus: intBonus + other.intBonus,
|
intBonus: intBonus + other.intBonus,
|
||||||
wisBonus: wisBonus + other.wisBonus,
|
wisBonus: wisBonus + other.wisBonus,
|
||||||
chaBonus: chaBonus + other.chaBonus,
|
chaBonus: chaBonus + other.chaBonus,
|
||||||
|
// attackSpeed는 무기에서만 직접 참조
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -159,6 +169,7 @@ class ItemStats {
|
|||||||
int? intBonus,
|
int? intBonus,
|
||||||
int? wisBonus,
|
int? wisBonus,
|
||||||
int? chaBonus,
|
int? chaBonus,
|
||||||
|
int? attackSpeed,
|
||||||
}) {
|
}) {
|
||||||
return ItemStats(
|
return ItemStats(
|
||||||
atk: atk ?? this.atk,
|
atk: atk ?? this.atk,
|
||||||
@@ -177,6 +188,7 @@ class ItemStats {
|
|||||||
intBonus: intBonus ?? this.intBonus,
|
intBonus: intBonus ?? this.intBonus,
|
||||||
wisBonus: wisBonus ?? this.wisBonus,
|
wisBonus: wisBonus ?? this.wisBonus,
|
||||||
chaBonus: chaBonus ?? this.chaBonus,
|
chaBonus: chaBonus ?? this.chaBonus,
|
||||||
|
attackSpeed: attackSpeed ?? this.attackSpeed,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
136
lib/src/core/model/potion.dart
Normal file
136
lib/src/core/model/potion.dart
Normal file
@@ -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<String, int> potions;
|
||||||
|
|
||||||
|
/// 현재 전투에서 사용한 물약 종류
|
||||||
|
final Set<PotionType> 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<String, int>.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<String, int>.from(potions);
|
||||||
|
newPotions[potionId] = currentQty - 1;
|
||||||
|
if (newPotions[potionId] == 0) {
|
||||||
|
newPotions.remove(potionId);
|
||||||
|
}
|
||||||
|
|
||||||
|
final newUsed = Set<PotionType>.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<String, int>? potions,
|
||||||
|
Set<PotionType>? usedInBattle,
|
||||||
|
}) {
|
||||||
|
return PotionInventory(
|
||||||
|
potions: potions ?? this.potions,
|
||||||
|
usedInBattle: usedInBattle ?? this.usedInBattle,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -13,6 +13,42 @@ enum SkillType {
|
|||||||
debuff,
|
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 {
|
class BuffEffect {
|
||||||
const BuffEffect({
|
const BuffEffect({
|
||||||
@@ -62,6 +98,11 @@ class Skill {
|
|||||||
this.buff,
|
this.buff,
|
||||||
this.selfDamagePercent = 0.0,
|
this.selfDamagePercent = 0.0,
|
||||||
this.targetDefReduction = 0.0,
|
this.targetDefReduction = 0.0,
|
||||||
|
this.element,
|
||||||
|
this.attackMode = AttackMode.instant,
|
||||||
|
this.baseDotDamage,
|
||||||
|
this.baseDotDurationMs,
|
||||||
|
this.baseDotTickMs,
|
||||||
});
|
});
|
||||||
|
|
||||||
/// 스킬 ID
|
/// 스킬 ID
|
||||||
@@ -100,6 +141,21 @@ class Skill {
|
|||||||
/// 적 방어력 감소 % (일부 공격 스킬)
|
/// 적 방어력 감소 % (일부 공격 스킬)
|
||||||
final double targetDefReduction;
|
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;
|
bool get isAttack => type == SkillType.attack;
|
||||||
|
|
||||||
@@ -112,6 +168,9 @@ class Skill {
|
|||||||
/// 디버프 스킬 여부
|
/// 디버프 스킬 여부
|
||||||
bool get isDebuff => type == SkillType.debuff;
|
bool get isDebuff => type == SkillType.debuff;
|
||||||
|
|
||||||
|
/// DOT 스킬 여부
|
||||||
|
bool get isDot => attackMode == AttackMode.dot;
|
||||||
|
|
||||||
/// MP 효율 (데미지 당 MP 비용)
|
/// MP 효율 (데미지 당 MP 비용)
|
||||||
double get mpEfficiency {
|
double get mpEfficiency {
|
||||||
if (type != SkillType.attack || damageMultiplier <= 0) return 0;
|
if (type != SkillType.attack || damageMultiplier <= 0) return 0;
|
||||||
@@ -265,3 +324,140 @@ enum SkillFailReason {
|
|||||||
/// 사용 불가 상태
|
/// 사용 불가 상태
|
||||||
invalidState,
|
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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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/notification_overlay.dart';
|
||||||
import 'package:askiineverdie/src/features/game/widgets/skill_panel.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/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/task_progress_panel.dart';
|
||||||
|
import 'package:askiineverdie/src/features/game/widgets/active_buff_panel.dart';
|
||||||
|
|
||||||
/// 게임 진행 화면 (Main.dfm 기반 3패널 레이아웃)
|
/// 게임 진행 화면 (Main.dfm 기반 3패널 레이아웃)
|
||||||
///
|
///
|
||||||
@@ -199,6 +202,18 @@ class _GamePlayScreenState extends State<GamePlayScreen>
|
|||||||
'${event.skillName} activated!',
|
'${event.skillName} activated!',
|
||||||
CombatLogType.buff,
|
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<GamePlayScreen>
|
|||||||
// Phase 8: 스킬 (Skills with cooldown glow)
|
// Phase 8: 스킬 (Skills with cooldown glow)
|
||||||
_buildSectionHeader('Skills'),
|
_buildSectionHeader('Skills'),
|
||||||
Expanded(flex: 2, child: SkillPanel(skillSystem: state.skillSystem)),
|
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<GamePlayScreen>
|
|||||||
children: [
|
children: [
|
||||||
_buildPanelHeader(l10n.equipment),
|
_buildPanelHeader(l10n.equipment),
|
||||||
|
|
||||||
// Equipment 목록
|
// Equipment 목록 (확장 가능 스탯 패널)
|
||||||
Expanded(flex: 2, child: _buildEquipmentList(state)),
|
Expanded(
|
||||||
|
flex: 2,
|
||||||
|
child: EquipmentStatsPanel(equipment: state.equipment),
|
||||||
|
),
|
||||||
|
|
||||||
// Inventory
|
// Inventory
|
||||||
_buildPanelHeader(l10n.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 바
|
// Encumbrance 바
|
||||||
_buildSectionHeader(l10n.encumbrance),
|
_buildSectionHeader(l10n.encumbrance),
|
||||||
@@ -729,58 +766,6 @@ class _GamePlayScreenState extends State<GamePlayScreen>
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
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) {
|
Widget _buildInventoryList(GameState state) {
|
||||||
final l10n = L10n.of(context);
|
final l10n = L10n.of(context);
|
||||||
if (state.inventory.items.isEmpty) {
|
if (state.inventory.items.isEmpty) {
|
||||||
|
|||||||
206
lib/src/features/game/widgets/active_buff_panel.dart
Normal file
206
lib/src/features/game/widgets/active_buff_panel.dart
Normal file
@@ -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<ActiveBuff> 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<Widget> _buildModifierList() {
|
||||||
|
final modifiers = <Widget>[];
|
||||||
|
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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -200,6 +200,15 @@ class _AsciiAnimationCardState extends State<AsciiAnimationCard> {
|
|||||||
// 회복/버프 → idle 페이즈 유지
|
// 회복/버프 → idle 페이즈 유지
|
||||||
CombatEventType.playerHeal => (BattlePhase.idle, false),
|
CombatEventType.playerHeal => (BattlePhase.idle, false),
|
||||||
CombatEventType.playerBuff => (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(() {
|
setState(() {
|
||||||
|
|||||||
@@ -21,13 +21,16 @@ enum CombatLogType {
|
|||||||
levelUp, // 레벨업
|
levelUp, // 레벨업
|
||||||
questComplete, // 퀘스트 완료
|
questComplete, // 퀘스트 완료
|
||||||
loot, // 전리품 획득
|
loot, // 전리품 획득
|
||||||
spell, // 주문 습득
|
spell, // 스킬 사용
|
||||||
critical, // 크리티컬 히트
|
critical, // 크리티컬 히트
|
||||||
evade, // 회피
|
evade, // 회피
|
||||||
block, // 방패 방어
|
block, // 방패 방어
|
||||||
parry, // 무기 쳐내기
|
parry, // 무기 쳐내기
|
||||||
monsterAttack, // 몬스터 공격
|
monsterAttack, // 몬스터 공격
|
||||||
buff, // 버프 활성화
|
buff, // 버프 활성화
|
||||||
|
dotTick, // DOT 틱 데미지
|
||||||
|
potion, // 물약 사용
|
||||||
|
potionDrop, // 물약 드랍
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 전투 로그 위젯 (Phase 8: 실시간 전투 이벤트 표시)
|
/// 전투 로그 위젯 (Phase 8: 실시간 전투 이벤트 표시)
|
||||||
@@ -157,6 +160,9 @@ class _LogEntryTile extends StatelessWidget {
|
|||||||
CombatLogType.parry => (Colors.teal.shade300, Icons.sports_kabaddi),
|
CombatLogType.parry => (Colors.teal.shade300, Icons.sports_kabaddi),
|
||||||
CombatLogType.monsterAttack => (Colors.deepOrange.shade300, Icons.dangerous),
|
CombatLogType.monsterAttack => (Colors.deepOrange.shade300, Icons.dangerous),
|
||||||
CombatLogType.buff => (Colors.lightBlue.shade300, Icons.trending_up),
|
CombatLogType.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),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -426,6 +426,21 @@ class DeathOverlay extends StatelessWidget {
|
|||||||
Colors.lightBlue.shade300,
|
Colors.lightBlue.shade300,
|
||||||
'${event.skillName} activated',
|
'${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}',
|
||||||
|
),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
456
lib/src/features/game/widgets/equipment_stats_panel.dart
Normal file
456
lib/src/features/game/widgets/equipment_stats_panel.dart
Normal file
@@ -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',
|
||||||
|
};
|
||||||
|
}
|
||||||
238
lib/src/features/game/widgets/potion_inventory_panel.dart
Normal file
238
lib/src/features/game/widgets/potion_inventory_panel.dart
Normal file
@@ -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<PotionType> 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 = <String>[];
|
||||||
|
|
||||||
|
if (potion.healAmount > 0) {
|
||||||
|
parts.add('+${potion.healAmount}');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (potion.healPercent > 0) {
|
||||||
|
final percent = (potion.healPercent * 100).round();
|
||||||
|
parts.add('+$percent%');
|
||||||
|
}
|
||||||
|
|
||||||
|
return parts.join(' ');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -148,25 +148,58 @@ class _SkillRow extends StatelessWidget {
|
|||||||
|
|
||||||
final skillIcon = _getSkillIcon(skill.type);
|
final skillIcon = _getSkillIcon(skill.type);
|
||||||
final skillColor = _getSkillColor(skill.type);
|
final skillColor = _getSkillColor(skill.type);
|
||||||
|
final elementColor = _getElementColor(skill.element);
|
||||||
|
final elementIcon = _getElementIcon(skill.element);
|
||||||
|
|
||||||
Widget row = Container(
|
Widget row = Container(
|
||||||
padding: const EdgeInsets.symmetric(vertical: 2),
|
padding: const EdgeInsets.symmetric(vertical: 2),
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
// 스킬 아이콘
|
// 스킬 타입 아이콘
|
||||||
Icon(skillIcon, size: 14, color: skillColor),
|
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),
|
const SizedBox(width: 4),
|
||||||
// 스킬 이름
|
|
||||||
|
// 스킬 이름 (속성 색상 적용)
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Text(
|
child: Text(
|
||||||
skill.name,
|
skill.name,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 10,
|
fontSize: 10,
|
||||||
color: isReady ? Colors.white : Colors.grey,
|
color: isReady
|
||||||
|
? (skill.element != null ? elementColor : Colors.white)
|
||||||
|
: Colors.grey,
|
||||||
),
|
),
|
||||||
overflow: TextOverflow.ellipsis,
|
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(
|
Text(
|
||||||
'Lv.$rank',
|
'Lv.$rank',
|
||||||
@@ -241,4 +274,69 @@ class _SkillRow extends StatelessWidget {
|
|||||||
return Colors.purple;
|
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,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user