feat(arena): 결과 패널 및 소멸 애니메이션 위젯 추가

- ArenaResultPanel: 전투 결과 표시 패널
- AsciiDisintegrateWidget: ASCII 소멸 애니메이션 효과
- ArenaBattleScreen 개선
This commit is contained in:
JiWoong Sul
2026-01-06 18:57:29 +09:00
parent 8d51263b2e
commit f18f3ceaee
3 changed files with 856 additions and 27 deletions

View File

@@ -7,7 +7,9 @@ import 'package:asciineverdie/src/core/model/arena_match.dart';
import 'package:asciineverdie/src/core/model/combat_event.dart';
import 'package:asciineverdie/src/core/model/game_state.dart';
import 'package:asciineverdie/src/core/model/hall_of_fame.dart';
import 'package:asciineverdie/src/features/arena/widgets/arena_result_dialog.dart';
import 'package:asciineverdie/src/core/animation/race_character_frames.dart';
import 'package:asciineverdie/src/features/arena/widgets/arena_result_panel.dart';
import 'package:asciineverdie/src/features/arena/widgets/ascii_disintegrate_widget.dart';
import 'package:asciineverdie/src/features/game/widgets/ascii_animation_card.dart';
import 'package:asciineverdie/src/features/game/widgets/combat_log.dart';
import 'package:asciineverdie/src/shared/retro_colors.dart';
@@ -88,6 +90,9 @@ class _ArenaBattleScreenState extends State<ArenaBattleScreen>
/// 현재 표시 중인 스킬 이름
String? _currentSkillName;
/// 전투 종료 여부 (결과 패널 표시용)
bool _isFinished = false;
@override
void initState() {
super.initState();
@@ -374,20 +379,19 @@ class _ArenaBattleScreenState extends State<ArenaBattleScreen>
// 최종 결과 계산
_result = _arenaService.executeCombat(widget.match);
// 결과 다이얼로그 표시
Future.delayed(const Duration(milliseconds: 500), () {
if (mounted && _result != null) {
showArenaResultDialog(
context,
result: _result!,
onClose: () {
widget.onBattleComplete(_result!);
},
);
}
// 전투 종료 상태로 전환 (인라인 결과 패널 표시)
setState(() {
_isFinished = true;
});
}
/// Continue 버튼 콜백
void _handleContinue() {
if (_result != null) {
widget.onBattleComplete(_result!);
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
@@ -413,23 +417,17 @@ class _ArenaBattleScreenState extends State<ArenaBattleScreen>
_buildRetroHpBars(),
// 전투 이벤트 아이콘 (HP 바와 애니메이션 사이)
_buildCombatEventIcons(),
// ASCII 애니메이션 (중앙) - 기존 AsciiAnimationCard 재사용
Padding(
padding: const EdgeInsets.symmetric(horizontal: 8),
child: SizedBox(
height: 120,
child: AsciiAnimationCard(
taskType: TaskType.kill,
raceId: widget.match.challenger.race,
shieldName: _hasShield(widget.match.challenger) ? 'shield' : null,
opponentRaceId: widget.match.opponent.race,
opponentHasShield: _hasShield(widget.match.opponent),
latestCombatEvent: _latestCombatEvent,
),
),
),
// ASCII 애니메이션 (전투 중 / 종료 분기)
_buildBattleArea(),
// 로그 영역 (남은 공간 채움)
Expanded(child: _buildBattleLog()),
// 결과 패널 (전투 종료 시)
if (_isFinished && _result != null)
ArenaResultPanel(
result: _result!,
turnCount: _currentTurn,
onContinue: _handleContinue,
),
],
),
),
@@ -443,6 +441,141 @@ class _ArenaBattleScreenState extends State<ArenaBattleScreen>
return equipment.any((item) => item.slot.name == 'shield');
}
/// 전투 영역 (전투 중 / 종료 분기)
Widget _buildBattleArea() {
if (_isFinished && _result != null) {
return _buildFinishedBattleArea();
}
return _buildActiveBattleArea();
}
/// 활성 전투 영역 (기존 AsciiAnimationCard)
Widget _buildActiveBattleArea() {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 8),
child: SizedBox(
height: 120,
child: AsciiAnimationCard(
taskType: TaskType.kill,
raceId: widget.match.challenger.race,
shieldName: _hasShield(widget.match.challenger) ? 'shield' : null,
opponentRaceId: widget.match.opponent.race,
opponentHasShield: _hasShield(widget.match.opponent),
latestCombatEvent: _latestCombatEvent,
),
),
);
}
/// 종료된 전투 영역 (승자 유지 + 패자 분해)
Widget _buildFinishedBattleArea() {
final isVictory = _result!.isVictory;
final winnerRaceId =
isVictory ? widget.match.challenger.race : widget.match.opponent.race;
final loserRaceId =
isVictory ? widget.match.opponent.race : widget.match.challenger.race;
// 패자 캐릭터 프레임 (idle 첫 프레임)
final loserFrameData = RaceCharacterFrames.get(loserRaceId) ??
RaceCharacterFrames.defaultFrames;
final loserLines = loserFrameData.idle.first.lines;
// 승자 캐릭터 프레임 (idle 첫 프레임)
final winnerFrameData = RaceCharacterFrames.get(winnerRaceId) ??
RaceCharacterFrames.defaultFrames;
final winnerLines = winnerFrameData.idle.first.lines;
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 8),
child: SizedBox(
height: 120,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
// 좌측: 도전자 (승자면 유지, 패자면 분해)
Expanded(
child: Center(
child: isVictory
? _buildStaticCharacter(winnerLines, false)
: AsciiDisintegrateWidget(
characterLines: _mirrorLines(loserLines),
),
),
),
// 중앙 VS
Text(
'VS',
style: TextStyle(
fontFamily: 'PressStart2P',
fontSize: 10,
color: RetroColors.goldOf(context).withValues(alpha: 0.5),
),
),
// 우측: 상대 (승자면 유지, 패자면 분해)
Expanded(
child: Center(
child: isVictory
? AsciiDisintegrateWidget(characterLines: loserLines)
: _buildStaticCharacter(
_mirrorLines(winnerLines),
false,
),
),
),
],
),
),
);
}
/// 정적 ASCII 캐릭터 표시
Widget _buildStaticCharacter(List<String> lines, bool mirrored) {
final textColor = RetroColors.textPrimaryOf(context);
return Column(
mainAxisSize: MainAxisSize.min,
children: lines
.map((line) => Text(
line,
style: TextStyle(
fontFamily: 'JetBrainsMono',
fontSize: 10,
color: textColor,
height: 1.2,
),
))
.toList(),
);
}
/// ASCII 문자열 미러링 (좌우 대칭)
List<String> _mirrorLines(List<String> lines) {
return lines.map((line) {
final chars = line.split('');
return chars.reversed.map(_mirrorChar).join();
}).toList();
}
/// 개별 문자 미러링
String _mirrorChar(String char) {
return switch (char) {
'/' => r'\',
r'\' => '/',
'(' => ')',
')' => '(',
'[' => ']',
']' => '[',
'{' => '}',
'}' => '{',
'<' => '>',
'>' => '<',
'd' => 'b',
'b' => 'd',
'q' => 'p',
'p' => 'q',
_ => char,
};
}
Widget _buildTurnIndicator() {
return Container(
padding: const EdgeInsets.symmetric(vertical: 8),