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: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/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/core/util/pq_logic.dart' as pq_logic;
|
||||||
import 'package:askiineverdie/src/features/game/game_session_controller.dart';
|
import 'package:askiineverdie/src/features/game/game_session_controller.dart';
|
||||||
|
import 'package:askiineverdie/src/features/game/widgets/task_progress_panel.dart';
|
||||||
|
|
||||||
/// 게임 진행 화면 (Main.dfm 기반 3패널 레이아웃)
|
/// 게임 진행 화면 (Main.dfm 기반 3패널 레이아웃)
|
||||||
class GamePlayScreen extends StatefulWidget {
|
class GamePlayScreen extends StatefulWidget {
|
||||||
@@ -16,11 +20,85 @@ class GamePlayScreen extends StatefulWidget {
|
|||||||
|
|
||||||
class _GamePlayScreenState extends State<GamePlayScreen>
|
class _GamePlayScreenState extends State<GamePlayScreen>
|
||||||
with WidgetsBindingObserver {
|
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
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
widget.controller.addListener(_onControllerChanged);
|
widget.controller.addListener(_onControllerChanged);
|
||||||
WidgetsBinding.instance.addObserver(this);
|
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
|
@override
|
||||||
@@ -83,6 +161,10 @@ class _GamePlayScreenState extends State<GamePlayScreen>
|
|||||||
}
|
}
|
||||||
|
|
||||||
void _onControllerChanged() {
|
void _onControllerChanged() {
|
||||||
|
final state = widget.controller.state;
|
||||||
|
if (state != null) {
|
||||||
|
_checkSpecialEvents(state);
|
||||||
|
}
|
||||||
setState(() {});
|
setState(() {});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -131,6 +213,19 @@ class _GamePlayScreenState extends State<GamePlayScreen>
|
|||||||
),
|
),
|
||||||
body: Column(
|
body: Column(
|
||||||
children: [
|
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패널 영역
|
// 메인 3패널 영역
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Row(
|
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) {
|
Widget _buildPanelHeader(String title) {
|
||||||
return Container(
|
return Container(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
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