import sqlite3 from pathlib import Path import pytest from engine.character import Ailment, BodyPart, Character, ClothingPiece, HeldItem, Organ from engine.character_library import CharacterLibrary from engine.defs import DefRegistry from engine.inventory import Inventory from engine.item import ItemInstance DEFS_DIR = Path(__file__).resolve().parent.parent / "resources" / "defs" @pytest.fixture() def registry(): return DefRegistry.load(DEFS_DIR) @pytest.fixture() def library(): return CharacterLibrary(sqlite3.connect(":memory:")) def build_character(registry) -> Character: character = Character( entity_id="ignored-on-save", # library keys on (account_id, character_id), not entity_id name="Vex", species_id="reptilian_quad", gender="none", blood_percentage=88.0, strength=14, body_parts=[ BodyPart( "torso", "reptilian_torso", integrity=70.0, ailments=[Ailment("poisoned", "Poisoned", severity=0.3, progress=12.0)], organs=[Organ("reptilian_heart", integrity=60.0)], ), ], default_body_parts=registry.new_default_body_parts("reptilian_quad"), clothing=[ClothingPiece("torso", "leather_jacket")], default_clothing=[], held_items=[HeldItem("right_hand", "chronowand", attachments=["scope_basic"])], implants=["chronoimplant"], mental_stats={"insanity": 4.0}, ) character.bio["stealth"] = 5 backpack = Inventory(4, 4) backpack.place(ItemInstance("rope-1", "sandwich"), registry.get_item_def("sandwich"), 0, 0) character.clothing[0] = ClothingPiece("torso", "backpack_basic") character.clothing[0].inventory = backpack character.inventory = Inventory(6, 5) character.inventory.place(ItemInstance("staff-1", "staff_oak"), registry.get_item_def("staff_oak"), 0, 0) return character def test_save_and_load_roundtrip_preserves_identity_and_species(registry, library): character = build_character(registry) library.save_character("account-1", "char-1", character) loaded = library.load_character("account-1", "char-1", registry) assert loaded is not None assert loaded.name == "Vex" assert loaded.species_id == "reptilian_quad" assert loaded.gender == "none" assert loaded.blood_percentage == pytest.approx(88.0) assert loaded.strength == 14 assert loaded.position is None # portable template, not tied to any world def test_roundtrip_preserves_body_parts_ailments_and_organs(registry, library): character = build_character(registry) library.save_character("account-1", "char-1", character) loaded = library.load_character("account-1", "char-1", registry) torso = loaded.get_body_part("torso") assert torso.part_def_id == "reptilian_torso" assert torso.integrity == pytest.approx(70.0) assert [a.id for a in torso.ailments] == ["poisoned"] assert torso.ailments[0].progress == pytest.approx(12.0) heart = next(o for o in torso.organs if o.organ_def_id == "reptilian_heart") assert heart.integrity == pytest.approx(60.0) # default (non-overridden) parts and their organs came back too head = loaded.get_body_part("head") assert head.part_def_id == "reptilian_head" assert {o.organ_def_id for o in head.organs} == {"reptilian_brain"} def test_roundtrip_preserves_clothing_and_its_granted_inventory(registry, library): character = build_character(registry) library.save_character("account-1", "char-1", character) loaded = library.load_character("account-1", "char-1", registry) backpack = loaded.get_clothing("torso") assert backpack.item_def_id == "backpack_basic" assert backpack.inventory is not None assert {p.item.item_id for p in backpack.inventory.items()} == {"rope-1"} def test_roundtrip_preserves_held_items_and_attachments(registry, library): character = build_character(registry) library.save_character("account-1", "char-1", character) loaded = library.load_character("account-1", "char-1", registry) held = loaded.get_held_item("right_hand") assert held.item_def_id == "chronowand" assert held.attachments == ["scope_basic"] def test_roundtrip_preserves_implants(registry, library): character = build_character(registry) library.save_character("account-1", "char-1", character) loaded = library.load_character("account-1", "char-1", registry) assert loaded.implants == ["chronoimplant"] def test_roundtrip_preserves_bio_and_mental_stats(registry, library): character = build_character(registry) library.save_character("account-1", "char-1", character) loaded = library.load_character("account-1", "char-1", registry) assert loaded.bio["stealth"] == 5 assert loaded.mental_stats == {"insanity": 4.0} def test_roundtrip_preserves_top_level_inventory(registry, library): character = build_character(registry) library.save_character("account-1", "char-1", character) loaded = library.load_character("account-1", "char-1", registry) placed = {p.item.item_id: (p.x, p.y) for p in loaded.inventory.items()} assert placed == {"staff-1": (0, 0)} def test_load_character_returns_none_when_not_found(registry, library): assert library.load_character("account-1", "no-such-char", registry) is None def test_list_characters_returns_id_name_species_for_an_account(registry, library): library.save_character("account-1", "char-1", build_character(registry)) other = build_character(registry) other.name = "Kess" library.save_character("account-1", "char-2", other) library.save_character("account-2", "char-3", build_character(registry)) # different account listed = library.list_characters("account-1") assert {(cid, name) for cid, name, species in listed if species} == {("char-1", "Vex"), ("char-2", "Kess")} def test_delete_character_removes_it_and_leaves_others(registry, library): library.save_character("account-1", "char-1", build_character(registry)) library.save_character("account-1", "char-2", build_character(registry)) library.delete_character("account-1", "char-1") assert library.load_character("account-1", "char-1", registry) is None assert library.load_character("account-1", "char-2", registry) is not None def test_saving_twice_under_the_same_id_overwrites_not_duplicates(registry, library): character = build_character(registry) library.save_character("account-1", "char-1", character) character.blood_percentage = 50.0 library.save_character("account-1", "char-1", character) loaded = library.load_character("account-1", "char-1", registry) assert loaded.blood_percentage == pytest.approx(50.0) assert len(library.list_characters("account-1")) == 1 def test_same_character_id_isolated_between_accounts(registry, library): a = build_character(registry) a.name = "Account A's Vex" b = build_character(registry) b.name = "Account B's Vex" library.save_character("account-a", "char-1", a) library.save_character("account-b", "char-1", b) assert library.load_character("account-a", "char-1", registry).name == "Account A's Vex" assert library.load_character("account-b", "char-1", registry).name == "Account B's Vex"