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 애니메이션 프레임 데이터
|
||||
class AsciiAnimationData {
|
||||
const AsciiAnimationData({
|
||||
required this.frames,
|
||||
this.frameIntervalMs = 200,
|
||||
});
|
||||
const AsciiAnimationData({required this.frames, this.frameIntervalMs = 200});
|
||||
|
||||
/// 각 프레임 (문자열, 최소 5줄)
|
||||
final List<String> frames;
|
||||
@@ -46,26 +43,27 @@ class AsciiThemeColors {
|
||||
AsciiThemeColors getThemeColors(AsciiColorTheme theme, Brightness brightness) {
|
||||
return switch (theme) {
|
||||
AsciiColorTheme.green => const AsciiThemeColors(
|
||||
textColor: Color(0xFF00FF00),
|
||||
backgroundColor: Color(0xFF0D0D0D),
|
||||
),
|
||||
textColor: Color(0xFF00FF00),
|
||||
backgroundColor: Color(0xFF0D0D0D),
|
||||
),
|
||||
AsciiColorTheme.amber => const AsciiThemeColors(
|
||||
textColor: Color(0xFFFFB000),
|
||||
backgroundColor: Color(0xFF1A1000),
|
||||
),
|
||||
textColor: Color(0xFFFFB000),
|
||||
backgroundColor: Color(0xFF1A1000),
|
||||
),
|
||||
AsciiColorTheme.white => const AsciiThemeColors(
|
||||
textColor: Color(0xFFE0E0E0),
|
||||
backgroundColor: Color(0xFF121212),
|
||||
),
|
||||
AsciiColorTheme.system => brightness == Brightness.dark
|
||||
? const AsciiThemeColors(
|
||||
textColor: Color(0xFFE0E0E0),
|
||||
backgroundColor: Color(0xFF1E1E1E),
|
||||
)
|
||||
: const AsciiThemeColors(
|
||||
textColor: Color(0xFF1E1E1E),
|
||||
backgroundColor: Color(0xFFF5F5F5),
|
||||
),
|
||||
textColor: Color(0xFFE0E0E0),
|
||||
backgroundColor: Color(0xFF121212),
|
||||
),
|
||||
AsciiColorTheme.system =>
|
||||
brightness == Brightness.dark
|
||||
? const AsciiThemeColors(
|
||||
textColor: Color(0xFFE0E0E0),
|
||||
backgroundColor: Color(0xFF1E1E1E),
|
||||
)
|
||||
: const AsciiThemeColors(
|
||||
textColor: Color(0xFF1E1E1E),
|
||||
backgroundColor: Color(0xFFF5F5F5),
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -182,93 +180,133 @@ const battleAnimationBeast = AsciiAnimationData(
|
||||
frames: [
|
||||
// 프레임 1: 대치
|
||||
'''
|
||||
o vs /\\_/\\
|
||||
/|\\ ( o.o )
|
||||
/ \\ > ^ <''',
|
||||
// 프레임 2: 공격 준비
|
||||
o /\\_/\\
|
||||
/|\\ ( o.o )
|
||||
/ \\ > ^ <''',
|
||||
// 프레임 2: 접근
|
||||
'''
|
||||
o----o /\\_/\\
|
||||
/|\\ ( o.o )
|
||||
/ \\ > ^ <''',
|
||||
// 프레임 3: 공격 중
|
||||
o /\\_/\\
|
||||
/|\\ ( o.o )
|
||||
/ \\ > ^ <''',
|
||||
// 프레임 3: 공격 (근접)
|
||||
'''
|
||||
o o-----> /\\_/\\
|
||||
/|\\ ( X.X )
|
||||
/ \\ > ^ <''',
|
||||
o_/ /\\_/\\
|
||||
/| ( >.< )
|
||||
/ \\ > ^ <''',
|
||||
// 프레임 4: 히트
|
||||
'''
|
||||
o **** /\\_/\\
|
||||
/|\\ *** ( X.X ) ***
|
||||
/ \\ > ~ <''',
|
||||
// 프레임 5: 복귀
|
||||
o **** /\\_/\\
|
||||
/|\\ *** ( X.X )
|
||||
/ \\ > ~ <''',
|
||||
// 프레임 5: 복귀 (승리 포즈)
|
||||
'''
|
||||
\\o/ /\\_/\\
|
||||
| ( -.-)
|
||||
/ \\ > ^ <''',
|
||||
\\o/ /\\_/\\
|
||||
/|\\ ( -.-)
|
||||
/ \\ > ^ <''',
|
||||
],
|
||||
frameIntervalMs: 220,
|
||||
);
|
||||
|
||||
/// 마을/상점 애니메이션 (심플 3줄 캐릭터)
|
||||
/// 마을/상점 애니메이션 (8줄 x 40자 고정)
|
||||
const townAnimation = AsciiAnimationData(
|
||||
frames: [
|
||||
// 프레임 1: 상점 앞에서 대기
|
||||
'''
|
||||
___________ o
|
||||
/ SHOP \\/|\\
|
||||
~~|__|____|__|/ \\~~~~~~~~~~~~~''',
|
||||
// 프레임 2: 상점으로 이동
|
||||
'''
|
||||
___________ o
|
||||
/ SHOP \\/|\\
|
||||
~~|__|____|__|/ \\~~~~~~~~~~~~~''',
|
||||
// 프레임 1: 상점 앞 대기
|
||||
' \n'
|
||||
' ___________ \n'
|
||||
' / SHOP \\ o \n'
|
||||
' | [======] | /|\\ \n'
|
||||
' | @@@@ | / \\ \n'
|
||||
' | ITEMS | \n'
|
||||
' |___________| \n'
|
||||
'~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~',
|
||||
// 프레임 2: 이동 중
|
||||
' \n'
|
||||
' ___________ \n'
|
||||
' / SHOP \\ o \n'
|
||||
' | [======] | /|\\ \n'
|
||||
' | @@@@ | / \\ \n'
|
||||
' | ITEMS | \n'
|
||||
' |___________| \n'
|
||||
'~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~',
|
||||
// 프레임 3: 거래 시작
|
||||
'''
|
||||
___________ o \$
|
||||
/ SHOP \\/|\\ \$
|
||||
~~|__[ @@ ]__|/ \\ \$~~~~~~~~~~~''',
|
||||
' \n'
|
||||
' ___________ \n'
|
||||
' / SHOP \\ o \$ \n'
|
||||
' | [======] | /|\\ \$ \n'
|
||||
' | @@@@ | / \\ \$ \n'
|
||||
' | ITEMS | \n'
|
||||
' |___________| \n'
|
||||
'~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~',
|
||||
// 프레임 4: 거래 중
|
||||
'''
|
||||
___________ o \$\$
|
||||
/ SHOP \\/|\\ \$\$
|
||||
~~|__[ @@ ]__|/ \\ \$\$~~~~~~~~~~''',
|
||||
' \n'
|
||||
' ___________ \n'
|
||||
' / SHOP \\ o \$\$\$ \n'
|
||||
' | [<====>] | /|\\ \$\$\$ \n'
|
||||
' | @@@@ | / \\ \n'
|
||||
' | SOLD! | \n'
|
||||
' |___________| \n'
|
||||
'~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~',
|
||||
// 프레임 5: 거래 완료
|
||||
'''
|
||||
___________ \\o/ +
|
||||
/ SHOP \\ | +
|
||||
~~|__[ @@ ]__|/ \\ +~~~~~~~~~~~''',
|
||||
' \n'
|
||||
' ___________ \n'
|
||||
' / SHOP \\ \\o/ + \n'
|
||||
' | [======] | /|\\ + \n'
|
||||
' | @@@@ | / \\ \n'
|
||||
' | ITEMS | \n'
|
||||
' |___________| \n'
|
||||
'~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~',
|
||||
],
|
||||
frameIntervalMs: 280,
|
||||
);
|
||||
|
||||
/// 걷는 애니메이션 (심플 3줄 캐릭터 + 배경)
|
||||
/// 걷는 애니메이션 (8줄 x 40자 고정)
|
||||
const walkingAnimation = AsciiAnimationData(
|
||||
frames: [
|
||||
// 프레임 1: 서있기
|
||||
'''
|
||||
~~~~ o ~~~~
|
||||
~~~~~~ /|\\ ~~~~~~
|
||||
~~~~~~~~ / \\ ~~~~~~~~''',
|
||||
// 프레임 2: 왼발 앞
|
||||
'''
|
||||
~~~~ o ~~~~
|
||||
~~~~~~ /|\\ ~~~~~~
|
||||
~~~~~~~~ /| ~~~~~~~~''',
|
||||
// 프레임 3: 이동 중
|
||||
'''
|
||||
~~~~ o ~~~~
|
||||
~~~~~~ /|\\ ~~~~~~
|
||||
~~~~~~~~ |\\ ~~~~~~~~''',
|
||||
// 프레임 4: 오른발 앞
|
||||
'''
|
||||
~~~~ o ~~~~
|
||||
~~~~~~ /|\\ ~~~~~~
|
||||
~~~~~~~~ |/ ~~~~~~~~''',
|
||||
// 프레임 5: 복귀
|
||||
'''
|
||||
~~~~ o ~~~~
|
||||
~~~~~~ /|\\ ~~~~~~
|
||||
~~~~~~~~ / \\ ~~~~~~~~''',
|
||||
// 프레임 1: 양발 벌림
|
||||
' \n'
|
||||
' \n'
|
||||
' \n'
|
||||
' \n'
|
||||
' o \n'
|
||||
' /|\\ \n'
|
||||
' / \\ \n'
|
||||
'~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~',
|
||||
// 프레임 2: 왼발 앞으로
|
||||
' \n'
|
||||
' \n'
|
||||
' \n'
|
||||
' \n'
|
||||
' o \n'
|
||||
' /|\\ \n'
|
||||
' /| \n'
|
||||
'~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~',
|
||||
// 프레임 3: 두 발 모음
|
||||
' \n'
|
||||
' \n'
|
||||
' \n'
|
||||
' \n'
|
||||
' o \n'
|
||||
' /|\\ \n'
|
||||
' || \n'
|
||||
'~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~',
|
||||
// 프레임 4: 오른발 앞으로
|
||||
' \n'
|
||||
' \n'
|
||||
' \n'
|
||||
' \n'
|
||||
' o \n'
|
||||
' /|\\ \n'
|
||||
' |\\ \n'
|
||||
'~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~',
|
||||
// 프레임 5: 양발 벌림 (복귀)
|
||||
' \n'
|
||||
' \n'
|
||||
' \n'
|
||||
' \n'
|
||||
' o \n'
|
||||
' /|\\ \n'
|
||||
' / \\ \n'
|
||||
'~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~',
|
||||
],
|
||||
frameIntervalMs: 180,
|
||||
);
|
||||
@@ -278,29 +316,29 @@ const battleAnimationInsect = AsciiAnimationData(
|
||||
frames: [
|
||||
// 프레임 1: 대치
|
||||
'''
|
||||
o vs /\\_/\\
|
||||
/|\\ ( o o )
|
||||
/ \\ /|=====|\\''',
|
||||
// 프레임 2: 공격 준비
|
||||
o /\\_/\\
|
||||
/|\\ ( o o )
|
||||
/ \\ /|=====|\\''',
|
||||
// 프레임 2: 접근
|
||||
'''
|
||||
o----o /\\_/\\
|
||||
/|\\ ( o o )
|
||||
/ \\ /|=====|\\''',
|
||||
// 프레임 3: 공격 중
|
||||
o /\\_/\\
|
||||
/|\\ ( o o )
|
||||
/ \\ /|=====|\\''',
|
||||
// 프레임 3: 공격 (근접)
|
||||
'''
|
||||
o o-----> /\\_/\\
|
||||
/|\\ ( X X )
|
||||
/ \\ /|=====|\\''',
|
||||
o_/ /\\_/\\
|
||||
/| ( >.< )
|
||||
/ \\ /|=====|\\''',
|
||||
// 프레임 4: 히트
|
||||
'''
|
||||
o **** /\\_/\\
|
||||
/|\\ *** ( X X ) ***
|
||||
/ \\ /|=====|\\''',
|
||||
// 프레임 5: 복귀
|
||||
o **** /\\_/\\
|
||||
/|\\*** ( X X )
|
||||
/ \\ /|=====|\\''',
|
||||
// 프레임 5: 복귀 (승리 포즈)
|
||||
'''
|
||||
\\o/ /\\_/\\
|
||||
| ( - - )
|
||||
/ \\ /|=====|\\''',
|
||||
\\o/ /\\_/\\
|
||||
/|\\ ( - - )
|
||||
/ \\ /|=====|\\''',
|
||||
],
|
||||
frameIntervalMs: 220,
|
||||
);
|
||||
@@ -310,29 +348,29 @@ const battleAnimationHumanoid = AsciiAnimationData(
|
||||
frames: [
|
||||
// 프레임 1: 대치
|
||||
'''
|
||||
o vs O
|
||||
/|\\ /|\\
|
||||
/ \\ / | \\''',
|
||||
// 프레임 2: 공격 준비
|
||||
o O
|
||||
/|\\ /|\\
|
||||
/ \\ / | \\''',
|
||||
// 프레임 2: 접근
|
||||
'''
|
||||
o----o O
|
||||
/|\\ /|\\
|
||||
/ \\ / | \\''',
|
||||
// 프레임 3: 공격 중
|
||||
o O
|
||||
/|\\ /|\\
|
||||
/ \\ / | \\''',
|
||||
// 프레임 3: 공격 (근접)
|
||||
'''
|
||||
o o-----> O
|
||||
/|\\ X|X
|
||||
/ \\ / | \\''',
|
||||
o_/ O
|
||||
/| X|X
|
||||
/ \\ / | \\''',
|
||||
// 프레임 4: 히트
|
||||
'''
|
||||
o **** O
|
||||
/|\\ *** X|X ***
|
||||
/ \\ / | \\''',
|
||||
// 프레임 5: 복귀
|
||||
o **** O
|
||||
/|\\ *** X|X
|
||||
/ \\ / | \\''',
|
||||
// 프레임 5: 복귀 (승리 포즈)
|
||||
'''
|
||||
\\o/ O
|
||||
| /|\\
|
||||
/ \\ / | \\''',
|
||||
\\o/ O
|
||||
/|\\ /|\\
|
||||
/ \\ / | \\''',
|
||||
],
|
||||
frameIntervalMs: 220,
|
||||
);
|
||||
@@ -342,29 +380,29 @@ const battleAnimationUndead = AsciiAnimationData(
|
||||
frames: [
|
||||
// 프레임 1: 대치
|
||||
'''
|
||||
o vs .-.
|
||||
/|\\ (o.o)
|
||||
/ \\ |=|''',
|
||||
// 프레임 2: 공격 준비
|
||||
o .-.
|
||||
/|\\ (o.o)
|
||||
/ \\ |=|''',
|
||||
// 프레임 2: 접근
|
||||
'''
|
||||
o----o .-.
|
||||
/|\\ (o.o)
|
||||
/ \\ |=|''',
|
||||
// 프레임 3: 공격 중
|
||||
o .-.
|
||||
/|\\ (o.o)
|
||||
/ \\ |=|''',
|
||||
// 프레임 3: 공격 (근접)
|
||||
'''
|
||||
o o-----> .-.
|
||||
/|\\ (X.X)
|
||||
/ \\ |=|''',
|
||||
o_/ .-.
|
||||
/| (>.>)
|
||||
/ \\ |=|''',
|
||||
// 프레임 4: 히트
|
||||
'''
|
||||
o **** .-.
|
||||
/|\\ *** (X.X) ***
|
||||
/ \\ |~|''',
|
||||
// 프레임 5: 복귀
|
||||
o **** .-.
|
||||
/|\\*** (X.X)
|
||||
/ \\ |~|''',
|
||||
// 프레임 5: 복귀 (승리 포즈)
|
||||
'''
|
||||
\\o/ .-.
|
||||
| (-.-)
|
||||
/ \\ |=|''',
|
||||
\\o/ .-.
|
||||
/|\\ (-.-)
|
||||
/ \\ |=|''',
|
||||
],
|
||||
frameIntervalMs: 250,
|
||||
);
|
||||
@@ -374,29 +412,29 @@ const battleAnimationDragon = AsciiAnimationData(
|
||||
frames: [
|
||||
// 프레임 1: 대치
|
||||
'''
|
||||
o vs __/\\__
|
||||
/|\\ < (O)(O) >
|
||||
/ \\ \\ \\/ /''',
|
||||
// 프레임 2: 공격 준비
|
||||
o __/\\__
|
||||
/|\\ < (O)(O) >
|
||||
/ \\ \\ \\/ /''',
|
||||
// 프레임 2: 접근
|
||||
'''
|
||||
o----o __/\\__
|
||||
/|\\ < (O)(O) >
|
||||
/ \\ \\ \\/ /''',
|
||||
// 프레임 3: 공격 중
|
||||
o __/\\__
|
||||
/|\\ < (O)(O) >
|
||||
/ \\ \\ \\/ /''',
|
||||
// 프레임 3: 공격 (근접)
|
||||
'''
|
||||
o o-----> __/\\__
|
||||
/|\\ < (X)(X) >
|
||||
/ \\ \\ \\/ /''',
|
||||
o_/ __/\\__
|
||||
/| < (X)(X) >
|
||||
/ \\ \\ \\/ /''',
|
||||
// 프레임 4: 히트
|
||||
'''
|
||||
o **** __/\\__
|
||||
/|\\ *** < (X)(X) > ***
|
||||
/ \\ \\ ~~ /''',
|
||||
// 프레임 5: 복귀
|
||||
o **** __/\\__
|
||||
/|\\***< (X)(X) >
|
||||
/ \\ \\ ~~ /''',
|
||||
// 프레임 5: 복귀 (승리 포즈)
|
||||
'''
|
||||
\\o/ __/\\__
|
||||
| < (-)(-)>
|
||||
/ \\ \\ \\/ /''',
|
||||
\\o/ __/\\__
|
||||
/|\\ < (-)(-)>
|
||||
/ \\ \\ \\/ /''',
|
||||
],
|
||||
frameIntervalMs: 200,
|
||||
);
|
||||
@@ -406,29 +444,29 @@ const battleAnimationSlime = AsciiAnimationData(
|
||||
frames: [
|
||||
// 프레임 1: 대치
|
||||
'''
|
||||
o vs .---.
|
||||
/|\\ ( o o )
|
||||
/ \\ ~~~~~''',
|
||||
// 프레임 2: 공격 준비
|
||||
o .---.
|
||||
/|\\ ( o o )
|
||||
/ \\ ~~~~~''',
|
||||
// 프레임 2: 접근
|
||||
'''
|
||||
o----o .---.
|
||||
/|\\ ( o o )
|
||||
/ \\ ~~~~~''',
|
||||
// 프레임 3: 공격 중
|
||||
o .---.
|
||||
/|\\ ( o o )
|
||||
/ \\ ~~~~~''',
|
||||
// 프레임 3: 공격 (근접)
|
||||
'''
|
||||
o o-----> .---.
|
||||
/|\\ ( X X )
|
||||
/ \\ ~~~~~''',
|
||||
o_/ .---.
|
||||
/| ( >.< )
|
||||
/ \\ ~~~~~''',
|
||||
// 프레임 4: 히트
|
||||
'''
|
||||
o **** .---.
|
||||
/|\\ *** ( X X ) ***
|
||||
/ \\ ~~~~~''',
|
||||
// 프레임 5: 복귀
|
||||
o **** .---.
|
||||
/|\\** ( X X )
|
||||
/ \\ ~~~~~''',
|
||||
// 프레임 5: 복귀 (승리 포즈)
|
||||
'''
|
||||
\\o/ .---.
|
||||
| ( - - )
|
||||
/ \\ ~~~~~''',
|
||||
\\o/ .---.
|
||||
/|\\ ( - - )
|
||||
/ \\ ~~~~~''',
|
||||
],
|
||||
frameIntervalMs: 280,
|
||||
);
|
||||
@@ -438,29 +476,29 @@ const battleAnimationDemon = AsciiAnimationData(
|
||||
frames: [
|
||||
// 프레임 1: 대치
|
||||
'''
|
||||
o vs /\\ /\\
|
||||
/|\\ ( o V o )
|
||||
/ \\ \\ ~~~ /''',
|
||||
// 프레임 2: 공격 준비
|
||||
o /\\ /\\
|
||||
/|\\ ( o V o )
|
||||
/ \\ \\ ~~~ /''',
|
||||
// 프레임 2: 접근
|
||||
'''
|
||||
o----o /\\ /\\
|
||||
/|\\ ( o V o )
|
||||
/ \\ \\ ~~~ /''',
|
||||
// 프레임 3: 공격 중
|
||||
o /\\ /\\
|
||||
/|\\ ( o V o )
|
||||
/ \\ \\ ~~~ /''',
|
||||
// 프레임 3: 공격 (근접)
|
||||
'''
|
||||
o o-----> /\\ /\\
|
||||
/|\\ ( X V X )
|
||||
/ \\ \\ ~~~ /''',
|
||||
o_/ /\\ /\\
|
||||
/| ( X V X )
|
||||
/ \\ \\ ~~~ /''',
|
||||
// 프레임 4: 히트
|
||||
'''
|
||||
o **** /\\ /\\
|
||||
/|\\ *** ( X V X ) ***
|
||||
/ \\ \\ ~~~ /''',
|
||||
// 프레임 5: 복귀
|
||||
o ****/\\ /\\
|
||||
/|\\** ( X V X )
|
||||
/ \\ \\ ~~~ /''',
|
||||
// 프레임 5: 복귀 (승리 포즈)
|
||||
'''
|
||||
\\o/ /\\ /\\
|
||||
| ( - V - )
|
||||
/ \\ \\ ~~~ /''',
|
||||
\\o/ /\\ /\\
|
||||
/|\\ ( - V - )
|
||||
/ \\ \\ ~~~ /''',
|
||||
],
|
||||
frameIntervalMs: 200,
|
||||
);
|
||||
@@ -478,158 +516,158 @@ AsciiAnimationData getBattleAnimation(MonsterCategory category) {
|
||||
};
|
||||
}
|
||||
|
||||
/// 레벨업 축하 애니메이션
|
||||
/// 레벨업 축하 애니메이션 (8줄 x 40자 고정)
|
||||
const levelUpAnimation = AsciiAnimationData(
|
||||
frames: [
|
||||
// 프레임 1: 시작
|
||||
'''
|
||||
* * *
|
||||
* * *
|
||||
\\O/
|
||||
* | *
|
||||
/ \\
|
||||
* *
|
||||
~~~~~~~~~~~~~~~~~~~~~''',
|
||||
' * * * \n'
|
||||
' * * * \n'
|
||||
' \n'
|
||||
' \\O/ \n'
|
||||
' * /|\\ * \n'
|
||||
' / \\ \n'
|
||||
' * * \n'
|
||||
'~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~',
|
||||
// 프레임 2: 별 확산
|
||||
'''
|
||||
* * *
|
||||
* *
|
||||
* \\O/ *
|
||||
|
|
||||
* / \\ *
|
||||
* *
|
||||
~~~~~~~~~~~~~~~~~~~~~''',
|
||||
' * * * \n'
|
||||
' * * \n'
|
||||
' \n'
|
||||
' * \\O/ * \n'
|
||||
' /|\\ \n'
|
||||
' * / \\ * \n'
|
||||
' * * \n'
|
||||
'~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~',
|
||||
// 프레임 3: 레벨업 텍스트
|
||||
'''
|
||||
* L E V E L U P ! *
|
||||
* *
|
||||
* \\O/ *
|
||||
|
|
||||
* / \\ *
|
||||
* *
|
||||
~~~~~~~~~~~~~~~~~~~~~''',
|
||||
' * L E V E L U P ! * \n'
|
||||
' * * \n'
|
||||
' \n'
|
||||
' * \\O/ * \n'
|
||||
' /|\\ \n'
|
||||
' * / \\ * \n'
|
||||
' * * \n'
|
||||
'~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~',
|
||||
// 프레임 4: 빛나는 캐릭터
|
||||
'''
|
||||
* * * * *
|
||||
* *
|
||||
* \\O/ *
|
||||
* | *
|
||||
* / \\ *
|
||||
* * * *
|
||||
~~~~~~~~~~~~~~~~~~~~~''',
|
||||
' * * * * * \n'
|
||||
' * * \n'
|
||||
' \n'
|
||||
' * \\O/ * \n'
|
||||
' * /|\\ * \n'
|
||||
' * / \\ * \n'
|
||||
' * * * * \n'
|
||||
'~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~',
|
||||
// 프레임 5: 마무리
|
||||
'''
|
||||
+
|
||||
+++
|
||||
+++++
|
||||
\\O/
|
||||
|
|
||||
/ \\
|
||||
~~~~~~~~~~~~~~~~~~~~~''',
|
||||
' + \n'
|
||||
' +++ \n'
|
||||
' +++++ \n'
|
||||
' \\O/ \n'
|
||||
' /|\\ \n'
|
||||
' / \\ \n'
|
||||
' \n'
|
||||
'~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~',
|
||||
],
|
||||
frameIntervalMs: 300,
|
||||
);
|
||||
|
||||
/// 퀘스트 완료 애니메이션
|
||||
/// 퀘스트 완료 애니메이션 (8줄 x 40자 고정)
|
||||
const questCompleteAnimation = AsciiAnimationData(
|
||||
frames: [
|
||||
// 프레임 1: 퀘스트 깃발
|
||||
'''
|
||||
[=======]
|
||||
|| ||
|
||||
|| \\O/ ||
|
||||
|| | ||
|
||||
|| / \\ ||
|
||||
||_____||
|
||||
~~~~~~~~~~~~~~~~~~~''',
|
||||
' [=======] \n'
|
||||
' || || \n'
|
||||
' || \\O/ || \n'
|
||||
' || /|\\ || \n'
|
||||
' || / \\ || \n'
|
||||
' ||_____|| \n'
|
||||
' \n'
|
||||
'~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~',
|
||||
// 프레임 2: 승리
|
||||
'''
|
||||
[QUEST!]
|
||||
|| ||
|
||||
\\\\O//
|
||||
\\|/
|
||||
/ \\
|
||||
||_____||
|
||||
~~~~~~~~~~~~~~~~~~~''',
|
||||
' [QUEST!] \n'
|
||||
' || || \n'
|
||||
' \\\\O// \n'
|
||||
' /|\\ \n'
|
||||
' / \\ \n'
|
||||
' ||_____|| \n'
|
||||
' \n'
|
||||
'~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~',
|
||||
// 프레임 3: 보상
|
||||
'''
|
||||
COMPLETE!
|
||||
|
||||
\\O/ \$\$\$
|
||||
| \$\$\$
|
||||
/ \\ \$\$\$
|
||||
|
||||
~~~~~~~~~~~~~~~~~~~''',
|
||||
' COMPLETE! \n'
|
||||
' \n'
|
||||
' \\O/ \$\$\$ \n'
|
||||
' /|\\ \$\$\$ \n'
|
||||
' / \\ \$\$\$ \n'
|
||||
' \n'
|
||||
' \n'
|
||||
'~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~',
|
||||
// 프레임 4: 축하
|
||||
'''
|
||||
* * * * *
|
||||
\\O/
|
||||
| +EXP
|
||||
/ \\ +GOLD
|
||||
* * * * *
|
||||
|
||||
~~~~~~~~~~~~~~~~~~~''',
|
||||
' * * * * * \n'
|
||||
' \\O/ \n'
|
||||
' /|\\ +EXP \n'
|
||||
' / \\ +GOLD \n'
|
||||
' * * * * * \n'
|
||||
' \n'
|
||||
' \n'
|
||||
'~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~',
|
||||
// 프레임 5: 마무리
|
||||
'''
|
||||
[ VICTORY! ]
|
||||
|
||||
\\O/
|
||||
|
|
||||
/ \\
|
||||
|
||||
~~~~~~~~~~~~~~~~~~~''',
|
||||
' [ VICTORY! ] \n'
|
||||
' \n'
|
||||
' \\O/ \n'
|
||||
' /|\\ \n'
|
||||
' / \\ \n'
|
||||
' \n'
|
||||
' \n'
|
||||
'~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~',
|
||||
],
|
||||
frameIntervalMs: 350,
|
||||
);
|
||||
|
||||
/// Act 완료 애니메이션 (플롯 진행)
|
||||
/// Act 완료 애니메이션 (8줄 x 40자 고정)
|
||||
const actCompleteAnimation = AsciiAnimationData(
|
||||
frames: [
|
||||
// 프레임 1: 커튼
|
||||
'''
|
||||
____________________
|
||||
| |
|
||||
| A C T |
|
||||
| |
|
||||
| C O M P L E T E |
|
||||
| |
|
||||
|____________________|''',
|
||||
' ______________________________ \n'
|
||||
' | | \n'
|
||||
' | A C T | \n'
|
||||
' | | \n'
|
||||
' | C O M P L E T E | \n'
|
||||
' | | \n'
|
||||
' |______________________________| \n'
|
||||
'~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~',
|
||||
// 프레임 2: 캐릭터 등장
|
||||
'''
|
||||
____________________
|
||||
| * * * * * |
|
||||
| \\O/ |
|
||||
| | |
|
||||
| / \\ |
|
||||
| * * * * * |
|
||||
|____________________|''',
|
||||
' ______________________________ \n'
|
||||
' | * * * * * | \n'
|
||||
' | \\O/ | \n'
|
||||
' | /|\\ | \n'
|
||||
' | / \\ | \n'
|
||||
' | * * * * * | \n'
|
||||
' |______________________________| \n'
|
||||
'~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~',
|
||||
// 프레임 3: 플롯 진행 표시
|
||||
'''
|
||||
____________________
|
||||
| PROLOGUE --> ACT |
|
||||
| \\O/ |
|
||||
| | --> |
|
||||
| / \\ |
|
||||
| STORY CONTINUES |
|
||||
|____________________|''',
|
||||
' ______________________________ \n'
|
||||
' | PROLOGUE --> ACT | \n'
|
||||
' | \\O/ | \n'
|
||||
' | /|\\ --> | \n'
|
||||
' | / \\ | \n'
|
||||
' | STORY CONTINUES | \n'
|
||||
' |______________________________| \n'
|
||||
'~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~',
|
||||
// 프레임 4: 축하
|
||||
'''
|
||||
____________________
|
||||
| * * * * * |
|
||||
| * \\O/ * |
|
||||
| | |
|
||||
| * / \\ * |
|
||||
| * * * * * |
|
||||
|____________________|''',
|
||||
' ______________________________ \n'
|
||||
' | * * * * * | \n'
|
||||
' | * \\O/ * | \n'
|
||||
' | /|\\ | \n'
|
||||
' | * / \\ * | \n'
|
||||
' | * * * * * | \n'
|
||||
' |______________________________| \n'
|
||||
'~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~',
|
||||
// 프레임 5: 마무리
|
||||
'''
|
||||
____________________
|
||||
| +---------+ |
|
||||
| | NEXT | |
|
||||
| | CHAPTER | |
|
||||
| +---------+ |
|
||||
| \\O/ |
|
||||
|____________________|''',
|
||||
' ______________________________ \n'
|
||||
' | +---------+ | \n'
|
||||
' | | NEXT | | \n'
|
||||
' | | CHAPTER | | \n'
|
||||
' | +---------+ | \n'
|
||||
' | \\O/ | \n'
|
||||
' |______________________________| \n'
|
||||
'~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~',
|
||||
],
|
||||
frameIntervalMs: 400,
|
||||
);
|
||||
|
||||
@@ -61,9 +61,9 @@ const _forestLayers = [
|
||||
scrollSpeed: 0.15,
|
||||
yStart: 1,
|
||||
),
|
||||
// 전경 - 풀/바닥
|
||||
// 전경 - 바닥
|
||||
BackgroundLayer(
|
||||
lines: [r'____||____||____||____||____||____||'],
|
||||
lines: [r'______________________________________'],
|
||||
scrollSpeed: 0.3,
|
||||
yStart: 7,
|
||||
),
|
||||
|
||||
@@ -74,14 +74,23 @@ class BattleComposer {
|
||||
_drawBackgroundLayer(canvas, layer, globalTick);
|
||||
}
|
||||
|
||||
// 3. 캐릭터 프레임 (정규화하여 왼쪽 정렬)
|
||||
// 3. 캐릭터 프레임 (페이즈에 따라 X 위치 변경 - 근접 전투)
|
||||
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);
|
||||
// 바닥 레이어(Y=7) 위에 서있도록 -1
|
||||
final charY = frameHeight - normalizedChar.length - 1;
|
||||
// 페이즈별 캐릭터 X 위치 (몬스터에게 접근)
|
||||
final charX = switch (phase) {
|
||||
BattlePhase.idle => 0,
|
||||
BattlePhase.prepare => 12,
|
||||
BattlePhase.attack => 24,
|
||||
BattlePhase.hit => 28,
|
||||
BattlePhase.recover => 8,
|
||||
};
|
||||
_overlaySpriteWithSpaces(canvas, normalizedChar, charX, charY);
|
||||
|
||||
// 4. 몬스터 프레임 (정규화하여 오른쪽 정렬)
|
||||
final monsterFrames =
|
||||
@@ -89,22 +98,23 @@ class BattleComposer {
|
||||
final monsterFrame = monsterFrames[subFrame % monsterFrames.length];
|
||||
final normalizedMonster = _normalizeSpriteRight(monsterFrame, monsterWidth);
|
||||
final monsterX = frameWidth - monsterWidth;
|
||||
final monsterY = frameHeight - normalizedMonster.length;
|
||||
// 바닥 레이어(Y=7) 위에 서있도록 -1
|
||||
final monsterY = frameHeight - normalizedMonster.length - 1;
|
||||
_overlaySpriteWithSpaces(canvas, normalizedMonster, monsterX, monsterY);
|
||||
|
||||
// 5. 멀티라인 이펙트 오버레이 (공격/히트/준비 페이즈)
|
||||
if (phase == BattlePhase.prepare ||
|
||||
phase == BattlePhase.attack ||
|
||||
phase == BattlePhase.hit) {
|
||||
// 5. 멀티라인 이펙트 오버레이 (공격/히트 페이즈)
|
||||
if (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;
|
||||
// 이펙트 X 위치: 캐릭터 오른쪽에 붙여서 표시
|
||||
final effectX = charX + 6;
|
||||
for (var i = 0; i < effectLines.length; i++) {
|
||||
final y = effectY + i;
|
||||
if (y >= 0 && y < frameHeight && effectLines[i].isNotEmpty) {
|
||||
_overlayText(canvas, effectLines[i], characterWidth, y);
|
||||
_overlayText(canvas, effectLines[i], effectX, y);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,6 +59,7 @@ CharacterFrame getCharacterFrame(BattlePhase phase, int subFrame) {
|
||||
|
||||
// ============================================================================
|
||||
// 대기 프레임 (숨쉬기 애니메이션) - 4프레임, 심플 3줄 스타일, 폭 6자
|
||||
// 구조: [머리, 몸통+팔, 다리]
|
||||
// ============================================================================
|
||||
const _idleFrames = [
|
||||
CharacterFrame([
|
||||
@@ -85,41 +86,38 @@ const _idleFrames = [
|
||||
|
||||
// ============================================================================
|
||||
// 준비 프레임 (무기 들기) - 3프레임, 심플 3줄 스타일, 폭 6자
|
||||
// 구조: [머리, 몸통+팔, 다리]
|
||||
// ============================================================================
|
||||
const _prepareFrames = [
|
||||
CharacterFrame([
|
||||
r' \o ',
|
||||
r' |\ ',
|
||||
r' o ',
|
||||
r' \|\ ',
|
||||
r' / \ ',
|
||||
]),
|
||||
CharacterFrame([
|
||||
r' _ ',
|
||||
r' \o ',
|
||||
r' o_ ',
|
||||
r' \| ',
|
||||
r' / \ ',
|
||||
]),
|
||||
CharacterFrame([
|
||||
r' \_ ',
|
||||
r' \o/ ',
|
||||
r' o/ ',
|
||||
r' \| ',
|
||||
r' / \ ',
|
||||
]),
|
||||
];
|
||||
|
||||
// ============================================================================
|
||||
// 공격 프레임 (전진 + 휘두르기) - 5프레임, 심플 3줄 스타일
|
||||
// 구조: [머리, 몸통+팔+무기, 다리]
|
||||
// ============================================================================
|
||||
const _attackFrames = [
|
||||
CharacterFrame([
|
||||
r' \_/ ',
|
||||
r' o ',
|
||||
r' o\ ',
|
||||
r' /| ',
|
||||
r' / \ ',
|
||||
]),
|
||||
CharacterFrame([
|
||||
r' _/ ',
|
||||
r' o ',
|
||||
r' /|\ ',
|
||||
]),
|
||||
CharacterFrame([
|
||||
r' o-- ',
|
||||
r' o- ',
|
||||
r' /| ',
|
||||
r' / \ ',
|
||||
]),
|
||||
@@ -130,13 +128,19 @@ const _attackFrames = [
|
||||
]),
|
||||
CharacterFrame([
|
||||
r' o ',
|
||||
r' /|\_ ',
|
||||
r' /|-=>',
|
||||
r' / \ ',
|
||||
]),
|
||||
CharacterFrame([
|
||||
r' o ',
|
||||
r' /|\ ',
|
||||
r' / \ ',
|
||||
]),
|
||||
];
|
||||
|
||||
// ============================================================================
|
||||
// 히트 프레임 (공격 명중) - 3프레임, 심플 3줄 스타일
|
||||
// 구조: [머리, 몸통+팔+이펙트, 다리]
|
||||
// ============================================================================
|
||||
const _hitFrames = [
|
||||
CharacterFrame([
|
||||
@@ -158,6 +162,7 @@ const _hitFrames = [
|
||||
|
||||
// ============================================================================
|
||||
// 복귀 프레임 - 3프레임, 심플 3줄 스타일
|
||||
// 구조: [머리, 몸통+팔, 다리]
|
||||
// ============================================================================
|
||||
const _recoverFrames = [
|
||||
CharacterFrame([
|
||||
|
||||
@@ -2,27 +2,28 @@
|
||||
// 몬스터 레벨에 따라 ASCII 아트 크기 결정
|
||||
|
||||
/// 몬스터 크기 enum
|
||||
/// 실제 프레임 줄 수와 일치하도록 설정
|
||||
enum MonsterSize {
|
||||
/// 1줄 (레벨 1-5)
|
||||
tiny(1),
|
||||
/// 2줄 (레벨 1-5)
|
||||
tiny(2),
|
||||
|
||||
/// 2줄 (레벨 6-10)
|
||||
small(2),
|
||||
/// 4줄 (레벨 6-10)
|
||||
small(4),
|
||||
|
||||
/// 3줄 (레벨 11-15)
|
||||
medium(3),
|
||||
/// 6줄 (레벨 11-15)
|
||||
medium(6),
|
||||
|
||||
/// 4줄 (레벨 16-25)
|
||||
large(4),
|
||||
/// 8줄 (레벨 16-25)
|
||||
large(8),
|
||||
|
||||
/// 5줄 (레벨 26-35)
|
||||
huge(5),
|
||||
/// 8줄 (레벨 26-35)
|
||||
huge(8),
|
||||
|
||||
/// 6줄 (레벨 36-50)
|
||||
giant(6),
|
||||
/// 8줄 (레벨 36-50)
|
||||
giant(8),
|
||||
|
||||
/// 7줄 (레벨 51+, 보스급)
|
||||
titanic(7);
|
||||
/// 8줄 (레벨 51+, 보스급)
|
||||
titanic(8);
|
||||
|
||||
const MonsterSize(this.lines);
|
||||
|
||||
|
||||
@@ -127,7 +127,9 @@ class ProgressService {
|
||||
|
||||
/// Tick the timer loop (equivalent to Timer1Timer in the original code).
|
||||
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 queue = state.queue;
|
||||
var nextState = state;
|
||||
|
||||
@@ -226,6 +226,11 @@ class _GamePlayScreenState extends State<GamePlayScreen>
|
||||
},
|
||||
colorTheme: _colorTheme,
|
||||
onThemeCycle: _cycleColorTheme,
|
||||
isPaused: !widget.controller.isRunning,
|
||||
onPauseToggle: () async {
|
||||
await widget.controller.togglePause();
|
||||
setState(() {});
|
||||
},
|
||||
specialAnimation: _specialAnimation,
|
||||
weaponName: state.equipment.weapon,
|
||||
shieldName: state.equipment.shield,
|
||||
|
||||
@@ -104,6 +104,21 @@ class GameSessionController extends ChangeNotifier {
|
||||
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
|
||||
void dispose() {
|
||||
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/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';
|
||||
@@ -230,6 +229,54 @@ class _AsciiAnimationCardState extends State<AsciiAnimationCard> {
|
||||
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
|
||||
Widget build(BuildContext context) {
|
||||
final brightness = Theme.of(context).brightness;
|
||||
@@ -254,13 +301,8 @@ class _AsciiAnimationCardState extends State<AsciiAnimationCard> {
|
||||
_environment,
|
||||
_globalTick,
|
||||
);
|
||||
|
||||
// 히트 페이즈면 몬스터 색상 변경
|
||||
if (_battlePhase == BattlePhase.hit) {
|
||||
final monsterColorCategory =
|
||||
getMonsterColorCategory(widget.monsterBaseName);
|
||||
textColor = getMonsterColors(monsterColorCategory).hit;
|
||||
}
|
||||
// 이펙트는 텍스트 자체로 구분 (*, !, =, ~ 등)
|
||||
// 전체 색상 변경 제거 - 기본 테마 색상 유지
|
||||
} else {
|
||||
// 기존 레거시 시스템 사용
|
||||
final frameIndex =
|
||||
@@ -293,14 +335,16 @@ class _AsciiAnimationCardState extends State<AsciiAnimationCard> {
|
||||
.clamp(6.0, 14.0);
|
||||
|
||||
return Center(
|
||||
child: Text(
|
||||
frameText,
|
||||
style: TextStyle(
|
||||
fontFamily: 'Courier',
|
||||
fontSize: fontSize,
|
||||
color: textColor,
|
||||
height: 1.2,
|
||||
letterSpacing: 0,
|
||||
child: RichText(
|
||||
text: _buildColoredTextSpan(
|
||||
frameText,
|
||||
TextStyle(
|
||||
fontFamily: 'Courier',
|
||||
fontSize: fontSize,
|
||||
color: textColor,
|
||||
height: 1.2,
|
||||
letterSpacing: 0,
|
||||
),
|
||||
),
|
||||
textAlign: TextAlign.left,
|
||||
),
|
||||
@@ -317,7 +361,7 @@ class _AsciiAnimationCardState extends State<AsciiAnimationCard> {
|
||||
height: 1.1,
|
||||
letterSpacing: 0,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
textAlign: TextAlign.left,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -15,6 +15,8 @@ class TaskProgressPanel extends StatelessWidget {
|
||||
required this.onSpeedCycle,
|
||||
required this.colorTheme,
|
||||
required this.onThemeCycle,
|
||||
required this.isPaused,
|
||||
required this.onPauseToggle,
|
||||
this.specialAnimation,
|
||||
this.weaponName,
|
||||
this.shieldName,
|
||||
@@ -28,6 +30,10 @@ class TaskProgressPanel extends StatelessWidget {
|
||||
final AsciiColorTheme colorTheme;
|
||||
final VoidCallback onThemeCycle;
|
||||
|
||||
/// 일시 정지 상태
|
||||
final bool isPaused;
|
||||
final VoidCallback onPauseToggle;
|
||||
|
||||
/// 특수 애니메이션 (레벨업, 퀘스트 완료 등)
|
||||
final AsciiAnimationType? specialAnimation;
|
||||
|
||||
@@ -70,6 +76,8 @@ class TaskProgressPanel extends StatelessWidget {
|
||||
Row(
|
||||
children: [
|
||||
_buildThemeButton(context),
|
||||
const SizedBox(width: 4),
|
||||
_buildPauseButton(context),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
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) {
|
||||
return SizedBox(
|
||||
height: 28,
|
||||
|
||||
Reference in New Issue
Block a user