feat: add tierFrom, Resistances.tierOf and wordKey
This commit is contained in:
1 parent
63a9fb5f90
commit
cf286f9da3
2 files changed
+144
No files matched your search
@@ -0,0 +1,67 @@
|
|||||||
|
-- Elemental damage typing. Every hit that lands - melee, a bolt, a thrown
|
||||||
|
-- weapon, a hazard tick, a status effect's damage-over-time - is tagged with
|
||||||
|
-- one of the types below, and this module decides how much of it actually
|
||||||
|
-- gets through. A defender's tier for a type comes from up to two places: its
|
||||||
|
-- own base profile (bestiary data, e.g. a dragon's own hide) and whatever it
|
||||||
|
-- has equipped (weapon/armor/ring/bow, e.g. a ring of cold resistance). The
|
||||||
|
-- single best (most protective) tier across those sources wins - immune beats
|
||||||
|
-- resist beats weak beats the normal default - so a found ring can cancel out
|
||||||
|
-- an innate vulnerability, but nothing ever stacks two resists into a second
|
||||||
|
-- discount or two weaknesses into a bigger penalty.
|
||||||
|
|
||||||
|
local Resistances = {}
|
||||||
|
|
||||||
|
Resistances.PHYSICAL = "physical"
|
||||||
|
Resistances.FIRE = "fire"
|
||||||
|
Resistances.COLD = "cold"
|
||||||
|
Resistances.POISON = "poison"
|
||||||
|
Resistances.LIGHTNING = "lightning"
|
||||||
|
Resistances.ARCANE = "arcane"
|
||||||
|
|
||||||
|
Resistances.TYPES = {
|
||||||
|
Resistances.PHYSICAL, Resistances.FIRE, Resistances.COLD,
|
||||||
|
Resistances.POISON, Resistances.LIGHTNING, Resistances.ARCANE,
|
||||||
|
}
|
||||||
|
|
||||||
|
Resistances.IMMUNE = "immune"
|
||||||
|
Resistances.RESIST = "resist"
|
||||||
|
Resistances.WEAK = "weak"
|
||||||
|
Resistances.NORMAL = "normal"
|
||||||
|
|
||||||
|
local RANK = { immune = 3, resist = 2, weak = 1, normal = 0 }
|
||||||
|
|
||||||
|
-- Slots checked alongside an entity's own `resist` table, in a fixed order so
|
||||||
|
-- the result never depends on Lua's unspecified pairs() iteration order.
|
||||||
|
local EQUIP_SLOTS = { "weapon", "armor", "ring", "bow" }
|
||||||
|
|
||||||
|
local function tierFrom(source, damageType)
|
||||||
|
return source and source.resist and source.resist[damageType]
|
||||||
|
end
|
||||||
|
|
||||||
|
-- The single tier that governs `damageType` incoming against `entity`, folding
|
||||||
|
-- in its own base profile and every equipped slot that carries one.
|
||||||
|
function Resistances.tierOf(entity, damageType)
|
||||||
|
local best = tierFrom(entity, damageType) or Resistances.NORMAL
|
||||||
|
for _, slot in ipairs(EQUIP_SLOTS) do
|
||||||
|
local tier = tierFrom(entity[slot], damageType)
|
||||||
|
if tier and RANK[tier] > RANK[best] then
|
||||||
|
best = tier
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return best
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Scales a already-computed `amount` of `damageType` damage by `entity`'s
|
||||||
|
-- tier for that type. Immune drops it to nothing outright rather than merely
|
||||||
|
-- halving it, so it can floor damage below the "always at least 1" guarantee
|
||||||
|
-- combat.lua's own minimum otherwise enforces.
|
||||||
|
function Resistances.apply(entity, amount, damageType)
|
||||||
|
if amount <= 0 then return amount end
|
||||||
|
local tier = Resistances.tierOf(entity, damageType or Resistances.PHYSICAL)
|
||||||
|
if tier == Resistances.IMMUNE then return 0 end
|
||||||
|
if tier == Resistances.RESIST then return math.floor(amount / 2) end
|
||||||
|
if tier == Resistances.WEAK then return math.floor(amount * 1.5) end
|
||||||
|
return amount
|
||||||
|
end
|
||||||
|
|
||||||
|
return Resistances
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
-- Rune stones drop as dungeon loot and slot into a weapon or armor's rune
|
||||||
|
-- chain in the order they are socketed. A chain that exactly matches a
|
||||||
|
-- cataloged sequence forms a runeword: a fixed, always-on stat bonus read
|
||||||
|
-- live by combat.lua (the same additive-bonus pattern skills.lua already
|
||||||
|
-- uses for berserk/mana shield), rather than baked permanently into the
|
||||||
|
-- item's own power the way affixes.lua or crafting.lua's gems are - so a
|
||||||
|
-- runeword needs no equip-time stat sync, and simply stops applying the
|
||||||
|
-- instant a further rune breaks its exact match.
|
||||||
|
|
||||||
|
local Items = require("items")
|
||||||
|
|
||||||
|
local Runewords = {}
|
||||||
|
|
||||||
|
Runewords.MAX_RUNES = 3
|
||||||
|
|
||||||
|
Runewords.RUNES = { "sol", "ith", "tal", "ral", "eth", "amn" }
|
||||||
|
|
||||||
|
-- Keyed by the chain's runes joined in slotted order - an exact match only,
|
||||||
|
-- so a chain longer or shorter than a given word never qualifies for it.
|
||||||
|
Runewords.WORDS = {
|
||||||
|
["sol-ith"] = { name = "Zenith", attack = 3 },
|
||||||
|
["tal-ral"] = { name = "Bastion", defense = 3 },
|
||||||
|
["eth-amn-sol"] = { name = "Undertow", attack = 2, defense = 2 },
|
||||||
|
}
|
||||||
|
|
||||||
|
local function wordKey(runes)
|
||||||
|
return table.concat(runes, "-")
|
||||||
|
end
|
||||||
|
|
||||||
|
function Runewords.rune(key)
|
||||||
|
return { kind = Items.RUNE, rune = key, name = key .. " rune", glyph = "1" }
|
||||||
|
end
|
||||||
|
|
||||||
|
local function slottable(item)
|
||||||
|
return item.kind == Items.WEAPON or item.kind == Items.ARMOR
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Appends `rune`'s key to `item`'s chain. Fails once the item is full or is
|
||||||
|
-- not a weapon/armor; never fails on a rune that will not complete a known
|
||||||
|
-- word, since an unmatched chain simply confers no bonus rather than being
|
||||||
|
-- rejected outright - there is no wasted resource to protect against the way
|
||||||
|
-- crafting.lua guards a gem merge against mismatched pairs.
|
||||||
|
function Runewords.socket(item, rune)
|
||||||
|
if not slottable(item) then return false, "cannot hold runes" end
|
||||||
|
item.runes = item.runes or {}
|
||||||
|
if #item.runes >= Runewords.MAX_RUNES then return false, "the rune chain is full" end
|
||||||
|
item.runes[#item.runes + 1] = rune.rune
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
-- The cataloged word `item`'s current chain forms, or nil if it matches none
|
||||||
|
-- (including an empty chain, or one that no longer exactly matches any entry
|
||||||
|
-- once it has grown past a shorter word's own length).
|
||||||
|
function Runewords.activeWord(item)
|
||||||
|
if not item or not item.runes or #item.runes == 0 then return nil end
|
||||||
|
return Runewords.WORDS[wordKey(item.runes)]
|
||||||
|
end
|
||||||
|
|
||||||
|
function Runewords.attackBonus(entity)
|
||||||
|
local total = 0
|
||||||
|
local weaponWord = entity.weapon and Runewords.activeWord(entity.weapon)
|
||||||
|
if weaponWord then total = total + (weaponWord.attack or 0) end
|
||||||
|
local armorWord = entity.armor and Runewords.activeWord(entity.armor)
|
||||||
|
if armorWord then total = total + (armorWord.attack or 0) end
|
||||||
|
return total
|
||||||
|
end
|
||||||
|
|
||||||
|
function Runewords.defenseBonus(entity)
|
||||||
|
local total = 0
|
||||||
|
local weaponWord = entity.weapon and Runewords.activeWord(entity.weapon)
|
||||||
|
if weaponWord then total = total + (weaponWord.defense or 0) end
|
||||||
|
local armorWord = entity.armor and Runewords.activeWord(entity.armor)
|
||||||
|
if armorWord then total = total + (armorWord.defense or 0) end
|
||||||
|
return total
|
||||||
|
end
|
||||||
|
|
||||||
|
return Runewords
|
||||||
Reference in new issue
Block a user