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) | ||
-- | -- | ||
-- Layout: | -- Layout: | ||
-- 1) hero-title-bar (TOP BAR, 2 slots: herobar 1..2) | -- 1) hero-title-bar (TOP BAR, 2 slots: herobar 1..2) | ||
| Line 10: | Line 9: | ||
-- | -- | ||
-- Requires Common.js: | -- Requires Common.js: | ||
-- - | -- - updates .sv-dyn spans via data-series | ||
-- - | -- - updates .sv-level-num + data-level on .sv-skill-card | ||
-- - | -- - binds to input.sv-level-range inside each card | ||
local GameData = require("Module:GameData") | local GameData = require("Module:GameData") | ||
| Line 24: | Line 23: | ||
local skillsCache | local skillsCache | ||
-- | -- getSkills: lazy-load + cache skill dataset from GameData. | ||
local function getSkills() | local function getSkills() | ||
if not skillsCache then | if not skillsCache then | ||
| Line 36: | Line 35: | ||
---------------------------------------------------------------------- | ---------------------------------------------------------------------- | ||
-- | -- getArgs: read args from parent frame when invoked from a template. | ||
local function getArgs(frame) | local function getArgs(frame) | ||
local parent = frame:getParent() | local parent = frame:getParent() | ||
| Line 42: | Line 41: | ||
end | end | ||
-- | -- trim: normalize strings (nil if empty). | ||
local function trim(s) | local function trim(s) | ||
if type(s) ~= "string" then | if type(s) ~= "string" then | ||
| Line 51: | Line 50: | ||
end | end | ||
-- | -- toNum: convert common scalar/table forms to a Lua number. | ||
local function toNum(v) | local function toNum(v) | ||
if type(v) == "number" then | if type(v) == "number" then | ||
| Line 65: | Line 64: | ||
end | end | ||
-- | -- clamp: clamp a number into [lo, hi]. | ||
local function clamp(n, lo, hi) | local function clamp(n, lo, hi) | ||
if type(n) ~= "number" then | if type(n) ~= "number" then | ||
| Line 75: | Line 74: | ||
end | end | ||
-- | -- fmtNum: consistent number formatting (trim trailing zeros). | ||
local function fmtNum(n) | local function fmtNum(n) | ||
if type(n) ~= "number" then | if type(n) ~= "number" then | ||
| Line 91: | Line 90: | ||
end | end | ||
-- | -- listToText: join an array into a readable string. | ||
local function listToText(list, sep) | local function listToText(list, sep) | ||
if type(list) ~= "table" or #list == 0 then | if type(list) ~= "table" or #list == 0 then | ||
| Line 99: | Line 98: | ||
end | end | ||
-- | -- isNoneLike: treat common "none" spellings as empty. | ||
local function isNoneLike(v) | local function isNoneLike(v) | ||
if v == nil then return true end | if v == nil then return true end | ||
| Line 108: | Line 107: | ||
end | end | ||
-- | -- addRow: add a standard <tr><th>Label</th><td>Value</td></tr> 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 123: | Line 122: | ||
end | end | ||
-- | -- formatUnitValue: format {Value, Unit} blocks (or scalar) for display. | ||
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 151: | Line 150: | ||
---------------------------------------------------------------------- | ---------------------------------------------------------------------- | ||
-- | -- dynSpan: render a JS-updated span for a level series. | ||
local function dynSpan(series, level) | local function dynSpan(series, level) | ||
if type(series) ~= "table" or #series == 0 then | if type(series) ~= "table" or #series == 0 then | ||
| Line 167: | Line 166: | ||
end | end | ||
-- | -- isFlatList: true if all values in list are identical. | ||
local function isFlatList(list) | local function isFlatList(list) | ||
if type(list) ~= "table" or #list == 0 then | if type(list) ~= "table" or #list == 0 then | ||
| Line 181: | Line 180: | ||
end | end | ||
-- | -- isNonZeroScalar: detect if a value is present and not effectively zero. | ||
local function isNonZeroScalar(v) | local function isNonZeroScalar(v) | ||
if v == nil then return false end | if v == nil then return false end | ||
| Line 196: | Line 195: | ||
end | end | ||
-- | -- isZeroish: aggressively treat common “zero” text forms as zero. | ||
local function isZeroish(v) | local function isZeroish(v) | ||
if v == nil then return true end | if v == nil then return true end | ||
| Line 215: | Line 214: | ||
end | end | ||
-- | -- valuePairRawText: render Base/Per Level blocks into readable text (fallback). | ||
local function valuePairRawText(block) | local function valuePairRawText(block) | ||
if type(block) ~= "table" then | if type(block) ~= "table" then | ||
| Line 249: | Line 248: | ||
end | end | ||
-- | -- valuePairDynamicValueOnly: render Base/Per Level blocks using dyn spans where possible. | ||
local function valuePairDynamicValueOnly(block, maxLevel, level) | local function valuePairDynamicValueOnly(block, maxLevel, level) | ||
if type(block) ~= "table" then | if type(block) ~= "table" then | ||
| Line 285: | Line 284: | ||
---------------------------------------------------------------------- | ---------------------------------------------------------------------- | ||
-- | -- getSkillById: locate a skill by internal ID. | ||
local function getSkillById(id) | local function getSkillById(id) | ||
id = trim(id) | id = trim(id) | ||
| Line 293: | Line 292: | ||
end | end | ||
-- | -- findSkillByName: locate a skill by external/display name. | ||
local function findSkillByName(name) | local function findSkillByName(name) | ||
name = trim(name) | name = trim(name) | ||
| Line 320: | Line 319: | ||
---------------------------------------------------------------------- | ---------------------------------------------------------------------- | ||
-- | -- basisLabel: label ATK/MATK basis in legacy damage blocks. | ||
local function basisLabel(entry, isHealing) | local function basisLabel(entry, isHealing) | ||
if isHealing then | if isHealing then | ||
| Line 340: | Line 339: | ||
end | end | ||
-- | -- formatDamageEntry: legacy percent damage formatting (dynamic by level). | ||
local function formatDamageEntry(entry, maxLevel, level) | local function formatDamageEntry(entry, maxLevel, level) | ||
if type(entry) ~= "table" then | if type(entry) ~= "table" then | ||
| Line 392: | Line 391: | ||
end | end | ||
-- | -- formatDamageList: render a list of legacy damage entries into <br/> blocks. | ||
local function formatDamageList(list, maxLevel, level, includeTypePrefix) | local function formatDamageList(list, maxLevel, level, includeTypePrefix) | ||
if type(list) ~= "table" or #list == 0 then | if type(list) ~= "table" or #list == 0 then | ||
| Line 419: | Line 418: | ||
---------------------------------------------------------------------- | ---------------------------------------------------------------------- | ||
-- | -- skillMatchesUser: check if a skill is used by a specific class/monster/summon/event. | ||
local function skillMatchesUser(rec, userName) | local function skillMatchesUser(rec, userName) | ||
if type(rec) ~= "table" or not userName or userName == "" then | if type(rec) ~= "table" or not userName or userName == "" then | ||
| Line 451: | Line 450: | ||
---------------------------------------------------------------------- | ---------------------------------------------------------------------- | ||
-- | -- isDirectSkillPage: hide Users rows when viewing the skill page itself. | ||
local function isDirectSkillPage(rec) | local function isDirectSkillPage(rec) | ||
if type(rec) ~= "table" then | if type(rec) ~= "table" then | ||
| Line 478: | Line 477: | ||
local HERO_BAR_SLOT_ASSIGNMENT = { | local HERO_BAR_SLOT_ASSIGNMENT = { | ||
[1] = "IconName", | [1] = "IconName", | ||
[2] = "SkillType", | [2] = "SkillType", -- Damage/Element/Hits/Target/Cast/Combo strip | ||
} | } | ||
| Line 492: | Line 491: | ||
---------------------------------------------------------------------- | ---------------------------------------------------------------------- | ||
-- | -- heroBarBox: wrapper for hero-bar slot modules. | ||
local function heroBarBox(slot, extraClasses, innerHtml, isEmpty) | local function heroBarBox(slot, extraClasses, innerHtml, isEmpty) | ||
local box = mw.html.create("div") | local box = mw.html.create("div") | ||
| Line 523: | Line 522: | ||
end | end | ||
-- | -- moduleBox: wrapper for hero-module (2x2 grid) slot modules. | ||
local function moduleBox(slot, extraClasses, innerHtml, isEmpty) | local function moduleBox(slot, extraClasses, innerHtml, isEmpty) | ||
local box = mw.html.create("div") | local box = mw.html.create("div") | ||
| Line 554: | Line 553: | ||
---------------------------------------------------------------------- | ---------------------------------------------------------------------- | ||
-- | -- formatScalingCompactLines: build compact “Scaling” lines for SourceType. | ||
local function formatScalingCompactLines(scaling) | local function formatScalingCompactLines(scaling) | ||
if type(scaling) ~= "table" then | if type(scaling) ~= "table" then | ||
| Line 587: | Line 586: | ||
end | end | ||
-- | -- basisWordFromFlags: compute “Attack/Magic/Hybrid” from ATK/MATK booleans. | ||
local function basisWordFromFlags(atkFlag, matkFlag) | local function basisWordFromFlags(atkFlag, matkFlag) | ||
if atkFlag and matkFlag then | if atkFlag and matkFlag then | ||
| Line 599: | Line 598: | ||
end | end | ||
-- | -- legacyPercentAtLevel: compute “Base% + PerLevel%*level” for legacy entries. | ||
local function legacyPercentAtLevel(entry, level) | local function legacyPercentAtLevel(entry, level) | ||
if type(entry) ~= "table" then | if type(entry) ~= "table" then | ||
| Line 625: | Line 624: | ||
end | end | ||
-- | -- seriesFromValuePair: normalize Base/Per Level blocks into a level-indexed series. | ||
local function seriesFromValuePair(block, maxLevel) | local function seriesFromValuePair(block, maxLevel) | ||
if type(block) ~= "table" then | if type(block) ~= "table" then | ||
| Line 706: | Line 705: | ||
end | end | ||
-- | -- displayFromSeries: render a series as fixed text or dynSpan (nil if all “—”). | ||
local function displayFromSeries(series, level) | local function displayFromSeries(series, level) | ||
if type(series) ~= "table" or #series == 0 then | if type(series) ~= "table" or #series == 0 then | ||
| Line 729: | Line 728: | ||
end | end | ||
-- | -- formatAreaSize: human readable area sizing for QuickStats. | ||
local function formatAreaSize(area) | local function formatAreaSize(area) | ||
if type(area) ~= "table" then | if type(area) ~= "table" then | ||
| Line 779: | Line 778: | ||
end | end | ||
-- | -- skillHasAnyDamage: determine if a skill has any meaningful damage (for non-damaging rules). | ||
local function skillHasAnyDamage(rec, maxLevel) | local function skillHasAnyDamage(rec, maxLevel) | ||
if type(rec.Source) == "table" then | if type(rec.Source) == "table" then | ||
| Line 803: | Line 802: | ||
end | end | ||
-- | -- computeDurationPromotion: promote status-duration into QuickStats when a skill is non-damaging. | ||
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 | ||
| Line 850: | Line 849: | ||
local PLUGINS = {} | local PLUGINS = {} | ||
-- Hero Bar Slot 1 | -- PLUGIN: IconName (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 874: | Line 873: | ||
end | end | ||
-- Hero Bar Slot 2: | -- PLUGIN: ReservedInfo (Hero Bar Slot 2 placeholder) - kept for future. | ||
function PLUGINS. | function PLUGINS.ReservedInfo(rec, ctx) | ||
local wrap = mw.html.create("div") | |||
wrap:addClass("sv-herobar-2-wrap") | |||
return { | |||
inner = tostring(wrap), | |||
classes = "module-herobar-2", | |||
} | |||
end | |||
-- PLUGIN: LevelSelector (Hero Module Slot 4) - JS level slider. | |||
function PLUGINS.LevelSelector(rec, ctx) | |||
local level = ctx.level or 1 | local level = ctx.level or 1 | ||
local maxLevel = ctx.maxLevel or 1 | local maxLevel = ctx.maxLevel or 1 | ||
local | local inner = mw.html.create("div") | ||
inner:addClass("sv-level-ui") | |||
inner:tag("div") | |||
:addClass("sv-level-label") | |||
:wikitext("Level <span class=\"sv-level-num\">" .. tostring(level) .. "</span> / " .. tostring(maxLevel)) | |||
local slider = inner:tag("div"):addClass("sv-level-slider") | |||
-- | if tonumber(maxLevel) and tonumber(maxLevel) > 1 then | ||
local function valName(x) | slider:tag("input") | ||
if x == nil then return nil end | :attr("type", "range") | ||
if type(x) == "table" then | :attr("min", "1") | ||
if x.Name and x.Name ~= "" then return tostring(x.Name) end | :attr("max", tostring(maxLevel)) | ||
if x.ID and x.ID ~= "" then return tostring(x.ID) end | :attr("value", tostring(level)) | ||
:addClass("sv-level-range") | |||
:attr("aria-label", "Skill level select") | |||
else | |||
inner:addClass("sv-level-ui-single") | |||
slider:addClass("sv-level-slider-single") | |||
end | |||
return { | |||
inner = tostring(inner), | |||
classes = "module-level-selector", | |||
} | |||
end | |||
-- PLUGIN: SkillType (Hero Bar Slot 2) - 2 rows x 3 cells (desktop + mobile). | |||
-- Rules: | |||
-- - If skill is non-damaging, hide Damage/Element/Hits. | |||
-- - If Hits is empty, hide Hits. | |||
-- - If Combo is empty, hide Combo. | |||
-- Ordering: | |||
-- - Desktop: Damage, Element, Hits, Target, Cast, Combo | |||
-- - Mobile: Damage, Element, Target, Cast, Hits, Combo (CSS reorder) | |||
function PLUGINS.SkillType(rec, ctx) | |||
local typeBlock = (type(rec.Type) == "table") and rec.Type or {} | |||
local mech = (type(rec.Mechanics) == "table") and rec.Mechanics or {} | |||
local level = ctx.level or 1 | |||
local maxLevel = ctx.maxLevel or 1 | |||
local hideDamageBundle = (ctx.nonDamaging == true) | |||
-- valName: extract a display string from typical {Name/ID/Value} objects. | |||
local function valName(x) | |||
if x == nil then return nil end | |||
if type(x) == "table" then | |||
if x.Name and x.Name ~= "" then return tostring(x.Name) end | |||
if x.ID and x.ID ~= "" then return tostring(x.ID) end | |||
if x.Value ~= nil then return tostring(x.Value) end | if x.Value ~= nil then return tostring(x.Value) end | ||
end | end | ||
| Line 899: | Line 947: | ||
end | end | ||
-- | -- hitsDisplay: find + render Hits from multiple possible structured locations. | ||
local function | local function hitsDisplay() | ||
local h = | |||
typeBlock.Hits or typeBlock["Hits"] or typeBlock["Hit Count"] or typeBlock["Hits Count"] or | |||
mech.Hits or mech["Hits"] or mech["Hit Count"] or mech["Hits Count"] or | |||
rec.Hits or rec["Hits"] | |||
-- | if h == nil or isNoneLike(h) then | ||
if type( | return nil | ||
local | end | ||
-- ValuePair-style table (Base/Per Level) => dynamic series | |||
if type(h) == "table" then | |||
if h.Base ~= nil or h["Per Level"] ~= nil or type(h["Per Level"]) == "table" then | |||
return displayFromSeries(seriesFromValuePair(h, maxLevel), level) | |||
end | |||
-- Unit block {Value, Unit} | |||
if h.Value ~= nil then | |||
local t = formatUnitValue(h) | |||
return t and mw.text.nowiki(t) or nil | |||
end | |||
-- Fallback name extraction | |||
local vn = valName(h) | |||
if vn and not isNoneLike(vn) then | |||
return mw.text.nowiki(vn) | |||
end | |||
end | end | ||
if type( | -- Scalar number/string | ||
return nil | if type(h) == "number" then | ||
return mw.text.nowiki(fmtNum(h)) | |||
end | |||
if type(h) == "string" then | |||
local t = trim(h) | |||
return (t and not isNoneLike(t)) and mw.text.nowiki(t) or nil | |||
end | end | ||
local typ = trim( | return nil | ||
end | |||
-- comboDisplay: render Combo as a compact text block (Type (+ details)). | |||
local function comboDisplay() | |||
local c = (type(mech.Combo) == "table") and mech.Combo or nil | |||
if not c then return nil end | |||
local typ = trim(c.Type) | |||
if not typ or isNoneLike(typ) then | if not typ or isNoneLike(typ) then | ||
return nil | return nil | ||
| Line 920: | Line 1,001: | ||
local details = {} | local details = {} | ||
local pct = formatUnitValue( | local pct = formatUnitValue(c.Percent) | ||
if pct and not isZeroish(pct) then | if pct and not isZeroish(pct) then | ||
table.insert(details, mw.text.nowiki(pct)) | table.insert(details, mw.text.nowiki(pct)) | ||
end | end | ||
local dur = formatUnitValue( | local dur = formatUnitValue(c.Duration) | ||
if dur and not isZeroish(dur) then | if dur and not isZeroish(dur) then | ||
table.insert(details, mw.text.nowiki(dur)) | table.insert(details, mw.text.nowiki(dur)) | ||
| Line 935: | Line 1,016: | ||
return mw.text.nowiki(typ) | return mw.text.nowiki(typ) | ||
end | end | ||
local grid = mw.html.create("div") | local grid = mw.html.create("div") | ||
| Line 969: | Line 1,023: | ||
local added = false | local added = false | ||
-- | -- addChunk: add one labeled value cell (key drives CSS ordering). | ||
local function addChunk(label, | local function addChunk(key, label, valueHtml) | ||
if | if valueHtml == nil or valueHtml == "" then return end | ||
added = true | added = true | ||
local chunk = grid:tag("div"):addClass("sv-type-chunk") | local chunk = grid:tag("div") | ||
:addClass("sv-type-chunk") | |||
:addClass("sv-type-" .. tostring(key)) | |||
:attr("data-type-key", tostring(key)) | |||
chunk:tag("div"):addClass("sv-type-label"):wikitext(mw.text.nowiki(label)) | chunk:tag("div") | ||
:addClass("sv-type-label") | |||
:wikitext(mw.text.nowiki(label)) | |||
chunk:tag("div") | |||
:addClass("sv-type-value") | |||
:wikitext(valueHtml) | |||
end | end | ||
-- | -- Damage + Element + Hits bundle (hidden when non-damaging) | ||
if not hideDamageBundle then | |||
if not | local dmg = valName(typeBlock.Damage or typeBlock["Damage Type"]) | ||
local ele = valName(typeBlock.Element or typeBlock["Element Type"]) | |||
addChunk("Element", valName(typeBlock. | local hits = hitsDisplay() | ||
addChunk(" | |||
if dmg and not isNoneLike(dmg) then | |||
addChunk("damage", "Damage", mw.text.nowiki(dmg)) | |||
end | |||
if ele and not isNoneLike(ele) then | |||
addChunk("element", "Element", mw.text.nowiki(ele)) | |||
end | |||
-- Hits: only render when present and meaningful | |||
if hits then | |||
addChunk("hits", "Hits", hits) | |||
end | |||
end | |||
-- Target + Cast | |||
local tgt = valName(typeBlock.Target or typeBlock["Target Type"]) | |||
local cst = valName(typeBlock.Cast or typeBlock["Cast Type"]) | |||
if tgt and not isNoneLike(tgt) then | |||
addChunk("target", "Target", mw.text.nowiki(tgt)) | |||
end | |||
if cst and not isNoneLike(cst) then | |||
addChunk("cast", "Cast", mw.text.nowiki(cst)) | |||
end | end | ||
-- Combo (moved here) | |||
local combo = comboDisplay() | |||
addChunk(" | if combo then | ||
addChunk("combo", "Combo", combo) | |||
end | |||
return { | return { | ||
| Line 1,005: | Line 1,083: | ||
end | end | ||
-- | -- PLUGIN: SourceType (Hero Module Slot 1) - Modifier + Source + Scaling. | ||
function PLUGINS. | function PLUGINS.SourceType(rec, ctx) | ||
local level = ctx.level or 1 | local level = ctx.level or 1 | ||
local maxLevel = ctx.maxLevel or 1 | local maxLevel = ctx.maxLevel or 1 | ||
local | local basisWord = nil | ||
local sourceKind = nil | |||
local sourceVal = nil | |||
local scaling = nil | |||
-- sourceValueForLevel: dynamic formatting for structured Source blocks. | |||
local function sourceValueForLevel(src) | |||
if type(src) ~= "table" then | |||
return nil | |||
end | |||
local per = src["Per Level"] | |||
if type(per) == "table" and #per > 0 then | |||
if isFlatList(per) then | |||
local one = formatUnitValue(per[1]) or tostring(per[1]) | |||
local show = formatUnitValue(src.Base) or one | |||
return show and mw.text.nowiki(show) or nil | |||
end | |||
local series = {} | |||
for _, v in ipairs(per) do | |||
table.insert(series, formatUnitValue(v) or tostring(v)) | |||
end | |||
return dynSpan(series, level) | |||
end | |||
return valuePairDynamicValueOnly(src, maxLevel, level) | |||
end | end | ||
if type(rec.Source) == "table" then | |||
local src = rec.Source | |||
local atkFlag = (src["ATK-Based"] == true) | |||
local matkFlag = (src["MATK-Based"] == true) | |||
basisWord = basisWordFromFlags(atkFlag, matkFlag) | |||
sourceKind = src.Type or ((src.Healing == true) and "Healing") or "Damage" | |||
sourceVal = sourceValueForLevel(src) | |||
scaling = src.Scaling | |||
end | |||
-- Fallback to legacy Damage lists if Source absent | |||
if (sourceVal == nil or sourceVal == "") and type(rec.Damage) == "table" then | |||
local dmg = rec.Damage | |||
scaling = scaling or dmg.Scaling | |||
local main = dmg["Main Damage"] | |||
local refl = dmg["Reflect Damage"] | |||
local flat = dmg["Flat Damage"] | |||
if type(main) == "table" and #main > 0 then | |||
if type( | local pick = nil | ||
for _, d in ipairs(main) do | |||
if type(d) == "table" and d.Type ~= "Healing" then | |||
pick = d | |||
break | |||
end | |||
end | end | ||
pick = pick or main[1] | |||
local | if type(pick) == "table" then | ||
local atkFlag = (pick["ATK-Based"] == true) | |||
local matkFlag = (pick["MATK-Based"] == true) | |||
basisWord = basisWord or basisWordFromFlags(atkFlag, matkFlag) | |||
sourceKind = (pick.Type == "Healing") and "Healing" or "Damage" | |||
sourceVal = legacyPercentAtLevel(pick, level) | |||
end | end | ||
elseif type(refl) == "table" and #refl > 0 and type(refl[1]) == "table" then | |||
local pick = refl[1] | |||
local atkFlag = (pick["ATK-Based"] == true) | |||
local matkFlag = (pick["MATK-Based"] == true) | |||
basisWord = basisWord or basisWordFromFlags(atkFlag, matkFlag) | |||
sourceKind = "Reflect" | |||
sourceVal = legacyPercentAtLevel(pick, level) | |||
elseif type(flat) == "table" and #flat > 0 and type(flat[1]) == "table" then | |||
local pick = flat[1] | |||
local atkFlag = (pick["ATK-Based"] == true) | |||
local matkFlag = (pick["MATK-Based"] == true) | |||
basisWord = basisWord or basisWordFromFlags(atkFlag, matkFlag) | |||
sourceKind = "Flat" | |||
sourceVal = legacyPercentAtLevel(pick, level) | |||
end | end | ||
end | |||
return | local scalingLines = formatScalingCompactLines(scaling) | ||
local hasSource = (sourceVal ~= nil and tostring(sourceVal) ~= "") | |||
local hasScaling = (type(scalingLines) == "table" and #scalingLines > 0) | |||
if (not hasSource) and (not hasScaling) then | |||
return nil | |||
end | end | ||
local hasMod = (basisWord ~= nil and tostring(basisWord) ~= "") | |||
local extra = { "skill-source-module" } | |||
table.insert(extra, hasMod and "sv-has-mod" or "sv-no-mod") | |||
if hasSource and (not hasScaling) then | |||
table.insert(extra, "sv-only-source") | |||
scaling | elseif hasScaling and (not hasSource) then | ||
table.insert(extra, "sv-only-scaling") | |||
end | end | ||
local wrap = mw.html.create("div") | |||
wrap:addClass("sv-source-grid") | |||
wrap:addClass("sv-compact-root") | |||
local | if hasMod then | ||
local modCol = wrap:tag("div"):addClass("sv-source-col"):addClass("sv-source-modifier") | |||
modCol:tag("div"):addClass("sv-source-pill"):wikitext("Modifier") | |||
modCol:tag("div"):addClass("sv-modifier-value"):wikitext(mw.text.nowiki(basisWord)) | |||
end | |||
if hasSource then | |||
local sourceCol = wrap:tag("div"):addClass("sv-source-col"):addClass("sv-source-main") | |||
sourceCol:tag("div"):addClass("sv-source-pill"):wikitext(mw.text.nowiki(sourceKind or "Source")) | |||
sourceCol:tag("div"):addClass("sv-source-value"):wikitext(sourceVal) | |||
end | |||
if hasScaling then | |||
local scalingCol = wrap:tag("div"):addClass("sv-source-col"):addClass("sv-source-scaling") | |||
scalingCol:tag("div"):addClass("sv-source-pill"):wikitext("Scaling") | |||
local list = scalingCol:tag("div"):addClass("sv-scaling-list") | |||
for _, line in ipairs(scalingLines) do | |||
list:tag("div"):addClass("sv-scaling-item"):wikitext(mw.text.nowiki(line)) | |||
end | end | ||
end | end | ||
return { | |||
inner = tostring(wrap), | |||
classes = extra, | |||
} | |||
end | |||
-- PLUGIN: QuickStats (Hero Module Slot 2) - 3x2 grid (range/area/cost/cast/cd/duration). | |||
function PLUGINS.QuickStats(rec, ctx) | |||
local level = ctx.level or 1 | |||
local maxLevel = ctx.maxLevel or 1 | |||
local promo = ctx.promo | |||
local | local mech = (type(rec) == "table" and type(rec.Mechanics) == "table") and rec.Mechanics or {} | ||
local bt = (type(mech["Basic Timings"]) == "table") and mech["Basic Timings"] or {} | |||
local rc = (type(mech["Resource Cost"]) == "table") and mech["Resource Cost"] or {} | |||
local | local function dash() return "—" end | ||
if | -- Range (0 => —) | ||
local rangeVal = nil | |||
if mech.Range ~= nil and not isNoneLike(mech.Range) then | |||
local n = toNum(mech.Range) | |||
if n ~= nil then | |||
if n ~= 0 then | |||
rangeVal = mw.text.nowiki(formatUnitValue(mech.Range) or tostring(mech.Range)) | |||
end | |||
else | |||
local t = mw.text.trim(tostring(mech.Range)) | |||
if t ~= "" and not isNoneLike(t) then | |||
rangeVal = mw.text.nowiki(t) | |||
end | |||
end | |||
end | |||
-- Area | |||
local areaVal = formatAreaSize(mech.Area) | |||
-- Timings | |||
local castVal = displayFromSeries(seriesFromValuePair(bt["Cast Time"], maxLevel), level) | |||
local cdVal = displayFromSeries(seriesFromValuePair(bt["Cooldown"], maxLevel), level) | |||
local durVal = displayFromSeries(seriesFromValuePair(bt["Duration"], maxLevel), level) | |||
if | -- Promote status duration if needed | ||
if (durVal == nil) and type(promo) == "table" and type(promo.durationBlock) == "table" then | |||
durVal = displayFromSeries(seriesFromValuePair(promo.durationBlock, maxLevel), level) | |||
end | end | ||
-- Cost: MP + HP | |||
local function labeledSeries(block, label) | |||
local s = seriesFromValuePair(block, maxLevel) | |||
if not s then return nil end | |||
local | local any = false | ||
for | for i, v in ipairs(s) do | ||
if v ~= "—" then | |||
s[i] = tostring(v) .. " " .. label | |||
any = true | |||
else | |||
s[i] = "—" | |||
end | |||
end | end | ||
return any and s or nil | |||
end | end | ||
local mpS = labeledSeries(rc["Mana Cost"], "MP") | |||
local hpS = labeledSeries(rc["Health Cost"], "HP") | |||
local costSeries = {} | |||
for lv = 1, maxLevel do | |||
local mp = mpS and mpS[lv] or "—" | |||
local hp = hpS and hpS[lv] or "—" | |||
if mp ~= "—" and hp ~= "—" then | |||
costSeries[lv] = mp .. " + " .. hp | |||
elseif mp ~= "—" then | |||
costSeries[lv] = mp | |||
elseif hp ~= "—" then | |||
costSeries[lv] = hp | |||
else | |||
costSeries[lv] = "—" | |||
end | |||
end | |||
local | local costVal = displayFromSeries(costSeries, level) | ||
local | local grid = mw.html.create("div") | ||
grid:addClass("sv-m4-grid") | |||
grid:addClass("sv-compact-root") | |||
local function addCell(label, val) | |||
local | local cell = grid:tag("div"):addClass("sv-m4-cell") | ||
cell:tag("div"):addClass("sv-m4-label"):wikitext(mw.text.nowiki(label)) | |||
local | cell:tag("div"):addClass("sv-m4-value"):wikitext(val or dash()) | ||
end | end | ||
addCell("Range", rangeVal) | |||
addCell("Area", areaVal) | |||
addCell("Cost", costVal) | |||
addCell("Cast Time", castVal) | |||
addCell("Cooldown", cdVal) | |||
addCell("Duration", durVal) | |||
return { | |||
inner = tostring(grid), | |||
classes = "module-quick-stats", | |||
} | |||
end | |||
-- PLUGIN: SpecialMechanics (Hero Module Slot 3) | |||
-- Shows: | |||
-- - Flags (deduped) | |||
-- - Special mechanics (mech.Effects) | |||
-- NOTE: Combo has been moved to SkillType (Hero Bar Slot 2). | |||
function PLUGINS.SpecialMechanics(rec, ctx) | |||
local level = ctx.level or 1 | |||
local maxLevel = ctx.maxLevel or 1 | |||
local mech = (type(rec) == "table" and type(rec.Mechanics) == "table") and rec.Mechanics or {} | |||
local effects = (type(mech.Effects) == "table") and mech.Effects or nil | |||
local mods = (type(rec.Modifiers) == "table") and rec.Modifiers or nil | |||
------------------------------------------------------------------ | |||
local | -- Flags (flat, de-duped) | ||
------------------------------------------------------------------ | |||
local flagSet = {} | |||
-- Filter out mechanics you said to remove/ignore here. | |||
local denyFlags = { | |||
["self centered"] = true, | |||
["self-centred"] = true, | |||
local | |||
[" | |||
["self centered"] = true, | ["self centered"] = true, | ||
["bond"] = true, | ["bond"] = true, | ||
["combo"] = true, | ["combo"] = true, | ||
| Line 1,302: | Line 1,354: | ||
} | } | ||
local function allowFlag(name) | |||
local function | |||
if not name then return false end | if not name then return false end | ||
local | local k = mw.ustring.lower(mw.text.trim(tostring(name))) | ||
if k == "" then return false end | |||
if denyFlags[k] then return false end | |||
return true | |||
end | end | ||
local function addFlags(sub) | local function addFlags(sub) | ||
if type(sub) ~= "table" then return end | if type(sub) ~= "table" then return end | ||
for k, v in pairs(sub) do | for k, v in pairs(sub) do | ||
if v and | if v and allowFlag(k) then | ||
flagSet[tostring(k)] = true | flagSet[tostring(k)] = true | ||
end | end | ||
| Line 1,329: | Line 1,376: | ||
addFlags(mods["Special Modifiers"]) | addFlags(mods["Special Modifiers"]) | ||
for k, v in pairs(mods) do | for k, v in pairs(mods) do | ||
if type(v) == "boolean" and v and | if type(v) == "boolean" and v and allowFlag(k) then | ||
flagSet[tostring(k)] = true | flagSet[tostring(k)] = true | ||
end | end | ||
| Line 1,340: | Line 1,387: | ||
------------------------------------------------------------------ | ------------------------------------------------------------------ | ||
-- Special | -- Special mechanics (name => value) | ||
------------------------------------------------------------------ | ------------------------------------------------------------------ | ||
local mechItems = {} | local mechItems = {} | ||
| Line 1,346: | Line 1,393: | ||
if effects then | if effects then | ||
local keys = {} | local keys = {} | ||
for k, _ in pairs(effects) do | for k, _ in pairs(effects) do table.insert(keys, k) end | ||
table.sort(keys) | table.sort(keys) | ||
| Line 1,396: | Line 1,439: | ||
if hasMech then count = count + 1 end | if hasMech then count = count + 1 end | ||
local root = mw.html.create("div") | local root = mw.html.create("div") | ||
root:addClass("sv-sm-root") | root:addClass("sv-sm-root") | ||
| Line 1,406: | Line 1,446: | ||
layout:addClass("sv-sm-count-" .. tostring(count)) | layout:addClass("sv-sm-count-" .. tostring(count)) | ||
-- Column: Flags | -- Column 1: Flags | ||
if hasFlags then | if hasFlags then | ||
local fcol = layout:tag("div"):addClass("sv-sm-col"):addClass("sv-sm-col-flags") | local fcol = layout:tag("div"):addClass("sv-sm-col"):addClass("sv-sm-col-flags") | ||
| Line 1,414: | Line 1,454: | ||
end | end | ||
-- Column: Special | -- Column 2: Special Mechanics (stacked) | ||
if hasMech then | if hasMech then | ||
local mcol = layout:tag("div"):addClass("sv-sm-col"):addClass("sv-sm-col-mech") | local mcol = layout:tag("div"):addClass("sv-sm-col"):addClass("sv-sm-col-mech") | ||
| Line 1,434: | Line 1,474: | ||
---------------------------------------------------------------------- | ---------------------------------------------------------------------- | ||
-- | -- normalizeResult: normalize plugin return values into {inner, classes}. | ||
local function normalizeResult(res) | local function normalizeResult(res) | ||
if res == nil then return nil end | if res == nil then return nil end | ||
| Line 1,450: | Line 1,490: | ||
end | end | ||
-- | -- safeCallPlugin: pcall wrapper to prevent infobox failure on plugin errors. | ||
local function safeCallPlugin(name, rec, ctx) | local function safeCallPlugin(name, rec, ctx) | ||
local fn = PLUGINS[name] | local fn = PLUGINS[name] | ||
| Line 1,463: | Line 1,503: | ||
end | end | ||
-- | -- renderHeroBarSlot: render a hero-bar slot by plugin assignment. | ||
local function renderHeroBarSlot(slotIndex, rec, ctx) | local function renderHeroBarSlot(slotIndex, rec, ctx) | ||
local pluginName = HERO_BAR_SLOT_ASSIGNMENT[slotIndex] | local pluginName = HERO_BAR_SLOT_ASSIGNMENT[slotIndex] | ||
| Line 1,478: | Line 1,518: | ||
end | end | ||
-- | -- renderModuleSlot: render a hero-module slot by plugin assignment. | ||
local function renderModuleSlot(slotIndex, rec, ctx) | local function renderModuleSlot(slotIndex, rec, ctx) | ||
local pluginName = HERO_MODULE_SLOT_ASSIGNMENT[slotIndex] | local pluginName = HERO_MODULE_SLOT_ASSIGNMENT[slotIndex] | ||
| Line 1,497: | Line 1,537: | ||
---------------------------------------------------------------------- | ---------------------------------------------------------------------- | ||
-- | -- buildHeroBarUI: build the top hero bar (2 slots). | ||
local function buildHeroBarUI(rec, ctx) | local function buildHeroBarUI(rec, ctx) | ||
local bar = mw.html.create("div") | local bar = mw.html.create("div") | ||
| Line 1,506: | Line 1,546: | ||
end | end | ||
-- | -- buildHeroModulesUI: build the 2x2 module grid row (4 slots). | ||
local function buildHeroModulesUI(rec, ctx) | local function buildHeroModulesUI(rec, ctx) | ||
local grid = mw.html.create("div") | local grid = mw.html.create("div") | ||
| Line 1,516: | Line 1,556: | ||
end | end | ||
-- | -- addHeroModulesRow: add the hero-modules row into the infobox table. | ||
local function addHeroModulesRow(tbl, modulesUI) | local function addHeroModulesRow(tbl, modulesUI) | ||
if not modulesUI or modulesUI == "" then | if not modulesUI or modulesUI == "" then | ||
| Line 1,535: | Line 1,575: | ||
---------------------------------------------------------------------- | ---------------------------------------------------------------------- | ||
-- | -- buildInfobox: render a single skill infobox. | ||
local function buildInfobox(rec, opts) | local function buildInfobox(rec, opts) | ||
opts = opts or {} | opts = opts or {} | ||
| Line 1,551: | Line 1,591: | ||
} | } | ||
-- | -- Non-damaging hides Damage/Element/Hits in SkillType | ||
do | do | ||
local dmgVal = nil | local dmgVal = nil | ||
| Line 1,563: | Line 1,603: | ||
end | end | ||
ctx.promo = computeDurationPromotion(rec, maxLevel) | ctx.promo = computeDurationPromotion(rec, maxLevel) | ||
| Line 1,616: | Line 1,655: | ||
addHeroModulesRow(root, buildHeroModulesUI(rec, ctx)) | addHeroModulesRow(root, buildHeroModulesUI(rec, ctx)) | ||
-- Users ( | -- Users (hide on direct skill page) | ||
if showUsers then | if showUsers then | ||
local users = rec.Users or {} | local users = rec.Users or {} | ||
| Line 1,651: | Line 1,690: | ||
end | end | ||
-- Mechanics (keep | -- Mechanics (keep small extras only) | ||
local mech = rec.Mechanics or {} | local mech = rec.Mechanics or {} | ||
if next(mech) ~= nil then | if next(mech) ~= nil then | ||
| Line 1,684: | Line 1,723: | ||
-- Status rows | -- Status rows | ||
local function formatStatusApplications(list, suppressDurationIndex) | local function formatStatusApplications(list, suppressDurationIndex) | ||
if type(list) ~= "table" or #list == 0 then return nil end | if type(list) ~= "table" or #list == 0 then return nil end | ||
| Line 1,717: | Line 1,755: | ||
end | end | ||
local function formatStatusRemoval(list) | local function formatStatusRemoval(list) | ||
if type(list) ~= "table" or #list == 0 then return nil end | if type(list) ~= "table" or #list == 0 then return nil end | ||
| Line 1,758: | Line 1,795: | ||
-- Events | -- Events | ||
local function formatEvents(list) | local function formatEvents(list) | ||
if type(list) ~= "table" or #list == 0 then return nil end | if type(list) ~= "table" or #list == 0 then return nil end | ||