using System; using System.Collections.Generic; using Cthangover.Core.Utils; namespace Cthangover.Core.Relationship { /// /// Container and factory for the party's recruit roster. The roster is owned /// by the recruiting mod through the static instead of /// living in the core RuntimeData, so the core knows nothing about /// recruiting. Persistence is handled by the mod's RecruitingSaveable /// (IModSaveable), and a new game clears the roster through the /// RecruitingReset IResettable that RecruitingInitializer /// registers with AppServices. /// /// Provides ID-based lookup, duplicate-safe addition (auto-generates a unique /// Guid if the requested ID collides, and rejects a second recruit of /// the same enemy type), and removal that notifies /// so behaviours can clean up and /// detaches the recruit from the squad through . /// initialises the recruit's Properties with a default /// health of 10 via and immediately calls /// RecruitBehaviourRegistry.OnConfigure, giving every registered /// behaviour a chance to set up per-recruit state before the next tick fires. /// public class RecruitingData { /// /// Session-wide roster. There is exactly one roster per playthrough — /// starting a new game clears it, loading a save replaces its contents. /// public static readonly RecruitingData Instance = new(); /// The current recruit roster. public List Data { get; set; } = new(); /// True if a recruit with the given ID exists in the roster. public bool HasID(string id) { return GetByID(id) != null; } /// Returns the recruit with the given ID, or null if not found. public Recruit GetByID(string id) { foreach (var item in Data) { if (item.ID == id) return item; } return null; } /// /// True if a recruit spawned from the given character template is already /// in the roster. Recruitment allows only one instance per enemy type, /// which also keeps the template id usable as a unique party key. /// public bool HasCharacterID(string characterID) { return GetByCharacterID(characterID) != null; } /// Returns the recruit spawned from the given template, or null if not found. public Recruit GetByCharacterID(string characterID) { foreach (var item in Data) { if (item.CharacterID == characterID) return item; } return null; } /// /// Generates a unique ID: if is free, /// returns it unchanged; otherwise appends a Guid suffix. /// Loops until an unused ID is found (guaranteed because Guid /// is unique enough for any practical roster size). /// private string GenID(string id) { for (;;) { if (!HasID(id)) return id; id = Guid.NewGuid().ToString(); } } /// /// Creates a new from the given /// , assigns a unique instance ID, /// initialises default health, appends to the roster, and fires /// RecruitBehaviourRegistry.OnConfigure so behaviours /// can set up per-recruit state. /// /// Only one recruit per enemy type is allowed: a second attempt for an /// already captured is ignored and the /// existing entry is returned, so callers cannot create duplicates and /// the template id remains a unique party card key. /// public Recruit Add(string id, string characterID) { var existing = GetByCharacterID(characterID); if (existing != null) { GameLogger.Log("RECRUIT", $"Recruit '{characterID}' already captured, duplicate ignored", LogLevel.Warning); return existing; } var data = new Recruit { ID = GenID(id), CharacterID = characterID, }; data.Properties.SetInt(Recruit.PROP_HEALTH, 10); Data.Add(data); RecruitBehaviourRegistry.Instance.OnConfigure(data); return data; } /// /// Removes the recruit with the given ID from the roster. /// Notifies /// before deleting the entry so behaviours can persist state /// or clean up companion nodes, then detaches the recruit from the /// party and deletes its placeholder card via /// , so no orphan party member /// survives in the core save. /// public void Remove(string id) { for (int i = Data.Count - 1; i >= 0; i--) { if (Data[i].ID == id) { RecruitBehaviourRegistry.Instance.OnRemove(Data[i]); RecruitPartyBridge.Forget(Data[i]); Data.RemoveAt(i); return; } } } } }