refactor(shared): animation, l10n, theme 모듈을 core에서 shared로 이동

- core/animation → shared/animation
- core/l10n → shared/l10n
- core/constants/ascii_colors → shared/theme/ascii_colors
- import 경로 업데이트
This commit is contained in:
JiWoong Sul
2026-02-23 15:49:14 +09:00
parent 8fcb7bf2b7
commit 8f351df0b6
24 changed files with 1409 additions and 1498 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
import 'package:flutter/material.dart';
import 'package:asciineverdie/src/core/animation/ascii_animation_type.dart';
import 'package:asciineverdie/src/shared/animation/ascii_animation_type.dart';
/// ASCII
class AsciiAnimationData {

View File

@@ -1,7 +1,7 @@
//
// ASCII Patrol -
import 'package:asciineverdie/src/core/animation/background_layer.dart';
import 'package:asciineverdie/src/shared/animation/background_layer.dart';
///
List<BackgroundLayer> getBackgroundLayers(EnvironmentType environment) {

View File

@@ -1,8 +1,8 @@
import 'dart:ui' as ui;
import 'package:asciineverdie/src/core/animation/canvas/ascii_cell.dart';
import 'package:asciineverdie/src/core/animation/canvas/ascii_layer.dart';
import 'package:asciineverdie/src/core/constants/ascii_colors.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/theme/ascii_colors.dart';
import 'package:flutter/material.dart';
/// Paragraph

View File

@@ -1,6 +1,6 @@
import 'package:asciineverdie/src/core/animation/canvas/ascii_canvas_painter.dart';
import 'package:asciineverdie/src/core/animation/canvas/ascii_layer.dart';
import 'package:asciineverdie/src/core/constants/ascii_colors.dart';
import 'package:asciineverdie/src/shared/animation/canvas/ascii_canvas_painter.dart';
import 'package:asciineverdie/src/shared/animation/canvas/ascii_layer.dart';
import 'package:asciineverdie/src/shared/theme/ascii_colors.dart';
import 'package:flutter/material.dart';
/// ASCII Canvas (RepaintBoundary )

View File

@@ -1,4 +1,4 @@
import 'package:asciineverdie/src/core/animation/canvas/ascii_cell.dart';
import 'package:asciineverdie/src/shared/animation/canvas/ascii_cell.dart';
/// ASCII (Canvas )
///

View File

@@ -0,0 +1,544 @@
import 'package:asciineverdie/src/shared/animation/ascii_animation_data.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/canvas/combat_text_frames.dart';
import 'package:asciineverdie/src/shared/animation/canvas/monster_frames.dart';
import 'package:asciineverdie/src/shared/animation/canvas/rarity_color_mapper.dart';
import 'package:asciineverdie/src/shared/animation/character_frames.dart';
import 'package:asciineverdie/src/shared/animation/monster_size.dart';
import 'package:asciineverdie/src/shared/animation/race_character_frames.dart';
import 'package:asciineverdie/src/shared/animation/weapon_category.dart';
import 'package:asciineverdie/src/shared/animation/weapon_effects.dart';
import 'package:asciineverdie/src/core/model/item_stats.dart';
// 하위 호환성(backward compatibility)을 위한 re-export
export 'package:asciineverdie/src/shared/animation/canvas/monster_frames.dart'
show getMonsterIdleFrames;
/// Canvas용 전투 프레임 합성기
///
/// 기존 BattleComposer의 로직을 레이어 기반으로 변환.
/// 출력: `List<AsciiLayer>` (z-order 정렬됨)
///
/// PvP 모드: [opponentRaceId]가 설정되면 몬스터 대신 상대 캐릭터(좌우 반전) 표시
class CanvasBattleComposer {
const CanvasBattleComposer({
required this.weaponCategory,
required this.hasShield,
required this.monsterCategory,
required this.monsterSize,
this.raceId,
this.weaponRarity,
this.opponentRaceId,
this.opponentHasShield = false,
});
final WeaponCategory weaponCategory;
final bool hasShield;
final MonsterCategory monsterCategory;
final MonsterSize monsterSize;
/// 종족 ID (Phase 4: 종족별 캐릭터 애니메이션)
final String? raceId;
/// 무기 희귀도 (Phase 9: 무기 등급별 이펙트 색상)
final ItemRarity? weaponRarity;
/// 상대 종족 ID (PvP 모드: 설정 시 몬스터 대신 캐릭터 표시)
final String? opponentRaceId;
/// 상대 방패 장착 여부 (PvP 모드)
final bool opponentHasShield;
/// PvP 모드 여부
bool get isPvP => opponentRaceId != null;
/// 프레임 상수
static const int frameWidth = 60;
static const int frameHeight = 8;
static const int monsterWidth = 18;
/// 레이어 기반 프레임 생성
List<AsciiLayer> composeLayers(
BattlePhase phase,
int subFrame,
String? monsterBaseName,
EnvironmentType environment,
int globalTick, {
AttackerType attacker = AttackerType.none,
bool isCritical = false,
bool isEvade = false,
bool isMiss = false,
bool isDebuff = false,
bool isDot = false,
bool isBlock = false,
bool isParry = false,
bool hideMonster = false,
}) {
final layers = <AsciiLayer>[
_createBackgroundLayer(environment, globalTick),
_createCharacterLayer(phase, subFrame, attacker),
// PvP 모드: 몬스터 대신 상대 캐릭터(좌우 반전) 표시
// hideMonster: 몬스터 사망 애니메이션 중에는 렌더링 안함
if (!hideMonster)
isPvP
? _createOpponentCharacterLayer(phase, subFrame, attacker)
: _createMonsterLayer(phase, subFrame, attacker),
];
// 이펙트 레이어 (준비/공격/히트 페이즈에서, 공격자 있을 때)
if ((phase == BattlePhase.prepare ||
phase == BattlePhase.attack ||
phase == BattlePhase.hit) &&
attacker != AttackerType.none) {
final effectLayer = _createEffectLayer(phase, subFrame, attacker);
if (effectLayer != null) {
layers.add(effectLayer);
}
}
// 텍스트 이펙트 레이어
if (isCritical &&
(phase == BattlePhase.attack || phase == BattlePhase.hit)) {
layers.add(_createCriticalTextLayer(subFrame));
}
if (isEvade) layers.add(_createEvadeTextLayer(subFrame));
if (isMiss) layers.add(_createMissTextLayer(subFrame));
if (isDebuff) layers.add(_createDebuffTextLayer(subFrame));
if (isDot) layers.add(_createDotTextLayer(subFrame));
if (isBlock) layers.add(_createBlockTextLayer(subFrame));
if (isParry) layers.add(_createParryTextLayer(subFrame));
// z-order 정렬
layers.sort((a, b) => a.zIndex.compareTo(b.zIndex));
return layers;
}
/// 배경 레이어 생성 (z=0)
AsciiLayer _createBackgroundLayer(
EnvironmentType environment,
int globalTick,
) {
final cells = List.generate(
frameHeight,
(_) => List.filled(frameWidth, AsciiCell.empty),
);
final bgLayers = getBackgroundLayers(environment);
for (final layer in bgLayers) {
final offset = (globalTick * layer.scrollSpeed).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, opacity: 0.5);
}
/// 캐릭터 레이어 생성 (z=2)
AsciiLayer _createCharacterLayer(
BattlePhase phase,
int subFrame,
AttackerType attacker,
) {
CharacterFrame charFrame;
if (raceId != null && raceId!.isNotEmpty) {
final raceData = RaceCharacterFrames.get(raceId!);
if (raceData != null) {
final frames = raceData.getFrames(phase);
charFrame = frames[subFrame % frames.length];
} else {
charFrame = getCharacterFrame(phase, subFrame);
}
} else {
charFrame = getCharacterFrame(phase, subFrame);
}
if (hasShield) charFrame = charFrame.withShield();
final isPlayerAttacking =
attacker == AttackerType.player || attacker == AttackerType.both;
final charX = _getCharacterX(phase, isPlayerAttacking);
final cells = _spriteToCells(charFrame.lines);
final charY = frameHeight - cells.length - 1;
return AsciiLayer(
cells: cells,
zIndex: 2,
offsetX: charX,
offsetY: charY,
);
}
/// 몬스터 레이어 생성 (z=1)
AsciiLayer _createMonsterLayer(
BattlePhase phase,
int subFrame,
AttackerType attacker,
) {
final monsterFrames = getAnimatedMonsterFrames(
monsterCategory,
monsterSize,
phase,
);
final monsterFrame = monsterFrames[subFrame % monsterFrames.length];
final cells = _spriteToRightAlignedCells(monsterFrame, monsterWidth);
final isMonsterAttacking =
attacker == AttackerType.monster || attacker == AttackerType.both;
final monsterRightEdge = _getMonsterRightEdge(phase, isMonsterAttacking);
final monsterX = monsterRightEdge - monsterWidth;
final monsterY = frameHeight - cells.length - 1;
return AsciiLayer(
cells: cells,
zIndex: 1,
offsetX: monsterX,
offsetY: monsterY,
);
}
/// 상대 캐릭터 레이어 생성 (PvP 모드, z=1)
AsciiLayer _createOpponentCharacterLayer(
BattlePhase phase,
int subFrame,
AttackerType attacker,
) {
CharacterFrame opponentFrame;
if (opponentRaceId != null && opponentRaceId!.isNotEmpty) {
final raceData = RaceCharacterFrames.get(opponentRaceId!);
if (raceData != null) {
final frames = raceData.getFrames(phase);
opponentFrame = frames[subFrame % frames.length];
} else {
opponentFrame = getCharacterFrame(phase, subFrame);
}
} else {
opponentFrame = getCharacterFrame(phase, subFrame);
}
if (opponentHasShield) opponentFrame = opponentFrame.withShield();
final mirroredLines = _mirrorLines(opponentFrame.lines);
final isOpponentAttacking =
attacker == AttackerType.monster || attacker == AttackerType.both;
const opponentWidth = 6;
final opponentRightEdge = _getMonsterRightEdge(phase, isOpponentAttacking);
final opponentX = opponentRightEdge - opponentWidth;
final cells = _spriteToCells(mirroredLines);
final opponentY = frameHeight - cells.length - 1;
return AsciiLayer(
cells: cells,
zIndex: 1,
offsetX: opponentX,
offsetY: opponentY,
);
}
/// 이펙트 레이어 생성 (z=3)
AsciiLayer? _createEffectLayer(
BattlePhase phase,
int subFrame,
AttackerType attacker,
) {
final isPlayerAttacking =
attacker == AttackerType.player || attacker == AttackerType.both;
final isMonsterAttacking =
attacker == AttackerType.monster || attacker == AttackerType.both;
const charWidth = 6;
final charX = _getCharacterX(phase, isPlayerAttacking);
final monsterRightEdge = _getMonsterRightEdge(phase, isMonsterAttacking);
final monsterX = monsterRightEdge - monsterWidth;
final List<String> effectLines;
final int effectX;
if (attacker == AttackerType.player) {
final effect = getWeaponEffect(weaponCategory);
effectLines = _getEffectLines(effect, phase, subFrame);
effectX = monsterX - 2;
} else if (attacker == AttackerType.monster) {
effectLines = _getMonsterAttackEffect(phase, subFrame);
effectX = charX + charWidth;
} else {
final effect = getWeaponEffect(weaponCategory);
effectLines = _getEffectLines(effect, phase, subFrame);
effectX = (charX + charWidth + monsterX) ~/ 2;
}
if (effectLines.isEmpty) return null;
final List<List<AsciiCell>> cells;
if (attacker == AttackerType.player && weaponRarity != null) {
cells = _spriteToCellsWithColor(
effectLines,
weaponRarity!.effectCellColor,
);
} else {
cells = _spriteToCells(effectLines);
}
final effectHeight = effectLines.length;
final effectY = frameHeight - effectHeight - 1;
return AsciiLayer(
cells: cells,
zIndex: 3,
offsetX: effectX,
offsetY: effectY,
);
}
// ============================================================================
// 텍스트 이펙트 레이어
// ============================================================================
AsciiLayer _createCriticalTextLayer(int subFrame) {
final textLines = subFrame % 2 == 0
? critTextFrames[0]
: critTextFrames[1];
final cells = _textLinesToCells(textLines, AsciiCellColor.positive);
final textWidth = textLines.isNotEmpty ? textLines[0].length : 0;
final offsetX = (frameWidth - textWidth) ~/ 2;
return AsciiLayer(cells: cells, zIndex: 4, offsetX: offsetX, offsetY: 0);
}
AsciiLayer _createEvadeTextLayer(int subFrame) {
return _createTextLayer(
frames: evadeTextFrames,
subFrame: subFrame,
color: AsciiCellColor.positive,
offsetX: 15,
);
}
AsciiLayer _createMissTextLayer(int subFrame) {
return _createTextLayer(
frames: missTextFrames,
subFrame: subFrame,
color: AsciiCellColor.negative,
offsetX: 35,
);
}
AsciiLayer _createDebuffTextLayer(int subFrame) {
return _createTextLayer(
frames: debuffTextFrames,
subFrame: subFrame,
color: AsciiCellColor.negative,
offsetX: 35,
);
}
AsciiLayer _createDotTextLayer(int subFrame) {
return _createTextLayer(
frames: dotTextFrames,
subFrame: subFrame,
color: AsciiCellColor.negative,
offsetX: 35,
);
}
AsciiLayer _createBlockTextLayer(int subFrame) {
return _createTextLayer(
frames: blockTextFrames,
subFrame: subFrame,
color: AsciiCellColor.positive,
offsetX: 15,
);
}
AsciiLayer _createParryTextLayer(int subFrame) {
return _createTextLayer(
frames: parryTextFrames,
subFrame: subFrame,
color: AsciiCellColor.positive,
offsetX: 15,
);
}
/// 공통 텍스트 레이어 생성 헬퍼
AsciiLayer _createTextLayer({
required List<List<String>> frames,
required int subFrame,
required AsciiCellColor color,
required int offsetX,
}) {
final textLines = frames[subFrame % 2];
final cells = _textLinesToCells(textLines, color);
return AsciiLayer(cells: cells, zIndex: 4, offsetX: offsetX, offsetY: 0);
}
/// 텍스트 라인을 AsciiCell 배열로 변환
List<List<AsciiCell>> _textLinesToCells(
List<String> lines,
AsciiCellColor color,
) {
return lines.map((String line) {
return line.split('').map((String char) {
if (char == ' ') return AsciiCell.empty;
return AsciiCell(char: char, color: color);
}).toList();
}).toList();
}
// ============================================================================
// 위치 계산 헬퍼
// ============================================================================
/// 캐릭터 X 위치 계산
int _getCharacterX(BattlePhase phase, bool isPlayerAttacking) {
return switch (phase) {
BattlePhase.idle => 12,
BattlePhase.prepare => isPlayerAttacking ? 15 : 12,
BattlePhase.attack => isPlayerAttacking ? 18 : 12,
BattlePhase.hit => isPlayerAttacking ? 18 : 12,
BattlePhase.recover => isPlayerAttacking ? 15 : 12,
};
}
/// 몬스터 오른쪽 가장자리 위치 계산
int _getMonsterRightEdge(BattlePhase phase, bool isMonsterAttacking) {
return switch (phase) {
BattlePhase.idle => 48,
BattlePhase.prepare => isMonsterAttacking ? 45 : 48,
BattlePhase.attack => isMonsterAttacking ? 42 : 48,
BattlePhase.hit => isMonsterAttacking ? 42 : 48,
BattlePhase.recover => isMonsterAttacking ? 45 : 48,
};
}
// ============================================================================
// 이펙트 헬퍼
// ============================================================================
/// 몬스터 공격 이펙트 (방향)
List<String> _getMonsterAttackEffect(BattlePhase phase, int subFrame) {
return switch (phase) {
BattlePhase.prepare =>
monsterPrepareFrames[subFrame % monsterPrepareFrames.length],
BattlePhase.attack =>
monsterAttackFrames[subFrame % monsterAttackFrames.length],
BattlePhase.hit => monsterHitFrames[subFrame % monsterHitFrames.length],
_ => <String>[],
};
}
/// 멀티라인 이펙트 프레임 반환
List<String> _getEffectLines(
WeaponEffect effect,
BattlePhase phase,
int subFrame,
) {
final frames = switch (phase) {
BattlePhase.idle => <List<String>>[],
BattlePhase.prepare => effect.prepareFrames,
BattlePhase.attack => effect.attackFrames,
BattlePhase.hit => effect.hitFrames,
BattlePhase.recover => <List<String>>[],
};
if (frames.isEmpty) return [];
return frames[subFrame % frames.length];
}
// ============================================================================
// 스프라이트 변환 유틸리티
// ============================================================================
/// 문자열 좌우 반전 (PvP 모드용)
List<String> _mirrorLines(List<String> 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'\' => '/',
'(' => ')',
')' => '(',
'[' => ']',
']' => '[',
'{' => '}',
'}' => '{',
'<' => '>',
'>' => '<',
'\u2518' => '\u2514',
'\u2514' => '\u2518',
'\u2510' => '\u250c',
'\u250c' => '\u2510',
_ => char,
};
}
/// 문자열 스프라이트를 AsciiCell 2D 배열로 변환
List<List<AsciiCell>> _spriteToCells(List<String> lines) {
return lines.map((line) {
return line.split('').map(AsciiCell.fromChar).toList();
}).toList();
}
/// 문자열 스프라이트를 지정된 색상으로 변환
List<List<AsciiCell>> _spriteToCellsWithColor(
List<String> lines,
AsciiCellColor effectColor,
) {
return lines.map((line) {
return line.split('').map((char) {
if (char == ' ' || char.isEmpty) return AsciiCell.empty;
return AsciiCell(char: char, color: effectColor);
}).toList();
}).toList();
}
/// 문자열 스프라이트를 오른쪽 정렬된 AsciiCell 2D 배열로 변환
List<List<AsciiCell>> _spriteToRightAlignedCells(
List<String> lines,
int width,
) {
int maxWidth = 0;
for (final line in lines) {
final trimmed = line.trimRight().length;
if (trimmed > maxWidth) maxWidth = trimmed;
}
final leftPadding = width - maxWidth;
return lines.map((line) {
final trimmed = line.trimRight();
final cells = <AsciiCell>[];
for (var i = 0; i < leftPadding; i++) {
cells.add(AsciiCell.empty);
}
for (var i = 0; i < trimmed.length; i++) {
cells.add(AsciiCell.fromChar(trimmed[i]));
}
while (cells.length < width) {
cells.add(AsciiCell.empty);
}
return cells;
}).toList();
}
}

View File

@@ -1,6 +1,6 @@
import 'package:asciineverdie/src/core/animation/ascii_animation_type.dart';
import 'package:asciineverdie/src/core/animation/canvas/ascii_cell.dart';
import 'package:asciineverdie/src/core/animation/canvas/ascii_layer.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용
///

View File

@@ -1,6 +1,6 @@
import 'package:asciineverdie/src/core/animation/canvas/ascii_cell.dart';
import 'package:asciineverdie/src/core/animation/canvas/ascii_layer.dart';
import 'package:asciineverdie/src/core/animation/race_character_frames.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용 /
///

View File

@@ -1,8 +1,8 @@
import 'package:asciineverdie/src/core/animation/background_data.dart';
import 'package:asciineverdie/src/core/animation/background_layer.dart';
import 'package:asciineverdie/src/core/animation/canvas/ascii_cell.dart';
import 'package:asciineverdie/src/core/animation/canvas/ascii_layer.dart';
import 'package:asciineverdie/src/core/animation/race_character_frames.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용
///

View File

@@ -0,0 +1,77 @@
/// 전투 텍스트 이펙트 프레임 데이터
///
/// CanvasBattleComposer에서 분리된 전투 텍스트 프레임 상수.
/// 크리티컬, 회피, 미스, 디버프, DOT, 블록, 패리 텍스트 프레임.
// ============================================================================
// 몬스터 공격 이펙트 (← 방향, Phase 8) - 5줄
// ============================================================================
/// 몬스터 공격 준비 프레임 (5줄)
const monsterPrepareFrames = <List<String>>[
[r' ', r' ', r' < ', r' ', r' '],
[r' ', r' _ ', r' << ', r' - ', r' '],
];
/// 몬스터 공격 프레임 (5줄)
const monsterAttackFrames = <List<String>>[
[r' ', r' __ ', r' <-- ', r' -- ', r' '],
[r' ', r' ___ ', r' <--- ', r' --- ', r' '],
[r' ', r' ____ ', r' <----- ', r' ---- ', r' '],
];
/// 몬스터 히트 프레임 (5줄)
const monsterHitFrames = <List<String>>[
[r' *SLASH!* ', r' **** ', r' <----- ', r' **** ', r' '],
[r'*ATTACK!* ', r' **** ', r' <---- ', r' **** ', r' '],
];
// ============================================================================
// 크리티컬 텍스트 프레임 (2줄, Phase 10)
// ============================================================================
/// 크리티컬 히트 텍스트 프레임 (반짝임 애니메이션)
const critTextFrames = <List<String>>[
[r'*CRITICAL!*', r' ========='],
[r'=CRITICAL!=', r' *********'],
];
// ============================================================================
// 전투 텍스트 이펙트 프레임 (Phase 11)
// ============================================================================
/// 회피 텍스트 프레임 (플레이어 회피 성공)
const evadeTextFrames = <List<String>>[
[r'*EVADE!*', r'========'],
[r'=EVADE!=', r'********'],
];
/// 미스 텍스트 프레임 (플레이어 공격 빗나감)
const missTextFrames = <List<String>>[
[r'*MISS!*', r'======='],
[r'=MISS!=', r'*******'],
];
/// 디버프 텍스트 프레임 (적에게 디버프 적용)
const debuffTextFrames = <List<String>>[
[r'*DEBUFF!*', r'========='],
[r'=DEBUFF!=', r'*********'],
];
/// DOT 텍스트 프레임 (지속 피해)
const dotTextFrames = <List<String>>[
[r'*DOT!*', r'======'],
[r'=DOT!=', r'******'],
];
/// 블록 텍스트 프레임 (방패 방어)
const blockTextFrames = <List<String>>[
[r'*BLOCK!*', r'========'],
[r'=BLOCK!=', r'********'],
];
/// 패리 텍스트 프레임 (무기 쳐내기)
const parryTextFrames = <List<String>>[
[r'*PARRY!*', r'========'],
[r'=PARRY!=', r'********'],
];

View File

@@ -0,0 +1,765 @@
import 'package:asciineverdie/src/shared/animation/ascii_animation_data.dart';
import 'package:asciineverdie/src/shared/animation/character_frames.dart';
import 'package:asciineverdie/src/shared/animation/monster_size.dart';
/// 몬스터 애니메이션 프레임 데이터
///
/// CanvasBattleComposer에서 분리된 몬스터 스프라이트 프레임.
/// 카테고리/크기/페이즈별 프레임을 제공한다.
/// 몬스터 애니메이션 프레임 반환 (페이즈별 다른 동작)
List<List<String>> getAnimatedMonsterFrames(
MonsterCategory category,
MonsterSize size,
BattlePhase phase,
) {
// 피격 상태
if (phase == BattlePhase.hit) {
return getMonsterHitFrames(category, size);
}
// 경계 상태 (prepare, attack)
if (phase == BattlePhase.prepare || phase == BattlePhase.attack) {
return _getMonsterAlertFrames(category, size);
}
// 일반 상태 (idle, recover)
return getMonsterIdleFrames(category, size);
}
/// 몬스터 Idle 프레임 가져오기 (외부에서 접근 가능)
///
/// 몬스터 사망 애니메이션에서 분해할 프레임을 가져올 때 사용
List<List<String>> getMonsterIdleFrames(
MonsterCategory category,
MonsterSize size,
) {
return switch (size) {
MonsterSize.tiny => _tinyIdleFrames(category),
MonsterSize.small => _smallIdleFrames(category),
MonsterSize.medium => _mediumIdleFrames(category),
// large, huge, giant, titanic 모두 8줄 (large 프레임 사용)
MonsterSize.large ||
MonsterSize.huge ||
MonsterSize.giant ||
MonsterSize.titanic => _largeIdleFrames(category),
};
}
List<List<String>> getMonsterHitFrames(
MonsterCategory category,
MonsterSize size,
) {
return switch (size) {
MonsterSize.tiny => _tinyHitFrames(category),
MonsterSize.small => _smallHitFrames(category),
MonsterSize.medium => _mediumHitFrames(category),
MonsterSize.large ||
MonsterSize.huge ||
MonsterSize.giant ||
MonsterSize.titanic => _largeHitFrames(category),
};
}
List<List<String>> _getMonsterAlertFrames(
MonsterCategory category,
MonsterSize size,
) {
return switch (size) {
MonsterSize.tiny => _tinyAlertFrames(category),
MonsterSize.small => _smallAlertFrames(category),
MonsterSize.medium => _mediumAlertFrames(category),
MonsterSize.large ||
MonsterSize.huge ||
MonsterSize.giant ||
MonsterSize.titanic => _largeAlertFrames(category),
};
}
// ============================================================================
// Tiny 몬스터 (2줄)
// ============================================================================
List<List<String>> _tinyIdleFrames(MonsterCategory category) {
return switch (category) {
MonsterCategory.bug => [
[r'(o.o)', r' |_|'],
[r'(o o)', r' |_|'],
],
MonsterCategory.malware => [
[r'<o_o>', r' \_/'],
[r'<o-o>', r' /_\'],
],
MonsterCategory.network => [
[r' @', r'/|\'],
[r' @', r'\|/'],
],
MonsterCategory.system => [
[r'[x_x]', r' /_\'],
[r'[X_X]', r' \_/'],
],
MonsterCategory.crypto => [
[r'$(.)~', r' /_\'],
[r'$(.)`', r' \_/'],
],
MonsterCategory.ai => [
[r'{o}', r'/_\'],
[r'{O}', r'\_/'],
],
MonsterCategory.boss => [
[r'(|o|)', r' V V'],
[r'(|O|)', r' v v'],
],
};
}
List<List<String>> _tinyHitFrames(MonsterCategory category) {
return [
[r'(*!*)', r' X_X'],
[r'(!*!)', r' x_x'],
];
}
List<List<String>> _tinyAlertFrames(MonsterCategory category) {
return switch (category) {
MonsterCategory.bug => [
[r'(O!O)', r' |!|'],
[r'(!O!)', r' |!|'],
],
MonsterCategory.malware => [
[r'<!_!>', r' \!/'],
[r'<!-!>', r' /!\'],
],
MonsterCategory.network => [
[r' @!', r'/|\'],
[r' !@', r'\|/'],
],
MonsterCategory.system => [
[r'[!_!]', r' /!\'],
[r'[!_!]', r' \!/'],
],
MonsterCategory.crypto => [
[r'$(!)~', r' /!\'],
[r'$(!)`', r' \!/'],
],
MonsterCategory.ai => [
[r'{!}', r'/!\'],
[r'{!}', r'\!/'],
],
MonsterCategory.boss => [
[r'(|!|)', r' V!V'],
[r'(|!|)', r' v!v'],
],
};
}
// ============================================================================
// Small 몬스터 (4줄)
// ============================================================================
List<List<String>> _smallIdleFrames(MonsterCategory category) {
return switch (category) {
MonsterCategory.bug => [
[r' /\_/\', r'( O.O )', r' > ^ <', r' /| |\'],
[r' /\_/\', r'( O O )', r' > v <', r' \| |/'],
],
MonsterCategory.malware => [
[r' /\/\', r' (O O)', r' / \', r' \/ \/'],
[r' \/\/\', r' (O O)', r' \ /', r' /\ /\'],
],
MonsterCategory.network => [
[r' O', r' /|\', r' / \', r' _| |_'],
[r' O', r' \|/', r' | |', r' _/ \_'],
],
MonsterCategory.system => [
[r' _+_', r' (x_x)', r' /|\', r' _/ \_'],
[r' _+_', r' (X_X)', r' \|/', r' _| |_'],
],
MonsterCategory.crypto => [
[r' __', r' <(oo)~', r' / \', r' <_ _>'],
[r' __', r' (oo)>', r' \ /', r' <_ _>'],
],
MonsterCategory.ai => [
[r' ___', r' ( )', r' ( )', r' \_/'],
[r' _', r' / \', r' { }', r' \_/'],
],
MonsterCategory.boss => [
[r' ^w^', r' (|o|)', r' /|\', r' V V'],
[r' ^W^', r' (|O|)', r' \|/', r' v v'],
],
};
}
List<List<String>> _smallHitFrames(MonsterCategory category) {
return [
[r' *!*', r' (>_<)', r' \X/', r' _/_\_'],
[r' !*!', r' (@_@)', r' /X\', r' _\_/_'],
];
}
List<List<String>> _smallAlertFrames(MonsterCategory category) {
return switch (category) {
MonsterCategory.bug => [
[r' /\_/\', r'( O!O )', r' > ! <', r' /| |\'],
[r' /\_/\', r'( !O! )', r' > ! <', r' \| |/'],
],
MonsterCategory.malware => [
[r' /\/\', r' (! !)', r' / \', r' \/ \/'],
[r' \/\/\', r' (! !)', r' \ /', r' /\ /\'],
],
MonsterCategory.network => [
[r' O!', r' /|\', r' / \', r' _| |_'],
[r' !O', r' \|/', r' | |', r' _/ \_'],
],
MonsterCategory.system => [
[r' _!_', r' (!_!)', r' /|\', r' _/ \_'],
[r' _!_', r' (!_!)', r' \|/', r' _| |_'],
],
MonsterCategory.crypto => [
[r' __', r' <(!!)~', r' / \', r' <_ _>'],
[r' __', r' (!!)>', r' \ /', r' <_ _>'],
],
MonsterCategory.ai => [
[r' ___', r' ( ! )', r' ( ! )', r' \_/'],
[r' _', r' /!\', r' { ! }', r' \_/'],
],
MonsterCategory.boss => [
[r' ^!^', r' (|!|)', r' /|\', r' V V'],
[r' ^!^', r' (|!|)', r' \|/', r' v v'],
],
};
}
// ============================================================================
// Medium 몬스터 (6줄)
// ============================================================================
List<List<String>> _mediumIdleFrames(MonsterCategory category) {
return switch (category) {
MonsterCategory.bug => [
[
r' /\_/\',
r' ( O.O )',
r' > ^ <',
r' /| |\',
r' | | | |',
r'_|_| |_|_',
],
[
r' /\_/\',
r' ( O O )',
r' > v <',
r' \| |/',
r' | | | |',
r'_|_| |_|_',
],
],
MonsterCategory.malware => [
[
r' /\/\',
r' /O O\',
r' \ /',
r' / \',
r' \/ \/',
r' _/ \_',
],
[
r' \/\/\',
r' \O O/',
r' / \',
r' \ /',
r' /\ /\',
r' _\ /_',
],
],
MonsterCategory.network => [
[r' O', r' /|\', r' / \', r' | |', r' | |', r' _| |_'],
[r' O', r' \|/', r' | |', r' | |', r' | |', r' _/ \_'],
],
MonsterCategory.system => [
[r' _+_', r' (X_X)', r' /|\', r' / | \', r' | | |', r'_/ | \_'],
[r' _x_', r' (x_x)', r' \|/', r' \ | /', r' | | |', r'_\ | /_'],
],
MonsterCategory.crypto => [
[
r' __',
r' <(OO)~',
r' / \',
r' / \',
r' | |',
r'<__ __>',
],
[
r' __',
r' (OO)>',
r' \ /',
r' \ /',
r' | |',
r'<__ __>',
],
],
MonsterCategory.ai => [
[
r' ____',
r' / \',
r' ( )',
r' ( )',
r' \ /',
r' \__/',
],
[
r' __',
r' / \',
r' / \',
r' { }',
r' \ /',
r' \__/',
],
],
MonsterCategory.boss => [
[r' ^W^', r' (|O|)', r' /|\', r' / | \', r' V V', r' _/ \_'],
[r' ^w^', r' (|o|)', r' \|/', r' \ | /', r' v v', r' _\ /_'],
],
};
}
List<List<String>> _mediumHitFrames(MonsterCategory category) {
return [
[r' *!*', r' (>.<)', r' \X/', r' / \', r' | |', r'_/_ \_\'],
[r' !*!', r' (@_@)', r' /X\', r' \ /', r' | |', r'_\_ /_/'],
];
}
List<List<String>> _mediumAlertFrames(MonsterCategory category) {
return switch (category) {
MonsterCategory.bug => [
[
r' /\_/\',
r' ( O!O )',
r' > ! <',
r' /| |\',
r' | | | |',
r'_|_| |_|_',
],
[
r' /\_/\',
r' ( !O! )',
r' > ! <',
r' \| |/',
r' | | | |',
r'_|_| |_|_',
],
],
MonsterCategory.malware => [
[
r' /\/\',
r' /! !\',
r' \ /',
r' / \',
r' \/ \/',
r' _/ \_',
],
[
r' \/\/\',
r' \! !/',
r' / \',
r' \ /',
r' /\ /\',
r' _\ /_',
],
],
MonsterCategory.network => [
[r' O!', r' /|\', r' / \', r' | |', r' | |', r' _| |_'],
[r' !O', r' \|/', r' | |', r' | |', r' | |', r' _/ \_'],
],
MonsterCategory.system => [
[r' _!_', r' (!_!)', r' /|\', r' / | \', r' | | |', r'_/ | \_'],
[r' _!_', r' (!_!)', r' \|/', r' \ | /', r' | | |', r'_\ | /_'],
],
MonsterCategory.crypto => [
[
r' __',
r' <(!!)~',
r' / \',
r' / \',
r' | |',
r'<__ __>',
],
[
r' __',
r' (!!)>',
r' \ /',
r' \ /',
r' | |',
r'<__ __>',
],
],
MonsterCategory.ai => [
[
r' ____',
r' / ! \',
r' ( ! )',
r' ( ! )',
r' \ /',
r' \__/',
],
[
r' __',
r' / !\',
r' / ! \',
r' { ! }',
r' \ /',
r' \__/',
],
],
MonsterCategory.boss => [
[r' ^!^', r' (|!|)', r' /|\', r' / | \', r' V V', r' _/ \_'],
[r' ^!^', r' (|!|)', r' \|/', r' \ | /', r' v v', r' _\ /_'],
],
};
}
// ============================================================================
// Large 몬스터 (8줄)
// ============================================================================
List<List<String>> _largeIdleFrames(MonsterCategory category) {
return switch (category) {
MonsterCategory.bug => [
[
r' /\__/\',
r' ( O O )',
r' > ^^ <',
r' /| |\',
r' | | | |',
r' | | | |',
r'_| | | |_',
r'|__|____|__|',
],
[
r' /\__/\',
r' ( O O )',
r' > vv <',
r' \| |/',
r' | | | |',
r' | | | |',
r'_| | | |_',
r'|__|____|__|',
],
],
MonsterCategory.malware => [
[
r' /\/\',
r' /O O\',
r' \ /',
r' / \',
r' / \',
r' \/ \/',
r' _/ \_',
r'/__ __\\',
],
[
r' \/\/\',
r' \O O/',
r' / \',
r' \ /',
r' \ /',
r' /\ /\',
r' _\ /_',
r'\__ __/',
],
],
MonsterCategory.network => [
[
r' O',
r' /|\',
r' / \',
r' | |',
r' | |',
r' | |',
r' _| |_',
r'|__ __|',
],
[
r' O',
r' \|/',
r' | |',
r' | |',
r' | |',
r' | |',
r' _/ \_',
r'/__ __\\',
],
],
MonsterCategory.system => [
[
r' _+_',
r' (X_X)',
r' /|\',
r' / | \',
r' | | |',
r' | | |',
r' _/ | \_',
r'|____|____|',
],
[
r' _x_',
r' (x_x)',
r' \|/',
r' \ | /',
r' | | |',
r' | | |',
r' _\ | /_',
r'|____|____|',
],
],
MonsterCategory.crypto => [
[
r' ___',
r' <(O O)~',
r' / \',
r' / \',
r' | |',
r' | |',
r' <__ __>',
r'|___ ___|',
],
[
r' ___',
r' (O O)>',
r' \ /',
r' \ /',
r' | |',
r' | |',
r' <__ __>',
r'|___ ___|',
],
],
MonsterCategory.ai => [
[
r' _____',
r' / \',
r' ( )',
r' ( )',
r' ( )',
r' \ /',
r' \ /',
r' \_/',
],
[
r' ___',
r' / \',
r' / \',
r' { }',
r' { }',
r' \ /',
r' \ /',
r' \_/',
],
],
MonsterCategory.boss => [
[
r' ^W^',
r' /|O|\',
r' /|\',
r' / | \',
r' | | |',
r' V | V',
r' _/ | \_',
r'|_____|_____|',
],
[
r' ^w^',
r' \|o|/',
r' \|/',
r' \ | /',
r' | | |',
r' v | v',
r' _\ | /_',
r'|_____|_____|',
],
],
};
}
List<List<String>> _largeHitFrames(MonsterCategory category) {
return [
[
r' *!*',
r' (>.<)',
r' \X/',
r' / | \',
r' | | |',
r' X | X',
r' _/ | \_',
r'|_____|_____|',
],
[
r' !*!',
r' (@_@)',
r' /X\',
r' \ | /',
r' | | |',
r' x | x',
r' _\ | /_',
r'|_____|_____|',
],
];
}
List<List<String>> _largeAlertFrames(MonsterCategory category) {
return switch (category) {
MonsterCategory.bug => [
[
r' /\__/\',
r' ( O!O )',
r' > !! <',
r' /| |\',
r' | | | |',
r' | | | |',
r'_| | | |_',
r'|__|____|__|',
],
[
r' /\__/\',
r' ( !O! )',
r' > !! <',
r' \| |/',
r' | | | |',
r' | | | |',
r'_| | | |_',
r'|__|____|__|',
],
],
MonsterCategory.malware => [
[
r' /\/\',
r' /! !\',
r' \ /',
r' / \',
r' / \',
r' \/ \/',
r' _/ \_',
r'/__ __\\',
],
[
r' \/\/\',
r' \! !/',
r' / \',
r' \ /',
r' \ /',
r' /\ /\',
r' _\ /_',
r'\__ __/',
],
],
MonsterCategory.network => [
[
r' O!',
r' /|\',
r' / \',
r' | |',
r' | |',
r' | |',
r' _| |_',
r'|__ __|',
],
[
r' !O',
r' \|/',
r' | |',
r' | |',
r' | |',
r' | |',
r' _/ \_',
r'/__ __\\',
],
],
MonsterCategory.system => [
[
r' _!_',
r' (!_!)',
r' /|\',
r' / | \',
r' | | |',
r' | | |',
r' _/ | \_',
r'|____|____|',
],
[
r' _!_',
r' (!_!)',
r' \|/',
r' \ | /',
r' | | |',
r' | | |',
r' _\ | /_',
r'|____|____|',
],
],
MonsterCategory.crypto => [
[
r' ___',
r' <(! !)~',
r' / \',
r' / \',
r' | |',
r' | |',
r' <__ __>',
r'|___ ___|',
],
[
r' ___',
r' (! !)>',
r' \ /',
r' \ /',
r' | |',
r' | |',
r' <__ __>',
r'|___ ___|',
],
],
MonsterCategory.ai => [
[
r' _____',
r' / ! \',
r' ( ! )',
r' ( ! )',
r' ( ! )',
r' \ /',
r' \ /',
r' \_/',
],
[
r' ___',
r' / ! \',
r' / ! \',
r' { ! }',
r' { ! }',
r' \ /',
r' \ /',
r' \_/',
],
],
MonsterCategory.boss => [
[
r' ^!^',
r' /|!|\',
r' /|\',
r' / | \',
r' | | |',
r' V | V',
r' _/ | \_',
r'|_____|_____|',
],
[
r' ^!^',
r' \|!|/',
r' \|/',
r' \ | /',
r' | | |',
r' v | v',
r' _\ | /_',
r'|_____|_____|',
],
],
};
}

View File

@@ -1,4 +1,4 @@
import 'package:asciineverdie/src/core/animation/canvas/ascii_cell.dart';
import 'package:asciineverdie/src/shared/animation/canvas/ascii_cell.dart';
import 'package:asciineverdie/src/core/model/item_stats.dart';
///

View File

@@ -1,7 +1,7 @@
// ASCII
// 3 × 6 ( 10 )
import 'package:asciineverdie/src/core/animation/character_frames.dart';
import 'package:asciineverdie/src/shared/animation/character_frames.dart';
///
class RaceCharacterFrames {

View File

@@ -1,4 +1,4 @@
import 'package:asciineverdie/src/core/animation/weapon_category.dart';
import 'package:asciineverdie/src/shared/animation/weapon_category.dart';
/// ASCII
///