refactor(arena): arena_battle_screen 759줄 → 155줄 분할
- arena_battle_controller.dart: 전투 로직 (스트림, 턴, 로그) - arena_battle_area.dart: 전투 영역 위젯 - arena_turn_indicator.dart: 경과 시간 위젯 - arena_combat_event_icons.dart: 전투 이벤트 아이콘 위젯
This commit is contained in:
403
lib/src/features/arena/arena_battle_controller.dart
Normal file
403
lib/src/features/arena/arena_battle_controller.dart
Normal file
@@ -0,0 +1,403 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:asciineverdie/src/core/engine/arena_service.dart';
|
||||||
|
import 'package:asciineverdie/src/core/model/arena_match.dart';
|
||||||
|
import 'package:asciineverdie/src/core/model/combat_event.dart';
|
||||||
|
import 'package:asciineverdie/src/features/game/widgets/combat_log.dart';
|
||||||
|
|
||||||
|
/// 아레나 전투 상태 (Arena Battle State)
|
||||||
|
///
|
||||||
|
/// 컨트롤러가 관리하는 전투 상태 스냅샷
|
||||||
|
class ArenaBattleState {
|
||||||
|
ArenaBattleState({
|
||||||
|
required this.currentTurn,
|
||||||
|
required this.challengerHp,
|
||||||
|
required this.challengerHpMax,
|
||||||
|
required this.challengerMp,
|
||||||
|
required this.challengerMpMax,
|
||||||
|
required this.opponentHp,
|
||||||
|
required this.opponentHpMax,
|
||||||
|
required this.opponentMp,
|
||||||
|
required this.opponentMpMax,
|
||||||
|
required this.battleLog,
|
||||||
|
required this.isFinished,
|
||||||
|
this.result,
|
||||||
|
this.latestCombatEvent,
|
||||||
|
this.currentEventIcon,
|
||||||
|
this.currentSkillName,
|
||||||
|
this.challengerHpChange = 0,
|
||||||
|
this.opponentHpChange = 0,
|
||||||
|
this.battleStartTime,
|
||||||
|
});
|
||||||
|
|
||||||
|
final int currentTurn;
|
||||||
|
final int challengerHp;
|
||||||
|
final int challengerHpMax;
|
||||||
|
final int challengerMp;
|
||||||
|
final int challengerMpMax;
|
||||||
|
final int opponentHp;
|
||||||
|
final int opponentHpMax;
|
||||||
|
final int opponentMp;
|
||||||
|
final int opponentMpMax;
|
||||||
|
final List<CombatLogEntry> battleLog;
|
||||||
|
final bool isFinished;
|
||||||
|
final ArenaMatchResult? result;
|
||||||
|
final CombatEvent? latestCombatEvent;
|
||||||
|
final CombatEventType? currentEventIcon;
|
||||||
|
final String? currentSkillName;
|
||||||
|
final int challengerHpChange;
|
||||||
|
final int opponentHpChange;
|
||||||
|
final DateTime? battleStartTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 아레나 전투 컨트롤러 (Arena Battle Controller)
|
||||||
|
///
|
||||||
|
/// 전투 시뮬레이션 스트림 구독, 턴 처리, 로그 생성을 담당
|
||||||
|
class ArenaBattleController {
|
||||||
|
ArenaBattleController({required this.match});
|
||||||
|
|
||||||
|
final ArenaMatch match;
|
||||||
|
final ArenaService _arenaService = ArenaService();
|
||||||
|
|
||||||
|
// 상태 (State)
|
||||||
|
int _currentTurn = 0;
|
||||||
|
DateTime? _battleStartTime;
|
||||||
|
late int _challengerHp;
|
||||||
|
late int _challengerHpMax;
|
||||||
|
late int _challengerMp;
|
||||||
|
late int _challengerMpMax;
|
||||||
|
late int _opponentHp;
|
||||||
|
late int _opponentHpMax;
|
||||||
|
late int _opponentMp;
|
||||||
|
late int _opponentMpMax;
|
||||||
|
final List<CombatLogEntry> _battleLog = [];
|
||||||
|
ArenaMatchResult? _result;
|
||||||
|
CombatEvent? _latestCombatEvent;
|
||||||
|
CombatEventType? _currentEventIcon;
|
||||||
|
String? _currentSkillName;
|
||||||
|
int _challengerHpChange = 0;
|
||||||
|
int _opponentHpChange = 0;
|
||||||
|
bool _isFinished = false;
|
||||||
|
|
||||||
|
StreamSubscription<ArenaCombatTurn>? _combatSubscription;
|
||||||
|
Timer? _eventIconTimer;
|
||||||
|
|
||||||
|
/// 상태 변경 콜백 (setState 대체)
|
||||||
|
void Function()? onStateChanged;
|
||||||
|
|
||||||
|
/// HP 변화 콜백 (애니메이션 트리거용)
|
||||||
|
/// challenger: true = 도전자, false = 상대
|
||||||
|
void Function(bool challenger)? onHpChanged;
|
||||||
|
|
||||||
|
/// 현재 상태 스냅샷
|
||||||
|
ArenaBattleState get state => ArenaBattleState(
|
||||||
|
currentTurn: _currentTurn,
|
||||||
|
challengerHp: _challengerHp,
|
||||||
|
challengerHpMax: _challengerHpMax,
|
||||||
|
challengerMp: _challengerMp,
|
||||||
|
challengerMpMax: _challengerMpMax,
|
||||||
|
opponentHp: _opponentHp,
|
||||||
|
opponentHpMax: _opponentHpMax,
|
||||||
|
opponentMp: _opponentMp,
|
||||||
|
opponentMpMax: _opponentMpMax,
|
||||||
|
battleLog: _battleLog,
|
||||||
|
isFinished: _isFinished,
|
||||||
|
result: _result,
|
||||||
|
latestCombatEvent: _latestCombatEvent,
|
||||||
|
currentEventIcon: _currentEventIcon,
|
||||||
|
currentSkillName: _currentSkillName,
|
||||||
|
challengerHpChange: _challengerHpChange,
|
||||||
|
opponentHpChange: _opponentHpChange,
|
||||||
|
battleStartTime: _battleStartTime,
|
||||||
|
);
|
||||||
|
|
||||||
|
/// HP/MP 초기화
|
||||||
|
void initialize() {
|
||||||
|
_challengerHpMax = match.challenger.finalStats?.hpMax ?? 100;
|
||||||
|
_challengerHp = _challengerHpMax;
|
||||||
|
_challengerMpMax = match.challenger.finalStats?.mpMax ?? 50;
|
||||||
|
_challengerMp = _challengerMpMax;
|
||||||
|
_opponentHpMax = match.opponent.finalStats?.hpMax ?? 100;
|
||||||
|
_opponentHp = _opponentHpMax;
|
||||||
|
_opponentMpMax = match.opponent.finalStats?.mpMax ?? 50;
|
||||||
|
_opponentMp = _opponentMpMax;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 전투 시작
|
||||||
|
void startBattle() {
|
||||||
|
_battleStartTime = DateTime.now();
|
||||||
|
_combatSubscription = _arenaService
|
||||||
|
.simulateCombat(match)
|
||||||
|
.listen(_processTurn, onDone: _endBattle);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 턴 처리 (Turn Processing)
|
||||||
|
void _processTurn(ArenaCombatTurn turn) {
|
||||||
|
final oldChallengerHp = _challengerHp;
|
||||||
|
final oldOpponentHp = _opponentHp;
|
||||||
|
|
||||||
|
_currentTurn++;
|
||||||
|
_challengerHp = turn.challengerHp;
|
||||||
|
_opponentHp = turn.opponentHp;
|
||||||
|
_challengerMp = turn.challengerMp ?? _challengerMp;
|
||||||
|
_opponentMp = turn.opponentMp ?? _opponentMp;
|
||||||
|
|
||||||
|
// 도전자 HP 변화 감지
|
||||||
|
if (oldChallengerHp != _challengerHp) {
|
||||||
|
_challengerHpChange = _challengerHp - oldChallengerHp;
|
||||||
|
onHpChanged?.call(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 상대 HP 변화 감지
|
||||||
|
if (oldOpponentHp != _opponentHp) {
|
||||||
|
_opponentHpChange = _opponentHp - oldOpponentHp;
|
||||||
|
onHpChanged?.call(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 전투 로그 생성
|
||||||
|
_addTurnLogs(turn);
|
||||||
|
|
||||||
|
// 전투 이벤트 생성 (테두리 이펙트용)
|
||||||
|
_latestCombatEvent = _createCombatEvent(turn);
|
||||||
|
|
||||||
|
// 전투 이벤트 아이콘 표시
|
||||||
|
_showEventIcon(turn);
|
||||||
|
|
||||||
|
onStateChanged?.call();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 턴 로그 생성 (Turn Log Generation)
|
||||||
|
void _addTurnLogs(ArenaCombatTurn turn) {
|
||||||
|
final challengerName = match.challenger.characterName;
|
||||||
|
final opponentName = match.opponent.characterName;
|
||||||
|
|
||||||
|
// 도전자 스킬 사용 로그
|
||||||
|
if (turn.challengerSkillUsed != null) {
|
||||||
|
_battleLog.add(
|
||||||
|
CombatLogEntry(
|
||||||
|
message: '$challengerName uses ${turn.challengerSkillUsed}!',
|
||||||
|
timestamp: DateTime.now(),
|
||||||
|
type: CombatLogType.skill,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 도전자 회복 로그
|
||||||
|
if (turn.challengerHealAmount != null && turn.challengerHealAmount! > 0) {
|
||||||
|
_battleLog.add(
|
||||||
|
CombatLogEntry(
|
||||||
|
message: '$challengerName heals ${turn.challengerHealAmount} HP!',
|
||||||
|
timestamp: DateTime.now(),
|
||||||
|
type: CombatLogType.heal,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 도전자 데미지 로그
|
||||||
|
if (turn.challengerDamage != null) {
|
||||||
|
final type = turn.isChallengerCritical
|
||||||
|
? CombatLogType.critical
|
||||||
|
: CombatLogType.damage;
|
||||||
|
final critText = turn.isChallengerCritical ? ' CRITICAL!' : '';
|
||||||
|
final skillText = turn.challengerSkillUsed != null ? '' : '';
|
||||||
|
_battleLog.add(
|
||||||
|
CombatLogEntry(
|
||||||
|
message:
|
||||||
|
'$challengerName deals ${turn.challengerDamage}'
|
||||||
|
'$critText$skillText',
|
||||||
|
timestamp: DateTime.now(),
|
||||||
|
type: type,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 상대 회피/블록 이벤트
|
||||||
|
if (turn.isOpponentEvaded) {
|
||||||
|
_battleLog.add(
|
||||||
|
CombatLogEntry(
|
||||||
|
message: '$opponentName evaded!',
|
||||||
|
timestamp: DateTime.now(),
|
||||||
|
type: CombatLogType.evade,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (turn.isOpponentBlocked) {
|
||||||
|
_battleLog.add(
|
||||||
|
CombatLogEntry(
|
||||||
|
message: '$opponentName blocked!',
|
||||||
|
timestamp: DateTime.now(),
|
||||||
|
type: CombatLogType.block,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 상대 스킬 사용 로그
|
||||||
|
if (turn.opponentSkillUsed != null) {
|
||||||
|
_battleLog.add(
|
||||||
|
CombatLogEntry(
|
||||||
|
message: '$opponentName uses ${turn.opponentSkillUsed}!',
|
||||||
|
timestamp: DateTime.now(),
|
||||||
|
type: CombatLogType.skill,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 상대 회복 로그
|
||||||
|
if (turn.opponentHealAmount != null && turn.opponentHealAmount! > 0) {
|
||||||
|
_battleLog.add(
|
||||||
|
CombatLogEntry(
|
||||||
|
message: '$opponentName heals ${turn.opponentHealAmount} HP!',
|
||||||
|
timestamp: DateTime.now(),
|
||||||
|
type: CombatLogType.heal,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 상대 데미지 로그
|
||||||
|
if (turn.opponentDamage != null) {
|
||||||
|
final type = turn.isOpponentCritical
|
||||||
|
? CombatLogType.critical
|
||||||
|
: CombatLogType.monsterAttack;
|
||||||
|
final critText = turn.isOpponentCritical ? ' CRITICAL!' : '';
|
||||||
|
_battleLog.add(
|
||||||
|
CombatLogEntry(
|
||||||
|
message: '$opponentName deals ${turn.opponentDamage}$critText',
|
||||||
|
timestamp: DateTime.now(),
|
||||||
|
type: type,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 도전자 회피/블록 이벤트
|
||||||
|
if (turn.isChallengerEvaded) {
|
||||||
|
_battleLog.add(
|
||||||
|
CombatLogEntry(
|
||||||
|
message: '$challengerName evaded!',
|
||||||
|
timestamp: DateTime.now(),
|
||||||
|
type: CombatLogType.evade,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (turn.isChallengerBlocked) {
|
||||||
|
_battleLog.add(
|
||||||
|
CombatLogEntry(
|
||||||
|
message: '$challengerName blocked!',
|
||||||
|
timestamp: DateTime.now(),
|
||||||
|
type: CombatLogType.block,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 전투 이벤트 아이콘 표시 (일정 시간 후 사라짐)
|
||||||
|
void _showEventIcon(ArenaCombatTurn turn) {
|
||||||
|
_eventIconTimer?.cancel();
|
||||||
|
|
||||||
|
_currentSkillName = turn.challengerSkillUsed ?? turn.opponentSkillUsed;
|
||||||
|
|
||||||
|
// 이벤트 타입 결정 (우선순위: 스킬 > 크리티컬 > 블록 > 회피 > 일반공격)
|
||||||
|
CombatEventType? eventType;
|
||||||
|
if (_currentSkillName != null) {
|
||||||
|
eventType = CombatEventType.playerSkill;
|
||||||
|
} else if (turn.isChallengerCritical || turn.isOpponentCritical) {
|
||||||
|
eventType = CombatEventType.playerAttack;
|
||||||
|
} else if (turn.isChallengerBlocked || turn.isOpponentBlocked) {
|
||||||
|
eventType = CombatEventType.playerBlock;
|
||||||
|
} else if (turn.isChallengerEvaded || turn.isOpponentEvaded) {
|
||||||
|
eventType = CombatEventType.playerEvade;
|
||||||
|
} else if (turn.challengerDamage != null || turn.opponentDamage != null) {
|
||||||
|
eventType = CombatEventType.playerAttack;
|
||||||
|
}
|
||||||
|
|
||||||
|
_currentEventIcon = eventType;
|
||||||
|
|
||||||
|
// 800ms 후 아이콘 숨김
|
||||||
|
_eventIconTimer = Timer(const Duration(milliseconds: 800), () {
|
||||||
|
_currentEventIcon = null;
|
||||||
|
_currentSkillName = null;
|
||||||
|
onStateChanged?.call();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// ArenaCombatTurn에서 CombatEvent 생성 (테두리 이펙트용)
|
||||||
|
CombatEvent? _createCombatEvent(ArenaCombatTurn turn) {
|
||||||
|
final timestamp = DateTime.now().millisecondsSinceEpoch;
|
||||||
|
final challengerName = match.challenger.characterName;
|
||||||
|
final opponentName = match.opponent.characterName;
|
||||||
|
|
||||||
|
// 도전자 스킬 사용 (보라색 테두리)
|
||||||
|
if (turn.challengerSkillUsed != null && turn.challengerDamage != null) {
|
||||||
|
return CombatEvent.playerSkill(
|
||||||
|
timestamp: timestamp,
|
||||||
|
skillName: turn.challengerSkillUsed!,
|
||||||
|
damage: turn.challengerDamage!,
|
||||||
|
targetName: opponentName,
|
||||||
|
isCritical: turn.isChallengerCritical,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 도전자 공격 이벤트
|
||||||
|
if (turn.challengerDamage != null) {
|
||||||
|
return CombatEvent.playerAttack(
|
||||||
|
timestamp: timestamp,
|
||||||
|
damage: turn.challengerDamage!,
|
||||||
|
targetName: opponentName,
|
||||||
|
isCritical: turn.isChallengerCritical,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 도전자 회복 이벤트
|
||||||
|
if (turn.challengerHealAmount != null && turn.challengerSkillUsed != null) {
|
||||||
|
return CombatEvent.playerHeal(
|
||||||
|
timestamp: timestamp,
|
||||||
|
healAmount: turn.challengerHealAmount!,
|
||||||
|
skillName: turn.challengerSkillUsed,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 도전자 방어 이벤트 (회피/블록)
|
||||||
|
if (turn.isChallengerEvaded) {
|
||||||
|
return CombatEvent.playerEvade(
|
||||||
|
timestamp: timestamp,
|
||||||
|
attackerName: opponentName,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (turn.isChallengerBlocked) {
|
||||||
|
return CombatEvent.playerBlock(
|
||||||
|
timestamp: timestamp,
|
||||||
|
reducedDamage: turn.opponentDamage ?? 0,
|
||||||
|
attackerName: opponentName,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 상대 공격 이벤트
|
||||||
|
if (turn.opponentDamage != null) {
|
||||||
|
return CombatEvent.monsterAttack(
|
||||||
|
timestamp: timestamp,
|
||||||
|
damage: turn.opponentDamage!,
|
||||||
|
attackerName: challengerName,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 전투 종료 처리
|
||||||
|
void _endBattle() {
|
||||||
|
_result = _arenaService.createResultFromSimulation(
|
||||||
|
match: match,
|
||||||
|
challengerHp: _challengerHp,
|
||||||
|
opponentHp: _opponentHp,
|
||||||
|
turns: _currentTurn,
|
||||||
|
);
|
||||||
|
|
||||||
|
_isFinished = true;
|
||||||
|
onStateChanged?.call();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 리소스 해제
|
||||||
|
void dispose() {
|
||||||
|
_combatSubscription?.cancel();
|
||||||
|
_eventIconTimer?.cancel();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,23 +1,17 @@
|
|||||||
import 'dart:async';
|
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
import 'package:asciineverdie/l10n/app_localizations.dart';
|
import 'package:asciineverdie/l10n/app_localizations.dart';
|
||||||
import 'package:asciineverdie/src/core/engine/arena_service.dart';
|
|
||||||
import 'package:asciineverdie/src/core/model/arena_match.dart';
|
import 'package:asciineverdie/src/core/model/arena_match.dart';
|
||||||
import 'package:asciineverdie/src/core/model/combat_event.dart';
|
import 'package:asciineverdie/src/features/arena/arena_battle_controller.dart';
|
||||||
import 'package:asciineverdie/src/core/model/game_state.dart';
|
import 'package:asciineverdie/src/features/arena/widgets/arena_battle_area.dart';
|
||||||
import 'package:asciineverdie/src/core/model/hall_of_fame.dart';
|
import 'package:asciineverdie/src/features/arena/widgets/arena_combat_event_icons.dart';
|
||||||
import 'package:asciineverdie/src/shared/animation/race_character_frames.dart';
|
|
||||||
import 'package:asciineverdie/src/features/arena/widgets/arena_combat_log.dart';
|
import 'package:asciineverdie/src/features/arena/widgets/arena_combat_log.dart';
|
||||||
import 'package:asciineverdie/src/features/arena/widgets/arena_hp_bar.dart';
|
import 'package:asciineverdie/src/features/arena/widgets/arena_hp_bar.dart';
|
||||||
import 'package:asciineverdie/src/features/arena/widgets/arena_result_panel.dart';
|
import 'package:asciineverdie/src/features/arena/widgets/arena_result_panel.dart';
|
||||||
import 'package:asciineverdie/src/shared/widgets/ascii_disintegrate_widget.dart';
|
import 'package:asciineverdie/src/features/arena/widgets/arena_turn_indicator.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';
|
import 'package:asciineverdie/src/shared/retro_colors.dart';
|
||||||
|
|
||||||
/// 아레나 전투 화면
|
/// 아레나 전투 화면 (Arena Battle Screen)
|
||||||
///
|
///
|
||||||
/// ASCII 애니메이션 기반 턴제 전투 표시
|
/// ASCII 애니메이션 기반 턴제 전투 표시
|
||||||
/// 레트로 RPG 스타일 HP 바 (세그먼트)
|
/// 레트로 RPG 스타일 HP 바 (세그먼트)
|
||||||
@@ -40,73 +34,25 @@ class ArenaBattleScreen extends StatefulWidget {
|
|||||||
|
|
||||||
class _ArenaBattleScreenState extends State<ArenaBattleScreen>
|
class _ArenaBattleScreenState extends State<ArenaBattleScreen>
|
||||||
with TickerProviderStateMixin {
|
with TickerProviderStateMixin {
|
||||||
final ArenaService _arenaService = ArenaService();
|
late final ArenaBattleController _controller;
|
||||||
|
|
||||||
/// 현재 턴
|
// HP 변화 애니메이션 (Animation)
|
||||||
int _currentTurn = 0;
|
|
||||||
|
|
||||||
/// 전투 시작 시간 (경과 시간 계산용)
|
|
||||||
DateTime? _battleStartTime;
|
|
||||||
|
|
||||||
/// 도전자 HP/MP
|
|
||||||
late int _challengerHp;
|
|
||||||
late int _challengerHpMax;
|
|
||||||
late int _challengerMp;
|
|
||||||
late int _challengerMpMax;
|
|
||||||
|
|
||||||
/// 상대 HP/MP
|
|
||||||
late int _opponentHp;
|
|
||||||
late int _opponentHpMax;
|
|
||||||
late int _opponentMp;
|
|
||||||
late int _opponentMpMax;
|
|
||||||
|
|
||||||
/// 전투 로그 (CombatLogEntry 사용)
|
|
||||||
final List<CombatLogEntry> _battleLog = [];
|
|
||||||
|
|
||||||
/// 전투 시뮬레이션 스트림 구독
|
|
||||||
StreamSubscription<ArenaCombatTurn>? _combatSubscription;
|
|
||||||
|
|
||||||
/// 최종 결과
|
|
||||||
ArenaMatchResult? _result;
|
|
||||||
|
|
||||||
// HP 변화 애니메이션
|
|
||||||
late AnimationController _challengerFlashController;
|
late AnimationController _challengerFlashController;
|
||||||
late AnimationController _opponentFlashController;
|
late AnimationController _opponentFlashController;
|
||||||
late Animation<double> _challengerFlashAnimation;
|
late Animation<double> _challengerFlashAnimation;
|
||||||
late Animation<double> _opponentFlashAnimation;
|
late Animation<double> _opponentFlashAnimation;
|
||||||
|
|
||||||
// 변화량 표시용
|
|
||||||
int _challengerHpChange = 0;
|
|
||||||
int _opponentHpChange = 0;
|
|
||||||
|
|
||||||
/// 최신 전투 이벤트 (테두리 이펙트용)
|
|
||||||
CombatEvent? _latestCombatEvent;
|
|
||||||
|
|
||||||
/// 전투 이벤트 아이콘 타이머 (페이드 아웃용)
|
|
||||||
Timer? _eventIconTimer;
|
|
||||||
|
|
||||||
/// 현재 표시 중인 이벤트 아이콘 타입
|
|
||||||
CombatEventType? _currentEventIcon;
|
|
||||||
|
|
||||||
/// 현재 표시 중인 스킬 이름
|
|
||||||
String? _currentSkillName;
|
|
||||||
|
|
||||||
/// 전투 종료 여부 (결과 패널 표시용)
|
|
||||||
bool _isFinished = false;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
|
|
||||||
// HP/MP 초기화
|
// 컨트롤러 초기화
|
||||||
_challengerHpMax = widget.match.challenger.finalStats?.hpMax ?? 100;
|
_controller = ArenaBattleController(match: widget.match);
|
||||||
_challengerHp = _challengerHpMax;
|
_controller.initialize();
|
||||||
_challengerMpMax = widget.match.challenger.finalStats?.mpMax ?? 50;
|
_controller.onStateChanged = () {
|
||||||
_challengerMp = _challengerMpMax;
|
if (mounted) setState(() {});
|
||||||
_opponentHpMax = widget.match.opponent.finalStats?.hpMax ?? 100;
|
};
|
||||||
_opponentHp = _opponentHpMax;
|
_controller.onHpChanged = _handleHpChanged;
|
||||||
_opponentMpMax = widget.match.opponent.finalStats?.mpMax ?? 50;
|
|
||||||
_opponentMp = _opponentMpMax;
|
|
||||||
|
|
||||||
// 플래시 애니메이션 초기화
|
// 플래시 애니메이션 초기화
|
||||||
_challengerFlashController = AnimationController(
|
_challengerFlashController = AnimationController(
|
||||||
@@ -129,309 +75,38 @@ class _ArenaBattleScreenState extends State<ArenaBattleScreen>
|
|||||||
);
|
);
|
||||||
|
|
||||||
// 전투 시작 (딜레이 후)
|
// 전투 시작 (딜레이 후)
|
||||||
Future.delayed(const Duration(milliseconds: 500), _startBattle);
|
Future.delayed(const Duration(milliseconds: 500), _controller.startBattle);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
_combatSubscription?.cancel();
|
_controller.dispose();
|
||||||
_eventIconTimer?.cancel();
|
|
||||||
_challengerFlashController.dispose();
|
_challengerFlashController.dispose();
|
||||||
_opponentFlashController.dispose();
|
_opponentFlashController.dispose();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
void _startBattle() {
|
/// HP 변화 시 플래시 애니메이션 트리거
|
||||||
_battleStartTime = DateTime.now();
|
void _handleHpChanged(bool isChallenger) {
|
||||||
_combatSubscription = _arenaService
|
if (isChallenger) {
|
||||||
.simulateCombat(widget.match)
|
|
||||||
.listen(
|
|
||||||
(turn) {
|
|
||||||
_processTurn(turn);
|
|
||||||
},
|
|
||||||
onDone: () {
|
|
||||||
_endBattle();
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
void _processTurn(ArenaCombatTurn turn) {
|
|
||||||
final oldChallengerHp = _challengerHp;
|
|
||||||
final oldOpponentHp = _opponentHp;
|
|
||||||
|
|
||||||
setState(() {
|
|
||||||
_currentTurn++;
|
|
||||||
_challengerHp = turn.challengerHp;
|
|
||||||
_opponentHp = turn.opponentHp;
|
|
||||||
_challengerMp = turn.challengerMp ?? _challengerMp;
|
|
||||||
_opponentMp = turn.opponentMp ?? _opponentMp;
|
|
||||||
|
|
||||||
// 도전자 HP 변화 감지
|
|
||||||
if (oldChallengerHp != _challengerHp) {
|
|
||||||
_challengerHpChange = _challengerHp - oldChallengerHp;
|
|
||||||
_challengerFlashController.forward(from: 0.0);
|
_challengerFlashController.forward(from: 0.0);
|
||||||
}
|
} else {
|
||||||
|
|
||||||
// 상대 HP 변화 감지
|
|
||||||
if (oldOpponentHp != _opponentHp) {
|
|
||||||
_opponentHpChange = _opponentHp - oldOpponentHp;
|
|
||||||
_opponentFlashController.forward(from: 0.0);
|
_opponentFlashController.forward(from: 0.0);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 도전자 스킬 사용 로그
|
|
||||||
if (turn.challengerSkillUsed != null) {
|
|
||||||
_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,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 로그 추가 (CombatLogEntry 사용)
|
|
||||||
if (turn.challengerDamage != null) {
|
|
||||||
final type = turn.isChallengerCritical
|
|
||||||
? CombatLogType.critical
|
|
||||||
: 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,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 상대 회피/블록 이벤트
|
|
||||||
if (turn.isOpponentEvaded) {
|
|
||||||
_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,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 상대 스킬 사용 로그
|
|
||||||
if (turn.opponentSkillUsed != null) {
|
|
||||||
_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,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (turn.opponentDamage != null) {
|
|
||||||
final type = turn.isOpponentCritical
|
|
||||||
? 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,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 도전자 회피/블록 이벤트
|
|
||||||
if (turn.isChallengerEvaded) {
|
|
||||||
_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,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 전투 이벤트 생성 (테두리 이펙트용)
|
|
||||||
_latestCombatEvent = _createCombatEvent(turn);
|
|
||||||
|
|
||||||
// 전투 이벤트 아이콘 표시
|
|
||||||
_showEventIcon(turn);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 전투 이벤트 아이콘 표시 (일정 시간 후 사라짐)
|
|
||||||
void _showEventIcon(ArenaCombatTurn turn) {
|
|
||||||
// 이전 타이머 취소
|
|
||||||
_eventIconTimer?.cancel();
|
|
||||||
|
|
||||||
// 스킬 이름 저장
|
|
||||||
_currentSkillName = turn.challengerSkillUsed ?? turn.opponentSkillUsed;
|
|
||||||
|
|
||||||
// 이벤트 타입 결정 (우선순위: 스킬 > 크리티컬 > 블록 > 회피 > 일반공격)
|
|
||||||
CombatEventType? eventType;
|
|
||||||
if (_currentSkillName != null) {
|
|
||||||
eventType = CombatEventType.playerSkill;
|
|
||||||
} else if (turn.isChallengerCritical || turn.isOpponentCritical) {
|
|
||||||
eventType = CombatEventType.playerAttack; // 크리티컬 표시용
|
|
||||||
} else if (turn.isChallengerBlocked || turn.isOpponentBlocked) {
|
|
||||||
eventType = CombatEventType.playerBlock;
|
|
||||||
} else if (turn.isChallengerEvaded || turn.isOpponentEvaded) {
|
|
||||||
eventType = CombatEventType.playerEvade;
|
|
||||||
} else if (turn.challengerDamage != null || turn.opponentDamage != null) {
|
|
||||||
eventType = CombatEventType.playerAttack;
|
|
||||||
}
|
|
||||||
|
|
||||||
_currentEventIcon = eventType;
|
|
||||||
|
|
||||||
// 1초 후 아이콘 숨김
|
|
||||||
_eventIconTimer = Timer(const Duration(milliseconds: 800), () {
|
|
||||||
if (mounted) {
|
|
||||||
setState(() {
|
|
||||||
_currentEventIcon = null;
|
|
||||||
_currentSkillName = null;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/// ArenaCombatTurn에서 CombatEvent 생성 (테두리 이펙트용)
|
|
||||||
CombatEvent? _createCombatEvent(ArenaCombatTurn turn) {
|
|
||||||
final timestamp = DateTime.now().millisecondsSinceEpoch;
|
|
||||||
|
|
||||||
// 도전자 스킬 사용 (보라색 테두리)
|
|
||||||
if (turn.challengerSkillUsed != null && turn.challengerDamage != null) {
|
|
||||||
return CombatEvent.playerSkill(
|
|
||||||
timestamp: timestamp,
|
|
||||||
skillName: turn.challengerSkillUsed!,
|
|
||||||
damage: turn.challengerDamage!,
|
|
||||||
targetName: widget.match.opponent.characterName,
|
|
||||||
isCritical: turn.isChallengerCritical,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 도전자 공격 이벤트 (우선순위: 크리티컬 > 일반 공격)
|
|
||||||
if (turn.challengerDamage != null) {
|
|
||||||
return CombatEvent.playerAttack(
|
|
||||||
timestamp: timestamp,
|
|
||||||
damage: turn.challengerDamage!,
|
|
||||||
targetName: widget.match.opponent.characterName,
|
|
||||||
isCritical: turn.isChallengerCritical,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 도전자 회복 이벤트
|
|
||||||
if (turn.challengerHealAmount != null && turn.challengerSkillUsed != null) {
|
|
||||||
return CombatEvent.playerHeal(
|
|
||||||
timestamp: timestamp,
|
|
||||||
healAmount: turn.challengerHealAmount!,
|
|
||||||
skillName: turn.challengerSkillUsed,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 도전자 방어 이벤트 (회피/블록)
|
|
||||||
if (turn.isChallengerEvaded) {
|
|
||||||
return CombatEvent.playerEvade(
|
|
||||||
timestamp: timestamp,
|
|
||||||
attackerName: widget.match.opponent.characterName,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (turn.isChallengerBlocked) {
|
|
||||||
return CombatEvent.playerBlock(
|
|
||||||
timestamp: timestamp,
|
|
||||||
reducedDamage: turn.opponentDamage ?? 0,
|
|
||||||
attackerName: widget.match.opponent.characterName,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 상대 공격 이벤트 (몬스터 공격으로 처리)
|
|
||||||
if (turn.opponentDamage != null) {
|
|
||||||
return CombatEvent.monsterAttack(
|
|
||||||
timestamp: timestamp,
|
|
||||||
damage: turn.opponentDamage!,
|
|
||||||
attackerName: widget.match.opponent.characterName,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
void _endBattle() {
|
|
||||||
// 시뮬레이션 HP 결과를 기반으로 최종 결과 계산
|
|
||||||
_result = _arenaService.createResultFromSimulation(
|
|
||||||
match: widget.match,
|
|
||||||
challengerHp: _challengerHp,
|
|
||||||
opponentHp: _opponentHp,
|
|
||||||
turns: _currentTurn,
|
|
||||||
);
|
|
||||||
|
|
||||||
// 전투 종료 상태로 전환 (인라인 결과 패널 표시)
|
|
||||||
setState(() {
|
|
||||||
_isFinished = true;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Continue 버튼 콜백
|
/// Continue 버튼 콜백
|
||||||
void _handleContinue() {
|
void _handleContinue() {
|
||||||
if (_result != null) {
|
final result = _controller.state.result;
|
||||||
widget.onBattleComplete(_result!);
|
if (result != null) {
|
||||||
|
widget.onBattleComplete(result);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final s = _controller.state;
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
backgroundColor: RetroColors.backgroundOf(context),
|
backgroundColor: RetroColors.backgroundOf(context),
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
@@ -447,313 +122,56 @@ class _ArenaBattleScreenState extends State<ArenaBattleScreen>
|
|||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
// 턴 표시
|
// 턴 표시
|
||||||
_buildTurnIndicator(),
|
ArenaTurnIndicator(battleStartTime: s.battleStartTime),
|
||||||
// HP 바 (레트로 세그먼트 스타일)
|
// HP 바 (레트로 세그먼트 스타일)
|
||||||
ArenaHpBars(
|
ArenaHpBars(
|
||||||
challengerName: widget.match.challenger.characterName,
|
challengerName: widget.match.challenger.characterName,
|
||||||
challengerHp: _challengerHp,
|
challengerHp: s.challengerHp,
|
||||||
challengerHpMax: _challengerHpMax,
|
challengerHpMax: s.challengerHpMax,
|
||||||
challengerFlashAnimation: _challengerFlashAnimation,
|
challengerFlashAnimation: _challengerFlashAnimation,
|
||||||
challengerHpChange: _challengerHpChange,
|
challengerHpChange: s.challengerHpChange,
|
||||||
opponentName: widget.match.opponent.characterName,
|
opponentName: widget.match.opponent.characterName,
|
||||||
opponentHp: _opponentHp,
|
opponentHp: s.opponentHp,
|
||||||
opponentHpMax: _opponentHpMax,
|
opponentHpMax: s.opponentHpMax,
|
||||||
opponentFlashAnimation: _opponentFlashAnimation,
|
opponentFlashAnimation: _opponentFlashAnimation,
|
||||||
opponentHpChange: _opponentHpChange,
|
opponentHpChange: s.opponentHpChange,
|
||||||
|
),
|
||||||
|
// 전투 이벤트 아이콘
|
||||||
|
ArenaCombatEventIcons(
|
||||||
|
currentEventIcon: s.currentEventIcon,
|
||||||
|
currentSkillName: s.currentSkillName,
|
||||||
|
latestCombatEvent: s.latestCombatEvent,
|
||||||
),
|
),
|
||||||
// 전투 이벤트 아이콘 (HP 바와 애니메이션 사이)
|
|
||||||
_buildCombatEventIcons(),
|
|
||||||
// ASCII 애니메이션 (전투 중 / 종료 분기)
|
// ASCII 애니메이션 (전투 중 / 종료 분기)
|
||||||
_buildBattleArea(),
|
ArenaBattleArea(
|
||||||
|
match: widget.match,
|
||||||
|
isFinished: s.isFinished,
|
||||||
|
result: s.result,
|
||||||
|
latestCombatEvent: s.latestCombatEvent,
|
||||||
|
),
|
||||||
// 로그 영역 (남은 공간 채움)
|
// 로그 영역 (남은 공간 채움)
|
||||||
Expanded(child: _buildBattleLog()),
|
|
||||||
// 결과 패널 (전투 종료 시)
|
|
||||||
if (_isFinished && _result != null)
|
|
||||||
ArenaResultPanel(
|
|
||||||
result: _result!,
|
|
||||||
turnCount: _currentTurn,
|
|
||||||
onContinue: _handleContinue,
|
|
||||||
battleLog: _battleLog,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 방패 장착 여부 확인
|
|
||||||
bool _hasShield(HallOfFameEntry entry) {
|
|
||||||
final equipment = entry.finalEquipment;
|
|
||||||
if (equipment == null) return false;
|
|
||||||
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.raceId,
|
|
||||||
shieldName: _hasShield(widget.match.challenger) ? 'shield' : null,
|
|
||||||
opponentRaceId: widget.match.opponent.raceId,
|
|
||||||
opponentHasShield: _hasShield(widget.match.opponent),
|
|
||||||
latestCombatEvent: _latestCombatEvent,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 종료된 전투 영역 (승자 유지 + 패자 분해)
|
|
||||||
Widget _buildFinishedBattleArea() {
|
|
||||||
final isVictory = _result!.isVictory;
|
|
||||||
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) ??
|
|
||||||
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(
|
Expanded(
|
||||||
child: Center(
|
child: Container(
|
||||||
child: isVictory
|
|
||||||
? _buildStaticCharacter(winnerLines, false)
|
|
||||||
: AsciiDisintegrateWidget(
|
|
||||||
characterLines: _mirrorLines(loserLines),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
// 중앙 VS
|
|
||||||
Text(
|
|
||||||
'VS',
|
|
||||||
style: TextStyle(
|
|
||||||
fontFamily: 'PressStart2P',
|
|
||||||
fontSize: 14,
|
|
||||||
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: 15,
|
|
||||||
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() {
|
|
||||||
// 경과 시간 계산 (분:초 형식)
|
|
||||||
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.timer, color: RetroColors.goldOf(context), size: 16),
|
|
||||||
const SizedBox(width: 8),
|
|
||||||
Text(
|
|
||||||
elapsedTime,
|
|
||||||
style: TextStyle(
|
|
||||||
fontFamily: 'PressStart2P',
|
|
||||||
fontSize: 14,
|
|
||||||
color: RetroColors.goldOf(context),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildBattleLog() {
|
|
||||||
return Container(
|
|
||||||
margin: const EdgeInsets.all(12),
|
margin: const EdgeInsets.all(12),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: RetroColors.panelBgOf(context),
|
color: RetroColors.panelBgOf(context),
|
||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(8),
|
||||||
border: Border.all(color: RetroColors.borderOf(context)),
|
border: Border.all(color: RetroColors.borderOf(context)),
|
||||||
),
|
),
|
||||||
child: ArenaCombatLog(entries: _battleLog),
|
child: ArenaCombatLog(entries: s.battleLog),
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 전투 이벤트 아이콘 영역 (HP 바와 애니메이션 사이)
|
|
||||||
///
|
|
||||||
/// 메인 게임의 _buildBuffIcons() 스타일을 따름
|
|
||||||
/// 스킬 사용, 크리티컬, 블록, 회피 표시
|
|
||||||
Widget _buildCombatEventIcons() {
|
|
||||||
// 스킬 사용 또는 특수 액션만 표시
|
|
||||||
final hasSpecialEvent =
|
|
||||||
_currentSkillName != null ||
|
|
||||||
_latestCombatEvent?.isCritical == true ||
|
|
||||||
_currentEventIcon == CombatEventType.playerBlock ||
|
|
||||||
_currentEventIcon == CombatEventType.playerEvade ||
|
|
||||||
_currentEventIcon == CombatEventType.playerParry ||
|
|
||||||
_currentEventIcon == CombatEventType.playerSkill;
|
|
||||||
|
|
||||||
if (!hasSpecialEvent) {
|
|
||||||
return const SizedBox(height: 28);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 이벤트 타입에 따른 아이콘/색상 결정
|
|
||||||
final (icon, color) = _getEventIconData();
|
|
||||||
|
|
||||||
return AnimatedOpacity(
|
|
||||||
opacity: _currentEventIcon != null ? 1.0 : 0.0,
|
|
||||||
duration: const Duration(milliseconds: 200),
|
|
||||||
child: SizedBox(
|
|
||||||
height: 28,
|
|
||||||
child: Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
children: [
|
|
||||||
// 버프 아이콘 스타일 (CircularProgressIndicator)
|
|
||||||
Stack(
|
|
||||||
alignment: Alignment.center,
|
|
||||||
children: [
|
|
||||||
// 원형 진행률 표시 (펄스 효과용)
|
|
||||||
SizedBox(
|
|
||||||
width: 24,
|
|
||||||
height: 24,
|
|
||||||
child: CircularProgressIndicator(
|
|
||||||
value: 1.0,
|
|
||||||
strokeWidth: 2,
|
|
||||||
backgroundColor: Colors.grey.shade700,
|
|
||||||
valueColor: AlwaysStoppedAnimation(color),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
// 아이콘
|
// 결과 패널 (전투 종료 시)
|
||||||
Icon(icon, size: 12, color: color),
|
if (s.isFinished && s.result != null)
|
||||||
],
|
ArenaResultPanel(
|
||||||
|
result: s.result!,
|
||||||
|
turnCount: s.currentTurn,
|
||||||
|
onContinue: _handleContinue,
|
||||||
|
battleLog: s.battleLog,
|
||||||
),
|
),
|
||||||
// 스킬 이름 표시
|
|
||||||
if (_currentSkillName != null) ...[
|
|
||||||
const SizedBox(width: 6),
|
|
||||||
Text(
|
|
||||||
_currentSkillName!,
|
|
||||||
style: TextStyle(
|
|
||||||
fontFamily: 'PressStart2P',
|
|
||||||
fontSize: 12,
|
|
||||||
color: color,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 이벤트 타입에 따른 아이콘, 색상 반환
|
|
||||||
(IconData, Color) _getEventIconData() {
|
|
||||||
// 스킬 사용
|
|
||||||
if (_currentSkillName != null ||
|
|
||||||
_currentEventIcon == CombatEventType.playerSkill) {
|
|
||||||
return (Icons.auto_fix_high, Colors.purple);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 크리티컬 체크 (latestCombatEvent에서)
|
|
||||||
if (_latestCombatEvent?.isCritical == true) {
|
|
||||||
return (Icons.flash_on, Colors.yellow.shade600);
|
|
||||||
}
|
|
||||||
|
|
||||||
return switch (_currentEventIcon) {
|
|
||||||
CombatEventType.playerBlock => (Icons.shield, Colors.blue),
|
|
||||||
CombatEventType.playerEvade => (Icons.directions_run, Colors.cyan),
|
|
||||||
CombatEventType.playerParry => (Icons.sports_kabaddi, Colors.purple),
|
|
||||||
_ => (Icons.trending_up, Colors.lightBlue),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
173
lib/src/features/arena/widgets/arena_battle_area.dart
Normal file
173
lib/src/features/arena/widgets/arena_battle_area.dart
Normal file
@@ -0,0 +1,173 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
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/shared/animation/race_character_frames.dart';
|
||||||
|
import 'package:asciineverdie/src/shared/widgets/ascii_disintegrate_widget.dart';
|
||||||
|
import 'package:asciineverdie/src/features/game/widgets/ascii_animation_card.dart';
|
||||||
|
import 'package:asciineverdie/src/shared/retro_colors.dart';
|
||||||
|
|
||||||
|
/// 아레나 전투 영역 위젯 (Arena Battle Area)
|
||||||
|
///
|
||||||
|
/// 활성 전투 중 ASCII 애니메이션 표시, 종료 시 승자/패자 분기 처리
|
||||||
|
class ArenaBattleArea extends StatelessWidget {
|
||||||
|
const ArenaBattleArea({
|
||||||
|
super.key,
|
||||||
|
required this.match,
|
||||||
|
required this.isFinished,
|
||||||
|
this.result,
|
||||||
|
this.latestCombatEvent,
|
||||||
|
});
|
||||||
|
|
||||||
|
final ArenaMatch match;
|
||||||
|
final bool isFinished;
|
||||||
|
final ArenaMatchResult? result;
|
||||||
|
final CombatEvent? latestCombatEvent;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
if (isFinished && result != null) {
|
||||||
|
return _buildFinishedArea(context);
|
||||||
|
}
|
||||||
|
return _buildActiveArea(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 방패 장착 여부 확인
|
||||||
|
bool _hasShield(HallOfFameEntry entry) {
|
||||||
|
final equipment = entry.finalEquipment;
|
||||||
|
if (equipment == null) return false;
|
||||||
|
return equipment.any((item) => item.slot.name == 'shield');
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 활성 전투 영역 (기존 AsciiAnimationCard)
|
||||||
|
Widget _buildActiveArea(BuildContext context) {
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||||
|
child: SizedBox(
|
||||||
|
height: 120,
|
||||||
|
child: AsciiAnimationCard(
|
||||||
|
taskType: TaskType.kill,
|
||||||
|
raceId: match.challenger.raceId,
|
||||||
|
shieldName: _hasShield(match.challenger) ? 'shield' : null,
|
||||||
|
opponentRaceId: match.opponent.raceId,
|
||||||
|
opponentHasShield: _hasShield(match.opponent),
|
||||||
|
latestCombatEvent: latestCombatEvent,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 종료된 전투 영역 (승자 유지 + 패자 분해)
|
||||||
|
Widget _buildFinishedArea(BuildContext context) {
|
||||||
|
final isVictory = result!.isVictory;
|
||||||
|
final winnerRaceId = isVictory
|
||||||
|
? match.challenger.raceId
|
||||||
|
: match.opponent.raceId;
|
||||||
|
final loserRaceId = isVictory
|
||||||
|
? match.opponent.raceId
|
||||||
|
: match.challenger.raceId;
|
||||||
|
|
||||||
|
// 패자 캐릭터 프레임 (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(context, winnerLines)
|
||||||
|
: AsciiDisintegrateWidget(
|
||||||
|
characterLines: _mirrorLines(loserLines),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
// 중앙 VS
|
||||||
|
Text(
|
||||||
|
'VS',
|
||||||
|
style: TextStyle(
|
||||||
|
fontFamily: 'PressStart2P',
|
||||||
|
fontSize: 14,
|
||||||
|
color: RetroColors.goldOf(context).withValues(alpha: 0.5),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
// 우측: 상대 (승자면 유지, 패자면 분해)
|
||||||
|
Expanded(
|
||||||
|
child: Center(
|
||||||
|
child: isVictory
|
||||||
|
? AsciiDisintegrateWidget(characterLines: loserLines)
|
||||||
|
: _buildStaticCharacter(context, _mirrorLines(winnerLines)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 정적 ASCII 캐릭터 표시
|
||||||
|
Widget _buildStaticCharacter(BuildContext context, List<String> lines) {
|
||||||
|
final textColor = RetroColors.textPrimaryOf(context);
|
||||||
|
return Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: lines
|
||||||
|
.map(
|
||||||
|
(line) => Text(
|
||||||
|
line,
|
||||||
|
style: TextStyle(
|
||||||
|
fontFamily: 'JetBrainsMono',
|
||||||
|
fontSize: 15,
|
||||||
|
color: textColor,
|
||||||
|
height: 1.2,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.toList(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// ASCII 문자열 미러링 (좌우 대칭)
|
||||||
|
static List<String> _mirrorLines(List<String> lines) {
|
||||||
|
return lines.map((line) {
|
||||||
|
final chars = line.split('');
|
||||||
|
return chars.reversed.map(_mirrorChar).join();
|
||||||
|
}).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 개별 문자 미러링
|
||||||
|
static String _mirrorChar(String char) {
|
||||||
|
return switch (char) {
|
||||||
|
'/' => r'\',
|
||||||
|
r'\' => '/',
|
||||||
|
'(' => ')',
|
||||||
|
')' => '(',
|
||||||
|
'[' => ']',
|
||||||
|
']' => '[',
|
||||||
|
'{' => '}',
|
||||||
|
'}' => '{',
|
||||||
|
'<' => '>',
|
||||||
|
'>' => '<',
|
||||||
|
'd' => 'b',
|
||||||
|
'b' => 'd',
|
||||||
|
'q' => 'p',
|
||||||
|
'p' => 'q',
|
||||||
|
_ => char,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
104
lib/src/features/arena/widgets/arena_combat_event_icons.dart
Normal file
104
lib/src/features/arena/widgets/arena_combat_event_icons.dart
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import 'package:asciineverdie/src/core/model/combat_event.dart';
|
||||||
|
|
||||||
|
/// 아레나 전투 이벤트 아이콘 위젯 (Arena Combat Event Icons)
|
||||||
|
///
|
||||||
|
/// 스킬 사용, 크리티컬, 블록, 회피 등 특수 이벤트를 아이콘으로 표시
|
||||||
|
class ArenaCombatEventIcons extends StatelessWidget {
|
||||||
|
const ArenaCombatEventIcons({
|
||||||
|
super.key,
|
||||||
|
this.currentEventIcon,
|
||||||
|
this.currentSkillName,
|
||||||
|
this.latestCombatEvent,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// 현재 표시 중인 이벤트 아이콘 타입
|
||||||
|
final CombatEventType? currentEventIcon;
|
||||||
|
|
||||||
|
/// 현재 표시 중인 스킬 이름
|
||||||
|
final String? currentSkillName;
|
||||||
|
|
||||||
|
/// 최신 전투 이벤트 (크리티컬 체크용)
|
||||||
|
final CombatEvent? latestCombatEvent;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final hasSpecialEvent =
|
||||||
|
currentSkillName != null ||
|
||||||
|
latestCombatEvent?.isCritical == true ||
|
||||||
|
currentEventIcon == CombatEventType.playerBlock ||
|
||||||
|
currentEventIcon == CombatEventType.playerEvade ||
|
||||||
|
currentEventIcon == CombatEventType.playerParry ||
|
||||||
|
currentEventIcon == CombatEventType.playerSkill;
|
||||||
|
|
||||||
|
if (!hasSpecialEvent) {
|
||||||
|
return const SizedBox(height: 28);
|
||||||
|
}
|
||||||
|
|
||||||
|
final (icon, color) = _getEventIconData();
|
||||||
|
|
||||||
|
return AnimatedOpacity(
|
||||||
|
opacity: currentEventIcon != null ? 1.0 : 0.0,
|
||||||
|
duration: const Duration(milliseconds: 200),
|
||||||
|
child: SizedBox(
|
||||||
|
height: 28,
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
// 버프 아이콘 스타일 (CircularProgressIndicator)
|
||||||
|
Stack(
|
||||||
|
alignment: Alignment.center,
|
||||||
|
children: [
|
||||||
|
SizedBox(
|
||||||
|
width: 24,
|
||||||
|
height: 24,
|
||||||
|
child: CircularProgressIndicator(
|
||||||
|
value: 1.0,
|
||||||
|
strokeWidth: 2,
|
||||||
|
backgroundColor: Colors.grey.shade700,
|
||||||
|
valueColor: AlwaysStoppedAnimation(color),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Icon(icon, size: 12, color: color),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
// 스킬 이름 표시
|
||||||
|
if (currentSkillName != null) ...[
|
||||||
|
const SizedBox(width: 6),
|
||||||
|
Text(
|
||||||
|
currentSkillName!,
|
||||||
|
style: TextStyle(
|
||||||
|
fontFamily: 'PressStart2P',
|
||||||
|
fontSize: 12,
|
||||||
|
color: color,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 이벤트 타입에 따른 아이콘, 색상 반환
|
||||||
|
(IconData, Color) _getEventIconData() {
|
||||||
|
// 스킬 사용
|
||||||
|
if (currentSkillName != null ||
|
||||||
|
currentEventIcon == CombatEventType.playerSkill) {
|
||||||
|
return (Icons.auto_fix_high, Colors.purple);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 크리티컬 체크
|
||||||
|
if (latestCombatEvent?.isCritical == true) {
|
||||||
|
return (Icons.flash_on, Colors.yellow.shade600);
|
||||||
|
}
|
||||||
|
|
||||||
|
return switch (currentEventIcon) {
|
||||||
|
CombatEventType.playerBlock => (Icons.shield, Colors.blue),
|
||||||
|
CombatEventType.playerEvade => (Icons.directions_run, Colors.cyan),
|
||||||
|
CombatEventType.playerParry => (Icons.sports_kabaddi, Colors.purple),
|
||||||
|
_ => (Icons.trending_up, Colors.lightBlue),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
46
lib/src/features/arena/widgets/arena_turn_indicator.dart
Normal file
46
lib/src/features/arena/widgets/arena_turn_indicator.dart
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import 'package:asciineverdie/src/shared/retro_colors.dart';
|
||||||
|
|
||||||
|
/// 아레나 턴 표시 위젯 (Arena Turn Indicator)
|
||||||
|
///
|
||||||
|
/// 경과 시간을 분:초 형식으로 표시
|
||||||
|
class ArenaTurnIndicator extends StatelessWidget {
|
||||||
|
const ArenaTurnIndicator({super.key, this.battleStartTime});
|
||||||
|
|
||||||
|
/// 전투 시작 시간 (경과 시간 계산용)
|
||||||
|
final DateTime? battleStartTime;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
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.timer, color: RetroColors.goldOf(context), size: 16),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Text(
|
||||||
|
elapsedTime,
|
||||||
|
style: TextStyle(
|
||||||
|
fontFamily: 'PressStart2P',
|
||||||
|
fontSize: 14,
|
||||||
|
color: RetroColors.goldOf(context),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user