feat: extend mimics with Mimics.floorItem and Mimics.reveal

This commit is contained in:
dualfroz committed 2026-09-07 14:30:20 +02:00
1 parent 06714fc72a
commit 6a26f1f65e
3 files changed
+160 -11

No files matched your search

+60
View File
@@ -0,0 +1,60 @@
-- A mimic disguises itself as ordinary loot until a hand reaches for it.
-- Builder.build drops one as a plain floorItem tagged `mimic = true`,
-- wearing a random disguise's name and glyph (Mimics.DISGUISES) - utterly
-- indistinguishable at rest from a real item, since render.lua and
-- Game:pickupHere both just read an ordinary floorItem's own `.item.glyph`
-- and `.item.name`, with no idea a given tile might be a trap. The instant
-- the player steps onto it (Game:triggerMimic, called from pickupHere),
-- Mimics.reveal builds the real hostile monster underneath, scaled by
-- depth the same way Monsters.create scales a wild species's hp, and gets
-- one free ambush swing in (via combat.lua, so it still respects the
-- player's defense and resistances) before it settles into an ordinary
-- chase like any other monster on its own turns from then on.
local Entity = require("entity")
local Factions = require("factions")
local Mimics = {}
Mimics.FACTION = Factions.BEAST
Mimics.DISGUISES = {
{ name = "a weathered chest", glyph = "=" },
{ name = "a glinting pile of gold", glyph = "$" },
{ name = "an ornate tome", glyph = "&" },
{ name = "a discarded shield", glyph = "[" },
}
-- The inert floorItem a builder drops: it looks exactly like whichever
-- disguise it rolled until Game:pickupHere unmasks it.
function Mimics.floorItem(rng)
local disguise = Mimics.DISGUISES[rng:range(1, #Mimics.DISGUISES)]
return { kind = "mimic", mimic = true, name = disguise.name, glyph = disguise.glyph }
end
-- The hostile monster a disguised item becomes the instant it is
-- disturbed, its hp scaled by depth exactly the way Monsters.create scales
-- a wild species's own base hp.
function Mimics.reveal(id, x, y, depth)
local bonusHp = depth - 1
local entity = Entity.new({
id = id, x = x, y = y,
glyph = "M", name = "mimic", kind = "monster",
species = "mimic", ai = "chase",
hp = 12 + bonusHp, maxHp = 12 + bonusHp,
attack = 5, defense = 1, speed = 100,
})
entity.cooldown = 0
entity.faction = Mimics.FACTION
entity.mimic = true
return entity
end
-- Flat xp reward for felling a revealed mimic - kept separate from
-- Monsters.xpFor since a mimic is never a member of the wild bestiary
-- Monsters.forDepth draws its ordinary per-floor population from.
function Mimics.xpFor()
return 10
end
return Mimics
+39
View File
@@ -0,0 +1,39 @@
-- A player-only combo meter that rewards sustained melee aggression: every
-- landed melee swing raises the stack by one, up to a hard cap, and each
-- stack adds a flat attack bonus that reads live through
-- Entity:effectiveAttack - the very next swing already hits harder, not
-- just some future one. Taking a hit from a monster's claw, breath or
-- ambush snaps the whole stack back to zero outright - unlike a status
-- effect's timed decay, there is no partial forgiveness, so holding a combo
-- against several foes at once is a far harder rhythm than a lone duel. An
-- evaded attack (see skills.lua) never reaches Combat.attack at all, so it
-- never breaks the stack either - only a blow that actually lands does.
-- Ranged shots, thrown items and environmental damage (traps, poison,
-- lava) neither build nor break it: momentum is specifically a melee-vs-
-- monster reward, not a universal combat buff.
local Momentum = {}
Momentum.MAX_STACKS = 5
Momentum.BONUS_PER_STACK = 1
function Momentum.stacks(entity)
return entity.combo or 0
end
-- Raises `entity`'s stack by one landed melee hit, capped at MAX_STACKS.
function Momentum.onMeleeHit(entity)
entity.combo = math.min(Momentum.MAX_STACKS, Momentum.stacks(entity) + 1)
end
-- Snaps the stack back to zero the instant `entity` takes a hit.
function Momentum.onDamaged(entity)
entity.combo = 0
end
-- The flat attack bonus `entity`'s current stack grants.
function Momentum.attackBonus(entity)
return Momentum.stacks(entity) * Momentum.BONUS_PER_STACK
end
return Momentum
+61 -11
View File
@@ -4,20 +4,41 @@
-- depth so lower floors bite harder.
local Entity = require("entity")
local Factions = require("factions")
local Disease = require("disease")
local Monsters = {}
Monsters.species = {
{ key = "rat", glyph = "r", name = "rat", hp = 4, attack = 2, defense = 0, speed = 110, ai = "chase", minDepth = 1 },
{ key = "bat", glyph = "v", name = "cave bat", hp = 3, attack = 2, defense = 0, speed = 150, ai = "erratic", minDepth = 1 },
{ key = "kobold", glyph = "k", name = "kobold", hp = 6, attack = 3, defense = 1, speed = 100, ai = "pack", minDepth = 1 },
{ key = "goblin", glyph = "g", name = "goblin", hp = 5, attack = 3, defense = 0, speed = 110, ai = "pack", minDepth = 2 },
{ key = "archer", glyph = "a", name = "skeleton archer", hp = 6, attack = 3, defense = 1, speed = 100, ai = "ranged", minDepth = 2, range = 5 },
{ key = "orc", glyph = "o", name = "orc", hp = 10, attack = 4, defense = 1, speed = 100, ai = "chase", minDepth = 3 },
{ key = "wretch", glyph = "w", name = "cave wretch", hp = 8, attack = 2, defense = 0, speed = 120, ai = "flee", minDepth = 3 },
{ key = "troll", glyph = "T", name = "troll", hp = 16, attack = 6, defense = 2, speed = 80, ai = "chase", minDepth = 4 },
{ key = "mage", glyph = "m", name = "dark mage", hp = 8, attack = 2, defense = 0, speed = 100, ai = "ranged", minDepth = 5, range = 6 },
{ key = "dragon", glyph = "D", name = "young dragon", hp = 30, attack = 8, defense = 3, speed = 90, ai = "ranged", minDepth = 6, range = 4 },
{ key = "rat", glyph = "r", name = "rat", hp = 4, attack = 2, defense = 0, speed = 110, ai = "chase", minDepth = 1, xp = 3, faction = Factions.VERMIN, disease = Disease.PARASITES },
{ key = "bat", glyph = "v", name = "cave bat", hp = 3, attack = 2, defense = 0, speed = 150, ai = "erratic", minDepth = 1, xp = 3, faction = Factions.VERMIN },
{ key = "kobold", glyph = "k", name = "kobold", hp = 6, attack = 3, defense = 1, speed = 100, ai = "pack", minDepth = 1, xp = 5, faction = Factions.GOBLINOID, disease = Disease.PLAGUE },
{ key = "goblin", glyph = "g", name = "goblin", hp = 5, attack = 3, defense = 0, speed = 110, ai = "pack", minDepth = 2, xp = 5, faction = Factions.GOBLINOID },
{ key = "archer", glyph = "a", name = "skeleton archer", hp = 6, attack = 3, defense = 1, speed = 100, ai = "ranged", minDepth = 2, range = 5, xp = 6, faction = Factions.UNDEAD },
{ key = "orc", glyph = "o", name = "orc", hp = 10, attack = 4, defense = 1, speed = 100, ai = "chase", minDepth = 3, xp = 9, faction = Factions.ORCISH },
{ key = "wretch", glyph = "w", name = "cave wretch", hp = 8, attack = 2, defense = 0, speed = 120, ai = "flee", minDepth = 3, xp = 6, onDeath = "spores", faction = Factions.BEAST },
{ key = "troll", glyph = "T", name = "troll", hp = 16, attack = 6, defense = 2, speed = 80, ai = "chase", minDepth = 4, xp = 15, regen = 3, resist = { fire = "weak" }, faction = Factions.BEAST },
{ key = "mage", glyph = "m", name = "dark mage", hp = 8, attack = 2, defense = 0, speed = 100, ai = "ranged", minDepth = 5, range = 6, xp = 12, faction = Factions.CULT },
{ key = "dragon", glyph = "D", name = "young dragon", hp = 30, attack = 8, defense = 3, speed = 90, ai = "ranged", minDepth = 6, range = 4, xp = 25, breath = "fire", resist = { fire = "immune", cold = "weak" }, faction = Factions.DRAKE },
-- Ability bearers: each carries an `ability` tag resolved by abilities.lua's
-- tryAct dispatch, which runs before the plain ai.lua behaviour listed here
-- and only falls through to it when the ability is on cooldown or has no
-- valid target. minDepth is kept at 4+ so these never appear on the shallow
-- floors exercised by the fixed-maxDepth win-condition tests.
{ key = "zombie", glyph = "z", name = "shambling zombie", hp = 7, attack = 3, defense = 1, speed = 90, ai = "chase", minDepth = 4, xp = 5, faction = Factions.UNDEAD, disease = Disease.ROT },
{ key = "slime", glyph = "j", name = "gelatinous slime", hp = 10, attack = 2, defense = 0, speed = 90, ai = "chase", minDepth = 4, xp = 5, ability = "split", faction = Factions.BEAST },
{ key = "footpad", glyph = "f", name = "footpad", hp = 6, attack = 2, defense = 0, speed = 120, ai = "chase", minDepth = 4, xp = 6, ability = "steal", faction = Factions.BANDIT },
{ key = "shaman", glyph = "s", name = "goblin shaman", hp = 7, attack = 2, defense = 0, speed = 100, ai = "flee", minDepth = 4, xp = 9, ability = "curse", faction = Factions.GOBLINOID },
{ key = "necromancer", glyph = "n", name = "necromancer", hp = 9, attack = 2, defense = 0, speed = 95, ai = "flee", minDepth = 4, xp = 10, ability = "summon", summonSpecies = "zombie", faction = Factions.CULT },
{ key = "cleric", glyph = "c", name = "dark cleric", hp = 8, attack = 2, defense = 1, speed = 100, ai = "pack", minDepth = 5, xp = 9, ability = "heal", faction = Factions.CULT },
{ key = "chieftain", glyph = "C", name = "orc chieftain", hp = 14, attack = 5, defense = 2, speed = 100, ai = "pack", minDepth = 6, xp = 14, ability = "rally", faction = Factions.GOBLINOID },
{ key = "banshee", glyph = "b", name = "banshee", hp = 7, attack = 2, defense = 0, speed = 100, ai = "flee", minDepth = 5, xp = 11, ability = "shriek", faction = Factions.UNDEAD },
-- Shopkeeper: a passive vendor guarding priced wares (see shop.lua). Its
-- "vend" behaviour idles until game.lua flags it hostile after a theft, at
-- which point it falls back to a plain chase like any other monster.
{ key = "shopkeeper", glyph = "h", name = "shopkeeper", hp = 8, attack = 3, defense = 1, speed = 100, ai = "vend", minDepth = 4, xp = 7, faction = Factions.MERCHANT },
}
Monsters.byKey = {}
@@ -36,9 +57,28 @@ function Monsters.forDepth(depth)
return eligible
end
-- Xp reward for felling an entity, looked up by the species key stamped onto
-- it at creation. Falls back to a flat amount for anything without one.
function Monsters.xpFor(entity)
local spec = Monsters.byKey[entity.species]
return spec and spec.xp or 4
end
-- A fresh copy of a species' innate resist table, never the catalog's own
-- object: elites.lua's "warded" affix mutates entity.resist in place once
-- rolled, and a shared reference there would leak that bump onto every
-- other spawn of the same species for the rest of the process, not just
-- the one entity actually promoted.
local function copyResist(resist)
if not resist then return nil end
local copy = {}
for damageType, tier in pairs(resist) do copy[damageType] = tier end
return copy
end
function Monsters.create(spec, id, x, y, depth)
local bonusHp = depth - 1
return Entity.new({
local entity = Entity.new({
id = id,
x = x, y = y,
glyph = spec.glyph, name = spec.name, kind = "monster",
@@ -47,6 +87,16 @@ function Monsters.create(spec, id, x, y, depth)
attack = spec.attack, defense = spec.defense,
speed = spec.speed, range = spec.range or 0,
})
entity.regen = spec.regen
entity.onDeath = spec.onDeath
entity.breath = spec.breath
entity.ability = spec.ability
entity.summonSpecies = spec.summonSpecies
entity.cooldown = 0
entity.resist = copyResist(spec.resist)
entity.faction = spec.faction
entity.biteDisease = spec.disease
return entity
end
return Monsters