feat(ui): 일시 정지 버튼 추가 및 배속 버그 수정
- 게임 중 일시 정지/재개 버튼 추가 (테마 버튼 옆) - 5x 배속이 2x와 동일하게 작동하던 버그 수정 - progress_service.dart clamp 제한을 100ms에서 500ms로 확장 - ASCII 애니메이션 40x8 규격 통일 - townAnimation, walkingAnimation, levelUpAnimation 등 8줄로 통일 - 레거시 애니메이션 TextAlign.left로 정렬 문제 수정 - 캐릭터 프레임 구조 통일 (머리/몸통/다리 3줄) - 몬스터 크기 enum 실제 프레임 줄 수와 일치하도록 수정
This commit is contained in:
@@ -4,10 +4,7 @@ import 'package:askiineverdie/src/core/animation/ascii_animation_type.dart';
|
|||||||
|
|
||||||
/// ASCII 애니메이션 프레임 데이터
|
/// ASCII 애니메이션 프레임 데이터
|
||||||
class AsciiAnimationData {
|
class AsciiAnimationData {
|
||||||
const AsciiAnimationData({
|
const AsciiAnimationData({required this.frames, this.frameIntervalMs = 200});
|
||||||
required this.frames,
|
|
||||||
this.frameIntervalMs = 200,
|
|
||||||
});
|
|
||||||
|
|
||||||
/// 각 프레임 (문자열, 최소 5줄)
|
/// 각 프레임 (문자열, 최소 5줄)
|
||||||
final List<String> frames;
|
final List<String> frames;
|
||||||
@@ -46,26 +43,27 @@ class AsciiThemeColors {
|
|||||||
AsciiThemeColors getThemeColors(AsciiColorTheme theme, Brightness brightness) {
|
AsciiThemeColors getThemeColors(AsciiColorTheme theme, Brightness brightness) {
|
||||||
return switch (theme) {
|
return switch (theme) {
|
||||||
AsciiColorTheme.green => const AsciiThemeColors(
|
AsciiColorTheme.green => const AsciiThemeColors(
|
||||||
textColor: Color(0xFF00FF00),
|
textColor: Color(0xFF00FF00),
|
||||||
backgroundColor: Color(0xFF0D0D0D),
|
backgroundColor: Color(0xFF0D0D0D),
|
||||||
),
|
),
|
||||||
AsciiColorTheme.amber => const AsciiThemeColors(
|
AsciiColorTheme.amber => const AsciiThemeColors(
|
||||||
textColor: Color(0xFFFFB000),
|
textColor: Color(0xFFFFB000),
|
||||||
backgroundColor: Color(0xFF1A1000),
|
backgroundColor: Color(0xFF1A1000),
|
||||||
),
|
),
|
||||||
AsciiColorTheme.white => const AsciiThemeColors(
|
AsciiColorTheme.white => const AsciiThemeColors(
|
||||||
textColor: Color(0xFFE0E0E0),
|
textColor: Color(0xFFE0E0E0),
|
||||||
backgroundColor: Color(0xFF121212),
|
backgroundColor: Color(0xFF121212),
|
||||||
),
|
),
|
||||||
AsciiColorTheme.system => brightness == Brightness.dark
|
AsciiColorTheme.system =>
|
||||||
? const AsciiThemeColors(
|
brightness == Brightness.dark
|
||||||
textColor: Color(0xFFE0E0E0),
|
? const AsciiThemeColors(
|
||||||
backgroundColor: Color(0xFF1E1E1E),
|
textColor: Color(0xFFE0E0E0),
|
||||||
)
|
backgroundColor: Color(0xFF1E1E1E),
|
||||||
: const AsciiThemeColors(
|
)
|
||||||
textColor: Color(0xFF1E1E1E),
|
: const AsciiThemeColors(
|
||||||
backgroundColor: Color(0xFFF5F5F5),
|
textColor: Color(0xFF1E1E1E),
|
||||||
),
|
backgroundColor: Color(0xFFF5F5F5),
|
||||||
|
),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -182,93 +180,133 @@ const battleAnimationBeast = AsciiAnimationData(
|
|||||||
frames: [
|
frames: [
|
||||||
// 프레임 1: 대치
|
// 프레임 1: 대치
|
||||||
'''
|
'''
|
||||||
o vs /\\_/\\
|
o /\\_/\\
|
||||||
/|\\ ( o.o )
|
/|\\ ( o.o )
|
||||||
/ \\ > ^ <''',
|
/ \\ > ^ <''',
|
||||||
// 프레임 2: 공격 준비
|
// 프레임 2: 접근
|
||||||
'''
|
'''
|
||||||
o----o /\\_/\\
|
o /\\_/\\
|
||||||
/|\\ ( o.o )
|
/|\\ ( o.o )
|
||||||
/ \\ > ^ <''',
|
/ \\ > ^ <''',
|
||||||
// 프레임 3: 공격 중
|
// 프레임 3: 공격 (근접)
|
||||||
'''
|
'''
|
||||||
o o-----> /\\_/\\
|
o_/ /\\_/\\
|
||||||
/|\\ ( X.X )
|
/| ( >.< )
|
||||||
/ \\ > ^ <''',
|
/ \\ > ^ <''',
|
||||||
// 프레임 4: 히트
|
// 프레임 4: 히트
|
||||||
'''
|
'''
|
||||||
o **** /\\_/\\
|
o **** /\\_/\\
|
||||||
/|\\ *** ( X.X ) ***
|
/|\\ *** ( X.X )
|
||||||
/ \\ > ~ <''',
|
/ \\ > ~ <''',
|
||||||
// 프레임 5: 복귀
|
// 프레임 5: 복귀 (승리 포즈)
|
||||||
'''
|
'''
|
||||||
\\o/ /\\_/\\
|
\\o/ /\\_/\\
|
||||||
| ( -.-)
|
/|\\ ( -.-)
|
||||||
/ \\ > ^ <''',
|
/ \\ > ^ <''',
|
||||||
],
|
],
|
||||||
frameIntervalMs: 220,
|
frameIntervalMs: 220,
|
||||||
);
|
);
|
||||||
|
|
||||||
/// 마을/상점 애니메이션 (심플 3줄 캐릭터)
|
/// 마을/상점 애니메이션 (8줄 x 40자 고정)
|
||||||
const townAnimation = AsciiAnimationData(
|
const townAnimation = AsciiAnimationData(
|
||||||
frames: [
|
frames: [
|
||||||
// 프레임 1: 상점 앞에서 대기
|
// 프레임 1: 상점 앞 대기
|
||||||
'''
|
' \n'
|
||||||
___________ o
|
' ___________ \n'
|
||||||
/ SHOP \\/|\\
|
' / SHOP \\ o \n'
|
||||||
~~|__|____|__|/ \\~~~~~~~~~~~~~''',
|
' | [======] | /|\\ \n'
|
||||||
// 프레임 2: 상점으로 이동
|
' | @@@@ | / \\ \n'
|
||||||
'''
|
' | ITEMS | \n'
|
||||||
___________ o
|
' |___________| \n'
|
||||||
/ SHOP \\/|\\
|
'~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~',
|
||||||
~~|__|____|__|/ \\~~~~~~~~~~~~~''',
|
// 프레임 2: 이동 중
|
||||||
|
' \n'
|
||||||
|
' ___________ \n'
|
||||||
|
' / SHOP \\ o \n'
|
||||||
|
' | [======] | /|\\ \n'
|
||||||
|
' | @@@@ | / \\ \n'
|
||||||
|
' | ITEMS | \n'
|
||||||
|
' |___________| \n'
|
||||||
|
'~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~',
|
||||||
// 프레임 3: 거래 시작
|
// 프레임 3: 거래 시작
|
||||||
'''
|
' \n'
|
||||||
___________ o \$
|
' ___________ \n'
|
||||||
/ SHOP \\/|\\ \$
|
' / SHOP \\ o \$ \n'
|
||||||
~~|__[ @@ ]__|/ \\ \$~~~~~~~~~~~''',
|
' | [======] | /|\\ \$ \n'
|
||||||
|
' | @@@@ | / \\ \$ \n'
|
||||||
|
' | ITEMS | \n'
|
||||||
|
' |___________| \n'
|
||||||
|
'~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~',
|
||||||
// 프레임 4: 거래 중
|
// 프레임 4: 거래 중
|
||||||
'''
|
' \n'
|
||||||
___________ o \$\$
|
' ___________ \n'
|
||||||
/ SHOP \\/|\\ \$\$
|
' / SHOP \\ o \$\$\$ \n'
|
||||||
~~|__[ @@ ]__|/ \\ \$\$~~~~~~~~~~''',
|
' | [<====>] | /|\\ \$\$\$ \n'
|
||||||
|
' | @@@@ | / \\ \n'
|
||||||
|
' | SOLD! | \n'
|
||||||
|
' |___________| \n'
|
||||||
|
'~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~',
|
||||||
// 프레임 5: 거래 완료
|
// 프레임 5: 거래 완료
|
||||||
'''
|
' \n'
|
||||||
___________ \\o/ +
|
' ___________ \n'
|
||||||
/ SHOP \\ | +
|
' / SHOP \\ \\o/ + \n'
|
||||||
~~|__[ @@ ]__|/ \\ +~~~~~~~~~~~''',
|
' | [======] | /|\\ + \n'
|
||||||
|
' | @@@@ | / \\ \n'
|
||||||
|
' | ITEMS | \n'
|
||||||
|
' |___________| \n'
|
||||||
|
'~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~',
|
||||||
],
|
],
|
||||||
frameIntervalMs: 280,
|
frameIntervalMs: 280,
|
||||||
);
|
);
|
||||||
|
|
||||||
/// 걷는 애니메이션 (심플 3줄 캐릭터 + 배경)
|
/// 걷는 애니메이션 (8줄 x 40자 고정)
|
||||||
const walkingAnimation = AsciiAnimationData(
|
const walkingAnimation = AsciiAnimationData(
|
||||||
frames: [
|
frames: [
|
||||||
// 프레임 1: 서있기
|
// 프레임 1: 양발 벌림
|
||||||
'''
|
' \n'
|
||||||
~~~~ o ~~~~
|
' \n'
|
||||||
~~~~~~ /|\\ ~~~~~~
|
' \n'
|
||||||
~~~~~~~~ / \\ ~~~~~~~~''',
|
' \n'
|
||||||
// 프레임 2: 왼발 앞
|
' o \n'
|
||||||
'''
|
' /|\\ \n'
|
||||||
~~~~ o ~~~~
|
' / \\ \n'
|
||||||
~~~~~~ /|\\ ~~~~~~
|
'~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~',
|
||||||
~~~~~~~~ /| ~~~~~~~~''',
|
// 프레임 2: 왼발 앞으로
|
||||||
// 프레임 3: 이동 중
|
' \n'
|
||||||
'''
|
' \n'
|
||||||
~~~~ o ~~~~
|
' \n'
|
||||||
~~~~~~ /|\\ ~~~~~~
|
' \n'
|
||||||
~~~~~~~~ |\\ ~~~~~~~~''',
|
' o \n'
|
||||||
// 프레임 4: 오른발 앞
|
' /|\\ \n'
|
||||||
'''
|
' /| \n'
|
||||||
~~~~ o ~~~~
|
'~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~',
|
||||||
~~~~~~ /|\\ ~~~~~~
|
// 프레임 3: 두 발 모음
|
||||||
~~~~~~~~ |/ ~~~~~~~~''',
|
' \n'
|
||||||
// 프레임 5: 복귀
|
' \n'
|
||||||
'''
|
' \n'
|
||||||
~~~~ o ~~~~
|
' \n'
|
||||||
~~~~~~ /|\\ ~~~~~~
|
' o \n'
|
||||||
~~~~~~~~ / \\ ~~~~~~~~''',
|
' /|\\ \n'
|
||||||
|
' || \n'
|
||||||
|
'~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~',
|
||||||
|
// 프레임 4: 오른발 앞으로
|
||||||
|
' \n'
|
||||||
|
' \n'
|
||||||
|
' \n'
|
||||||
|
' \n'
|
||||||
|
' o \n'
|
||||||
|
' /|\\ \n'
|
||||||
|
' |\\ \n'
|
||||||
|
'~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~',
|
||||||
|
// 프레임 5: 양발 벌림 (복귀)
|
||||||
|
' \n'
|
||||||
|
' \n'
|
||||||
|
' \n'
|
||||||
|
' \n'
|
||||||
|
' o \n'
|
||||||
|
' /|\\ \n'
|
||||||
|
' / \\ \n'
|
||||||
|
'~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~',
|
||||||
],
|
],
|
||||||
frameIntervalMs: 180,
|
frameIntervalMs: 180,
|
||||||
);
|
);
|
||||||
@@ -278,29 +316,29 @@ const battleAnimationInsect = AsciiAnimationData(
|
|||||||
frames: [
|
frames: [
|
||||||
// 프레임 1: 대치
|
// 프레임 1: 대치
|
||||||
'''
|
'''
|
||||||
o vs /\\_/\\
|
o /\\_/\\
|
||||||
/|\\ ( o o )
|
/|\\ ( o o )
|
||||||
/ \\ /|=====|\\''',
|
/ \\ /|=====|\\''',
|
||||||
// 프레임 2: 공격 준비
|
// 프레임 2: 접근
|
||||||
'''
|
'''
|
||||||
o----o /\\_/\\
|
o /\\_/\\
|
||||||
/|\\ ( o o )
|
/|\\ ( o o )
|
||||||
/ \\ /|=====|\\''',
|
/ \\ /|=====|\\''',
|
||||||
// 프레임 3: 공격 중
|
// 프레임 3: 공격 (근접)
|
||||||
'''
|
'''
|
||||||
o o-----> /\\_/\\
|
o_/ /\\_/\\
|
||||||
/|\\ ( X X )
|
/| ( >.< )
|
||||||
/ \\ /|=====|\\''',
|
/ \\ /|=====|\\''',
|
||||||
// 프레임 4: 히트
|
// 프레임 4: 히트
|
||||||
'''
|
'''
|
||||||
o **** /\\_/\\
|
o **** /\\_/\\
|
||||||
/|\\ *** ( X X ) ***
|
/|\\*** ( X X )
|
||||||
/ \\ /|=====|\\''',
|
/ \\ /|=====|\\''',
|
||||||
// 프레임 5: 복귀
|
// 프레임 5: 복귀 (승리 포즈)
|
||||||
'''
|
'''
|
||||||
\\o/ /\\_/\\
|
\\o/ /\\_/\\
|
||||||
| ( - - )
|
/|\\ ( - - )
|
||||||
/ \\ /|=====|\\''',
|
/ \\ /|=====|\\''',
|
||||||
],
|
],
|
||||||
frameIntervalMs: 220,
|
frameIntervalMs: 220,
|
||||||
);
|
);
|
||||||
@@ -310,29 +348,29 @@ const battleAnimationHumanoid = AsciiAnimationData(
|
|||||||
frames: [
|
frames: [
|
||||||
// 프레임 1: 대치
|
// 프레임 1: 대치
|
||||||
'''
|
'''
|
||||||
o vs O
|
o O
|
||||||
/|\\ /|\\
|
/|\\ /|\\
|
||||||
/ \\ / | \\''',
|
/ \\ / | \\''',
|
||||||
// 프레임 2: 공격 준비
|
// 프레임 2: 접근
|
||||||
'''
|
'''
|
||||||
o----o O
|
o O
|
||||||
/|\\ /|\\
|
/|\\ /|\\
|
||||||
/ \\ / | \\''',
|
/ \\ / | \\''',
|
||||||
// 프레임 3: 공격 중
|
// 프레임 3: 공격 (근접)
|
||||||
'''
|
'''
|
||||||
o o-----> O
|
o_/ O
|
||||||
/|\\ X|X
|
/| X|X
|
||||||
/ \\ / | \\''',
|
/ \\ / | \\''',
|
||||||
// 프레임 4: 히트
|
// 프레임 4: 히트
|
||||||
'''
|
'''
|
||||||
o **** O
|
o **** O
|
||||||
/|\\ *** X|X ***
|
/|\\ *** X|X
|
||||||
/ \\ / | \\''',
|
/ \\ / | \\''',
|
||||||
// 프레임 5: 복귀
|
// 프레임 5: 복귀 (승리 포즈)
|
||||||
'''
|
'''
|
||||||
\\o/ O
|
\\o/ O
|
||||||
| /|\\
|
/|\\ /|\\
|
||||||
/ \\ / | \\''',
|
/ \\ / | \\''',
|
||||||
],
|
],
|
||||||
frameIntervalMs: 220,
|
frameIntervalMs: 220,
|
||||||
);
|
);
|
||||||
@@ -342,29 +380,29 @@ const battleAnimationUndead = AsciiAnimationData(
|
|||||||
frames: [
|
frames: [
|
||||||
// 프레임 1: 대치
|
// 프레임 1: 대치
|
||||||
'''
|
'''
|
||||||
o vs .-.
|
o .-.
|
||||||
/|\\ (o.o)
|
/|\\ (o.o)
|
||||||
/ \\ |=|''',
|
/ \\ |=|''',
|
||||||
// 프레임 2: 공격 준비
|
// 프레임 2: 접근
|
||||||
'''
|
'''
|
||||||
o----o .-.
|
o .-.
|
||||||
/|\\ (o.o)
|
/|\\ (o.o)
|
||||||
/ \\ |=|''',
|
/ \\ |=|''',
|
||||||
// 프레임 3: 공격 중
|
// 프레임 3: 공격 (근접)
|
||||||
'''
|
'''
|
||||||
o o-----> .-.
|
o_/ .-.
|
||||||
/|\\ (X.X)
|
/| (>.>)
|
||||||
/ \\ |=|''',
|
/ \\ |=|''',
|
||||||
// 프레임 4: 히트
|
// 프레임 4: 히트
|
||||||
'''
|
'''
|
||||||
o **** .-.
|
o **** .-.
|
||||||
/|\\ *** (X.X) ***
|
/|\\*** (X.X)
|
||||||
/ \\ |~|''',
|
/ \\ |~|''',
|
||||||
// 프레임 5: 복귀
|
// 프레임 5: 복귀 (승리 포즈)
|
||||||
'''
|
'''
|
||||||
\\o/ .-.
|
\\o/ .-.
|
||||||
| (-.-)
|
/|\\ (-.-)
|
||||||
/ \\ |=|''',
|
/ \\ |=|''',
|
||||||
],
|
],
|
||||||
frameIntervalMs: 250,
|
frameIntervalMs: 250,
|
||||||
);
|
);
|
||||||
@@ -374,29 +412,29 @@ const battleAnimationDragon = AsciiAnimationData(
|
|||||||
frames: [
|
frames: [
|
||||||
// 프레임 1: 대치
|
// 프레임 1: 대치
|
||||||
'''
|
'''
|
||||||
o vs __/\\__
|
o __/\\__
|
||||||
/|\\ < (O)(O) >
|
/|\\ < (O)(O) >
|
||||||
/ \\ \\ \\/ /''',
|
/ \\ \\ \\/ /''',
|
||||||
// 프레임 2: 공격 준비
|
// 프레임 2: 접근
|
||||||
'''
|
'''
|
||||||
o----o __/\\__
|
o __/\\__
|
||||||
/|\\ < (O)(O) >
|
/|\\ < (O)(O) >
|
||||||
/ \\ \\ \\/ /''',
|
/ \\ \\ \\/ /''',
|
||||||
// 프레임 3: 공격 중
|
// 프레임 3: 공격 (근접)
|
||||||
'''
|
'''
|
||||||
o o-----> __/\\__
|
o_/ __/\\__
|
||||||
/|\\ < (X)(X) >
|
/| < (X)(X) >
|
||||||
/ \\ \\ \\/ /''',
|
/ \\ \\ \\/ /''',
|
||||||
// 프레임 4: 히트
|
// 프레임 4: 히트
|
||||||
'''
|
'''
|
||||||
o **** __/\\__
|
o **** __/\\__
|
||||||
/|\\ *** < (X)(X) > ***
|
/|\\***< (X)(X) >
|
||||||
/ \\ \\ ~~ /''',
|
/ \\ \\ ~~ /''',
|
||||||
// 프레임 5: 복귀
|
// 프레임 5: 복귀 (승리 포즈)
|
||||||
'''
|
'''
|
||||||
\\o/ __/\\__
|
\\o/ __/\\__
|
||||||
| < (-)(-)>
|
/|\\ < (-)(-)>
|
||||||
/ \\ \\ \\/ /''',
|
/ \\ \\ \\/ /''',
|
||||||
],
|
],
|
||||||
frameIntervalMs: 200,
|
frameIntervalMs: 200,
|
||||||
);
|
);
|
||||||
@@ -406,29 +444,29 @@ const battleAnimationSlime = AsciiAnimationData(
|
|||||||
frames: [
|
frames: [
|
||||||
// 프레임 1: 대치
|
// 프레임 1: 대치
|
||||||
'''
|
'''
|
||||||
o vs .---.
|
o .---.
|
||||||
/|\\ ( o o )
|
/|\\ ( o o )
|
||||||
/ \\ ~~~~~''',
|
/ \\ ~~~~~''',
|
||||||
// 프레임 2: 공격 준비
|
// 프레임 2: 접근
|
||||||
'''
|
'''
|
||||||
o----o .---.
|
o .---.
|
||||||
/|\\ ( o o )
|
/|\\ ( o o )
|
||||||
/ \\ ~~~~~''',
|
/ \\ ~~~~~''',
|
||||||
// 프레임 3: 공격 중
|
// 프레임 3: 공격 (근접)
|
||||||
'''
|
'''
|
||||||
o o-----> .---.
|
o_/ .---.
|
||||||
/|\\ ( X X )
|
/| ( >.< )
|
||||||
/ \\ ~~~~~''',
|
/ \\ ~~~~~''',
|
||||||
// 프레임 4: 히트
|
// 프레임 4: 히트
|
||||||
'''
|
'''
|
||||||
o **** .---.
|
o **** .---.
|
||||||
/|\\ *** ( X X ) ***
|
/|\\** ( X X )
|
||||||
/ \\ ~~~~~''',
|
/ \\ ~~~~~''',
|
||||||
// 프레임 5: 복귀
|
// 프레임 5: 복귀 (승리 포즈)
|
||||||
'''
|
'''
|
||||||
\\o/ .---.
|
\\o/ .---.
|
||||||
| ( - - )
|
/|\\ ( - - )
|
||||||
/ \\ ~~~~~''',
|
/ \\ ~~~~~''',
|
||||||
],
|
],
|
||||||
frameIntervalMs: 280,
|
frameIntervalMs: 280,
|
||||||
);
|
);
|
||||||
@@ -438,29 +476,29 @@ const battleAnimationDemon = AsciiAnimationData(
|
|||||||
frames: [
|
frames: [
|
||||||
// 프레임 1: 대치
|
// 프레임 1: 대치
|
||||||
'''
|
'''
|
||||||
o vs /\\ /\\
|
o /\\ /\\
|
||||||
/|\\ ( o V o )
|
/|\\ ( o V o )
|
||||||
/ \\ \\ ~~~ /''',
|
/ \\ \\ ~~~ /''',
|
||||||
// 프레임 2: 공격 준비
|
// 프레임 2: 접근
|
||||||
'''
|
'''
|
||||||
o----o /\\ /\\
|
o /\\ /\\
|
||||||
/|\\ ( o V o )
|
/|\\ ( o V o )
|
||||||
/ \\ \\ ~~~ /''',
|
/ \\ \\ ~~~ /''',
|
||||||
// 프레임 3: 공격 중
|
// 프레임 3: 공격 (근접)
|
||||||
'''
|
'''
|
||||||
o o-----> /\\ /\\
|
o_/ /\\ /\\
|
||||||
/|\\ ( X V X )
|
/| ( X V X )
|
||||||
/ \\ \\ ~~~ /''',
|
/ \\ \\ ~~~ /''',
|
||||||
// 프레임 4: 히트
|
// 프레임 4: 히트
|
||||||
'''
|
'''
|
||||||
o **** /\\ /\\
|
o ****/\\ /\\
|
||||||
/|\\ *** ( X V X ) ***
|
/|\\** ( X V X )
|
||||||
/ \\ \\ ~~~ /''',
|
/ \\ \\ ~~~ /''',
|
||||||
// 프레임 5: 복귀
|
// 프레임 5: 복귀 (승리 포즈)
|
||||||
'''
|
'''
|
||||||
\\o/ /\\ /\\
|
\\o/ /\\ /\\
|
||||||
| ( - V - )
|
/|\\ ( - V - )
|
||||||
/ \\ \\ ~~~ /''',
|
/ \\ \\ ~~~ /''',
|
||||||
],
|
],
|
||||||
frameIntervalMs: 200,
|
frameIntervalMs: 200,
|
||||||
);
|
);
|
||||||
@@ -478,158 +516,158 @@ AsciiAnimationData getBattleAnimation(MonsterCategory category) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 레벨업 축하 애니메이션
|
/// 레벨업 축하 애니메이션 (8줄 x 40자 고정)
|
||||||
const levelUpAnimation = AsciiAnimationData(
|
const levelUpAnimation = AsciiAnimationData(
|
||||||
frames: [
|
frames: [
|
||||||
// 프레임 1: 시작
|
// 프레임 1: 시작
|
||||||
'''
|
' * * * \n'
|
||||||
* * *
|
' * * * \n'
|
||||||
* * *
|
' \n'
|
||||||
\\O/
|
' \\O/ \n'
|
||||||
* | *
|
' * /|\\ * \n'
|
||||||
/ \\
|
' / \\ \n'
|
||||||
* *
|
' * * \n'
|
||||||
~~~~~~~~~~~~~~~~~~~~~''',
|
'~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~',
|
||||||
// 프레임 2: 별 확산
|
// 프레임 2: 별 확산
|
||||||
'''
|
' * * * \n'
|
||||||
* * *
|
' * * \n'
|
||||||
* *
|
' \n'
|
||||||
* \\O/ *
|
' * \\O/ * \n'
|
||||||
|
|
' /|\\ \n'
|
||||||
* / \\ *
|
' * / \\ * \n'
|
||||||
* *
|
' * * \n'
|
||||||
~~~~~~~~~~~~~~~~~~~~~''',
|
'~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~',
|
||||||
// 프레임 3: 레벨업 텍스트
|
// 프레임 3: 레벨업 텍스트
|
||||||
'''
|
' * L E V E L U P ! * \n'
|
||||||
* L E V E L U P ! *
|
' * * \n'
|
||||||
* *
|
' \n'
|
||||||
* \\O/ *
|
' * \\O/ * \n'
|
||||||
|
|
' /|\\ \n'
|
||||||
* / \\ *
|
' * / \\ * \n'
|
||||||
* *
|
' * * \n'
|
||||||
~~~~~~~~~~~~~~~~~~~~~''',
|
'~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~',
|
||||||
// 프레임 4: 빛나는 캐릭터
|
// 프레임 4: 빛나는 캐릭터
|
||||||
'''
|
' * * * * * \n'
|
||||||
* * * * *
|
' * * \n'
|
||||||
* *
|
' \n'
|
||||||
* \\O/ *
|
' * \\O/ * \n'
|
||||||
* | *
|
' * /|\\ * \n'
|
||||||
* / \\ *
|
' * / \\ * \n'
|
||||||
* * * *
|
' * * * * \n'
|
||||||
~~~~~~~~~~~~~~~~~~~~~''',
|
'~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~',
|
||||||
// 프레임 5: 마무리
|
// 프레임 5: 마무리
|
||||||
'''
|
' + \n'
|
||||||
+
|
' +++ \n'
|
||||||
+++
|
' +++++ \n'
|
||||||
+++++
|
' \\O/ \n'
|
||||||
\\O/
|
' /|\\ \n'
|
||||||
|
|
' / \\ \n'
|
||||||
/ \\
|
' \n'
|
||||||
~~~~~~~~~~~~~~~~~~~~~''',
|
'~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~',
|
||||||
],
|
],
|
||||||
frameIntervalMs: 300,
|
frameIntervalMs: 300,
|
||||||
);
|
);
|
||||||
|
|
||||||
/// 퀘스트 완료 애니메이션
|
/// 퀘스트 완료 애니메이션 (8줄 x 40자 고정)
|
||||||
const questCompleteAnimation = AsciiAnimationData(
|
const questCompleteAnimation = AsciiAnimationData(
|
||||||
frames: [
|
frames: [
|
||||||
// 프레임 1: 퀘스트 깃발
|
// 프레임 1: 퀘스트 깃발
|
||||||
'''
|
' [=======] \n'
|
||||||
[=======]
|
' || || \n'
|
||||||
|| ||
|
' || \\O/ || \n'
|
||||||
|| \\O/ ||
|
' || /|\\ || \n'
|
||||||
|| | ||
|
' || / \\ || \n'
|
||||||
|| / \\ ||
|
' ||_____|| \n'
|
||||||
||_____||
|
' \n'
|
||||||
~~~~~~~~~~~~~~~~~~~''',
|
'~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~',
|
||||||
// 프레임 2: 승리
|
// 프레임 2: 승리
|
||||||
'''
|
' [QUEST!] \n'
|
||||||
[QUEST!]
|
' || || \n'
|
||||||
|| ||
|
' \\\\O// \n'
|
||||||
\\\\O//
|
' /|\\ \n'
|
||||||
\\|/
|
' / \\ \n'
|
||||||
/ \\
|
' ||_____|| \n'
|
||||||
||_____||
|
' \n'
|
||||||
~~~~~~~~~~~~~~~~~~~''',
|
'~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~',
|
||||||
// 프레임 3: 보상
|
// 프레임 3: 보상
|
||||||
'''
|
' COMPLETE! \n'
|
||||||
COMPLETE!
|
' \n'
|
||||||
|
' \\O/ \$\$\$ \n'
|
||||||
\\O/ \$\$\$
|
' /|\\ \$\$\$ \n'
|
||||||
| \$\$\$
|
' / \\ \$\$\$ \n'
|
||||||
/ \\ \$\$\$
|
' \n'
|
||||||
|
' \n'
|
||||||
~~~~~~~~~~~~~~~~~~~''',
|
'~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~',
|
||||||
// 프레임 4: 축하
|
// 프레임 4: 축하
|
||||||
'''
|
' * * * * * \n'
|
||||||
* * * * *
|
' \\O/ \n'
|
||||||
\\O/
|
' /|\\ +EXP \n'
|
||||||
| +EXP
|
' / \\ +GOLD \n'
|
||||||
/ \\ +GOLD
|
' * * * * * \n'
|
||||||
* * * * *
|
' \n'
|
||||||
|
' \n'
|
||||||
~~~~~~~~~~~~~~~~~~~''',
|
'~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~',
|
||||||
// 프레임 5: 마무리
|
// 프레임 5: 마무리
|
||||||
'''
|
' [ VICTORY! ] \n'
|
||||||
[ VICTORY! ]
|
' \n'
|
||||||
|
' \\O/ \n'
|
||||||
\\O/
|
' /|\\ \n'
|
||||||
|
|
' / \\ \n'
|
||||||
/ \\
|
' \n'
|
||||||
|
' \n'
|
||||||
~~~~~~~~~~~~~~~~~~~''',
|
'~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~',
|
||||||
],
|
],
|
||||||
frameIntervalMs: 350,
|
frameIntervalMs: 350,
|
||||||
);
|
);
|
||||||
|
|
||||||
/// Act 완료 애니메이션 (플롯 진행)
|
/// Act 완료 애니메이션 (8줄 x 40자 고정)
|
||||||
const actCompleteAnimation = AsciiAnimationData(
|
const actCompleteAnimation = AsciiAnimationData(
|
||||||
frames: [
|
frames: [
|
||||||
// 프레임 1: 커튼
|
// 프레임 1: 커튼
|
||||||
'''
|
' ______________________________ \n'
|
||||||
____________________
|
' | | \n'
|
||||||
| |
|
' | A C T | \n'
|
||||||
| A C T |
|
' | | \n'
|
||||||
| |
|
' | C O M P L E T E | \n'
|
||||||
| C O M P L E T E |
|
' | | \n'
|
||||||
| |
|
' |______________________________| \n'
|
||||||
|____________________|''',
|
'~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~',
|
||||||
// 프레임 2: 캐릭터 등장
|
// 프레임 2: 캐릭터 등장
|
||||||
'''
|
' ______________________________ \n'
|
||||||
____________________
|
' | * * * * * | \n'
|
||||||
| * * * * * |
|
' | \\O/ | \n'
|
||||||
| \\O/ |
|
' | /|\\ | \n'
|
||||||
| | |
|
' | / \\ | \n'
|
||||||
| / \\ |
|
' | * * * * * | \n'
|
||||||
| * * * * * |
|
' |______________________________| \n'
|
||||||
|____________________|''',
|
'~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~',
|
||||||
// 프레임 3: 플롯 진행 표시
|
// 프레임 3: 플롯 진행 표시
|
||||||
'''
|
' ______________________________ \n'
|
||||||
____________________
|
' | PROLOGUE --> ACT | \n'
|
||||||
| PROLOGUE --> ACT |
|
' | \\O/ | \n'
|
||||||
| \\O/ |
|
' | /|\\ --> | \n'
|
||||||
| | --> |
|
' | / \\ | \n'
|
||||||
| / \\ |
|
' | STORY CONTINUES | \n'
|
||||||
| STORY CONTINUES |
|
' |______________________________| \n'
|
||||||
|____________________|''',
|
'~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~',
|
||||||
// 프레임 4: 축하
|
// 프레임 4: 축하
|
||||||
'''
|
' ______________________________ \n'
|
||||||
____________________
|
' | * * * * * | \n'
|
||||||
| * * * * * |
|
' | * \\O/ * | \n'
|
||||||
| * \\O/ * |
|
' | /|\\ | \n'
|
||||||
| | |
|
' | * / \\ * | \n'
|
||||||
| * / \\ * |
|
' | * * * * * | \n'
|
||||||
| * * * * * |
|
' |______________________________| \n'
|
||||||
|____________________|''',
|
'~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~',
|
||||||
// 프레임 5: 마무리
|
// 프레임 5: 마무리
|
||||||
'''
|
' ______________________________ \n'
|
||||||
____________________
|
' | +---------+ | \n'
|
||||||
| +---------+ |
|
' | | NEXT | | \n'
|
||||||
| | NEXT | |
|
' | | CHAPTER | | \n'
|
||||||
| | CHAPTER | |
|
' | +---------+ | \n'
|
||||||
| +---------+ |
|
' | \\O/ | \n'
|
||||||
| \\O/ |
|
' |______________________________| \n'
|
||||||
|____________________|''',
|
'~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~',
|
||||||
],
|
],
|
||||||
frameIntervalMs: 400,
|
frameIntervalMs: 400,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -61,9 +61,9 @@ const _forestLayers = [
|
|||||||
scrollSpeed: 0.15,
|
scrollSpeed: 0.15,
|
||||||
yStart: 1,
|
yStart: 1,
|
||||||
),
|
),
|
||||||
// 전경 - 풀/바닥
|
// 전경 - 바닥
|
||||||
BackgroundLayer(
|
BackgroundLayer(
|
||||||
lines: [r'____||____||____||____||____||____||'],
|
lines: [r'______________________________________'],
|
||||||
scrollSpeed: 0.3,
|
scrollSpeed: 0.3,
|
||||||
yStart: 7,
|
yStart: 7,
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -74,14 +74,23 @@ class BattleComposer {
|
|||||||
_drawBackgroundLayer(canvas, layer, globalTick);
|
_drawBackgroundLayer(canvas, layer, globalTick);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. 캐릭터 프레임 (정규화하여 왼쪽 정렬)
|
// 3. 캐릭터 프레임 (페이즈에 따라 X 위치 변경 - 근접 전투)
|
||||||
var charFrame = getCharacterFrame(phase, subFrame);
|
var charFrame = getCharacterFrame(phase, subFrame);
|
||||||
if (hasShield) {
|
if (hasShield) {
|
||||||
charFrame = charFrame.withShield();
|
charFrame = charFrame.withShield();
|
||||||
}
|
}
|
||||||
final normalizedChar = _normalizeSprite(charFrame.lines, characterWidth);
|
final normalizedChar = _normalizeSprite(charFrame.lines, characterWidth);
|
||||||
final charY = frameHeight - normalizedChar.length;
|
// 바닥 레이어(Y=7) 위에 서있도록 -1
|
||||||
_overlaySpriteWithSpaces(canvas, normalizedChar, 0, charY);
|
final charY = frameHeight - normalizedChar.length - 1;
|
||||||
|
// 페이즈별 캐릭터 X 위치 (몬스터에게 접근)
|
||||||
|
final charX = switch (phase) {
|
||||||
|
BattlePhase.idle => 0,
|
||||||
|
BattlePhase.prepare => 12,
|
||||||
|
BattlePhase.attack => 24,
|
||||||
|
BattlePhase.hit => 28,
|
||||||
|
BattlePhase.recover => 8,
|
||||||
|
};
|
||||||
|
_overlaySpriteWithSpaces(canvas, normalizedChar, charX, charY);
|
||||||
|
|
||||||
// 4. 몬스터 프레임 (정규화하여 오른쪽 정렬)
|
// 4. 몬스터 프레임 (정규화하여 오른쪽 정렬)
|
||||||
final monsterFrames =
|
final monsterFrames =
|
||||||
@@ -89,22 +98,23 @@ class BattleComposer {
|
|||||||
final monsterFrame = monsterFrames[subFrame % monsterFrames.length];
|
final monsterFrame = monsterFrames[subFrame % monsterFrames.length];
|
||||||
final normalizedMonster = _normalizeSpriteRight(monsterFrame, monsterWidth);
|
final normalizedMonster = _normalizeSpriteRight(monsterFrame, monsterWidth);
|
||||||
final monsterX = frameWidth - monsterWidth;
|
final monsterX = frameWidth - monsterWidth;
|
||||||
final monsterY = frameHeight - normalizedMonster.length;
|
// 바닥 레이어(Y=7) 위에 서있도록 -1
|
||||||
|
final monsterY = frameHeight - normalizedMonster.length - 1;
|
||||||
_overlaySpriteWithSpaces(canvas, normalizedMonster, monsterX, monsterY);
|
_overlaySpriteWithSpaces(canvas, normalizedMonster, monsterX, monsterY);
|
||||||
|
|
||||||
// 5. 멀티라인 이펙트 오버레이 (공격/히트/준비 페이즈)
|
// 5. 멀티라인 이펙트 오버레이 (공격/히트 페이즈)
|
||||||
if (phase == BattlePhase.prepare ||
|
if (phase == BattlePhase.attack || phase == BattlePhase.hit) {
|
||||||
phase == BattlePhase.attack ||
|
|
||||||
phase == BattlePhase.hit) {
|
|
||||||
final effect = getWeaponEffect(weaponCategory);
|
final effect = getWeaponEffect(weaponCategory);
|
||||||
final effectLines = _getEffectLines(effect, phase, subFrame);
|
final effectLines = _getEffectLines(effect, phase, subFrame);
|
||||||
if (effectLines.isNotEmpty) {
|
if (effectLines.isNotEmpty) {
|
||||||
// 이펙트 Y 위치: 캐릭터 팔 높이 (2번째 줄, 몸통) 기준
|
// 이펙트 Y 위치: 캐릭터 팔 높이 (2번째 줄, 몸통) 기준
|
||||||
final effectY = charY + 1;
|
final effectY = charY + 1;
|
||||||
|
// 이펙트 X 위치: 캐릭터 오른쪽에 붙여서 표시
|
||||||
|
final effectX = charX + 6;
|
||||||
for (var i = 0; i < effectLines.length; i++) {
|
for (var i = 0; i < effectLines.length; i++) {
|
||||||
final y = effectY + i;
|
final y = effectY + i;
|
||||||
if (y >= 0 && y < frameHeight && effectLines[i].isNotEmpty) {
|
if (y >= 0 && y < frameHeight && effectLines[i].isNotEmpty) {
|
||||||
_overlayText(canvas, effectLines[i], characterWidth, y);
|
_overlayText(canvas, effectLines[i], effectX, y);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -59,6 +59,7 @@ CharacterFrame getCharacterFrame(BattlePhase phase, int subFrame) {
|
|||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// 대기 프레임 (숨쉬기 애니메이션) - 4프레임, 심플 3줄 스타일, 폭 6자
|
// 대기 프레임 (숨쉬기 애니메이션) - 4프레임, 심플 3줄 스타일, 폭 6자
|
||||||
|
// 구조: [머리, 몸통+팔, 다리]
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
const _idleFrames = [
|
const _idleFrames = [
|
||||||
CharacterFrame([
|
CharacterFrame([
|
||||||
@@ -85,41 +86,38 @@ const _idleFrames = [
|
|||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// 준비 프레임 (무기 들기) - 3프레임, 심플 3줄 스타일, 폭 6자
|
// 준비 프레임 (무기 들기) - 3프레임, 심플 3줄 스타일, 폭 6자
|
||||||
|
// 구조: [머리, 몸통+팔, 다리]
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
const _prepareFrames = [
|
const _prepareFrames = [
|
||||||
CharacterFrame([
|
CharacterFrame([
|
||||||
r' \o ',
|
r' o ',
|
||||||
r' |\ ',
|
r' \|\ ',
|
||||||
r' / \ ',
|
r' / \ ',
|
||||||
]),
|
]),
|
||||||
CharacterFrame([
|
CharacterFrame([
|
||||||
r' _ ',
|
r' o_ ',
|
||||||
r' \o ',
|
r' \| ',
|
||||||
r' / \ ',
|
r' / \ ',
|
||||||
]),
|
]),
|
||||||
CharacterFrame([
|
CharacterFrame([
|
||||||
r' \_ ',
|
r' o/ ',
|
||||||
r' \o/ ',
|
r' \| ',
|
||||||
r' / \ ',
|
r' / \ ',
|
||||||
]),
|
]),
|
||||||
];
|
];
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// 공격 프레임 (전진 + 휘두르기) - 5프레임, 심플 3줄 스타일
|
// 공격 프레임 (전진 + 휘두르기) - 5프레임, 심플 3줄 스타일
|
||||||
|
// 구조: [머리, 몸통+팔+무기, 다리]
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
const _attackFrames = [
|
const _attackFrames = [
|
||||||
CharacterFrame([
|
CharacterFrame([
|
||||||
r' \_/ ',
|
r' o\ ',
|
||||||
r' o ',
|
|
||||||
r' /| ',
|
r' /| ',
|
||||||
|
r' / \ ',
|
||||||
]),
|
]),
|
||||||
CharacterFrame([
|
CharacterFrame([
|
||||||
r' _/ ',
|
r' o- ',
|
||||||
r' o ',
|
|
||||||
r' /|\ ',
|
|
||||||
]),
|
|
||||||
CharacterFrame([
|
|
||||||
r' o-- ',
|
|
||||||
r' /| ',
|
r' /| ',
|
||||||
r' / \ ',
|
r' / \ ',
|
||||||
]),
|
]),
|
||||||
@@ -130,13 +128,19 @@ const _attackFrames = [
|
|||||||
]),
|
]),
|
||||||
CharacterFrame([
|
CharacterFrame([
|
||||||
r' o ',
|
r' o ',
|
||||||
r' /|\_ ',
|
r' /|-=>',
|
||||||
|
r' / \ ',
|
||||||
|
]),
|
||||||
|
CharacterFrame([
|
||||||
|
r' o ',
|
||||||
|
r' /|\ ',
|
||||||
r' / \ ',
|
r' / \ ',
|
||||||
]),
|
]),
|
||||||
];
|
];
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// 히트 프레임 (공격 명중) - 3프레임, 심플 3줄 스타일
|
// 히트 프레임 (공격 명중) - 3프레임, 심플 3줄 스타일
|
||||||
|
// 구조: [머리, 몸통+팔+이펙트, 다리]
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
const _hitFrames = [
|
const _hitFrames = [
|
||||||
CharacterFrame([
|
CharacterFrame([
|
||||||
@@ -158,6 +162,7 @@ const _hitFrames = [
|
|||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// 복귀 프레임 - 3프레임, 심플 3줄 스타일
|
// 복귀 프레임 - 3프레임, 심플 3줄 스타일
|
||||||
|
// 구조: [머리, 몸통+팔, 다리]
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
const _recoverFrames = [
|
const _recoverFrames = [
|
||||||
CharacterFrame([
|
CharacterFrame([
|
||||||
|
|||||||
@@ -2,27 +2,28 @@
|
|||||||
// 몬스터 레벨에 따라 ASCII 아트 크기 결정
|
// 몬스터 레벨에 따라 ASCII 아트 크기 결정
|
||||||
|
|
||||||
/// 몬스터 크기 enum
|
/// 몬스터 크기 enum
|
||||||
|
/// 실제 프레임 줄 수와 일치하도록 설정
|
||||||
enum MonsterSize {
|
enum MonsterSize {
|
||||||
/// 1줄 (레벨 1-5)
|
/// 2줄 (레벨 1-5)
|
||||||
tiny(1),
|
tiny(2),
|
||||||
|
|
||||||
/// 2줄 (레벨 6-10)
|
/// 4줄 (레벨 6-10)
|
||||||
small(2),
|
small(4),
|
||||||
|
|
||||||
/// 3줄 (레벨 11-15)
|
/// 6줄 (레벨 11-15)
|
||||||
medium(3),
|
medium(6),
|
||||||
|
|
||||||
/// 4줄 (레벨 16-25)
|
/// 8줄 (레벨 16-25)
|
||||||
large(4),
|
large(8),
|
||||||
|
|
||||||
/// 5줄 (레벨 26-35)
|
/// 8줄 (레벨 26-35)
|
||||||
huge(5),
|
huge(8),
|
||||||
|
|
||||||
/// 6줄 (레벨 36-50)
|
/// 8줄 (레벨 36-50)
|
||||||
giant(6),
|
giant(8),
|
||||||
|
|
||||||
/// 7줄 (레벨 51+, 보스급)
|
/// 8줄 (레벨 51+, 보스급)
|
||||||
titanic(7);
|
titanic(8);
|
||||||
|
|
||||||
const MonsterSize(this.lines);
|
const MonsterSize(this.lines);
|
||||||
|
|
||||||
|
|||||||
@@ -127,7 +127,9 @@ class ProgressService {
|
|||||||
|
|
||||||
/// Tick the timer loop (equivalent to Timer1Timer in the original code).
|
/// Tick the timer loop (equivalent to Timer1Timer in the original code).
|
||||||
ProgressTickResult tick(GameState state, int elapsedMillis) {
|
ProgressTickResult tick(GameState state, int elapsedMillis) {
|
||||||
final int clamped = elapsedMillis.clamp(0, 100).toInt();
|
// 500ms 제한: 5x 배속 (50ms * 5 = 250ms) + 여유 공간
|
||||||
|
// 원본은 100ms 제한이었으나 배속 기능 지원을 위해 확장
|
||||||
|
final int clamped = elapsedMillis.clamp(0, 500).toInt();
|
||||||
var progress = state.progress;
|
var progress = state.progress;
|
||||||
var queue = state.queue;
|
var queue = state.queue;
|
||||||
var nextState = state;
|
var nextState = state;
|
||||||
|
|||||||
@@ -226,6 +226,11 @@ class _GamePlayScreenState extends State<GamePlayScreen>
|
|||||||
},
|
},
|
||||||
colorTheme: _colorTheme,
|
colorTheme: _colorTheme,
|
||||||
onThemeCycle: _cycleColorTheme,
|
onThemeCycle: _cycleColorTheme,
|
||||||
|
isPaused: !widget.controller.isRunning,
|
||||||
|
onPauseToggle: () async {
|
||||||
|
await widget.controller.togglePause();
|
||||||
|
setState(() {});
|
||||||
|
},
|
||||||
specialAnimation: _specialAnimation,
|
specialAnimation: _specialAnimation,
|
||||||
weaponName: state.equipment.weapon,
|
weaponName: state.equipment.weapon,
|
||||||
shieldName: state.equipment.shield,
|
shieldName: state.equipment.shield,
|
||||||
|
|||||||
@@ -104,6 +104,21 @@ class GameSessionController extends ChangeNotifier {
|
|||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 일시 정지 상태에서 재개
|
||||||
|
Future<void> resume() async {
|
||||||
|
if (_state == null || _status != GameSessionStatus.idle) return;
|
||||||
|
await startNew(_state!, cheatsEnabled: _cheatsEnabled, isNewGame: false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 일시 정지/재개 토글
|
||||||
|
Future<void> togglePause() async {
|
||||||
|
if (isRunning) {
|
||||||
|
await pause(saveOnStop: true);
|
||||||
|
} else if (_state != null && _status == GameSessionStatus.idle) {
|
||||||
|
await resume();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
final stop = _stopLoop(saveOnStop: false);
|
final stop = _stopLoop(saveOnStop: false);
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ 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/background_layer.dart';
|
||||||
import 'package:askiineverdie/src/core/animation/battle_composer.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/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/monster_size.dart';
|
||||||
import 'package:askiineverdie/src/core/animation/weapon_category.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';
|
||||||
@@ -230,6 +229,54 @@ class _AsciiAnimationCardState extends State<AsciiAnimationCard> {
|
|||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 이펙트 문자에 색상을 적용한 TextSpan 생성
|
||||||
|
TextSpan _buildColoredTextSpan(String text, TextStyle baseStyle) {
|
||||||
|
final spans = <TextSpan>[];
|
||||||
|
final buffer = StringBuffer();
|
||||||
|
|
||||||
|
// 이펙트 문자 정의
|
||||||
|
const effectChars = {'*', '!', '=', '>', '<', '~'};
|
||||||
|
|
||||||
|
for (var i = 0; i < text.length; i++) {
|
||||||
|
final char = text[i];
|
||||||
|
|
||||||
|
if (effectChars.contains(char)) {
|
||||||
|
// 버퍼에 쌓인 일반 텍스트 추가
|
||||||
|
if (buffer.isNotEmpty) {
|
||||||
|
spans.add(TextSpan(text: buffer.toString(), style: baseStyle));
|
||||||
|
buffer.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 이펙트 문자에 색상 적용
|
||||||
|
final effectColor = _getEffectColor(char);
|
||||||
|
spans.add(TextSpan(
|
||||||
|
text: char,
|
||||||
|
style: baseStyle.copyWith(color: effectColor),
|
||||||
|
));
|
||||||
|
} else {
|
||||||
|
buffer.write(char);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 남은 일반 텍스트 추가
|
||||||
|
if (buffer.isNotEmpty) {
|
||||||
|
spans.add(TextSpan(text: buffer.toString(), style: baseStyle));
|
||||||
|
}
|
||||||
|
|
||||||
|
return TextSpan(children: spans);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 이펙트 문자별 색상 반환
|
||||||
|
Color _getEffectColor(String char) {
|
||||||
|
return switch (char) {
|
||||||
|
'*' => Colors.orange, // 히트/폭발
|
||||||
|
'!' => Colors.yellow, // 강조
|
||||||
|
'=' || '>' || '<' => Colors.cyan, // 슬래시/찌르기
|
||||||
|
'~' => Colors.purple, // 물결/마법
|
||||||
|
_ => Colors.white,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final brightness = Theme.of(context).brightness;
|
final brightness = Theme.of(context).brightness;
|
||||||
@@ -254,13 +301,8 @@ class _AsciiAnimationCardState extends State<AsciiAnimationCard> {
|
|||||||
_environment,
|
_environment,
|
||||||
_globalTick,
|
_globalTick,
|
||||||
);
|
);
|
||||||
|
// 이펙트는 텍스트 자체로 구분 (*, !, =, ~ 등)
|
||||||
// 히트 페이즈면 몬스터 색상 변경
|
// 전체 색상 변경 제거 - 기본 테마 색상 유지
|
||||||
if (_battlePhase == BattlePhase.hit) {
|
|
||||||
final monsterColorCategory =
|
|
||||||
getMonsterColorCategory(widget.monsterBaseName);
|
|
||||||
textColor = getMonsterColors(monsterColorCategory).hit;
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
// 기존 레거시 시스템 사용
|
// 기존 레거시 시스템 사용
|
||||||
final frameIndex =
|
final frameIndex =
|
||||||
@@ -293,14 +335,16 @@ class _AsciiAnimationCardState extends State<AsciiAnimationCard> {
|
|||||||
.clamp(6.0, 14.0);
|
.clamp(6.0, 14.0);
|
||||||
|
|
||||||
return Center(
|
return Center(
|
||||||
child: Text(
|
child: RichText(
|
||||||
frameText,
|
text: _buildColoredTextSpan(
|
||||||
style: TextStyle(
|
frameText,
|
||||||
fontFamily: 'Courier',
|
TextStyle(
|
||||||
fontSize: fontSize,
|
fontFamily: 'Courier',
|
||||||
color: textColor,
|
fontSize: fontSize,
|
||||||
height: 1.2,
|
color: textColor,
|
||||||
letterSpacing: 0,
|
height: 1.2,
|
||||||
|
letterSpacing: 0,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
textAlign: TextAlign.left,
|
textAlign: TextAlign.left,
|
||||||
),
|
),
|
||||||
@@ -317,7 +361,7 @@ class _AsciiAnimationCardState extends State<AsciiAnimationCard> {
|
|||||||
height: 1.1,
|
height: 1.1,
|
||||||
letterSpacing: 0,
|
letterSpacing: 0,
|
||||||
),
|
),
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.left,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -15,6 +15,8 @@ class TaskProgressPanel extends StatelessWidget {
|
|||||||
required this.onSpeedCycle,
|
required this.onSpeedCycle,
|
||||||
required this.colorTheme,
|
required this.colorTheme,
|
||||||
required this.onThemeCycle,
|
required this.onThemeCycle,
|
||||||
|
required this.isPaused,
|
||||||
|
required this.onPauseToggle,
|
||||||
this.specialAnimation,
|
this.specialAnimation,
|
||||||
this.weaponName,
|
this.weaponName,
|
||||||
this.shieldName,
|
this.shieldName,
|
||||||
@@ -28,6 +30,10 @@ class TaskProgressPanel extends StatelessWidget {
|
|||||||
final AsciiColorTheme colorTheme;
|
final AsciiColorTheme colorTheme;
|
||||||
final VoidCallback onThemeCycle;
|
final VoidCallback onThemeCycle;
|
||||||
|
|
||||||
|
/// 일시 정지 상태
|
||||||
|
final bool isPaused;
|
||||||
|
final VoidCallback onPauseToggle;
|
||||||
|
|
||||||
/// 특수 애니메이션 (레벨업, 퀘스트 완료 등)
|
/// 특수 애니메이션 (레벨업, 퀘스트 완료 등)
|
||||||
final AsciiAnimationType? specialAnimation;
|
final AsciiAnimationType? specialAnimation;
|
||||||
|
|
||||||
@@ -70,6 +76,8 @@ class TaskProgressPanel extends StatelessWidget {
|
|||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
_buildThemeButton(context),
|
_buildThemeButton(context),
|
||||||
|
const SizedBox(width: 4),
|
||||||
|
_buildPauseButton(context),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Text(
|
child: Text(
|
||||||
@@ -128,6 +136,29 @@ class TaskProgressPanel extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Widget _buildPauseButton(BuildContext context) {
|
||||||
|
return SizedBox(
|
||||||
|
height: 28,
|
||||||
|
child: OutlinedButton(
|
||||||
|
onPressed: onPauseToggle,
|
||||||
|
style: OutlinedButton.styleFrom(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||||
|
visualDensity: VisualDensity.compact,
|
||||||
|
side: BorderSide(
|
||||||
|
color: isPaused
|
||||||
|
? Colors.orange.withValues(alpha: 0.7)
|
||||||
|
: Theme.of(context).colorScheme.outline.withValues(alpha: 0.5),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Icon(
|
||||||
|
isPaused ? Icons.play_arrow : Icons.pause,
|
||||||
|
size: 16,
|
||||||
|
color: isPaused ? Colors.orange : null,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
Widget _buildSpeedButton(BuildContext context) {
|
Widget _buildSpeedButton(BuildContext context) {
|
||||||
return SizedBox(
|
return SizedBox(
|
||||||
height: 28,
|
height: 28,
|
||||||
|
|||||||
Reference in New Issue
Block a user