feat: extend builder with Builder.maxDepth and reachSet
This commit is contained in:
1 parent
907f57db41
commit
86b7017214
1 file changed
+143
-14
+143
-14
@@ -9,12 +9,51 @@ local Gen = require("gen.init")
|
|||||||
local Monsters = require("monsters")
|
local Monsters = require("monsters")
|
||||||
local Items = require("items")
|
local Items = require("items")
|
||||||
local Affixes = require("affixes")
|
local Affixes = require("affixes")
|
||||||
|
local Shop = require("shop")
|
||||||
|
local Shrines = require("shrines")
|
||||||
|
local Companions = require("companions")
|
||||||
|
local Artifacts = require("artifacts")
|
||||||
|
local Bosses = require("bosses")
|
||||||
|
local Quests = require("quests")
|
||||||
|
local Alchemy = require("alchemy")
|
||||||
|
local Elites = require("elites")
|
||||||
|
local Vaults = require("vaults")
|
||||||
|
local Runewords = require("runewords")
|
||||||
|
local Atmosphere = require("atmosphere")
|
||||||
|
local Stealth = require("stealth")
|
||||||
|
local Crafting = require("crafting")
|
||||||
|
local Nodes = require("nodes")
|
||||||
|
local Sets = require("sets")
|
||||||
|
local Mimics = require("mimics")
|
||||||
|
local Curses = require("curses")
|
||||||
|
local Graves = require("graves")
|
||||||
|
local Chests = require("chests")
|
||||||
|
local Biomes = require("biomes")
|
||||||
|
local Hazards = require("hazards")
|
||||||
|
local Blocks = require("blocks")
|
||||||
|
local Prefab = require("gen.prefab")
|
||||||
local Level = require("level")
|
local Level = require("level")
|
||||||
|
|
||||||
local Builder = {}
|
local Builder = {}
|
||||||
|
|
||||||
local WIDTH, HEIGHT = 60, 24
|
local WIDTH, HEIGHT = 60, 24
|
||||||
|
|
||||||
|
-- The dungeon bottoms out here: the Amulet of Warren is guaranteed to be
|
||||||
|
-- somewhere on this floor, and there is no way to descend past it. Tests
|
||||||
|
-- override it via options.maxDepth to exercise a full fetch-and-return
|
||||||
|
-- playthrough without generating nine intervening floors.
|
||||||
|
Builder.MAX_DEPTH = 10
|
||||||
|
|
||||||
|
-- The effects a random floor trap, or a trap kit rolled as loot, can carry -
|
||||||
|
-- shared by the two draws below and exposed here so tests can assert against
|
||||||
|
-- the authoritative list instead of a copy that would go stale the next time
|
||||||
|
-- a new effect is appended.
|
||||||
|
Builder.TRAP_EFFECTS = { "dart", "poison", "teleport", "confuse", "blind", "stun" }
|
||||||
|
|
||||||
|
function Builder.maxDepth(options)
|
||||||
|
return (options and options.maxDepth) or Builder.MAX_DEPTH
|
||||||
|
end
|
||||||
|
|
||||||
local weaponBases = {
|
local weaponBases = {
|
||||||
{ name = "dagger", power = 2 }, { name = "mace", power = 3 },
|
{ name = "dagger", power = 2 }, { name = "mace", power = 3 },
|
||||||
{ name = "sword", power = 4 }, { name = "battle axe", power = 5 },
|
{ name = "sword", power = 4 }, { name = "battle axe", power = 5 },
|
||||||
@@ -23,13 +62,21 @@ local armorBases = {
|
|||||||
{ name = "leather armor", power = 1 }, { name = "chain mail", power = 2 },
|
{ name = "leather armor", power = 1 }, { name = "chain mail", power = 2 },
|
||||||
{ name = "plate armor", power = 3 },
|
{ name = "plate armor", power = 3 },
|
||||||
}
|
}
|
||||||
|
local bowBases = {
|
||||||
|
{ name = "short bow", power = 2, range = 5 },
|
||||||
|
{ name = "long bow", power = 3, range = 7 },
|
||||||
|
{ name = "crossbow", power = 4, range = 6 },
|
||||||
|
}
|
||||||
|
|
||||||
local NEIGHBOURS = { { 1, 0 }, { -1, 0 }, { 0, 1 }, { 0, -1 } }
|
local NEIGHBOURS = { { 1, 0 }, { -1, 0 }, { 0, 1 }, { 0, -1 } }
|
||||||
|
|
||||||
-- Cells reachable from (sx, sy) through walkable terrain and closed doors, and -
|
-- Cells reachable from (sx, sy) through walkable terrain and closed doors, and -
|
||||||
-- when allowLocked is true - locked doors too. Used to guarantee the descent is
|
-- when allowLocked is true - locked doors too. Used to guarantee the descent is
|
||||||
-- never sealed behind a locked door and that keys always sit on an openable side.
|
-- never sealed behind a locked door and that keys always sit on an openable side.
|
||||||
local function reachSet(map, sx, sy, allowLocked)
|
-- When allowVault is also true, a sealed vault door or a closed gate (see
|
||||||
|
-- below) counts as passable too, so the far side of an unopened vault or
|
||||||
|
-- gate puzzle can still be found for its treasure's placement.
|
||||||
|
local function reachSet(map, sx, sy, allowLocked, allowVault)
|
||||||
local seen = { [map:index(sx, sy)] = true }
|
local seen = { [map:index(sx, sy)] = true }
|
||||||
local stack = { { sx, sy } }
|
local stack = { { sx, sy } }
|
||||||
while #stack > 0 do
|
while #stack > 0 do
|
||||||
@@ -37,11 +84,15 @@ local function reachSet(map, sx, sy, allowLocked)
|
|||||||
stack[#stack] = nil
|
stack[#stack] = nil
|
||||||
for _, step in ipairs(NEIGHBOURS) do
|
for _, step in ipairs(NEIGHBOURS) do
|
||||||
local nx, ny = cell[1] + step[1], cell[2] + step[2]
|
local nx, ny = cell[1] + step[1], cell[2] + step[2]
|
||||||
if map:inBounds(nx, ny) and map:isPassable(nx, ny, allowLocked) then
|
if map:inBounds(nx, ny) then
|
||||||
local idx = map:index(nx, ny)
|
local passable = map:isPassable(nx, ny, allowLocked)
|
||||||
if not seen[idx] then
|
or (allowVault and (Tiles.isVaultDoor(map:get(nx, ny)) or Tiles.isGate(map:get(nx, ny))))
|
||||||
seen[idx] = true
|
if passable then
|
||||||
stack[#stack + 1] = { nx, ny }
|
local idx = map:index(nx, ny)
|
||||||
|
if not seen[idx] then
|
||||||
|
seen[idx] = true
|
||||||
|
stack[#stack + 1] = { nx, ny }
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -81,25 +132,42 @@ end
|
|||||||
|
|
||||||
local function rollItem(rng, depth)
|
local function rollItem(rng, depth)
|
||||||
local roll = rng:range(1, 100)
|
local roll = rng:range(1, 100)
|
||||||
if roll <= 24 then
|
if roll <= 22 then
|
||||||
local effects = { "heal", "heal", "strength", "haste", "poison" }
|
local effects = { "heal", "heal", "strength", "haste", "poison", "clarity", "cure", "confuse", "blind", "levitate" }
|
||||||
return Items.potionOf(effects[rng:range(1, #effects)])
|
return Items.potionOf(effects[rng:range(1, #effects)])
|
||||||
elseif roll <= 40 then
|
elseif roll <= 36 then
|
||||||
local effects = { "teleport", "identify", "mapping", "fireball" }
|
local effects = { "teleport", "identify", "mapping", "fireball" }
|
||||||
return Items.scroll(effects[rng:range(1, #effects)])
|
return Items.scroll(effects[rng:range(1, #effects)])
|
||||||
elseif roll <= 60 then
|
elseif roll <= 54 then
|
||||||
local base = weaponBases[rng:range(1, #weaponBases)]
|
local base = weaponBases[rng:range(1, #weaponBases)]
|
||||||
return Affixes.decorate(Items.weapon(base.name, base.power), Affixes.rollWeapon(rng))
|
return Affixes.decorate(Items.weapon(base.name, base.power), Affixes.rollWeapon(rng))
|
||||||
elseif roll <= 76 then
|
elseif roll <= 68 then
|
||||||
local base = armorBases[rng:range(1, #armorBases)]
|
local base = armorBases[rng:range(1, #armorBases)]
|
||||||
return Affixes.decorate(Items.armor(base.name, base.power), Affixes.rollArmor(rng))
|
return Affixes.decorate(Items.armor(base.name, base.power), Affixes.rollArmor(rng))
|
||||||
elseif roll <= 86 then
|
elseif roll <= 76 then
|
||||||
return Items.ring("ring of power", rng:range(0, 1), rng:range(0, 1))
|
return Items.ring("ring of power", rng:range(0, 1), rng:range(0, 1))
|
||||||
|
elseif roll <= 80 then
|
||||||
|
local learnable = { "heal", "shield", "blink", "shock", "discord", "polymorph", "confuse", "blind", "levitate", "weaken", "cleanse" }
|
||||||
|
return Items.tome(learnable[rng:range(1, #learnable)])
|
||||||
|
elseif roll <= 84 then
|
||||||
|
local base = bowBases[rng:range(1, #bowBases)]
|
||||||
|
return Items.bow(base.name, base.power, base.range)
|
||||||
|
elseif roll <= 92 then
|
||||||
|
return Items.ammo("arrow", rng:range(4, 8))
|
||||||
else
|
else
|
||||||
return Items.food("ration", 400)
|
return Items.food("ration", 400)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- Maps a floor item's kind to the wearable slot curses.lua knows how to
|
||||||
|
-- afflict, so the dead-last curse pass below can find every eligible piece
|
||||||
|
-- regardless of which earlier system placed it (a plain drop, a set piece,
|
||||||
|
-- vault treasure, ...).
|
||||||
|
local slotForCursableItem = {
|
||||||
|
[Items.WEAPON] = "weapon", [Items.ARMOR] = "armor",
|
||||||
|
[Items.RING] = "ring", [Items.BOW] = "bow",
|
||||||
|
}
|
||||||
|
|
||||||
local function chosenGenerator(depth, options)
|
local function chosenGenerator(depth, options)
|
||||||
if options and options.generator and Gen.has(options.generator) then
|
if options and options.generator and Gen.has(options.generator) then
|
||||||
return options.generator
|
return options.generator
|
||||||
@@ -175,7 +243,11 @@ function Builder.build(depth, rng, options)
|
|||||||
end)
|
end)
|
||||||
if x then
|
if x then
|
||||||
local spec = eligible[rng:range(1, #eligible)]
|
local spec = eligible[rng:range(1, #eligible)]
|
||||||
entities[#entities + 1] = Monsters.create(spec, #entities + 1, x, y, depth)
|
local monster = Monsters.create(spec, #entities + 1, x, y, depth)
|
||||||
|
-- A wild monster starts asleep (see stealth.lua): a plain field set,
|
||||||
|
-- never an rng draw, so it cannot perturb any placement roll below.
|
||||||
|
monster.awareness = Stealth.DORMANT
|
||||||
|
entities[#entities + 1] = monster
|
||||||
mark(x, y)
|
mark(x, y)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -189,7 +261,56 @@ function Builder.build(depth, rng, options)
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
local trapEffects = { "dart", "poison", "teleport" }
|
-- Loose gold piles and the shopkeeper (below) are both gated to depth 4+:
|
||||||
|
-- the shopkeeper joins Monsters.forDepth's eligible array from that depth
|
||||||
|
-- on, and any new rng draw here would otherwise reshuffle the trap/light/
|
||||||
|
-- amulet placement the fixed-maxDepth win-condition tests rely on for the
|
||||||
|
-- shallow floors they actually exercise.
|
||||||
|
if depth >= 4 then
|
||||||
|
local goldCount = rng:range(1, 3)
|
||||||
|
for _ = 1, goldCount do
|
||||||
|
local x, y = pickFree(reachable, rng, freeFloor)
|
||||||
|
if x then
|
||||||
|
floorItems[#floorItems + 1] = { x = x, y = y, item = Items.gold(rng:range(8, 20) + depth * 4) }
|
||||||
|
mark(x, y)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- A shopkeeper occasionally sets up on a floor, guarding a handful of
|
||||||
|
-- priced wares. Gold to spend there comes from the loose piles above.
|
||||||
|
if depth >= 4 and rng:chance(1, 3) then
|
||||||
|
local sx, sy = pickFree(reachable, rng, freeFloor)
|
||||||
|
if sx then
|
||||||
|
local keeper = Monsters.create(Monsters.byKey.shopkeeper, #entities + 1, sx, sy, depth)
|
||||||
|
entities[#entities + 1] = keeper
|
||||||
|
mark(sx, sy)
|
||||||
|
for _, item in ipairs(Shop.stock(rng)) do
|
||||||
|
local x, y = pickFree(reachable, rng, freeFloor)
|
||||||
|
if x then
|
||||||
|
floorItems[#floorItems + 1] = {
|
||||||
|
x = x, y = y, item = item, price = Shop.priceOf(item), shopId = keeper.id,
|
||||||
|
}
|
||||||
|
mark(x, y)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Shrines join the shop above at depth 4+, for the same reason: an earlier
|
||||||
|
-- rng draw would reshuffle the trap/light/amulet placement the fixed-
|
||||||
|
-- maxDepth win-condition tests rely on for the shallow floors they exercise.
|
||||||
|
local shrines = {}
|
||||||
|
if depth >= 4 and rng:chance(1, 4) then
|
||||||
|
local x, y = pickFree(reachable, rng, freeFloor)
|
||||||
|
if x then
|
||||||
|
local kind = Shrines.order[rng:range(1, #Shrines.order)]
|
||||||
|
shrines[#shrines + 1] = { x = x, y = y, kind = kind, uses = 0 }
|
||||||
|
mark(x, y)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local trapEffects = Builder.TRAP_EFFECTS
|
||||||
local trapCount = rng:range(1, 3) + depth // 3
|
local trapCount = rng:range(1, 3) + depth // 3
|
||||||
for _ = 1, trapCount do
|
for _ = 1, trapCount do
|
||||||
local x, y = pickFree(reachable, rng, freeFloor)
|
local x, y = pickFree(reachable, rng, freeFloor)
|
||||||
@@ -199,6 +320,14 @@ function Builder.build(depth, rng, options)
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
if depth == Builder.maxDepth(options) then
|
||||||
|
local x, y = pickFree(reachable, rng, freeFloor)
|
||||||
|
if x then
|
||||||
|
floorItems[#floorItems + 1] = { x = x, y = y, item = Items.amulet() }
|
||||||
|
mark(x, y)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
local dark = depth >= 3
|
local dark = depth >= 3
|
||||||
if dark then
|
if dark then
|
||||||
for _ = 1, rng:range(1, 3) do
|
for _ = 1, rng:range(1, 3) do
|
||||||
|
|||||||
Reference in new issue
Block a user