131 lines
4.6 KiB
Python
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)
|