feat(arena): 아레나 화면 구현
- ArenaScreen: 아레나 메인 화면 - ArenaSetupScreen: 전투 설정 화면 - ArenaBattleScreen: 전투 진행 화면 - 관련 위젯 추가
This commit is contained in:
181
lib/src/features/arena/widgets/arena_idle_preview.dart
Normal file
181
lib/src/features/arena/widgets/arena_idle_preview.dart
Normal file
@@ -0,0 +1,181 @@
|
||||
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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user