diff --git a/lib/src/core/l10n/game_data_l10n.dart b/lib/src/core/l10n/game_data_l10n.dart index e16733b..30ed656 100644 --- a/lib/src/core/l10n/game_data_l10n.dart +++ b/lib/src/core/l10n/game_data_l10n.dart @@ -1,4 +1,5 @@ import 'package:askiineverdie/data/game_translations_ko.dart'; +import 'package:askiineverdie/src/core/util/pq_logic.dart'; import 'package:flutter/widgets.dart'; /// 게임 데이터 번역을 위한 헬퍼 클래스 @@ -169,4 +170,220 @@ class GameDataL10n { } return englishName; } + + /// 구조화된 장비 결과를 로컬라이즈된 문자열로 렌더링 + /// 예: EquipResult(baseName: "Keyboard", modifiers: ["Optimized"], plusValue: 2) + /// → 영어: "+2 Optimized Keyboard" + /// → 한국어: "+2 최적화된 키보드" + static String renderEquipResult( + BuildContext context, + EquipResult result, + int slotIndex, + ) { + if (_isKorean(context)) { + return _renderEquipResultKo(result, slotIndex); + } + return result.displayName; + } + + /// 한국어 장비 렌더링 (내부 함수) + static String _renderEquipResultKo(EquipResult result, int slotIndex) { + // 기본 장비 이름 번역 + String baseName; + if (slotIndex == 0) { + baseName = weaponTranslationsKo[result.baseName] ?? result.baseName; + } else if (slotIndex == 1) { + baseName = shieldTranslationsKo[result.baseName] ?? result.baseName; + } else { + baseName = armorTranslationsKo[result.baseName] ?? result.baseName; + } + + // 수식어 번역 (공격용 vs 방어용) + final isWeapon = slotIndex == 0; + final translatedModifiers = result.modifiers.map((mod) { + if (isWeapon) { + // 공격 속성: offenseAttrib 또는 offenseBad + return offenseAttribTranslationsKo[mod] ?? + offenseBadTranslationsKo[mod] ?? + mod; + } else { + // 방어 속성: defenseAttrib 또는 defenseBad + return defenseAttribTranslationsKo[mod] ?? + defenseBadTranslationsKo[mod] ?? + mod; + } + }).toList(); + + // 조합: 수식어들 + 기본 이름 + var name = baseName; + for (final mod in translatedModifiers) { + name = '$mod $name'; + } + + // +/- 수치 추가 + if (result.plusValue != 0) { + final sign = result.plusValue > 0 ? '+' : ''; + name = '$sign${result.plusValue} $name'; + } + + return name; + } + + /// 구조화된 아이템 결과를 로컬라이즈된 문자열로 렌더링 + /// 예: ItemResult(attrib: "Golden", special: "Iterator", itemOf: "Compilation") + /// → 영어: "Golden Iterator of Compilation" + /// → 한국어: "컴파일의 황금 이터레이터" + static String renderItemResult(BuildContext context, ItemResult result) { + if (_isKorean(context)) { + return _renderItemResultKo(result); + } + return result.displayName; + } + + /// 한국어 아이템 렌더링 (내부 함수) + static String _renderItemResultKo(ItemResult result) { + // 단순 아이템 (boringItem) + if (result.boringItem != null) { + // boringItem은 별도 번역 맵이 필요할 수 있음 + // 현재는 그대로 반환 (대부분 영어 그대로 사용) + return result.boringItem!; + } + + // 복합 아이템: attrib + special + itemOf + final attrib = result.attrib != null + ? (itemAttribTranslationsKo[result.attrib] ?? result.attrib!) + : null; + final special = result.special != null + ? (specialTranslationsKo[result.special] ?? result.special!) + : null; + final itemOf = result.itemOf != null + ? (itemOfsTranslationsKo[result.itemOf] ?? result.itemOf!) + : null; + + // 한국어 어순: "X의 Y" 패턴 + // "Golden Iterator of Compilation" → "컴파일의 황금 이터레이터" + if (attrib != null && special != null && itemOf != null) { + return '$itemOf의 $attrib $special'; + } + + // attrib + special만 있는 경우 + if (attrib != null && special != null) { + return '$attrib $special'; + } + + return ''; + } + + /// 장비 이름 문자열 파싱 후 번역 (기존 저장 데이터 호환) + /// 예: "+2 Optimized GPU-Powered Keyboard" → "+2 최적화된 GPU 파워 키보드" + static String translateEquipString( + BuildContext context, + String equipString, + int slotIndex, + ) { + if (!_isKorean(context) || equipString.isEmpty) return equipString; + + // 1. +/- 값 추출 + final plusMatch = RegExp(r'^([+-]?\d+)\s+').firstMatch(equipString); + String remaining = equipString; + String plusPart = ''; + if (plusMatch != null) { + plusPart = plusMatch.group(1)!; + remaining = equipString.substring(plusMatch.end); + } + + // 2. 기본 장비 이름 찾기 (가장 긴 매칭 우선) + final Map baseMap; + if (slotIndex == 0) { + baseMap = weaponTranslationsKo; + } else if (slotIndex == 1) { + baseMap = shieldTranslationsKo; + } else { + baseMap = armorTranslationsKo; + } + + String baseTranslated = remaining; + String modifierPart = ''; + + // 기본 장비 이름을 뒤에서부터 찾기 + for (final entry in baseMap.entries) { + if (remaining.endsWith(entry.key)) { + baseTranslated = entry.value; + modifierPart = remaining.substring( + 0, + remaining.length - entry.key.length, + ).trim(); + break; + } + } + + // 3. 수식어 번역 + final isWeapon = slotIndex == 0; + final modWords = modifierPart.split(' ').where((s) => s.isNotEmpty).toList(); + final translatedMods = modWords.map((mod) { + if (isWeapon) { + return offenseAttribTranslationsKo[mod] ?? + offenseBadTranslationsKo[mod] ?? + mod; + } else { + return defenseAttribTranslationsKo[mod] ?? + defenseBadTranslationsKo[mod] ?? + mod; + } + }).toList(); + + // 4. 조합 + var result = baseTranslated; + for (final mod in translatedMods.reversed) { + result = '$mod $result'; + } + if (plusPart.isNotEmpty) { + result = '$plusPart $result'; + } + + return result; + } + + /// 아이템 이름 문자열 파싱 후 번역 (기존 저장 데이터 호환) + /// 예: "Golden Iterator of Compilation" → "컴파일의 황금 이터레이터" + 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); + + if (ofMatch != null) { + final beforeOf = ofMatch.group(1)!; // "Golden Iterator" + final afterOf = ofMatch.group(2)!; // "Compilation" + + // "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 없음) + final words = itemString.split(' '); + if (words.length == 2) { + final attrib = words[0]; + final special = words[1]; + + final attribKo = itemAttribTranslationsKo[attrib] ?? attrib; + final specialKo = specialTranslationsKo[special] ?? special; + + return '$attribKo $specialKo'; + } + + // 단일 단어 (boringItem 등) + return itemString; + } } diff --git a/lib/src/core/util/pq_logic.dart b/lib/src/core/util/pq_logic.dart index 9eaff7c..8e54428 100644 --- a/lib/src/core/util/pq_logic.dart +++ b/lib/src/core/util/pq_logic.dart @@ -9,6 +9,70 @@ import 'package:askiineverdie/src/core/model/game_state.dart'; // Mirrors core utility functions from the original Delphi sources (Main.pas / NewGuy.pas). +/// 장비 생성 결과 (구조화된 데이터로 l10n 지원) +class EquipResult { + const EquipResult({ + required this.baseName, + this.modifiers = const [], + this.plusValue = 0, + }); + + /// 기본 장비 이름 (예: "VPN Cloak") + final String baseName; + + /// 수식어 목록 (예: ["Holey", "Deprecated"]) + final List modifiers; + + /// +/- 수치 (예: -1, +2) + final int plusValue; + + /// 영문 전체 이름 생성 (기존 방식) + String get displayName { + var name = baseName; + for (final mod in modifiers) { + name = '$mod $name'; + } + if (plusValue != 0) { + name = '${plusValue > 0 ? '+' : ''}$plusValue $name'; + } + return name; + } +} + +/// 아이템 생성 결과 (구조화된 데이터로 l10n 지원) +class ItemResult { + const ItemResult({ + this.attrib, + this.special, + this.itemOf, + this.boringItem, + }); + + /// 아이템 속성 (예: "Golden") + final String? attrib; + + /// 특수 아이템 (예: "Iterator") + final String? special; + + /// "~의" 접미사 (예: "Monitoring") + final String? itemOf; + + /// 단순 아이템 (보링 아이템용) + final String? boringItem; + + /// 영문 전체 이름 생성 (기존 방식) + String get displayName { + if (boringItem != null) return boringItem!; + if (attrib != null && special != null && itemOf != null) { + return '$attrib $special of $itemOf'; + } + if (attrib != null && special != null) { + return '$attrib $special'; + } + return ''; + } +} + int levelUpTimeSeconds(int level) { // ~20 minutes for level 1, then exponential growth (same as LevelUpTime in Main.pas). final seconds = (20.0 + math.pow(1.15, level)) * 60.0; @@ -101,6 +165,41 @@ String specialItem(PqConfig config, DeterministicRandom rng) { return '${interestingItem(config, rng)} of ${pick(config.itemOfs, rng)}'; } +/// 구조화된 아이템 결과 반환 (l10n 지원) +ItemResult boringItemStructured(PqConfig config, DeterministicRandom rng) { + return ItemResult(boringItem: pick(config.boringItems, rng)); +} + +/// 구조화된 아이템 결과 반환 (l10n 지원) +ItemResult interestingItemStructured(PqConfig config, DeterministicRandom rng) { + return ItemResult( + attrib: pick(config.itemAttrib, rng), + special: pick(config.specials, rng), + ); +} + +/// 구조화된 아이템 결과 반환 (l10n 지원) +ItemResult specialItemStructured(PqConfig config, DeterministicRandom rng) { + return ItemResult( + attrib: pick(config.itemAttrib, rng), + special: pick(config.specials, rng), + itemOf: pick(config.itemOfs, rng), + ); +} + +/// 구조화된 아이템 결과 반환 (l10n 지원) +ItemResult winItemStructured( + PqConfig config, + DeterministicRandom rng, + int inventoryCount, +) { + final threshold = math.max(250, rng.nextInt(999)); + if (inventoryCount > threshold) { + return const ItemResult(); // 빈 결과 + } + return specialItemStructured(config, rng); +} + String pickWeapon(PqConfig config, DeterministicRandom rng, int level) { return _lPick(config.weapons, rng, level); } @@ -256,6 +355,78 @@ String winEquipBySlot( return winEquip(config, rng, level, slot.index); } +/// 구조화된 장비 생성 결과 반환 (l10n 지원) +EquipResult winEquipStructured( + PqConfig config, + DeterministicRandom rng, + int level, + int slotIndex, +) { + final bool isWeapon = slotIndex == 0; + final List items; + if (slotIndex == 0) { + items = config.weapons; + } else if (slotIndex == 1) { + items = config.shields; + } else { + items = config.armors; + } + final better = isWeapon ? config.offenseAttrib : config.defenseAttrib; + final worse = isWeapon ? config.offenseBad : config.defenseBad; + + final base = _lPick(items, rng, level); + final parts = base.split('|'); + final baseName = parts[0]; + final qual = parts.length > 1 + ? int.tryParse(parts[1].replaceAll('+', '')) ?? 0 + : 0; + + final plus = level - qual; + final modifierList = plus >= 0 ? better : worse; + return _addModifierStructured(rng, baseName, modifierList, plus); +} + +/// 구조화된 장비 결과 반환 (내부 함수) +EquipResult _addModifierStructured( + DeterministicRandom rng, + String baseName, + List modifiers, + int plus, +) { + final collectedModifiers = []; + var remaining = plus; + var count = 0; + + while (count < 2 && remaining != 0) { + final modifier = pick(modifiers, rng); + final parts = modifier.split('|'); + if (parts.isEmpty) break; + final label = parts[0]; + final qual = parts.length > 1 ? int.tryParse(parts[1]) ?? 0 : 0; + if (collectedModifiers.contains(label)) break; // avoid repeats + if (remaining.abs() < qual.abs()) break; + collectedModifiers.add(label); + remaining -= qual; + count++; + } + + return EquipResult( + baseName: baseName, + modifiers: collectedModifiers, + plusValue: remaining, + ); +} + +/// EquipmentSlot enum을 사용하는 구조화된 버전 +EquipResult winEquipBySlotStructured( + PqConfig config, + DeterministicRandom rng, + int level, + EquipmentSlot slot, +) { + return winEquipStructured(config, rng, level, slot.index); +} + int winStatIndex(DeterministicRandom rng, List statValues) { // 원본 Main.pas:870-883 // 50%: 모든 8개 스탯 중 랜덤 diff --git a/lib/src/features/game/game_play_screen.dart b/lib/src/features/game/game_play_screen.dart index f942792..a6b228c 100644 --- a/lib/src/features/game/game_play_screen.dart +++ b/lib/src/features/game/game_play_screen.dart @@ -510,19 +510,20 @@ class _GamePlayScreenState extends State Widget _buildEquipmentList(GameState state) { // 원본 Main.dfm Equips ListView - 11개 슬롯 + // (슬롯 레이블, 장비 이름, 슬롯 인덱스) 튜플 final l10n = L10n.of(context); final equipment = [ - (l10n.equipWeapon, state.equipment.weapon), - (l10n.equipShield, state.equipment.shield), - (l10n.equipHelm, state.equipment.helm), - (l10n.equipHauberk, state.equipment.hauberk), - (l10n.equipBrassairts, state.equipment.brassairts), - (l10n.equipVambraces, state.equipment.vambraces), - (l10n.equipGauntlets, state.equipment.gauntlets), - (l10n.equipGambeson, state.equipment.gambeson), - (l10n.equipCuisses, state.equipment.cuisses), - (l10n.equipGreaves, state.equipment.greaves), - (l10n.equipSollerets, state.equipment.sollerets), + (l10n.equipWeapon, state.equipment.weapon, 0), + (l10n.equipShield, state.equipment.shield, 1), + (l10n.equipHelm, state.equipment.helm, 2), + (l10n.equipHauberk, state.equipment.hauberk, 3), + (l10n.equipBrassairts, state.equipment.brassairts, 4), + (l10n.equipVambraces, state.equipment.vambraces, 5), + (l10n.equipGauntlets, state.equipment.gauntlets, 6), + (l10n.equipGambeson, state.equipment.gambeson, 7), + (l10n.equipCuisses, state.equipment.cuisses, 8), + (l10n.equipGreaves, state.equipment.greaves, 9), + (l10n.equipSollerets, state.equipment.sollerets, 10), ]; return ListView.builder( @@ -530,6 +531,10 @@ class _GamePlayScreenState extends State padding: const EdgeInsets.symmetric(horizontal: 8), itemBuilder: (context, index) { final equip = equipment[index]; + // 장비 이름 번역 (슬롯 인덱스 사용) + final translatedName = equip.$2.isNotEmpty + ? GameDataL10n.translateEquipString(context, equip.$2, equip.$3) + : '-'; return Padding( padding: const EdgeInsets.symmetric(vertical: 2), child: Row( @@ -540,7 +545,7 @@ class _GamePlayScreenState extends State ), Expanded( child: Text( - equip.$2.isNotEmpty ? equip.$2 : '-', + translatedName, style: const TextStyle( fontSize: 11, fontWeight: FontWeight.bold, @@ -587,11 +592,16 @@ class _GamePlayScreenState extends State ); } final item = state.inventory.items[index - 1]; + // 아이템 이름 번역 + final translatedName = GameDataL10n.translateItemString( + context, + item.name, + ); return Row( children: [ Expanded( child: Text( - item.name, + translatedName, style: const TextStyle(fontSize: 11), overflow: TextOverflow.ellipsis, ),