Files
asciinevrdie/lib/src/features/arena/widgets/arena_idle_preview.dart
JiWoong Sul a2e93efc97 feat(arena): 아레나 화면 구현
- ArenaScreen: 아레나 메인 화면
- ArenaSetupScreen: 전투 설정 화면
- ArenaBattleScreen: 전투 진행 화면
- 관련 위젯 추가
2026-01-06 17:55:02 +09:00

182 lines
4.5 KiB
Dart

import 'dart:async';
import 'package:asciineverdie/src/core/animation/canvas/ascii_cell.dart';
import 'package:asciineverdie/src/core/animation/canvas/ascii_canvas_widget.dart';
import 'package:asciineverdie/src/core/animation/canvas/ascii_layer.dart';
import 'package:asciineverdie/src/core/animation/character_frames.dart';
import 'package:asciineverdie/src/core/animation/race_character_frames.dart';
import 'package:flutter/material.dart';
/// 아레나 idle 상태 캐릭터 미리보기 위젯
///
/// 좌측에 도전자, 우측에 상대(좌우 반전)를 idle 상태로 표시
class ArenaIdlePreview extends StatefulWidget {
const ArenaIdlePreview({
super.key,
required this.challengerRaceId,
required this.opponentRaceId,
});
/// 도전자 종족 ID
final String? challengerRaceId;
/// 상대 종족 ID
final String? opponentRaceId;
@override
State<ArenaIdlePreview> createState() => _ArenaIdlePreviewState();
}
class _ArenaIdlePreviewState extends State<ArenaIdlePreview> {
/// 현재 idle 프레임 인덱스 (0~3)
int _frameIndex = 0;
/// 애니메이션 타이머
Timer? _timer;
/// 레이어 버전 (변경 감지용)
int _layerVersion = 0;
/// 캔버스 크기
static const int _gridWidth = 32;
static const int _gridHeight = 5;
/// 캐릭터 위치
static const int _leftCharX = 4;
static const int _rightCharX = 22;
static const int _charY = 1; // 상단 여백
@override
void initState() {
super.initState();
_startAnimation();
}
@override
void dispose() {
_timer?.cancel();
super.dispose();
}
void _startAnimation() {
// 200ms마다 프레임 업데이트 (원본 틱 속도)
_timer = Timer.periodic(const Duration(milliseconds: 200), (_) {
setState(() {
_frameIndex = (_frameIndex + 1) % 4;
_layerVersion++;
});
});
}
@override
Widget build(BuildContext context) {
final layers = _composeLayers();
return SizedBox(
height: 60,
child: AsciiCanvasWidget(
layers: layers,
gridWidth: _gridWidth,
gridHeight: _gridHeight,
backgroundOpacity: 0.3,
isAnimating: true,
layerVersion: _layerVersion,
),
);
}
/// 레이어 합성
List<AsciiLayer> _composeLayers() {
final layers = <AsciiLayer>[];
// 도전자 캐릭터 (좌측, 정방향)
final challengerLayer = _createCharacterLayer(
widget.challengerRaceId,
_leftCharX,
mirrored: false,
);
layers.add(challengerLayer);
// 상대 캐릭터 (우측, 좌우 반전)
final opponentLayer = _createCharacterLayer(
widget.opponentRaceId,
_rightCharX,
mirrored: true,
);
layers.add(opponentLayer);
return layers;
}
/// 캐릭터 레이어 생성
AsciiLayer _createCharacterLayer(
String? raceId,
int xOffset, {
required bool mirrored,
}) {
// 종족별 idle 프레임 조회
CharacterFrame frame;
if (raceId != null && raceId.isNotEmpty) {
final raceData = RaceCharacterFrames.get(raceId);
if (raceData != null) {
frame = raceData.idle[_frameIndex % raceData.idle.length];
} else {
frame = getCharacterFrame(BattlePhase.idle, _frameIndex);
}
} else {
frame = getCharacterFrame(BattlePhase.idle, _frameIndex);
}
// 미러링 적용
final lines = mirrored ? _mirrorLines(frame.lines) : frame.lines;
// 셀 변환
final cells = _spriteToCells(lines);
return AsciiLayer(
cells: cells,
zIndex: 1,
offsetX: xOffset,
offsetY: _charY,
);
}
/// 문자열 좌우 반전
List<String> _mirrorLines(List<String> lines) {
return lines.map((line) {
final chars = line.split('');
final mirrored = chars.reversed.map(_mirrorChar).toList();
return mirrored.join();
}).toList();
}
/// 개별 문자 미러링 (방향성 문자 변환)
String _mirrorChar(String char) {
return switch (char) {
'/' => r'\',
r'\' => '/',
'(' => ')',
')' => '(',
'[' => ']',
']' => '[',
'{' => '}',
'}' => '{',
'<' => '>',
'>' => '<',
'' => '',
'' => '',
'' => '',
'' => '',
'λ' => 'λ', // 대칭
_ => char,
};
}
/// 문자열 스프라이트를 AsciiCell 2D 배열로 변환
List<List<AsciiCell>> _spriteToCells(List<String> lines) {
return lines.map((line) {
return line.split('').map(AsciiCell.fromChar).toList();
}).toList();
}
}