feat: extend horde with Horde.essenceFor and Horde.pool
This commit is contained in:
1 parent
58994a6132
commit
1f4ea71a1f
3 files changed
+198
-1
No files matched your search
@@ -157,3 +157,88 @@ function Horde.countFor(wave)
|
||||
return count
|
||||
end
|
||||
|
||||
function Horde.essenceFor(wave)
|
||||
return wave * Horde.ESSENCE_PER_WAVE
|
||||
end
|
||||
|
||||
-- Species eligible for wave `wave`, drawn from Monsters.forDepth at
|
||||
-- Horde.depthFor(wave) exactly like a real floor's population, minus the
|
||||
-- passive vend-behaviour shopkeeper - a wave the player can never provoke
|
||||
-- into fighting back would leave Horde.cleared permanently false.
|
||||
function Horde.pool(wave)
|
||||
local eligible = {}
|
||||
for _, spec in ipairs(Monsters.forDepth(Horde.depthFor(wave))) do
|
||||
if spec.ai ~= "vend" then eligible[#eligible + 1] = spec end
|
||||
end
|
||||
return eligible
|
||||
end
|
||||
|
||||
-- True once every hostile monster the arena has ever placed is dead -
|
||||
-- vacuously true against a still-empty arena too, which is exactly what
|
||||
-- lets Horde.tick reuse the same check to bootstrap wave 1 as it does to
|
||||
-- clear every wave after it (see Horde.tick's own comment). Filtered to
|
||||
-- entity.kind == "monster", the same "hostile" tag every other module
|
||||
-- (companions.lua/mercenaries.lua/reanimation.lua/abilities.lua...) uses:
|
||||
-- Game:startHorde can now carry a bonded companion/hireling/thrall into the
|
||||
-- arena, and an ally that never dies to the wave itself would otherwise
|
||||
-- keep this permanently false, freezing the arena on wave 0 forever.
|
||||
function Horde.cleared(level)
|
||||
for _, entity in ipairs(level.entities) do
|
||||
if entity.kind == "monster" and entity:isAlive() then return false end
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
-- Populates `game.level` with wave `wave`'s monsters, drawn from
|
||||
-- Horde.pool and placed one to a cell along Horde.spawnPoints's queue -
|
||||
-- never doubling two monsters onto the same cell, the one-entity-per-cell
|
||||
-- invariant every other spawn path in game.lua already relies on. Every
|
||||
-- spawn is already hunting, exactly like Game:spawnMinion's own summons -
|
||||
-- an arena has no stealth approach to sneak past. Returns the spawned
|
||||
-- entities.
|
||||
function Horde.spawnWave(game, wave)
|
||||
local level = game.level
|
||||
local points = Horde.spawnPoints(level.map)
|
||||
local pool = Horde.pool(wave)
|
||||
local count = math.min(Horde.countFor(wave), #points)
|
||||
local depth = Horde.depthFor(wave)
|
||||
local spawned = {}
|
||||
for i = 1, count do
|
||||
local point = points[i]
|
||||
local spec = game.rng:pick(pool)
|
||||
local entity = Monsters.create(spec, game:nextMonsterId(), point.x, point.y, depth)
|
||||
entity.awareness = Stealth.HUNTING
|
||||
Elites.maybePromote(game.rng, entity, depth)
|
||||
level.entities[#level.entities + 1] = entity
|
||||
spawned[#spawned + 1] = entity
|
||||
end
|
||||
return spawned
|
||||
end
|
||||
|
||||
-- Advances horde state by one player turn, called from Game:endPlayerTurn
|
||||
-- behind a `game.horde` check. A no-op while the current wave still has a
|
||||
-- living monster in it; the instant it does not - including the very first
|
||||
-- call, against the still-empty arena Game:startHorde just built, since
|
||||
-- Horde.cleared reads that the same way as a genuinely finished wave - it
|
||||
-- banks Horde.essenceFor(wave) essence (zero the first time, wave still
|
||||
-- being 0), heals Horde.REST_HEAL, advances the wave counter, and spawns
|
||||
-- the new wave outright, so the arena is never left standing empty for
|
||||
-- even a single turn. Returns a log line, or nil when nothing happened.
|
||||
function Horde.tick(game)
|
||||
local horde = game.horde
|
||||
if not horde then return nil end
|
||||
if not Horde.cleared(game.level) then return nil end
|
||||
local reward = Horde.essenceFor(horde.wave)
|
||||
if reward > 0 then
|
||||
game.player.essence = game.player.essence + reward
|
||||
end
|
||||
game.player.hp = math.min(game.player.maxHp, game.player.hp + Horde.REST_HEAL)
|
||||
horde.wave = horde.wave + 1
|
||||
Horde.spawnWave(game, horde.wave)
|
||||
if horde.wave == 1 then
|
||||
return "the horde arena falls silent - wave 1 begins"
|
||||
end
|
||||
return string.format("the arena falls silent - wave %d begins (+%d essence)", horde.wave, reward)
|
||||
end
|
||||
|
||||
return Horde
|
||||
+112
@@ -0,0 +1,112 @@
|
||||
-- Named bands layered over the player's raw satiation counter, the same
|
||||
-- shape sanity.lua already gives its own slow-moving meter: a plain number
|
||||
-- that Game:hungerTorchTick drains by one every round (untouched here),
|
||||
-- classified into FED/HUNGRY/WEAK/STARVING tiers that a caller reads back
|
||||
-- instead of re-deriving the cutoffs itself. Falling into WEAK or worse
|
||||
-- costs real attack and defense - wired into entity.lua's effectiveAttack/
|
||||
-- effectiveDefense alongside Sanity's own penalty, so a gaunt fighter hits
|
||||
-- and blocks softer without combat.lua needing to know hunger exists at
|
||||
-- all - and blocks the passive mending hungerTorchTick already grants from
|
||||
-- a worn ring, an ironclad set or consecrated ground: a starving adventurer
|
||||
-- can still down a potion, just not coast on a ring of regeneration while
|
||||
-- fading. STARVING alone keeps hungerTorchTick's own flat hp drain, exactly
|
||||
-- as before.
|
||||
--
|
||||
-- Eating is the mirror case: Hunger.eat folds fresh nutrition into the same
|
||||
-- counter, capped at Hunger.CAP, but food eaten while already at that cap
|
||||
-- is entirely wasted and leaves the diner Status.SLOW for a few turns - the
|
||||
-- same overfull stumble a real glutton earns, reusing a status the engine
|
||||
-- already ticks, serializes and renders with no special handling of its own.
|
||||
|
||||
local Status = require("status")
|
||||
|
||||
local Hunger = {}
|
||||
|
||||
Hunger.FED = "fed"
|
||||
Hunger.HUNGRY = "hungry"
|
||||
Hunger.WEAK = "weak"
|
||||
Hunger.STARVING = "starving"
|
||||
|
||||
-- Starting ration for a fresh game (Game.new) and the ceiling Hunger.eat
|
||||
-- refuses to bank past. Comfortably above START so a couple of found
|
||||
-- rations can stack up before a long dive, without letting the pool grow
|
||||
-- without bound.
|
||||
Hunger.START = 1000
|
||||
Hunger.CAP = 1200
|
||||
|
||||
-- Tier ceilings: a value at or below HUNGRY_MAX but above WEAK_MAX reads as
|
||||
-- HUNGRY, and so on downward. 0 itself is always STARVING regardless of
|
||||
-- where these ceilings sit.
|
||||
Hunger.HUNGRY_MAX = 300
|
||||
Hunger.WEAK_MAX = 100
|
||||
|
||||
Hunger.BLOAT_DURATION = 4
|
||||
|
||||
local ATTACK_PENALTY = {
|
||||
[Hunger.FED] = 0, [Hunger.HUNGRY] = 0, [Hunger.WEAK] = -2, [Hunger.STARVING] = -4,
|
||||
}
|
||||
local DEFENSE_PENALTY = {
|
||||
[Hunger.FED] = 0, [Hunger.HUNGRY] = 0, [Hunger.WEAK] = -1, [Hunger.STARVING] = -3,
|
||||
}
|
||||
|
||||
-- A fresh entity - or a monster, which never accrues hunger of its own
|
||||
-- since nothing ever calls Hunger.tick for one - reads as fully fed.
|
||||
function Hunger.current(entity)
|
||||
return entity.satiation or Hunger.CAP
|
||||
end
|
||||
|
||||
function Hunger.tierOf(entity)
|
||||
local value = Hunger.current(entity)
|
||||
if value <= 0 then return Hunger.STARVING end
|
||||
if value <= Hunger.WEAK_MAX then return Hunger.WEAK end
|
||||
if value <= Hunger.HUNGRY_MAX then return Hunger.HUNGRY end
|
||||
return Hunger.FED
|
||||
end
|
||||
|
||||
function Hunger.attackPenalty(entity)
|
||||
return ATTACK_PENALTY[Hunger.tierOf(entity)]
|
||||
end
|
||||
|
||||
function Hunger.defensePenalty(entity)
|
||||
return DEFENSE_PENALTY[Hunger.tierOf(entity)]
|
||||
end
|
||||
|
||||
-- WEAK or worse blocks the passive regen hungerTorchTick otherwise grants
|
||||
-- from a ring, a set bonus or friendly weather - a fading body has nothing
|
||||
-- left over to spend on mending.
|
||||
function Hunger.suppressesRegen(entity)
|
||||
local tier = Hunger.tierOf(entity)
|
||||
return tier == Hunger.WEAK or tier == Hunger.STARVING
|
||||
end
|
||||
|
||||
-- One round of drain. Returns a log line only when the tier actually
|
||||
-- changes, except STARVING which - matching the flat drain this replaces -
|
||||
-- reports and bites for 1 hp every single round it holds, not just on the
|
||||
-- turn it is first reached.
|
||||
function Hunger.tick(entity)
|
||||
local before = Hunger.tierOf(entity)
|
||||
entity.satiation = math.max(0, Hunger.current(entity) - 1)
|
||||
local after = Hunger.tierOf(entity)
|
||||
if after == Hunger.STARVING then
|
||||
entity.hp = entity.hp - 1
|
||||
return "you are starving"
|
||||
end
|
||||
if after ~= before then
|
||||
return "you feel " .. after
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
-- Feeds `nutrition` in, clamped at Hunger.CAP. Returns true when the diner
|
||||
-- was already at the cap - all of it wasted, and left Status.SLOW for
|
||||
-- Hunger.BLOAT_DURATION turns - or false for an ordinary meal that landed.
|
||||
function Hunger.eat(entity, nutrition)
|
||||
local wasFull = Hunger.current(entity) >= Hunger.CAP
|
||||
entity.satiation = math.min(Hunger.CAP, Hunger.current(entity) + nutrition)
|
||||
if wasFull then
|
||||
Status.apply(entity, Status.SLOW, Hunger.BLOAT_DURATION, 1)
|
||||
end
|
||||
return wasFull
|
||||
end
|
||||
|
||||
return Hunger
|
||||
+1
-1
@@ -8,7 +8,7 @@ local Items = require("items")
|
||||
local Identify = {}
|
||||
|
||||
local potionLooks = {
|
||||
"ruby", "azure", "cloudy", "fizzy", "smoky", "golden", "murky", "viscous",
|
||||
"ruby", "azure", "cloudy", "fizzy", "smoky", "golden", "murky", "viscous", "inky", "frothy",
|
||||
}
|
||||
local scrollLooks = {
|
||||
"XYZZY", "ELBERETH", "ZELGO KAI", "JUYED AWK", "VE FORBRYDERNE",
|
||||
|
||||
Reference in new issue
Block a user