-- 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