From 4c5bae5a0001c2052f5a3c46b7e673749282ac53 Mon Sep 17 00:00:00 2001 From: DualFroz Date: Fri, 4 Sep 2026 11:48:10 +0200 Subject: [PATCH] feat: add Compendium.new, Compendium.noteSighting and Conductivity.isWet --- src/compendium.lua | 93 ++++++++++++++++++++++++++++++++++++++++++++ src/conductivity.lua | 80 +++++++++++++++++++++++++++++++++++++ 2 files changed, 173 insertions(+) create mode 100644 src/compendium.lua create mode 100644 src/conductivity.lua diff --git a/src/compendium.lua b/src/compendium.lua new file mode 100644 index 0000000..76f65a3 --- /dev/null +++ b/src/compendium.lua @@ -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 diff --git a/src/conductivity.lua b/src/conductivity.lua new file mode 100644 index 0000000..24dec0c --- /dev/null +++ b/src/conductivity.lua @@ -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