Mi2dRPGamEng/tests/test_launch_lightning.py

354 lines
15 KiB
Python

import random
from pathlib import Path
import pytest
from engine.character import BodyPart, Character
from engine.defs import DefRegistry
from engine.entity import EntityPosition
from engine.map import Map
from engine.tile import Tile, TileLayerInstance
from engine.world import World
from resources.logic.abilities import (
DEFAULT_LAUNCH_LIGHTNING_RANGE,
HEART_ORGAN_DAMAGE,
HEART_PALPITATIONS_SEVERITY,
SHORTCIRCUIT_TOTAL_ORGAN_DAMAGE,
cast_launch_lightning,
)
from resources.logic.actions import ABILITY_COOLDOWNS, activate_hand, activate_implant
from resources.logic.organs import apply_organ_interactions_tick
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_world(registry, width=15, height=15):
field = Map("field", width, height, 1)
for x in range(width):
for y in range(height):
field.set_tile(x, y, 0, Tile(flooring=TileLayerInstance("grass")))
world = World(registry=registry)
world.add_map(field)
return world
# --- cast_launch_lightning: targeting / range ------------------------------------------------
def test_hits_a_target_within_range(registry):
world = make_world(registry)
caster = make_human(registry, position=EntityPosition("field", 5, 5, 0))
target = make_human(registry, position=EntityPosition("field", 5, 8, 0))
world.add_entity(caster)
world.add_entity(target)
hit = cast_launch_lightning(caster, world, now=0.0, aim_direction=(0, 1, 0))
assert hit is target
def test_default_range_matches_the_documented_constant(registry):
assert DEFAULT_LAUNCH_LIGHTNING_RANGE == 8
def test_misses_a_target_beyond_range(registry):
world = make_world(registry)
caster = make_human(registry, position=EntityPosition("field", 0, 0, 0))
target = make_human(registry, position=EntityPosition("field", 0, 10, 0)) # 10 tiles away
world.add_entity(caster)
world.add_entity(target)
hit = cast_launch_lightning(caster, world, now=0.0, aim_direction=(0, 1, 0), max_range=8)
assert hit is None
assert target.get_body_part("torso").ailments == []
def test_range_is_configurable_per_cast(registry):
world = make_world(registry)
caster = make_human(registry, position=EntityPosition("field", 0, 0, 0))
target = make_human(registry, position=EntityPosition("field", 0, 10, 0))
world.add_entity(caster)
world.add_entity(target)
hit = cast_launch_lightning(caster, world, now=0.0, aim_direction=(0, 1, 0), max_range=12)
assert hit is target
def test_no_target_in_the_aimed_direction_is_a_no_op(registry):
world = make_world(registry)
caster = make_human(registry, position=EntityPosition("field", 5, 5, 0))
world.add_entity(caster)
assert cast_launch_lightning(caster, world, now=0.0, aim_direction=(1, 0, 0)) is None
def test_a_wall_blocks_the_bolt(registry):
world = make_world(registry)
field = world.maps["field"]
field.get_tile(5, 6, 0).room = TileLayerInstance("wood_wall")
caster = make_human(registry, position=EntityPosition("field", 5, 5, 0))
target = make_human(registry, position=EntityPosition("field", 5, 8, 0))
world.add_entity(caster)
world.add_entity(target)
assert cast_launch_lightning(caster, world, now=0.0, aim_direction=(0, 1, 0)) is None
assert target.get_body_part("torso").ailments == []
def test_never_misses_regardless_of_skill_or_bio(registry):
# no skill check at all is performed - a bare, unskilled character still always hits
world = make_world(registry)
caster = make_human(registry, position=EntityPosition("field", 5, 5, 0))
target = make_human(registry, position=EntityPosition("field", 5, 6, 0))
world.add_entity(caster)
world.add_entity(target)
assert cast_launch_lightning(caster, world, now=0.0, aim_direction=(0, 1, 0)) is target
# --- burn ailments -----------------------------------------------------------------------------
def test_burns_roughly_half_the_body_parts(registry):
world = make_world(registry)
caster = make_human(registry, position=EntityPosition("field", 5, 5, 0))
target = make_human(registry, position=EntityPosition("field", 5, 6, 0))
world.add_entity(caster)
world.add_entity(target)
cast_launch_lightning(caster, world, now=0.0, aim_direction=(0, 1, 0))
burned = [p for p in target.body_parts if any(a.id == "burn" for a in p.ailments)]
assert 2 <= len(burned) <= 4 # ~half of the human's 6 default parts
for part in burned:
assert part.integrity < 100.0
# --- heart_palpitations + organ damage ----------------------------------------------------------
def test_inflicts_heart_palpitations_on_the_torso(registry):
world = make_world(registry)
caster = make_human(registry, position=EntityPosition("field", 5, 5, 0))
target = make_human(registry, position=EntityPosition("field", 5, 6, 0))
world.add_entity(caster)
world.add_entity(target)
cast_launch_lightning(caster, world, now=0.0, aim_direction=(0, 1, 0))
torso = target.get_body_part("torso")
palpitations = next(a for a in torso.ailments if a.id == "heart_palpitations")
assert palpitations.severity == pytest.approx(HEART_PALPITATIONS_SEVERITY)
def test_damages_the_heart_organ_directly(registry):
world = make_world(registry)
caster = make_human(registry, position=EntityPosition("field", 5, 5, 0))
target = make_human(registry, position=EntityPosition("field", 5, 6, 0))
world.add_entity(caster)
world.add_entity(target)
cast_launch_lightning(caster, world, now=0.0, aim_direction=(0, 1, 0))
torso = target.get_body_part("torso")
heart = next(o for o in torso.organs if o.organ_def_id == "human_heart")
assert heart.integrity == pytest.approx(100.0 - HEART_ORGAN_DAMAGE)
def test_heart_palpitations_weakens_strength_and_speed_checks(registry):
world = make_world(registry)
caster = make_human(registry, position=EntityPosition("field", 5, 5, 0))
target = make_human(registry, position=EntityPosition("field", 5, 6, 0))
world.add_entity(caster)
world.add_entity(target)
before = target.total_check_penalty("strength", registry)
cast_launch_lightning(caster, world, now=0.0, aim_direction=(0, 1, 0))
after = target.total_check_penalty("strength", registry)
assert after > before
def test_heart_damage_cascades_to_dependent_organs_via_organ_tick(registry):
world = make_world(registry)
caster = make_human(registry, position=EntityPosition("field", 5, 5, 0))
target = make_human(registry, position=EntityPosition("field", 5, 6, 0))
world.add_entity(caster)
world.add_entity(target)
cast_launch_lightning(caster, world, now=0.0, aim_direction=(0, 1, 0))
apply_organ_interactions_tick(target, registry, ticks=10)
kidneys = next(o for o in target.get_body_part("torso").organs if o.organ_def_id == "human_kidneys")
assert kidneys.integrity < 100.0 # the damaged heart affects_organs its way into the kidneys
def test_species_with_no_heart_equivalent_still_gets_burned(registry):
world = make_world(registry)
caster = make_human(registry, position=EntityPosition("field", 5, 5, 0))
target = Character(
position=EntityPosition("field", 5, 6, 0),
default_body_parts=[BodyPart("torso", "human_torso"), BodyPart("head", "human_head")],
)
world.add_entity(caster)
world.add_entity(target)
hit = cast_launch_lightning(caster, world, now=0.0, aim_direction=(0, 1, 0))
assert hit is target
assert any(any(a.id == "burn" for a in p.ailments) for p in target.body_parts)
assert not any(a.id == "heart_palpitations" for p in target.body_parts for a in p.ailments)
def test_recasting_refreshes_rather_than_stacks_heart_palpitations(registry):
world = make_world(registry)
caster = make_human(registry, position=EntityPosition("field", 5, 5, 0))
target = make_human(registry, position=EntityPosition("field", 5, 6, 0))
world.add_entity(caster)
world.add_entity(target)
cast_launch_lightning(caster, world, now=0.0, aim_direction=(0, 1, 0))
cast_launch_lightning(caster, world, now=100.0, aim_direction=(0, 1, 0))
torso = target.get_body_part("torso")
assert len([a for a in torso.ailments if a.id == "heart_palpitations"]) == 1
# --- item attachment + actions.py plumbing ------------------------------------------------------
def test_storm_rod_casts_launch_lightning_via_activate_hand(registry):
world = make_world(registry)
caster = make_human(registry, position=EntityPosition("field", 5, 5, 0))
target = make_human(registry, position=EntityPosition("field", 5, 6, 0))
caster.wield_item("right_hand", "storm_rod", registry)
world.add_entity(caster)
world.add_entity(target)
result = activate_hand(caster, world, registry, "right_hand", (0, 1, 0), now=0.0)
assert result is target
assert any(a.id == "burn" for p in target.body_parts for a in p.ailments)
def test_launch_lightning_respects_its_cooldown(registry):
world = make_world(registry)
caster = make_human(registry, position=EntityPosition("field", 5, 5, 0))
target = make_human(registry, position=EntityPosition("field", 5, 6, 0))
caster.wield_item("right_hand", "storm_rod", registry)
world.add_entity(caster)
world.add_entity(target)
activate_hand(caster, world, registry, "right_hand", (0, 1, 0), now=0.0)
assert caster.is_ability_ready("launch_lightning", now=1.0) is False
cooldown = ABILITY_COOLDOWNS["launch_lightning"]
assert caster.is_ability_ready("launch_lightning", now=cooldown + 0.1) is True
def test_time_warp_sphere_via_implant_still_works_after_aim_direction_plumbing(registry):
# regression check: activate_implant gained an aim_direction param for launch_lightning-
# style abilities, but a non-directional ability (time_warp_sphere) must be unaffected
world = make_world(registry)
caster = make_human(registry, position=EntityPosition("field", 5, 5, 0))
caster.install_implant("chronoimplant", registry)
world.add_entity(caster)
result = activate_implant(caster, world, registry, "chronoimplant", aim_direction=(1, 0, 0), now=0.0)
assert result is not None
assert len(world.time_warp_zones) == 1
# --- shortcircuit: synthetic-specific, randomized organ damage spread -------------------------
def make_hydraulibot(registry, **kwargs) -> Character:
return Character(species_id="hydraulibot", default_body_parts=registry.new_default_body_parts("hydraulibot"), **kwargs)
def test_shortcircuit_only_applies_to_synthetic_species(registry):
world = make_world(registry)
caster = make_human(registry, position=EntityPosition("field", 5, 5, 0))
target = make_human(registry, position=EntityPosition("field", 5, 6, 0))
world.add_entity(caster)
world.add_entity(target)
cast_launch_lightning(caster, world, now=0.0, aim_direction=(0, 1, 0))
assert not any(a.id == "shortcircuit" for p in target.body_parts for a in p.ailments)
def test_shortcircuit_hits_a_hydraulibot(registry):
world = make_world(registry)
caster = make_human(registry, position=EntityPosition("field", 5, 5, 0))
target = make_hydraulibot(registry, position=EntityPosition("field", 5, 6, 0))
world.add_entity(caster)
world.add_entity(target)
cast_launch_lightning(caster, world, now=0.0, aim_direction=(0, 1, 0), rng=random.Random(1))
assert any(a.id == "shortcircuit" for p in target.body_parts for a in p.ailments)
def test_shortcircuit_damages_every_organ_the_hydraulibot_has(registry):
world = make_world(registry)
caster = make_human(registry, position=EntityPosition("field", 5, 5, 0))
target = make_hydraulibot(registry, position=EntityPosition("field", 5, 6, 0))
world.add_entity(caster)
world.add_entity(target)
cast_launch_lightning(caster, world, now=0.0, aim_direction=(0, 1, 0), rng=random.Random(7))
all_organ_ids = {
"hydraulibot_cpu", "hydraulibot_npu", "hydraulibot_camera_module", "hydraulibot_mic",
"hydraulibot_power_cell", "hydraulibot_hydraulic_fluid_bladder", "hydraulibot_cooling_fan",
}
damaged_ids = set()
total_damage = 0.0
for part in target.body_parts:
for organ in part.organs:
if organ.integrity < 100.0 or organ.removed:
damaged_ids.add(organ.organ_def_id)
total_damage += 100.0 - organ.integrity
assert damaged_ids == all_organ_ids # every organ took at least some damage
# shortcircuit's 120 spread across all 7 organs, plus heart_palpitations' own separate hit
# to hydraulibot_power_cell (the hydraulibot's heart-equivalent organ) on top of its shortcircuit share
assert total_damage <= SHORTCIRCUIT_TOTAL_ORGAN_DAMAGE + HEART_ORGAN_DAMAGE + 1e-6
def test_shortcircuit_ailment_lands_on_every_body_part_hosting_a_damaged_organ(registry):
world = make_world(registry)
caster = make_human(registry, position=EntityPosition("field", 5, 5, 0))
target = make_hydraulibot(registry, position=EntityPosition("field", 5, 6, 0))
world.add_entity(caster)
world.add_entity(target)
cast_launch_lightning(caster, world, now=0.0, aim_direction=(0, 1, 0), rng=random.Random(3))
head = target.get_body_part("head")
torso = target.get_body_part("torso")
assert any(a.id == "shortcircuit" for a in head.ailments) # hosts cpu/npu
assert any(a.id == "shortcircuit" for a in torso.ailments) # hosts the other three
def test_shortcircuit_gives_a_huge_intellect_debuff(registry):
world = make_world(registry)
caster = make_human(registry, position=EntityPosition("field", 5, 5, 0))
target = make_hydraulibot(registry, position=EntityPosition("field", 5, 6, 0))
world.add_entity(caster)
world.add_entity(target)
before = target.total_check_penalty("insight", registry)
cast_launch_lightning(caster, world, now=0.0, aim_direction=(0, 1, 0), rng=random.Random(1))
after = target.total_check_penalty("insight", registry)
assert after > before
assert after > 0.5 # a "huge" debuff, not a marginal one
def test_shortcircuit_recast_refreshes_rather_than_stacks(registry):
world = make_world(registry)
caster = make_human(registry, position=EntityPosition("field", 5, 5, 0))
target = make_hydraulibot(registry, position=EntityPosition("field", 5, 6, 0))
world.add_entity(caster)
world.add_entity(target)
cast_launch_lightning(caster, world, now=0.0, aim_direction=(0, 1, 0), rng=random.Random(1))
cast_launch_lightning(caster, world, now=100.0, aim_direction=(0, 1, 0), rng=random.Random(2))
head = target.get_body_part("head")
assert len([a for a in head.ailments if a.id == "shortcircuit"]) == 1