Files
asciinevrdie/lib/src/shared/animation/canvas/canvas_special_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

176 lines
5.9 KiB
Dart

import 'package:asciineverdie/src/shared/animation/ascii_animation_type.dart';
import 'package:asciineverdie/src/shared/animation/canvas/ascii_cell.dart';
import 'package:asciineverdie/src/shared/animation/canvas/ascii_layer.dart';
/// Canvas용 특수 이벤트 애니메이션 합성기
///
/// 레벨업, 퀘스트 완료, Act 완료, 부활 등
class CanvasSpecialComposer {
const CanvasSpecialComposer();
/// 프레임 상수
static const int frameWidth = 60;
static const int frameHeight = 8;
/// 레이어 기반 프레임 생성
List<AsciiLayer> composeLayers(
AsciiAnimationType type,
int frameIndex,
int globalTick,
) {
return switch (type) {
AsciiAnimationType.levelUp => _composeLevelUp(frameIndex, globalTick),
AsciiAnimationType.questComplete => _composeQuestComplete(
frameIndex,
globalTick,
),
AsciiAnimationType.actComplete => _composeActComplete(
frameIndex,
globalTick,
),
AsciiAnimationType.resurrection => _composeResurrection(
frameIndex,
globalTick,
),
_ => [AsciiLayer.empty()],
};
}
/// 레벨업 애니메이션
List<AsciiLayer> _composeLevelUp(int frameIndex, int globalTick) {
final layers = <AsciiLayer>[
_createEffectBackground(globalTick, '*'),
_createCenteredSprite(_levelUpFrames[frameIndex % _levelUpFrames.length]),
];
return layers;
}
/// 퀘스트 완료 애니메이션
List<AsciiLayer> _composeQuestComplete(int frameIndex, int globalTick) {
final layers = <AsciiLayer>[
_createEffectBackground(globalTick, '+'),
_createCenteredSprite(
_questCompleteFrames[frameIndex % _questCompleteFrames.length],
),
];
return layers;
}
/// Act 완료 애니메이션
List<AsciiLayer> _composeActComplete(int frameIndex, int globalTick) {
final layers = <AsciiLayer>[
_createEffectBackground(globalTick, '~'),
_createCenteredSprite(
_actCompleteFrames[frameIndex % _actCompleteFrames.length],
),
];
return layers;
}
/// 부활 애니메이션
List<AsciiLayer> _composeResurrection(int frameIndex, int globalTick) {
final layers = <AsciiLayer>[
_createEffectBackground(globalTick, '.'),
_createCenteredSprite(
_resurrectionFrames[frameIndex % _resurrectionFrames.length],
),
];
return layers;
}
/// 이펙트 배경 레이어 생성 (z=0)
AsciiLayer _createEffectBackground(int globalTick, String effectChar) {
final cells = List.generate(
frameHeight,
(_) => List.filled(frameWidth, AsciiCell.empty),
);
// 반짝이는 이펙트
for (var y = 0; y < frameHeight; y++) {
for (var x = 0; x < frameWidth; x++) {
final offset = (x + y + globalTick) % 8;
if (offset == 0) {
cells[y][x] = AsciiCell.fromChar(effectChar);
}
}
}
return AsciiLayer(cells: cells, zIndex: 0);
}
/// 중앙 정렬 스프라이트 레이어 생성 (z=1)
AsciiLayer _createCenteredSprite(List<String> lines) {
final cells = _spriteToCells(lines);
// 중앙 정렬
final spriteWidth = lines.isEmpty ? 0 : lines[0].length;
final offsetX = (frameWidth - spriteWidth) ~/ 2;
final offsetY = (frameHeight - cells.length) ~/ 2;
return AsciiLayer(
cells: cells,
zIndex: 1,
offsetX: offsetX,
offsetY: offsetY,
);
}
/// 문자열 스프라이트를 AsciiCell 2D 배열로 변환
List<List<AsciiCell>> _spriteToCells(List<String> lines) {
return lines.map((line) {
return line.split('').map(AsciiCell.fromChar).toList();
}).toList();
}
}
// ============================================================================
// 레벨업 프레임 (5프레임)
// ============================================================================
const _levelUpFrames = [
[r' * ', r' \|/ ', r' o ', r' /|\ ', r' / \ '],
[r' * * ', r' \|/ ', r' O ', r' </|\> ', r' / \ '],
[r' * * * ', r' \|/ ', r' O ', r' <\|/> ', r' / \ '],
[r' * * * * ', r' LEVEL ', r' UP! ', r' \O/ ', r' / \ '],
[r'* * * * *', r' LEVEL ', r' UP! ', r' \O/ ', r' | | '],
];
// ============================================================================
// 퀘스트 완료 프레임 (4프레임)
// ============================================================================
const _questCompleteFrames = [
[r' [?] ', r' | ', r' o ', r' /|\ ', r' / \ '],
[r' [???] ', r' | ', r' o! ', r' /|\ ', r' / \ '],
[r' [DONE] ', r' ! ', r' \o/ ', r' | ', r' / \ '],
[r' +[DONE]+', r' \!/ ', r' \o/ ', r' | ', r' / \ '],
];
// ============================================================================
// Act 완료 프레임 (4프레임)
// ============================================================================
const _actCompleteFrames = [
[r'=========', r' ACT ', r' CLEAR ', r' o ', r' /|\ '],
[r'~~~~~~~~~', r' ACT ', r' CLEAR! ', r' \o/ ', r' | '],
[r'*~*~*~*~*', r' ACT ', r' CLEAR!! ', r' \O/ ', r' / \ '],
[r'*********', r' ACT ', r' CLEAR!! ', r' \O/ ', r' | | '],
];
// ============================================================================
// 부활 프레임 (5프레임)
// ============================================================================
const _resurrectionFrames = [
// 프레임 1: R.I.P 묘비
[r' ___ ', r' |RIP| ', r' | | ', r'__|___|__'],
// 프레임 2: 빛 내림
[r' \|/ ', r' -|R|- ', r' | | ', r'__|___|__'],
// 프레임 3: 일어남
[r' \o/ ', r' --|-- ', r' | | ', r'__|___|__'],
// 프레임 4: 서있음
[r' o ', r' /|\ ', r' / \ ', r'_________'],
// 프레임 5: 부활 완료
[r' REVIVED ', r' \o/ ', r' | ', r'___/ \___'],
];