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 + Context-Aware Rendering)
-- Phase 6.5+ (Plug-in Slot Architecture)
--
--
-- Standard Hero Layout:
-- Standard Hero Layout (reused across the wiki):
--  1) hero-title-bar        (TOP BAR, 2 slots: herobar 1..2)
--  1) hero-title-bar        (TOP BAR, 2 slots: herobar 1..2)
--  2) hero-description-bar  (description strip)
--        - Herobar 1: Icon + Skill Name (left)
--        - 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)
--
--
-- NEW: Plug-ins receive ctx:
-- Requires Common.js logic that updates:
--  ctx.region = "herobar" | "modules"
--  - .sv-dyn spans via data-series
--  ctx.slot  = slot index within that region
--  - .sv-level-num + data-level on .sv-skill-card
--  ctx.level / ctx.maxLevel / ctx.nonDamaging / ctx.promo
--  - binds to input.sv-level-range inside each card
--
-- Upgrade: shared slot wrapper classes
--   - .sv-hero-slot and .sv-hero-slot-body are applied to BOTH:
--      * hero-bar-module (hero bar)
--      * hero-module    (tile grid)
--  This allows "real" modules (SkillType/SourceType/QuickStats/etc.) to be moved
--  between slots without CSS refactors.


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


local function ctxClone(base, extra)
local function kebabCase(s)
local out = {}
if type(s) ~= "string" then
if type(base) == "table" then
return nil
for k, v in pairs(base) do out[k] = v end
end
end
if type(extra) == "table" then
-- Handle "ABc" boundaries, then "aB" boundaries
for k, v in pairs(extra) do out[k] = v end
s = mw.ustring.gsub(s, "([A-Z]+)([A-Z][a-z])", "%1-%2")
end
s = mw.ustring.gsub(s, "([a-z0-9])([A-Z])", "%1-%2")
return out
s = mw.ustring.lower(s)
return s
end
end


-- Add a labeled infobox row
-- Add a labeled infobox row (with optional hooks for future)
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 130: Line 143:


-- 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 213: Line 227:
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 222: Line 238:


-- 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 248:
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 252: Line 271:
end
end


-- scalar Per Level
local lines = {}
local lines = {}
local baseText = formatUnitValue(base)
local baseText = formatUnitValue(base)
Line 304: Line 324:
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 373: Line 394:


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


Line 598: Line 619:
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 734: Line 756:


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


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


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


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


Line 758: Line 782:
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))
-- Shared cross-slot hooks
box:addClass("sv-hero-slot")
box:addClass("sv-hero-slot-bar")
 
if pluginName then
if pluginName then
box:attr("data-plugin", tostring(pluginName))
local k = kebabCase(pluginName)
if k then
box:attr("data-plugin", k)
box:addClass("sv-plugin-" .. k)
end
end
end


Line 778: Line 809:
end
end


local body = box:tag("div"):addClass("hero-bar-module-body")
local body = box:tag("div"):addClass("hero-bar-module-body"):addClass("sv-hero-slot-body")
if innerHtml and innerHtml ~= "" then
if innerHtml and innerHtml ~= "" then
body:wikitext(innerHtml)
body:wikitext(innerHtml)
Line 791: Line 822:
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))
-- Shared cross-slot hooks
box:addClass("sv-hero-slot")
box:addClass("sv-hero-slot-tile")
 
if pluginName then
if pluginName then
box:attr("data-plugin", tostring(pluginName))
local k = kebabCase(pluginName)
if k then
box:attr("data-plugin", k)
box:addClass("sv-plugin-" .. k)
end
end
end


Line 811: Line 849:
end
end


local body = box:tag("div"):addClass("hero-module-body")
local body = box:tag("div"):addClass("hero-module-body"):addClass("sv-hero-slot-body")
if innerHtml and innerHtml ~= "" then
if innerHtml and innerHtml ~= "" then
body:wikitext(innerHtml)
body:wikitext(innerHtml)
Line 820: Line 858:


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


Line 937: Line 975:
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 949: Line 988:
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 960: Line 1,000:
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 976: Line 1,017:
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,127:


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,101: Line 1,148:
end
end
end
end
-- if durS is nil OR all "—", allow promotion


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


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


local PLUGINS = {}
local PLUGINS = {}


-- Hero Bar Slot 1: Icon + Name  (EXCEPTION: not required to be "portable")
function PLUGINS.IconName(rec, ctx)
function PLUGINS.IconName(rec, ctx)
local icon  = rec.Icon
local icon  = rec.Icon
Line 1,153: Line 1,202:
end
end


-- Hero Bar Slot 2: Reserved container (EXCEPTION)
function PLUGINS.ReservedInfo(rec, ctx)
function PLUGINS.ReservedInfo(rec, ctx)
local wrap = mw.html.create("div")
local wrap = mw.html.create("div")
Line 1,162: Line 1,212:
end
end


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


-- Module: Skill Type  (PORTABLE)
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,239: Line 1,291:
end
end


-- Module: SourceType (Modifier + Source + Scaling)  (PORTABLE)
function PLUGINS.SourceType(rec, ctx)
function PLUGINS.SourceType(rec, ctx)
local level = ctx.level or 1
local level = ctx.level or 1
Line 1,259: Line 1,312:
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,314: Line 1,368:
local hasMod = (basisWord ~= nil and tostring(basisWord) ~= "")
local hasMod = (basisWord ~= nil and tostring(basisWord) ~= "")


local extra = { "skill-source-module" }
local extra = { "module-source-type" }
table.insert(extra, hasMod and "sv-has-mod" or "sv-no-mod")
table.insert(extra, hasMod and "sv-has-mod" or "sv-no-mod")


Line 1,354: Line 1,408:
end
end


-- QuickStats:
-- Module: Quick Stats (PORTABLE)
-- - 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,368: Line 1,420:
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,383: Line 1,436:
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,393: Line 1,448:
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,398: Line 1,454:
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,434: Line 1,491:
local costVal = displayFromSeries(costSeries, level)
local costVal = displayFromSeries(costSeries, level)


local inHeroBar = (type(ctx) == "table" and ctx.region == "herobar")
local grid = mw.html.create("div")
 
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 = wrap:tag("div"):addClass("sv-m4-cell")
local cell = grid: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,448: Line 1,503:
addCell("Area",      areaVal)
addCell("Area",      areaVal)
addCell("Cost",      costVal)
addCell("Cost",      costVal)
addCell("Cast",     castVal)
addCell("Cast Time", castVal)
addCell("CD",       cdVal)
addCell("Cooldown", cdVal)
addCell("Dur",       durVal)
addCell("Duration", durVal)
 
local classes = { "module-quick-stats" }
if inHeroBar then
table.insert(classes, "sv-qs-in-herobar")
end


return {
return {
inner = tostring(wrap),
inner = tostring(grid),
classes = classes,
classes = "module-quick-stats",
}
}
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,517: Line 1,550:
end
end


local ctxSlot = ctxClone(ctx, { region = "herobar", slot = slotIndex })
local res = safeCallPlugin(pluginName, rec, ctx)
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, pluginName, nil, "", true)
return heroBarBox(slotIndex, pluginName, nil, "", true)
Line 1,532: Line 1,564:
end
end


local ctxSlot = ctxClone(ctx, { region = "modules", slot = slotIndex })
local res = safeCallPlugin(pluginName, rec, ctx)
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, pluginName, nil, "", true)
return moduleBox(slotIndex, pluginName, nil, "", true)
Line 1,542: Line 1,573:


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


Line 1,592: Line 1,623:
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,610: Line 1,644:
end
end


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


Line 1,629: Line 1,664:
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,639: Line 1,675:
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,657: Line 1,694:
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,668: Line 1,707:
end
end


-- Requirements
local req = rec.Requirements or {}
local req = rec.Requirements or {}
local hasReq =
local hasReq =
Line 1,693: Line 1,733:
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,703: Line 1,744:
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,729: Line 1,772:
end
end


-- Modifiers flags
local modsText = formatModifiers(rec.Modifiers)
local modsText = formatModifiers(rec.Modifiers)
if modsText then
if modsText then
Line 1,734: Line 1,778:
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,742: Line 1,787:
end
end


-- Events
local eventsText = formatEvents(rec.Events)
local eventsText = formatEvents(rec.Events)
if eventsText then
if eventsText then
Line 1,747: Line 1,793:
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")