Files
TheriapolisV3/Theriapolis.Core/Persistence/CharacterCodec.cs
T

177 lines
7.5 KiB
C#
Raw Normal View History

using Theriapolis.Core.Data;
using Theriapolis.Core.Items;
using Theriapolis.Core.Rules.Character;
using Theriapolis.Core.Rules.Stats;
namespace Theriapolis.Core.Persistence;
/// <summary>
/// Converts between the live <see cref="Character"/> model and its flat
/// serializable snapshot <see cref="PlayerCharacterState"/>. Restore needs a
/// <see cref="ContentResolver"/> so it can re-attach to immutable defs by id.
/// </summary>
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<byte>(),
AsiAdjustmentsValues = h.AsiValues ?? Array.Empty<int>(),
FeaturesUnlocked = h.FeaturesUnlocked ?? Array.Empty<string>(),
});
}
}
// 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<int>())
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;
}
}