import random from pathlib import Path import pytest from engine.character import Character from engine.defs import DefRegistry from engine.entity import EntityPosition from engine.map import Map from engine.map_loader import MapLoader from engine.tile import Tile, TileLayerInstance from engine.world import World from resources.logic.weather import ( HEATSTROKE_SLOT, apply_heatstroke_tick, apply_weather_tick, find_random_exposed_position, is_exposed, strike_lightning, ) 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 load_storm_field(registry): loader = MapLoader(DEFS_DIR / "maps") return loader.load("storm_field.json") # --- is_exposed -------------------------------------------------------------------------- def test_is_exposed_true_in_the_open(registry): field = load_storm_field(registry) assert is_exposed(field, 0, 0, 0, registry) is True def test_is_exposed_false_under_the_shelter_roof(registry): field = load_storm_field(registry) assert is_exposed(field, 8, 8, 0, registry) is False # --- heatstroke ---------------------------------------------------------------------------- def test_heatstroke_progresses_while_exposed(registry): character = make_human(registry) apply_heatstroke_tick(character, exposed=True, ticks=3) ailment = next(a for a in character.get_body_part(HEATSTROKE_SLOT).ailments if a.id == "heatstroke") assert ailment.progress == pytest.approx(15.0) assert ailment.severity == pytest.approx(0.15) def test_heatstroke_progress_caps_at_100(registry): character = make_human(registry) apply_heatstroke_tick(character, exposed=True, ticks=1000) ailment = next(a for a in character.get_body_part(HEATSTROKE_SLOT).ailments if a.id == "heatstroke") assert ailment.progress == 100.0 def test_heatstroke_recovers_while_sheltered(registry): character = make_human(registry) apply_heatstroke_tick(character, exposed=True, ticks=5) apply_heatstroke_tick(character, exposed=False, ticks=2) ailment = next(a for a in character.get_body_part(HEATSTROKE_SLOT).ailments if a.id == "heatstroke") assert ailment.progress == pytest.approx(25.0 - 6.0) def test_heatstroke_ailment_removed_once_fully_recovered(registry): character = make_human(registry) apply_heatstroke_tick(character, exposed=True, ticks=1) apply_heatstroke_tick(character, exposed=False, ticks=10) part = character.get_body_part(HEATSTROKE_SLOT) assert not any(a.id == "heatstroke" for a in part.ailments) def test_no_heatstroke_ailment_if_never_exposed(registry): character = make_human(registry) apply_heatstroke_tick(character, exposed=False, ticks=5) part = character.get_body_part(HEATSTROKE_SLOT) assert not any(a.id == "heatstroke" for a in part.ailments) def test_heatstroke_zero_ticks_is_a_no_op(registry): character = make_human(registry) apply_heatstroke_tick(character, exposed=True, ticks=0) part = character.get_body_part(HEATSTROKE_SLOT) assert not any(a.id == "heatstroke" for a in part.ailments) # --- lightning ----------------------------------------------------------------------------- def test_find_random_exposed_position_never_returns_the_sheltered_patch(registry): field = load_storm_field(registry) rng = random.Random(1234) for _ in range(200): pos = find_random_exposed_position(field, registry, rng) assert pos is not None assert is_exposed(field, *pos, registry) is True def test_strike_lightning_returns_none_on_a_fully_roofed_map(registry): roofed = Map("roofed", 3, 3, 1) for x in range(3): for y in range(3): roofed.set_tile(x, y, 0, Tile(flooring=TileLayerInstance("grass"), roof=TileLayerInstance("shingles"))) world = World(registry=registry) world.add_map(roofed) assert strike_lightning(world, "roofed", registry, random.Random(1)) is None def test_strike_lightning_burns_about_half_the_body_parts_when_it_hits_someone(registry): field = load_storm_field(registry) world = World(registry=registry) world.add_map(field) rng = random.Random(42) # find an exposed tile deterministically and place the victim exactly there pos = find_random_exposed_position(field, registry, random.Random(42)) victim = make_human(registry, position=EntityPosition("storm_field", pos[0], pos[1], pos[2])) world.add_entity(victim) struck = strike_lightning(world, "storm_field", registry, rng) assert struck == pos burned_parts = [p for p in victim.body_parts if any(a.id == "burn" for a in p.ailments)] assert 2 <= len(burned_parts) <= 4 # ~half of 6 default parts for part in burned_parts: assert part.integrity <= 40.0 # took "large" damage (started at 100, -60) def test_strike_lightning_misses_if_nobody_is_on_the_struck_tile(registry): field = load_storm_field(registry) world = World(registry=registry) world.add_map(field) # no entities added at all struck = strike_lightning(world, "storm_field", registry, random.Random(7)) assert struck is not None # still strikes a tile... assert is_exposed(field, *struck, registry) is True # --- apply_weather_tick orchestration -------------------------------------------------------- def test_apply_weather_tick_progresses_heatstroke_for_exposed_entities(registry): field = load_storm_field(registry) world = World(registry=registry) world.add_map(field) victim = make_human(registry, position=EntityPosition("storm_field", 0, 0, 0)) world.add_entity(victim) apply_weather_tick(world, registry, "storm_field", ticks=2, rng=random.Random(1), lightning_chance_per_tick=0.0) ailment = next(a for a in victim.get_body_part(HEATSTROKE_SLOT).ailments if a.id == "heatstroke") assert ailment.progress == pytest.approx(10.0) def test_apply_weather_tick_does_not_progress_heatstroke_under_shelter(registry): field = load_storm_field(registry) world = World(registry=registry) world.add_map(field) sheltered = make_human(registry, position=EntityPosition("storm_field", 8, 8, 0)) world.add_entity(sheltered) apply_weather_tick(world, registry, "storm_field", ticks=3, rng=random.Random(1), lightning_chance_per_tick=0.0) part = sheltered.get_body_part(HEATSTROKE_SLOT) assert not any(a.id == "heatstroke" for a in part.ailments) def test_apply_weather_tick_can_strike_lightning_with_high_chance(registry): field = load_storm_field(registry) world = World(registry=registry) world.add_map(field) struck = apply_weather_tick(world, registry, "storm_field", ticks=5, rng=random.Random(2), lightning_chance_per_tick=1.0) assert struck is not None