- ArenaScreen: 아레나 메인 화면 - ArenaSetupScreen: 전투 설정 화면 - ArenaBattleScreen: 전투 진행 화면 - 관련 위젯 추가
182 lines
4.5 KiB
Dart
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();
|
|
}
|
|
}
|