feat(ui): 게임 위젯 개선

- AsciiAnimationCard 확장
- EnhancedAnimationPanel 개선
- HpMpBar UI 개선
This commit is contained in:
JiWoong Sul
2026-01-14 00:18:10 +09:00
parent d52dea56ea
commit f65bab6312
3 changed files with 242 additions and 70 deletions

View File

@@ -3,6 +3,7 @@ import 'dart:async';
import 'package:flutter/material.dart';
import 'package:asciineverdie/src/core/animation/ascii_animation_data.dart';
import 'package:asciineverdie/src/shared/widgets/ascii_disintegrate_widget.dart';
import 'package:asciineverdie/src/core/animation/ascii_animation_type.dart';
import 'package:asciineverdie/src/core/animation/background_layer.dart';
import 'package:asciineverdie/src/core/animation/canvas/ascii_canvas_widget.dart';
@@ -47,6 +48,8 @@ class AsciiAnimationCard extends StatefulWidget {
this.monsterLevel,
this.monsterGrade,
this.isPaused = false,
this.isInCombat = true,
this.monsterDied = false,
this.latestCombatEvent,
this.raceId,
this.weaponRarity,
@@ -59,6 +62,12 @@ class AsciiAnimationCard extends StatefulWidget {
/// 일시정지 상태 (true면 애니메이션 정지)
final bool isPaused;
/// 전투 활성 상태 (false면 kill 태스크여도 walking 애니메이션)
final bool isInCombat;
/// 몬스터 사망 여부 (true면 분해 애니메이션 재생)
final bool monsterDied;
/// 전투 중인 몬스터 기본 이름 (kill 타입일 때만 사용)
final String? monsterBaseName;
final AsciiColorTheme colorTheme;
@@ -162,6 +171,11 @@ class _AsciiAnimationCardState extends State<AsciiAnimationCard> {
// 특수 애니메이션 프레임 수는 ascii_animation_type.dart의
// specialAnimationFrameCounts 상수 사용
// 몬스터 사망 분해 애니메이션 상태
bool _showDeathAnimation = false;
List<String>? _deathAnimationMonsterLines;
String? _lastMonsterBaseName;
@override
void initState() {
super.initState();
@@ -204,12 +218,32 @@ class _AsciiAnimationCardState extends State<AsciiAnimationCard> {
return;
}
// 몬스터 사망 애니메이션 트리거
if (!oldWidget.monsterDied && widget.monsterDied && !_showDeathAnimation) {
// 현재 몬스터 프레임 캡처 (분해 애니메이션용)
_deathAnimationMonsterLines = _captureMonsterFrame();
if (_deathAnimationMonsterLines != null) {
setState(() {
_showDeathAnimation = true;
});
return; // 사망 애니메이션 중에는 다른 업데이트 무시
}
}
// 사망 애니메이션 중에는 다른 업데이트 무시
if (_showDeathAnimation) return;
// 전투 이벤트 동기화 (Phase 5)
if (widget.latestCombatEvent != null &&
widget.latestCombatEvent!.timestamp != _lastEventTimestamp) {
_handleCombatEvent(widget.latestCombatEvent!);
}
// 몬스터 이름 저장 (사망 시 프레임 캡처용)
if (widget.monsterBaseName != null) {
_lastMonsterBaseName = widget.monsterBaseName;
}
if (oldWidget.taskType != widget.taskType ||
oldWidget.monsterBaseName != widget.monsterBaseName ||
oldWidget.weaponName != widget.weaponName ||
@@ -218,7 +252,8 @@ class _AsciiAnimationCardState extends State<AsciiAnimationCard> {
oldWidget.raceId != widget.raceId ||
oldWidget.weaponRarity != widget.weaponRarity ||
oldWidget.opponentRaceId != widget.opponentRaceId ||
oldWidget.opponentHasShield != widget.opponentHasShield) {
oldWidget.opponentHasShield != widget.opponentHasShield ||
oldWidget.isInCombat != widget.isInCombat) {
_updateAnimation();
}
}
@@ -505,12 +540,18 @@ class _AsciiAnimationCardState extends State<AsciiAnimationCard> {
switch (animationType) {
case AsciiAnimationType.battle:
_animationMode = AnimationMode.battle;
_setupBattleComposer();
_battlePhase = BattlePhase.idle;
_battleSubFrame = 0;
_phaseIndex = 0;
_phaseFrameCount = 0;
// 전투 비활성 상태면 walking 모드로 전환 (몬스터 처치 후 이동 중)
if (!widget.isInCombat) {
_animationMode = AnimationMode.walking;
_walkingComposer = CanvasWalkingComposer(raceId: widget.raceId);
} else {
_animationMode = AnimationMode.battle;
_setupBattleComposer();
_battlePhase = BattlePhase.idle;
_battleSubFrame = 0;
_phaseIndex = 0;
_phaseFrameCount = 0;
}
case AsciiAnimationType.town:
_animationMode = AnimationMode.town;
@@ -556,6 +597,31 @@ class _AsciiAnimationCardState extends State<AsciiAnimationCard> {
);
}
/// 현재 몬스터 프레임을 텍스트 라인으로 캡처 (분해 애니메이션용)
List<String>? _captureMonsterFrame() {
final monsterName = _lastMonsterBaseName ?? widget.monsterBaseName;
if (monsterName == null) return null;
final monsterCategory = getMonsterCategory(monsterName);
final monsterSize = getMonsterSize(widget.monsterLevel);
// 몬스터 Idle 프레임 가져오기
final frames = getMonsterIdleFrames(monsterCategory, monsterSize);
if (frames.isEmpty) return null;
return frames.first;
}
/// 사망 애니메이션 완료 콜백
void _onDeathAnimationComplete() {
setState(() {
_showDeathAnimation = false;
_deathAnimationMonsterLines = null;
});
// Walking 모드로 전환
_updateAnimation();
}
void _advanceBattleFrame() {
_phaseFrameCount++;
final currentPhase = _battlePhaseSequence[_phaseIndex];
@@ -617,6 +683,7 @@ class _AsciiAnimationCardState extends State<AsciiAnimationCard> {
isDot: _showDotEffect,
isBlock: _showBlockEffect,
isParry: _showParryEffect,
hideMonster: _showDeathAnimation,
) ??
[AsciiLayer.empty()],
AnimationMode.walking =>
@@ -676,7 +743,26 @@ class _AsciiAnimationCardState extends State<AsciiAnimationCard> {
borderRadius: BorderRadius.circular(4),
border: borderEffect,
),
child: AsciiCanvasWidget(layers: _composeLayers()),
child: Stack(
children: [
// 기본 애니메이션
AsciiCanvasWidget(layers: _composeLayers()),
// 몬스터 사망 분해 애니메이션 오버레이
// 몬스터 위치: 캔버스 60열 중 30~48열 (중앙값 41열)
// Alignment x = (41/60) * 2 - 1 = 0.37
if (_showDeathAnimation && _deathAnimationMonsterLines != null)
Align(
alignment: const Alignment(0.37, 0.0),
child: AsciiDisintegrateWidget(
characterLines: _deathAnimationMonsterLines!,
duration: const Duration(milliseconds: 800),
textColor: widget.monsterGrade?.displayColor,
onComplete: _onDeathAnimationComplete,
),
),
],
),
);
}
}