Files
chameleon/Assets/Scripts/Helper/GameSettings.cs
T
adminandClaude Fable 5 270c42d85b Add host-configurable match settings persisted in settings
- GameSettings: 7 match fields (mode, hunter count, hide/seek/resolve
  durations, radar ping interval/lifetime) with authored defaults
- MatchSettingSlider (new): enum-driven panel binder, seeds from
  settings, saves on change; lifetime label shows one decimal
  (invariant culture)
- MatchManager: ApplyHostMatchSettings() on StartMatch (clamped);
  phase message now carries ping interval so clients tick correctly

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-08 22:04:54 +02:00

155 lines
5.5 KiB
C#

using System;
using System.IO;
using UnityEngine;
/// Shared JSON-backed store for player-facing game settings (<persistentDataPath>/Settings.json).
/// Loaded lazily and cached in memory, so per-frame reads never touch the disk; each change is
/// written immediately (settings edits are rare), so a hard-killed build keeps them. The typed
/// facades (MouseSensitivity, MasterVolume) own the ranges/clamping; this class only persists raw values.
public static class GameSettings
{
private const string FileName = "Settings.json";
private const int FileVersion = 1;
[Serializable]
private class Data
{
public int version = FileVersion;
public float mouseSensitivity = global::MouseSensitivity.Default;
public float masterVolume = global::MasterVolume.Default;
public string playerName = "";
// Host-side match rules (Esc-menu Match Panel). Ranges are authored on the panel sliders; the
// values are applied server-side in MatchManager.StartMatch (with defensive clamps there).
// Defaults mirror the Main scene's authored MatchManager values, so a fresh Settings.json
// starts matches exactly like before the panel existed.
public int matchMode = 0; // MatchManager.MatchMode index (0 = Classic, 1 = Conversion)
public int matchHunterCount = 1;
public float matchHideDuration = 120f;
public float matchSeekDuration = 300f;
public float matchResolveDuration = 50f;
public float matchRadarPingInterval = 30f;
public float matchRadarPingLifetime = 1f;
}
private static Data _data;
private static Data Store => _data ??= Load();
private static string SavePath() => Path.Combine(Application.persistentDataPath, FileName);
public static float MouseSensitivity
{
get => Store.mouseSensitivity;
set
{
if (Mathf.Approximately(Store.mouseSensitivity, value)) return;
Store.mouseSensitivity = value;
Save();
}
}
public static float MasterVolume
{
get => Store.masterVolume;
set
{
if (Mathf.Approximately(Store.masterVolume, value)) return;
Store.masterVolume = value;
Save();
}
}
/// <summary>Display name shown above the avatar. Empty = per-spawn fallback ("Player &lt;clientId&gt;").</summary>
public static string PlayerName
{
get => Store.playerName ?? "";
set
{
string v = value == null ? "" : value.Trim();
if (Store.playerName == v) return;
Store.playerName = v;
Save();
}
}
/// <summary>MatchManager.MatchMode enum index the host starts matches with (0 = Classic, 1 = Conversion).</summary>
public static int MatchModeIndex
{
get => Store.matchMode;
set { if (Store.matchMode != value) { Store.matchMode = value; Save(); } }
}
/// <summary>How many randomly chosen players become Hunters at match start.</summary>
public static int MatchHunterCount
{
get => Store.matchHunterCount;
set { if (Store.matchHunterCount != value) { Store.matchHunterCount = value; Save(); } }
}
/// <summary>HIDE phase length in seconds.</summary>
public static float MatchHideDuration
{
get => Store.matchHideDuration;
set { if (!Mathf.Approximately(Store.matchHideDuration, value)) { Store.matchHideDuration = value; Save(); } }
}
/// <summary>SEEK phase length in seconds.</summary>
public static float MatchSeekDuration
{
get => Store.matchSeekDuration;
set { if (!Mathf.Approximately(Store.matchSeekDuration, value)) { Store.matchSeekDuration = value; Save(); } }
}
/// <summary>RESOLUTION (result screen) phase length in seconds.</summary>
public static float MatchResolveDuration
{
get => Store.matchResolveDuration;
set { if (!Mathf.Approximately(Store.matchResolveDuration, value)) { Store.matchResolveDuration = value; Save(); } }
}
/// <summary>Seconds between radar ping sweeps during Seek (0 = pings disabled).</summary>
public static float MatchRadarPingInterval
{
get => Store.matchRadarPingInterval;
set { if (!Mathf.Approximately(Store.matchRadarPingInterval, value)) { Store.matchRadarPingInterval = value; Save(); } }
}
/// <summary>Seconds each hider radar blip stays alive before it fades.</summary>
public static float MatchRadarPingLifetime
{
get => Store.matchRadarPingLifetime;
set { if (!Mathf.Approximately(Store.matchRadarPingLifetime, value)) { Store.matchRadarPingLifetime = value; Save(); } }
}
private static Data Load()
{
string path = SavePath();
if (File.Exists(path))
{
try
{
var data = JsonUtility.FromJson<Data>(File.ReadAllText(path));
if (data != null) return data;
}
catch (Exception e)
{
Debug.LogWarning($"[GameSettings] Loading {FileName} failed, using defaults: {e.Message}");
}
}
return new Data();
}
private static void Save()
{
try
{
Directory.CreateDirectory(Application.persistentDataPath);
File.WriteAllText(SavePath(), JsonUtility.ToJson(Store, true));
}
catch (Exception e)
{
Debug.LogWarning($"[GameSettings] Saving {FileName} failed: {e.Message}");
}
}
}