using Theriapolis.Core.Data; using Theriapolis.Core.Items; using Theriapolis.Core.Rules.Character; using Theriapolis.Core.Rules.Stats; namespace Theriapolis.Core.Persistence; /// /// Converts between the live model and its flat /// serializable snapshot . Restore needs a /// so it can re-attach to immutable defs by id. /// public static class CharacterCodec { public static PlayerCharacterState Capture(Character c) { var state = new PlayerCharacterState { CladeId = c.Clade.Id, SpeciesId = c.Species.Id, ClassId = c.ClassDef.Id, BackgroundId = c.Background.Id, STR = c.Abilities.STR, DEX = c.Abilities.DEX, CON = c.Abilities.CON, INT = c.Abilities.INT, WIS = c.Abilities.WIS, CHA = c.Abilities.CHA, Level = c.Level, Xp = c.Xp, MaxHp = c.MaxHp, CurrentHp = c.CurrentHp, ExhaustionLevel = c.ExhaustionLevel, FightingStyle = c.FightingStyle, RageUsesRemaining = c.RageUsesRemaining, CurrencyFang = c.CurrencyFang, SubclassId = c.SubclassId, LearnedFeatureIds = c.LearnedFeatureIds.ToArray(), LevelUpHistory = c.LevelUpHistory.Select(h => new LevelUpRecordState { Level = h.Level, HpGained = h.HpGained, HpWasAveraged = h.HpWasAveraged, HpHitDieResult = h.HpHitDieResult, SubclassChosen = h.SubclassChosen ?? "", AsiKeys = h.AsiAdjustmentsKeys, AsiValues = h.AsiAdjustmentsValues, FeaturesUnlocked = h.FeaturesUnlocked, }).ToArray(), // Phase 6.5 M4 — hybrid state. Null for purebred PCs. // Phase 6.5 M5 adds ActiveMaskTier. Hybrid = c.Hybrid is null ? null : new HybridStateSnapshot { SireClade = c.Hybrid.SireClade, SireSpecies = c.Hybrid.SireSpecies, DamClade = c.Hybrid.DamClade, DamSpecies = c.Hybrid.DamSpecies, DominantParent = (byte)c.Hybrid.DominantParent, PassingActive = c.Hybrid.PassingActive, NpcsWhoKnow = c.Hybrid.NpcsWhoKnow.ToArray(), ActiveMaskTier = (byte)c.Hybrid.ActiveMaskTier, }, }; var skills = new byte[c.SkillProficiencies.Count]; int i = 0; foreach (var s in c.SkillProficiencies) skills[i++] = (byte)s; state.SkillProficiencies = skills; var conds = new byte[c.Conditions.Count]; i = 0; foreach (var x in c.Conditions) conds[i++] = (byte)x; state.Conditions = conds; var inv = new InventoryItemState[c.Inventory.Items.Count]; for (int k = 0; k < c.Inventory.Items.Count; k++) { var it = c.Inventory.Items[k]; inv[k] = new InventoryItemState { ItemId = it.Def.Id, Qty = it.Qty, Condition = it.Condition, EquippedAt = it.EquippedAt is { } slot ? (byte)slot : null, }; } state.Inventory = inv; return state; } public static Character Restore(PlayerCharacterState state, ContentResolver content) { if (!content.Clades.TryGetValue(state.CladeId, out var clade)) throw new InvalidDataException($"Save references unknown clade '{state.CladeId}'."); if (!content.Species.TryGetValue(state.SpeciesId, out var species)) throw new InvalidDataException($"Save references unknown species '{state.SpeciesId}'."); if (!content.Classes.TryGetValue(state.ClassId, out var classDef)) throw new InvalidDataException($"Save references unknown class '{state.ClassId}'."); if (!content.Backgrounds.TryGetValue(state.BackgroundId, out var bg)) throw new InvalidDataException($"Save references unknown background '{state.BackgroundId}'."); var abilities = new AbilityScores(state.STR, state.DEX, state.CON, state.INT, state.WIS, state.CHA); var c = new Character(clade, species, classDef, bg, abilities) { Level = state.Level, Xp = state.Xp, MaxHp = state.MaxHp, CurrentHp = state.CurrentHp, ExhaustionLevel = state.ExhaustionLevel, FightingStyle = state.FightingStyle, RageUsesRemaining = state.RageUsesRemaining, CurrencyFang = state.CurrencyFang, SubclassId = state.SubclassId ?? "", }; // Phase 6.5 M0 — restore learned features + level-up history. if (state.LearnedFeatureIds is not null) foreach (var fid in state.LearnedFeatureIds) c.LearnedFeatureIds.Add(fid); if (state.LevelUpHistory is not null) { foreach (var h in state.LevelUpHistory) { c.LevelUpHistory.Add(new LevelUpRecord { Level = h.Level, HpGained = h.HpGained, HpWasAveraged = h.HpWasAveraged, HpHitDieResult = h.HpHitDieResult, SubclassChosen = string.IsNullOrEmpty(h.SubclassChosen) ? null : h.SubclassChosen, AsiAdjustmentsKeys = h.AsiKeys ?? Array.Empty(), AsiAdjustmentsValues = h.AsiValues ?? Array.Empty(), FeaturesUnlocked = h.FeaturesUnlocked ?? Array.Empty(), }); } } // Phase 6.5 M4 — restore hybrid state when present. if (state.Hybrid is not null) { c.Hybrid = new HybridState { SireClade = state.Hybrid.SireClade, SireSpecies = state.Hybrid.SireSpecies, DamClade = state.Hybrid.DamClade, DamSpecies = state.Hybrid.DamSpecies, DominantParent = (ParentLineage)state.Hybrid.DominantParent, PassingActive = state.Hybrid.PassingActive, ActiveMaskTier = (ScentMaskTier)state.Hybrid.ActiveMaskTier, }; foreach (int npcId in state.Hybrid.NpcsWhoKnow ?? Array.Empty()) c.Hybrid.NpcsWhoKnow.Add(npcId); } foreach (var s in state.SkillProficiencies) c.SkillProficiencies.Add((SkillId)s); foreach (var x in state.Conditions) c.Conditions.Add((Condition)x); foreach (var it in state.Inventory) { if (!content.Items.TryGetValue(it.ItemId, out var def)) throw new InvalidDataException($"Save references unknown item '{it.ItemId}'."); var inst = c.Inventory.Add(def, it.Qty); inst.Condition = it.Condition; if (it.EquippedAt is { } slotByte) { var slot = (EquipSlot)slotByte; if (!c.Inventory.TryEquip(inst, slot, out var err)) throw new InvalidDataException($"Could not re-equip '{it.ItemId}' into {slot}: {err}"); } } return c; } }