diff --git a/lib/src/features/arena/widgets/arena_result_panel.dart b/lib/src/features/arena/widgets/arena_result_panel.dart index 2700991..ab01ac0 100644 --- a/lib/src/features/arena/widgets/arena_result_panel.dart +++ b/lib/src/features/arena/widgets/arena_result_panel.dart @@ -10,6 +10,7 @@ import 'package:asciineverdie/src/core/engine/item_service.dart'; import 'package:asciineverdie/src/core/model/arena_match.dart'; import 'package:asciineverdie/src/core/model/equipment_item.dart'; import 'package:asciineverdie/src/core/model/equipment_slot.dart'; +import 'package:asciineverdie/src/core/model/hall_of_fame.dart'; import 'package:asciineverdie/src/core/model/item_stats.dart'; import 'package:asciineverdie/src/features/game/widgets/combat_log.dart'; import 'package:asciineverdie/src/shared/retro_colors.dart'; @@ -103,6 +104,9 @@ class _ArenaResultPanelState extends State final fileName = 'arena_${challenger}_vs_${opponent}_$timestamp.json'; final file = File('${directory.path}/$fileName'); + // 전투 통계 계산 + final stats = _calculateBattleStats(); + final jsonData = { 'match': { 'challenger': challenger, @@ -111,6 +115,11 @@ class _ArenaResultPanelState extends State 'turnCount': widget.turnCount, 'timestamp': DateTime.now().toIso8601String(), }, + 'characters': { + 'challenger': _characterToJson(widget.result.match.challenger), + 'opponent': _characterToJson(widget.result.match.opponent), + }, + 'stats': stats, 'battleLog': widget.battleLog!.map((e) => e.toJson()).toList(), }; @@ -146,6 +155,126 @@ class _ArenaResultPanelState extends State } } + /// 캐릭터 정보를 JSON으로 변환 + Map _characterToJson(HallOfFameEntry entry) { + return { + 'name': entry.characterName, + 'level': entry.level, + 'race': entry.race, + 'class': entry.klass, + 'combatStats': entry.finalStats?.toJson(), + 'equipment': entry.finalEquipment + ?.map((EquipmentItem e) => { + 'slot': e.slot.name, + 'name': e.name, + 'level': e.level, + 'rarity': e.rarity.name, + 'stats': e.stats.toJson(), + }) + .toList(), + 'skills': entry.finalSkills, + }; + } + + /// 배틀 로그에서 전투 통계 계산 + Map _calculateBattleStats() { + if (widget.battleLog == null || widget.battleLog!.isEmpty) { + return {}; + } + + int challengerTotalDamage = 0; + int opponentTotalDamage = 0; + int challengerTotalHeal = 0; + int opponentTotalHeal = 0; + int challengerCriticals = 0; + int opponentCriticals = 0; + int challengerBlocks = 0; + int opponentBlocks = 0; + int challengerEvades = 0; + int opponentEvades = 0; + int challengerSkillsUsed = 0; + int opponentSkillsUsed = 0; + + final challenger = widget.result.match.challenger.characterName; + + for (final entry in widget.battleLog!) { + final msg = entry.message; + final isChallenger = msg.startsWith(challenger); + + switch (entry.type) { + case CombatLogType.damage: + final dmg = _extractNumber(msg); + if (isChallenger) { + challengerTotalDamage += dmg; + } + case CombatLogType.monsterAttack: + final dmg = _extractNumber(msg); + opponentTotalDamage += dmg; + case CombatLogType.critical: + final dmg = _extractNumber(msg); + if (isChallenger) { + challengerTotalDamage += dmg; + challengerCriticals++; + } else { + opponentTotalDamage += dmg; + opponentCriticals++; + } + case CombatLogType.heal: + final heal = _extractNumber(msg); + if (isChallenger) { + challengerTotalHeal += heal; + } else { + opponentTotalHeal += heal; + } + case CombatLogType.block: + if (isChallenger) { + challengerBlocks++; + } else { + opponentBlocks++; + } + case CombatLogType.evade: + if (isChallenger) { + challengerEvades++; + } else { + opponentEvades++; + } + case CombatLogType.skill: + if (isChallenger) { + challengerSkillsUsed++; + } else { + opponentSkillsUsed++; + } + default: + break; + } + } + + return { + 'challenger': { + 'totalDamage': challengerTotalDamage, + 'totalHeal': challengerTotalHeal, + 'criticals': challengerCriticals, + 'blocks': challengerBlocks, + 'evades': challengerEvades, + 'skillsUsed': challengerSkillsUsed, + }, + 'opponent': { + 'totalDamage': opponentTotalDamage, + 'totalHeal': opponentTotalHeal, + 'criticals': opponentCriticals, + 'blocks': opponentBlocks, + 'evades': opponentEvades, + 'skillsUsed': opponentSkillsUsed, + }, + }; + } + + /// 메시지에서 숫자 추출 + int _extractNumber(String msg) { + final match = RegExp(r'(\d+)').firstMatch(msg); + return match != null ? int.tryParse(match.group(1)!) ?? 0 : 0; + } + @override Widget build(BuildContext context) { final isVictory = widget.result.isVictory; diff --git a/lib/src/features/game/widgets/ascii_animation_card.dart b/lib/src/features/game/widgets/ascii_animation_card.dart index 68c59f4..cf0f0c1 100644 --- a/lib/src/features/game/widgets/ascii_animation_card.dart +++ b/lib/src/features/game/widgets/ascii_animation_card.dart @@ -226,13 +226,11 @@ class _AsciiAnimationCardState extends State { setState(() { _showDeathAnimation = true; }); - return; // 사망 애니메이션 중에는 다른 업데이트 무시 + // 분해 애니메이션은 오버레이로 표시되므로 + // 백그라운드 상태 업데이트는 계속 진행 (20배속 대응) } } - // 사망 애니메이션 중에는 다른 업데이트 무시 - if (_showDeathAnimation) return; - // 전투 이벤트 동기화 (Phase 5) if (widget.latestCombatEvent != null && widget.latestCombatEvent!.timestamp != _lastEventTimestamp) { @@ -253,7 +251,8 @@ class _AsciiAnimationCardState extends State { oldWidget.weaponRarity != widget.weaponRarity || oldWidget.opponentRaceId != widget.opponentRaceId || oldWidget.opponentHasShield != widget.opponentHasShield || - oldWidget.isInCombat != widget.isInCombat) { + oldWidget.isInCombat != widget.isInCombat || + oldWidget.monsterDied != widget.monsterDied) { _updateAnimation(); } }