feat: extend game with Game:triggerMimic and Game:triggerGrave

This commit is contained in:
dualfroz committed 2026-09-06 17:06:04 +02:00
1 parent 6f54f4431d
commit a43ad023f7
1 file changed
+173
+173
View File
@@ -1325,6 +1325,179 @@ function Game:pickupHere()
end
end
-- Unmasks a disguised floorItem the moment the player reaches for it:
-- drops the real hostile monster in its place (already hunting, never
-- dormant - see stealth.lua's header for why a builder-placed wild
-- monster differs) and lands one free ambush swing through combat.lua,
-- so the surprise still respects the player's defense and resistances
-- instead of being a flat, unavoidable trap tick.
function Game:triggerMimic(ground)
local mimic = Mimics.reveal(self:nextMonsterId(), ground.x, ground.y, self.depth)
mimic.awareness = Stealth.HUNTING
self.level.entities[#self.level.entities + 1] = mimic
self.log:add(ground.item.name .. " was a mimic!")
local outcome = Combat.attack(mimic, self.player, self:monsterDamageMultiplier())
Momentum.onDamaged(self.player)
self.log:add(string.format("it lashes out at you for %d damage", outcome.damage))
local broke = Durability.wearArmor(self.player)
if broke then self.log:add(broke) end
end
-- Wakes the vengeful shade sleeping under a disturbed grave marker (see
-- graves.lua): unlike a mimic's disguise, the marker never hid what it
-- was, but stepping onto it is still a mistake the same instant Game:
-- pickupHere makes it - one free ambush swing through combat.lua before
-- the shade settles into an ordinary chase on its own turns from then on.
function Game:triggerGrave(ground)
local shade = Graves.reveal(self:nextMonsterId(), ground.x, ground.y, ground)
shade.awareness = Stealth.HUNTING
self.level.entities[#self.level.entities + 1] = shade
self.log:add("the grave stirs - a vengeful shade rises!")
local outcome = Combat.attack(shade, self.player, self:monsterDamageMultiplier())
Momentum.onDamaged(self.player)
self.log:add(string.format("it lashes out at you for %d damage", outcome.damage))
local broke = Durability.wearArmor(self.player)
if broke then self.log:add(broke) end
end
-- Discards an inventory item onto the floor at the player's feet: the
-- opposite of pickupHere, needed once a hoard grows heavier than is
-- comfortable to carry (see encumbrance.lua). Equipped gear never reaches
-- this path - it always trades in place through items.equipWeapon and
-- friends - so this only ever touches the loose inventory array.
function Game:dropItem(index)
local item = self.player.inventory[index]
if not item then return nil end
table.remove(self.player.inventory, index)
self.level.floorItems[#self.level.floorItems + 1] = { x = self.player.x, y = self.player.y, item = item }
self.log:add("you drop " .. Identify.label(self.identify, item))
return item
end
-- The cheapest inventory item that is safe to shed first when overloaded:
-- never the amulet (the entire point of a run) or a key (worthless to a
-- shop and needed right where it was found). Returns nil if nothing but
-- those two remains.
function Game:cheapestDroppableItem()
local bestIndex, bestPrice
for i, item in ipairs(self.player.inventory) do
if item.kind ~= Items.AMULET and item.kind ~= Items.KEY then
local price = Shop.priceOf(item)
if not bestPrice or price < bestPrice then
bestIndex, bestPrice = i, price
end
end
end
return bestIndex
end
-- Sells an inventory item back to the economy at half its catalog price.
function Game:sellItem(index)
local item = self.player.inventory[index]
local value = Shop.sell(self, index)
if value then
self.log:add(string.format("you sell %s for %d gold", Identify.label(self.identify, item), value))
end
return value
end
-- Spends gold to restore whichever of the player's weapon, armor or bow sits
-- in `slotName` back to full durability (see durability.lua), folding its
-- power back into attack/defense if wear had already broken it.
function Game:repairEquipped(slotName)
local item = self.player[slotName]
local success, result = Durability.repair(self.player, slotName)
if success then
self.log:add(string.format("you repair your %s for %d gold", item.name, result))
else
self.log:add("cannot repair: " .. result)
end
return success
end
-- The first of weapon/armor/bow, in that priority order, that has been worn
-- clean through to uselessness - never a merely dinged one still pulling its
-- weight, so the autopilot only ever reaches for its purse over gear that
-- has actually stopped working.
function Game:brokenEquippedSlot()
for _, slot in ipairs({ "weapon", "armor", "bow" }) do
local item = self.player[slot]
if item and Durability.isBroken(item) then return slot end
end
return nil
end
-- Taking a warded item without paying: the item is stolen outright and its
-- shopkeeper turns permanently hostile, chasing the player like any monster.
function Game:stealFromShop(index)
local ground = self.level.floorItems[index]
if not ground or not self:wardedByLiveKeeper(ground) then return false end
self.player.inventory[#self.player.inventory + 1] = ground.item
local keeper = self.level:entityById(ground.shopId)
keeper.hostile = true
Factions.adjust(self.player, Factions.MERCHANT, -20)
self.log:add("you steal " .. Identify.label(self.identify, ground.item) ..
"! the shopkeeper turns on you")
table.remove(self.level.floorItems, index)
return true
end
-- Resolves a crafting target: "weapon"/"armor" reach the currently equipped
-- piece (which has left the inventory array entirely - see items.equipWeapon/
-- equipArmor), anything else is an inventory index.
function Game:resolveGear(target)
if target == "weapon" then return self.player.weapon end
if target == "armor" then return self.player.armor end
return self.player.inventory[target]
end
function Game:forgeGem(element)
local ok, result = Crafting.forge(self.player, element)
if not ok then
self.log:add("cannot forge: " .. result)
return false
end
self.player.inventory[#self.player.inventory + 1] = result
self.log:add("you forge a " .. result.name)
return true
end
function Game:mergeGemsAt(indexA, indexB)
local gemA, gemB = self.player.inventory[indexA], self.player.inventory[indexB]
if not gemA or not gemB or gemA.kind ~= Items.GEM or gemB.kind ~= Items.GEM or indexA == indexB then
return false
end
local success, reason, merged = Crafting.mergeGems(gemA, gemB)
if not success then
self.log:add("cannot merge gems: " .. reason)
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)
self.player.inventory[#self.player.inventory + 1] = merged
self.log:add("you merge two gems into a " .. merged.name)
return true
end
function Game:socketItem(target, gemIndex)
local item = self:resolveGear(target)
local gem = self.player.inventory[gemIndex]
if not item or not gem or gem.kind ~= Items.GEM then return false end
local success, reason = Crafting.socket(self.player, item, gem)
if not success then
self.log:add("cannot socket: " .. reason)
return false
end
table.remove(self.player.inventory, gemIndex)
self.log:add("you socket a " .. gem.name .. " into your " .. item.name)
return true
end
-- Socketing a rune never removes it from the game the way a failed gem
-- 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
function Game:movePlayer(dx, dy)
local nx, ny = self.player.x + dx, self.player.y + dy
local map = self.level.map