Files
warren/src/crafting.lua
T

182 lines
7.0 KiB
Lua

-- Deterministic gear crafting, layered on top of the loot-rolled affix
-- system (affixes.lua). Killing monsters earns essence; essence forges tier-1
-- gems; two matching gems merge into one gem a tier higher; a gem sockets
-- into gear for a guaranteed on-hit rider alongside a flat power bump;
-- tempering spends essence directly for power at rising cost; disenchanting
-- strips a rolled affix back into essence. Nothing here rolls dice - a build
-- is entirely a function of the essence spent on it, distinct from the luck
-- of what affixes.lua hands out on drops.
local Items = require("items")
local Status = require("status")
local Affixes = require("affixes")
local Crafting = {}
Crafting.FIRE = "fire"
Crafting.FROST = "frost"
Crafting.SHOCK = "shock"
Crafting.VENOM = "venom"
Crafting.elements = { Crafting.FIRE, Crafting.FROST, Crafting.SHOCK, Crafting.VENOM }
-- SHOCK's duration is 2, not 1: Status.tick decrements duration before
-- skipsTurn is ever checked (see status.lua), and a hit landed during the
-- player's own turn ticks the victim's status for the first time on that
-- SAME round, in the victim's own upcoming takeMonsterTurn - a duration-1
-- stun would already read 0 and be gone before skipsTurn ever saw it,
-- silently never stunning anything. 2 survives that first tick with 1 left,
-- which is exactly enough to skip the one turn immediately after the hit.
Crafting.riders = {
[Crafting.FIRE] = { status = Status.BURNING, duration = 3, potency = 2 },
[Crafting.FROST] = { status = Status.SLOW, duration = 2, potency = 1 },
[Crafting.SHOCK] = { status = Status.STUN, duration = 2, potency = 1 },
[Crafting.VENOM] = { status = Status.POISON, duration = 4, potency = 2 },
}
Crafting.tierNames = { "chip", "flawed", "perfect" }
Crafting.MAX_TIER = #Crafting.tierNames
Crafting.FORGE_COST = 6
Crafting.ENCHANT_COST = 10
Crafting.TEMPER_MAX = 5
local function isElement(element)
return Crafting.riders[element] ~= nil
end
function Crafting.gem(element, tier)
return {
kind = Items.GEM,
glyph = "*",
element = element,
tier = tier,
power = tier,
name = Crafting.tierNames[tier] .. " " .. element .. " gem",
}
end
-- A kill's essence scales with the monster's own xp reward, so the crafting
-- economy tracks the game's existing difficulty curve instead of needing a
-- second balance table.
function Crafting.essenceFor(xp)
return math.max(1, xp // 3)
end
-- Spends essence to forge a fresh tier-1 gem of the given element.
function Crafting.forge(entity, element)
if not isElement(element) then return false, "no such element" end
if entity.essence < Crafting.FORGE_COST then return false, "not enough essence" end
entity.essence = entity.essence - Crafting.FORGE_COST
return true, Crafting.gem(element, 1)
end
-- Combines two gems of the same element and tier into one gem a tier higher.
-- Mismatched pairs are rejected untouched, so a misclick costs nothing.
function Crafting.mergeGems(gemA, gemB)
if gemA.element ~= gemB.element then return false, "elements do not match" end
if gemA.tier ~= gemB.tier then return false, "tiers do not match" end
if gemA.tier >= Crafting.MAX_TIER then return false, "already at maximum tier" end
return true, nil, Crafting.gem(gemA.element, gemA.tier + 1)
end
local function socketable(item)
return item.kind == Items.WEAPON or item.kind == Items.ARMOR
or item.kind == Items.BOW or item.kind == Items.RING
end
-- weapon/armor bake their equipped power straight into attack/defense
-- (see items.equipWeapon/equipArmor), so any crafting action that edits
-- item.power after the fact must apply the same delta to the wielder to
-- avoid a stat that only updates on the next re-equip.
local function applyPowerDelta(entity, item, delta)
if delta == 0 then return end
if entity.weapon == item then
entity.attack = entity.attack + delta
elseif entity.armor == item then
entity.defense = entity.defense + delta
end
end
-- Attaches a gem's rider and power to a piece of gear, replacing whatever was
-- socketed before - the old gem is pried loose and breaks, it is not
-- returned. Rings and bows take the rider (for ranged/aura play) but only
-- weapon and armor power feeds attack/defense; see socketPowerKind below.
function Crafting.socket(entity, item, gem)
if not socketable(item) then return false, "cannot be socketed" end
if gem.kind ~= Items.GEM then return false, "not a gem" end
local previousPower = item.socket and item.socket.power or 0
item.socket = { element = gem.element, tier = gem.tier, power = gem.power, onHit = Crafting.riders[gem.element] }
if item.kind == Items.WEAPON or item.kind == Items.ARMOR then
local delta = gem.power - previousPower
item.power = item.power + delta
applyPowerDelta(entity, item, delta)
end
return true
end
function Crafting.temperCost(item)
return ((item.temper or 0) + 1) * 4
end
-- Flat, permanent power increase capped so tempering cannot out-scale simply
-- finding better gear; each step costs more essence than the last.
function Crafting.temper(entity, item)
if item.kind ~= Items.WEAPON and item.kind ~= Items.ARMOR then
return false, "cannot be tempered"
end
local level = item.temper or 0
if level >= Crafting.TEMPER_MAX then return false, "fully tempered" end
local cost = Crafting.temperCost(item)
if entity.essence < cost then return false, "not enough essence" end
entity.essence = entity.essence - cost
item.temper = level + 1
item.power = item.power + 1
applyPowerDelta(entity, item, 1)
return true
end
local function affixPool(item)
if item.kind == Items.WEAPON then return Affixes.weapon end
if item.kind == Items.ARMOR then return Affixes.armor end
return nil
end
-- Grants a guaranteed, chosen affix to gear that has none - the deterministic
-- counterpart to affixes.rollWeapon/rollArmor rolling one by chance on drop.
function Crafting.enchant(entity, item, affixKey)
local pool = affixPool(item)
if not pool then return false, "cannot be enchanted" end
if item.affix then return false, "already bears an affix" end
local chosen
for _, entry in ipairs(pool) do
if entry.key == affixKey then chosen = entry end
end
if not chosen then return false, "unknown affix" end
if entity.essence < Crafting.ENCHANT_COST then return false, "not enough essence" end
entity.essence = entity.essence - Crafting.ENCHANT_COST
Affixes.decorate(item, chosen)
applyPowerDelta(entity, item, chosen.power)
return true
end
-- Strips a rolled affix back into essence, worth more the stronger the affix
-- was, undoing affixes.decorate's power bump and name prefix exactly.
function Crafting.disenchant(entity, item)
if not item.affix then return false, "nothing to disenchant" end
local affix = item.affix
local refund = Crafting.FORGE_COST + affix.power * 3
applyPowerDelta(entity, item, -affix.power)
item.power = item.power - affix.power
local prefix = affix.name .. " "
if item.name:sub(1, #prefix) == prefix then
item.name = item.name:sub(#prefix + 1)
end
item.affix = nil
item.damageType = nil
entity.essence = entity.essence + refund
return true, refund
end
return Crafting