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