feat: extend game with Game:anyHostileVisible
This commit is contained in:
1 parent
7e20884d3d
commit
b4e2c7a431
1 file changed
+197
-7
+197
-7
@@ -2040,6 +2040,22 @@ function Game:anyMonsterVisible()
|
||||
return false
|
||||
end
|
||||
|
||||
-- Only true hostiles (isPlayerAlly's same companion/hireling/thrall
|
||||
-- allowlist that Game:movePlayer's own bump-attack already respects) -
|
||||
-- unlike Game:anyMonsterVisible above, which choosePlayerAction uses to gate
|
||||
-- inventory downtime and deliberately treats ANY visible entity as "not
|
||||
-- safe to fiddle right now". Game:autoExplore needs the narrower question:
|
||||
-- a loyal ally standing in plain sight next to the player it always follows
|
||||
-- must never interrupt an autoexplore run on its own.
|
||||
function Game:anyHostileVisible()
|
||||
for _, entity in ipairs(self.level.entities) do
|
||||
if not isPlayerAlly(entity) and self.visible[self.level.map:index(entity.x, entity.y)] then
|
||||
return true
|
||||
end
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
-- Inventory index of the single best equipment upgrade to put on this turn, or
|
||||
-- nil. Weapons and armour compare on power, rings on attack plus defence.
|
||||
function Game:bestUpgrade()
|
||||
@@ -2059,11 +2075,135 @@ function Game:bestUpgrade()
|
||||
return nil
|
||||
end
|
||||
|
||||
-- The first not-yet-known skill of the player's class whose level requirement
|
||||
-- is already met, or nil. Learning costs no turn, so the autopilot spends
|
||||
-- any banked skill point the moment it can, in the tree's fixed order.
|
||||
function Game:nextLearnableSkill()
|
||||
local player = self.player
|
||||
if not player.class or (player.skillPoints or 0) <= 0 then return nil end
|
||||
for _, key in ipairs(Skills.forClass(player.class)) do
|
||||
if not Skills.has(player, key) then
|
||||
local def = Skills.active[key] or Skills.passive[key]
|
||||
if player.level >= def.reqLevel then return key end
|
||||
end
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
function Game:hasCompanion()
|
||||
for _, entity in ipairs(self.level.entities) do
|
||||
if entity.kind == "companion" and entity:isAlive() then return true end
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
function Game:hasHireling()
|
||||
for _, entity in ipairs(self.level.entities) do
|
||||
if entity.kind == "hireling" and entity:isAlive() then return true end
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
-- Voluntary, penalty-free release of the live hireling, if any - contrast
|
||||
-- Mercenaries.payWage's desertion, which fires on its own once loyalty
|
||||
-- bottoms out from missed wages rather than on the player's own say-so.
|
||||
function Game:dismissHireling()
|
||||
local name = Mercenaries.dismiss(self)
|
||||
if not name then
|
||||
self.log:add("you have no one under contract to release")
|
||||
return false
|
||||
end
|
||||
self.log:add("you release your " .. name .. " from service")
|
||||
return true
|
||||
end
|
||||
|
||||
function Game:choosePlayerAction()
|
||||
local player = self.player
|
||||
local learnable = self:nextLearnableSkill()
|
||||
if learnable then self:learnSkill(learnable) end
|
||||
if not player.quest then
|
||||
local board = self.level:questBoardAt(player.x, player.y)
|
||||
if board and not board.claimed then self:tryAcceptQuest(board) end
|
||||
end
|
||||
if not self:anyMonsterVisible() then
|
||||
local upgrade = self:bestUpgrade()
|
||||
if upgrade then self:useItem(upgrade); return end
|
||||
if not self:hasCompanion() then
|
||||
local charm = self:findInInventory(function(item) return item.kind == Items.CHARM end)
|
||||
if charm then self:useItem(charm); return end
|
||||
end
|
||||
if not self:hasHireling() then
|
||||
local contract = self:findInInventory(function(item) return item.kind == Items.CONTRACT end)
|
||||
if contract then self:useItem(contract); return end
|
||||
end
|
||||
-- Only ever risks a shrine's first, cheapest, least cursed-prone prayer:
|
||||
-- the autopilot is not greedy enough to keep re-praying at rising odds.
|
||||
local shrine = self.level:shrineAt(player.x, player.y)
|
||||
if shrine and shrine.uses == 0 and player.gold >= Shrines.costFor(shrine) then
|
||||
self:prayAt(shrine)
|
||||
return
|
||||
end
|
||||
-- Restores whatever gear has actually stopped working the moment the
|
||||
-- purse can cover it, same "take it as soon as it's affordable" spirit
|
||||
-- as the shrine check just above.
|
||||
local brokenSlot = self:brokenEquippedSlot()
|
||||
if brokenSlot and player.gold >= Durability.repairCost(player[brokenSlot]) then
|
||||
self:repairEquipped(brokenSlot)
|
||||
return
|
||||
end
|
||||
-- Free income with no downside, so the autopilot takes it on sight
|
||||
-- rather than weighing it like a shrine's costed, risky prayer.
|
||||
local node = self.level:nodeAt(player.x, player.y)
|
||||
if node and not Nodes.isFallow(node) then
|
||||
self:harvestNode()
|
||||
return
|
||||
end
|
||||
-- Free protein with no gold cost, so the autopilot butchers a corpse on
|
||||
-- sight same as it strip-mines a node - but only while the meat is
|
||||
-- still good; a rotten one it leaves alone rather than poisoning itself
|
||||
-- for nothing.
|
||||
local corpse = self.level:corpseAt(player.x, player.y)
|
||||
if corpse and not Corpses.isRotten(corpse) then
|
||||
self:butcherCorpse()
|
||||
return
|
||||
end
|
||||
-- A chest is worth cracking on sight exactly like the node and corpse
|
||||
-- above, but only once it can actually be: an unlocked one is free
|
||||
-- money, and a locked one is only ever tried with a key or lockpick
|
||||
-- already in hand (see Game:openChest). Skipping a locked chest with
|
||||
-- neither on hand keeps the autopilot from parking on it and burning
|
||||
-- every remaining turn on a lock it has no way to open.
|
||||
local chest = self.level:chestAt(player.x, player.y)
|
||||
if chest and not chest.opened and (not Chests.isLocked(chest) or self:hasKey()
|
||||
or self:findInInventory(function(item) return item.kind == Items.LOCKPICK end)) then
|
||||
self:openChest()
|
||||
return
|
||||
end
|
||||
-- A held captive is worth cashing in on sight exactly like the chest
|
||||
-- above: the autopilot always takes the plain gold of Game:sellCaptive
|
||||
-- rather than gambling on Game:ransomCaptive's goodwill or
|
||||
-- Game:interrogateCaptive's map intel, neither of which it is smart
|
||||
-- enough here to weigh against the other.
|
||||
local captive = self.level:captiveAt(player.x, player.y)
|
||||
if captive then
|
||||
self:sellCaptive()
|
||||
return
|
||||
end
|
||||
-- A rift crossing is only ever attempted at good health: its own pocket
|
||||
-- floor is deliberately tuned harder than the main-track floor the rift
|
||||
-- itself sits on (see branches.lua), so the autopilot never spends its
|
||||
-- one shard gambling into a fight it is not ready for.
|
||||
if player.hp >= player.maxHp * 0.8 and self:enterRift() then
|
||||
return
|
||||
end
|
||||
-- Sheds the cheapest thing it is hauling once genuinely overloaded, but
|
||||
-- only in this same downtime window as the checks above - never instead
|
||||
-- of fighting or fleeing, since a visible monster always skips this
|
||||
-- whole block.
|
||||
if self:encumbranceTier() == Encumbrance.OVERLOADED then
|
||||
local junk = self:cheapestDroppableItem()
|
||||
if junk then self:dropItem(junk); return end
|
||||
end
|
||||
end
|
||||
if player.hp < player.maxHp // 3 then
|
||||
local potion = self:findInInventory(function(item)
|
||||
@@ -2077,14 +2217,60 @@ function Game:choosePlayerAction()
|
||||
end
|
||||
local target = self:adjacentMonster()
|
||||
if target then
|
||||
-- A carried net beats the killing blow once the target is already
|
||||
-- battered down to Captives.eligible's own threshold: a capture forgoes
|
||||
-- this kill's xp and essence, but a live captive can still be cashed in
|
||||
-- for gold moments later (see the sell-on-sight check above), so this is
|
||||
-- a trade the autopilot is always willing to make rather than a strict
|
||||
-- loss.
|
||||
if Captives.eligible(target) then
|
||||
local net = self:findInInventory(function(item) return item.kind == Items.NET end)
|
||||
if net then
|
||||
self:captureAt(net, target)
|
||||
return
|
||||
end
|
||||
end
|
||||
self:movePlayer(sign(target.x - player.x), sign(target.y - player.y))
|
||||
return
|
||||
end
|
||||
if player.x == self.level.downX and player.y == self.level.downY then
|
||||
|
||||
-- Across a rift, the only objective is its own pocket floor's exit: never
|
||||
-- the main track's descend/ascend goals below, which Game:descend and
|
||||
-- Game:ascend both refuse outright for as long as self.rift holds anyway.
|
||||
if self.rift then
|
||||
if player.x == self.level.downX and player.y == self.level.downY then
|
||||
self:exitRift()
|
||||
return
|
||||
end
|
||||
local path = self:playerPathTo(self.level.downX, self.level.downY)
|
||||
if path and #path >= 2 then
|
||||
self:movePlayer(path[2].x - player.x, path[2].y - player.y)
|
||||
return
|
||||
end
|
||||
local hunt = self:reachableMonsterStep()
|
||||
if hunt then
|
||||
self:movePlayer(hunt.x - player.x, hunt.y - player.y)
|
||||
return
|
||||
end
|
||||
local ex, ey = self:exploreStep()
|
||||
if ex then self:movePlayer(ex, ey) end
|
||||
return
|
||||
end
|
||||
|
||||
-- Once the amulet is in hand the objective flips: every remaining step
|
||||
-- heads for the up stairs instead of the down stairs, floor by floor.
|
||||
local goalX, goalY = self.level.downX, self.level.downY
|
||||
if self:hasAmulet() then
|
||||
goalX, goalY = self.level.upX, self.level.upY
|
||||
if player.x == goalX and player.y == goalY then
|
||||
self:ascend()
|
||||
return
|
||||
end
|
||||
elseif self.depth < self.maxDepth and player.x == goalX and player.y == goalY then
|
||||
self:descend()
|
||||
return
|
||||
end
|
||||
local path = self:playerPathTo(self.level.downX, self.level.downY)
|
||||
local path = self:playerPathTo(goalX, goalY)
|
||||
if path and #path >= 2 then
|
||||
self:movePlayer(path[2].x - player.x, path[2].y - player.y)
|
||||
return
|
||||
@@ -2099,11 +2285,15 @@ function Game:choosePlayerAction()
|
||||
end
|
||||
|
||||
function Game:hungerTorchTick()
|
||||
self.player.satiation = self.player.satiation - 1
|
||||
if self.player.satiation <= 0 then
|
||||
self.player.satiation = 0
|
||||
self.player.hp = self.player.hp - 1
|
||||
self.log:add("you are starving")
|
||||
if not Hunger.suppressesRegen(self.player) then
|
||||
local ringRegen = Artifacts.ringRegen(self.player) + Sets.regenAmount(self.player)
|
||||
if ringRegen > 0 then
|
||||
self.player.hp = math.min(self.player.maxHp, self.player.hp + ringRegen)
|
||||
end
|
||||
local weatherRegen = Atmosphere.regenBonus(self.level.weather)
|
||||
if weatherRegen > 0 then
|
||||
self.player.hp = math.min(self.player.maxHp, self.player.hp + weatherRegen)
|
||||
end
|
||||
end
|
||||
if self.player.light.fuel > 0 then
|
||||
self.player.light.fuel = self.player.light.fuel - 1
|
||||
|
||||
Reference in new issue
Block a user