fix(animation): ASCII 애니메이션 높낮이/공백 문제 수정
- walkingAnimation, townAnimation 4줄 → 3줄 통일 - character_frames.dart 모든 프레임 폭 6자로 통일 - _compose() 이펙트 Y 위치 동적 계산 (하드코딩 제거) - withShield() 3줄 캐릭터용으로 수정 (index 3 → index 1) - BattleComposer 캔버스 시스템 및 배경 합성 추가 - 무기 카테고리별 이펙트, 몬스터 크기/색상 시스템 구현
This commit is contained in:
@@ -177,470 +177,290 @@ MonsterCategory getMonsterCategory(String? monsterBaseName) {
|
|||||||
return MonsterCategory.beast;
|
return MonsterCategory.beast;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 기본 전투 애니메이션 (beast - 고양이 모양)
|
/// 기본 전투 애니메이션 (beast - 고양이 모양, 심플 3줄)
|
||||||
const battleAnimationBeast = AsciiAnimationData(
|
const battleAnimationBeast = AsciiAnimationData(
|
||||||
frames: [
|
frames: [
|
||||||
// 프레임 1: 대치
|
// 프레임 1: 대치
|
||||||
'''
|
'''
|
||||||
,O, /\\___/\\
|
o vs /\\_/\\
|
||||||
/( )\\ ( o o )
|
/|\\ ( o.o )
|
||||||
/ \\ vs ( =^= )
|
/ \\ > ^ <''',
|
||||||
_| |_ /| |\\
|
|
||||||
| | / | | \\
|
|
||||||
_| |_ | |_____| |
|
|
||||||
|_________| |___| |___|''',
|
|
||||||
// 프레임 2: 공격 준비
|
// 프레임 2: 공격 준비
|
||||||
'''
|
'''
|
||||||
O /\\___/\\
|
o----o /\\_/\\
|
||||||
/|\\----o ( o o )
|
/|\\ ( o.o )
|
||||||
/ \\ ( =^= )
|
/ \\ > ^ <''',
|
||||||
_| |_ /| |\\
|
|
||||||
| | / | | \\
|
|
||||||
_| |_ | |_____| |
|
|
||||||
|_________| |___| |___|''',
|
|
||||||
// 프레임 3: 공격 중
|
// 프레임 3: 공격 중
|
||||||
'''
|
'''
|
||||||
O o--->/\\___/\\
|
o o-----> /\\_/\\
|
||||||
/|\\-----------> ( X X )
|
/|\\ ( X.X )
|
||||||
/ \\ ( =^= )
|
/ \\ > ^ <''',
|
||||||
_| |_ /| |\\
|
|
||||||
| | / | | \\
|
|
||||||
_| |_ | |_____| |
|
|
||||||
|_________| |___| |___|''',
|
|
||||||
// 프레임 4: 히트
|
// 프레임 4: 히트
|
||||||
'''
|
'''
|
||||||
O /\\___/\\
|
o **** /\\_/\\
|
||||||
/|\\ **** ( X X ) ****
|
/|\\ *** ( X.X ) ***
|
||||||
/ \\ ** ( =^= ) **
|
/ \\ > ~ <''',
|
||||||
_| |_ /| |\\
|
|
||||||
| | / | | \\
|
|
||||||
_| |_ | |_____| |
|
|
||||||
|_________| |___| |___|''',
|
|
||||||
// 프레임 5: 복귀
|
// 프레임 5: 복귀
|
||||||
'''
|
'''
|
||||||
\\O/ /\\___/\\
|
\\o/ /\\_/\\
|
||||||
| ( - - )
|
| ( -.-)
|
||||||
/ \\ ( =^= )
|
/ \\ > ^ <''',
|
||||||
_| |_ /| |\\
|
|
||||||
| | / | | \\
|
|
||||||
_| |_ | |_____| |
|
|
||||||
|_________| |___| |___|''',
|
|
||||||
],
|
],
|
||||||
frameIntervalMs: 220,
|
frameIntervalMs: 220,
|
||||||
);
|
);
|
||||||
|
|
||||||
/// 마을/상점 애니메이션 (7줄)
|
/// 마을/상점 애니메이션 (심플 3줄 캐릭터)
|
||||||
const townAnimation = AsciiAnimationData(
|
const townAnimation = AsciiAnimationData(
|
||||||
frames: [
|
frames: [
|
||||||
// 프레임 1: 상점 앞에서 대기
|
// 프레임 1: 상점 앞에서 대기
|
||||||
'''
|
'''
|
||||||
_______________
|
___________ o
|
||||||
/ \\ O
|
/ SHOP \\/|\\
|
||||||
| SHOP | /|\\
|
~~|__|____|__|/ \\~~~~~~~~~~~~~''',
|
||||||
| [=====] | / \\
|
|
||||||
| | | | |
|
|
||||||
|___|_____|______| _|_
|
|
||||||
~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~''',
|
|
||||||
// 프레임 2: 상점으로 이동
|
// 프레임 2: 상점으로 이동
|
||||||
'''
|
'''
|
||||||
_______________
|
___________ o
|
||||||
/ \\ O
|
/ SHOP \\/|\\
|
||||||
| SHOP | /|\\
|
~~|__|____|__|/ \\~~~~~~~~~~~~~''',
|
||||||
| [=====] | / \\
|
// 프레임 3: 거래 시작
|
||||||
| | | | |
|
|
||||||
|___|_____|______| _|_
|
|
||||||
~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~''',
|
|
||||||
// 프레임 3: 상점 앞 도착
|
|
||||||
'''
|
'''
|
||||||
_______________
|
___________ o \$
|
||||||
/ \\ O
|
/ SHOP \\/|\\ \$
|
||||||
| SHOP | /|\\
|
~~|__[ @@ ]__|/ \\ \$~~~~~~~~~~~''',
|
||||||
| [=====] | / \\
|
|
||||||
| | | | |
|
|
||||||
|___|_____|______| _|_
|
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~''',
|
|
||||||
// 프레임 4: 거래 중
|
// 프레임 4: 거래 중
|
||||||
'''
|
'''
|
||||||
_______________
|
___________ o \$\$
|
||||||
/ \\ O \$
|
/ SHOP \\/|\\ \$\$
|
||||||
| SHOP | /|\\ \$
|
~~|__[ @@ ]__|/ \\ \$\$~~~~~~~~~~''',
|
||||||
| [=====] | /\\\$
|
|
||||||
| | @ | | |
|
|
||||||
|___|_____|______| _|_
|
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~''',
|
|
||||||
// 프레임 5: 거래 완료
|
// 프레임 5: 거래 완료
|
||||||
'''
|
'''
|
||||||
_______________
|
___________ \\o/ +
|
||||||
/ \\ \\O/
|
/ SHOP \\ | +
|
||||||
| SHOP | | +
|
~~|__[ @@ ]__|/ \\ +~~~~~~~~~~~''',
|
||||||
| [=====] | / \\ +
|
|
||||||
| | @ | | | +
|
|
||||||
|___|_____|______| _|_
|
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~''',
|
|
||||||
],
|
],
|
||||||
frameIntervalMs: 280,
|
frameIntervalMs: 280,
|
||||||
);
|
);
|
||||||
|
|
||||||
/// 걷는 애니메이션 (7줄, 배경 포함)
|
/// 걷는 애니메이션 (심플 3줄 캐릭터 + 배경)
|
||||||
const walkingAnimation = AsciiAnimationData(
|
const walkingAnimation = AsciiAnimationData(
|
||||||
frames: [
|
frames: [
|
||||||
// 프레임 1: 서있기
|
// 프레임 1: 서있기
|
||||||
'''
|
'''
|
||||||
O
|
~~~~ o ~~~~
|
||||||
/|\\
|
~~~~~~ /|\\ ~~~~~~
|
||||||
/ \\
|
~~~~~~~~ / \\ ~~~~~~~~''',
|
||||||
~~ | ~~
|
|
||||||
~~~~ _|_ ~~~~
|
|
||||||
~~~~~~ ~~~~~~~~ ~~~~~~
|
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~''',
|
|
||||||
// 프레임 2: 왼발 앞
|
// 프레임 2: 왼발 앞
|
||||||
'''
|
'''
|
||||||
O
|
~~~~ o ~~~~
|
||||||
/|\\
|
~~~~~~ /|\\ ~~~~~~
|
||||||
/|
|
~~~~~~~~ /| ~~~~~~~~''',
|
||||||
~~ / \\ ~~
|
|
||||||
~~~~ _|_ ~~~~
|
|
||||||
~~~~~~ ~~~~~~~~ ~~~~~~
|
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~''',
|
|
||||||
// 프레임 3: 이동 중
|
// 프레임 3: 이동 중
|
||||||
'''
|
'''
|
||||||
O
|
~~~~ o ~~~~
|
||||||
/|\\
|
~~~~~~ /|\\ ~~~~~~
|
||||||
|\\
|
~~~~~~~~ |\\ ~~~~~~~~''',
|
||||||
~~ / \\ ~~
|
|
||||||
~~~~ _|_ ~~~~
|
|
||||||
~~~~~~ ~~~~~~~~ ~~~~~~
|
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~''',
|
|
||||||
// 프레임 4: 오른발 앞
|
// 프레임 4: 오른발 앞
|
||||||
'''
|
'''
|
||||||
O
|
~~~~ o ~~~~
|
||||||
/|\\
|
~~~~~~ /|\\ ~~~~~~
|
||||||
|/
|
~~~~~~~~ |/ ~~~~~~~~''',
|
||||||
~~ / \\ ~~
|
|
||||||
~~~~ _|_ ~~~~
|
|
||||||
~~~~~~ ~~~~~~~~ ~~~~~~
|
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~''',
|
|
||||||
// 프레임 5: 복귀
|
// 프레임 5: 복귀
|
||||||
'''
|
'''
|
||||||
O
|
~~~~ o ~~~~
|
||||||
/|\\
|
~~~~~~ /|\\ ~~~~~~
|
||||||
/ \\
|
~~~~~~~~ / \\ ~~~~~~~~''',
|
||||||
~~ | ~~
|
|
||||||
~~~~ _|_ ~~~~
|
|
||||||
~~~~~~ ~~~~~~~~ ~~~~~~
|
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~''',
|
|
||||||
],
|
],
|
||||||
frameIntervalMs: 180,
|
frameIntervalMs: 180,
|
||||||
);
|
);
|
||||||
|
|
||||||
/// 곤충 전투 애니메이션
|
/// 곤충 전투 애니메이션 (심플 3줄)
|
||||||
const battleAnimationInsect = AsciiAnimationData(
|
const battleAnimationInsect = AsciiAnimationData(
|
||||||
frames: [
|
frames: [
|
||||||
// 프레임 1: 대치
|
// 프레임 1: 대치
|
||||||
'''
|
'''
|
||||||
,O, /\\_/\\
|
o vs /\\_/\\
|
||||||
/( )\\ ( o o )
|
/|\\ ( o o )
|
||||||
/ \\ vs /|=====|\\
|
/ \\ /|=====|\\''',
|
||||||
_| |_ < | | >
|
|
||||||
| | \\|_____|/
|
|
||||||
_| |_ / \\
|
|
||||||
|_________| /_______\\''',
|
|
||||||
// 프레임 2: 공격 준비
|
// 프레임 2: 공격 준비
|
||||||
'''
|
'''
|
||||||
O /\\_/\\
|
o----o /\\_/\\
|
||||||
/|\\----o ( o o )
|
/|\\ ( o o )
|
||||||
/ \\ /|=====|\\
|
/ \\ /|=====|\\''',
|
||||||
_| |_ < | | >
|
|
||||||
| | \\|_____|/
|
|
||||||
_| |_ / \\
|
|
||||||
|_________| /_______\\''',
|
|
||||||
// 프레임 3: 공격 중
|
// 프레임 3: 공격 중
|
||||||
'''
|
'''
|
||||||
O o-->/\\_/\\
|
o o-----> /\\_/\\
|
||||||
/|\\----------> ( o o )
|
/|\\ ( X X )
|
||||||
/ \\ /|=====|\\
|
/ \\ /|=====|\\''',
|
||||||
_| |_ < | | >
|
|
||||||
| | \\|_____|/
|
|
||||||
_| |_ / \\
|
|
||||||
|_________| /_______\\''',
|
|
||||||
// 프레임 4: 히트
|
// 프레임 4: 히트
|
||||||
'''
|
'''
|
||||||
O /\\_/\\
|
o **** /\\_/\\
|
||||||
/|\\ **** ( X X ) ****
|
/|\\ *** ( X X ) ***
|
||||||
/ \\ ** /|=====|\\ **
|
/ \\ /|=====|\\''',
|
||||||
_| |_ < | | >
|
|
||||||
| | \\|_____|/
|
|
||||||
_| |_ / \\
|
|
||||||
|_________| /_______\\''',
|
|
||||||
// 프레임 5: 복귀
|
// 프레임 5: 복귀
|
||||||
'''
|
'''
|
||||||
\\O/ /\\_/\\
|
\\o/ /\\_/\\
|
||||||
| ( - - )
|
| ( - - )
|
||||||
/ \\ /|=====|\\
|
/ \\ /|=====|\\''',
|
||||||
_| |_ < | | >
|
|
||||||
| | \\|_____|/
|
|
||||||
_| |_ / \\
|
|
||||||
|_________| /_______\\''',
|
|
||||||
],
|
],
|
||||||
frameIntervalMs: 220,
|
frameIntervalMs: 220,
|
||||||
);
|
);
|
||||||
|
|
||||||
/// 인간형 전투 애니메이션
|
/// 인간형 전투 애니메이션 (심플 3줄)
|
||||||
const battleAnimationHumanoid = AsciiAnimationData(
|
const battleAnimationHumanoid = AsciiAnimationData(
|
||||||
frames: [
|
frames: [
|
||||||
// 프레임 1: 대치
|
// 프레임 1: 대치
|
||||||
'''
|
'''
|
||||||
,O, O
|
o vs O
|
||||||
/( )\\ /|\\
|
/|\\ /|\\
|
||||||
/ \\ vs / | \\
|
/ \\ / | \\''',
|
||||||
_| |_ ___|___
|
|
||||||
| | | |
|
|
||||||
_| |_ | orc |
|
|
||||||
|_________| |_______|''',
|
|
||||||
// 프레임 2: 공격 준비
|
// 프레임 2: 공격 준비
|
||||||
'''
|
'''
|
||||||
O O
|
o----o O
|
||||||
/|\\----o /|\\
|
/|\\ /|\\
|
||||||
/ \\ / | \\
|
/ \\ / | \\''',
|
||||||
_| |_ ___|___
|
|
||||||
| | | |
|
|
||||||
_| |_ | orc |
|
|
||||||
|_________| |_______|''',
|
|
||||||
// 프레임 3: 공격 중
|
// 프레임 3: 공격 중
|
||||||
'''
|
'''
|
||||||
O o----> O
|
o o-----> O
|
||||||
/|\\-----------> /|\\
|
/|\\ X|X
|
||||||
/ \\ / | \\
|
/ \\ / | \\''',
|
||||||
_| |_ ___|___
|
|
||||||
| | | |
|
|
||||||
_| |_ | orc |
|
|
||||||
|_________| |_______|''',
|
|
||||||
// 프레임 4: 히트
|
// 프레임 4: 히트
|
||||||
'''
|
'''
|
||||||
O O
|
o **** O
|
||||||
/|\\ **** X|X ****
|
/|\\ *** X|X ***
|
||||||
/ \\ ** / | \\ **
|
/ \\ / | \\''',
|
||||||
_| |_ ___|___
|
|
||||||
| | | |
|
|
||||||
_| |_ | orc |
|
|
||||||
|_________| |_______|''',
|
|
||||||
// 프레임 5: 복귀
|
// 프레임 5: 복귀
|
||||||
'''
|
'''
|
||||||
\\O/ O
|
\\o/ O
|
||||||
| /|\\
|
| /|\\
|
||||||
/ \\ / | \\
|
/ \\ / | \\''',
|
||||||
_| |_ ___|___
|
|
||||||
| | | |
|
|
||||||
_| |_ | orc |
|
|
||||||
|_________| |_______|''',
|
|
||||||
],
|
],
|
||||||
frameIntervalMs: 220,
|
frameIntervalMs: 220,
|
||||||
);
|
);
|
||||||
|
|
||||||
/// 언데드 전투 애니메이션
|
/// 언데드 전투 애니메이션 (심플 3줄)
|
||||||
const battleAnimationUndead = AsciiAnimationData(
|
const battleAnimationUndead = AsciiAnimationData(
|
||||||
frames: [
|
frames: [
|
||||||
// 프레임 1: 대치
|
// 프레임 1: 대치
|
||||||
'''
|
'''
|
||||||
,O, .-.
|
o vs .-.
|
||||||
/( )\\ (o.o)
|
/|\\ (o.o)
|
||||||
/ \\ vs |=|
|
/ \\ |=|''',
|
||||||
_| |_ /|X|\\
|
|
||||||
| | / | | \\
|
|
||||||
_| |_ \\_|_|_/
|
|
||||||
|_________| _/ \\_''',
|
|
||||||
// 프레임 2: 공격 준비
|
// 프레임 2: 공격 준비
|
||||||
'''
|
'''
|
||||||
O .-.
|
o----o .-.
|
||||||
/|\\----o (o.o)
|
/|\\ (o.o)
|
||||||
/ \\ |=|
|
/ \\ |=|''',
|
||||||
_| |_ /|X|\\
|
|
||||||
| | / | | \\
|
|
||||||
_| |_ \\_|_|_/
|
|
||||||
|_________| _/ \\_''',
|
|
||||||
// 프레임 3: 공격 중
|
// 프레임 3: 공격 중
|
||||||
'''
|
'''
|
||||||
O o--->.-.
|
o o-----> .-.
|
||||||
/|\\-----------> (o.o)
|
/|\\ (X.X)
|
||||||
/ \\ |=|
|
/ \\ |=|''',
|
||||||
_| |_ /|X|\\
|
|
||||||
| | / | | \\
|
|
||||||
_| |_ \\_|_|_/
|
|
||||||
|_________| _/ \\_''',
|
|
||||||
// 프레임 4: 히트
|
// 프레임 4: 히트
|
||||||
'''
|
'''
|
||||||
O .-.
|
o **** .-.
|
||||||
/|\\ **** (X.X) ****
|
/|\\ *** (X.X) ***
|
||||||
/ \\ ** |=| **
|
/ \\ |~|''',
|
||||||
_| |_ /|X|\\
|
|
||||||
| | / | | \\
|
|
||||||
_| |_ \\_|_|_/
|
|
||||||
|_________| _/ \\_''',
|
|
||||||
// 프레임 5: 복귀
|
// 프레임 5: 복귀
|
||||||
'''
|
'''
|
||||||
\\O/ .-.
|
\\o/ .-.
|
||||||
| (-.-)
|
| (-.-)
|
||||||
/ \\ |=|
|
/ \\ |=|''',
|
||||||
_| |_ /|X|\\
|
|
||||||
| | / | | \\
|
|
||||||
_| |_ \\_|_|_/
|
|
||||||
|_________| _/ \\_''',
|
|
||||||
],
|
],
|
||||||
frameIntervalMs: 250,
|
frameIntervalMs: 250,
|
||||||
);
|
);
|
||||||
|
|
||||||
/// 드래곤 전투 애니메이션
|
/// 드래곤 전투 애니메이션 (심플 3줄)
|
||||||
const battleAnimationDragon = AsciiAnimationData(
|
const battleAnimationDragon = AsciiAnimationData(
|
||||||
frames: [
|
frames: [
|
||||||
// 프레임 1: 대치
|
// 프레임 1: 대치
|
||||||
'''
|
'''
|
||||||
,O, __/\\__
|
o vs __/\\__
|
||||||
/( )\\ / \\
|
/|\\ < (O)(O) >
|
||||||
/ \\ vs < (O)(O) >
|
/ \\ \\ \\/ /''',
|
||||||
_| |_ \\ \\/ /
|
|
||||||
| | \\ /
|
|
||||||
_| |_ /|\\~~~/|\\
|
|
||||||
|_________| /_________\\''',
|
|
||||||
// 프레임 2: 공격 준비
|
// 프레임 2: 공격 준비
|
||||||
'''
|
'''
|
||||||
O __/\\__
|
o----o __/\\__
|
||||||
/|\\----o / \\
|
/|\\ < (O)(O) >
|
||||||
/ \\ < (O)(O) >
|
/ \\ \\ \\/ /''',
|
||||||
_| |_ \\ \\/ /
|
|
||||||
| | \\ /
|
|
||||||
_| |_ /|\\~~~/|\\
|
|
||||||
|_________| /_________\\''',
|
|
||||||
// 프레임 3: 공격 중
|
// 프레임 3: 공격 중
|
||||||
'''
|
'''
|
||||||
O o--->__/\\__
|
o o-----> __/\\__
|
||||||
/|\\---------> / \\
|
/|\\ < (X)(X) >
|
||||||
/ \\ < (O)(O) >
|
/ \\ \\ \\/ /''',
|
||||||
_| |_ \\ \\/ /
|
|
||||||
| | \\ /
|
|
||||||
_| |_ /|\\~~~/|\\
|
|
||||||
|_________| /_________\\''',
|
|
||||||
// 프레임 4: 히트
|
// 프레임 4: 히트
|
||||||
'''
|
'''
|
||||||
O __/\\__
|
o **** __/\\__
|
||||||
/|\\ **** / >< \\ ****
|
/|\\ *** < (X)(X) > ***
|
||||||
/ \\ ** < (X)(X) > **
|
/ \\ \\ ~~ /''',
|
||||||
_| |_ \\ \\/ /
|
|
||||||
| | \\ /
|
|
||||||
_| |_ /|\\~~~/|\\
|
|
||||||
|_________| /_________\\''',
|
|
||||||
// 프레임 5: 복귀
|
// 프레임 5: 복귀
|
||||||
'''
|
'''
|
||||||
\\O/ __/\\__
|
\\o/ __/\\__
|
||||||
| / \\
|
| < (-)(-)>
|
||||||
/ \\ < (-)(-)>
|
/ \\ \\ \\/ /''',
|
||||||
_| |_ \\ \\/ /
|
|
||||||
| | \\ /
|
|
||||||
_| |_ /|\\~~~/|\\
|
|
||||||
|_________| /_________\\''',
|
|
||||||
],
|
],
|
||||||
frameIntervalMs: 200,
|
frameIntervalMs: 200,
|
||||||
);
|
);
|
||||||
|
|
||||||
/// 슬라임 전투 애니메이션
|
/// 슬라임 전투 애니메이션 (심플 3줄)
|
||||||
const battleAnimationSlime = AsciiAnimationData(
|
const battleAnimationSlime = AsciiAnimationData(
|
||||||
frames: [
|
frames: [
|
||||||
// 프레임 1: 대치
|
// 프레임 1: 대치
|
||||||
'''
|
'''
|
||||||
,O, .---.
|
o vs .---.
|
||||||
/( )\\ / \\
|
/|\\ ( o o )
|
||||||
/ \\ vs ( o o )
|
/ \\ ~~~~~''',
|
||||||
_| |_ \\ ~ /
|
|
||||||
| | '---'
|
|
||||||
_| |_ ~~~~~~~
|
|
||||||
|_________| ~~~~~~~~~''',
|
|
||||||
// 프레임 2: 공격 준비
|
// 프레임 2: 공격 준비
|
||||||
'''
|
'''
|
||||||
O .---.
|
o----o .---.
|
||||||
/|\\----o / \\
|
/|\\ ( o o )
|
||||||
/ \\ ( o o )
|
/ \\ ~~~~~''',
|
||||||
_| |_ \\ ~ /
|
|
||||||
| | '---'
|
|
||||||
_| |_ ~~~~~~~
|
|
||||||
|_________| ~~~~~~~~~''',
|
|
||||||
// 프레임 3: 공격 중
|
// 프레임 3: 공격 중
|
||||||
'''
|
'''
|
||||||
O o--->.---.
|
o o-----> .---.
|
||||||
/|\\---------> / \\
|
/|\\ ( X X )
|
||||||
/ \\ ( o o )
|
/ \\ ~~~~~''',
|
||||||
_| |_ \\ ~ /
|
|
||||||
| | '---'
|
|
||||||
_| |_ ~~~~~~~
|
|
||||||
|_________| ~~~~~~~~~''',
|
|
||||||
// 프레임 4: 히트
|
// 프레임 4: 히트
|
||||||
'''
|
'''
|
||||||
O .---.
|
o **** .---.
|
||||||
/|\\ **** / X X \\ ****
|
/|\\ *** ( X X ) ***
|
||||||
/ \\ ** ( ~ ) **
|
/ \\ ~~~~~''',
|
||||||
_| |_ \\ /
|
|
||||||
| | '---'
|
|
||||||
_| |_ ~~~~~~~
|
|
||||||
|_________| ~~~~~~~~~''',
|
|
||||||
// 프레임 5: 복귀
|
// 프레임 5: 복귀
|
||||||
'''
|
'''
|
||||||
\\O/ .---.
|
\\o/ .---.
|
||||||
| / \\
|
| ( - - )
|
||||||
/ \\ ( - - )
|
/ \\ ~~~~~''',
|
||||||
_| |_ \\ ~ /
|
|
||||||
| | '---'
|
|
||||||
_| |_ ~~~~~~~
|
|
||||||
|_________| ~~~~~~~~~''',
|
|
||||||
],
|
],
|
||||||
frameIntervalMs: 280,
|
frameIntervalMs: 280,
|
||||||
);
|
);
|
||||||
|
|
||||||
/// 악마 전투 애니메이션
|
/// 악마 전투 애니메이션 (심플 3줄)
|
||||||
const battleAnimationDemon = AsciiAnimationData(
|
const battleAnimationDemon = AsciiAnimationData(
|
||||||
frames: [
|
frames: [
|
||||||
// 프레임 1: 대치
|
// 프레임 1: 대치
|
||||||
'''
|
'''
|
||||||
,O, /\\ /\\
|
o vs /\\ /\\
|
||||||
/( )\\ ( \\ / )
|
/|\\ ( o V o )
|
||||||
/ \\ vs \\ o o /
|
/ \\ \\ ~~~ /''',
|
||||||
_| |_ | V |
|
|
||||||
| | | ~~~ |
|
|
||||||
_| |_ /| |\\
|
|
||||||
|_________| /___|___|_\\''',
|
|
||||||
// 프레임 2: 공격 준비
|
// 프레임 2: 공격 준비
|
||||||
'''
|
'''
|
||||||
O /\\ /\\
|
o----o /\\ /\\
|
||||||
/|\\----o ( \\ / )
|
/|\\ ( o V o )
|
||||||
/ \\ \\ o o /
|
/ \\ \\ ~~~ /''',
|
||||||
_| |_ | V |
|
|
||||||
| | | ~~~ |
|
|
||||||
_| |_ /| |\\
|
|
||||||
|_________| /___|___|_\\''',
|
|
||||||
// 프레임 3: 공격 중
|
// 프레임 3: 공격 중
|
||||||
'''
|
'''
|
||||||
O o--->/\\ /\\
|
o o-----> /\\ /\\
|
||||||
/|\\--------> ( \\ / )
|
/|\\ ( X V X )
|
||||||
/ \\ \\ o o /
|
/ \\ \\ ~~~ /''',
|
||||||
_| |_ | V |
|
|
||||||
| | | ~~~ |
|
|
||||||
_| |_ /| |\\
|
|
||||||
|_________| /___|___|_\\''',
|
|
||||||
// 프레임 4: 히트
|
// 프레임 4: 히트
|
||||||
'''
|
'''
|
||||||
O /\\ /\\
|
o **** /\\ /\\
|
||||||
/|\\ **** ( X X ) ****
|
/|\\ *** ( X V X ) ***
|
||||||
/ \\ ** \\ X X / **
|
/ \\ \\ ~~~ /''',
|
||||||
_| |_ | V |
|
|
||||||
| | | ~~~ |
|
|
||||||
_| |_ /| |\\
|
|
||||||
|_________| /___|___|_\\''',
|
|
||||||
// 프레임 5: 복귀
|
// 프레임 5: 복귀
|
||||||
'''
|
'''
|
||||||
\\O/ /\\ /\\
|
\\o/ /\\ /\\
|
||||||
| ( \\ / )
|
| ( - V - )
|
||||||
/ \\ \\ - - /
|
/ \\ \\ ~~~ /''',
|
||||||
_| |_ | V |
|
|
||||||
| | | ~~~ |
|
|
||||||
_| |_ /| |\\
|
|
||||||
|_________| /___|___|_\\''',
|
|
||||||
],
|
],
|
||||||
frameIntervalMs: 200,
|
frameIntervalMs: 200,
|
||||||
);
|
);
|
||||||
|
|||||||
178
lib/src/core/animation/background_data.dart
Normal file
178
lib/src/core/animation/background_data.dart
Normal file
@@ -0,0 +1,178 @@
|
|||||||
|
// 환경별 배경 패턴 데이터
|
||||||
|
// ASCII Patrol 스타일 - 패럴렉스 스크롤링 배경
|
||||||
|
|
||||||
|
import 'package:askiineverdie/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,
|
||||||
|
),
|
||||||
|
];
|
||||||
92
lib/src/core/animation/background_layer.dart
Normal file
92
lib/src/core/animation/background_layer.dart
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
// 배경 레이어 시스템 (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;
|
||||||
|
}
|
||||||
713
lib/src/core/animation/battle_composer.dart
Normal file
713
lib/src/core/animation/battle_composer.dart
Normal file
@@ -0,0 +1,713 @@
|
|||||||
|
// BattleComposer - 전투 프레임 실시간 합성
|
||||||
|
// Stone Story RPG 스타일 참고 - 8줄 캐릭터/몬스터, 60자 폭
|
||||||
|
// ASCII Patrol 스타일 패럴렉스 배경
|
||||||
|
|
||||||
|
import 'package:askiineverdie/src/core/animation/ascii_animation_data.dart';
|
||||||
|
import 'package:askiineverdie/src/core/animation/background_data.dart';
|
||||||
|
import 'package:askiineverdie/src/core/animation/background_layer.dart';
|
||||||
|
import 'package:askiineverdie/src/core/animation/character_frames.dart';
|
||||||
|
import 'package:askiineverdie/src/core/animation/monster_size.dart';
|
||||||
|
import 'package:askiineverdie/src/core/animation/weapon_category.dart';
|
||||||
|
import 'package:askiineverdie/src/core/animation/weapon_effects.dart';
|
||||||
|
|
||||||
|
/// 전투 프레임 합성기
|
||||||
|
class BattleComposer {
|
||||||
|
const BattleComposer({
|
||||||
|
required this.weaponCategory,
|
||||||
|
required this.hasShield,
|
||||||
|
required this.monsterCategory,
|
||||||
|
required this.monsterSize,
|
||||||
|
});
|
||||||
|
|
||||||
|
final WeaponCategory weaponCategory;
|
||||||
|
final bool hasShield;
|
||||||
|
final MonsterCategory monsterCategory;
|
||||||
|
final MonsterSize monsterSize;
|
||||||
|
|
||||||
|
/// 전체 프레임 폭 (문자 수)
|
||||||
|
static const int frameWidth = 60;
|
||||||
|
|
||||||
|
/// 프레임 높이 (줄 수)
|
||||||
|
static const int frameHeight = 8;
|
||||||
|
|
||||||
|
/// 영역 분할
|
||||||
|
static const int characterWidth = 18;
|
||||||
|
static const int effectWidth = 24;
|
||||||
|
static const int monsterWidth = 18;
|
||||||
|
|
||||||
|
/// 전투 프레임 생성 (배경 없음)
|
||||||
|
String composeFrame(BattlePhase phase, int subFrame, String? monsterBaseName) {
|
||||||
|
// 캐릭터 프레임
|
||||||
|
var charFrame = getCharacterFrame(phase, subFrame);
|
||||||
|
if (hasShield) {
|
||||||
|
charFrame = charFrame.withShield();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 몬스터 프레임 (애니메이션 포함)
|
||||||
|
final monsterFrames =
|
||||||
|
_getAnimatedMonsterFrames(monsterCategory, monsterSize, phase);
|
||||||
|
final monsterFrame = monsterFrames[subFrame % monsterFrames.length];
|
||||||
|
|
||||||
|
// 무기 이펙트 (단일 라인)
|
||||||
|
final effect = getWeaponEffect(weaponCategory);
|
||||||
|
final effectLine = _getEffectLine(effect, phase, subFrame);
|
||||||
|
|
||||||
|
// 프레임 합성
|
||||||
|
return _compose(charFrame.lines, monsterFrame, effectLine, phase);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 전투 프레임 생성 (배경 포함, ASCII Patrol 스타일)
|
||||||
|
String composeFrameWithBackground(
|
||||||
|
BattlePhase phase,
|
||||||
|
int subFrame,
|
||||||
|
String? monsterBaseName,
|
||||||
|
EnvironmentType environment,
|
||||||
|
int globalTick,
|
||||||
|
) {
|
||||||
|
// 1. 8x60 캔버스 생성 (공백으로 초기화)
|
||||||
|
final canvas =
|
||||||
|
List.generate(frameHeight, (_) => List.filled(frameWidth, ' '));
|
||||||
|
|
||||||
|
// 2. 배경 레이어 그리기 (뒤에서 앞으로)
|
||||||
|
final layers = getBackgroundLayers(environment);
|
||||||
|
for (final layer in layers) {
|
||||||
|
_drawBackgroundLayer(canvas, layer, globalTick);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 캐릭터 프레임 (정규화하여 왼쪽 정렬)
|
||||||
|
var charFrame = getCharacterFrame(phase, subFrame);
|
||||||
|
if (hasShield) {
|
||||||
|
charFrame = charFrame.withShield();
|
||||||
|
}
|
||||||
|
final normalizedChar = _normalizeSprite(charFrame.lines, characterWidth);
|
||||||
|
final charY = frameHeight - normalizedChar.length;
|
||||||
|
_overlaySpriteWithSpaces(canvas, normalizedChar, 0, charY);
|
||||||
|
|
||||||
|
// 4. 몬스터 프레임 (정규화하여 오른쪽 정렬)
|
||||||
|
final monsterFrames =
|
||||||
|
_getAnimatedMonsterFrames(monsterCategory, monsterSize, phase);
|
||||||
|
final monsterFrame = monsterFrames[subFrame % monsterFrames.length];
|
||||||
|
final normalizedMonster = _normalizeSpriteRight(monsterFrame, monsterWidth);
|
||||||
|
final monsterX = frameWidth - monsterWidth;
|
||||||
|
final monsterY = frameHeight - normalizedMonster.length;
|
||||||
|
_overlaySpriteWithSpaces(canvas, normalizedMonster, monsterX, monsterY);
|
||||||
|
|
||||||
|
// 5. 멀티라인 이펙트 오버레이 (공격/히트/준비 페이즈)
|
||||||
|
if (phase == BattlePhase.prepare ||
|
||||||
|
phase == BattlePhase.attack ||
|
||||||
|
phase == BattlePhase.hit) {
|
||||||
|
final effect = getWeaponEffect(weaponCategory);
|
||||||
|
final effectLines = _getEffectLines(effect, phase, subFrame);
|
||||||
|
if (effectLines.isNotEmpty) {
|
||||||
|
// 이펙트 Y 위치: 캐릭터 팔 높이 (2번째 줄, 몸통) 기준
|
||||||
|
final effectY = charY + 1;
|
||||||
|
for (var i = 0; i < effectLines.length; i++) {
|
||||||
|
final y = effectY + i;
|
||||||
|
if (y >= 0 && y < frameHeight && effectLines[i].isNotEmpty) {
|
||||||
|
_overlayText(canvas, effectLines[i], characterWidth, y);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 6. 문자열로 변환
|
||||||
|
return canvas.map((row) => row.join()).join('\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 스프라이트를 지정 폭으로 정규화 (왼쪽 정렬)
|
||||||
|
List<String> _normalizeSprite(List<String> sprite, int width) {
|
||||||
|
return sprite.map((line) => line.padRight(width).substring(0, width)).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 스프라이트를 지정 폭으로 정규화 (오른쪽 정렬)
|
||||||
|
List<String> _normalizeSpriteRight(List<String> sprite, int width) {
|
||||||
|
return sprite.map((line) {
|
||||||
|
final trimmed = line.trimRight();
|
||||||
|
if (trimmed.length >= width) return trimmed.substring(0, width);
|
||||||
|
return trimmed.padLeft(width);
|
||||||
|
}).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 스프라이트를 캔버스에 오버레이 (공백도 덮어쓰기 - Z-order용)
|
||||||
|
void _overlaySpriteWithSpaces(
|
||||||
|
List<List<String>> canvas,
|
||||||
|
List<String> sprite,
|
||||||
|
int startX,
|
||||||
|
int startY,
|
||||||
|
) {
|
||||||
|
for (var i = 0; i < sprite.length; i++) {
|
||||||
|
final y = startY + i;
|
||||||
|
if (y < 0 || y >= frameHeight) continue;
|
||||||
|
|
||||||
|
final line = sprite[i];
|
||||||
|
for (var j = 0; j < line.length; j++) {
|
||||||
|
final x = startX + j;
|
||||||
|
if (x < 0 || x >= frameWidth) continue;
|
||||||
|
|
||||||
|
final char = line[j];
|
||||||
|
// 공백이 아닌 문자만 덮어쓰기 (투명 배경 효과)
|
||||||
|
if (char != ' ') {
|
||||||
|
canvas[y][x] = char;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 배경 레이어를 캔버스에 그리기
|
||||||
|
void _drawBackgroundLayer(
|
||||||
|
List<List<String>> canvas,
|
||||||
|
BackgroundLayer layer,
|
||||||
|
int globalTick,
|
||||||
|
) {
|
||||||
|
for (var i = 0; i < layer.lines.length; i++) {
|
||||||
|
final y = layer.yStart + i;
|
||||||
|
if (y >= frameHeight) break;
|
||||||
|
|
||||||
|
final pattern = layer.lines[i];
|
||||||
|
if (pattern.isEmpty) continue;
|
||||||
|
|
||||||
|
// 스크롤 오프셋 계산
|
||||||
|
final offset = (globalTick * layer.scrollSpeed).toInt() % pattern.length;
|
||||||
|
|
||||||
|
// 패턴을 스크롤하며 그리기
|
||||||
|
for (var x = 0; x < frameWidth; x++) {
|
||||||
|
final patternIdx = (x + offset) % pattern.length;
|
||||||
|
final char = pattern[patternIdx];
|
||||||
|
if (char != ' ') {
|
||||||
|
canvas[y][x] = char;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 텍스트를 캔버스에 오버레이
|
||||||
|
void _overlayText(
|
||||||
|
List<List<String>> canvas,
|
||||||
|
String text,
|
||||||
|
int startX,
|
||||||
|
int y,
|
||||||
|
) {
|
||||||
|
if (y < 0 || y >= frameHeight) return;
|
||||||
|
|
||||||
|
for (var i = 0; i < text.length; i++) {
|
||||||
|
final x = startX + i;
|
||||||
|
if (x < 0 || x >= frameWidth) continue;
|
||||||
|
|
||||||
|
final char = text[i];
|
||||||
|
if (char != ' ') {
|
||||||
|
canvas[y][x] = char;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 멀티라인 이펙트 프레임 반환
|
||||||
|
List<String> _getEffectLines(
|
||||||
|
WeaponEffect effect, BattlePhase phase, int subFrame) {
|
||||||
|
final frames = switch (phase) {
|
||||||
|
BattlePhase.idle => <List<String>>[],
|
||||||
|
BattlePhase.prepare => effect.prepareFrames,
|
||||||
|
BattlePhase.attack => effect.attackFrames,
|
||||||
|
BattlePhase.hit => effect.hitFrames,
|
||||||
|
BattlePhase.recover => <List<String>>[],
|
||||||
|
};
|
||||||
|
if (frames.isEmpty) return [];
|
||||||
|
return frames[subFrame % frames.length];
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 단일 라인 이펙트 (하위 호환용)
|
||||||
|
String _getEffectLine(WeaponEffect effect, BattlePhase phase, int subFrame) {
|
||||||
|
final lines = _getEffectLines(effect, phase, subFrame);
|
||||||
|
if (lines.isEmpty) return '';
|
||||||
|
// 멀티라인 중 중간 라인 반환 (메인 이펙트)
|
||||||
|
final midIndex = lines.length ~/ 2;
|
||||||
|
return lines.length > midIndex ? lines[midIndex] : lines.first;
|
||||||
|
}
|
||||||
|
|
||||||
|
String _compose(
|
||||||
|
List<String> charLines,
|
||||||
|
List<String> monsterLines,
|
||||||
|
String effectLine,
|
||||||
|
BattlePhase phase,
|
||||||
|
) {
|
||||||
|
final result = <String>[];
|
||||||
|
|
||||||
|
// 캐릭터와 몬스터를 하단 정렬 (8줄 기준)
|
||||||
|
final charOffset = frameHeight - charLines.length;
|
||||||
|
final monsterOffset = frameHeight - monsterLines.length;
|
||||||
|
|
||||||
|
// 이펙트 Y 위치: 캐릭터 body/arm 줄 (charOffset + 1)
|
||||||
|
final effectRow = charOffset + 1;
|
||||||
|
|
||||||
|
for (var i = 0; i < frameHeight; i++) {
|
||||||
|
// 캐릭터 파트 (왼쪽 18자)
|
||||||
|
final charIdx = i - charOffset;
|
||||||
|
final charPart =
|
||||||
|
(charIdx >= 0 && charIdx < charLines.length ? charLines[charIdx] : '')
|
||||||
|
.padRight(characterWidth);
|
||||||
|
|
||||||
|
// 이펙트 파트 (중앙 24자) - 캐릭터 팔 높이에 표시
|
||||||
|
String effectPart = '';
|
||||||
|
if (i == effectRow &&
|
||||||
|
(phase == BattlePhase.attack || phase == BattlePhase.hit)) {
|
||||||
|
effectPart = effectLine;
|
||||||
|
}
|
||||||
|
effectPart = effectPart.padRight(effectWidth);
|
||||||
|
|
||||||
|
// 몬스터 파트 (오른쪽 18자)
|
||||||
|
final monsterIdx = i - monsterOffset;
|
||||||
|
final monsterPart = (monsterIdx >= 0 && monsterIdx < monsterLines.length
|
||||||
|
? monsterLines[monsterIdx]
|
||||||
|
: '')
|
||||||
|
.padLeft(monsterWidth);
|
||||||
|
|
||||||
|
result.add('$charPart$effectPart$monsterPart');
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.join('\n');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// 몬스터 애니메이션 프레임
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
/// 몬스터 애니메이션 프레임 반환 (페이즈별 다른 동작)
|
||||||
|
List<List<String>> _getAnimatedMonsterFrames(
|
||||||
|
MonsterCategory category,
|
||||||
|
MonsterSize size,
|
||||||
|
BattlePhase phase,
|
||||||
|
) {
|
||||||
|
// 피격 상태
|
||||||
|
if (phase == BattlePhase.hit) {
|
||||||
|
return _getMonsterHitFrames(category, size);
|
||||||
|
}
|
||||||
|
// 경계 상태 (prepare, attack)
|
||||||
|
if (phase == BattlePhase.prepare || phase == BattlePhase.attack) {
|
||||||
|
return _getMonsterAlertFrames(category, size);
|
||||||
|
}
|
||||||
|
// 일반 상태 (idle, recover)
|
||||||
|
return _getMonsterIdleFrames(category, size);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 일반 상태 몬스터 프레임
|
||||||
|
List<List<String>> _getMonsterIdleFrames(MonsterCategory category, MonsterSize size) {
|
||||||
|
return switch (size) {
|
||||||
|
MonsterSize.tiny => _tinyIdleFrames(category),
|
||||||
|
MonsterSize.small => _smallIdleFrames(category),
|
||||||
|
MonsterSize.medium => _mediumIdleFrames(category),
|
||||||
|
MonsterSize.large => _largeIdleFrames(category),
|
||||||
|
_ => _hugeIdleFrames(category), // huge 이상은 같은 프레임 사용
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 피격 상태 몬스터 프레임
|
||||||
|
List<List<String>> _getMonsterHitFrames(MonsterCategory category, MonsterSize size) {
|
||||||
|
return switch (size) {
|
||||||
|
MonsterSize.tiny => _tinyHitFrames(category),
|
||||||
|
MonsterSize.small => _smallHitFrames(category),
|
||||||
|
MonsterSize.medium => _mediumHitFrames(category),
|
||||||
|
MonsterSize.large => _largeHitFrames(category),
|
||||||
|
_ => _hugeHitFrames(category),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 경계 상태 몬스터 프레임 (prepare/attack 시)
|
||||||
|
List<List<String>> _getMonsterAlertFrames(MonsterCategory category, MonsterSize size) {
|
||||||
|
return switch (size) {
|
||||||
|
MonsterSize.tiny => _tinyAlertFrames(category),
|
||||||
|
MonsterSize.small => _smallAlertFrames(category),
|
||||||
|
MonsterSize.medium => _mediumAlertFrames(category),
|
||||||
|
MonsterSize.large => _largeAlertFrames(category),
|
||||||
|
_ => _hugeAlertFrames(category),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Tiny 몬스터 (2줄, 8줄 캔버스 하단 정렬)
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
List<List<String>> _tinyIdleFrames(MonsterCategory category) {
|
||||||
|
return switch (category) {
|
||||||
|
MonsterCategory.beast => [
|
||||||
|
[r'*', r'/\'],
|
||||||
|
[r'o', r'\/'],
|
||||||
|
],
|
||||||
|
MonsterCategory.insect => [
|
||||||
|
[r'><', r'\/'],
|
||||||
|
[r'<>', r'/\'],
|
||||||
|
],
|
||||||
|
MonsterCategory.humanoid => [
|
||||||
|
[r'o', r'|'],
|
||||||
|
[r'O', r'|'],
|
||||||
|
],
|
||||||
|
MonsterCategory.undead => [
|
||||||
|
[r'+', r'|'],
|
||||||
|
[r'x', r'|'],
|
||||||
|
],
|
||||||
|
MonsterCategory.dragon => [
|
||||||
|
[r'~<', r'>>'],
|
||||||
|
[r'<~', r'<<'],
|
||||||
|
],
|
||||||
|
MonsterCategory.slime => [
|
||||||
|
[r'()', r''],
|
||||||
|
[r'{}', r''],
|
||||||
|
],
|
||||||
|
MonsterCategory.demon => [
|
||||||
|
[r'^v', r'\/'],
|
||||||
|
[r'v^', r'/\'],
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
List<List<String>> _tinyHitFrames(MonsterCategory category) {
|
||||||
|
return [
|
||||||
|
[r'*!', r'><'],
|
||||||
|
[r'!*', r'<>'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
List<List<String>> _tinyAlertFrames(MonsterCategory category) {
|
||||||
|
return switch (category) {
|
||||||
|
MonsterCategory.beast => [
|
||||||
|
[r'!!', r'/\'],
|
||||||
|
[r'OO', r'><'],
|
||||||
|
],
|
||||||
|
MonsterCategory.insect => [
|
||||||
|
[r'!!', r'\/'],
|
||||||
|
[r'@@', r'/\'],
|
||||||
|
],
|
||||||
|
MonsterCategory.humanoid => [
|
||||||
|
[r'O!', r'|'],
|
||||||
|
[r'!O', r'X'],
|
||||||
|
],
|
||||||
|
MonsterCategory.undead => [
|
||||||
|
[r'!!', r'X'],
|
||||||
|
[r'@@', r'|'],
|
||||||
|
],
|
||||||
|
MonsterCategory.dragon => [
|
||||||
|
[r'!<', r'>>'],
|
||||||
|
[r'>!', r'<<'],
|
||||||
|
],
|
||||||
|
MonsterCategory.slime => [
|
||||||
|
[r'(!)', r''],
|
||||||
|
[r'{!}', r''],
|
||||||
|
],
|
||||||
|
MonsterCategory.demon => [
|
||||||
|
[r'^!', r'><'],
|
||||||
|
[r'!^', r'<>'],
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Small 몬스터 (4줄)
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
List<List<String>> _smallIdleFrames(MonsterCategory category) {
|
||||||
|
return switch (category) {
|
||||||
|
MonsterCategory.beast => [
|
||||||
|
[r' /\_/\', r'( o.o )', r' > ^ <', r' /| |\'],
|
||||||
|
[r' /\_/\', r'( o o )', r' > v <', r' \| |/'],
|
||||||
|
],
|
||||||
|
MonsterCategory.insect => [
|
||||||
|
[r' /\/\', r' (O O)', r' / \', r' \/ \/'],
|
||||||
|
[r' \/\/\', r' (O O)', r' \ /', r' /\ /\'],
|
||||||
|
],
|
||||||
|
MonsterCategory.humanoid => [
|
||||||
|
[r' O', r' /|\', r' / \', r' _| |_'],
|
||||||
|
[r' O', r' \|/', r' | |', r' _/ \_'],
|
||||||
|
],
|
||||||
|
MonsterCategory.undead => [
|
||||||
|
[r' _+_', r' (x_x)', r' /|\', r' _/ \_'],
|
||||||
|
[r' _+_', r' (X_X)', r' \|/', r' _| |_'],
|
||||||
|
],
|
||||||
|
MonsterCategory.dragon => [
|
||||||
|
[r' __', r' <(oo)~', r' / \', r' <_ _>'],
|
||||||
|
[r' __', r' (oo)>', r' \ /', r' <_ _>'],
|
||||||
|
],
|
||||||
|
MonsterCategory.slime => [
|
||||||
|
[r' ___', r' ( )', r' ( )', r' \_/'],
|
||||||
|
[r' _', r' / \', r' { }', r' \_/'],
|
||||||
|
],
|
||||||
|
MonsterCategory.demon => [
|
||||||
|
[r' ^w^', r' (|o|)', r' /|\', r' V V'],
|
||||||
|
[r' ^W^', r' (|O|)', r' \|/', r' v v'],
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
List<List<String>> _smallHitFrames(MonsterCategory category) {
|
||||||
|
return [
|
||||||
|
[r' *!*', r' (>_<)', r' \X/', r' _/_\_'],
|
||||||
|
[r' !*!', r' (@_@)', r' /X\', r' _\_/_'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
List<List<String>> _smallAlertFrames(MonsterCategory category) {
|
||||||
|
return switch (category) {
|
||||||
|
MonsterCategory.beast => [
|
||||||
|
[r' /\_/\', r'( O!O )', r' > ! <', r' /| |\'],
|
||||||
|
[r' /\_/\', r'( !O! )', r' > ! <', r' \| |/'],
|
||||||
|
],
|
||||||
|
MonsterCategory.insect => [
|
||||||
|
[r' /\/\', r' (! !)', r' / \', r' \/ \/'],
|
||||||
|
[r' \/\/\', r' (! !)', r' \ /', r' /\ /\'],
|
||||||
|
],
|
||||||
|
MonsterCategory.humanoid => [
|
||||||
|
[r' O!', r' /|\', r' / \', r' _| |_'],
|
||||||
|
[r' !O', r' \|/', r' | |', r' _/ \_'],
|
||||||
|
],
|
||||||
|
MonsterCategory.undead => [
|
||||||
|
[r' _!_', r' (!_!)', r' /|\', r' _/ \_'],
|
||||||
|
[r' _!_', r' (!_!)', r' \|/', r' _| |_'],
|
||||||
|
],
|
||||||
|
MonsterCategory.dragon => [
|
||||||
|
[r' __', r' <(!!)~', r' / \', r' <_ _>'],
|
||||||
|
[r' __', r' (!!)>', r' \ /', r' <_ _>'],
|
||||||
|
],
|
||||||
|
MonsterCategory.slime => [
|
||||||
|
[r' ___', r' ( ! )', r' ( ! )', r' \_/'],
|
||||||
|
[r' _', r' /!\', r' { ! }', r' \_/'],
|
||||||
|
],
|
||||||
|
MonsterCategory.demon => [
|
||||||
|
[r' ^!^', r' (|!|)', r' /|\', r' V V'],
|
||||||
|
[r' ^!^', r' (|!|)', r' \|/', r' v v'],
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Medium 몬스터 (6줄)
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
List<List<String>> _mediumIdleFrames(MonsterCategory category) {
|
||||||
|
return switch (category) {
|
||||||
|
MonsterCategory.beast => [
|
||||||
|
[r' /\_/\', r' ( O.O )', r' > ^ <', r' /| |\', r' | | | |', r'_|_| |_|_'],
|
||||||
|
[r' /\_/\', r' ( O O )', r' > v <', r' \| |/', r' | | | |', r'_|_| |_|_'],
|
||||||
|
],
|
||||||
|
MonsterCategory.insect => [
|
||||||
|
[r' /\/\', r' /O O\', r' \ /', r' / \', r' \/ \/', r' _/ \_'],
|
||||||
|
[r' \/\/\', r' \O O/', r' / \', r' \ /', r' /\ /\', r' _\ /_'],
|
||||||
|
],
|
||||||
|
MonsterCategory.humanoid => [
|
||||||
|
[r' O', r' /|\', r' / \', r' | |', r' | |', r' _| |_'],
|
||||||
|
[r' O', r' \|/', r' | |', r' | |', r' | |', r' _/ \_'],
|
||||||
|
],
|
||||||
|
MonsterCategory.undead => [
|
||||||
|
[r' _+_', r' (X_X)', r' /|\', r' / | \', r' | | |', r'_/ | \_'],
|
||||||
|
[r' _x_', r' (x_x)', r' \|/', r' \ | /', r' | | |', r'_\ | /_'],
|
||||||
|
],
|
||||||
|
MonsterCategory.dragon => [
|
||||||
|
[r' __', r' <(OO)~', r' / \', r' / \', r' | |', r'<__ __>'],
|
||||||
|
[r' __', r' (OO)>', r' \ /', r' \ /', r' | |', r'<__ __>'],
|
||||||
|
],
|
||||||
|
MonsterCategory.slime => [
|
||||||
|
[r' ____', r' / \', r' ( )', r' ( )', r' \ /', r' \__/'],
|
||||||
|
[r' __', r' / \', r' / \', r' { }', r' \ /', r' \__/'],
|
||||||
|
],
|
||||||
|
MonsterCategory.demon => [
|
||||||
|
[r' ^W^', r' (|O|)', r' /|\', r' / | \', r' V V', r' _/ \_'],
|
||||||
|
[r' ^w^', r' (|o|)', r' \|/', r' \ | /', r' v v', r' _\ /_'],
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
List<List<String>> _mediumHitFrames(MonsterCategory category) {
|
||||||
|
return [
|
||||||
|
[r' *!*', r' (>.<)', r' \X/', r' / \', r' | |', r'_/_ \_\'],
|
||||||
|
[r' !*!', r' (@_@)', r' /X\', r' \ /', r' | |', r'_\_ /_/'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
List<List<String>> _mediumAlertFrames(MonsterCategory category) {
|
||||||
|
return switch (category) {
|
||||||
|
MonsterCategory.beast => [
|
||||||
|
[r' /\_/\', r' ( O!O )', r' > ! <', r' /| |\', r' | | | |', r'_|_| |_|_'],
|
||||||
|
[r' /\_/\', r' ( !O! )', r' > ! <', r' \| |/', r' | | | |', r'_|_| |_|_'],
|
||||||
|
],
|
||||||
|
MonsterCategory.insect => [
|
||||||
|
[r' /\/\', r' /! !\', r' \ /', r' / \', r' \/ \/', r' _/ \_'],
|
||||||
|
[r' \/\/\', r' \! !/', r' / \', r' \ /', r' /\ /\', r' _\ /_'],
|
||||||
|
],
|
||||||
|
MonsterCategory.humanoid => [
|
||||||
|
[r' O!', r' /|\', r' / \', r' | |', r' | |', r' _| |_'],
|
||||||
|
[r' !O', r' \|/', r' | |', r' | |', r' | |', r' _/ \_'],
|
||||||
|
],
|
||||||
|
MonsterCategory.undead => [
|
||||||
|
[r' _!_', r' (!_!)', r' /|\', r' / | \', r' | | |', r'_/ | \_'],
|
||||||
|
[r' _!_', r' (!_!)', r' \|/', r' \ | /', r' | | |', r'_\ | /_'],
|
||||||
|
],
|
||||||
|
MonsterCategory.dragon => [
|
||||||
|
[r' __', r' <(!!)~', r' / \', r' / \', r' | |', r'<__ __>'],
|
||||||
|
[r' __', r' (!!)>', r' \ /', r' \ /', r' | |', r'<__ __>'],
|
||||||
|
],
|
||||||
|
MonsterCategory.slime => [
|
||||||
|
[r' ____', r' / ! \', r' ( ! )', r' ( ! )', r' \ /', r' \__/'],
|
||||||
|
[r' __', r' / !\', r' / ! \', r' { ! }', r' \ /', r' \__/'],
|
||||||
|
],
|
||||||
|
MonsterCategory.demon => [
|
||||||
|
[r' ^!^', r' (|!|)', r' /|\', r' / | \', r' V V', r' _/ \_'],
|
||||||
|
[r' ^!^', r' (|!|)', r' \|/', r' \ | /', r' v v', r' _\ /_'],
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Large 몬스터 (8줄)
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
List<List<String>> _largeIdleFrames(MonsterCategory category) {
|
||||||
|
return switch (category) {
|
||||||
|
MonsterCategory.beast => [
|
||||||
|
[r' /\__/\', r' ( O O )', r' > ^^ <', r' /| |\', r' | | | |', r' | | | |', r'_| | | |_', r'|__|____|__|'],
|
||||||
|
[r' /\__/\', r' ( O O )', r' > vv <', r' \| |/', r' | | | |', r' | | | |', r'_| | | |_', r'|__|____|__|'],
|
||||||
|
],
|
||||||
|
MonsterCategory.insect => [
|
||||||
|
[r' /\/\/\', r' /O O\', r' \ /', r' / \', r' \/ \/', r' | \ / |', r' _| \/ |_', r'|___________|'],
|
||||||
|
[r' \/\/\/\', r' \O O/', r' / \', r' \ /', r' /\ /\', r' | / \ |', r' _| /\ |_', r'|___________|'],
|
||||||
|
],
|
||||||
|
MonsterCategory.humanoid => [
|
||||||
|
[r' O', r' /|\', r' / \', r' | |', r' | |', r' | |', r' _| |_', r'|_________|'],
|
||||||
|
[r' O', r' \|/', r' | |', r' | |', r' | |', r' | |', r' _/ \_', r'|_________|'],
|
||||||
|
],
|
||||||
|
MonsterCategory.undead => [
|
||||||
|
[r' _/+\_', r' (X___X)', r' /|||\', r' / ||| \', r' | ||| |', r' | / \ |', r' _|/ \|_', r'|_/ \_|'],
|
||||||
|
[r' _\+/_', r' (x___x)', r' \|||/', r' \ ||| /', r' | ||| |', r' | \ / |', r' _|\ /|_', r'|_\ /_|'],
|
||||||
|
],
|
||||||
|
MonsterCategory.dragon => [
|
||||||
|
[r' ___', r' <<(O O)~~', r' / || \', r' / || \', r' | || |', r' | || |', r' _| || |_', r'<___________|>'],
|
||||||
|
[r' ___', r' (O O)>>', r' \ || /', r' \ || /', r' | || |', r' | || |', r' _| || |_', r'<___________|>'],
|
||||||
|
],
|
||||||
|
MonsterCategory.slime => [
|
||||||
|
[r' _____', r' / \', r' / \', r' ( )', r' ( )', r' \ /', r' \_____/', r' \___/'],
|
||||||
|
[r' ___', r' / \', r' / \', r' { }', r' { }', r' \ /', r' \___/', r' \_/'],
|
||||||
|
],
|
||||||
|
MonsterCategory.demon => [
|
||||||
|
[r' ^W^', r' /|O|\', r' /|\', r' / | \', r' | | |', r' V | V', r' _/ | \_', r'|____|____|'],
|
||||||
|
[r' ^w^', r' \|o|/', r' \|/', r' \ | /', r' | | |', r' v | v', r' _\ | /_', r'|____|____|'],
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
List<List<String>> _largeHitFrames(MonsterCategory category) {
|
||||||
|
return [
|
||||||
|
[r' *!*!*', r' (>___<)', r' \\X//', r' / \\// \', r' | \\/ |', r' | / \ |', r' _|/ \|_', r'|___/\\___|'],
|
||||||
|
[r' !*!*!', r' (@___@)', r' //X\\', r' \ /\\/ /', r' | //\\ |', r' | \ / |', r' _|\ /|_', r'|___\\/__|'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
List<List<String>> _largeAlertFrames(MonsterCategory category) {
|
||||||
|
return switch (category) {
|
||||||
|
MonsterCategory.beast => [
|
||||||
|
[r' /\__/\', r' ( O!!O )', r' > !! <', r' /| |\', r' | | | |', r' | | | |', r'_| | | |_', r'|__|____|__|'],
|
||||||
|
[r' /\__/\', r' ( !!O! )', r' > !! <', r' \| |/', r' | | | |', r' | | | |', r'_| | | |_', r'|__|____|__|'],
|
||||||
|
],
|
||||||
|
MonsterCategory.insect => [
|
||||||
|
[r' /\/\/\', r' /! !\', r' \ /', r' / \', r' \/ \/', r' | \ / |', r' _| \/ |_', r'|___________|'],
|
||||||
|
[r' \/\/\/\', r' \! !/', r' / \', r' \ /', r' /\ /\', r' | / \ |', r' _| /\ |_', r'|___________|'],
|
||||||
|
],
|
||||||
|
MonsterCategory.humanoid => [
|
||||||
|
[r' O!', r' /|\', r' / \', r' | |', r' | |', r' | |', r' _| |_', r'|_________|'],
|
||||||
|
[r' !O', r' \|/', r' | |', r' | |', r' | |', r' | |', r' _/ \_', r'|_________|'],
|
||||||
|
],
|
||||||
|
MonsterCategory.undead => [
|
||||||
|
[r' _/!\_', r' (!___!)', r' /|||\', r' / ||| \', r' | ||| |', r' | / \ |', r' _|/ \|_', r'|_/ \_|'],
|
||||||
|
[r' _\!/_', r' (!___!)', r' \|||/', r' \ ||| /', r' | ||| |', r' | \ / |', r' _|\ /|_', r'|_\ /_|'],
|
||||||
|
],
|
||||||
|
MonsterCategory.dragon => [
|
||||||
|
[r' ___', r' <<(! !)~~', r' / || \', r' / || \', r' | || |', r' | || |', r' _| || |_', r'<___________|>'],
|
||||||
|
[r' ___', r' (! !)>>', r' \ || /', r' \ || /', r' | || |', r' | || |', r' _| || |_', r'<___________|>'],
|
||||||
|
],
|
||||||
|
MonsterCategory.slime => [
|
||||||
|
[r' _____', r' / ! \', r' / ! \', r' ( ! )', r' ( ! )', r' \ /', r' \_____/', r' \___/'],
|
||||||
|
[r' ___', r' / ! \', r' / ! \', r' { ! }', r' { ! }', r' \ /', r' \___/', r' \_/'],
|
||||||
|
],
|
||||||
|
MonsterCategory.demon => [
|
||||||
|
[r' ^!^', r' /|!|\', r' /|\', r' / | \', r' | | |', r' V | V', r' _/ | \_', r'|____|____|'],
|
||||||
|
[r' ^!^', r' \|!|/', r' \|/', r' \ | /', r' | | |', r' v | v', r' _\ | /_', r'|____|____|'],
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Huge+ 몬스터 (8줄, 더 넓게)
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
List<List<String>> _hugeIdleFrames(MonsterCategory category) {
|
||||||
|
return switch (category) {
|
||||||
|
MonsterCategory.beast => [
|
||||||
|
[r' /\____/\', r' ( O O )', r' > ^^^^ <', r' /| |\', r' | | | |', r' | | | |', r' _| | | |_', r'|___|______|___|'],
|
||||||
|
[r' /\____/\', r' ( O O )', r' > vvvv <', r' \| |/', r' | | | |', r' | | | |', r' _| | | |_', r'|___|______|___|'],
|
||||||
|
],
|
||||||
|
MonsterCategory.insect => [
|
||||||
|
[r' /\/\/\/\', r' /O O\', r' \ /', r' / \', r' \/ \/', r' | \ / |', r' _| \ / |_', r'|_______________|'],
|
||||||
|
[r' \/\/\/\/\', r' \O O/', r' / \', r' \ /', r' /\ /\', r' | / \ |', r' _| / \ |_', r'|_______________|'],
|
||||||
|
],
|
||||||
|
MonsterCategory.humanoid => [
|
||||||
|
[r' O', r' _/|\\_', r' / | \\', r' | |', r' | |', r' | |', r' _| |_', r'|___________|'],
|
||||||
|
[r' O', r' \\_|_/', r' \\|/', r' | |', r' | |', r' | |', r' _/ \\_', r'|___________|'],
|
||||||
|
],
|
||||||
|
MonsterCategory.undead => [
|
||||||
|
[r' _/+\\_', r' (X_____X)', r' /|||||\', r' / ||||| \\', r' | ||||| |', r' | / \\ |', r' _|/ \\|_', r'|_/ \\_|'],
|
||||||
|
[r' _\\+/_', r' (x_____x)', r' \\|||||/', r' \\ ||||| /', r' | ||||| |', r' | \\ / |', r' _|\\ /|_', r'|_\\ /_|'],
|
||||||
|
],
|
||||||
|
MonsterCategory.dragon => [
|
||||||
|
[r' ____', r' <<<(O O)~~~', r' / |||| \\', r' / |||| \\', r' | |||| |', r' | |||| |', r' _| |||| |_', r'<_______________|>'],
|
||||||
|
[r' ____', r' (O O)>>>', r' \\ |||| /', r' \\ |||| /', r' | |||| |', r' | |||| |', r' _| |||| |_', r'<_______________|>'],
|
||||||
|
],
|
||||||
|
MonsterCategory.slime => [
|
||||||
|
[r' ______', r' / \\', r' / \\', r' ( )', r' ( )', r' \\ /', r' \\______/', r' \\____/'],
|
||||||
|
[r' ____', r' / \\', r' / \\', r' { }', r' { }', r' \\ /', r' \\____/', r' \\__/'],
|
||||||
|
],
|
||||||
|
MonsterCategory.demon => [
|
||||||
|
[r' ^W^', r' /|O|\\ ', r' /|\\', r' / | \\', r' | | |', r' V | V', r' _/ | \\_', r'|_____|_____|'],
|
||||||
|
[r' ^w^', r' \\|o|/', r' \\|/', r' \\ | /', r' | | |', r' v | v', r' _\\ | /_', r'|_____|_____|'],
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
List<List<String>> _hugeHitFrames(MonsterCategory category) {
|
||||||
|
return [
|
||||||
|
[r' *!*!*!*', r' (>_____<)', r' \\\\X////', r' / \\\\// \\', r' | \\\\/ |', r' | / \\ |', r' _|/ \\|_', r'|____/\\\\___|'],
|
||||||
|
[r' !*!*!*!', r' (@_____@)', r' ////X\\\\', r' \\ /\\\\/ /', r' | ////\\\\ |', r' | \\ / |', r' _|\\ /|_', r'|____\\\\/___|'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
List<List<String>> _hugeAlertFrames(MonsterCategory category) {
|
||||||
|
return switch (category) {
|
||||||
|
MonsterCategory.beast => [
|
||||||
|
[r' /\____/\', r' ( ! ! )', r' > !!!! <', r' /| |\', r' | | | |', r' | | | |', r' _| | | |_', r'|___|______|___|'],
|
||||||
|
[r' /\____/\', r' ( ! ! )', r' > !!!! <', r' \| |/', r' | | | |', r' | | | |', r' _| | | |_', r'|___|______|___|'],
|
||||||
|
],
|
||||||
|
MonsterCategory.insect => [
|
||||||
|
[r' /\/\/\/\', r' /! !\', r' \ /', r' / \', r' \/ \/', r' | \ / |', r' _| \ / |_', r'|_______________|'],
|
||||||
|
[r' \/\/\/\/\', r' \! !/', r' / \', r' \ /', r' /\ /\', r' | / \ |', r' _| / \ |_', r'|_______________|'],
|
||||||
|
],
|
||||||
|
MonsterCategory.humanoid => [
|
||||||
|
[r' O!', r' _/|\\__', r' / | \\', r' | |', r' | |', r' | |', r' _| |_', r'|___________|'],
|
||||||
|
[r' !O', r' \\_|_/', r' \\|/', r' | |', r' | |', r' | |', r' _/ \\_', r'|___________|'],
|
||||||
|
],
|
||||||
|
MonsterCategory.undead => [
|
||||||
|
[r' _/!\\__', r' (!_____!)', r' /|||||\', r' / ||||| \\', r' | ||||| |', r' | / \\ |', r' _|/ \\|_', r'|_/ \\_|'],
|
||||||
|
[r' _\\!/_', r' (!_____!)', r' \\|||||/', r' \\ ||||| /', r' | ||||| |', r' | \\ / |', r' _|\\ /|_', r'|_\\ /_|'],
|
||||||
|
],
|
||||||
|
MonsterCategory.dragon => [
|
||||||
|
[r' ____', r' <<<(! !)~~~', r' / |||| \\', r' / |||| \\', r' | |||| |', r' | |||| |', r' _| |||| |_', r'<_______________|>'],
|
||||||
|
[r' ____', r' (! !)>>>', r' \\ |||| /', r' \\ |||| /', r' | |||| |', r' | |||| |', r' _| |||| |_', r'<_______________|>'],
|
||||||
|
],
|
||||||
|
MonsterCategory.slime => [
|
||||||
|
[r' ______', r' / ! \\', r' / ! \\', r' ( ! )', r' ( ! )', r' \\ /', r' \\______/', r' \\____/'],
|
||||||
|
[r' ____', r' / ! \\', r' / ! \\', r' { ! }', r' { ! }', r' \\ /', r' \\____/', r' \\__/'],
|
||||||
|
],
|
||||||
|
MonsterCategory.demon => [
|
||||||
|
[r' ^!^', r' /|!|\\ ', r' /|\\', r' / | \\', r' | | |', r' V | V', r' _/ | \\_', r'|_____|_____|'],
|
||||||
|
[r' ^!^', r' \\|!|/', r' \\|/', r' \\ | /', r' | | |', r' v | v', r' _\\ | /_', r'|_____|_____|'],
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 레거시 호환용 함수
|
||||||
|
List<List<String>> getMonsterFrames(MonsterCategory category, MonsterSize size) {
|
||||||
|
return _getMonsterIdleFrames(category, size);
|
||||||
|
}
|
||||||
178
lib/src/core/animation/character_frames.dart
Normal file
178
lib/src/core/animation/character_frames.dart
Normal file
@@ -0,0 +1,178 @@
|
|||||||
|
// 캐릭터 애니메이션 프레임 (8줄 Stone Story RPG 스타일)
|
||||||
|
// 참조: Stone Story RPG - 상세하고 생동감 있는 ASCII 아트
|
||||||
|
|
||||||
|
/// 전투 페이즈
|
||||||
|
enum BattlePhase {
|
||||||
|
/// 대치 상태 (기본)
|
||||||
|
idle,
|
||||||
|
|
||||||
|
/// 공격 준비
|
||||||
|
prepare,
|
||||||
|
|
||||||
|
/// 공격 중
|
||||||
|
attack,
|
||||||
|
|
||||||
|
/// 피격 (몬스터가 맞음)
|
||||||
|
hit,
|
||||||
|
|
||||||
|
/// 복귀
|
||||||
|
recover,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 캐릭터 프레임 데이터
|
||||||
|
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' _ ',
|
||||||
|
r' \o ',
|
||||||
|
r' / \ ',
|
||||||
|
]),
|
||||||
|
CharacterFrame([
|
||||||
|
r' \_ ',
|
||||||
|
r' \o/ ',
|
||||||
|
r' / \ ',
|
||||||
|
]),
|
||||||
|
];
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// 공격 프레임 (전진 + 휘두르기) - 5프레임, 심플 3줄 스타일
|
||||||
|
// ============================================================================
|
||||||
|
const _attackFrames = [
|
||||||
|
CharacterFrame([
|
||||||
|
r' \_/ ',
|
||||||
|
r' o ',
|
||||||
|
r' /| ',
|
||||||
|
]),
|
||||||
|
CharacterFrame([
|
||||||
|
r' _/ ',
|
||||||
|
r' o ',
|
||||||
|
r' /|\ ',
|
||||||
|
]),
|
||||||
|
CharacterFrame([
|
||||||
|
r' o-- ',
|
||||||
|
r' /| ',
|
||||||
|
r' / \ ',
|
||||||
|
]),
|
||||||
|
CharacterFrame([
|
||||||
|
r' o ',
|
||||||
|
r' /|-- ',
|
||||||
|
r' / \ ',
|
||||||
|
]),
|
||||||
|
CharacterFrame([
|
||||||
|
r' o ',
|
||||||
|
r' /|\_ ',
|
||||||
|
r' / \ ',
|
||||||
|
]),
|
||||||
|
];
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// 히트 프레임 (공격 명중) - 3프레임, 심플 3줄 스타일
|
||||||
|
// ============================================================================
|
||||||
|
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' / \ ',
|
||||||
|
]),
|
||||||
|
];
|
||||||
192
lib/src/core/animation/monster_colors.dart
Normal file
192
lib/src/core/animation/monster_colors.dart
Normal file
@@ -0,0 +1,192 @@
|
|||||||
|
// 몬스터 카테고리별 색상 시스템
|
||||||
|
// 각 몬스터 카테고리에 따라 다른 색상 적용
|
||||||
|
|
||||||
|
import 'dart:ui';
|
||||||
|
|
||||||
|
/// 몬스터 카테고리 (ascii_animation_data.dart의 MonsterCategory와 매칭)
|
||||||
|
enum MonsterColorCategory {
|
||||||
|
beast,
|
||||||
|
insect,
|
||||||
|
humanoid,
|
||||||
|
undead,
|
||||||
|
dragon,
|
||||||
|
slime,
|
||||||
|
demon,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 몬스터 색상 정보
|
||||||
|
class MonsterColors {
|
||||||
|
const MonsterColors({
|
||||||
|
required this.normal,
|
||||||
|
required this.hit,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// 일반 상태 색상
|
||||||
|
final Color normal;
|
||||||
|
|
||||||
|
/// 피격 상태 색상
|
||||||
|
final Color hit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 카테고리별 몬스터 색상 반환
|
||||||
|
MonsterColors getMonsterColors(MonsterColorCategory category) {
|
||||||
|
return switch (category) {
|
||||||
|
MonsterColorCategory.beast => const MonsterColors(
|
||||||
|
normal: Color(0xFF00FF00), // 녹색
|
||||||
|
hit: Color(0xFFFF0000), // 빨강
|
||||||
|
),
|
||||||
|
MonsterColorCategory.insect => const MonsterColors(
|
||||||
|
normal: Color(0xFFFFFF00), // 노랑
|
||||||
|
hit: Color(0xFFFF6600), // 주황
|
||||||
|
),
|
||||||
|
MonsterColorCategory.humanoid => const MonsterColors(
|
||||||
|
normal: Color(0xFF00FFFF), // 시안
|
||||||
|
hit: Color(0xFFFF00FF), // 마젠타
|
||||||
|
),
|
||||||
|
MonsterColorCategory.undead => const MonsterColors(
|
||||||
|
normal: Color(0xFF9966FF), // 보라
|
||||||
|
hit: Color(0xFFCCCCCC), // 회색
|
||||||
|
),
|
||||||
|
MonsterColorCategory.dragon => const MonsterColors(
|
||||||
|
normal: Color(0xFFFF6600), // 주황
|
||||||
|
hit: Color(0xFFFFFF00), // 노랑
|
||||||
|
),
|
||||||
|
MonsterColorCategory.slime => const MonsterColors(
|
||||||
|
normal: Color(0xFF66FF66), // 연녹색
|
||||||
|
hit: Color(0xFF00CC00), // 진녹색
|
||||||
|
),
|
||||||
|
MonsterColorCategory.demon => const MonsterColors(
|
||||||
|
normal: Color(0xFFFF0066), // 핑크
|
||||||
|
hit: Color(0xFFFFFFFF), // 흰색
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 몬스터 기본 이름에서 색상 카테고리 추론
|
||||||
|
///
|
||||||
|
/// ascii_animation_data.dart의 getMonsterCategory 결과를 변환
|
||||||
|
MonsterColorCategory getMonsterColorCategory(String? baseName) {
|
||||||
|
if (baseName == null || baseName.isEmpty) {
|
||||||
|
return MonsterColorCategory.beast;
|
||||||
|
}
|
||||||
|
|
||||||
|
final lower = baseName.toLowerCase();
|
||||||
|
|
||||||
|
// insect (곤충류)
|
||||||
|
if (_matchesAny(lower, _insectKeywords)) {
|
||||||
|
return MonsterColorCategory.insect;
|
||||||
|
}
|
||||||
|
|
||||||
|
// undead (언데드)
|
||||||
|
if (_matchesAny(lower, _undeadKeywords)) {
|
||||||
|
return MonsterColorCategory.undead;
|
||||||
|
}
|
||||||
|
|
||||||
|
// dragon (드래곤류)
|
||||||
|
if (_matchesAny(lower, _dragonKeywords)) {
|
||||||
|
return MonsterColorCategory.dragon;
|
||||||
|
}
|
||||||
|
|
||||||
|
// slime (슬라임류)
|
||||||
|
if (_matchesAny(lower, _slimeKeywords)) {
|
||||||
|
return MonsterColorCategory.slime;
|
||||||
|
}
|
||||||
|
|
||||||
|
// demon (악마류)
|
||||||
|
if (_matchesAny(lower, _demonKeywords)) {
|
||||||
|
return MonsterColorCategory.demon;
|
||||||
|
}
|
||||||
|
|
||||||
|
// humanoid (인간형)
|
||||||
|
if (_matchesAny(lower, _humanoidKeywords)) {
|
||||||
|
return MonsterColorCategory.humanoid;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 기본은 beast
|
||||||
|
return MonsterColorCategory.beast;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool _matchesAny(String text, List<String> keywords) {
|
||||||
|
return keywords.any((kw) => text.contains(kw));
|
||||||
|
}
|
||||||
|
|
||||||
|
const _insectKeywords = [
|
||||||
|
'bug',
|
||||||
|
'beetle',
|
||||||
|
'spider',
|
||||||
|
'ant',
|
||||||
|
'bee',
|
||||||
|
'wasp',
|
||||||
|
'moth',
|
||||||
|
'worm',
|
||||||
|
'larva',
|
||||||
|
'crawler',
|
||||||
|
'centipede',
|
||||||
|
'scorpion',
|
||||||
|
];
|
||||||
|
|
||||||
|
const _undeadKeywords = [
|
||||||
|
'zombie',
|
||||||
|
'skeleton',
|
||||||
|
'ghost',
|
||||||
|
'wraith',
|
||||||
|
'vampire',
|
||||||
|
'lich',
|
||||||
|
'specter',
|
||||||
|
'phantom',
|
||||||
|
'revenant',
|
||||||
|
'undead',
|
||||||
|
'corpse',
|
||||||
|
'bone',
|
||||||
|
];
|
||||||
|
|
||||||
|
const _dragonKeywords = [
|
||||||
|
'dragon',
|
||||||
|
'drake',
|
||||||
|
'wyrm',
|
||||||
|
'wyvern',
|
||||||
|
'serpent',
|
||||||
|
'hydra',
|
||||||
|
'basilisk',
|
||||||
|
];
|
||||||
|
|
||||||
|
const _slimeKeywords = [
|
||||||
|
'slime',
|
||||||
|
'ooze',
|
||||||
|
'blob',
|
||||||
|
'jelly',
|
||||||
|
'pudding',
|
||||||
|
'gel',
|
||||||
|
'goo',
|
||||||
|
];
|
||||||
|
|
||||||
|
const _demonKeywords = [
|
||||||
|
'demon',
|
||||||
|
'devil',
|
||||||
|
'imp',
|
||||||
|
'fiend',
|
||||||
|
'daemon',
|
||||||
|
'succubus',
|
||||||
|
'incubus',
|
||||||
|
'hell',
|
||||||
|
'infernal',
|
||||||
|
];
|
||||||
|
|
||||||
|
const _humanoidKeywords = [
|
||||||
|
'goblin',
|
||||||
|
'orc',
|
||||||
|
'troll',
|
||||||
|
'ogre',
|
||||||
|
'giant',
|
||||||
|
'bandit',
|
||||||
|
'knight',
|
||||||
|
'mage',
|
||||||
|
'wizard',
|
||||||
|
'warrior',
|
||||||
|
'guard',
|
||||||
|
'soldier',
|
||||||
|
'cultist',
|
||||||
|
'hacker',
|
||||||
|
'admin',
|
||||||
|
'user',
|
||||||
|
];
|
||||||
49
lib/src/core/animation/monster_size.dart
Normal file
49
lib/src/core/animation/monster_size.dart
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
// 몬스터 크기 시스템
|
||||||
|
// 몬스터 레벨에 따라 ASCII 아트 크기 결정
|
||||||
|
|
||||||
|
/// 몬스터 크기 enum
|
||||||
|
enum MonsterSize {
|
||||||
|
/// 1줄 (레벨 1-5)
|
||||||
|
tiny(1),
|
||||||
|
|
||||||
|
/// 2줄 (레벨 6-10)
|
||||||
|
small(2),
|
||||||
|
|
||||||
|
/// 3줄 (레벨 11-15)
|
||||||
|
medium(3),
|
||||||
|
|
||||||
|
/// 4줄 (레벨 16-25)
|
||||||
|
large(4),
|
||||||
|
|
||||||
|
/// 5줄 (레벨 26-35)
|
||||||
|
huge(5),
|
||||||
|
|
||||||
|
/// 6줄 (레벨 36-50)
|
||||||
|
giant(6),
|
||||||
|
|
||||||
|
/// 7줄 (레벨 51+, 보스급)
|
||||||
|
titanic(7);
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
109
lib/src/core/animation/weapon_category.dart
Normal file
109
lib/src/core/animation/weapon_category.dart
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
/// 무기 카테고리 (공격 스타일 결정용)
|
||||||
|
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',
|
||||||
|
];
|
||||||
184
lib/src/core/animation/weapon_effects.dart
Normal file
184
lib/src/core/animation/weapon_effects.dart
Normal file
@@ -0,0 +1,184 @@
|
|||||||
|
import 'package:askiineverdie/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,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// 둔기류 - 휘두르기 (3줄)
|
||||||
|
// ============================================================================
|
||||||
|
const _bluntEffect = WeaponEffect(
|
||||||
|
prepareFrames: [
|
||||||
|
[r' _', r' /', r' /'],
|
||||||
|
[r' _/', r' / ', r' / '],
|
||||||
|
],
|
||||||
|
attackFrames: [
|
||||||
|
[r' _/ ', r' / ', r'/ '],
|
||||||
|
[r' /__ ', r'/ ', r' '],
|
||||||
|
[r'/__ ', r' ', r' '],
|
||||||
|
[r'/__=>', r' ', r' '],
|
||||||
|
],
|
||||||
|
hitFrames: [
|
||||||
|
[r' *BASH* ', r'/__=> ', r' '],
|
||||||
|
[r'*SMASH!*', r' /__ ', r' '],
|
||||||
|
],
|
||||||
|
hitSound: '*BASH*',
|
||||||
|
effectHeight: 3,
|
||||||
|
effectYStart: 2,
|
||||||
|
);
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// 케이블류 - 채찍질 (3줄)
|
||||||
|
// ============================================================================
|
||||||
|
const _cableEffect = WeaponEffect(
|
||||||
|
prepareFrames: [
|
||||||
|
[r' ', r'~ ', r' ~ '],
|
||||||
|
[r' ', r'~~ ', r' ~ '],
|
||||||
|
],
|
||||||
|
attackFrames: [
|
||||||
|
[r' ', r'~~~ ', r' ~~ '],
|
||||||
|
[r' ', r'~~~~ ', r' ~~ '],
|
||||||
|
[r' ', r'~~~~~> ', r' ~~ '],
|
||||||
|
[r' ', r'~~~~~~> ', r' ~~'],
|
||||||
|
],
|
||||||
|
hitFrames: [
|
||||||
|
[r' *WHIP*', r'~~~~~~> ', r' ~~'],
|
||||||
|
[r' *CRACK*', r'~~~~~> ', r' ~~ '],
|
||||||
|
],
|
||||||
|
hitSound: '*WHIP*',
|
||||||
|
effectHeight: 3,
|
||||||
|
effectYStart: 2,
|
||||||
|
);
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// 투척류 - 발사 (3줄)
|
||||||
|
// ============================================================================
|
||||||
|
const _projectileEffect = WeaponEffect(
|
||||||
|
prepareFrames: [
|
||||||
|
[r' ', r'[=] ', r' '],
|
||||||
|
[r' ', r'[==] ', r' '],
|
||||||
|
],
|
||||||
|
attackFrames: [
|
||||||
|
[r' ', r' [> ', r' '],
|
||||||
|
[r' ', r' [>', r' '],
|
||||||
|
[r' ', r' [>', r' '],
|
||||||
|
[r' ', r' [>', r' '],
|
||||||
|
],
|
||||||
|
hitFrames: [
|
||||||
|
[r' *CLANG*', r' [>', r' '],
|
||||||
|
[r' *CRASH* ', r' [> ', r' '],
|
||||||
|
],
|
||||||
|
hitSound: '*CLANG*',
|
||||||
|
effectHeight: 3,
|
||||||
|
effectYStart: 2,
|
||||||
|
);
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// 에너지류 - 빔 발사 (3줄)
|
||||||
|
// ============================================================================
|
||||||
|
const _energyEffect = WeaponEffect(
|
||||||
|
prepareFrames: [
|
||||||
|
[r' ', r' <*> ', r' '],
|
||||||
|
[r' == ', r' <**> ', r' == '],
|
||||||
|
],
|
||||||
|
attackFrames: [
|
||||||
|
[r' ==== ', r'==<*>== ', r' ==== '],
|
||||||
|
[r' ====== ', r'===<*>==', r' ====== '],
|
||||||
|
[r'========', r'===<*>==', r'========'],
|
||||||
|
[r'========', r'====<*>=', r'========'],
|
||||||
|
],
|
||||||
|
hitFrames: [
|
||||||
|
[r'==*ZAP*=', r'===<*>==', r'========'],
|
||||||
|
[r'*BZZT!*=', r'====<*>=', r'========'],
|
||||||
|
],
|
||||||
|
hitSound: '*ZAP*',
|
||||||
|
effectHeight: 3,
|
||||||
|
effectYStart: 2,
|
||||||
|
);
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// 우주급 - 초월적 공격 (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,
|
||||||
|
);
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// 맨손 - 기본 펀치 (3줄)
|
||||||
|
// ============================================================================
|
||||||
|
const _unarmedEffect = WeaponEffect(
|
||||||
|
prepareFrames: [
|
||||||
|
[r' ', r' ', r' '],
|
||||||
|
[r' ', r' > ', r' '],
|
||||||
|
],
|
||||||
|
attackFrames: [
|
||||||
|
[r' ', r'-> ', r' '],
|
||||||
|
[r' ', r'---> ', r' '],
|
||||||
|
[r' ', r'-----> ', r' '],
|
||||||
|
],
|
||||||
|
hitFrames: [
|
||||||
|
[r' *POW* ', r'-----> ', r' '],
|
||||||
|
[r'*PUNCH*', r'----> ', r' '],
|
||||||
|
],
|
||||||
|
hitSound: '*POW*',
|
||||||
|
effectHeight: 3,
|
||||||
|
effectYStart: 2,
|
||||||
|
);
|
||||||
@@ -383,6 +383,7 @@ class ProgressService {
|
|||||||
type: TaskType.kill,
|
type: TaskType.kill,
|
||||||
monsterBaseName: monsterResult.baseName,
|
monsterBaseName: monsterResult.baseName,
|
||||||
monsterPart: monsterResult.part,
|
monsterPart: monsterResult.part,
|
||||||
|
monsterLevel: monsterResult.level,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -95,6 +95,7 @@ class TaskInfo {
|
|||||||
required this.type,
|
required this.type,
|
||||||
this.monsterBaseName,
|
this.monsterBaseName,
|
||||||
this.monsterPart,
|
this.monsterPart,
|
||||||
|
this.monsterLevel,
|
||||||
});
|
});
|
||||||
|
|
||||||
final String caption;
|
final String caption;
|
||||||
@@ -106,6 +107,9 @@ class TaskInfo {
|
|||||||
/// 킬 태스크의 전리품 부위 (예: "claw", "tail", "*"는 WinItem)
|
/// 킬 태스크의 전리품 부위 (예: "claw", "tail", "*"는 WinItem)
|
||||||
final String? monsterPart;
|
final String? monsterPart;
|
||||||
|
|
||||||
|
/// 킬 태스크의 몬스터 레벨 (애니메이션 크기 결정용)
|
||||||
|
final int? monsterLevel;
|
||||||
|
|
||||||
factory TaskInfo.empty() =>
|
factory TaskInfo.empty() =>
|
||||||
const TaskInfo(caption: '', type: TaskType.neutral);
|
const TaskInfo(caption: '', type: TaskType.neutral);
|
||||||
|
|
||||||
@@ -114,12 +118,14 @@ class TaskInfo {
|
|||||||
TaskType? type,
|
TaskType? type,
|
||||||
String? monsterBaseName,
|
String? monsterBaseName,
|
||||||
String? monsterPart,
|
String? monsterPart,
|
||||||
|
int? monsterLevel,
|
||||||
}) {
|
}) {
|
||||||
return TaskInfo(
|
return TaskInfo(
|
||||||
caption: caption ?? this.caption,
|
caption: caption ?? this.caption,
|
||||||
type: type ?? this.type,
|
type: type ?? this.type,
|
||||||
monsterBaseName: monsterBaseName ?? this.monsterBaseName,
|
monsterBaseName: monsterBaseName ?? this.monsterBaseName,
|
||||||
monsterPart: monsterPart ?? this.monsterPart,
|
monsterPart: monsterPart ?? this.monsterPart,
|
||||||
|
monsterLevel: monsterLevel ?? this.monsterLevel,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -227,6 +227,10 @@ class _GamePlayScreenState extends State<GamePlayScreen>
|
|||||||
colorTheme: _colorTheme,
|
colorTheme: _colorTheme,
|
||||||
onThemeCycle: _cycleColorTheme,
|
onThemeCycle: _cycleColorTheme,
|
||||||
specialAnimation: _specialAnimation,
|
specialAnimation: _specialAnimation,
|
||||||
|
weaponName: state.equipment.weapon,
|
||||||
|
shieldName: state.equipment.shield,
|
||||||
|
characterLevel: state.traits.level,
|
||||||
|
monsterLevel: state.progress.currentTask.monsterLevel,
|
||||||
),
|
),
|
||||||
|
|
||||||
// 메인 3패널 영역
|
// 메인 3패널 영역
|
||||||
|
|||||||
@@ -4,6 +4,12 @@ import 'package:flutter/material.dart';
|
|||||||
|
|
||||||
import 'package:askiineverdie/src/core/animation/ascii_animation_data.dart';
|
import 'package:askiineverdie/src/core/animation/ascii_animation_data.dart';
|
||||||
import 'package:askiineverdie/src/core/animation/ascii_animation_type.dart';
|
import 'package:askiineverdie/src/core/animation/ascii_animation_type.dart';
|
||||||
|
import 'package:askiineverdie/src/core/animation/background_layer.dart';
|
||||||
|
import 'package:askiineverdie/src/core/animation/battle_composer.dart';
|
||||||
|
import 'package:askiineverdie/src/core/animation/character_frames.dart';
|
||||||
|
import 'package:askiineverdie/src/core/animation/monster_colors.dart';
|
||||||
|
import 'package:askiineverdie/src/core/animation/monster_size.dart';
|
||||||
|
import 'package:askiineverdie/src/core/animation/weapon_category.dart';
|
||||||
import 'package:askiineverdie/src/core/model/game_state.dart';
|
import 'package:askiineverdie/src/core/model/game_state.dart';
|
||||||
|
|
||||||
/// ASCII 애니메이션 카드 위젯
|
/// ASCII 애니메이션 카드 위젯
|
||||||
@@ -19,6 +25,10 @@ class AsciiAnimationCard extends StatefulWidget {
|
|||||||
this.monsterBaseName,
|
this.monsterBaseName,
|
||||||
this.colorTheme = AsciiColorTheme.green,
|
this.colorTheme = AsciiColorTheme.green,
|
||||||
this.specialAnimation,
|
this.specialAnimation,
|
||||||
|
this.weaponName,
|
||||||
|
this.shieldName,
|
||||||
|
this.characterLevel,
|
||||||
|
this.monsterLevel,
|
||||||
});
|
});
|
||||||
|
|
||||||
final TaskType taskType;
|
final TaskType taskType;
|
||||||
@@ -31,6 +41,18 @@ class AsciiAnimationCard extends StatefulWidget {
|
|||||||
/// 설정되면 일반 애니메이션 대신 표시
|
/// 설정되면 일반 애니메이션 대신 표시
|
||||||
final AsciiAnimationType? specialAnimation;
|
final AsciiAnimationType? specialAnimation;
|
||||||
|
|
||||||
|
/// 현재 장착 무기 이름 (공격 스타일 결정용)
|
||||||
|
final String? weaponName;
|
||||||
|
|
||||||
|
/// 현재 장착 방패 이름 (방패 표시용)
|
||||||
|
final String? shieldName;
|
||||||
|
|
||||||
|
/// 캐릭터 레벨
|
||||||
|
final int? characterLevel;
|
||||||
|
|
||||||
|
/// 몬스터 레벨 (몬스터 크기 결정용)
|
||||||
|
final int? monsterLevel;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<AsciiAnimationCard> createState() => _AsciiAnimationCardState();
|
State<AsciiAnimationCard> createState() => _AsciiAnimationCardState();
|
||||||
}
|
}
|
||||||
@@ -41,6 +63,29 @@ class _AsciiAnimationCardState extends State<AsciiAnimationCard> {
|
|||||||
late AsciiAnimationData _animationData;
|
late AsciiAnimationData _animationData;
|
||||||
AsciiAnimationType? _currentSpecialAnimation;
|
AsciiAnimationType? _currentSpecialAnimation;
|
||||||
|
|
||||||
|
// 전투 애니메이션 상태
|
||||||
|
bool _isBattleMode = false;
|
||||||
|
BattlePhase _battlePhase = BattlePhase.idle;
|
||||||
|
int _battleSubFrame = 0;
|
||||||
|
BattleComposer? _battleComposer;
|
||||||
|
|
||||||
|
// 글로벌 틱 (배경 스크롤용)
|
||||||
|
int _globalTick = 0;
|
||||||
|
|
||||||
|
// 환경 타입
|
||||||
|
EnvironmentType _environment = EnvironmentType.forest;
|
||||||
|
|
||||||
|
// 전투 페이즈 시퀀스 (반복)
|
||||||
|
static const _battlePhaseSequence = [
|
||||||
|
(BattlePhase.idle, 4), // 4 프레임 대기
|
||||||
|
(BattlePhase.prepare, 2), // 2 프레임 준비
|
||||||
|
(BattlePhase.attack, 3), // 3 프레임 공격
|
||||||
|
(BattlePhase.hit, 2), // 2 프레임 히트
|
||||||
|
(BattlePhase.recover, 2), // 2 프레임 복귀
|
||||||
|
];
|
||||||
|
int _phaseIndex = 0;
|
||||||
|
int _phaseFrameCount = 0;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
@@ -64,7 +109,10 @@ class _AsciiAnimationCardState extends State<AsciiAnimationCard> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (oldWidget.taskType != widget.taskType ||
|
if (oldWidget.taskType != widget.taskType ||
|
||||||
oldWidget.monsterBaseName != widget.monsterBaseName) {
|
oldWidget.monsterBaseName != widget.monsterBaseName ||
|
||||||
|
oldWidget.weaponName != widget.weaponName ||
|
||||||
|
oldWidget.shieldName != widget.shieldName ||
|
||||||
|
oldWidget.monsterLevel != widget.monsterLevel) {
|
||||||
_updateAnimation();
|
_updateAnimation();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -74,6 +122,7 @@ class _AsciiAnimationCardState extends State<AsciiAnimationCard> {
|
|||||||
|
|
||||||
// 특수 애니메이션이 있으면 우선 적용
|
// 특수 애니메이션이 있으면 우선 적용
|
||||||
if (_currentSpecialAnimation != null) {
|
if (_currentSpecialAnimation != null) {
|
||||||
|
_isBattleMode = false;
|
||||||
_animationData = getAnimationData(_currentSpecialAnimation!);
|
_animationData = getAnimationData(_currentSpecialAnimation!);
|
||||||
_currentFrame = 0;
|
_currentFrame = 0;
|
||||||
|
|
||||||
@@ -99,26 +148,80 @@ class _AsciiAnimationCardState extends State<AsciiAnimationCard> {
|
|||||||
// 일반 애니메이션 처리
|
// 일반 애니메이션 처리
|
||||||
final animationType = taskTypeToAnimation(widget.taskType);
|
final animationType = taskTypeToAnimation(widget.taskType);
|
||||||
|
|
||||||
// 전투 타입이면 몬스터 카테고리에 따라 다른 애니메이션 선택
|
// 전투 타입이면 새 BattleComposer 시스템 사용
|
||||||
if (animationType == AsciiAnimationType.battle) {
|
if (animationType == AsciiAnimationType.battle) {
|
||||||
final category = getMonsterCategory(widget.monsterBaseName);
|
_isBattleMode = true;
|
||||||
_animationData = getBattleAnimation(category);
|
_setupBattleComposer();
|
||||||
|
_battlePhase = BattlePhase.idle;
|
||||||
|
_battleSubFrame = 0;
|
||||||
|
_phaseIndex = 0;
|
||||||
|
_phaseFrameCount = 0;
|
||||||
|
|
||||||
|
_timer = Timer.periodic(
|
||||||
|
const Duration(milliseconds: 200),
|
||||||
|
(_) => _advanceBattleFrame(),
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
|
_isBattleMode = false;
|
||||||
_animationData = getAnimationData(animationType);
|
_animationData = getAnimationData(animationType);
|
||||||
|
_currentFrame = 0;
|
||||||
|
|
||||||
|
_timer = Timer.periodic(
|
||||||
|
Duration(milliseconds: _animationData.frameIntervalMs),
|
||||||
|
(_) {
|
||||||
|
if (mounted) {
|
||||||
|
setState(() {
|
||||||
|
_currentFrame =
|
||||||
|
(_currentFrame + 1) % _animationData.frames.length;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
_currentFrame = 0;
|
void _setupBattleComposer() {
|
||||||
|
final weaponCategory = getWeaponCategory(widget.weaponName);
|
||||||
|
final hasShield =
|
||||||
|
widget.shieldName != null && widget.shieldName!.isNotEmpty;
|
||||||
|
final monsterCategory = getMonsterCategory(widget.monsterBaseName);
|
||||||
|
final monsterSize = getMonsterSize(widget.monsterLevel);
|
||||||
|
|
||||||
_timer = Timer.periodic(
|
_battleComposer = BattleComposer(
|
||||||
Duration(milliseconds: _animationData.frameIntervalMs),
|
weaponCategory: weaponCategory,
|
||||||
(_) {
|
hasShield: hasShield,
|
||||||
if (mounted) {
|
monsterCategory: monsterCategory,
|
||||||
setState(() {
|
monsterSize: monsterSize,
|
||||||
_currentFrame = (_currentFrame + 1) % _animationData.frames.length;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// 환경 타입 추론
|
||||||
|
_environment = inferEnvironment(
|
||||||
|
widget.taskType.name,
|
||||||
|
widget.monsterBaseName,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _advanceBattleFrame() {
|
||||||
|
if (!mounted) return;
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
// 글로벌 틱 증가 (배경 스크롤용)
|
||||||
|
_globalTick++;
|
||||||
|
|
||||||
|
_phaseFrameCount++;
|
||||||
|
final currentPhase = _battlePhaseSequence[_phaseIndex];
|
||||||
|
|
||||||
|
// 현재 페이즈의 프레임 수를 초과하면 다음 페이즈로
|
||||||
|
if (_phaseFrameCount >= currentPhase.$2) {
|
||||||
|
_phaseIndex = (_phaseIndex + 1) % _battlePhaseSequence.length;
|
||||||
|
_phaseFrameCount = 0;
|
||||||
|
_battleSubFrame = 0;
|
||||||
|
} else {
|
||||||
|
_battleSubFrame++;
|
||||||
|
}
|
||||||
|
|
||||||
|
_battlePhase = _battlePhaseSequence[_phaseIndex].$1;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -138,11 +241,35 @@ class _AsciiAnimationCardState extends State<AsciiAnimationCard> {
|
|||||||
? colors.backgroundColor.withValues(alpha: 0.95)
|
? colors.backgroundColor.withValues(alpha: 0.95)
|
||||||
: colors.backgroundColor;
|
: colors.backgroundColor;
|
||||||
|
|
||||||
// 프레임 인덱스가 범위를 벗어나지 않도록 보정
|
// 프레임 텍스트 결정
|
||||||
final frameIndex = _currentFrame.clamp(0, _animationData.frames.length - 1);
|
String frameText;
|
||||||
|
Color textColor = colors.textColor;
|
||||||
|
|
||||||
|
if (_isBattleMode && _battleComposer != null) {
|
||||||
|
// 새 배틀 시스템 사용 (배경 포함)
|
||||||
|
frameText = _battleComposer!.composeFrameWithBackground(
|
||||||
|
_battlePhase,
|
||||||
|
_battleSubFrame,
|
||||||
|
widget.monsterBaseName,
|
||||||
|
_environment,
|
||||||
|
_globalTick,
|
||||||
|
);
|
||||||
|
|
||||||
|
// 히트 페이즈면 몬스터 색상 변경
|
||||||
|
if (_battlePhase == BattlePhase.hit) {
|
||||||
|
final monsterColorCategory =
|
||||||
|
getMonsterColorCategory(widget.monsterBaseName);
|
||||||
|
textColor = getMonsterColors(monsterColorCategory).hit;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 기존 레거시 시스템 사용
|
||||||
|
final frameIndex =
|
||||||
|
_currentFrame.clamp(0, _animationData.frames.length - 1);
|
||||||
|
frameText = _animationData.frames[frameIndex];
|
||||||
|
}
|
||||||
|
|
||||||
return Container(
|
return Container(
|
||||||
padding: const EdgeInsets.all(8),
|
padding: const EdgeInsets.all(4),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: bgColor,
|
color: bgColor,
|
||||||
borderRadius: BorderRadius.circular(4),
|
borderRadius: BorderRadius.circular(4),
|
||||||
@@ -150,19 +277,49 @@ class _AsciiAnimationCardState extends State<AsciiAnimationCard> {
|
|||||||
? Border.all(color: colors.textColor.withValues(alpha: 0.5))
|
? Border.all(color: colors.textColor.withValues(alpha: 0.5))
|
||||||
: null,
|
: null,
|
||||||
),
|
),
|
||||||
child: Center(
|
child: _isBattleMode
|
||||||
child: Text(
|
? LayoutBuilder(
|
||||||
_animationData.frames[frameIndex],
|
builder: (context, constraints) {
|
||||||
style: TextStyle(
|
// 60x8 프레임에 맞게 폰트 크기 자동 계산
|
||||||
fontFamily: 'monospace',
|
// ASCII 문자 비율: 너비 = 높이 * 0.6 (모노스페이스)
|
||||||
fontSize: 10,
|
final maxWidth = constraints.maxWidth;
|
||||||
color: colors.textColor,
|
final maxHeight = constraints.maxHeight;
|
||||||
height: 1.1,
|
// 60자 폭, 8줄 높이 기준
|
||||||
letterSpacing: 0,
|
final fontSizeByWidth = maxWidth / 60 / 0.6;
|
||||||
),
|
final fontSizeByHeight = maxHeight / 8 / 1.2;
|
||||||
textAlign: TextAlign.center,
|
final fontSize = (fontSizeByWidth < fontSizeByHeight
|
||||||
),
|
? fontSizeByWidth
|
||||||
),
|
: fontSizeByHeight)
|
||||||
|
.clamp(6.0, 14.0);
|
||||||
|
|
||||||
|
return Center(
|
||||||
|
child: Text(
|
||||||
|
frameText,
|
||||||
|
style: TextStyle(
|
||||||
|
fontFamily: 'Courier',
|
||||||
|
fontSize: fontSize,
|
||||||
|
color: textColor,
|
||||||
|
height: 1.2,
|
||||||
|
letterSpacing: 0,
|
||||||
|
),
|
||||||
|
textAlign: TextAlign.left,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
)
|
||||||
|
: Center(
|
||||||
|
child: Text(
|
||||||
|
frameText,
|
||||||
|
style: TextStyle(
|
||||||
|
fontFamily: 'monospace',
|
||||||
|
fontSize: 10,
|
||||||
|
color: textColor,
|
||||||
|
height: 1.1,
|
||||||
|
letterSpacing: 0,
|
||||||
|
),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,6 +16,10 @@ class TaskProgressPanel extends StatelessWidget {
|
|||||||
required this.colorTheme,
|
required this.colorTheme,
|
||||||
required this.onThemeCycle,
|
required this.onThemeCycle,
|
||||||
this.specialAnimation,
|
this.specialAnimation,
|
||||||
|
this.weaponName,
|
||||||
|
this.shieldName,
|
||||||
|
this.characterLevel,
|
||||||
|
this.monsterLevel,
|
||||||
});
|
});
|
||||||
|
|
||||||
final ProgressState progress;
|
final ProgressState progress;
|
||||||
@@ -27,6 +31,12 @@ class TaskProgressPanel extends StatelessWidget {
|
|||||||
/// 특수 애니메이션 (레벨업, 퀘스트 완료 등)
|
/// 특수 애니메이션 (레벨업, 퀘스트 완료 등)
|
||||||
final AsciiAnimationType? specialAnimation;
|
final AsciiAnimationType? specialAnimation;
|
||||||
|
|
||||||
|
/// 장비 정보 (애니메이션 스타일 결정용)
|
||||||
|
final String? weaponName;
|
||||||
|
final String? shieldName;
|
||||||
|
final int? characterLevel;
|
||||||
|
final int? monsterLevel;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Container(
|
return Container(
|
||||||
@@ -48,6 +58,10 @@ class TaskProgressPanel extends StatelessWidget {
|
|||||||
monsterBaseName: progress.currentTask.monsterBaseName,
|
monsterBaseName: progress.currentTask.monsterBaseName,
|
||||||
colorTheme: colorTheme,
|
colorTheme: colorTheme,
|
||||||
specialAnimation: specialAnimation,
|
specialAnimation: specialAnimation,
|
||||||
|
weaponName: weaponName,
|
||||||
|
shieldName: shieldName,
|
||||||
|
characterLevel: characterLevel,
|
||||||
|
monsterLevel: monsterLevel,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
|
|||||||
Reference in New Issue
Block a user