feat(game): 게임 시스템 전면 개편 및 다국어 지원 확장

## 스킬 시스템 개선
- skill_data.dart: 스킬 데이터 구조 전면 개편 (+1176 라인)
- skill_service.dart: 스킬 발동 로직 확장 및 버프 시스템 연동
- skill.dart: 스킬 모델 개선, 쿨다운/효과 타입 추가

## Canvas 애니메이션 리팩토링
- battle_composer.dart 삭제 (레거시 위젯 기반 렌더러)
- monster_colors.dart 삭제 (AsciiCell 색상 시스템으로 통합)
- canvas_battle_composer.dart: z-index 정렬 (몬스터 z=1, 캐릭터 z=2, 이펙트 z=3)
- ascii_cell.dart, ascii_layer.dart: 코드 정리

## UI/UX 개선
- hp_mp_bar.dart: l10n 적용, 몬스터 HP 바 컴팩트화
- death_overlay.dart: 사망 화면 개선
- equipment_stats_panel.dart: 장비 스탯 표시 확장
- active_buff_panel.dart: 버프 패널 개선
- notification_overlay.dart: 알림 시스템 개선

## 다국어 지원 확장
- game_text_l10n.dart: 게임 텍스트 통합 (+758 라인)
- 한국어/일본어/영어/중국어 번역 업데이트
- ARB 파일 동기화

## 게임 로직 개선
- progress_service.dart: 진행 로직 리팩토링
- combat_calculator.dart: 전투 계산 로직 개선
- stat_calculator.dart: 스탯 계산 시스템 개선
- story_service.dart: 스토리 진행 로직 개선

## 기타
- theme_preferences.dart 삭제 (미사용)
- 테스트 파일 업데이트
- class_data.dart: 클래스 데이터 정리
This commit is contained in:
JiWoong Sul
2025-12-22 19:00:58 +09:00
parent f606fca063
commit 99f5b74802
63 changed files with 3403 additions and 2740 deletions

View File

@@ -1,815 +0,0 @@
// BattleComposer - 전투 프레임 실시간 합성
// Stone Story RPG 스타일 참고 - 8줄 캐릭터/몬스터, 60자 폭
// ASCII Patrol 스타일 패럴렉스 배경
import 'package:askiineverdie/src/core/animation/ascii_animation_data.dart';
import 'package:askiineverdie/src/core/animation/background_data.dart';
import 'package:askiineverdie/src/core/animation/background_layer.dart';
import 'package:askiineverdie/src/core/animation/character_frames.dart';
import 'package:askiineverdie/src/core/animation/monster_size.dart';
import 'package:askiineverdie/src/core/animation/weapon_category.dart';
import 'package:askiineverdie/src/core/animation/weapon_effects.dart';
/// 전투 프레임 합성기
class BattleComposer {
const BattleComposer({
required this.weaponCategory,
required this.hasShield,
required this.monsterCategory,
required this.monsterSize,
});
final WeaponCategory weaponCategory;
final bool hasShield;
final MonsterCategory monsterCategory;
final MonsterSize monsterSize;
/// 전체 프레임 폭 (문자 수)
static const int frameWidth = 60;
/// 프레임 높이 (줄 수)
static const int frameHeight = 8;
/// 영역 분할
static const int characterWidth = 18;
static const int effectWidth = 24;
static const int monsterWidth = 18;
/// 전투 프레임 생성 (배경 없음)
String composeFrame(BattlePhase phase, int subFrame, String? monsterBaseName) {
// 캐릭터 프레임
var charFrame = getCharacterFrame(phase, subFrame);
if (hasShield) {
charFrame = charFrame.withShield();
}
// 몬스터 프레임 (애니메이션 포함)
final monsterFrames =
_getAnimatedMonsterFrames(monsterCategory, monsterSize, phase);
final monsterFrame = monsterFrames[subFrame % monsterFrames.length];
// 무기 이펙트 (단일 라인)
final effect = getWeaponEffect(weaponCategory);
final effectLine = _getEffectLine(effect, phase, subFrame);
// 프레임 합성
return _compose(charFrame.lines, monsterFrame, effectLine, phase);
}
/// 전투 프레임 생성 (배경 포함, ASCII Patrol 스타일)
String composeFrameWithBackground(
BattlePhase phase,
int subFrame,
String? monsterBaseName,
EnvironmentType environment,
int globalTick,
) {
// 1. 8x60 캔버스 생성 (공백으로 초기화)
final canvas =
List.generate(frameHeight, (_) => List.filled(frameWidth, ' '));
// 2. 배경 레이어 그리기 (뒤에서 앞으로)
final layers = getBackgroundLayers(environment);
for (final layer in layers) {
_drawBackgroundLayer(canvas, layer, globalTick);
}
// 3. 캐릭터 프레임 (페이즈에 따라 X 위치 변경 - 근접 전투)
var charFrame = getCharacterFrame(phase, subFrame);
if (hasShield) {
charFrame = charFrame.withShield();
}
final normalizedChar = _normalizeSprite(charFrame.lines, characterWidth);
// 바닥 레이어(Y=7) 위에 서있도록 -1
final charY = frameHeight - normalizedChar.length - 1;
// 페이즈별 캐릭터 X 위치 (몬스터에게 접근)
final charX = switch (phase) {
BattlePhase.idle => 0,
BattlePhase.prepare => 12,
BattlePhase.attack => 24,
BattlePhase.hit => 28,
BattlePhase.recover => 8,
};
_overlaySpriteWithSpaces(canvas, normalizedChar, charX, charY);
// 4. 몬스터 프레임 (정규화하여 오른쪽 정렬)
// idle 프레임 기준 너비로 정렬하여 hit/alert 시 위치 이동 방지
final monsterRefWidth = _getMonsterReferenceWidth(monsterCategory, monsterSize);
final monsterFrames =
_getAnimatedMonsterFrames(monsterCategory, monsterSize, phase);
final monsterFrame = monsterFrames[subFrame % monsterFrames.length];
final normalizedMonster = _normalizeSpriteRight(
monsterFrame,
monsterWidth,
referenceWidth: monsterRefWidth,
);
final monsterX = frameWidth - monsterWidth;
// 바닥 레이어(Y=7) 위에 서있도록 -1
final monsterY = frameHeight - normalizedMonster.length - 1;
// 몬스터는 경계 내 완전 렌더링 (내부 공백에 배경이 비치지 않도록)
_overlaySpriteWithBounds(canvas, normalizedMonster, monsterX, monsterY);
// 5. 멀티라인 이펙트 오버레이 (공격/히트 페이즈)
if (phase == BattlePhase.attack || phase == BattlePhase.hit) {
final effect = getWeaponEffect(weaponCategory);
final effectLines = _getEffectLines(effect, phase, subFrame);
if (effectLines.isNotEmpty) {
// 이펙트 Y 위치: 캐릭터 머리 높이 (1번째 줄) 기준 - 수정됨
final effectY = charY;
// 이펙트 X 위치: 캐릭터 오른쪽에 붙여서 표시
final effectX = charX + 6;
for (var i = 0; i < effectLines.length; i++) {
final y = effectY + i;
if (y >= 0 && y < frameHeight && effectLines[i].isNotEmpty) {
_overlayText(canvas, effectLines[i], effectX, y);
}
}
}
}
// 6. 문자열로 변환
return canvas.map((row) => row.join()).join('\n');
}
/// 스프라이트를 지정 폭으로 정규화 (왼쪽 정렬)
List<String> _normalizeSprite(List<String> sprite, int width) {
return sprite.map((line) => line.padRight(width).substring(0, width)).toList();
}
/// 스프라이트를 지정 폭으로 정규화 (오른쪽 정렬, 전체 스프라이트 기준)
///
/// 모든 줄을 동일한 기준점에서 오른쪽 정렬하여
/// 머리와 몸통이 분리되지 않도록 함
///
/// [referenceWidth] 지정 시 해당 너비를 기준으로 정렬 (idle/hit 프레임 일관성용)
List<String> _normalizeSpriteRight(
List<String> sprite,
int width, {
int? referenceWidth,
}) {
// 1. 각 줄의 실제 너비(오른쪽 공백 제외) 계산
final trimmedLines = sprite.map((line) => line.trimRight()).toList();
// 2. 기준 너비 결정 (referenceWidth 있으면 사용, 없으면 현재 스프라이트 기준)
int maxLineWidth;
if (referenceWidth != null) {
maxLineWidth = referenceWidth;
} else {
maxLineWidth = 0;
for (final line in trimmedLines) {
if (line.length > maxLineWidth) {
maxLineWidth = line.length;
}
}
}
// 3. 전체 스프라이트를 오른쪽 정렬 (width 기준)
// 모든 줄에 동일한 왼쪽 패딩 적용
final leftPadding = width - maxLineWidth;
final paddingStr = leftPadding > 0 ? ' ' * leftPadding : '';
return trimmedLines.map((line) {
// 각 줄을 왼쪽에 공통 패딩 추가 후 width로 자르기
final paddedLine = paddingStr + line;
if (paddedLine.length > width) {
return paddedLine.substring(paddedLine.length - width);
}
return paddedLine.padRight(width);
}).toList();
}
/// 몬스터 스프라이트의 기준 너비 계산 (idle 프레임 기준)
int _getMonsterReferenceWidth(MonsterCategory category, MonsterSize size) {
final idleFrames = _getMonsterIdleFrames(category, size);
int maxWidth = 0;
for (final frame in idleFrames) {
for (final line in frame) {
final trimmedLength = line.trimRight().length;
if (trimmedLength > maxWidth) {
maxWidth = trimmedLength;
}
}
}
return maxWidth;
}
/// 스프라이트를 캔버스에 오버레이 (공백은 투명 처리)
void _overlaySpriteWithSpaces(
List<List<String>> canvas,
List<String> sprite,
int startX,
int startY,
) {
for (var i = 0; i < sprite.length; i++) {
final y = startY + i;
if (y < 0 || y >= frameHeight) continue;
final line = sprite[i];
for (var j = 0; j < line.length; j++) {
final x = startX + j;
if (x < 0 || x >= frameWidth) continue;
final char = line[j];
// 공백이 아닌 문자만 덮어쓰기 (투명 배경 효과)
if (char != ' ') {
canvas[y][x] = char;
}
}
}
}
/// 스프라이트를 캔버스에 오버레이 (라인별 경계 내 완전 렌더링)
///
/// 각 라인에서 첫 번째와 마지막 비공백 문자 사이의 모든 문자를 그림.
/// 내부 공백도 그려져서 스크롤링 배경이 비치지 않음.
void _overlaySpriteWithBounds(
List<List<String>> canvas,
List<String> sprite,
int startX,
int startY,
) {
for (var i = 0; i < sprite.length; i++) {
final y = startY + i;
if (y < 0 || y >= frameHeight) continue;
final line = sprite[i];
// 각 라인에서 첫/마지막 비공백 문자 위치 찾기
int firstNonSpace = -1;
int lastNonSpace = -1;
for (var j = 0; j < line.length; j++) {
if (line[j] != ' ') {
if (firstNonSpace == -1) firstNonSpace = j;
lastNonSpace = j;
}
}
if (firstNonSpace == -1) continue; // 빈 라인
// 경계 내 모든 문자 그리기 (공백 포함)
for (var j = firstNonSpace; j <= lastNonSpace; j++) {
final x = startX + j;
if (x < 0 || x >= frameWidth) continue;
canvas[y][x] = line[j];
}
}
}
/// 배경 레이어를 캔버스에 그리기
void _drawBackgroundLayer(
List<List<String>> canvas,
BackgroundLayer layer,
int globalTick,
) {
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;
// 스크롤 오프셋 계산
final offset = (globalTick * layer.scrollSpeed).toInt() % pattern.length;
// 패턴을 스크롤하며 그리기
for (var x = 0; x < frameWidth; x++) {
final patternIdx = (x + offset) % pattern.length;
final char = pattern[patternIdx];
if (char != ' ') {
canvas[y][x] = char;
}
}
}
}
/// 텍스트를 캔버스에 오버레이
void _overlayText(
List<List<String>> canvas,
String text,
int startX,
int y,
) {
if (y < 0 || y >= frameHeight) return;
for (var i = 0; i < text.length; i++) {
final x = startX + i;
if (x < 0 || x >= frameWidth) continue;
final char = text[i];
if (char != ' ') {
canvas[y][x] = char;
}
}
}
/// 멀티라인 이펙트 프레임 반환
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];
}
/// 단일 라인 이펙트 (하위 호환용)
String _getEffectLine(WeaponEffect effect, BattlePhase phase, int subFrame) {
final lines = _getEffectLines(effect, phase, subFrame);
if (lines.isEmpty) return '';
// 멀티라인 중 중간 라인 반환 (메인 이펙트)
final midIndex = lines.length ~/ 2;
return lines.length > midIndex ? lines[midIndex] : lines.first;
}
String _compose(
List<String> charLines,
List<String> monsterLines,
String effectLine,
BattlePhase phase,
) {
final result = <String>[];
// 캐릭터와 몬스터를 하단 정렬 (8줄 기준)
final charOffset = frameHeight - charLines.length;
final monsterOffset = frameHeight - monsterLines.length;
// 이펙트 Y 위치: 캐릭터 body/arm 줄 (charOffset + 1)
final effectRow = charOffset + 1;
for (var i = 0; i < frameHeight; i++) {
// 캐릭터 파트 (왼쪽 18자)
final charIdx = i - charOffset;
final charPart =
(charIdx >= 0 && charIdx < charLines.length ? charLines[charIdx] : '')
.padRight(characterWidth);
// 이펙트 파트 (중앙 24자) - 캐릭터 팔 높이에 표시
String effectPart = '';
if (i == effectRow &&
(phase == BattlePhase.attack || phase == BattlePhase.hit)) {
effectPart = effectLine;
}
effectPart = effectPart.padRight(effectWidth);
// 몬스터 파트 (오른쪽 18자)
final monsterIdx = i - monsterOffset;
final monsterPart = (monsterIdx >= 0 && monsterIdx < monsterLines.length
? monsterLines[monsterIdx]
: '')
.padLeft(monsterWidth);
result.add('$charPart$effectPart$monsterPart');
}
return result.join('\n');
}
}
// ============================================================================
// 몬스터 애니메이션 프레임
// ============================================================================
/// 몬스터 애니메이션 프레임 반환 (페이즈별 다른 동작)
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);
}
/// 일반 상태 몬스터 프레임
List<List<String>> _getMonsterIdleFrames(MonsterCategory category, MonsterSize size) {
return switch (size) {
MonsterSize.tiny => _tinyIdleFrames(category),
MonsterSize.small => _smallIdleFrames(category),
MonsterSize.medium => _mediumIdleFrames(category),
MonsterSize.large => _largeIdleFrames(category),
_ => _hugeIdleFrames(category), // huge 이상은 같은 프레임 사용
};
}
/// 피격 상태 몬스터 프레임
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 => _largeHitFrames(category),
_ => _hugeHitFrames(category),
};
}
/// 경계 상태 몬스터 프레임 (prepare/attack 시)
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 => _largeAlertFrames(category),
_ => _hugeAlertFrames(category),
};
}
// ============================================================================
// Tiny 몬스터 (2줄, 8줄 캔버스 하단 정렬)
// ============================================================================
List<List<String>> _tinyIdleFrames(MonsterCategory category) {
return switch (category) {
MonsterCategory.bug => [
[r'*', r'/\'],
[r'o', r'\/'],
],
MonsterCategory.malware => [
[r'><', r'\/'],
[r'<>', r'/\'],
],
MonsterCategory.network => [
[r'o', r'|'],
[r'O', r'|'],
],
MonsterCategory.system => [
[r'+', r'|'],
[r'x', r'|'],
],
MonsterCategory.crypto => [
[r'~<', r'>>'],
[r'<~', r'<<'],
],
MonsterCategory.ai => [
[r'()', r''],
[r'{}', r''],
],
MonsterCategory.boss => [
[r'^v', r'\/'],
[r'v^', r'/\'],
],
};
}
List<List<String>> _tinyHitFrames(MonsterCategory category) {
return [
[r'*!', r'><'],
[r'!*', r'<>'],
];
}
List<List<String>> _tinyAlertFrames(MonsterCategory category) {
return switch (category) {
MonsterCategory.bug => [
[r'!!', r'/\'],
[r'OO', r'><'],
],
MonsterCategory.malware => [
[r'!!', r'\/'],
[r'@@', r'/\'],
],
MonsterCategory.network => [
[r'O!', r'|'],
[r'!O', r'X'],
],
MonsterCategory.system => [
[r'!!', r'X'],
[r'@@', r'|'],
],
MonsterCategory.crypto => [
[r'!<', r'>>'],
[r'>!', r'<<'],
],
MonsterCategory.ai => [
[r'(!)', r''],
[r'{!}', r''],
],
MonsterCategory.boss => [
[r'^!', r'><'],
[r'!^', r'<>'],
],
};
}
// ============================================================================
// 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' _\+/_', 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' | / \ |', r' _|/ \|_', r'|___/\\___|'],
[r' !*!*!', r' (@___@)', r' //X\\', r' \ /\\/ /', r' | //\\ |', r' | \ / |', 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'|____|____|'],
],
};
}
// ============================================================================
// Huge+ 몬스터 (8줄, 더 넓게)
// ============================================================================
List<List<String>> _hugeIdleFrames(MonsterCategory category) {
return switch (category) {
MonsterCategory.bug => [
[r' /\____/\', r' ( O O )', r' > ^^^^ <', r' /| |\', r' | | | |', r' | | | |', r' _| | | |_', r'|___|______|___|'],
[r' /\____/\', r' ( O O )', r' > vvvv <', 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' _\\+/_', 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>> _hugeHitFrames(MonsterCategory category) {
return [
[r' *!*!*!*', r' (>_____<)', r' \\\\X////', r' / \\\\// \\', r' | \\\\/ |', r' | / \\ |', r' _|/ \\|_', r'|____/\\\\___|'],
[r' !*!*!*!', r' (@_____@)', r' ////X\\\\', r' \\ /\\\\/ /', r' | ////\\\\ |', r' | \\ / |', r' _|\\ /|_', r'|____\\\\/___|'],
];
}
List<List<String>> _hugeAlertFrames(MonsterCategory category) {
return switch (category) {
MonsterCategory.bug => [
[r' /\____/\', r' ( ! ! )', r' > !!!! <', r' /| |\', r' | | | |', r' | | | |', r' _| | | |_', r'|___|______|___|'],
[r' /\____/\', r' ( ! ! )', 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'|_____|_____|'],
],
};
}
// 레거시 호환용 함수
List<List<String>> getMonsterFrames(MonsterCategory category, MonsterSize size) {
return _getMonsterIdleFrames(category, size);
}

View File

@@ -15,10 +15,7 @@ enum AsciiCellColor {
/// 단일 ASCII 셀 데이터
class AsciiCell {
const AsciiCell({
required this.char,
this.color = AsciiCellColor.object,
});
const AsciiCell({required this.char, this.color = AsciiCellColor.object});
/// 표시할 문자 (단일 문자)
final String char;
@@ -45,10 +42,7 @@ class AsciiCell {
/// 문자열에서 AsciiCell 생성 (자동 색상)
factory AsciiCell.fromChar(String char) {
if (char.isEmpty || char == ' ') return empty;
return AsciiCell(
char: char,
color: colorFromChar(char),
);
return AsciiCell(char: char, color: colorFromChar(char));
}
@override

View File

@@ -37,11 +37,7 @@ class AsciiLayer {
}
/// 빈 레이어 생성
factory AsciiLayer.empty({
int width = 60,
int height = 8,
int zIndex = 0,
}) {
factory AsciiLayer.empty({int width = 60, int height = 8, int zIndex = 0}) {
final cells = List.generate(
height,
(_) => List.filled(width, AsciiCell.empty),

View File

@@ -279,8 +279,7 @@ List<List<String>> _getMonsterIdleFrames(
MonsterSize.large ||
MonsterSize.huge ||
MonsterSize.giant ||
MonsterSize.titanic =>
_largeIdleFrames(category),
MonsterSize.titanic => _largeIdleFrames(category),
};
}
@@ -295,8 +294,7 @@ List<List<String>> _getMonsterHitFrames(
MonsterSize.large ||
MonsterSize.huge ||
MonsterSize.giant ||
MonsterSize.titanic =>
_largeHitFrames(category),
MonsterSize.titanic => _largeHitFrames(category),
};
}
@@ -311,8 +309,7 @@ List<List<String>> _getMonsterAlertFrames(
MonsterSize.large ||
MonsterSize.huge ||
MonsterSize.giant ||
MonsterSize.titanic =>
_largeAlertFrames(category),
MonsterSize.titanic => _largeAlertFrames(category),
};
}
@@ -483,7 +480,7 @@ List<List<String>> _mediumIdleFrames(MonsterCategory category) {
r' > ^ <',
r' /| |\',
r' | | | |',
r'_|_| |_|_'
r'_|_| |_|_',
],
[
r' /\_/\',
@@ -491,7 +488,7 @@ List<List<String>> _mediumIdleFrames(MonsterCategory category) {
r' > v <',
r' \| |/',
r' | | | |',
r'_|_| |_|_'
r'_|_| |_|_',
],
],
MonsterCategory.malware => [
@@ -501,7 +498,7 @@ List<List<String>> _mediumIdleFrames(MonsterCategory category) {
r' \ /',
r' / \',
r' \/ \/',
r' _/ \_'
r' _/ \_',
],
[
r' \/\/\',
@@ -509,7 +506,7 @@ List<List<String>> _mediumIdleFrames(MonsterCategory category) {
r' / \',
r' \ /',
r' /\ /\',
r' _\ /_'
r' _\ /_',
],
],
MonsterCategory.network => [
@@ -517,22 +514,8 @@ List<List<String>> _mediumIdleFrames(MonsterCategory category) {
[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'_\ | /_'
],
[r' _+_', r' (X_X)', r' /|\', r' / | \', r' | | |', r'_/ | \_'],
[r' _x_', r' (x_x)', r' \|/', r' \ | /', r' | | |', r'_\ | /_'],
],
MonsterCategory.crypto => [
[
@@ -541,7 +524,7 @@ List<List<String>> _mediumIdleFrames(MonsterCategory category) {
r' / \',
r' / \',
r' | |',
r'<__ __>'
r'<__ __>',
],
[
r' __',
@@ -549,12 +532,26 @@ List<List<String>> _mediumIdleFrames(MonsterCategory category) {
r' \ /',
r' \ /',
r' | |',
r'<__ __>'
r'<__ __>',
],
],
MonsterCategory.ai => [
[r' ____', r' / \', r' ( )', r' ( )', r' \ /', r' \__/'],
[r' __', r' / \', 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' V V', r' _/ \_'],
@@ -579,7 +576,7 @@ List<List<String>> _mediumAlertFrames(MonsterCategory category) {
r' > ! <',
r' /| |\',
r' | | | |',
r'_|_| |_|_'
r'_|_| |_|_',
],
[
r' /\_/\',
@@ -587,7 +584,7 @@ List<List<String>> _mediumAlertFrames(MonsterCategory category) {
r' > ! <',
r' \| |/',
r' | | | |',
r'_|_| |_|_'
r'_|_| |_|_',
],
],
MonsterCategory.malware => [
@@ -597,7 +594,7 @@ List<List<String>> _mediumAlertFrames(MonsterCategory category) {
r' \ /',
r' / \',
r' \/ \/',
r' _/ \_'
r' _/ \_',
],
[
r' \/\/\',
@@ -605,7 +602,7 @@ List<List<String>> _mediumAlertFrames(MonsterCategory category) {
r' / \',
r' \ /',
r' /\ /\',
r' _\ /_'
r' _\ /_',
],
],
MonsterCategory.network => [
@@ -613,22 +610,8 @@ List<List<String>> _mediumAlertFrames(MonsterCategory category) {
[r' !O', r' \|/', r' | |', r' | |', r' | |', r' _/ \_'],
],
MonsterCategory.system => [
[
r' _!_',
r' (!_!)',
r' /|\',
r' / | \',
r' | | |',
r'_/ | \_'
],
[
r' _!_',
r' (!_!)',
r' \|/',
r' \ | /',
r' | | |',
r'_\ | /_'
],
[r' _!_', r' (!_!)', r' /|\', r' / | \', r' | | |', r'_/ | \_'],
[r' _!_', r' (!_!)', r' \|/', r' \ | /', r' | | |', r'_\ | /_'],
],
MonsterCategory.crypto => [
[
@@ -637,7 +620,7 @@ List<List<String>> _mediumAlertFrames(MonsterCategory category) {
r' / \',
r' / \',
r' | |',
r'<__ __>'
r'<__ __>',
],
[
r' __',
@@ -645,7 +628,7 @@ List<List<String>> _mediumAlertFrames(MonsterCategory category) {
r' \ /',
r' \ /',
r' | |',
r'<__ __>'
r'<__ __>',
],
],
MonsterCategory.ai => [
@@ -655,9 +638,16 @@ List<List<String>> _mediumAlertFrames(MonsterCategory category) {
r' ( ! )',
r' ( ! )',
r' \ /',
r' \__/'
r' \__/',
],
[
r' __',
r' / !\',
r' / ! \',
r' { ! }',
r' \ /',
r' \__/',
],
[r' __', r' / !\', r' / ! \', r' { ! }', r' \ /', r' \__/'],
],
MonsterCategory.boss => [
[r' ^!^', r' (|!|)', r' /|\', r' / | \', r' V V', r' _/ \_'],
@@ -681,7 +671,7 @@ List<List<String>> _largeIdleFrames(MonsterCategory category) {
r' | | | |',
r' | | | |',
r'_| | | |_',
r'|__|____|__|'
r'|__|____|__|',
],
[
r' /\__/\',
@@ -691,7 +681,7 @@ List<List<String>> _largeIdleFrames(MonsterCategory category) {
r' | | | |',
r' | | | |',
r'_| | | |_',
r'|__|____|__|'
r'|__|____|__|',
],
],
MonsterCategory.malware => [
@@ -703,7 +693,7 @@ List<List<String>> _largeIdleFrames(MonsterCategory category) {
r' / \',
r' \/ \/',
r' _/ \_',
r'/__ __\\'
r'/__ __\\',
],
[
r' \/\/\',
@@ -713,7 +703,7 @@ List<List<String>> _largeIdleFrames(MonsterCategory category) {
r' \ /',
r' /\ /\',
r' _\ /_',
r'\__ __/'
r'\__ __/',
],
],
MonsterCategory.network => [
@@ -725,7 +715,7 @@ List<List<String>> _largeIdleFrames(MonsterCategory category) {
r' | |',
r' | |',
r' _| |_',
r'|__ __|'
r'|__ __|',
],
[
r' O',
@@ -735,7 +725,7 @@ List<List<String>> _largeIdleFrames(MonsterCategory category) {
r' | |',
r' | |',
r' _/ \_',
r'/__ __\\'
r'/__ __\\',
],
],
MonsterCategory.system => [
@@ -747,7 +737,7 @@ List<List<String>> _largeIdleFrames(MonsterCategory category) {
r' | | |',
r' | | |',
r' _/ | \_',
r'|____|____|'
r'|____|____|',
],
[
r' _x_',
@@ -757,7 +747,7 @@ List<List<String>> _largeIdleFrames(MonsterCategory category) {
r' | | |',
r' | | |',
r' _\ | /_',
r'|____|____|'
r'|____|____|',
],
],
MonsterCategory.crypto => [
@@ -769,7 +759,7 @@ List<List<String>> _largeIdleFrames(MonsterCategory category) {
r' | |',
r' | |',
r' <__ __>',
r'|___ ___|'
r'|___ ___|',
],
[
r' ___',
@@ -779,7 +769,7 @@ List<List<String>> _largeIdleFrames(MonsterCategory category) {
r' | |',
r' | |',
r' <__ __>',
r'|___ ___|'
r'|___ ___|',
],
],
MonsterCategory.ai => [
@@ -791,7 +781,7 @@ List<List<String>> _largeIdleFrames(MonsterCategory category) {
r' ( )',
r' \ /',
r' \ /',
r' \_/'
r' \_/',
],
[
r' ___',
@@ -801,7 +791,7 @@ List<List<String>> _largeIdleFrames(MonsterCategory category) {
r' { }',
r' \ /',
r' \ /',
r' \_/'
r' \_/',
],
],
MonsterCategory.boss => [
@@ -813,7 +803,7 @@ List<List<String>> _largeIdleFrames(MonsterCategory category) {
r' | | |',
r' V | V',
r' _/ | \_',
r'|_____|_____|'
r'|_____|_____|',
],
[
r' ^w^',
@@ -823,7 +813,7 @@ List<List<String>> _largeIdleFrames(MonsterCategory category) {
r' | | |',
r' v | v',
r' _\ | /_',
r'|_____|_____|'
r'|_____|_____|',
],
],
};
@@ -839,7 +829,7 @@ List<List<String>> _largeHitFrames(MonsterCategory category) {
r' | | |',
r' X | X',
r' _/ | \_',
r'|_____|_____|'
r'|_____|_____|',
],
[
r' !*!',
@@ -849,7 +839,7 @@ List<List<String>> _largeHitFrames(MonsterCategory category) {
r' | | |',
r' x | x',
r' _\ | /_',
r'|_____|_____|'
r'|_____|_____|',
],
];
}
@@ -865,7 +855,7 @@ List<List<String>> _largeAlertFrames(MonsterCategory category) {
r' | | | |',
r' | | | |',
r'_| | | |_',
r'|__|____|__|'
r'|__|____|__|',
],
[
r' /\__/\',
@@ -875,7 +865,7 @@ List<List<String>> _largeAlertFrames(MonsterCategory category) {
r' | | | |',
r' | | | |',
r'_| | | |_',
r'|__|____|__|'
r'|__|____|__|',
],
],
MonsterCategory.malware => [
@@ -887,7 +877,7 @@ List<List<String>> _largeAlertFrames(MonsterCategory category) {
r' / \',
r' \/ \/',
r' _/ \_',
r'/__ __\\'
r'/__ __\\',
],
[
r' \/\/\',
@@ -897,7 +887,7 @@ List<List<String>> _largeAlertFrames(MonsterCategory category) {
r' \ /',
r' /\ /\',
r' _\ /_',
r'\__ __/'
r'\__ __/',
],
],
MonsterCategory.network => [
@@ -909,7 +899,7 @@ List<List<String>> _largeAlertFrames(MonsterCategory category) {
r' | |',
r' | |',
r' _| |_',
r'|__ __|'
r'|__ __|',
],
[
r' !O',
@@ -919,7 +909,7 @@ List<List<String>> _largeAlertFrames(MonsterCategory category) {
r' | |',
r' | |',
r' _/ \_',
r'/__ __\\'
r'/__ __\\',
],
],
MonsterCategory.system => [
@@ -931,7 +921,7 @@ List<List<String>> _largeAlertFrames(MonsterCategory category) {
r' | | |',
r' | | |',
r' _/ | \_',
r'|____|____|'
r'|____|____|',
],
[
r' _!_',
@@ -941,7 +931,7 @@ List<List<String>> _largeAlertFrames(MonsterCategory category) {
r' | | |',
r' | | |',
r' _\ | /_',
r'|____|____|'
r'|____|____|',
],
],
MonsterCategory.crypto => [
@@ -953,7 +943,7 @@ List<List<String>> _largeAlertFrames(MonsterCategory category) {
r' | |',
r' | |',
r' <__ __>',
r'|___ ___|'
r'|___ ___|',
],
[
r' ___',
@@ -963,7 +953,7 @@ List<List<String>> _largeAlertFrames(MonsterCategory category) {
r' | |',
r' | |',
r' <__ __>',
r'|___ ___|'
r'|___ ___|',
],
],
MonsterCategory.ai => [
@@ -975,7 +965,7 @@ List<List<String>> _largeAlertFrames(MonsterCategory category) {
r' ( ! )',
r' \ /',
r' \ /',
r' \_/'
r' \_/',
],
[
r' ___',
@@ -985,7 +975,7 @@ List<List<String>> _largeAlertFrames(MonsterCategory category) {
r' { ! }',
r' \ /',
r' \ /',
r' \_/'
r' \_/',
],
],
MonsterCategory.boss => [
@@ -997,7 +987,7 @@ List<List<String>> _largeAlertFrames(MonsterCategory category) {
r' | | |',
r' V | V',
r' _/ | \_',
r'|_____|_____|'
r'|_____|_____|',
],
[
r' ^!^',
@@ -1007,7 +997,7 @@ List<List<String>> _largeAlertFrames(MonsterCategory category) {
r' | | |',
r' v | v',
r' _\ | /_',
r'|_____|_____|'
r'|_____|_____|',
],
],
};

View File

@@ -20,12 +20,18 @@ class CanvasSpecialComposer {
) {
return switch (type) {
AsciiAnimationType.levelUp => _composeLevelUp(frameIndex, globalTick),
AsciiAnimationType.questComplete =>
_composeQuestComplete(frameIndex, globalTick),
AsciiAnimationType.actComplete =>
_composeActComplete(frameIndex, globalTick),
AsciiAnimationType.resurrection =>
_composeResurrection(frameIndex, globalTick),
AsciiAnimationType.questComplete => _composeQuestComplete(
frameIndex,
globalTick,
),
AsciiAnimationType.actComplete => _composeActComplete(
frameIndex,
globalTick,
),
AsciiAnimationType.resurrection => _composeResurrection(
frameIndex,
globalTick,
),
_ => [AsciiLayer.empty()],
};
}
@@ -44,7 +50,8 @@ class CanvasSpecialComposer {
final layers = <AsciiLayer>[
_createEffectBackground(globalTick, '+'),
_createCenteredSprite(
_questCompleteFrames[frameIndex % _questCompleteFrames.length]),
_questCompleteFrames[frameIndex % _questCompleteFrames.length],
),
];
return layers;
}
@@ -54,7 +61,8 @@ class CanvasSpecialComposer {
final layers = <AsciiLayer>[
_createEffectBackground(globalTick, '~'),
_createCenteredSprite(
_actCompleteFrames[frameIndex % _actCompleteFrames.length]),
_actCompleteFrames[frameIndex % _actCompleteFrames.length],
),
];
return layers;
}
@@ -64,7 +72,8 @@ class CanvasSpecialComposer {
final layers = <AsciiLayer>[
_createEffectBackground(globalTick, '.'),
_createCenteredSprite(
_resurrectionFrames[frameIndex % _resurrectionFrames.length]),
_resurrectionFrames[frameIndex % _resurrectionFrames.length],
),
];
return layers;
}
@@ -119,41 +128,11 @@ class CanvasSpecialComposer {
// ============================================================================
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' | | ',
],
[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' | | '],
];
// ============================================================================
@@ -161,34 +140,10 @@ const _levelUpFrames = [
// ============================================================================
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' / \ ',
],
[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' / \ '],
];
// ============================================================================
@@ -196,34 +151,10 @@ const _questCompleteFrames = [
// ============================================================================
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' | | ',
],
[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' | | '],
];
// ============================================================================
@@ -232,38 +163,13 @@ const _actCompleteFrames = [
const _resurrectionFrames = [
// 프레임 1: R.I.P 묘비
[
r' ___ ',
r' |RIP| ',
r' | | ',
r'__|___|__',
],
[r' ___ ', r' |RIP| ', r' | | ', r'__|___|__'],
// 프레임 2: 빛 내림
[
r' \|/ ',
r' -|R|- ',
r' | | ',
r'__|___|__',
],
[r' \|/ ', r' -|R|- ', r' | | ', r'__|___|__'],
// 프레임 3: 일어남
[
r' \o/ ',
r' --|-- ',
r' | | ',
r'__|___|__',
],
[r' \o/ ', r' --|-- ', r' | | ', r'__|___|__'],
// 프레임 4: 서있음
[
r' o ',
r' /|\ ',
r' / \ ',
r'_________',
],
[r' o ', r' /|\ ', r' / \ ', r'_________'],
// 프레임 5: 부활 완료
[
r' REVIVED ',
r' \o/ ',
r' | ',
r'___/ \___',
],
[r' REVIVED ', r' \o/ ', r' | ', r'___/ \___'],
];

View File

@@ -63,12 +63,7 @@ class CanvasTownComposer {
const shopX = 32;
final shopY = frameHeight - cells.length - 1;
return AsciiLayer(
cells: cells,
zIndex: 1,
offsetX: shopX,
offsetY: shopY,
);
return AsciiLayer(cells: cells, zIndex: 1, offsetX: shopX, offsetY: shopY);
}
/// 캐릭터 레이어 생성 (z=2)
@@ -82,12 +77,7 @@ class CanvasTownComposer {
const charX = 25;
final charY = frameHeight - cells.length - 1;
return AsciiLayer(
cells: cells,
zIndex: 2,
offsetX: charX,
offsetY: charY,
);
return AsciiLayer(cells: cells, zIndex: 2, offsetX: charX, offsetY: charY);
}
/// 문자열 스프라이트를 AsciiCell 2D 배열로 변환
@@ -104,27 +94,11 @@ class CanvasTownComposer {
const _shopIdleFrames = [
// 프레임 1: 기본
[
r' o ',
r' /|\ ',
r' / \ ',
],
[r' o ', r' /|\ ', r' / \ '],
// 프레임 2: 머리 숙임
[
r' o ',
r' /|~ ',
r' / \ ',
],
[r' o ', r' /|~ ', r' / \ '],
// 프레임 3: 물건 보기
[
r' o? ',
r' /| ',
r' / \ ',
],
[r' o? ', r' /| ', r' / \ '],
// 프레임 4: 고개 끄덕
[
r' o! ',
r' /|\ ',
r' / \ ',
],
[r' o! ', r' /|\ ', r' / \ '],
];

View File

@@ -62,26 +62,10 @@ CharacterFrame getCharacterFrame(BattlePhase phase, int subFrame) {
// 구조: [머리, 몸통+팔, 다리]
// ============================================================================
const _idleFrames = [
CharacterFrame([
r' o ',
r' /|\ ',
r' / \ ',
]),
CharacterFrame([
r' o ',
r' /|\ ',
r' | | ',
]),
CharacterFrame([
r' o ',
r' /|\ ',
r' / \ ',
]),
CharacterFrame([
r' O ',
r' /|\ ',
r' / \ ',
]),
CharacterFrame([r' o ', r' /|\ ', r' / \ ']),
CharacterFrame([r' o ', r' /|\ ', r' | | ']),
CharacterFrame([r' o ', r' /|\ ', r' / \ ']),
CharacterFrame([r' O ', r' /|\ ', r' / \ ']),
];
// ============================================================================
@@ -89,21 +73,9 @@ const _idleFrames = [
// 구조: [머리, 몸통+팔, 다리]
// ============================================================================
const _prepareFrames = [
CharacterFrame([
r' o ',
r' \|\ ',
r' / \ ',
]),
CharacterFrame([
r' o_ ',
r' \| ',
r' / \ ',
]),
CharacterFrame([
r' o/ ',
r' \| ',
r' / \ ',
]),
CharacterFrame([r' o ', r' \|\ ', r' / \ ']),
CharacterFrame([r' o_ ', r' \| ', r' / \ ']),
CharacterFrame([r' o/ ', r' \| ', r' / \ ']),
];
// ============================================================================
@@ -112,31 +84,11 @@ const _prepareFrames = [
// 수정: 공격 이펙트를 머리 줄로 통일 (1칸 위로)
// ============================================================================
const _attackFrames = [
CharacterFrame([
r' o\ ',
r' /| ',
r' / \ ',
]),
CharacterFrame([
r' o- ',
r' /| ',
r' / \ ',
]),
CharacterFrame([
r' o-- ',
r' /| ',
r' / \ ',
]),
CharacterFrame([
r' o-=>',
r' /| ',
r' / \ ',
]),
CharacterFrame([
r' o ',
r' /|\ ',
r' / \ ',
]),
CharacterFrame([r' o\ ', r' /| ', r' / \ ']),
CharacterFrame([r' o- ', r' /| ', r' / \ ']),
CharacterFrame([r' o-- ', r' /| ', r' / \ ']),
CharacterFrame([r' o-=>', r' /| ', r' / \ ']),
CharacterFrame([r' o ', r' /|\ ', r' / \ ']),
];
// ============================================================================
@@ -145,21 +97,9 @@ const _attackFrames = [
// 수정: 히트 이펙트를 머리 줄로 통일 (1칸 위로)
// ============================================================================
const _hitFrames = [
CharacterFrame([
r' o-* ',
r' /| ',
r' / \ ',
]),
CharacterFrame([
r' o=* ',
r' /| ',
r' / \ ',
]),
CharacterFrame([
r' o~* ',
r' /| ',
r' / \ ',
]),
CharacterFrame([r' o-* ', r' /| ', r' / \ ']),
CharacterFrame([r' o=* ', r' /| ', r' / \ ']),
CharacterFrame([r' o~* ', r' /| ', r' / \ ']),
];
// ============================================================================
@@ -167,19 +107,7 @@ const _hitFrames = [
// 구조: [머리, 몸통+팔, 다리]
// ============================================================================
const _recoverFrames = [
CharacterFrame([
r' o ',
r' /|\ ',
r' | ',
]),
CharacterFrame([
r' o ',
r' /|\ ',
r' / \ ',
]),
CharacterFrame([
r' o ',
r' /|\ ',
r' / \ ',
]),
CharacterFrame([r' o ', r' /|\ ', r' | ']),
CharacterFrame([r' o ', r' /|\ ', r' / \ ']),
CharacterFrame([r' o ', r' /|\ ', r' / \ ']),
];

View File

@@ -1,192 +0,0 @@
// 몬스터 카테고리별 색상 시스템
// 각 몬스터 카테고리에 따라 다른 색상 적용
import 'dart:ui';
/// 몬스터 카테고리 (ascii_animation_data.dart의 MonsterCategory와 매칭)
enum MonsterColorCategory {
beast,
insect,
humanoid,
undead,
dragon,
slime,
demon,
}
/// 몬스터 색상 정보
class MonsterColors {
const MonsterColors({
required this.normal,
required this.hit,
});
/// 일반 상태 색상
final Color normal;
/// 피격 상태 색상
final Color hit;
}
/// 카테고리별 몬스터 색상 반환
MonsterColors getMonsterColors(MonsterColorCategory category) {
return switch (category) {
MonsterColorCategory.beast => const MonsterColors(
normal: Color(0xFF00FF00), // 녹색
hit: Color(0xFFFF0000), // 빨강
),
MonsterColorCategory.insect => const MonsterColors(
normal: Color(0xFFFFFF00), // 노랑
hit: Color(0xFFFF6600), // 주황
),
MonsterColorCategory.humanoid => const MonsterColors(
normal: Color(0xFF00FFFF), // 시안
hit: Color(0xFFFF00FF), // 마젠타
),
MonsterColorCategory.undead => const MonsterColors(
normal: Color(0xFF9966FF), // 보라
hit: Color(0xFFCCCCCC), // 회색
),
MonsterColorCategory.dragon => const MonsterColors(
normal: Color(0xFFFF6600), // 주황
hit: Color(0xFFFFFF00), // 노랑
),
MonsterColorCategory.slime => const MonsterColors(
normal: Color(0xFF66FF66), // 연녹색
hit: Color(0xFF00CC00), // 진녹색
),
MonsterColorCategory.demon => const MonsterColors(
normal: Color(0xFFFF0066), // 핑크
hit: Color(0xFFFFFFFF), // 흰색
),
};
}
/// 몬스터 기본 이름에서 색상 카테고리 추론
///
/// ascii_animation_data.dart의 getMonsterCategory 결과를 변환
MonsterColorCategory getMonsterColorCategory(String? baseName) {
if (baseName == null || baseName.isEmpty) {
return MonsterColorCategory.beast;
}
final lower = baseName.toLowerCase();
// insect (곤충류)
if (_matchesAny(lower, _insectKeywords)) {
return MonsterColorCategory.insect;
}
// undead (언데드)
if (_matchesAny(lower, _undeadKeywords)) {
return MonsterColorCategory.undead;
}
// dragon (드래곤류)
if (_matchesAny(lower, _dragonKeywords)) {
return MonsterColorCategory.dragon;
}
// slime (슬라임류)
if (_matchesAny(lower, _slimeKeywords)) {
return MonsterColorCategory.slime;
}
// demon (악마류)
if (_matchesAny(lower, _demonKeywords)) {
return MonsterColorCategory.demon;
}
// humanoid (인간형)
if (_matchesAny(lower, _humanoidKeywords)) {
return MonsterColorCategory.humanoid;
}
// 기본은 beast
return MonsterColorCategory.beast;
}
bool _matchesAny(String text, List<String> keywords) {
return keywords.any((kw) => text.contains(kw));
}
const _insectKeywords = [
'bug',
'beetle',
'spider',
'ant',
'bee',
'wasp',
'moth',
'worm',
'larva',
'crawler',
'centipede',
'scorpion',
];
const _undeadKeywords = [
'zombie',
'skeleton',
'ghost',
'wraith',
'vampire',
'lich',
'specter',
'phantom',
'revenant',
'undead',
'corpse',
'bone',
];
const _dragonKeywords = [
'dragon',
'drake',
'wyrm',
'wyvern',
'serpent',
'hydra',
'basilisk',
];
const _slimeKeywords = [
'slime',
'ooze',
'blob',
'jelly',
'pudding',
'gel',
'goo',
];
const _demonKeywords = [
'demon',
'devil',
'imp',
'fiend',
'daemon',
'succubus',
'incubus',
'hell',
'infernal',
];
const _humanoidKeywords = [
'goblin',
'orc',
'troll',
'ogre',
'giant',
'bandit',
'knight',
'mage',
'wizard',
'warrior',
'guard',
'soldier',
'cultist',
'hacker',
'admin',
'user',
];

View File

@@ -65,12 +65,7 @@ bool _matchesAny(String text, List<String> keywords) {
// 카테고리별 키워드 목록
const _cosmicKeywords = [
'dyson',
'black hole',
'universe',
'singularity',
];
const _cosmicKeywords = ['dyson', 'black hole', 'universe', 'singularity'];
const _cableKeywords = [
'cable',

View File

@@ -58,7 +58,7 @@ const _bluntEffect = WeaponEffect(
[r' _/ ', r' / ', r'/ '],
[r' /__ ', r'/ ', r' '],
[r'/__ ', r' ', r' '],
[r'/__=>', r' ', r' '],
[r'/__=>', r' ', r' '],
],
hitFrames: [
[r' *BASH* ', r'/__=> ', r' '],