using System;
using System.Collections.Generic;
using System.IO;
using UnityEngine;
using UnityEngine.Events;
/// <summary>
/// GAME 1303 Tier 7 — Writes the game state to a JSON file and reads it back.
/// Cozy Critters (essential — offline decay), Zap Attack, MythoClash, Anabasis,
/// Store Simulator.
///
/// WHERE THE FILE GOES: Application.persistentDataPath, which is a real folder on disk
/// (Windows: %AppData%/../LocalLow/<Company>/<Product>). The Log Save Path option prints
/// it to the Console so students can open it in Notepad and SEE their save — that moment
/// is worth more than any explanation of serialization.
///
/// SETUP
/// 1. One empty GameObject "SaveSystem" with this script. Tick Persist Across Scenes.
/// 2. Tick which systems to include. Each one is optional and safely skipped if the
/// matching component isn't in the scene.
/// 3. Saving:
/// - Auto Save Every N seconds (0 = off)
/// - Save On Quit (recommended — catches the alt-F4 case)
/// - or call SaveSystem.Instance.Save() from a button / a checkpoint.
/// 4. Loading: Load On Start reads the file when the game opens. Apply Offline Decay
/// tells every NeedStat how long the player has been away, which is exactly Rhys's
/// Tamagotchi model without the punishment.
/// 5. Slots: Slot Name lets you have "save1", "save2", or one per player.
///
/// This deliberately replaces nothing: Tier 4's LevelProgress (PlayerPrefs) still handles
/// level unlocks and best times. Use this for the *state of the world*, that for *progress*.
/// </summary>
public class SaveSystem : MonoBehaviour
{
public static SaveSystem Instance { get; private set; }
[Header("File")]
public string slotName = "save1";
public bool prettyPrint = true;
public bool logSavePath = true;
[Header("What to include")]
public bool saveFlags = true;
public bool saveCollectibles = true;
public bool savePlayerPosition = false;
public bool savePetStats = true;
public bool saveClues = true;
[Header("When")]
public bool loadOnStart = true;
public bool saveOnQuit = true;
[Tooltip("Seconds between automatic saves. 0 = off.")]
public float autoSaveEvery = 60f;
public bool persistAcrossScenes = true;
[Header("Offline")]
[Tooltip("Cozy Critters: decay needs by the real time the player was away.")]
public bool applyOfflineDecay = true;
[Tooltip("Cap the offline decay so a week away doesn't empty everything.")]
public float maxOfflineMinutes = 480f;
[Header("Events")]
public UnityEvent OnSaved;
public UnityEvent OnLoaded;
public UnityEvent<float> OnReturnedAfterMinutes; // for a "welcome back" message
public UnityEvent OnNoSaveFound;
public string FilePath => Path.Combine(Application.persistentDataPath, slotName + ".json");
public bool HasSave => File.Exists(FilePath);
private float autoTimer;
void Awake()
{
if (Instance != null && Instance != this) { Destroy(gameObject); return; }
Instance = this;
if (persistAcrossScenes) DontDestroyOnLoad(gameObject);
if (logSavePath) Debug.Log("SaveSystem: save file is at " + FilePath);
}
void Start()
{
if (loadOnStart) Load();
autoTimer = autoSaveEvery;
}
void Update()
{
if (autoSaveEvery <= 0f) return;
autoTimer -= Time.unscaledDeltaTime;
if (autoTimer <= 0f) { autoTimer = autoSaveEvery; Save(); }
}
void OnApplicationQuit() { if (saveOnQuit) Save(); }
void OnApplicationPause(bool paused) { if (paused && saveOnQuit) Save(); } // mobile
// ---------------------------------------------------------------- SAVE
public void Save()
{
var data = new SaveData
{
savedAtUtc = DateTime.UtcNow.ToString("o"),
sceneName = UnityEngine.SceneManagement.SceneManager.GetActiveScene().name
};
if (saveFlags && ChoiceFlagManager.Instance != null)
data.flags = ChoiceFlagManager.Instance.GetAllFlags();
if (saveCollectibles && CollectionTracker.Instance != null)
{
data.score = CollectionTracker.Instance.Score;
foreach (var kv in CollectionTracker.Instance.GetAllCounts())
{
data.itemNames.Add(kv.Key.name);
data.itemCounts.Add(kv.Value);
}
}
if (savePlayerPosition)
{
var p = GameObject.FindGameObjectWithTag("Player");
if (p != null)
{
data.playerX = p.transform.position.x;
data.playerY = p.transform.position.y;
data.hasPlayerPosition = true;
var hp = p.GetComponent<Health>();
if (hp != null) data.playerHealth = hp.CurrentHealth;
}
}
if (savePetStats)
{
foreach (var stat in FindObjectsByType<NeedStat>(FindObjectsSortMode.None))
{
data.statNames.Add(stat.statName);
data.statValues.Add(stat.value);
}
var personality = FindFirstObjectByType<PersonalityTracker>();
if (personality != null && personality.Current != null)
data.personalityId = personality.Current.id;
}
if (saveClues && ClueJournal.Instance != null)
foreach (var clue in ClueJournal.Instance.allClues)
if (ClueJournal.Instance.HasFound(clue.id)) data.clueIds.Add(clue.id);
try
{
File.WriteAllText(FilePath, JsonUtility.ToJson(data, prettyPrint));
OnSaved?.Invoke();
}
catch (Exception e)
{
Debug.LogError("SaveSystem: could not write the save file. " + e.Message);
}
}
// ---------------------------------------------------------------- LOAD
public bool Load()
{
if (!HasSave) { OnNoSaveFound?.Invoke(); return false; }
SaveData data;
try { data = JsonUtility.FromJson<SaveData>(File.ReadAllText(FilePath)); }
catch (Exception e)
{
Debug.LogError("SaveSystem: save file is unreadable, starting fresh. " + e.Message);
OnNoSaveFound?.Invoke();
return false;
}
if (data == null) { OnNoSaveFound?.Invoke(); return false; }
float away = Mathf.Min(data.MinutesSinceSaved, maxOfflineMinutes);
if (saveFlags && ChoiceFlagManager.Instance != null)
ChoiceFlagManager.Instance.LoadFlags(data.flags);
if (saveCollectibles && CollectionTracker.Instance != null)
{
var lookup = new Dictionary<string, ItemType>();
foreach (var t in Resources.FindObjectsOfTypeAll<ItemType>()) lookup[t.name] = t;
CollectionTracker.Instance.ResetAll();
for (int i = 0; i < data.itemNames.Count && i < data.itemCounts.Count; i++)
if (lookup.TryGetValue(data.itemNames[i], out var type))
CollectionTracker.Instance.Add(type, data.itemCounts[i]);
}
if (savePlayerPosition && data.hasPlayerPosition)
{
var p = GameObject.FindGameObjectWithTag("Player");
if (p != null)
{
p.transform.position = new Vector3(data.playerX, data.playerY, p.transform.position.z);
var hp = p.GetComponent<Health>();
if (hp != null && data.playerHealth > 0) hp.SetCurrentHealth(data.playerHealth);
}
}
if (savePetStats)
{
var stats = FindObjectsByType<NeedStat>(FindObjectsSortMode.None);
for (int i = 0; i < data.statNames.Count && i < data.statValues.Count; i++)
{
foreach (var s in stats)
{
if (s.statName != data.statNames[i]) continue;
s.SetValue(data.statValues[i]);
if (applyOfflineDecay && away > 0f) s.ApplyOfflineTime(away);
break;
}
}
}
if (saveClues && ClueJournal.Instance != null)
foreach (var id in data.clueIds) ClueJournal.Instance.Discover(id);
if (away > 1f) OnReturnedAfterMinutes?.Invoke(away);
OnLoaded?.Invoke();
return true;
}
// ---------------------------------------------------------------- HOUSEKEEPING
public void DeleteSave()
{
if (HasSave) File.Delete(FilePath);
}
/// <summary>Wire to a "New Game" button: wipes the file and the in-memory state.</summary>
public void NewGame()
{
DeleteSave();
ChoiceFlagManager.Instance?.ResetAll();
CollectionTracker.Instance?.ResetAll();
ClueJournal.Instance?.ResetJournal();
DialogueRunner.ResetPlayedHistory();
}
}