from pathlib import Path import pytest from engine.character import BodyPart, Character from engine.defs import DefRegistry from resources.logic.combat import BASE_DIFFICULTY, apply_body_part_damage, choose_hit_slot, perform_attack 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) class FixedRng: """Deterministic stand-in for random.Random: fixed die roll, fixed choice.""" def __init__(self, roll: int, choice_value=None): self._roll = roll self._choice_value = choice_value def randint(self, _lo, _hi): return self._roll def choice(self, seq): return self._choice_value if self._choice_value is not None else seq[0] def test_high_roll_and_skill_hits_and_damages_a_body_part(registry): attacker = make_human(registry) attacker.bio["athletics"] = 10 defender = make_human(registry) result = perform_attack(attacker, defender, registry, rng=FixedRng(roll=15, choice_value="torso")) assert result.hit is True assert result.skill_id == "athletics" assert result.damage > 0 assert result.target_slot is not None hit_part = defender.get_body_part(result.target_slot) assert hit_part.integrity == pytest.approx(100.0 - result.damage) def test_low_roll_and_no_skill_misses(registry): attacker = make_human(registry) # bio athletics defaults to 0 defender = make_human(registry) result = perform_attack(attacker, defender, registry, rng=FixedRng(roll=1)) assert result.hit is False assert result.damage == 0.0 assert result.target_slot is None # nothing was damaged assert defender.get_body_part("torso").integrity == pytest.approx(100.0) def test_weapon_bonus_increases_damage_on_hit(registry): attacker = make_human(registry) attacker.bio["athletics"] = 5 defender = make_human(registry) weapon = registry.get_item_def("staff_oak") unarmed = perform_attack(attacker, defender, registry, rng=FixedRng(roll=12, choice_value="torso")) defender2 = make_human(registry) armed = perform_attack(attacker, defender2, registry, weapon_def=weapon, rng=FixedRng(roll=12, choice_value="torso")) assert armed.damage > unarmed.damage assert armed.weapon_bonus == pytest.approx(weapon.weapon_damage) def test_skill_penalty_from_missing_arm_reduces_effective_skill(registry): attacker = make_human(registry) attacker.bio["athletics"] = 10 attacker.body_parts = [BodyPart("left_arm", "human_left_arm", removed=True)] defender = make_human(registry) result = perform_attack(attacker, defender, registry, rng=FixedRng(roll=10, choice_value="torso")) assert result.effective_skill < 10.0 def test_apply_body_part_damage_marks_removed_at_zero_integrity(registry): character = make_human(registry) apply_body_part_damage(character, "left_arm", 100.0) part = next(p for p in character.body_parts if p.slot == "left_arm") assert part.integrity == 0.0 assert part.removed is True assert character.get_body_part("left_arm") is None def test_apply_body_part_damage_on_unknown_slot_returns_none(registry): character = make_human(registry) assert apply_body_part_damage(character, "tail", 10.0) is None def test_base_difficulty_is_reasonable_default(): assert BASE_DIFFICULTY == 10 def test_choose_hit_slot_low_precision_hits_easiest_target(registry): defender = make_human(registry) assert choose_hit_slot(defender, precision_check=0) == "torso" def test_choose_hit_slot_high_precision_reaches_the_hardest_target(registry): defender = make_human(registry) assert choose_hit_slot(defender, precision_check=1000) == "head" def test_choose_hit_slot_skips_missing_body_parts(registry): defender = make_human(registry, body_parts=[BodyPart("head", "human_head", removed=True)]) # even at max precision, a missing head can never be the target assert choose_hit_slot(defender, precision_check=1000) != "head" def test_choose_hit_slot_none_when_no_body_parts_at_all(registry): defender = Character() assert choose_hit_slot(defender, precision_check=50) is None def test_higher_precision_check_reaches_further_down_the_target_list(registry): defender = make_human(registry) low = choose_hit_slot(defender, precision_check=0) high = choose_hit_slot(defender, precision_check=20) from resources.logic.combat import BODY_PART_DIFFICULTY_ORDER assert BODY_PART_DIFFICULTY_ORDER.index(high) >= BODY_PART_DIFFICULTY_ORDER.index(low)