feat: extend game with Game:sellCaptive and Game:interrogateCaptive
This commit is contained in:
1 parent
b3943d9b6e
commit
6f54f4431d
1 file changed
+181
-10
+181
-10
@@ -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
|
||||||
|
|||||||
Reference in new issue
Block a user