feat(ui): Phase 8 실시간 피드백 시스템 구현
- StatsPanel: 스탯 변화 애니메이션 (증감 표시) - CombatLog: 전투 이벤트 로그 위젯 - NotificationService: 큐 기반 알림 관리 - NotificationOverlay: 레벨업/퀘스트 완료 팝업 알림 - GamePlayScreen: 새 위젯 통합
This commit is contained in:
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user