feat: add Nodes.new, Nodes.isFallow and Noise.propagate
This commit is contained in:
1 parent
dfa4ef9bbc
commit
a10eb10d22
2 files changed
+149
No files matched your search
@@ -0,0 +1,75 @@
|
||||
-- Renewable resource nodes scattered on the map: an ore vein pays out
|
||||
-- crafting essence (crafting.lua's forging currency) and a gem seam yields a
|
||||
-- raw tier-1 gem outright, both without spending a single turn in combat -
|
||||
-- a genuine second income alongside killing monsters (Crafting.essenceFor),
|
||||
-- not a strictly better replacement for it. Each node holds a fixed number
|
||||
-- of charges before it plays out; a played-out node does not vanish, it goes
|
||||
-- fallow and slowly reopens on its own, so a floor already cleared of
|
||||
-- monsters still rewards a return visit. Nodes are plain data on
|
||||
-- level.nodes, exactly like level.shrines and level.cauldrons, so they
|
||||
-- serialize with no special save-format handling.
|
||||
|
||||
local Crafting = require("crafting")
|
||||
|
||||
local Nodes = {}
|
||||
|
||||
Nodes.ORE = "ore"
|
||||
Nodes.GEM = "gem"
|
||||
|
||||
Nodes.glyphs = { [Nodes.ORE] = "O", [Nodes.GEM] = "M" }
|
||||
|
||||
Nodes.MAX_CHARGES = 3
|
||||
Nodes.REGEN_TURNS = 40
|
||||
Nodes.ORE_YIELD_MIN = 3
|
||||
Nodes.ORE_YIELD_MAX = 7
|
||||
|
||||
-- A freshly placed node at full charges, never on cooldown. Builder stamps
|
||||
-- x/y directly onto the returned table, matching how it stamps every other
|
||||
-- floor feature (shrines, cauldrons, vault levers).
|
||||
function Nodes.new(kind, element)
|
||||
return { kind = kind, element = element, charges = Nodes.MAX_CHARGES, cooldown = 0 }
|
||||
end
|
||||
|
||||
function Nodes.isFallow(node)
|
||||
return node.charges <= 0
|
||||
end
|
||||
|
||||
function Nodes.glyph(node)
|
||||
return Nodes.glyphs[node.kind]
|
||||
end
|
||||
|
||||
-- Extracts one charge's worth of resource from `node` into `game.player`.
|
||||
-- Returns (false, line) without touching the node when it has nothing left
|
||||
-- to give, or (true, line) once a charge is spent - dropping the node into
|
||||
-- cooldown the instant that spends its last one.
|
||||
function Nodes.harvest(game, node)
|
||||
if Nodes.isFallow(node) then
|
||||
return false, "the vein is played out; give it time to reopen"
|
||||
end
|
||||
node.charges = node.charges - 1
|
||||
if node.charges <= 0 then node.cooldown = Nodes.REGEN_TURNS end
|
||||
local player = game.player
|
||||
if node.kind == Nodes.ORE then
|
||||
local amount = math.floor(game.rng:range(Nodes.ORE_YIELD_MIN, Nodes.ORE_YIELD_MAX) * game:essenceMultiplier())
|
||||
player.essence = player.essence + amount
|
||||
return true, string.format("you chip %d essence from the ore vein", amount)
|
||||
end
|
||||
local gem = Crafting.gem(node.element, 1)
|
||||
player.inventory[#player.inventory + 1] = gem
|
||||
return true, "you pry a " .. gem.name .. " loose from the seam"
|
||||
end
|
||||
|
||||
-- Advances every fallow node's cooldown one world round, exactly like
|
||||
-- hazards.step advances fire and gas - called once per round, not per actor,
|
||||
-- so a seeded run always reopens a vein on the identical turn regardless of
|
||||
-- how many actors acted that round.
|
||||
function Nodes.step(level)
|
||||
for _, node in ipairs(level.nodes) do
|
||||
if node.cooldown > 0 then
|
||||
node.cooldown = node.cooldown - 1
|
||||
if node.cooldown <= 0 then node.charges = Nodes.MAX_CHARGES end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return Nodes
|
||||
@@ -0,0 +1,74 @@
|
||||
-- Sound propagation. Unlike los.lua's straight sightline, a noisy action's
|
||||
-- racket bends around corners: it floods outward from its origin through
|
||||
-- open, walkable cells only, using the same four-directional adjacency
|
||||
-- astar.lua's own pathing already assumes (see its DIRS and game.lua's
|
||||
-- MONSTER_DIRS), and fades by exactly one point of loudness for every step
|
||||
-- the flood has to take to get there. A closed door or a solid wall stops
|
||||
-- it dead - nothing leaks through a wall diagonally the way it might with
|
||||
-- an eight-directional flood, so a noise never reaches anywhere a monster
|
||||
-- could not actually have walked to investigate it.
|
||||
--
|
||||
-- The flood itself has no idea what a monster or a player is; it is a pure
|
||||
-- function of the map, an origin and a starting loudness. game.lua's
|
||||
-- Game:emitNoise is what turns a resulting field into a dormant monster
|
||||
-- actually stirring (see stealth.lua's Stealth.hear).
|
||||
|
||||
local Noise = {}
|
||||
|
||||
local DIRS = { { 1, 0 }, { -1, 0 }, { 0, 1 }, { 0, -1 } }
|
||||
|
||||
-- Loudness constants for the actions game.lua wires up to emitNoise, tuned
|
||||
-- so a whisper-quiet action (nothing currently that quiet) would decay
|
||||
-- within a couple of cells while a pick striking stone carries the length
|
||||
-- of a small floor.
|
||||
Noise.MELEE = 6
|
||||
Noise.RANGED = 6
|
||||
Noise.THROW = 5
|
||||
Noise.SPELL = 8
|
||||
Noise.DIG = 9
|
||||
|
||||
-- Floods outward from (x, y) through walkable cells, returning a table
|
||||
-- keyed by map:index(nx, ny) -> the loudness still remaining once the sound
|
||||
-- has traveled there (loudness itself at the origin, decaying by one per
|
||||
-- step, never revisiting a cell at an intensity no better than one already
|
||||
-- recorded there). A cell the flood never reaches simply has no entry. The
|
||||
-- origin itself need not be walkable - a pick striking solid rock (see
|
||||
-- game.lua's Game:dig) is exactly as loud as one striking rubble - only the
|
||||
-- cells the flood actually spreads into have to be open for anything to
|
||||
-- ever stand in them and hear it.
|
||||
function Noise.propagate(map, x, y, loudness)
|
||||
local field = {}
|
||||
if loudness <= 0 or not map:inBounds(x, y) then return field end
|
||||
local startIdx = map:index(x, y)
|
||||
field[startIdx] = loudness
|
||||
local queue = { { x = x, y = y, intensity = loudness } }
|
||||
local head = 1
|
||||
while head <= #queue do
|
||||
local cur = queue[head]
|
||||
head = head + 1
|
||||
if cur.intensity > 0 then
|
||||
local nextIntensity = cur.intensity - 1
|
||||
for _, dir in ipairs(DIRS) do
|
||||
local nx, ny = cur.x + dir[1], cur.y + dir[2]
|
||||
if map:isWalkable(nx, ny) then
|
||||
local idx = map:index(nx, ny)
|
||||
if (field[idx] or -1) < nextIntensity then
|
||||
field[idx] = nextIntensity
|
||||
queue[#queue + 1] = { x = nx, y = ny, intensity = nextIntensity }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
return field
|
||||
end
|
||||
|
||||
-- Remaining loudness a previously computed field still carries at (x, y),
|
||||
-- or 0 for anywhere the flood never reached (including out of bounds, which
|
||||
-- map:index would otherwise happily compute an index for).
|
||||
function Noise.intensityAt(field, map, x, y)
|
||||
if not map:inBounds(x, y) then return 0 end
|
||||
return field[map:index(x, y)] or 0
|
||||
end
|
||||
|
||||
return Noise
|
||||
Reference in new issue
Block a user