feat: add Digging.canDig, Digging.swing and Disarm.sweep
This commit is contained in:
1 parent
bdf3624b02
commit
c214f5c127
2 files changed
+195
No files matched your search
+113
@@ -0,0 +1,113 @@
|
||||
-- Tunneling through plain rock, player-driven with a limited-use pick and
|
||||
-- monster-driven for a burrower born to it (see monsters.lua's "burrower"
|
||||
-- species and ai.lua's "tunnel" behaviour). A dug wall never clears in one
|
||||
-- swing: it crumbles to Tiles.RUBBLE first - already walkable and already
|
||||
-- open to sight, unlike the wall it replaced - and only a second swing
|
||||
-- against that same cell clears it the rest of the way to open floor, so a
|
||||
-- tunnel always costs two hits, never one, whoever is swinging. A vault's
|
||||
-- own boundary (Tiles.VAULT_DOOR) never yields no matter how many swings it
|
||||
-- takes - only the lever it guards opens it (see Game:pullLever). Every
|
||||
-- swing risks a cave-in that buries the digger for damage instead of
|
||||
-- yielding progress, and a lucky one strikes a raw vein outright - clearing
|
||||
-- the wall in a single stroke and paying out essence exactly like a spent
|
||||
-- nodes.lua vein, but only for a digger that tracks essence at all (the
|
||||
-- player; a no-op reward for anything else, i.e. a monster).
|
||||
-- Digging mutates the map tile grid directly, exactly like terrain.lua's
|
||||
-- hazards bake straight into it, so a dug tunnel needs no special
|
||||
-- save-format handling of its own - the map already serializes.
|
||||
|
||||
local Tiles = require("tiles")
|
||||
|
||||
local Digging = {}
|
||||
|
||||
Digging.CAVE_IN_CHANCE_DEN = 8
|
||||
Digging.CAVE_IN_DAMAGE_MIN = 2
|
||||
Digging.CAVE_IN_DAMAGE_MAX = 5
|
||||
Digging.ORE_STRIKE_CHANCE_DEN = 10
|
||||
Digging.ORE_ESSENCE_MIN = 4
|
||||
Digging.ORE_ESSENCE_MAX = 9
|
||||
Digging.DEFAULT_USES = 20
|
||||
|
||||
-- A burrower senses the player's direction straight through solid rock out
|
||||
-- to this many Chebyshev cells - beyond it, it idles exactly like "flee"
|
||||
-- idles once it loses sight, rather than clawing forever from across the map.
|
||||
Digging.SENSE_RANGE = 6
|
||||
|
||||
-- Only a plain wall or the rubble left by a prior swing can be dug further;
|
||||
-- open floor has nothing left to carve and a vault door never yields.
|
||||
function Digging.canDig(map, x, y)
|
||||
if not map:inBounds(x, y) then return false end
|
||||
local tile = map:get(x, y)
|
||||
return tile == Tiles.WALL or tile == Tiles.RUBBLE
|
||||
end
|
||||
|
||||
-- Resolves one swing of `digger`'s pick (or a burrower's own claws) against
|
||||
-- (x, y), already known diggable by the caller. Mutates the map tile in
|
||||
-- place on progress, deals cave-in damage to the digger on bad luck, and
|
||||
-- always returns a log line describing what happened.
|
||||
function Digging.swing(game, digger, x, y)
|
||||
local map = game.level.map
|
||||
local tile = map:get(x, y)
|
||||
local isPlayer = digger == game.player
|
||||
if game.rng:chance(1, Digging.CAVE_IN_CHANCE_DEN) then
|
||||
local dealt = game.rng:range(Digging.CAVE_IN_DAMAGE_MIN, Digging.CAVE_IN_DAMAGE_MAX)
|
||||
digger.hp = digger.hp - dealt
|
||||
if isPlayer then return string.format("the rock groans and buries you for %d", dealt) end
|
||||
return string.format("the rock groans and buries %s for %d", digger.name, dealt)
|
||||
end
|
||||
if tile == Tiles.WALL then
|
||||
if game.rng:chance(1, Digging.ORE_STRIKE_CHANCE_DEN) then
|
||||
map:set(x, y, Tiles.FLOOR)
|
||||
if digger.essence ~= nil then
|
||||
local amount = game.rng:range(Digging.ORE_ESSENCE_MIN, Digging.ORE_ESSENCE_MAX)
|
||||
digger.essence = digger.essence + amount
|
||||
return string.format("you strike a vein and pocket %d essence", amount)
|
||||
end
|
||||
return string.format("%s claws straight through a vein of ore", digger.name)
|
||||
end
|
||||
map:set(x, y, Tiles.RUBBLE)
|
||||
if isPlayer then return "you carve through the rock" end
|
||||
return string.format("%s carves through the rock", digger.name)
|
||||
end
|
||||
map:set(x, y, Tiles.FLOOR)
|
||||
if isPlayer then return "you clear the rubble away" end
|
||||
return string.format("%s clears the rubble away", digger.name)
|
||||
end
|
||||
|
||||
-- Consumes one use from `entity.pick`'s durability, discarding the tool
|
||||
-- once its edge dulls past use. A no-op (returns nil) for anything without
|
||||
-- a pick equipped - a burrower digs with its own claws instead, which never
|
||||
-- wear out. Returns a log line only on the swing that breaks the tool.
|
||||
function Digging.wearPick(entity)
|
||||
local pick = entity.pick
|
||||
if not pick then return nil end
|
||||
pick.uses = pick.uses - 1
|
||||
if pick.uses <= 0 then
|
||||
entity.pick = nil
|
||||
return "your " .. pick.name .. " crumbles apart from the strain"
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
-- Orthogonal step directions from (mx, my) toward (tx, ty), longer axis
|
||||
-- first, in the same 4-connected grid every monster already moves and
|
||||
-- paths on (see game.lua's MONSTER_DIRS, astar.lua's DIRS) - never a
|
||||
-- diagonal, so a dug tunnel is always walkable by the same step logic that
|
||||
-- carved it. Returns an empty list when already on the target cell.
|
||||
function Digging.directionsToward(mx, my, tx, ty)
|
||||
local ddx = tx - mx
|
||||
local ddy = ty - my
|
||||
local dx = ddx > 0 and 1 or (ddx < 0 and -1 or 0)
|
||||
local dy = ddy > 0 and 1 or (ddy < 0 and -1 or 0)
|
||||
local dirs = {}
|
||||
if math.abs(ddx) >= math.abs(ddy) then
|
||||
if dx ~= 0 then dirs[#dirs + 1] = { dx, 0 } end
|
||||
if dy ~= 0 then dirs[#dirs + 1] = { 0, dy } end
|
||||
else
|
||||
if dy ~= 0 then dirs[#dirs + 1] = { 0, dy } end
|
||||
if dx ~= 0 then dirs[#dirs + 1] = { dx, 0 } end
|
||||
end
|
||||
return dirs
|
||||
end
|
||||
|
||||
return Digging
|
||||
@@ -0,0 +1,82 @@
|
||||
-- Passive trap sense and the deliberate act of disarming what it finds.
|
||||
-- Every trap already carries a hidden/revealed pair (level.traps entries -
|
||||
-- see level.lua and game.lua's Game:springTrap, which flips a trap's
|
||||
-- `revealed` the instant it is triggered, whether by a footstep or a failed
|
||||
-- disarm below). This module adds a third state in between the two,
|
||||
-- `detected`: noticed but not yet sprung. Detection is a passive roll
|
||||
-- against every still-hidden trap within a step of the player, rolled once
|
||||
-- per player turn from Game:endPlayerTurn - once per round rather than once
|
||||
-- per actor, exactly like hazards.step and events.step already advance the
|
||||
-- world. Disarming a detected trap is the deliberate, risky counterpart:
|
||||
-- succeed and it is gone for good, plus a handful of essence for the
|
||||
-- salvaged parts; fail and it springs on the disarmer exactly as if they
|
||||
-- had blundered onto it outright, through the very same Game:springTrap
|
||||
-- every other trigger already shares.
|
||||
|
||||
local Disarm = {}
|
||||
|
||||
Disarm.DETECT_NUMERATOR = 1
|
||||
Disarm.DETECT_DENOMINATOR = 4
|
||||
|
||||
Disarm.DISARM_BASE_NUMERATOR = 3
|
||||
Disarm.DISARM_BASE_DENOMINATOR = 5
|
||||
Disarm.LEVEL_BONUS_DIVISOR = 4
|
||||
|
||||
Disarm.SALVAGE_MIN = 3
|
||||
Disarm.SALVAGE_MAX = 6
|
||||
|
||||
-- Sweeps every trap within Chebyshev distance 1 of (px, py) - the same
|
||||
-- adjacency a monster's melee reach and a shrine's own footprint are judged
|
||||
-- by elsewhere - that is neither already detected nor already sprung, and
|
||||
-- rolls each independently. Returns the traps newly detected this call (in
|
||||
-- level.traps order), so a caller can report exactly what was noticed.
|
||||
function Disarm.sweep(level, rng, px, py)
|
||||
local found = {}
|
||||
for _, trap in ipairs(level.traps) do
|
||||
if not trap.detected and not trap.revealed
|
||||
and math.max(math.abs(trap.x - px), math.abs(trap.y - py)) <= 1
|
||||
and rng:chance(Disarm.DETECT_NUMERATOR, Disarm.DETECT_DENOMINATOR) then
|
||||
trap.detected = true
|
||||
found[#found + 1] = trap
|
||||
end
|
||||
end
|
||||
return found
|
||||
end
|
||||
|
||||
-- Odds of successfully disarming a detected trap: a flat base that climbs
|
||||
-- slightly with player level, capped a point shy of the denominator so
|
||||
-- disarming never becomes a sure thing no matter how experienced the
|
||||
-- player gets.
|
||||
function Disarm.oddsFor(player)
|
||||
local numerator = Disarm.DISARM_BASE_NUMERATOR + player.level // Disarm.LEVEL_BONUS_DIVISOR
|
||||
if numerator > Disarm.DISARM_BASE_DENOMINATOR - 1 then
|
||||
numerator = Disarm.DISARM_BASE_DENOMINATOR - 1
|
||||
end
|
||||
return numerator, Disarm.DISARM_BASE_DENOMINATOR
|
||||
end
|
||||
|
||||
-- Resolves one disarm attempt against `trap`, already known to `game`'s
|
||||
-- player (see Game:disarmTrap, which enforces that it was actually
|
||||
-- detected first). On success the trap is spliced out of level.traps for
|
||||
-- good and its salvage is credited as essence; on failure it springs on the
|
||||
-- player through Game:springTrap, the identical consequence a careless
|
||||
-- footstep would have caused. Returns (true, line) or (false, nil) - a
|
||||
-- failure logs nothing of its own since springTrap already does.
|
||||
function Disarm.attempt(game, trap)
|
||||
local numerator, denominator = Disarm.oddsFor(game.player)
|
||||
if not game.rng:chance(numerator, denominator) then
|
||||
game:springTrap(trap, game.player)
|
||||
return false, nil
|
||||
end
|
||||
for i, candidate in ipairs(game.level.traps) do
|
||||
if candidate == trap then
|
||||
table.remove(game.level.traps, i)
|
||||
break
|
||||
end
|
||||
end
|
||||
local salvage = game.rng:range(Disarm.SALVAGE_MIN, Disarm.SALVAGE_MAX)
|
||||
game.player.essence = game.player.essence + salvage
|
||||
return true, string.format("you carefully disarm the trap, salvaging %d essence", salvage)
|
||||
end
|
||||
|
||||
return Disarm
|
||||
Reference in new issue
Block a user