177 lines
7.5 KiB
C#
177 lines
7.5 KiB
C#
|
|
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;
|
||
|
|
}
|
||
|
|
}
|