feat(ui): add persistent bilingual settings and combat HUD
This commit is contained in:
@@ -0,0 +1,152 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
using UnityEngine.InputSystem;
|
||||
|
||||
namespace Ironspore
|
||||
{
|
||||
public enum SettingsApplyResult { Saved, ConfirmDisplay, Failed }
|
||||
|
||||
// Runtime settings orchestration; UI owns its draft, while the store owns durable snapshots.
|
||||
public sealed class SettingsService : IDisposable
|
||||
{
|
||||
readonly SettingsStore store;
|
||||
readonly GameSettingsData defaults;
|
||||
readonly float initialVolume;
|
||||
readonly int initialQuality;
|
||||
readonly Func<float> clock;
|
||||
GameSettingsData current, preview;
|
||||
float displayDeadline;
|
||||
public InputBindingService Bindings { get; }
|
||||
public GameSettingsData Current => current.Copy();
|
||||
public GameSettingsData Defaults => defaults.Copy();
|
||||
public bool ConfirmingDisplay => preview != null;
|
||||
public int ConfirmationSeconds => Mathf.Max(0, Mathf.CeilToInt(displayDeadline - clock()));
|
||||
public string Error => store.Error;
|
||||
public float LookSensitivity => current.mouseSensitivity;
|
||||
public float AimSensitivity => current.aimSensitivity;
|
||||
|
||||
public SettingsService(SettingsStore store, InputActionAsset input, GameSettingsData defaults, Func<float> clock = null)
|
||||
{
|
||||
this.store = store;
|
||||
this.defaults = defaults.Copy();
|
||||
this.clock = clock ?? (() => Time.realtimeSinceStartup);
|
||||
initialVolume = AudioListener.volume;
|
||||
initialQuality = QualitySettings.GetQualityLevel();
|
||||
Bindings = new InputBindingService(input);
|
||||
current = store.Data.Normalize(QualitySettings.names.Length);
|
||||
if (!Bindings.TryLoad(current.bindingOverrides))
|
||||
Debug.LogWarning("Invalid input overrides were ignored; using default controls.");
|
||||
current.bindingOverrides = Bindings.Save();
|
||||
ApplyRuntime(current, true);
|
||||
}
|
||||
|
||||
public static GameSettingsData RuntimeDefaults() => new GameSettingsData
|
||||
{
|
||||
qualityLevel = Mathf.Min(2, QualitySettings.names.Length - 1),
|
||||
resolutionWidth = Application.isEditor ? 1280 : Mathf.Max(640, Screen.width),
|
||||
resolutionHeight = Application.isEditor ? 720 : Mathf.Max(360, Screen.height),
|
||||
fullscreen = !Application.isEditor && Screen.fullScreen
|
||||
};
|
||||
|
||||
public List<Vector2Int> Resolutions()
|
||||
{
|
||||
var sizes = new List<Vector2Int>();
|
||||
foreach (var resolution in Screen.resolutions)
|
||||
AddSize(sizes, resolution.width, resolution.height);
|
||||
AddSize(sizes, 1280, 720);
|
||||
AddSize(sizes, 1600, 900);
|
||||
AddSize(sizes, 1920, 1080);
|
||||
AddSize(sizes, current.resolutionWidth, current.resolutionHeight);
|
||||
sizes.Sort((a, b) => a.x == b.x ? a.y.CompareTo(b.y) : a.x.CompareTo(b.x));
|
||||
return sizes;
|
||||
}
|
||||
|
||||
static void AddSize(List<Vector2Int> sizes, int width, int height)
|
||||
{
|
||||
var size = new Vector2Int(width, height);
|
||||
if (width >= 640 && height >= 360 && !sizes.Contains(size))
|
||||
sizes.Add(size);
|
||||
}
|
||||
|
||||
public SettingsApplyResult Apply(GameSettingsData draft)
|
||||
{
|
||||
CancelDisplay();
|
||||
var candidate = draft.Normalize(QualitySettings.names.Length);
|
||||
candidate.bindingOverrides = Bindings.Save();
|
||||
if (!candidate.SameDisplay(current))
|
||||
{
|
||||
preview = candidate;
|
||||
displayDeadline = clock() + 15;
|
||||
ApplyRuntime(preview, true);
|
||||
return SettingsApplyResult.ConfirmDisplay;
|
||||
}
|
||||
return Save(candidate) ? SettingsApplyResult.Saved : SettingsApplyResult.Failed;
|
||||
}
|
||||
|
||||
public bool ConfirmDisplay()
|
||||
{
|
||||
if (preview == null)
|
||||
return false;
|
||||
var candidate = preview;
|
||||
preview = null;
|
||||
if (Save(candidate))
|
||||
return true;
|
||||
ApplyRuntime(current, true);
|
||||
Bindings.TryLoad(current.bindingOverrides);
|
||||
return false;
|
||||
}
|
||||
|
||||
bool Save(GameSettingsData candidate)
|
||||
{
|
||||
if (!store.Save(candidate))
|
||||
return false;
|
||||
current = candidate.Copy();
|
||||
ApplyRuntime(current, false);
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool Tick()
|
||||
{
|
||||
if (!ConfirmingDisplay || clock() < displayDeadline)
|
||||
return false;
|
||||
CancelDisplay();
|
||||
return true;
|
||||
}
|
||||
|
||||
public void CancelDisplay()
|
||||
{
|
||||
if (preview == null)
|
||||
return;
|
||||
preview = null;
|
||||
ApplyRuntime(current, true);
|
||||
}
|
||||
|
||||
public void DiscardDraft()
|
||||
{
|
||||
Bindings.Cancel();
|
||||
CancelDisplay();
|
||||
Bindings.TryLoad(current.bindingOverrides);
|
||||
}
|
||||
|
||||
void ApplyRuntime(GameSettingsData data, bool display)
|
||||
{
|
||||
AudioListener.volume = data.masterVolume;
|
||||
if (QualitySettings.GetQualityLevel() != data.qualityLevel)
|
||||
QualitySettings.SetQualityLevel(data.qualityLevel, true);
|
||||
if (display && !Application.isEditor)
|
||||
Screen.SetResolution(data.resolutionWidth, data.resolutionHeight,
|
||||
data.fullscreen ? FullScreenMode.ExclusiveFullScreen : FullScreenMode.Windowed);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
DiscardDraft();
|
||||
Bindings.Dispose();
|
||||
#if UNITY_EDITOR
|
||||
AudioListener.volume = initialVolume;
|
||||
QualitySettings.SetQualityLevel(initialQuality, true);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user