Files
warren/src/curses.lua
T

162 lines
5.7 KiB
Lua

-- Malevolent gear rolled as the dark mirror of affixes.lua's beneficial
-- prefixes. A cursed weapon, suit of armor, bow or ring looks completely
-- ordinary on the ground and in the pack - its stat penalty is already
-- folded into item.power/item.range at generation time, so nothing
-- downstream needs to know an item is cursed to use its numbers correctly.
-- Putting one on binds it in place: trying to swap it out for something
-- else fails and reveals the curse right there, the same "you find out by
-- trying" moment as a classic roguelike's stuck cursed ring. This is a
-- different curse from shrines.lua's: that one is a temporary status
-- inflicted the instant a prayer backfires, this one travels with a
-- specific item and needs a specific remedy - a scroll of remove curse, or
-- a prayer at a shrine of fortune - to lift.
local Curses = {}
Curses.WEAKNESS = "weakness"
Curses.BRITTLE = "brittle"
Curses.FAULTY = "faulty"
Curses.AFFLICTION = "affliction"
-- The four wearable slots a curse can ever occupy, in a fixed order so
-- "the earliest cursed slot" always means the same thing across callers.
Curses.SLOTS = { "weapon", "armor", "ring", "bow" }
Curses.pool = {
weapon = { { key = Curses.WEAKNESS, powerPenalty = 2 } },
armor = { { key = Curses.BRITTLE, powerPenalty = 2 } },
bow = { { key = Curses.FAULTY, powerPenalty = 1, rangePenalty = 2 } },
ring = { { key = Curses.AFFLICTION, drain = 1 } },
}
Curses.CHANCE_NUMERATOR = 1
Curses.CHANCE_DENOMINATOR = 8
-- Rolls a curse for a freshly generated `slot` item, or nil most of the
-- time. Weapons and armor should only ever be offered here when they did
-- not already land a beneficial affix (see builder.lua's rollItem), so a
-- single drop is never simultaneously a blessing and a curse.
function Curses.roll(rng, slot)
local pool = Curses.pool[slot]
if not pool then return nil end
if not rng:chance(Curses.CHANCE_NUMERATOR, Curses.CHANCE_DENOMINATOR) then return nil end
return pool[rng:range(1, #pool)]
end
-- Binds `curse` onto `item`, silently folding its stat penalty into the
-- item's own numbers. The curse itself stays hidden (curseRevealed false)
-- until reveal() is called by a discovery path.
function Curses.afflict(item, curse)
item.cursed = true
item.curseKind = curse.key
item.curseRevealed = false
if curse.powerPenalty then
item.cursePenalty = curse.powerPenalty
item.power = math.max(0, item.power - curse.powerPenalty)
end
if curse.rangePenalty then
item.curseRangePenalty = curse.rangePenalty
item.range = math.max(1, item.range - curse.rangePenalty)
end
if curse.drain then
item.curseDrain = curse.drain
end
return item
end
function Curses.isCursed(item)
return item ~= nil and item.cursed == true
end
function Curses.isRevealed(item)
return item ~= nil and item.curseRevealed == true
end
function Curses.reveal(item)
if item then item.curseRevealed = true end
end
-- True once whatever `entity` currently has worn in `slot` is cursed.
function Curses.isBoundIn(entity, slot)
return Curses.isCursed(entity[slot])
end
-- Refuses to let go of a bound item: reveals the curse the instant the
-- wearer discovers it - the whole point of the mechanic is finding out by
-- trying - and reports false so the caller can leave the replacement
-- sitting in the pack instead of swapping it in. An empty or clean slot
-- always succeeds without side effects.
function Curses.tryRelease(entity, slot)
local item = entity[slot]
if not Curses.isCursed(item) then return true end
Curses.reveal(item)
return false
end
-- Undoes `item`'s curse in place: restores whatever power/range it cost,
-- drops the drain, and clears every curse field so the piece swaps out
-- normally from here on. No-op (returns false) on clean or absent gear.
function Curses.cleanse(item)
if not Curses.isCursed(item) then return false end
if item.cursePenalty then
item.power = item.power + item.cursePenalty
item.cursePenalty = nil
end
if item.curseRangePenalty then
item.range = item.range + item.curseRangePenalty
item.curseRangePenalty = nil
end
item.curseDrain = nil
item.cursed = nil
item.curseKind = nil
item.curseRevealed = nil
return true
end
-- Lifts every curse currently worn by `entity` across all four slots,
-- returning how many pieces were cleansed - 0 when none of its gear was
-- cursed to begin with, so callers can report "nothing to cleanse".
function Curses.cleanseWorn(entity)
local count = 0
for _, slot in ipairs(Curses.SLOTS) do
if Curses.cleanse(entity[slot]) then count = count + 1 end
end
return count
end
-- Reveals the first still-hidden curse among `entity`'s worn gear, in
-- Curses.SLOTS order, and returns the item it found - or nil once nothing
-- worn is hiding a curse anymore.
function Curses.revealFirstUnknown(entity)
for _, slot in ipairs(Curses.SLOTS) do
local item = entity[slot]
if Curses.isCursed(item) and not Curses.isRevealed(item) then
Curses.reveal(item)
return item
end
end
return nil
end
-- The hit points a cursed ring bleeds from its wearer this round. Only a
-- ring's affliction drains on a timer - weapon, armor and bow curses are a
-- flat, one-time stat tax instead - so this is the only per-turn cost the
-- whole system has.
function Curses.drainAmount(entity)
local ring = entity.ring
if ring and ring.curseDrain then return ring.curseDrain end
return 0
end
-- Display name for `item`: an unrevealed curse hides behind the item's
-- ordinary name exactly like a clean piece, a revealed one is prefixed so
-- the player never again mistakes it for something safe to swap out.
function Curses.label(item, name)
if Curses.isCursed(item) and Curses.isRevealed(item) then
return "cursed " .. name
end
return name
end
return Curses