diff --git a/src/disease.lua b/src/disease.lua new file mode 100644 index 0000000..8eee9e6 --- /dev/null +++ b/src/disease.lua @@ -0,0 +1,131 @@ +-- Progressive affliction caught from a carrier's bite, distinct from both +-- status.lua's per-turn buffs/debuffs (a fixed potency for a fixed +-- duration) and curses.lua's item-bound penalties (permanent until a +-- specific remedy). A disease sits silently through its incubation with no +-- outward sign at all, then erupts and starts dealing damage every turn, +-- worsening to a harsher stage on a fixed clock if nothing cures it first. +-- Its tick damage routes through resistances.lua exactly like poison's own +-- damage-over-time (see status.lua's POISON tick), so a plague-resistant +-- survivor shrugs off what would gut anyone else. A disease is plain data +-- on entity.disease, so it serializes with no special handling; the carrier +-- tag a species bite leaves on entity.biteDisease (see monsters.lua) is +-- copied on exactly like breath or ability already are. + +local Resistances = require("resistances") + +local Disease = {} + +Disease.ROT = "rot" +Disease.PLAGUE = "plague" +Disease.PARASITES = "parasites" + +-- Fixed order so a given rng draw always names the same affliction. +Disease.order = { Disease.ROT, Disease.PLAGUE, Disease.PARASITES } + +Disease.MAX_STAGE = 3 + +Disease.defs = { + [Disease.ROT] = { + name = "wound rot", incubation = 4, stageTurns = 8, + stagePotency = { 2, 4, 7 }, + }, + [Disease.PLAGUE] = { + name = "black plague", incubation = 6, stageTurns = 6, + stagePotency = { 3, 6, 10 }, + }, + [Disease.PARASITES] = { + name = "gut parasites", incubation = 3, stageTurns = 10, + stagePotency = { 1, 3, 5 }, + }, +} + +function Disease.isInfected(entity) + return entity.disease ~= nil +end + +-- Whether the infection has finished incubating and is actively dealing +-- damage each tick, as opposed to sitting silent and undetected. +function Disease.hasErupted(entity) + return entity.disease ~= nil and entity.disease.stage > 0 +end + +-- Seeds a fresh infection of `key` on `entity`, refusing to pile a second +-- affliction on top of one already incubating or erupted - the same +-- one-at-a-time rule status.lua's own stacking already enforces for e.g. +-- poison overwriting duration rather than compounding. Returns whether the +-- infection actually took hold. +function Disease.infect(entity, key) + if Disease.isInfected(entity) then return false end + local def = Disease.defs[key] + if not def then return false end + entity.disease = { key = key, incubation = def.incubation, stage = 0, sinceStage = 0 } + return true +end + +-- Lifts whatever infection `entity` is carrying, incubating or already +-- erupted alike. Returns false without state change when there was nothing +-- to cure. +function Disease.cure(entity) + if not Disease.isInfected(entity) then return false end + entity.disease = nil + return true +end + +-- The stage's flat tick damage before resistance is applied, or 0 while +-- still incubating (no outward symptom yet) or entirely uninfected. +function Disease.stagePotency(entity) + local infection = entity.disease + if not infection or infection.stage == 0 then return 0 end + return Disease.defs[infection.key].stagePotency[infection.stage] +end + +-- One tick per turn on the host's own clock: counts incubation down in +-- silence, breaks out into stage one the moment it reaches zero, then +-- deals that stage's potency each further tick and escalates to the next +-- stage once it has lingered untreated for its full stageTurns window (the +-- top stage simply keeps ticking rather than escalating further). Returns a +-- log line for anything a player would actually notice - the eruption +-- itself, an escalation, or a nonzero tick of damage - and nil for a turn +-- that only ticked incubation down uneventfully. +-- True only for the player, whose entity.name is the pronoun "you" - a +-- subject that takes plain ("you break out", "you are wracked") verb forms +-- in English rather than the third-person-singular ("breaks out", "is +-- wracked") a named host like "orc" takes. Every message below branches on +-- this exactly like Game:springTrap and Game:checkTerrainFor already branch +-- on the same pronoun problem elsewhere in the engine. +local function isPlayer(entity) + return entity.kind == "player" +end + +function Disease.tick(entity) + local infection = entity.disease + if not infection then return nil end + local def = Disease.defs[infection.key] + if infection.stage == 0 then + infection.incubation = infection.incubation - 1 + if infection.incubation <= 0 then + infection.stage = 1 + infection.sinceStage = 0 + return isPlayer(entity) and string.format("you break out in %s", def.name) + or string.format("%s breaks out in %s", entity.name, def.name) + end + return nil + end + local dealt = Resistances.apply(entity, def.stagePotency[infection.stage], Resistances.POISON) + entity.hp = entity.hp - dealt + infection.sinceStage = infection.sinceStage + 1 + local line = nil + if dealt > 0 then + line = isPlayer(entity) and string.format("you are wracked by %s for %d", def.name, dealt) + or string.format("%s is wracked by %s for %d", entity.name, def.name, dealt) + end + if infection.sinceStage >= def.stageTurns and infection.stage < Disease.MAX_STAGE then + infection.stage = infection.stage + 1 + infection.sinceStage = 0 + line = isPlayer(entity) and string.format("your %s worsens", def.name) + or string.format("%s's %s worsens", entity.name, def.name) + end + return line +end + +return Disease