diff --git a/lib/data/game_text_l10n.dart b/lib/data/game_text_l10n.dart new file mode 100644 index 0000000..0f943f2 --- /dev/null +++ b/lib/data/game_text_l10n.dart @@ -0,0 +1,284 @@ +// 게임 텍스트 로컬라이제이션 (BuildContext 없이 사용) +// progress_service.dart, pq_logic.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 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' : 'foetal $s'; +String modifierBaby(String s) => isKoreanLocale ? '아기 $s' : 'baby $s'; +String modifierPreadolescent(String s) => + isKoreanLocale ? '사춘기 전 $s' : 'preadolescent $s'; +String modifierTeenage(String s) => isKoreanLocale ? '십대 $s' : 'teenage $s'; +String modifierUnderage(String s) => isKoreanLocale ? '미성년 $s' : 'underage $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'; diff --git a/lib/src/core/engine/progress_service.dart b/lib/src/core/engine/progress_service.dart index 8f92eed..eeaf428 100644 --- a/lib/src/core/engine/progress_service.dart +++ b/lib/src/core/engine/progress_service.dart @@ -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-Nara) 세계관 프롤로그 (l10n 지원) + final prologueTexts = l10n.prologueTexts; final initialQueue = [ - 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, ); @@ -711,9 +714,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( diff --git a/lib/src/core/util/pq_logic.dart b/lib/src/core/util/pq_logic.dart index 8e54428..4cc59da 100644 --- a/lib/src/core/util/pq_logic.dart +++ b/lib/src/core/util/pq_logic.dart @@ -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)); } } @@ -512,7 +514,7 @@ MonsterTaskResult monsterTask( if (rng.nextInt(2) == 0) { // 'passing Race Class' 형태 final klass = pick(config.klasses, rng).split('|').first; - monster = 'passing $race $klass'; + monster = l10n.modifierPassing('$race $klass'); } else { // 'Title Name the Race' 형태 (원본은 PickLow(Titles) 사용) final title = pickLow(config.titles, rng); @@ -562,7 +564,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 +575,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)); @@ -669,7 +671,7 @@ QuestResult completeQuest(PqConfig config, DeterministicRandom rng, int level) { } final name = best.split('|').first; return QuestResult( - caption: 'Patch ${definite(name, 2)}', + caption: l10n.questPatch(l10n.definiteL10n(name, 2)), reward: reward, monsterName: best, monsterLevel: bestLevel, @@ -677,14 +679,20 @@ QuestResult completeQuest(PqConfig config, DeterministicRandom rng, int level) { ); case 1: final item = interestingItem(config, rng); - return QuestResult(caption: 'Locate ${definite(item, 1)}', reward: reward); + return QuestResult( + caption: l10n.questLocate(l10n.definiteL10n(item, 1)), + reward: reward, + ); case 2: final item = boringItem(config, rng); - return QuestResult(caption: 'Transfer this $item', reward: reward); + return QuestResult( + caption: l10n.questTransfer(item), + reward: reward, + ); case 3: final item = boringItem(config, rng); return QuestResult( - caption: 'Download ${indefinite(item, 1)}', + caption: l10n.questDownload(l10n.indefiniteL10n(item, 1)), reward: reward, ); default: @@ -703,7 +711,7 @@ QuestResult completeQuest(PqConfig config, DeterministicRandom rng, int level) { final name = best.split('|').first; // Stabilize는 fQuest.Caption := '' 로 비움 → monsterIndex 미저장 return QuestResult( - caption: 'Stabilize ${definite(name, 2)}', + caption: l10n.questStabilize(l10n.definiteL10n(name, 2)), reward: reward, ); } @@ -723,7 +731,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 = []; @@ -815,19 +823,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 +845,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 +867,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 +889,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 +924,26 @@ 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 title = pick(config.impressiveTitles, rng); switch (rng.nextInt(2)) { case 0: - // "the King of the Elves" 형태 + // "the King of the Elves" / "엘프들의 왕" 형태 final race = pick(config.races, rng).split('|').first; - result = 'the $result of the ${pluralize(race)}'; - break; + 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 +961,12 @@ String namedMonster(PqConfig config, DeterministicRandom rng, int level) { } } - return '${generateName(rng)} the $best'; + // "GeneratedName the MonsterType" / "몬스터타입 GeneratedName" 형태 + return l10n.namedMonsterFormat(generateName(rng), best); } /// 플롯 간 시네마틱 이벤트 생성 (InterplotCinematic, Main.pas:456-495) -/// 3가지 시나리오 중 하나를 랜덤 선택 +/// 3가지 시나리오 중 하나를 랜덤 선택 (l10n 지원) List interplotCinematic( PqConfig config, DeterministicRandom rng, @@ -975,25 +985,17 @@ List 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 +1003,35 @@ List 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', - ); + q(QueueKind.task, 2, l10n.cinematicBetrayal1(guy)); + q(QueueKind.task, 3, l10n.cinematicBetrayal2(guy)); + q(QueueKind.task, 2, l10n.cinematicBetrayal3(boringItem(config, rng))); + 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; }