Files
asciinevrdie/lib/src/shared/animation/canvas/canvas_walking_composer.dart
JiWoong Sul 8f351df0b6 refactor(shared): animation, l10n, theme 모듈을 core에서 shared로 이동
- core/animation → shared/animation
- core/l10n → shared/l10n
- core/constants/ascii_colors → shared/theme/ascii_colors
- import 경로 업데이트
2026-02-23 15:49:14 +09:00

121 lines
4.1 KiB
Dart

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<AsciiLayer> 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<String> 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<String> _animateWalking(List<String> idleLines, int frameIndex) {
// idle 프레임을 그대로 사용 (종족별 다리 모양 유지)
// frameIndex에 따라 idle[0~3] 중 하나가 선택되어 자연스럽게 애니메이션됨
return idleLines;
}
/// 문자열 스프라이트를 AsciiCell 2D 배열로 변환
List<List<AsciiCell>> _spriteToCells(List<String> 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' |\ '],
];