211 lines
7.5 KiB
Python
211 lines
7.5 KiB
Python
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
from engine.character import Ailment, BodyPart, Character
|
|
from engine.defs import DefRegistry
|
|
from resources.logic.combat import (
|
|
FRACTURE_ROLL_THRESHOLD,
|
|
IMPACT_TRAUMA_ROLL_THRESHOLD,
|
|
apply_combat_ailment,
|
|
combat_ailment_for_roll,
|
|
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:
|
|
def __init__(self, roll: int):
|
|
self._roll = roll
|
|
|
|
def randint(self, _lo, _hi):
|
|
return self._roll
|
|
|
|
def choice(self, seq):
|
|
return seq[0]
|
|
|
|
|
|
# --- combat_ailment_for_roll: pure roll -> ailment tiers -------------------------------------
|
|
|
|
|
|
def test_low_roll_causes_no_ailment():
|
|
assert combat_ailment_for_roll(IMPACT_TRAUMA_ROLL_THRESHOLD - 1, "torso") is None
|
|
|
|
|
|
def test_solid_roll_causes_impact_trauma():
|
|
assert combat_ailment_for_roll(IMPACT_TRAUMA_ROLL_THRESHOLD, "torso") == "impact_trauma"
|
|
assert combat_ailment_for_roll(FRACTURE_ROLL_THRESHOLD - 1, "left_arm") == "impact_trauma"
|
|
|
|
|
|
def test_near_perfect_roll_causes_a_fracture_on_a_non_head_slot():
|
|
assert combat_ailment_for_roll(FRACTURE_ROLL_THRESHOLD, "left_arm") == "fracture"
|
|
assert combat_ailment_for_roll(20, "torso") == "fracture"
|
|
|
|
|
|
def test_near_perfect_roll_on_the_head_causes_brain_trauma_instead():
|
|
assert combat_ailment_for_roll(FRACTURE_ROLL_THRESHOLD, "head") == "brain_trauma"
|
|
assert combat_ailment_for_roll(20, "head") == "brain_trauma"
|
|
|
|
|
|
# --- apply_combat_ailment: add vs. refresh -----------------------------------------------------
|
|
|
|
|
|
def test_apply_combat_ailment_adds_a_new_ailment():
|
|
part = BodyPart("torso", "human_torso")
|
|
ailment = apply_combat_ailment(part, "fracture")
|
|
assert part.ailments == [ailment]
|
|
assert ailment.id == "fracture"
|
|
assert ailment.severity == pytest.approx(0.8)
|
|
|
|
|
|
def test_apply_combat_ailment_does_not_duplicate_the_same_ailment():
|
|
part = BodyPart("torso", "human_torso")
|
|
apply_combat_ailment(part, "impact_trauma")
|
|
apply_combat_ailment(part, "impact_trauma")
|
|
assert len(part.ailments) == 1
|
|
|
|
|
|
def test_apply_combat_ailment_upgrades_severity_but_never_downgrades():
|
|
part = BodyPart("torso", "human_torso")
|
|
part.ailments.append(Ailment(id="fracture", name="Fracture", severity=0.2)) # e.g. healing
|
|
apply_combat_ailment(part, "fracture")
|
|
assert part.ailments[0].severity == pytest.approx(0.8) # took the worse (combat) severity
|
|
|
|
|
|
# --- integration: perform_attack actually inflicts ailments on a hit ---------------------------
|
|
|
|
|
|
def test_low_precision_hit_damages_but_inflicts_no_ailment(registry):
|
|
attacker = make_human(registry)
|
|
attacker.bio["athletics"] = 10
|
|
defender = make_human(registry)
|
|
|
|
result = perform_attack(attacker, defender, registry, target_slot="torso", rng=FixedRng(roll=12))
|
|
assert result.hit is True
|
|
torso = defender.get_body_part("torso")
|
|
assert torso.integrity < 100.0
|
|
assert torso.ailments == []
|
|
|
|
|
|
def test_solid_hit_inflicts_impact_trauma(registry):
|
|
attacker = make_human(registry)
|
|
attacker.bio["athletics"] = 10
|
|
defender = make_human(registry)
|
|
|
|
result = perform_attack(attacker, defender, registry, target_slot="torso", rng=FixedRng(roll=IMPACT_TRAUMA_ROLL_THRESHOLD))
|
|
assert result.hit is True
|
|
torso = defender.get_body_part("torso")
|
|
assert [a.id for a in torso.ailments] == ["impact_trauma"]
|
|
|
|
|
|
def test_near_perfect_hit_inflicts_a_fracture(registry):
|
|
attacker = make_human(registry)
|
|
attacker.bio["athletics"] = 10
|
|
defender = make_human(registry)
|
|
|
|
result = perform_attack(attacker, defender, registry, target_slot="left_arm", rng=FixedRng(roll=FRACTURE_ROLL_THRESHOLD))
|
|
assert result.hit is True
|
|
arm = defender.get_body_part("left_arm")
|
|
assert [a.id for a in arm.ailments] == ["fracture"]
|
|
|
|
|
|
def test_near_perfect_hit_on_the_head_inflicts_brain_trauma(registry):
|
|
attacker = make_human(registry)
|
|
attacker.bio["athletics"] = 10
|
|
defender = make_human(registry)
|
|
|
|
result = perform_attack(attacker, defender, registry, target_slot="head", rng=FixedRng(roll=20))
|
|
assert result.hit is True
|
|
head = defender.get_body_part("head")
|
|
assert [a.id for a in head.ailments] == ["brain_trauma"]
|
|
|
|
|
|
def test_a_miss_inflicts_no_ailment(registry):
|
|
attacker = make_human(registry) # athletics 0
|
|
defender = make_human(registry)
|
|
result = perform_attack(attacker, defender, registry, target_slot="head", rng=FixedRng(roll=1))
|
|
assert result.hit is False
|
|
assert defender.get_body_part("head").ailments == []
|
|
|
|
|
|
# --- ailment_penalty / total_check_penalty ------------------------------------------------------
|
|
|
|
|
|
def test_ailment_penalty_zero_when_no_ailments(registry):
|
|
character = make_human(registry)
|
|
assert character.ailment_penalty("strength", registry) == 0.0
|
|
|
|
|
|
def test_ailment_penalty_zero_for_an_ailment_with_no_matching_def(registry):
|
|
character = make_human(registry)
|
|
torso = character.get_or_create_body_part_override("torso")
|
|
torso.ailments.append(Ailment(id="frostbite", name="Frostbite", severity=1.0)) # no AilmentDef exists for this
|
|
assert character.ailment_penalty("strength", registry) == 0.0
|
|
|
|
|
|
def test_ailment_penalty_rises_with_a_fracture(registry):
|
|
character = make_human(registry)
|
|
arm = character.get_or_create_body_part_override("left_arm")
|
|
apply_combat_ailment(arm, "fracture")
|
|
penalty = character.ailment_penalty("strength", registry)
|
|
assert penalty > 0.0
|
|
|
|
|
|
def test_ailment_penalty_zero_for_an_unrelated_check(registry):
|
|
character = make_human(registry)
|
|
torso = character.get_or_create_body_part_override("torso")
|
|
apply_combat_ailment(torso, "fracture") # fracture doesn't weight "insight"
|
|
assert character.ailment_penalty("insight", registry) == 0.0
|
|
|
|
|
|
def test_brain_trauma_hits_insight_and_perception_hard(registry):
|
|
character = make_human(registry)
|
|
head = character.get_or_create_body_part_override("head")
|
|
apply_combat_ailment(head, "brain_trauma")
|
|
assert character.ailment_penalty("insight", registry) == pytest.approx(0.9)
|
|
|
|
|
|
def test_total_check_penalty_combines_skill_organ_and_ailment_damage(registry):
|
|
character = make_human(registry)
|
|
character.body_parts = [BodyPart("left_arm", "human_left_arm", integrity=60.0)]
|
|
arm = character.get_or_create_body_part_override("left_arm")
|
|
apply_combat_ailment(arm, "impact_trauma")
|
|
|
|
combined = character.total_check_penalty("athletics", registry)
|
|
body_only = character.skill_penalty("athletics", registry)
|
|
ailment_only = character.ailment_penalty("athletics", registry)
|
|
assert combined > body_only
|
|
assert combined > ailment_only
|
|
assert combined <= 1.0
|
|
|
|
|
|
def test_total_check_penalty_unaffected_when_no_ailments_present(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)
|
|
)
|
|
|
|
|
|
# --- registry loading -----------------------------------------------------------------------
|
|
|
|
|
|
def test_ailment_defs_load_with_check_weights(registry):
|
|
fracture = registry.get_ailment_def("fracture")
|
|
assert fracture is not None
|
|
assert fracture.check_weights["strength"] == pytest.approx(0.5)
|
|
|
|
|
|
def test_get_ailment_def_returns_none_for_unknown_id(registry):
|
|
assert registry.get_ailment_def("not_a_real_ailment") is None
|