351 lines
14 KiB
Python
351 lines
14 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: bot-specific, randomized organ damage spread -------------------------------
|
|
|
|
|
|
def make_bot(registry, **kwargs) -> Character:
|
|
return Character(species_id="bot", default_body_parts=registry.new_default_body_parts("bot"), **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_bot(registry):
|
|
world = make_world(registry)
|
|
caster = make_human(registry, position=EntityPosition("field", 5, 5, 0))
|
|
target = make_bot(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_bot_has(registry):
|
|
world = make_world(registry)
|
|
caster = make_human(registry, position=EntityPosition("field", 5, 5, 0))
|
|
target = make_bot(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 = {"bot_cpu", "bot_npu", "bot_power_cell", "bot_hydraulic_fluid_bladder", "bot_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 5 organs, plus heart_palpitations' own separate hit
|
|
# to bot_power_cell (the bot'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_bot(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_bot(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_bot(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
|