Compare commits

...

10 Commits

Author SHA1 Message Date
JiWoong Sul
598c25e4c9 fix(animation): ASCII 애니메이션 높낮이/공백 문제 수정
- walkingAnimation, townAnimation 4줄 → 3줄 통일
- character_frames.dart 모든 프레임 폭 6자로 통일
- _compose() 이펙트 Y 위치 동적 계산 (하드코딩 제거)
- withShield() 3줄 캐릭터용으로 수정 (index 3 → index 1)
- BattleComposer 캔버스 시스템 및 배경 합성 추가
- 무기 카테고리별 이펙트, 몬스터 크기/색상 시스템 구현
2025-12-13 18:22:50 +09:00
JiWoong Sul
e30177e788 refactor(content): 게임 제목 변경 및 부적절한 내용 수정
- 게임 제목을 'ASCII NEVER DIE'로 통일 (모든 ARB 파일, app.dart)
- 미성년 관련 몬스터 수식어를 RPG에 적합하게 변경:
  - foetal → primordial (원시)
  - baby → immature (미숙한)
  - preadolescent → growing (성장 중인)
  - teenage → young (어린)
  - underage → inexperienced (경험 부족)
- 테스트 파일 업데이트 (새 제목에 맞춤)
2025-12-12 15:50:59 +09:00
JiWoong Sul
8314aea578 fix(ui): 퀘스트 리스트를 원본처럼 히스토리 형태로 수정
- _buildQuestList에서 questHistory를 리스트로 표시
- 완료된 퀘스트: 체크 표시 + 취소선
- 현재 퀘스트: 화살표 아이콘
- 원본 PQ의 Quests TListView와 동일한 동작
2025-12-12 15:35:46 +09:00
JiWoong Sul
13198f9f1f feat(l10n): 퀘스트 및 시네마틱 텍스트 번역 적용
- game_text_l10n.dart에 게임 데이터 번역 함수 추가
  - translateMonster, translateRace, translateKlass
  - translateTitle, translateImpressiveTitle
  - translateBoringItem, translateInterestingItem
- pq_logic.dart monsterTask에서 몬스터 이름 번역
- completeQuest에서 퀘스트 아이템/몬스터 번역
- impressiveGuy, namedMonster에서 NPC 이름 번역
- interplotCinematic에서 시네마틱 아이템 번역
2025-12-11 19:42:25 +09:00
JiWoong Sul
b16ae6c2b8 feat(l10n): 몬스터 드롭 아이템 번역 로직 개선
- dropItemTranslationsKo 추가 (250+ 드롭 아이템 번역)
- translateItemString 함수 리팩터링:
  - specialItem 형식 정확히 감지 (itemOfs 검증)
  - 몬스터 드롭 형식 지원 ("{monster} {drop}" → "{몬스터}의 {드롭}")
- 인벤토리 아이템이 올바르게 한글로 표시됨
2025-12-11 19:30:49 +09:00
JiWoong Sul
071ac5f1e3 feat(l10n): 누락된 번역 및 기본 무기 수정
- 기본 무기를 'Sharp Stick'에서 'Keyboard'로 변경 (아스키나라 세계관)
- 몬스터 번역 168개 추가 (보안 위협, 버그 등)
- BoringItems(잡템) 번역 42개 추가
- game_data_l10n에서 boringItem 번역 적용
2025-12-11 19:22:43 +09:00
JiWoong Sul
5a567bc3e3 fix(l10n): 게임 텍스트 로케일 동기화 추가
- MaterialApp의 builder에서 setGameLocale() 호출
- Flutter l10n 시스템과 게임 텍스트 l10n의 로케일 동기화
- 이로써 프롤로그, 퀘스트, 몬스터 수식어 등이 올바른 언어로 표시됨
2025-12-11 19:12:29 +09:00
JiWoong Sul
ff0e0b7eb1 chore(l10n): ja/zh ARB 파일에 누락된 키 추가
- newCharacterTitle, soldButton 키를 app_ja.arb, app_zh.arb에 추가
- 현재는 영어 플레이스홀더 (향후 현지화 예정)
2025-12-11 19:08:12 +09:00
JiWoong Sul
fac7c7e6fc feat(l10n): 캐릭터 생성 화면 하드코딩 텍스트 l10n 적용
- newCharacterTitle, soldButton 키 추가 (app_en.arb, app_ko.arb)
- new_character_screen.dart 하드코딩 텍스트를 L10n 함수로 변경
- 관련 테스트 업데이트 (widget_test.dart, new_character_screen_test.dart)

변경 내역:
- "Progress Quest - New Character" → L10n.newCharacterTitle
- "Sold!" → L10n.soldButton
2025-12-11 18:52:24 +09:00
JiWoong Sul
0216eb1261 feat(l10n): 게임 텍스트 로컬라이제이션 확장
- game_text_l10n.dart: BuildContext 없이 사용할 수 있는 게임 텍스트 l10n 파일 생성
- progress_service.dart: 프롤로그/태스크 캡션 l10n 함수 사용으로 변경
- pq_logic.dart: 퀘스트/시네마틱/몬스터 수식어 l10n 함수 사용으로 변경

번역 적용 범위:
- 프롤로그 텍스트 (4개)
- 태스크 캡션 (컴파일, 이동, 디버깅, 판매 등)
- 퀘스트 캡션 (패치, 찾기, 전송, 다운로드, 안정화)
- 시네마틱 텍스트 (캐시 존, 전투, 배신 시나리오)
- 몬스터 수식어 (sick, young, big, special 등 모든 수식어)
- 시간 표시 (초, 분, 시간, 일)
- impressiveGuy, namedMonster 패턴
2025-12-11 18:49:02 +09:00
34 changed files with 3290 additions and 573 deletions

View 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';
}

View File

@@ -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': '힙 오버플로우',
};

View File

@@ -1,4 +1,4 @@
// 아스키나라(ASCII-Nara) 세계관 게임 데이터
// ASCII NEVER DIE 세계관 게임 데이터
// 코드의 신이 창조한 디지털 판타지 세계
const Map<String, List<String>> pqConfigData = {

View File

@@ -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" }
}

View File

@@ -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!"
}

View File

@@ -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": "확인!"
}

View File

@@ -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> {

View File

@@ -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!';
}

View File

@@ -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!';
}

View File

@@ -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 => '확인!';
}

View File

@@ -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!';
}

View File

@@ -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!"
}

View File

@@ -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,

View File

@@ -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,
);

View 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,
),
];

View 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;
}

View 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);
}

View 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' / \ ',
]),
];

View 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',
];

View 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;
}

View 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',
];

View 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,
);

View File

@@ -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(

View File

@@ -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(' ');
}
}

View File

@@ -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: '',

View File

@@ -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? ?? '',

View File

@@ -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;
}

View File

@@ -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,
),
),
],
),
],
);
},
);
}

View File

@@ -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,
),
),
);
}
}

View File

@@ -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),

View File

@@ -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),
),

View File

@@ -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);

View File

@@ -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);

View File

@@ -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);