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 + Context-Aware Rendering) | ||
-- | -- | ||
-- Standard Hero Layout | -- 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) | ||
-- 2) hero-description-bar (description strip) | |||
-- 2) hero-description-bar (description strip) | |||
-- 3) hero-modules row (4 slots: hero-module-1..4) | -- 3) hero-modules row (4 slots: hero-module-1..4) | ||
-- | -- | ||
-- | -- NEW: Plug-ins receive ctx: | ||
-- | -- ctx.region = "herobar" | "modules" | ||
-- | -- ctx.slot = slot index within that region | ||
-- | -- 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 | 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 = ... } | ||
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 | ||
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 | ||
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: | ||
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"] | ||
if type(per) == "table" then | if type(per) == "table" then | ||
if #per == 0 then | if #per == 0 then | ||
| Line 253: | Line 252: | ||
end | end | ||
local lines = {} | local lines = {} | ||
local baseText = formatUnitValue(base) | local baseText = formatUnitValue(base) | ||
| Line 306: | Line 304: | ||
end | end | ||
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 | -- Formatting helpers | ||
---------------------------------------------------------------------- | ---------------------------------------------------------------------- | ||
| Line 601: | Line 598: | ||
local detail = {} | local detail = {} | ||
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: | ||
---------------------------------------------------------------------- | ---------------------------------------------------------------------- | ||
-- | -- Slot config (edit these only to rearrange layout) | ||
---------------------------------------------------------------------- | ---------------------------------------------------------------------- | ||
local HERO_BAR_SLOT_ASSIGNMENT = { | local HERO_BAR_SLOT_ASSIGNMENT = { | ||
[1] = "IconName", | [1] = "IconName", | ||
| Line 747: | Line 742: | ||
} | } | ||
local HERO_MODULE_SLOT_ASSIGNMENT = { | local HERO_MODULE_SLOT_ASSIGNMENT = { | ||
[1] = "LevelSelector", | [1] = "LevelSelector", | ||
| Line 756: | Line 750: | ||
---------------------------------------------------------------------- | ---------------------------------------------------------------------- | ||
-- Slot scaffolds | -- 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 | -- Shared helpers used by plug-ins | ||
---------------------------------------------------------------------- | ---------------------------------------------------------------------- | ||
| Line 933: | Line 937: | ||
local series = {} | local series = {} | ||
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 | ||
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 | ||
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 | ||
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) | ||
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 | ||
local apps = rec["Status Applications"] | local apps = rec["Status Applications"] | ||
| Line 1,131: | Line 1,125: | ||
---------------------------------------------------------------------- | ---------------------------------------------------------------------- | ||
-- Plug-ins | -- Plug-ins | ||
---------------------------------------------------------------------- | ---------------------------------------------------------------------- | ||
local PLUGINS = {} | local PLUGINS = {} | ||
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 | ||
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 | ||
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 | ||
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 | ||
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 | ||
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 | ||
-- | -- 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 | ||
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 | ||
local areaVal = formatAreaSize(mech.Area) | local areaVal = formatAreaSize(mech.Area) | ||
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) | ||
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 | ||
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 | local inHeroBar = (type(ctx) == "table" and ctx.region == "herobar") | ||
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 = | 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 | addCell("Cast", castVal) | ||
addCell(" | addCell("CD", cdVal) | ||
addCell(" | addCell("Dur", durVal) | ||
local classes = { "module-quick-stats" } | |||
if inHeroBar then | |||
table.insert(classes, "sv-qs-in-herobar") | |||
end | |||
return { | return { | ||
inner = tostring( | inner = tostring(wrap), | ||
classes = | 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, | 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, | 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 | -- UI builders | ||
---------------------------------------------------------------------- | ---------------------------------------------------------------------- | ||
| Line 1,581: | Line 1,592: | ||
local level = clamp(maxLevel, 1, maxLevel) | local level = clamp(maxLevel, 1, maxLevel) | ||
local ctx = { | local ctx = { | ||
maxLevel = maxLevel, | maxLevel = maxLevel, | ||
level = level, | level = level, | ||
nonDamaging = false, | nonDamaging = false, | ||
promo = nil, | promo = nil, | ||
} | } | ||
do | do | ||
local dmgVal = nil | local dmgVal = nil | ||
| Line 1,602: | Line 1,610: | ||
end | end | ||
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 "" | ||
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)) | ||
if desc ~= "" then | if desc ~= "" then | ||
local descRow = root:tag("tr") | local descRow = root:tag("tr") | ||
| Line 1,652: | Line 1,657: | ||
end | end | ||
local modulesUI = buildHeroModulesUI(rec, ctx) | local modulesUI = buildHeroModulesUI(rec, ctx) | ||
addHeroModulesRow(root, modulesUI) | addHeroModulesRow(root, modulesUI) | ||
if showUsers then | if showUsers then | ||
local users = rec.Users or {} | local users = rec.Users or {} | ||
| Line 1,665: | Line 1,668: | ||
end | end | ||
local req = rec.Requirements or {} | local req = rec.Requirements or {} | ||
local hasReq = | local hasReq = | ||
| Line 1,691: | Line 1,693: | ||
end | end | ||
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 | ||
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 | ||
local modsText = formatModifiers(rec.Modifiers) | local modsText = formatModifiers(rec.Modifiers) | ||
if modsText then | if modsText then | ||
| Line 1,736: | Line 1,734: | ||
end | end | ||
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 | ||
local eventsText = formatEvents(rec.Events) | local eventsText = formatEvents(rec.Events) | ||
if eventsText then | if eventsText then | ||
| Line 1,751: | Line 1,747: | ||
end | end | ||
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") | ||