feat: add Ascension.isUnlocked, Ascension.resolve
This commit is contained in:
1 parent
023fa35caf
commit
fbfe8411ac
2 files changed
+185
No files matched your search
@@ -0,0 +1,136 @@
|
|||||||
|
-- Optional run-wide difficulty mutators a player opts into before a run
|
||||||
|
-- starts, in exchange for extra renown once it ends: risk taken on purpose,
|
||||||
|
-- unlike events.lua's mutators which fire and fade on their own mid-run
|
||||||
|
-- regardless of anyone's choice. A mutator unlocks once meta.lua's own
|
||||||
|
-- cross-run tallies (best depth, wins) cross its fixed requirement -
|
||||||
|
-- computed live off those tallies rather than persisted, since both only
|
||||||
|
-- ever climb, so nothing here needs its own entry in meta.lua's save shape.
|
||||||
|
--
|
||||||
|
-- Active mutators for the run in progress live on game.ascension, a plain
|
||||||
|
-- array of keys in catalog order (see Ascension.resolve), exactly the shape
|
||||||
|
-- warren.lua's --mutators flag builds. Plain data, so it serializes with no
|
||||||
|
-- special save-format handling, and every combat/essence/sight hook that
|
||||||
|
-- reads it (Game:monsterDamageMultiplier, Game:essenceMultiplier,
|
||||||
|
-- Game:effectiveRadius) stays a pure function of that array alongside
|
||||||
|
-- whatever level-scoped event Events.lua itself is contributing.
|
||||||
|
|
||||||
|
local Ascension = {}
|
||||||
|
|
||||||
|
Ascension.MUTATORS = {
|
||||||
|
{ key = "bloodbound", name = "Bloodbound", desc = "monsters hit 50% harder",
|
||||||
|
renownBonus = 15, requiresDepth = 3, requiresWins = 0, damageMultiplier = 1.5 },
|
||||||
|
{ key = "famine", name = "Famine", desc = "essence income halved",
|
||||||
|
renownBonus = 10, requiresDepth = 2, requiresWins = 0, essenceMultiplier = 0.5 },
|
||||||
|
{ key = "nightblind", name = "Nightblind", desc = "sight radius reduced by 2",
|
||||||
|
renownBonus = 10, requiresDepth = 2, requiresWins = 0, sightPenalty = 2 },
|
||||||
|
{ key = "frail", name = "Frail", desc = "defense halved",
|
||||||
|
renownBonus = 12, requiresDepth = 4, requiresWins = 0,
|
||||||
|
applyToPlayer = function(player)
|
||||||
|
player.baseDefense = player.baseDefense // 2
|
||||||
|
player.defense = player.defense // 2
|
||||||
|
end },
|
||||||
|
{ key = "glasscannon", name = "Glass Cannon", desc = "max hp halved, attack doubled",
|
||||||
|
renownBonus = 20, requiresDepth = 0, requiresWins = 1,
|
||||||
|
applyToPlayer = function(player)
|
||||||
|
player.maxHp = math.max(1, player.maxHp // 2)
|
||||||
|
player.hp = math.min(player.hp, player.maxHp)
|
||||||
|
player.baseAttack = player.baseAttack * 2
|
||||||
|
player.attack = player.attack * 2
|
||||||
|
end },
|
||||||
|
}
|
||||||
|
|
||||||
|
Ascension.byKey = {}
|
||||||
|
Ascension.order = {}
|
||||||
|
for _, def in ipairs(Ascension.MUTATORS) do
|
||||||
|
Ascension.byKey[def.key] = def
|
||||||
|
Ascension.order[#Ascension.order + 1] = def.key
|
||||||
|
end
|
||||||
|
|
||||||
|
-- A meta-less caller (no --meta flag at all) can still activate whatever
|
||||||
|
-- costs nothing to unlock; every other mutator needs a persistent meta file
|
||||||
|
-- to have already banked the depth or win it requires.
|
||||||
|
function Ascension.isUnlocked(meta, key)
|
||||||
|
local def = Ascension.byKey[key]
|
||||||
|
if not def then return false end
|
||||||
|
if not meta then return def.requiresDepth == 0 and def.requiresWins == 0 end
|
||||||
|
return meta.bestDepth >= def.requiresDepth and meta.wins >= def.requiresWins
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Validates a requested list of mutator keys against both the catalog and
|
||||||
|
-- `meta`'s own unlock state, returned de-duplicated in catalog order rather
|
||||||
|
-- than whatever order the caller supplied - so two runs activating the same
|
||||||
|
-- set always see it identically regardless of how --mutators listed them.
|
||||||
|
-- An unknown or still-locked key is silently dropped rather than failing
|
||||||
|
-- the whole run over one bad flag.
|
||||||
|
function Ascension.resolve(meta, requested)
|
||||||
|
if not requested or #requested == 0 then return {} end
|
||||||
|
local wanted = {}
|
||||||
|
for _, key in ipairs(requested) do wanted[key] = true end
|
||||||
|
local active = {}
|
||||||
|
for _, key in ipairs(Ascension.order) do
|
||||||
|
if wanted[key] and Ascension.isUnlocked(meta, key) then
|
||||||
|
active[#active + 1] = key
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return active
|
||||||
|
end
|
||||||
|
|
||||||
|
function Ascension.isActive(active, key)
|
||||||
|
for _, k in ipairs(active or {}) do
|
||||||
|
if k == key then return true end
|
||||||
|
end
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
function Ascension.damageMultiplier(active)
|
||||||
|
local mult = 1
|
||||||
|
for _, key in ipairs(active or {}) do
|
||||||
|
local def = Ascension.byKey[key]
|
||||||
|
if def and def.damageMultiplier then mult = mult * def.damageMultiplier end
|
||||||
|
end
|
||||||
|
return mult
|
||||||
|
end
|
||||||
|
|
||||||
|
function Ascension.essenceMultiplier(active)
|
||||||
|
local mult = 1
|
||||||
|
for _, key in ipairs(active or {}) do
|
||||||
|
local def = Ascension.byKey[key]
|
||||||
|
if def and def.essenceMultiplier then mult = mult * def.essenceMultiplier end
|
||||||
|
end
|
||||||
|
return mult
|
||||||
|
end
|
||||||
|
|
||||||
|
function Ascension.sightPenalty(active)
|
||||||
|
local penalty = 0
|
||||||
|
for _, key in ipairs(active or {}) do
|
||||||
|
local def = Ascension.byKey[key]
|
||||||
|
if def and def.sightPenalty then penalty = penalty + def.sightPenalty end
|
||||||
|
end
|
||||||
|
return penalty
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Renown bonus percentage this active set adds on top of Meta.renownFor's
|
||||||
|
-- own base score, summed rather than compounded - three 15% mutators add
|
||||||
|
-- 45%, not a runaway 1.15^3 - so stacking more of them is always a
|
||||||
|
-- straightforward trade of more risk for more reward.
|
||||||
|
function Ascension.renownBonusPercent(active)
|
||||||
|
local percent = 0
|
||||||
|
for _, key in ipairs(active or {}) do
|
||||||
|
local def = Ascension.byKey[key]
|
||||||
|
if def then percent = percent + def.renownBonus end
|
||||||
|
end
|
||||||
|
return percent
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Applies every active mutator's one-time player-stat delta, in catalog
|
||||||
|
-- order. Meant to run once in Game.new, after Meta.applyAll's perks and any
|
||||||
|
-- class delta, since a mutator is the final, voluntary layer of difficulty
|
||||||
|
-- stacked on top of whatever build the player already chose.
|
||||||
|
function Ascension.applyToPlayer(active, player)
|
||||||
|
for _, key in ipairs(active or {}) do
|
||||||
|
local def = Ascension.byKey[key]
|
||||||
|
if def and def.applyToPlayer then def.applyToPlayer(player) end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return Ascension
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
-- Floor-wide weather: a single condition drawn once at generation (like
|
||||||
|
-- `dark`) that colors the whole level rather than one cell of it. Unlike
|
||||||
|
-- terrain.lua's per-tile hazards (lava, chasm, ice), a floor's weather has no
|
||||||
|
-- position of its own - every numeric hook below is a flat, floor-lifetime
|
||||||
|
-- modifier the caller applies once per turn or once at generation, never
|
||||||
|
-- something a player can step in or out of.
|
||||||
|
|
||||||
|
local Atmosphere = {}
|
||||||
|
|
||||||
|
Atmosphere.FOGGY = "foggy"
|
||||||
|
Atmosphere.CONSECRATED = "consecrated"
|
||||||
|
Atmosphere.INFESTED = "infested"
|
||||||
|
|
||||||
|
-- Fixed order so a given rng draw always names the same condition.
|
||||||
|
Atmosphere.order = { Atmosphere.FOGGY, Atmosphere.CONSECRATED, Atmosphere.INFESTED }
|
||||||
|
|
||||||
|
-- A thick, clinging fog: everyone's sight radius shrinks, monsters and traps
|
||||||
|
-- included, before the darkness/torch radius in Game:effectiveRadius is
|
||||||
|
-- even considered.
|
||||||
|
function Atmosphere.sightPenalty(weather)
|
||||||
|
if weather == Atmosphere.FOGGY then return 2 end
|
||||||
|
return 0
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Consecrated ground mends the living each turn they linger on it, exactly
|
||||||
|
-- like an artifact's fixed regen (see artifacts.ringRegen) but floor-wide
|
||||||
|
-- and free of any equipment.
|
||||||
|
function Atmosphere.regenBonus(weather)
|
||||||
|
if weather == Atmosphere.CONSECRATED then return 1 end
|
||||||
|
return 0
|
||||||
|
end
|
||||||
|
|
||||||
|
-- An infested floor's monsters hit harder from the moment they are placed -
|
||||||
|
-- a flat, permanent bump baked into their stats at generation time (see
|
||||||
|
-- builder.lua), the same way an elite affix or a boss's own catalog entry
|
||||||
|
-- already sets a monster's power once and for all.
|
||||||
|
function Atmosphere.monsterAttackBonus(weather)
|
||||||
|
if weather == Atmosphere.INFESTED then return 2 end
|
||||||
|
return 0
|
||||||
|
end
|
||||||
|
|
||||||
|
function Atmosphere.describe(weather)
|
||||||
|
if weather == Atmosphere.FOGGY then return "a thick fog blankets this floor" end
|
||||||
|
if weather == Atmosphere.CONSECRATED then return "this floor is consecrated ground" end
|
||||||
|
if weather == Atmosphere.INFESTED then return "this floor teems with infestation" end
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
|
||||||
|
return Atmosphere
|
||||||
Reference in new issue
Block a user