feat: extend game with Game:sellCaptive and Game:interrogateCaptive

This commit is contained in:
dualfroz committed 2026-09-06 16:40:52 +02:00
1 parent b3943d9b6e
commit 6f54f4431d
1 file changed
+181 -10
+181 -10
View File
@@ -1105,6 +1105,61 @@ function Game:ransomCaptive()
return true return true
end end
-- Sells whatever captive stands under the player for gold at its own
-- faction's expense (see Captives.sell). Returns false without state change
-- when there is no captive here.
function Game:sellCaptive()
local captive = self.level:captiveAt(self.player.x, self.player.y)
if not captive then
self.log:add("there is nothing here to sell")
return false
end
local paid = Captives.sell(self.level, self.player, captive)
self.log:add(string.format("you trade %s away for %d gold", captive.name, paid))
return true
end
-- Interrogates whatever captive stands under the player instead of ransoming
-- or selling it (see Captives.interrogate): no gold, no reputation swing,
-- just every currently-hidden secret door on this floor found outright plus
-- a trickle of essence. Returns false without state change when there is no
-- captive here.
function Game:interrogateCaptive()
local captive = self.level:captiveAt(self.player.x, self.player.y)
if not captive then
self.log:add("there is nothing here to interrogate")
return false
end
local revealed = Captives.interrogate(self.level, self.player, captive)
self.log:add(string.format("%s breaks - %d hidden door%s in this floor give themselves away",
captive.name, #revealed, #revealed == 1 and "" or "s"))
return true
end
-- Binds the player to `board`'s bounty for free, logging the pact struck or
-- the reason it could not be. See Quests.accept for the acceptance rules.
function Game:tryAcceptQuest(board)
local success, line = Quests.accept(self.player, board)
self.log:add(line)
return success
end
-- Pays out the player's active bounty the moment it is fulfilled, granting
-- gold, xp and essence in one stroke and freeing the player to bind another.
-- A no-op with no active bounty or one still unmet.
function Game:resolveQuest()
local bounty = self.player.quest
if not bounty or not Quests.resolved(self.player, self.depth) then return false end
self.player.gold = self.player.gold + bounty.rewardGold
self.log:addAll(Progression.grantXp(self.player, bounty.rewardXp))
self.player.essence = self.player.essence + Crafting.essenceFor(bounty.rewardXp)
self.player.quest = nil
self.stats.questsCompleted = self.stats.questsCompleted + 1
Factions.adjust(self.player, Factions.MERCHANT, 3)
self.log:add(string.format("the bounty resolves: %d gold and %d xp", bounty.rewardGold, bounty.rewardXp))
return true
end
function Game:openDoor(x, y) function Game:openDoor(x, y)
local map = self.level.map local map = self.level.map
if map:get(x, y) == Tiles.DOOR_LOCKED then if map:get(x, y) == Tiles.DOOR_LOCKED then
@@ -1124,32 +1179,148 @@ function Game:openDoor(x, y)
return true return true
end end
function Game:triggerTrap(trap) -- Springs `trap` on `victim`, whoever that is: reveals it on the map and
-- resolves its effect against that entity's own stats and position. Kept
-- generic so a player-planted trap kit (Game:useItem's Items.KIT branch)
-- harms a monster exactly as a builder-placed one harms the player.
function Game:springTrap(trap, victim)
trap.revealed = true trap.revealed = true
self.level.map:set(trap.x, trap.y, Tiles.TRAP) self.level.map:set(trap.x, trap.y, Tiles.TRAP)
local isPlayer = victim == self.player
local name = isPlayer and "you" or victim.name
if trap.effect == "dart" then if trap.effect == "dart" then
local dmg = 3 + self.depth // 2 local dmg = 3 + self.depth // 2
self.player.hp = self.player.hp - dmg victim.hp = victim.hp - dmg
self.log:add(string.format("a dart trap hits you for %d", dmg)) self.log:add(isPlayer and string.format("a dart trap hits you for %d", dmg)
or string.format("a dart trap hits %s for %d", name, dmg))
elseif trap.effect == "poison" then elseif trap.effect == "poison" then
Status.apply(self.player, Status.POISON, 5, 2) Status.apply(victim, Status.POISON, 5, 2)
self.log:add("you stumble into a poison trap") self.log:add(isPlayer and "you stumble into a poison trap"
or string.format("%s stumbles into a poison trap", name))
elseif trap.effect == "confuse" then
Status.apply(victim, Status.CONFUSE, 6, 1)
self.log:add(isPlayer and "a cloud of disorienting spores bursts around you"
or string.format("a cloud of disorienting spores bursts around %s", name))
elseif trap.effect == "blind" then
Status.apply(victim, Status.BLIND, 6, 1)
self.log:add(isPlayer and "a flash of searing light leaves you blind"
or string.format("a flash of searing light leaves %s blind", name))
elseif trap.effect == "stun" then
Status.apply(victim, Status.STUN, 2, 1)
self.log:add(isPlayer and "a jolt of current locks up your muscles"
or string.format("a jolt of current locks up %s's muscles", name))
else else
local x, y = self:randomFloorCell() local x, y = self:randomFloorCell()
if x then if x then
self.player.x, self.player.y = x, y victim.x, victim.y = x, y
self.log:add("a trap flings you across the level") self.log:add(isPlayer and "a trap flings you across the level"
or string.format("a trap flings %s across the level", name))
end end
end end
end end
function Game:triggerTrap(trap)
self:springTrap(trap, self.player)
end
-- Attempts to disarm `trap`, already noticed (see disarm.lua's Disarm.sweep,
-- rolled once per turn in Game:endPlayerTurn) but not yet sprung. Refuses
-- outright, without even risking a spring, against a trap never detected or
-- one already triggered - there is nothing left here to disarm.
function Game:disarmTrap(trap)
if not trap or not trap.detected or trap.revealed then
self.log:add("there is nothing here you have noticed to disarm")
return false
end
local success, line = Disarm.attempt(self, trap)
if success then self.log:add(line) end
return success
end
-- Throws `lever`, a no-op once it has already been thrown. The instant this
-- pull completes every lever sharing its vault id, the shared door swings
-- open (its tile flips to a plain open door - see Vaults.pull) and the run's
-- vault tally advances for meta scoring; otherwise the lever just clunks
-- into its pulled state.
function Game:pullLever(lever)
local pulled, door = Vaults.pull(self.level, lever)
if not pulled then return false end
self.level.map:set(lever.x, lever.y, Tiles.LEVER_PULLED)
if door then
door.opened = true
self.level.map:set(door.x, door.y, Tiles.DOOR_OPEN)
self.stats.vaultsOpened = self.stats.vaultsOpened + 1
self.log:add("the vault door grinds open")
else
self.log:add("you pull the lever - it clunks into place")
end
return true
end
-- A priced ware is only free for the taking once its shopkeeper is dead;
-- otherwise it charges the stamped price and is left on the ground when the
-- player cannot afford it yet (they simply walk past it).
function Game:wardedByLiveKeeper(ground)
if not ground.price then return false end
local keeper = self.level:entityById(ground.shopId)
return keeper ~= nil and keeper:isAlive()
end
-- Player-facing wrapper so callers never need to know encumbrance.lua works
-- entirely off entity.inventory/entity.level rather than any saved field.
function Game:encumbranceTier()
return Encumbrance.tierFor(self.player)
end
function Game:pickupHere() function Game:pickupHere()
local tierBefore = self:encumbranceTier()
for i = #self.level.floorItems, 1, -1 do for i = #self.level.floorItems, 1, -1 do
local ground = self.level.floorItems[i] local ground = self.level.floorItems[i]
if ground.x == self.player.x and ground.y == self.player.y then if ground.x == self.player.x and ground.y == self.player.y then
self.player.inventory[#self.player.inventory + 1] = ground.item local item = ground.item
self.log:add("you pick up " .. Identify.label(self.identify, ground.item)) if item.mimic then
table.remove(self.level.floorItems, i) table.remove(self.level.floorItems, i)
self:triggerMimic(ground)
elseif item.grave then
table.remove(self.level.floorItems, i)
self:triggerGrave(ground)
elseif item.kind == Items.GOLD then
self.player.gold = self.player.gold + item.amount
self.log:add("you find " .. item.amount .. " gold")
table.remove(self.level.floorItems, i)
elseif self:wardedByLiveKeeper(ground) then
local paid = Shop.buy(self, ground)
if paid then
self.log:add(string.format("you buy %s for %d gold",
Identify.label(self.identify, item), paid))
table.remove(self.level.floorItems, i)
end
elseif item.kind == Items.AMMO then
local stack = self:findInInventory(function(existing)
return existing.kind == Items.AMMO and existing.name == item.name
end)
if stack then
self.player.inventory[stack].count = self.player.inventory[stack].count + item.count
else
self.player.inventory[#self.player.inventory + 1] = item
end
self.log:add(string.format("you pick up %d %s", item.count, item.name))
table.remove(self.level.floorItems, i)
else
if item.artifact then self.stats.artifactsFound = self.stats.artifactsFound + 1 end
if item.set then self.stats.setPiecesFound = self.stats.setPiecesFound + 1 end
self.player.inventory[#self.player.inventory + 1] = item
self.log:add("you pick up " .. Identify.label(self.identify, item))
table.remove(self.level.floorItems, i)
end
end
end
local tierAfter = self:encumbranceTier()
if tierAfter ~= tierBefore and tierAfter ~= Encumbrance.LIGHT then
if tierAfter == Encumbrance.OVERLOADED then
self.log:add("you are overloaded and moving sluggishly")
else
self.log:add("your pack grows heavy")
end end
end end
end end