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;
|
||||
}
|
||||
|
||||
/// 기본 전투 애니메이션 (beast - 고양이 모양)
|
||||
/// 기본 전투 애니메이션 (beast - 고양이 모양, 심플 3줄)
|
||||
const battleAnimationBeast = AsciiAnimationData(
|
||||
frames: [
|
||||
// 프레임 1: 대치
|
||||
'''
|
||||
,O, /\\___/\\
|
||||
/( )\\ ( o o )
|
||||
/ \\ vs ( =^= )
|
||||
_| |_ /| |\\
|
||||
| | / | | \\
|
||||
_| |_ | |_____| |
|
||||
|_________| |___| |___|''',
|
||||
o vs /\\_/\\
|
||||
/|\\ ( o.o )
|
||||
/ \\ > ^ <''',
|
||||
// 프레임 2: 공격 준비
|
||||
'''
|
||||
O /\\___/\\
|
||||
/|\\----o ( o o )
|
||||
/ \\ ( =^= )
|
||||
_| |_ /| |\\
|
||||
| | / | | \\
|
||||
_| |_ | |_____| |
|
||||
|_________| |___| |___|''',
|
||||
o----o /\\_/\\
|
||||
/|\\ ( o.o )
|
||||
/ \\ > ^ <''',
|
||||
// 프레임 3: 공격 중
|
||||
'''
|
||||
O o--->/\\___/\\
|
||||
/|\\-----------> ( X X )
|
||||
/ \\ ( =^= )
|
||||
_| |_ /| |\\
|
||||
| | / | | \\
|
||||
_| |_ | |_____| |
|
||||
|_________| |___| |___|''',
|
||||
o o-----> /\\_/\\
|
||||
/|\\ ( X.X )
|
||||
/ \\ > ^ <''',
|
||||
// 프레임 4: 히트
|
||||
'''
|
||||
O /\\___/\\
|
||||
/|\\ **** ( X X ) ****
|
||||
/ \\ ** ( =^= ) **
|
||||
_| |_ /| |\\
|
||||
| | / | | \\
|
||||
_| |_ | |_____| |
|
||||
|_________| |___| |___|''',
|
||||
o **** /\\_/\\
|
||||
/|\\ *** ( X.X ) ***
|
||||
/ \\ > ~ <''',
|
||||
// 프레임 5: 복귀
|
||||
'''
|
||||
\\O/ /\\___/\\
|
||||
| ( - - )
|
||||
/ \\ ( =^= )
|
||||
_| |_ /| |\\
|
||||
| | / | | \\
|
||||
_| |_ | |_____| |
|
||||
|_________| |___| |___|''',
|
||||
\\o/ /\\_/\\
|
||||
| ( -.-)
|
||||
/ \\ > ^ <''',
|
||||
],
|
||||
frameIntervalMs: 220,
|
||||
);
|
||||
|
||||
/// 마을/상점 애니메이션 (7줄)
|
||||
/// 마을/상점 애니메이션 (심플 3줄 캐릭터)
|
||||
const townAnimation = AsciiAnimationData(
|
||||
frames: [
|
||||
// 프레임 1: 상점 앞에서 대기
|
||||
'''
|
||||
_______________
|
||||
/ \\ O
|
||||
| SHOP | /|\\
|
||||
| [=====] | / \\
|
||||
| | | | |
|
||||
|___|_____|______| _|_
|
||||
~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~''',
|
||||
___________ o
|
||||
/ SHOP \\/|\\
|
||||
~~|__|____|__|/ \\~~~~~~~~~~~~~''',
|
||||
// 프레임 2: 상점으로 이동
|
||||
'''
|
||||
_______________
|
||||
/ \\ O
|
||||
| SHOP | /|\\
|
||||
| [=====] | / \\
|
||||
| | | | |
|
||||
|___|_____|______| _|_
|
||||
~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~''',
|
||||
// 프레임 3: 상점 앞 도착
|
||||
___________ o
|
||||
/ SHOP \\/|\\
|
||||
~~|__|____|__|/ \\~~~~~~~~~~~~~''',
|
||||
// 프레임 3: 거래 시작
|
||||
'''
|
||||
_______________
|
||||
/ \\ O
|
||||
| SHOP | /|\\
|
||||
| [=====] | / \\
|
||||
| | | | |
|
||||
|___|_____|______| _|_
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~''',
|
||||
___________ o \$
|
||||
/ SHOP \\/|\\ \$
|
||||
~~|__[ @@ ]__|/ \\ \$~~~~~~~~~~~''',
|
||||
// 프레임 4: 거래 중
|
||||
'''
|
||||
_______________
|
||||
/ \\ O \$
|
||||
| SHOP | /|\\ \$
|
||||
| [=====] | /\\\$
|
||||
| | @ | | |
|
||||
|___|_____|______| _|_
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~''',
|
||||
___________ o \$\$
|
||||
/ SHOP \\/|\\ \$\$
|
||||
~~|__[ @@ ]__|/ \\ \$\$~~~~~~~~~~''',
|
||||
// 프레임 5: 거래 완료
|
||||
'''
|
||||
_______________
|
||||
/ \\ \\O/
|
||||
| SHOP | | +
|
||||
| [=====] | / \\ +
|
||||
| | @ | | | +
|
||||
|___|_____|______| _|_
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~''',
|
||||
___________ \\o/ +
|
||||
/ SHOP \\ | +
|
||||
~~|__[ @@ ]__|/ \\ +~~~~~~~~~~~''',
|
||||
],
|
||||
frameIntervalMs: 280,
|
||||
);
|
||||
|
||||
/// 걷는 애니메이션 (7줄, 배경 포함)
|
||||
/// 걷는 애니메이션 (심플 3줄 캐릭터 + 배경)
|
||||
const walkingAnimation = AsciiAnimationData(
|
||||
frames: [
|
||||
// 프레임 1: 서있기
|
||||
'''
|
||||
O
|
||||
/|\\
|
||||
/ \\
|
||||
~~ | ~~
|
||||
~~~~ _|_ ~~~~
|
||||
~~~~~~ ~~~~~~~~ ~~~~~~
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~''',
|
||||
~~~~ o ~~~~
|
||||
~~~~~~ /|\\ ~~~~~~
|
||||
~~~~~~~~ / \\ ~~~~~~~~''',
|
||||
// 프레임 2: 왼발 앞
|
||||
'''
|
||||
O
|
||||
/|\\
|
||||
/|
|
||||
~~ / \\ ~~
|
||||
~~~~ _|_ ~~~~
|
||||
~~~~~~ ~~~~~~~~ ~~~~~~
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~''',
|
||||
~~~~ o ~~~~
|
||||
~~~~~~ /|\\ ~~~~~~
|
||||
~~~~~~~~ /| ~~~~~~~~''',
|
||||
// 프레임 3: 이동 중
|
||||
'''
|
||||
O
|
||||
/|\\
|
||||
|\\
|
||||
~~ / \\ ~~
|
||||
~~~~ _|_ ~~~~
|
||||
~~~~~~ ~~~~~~~~ ~~~~~~
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~''',
|
||||
~~~~ o ~~~~
|
||||
~~~~~~ /|\\ ~~~~~~
|
||||
~~~~~~~~ |\\ ~~~~~~~~''',
|
||||
// 프레임 4: 오른발 앞
|
||||
'''
|
||||
O
|
||||
/|\\
|
||||
|/
|
||||
~~ / \\ ~~
|
||||
~~~~ _|_ ~~~~
|
||||
~~~~~~ ~~~~~~~~ ~~~~~~
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~''',
|
||||
~~~~ o ~~~~
|
||||
~~~~~~ /|\\ ~~~~~~
|
||||
~~~~~~~~ |/ ~~~~~~~~''',
|
||||
// 프레임 5: 복귀
|
||||
'''
|
||||
O
|
||||
/|\\
|
||||
/ \\
|
||||
~~ | ~~
|
||||
~~~~ _|_ ~~~~
|
||||
~~~~~~ ~~~~~~~~ ~~~~~~
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~''',
|
||||
~~~~ o ~~~~
|
||||
~~~~~~ /|\\ ~~~~~~
|
||||
~~~~~~~~ / \\ ~~~~~~~~''',
|
||||
],
|
||||
frameIntervalMs: 180,
|
||||
);
|
||||
|
||||
/// 곤충 전투 애니메이션
|
||||
/// 곤충 전투 애니메이션 (심플 3줄)
|
||||
const battleAnimationInsect = AsciiAnimationData(
|
||||
frames: [
|
||||
// 프레임 1: 대치
|
||||
'''
|
||||
,O, /\\_/\\
|
||||
/( )\\ ( o o )
|
||||
/ \\ vs /|=====|\\
|
||||
_| |_ < | | >
|
||||
| | \\|_____|/
|
||||
_| |_ / \\
|
||||
|_________| /_______\\''',
|
||||
o vs /\\_/\\
|
||||
/|\\ ( o o )
|
||||
/ \\ /|=====|\\''',
|
||||
// 프레임 2: 공격 준비
|
||||
'''
|
||||
O /\\_/\\
|
||||
/|\\----o ( o o )
|
||||
/ \\ /|=====|\\
|
||||
_| |_ < | | >
|
||||
| | \\|_____|/
|
||||
_| |_ / \\
|
||||
|_________| /_______\\''',
|
||||
o----o /\\_/\\
|
||||
/|\\ ( o o )
|
||||
/ \\ /|=====|\\''',
|
||||
// 프레임 3: 공격 중
|
||||
'''
|
||||
O o-->/\\_/\\
|
||||
/|\\----------> ( o o )
|
||||
/ \\ /|=====|\\
|
||||
_| |_ < | | >
|
||||
| | \\|_____|/
|
||||
_| |_ / \\
|
||||
|_________| /_______\\''',
|
||||
o o-----> /\\_/\\
|
||||
/|\\ ( X X )
|
||||
/ \\ /|=====|\\''',
|
||||
// 프레임 4: 히트
|
||||
'''
|
||||
O /\\_/\\
|
||||
/|\\ **** ( X X ) ****
|
||||
/ \\ ** /|=====|\\ **
|
||||
_| |_ < | | >
|
||||
| | \\|_____|/
|
||||
_| |_ / \\
|
||||
|_________| /_______\\''',
|
||||
o **** /\\_/\\
|
||||
/|\\ *** ( X X ) ***
|
||||
/ \\ /|=====|\\''',
|
||||
// 프레임 5: 복귀
|
||||
'''
|
||||
\\O/ /\\_/\\
|
||||
| ( - - )
|
||||
/ \\ /|=====|\\
|
||||
_| |_ < | | >
|
||||
| | \\|_____|/
|
||||
_| |_ / \\
|
||||
|_________| /_______\\''',
|
||||
\\o/ /\\_/\\
|
||||
| ( - - )
|
||||
/ \\ /|=====|\\''',
|
||||
],
|
||||
frameIntervalMs: 220,
|
||||
);
|
||||
|
||||
/// 인간형 전투 애니메이션
|
||||
/// 인간형 전투 애니메이션 (심플 3줄)
|
||||
const battleAnimationHumanoid = AsciiAnimationData(
|
||||
frames: [
|
||||
// 프레임 1: 대치
|
||||
'''
|
||||
,O, O
|
||||
/( )\\ /|\\
|
||||
/ \\ vs / | \\
|
||||
_| |_ ___|___
|
||||
| | | |
|
||||
_| |_ | orc |
|
||||
|_________| |_______|''',
|
||||
o vs O
|
||||
/|\\ /|\\
|
||||
/ \\ / | \\''',
|
||||
// 프레임 2: 공격 준비
|
||||
'''
|
||||
O O
|
||||
/|\\----o /|\\
|
||||
/ \\ / | \\
|
||||
_| |_ ___|___
|
||||
| | | |
|
||||
_| |_ | orc |
|
||||
|_________| |_______|''',
|
||||
o----o O
|
||||
/|\\ /|\\
|
||||
/ \\ / | \\''',
|
||||
// 프레임 3: 공격 중
|
||||
'''
|
||||
O o----> O
|
||||
/|\\-----------> /|\\
|
||||
/ \\ / | \\
|
||||
_| |_ ___|___
|
||||
| | | |
|
||||
_| |_ | orc |
|
||||
|_________| |_______|''',
|
||||
o o-----> O
|
||||
/|\\ X|X
|
||||
/ \\ / | \\''',
|
||||
// 프레임 4: 히트
|
||||
'''
|
||||
O O
|
||||
/|\\ **** X|X ****
|
||||
/ \\ ** / | \\ **
|
||||
_| |_ ___|___
|
||||
| | | |
|
||||
_| |_ | orc |
|
||||
|_________| |_______|''',
|
||||
o **** O
|
||||
/|\\ *** X|X ***
|
||||
/ \\ / | \\''',
|
||||
// 프레임 5: 복귀
|
||||
'''
|
||||
\\O/ O
|
||||
| /|\\
|
||||
/ \\ / | \\
|
||||
_| |_ ___|___
|
||||
| | | |
|
||||
_| |_ | orc |
|
||||
|_________| |_______|''',
|
||||
\\o/ O
|
||||
| /|\\
|
||||
/ \\ / | \\''',
|
||||
],
|
||||
frameIntervalMs: 220,
|
||||
);
|
||||
|
||||
/// 언데드 전투 애니메이션
|
||||
/// 언데드 전투 애니메이션 (심플 3줄)
|
||||
const battleAnimationUndead = AsciiAnimationData(
|
||||
frames: [
|
||||
// 프레임 1: 대치
|
||||
'''
|
||||
,O, .-.
|
||||
/( )\\ (o.o)
|
||||
/ \\ vs |=|
|
||||
_| |_ /|X|\\
|
||||
| | / | | \\
|
||||
_| |_ \\_|_|_/
|
||||
|_________| _/ \\_''',
|
||||
o vs .-.
|
||||
/|\\ (o.o)
|
||||
/ \\ |=|''',
|
||||
// 프레임 2: 공격 준비
|
||||
'''
|
||||
O .-.
|
||||
/|\\----o (o.o)
|
||||
/ \\ |=|
|
||||
_| |_ /|X|\\
|
||||
| | / | | \\
|
||||
_| |_ \\_|_|_/
|
||||
|_________| _/ \\_''',
|
||||
o----o .-.
|
||||
/|\\ (o.o)
|
||||
/ \\ |=|''',
|
||||
// 프레임 3: 공격 중
|
||||
'''
|
||||
O o--->.-.
|
||||
/|\\-----------> (o.o)
|
||||
/ \\ |=|
|
||||
_| |_ /|X|\\
|
||||
| | / | | \\
|
||||
_| |_ \\_|_|_/
|
||||
|_________| _/ \\_''',
|
||||
o o-----> .-.
|
||||
/|\\ (X.X)
|
||||
/ \\ |=|''',
|
||||
// 프레임 4: 히트
|
||||
'''
|
||||
O .-.
|
||||
/|\\ **** (X.X) ****
|
||||
/ \\ ** |=| **
|
||||
_| |_ /|X|\\
|
||||
| | / | | \\
|
||||
_| |_ \\_|_|_/
|
||||
|_________| _/ \\_''',
|
||||
o **** .-.
|
||||
/|\\ *** (X.X) ***
|
||||
/ \\ |~|''',
|
||||
// 프레임 5: 복귀
|
||||
'''
|
||||
\\O/ .-.
|
||||
| (-.-)
|
||||
/ \\ |=|
|
||||
_| |_ /|X|\\
|
||||
| | / | | \\
|
||||
_| |_ \\_|_|_/
|
||||
|_________| _/ \\_''',
|
||||
\\o/ .-.
|
||||
| (-.-)
|
||||
/ \\ |=|''',
|
||||
],
|
||||
frameIntervalMs: 250,
|
||||
);
|
||||
|
||||
/// 드래곤 전투 애니메이션
|
||||
/// 드래곤 전투 애니메이션 (심플 3줄)
|
||||
const battleAnimationDragon = AsciiAnimationData(
|
||||
frames: [
|
||||
// 프레임 1: 대치
|
||||
'''
|
||||
,O, __/\\__
|
||||
/( )\\ / \\
|
||||
/ \\ vs < (O)(O) >
|
||||
_| |_ \\ \\/ /
|
||||
| | \\ /
|
||||
_| |_ /|\\~~~/|\\
|
||||
|_________| /_________\\''',
|
||||
o vs __/\\__
|
||||
/|\\ < (O)(O) >
|
||||
/ \\ \\ \\/ /''',
|
||||
// 프레임 2: 공격 준비
|
||||
'''
|
||||
O __/\\__
|
||||
/|\\----o / \\
|
||||
/ \\ < (O)(O) >
|
||||
_| |_ \\ \\/ /
|
||||
| | \\ /
|
||||
_| |_ /|\\~~~/|\\
|
||||
|_________| /_________\\''',
|
||||
o----o __/\\__
|
||||
/|\\ < (O)(O) >
|
||||
/ \\ \\ \\/ /''',
|
||||
// 프레임 3: 공격 중
|
||||
'''
|
||||
O o--->__/\\__
|
||||
/|\\---------> / \\
|
||||
/ \\ < (O)(O) >
|
||||
_| |_ \\ \\/ /
|
||||
| | \\ /
|
||||
_| |_ /|\\~~~/|\\
|
||||
|_________| /_________\\''',
|
||||
o o-----> __/\\__
|
||||
/|\\ < (X)(X) >
|
||||
/ \\ \\ \\/ /''',
|
||||
// 프레임 4: 히트
|
||||
'''
|
||||
O __/\\__
|
||||
/|\\ **** / >< \\ ****
|
||||
/ \\ ** < (X)(X) > **
|
||||
_| |_ \\ \\/ /
|
||||
| | \\ /
|
||||
_| |_ /|\\~~~/|\\
|
||||
|_________| /_________\\''',
|
||||
o **** __/\\__
|
||||
/|\\ *** < (X)(X) > ***
|
||||
/ \\ \\ ~~ /''',
|
||||
// 프레임 5: 복귀
|
||||
'''
|
||||
\\O/ __/\\__
|
||||
| / \\
|
||||
/ \\ < (-)(-)>
|
||||
_| |_ \\ \\/ /
|
||||
| | \\ /
|
||||
_| |_ /|\\~~~/|\\
|
||||
|_________| /_________\\''',
|
||||
\\o/ __/\\__
|
||||
| < (-)(-)>
|
||||
/ \\ \\ \\/ /''',
|
||||
],
|
||||
frameIntervalMs: 200,
|
||||
);
|
||||
|
||||
/// 슬라임 전투 애니메이션
|
||||
/// 슬라임 전투 애니메이션 (심플 3줄)
|
||||
const battleAnimationSlime = AsciiAnimationData(
|
||||
frames: [
|
||||
// 프레임 1: 대치
|
||||
'''
|
||||
,O, .---.
|
||||
/( )\\ / \\
|
||||
/ \\ vs ( o o )
|
||||
_| |_ \\ ~ /
|
||||
| | '---'
|
||||
_| |_ ~~~~~~~
|
||||
|_________| ~~~~~~~~~''',
|
||||
o vs .---.
|
||||
/|\\ ( o o )
|
||||
/ \\ ~~~~~''',
|
||||
// 프레임 2: 공격 준비
|
||||
'''
|
||||
O .---.
|
||||
/|\\----o / \\
|
||||
/ \\ ( o o )
|
||||
_| |_ \\ ~ /
|
||||
| | '---'
|
||||
_| |_ ~~~~~~~
|
||||
|_________| ~~~~~~~~~''',
|
||||
o----o .---.
|
||||
/|\\ ( o o )
|
||||
/ \\ ~~~~~''',
|
||||
// 프레임 3: 공격 중
|
||||
'''
|
||||
O o--->.---.
|
||||
/|\\---------> / \\
|
||||
/ \\ ( o o )
|
||||
_| |_ \\ ~ /
|
||||
| | '---'
|
||||
_| |_ ~~~~~~~
|
||||
|_________| ~~~~~~~~~''',
|
||||
o o-----> .---.
|
||||
/|\\ ( X X )
|
||||
/ \\ ~~~~~''',
|
||||
// 프레임 4: 히트
|
||||
'''
|
||||
O .---.
|
||||
/|\\ **** / X X \\ ****
|
||||
/ \\ ** ( ~ ) **
|
||||
_| |_ \\ /
|
||||
| | '---'
|
||||
_| |_ ~~~~~~~
|
||||
|_________| ~~~~~~~~~''',
|
||||
o **** .---.
|
||||
/|\\ *** ( X X ) ***
|
||||
/ \\ ~~~~~''',
|
||||
// 프레임 5: 복귀
|
||||
'''
|
||||
\\O/ .---.
|
||||
| / \\
|
||||
/ \\ ( - - )
|
||||
_| |_ \\ ~ /
|
||||
| | '---'
|
||||
_| |_ ~~~~~~~
|
||||
|_________| ~~~~~~~~~''',
|
||||
\\o/ .---.
|
||||
| ( - - )
|
||||
/ \\ ~~~~~''',
|
||||
],
|
||||
frameIntervalMs: 280,
|
||||
);
|
||||
|
||||
/// 악마 전투 애니메이션
|
||||
/// 악마 전투 애니메이션 (심플 3줄)
|
||||
const battleAnimationDemon = AsciiAnimationData(
|
||||
frames: [
|
||||
// 프레임 1: 대치
|
||||
'''
|
||||
,O, /\\ /\\
|
||||
/( )\\ ( \\ / )
|
||||
/ \\ vs \\ o o /
|
||||
_| |_ | V |
|
||||
| | | ~~~ |
|
||||
_| |_ /| |\\
|
||||
|_________| /___|___|_\\''',
|
||||
o vs /\\ /\\
|
||||
/|\\ ( o V o )
|
||||
/ \\ \\ ~~~ /''',
|
||||
// 프레임 2: 공격 준비
|
||||
'''
|
||||
O /\\ /\\
|
||||
/|\\----o ( \\ / )
|
||||
/ \\ \\ o o /
|
||||
_| |_ | V |
|
||||
| | | ~~~ |
|
||||
_| |_ /| |\\
|
||||
|_________| /___|___|_\\''',
|
||||
o----o /\\ /\\
|
||||
/|\\ ( o V o )
|
||||
/ \\ \\ ~~~ /''',
|
||||
// 프레임 3: 공격 중
|
||||
'''
|
||||
O o--->/\\ /\\
|
||||
/|\\--------> ( \\ / )
|
||||
/ \\ \\ o o /
|
||||
_| |_ | V |
|
||||
| | | ~~~ |
|
||||
_| |_ /| |\\
|
||||
|_________| /___|___|_\\''',
|
||||
o o-----> /\\ /\\
|
||||
/|\\ ( X V X )
|
||||
/ \\ \\ ~~~ /''',
|
||||
// 프레임 4: 히트
|
||||
'''
|
||||
O /\\ /\\
|
||||
/|\\ **** ( X X ) ****
|
||||
/ \\ ** \\ X X / **
|
||||
_| |_ | V |
|
||||
| | | ~~~ |
|
||||
_| |_ /| |\\
|
||||
|_________| /___|___|_\\''',
|
||||
o **** /\\ /\\
|
||||
/|\\ *** ( X V X ) ***
|
||||
/ \\ \\ ~~~ /''',
|
||||
// 프레임 5: 복귀
|
||||
'''
|
||||
\\O/ /\\ /\\
|
||||
| ( \\ / )
|
||||
/ \\ \\ - - /
|
||||
_| |_ | V |
|
||||
| | | ~~~ |
|
||||
_| |_ /| |\\
|
||||
|_________| /___|___|_\\''',
|
||||
\\o/ /\\ /\\
|
||||
| ( - V - )
|
||||
/ \\ \\ ~~~ /''',
|
||||
],
|
||||
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,
|
||||
monsterBaseName: monsterResult.baseName,
|
||||
monsterPart: monsterResult.part,
|
||||
monsterLevel: monsterResult.level,
|
||||
),
|
||||
);
|
||||
|
||||
|
||||
@@ -95,6 +95,7 @@ class TaskInfo {
|
||||
required this.type,
|
||||
this.monsterBaseName,
|
||||
this.monsterPart,
|
||||
this.monsterLevel,
|
||||
});
|
||||
|
||||
final String caption;
|
||||
@@ -106,6 +107,9 @@ class TaskInfo {
|
||||
/// 킬 태스크의 전리품 부위 (예: "claw", "tail", "*"는 WinItem)
|
||||
final String? monsterPart;
|
||||
|
||||
/// 킬 태스크의 몬스터 레벨 (애니메이션 크기 결정용)
|
||||
final int? monsterLevel;
|
||||
|
||||
factory TaskInfo.empty() =>
|
||||
const TaskInfo(caption: '', type: TaskType.neutral);
|
||||
|
||||
@@ -114,12 +118,14 @@ class TaskInfo {
|
||||
TaskType? type,
|
||||
String? monsterBaseName,
|
||||
String? monsterPart,
|
||||
int? monsterLevel,
|
||||
}) {
|
||||
return TaskInfo(
|
||||
caption: caption ?? this.caption,
|
||||
type: type ?? this.type,
|
||||
monsterBaseName: monsterBaseName ?? this.monsterBaseName,
|
||||
monsterPart: monsterPart ?? this.monsterPart,
|
||||
monsterLevel: monsterLevel ?? this.monsterLevel,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -227,6 +227,10 @@ class _GamePlayScreenState extends State<GamePlayScreen>
|
||||
colorTheme: _colorTheme,
|
||||
onThemeCycle: _cycleColorTheme,
|
||||
specialAnimation: _specialAnimation,
|
||||
weaponName: state.equipment.weapon,
|
||||
shieldName: state.equipment.shield,
|
||||
characterLevel: state.traits.level,
|
||||
monsterLevel: state.progress.currentTask.monsterLevel,
|
||||
),
|
||||
|
||||
// 메인 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_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';
|
||||
|
||||
/// ASCII 애니메이션 카드 위젯
|
||||
@@ -19,6 +25,10 @@ class AsciiAnimationCard extends StatefulWidget {
|
||||
this.monsterBaseName,
|
||||
this.colorTheme = AsciiColorTheme.green,
|
||||
this.specialAnimation,
|
||||
this.weaponName,
|
||||
this.shieldName,
|
||||
this.characterLevel,
|
||||
this.monsterLevel,
|
||||
});
|
||||
|
||||
final TaskType taskType;
|
||||
@@ -31,6 +41,18 @@ class AsciiAnimationCard extends StatefulWidget {
|
||||
/// 설정되면 일반 애니메이션 대신 표시
|
||||
final AsciiAnimationType? specialAnimation;
|
||||
|
||||
/// 현재 장착 무기 이름 (공격 스타일 결정용)
|
||||
final String? weaponName;
|
||||
|
||||
/// 현재 장착 방패 이름 (방패 표시용)
|
||||
final String? shieldName;
|
||||
|
||||
/// 캐릭터 레벨
|
||||
final int? characterLevel;
|
||||
|
||||
/// 몬스터 레벨 (몬스터 크기 결정용)
|
||||
final int? monsterLevel;
|
||||
|
||||
@override
|
||||
State<AsciiAnimationCard> createState() => _AsciiAnimationCardState();
|
||||
}
|
||||
@@ -41,6 +63,29 @@ class _AsciiAnimationCardState extends State<AsciiAnimationCard> {
|
||||
late AsciiAnimationData _animationData;
|
||||
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
|
||||
void initState() {
|
||||
super.initState();
|
||||
@@ -64,7 +109,10 @@ class _AsciiAnimationCardState extends State<AsciiAnimationCard> {
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
@@ -74,6 +122,7 @@ class _AsciiAnimationCardState extends State<AsciiAnimationCard> {
|
||||
|
||||
// 특수 애니메이션이 있으면 우선 적용
|
||||
if (_currentSpecialAnimation != null) {
|
||||
_isBattleMode = false;
|
||||
_animationData = getAnimationData(_currentSpecialAnimation!);
|
||||
_currentFrame = 0;
|
||||
|
||||
@@ -99,26 +148,80 @@ class _AsciiAnimationCardState extends State<AsciiAnimationCard> {
|
||||
// 일반 애니메이션 처리
|
||||
final animationType = taskTypeToAnimation(widget.taskType);
|
||||
|
||||
// 전투 타입이면 몬스터 카테고리에 따라 다른 애니메이션 선택
|
||||
// 전투 타입이면 새 BattleComposer 시스템 사용
|
||||
if (animationType == AsciiAnimationType.battle) {
|
||||
final category = getMonsterCategory(widget.monsterBaseName);
|
||||
_animationData = getBattleAnimation(category);
|
||||
_isBattleMode = true;
|
||||
_setupBattleComposer();
|
||||
_battlePhase = BattlePhase.idle;
|
||||
_battleSubFrame = 0;
|
||||
_phaseIndex = 0;
|
||||
_phaseFrameCount = 0;
|
||||
|
||||
_timer = Timer.periodic(
|
||||
const Duration(milliseconds: 200),
|
||||
(_) => _advanceBattleFrame(),
|
||||
);
|
||||
} else {
|
||||
_isBattleMode = false;
|
||||
_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(
|
||||
Duration(milliseconds: _animationData.frameIntervalMs),
|
||||
(_) {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_currentFrame = (_currentFrame + 1) % _animationData.frames.length;
|
||||
});
|
||||
}
|
||||
},
|
||||
_battleComposer = BattleComposer(
|
||||
weaponCategory: weaponCategory,
|
||||
hasShield: hasShield,
|
||||
monsterCategory: monsterCategory,
|
||||
monsterSize: monsterSize,
|
||||
);
|
||||
|
||||
// 환경 타입 추론
|
||||
_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
|
||||
@@ -138,11 +241,35 @@ class _AsciiAnimationCardState extends State<AsciiAnimationCard> {
|
||||
? colors.backgroundColor.withValues(alpha: 0.95)
|
||||
: 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(
|
||||
padding: const EdgeInsets.all(8),
|
||||
padding: const EdgeInsets.all(4),
|
||||
decoration: BoxDecoration(
|
||||
color: bgColor,
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
@@ -150,19 +277,49 @@ class _AsciiAnimationCardState extends State<AsciiAnimationCard> {
|
||||
? Border.all(color: colors.textColor.withValues(alpha: 0.5))
|
||||
: null,
|
||||
),
|
||||
child: Center(
|
||||
child: Text(
|
||||
_animationData.frames[frameIndex],
|
||||
style: TextStyle(
|
||||
fontFamily: 'monospace',
|
||||
fontSize: 10,
|
||||
color: colors.textColor,
|
||||
height: 1.1,
|
||||
letterSpacing: 0,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
child: _isBattleMode
|
||||
? LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
// 60x8 프레임에 맞게 폰트 크기 자동 계산
|
||||
// ASCII 문자 비율: 너비 = 높이 * 0.6 (모노스페이스)
|
||||
final maxWidth = constraints.maxWidth;
|
||||
final maxHeight = constraints.maxHeight;
|
||||
// 60자 폭, 8줄 높이 기준
|
||||
final fontSizeByWidth = maxWidth / 60 / 0.6;
|
||||
final fontSizeByHeight = maxHeight / 8 / 1.2;
|
||||
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.onThemeCycle,
|
||||
this.specialAnimation,
|
||||
this.weaponName,
|
||||
this.shieldName,
|
||||
this.characterLevel,
|
||||
this.monsterLevel,
|
||||
});
|
||||
|
||||
final ProgressState progress;
|
||||
@@ -27,6 +31,12 @@ class TaskProgressPanel extends StatelessWidget {
|
||||
/// 특수 애니메이션 (레벨업, 퀘스트 완료 등)
|
||||
final AsciiAnimationType? specialAnimation;
|
||||
|
||||
/// 장비 정보 (애니메이션 스타일 결정용)
|
||||
final String? weaponName;
|
||||
final String? shieldName;
|
||||
final int? characterLevel;
|
||||
final int? monsterLevel;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
@@ -48,6 +58,10 @@ class TaskProgressPanel extends StatelessWidget {
|
||||
monsterBaseName: progress.currentTask.monsterBaseName,
|
||||
colorTheme: colorTheme,
|
||||
specialAnimation: specialAnimation,
|
||||
weaponName: weaponName,
|
||||
shieldName: shieldName,
|
||||
characterLevel: characterLevel,
|
||||
monsterLevel: monsterLevel,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
|
||||
Reference in New Issue
Block a user