115 lines
4.5 KiB
Lua
115 lines
4.5 KiB
Lua
-- Timed area hazards that live on the map itself rather than on an entity:
|
|
-- fire and caustic gas. Both linger on their cell, hurt and afflict whoever
|
|
-- ends a turn standing there, and can spread into open neighbouring floor
|
|
-- before burning or blowing themselves out. Fire is doused outright by water.
|
|
-- Hazards are plain data on level.hazards so they serialize like floor items
|
|
-- and traps do, with no special save-format handling.
|
|
|
|
local Tiles = require("tiles")
|
|
local Status = require("status")
|
|
local Resistances = require("resistances")
|
|
local Conductivity = require("conductivity")
|
|
|
|
local Hazards = {}
|
|
|
|
Hazards.FIRE = "fire"
|
|
Hazards.GAS = "gas"
|
|
|
|
local DIRS = { { 1, 0 }, { -1, 0 }, { 0, 1 }, { 0, -1 } }
|
|
|
|
-- First hazard occupying (x, y), optionally filtered to a specific kind so
|
|
-- fire and gas can coexist on the same cell without merging into each other.
|
|
function Hazards.at(level, x, y, kind)
|
|
for _, hz in ipairs(level.hazards) do
|
|
if hz.x == x and hz.y == y and (not kind or hz.kind == kind) then return hz end
|
|
end
|
|
return nil
|
|
end
|
|
|
|
-- Drops a hazard on (x, y), or refreshes one already there of the same kind
|
|
-- to whichever of the two durations and potencies is stronger. Walls never
|
|
-- catch fire or hold gas.
|
|
function Hazards.spawn(level, x, y, kind, duration, potency)
|
|
if not level.map:inBounds(x, y) then return nil end
|
|
if level.map:get(x, y) == Tiles.WALL then return nil end
|
|
local existing = Hazards.at(level, x, y, kind)
|
|
if existing then
|
|
existing.duration = math.max(existing.duration, duration)
|
|
existing.potency = math.max(existing.potency, potency)
|
|
return existing
|
|
end
|
|
local hz = { x = x, y = y, kind = kind, duration = duration, potency = potency }
|
|
level.hazards[#level.hazards + 1] = hz
|
|
return hz
|
|
end
|
|
|
|
function Hazards.ignite(level, x, y, duration, potency)
|
|
return Hazards.spawn(level, x, y, Hazards.FIRE, duration or 3, potency or 2)
|
|
end
|
|
|
|
function Hazards.spawnGas(level, x, y, duration, potency)
|
|
return Hazards.spawn(level, x, y, Hazards.GAS, duration or 4, potency or 2)
|
|
end
|
|
|
|
-- Applies whichever hazard occupies entity's current cell, mirroring how
|
|
-- Status.tick afflicts the same actor once per turn. Fire and gas both deal
|
|
-- their potency in direct damage and lay down a fading status effect on top,
|
|
-- so stepping out of the hazard still leaves a lingering burn or sickness.
|
|
function Hazards.afflict(level, entity)
|
|
local hz = Hazards.at(level, entity.x, entity.y)
|
|
if not hz then return nil end
|
|
if hz.kind == Hazards.FIRE then
|
|
local dealt = Conductivity.applyFire(entity, hz.potency)
|
|
entity.hp = entity.hp - dealt
|
|
Status.apply(entity, Status.BURNING, 2, math.max(1, hz.potency // 2))
|
|
if Conductivity.isWet(entity) then
|
|
return string.format("flames hiss against %s's soaked skin for %d", entity.name, dealt)
|
|
end
|
|
return string.format("flames engulf %s for %d", entity.name, dealt)
|
|
end
|
|
local dealt = Resistances.apply(entity, hz.potency, Resistances.POISON)
|
|
entity.hp = entity.hp - dealt
|
|
Status.apply(entity, Status.POISON, 2, math.max(1, hz.potency // 2))
|
|
return string.format("choking fumes sicken %s for %d", entity.name, dealt)
|
|
end
|
|
|
|
local function canSpreadInto(level, x, y)
|
|
if not level.map:inBounds(x, y) then return false end
|
|
local tile = level.map:get(x, y)
|
|
return Tiles.isWalkable(tile) and tile ~= Tiles.WATER
|
|
end
|
|
|
|
-- Advances every hazard one environmental step: each has an even chance to
|
|
-- bloom into an open neighbouring cell that isn't already burning or gassed
|
|
-- the same way, water douses fire outright, and every hazard's clock counts
|
|
-- down until it burns or blows itself out. Called once per world round (not
|
|
-- per actor) so a seeded run always spreads identically regardless of how
|
|
-- many actors acted that round.
|
|
function Hazards.step(level, rng)
|
|
local spawns = {}
|
|
for _, hz in ipairs(level.hazards) do
|
|
for _, dir in ipairs(DIRS) do
|
|
local nx, ny = hz.x + dir[1], hz.y + dir[2]
|
|
if canSpreadInto(level, nx, ny) and not Hazards.at(level, nx, ny, hz.kind)
|
|
and rng:chance(1, 2) then
|
|
spawns[#spawns + 1] = { nx, ny, hz.kind, hz.duration, hz.potency }
|
|
end
|
|
end
|
|
end
|
|
for _, s in ipairs(spawns) do
|
|
Hazards.spawn(level, s[1], s[2], s[3], s[4], s[5])
|
|
end
|
|
|
|
local kept = {}
|
|
for _, hz in ipairs(level.hazards) do
|
|
local doused = hz.kind == Hazards.FIRE and level.map:get(hz.x, hz.y) == Tiles.WATER
|
|
hz.duration = hz.duration - 1
|
|
if hz.duration > 0 and not doused then
|
|
kept[#kept + 1] = hz
|
|
end
|
|
end
|
|
level.hazards = kept
|
|
end
|
|
|
|
return Hazards
|