Mi2dRPGamEng/resources/logic/weather.py

123 lines
4.7 KiB
Python

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