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

@@ -18,7 +18,6 @@ import 'package:asciineverdie/src/shared/retro_colors.dart';
// 임시 문자열 (추후 l10n으로 이동)
const _battleTitle = 'ARENA BATTLE';
const _hpLabel = 'HP';
const _turnLabel = 'TURN';
/// 아레나 전투 화면
///
@@ -48,6 +47,9 @@ class _ArenaBattleScreenState extends State<ArenaBattleScreen>
/// 현재 턴
int _currentTurn = 0;
/// 전투 시작 시간 (경과 시간 계산용)
DateTime? _battleStartTime;
/// 도전자 HP/MP
late int _challengerHp;
late int _challengerHpMax;
@@ -114,7 +116,10 @@ class _ArenaBattleScreenState extends State<ArenaBattleScreen>
vsync: this,
);
_challengerFlashAnimation = Tween<double>(begin: 1.0, end: 0.0).animate(
CurvedAnimation(parent: _challengerFlashController, curve: Curves.easeOut),
CurvedAnimation(
parent: _challengerFlashController,
curve: Curves.easeOut,
),
);
_opponentFlashController = AnimationController(
@@ -139,14 +144,17 @@ class _ArenaBattleScreenState extends State<ArenaBattleScreen>
}
void _startBattle() {
_combatSubscription = _arenaService.simulateCombat(widget.match).listen(
(turn) {
_processTurn(turn);
},
onDone: () {
_endBattle();
},
);
_battleStartTime = DateTime.now();
_combatSubscription = _arenaService
.simulateCombat(widget.match)
.listen(
(turn) {
_processTurn(turn);
},
onDone: () {
_endBattle();
},
);
}
void _processTurn(ArenaCombatTurn turn) {
@@ -174,22 +182,28 @@ class _ArenaBattleScreenState extends State<ArenaBattleScreen>
// 도전자 스킬 사용 로그
if (turn.challengerSkillUsed != null) {
_battleLog.add(CombatLogEntry(
message: '${widget.match.challenger.characterName} uses '
'${turn.challengerSkillUsed}!',
timestamp: DateTime.now(),
type: CombatLogType.skill,
));
_battleLog.add(
CombatLogEntry(
message:
'${widget.match.challenger.characterName} uses '
'${turn.challengerSkillUsed}!',
timestamp: DateTime.now(),
type: CombatLogType.skill,
),
);
}
// 도전자 회복 로그
if (turn.challengerHealAmount != null && turn.challengerHealAmount! > 0) {
_battleLog.add(CombatLogEntry(
message: '${widget.match.challenger.characterName} heals '
'${turn.challengerHealAmount} HP!',
timestamp: DateTime.now(),
type: CombatLogType.heal,
));
_battleLog.add(
CombatLogEntry(
message:
'${widget.match.challenger.characterName} heals '
'${turn.challengerHealAmount} HP!',
timestamp: DateTime.now(),
type: CombatLogType.heal,
),
);
}
// 로그 추가 (CombatLogEntry 사용)
@@ -199,48 +213,61 @@ class _ArenaBattleScreenState extends State<ArenaBattleScreen>
: CombatLogType.damage;
final critText = turn.isChallengerCritical ? ' CRITICAL!' : '';
final skillText = turn.challengerSkillUsed != null ? '' : '';
_battleLog.add(CombatLogEntry(
message: '${widget.match.challenger.characterName} deals '
'${turn.challengerDamage}$critText$skillText',
timestamp: DateTime.now(),
type: type,
));
_battleLog.add(
CombatLogEntry(
message:
'${widget.match.challenger.characterName} deals '
'${turn.challengerDamage}$critText$skillText',
timestamp: DateTime.now(),
type: type,
),
);
}
// 상대 회피/블록 이벤트
if (turn.isOpponentEvaded) {
_battleLog.add(CombatLogEntry(
message: '${widget.match.opponent.characterName} evaded!',
timestamp: DateTime.now(),
type: CombatLogType.evade,
));
_battleLog.add(
CombatLogEntry(
message: '${widget.match.opponent.characterName} evaded!',
timestamp: DateTime.now(),
type: CombatLogType.evade,
),
);
}
if (turn.isOpponentBlocked) {
_battleLog.add(CombatLogEntry(
message: '${widget.match.opponent.characterName} blocked!',
timestamp: DateTime.now(),
type: CombatLogType.block,
));
_battleLog.add(
CombatLogEntry(
message: '${widget.match.opponent.characterName} blocked!',
timestamp: DateTime.now(),
type: CombatLogType.block,
),
);
}
// 상대 스킬 사용 로그
if (turn.opponentSkillUsed != null) {
_battleLog.add(CombatLogEntry(
message: '${widget.match.opponent.characterName} uses '
'${turn.opponentSkillUsed}!',
timestamp: DateTime.now(),
type: CombatLogType.skill,
));
_battleLog.add(
CombatLogEntry(
message:
'${widget.match.opponent.characterName} uses '
'${turn.opponentSkillUsed}!',
timestamp: DateTime.now(),
type: CombatLogType.skill,
),
);
}
// 상대 회복 로그
if (turn.opponentHealAmount != null && turn.opponentHealAmount! > 0) {
_battleLog.add(CombatLogEntry(
message: '${widget.match.opponent.characterName} heals '
'${turn.opponentHealAmount} HP!',
timestamp: DateTime.now(),
type: CombatLogType.heal,
));
_battleLog.add(
CombatLogEntry(
message:
'${widget.match.opponent.characterName} heals '
'${turn.opponentHealAmount} HP!',
timestamp: DateTime.now(),
type: CombatLogType.heal,
),
);
}
if (turn.opponentDamage != null) {
@@ -248,28 +275,35 @@ class _ArenaBattleScreenState extends State<ArenaBattleScreen>
? CombatLogType.critical
: CombatLogType.monsterAttack;
final critText = turn.isOpponentCritical ? ' CRITICAL!' : '';
_battleLog.add(CombatLogEntry(
message: '${widget.match.opponent.characterName} deals '
'${turn.opponentDamage}$critText',
timestamp: DateTime.now(),
type: type,
));
_battleLog.add(
CombatLogEntry(
message:
'${widget.match.opponent.characterName} deals '
'${turn.opponentDamage}$critText',
timestamp: DateTime.now(),
type: type,
),
);
}
// 도전자 회피/블록 이벤트
if (turn.isChallengerEvaded) {
_battleLog.add(CombatLogEntry(
message: '${widget.match.challenger.characterName} evaded!',
timestamp: DateTime.now(),
type: CombatLogType.evade,
));
_battleLog.add(
CombatLogEntry(
message: '${widget.match.challenger.characterName} evaded!',
timestamp: DateTime.now(),
type: CombatLogType.evade,
),
);
}
if (turn.isChallengerBlocked) {
_battleLog.add(CombatLogEntry(
message: '${widget.match.challenger.characterName} blocked!',
timestamp: DateTime.now(),
type: CombatLogType.block,
));
_battleLog.add(
CombatLogEntry(
message: '${widget.match.challenger.characterName} blocked!',
timestamp: DateTime.now(),
type: CombatLogType.block,
),
);
}
// 전투 이벤트 생성 (테두리 이펙트용)
@@ -405,10 +439,7 @@ class _ArenaBattleScreenState extends State<ArenaBattleScreen>
appBar: AppBar(
title: Text(
_battleTitle,
style: const TextStyle(
fontFamily: 'PressStart2P',
fontSize: 12,
),
style: const TextStyle(fontFamily: 'PressStart2P', fontSize: 12),
),
centerTitle: true,
backgroundColor: RetroColors.panelBgOf(context),
@@ -433,6 +464,7 @@ class _ArenaBattleScreenState extends State<ArenaBattleScreen>
result: _result!,
turnCount: _currentTurn,
onContinue: _handleContinue,
battleLog: _battleLog,
),
],
),
@@ -463,9 +495,9 @@ class _ArenaBattleScreenState extends State<ArenaBattleScreen>
height: 120,
child: AsciiAnimationCard(
taskType: TaskType.kill,
raceId: widget.match.challenger.race,
raceId: widget.match.challenger.raceId,
shieldName: _hasShield(widget.match.challenger) ? 'shield' : null,
opponentRaceId: widget.match.opponent.race,
opponentRaceId: widget.match.opponent.raceId,
opponentHasShield: _hasShield(widget.match.opponent),
latestCombatEvent: _latestCombatEvent,
),
@@ -476,18 +508,22 @@ class _ArenaBattleScreenState extends State<ArenaBattleScreen>
/// 종료된 전투 영역 (승자 유지 + 패자 분해)
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;
final winnerRaceId = isVictory
? widget.match.challenger.raceId
: widget.match.opponent.raceId;
final loserRaceId = isVictory
? widget.match.opponent.raceId
: widget.match.challenger.raceId;
// 패자 캐릭터 프레임 (idle 첫 프레임)
final loserFrameData = RaceCharacterFrames.get(loserRaceId) ??
final loserFrameData =
RaceCharacterFrames.get(loserRaceId) ??
RaceCharacterFrames.defaultFrames;
final loserLines = loserFrameData.idle.first.lines;
// 승자 캐릭터 프레임 (idle 첫 프레임)
final winnerFrameData = RaceCharacterFrames.get(winnerRaceId) ??
final winnerFrameData =
RaceCharacterFrames.get(winnerRaceId) ??
RaceCharacterFrames.defaultFrames;
final winnerLines = winnerFrameData.idle.first.lines;
@@ -522,10 +558,7 @@ class _ArenaBattleScreenState extends State<ArenaBattleScreen>
child: Center(
child: isVictory
? AsciiDisintegrateWidget(characterLines: loserLines)
: _buildStaticCharacter(
_mirrorLines(winnerLines),
false,
),
: _buildStaticCharacter(_mirrorLines(winnerLines), false),
),
),
],
@@ -540,15 +573,17 @@ class _ArenaBattleScreenState extends State<ArenaBattleScreen>
return Column(
mainAxisSize: MainAxisSize.min,
children: lines
.map((line) => Text(
line,
style: TextStyle(
fontFamily: 'JetBrainsMono',
fontSize: 10,
color: textColor,
height: 1.2,
),
))
.map(
(line) => Text(
line,
style: TextStyle(
fontFamily: 'JetBrainsMono',
fontSize: 10,
color: textColor,
height: 1.2,
),
),
)
.toList(),
);
}
@@ -583,20 +618,26 @@ class _ArenaBattleScreenState extends State<ArenaBattleScreen>
}
Widget _buildTurnIndicator() {
// 경과 시간 계산 (분:초 형식)
String elapsedTime = '00:00';
if (_battleStartTime != null) {
final elapsed = DateTime.now().difference(_battleStartTime!);
final minutes = elapsed.inMinutes;
final seconds = elapsed.inSeconds % 60;
elapsedTime =
'${minutes.toString().padLeft(2, '0')}:${seconds.toString().padLeft(2, '0')}';
}
return Container(
padding: const EdgeInsets.symmetric(vertical: 8),
color: RetroColors.panelBgOf(context).withValues(alpha: 0.5),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.sports_kabaddi,
color: RetroColors.goldOf(context),
size: 16,
),
Icon(Icons.timer, color: RetroColors.goldOf(context), size: 16),
const SizedBox(width: 8),
Text(
'$_turnLabel $_currentTurn',
elapsedTime,
style: TextStyle(
fontFamily: 'PressStart2P',
fontSize: 10,
@@ -615,10 +656,7 @@ class _ArenaBattleScreenState extends State<ArenaBattleScreen>
decoration: BoxDecoration(
color: RetroColors.panelBgOf(context),
border: Border(
bottom: BorderSide(
color: RetroColors.borderOf(context),
width: 2,
),
bottom: BorderSide(color: RetroColors.borderOf(context), width: 2),
),
),
child: Row(
@@ -688,7 +726,9 @@ class _ArenaBattleScreenState extends State<ArenaBattleScreen>
final isDamage = hpChange < 0;
final flashColor = isDamage
? RetroColors.hpRed.withValues(alpha: flashAnimation.value * 0.4)
: RetroColors.expGreen.withValues(alpha: flashAnimation.value * 0.4);
: RetroColors.expGreen.withValues(
alpha: flashAnimation.value * 0.4,
);
return Container(
padding: const EdgeInsets.all(6),
@@ -703,8 +743,9 @@ class _ArenaBattleScreenState extends State<ArenaBattleScreen>
clipBehavior: Clip.none,
children: [
Column(
crossAxisAlignment:
isReversed ? CrossAxisAlignment.end : CrossAxisAlignment.start,
crossAxisAlignment: isReversed
? CrossAxisAlignment.end
: CrossAxisAlignment.start,
children: [
// 이름
Text(
@@ -728,8 +769,9 @@ class _ArenaBattleScreenState extends State<ArenaBattleScreen>
const SizedBox(height: 2),
// HP 수치
Row(
mainAxisAlignment:
isReversed ? MainAxisAlignment.end : MainAxisAlignment.start,
mainAxisAlignment: isReversed
? MainAxisAlignment.end
: MainAxisAlignment.start,
children: [
Text(
_hpLabel,
@@ -769,7 +811,9 @@ class _ArenaBattleScreenState extends State<ArenaBattleScreen>
fontFamily: 'PressStart2P',
fontSize: 8,
fontWeight: FontWeight.bold,
color: isDamage ? RetroColors.hpRed : RetroColors.expGreen,
color: isDamage
? RetroColors.hpRed
: RetroColors.expGreen,
shadows: const [
Shadow(color: Colors.black, blurRadius: 3),
Shadow(color: Colors.black, blurRadius: 6),
@@ -811,7 +855,9 @@ class _ArenaBattleScreenState extends State<ArenaBattleScreen>
border: Border(
right: index < segmentCount - 1
? BorderSide(
color: RetroColors.borderOf(context).withValues(alpha: 0.3),
color: RetroColors.borderOf(
context,
).withValues(alpha: 0.3),
width: 1,
)
: BorderSide.none,
@@ -823,14 +869,9 @@ class _ArenaBattleScreenState extends State<ArenaBattleScreen>
return Container(
decoration: BoxDecoration(
border: Border.all(
color: RetroColors.borderOf(context),
width: 1,
),
),
child: Row(
children: isReversed ? segments.reversed.toList() : segments,
border: Border.all(color: RetroColors.borderOf(context), width: 1),
),
child: Row(children: isReversed ? segments.reversed.toList() : segments),
);
}
@@ -852,7 +893,8 @@ class _ArenaBattleScreenState extends State<ArenaBattleScreen>
/// 스킬 사용, 크리티컬, 블록, 회피 표시
Widget _buildCombatEventIcons() {
// 스킬 사용 또는 특수 액션만 표시
final hasSpecialEvent = _currentSkillName != null ||
final hasSpecialEvent =
_currentSkillName != null ||
_latestCombatEvent?.isCritical == true ||
_currentEventIcon == CombatEventType.playerBlock ||
_currentEventIcon == CombatEventType.playerEvade ||