Initial commit: Theriapolis baseline at port/godot branch point
Captures the pre-Godot-port state of the codebase. This is the rollback anchor for the Godot port (M0 of theriapolis-rpg-implementation-plan-godot-port.md). All Phase 0 through Phase 6.5 work is included; Phase 7 is in flight. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,104 @@
|
||||
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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user