import 'dart:async'; import 'package:flutter/material.dart'; import 'package:askiineverdie/src/core/animation/ascii_animation_data.dart'; import 'package:askiineverdie/src/core/animation/ascii_animation_type.dart'; import 'package:askiineverdie/src/core/animation/background_layer.dart'; import 'package:askiineverdie/src/core/animation/battle_composer.dart'; import 'package:askiineverdie/src/core/animation/character_frames.dart'; import 'package:askiineverdie/src/core/animation/monster_colors.dart'; import 'package:askiineverdie/src/core/animation/monster_size.dart'; import 'package:askiineverdie/src/core/animation/weapon_category.dart'; import 'package:askiineverdie/src/core/model/game_state.dart'; /// ASCII 애니메이션 카드 위젯 /// /// TaskType에 따라 다른 애니메이션을 표시. /// 전투 시 몬스터 이름에 따라 다른 애니메이션 선택. /// 특수 이벤트(레벨업, 퀘스트 완료) 시 오버라이드 애니메이션 재생. /// 자체 타이머로 프레임 전환 (게임 틱과 독립). class AsciiAnimationCard extends StatefulWidget { const AsciiAnimationCard({ super.key, required this.taskType, this.monsterBaseName, this.colorTheme = AsciiColorTheme.green, this.specialAnimation, this.weaponName, this.shieldName, this.characterLevel, this.monsterLevel, }); final TaskType taskType; /// 전투 중인 몬스터 기본 이름 (kill 타입일 때만 사용) final String? monsterBaseName; final AsciiColorTheme colorTheme; /// 특수 애니메이션 오버라이드 (레벨업, 퀘스트 완료 등) /// 설정되면 일반 애니메이션 대신 표시 final AsciiAnimationType? specialAnimation; /// 현재 장착 무기 이름 (공격 스타일 결정용) final String? weaponName; /// 현재 장착 방패 이름 (방패 표시용) final String? shieldName; /// 캐릭터 레벨 final int? characterLevel; /// 몬스터 레벨 (몬스터 크기 결정용) final int? monsterLevel; @override State createState() => _AsciiAnimationCardState(); } class _AsciiAnimationCardState extends State { Timer? _timer; int _currentFrame = 0; late AsciiAnimationData _animationData; AsciiAnimationType? _currentSpecialAnimation; // 전투 애니메이션 상태 bool _isBattleMode = false; BattlePhase _battlePhase = BattlePhase.idle; int _battleSubFrame = 0; BattleComposer? _battleComposer; // 글로벌 틱 (배경 스크롤용) int _globalTick = 0; // 환경 타입 EnvironmentType _environment = EnvironmentType.forest; // 전투 페이즈 시퀀스 (반복) static const _battlePhaseSequence = [ (BattlePhase.idle, 4), // 4 프레임 대기 (BattlePhase.prepare, 2), // 2 프레임 준비 (BattlePhase.attack, 3), // 3 프레임 공격 (BattlePhase.hit, 2), // 2 프레임 히트 (BattlePhase.recover, 2), // 2 프레임 복귀 ]; int _phaseIndex = 0; int _phaseFrameCount = 0; @override void initState() { super.initState(); _updateAnimation(); } @override void didUpdateWidget(AsciiAnimationCard oldWidget) { super.didUpdateWidget(oldWidget); // 특수 애니메이션이 변경되었으면 업데이트 if (oldWidget.specialAnimation != widget.specialAnimation) { _currentSpecialAnimation = widget.specialAnimation; _updateAnimation(); return; } // 특수 애니메이션이 활성화되어 있으면 일반 업데이트 무시 if (_currentSpecialAnimation != null) { return; } if (oldWidget.taskType != widget.taskType || oldWidget.monsterBaseName != widget.monsterBaseName || oldWidget.weaponName != widget.weaponName || oldWidget.shieldName != widget.shieldName || oldWidget.monsterLevel != widget.monsterLevel) { _updateAnimation(); } } void _updateAnimation() { _timer?.cancel(); // 특수 애니메이션이 있으면 우선 적용 if (_currentSpecialAnimation != null) { _isBattleMode = false; _animationData = getAnimationData(_currentSpecialAnimation!); _currentFrame = 0; // 특수 애니메이션은 한 번 재생 후 종료 _timer = Timer.periodic( Duration(milliseconds: _animationData.frameIntervalMs), (_) { if (mounted) { setState(() { _currentFrame++; // 마지막 프레임에 도달하면 특수 애니메이션 종료 if (_currentFrame >= _animationData.frames.length) { _currentSpecialAnimation = null; _updateAnimation(); } }); } }, ); return; } // 일반 애니메이션 처리 final animationType = taskTypeToAnimation(widget.taskType); // 전투 타입이면 새 BattleComposer 시스템 사용 if (animationType == AsciiAnimationType.battle) { _isBattleMode = true; _setupBattleComposer(); _battlePhase = BattlePhase.idle; _battleSubFrame = 0; _phaseIndex = 0; _phaseFrameCount = 0; _timer = Timer.periodic( const Duration(milliseconds: 200), (_) => _advanceBattleFrame(), ); } else { _isBattleMode = false; _animationData = getAnimationData(animationType); _currentFrame = 0; _timer = Timer.periodic( Duration(milliseconds: _animationData.frameIntervalMs), (_) { if (mounted) { setState(() { _currentFrame = (_currentFrame + 1) % _animationData.frames.length; }); } }, ); } } void _setupBattleComposer() { final weaponCategory = getWeaponCategory(widget.weaponName); final hasShield = widget.shieldName != null && widget.shieldName!.isNotEmpty; final monsterCategory = getMonsterCategory(widget.monsterBaseName); final monsterSize = getMonsterSize(widget.monsterLevel); _battleComposer = BattleComposer( weaponCategory: weaponCategory, hasShield: hasShield, monsterCategory: monsterCategory, monsterSize: monsterSize, ); // 환경 타입 추론 _environment = inferEnvironment( widget.taskType.name, widget.monsterBaseName, ); } void _advanceBattleFrame() { if (!mounted) return; setState(() { // 글로벌 틱 증가 (배경 스크롤용) _globalTick++; _phaseFrameCount++; final currentPhase = _battlePhaseSequence[_phaseIndex]; // 현재 페이즈의 프레임 수를 초과하면 다음 페이즈로 if (_phaseFrameCount >= currentPhase.$2) { _phaseIndex = (_phaseIndex + 1) % _battlePhaseSequence.length; _phaseFrameCount = 0; _battleSubFrame = 0; } else { _battleSubFrame++; } _battlePhase = _battlePhaseSequence[_phaseIndex].$1; }); } @override void dispose() { _timer?.cancel(); super.dispose(); } @override Widget build(BuildContext context) { final brightness = Theme.of(context).brightness; final colors = getThemeColors(widget.colorTheme, brightness); // 특수 애니메이션 중이면 특별한 배경색 적용 final isSpecial = _currentSpecialAnimation != null; final bgColor = isSpecial ? colors.backgroundColor.withValues(alpha: 0.95) : colors.backgroundColor; // 프레임 텍스트 결정 String frameText; Color textColor = colors.textColor; if (_isBattleMode && _battleComposer != null) { // 새 배틀 시스템 사용 (배경 포함) frameText = _battleComposer!.composeFrameWithBackground( _battlePhase, _battleSubFrame, widget.monsterBaseName, _environment, _globalTick, ); // 히트 페이즈면 몬스터 색상 변경 if (_battlePhase == BattlePhase.hit) { final monsterColorCategory = getMonsterColorCategory(widget.monsterBaseName); textColor = getMonsterColors(monsterColorCategory).hit; } } else { // 기존 레거시 시스템 사용 final frameIndex = _currentFrame.clamp(0, _animationData.frames.length - 1); frameText = _animationData.frames[frameIndex]; } return Container( padding: const EdgeInsets.all(4), decoration: BoxDecoration( color: bgColor, borderRadius: BorderRadius.circular(4), border: isSpecial ? Border.all(color: colors.textColor.withValues(alpha: 0.5)) : null, ), child: _isBattleMode ? LayoutBuilder( builder: (context, constraints) { // 60x8 프레임에 맞게 폰트 크기 자동 계산 // ASCII 문자 비율: 너비 = 높이 * 0.6 (모노스페이스) final maxWidth = constraints.maxWidth; final maxHeight = constraints.maxHeight; // 60자 폭, 8줄 높이 기준 final fontSizeByWidth = maxWidth / 60 / 0.6; final fontSizeByHeight = maxHeight / 8 / 1.2; final fontSize = (fontSizeByWidth < fontSizeByHeight ? fontSizeByWidth : fontSizeByHeight) .clamp(6.0, 14.0); return Center( child: Text( frameText, style: TextStyle( fontFamily: 'Courier', fontSize: fontSize, color: textColor, height: 1.2, letterSpacing: 0, ), textAlign: TextAlign.left, ), ); }, ) : Center( child: Text( frameText, style: TextStyle( fontFamily: 'monospace', fontSize: 10, color: textColor, height: 1.1, letterSpacing: 0, ), textAlign: TextAlign.center, ), ), ); } }