Module:GameSkills: Difference between revisions
From SpiritVale Wiki
More actions
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) | ||
-- | -- | ||
-- 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) | |||
-- | -- | ||
-- | -- Requires Common.js logic that updates: | ||
-- | -- - .sv-dyn spans via data-series | ||
-- | -- - .sv-level-num + data-level on .sv-skill-card | ||
-- | -- - 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 | local function kebabCase(s) | ||
if type(s) ~= "string" then | |||
if type( | return nil | ||
end | end | ||
-- Handle "ABc" boundaries, then "aB" boundaries | |||
s = mw.ustring.gsub(s, "([A-Z]+)([A-Z][a-z])", "%1-%2") | |||
s = mw.ustring.gsub(s, "([a-z0-9])([A-Z])", "%1-%2") | |||
return | 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: | ||
---------------------------------------------------------------------- | ---------------------------------------------------------------------- | ||
-- | -- 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: | |||
box: | -- 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", | 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: | |||
box: | -- 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", | 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 = { " | 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 | ||
-- | -- Module: Quick Stats (PORTABLE) | ||
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 | local grid = mw.html.create("div") | ||
grid:addClass("sv-m4-grid") | |||
local function addCell(label, val) | local function addCell(label, val) | ||
local 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", | addCell("Cast Time", castVal) | ||
addCell(" | addCell("Cooldown", cdVal) | ||
addCell(" | addCell("Duration", durVal) | ||
return { | return { | ||
inner = tostring( | inner = tostring(grid), | ||
classes = | classes = "module-quick-stats", | ||
} | } | ||
end | end | ||
| Line 1,517: | Line 1,550: | ||
end | end | ||
local res = safeCallPlugin(pluginName, rec, ctx) | |||
local res = safeCallPlugin(pluginName, rec, | |||
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 res = safeCallPlugin(pluginName, rec, ctx) | |||
local res = safeCallPlugin(pluginName, rec, | |||
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") | ||