feat(animation): 종족별 캐릭터 애니메이션 시스템 추가

- 21개 종족별 고유 ASCII 캐릭터 프레임 데이터 추가
  - 각 종족당 5가지 상태 애니메이션: idle, prepare, attack, hit, recover
  - 종족 특성에 맞는 시각적 차별화 (마법사 ~, 기사 ♦, 언데드 ☠ 등)
- 캐릭터 생성 화면 종족 미리보기 위젯 추가
- 프론트 화면 Hero vs Boss 애니메이션 개선
- 게임 플레이 화면 애니메이션 패널 연동 강화
This commit is contained in:
JiWoong Sul
2025-12-23 20:00:41 +09:00
parent 549851f693
commit 7219f58853
11 changed files with 1186 additions and 64 deletions

View File

@@ -3,12 +3,13 @@ import 'dart:math';
import 'package:flutter/material.dart';
import 'package:askiineverdie/data/race_data.dart';
import 'package:askiineverdie/src/core/animation/front_screen_animation.dart';
/// 프론트 화면용 Hero vs Glitch God ASCII 애니메이션 위젯
///
/// 작은 용사가 거대한 Glitch God에 맞서는 루프 애니메이션
/// 항상 검은 배경에 흰색 텍스트, 특수 효과만 컬러로 표시
/// RichText로 문자별 색상 적용, 랜덤 종족 변경 지원
class HeroVsBossAnimation extends StatefulWidget {
const HeroVsBossAnimation({super.key});
@@ -18,23 +19,30 @@ class HeroVsBossAnimation extends StatefulWidget {
class _HeroVsBossAnimationState extends State<HeroVsBossAnimation> {
int _currentFrame = 0;
Timer? _timer;
Timer? _animationTimer;
Timer? _raceChangeTimer;
final Random _random = Random();
// 현재 종족 (랜덤 변경)
String _currentRaceId = 'byte_human';
int _raceIndex = 0;
@override
void initState() {
super.initState();
_startAnimation();
_startRaceChangeTimer();
}
@override
void dispose() {
_timer?.cancel();
_animationTimer?.cancel();
_raceChangeTimer?.cancel();
super.dispose();
}
void _startAnimation() {
_timer = Timer.periodic(
_animationTimer = Timer.periodic(
const Duration(milliseconds: frontScreenAnimationIntervalMs),
(_) {
if (mounted) {
@@ -47,6 +55,26 @@ class _HeroVsBossAnimationState extends State<HeroVsBossAnimation> {
);
}
/// 8초마다 랜덤 종족 변경
void _startRaceChangeTimer() {
_raceChangeTimer = Timer.periodic(
const Duration(seconds: 8),
(_) => _changeRace(),
);
}
void _changeRace() {
if (!mounted) return;
final allRaces = RaceData.all;
if (allRaces.isEmpty) return;
setState(() {
_raceIndex = (_raceIndex + 1) % allRaces.length;
_currentRaceId = allRaces[_raceIndex].raceId;
});
}
/// 글리치 효과: 랜덤 문자 대체
String _applyGlitchEffect(String frame) {
// 10% 확률로 글리치 효과 적용
@@ -66,12 +94,52 @@ class _HeroVsBossAnimationState extends State<HeroVsBossAnimation> {
return chars.join();
}
/// 문자별 색상 결정
Color _getCharColor(String char) {
// 공격/이펙트 (시안)
if ('><=!+'.contains(char)) return Colors.cyan;
// 데미지/글리치 (마젠타)
if ('*~@#\$%&'.contains(char)) return const Color(0xFFFF00FF);
// 보스 눈 (빨강)
if ('◈◉X'.contains(char)) return Colors.red;
// 보스 타이틀 텍스트 (시안)
if ('GLITCH'.contains(char) || 'GOD'.contains(char)) return Colors.cyan;
// 기본 (흰색)
return Colors.white;
}
/// 문자열을 색상별 TextSpan으로 변환
TextSpan _buildColoredTextSpan(String text) {
final spans = <TextSpan>[];
for (final char in text.split('')) {
final color = _getCharColor(char);
spans.add(
TextSpan(
text: char,
style: TextStyle(
fontFamily: 'JetBrainsMono',
fontSize: 10,
height: 1.1,
color: color,
letterSpacing: 0,
),
),
);
}
return TextSpan(children: spans);
}
@override
Widget build(BuildContext context) {
final frame = _applyGlitchEffect(
frontScreenAnimationFrames[_currentFrame],
);
// 현재 종족 이름 (UI 표시용)
final raceName = RaceData.findById(_currentRaceId)?.name ?? 'Hero';
return Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 16),
decoration: BoxDecoration(
@@ -93,23 +161,26 @@ class _HeroVsBossAnimationState extends State<HeroVsBossAnimation> {
),
child: Column(
children: [
// ASCII 애니메이션 (흰색 텍스트)
// ASCII 애니메이션 (컬러 적용)
FittedBox(
fit: BoxFit.scaleDown,
child: Text(
frame,
style: const TextStyle(
fontFamily: 'JetBrainsMono',
fontSize: 10,
height: 1.1,
color: Colors.white,
letterSpacing: 0,
),
child: RichText(
text: _buildColoredTextSpan(frame),
),
),
const SizedBox(height: 8),
// 하단 효과 바 (컬러)
_buildEffectBar(),
const SizedBox(height: 4),
// 현재 종족 표시
Text(
'$raceName',
style: TextStyle(
fontFamily: 'JetBrainsMono',
fontSize: 9,
color: Colors.cyan.withValues(alpha: 0.7),
),
),
],
),
);