feat: extend game with Game:checkTrapFor and Game:fallIntoChasm
This commit is contained in:
1 parent
d04add4a7b
commit
928f1e6a9e
1 file changed
+173
-3
+173
-3
@@ -467,6 +467,81 @@ local function blockedForMonster(game, self, goalX, goalY)
|
||||
end
|
||||
end
|
||||
|
||||
-- Springs whatever untouched trap sits under `entity` after it has just
|
||||
-- moved there - a player-planted trap kit harms a monster or companion
|
||||
-- exactly as a builder-placed one harms the player, since both live in the
|
||||
-- same level.traps list and share springTrap's resolution. A levitating
|
||||
-- entity (Status.LEVITATE) drifts clear of the pressure plate entirely -
|
||||
-- deliberately silent and without marking the trap revealed, unlike the
|
||||
-- lava/chasm skip in checkTerrainFor: a hidden trap is information the
|
||||
-- player never earned just by floating over it, so levitation buys safety
|
||||
-- without also buying detection.
|
||||
function Game:checkTrapFor(entity)
|
||||
if Status.has(entity, Status.LEVITATE) then return end
|
||||
local trap = self.level:trapAt(entity.x, entity.y)
|
||||
if trap and not trap.revealed then
|
||||
self:springTrap(trap, entity)
|
||||
if not entity:isAlive() then self:onActorDeath(entity) end
|
||||
end
|
||||
end
|
||||
|
||||
-- A pit with no floor below it: the player falls to the next depth and
|
||||
-- takes fall damage, a monster that blunders in is simply gone. Builder
|
||||
-- never seeds a chasm on the final floor, so there is always a level below
|
||||
-- to land on.
|
||||
function Game:fallIntoChasm(entity)
|
||||
if entity == self.player then
|
||||
local dealt = 6 + self.rng:range(0, 6)
|
||||
self.player.hp = self.player.hp - dealt
|
||||
self.log:add(string.format("the floor gives way - you plunge into the dark for %d", dealt))
|
||||
if not self.player:isAlive() then return end
|
||||
self.depth = self.depth + 1
|
||||
self.level = self.dungeon:get(self.depth)
|
||||
local x, y = self:randomFloorCell()
|
||||
if not x then x, y = self.level.upX, self.level.upY end
|
||||
self.player.x, self.player.y = x, y
|
||||
if self.depth > self.deepestDepth then self.deepestDepth = self.depth end
|
||||
self:computeVisible()
|
||||
else
|
||||
entity.hp = 0
|
||||
self.log:add(string.format("%s plunges into the chasm and is gone", entity.name))
|
||||
self:onActorDeath(entity)
|
||||
end
|
||||
end
|
||||
|
||||
-- Runs after entity has just come to rest on (dx, dy) from its previous
|
||||
-- cell: ice keeps sliding it onward, lava burns it in place, and a chasm
|
||||
-- ends the move altogether. Order matters - a slide can carry a traveller
|
||||
-- straight off the ice and into whatever waits beyond it. A levitating
|
||||
-- entity (Status.LEVITATE) never touches the ground at all, so it short
|
||||
-- circuits every branch below - no slide, no burn, no fall - though it
|
||||
-- still gets a log line when that skip actually mattered (standing over
|
||||
-- lava or a chasm), since both are visible on the map and worth calling out.
|
||||
function Game:checkTerrainFor(entity, dx, dy)
|
||||
if not entity:isAlive() then return end
|
||||
if Status.has(entity, Status.LEVITATE) then
|
||||
if self.level.map:get(entity.x, entity.y) == Tiles.LAVA or Terrain.isChasm(self.level, entity) then
|
||||
self.log:add(entity == self.player and "you drift safely over the hazard below"
|
||||
or string.format("%s drifts safely over the hazard below", entity.name))
|
||||
end
|
||||
return
|
||||
end
|
||||
if self.level.map:get(entity.x, entity.y) == Tiles.ICE then
|
||||
Terrain.slide(self.level, entity, dx, dy)
|
||||
end
|
||||
local line = Terrain.afflictLava(self.level, entity)
|
||||
if line then
|
||||
self.log:add(line)
|
||||
if not entity:isAlive() then
|
||||
if entity ~= self.player then self:onActorDeath(entity) end
|
||||
return
|
||||
end
|
||||
end
|
||||
if Terrain.isChasm(self.level, entity) then
|
||||
self:fallIntoChasm(entity)
|
||||
end
|
||||
end
|
||||
|
||||
function Game:stepMonsterToward(monster, tx, ty)
|
||||
local path = Astar.find(self.level.map, monster.x, monster.y, tx, ty,
|
||||
blockedForMonster(self, monster, tx, ty))
|
||||
@@ -474,7 +549,10 @@ function Game:stepMonsterToward(monster, tx, ty)
|
||||
local step = path[2]
|
||||
if self.level.map:isWalkable(step.x, step.y) and not self.level:entityAt(step.x, step.y)
|
||||
and not (step.x == self.player.x and step.y == self.player.y) then
|
||||
local dx, dy = step.x - monster.x, step.y - monster.y
|
||||
monster.x, monster.y = step.x, step.y
|
||||
self:checkTrapFor(monster)
|
||||
self:checkTerrainFor(monster, dx, dy)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -494,15 +572,89 @@ function Game:stepMonsterAway(monster, tx, ty)
|
||||
end
|
||||
end
|
||||
end
|
||||
if bx then monster.x, monster.y = bx, by end
|
||||
if bx then
|
||||
local dx, dy = bx - monster.x, by - monster.y
|
||||
monster.x, monster.y = bx, by
|
||||
self:checkTrapFor(monster)
|
||||
self:checkTerrainFor(monster, dx, dy)
|
||||
end
|
||||
end
|
||||
|
||||
-- An erratic wanderer's random step or a confused chaser's overridden one
|
||||
-- (Ai.act, both routed here) can land on an ally's cell exactly as easily as
|
||||
-- the player's - Infighting.strike resolves that the same generic way
|
||||
-- Game:adjacentAlly's own callers do, rather than the bump silently doing
|
||||
-- nothing the way it would against an impassable wall.
|
||||
function Game:moveMonster(monster, dx, dy)
|
||||
local nx, ny = monster.x + dx, monster.y + dy
|
||||
if nx == self.player.x and ny == self.player.y then
|
||||
self:monsterMelee(monster)
|
||||
elseif self.level.map:isWalkable(nx, ny) and not self.level:entityAt(nx, ny) then
|
||||
return
|
||||
end
|
||||
local occupant = self.level:entityAt(nx, ny)
|
||||
if occupant and (occupant.kind == "companion" or occupant.kind == "hireling" or occupant.kind == "thrall") then
|
||||
Infighting.strike(self, monster, occupant)
|
||||
return
|
||||
end
|
||||
if self.level.map:isWalkable(nx, ny) and not occupant then
|
||||
monster.x, monster.y = nx, ny
|
||||
self:checkTrapFor(monster)
|
||||
self:checkTerrainFor(monster, dx, dy)
|
||||
end
|
||||
end
|
||||
|
||||
-- Mirrors ai.lua's own Ai.act confusion stumble for the three player-aligned
|
||||
-- actor kinds (companion/hireling/thrall). None of those ever dispatch
|
||||
-- through Ai.act, so without this they would leave Status.CONFUSE completely
|
||||
-- inert on them despite Game:springTrap and friends already applying it to
|
||||
-- "any victim" regardless of kind. Returns true whenever the roll actually
|
||||
-- fires (whether or not the stumble itself finds anywhere to go), so each
|
||||
-- caller can `if game:allyActsConfused(ally) then return end` first thing,
|
||||
-- exactly the precedence Ai.act gives the same roll over chase/pack/etc.
|
||||
-- Unlike a confused monster's stumble (moveMonster above, which treats a
|
||||
-- step onto the player's cell as an attack), a confused ally's stumble onto
|
||||
-- its own master's cell is harmless - an ally never attacks the player it is
|
||||
-- bonded or hired to protect, so that one direction is simply blocked like a
|
||||
-- wall rather than routed into any melee helper.
|
||||
function Game:allyActsConfused(ally)
|
||||
if not Status.has(ally, Status.CONFUSE) then return false end
|
||||
if not self.rng:chance(Status.CONFUSE_NUMERATOR, Status.CONFUSE_DENOMINATOR) then return false end
|
||||
local dir = MONSTER_DIRS[self.rng:range(1, #MONSTER_DIRS)]
|
||||
local nx, ny = ally.x + dir[1], ally.y + dir[2]
|
||||
if not (nx == self.player.x and ny == self.player.y)
|
||||
and self.level.map:isWalkable(nx, ny) and not self.level:entityAt(nx, ny) then
|
||||
ally.x, ally.y = nx, ny
|
||||
self:checkTrapFor(ally)
|
||||
self:checkTerrainFor(ally, dir[1], dir[2])
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
-- A burrower's turn when it can neither reach nor see the player through
|
||||
-- open floor: senses the player's general direction through solid rock out
|
||||
-- to Digging.SENSE_RANGE away and, of the two candidate cells toward them
|
||||
-- (longer axis first - see Digging.directionsToward), either claws one
|
||||
-- swing into the first one that is still diggable, exactly like Game:dig
|
||||
-- but toolless and self-directed, or - if open floor already leads that
|
||||
-- way - simply steps there so it closes the distance before it ever needs
|
||||
-- to swing at all. Idles once out of range or once neither candidate cell
|
||||
-- is diggable nor walkable, the same way "flee" idles once it loses sight.
|
||||
function Game:monsterDig(monster, tx, ty)
|
||||
local cheb = math.max(math.abs(monster.x - tx), math.abs(monster.y - ty))
|
||||
if cheb == 0 or cheb > Digging.SENSE_RANGE then return end
|
||||
for _, dir in ipairs(Digging.directionsToward(monster.x, monster.y, tx, ty)) do
|
||||
local nx, ny = monster.x + dir[1], monster.y + dir[2]
|
||||
if Digging.canDig(self.level.map, nx, ny) then
|
||||
self.log:add(Digging.swing(self, monster, nx, ny))
|
||||
if not monster:isAlive() then self:onActorDeath(monster) end
|
||||
return
|
||||
elseif self.level.map:isWalkable(nx, ny) and not self.level:entityAt(nx, ny)
|
||||
and not (nx == self.player.x and ny == self.player.y) then
|
||||
monster.x, monster.y = nx, ny
|
||||
self:checkTrapFor(monster)
|
||||
self:checkTerrainFor(monster, dir[1], dir[2])
|
||||
return
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -519,10 +671,28 @@ function Game:nearestAlly(monster)
|
||||
return ally
|
||||
end
|
||||
|
||||
-- A monster's death sweeps xp and essence to the player; a companion fights
|
||||
-- for the player rather than yielding a reward for dying, so its death is
|
||||
-- only logged and swept.
|
||||
function Game:onActorDeath(actor)
|
||||
if actor.kind == "companion" then
|
||||
self.log:add("your " .. actor.name .. " falls")
|
||||
self:cleanupDead()
|
||||
elseif actor.kind == "hireling" then
|
||||
self.log:add("your " .. actor.name .. " falls")
|
||||
self:cleanupDead()
|
||||
elseif actor.kind == "thrall" then
|
||||
self.log:add("your " .. actor.name .. " crumbles back to bones")
|
||||
self:cleanupDead()
|
||||
else
|
||||
self:awardKill(actor)
|
||||
end
|
||||
end
|
||||
|
||||
function Game:takeMonsterTurn(monster)
|
||||
self.log:addAll(Status.tick(monster))
|
||||
if not monster:isAlive() then
|
||||
self:cleanupDead()
|
||||
self:onActorDeath(monster)
|
||||
return
|
||||
end
|
||||
if not Status.skipsTurn(monster) then
|
||||
|
||||
Reference in new issue
Block a user