feat: extend game with Game:socketRune and Game:temperItem

This commit is contained in:
dualfroz committed 2026-09-06 17:40:31 +02:00
1 parent a43ad023f7
commit 3427a1a8f0
1 file changed
+196 -6
+196 -6
View File
@@ -1498,25 +1498,167 @@ end
-- socket attempt would leave the gem untouched in inventory - the rune is
-- always spent once it joins the chain, whether or not that completes a
-- runeword, since there is no "wrong" rune to protect the player from (see
-- Runewords.socket).
function Game:socketRune(target, runeIndex)
local item = self:resolveGear(target)
local rune = self.player.inventory[runeIndex]
if not item or not rune or rune.kind ~= Items.RUNE then return false end
local success, reason = Runewords.socket(item, rune)
if not success then
self.log:add("cannot socket rune: " .. reason)
return false
end
table.remove(self.player.inventory, runeIndex)
local word = Runewords.activeWord(item)
if word then
self.log:add("the runes align into " .. word.name .. "! your " .. item.name .. " hums with power")
else
self.log:add("you socket a " .. rune.name .. " into your " .. item.name)
end
return true
end
function Game:temperItem(target)
local item = self:resolveGear(target)
if not item then return false end
local success, reason = Crafting.temper(self.player, item)
if not success then
self.log:add("cannot temper: " .. reason)
return false
end
self.log:add("you temper your " .. item.name)
return true
end
function Game:enchantItem(target, affixKey)
local item = self:resolveGear(target)
if not item then return false end
local success, reason = Crafting.enchant(self.player, item, affixKey)
if not success then
self.log:add("cannot enchant: " .. reason)
return false
end
self.log:add("you enchant your " .. item.name)
return true
end
function Game:disenchantItem(target)
local item = self:resolveGear(target)
if not item then return false end
local success, refund = Crafting.disenchant(self.player, item)
if not success then
self.log:add("cannot disenchant: " .. refund)
return false
end
self.log:add(string.format("you disenchant %s for %d essence", item.name, refund))
return true
end
-- Combines the two carried herbs at indexA and indexB into whichever potion
-- Alchemy.brew resolves the pair to. Both herbs are spent regardless of the
-- outcome - a botched brew that curdles is not a free retry, and vents its
-- fumes as a gas hazard right underfoot instead of handing back a potion.
function Game:brewAt(indexA, indexB)
if not self.level:cauldronAt(self.player.x, self.player.y) then
self.log:add("there is no cauldron here")
return false
end
if indexA == indexB then return false end
local herbA, herbB = self.player.inventory[indexA], self.player.inventory[indexB]
if not herbA or not herbB or herbA.kind ~= Items.HERB or herbB.kind ~= Items.HERB then
self.log:add("you need two herbs to brew")
return false
end
local hi, lo = math.max(indexA, indexB), math.min(indexA, indexB)
table.remove(self.player.inventory, hi)
table.remove(self.player.inventory, lo)
local potion = Alchemy.brew(herbA.herb, herbB.herb)
if not potion then
Hazards.spawnGas(self.level, self.player.x, self.player.y, 3, 2)
self.log:add(string.format("the %s and %s curdle into choking fumes", herbA.name, herbB.name))
return false
end
Identify.reveal(self.identify, potion)
self.player.inventory[#self.player.inventory + 1] = potion
self.log:add("you brew " .. potion.name)
return true
end
-- Companions, hirelings and thralls are all player-owned allies (see
-- Game:onActorDeath), never a valid melee target for the player's own bump-
-- attack or autopilot targeting - both Game:movePlayer and Game:adjacentMonster
-- below share this one check so neither can turn "walk into your own hound"
-- into an accidental kill.
local function isPlayerAlly(entity)
return entity.kind == "companion" or entity.kind == "hireling" or entity.kind == "thrall"
end
local CONFUSE_DIRS = {
{ 1, 0 }, { -1, 0 }, { 0, 1 }, { 0, -1 }, { 1, 1 }, { 1, -1 }, { -1, 1 }, { -1, -1 },
}
-- Whether `dx, dy` (the direction the player actually typed, or the one
-- autopilot's own targeting picked) gets overridden by a random one of the
-- 8 compass directions this call - the confusion-lands roll only ever fires
-- once per Game:movePlayer, never re-rolled mid-call. Confusion strikes
-- before Game:movePlayer even looks at what's in the chosen direction, so a
-- stumble can just as easily open a door, bump a rival into a fight, shove a
-- block, or walk the player straight onto a trap as a deliberate move would.
local function confusionRedirect(self, dx, dy)
if not Status.has(self.player, Status.CONFUSE) then return dx, dy, false end
if not self.rng:chance(Status.CONFUSE_NUMERATOR, Status.CONFUSE_DENOMINATOR) then
return dx, dy, false
end
local dir = CONFUSE_DIRS[self.rng:range(1, #CONFUSE_DIRS)]
return dir[1], dir[2], true
end
-- `stumbled` (below) is what makes a confused player's failed lurch still
-- cost a turn: an ordinary bump into a wall or an unpushable block returns
-- false and Game:manualStep spends nothing, exactly as a rejected typed
-- command should, but a confused player who meant to go one way and staggered
-- into a wall instead still acted - the turn is spent on the stumble itself,
-- not on whatever (if anything) it accomplished.
function Game:movePlayer(dx, dy)
local stumbled
dx, dy, stumbled = confusionRedirect(self, dx, dy)
if stumbled then self.log:add("your senses reel and you stumble sideways") end
local nx, ny = self.player.x + dx, self.player.y + dy
local map = self.level.map
if Tiles.isClosedDoor(map:get(nx, ny)) then
return self:openDoor(nx, ny)
end
local target = self.level:entityAt(nx, ny)
if target then
if target and not isPlayerAlly(target) then
self:playerAttack(target)
return true
end
if not target then
local block = self.level:blockAt(nx, ny)
if block then
local pushed, line = Blocks.push(self.level, block, dx, dy)
if not pushed then return stumbled end
self.log:add(line)
Blocks.updateGates(self.level)
end
end
if map:isWalkable(nx, ny) then
if target then
target.x, target.y = self.player.x, self.player.y
self.log:add("you swap places with your " .. target.name)
end
self.player.x, self.player.y = nx, ny
local trap = self.level:trapAt(nx, ny)
if trap and not trap.revealed then self:triggerTrap(trap) end
if trap and not trap.revealed and not Status.has(self.player, Status.LEVITATE) then
self:triggerTrap(trap)
end
self:checkTerrainFor(self.player, dx, dy)
local lever = self.level:vaultLeverAt(nx, ny)
if lever then self:pullLever(lever) end
self:pickupHere()
return true
end
return false
return stumbled
end
function Game:useItem(index)
@@ -1527,12 +1669,60 @@ function Game:useItem(index)
Identify.reveal(self.identify, item)
table.remove(self.player.inventory, index)
elseif item.kind == Items.FOOD then
self.player.satiation = self.player.satiation + item.nutrition
local bloated = Hunger.eat(self.player, item.nutrition)
if bloated then
self.log:add("you force down the " .. item.name .. " past comfort and feel bloated")
else
self.log:add("you eat the " .. item.name)
end
table.remove(self.player.inventory, index)
elseif item.kind == Items.WEAPON or item.kind == Items.ARMOR or item.kind == Items.RING then
elseif item.kind == Items.WEAPON or item.kind == Items.ARMOR or item.kind == Items.RING
or item.kind == Items.BOW then
local slot = slotByItemKind[item.kind]
if not Curses.tryRelease(self.player, slot) then
self.log:add("the " .. Curses.label(self.player[slot], Identify.label(self.identify, self.player[slot])) ..
" will not come off!")
return false
end
Items.useFromInventory(self.player, index)
self.log:add("you equip " .. Identify.label(self.identify, item))
self.log:add("you equip " .. Curses.label(item, Identify.label(self.identify, item)))
if item.set then
local count = Sets.equippedCounts(self.player)[item.set]
local bonus = Sets.bonusFor(item.set, count)
if bonus then
self.log:add("the " .. Sets.byKey[item.set].name .. " set (" .. count .. " pieces) awakens")
end
end
elseif item.kind == Items.TOME then
if Spells.learn(self.player, item.spell) then
self.log:add("you learn " .. Spells.catalog[item.spell].name)
else
self.log:add("you already know that spell")
end
table.remove(self.player.inventory, index)
elseif item.kind == Items.CHARM then
local companion = Companions.summon(self, item.companion)
if not companion then return false end
self.log:add("your " .. companion.name .. " bonds to you")
table.remove(self.player.inventory, index)
elseif item.kind == Items.CONTRACT then
local hireling = Mercenaries.hire(self, item.hireling)
if not hireling then return false end
self.log:add("your " .. hireling.name .. " joins you for hire")
table.remove(self.player.inventory, index)
elseif item.kind == Items.KIT then
if self.level:trapAt(self.player.x, self.player.y) then
self.log:add("there is already a trap here")
return false
end
self.level.traps[#self.level.traps + 1] = {
x = self.player.x, y = self.player.y, effect = item.effect, revealed = false, placedByPlayer = true,
}
self.log:add("you plant a " .. item.name)
table.remove(self.player.inventory, index)
elseif item.kind == Items.PICK then
Items.useFromInventory(self.player, index)
self.log:add("you ready your " .. item.name)
else
return false
end