From e850ef2d5ef9a0f45777fd3b4fe93d4d52b1c59e Mon Sep 17 00:00:00 2001 From: DualFroz Date: Sun, 6 Sep 2026 19:51:54 +0200 Subject: [PATCH] feat: extend game with Game:learnSkill and Game:useSkill --- src/game.lua | 123 +++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 115 insertions(+), 8 deletions(-) diff --git a/src/game.lua b/src/game.lua index 86ab860..7c60943 100644 --- a/src/game.lua +++ b/src/game.lua @@ -2295,27 +2295,134 @@ function Game:hungerTorchTick() self.player.hp = math.min(self.player.maxHp, self.player.hp + weatherRegen) end end + local curseDrain = Curses.drainAmount(self.player) + if curseDrain > 0 then + self.player.hp = self.player.hp - curseDrain + self.log:add("a cursed affliction saps your strength") + end + local diseaseLine = Disease.tick(self.player) + if diseaseLine then self.log:add(diseaseLine) end + local hungerLine = Hunger.tick(self.player) + if hungerLine then self.log:add(hungerLine) end if self.player.light.fuel > 0 then self.player.light.fuel = self.player.light.fuel - 1 end + if self.player.mana < self.player.maxMana then + self.player.mana = self.player.mana + 1 + end end function Game:endPlayerTurn() + self.turnState = nil Scheduler.spend(self.player) self:hungerTorchTick() + Hazards.step(self.level, self.rng) + Nodes.step(self.level) + Corpses.step(self.level) + for _, trap in ipairs(Disarm.sweep(self.level, self.rng, self.player.x, self.player.y)) do + self.log:add("you notice a hidden trap nearby") + end + for _, door in ipairs(Secrets.sweep(self.level, self.rng, self.player.x, self.player.y)) do + self.log:add("you notice a hidden door in the wall") + end + local eventLine = Events.step(self.level, self.rng) + if eventLine then self.log:add(eventLine) end + Skills.tickCooldowns(self.player) + self:resolveQuest() self.turn = self.turn + 1 + if self.turn % Mercenaries.WAGE_INTERVAL == 0 then + local wageLine = Mercenaries.payWage(self) + if wageLine then self.log:add(wageLine) end + end self:computeVisible() + local sanityLine = Sanity.tick(self) + if sanityLine then self.log:add(sanityLine) end + if Sanity.rollScream(self.rng, self) then + self.log:add("a scream of raw terror tears from your throat") + end + if self.horde then + local hordeLine = Horde.tick(self) + if hordeLine then self.log:add(hordeLine) end + end +end + +-- Spends a skill point to permanently learn `key`, gated by class, level and +-- point balance (see skills.lua). Free of any turn cost. +function Game:learnSkill(key) + local success, message = Skills.learn(self.player, key) + if message then self.log:add(message) end + return success +end + +-- Attempts to use the active skill `key`, forwarding any extra arguments +-- (a target monster, or two for multishot) straight to skills.lua. +function Game:useSkill(key, ...) + local success, lines = Skills.use(self, key, ...) + self.log:addAll(lines) + return success +end + +-- Shared head of any player turn, whether the autopilot or a typed command +-- is about to decide what happens next: runs every other actor up to the +-- player, ticks the status/water/hazard afflictions that land on the +-- player's own turn boundary, and rolls the stun/panic checks that can +-- consume the turn before any action is even chosen. Returns "halt" once +-- play cannot continue this call (already won, or dead at any point along +-- the way - the caller must not act or spend a turn), "skip" once the turn +-- is already spent on a stun or a panic scream, or "ready" once the caller +-- is clear to choose and perform an action of its own. +-- +-- Cached on self.turnState until Game:endPlayerTurn clears it, so calling +-- this more than once before a turn is actually spent - the one new +-- situation Game:manualStep introduces, since a rejected typed command +-- spends no turn and its caller is free to just try again - never re-rolls +-- the stun/panic check or re-ticks poison, fire or hunger a second time for +-- a turn the player has not actually finished taking yet. Game:autoStep +-- never revisits an unspent turn this way, so the cache is invisible to it. +function Game:beginTurn() + if self.turnState then return self.turnState end + if self.won then self.turnState = "halt"; return "halt" end + self:runToPlayer() + if not self.player:isAlive() then self.turnState = "halt"; return "halt" end + self.log:addAll(Status.tick(self.player)) + if not self.player:isAlive() then self.turnState = "halt"; return "halt" end + Conductivity.tick(self.level, self.player) + local hazardLine = Hazards.afflict(self.level, self.player) + if hazardLine then self.log:add(hazardLine) end + if not self.player:isAlive() then self.turnState = "halt"; return "halt" end + if Status.skipsTurn(self.player) then + self.log:add("you are stunned") + self.turnState = "skip" + elseif Sanity.rollPanic(self.rng, self.player) then + self.log:add("terror seizes you and the turn slips away") + self.turnState = "skip" + else + self.turnState = "ready" + end + return self.turnState end function Game:autoStep() - self:runToPlayer() - if not self.player:isAlive() then return end - self.log:addAll(Status.tick(self.player)) - if not self.player:isAlive() then return end - if Status.skipsTurn(self.player) then - self.log:add("you are stunned") - else - self:choosePlayerAction() + local state = self:beginTurn() + if state == "halt" then return end + if state == "ready" then self:choosePlayerAction() end + self:endPlayerTurn() +end + +-- Human-driven counterpart to Game:autoStep: runs the exact same pre-turn +-- checks, then hands `text` to commands.lua instead of letting +-- choosePlayerAction decide on its own. Returns (consumesTurn, message) from +-- Commands.apply so a caller (warren.lua's --play REPL, or a test driving a +-- fixed script of commands) can tell a wasted keystroke from a turn the +-- world actually answered. A stun or panic rolled during beginTurn still +-- spends the turn and ends it exactly like autoStep, without ever looking at +-- `text` - the player was never in control of that turn to begin with. +function Game:manualStep(text) + local state = self:beginTurn() + if state == "halt" then return false, "there is nothing left to do" end + if state == "skip" then + self:endPlayerTurn() + return true, nil end self:endPlayerTurn() end