feat(ui): 게임 플레이 화면 레트로 UI 및 로직 개선

- 레트로 색상/스타일 전체 적용
- 다이얼로그들 RetroDialog로 통일
- 설정 화면 레트로 테마 적용
This commit is contained in:
JiWoong Sul
2025-12-30 19:04:00 +09:00
parent 27e05fb3c1
commit 9e96b94465

View File

@@ -2,6 +2,7 @@ import 'package:flutter/foundation.dart'
show kIsWeb, defaultTargetPlatform, TargetPlatform; show kIsWeb, defaultTargetPlatform, TargetPlatform;
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart' show SchedulerBinding, SchedulerPhase; import 'package:flutter/scheduler.dart' show SchedulerBinding, SchedulerPhase;
import 'package:flutter/services.dart' show KeyDownEvent, LogicalKeyboardKey;
import 'package:askiineverdie/data/game_text_l10n.dart' as game_l10n; import 'package:askiineverdie/data/game_text_l10n.dart' as game_l10n;
import 'package:askiineverdie/data/skill_data.dart'; import 'package:askiineverdie/data/skill_data.dart';
@@ -846,7 +847,11 @@ class _GamePlayScreenState extends State<GamePlayScreen>
} }
} }
}, },
child: Scaffold( // 웹/데스크톱 키보드 단축키 지원
child: Focus(
autofocus: true,
onKeyEvent: (node, event) => _handleKeyboardShortcut(event, context),
child: Scaffold(
backgroundColor: RetroColors.deepBrown, backgroundColor: RetroColors.deepBrown,
appBar: AppBar( appBar: AppBar(
backgroundColor: RetroColors.darkBrown, backgroundColor: RetroColors.darkBrown,
@@ -956,10 +961,54 @@ class _GamePlayScreenState extends State<GamePlayScreen>
], ],
), ),
), ),
),
), ),
); );
} }
/// 키보드 단축키 핸들러 (웹/데스크톱)
/// Space: 일시정지/재개, S: 속도 변경, H: 도움말, Esc: 설정
KeyEventResult _handleKeyboardShortcut(KeyEvent event, BuildContext context) {
// KeyDown 이벤트만 처리
if (event is! KeyDownEvent) return KeyEventResult.ignored;
final key = event.logicalKey;
// Space: 일시정지/재개
if (key == LogicalKeyboardKey.space) {
widget.controller.togglePause();
setState(() {});
return KeyEventResult.handled;
}
// S: 속도 변경
if (key == LogicalKeyboardKey.keyS) {
widget.controller.loop?.cycleSpeed();
setState(() {});
return KeyEventResult.handled;
}
// H 또는 F1: 도움말
if (key == LogicalKeyboardKey.keyH || key == LogicalKeyboardKey.f1) {
HelpDialog.show(context);
return KeyEventResult.handled;
}
// Escape: 설정
if (key == LogicalKeyboardKey.escape) {
_showSettingsScreen(context);
return KeyEventResult.handled;
}
// I: 통계
if (key == LogicalKeyboardKey.keyI) {
_showStatisticsDialog(context);
return KeyEventResult.handled;
}
return KeyEventResult.ignored;
}
/// 좌측 패널: Character Sheet (Traits, Stats, Experience, Spells) /// 좌측 패널: Character Sheet (Traits, Stats, Experience, Spells)
Widget _buildCharacterPanel(GameState state) { Widget _buildCharacterPanel(GameState state) {
final l10n = L10n.of(context); final l10n = L10n.of(context);
@@ -1170,6 +1219,7 @@ class _GamePlayScreenState extends State<GamePlayScreen>
); );
} }
/// 레트로 스타일 세그먼트 프로그레스 바
Widget _buildProgressBar( Widget _buildProgressBar(
int position, int position,
int max, int max,
@@ -1177,13 +1227,37 @@ class _GamePlayScreenState extends State<GamePlayScreen>
String? tooltip, String? tooltip,
}) { }) {
final progress = max > 0 ? (position / max).clamp(0.0, 1.0) : 0.0; final progress = max > 0 ? (position / max).clamp(0.0, 1.0) : 0.0;
const segmentCount = 20;
final filledSegments = (progress * segmentCount).round();
final bar = Padding( final bar = Padding(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
child: LinearProgressIndicator( child: Container(
value: progress, height: 12,
backgroundColor: color.withValues(alpha: 0.2), decoration: BoxDecoration(
valueColor: AlwaysStoppedAnimation<Color>(color), color: color.withValues(alpha: 0.15),
minHeight: 12, border: Border.all(color: RetroColors.panelBorderOuter, width: 1),
),
child: Row(
children: List.generate(segmentCount, (index) {
final isFilled = index < filledSegments;
return Expanded(
child: Container(
decoration: BoxDecoration(
color: isFilled ? color : color.withValues(alpha: 0.1),
border: Border(
right: index < segmentCount - 1
? BorderSide(
color: RetroColors.panelBorderOuter.withValues(alpha: 0.3),
width: 1,
)
: BorderSide.none,
),
),
),
);
}),
),
), ),
); );
@@ -1203,26 +1277,37 @@ class _GamePlayScreenState extends State<GamePlayScreen>
]; ];
return Padding( return Padding(
padding: const EdgeInsets.symmetric(horizontal: 8), padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
child: Column( child: Column(
children: traits.map((t) { children: traits.map((t) {
return Row( return Padding(
children: [ padding: const EdgeInsets.symmetric(vertical: 1),
SizedBox( child: Row(
width: 50, children: [
child: Text(t.$1, style: const TextStyle(fontSize: 11)), SizedBox(
), width: 50,
Expanded( child: Text(
child: Text( t.$1.toUpperCase(),
t.$2, style: const TextStyle(
style: const TextStyle( fontFamily: 'PressStart2P',
fontSize: 11, fontSize: 6,
fontWeight: FontWeight.bold, color: RetroColors.textDisabled,
),
), ),
overflow: TextOverflow.ellipsis,
), ),
), Expanded(
], child: Text(
t.$2,
style: const TextStyle(
fontFamily: 'PressStart2P',
fontSize: 7,
color: RetroColors.textLight,
),
overflow: TextOverflow.ellipsis,
),
),
],
),
); );
}).toList(), }).toList(),
), ),
@@ -1237,7 +1322,11 @@ class _GamePlayScreenState extends State<GamePlayScreen>
return Center( return Center(
child: Text( child: Text(
L10n.of(context).noSpellsYet, L10n.of(context).noSpellsYet,
style: const TextStyle(fontSize: 11), style: const TextStyle(
fontFamily: 'PressStart2P',
fontSize: 7,
color: RetroColors.textDisabled,
),
), ),
); );
} }
@@ -1274,7 +1363,11 @@ class _GamePlayScreenState extends State<GamePlayScreen>
return Center( return Center(
child: Text( child: Text(
l10n.goldAmount(state.inventory.gold), l10n.goldAmount(state.inventory.gold),
style: const TextStyle(fontSize: 11), style: const TextStyle(
fontFamily: 'PressStart2P',
fontSize: 7,
color: RetroColors.gold,
),
), ),
); );
} }
@@ -1284,19 +1377,32 @@ class _GamePlayScreenState extends State<GamePlayScreen>
padding: const EdgeInsets.symmetric(horizontal: 8), padding: const EdgeInsets.symmetric(horizontal: 8),
itemBuilder: (context, index) { itemBuilder: (context, index) {
if (index == 0) { if (index == 0) {
return Row( return Padding(
children: [ padding: const EdgeInsets.symmetric(vertical: 2),
Expanded( child: Row(
child: Text(l10n.gold, style: const TextStyle(fontSize: 11)), children: [
), const Icon(Icons.monetization_on, size: 10, color: RetroColors.gold),
Text( const SizedBox(width: 4),
'${state.inventory.gold}', Expanded(
style: const TextStyle( child: Text(
fontSize: 11, l10n.gold.toUpperCase(),
fontWeight: FontWeight.bold, style: const TextStyle(
fontFamily: 'PressStart2P',
fontSize: 6,
color: RetroColors.gold,
),
),
), ),
), Text(
], '${state.inventory.gold}',
style: const TextStyle(
fontFamily: 'PressStart2P',
fontSize: 7,
color: RetroColors.gold,
),
),
],
),
); );
} }
final item = state.inventory.items[index - 1]; final item = state.inventory.items[index - 1];
@@ -1305,20 +1411,31 @@ class _GamePlayScreenState extends State<GamePlayScreen>
context, context,
item.name, item.name,
); );
return Row( return Padding(
children: [ padding: const EdgeInsets.symmetric(vertical: 1),
Expanded( child: Row(
child: Text( children: [
translatedName, Expanded(
style: const TextStyle(fontSize: 11), child: Text(
overflow: TextOverflow.ellipsis, translatedName,
style: const TextStyle(
fontFamily: 'PressStart2P',
fontSize: 6,
color: RetroColors.textLight,
),
overflow: TextOverflow.ellipsis,
),
), ),
), Text(
Text( '${item.count}',
'${item.count}', style: const TextStyle(
style: const TextStyle(fontSize: 11, fontWeight: FontWeight.bold), fontFamily: 'PressStart2P',
), fontSize: 6,
], color: RetroColors.cream,
),
),
],
),
); );
}, },
); );
@@ -1330,7 +1447,14 @@ class _GamePlayScreenState extends State<GamePlayScreen>
final plotCount = state.progress.plotStageCount; final plotCount = state.progress.plotStageCount;
if (plotCount == 0) { if (plotCount == 0) {
return Center( return Center(
child: Text(l10n.prologue, style: const TextStyle(fontSize: 11)), child: Text(
l10n.prologue.toUpperCase(),
style: const TextStyle(
fontFamily: 'PressStart2P',
fontSize: 7,
color: RetroColors.textDisabled,
),
),
); );
} }
@@ -1339,24 +1463,38 @@ class _GamePlayScreenState extends State<GamePlayScreen>
padding: const EdgeInsets.symmetric(horizontal: 8), padding: const EdgeInsets.symmetric(horizontal: 8),
itemBuilder: (context, index) { itemBuilder: (context, index) {
final isCompleted = index < plotCount - 1; final isCompleted = index < plotCount - 1;
return Row( final isCurrent = index == plotCount - 1;
children: [ return Padding(
Icon( padding: const EdgeInsets.symmetric(vertical: 1),
isCompleted ? Icons.check_box : Icons.check_box_outline_blank, child: Row(
size: 14, children: [
color: isCompleted ? Colors.green : Colors.grey, Icon(
), isCompleted
const SizedBox(width: 4), ? Icons.check_box
Text( : (isCurrent ? Icons.arrow_right : Icons.check_box_outline_blank),
index == 0 ? l10n.prologue : l10n.actNumber(_toRoman(index)), size: 12,
style: TextStyle( color: isCompleted
fontSize: 11, ? RetroColors.expGreen
decoration: isCompleted : (isCurrent ? RetroColors.gold : RetroColors.textDisabled),
? TextDecoration.lineThrough
: TextDecoration.none,
), ),
), const SizedBox(width: 4),
], Expanded(
child: Text(
index == 0 ? l10n.prologue : l10n.actNumber(_toRoman(index)),
style: TextStyle(
fontFamily: 'PressStart2P',
fontSize: 6,
color: isCompleted
? RetroColors.textDisabled
: (isCurrent ? RetroColors.gold : RetroColors.textLight),
decoration: isCompleted
? TextDecoration.lineThrough
: TextDecoration.none,
),
),
),
],
),
); );
}, },
); );
@@ -1368,7 +1506,14 @@ class _GamePlayScreenState extends State<GamePlayScreen>
if (questHistory.isEmpty) { if (questHistory.isEmpty) {
return Center( return Center(
child: Text(l10n.noActiveQuests, style: const TextStyle(fontSize: 11)), child: Text(
l10n.noActiveQuests.toUpperCase(),
style: const TextStyle(
fontFamily: 'PressStart2P',
fontSize: 7,
color: RetroColors.textDisabled,
),
),
); );
} }
@@ -1382,32 +1527,44 @@ class _GamePlayScreenState extends State<GamePlayScreen>
final isCurrentQuest = final isCurrentQuest =
index == questHistory.length - 1 && !quest.isComplete; index == questHistory.length - 1 && !quest.isComplete;
return Row( return Padding(
children: [ padding: const EdgeInsets.symmetric(vertical: 1),
if (isCurrentQuest) child: Row(
const Icon(Icons.arrow_right, size: 14) children: [
else
Icon( Icon(
quest.isComplete isCurrentQuest
? Icons.check_box ? Icons.arrow_right
: Icons.check_box_outline_blank, : (quest.isComplete
size: 14, ? Icons.check_box
color: quest.isComplete ? Colors.green : Colors.grey, : Icons.check_box_outline_blank),
size: 12,
color: isCurrentQuest
? RetroColors.gold
: (quest.isComplete
? RetroColors.expGreen
: RetroColors.textDisabled),
), ),
const SizedBox(width: 4), const SizedBox(width: 4),
Expanded( Expanded(
child: Text( child: Text(
quest.caption, quest.caption,
style: TextStyle( style: TextStyle(
fontSize: 11, fontFamily: 'PressStart2P',
decoration: quest.isComplete fontSize: 6,
? TextDecoration.lineThrough color: isCurrentQuest
: TextDecoration.none, ? RetroColors.gold
: (quest.isComplete
? RetroColors.textDisabled
: RetroColors.textLight),
decoration: quest.isComplete
? TextDecoration.lineThrough
: TextDecoration.none,
),
overflow: TextOverflow.ellipsis,
), ),
overflow: TextOverflow.ellipsis,
), ),
), ],
], ),
); );
}, },
); );