123 lines
4.7 KiB
Python
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
|