import 'package:asciineverdie/src/shared/animation/background_data.dart'; import 'package:asciineverdie/src/shared/animation/background_layer.dart'; import 'package:asciineverdie/src/shared/animation/canvas/ascii_cell.dart'; import 'package:asciineverdie/src/shared/animation/canvas/ascii_layer.dart'; import 'package:asciineverdie/src/shared/animation/race_character_frames.dart'; /// Canvas용 걷기 애니메이션 합성기 /// /// 배경 스크롤 + 걷는 캐릭터 /// Phase 4: 종족별 캐릭터 프레임 지원 class CanvasWalkingComposer { const CanvasWalkingComposer({this.raceId}); /// 종족 ID (종족별 캐릭터 프레임 선택용) final String? raceId; /// 프레임 상수 static const int frameWidth = 60; static const int frameHeight = 8; /// 레이어 기반 프레임 생성 List composeLayers(int globalTick) { return [ _createBackgroundLayer(globalTick), _createCharacterLayer(globalTick), ]; } /// 배경 레이어 생성 (z=0) - 숲 환경 기본 AsciiLayer _createBackgroundLayer(int globalTick) { final cells = List.generate( frameHeight, (_) => List.filled(frameWidth, AsciiCell.empty), ); final bgLayers = getBackgroundLayers(EnvironmentType.forest); for (final layer in bgLayers) { // 스크롤 오프셋 계산 (걷기는 더 빠른 스크롤) final offset = (globalTick * layer.scrollSpeed * 2).toInt(); for (var i = 0; i < layer.lines.length; i++) { final y = layer.yStart + i; if (y >= frameHeight) break; final pattern = layer.lines[i]; if (pattern.isEmpty) continue; for (var x = 0; x < frameWidth; x++) { final patternIdx = (x + offset) % pattern.length; final char = pattern[patternIdx]; if (char != ' ') { cells[y][x] = AsciiCell.fromChar(char); } } } } return AsciiLayer(cells: cells, zIndex: 0); } /// 걷는 캐릭터 레이어 생성 (z=1) /// Phase 4: 종족별 프레임 지원 AsciiLayer _createCharacterLayer(int globalTick) { final frameIndex = globalTick % 4; // 4프레임 루프 List charFrame; // 종족별 프레임 사용 시도 if (raceId != null && raceId!.isNotEmpty) { final raceData = RaceCharacterFrames.get(raceId!); if (raceData != null) { // idle 프레임을 기반으로 걷기 애니메이션 생성 final idleFrame = raceData.idle[frameIndex % raceData.idle.length]; charFrame = _animateWalking(idleFrame.lines, frameIndex); } else { charFrame = _walkingFrames[frameIndex]; } } else { charFrame = _walkingFrames[frameIndex]; } final cells = _spriteToCells(charFrame); // 화면 중앙에 캐릭터 배치 (25% 위치) const charX = 15; // 바닥 레이어(Y=7) 위에 서있도록 final charY = frameHeight - cells.length - 1; return AsciiLayer(cells: cells, zIndex: 1, offsetX: charX, offsetY: charY); } /// idle 프레임 기반 걷기 애니메이션 생성 /// 종족별 다리 모양을 유지 (idle 프레임이 4개라 자연스럽게 변화) List _animateWalking(List idleLines, int frameIndex) { // idle 프레임을 그대로 사용 (종족별 다리 모양 유지) // frameIndex에 따라 idle[0~3] 중 하나가 선택되어 자연스럽게 애니메이션됨 return idleLines; } /// 문자열 스프라이트를 AsciiCell 2D 배열로 변환 List> _spriteToCells(List lines) { return lines.map((line) { return line.split('').map(AsciiCell.fromChar).toList(); }).toList(); } } // ============================================================================ // 걷기 프레임 (4프레임 루프) // ============================================================================ const _walkingFrames = [ // 프레임 1: 오른발 앞 [r' o ', r' /|\ ', r' /| '], // 프레임 2: 모음 [r' o ', r' /|\ ', r' |\ '], // 프레임 3: 왼발 앞 [r' o ', r' /|\ ', r' /| '], // 프레임 4: 모음 [r' o ', r' /|\ ', r' |\ '], ];