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:
@@ -1,760 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:asciineverdie/src/core/animation/ascii_animation_type.dart';
|
||||
|
||||
/// ASCII 애니메이션 프레임 데이터
|
||||
class AsciiAnimationData {
|
||||
const AsciiAnimationData({required this.frames, this.frameIntervalMs = 200});
|
||||
|
||||
/// 각 프레임 (문자열, 최소 5줄)
|
||||
final List<String> frames;
|
||||
|
||||
/// 프레임 간격 (밀리초)
|
||||
final int frameIntervalMs;
|
||||
}
|
||||
|
||||
/// 터미널 색상 테마
|
||||
enum AsciiColorTheme {
|
||||
/// 클래식 녹색 터미널
|
||||
green,
|
||||
|
||||
/// 엠버 (호박색) 터미널
|
||||
amber,
|
||||
|
||||
/// 화이트 온 블랙
|
||||
white,
|
||||
|
||||
/// 시스템 테마 (라이트/다크 모드 따름)
|
||||
system,
|
||||
}
|
||||
|
||||
/// 테마별 색상 데이터
|
||||
class AsciiThemeColors {
|
||||
const AsciiThemeColors({
|
||||
required this.textColor,
|
||||
required this.backgroundColor,
|
||||
});
|
||||
|
||||
final Color textColor;
|
||||
final Color backgroundColor;
|
||||
}
|
||||
|
||||
/// 테마별 색상 반환
|
||||
AsciiThemeColors getThemeColors(AsciiColorTheme theme, Brightness brightness) {
|
||||
return switch (theme) {
|
||||
AsciiColorTheme.green => const AsciiThemeColors(
|
||||
textColor: Color(0xFF00FF00),
|
||||
backgroundColor: Color(0xFF0D0D0D),
|
||||
),
|
||||
AsciiColorTheme.amber => const AsciiThemeColors(
|
||||
textColor: Color(0xFFFFB000),
|
||||
backgroundColor: Color(0xFF1A1000),
|
||||
),
|
||||
AsciiColorTheme.white => const AsciiThemeColors(
|
||||
textColor: Color(0xFFE0E0E0),
|
||||
backgroundColor: Color(0xFF121212),
|
||||
),
|
||||
AsciiColorTheme.system =>
|
||||
brightness == Brightness.dark
|
||||
? const AsciiThemeColors(
|
||||
textColor: Color(0xFFE0E0E0),
|
||||
backgroundColor: Color(0xFF1E1E1E),
|
||||
)
|
||||
: const AsciiThemeColors(
|
||||
textColor: Color(0xFF1E1E1E),
|
||||
backgroundColor: Color(0xFFF5F5F5),
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
/// 몬스터 카테고리 (ASCII NEVER DIE 테마)
|
||||
enum MonsterCategory {
|
||||
/// 기본 버그 (Syntax Error, Type Mismatch 등)
|
||||
bug,
|
||||
|
||||
/// 멀웨어 (Virus, Worm, Trojan 등)
|
||||
malware,
|
||||
|
||||
/// 네트워크 위협 (Flood, DDoS, Injection 등)
|
||||
network,
|
||||
|
||||
/// 시스템 위협 (Kernel, Memory, Buffer 등)
|
||||
system,
|
||||
|
||||
/// 암호화/보안 (Encryption, Hash, Zero-Day 등)
|
||||
crypto,
|
||||
|
||||
/// AI/ML 위협 (Neural Network, Machine Learning 등)
|
||||
ai,
|
||||
|
||||
/// 보스 몬스터
|
||||
boss,
|
||||
}
|
||||
|
||||
/// 몬스터 이름으로 카테고리 결정 (ASCII NEVER DIE 테마)
|
||||
MonsterCategory getMonsterCategory(String? monsterBaseName) {
|
||||
if (monsterBaseName == null || monsterBaseName.isEmpty) {
|
||||
return MonsterCategory.bug;
|
||||
}
|
||||
|
||||
final name = monsterBaseName.toLowerCase();
|
||||
|
||||
// 보스 몬스터
|
||||
if (name.startsWith('boss:') ||
|
||||
name.contains('dragon') ||
|
||||
name.contains('hydra') ||
|
||||
name.contains('titan') ||
|
||||
name.contains('leviathan') ||
|
||||
name.contains('colossus') ||
|
||||
name.contains('emperor') ||
|
||||
name.contains('singularity') ||
|
||||
name.contains('primordial') ||
|
||||
name.contains('glitch god')) {
|
||||
return MonsterCategory.boss;
|
||||
}
|
||||
|
||||
// AI/ML 위협
|
||||
if (name.contains('neural') ||
|
||||
name.contains('ai ') ||
|
||||
name.contains('deep') ||
|
||||
name.contains('model') ||
|
||||
name.contains('adversarial') ||
|
||||
name.contains('training') ||
|
||||
name.contains('federated') ||
|
||||
name.contains('prompt') ||
|
||||
name.contains('hallucination')) {
|
||||
return MonsterCategory.ai;
|
||||
}
|
||||
|
||||
// 암호화/보안 위협
|
||||
if (name.contains('quantum') ||
|
||||
name.contains('zero day') ||
|
||||
name.contains('zero-day') ||
|
||||
name.contains('crypto') ||
|
||||
name.contains('hash') ||
|
||||
name.contains('blockchain') ||
|
||||
name.contains('homomorphic') ||
|
||||
name.contains('zero knowledge') ||
|
||||
name.contains('smart contract')) {
|
||||
return MonsterCategory.crypto;
|
||||
}
|
||||
|
||||
// 네트워크 위협
|
||||
if (name.contains('flood') ||
|
||||
name.contains('ddos') ||
|
||||
name.contains('dos') ||
|
||||
name.contains('injection') ||
|
||||
name.contains('sql') ||
|
||||
name.contains('xss') ||
|
||||
name.contains('csrf') ||
|
||||
name.contains('amplification') ||
|
||||
name.contains('tunnel') ||
|
||||
name.contains('shell') ||
|
||||
name.contains('backdoor') ||
|
||||
name.contains('c2') ||
|
||||
name.contains('beacon')) {
|
||||
return MonsterCategory.network;
|
||||
}
|
||||
|
||||
// 시스템 위협
|
||||
if (name.contains('kernel') ||
|
||||
name.contains('memory') ||
|
||||
name.contains('buffer') ||
|
||||
name.contains('stack') ||
|
||||
name.contains('heap') ||
|
||||
name.contains('overflow') ||
|
||||
name.contains('corruption') ||
|
||||
name.contains('segfault') ||
|
||||
name.contains('panic') ||
|
||||
name.contains('rootkit') ||
|
||||
name.contains('firmware') ||
|
||||
name.contains('bootkit')) {
|
||||
return MonsterCategory.system;
|
||||
}
|
||||
|
||||
// 멀웨어
|
||||
if (name.contains('virus') ||
|
||||
name.contains('worm') ||
|
||||
name.contains('trojan') ||
|
||||
name.contains('ransomware') ||
|
||||
name.contains('malware') ||
|
||||
name.contains('botnet') ||
|
||||
name.contains('cryptominer') ||
|
||||
name.contains('keylogger') ||
|
||||
name.contains('spyware') ||
|
||||
name.contains('dropper') ||
|
||||
name.contains('loader') ||
|
||||
name.contains('payload')) {
|
||||
return MonsterCategory.malware;
|
||||
}
|
||||
|
||||
// 기본: 버그
|
||||
return MonsterCategory.bug;
|
||||
}
|
||||
|
||||
/// 버그 전투 애니메이션 (기본 버그 모양)
|
||||
const battleAnimationBug = AsciiAnimationData(
|
||||
frames: [
|
||||
// 프레임 1: 대치
|
||||
'''
|
||||
o /\\_/\\
|
||||
/|\\ ( o.o )
|
||||
/ \\ > ^ <''',
|
||||
// 프레임 2: 접근
|
||||
'''
|
||||
o /\\_/\\
|
||||
/|\\ ( o.o )
|
||||
/ \\ > ^ <''',
|
||||
// 프레임 3: 공격 (근접)
|
||||
'''
|
||||
o_/ /\\_/\\
|
||||
/| ( >.< )
|
||||
/ \\ > ^ <''',
|
||||
// 프레임 4: 히트
|
||||
'''
|
||||
o **** /\\_/\\
|
||||
/|\\ *** ( X.X )
|
||||
/ \\ > ~ <''',
|
||||
// 프레임 5: 복귀 (승리 포즈)
|
||||
'''
|
||||
\\o/ /\\_/\\
|
||||
/|\\ ( -.-)
|
||||
/ \\ > ^ <''',
|
||||
],
|
||||
frameIntervalMs: 220,
|
||||
);
|
||||
|
||||
/// 마을/상점 애니메이션 (8줄 x 40자 고정)
|
||||
/// 캐릭터 위치: 머리=4, 몸통=5, 다리=6 (전투 애니메이션 기준)
|
||||
const townAnimation = AsciiAnimationData(
|
||||
frames: [
|
||||
// 프레임 1: 상점 앞 대기
|
||||
' ___________ \n'
|
||||
' / SHOP \\ \n'
|
||||
' | [======] | \n'
|
||||
' | @@@@ | \n'
|
||||
' | ITEMS | o \n'
|
||||
' | | /|\\ \n'
|
||||
' |___________| / \\ \n'
|
||||
'~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~',
|
||||
// 프레임 2: 이동 중
|
||||
' ___________ \n'
|
||||
' / SHOP \\ \n'
|
||||
' | [======] | \n'
|
||||
' | @@@@ | \n'
|
||||
' | ITEMS | o \n'
|
||||
' | | /|\\ \n'
|
||||
' |___________| / \\ \n'
|
||||
'~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~',
|
||||
// 프레임 3: 거래 시작
|
||||
' ___________ \n'
|
||||
' / SHOP \\ \n'
|
||||
' | [======] | \$ \n'
|
||||
' | @@@@ | \$ \n'
|
||||
' | ITEMS | o \$ \n'
|
||||
' | | /|\\ \n'
|
||||
' |___________| / \\ \n'
|
||||
'~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~',
|
||||
// 프레임 4: 거래 중
|
||||
' ___________ \n'
|
||||
' / SHOP \\ \n'
|
||||
' | [<====>] | \$\$\$ \n'
|
||||
' | @@@@ | \$\$\$ \n'
|
||||
' | SOLD! | o \n'
|
||||
' | | /|\\ \n'
|
||||
' |___________| / \\ \n'
|
||||
'~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~',
|
||||
// 프레임 5: 거래 완료
|
||||
' ___________ \n'
|
||||
' / SHOP \\ \n'
|
||||
' | [======] | + \n'
|
||||
' | @@@@ | + \n'
|
||||
' | ITEMS | \\o/ \n'
|
||||
' | | /|\\ \n'
|
||||
' |___________| / \\ \n'
|
||||
'~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~',
|
||||
],
|
||||
frameIntervalMs: 280,
|
||||
);
|
||||
|
||||
/// 걷는 애니메이션 (8줄 x 40자 고정)
|
||||
const walkingAnimation = AsciiAnimationData(
|
||||
frames: [
|
||||
// 프레임 1: 양발 벌림
|
||||
' \n'
|
||||
' \n'
|
||||
' \n'
|
||||
' \n'
|
||||
' o \n'
|
||||
' /|\\ \n'
|
||||
' / \\ \n'
|
||||
'~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~',
|
||||
// 프레임 2: 왼발 앞으로
|
||||
' \n'
|
||||
' \n'
|
||||
' \n'
|
||||
' \n'
|
||||
' o \n'
|
||||
' /|\\ \n'
|
||||
' /| \n'
|
||||
'~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~',
|
||||
// 프레임 3: 두 발 모음
|
||||
' \n'
|
||||
' \n'
|
||||
' \n'
|
||||
' \n'
|
||||
' o \n'
|
||||
' /|\\ \n'
|
||||
' || \n'
|
||||
'~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~',
|
||||
// 프레임 4: 오른발 앞으로
|
||||
' \n'
|
||||
' \n'
|
||||
' \n'
|
||||
' \n'
|
||||
' o \n'
|
||||
' /|\\ \n'
|
||||
' |\\ \n'
|
||||
'~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~',
|
||||
// 프레임 5: 양발 벌림 (복귀)
|
||||
' \n'
|
||||
' \n'
|
||||
' \n'
|
||||
' \n'
|
||||
' o \n'
|
||||
' /|\\ \n'
|
||||
' / \\ \n'
|
||||
'~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~',
|
||||
],
|
||||
frameIntervalMs: 180,
|
||||
);
|
||||
|
||||
/// 멀웨어 전투 애니메이션 (바이러스 모양)
|
||||
const battleAnimationMalware = AsciiAnimationData(
|
||||
frames: [
|
||||
// 프레임 1: 대치
|
||||
'''
|
||||
o /\\_/\\
|
||||
/|\\ ( o o )
|
||||
/ \\ /|=====|\\''',
|
||||
// 프레임 2: 접근
|
||||
'''
|
||||
o /\\_/\\
|
||||
/|\\ ( o o )
|
||||
/ \\ /|=====|\\''',
|
||||
// 프레임 3: 공격 (근접)
|
||||
'''
|
||||
o_/ /\\_/\\
|
||||
/| ( >.< )
|
||||
/ \\ /|=====|\\''',
|
||||
// 프레임 4: 히트
|
||||
'''
|
||||
o **** /\\_/\\
|
||||
/|\\*** ( X X )
|
||||
/ \\ /|=====|\\''',
|
||||
// 프레임 5: 복귀 (승리 포즈)
|
||||
'''
|
||||
\\o/ /\\_/\\
|
||||
/|\\ ( - - )
|
||||
/ \\ /|=====|\\''',
|
||||
],
|
||||
frameIntervalMs: 220,
|
||||
);
|
||||
|
||||
/// 네트워크 전투 애니메이션 (패킷 모양)
|
||||
const battleAnimationNetwork = AsciiAnimationData(
|
||||
frames: [
|
||||
// 프레임 1: 대치
|
||||
'''
|
||||
o O
|
||||
/|\\ /|\\
|
||||
/ \\ / | \\''',
|
||||
// 프레임 2: 접근
|
||||
'''
|
||||
o O
|
||||
/|\\ /|\\
|
||||
/ \\ / | \\''',
|
||||
// 프레임 3: 공격 (근접)
|
||||
'''
|
||||
o_/ O
|
||||
/| X|X
|
||||
/ \\ / | \\''',
|
||||
// 프레임 4: 히트
|
||||
'''
|
||||
o **** O
|
||||
/|\\ *** X|X
|
||||
/ \\ / | \\''',
|
||||
// 프레임 5: 복귀 (승리 포즈)
|
||||
'''
|
||||
\\o/ O
|
||||
/|\\ /|\\
|
||||
/ \\ / | \\''',
|
||||
],
|
||||
frameIntervalMs: 220,
|
||||
);
|
||||
|
||||
/// 시스템 전투 애니메이션 (커널 모양)
|
||||
const battleAnimationSystem = AsciiAnimationData(
|
||||
frames: [
|
||||
// 프레임 1: 대치
|
||||
'''
|
||||
o .-.
|
||||
/|\\ (o.o)
|
||||
/ \\ |=|''',
|
||||
// 프레임 2: 접근
|
||||
'''
|
||||
o .-.
|
||||
/|\\ (o.o)
|
||||
/ \\ |=|''',
|
||||
// 프레임 3: 공격 (근접)
|
||||
'''
|
||||
o_/ .-.
|
||||
/| (>.>)
|
||||
/ \\ |=|''',
|
||||
// 프레임 4: 히트
|
||||
'''
|
||||
o **** .-.
|
||||
/|\\*** (X.X)
|
||||
/ \\ |~|''',
|
||||
// 프레임 5: 복귀 (승리 포즈)
|
||||
'''
|
||||
\\o/ .-.
|
||||
/|\\ (-.-)
|
||||
/ \\ |=|''',
|
||||
],
|
||||
frameIntervalMs: 250,
|
||||
);
|
||||
|
||||
/// 암호화 전투 애니메이션 (자물쇠 모양)
|
||||
const battleAnimationCrypto = AsciiAnimationData(
|
||||
frames: [
|
||||
// 프레임 1: 대치
|
||||
'''
|
||||
o __/\\__
|
||||
/|\\ < (O)(O) >
|
||||
/ \\ \\ \\/ /''',
|
||||
// 프레임 2: 접근
|
||||
'''
|
||||
o __/\\__
|
||||
/|\\ < (O)(O) >
|
||||
/ \\ \\ \\/ /''',
|
||||
// 프레임 3: 공격 (근접)
|
||||
'''
|
||||
o_/ __/\\__
|
||||
/| < (X)(X) >
|
||||
/ \\ \\ \\/ /''',
|
||||
// 프레임 4: 히트
|
||||
'''
|
||||
o **** __/\\__
|
||||
/|\\***< (X)(X) >
|
||||
/ \\ \\ ~~ /''',
|
||||
// 프레임 5: 복귀 (승리 포즈)
|
||||
'''
|
||||
\\o/ __/\\__
|
||||
/|\\ < (-)(-)>
|
||||
/ \\ \\ \\/ /''',
|
||||
],
|
||||
frameIntervalMs: 200,
|
||||
);
|
||||
|
||||
/// AI 전투 애니메이션 (뉴럴넷 모양)
|
||||
const battleAnimationAI = AsciiAnimationData(
|
||||
frames: [
|
||||
// 프레임 1: 대치
|
||||
'''
|
||||
o .---.
|
||||
/|\\ ( o o )
|
||||
/ \\ ~~~~~''',
|
||||
// 프레임 2: 접근
|
||||
'''
|
||||
o .---.
|
||||
/|\\ ( o o )
|
||||
/ \\ ~~~~~''',
|
||||
// 프레임 3: 공격 (근접)
|
||||
'''
|
||||
o_/ .---.
|
||||
/| ( >.< )
|
||||
/ \\ ~~~~~''',
|
||||
// 프레임 4: 히트
|
||||
'''
|
||||
o **** .---.
|
||||
/|\\** ( X X )
|
||||
/ \\ ~~~~~''',
|
||||
// 프레임 5: 복귀 (승리 포즈)
|
||||
'''
|
||||
\\o/ .---.
|
||||
/|\\ ( - - )
|
||||
/ \\ ~~~~~''',
|
||||
],
|
||||
frameIntervalMs: 280,
|
||||
);
|
||||
|
||||
/// 보스 전투 애니메이션 (드래곤/보스 모양)
|
||||
const battleAnimationBoss = AsciiAnimationData(
|
||||
frames: [
|
||||
// 프레임 1: 대치
|
||||
'''
|
||||
o /\\ /\\
|
||||
/|\\ ( o V o )
|
||||
/ \\ \\ ~~~ /''',
|
||||
// 프레임 2: 접근
|
||||
'''
|
||||
o /\\ /\\
|
||||
/|\\ ( o V o )
|
||||
/ \\ \\ ~~~ /''',
|
||||
// 프레임 3: 공격 (근접)
|
||||
'''
|
||||
o_/ /\\ /\\
|
||||
/| ( X V X )
|
||||
/ \\ \\ ~~~ /''',
|
||||
// 프레임 4: 히트
|
||||
'''
|
||||
o ****/\\ /\\
|
||||
/|\\** ( X V X )
|
||||
/ \\ \\ ~~~ /''',
|
||||
// 프레임 5: 복귀 (승리 포즈)
|
||||
'''
|
||||
\\o/ /\\ /\\
|
||||
/|\\ ( - V - )
|
||||
/ \\ \\ ~~~ /''',
|
||||
],
|
||||
frameIntervalMs: 200,
|
||||
);
|
||||
|
||||
/// 몬스터 카테고리별 전투 애니메이션 반환
|
||||
AsciiAnimationData getBattleAnimation(MonsterCategory category) {
|
||||
return switch (category) {
|
||||
MonsterCategory.bug => battleAnimationBug,
|
||||
MonsterCategory.malware => battleAnimationMalware,
|
||||
MonsterCategory.network => battleAnimationNetwork,
|
||||
MonsterCategory.system => battleAnimationSystem,
|
||||
MonsterCategory.crypto => battleAnimationCrypto,
|
||||
MonsterCategory.ai => battleAnimationAI,
|
||||
MonsterCategory.boss => battleAnimationBoss,
|
||||
};
|
||||
}
|
||||
|
||||
/// 레벨업 축하 애니메이션 (8줄 x 40자 고정)
|
||||
/// 캐릭터 위치: 머리=4, 몸통=5, 다리=6 (전투 애니메이션 기준)
|
||||
const levelUpAnimation = AsciiAnimationData(
|
||||
frames: [
|
||||
// 프레임 1: 시작
|
||||
' * * * \n'
|
||||
' * * * \n'
|
||||
' * * \n'
|
||||
' \n'
|
||||
' * \\O/ * \n'
|
||||
' /|\\ \n'
|
||||
' * / \\ * \n'
|
||||
'~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~',
|
||||
// 프레임 2: 별 확산
|
||||
' * * * \n'
|
||||
' * * \n'
|
||||
' * * \n'
|
||||
' \n'
|
||||
' * \\O/ * \n'
|
||||
' /|\\ \n'
|
||||
' * / \\ * \n'
|
||||
'~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~',
|
||||
// 프레임 3: 레벨업 텍스트
|
||||
' * L E V E L U P ! * \n'
|
||||
' * * \n'
|
||||
' * * \n'
|
||||
' \n'
|
||||
' * \\O/ * \n'
|
||||
' /|\\ \n'
|
||||
' * / \\ * \n'
|
||||
'~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~',
|
||||
// 프레임 4: 빛나는 캐릭터
|
||||
' * * * * * \n'
|
||||
' * * \n'
|
||||
' * * * * \n'
|
||||
' \n'
|
||||
' * \\O/ * \n'
|
||||
' * /|\\ * \n'
|
||||
' * / \\ * \n'
|
||||
'~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~',
|
||||
// 프레임 5: 마무리
|
||||
' + \n'
|
||||
' +++ \n'
|
||||
' +++++ \n'
|
||||
' \n'
|
||||
' \\O/ \n'
|
||||
' /|\\ \n'
|
||||
' / \\ \n'
|
||||
'~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~',
|
||||
],
|
||||
frameIntervalMs: 300,
|
||||
);
|
||||
|
||||
/// 퀘스트 완료 애니메이션 (8줄 x 40자 고정)
|
||||
/// 캐릭터 위치: 머리=4, 몸통=5, 다리=6 (전투 애니메이션 기준)
|
||||
const questCompleteAnimation = AsciiAnimationData(
|
||||
frames: [
|
||||
// 프레임 1: 퀘스트 깃발
|
||||
' [=======] \n'
|
||||
' || || \n'
|
||||
' || || \n'
|
||||
' ||_____|| \n'
|
||||
' \\O/ \n'
|
||||
' /|\\ \n'
|
||||
' / \\ \n'
|
||||
'~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~',
|
||||
// 프레임 2: 승리
|
||||
' [QUEST!] \n'
|
||||
' || || \n'
|
||||
' || || \n'
|
||||
' ||_____|| \n'
|
||||
' \\\\O// \n'
|
||||
' /|\\ \n'
|
||||
' / \\ \n'
|
||||
'~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~',
|
||||
// 프레임 3: 보상
|
||||
' COMPLETE! \n'
|
||||
' \n'
|
||||
' \n'
|
||||
' \$\$\$ \n'
|
||||
' \\O/ \$\$\$ \n'
|
||||
' /|\\ \$\$\$ \n'
|
||||
' / \\ \n'
|
||||
'~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~',
|
||||
// 프레임 4: 축하
|
||||
' * * * * * \n'
|
||||
' \n'
|
||||
' \n'
|
||||
' * * * * * \n'
|
||||
' \\O/ +EXP \n'
|
||||
' /|\\ +GOLD \n'
|
||||
' / \\ \n'
|
||||
'~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~',
|
||||
// 프레임 5: 마무리
|
||||
' [ VICTORY! ] \n'
|
||||
' \n'
|
||||
' \n'
|
||||
' \n'
|
||||
' \\O/ \n'
|
||||
' /|\\ \n'
|
||||
' / \\ \n'
|
||||
'~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~',
|
||||
],
|
||||
frameIntervalMs: 350,
|
||||
);
|
||||
|
||||
/// Act 완료 애니메이션 (8줄 x 40자 고정)
|
||||
/// 캐릭터 위치: 머리=4, 몸통=5, 다리=6 (전투 애니메이션 기준)
|
||||
const actCompleteAnimation = AsciiAnimationData(
|
||||
frames: [
|
||||
// 프레임 1: 커튼
|
||||
' ______________________________ \n'
|
||||
' | A C T | \n'
|
||||
' | C O M P L E T E | \n'
|
||||
' |______________________________| \n'
|
||||
' \n'
|
||||
' \n'
|
||||
' \n'
|
||||
'~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~',
|
||||
// 프레임 2: 캐릭터 등장
|
||||
' ______________________________ \n'
|
||||
' | * * * * * | \n'
|
||||
' | * * * * * | \n'
|
||||
' |______________________________| \n'
|
||||
' \\O/ \n'
|
||||
' /|\\ \n'
|
||||
' / \\ \n'
|
||||
'~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~',
|
||||
// 프레임 3: 플롯 진행 표시
|
||||
' ______________________________ \n'
|
||||
' | PROLOGUE --> ACT | \n'
|
||||
' | STORY CONTINUES --> | \n'
|
||||
' |______________________________| \n'
|
||||
' \\O/ \n'
|
||||
' /|\\ \n'
|
||||
' / \\ \n'
|
||||
'~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~',
|
||||
// 프레임 4: 축하
|
||||
' ______________________________ \n'
|
||||
' | * * * * * | \n'
|
||||
' | * * * * * | \n'
|
||||
' |______________________________| \n'
|
||||
' * \\O/ * \n'
|
||||
' /|\\ \n'
|
||||
' * / \\ * \n'
|
||||
'~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~',
|
||||
// 프레임 5: 마무리
|
||||
' ______________________________ \n'
|
||||
' | +---------+ | \n'
|
||||
' | | NEXT | | \n'
|
||||
' |____| CHAPTER |_______________| \n'
|
||||
' \\O/ \n'
|
||||
' /|\\ \n'
|
||||
' / \\ \n'
|
||||
'~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~',
|
||||
],
|
||||
frameIntervalMs: 400,
|
||||
);
|
||||
|
||||
/// 부활 애니메이션 (8줄 x 40자 고정)
|
||||
/// 어둠에서 빛으로, 캐릭터가 다시 일어남
|
||||
const resurrectionAnimation = AsciiAnimationData(
|
||||
frames: [
|
||||
// 프레임 1: 어둠
|
||||
' \n'
|
||||
' . . . . . . \n'
|
||||
' . . \n'
|
||||
' . R . I . P . \n'
|
||||
' . ___ . \n'
|
||||
' . |___| . \n'
|
||||
' . . . . . . \n'
|
||||
'~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~',
|
||||
// 프레임 2: 빛이 비침
|
||||
' * \n'
|
||||
' . .|. . . . \n'
|
||||
' . | . \n'
|
||||
' . | . \n'
|
||||
' . _O_ . \n'
|
||||
' . /___\\ . \n'
|
||||
' . . . . . . \n'
|
||||
'~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~',
|
||||
// 프레임 3: 일어나는 중
|
||||
' * * * \n'
|
||||
' . | . . . \n'
|
||||
' . | . \n'
|
||||
' . O . \n'
|
||||
' . /|\\ . \n'
|
||||
' . | | . \n'
|
||||
' . . . . . . \n'
|
||||
'~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~',
|
||||
// 프레임 4: 서있음
|
||||
' * * * * * \n'
|
||||
' \n'
|
||||
' \n'
|
||||
' O \n'
|
||||
' /|\\ \n'
|
||||
' / \\ \n'
|
||||
' \n'
|
||||
'~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~',
|
||||
// 프레임 5: 부활 완료
|
||||
' * RESURRECTED! * \n'
|
||||
' \n'
|
||||
' \n'
|
||||
' \\O/ \n'
|
||||
' /|\\ \n'
|
||||
' / \\ \n'
|
||||
' \n'
|
||||
'~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~',
|
||||
],
|
||||
frameIntervalMs: 600, // 5프레임 × 600ms = 3초
|
||||
);
|
||||
|
||||
/// 타입별 애니메이션 데이터 반환 (기본 전투는 bug)
|
||||
AsciiAnimationData getAnimationData(AsciiAnimationType type) {
|
||||
return switch (type) {
|
||||
AsciiAnimationType.battle => battleAnimationBug,
|
||||
AsciiAnimationType.town => townAnimation,
|
||||
AsciiAnimationType.walking => walkingAnimation,
|
||||
AsciiAnimationType.levelUp => levelUpAnimation,
|
||||
AsciiAnimationType.questComplete => questCompleteAnimation,
|
||||
AsciiAnimationType.actComplete => actCompleteAnimation,
|
||||
AsciiAnimationType.resurrection => resurrectionAnimation,
|
||||
};
|
||||
}
|
||||
@@ -1,65 +0,0 @@
|
||||
import 'package:asciineverdie/src/core/model/game_state.dart';
|
||||
|
||||
/// ASCII 애니메이션 타입 (TaskType과 매핑)
|
||||
enum AsciiAnimationType {
|
||||
/// 전투 장면 (캐릭터 vs 몬스터)
|
||||
battle,
|
||||
|
||||
/// 마을/상점 장면
|
||||
town,
|
||||
|
||||
/// 걷는 캐릭터
|
||||
walking,
|
||||
|
||||
/// 레벨업 축하
|
||||
levelUp,
|
||||
|
||||
/// 퀘스트 완료
|
||||
questComplete,
|
||||
|
||||
/// Act 완료 (플롯 진행)
|
||||
actComplete,
|
||||
|
||||
/// 부활 (사망 후)
|
||||
resurrection,
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 특수 애니메이션 타이밍 상수
|
||||
// ============================================================================
|
||||
|
||||
/// 특수 애니메이션 프레임 수
|
||||
const specialAnimationFrameCounts = {
|
||||
AsciiAnimationType.levelUp: 5,
|
||||
AsciiAnimationType.questComplete: 4,
|
||||
AsciiAnimationType.actComplete: 4,
|
||||
AsciiAnimationType.resurrection: 5,
|
||||
};
|
||||
|
||||
/// 특수 애니메이션 프레임 간격 (밀리초)
|
||||
const specialAnimationFrameIntervals = {
|
||||
AsciiAnimationType.levelUp: 300,
|
||||
AsciiAnimationType.questComplete: 350,
|
||||
AsciiAnimationType.actComplete: 400,
|
||||
AsciiAnimationType.resurrection: 600,
|
||||
};
|
||||
|
||||
/// 특수 애니메이션 총 지속 시간 (밀리초)
|
||||
int getSpecialAnimationDuration(AsciiAnimationType type) {
|
||||
final frames = specialAnimationFrameCounts[type] ?? 1;
|
||||
final interval = specialAnimationFrameIntervals[type] ?? 200;
|
||||
return frames * interval;
|
||||
}
|
||||
|
||||
/// TaskType을 AsciiAnimationType으로 변환
|
||||
AsciiAnimationType taskTypeToAnimation(TaskType taskType) {
|
||||
return switch (taskType) {
|
||||
TaskType.kill => AsciiAnimationType.battle,
|
||||
TaskType.market ||
|
||||
TaskType.sell ||
|
||||
TaskType.buying => AsciiAnimationType.town,
|
||||
TaskType.neutral ||
|
||||
TaskType.load ||
|
||||
TaskType.plot => AsciiAnimationType.walking,
|
||||
};
|
||||
}
|
||||
@@ -1,178 +0,0 @@
|
||||
// 환경별 배경 패턴 데이터
|
||||
// ASCII Patrol 스타일 - 패럴렉스 스크롤링 배경
|
||||
|
||||
import 'package:asciineverdie/src/core/animation/background_layer.dart';
|
||||
|
||||
/// 환경별 배경 레이어 반환
|
||||
List<BackgroundLayer> getBackgroundLayers(EnvironmentType environment) {
|
||||
return switch (environment) {
|
||||
EnvironmentType.town => _townLayers,
|
||||
EnvironmentType.forest => _forestLayers,
|
||||
EnvironmentType.cave => _caveLayers,
|
||||
EnvironmentType.dungeon => _dungeonLayers,
|
||||
EnvironmentType.tech => _techLayers,
|
||||
EnvironmentType.void_ => _voidLayers,
|
||||
};
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 마을 (Town) - 건물 실루엣
|
||||
// ============================================================================
|
||||
const _townLayers = [
|
||||
// 원경 - 하늘/별
|
||||
BackgroundLayer(
|
||||
lines: [r'. * . * . * . * . * . * '],
|
||||
scrollSpeed: 0.05,
|
||||
yStart: 0,
|
||||
),
|
||||
// 중경 - 건물 실루엣
|
||||
BackgroundLayer(
|
||||
lines: [
|
||||
r' _|__|_ _|__|_ _|__|_ ',
|
||||
r' | | | | | | ',
|
||||
],
|
||||
scrollSpeed: 0.15,
|
||||
yStart: 1,
|
||||
),
|
||||
// 전경 - 바닥
|
||||
BackgroundLayer(
|
||||
lines: [r'====[]====[]====[]====[]====[]====[]'],
|
||||
scrollSpeed: 0.3,
|
||||
yStart: 7,
|
||||
),
|
||||
];
|
||||
|
||||
// ============================================================================
|
||||
// 숲 (Forest) - 나무
|
||||
// ============================================================================
|
||||
const _forestLayers = [
|
||||
// 원경 - 하늘/별
|
||||
BackgroundLayer(
|
||||
lines: [r'. * . * . * . * . * '],
|
||||
scrollSpeed: 0.05,
|
||||
yStart: 0,
|
||||
),
|
||||
// 중경 - 나무 실루엣
|
||||
BackgroundLayer(
|
||||
lines: [
|
||||
r' ,@@@, ,@@, ,@@@',
|
||||
r' @@ @@ @@ @@ @@ ',
|
||||
],
|
||||
scrollSpeed: 0.15,
|
||||
yStart: 1,
|
||||
),
|
||||
// 전경 - 바닥
|
||||
BackgroundLayer(
|
||||
lines: [r'______________________________________'],
|
||||
scrollSpeed: 0.3,
|
||||
yStart: 7,
|
||||
),
|
||||
];
|
||||
|
||||
// ============================================================================
|
||||
// 동굴 (Cave) - 바위
|
||||
// ============================================================================
|
||||
const _caveLayers = [
|
||||
// 천장
|
||||
BackgroundLayer(
|
||||
lines: [r'vvVVvvVVvvVVvvVVvvVVvvVVvvVVvvVVvvVV'],
|
||||
scrollSpeed: 0.1,
|
||||
yStart: 0,
|
||||
),
|
||||
// 종유석
|
||||
BackgroundLayer(
|
||||
lines: [
|
||||
r' | V | V | ',
|
||||
r' V V V ',
|
||||
],
|
||||
scrollSpeed: 0.15,
|
||||
yStart: 1,
|
||||
),
|
||||
// 바닥 - 석순
|
||||
BackgroundLayer(
|
||||
lines: [r'^__/\__^__/\__^__/\__^__/\__^__/\__^'],
|
||||
scrollSpeed: 0.25,
|
||||
yStart: 7,
|
||||
),
|
||||
];
|
||||
|
||||
// ============================================================================
|
||||
// 던전 (Dungeon) - 벽돌
|
||||
// ============================================================================
|
||||
const _dungeonLayers = [
|
||||
// 천장 - 벽돌
|
||||
BackgroundLayer(
|
||||
lines: [r'####|####|####|####|####|####|####|#'],
|
||||
scrollSpeed: 0.1,
|
||||
yStart: 0,
|
||||
),
|
||||
// 횃불
|
||||
BackgroundLayer(
|
||||
lines: [
|
||||
r' * * * ',
|
||||
r' )| )| )| ',
|
||||
],
|
||||
scrollSpeed: 0.15,
|
||||
yStart: 1,
|
||||
),
|
||||
// 바닥 - 타일
|
||||
BackgroundLayer(
|
||||
lines: [r'====[]====[]====[]====[]====[]====[]'],
|
||||
scrollSpeed: 0.25,
|
||||
yStart: 7,
|
||||
),
|
||||
];
|
||||
|
||||
// ============================================================================
|
||||
// 기술 (Tech) - 회로
|
||||
// ============================================================================
|
||||
const _techLayers = [
|
||||
// 상단 - 회로
|
||||
BackgroundLayer(
|
||||
lines: [r'-+-+-+-||-+-+-+-||-+-+-+-||-+-+-+-||'],
|
||||
scrollSpeed: 0.1,
|
||||
yStart: 0,
|
||||
),
|
||||
// 데이터 스트림
|
||||
BackgroundLayer(
|
||||
lines: [
|
||||
r' 10110 01101 10110 01101 101',
|
||||
r' 01 10 01 10 ',
|
||||
],
|
||||
scrollSpeed: 0.2,
|
||||
yStart: 2,
|
||||
),
|
||||
// 바닥 - 패널
|
||||
BackgroundLayer(
|
||||
lines: [r'[====][====][====][====][====][====]'],
|
||||
scrollSpeed: 0.3,
|
||||
yStart: 7,
|
||||
),
|
||||
];
|
||||
|
||||
// ============================================================================
|
||||
// 보이드 (Void) - 별/공허
|
||||
// ============================================================================
|
||||
const _voidLayers = [
|
||||
// 별
|
||||
BackgroundLayer(
|
||||
lines: [r' * . * . * . * . * '],
|
||||
scrollSpeed: 0.03,
|
||||
yStart: 0,
|
||||
),
|
||||
// 은하
|
||||
BackgroundLayer(
|
||||
lines: [
|
||||
r' ~*~ ~*~ ~*~ ',
|
||||
r' *~ ~* *~ ~* *~ ~*',
|
||||
],
|
||||
scrollSpeed: 0.08,
|
||||
yStart: 2,
|
||||
),
|
||||
// 심연
|
||||
BackgroundLayer(
|
||||
lines: [r'~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.~.'],
|
||||
scrollSpeed: 0.15,
|
||||
yStart: 7,
|
||||
),
|
||||
];
|
||||
@@ -1,92 +0,0 @@
|
||||
// 배경 레이어 시스템 (ASCII Patrol 스타일 패럴렉스)
|
||||
// 각 환경은 여러 레이어로 구성되며, 레이어마다 다른 스크롤 속도를 가짐
|
||||
|
||||
/// 배경 레이어 데이터
|
||||
class BackgroundLayer {
|
||||
const BackgroundLayer({
|
||||
required this.lines,
|
||||
required this.scrollSpeed,
|
||||
this.yStart = 0,
|
||||
});
|
||||
|
||||
/// 레이어 패턴 (각 줄은 반복 가능한 패턴)
|
||||
final List<String> lines;
|
||||
|
||||
/// 스크롤 속도 (0.0 = 정지, 1.0 = 최고속)
|
||||
/// 원경일수록 느리게, 전경일수록 빠르게
|
||||
final double scrollSpeed;
|
||||
|
||||
/// 시작 Y 위치 (0~7)
|
||||
final int yStart;
|
||||
}
|
||||
|
||||
/// 환경 타입
|
||||
enum EnvironmentType {
|
||||
/// 마을 - 건물 실루엣
|
||||
town,
|
||||
|
||||
/// 숲 - 나무
|
||||
forest,
|
||||
|
||||
/// 동굴 - 바위
|
||||
cave,
|
||||
|
||||
/// 던전 - 벽돌
|
||||
dungeon,
|
||||
|
||||
/// 기술 - 회로
|
||||
tech,
|
||||
|
||||
/// 보이드 - 별/공허 (보스)
|
||||
void_,
|
||||
}
|
||||
|
||||
/// TaskType과 몬스터 이름에서 환경 타입 추론
|
||||
EnvironmentType inferEnvironment(String? taskType, String? monsterName) {
|
||||
// 마을 관련 태스크
|
||||
if (taskType == 'heading' || taskType == 'buyEquip') {
|
||||
return EnvironmentType.town;
|
||||
}
|
||||
|
||||
// 몬스터 이름에서 환경 추론
|
||||
if (monsterName != null) {
|
||||
final lower = monsterName.toLowerCase();
|
||||
|
||||
// 보이드/우주
|
||||
if (lower.contains('void') ||
|
||||
lower.contains('cosmic') ||
|
||||
lower.contains('star') ||
|
||||
lower.contains('galaxy')) {
|
||||
return EnvironmentType.void_;
|
||||
}
|
||||
|
||||
// 기술/사이버
|
||||
if (lower.contains('cyber') ||
|
||||
lower.contains('robot') ||
|
||||
lower.contains('ai') ||
|
||||
lower.contains('data') ||
|
||||
lower.contains('server')) {
|
||||
return EnvironmentType.tech;
|
||||
}
|
||||
|
||||
// 언데드/던전
|
||||
if (lower.contains('zombie') ||
|
||||
lower.contains('skeleton') ||
|
||||
lower.contains('ghost') ||
|
||||
lower.contains('undead') ||
|
||||
lower.contains('dungeon')) {
|
||||
return EnvironmentType.dungeon;
|
||||
}
|
||||
|
||||
// 동굴
|
||||
if (lower.contains('cave') ||
|
||||
lower.contains('bat') ||
|
||||
lower.contains('spider') ||
|
||||
lower.contains('worm')) {
|
||||
return EnvironmentType.cave;
|
||||
}
|
||||
}
|
||||
|
||||
// 기본: 숲
|
||||
return EnvironmentType.forest;
|
||||
}
|
||||
@@ -1,265 +0,0 @@
|
||||
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:flutter/material.dart';
|
||||
|
||||
/// Paragraph 캐시 키
|
||||
class _ParagraphCacheKey {
|
||||
const _ParagraphCacheKey({
|
||||
required this.char,
|
||||
required this.color,
|
||||
required this.fontSize,
|
||||
required this.cellWidth,
|
||||
required this.opacity,
|
||||
});
|
||||
|
||||
final String char;
|
||||
final AsciiCellColor color;
|
||||
final double fontSize;
|
||||
final double cellWidth;
|
||||
final double opacity;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) ||
|
||||
other is _ParagraphCacheKey &&
|
||||
char == other.char &&
|
||||
color == other.color &&
|
||||
fontSize == other.fontSize &&
|
||||
cellWidth == other.cellWidth &&
|
||||
opacity == other.opacity;
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(char, color, fontSize, cellWidth, opacity);
|
||||
}
|
||||
|
||||
/// ASCII Canvas 페인터 (CustomPainter 구현)
|
||||
///
|
||||
/// 레이어 기반으로 ASCII 문자를 Canvas에 그린다.
|
||||
/// 각 문자는 고정 크기 그리드 셀에 배치된다.
|
||||
/// Paragraph 캐싱으로 GC 압박 최소화.
|
||||
/// 테마 인식 색상 지원 (Phase 5).
|
||||
class AsciiCanvasPainter extends CustomPainter {
|
||||
AsciiCanvasPainter({
|
||||
required this.layers,
|
||||
this.gridWidth = 60,
|
||||
this.gridHeight = 8,
|
||||
this.backgroundColor = AsciiColors.background,
|
||||
this.backgroundOpacity = 0.5,
|
||||
this.layerVersion = 0,
|
||||
this.objectColor = AsciiColors.object,
|
||||
this.positiveColor = AsciiColors.positive,
|
||||
this.negativeColor = AsciiColors.negative,
|
||||
this.rarityUncommonColor = AsciiColors.rarityUncommon,
|
||||
this.rarityRareColor = AsciiColors.rarityRare,
|
||||
this.rarityEpicColor = AsciiColors.rarityEpic,
|
||||
this.rarityLegendaryColor = AsciiColors.rarityLegendary,
|
||||
});
|
||||
|
||||
/// 렌더링할 레이어 목록 (z-order 정렬 필요)
|
||||
final List<AsciiLayer> layers;
|
||||
|
||||
/// 그리드 너비 (열 수)
|
||||
final int gridWidth;
|
||||
|
||||
/// 그리드 높이 (행 수)
|
||||
final int gridHeight;
|
||||
|
||||
/// 배경색 (테마 인식 가능)
|
||||
final Color backgroundColor;
|
||||
|
||||
/// 배경 투명도 (0.0 ~ 1.0, 기본값 0.5 = 50%)
|
||||
final double backgroundOpacity;
|
||||
|
||||
/// 레이어 버전 (변경 감지용)
|
||||
final int layerVersion;
|
||||
|
||||
/// 오브젝트 색상 (테마 인식)
|
||||
final Color objectColor;
|
||||
|
||||
/// 포지티브 이펙트 색상 (테마 인식)
|
||||
final Color positiveColor;
|
||||
|
||||
/// 네거티브 이펙트 색상 (테마 인식)
|
||||
final Color negativeColor;
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
// 무기 등급(ItemRarity) 색상 (Phase 9)
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
|
||||
/// Uncommon 등급 색상 (초록)
|
||||
final Color rarityUncommonColor;
|
||||
|
||||
/// Rare 등급 색상 (파랑)
|
||||
final Color rarityRareColor;
|
||||
|
||||
/// Epic 등급 색상 (보라)
|
||||
final Color rarityEpicColor;
|
||||
|
||||
/// Legendary 등급 색상 (금색)
|
||||
final Color rarityLegendaryColor;
|
||||
|
||||
/// Paragraph 캐시 (문자+색상+크기 조합별)
|
||||
static final Map<_ParagraphCacheKey, ui.Paragraph> _paragraphCache = {};
|
||||
|
||||
/// 캐시 크기 제한
|
||||
static const int _maxCacheSize = 256;
|
||||
|
||||
/// 마지막 사용된 폰트 크기 (크기 변경 시 캐시 무효화)
|
||||
static double _lastFontSize = 0;
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Size size) {
|
||||
// 1. 셀 크기 계산
|
||||
final cellWidth = size.width / gridWidth;
|
||||
final cellHeight = size.height / gridHeight;
|
||||
final fontSize = cellHeight * 0.85;
|
||||
|
||||
// 폰트 크기 변경 시 캐시 무효화
|
||||
if ((fontSize - _lastFontSize).abs() > 0.5) {
|
||||
_paragraphCache.clear();
|
||||
_lastFontSize = fontSize;
|
||||
}
|
||||
|
||||
// 2. 배경 채우기 (투명도 적용)
|
||||
canvas.drawRect(
|
||||
Rect.fromLTWH(0, 0, size.width, size.height),
|
||||
Paint()..color = backgroundColor.withValues(alpha: backgroundOpacity),
|
||||
);
|
||||
|
||||
// 3. 레이어별 렌더링 (z-order 순)
|
||||
for (final layer in layers) {
|
||||
_renderLayer(canvas, layer, cellWidth, cellHeight, fontSize);
|
||||
}
|
||||
}
|
||||
|
||||
/// 단일 레이어 렌더링
|
||||
void _renderLayer(
|
||||
Canvas canvas,
|
||||
AsciiLayer layer,
|
||||
double cellWidth,
|
||||
double cellHeight,
|
||||
double fontSize,
|
||||
) {
|
||||
for (var row = 0; row < layer.height; row++) {
|
||||
for (var col = 0; col < layer.width; col++) {
|
||||
final cell = layer.cells[row][col];
|
||||
if (cell.isEmpty) continue;
|
||||
|
||||
// 오프셋 적용된 실제 그리드 위치
|
||||
final gridX = col + layer.offsetX;
|
||||
final gridY = row + layer.offsetY;
|
||||
|
||||
// 그리드 범위 확인
|
||||
if (gridX < 0 || gridX >= gridWidth) continue;
|
||||
if (gridY < 0 || gridY >= gridHeight) continue;
|
||||
|
||||
// 픽셀 좌표 계산
|
||||
final x = gridX * cellWidth;
|
||||
final y = gridY * cellHeight;
|
||||
|
||||
// 레이어 투명도를 셀 렌더링에 전달
|
||||
_drawCell(
|
||||
canvas,
|
||||
cell,
|
||||
x,
|
||||
y,
|
||||
cellWidth,
|
||||
cellHeight,
|
||||
fontSize,
|
||||
layer.opacity,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 단일 셀 그리기 (캐시 활용)
|
||||
void _drawCell(
|
||||
Canvas canvas,
|
||||
AsciiCell cell,
|
||||
double x,
|
||||
double y,
|
||||
double cellWidth,
|
||||
double cellHeight,
|
||||
double fontSize,
|
||||
double opacity,
|
||||
) {
|
||||
final cacheKey = _ParagraphCacheKey(
|
||||
char: cell.char,
|
||||
color: cell.color,
|
||||
fontSize: fontSize,
|
||||
cellWidth: cellWidth,
|
||||
opacity: opacity,
|
||||
);
|
||||
|
||||
// 캐시 히트 확인
|
||||
var paragraph = _paragraphCache[cacheKey];
|
||||
|
||||
if (paragraph == null) {
|
||||
// 캐시 미스: 새 Paragraph 생성 (opacity 적용)
|
||||
final color = _getColor(cell.color).withValues(alpha: opacity);
|
||||
|
||||
final paragraphBuilder = ui.ParagraphBuilder(
|
||||
ui.ParagraphStyle(
|
||||
fontFamily: 'JetBrainsMono',
|
||||
fontSize: fontSize,
|
||||
textAlign: TextAlign.center,
|
||||
height: 1.0,
|
||||
),
|
||||
);
|
||||
|
||||
paragraphBuilder.pushStyle(ui.TextStyle(color: color));
|
||||
paragraphBuilder.addText(cell.char);
|
||||
|
||||
paragraph = paragraphBuilder.build();
|
||||
paragraph.layout(ui.ParagraphConstraints(width: cellWidth));
|
||||
|
||||
// 캐시 크기 제한 (LRU 대신 단순 클리어)
|
||||
if (_paragraphCache.length >= _maxCacheSize) {
|
||||
_paragraphCache.clear();
|
||||
}
|
||||
|
||||
_paragraphCache[cacheKey] = paragraph;
|
||||
}
|
||||
|
||||
// 셀 중앙에 문자 배치
|
||||
final offsetX = x + (cellWidth - paragraph.maxIntrinsicWidth) / 2;
|
||||
final offsetY = y + (cellHeight - paragraph.height) / 2;
|
||||
|
||||
canvas.drawParagraph(paragraph, Offset(offsetX, offsetY));
|
||||
}
|
||||
|
||||
/// AsciiCellColor를 Flutter Color로 변환 (테마 인식 색상 사용)
|
||||
Color _getColor(AsciiCellColor cellColor) {
|
||||
return switch (cellColor) {
|
||||
AsciiCellColor.background => backgroundColor,
|
||||
AsciiCellColor.object => objectColor,
|
||||
AsciiCellColor.positive => positiveColor,
|
||||
AsciiCellColor.negative => negativeColor,
|
||||
// 무기 등급 색상 (Phase 9)
|
||||
AsciiCellColor.rarityUncommon => rarityUncommonColor,
|
||||
AsciiCellColor.rarityRare => rarityRareColor,
|
||||
AsciiCellColor.rarityEpic => rarityEpicColor,
|
||||
AsciiCellColor.rarityLegendary => rarityLegendaryColor,
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldRepaint(AsciiCanvasPainter oldDelegate) {
|
||||
// 레이어 버전으로 빠른 비교
|
||||
if (layerVersion != oldDelegate.layerVersion) return true;
|
||||
|
||||
// 동일 참조면 스킵
|
||||
if (identical(layers, oldDelegate.layers)) return false;
|
||||
|
||||
// 그리드 설정 변경 확인
|
||||
if (gridWidth != oldDelegate.gridWidth ||
|
||||
gridHeight != oldDelegate.gridHeight) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -1,79 +0,0 @@
|
||||
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:flutter/material.dart';
|
||||
|
||||
/// ASCII Canvas 위젯 (RepaintBoundary 포함)
|
||||
///
|
||||
/// AsciiCanvasPainter를 감싸는 위젯.
|
||||
/// RepaintBoundary로 성능 최적화.
|
||||
/// willChange를 애니메이션 상태에 따라 동적 설정.
|
||||
/// 테마 인식 색상 자동 적용 (Phase 5).
|
||||
class AsciiCanvasWidget extends StatelessWidget {
|
||||
const AsciiCanvasWidget({
|
||||
super.key,
|
||||
required this.layers,
|
||||
this.gridWidth = 60,
|
||||
this.gridHeight = 8,
|
||||
this.backgroundOpacity = 0.5,
|
||||
this.isAnimating = true,
|
||||
this.layerVersion = 0,
|
||||
});
|
||||
|
||||
/// 렌더링할 레이어 목록
|
||||
final List<AsciiLayer> layers;
|
||||
|
||||
/// 그리드 너비 (열 수)
|
||||
final int gridWidth;
|
||||
|
||||
/// 그리드 높이 (행 수)
|
||||
final int gridHeight;
|
||||
|
||||
/// 배경 투명도 (0.0 ~ 1.0, 기본값 0.5 = 50%)
|
||||
final double backgroundOpacity;
|
||||
|
||||
/// 애니메이션 활성 상태 (willChange 최적화용)
|
||||
final bool isAnimating;
|
||||
|
||||
/// 레이어 버전 (변경 감지용, shouldRepaint 최적화)
|
||||
final int layerVersion;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// 테마 인식 색상 (다크/라이트 모드 자동 전환)
|
||||
final bgColor = AsciiColors.backgroundOf(context);
|
||||
final objColor = AsciiColors.objectOf(context);
|
||||
final posColor = AsciiColors.positiveOf(context);
|
||||
final negColor = AsciiColors.negativeOf(context);
|
||||
|
||||
// 무기 등급 색상 (Phase 9)
|
||||
final uncommonColor = AsciiColors.rarityUncommonOf(context);
|
||||
final rareColor = AsciiColors.rarityRareOf(context);
|
||||
final epicColor = AsciiColors.rarityEpicOf(context);
|
||||
final legendaryColor = AsciiColors.rarityLegendaryOf(context);
|
||||
|
||||
return RepaintBoundary(
|
||||
child: CustomPaint(
|
||||
painter: AsciiCanvasPainter(
|
||||
layers: layers,
|
||||
gridWidth: gridWidth,
|
||||
gridHeight: gridHeight,
|
||||
backgroundColor: bgColor,
|
||||
backgroundOpacity: backgroundOpacity,
|
||||
layerVersion: layerVersion,
|
||||
objectColor: objColor,
|
||||
positiveColor: posColor,
|
||||
negativeColor: negColor,
|
||||
rarityUncommonColor: uncommonColor,
|
||||
rarityRareColor: rareColor,
|
||||
rarityEpicColor: epicColor,
|
||||
rarityLegendaryColor: legendaryColor,
|
||||
),
|
||||
size: Size.infinite,
|
||||
isComplex: true,
|
||||
// 애니메이션 중일 때만 willChange 활성화
|
||||
willChange: isAnimating,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,74 +0,0 @@
|
||||
/// ASCII 셀 색상 (4색 팔레트 + 무기 등급 색상)
|
||||
enum AsciiCellColor {
|
||||
/// 배경색 (검정)
|
||||
background,
|
||||
|
||||
/// 오브젝트 (흰색) - 캐릭터, 몬스터, 지형
|
||||
object,
|
||||
|
||||
/// 포지티브 이펙트 (시안) - !, +, =, >, < / common 무기
|
||||
positive,
|
||||
|
||||
/// 네거티브 이펙트 (마젠타) - *, ~
|
||||
negative,
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
// 무기 등급(ItemRarity) 색상 (Phase 9)
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
|
||||
/// Uncommon 등급 (초록)
|
||||
rarityUncommon,
|
||||
|
||||
/// Rare 등급 (파랑)
|
||||
rarityRare,
|
||||
|
||||
/// Epic 등급 (보라)
|
||||
rarityEpic,
|
||||
|
||||
/// Legendary 등급 (금색)
|
||||
rarityLegendary,
|
||||
}
|
||||
|
||||
/// 단일 ASCII 셀 데이터
|
||||
class AsciiCell {
|
||||
const AsciiCell({required this.char, this.color = AsciiCellColor.object});
|
||||
|
||||
/// 표시할 문자 (단일 문자)
|
||||
final String char;
|
||||
|
||||
/// 셀 색상 타입
|
||||
final AsciiCellColor color;
|
||||
|
||||
/// 빈 셀 (투명)
|
||||
static const empty = AsciiCell(char: ' ');
|
||||
|
||||
/// 문자가 공백인지 확인 (투명 처리용)
|
||||
bool get isEmpty => char == ' ' || char.isEmpty;
|
||||
|
||||
/// 문자에서 색상 자동 결정
|
||||
static AsciiCellColor colorFromChar(String char) {
|
||||
// 포지티브 이펙트 문자 (시안)
|
||||
if ('!+=><'.contains(char)) return AsciiCellColor.positive;
|
||||
// 네거티브 이펙트 문자 (마젠타)
|
||||
if ('*~'.contains(char)) return AsciiCellColor.negative;
|
||||
// 기본 오브젝트 (흰색)
|
||||
return AsciiCellColor.object;
|
||||
}
|
||||
|
||||
/// 문자열에서 AsciiCell 생성 (자동 색상)
|
||||
factory AsciiCell.fromChar(String char) {
|
||||
if (char.isEmpty || char == ' ') return empty;
|
||||
return AsciiCell(char: char, color: colorFromChar(char));
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) ||
|
||||
other is AsciiCell &&
|
||||
runtimeType == other.runtimeType &&
|
||||
char == other.char &&
|
||||
color == other.color;
|
||||
|
||||
@override
|
||||
int get hashCode => char.hashCode ^ color.hashCode;
|
||||
}
|
||||
@@ -1,70 +0,0 @@
|
||||
import 'package:asciineverdie/src/core/animation/canvas/ascii_cell.dart';
|
||||
|
||||
/// ASCII 레이어 데이터 구조 (Canvas 렌더러용)
|
||||
///
|
||||
/// 2D 셀 배열과 위치/깊이 정보를 담는다.
|
||||
class AsciiLayer {
|
||||
const AsciiLayer({
|
||||
required this.cells,
|
||||
this.zIndex = 0,
|
||||
this.offsetX = 0,
|
||||
this.offsetY = 0,
|
||||
this.opacity = 1.0,
|
||||
});
|
||||
|
||||
/// 2D 셀 배열 [row][column]
|
||||
final List<List<AsciiCell>> cells;
|
||||
|
||||
/// Z 순서 (낮을수록 뒤쪽)
|
||||
final int zIndex;
|
||||
|
||||
/// X 오프셋 (스크롤/이동용)
|
||||
final int offsetX;
|
||||
|
||||
/// Y 오프셋
|
||||
final int offsetY;
|
||||
|
||||
/// 레이어 투명도 (0.0 ~ 1.0, 배경 레이어 등에서 사용)
|
||||
final double opacity;
|
||||
|
||||
/// 레이어 높이 (줄 수)
|
||||
int get height => cells.length;
|
||||
|
||||
/// 레이어 너비 (열 수)
|
||||
int get width => cells.isEmpty ? 0 : cells.first.length;
|
||||
|
||||
/// 특정 위치의 셀 반환 (범위 밖이면 empty)
|
||||
AsciiCell getCell(int row, int col) {
|
||||
if (row < 0 || row >= height) return AsciiCell.empty;
|
||||
if (col < 0 || col >= cells[row].length) return AsciiCell.empty;
|
||||
return cells[row][col];
|
||||
}
|
||||
|
||||
/// 빈 레이어 생성
|
||||
factory AsciiLayer.empty({int width = 60, int height = 8, int zIndex = 0}) {
|
||||
final cells = List.generate(
|
||||
height,
|
||||
(_) => List.filled(width, AsciiCell.empty),
|
||||
);
|
||||
return AsciiLayer(cells: cells, zIndex: zIndex);
|
||||
}
|
||||
|
||||
/// 문자열 리스트에서 레이어 생성 (자동 색상)
|
||||
factory AsciiLayer.fromLines(
|
||||
List<String> lines, {
|
||||
int zIndex = 0,
|
||||
int offsetX = 0,
|
||||
int offsetY = 0,
|
||||
}) {
|
||||
final cells = lines.map((line) {
|
||||
return line.split('').map(AsciiCell.fromChar).toList();
|
||||
}).toList();
|
||||
|
||||
return AsciiLayer(
|
||||
cells: cells,
|
||||
zIndex: zIndex,
|
||||
offsetX: offsetX,
|
||||
offsetY: offsetY,
|
||||
);
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,175 +0,0 @@
|
||||
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';
|
||||
|
||||
/// Canvas용 특수 이벤트 애니메이션 합성기
|
||||
///
|
||||
/// 레벨업, 퀘스트 완료, Act 완료, 부활 등
|
||||
class CanvasSpecialComposer {
|
||||
const CanvasSpecialComposer();
|
||||
|
||||
/// 프레임 상수
|
||||
static const int frameWidth = 60;
|
||||
static const int frameHeight = 8;
|
||||
|
||||
/// 레이어 기반 프레임 생성
|
||||
List<AsciiLayer> composeLayers(
|
||||
AsciiAnimationType type,
|
||||
int frameIndex,
|
||||
int globalTick,
|
||||
) {
|
||||
return switch (type) {
|
||||
AsciiAnimationType.levelUp => _composeLevelUp(frameIndex, globalTick),
|
||||
AsciiAnimationType.questComplete => _composeQuestComplete(
|
||||
frameIndex,
|
||||
globalTick,
|
||||
),
|
||||
AsciiAnimationType.actComplete => _composeActComplete(
|
||||
frameIndex,
|
||||
globalTick,
|
||||
),
|
||||
AsciiAnimationType.resurrection => _composeResurrection(
|
||||
frameIndex,
|
||||
globalTick,
|
||||
),
|
||||
_ => [AsciiLayer.empty()],
|
||||
};
|
||||
}
|
||||
|
||||
/// 레벨업 애니메이션
|
||||
List<AsciiLayer> _composeLevelUp(int frameIndex, int globalTick) {
|
||||
final layers = <AsciiLayer>[
|
||||
_createEffectBackground(globalTick, '*'),
|
||||
_createCenteredSprite(_levelUpFrames[frameIndex % _levelUpFrames.length]),
|
||||
];
|
||||
return layers;
|
||||
}
|
||||
|
||||
/// 퀘스트 완료 애니메이션
|
||||
List<AsciiLayer> _composeQuestComplete(int frameIndex, int globalTick) {
|
||||
final layers = <AsciiLayer>[
|
||||
_createEffectBackground(globalTick, '+'),
|
||||
_createCenteredSprite(
|
||||
_questCompleteFrames[frameIndex % _questCompleteFrames.length],
|
||||
),
|
||||
];
|
||||
return layers;
|
||||
}
|
||||
|
||||
/// Act 완료 애니메이션
|
||||
List<AsciiLayer> _composeActComplete(int frameIndex, int globalTick) {
|
||||
final layers = <AsciiLayer>[
|
||||
_createEffectBackground(globalTick, '~'),
|
||||
_createCenteredSprite(
|
||||
_actCompleteFrames[frameIndex % _actCompleteFrames.length],
|
||||
),
|
||||
];
|
||||
return layers;
|
||||
}
|
||||
|
||||
/// 부활 애니메이션
|
||||
List<AsciiLayer> _composeResurrection(int frameIndex, int globalTick) {
|
||||
final layers = <AsciiLayer>[
|
||||
_createEffectBackground(globalTick, '.'),
|
||||
_createCenteredSprite(
|
||||
_resurrectionFrames[frameIndex % _resurrectionFrames.length],
|
||||
),
|
||||
];
|
||||
return layers;
|
||||
}
|
||||
|
||||
/// 이펙트 배경 레이어 생성 (z=0)
|
||||
AsciiLayer _createEffectBackground(int globalTick, String effectChar) {
|
||||
final cells = List.generate(
|
||||
frameHeight,
|
||||
(_) => List.filled(frameWidth, AsciiCell.empty),
|
||||
);
|
||||
|
||||
// 반짝이는 이펙트
|
||||
for (var y = 0; y < frameHeight; y++) {
|
||||
for (var x = 0; x < frameWidth; x++) {
|
||||
final offset = (x + y + globalTick) % 8;
|
||||
if (offset == 0) {
|
||||
cells[y][x] = AsciiCell.fromChar(effectChar);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return AsciiLayer(cells: cells, zIndex: 0);
|
||||
}
|
||||
|
||||
/// 중앙 정렬 스프라이트 레이어 생성 (z=1)
|
||||
AsciiLayer _createCenteredSprite(List<String> lines) {
|
||||
final cells = _spriteToCells(lines);
|
||||
|
||||
// 중앙 정렬
|
||||
final spriteWidth = lines.isEmpty ? 0 : lines[0].length;
|
||||
final offsetX = (frameWidth - spriteWidth) ~/ 2;
|
||||
final offsetY = (frameHeight - cells.length) ~/ 2;
|
||||
|
||||
return AsciiLayer(
|
||||
cells: cells,
|
||||
zIndex: 1,
|
||||
offsetX: offsetX,
|
||||
offsetY: offsetY,
|
||||
);
|
||||
}
|
||||
|
||||
/// 문자열 스프라이트를 AsciiCell 2D 배열로 변환
|
||||
List<List<AsciiCell>> _spriteToCells(List<String> lines) {
|
||||
return lines.map((line) {
|
||||
return line.split('').map(AsciiCell.fromChar).toList();
|
||||
}).toList();
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 레벨업 프레임 (5프레임)
|
||||
// ============================================================================
|
||||
|
||||
const _levelUpFrames = [
|
||||
[r' * ', r' \|/ ', r' o ', r' /|\ ', r' / \ '],
|
||||
[r' * * ', r' \|/ ', r' O ', r' </|\> ', r' / \ '],
|
||||
[r' * * * ', r' \|/ ', r' O ', r' <\|/> ', r' / \ '],
|
||||
[r' * * * * ', r' LEVEL ', r' UP! ', r' \O/ ', r' / \ '],
|
||||
[r'* * * * *', r' LEVEL ', r' UP! ', r' \O/ ', r' | | '],
|
||||
];
|
||||
|
||||
// ============================================================================
|
||||
// 퀘스트 완료 프레임 (4프레임)
|
||||
// ============================================================================
|
||||
|
||||
const _questCompleteFrames = [
|
||||
[r' [?] ', r' | ', r' o ', r' /|\ ', r' / \ '],
|
||||
[r' [???] ', r' | ', r' o! ', r' /|\ ', r' / \ '],
|
||||
[r' [DONE] ', r' ! ', r' \o/ ', r' | ', r' / \ '],
|
||||
[r' +[DONE]+', r' \!/ ', r' \o/ ', r' | ', r' / \ '],
|
||||
];
|
||||
|
||||
// ============================================================================
|
||||
// Act 완료 프레임 (4프레임)
|
||||
// ============================================================================
|
||||
|
||||
const _actCompleteFrames = [
|
||||
[r'=========', r' ACT ', r' CLEAR ', r' o ', r' /|\ '],
|
||||
[r'~~~~~~~~~', r' ACT ', r' CLEAR! ', r' \o/ ', r' | '],
|
||||
[r'*~*~*~*~*', r' ACT ', r' CLEAR!! ', r' \O/ ', r' / \ '],
|
||||
[r'*********', r' ACT ', r' CLEAR!! ', r' \O/ ', r' | | '],
|
||||
];
|
||||
|
||||
// ============================================================================
|
||||
// 부활 프레임 (5프레임)
|
||||
// ============================================================================
|
||||
|
||||
const _resurrectionFrames = [
|
||||
// 프레임 1: R.I.P 묘비
|
||||
[r' ___ ', r' |RIP| ', r' | | ', r'__|___|__'],
|
||||
// 프레임 2: 빛 내림
|
||||
[r' \|/ ', r' -|R|- ', r' | | ', r'__|___|__'],
|
||||
// 프레임 3: 일어남
|
||||
[r' \o/ ', r' --|-- ', r' | | ', r'__|___|__'],
|
||||
// 프레임 4: 서있음
|
||||
[r' o ', r' /|\ ', r' / \ ', r'_________'],
|
||||
// 프레임 5: 부활 완료
|
||||
[r' REVIVED ', r' \o/ ', r' | ', r'___/ \___'],
|
||||
];
|
||||
@@ -1,124 +0,0 @@
|
||||
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';
|
||||
|
||||
/// Canvas용 마을/상점 애니메이션 합성기
|
||||
///
|
||||
/// 마을 배경 + 상점 건물 + 캐릭터
|
||||
/// Phase 4: 종족별 캐릭터 프레임 지원
|
||||
class CanvasTownComposer {
|
||||
const CanvasTownComposer({this.raceId});
|
||||
|
||||
/// 종족 ID (종족별 캐릭터 프레임 선택용)
|
||||
final String? raceId;
|
||||
|
||||
/// 프레임 상수
|
||||
static const int frameWidth = 60;
|
||||
static const int frameHeight = 8;
|
||||
|
||||
/// 레이어 기반 프레임 생성
|
||||
List<AsciiLayer> composeLayers(int globalTick) {
|
||||
return [
|
||||
_createBackgroundLayer(),
|
||||
_createShopLayer(),
|
||||
_createCharacterLayer(globalTick),
|
||||
];
|
||||
}
|
||||
|
||||
/// 마을 배경 레이어 생성 (z=0)
|
||||
AsciiLayer _createBackgroundLayer() {
|
||||
final cells = List.generate(
|
||||
frameHeight,
|
||||
(_) => List.filled(frameWidth, AsciiCell.empty),
|
||||
);
|
||||
|
||||
// 하늘 (상단 2줄)
|
||||
for (var x = 0; x < frameWidth; x++) {
|
||||
// 별/구름 패턴
|
||||
if (x % 12 == 3) {
|
||||
cells[0][x] = AsciiCell.fromChar('*');
|
||||
}
|
||||
if (x % 8 == 5) {
|
||||
cells[1][x] = AsciiCell.fromChar('~');
|
||||
}
|
||||
}
|
||||
|
||||
// 바닥 (하단 1줄)
|
||||
for (var x = 0; x < frameWidth; x++) {
|
||||
cells[7][x] = AsciiCell.fromChar('=');
|
||||
}
|
||||
|
||||
return AsciiLayer(cells: cells, zIndex: 0);
|
||||
}
|
||||
|
||||
/// 상점 건물 레이어 생성 (z=1)
|
||||
AsciiLayer _createShopLayer() {
|
||||
const shopLines = [
|
||||
r' ___________ ',
|
||||
r' / SHOP \ ',
|
||||
r' | _______ | ',
|
||||
r' | | | | ',
|
||||
r' | | $$ | || ',
|
||||
r'___|_|_______|__||____',
|
||||
];
|
||||
|
||||
final cells = _spriteToCells(shopLines);
|
||||
|
||||
// 상점 위치 (오른쪽)
|
||||
const shopX = 32;
|
||||
final shopY = frameHeight - cells.length - 1;
|
||||
|
||||
return AsciiLayer(cells: cells, zIndex: 1, offsetX: shopX, offsetY: shopY);
|
||||
}
|
||||
|
||||
/// 캐릭터 레이어 생성 (z=2)
|
||||
/// Phase 4: 종족별 프레임 지원
|
||||
AsciiLayer _createCharacterLayer(int globalTick) {
|
||||
final frameIndex = globalTick % 4; // 4프레임 루프
|
||||
List<String> charFrame;
|
||||
|
||||
// 종족별 프레임 사용 시도
|
||||
if (raceId != null && raceId!.isNotEmpty) {
|
||||
final raceData = RaceCharacterFrames.get(raceId!);
|
||||
if (raceData != null) {
|
||||
// idle 프레임 직접 사용 (마을에서는 서있는 자세)
|
||||
final idleFrame = raceData.idle[frameIndex % raceData.idle.length];
|
||||
charFrame = idleFrame.lines;
|
||||
} else {
|
||||
charFrame = _shopIdleFrames[frameIndex];
|
||||
}
|
||||
} else {
|
||||
charFrame = _shopIdleFrames[frameIndex];
|
||||
}
|
||||
|
||||
final cells = _spriteToCells(charFrame);
|
||||
|
||||
// 상점 앞에 캐릭터 배치
|
||||
const charX = 25;
|
||||
final charY = frameHeight - cells.length - 1;
|
||||
|
||||
return AsciiLayer(cells: cells, zIndex: 2, offsetX: charX, offsetY: charY);
|
||||
}
|
||||
|
||||
/// 문자열 스프라이트를 AsciiCell 2D 배열로 변환
|
||||
List<List<AsciiCell>> _spriteToCells(List<String> lines) {
|
||||
return lines.map((line) {
|
||||
return line.split('').map(AsciiCell.fromChar).toList();
|
||||
}).toList();
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 상점 앞 대기 프레임 (4프레임 루프) - 물건 보는 동작
|
||||
// ============================================================================
|
||||
|
||||
const _shopIdleFrames = [
|
||||
// 프레임 1: 기본
|
||||
[r' o ', r' /|\ ', r' / \ '],
|
||||
// 프레임 2: 머리 숙임
|
||||
[r' o ', r' /|~ ', r' / \ '],
|
||||
// 프레임 3: 물건 보기
|
||||
[r' o? ', r' /| ', r' / \ '],
|
||||
// 프레임 4: 고개 끄덕
|
||||
[r' o! ', r' /|\ ', r' / \ '],
|
||||
];
|
||||
@@ -1,120 +0,0 @@
|
||||
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';
|
||||
|
||||
/// Canvas용 걷기 애니메이션 합성기
|
||||
///
|
||||
/// 배경 스크롤 + 걷는 캐릭터
|
||||
/// Phase 4: 종족별 캐릭터 프레임 지원
|
||||
class CanvasWalkingComposer {
|
||||
const CanvasWalkingComposer({this.raceId});
|
||||
|
||||
/// 종족 ID (종족별 캐릭터 프레임 선택용)
|
||||
final String? raceId;
|
||||
|
||||
/// 프레임 상수
|
||||
static const int frameWidth = 60;
|
||||
static const int frameHeight = 8;
|
||||
|
||||
/// 레이어 기반 프레임 생성
|
||||
List<AsciiLayer> composeLayers(int globalTick) {
|
||||
return [
|
||||
_createBackgroundLayer(globalTick),
|
||||
_createCharacterLayer(globalTick),
|
||||
];
|
||||
}
|
||||
|
||||
/// 배경 레이어 생성 (z=0) - 숲 환경 기본
|
||||
AsciiLayer _createBackgroundLayer(int globalTick) {
|
||||
final cells = List.generate(
|
||||
frameHeight,
|
||||
(_) => List.filled(frameWidth, AsciiCell.empty),
|
||||
);
|
||||
|
||||
final bgLayers = getBackgroundLayers(EnvironmentType.forest);
|
||||
for (final layer in bgLayers) {
|
||||
// 스크롤 오프셋 계산 (걷기는 더 빠른 스크롤)
|
||||
final offset = (globalTick * layer.scrollSpeed * 2).toInt();
|
||||
|
||||
for (var i = 0; i < layer.lines.length; i++) {
|
||||
final y = layer.yStart + i;
|
||||
if (y >= frameHeight) break;
|
||||
|
||||
final pattern = layer.lines[i];
|
||||
if (pattern.isEmpty) continue;
|
||||
|
||||
for (var x = 0; x < frameWidth; x++) {
|
||||
final patternIdx = (x + offset) % pattern.length;
|
||||
final char = pattern[patternIdx];
|
||||
if (char != ' ') {
|
||||
cells[y][x] = AsciiCell.fromChar(char);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return AsciiLayer(cells: cells, zIndex: 0);
|
||||
}
|
||||
|
||||
/// 걷는 캐릭터 레이어 생성 (z=1)
|
||||
/// Phase 4: 종족별 프레임 지원
|
||||
AsciiLayer _createCharacterLayer(int globalTick) {
|
||||
final frameIndex = globalTick % 4; // 4프레임 루프
|
||||
List<String> charFrame;
|
||||
|
||||
// 종족별 프레임 사용 시도
|
||||
if (raceId != null && raceId!.isNotEmpty) {
|
||||
final raceData = RaceCharacterFrames.get(raceId!);
|
||||
if (raceData != null) {
|
||||
// idle 프레임을 기반으로 걷기 애니메이션 생성
|
||||
final idleFrame = raceData.idle[frameIndex % raceData.idle.length];
|
||||
charFrame = _animateWalking(idleFrame.lines, frameIndex);
|
||||
} else {
|
||||
charFrame = _walkingFrames[frameIndex];
|
||||
}
|
||||
} else {
|
||||
charFrame = _walkingFrames[frameIndex];
|
||||
}
|
||||
|
||||
final cells = _spriteToCells(charFrame);
|
||||
|
||||
// 화면 중앙에 캐릭터 배치 (25% 위치)
|
||||
const charX = 15;
|
||||
// 바닥 레이어(Y=7) 위에 서있도록
|
||||
final charY = frameHeight - cells.length - 1;
|
||||
|
||||
return AsciiLayer(cells: cells, zIndex: 1, offsetX: charX, offsetY: charY);
|
||||
}
|
||||
|
||||
/// idle 프레임 기반 걷기 애니메이션 생성
|
||||
/// 종족별 다리 모양을 유지 (idle 프레임이 4개라 자연스럽게 변화)
|
||||
List<String> _animateWalking(List<String> idleLines, int frameIndex) {
|
||||
// idle 프레임을 그대로 사용 (종족별 다리 모양 유지)
|
||||
// frameIndex에 따라 idle[0~3] 중 하나가 선택되어 자연스럽게 애니메이션됨
|
||||
return idleLines;
|
||||
}
|
||||
|
||||
/// 문자열 스프라이트를 AsciiCell 2D 배열로 변환
|
||||
List<List<AsciiCell>> _spriteToCells(List<String> lines) {
|
||||
return lines.map((line) {
|
||||
return line.split('').map(AsciiCell.fromChar).toList();
|
||||
}).toList();
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 걷기 프레임 (4프레임 루프)
|
||||
// ============================================================================
|
||||
|
||||
const _walkingFrames = [
|
||||
// 프레임 1: 오른발 앞
|
||||
[r' o ', r' /|\ ', r' /| '],
|
||||
// 프레임 2: 모음
|
||||
[r' o ', r' /|\ ', r' |\ '],
|
||||
// 프레임 3: 왼발 앞
|
||||
[r' o ', r' /|\ ', r' /| '],
|
||||
// 프레임 4: 모음
|
||||
[r' o ', r' /|\ ', r' |\ '],
|
||||
];
|
||||
@@ -1,19 +0,0 @@
|
||||
import 'package:asciineverdie/src/core/animation/canvas/ascii_cell.dart';
|
||||
import 'package:asciineverdie/src/core/model/item_stats.dart';
|
||||
|
||||
/// 아이템 희귀도와 애니메이션 색상 간의 매핑
|
||||
///
|
||||
/// Clean Architecture 준수를 위해 model(ItemRarity) → animation(AsciiCellColor)
|
||||
/// 의존성을 animation 레이어에서 처리
|
||||
extension ItemRarityColorMapper on ItemRarity {
|
||||
/// 공격 이펙트 셀 색상 (Phase 9: 무기 등급별 이펙트)
|
||||
///
|
||||
/// common은 기본 positive(시안), 나머지는 등급별 고유 색상
|
||||
AsciiCellColor get effectCellColor => switch (this) {
|
||||
ItemRarity.common => AsciiCellColor.positive,
|
||||
ItemRarity.uncommon => AsciiCellColor.rarityUncommon,
|
||||
ItemRarity.rare => AsciiCellColor.rarityRare,
|
||||
ItemRarity.epic => AsciiCellColor.rarityEpic,
|
||||
ItemRarity.legendary => AsciiCellColor.rarityLegendary,
|
||||
};
|
||||
}
|
||||
@@ -1,128 +0,0 @@
|
||||
// 캐릭터 애니메이션 프레임 (8줄 Stone Story RPG 스타일)
|
||||
// 참조: Stone Story RPG - 상세하고 생동감 있는 ASCII 아트
|
||||
|
||||
/// 전투 페이즈
|
||||
enum BattlePhase {
|
||||
/// 대치 상태 (기본)
|
||||
idle,
|
||||
|
||||
/// 공격 준비
|
||||
prepare,
|
||||
|
||||
/// 공격 중
|
||||
attack,
|
||||
|
||||
/// 피격 (몬스터가 맞음)
|
||||
hit,
|
||||
|
||||
/// 복귀
|
||||
recover,
|
||||
}
|
||||
|
||||
/// 공격자 타입 (위치 계산용)
|
||||
enum AttackerType {
|
||||
/// 공격 없음 (idle 상태)
|
||||
none,
|
||||
|
||||
/// 플레이어가 공격
|
||||
player,
|
||||
|
||||
/// 몬스터가 공격
|
||||
monster,
|
||||
|
||||
/// 동시 공격 (양쪽 모두 이동)
|
||||
both,
|
||||
}
|
||||
|
||||
/// 캐릭터 프레임 데이터
|
||||
class CharacterFrame {
|
||||
const CharacterFrame(this.lines);
|
||||
|
||||
/// 프레임 데이터 (3줄)
|
||||
final List<String> lines;
|
||||
|
||||
/// 방패 오버레이 적용
|
||||
/// 3줄 캐릭터: [0]=머리, [1]=몸통/팔, [2]=다리
|
||||
CharacterFrame withShield() {
|
||||
if (lines.length < 2) return this;
|
||||
final newLines = List<String>.from(lines);
|
||||
// 몸통 줄(1번줄, 팔 위치)에 방패 추가
|
||||
final bodyIdx = 1;
|
||||
if (newLines[bodyIdx].length >= 2) {
|
||||
// 첫 두 문자를 방패로 대체
|
||||
newLines[bodyIdx] = '[]${newLines[bodyIdx].substring(2)}';
|
||||
} else {
|
||||
newLines[bodyIdx] = '[]${newLines[bodyIdx]}';
|
||||
}
|
||||
return CharacterFrame(newLines);
|
||||
}
|
||||
}
|
||||
|
||||
/// 특정 페이즈와 서브프레임에 해당하는 캐릭터 프레임 반환
|
||||
CharacterFrame getCharacterFrame(BattlePhase phase, int subFrame) {
|
||||
final frames = switch (phase) {
|
||||
BattlePhase.idle => _idleFrames,
|
||||
BattlePhase.prepare => _prepareFrames,
|
||||
BattlePhase.attack => _attackFrames,
|
||||
BattlePhase.hit => _hitFrames,
|
||||
BattlePhase.recover => _recoverFrames,
|
||||
};
|
||||
|
||||
final index = subFrame % frames.length;
|
||||
return frames[index];
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 대기 프레임 (숨쉬기 애니메이션) - 4프레임, 심플 3줄 스타일, 폭 6자
|
||||
// 구조: [머리, 몸통+팔, 다리]
|
||||
// ============================================================================
|
||||
const _idleFrames = [
|
||||
CharacterFrame([r' o ', r' /|\ ', r' / \ ']),
|
||||
CharacterFrame([r' o ', r' /|\ ', r' | | ']),
|
||||
CharacterFrame([r' o ', r' /|\ ', r' / \ ']),
|
||||
CharacterFrame([r' O ', r' /|\ ', r' / \ ']),
|
||||
];
|
||||
|
||||
// ============================================================================
|
||||
// 준비 프레임 (무기 들기) - 3프레임, 심플 3줄 스타일, 폭 6자
|
||||
// 구조: [머리, 몸통+팔, 다리]
|
||||
// ============================================================================
|
||||
const _prepareFrames = [
|
||||
CharacterFrame([r' o ', r' \|\ ', r' / \ ']),
|
||||
CharacterFrame([r' o_ ', r' \| ', r' / \ ']),
|
||||
CharacterFrame([r' o/ ', r' \| ', r' / \ ']),
|
||||
];
|
||||
|
||||
// ============================================================================
|
||||
// 공격 프레임 (전진 + 휘두르기) - 5프레임, 심플 3줄 스타일
|
||||
// 구조: [머리+공격, 몸통+팔, 다리]
|
||||
// 수정: 공격 이펙트를 머리 줄로 통일 (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' / \ ']),
|
||||
];
|
||||
|
||||
// ============================================================================
|
||||
// 히트 프레임 (공격 명중) - 3프레임, 심플 3줄 스타일
|
||||
// 구조: [머리+이펙트, 몸통+팔, 다리]
|
||||
// 수정: 히트 이펙트를 머리 줄로 통일 (1칸 위로)
|
||||
// ============================================================================
|
||||
const _hitFrames = [
|
||||
CharacterFrame([r' o-* ', r' /| ', r' / \ ']),
|
||||
CharacterFrame([r' o=* ', r' /| ', r' / \ ']),
|
||||
CharacterFrame([r' o~* ', r' /| ', r' / \ ']),
|
||||
];
|
||||
|
||||
// ============================================================================
|
||||
// 복귀 프레임 - 3프레임, 심플 3줄 스타일
|
||||
// 구조: [머리, 몸통+팔, 다리]
|
||||
// ============================================================================
|
||||
const _recoverFrames = [
|
||||
CharacterFrame([r' o ', r' /|\ ', r' | ']),
|
||||
CharacterFrame([r' o ', r' /|\ ', r' / \ ']),
|
||||
CharacterFrame([r' o ', r' /|\ ', r' / \ ']),
|
||||
];
|
||||
@@ -1,192 +0,0 @@
|
||||
// 프론트 화면용 Hero vs Glitch God ASCII 애니메이션 데이터
|
||||
// 작은 용사(3줄)가 거대한 Glitch God(10줄)에 맞서는 장면
|
||||
// 캐릭터 해부학: 머리(1개), 양팔(2개), 양다리(2개)
|
||||
|
||||
/// 애니메이션 프레임 (10줄, 12프레임 루프)
|
||||
const frontScreenAnimationFrames = [
|
||||
// ========================================================================
|
||||
// 프레임 0: 대치 상태 1 (숨쉬기 - 들숨)
|
||||
// ========================================================================
|
||||
'''
|
||||
░░▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓░░
|
||||
░▓▓ G L I T C H ▓▓░
|
||||
░▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓░
|
||||
░▓▓▓ ◈◈ ◈◈ ▓▓▓░
|
||||
░▓▓▓▓ ▼▼▼ ▓▓▓▓░
|
||||
░▓▓▓▓▓ ████████ ▓▓▓▓▓░
|
||||
o ░▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓░
|
||||
/|\\ ░▓▓▓ G O D ▓▓▓░
|
||||
/ \\ ░▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓░
|
||||
~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~''',
|
||||
|
||||
// ========================================================================
|
||||
// 프레임 1: 대치 상태 2 (숨쉬기 - 날숨)
|
||||
// ========================================================================
|
||||
'''
|
||||
░░▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓░░
|
||||
░▓▓ G L I T C H ▓▓░
|
||||
░▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓░
|
||||
░▓▓▓ ◉◉ ◉◉ ▓▓▓░
|
||||
░▓▓▓▓ ~~~ ▓▓▓▓░
|
||||
░▓▓▓▓▓ ████████ ▓▓▓▓▓░
|
||||
O ░▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓░
|
||||
/|\\ ░▓▓▓ G O D ▓▓▓░
|
||||
| | ░▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓░
|
||||
~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~''',
|
||||
|
||||
// ========================================================================
|
||||
// 프레임 2: 용사 전진 1
|
||||
// ========================================================================
|
||||
'''
|
||||
▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓
|
||||
▓▓ G L I T C H ▓▓
|
||||
▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓
|
||||
▓▓▓▓ ◈◈ ◈◈ ▓▓▓▓
|
||||
▓▓▓▓▓ ▼▼▼ ▓▓▓▓▓
|
||||
o ▓▓▓▓▓▓ ████████ ▓▓▓▓▓▓
|
||||
/|\\ ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓
|
||||
/ \\ ▓▓▓▓ G O D ▓▓▓▓
|
||||
▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓
|
||||
~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~''',
|
||||
|
||||
// ========================================================================
|
||||
// 프레임 3: 용사 전진 2
|
||||
// ========================================================================
|
||||
'''
|
||||
▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓
|
||||
▓▓ G L I T C H ▓▓
|
||||
▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓
|
||||
▓▓▓▓ ◉◉ ◉◉ ▓▓▓▓
|
||||
▓▓▓▓▓ ~~~ ▓▓▓▓▓
|
||||
o ▓▓▓▓▓▓ ████████ ▓▓▓▓▓▓
|
||||
/|\\ ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓
|
||||
| | ▓▓▓▓ G O D ▓▓▓▓
|
||||
▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓
|
||||
~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~''',
|
||||
|
||||
// ========================================================================
|
||||
// 프레임 4: 공격 준비 1 (무기 들기)
|
||||
// ========================================================================
|
||||
'''
|
||||
▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▒
|
||||
▒▓ G L I T C H ▓▒
|
||||
▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▒
|
||||
▒▓▓▓ ◈◈ ◈◈ ▓▓▓▒
|
||||
▒▓▓▓▓ ▼▼▼ ▓▓▓▓▒
|
||||
o ▒▓▓▓▓▓ ████████ ▓▓▓▓▓▒
|
||||
\\| ▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▒
|
||||
/ \\ ▒▓▓▓ G O D ▓▓▓▒
|
||||
▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▒
|
||||
~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~''',
|
||||
|
||||
// ========================================================================
|
||||
// 프레임 5: 공격 준비 2 (무기 뒤로)
|
||||
// ========================================================================
|
||||
'''
|
||||
▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▒
|
||||
▒▓ G L I T C H ▓▒
|
||||
▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▒
|
||||
▒▓▓▓ ◉◉ ◉◉ ▓▓▓▒
|
||||
▒▓▓▓▓ ~~~ ▓▓▓▓▒
|
||||
o ▒▓▓▓▓▓ ████████ ▓▓▓▓▓▒
|
||||
\\|/ ▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▒
|
||||
/ \\ ▒▓▓▓ G O D ▓▓▓▒
|
||||
▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▒
|
||||
~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~''',
|
||||
|
||||
// ========================================================================
|
||||
// 프레임 6: 공격 1 (휘두르기)
|
||||
// ========================================================================
|
||||
'''
|
||||
░▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓░
|
||||
░▓ G L I T C H ▓░
|
||||
░▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓░
|
||||
░▓▓▓ ◈◈ ◈◈ ▓▓▓░
|
||||
░▓▓▓▓ ▼▼▼ ▓▓▓▓░
|
||||
o ░▓▓▓▓▓ ████████ ▓▓▓▓▓░
|
||||
\\|- ░▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓░
|
||||
/ \\ ░▓▓▓ G O D ▓▓▓░
|
||||
░▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓░
|
||||
~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~''',
|
||||
|
||||
// ========================================================================
|
||||
// 프레임 7: 공격 2 (이펙트 + 히트)
|
||||
// ========================================================================
|
||||
'''
|
||||
░▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓░
|
||||
░▓ G#L@I*T&C!H ▓░
|
||||
░▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓░
|
||||
░▓▓▓ X X X X ▓▓▓░
|
||||
░▓▓▓▓ !!! ▓▓▓▓░
|
||||
o ░▓▓▓▓▓ ████████ ▓▓▓▓▓░
|
||||
\\|-=>* ░▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓░
|
||||
/ \\ ░▓▓▓ G O D ▓▓▓░
|
||||
░▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓░
|
||||
~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~''',
|
||||
|
||||
// ========================================================================
|
||||
// 프레임 8: 보스 반격 준비 1
|
||||
// ========================================================================
|
||||
'''
|
||||
▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓
|
||||
▓▓ G L I T C H ▓▓
|
||||
▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓
|
||||
▓▓▓▓ @@ @@ ▓▓▓▓
|
||||
▓▓▓▓▓ <=== ▓▓▓▓▓
|
||||
o ▓▓▓▓▓▓ ████████ ▓▓▓▓▓▓
|
||||
/|\\ ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓
|
||||
/ \\ ▓▓▓▓ G O D ▓▓▓▓
|
||||
▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓
|
||||
~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~''',
|
||||
|
||||
// ========================================================================
|
||||
// 프레임 9: 보스 반격 2 (글리치 증가)
|
||||
// ========================================================================
|
||||
'''
|
||||
▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▒
|
||||
▒▓ G L I T C H ▓▒
|
||||
▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▒
|
||||
▒▓▓▓ ◉◉ ◉◉ ▓▓▓▒
|
||||
▒▓▓▓▓ <====== ▓▓▓▓▒
|
||||
o ▒▓▓▓▓▓ ████████ ▓▓▓▓▓▒
|
||||
/|\\ ▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▒
|
||||
/ \\ ▒▓▓▓ G O D ▓▓▓▒
|
||||
▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▒
|
||||
~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~''',
|
||||
|
||||
// ========================================================================
|
||||
// 프레임 10: 방어 1 (방패 들기)
|
||||
// ========================================================================
|
||||
'''
|
||||
▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓
|
||||
▓▓ G L I T C H ▓▓
|
||||
▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓
|
||||
▓▓▓▓ @@ @@ ▓▓▓▓
|
||||
▓▓▓▓▓ <==== ▓▓▓▓▓
|
||||
o[] ▓▓▓▓▓▓ ████████ ▓▓▓▓▓▓
|
||||
/| ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓
|
||||
/ \\ ▓▓▓▓ G O D ▓▓▓▓
|
||||
▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓
|
||||
~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~''',
|
||||
|
||||
// ========================================================================
|
||||
// 프레임 11: 방어 2 (충격파 막기)
|
||||
// ========================================================================
|
||||
'''
|
||||
▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▒
|
||||
▒▓ G L I T C H ▓▒
|
||||
▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▒
|
||||
▒▓▓▓ ◉◉ ◉◉ ▓▓▓▒
|
||||
▒▓▓▓▓ <===== ▓▓▓▓▒
|
||||
o[]* ▒▓▓▓▓▓ ████████ ▓▓▓▓▓▒
|
||||
/| ▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▒
|
||||
/ \\ ▒▓▓▓ G O D ▓▓▓▒
|
||||
▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▒
|
||||
~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~''',
|
||||
];
|
||||
|
||||
/// 애니메이션 프레임 간격 (밀리초)
|
||||
const frontScreenAnimationIntervalMs = 350;
|
||||
|
||||
/// 애니메이션 총 프레임 수
|
||||
const frontScreenAnimationFrameCount = 12;
|
||||
@@ -1,105 +0,0 @@
|
||||
// 몬스터 크기 시스템
|
||||
// Act와 몬스터 등급에 따라 ASCII 아트 크기 결정
|
||||
|
||||
import 'package:asciineverdie/src/core/model/monster_grade.dart';
|
||||
import 'package:asciineverdie/src/core/util/deterministic_random.dart';
|
||||
|
||||
/// 몬스터 크기 enum
|
||||
/// 실제 프레임 줄 수와 일치하도록 설정
|
||||
enum MonsterSize {
|
||||
/// 2줄 (레벨 1-5)
|
||||
tiny(2),
|
||||
|
||||
/// 4줄 (레벨 6-10)
|
||||
small(4),
|
||||
|
||||
/// 6줄 (레벨 11-15)
|
||||
medium(6),
|
||||
|
||||
/// 8줄 (레벨 16-25)
|
||||
large(8),
|
||||
|
||||
/// 8줄 (레벨 26-35)
|
||||
huge(8),
|
||||
|
||||
/// 8줄 (레벨 36-50)
|
||||
giant(8),
|
||||
|
||||
/// 8줄 (레벨 51+, 보스급)
|
||||
titanic(8);
|
||||
|
||||
const MonsterSize(this.lines);
|
||||
|
||||
/// 해당 크기의 줄 수
|
||||
final int lines;
|
||||
}
|
||||
|
||||
/// 몬스터 레벨에서 크기 결정
|
||||
MonsterSize getMonsterSize(int? level) {
|
||||
if (level == null || level <= 0) return MonsterSize.tiny;
|
||||
|
||||
if (level <= 5) return MonsterSize.tiny;
|
||||
if (level <= 10) return MonsterSize.small;
|
||||
if (level <= 15) return MonsterSize.medium;
|
||||
if (level <= 25) return MonsterSize.large;
|
||||
if (level <= 35) return MonsterSize.huge;
|
||||
if (level <= 50) return MonsterSize.giant;
|
||||
return MonsterSize.titanic;
|
||||
}
|
||||
|
||||
/// 몬스터 크기에 따른 세로 패딩 계산 (7줄 프레임에서 중앙 정렬)
|
||||
int getMonsterVerticalPadding(MonsterSize size) {
|
||||
return (7 - size.lines) ~/ 2;
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Act 기반 몬스터 사이즈 결정 (Phase 13)
|
||||
// =============================================================================
|
||||
|
||||
/// 보스 몬스터 사이즈 결정 (Act별 고정)
|
||||
///
|
||||
/// - Prologue(1), Act I(2): small (4줄)
|
||||
/// - Act II(3), Act III(4): medium (6줄)
|
||||
/// - Act IV(5), Act V(6)+: large (8줄)
|
||||
MonsterSize getBossSizeForAct(int plotStageCount) {
|
||||
if (plotStageCount <= 2) return MonsterSize.small;
|
||||
if (plotStageCount <= 4) return MonsterSize.medium;
|
||||
return MonsterSize.large;
|
||||
}
|
||||
|
||||
/// 일반/엘리트 몬스터 사이즈 결정 (Act별 확률 랜덤)
|
||||
///
|
||||
/// Act가 진행될수록 큰 몬스터가 나올 확률 증가.
|
||||
/// 프롤로그에서는 대형 몬스터 0%.
|
||||
MonsterSize getRandomSizeForAct(int plotStageCount, DeterministicRandom rng) {
|
||||
final roll = rng.nextInt(100);
|
||||
|
||||
// Act별 확률 테이블 (small %, medium %, large = 나머지)
|
||||
final (smallChance, mediumChance) = switch (plotStageCount) {
|
||||
<= 1 => (70, 30), // Prologue: 70% small, 30% medium, 0% large
|
||||
2 => (50, 40), // Act I: 50% small, 40% medium, 10% large
|
||||
3 => (35, 40), // Act II: 35% small, 40% medium, 25% large
|
||||
4 => (25, 40), // Act III: 25% small, 40% medium, 35% large
|
||||
5 => (15, 35), // Act IV: 15% small, 35% medium, 50% large
|
||||
_ => (10, 25), // Act V+: 10% small, 25% medium, 65% large
|
||||
};
|
||||
|
||||
if (roll < smallChance) return MonsterSize.small;
|
||||
if (roll < smallChance + mediumChance) return MonsterSize.medium;
|
||||
return MonsterSize.large;
|
||||
}
|
||||
|
||||
/// 몬스터 등급과 Act에 따른 사이즈 결정 (통합 함수)
|
||||
///
|
||||
/// 보스: Act별 고정 사이즈
|
||||
/// 일반/엘리트: Act별 확률 랜덤
|
||||
MonsterSize getMonsterSizeForAct({
|
||||
required int plotStageCount,
|
||||
required MonsterGrade grade,
|
||||
required DeterministicRandom rng,
|
||||
}) {
|
||||
if (grade == MonsterGrade.boss) {
|
||||
return getBossSizeForAct(plotStageCount);
|
||||
}
|
||||
return getRandomSizeForAct(plotStageCount, rng);
|
||||
}
|
||||
@@ -1,754 +0,0 @@
|
||||
// 종족별 ASCII 캐릭터 프레임 데이터
|
||||
// 모든 캐릭터는 3줄 × 6자 폭으로 통일 (보스 10줄과 대비)
|
||||
|
||||
import 'package:asciineverdie/src/core/animation/character_frames.dart';
|
||||
|
||||
/// 종족별 캐릭터 프레임 저장소
|
||||
class RaceCharacterFrames {
|
||||
RaceCharacterFrames._();
|
||||
|
||||
/// 종족 ID로 프레임 데이터 조회
|
||||
static RaceFrameData? get(String raceId) => _raceFrames[raceId];
|
||||
|
||||
/// 기본 프레임 (종족 미지정 시)
|
||||
static RaceFrameData get defaultFrames => _raceFrames['byte_human']!;
|
||||
|
||||
/// 모든 종족 ID 목록
|
||||
static List<String> get allRaceIds => _raceFrames.keys.toList();
|
||||
}
|
||||
|
||||
/// 종족별 프레임 데이터
|
||||
class RaceFrameData {
|
||||
const RaceFrameData({
|
||||
required this.raceId,
|
||||
required this.idle,
|
||||
required this.prepare,
|
||||
required this.attack,
|
||||
required this.hit,
|
||||
required this.recover,
|
||||
});
|
||||
|
||||
final String raceId;
|
||||
final List<CharacterFrame> idle; // 4 프레임
|
||||
final List<CharacterFrame> prepare; // 3 프레임
|
||||
final List<CharacterFrame> attack; // 5 프레임
|
||||
final List<CharacterFrame> hit; // 3 프레임
|
||||
final List<CharacterFrame> recover; // 3 프레임
|
||||
|
||||
/// BattlePhase로 프레임 목록 조회
|
||||
List<CharacterFrame> getFrames(BattlePhase phase) {
|
||||
return switch (phase) {
|
||||
BattlePhase.idle => idle,
|
||||
BattlePhase.prepare => prepare,
|
||||
BattlePhase.attack => attack,
|
||||
BattlePhase.hit => hit,
|
||||
BattlePhase.recover => recover,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 종족별 프레임 데이터 (21개 종족)
|
||||
// 구조: [머리, 몸통+팔, 다리] - 모두 6자 폭
|
||||
// ============================================================================
|
||||
|
||||
const _raceFrames = <String, RaceFrameData>{
|
||||
// --------------------------------------------------------------------------
|
||||
// 기본형 (Byte Human, Buffer Dwarf)
|
||||
// --------------------------------------------------------------------------
|
||||
'byte_human': RaceFrameData(
|
||||
raceId: 'byte_human',
|
||||
idle: [
|
||||
CharacterFrame([r' o ', r' /|\ ', r' / \ ']),
|
||||
CharacterFrame([r' o ', r' /|\ ', r' | | ']),
|
||||
CharacterFrame([r' o ', r' /|\ ', r' / \ ']),
|
||||
CharacterFrame([r' O ', r' /|\ ', r' / \ ']),
|
||||
],
|
||||
prepare: [
|
||||
CharacterFrame([r' o ', r' \|\ ', r' / \ ']),
|
||||
CharacterFrame([r' o ', r' \| ', r' / \ ']),
|
||||
CharacterFrame([r' o ', r' \|/ ', r' / \ ']),
|
||||
],
|
||||
attack: [
|
||||
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' / \ ']),
|
||||
],
|
||||
hit: [
|
||||
CharacterFrame([r' o ', r'/|\* ', r' / \ ']),
|
||||
CharacterFrame([r' o ', r'/|\ * ', r' / \ ']),
|
||||
CharacterFrame([r' o ', r' /|\ ', r' / \ ']),
|
||||
],
|
||||
recover: [
|
||||
CharacterFrame([r' o ', r' /|\ ', r' | ']),
|
||||
CharacterFrame([r' o ', r' /|\ ', r' / \ ']),
|
||||
CharacterFrame([r' o ', r' /|\ ', r' / \ ']),
|
||||
],
|
||||
),
|
||||
|
||||
'buffer_dwarf': RaceFrameData(
|
||||
raceId: 'buffer_dwarf',
|
||||
idle: [
|
||||
CharacterFrame([r' o ', r' /█\ ', r' / \ ']),
|
||||
CharacterFrame([r' o ', r' /█\ ', r' | | ']),
|
||||
CharacterFrame([r' o ', r' /█\ ', r' / \ ']),
|
||||
CharacterFrame([r' O ', r' /█\ ', r' / \ ']),
|
||||
],
|
||||
prepare: [
|
||||
CharacterFrame([r' o ', r' \█\ ', r' / \ ']),
|
||||
CharacterFrame([r' o ', r' \█ ', r' / \ ']),
|
||||
CharacterFrame([r' o ', r' \█/ ', r' / \ ']),
|
||||
],
|
||||
attack: [
|
||||
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' / \ ']),
|
||||
],
|
||||
hit: [
|
||||
CharacterFrame([r' o ', r'/█\* ', r' / \ ']),
|
||||
CharacterFrame([r' o ', r'/█\ * ', r' / \ ']),
|
||||
CharacterFrame([r' o ', r' /█\ ', r' / \ ']),
|
||||
],
|
||||
recover: [
|
||||
CharacterFrame([r' o ', r' /█\ ', r' | ']),
|
||||
CharacterFrame([r' o ', r' /█\ ', r' / \ ']),
|
||||
CharacterFrame([r' o ', r' /█\ ', r' / \ ']),
|
||||
],
|
||||
),
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// 대형 (Kernel Giant, Heap Troll)
|
||||
// --------------------------------------------------------------------------
|
||||
'kernel_giant': RaceFrameData(
|
||||
raceId: 'kernel_giant',
|
||||
idle: [
|
||||
CharacterFrame([r' O ', r' /█\ ', r' ┘ └ ']),
|
||||
CharacterFrame([r' O ', r' /█\ ', r' | | ']),
|
||||
CharacterFrame([r' O ', r' /█\ ', r' ┘ └ ']),
|
||||
CharacterFrame([r' O ', r' /█\ ', r' ┘ └ ']),
|
||||
],
|
||||
prepare: [
|
||||
CharacterFrame([r' O ', r' \█\ ', r' ┘ └ ']),
|
||||
CharacterFrame([r' O ', r' \█ ', r' ┘ └ ']),
|
||||
CharacterFrame([r' O ', r' \█/ ', r' ┘ └ ']),
|
||||
],
|
||||
attack: [
|
||||
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' ┘ └ ']),
|
||||
],
|
||||
hit: [
|
||||
CharacterFrame([r' O ', r'/█\* ', r' ┘ └ ']),
|
||||
CharacterFrame([r' O ', r'/█\ * ', r' ┘ └ ']),
|
||||
CharacterFrame([r' O ', r' /█\ ', r' ┘ └ ']),
|
||||
],
|
||||
recover: [
|
||||
CharacterFrame([r' O ', r' /█\ ', r' || ']),
|
||||
CharacterFrame([r' O ', r' /█\ ', r' ┘ └ ']),
|
||||
CharacterFrame([r' O ', r' /█\ ', r' ┘ └ ']),
|
||||
],
|
||||
),
|
||||
|
||||
'heap_troll': RaceFrameData(
|
||||
raceId: 'heap_troll',
|
||||
idle: [
|
||||
CharacterFrame([r' Ö ', r' /▓\ ', r' ┘ └ ']),
|
||||
CharacterFrame([r' Ö ', r' /▓\ ', r' | | ']),
|
||||
CharacterFrame([r' Ö ', r' /▓\ ', r' ┘ └ ']),
|
||||
CharacterFrame([r' Ö ', r' /▓\ ', r' ┘ └ ']),
|
||||
],
|
||||
prepare: [
|
||||
CharacterFrame([r' Ö ', r' \▓\ ', r' ┘ └ ']),
|
||||
CharacterFrame([r' Ö ', r' \▓ ', r' ┘ └ ']),
|
||||
CharacterFrame([r' Ö ', r' \▓/ ', r' ┘ └ ']),
|
||||
],
|
||||
attack: [
|
||||
CharacterFrame([r' Ö ', r' \▓ ', r' ┘ └ ']),
|
||||
CharacterFrame([r' Ö ', r' \▓- ', r' ┘ └ ']),
|
||||
CharacterFrame([r' Ö ', r' \▓-- ', r' ┘ └ ']),
|
||||
CharacterFrame([r' Ö ', r'\▓-=> ', r' ┘ └ ']),
|
||||
CharacterFrame([r' Ö ', r' /▓\ ', r' ┘ └ ']),
|
||||
],
|
||||
hit: [
|
||||
CharacterFrame([r' Ö ', r'/▓\* ', r' ┘ └ ']),
|
||||
CharacterFrame([r' Ö ', r'/▓\ * ', r' ┘ └ ']),
|
||||
CharacterFrame([r' Ö ', r' /▓\ ', r' ┘ └ ']),
|
||||
],
|
||||
recover: [
|
||||
CharacterFrame([r' Ö ', r' /▓\ ', r' || ']),
|
||||
CharacterFrame([r' Ö ', r' /▓\ ', r' ┘ └ ']),
|
||||
CharacterFrame([r' Ö ', r' /▓\ ', r' ┘ └ ']),
|
||||
],
|
||||
),
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// 전사형 (Protocol Valkyrie, Flag Golem, Array Orc)
|
||||
// --------------------------------------------------------------------------
|
||||
'protocol_valkyrie': RaceFrameData(
|
||||
raceId: 'protocol_valkyrie',
|
||||
idle: [
|
||||
CharacterFrame([r' o ', r' /|+ ', r' / \ ']),
|
||||
CharacterFrame([r' o ', r' /|+ ', r' | | ']),
|
||||
CharacterFrame([r' o ', r' /|+ ', r' / \ ']),
|
||||
CharacterFrame([r' O ', r' /|+ ', r' / \ ']),
|
||||
],
|
||||
prepare: [
|
||||
CharacterFrame([r' o ', r' \|+ ', r' / \ ']),
|
||||
CharacterFrame([r' o ', r' \| ', r' / \ ']),
|
||||
CharacterFrame([r' o ', r' \|/ ', r' / \ ']),
|
||||
],
|
||||
attack: [
|
||||
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' / \ ']),
|
||||
],
|
||||
hit: [
|
||||
CharacterFrame([r' o ', r'/|+* ', r' / \ ']),
|
||||
CharacterFrame([r' o ', r'/|+ * ', r' / \ ']),
|
||||
CharacterFrame([r' o ', r' /|+ ', r' / \ ']),
|
||||
],
|
||||
recover: [
|
||||
CharacterFrame([r' o ', r' /|+ ', r' | ']),
|
||||
CharacterFrame([r' o ', r' /|+ ', r' / \ ']),
|
||||
CharacterFrame([r' o ', r' /|+ ', r' / \ ']),
|
||||
],
|
||||
),
|
||||
|
||||
'flag_golem': RaceFrameData(
|
||||
raceId: 'flag_golem',
|
||||
idle: [
|
||||
CharacterFrame([r' ♦ ', r' /|] ', r' / \ ']),
|
||||
CharacterFrame([r' ♦ ', r' /|] ', r' | | ']),
|
||||
CharacterFrame([r' ♦ ', r' /|] ', r' / \ ']),
|
||||
CharacterFrame([r' ◆ ', r' /|] ', r' / \ ']),
|
||||
],
|
||||
prepare: [
|
||||
CharacterFrame([r' ♦ ', r' \|] ', r' / \ ']),
|
||||
CharacterFrame([r' ♦ ', r' \| ', r' / \ ']),
|
||||
CharacterFrame([r' ♦ ', r' \|/ ', r' / \ ']),
|
||||
],
|
||||
attack: [
|
||||
CharacterFrame([r' ♦ ', r' \| ', r' / \ ']),
|
||||
CharacterFrame([r' ♦ ', r' \|- ', r' / \ ']),
|
||||
CharacterFrame([r' ♦ ', r' \|-- ', r' / \ ']),
|
||||
CharacterFrame([r' ♦ ', r'\|--> ', r' / \ ']),
|
||||
CharacterFrame([r' ♦ ', r' /|] ', r' / \ ']),
|
||||
],
|
||||
hit: [
|
||||
CharacterFrame([r' ♦ ', r'/|]* ', r' / \ ']),
|
||||
CharacterFrame([r' ♦ ', r'/|] * ', r' / \ ']),
|
||||
CharacterFrame([r' ♦ ', r' /|] ', r' / \ ']),
|
||||
],
|
||||
recover: [
|
||||
CharacterFrame([r' ♦ ', r' /|] ', r' | ']),
|
||||
CharacterFrame([r' ♦ ', r' /|] ', r' / \ ']),
|
||||
CharacterFrame([r' ♦ ', r' /|] ', r' / \ ']),
|
||||
],
|
||||
),
|
||||
|
||||
'array_orc': RaceFrameData(
|
||||
raceId: 'array_orc',
|
||||
idle: [
|
||||
CharacterFrame([r' ö ', r' /▒\ ', r' / \ ']),
|
||||
CharacterFrame([r' ö ', r' /▒\ ', r' | | ']),
|
||||
CharacterFrame([r' ö ', r' /▒\ ', r' / \ ']),
|
||||
CharacterFrame([r' Ö ', r' /▒\ ', r' / \ ']),
|
||||
],
|
||||
prepare: [
|
||||
CharacterFrame([r' ö ', r' \▒\ ', r' / \ ']),
|
||||
CharacterFrame([r' ö ', r' \▒ ', r' / \ ']),
|
||||
CharacterFrame([r' ö ', r' \▒/ ', r' / \ ']),
|
||||
],
|
||||
attack: [
|
||||
CharacterFrame([r' ö ', r' \▒ ', r' / \ ']),
|
||||
CharacterFrame([r' ö ', r' \▒- ', r' / \ ']),
|
||||
CharacterFrame([r' ö ', r' \▒-- ', r' / \ ']),
|
||||
CharacterFrame([r' ö ', r'\▒-=> ', r' / \ ']),
|
||||
CharacterFrame([r' ö ', r' /▒\ ', r' / \ ']),
|
||||
],
|
||||
hit: [
|
||||
CharacterFrame([r' ö ', r'/▒\* ', r' / \ ']),
|
||||
CharacterFrame([r' ö ', r'/▒\ * ', r' / \ ']),
|
||||
CharacterFrame([r' ö ', r' /▒\ ', r' / \ ']),
|
||||
],
|
||||
recover: [
|
||||
CharacterFrame([r' ö ', r' /▒\ ', r' | ']),
|
||||
CharacterFrame([r' ö ', r' /▒\ ', r' / \ ']),
|
||||
CharacterFrame([r' ö ', r' /▒\ ', r' / \ ']),
|
||||
],
|
||||
),
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// 마법형 (Loop Djinn, Lambda Dryad, Recursive Sylvan)
|
||||
// --------------------------------------------------------------------------
|
||||
'loop_djinn': RaceFrameData(
|
||||
raceId: 'loop_djinn',
|
||||
idle: [
|
||||
CharacterFrame([r' o~ ', r' /|) ', r' / \ ']),
|
||||
CharacterFrame([r' o~ ', r' /|) ', r' | | ']),
|
||||
CharacterFrame([r' o~ ', r' /|) ', r' / \ ']),
|
||||
CharacterFrame([r' O~ ', r' /|) ', r' / \ ']),
|
||||
],
|
||||
prepare: [
|
||||
CharacterFrame([r' o~ ', r' \|) ', r' / \ ']),
|
||||
CharacterFrame([r' o~ ', r' \| ', r' / \ ']),
|
||||
CharacterFrame([r' o~ ', r' \|/ ', r' / \ ']),
|
||||
],
|
||||
attack: [
|
||||
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' / \ ']),
|
||||
],
|
||||
hit: [
|
||||
CharacterFrame([r' o~ ', r'/|)* ', r' / \ ']),
|
||||
CharacterFrame([r' o~ ', r'/|) * ', r' / \ ']),
|
||||
CharacterFrame([r' o~ ', r' /|) ', r' / \ ']),
|
||||
],
|
||||
recover: [
|
||||
CharacterFrame([r' o~ ', r' /|) ', r' | ']),
|
||||
CharacterFrame([r' o~ ', r' /|) ', r' / \ ']),
|
||||
CharacterFrame([r' o~ ', r' /|) ', r' / \ ']),
|
||||
],
|
||||
),
|
||||
|
||||
'lambda_dryad': RaceFrameData(
|
||||
raceId: 'lambda_dryad',
|
||||
idle: [
|
||||
CharacterFrame([r' o♣ ', r' /|) ', r' / \ ']),
|
||||
CharacterFrame([r' o♣ ', r' /|) ', r' | | ']),
|
||||
CharacterFrame([r' o♣ ', r' /|) ', r' / \ ']),
|
||||
CharacterFrame([r' O♣ ', r' /|) ', r' / \ ']),
|
||||
],
|
||||
prepare: [
|
||||
CharacterFrame([r' o♣ ', r' \|) ', r' / \ ']),
|
||||
CharacterFrame([r' o♣ ', r' \| ', r' / \ ']),
|
||||
CharacterFrame([r' o♣ ', r' \|/ ', r' / \ ']),
|
||||
],
|
||||
attack: [
|
||||
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' / \ ']),
|
||||
],
|
||||
hit: [
|
||||
CharacterFrame([r' o♣ ', r'/|)* ', r' / \ ']),
|
||||
CharacterFrame([r' o♣ ', r'/|) * ', r' / \ ']),
|
||||
CharacterFrame([r' o♣ ', r' /|) ', r' / \ ']),
|
||||
],
|
||||
recover: [
|
||||
CharacterFrame([r' o♣ ', r' /|) ', r' | ']),
|
||||
CharacterFrame([r' o♣ ', r' /|) ', r' / \ ']),
|
||||
CharacterFrame([r' o♣ ', r' /|) ', r' / \ ']),
|
||||
],
|
||||
),
|
||||
|
||||
'recursive_sylvan': RaceFrameData(
|
||||
raceId: 'recursive_sylvan',
|
||||
idle: [
|
||||
CharacterFrame([r' o∞ ', r' /|) ', r' / \ ']),
|
||||
CharacterFrame([r' o∞ ', r' /|) ', r' | | ']),
|
||||
CharacterFrame([r' o∞ ', r' /|) ', r' / \ ']),
|
||||
CharacterFrame([r' O∞ ', r' /|) ', r' / \ ']),
|
||||
],
|
||||
prepare: [
|
||||
CharacterFrame([r' o∞ ', r' \|) ', r' / \ ']),
|
||||
CharacterFrame([r' o∞ ', r' \| ', r' / \ ']),
|
||||
CharacterFrame([r' o∞ ', r' \|/ ', r' / \ ']),
|
||||
],
|
||||
attack: [
|
||||
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' / \ ']),
|
||||
],
|
||||
hit: [
|
||||
CharacterFrame([r' o∞ ', r'/|)* ', r' / \ ']),
|
||||
CharacterFrame([r' o∞ ', r'/|) * ', r' / \ ']),
|
||||
CharacterFrame([r' o∞ ', r' /|) ', r' / \ ']),
|
||||
],
|
||||
recover: [
|
||||
CharacterFrame([r' o∞ ', r' /|) ', r' | ']),
|
||||
CharacterFrame([r' o∞ ', r' /|) ', r' / \ ']),
|
||||
CharacterFrame([r' o∞ ', r' /|) ', r' / \ ']),
|
||||
],
|
||||
),
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// 언데드/엘프 (Coredump Undead, Null Elf, Callback Seraph)
|
||||
// --------------------------------------------------------------------------
|
||||
'coredump_undead': RaceFrameData(
|
||||
raceId: 'coredump_undead',
|
||||
idle: [
|
||||
CharacterFrame([r' ☠ ', r' /|\ ', r' / \ ']),
|
||||
CharacterFrame([r' ☠ ', r' /|\ ', r' | | ']),
|
||||
CharacterFrame([r' ☠ ', r' /|\ ', r' / \ ']),
|
||||
CharacterFrame([r' ☠ ', r' /|\ ', r' / \ ']),
|
||||
],
|
||||
prepare: [
|
||||
CharacterFrame([r' ☠ ', r' \|\ ', r' / \ ']),
|
||||
CharacterFrame([r' ☠ ', r' \| ', r' / \ ']),
|
||||
CharacterFrame([r' ☠ ', r' \|/ ', r' / \ ']),
|
||||
],
|
||||
attack: [
|
||||
CharacterFrame([r' ☠ ', r' \| ', r' / \ ']),
|
||||
CharacterFrame([r' ☠ ', r' \|- ', r' / \ ']),
|
||||
CharacterFrame([r' ☠ ', r' \|-- ', r' / \ ']),
|
||||
CharacterFrame([r' ☠ ', r'\|-~> ', r' / \ ']),
|
||||
CharacterFrame([r' ☠ ', r' /|\ ', r' / \ ']),
|
||||
],
|
||||
hit: [
|
||||
CharacterFrame([r' ☠ ', r'/|\~ ', r' / \ ']),
|
||||
CharacterFrame([r' ☠ ', r'/|\ ~ ', r' / \ ']),
|
||||
CharacterFrame([r' ☠ ', r' /|\ ', r' / \ ']),
|
||||
],
|
||||
recover: [
|
||||
CharacterFrame([r' ☠ ', r' /|\ ', r' | ']),
|
||||
CharacterFrame([r' ☠ ', r' /|\ ', r' / \ ']),
|
||||
CharacterFrame([r' ☠ ', r' /|\ ', r' / \ ']),
|
||||
],
|
||||
),
|
||||
|
||||
'null_elf': RaceFrameData(
|
||||
raceId: 'null_elf',
|
||||
idle: [
|
||||
CharacterFrame([r' ö ', r' /|\ ', r' / \ ']),
|
||||
CharacterFrame([r' ö ', r' /|\ ', r' | | ']),
|
||||
CharacterFrame([r' ö ', r' /|\ ', r' / \ ']),
|
||||
CharacterFrame([r' Ö ', r' /|\ ', r' / \ ']),
|
||||
],
|
||||
prepare: [
|
||||
CharacterFrame([r' ö ', r' \|\ ', r' / \ ']),
|
||||
CharacterFrame([r' ö ', r' \| ', r' / \ ']),
|
||||
CharacterFrame([r' ö ', r' \|/ ', r' / \ ']),
|
||||
],
|
||||
attack: [
|
||||
CharacterFrame([r' ö ', r' \| ', r' / \ ']),
|
||||
CharacterFrame([r' ö ', r' \|~ ', r' / \ ']),
|
||||
CharacterFrame([r' ö ', r' \|~~ ', r' / \ ']),
|
||||
CharacterFrame([r' ö ', r'\|~~* ', r' / \ ']),
|
||||
CharacterFrame([r' ö ', r' /|\ ', r' / \ ']),
|
||||
],
|
||||
hit: [
|
||||
CharacterFrame([r' ö ', r'/|\* ', r' / \ ']),
|
||||
CharacterFrame([r' ö ', r'/|\ * ', r' / \ ']),
|
||||
CharacterFrame([r' ö ', r' /|\ ', r' / \ ']),
|
||||
],
|
||||
recover: [
|
||||
CharacterFrame([r' ö ', r' /|\ ', r' | ']),
|
||||
CharacterFrame([r' ö ', r' /|\ ', r' / \ ']),
|
||||
CharacterFrame([r' ö ', r' /|\ ', r' / \ ']),
|
||||
],
|
||||
),
|
||||
|
||||
'callback_seraph': RaceFrameData(
|
||||
raceId: 'callback_seraph',
|
||||
idle: [
|
||||
CharacterFrame([r' ô ', r' /|\ ', r' / \ ']),
|
||||
CharacterFrame([r' ô ', r' /|\ ', r' | | ']),
|
||||
CharacterFrame([r' ô ', r' /|\ ', r' / \ ']),
|
||||
CharacterFrame([r' Ô ', r' /|\ ', r' / \ ']),
|
||||
],
|
||||
prepare: [
|
||||
CharacterFrame([r' ô ', r' \|\ ', r' / \ ']),
|
||||
CharacterFrame([r' ô ', r' \| ', r' / \ ']),
|
||||
CharacterFrame([r' ô ', r' \|/ ', r' / \ ']),
|
||||
],
|
||||
attack: [
|
||||
CharacterFrame([r' ô ', r' \| ', r' / \ ']),
|
||||
CharacterFrame([r' ô ', r' \|+ ', r' / \ ']),
|
||||
CharacterFrame([r' ô ', r' \|++ ', r' / \ ']),
|
||||
CharacterFrame([r' ô ', r'\|++* ', r' / \ ']),
|
||||
CharacterFrame([r' ô ', r' /|\ ', r' / \ ']),
|
||||
],
|
||||
hit: [
|
||||
CharacterFrame([r' ô ', r'/|\+ ', r' / \ ']),
|
||||
CharacterFrame([r' ô ', r'/|\ + ', r' / \ ']),
|
||||
CharacterFrame([r' ô ', r' /|\ ', r' / \ ']),
|
||||
],
|
||||
recover: [
|
||||
CharacterFrame([r' ô ', r' /|\ ', r' | ']),
|
||||
CharacterFrame([r' ô ', r' /|\ ', r' / \ ']),
|
||||
CharacterFrame([r' ô ', r' /|\ ', r' / \ ']),
|
||||
],
|
||||
),
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// 소형 (Bit Halfling, Stack Goblin, Cache Imp)
|
||||
// --------------------------------------------------------------------------
|
||||
'bit_halfling': RaceFrameData(
|
||||
raceId: 'bit_halfling',
|
||||
idle: [
|
||||
CharacterFrame([r' o ', r' /|\ ', r' Y ']),
|
||||
CharacterFrame([r' o ', r' /|\ ', r' | ']),
|
||||
CharacterFrame([r' o ', r' /|\ ', r' Y ']),
|
||||
CharacterFrame([r' O ', r' /|\ ', r' Y ']),
|
||||
],
|
||||
prepare: [
|
||||
CharacterFrame([r' o ', r' \|\ ', r' Y ']),
|
||||
CharacterFrame([r' o ', r' \| ', r' Y ']),
|
||||
CharacterFrame([r' o ', r' \|/ ', r' Y ']),
|
||||
],
|
||||
attack: [
|
||||
CharacterFrame([r' o ', r' \| ', r' Y ']),
|
||||
CharacterFrame([r' o ', r' \|- ', r' Y ']),
|
||||
CharacterFrame([r' o ', r' \|-- ', r' Y ']),
|
||||
CharacterFrame([r' o ', r'\|-=> ', r' Y ']),
|
||||
CharacterFrame([r' o ', r' /|\ ', r' Y ']),
|
||||
],
|
||||
hit: [
|
||||
CharacterFrame([r' o ', r'/|\* ', r' Y ']),
|
||||
CharacterFrame([r' o ', r'/|\ * ', r' Y ']),
|
||||
CharacterFrame([r' o ', r' /|\ ', r' Y ']),
|
||||
],
|
||||
recover: [
|
||||
CharacterFrame([r' o ', r' /|\ ', r' | ']),
|
||||
CharacterFrame([r' o ', r' /|\ ', r' Y ']),
|
||||
CharacterFrame([r' o ', r' /|\ ', r' Y ']),
|
||||
],
|
||||
),
|
||||
|
||||
'stack_goblin': RaceFrameData(
|
||||
raceId: 'stack_goblin',
|
||||
idle: [
|
||||
CharacterFrame([r' ◦ ', r' /|\ ', r' Y ']),
|
||||
CharacterFrame([r' ◦ ', r' /|\ ', r' | ']),
|
||||
CharacterFrame([r' ◦ ', r' /|\ ', r' Y ']),
|
||||
CharacterFrame([r' ● ', r' /|\ ', r' Y ']),
|
||||
],
|
||||
prepare: [
|
||||
CharacterFrame([r' ◦ ', r' \|\ ', r' Y ']),
|
||||
CharacterFrame([r' ◦ ', r' \| ', r' Y ']),
|
||||
CharacterFrame([r' ◦ ', r' \|/ ', r' Y ']),
|
||||
],
|
||||
attack: [
|
||||
CharacterFrame([r' ◦ ', r' \| ', r' Y ']),
|
||||
CharacterFrame([r' ◦ ', r' \|- ', r' Y ']),
|
||||
CharacterFrame([r' ◦ ', r' \|-- ', r' Y ']),
|
||||
CharacterFrame([r' ◦ ', r'\|-=> ', r' Y ']),
|
||||
CharacterFrame([r' ◦ ', r' /|\ ', r' Y ']),
|
||||
],
|
||||
hit: [
|
||||
CharacterFrame([r' ◦ ', r'/|\* ', r' Y ']),
|
||||
CharacterFrame([r' ◦ ', r'/|\ * ', r' Y ']),
|
||||
CharacterFrame([r' ◦ ', r' /|\ ', r' Y ']),
|
||||
],
|
||||
recover: [
|
||||
CharacterFrame([r' ◦ ', r' /|\ ', r' | ']),
|
||||
CharacterFrame([r' ◦ ', r' /|\ ', r' Y ']),
|
||||
CharacterFrame([r' ◦ ', r' /|\ ', r' Y ']),
|
||||
],
|
||||
),
|
||||
|
||||
'cache_imp': RaceFrameData(
|
||||
raceId: 'cache_imp',
|
||||
idle: [
|
||||
CharacterFrame([r' ^ ', r' /|\ ', r' Y ']),
|
||||
CharacterFrame([r' ^ ', r' /|\ ', r' | ']),
|
||||
CharacterFrame([r' ^ ', r' /|\ ', r' Y ']),
|
||||
CharacterFrame([r' ^ ', r' /|\ ', r' Y ']),
|
||||
],
|
||||
prepare: [
|
||||
CharacterFrame([r' ^ ', r' \|\ ', r' Y ']),
|
||||
CharacterFrame([r' ^ ', r' \| ', r' Y ']),
|
||||
CharacterFrame([r' ^ ', r' \|/ ', r' Y ']),
|
||||
],
|
||||
attack: [
|
||||
CharacterFrame([r' ^ ', r' \| ', r' Y ']),
|
||||
CharacterFrame([r' ^ ', r' \|- ', r' Y ']),
|
||||
CharacterFrame([r' ^ ', r' \|-- ', r' Y ']),
|
||||
CharacterFrame([r' ^ ', r'\|--> ', r' Y ']),
|
||||
CharacterFrame([r' ^ ', r' /|\ ', r' Y ']),
|
||||
],
|
||||
hit: [
|
||||
CharacterFrame([r' ^ ', r'/|\* ', r' Y ']),
|
||||
CharacterFrame([r' ^ ', r'/|\ * ', r' Y ']),
|
||||
CharacterFrame([r' ^ ', r' /|\ ', r' Y ']),
|
||||
],
|
||||
recover: [
|
||||
CharacterFrame([r' ^ ', r' /|\ ', r' | ']),
|
||||
CharacterFrame([r' ^ ', r' /|\ ', r' Y ']),
|
||||
CharacterFrame([r' ^ ', r' /|\ ', r' Y ']),
|
||||
],
|
||||
),
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// 영체 (Thread Spirit, Pointer Fairy)
|
||||
// --------------------------------------------------------------------------
|
||||
'thread_spirit': RaceFrameData(
|
||||
raceId: 'thread_spirit',
|
||||
idle: [
|
||||
CharacterFrame([r' ·o· ', r' | ', r' ~~~ ']),
|
||||
CharacterFrame([r' ·o· ', r' | ', r' ~~ ']),
|
||||
CharacterFrame([r' ·o· ', r' | ', r' ~~~ ']),
|
||||
CharacterFrame([r' ·O· ', r' | ', r' ~~~ ']),
|
||||
],
|
||||
prepare: [
|
||||
CharacterFrame([r' ·o· ', r' \| ', r' ~~~ ']),
|
||||
CharacterFrame([r' ·o· ', r' \ ', r' ~~~ ']),
|
||||
CharacterFrame([r' ·o· ', r' \/ ', r' ~~~ ']),
|
||||
],
|
||||
attack: [
|
||||
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' ~~~ ']),
|
||||
],
|
||||
hit: [
|
||||
CharacterFrame([r' ·o· ', r' |* ', r' ~~~ ']),
|
||||
CharacterFrame([r' ·o· ', r' | * ', r' ~~~ ']),
|
||||
CharacterFrame([r' ·o· ', r' | ', r' ~~~ ']),
|
||||
],
|
||||
recover: [
|
||||
CharacterFrame([r' ·o· ', r' | ', r' ~~ ']),
|
||||
CharacterFrame([r' ·o· ', r' | ', r' ~~~ ']),
|
||||
CharacterFrame([r' ·o· ', r' | ', r' ~~~ ']),
|
||||
],
|
||||
),
|
||||
|
||||
'pointer_fairy': RaceFrameData(
|
||||
raceId: 'pointer_fairy',
|
||||
idle: [
|
||||
CharacterFrame([r' *o* ', r' | ', r' ~~~ ']),
|
||||
CharacterFrame([r' *o* ', r' | ', r' ~~ ']),
|
||||
CharacterFrame([r' *o* ', r' | ', r' ~~~ ']),
|
||||
CharacterFrame([r' *O* ', r' | ', r' ~~~ ']),
|
||||
],
|
||||
prepare: [
|
||||
CharacterFrame([r' *o* ', r' \| ', r' ~~~ ']),
|
||||
CharacterFrame([r' *o* ', r' \ ', r' ~~~ ']),
|
||||
CharacterFrame([r' *o* ', r' \/ ', r' ~~~ ']),
|
||||
],
|
||||
attack: [
|
||||
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' ~~~ ']),
|
||||
],
|
||||
hit: [
|
||||
CharacterFrame([r' *o* ', r' |+ ', r' ~~~ ']),
|
||||
CharacterFrame([r' *o* ', r' | + ', r' ~~~ ']),
|
||||
CharacterFrame([r' *o* ', r' | ', r' ~~~ ']),
|
||||
],
|
||||
recover: [
|
||||
CharacterFrame([r' *o* ', r' | ', r' ~~ ']),
|
||||
CharacterFrame([r' *o* ', r' | ', r' ~~~ ']),
|
||||
CharacterFrame([r' *o* ', r' | ', r' ~~~ ']),
|
||||
],
|
||||
),
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// 민첩형 (Index Feline, Iterator Shade)
|
||||
// --------------------------------------------------------------------------
|
||||
'index_feline': RaceFrameData(
|
||||
raceId: 'index_feline',
|
||||
idle: [
|
||||
CharacterFrame([r' o ', r' /|\ ', r' λ \ ']),
|
||||
CharacterFrame([r' o ', r' /|\ ', r' | | ']),
|
||||
CharacterFrame([r' o ', r' /|\ ', r' λ \ ']),
|
||||
CharacterFrame([r' O ', r' /|\ ', r' λ \ ']),
|
||||
],
|
||||
prepare: [
|
||||
CharacterFrame([r' o ', r' \|\ ', r' λ \ ']),
|
||||
CharacterFrame([r' o ', r' \| ', r' λ \ ']),
|
||||
CharacterFrame([r' o ', r' \|/ ', r' λ \ ']),
|
||||
],
|
||||
attack: [
|
||||
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' λ \ ']),
|
||||
],
|
||||
hit: [
|
||||
CharacterFrame([r' o ', r'/|\* ', r' λ \ ']),
|
||||
CharacterFrame([r' o ', r'/|\ * ', r' λ \ ']),
|
||||
CharacterFrame([r' o ', r' /|\ ', r' λ \ ']),
|
||||
],
|
||||
recover: [
|
||||
CharacterFrame([r' o ', r' /|\ ', r' | ']),
|
||||
CharacterFrame([r' o ', r' /|\ ', r' λ \ ']),
|
||||
CharacterFrame([r' o ', r' /|\ ', r' λ \ ']),
|
||||
],
|
||||
),
|
||||
|
||||
'iterator_shade': RaceFrameData(
|
||||
raceId: 'iterator_shade',
|
||||
idle: [
|
||||
CharacterFrame([r' o ', r' /|\ ', r' λ \ ']),
|
||||
CharacterFrame([r' o ', r' /|\ ', r' | | ']),
|
||||
CharacterFrame([r' o ', r' /|\ ', r' λ \ ']),
|
||||
CharacterFrame([r' o ', r' /|\ ', r' λ \ ']),
|
||||
],
|
||||
prepare: [
|
||||
CharacterFrame([r' o ', r' \|\ ', r' λ \ ']),
|
||||
CharacterFrame([r' o ', r' \| ', r' λ \ ']),
|
||||
CharacterFrame([r' o ', r' \|/ ', r' λ \ ']),
|
||||
],
|
||||
attack: [
|
||||
CharacterFrame([r' o ', r' \| ', r' λ \ ']),
|
||||
CharacterFrame([r' o ', r' \|- ', r' λ \ ']),
|
||||
CharacterFrame([r' o ', r' \|-- ', r' λ \ ']),
|
||||
CharacterFrame([r' o ', r'\|--x ', r' λ \ ']),
|
||||
CharacterFrame([r' o ', r' /|\ ', r' λ \ ']),
|
||||
],
|
||||
hit: [
|
||||
CharacterFrame([r' o ', r'/|\x ', r' λ \ ']),
|
||||
CharacterFrame([r' o ', r'/|\ x ', r' λ \ ']),
|
||||
CharacterFrame([r' o ', r' /|\ ', r' λ \ ']),
|
||||
],
|
||||
recover: [
|
||||
CharacterFrame([r' o ', r' /|\ ', r' | ']),
|
||||
CharacterFrame([r' o ', r' /|\ ', r' λ \ ']),
|
||||
CharacterFrame([r' o ', r' /|\ ', r' λ \ ']),
|
||||
],
|
||||
),
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// 노움 (Register Gnome)
|
||||
// --------------------------------------------------------------------------
|
||||
'register_gnome': RaceFrameData(
|
||||
raceId: 'register_gnome',
|
||||
idle: [
|
||||
CharacterFrame([r' ô ', r' /|\ ', r' ∧ ']),
|
||||
CharacterFrame([r' ô ', r' /|\ ', r' | ']),
|
||||
CharacterFrame([r' ô ', r' /|\ ', r' ∧ ']),
|
||||
CharacterFrame([r' Ô ', r' /|\ ', r' ∧ ']),
|
||||
],
|
||||
prepare: [
|
||||
CharacterFrame([r' ô ', r' \|\ ', r' ∧ ']),
|
||||
CharacterFrame([r' ô ', r' \| ', r' ∧ ']),
|
||||
CharacterFrame([r' ô ', r' \|/ ', r' ∧ ']),
|
||||
],
|
||||
attack: [
|
||||
CharacterFrame([r' ô ', r' \| ', r' ∧ ']),
|
||||
CharacterFrame([r' ô ', r' \|* ', r' ∧ ']),
|
||||
CharacterFrame([r' ô ', r' \|** ', r' ∧ ']),
|
||||
CharacterFrame([r' ô ', r'\|**> ', r' ∧ ']),
|
||||
CharacterFrame([r' ô ', r' /|\ ', r' ∧ ']),
|
||||
],
|
||||
hit: [
|
||||
CharacterFrame([r' ô ', r'/|\* ', r' ∧ ']),
|
||||
CharacterFrame([r' ô ', r'/|\ * ', r' ∧ ']),
|
||||
CharacterFrame([r' ô ', r' /|\ ', r' ∧ ']),
|
||||
],
|
||||
recover: [
|
||||
CharacterFrame([r' ô ', r' /|\ ', r' | ']),
|
||||
CharacterFrame([r' ô ', r' /|\ ', r' ∧ ']),
|
||||
CharacterFrame([r' ô ', r' /|\ ', r' ∧ ']),
|
||||
],
|
||||
),
|
||||
};
|
||||
@@ -1,104 +0,0 @@
|
||||
/// 무기 카테고리 (공격 스타일 결정용)
|
||||
enum WeaponCategory {
|
||||
/// 둔기류 - 휘두르기/타격
|
||||
/// Keyboard, Mouse, Monitor Stand, Server Rack 등
|
||||
blunt,
|
||||
|
||||
/// 케이블류 - 채찍질
|
||||
/// USB Cable, Ethernet Cord, Fiber Optic 등
|
||||
cable,
|
||||
|
||||
/// 칩류 - 투척/발사
|
||||
/// SSD, RAM Stick, GPU 등
|
||||
projectile,
|
||||
|
||||
/// 프로세서류 - 에너지 빔
|
||||
/// Tensor Core, TPU, Neural Processor 등
|
||||
energy,
|
||||
|
||||
/// 우주급 - 초월적 공격
|
||||
/// Dyson Sphere, Black Hole Computer, Universe Simulator
|
||||
cosmic,
|
||||
|
||||
/// 기본 (무기 없음)
|
||||
unarmed,
|
||||
}
|
||||
|
||||
/// 무기 이름에서 카테고리를 결정
|
||||
///
|
||||
/// 무기 이름의 키워드를 분석하여 공격 스타일 결정.
|
||||
/// 예: "Flaming USB Cable" → cable
|
||||
WeaponCategory getWeaponCategory(String? weaponName) {
|
||||
if (weaponName == null || weaponName.isEmpty) {
|
||||
return WeaponCategory.unarmed;
|
||||
}
|
||||
|
||||
final lower = weaponName.toLowerCase();
|
||||
|
||||
// 우주급 (가장 먼저 체크 - 가장 특별함)
|
||||
if (_matchesAny(lower, _cosmicKeywords)) {
|
||||
return WeaponCategory.cosmic;
|
||||
}
|
||||
|
||||
// 케이블류
|
||||
if (_matchesAny(lower, _cableKeywords)) {
|
||||
return WeaponCategory.cable;
|
||||
}
|
||||
|
||||
// 에너지/프로세서류
|
||||
if (_matchesAny(lower, _energyKeywords)) {
|
||||
return WeaponCategory.energy;
|
||||
}
|
||||
|
||||
// 칩/메모리류
|
||||
if (_matchesAny(lower, _projectileKeywords)) {
|
||||
return WeaponCategory.projectile;
|
||||
}
|
||||
|
||||
// 나머지는 모두 둔기류
|
||||
return WeaponCategory.blunt;
|
||||
}
|
||||
|
||||
bool _matchesAny(String text, List<String> keywords) {
|
||||
return keywords.any((kw) => text.contains(kw));
|
||||
}
|
||||
|
||||
// 카테고리별 키워드 목록
|
||||
|
||||
const _cosmicKeywords = ['dyson', 'black hole', 'universe', 'singularity'];
|
||||
|
||||
const _cableKeywords = [
|
||||
'cable',
|
||||
'cord',
|
||||
'fiber',
|
||||
'optic',
|
||||
'submarine',
|
||||
'satellite',
|
||||
'link',
|
||||
'ethernet',
|
||||
'usb',
|
||||
];
|
||||
|
||||
const _energyKeywords = [
|
||||
'tensor',
|
||||
'tpu',
|
||||
'fpga',
|
||||
'asic',
|
||||
'quantum',
|
||||
'photonic',
|
||||
'neural',
|
||||
'entangler',
|
||||
'processor',
|
||||
'core',
|
||||
];
|
||||
|
||||
const _projectileKeywords = [
|
||||
'ssd',
|
||||
'nvme',
|
||||
'raid',
|
||||
'ram',
|
||||
'gpu',
|
||||
'drive',
|
||||
'stick',
|
||||
'array',
|
||||
];
|
||||
@@ -1,190 +0,0 @@
|
||||
import 'package:asciineverdie/src/core/animation/weapon_category.dart';
|
||||
|
||||
/// 무기 카테고리별 공격 이펙트 ASCII 프레임
|
||||
///
|
||||
/// 각 이펙트는 멀티라인 (최대 5줄, 24자 폭).
|
||||
/// 캐릭터와 몬스터 사이에 표시됨.
|
||||
|
||||
class WeaponEffect {
|
||||
const WeaponEffect({
|
||||
required this.prepareFrames,
|
||||
required this.attackFrames,
|
||||
required this.hitFrames,
|
||||
this.hitSound = '*HIT!*',
|
||||
this.effectHeight = 3,
|
||||
this.effectYStart = 2,
|
||||
});
|
||||
|
||||
/// 준비 프레임 (멀티라인)
|
||||
final List<List<String>> prepareFrames;
|
||||
|
||||
/// 공격 프레임 (멀티라인)
|
||||
final List<List<String>> attackFrames;
|
||||
|
||||
/// 히트 프레임 (멀티라인)
|
||||
final List<List<String>> hitFrames;
|
||||
|
||||
/// 히트 효과음 텍스트
|
||||
final String hitSound;
|
||||
|
||||
/// 이펙트 높이 (줄 수)
|
||||
final int effectHeight;
|
||||
|
||||
/// 이펙트 시작 Y 위치 (0~7)
|
||||
final int effectYStart;
|
||||
}
|
||||
|
||||
/// 카테고리별 무기 이펙트 반환
|
||||
WeaponEffect getWeaponEffect(WeaponCategory category) {
|
||||
return switch (category) {
|
||||
WeaponCategory.blunt => _bluntEffect,
|
||||
WeaponCategory.cable => _cableEffect,
|
||||
WeaponCategory.projectile => _projectileEffect,
|
||||
WeaponCategory.energy => _energyEffect,
|
||||
WeaponCategory.cosmic => _cosmicEffect,
|
||||
WeaponCategory.unarmed => _unarmedEffect,
|
||||
};
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 둔기류 - 휘두르기 (5줄)
|
||||
// ============================================================================
|
||||
const _bluntEffect = WeaponEffect(
|
||||
prepareFrames: [
|
||||
[r' ', r' _ ', r' / ', r' / ', r' / '],
|
||||
[r' ', r' _/ ', r' / ', r' / ', r' / '],
|
||||
],
|
||||
attackFrames: [
|
||||
[r' ', r' _/ ', r' / ', r' / ', r' / '],
|
||||
[r' ', r' _/ ', r' / ', r' /________', r'/ '],
|
||||
[r' ', r' _/ ', r' /________', r'/ ', r' '],
|
||||
[r' ', r' _/____=> ', r'/ ', r' ', r' '],
|
||||
],
|
||||
hitFrames: [
|
||||
[r' *BASH* ', r' _/____=> ', r'/ ** ', r' ** ', r' '],
|
||||
[r' *SMASH!* ', r' _/____ ', r' / ** ', r' ** ', r' '],
|
||||
],
|
||||
hitSound: '*BASH*',
|
||||
effectHeight: 5,
|
||||
effectYStart: 1,
|
||||
);
|
||||
|
||||
// ============================================================================
|
||||
// 케이블류 - 채찍질 (5줄)
|
||||
// ============================================================================
|
||||
const _cableEffect = WeaponEffect(
|
||||
prepareFrames: [
|
||||
[r' ', r'~ ', r' ~ ', r' ~ ', r' ~ '],
|
||||
[r' ', r'~~ ', r' ~~ ', r' ~ ', r' ~ '],
|
||||
],
|
||||
attackFrames: [
|
||||
[r' ', r'~~~ ', r' ~~~ ', r' ~~ ', r' ~ '],
|
||||
[r' ', r'~~~~ ', r' ~~~~ ', r' ~~', r' '],
|
||||
[r' ', r'~~~~~> ', r' ~~~~~', r' ', r' '],
|
||||
[r' ', r'~~~~~~> ', r' ~~~~', r' ', r' '],
|
||||
],
|
||||
hitFrames: [
|
||||
[r' *WHIP* ', r'~~~~~~> ', r' ~~~~', r' **', r' '],
|
||||
[r' *CRACK!* ', r'~~~~~> ', r' ~~~~~', r' **', r' '],
|
||||
],
|
||||
hitSound: '*WHIP*',
|
||||
effectHeight: 5,
|
||||
effectYStart: 1,
|
||||
);
|
||||
|
||||
// ============================================================================
|
||||
// 투척류 - 발사 (5줄)
|
||||
// ============================================================================
|
||||
const _projectileEffect = WeaponEffect(
|
||||
prepareFrames: [
|
||||
[r' ', r' ', r' [=] ', r' ', r' '],
|
||||
[r' ', r' _ ', r' [==] ', r' - ', r' '],
|
||||
],
|
||||
attackFrames: [
|
||||
[r' ', r' . ', r' [> ', r" ' ", r' '],
|
||||
[r' ', r' . ', r' [> ', r" ' ", r' '],
|
||||
[r' ', r' . ', r' [>', r" ' ", r' '],
|
||||
[
|
||||
r' ',
|
||||
r' ',
|
||||
r' [>',
|
||||
r' ',
|
||||
r' ',
|
||||
],
|
||||
],
|
||||
hitFrames: [
|
||||
[r' *CLANG!* ', r' ***', r' [>', r' ***', r' '],
|
||||
[r' *CRASH!* ', r' *** ', r' [> ', r' *** ', r' '],
|
||||
],
|
||||
hitSound: '*CLANG*',
|
||||
effectHeight: 5,
|
||||
effectYStart: 1,
|
||||
);
|
||||
|
||||
// ============================================================================
|
||||
// 에너지류 - 빔 발사 (5줄)
|
||||
// ============================================================================
|
||||
const _energyEffect = WeaponEffect(
|
||||
prepareFrames: [
|
||||
[r' ', r' == ', r' <**> ', r' == ', r' '],
|
||||
[r' ==== ', r' ====== ', r' <****> ', r' ====== ', r' ==== '],
|
||||
],
|
||||
attackFrames: [
|
||||
[r' ====== ', r' ======== ', r'===<**>===', r' ======== ', r' ====== '],
|
||||
[r' ======== ', r'==========', r'===<**>===', r'==========', r' ======== '],
|
||||
[r'==========', r'==========', r'====<**>==', r'==========', r'=========='],
|
||||
[r'==========', r'==========', r'=====<**>=', r'==========', r'=========='],
|
||||
],
|
||||
hitFrames: [
|
||||
[r'===*ZAP*==', r'==========', r'====<**>==', r'==========', r'===*ZAP*=='],
|
||||
[r'==*BZZT!*=', r'==========', r'=====<**>=', r'==========', r'==*BZZT!*='],
|
||||
],
|
||||
hitSound: '*ZAP*',
|
||||
effectHeight: 5,
|
||||
effectYStart: 1,
|
||||
);
|
||||
|
||||
// ============================================================================
|
||||
// 우주급 - 초월적 공격 (5줄)
|
||||
// ============================================================================
|
||||
const _cosmicEffect = WeaponEffect(
|
||||
prepareFrames: [
|
||||
[r' * ', r' @ @ ', r' @ @ ', r' @ @ ', r' * '],
|
||||
[r' * * ', r' @ @ ', r' @ @', r' @ @ ', r' * * '],
|
||||
],
|
||||
attackFrames: [
|
||||
[r' * ', r' * * * ', r' * * * *', r' * * * ', r' * '],
|
||||
[r' *** ', r' * * * *', r'* * * * ', r' * * * *', r' *** '],
|
||||
[r' ***** ', r' ******* ', r'*********', r' ******* ', r' ***** '],
|
||||
[r' **VOID**', r'*********', r'*********', r'*********', r' **VOID**'],
|
||||
],
|
||||
hitFrames: [
|
||||
[r'*SINGULAR', r'*********', r'***!!!***', r'*********', r'*DESTROY*'],
|
||||
[r'!!!VOID!!', r'*********', r'*********', r'*********', r'!!!VOID!!'],
|
||||
],
|
||||
hitSound: '***VOID***',
|
||||
effectHeight: 5,
|
||||
effectYStart: 1,
|
||||
);
|
||||
|
||||
// ============================================================================
|
||||
// 맨손 - 기본 펀치 (5줄)
|
||||
// ============================================================================
|
||||
const _unarmedEffect = WeaponEffect(
|
||||
prepareFrames: [
|
||||
[r' ', r' ', r' > ', r' ', r' '],
|
||||
[r' ', r' _ ', r' -> ', r' - ', r' '],
|
||||
],
|
||||
attackFrames: [
|
||||
[r' ', r' __ ', r' ---> ', r' -- ', r' '],
|
||||
[r' ', r' ___ ', r' -----> ', r' --- ', r' '],
|
||||
[r' ', r' ____', r' ======>', r' ----', r' '],
|
||||
],
|
||||
hitFrames: [
|
||||
[r' *POW!* ', r' ****', r' ======>', r' ****', r' '],
|
||||
[r' *PUNCH!* ', r' **** ', r' =====> ', r' **** ', r' '],
|
||||
],
|
||||
hitSound: '*POW*',
|
||||
effectHeight: 5,
|
||||
effectYStart: 1,
|
||||
);
|
||||
@@ -1,171 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:asciineverdie/src/shared/retro_colors.dart';
|
||||
|
||||
/// ASCII 애니메이션 4색 팔레트 (Phase 7)
|
||||
///
|
||||
/// 시각적 명확성을 위해 4가지 색상만 사용한다.
|
||||
/// - 흰색/검정: 오브젝트 (캐릭터, 몬스터, 아이템) - 테마에 따라 변환
|
||||
/// - 시안/파랑: 포지티브 이펙트 (힐, 버프, 레벨업, 획득) - 테마에 따라 변환
|
||||
/// - 마젠타/빨강: 네거티브 이펙트 (데미지, 디버프, 사망, 손실) - 테마에 따라 변환
|
||||
/// - 검정/밝은색: 배경 - 테마에 따라 변환
|
||||
class AsciiColors {
|
||||
AsciiColors._();
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
// 동적 색상 Getter (테마에 따라 자동 전환)
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
|
||||
/// 오브젝트 색상 (캐릭터, 몬스터, 아이템) - 테마 인식
|
||||
static Color objectOf(BuildContext context) =>
|
||||
RetroColors.isDarkMode(context) ? object : _lightObject;
|
||||
|
||||
/// 포지티브 이펙트 색상 - 테마 인식
|
||||
static Color positiveOf(BuildContext context) =>
|
||||
RetroColors.isDarkMode(context) ? positive : _lightPositive;
|
||||
|
||||
/// 네거티브 이펙트 색상 - 테마 인식
|
||||
static Color negativeOf(BuildContext context) =>
|
||||
RetroColors.isDarkMode(context) ? negative : _lightNegative;
|
||||
|
||||
/// 배경 색상 - 테마 인식
|
||||
static Color backgroundOf(BuildContext context) =>
|
||||
RetroColors.isDarkMode(context) ? background : _lightBackground;
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
// 무기 등급(ItemRarity) 색상 Getter (테마 인식, Phase 9)
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
|
||||
/// Uncommon 등급 색상 - 테마 인식
|
||||
static Color rarityUncommonOf(BuildContext context) =>
|
||||
RetroColors.isDarkMode(context) ? rarityUncommon : _lightRarityUncommon;
|
||||
|
||||
/// Rare 등급 색상 - 테마 인식
|
||||
static Color rarityRareOf(BuildContext context) =>
|
||||
RetroColors.isDarkMode(context) ? rarityRare : _lightRarityRare;
|
||||
|
||||
/// Epic 등급 색상 - 테마 인식
|
||||
static Color rarityEpicOf(BuildContext context) =>
|
||||
RetroColors.isDarkMode(context) ? rarityEpic : _lightRarityEpic;
|
||||
|
||||
/// Legendary 등급 색상 - 테마 인식
|
||||
static Color rarityLegendaryOf(BuildContext context) =>
|
||||
RetroColors.isDarkMode(context) ? rarityLegendary : _lightRarityLegendary;
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
// 라이트 모드 색상 (양피지/크림 기반)
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
|
||||
/// 라이트 모드 오브젝트 색상 (어두운 갈색)
|
||||
static const Color _lightObject = Color(0xFF2D1B0E);
|
||||
|
||||
/// 라이트 모드 포지티브 이펙트 (진한 청록)
|
||||
static const Color _lightPositive = Color(0xFF006666);
|
||||
|
||||
/// 라이트 모드 네거티브 이펙트 (진한 자주)
|
||||
static const Color _lightNegative = Color(0xFFAA0066);
|
||||
|
||||
/// 라이트 모드 배경 (양피지 크림)
|
||||
static const Color _lightBackground = Color(0xFFF5E6C8);
|
||||
|
||||
/// 라이트 모드 Uncommon 등급 (진한 초록)
|
||||
static const Color _lightRarityUncommon = Color(0xFF008800);
|
||||
|
||||
/// 라이트 모드 Rare 등급 (진한 파랑)
|
||||
static const Color _lightRarityRare = Color(0xFF0055AA);
|
||||
|
||||
/// 라이트 모드 Epic 등급 (진한 보라)
|
||||
static const Color _lightRarityEpic = Color(0xFF660099);
|
||||
|
||||
/// 라이트 모드 Legendary 등급 (진한 금색)
|
||||
static const Color _lightRarityLegendary = Color(0xFFCC7700);
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
// 레거시 정적 색상 (다크 모드 기본값 / context 없는 곳에서 사용)
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
|
||||
/// 오브젝트 색상 (캐릭터, 몬스터, 아이템)
|
||||
static const Color object = Colors.white;
|
||||
|
||||
/// 포지티브 이펙트 색상 (힐, 버프, 레벨업, 획득)
|
||||
static const Color positive = Colors.cyan;
|
||||
|
||||
/// 네거티브 이펙트 색상 (데미지, 디버프, 사망, 손실)
|
||||
static const Color negative = Color(0xFFFF00FF); // 마젠타
|
||||
|
||||
/// 배경 색상
|
||||
static const Color background = Colors.black;
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
// 무기 등급(ItemRarity) 정적 색상 (다크 모드 기본값, Phase 9)
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
|
||||
/// Uncommon 등급 (밝은 초록)
|
||||
static const Color rarityUncommon = Color(0xFF00FF00);
|
||||
|
||||
/// Rare 등급 (밝은 파랑)
|
||||
static const Color rarityRare = Color(0xFF0088FF);
|
||||
|
||||
/// Epic 등급 (밝은 보라)
|
||||
static const Color rarityEpic = Color(0xFF9900FF);
|
||||
|
||||
/// Legendary 등급 (밝은 금색)
|
||||
static const Color rarityLegendary = Color(0xFFFFAA00);
|
||||
|
||||
/// 상황에 따른 색상 반환
|
||||
static Color forContext(AsciiColorContext context) {
|
||||
return switch (context) {
|
||||
AsciiColorContext.idle => object,
|
||||
AsciiColorContext.attack => object,
|
||||
AsciiColorContext.critical => negative,
|
||||
AsciiColorContext.heal => positive,
|
||||
AsciiColorContext.buff => positive,
|
||||
AsciiColorContext.debuff => negative,
|
||||
AsciiColorContext.levelUp => positive,
|
||||
AsciiColorContext.death => negative,
|
||||
AsciiColorContext.itemGain => positive,
|
||||
AsciiColorContext.itemLoss => negative,
|
||||
AsciiColorContext.dodge => object,
|
||||
AsciiColorContext.block => object,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// ASCII 애니메이션 색상 컨텍스트
|
||||
enum AsciiColorContext {
|
||||
/// 대기 상태
|
||||
idle,
|
||||
|
||||
/// 일반 공격
|
||||
attack,
|
||||
|
||||
/// 크리티컬 히트
|
||||
critical,
|
||||
|
||||
/// 회복
|
||||
heal,
|
||||
|
||||
/// 버프 획득
|
||||
buff,
|
||||
|
||||
/// 디버프 적용
|
||||
debuff,
|
||||
|
||||
/// 레벨업
|
||||
levelUp,
|
||||
|
||||
/// 사망
|
||||
death,
|
||||
|
||||
/// 아이템 획득
|
||||
itemGain,
|
||||
|
||||
/// 아이템 손실
|
||||
itemLoss,
|
||||
|
||||
/// 회피 성공
|
||||
dodge,
|
||||
|
||||
/// 방패 방어
|
||||
block,
|
||||
}
|
||||
@@ -1,545 +0,0 @@
|
||||
import 'package:asciineverdie/data/game_text_l10n.dart' as l10n;
|
||||
import 'package:asciineverdie/data/game_translations_ja.dart';
|
||||
import 'package:asciineverdie/data/game_translations_ko.dart';
|
||||
import 'package:asciineverdie/src/core/util/pq_logic.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
/// 게임 데이터 번역을 위한 헬퍼 클래스
|
||||
/// 현재 로케일에 따라 게임 데이터의 번역을 제공합니다.
|
||||
///
|
||||
/// 글로벌 로케일 시스템(game_text_l10n.dart)을 사용하여 일관성 보장
|
||||
class GameDataL10n {
|
||||
GameDataL10n._();
|
||||
|
||||
/// 현재 로케일이 한국어인지 확인 (글로벌 로케일 사용)
|
||||
static bool _isKorean(BuildContext context) {
|
||||
// 글로벌 로케일 우선, 폴백으로 context 로케일 사용
|
||||
if (l10n.isKoreanLocale) return true;
|
||||
final locale = Localizations.localeOf(context);
|
||||
return locale.languageCode == 'ko';
|
||||
}
|
||||
|
||||
/// 현재 로케일이 일본어인지 확인 (글로벌 로케일 사용)
|
||||
static bool _isJapanese(BuildContext context) {
|
||||
if (l10n.isJapaneseLocale) return true;
|
||||
final locale = Localizations.localeOf(context);
|
||||
return locale.languageCode == 'ja';
|
||||
}
|
||||
|
||||
/// 종족 이름 번역
|
||||
static String getRaceName(BuildContext context, String englishName) {
|
||||
if (_isKorean(context)) {
|
||||
return raceTranslationsKo[englishName] ?? englishName;
|
||||
}
|
||||
return englishName;
|
||||
}
|
||||
|
||||
/// 직업 이름 번역
|
||||
static String getKlassName(BuildContext context, String englishName) {
|
||||
if (_isKorean(context)) {
|
||||
return klassTranslationsKo[englishName] ?? englishName;
|
||||
}
|
||||
return englishName;
|
||||
}
|
||||
|
||||
/// 주문 이름 번역
|
||||
static String getSpellName(BuildContext context, String englishName) {
|
||||
if (_isKorean(context)) {
|
||||
return spellTranslationsKo[englishName] ?? englishName;
|
||||
}
|
||||
return englishName;
|
||||
}
|
||||
|
||||
/// 몬스터 이름 번역
|
||||
static String getMonsterName(BuildContext context, String englishName) {
|
||||
if (_isKorean(context)) {
|
||||
return monsterTranslationsKo[englishName] ?? englishName;
|
||||
}
|
||||
return englishName;
|
||||
}
|
||||
|
||||
/// 무기 이름 번역
|
||||
static String getWeaponName(BuildContext context, String englishName) {
|
||||
if (_isKorean(context)) {
|
||||
return weaponTranslationsKo[englishName] ?? englishName;
|
||||
}
|
||||
return englishName;
|
||||
}
|
||||
|
||||
/// 갑옷 이름 번역
|
||||
static String getArmorName(BuildContext context, String englishName) {
|
||||
if (_isKorean(context)) {
|
||||
return armorTranslationsKo[englishName] ?? englishName;
|
||||
}
|
||||
return englishName;
|
||||
}
|
||||
|
||||
/// 방패 이름 번역
|
||||
static String getShieldName(BuildContext context, String englishName) {
|
||||
if (_isKorean(context)) {
|
||||
return shieldTranslationsKo[englishName] ?? englishName;
|
||||
}
|
||||
return englishName;
|
||||
}
|
||||
|
||||
/// 칭호 번역
|
||||
static String getTitleName(BuildContext context, String englishName) {
|
||||
if (_isKorean(context)) {
|
||||
return titleTranslationsKo[englishName] ?? englishName;
|
||||
}
|
||||
return englishName;
|
||||
}
|
||||
|
||||
/// 인상적인 칭호 번역
|
||||
static String getImpressiveTitleName(
|
||||
BuildContext context,
|
||||
String englishName,
|
||||
) {
|
||||
if (_isKorean(context)) {
|
||||
return impressiveTitleTranslationsKo[englishName] ?? englishName;
|
||||
}
|
||||
return englishName;
|
||||
}
|
||||
|
||||
/// 아이템 속성 번역
|
||||
static String getItemAttribName(BuildContext context, String englishName) {
|
||||
if (_isKorean(context)) {
|
||||
return itemAttribTranslationsKo[englishName] ?? englishName;
|
||||
}
|
||||
return englishName;
|
||||
}
|
||||
|
||||
/// 아이템 접미사 번역
|
||||
static String getItemOfName(BuildContext context, String englishName) {
|
||||
if (_isKorean(context)) {
|
||||
return itemOfsTranslationsKo[englishName] ?? englishName;
|
||||
}
|
||||
return englishName;
|
||||
}
|
||||
|
||||
/// 공격 속성 번역
|
||||
static String getOffenseAttribName(BuildContext context, String englishName) {
|
||||
if (_isKorean(context)) {
|
||||
return offenseAttribTranslationsKo[englishName] ?? englishName;
|
||||
}
|
||||
return englishName;
|
||||
}
|
||||
|
||||
/// 나쁜 공격 속성 번역
|
||||
static String getOffenseBadName(BuildContext context, String englishName) {
|
||||
if (_isKorean(context)) {
|
||||
return offenseBadTranslationsKo[englishName] ?? englishName;
|
||||
}
|
||||
return englishName;
|
||||
}
|
||||
|
||||
/// 방어 속성 번역
|
||||
static String getDefenseAttribName(BuildContext context, String englishName) {
|
||||
if (_isKorean(context)) {
|
||||
return defenseAttribTranslationsKo[englishName] ?? englishName;
|
||||
}
|
||||
return englishName;
|
||||
}
|
||||
|
||||
/// 나쁜 방어 속성 번역
|
||||
static String getDefenseBadName(BuildContext context, String englishName) {
|
||||
if (_isKorean(context)) {
|
||||
return defenseBadTranslationsKo[englishName] ?? englishName;
|
||||
}
|
||||
return englishName;
|
||||
}
|
||||
|
||||
/// 특수 아이템 번역
|
||||
static String getSpecialName(BuildContext context, String englishName) {
|
||||
if (_isKorean(context)) {
|
||||
return specialTranslationsKo[englishName] ?? englishName;
|
||||
}
|
||||
return englishName;
|
||||
}
|
||||
|
||||
/// 일반 게임 텍스트 번역 (context 없이 로케일 직접 지정)
|
||||
static String getRaceNameByLocale(String englishName, String languageCode) {
|
||||
if (languageCode == 'ko') {
|
||||
return raceTranslationsKo[englishName] ?? englishName;
|
||||
}
|
||||
return englishName;
|
||||
}
|
||||
|
||||
/// 직업 이름 번역 (context 없이)
|
||||
static String getKlassNameByLocale(String englishName, String languageCode) {
|
||||
if (languageCode == 'ko') {
|
||||
return klassTranslationsKo[englishName] ?? englishName;
|
||||
}
|
||||
return englishName;
|
||||
}
|
||||
|
||||
/// 몬스터 이름 번역 (context 없이)
|
||||
static String getMonsterNameByLocale(
|
||||
String englishName,
|
||||
String languageCode,
|
||||
) {
|
||||
if (languageCode == 'ko') {
|
||||
return monsterTranslationsKo[englishName] ?? englishName;
|
||||
}
|
||||
return englishName;
|
||||
}
|
||||
|
||||
/// 구조화된 장비 결과를 로컬라이즈된 문자열로 렌더링
|
||||
/// 예: EquipResult(baseName: "Keyboard", modifiers: ["Optimized"], plusValue: 2)
|
||||
/// → 영어: "+2 Optimized Keyboard"
|
||||
/// → 한국어: "+2 최적화된 키보드"
|
||||
static String renderEquipResult(
|
||||
BuildContext context,
|
||||
EquipResult result,
|
||||
int slotIndex,
|
||||
) {
|
||||
if (_isKorean(context)) {
|
||||
return _renderEquipResultKo(result, slotIndex);
|
||||
}
|
||||
return result.displayName;
|
||||
}
|
||||
|
||||
/// 한국어 장비 렌더링 (내부 함수)
|
||||
static String _renderEquipResultKo(EquipResult result, int slotIndex) {
|
||||
// 기본 장비 이름 번역
|
||||
String baseName;
|
||||
if (slotIndex == 0) {
|
||||
baseName = weaponTranslationsKo[result.baseName] ?? result.baseName;
|
||||
} else if (slotIndex == 1) {
|
||||
baseName = shieldTranslationsKo[result.baseName] ?? result.baseName;
|
||||
} else {
|
||||
baseName = armorTranslationsKo[result.baseName] ?? result.baseName;
|
||||
}
|
||||
|
||||
// 수식어 번역 (공격용 vs 방어용)
|
||||
final isWeapon = slotIndex == 0;
|
||||
final translatedModifiers = result.modifiers.map((mod) {
|
||||
if (isWeapon) {
|
||||
// 공격 속성: offenseAttrib 또는 offenseBad
|
||||
return offenseAttribTranslationsKo[mod] ??
|
||||
offenseBadTranslationsKo[mod] ??
|
||||
mod;
|
||||
} else {
|
||||
// 방어 속성: defenseAttrib 또는 defenseBad
|
||||
return defenseAttribTranslationsKo[mod] ??
|
||||
defenseBadTranslationsKo[mod] ??
|
||||
mod;
|
||||
}
|
||||
}).toList();
|
||||
|
||||
// 조합: 수식어들 + 기본 이름
|
||||
var name = baseName;
|
||||
for (final mod in translatedModifiers) {
|
||||
name = '$mod $name';
|
||||
}
|
||||
|
||||
// +/- 수치 추가
|
||||
if (result.plusValue != 0) {
|
||||
final sign = result.plusValue > 0 ? '+' : '';
|
||||
name = '$sign${result.plusValue} $name';
|
||||
}
|
||||
|
||||
return name;
|
||||
}
|
||||
|
||||
/// 구조화된 아이템 결과를 로컬라이즈된 문자열로 렌더링
|
||||
/// 예: ItemResult(attrib: "Golden", special: "Iterator", itemOf: "Compilation")
|
||||
/// → 영어: "Golden Iterator of Compilation"
|
||||
/// → 한국어: "컴파일의 황금 이터레이터"
|
||||
static String renderItemResult(BuildContext context, ItemResult result) {
|
||||
if (_isKorean(context)) {
|
||||
return _renderItemResultKo(result);
|
||||
}
|
||||
return result.displayName;
|
||||
}
|
||||
|
||||
/// 한국어 아이템 렌더링 (내부 함수)
|
||||
static String _renderItemResultKo(ItemResult result) {
|
||||
// 단순 아이템 (boringItem)
|
||||
if (result.boringItem != null) {
|
||||
return boringItemTranslationsKo[result.boringItem] ?? result.boringItem!;
|
||||
}
|
||||
|
||||
// 복합 아이템: attrib + special + itemOf
|
||||
final attrib = result.attrib != null
|
||||
? (itemAttribTranslationsKo[result.attrib] ?? result.attrib!)
|
||||
: null;
|
||||
final special = result.special != null
|
||||
? (specialTranslationsKo[result.special] ?? result.special!)
|
||||
: null;
|
||||
final itemOf = result.itemOf != null
|
||||
? (itemOfsTranslationsKo[result.itemOf] ?? result.itemOf!)
|
||||
: null;
|
||||
|
||||
// 한국어 어순: "X의 Y" 패턴
|
||||
// "Golden Iterator of Compilation" → "컴파일의 황금 이터레이터"
|
||||
if (attrib != null && special != null && itemOf != null) {
|
||||
return '$itemOf의 $attrib $special';
|
||||
}
|
||||
|
||||
// attrib + special만 있는 경우
|
||||
if (attrib != null && special != null) {
|
||||
return '$attrib $special';
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
/// 장비 이름 문자열 파싱 후 번역 (기존 저장 데이터 호환)
|
||||
/// 예: "+2 Optimized GPU-Powered Keyboard" → "+2 최적화된 GPU 파워 키보드"
|
||||
static String translateEquipString(
|
||||
BuildContext context,
|
||||
String equipString,
|
||||
int slotIndex,
|
||||
) {
|
||||
final isKo = _isKorean(context);
|
||||
final isJa = _isJapanese(context);
|
||||
if ((!isKo && !isJa) || equipString.isEmpty) return equipString;
|
||||
|
||||
// 1. +/- 값 추출
|
||||
final plusMatch = RegExp(r'^([+-]?\d+)\s+').firstMatch(equipString);
|
||||
String remaining = equipString;
|
||||
String plusPart = '';
|
||||
if (plusMatch != null) {
|
||||
plusPart = plusMatch.group(1)!;
|
||||
remaining = equipString.substring(plusMatch.end);
|
||||
}
|
||||
|
||||
// 2. 기본 장비 이름 찾기 (가장 긴 매칭 우선)
|
||||
// 통합 맵 사용 (추가 번역 포함)
|
||||
final Map<String, String> baseMap;
|
||||
if (isKo) {
|
||||
if (slotIndex == 0) {
|
||||
baseMap = weaponTranslationsKo;
|
||||
} else if (slotIndex == 1) {
|
||||
baseMap = allShieldTranslationsKo;
|
||||
} else {
|
||||
baseMap = allArmorTranslationsKo;
|
||||
}
|
||||
} else {
|
||||
if (slotIndex == 0) {
|
||||
baseMap = weaponTranslationsJa;
|
||||
} else if (slotIndex == 1) {
|
||||
baseMap = allShieldTranslationsJa;
|
||||
} else {
|
||||
baseMap = allArmorTranslationsJa;
|
||||
}
|
||||
}
|
||||
|
||||
String baseTranslated = remaining;
|
||||
String modifierPart = '';
|
||||
|
||||
// 기본 장비 이름을 뒤에서부터 찾기
|
||||
for (final entry in baseMap.entries) {
|
||||
if (remaining.endsWith(entry.key)) {
|
||||
baseTranslated = entry.value;
|
||||
modifierPart = remaining
|
||||
.substring(0, remaining.length - entry.key.length)
|
||||
.trim();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 수식어 번역
|
||||
final isWeapon = slotIndex == 0;
|
||||
final modWords = modifierPart
|
||||
.split(' ')
|
||||
.where((s) => s.isNotEmpty)
|
||||
.toList();
|
||||
final translatedMods = modWords.map((mod) {
|
||||
if (isKo) {
|
||||
if (isWeapon) {
|
||||
return offenseAttribTranslationsKo[mod] ??
|
||||
offenseBadTranslationsKo[mod] ??
|
||||
mod;
|
||||
} else {
|
||||
return defenseAttribTranslationsKo[mod] ??
|
||||
defenseBadTranslationsKo[mod] ??
|
||||
mod;
|
||||
}
|
||||
} else {
|
||||
if (isWeapon) {
|
||||
return offenseAttribTranslationsJa[mod] ??
|
||||
offenseBadTranslationsJa[mod] ??
|
||||
mod;
|
||||
} else {
|
||||
return defenseAttribTranslationsJa[mod] ??
|
||||
defenseBadTranslationsJa[mod] ??
|
||||
mod;
|
||||
}
|
||||
}
|
||||
}).toList();
|
||||
|
||||
// 4. 조합
|
||||
var result = baseTranslated;
|
||||
for (final mod in translatedMods.reversed) {
|
||||
result = '$mod $result';
|
||||
}
|
||||
if (plusPart.isNotEmpty) {
|
||||
result = '$plusPart $result';
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// 아이템 이름 문자열 파싱 후 번역 (기존 저장 데이터 호환)
|
||||
/// 예: "Golden Iterator of Compilation" → "컴파일의 황금 이터레이터"
|
||||
/// 예: "index out of bounds Array fragment" → "인덱스 초과의 배열 조각"
|
||||
static String translateItemString(BuildContext context, String itemString) {
|
||||
final isKo = _isKorean(context);
|
||||
final isJa = _isJapanese(context);
|
||||
if ((!isKo && !isJa) || itemString.isEmpty) return itemString;
|
||||
|
||||
// 1. specialItem 형식 체크: "Attrib Special of ItemOf"
|
||||
// itemOfs에 있는 값으로 끝나는지 확인
|
||||
final specialItemResult = _tryTranslateSpecialItem(itemString, isKo);
|
||||
if (specialItemResult != null) return specialItemResult;
|
||||
|
||||
// 2. 몬스터 드롭 형식 체크: "{monster_lowercase} {drop_ProperCase}"
|
||||
final monsterDropResult = _tryTranslateMonsterDrop(itemString, isKo);
|
||||
if (monsterDropResult != null) return monsterDropResult;
|
||||
|
||||
// 3. interestingItem 형식: "Attrib Special" (2단어)
|
||||
final words = itemString.split(' ');
|
||||
if (words.length == 2) {
|
||||
final attrib = words[0];
|
||||
final special = words[1];
|
||||
|
||||
if (isKo) {
|
||||
final attribKo = itemAttribTranslationsKo[attrib] ?? attrib;
|
||||
final specialKo = specialTranslationsKo[special] ?? special;
|
||||
return '$attribKo $specialKo';
|
||||
} else {
|
||||
final attribJa = itemAttribTranslationsJa[attrib] ?? attrib;
|
||||
final specialJa = specialTranslationsJa[special] ?? special;
|
||||
return '$attribJa $specialJa';
|
||||
}
|
||||
}
|
||||
|
||||
// 4. 단일 단어 (boringItem 등) - 잡템 번역 시도
|
||||
if (isKo) {
|
||||
return boringItemTranslationsKo[itemString] ??
|
||||
allDropTranslationsKo[itemString.toLowerCase()] ??
|
||||
itemString;
|
||||
} else {
|
||||
return boringItemTranslationsJa[itemString] ??
|
||||
allDropTranslationsJa[itemString.toLowerCase()] ??
|
||||
itemString;
|
||||
}
|
||||
}
|
||||
|
||||
/// specialItem 형식 번역 시도
|
||||
/// "Attrib Special of ItemOf" → "ItemOf의 Attrib Special"
|
||||
static String? _tryTranslateSpecialItem(String itemString, bool isKo) {
|
||||
// "of" 뒤의 부분이 itemOfs에 있는지 확인
|
||||
final ofMatch = RegExp(r'^(.+)\s+of\s+(.+)$').firstMatch(itemString);
|
||||
if (ofMatch == null) return null;
|
||||
|
||||
final beforeOf = ofMatch.group(1)!;
|
||||
final afterOf = ofMatch.group(2)!;
|
||||
|
||||
// afterOf가 itemOfs에 있어야 specialItem 형식
|
||||
final itemOfsMap = isKo ? itemOfsTranslationsKo : itemOfsTranslationsJa;
|
||||
if (!itemOfsMap.containsKey(afterOf)) return null;
|
||||
|
||||
// beforeOf를 Attrib + Special로 분리
|
||||
final words = beforeOf.split(' ');
|
||||
if (words.length < 2) return null;
|
||||
|
||||
final attrib = words.sublist(0, words.length - 1).join(' ');
|
||||
final special = words.last;
|
||||
|
||||
// Attrib와 Special이 유효한지 확인
|
||||
final attribMap = isKo
|
||||
? itemAttribTranslationsKo
|
||||
: itemAttribTranslationsJa;
|
||||
final specialMap = isKo ? specialTranslationsKo : specialTranslationsJa;
|
||||
if (!attribMap.containsKey(attrib) && !specialMap.containsKey(special)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final attribT = attribMap[attrib] ?? attrib;
|
||||
final specialT = specialMap[special] ?? special;
|
||||
final itemOfT = itemOfsMap[afterOf] ?? afterOf;
|
||||
|
||||
if (isKo) {
|
||||
return '$itemOfT의 $attribT $specialT';
|
||||
} else {
|
||||
return '$itemOfTの$attribT $specialT';
|
||||
}
|
||||
}
|
||||
|
||||
/// 몬스터 드롭 형식 번역 시도
|
||||
/// "{monster_lowercase} {drop_ProperCase}" → "{몬스터}의 {드롭아이템}"
|
||||
static String? _tryTranslateMonsterDrop(String itemString, bool isKo) {
|
||||
// 드롭 아이템 번역 맵 선택 (통합 맵 사용)
|
||||
final dropMap = isKo ? allDropTranslationsKo : allDropTranslationsJa;
|
||||
final monsterMap = isKo
|
||||
? allMonsterTranslationsKo
|
||||
: allMonsterTranslationsJa;
|
||||
|
||||
// (대소문자 무시, 아이템 문자열 끝에서 매칭)
|
||||
for (final entry in dropMap.entries) {
|
||||
final dropItem = entry.key;
|
||||
final dropItemProperCase = _properCase(dropItem);
|
||||
|
||||
// 아이템 문자열이 드롭 아이템으로 끝나는지 확인
|
||||
if (itemString.endsWith(dropItemProperCase) ||
|
||||
itemString.endsWith(dropItem)) {
|
||||
// 드롭 아이템 앞 부분이 몬스터 이름
|
||||
String monsterPart;
|
||||
if (itemString.endsWith(dropItemProperCase)) {
|
||||
monsterPart = itemString
|
||||
.substring(0, itemString.length - dropItemProperCase.length)
|
||||
.trim();
|
||||
} else {
|
||||
monsterPart = itemString
|
||||
.substring(0, itemString.length - dropItem.length)
|
||||
.trim();
|
||||
}
|
||||
|
||||
if (monsterPart.isEmpty) continue;
|
||||
|
||||
// 몬스터 이름 번역 (소문자를 원래 형태로 변환하여 찾기)
|
||||
final monsterNameKey = _toTitleCase(monsterPart);
|
||||
final monsterT = monsterMap[monsterNameKey] ?? monsterPart;
|
||||
|
||||
final dropT = entry.value;
|
||||
if (isKo) {
|
||||
return '$monsterT의 $dropT';
|
||||
} else {
|
||||
return '$monsterTの$dropT';
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// 첫 글자만 대문자로 (나머지는 그대로)
|
||||
static String _properCase(String s) {
|
||||
if (s.isEmpty) return s;
|
||||
return s[0].toUpperCase() + s.substring(1);
|
||||
}
|
||||
|
||||
/// 각 단어의 첫 글자를 대문자로 (Title Case)
|
||||
/// 하이픈으로 연결된 단어도 처리 (예: "off-by-one" → "Off-by-One")
|
||||
static String _toTitleCase(String s) {
|
||||
return s
|
||||
.split(' ')
|
||||
.map((word) {
|
||||
if (word.isEmpty) return word;
|
||||
// 하이픈 포함 단어 처리
|
||||
if (word.contains('-')) {
|
||||
return word
|
||||
.split('-')
|
||||
.map((part) {
|
||||
if (part.isEmpty) return part;
|
||||
return part[0].toUpperCase() + part.substring(1);
|
||||
})
|
||||
.join('-');
|
||||
}
|
||||
return word[0].toUpperCase() + word.substring(1);
|
||||
})
|
||||
.join(' ');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user