feat: extend game with Game:autoExplore and Game:restUntilHealed

This commit is contained in:
dualfroz committed 2026-09-06 19:59:51 +02:00
1 parent e850ef2d5e
commit 5f13d7012e
1 file changed
+198
+198
View File
@@ -2424,7 +2424,201 @@ function Game:manualStep(text)
self:endPlayerTurn()
return true, nil
end
local consumesTurn, message = Commands.apply(self, text)
-- "explore" (Game:autoExplore, wired up in commands.lua) can advance and
-- close several turns entirely on its own inside this one call, unlike
-- every other handler here which only ever acts within the single turn
-- already opened above - self.turnState is already nil by the time it
-- returns, so this must not end an already-ended turn a second time - and
-- must never end one at all if the very last thing that happened was a
-- halt (self.turnState == "halt"), exactly like Game:autoStep and this
-- same function's own "halt" branch above never do either. Every other
-- command leaves turnState exactly as beginTurn set it above ("ready"),
-- so the extra check below is a no-op for them.
if consumesTurn and self.turnState == "ready" then self:endPlayerTurn() end
return consumesTurn, message
end
local EXPLORE_STEP_CAP = 2000
-- Repeats Game:exploreStep/Game:movePlayer turn after turn inside a single
-- call, the way autoexplore works in most other roguelikes, instead of
-- making the player retype "explore" once per step. Built on the exact same
-- beginTurn/endPlayerTurn pair Game:autoStep already drives, so monster AI,
-- status ticks and hazards all run normally on every turn it takes - the
-- only difference from a hand-typed run of single moves is that this stops
-- itself the instant something worth the player's attention shows up,
-- rather than walking on regardless. Returns (consumesTurn, message) in the
-- exact same shape every Commands.apply handler does: consumesTurn is true
-- iff at least one turn was actually spent, so "already interrupted before
-- moving at all" and "an ordinary rejected command" both read the same way
-- to a caller that only checks that first value.
function Game:autoExplore()
local steps = 0
while steps < EXPLORE_STEP_CAP do
if self:anyHostileVisible() then
if steps == 0 then return false, "a monster is already in sight" end
return true, "you stop exploring - a monster is in sight"
end
local hpBefore = self.player.hp
local state = self:beginTurn()
if state == "halt" then
if steps == 0 then return false, "there is nothing left to do" end
return true, "you stop exploring"
end
if state == "ready" then
local ex, ey = self:exploreStep()
if not ex then
if steps == 0 then return false, "there is nothing left to explore" end
-- beginTurn above already ran this turn's monster moves and status
-- ticks for real, even though there turns out to be nothing left to
-- walk toward - so, unlike the steps == 0 case above (nothing at
-- all has happened yet, left dangling for a retry within the same
-- still-open turn, exactly like a rejected command), this turn must
-- still be closed out properly rather than abandoned half-finished.
self:endPlayerTurn()
return true, "you have finished exploring"
end
self:movePlayer(ex, ey)
end
self:endPlayerTurn()
steps = steps + 1
if self.player.hp < hpBefore then
return true, "you stop exploring - you have taken damage"
end
end
return true, "you stop exploring - that is far enough for now"
end
local REST_STEP_CAP = 500
-- Repeats a do-nothing turn (Handlers.wait's own move) inside a single call
-- until the player's hp is back to full, the way "rest" works in most other
-- roguelikes, instead of making the player retype "wait" once per turn.
-- Built on the same beginTurn/endPlayerTurn pair Game:autoExplore already
-- drives, so status ticks, hazards and the passive hp regen sources that
-- actually restore hp (a regen ring or artifact, Status.REGEN, favorable
-- weather - see Game:hungerTorchTick and Status.tick) all run exactly as
-- they would turn by turn. Stops itself the instant a hostile comes into
-- sight, the player's hp actually drops, or two full turns in a row pass
-- with the player's hp completely unchanged - the last case catches the
-- common situation where none of those regen sources are active at all, so
-- waiting here would otherwise never do anything but burn turns up to the
-- safety cap below. Returns (consumesTurn, message) in the exact same shape
-- every other handler in commands.lua does, exactly like Game:autoExplore.
--
-- The "no progress" check deliberately waits for a *second* completed turn
-- (steps > 1) rather than judging the very first one: Game:manualStep (the
-- only caller a real player's typed "rest" ever goes through) already calls
-- Game:beginTurn once of its own, before Commands.apply even runs, to check
-- whether the turn is allowed to start at all - so by the time this loop's
-- own first Game:beginTurn call runs, self.turnState may already be primed
-- from that earlier call, and it is a no-op cache hit rather than a fresh
-- tick (see Game:beginTurn's own comment). Any regen this call's Status.tick
-- and Game:hungerTorchTick would otherwise have produced already landed on
-- self.player.hp *before* this function's very first hpBefore snapshot was
-- even taken, so a first-iteration hp==hpBefore reading is inconclusive, not
-- proof that nothing is healing the player - judging it anyway would have
-- reported a perfectly working regen ring as "nothing here is healing you"
-- after a single real turn every time rest is typed in the actual game.
-- Every iteration from the second one on always starts with self.turnState
-- already nil (this loop's own prior Game:endPlayerTurn call clears it), so
-- its own Game:beginTurn call is always a fresh tick there - reliable either
-- way this function is entered, direct or through Game:manualStep.
function Game:restUntilHealed()
if self.player.hp >= self.player.maxHp then
return false, "you are already at full health"
end
local steps = 0
while steps < REST_STEP_CAP do
if self:anyHostileVisible() then
if steps == 0 then return false, "a monster is already in sight" end
return true, "you stop resting - a monster is in sight"
end
local hpBefore = self.player.hp
local state = self:beginTurn()
if state == "halt" then
if steps == 0 then return false, "there is nothing left to do" end
return true, "you stop resting"
end
self:endPlayerTurn()
steps = steps + 1
if self.player.hp >= self.player.maxHp then
return true, "you rest until your wounds close"
end
if self.player.hp < hpBefore then
return true, "you stop resting - you have taken damage"
end
if steps > 1 and self.player.hp == hpBefore then
return true, "you stop resting - nothing here is healing you"
end
end
return true, "you stop resting - that is far enough for now"
end
local TRAVEL_STEP_CAP = 500
-- Repeats Game:playerPathTo/Game:movePlayer turn after turn toward a fixed
-- destination inside a single call, the way "travel"/autotravel works in
-- most other roguelikes (DCSS's `>`, Brogue's travel cursor) - instead of
-- making the player retype a direction once per step to walk back to a spot
-- they already know is safe (the stairs they just left behind). Built on the
-- exact same beginTurn/endPlayerTurn pair Game:autoExplore/Game:restUntilHealed
-- already drive, so monster AI, status ticks and hazards all run normally on
-- every turn it takes. Reuses Game:playerPathTo, the same pathfinder
-- choosePlayerAction already relies on to walk the autopilot toward the
-- stairs turn by turn - this only exposes that existing primitive to a typed
-- command instead of teaching a second way to path anywhere. Stops itself
-- the instant a hostile comes into sight, the player's hp actually drops, the
-- destination is reached, or the route becomes blocked (a monster or a
-- spawned block stepped onto the only way through). Returns (consumesTurn,
-- message) in the exact same shape every other Commands.apply handler does.
function Game:travelTo(tx, ty)
if self.player.x == tx and self.player.y == ty then
return false, "you are already there"
end
if not self:playerPathTo(tx, ty) then
return false, "you cannot find a path there"
end
local steps = 0
while steps < TRAVEL_STEP_CAP do
if self:anyHostileVisible() then
if steps == 0 then return false, "a monster is already in sight" end
return true, "you stop traveling - a monster is in sight"
end
local hpBefore = self.player.hp
local state = self:beginTurn()
if state == "halt" then
if steps == 0 then return false, "there is nothing left to do" end
return true, "you stop traveling"
end
if state == "ready" then
local path = self:playerPathTo(tx, ty)
if not path or #path < 2 then
-- beginTurn above already ran this turn's monster moves and status
-- ticks for real (see Game:autoExplore's own "nothing left to
-- explore" branch for the identical reasoning), so unlike the
-- steps == 0 case below - where nothing has happened yet and the
-- turn is left open for a retry, exactly like a rejected command -
-- a route that only went dead after steps have already been taken
-- must close out the turn that already ran rather than abandon it
-- half-finished.
if steps == 0 then return false, "you cannot find a path there" end
self:endPlayerTurn()
return true, "you stop traveling - the way is blocked"
end
self:movePlayer(path[2].x - self.player.x, path[2].y - self.player.y)
end
self:endPlayerTurn()
steps = steps + 1
if self.player.x == tx and self.player.y == ty then
return true, "you arrive"
end
if self.player.hp < hpBefore then
return true, "you stop traveling - you have taken damage"
end
end
return true, "you stop traveling - that is far enough for now"
end
function Game:render()
@@ -2436,6 +2630,10 @@ function Game:toData()
seed = self.seed,
depth = self.depth,
turn = self.turn,
won = self.won,
monsterSeq = self.monsterSeq,
deepestDepth = self.deepestDepth,
stats = self.stats,
rng = self.rng:serialize(),
identify = self.identify,
dungeon = self.dungeon:toData(),