- FrontScreen, HallOfFameScreen 개선 - NewCharacterScreen, SettingsScreen 정리 - App 초기화 로직 정리
138 lines
3.6 KiB
Dart
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;
|
|
}
|
|
}
|