Toggle menu
Toggle preferences menu
Toggle personal menu
Not logged in
Your IP address will be publicly visible if you make any edits.

Join the Playtest on Steam Now: SpiritVale

Module:GameSkills: Difference between revisions

From SpiritVale Wiki
No edit summary
No edit summary
Line 1: Line 1:
-- Module:GameSkills
-- Module:GameSkills
--
--
-- Phase 6.5+ (Plug-in Slot Architecture)
-- Phase 6.5+ (Plug-in Slot Architecture + Context-Aware Rendering)
--
--
-- Standard Hero Layout (reused across the wiki):
-- Standard Hero Layout:
--  1) hero-title-bar        (TOP BAR, 2 slots: herobar 1..2)
--  1) hero-title-bar        (TOP BAR, 2 slots: herobar 1..2)
--        - Herobar 1: Icon + Skill Name (left)
--  2) hero-description-bar  (description strip)
--        - Herobar 2: Reserved for compact info (right)
--  2) hero-description-bar  (description strip) [special-cased for now]
--  3) hero-modules row      (4 slots: hero-module-1..4)
--  3) hero-modules row      (4 slots: hero-module-1..4)
--        - Slot 1: Level Selector
--        - Slot 2: Skill Type
--        - Slot 3: SourceType (Modifier + Source + Scaling) OR blank
--        - Slot 4: Quick Stats (Range/Area/Cost/Cast/Cooldown/Duration)
--
--
-- Requires Common.js logic that updates:
-- NEW: Plug-ins receive ctx:
--  - .sv-dyn spans via data-series
--  ctx.region = "herobar" | "modules"
--  - .sv-level-num + data-level on .sv-skill-card
--  ctx.slot  = slot index within that region
--  - binds to input.sv-level-range inside each card
--  ctx.level / ctx.maxLevel / ctx.nonDamaging / ctx.promo


local GameData = require("Module:GameData")
local GameData = require("Module:GameData")
Line 105: Line 99:
end
end


-- Add a labeled infobox row (with optional hooks for future)
local function ctxClone(base, extra)
local out = {}
if type(base) == "table" then
for k, v in pairs(base) do out[k] = v end
end
if type(extra) == "table" then
for k, v in pairs(extra) do out[k] = v end
end
return out
end
 
-- Add a labeled infobox row
local function addRow(tbl, label, value, rowClass, dataKey)
local function addRow(tbl, label, value, rowClass, dataKey)
if value == nil or value == "" then
if value == nil or value == "" then
Line 125: Line 130:


-- Handles either a scalar OR { Value = ..., Unit = ... }
-- Handles either a scalar OR { Value = ..., Unit = ... }
-- IMPORTANT: wikiprep often converts unit-wrapped pairs into strings already.
local function formatUnitValue(v)
local function formatUnitValue(v)
if type(v) == "table" and v.Value ~= nil then
if type(v) == "table" and v.Value ~= nil then
Line 209: Line 213:
local s = mw.text.trim(tostring(v))
local s = mw.text.trim(tostring(v))
if s == "" then return true end
if s == "" then return true end
-- common “0” shapes once wikiprep has stringified
if s == "0" or s == "0.0" or s == "0.00" then return true end
if s == "0" or s == "0.0" or s == "0.00" then return true end
if s == "0s" or s == "0 s" then return true end
if s == "0s" or s == "0 s" then return true end
if s == "0m" or s == "0 m" then return true end
if s == "0m" or s == "0 m" then return true end
if s == "0%" or s == "0 %" then return true end
if s == "0%" or s == "0 %" then return true end
-- numeric-with-unit like "0.0000s"
local n = tonumber((mw.ustring.gsub(s, "[^0-9%.%-]", "")))
local n = tonumber((mw.ustring.gsub(s, "[^0-9%.%-]", "")))
return (n ~= nil and n == 0)
return (n ~= nil and n == 0)
Line 220: Line 222:


-- Base/Per Level renderer:
-- Base/Per Level renderer:
-- - Per Level list -> dynamic span (or single value if flat)
-- - Per Level scalar -> "Base" + "Per Level" lines
local function valuePairDynamicLines(name, block, maxLevel, level)
local function valuePairDynamicLines(name, block, maxLevel, level)
if type(block) ~= "table" then
if type(block) ~= "table" then
Line 230: Line 230:
local per  = block["Per Level"]
local per  = block["Per Level"]


-- Per Level list (expanded by wikiprep)
if type(per) == "table" then
if type(per) == "table" then
if #per == 0 then
if #per == 0 then
Line 253: Line 252:
end
end


-- scalar Per Level
local lines = {}
local lines = {}
local baseText = formatUnitValue(base)
local baseText = formatUnitValue(base)
Line 306: Line 304:
end
end


-- Prefer “value only” (dynSpan when series list exists)
local function valuePairDynamicValueOnly(block, maxLevel, level)
local function valuePairDynamicValueOnly(block, maxLevel, level)
if type(block) ~= "table" then
if type(block) ~= "table" then
Line 376: Line 373:


----------------------------------------------------------------------
----------------------------------------------------------------------
-- Formatting helpers (legacy + mechanics/status/etc.)
-- Formatting helpers
----------------------------------------------------------------------
----------------------------------------------------------------------


Line 601: Line 598:
local detail = {}
local detail = {}


-- Duration (optionally suppressed if QuickStats promoted it)
if idx ~= suppressDurationIndex and type(s.Duration) == "table" then
if idx ~= suppressDurationIndex and type(s.Duration) == "table" then
local t = valuePairDynamicText("Duration", s.Duration, maxLevel, level, "; ")
local t = valuePairDynamicText("Duration", s.Duration, maxLevel, level, "; ")
Line 738: Line 734:


----------------------------------------------------------------------
----------------------------------------------------------------------
-- Plug-in slot config (EDIT THESE TABLES ONLY to rearrange layout)
-- Slot config (edit these only to rearrange layout)
----------------------------------------------------------------------
----------------------------------------------------------------------


-- Hero bar: 2 slots
local HERO_BAR_SLOT_ASSIGNMENT = {
local HERO_BAR_SLOT_ASSIGNMENT = {
[1] = "IconName",
[1] = "IconName",
Line 747: Line 742:
}
}


-- Modules row: 4 slots
local HERO_MODULE_SLOT_ASSIGNMENT = {
local HERO_MODULE_SLOT_ASSIGNMENT = {
[1] = "LevelSelector",
[1] = "LevelSelector",
Line 756: Line 750:


----------------------------------------------------------------------
----------------------------------------------------------------------
-- Slot scaffolds (stable containers; plug-ins provide inner content)
-- Slot scaffolds
----------------------------------------------------------------------
----------------------------------------------------------------------


local function heroBarBox(slot, extraClasses, innerHtml, isEmpty)
local function heroBarBox(slot, pluginName, extraClasses, innerHtml, isEmpty)
local box = mw.html.create("div")
local box = mw.html.create("div")
box:addClass("hero-bar-module")
box:addClass("hero-bar-module")
box:addClass("hero-bar-module-" .. tostring(slot))
box:addClass("hero-bar-module-" .. tostring(slot))
box:attr("data-hero-bar-module", tostring(slot))
box:attr("data-hero-bar-module", tostring(slot))
box:attr("data-region", "herobar")
box:attr("data-slot", tostring(slot))
if pluginName then
box:attr("data-plugin", tostring(pluginName))
end


if extraClasses then
if extraClasses then
Line 787: Line 786:
end
end


local function moduleBox(slot, extraClasses, innerHtml, isEmpty)
local function moduleBox(slot, pluginName, extraClasses, innerHtml, isEmpty)
local box = mw.html.create("div")
local box = mw.html.create("div")
box:addClass("hero-module")
box:addClass("hero-module")
box:addClass("hero-module-" .. tostring(slot))
box:addClass("hero-module-" .. tostring(slot))
box:attr("data-hero-module", tostring(slot))
box:attr("data-hero-module", tostring(slot))
box:attr("data-region", "modules")
box:attr("data-slot", tostring(slot))
if pluginName then
box:attr("data-plugin", tostring(pluginName))
end


if extraClasses then
if extraClasses then
Line 816: Line 820:


----------------------------------------------------------------------
----------------------------------------------------------------------
-- Shared helpers used by plug-ins (Source + QuickStats)
-- Shared helpers used by plug-ins
----------------------------------------------------------------------
----------------------------------------------------------------------


Line 933: Line 937:
local series = {}
local series = {}


-- Preferred: expanded series (wikiprep)
if type(per) == "table" and #per > 0 then
if type(per) == "table" and #per > 0 then
for lv = 1, maxLevel do
for lv = 1, maxLevel do
Line 946: Line 949:
end
end


-- Empty per-list -> base only
if type(per) == "table" and #per == 0 then
if type(per) == "table" and #per == 0 then
local one = fmtAny(base)
local one = fmtAny(base)
Line 958: Line 960:
end
end


-- Scalar per -> compute base + per*level (fallback)
local baseN = toNum(base) or 0
local baseN = toNum(base) or 0
local perN  = toNum(per)
local perN  = toNum(per)
Line 975: Line 976:
end
end


-- Base-only scalar
local raw = (base ~= nil) and base or per
local raw = (base ~= nil) and base or per
local one = fmtAny(raw)
local one = fmtAny(raw)
Line 1,085: Line 1,085:


local function computeDurationPromotion(rec, maxLevel)
local function computeDurationPromotion(rec, maxLevel)
-- Only:
-- - non-damaging skills
-- - AND missing/blank Duration in Basic Timings
-- - AND a Status Application has a Duration block
-- Then: promote that status duration into QuickStats and suppress it in Applies row.
if type(rec) ~= "table" then return nil end
if type(rec) ~= "table" then return nil end
if skillHasAnyDamage(rec, maxLevel) then return nil end
if skillHasAnyDamage(rec, maxLevel) then return nil end
Line 1,106: Line 1,101:
end
end
end
end
-- if durS is nil OR all "—", allow promotion


local apps = rec["Status Applications"]
local apps = rec["Status Applications"]
Line 1,131: Line 1,125:


----------------------------------------------------------------------
----------------------------------------------------------------------
-- Plug-ins (name -> { inner = "...", classes = "..."/{...} } OR nil/"")
-- Plug-ins
----------------------------------------------------------------------
----------------------------------------------------------------------


local PLUGINS = {}
local PLUGINS = {}


-- Hero Bar Slot 1: Icon + Name
function PLUGINS.IconName(rec, ctx)
function PLUGINS.IconName(rec, ctx)
local icon  = rec.Icon
local icon  = rec.Icon
Line 1,160: Line 1,153:
end
end


-- Hero Bar Slot 2: Reserved container (kept wired for future use)
function PLUGINS.ReservedInfo(rec, ctx)
function PLUGINS.ReservedInfo(rec, ctx)
local wrap = mw.html.create("div")
local wrap = mw.html.create("div")
Line 1,170: Line 1,162:
end
end


-- Module Slot 1: Level Selector
function PLUGINS.LevelSelector(rec, ctx)
function PLUGINS.LevelSelector(rec, ctx)
local level = ctx.level or 1
local level = ctx.level or 1
Line 1,203: Line 1,194:
end
end


-- Module Slot 2: Skill Type
function PLUGINS.SkillType(rec, ctx)
function PLUGINS.SkillType(rec, ctx)
local typeBlock = (type(rec.Type) == "table") and rec.Type or {}
local typeBlock = (type(rec.Type) == "table") and rec.Type or {}
Line 1,249: Line 1,239:
end
end


-- Module Slot 3: SourceType (Modifier + Source + Scaling) OR nil for blank
function PLUGINS.SourceType(rec, ctx)
function PLUGINS.SourceType(rec, ctx)
local level = ctx.level or 1
local level = ctx.level or 1
Line 1,270: Line 1,259:
end
end


-- Fallback to legacy Damage lists if Source absent
if (sourceVal == nil or sourceVal == "") and type(rec.Damage) == "table" then
if (sourceVal == nil or sourceVal == "") and type(rec.Damage) == "table" then
local dmg = rec.Damage
local dmg = rec.Damage
Line 1,366: Line 1,354:
end
end


-- Module Slot 4: Quick Stats (3x2)
-- QuickStats:
-- - In modules row: classic 3x2 grid (.sv-m4-grid)
-- - In hero bar: compact strip wrapper (.sv-qs-strip) so CSS can "flatten" on mobile
function PLUGINS.QuickStats(rec, ctx)
function PLUGINS.QuickStats(rec, ctx)
local level = ctx.level or 1
local level = ctx.level or 1
Line 1,378: Line 1,368:
local function dash() return "—" end
local function dash() return "—" end


-- Range (0 => —)
local rangeVal = nil
local rangeVal = nil
if mech.Range ~= nil and not isNoneLike(mech.Range) then
if mech.Range ~= nil and not isNoneLike(mech.Range) then
Line 1,394: Line 1,383:
end
end


-- Area
local areaVal = formatAreaSize(mech.Area)
local areaVal = formatAreaSize(mech.Area)


-- Timings
local castSeries = seriesFromValuePair(bt["Cast Time"], maxLevel)
local castSeries = seriesFromValuePair(bt["Cast Time"], maxLevel)
local cdSeries  = seriesFromValuePair(bt["Cooldown"],  maxLevel)
local cdSeries  = seriesFromValuePair(bt["Cooldown"],  maxLevel)
Line 1,406: Line 1,393:
local durVal  = displayFromSeries(durSeries, level)
local durVal  = displayFromSeries(durSeries, level)


-- Promote status duration if needed
if (durVal == nil) and type(promo) == "table" and type(promo.durationBlock) == "table" then
if (durVal == nil) and type(promo) == "table" and type(promo.durationBlock) == "table" then
durSeries = seriesFromValuePair(promo.durationBlock, maxLevel)
durSeries = seriesFromValuePair(promo.durationBlock, maxLevel)
Line 1,412: Line 1,398:
end
end


-- Cost: combine MP + HP; never show "0 HP" (seriesFromValuePair already turns 0 into "—")
local function labeledSeries(block, label)
local function labeledSeries(block, label)
local s = seriesFromValuePair(block, maxLevel)
local s = seriesFromValuePair(block, maxLevel)
Line 1,449: Line 1,434:
local costVal = displayFromSeries(costSeries, level)
local costVal = displayFromSeries(costSeries, level)


local grid = mw.html.create("div")
local inHeroBar = (type(ctx) == "table" and ctx.region == "herobar")
grid:addClass("sv-m4-grid")
 
local wrap = mw.html.create("div")
wrap:addClass(inHeroBar and "sv-qs-strip" or "sv-m4-grid")


local function addCell(label, val)
local function addCell(label, val)
local cell = grid:tag("div"):addClass("sv-m4-cell")
local cell = wrap:tag("div"):addClass("sv-m4-cell")
cell:tag("div"):addClass("sv-m4-label"):wikitext(mw.text.nowiki(label))
cell:tag("div"):addClass("sv-m4-label"):wikitext(mw.text.nowiki(label))
cell:tag("div"):addClass("sv-m4-value"):wikitext(val or dash())
cell:tag("div"):addClass("sv-m4-value"):wikitext(val or dash())
Line 1,461: Line 1,448:
addCell("Area",      areaVal)
addCell("Area",      areaVal)
addCell("Cost",      costVal)
addCell("Cost",      costVal)
addCell("Cast Time", castVal)
addCell("Cast",     castVal)
addCell("Cooldown", cdVal)
addCell("CD",       cdVal)
addCell("Duration", durVal)
addCell("Dur",       durVal)
 
local classes = { "module-quick-stats" }
if inHeroBar then
table.insert(classes, "sv-qs-in-herobar")
end


return {
return {
inner = tostring(grid),
inner = tostring(wrap),
classes = "module-quick-stats",
classes = classes,
}
}
end
-- Alias plug-in name you can assign in slots:
-- HERO_BAR_SLOT_ASSIGNMENT[2] = "Mechanics"
function PLUGINS.Mechanics(rec, ctx)
local res = PLUGINS.QuickStats(rec, ctx)
if type(res) == "table" then
local cls = res.classes
if type(cls) == "string" then
cls = { cls }
elseif type(cls) ~= "table" then
cls = {}
end
table.insert(cls, "module-mechanics")
res.classes = cls
end
return res
end
end


Line 1,505: Line 1,514:
local pluginName = HERO_BAR_SLOT_ASSIGNMENT[slotIndex]
local pluginName = HERO_BAR_SLOT_ASSIGNMENT[slotIndex]
if not pluginName then
if not pluginName then
return heroBarBox(slotIndex, nil, "", true)
return heroBarBox(slotIndex, nil, nil, "", true)
end
end


local res = safeCallPlugin(pluginName, rec, ctx)
local ctxSlot = ctxClone(ctx, { region = "herobar", slot = slotIndex })
local res = safeCallPlugin(pluginName, rec, ctxSlot)
if not res or not res.inner or res.inner == "" then
if not res or not res.inner or res.inner == "" then
return heroBarBox(slotIndex, nil, "", true)
return heroBarBox(slotIndex, pluginName, nil, "", true)
end
end


return heroBarBox(slotIndex, res.classes, res.inner, false)
return heroBarBox(slotIndex, pluginName, res.classes, res.inner, false)
end
end


Line 1,519: Line 1,529:
local pluginName = HERO_MODULE_SLOT_ASSIGNMENT[slotIndex]
local pluginName = HERO_MODULE_SLOT_ASSIGNMENT[slotIndex]
if not pluginName then
if not pluginName then
return moduleBox(slotIndex, nil, "", true)
return moduleBox(slotIndex, nil, nil, "", true)
end
end


local res = safeCallPlugin(pluginName, rec, ctx)
local ctxSlot = ctxClone(ctx, { region = "modules", slot = slotIndex })
local res = safeCallPlugin(pluginName, rec, ctxSlot)
if not res or not res.inner or res.inner == "" then
if not res or not res.inner or res.inner == "" then
return moduleBox(slotIndex, nil, "", true)
return moduleBox(slotIndex, pluginName, nil, "", true)
end
end


return moduleBox(slotIndex, res.classes, res.inner, false)
return moduleBox(slotIndex, pluginName, res.classes, res.inner, false)
end
end


----------------------------------------------------------------------
----------------------------------------------------------------------
-- UI builders (hero bar + modules grid)
-- UI builders
----------------------------------------------------------------------
----------------------------------------------------------------------


Line 1,581: Line 1,592:
local level = clamp(maxLevel, 1, maxLevel)
local level = clamp(maxLevel, 1, maxLevel)


-- ctx passed to every plug-in (the only allowed “shared state”)
local ctx = {
local ctx = {
maxLevel = maxLevel,
maxLevel = maxLevel,
level = level,
level = level,
-- computed below:
nonDamaging = false,
nonDamaging = false,
promo = nil,
promo = nil,
}
}


-- Determine “non-damaging” for SkillType (hide Damage/Element)
do
do
local dmgVal = nil
local dmgVal = nil
Line 1,602: Line 1,610:
end
end


-- Duration promotion (for QuickStats + Applies suppression)
ctx.promo = computeDurationPromotion(rec, maxLevel)
ctx.promo = computeDurationPromotion(rec, maxLevel)


Line 1,622: Line 1,629:
local desc  = rec.Description or ""
local desc  = rec.Description or ""


-- Hero Title Bar
local heroRow = root:tag("tr")
local heroRow = root:tag("tr")
heroRow:addClass("spiritvale-infobox-main")
heroRow:addClass("spiritvale-infobox-main")
Line 1,633: Line 1,639:
heroCell:wikitext(buildHeroBarUI(rec, ctx))
heroCell:wikitext(buildHeroBarUI(rec, ctx))


-- Description Bar (special-cased for now)
if desc ~= "" then
if desc ~= "" then
local descRow = root:tag("tr")
local descRow = root:tag("tr")
Line 1,652: Line 1,657:
end
end


-- Modules row
local modulesUI = buildHeroModulesUI(rec, ctx)
local modulesUI = buildHeroModulesUI(rec, ctx)
addHeroModulesRow(root, modulesUI)
addHeroModulesRow(root, modulesUI)


-- Users (hide on direct skill page)
if showUsers then
if showUsers then
local users = rec.Users or {}
local users = rec.Users or {}
Line 1,665: Line 1,668:
end
end


-- Requirements
local req = rec.Requirements or {}
local req = rec.Requirements or {}
local hasReq =
local hasReq =
Line 1,691: Line 1,693:
end
end


-- Mechanics (detailed rows still shown; Range/Area/Timings/Cost are in QuickStats)
local mech = rec.Mechanics or {}
local mech = rec.Mechanics or {}
if next(mech) ~= nil then
if next(mech) ~= nil then
Line 1,702: Line 1,703:
end
end


-- Source + Scaling are displayed in SourceType module now, so we do NOT repeat them as body rows.
-- Keep the detailed legacy damage breakdown rows only (Main/Flat/Reflect/Healing) when Source missing.
if type(rec.Source) ~= "table" then
if type(rec.Source) ~= "table" then
local dmg = rec.Damage or {}
local dmg = rec.Damage or {}
Line 1,730: Line 1,729:
end
end


-- Modifiers flags
local modsText = formatModifiers(rec.Modifiers)
local modsText = formatModifiers(rec.Modifiers)
if modsText then
if modsText then
Line 1,736: Line 1,734:
end
end


-- Status (suppress promoted duration line if needed)
local suppressIdx = (type(ctx.promo) == "table") and ctx.promo.suppressDurationIndex or nil
local suppressIdx = (type(ctx.promo) == "table") and ctx.promo.suppressDurationIndex or nil
local statusApps = formatStatusApplications(rec["Status Applications"], maxLevel, level, suppressIdx)
local statusApps = formatStatusApplications(rec["Status Applications"], maxLevel, level, suppressIdx)
Line 1,745: Line 1,742:
end
end


-- Events
local eventsText = formatEvents(rec.Events)
local eventsText = formatEvents(rec.Events)
if eventsText then
if eventsText then
Line 1,751: Line 1,747:
end
end


-- Notes
if type(rec.Notes) == "table" and #rec.Notes > 0 then
if type(rec.Notes) == "table" and #rec.Notes > 0 then
addRow(root, "Notes", table.concat(rec.Notes, "<br />"), "sv-row-meta", "Notes")
addRow(root, "Notes", table.concat(rec.Notes, "<br />"), "sv-row-meta", "Notes")