Mi2dRPGamEng/tests/test_combat.py

131 lines
4.6 KiB
Python

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)