feat: add Biomes.forDepth, Biomes.describe and Blocks.treasure
This commit is contained in:
1 parent
fbfe8411ac
commit
9bcc8fe6e2
2 files changed
+175
No files matched your search
@@ -0,0 +1,62 @@
|
||||
-- Depth-band environmental theme layered on top of the systems that already
|
||||
-- read raw depth alone: unlike atmosphere.lua's weather (a coin flip drawn
|
||||
-- fresh on every floor from depth two on, see Atmosphere.order), a biome is
|
||||
-- a fixed function of depth by itself, so floor three always belongs to the
|
||||
-- same people and the same climate no matter which seed generated it -
|
||||
-- weather is what a given run of that floor happens to be like today, a
|
||||
-- biome is what the floor always is. Each entry names the faction that
|
||||
-- actually lives there (see factions.lua) and the hazard kind its climate
|
||||
-- tends toward (see hazards.lua), which the builder's biome pass - appended
|
||||
-- dead last of all in Builder.build, after even the grave, so it can never
|
||||
-- perturb any earlier system's rng sequence - may draw on to reinforce a
|
||||
-- floor with one more native monster or bloom a small pocket of that
|
||||
-- hazard, strictly on top of everything already placed there.
|
||||
|
||||
local Factions = require("factions")
|
||||
local Hazards = require("hazards")
|
||||
|
||||
local Biomes = {}
|
||||
|
||||
Biomes.CATALOG = {
|
||||
{ key = "warrens", name = "the mossy warrens", favoredFaction = Factions.VERMIN, hazardKind = Hazards.GAS, ambient = "damp stone and skittering claws" },
|
||||
{ key = "burrows", name = "the goblin burrows", favoredFaction = Factions.GOBLINOID, hazardKind = Hazards.GAS, ambient = "crude tunnels reeking of tallow smoke" },
|
||||
{ key = "crypts", name = "the sunken crypts", favoredFaction = Factions.UNDEAD, hazardKind = Hazards.GAS, ambient = "cold air thick with grave rot" },
|
||||
{ key = "warcamp", name = "the orcish warcamp", favoredFaction = Factions.ORCISH, hazardKind = Hazards.FIRE, ambient = "smoke-stained banners and cookfires" },
|
||||
{ key = "sanctum", name = "the cult sanctum", favoredFaction = Factions.CULT, hazardKind = Hazards.FIRE, ambient = "chanting echoes off blood-lit stone" },
|
||||
{ key = "wyrmnest", name = "the wyrm's nest", favoredFaction = Factions.DRAKE, hazardKind = Hazards.FIRE, ambient = "scorched walls and the reek of sulfur" },
|
||||
}
|
||||
|
||||
Biomes.byKey = {}
|
||||
for _, biome in ipairs(Biomes.CATALOG) do
|
||||
Biomes.byKey[biome.key] = biome
|
||||
end
|
||||
|
||||
-- Cyclic by depth, exactly the way chosenGenerator above already cycles
|
||||
-- through Gen.order: a dungeon of any Builder.maxDepth wraps back through
|
||||
-- the catalog rather than running off its end once depth outgrows it.
|
||||
function Biomes.forDepth(depth)
|
||||
return Biomes.CATALOG[((depth - 1) % #Biomes.CATALOG) + 1]
|
||||
end
|
||||
|
||||
function Biomes.describe(biome)
|
||||
return string.format("%s: %s", biome.name, biome.ambient)
|
||||
end
|
||||
|
||||
-- The subset of an already depth-filtered species list (see
|
||||
-- Monsters.forDepth) that actually belongs to biome's own faction - the
|
||||
-- pool the builder's reinforcement pass draws a native from, empty whenever
|
||||
-- nothing eligible this shallow yet belongs to the biome's own people.
|
||||
-- Ranged specs are excluded outright: a second archer stacks free damage no
|
||||
-- melee approach can answer, so reinforcement only ever sends backup a
|
||||
-- player can actually close the distance on.
|
||||
function Biomes.nativesAmong(biome, eligible)
|
||||
local natives = {}
|
||||
for _, spec in ipairs(eligible) do
|
||||
if spec.faction == biome.favoredFaction and spec.ai ~= "ranged" then
|
||||
natives[#natives + 1] = spec
|
||||
end
|
||||
end
|
||||
return natives
|
||||
end
|
||||
|
||||
return Biomes
|
||||
+113
@@ -0,0 +1,113 @@
|
||||
-- Pushable blocks: a block occupies a floor cell and blocks movement and
|
||||
-- pathing exactly like a wall, but shoving it (see Game:movePlayer) relocates
|
||||
-- it one cell in the push direction instead of just refusing the move, the
|
||||
-- same "walk into it to interact" shape a closed door or a vault lever
|
||||
-- already has. A block shoved into lava solidifies into rubble and one
|
||||
-- shoved into a chasm fills the gap into plain floor - both permanently
|
||||
-- consume the block, since it has nothing left to rest on once the hazard
|
||||
-- it corrected is gone. Shoved onto a pressure plate instead, the block
|
||||
-- survives and holds its linked gate open for as long as it stays put (see
|
||||
-- Blocks.updateGates) - unlike vaults.lua's lever, a gate reseals the
|
||||
-- instant the weight holding it open moves off the plate again.
|
||||
|
||||
local Tiles = require("tiles")
|
||||
local Items = require("items")
|
||||
local Affixes = require("affixes")
|
||||
|
||||
local Blocks = {}
|
||||
|
||||
local treasureWeapons = {
|
||||
{ name = "gate-forged blade", power = 7 },
|
||||
{ name = "gate-forged maul", power = 8 },
|
||||
}
|
||||
local treasureArmors = {
|
||||
{ name = "gate-forged plate", power = 6 },
|
||||
}
|
||||
|
||||
-- A guaranteed affix on a base already well above anything the floor's own
|
||||
-- loot table can roll at this depth, mirroring vaults.lua's own Vaults.
|
||||
-- treasure but with names that never claim to come from a vault.
|
||||
function Blocks.treasure(rng, depth)
|
||||
if rng:chance(1, 2) then
|
||||
local base = treasureWeapons[rng:range(1, #treasureWeapons)]
|
||||
local item = Items.weapon(base.name, base.power + depth // 3)
|
||||
return Affixes.decorate(item, Affixes.weapon[rng:range(1, #Affixes.weapon)])
|
||||
end
|
||||
local base = treasureArmors[rng:range(1, #treasureArmors)]
|
||||
local item = Items.armor(base.name, base.power + depth // 3)
|
||||
return Affixes.decorate(item, Affixes.armor[rng:range(1, #Affixes.armor)])
|
||||
end
|
||||
|
||||
-- Whether a cell a block might be shoved into can actually receive it: in
|
||||
-- bounds, not a wall, a door of any kind, or a gate, and not already host to
|
||||
-- another block or a living entity. Lava and chasm cells count as
|
||||
-- receiving even though Blocks.push special-cases what happens to them
|
||||
-- next, rather than refusing the shove outright.
|
||||
function Blocks.canReceive(level, x, y)
|
||||
if not level.map:inBounds(x, y) then return false end
|
||||
local tile = level.map:get(x, y)
|
||||
if tile == Tiles.WALL or Tiles.isDoor(tile) or Tiles.isVaultDoor(tile) or Tiles.isGate(tile) then
|
||||
return false
|
||||
end
|
||||
if level:blockAt(x, y) or level:entityAt(x, y) then return false end
|
||||
return true
|
||||
end
|
||||
|
||||
local function remove(level, block)
|
||||
for i, b in ipairs(level.blocks) do
|
||||
if b == block then
|
||||
table.remove(level.blocks, i)
|
||||
return
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Shoves `block` by (dx, dy) if the far cell can receive it. Returns
|
||||
-- (true, message) on a successful shove, resolving whatever lava or chasm
|
||||
-- it lands in along the way (Blocks.remove), or (false) when the far cell
|
||||
-- refuses it, exactly like bumping into a wall.
|
||||
function Blocks.push(level, block, dx, dy)
|
||||
local nx, ny = block.x + dx, block.y + dy
|
||||
if not Blocks.canReceive(level, nx, ny) then return false end
|
||||
local tile = level.map:get(nx, ny)
|
||||
if tile == Tiles.LAVA then
|
||||
level.map:set(nx, ny, Tiles.RUBBLE)
|
||||
remove(level, block)
|
||||
return true, "the block hisses into the lava and cools into rubble"
|
||||
end
|
||||
if tile == Tiles.CHASM then
|
||||
level.map:set(nx, ny, Tiles.FLOOR)
|
||||
remove(level, block)
|
||||
return true, "the block tumbles into the chasm and fills it"
|
||||
end
|
||||
block.x, block.y = nx, ny
|
||||
return true, "you shove the block"
|
||||
end
|
||||
|
||||
-- Whether any plate linked to `gate` currently carries a block's weight.
|
||||
local function gateHeld(level, gate)
|
||||
for _, plate in ipairs(level.plates) do
|
||||
if plate.gateX == gate.x and plate.gateY == gate.y and level:blockAt(plate.x, plate.y) then
|
||||
return true
|
||||
end
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
-- Re-evaluates every gate on the level against its own plates' current
|
||||
-- weight, flipping both its tile glyph and its `open` field the instant a
|
||||
-- plate gains or loses a block. Called after any block shove (Game:
|
||||
-- movePlayer) so a gate reacts immediately rather than waiting for a full
|
||||
-- turn tick, the same way Vaults.pull's door flip is immediate on the pull
|
||||
-- that completes it.
|
||||
function Blocks.updateGates(level)
|
||||
for _, gate in ipairs(level.gates) do
|
||||
local held = gateHeld(level, gate)
|
||||
if held ~= gate.open then
|
||||
gate.open = held
|
||||
level.map:set(gate.x, gate.y, held and Tiles.GATE_OPEN or Tiles.GATE_CLOSED)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return Blocks
|
||||
Reference in new issue
Block a user