refactor(arena): 아레나 화면 및 위젯 정리

This commit is contained in:
JiWoong Sul
2026-01-12 16:17:16 +09:00
parent a404c82f35
commit 104d23cdfd
8 changed files with 336 additions and 230 deletions

View File

@@ -1,4 +1,9 @@
import 'dart:convert';
import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:path_provider/path_provider.dart';
import 'package:asciineverdie/data/game_text_l10n.dart' as l10n;
import 'package:asciineverdie/src/core/engine/item_service.dart';
@@ -6,6 +11,7 @@ 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/item_stats.dart';
import 'package:asciineverdie/src/features/game/widgets/combat_log.dart';
import 'package:asciineverdie/src/shared/retro_colors.dart';
// 임시 문자열
@@ -23,6 +29,7 @@ class ArenaResultPanel extends StatefulWidget {
required this.result,
required this.turnCount,
required this.onContinue,
this.battleLog,
});
/// 대전 결과
@@ -34,6 +41,9 @@ class ArenaResultPanel extends StatefulWidget {
/// Continue 콜백
final VoidCallback onContinue;
/// 배틀 로그 (디버그 모드 저장용)
final List<CombatLogEntry>? battleLog;
@override
State<ArenaResultPanel> createState() => _ArenaResultPanelState();
}
@@ -52,21 +62,18 @@ class _ArenaResultPanelState extends State<ArenaResultPanel>
vsync: this,
);
_slideAnimation = Tween<Offset>(
begin: const Offset(0, 1), // 아래에서 위로
end: Offset.zero,
).animate(CurvedAnimation(
parent: _slideController,
curve: Curves.easeOutCubic,
));
_slideAnimation =
Tween<Offset>(
begin: const Offset(0, 1), // 아래에서 위로
end: Offset.zero,
).animate(
CurvedAnimation(parent: _slideController, curve: Curves.easeOutCubic),
);
_fadeAnimation = Tween<double>(
begin: 0.0,
end: 1.0,
).animate(CurvedAnimation(
parent: _slideController,
curve: Curves.easeOut,
));
).animate(CurvedAnimation(parent: _slideController, curve: Curves.easeOut));
// 약간 지연 후 애니메이션 시작 (분해 애니메이션과 동기화)
Future.delayed(const Duration(milliseconds: 800), () {
@@ -82,6 +89,63 @@ class _ArenaResultPanelState extends State<ArenaResultPanel>
super.dispose();
}
/// 배틀 로그 JSON 저장 (macOS 디버그 모드 전용)
Future<void> _saveBattleLog() async {
if (widget.battleLog == null || widget.battleLog!.isEmpty) return;
try {
// macOS: Downloads 폴더에 저장 (사용자가 쉽게 찾을 수 있도록)
final directory = await getDownloadsDirectory() ??
await getApplicationDocumentsDirectory();
final timestamp = DateTime.now().toIso8601String().replaceAll(':', '-');
final challenger = widget.result.match.challenger.characterName;
final opponent = widget.result.match.opponent.characterName;
final fileName = 'arena_${challenger}_vs_${opponent}_$timestamp.json';
final file = File('${directory.path}/$fileName');
final jsonData = {
'match': {
'challenger': challenger,
'opponent': opponent,
'isVictory': widget.result.isVictory,
'turnCount': widget.turnCount,
'timestamp': DateTime.now().toIso8601String(),
},
'battleLog': widget.battleLog!.map((e) => e.toJson()).toList(),
};
await file.writeAsString(
const JsonEncoder.withIndent(' ').convert(jsonData),
);
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
'${l10n.uiSaved}: $fileName',
style: const TextStyle(fontFamily: 'PressStart2P', fontSize: 6),
),
backgroundColor: RetroColors.mpOf(context),
duration: const Duration(seconds: 3),
),
);
}
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
'${l10n.uiError}: $e',
style: const TextStyle(fontFamily: 'PressStart2P', fontSize: 6),
),
backgroundColor: RetroColors.hpOf(context),
duration: const Duration(seconds: 3),
),
);
}
}
}
@override
Widget build(BuildContext context) {
final isVictory = widget.result.isVictory;
@@ -137,6 +201,13 @@ class _ArenaResultPanelState extends State<ArenaResultPanel>
// 장비 교환
_buildExchangeSection(context),
const SizedBox(height: 12),
// 배틀로그 저장 버튼 (macOS 디버그 모드 전용)
if (kDebugMode &&
Platform.isMacOS &&
widget.battleLog != null) ...[
_buildSaveLogButton(context),
const SizedBox(height: 8),
],
// Continue 버튼
_buildContinueButton(context, resultColor),
],
@@ -262,10 +333,12 @@ class _ArenaResultPanelState extends State<ArenaResultPanel>
slot,
);
final oldScore =
oldItem != null ? ItemService.calculateEquipmentScore(oldItem) : 0;
final newScore =
newItem != null ? ItemService.calculateEquipmentScore(newItem) : 0;
final oldScore = oldItem != null
? ItemService.calculateEquipmentScore(oldItem)
: 0;
final newScore = newItem != null
? ItemService.calculateEquipmentScore(newItem)
: 0;
final scoreDiff = newScore - oldScore;
return Container(
@@ -344,7 +417,9 @@ class _ArenaResultPanelState extends State<ArenaResultPanel>
mainAxisSize: MainAxisSize.min,
children: [
Icon(
scoreDiff >= 0 ? Icons.arrow_upward : Icons.arrow_downward,
scoreDiff >= 0
? Icons.arrow_upward
: Icons.arrow_downward,
size: 10,
color: scoreDiff >= 0 ? Colors.green : Colors.red,
),
@@ -366,11 +441,7 @@ class _ArenaResultPanelState extends State<ArenaResultPanel>
);
}
Widget _buildItemBadge(
BuildContext context,
EquipmentItem? item,
int score,
) {
Widget _buildItemBadge(BuildContext context, EquipmentItem? item, int score) {
if (item == null || item.isEmpty) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 4),
@@ -433,9 +504,7 @@ class _ArenaResultPanelState extends State<ArenaResultPanel>
style: FilledButton.styleFrom(
backgroundColor: color,
padding: const EdgeInsets.symmetric(vertical: 10),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(6),
),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(6)),
),
child: Text(
l10n.buttonConfirm,
@@ -449,6 +518,27 @@ class _ArenaResultPanelState extends State<ArenaResultPanel>
);
}
/// 배틀로그 저장 버튼 (macOS 디버그 모드 전용)
Widget _buildSaveLogButton(BuildContext context) {
return SizedBox(
width: double.infinity,
child: OutlinedButton.icon(
onPressed: _saveBattleLog,
icon: const Icon(Icons.save_alt, size: 14),
label: Text(
l10n.uiSaveBattleLog,
style: const TextStyle(fontFamily: 'PressStart2P', fontSize: 6),
),
style: OutlinedButton.styleFrom(
foregroundColor: RetroColors.mpOf(context),
side: BorderSide(color: RetroColors.mpOf(context)),
padding: const EdgeInsets.symmetric(vertical: 8),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(6)),
),
),
);
}
EquipmentItem? _findItem(List<EquipmentItem>? equipment, EquipmentSlot slot) {
if (equipment == null) return null;
for (final item in equipment) {