feat(animation): ASCII 애니메이션 시스템 구현
- TaskType별 애니메이션 (전투, 마을, 걷기) - 몬스터 카테고리별 전투 애니메이션 (7종) - 특수 애니메이션 (레벨업, 퀘스트 완료, Act 완료) - 색상 테마 옵션 (green, amber, white, system) - 테마 설정 SharedPreferences 저장 - 프로그레스 바를 상단으로 이동
This commit is contained in:
827
lib/src/core/animation/ascii_animation_data.dart
Normal file
827
lib/src/core/animation/ascii_animation_data.dart
Normal file
@@ -0,0 +1,827 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:askiineverdie/src/core/animation/ascii_animation_type.dart';
|
||||
|
||||
/// ASCII 애니메이션 프레임 데이터
|
||||
class AsciiAnimationData {
|
||||
const AsciiAnimationData({
|
||||
required this.frames,
|
||||
this.frameIntervalMs = 200,
|
||||
});
|
||||
|
||||
/// 각 프레임 (문자열, 최소 5줄)
|
||||
final List<String> frames;
|
||||
|
||||
/// 프레임 간격 (밀리초)
|
||||
final int frameIntervalMs;
|
||||
}
|
||||
|
||||
/// 터미널 색상 테마
|
||||
enum AsciiColorTheme {
|
||||
/// 클래식 녹색 터미널
|
||||
green,
|
||||
|
||||
/// 엠버 (호박색) 터미널
|
||||
amber,
|
||||
|
||||
/// 화이트 온 블랙
|
||||
white,
|
||||
|
||||
/// 시스템 테마 (라이트/다크 모드 따름)
|
||||
system,
|
||||
}
|
||||
|
||||
/// 테마별 색상 데이터
|
||||
class AsciiThemeColors {
|
||||
const AsciiThemeColors({
|
||||
required this.textColor,
|
||||
required this.backgroundColor,
|
||||
});
|
||||
|
||||
final Color textColor;
|
||||
final Color backgroundColor;
|
||||
}
|
||||
|
||||
/// 테마별 색상 반환
|
||||
AsciiThemeColors getThemeColors(AsciiColorTheme theme, Brightness brightness) {
|
||||
return switch (theme) {
|
||||
AsciiColorTheme.green => const AsciiThemeColors(
|
||||
textColor: Color(0xFF00FF00),
|
||||
backgroundColor: Color(0xFF0D0D0D),
|
||||
),
|
||||
AsciiColorTheme.amber => const AsciiThemeColors(
|
||||
textColor: Color(0xFFFFB000),
|
||||
backgroundColor: Color(0xFF1A1000),
|
||||
),
|
||||
AsciiColorTheme.white => const AsciiThemeColors(
|
||||
textColor: Color(0xFFE0E0E0),
|
||||
backgroundColor: Color(0xFF121212),
|
||||
),
|
||||
AsciiColorTheme.system => brightness == Brightness.dark
|
||||
? const AsciiThemeColors(
|
||||
textColor: Color(0xFFE0E0E0),
|
||||
backgroundColor: Color(0xFF1E1E1E),
|
||||
)
|
||||
: const AsciiThemeColors(
|
||||
textColor: Color(0xFF1E1E1E),
|
||||
backgroundColor: Color(0xFFF5F5F5),
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
/// 몬스터 카테고리
|
||||
enum MonsterCategory {
|
||||
/// 기본 (고양이 모양)
|
||||
beast,
|
||||
|
||||
/// 곤충/벌레류
|
||||
insect,
|
||||
|
||||
/// 인간형 (고블린, 오크 등)
|
||||
humanoid,
|
||||
|
||||
/// 언데드 (스켈레톤, 좀비)
|
||||
undead,
|
||||
|
||||
/// 드래곤/비행 생물
|
||||
dragon,
|
||||
|
||||
/// 슬라임/젤리
|
||||
slime,
|
||||
|
||||
/// 악마/마법 생물
|
||||
demon,
|
||||
}
|
||||
|
||||
/// 몬스터 이름으로 카테고리 결정
|
||||
MonsterCategory getMonsterCategory(String? monsterBaseName) {
|
||||
if (monsterBaseName == null || monsterBaseName.isEmpty) {
|
||||
return MonsterCategory.beast;
|
||||
}
|
||||
|
||||
final name = monsterBaseName.toLowerCase();
|
||||
|
||||
// 곤충/벌레류
|
||||
if (name.contains('ant') ||
|
||||
name.contains('centipede') ||
|
||||
name.contains('spider') ||
|
||||
name.contains('beetle') ||
|
||||
name.contains('crawler') ||
|
||||
name.contains('crayfish') ||
|
||||
name.contains('anhkheg')) {
|
||||
return MonsterCategory.insect;
|
||||
}
|
||||
|
||||
// 인간형
|
||||
if (name.contains('goblin') ||
|
||||
name.contains('orc') ||
|
||||
name.contains('troll') ||
|
||||
name.contains('ogre') ||
|
||||
name.contains('giant') ||
|
||||
name.contains('scout') ||
|
||||
name.contains('bugbear') ||
|
||||
name.contains('gnoll') ||
|
||||
name.contains('kobold') ||
|
||||
name.contains('hobgoblin')) {
|
||||
return MonsterCategory.humanoid;
|
||||
}
|
||||
|
||||
// 언데드
|
||||
if (name.contains('skeleton') ||
|
||||
name.contains('zombie') ||
|
||||
name.contains('ghoul') ||
|
||||
name.contains('ghost') ||
|
||||
name.contains('wight') ||
|
||||
name.contains('wraith') ||
|
||||
name.contains('vampire') ||
|
||||
name.contains('lich') ||
|
||||
name.contains('mummy')) {
|
||||
return MonsterCategory.undead;
|
||||
}
|
||||
|
||||
// 드래곤/비행
|
||||
if (name.contains('dragon') ||
|
||||
name.contains('wyvern') ||
|
||||
name.contains('cockatrice') ||
|
||||
name.contains('griffin') ||
|
||||
name.contains('roc') ||
|
||||
name.contains('harpy') ||
|
||||
name.contains('couatl')) {
|
||||
return MonsterCategory.dragon;
|
||||
}
|
||||
|
||||
// 슬라임/젤리
|
||||
if (name.contains('slime') ||
|
||||
name.contains('pudding') ||
|
||||
name.contains('ooze') ||
|
||||
name.contains('jelly') ||
|
||||
name.contains('boogie') ||
|
||||
name.contains('blob') ||
|
||||
name.contains('jubilex')) {
|
||||
return MonsterCategory.slime;
|
||||
}
|
||||
|
||||
// 악마/마법 생물
|
||||
if (name.contains('demon') ||
|
||||
name.contains('devil') ||
|
||||
name.contains('succubus') ||
|
||||
name.contains('beholder') ||
|
||||
name.contains('demogorgon') ||
|
||||
name.contains('orcus') ||
|
||||
name.contains('vrock') ||
|
||||
name.contains('hezrou') ||
|
||||
name.contains('glabrezu')) {
|
||||
return MonsterCategory.demon;
|
||||
}
|
||||
|
||||
return MonsterCategory.beast;
|
||||
}
|
||||
|
||||
/// 기본 전투 애니메이션 (beast - 고양이 모양)
|
||||
const battleAnimationBeast = AsciiAnimationData(
|
||||
frames: [
|
||||
// 프레임 1: 대치
|
||||
'''
|
||||
,O, /\\___/\\
|
||||
/( )\\ ( o o )
|
||||
/ \\ vs ( =^= )
|
||||
_| |_ /| |\\
|
||||
| | / | | \\
|
||||
_| |_ | |_____| |
|
||||
|_________| |___| |___|''',
|
||||
// 프레임 2: 공격 준비
|
||||
'''
|
||||
O /\\___/\\
|
||||
/|\\----o ( o o )
|
||||
/ \\ ( =^= )
|
||||
_| |_ /| |\\
|
||||
| | / | | \\
|
||||
_| |_ | |_____| |
|
||||
|_________| |___| |___|''',
|
||||
// 프레임 3: 공격 중
|
||||
'''
|
||||
O o--->/\\___/\\
|
||||
/|\\-----------> ( X X )
|
||||
/ \\ ( =^= )
|
||||
_| |_ /| |\\
|
||||
| | / | | \\
|
||||
_| |_ | |_____| |
|
||||
|_________| |___| |___|''',
|
||||
// 프레임 4: 히트
|
||||
'''
|
||||
O /\\___/\\
|
||||
/|\\ **** ( X X ) ****
|
||||
/ \\ ** ( =^= ) **
|
||||
_| |_ /| |\\
|
||||
| | / | | \\
|
||||
_| |_ | |_____| |
|
||||
|_________| |___| |___|''',
|
||||
// 프레임 5: 복귀
|
||||
'''
|
||||
\\O/ /\\___/\\
|
||||
| ( - - )
|
||||
/ \\ ( =^= )
|
||||
_| |_ /| |\\
|
||||
| | / | | \\
|
||||
_| |_ | |_____| |
|
||||
|_________| |___| |___|''',
|
||||
],
|
||||
frameIntervalMs: 220,
|
||||
);
|
||||
|
||||
/// 마을/상점 애니메이션 (7줄)
|
||||
const townAnimation = AsciiAnimationData(
|
||||
frames: [
|
||||
// 프레임 1: 상점 앞에서 대기
|
||||
'''
|
||||
_______________
|
||||
/ \\ O
|
||||
| SHOP | /|\\
|
||||
| [=====] | / \\
|
||||
| | | | |
|
||||
|___|_____|______| _|_
|
||||
~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~''',
|
||||
// 프레임 2: 상점으로 이동
|
||||
'''
|
||||
_______________
|
||||
/ \\ O
|
||||
| SHOP | /|\\
|
||||
| [=====] | / \\
|
||||
| | | | |
|
||||
|___|_____|______| _|_
|
||||
~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~''',
|
||||
// 프레임 3: 상점 앞 도착
|
||||
'''
|
||||
_______________
|
||||
/ \\ O
|
||||
| SHOP | /|\\
|
||||
| [=====] | / \\
|
||||
| | | | |
|
||||
|___|_____|______| _|_
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~''',
|
||||
// 프레임 4: 거래 중
|
||||
'''
|
||||
_______________
|
||||
/ \\ O \$
|
||||
| SHOP | /|\\ \$
|
||||
| [=====] | /\\\$
|
||||
| | @ | | |
|
||||
|___|_____|______| _|_
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~''',
|
||||
// 프레임 5: 거래 완료
|
||||
'''
|
||||
_______________
|
||||
/ \\ \\O/
|
||||
| SHOP | | +
|
||||
| [=====] | / \\ +
|
||||
| | @ | | | +
|
||||
|___|_____|______| _|_
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~''',
|
||||
],
|
||||
frameIntervalMs: 280,
|
||||
);
|
||||
|
||||
/// 걷는 애니메이션 (7줄, 배경 포함)
|
||||
const walkingAnimation = AsciiAnimationData(
|
||||
frames: [
|
||||
// 프레임 1: 서있기
|
||||
'''
|
||||
O
|
||||
/|\\
|
||||
/ \\
|
||||
~~ | ~~
|
||||
~~~~ _|_ ~~~~
|
||||
~~~~~~ ~~~~~~~~ ~~~~~~
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~''',
|
||||
// 프레임 2: 왼발 앞
|
||||
'''
|
||||
O
|
||||
/|\\
|
||||
/|
|
||||
~~ / \\ ~~
|
||||
~~~~ _|_ ~~~~
|
||||
~~~~~~ ~~~~~~~~ ~~~~~~
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~''',
|
||||
// 프레임 3: 이동 중
|
||||
'''
|
||||
O
|
||||
/|\\
|
||||
|\\
|
||||
~~ / \\ ~~
|
||||
~~~~ _|_ ~~~~
|
||||
~~~~~~ ~~~~~~~~ ~~~~~~
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~''',
|
||||
// 프레임 4: 오른발 앞
|
||||
'''
|
||||
O
|
||||
/|\\
|
||||
|/
|
||||
~~ / \\ ~~
|
||||
~~~~ _|_ ~~~~
|
||||
~~~~~~ ~~~~~~~~ ~~~~~~
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~''',
|
||||
// 프레임 5: 복귀
|
||||
'''
|
||||
O
|
||||
/|\\
|
||||
/ \\
|
||||
~~ | ~~
|
||||
~~~~ _|_ ~~~~
|
||||
~~~~~~ ~~~~~~~~ ~~~~~~
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~''',
|
||||
],
|
||||
frameIntervalMs: 180,
|
||||
);
|
||||
|
||||
/// 곤충 전투 애니메이션
|
||||
const battleAnimationInsect = AsciiAnimationData(
|
||||
frames: [
|
||||
// 프레임 1: 대치
|
||||
'''
|
||||
,O, /\\_/\\
|
||||
/( )\\ ( o o )
|
||||
/ \\ vs /|=====|\\
|
||||
_| |_ < | | >
|
||||
| | \\|_____|/
|
||||
_| |_ / \\
|
||||
|_________| /_______\\''',
|
||||
// 프레임 2: 공격 준비
|
||||
'''
|
||||
O /\\_/\\
|
||||
/|\\----o ( o o )
|
||||
/ \\ /|=====|\\
|
||||
_| |_ < | | >
|
||||
| | \\|_____|/
|
||||
_| |_ / \\
|
||||
|_________| /_______\\''',
|
||||
// 프레임 3: 공격 중
|
||||
'''
|
||||
O o-->/\\_/\\
|
||||
/|\\----------> ( o o )
|
||||
/ \\ /|=====|\\
|
||||
_| |_ < | | >
|
||||
| | \\|_____|/
|
||||
_| |_ / \\
|
||||
|_________| /_______\\''',
|
||||
// 프레임 4: 히트
|
||||
'''
|
||||
O /\\_/\\
|
||||
/|\\ **** ( X X ) ****
|
||||
/ \\ ** /|=====|\\ **
|
||||
_| |_ < | | >
|
||||
| | \\|_____|/
|
||||
_| |_ / \\
|
||||
|_________| /_______\\''',
|
||||
// 프레임 5: 복귀
|
||||
'''
|
||||
\\O/ /\\_/\\
|
||||
| ( - - )
|
||||
/ \\ /|=====|\\
|
||||
_| |_ < | | >
|
||||
| | \\|_____|/
|
||||
_| |_ / \\
|
||||
|_________| /_______\\''',
|
||||
],
|
||||
frameIntervalMs: 220,
|
||||
);
|
||||
|
||||
/// 인간형 전투 애니메이션
|
||||
const battleAnimationHumanoid = AsciiAnimationData(
|
||||
frames: [
|
||||
// 프레임 1: 대치
|
||||
'''
|
||||
,O, O
|
||||
/( )\\ /|\\
|
||||
/ \\ vs / | \\
|
||||
_| |_ ___|___
|
||||
| | | |
|
||||
_| |_ | orc |
|
||||
|_________| |_______|''',
|
||||
// 프레임 2: 공격 준비
|
||||
'''
|
||||
O O
|
||||
/|\\----o /|\\
|
||||
/ \\ / | \\
|
||||
_| |_ ___|___
|
||||
| | | |
|
||||
_| |_ | orc |
|
||||
|_________| |_______|''',
|
||||
// 프레임 3: 공격 중
|
||||
'''
|
||||
O o----> O
|
||||
/|\\-----------> /|\\
|
||||
/ \\ / | \\
|
||||
_| |_ ___|___
|
||||
| | | |
|
||||
_| |_ | orc |
|
||||
|_________| |_______|''',
|
||||
// 프레임 4: 히트
|
||||
'''
|
||||
O O
|
||||
/|\\ **** X|X ****
|
||||
/ \\ ** / | \\ **
|
||||
_| |_ ___|___
|
||||
| | | |
|
||||
_| |_ | orc |
|
||||
|_________| |_______|''',
|
||||
// 프레임 5: 복귀
|
||||
'''
|
||||
\\O/ O
|
||||
| /|\\
|
||||
/ \\ / | \\
|
||||
_| |_ ___|___
|
||||
| | | |
|
||||
_| |_ | orc |
|
||||
|_________| |_______|''',
|
||||
],
|
||||
frameIntervalMs: 220,
|
||||
);
|
||||
|
||||
/// 언데드 전투 애니메이션
|
||||
const battleAnimationUndead = AsciiAnimationData(
|
||||
frames: [
|
||||
// 프레임 1: 대치
|
||||
'''
|
||||
,O, .-.
|
||||
/( )\\ (o.o)
|
||||
/ \\ vs |=|
|
||||
_| |_ /|X|\\
|
||||
| | / | | \\
|
||||
_| |_ \\_|_|_/
|
||||
|_________| _/ \\_''',
|
||||
// 프레임 2: 공격 준비
|
||||
'''
|
||||
O .-.
|
||||
/|\\----o (o.o)
|
||||
/ \\ |=|
|
||||
_| |_ /|X|\\
|
||||
| | / | | \\
|
||||
_| |_ \\_|_|_/
|
||||
|_________| _/ \\_''',
|
||||
// 프레임 3: 공격 중
|
||||
'''
|
||||
O o--->.-.
|
||||
/|\\-----------> (o.o)
|
||||
/ \\ |=|
|
||||
_| |_ /|X|\\
|
||||
| | / | | \\
|
||||
_| |_ \\_|_|_/
|
||||
|_________| _/ \\_''',
|
||||
// 프레임 4: 히트
|
||||
'''
|
||||
O .-.
|
||||
/|\\ **** (X.X) ****
|
||||
/ \\ ** |=| **
|
||||
_| |_ /|X|\\
|
||||
| | / | | \\
|
||||
_| |_ \\_|_|_/
|
||||
|_________| _/ \\_''',
|
||||
// 프레임 5: 복귀
|
||||
'''
|
||||
\\O/ .-.
|
||||
| (-.-)
|
||||
/ \\ |=|
|
||||
_| |_ /|X|\\
|
||||
| | / | | \\
|
||||
_| |_ \\_|_|_/
|
||||
|_________| _/ \\_''',
|
||||
],
|
||||
frameIntervalMs: 250,
|
||||
);
|
||||
|
||||
/// 드래곤 전투 애니메이션
|
||||
const battleAnimationDragon = AsciiAnimationData(
|
||||
frames: [
|
||||
// 프레임 1: 대치
|
||||
'''
|
||||
,O, __/\\__
|
||||
/( )\\ / \\
|
||||
/ \\ vs < (O)(O) >
|
||||
_| |_ \\ \\/ /
|
||||
| | \\ /
|
||||
_| |_ /|\\~~~/|\\
|
||||
|_________| /_________\\''',
|
||||
// 프레임 2: 공격 준비
|
||||
'''
|
||||
O __/\\__
|
||||
/|\\----o / \\
|
||||
/ \\ < (O)(O) >
|
||||
_| |_ \\ \\/ /
|
||||
| | \\ /
|
||||
_| |_ /|\\~~~/|\\
|
||||
|_________| /_________\\''',
|
||||
// 프레임 3: 공격 중
|
||||
'''
|
||||
O o--->__/\\__
|
||||
/|\\---------> / \\
|
||||
/ \\ < (O)(O) >
|
||||
_| |_ \\ \\/ /
|
||||
| | \\ /
|
||||
_| |_ /|\\~~~/|\\
|
||||
|_________| /_________\\''',
|
||||
// 프레임 4: 히트
|
||||
'''
|
||||
O __/\\__
|
||||
/|\\ **** / >< \\ ****
|
||||
/ \\ ** < (X)(X) > **
|
||||
_| |_ \\ \\/ /
|
||||
| | \\ /
|
||||
_| |_ /|\\~~~/|\\
|
||||
|_________| /_________\\''',
|
||||
// 프레임 5: 복귀
|
||||
'''
|
||||
\\O/ __/\\__
|
||||
| / \\
|
||||
/ \\ < (-)(-)>
|
||||
_| |_ \\ \\/ /
|
||||
| | \\ /
|
||||
_| |_ /|\\~~~/|\\
|
||||
|_________| /_________\\''',
|
||||
],
|
||||
frameIntervalMs: 200,
|
||||
);
|
||||
|
||||
/// 슬라임 전투 애니메이션
|
||||
const battleAnimationSlime = AsciiAnimationData(
|
||||
frames: [
|
||||
// 프레임 1: 대치
|
||||
'''
|
||||
,O, .---.
|
||||
/( )\\ / \\
|
||||
/ \\ vs ( o o )
|
||||
_| |_ \\ ~ /
|
||||
| | '---'
|
||||
_| |_ ~~~~~~~
|
||||
|_________| ~~~~~~~~~''',
|
||||
// 프레임 2: 공격 준비
|
||||
'''
|
||||
O .---.
|
||||
/|\\----o / \\
|
||||
/ \\ ( o o )
|
||||
_| |_ \\ ~ /
|
||||
| | '---'
|
||||
_| |_ ~~~~~~~
|
||||
|_________| ~~~~~~~~~''',
|
||||
// 프레임 3: 공격 중
|
||||
'''
|
||||
O o--->.---.
|
||||
/|\\---------> / \\
|
||||
/ \\ ( o o )
|
||||
_| |_ \\ ~ /
|
||||
| | '---'
|
||||
_| |_ ~~~~~~~
|
||||
|_________| ~~~~~~~~~''',
|
||||
// 프레임 4: 히트
|
||||
'''
|
||||
O .---.
|
||||
/|\\ **** / X X \\ ****
|
||||
/ \\ ** ( ~ ) **
|
||||
_| |_ \\ /
|
||||
| | '---'
|
||||
_| |_ ~~~~~~~
|
||||
|_________| ~~~~~~~~~''',
|
||||
// 프레임 5: 복귀
|
||||
'''
|
||||
\\O/ .---.
|
||||
| / \\
|
||||
/ \\ ( - - )
|
||||
_| |_ \\ ~ /
|
||||
| | '---'
|
||||
_| |_ ~~~~~~~
|
||||
|_________| ~~~~~~~~~''',
|
||||
],
|
||||
frameIntervalMs: 280,
|
||||
);
|
||||
|
||||
/// 악마 전투 애니메이션
|
||||
const battleAnimationDemon = AsciiAnimationData(
|
||||
frames: [
|
||||
// 프레임 1: 대치
|
||||
'''
|
||||
,O, /\\ /\\
|
||||
/( )\\ ( \\ / )
|
||||
/ \\ vs \\ o o /
|
||||
_| |_ | V |
|
||||
| | | ~~~ |
|
||||
_| |_ /| |\\
|
||||
|_________| /___|___|_\\''',
|
||||
// 프레임 2: 공격 준비
|
||||
'''
|
||||
O /\\ /\\
|
||||
/|\\----o ( \\ / )
|
||||
/ \\ \\ o o /
|
||||
_| |_ | V |
|
||||
| | | ~~~ |
|
||||
_| |_ /| |\\
|
||||
|_________| /___|___|_\\''',
|
||||
// 프레임 3: 공격 중
|
||||
'''
|
||||
O o--->/\\ /\\
|
||||
/|\\--------> ( \\ / )
|
||||
/ \\ \\ o o /
|
||||
_| |_ | V |
|
||||
| | | ~~~ |
|
||||
_| |_ /| |\\
|
||||
|_________| /___|___|_\\''',
|
||||
// 프레임 4: 히트
|
||||
'''
|
||||
O /\\ /\\
|
||||
/|\\ **** ( X X ) ****
|
||||
/ \\ ** \\ X X / **
|
||||
_| |_ | V |
|
||||
| | | ~~~ |
|
||||
_| |_ /| |\\
|
||||
|_________| /___|___|_\\''',
|
||||
// 프레임 5: 복귀
|
||||
'''
|
||||
\\O/ /\\ /\\
|
||||
| ( \\ / )
|
||||
/ \\ \\ - - /
|
||||
_| |_ | V |
|
||||
| | | ~~~ |
|
||||
_| |_ /| |\\
|
||||
|_________| /___|___|_\\''',
|
||||
],
|
||||
frameIntervalMs: 200,
|
||||
);
|
||||
|
||||
/// 몬스터 카테고리별 전투 애니메이션 반환
|
||||
AsciiAnimationData getBattleAnimation(MonsterCategory category) {
|
||||
return switch (category) {
|
||||
MonsterCategory.beast => battleAnimationBeast,
|
||||
MonsterCategory.insect => battleAnimationInsect,
|
||||
MonsterCategory.humanoid => battleAnimationHumanoid,
|
||||
MonsterCategory.undead => battleAnimationUndead,
|
||||
MonsterCategory.dragon => battleAnimationDragon,
|
||||
MonsterCategory.slime => battleAnimationSlime,
|
||||
MonsterCategory.demon => battleAnimationDemon,
|
||||
};
|
||||
}
|
||||
|
||||
/// 레벨업 축하 애니메이션
|
||||
const levelUpAnimation = AsciiAnimationData(
|
||||
frames: [
|
||||
// 프레임 1: 시작
|
||||
'''
|
||||
* * *
|
||||
* * *
|
||||
\\O/
|
||||
* | *
|
||||
/ \\
|
||||
* *
|
||||
~~~~~~~~~~~~~~~~~~~~~''',
|
||||
// 프레임 2: 별 확산
|
||||
'''
|
||||
* * *
|
||||
* *
|
||||
* \\O/ *
|
||||
|
|
||||
* / \\ *
|
||||
* *
|
||||
~~~~~~~~~~~~~~~~~~~~~''',
|
||||
// 프레임 3: 레벨업 텍스트
|
||||
'''
|
||||
* L E V E L U P ! *
|
||||
* *
|
||||
* \\O/ *
|
||||
|
|
||||
* / \\ *
|
||||
* *
|
||||
~~~~~~~~~~~~~~~~~~~~~''',
|
||||
// 프레임 4: 빛나는 캐릭터
|
||||
'''
|
||||
* * * * *
|
||||
* *
|
||||
* \\O/ *
|
||||
* | *
|
||||
* / \\ *
|
||||
* * * *
|
||||
~~~~~~~~~~~~~~~~~~~~~''',
|
||||
// 프레임 5: 마무리
|
||||
'''
|
||||
+
|
||||
+++
|
||||
+++++
|
||||
\\O/
|
||||
|
|
||||
/ \\
|
||||
~~~~~~~~~~~~~~~~~~~~~''',
|
||||
],
|
||||
frameIntervalMs: 300,
|
||||
);
|
||||
|
||||
/// 퀘스트 완료 애니메이션
|
||||
const questCompleteAnimation = AsciiAnimationData(
|
||||
frames: [
|
||||
// 프레임 1: 퀘스트 깃발
|
||||
'''
|
||||
[=======]
|
||||
|| ||
|
||||
|| \\O/ ||
|
||||
|| | ||
|
||||
|| / \\ ||
|
||||
||_____||
|
||||
~~~~~~~~~~~~~~~~~~~''',
|
||||
// 프레임 2: 승리
|
||||
'''
|
||||
[QUEST!]
|
||||
|| ||
|
||||
\\\\O//
|
||||
\\|/
|
||||
/ \\
|
||||
||_____||
|
||||
~~~~~~~~~~~~~~~~~~~''',
|
||||
// 프레임 3: 보상
|
||||
'''
|
||||
COMPLETE!
|
||||
|
||||
\\O/ \$\$\$
|
||||
| \$\$\$
|
||||
/ \\ \$\$\$
|
||||
|
||||
~~~~~~~~~~~~~~~~~~~''',
|
||||
// 프레임 4: 축하
|
||||
'''
|
||||
* * * * *
|
||||
\\O/
|
||||
| +EXP
|
||||
/ \\ +GOLD
|
||||
* * * * *
|
||||
|
||||
~~~~~~~~~~~~~~~~~~~''',
|
||||
// 프레임 5: 마무리
|
||||
'''
|
||||
[ VICTORY! ]
|
||||
|
||||
\\O/
|
||||
|
|
||||
/ \\
|
||||
|
||||
~~~~~~~~~~~~~~~~~~~''',
|
||||
],
|
||||
frameIntervalMs: 350,
|
||||
);
|
||||
|
||||
/// Act 완료 애니메이션 (플롯 진행)
|
||||
const actCompleteAnimation = AsciiAnimationData(
|
||||
frames: [
|
||||
// 프레임 1: 커튼
|
||||
'''
|
||||
____________________
|
||||
| |
|
||||
| A C T |
|
||||
| |
|
||||
| C O M P L E T E |
|
||||
| |
|
||||
|____________________|''',
|
||||
// 프레임 2: 캐릭터 등장
|
||||
'''
|
||||
____________________
|
||||
| * * * * * |
|
||||
| \\O/ |
|
||||
| | |
|
||||
| / \\ |
|
||||
| * * * * * |
|
||||
|____________________|''',
|
||||
// 프레임 3: 플롯 진행 표시
|
||||
'''
|
||||
____________________
|
||||
| PROLOGUE --> ACT |
|
||||
| \\O/ |
|
||||
| | --> |
|
||||
| / \\ |
|
||||
| STORY CONTINUES |
|
||||
|____________________|''',
|
||||
// 프레임 4: 축하
|
||||
'''
|
||||
____________________
|
||||
| * * * * * |
|
||||
| * \\O/ * |
|
||||
| | |
|
||||
| * / \\ * |
|
||||
| * * * * * |
|
||||
|____________________|''',
|
||||
// 프레임 5: 마무리
|
||||
'''
|
||||
____________________
|
||||
| +---------+ |
|
||||
| | NEXT | |
|
||||
| | CHAPTER | |
|
||||
| +---------+ |
|
||||
| \\O/ |
|
||||
|____________________|''',
|
||||
],
|
||||
frameIntervalMs: 400,
|
||||
);
|
||||
|
||||
/// 타입별 애니메이션 데이터 반환 (기본 전투는 beast)
|
||||
AsciiAnimationData getAnimationData(AsciiAnimationType type) {
|
||||
return switch (type) {
|
||||
AsciiAnimationType.battle => battleAnimationBeast,
|
||||
AsciiAnimationType.town => townAnimation,
|
||||
AsciiAnimationType.walking => walkingAnimation,
|
||||
AsciiAnimationType.levelUp => levelUpAnimation,
|
||||
AsciiAnimationType.questComplete => questCompleteAnimation,
|
||||
AsciiAnimationType.actComplete => actCompleteAnimation,
|
||||
};
|
||||
}
|
||||
35
lib/src/core/animation/ascii_animation_type.dart
Normal file
35
lib/src/core/animation/ascii_animation_type.dart
Normal file
@@ -0,0 +1,35 @@
|
||||
import 'package:askiineverdie/src/core/model/game_state.dart';
|
||||
|
||||
/// ASCII 애니메이션 타입 (TaskType과 매핑)
|
||||
enum AsciiAnimationType {
|
||||
/// 전투 장면 (캐릭터 vs 몬스터)
|
||||
battle,
|
||||
|
||||
/// 마을/상점 장면
|
||||
town,
|
||||
|
||||
/// 걷는 캐릭터
|
||||
walking,
|
||||
|
||||
/// 레벨업 축하
|
||||
levelUp,
|
||||
|
||||
/// 퀘스트 완료
|
||||
questComplete,
|
||||
|
||||
/// Act 완료 (플롯 진행)
|
||||
actComplete,
|
||||
}
|
||||
|
||||
/// TaskType을 AsciiAnimationType으로 변환
|
||||
AsciiAnimationType taskTypeToAnimation(TaskType taskType) {
|
||||
return switch (taskType) {
|
||||
TaskType.kill => AsciiAnimationType.battle,
|
||||
TaskType.market ||
|
||||
TaskType.sell ||
|
||||
TaskType.buying => AsciiAnimationType.town,
|
||||
TaskType.neutral ||
|
||||
TaskType.load ||
|
||||
TaskType.plot => AsciiAnimationType.walking,
|
||||
};
|
||||
}
|
||||
24
lib/src/core/storage/theme_preferences.dart
Normal file
24
lib/src/core/storage/theme_preferences.dart
Normal file
@@ -0,0 +1,24 @@
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
import 'package:askiineverdie/src/core/animation/ascii_animation_data.dart';
|
||||
|
||||
/// 테마 설정 저장/로드 서비스
|
||||
class ThemePreferences {
|
||||
static const _keyColorTheme = 'ascii_color_theme';
|
||||
|
||||
/// 테마 설정 저장
|
||||
static Future<void> saveColorTheme(AsciiColorTheme theme) async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setInt(_keyColorTheme, theme.index);
|
||||
}
|
||||
|
||||
/// 테마 설정 로드 (기본값: green)
|
||||
static Future<AsciiColorTheme> loadColorTheme() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final index = prefs.getInt(_keyColorTheme);
|
||||
if (index == null || index < 0 || index >= AsciiColorTheme.values.length) {
|
||||
return AsciiColorTheme.green;
|
||||
}
|
||||
return AsciiColorTheme.values[index];
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,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/model/game_state.dart';
|
||||
import 'package:askiineverdie/src/core/storage/theme_preferences.dart';
|
||||
import 'package:askiineverdie/src/core/util/pq_logic.dart' as pq_logic;
|
||||
import 'package:askiineverdie/src/features/game/game_session_controller.dart';
|
||||
import 'package:askiineverdie/src/features/game/widgets/task_progress_panel.dart';
|
||||
|
||||
/// 게임 진행 화면 (Main.dfm 기반 3패널 레이아웃)
|
||||
class GamePlayScreen extends StatefulWidget {
|
||||
@@ -16,11 +20,85 @@ class GamePlayScreen extends StatefulWidget {
|
||||
|
||||
class _GamePlayScreenState extends State<GamePlayScreen>
|
||||
with WidgetsBindingObserver {
|
||||
AsciiColorTheme _colorTheme = AsciiColorTheme.green;
|
||||
AsciiAnimationType? _specialAnimation;
|
||||
|
||||
// 이전 상태 추적 (레벨업/퀘스트/Act 완료 감지용)
|
||||
int _lastLevel = 0;
|
||||
int _lastQuestCount = 0;
|
||||
int _lastPlotStageCount = 0;
|
||||
|
||||
void _cycleColorTheme() {
|
||||
setState(() {
|
||||
_colorTheme = switch (_colorTheme) {
|
||||
AsciiColorTheme.green => AsciiColorTheme.amber,
|
||||
AsciiColorTheme.amber => AsciiColorTheme.white,
|
||||
AsciiColorTheme.white => AsciiColorTheme.system,
|
||||
AsciiColorTheme.system => AsciiColorTheme.green,
|
||||
};
|
||||
});
|
||||
// 테마 변경 시 저장
|
||||
ThemePreferences.saveColorTheme(_colorTheme);
|
||||
}
|
||||
|
||||
Future<void> _loadColorTheme() async {
|
||||
final theme = await ThemePreferences.loadColorTheme();
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_colorTheme = theme;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void _checkSpecialEvents(GameState state) {
|
||||
// 레벨업 감지
|
||||
if (state.traits.level > _lastLevel && _lastLevel > 0) {
|
||||
_specialAnimation = AsciiAnimationType.levelUp;
|
||||
_resetSpecialAnimationAfterFrame();
|
||||
}
|
||||
_lastLevel = state.traits.level;
|
||||
|
||||
// 퀘스트 완료 감지
|
||||
if (state.progress.questCount > _lastQuestCount && _lastQuestCount > 0) {
|
||||
_specialAnimation = AsciiAnimationType.questComplete;
|
||||
_resetSpecialAnimationAfterFrame();
|
||||
}
|
||||
_lastQuestCount = state.progress.questCount;
|
||||
|
||||
// Act 완료 감지 (plotStageCount 증가)
|
||||
if (state.progress.plotStageCount > _lastPlotStageCount &&
|
||||
_lastPlotStageCount > 0) {
|
||||
_specialAnimation = AsciiAnimationType.actComplete;
|
||||
_resetSpecialAnimationAfterFrame();
|
||||
}
|
||||
_lastPlotStageCount = state.progress.plotStageCount;
|
||||
}
|
||||
|
||||
void _resetSpecialAnimationAfterFrame() {
|
||||
// 다음 프레임에서 리셋 (AsciiAnimationCard가 값을 받은 후)
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_specialAnimation = null;
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
widget.controller.addListener(_onControllerChanged);
|
||||
WidgetsBinding.instance.addObserver(this);
|
||||
_loadColorTheme();
|
||||
|
||||
// 초기 상태 설정
|
||||
final state = widget.controller.state;
|
||||
if (state != null) {
|
||||
_lastLevel = state.traits.level;
|
||||
_lastQuestCount = state.progress.questCount;
|
||||
_lastPlotStageCount = state.progress.plotStageCount;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -83,6 +161,10 @@ class _GamePlayScreenState extends State<GamePlayScreen>
|
||||
}
|
||||
|
||||
void _onControllerChanged() {
|
||||
final state = widget.controller.state;
|
||||
if (state != null) {
|
||||
_checkSpecialEvents(state);
|
||||
}
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
@@ -131,6 +213,19 @@ class _GamePlayScreenState extends State<GamePlayScreen>
|
||||
),
|
||||
body: Column(
|
||||
children: [
|
||||
// 상단: ASCII 애니메이션 + Task Progress
|
||||
TaskProgressPanel(
|
||||
progress: state.progress,
|
||||
speedMultiplier: widget.controller.loop?.speedMultiplier ?? 1,
|
||||
onSpeedCycle: () {
|
||||
widget.controller.loop?.cycleSpeed();
|
||||
setState(() {});
|
||||
},
|
||||
colorTheme: _colorTheme,
|
||||
onThemeCycle: _cycleColorTheme,
|
||||
specialAnimation: _specialAnimation,
|
||||
),
|
||||
|
||||
// 메인 3패널 영역
|
||||
Expanded(
|
||||
child: Row(
|
||||
@@ -147,9 +242,6 @@ class _GamePlayScreenState extends State<GamePlayScreen>
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// 하단: Task Progress
|
||||
_buildBottomPanel(state),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -261,71 +353,6 @@ class _GamePlayScreenState extends State<GamePlayScreen>
|
||||
);
|
||||
}
|
||||
|
||||
/// 하단 패널: Task Progress + Status
|
||||
Widget _buildBottomPanel(GameState state) {
|
||||
final speed = widget.controller.loop?.speedMultiplier ?? 1;
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.surfaceContainerHighest,
|
||||
border: Border(top: BorderSide(color: Theme.of(context).dividerColor)),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// 상태 메시지 + 배속 버튼
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
state.progress.currentTask.caption.isNotEmpty
|
||||
? state.progress.currentTask.caption
|
||||
: 'Welcome to Progress Quest!',
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
// 배속 버튼
|
||||
SizedBox(
|
||||
height: 28,
|
||||
child: OutlinedButton(
|
||||
onPressed: () {
|
||||
widget.controller.loop?.cycleSpeed();
|
||||
setState(() {});
|
||||
},
|
||||
style: OutlinedButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||
visualDensity: VisualDensity.compact,
|
||||
),
|
||||
child: Text(
|
||||
'${speed}x',
|
||||
style: TextStyle(
|
||||
fontWeight: speed > 1
|
||||
? FontWeight.bold
|
||||
: FontWeight.normal,
|
||||
color: speed > 1
|
||||
? Theme.of(context).colorScheme.primary
|
||||
: null,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
|
||||
// Task Progress 바
|
||||
_buildProgressBar(
|
||||
state.progress.task.position,
|
||||
state.progress.task.max,
|
||||
Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildPanelHeader(String title) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
|
||||
168
lib/src/features/game/widgets/ascii_animation_card.dart
Normal file
168
lib/src/features/game/widgets/ascii_animation_card.dart
Normal file
@@ -0,0 +1,168 @@
|
||||
import 'dart:async';
|
||||
|
||||
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/model/game_state.dart';
|
||||
|
||||
/// ASCII 애니메이션 카드 위젯
|
||||
///
|
||||
/// TaskType에 따라 다른 애니메이션을 표시.
|
||||
/// 전투 시 몬스터 이름에 따라 다른 애니메이션 선택.
|
||||
/// 특수 이벤트(레벨업, 퀘스트 완료) 시 오버라이드 애니메이션 재생.
|
||||
/// 자체 타이머로 프레임 전환 (게임 틱과 독립).
|
||||
class AsciiAnimationCard extends StatefulWidget {
|
||||
const AsciiAnimationCard({
|
||||
super.key,
|
||||
required this.taskType,
|
||||
this.monsterBaseName,
|
||||
this.colorTheme = AsciiColorTheme.green,
|
||||
this.specialAnimation,
|
||||
});
|
||||
|
||||
final TaskType taskType;
|
||||
|
||||
/// 전투 중인 몬스터 기본 이름 (kill 타입일 때만 사용)
|
||||
final String? monsterBaseName;
|
||||
final AsciiColorTheme colorTheme;
|
||||
|
||||
/// 특수 애니메이션 오버라이드 (레벨업, 퀘스트 완료 등)
|
||||
/// 설정되면 일반 애니메이션 대신 표시
|
||||
final AsciiAnimationType? specialAnimation;
|
||||
|
||||
@override
|
||||
State<AsciiAnimationCard> createState() => _AsciiAnimationCardState();
|
||||
}
|
||||
|
||||
class _AsciiAnimationCardState extends State<AsciiAnimationCard> {
|
||||
Timer? _timer;
|
||||
int _currentFrame = 0;
|
||||
late AsciiAnimationData _animationData;
|
||||
AsciiAnimationType? _currentSpecialAnimation;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_updateAnimation();
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(AsciiAnimationCard oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
|
||||
// 특수 애니메이션이 변경되었으면 업데이트
|
||||
if (oldWidget.specialAnimation != widget.specialAnimation) {
|
||||
_currentSpecialAnimation = widget.specialAnimation;
|
||||
_updateAnimation();
|
||||
return;
|
||||
}
|
||||
|
||||
// 특수 애니메이션이 활성화되어 있으면 일반 업데이트 무시
|
||||
if (_currentSpecialAnimation != null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (oldWidget.taskType != widget.taskType ||
|
||||
oldWidget.monsterBaseName != widget.monsterBaseName) {
|
||||
_updateAnimation();
|
||||
}
|
||||
}
|
||||
|
||||
void _updateAnimation() {
|
||||
_timer?.cancel();
|
||||
|
||||
// 특수 애니메이션이 있으면 우선 적용
|
||||
if (_currentSpecialAnimation != null) {
|
||||
_animationData = getAnimationData(_currentSpecialAnimation!);
|
||||
_currentFrame = 0;
|
||||
|
||||
// 특수 애니메이션은 한 번 재생 후 종료
|
||||
_timer = Timer.periodic(
|
||||
Duration(milliseconds: _animationData.frameIntervalMs),
|
||||
(_) {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_currentFrame++;
|
||||
// 마지막 프레임에 도달하면 특수 애니메이션 종료
|
||||
if (_currentFrame >= _animationData.frames.length) {
|
||||
_currentSpecialAnimation = null;
|
||||
_updateAnimation();
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// 일반 애니메이션 처리
|
||||
final animationType = taskTypeToAnimation(widget.taskType);
|
||||
|
||||
// 전투 타입이면 몬스터 카테고리에 따라 다른 애니메이션 선택
|
||||
if (animationType == AsciiAnimationType.battle) {
|
||||
final category = getMonsterCategory(widget.monsterBaseName);
|
||||
_animationData = getBattleAnimation(category);
|
||||
} else {
|
||||
_animationData = getAnimationData(animationType);
|
||||
}
|
||||
|
||||
_currentFrame = 0;
|
||||
|
||||
_timer = Timer.periodic(
|
||||
Duration(milliseconds: _animationData.frameIntervalMs),
|
||||
(_) {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_currentFrame = (_currentFrame + 1) % _animationData.frames.length;
|
||||
});
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_timer?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final brightness = Theme.of(context).brightness;
|
||||
final colors = getThemeColors(widget.colorTheme, brightness);
|
||||
|
||||
// 특수 애니메이션 중이면 특별한 배경색 적용
|
||||
final isSpecial = _currentSpecialAnimation != null;
|
||||
final bgColor = isSpecial
|
||||
? colors.backgroundColor.withValues(alpha: 0.95)
|
||||
: colors.backgroundColor;
|
||||
|
||||
// 프레임 인덱스가 범위를 벗어나지 않도록 보정
|
||||
final frameIndex = _currentFrame.clamp(0, _animationData.frames.length - 1);
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: bgColor,
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
border: isSpecial
|
||||
? 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,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
157
lib/src/features/game/widgets/task_progress_panel.dart
Normal file
157
lib/src/features/game/widgets/task_progress_panel.dart
Normal file
@@ -0,0 +1,157 @@
|
||||
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/model/game_state.dart';
|
||||
import 'package:askiineverdie/src/features/game/widgets/ascii_animation_card.dart';
|
||||
|
||||
/// 상단 패널: ASCII 애니메이션 + Task Progress 바
|
||||
class TaskProgressPanel extends StatelessWidget {
|
||||
const TaskProgressPanel({
|
||||
super.key,
|
||||
required this.progress,
|
||||
required this.speedMultiplier,
|
||||
required this.onSpeedCycle,
|
||||
required this.colorTheme,
|
||||
required this.onThemeCycle,
|
||||
this.specialAnimation,
|
||||
});
|
||||
|
||||
final ProgressState progress;
|
||||
final int speedMultiplier;
|
||||
final VoidCallback onSpeedCycle;
|
||||
final AsciiColorTheme colorTheme;
|
||||
final VoidCallback onThemeCycle;
|
||||
|
||||
/// 특수 애니메이션 (레벨업, 퀘스트 완료 등)
|
||||
final AsciiAnimationType? specialAnimation;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.surfaceContainerHighest,
|
||||
border: Border(
|
||||
bottom: BorderSide(color: Theme.of(context).dividerColor),
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// ASCII 애니메이션 카드
|
||||
SizedBox(
|
||||
height: 120,
|
||||
child: AsciiAnimationCard(
|
||||
taskType: progress.currentTask.type,
|
||||
monsterBaseName: progress.currentTask.monsterBaseName,
|
||||
colorTheme: colorTheme,
|
||||
specialAnimation: specialAnimation,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
|
||||
// 상태 메시지 + 버튼들
|
||||
Row(
|
||||
children: [
|
||||
_buildThemeButton(context),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
progress.currentTask.caption.isNotEmpty
|
||||
? progress.currentTask.caption
|
||||
: 'Welcome to Progress Quest!',
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
_buildSpeedButton(context),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
|
||||
// Task Progress 바
|
||||
_buildProgressBar(context),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildThemeButton(BuildContext context) {
|
||||
final themeLabel = switch (colorTheme) {
|
||||
AsciiColorTheme.green => 'G',
|
||||
AsciiColorTheme.amber => 'A',
|
||||
AsciiColorTheme.white => 'W',
|
||||
AsciiColorTheme.system => 'S',
|
||||
};
|
||||
|
||||
final themeColor = switch (colorTheme) {
|
||||
AsciiColorTheme.green => const Color(0xFF00FF00),
|
||||
AsciiColorTheme.amber => const Color(0xFFFFB000),
|
||||
AsciiColorTheme.white => Colors.white,
|
||||
AsciiColorTheme.system => Theme.of(context).colorScheme.primary,
|
||||
};
|
||||
|
||||
return SizedBox(
|
||||
height: 28,
|
||||
child: OutlinedButton(
|
||||
onPressed: onThemeCycle,
|
||||
style: OutlinedButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||
visualDensity: VisualDensity.compact,
|
||||
side: BorderSide(color: themeColor.withValues(alpha: 0.5)),
|
||||
),
|
||||
child: Text(
|
||||
themeLabel,
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: themeColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSpeedButton(BuildContext context) {
|
||||
return SizedBox(
|
||||
height: 28,
|
||||
child: OutlinedButton(
|
||||
onPressed: onSpeedCycle,
|
||||
style: OutlinedButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||
visualDensity: VisualDensity.compact,
|
||||
),
|
||||
child: Text(
|
||||
'${speedMultiplier}x',
|
||||
style: TextStyle(
|
||||
fontWeight:
|
||||
speedMultiplier > 1 ? FontWeight.bold : FontWeight.normal,
|
||||
color: speedMultiplier > 1
|
||||
? Theme.of(context).colorScheme.primary
|
||||
: null,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildProgressBar(BuildContext context) {
|
||||
final progressValue = progress.task.max > 0
|
||||
? (progress.task.position / progress.task.max).clamp(0.0, 1.0)
|
||||
: 0.0;
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
child: LinearProgressIndicator(
|
||||
value: progressValue,
|
||||
backgroundColor:
|
||||
Theme.of(context).colorScheme.primary.withValues(alpha: 0.2),
|
||||
valueColor: AlwaysStoppedAnimation<Color>(
|
||||
Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
minHeight: 12,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user