feat(ui): Phase 8 실시간 피드백 시스템 구현

- StatsPanel: 스탯 변화 애니메이션 (증감 표시)
- CombatLog: 전투 이벤트 로그 위젯
- NotificationService: 큐 기반 알림 관리
- NotificationOverlay: 레벨업/퀘스트 완료 팝업 알림
- GamePlayScreen: 새 위젯 통합
This commit is contained in:
JiWoong Sul
2025-12-17 18:33:21 +09:00
parent bfcec44ac7
commit 8cbef3475b
5 changed files with 760 additions and 48 deletions

View File

@@ -4,8 +4,11 @@ import 'package:askiineverdie/l10n/app_localizations.dart';
import 'package:askiineverdie/src/core/animation/ascii_animation_type.dart';
import 'package:askiineverdie/src/core/l10n/game_data_l10n.dart';
import 'package:askiineverdie/src/core/model/game_state.dart';
import 'package:askiineverdie/src/core/notification/notification_service.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/notification_overlay.dart';
import 'package:askiineverdie/src/features/game/widgets/stats_panel.dart';
import 'package:askiineverdie/src/features/game/widgets/task_progress_panel.dart';
/// 게임 진행 화면 (Main.dfm 기반 3패널 레이아웃)
@@ -24,6 +27,9 @@ class _GamePlayScreenState extends State<GamePlayScreen>
with WidgetsBindingObserver {
AsciiAnimationType? _specialAnimation;
// Phase 8: 알림 서비스 (Notification Service)
late final NotificationService _notificationService;
// 이전 상태 추적 (레벨업/퀘스트/Act 완료 감지용)
int _lastLevel = 0;
int _lastQuestCount = 0;
@@ -33,6 +39,7 @@ class _GamePlayScreenState extends State<GamePlayScreen>
// 레벨업 감지
if (state.traits.level > _lastLevel && _lastLevel > 0) {
_specialAnimation = AsciiAnimationType.levelUp;
_notificationService.showLevelUp(state.traits.level);
_resetSpecialAnimationAfterFrame();
}
_lastLevel = state.traits.level;
@@ -40,6 +47,13 @@ class _GamePlayScreenState extends State<GamePlayScreen>
// 퀘스트 완료 감지
if (state.progress.questCount > _lastQuestCount && _lastQuestCount > 0) {
_specialAnimation = AsciiAnimationType.questComplete;
// 완료된 퀘스트 이름 가져오기
final completedQuest = state.progress.questHistory
.where((q) => q.isComplete)
.lastOrNull;
if (completedQuest != null) {
_notificationService.showQuestComplete(completedQuest.caption);
}
_resetSpecialAnimationAfterFrame();
}
_lastQuestCount = state.progress.questCount;
@@ -48,6 +62,7 @@ class _GamePlayScreenState extends State<GamePlayScreen>
if (state.progress.plotStageCount > _lastPlotStageCount &&
_lastPlotStageCount > 0) {
_specialAnimation = AsciiAnimationType.actComplete;
_notificationService.showActComplete(state.progress.plotStageCount - 1);
_resetSpecialAnimationAfterFrame();
}
_lastPlotStageCount = state.progress.plotStageCount;
@@ -67,6 +82,7 @@ class _GamePlayScreenState extends State<GamePlayScreen>
@override
void initState() {
super.initState();
_notificationService = NotificationService();
widget.controller.addListener(_onControllerChanged);
WidgetsBinding.instance.addObserver(this);
@@ -81,6 +97,7 @@ class _GamePlayScreenState extends State<GamePlayScreen>
@override
void dispose() {
_notificationService.dispose();
WidgetsBinding.instance.removeObserver(this);
widget.controller.removeListener(_onControllerChanged);
super.dispose();
@@ -154,19 +171,21 @@ class _GamePlayScreenState extends State<GamePlayScreen>
return const Scaffold(body: Center(child: CircularProgressIndicator()));
}
return PopScope(
canPop: false,
onPopInvokedWithResult: (didPop, result) async {
if (didPop) return;
final shouldPop = await _onPopInvoked();
if (shouldPop && context.mounted) {
await widget.controller.pause(saveOnStop: false);
if (context.mounted) {
Navigator.of(context).pop();
return NotificationOverlay(
notificationService: _notificationService,
child: PopScope(
canPop: false,
onPopInvokedWithResult: (didPop, result) async {
if (didPop) return;
final shouldPop = await _onPopInvoked();
if (shouldPop && context.mounted) {
await widget.controller.pause(saveOnStop: false);
if (context.mounted) {
Navigator.of(context).pop();
}
}
}
},
child: Scaffold(
},
child: Scaffold(
appBar: AppBar(
title: Text(L10n.of(context).progressQuestTitle(state.traits.name)),
actions: [
@@ -230,6 +249,7 @@ class _GamePlayScreenState extends State<GamePlayScreen>
),
],
),
),
),
);
}
@@ -248,9 +268,9 @@ class _GamePlayScreenState extends State<GamePlayScreen>
_buildSectionHeader(l10n.traits),
_buildTraitsList(state),
// Stats 목록
// Stats 목록 (Phase 8: 애니메이션 변화 표시)
_buildSectionHeader(l10n.stats),
Expanded(flex: 2, child: _buildStatsList(state)),
Expanded(flex: 2, child: StatsPanel(stats: state.stats)),
// Experience 바
_buildSectionHeader(l10n.experience),
@@ -425,40 +445,6 @@ class _GamePlayScreenState extends State<GamePlayScreen>
);
}
Widget _buildStatsList(GameState state) {
final l10n = L10n.of(context);
final stats = [
(l10n.statStr, state.stats.str),
(l10n.statCon, state.stats.con),
(l10n.statDex, state.stats.dex),
(l10n.statInt, state.stats.intelligence),
(l10n.statWis, state.stats.wis),
(l10n.statCha, state.stats.cha),
(l10n.statHpMax, state.stats.hpMax),
(l10n.statMpMax, state.stats.mpMax),
];
return ListView.builder(
itemCount: stats.length,
padding: const EdgeInsets.symmetric(horizontal: 8),
itemBuilder: (context, index) {
final stat = stats[index];
return Row(
children: [
SizedBox(
width: 50,
child: Text(stat.$1, style: const TextStyle(fontSize: 11)),
),
Text(
'${stat.$2}',
style: const TextStyle(fontSize: 11, fontWeight: FontWeight.bold),
),
],
);
},
);
}
Widget _buildSpellsList(GameState state) {
if (state.spellBook.spells.isEmpty) {
return Center(