refactor(arena): 아레나 화면 및 위젯 정리
This commit is contained in:
@@ -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,7 +144,10 @@ class _ArenaBattleScreenState extends State<ArenaBattleScreen>
|
||||
}
|
||||
|
||||
void _startBattle() {
|
||||
_combatSubscription = _arenaService.simulateCombat(widget.match).listen(
|
||||
_battleStartTime = DateTime.now();
|
||||
_combatSubscription = _arenaService
|
||||
.simulateCombat(widget.match)
|
||||
.listen(
|
||||
(turn) {
|
||||
_processTurn(turn);
|
||||
},
|
||||
@@ -174,22 +182,28 @@ class _ArenaBattleScreenState extends State<ArenaBattleScreen>
|
||||
|
||||
// 도전자 스킬 사용 로그
|
||||
if (turn.challengerSkillUsed != null) {
|
||||
_battleLog.add(CombatLogEntry(
|
||||
message: '${widget.match.challenger.characterName} uses '
|
||||
_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 '
|
||||
_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 '
|
||||
_battleLog.add(
|
||||
CombatLogEntry(
|
||||
message:
|
||||
'${widget.match.challenger.characterName} deals '
|
||||
'${turn.challengerDamage}$critText$skillText',
|
||||
timestamp: DateTime.now(),
|
||||
type: type,
|
||||
));
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// 상대 회피/블록 이벤트
|
||||
if (turn.isOpponentEvaded) {
|
||||
_battleLog.add(CombatLogEntry(
|
||||
_battleLog.add(
|
||||
CombatLogEntry(
|
||||
message: '${widget.match.opponent.characterName} evaded!',
|
||||
timestamp: DateTime.now(),
|
||||
type: CombatLogType.evade,
|
||||
));
|
||||
),
|
||||
);
|
||||
}
|
||||
if (turn.isOpponentBlocked) {
|
||||
_battleLog.add(CombatLogEntry(
|
||||
_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 '
|
||||
_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 '
|
||||
_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 '
|
||||
_battleLog.add(
|
||||
CombatLogEntry(
|
||||
message:
|
||||
'${widget.match.opponent.characterName} deals '
|
||||
'${turn.opponentDamage}$critText',
|
||||
timestamp: DateTime.now(),
|
||||
type: type,
|
||||
));
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// 도전자 회피/블록 이벤트
|
||||
if (turn.isChallengerEvaded) {
|
||||
_battleLog.add(CombatLogEntry(
|
||||
_battleLog.add(
|
||||
CombatLogEntry(
|
||||
message: '${widget.match.challenger.characterName} evaded!',
|
||||
timestamp: DateTime.now(),
|
||||
type: CombatLogType.evade,
|
||||
));
|
||||
),
|
||||
);
|
||||
}
|
||||
if (turn.isChallengerBlocked) {
|
||||
_battleLog.add(CombatLogEntry(
|
||||
_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,7 +573,8 @@ class _ArenaBattleScreenState extends State<ArenaBattleScreen>
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: lines
|
||||
.map((line) => Text(
|
||||
.map(
|
||||
(line) => Text(
|
||||
line,
|
||||
style: TextStyle(
|
||||
fontFamily: 'JetBrainsMono',
|
||||
@@ -548,7 +582,8 @@ class _ArenaBattleScreenState extends State<ArenaBattleScreen>
|
||||
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 ||
|
||||
|
||||
@@ -73,10 +73,7 @@ class _ArenaScreenState extends State<ArenaScreen> {
|
||||
appBar: AppBar(
|
||||
title: Text(
|
||||
_arenaTitle,
|
||||
style: const TextStyle(
|
||||
fontFamily: 'PressStart2P',
|
||||
fontSize: 12,
|
||||
),
|
||||
style: const TextStyle(fontFamily: 'PressStart2P', fontSize: 12),
|
||||
),
|
||||
centerTitle: true,
|
||||
backgroundColor: RetroColors.panelBgOf(context),
|
||||
@@ -164,5 +161,4 @@ class _ArenaScreenState extends State<ArenaScreen> {
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -133,10 +133,7 @@ class _ArenaSetupScreenState extends State<ArenaSetupScreen> {
|
||||
appBar: AppBar(
|
||||
title: Text(
|
||||
_setupTitle,
|
||||
style: const TextStyle(
|
||||
fontFamily: 'PressStart2P',
|
||||
fontSize: 12,
|
||||
),
|
||||
style: const TextStyle(fontFamily: 'PressStart2P', fontSize: 12),
|
||||
),
|
||||
centerTitle: true,
|
||||
backgroundColor: RetroColors.panelBgOf(context),
|
||||
@@ -232,10 +229,12 @@ class _ArenaSetupScreenState extends State<ArenaSetupScreen> {
|
||||
final myItem = _findItem(slot, _challenger!.finalEquipment);
|
||||
final enemyItem = _findItem(slot, _opponent!.finalEquipment);
|
||||
|
||||
final myScore =
|
||||
myItem != null ? ItemService.calculateEquipmentScore(myItem) : 0;
|
||||
final enemyScore =
|
||||
enemyItem != null ? ItemService.calculateEquipmentScore(enemyItem) : 0;
|
||||
final myScore = myItem != null
|
||||
? ItemService.calculateEquipmentScore(myItem)
|
||||
: 0;
|
||||
final enemyScore = enemyItem != null
|
||||
? ItemService.calculateEquipmentScore(enemyItem)
|
||||
: 0;
|
||||
final gain = enemyScore - myScore;
|
||||
|
||||
if (gain > maxGain) {
|
||||
@@ -354,8 +353,9 @@ class _ArenaSetupScreenState extends State<ArenaSetupScreen> {
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: RetroColors.goldOf(context),
|
||||
foregroundColor: RetroColors.backgroundOf(context),
|
||||
disabledBackgroundColor:
|
||||
RetroColors.borderOf(context).withValues(alpha: 0.5),
|
||||
disabledBackgroundColor: RetroColors.borderOf(
|
||||
context,
|
||||
).withValues(alpha: 0.5),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
|
||||
@@ -175,10 +175,12 @@ class _ArenaEquipmentCompareListState extends State<ArenaEquipmentCompareList> {
|
||||
final isOpponentTarget =
|
||||
widget.opponentBettingSlot == slot; // 상대가 선택 = 내 장비 손실 예정
|
||||
|
||||
final myScore =
|
||||
myItem != null ? ItemService.calculateEquipmentScore(myItem) : 0;
|
||||
final enemyScore =
|
||||
enemyItem != null ? ItemService.calculateEquipmentScore(enemyItem) : 0;
|
||||
final myScore = myItem != null
|
||||
? ItemService.calculateEquipmentScore(myItem)
|
||||
: 0;
|
||||
final enemyScore = enemyItem != null
|
||||
? ItemService.calculateEquipmentScore(enemyItem)
|
||||
: 0;
|
||||
final scoreDiff = enemyScore - myScore;
|
||||
|
||||
return Column(
|
||||
@@ -548,10 +550,7 @@ class _ArenaEquipmentCompareListState extends State<ArenaEquipmentCompareList> {
|
||||
decoration: BoxDecoration(
|
||||
color: RetroColors.goldOf(context).withValues(alpha: 0.2),
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
border: Border.all(
|
||||
color: RetroColors.goldOf(context),
|
||||
width: 2,
|
||||
),
|
||||
border: Border.all(color: RetroColors.goldOf(context), width: 2),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
@@ -643,11 +642,7 @@ class _ArenaEquipmentCompareListState extends State<ArenaEquipmentCompareList> {
|
||||
|
||||
if (statWidgets.isEmpty) return const SizedBox.shrink();
|
||||
|
||||
return Wrap(
|
||||
spacing: 3,
|
||||
runSpacing: 3,
|
||||
children: statWidgets,
|
||||
);
|
||||
return Wrap(spacing: 3, runSpacing: 3, children: statWidgets);
|
||||
}
|
||||
|
||||
Widget _buildStatChip(String label, int value, Color color) {
|
||||
@@ -659,11 +654,7 @@ class _ArenaEquipmentCompareListState extends State<ArenaEquipmentCompareList> {
|
||||
),
|
||||
child: Text(
|
||||
'$label +$value',
|
||||
style: TextStyle(
|
||||
fontFamily: 'PressStart2P',
|
||||
fontSize: 4,
|
||||
color: color,
|
||||
),
|
||||
style: TextStyle(fontFamily: 'PressStart2P', fontSize: 4, color: color),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -63,15 +63,10 @@ class ArenaResultDialog extends StatelessWidget {
|
||||
actions: [
|
||||
FilledButton(
|
||||
onPressed: onClose,
|
||||
style: FilledButton.styleFrom(
|
||||
backgroundColor: resultColor,
|
||||
),
|
||||
style: FilledButton.styleFrom(backgroundColor: resultColor),
|
||||
child: Text(
|
||||
l10n.buttonConfirm,
|
||||
style: const TextStyle(
|
||||
fontFamily: 'PressStart2P',
|
||||
fontSize: 8,
|
||||
),
|
||||
style: const TextStyle(fontFamily: 'PressStart2P', fontSize: 8),
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -249,10 +244,12 @@ class ArenaResultDialog extends StatelessWidget {
|
||||
EquipmentItem? newItem,
|
||||
bool isWinner,
|
||||
) {
|
||||
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;
|
||||
final isGain = scoreDiff > 0;
|
||||
|
||||
@@ -287,12 +284,7 @@ class ArenaResultDialog extends StatelessWidget {
|
||||
children: [
|
||||
// 이전 장비
|
||||
Expanded(
|
||||
child: _buildItemChip(
|
||||
context,
|
||||
oldItem,
|
||||
oldScore,
|
||||
isOld: true,
|
||||
),
|
||||
child: _buildItemChip(context, oldItem, oldScore, isOld: true),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 4),
|
||||
@@ -304,12 +296,7 @@ class ArenaResultDialog extends StatelessWidget {
|
||||
),
|
||||
// 새 장비
|
||||
Expanded(
|
||||
child: _buildItemChip(
|
||||
context,
|
||||
newItem,
|
||||
newScore,
|
||||
isOld: false,
|
||||
),
|
||||
child: _buildItemChip(context, newItem, newScore, isOld: false),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -355,9 +342,7 @@ class ArenaResultDialog extends StatelessWidget {
|
||||
decoration: BoxDecoration(
|
||||
color: rarityColor.withValues(alpha: isOld ? 0.1 : 0.2),
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
border: Border.all(
|
||||
color: rarityColor.withValues(alpha: 0.5),
|
||||
),
|
||||
border: Border.all(color: rarityColor.withValues(alpha: 0.5)),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
|
||||
@@ -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>(
|
||||
_slideAnimation =
|
||||
Tween<Offset>(
|
||||
begin: const Offset(0, 1), // 아래에서 위로
|
||||
end: Offset.zero,
|
||||
).animate(CurvedAnimation(
|
||||
parent: _slideController,
|
||||
curve: Curves.easeOutCubic,
|
||||
));
|
||||
).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) {
|
||||
|
||||
@@ -29,7 +29,10 @@ class AsciiParticle {
|
||||
/// 진행도에 따라 파티클 상태 업데이트
|
||||
void update(double progress) {
|
||||
// 지연 적용
|
||||
final adjustedProgress = ((progress - delay) / (1.0 - delay)).clamp(0.0, 1.0);
|
||||
final adjustedProgress = ((progress - delay) / (1.0 - delay)).clamp(
|
||||
0.0,
|
||||
1.0,
|
||||
);
|
||||
|
||||
if (adjustedProgress <= 0) {
|
||||
// 아직 분해 시작 전
|
||||
@@ -101,10 +104,7 @@ class _AsciiDisintegrateWidgetState extends State<AsciiDisintegrateWidget>
|
||||
void initState() {
|
||||
super.initState();
|
||||
_initParticles();
|
||||
_controller = AnimationController(
|
||||
duration: widget.duration,
|
||||
vsync: this,
|
||||
)
|
||||
_controller = AnimationController(duration: widget.duration, vsync: this)
|
||||
..addListener(() => setState(() {}))
|
||||
..addStatusListener((status) {
|
||||
if (status == AnimationStatus.completed) {
|
||||
@@ -129,7 +129,8 @@ class _AsciiDisintegrateWidgetState extends State<AsciiDisintegrateWidget>
|
||||
final char = line[x];
|
||||
// 공백은 파티클로 변환하지 않음
|
||||
if (char != ' ') {
|
||||
_particles.add(AsciiParticle(
|
||||
_particles.add(
|
||||
AsciiParticle(
|
||||
char: char,
|
||||
initialX: x.toDouble(),
|
||||
initialY: y.toDouble(),
|
||||
@@ -138,7 +139,8 @@ class _AsciiDisintegrateWidgetState extends State<AsciiDisintegrateWidget>
|
||||
vy: -_random.nextDouble() * 2.0 - 0.5, // 위쪽으로
|
||||
// 랜덤 지연 (안쪽에서 바깥쪽으로 분해)
|
||||
delay: _random.nextDouble() * 0.3,
|
||||
));
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user