feat(animation): ASCII 애니메이션 시스템 구현

- TaskType별 애니메이션 (전투, 마을, 걷기)
- 몬스터 카테고리별 전투 애니메이션 (7종)
- 특수 애니메이션 (레벨업, 퀘스트 완료, Act 완료)
- 색상 테마 옵션 (green, amber, white, system)
- 테마 설정 SharedPreferences 저장
- 프로그레스 바를 상단으로 이동
This commit is contained in:
JiWoong Sul
2025-12-11 16:49:02 +09:00
parent b450bf2600
commit 2b10deba5d
6 changed files with 1306 additions and 68 deletions

View File

@@ -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),