Add options menu persistence

This commit is contained in:
2025-10-30 23:59:05 -07:00
parent dc3c458414
commit 9fc875eda5
18 changed files with 333 additions and 26 deletions

View File

@@ -7,7 +7,7 @@ public partial class AppLogic
public partial record State
{
[Meta]
public partial record MainMenu : State, IGet<Input.NewGame>, IGet<Input.LoadGame>, IGet<Input.EnemyViewerOpened>
public partial record MainMenu : State, IGet<Input.NewGame>, IGet<Input.EnemyViewerOpened>, IGet<Input.QuitGame>
{
public MainMenu()
{
@@ -16,7 +16,11 @@ public partial class AppLogic
public Transition On(in Input.NewGame input) => To<GameStarted>();
public Transition On(in Input.EnemyViewerOpened input) => To<EnemyViewer>();
public Transition On(in Input.LoadGame input) => To<LoadingSaveFile>();
public Transition On(in Input.QuitGame input)
{
Output(new Output.ExitGame());
return ToSelf();
}
}
}
}

View File

@@ -43,7 +43,7 @@ project/assembly_name="Ma"
[editor_plugins]
enabled=PackedStringArray("res://addons/dialogue_manager/plugin.cfg", "res://addons/dungeon_floor_layout/plugin.cfg", "res://addons/special_floor_layout_node/plugin.cfg")
enabled=PackedStringArray("res://addons/dialogue_manager/plugin.cfg")
[file_customization]

View File

@@ -5,8 +5,12 @@ using Chickensoft.Introspection;
using Godot;
using Godot.Collections;
using SimpleInjector.Lifestyles;
using System.IO;
using System.IO.Abstractions;
using System.Linq;
using System.Threading.Tasks;
using Zennysoft.Game.Abstractions;
using Zennysoft.Game.Implementation;
using Zennysoft.Ma.Adapter;
namespace Zennysoft.Game.Ma;
@@ -26,6 +30,8 @@ public partial class App : Node, IApp
[Node] private LoadingScreen LoadingScreen { get; set; } = default!;
[Node] private OptionsMenu OptionsMenu { get; set; }
public IInstantiator Instantiator { get; set; } = default!;
IAppRepo IProvide<IAppRepo>.Value() => AppRepo;
@@ -35,25 +41,43 @@ public partial class App : Node, IApp
public AppLogic.IBinding AppBinding { get; set; } = default!;
private Array _progress;
private SimpleInjector.Container _container;
private AutoProp<string> _loadedScene = new(string.Empty);
private bool _loadingGame = false;
private bool _loadingEnemyViewer = false;
private string _optionsSavePath = string.Empty;
private ISaveFileManager _saveFileManager;
public void Initialize()
{
var container = new SimpleInjector.Container();
container.Options.DefaultScopedLifestyle = new AsyncScopedLifestyle();
container.RegisterSingleton<IAppRepo, AppRepo>();
container.RegisterSingleton<IAppLogic, AppLogic>();
MainMenu.Hide();
_container = new SimpleInjector.Container();
_container.Options.DefaultScopedLifestyle = new AsyncScopedLifestyle();
_container.RegisterSingleton<IAppRepo, AppRepo>();
_container.RegisterSingleton<IAppLogic, AppLogic>();
_container.RegisterSingleton<IFileSystem, FileSystem>();
_container.RegisterSingleton<ISaveFileManager, SaveFileManager>();
_saveFileManager = _container.GetInstance<ISaveFileManager>();
_optionsSavePath = $"{OS.GetUserDataDir()}/options.json";
Task.Run(() => _saveFileManager.ReadFromFile<OptionsData>(_optionsSavePath).ContinueWith((data) =>
{
if (data.IsCompletedSuccessfully)
OptionsMenu.CallDeferred("Load", data.Result);
}));
MainMenu.StartGame += OnStartGame;
MainMenu.EnemyViewer += OnEnemyViewer;
MainMenu.Options += OnOptions;
MainMenu.Quit += OnQuit;
_loadedScene.Changed += OnGameLoaded;
AppRepo = container.GetInstance<IAppRepo>();
AppLogic = container.GetInstance<IAppLogic>();
OptionsMenu.OptionsMenuExited += OptionsMenu_OptionsMenuExited;
AppRepo = _container.GetInstance<IAppRepo>();
AppLogic = _container.GetInstance<IAppLogic>();
AppLogic.Set(AppRepo);
AppLogic.Set(new AppLogic.Data());
@@ -63,6 +87,14 @@ public partial class App : Node, IApp
this.Provide();
}
private async void OptionsMenu_OptionsMenuExited()
{
var saveFileManager = _container.GetInstance<ISaveFileManager>();
await saveFileManager.WriteToFile(OptionsMenu.OptionsData, _optionsSavePath);
MainMenu.Show();
OptionsMenu.Hide();
}
private void OnGameLoaded(string sceneName)
{
LoadingScreen.Hide();
@@ -135,7 +167,11 @@ public partial class App : Node, IApp
private void OnEnemyViewer() => AppLogic.Input(new AppLogic.Input.EnemyViewerOpened());
private void OnLoadGame() => AppLogic.Input(new AppLogic.Input.LoadGame());
private async void OnOptions()
{
OptionsMenu.Show();
MainMenu.Hide();
}
public void OnQuit() => AppLogic.Input(new AppLogic.Input.QuitGame());

View File

@@ -1,7 +1,8 @@
[gd_scene load_steps=4 format=3 uid="uid://cagfc5ridmteu"]
[gd_scene load_steps=5 format=3 uid="uid://cagfc5ridmteu"]
[ext_resource type="Script" uid="uid://d1f8blk5ucqvq" path="res://src/app/App.cs" id="1_rt73h"]
[ext_resource type="PackedScene" uid="uid://rfvnddfqufho" path="res://src/menu/MainMenu.tscn" id="2_1uiag"]
[ext_resource type="PackedScene" uid="uid://drkl3btdy6uxj" path="res://src/options/OptionsMenu.tscn" id="2_v0mgf"]
[ext_resource type="PackedScene" uid="uid://cpjlj7kxdhv16" path="res://src/menu/LoadingScreen.tscn" id="3_3st5l"]
[node name="App" type="Node"]
@@ -11,6 +12,9 @@ script = ExtResource("1_rt73h")
[node name="LoadingScreen" parent="." instance=ExtResource("3_3st5l")]
unique_name_in_owner = true
[node name="MainMenu" parent="." instance=ExtResource("2_1uiag")]
[node name="OptionsMenu" parent="." instance=ExtResource("2_v0mgf")]
unique_name_in_owner = true
visible = false
[node name="MainMenu" parent="." instance=ExtResource("2_1uiag")]
unique_name_in_owner = true

View File

@@ -3,12 +3,12 @@
importer="wav"
type="AudioStreamWAV"
uid="uid://ddii3pi8x75xc"
path="res://.godot/imported/amb_beach.wav-e64adf8f733e6a108ae15edd5f0499ab.sample"
path="res://.godot/imported/amb_beach.wav-046e4f838e50e43a1aba1a754b92aad6.sample"
[deps]
source_file="res://src/audio/amb/amb_beach.wav"
dest_files=["res://.godot/imported/amb_beach.wav-e64adf8f733e6a108ae15edd5f0499ab.sample"]
source_file="res://src/audio/AMB/amb_beach.wav"
dest_files=["res://.godot/imported/amb_beach.wav-046e4f838e50e43a1aba1a754b92aad6.sample"]
[params]

View File

@@ -3,12 +3,12 @@
importer="wav"
type="AudioStreamWAV"
uid="uid://ym4ur8a2qxhp"
path="res://.godot/imported/amb_perlin.wav-dea63667b2a56d37d48ba209f56f8900.sample"
path="res://.godot/imported/amb_perlin.wav-ba6da0d5591f392e4aca7d2f85c4dfc2.sample"
[deps]
source_file="res://src/audio/amb/amb_perlin.wav"
dest_files=["res://.godot/imported/amb_perlin.wav-dea63667b2a56d37d48ba209f56f8900.sample"]
source_file="res://src/audio/AMB/amb_perlin.wav"
dest_files=["res://.godot/imported/amb_perlin.wav-ba6da0d5591f392e4aca7d2f85c4dfc2.sample"]
[params]

View File

@@ -3,12 +3,12 @@
importer="wav"
type="AudioStreamWAV"
uid="uid://b7wxddjx3qw5o"
path="res://.godot/imported/amb_white_noise.wav-c98b45aa94120bc0c660bf2d6af1c696.sample"
path="res://.godot/imported/amb_white_noise.wav-d316dd05afe429f6bcdda594285ad718.sample"
[deps]
source_file="res://src/audio/amb/amb_white_noise.wav"
dest_files=["res://.godot/imported/amb_white_noise.wav-c98b45aa94120bc0c660bf2d6af1c696.sample"]
source_file="res://src/audio/AMB/amb_white_noise.wav"
dest_files=["res://.godot/imported/amb_white_noise.wav-d316dd05afe429f6bcdda594285ad718.sample"]
[params]

View File

@@ -3,12 +3,12 @@
importer="wav"
type="AudioStreamWAV"
uid="uid://bmiitw4fcs68e"
path="res://.godot/imported/amb_wind_loop_altar.wav-b9d60e3c3c10ec00833903539a7f3796.sample"
path="res://.godot/imported/amb_wind_loop_altar.wav-e766e3db29faa01ad6dbaa8cb18d7de6.sample"
[deps]
source_file="res://src/audio/amb/amb_wind_loop_altar.wav"
dest_files=["res://.godot/imported/amb_wind_loop_altar.wav-b9d60e3c3c10ec00833903539a7f3796.sample"]
source_file="res://src/audio/AMB/amb_wind_loop_altar.wav"
dest_files=["res://.godot/imported/amb_wind_loop_altar.wav-e766e3db29faa01ad6dbaa8cb18d7de6.sample"]
[params]

View File

@@ -18,6 +18,7 @@ public partial class AudioManager : Node
var soundEffects = Enum.GetValues(typeof(SoundEffect));
foreach (var effect in soundEffects)
_sfxDictionary.Add((SoundEffect)effect, GD.Load<AudioStream>(sfxPath + effect + ".ogg"));
_audioPlayer.Bus = "SFX";
}
public void Play(SoundEffect soundEffect)

View File

@@ -0,0 +1 @@
uid://ry5iewox61vs

View File

@@ -0,0 +1 @@
uid://t0j1h13hgo0t

View File

@@ -80,7 +80,7 @@ unique_name_in_owner = true
script = ExtResource("2_00xd7")
FolderName = "SetAFloors"
FloorOdds = Array[float]([0.0, 1.0])
GoldSproingy = 1.0
Sproingy = 1.0
[node name="Overworld" type="Node" parent="MapOrder"]
script = ExtResource("3_v14r0")

View File

@@ -26,6 +26,8 @@ public partial class MainMenu : Control, IMainMenu
[Node] public IButton GalleryButton { get; set; } = default!;
[Node] public IButton OptionsButton { get; set; } = default!;
[Node] public IButton QuitButton { get; set; } = default!;
[Signal]
@@ -35,6 +37,8 @@ public partial class MainMenu : Control, IMainMenu
[Signal]
public delegate void GalleryEventHandler();
[Signal]
public delegate void OptionsEventHandler();
[Signal]
public delegate void QuitEventHandler();
public void OnReady()
@@ -42,6 +46,7 @@ public partial class MainMenu : Control, IMainMenu
StartGameButton.Pressed += OnStartGamePressed;
EnemyViewerButton.Pressed += EnemyViewerButton_Pressed;
GalleryButton.Pressed += GalleryButton_Pressed;
OptionsButton.Pressed += OptionsButton_Pressed;
QuitButton.Pressed += OnQuitPressed;
StartGameButton.GrabFocus();
}
@@ -57,6 +62,7 @@ public partial class MainMenu : Control, IMainMenu
StartGameButton.Pressed -= OnStartGamePressed;
EnemyViewerButton.Pressed -= EnemyViewerButton_Pressed;
GalleryButton.Pressed -= GalleryButton_Pressed;
OptionsButton.Pressed -= OptionsButton_Pressed;
QuitButton.Pressed -= OnQuitPressed;
}
@@ -66,5 +72,7 @@ public partial class MainMenu : Control, IMainMenu
private void EnemyViewerButton_Pressed() => EmitSignal(SignalName.EnemyViewer);
private void OptionsButton_Pressed() => EmitSignal(SignalName.Options);
public void OnQuitPressed() => EmitSignal(SignalName.Quit);
}

View File

@@ -60,6 +60,14 @@ focus_neighbor_bottom = NodePath("../QuitButton")
theme_override_colors/font_focus_color = Color(0.976471, 0.827451, 0, 1)
text = "Gallery"
[node name="OptionsButton" type="Button" parent="MarginContainer/VBoxContainer"]
unique_name_in_owner = true
layout_mode = 2
focus_neighbor_top = NodePath("../EnemyViewerButton")
focus_neighbor_bottom = NodePath("../QuitButton")
theme_override_colors/font_focus_color = Color(0.976471, 0.827451, 0, 1)
text = "Options"
[node name="QuitButton" type="Button" parent="MarginContainer/VBoxContainer"]
unique_name_in_owner = true
layout_mode = 2

View File

@@ -0,0 +1,109 @@
using Chickensoft.AutoInject;
using Chickensoft.Introspection;
using Chickensoft.Serialization;
using Godot;
namespace Zennysoft.Game.Ma;
[Meta(typeof(IAutoNode))]
public partial class OptionsMenu : Control
{
public override void _Notification(int what) => this.Notify(what);
[Node] public OptionButton ResolutionOptions { get; set; }
[Node] public HSlider MasterVolumeSlider { get; set; }
[Node] public HSlider MusicVolumeSlider { get; set; }
[Node] public HSlider SFXVolumeSlider { get; set; }
[Node] public Button SaveAndExitButton { get; set; }
public OptionsData OptionsData;
private int _masterBusIndex;
private int _musicBusIndex;
private int _sfxBusIndex;
private readonly DisplayServer.WindowMode[] _windowModes = [DisplayServer.WindowMode.Windowed, DisplayServer.WindowMode.Maximized, DisplayServer.WindowMode.Fullscreen, DisplayServer.WindowMode.ExclusiveFullscreen];
[Signal] public delegate void OptionsMenuExitedEventHandler();
public void OnReady()
{
ResolutionOptions.AddItem("Windowed");
ResolutionOptions.AddItem("Maximized");
ResolutionOptions.AddItem("Fullscreen");
ResolutionOptions.AddItem("Exclusive Fullscreen");
ResolutionOptions.Select(0);
OptionsData = new OptionsData()
{
MasterVolumeLevel = MasterVolumeSlider.Value,
MusicVolumeLevel = MusicVolumeSlider.Value,
SFXVolumeLevel = SFXVolumeSlider.Value,
ScreenResolution = ResolutionOptions.GetSelectedId()
};
MasterVolumeSlider.ValueChanged += MasterVolumeSlider_Changed;
MusicVolumeSlider.ValueChanged += MusicVolumeSlider_Changed;
SFXVolumeSlider.ValueChanged += SFXVolumeSlider_Changed;
ResolutionOptions.ItemSelected += ResolutionOptions_ItemSelected;
SaveAndExitButton.ButtonUp += SaveAndExitButton_ButtonUp;
_masterBusIndex = AudioServer.GetBusIndex("Master");
_musicBusIndex = AudioServer.GetBusIndex("MUSIC");
_sfxBusIndex = AudioServer.GetBusIndex("SFX");
}
private void ResolutionOptions_ItemSelected(long index)
{
var resolutionIndex = ResolutionOptions.GetSelectedId();
OptionsData.ScreenResolution = resolutionIndex;
DisplayServer.WindowSetMode(_windowModes[resolutionIndex]);
}
public void Load(OptionsData optionsData)
{
MasterVolumeSlider.Value = optionsData.MasterVolumeLevel;
MusicVolumeSlider.Value = optionsData.MusicVolumeLevel;
SFXVolumeSlider.Value = optionsData.SFXVolumeLevel;
ResolutionOptions.Select(optionsData.ScreenResolution);
DisplayServer.WindowSetMode(_windowModes[optionsData.ScreenResolution]);
}
private void SaveAndExitButton_ButtonUp() => EmitSignal(SignalName.OptionsMenuExited);
private void MasterVolumeSlider_Changed(double valueChanged)
{
OptionsData.MasterVolumeLevel = valueChanged;
AudioServer.SetBusVolumeDb(_masterBusIndex, Mathf.LinearToDb((float)valueChanged));
}
private void MusicVolumeSlider_Changed(double valueChanged)
{
OptionsData.MusicVolumeLevel = valueChanged;
AudioServer.SetBusVolumeDb(_musicBusIndex, Mathf.LinearToDb((float)valueChanged));
}
private void SFXVolumeSlider_Changed(double valueChanged)
{
OptionsData.SFXVolumeLevel = valueChanged;
AudioServer.SetBusVolumeDb(_sfxBusIndex, Mathf.LinearToDb((float)valueChanged));
}
}
[Meta, Id("options_data")]
public partial class OptionsData : Node
{
[Save("MasterVolume")]
public required double MasterVolumeLevel { get; set; }
[Save("MusicVolume")]
public required double MusicVolumeLevel { get; set; }
[Save("SFXVolume")]
public required double SFXVolumeLevel { get; set; }
[Save("ScreenResolution")]
public required int ScreenResolution { get; set; }
}

View File

@@ -0,0 +1 @@
uid://cjxmdvhixcj6e

View File

@@ -0,0 +1,134 @@
[gd_scene load_steps=5 format=3 uid="uid://drkl3btdy6uxj"]
[ext_resource type="Script" uid="uid://cjxmdvhixcj6e" path="res://src/options/OptionsMenu.cs" id="1_jli36"]
[sub_resource type="StyleBoxLine" id="StyleBoxLine_jli36"]
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_utd4g"]
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_1egkf"]
bg_color = Color(2.5028e-06, 0.712708, 0.445629, 1)
[node name="OptionsMenu" type="Control"]
layout_mode = 3
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
script = ExtResource("1_jli36")
[node name="ColorRect" type="ColorRect" parent="."]
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
color = Color(0.137255, 0.121569, 0.12549, 1)
[node name="MarginContainer" type="MarginContainer" parent="."]
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
mouse_filter = 0
theme_override_constants/margin_left = 100
theme_override_constants/margin_top = 100
theme_override_constants/margin_right = 100
theme_override_constants/margin_bottom = 100
[node name="VBoxContainer" type="VBoxContainer" parent="MarginContainer"]
layout_mode = 2
size_flags_horizontal = 0
mouse_filter = 0
[node name="MasterVolume" type="HBoxContainer" parent="MarginContainer/VBoxContainer"]
layout_mode = 2
[node name="VolumeLabel" type="Label" parent="MarginContainer/VBoxContainer/MasterVolume"]
custom_minimum_size = Vector2(125, 0)
layout_mode = 2
text = "Master Volume"
horizontal_alignment = 2
[node name="MasterVolumeSlider" type="HSlider" parent="MarginContainer/VBoxContainer/MasterVolume"]
unique_name_in_owner = true
custom_minimum_size = Vector2(300, 0)
layout_mode = 2
size_flags_vertical = 1
theme_override_styles/slider = SubResource("StyleBoxLine_jli36")
theme_override_styles/grabber_area = SubResource("StyleBoxFlat_utd4g")
theme_override_styles/grabber_area_highlight = SubResource("StyleBoxFlat_1egkf")
max_value = 1.0
step = 0.001
value = 1.0
[node name="MusicVolume" type="HBoxContainer" parent="MarginContainer/VBoxContainer"]
layout_mode = 2
[node name="VolumeLabel" type="Label" parent="MarginContainer/VBoxContainer/MusicVolume"]
custom_minimum_size = Vector2(125, 0)
layout_mode = 2
text = "Music Volume"
horizontal_alignment = 2
[node name="MusicVolumeSlider" type="HSlider" parent="MarginContainer/VBoxContainer/MusicVolume"]
unique_name_in_owner = true
custom_minimum_size = Vector2(300, 0)
layout_mode = 2
size_flags_vertical = 1
theme_override_styles/slider = SubResource("StyleBoxLine_jli36")
theme_override_styles/grabber_area = SubResource("StyleBoxFlat_utd4g")
theme_override_styles/grabber_area_highlight = SubResource("StyleBoxFlat_1egkf")
max_value = 1.0
step = 0.001
value = 1.0
[node name="SFXVolume" type="HBoxContainer" parent="MarginContainer/VBoxContainer"]
layout_mode = 2
[node name="VolumeLabel" type="Label" parent="MarginContainer/VBoxContainer/SFXVolume"]
custom_minimum_size = Vector2(125, 0)
layout_mode = 2
text = "SFX Volume"
horizontal_alignment = 2
[node name="SFXVolumeSlider" type="HSlider" parent="MarginContainer/VBoxContainer/SFXVolume"]
unique_name_in_owner = true
custom_minimum_size = Vector2(300, 0)
layout_mode = 2
size_flags_vertical = 1
theme_override_styles/slider = SubResource("StyleBoxLine_jli36")
theme_override_styles/grabber_area = SubResource("StyleBoxFlat_utd4g")
theme_override_styles/grabber_area_highlight = SubResource("StyleBoxFlat_1egkf")
max_value = 1.0
step = 0.001
value = 1.0
[node name="Resolution" type="HBoxContainer" parent="MarginContainer/VBoxContainer"]
layout_mode = 2
[node name="ResolutionLabel" type="Label" parent="MarginContainer/VBoxContainer/Resolution"]
custom_minimum_size = Vector2(125, 0)
layout_mode = 2
text = "Resolution: "
horizontal_alignment = 2
[node name="ResolutionOptions" type="OptionButton" parent="MarginContainer/VBoxContainer/Resolution"]
unique_name_in_owner = true
custom_minimum_size = Vector2(200, 0)
layout_mode = 2
flat = true
[node name="HBoxContainer" type="HBoxContainer" parent="MarginContainer/VBoxContainer"]
layout_mode = 2
size_flags_vertical = 10
[node name="SaveAndExitButton" type="Button" parent="MarginContainer/VBoxContainer/HBoxContainer"]
unique_name_in_owner = true
custom_minimum_size = Vector2(150, 75)
layout_mode = 2
text = "Save and Exit"

View File

@@ -124,7 +124,7 @@ public partial class InventoryMenu : Control, IInventoryMenu
#pragma warning disable CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed
public override void _Input(InputEvent @event)
{
if (@event.IsActionPressed(GameInputs.UiCancel))
if (Visible && @event.IsActionPressed(GameInputs.UiCancel))
{
if (UseButton.HasFocus() || DropButton.HasFocus() || ThrowButton.HasFocus())
{