feat(animation): 종족별 캐릭터 애니메이션 시스템 추가
- 21개 종족별 고유 ASCII 캐릭터 프레임 데이터 추가 - 각 종족당 5가지 상태 애니메이션: idle, prepare, attack, hit, recover - 종족 특성에 맞는 시각적 차별화 (마법사 ~, 기사 ♦, 언데드 ☠ 등) - 캐릭터 생성 화면 종족 미리보기 위젯 추가 - 프론트 화면 Hero vs Boss 애니메이션 개선 - 게임 플레이 화면 애니메이션 패널 연동 강화
This commit is contained in:
129
lib/src/features/new_character/widgets/race_preview.dart
Normal file
129
lib/src/features/new_character/widgets/race_preview.dart
Normal file
@@ -0,0 +1,129 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:askiineverdie/src/core/animation/character_frames.dart';
|
||||
import 'package:askiineverdie/src/core/animation/race_character_frames.dart';
|
||||
|
||||
/// 종족 미리보기 위젯
|
||||
///
|
||||
/// 새 캐릭터 생성 화면에서 선택한 종족의 idle 애니메이션을 보여줌.
|
||||
/// RichText 기반 색상 적용.
|
||||
class RacePreview extends StatefulWidget {
|
||||
const RacePreview({
|
||||
super.key,
|
||||
required this.raceId,
|
||||
});
|
||||
|
||||
/// 종족 ID (예: "byte_human", "kernel_giant")
|
||||
final String raceId;
|
||||
|
||||
@override
|
||||
State<RacePreview> createState() => _RacePreviewState();
|
||||
}
|
||||
|
||||
class _RacePreviewState extends State<RacePreview> {
|
||||
Timer? _timer;
|
||||
int _currentFrame = 0;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_startAnimation();
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(RacePreview oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (oldWidget.raceId != widget.raceId) {
|
||||
_currentFrame = 0;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_timer?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _startAnimation() {
|
||||
_timer = Timer.periodic(const Duration(milliseconds: 400), (_) {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_currentFrame++;
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final raceData = RaceCharacterFrames.get(widget.raceId);
|
||||
final frames = raceData?.idle ?? RaceCharacterFrames.defaultFrames.idle;
|
||||
final frame = frames[_currentFrame % frames.length];
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.black,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(
|
||||
color: Theme.of(context).colorScheme.outline.withValues(alpha: 0.3),
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// 캐릭터 프레임
|
||||
_buildColoredFrame(frame),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// RichText 기반 색상 적용 프레임
|
||||
Widget _buildColoredFrame(CharacterFrame frame) {
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: frame.lines.map((line) => _buildColoredLine(line)).toList(),
|
||||
);
|
||||
}
|
||||
|
||||
/// 한 줄을 색상 적용하여 렌더링
|
||||
Widget _buildColoredLine(String line) {
|
||||
final spans = <TextSpan>[];
|
||||
|
||||
for (var i = 0; i < line.length; i++) {
|
||||
final char = line[i];
|
||||
spans.add(
|
||||
TextSpan(
|
||||
text: char,
|
||||
style: TextStyle(
|
||||
fontFamily: 'monospace',
|
||||
fontSize: 18,
|
||||
height: 1.2,
|
||||
color: _getCharColor(char),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return RichText(
|
||||
text: TextSpan(children: spans),
|
||||
);
|
||||
}
|
||||
|
||||
/// 문자별 색상 매핑
|
||||
Color _getCharColor(String char) {
|
||||
// 공격/이펙트 (시안)
|
||||
if ('><=!+~'.contains(char)) return Colors.cyan;
|
||||
// 데미지/글리치 (마젠타)
|
||||
if ('*@#\$%&'.contains(char)) return const Color(0xFFFF00FF);
|
||||
// 특수 문자 (노랑)
|
||||
if ('☠◈◉'.contains(char)) return Colors.yellow;
|
||||
// 대형 문자 (밝은 녹색)
|
||||
if ('█▓░'.contains(char)) return Colors.lightGreen;
|
||||
// 기본 (흰색)
|
||||
return Colors.white;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user