- core/animation → shared/animation - core/l10n → shared/l10n - core/constants/ascii_colors → shared/theme/ascii_colors - import 경로 업데이트
121 lines
4.1 KiB
Dart
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' |\ '],
|
|
];
|