feat: extend game with Game:autoExplore and Game:restUntilHealed
This commit is contained in:
1 parent
e850ef2d5e
commit
5f13d7012e
1 file changed
+198
+198
@@ -2424,7 +2424,201 @@ function Game:manualStep(text)
|
|||||||
self:endPlayerTurn()
|
self:endPlayerTurn()
|
||||||
return true, nil
|
return true, nil
|
||||||
end
|
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()
|
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
|
end
|
||||||
|
|
||||||
function Game:render()
|
function Game:render()
|
||||||
@@ -2436,6 +2630,10 @@ function Game:toData()
|
|||||||
seed = self.seed,
|
seed = self.seed,
|
||||||
depth = self.depth,
|
depth = self.depth,
|
||||||
turn = self.turn,
|
turn = self.turn,
|
||||||
|
won = self.won,
|
||||||
|
monsterSeq = self.monsterSeq,
|
||||||
|
deepestDepth = self.deepestDepth,
|
||||||
|
stats = self.stats,
|
||||||
rng = self.rng:serialize(),
|
rng = self.rng:serialize(),
|
||||||
identify = self.identify,
|
identify = self.identify,
|
||||||
dungeon = self.dungeon:toData(),
|
dungeon = self.dungeon:toData(),
|
||||||
|
|||||||
Reference in new issue
Block a user