Compare commits
10 Commits
d4acd3503b
...
598c25e4c9
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
598c25e4c9 | ||
|
|
e30177e788 | ||
|
|
8314aea578 | ||
|
|
13198f9f1f | ||
|
|
b16ae6c2b8 | ||
|
|
071ac5f1e3 | ||
|
|
5a567bc3e3 | ||
|
|
ff0e0b7eb1 | ||
|
|
fac7c7e6fc | ||
|
|
0216eb1261 |
335
lib/data/game_text_l10n.dart
Normal file
335
lib/data/game_text_l10n.dart
Normal file
@@ -0,0 +1,335 @@
|
||||
// 게임 텍스트 로컬라이제이션 (BuildContext 없이 사용)
|
||||
// progress_service.dart, pq_logic.dart 등에서 사용
|
||||
|
||||
import 'package:askiineverdie/data/game_translations_ko.dart';
|
||||
|
||||
/// 현재 게임 로케일 설정 (전역)
|
||||
String _currentLocale = 'en';
|
||||
|
||||
/// 현재 로케일 가져오기
|
||||
String get currentGameLocale => _currentLocale;
|
||||
|
||||
/// 로케일 설정 (앱 시작 시 호출)
|
||||
void setGameLocale(String locale) {
|
||||
_currentLocale = locale;
|
||||
}
|
||||
|
||||
/// 한국어 여부 확인
|
||||
bool get isKoreanLocale => _currentLocale == 'ko';
|
||||
|
||||
// ============================================================================
|
||||
// 프롤로그 텍스트
|
||||
// ============================================================================
|
||||
|
||||
const _prologueTextsEn = [
|
||||
'Receiving an ominous vision from the Code God',
|
||||
'The old Compiler Sage reveals a prophecy: "The Glitch God has awakened"',
|
||||
'A sudden Buffer Overflow resets your village, leaving you as the sole survivor',
|
||||
'With unexpected resolve, you embark on a perilous journey to the Null Kingdom',
|
||||
];
|
||||
|
||||
const _prologueTextsKo = [
|
||||
'코드의 신으로부터 불길한 환영을 받다',
|
||||
'늙은 컴파일러 현자가 예언을 밝히다: "글리치 신이 깨어났다"',
|
||||
'갑작스러운 버퍼 오버플로우가 마을을 초기화하고, 당신만이 유일한 생존자로 남다',
|
||||
'예상치 못한 결의로 널(Null) 왕국을 향한 위험한 여정을 시작하다',
|
||||
];
|
||||
|
||||
List<String> get prologueTexts =>
|
||||
isKoreanLocale ? _prologueTextsKo : _prologueTextsEn;
|
||||
|
||||
// ============================================================================
|
||||
// 태스크 캡션
|
||||
// ============================================================================
|
||||
|
||||
String get taskCompiling => isKoreanLocale ? '컴파일 중' : 'Compiling';
|
||||
|
||||
String get taskPrologue => isKoreanLocale ? '프롤로그' : 'Prologue';
|
||||
|
||||
String taskHeadingToMarket() =>
|
||||
isKoreanLocale ? '전리품을 팔기 위해 데이터 마켓으로 이동 중' : 'Heading to the Data Market to trade loot';
|
||||
|
||||
String taskUpgradingHardware() =>
|
||||
isKoreanLocale ? '테크 샵에서 하드웨어 업그레이드 중' : 'Upgrading hardware at the Tech Shop';
|
||||
|
||||
String taskEnteringDebugZone() =>
|
||||
isKoreanLocale ? '디버그 존 진입 중' : 'Entering the Debug Zone';
|
||||
|
||||
String taskDebugging(String monsterName) =>
|
||||
isKoreanLocale ? '$monsterName 디버깅 중' : 'Debugging $monsterName';
|
||||
|
||||
String taskSelling(String itemDescription) =>
|
||||
isKoreanLocale ? '$itemDescription 판매 중' : 'Selling $itemDescription';
|
||||
|
||||
// ============================================================================
|
||||
// 퀘스트 캡션
|
||||
// ============================================================================
|
||||
|
||||
String questPatch(String name) =>
|
||||
isKoreanLocale ? '$name 패치하기' : 'Patch $name';
|
||||
|
||||
String questLocate(String item) =>
|
||||
isKoreanLocale ? '$item 찾기' : 'Locate $item';
|
||||
|
||||
String questTransfer(String item) =>
|
||||
isKoreanLocale ? '이 $item 전송하기' : 'Transfer this $item';
|
||||
|
||||
String questDownload(String item) =>
|
||||
isKoreanLocale ? '$item 다운로드하기' : 'Download $item';
|
||||
|
||||
String questStabilize(String name) =>
|
||||
isKoreanLocale ? '$name 안정화하기' : 'Stabilize $name';
|
||||
|
||||
// ============================================================================
|
||||
// Act 제목
|
||||
// ============================================================================
|
||||
|
||||
String actTitle(String romanNumeral) =>
|
||||
isKoreanLocale ? '$romanNumeral막' : 'Act $romanNumeral';
|
||||
|
||||
// ============================================================================
|
||||
// 시네마틱 텍스트 - 시나리오 1: 캐시 존
|
||||
// ============================================================================
|
||||
|
||||
String cinematicCacheZone1() => isKoreanLocale
|
||||
? '지쳐서 손상된 네트워크의 안전한 캐시 존에 도착하다'
|
||||
: 'Exhausted, you reach a safe Cache Zone in the corrupted network';
|
||||
|
||||
String cinematicCacheZone2() => isKoreanLocale
|
||||
? '옛 동맹들과 재연결하고 새로운 동료들을 포크하다'
|
||||
: 'You reconnect with old allies and fork new ones';
|
||||
|
||||
String cinematicCacheZone3() => isKoreanLocale
|
||||
? '디버거 기사단 회의에 참석하다'
|
||||
: 'You attend a council of the Debugger Knights';
|
||||
|
||||
String cinematicCacheZone4() => isKoreanLocale
|
||||
? '많은 버그들이 기다린다. 당신이 패치하도록 선택되었다!'
|
||||
: 'Many bugs await. You are chosen to patch them!';
|
||||
|
||||
// ============================================================================
|
||||
// 시네마틱 텍스트 - 시나리오 2: 전투
|
||||
// ============================================================================
|
||||
|
||||
String cinematicCombat1() => isKoreanLocale
|
||||
? '목표가 눈앞에 있지만, 치명적인 버그가 길을 막는다!'
|
||||
: 'Your target is in sight, but a critical bug blocks your path!';
|
||||
|
||||
String cinematicCombat2(String nemesis) => isKoreanLocale
|
||||
? '$nemesis와의 필사적인 디버깅 세션이 시작되다'
|
||||
: 'A desperate debugging session begins with $nemesis';
|
||||
|
||||
String cinematicCombatLocked(String nemesis) => isKoreanLocale
|
||||
? '$nemesis와 치열한 디버깅 중'
|
||||
: 'Locked in intense debugging with $nemesis';
|
||||
|
||||
String cinematicCombatCorrupts(String nemesis) => isKoreanLocale
|
||||
? '$nemesis가 당신의 스택 트레이스를 손상시키다'
|
||||
: '$nemesis corrupts your stack trace';
|
||||
|
||||
String cinematicCombatWorking(String nemesis) => isKoreanLocale
|
||||
? '당신의 패치가 $nemesis에게 효과를 보이는 것 같다'
|
||||
: 'Your patch seems to be working against $nemesis';
|
||||
|
||||
String cinematicCombatVictory(String nemesis) => isKoreanLocale
|
||||
? '승리! $nemesis가 패치되었다! 복구를 위해 시스템이 재부팅된다'
|
||||
: 'Victory! $nemesis is patched! System reboots for recovery';
|
||||
|
||||
String cinematicCombatWakeUp() => isKoreanLocale
|
||||
? '안전 모드에서 깨어나지만, 커널이 기다린다'
|
||||
: 'You wake up in a Safe Mode, but the kernel awaits';
|
||||
|
||||
// ============================================================================
|
||||
// 시네마틱 텍스트 - 시나리오 3: 배신
|
||||
// ============================================================================
|
||||
|
||||
String cinematicBetrayal1(String guy) => isKoreanLocale
|
||||
? '안도감! $guy의 보안 서버에 도착하다'
|
||||
: 'What relief! You reach the secure server of $guy';
|
||||
|
||||
String cinematicBetrayal2(String guy) => isKoreanLocale
|
||||
? '축하가 이어지고, $guy와 수상한 비밀 핸드셰이크를 나누다'
|
||||
: 'There is celebration, and a suspicious private handshake with $guy';
|
||||
|
||||
String cinematicBetrayal3(String item) => isKoreanLocale
|
||||
? '$item을 잊고 다시 가져오러 돌아가다'
|
||||
: 'You forget your $item and go back to retrieve it';
|
||||
|
||||
String cinematicBetrayal4() => isKoreanLocale
|
||||
? '이게 뭐지!? 손상된 패킷을 가로채다!'
|
||||
: 'What is this!? You intercept a corrupted packet!';
|
||||
|
||||
String cinematicBetrayal5(String guy) => isKoreanLocale
|
||||
? '$guy가 글리치 신의 백도어일 수 있을까?'
|
||||
: 'Could $guy be a backdoor for the Glitch God?';
|
||||
|
||||
String cinematicBetrayal6() => isKoreanLocale
|
||||
? '이 정보를 누구에게 맡길 수 있을까!? -- 바이너리 신전이다'
|
||||
: 'Who can be trusted with this intel!? -- The Binary Temple, of course';
|
||||
|
||||
// ============================================================================
|
||||
// 몬스터 수식어
|
||||
// ============================================================================
|
||||
|
||||
String modifierDead(String s) => isKoreanLocale ? '죽은 $s' : 'dead $s';
|
||||
String modifierComatose(String s) => isKoreanLocale ? '혼수상태의 $s' : 'comatose $s';
|
||||
String modifierCrippled(String s) => isKoreanLocale ? '불구의 $s' : 'crippled $s';
|
||||
String modifierSick(String s) => isKoreanLocale ? '병든 $s' : 'sick $s';
|
||||
String modifierUndernourished(String s) =>
|
||||
isKoreanLocale ? '영양실조 $s' : 'undernourished $s';
|
||||
|
||||
String modifierFoetal(String s) => isKoreanLocale ? '원시 $s' : 'primordial $s';
|
||||
String modifierBaby(String s) => isKoreanLocale ? '미숙한 $s' : 'immature $s';
|
||||
String modifierPreadolescent(String s) =>
|
||||
isKoreanLocale ? '성장 중인 $s' : 'growing $s';
|
||||
String modifierTeenage(String s) => isKoreanLocale ? '어린 $s' : 'young $s';
|
||||
String modifierUnderage(String s) => isKoreanLocale ? '경험 부족 $s' : 'inexperienced $s';
|
||||
|
||||
String modifierGreater(String s) => isKoreanLocale ? '상위 $s' : 'greater $s';
|
||||
String modifierMassive(String s) => isKoreanLocale ? '거대한 $s' : 'massive $s';
|
||||
String modifierEnormous(String s) => isKoreanLocale ? '초거대 $s' : 'enormous $s';
|
||||
String modifierGiant(String s) => isKoreanLocale ? '자이언트 $s' : 'giant $s';
|
||||
String modifierTitanic(String s) => isKoreanLocale ? '타이타닉 $s' : 'titanic $s';
|
||||
|
||||
String modifierVeteran(String s) => isKoreanLocale ? '베테랑 $s' : 'veteran $s';
|
||||
String modifierBattle(String s) => isKoreanLocale ? '전투-$s' : 'Battle-$s';
|
||||
String modifierCursed(String s) => isKoreanLocale ? '저주받은 $s' : 'cursed $s';
|
||||
String modifierWarrior(String s) => isKoreanLocale ? '전사 $s' : 'warrior $s';
|
||||
String modifierWere(String s) => isKoreanLocale ? '늑대인간-$s' : 'Were-$s';
|
||||
String modifierUndead(String s) => isKoreanLocale ? '언데드 $s' : 'undead $s';
|
||||
String modifierDemon(String s) => isKoreanLocale ? '데몬 $s' : 'demon $s';
|
||||
|
||||
String modifierMessianic(String s) => isKoreanLocale ? '메시아닉 $s' : 'messianic $s';
|
||||
String modifierImaginary(String s) => isKoreanLocale ? '상상의 $s' : 'imaginary $s';
|
||||
String modifierPassing(String s) => isKoreanLocale ? '지나가는 $s' : 'passing $s';
|
||||
|
||||
// ============================================================================
|
||||
// 시간 표시
|
||||
// ============================================================================
|
||||
|
||||
String roughTimeSeconds(int seconds) =>
|
||||
isKoreanLocale ? '$seconds초' : '$seconds seconds';
|
||||
|
||||
String roughTimeMinutes(int minutes) =>
|
||||
isKoreanLocale ? '$minutes분' : '$minutes minutes';
|
||||
|
||||
String roughTimeHours(int hours) =>
|
||||
isKoreanLocale ? '$hours시간' : '$hours hours';
|
||||
|
||||
String roughTimeDays(int days) =>
|
||||
isKoreanLocale ? '$days일' : '$days days';
|
||||
|
||||
// ============================================================================
|
||||
// 영어 문법 함수 (한국어에서는 단순화)
|
||||
// ============================================================================
|
||||
|
||||
/// 관사 + 명사 (한국어: 수량만 표시)
|
||||
String indefiniteL10n(String s, int qty) {
|
||||
if (isKoreanLocale) {
|
||||
return qty == 1 ? s : '$qty $s';
|
||||
}
|
||||
// 영어 로직
|
||||
if (qty == 1) {
|
||||
const vowels = 'AEIOUÜaeiouü';
|
||||
final first = s.isNotEmpty ? s[0] : 'a';
|
||||
final article = vowels.contains(first) ? 'an' : 'a';
|
||||
return '$article $s';
|
||||
}
|
||||
return '$qty ${_pluralize(s)}';
|
||||
}
|
||||
|
||||
/// the + 명사 (한국어: 그냥 명사)
|
||||
String definiteL10n(String s, int qty) {
|
||||
if (isKoreanLocale) {
|
||||
return s;
|
||||
}
|
||||
// 영어 로직
|
||||
if (qty > 1) {
|
||||
s = _pluralize(s);
|
||||
}
|
||||
return 'the $s';
|
||||
}
|
||||
|
||||
/// 복수형 (영어만 해당)
|
||||
String _pluralize(String s) {
|
||||
if (s.endsWith('y')) return '${s.substring(0, s.length - 1)}ies';
|
||||
if (s.endsWith('us')) return '${s.substring(0, s.length - 2)}i';
|
||||
if (s.endsWith('ch') || s.endsWith('x') || s.endsWith('s')) return '${s}es';
|
||||
if (s.endsWith('f')) return '${s.substring(0, s.length - 1)}ves';
|
||||
if (s.endsWith('man') || s.endsWith('Man')) {
|
||||
return '${s.substring(0, s.length - 2)}en';
|
||||
}
|
||||
return '${s}s'; // ignore: unnecessary_brace_in_string_interp
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// impressiveGuy 관련
|
||||
// ============================================================================
|
||||
|
||||
String impressiveGuyPattern1(String title, String race) => isKoreanLocale
|
||||
// ignore: unnecessary_brace_in_string_interps
|
||||
? '${race}들의 $title' // 한국어 조사 연결을 위해 중괄호 필요
|
||||
: 'the $title of the ${_pluralize(race)}';
|
||||
|
||||
String impressiveGuyPattern2(String title, String name1, String name2) =>
|
||||
isKoreanLocale
|
||||
? '$name2의 $title $name1'
|
||||
: '$title $name1 of $name2';
|
||||
|
||||
// ============================================================================
|
||||
// namedMonster 관련
|
||||
// ============================================================================
|
||||
|
||||
String namedMonsterFormat(String generatedName, String monsterType) =>
|
||||
isKoreanLocale
|
||||
? '$monsterType $generatedName'
|
||||
: '$generatedName the $monsterType';
|
||||
|
||||
// ============================================================================
|
||||
// 게임 데이터 번역 함수 (BuildContext 없이 사용)
|
||||
// ============================================================================
|
||||
|
||||
/// 몬스터 이름 번역
|
||||
String translateMonster(String englishName) =>
|
||||
isKoreanLocale ? (monsterTranslationsKo[englishName] ?? englishName) : englishName;
|
||||
|
||||
/// 종족 이름 번역
|
||||
String translateRace(String englishName) =>
|
||||
isKoreanLocale ? (raceTranslationsKo[englishName] ?? englishName) : englishName;
|
||||
|
||||
/// 직업 이름 번역
|
||||
String translateKlass(String englishName) =>
|
||||
isKoreanLocale ? (klassTranslationsKo[englishName] ?? englishName) : englishName;
|
||||
|
||||
/// 칭호 이름 번역
|
||||
String translateTitle(String englishName) =>
|
||||
isKoreanLocale ? (titleTranslationsKo[englishName] ?? englishName) : englishName;
|
||||
|
||||
/// 인상적인 칭호 번역 (impressiveTitles용)
|
||||
String translateImpressiveTitle(String englishName) =>
|
||||
isKoreanLocale ? (impressiveTitleTranslationsKo[englishName] ?? englishName) : englishName;
|
||||
|
||||
/// 특수 아이템 이름 번역
|
||||
String translateSpecial(String englishName) =>
|
||||
isKoreanLocale ? (specialTranslationsKo[englishName] ?? englishName) : englishName;
|
||||
|
||||
/// 아이템 속성 이름 번역
|
||||
String translateItemAttrib(String englishName) =>
|
||||
isKoreanLocale ? (itemAttribTranslationsKo[englishName] ?? englishName) : englishName;
|
||||
|
||||
/// 아이템 "~의" 접미사 번역
|
||||
String translateItemOf(String englishName) =>
|
||||
isKoreanLocale ? (itemOfsTranslationsKo[englishName] ?? englishName) : englishName;
|
||||
|
||||
/// 단순 아이템 번역
|
||||
String translateBoringItem(String englishName) =>
|
||||
isKoreanLocale ? (boringItemTranslationsKo[englishName] ?? englishName) : englishName;
|
||||
|
||||
/// interestingItem 번역 (attrib + special 조합)
|
||||
/// 예: "Golden Iterator" → "황금 이터레이터"
|
||||
String translateInterestingItem(String attrib, String special) {
|
||||
if (!isKoreanLocale) return '$attrib $special';
|
||||
final translatedAttrib = itemAttribTranslationsKo[attrib] ?? attrib;
|
||||
final translatedSpecial = specialTranslationsKo[special] ?? special;
|
||||
return '$translatedAttrib $translatedSpecial';
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
// 아스키나라(ASCII-Nara) 한국어 번역 데이터
|
||||
// ASCII NEVER DIE 한국어 번역 데이터
|
||||
// 게임 데이터의 한국어 번역을 제공합니다.
|
||||
|
||||
/// 종족 이름 한국어 번역
|
||||
@@ -248,6 +248,189 @@ const Map<String, String> monsterTranslationsKo = {
|
||||
'Bohrbug': '보어버그',
|
||||
'Mandelbug': '만델버그',
|
||||
'Hindenbug': '힌덴버그',
|
||||
|
||||
// 추가 버그 및 회피 기법
|
||||
'Schroedinbug': '슈뢰딘버그',
|
||||
'Antivirus Evasion': '안티바이러스 회피',
|
||||
'Packer': '패커',
|
||||
'Crypter': '크립터',
|
||||
'Dropper': '드로퍼',
|
||||
'Loader': '로더',
|
||||
'Payload Carrier': '페이로드 운반자',
|
||||
'Persistence Mechanism': '지속성 메커니즘',
|
||||
'Privilege Escalation': '권한 상승',
|
||||
'Lateral Movement': '측면 이동',
|
||||
'Exfil Channel': '유출 채널',
|
||||
'C2 Beacon': 'C2 비콘',
|
||||
'DNS Tunnel': 'DNS 터널',
|
||||
'ICMP Shell': 'ICMP 쉘',
|
||||
'HTTP Backdoor': 'HTTP 백도어',
|
||||
'Reverse Shell': '리버스 쉘',
|
||||
'Bind Shell': '바인드 쉘',
|
||||
'Web Shell': '웹 쉘',
|
||||
'Cron Job Malware': '크론잡 악성코드',
|
||||
'Init Script Virus': '초기화 스크립트 바이러스',
|
||||
|
||||
// 코드 인젝션 기법
|
||||
'Library Injection': '라이브러리 인젝션',
|
||||
'Process Hollowing': '프로세스 할로잉',
|
||||
'Thread Injection': '스레드 인젝션',
|
||||
'APC Injection': 'APC 인젝션',
|
||||
'Atom Bombing': '아톰 봄빙',
|
||||
'Process Doppelganging': '프로세스 도플갱잉',
|
||||
'Ghostwriting': '고스트라이팅',
|
||||
'Module Stomping': '모듈 스톰핑',
|
||||
'Reflective Loading': '리플렉티브 로딩',
|
||||
'Manual Mapping': '수동 매핑',
|
||||
'Syscall Stub': '시스콜 스텁',
|
||||
'Heaven Gate': '헤븐 게이트',
|
||||
|
||||
// 비동기 및 메모리 관련
|
||||
'Callback Hell': '콜백 지옥',
|
||||
'Promise Rejection': '프로미스 거부',
|
||||
'Event Loop Block': '이벤트 루프 블록',
|
||||
'Memory Pressure': '메모리 압박',
|
||||
'Garbage Storm': '가비지 폭풍',
|
||||
'Finalizer Bug': '파이널라이저 버그',
|
||||
'Weak Reference Leak': '약한 참조 누수',
|
||||
'String Interning Bug': '문자열 인터닝 버그',
|
||||
'Classloader Leak': '클래스로더 누수',
|
||||
'Native Memory Leak': '네이티브 메모리 누수',
|
||||
'Direct Buffer Leak': '다이렉트 버퍼 누수',
|
||||
'Thread Local Leak': '스레드 로컬 누수',
|
||||
'Connection Pool Leak': '커넥션 풀 누수',
|
||||
'File Handle Leak': '파일 핸들 누수',
|
||||
'Timer Leak': '타이머 누수',
|
||||
'Listener Leak': '리스너 누수',
|
||||
'Observable Leak': '옵저버블 누수',
|
||||
'Async Leak': '비동기 누수',
|
||||
'Context Leak': '컨텍스트 누수',
|
||||
'Bitmap Leak': '비트맵 누수',
|
||||
'Cursor Leak': '커서 누수',
|
||||
'Stream Leak': '스트림 누수',
|
||||
'Transaction Leak': '트랜잭션 누수',
|
||||
'Session Leak': '세션 누수',
|
||||
'Cache Bloat': '캐시 비대화',
|
||||
'Queue Overflow': '큐 오버플로우',
|
||||
'Ring Buffer Bug': '링 버퍼 버그',
|
||||
|
||||
// 동시성 및 락 관련
|
||||
'Lock Contention': '락 경합',
|
||||
'Spin Lock Burn': '스핀 락 연소',
|
||||
'False Sharing': '거짓 공유',
|
||||
'NUMA Bug': 'NUMA 버그',
|
||||
'Affinity Bug': '어피니티 버그',
|
||||
'Context Switch Storm': '컨텍스트 스위치 폭풍',
|
||||
'TLB Shootdown': 'TLB 슛다운',
|
||||
'IPI Storm': 'IPI 폭풍',
|
||||
'Interrupt Disable Bug': '인터럽트 비활성화 버그',
|
||||
'Preemption Bug': '선점 버그',
|
||||
'RCU Bug': 'RCU 버그',
|
||||
'Seqlock Bug': '시퀀스 락 버그',
|
||||
'RWLock Bug': '읽기쓰기 락 버그',
|
||||
'Futex Bug': 'Futex 버그',
|
||||
'Spinlock Bug': '스핀락 버그',
|
||||
'Barrier Bug': '배리어 버그',
|
||||
'Condition Variable Bug': '조건 변수 버그',
|
||||
'Semaphore Leak': '세마포어 누수',
|
||||
'Message Queue Bug': '메시지 큐 버그',
|
||||
'Shared Memory Bug': '공유 메모리 버그',
|
||||
'Pipe Deadlock': '파이프 데드락',
|
||||
'Socket Leak': '소켓 누수',
|
||||
'Epoll Bug': 'Epoll 버그',
|
||||
'Kqueue Bug': 'Kqueue 버그',
|
||||
'IOCP Bug': 'IOCP 버그',
|
||||
'AIO Bug': 'AIO 버그',
|
||||
'Scatter Gather Bug': '스캐터 개더 버그',
|
||||
'Zero Copy Bug': '제로 카피 버그',
|
||||
'Splice Bug': '스플라이스 버그',
|
||||
'Vmsplice Bug': 'Vmsplice 버그',
|
||||
'Tee Bug': 'Tee 버그',
|
||||
'Fallocate Bug': 'Fallocate 버그',
|
||||
'Punch Hole Bug': '펀치 홀 버그',
|
||||
'Direct IO Bug': '다이렉트 IO 버그',
|
||||
'Sync Bug': '동기화 버그',
|
||||
'Datasync Bug': '데이터 동기화 버그',
|
||||
'Journal Bug': '저널 버그',
|
||||
'Inode Leak': 'Inode 누수',
|
||||
'Dentry Cache Bug': 'Dentry 캐시 버그',
|
||||
'Buffer Head Bug': '버퍼 헤드 버그',
|
||||
'Page Cache Bug': '페이지 캐시 버그',
|
||||
'Slab Leak': '슬랩 누수',
|
||||
'Kmalloc Bug': 'Kmalloc 버그',
|
||||
'Vmalloc Bug': 'Vmalloc 버그',
|
||||
'Highmem Bug': '상위 메모리 버그',
|
||||
'Lowmem Bug': '하위 메모리 버그',
|
||||
'OOM Killer': 'OOM 킬러',
|
||||
|
||||
// 폭탄 및 DoS 관련
|
||||
'Fork Bomb': '포크 폭탄',
|
||||
'Zip Bomb': '압축 폭탄',
|
||||
'Xml Bomb': 'XML 폭탄',
|
||||
'Regex Bomb': '정규식 폭탄',
|
||||
'Hash Collision Attack': '해시 충돌 공격',
|
||||
'Algorithmic Complexity': '알고리즘 복잡도 공격',
|
||||
'Slowloris': '슬로우로리스',
|
||||
'RUDY': 'RUDY',
|
||||
'Apache Killer': '아파치 킬러',
|
||||
'HashDoS': '해시 DoS',
|
||||
'SYN Flood Spirit': 'SYN 플러드 정령',
|
||||
'UDP Flood': 'UDP 플러드',
|
||||
'ICMP Flood': 'ICMP 플러드',
|
||||
'Smurf Attack': '스머프 공격',
|
||||
'Fraggle Attack': '프래글 공격',
|
||||
'DNS Amplification': 'DNS 증폭',
|
||||
'NTP Amplification': 'NTP 증폭',
|
||||
'SSDP Amplification': 'SSDP 증폭',
|
||||
'Memcached Amplification': 'Memcached 증폭',
|
||||
'CLDAP Amplification': 'CLDAP 증폭',
|
||||
'Reflection Attack': '반사 공격',
|
||||
'Carpet Bombing': '융단 폭격',
|
||||
'Pulse Wave': '펄스 웨이브',
|
||||
'Low and Slow': '저속 공격',
|
||||
'Application Layer': '애플리케이션 레이어 공격',
|
||||
'SSL Exhaustion': 'SSL 고갈',
|
||||
'Renegotiation Attack': '재협상 공격',
|
||||
|
||||
// 유명 취약점들
|
||||
'BEAST': 'BEAST',
|
||||
'CRIME': 'CRIME',
|
||||
'BREACH': 'BREACH',
|
||||
'POODLE': 'POODLE',
|
||||
'Heartbleed Ghost': '하트블리드 유령',
|
||||
'Shellshock': '쉘쇼크',
|
||||
'Dirty COW': '더티 카우',
|
||||
'VENOM': 'VENOM',
|
||||
'Cloudbleed': '클라우드블리드',
|
||||
'Krack Attack': 'KRACK 공격',
|
||||
'Dragonblood': '드래곤블러드',
|
||||
'Frag Attack': '프래그 공격',
|
||||
'Kr00k': 'Kr00k',
|
||||
'PMKID Attack': 'PMKID 공격',
|
||||
|
||||
// 무선 네트워크 공격
|
||||
'Evil Twin': '이블 트윈',
|
||||
'Karma Attack': '카르마 공격',
|
||||
'Deauth Attack': '인증해제 공격',
|
||||
'Beacon Flood': '비콘 플러드',
|
||||
'Bluetooth Bug': '블루투스 버그',
|
||||
'Blueborne': '블루본',
|
||||
'Sweyntooth': '스웨인투스',
|
||||
'Braktooth': '브랙투스',
|
||||
'Knob Attack': 'KNOB 공격',
|
||||
'Bias Attack': 'BIAS 공격',
|
||||
|
||||
// 네트워크 장비 및 프로토콜 버그
|
||||
'Cable Haunt': '케이블 헌트',
|
||||
'CallStranger': '콜스트레인저',
|
||||
'Ripple20': 'Ripple20',
|
||||
'Amnesia33': 'Amnesia33',
|
||||
'Number Jack': '넘버 잭',
|
||||
'NAME:WRECK': 'NAME:WRECK',
|
||||
'BadAlloc': 'BadAlloc',
|
||||
'PwnKit': 'PwnKit',
|
||||
'Sudo Bug': 'Sudo 버그',
|
||||
'Baron Samedit': '바론 사메딧',
|
||||
};
|
||||
|
||||
/// 무기 이름 한국어 번역
|
||||
@@ -555,3 +738,366 @@ const Map<String, String> specialTranslationsKo = {
|
||||
'Prototype': '프로토타입',
|
||||
'Builder': '빌더',
|
||||
};
|
||||
|
||||
/// 잡템(BoringItems) 한국어 번역
|
||||
/// 몬스터 처치 시 획득하는 일반 아이템들
|
||||
const Map<String, String> boringItemTranslationsKo = {
|
||||
'semicolon': '세미콜론',
|
||||
'curly brace': '중괄호',
|
||||
'null pointer': '널 포인터',
|
||||
'empty string': '빈 문자열',
|
||||
'deprecated token': '사용 중단 토큰',
|
||||
'legacy code': '레거시 코드',
|
||||
'tab character': '탭 문자',
|
||||
'whitespace': '공백 문자',
|
||||
'comment block': '주석 블록',
|
||||
'todo marker': 'TODO 마커',
|
||||
'fixme note': 'FIXME 노트',
|
||||
'readme fragment': 'README 조각',
|
||||
'config shard': '설정 파편',
|
||||
'log entry': '로그 항목',
|
||||
'stack trace': '스택 트레이스',
|
||||
'core dump': '코어 덤프',
|
||||
'crash report': '충돌 보고서',
|
||||
'error message': '오류 메시지',
|
||||
'warning flag': '경고 플래그',
|
||||
'lint error': '린트 오류',
|
||||
'syntax fragment': '구문 조각',
|
||||
'broken link': '깨진 링크',
|
||||
'orphan process': '고아 프로세스',
|
||||
'zombie thread': '좀비 스레드',
|
||||
'dangling pointer': '댕글링 포인터',
|
||||
'memory leak': '메모리 누수',
|
||||
'buffer scrap': '버퍼 조각',
|
||||
'bit bucket': '비트 버킷',
|
||||
'dev null': '/dev/null',
|
||||
'/dev/random': '/dev/random',
|
||||
'entropy pool': '엔트로피 풀',
|
||||
'hash collision': '해시 충돌',
|
||||
'race condition': '레이스 컨디션',
|
||||
'deadlock key': '데드락 키',
|
||||
'mutex token': '뮤텍스 토큰',
|
||||
'semaphore': '세마포어',
|
||||
'signal handler': '시그널 핸들러',
|
||||
'interrupt vector': '인터럽트 벡터',
|
||||
'return value': '반환값',
|
||||
'exit code': '종료 코드',
|
||||
'errno': 'errno',
|
||||
};
|
||||
|
||||
/// 몬스터 드롭 아이템 한국어 번역
|
||||
/// 몬스터 처치 시 드롭되는 아이템들 (pq_config_data의 Monsters 테이블 3번째 필드)
|
||||
const Map<String, String> dropItemTranslationsKo = {
|
||||
// 레벨 0-5 드롭
|
||||
'misspelling': '오타',
|
||||
'yellow flag': '노란 깃발',
|
||||
'punctuation': '구두점',
|
||||
'question mark': '물음표',
|
||||
'red squiggle': '빨간 밑줄',
|
||||
'amber light': '주황 신호등',
|
||||
'empty pointer': '빈 포인터',
|
||||
'array fragment': '배열 조각',
|
||||
'infinity shard': '무한 파편',
|
||||
'failed check': '실패한 검사',
|
||||
'malformed token': '잘못된 토큰',
|
||||
'garbled text': '깨진 텍스트',
|
||||
'fence post': '울타리 기둥',
|
||||
'twisted gate': '비틀린 게이트',
|
||||
'spinning wheel': '돌아가는 바퀴',
|
||||
'dripping byte': '새는 바이트',
|
||||
'overflowing cup': '넘치는 컵',
|
||||
'tangled thread': '엉킨 스레드',
|
||||
'void fragment': '공허 조각',
|
||||
'crash crystal': '충돌 결정',
|
||||
'thrown object': '던진 객체',
|
||||
'hourglass': '모래시계',
|
||||
'severed cable': '끊어진 케이블',
|
||||
'missing icon': '없는 아이콘',
|
||||
'locked door': '잠긴 문',
|
||||
|
||||
// 레벨 6-10 드롭
|
||||
'leaked byte': '누수 바이트',
|
||||
'overflow data': '오버플로우 데이터',
|
||||
'null crystal': '널 결정',
|
||||
'broken index': '깨진 인덱스',
|
||||
'morphed type': '변형된 타입',
|
||||
'dangling reference': '댕글링 참조',
|
||||
'duplicate key': '중복 키',
|
||||
'wrapped number': '래핑된 숫자',
|
||||
'format specifier': '포맷 지정자',
|
||||
'malicious query': '악성 쿼리',
|
||||
'script tag': '스크립트 태그',
|
||||
'forged request': '위조된 요청',
|
||||
'escaped path': '이스케이프된 경로',
|
||||
'shell command': '쉘 명령어',
|
||||
'tangled threads': '엉킨 스레드들',
|
||||
'locked mutex': '잠긴 뮤텍스',
|
||||
'spinning lock': '돌아가는 락',
|
||||
'inverted queue': '역전된 큐',
|
||||
'hungry process': '굶주린 프로세스',
|
||||
'corrupted block': '손상된 블록',
|
||||
'crushed frame': '부서진 프레임',
|
||||
'garbled bytes': '깨진 바이트들',
|
||||
'racing bits': '레이싱 비트',
|
||||
'floating reference': '떠다니는 참조',
|
||||
|
||||
// 레벨 11-20 드롭
|
||||
'panic message': '패닉 메시지',
|
||||
'blue fragment': '블루 조각',
|
||||
'dumped core': '덤프된 코어',
|
||||
'segment piece': '세그먼트 조각',
|
||||
'bus token': '버스 토큰',
|
||||
'missing page': '누락된 페이지',
|
||||
'invalid cache': '무효 캐시',
|
||||
'translation fail': '변환 실패',
|
||||
'transfer error': '전송 오류',
|
||||
'signal flood': '신호 홍수',
|
||||
'expired timer': '만료된 타이머',
|
||||
'rom error': 'ROM 오류',
|
||||
'boot failure': '부팅 실패',
|
||||
'boot sector': '부트 섹터',
|
||||
'infected mbr': '감염된 MBR',
|
||||
'hidden process': '숨겨진 프로세스',
|
||||
'kernel exploit': '커널 익스플로잇',
|
||||
'vm breach': 'VM 침투',
|
||||
'management mode': '관리 모드',
|
||||
'cpu patch': 'CPU 패치',
|
||||
'speculative exec': '추측 실행',
|
||||
'kernel leak': '커널 누출',
|
||||
'bit flip': '비트 플립',
|
||||
'frozen memory': '얼어붙은 메모리',
|
||||
'direct access': '직접 접근',
|
||||
'timing info': '타이밍 정보',
|
||||
'persistent threat': '지속적 위협',
|
||||
'boot implant': '부트 임플란트',
|
||||
'management engine': '관리 엔진',
|
||||
|
||||
// 레벨 21-30 드롭
|
||||
'apt sample': 'APT 샘플',
|
||||
'classified doc': '기밀 문서',
|
||||
'undisclosed vuln': '미공개 취약점',
|
||||
'compromised package': '침해된 패키지',
|
||||
'poisoned source': '오염된 소스',
|
||||
'crafted email': '조작된 이메일',
|
||||
'system tool': '시스템 도구',
|
||||
'memory only': '메모리 전용',
|
||||
'mutating code': '변이 코드',
|
||||
'self-modifying': '자기 변형',
|
||||
'hidden section': '숨겨진 섹션',
|
||||
|
||||
// 레벨 31-40 드롭
|
||||
'propagating mass': '전파되는 덩어리',
|
||||
'c2 beacon': 'C2 비콘',
|
||||
'encrypted key': '암호화된 키',
|
||||
'mining rig': '채굴 장비',
|
||||
'stolen data': '탈취된 데이터',
|
||||
'password hash': '비밀번호 해시',
|
||||
'keystroke log': '키스트로크 로그',
|
||||
'captured frame': '캡처된 프레임',
|
||||
'clipboard data': '클립보드 데이터',
|
||||
'forged record': '위조된 레코드',
|
||||
|
||||
// 레벨 41-45 드롭
|
||||
'plc payload': 'PLC 페이로드',
|
||||
'wiper code': '와이퍼 코드',
|
||||
'smb exploit': 'SMB 익스플로잇',
|
||||
'nsa implant': 'NSA 임플란트',
|
||||
'leaked tool': '유출된 도구',
|
||||
|
||||
// 레벨 46-53 드롭
|
||||
'corrupted scale': '손상된 비늘',
|
||||
'garbled essence': '깨진 정수',
|
||||
'system fragment': '시스템 조각',
|
||||
'shattered block': '산산조각난 블록',
|
||||
'reality tear': '현실의 균열',
|
||||
'divine error': '신성한 오류',
|
||||
'primordial bug': '원초적 버그',
|
||||
|
||||
// 추가 몬스터 드롭 (코드 품질, 공격 등)
|
||||
'old signature': '오래된 시그니처',
|
||||
'outdated syntax': '구식 문법',
|
||||
'tangled logic': '꼬인 로직',
|
||||
'monolithic blob': '모놀리식 덩어리',
|
||||
'loop reference': '루프 참조',
|
||||
'unexplained constant': '설명 없는 상수',
|
||||
'fixed string': '고정된 문자열',
|
||||
'shared state': '공유 상태',
|
||||
'duplicate bug': '중복 버그',
|
||||
'mysterious ritual': '신비로운 의식',
|
||||
'unreachable block': '도달 불가 블록',
|
||||
'undead thread': '언데드 스레드',
|
||||
'parentless process': '부모 없는 프로세스',
|
||||
'ghost reference': '유령 참조',
|
||||
'observer effect': '관찰자 효과',
|
||||
'quantum state': '양자 상태',
|
||||
'deterministic flaw': '결정론적 결함',
|
||||
'fractal complexity': '프랙탈 복잡도',
|
||||
'catastrophic fail': '재앙적 실패',
|
||||
'documentation bug': '문서화 버그',
|
||||
'stealth code': '스텔스 코드',
|
||||
'compressed threat': '압축된 위협',
|
||||
'encrypted payload': '암호화된 페이로드',
|
||||
'delivery mechanism': '전달 메커니즘',
|
||||
'stage one': '1단계',
|
||||
'stage two': '2단계',
|
||||
'startup entry': '시작 항목',
|
||||
'elevated token': '상승된 토큰',
|
||||
'network hop': '네트워크 홉',
|
||||
'covert comm': '은밀한 통신',
|
||||
'command callback': '명령 콜백',
|
||||
'hidden channel': '숨겨진 채널',
|
||||
'ping payload': '핑 페이로드',
|
||||
'web shell': '웹 쉘',
|
||||
'callback conn': '콜백 연결',
|
||||
'listening port': '리스닝 포트',
|
||||
'uploaded script': '업로드된 스크립트',
|
||||
'scheduled task': '예약된 작업',
|
||||
'boot persistence': '부트 지속성',
|
||||
'dll implant': 'DLL 임플란트',
|
||||
'memory injection': '메모리 인젝션',
|
||||
'code injection': '코드 인젝션',
|
||||
'async payload': '비동기 페이로드',
|
||||
'global table': '전역 테이블',
|
||||
'transaction ntfs': '트랜잭션 NTFS',
|
||||
'mapped memory': '매핑된 메모리',
|
||||
'overwritten dll': '덮어쓰인 DLL',
|
||||
'fileless dll': '파일리스 DLL',
|
||||
'custom loader': '커스텀 로더',
|
||||
'direct invoke': '직접 호출',
|
||||
'wow64 transition': 'WoW64 전환',
|
||||
'nested async': '중첩된 비동기',
|
||||
'unhandled await': '처리 안 된 await',
|
||||
'main thread': '메인 스레드',
|
||||
'gc stress': 'GC 스트레스',
|
||||
'allocation spike': '할당 급증',
|
||||
'destructor fail': '소멸자 실패',
|
||||
'soft memory': '소프트 메모리',
|
||||
'pool overflow': '풀 오버플로우',
|
||||
'permgen fill': 'PermGen 가득참',
|
||||
'off-heap grow': 'Off-heap 증가',
|
||||
'nio overflow': 'NIO 오버플로우',
|
||||
'tls accumulate': 'TLS 누적',
|
||||
'socket drain': '소켓 고갈',
|
||||
'descriptor exhaust': '디스크립터 고갈',
|
||||
'event handler': '이벤트 핸들러',
|
||||
'subscription miss': '구독 누락',
|
||||
'pending promise': '대기 중인 Promise',
|
||||
'activity ref': '액티비티 참조',
|
||||
'image buffer': '이미지 버퍼',
|
||||
'db resource': 'DB 리소스',
|
||||
'unclosed io': '닫히지 않은 IO',
|
||||
'uncommitted tx': '커밋 안 된 TX',
|
||||
'orphan session': '고아 세션',
|
||||
'unlimited cache': '무제한 캐시',
|
||||
'unbounded queue': '무한 큐',
|
||||
'circular fail': '순환 실패',
|
||||
'mutex fight': '뮤텍스 싸움',
|
||||
'cpu spin': 'CPU 스핀',
|
||||
'cache line': '캐시 라인',
|
||||
'memory locality': '메모리 지역성',
|
||||
'core binding': '코어 바인딩',
|
||||
'thread thrash': '스레드 스래싱',
|
||||
'page table': '페이지 테이블',
|
||||
'inter-processor': '프로세서 간',
|
||||
'cli hang': 'CLI 행',
|
||||
'scheduler race': '스케줄러 레이스',
|
||||
'read-copy-update': 'RCU',
|
||||
'sequence lock': '시퀀스 락',
|
||||
'reader-writer': '리더-라이터',
|
||||
'fast mutex': '패스트 뮤텍스',
|
||||
'atomic spin': '아토믹 스핀',
|
||||
'sync point': '동기화 지점',
|
||||
'signal wait': '시그널 대기',
|
||||
'count error': '카운트 오류',
|
||||
'ipc fail': 'IPC 실패',
|
||||
'shm corrupt': 'SHM 손상',
|
||||
'fd block': 'FD 블록',
|
||||
'network fd': '네트워크 FD',
|
||||
'event poll': '이벤트 폴',
|
||||
'kernel queue': '커널 큐',
|
||||
'completion port': '완료 포트',
|
||||
'async io': '비동기 IO',
|
||||
'vectored io': '벡터 IO',
|
||||
'sendfile fail': 'sendfile 실패',
|
||||
'pipe transfer': '파이프 전송',
|
||||
'vm splice': 'VM 스플라이스',
|
||||
'pipe duplicate': '파이프 복제',
|
||||
'preallocate': '사전 할당',
|
||||
'sparse file': '희소 파일',
|
||||
'o_direct': 'O_DIRECT',
|
||||
'fsync fail': 'fsync 실패',
|
||||
'fdatasync': 'fdatasync',
|
||||
'write barrier': '쓰기 배리어',
|
||||
'filesystem log': '파일시스템 로그',
|
||||
'metadata exhaust': '메타데이터 고갈',
|
||||
'dcache corrupt': 'dcache 손상',
|
||||
'block buffer': '블록 버퍼',
|
||||
'file cache': '파일 캐시',
|
||||
'kernel alloc': '커널 할당',
|
||||
'kernel malloc': '커널 malloc',
|
||||
'virtual alloc': '가상 할당',
|
||||
'high memory': '상위 메모리',
|
||||
'low memory': '하위 메모리',
|
||||
'out of memory': '메모리 부족',
|
||||
'process flood': '프로세스 홍수',
|
||||
'decompression': '압축 해제',
|
||||
'entity expand': '엔티티 확장',
|
||||
'backtrack': '백트래킹',
|
||||
'hashtable dos': '해시테이블 DoS',
|
||||
'o(n^2) attack': 'O(n²) 공격',
|
||||
'slow http': '느린 HTTP',
|
||||
'slow post': '느린 POST',
|
||||
'range header': '범위 헤더',
|
||||
'hash flood': '해시 홍수',
|
||||
'tcp handshake': 'TCP 핸드셰이크',
|
||||
'datagram storm': '데이터그램 폭풍',
|
||||
'ping storm': '핑 폭풍',
|
||||
'broadcast amp': '브로드캐스트 증폭',
|
||||
'udp amp': 'UDP 증폭',
|
||||
'resolver abuse': '리졸버 남용',
|
||||
'monlist abuse': 'monlist 남용',
|
||||
'upnp abuse': 'UPnP 남용',
|
||||
'cache abuse': '캐시 남용',
|
||||
'ldap abuse': 'LDAP 남용',
|
||||
'spoofed source': '스푸핑된 소스',
|
||||
'distributed target': '분산 타겟',
|
||||
'burst attack': '버스트 공격',
|
||||
'evasive dos': '회피형 DoS',
|
||||
'l7 attack': 'L7 공격',
|
||||
'handshake abuse': '핸드셰이크 남용',
|
||||
'ssl reneg': 'SSL 재협상',
|
||||
'ssl downgrade': 'SSL 다운그레이드',
|
||||
'compression leak': '압축 누출',
|
||||
'http compression': 'HTTP 압축',
|
||||
'ssl3 fallback': 'SSL3 폴백',
|
||||
'openssl leak': 'OpenSSL 누출',
|
||||
'bash bug': 'Bash 버그',
|
||||
'copy on write': 'Copy-on-Write',
|
||||
'vm escape': 'VM 탈출',
|
||||
'buffer overread': '버퍼 오버리드',
|
||||
'wifi handshake': 'WiFi 핸드셰이크',
|
||||
'wpa3 attack': 'WPA3 공격',
|
||||
'wifi frag': 'WiFi 조각화',
|
||||
'wifi encryption': 'WiFi 암호화',
|
||||
'wifi pmk': 'WiFi PMK',
|
||||
'rogue ap': '불량 AP',
|
||||
'probe response': '프로브 응답',
|
||||
'wifi disassoc': 'WiFi 연결해제',
|
||||
'ssid spam': 'SSID 스팸',
|
||||
'bt exploit': 'BT 익스플로잇',
|
||||
'bt remote': 'BT 원격',
|
||||
'ble bug': 'BLE 버그',
|
||||
'bt classic': 'BT 클래식',
|
||||
'bt key': 'BT 키',
|
||||
'bt pairing': 'BT 페어링',
|
||||
'docsis bug': 'DOCSIS 버그',
|
||||
'upnp vuln': 'UPnP 취약점',
|
||||
'tcp/ip bug': 'TCP/IP 버그',
|
||||
'tcpip stack': 'TCP/IP 스택',
|
||||
'tcp random': 'TCP 랜덤',
|
||||
'dns bug': 'DNS 버그',
|
||||
'memory bug': '메모리 버그',
|
||||
'polkit priv': 'Polkit 권한',
|
||||
'privilege escape': '권한 탈출',
|
||||
'heap overflow': '힙 오버플로우',
|
||||
};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// 아스키나라(ASCII-Nara) 세계관 게임 데이터
|
||||
// ASCII NEVER DIE 세계관 게임 데이터
|
||||
// 코드의 신이 창조한 디지털 판타지 세계
|
||||
|
||||
const Map<String, List<String>> pqConfigData = {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"@@locale": "en",
|
||||
|
||||
"appTitle": "ASCII-Nara",
|
||||
"appTitle": "ASCII NEVER DIE",
|
||||
"@appTitle": { "description": "Application title" },
|
||||
|
||||
"tagNoNetwork": "No network",
|
||||
@@ -46,7 +46,7 @@
|
||||
"saveAndExit": "Save and Exit",
|
||||
"@saveAndExit": { "description": "Save and exit button" },
|
||||
|
||||
"progressQuestTitle": "ASCII-Nara - {name}",
|
||||
"progressQuestTitle": "ASCII NEVER DIE - {name}",
|
||||
"@progressQuestTitle": {
|
||||
"description": "Game screen title with character name",
|
||||
"placeholders": {
|
||||
@@ -201,7 +201,7 @@
|
||||
}
|
||||
},
|
||||
|
||||
"welcomeMessage": "Welcome to ASCII-Nara!",
|
||||
"welcomeMessage": "Welcome to ASCII NEVER DIE!",
|
||||
"@welcomeMessage": { "description": "Welcome message in task progress panel" },
|
||||
|
||||
"noSavedGames": "No saved games found.",
|
||||
@@ -242,5 +242,11 @@
|
||||
"placeholders": {
|
||||
"percent": { "type": "int" }
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
"newCharacterTitle": "ASCII NEVER DIE - New Character",
|
||||
"@newCharacterTitle": { "description": "New character screen title" },
|
||||
|
||||
"soldButton": "Sold!",
|
||||
"@soldButton": { "description": "Confirm character creation button" }
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"@@locale": "ja",
|
||||
|
||||
"appTitle": "Ascii Never Die",
|
||||
"appTitle": "ASCII NEVER DIE",
|
||||
"tagNoNetwork": "No network",
|
||||
"tagIdleRpg": "Idle RPG loop",
|
||||
"tagLocalSaves": "Local saves",
|
||||
@@ -71,5 +71,7 @@
|
||||
"roll": "Roll",
|
||||
"race": "Race",
|
||||
"classTitle": "Class",
|
||||
"percentComplete": "{percent}% complete"
|
||||
"percentComplete": "{percent}% complete",
|
||||
"newCharacterTitle": "ASCII NEVER DIE - New Character",
|
||||
"soldButton": "Sold!"
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"@@locale": "ko",
|
||||
|
||||
"appTitle": "아스키나라",
|
||||
"appTitle": "아스키 네버 다이",
|
||||
"tagNoNetwork": "오프라인",
|
||||
"tagIdleRpg": "방치형 RPG",
|
||||
"tagLocalSaves": "로컬 저장",
|
||||
@@ -16,7 +16,7 @@
|
||||
"saveProgressQuestion": "나가기 전에 저장하시겠습니까?",
|
||||
"exitWithoutSaving": "저장하지 않고 종료",
|
||||
"saveAndExit": "저장 후 종료",
|
||||
"progressQuestTitle": "아스키나라 - {name}",
|
||||
"progressQuestTitle": "아스키 네버 다이 - {name}",
|
||||
"levelUp": "레벨 업",
|
||||
"completeQuest": "퀘스트 완료",
|
||||
"completePlot": "플롯 완료",
|
||||
@@ -61,7 +61,7 @@
|
||||
"actNumber": "{number}막",
|
||||
"noActiveQuests": "진행 중인 퀘스트 없음",
|
||||
"questNumber": "퀘스트 #{number}",
|
||||
"welcomeMessage": "아스키나라에 오신 것을 환영합니다!",
|
||||
"welcomeMessage": "아스키 네버 다이에 오신 것을 환영합니다!",
|
||||
"noSavedGames": "저장된 게임이 없습니다.",
|
||||
"loadError": "저장 파일 로드 실패: {error}",
|
||||
"name": "이름",
|
||||
@@ -71,5 +71,7 @@
|
||||
"roll": "굴리기",
|
||||
"race": "종족",
|
||||
"classTitle": "직업",
|
||||
"percentComplete": "{percent}% 완료"
|
||||
"percentComplete": "{percent}% 완료",
|
||||
"newCharacterTitle": "아스키 네버 다이 - 새 캐릭터",
|
||||
"soldButton": "확인!"
|
||||
}
|
||||
|
||||
@@ -104,7 +104,7 @@ abstract class L10n {
|
||||
/// Application title
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'ASCII-Nara'**
|
||||
/// **'ASCII NEVER DIE'**
|
||||
String get appTitle;
|
||||
|
||||
/// Tag indicating offline mode
|
||||
@@ -194,7 +194,7 @@ abstract class L10n {
|
||||
/// Game screen title with character name
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'ASCII-Nara - {name}'**
|
||||
/// **'ASCII NEVER DIE - {name}'**
|
||||
String progressQuestTitle(String name);
|
||||
|
||||
/// Level up tooltip
|
||||
@@ -464,7 +464,7 @@ abstract class L10n {
|
||||
/// Welcome message in task progress panel
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Welcome to ASCII-Nara!'**
|
||||
/// **'Welcome to ASCII NEVER DIE!'**
|
||||
String get welcomeMessage;
|
||||
|
||||
/// No saved games message
|
||||
@@ -526,6 +526,18 @@ abstract class L10n {
|
||||
/// In en, this message translates to:
|
||||
/// **'{percent}% complete'**
|
||||
String percentComplete(int percent);
|
||||
|
||||
/// New character screen title
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'ASCII NEVER DIE - New Character'**
|
||||
String get newCharacterTitle;
|
||||
|
||||
/// Confirm character creation button
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Sold!'**
|
||||
String get soldButton;
|
||||
}
|
||||
|
||||
class _L10nDelegate extends LocalizationsDelegate<L10n> {
|
||||
|
||||
@@ -9,7 +9,7 @@ class L10nEn extends L10n {
|
||||
L10nEn([String locale = 'en']) : super(locale);
|
||||
|
||||
@override
|
||||
String get appTitle => 'ASCII-Nara';
|
||||
String get appTitle => 'ASCII NEVER DIE';
|
||||
|
||||
@override
|
||||
String get tagNoNetwork => 'No network';
|
||||
@@ -55,7 +55,7 @@ class L10nEn extends L10n {
|
||||
|
||||
@override
|
||||
String progressQuestTitle(String name) {
|
||||
return 'ASCII-Nara - $name';
|
||||
return 'ASCII NEVER DIE - $name';
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -197,7 +197,7 @@ class L10nEn extends L10n {
|
||||
}
|
||||
|
||||
@override
|
||||
String get welcomeMessage => 'Welcome to ASCII-Nara!';
|
||||
String get welcomeMessage => 'Welcome to ASCII NEVER DIE!';
|
||||
|
||||
@override
|
||||
String get noSavedGames => 'No saved games found.';
|
||||
@@ -232,4 +232,10 @@ class L10nEn extends L10n {
|
||||
String percentComplete(int percent) {
|
||||
return '$percent% complete';
|
||||
}
|
||||
|
||||
@override
|
||||
String get newCharacterTitle => 'ASCII NEVER DIE - New Character';
|
||||
|
||||
@override
|
||||
String get soldButton => 'Sold!';
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ class L10nJa extends L10n {
|
||||
L10nJa([String locale = 'ja']) : super(locale);
|
||||
|
||||
@override
|
||||
String get appTitle => 'Ascii Never Die';
|
||||
String get appTitle => 'ASCII NEVER DIE';
|
||||
|
||||
@override
|
||||
String get tagNoNetwork => 'No network';
|
||||
@@ -232,4 +232,10 @@ class L10nJa extends L10n {
|
||||
String percentComplete(int percent) {
|
||||
return '$percent% complete';
|
||||
}
|
||||
|
||||
@override
|
||||
String get newCharacterTitle => 'ASCII NEVER DIE - New Character';
|
||||
|
||||
@override
|
||||
String get soldButton => 'Sold!';
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ class L10nKo extends L10n {
|
||||
L10nKo([String locale = 'ko']) : super(locale);
|
||||
|
||||
@override
|
||||
String get appTitle => '아스키나라';
|
||||
String get appTitle => '아스키 네버 다이';
|
||||
|
||||
@override
|
||||
String get tagNoNetwork => '오프라인';
|
||||
@@ -55,7 +55,7 @@ class L10nKo extends L10n {
|
||||
|
||||
@override
|
||||
String progressQuestTitle(String name) {
|
||||
return '아스키나라 - $name';
|
||||
return '아스키 네버 다이 - $name';
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -197,7 +197,7 @@ class L10nKo extends L10n {
|
||||
}
|
||||
|
||||
@override
|
||||
String get welcomeMessage => '아스키나라에 오신 것을 환영합니다!';
|
||||
String get welcomeMessage => '아스키 네버 다이에 오신 것을 환영합니다!';
|
||||
|
||||
@override
|
||||
String get noSavedGames => '저장된 게임이 없습니다.';
|
||||
@@ -232,4 +232,10 @@ class L10nKo extends L10n {
|
||||
String percentComplete(int percent) {
|
||||
return '$percent% 완료';
|
||||
}
|
||||
|
||||
@override
|
||||
String get newCharacterTitle => '아스키 네버 다이 - 새 캐릭터';
|
||||
|
||||
@override
|
||||
String get soldButton => '확인!';
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ class L10nZh extends L10n {
|
||||
L10nZh([String locale = 'zh']) : super(locale);
|
||||
|
||||
@override
|
||||
String get appTitle => 'Ascii Never Die';
|
||||
String get appTitle => 'ASCII NEVER DIE';
|
||||
|
||||
@override
|
||||
String get tagNoNetwork => 'No network';
|
||||
@@ -232,4 +232,10 @@ class L10nZh extends L10n {
|
||||
String percentComplete(int percent) {
|
||||
return '$percent% complete';
|
||||
}
|
||||
|
||||
@override
|
||||
String get newCharacterTitle => 'ASCII NEVER DIE - New Character';
|
||||
|
||||
@override
|
||||
String get soldButton => 'Sold!';
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"@@locale": "zh",
|
||||
|
||||
"appTitle": "Ascii Never Die",
|
||||
"appTitle": "ASCII NEVER DIE",
|
||||
"tagNoNetwork": "No network",
|
||||
"tagIdleRpg": "Idle RPG loop",
|
||||
"tagLocalSaves": "Local saves",
|
||||
@@ -71,5 +71,7 @@
|
||||
"roll": "Roll",
|
||||
"race": "Race",
|
||||
"classTitle": "Class",
|
||||
"percentComplete": "{percent}% complete"
|
||||
"percentComplete": "{percent}% complete",
|
||||
"newCharacterTitle": "ASCII NEVER DIE - New Character",
|
||||
"soldButton": "Sold!"
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:askiineverdie/data/game_text_l10n.dart' as game_l10n;
|
||||
import 'package:askiineverdie/l10n/app_localizations.dart';
|
||||
import 'package:askiineverdie/src/core/engine/game_mutations.dart';
|
||||
import 'package:askiineverdie/src/core/engine/progress_service.dart';
|
||||
@@ -50,7 +51,7 @@ class _AskiiNeverDieAppState extends State<AskiiNeverDieApp> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MaterialApp(
|
||||
title: 'Ascii Never Die',
|
||||
title: 'ASCII NEVER DIE',
|
||||
debugShowCheckedModeBanner: false,
|
||||
localizationsDelegates: L10n.localizationsDelegates,
|
||||
supportedLocales: L10n.supportedLocales,
|
||||
@@ -59,6 +60,12 @@ class _AskiiNeverDieAppState extends State<AskiiNeverDieApp> {
|
||||
scaffoldBackgroundColor: const Color(0xFFF4F5F7),
|
||||
useMaterial3: true,
|
||||
),
|
||||
builder: (context, child) {
|
||||
// 현재 로케일을 게임 텍스트 l10n 시스템에 동기화
|
||||
final locale = Localizations.localeOf(context);
|
||||
game_l10n.setGameLocale(locale.languageCode);
|
||||
return child ?? const SizedBox.shrink();
|
||||
},
|
||||
home: FrontScreen(
|
||||
onNewCharacter: _navigateToNewCharacter,
|
||||
onLoadSave: _loadSave,
|
||||
|
||||
@@ -177,470 +177,290 @@ MonsterCategory getMonsterCategory(String? monsterBaseName) {
|
||||
return MonsterCategory.beast;
|
||||
}
|
||||
|
||||
/// 기본 전투 애니메이션 (beast - 고양이 모양)
|
||||
/// 기본 전투 애니메이션 (beast - 고양이 모양, 심플 3줄)
|
||||
const battleAnimationBeast = AsciiAnimationData(
|
||||
frames: [
|
||||
// 프레임 1: 대치
|
||||
'''
|
||||
,O, /\\___/\\
|
||||
/( )\\ ( o o )
|
||||
/ \\ vs ( =^= )
|
||||
_| |_ /| |\\
|
||||
| | / | | \\
|
||||
_| |_ | |_____| |
|
||||
|_________| |___| |___|''',
|
||||
o vs /\\_/\\
|
||||
/|\\ ( o.o )
|
||||
/ \\ > ^ <''',
|
||||
// 프레임 2: 공격 준비
|
||||
'''
|
||||
O /\\___/\\
|
||||
/|\\----o ( o o )
|
||||
/ \\ ( =^= )
|
||||
_| |_ /| |\\
|
||||
| | / | | \\
|
||||
_| |_ | |_____| |
|
||||
|_________| |___| |___|''',
|
||||
o----o /\\_/\\
|
||||
/|\\ ( o.o )
|
||||
/ \\ > ^ <''',
|
||||
// 프레임 3: 공격 중
|
||||
'''
|
||||
O o--->/\\___/\\
|
||||
/|\\-----------> ( X X )
|
||||
/ \\ ( =^= )
|
||||
_| |_ /| |\\
|
||||
| | / | | \\
|
||||
_| |_ | |_____| |
|
||||
|_________| |___| |___|''',
|
||||
o o-----> /\\_/\\
|
||||
/|\\ ( X.X )
|
||||
/ \\ > ^ <''',
|
||||
// 프레임 4: 히트
|
||||
'''
|
||||
O /\\___/\\
|
||||
/|\\ **** ( X X ) ****
|
||||
/ \\ ** ( =^= ) **
|
||||
_| |_ /| |\\
|
||||
| | / | | \\
|
||||
_| |_ | |_____| |
|
||||
|_________| |___| |___|''',
|
||||
o **** /\\_/\\
|
||||
/|\\ *** ( X.X ) ***
|
||||
/ \\ > ~ <''',
|
||||
// 프레임 5: 복귀
|
||||
'''
|
||||
\\O/ /\\___/\\
|
||||
| ( - - )
|
||||
/ \\ ( =^= )
|
||||
_| |_ /| |\\
|
||||
| | / | | \\
|
||||
_| |_ | |_____| |
|
||||
|_________| |___| |___|''',
|
||||
\\o/ /\\_/\\
|
||||
| ( -.-)
|
||||
/ \\ > ^ <''',
|
||||
],
|
||||
frameIntervalMs: 220,
|
||||
);
|
||||
|
||||
/// 마을/상점 애니메이션 (7줄)
|
||||
/// 마을/상점 애니메이션 (심플 3줄 캐릭터)
|
||||
const townAnimation = AsciiAnimationData(
|
||||
frames: [
|
||||
// 프레임 1: 상점 앞에서 대기
|
||||
'''
|
||||
_______________
|
||||
/ \\ O
|
||||
| SHOP | /|\\
|
||||
| [=====] | / \\
|
||||
| | | | |
|
||||
|___|_____|______| _|_
|
||||
~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~''',
|
||||
___________ o
|
||||
/ SHOP \\/|\\
|
||||
~~|__|____|__|/ \\~~~~~~~~~~~~~''',
|
||||
// 프레임 2: 상점으로 이동
|
||||
'''
|
||||
_______________
|
||||
/ \\ O
|
||||
| SHOP | /|\\
|
||||
| [=====] | / \\
|
||||
| | | | |
|
||||
|___|_____|______| _|_
|
||||
~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~''',
|
||||
// 프레임 3: 상점 앞 도착
|
||||
___________ o
|
||||
/ SHOP \\/|\\
|
||||
~~|__|____|__|/ \\~~~~~~~~~~~~~''',
|
||||
// 프레임 3: 거래 시작
|
||||
'''
|
||||
_______________
|
||||
/ \\ O
|
||||
| SHOP | /|\\
|
||||
| [=====] | / \\
|
||||
| | | | |
|
||||
|___|_____|______| _|_
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~''',
|
||||
___________ o \$
|
||||
/ SHOP \\/|\\ \$
|
||||
~~|__[ @@ ]__|/ \\ \$~~~~~~~~~~~''',
|
||||
// 프레임 4: 거래 중
|
||||
'''
|
||||
_______________
|
||||
/ \\ O \$
|
||||
| SHOP | /|\\ \$
|
||||
| [=====] | /\\\$
|
||||
| | @ | | |
|
||||
|___|_____|______| _|_
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~''',
|
||||
___________ o \$\$
|
||||
/ SHOP \\/|\\ \$\$
|
||||
~~|__[ @@ ]__|/ \\ \$\$~~~~~~~~~~''',
|
||||
// 프레임 5: 거래 완료
|
||||
'''
|
||||
_______________
|
||||
/ \\ \\O/
|
||||
| SHOP | | +
|
||||
| [=====] | / \\ +
|
||||
| | @ | | | +
|
||||
|___|_____|______| _|_
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~''',
|
||||
___________ \\o/ +
|
||||
/ SHOP \\ | +
|
||||
~~|__[ @@ ]__|/ \\ +~~~~~~~~~~~''',
|
||||
],
|
||||
frameIntervalMs: 280,
|
||||
);
|
||||
|
||||
/// 걷는 애니메이션 (7줄, 배경 포함)
|
||||
/// 걷는 애니메이션 (심플 3줄 캐릭터 + 배경)
|
||||
const walkingAnimation = AsciiAnimationData(
|
||||
frames: [
|
||||
// 프레임 1: 서있기
|
||||
'''
|
||||
O
|
||||
/|\\
|
||||
/ \\
|
||||
~~ | ~~
|
||||
~~~~ _|_ ~~~~
|
||||
~~~~~~ ~~~~~~~~ ~~~~~~
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~''',
|
||||
~~~~ o ~~~~
|
||||
~~~~~~ /|\\ ~~~~~~
|
||||
~~~~~~~~ / \\ ~~~~~~~~''',
|
||||
// 프레임 2: 왼발 앞
|
||||
'''
|
||||
O
|
||||
/|\\
|
||||
/|
|
||||
~~ / \\ ~~
|
||||
~~~~ _|_ ~~~~
|
||||
~~~~~~ ~~~~~~~~ ~~~~~~
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~''',
|
||||
~~~~ o ~~~~
|
||||
~~~~~~ /|\\ ~~~~~~
|
||||
~~~~~~~~ /| ~~~~~~~~''',
|
||||
// 프레임 3: 이동 중
|
||||
'''
|
||||
O
|
||||
/|\\
|
||||
|\\
|
||||
~~ / \\ ~~
|
||||
~~~~ _|_ ~~~~
|
||||
~~~~~~ ~~~~~~~~ ~~~~~~
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~''',
|
||||
~~~~ o ~~~~
|
||||
~~~~~~ /|\\ ~~~~~~
|
||||
~~~~~~~~ |\\ ~~~~~~~~''',
|
||||
// 프레임 4: 오른발 앞
|
||||
'''
|
||||
O
|
||||
/|\\
|
||||
|/
|
||||
~~ / \\ ~~
|
||||
~~~~ _|_ ~~~~
|
||||
~~~~~~ ~~~~~~~~ ~~~~~~
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~''',
|
||||
~~~~ o ~~~~
|
||||
~~~~~~ /|\\ ~~~~~~
|
||||
~~~~~~~~ |/ ~~~~~~~~''',
|
||||
// 프레임 5: 복귀
|
||||
'''
|
||||
O
|
||||
/|\\
|
||||
/ \\
|
||||
~~ | ~~
|
||||
~~~~ _|_ ~~~~
|
||||
~~~~~~ ~~~~~~~~ ~~~~~~
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~''',
|
||||
~~~~ o ~~~~
|
||||
~~~~~~ /|\\ ~~~~~~
|
||||
~~~~~~~~ / \\ ~~~~~~~~''',
|
||||
],
|
||||
frameIntervalMs: 180,
|
||||
);
|
||||
|
||||
/// 곤충 전투 애니메이션
|
||||
/// 곤충 전투 애니메이션 (심플 3줄)
|
||||
const battleAnimationInsect = AsciiAnimationData(
|
||||
frames: [
|
||||
// 프레임 1: 대치
|
||||
'''
|
||||
,O, /\\_/\\
|
||||
/( )\\ ( o o )
|
||||
/ \\ vs /|=====|\\
|
||||
_| |_ < | | >
|
||||
| | \\|_____|/
|
||||
_| |_ / \\
|
||||
|_________| /_______\\''',
|
||||
o vs /\\_/\\
|
||||
/|\\ ( o o )
|
||||
/ \\ /|=====|\\''',
|
||||
// 프레임 2: 공격 준비
|
||||
'''
|
||||
O /\\_/\\
|
||||
/|\\----o ( o o )
|
||||
/ \\ /|=====|\\
|
||||
_| |_ < | | >
|
||||
| | \\|_____|/
|
||||
_| |_ / \\
|
||||
|_________| /_______\\''',
|
||||
o----o /\\_/\\
|
||||
/|\\ ( o o )
|
||||
/ \\ /|=====|\\''',
|
||||
// 프레임 3: 공격 중
|
||||
'''
|
||||
O o-->/\\_/\\
|
||||
/|\\----------> ( o o )
|
||||
/ \\ /|=====|\\
|
||||
_| |_ < | | >
|
||||
| | \\|_____|/
|
||||
_| |_ / \\
|
||||
|_________| /_______\\''',
|
||||
o o-----> /\\_/\\
|
||||
/|\\ ( X X )
|
||||
/ \\ /|=====|\\''',
|
||||
// 프레임 4: 히트
|
||||
'''
|
||||
O /\\_/\\
|
||||
/|\\ **** ( X X ) ****
|
||||
/ \\ ** /|=====|\\ **
|
||||
_| |_ < | | >
|
||||
| | \\|_____|/
|
||||
_| |_ / \\
|
||||
|_________| /_______\\''',
|
||||
o **** /\\_/\\
|
||||
/|\\ *** ( X X ) ***
|
||||
/ \\ /|=====|\\''',
|
||||
// 프레임 5: 복귀
|
||||
'''
|
||||
\\O/ /\\_/\\
|
||||
| ( - - )
|
||||
/ \\ /|=====|\\
|
||||
_| |_ < | | >
|
||||
| | \\|_____|/
|
||||
_| |_ / \\
|
||||
|_________| /_______\\''',
|
||||
\\o/ /\\_/\\
|
||||
| ( - - )
|
||||
/ \\ /|=====|\\''',
|
||||
],
|
||||
frameIntervalMs: 220,
|
||||
);
|
||||
|
||||
/// 인간형 전투 애니메이션
|
||||
/// 인간형 전투 애니메이션 (심플 3줄)
|
||||
const battleAnimationHumanoid = AsciiAnimationData(
|
||||
frames: [
|
||||
// 프레임 1: 대치
|
||||
'''
|
||||
,O, O
|
||||
/( )\\ /|\\
|
||||
/ \\ vs / | \\
|
||||
_| |_ ___|___
|
||||
| | | |
|
||||
_| |_ | orc |
|
||||
|_________| |_______|''',
|
||||
o vs O
|
||||
/|\\ /|\\
|
||||
/ \\ / | \\''',
|
||||
// 프레임 2: 공격 준비
|
||||
'''
|
||||
O O
|
||||
/|\\----o /|\\
|
||||
/ \\ / | \\
|
||||
_| |_ ___|___
|
||||
| | | |
|
||||
_| |_ | orc |
|
||||
|_________| |_______|''',
|
||||
o----o O
|
||||
/|\\ /|\\
|
||||
/ \\ / | \\''',
|
||||
// 프레임 3: 공격 중
|
||||
'''
|
||||
O o----> O
|
||||
/|\\-----------> /|\\
|
||||
/ \\ / | \\
|
||||
_| |_ ___|___
|
||||
| | | |
|
||||
_| |_ | orc |
|
||||
|_________| |_______|''',
|
||||
o o-----> O
|
||||
/|\\ X|X
|
||||
/ \\ / | \\''',
|
||||
// 프레임 4: 히트
|
||||
'''
|
||||
O O
|
||||
/|\\ **** X|X ****
|
||||
/ \\ ** / | \\ **
|
||||
_| |_ ___|___
|
||||
| | | |
|
||||
_| |_ | orc |
|
||||
|_________| |_______|''',
|
||||
o **** O
|
||||
/|\\ *** X|X ***
|
||||
/ \\ / | \\''',
|
||||
// 프레임 5: 복귀
|
||||
'''
|
||||
\\O/ O
|
||||
| /|\\
|
||||
/ \\ / | \\
|
||||
_| |_ ___|___
|
||||
| | | |
|
||||
_| |_ | orc |
|
||||
|_________| |_______|''',
|
||||
\\o/ O
|
||||
| /|\\
|
||||
/ \\ / | \\''',
|
||||
],
|
||||
frameIntervalMs: 220,
|
||||
);
|
||||
|
||||
/// 언데드 전투 애니메이션
|
||||
/// 언데드 전투 애니메이션 (심플 3줄)
|
||||
const battleAnimationUndead = AsciiAnimationData(
|
||||
frames: [
|
||||
// 프레임 1: 대치
|
||||
'''
|
||||
,O, .-.
|
||||
/( )\\ (o.o)
|
||||
/ \\ vs |=|
|
||||
_| |_ /|X|\\
|
||||
| | / | | \\
|
||||
_| |_ \\_|_|_/
|
||||
|_________| _/ \\_''',
|
||||
o vs .-.
|
||||
/|\\ (o.o)
|
||||
/ \\ |=|''',
|
||||
// 프레임 2: 공격 준비
|
||||
'''
|
||||
O .-.
|
||||
/|\\----o (o.o)
|
||||
/ \\ |=|
|
||||
_| |_ /|X|\\
|
||||
| | / | | \\
|
||||
_| |_ \\_|_|_/
|
||||
|_________| _/ \\_''',
|
||||
o----o .-.
|
||||
/|\\ (o.o)
|
||||
/ \\ |=|''',
|
||||
// 프레임 3: 공격 중
|
||||
'''
|
||||
O o--->.-.
|
||||
/|\\-----------> (o.o)
|
||||
/ \\ |=|
|
||||
_| |_ /|X|\\
|
||||
| | / | | \\
|
||||
_| |_ \\_|_|_/
|
||||
|_________| _/ \\_''',
|
||||
o o-----> .-.
|
||||
/|\\ (X.X)
|
||||
/ \\ |=|''',
|
||||
// 프레임 4: 히트
|
||||
'''
|
||||
O .-.
|
||||
/|\\ **** (X.X) ****
|
||||
/ \\ ** |=| **
|
||||
_| |_ /|X|\\
|
||||
| | / | | \\
|
||||
_| |_ \\_|_|_/
|
||||
|_________| _/ \\_''',
|
||||
o **** .-.
|
||||
/|\\ *** (X.X) ***
|
||||
/ \\ |~|''',
|
||||
// 프레임 5: 복귀
|
||||
'''
|
||||
\\O/ .-.
|
||||
| (-.-)
|
||||
/ \\ |=|
|
||||
_| |_ /|X|\\
|
||||
| | / | | \\
|
||||
_| |_ \\_|_|_/
|
||||
|_________| _/ \\_''',
|
||||
\\o/ .-.
|
||||
| (-.-)
|
||||
/ \\ |=|''',
|
||||
],
|
||||
frameIntervalMs: 250,
|
||||
);
|
||||
|
||||
/// 드래곤 전투 애니메이션
|
||||
/// 드래곤 전투 애니메이션 (심플 3줄)
|
||||
const battleAnimationDragon = AsciiAnimationData(
|
||||
frames: [
|
||||
// 프레임 1: 대치
|
||||
'''
|
||||
,O, __/\\__
|
||||
/( )\\ / \\
|
||||
/ \\ vs < (O)(O) >
|
||||
_| |_ \\ \\/ /
|
||||
| | \\ /
|
||||
_| |_ /|\\~~~/|\\
|
||||
|_________| /_________\\''',
|
||||
o vs __/\\__
|
||||
/|\\ < (O)(O) >
|
||||
/ \\ \\ \\/ /''',
|
||||
// 프레임 2: 공격 준비
|
||||
'''
|
||||
O __/\\__
|
||||
/|\\----o / \\
|
||||
/ \\ < (O)(O) >
|
||||
_| |_ \\ \\/ /
|
||||
| | \\ /
|
||||
_| |_ /|\\~~~/|\\
|
||||
|_________| /_________\\''',
|
||||
o----o __/\\__
|
||||
/|\\ < (O)(O) >
|
||||
/ \\ \\ \\/ /''',
|
||||
// 프레임 3: 공격 중
|
||||
'''
|
||||
O o--->__/\\__
|
||||
/|\\---------> / \\
|
||||
/ \\ < (O)(O) >
|
||||
_| |_ \\ \\/ /
|
||||
| | \\ /
|
||||
_| |_ /|\\~~~/|\\
|
||||
|_________| /_________\\''',
|
||||
o o-----> __/\\__
|
||||
/|\\ < (X)(X) >
|
||||
/ \\ \\ \\/ /''',
|
||||
// 프레임 4: 히트
|
||||
'''
|
||||
O __/\\__
|
||||
/|\\ **** / >< \\ ****
|
||||
/ \\ ** < (X)(X) > **
|
||||
_| |_ \\ \\/ /
|
||||
| | \\ /
|
||||
_| |_ /|\\~~~/|\\
|
||||
|_________| /_________\\''',
|
||||
o **** __/\\__
|
||||
/|\\ *** < (X)(X) > ***
|
||||
/ \\ \\ ~~ /''',
|
||||
// 프레임 5: 복귀
|
||||
'''
|
||||
\\O/ __/\\__
|
||||
| / \\
|
||||
/ \\ < (-)(-)>
|
||||
_| |_ \\ \\/ /
|
||||
| | \\ /
|
||||
_| |_ /|\\~~~/|\\
|
||||
|_________| /_________\\''',
|
||||
\\o/ __/\\__
|
||||
| < (-)(-)>
|
||||
/ \\ \\ \\/ /''',
|
||||
],
|
||||
frameIntervalMs: 200,
|
||||
);
|
||||
|
||||
/// 슬라임 전투 애니메이션
|
||||
/// 슬라임 전투 애니메이션 (심플 3줄)
|
||||
const battleAnimationSlime = AsciiAnimationData(
|
||||
frames: [
|
||||
// 프레임 1: 대치
|
||||
'''
|
||||
,O, .---.
|
||||
/( )\\ / \\
|
||||
/ \\ vs ( o o )
|
||||
_| |_ \\ ~ /
|
||||
| | '---'
|
||||
_| |_ ~~~~~~~
|
||||
|_________| ~~~~~~~~~''',
|
||||
o vs .---.
|
||||
/|\\ ( o o )
|
||||
/ \\ ~~~~~''',
|
||||
// 프레임 2: 공격 준비
|
||||
'''
|
||||
O .---.
|
||||
/|\\----o / \\
|
||||
/ \\ ( o o )
|
||||
_| |_ \\ ~ /
|
||||
| | '---'
|
||||
_| |_ ~~~~~~~
|
||||
|_________| ~~~~~~~~~''',
|
||||
o----o .---.
|
||||
/|\\ ( o o )
|
||||
/ \\ ~~~~~''',
|
||||
// 프레임 3: 공격 중
|
||||
'''
|
||||
O o--->.---.
|
||||
/|\\---------> / \\
|
||||
/ \\ ( o o )
|
||||
_| |_ \\ ~ /
|
||||
| | '---'
|
||||
_| |_ ~~~~~~~
|
||||
|_________| ~~~~~~~~~''',
|
||||
o o-----> .---.
|
||||
/|\\ ( X X )
|
||||
/ \\ ~~~~~''',
|
||||
// 프레임 4: 히트
|
||||
'''
|
||||
O .---.
|
||||
/|\\ **** / X X \\ ****
|
||||
/ \\ ** ( ~ ) **
|
||||
_| |_ \\ /
|
||||
| | '---'
|
||||
_| |_ ~~~~~~~
|
||||
|_________| ~~~~~~~~~''',
|
||||
o **** .---.
|
||||
/|\\ *** ( X X ) ***
|
||||
/ \\ ~~~~~''',
|
||||
// 프레임 5: 복귀
|
||||
'''
|
||||
\\O/ .---.
|
||||
| / \\
|
||||
/ \\ ( - - )
|
||||
_| |_ \\ ~ /
|
||||
| | '---'
|
||||
_| |_ ~~~~~~~
|
||||
|_________| ~~~~~~~~~''',
|
||||
\\o/ .---.
|
||||
| ( - - )
|
||||
/ \\ ~~~~~''',
|
||||
],
|
||||
frameIntervalMs: 280,
|
||||
);
|
||||
|
||||
/// 악마 전투 애니메이션
|
||||
/// 악마 전투 애니메이션 (심플 3줄)
|
||||
const battleAnimationDemon = AsciiAnimationData(
|
||||
frames: [
|
||||
// 프레임 1: 대치
|
||||
'''
|
||||
,O, /\\ /\\
|
||||
/( )\\ ( \\ / )
|
||||
/ \\ vs \\ o o /
|
||||
_| |_ | V |
|
||||
| | | ~~~ |
|
||||
_| |_ /| |\\
|
||||
|_________| /___|___|_\\''',
|
||||
o vs /\\ /\\
|
||||
/|\\ ( o V o )
|
||||
/ \\ \\ ~~~ /''',
|
||||
// 프레임 2: 공격 준비
|
||||
'''
|
||||
O /\\ /\\
|
||||
/|\\----o ( \\ / )
|
||||
/ \\ \\ o o /
|
||||
_| |_ | V |
|
||||
| | | ~~~ |
|
||||
_| |_ /| |\\
|
||||
|_________| /___|___|_\\''',
|
||||
o----o /\\ /\\
|
||||
/|\\ ( o V o )
|
||||
/ \\ \\ ~~~ /''',
|
||||
// 프레임 3: 공격 중
|
||||
'''
|
||||
O o--->/\\ /\\
|
||||
/|\\--------> ( \\ / )
|
||||
/ \\ \\ o o /
|
||||
_| |_ | V |
|
||||
| | | ~~~ |
|
||||
_| |_ /| |\\
|
||||
|_________| /___|___|_\\''',
|
||||
o o-----> /\\ /\\
|
||||
/|\\ ( X V X )
|
||||
/ \\ \\ ~~~ /''',
|
||||
// 프레임 4: 히트
|
||||
'''
|
||||
O /\\ /\\
|
||||
/|\\ **** ( X X ) ****
|
||||
/ \\ ** \\ X X / **
|
||||
_| |_ | V |
|
||||
| | | ~~~ |
|
||||
_| |_ /| |\\
|
||||
|_________| /___|___|_\\''',
|
||||
o **** /\\ /\\
|
||||
/|\\ *** ( X V X ) ***
|
||||
/ \\ \\ ~~~ /''',
|
||||
// 프레임 5: 복귀
|
||||
'''
|
||||
\\O/ /\\ /\\
|
||||
| ( \\ / )
|
||||
/ \\ \\ - - /
|
||||
_| |_ | V |
|
||||
| | | ~~~ |
|
||||
_| |_ /| |\\
|
||||
|_________| /___|___|_\\''',
|
||||
\\o/ /\\ /\\
|
||||
| ( - V - )
|
||||
/ \\ \\ ~~~ /''',
|
||||
],
|
||||
frameIntervalMs: 200,
|
||||
);
|
||||
|
||||
178
lib/src/core/animation/background_data.dart
Normal file
178
lib/src/core/animation/background_data.dart
Normal file
@@ -0,0 +1,178 @@
|
||||
// 환경별 배경 패턴 데이터
|
||||
// ASCII Patrol 스타일 - 패럴렉스 스크롤링 배경
|
||||
|
||||
import 'package:askiineverdie/src/core/animation/background_layer.dart';
|
||||
|
||||
/// 환경별 배경 레이어 반환
|
||||
List<BackgroundLayer> getBackgroundLayers(EnvironmentType environment) {
|
||||
return switch (environment) {
|
||||
EnvironmentType.town => _townLayers,
|
||||
EnvironmentType.forest => _forestLayers,
|
||||
EnvironmentType.cave => _caveLayers,
|
||||
EnvironmentType.dungeon => _dungeonLayers,
|
||||
EnvironmentType.tech => _techLayers,
|
||||
EnvironmentType.void_ => _voidLayers,
|
||||
};
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 마을 (Town) - 건물 실루엣
|
||||
// ============================================================================
|
||||
const _townLayers = [
|
||||
// 원경 - 하늘/별
|
||||
BackgroundLayer(
|
||||
lines: [r'. * . * . * . * . * . * '],
|
||||
scrollSpeed: 0.05,
|
||||
yStart: 0,
|
||||
),
|
||||
// 중경 - 건물 실루엣
|
||||
BackgroundLayer(
|
||||
lines: [
|
||||
r' _|__|_ _|__|_ _|__|_ ',
|
||||
r' | | | | | | ',
|
||||
],
|
||||
scrollSpeed: 0.15,
|
||||
yStart: 1,
|
||||
),
|
||||
// 전경 - 바닥
|
||||
BackgroundLayer(
|
||||
lines: [r'====[]====[]====[]====[]====[]====[]'],
|
||||
scrollSpeed: 0.3,
|
||||
yStart: 7,
|
||||
),
|
||||
];
|
||||
|
||||
// ============================================================================
|
||||
// 숲 (Forest) - 나무
|
||||
// ============================================================================
|
||||
const _forestLayers = [
|
||||
// 원경 - 하늘/별
|
||||
BackgroundLayer(
|
||||
lines: [r'. * . * . * . * . * '],
|
||||
scrollSpeed: 0.05,
|
||||
yStart: 0,
|
||||
),
|
||||
// 중경 - 나무 실루엣
|
||||
BackgroundLayer(
|
||||
lines: [
|
||||
r' ,@@@, ,@@, ,@@@',
|
||||
r' @@ @@ @@ @@ @@ ',
|
||||
],
|
||||
scrollSpeed: 0.15,
|
||||
yStart: 1,
|
||||
),
|
||||
// 전경 - 풀/바닥
|
||||
BackgroundLayer(
|
||||
lines: [r'____||____||____||____||____||____||'],
|
||||
scrollSpeed: 0.3,
|
||||
yStart: 7,
|
||||
),
|
||||
];
|
||||
|
||||
// ============================================================================
|
||||
// 동굴 (Cave) - 바위
|
||||
// ============================================================================
|
||||
const _caveLayers = [
|
||||
// 천장
|
||||
BackgroundLayer(
|
||||
lines: [r'vvVVvvVVvvVVvvVVvvVVvvVVvvVVvvVVvvVV'],
|
||||
scrollSpeed: 0.1,
|
||||
yStart: 0,
|
||||
),
|
||||
// 종유석
|
||||
BackgroundLayer(
|
||||
lines: [
|
||||
r' | V | V | ',
|
||||
r' V V V ',
|
||||
],
|
||||
scrollSpeed: 0.15,
|
||||
yStart: 1,
|
||||
),
|
||||
// 바닥 - 석순
|
||||
BackgroundLayer(
|
||||
lines: [r'^__/\__^__/\__^__/\__^__/\__^__/\__^'],
|
||||
scrollSpeed: 0.25,
|
||||
yStart: 7,
|
||||
),
|
||||
];
|
||||
|
||||
// ============================================================================
|
||||
// 던전 (Dungeon) - 벽돌
|
||||
// ============================================================================
|
||||
const _dungeonLayers = [
|
||||
// 천장 - 벽돌
|
||||
BackgroundLayer(
|
||||
lines: [r'####|####|####|####|####|####|####|#'],
|
||||
scrollSpeed: 0.1,
|
||||
yStart: 0,
|
||||
),
|
||||
// 횃불
|
||||
BackgroundLayer(
|
||||
lines: [
|
||||
r' * * * ',
|
||||
r' )| )| )| ',
|
||||
],
|
||||
scrollSpeed: 0.15,
|
||||
yStart: 1,
|
||||
),
|
||||
// 바닥 - 타일
|
||||
BackgroundLayer(
|
||||
lines: [r'====[]====[]====[]====[]====[]====[]'],
|
||||
scrollSpeed: 0.25,
|
||||
yStart: 7,
|
||||
),
|
||||
];
|
||||
|
||||
// ============================================================================
|
||||
// 기술 (Tech) - 회로
|
||||
// ============================================================================
|
||||
const _techLayers = [
|
||||
// 상단 - 회로
|
||||
BackgroundLayer(
|
||||
lines: [r'-+-+-+-||-+-+-+-||-+-+-+-||-+-+-+-||'],
|
||||
scrollSpeed: 0.1,
|
||||
yStart: 0,
|
||||
),
|
||||
// 데이터 스트림
|
||||
BackgroundLayer(
|
||||
lines: [
|
||||
r' 10110 01101 10110 01101 101',
|
||||
r' 01 10 01 10 ',
|
||||
],
|
||||
scrollSpeed: 0.2,
|
||||
yStart: 2,
|
||||
),
|
||||
// 바닥 - 패널
|
||||
BackgroundLayer(
|
||||
lines: [r'[====][====][====][====][====][====]'],
|
||||
scrollSpeed: 0.3,
|
||||
yStart: 7,
|
||||
),
|
||||
];
|
||||
|
||||
// ============================================================================
|
||||
// 보이드 (Void) - 별/공허
|
||||
// ============================================================================
|
||||
const _voidLayers = [
|
||||
// 별
|
||||
BackgroundLayer(
|
||||
lines: [r' * . * . * . * . * '],
|
||||
scrollSpeed: 0.03,
|
||||
yStart: 0,
|
||||
),
|
||||
// 은하
|
||||
BackgroundLayer(
|
||||
lines: [
|
||||
r' ~*~ ~*~ ~*~ ',
|
||||
r' *~ ~* *~ ~* *~ ~*',
|
||||
],
|
||||
scrollSpeed: 0.08,
|
||||
yStart: 2,
|
||||
),
|
||||
// 심연
|
||||
BackgroundLayer(
|
||||
lines: [r'~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.'],
|
||||
scrollSpeed: 0.15,
|
||||
yStart: 7,
|
||||
),
|
||||
];
|
||||
92
lib/src/core/animation/background_layer.dart
Normal file
92
lib/src/core/animation/background_layer.dart
Normal file
@@ -0,0 +1,92 @@
|
||||
// 배경 레이어 시스템 (ASCII Patrol 스타일 패럴렉스)
|
||||
// 각 환경은 여러 레이어로 구성되며, 레이어마다 다른 스크롤 속도를 가짐
|
||||
|
||||
/// 배경 레이어 데이터
|
||||
class BackgroundLayer {
|
||||
const BackgroundLayer({
|
||||
required this.lines,
|
||||
required this.scrollSpeed,
|
||||
this.yStart = 0,
|
||||
});
|
||||
|
||||
/// 레이어 패턴 (각 줄은 반복 가능한 패턴)
|
||||
final List<String> lines;
|
||||
|
||||
/// 스크롤 속도 (0.0 = 정지, 1.0 = 최고속)
|
||||
/// 원경일수록 느리게, 전경일수록 빠르게
|
||||
final double scrollSpeed;
|
||||
|
||||
/// 시작 Y 위치 (0~7)
|
||||
final int yStart;
|
||||
}
|
||||
|
||||
/// 환경 타입
|
||||
enum EnvironmentType {
|
||||
/// 마을 - 건물 실루엣
|
||||
town,
|
||||
|
||||
/// 숲 - 나무
|
||||
forest,
|
||||
|
||||
/// 동굴 - 바위
|
||||
cave,
|
||||
|
||||
/// 던전 - 벽돌
|
||||
dungeon,
|
||||
|
||||
/// 기술 - 회로
|
||||
tech,
|
||||
|
||||
/// 보이드 - 별/공허 (보스)
|
||||
void_,
|
||||
}
|
||||
|
||||
/// TaskType과 몬스터 이름에서 환경 타입 추론
|
||||
EnvironmentType inferEnvironment(String? taskType, String? monsterName) {
|
||||
// 마을 관련 태스크
|
||||
if (taskType == 'heading' || taskType == 'buyEquip') {
|
||||
return EnvironmentType.town;
|
||||
}
|
||||
|
||||
// 몬스터 이름에서 환경 추론
|
||||
if (monsterName != null) {
|
||||
final lower = monsterName.toLowerCase();
|
||||
|
||||
// 보이드/우주
|
||||
if (lower.contains('void') ||
|
||||
lower.contains('cosmic') ||
|
||||
lower.contains('star') ||
|
||||
lower.contains('galaxy')) {
|
||||
return EnvironmentType.void_;
|
||||
}
|
||||
|
||||
// 기술/사이버
|
||||
if (lower.contains('cyber') ||
|
||||
lower.contains('robot') ||
|
||||
lower.contains('ai') ||
|
||||
lower.contains('data') ||
|
||||
lower.contains('server')) {
|
||||
return EnvironmentType.tech;
|
||||
}
|
||||
|
||||
// 언데드/던전
|
||||
if (lower.contains('zombie') ||
|
||||
lower.contains('skeleton') ||
|
||||
lower.contains('ghost') ||
|
||||
lower.contains('undead') ||
|
||||
lower.contains('dungeon')) {
|
||||
return EnvironmentType.dungeon;
|
||||
}
|
||||
|
||||
// 동굴
|
||||
if (lower.contains('cave') ||
|
||||
lower.contains('bat') ||
|
||||
lower.contains('spider') ||
|
||||
lower.contains('worm')) {
|
||||
return EnvironmentType.cave;
|
||||
}
|
||||
}
|
||||
|
||||
// 기본: 숲
|
||||
return EnvironmentType.forest;
|
||||
}
|
||||
713
lib/src/core/animation/battle_composer.dart
Normal file
713
lib/src/core/animation/battle_composer.dart
Normal file
@@ -0,0 +1,713 @@
|
||||
// BattleComposer - 전투 프레임 실시간 합성
|
||||
// Stone Story RPG 스타일 참고 - 8줄 캐릭터/몬스터, 60자 폭
|
||||
// ASCII Patrol 스타일 패럴렉스 배경
|
||||
|
||||
import 'package:askiineverdie/src/core/animation/ascii_animation_data.dart';
|
||||
import 'package:askiineverdie/src/core/animation/background_data.dart';
|
||||
import 'package:askiineverdie/src/core/animation/background_layer.dart';
|
||||
import 'package:askiineverdie/src/core/animation/character_frames.dart';
|
||||
import 'package:askiineverdie/src/core/animation/monster_size.dart';
|
||||
import 'package:askiineverdie/src/core/animation/weapon_category.dart';
|
||||
import 'package:askiineverdie/src/core/animation/weapon_effects.dart';
|
||||
|
||||
/// 전투 프레임 합성기
|
||||
class BattleComposer {
|
||||
const BattleComposer({
|
||||
required this.weaponCategory,
|
||||
required this.hasShield,
|
||||
required this.monsterCategory,
|
||||
required this.monsterSize,
|
||||
});
|
||||
|
||||
final WeaponCategory weaponCategory;
|
||||
final bool hasShield;
|
||||
final MonsterCategory monsterCategory;
|
||||
final MonsterSize monsterSize;
|
||||
|
||||
/// 전체 프레임 폭 (문자 수)
|
||||
static const int frameWidth = 60;
|
||||
|
||||
/// 프레임 높이 (줄 수)
|
||||
static const int frameHeight = 8;
|
||||
|
||||
/// 영역 분할
|
||||
static const int characterWidth = 18;
|
||||
static const int effectWidth = 24;
|
||||
static const int monsterWidth = 18;
|
||||
|
||||
/// 전투 프레임 생성 (배경 없음)
|
||||
String composeFrame(BattlePhase phase, int subFrame, String? monsterBaseName) {
|
||||
// 캐릭터 프레임
|
||||
var charFrame = getCharacterFrame(phase, subFrame);
|
||||
if (hasShield) {
|
||||
charFrame = charFrame.withShield();
|
||||
}
|
||||
|
||||
// 몬스터 프레임 (애니메이션 포함)
|
||||
final monsterFrames =
|
||||
_getAnimatedMonsterFrames(monsterCategory, monsterSize, phase);
|
||||
final monsterFrame = monsterFrames[subFrame % monsterFrames.length];
|
||||
|
||||
// 무기 이펙트 (단일 라인)
|
||||
final effect = getWeaponEffect(weaponCategory);
|
||||
final effectLine = _getEffectLine(effect, phase, subFrame);
|
||||
|
||||
// 프레임 합성
|
||||
return _compose(charFrame.lines, monsterFrame, effectLine, phase);
|
||||
}
|
||||
|
||||
/// 전투 프레임 생성 (배경 포함, ASCII Patrol 스타일)
|
||||
String composeFrameWithBackground(
|
||||
BattlePhase phase,
|
||||
int subFrame,
|
||||
String? monsterBaseName,
|
||||
EnvironmentType environment,
|
||||
int globalTick,
|
||||
) {
|
||||
// 1. 8x60 캔버스 생성 (공백으로 초기화)
|
||||
final canvas =
|
||||
List.generate(frameHeight, (_) => List.filled(frameWidth, ' '));
|
||||
|
||||
// 2. 배경 레이어 그리기 (뒤에서 앞으로)
|
||||
final layers = getBackgroundLayers(environment);
|
||||
for (final layer in layers) {
|
||||
_drawBackgroundLayer(canvas, layer, globalTick);
|
||||
}
|
||||
|
||||
// 3. 캐릭터 프레임 (정규화하여 왼쪽 정렬)
|
||||
var charFrame = getCharacterFrame(phase, subFrame);
|
||||
if (hasShield) {
|
||||
charFrame = charFrame.withShield();
|
||||
}
|
||||
final normalizedChar = _normalizeSprite(charFrame.lines, characterWidth);
|
||||
final charY = frameHeight - normalizedChar.length;
|
||||
_overlaySpriteWithSpaces(canvas, normalizedChar, 0, charY);
|
||||
|
||||
// 4. 몬스터 프레임 (정규화하여 오른쪽 정렬)
|
||||
final monsterFrames =
|
||||
_getAnimatedMonsterFrames(monsterCategory, monsterSize, phase);
|
||||
final monsterFrame = monsterFrames[subFrame % monsterFrames.length];
|
||||
final normalizedMonster = _normalizeSpriteRight(monsterFrame, monsterWidth);
|
||||
final monsterX = frameWidth - monsterWidth;
|
||||
final monsterY = frameHeight - normalizedMonster.length;
|
||||
_overlaySpriteWithSpaces(canvas, normalizedMonster, monsterX, monsterY);
|
||||
|
||||
// 5. 멀티라인 이펙트 오버레이 (공격/히트/준비 페이즈)
|
||||
if (phase == BattlePhase.prepare ||
|
||||
phase == BattlePhase.attack ||
|
||||
phase == BattlePhase.hit) {
|
||||
final effect = getWeaponEffect(weaponCategory);
|
||||
final effectLines = _getEffectLines(effect, phase, subFrame);
|
||||
if (effectLines.isNotEmpty) {
|
||||
// 이펙트 Y 위치: 캐릭터 팔 높이 (2번째 줄, 몸통) 기준
|
||||
final effectY = charY + 1;
|
||||
for (var i = 0; i < effectLines.length; i++) {
|
||||
final y = effectY + i;
|
||||
if (y >= 0 && y < frameHeight && effectLines[i].isNotEmpty) {
|
||||
_overlayText(canvas, effectLines[i], characterWidth, y);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 6. 문자열로 변환
|
||||
return canvas.map((row) => row.join()).join('\n');
|
||||
}
|
||||
|
||||
/// 스프라이트를 지정 폭으로 정규화 (왼쪽 정렬)
|
||||
List<String> _normalizeSprite(List<String> sprite, int width) {
|
||||
return sprite.map((line) => line.padRight(width).substring(0, width)).toList();
|
||||
}
|
||||
|
||||
/// 스프라이트를 지정 폭으로 정규화 (오른쪽 정렬)
|
||||
List<String> _normalizeSpriteRight(List<String> sprite, int width) {
|
||||
return sprite.map((line) {
|
||||
final trimmed = line.trimRight();
|
||||
if (trimmed.length >= width) return trimmed.substring(0, width);
|
||||
return trimmed.padLeft(width);
|
||||
}).toList();
|
||||
}
|
||||
|
||||
/// 스프라이트를 캔버스에 오버레이 (공백도 덮어쓰기 - Z-order용)
|
||||
void _overlaySpriteWithSpaces(
|
||||
List<List<String>> canvas,
|
||||
List<String> sprite,
|
||||
int startX,
|
||||
int startY,
|
||||
) {
|
||||
for (var i = 0; i < sprite.length; i++) {
|
||||
final y = startY + i;
|
||||
if (y < 0 || y >= frameHeight) continue;
|
||||
|
||||
final line = sprite[i];
|
||||
for (var j = 0; j < line.length; j++) {
|
||||
final x = startX + j;
|
||||
if (x < 0 || x >= frameWidth) continue;
|
||||
|
||||
final char = line[j];
|
||||
// 공백이 아닌 문자만 덮어쓰기 (투명 배경 효과)
|
||||
if (char != ' ') {
|
||||
canvas[y][x] = char;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 배경 레이어를 캔버스에 그리기
|
||||
void _drawBackgroundLayer(
|
||||
List<List<String>> canvas,
|
||||
BackgroundLayer layer,
|
||||
int globalTick,
|
||||
) {
|
||||
for (var i = 0; i < layer.lines.length; i++) {
|
||||
final y = layer.yStart + i;
|
||||
if (y >= frameHeight) break;
|
||||
|
||||
final pattern = layer.lines[i];
|
||||
if (pattern.isEmpty) continue;
|
||||
|
||||
// 스크롤 오프셋 계산
|
||||
final offset = (globalTick * layer.scrollSpeed).toInt() % pattern.length;
|
||||
|
||||
// 패턴을 스크롤하며 그리기
|
||||
for (var x = 0; x < frameWidth; x++) {
|
||||
final patternIdx = (x + offset) % pattern.length;
|
||||
final char = pattern[patternIdx];
|
||||
if (char != ' ') {
|
||||
canvas[y][x] = char;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 텍스트를 캔버스에 오버레이
|
||||
void _overlayText(
|
||||
List<List<String>> canvas,
|
||||
String text,
|
||||
int startX,
|
||||
int y,
|
||||
) {
|
||||
if (y < 0 || y >= frameHeight) return;
|
||||
|
||||
for (var i = 0; i < text.length; i++) {
|
||||
final x = startX + i;
|
||||
if (x < 0 || x >= frameWidth) continue;
|
||||
|
||||
final char = text[i];
|
||||
if (char != ' ') {
|
||||
canvas[y][x] = char;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 멀티라인 이펙트 프레임 반환
|
||||
List<String> _getEffectLines(
|
||||
WeaponEffect effect, BattlePhase phase, int subFrame) {
|
||||
final frames = switch (phase) {
|
||||
BattlePhase.idle => <List<String>>[],
|
||||
BattlePhase.prepare => effect.prepareFrames,
|
||||
BattlePhase.attack => effect.attackFrames,
|
||||
BattlePhase.hit => effect.hitFrames,
|
||||
BattlePhase.recover => <List<String>>[],
|
||||
};
|
||||
if (frames.isEmpty) return [];
|
||||
return frames[subFrame % frames.length];
|
||||
}
|
||||
|
||||
/// 단일 라인 이펙트 (하위 호환용)
|
||||
String _getEffectLine(WeaponEffect effect, BattlePhase phase, int subFrame) {
|
||||
final lines = _getEffectLines(effect, phase, subFrame);
|
||||
if (lines.isEmpty) return '';
|
||||
// 멀티라인 중 중간 라인 반환 (메인 이펙트)
|
||||
final midIndex = lines.length ~/ 2;
|
||||
return lines.length > midIndex ? lines[midIndex] : lines.first;
|
||||
}
|
||||
|
||||
String _compose(
|
||||
List<String> charLines,
|
||||
List<String> monsterLines,
|
||||
String effectLine,
|
||||
BattlePhase phase,
|
||||
) {
|
||||
final result = <String>[];
|
||||
|
||||
// 캐릭터와 몬스터를 하단 정렬 (8줄 기준)
|
||||
final charOffset = frameHeight - charLines.length;
|
||||
final monsterOffset = frameHeight - monsterLines.length;
|
||||
|
||||
// 이펙트 Y 위치: 캐릭터 body/arm 줄 (charOffset + 1)
|
||||
final effectRow = charOffset + 1;
|
||||
|
||||
for (var i = 0; i < frameHeight; i++) {
|
||||
// 캐릭터 파트 (왼쪽 18자)
|
||||
final charIdx = i - charOffset;
|
||||
final charPart =
|
||||
(charIdx >= 0 && charIdx < charLines.length ? charLines[charIdx] : '')
|
||||
.padRight(characterWidth);
|
||||
|
||||
// 이펙트 파트 (중앙 24자) - 캐릭터 팔 높이에 표시
|
||||
String effectPart = '';
|
||||
if (i == effectRow &&
|
||||
(phase == BattlePhase.attack || phase == BattlePhase.hit)) {
|
||||
effectPart = effectLine;
|
||||
}
|
||||
effectPart = effectPart.padRight(effectWidth);
|
||||
|
||||
// 몬스터 파트 (오른쪽 18자)
|
||||
final monsterIdx = i - monsterOffset;
|
||||
final monsterPart = (monsterIdx >= 0 && monsterIdx < monsterLines.length
|
||||
? monsterLines[monsterIdx]
|
||||
: '')
|
||||
.padLeft(monsterWidth);
|
||||
|
||||
result.add('$charPart$effectPart$monsterPart');
|
||||
}
|
||||
|
||||
return result.join('\n');
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 몬스터 애니메이션 프레임
|
||||
// ============================================================================
|
||||
|
||||
/// 몬스터 애니메이션 프레임 반환 (페이즈별 다른 동작)
|
||||
List<List<String>> _getAnimatedMonsterFrames(
|
||||
MonsterCategory category,
|
||||
MonsterSize size,
|
||||
BattlePhase phase,
|
||||
) {
|
||||
// 피격 상태
|
||||
if (phase == BattlePhase.hit) {
|
||||
return _getMonsterHitFrames(category, size);
|
||||
}
|
||||
// 경계 상태 (prepare, attack)
|
||||
if (phase == BattlePhase.prepare || phase == BattlePhase.attack) {
|
||||
return _getMonsterAlertFrames(category, size);
|
||||
}
|
||||
// 일반 상태 (idle, recover)
|
||||
return _getMonsterIdleFrames(category, size);
|
||||
}
|
||||
|
||||
/// 일반 상태 몬스터 프레임
|
||||
List<List<String>> _getMonsterIdleFrames(MonsterCategory category, MonsterSize size) {
|
||||
return switch (size) {
|
||||
MonsterSize.tiny => _tinyIdleFrames(category),
|
||||
MonsterSize.small => _smallIdleFrames(category),
|
||||
MonsterSize.medium => _mediumIdleFrames(category),
|
||||
MonsterSize.large => _largeIdleFrames(category),
|
||||
_ => _hugeIdleFrames(category), // huge 이상은 같은 프레임 사용
|
||||
};
|
||||
}
|
||||
|
||||
/// 피격 상태 몬스터 프레임
|
||||
List<List<String>> _getMonsterHitFrames(MonsterCategory category, MonsterSize size) {
|
||||
return switch (size) {
|
||||
MonsterSize.tiny => _tinyHitFrames(category),
|
||||
MonsterSize.small => _smallHitFrames(category),
|
||||
MonsterSize.medium => _mediumHitFrames(category),
|
||||
MonsterSize.large => _largeHitFrames(category),
|
||||
_ => _hugeHitFrames(category),
|
||||
};
|
||||
}
|
||||
|
||||
/// 경계 상태 몬스터 프레임 (prepare/attack 시)
|
||||
List<List<String>> _getMonsterAlertFrames(MonsterCategory category, MonsterSize size) {
|
||||
return switch (size) {
|
||||
MonsterSize.tiny => _tinyAlertFrames(category),
|
||||
MonsterSize.small => _smallAlertFrames(category),
|
||||
MonsterSize.medium => _mediumAlertFrames(category),
|
||||
MonsterSize.large => _largeAlertFrames(category),
|
||||
_ => _hugeAlertFrames(category),
|
||||
};
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Tiny 몬스터 (2줄, 8줄 캔버스 하단 정렬)
|
||||
// ============================================================================
|
||||
|
||||
List<List<String>> _tinyIdleFrames(MonsterCategory category) {
|
||||
return switch (category) {
|
||||
MonsterCategory.beast => [
|
||||
[r'*', r'/\'],
|
||||
[r'o', r'\/'],
|
||||
],
|
||||
MonsterCategory.insect => [
|
||||
[r'><', r'\/'],
|
||||
[r'<>', r'/\'],
|
||||
],
|
||||
MonsterCategory.humanoid => [
|
||||
[r'o', r'|'],
|
||||
[r'O', r'|'],
|
||||
],
|
||||
MonsterCategory.undead => [
|
||||
[r'+', r'|'],
|
||||
[r'x', r'|'],
|
||||
],
|
||||
MonsterCategory.dragon => [
|
||||
[r'~<', r'>>'],
|
||||
[r'<~', r'<<'],
|
||||
],
|
||||
MonsterCategory.slime => [
|
||||
[r'()', r''],
|
||||
[r'{}', r''],
|
||||
],
|
||||
MonsterCategory.demon => [
|
||||
[r'^v', r'\/'],
|
||||
[r'v^', r'/\'],
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
List<List<String>> _tinyHitFrames(MonsterCategory category) {
|
||||
return [
|
||||
[r'*!', r'><'],
|
||||
[r'!*', r'<>'],
|
||||
];
|
||||
}
|
||||
|
||||
List<List<String>> _tinyAlertFrames(MonsterCategory category) {
|
||||
return switch (category) {
|
||||
MonsterCategory.beast => [
|
||||
[r'!!', r'/\'],
|
||||
[r'OO', r'><'],
|
||||
],
|
||||
MonsterCategory.insect => [
|
||||
[r'!!', r'\/'],
|
||||
[r'@@', r'/\'],
|
||||
],
|
||||
MonsterCategory.humanoid => [
|
||||
[r'O!', r'|'],
|
||||
[r'!O', r'X'],
|
||||
],
|
||||
MonsterCategory.undead => [
|
||||
[r'!!', r'X'],
|
||||
[r'@@', r'|'],
|
||||
],
|
||||
MonsterCategory.dragon => [
|
||||
[r'!<', r'>>'],
|
||||
[r'>!', r'<<'],
|
||||
],
|
||||
MonsterCategory.slime => [
|
||||
[r'(!)', r''],
|
||||
[r'{!}', r''],
|
||||
],
|
||||
MonsterCategory.demon => [
|
||||
[r'^!', r'><'],
|
||||
[r'!^', r'<>'],
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Small 몬스터 (4줄)
|
||||
// ============================================================================
|
||||
|
||||
List<List<String>> _smallIdleFrames(MonsterCategory category) {
|
||||
return switch (category) {
|
||||
MonsterCategory.beast => [
|
||||
[r' /\_/\', r'( o.o )', r' > ^ <', r' /| |\'],
|
||||
[r' /\_/\', r'( o o )', r' > v <', r' \| |/'],
|
||||
],
|
||||
MonsterCategory.insect => [
|
||||
[r' /\/\', r' (O O)', r' / \', r' \/ \/'],
|
||||
[r' \/\/\', r' (O O)', r' \ /', r' /\ /\'],
|
||||
],
|
||||
MonsterCategory.humanoid => [
|
||||
[r' O', r' /|\', r' / \', r' _| |_'],
|
||||
[r' O', r' \|/', r' | |', r' _/ \_'],
|
||||
],
|
||||
MonsterCategory.undead => [
|
||||
[r' _+_', r' (x_x)', r' /|\', r' _/ \_'],
|
||||
[r' _+_', r' (X_X)', r' \|/', r' _| |_'],
|
||||
],
|
||||
MonsterCategory.dragon => [
|
||||
[r' __', r' <(oo)~', r' / \', r' <_ _>'],
|
||||
[r' __', r' (oo)>', r' \ /', r' <_ _>'],
|
||||
],
|
||||
MonsterCategory.slime => [
|
||||
[r' ___', r' ( )', r' ( )', r' \_/'],
|
||||
[r' _', r' / \', r' { }', r' \_/'],
|
||||
],
|
||||
MonsterCategory.demon => [
|
||||
[r' ^w^', r' (|o|)', r' /|\', r' V V'],
|
||||
[r' ^W^', r' (|O|)', r' \|/', r' v v'],
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
List<List<String>> _smallHitFrames(MonsterCategory category) {
|
||||
return [
|
||||
[r' *!*', r' (>_<)', r' \X/', r' _/_\_'],
|
||||
[r' !*!', r' (@_@)', r' /X\', r' _\_/_'],
|
||||
];
|
||||
}
|
||||
|
||||
List<List<String>> _smallAlertFrames(MonsterCategory category) {
|
||||
return switch (category) {
|
||||
MonsterCategory.beast => [
|
||||
[r' /\_/\', r'( O!O )', r' > ! <', r' /| |\'],
|
||||
[r' /\_/\', r'( !O! )', r' > ! <', r' \| |/'],
|
||||
],
|
||||
MonsterCategory.insect => [
|
||||
[r' /\/\', r' (! !)', r' / \', r' \/ \/'],
|
||||
[r' \/\/\', r' (! !)', r' \ /', r' /\ /\'],
|
||||
],
|
||||
MonsterCategory.humanoid => [
|
||||
[r' O!', r' /|\', r' / \', r' _| |_'],
|
||||
[r' !O', r' \|/', r' | |', r' _/ \_'],
|
||||
],
|
||||
MonsterCategory.undead => [
|
||||
[r' _!_', r' (!_!)', r' /|\', r' _/ \_'],
|
||||
[r' _!_', r' (!_!)', r' \|/', r' _| |_'],
|
||||
],
|
||||
MonsterCategory.dragon => [
|
||||
[r' __', r' <(!!)~', r' / \', r' <_ _>'],
|
||||
[r' __', r' (!!)>', r' \ /', r' <_ _>'],
|
||||
],
|
||||
MonsterCategory.slime => [
|
||||
[r' ___', r' ( ! )', r' ( ! )', r' \_/'],
|
||||
[r' _', r' /!\', r' { ! }', r' \_/'],
|
||||
],
|
||||
MonsterCategory.demon => [
|
||||
[r' ^!^', r' (|!|)', r' /|\', r' V V'],
|
||||
[r' ^!^', r' (|!|)', r' \|/', r' v v'],
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Medium 몬스터 (6줄)
|
||||
// ============================================================================
|
||||
|
||||
List<List<String>> _mediumIdleFrames(MonsterCategory category) {
|
||||
return switch (category) {
|
||||
MonsterCategory.beast => [
|
||||
[r' /\_/\', r' ( O.O )', r' > ^ <', r' /| |\', r' | | | |', r'_|_| |_|_'],
|
||||
[r' /\_/\', r' ( O O )', r' > v <', r' \| |/', r' | | | |', r'_|_| |_|_'],
|
||||
],
|
||||
MonsterCategory.insect => [
|
||||
[r' /\/\', r' /O O\', r' \ /', r' / \', r' \/ \/', r' _/ \_'],
|
||||
[r' \/\/\', r' \O O/', r' / \', r' \ /', r' /\ /\', r' _\ /_'],
|
||||
],
|
||||
MonsterCategory.humanoid => [
|
||||
[r' O', r' /|\', r' / \', r' | |', r' | |', r' _| |_'],
|
||||
[r' O', r' \|/', r' | |', r' | |', r' | |', r' _/ \_'],
|
||||
],
|
||||
MonsterCategory.undead => [
|
||||
[r' _+_', r' (X_X)', r' /|\', r' / | \', r' | | |', r'_/ | \_'],
|
||||
[r' _x_', r' (x_x)', r' \|/', r' \ | /', r' | | |', r'_\ | /_'],
|
||||
],
|
||||
MonsterCategory.dragon => [
|
||||
[r' __', r' <(OO)~', r' / \', r' / \', r' | |', r'<__ __>'],
|
||||
[r' __', r' (OO)>', r' \ /', r' \ /', r' | |', r'<__ __>'],
|
||||
],
|
||||
MonsterCategory.slime => [
|
||||
[r' ____', r' / \', r' ( )', r' ( )', r' \ /', r' \__/'],
|
||||
[r' __', r' / \', r' / \', r' { }', r' \ /', r' \__/'],
|
||||
],
|
||||
MonsterCategory.demon => [
|
||||
[r' ^W^', r' (|O|)', r' /|\', r' / | \', r' V V', r' _/ \_'],
|
||||
[r' ^w^', r' (|o|)', r' \|/', r' \ | /', r' v v', r' _\ /_'],
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
List<List<String>> _mediumHitFrames(MonsterCategory category) {
|
||||
return [
|
||||
[r' *!*', r' (>.<)', r' \X/', r' / \', r' | |', r'_/_ \_\'],
|
||||
[r' !*!', r' (@_@)', r' /X\', r' \ /', r' | |', r'_\_ /_/'],
|
||||
];
|
||||
}
|
||||
|
||||
List<List<String>> _mediumAlertFrames(MonsterCategory category) {
|
||||
return switch (category) {
|
||||
MonsterCategory.beast => [
|
||||
[r' /\_/\', r' ( O!O )', r' > ! <', r' /| |\', r' | | | |', r'_|_| |_|_'],
|
||||
[r' /\_/\', r' ( !O! )', r' > ! <', r' \| |/', r' | | | |', r'_|_| |_|_'],
|
||||
],
|
||||
MonsterCategory.insect => [
|
||||
[r' /\/\', r' /! !\', r' \ /', r' / \', r' \/ \/', r' _/ \_'],
|
||||
[r' \/\/\', r' \! !/', r' / \', r' \ /', r' /\ /\', r' _\ /_'],
|
||||
],
|
||||
MonsterCategory.humanoid => [
|
||||
[r' O!', r' /|\', r' / \', r' | |', r' | |', r' _| |_'],
|
||||
[r' !O', r' \|/', r' | |', r' | |', r' | |', r' _/ \_'],
|
||||
],
|
||||
MonsterCategory.undead => [
|
||||
[r' _!_', r' (!_!)', r' /|\', r' / | \', r' | | |', r'_/ | \_'],
|
||||
[r' _!_', r' (!_!)', r' \|/', r' \ | /', r' | | |', r'_\ | /_'],
|
||||
],
|
||||
MonsterCategory.dragon => [
|
||||
[r' __', r' <(!!)~', r' / \', r' / \', r' | |', r'<__ __>'],
|
||||
[r' __', r' (!!)>', r' \ /', r' \ /', r' | |', r'<__ __>'],
|
||||
],
|
||||
MonsterCategory.slime => [
|
||||
[r' ____', r' / ! \', r' ( ! )', r' ( ! )', r' \ /', r' \__/'],
|
||||
[r' __', r' / !\', r' / ! \', r' { ! }', r' \ /', r' \__/'],
|
||||
],
|
||||
MonsterCategory.demon => [
|
||||
[r' ^!^', r' (|!|)', r' /|\', r' / | \', r' V V', r' _/ \_'],
|
||||
[r' ^!^', r' (|!|)', r' \|/', r' \ | /', r' v v', r' _\ /_'],
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Large 몬스터 (8줄)
|
||||
// ============================================================================
|
||||
|
||||
List<List<String>> _largeIdleFrames(MonsterCategory category) {
|
||||
return switch (category) {
|
||||
MonsterCategory.beast => [
|
||||
[r' /\__/\', r' ( O O )', r' > ^^ <', r' /| |\', r' | | | |', r' | | | |', r'_| | | |_', r'|__|____|__|'],
|
||||
[r' /\__/\', r' ( O O )', r' > vv <', r' \| |/', r' | | | |', r' | | | |', r'_| | | |_', r'|__|____|__|'],
|
||||
],
|
||||
MonsterCategory.insect => [
|
||||
[r' /\/\/\', r' /O O\', r' \ /', r' / \', r' \/ \/', r' | \ / |', r' _| \/ |_', r'|___________|'],
|
||||
[r' \/\/\/\', r' \O O/', r' / \', r' \ /', r' /\ /\', r' | / \ |', r' _| /\ |_', r'|___________|'],
|
||||
],
|
||||
MonsterCategory.humanoid => [
|
||||
[r' O', r' /|\', r' / \', r' | |', r' | |', r' | |', r' _| |_', r'|_________|'],
|
||||
[r' O', r' \|/', r' | |', r' | |', r' | |', r' | |', r' _/ \_', r'|_________|'],
|
||||
],
|
||||
MonsterCategory.undead => [
|
||||
[r' _/+\_', r' (X___X)', r' /|||\', r' / ||| \', r' | ||| |', r' | / \ |', r' _|/ \|_', r'|_/ \_|'],
|
||||
[r' _\+/_', r' (x___x)', r' \|||/', r' \ ||| /', r' | ||| |', r' | \ / |', r' _|\ /|_', r'|_\ /_|'],
|
||||
],
|
||||
MonsterCategory.dragon => [
|
||||
[r' ___', r' <<(O O)~~', r' / || \', r' / || \', r' | || |', r' | || |', r' _| || |_', r'<___________|>'],
|
||||
[r' ___', r' (O O)>>', r' \ || /', r' \ || /', r' | || |', r' | || |', r' _| || |_', r'<___________|>'],
|
||||
],
|
||||
MonsterCategory.slime => [
|
||||
[r' _____', r' / \', r' / \', r' ( )', r' ( )', r' \ /', r' \_____/', r' \___/'],
|
||||
[r' ___', r' / \', r' / \', r' { }', r' { }', r' \ /', r' \___/', r' \_/'],
|
||||
],
|
||||
MonsterCategory.demon => [
|
||||
[r' ^W^', r' /|O|\', r' /|\', r' / | \', r' | | |', r' V | V', r' _/ | \_', r'|____|____|'],
|
||||
[r' ^w^', r' \|o|/', r' \|/', r' \ | /', r' | | |', r' v | v', r' _\ | /_', r'|____|____|'],
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
List<List<String>> _largeHitFrames(MonsterCategory category) {
|
||||
return [
|
||||
[r' *!*!*', r' (>___<)', r' \\X//', r' / \\// \', r' | \\/ |', r' | / \ |', r' _|/ \|_', r'|___/\\___|'],
|
||||
[r' !*!*!', r' (@___@)', r' //X\\', r' \ /\\/ /', r' | //\\ |', r' | \ / |', r' _|\ /|_', r'|___\\/__|'],
|
||||
];
|
||||
}
|
||||
|
||||
List<List<String>> _largeAlertFrames(MonsterCategory category) {
|
||||
return switch (category) {
|
||||
MonsterCategory.beast => [
|
||||
[r' /\__/\', r' ( O!!O )', r' > !! <', r' /| |\', r' | | | |', r' | | | |', r'_| | | |_', r'|__|____|__|'],
|
||||
[r' /\__/\', r' ( !!O! )', r' > !! <', r' \| |/', r' | | | |', r' | | | |', r'_| | | |_', r'|__|____|__|'],
|
||||
],
|
||||
MonsterCategory.insect => [
|
||||
[r' /\/\/\', r' /! !\', r' \ /', r' / \', r' \/ \/', r' | \ / |', r' _| \/ |_', r'|___________|'],
|
||||
[r' \/\/\/\', r' \! !/', r' / \', r' \ /', r' /\ /\', r' | / \ |', r' _| /\ |_', r'|___________|'],
|
||||
],
|
||||
MonsterCategory.humanoid => [
|
||||
[r' O!', r' /|\', r' / \', r' | |', r' | |', r' | |', r' _| |_', r'|_________|'],
|
||||
[r' !O', r' \|/', r' | |', r' | |', r' | |', r' | |', r' _/ \_', r'|_________|'],
|
||||
],
|
||||
MonsterCategory.undead => [
|
||||
[r' _/!\_', r' (!___!)', r' /|||\', r' / ||| \', r' | ||| |', r' | / \ |', r' _|/ \|_', r'|_/ \_|'],
|
||||
[r' _\!/_', r' (!___!)', r' \|||/', r' \ ||| /', r' | ||| |', r' | \ / |', r' _|\ /|_', r'|_\ /_|'],
|
||||
],
|
||||
MonsterCategory.dragon => [
|
||||
[r' ___', r' <<(! !)~~', r' / || \', r' / || \', r' | || |', r' | || |', r' _| || |_', r'<___________|>'],
|
||||
[r' ___', r' (! !)>>', r' \ || /', r' \ || /', r' | || |', r' | || |', r' _| || |_', r'<___________|>'],
|
||||
],
|
||||
MonsterCategory.slime => [
|
||||
[r' _____', r' / ! \', r' / ! \', r' ( ! )', r' ( ! )', r' \ /', r' \_____/', r' \___/'],
|
||||
[r' ___', r' / ! \', r' / ! \', r' { ! }', r' { ! }', r' \ /', r' \___/', r' \_/'],
|
||||
],
|
||||
MonsterCategory.demon => [
|
||||
[r' ^!^', r' /|!|\', r' /|\', r' / | \', r' | | |', r' V | V', r' _/ | \_', r'|____|____|'],
|
||||
[r' ^!^', r' \|!|/', r' \|/', r' \ | /', r' | | |', r' v | v', r' _\ | /_', r'|____|____|'],
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Huge+ 몬스터 (8줄, 더 넓게)
|
||||
// ============================================================================
|
||||
|
||||
List<List<String>> _hugeIdleFrames(MonsterCategory category) {
|
||||
return switch (category) {
|
||||
MonsterCategory.beast => [
|
||||
[r' /\____/\', r' ( O O )', r' > ^^^^ <', r' /| |\', r' | | | |', r' | | | |', r' _| | | |_', r'|___|______|___|'],
|
||||
[r' /\____/\', r' ( O O )', r' > vvvv <', r' \| |/', r' | | | |', r' | | | |', r' _| | | |_', r'|___|______|___|'],
|
||||
],
|
||||
MonsterCategory.insect => [
|
||||
[r' /\/\/\/\', r' /O O\', r' \ /', r' / \', r' \/ \/', r' | \ / |', r' _| \ / |_', r'|_______________|'],
|
||||
[r' \/\/\/\/\', r' \O O/', r' / \', r' \ /', r' /\ /\', r' | / \ |', r' _| / \ |_', r'|_______________|'],
|
||||
],
|
||||
MonsterCategory.humanoid => [
|
||||
[r' O', r' _/|\\_', r' / | \\', r' | |', r' | |', r' | |', r' _| |_', r'|___________|'],
|
||||
[r' O', r' \\_|_/', r' \\|/', r' | |', r' | |', r' | |', r' _/ \\_', r'|___________|'],
|
||||
],
|
||||
MonsterCategory.undead => [
|
||||
[r' _/+\\_', r' (X_____X)', r' /|||||\', r' / ||||| \\', r' | ||||| |', r' | / \\ |', r' _|/ \\|_', r'|_/ \\_|'],
|
||||
[r' _\\+/_', r' (x_____x)', r' \\|||||/', r' \\ ||||| /', r' | ||||| |', r' | \\ / |', r' _|\\ /|_', r'|_\\ /_|'],
|
||||
],
|
||||
MonsterCategory.dragon => [
|
||||
[r' ____', r' <<<(O O)~~~', r' / |||| \\', r' / |||| \\', r' | |||| |', r' | |||| |', r' _| |||| |_', r'<_______________|>'],
|
||||
[r' ____', r' (O O)>>>', r' \\ |||| /', r' \\ |||| /', r' | |||| |', r' | |||| |', r' _| |||| |_', r'<_______________|>'],
|
||||
],
|
||||
MonsterCategory.slime => [
|
||||
[r' ______', r' / \\', r' / \\', r' ( )', r' ( )', r' \\ /', r' \\______/', r' \\____/'],
|
||||
[r' ____', r' / \\', r' / \\', r' { }', r' { }', r' \\ /', r' \\____/', r' \\__/'],
|
||||
],
|
||||
MonsterCategory.demon => [
|
||||
[r' ^W^', r' /|O|\\ ', r' /|\\', r' / | \\', r' | | |', r' V | V', r' _/ | \\_', r'|_____|_____|'],
|
||||
[r' ^w^', r' \\|o|/', r' \\|/', r' \\ | /', r' | | |', r' v | v', r' _\\ | /_', r'|_____|_____|'],
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
List<List<String>> _hugeHitFrames(MonsterCategory category) {
|
||||
return [
|
||||
[r' *!*!*!*', r' (>_____<)', r' \\\\X////', r' / \\\\// \\', r' | \\\\/ |', r' | / \\ |', r' _|/ \\|_', r'|____/\\\\___|'],
|
||||
[r' !*!*!*!', r' (@_____@)', r' ////X\\\\', r' \\ /\\\\/ /', r' | ////\\\\ |', r' | \\ / |', r' _|\\ /|_', r'|____\\\\/___|'],
|
||||
];
|
||||
}
|
||||
|
||||
List<List<String>> _hugeAlertFrames(MonsterCategory category) {
|
||||
return switch (category) {
|
||||
MonsterCategory.beast => [
|
||||
[r' /\____/\', r' ( ! ! )', r' > !!!! <', r' /| |\', r' | | | |', r' | | | |', r' _| | | |_', r'|___|______|___|'],
|
||||
[r' /\____/\', r' ( ! ! )', r' > !!!! <', r' \| |/', r' | | | |', r' | | | |', r' _| | | |_', r'|___|______|___|'],
|
||||
],
|
||||
MonsterCategory.insect => [
|
||||
[r' /\/\/\/\', r' /! !\', r' \ /', r' / \', r' \/ \/', r' | \ / |', r' _| \ / |_', r'|_______________|'],
|
||||
[r' \/\/\/\/\', r' \! !/', r' / \', r' \ /', r' /\ /\', r' | / \ |', r' _| / \ |_', r'|_______________|'],
|
||||
],
|
||||
MonsterCategory.humanoid => [
|
||||
[r' O!', r' _/|\\__', r' / | \\', r' | |', r' | |', r' | |', r' _| |_', r'|___________|'],
|
||||
[r' !O', r' \\_|_/', r' \\|/', r' | |', r' | |', r' | |', r' _/ \\_', r'|___________|'],
|
||||
],
|
||||
MonsterCategory.undead => [
|
||||
[r' _/!\\__', r' (!_____!)', r' /|||||\', r' / ||||| \\', r' | ||||| |', r' | / \\ |', r' _|/ \\|_', r'|_/ \\_|'],
|
||||
[r' _\\!/_', r' (!_____!)', r' \\|||||/', r' \\ ||||| /', r' | ||||| |', r' | \\ / |', r' _|\\ /|_', r'|_\\ /_|'],
|
||||
],
|
||||
MonsterCategory.dragon => [
|
||||
[r' ____', r' <<<(! !)~~~', r' / |||| \\', r' / |||| \\', r' | |||| |', r' | |||| |', r' _| |||| |_', r'<_______________|>'],
|
||||
[r' ____', r' (! !)>>>', r' \\ |||| /', r' \\ |||| /', r' | |||| |', r' | |||| |', r' _| |||| |_', r'<_______________|>'],
|
||||
],
|
||||
MonsterCategory.slime => [
|
||||
[r' ______', r' / ! \\', r' / ! \\', r' ( ! )', r' ( ! )', r' \\ /', r' \\______/', r' \\____/'],
|
||||
[r' ____', r' / ! \\', r' / ! \\', r' { ! }', r' { ! }', r' \\ /', r' \\____/', r' \\__/'],
|
||||
],
|
||||
MonsterCategory.demon => [
|
||||
[r' ^!^', r' /|!|\\ ', r' /|\\', r' / | \\', r' | | |', r' V | V', r' _/ | \\_', r'|_____|_____|'],
|
||||
[r' ^!^', r' \\|!|/', r' \\|/', r' \\ | /', r' | | |', r' v | v', r' _\\ | /_', r'|_____|_____|'],
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
// 레거시 호환용 함수
|
||||
List<List<String>> getMonsterFrames(MonsterCategory category, MonsterSize size) {
|
||||
return _getMonsterIdleFrames(category, size);
|
||||
}
|
||||
178
lib/src/core/animation/character_frames.dart
Normal file
178
lib/src/core/animation/character_frames.dart
Normal file
@@ -0,0 +1,178 @@
|
||||
// 캐릭터 애니메이션 프레임 (8줄 Stone Story RPG 스타일)
|
||||
// 참조: Stone Story RPG - 상세하고 생동감 있는 ASCII 아트
|
||||
|
||||
/// 전투 페이즈
|
||||
enum BattlePhase {
|
||||
/// 대치 상태 (기본)
|
||||
idle,
|
||||
|
||||
/// 공격 준비
|
||||
prepare,
|
||||
|
||||
/// 공격 중
|
||||
attack,
|
||||
|
||||
/// 피격 (몬스터가 맞음)
|
||||
hit,
|
||||
|
||||
/// 복귀
|
||||
recover,
|
||||
}
|
||||
|
||||
/// 캐릭터 프레임 데이터
|
||||
class CharacterFrame {
|
||||
const CharacterFrame(this.lines);
|
||||
|
||||
/// 프레임 데이터 (3줄)
|
||||
final List<String> lines;
|
||||
|
||||
/// 방패 오버레이 적용
|
||||
/// 3줄 캐릭터: [0]=머리, [1]=몸통/팔, [2]=다리
|
||||
CharacterFrame withShield() {
|
||||
if (lines.length < 2) return this;
|
||||
final newLines = List<String>.from(lines);
|
||||
// 몸통 줄(1번줄, 팔 위치)에 방패 추가
|
||||
final bodyIdx = 1;
|
||||
if (newLines[bodyIdx].length >= 2) {
|
||||
// 첫 두 문자를 방패로 대체
|
||||
newLines[bodyIdx] = '[]${newLines[bodyIdx].substring(2)}';
|
||||
} else {
|
||||
newLines[bodyIdx] = '[]${newLines[bodyIdx]}';
|
||||
}
|
||||
return CharacterFrame(newLines);
|
||||
}
|
||||
}
|
||||
|
||||
/// 특정 페이즈와 서브프레임에 해당하는 캐릭터 프레임 반환
|
||||
CharacterFrame getCharacterFrame(BattlePhase phase, int subFrame) {
|
||||
final frames = switch (phase) {
|
||||
BattlePhase.idle => _idleFrames,
|
||||
BattlePhase.prepare => _prepareFrames,
|
||||
BattlePhase.attack => _attackFrames,
|
||||
BattlePhase.hit => _hitFrames,
|
||||
BattlePhase.recover => _recoverFrames,
|
||||
};
|
||||
|
||||
final index = subFrame % frames.length;
|
||||
return frames[index];
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 대기 프레임 (숨쉬기 애니메이션) - 4프레임, 심플 3줄 스타일, 폭 6자
|
||||
// ============================================================================
|
||||
const _idleFrames = [
|
||||
CharacterFrame([
|
||||
r' o ',
|
||||
r' /|\ ',
|
||||
r' / \ ',
|
||||
]),
|
||||
CharacterFrame([
|
||||
r' o ',
|
||||
r' /|\ ',
|
||||
r' | | ',
|
||||
]),
|
||||
CharacterFrame([
|
||||
r' o ',
|
||||
r' /|\ ',
|
||||
r' / \ ',
|
||||
]),
|
||||
CharacterFrame([
|
||||
r' O ',
|
||||
r' /|\ ',
|
||||
r' / \ ',
|
||||
]),
|
||||
];
|
||||
|
||||
// ============================================================================
|
||||
// 준비 프레임 (무기 들기) - 3프레임, 심플 3줄 스타일, 폭 6자
|
||||
// ============================================================================
|
||||
const _prepareFrames = [
|
||||
CharacterFrame([
|
||||
r' \o ',
|
||||
r' |\ ',
|
||||
r' / \ ',
|
||||
]),
|
||||
CharacterFrame([
|
||||
r' _ ',
|
||||
r' \o ',
|
||||
r' / \ ',
|
||||
]),
|
||||
CharacterFrame([
|
||||
r' \_ ',
|
||||
r' \o/ ',
|
||||
r' / \ ',
|
||||
]),
|
||||
];
|
||||
|
||||
// ============================================================================
|
||||
// 공격 프레임 (전진 + 휘두르기) - 5프레임, 심플 3줄 스타일
|
||||
// ============================================================================
|
||||
const _attackFrames = [
|
||||
CharacterFrame([
|
||||
r' \_/ ',
|
||||
r' o ',
|
||||
r' /| ',
|
||||
]),
|
||||
CharacterFrame([
|
||||
r' _/ ',
|
||||
r' o ',
|
||||
r' /|\ ',
|
||||
]),
|
||||
CharacterFrame([
|
||||
r' o-- ',
|
||||
r' /| ',
|
||||
r' / \ ',
|
||||
]),
|
||||
CharacterFrame([
|
||||
r' o ',
|
||||
r' /|-- ',
|
||||
r' / \ ',
|
||||
]),
|
||||
CharacterFrame([
|
||||
r' o ',
|
||||
r' /|\_ ',
|
||||
r' / \ ',
|
||||
]),
|
||||
];
|
||||
|
||||
// ============================================================================
|
||||
// 히트 프레임 (공격 명중) - 3프레임, 심플 3줄 스타일
|
||||
// ============================================================================
|
||||
const _hitFrames = [
|
||||
CharacterFrame([
|
||||
r' o ',
|
||||
r' /|-* ',
|
||||
r' / \ ',
|
||||
]),
|
||||
CharacterFrame([
|
||||
r' o ',
|
||||
r' /|=* ',
|
||||
r' / \ ',
|
||||
]),
|
||||
CharacterFrame([
|
||||
r' o ',
|
||||
r' /|~* ',
|
||||
r' / \ ',
|
||||
]),
|
||||
];
|
||||
|
||||
// ============================================================================
|
||||
// 복귀 프레임 - 3프레임, 심플 3줄 스타일
|
||||
// ============================================================================
|
||||
const _recoverFrames = [
|
||||
CharacterFrame([
|
||||
r' o ',
|
||||
r' /|\ ',
|
||||
r' | ',
|
||||
]),
|
||||
CharacterFrame([
|
||||
r' o ',
|
||||
r' /|\ ',
|
||||
r' / \ ',
|
||||
]),
|
||||
CharacterFrame([
|
||||
r' o ',
|
||||
r' /|\ ',
|
||||
r' / \ ',
|
||||
]),
|
||||
];
|
||||
192
lib/src/core/animation/monster_colors.dart
Normal file
192
lib/src/core/animation/monster_colors.dart
Normal file
@@ -0,0 +1,192 @@
|
||||
// 몬스터 카테고리별 색상 시스템
|
||||
// 각 몬스터 카테고리에 따라 다른 색상 적용
|
||||
|
||||
import 'dart:ui';
|
||||
|
||||
/// 몬스터 카테고리 (ascii_animation_data.dart의 MonsterCategory와 매칭)
|
||||
enum MonsterColorCategory {
|
||||
beast,
|
||||
insect,
|
||||
humanoid,
|
||||
undead,
|
||||
dragon,
|
||||
slime,
|
||||
demon,
|
||||
}
|
||||
|
||||
/// 몬스터 색상 정보
|
||||
class MonsterColors {
|
||||
const MonsterColors({
|
||||
required this.normal,
|
||||
required this.hit,
|
||||
});
|
||||
|
||||
/// 일반 상태 색상
|
||||
final Color normal;
|
||||
|
||||
/// 피격 상태 색상
|
||||
final Color hit;
|
||||
}
|
||||
|
||||
/// 카테고리별 몬스터 색상 반환
|
||||
MonsterColors getMonsterColors(MonsterColorCategory category) {
|
||||
return switch (category) {
|
||||
MonsterColorCategory.beast => const MonsterColors(
|
||||
normal: Color(0xFF00FF00), // 녹색
|
||||
hit: Color(0xFFFF0000), // 빨강
|
||||
),
|
||||
MonsterColorCategory.insect => const MonsterColors(
|
||||
normal: Color(0xFFFFFF00), // 노랑
|
||||
hit: Color(0xFFFF6600), // 주황
|
||||
),
|
||||
MonsterColorCategory.humanoid => const MonsterColors(
|
||||
normal: Color(0xFF00FFFF), // 시안
|
||||
hit: Color(0xFFFF00FF), // 마젠타
|
||||
),
|
||||
MonsterColorCategory.undead => const MonsterColors(
|
||||
normal: Color(0xFF9966FF), // 보라
|
||||
hit: Color(0xFFCCCCCC), // 회색
|
||||
),
|
||||
MonsterColorCategory.dragon => const MonsterColors(
|
||||
normal: Color(0xFFFF6600), // 주황
|
||||
hit: Color(0xFFFFFF00), // 노랑
|
||||
),
|
||||
MonsterColorCategory.slime => const MonsterColors(
|
||||
normal: Color(0xFF66FF66), // 연녹색
|
||||
hit: Color(0xFF00CC00), // 진녹색
|
||||
),
|
||||
MonsterColorCategory.demon => const MonsterColors(
|
||||
normal: Color(0xFFFF0066), // 핑크
|
||||
hit: Color(0xFFFFFFFF), // 흰색
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
/// 몬스터 기본 이름에서 색상 카테고리 추론
|
||||
///
|
||||
/// ascii_animation_data.dart의 getMonsterCategory 결과를 변환
|
||||
MonsterColorCategory getMonsterColorCategory(String? baseName) {
|
||||
if (baseName == null || baseName.isEmpty) {
|
||||
return MonsterColorCategory.beast;
|
||||
}
|
||||
|
||||
final lower = baseName.toLowerCase();
|
||||
|
||||
// insect (곤충류)
|
||||
if (_matchesAny(lower, _insectKeywords)) {
|
||||
return MonsterColorCategory.insect;
|
||||
}
|
||||
|
||||
// undead (언데드)
|
||||
if (_matchesAny(lower, _undeadKeywords)) {
|
||||
return MonsterColorCategory.undead;
|
||||
}
|
||||
|
||||
// dragon (드래곤류)
|
||||
if (_matchesAny(lower, _dragonKeywords)) {
|
||||
return MonsterColorCategory.dragon;
|
||||
}
|
||||
|
||||
// slime (슬라임류)
|
||||
if (_matchesAny(lower, _slimeKeywords)) {
|
||||
return MonsterColorCategory.slime;
|
||||
}
|
||||
|
||||
// demon (악마류)
|
||||
if (_matchesAny(lower, _demonKeywords)) {
|
||||
return MonsterColorCategory.demon;
|
||||
}
|
||||
|
||||
// humanoid (인간형)
|
||||
if (_matchesAny(lower, _humanoidKeywords)) {
|
||||
return MonsterColorCategory.humanoid;
|
||||
}
|
||||
|
||||
// 기본은 beast
|
||||
return MonsterColorCategory.beast;
|
||||
}
|
||||
|
||||
bool _matchesAny(String text, List<String> keywords) {
|
||||
return keywords.any((kw) => text.contains(kw));
|
||||
}
|
||||
|
||||
const _insectKeywords = [
|
||||
'bug',
|
||||
'beetle',
|
||||
'spider',
|
||||
'ant',
|
||||
'bee',
|
||||
'wasp',
|
||||
'moth',
|
||||
'worm',
|
||||
'larva',
|
||||
'crawler',
|
||||
'centipede',
|
||||
'scorpion',
|
||||
];
|
||||
|
||||
const _undeadKeywords = [
|
||||
'zombie',
|
||||
'skeleton',
|
||||
'ghost',
|
||||
'wraith',
|
||||
'vampire',
|
||||
'lich',
|
||||
'specter',
|
||||
'phantom',
|
||||
'revenant',
|
||||
'undead',
|
||||
'corpse',
|
||||
'bone',
|
||||
];
|
||||
|
||||
const _dragonKeywords = [
|
||||
'dragon',
|
||||
'drake',
|
||||
'wyrm',
|
||||
'wyvern',
|
||||
'serpent',
|
||||
'hydra',
|
||||
'basilisk',
|
||||
];
|
||||
|
||||
const _slimeKeywords = [
|
||||
'slime',
|
||||
'ooze',
|
||||
'blob',
|
||||
'jelly',
|
||||
'pudding',
|
||||
'gel',
|
||||
'goo',
|
||||
];
|
||||
|
||||
const _demonKeywords = [
|
||||
'demon',
|
||||
'devil',
|
||||
'imp',
|
||||
'fiend',
|
||||
'daemon',
|
||||
'succubus',
|
||||
'incubus',
|
||||
'hell',
|
||||
'infernal',
|
||||
];
|
||||
|
||||
const _humanoidKeywords = [
|
||||
'goblin',
|
||||
'orc',
|
||||
'troll',
|
||||
'ogre',
|
||||
'giant',
|
||||
'bandit',
|
||||
'knight',
|
||||
'mage',
|
||||
'wizard',
|
||||
'warrior',
|
||||
'guard',
|
||||
'soldier',
|
||||
'cultist',
|
||||
'hacker',
|
||||
'admin',
|
||||
'user',
|
||||
];
|
||||
49
lib/src/core/animation/monster_size.dart
Normal file
49
lib/src/core/animation/monster_size.dart
Normal file
@@ -0,0 +1,49 @@
|
||||
// 몬스터 크기 시스템
|
||||
// 몬스터 레벨에 따라 ASCII 아트 크기 결정
|
||||
|
||||
/// 몬스터 크기 enum
|
||||
enum MonsterSize {
|
||||
/// 1줄 (레벨 1-5)
|
||||
tiny(1),
|
||||
|
||||
/// 2줄 (레벨 6-10)
|
||||
small(2),
|
||||
|
||||
/// 3줄 (레벨 11-15)
|
||||
medium(3),
|
||||
|
||||
/// 4줄 (레벨 16-25)
|
||||
large(4),
|
||||
|
||||
/// 5줄 (레벨 26-35)
|
||||
huge(5),
|
||||
|
||||
/// 6줄 (레벨 36-50)
|
||||
giant(6),
|
||||
|
||||
/// 7줄 (레벨 51+, 보스급)
|
||||
titanic(7);
|
||||
|
||||
const MonsterSize(this.lines);
|
||||
|
||||
/// 해당 크기의 줄 수
|
||||
final int lines;
|
||||
}
|
||||
|
||||
/// 몬스터 레벨에서 크기 결정
|
||||
MonsterSize getMonsterSize(int? level) {
|
||||
if (level == null || level <= 0) return MonsterSize.tiny;
|
||||
|
||||
if (level <= 5) return MonsterSize.tiny;
|
||||
if (level <= 10) return MonsterSize.small;
|
||||
if (level <= 15) return MonsterSize.medium;
|
||||
if (level <= 25) return MonsterSize.large;
|
||||
if (level <= 35) return MonsterSize.huge;
|
||||
if (level <= 50) return MonsterSize.giant;
|
||||
return MonsterSize.titanic;
|
||||
}
|
||||
|
||||
/// 몬스터 크기에 따른 세로 패딩 계산 (7줄 프레임에서 중앙 정렬)
|
||||
int getMonsterVerticalPadding(MonsterSize size) {
|
||||
return (7 - size.lines) ~/ 2;
|
||||
}
|
||||
109
lib/src/core/animation/weapon_category.dart
Normal file
109
lib/src/core/animation/weapon_category.dart
Normal file
@@ -0,0 +1,109 @@
|
||||
/// 무기 카테고리 (공격 스타일 결정용)
|
||||
enum WeaponCategory {
|
||||
/// 둔기류 - 휘두르기/타격
|
||||
/// Keyboard, Mouse, Monitor Stand, Server Rack 등
|
||||
blunt,
|
||||
|
||||
/// 케이블류 - 채찍질
|
||||
/// USB Cable, Ethernet Cord, Fiber Optic 등
|
||||
cable,
|
||||
|
||||
/// 칩류 - 투척/발사
|
||||
/// SSD, RAM Stick, GPU 등
|
||||
projectile,
|
||||
|
||||
/// 프로세서류 - 에너지 빔
|
||||
/// Tensor Core, TPU, Neural Processor 등
|
||||
energy,
|
||||
|
||||
/// 우주급 - 초월적 공격
|
||||
/// Dyson Sphere, Black Hole Computer, Universe Simulator
|
||||
cosmic,
|
||||
|
||||
/// 기본 (무기 없음)
|
||||
unarmed,
|
||||
}
|
||||
|
||||
/// 무기 이름에서 카테고리를 결정
|
||||
///
|
||||
/// 무기 이름의 키워드를 분석하여 공격 스타일 결정.
|
||||
/// 예: "Flaming USB Cable" → cable
|
||||
WeaponCategory getWeaponCategory(String? weaponName) {
|
||||
if (weaponName == null || weaponName.isEmpty) {
|
||||
return WeaponCategory.unarmed;
|
||||
}
|
||||
|
||||
final lower = weaponName.toLowerCase();
|
||||
|
||||
// 우주급 (가장 먼저 체크 - 가장 특별함)
|
||||
if (_matchesAny(lower, _cosmicKeywords)) {
|
||||
return WeaponCategory.cosmic;
|
||||
}
|
||||
|
||||
// 케이블류
|
||||
if (_matchesAny(lower, _cableKeywords)) {
|
||||
return WeaponCategory.cable;
|
||||
}
|
||||
|
||||
// 에너지/프로세서류
|
||||
if (_matchesAny(lower, _energyKeywords)) {
|
||||
return WeaponCategory.energy;
|
||||
}
|
||||
|
||||
// 칩/메모리류
|
||||
if (_matchesAny(lower, _projectileKeywords)) {
|
||||
return WeaponCategory.projectile;
|
||||
}
|
||||
|
||||
// 나머지는 모두 둔기류
|
||||
return WeaponCategory.blunt;
|
||||
}
|
||||
|
||||
bool _matchesAny(String text, List<String> keywords) {
|
||||
return keywords.any((kw) => text.contains(kw));
|
||||
}
|
||||
|
||||
// 카테고리별 키워드 목록
|
||||
|
||||
const _cosmicKeywords = [
|
||||
'dyson',
|
||||
'black hole',
|
||||
'universe',
|
||||
'singularity',
|
||||
];
|
||||
|
||||
const _cableKeywords = [
|
||||
'cable',
|
||||
'cord',
|
||||
'fiber',
|
||||
'optic',
|
||||
'submarine',
|
||||
'satellite',
|
||||
'link',
|
||||
'ethernet',
|
||||
'usb',
|
||||
];
|
||||
|
||||
const _energyKeywords = [
|
||||
'tensor',
|
||||
'tpu',
|
||||
'fpga',
|
||||
'asic',
|
||||
'quantum',
|
||||
'photonic',
|
||||
'neural',
|
||||
'entangler',
|
||||
'processor',
|
||||
'core',
|
||||
];
|
||||
|
||||
const _projectileKeywords = [
|
||||
'ssd',
|
||||
'nvme',
|
||||
'raid',
|
||||
'ram',
|
||||
'gpu',
|
||||
'drive',
|
||||
'stick',
|
||||
'array',
|
||||
];
|
||||
184
lib/src/core/animation/weapon_effects.dart
Normal file
184
lib/src/core/animation/weapon_effects.dart
Normal file
@@ -0,0 +1,184 @@
|
||||
import 'package:askiineverdie/src/core/animation/weapon_category.dart';
|
||||
|
||||
/// 무기 카테고리별 공격 이펙트 ASCII 프레임
|
||||
///
|
||||
/// 각 이펙트는 멀티라인 (최대 5줄, 24자 폭).
|
||||
/// 캐릭터와 몬스터 사이에 표시됨.
|
||||
|
||||
class WeaponEffect {
|
||||
const WeaponEffect({
|
||||
required this.prepareFrames,
|
||||
required this.attackFrames,
|
||||
required this.hitFrames,
|
||||
this.hitSound = '*HIT!*',
|
||||
this.effectHeight = 3,
|
||||
this.effectYStart = 2,
|
||||
});
|
||||
|
||||
/// 준비 프레임 (멀티라인)
|
||||
final List<List<String>> prepareFrames;
|
||||
|
||||
/// 공격 프레임 (멀티라인)
|
||||
final List<List<String>> attackFrames;
|
||||
|
||||
/// 히트 프레임 (멀티라인)
|
||||
final List<List<String>> hitFrames;
|
||||
|
||||
/// 히트 효과음 텍스트
|
||||
final String hitSound;
|
||||
|
||||
/// 이펙트 높이 (줄 수)
|
||||
final int effectHeight;
|
||||
|
||||
/// 이펙트 시작 Y 위치 (0~7)
|
||||
final int effectYStart;
|
||||
}
|
||||
|
||||
/// 카테고리별 무기 이펙트 반환
|
||||
WeaponEffect getWeaponEffect(WeaponCategory category) {
|
||||
return switch (category) {
|
||||
WeaponCategory.blunt => _bluntEffect,
|
||||
WeaponCategory.cable => _cableEffect,
|
||||
WeaponCategory.projectile => _projectileEffect,
|
||||
WeaponCategory.energy => _energyEffect,
|
||||
WeaponCategory.cosmic => _cosmicEffect,
|
||||
WeaponCategory.unarmed => _unarmedEffect,
|
||||
};
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 둔기류 - 휘두르기 (3줄)
|
||||
// ============================================================================
|
||||
const _bluntEffect = WeaponEffect(
|
||||
prepareFrames: [
|
||||
[r' _', r' /', r' /'],
|
||||
[r' _/', r' / ', r' / '],
|
||||
],
|
||||
attackFrames: [
|
||||
[r' _/ ', r' / ', r'/ '],
|
||||
[r' /__ ', r'/ ', r' '],
|
||||
[r'/__ ', r' ', r' '],
|
||||
[r'/__=>', r' ', r' '],
|
||||
],
|
||||
hitFrames: [
|
||||
[r' *BASH* ', r'/__=> ', r' '],
|
||||
[r'*SMASH!*', r' /__ ', r' '],
|
||||
],
|
||||
hitSound: '*BASH*',
|
||||
effectHeight: 3,
|
||||
effectYStart: 2,
|
||||
);
|
||||
|
||||
// ============================================================================
|
||||
// 케이블류 - 채찍질 (3줄)
|
||||
// ============================================================================
|
||||
const _cableEffect = WeaponEffect(
|
||||
prepareFrames: [
|
||||
[r' ', r'~ ', r' ~ '],
|
||||
[r' ', r'~~ ', r' ~ '],
|
||||
],
|
||||
attackFrames: [
|
||||
[r' ', r'~~~ ', r' ~~ '],
|
||||
[r' ', r'~~~~ ', r' ~~ '],
|
||||
[r' ', r'~~~~~> ', r' ~~ '],
|
||||
[r' ', r'~~~~~~> ', r' ~~'],
|
||||
],
|
||||
hitFrames: [
|
||||
[r' *WHIP*', r'~~~~~~> ', r' ~~'],
|
||||
[r' *CRACK*', r'~~~~~> ', r' ~~ '],
|
||||
],
|
||||
hitSound: '*WHIP*',
|
||||
effectHeight: 3,
|
||||
effectYStart: 2,
|
||||
);
|
||||
|
||||
// ============================================================================
|
||||
// 투척류 - 발사 (3줄)
|
||||
// ============================================================================
|
||||
const _projectileEffect = WeaponEffect(
|
||||
prepareFrames: [
|
||||
[r' ', r'[=] ', r' '],
|
||||
[r' ', r'[==] ', r' '],
|
||||
],
|
||||
attackFrames: [
|
||||
[r' ', r' [> ', r' '],
|
||||
[r' ', r' [>', r' '],
|
||||
[r' ', r' [>', r' '],
|
||||
[r' ', r' [>', r' '],
|
||||
],
|
||||
hitFrames: [
|
||||
[r' *CLANG*', r' [>', r' '],
|
||||
[r' *CRASH* ', r' [> ', r' '],
|
||||
],
|
||||
hitSound: '*CLANG*',
|
||||
effectHeight: 3,
|
||||
effectYStart: 2,
|
||||
);
|
||||
|
||||
// ============================================================================
|
||||
// 에너지류 - 빔 발사 (3줄)
|
||||
// ============================================================================
|
||||
const _energyEffect = WeaponEffect(
|
||||
prepareFrames: [
|
||||
[r' ', r' <*> ', r' '],
|
||||
[r' == ', r' <**> ', r' == '],
|
||||
],
|
||||
attackFrames: [
|
||||
[r' ==== ', r'==<*>== ', r' ==== '],
|
||||
[r' ====== ', r'===<*>==', r' ====== '],
|
||||
[r'========', r'===<*>==', r'========'],
|
||||
[r'========', r'====<*>=', r'========'],
|
||||
],
|
||||
hitFrames: [
|
||||
[r'==*ZAP*=', r'===<*>==', r'========'],
|
||||
[r'*BZZT!*=', r'====<*>=', r'========'],
|
||||
],
|
||||
hitSound: '*ZAP*',
|
||||
effectHeight: 3,
|
||||
effectYStart: 2,
|
||||
);
|
||||
|
||||
// ============================================================================
|
||||
// 우주급 - 초월적 공격 (5줄)
|
||||
// ============================================================================
|
||||
const _cosmicEffect = WeaponEffect(
|
||||
prepareFrames: [
|
||||
[r' * ', r' @ @ ', r' @ @ ', r' @ @ ', r' * '],
|
||||
[r' * * ', r' @ @ ', r' @ @', r' @ @ ', r' * * '],
|
||||
],
|
||||
attackFrames: [
|
||||
[r' * ', r' * * * ', r' * * * *', r' * * * ', r' * '],
|
||||
[r' *** ', r' * * * *', r'* * * * ', r' * * * *', r' *** '],
|
||||
[r' ***** ', r' ******* ', r'*********', r' ******* ', r' ***** '],
|
||||
[r' **VOID**', r'*********', r'*********', r'*********', r' **VOID**'],
|
||||
],
|
||||
hitFrames: [
|
||||
[r'*SINGULAR', r'*********', r'***!!!***', r'*********', r'*DESTROY*'],
|
||||
[r'!!!VOID!!', r'*********', r'*********', r'*********', r'!!!VOID!!'],
|
||||
],
|
||||
hitSound: '***VOID***',
|
||||
effectHeight: 5,
|
||||
effectYStart: 1,
|
||||
);
|
||||
|
||||
// ============================================================================
|
||||
// 맨손 - 기본 펀치 (3줄)
|
||||
// ============================================================================
|
||||
const _unarmedEffect = WeaponEffect(
|
||||
prepareFrames: [
|
||||
[r' ', r' ', r' '],
|
||||
[r' ', r' > ', r' '],
|
||||
],
|
||||
attackFrames: [
|
||||
[r' ', r'-> ', r' '],
|
||||
[r' ', r'---> ', r' '],
|
||||
[r' ', r'-----> ', r' '],
|
||||
],
|
||||
hitFrames: [
|
||||
[r' *POW* ', r'-----> ', r' '],
|
||||
[r'*PUNCH*', r'----> ', r' '],
|
||||
],
|
||||
hitSound: '*POW*',
|
||||
effectHeight: 3,
|
||||
effectYStart: 2,
|
||||
);
|
||||
@@ -1,5 +1,6 @@
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:askiineverdie/data/game_text_l10n.dart' as l10n;
|
||||
import 'package:askiineverdie/src/core/engine/game_mutations.dart';
|
||||
import 'package:askiineverdie/src/core/engine/reward_service.dart';
|
||||
import 'package:askiineverdie/src/core/model/game_state.dart';
|
||||
@@ -37,48 +38,47 @@ class ProgressService {
|
||||
/// 새 게임 초기화 (원본 GoButtonClick, Main.pas:741-767)
|
||||
/// Prologue 태스크들을 큐에 추가하고 첫 태스크 시작
|
||||
GameState initializeNewGame(GameState state) {
|
||||
// 초기 큐 설정 - 아스키나라(ASCII-Nara) 세계관 프롤로그
|
||||
// 초기 큐 설정 - ASCII NEVER DIE 세계관 프롤로그 (l10n 지원)
|
||||
final prologueTexts = l10n.prologueTexts;
|
||||
final initialQueue = <QueueEntry>[
|
||||
const QueueEntry(
|
||||
QueueEntry(
|
||||
kind: QueueKind.task,
|
||||
durationMillis: 10 * 1000,
|
||||
caption: 'Receiving an ominous vision from the Code God',
|
||||
caption: prologueTexts[0],
|
||||
taskType: TaskType.load,
|
||||
),
|
||||
const QueueEntry(
|
||||
QueueEntry(
|
||||
kind: QueueKind.task,
|
||||
durationMillis: 6 * 1000,
|
||||
caption:
|
||||
'The old Compiler Sage reveals a prophecy: '
|
||||
'"The Glitch God has awakened"',
|
||||
caption: prologueTexts[1],
|
||||
taskType: TaskType.load,
|
||||
),
|
||||
const QueueEntry(
|
||||
QueueEntry(
|
||||
kind: QueueKind.task,
|
||||
durationMillis: 6 * 1000,
|
||||
caption:
|
||||
'A sudden Buffer Overflow resets your village, '
|
||||
'leaving you as the sole survivor',
|
||||
caption: prologueTexts[2],
|
||||
taskType: TaskType.load,
|
||||
),
|
||||
const QueueEntry(
|
||||
QueueEntry(
|
||||
kind: QueueKind.task,
|
||||
durationMillis: 4 * 1000,
|
||||
caption:
|
||||
'With unexpected resolve, you embark on a perilous journey '
|
||||
'to the Null Kingdom',
|
||||
caption: prologueTexts[3],
|
||||
taskType: TaskType.load,
|
||||
),
|
||||
const QueueEntry(
|
||||
QueueEntry(
|
||||
kind: QueueKind.plot,
|
||||
durationMillis: 2 * 1000,
|
||||
caption: 'Compiling',
|
||||
caption: l10n.taskCompiling,
|
||||
taskType: TaskType.plot,
|
||||
),
|
||||
];
|
||||
|
||||
// 첫 번째 태스크 시작 (원본 752줄)
|
||||
final taskResult = pq_logic.startTask(state.progress, 'Compiling', 2 * 1000);
|
||||
final taskResult = pq_logic.startTask(
|
||||
state.progress,
|
||||
l10n.taskCompiling,
|
||||
2 * 1000,
|
||||
);
|
||||
|
||||
// ExpBar 초기화 (원본 743-746줄)
|
||||
final expBar = ProgressBarState(position: 0, max: pq_logic.levelUpTime(1));
|
||||
@@ -89,10 +89,13 @@ class ProgressService {
|
||||
final progress = taskResult.progress.copyWith(
|
||||
exp: expBar,
|
||||
plot: plotBar,
|
||||
currentTask: const TaskInfo(caption: 'Compiling...', type: TaskType.load),
|
||||
currentTask: TaskInfo(
|
||||
caption: '${l10n.taskCompiling}...',
|
||||
type: TaskType.load,
|
||||
),
|
||||
plotStageCount: 1, // Prologue
|
||||
questCount: 0,
|
||||
plotHistory: const [HistoryEntry(caption: 'Prologue', isComplete: false)],
|
||||
plotHistory: [HistoryEntry(caption: l10n.taskPrologue, isComplete: false)],
|
||||
questHistory: const [],
|
||||
);
|
||||
|
||||
@@ -295,7 +298,7 @@ class ProgressService {
|
||||
progress.encumbrance.max > 0) {
|
||||
final taskResult = pq_logic.startTask(
|
||||
progress,
|
||||
'Heading to the Data Market to trade loot',
|
||||
l10n.taskHeadingToMarket(),
|
||||
4 * 1000,
|
||||
);
|
||||
progress = taskResult.progress.copyWith(
|
||||
@@ -316,7 +319,7 @@ class ProgressService {
|
||||
if (gold > equipPrice) {
|
||||
final taskResult = pq_logic.startTask(
|
||||
progress,
|
||||
'Upgrading hardware at the Tech Shop',
|
||||
l10n.taskUpgradingHardware(),
|
||||
5 * 1000,
|
||||
);
|
||||
progress = taskResult.progress.copyWith(
|
||||
@@ -331,7 +334,7 @@ class ProgressService {
|
||||
// Gold가 부족하면 전장으로 이동 (원본 674-676줄)
|
||||
final taskResult = pq_logic.startTask(
|
||||
progress,
|
||||
'Entering the Debug Zone',
|
||||
l10n.taskEnteringDebugZone(),
|
||||
4 * 1000,
|
||||
);
|
||||
progress = taskResult.progress.copyWith(
|
||||
@@ -370,7 +373,7 @@ class ProgressService {
|
||||
|
||||
final taskResult = pq_logic.startTask(
|
||||
progress,
|
||||
'Debugging ${monsterResult.displayName}',
|
||||
l10n.taskDebugging(monsterResult.displayName),
|
||||
durationMillis,
|
||||
);
|
||||
|
||||
@@ -380,6 +383,7 @@ class ProgressService {
|
||||
type: TaskType.kill,
|
||||
monsterBaseName: monsterResult.baseName,
|
||||
monsterPart: monsterResult.part,
|
||||
monsterLevel: monsterResult.level,
|
||||
),
|
||||
);
|
||||
|
||||
@@ -711,9 +715,10 @@ class ProgressService {
|
||||
if (hasItemsToSell) {
|
||||
// 다음 아이템 판매 태스크 시작
|
||||
final nextItem = items.first;
|
||||
final itemDesc = l10n.indefiniteL10n(nextItem.name, nextItem.count);
|
||||
final taskResult = pq_logic.startTask(
|
||||
state.progress,
|
||||
'Selling ${pq_logic.indefinite(nextItem.name, nextItem.count)}',
|
||||
l10n.taskSelling(itemDesc),
|
||||
1 * 1000,
|
||||
);
|
||||
final progress = taskResult.progress.copyWith(
|
||||
|
||||
@@ -244,9 +244,7 @@ class GameDataL10n {
|
||||
static String _renderItemResultKo(ItemResult result) {
|
||||
// 단순 아이템 (boringItem)
|
||||
if (result.boringItem != null) {
|
||||
// boringItem은 별도 번역 맵이 필요할 수 있음
|
||||
// 현재는 그대로 반환 (대부분 영어 그대로 사용)
|
||||
return result.boringItem!;
|
||||
return boringItemTranslationsKo[result.boringItem] ?? result.boringItem!;
|
||||
}
|
||||
|
||||
// 복합 아이템: attrib + special + itemOf
|
||||
@@ -346,32 +344,20 @@ class GameDataL10n {
|
||||
|
||||
/// 아이템 이름 문자열 파싱 후 번역 (기존 저장 데이터 호환)
|
||||
/// 예: "Golden Iterator of Compilation" → "컴파일의 황금 이터레이터"
|
||||
/// 예: "index out of bounds Array fragment" → "인덱스 초과의 배열 조각"
|
||||
static String translateItemString(BuildContext context, String itemString) {
|
||||
if (!_isKorean(context) || itemString.isEmpty) return itemString;
|
||||
|
||||
// "X Y of Z" 패턴 파싱
|
||||
final ofMatch = RegExp(r'^(.+)\s+of\s+(.+)$').firstMatch(itemString);
|
||||
// 1. specialItem 형식 체크: "Attrib Special of ItemOf"
|
||||
// itemOfs에 있는 값으로 끝나는지 확인
|
||||
final specialItemResult = _tryTranslateSpecialItem(itemString);
|
||||
if (specialItemResult != null) return specialItemResult;
|
||||
|
||||
if (ofMatch != null) {
|
||||
final beforeOf = ofMatch.group(1)!; // "Golden Iterator"
|
||||
final afterOf = ofMatch.group(2)!; // "Compilation"
|
||||
// 2. 몬스터 드롭 형식 체크: "{monster_lowercase} {drop_ProperCase}"
|
||||
final monsterDropResult = _tryTranslateMonsterDrop(itemString);
|
||||
if (monsterDropResult != null) return monsterDropResult;
|
||||
|
||||
// "X Y" 분리 (마지막 단어가 special)
|
||||
final words = beforeOf.split(' ');
|
||||
if (words.length >= 2) {
|
||||
final attrib = words.sublist(0, words.length - 1).join(' ');
|
||||
final special = words.last;
|
||||
|
||||
final attribKo = itemAttribTranslationsKo[attrib] ?? attrib;
|
||||
final specialKo = specialTranslationsKo[special] ?? special;
|
||||
final itemOfKo = itemOfsTranslationsKo[afterOf] ?? afterOf;
|
||||
|
||||
// 한국어 어순: "ItemOf의 Attrib Special"
|
||||
return '$itemOfKo의 $attribKo $specialKo';
|
||||
}
|
||||
}
|
||||
|
||||
// "X Y" 패턴 (of 없음)
|
||||
// 3. interestingItem 형식: "Attrib Special" (2단어)
|
||||
final words = itemString.split(' ');
|
||||
if (words.length == 2) {
|
||||
final attrib = words[0];
|
||||
@@ -383,7 +369,92 @@ class GameDataL10n {
|
||||
return '$attribKo $specialKo';
|
||||
}
|
||||
|
||||
// 단일 단어 (boringItem 등)
|
||||
return itemString;
|
||||
// 4. 단일 단어 (boringItem 등) - 잡템 번역 시도
|
||||
return boringItemTranslationsKo[itemString] ??
|
||||
dropItemTranslationsKo[itemString.toLowerCase()] ??
|
||||
itemString;
|
||||
}
|
||||
|
||||
/// specialItem 형식 번역 시도
|
||||
/// "Attrib Special of ItemOf" → "ItemOf의 Attrib Special"
|
||||
static String? _tryTranslateSpecialItem(String itemString) {
|
||||
// "of" 뒤의 부분이 itemOfs에 있는지 확인
|
||||
final ofMatch = RegExp(r'^(.+)\s+of\s+(.+)$').firstMatch(itemString);
|
||||
if (ofMatch == null) return null;
|
||||
|
||||
final beforeOf = ofMatch.group(1)!;
|
||||
final afterOf = ofMatch.group(2)!;
|
||||
|
||||
// afterOf가 itemOfs에 있어야 specialItem 형식
|
||||
if (!itemOfsTranslationsKo.containsKey(afterOf)) return null;
|
||||
|
||||
// beforeOf를 Attrib + Special로 분리
|
||||
final words = beforeOf.split(' ');
|
||||
if (words.length < 2) return null;
|
||||
|
||||
final attrib = words.sublist(0, words.length - 1).join(' ');
|
||||
final special = words.last;
|
||||
|
||||
// Attrib와 Special이 유효한지 확인
|
||||
if (!itemAttribTranslationsKo.containsKey(attrib) &&
|
||||
!specialTranslationsKo.containsKey(special)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final attribKo = itemAttribTranslationsKo[attrib] ?? attrib;
|
||||
final specialKo = specialTranslationsKo[special] ?? special;
|
||||
final itemOfKo = itemOfsTranslationsKo[afterOf] ?? afterOf;
|
||||
|
||||
return '$itemOfKo의 $attribKo $specialKo';
|
||||
}
|
||||
|
||||
/// 몬스터 드롭 형식 번역 시도
|
||||
/// "{monster_lowercase} {drop_ProperCase}" → "{몬스터}의 {드롭아이템}"
|
||||
static String? _tryTranslateMonsterDrop(String itemString) {
|
||||
// dropItemTranslationsKo에서 매칭되는 드롭 아이템 찾기
|
||||
// (대소문자 무시, 아이템 문자열 끝에서 매칭)
|
||||
for (final entry in dropItemTranslationsKo.entries) {
|
||||
final dropItem = entry.key;
|
||||
final dropItemProperCase = _properCase(dropItem);
|
||||
|
||||
// 아이템 문자열이 드롭 아이템으로 끝나는지 확인
|
||||
if (itemString.endsWith(dropItemProperCase) ||
|
||||
itemString.endsWith(dropItem)) {
|
||||
// 드롭 아이템 앞 부분이 몬스터 이름
|
||||
String monsterPart;
|
||||
if (itemString.endsWith(dropItemProperCase)) {
|
||||
monsterPart =
|
||||
itemString.substring(0, itemString.length - dropItemProperCase.length).trim();
|
||||
} else {
|
||||
monsterPart =
|
||||
itemString.substring(0, itemString.length - dropItem.length).trim();
|
||||
}
|
||||
|
||||
if (monsterPart.isEmpty) continue;
|
||||
|
||||
// 몬스터 이름 번역 (소문자를 원래 형태로 변환하여 찾기)
|
||||
final monsterNameKey = _toTitleCase(monsterPart);
|
||||
final monsterKo =
|
||||
monsterTranslationsKo[monsterNameKey] ?? monsterPart;
|
||||
|
||||
final dropKo = entry.value;
|
||||
return '$monsterKo의 $dropKo';
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// 첫 글자만 대문자로 (나머지는 그대로)
|
||||
static String _properCase(String s) {
|
||||
if (s.isEmpty) return s;
|
||||
return s[0].toUpperCase() + s.substring(1);
|
||||
}
|
||||
|
||||
/// 각 단어의 첫 글자를 대문자로 (Title Case)
|
||||
static String _toTitleCase(String s) {
|
||||
return s.split(' ').map((word) {
|
||||
if (word.isEmpty) return word;
|
||||
return word[0].toUpperCase() + word.substring(1);
|
||||
}).join(' ');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -95,6 +95,7 @@ class TaskInfo {
|
||||
required this.type,
|
||||
this.monsterBaseName,
|
||||
this.monsterPart,
|
||||
this.monsterLevel,
|
||||
});
|
||||
|
||||
final String caption;
|
||||
@@ -106,6 +107,9 @@ class TaskInfo {
|
||||
/// 킬 태스크의 전리품 부위 (예: "claw", "tail", "*"는 WinItem)
|
||||
final String? monsterPart;
|
||||
|
||||
/// 킬 태스크의 몬스터 레벨 (애니메이션 크기 결정용)
|
||||
final int? monsterLevel;
|
||||
|
||||
factory TaskInfo.empty() =>
|
||||
const TaskInfo(caption: '', type: TaskType.neutral);
|
||||
|
||||
@@ -114,12 +118,14 @@ class TaskInfo {
|
||||
TaskType? type,
|
||||
String? monsterBaseName,
|
||||
String? monsterPart,
|
||||
int? monsterLevel,
|
||||
}) {
|
||||
return TaskInfo(
|
||||
caption: caption ?? this.caption,
|
||||
type: type ?? this.type,
|
||||
monsterBaseName: monsterBaseName ?? this.monsterBaseName,
|
||||
monsterPart: monsterPart ?? this.monsterPart,
|
||||
monsterLevel: monsterLevel ?? this.monsterLevel,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -302,7 +308,7 @@ class Equipment {
|
||||
static const slotCount = 11;
|
||||
|
||||
factory Equipment.empty() => const Equipment(
|
||||
weapon: 'Sharp Stick',
|
||||
weapon: 'Keyboard',
|
||||
shield: '',
|
||||
helm: '',
|
||||
hauberk: '',
|
||||
|
||||
@@ -173,7 +173,7 @@ class GameSave {
|
||||
.toList(),
|
||||
),
|
||||
equipment: Equipment(
|
||||
weapon: equipmentJson['weapon'] as String? ?? 'Sharp Stick',
|
||||
weapon: equipmentJson['weapon'] as String? ?? 'Keyboard',
|
||||
shield: equipmentJson['shield'] as String? ?? '',
|
||||
helm: equipmentJson['helm'] as String? ?? '',
|
||||
hauberk: equipmentJson['hauberk'] as String? ?? '',
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import 'dart:collection';
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:askiineverdie/src/core/util/deterministic_random.dart';
|
||||
import 'package:askiineverdie/src/core/model/pq_config.dart';
|
||||
import 'package:askiineverdie/src/core/util/roman.dart';
|
||||
import 'package:askiineverdie/data/game_text_l10n.dart' as l10n;
|
||||
import 'package:askiineverdie/src/core/model/equipment_slot.dart';
|
||||
import 'package:askiineverdie/src/core/model/game_state.dart';
|
||||
import 'package:askiineverdie/src/core/model/pq_config.dart';
|
||||
import 'package:askiineverdie/src/core/util/deterministic_random.dart';
|
||||
import 'package:askiineverdie/src/core/util/roman.dart';
|
||||
|
||||
// Mirrors core utility functions from the original Delphi sources (Main.pas / NewGuy.pas).
|
||||
|
||||
@@ -80,15 +81,16 @@ int levelUpTimeSeconds(int level) {
|
||||
}
|
||||
|
||||
/// 초 단위 시간을 사람이 읽기 쉬운 형태로 변환 (원본 Main.pas:1265-1271)
|
||||
/// l10n 지원
|
||||
String roughTime(int seconds) {
|
||||
if (seconds < 120) {
|
||||
return '$seconds seconds';
|
||||
return l10n.roughTimeSeconds(seconds);
|
||||
} else if (seconds < 60 * 120) {
|
||||
return '${seconds ~/ 60} minutes';
|
||||
return l10n.roughTimeMinutes(seconds ~/ 60);
|
||||
} else if (seconds < 60 * 60 * 48) {
|
||||
return '${seconds ~/ 3600} hours';
|
||||
return l10n.roughTimeHours(seconds ~/ 3600);
|
||||
} else {
|
||||
return '${seconds ~/ (3600 * 24)} days';
|
||||
return l10n.roughTimeDays(seconds ~/ (3600 * 24));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -508,15 +510,18 @@ MonsterTaskResult monsterTask(
|
||||
|
||||
// 원본 Main.pas:537-547: 가끔 NPC를 몬스터로 사용
|
||||
if (rng.nextInt(25) == 0) {
|
||||
final race = pick(config.races, rng).split('|').first;
|
||||
final raceEn = pick(config.races, rng).split('|').first;
|
||||
final race = l10n.translateRace(raceEn);
|
||||
if (rng.nextInt(2) == 0) {
|
||||
// 'passing Race Class' 형태
|
||||
final klass = pick(config.klasses, rng).split('|').first;
|
||||
monster = 'passing $race $klass';
|
||||
final klassEn = pick(config.klasses, rng).split('|').first;
|
||||
final klass = l10n.translateKlass(klassEn);
|
||||
monster = l10n.modifierPassing('$race $klass');
|
||||
} else {
|
||||
// 'Title Name the Race' 형태 (원본은 PickLow(Titles) 사용)
|
||||
final title = pickLow(config.titles, rng);
|
||||
monster = '$title ${generateName(rng)} the $race';
|
||||
final titleEn = pickLow(config.titles, rng);
|
||||
final title = l10n.translateTitle(titleEn);
|
||||
monster = l10n.namedMonsterFormat(generateName(rng), '$title $race');
|
||||
definite = true;
|
||||
}
|
||||
monsterLevel = targetLevel;
|
||||
@@ -551,7 +556,8 @@ MonsterTaskResult monsterTask(
|
||||
// Adjust quantity and adjectives based on level delta.
|
||||
var qty = 1;
|
||||
final levelDiff = targetLevel - monsterLevel;
|
||||
var name = baseName;
|
||||
// 몬스터 이름 번역 (l10n 지원)
|
||||
var name = l10n.translateMonster(baseName);
|
||||
|
||||
if (levelDiff > 10) {
|
||||
qty =
|
||||
@@ -562,7 +568,7 @@ MonsterTaskResult monsterTask(
|
||||
}
|
||||
|
||||
if (levelDiff <= -10) {
|
||||
name = 'imaginary $name';
|
||||
name = l10n.modifierImaginary(name);
|
||||
} else if (levelDiff < -5) {
|
||||
final i = 5 - rng.nextInt(10 + levelDiff + 1);
|
||||
name = _sick(i, _young((monsterLevel - targetLevel) - i, name));
|
||||
@@ -573,7 +579,7 @@ MonsterTaskResult monsterTask(
|
||||
name = _young(levelDiff, name);
|
||||
}
|
||||
} else if (levelDiff >= 10) {
|
||||
name = 'messianic $name';
|
||||
name = l10n.modifierMessianic(name);
|
||||
} else if (levelDiff > 5) {
|
||||
final i = 5 - rng.nextInt(10 - levelDiff + 1);
|
||||
name = _big(i, _special((levelDiff) - i, name));
|
||||
@@ -667,24 +673,36 @@ QuestResult completeQuest(PqConfig config, DeterministicRandom rng, int level) {
|
||||
bestIndex = monsterIndex;
|
||||
}
|
||||
}
|
||||
final name = best.split('|').first;
|
||||
final nameEn = best.split('|').first;
|
||||
final name = l10n.translateMonster(nameEn);
|
||||
return QuestResult(
|
||||
caption: 'Patch ${definite(name, 2)}',
|
||||
caption: l10n.questPatch(l10n.definiteL10n(name, 2)),
|
||||
reward: reward,
|
||||
monsterName: best,
|
||||
monsterLevel: bestLevel,
|
||||
monsterIndex: bestIndex,
|
||||
);
|
||||
case 1:
|
||||
final item = interestingItem(config, rng);
|
||||
return QuestResult(caption: 'Locate ${definite(item, 1)}', reward: reward);
|
||||
case 2:
|
||||
final item = boringItem(config, rng);
|
||||
return QuestResult(caption: 'Transfer this $item', reward: reward);
|
||||
case 3:
|
||||
final item = boringItem(config, rng);
|
||||
// interestingItem: attrib + special 조합 후 번역
|
||||
final attr = pick(config.itemAttrib, rng);
|
||||
final special = pick(config.specials, rng);
|
||||
final item = l10n.translateInterestingItem(attr, special);
|
||||
return QuestResult(
|
||||
caption: 'Download ${indefinite(item, 1)}',
|
||||
caption: l10n.questLocate(l10n.definiteL10n(item, 1)),
|
||||
reward: reward,
|
||||
);
|
||||
case 2:
|
||||
final itemEn = boringItem(config, rng);
|
||||
final item = l10n.translateBoringItem(itemEn);
|
||||
return QuestResult(
|
||||
caption: l10n.questTransfer(item),
|
||||
reward: reward,
|
||||
);
|
||||
case 3:
|
||||
final itemEn = boringItem(config, rng);
|
||||
final item = l10n.translateBoringItem(itemEn);
|
||||
return QuestResult(
|
||||
caption: l10n.questDownload(l10n.indefiniteL10n(item, 1)),
|
||||
reward: reward,
|
||||
);
|
||||
default:
|
||||
@@ -700,10 +718,11 @@ QuestResult completeQuest(PqConfig config, DeterministicRandom rng, int level) {
|
||||
bestLevel = l;
|
||||
}
|
||||
}
|
||||
final name = best.split('|').first;
|
||||
final nameEn = best.split('|').first;
|
||||
final name = l10n.translateMonster(nameEn);
|
||||
// Stabilize는 fQuest.Caption := '' 로 비움 → monsterIndex 미저장
|
||||
return QuestResult(
|
||||
caption: 'Stabilize ${definite(name, 2)}',
|
||||
caption: l10n.questStabilize(l10n.definiteL10n(name, 2)),
|
||||
reward: reward,
|
||||
);
|
||||
}
|
||||
@@ -723,7 +742,7 @@ class ActResult {
|
||||
|
||||
ActResult completeAct(int existingActCount) {
|
||||
final nextActIndex = existingActCount;
|
||||
final title = 'Act ${intToRoman(nextActIndex)}';
|
||||
final title = l10n.actTitle(intToRoman(nextActIndex));
|
||||
final plotBarMax = 60 * 60 * (1 + 5 * existingActCount);
|
||||
|
||||
final rewards = <RewardKind>[];
|
||||
@@ -815,19 +834,19 @@ String _sick(int m, String s) {
|
||||
switch (m) {
|
||||
case -5:
|
||||
case 5:
|
||||
return 'dead $s';
|
||||
return l10n.modifierDead(s);
|
||||
case -4:
|
||||
case 4:
|
||||
return 'comatose $s';
|
||||
return l10n.modifierComatose(s);
|
||||
case -3:
|
||||
case 3:
|
||||
return 'crippled $s';
|
||||
return l10n.modifierCrippled(s);
|
||||
case -2:
|
||||
case 2:
|
||||
return 'sick $s';
|
||||
return l10n.modifierSick(s);
|
||||
case -1:
|
||||
case 1:
|
||||
return 'undernourished $s';
|
||||
return l10n.modifierUndernourished(s);
|
||||
default:
|
||||
return '$m$s';
|
||||
}
|
||||
@@ -837,19 +856,19 @@ String _young(int m, String s) {
|
||||
switch (-m) {
|
||||
case -5:
|
||||
case 5:
|
||||
return 'foetal $s';
|
||||
return l10n.modifierFoetal(s);
|
||||
case -4:
|
||||
case 4:
|
||||
return 'baby $s';
|
||||
return l10n.modifierBaby(s);
|
||||
case -3:
|
||||
case 3:
|
||||
return 'preadolescent $s';
|
||||
return l10n.modifierPreadolescent(s);
|
||||
case -2:
|
||||
case 2:
|
||||
return 'teenage $s';
|
||||
return l10n.modifierTeenage(s);
|
||||
case -1:
|
||||
case 1:
|
||||
return 'underage $s';
|
||||
return l10n.modifierUnderage(s);
|
||||
default:
|
||||
return '$m$s';
|
||||
}
|
||||
@@ -859,19 +878,19 @@ String _big(int m, String s) {
|
||||
switch (m) {
|
||||
case 1:
|
||||
case -1:
|
||||
return 'greater $s';
|
||||
return l10n.modifierGreater(s);
|
||||
case 2:
|
||||
case -2:
|
||||
return 'massive $s';
|
||||
return l10n.modifierMassive(s);
|
||||
case 3:
|
||||
case -3:
|
||||
return 'enormous $s';
|
||||
return l10n.modifierEnormous(s);
|
||||
case 4:
|
||||
case -4:
|
||||
return 'giant $s';
|
||||
return l10n.modifierGiant(s);
|
||||
case 5:
|
||||
case -5:
|
||||
return 'titanic $s';
|
||||
return l10n.modifierTitanic(s);
|
||||
default:
|
||||
return s;
|
||||
}
|
||||
@@ -881,19 +900,19 @@ String _special(int m, String s) {
|
||||
switch (-m) {
|
||||
case 1:
|
||||
case -1:
|
||||
return s.contains(' ') ? 'veteran $s' : 'Battle-$s';
|
||||
return s.contains(' ') ? l10n.modifierVeteran(s) : l10n.modifierBattle(s);
|
||||
case 2:
|
||||
case -2:
|
||||
return 'cursed $s';
|
||||
return l10n.modifierCursed(s);
|
||||
case 3:
|
||||
case -3:
|
||||
return s.contains(' ') ? 'warrior $s' : 'Were-$s';
|
||||
return s.contains(' ') ? l10n.modifierWarrior(s) : l10n.modifierWere(s);
|
||||
case 4:
|
||||
case -4:
|
||||
return 'undead $s';
|
||||
return l10n.modifierUndead(s);
|
||||
case 5:
|
||||
case -5:
|
||||
return 'demon $s';
|
||||
return l10n.modifierDemon(s);
|
||||
default:
|
||||
return s;
|
||||
}
|
||||
@@ -916,25 +935,28 @@ String _pick(String pipeSeparated, DeterministicRandom rng) {
|
||||
// =============================================================================
|
||||
|
||||
/// NPC 이름 생성 (ImpressiveGuy, Main.pas:514-521)
|
||||
/// 인상적인 타이틀 + 종족 또는 이름 조합
|
||||
/// 인상적인 타이틀 + 종족 또는 이름 조합 (l10n 지원)
|
||||
String impressiveGuy(PqConfig config, DeterministicRandom rng) {
|
||||
var result = pick(config.impressiveTitles, rng);
|
||||
final titleEn = pick(config.impressiveTitles, rng);
|
||||
final title = l10n.translateImpressiveTitle(titleEn);
|
||||
switch (rng.nextInt(2)) {
|
||||
case 0:
|
||||
// "the King of the Elves" 형태
|
||||
final race = pick(config.races, rng).split('|').first;
|
||||
result = 'the $result of the ${pluralize(race)}';
|
||||
break;
|
||||
// "the King of the Elves" / "엘프들의 왕" 형태
|
||||
final raceEn = pick(config.races, rng).split('|').first;
|
||||
final race = l10n.translateRace(raceEn);
|
||||
return l10n.impressiveGuyPattern1(title, race);
|
||||
case 1:
|
||||
// "King Vrognak of Zoxzik" 형태
|
||||
result = '$result ${generateName(rng)} of ${generateName(rng)}';
|
||||
break;
|
||||
// "King Vrognak of Zoxzik" / "Zoxzik의 왕 Vrognak" 형태
|
||||
final name1 = generateName(rng);
|
||||
final name2 = generateName(rng);
|
||||
return l10n.impressiveGuyPattern2(title, name1, name2);
|
||||
default:
|
||||
return title;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/// 이름 있는 몬스터 생성 (NamedMonster, Main.pas:498-512)
|
||||
/// 레벨에 맞는 몬스터 선택 후 GenerateName으로 이름 붙이기
|
||||
/// 레벨에 맞는 몬스터 선택 후 GenerateName으로 이름 붙이기 (l10n 지원)
|
||||
String namedMonster(PqConfig config, DeterministicRandom rng, int level) {
|
||||
String best = '';
|
||||
int bestLevel = 0;
|
||||
@@ -952,11 +974,14 @@ String namedMonster(PqConfig config, DeterministicRandom rng, int level) {
|
||||
}
|
||||
}
|
||||
|
||||
return '${generateName(rng)} the $best';
|
||||
// 몬스터 이름 번역
|
||||
final translatedMonster = l10n.translateMonster(best);
|
||||
// "GeneratedName the MonsterType" / "몬스터타입 GeneratedName" 형태
|
||||
return l10n.namedMonsterFormat(generateName(rng), translatedMonster);
|
||||
}
|
||||
|
||||
/// 플롯 간 시네마틱 이벤트 생성 (InterplotCinematic, Main.pas:456-495)
|
||||
/// 3가지 시나리오 중 하나를 랜덤 선택
|
||||
/// 3가지 시나리오 중 하나를 랜덤 선택 (l10n 지원)
|
||||
List<QueueEntry> interplotCinematic(
|
||||
PqConfig config,
|
||||
DeterministicRandom rng,
|
||||
@@ -975,25 +1000,17 @@ List<QueueEntry> interplotCinematic(
|
||||
switch (rng.nextInt(3)) {
|
||||
case 0:
|
||||
// 시나리오 1: 안전한 캐시 영역 도착
|
||||
q(
|
||||
QueueKind.task,
|
||||
1,
|
||||
'Exhausted, you reach a safe Cache Zone in the corrupted network',
|
||||
);
|
||||
q(QueueKind.task, 2, 'You reconnect with old allies and fork new ones');
|
||||
q(QueueKind.task, 2, 'You attend a council of the Debugger Knights');
|
||||
q(QueueKind.task, 1, 'Many bugs await. You are chosen to patch them!');
|
||||
q(QueueKind.task, 1, l10n.cinematicCacheZone1());
|
||||
q(QueueKind.task, 2, l10n.cinematicCacheZone2());
|
||||
q(QueueKind.task, 2, l10n.cinematicCacheZone3());
|
||||
q(QueueKind.task, 1, l10n.cinematicCacheZone4());
|
||||
break;
|
||||
|
||||
case 1:
|
||||
// 시나리오 2: 강력한 버그와의 전투
|
||||
q(
|
||||
QueueKind.task,
|
||||
1,
|
||||
'Your target is in sight, but a critical bug blocks your path!',
|
||||
);
|
||||
q(QueueKind.task, 1, l10n.cinematicCombat1());
|
||||
final nemesis = namedMonster(config, rng, level + 3);
|
||||
q(QueueKind.task, 4, 'A desperate debugging session begins with $nemesis');
|
||||
q(QueueKind.task, 4, l10n.cinematicCombat2(nemesis));
|
||||
|
||||
var s = rng.nextInt(3);
|
||||
final combatRounds = rng.nextInt(1 + plotCount);
|
||||
@@ -1001,63 +1018,37 @@ List<QueueEntry> interplotCinematic(
|
||||
s += 1 + rng.nextInt(2);
|
||||
switch (s % 3) {
|
||||
case 0:
|
||||
q(QueueKind.task, 2, 'Locked in intense debugging with $nemesis');
|
||||
q(QueueKind.task, 2, l10n.cinematicCombatLocked(nemesis));
|
||||
break;
|
||||
case 1:
|
||||
q(QueueKind.task, 2, '$nemesis corrupts your stack trace');
|
||||
q(QueueKind.task, 2, l10n.cinematicCombatCorrupts(nemesis));
|
||||
break;
|
||||
case 2:
|
||||
q(
|
||||
QueueKind.task,
|
||||
2,
|
||||
'Your patch seems to be working against $nemesis',
|
||||
);
|
||||
q(QueueKind.task, 2, l10n.cinematicCombatWorking(nemesis));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
q(
|
||||
QueueKind.task,
|
||||
3,
|
||||
'Victory! $nemesis is patched! System reboots for recovery',
|
||||
);
|
||||
q(
|
||||
QueueKind.task,
|
||||
2,
|
||||
'You wake up in a Safe Mode, but the kernel awaits',
|
||||
);
|
||||
q(QueueKind.task, 3, l10n.cinematicCombatVictory(nemesis));
|
||||
q(QueueKind.task, 2, l10n.cinematicCombatWakeUp());
|
||||
break;
|
||||
|
||||
case 2:
|
||||
// 시나리오 3: 내부자 위협 발견
|
||||
final guy = impressiveGuy(config, rng);
|
||||
q(
|
||||
QueueKind.task,
|
||||
2,
|
||||
'What relief! You reach the secure server of $guy',
|
||||
);
|
||||
q(
|
||||
QueueKind.task,
|
||||
3,
|
||||
'There is celebration, and a suspicious private handshake with $guy',
|
||||
);
|
||||
q(
|
||||
QueueKind.task,
|
||||
2,
|
||||
'You forget your ${boringItem(config, rng)} and go back to retrieve it',
|
||||
);
|
||||
q(QueueKind.task, 2, 'What is this!? You intercept a corrupted packet!');
|
||||
q(QueueKind.task, 2, 'Could $guy be a backdoor for the Glitch God?');
|
||||
q(
|
||||
QueueKind.task,
|
||||
3,
|
||||
'Who can be trusted with this intel!? -- The Binary Temple, of course',
|
||||
);
|
||||
final itemEn = boringItem(config, rng);
|
||||
final item = l10n.translateBoringItem(itemEn);
|
||||
q(QueueKind.task, 2, l10n.cinematicBetrayal1(guy));
|
||||
q(QueueKind.task, 3, l10n.cinematicBetrayal2(guy));
|
||||
q(QueueKind.task, 2, l10n.cinematicBetrayal3(item));
|
||||
q(QueueKind.task, 2, l10n.cinematicBetrayal4());
|
||||
q(QueueKind.task, 2, l10n.cinematicBetrayal5(guy));
|
||||
q(QueueKind.task, 3, l10n.cinematicBetrayal6());
|
||||
break;
|
||||
}
|
||||
|
||||
// 마지막에 plot 추가
|
||||
q(QueueKind.plot, 2, 'Compiling');
|
||||
q(QueueKind.plot, 2, l10n.taskCompiling);
|
||||
|
||||
return entries;
|
||||
}
|
||||
|
||||
@@ -227,6 +227,10 @@ class _GamePlayScreenState extends State<GamePlayScreen>
|
||||
colorTheme: _colorTheme,
|
||||
onThemeCycle: _cycleColorTheme,
|
||||
specialAnimation: _specialAnimation,
|
||||
weaponName: state.equipment.weapon,
|
||||
shieldName: state.equipment.shield,
|
||||
characterLevel: state.traits.level,
|
||||
monsterLevel: state.progress.currentTask.monsterLevel,
|
||||
),
|
||||
|
||||
// 메인 3패널 영역
|
||||
@@ -656,33 +660,51 @@ class _GamePlayScreenState extends State<GamePlayScreen>
|
||||
|
||||
Widget _buildQuestList(GameState state) {
|
||||
final l10n = L10n.of(context);
|
||||
final questCount = state.progress.questCount;
|
||||
if (questCount == 0) {
|
||||
final questHistory = state.progress.questHistory;
|
||||
|
||||
if (questHistory.isEmpty) {
|
||||
return Center(
|
||||
child: Text(l10n.noActiveQuests, style: const TextStyle(fontSize: 11)),
|
||||
);
|
||||
}
|
||||
|
||||
// 현재 퀘스트 캡션이 있으면 표시
|
||||
final currentTask = state.progress.currentTask;
|
||||
return ListView(
|
||||
// 원본처럼 퀘스트 히스토리를 리스트로 표시
|
||||
// 완료된 퀘스트는 체크박스, 현재 퀘스트는 화살표
|
||||
return ListView.builder(
|
||||
itemCount: questHistory.length,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||
children: [
|
||||
Row(
|
||||
itemBuilder: (context, index) {
|
||||
final quest = questHistory[index];
|
||||
final isCurrentQuest = index == questHistory.length - 1 && !quest.isComplete;
|
||||
|
||||
return Row(
|
||||
children: [
|
||||
const Icon(Icons.arrow_right, size: 14),
|
||||
if (isCurrentQuest)
|
||||
const Icon(Icons.arrow_right, size: 14)
|
||||
else
|
||||
Icon(
|
||||
quest.isComplete
|
||||
? Icons.check_box
|
||||
: Icons.check_box_outline_blank,
|
||||
size: 14,
|
||||
color: quest.isComplete ? Colors.green : Colors.grey,
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Expanded(
|
||||
child: Text(
|
||||
currentTask.caption.isNotEmpty
|
||||
? currentTask.caption
|
||||
: l10n.questNumber(questCount),
|
||||
style: const TextStyle(fontSize: 11),
|
||||
quest.caption,
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
decoration: quest.isComplete
|
||||
? TextDecoration.lineThrough
|
||||
: TextDecoration.none,
|
||||
),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,12 @@ import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:askiineverdie/src/core/animation/ascii_animation_data.dart';
|
||||
import 'package:askiineverdie/src/core/animation/ascii_animation_type.dart';
|
||||
import 'package:askiineverdie/src/core/animation/background_layer.dart';
|
||||
import 'package:askiineverdie/src/core/animation/battle_composer.dart';
|
||||
import 'package:askiineverdie/src/core/animation/character_frames.dart';
|
||||
import 'package:askiineverdie/src/core/animation/monster_colors.dart';
|
||||
import 'package:askiineverdie/src/core/animation/monster_size.dart';
|
||||
import 'package:askiineverdie/src/core/animation/weapon_category.dart';
|
||||
import 'package:askiineverdie/src/core/model/game_state.dart';
|
||||
|
||||
/// ASCII 애니메이션 카드 위젯
|
||||
@@ -19,6 +25,10 @@ class AsciiAnimationCard extends StatefulWidget {
|
||||
this.monsterBaseName,
|
||||
this.colorTheme = AsciiColorTheme.green,
|
||||
this.specialAnimation,
|
||||
this.weaponName,
|
||||
this.shieldName,
|
||||
this.characterLevel,
|
||||
this.monsterLevel,
|
||||
});
|
||||
|
||||
final TaskType taskType;
|
||||
@@ -31,6 +41,18 @@ class AsciiAnimationCard extends StatefulWidget {
|
||||
/// 설정되면 일반 애니메이션 대신 표시
|
||||
final AsciiAnimationType? specialAnimation;
|
||||
|
||||
/// 현재 장착 무기 이름 (공격 스타일 결정용)
|
||||
final String? weaponName;
|
||||
|
||||
/// 현재 장착 방패 이름 (방패 표시용)
|
||||
final String? shieldName;
|
||||
|
||||
/// 캐릭터 레벨
|
||||
final int? characterLevel;
|
||||
|
||||
/// 몬스터 레벨 (몬스터 크기 결정용)
|
||||
final int? monsterLevel;
|
||||
|
||||
@override
|
||||
State<AsciiAnimationCard> createState() => _AsciiAnimationCardState();
|
||||
}
|
||||
@@ -41,6 +63,29 @@ class _AsciiAnimationCardState extends State<AsciiAnimationCard> {
|
||||
late AsciiAnimationData _animationData;
|
||||
AsciiAnimationType? _currentSpecialAnimation;
|
||||
|
||||
// 전투 애니메이션 상태
|
||||
bool _isBattleMode = false;
|
||||
BattlePhase _battlePhase = BattlePhase.idle;
|
||||
int _battleSubFrame = 0;
|
||||
BattleComposer? _battleComposer;
|
||||
|
||||
// 글로벌 틱 (배경 스크롤용)
|
||||
int _globalTick = 0;
|
||||
|
||||
// 환경 타입
|
||||
EnvironmentType _environment = EnvironmentType.forest;
|
||||
|
||||
// 전투 페이즈 시퀀스 (반복)
|
||||
static const _battlePhaseSequence = [
|
||||
(BattlePhase.idle, 4), // 4 프레임 대기
|
||||
(BattlePhase.prepare, 2), // 2 프레임 준비
|
||||
(BattlePhase.attack, 3), // 3 프레임 공격
|
||||
(BattlePhase.hit, 2), // 2 프레임 히트
|
||||
(BattlePhase.recover, 2), // 2 프레임 복귀
|
||||
];
|
||||
int _phaseIndex = 0;
|
||||
int _phaseFrameCount = 0;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
@@ -64,7 +109,10 @@ class _AsciiAnimationCardState extends State<AsciiAnimationCard> {
|
||||
}
|
||||
|
||||
if (oldWidget.taskType != widget.taskType ||
|
||||
oldWidget.monsterBaseName != widget.monsterBaseName) {
|
||||
oldWidget.monsterBaseName != widget.monsterBaseName ||
|
||||
oldWidget.weaponName != widget.weaponName ||
|
||||
oldWidget.shieldName != widget.shieldName ||
|
||||
oldWidget.monsterLevel != widget.monsterLevel) {
|
||||
_updateAnimation();
|
||||
}
|
||||
}
|
||||
@@ -74,6 +122,7 @@ class _AsciiAnimationCardState extends State<AsciiAnimationCard> {
|
||||
|
||||
// 특수 애니메이션이 있으면 우선 적용
|
||||
if (_currentSpecialAnimation != null) {
|
||||
_isBattleMode = false;
|
||||
_animationData = getAnimationData(_currentSpecialAnimation!);
|
||||
_currentFrame = 0;
|
||||
|
||||
@@ -99,26 +148,80 @@ class _AsciiAnimationCardState extends State<AsciiAnimationCard> {
|
||||
// 일반 애니메이션 처리
|
||||
final animationType = taskTypeToAnimation(widget.taskType);
|
||||
|
||||
// 전투 타입이면 몬스터 카테고리에 따라 다른 애니메이션 선택
|
||||
// 전투 타입이면 새 BattleComposer 시스템 사용
|
||||
if (animationType == AsciiAnimationType.battle) {
|
||||
final category = getMonsterCategory(widget.monsterBaseName);
|
||||
_animationData = getBattleAnimation(category);
|
||||
_isBattleMode = true;
|
||||
_setupBattleComposer();
|
||||
_battlePhase = BattlePhase.idle;
|
||||
_battleSubFrame = 0;
|
||||
_phaseIndex = 0;
|
||||
_phaseFrameCount = 0;
|
||||
|
||||
_timer = Timer.periodic(
|
||||
const Duration(milliseconds: 200),
|
||||
(_) => _advanceBattleFrame(),
|
||||
);
|
||||
} else {
|
||||
_isBattleMode = false;
|
||||
_animationData = getAnimationData(animationType);
|
||||
_currentFrame = 0;
|
||||
|
||||
_timer = Timer.periodic(
|
||||
Duration(milliseconds: _animationData.frameIntervalMs),
|
||||
(_) {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_currentFrame =
|
||||
(_currentFrame + 1) % _animationData.frames.length;
|
||||
});
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
_currentFrame = 0;
|
||||
void _setupBattleComposer() {
|
||||
final weaponCategory = getWeaponCategory(widget.weaponName);
|
||||
final hasShield =
|
||||
widget.shieldName != null && widget.shieldName!.isNotEmpty;
|
||||
final monsterCategory = getMonsterCategory(widget.monsterBaseName);
|
||||
final monsterSize = getMonsterSize(widget.monsterLevel);
|
||||
|
||||
_timer = Timer.periodic(
|
||||
Duration(milliseconds: _animationData.frameIntervalMs),
|
||||
(_) {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_currentFrame = (_currentFrame + 1) % _animationData.frames.length;
|
||||
});
|
||||
}
|
||||
},
|
||||
_battleComposer = BattleComposer(
|
||||
weaponCategory: weaponCategory,
|
||||
hasShield: hasShield,
|
||||
monsterCategory: monsterCategory,
|
||||
monsterSize: monsterSize,
|
||||
);
|
||||
|
||||
// 환경 타입 추론
|
||||
_environment = inferEnvironment(
|
||||
widget.taskType.name,
|
||||
widget.monsterBaseName,
|
||||
);
|
||||
}
|
||||
|
||||
void _advanceBattleFrame() {
|
||||
if (!mounted) return;
|
||||
|
||||
setState(() {
|
||||
// 글로벌 틱 증가 (배경 스크롤용)
|
||||
_globalTick++;
|
||||
|
||||
_phaseFrameCount++;
|
||||
final currentPhase = _battlePhaseSequence[_phaseIndex];
|
||||
|
||||
// 현재 페이즈의 프레임 수를 초과하면 다음 페이즈로
|
||||
if (_phaseFrameCount >= currentPhase.$2) {
|
||||
_phaseIndex = (_phaseIndex + 1) % _battlePhaseSequence.length;
|
||||
_phaseFrameCount = 0;
|
||||
_battleSubFrame = 0;
|
||||
} else {
|
||||
_battleSubFrame++;
|
||||
}
|
||||
|
||||
_battlePhase = _battlePhaseSequence[_phaseIndex].$1;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -138,11 +241,35 @@ class _AsciiAnimationCardState extends State<AsciiAnimationCard> {
|
||||
? colors.backgroundColor.withValues(alpha: 0.95)
|
||||
: colors.backgroundColor;
|
||||
|
||||
// 프레임 인덱스가 범위를 벗어나지 않도록 보정
|
||||
final frameIndex = _currentFrame.clamp(0, _animationData.frames.length - 1);
|
||||
// 프레임 텍스트 결정
|
||||
String frameText;
|
||||
Color textColor = colors.textColor;
|
||||
|
||||
if (_isBattleMode && _battleComposer != null) {
|
||||
// 새 배틀 시스템 사용 (배경 포함)
|
||||
frameText = _battleComposer!.composeFrameWithBackground(
|
||||
_battlePhase,
|
||||
_battleSubFrame,
|
||||
widget.monsterBaseName,
|
||||
_environment,
|
||||
_globalTick,
|
||||
);
|
||||
|
||||
// 히트 페이즈면 몬스터 색상 변경
|
||||
if (_battlePhase == BattlePhase.hit) {
|
||||
final monsterColorCategory =
|
||||
getMonsterColorCategory(widget.monsterBaseName);
|
||||
textColor = getMonsterColors(monsterColorCategory).hit;
|
||||
}
|
||||
} else {
|
||||
// 기존 레거시 시스템 사용
|
||||
final frameIndex =
|
||||
_currentFrame.clamp(0, _animationData.frames.length - 1);
|
||||
frameText = _animationData.frames[frameIndex];
|
||||
}
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
padding: const EdgeInsets.all(4),
|
||||
decoration: BoxDecoration(
|
||||
color: bgColor,
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
@@ -150,19 +277,49 @@ class _AsciiAnimationCardState extends State<AsciiAnimationCard> {
|
||||
? Border.all(color: colors.textColor.withValues(alpha: 0.5))
|
||||
: null,
|
||||
),
|
||||
child: Center(
|
||||
child: Text(
|
||||
_animationData.frames[frameIndex],
|
||||
style: TextStyle(
|
||||
fontFamily: 'monospace',
|
||||
fontSize: 10,
|
||||
color: colors.textColor,
|
||||
height: 1.1,
|
||||
letterSpacing: 0,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
child: _isBattleMode
|
||||
? LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
// 60x8 프레임에 맞게 폰트 크기 자동 계산
|
||||
// ASCII 문자 비율: 너비 = 높이 * 0.6 (모노스페이스)
|
||||
final maxWidth = constraints.maxWidth;
|
||||
final maxHeight = constraints.maxHeight;
|
||||
// 60자 폭, 8줄 높이 기준
|
||||
final fontSizeByWidth = maxWidth / 60 / 0.6;
|
||||
final fontSizeByHeight = maxHeight / 8 / 1.2;
|
||||
final fontSize = (fontSizeByWidth < fontSizeByHeight
|
||||
? fontSizeByWidth
|
||||
: fontSizeByHeight)
|
||||
.clamp(6.0, 14.0);
|
||||
|
||||
return Center(
|
||||
child: Text(
|
||||
frameText,
|
||||
style: TextStyle(
|
||||
fontFamily: 'Courier',
|
||||
fontSize: fontSize,
|
||||
color: textColor,
|
||||
height: 1.2,
|
||||
letterSpacing: 0,
|
||||
),
|
||||
textAlign: TextAlign.left,
|
||||
),
|
||||
);
|
||||
},
|
||||
)
|
||||
: Center(
|
||||
child: Text(
|
||||
frameText,
|
||||
style: TextStyle(
|
||||
fontFamily: 'monospace',
|
||||
fontSize: 10,
|
||||
color: textColor,
|
||||
height: 1.1,
|
||||
letterSpacing: 0,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,6 +16,10 @@ class TaskProgressPanel extends StatelessWidget {
|
||||
required this.colorTheme,
|
||||
required this.onThemeCycle,
|
||||
this.specialAnimation,
|
||||
this.weaponName,
|
||||
this.shieldName,
|
||||
this.characterLevel,
|
||||
this.monsterLevel,
|
||||
});
|
||||
|
||||
final ProgressState progress;
|
||||
@@ -27,6 +31,12 @@ class TaskProgressPanel extends StatelessWidget {
|
||||
/// 특수 애니메이션 (레벨업, 퀘스트 완료 등)
|
||||
final AsciiAnimationType? specialAnimation;
|
||||
|
||||
/// 장비 정보 (애니메이션 스타일 결정용)
|
||||
final String? weaponName;
|
||||
final String? shieldName;
|
||||
final int? characterLevel;
|
||||
final int? monsterLevel;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
@@ -48,6 +58,10 @@ class TaskProgressPanel extends StatelessWidget {
|
||||
monsterBaseName: progress.currentTask.monsterBaseName,
|
||||
colorTheme: colorTheme,
|
||||
specialAnimation: specialAnimation,
|
||||
weaponName: weaponName,
|
||||
shieldName: shieldName,
|
||||
characterLevel: characterLevel,
|
||||
monsterLevel: monsterLevel,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
|
||||
@@ -200,7 +200,7 @@ class _NewCharacterScreenState extends State<NewCharacterScreen> {
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Progress Quest - New Character'),
|
||||
title: Text(L10n.of(context).newCharacterTitle),
|
||||
centerTitle: true,
|
||||
),
|
||||
body: SingleChildScrollView(
|
||||
@@ -231,7 +231,7 @@ class _NewCharacterScreenState extends State<NewCharacterScreen> {
|
||||
FilledButton.icon(
|
||||
onPressed: _onSold,
|
||||
icon: const Icon(Icons.check),
|
||||
label: const Text('Sold!'),
|
||||
label: Text(L10n.of(context).soldButton),
|
||||
style: FilledButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||
),
|
||||
|
||||
@@ -98,8 +98,8 @@ void main() {
|
||||
_buildTestApp(GamePlayScreen(controller: controller)),
|
||||
);
|
||||
|
||||
// AppBar 타이틀 확인 (L10n 사용) - 아스키나라 세계관
|
||||
expect(find.textContaining('ASCII-Nara'), findsOneWidget);
|
||||
// AppBar 타이틀 확인 (L10n 사용) - ASCII NEVER DIE
|
||||
expect(find.textContaining('ASCII NEVER DIE'), findsOneWidget);
|
||||
|
||||
// 3패널 헤더 확인
|
||||
expect(find.text('Character Sheet'), findsOneWidget);
|
||||
|
||||
@@ -19,8 +19,8 @@ void main() {
|
||||
_buildTestApp(NewCharacterScreen(onCharacterCreated: (_) {})),
|
||||
);
|
||||
|
||||
// 화면 타이틀 확인
|
||||
expect(find.text('Progress Quest - New Character'), findsOneWidget);
|
||||
// 화면 타이틀 확인 (l10n 적용됨)
|
||||
expect(find.text('ASCII NEVER DIE - New Character'), findsOneWidget);
|
||||
|
||||
// 종족 섹션 확인
|
||||
expect(find.text('Race'), findsOneWidget);
|
||||
|
||||
@@ -7,15 +7,15 @@ void main() {
|
||||
) async {
|
||||
await tester.pumpWidget(const AskiiNeverDieApp());
|
||||
|
||||
// 프런트 화면이 렌더링되었는지 확인 (아스키나라 세계관)
|
||||
expect(find.text('ASCII-Nara'), findsOneWidget);
|
||||
// 프런트 화면이 렌더링되었는지 확인
|
||||
expect(find.text('ASCII NEVER DIE'), findsOneWidget);
|
||||
|
||||
// "New character" 버튼 탭
|
||||
await tester.tap(find.text('New character'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// NewCharacterScreen으로 이동했는지 확인
|
||||
expect(find.text('Progress Quest - New Character'), findsOneWidget);
|
||||
// NewCharacterScreen으로 이동했는지 확인 (l10n 적용됨)
|
||||
expect(find.text('ASCII NEVER DIE - New Character'), findsOneWidget);
|
||||
expect(find.text('Race'), findsOneWidget);
|
||||
expect(find.text('Class'), findsOneWidget);
|
||||
expect(find.text('Sold!'), findsOneWidget);
|
||||
|
||||
Reference in New Issue
Block a user