feat: add Compendium.new, Compendium.noteSighting and Conductivity.isWet

This commit is contained in:
dualfroz committed 2026-09-04 11:48:10 +02:00
1 parent 972f6e00ec
commit 4c5bae5a00
2 files changed
+173

No files matched your search

+93
View File
@@ -0,0 +1,93 @@
-- Cross-run bestiary: which species the player has ever laid eyes on, and how
-- many of each they have slain. A compendium lives inside meta.lua's
-- persistent save (see game.lua's Game.new, which reuses options.meta's own
-- table by reference so every sighting and kill recorded mid-run is already
-- sitting in the very table warren.lua writes back out with Meta.save) so it
-- survives death exactly like renown and perks; a run started without
-- --meta still gets a fresh, un-persisted one so every code path exercises
-- the same bookkeeping either way.
local Monsters = require("monsters")
local Bosses = require("bosses")
local Compendium = {}
-- One evocative line per species, keyed the same as monsters.lua's species
-- key or, for a boss, its bosses.lua catalog key (without the "boss_"
-- prefix game.lua stamps onto the actual entity's `species` field).
Compendium.FLAVOR = {
rat = "Bold enough to bite an ankle, never bold enough to fight fair.",
bat = "Flits erratically through the dark on a course only it can hear.",
kobold = "Travels in twos and threes; alone it barely has the nerve to bite.",
goblin = "Quick with a rusty blade, quicker to run once its pack scatters.",
archer = "A skeleton archer, more patient dead than most men are living.",
orc = "Broad-shouldered muscle with no plan beyond the next swing.",
wretch = "Something that used to be a person, now just hungry and afraid.",
troll = "Wounds close almost as fast as they open; fire is the only argument it respects.",
mage = "A dark mage who traded caution for whatever is behind that door.",
dragon = "Young enough to still be reckless, old enough to already breathe fire.",
zombie = "Shambles forward on rot and spite alone.",
slime = "Splits when struck; killing one carelessly just makes two problems.",
footpad = "Light-fingered and lighter on its feet - it wants your purse, not a fight.",
shaman = "Chants curses from a safe distance and flees the moment that fails.",
necromancer = "Would rather raise a friend from your kills than make one of its own.",
cleric = "Keeps its allies standing longer than they have any right to.",
chieftain = "Rallies its kin with a bellow that carries through the whole floor.",
shopkeeper = "Minds its own business and its stock, right up until you take both.",
warlord = "The Orc Warlord, whose rage only sharpens the longer a fight runs.",
lich = "The Lich, a will that outlasted its own body decades ago.",
bonequeen = "The Bone Queen, who never lacks for company - the dead keep answering her call.",
}
function Compendium.new()
return { seen = {}, kills = {} }
end
-- Marks `speciesKey` as sighted. Returns true only the first time it is ever
-- seen, so a caller can choose whether to make anything of that moment; a
-- repeat sighting is silently a no-op.
function Compendium.noteSighting(compendium, speciesKey)
if not speciesKey or compendium.seen[speciesKey] then return false end
compendium.seen[speciesKey] = true
return true
end
function Compendium.noteKill(compendium, speciesKey)
if not speciesKey then return end
compendium.kills[speciesKey] = (compendium.kills[speciesKey] or 0) + 1
end
function Compendium.killCount(compendium, speciesKey)
return compendium.kills[speciesKey] or 0
end
function Compendium.hasSeen(compendium, speciesKey)
return compendium.seen[speciesKey] == true
end
-- Every species named in the wild bestiary or the boss catalog, in a fixed
-- order (wild species first, then bosses), each row carrying its catalogued
-- name, flavor line and this compendium's own seen/kill tally. `key` here
-- matches exactly what an actual entity's `species` field would hold, so a
-- caller can cross-reference the two without re-deriving the "boss_" prefix.
function Compendium.entries(compendium)
local rows = {}
for _, spec in ipairs(Monsters.species) do
rows[#rows + 1] = {
key = spec.key, name = spec.name, flavor = Compendium.FLAVOR[spec.key],
seen = Compendium.hasSeen(compendium, spec.key),
kills = Compendium.killCount(compendium, spec.key),
}
end
for _, spec in ipairs(Bosses.catalog) do
local key = "boss_" .. spec.key
rows[#rows + 1] = {
key = key, name = spec.name, flavor = Compendium.FLAVOR[spec.key],
seen = Compendium.hasSeen(compendium, key),
kills = Compendium.killCount(compendium, key),
}
end
return rows
end
return Compendium
+80
View File
@@ -0,0 +1,80 @@
-- Elemental terrain interaction between open water and the fire/lightning
-- damage types resistances.lua already defines. Stepping into water leaves an
-- entity soaked for a few turns after it moves on, not just on the exact
-- water tile - the lingering dampness other systems read through Conductivity
-- rather than re-checking the tile underfoot themselves. A soaked entity
-- resists catching fire outright (Status.apply refuses a BURNING application,
-- the same way a water tile already douses tile fire in Hazards.step) and,
-- for damage that already lands, dampens further fire (Conductivity.applyFire,
-- used by hazards.lua's fire tick and status.lua's own BURNING dot) while
-- conducting lightning for a bonus (Conductivity.applyLightning, used by
-- spells.lua's shock bolt) - the water clinging to them is exactly what makes
-- a live wire so much worse to be standing in. This is scoped to fire/
-- lightning that already routes through here; an instantaneous hit like
-- lava contact or a breath attack that computes its own damage inline is
-- left alone - only ignition-prevention (resistsIgnition) reaches those.
--
-- LIGHTNING was previously a fully declared Resistances type with nothing in
-- the game that ever dealt it - elites could roll a resist tier for a damage
-- type nothing ever used. Conductivity.applyLightning plus spells.lua's new
-- shock bolt spell are what makes an elite's rolled lightning resistance
-- (see elites.lua) matter for the first time.
local Tiles = require("tiles")
local Resistances = require("resistances")
local Conductivity = {}
Conductivity.SOAK_DURATION = 3
Conductivity.LIGHTNING_BONUS_PERCENT = 50
Conductivity.FIRE_DAMPEN_PERCENT = 50
function Conductivity.isWet(entity)
return (entity.wet or 0) > 0
end
-- Refreshes `entity`'s soak counter to the full duration when it ends its
-- turn standing on open water, or counts it down by one otherwise - the same
-- "renew on cause, decay on absence" shape a stacked status.lua effect's own
-- duration follows, but tracked as a plain entity field since it is a terrain
-- reaction rather than something any status.lua effect key describes.
function Conductivity.tick(level, entity)
if level.map:get(entity.x, entity.y) == Tiles.WATER then
entity.wet = Conductivity.SOAK_DURATION
elseif (entity.wet or 0) > 0 then
entity.wet = entity.wet - 1
end
end
-- Fire damage against a soaked target is dampened on top of whatever
-- Resistances.tierOf already resolves - drenched cloth or hide smoulders
-- rather than catching properly. Immune still floors it at zero.
function Conductivity.applyFire(entity, amount)
local dealt = Resistances.apply(entity, amount, Resistances.FIRE)
if dealt > 0 and Conductivity.isWet(entity) then
dealt = math.max(1, dealt - (dealt * Conductivity.FIRE_DAMPEN_PERCENT) // 100)
end
return dealt
end
-- Lightning damage against a soaked target conducts through the water
-- clinging to them for a bonus on top of whatever Resistances.tierOf already
-- resolves - immune still blocks it outright, same as any other type.
function Conductivity.applyLightning(entity, amount)
local dealt = Resistances.apply(entity, amount, Resistances.LIGHTNING)
if dealt > 0 and Conductivity.isWet(entity) then
dealt = dealt + (dealt * Conductivity.LIGHTNING_BONUS_PERCENT) // 100
end
return dealt
end
-- A soaked target's own dampness snuffs any attempt to set them alight
-- outright, before the BURNING effect is ever recorded (see status.lua's
-- Status.apply) - the same protection a water tile already gives tile fire
-- in Hazards.step, extended to an entity that carries its own wetness onto
-- dry ground.
function Conductivity.resistsIgnition(entity)
return Conductivity.isWet(entity)
end
return Conductivity