From b3943d9b6e3a0e2dade56689a70be2a28a7eacbe Mon Sep 17 00:00:00 2001 From: DualFroz Date: Sun, 6 Sep 2026 16:04:58 +0200 Subject: [PATCH] feat: extend game with Game:fireAt and Game:appeaseFaction --- src/game.lua | 173 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 173 insertions(+) diff --git a/src/game.lua b/src/game.lua index 27ade50..8b116b9 100644 --- a/src/game.lua +++ b/src/game.lua @@ -930,6 +930,179 @@ function Game:captureAt(index, target) self:emitNoise(self.player.x, self.player.y, Noise.THROW) self:cleanupDead() end + self.log:addAll(lines) + return success +end + +-- Looses an arrow from the player's equipped bow at a monster. The arrow +-- always leaves the quiver on a successful shot, whatever it did on arrival. +function Game:fireAt(target) + self:provoke(target) + local success, lines = Ranged.fire(self, self.player, target) + self.log:addAll(lines) + if success then self:emitNoise(self.player.x, self.player.y, Noise.RANGED) end + return success +end + +-- Spends gold to smooth over the player's standing with `faction` (see +-- factions.lua): cools a grudge, or pads an existing friendship, by a fixed +-- amount. The asking price climbs the deeper the relationship has soured. +function Game:appeaseFaction(faction) + local success, cost = Factions.appease(self.player, faction) + if success then + self.log:add(string.format("your tribute smooths things over with the %s (%d gold)", faction, cost)) + elseif cost == 0 then + self.log:add(string.format("the %s already think as well of you as they're going to", faction)) + else + self.log:add(string.format("you need %d gold to make peace with the %s", cost, faction)) + end + return success +end + +-- Prays at the shrine standing under the player, spending its gold price for +-- a shot at a permanent blessing (or, with rising odds the more this altar +-- has already been used, a curse instead). Returns false without any state +-- change when there is no shrine here or its price cannot be met. +function Game:prayAt(shrine) + local success, lines = Shrines.pray(self, shrine) + self.log:addAll(lines) + return success +end + +-- Swings the player's equipped pick at whatever lies one step away in +-- (dx, dy): a plain wall or the rubble left by an earlier swing. Returns +-- false without spending the swing when there is no pick readied or the +-- target cell has nothing left to dig (open floor, a vault door, out of +-- bounds); otherwise always spends one use of the pick's durability, even on +-- an unlucky cave-in that costs hp instead of progress (see digging.lua). +function Game:dig(dx, dy) + if not self.player.pick then + self.log:add("you have nothing to dig with") + return false + end + local nx, ny = self.player.x + dx, self.player.y + dy + if not Digging.canDig(self.level.map, nx, ny) then + self.log:add("there is nothing here to dig") + return false + end + self.log:add(Digging.swing(self, self.player, nx, ny)) + local brokenLine = Digging.wearPick(self.player) + if brokenLine then self.log:add(brokenLine) end + self:emitNoise(nx, ny, Noise.DIG) + return true +end + +-- Harvests whatever resource node stands under the player: an ore vein pays +-- out essence, a gem seam yields a raw gem, both free of any gold cost - +-- unlike a shrine's prayer, the only thing rationing this is the node's own +-- charges (see nodes.lua). Returns false without state change when there is +-- no node here or it has already been picked dry this trip. +function Game:harvestNode() + local node = self.level:nodeAt(self.player.x, self.player.y) + if not node then + self.log:add("there is nothing here to harvest") + return false + end + local success, line = Nodes.harvest(self, node) + self.log:add(line) + return success +end + +-- Butchers whatever corpse lies under the player into raw meat (Items.FOOD), +-- eaten later exactly like a found ration - unless the corpse has already +-- gone past its rot threshold (corpses.lua), in which case it poisons the +-- player instead. Either way the corpse itself is spent in one stroke. +-- Returns false without state change when there is nothing here to carve. +function Game:butcherCorpse() + local corpse = self.level:corpseAt(self.player.x, self.player.y) + if not corpse then + self.log:add("there is nothing here to butcher") + return false + end + self.log:add(Corpses.butcher(self, corpse)) + return true +end + +-- Raises whatever fresh corpse lies under the player into a temporary +-- skeletal thrall (see reanimation.lua), spending both the corpse and a +-- bite of sanity in one stroke - a deliberate trade the autopilot never +-- makes on its own, unlike Game:butcherCorpse's free meal, so this is only +-- ever called directly. Returns false without state change when there is +-- no corpse here or Reanimation.raise itself refuses the attempt. +function Game:raiseCorpse() + local corpse = self.level:corpseAt(self.player.x, self.player.y) + if not corpse then + self.log:add("there is nothing here to raise") + return false + end + local thrall, failMsg = Reanimation.raise(self, corpse) + if not thrall then + self.log:add(failMsg) + return false + end + self.log:add("bones knit and rise - your " .. thrall.name .. " answers the call") + return true +end + +-- Carves whatever fresh corpse lies under the player for the herb its own +-- kind carries (see scavenging.lua) instead of a meal or a thrall - a third +-- deliberate trade for the same tile, only ever called directly like +-- Game:raiseCorpse, never by the autopilot. Returns false without state +-- change when there is no corpse here or Scavenging.harvest itself refuses. +function Game:scavengeCorpse() + local corpse = self.level:corpseAt(self.player.x, self.player.y) + if not corpse then + self.log:add("there is nothing here to scavenge") + return false + end + local harvested, message = Scavenging.harvest(self, corpse) + self.log:add(message) + return harvested +end + +-- Opens whatever chest stands under the player. An unlocked one (or one +-- already forced open) yields its hoard outright; a locked one first tries +-- a carried key, exactly like Game:openDoor's own certain trade, and only +-- falls back to a lockpick gamble (Chests.pick, which risks springing a +-- trapped lock) once no key can be found. Returns false without state +-- change when there is nothing here to open, it is already empty, or it is +-- locked with neither a key nor a lockpick on hand. +function Game:openChest() + local chest = self.level:chestAt(self.player.x, self.player.y) + if not chest then + self.log:add("there is nothing here to open") + return false + end + if chest.opened then + self.log:add("this chest is already empty") + return false + end + local success, line + if not Chests.isLocked(chest) then + success, line = Chests.open(self.player, chest) + else + success, line = Chests.unlockWithKey(self.player, chest) + if not success then success, line = Chests.pick(self, chest) end + end + self.log:add(line) + if success then self.stats.chestsOpened = self.stats.chestsOpened + 1 end + return success +end + +-- Frees whatever captive stands under the player back to its own kind for +-- nothing but goodwill (see Captives.ransom) - the one resolution that can +-- pull a wary tribal faction (factions.lua's neutralFactions) back out of +-- open hostility rather than deepening it further. Returns false without +-- state change when there is no captive here. +function Game:ransomCaptive() + local captive = self.level:captiveAt(self.player.x, self.player.y) + if not captive then + self.log:add("there is nothing here to ransom") + return false + end + Captives.ransom(self.level, self.player, captive) + self.log:add(string.format("you release %s - the %s think a little better of you for it", captive.name, captive.faction)) + return true end function Game:openDoor(x, y)