feat(ui): 도움말 다이얼로그 및 UI 개선
- HelpDialog 추가 - 게임 화면에 통계/도움말 버튼 추가 - CombatLog에 디버프 이벤트 표시 - AudioService mp3 확장자 지원 - 설정 텍스트 l10n 추가
This commit is contained in:
@@ -435,6 +435,12 @@ String combatBuffActivated(String skillName) {
|
||||
return '$skillName activated!';
|
||||
}
|
||||
|
||||
String combatDebuffApplied(String skillName, String targetName) {
|
||||
if (isKoreanLocale) return '$skillName → $targetName에 적용!';
|
||||
if (isJapaneseLocale) return '$skillName → $targetNameに適用!';
|
||||
return '$skillName applied to $targetName!';
|
||||
}
|
||||
|
||||
String combatDotTick(String skillName, int damage) {
|
||||
if (isKoreanLocale) return '$skillName: $damage 지속 데미지';
|
||||
if (isJapaneseLocale) return '$skillName: $damage 継続ダメージ';
|
||||
@@ -1803,6 +1809,18 @@ String get uiSettings {
|
||||
return 'Settings';
|
||||
}
|
||||
|
||||
String get uiStatistics {
|
||||
if (isKoreanLocale) return '통계';
|
||||
if (isJapaneseLocale) return '統計';
|
||||
return 'Statistics';
|
||||
}
|
||||
|
||||
String get uiHelp {
|
||||
if (isKoreanLocale) return '도움말';
|
||||
if (isJapaneseLocale) return 'ヘルプ';
|
||||
return 'Help';
|
||||
}
|
||||
|
||||
String get uiTheme {
|
||||
if (isKoreanLocale) return '테마';
|
||||
if (isJapaneseLocale) return 'テーマ';
|
||||
@@ -1851,6 +1869,12 @@ String get uiSfxVolume {
|
||||
return 'SFX Volume';
|
||||
}
|
||||
|
||||
String get uiSoundOff {
|
||||
if (isKoreanLocale) return '음소거';
|
||||
if (isJapaneseLocale) return 'ミュート';
|
||||
return 'Muted';
|
||||
}
|
||||
|
||||
String get uiAnimationSpeed {
|
||||
if (isKoreanLocale) return '애니메이션 속도';
|
||||
if (isJapaneseLocale) return 'アニメーション速度';
|
||||
|
||||
@@ -161,10 +161,12 @@ class _AskiiNeverDieAppState extends State<AskiiNeverDieApp> {
|
||||
},
|
||||
currentThemeMode: _themeMode,
|
||||
onThemeModeChange: _changeThemeMode,
|
||||
audioService: _audioService,
|
||||
);
|
||||
}
|
||||
|
||||
// 세이브 파일이 없으면 기존 프론트 화면
|
||||
// 세이브 파일이 없으면 기존 프론트 화면 (타이틀 BGM 재생)
|
||||
_audioService.playBgm('title');
|
||||
return FrontScreen(
|
||||
onNewCharacter: _navigateToNewCharacter,
|
||||
onLoadSave: _loadSave,
|
||||
@@ -238,6 +240,7 @@ class _AskiiNeverDieAppState extends State<AskiiNeverDieApp> {
|
||||
MaterialPageRoute<void>(
|
||||
builder: (context) => GamePlayScreen(
|
||||
controller: _controller,
|
||||
audioService: _audioService,
|
||||
forceCarouselLayout: testMode,
|
||||
currentThemeMode: _themeMode,
|
||||
onThemeModeChange: _changeThemeMode,
|
||||
@@ -252,6 +255,7 @@ class _AskiiNeverDieAppState extends State<AskiiNeverDieApp> {
|
||||
MaterialPageRoute<void>(
|
||||
builder: (context) => GamePlayScreen(
|
||||
controller: _controller,
|
||||
audioService: _audioService,
|
||||
currentThemeMode: _themeMode,
|
||||
onThemeModeChange: _changeThemeMode,
|
||||
),
|
||||
@@ -298,12 +302,14 @@ class _AutoLoadScreen extends StatefulWidget {
|
||||
required this.onLoadFailed,
|
||||
required this.currentThemeMode,
|
||||
required this.onThemeModeChange,
|
||||
this.audioService,
|
||||
});
|
||||
|
||||
final GameSessionController controller;
|
||||
final VoidCallback onLoadFailed;
|
||||
final ThemeMode currentThemeMode;
|
||||
final void Function(ThemeMode mode) onThemeModeChange;
|
||||
final AudioService? audioService;
|
||||
|
||||
@override
|
||||
State<_AutoLoadScreen> createState() => _AutoLoadScreenState();
|
||||
@@ -313,6 +319,8 @@ class _AutoLoadScreenState extends State<_AutoLoadScreen> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
// 로딩 중에도 타이틀 BGM 재생
|
||||
widget.audioService?.playBgm('title');
|
||||
_autoLoad();
|
||||
}
|
||||
|
||||
@@ -327,6 +335,7 @@ class _AutoLoadScreenState extends State<_AutoLoadScreen> {
|
||||
MaterialPageRoute<void>(
|
||||
builder: (context) => GamePlayScreen(
|
||||
controller: widget.controller,
|
||||
audioService: widget.audioService,
|
||||
currentThemeMode: widget.currentThemeMode,
|
||||
onThemeModeChange: widget.onThemeModeChange,
|
||||
),
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import 'package:flutter/foundation.dart' show debugPrint, kIsWeb;
|
||||
import 'package:just_audio/just_audio.dart';
|
||||
|
||||
import 'package:askiineverdie/src/core/storage/settings_repository.dart';
|
||||
@@ -5,6 +6,7 @@ import 'package:askiineverdie/src/core/storage/settings_repository.dart';
|
||||
/// 게임 오디오 서비스
|
||||
///
|
||||
/// BGM과 SFX를 관리하며, 설정과 연동하여 볼륨을 조절합니다.
|
||||
/// 웹/WASM 환경에서는 제한적으로 작동할 수 있습니다.
|
||||
class AudioService {
|
||||
AudioService({SettingsRepository? settingsRepository})
|
||||
: _settingsRepository = settingsRepository ?? SettingsRepository();
|
||||
@@ -28,48 +30,58 @@ class AudioService {
|
||||
// 초기화 여부
|
||||
bool _initialized = false;
|
||||
|
||||
// 초기화 실패 여부 (WASM 등에서 오디오 지원 안됨)
|
||||
bool _initFailed = false;
|
||||
|
||||
/// 서비스 초기화
|
||||
Future<void> init() async {
|
||||
if (_initialized) return;
|
||||
if (_initialized || _initFailed) return;
|
||||
|
||||
// 설정에서 볼륨 불러오기
|
||||
_bgmVolume = await _settingsRepository.loadBgmVolume();
|
||||
_sfxVolume = await _settingsRepository.loadSfxVolume();
|
||||
try {
|
||||
// 설정에서 볼륨 불러오기
|
||||
_bgmVolume = await _settingsRepository.loadBgmVolume();
|
||||
_sfxVolume = await _settingsRepository.loadSfxVolume();
|
||||
|
||||
// BGM 플레이어 초기화
|
||||
_bgmPlayer = AudioPlayer();
|
||||
await _bgmPlayer!.setLoopMode(LoopMode.one);
|
||||
await _bgmPlayer!.setVolume(_bgmVolume);
|
||||
// BGM 플레이어 초기화
|
||||
_bgmPlayer = AudioPlayer();
|
||||
await _bgmPlayer!.setLoopMode(LoopMode.one);
|
||||
await _bgmPlayer!.setVolume(_bgmVolume);
|
||||
|
||||
// SFX 플레이어 풀 초기화
|
||||
for (var i = 0; i < _maxSfxPlayers; i++) {
|
||||
final player = AudioPlayer();
|
||||
await player.setVolume(_sfxVolume);
|
||||
_sfxPlayers.add(player);
|
||||
// SFX 플레이어 풀 초기화
|
||||
for (var i = 0; i < _maxSfxPlayers; i++) {
|
||||
final player = AudioPlayer();
|
||||
await player.setVolume(_sfxVolume);
|
||||
_sfxPlayers.add(player);
|
||||
}
|
||||
|
||||
_initialized = true;
|
||||
if (kIsWeb) {
|
||||
debugPrint('[AudioService] Initialized on Web platform');
|
||||
}
|
||||
} catch (e) {
|
||||
_initFailed = true;
|
||||
debugPrint('[AudioService] Init failed (likely WASM): $e');
|
||||
}
|
||||
|
||||
_initialized = true;
|
||||
}
|
||||
|
||||
/// BGM 재생
|
||||
///
|
||||
/// [name]은 assets/audio/bgm/ 폴더 내 파일명 (확장자 제외)
|
||||
/// 예: playBgm('battle') → assets/audio/bgm/battle.wav 또는 battle.mp3
|
||||
/// 예: playBgm('battle') → assets/audio/bgm/battle.mp3
|
||||
Future<void> playBgm(String name) async {
|
||||
if (_initFailed) return; // 초기화 실패 시 무시
|
||||
if (!_initialized) await init();
|
||||
if (_initFailed || !_initialized) return;
|
||||
if (_currentBgm == name) return; // 이미 재생 중
|
||||
|
||||
try {
|
||||
_currentBgm = name;
|
||||
// WAV 먼저 시도, 실패하면 MP3 시도
|
||||
try {
|
||||
await _bgmPlayer!.setAsset('assets/audio/bgm/$name.wav');
|
||||
} catch (_) {
|
||||
await _bgmPlayer!.setAsset('assets/audio/bgm/$name.mp3');
|
||||
}
|
||||
await _bgmPlayer!.setAsset('assets/audio/bgm/$name.mp3');
|
||||
await _bgmPlayer!.play();
|
||||
debugPrint('[AudioService] Playing BGM: $name');
|
||||
} catch (e) {
|
||||
// 파일이 없으면 무시 (개발 중 에셋 미추가 상태)
|
||||
debugPrint('[AudioService] Failed to play BGM $name: $e');
|
||||
_currentBgm = null;
|
||||
}
|
||||
}
|
||||
@@ -99,10 +111,13 @@ class AudioService {
|
||||
/// SFX 재생
|
||||
///
|
||||
/// [name]은 assets/audio/sfx/ 폴더 내 파일명 (확장자 제외)
|
||||
/// 예: playSfx('attack') → assets/audio/sfx/attack.wav 또는 attack.mp3
|
||||
/// 예: playSfx('attack') → assets/audio/sfx/attack.mp3
|
||||
Future<void> playSfx(String name) async {
|
||||
if (_initFailed) return; // 초기화 실패 시 무시
|
||||
if (!_initialized) await init();
|
||||
if (_initFailed || !_initialized) return;
|
||||
if (_sfxVolume == 0) return; // 볼륨이 0이면 재생 안함
|
||||
if (_sfxPlayers.isEmpty) return;
|
||||
|
||||
// 사용 가능한 플레이어 찾기
|
||||
AudioPlayer? availablePlayer;
|
||||
@@ -117,16 +132,12 @@ class AudioService {
|
||||
availablePlayer ??= _sfxPlayers.first;
|
||||
|
||||
try {
|
||||
// WAV 먼저 시도, 실패하면 MP3 시도
|
||||
try {
|
||||
await availablePlayer.setAsset('assets/audio/sfx/$name.wav');
|
||||
} catch (_) {
|
||||
await availablePlayer.setAsset('assets/audio/sfx/$name.mp3');
|
||||
}
|
||||
await availablePlayer.setAsset('assets/audio/sfx/$name.mp3');
|
||||
await availablePlayer.seek(Duration.zero);
|
||||
await availablePlayer.play();
|
||||
} catch (e) {
|
||||
// 파일이 없으면 무시
|
||||
debugPrint('[AudioService] Failed to play SFX $name: $e');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -172,6 +183,9 @@ class AudioService {
|
||||
|
||||
/// BGM 타입 열거형
|
||||
enum BgmType {
|
||||
/// 타이틀 화면 BGM
|
||||
title,
|
||||
|
||||
/// 마을/상점 BGM
|
||||
town,
|
||||
|
||||
|
||||
@@ -32,7 +32,10 @@ import 'package:askiineverdie/src/features/game/widgets/task_progress_panel.dart
|
||||
import 'package:askiineverdie/src/features/game/widgets/active_buff_panel.dart';
|
||||
import 'package:askiineverdie/src/features/game/layouts/mobile_carousel_layout.dart';
|
||||
import 'package:askiineverdie/src/features/settings/settings_screen.dart';
|
||||
import 'package:askiineverdie/src/features/game/widgets/statistics_dialog.dart';
|
||||
import 'package:askiineverdie/src/features/game/widgets/help_dialog.dart';
|
||||
import 'package:askiineverdie/src/core/storage/settings_repository.dart';
|
||||
import 'package:askiineverdie/src/core/audio/audio_service.dart';
|
||||
|
||||
/// 게임 진행 화면 (Main.dfm 기반 3패널 레이아웃)
|
||||
///
|
||||
@@ -41,6 +44,7 @@ class GamePlayScreen extends StatefulWidget {
|
||||
const GamePlayScreen({
|
||||
super.key,
|
||||
required this.controller,
|
||||
this.audioService,
|
||||
this.forceCarouselLayout = false,
|
||||
this.forceDesktopLayout = false,
|
||||
this.onThemeModeChange,
|
||||
@@ -49,6 +53,9 @@ class GamePlayScreen extends StatefulWidget {
|
||||
|
||||
final GameSessionController controller;
|
||||
|
||||
/// 오디오 서비스 (BGM/SFX 재생)
|
||||
final AudioService? audioService;
|
||||
|
||||
/// 테스트 모드: 웹에서도 모바일 캐로셀 레이아웃 강제 사용
|
||||
final bool forceCarouselLayout;
|
||||
|
||||
@@ -89,6 +96,13 @@ class _GamePlayScreenState extends State<GamePlayScreen>
|
||||
// 전투 이벤트 추적 (마지막 처리된 이벤트 수)
|
||||
int _lastProcessedEventCount = 0;
|
||||
|
||||
// 오디오 상태 추적
|
||||
bool _wasInCombat = false;
|
||||
|
||||
// 사운드 볼륨 상태 (모바일 설정 UI용)
|
||||
double _bgmVolume = 0.7;
|
||||
double _sfxVolume = 0.8;
|
||||
|
||||
void _checkSpecialEvents(GameState state) {
|
||||
// Phase 8: 태스크 변경 시 로그 추가
|
||||
final currentCaption = state.progress.currentTask.caption;
|
||||
@@ -102,6 +116,9 @@ class _GamePlayScreenState extends State<GamePlayScreen>
|
||||
// 전투 이벤트 처리 (Combat Events)
|
||||
_processCombatEvents(state);
|
||||
|
||||
// 오디오: 전투 상태 변경 시 BGM 전환
|
||||
_updateBgmForCombatState(state);
|
||||
|
||||
// 레벨업 감지
|
||||
if (state.traits.level > _lastLevel && _lastLevel > 0) {
|
||||
_specialAnimation = AsciiAnimationType.levelUp;
|
||||
@@ -110,6 +127,8 @@ class _GamePlayScreenState extends State<GamePlayScreen>
|
||||
'${game_l10n.uiLevelUp} Lv.${state.traits.level}',
|
||||
CombatLogType.levelUp,
|
||||
);
|
||||
// 오디오: 레벨업 SFX
|
||||
widget.audioService?.playSfx('level_up');
|
||||
_resetSpecialAnimationAfterFrame();
|
||||
|
||||
// Phase 9: Act 변경 감지 (레벨 기반)
|
||||
@@ -147,6 +166,8 @@ class _GamePlayScreenState extends State<GamePlayScreen>
|
||||
CombatLogType.questComplete,
|
||||
);
|
||||
}
|
||||
// 오디오: 퀘스트 완료 SFX
|
||||
widget.audioService?.playSfx('quest_complete');
|
||||
_resetSpecialAnimationAfterFrame();
|
||||
}
|
||||
_lastQuestCount = state.progress.questCount;
|
||||
@@ -192,11 +213,74 @@ class _GamePlayScreenState extends State<GamePlayScreen>
|
||||
for (final event in newEvents) {
|
||||
final (message, type) = _formatCombatEvent(event);
|
||||
_addCombatLog(message, type);
|
||||
|
||||
// 오디오: 전투 이벤트에 따른 SFX 재생
|
||||
_playCombatEventSfx(event);
|
||||
}
|
||||
|
||||
_lastProcessedEventCount = events.length;
|
||||
}
|
||||
|
||||
/// 전투 상태에 따른 BGM 전환
|
||||
void _updateBgmForCombatState(GameState state) {
|
||||
final audio = widget.audioService;
|
||||
if (audio == null) return;
|
||||
|
||||
final combat = state.progress.currentCombat;
|
||||
final isInCombat = combat != null && combat.isActive;
|
||||
|
||||
if (isInCombat && !_wasInCombat) {
|
||||
// 전투 시작: 보스 여부에 따라 BGM 선택
|
||||
// 몬스터 레벨이 플레이어보다 5 이상 높으면 보스로 간주
|
||||
final monsterLevel = state.progress.currentTask.monsterLevel ?? 0;
|
||||
final playerLevel = state.traits.level;
|
||||
final isBoss = monsterLevel >= playerLevel + 5;
|
||||
|
||||
if (isBoss) {
|
||||
audio.playBgm('boss');
|
||||
} else {
|
||||
audio.playBgm('battle');
|
||||
}
|
||||
} else if (!isInCombat && _wasInCombat) {
|
||||
// 전투 종료: 마을 BGM으로 복귀
|
||||
audio.playBgm('town');
|
||||
}
|
||||
|
||||
_wasInCombat = isInCombat;
|
||||
}
|
||||
|
||||
/// 전투 이벤트에 따른 SFX 재생
|
||||
void _playCombatEventSfx(CombatEvent event) {
|
||||
final audio = widget.audioService;
|
||||
if (audio == null) return;
|
||||
|
||||
switch (event.type) {
|
||||
case CombatEventType.playerAttack:
|
||||
audio.playSfx('attack');
|
||||
case CombatEventType.monsterAttack:
|
||||
audio.playSfx('hit');
|
||||
case CombatEventType.playerSkill:
|
||||
audio.playSfx('skill');
|
||||
case CombatEventType.playerHeal:
|
||||
case CombatEventType.playerPotion:
|
||||
audio.playSfx('item');
|
||||
case CombatEventType.potionDrop:
|
||||
audio.playSfx('item');
|
||||
case CombatEventType.playerBuff:
|
||||
case CombatEventType.playerDebuff:
|
||||
audio.playSfx('skill');
|
||||
case CombatEventType.dotTick:
|
||||
// DOT 틱은 SFX 없음 (너무 자주 발생)
|
||||
break;
|
||||
case CombatEventType.playerEvade:
|
||||
case CombatEventType.monsterEvade:
|
||||
case CombatEventType.playerBlock:
|
||||
case CombatEventType.playerParry:
|
||||
// 회피/방어는 별도 SFX 없음
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/// 전투 이벤트를 메시지와 타입으로 변환
|
||||
(String, CombatLogType) _formatCombatEvent(CombatEvent event) {
|
||||
final target = event.targetName ?? '';
|
||||
@@ -256,6 +340,10 @@ class _GamePlayScreenState extends State<GamePlayScreen>
|
||||
game_l10n.combatBuffActivated(skillName),
|
||||
CombatLogType.buff,
|
||||
),
|
||||
CombatEventType.playerDebuff => (
|
||||
game_l10n.combatDebuffApplied(skillName, target),
|
||||
CombatLogType.debuff,
|
||||
),
|
||||
CombatEventType.dotTick => (
|
||||
game_l10n.combatDotTick(skillName, event.damage),
|
||||
CombatLogType.dotTick,
|
||||
@@ -361,6 +449,29 @@ class _GamePlayScreenState extends State<GamePlayScreen>
|
||||
_lastQuestCount = state.progress.questCount;
|
||||
_lastPlotStageCount = state.progress.plotStageCount;
|
||||
_lastAct = getActForLevel(state.traits.level);
|
||||
|
||||
// 초기 전투 상태 확인
|
||||
final combat = state.progress.currentCombat;
|
||||
_wasInCombat = combat != null && combat.isActive;
|
||||
}
|
||||
|
||||
// 누적 통계 로드
|
||||
widget.controller.loadCumulativeStats();
|
||||
|
||||
// 초기 BGM 재생 (마을 테마)
|
||||
widget.audioService?.playBgm('town');
|
||||
|
||||
// 오디오 볼륨 초기화
|
||||
_initAudioVolumes();
|
||||
}
|
||||
|
||||
/// 오디오 볼륨 초기화 (설정에서 로드)
|
||||
Future<void> _initAudioVolumes() async {
|
||||
final audio = widget.audioService;
|
||||
if (audio != null) {
|
||||
_bgmVolume = audio.bgmVolume;
|
||||
_sfxVolume = audio.sfxVolume;
|
||||
if (mounted) setState(() {});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -465,6 +576,15 @@ class _GamePlayScreenState extends State<GamePlayScreen>
|
||||
return platform == TargetPlatform.iOS || platform == TargetPlatform.android;
|
||||
}
|
||||
|
||||
/// 통계 다이얼로그 표시
|
||||
void _showStatisticsDialog(BuildContext context) {
|
||||
StatisticsDialog.show(
|
||||
context,
|
||||
session: widget.controller.sessionStats,
|
||||
cumulative: widget.controller.cumulativeStats,
|
||||
);
|
||||
}
|
||||
|
||||
/// 설정 화면 표시
|
||||
void _showSettingsScreen(BuildContext context) {
|
||||
final settingsRepo = SettingsRepository();
|
||||
@@ -614,6 +734,17 @@ class _GamePlayScreenState extends State<GamePlayScreen>
|
||||
},
|
||||
currentThemeMode: widget.currentThemeMode,
|
||||
onThemeModeChange: widget.onThemeModeChange,
|
||||
// 사운드 설정
|
||||
bgmVolume: _bgmVolume,
|
||||
sfxVolume: _sfxVolume,
|
||||
onBgmVolumeChange: (volume) {
|
||||
setState(() => _bgmVolume = volume);
|
||||
widget.audioService?.setBgmVolume(volume);
|
||||
},
|
||||
onSfxVolumeChange: (volume) {
|
||||
setState(() => _sfxVolume = volume);
|
||||
widget.audioService?.setSfxVolume(volume);
|
||||
},
|
||||
),
|
||||
// 사망 오버레이
|
||||
if (state.isDead && state.deathInfo != null)
|
||||
@@ -666,6 +797,18 @@ class _GamePlayScreenState extends State<GamePlayScreen>
|
||||
onPressed: () => widget.controller.loop?.cheatCompletePlot(),
|
||||
),
|
||||
],
|
||||
// 통계 버튼
|
||||
IconButton(
|
||||
icon: const Icon(Icons.bar_chart),
|
||||
tooltip: game_l10n.uiStatistics,
|
||||
onPressed: () => _showStatisticsDialog(context),
|
||||
),
|
||||
// 도움말 버튼
|
||||
IconButton(
|
||||
icon: const Icon(Icons.help_outline),
|
||||
tooltip: game_l10n.uiHelp,
|
||||
onPressed: () => HelpDialog.show(context),
|
||||
),
|
||||
// 설정 버튼
|
||||
IconButton(
|
||||
icon: const Icon(Icons.settings),
|
||||
|
||||
@@ -5,7 +5,9 @@ import 'package:askiineverdie/src/core/engine/progress_service.dart';
|
||||
import 'package:askiineverdie/src/core/engine/resurrection_service.dart';
|
||||
import 'package:askiineverdie/src/core/engine/shop_service.dart';
|
||||
import 'package:askiineverdie/src/core/model/game_state.dart';
|
||||
import 'package:askiineverdie/src/core/model/game_statistics.dart';
|
||||
import 'package:askiineverdie/src/core/storage/save_manager.dart';
|
||||
import 'package:askiineverdie/src/core/storage/statistics_storage.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
enum GameSessionStatus { idle, loading, running, error, dead }
|
||||
@@ -18,12 +20,15 @@ class GameSessionController extends ChangeNotifier {
|
||||
this.autoSaveConfig = const AutoSaveConfig(),
|
||||
Duration tickInterval = const Duration(milliseconds: 50),
|
||||
DateTime Function()? now,
|
||||
StatisticsStorage? statisticsStorage,
|
||||
}) : _tickInterval = tickInterval,
|
||||
_now = now ?? DateTime.now;
|
||||
_now = now ?? DateTime.now,
|
||||
_statisticsStorage = statisticsStorage ?? StatisticsStorage();
|
||||
|
||||
final ProgressService progressService;
|
||||
final SaveManager saveManager;
|
||||
final AutoSaveConfig autoSaveConfig;
|
||||
final StatisticsStorage _statisticsStorage;
|
||||
|
||||
final Duration _tickInterval;
|
||||
final DateTime Function() _now;
|
||||
@@ -36,12 +41,26 @@ class GameSessionController extends ChangeNotifier {
|
||||
GameState? _state;
|
||||
String? _error;
|
||||
|
||||
// 통계 관련 필드
|
||||
SessionStatistics _sessionStats = SessionStatistics.empty();
|
||||
CumulativeStatistics _cumulativeStats = CumulativeStatistics.empty();
|
||||
int _previousLevel = 0;
|
||||
int _previousGold = 0;
|
||||
int _previousMonstersKilled = 0;
|
||||
int _previousQuestsCompleted = 0;
|
||||
|
||||
GameSessionStatus get status => _status;
|
||||
GameState? get state => _state;
|
||||
String? get error => _error;
|
||||
bool get isRunning => _status == GameSessionStatus.running;
|
||||
bool get cheatsEnabled => _cheatsEnabled;
|
||||
|
||||
/// 현재 세션 통계
|
||||
SessionStatistics get sessionStats => _sessionStats;
|
||||
|
||||
/// 누적 통계
|
||||
CumulativeStatistics get cumulativeStats => _cumulativeStats;
|
||||
|
||||
/// 현재 ProgressLoop 인스턴스 (치트 기능용)
|
||||
ProgressLoop? get loop => _loop;
|
||||
|
||||
@@ -62,6 +81,13 @@ class GameSessionController extends ChangeNotifier {
|
||||
_status = GameSessionStatus.running;
|
||||
_cheatsEnabled = cheatsEnabled;
|
||||
|
||||
// 통계 초기화
|
||||
if (isNewGame) {
|
||||
_sessionStats = SessionStatistics.empty();
|
||||
await _statisticsStorage.recordGameStart();
|
||||
}
|
||||
_initPreviousValues(state);
|
||||
|
||||
_loop = ProgressLoop(
|
||||
initialState: state,
|
||||
progressService: progressService,
|
||||
@@ -74,6 +100,7 @@ class GameSessionController extends ChangeNotifier {
|
||||
);
|
||||
|
||||
_subscription = _loop!.stream.listen((next) {
|
||||
_updateStatistics(next);
|
||||
_state = next;
|
||||
notifyListeners();
|
||||
});
|
||||
@@ -82,6 +109,76 @@ class GameSessionController extends ChangeNotifier {
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// 이전 값 초기화 (통계 변화 추적용)
|
||||
void _initPreviousValues(GameState state) {
|
||||
_previousLevel = state.traits.level;
|
||||
_previousGold = state.inventory.gold;
|
||||
_previousMonstersKilled = state.progress.monstersKilled;
|
||||
_previousQuestsCompleted = state.progress.questCount;
|
||||
}
|
||||
|
||||
/// 상태 변화에 따른 통계 업데이트
|
||||
void _updateStatistics(GameState next) {
|
||||
// 플레이 시간 업데이트
|
||||
_sessionStats = _sessionStats.updatePlayTime(next.skillSystem.elapsedMs);
|
||||
|
||||
// 레벨업 감지
|
||||
if (next.traits.level > _previousLevel) {
|
||||
final levelUps = next.traits.level - _previousLevel;
|
||||
for (var i = 0; i < levelUps; i++) {
|
||||
_sessionStats = _sessionStats.recordLevelUp();
|
||||
}
|
||||
_previousLevel = next.traits.level;
|
||||
|
||||
// 최고 레벨 업데이트
|
||||
unawaited(_statisticsStorage.updateHighestLevel(next.traits.level));
|
||||
}
|
||||
|
||||
// 골드 변화 감지
|
||||
if (next.inventory.gold > _previousGold) {
|
||||
final earned = next.inventory.gold - _previousGold;
|
||||
_sessionStats = _sessionStats.recordGoldEarned(earned);
|
||||
|
||||
// 최대 골드 업데이트
|
||||
unawaited(_statisticsStorage.updateHighestGold(next.inventory.gold));
|
||||
} else if (next.inventory.gold < _previousGold) {
|
||||
final spent = _previousGold - next.inventory.gold;
|
||||
_sessionStats = _sessionStats.recordGoldSpent(spent);
|
||||
}
|
||||
_previousGold = next.inventory.gold;
|
||||
|
||||
// 몬스터 처치 감지
|
||||
if (next.progress.monstersKilled > _previousMonstersKilled) {
|
||||
final kills = next.progress.monstersKilled - _previousMonstersKilled;
|
||||
for (var i = 0; i < kills; i++) {
|
||||
_sessionStats = _sessionStats.recordKill();
|
||||
}
|
||||
_previousMonstersKilled = next.progress.monstersKilled;
|
||||
}
|
||||
|
||||
// 퀘스트 완료 감지
|
||||
if (next.progress.questCount > _previousQuestsCompleted) {
|
||||
final quests = next.progress.questCount - _previousQuestsCompleted;
|
||||
for (var i = 0; i < quests; i++) {
|
||||
_sessionStats = _sessionStats.recordQuestComplete();
|
||||
}
|
||||
_previousQuestsCompleted = next.progress.questCount;
|
||||
}
|
||||
}
|
||||
|
||||
/// 누적 통계 로드
|
||||
Future<void> loadCumulativeStats() async {
|
||||
_cumulativeStats = await _statisticsStorage.loadCumulative();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// 세션 통계를 누적 통계에 병합
|
||||
Future<void> mergeSessionStats() async {
|
||||
await _statisticsStorage.mergeSession(_sessionStats);
|
||||
_cumulativeStats = await _statisticsStorage.loadCumulative();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<void> loadAndStart({
|
||||
String? fileName,
|
||||
bool cheatsEnabled = false,
|
||||
@@ -148,6 +245,7 @@ class GameSessionController extends ChangeNotifier {
|
||||
|
||||
/// 플레이어 사망 콜백 (ProgressLoop에서 호출)
|
||||
void _onPlayerDied() {
|
||||
_sessionStats = _sessionStats.recordDeath();
|
||||
_status = GameSessionStatus.dead;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
@@ -39,6 +39,10 @@ class MobileCarouselLayout extends StatefulWidget {
|
||||
this.specialAnimation,
|
||||
this.currentThemeMode = ThemeMode.system,
|
||||
this.onThemeModeChange,
|
||||
this.bgmVolume = 0.7,
|
||||
this.sfxVolume = 0.8,
|
||||
this.onBgmVolumeChange,
|
||||
this.onSfxVolumeChange,
|
||||
});
|
||||
|
||||
final GameState state;
|
||||
@@ -56,6 +60,18 @@ class MobileCarouselLayout extends StatefulWidget {
|
||||
final ThemeMode currentThemeMode;
|
||||
final void Function(ThemeMode mode)? onThemeModeChange;
|
||||
|
||||
/// BGM 볼륨 (0.0 ~ 1.0)
|
||||
final double bgmVolume;
|
||||
|
||||
/// SFX 볼륨 (0.0 ~ 1.0)
|
||||
final double sfxVolume;
|
||||
|
||||
/// BGM 볼륨 변경 콜백
|
||||
final void Function(double volume)? onBgmVolumeChange;
|
||||
|
||||
/// SFX 볼륨 변경 콜백
|
||||
final void Function(double volume)? onSfxVolumeChange;
|
||||
|
||||
@override
|
||||
State<MobileCarouselLayout> createState() => _MobileCarouselLayoutState();
|
||||
}
|
||||
@@ -200,6 +216,108 @@ class _MobileCarouselLayoutState extends State<MobileCarouselLayout> {
|
||||
);
|
||||
}
|
||||
|
||||
/// 사운드 상태 텍스트 가져오기
|
||||
String _getSoundStatus() {
|
||||
final bgmPercent = (widget.bgmVolume * 100).round();
|
||||
final sfxPercent = (widget.sfxVolume * 100).round();
|
||||
if (bgmPercent == 0 && sfxPercent == 0) {
|
||||
return l10n.uiSoundOff;
|
||||
}
|
||||
return 'BGM $bgmPercent% / SFX $sfxPercent%';
|
||||
}
|
||||
|
||||
/// 사운드 설정 다이얼로그 표시
|
||||
void _showSoundDialog(BuildContext context) {
|
||||
// StatefulBuilder를 사용하여 다이얼로그 내 상태 관리
|
||||
var bgmVolume = widget.bgmVolume;
|
||||
var sfxVolume = widget.sfxVolume;
|
||||
|
||||
showDialog<void>(
|
||||
context: context,
|
||||
builder: (context) => StatefulBuilder(
|
||||
builder: (context, setDialogState) => AlertDialog(
|
||||
title: Text(l10n.uiSound),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// BGM 볼륨
|
||||
Row(
|
||||
children: [
|
||||
Icon(
|
||||
bgmVolume == 0 ? Icons.music_off : Icons.music_note,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(l10n.uiBgmVolume),
|
||||
Text('${(bgmVolume * 100).round()}%'),
|
||||
],
|
||||
),
|
||||
Slider(
|
||||
value: bgmVolume,
|
||||
onChanged: (value) {
|
||||
setDialogState(() => bgmVolume = value);
|
||||
widget.onBgmVolumeChange?.call(value);
|
||||
},
|
||||
divisions: 10,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
// SFX 볼륨
|
||||
Row(
|
||||
children: [
|
||||
Icon(
|
||||
sfxVolume == 0 ? Icons.volume_off : Icons.volume_up,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(l10n.uiSfxVolume),
|
||||
Text('${(sfxVolume * 100).round()}%'),
|
||||
],
|
||||
),
|
||||
Slider(
|
||||
value: sfxVolume,
|
||||
onChanged: (value) {
|
||||
setDialogState(() => sfxVolume = value);
|
||||
widget.onSfxVolumeChange?.call(value);
|
||||
},
|
||||
divisions: 10,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: Text(l10n.buttonConfirm),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 세이브 삭제 확인 다이얼로그 표시
|
||||
void _showDeleteConfirmDialog(BuildContext context) {
|
||||
showDialog<void>(
|
||||
@@ -324,6 +442,27 @@ class _MobileCarouselLayoutState extends State<MobileCarouselLayout> {
|
||||
},
|
||||
),
|
||||
|
||||
// 사운드 설정
|
||||
if (widget.onBgmVolumeChange != null ||
|
||||
widget.onSfxVolumeChange != null)
|
||||
ListTile(
|
||||
leading: Icon(
|
||||
widget.bgmVolume == 0 && widget.sfxVolume == 0
|
||||
? Icons.volume_off
|
||||
: Icons.volume_up,
|
||||
color: Colors.indigo,
|
||||
),
|
||||
title: Text(l10n.uiSound),
|
||||
trailing: Text(
|
||||
_getSoundStatus(),
|
||||
style: TextStyle(color: Theme.of(context).colorScheme.primary),
|
||||
),
|
||||
onTap: () {
|
||||
Navigator.pop(context);
|
||||
_showSoundDialog(context);
|
||||
},
|
||||
),
|
||||
|
||||
const Divider(),
|
||||
|
||||
// 저장
|
||||
@@ -381,7 +520,7 @@ class _MobileCarouselLayoutState extends State<MobileCarouselLayout> {
|
||||
actions: [
|
||||
// 옵션 버튼
|
||||
IconButton(
|
||||
icon: const Icon(Icons.more_vert),
|
||||
icon: const Icon(Icons.settings),
|
||||
onPressed: () => _showOptionsMenu(context),
|
||||
tooltip: l10n.menuOptions,
|
||||
),
|
||||
|
||||
@@ -285,6 +285,15 @@ class _AsciiAnimationCardState extends State<AsciiAnimationCard> {
|
||||
false,
|
||||
),
|
||||
|
||||
// 디버프 적용 → idle 페이즈 유지
|
||||
CombatEventType.playerDebuff => (
|
||||
BattlePhase.idle,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
),
|
||||
|
||||
// DOT 틱 → attack 페이즈 (지속 피해)
|
||||
CombatEventType.dotTick => (
|
||||
BattlePhase.attack,
|
||||
|
||||
@@ -28,6 +28,7 @@ enum CombatLogType {
|
||||
parry, // 무기 쳐내기
|
||||
monsterAttack, // 몬스터 공격
|
||||
buff, // 버프 활성화
|
||||
debuff, // 디버프 적용
|
||||
dotTick, // DOT 틱 데미지
|
||||
potion, // 물약 사용
|
||||
potionDrop, // 물약 드랍
|
||||
@@ -166,6 +167,7 @@ class _LogEntryTile extends StatelessWidget {
|
||||
Icons.dangerous,
|
||||
),
|
||||
CombatLogType.buff => (Colors.lightBlue.shade300, Icons.trending_up),
|
||||
CombatLogType.debuff => (Colors.deepOrange.shade300, Icons.trending_down),
|
||||
CombatLogType.dotTick => (Colors.deepPurple.shade300, Icons.whatshot),
|
||||
CombatLogType.potion => (Colors.pink.shade300, Icons.local_drink),
|
||||
CombatLogType.potionDrop => (Colors.lime.shade300, Icons.card_giftcard),
|
||||
|
||||
@@ -429,6 +429,11 @@ class DeathOverlay extends StatelessWidget {
|
||||
Colors.lightBlue.shade300,
|
||||
l10n.combatBuffActivated(event.skillName ?? ''),
|
||||
),
|
||||
CombatEventType.playerDebuff => (
|
||||
Icons.trending_down,
|
||||
Colors.deepOrange.shade300,
|
||||
l10n.combatDebuffApplied(event.skillName ?? '', target),
|
||||
),
|
||||
CombatEventType.dotTick => (
|
||||
Icons.whatshot,
|
||||
Colors.deepOrange.shade300,
|
||||
|
||||
553
lib/src/features/game/widgets/help_dialog.dart
Normal file
553
lib/src/features/game/widgets/help_dialog.dart
Normal file
@@ -0,0 +1,553 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// 도움말 다이얼로그 (Help Dialog)
|
||||
///
|
||||
/// 게임 메카닉과 UI 설명을 제공
|
||||
class HelpDialog extends StatefulWidget {
|
||||
const HelpDialog({super.key});
|
||||
|
||||
/// 다이얼로그 표시
|
||||
static Future<void> show(BuildContext context) {
|
||||
return showDialog(
|
||||
context: context,
|
||||
builder: (_) => const HelpDialog(),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
State<HelpDialog> createState() => _HelpDialogState();
|
||||
}
|
||||
|
||||
class _HelpDialogState extends State<HelpDialog>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late TabController _tabController;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_tabController = TabController(length: 4, vsync: this);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_tabController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final isKorean = Localizations.localeOf(context).languageCode == 'ko';
|
||||
final isJapanese = Localizations.localeOf(context).languageCode == 'ja';
|
||||
|
||||
return Dialog(
|
||||
child: Container(
|
||||
constraints: const BoxConstraints(maxWidth: 500, maxHeight: 600),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// 헤더
|
||||
Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: theme.colorScheme.primaryContainer,
|
||||
borderRadius: const BorderRadius.vertical(
|
||||
top: Radius.circular(28),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.help_outline,
|
||||
color: theme.colorScheme.onPrimaryContainer,
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Text(
|
||||
isKorean
|
||||
? '게임 도움말'
|
||||
: isJapanese
|
||||
? 'ゲームヘルプ'
|
||||
: 'Game Help',
|
||||
style: theme.textTheme.titleLarge?.copyWith(
|
||||
color: theme.colorScheme.onPrimaryContainer,
|
||||
),
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.close),
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
color: theme.colorScheme.onPrimaryContainer,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
// 탭 바
|
||||
TabBar(
|
||||
controller: _tabController,
|
||||
isScrollable: true,
|
||||
tabs: [
|
||||
Tab(
|
||||
text: isKorean
|
||||
? '기본'
|
||||
: isJapanese
|
||||
? '基本'
|
||||
: 'Basics',
|
||||
),
|
||||
Tab(
|
||||
text: isKorean
|
||||
? '전투'
|
||||
: isJapanese
|
||||
? '戦闘'
|
||||
: 'Combat',
|
||||
),
|
||||
Tab(
|
||||
text: isKorean
|
||||
? '스킬'
|
||||
: isJapanese
|
||||
? 'スキル'
|
||||
: 'Skills',
|
||||
),
|
||||
Tab(
|
||||
text: isKorean
|
||||
? 'UI'
|
||||
: isJapanese
|
||||
? 'UI'
|
||||
: 'UI',
|
||||
),
|
||||
],
|
||||
),
|
||||
// 탭 내용
|
||||
Expanded(
|
||||
child: TabBarView(
|
||||
controller: _tabController,
|
||||
children: [
|
||||
_BasicsHelpView(
|
||||
isKorean: isKorean,
|
||||
isJapanese: isJapanese,
|
||||
),
|
||||
_CombatHelpView(
|
||||
isKorean: isKorean,
|
||||
isJapanese: isJapanese,
|
||||
),
|
||||
_SkillsHelpView(
|
||||
isKorean: isKorean,
|
||||
isJapanese: isJapanese,
|
||||
),
|
||||
_UIHelpView(
|
||||
isKorean: isKorean,
|
||||
isJapanese: isJapanese,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 기본 도움말 뷰
|
||||
class _BasicsHelpView extends StatelessWidget {
|
||||
const _BasicsHelpView({
|
||||
required this.isKorean,
|
||||
required this.isJapanese,
|
||||
});
|
||||
|
||||
final bool isKorean;
|
||||
final bool isJapanese;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ListView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
children: [
|
||||
_HelpSection(
|
||||
icon: Icons.info_outline,
|
||||
title: isKorean
|
||||
? '게임 소개'
|
||||
: isJapanese
|
||||
? 'ゲーム紹介'
|
||||
: 'About the Game',
|
||||
content: isKorean
|
||||
? 'Askii Never Die는 자동 진행 RPG입니다. 캐릭터가 자동으로 몬스터와 싸우고, '
|
||||
'퀘스트를 완료하며, 레벨업합니다. 여러분은 장비와 스킬을 관리하면 됩니다.'
|
||||
: isJapanese
|
||||
? 'Askii Never Dieは自動進行RPGです。キャラクターが自動でモンスターと戦い、'
|
||||
'クエストを完了し、レベルアップします。装備とスキルの管理だけで大丈夫です。'
|
||||
: 'Askii Never Die is an idle RPG. Your character automatically fights monsters, '
|
||||
'completes quests, and levels up. You manage equipment and skills.',
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
_HelpSection(
|
||||
icon: Icons.trending_up,
|
||||
title: isKorean
|
||||
? '진행 방식'
|
||||
: isJapanese
|
||||
? '進行方式'
|
||||
: 'Progression',
|
||||
content: isKorean
|
||||
? '• 몬스터 처치 → 전리품 획득 → 장비 업그레이드\n'
|
||||
'• 경험치 획득 → 레벨업 → 스탯 상승\n'
|
||||
'• 퀘스트 완료 → 보상 획득\n'
|
||||
'• 플롯 진행 → 새로운 Act 해금'
|
||||
: isJapanese
|
||||
? '• モンスター討伐 → 戦利品獲得 → 装備アップグレード\n'
|
||||
'• 経験値獲得 → レベルアップ → ステータス上昇\n'
|
||||
'• クエスト完了 → 報酬獲得\n'
|
||||
'• プロット進行 → 新しいAct解放'
|
||||
: '• Kill monsters → Get loot → Upgrade equipment\n'
|
||||
'• Gain XP → Level up → Stats increase\n'
|
||||
'• Complete quests → Get rewards\n'
|
||||
'• Progress plot → Unlock new Acts',
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
_HelpSection(
|
||||
icon: Icons.save,
|
||||
title: isKorean
|
||||
? '저장'
|
||||
: isJapanese
|
||||
? 'セーブ'
|
||||
: 'Saving',
|
||||
content: isKorean
|
||||
? '게임은 자동으로 저장됩니다. 레벨업, 퀘스트 완료, Act 진행 시 자동 저장됩니다. '
|
||||
'뒤로 가기 시 저장 여부를 선택할 수 있습니다.'
|
||||
: isJapanese
|
||||
? 'ゲームは自動保存されます。レベルアップ、クエスト完了、Act進行時に自動保存されます。'
|
||||
'戻る時に保存するかどうか選択できます。'
|
||||
: 'The game auto-saves. It saves on level up, quest completion, and Act progression. '
|
||||
'When exiting, you can choose whether to save.',
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 전투 도움말 뷰
|
||||
class _CombatHelpView extends StatelessWidget {
|
||||
const _CombatHelpView({
|
||||
required this.isKorean,
|
||||
required this.isJapanese,
|
||||
});
|
||||
|
||||
final bool isKorean;
|
||||
final bool isJapanese;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ListView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
children: [
|
||||
_HelpSection(
|
||||
icon: Icons.sports_mma,
|
||||
title: isKorean
|
||||
? '전투 시스템'
|
||||
: isJapanese
|
||||
? '戦闘システム'
|
||||
: 'Combat System',
|
||||
content: isKorean
|
||||
? '전투는 자동으로 진행됩니다. 플레이어와 몬스터가 번갈아 공격하며, '
|
||||
'공격 속도(Attack Speed)에 따라 공격 빈도가 결정됩니다.'
|
||||
: isJapanese
|
||||
? '戦闘は自動で進行します。プレイヤーとモンスターが交互に攻撃し、'
|
||||
'攻撃速度(Attack Speed)によって攻撃頻度が決まります。'
|
||||
: 'Combat is automatic. Player and monster take turns attacking, '
|
||||
'with attack frequency based on Attack Speed.',
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
_HelpSection(
|
||||
icon: Icons.shield,
|
||||
title: isKorean
|
||||
? '방어 메카닉'
|
||||
: isJapanese
|
||||
? '防御メカニック'
|
||||
: 'Defense Mechanics',
|
||||
content: isKorean
|
||||
? '• 회피(Evasion): DEX 기반, 공격을 완전히 피함\n'
|
||||
'• 방패 방어(Block): 방패 장착 시, 피해 감소\n'
|
||||
'• 무기 쳐내기(Parry): 무기로 공격 일부 막음\n'
|
||||
'• 방어력(DEF): 모든 피해에서 차감'
|
||||
: isJapanese
|
||||
? '• 回避(Evasion): DEX基準、攻撃を完全に回避\n'
|
||||
'• 盾防御(Block): 盾装備時、ダメージ軽減\n'
|
||||
'• 武器受け流し(Parry): 武器で攻撃を一部防ぐ\n'
|
||||
'• 防御力(DEF): 全ダメージから差し引き'
|
||||
: '• Evasion: DEX-based, completely avoid attacks\n'
|
||||
'• Block: With shield, reduce damage\n'
|
||||
'• Parry: Deflect some damage with weapon\n'
|
||||
'• DEF: Subtracted from all damage',
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
_HelpSection(
|
||||
icon: Icons.favorite,
|
||||
title: isKorean
|
||||
? '사망과 부활'
|
||||
: isJapanese
|
||||
? '死亡と復活'
|
||||
: 'Death & Revival',
|
||||
content: isKorean
|
||||
? 'HP가 0이 되면 사망합니다. 사망 시 장비 하나를 제물로 바쳐 부활할 수 있습니다. '
|
||||
'부활 후 HP/MP가 완전 회복되고 빈 장비 슬롯에 기본 장비가 지급됩니다.'
|
||||
: isJapanese
|
||||
? 'HPが0になると死亡します。死亡時に装備1つを捧げて復活できます。'
|
||||
'復活後HP/MPが完全回復し、空の装備スロットに基本装備が支給されます。'
|
||||
: 'You die when HP reaches 0. Sacrifice one equipment piece to revive. '
|
||||
'After revival, HP/MP fully restore and empty slots get basic equipment.',
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 스킬 도움말 뷰
|
||||
class _SkillsHelpView extends StatelessWidget {
|
||||
const _SkillsHelpView({
|
||||
required this.isKorean,
|
||||
required this.isJapanese,
|
||||
});
|
||||
|
||||
final bool isKorean;
|
||||
final bool isJapanese;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ListView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
children: [
|
||||
_HelpSection(
|
||||
icon: Icons.auto_awesome,
|
||||
title: isKorean
|
||||
? '스킬 종류'
|
||||
: isJapanese
|
||||
? 'スキル種類'
|
||||
: 'Skill Types',
|
||||
content: isKorean
|
||||
? '• 공격(Attack): 적에게 직접 피해\n'
|
||||
'• 회복(Heal): HP/MP 회복\n'
|
||||
'• 버프(Buff): 자신에게 유리한 효과\n'
|
||||
'• 디버프(Debuff): 적에게 불리한 효과\n'
|
||||
'• DOT: 시간에 걸쳐 지속 피해'
|
||||
: isJapanese
|
||||
? '• 攻撃(Attack): 敵に直接ダメージ\n'
|
||||
'• 回復(Heal): HP/MP回復\n'
|
||||
'• バフ(Buff): 自分に有利な効果\n'
|
||||
'• デバフ(Debuff): 敵に不利な効果\n'
|
||||
'• DOT: 時間経過でダメージ'
|
||||
: '• Attack: Deal direct damage\n'
|
||||
'• Heal: Restore HP/MP\n'
|
||||
'• Buff: Beneficial effects on self\n'
|
||||
'• Debuff: Harmful effects on enemies\n'
|
||||
'• DOT: Damage over time',
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
_HelpSection(
|
||||
icon: Icons.psychology,
|
||||
title: isKorean
|
||||
? '자동 스킬 선택'
|
||||
: isJapanese
|
||||
? '自動スキル選択'
|
||||
: 'Auto Skill Selection',
|
||||
content: isKorean
|
||||
? '스킬은 AI가 자동으로 선택합니다:\n'
|
||||
'1. HP 낮음 → 회복 스킬 우선\n'
|
||||
'2. HP/MP 충분 → 버프 스킬 사용\n'
|
||||
'3. 몬스터 HP 높음 → 디버프 적용\n'
|
||||
'4. 공격 스킬로 마무리'
|
||||
: isJapanese
|
||||
? 'スキルはAIが自動選択します:\n'
|
||||
'1. HP低い → 回復スキル優先\n'
|
||||
'2. HP/MP十分 → バフスキル使用\n'
|
||||
'3. モンスターHP高い → デバフ適用\n'
|
||||
'4. 攻撃スキルで仕上げ'
|
||||
: 'Skills are auto-selected by AI:\n'
|
||||
'1. Low HP → Heal skills priority\n'
|
||||
'2. HP/MP sufficient → Use buff skills\n'
|
||||
'3. Monster HP high → Apply debuffs\n'
|
||||
'4. Finish with attack skills',
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
_HelpSection(
|
||||
icon: Icons.upgrade,
|
||||
title: isKorean
|
||||
? '스킬 랭크'
|
||||
: isJapanese
|
||||
? 'スキルランク'
|
||||
: 'Skill Ranks',
|
||||
content: isKorean
|
||||
? '스킬은 I ~ IX 랭크가 있습니다. 랭크가 높을수록:\n'
|
||||
'• 데미지/회복량 증가\n'
|
||||
'• MP 소모량 증가\n'
|
||||
'• 쿨타임 증가\n'
|
||||
'레벨업 시 랜덤하게 스킬을 배웁니다.'
|
||||
: isJapanese
|
||||
? 'スキルにはI~IXランクがあります。ランクが高いほど:\n'
|
||||
'• ダメージ/回復量増加\n'
|
||||
'• MP消費量増加\n'
|
||||
'• クールタイム増加\n'
|
||||
'レベルアップ時にランダムでスキルを習得します。'
|
||||
: 'Skills have ranks I~IX. Higher rank means:\n'
|
||||
'• More damage/healing\n'
|
||||
'• More MP cost\n'
|
||||
'• Longer cooldown\n'
|
||||
'Learn random skills on level up.',
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// UI 도움말 뷰
|
||||
class _UIHelpView extends StatelessWidget {
|
||||
const _UIHelpView({
|
||||
required this.isKorean,
|
||||
required this.isJapanese,
|
||||
});
|
||||
|
||||
final bool isKorean;
|
||||
final bool isJapanese;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ListView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
children: [
|
||||
_HelpSection(
|
||||
icon: Icons.view_column,
|
||||
title: isKorean
|
||||
? '화면 구성'
|
||||
: isJapanese
|
||||
? '画面構成'
|
||||
: 'Screen Layout',
|
||||
content: isKorean
|
||||
? '• 상단: 전투 애니메이션, 태스크 진행바\n'
|
||||
'• 좌측: 캐릭터 정보, HP/MP, 스탯\n'
|
||||
'• 중앙: 장비, 인벤토리\n'
|
||||
'• 우측: 플롯/퀘스트 진행, 스펠북'
|
||||
: isJapanese
|
||||
? '• 上部: 戦闘アニメーション、タスク進行バー\n'
|
||||
'• 左側: キャラクター情報、HP/MP、ステータス\n'
|
||||
'• 中央: 装備、インベントリ\n'
|
||||
'• 右側: プロット/クエスト進行、スペルブック'
|
||||
: '• Top: Combat animation, task progress bar\n'
|
||||
'• Left: Character info, HP/MP, stats\n'
|
||||
'• Center: Equipment, inventory\n'
|
||||
'• Right: Plot/quest progress, spellbook',
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
_HelpSection(
|
||||
icon: Icons.speed,
|
||||
title: isKorean
|
||||
? '속도 조절'
|
||||
: isJapanese
|
||||
? '速度調整'
|
||||
: 'Speed Control',
|
||||
content: isKorean
|
||||
? '태스크 진행바 옆 속도 버튼으로 게임 속도를 조절할 수 있습니다:\n'
|
||||
'• 1x: 기본 속도\n'
|
||||
'• 2x: 2배 속도\n'
|
||||
'• 5x: 5배 속도\n'
|
||||
'• 10x: 10배 속도'
|
||||
: isJapanese
|
||||
? 'タスク進行バー横の速度ボタンでゲーム速度を調整できます:\n'
|
||||
'• 1x: 基本速度\n'
|
||||
'• 2x: 2倍速\n'
|
||||
'• 5x: 5倍速\n'
|
||||
'• 10x: 10倍速'
|
||||
: 'Use the speed button next to task bar to adjust game speed:\n'
|
||||
'• 1x: Normal speed\n'
|
||||
'• 2x: 2x speed\n'
|
||||
'• 5x: 5x speed\n'
|
||||
'• 10x: 10x speed',
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
_HelpSection(
|
||||
icon: Icons.pause,
|
||||
title: isKorean
|
||||
? '일시정지'
|
||||
: isJapanese
|
||||
? '一時停止'
|
||||
: 'Pause',
|
||||
content: isKorean
|
||||
? '일시정지 버튼으로 게임을 멈출 수 있습니다. '
|
||||
'일시정지 중에도 UI를 확인하고 설정을 변경할 수 있습니다.'
|
||||
: isJapanese
|
||||
? '一時停止ボタンでゲームを止められます。'
|
||||
'一時停止中もUIを確認し設定を変更できます。'
|
||||
: 'Use the pause button to stop the game. '
|
||||
'You can still view UI and change settings while paused.',
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
_HelpSection(
|
||||
icon: Icons.bar_chart,
|
||||
title: isKorean
|
||||
? '통계'
|
||||
: isJapanese
|
||||
? '統計'
|
||||
: 'Statistics',
|
||||
content: isKorean
|
||||
? '통계 버튼에서 현재 세션과 누적 게임 통계를 확인할 수 있습니다. '
|
||||
'처치한 몬스터, 획득 골드, 플레이 시간 등을 추적합니다.'
|
||||
: isJapanese
|
||||
? '統計ボタンで現在のセッションと累積ゲーム統計を確認できます。'
|
||||
'倒したモンスター、獲得ゴールド、プレイ時間などを追跡します。'
|
||||
: 'View current session and cumulative stats in the statistics button. '
|
||||
'Track monsters killed, gold earned, play time, etc.',
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 도움말 섹션 위젯
|
||||
class _HelpSection extends StatelessWidget {
|
||||
const _HelpSection({
|
||||
required this.icon,
|
||||
required this.title,
|
||||
required this.content,
|
||||
});
|
||||
|
||||
final IconData icon;
|
||||
final String title;
|
||||
final String content;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// 섹션 헤더
|
||||
Row(
|
||||
children: [
|
||||
Icon(icon, size: 20, color: theme.colorScheme.primary),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
title,
|
||||
style: theme.textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: theme.colorScheme.primary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
// 내용
|
||||
Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: theme.colorScheme.surfaceContainerHighest.withValues(alpha: 0.5),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Text(
|
||||
content,
|
||||
style: theme.textTheme.bodyMedium?.copyWith(
|
||||
height: 1.5,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user