from __future__ import annotations import random from engine.character import Ailment, Character from engine.defs import DefRegistry from engine.map import Map from engine.world import World HEATSTROKE_SLOT = "torso" HEATSTROKE_PROGRESS_PER_TICK = 5.0 HEATSTROKE_RECOVERY_PER_TICK = 3.0 # while sheltered LIGHTNING_BURN_FRACTION = 0.5 # "about half" the body parts LIGHTNING_INTEGRITY_DAMAGE = 60.0 # "large amounts" LIGHTNING_BURN_SEVERITY = 0.9 def is_exposed(map_: Map, x: float, y: float, z: float, registry: DefRegistry) -> bool: """True if the tile has no roof layer over it - open to the sky/weather.""" return map_.get_tile(x, y, z).roof is None def apply_heatstroke_tick(character: Character, exposed: bool, ticks: int) -> None: """Progresses (while exposed) or recovers (while sheltered) a heatstroke ailment with a 0-100 progress meter, attached to the torso (core body temperature). Fully recovering removes the ailment; it's otherwise created on first exposure. """ if ticks <= 0: return part = character.get_or_create_body_part_override(HEATSTROKE_SLOT) if part is None: return ailment = next((a for a in part.ailments if a.id == "heatstroke"), None) if exposed: if ailment is None: ailment = Ailment(id="heatstroke", name="Heatstroke") part.ailments.append(ailment) ailment.progress = min(100.0, ailment.progress + HEATSTROKE_PROGRESS_PER_TICK * ticks) ailment.severity = ailment.progress / 100.0 elif ailment is not None: ailment.progress = max(0.0, ailment.progress - HEATSTROKE_RECOVERY_PER_TICK * ticks) if ailment.progress <= 0.0: part.ailments.remove(ailment) else: ailment.severity = ailment.progress / 100.0 def find_random_exposed_position( map_: Map, registry: DefRegistry, rng: random.Random, z: int = 0 ) -> tuple[int, int, int] | None: """A random walkable, unroofed tile on the map's surface (z) layer - None if there isn't one.""" candidates = [ (x, y, z) for y in range(map_.height) for x in range(map_.width) if is_exposed(map_, x, y, z, registry) and map_.get_tile(x, y, z).is_walkable(registry) ] if not candidates: return None return rng.choice(candidates) def strike_lightning( world: World, map_id: str, registry: DefRegistry, rng: random.Random | None = None ) -> tuple[int, int, int] | None: """Strikes a random unroofed surface tile; if a character is standing there, burns about half their body parts badly (large integrity damage + a severe burn ailment on each). Returns the struck position, or None if the map has no exposed tile to strike. """ rng = rng or random.Random() map_ = world.maps[map_id] position = find_random_exposed_position(map_, registry, rng) if position is None: return None target = world.entity_at(map_id, *position) if isinstance(target, Character): _burn_random_body_parts(target, rng) return position def _burn_random_body_parts(character: Character, rng: random.Random) -> None: slots = [p.slot for p in character.default_body_parts] if not slots: return count = max(1, round(len(slots) * LIGHTNING_BURN_FRACTION)) for slot in rng.sample(slots, min(count, len(slots))): part = character.get_or_create_body_part_override(slot) if part is None: continue part.integrity = max(0.0, part.integrity - LIGHTNING_INTEGRITY_DAMAGE) if part.integrity <= 0.0: part.removed = True part.ailments.append(Ailment(id="burn", name="Burn", severity=LIGHTNING_BURN_SEVERITY, progress=100.0)) def apply_weather_tick( world: World, registry: DefRegistry, map_id: str, ticks: int, rng: random.Random | None = None, lightning_chance_per_tick: float = 0.3, ) -> tuple[int, int, int] | None: """Call once per game tick for a given map: progresses heatstroke for everyone there based on roof exposure, and rolls a chance of a lightning strike per elapsed tick. Returns the position of the last lightning strike this call, if any. """ rng = rng or random.Random() map_ = world.maps[map_id] for entity in world.entities_on_map(map_id): if not isinstance(entity, Character) or entity.position is None: continue exposed = is_exposed(map_, entity.position.x, entity.position.y, entity.position.z, registry) apply_heatstroke_tick(entity, exposed, ticks) struck = None for _ in range(ticks): if rng.random() < lightning_chance_per_tick: struck = strike_lightning(world, map_id, registry, rng) or struck return struck