feat: extend game with Game:triggerMimic and Game:triggerGrave
This commit is contained in:
1 parent
6f54f4431d
commit
a43ad023f7
1 file changed
+173
+173
@@ -1325,6 +1325,179 @@ function Game:pickupHere()
|
|||||||
end
|
end
|
||||||
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)
|
function Game:movePlayer(dx, dy)
|
||||||
local nx, ny = self.player.x + dx, self.player.y + dy
|
local nx, ny = self.player.x + dx, self.player.y + dy
|
||||||
local map = self.level.map
|
local map = self.level.map
|
||||||
|
|||||||
Reference in new issue
Block a user