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
|
||||||
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)
|
function Game:stepMonsterToward(monster, tx, ty)
|
||||||
local path = Astar.find(self.level.map, monster.x, monster.y, tx, ty,
|
local path = Astar.find(self.level.map, monster.x, monster.y, tx, ty,
|
||||||
blockedForMonster(self, monster, tx, ty))
|
blockedForMonster(self, monster, tx, ty))
|
||||||
@@ -474,7 +549,10 @@ function Game:stepMonsterToward(monster, tx, ty)
|
|||||||
local step = path[2]
|
local step = path[2]
|
||||||
if self.level.map:isWalkable(step.x, step.y) and not self.level:entityAt(step.x, step.y)
|
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
|
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
|
monster.x, monster.y = step.x, step.y
|
||||||
|
self:checkTrapFor(monster)
|
||||||
|
self:checkTerrainFor(monster, dx, dy)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -494,15 +572,89 @@ function Game:stepMonsterAway(monster, tx, ty)
|
|||||||
end
|
end
|
||||||
end
|
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
|
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)
|
function Game:moveMonster(monster, dx, dy)
|
||||||
local nx, ny = monster.x + dx, monster.y + dy
|
local nx, ny = monster.x + dx, monster.y + dy
|
||||||
if nx == self.player.x and ny == self.player.y then
|
if nx == self.player.x and ny == self.player.y then
|
||||||
self:monsterMelee(monster)
|
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
|
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
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -519,10 +671,28 @@ function Game:nearestAlly(monster)
|
|||||||
return ally
|
return ally
|
||||||
end
|
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)
|
function Game:takeMonsterTurn(monster)
|
||||||
self.log:addAll(Status.tick(monster))
|
self.log:addAll(Status.tick(monster))
|
||||||
if not monster:isAlive() then
|
if not monster:isAlive() then
|
||||||
self:cleanupDead()
|
self:onActorDeath(monster)
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
if not Status.skipsTurn(monster) then
|
if not Status.skipsTurn(monster) then
|
||||||
|
|||||||
Reference in new issue
Block a user