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 createState() => _ArenaIdlePreviewState(); } class _ArenaIdlePreviewState extends State { /// 현재 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 _composeLayers() { final layers = []; // 도전자 캐릭터 (좌측, 정방향) 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 _mirrorLines(List 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> _spriteToCells(List lines) { return lines.map((line) { return line.split('').map(AsciiCell.fromChar).toList(); }).toList(); } }