Mi2dRPGamEng/tests/test_falling.py

211 lines
7.4 KiB
Python

from pathlib import Path
import pytest
from engine.character import Character
from engine.defs import DefRegistry
from resources.logic.falling import (
ACROBATICS_CHECK_MIN_FALL,
CHECK_CLEAN_LANDING_DC,
CHECK_TENDON_DC,
LEG_SLOTS,
LOW_FALL_SPRAIN_CHANCE,
SEVERE_BROKEN_BONE_CHANCE,
SEVERE_FALL_HEIGHT,
SEVERE_FALL_HEIGHT_SKILLED,
apply_fall_damage,
)
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 make_hydraulibot(registry, **kwargs) -> Character:
return Character(species_id="hydraulibot", default_body_parts=registry.new_default_body_parts("hydraulibot"), **kwargs)
def make_motorbot(registry, **kwargs) -> Character:
return Character(species_id="motorbot", default_body_parts=registry.new_default_body_parts("motorbot"), **kwargs)
class FixedRng:
"""A random.Random look-alike with independently-fixed outputs for every method
apply_fall_damage/roll_skill_check might call.
"""
def __init__(self, randint_value: int = 10, choice_index: int = 0, uniform_value: float = 0.5):
self.randint_value = randint_value
self.choice_index = choice_index
self.uniform_value = uniform_value
def randint(self, a, b):
return self.randint_value
def choice(self, seq):
return seq[self.choice_index]
def uniform(self, a, b):
return a + (b - a) * self.uniform_value
def any_leg_has(character: Character, ailment_id: str) -> bool:
return any(
any(a.id == ailment_id for a in part.ailments)
for slot in LEG_SLOTS
if (part := character.get_body_part(slot)) is not None
)
# --- height 0: never fires -----------------------------------------------------------------
def test_no_fall_no_ailment(registry):
human = make_human(registry)
assert apply_fall_damage(human, registry, 0, FixedRng()) is None
# --- heights 1-2: flat rare chance, sprain only --------------------------------------------
def test_low_fall_below_chance_threshold_causes_a_sprain(registry):
human = make_human(registry)
rng = FixedRng(uniform_value=(LOW_FALL_SPRAIN_CHANCE / 2) / 1.0) # well under the chance
result = apply_fall_damage(human, registry, 1, rng)
assert result == "sprained_ankle"
assert any_leg_has(human, "sprained_ankle")
def test_low_fall_above_chance_threshold_causes_nothing(registry):
human = make_human(registry)
rng = FixedRng(uniform_value=0.99) # well over the chance
assert apply_fall_damage(human, registry, 2, rng) is None
# --- heights 3+ (below severe threshold): acrobatics check tiers ----------------------------
def test_acrobatics_check_clean_landing(registry):
human = make_human(registry)
human.bio["acrobatics"] = 0
rng = FixedRng(randint_value=CHECK_CLEAN_LANDING_DC)
assert apply_fall_damage(human, registry, ACROBATICS_CHECK_MIN_FALL, rng) is None
def test_acrobatics_check_mid_roll_causes_ripped_tendon(registry):
human = make_human(registry)
human.bio["acrobatics"] = 0
rng = FixedRng(randint_value=CHECK_TENDON_DC + 1) # lands in the tendon band
result = apply_fall_damage(human, registry, ACROBATICS_CHECK_MIN_FALL, rng)
assert result == "ripped_tendon"
def test_acrobatics_check_low_roll_causes_broken_bone(registry):
human = make_human(registry)
human.bio["acrobatics"] = 0
rng = FixedRng(randint_value=1)
result = apply_fall_damage(human, registry, ACROBATICS_CHECK_MIN_FALL, rng)
assert result == "broken_bone"
# --- severe heights: no check, flat 90/10 roll ----------------------------------------------
def test_severe_fall_mostly_breaks_a_bone(registry):
human = make_human(registry)
human.bio["acrobatics"] = 0
rng = FixedRng(uniform_value=(SEVERE_BROKEN_BONE_CHANCE / 2))
result = apply_fall_damage(human, registry, SEVERE_FALL_HEIGHT, rng)
assert result == "broken_bone"
def test_severe_fall_sometimes_rips_a_tendon_instead(registry):
human = make_human(registry)
human.bio["acrobatics"] = 0
rng = FixedRng(uniform_value=0.99) # above SEVERE_BROKEN_BONE_CHANCE
result = apply_fall_damage(human, registry, SEVERE_FALL_HEIGHT, rng)
assert result == "ripped_tendon"
def test_severe_fall_never_results_in_just_a_sprain(registry):
human = make_human(registry)
human.bio["acrobatics"] = 0
for uniform_value in (0.0, 0.5, 0.99):
result = apply_fall_damage(human, registry, SEVERE_FALL_HEIGHT, FixedRng(uniform_value=uniform_value))
assert result in ("broken_bone", "ripped_tendon")
def test_skilled_acrobat_gets_the_extended_threshold(registry):
# a skilled acrobat isn't subject to the flat severe roll until height 6, not 5 - height 5
# should still go through the tiered check instead
human = make_human(registry)
human.bio["acrobatics"] = 3 # 60% of DEFAULT_MAX_SKILL_LEVEL (5)
rng = FixedRng(randint_value=CHECK_CLEAN_LANDING_DC)
assert apply_fall_damage(human, registry, SEVERE_FALL_HEIGHT, rng) is None
assert SEVERE_FALL_HEIGHT < SEVERE_FALL_HEIGHT_SKILLED
def test_skilled_acrobat_still_hits_the_severe_roll_one_level_higher(registry):
human = make_human(registry)
human.bio["acrobatics"] = 3
rng = FixedRng(uniform_value=0.0)
result = apply_fall_damage(human, registry, SEVERE_FALL_HEIGHT_SKILLED, rng)
assert result == "broken_bone"
# --- synthetic renames -----------------------------------------------------------------------
def test_hydraulibot_gets_renamed_ailments(registry):
hydraulibot = make_hydraulibot(registry)
rng = FixedRng(uniform_value=(SEVERE_BROKEN_BONE_CHANCE / 2))
result = apply_fall_damage(hydraulibot, registry, SEVERE_FALL_HEIGHT, rng)
assert result == "broken_frame_piece"
assert any_leg_has(hydraulibot, "broken_frame_piece")
assert not any_leg_has(hydraulibot, "broken_bone")
def test_hydraulibot_ripped_tendon_becomes_popped_fluid_hose(registry):
hydraulibot = make_hydraulibot(registry)
rng = FixedRng(uniform_value=0.99)
result = apply_fall_damage(hydraulibot, registry, SEVERE_FALL_HEIGHT, rng)
assert result == "popped_fluid_hose"
def test_motorbot_also_gets_renamed_ailments(registry):
motorbot = make_motorbot(registry)
rng = FixedRng(uniform_value=(SEVERE_BROKEN_BONE_CHANCE / 2))
result = apply_fall_damage(motorbot, registry, SEVERE_FALL_HEIGHT, rng)
assert result == "broken_frame_piece"
# --- no legs: no-op ---------------------------------------------------------------------------
def test_no_legs_no_ailment(registry):
legless = Character(species_id="human", default_body_parts=[])
rng = FixedRng(uniform_value=0.0)
assert apply_fall_damage(legless, registry, SEVERE_FALL_HEIGHT, rng) is None
# --- refreshing an existing ailment doesn't duplicate it --------------------------------------
def test_repeated_falls_refresh_rather_than_duplicate(registry):
human = make_human(registry)
rng = FixedRng(uniform_value=(SEVERE_BROKEN_BONE_CHANCE / 2))
apply_fall_damage(human, registry, SEVERE_FALL_HEIGHT, rng)
apply_fall_damage(human, registry, SEVERE_FALL_HEIGHT, rng)
matching_parts = [
part for slot in LEG_SLOTS if (part := human.get_body_part(slot)) is not None
for a in part.ailments if a.id == "broken_bone"
]
assert len(matching_parts) == 1