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:
File diff suppressed because it is too large
Load Diff
@@ -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 {
|
||||
@@ -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) {
|
||||
@@ -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 캐시 키
|
||||
@@ -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 포함)
|
||||
@@ -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 렌더러용)
|
||||
///
|
||||
544
lib/src/shared/animation/canvas/canvas_battle_composer.dart
Normal file
544
lib/src/shared/animation/canvas/canvas_battle_composer.dart
Normal 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();
|
||||
}
|
||||
}
|
||||
@@ -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용 특수 이벤트 애니메이션 합성기
|
||||
///
|
||||
@@ -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용 마을/상점 애니메이션 합성기
|
||||
///
|
||||
@@ -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용 걷기 애니메이션 합성기
|
||||
///
|
||||
77
lib/src/shared/animation/canvas/combat_text_frames.dart
Normal file
77
lib/src/shared/animation/canvas/combat_text_frames.dart
Normal 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'********'],
|
||||
];
|
||||
765
lib/src/shared/animation/canvas/monster_frames.dart
Normal file
765
lib/src/shared/animation/canvas/monster_frames.dart
Normal 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'|_____|_____|',
|
||||
],
|
||||
],
|
||||
};
|
||||
}
|
||||
@@ -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';
|
||||
|
||||
/// 아이템 희귀도와 애니메이션 색상 간의 매핑
|
||||
@@ -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 {
|
||||
@@ -1,4 +1,4 @@
|
||||
import 'package:asciineverdie/src/core/animation/weapon_category.dart';
|
||||
import 'package:asciineverdie/src/shared/animation/weapon_category.dart';
|
||||
|
||||
/// 무기 카테고리별 공격 이펙트 ASCII 프레임
|
||||
///
|
||||
Reference in New Issue
Block a user