211 lines
7.4 KiB
Python
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
|