using Theriapolis.Core; using Theriapolis.Core.Data; using Theriapolis.Core.Items; using Theriapolis.Core.Persistence; using Theriapolis.Core.Rules.Character; using Theriapolis.Core.Rules.Stats; using Xunit; namespace Theriapolis.Tests.Persistence; public sealed class Phase5SaveRoundTripTests { private readonly ContentResolver _content = new(new ContentLoader(TestHelpers.DataDirectory)); [Fact] public void Character_RoundTripsThroughCharacterCodec() { var c = MakeBasicCharacter(); // Add and equip an item so the codec hits the inventory + equip-slot path. var sword = c.Inventory.Add(_content.Items["rend_sword"]); c.Inventory.TryEquip(sword, EquipSlot.MainHand, out _); c.Conditions.Add(Condition.Frightened); c.ExhaustionLevel = 2; c.CurrentHp = c.MaxHp - 3; c.Xp = 142; var snap = CharacterCodec.Capture(c); var restored = CharacterCodec.Restore(snap, _content); Assert.Equal(c.Clade.Id, restored.Clade.Id); Assert.Equal(c.Species.Id, restored.Species.Id); Assert.Equal(c.ClassDef.Id, restored.ClassDef.Id); Assert.Equal(c.Background.Id, restored.Background.Id); Assert.Equal(c.Abilities.STR, restored.Abilities.STR); Assert.Equal(c.Abilities.WIS, restored.Abilities.WIS); Assert.Equal(c.Level, restored.Level); Assert.Equal(c.Xp, restored.Xp); Assert.Equal(c.MaxHp, restored.MaxHp); Assert.Equal(c.CurrentHp, restored.CurrentHp); Assert.Equal(c.ExhaustionLevel, restored.ExhaustionLevel); Assert.Contains(Condition.Frightened, restored.Conditions); Assert.Single(restored.Inventory.Items); Assert.Equal("rend_sword", restored.Inventory.Items[0].Def.Id); Assert.Equal(EquipSlot.MainHand, restored.Inventory.Items[0].EquippedAt); Assert.Same(restored.Inventory.Items[0], restored.Inventory.GetEquipped(EquipSlot.MainHand)); // Skill set should match (set equality) Assert.Equal(c.SkillProficiencies, restored.SkillProficiencies); } [Fact] public void SaveBody_PlayerCharacter_RoundTripsThroughSaveCodec() { var c = MakeBasicCharacter(); var snap = CharacterCodec.Capture(c); var header = new SaveHeader { Version = C.SAVE_SCHEMA_VERSION, WorldSeedHex = "0xFEED" }; var body = new SaveBody { PlayerCharacter = snap }; // Player is not interesting for this test; clock + chunks empty. body.Player.Id = 1; body.Player.Name = "Tester"; var bytes = SaveCodec.Serialize(header, body); var (h2, body2) = SaveCodec.Deserialize(bytes); Assert.Equal(header.Version, h2.Version); Assert.NotNull(body2.PlayerCharacter); Assert.Equal(snap.CladeId, body2.PlayerCharacter!.CladeId); Assert.Equal(snap.SpeciesId, body2.PlayerCharacter.SpeciesId); Assert.Equal(snap.ClassId, body2.PlayerCharacter.ClassId); Assert.Equal(snap.STR, body2.PlayerCharacter.STR); Assert.Equal(snap.WIS, body2.PlayerCharacter.WIS); Assert.Equal(snap.MaxHp, body2.PlayerCharacter.MaxHp); } [Fact] public void SaveBody_NoCharacter_StillRoundTrips() { // Phase-4-style body with no character. Round-trips cleanly because // TAG_CHARACTER is only written when PlayerCharacter is non-null. var header = new SaveHeader { Version = C.SAVE_SCHEMA_VERSION, WorldSeedHex = "0xDEAD" }; var body = new SaveBody(); var bytes = SaveCodec.Serialize(header, body); var (_, body2) = SaveCodec.Deserialize(bytes); Assert.Null(body2.PlayerCharacter); } private Character MakeBasicCharacter() { var b = new CharacterBuilder { Clade = _content.Clades["canidae"], Species = _content.Species["wolf"], ClassDef = _content.Classes["fangsworn"], Background = _content.Backgrounds["pack_raised"], BaseAbilities = new AbilityScores(15, 14, 13, 12, 10, 8), Name = "Roundtrip", }; // Class.SkillsChoose = 2 for fangsworn b.ChosenClassSkills.Add(SkillId.Athletics); b.ChosenClassSkills.Add(SkillId.Intimidation); return b.Build(); } }