Files
asciinevrdie/lib/src/features/new_character/widgets/race_preview.dart
JiWoong Sul 448f500ca0 refactor(ui): 기타 화면 정리
- FrontScreen, HallOfFameScreen 개선
- NewCharacterScreen, SettingsScreen 정리
- App 초기화 로직 정리
2026-01-12 16:17:25 +09:00

138 lines
3.6 KiB
Dart

import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
import 'package:asciineverdie/src/core/animation/character_frames.dart';
import 'package:asciineverdie/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) return;
// 프레임 빌드 중이면 다음 프레임까지 대기 (WASM 모드 안정성)
if (SchedulerBinding.instance.schedulerPhase ==
SchedulerPhase.persistentCallbacks) {
SchedulerBinding.instance.addPostFrameCallback((_) {
if (mounted) {
setState(() {
_currentFrame++;
});
}
});
} else {
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: 'JetBrainsMono',
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;
}
}