from pathlib import Path import pytest from engine.character import Ailment, BodyPart, Character from engine.defs import DefRegistry from resources.logic.organs import apply_organ_interactions_tick DEFS_DIR = Path(__file__).resolve().parent.parent / "resources" / "defs" @pytest.fixture() def registry(): return DefRegistry.load(DEFS_DIR) def make_human(registry, **kwargs) -> Character: return Character(species_id="human", default_body_parts=registry.new_default_body_parts("human"), **kwargs) # --- def loading + auto-attachment --------------------------------------------------------- def test_organ_defs_load_with_check_weights_and_affects_organs(registry): heart = registry.get_organ_def("human_heart") assert heart.host_slot == "torso" assert heart.check_weights["strength"] == pytest.approx(0.6) assert heart.affects_organs["human_kidneys"] == pytest.approx(0.05) def test_new_default_body_parts_auto_attaches_organs_by_host_slot(registry): parts = registry.new_default_body_parts("human") torso = next(p for p in parts if p.slot == "torso") organ_ids = {o.organ_def_id for o in torso.organs} assert organ_ids == {"human_heart", "human_lungs", "human_liver", "human_kidneys"} head = next(p for p in parts if p.slot == "head") assert {o.organ_def_id for o in head.organs} == {"human_brain", "human_eyes", "human_ears"} arm = next(p for p in parts if p.slot == "left_arm") assert arm.organs == [] def test_new_default_body_parts_organs_are_independent_per_call(registry): a = registry.new_default_body_parts("human") b = registry.new_default_body_parts("human") torso_a = next(p for p in a if p.slot == "torso") torso_b = next(p for p in b if p.slot == "torso") heart_a = next(o for o in torso_a.organs if o.organ_def_id == "human_heart") heart_b = next(o for o in torso_b.organs if o.organ_def_id == "human_heart") heart_a.integrity = 10.0 assert heart_b.integrity == 100.0 # --- get_or_create_body_part_override preserves organs/ailments ---------------------------- def test_override_creation_preserves_default_organs(registry): character = make_human(registry) # damage integrity for an unrelated reason - this must not silently drop the organs override = character.get_or_create_body_part_override("torso") assert {o.organ_def_id for o in override.organs} == {"human_heart", "human_lungs", "human_liver", "human_kidneys"} def test_override_creation_preserves_existing_ailments(registry): character = make_human(registry) default_torso = character.get_body_part("torso") default_torso.ailments.append(Ailment(id="preexisting", name="Preexisting")) override = character.get_or_create_body_part_override("torso") assert any(a.id == "preexisting" for a in override.ailments) # --- organ_penalty / total_check_penalty ---------------------------------------------------- def test_organ_penalty_zero_when_healthy(registry): character = make_human(registry) assert character.organ_penalty("strength", registry) == 0.0 def test_organ_penalty_rises_with_heart_damage(registry): character = make_human(registry) torso = character.get_or_create_body_part_override("torso") heart = next(o for o in torso.organs if o.organ_def_id == "human_heart") heart.integrity = 50.0 penalty = character.organ_penalty("strength", registry) assert penalty > 0.0 def test_organ_penalty_zero_for_unrelated_check(registry): character = make_human(registry) torso = character.get_or_create_body_part_override("torso") heart = next(o for o in torso.organs if o.organ_def_id == "human_heart") heart.integrity = 0.0 heart.removed = True # heart doesn't weight "perception" at all assert character.organ_penalty("perception", registry) == 0.0 def test_total_check_penalty_equals_skill_penalty_when_organs_healthy(registry): character = make_human(registry) character.body_parts = [BodyPart("left_leg", "human_left_leg", integrity=20.0)] assert character.total_check_penalty("acrobatics", registry) == pytest.approx( character.skill_penalty("acrobatics", registry) ) def test_total_check_penalty_combines_body_part_and_organ_damage(registry): character = make_human(registry) character.body_parts = [BodyPart("left_arm", "human_left_arm", integrity=0.0, removed=True)] torso = character.get_or_create_body_part_override("torso") lungs = next(o for o in torso.organs if o.organ_def_id == "human_lungs") lungs.integrity = 40.0 # damaged but not removed, so neither penalty alone saturates at 1.0 combined = character.total_check_penalty("athletics", registry) body_only = character.skill_penalty("athletics", registry) organ_only = character.organ_penalty("athletics", registry) assert combined > body_only assert combined > organ_only assert combined <= 1.0 # --- effective_strength / effective_speed ---------------------------------------------------- def test_effective_strength_reduced_by_damaged_heart(registry): character = make_human(registry, strength=20) healthy = character.effective_strength(registry) torso = character.get_or_create_body_part_override("torso") heart = next(o for o in torso.organs if o.organ_def_id == "human_heart") heart.integrity = 20.0 damaged = character.effective_strength(registry) assert damaged < healthy def test_effective_speed_reduced_by_damaged_heart_and_lungs(registry): character = make_human(registry) healthy_speed = character.effective_speed(30.0, registry) torso = character.get_or_create_body_part_override("torso") for organ_id in ("human_heart", "human_lungs"): organ = next(o for o in torso.organs if o.organ_def_id == organ_id) organ.integrity = 0.0 organ.removed = True damaged_speed = character.effective_speed(30.0, registry) assert damaged_speed < healthy_speed # --- organ-to-organ interaction tick ---------------------------------------------------------- def test_apply_organ_interactions_tick_damages_kidneys_from_a_weak_heart(registry): character = make_human(registry) torso = character.get_or_create_body_part_override("torso") heart = next(o for o in torso.organs if o.organ_def_id == "human_heart") heart.integrity = 0.0 # fully damaged -> damage_fraction = 1.0 apply_organ_interactions_tick(character, registry, ticks=10) kidneys = next(o for o in character.get_body_part("torso").organs if o.organ_def_id == "human_kidneys") # rate 0.05 * damage_fraction 1.0 * 10 ticks = 0.5 damage assert kidneys.integrity == pytest.approx(99.5) def test_apply_organ_interactions_tick_no_op_when_organs_healthy(registry): character = make_human(registry) apply_organ_interactions_tick(character, registry, ticks=10) kidneys = next(o for o in character.get_body_part("torso").organs if o.organ_def_id == "human_kidneys") assert kidneys.integrity == 100.0 def test_apply_organ_interactions_tick_zero_ticks_is_a_no_op(registry): character = make_human(registry) torso = character.get_or_create_body_part_override("torso") heart = next(o for o in torso.organs if o.organ_def_id == "human_heart") heart.integrity = 0.0 apply_organ_interactions_tick(character, registry, ticks=0) kidneys = next(o for o in character.get_body_part("torso").organs if o.organ_def_id == "human_kidneys") assert kidneys.integrity == 100.0 def test_apply_organ_interactions_tick_does_not_cascade_within_one_call(registry): character = make_human(registry) torso = character.get_or_create_body_part_override("torso") heart = next(o for o in torso.organs if o.organ_def_id == "human_heart") heart.integrity = 0.0 # heart affects kidneys, but kidneys don't affect anything themselves apply_organ_interactions_tick(character, registry, ticks=1) # sanity: no exception, no unexpected organ (e.g. liver) got touched by a cascade liver = next(o for o in character.get_body_part("torso").organs if o.organ_def_id == "human_liver") assert liver.integrity == 100.0 def test_removed_organ_no_longer_contributes_to_further_interactions(registry): character = make_human(registry) torso = character.get_or_create_body_part_override("torso") heart = next(o for o in torso.organs if o.organ_def_id == "human_heart") heart.integrity = 0.0 heart.removed = True apply_organ_interactions_tick(character, registry, ticks=100) kidneys = next(o for o in character.get_body_part("torso").organs if o.organ_def_id == "human_kidneys") assert kidneys.integrity == 100.0 # removed heart is skipped as a source entirely # --- find_organ_by_role ----------------------------------------------------------------------- def test_find_organ_by_role_finds_the_heart(registry): character = make_human(registry) found = character.find_organ_by_role("heart", registry) assert found is not None body_part, organ = found assert body_part.slot == "torso" assert organ.organ_def_id == "human_heart" def test_find_organ_by_role_works_across_bespoke_species_organs(registry): hydraulibot = Character(species_id="hydraulibot", default_body_parts=registry.new_default_body_parts("hydraulibot")) found = hydraulibot.find_organ_by_role("heart", registry) assert found is not None _body_part, organ = found assert organ.organ_def_id == "hydraulibot_power_cell" # the robot's functional heart-equivalent def test_find_organ_by_role_works_for_motorbots_main_motor(registry): motorbot = Character(species_id="motorbot", default_body_parts=registry.new_default_body_parts("motorbot")) found = motorbot.find_organ_by_role("heart", registry) assert found is not None _body_part, organ = found assert organ.organ_def_id == "motorbot_main_motor" def test_find_organ_by_role_none_when_no_organs_at_all(registry): character = Character(default_body_parts=[BodyPart("torso", "human_torso")]) assert character.find_organ_by_role("heart", registry) is None def test_find_organ_by_role_none_for_unknown_role(registry): character = make_human(registry) assert character.find_organ_by_role("gizzard", registry) is None def test_find_organ_by_role_returns_the_mutable_override(registry): character = make_human(registry) found = character.find_organ_by_role("heart", registry) assert found is not None _body_part, organ = found organ.integrity = 10.0 # re-fetching sees the same mutated instance, not a fresh default copy torso = character.get_body_part("torso") heart = next(o for o in torso.organs if o.organ_def_id == "human_heart") assert heart.integrity == pytest.approx(10.0)