- arena_battle_controller.dart: 전투 로직 (스트림, 턴, 로그) - arena_battle_area.dart: 전투 영역 위젯 - arena_turn_indicator.dart: 경과 시간 위젯 - arena_combat_event_icons.dart: 전투 이벤트 아이콘 위젯
404 lines
12 KiB
Dart
404 lines
12 KiB
Dart
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();
|
|
}
|
|
}
|