Module:Sandbox/LVL: Difference between revisionsGive feedback
Jump to navigation
Jump to search
Blanked the page Tags: Blanking Manual revert |
No edit summary |
||
| Line 1: | Line 1: | ||
-- ==================================================================== | |||
-- Dependencies and Initial Setup | |||
-- ==================================================================== | |||
local lang = require "Module:Lang" | |||
local commonutils = require "Module:Utilities" | |||
local utils = require "Module:Abilities/utils" | |||
local p = {} | |||
local data = mw.loadJsonData("Data:AbilityCards.json") | |||
-- (ATTR_TYPE_ICON_MAP and helper functions remain the same) | |||
local ATTR_TYPE_ICON_MAP = { | |||
bullet_armor_up = {img='Bullet_Armor.png', link='Damage_Resistance', color = 'NoColor'}, | |||
bullet_armor_down = {img='Bullet_Armor.png', link='Damage_Resistance', color = 'NoColor'}, | |||
cast = {img='AttributeIconMaxChargesIncrease.png', link=''}, | |||
charges = {img='AttributeIconMaxChargesIncrease.png', link='', color = 'Purple'}, | |||
damage = {img='Damage_heart.png', link='', color = 'NoColor'}, | |||
bullet_damage = {img='Bullet_damage.png', link='Bullet_Damage', color = 'NoColor'}, | |||
fire_rate = {img='Fire Rate.png', link='Fire_Rate', color = 'Brown'}, | |||
healing = {img='Healing.png', link='Healing', color = 'Green'}, | |||
health = {img='Health.png', link='Health', color = 'Green'}, | |||
move_speed = {img='Move speed.png', link='Move Speed', color = 'Green'}, | |||
range = {img='CastRange.png', link='Ability_Range', color = 'Purple'}, | |||
tech_armor_up = {img='Spirit_Armor.png', link='Damage_Resistance', color = 'NoColor'}, | |||
tech_damage = {img='AttributeIconTechShieldHealth.png', link='Spirit_Damage', color = 'NoColor',size = '12px'}, | |||
distance = {img='AttributeIconTechRange.png', link='Ability_Range', color = 'Purple'}, | |||
duration = {img='AttributeIconTechDuration.png', link='Ability Duration', color = 'Purple'}, | |||
slow = {img='MoveSlow.png', link='', color = 'Purple'}, | |||
melee_damage = {img='Melee damage.png', link='Melee Damage'}, | |||
cooldown = {img='Cooldown Icon.png', link='Ability Cooldown', color = 'Purple'}, | |||
combat_barrier = {img='Barrier.png', link='Barrier', color = 'Green'}, | |||
time = {img='AttributeIconTechDuration.png', link='Ability Duration', color = 'Purple'}, | |||
} | |||
function get_hero_key(hero_name) -- Unchanged | |||
for i, hero in pairs(data) do | |||
if hero["Name"] == hero_name then return i end | |||
end | |||
return nil | |||
end | |||
function get_icon(attr_type) -- Unchanged | |||
local mappedAttr = ATTR_TYPE_ICON_MAP[attr_type] | |||
local img = 'GenericProperty.png' | |||
local link = '' | |||
local size = '' | |||
local color = 'Grey' | |||
if mappedAttr then | |||
img = mappedAttr.img | |||
link = mappedAttr.link | |||
size = mappedAttr.size | |||
color = mappedAttr.color or color | |||
end | |||
return {img=img, link=link, color=color, size=size} | |||
end | |||
function get_attr_ss(attr) -- Unchanged | |||
if not attr then return nil end | |||
local scale = attr.Scale | |||
if not scale or scale.Type ~= 'spirit' or scale.Value == 0 then return nil end | |||
return commonutils.round_to_sig_fig(scale.Value, 3) | |||
end | |||
-- ==================================================================== | |||
-- HTML Builder Functions (With Final Fixes) | |||
-- ==================================================================== | |||
local function buildMultiplierHtml(type, value, icon_size) -- Unchanged from last version | |||
local config = { | |||
spirit = { color = "#E3BDFA", bgColor = "#533669", icon = "Spirit scaling.png", link = "Spirit Power#Spirit Power Scaling", size = icon_size or "40px" }, | |||
melee = { color = "#cdb89e", bgColor = "#80550f", icon = "Melee scaling.png", link = "Melee Damage#Abilities", size = icon_size or "40px" }, | |||
boon = { color = "#bec1ac", bgColor = "#217a68", icon = "Boon scaling.png", link = "Level#Boons from Patron", size = icon_size or "25px" }, | |||
weapon_damage_increase = { color = "#cdb89e", bgColor = "#80550f", icon = "Weapon scaling.png", link = "Weapon Damage", size = icon_size or "40px" } | |||
} | |||
local c = config[type] | |||
if not c or not value then return '' end | |||
local icon = string.format('[[File:%s|link=%s|%s]]', c.icon, c.link, c.size) | |||
return string.format( | |||
'<span style="font-size: 0.8em; color: %s; white-space: nowrap;">%s<span style="background-color: %s; font-family: \'Retail Demo Regular\', \'PT Serif\', \'Palatino\'; font-weight:bold; width: auto; border-radius: 5px; padding: 2px 2px 0px 2px; margin-left: -2px;"><i>x</i>%s</span></span>', | |||
c.color, icon, c.bgColor, value | |||
) | |||
end | |||
--- FIXED: Replicates the original HeaderAttr template exactly. | |||
local function buildHeaderAttrHtml(value, uom, ss, icon, style) | |||
if not value or value == '' then return '' end | |||
local frame = mw.getCurrentFrame() | |||
-- The original template used {{Icon/Grey}}, which we can replicate with a span and filter for simplicity, | |||
-- or just output the icon directly if the filter isn't critical. Let's keep it simple. | |||
local mainBox = string.format('%s %s', | |||
icon, | |||
frame:expandTemplate{ title = "ValueAndUom", args = { | |||
"'''" .. value .. "'''", | |||
uom, | |||
uom_style = "font-size: calc(1em - 2px); color: #B2B2B2" | |||
}} | |||
) | |||
local spiritScale = '' | |||
if ss and ss ~= '' then | |||
spiritScale = string.format( | |||
'<div style="position: relative;"><div style="position: absolute; top: -24px; right: -14px">%s</div></div>', | |||
buildMultiplierHtml('spirit', ss, '28px') | |||
) | |||
end | |||
return string.format( | |||
'<div style="background-color: #2C2C2C; padding: 10px 6px 10px 6px; margin-right: 20px; white-space: nowrap; %s">%s</div>%s', | |||
style or '', mainBox, spiritScale | |||
) | |||
end | |||
--- FIXED: Removed "overflow: hidden;" to prevent clipping. | |||
local function buildMainBoxHtml(prop, hero_key) | |||
local frame = mw.getCurrentFrame() | |||
local scale_type = prop.Scale and prop.Scale.Type | |||
local scale_value = prop.Scale and commonutils.round_to_sig_fig(prop.Scale.Value, 3) | |||
local scale_styles = { | |||
melee = { grad = "radial-gradient(circle, rgba(36,37,36,1) 0%%, rgba(66,55,47,1) 100%%)", border = "#5b412b", shadow = "#3d2c4d" }, | |||
spirit = { grad = "radial-gradient(circle, rgba(42,41,43,1) 0%%, rgba(59,49,69,1) 100%%)", border = "#583D6F", shadow = "#3d2c4d" }, | |||
power_increase = { grad = "radial-gradient(circle, rgba(42,41,43,1) 0%%, rgba(61,78,70,1) 100%%)", border = "#48675a", shadow = "#3d2c4d" } | |||
} | |||
local current_style = scale_styles[scale_type] or { grad = "#2A2A2A", border = "#2A2A2A", shadow = "#2A2A2A" } | |||
local multiplierHtml = buildMultiplierHtml(scale_type, scale_value) | |||
local icon_data = get_icon(prop.Type) | |||
local value = prop.Value | |||
if scale_type == 'melee' then | |||
local hero_data = mw.loadJsonData("Data:HeroData.json") | |||
local hero = hero_data[hero_key] | |||
if hero and hero.LightMeleeDamage and prop.Scale.Value then | |||
value = prop.Value + hero.LightMeleeDamage * prop.Scale.Value | |||
end | |||
end | |||
local iconHtml = frame:expandTemplate{ title = 'Icon/' .. (icon_data.color or 'Grey'), args = { | |||
string.format('[[File:%s|%s|link=%s]]', icon_data.img, icon_data.size or '18px', icon_data.link), | |||
frame:expandTemplate{ title = 'ValueAndUom', args = { | |||
"'''" .. value .. "'''", | |||
id = prop.Key, | |||
frame:expandTemplate{ title = 'Lang', args = { key = prop.Key .. '_postfix' } }, | |||
uom_style = 'font-size: calc(1em - 2px); color: #B2B2B2' | |||
}} | |||
}} | |||
return string.format( | |||
-- REMOVED "overflow: hidden;" FROM THE FIRST DIV'S STYLE | |||
'<div style="display:flex; flex-direction:column; flex-grow: 1; flex-basis: 0; max-height: 100%%; padding: 10px 0px 10px 0px; margin: 3px;">' .. | |||
' %s' .. | |||
' <div style="position: relative;"><div style="position: absolute; top: -20px; right: -2px">%s</div></div>' .. | |||
' <div style="display:flex; flex-direction:column; flex-grow: 1; justify-content: space-between; background: %s; text-align: center; border: 3px solid %s; box-shadow: 0 0 10px %s; border-radius: 1px">' .. | |||
' <div style="font-weight: bold;">%s</div>' .. | |||
' <div style="font-size: 0.8rem; padding-bottom: 3px">%s</div>' .. | |||
' </div>' .. | |||
'</div>', | |||
prop.Title and string.format('<div style="padding-bottom: 2px; font-size: 0.8rem; color: #9C9C9C; font-variant: small-caps;"><b>%s</b></div>', prop.Title) or '', | |||
multiplierHtml, | |||
current_style.grad, | |||
current_style.border, | |||
current_style.shadow, | |||
iconHtml, | |||
frame:expandTemplate{ title = 'Lang', args = { key = prop.Key .. '_label' }} | |||
) | |||
end | |||
local buildAltBoxHtml = function(...) end -- Forward declare | |||
local buildUpgradeBoxHtml = function(...) end -- Forward declare | |||
-- ==================================================================== | |||
-- Main Entry Point (With Final Fixes) | |||
-- ==================================================================== | |||
function p.get_ability_card(frame_args) | |||
local frame = mw.getCurrentFrame() | |||
local args = frame_args.args | |||
local hero_name = args[1] | |||
local ability_num = args[2] | |||
local add_link = args[3] == 'true' | |||
local notes = args[4] | |||
local hero_key = get_hero_key(hero_name) | |||
if not hero_key then return 'Hero with name "' .. tostring(hero_name) .. '" not found' end | |||
local ability = utils.get_ability_card_data(hero_key, ability_num) | |||
if not ability then return 'Ability data not found for hero ' .. tostring(hero_key) .. ' and num ' .. tostring(ability_num) end | |||
local html = {} | |||
local ability_name_localized = lang.get_string(ability.Key) | |||
local name_link_target = add_link and ability_name_localized or nil | |||
-- Header Section | |||
local header_attrs_left = {} | |||
local cast_time_data = ability.Cast and ability.Cast.AbilityChannelTime | |||
table.insert(header_attrs_left, buildHeaderAttrHtml(cast_time_data and cast_time_data.Value, 's', get_attr_ss(cast_time_data), '[[File:AttributeIconMaxChargesIncrease.png|Channel Time|20px|link=]]')) | |||
table.insert(header_attrs_left, buildHeaderAttrHtml(ability.AbilityCastRange and ability.AbilityCastRange.Value, 'm', get_attr_ss(ability.AbilityCastRange), '[[File:CastRange.png|Cast Range|20px|link=]]')) | |||
table.insert(header_attrs_left, buildHeaderAttrHtml(ability.Radius and ability.Radius.Value, 'm', get_attr_ss(ability.Radius), '[[File:AttributeIconTechRange.png|Radius|20px|link=]]')) | |||
table.insert(header_attrs_left, buildHeaderAttrHtml(ability.AbilityDuration and ability.AbilityDuration.Value, 's', get_attr_ss(ability.AbilityDuration), '[[File:AttributeIconTechDuration.png|Duration|20px|link=]]')) | |||
local header_attrs_right_top = {} | |||
table.insert(header_attrs_right_top, buildHeaderAttrHtml(ability.AbilityCharges and ability.AbilityCharges.Value, '', nil, '[[File:AttributeIconMaxChargesIncrease.png|Number of Charges|20px|link=]]', 'margin-right: 0;')) | |||
table.insert(header_attrs_right_top, buildHeaderAttrHtml(ability.AbilityCooldownBetweenCharge and ability.AbilityCooldownBetweenCharge.Value, 's', get_attr_ss(ability.AbilityCooldownBetweenCharge), '[[File:AttributeIconTechCooldownBetweenChargeUses.png|Charge Cooldown|16px|link=]]', 'margin-left: 1px; margin-right: 0;')) | |||
local header_attrs_right_bottom = buildHeaderAttrHtml(ability.AbilityCooldown and ability.AbilityCooldown.Value, 's', get_attr_ss(ability.AbilityCooldown), '[[File:AttributeIconTechCooldown.png|Cooldown|20px|link=]]') | |||
table.insert(html, string.format( | |||
'<div style="width: 100%%;"><div style="display:flex; flex-direction:row; justify-content: space-between; background: #121212; font-size: calc(1em - 2px); border-radius: 13px 13px 0 0; padding: 0 8px 14px 8px;">' .. | |||
' <div style="display:flex; flex-direction:column;">' .. | |||
' <div style="font-size: 1.3rem;"><span style="filter: brightness(0) saturate(100%%) invert(98%%) sepia(19%%) saturate(1458%%) hue-rotate(301deg) brightness(102%%) contrast(109%%); padding: 0 5px 0 7px">[[File:%s.png|45px|link=]]</span><span style="font-family:\'Retail Demo\',\'Open Sans\'; font-weight:bold">%s</span></div>' .. | |||
' <div style="display:flex; flex-direction:row; margin-left: 10px; height: 40px;">%s</div>' .. | |||
' </div>' .. | |||
' <div style="display:flex; flex-direction:column; align-items: end; justify-content: flex-end;">' .. | |||
' <div style="display:flex; flex-direction:row; justify-content: flex-end; margin-bottom: 5px; margin-right: 20px;">%s</div>' .. | |||
' %s' .. | |||
' </div>' .. | |||
'</div></div>', | |||
lang.get_string(ability.Key, 'en'), | |||
name_link_target and string.format('[[%s|%s]]', name_link_target, ability_name_localized) or ability_name_localized, | |||
table.concat(header_attrs_left), | |||
table.concat(header_attrs_right_top), | |||
header_attrs_right_bottom | |||
)) | |||
-- Info Sections | |||
table.insert(html, '<div style="margin: 5px 10px 0px 10px; padding: 3px 0 5px 0; width: calc(100%% - 18px); box-sizing: unset;">') | |||
for i = 1, 3 do | |||
local info_section = ability['Info' .. i] | |||
if info_section then | |||
local desc = (info_section.DescKey and lang.get_string(info_section.DescKey)) or '' | |||
local main_boxes, alt_boxes = {}, {} | |||
if info_section.Main and info_section.Main.Props then | |||
for _, prop in ipairs(info_section.Main.Props) do | |||
if prop.Value and prop.Value ~= 0 then | |||
table.insert(main_boxes, buildMainBoxHtml(prop, hero_key)) | |||
end | |||
end | |||
end | |||
if info_section.Alt then | |||
for _, prop in ipairs(info_section.Alt) do | |||
if prop.Value and prop.Value ~= 0 then | |||
table.insert(alt_boxes, buildAltBoxHtml(prop)) | |||
end | |||
end | |||
end | |||
if desc ~= '' or #main_boxes > 0 or #alt_boxes > 0 then | |||
local alt_box_container = '' | |||
if #alt_boxes > 0 then | |||
-- FIXED: Added the missing wrapper div for alt boxes. | |||
alt_box_container = string.format( | |||
'<div style="display:flex; flex-direction:row; flex-wrap: wrap; justify-content: center; align-items: center; background-color: #2A2A2A; width: calc(97%% - 19px); min-width: 267px; padding: 0 4px 0 4px; column-gap: 15px">%s</div>', | |||
table.concat(alt_boxes) | |||
) | |||
end | |||
table.insert(html, string.format( | |||
'<div style="display:flex; flex-direction:column; align-items: center; width: 100%%; padding-top: 8px;">' .. | |||
' <div style="padding: 0 10px 10px 10px">%s</div>' .. | |||
' <div style="display:flex; flex-direction:row; flex-wrap: wrap; justify-content: center; width: calc(97%% - 5px); min-width: 280px;">%s</div>' .. | |||
' %s' .. | |||
'</div>', | |||
frame:preprocess(desc), | |||
table.concat(main_boxes), | |||
alt_box_container | |||
)) | |||
end | |||
end | |||
end | |||
table.insert(html, '</div>') | |||
-- Upgrades Section | |||
local upgrade_boxes = {} | |||
if ability.Upgrades then | |||
for i, prop in ipairs(ability.Upgrades) do | |||
table.insert(upgrade_boxes, buildUpgradeBoxHtml(prop, i)) | |||
end | |||
end | |||
table.insert(html, string.format( | |||
'<div style="display:flex; flex-direction:row; flex-wrap: wrap; margin-bottom: 5px; justify-content: center; width: calc(97%% - 16px); min-width: 280px;">%s</div>', | |||
table.concat(upgrade_boxes) | |||
)) | |||
-- Notes Section | |||
if notes and notes ~= '' then | |||
table.insert(html, string.format( | |||
'<div style="align-self: flex-start; margin: 10px 0 10px 20px; font-size: 0.8rem;">'.. | |||
'<div><span style="font-size:1.15rem;">%s</span></div>' .. | |||
'<div class="mw-collapsible mw-collapsed" style="margin-top: 5px;">' .. | |||
'<div>%s</div>' .. | |||
'</div></div>', | |||
frame:callParserFunction( '#invoke', 'Dictionary', 'translate', 'Notes' ), | |||
frame:preprocess(notes) | |||
)) | |||
end | |||
local final_card = string.format( | |||
'{{anchor|%s}}<div style="font-size: 0.9rem; line-height: 1.4; font-family: \'Retail Demo\', \'Open Sans\', \'PT Serif\', serif; color: #FFEFD7; display:flex; flex-direction:column; background: linear-gradient(90deg, rgba(52,52,52,1) 0%%, rgba(66,66,66,1) 14%%, rgba(77,77,77,1) 100%%); align-items: center; width: 100%%; max-width: 500px; border-radius: 13px 13px 4px 4px;">%s</div>', | |||
ability_name_localized, | |||
table.concat(html, '\n') | |||
) | |||
-- FIXED: Preprocess the entire output to render the anchor and other wikitext. | |||
return frame:preprocess(final_card) | |||
end | |||
-- We re-paste the remaining builder functions here so the module is complete in one block. | |||
buildAltBoxHtml = function(prop) | |||
local frame = mw.getCurrentFrame() | |||
local scaleHtml = '' | |||
if prop.Scale and prop.Scale.Type and prop.Scale.Value then | |||
scaleHtml = ' ' .. buildMultiplierHtml(prop.Scale.Type, commonutils.round_to_sig_fig(prop.Scale.Value, 3), '35px') | |||
end | |||
local icon_data = get_icon(prop.Type) | |||
local iconHtml = frame:expandTemplate{ title = 'Icon/' .. (icon_data.color or 'Grey'), args = { | |||
string.format('[[File:%s|%s|link=%s]]', icon_data.img, icon_data.size or '18px', icon_data.link), | |||
frame:expandTemplate{ title = 'ValueAndUom', args = { | |||
"'''" .. prop.Value .. "'''", | |||
id = prop.Key, | |||
frame:expandTemplate{ title = 'Lang', args = { key = prop.Key .. '_postfix' } }, | |||
uom_style = 'font-size: 10px; color: #9C9C9C' | |||
}} | |||
}} | |||
return string.format( | |||
'<div style="display:flex; flex-direction:row; flex-grow: 1; flex-basis: 0; justify-content: left; background-color: #2A2A2A; font-size: 0.85em; margin: 3px; text-align: center; box-shadow: 0 0 10px #2A2A2A; border-radius: 1px; white-space: nowrap">' .. | |||
' <span style="white-space: nowrap">%s <span style="font-size: 0.75rem; white-space: nowrap">%s%s</span></span>' .. | |||
'</div>', | |||
iconHtml, | |||
frame:expandTemplate{ title = 'Lang', args = { key = prop.Key .. '_label' }}, | |||
scaleHtml | |||
) | |||
end | |||
UPGRADE_COST_MAP = {1, 2, 5} | |||
buildUpgradeBoxHtml = function(prop, index) | |||
local frame = mw.getCurrentFrame() | |||
local description = lang.get_string(prop.DescKey) | |||
if (description == nil or description == '') then | |||
local desc_parts = {} | |||
local seen = {} | |||
for k, v in pairs(prop) do | |||
if type(v) ~= 'table' then | |||
local formatted_value = utils.format_value_with_prepost(k, v, frame) | |||
local attr_name = lang.get_string(k..'_label') | |||
local key = formatted_value .. '|' .. attr_name | |||
if not seen[key] then | |||
table.insert(desc_parts, string.format('%s %s', formatted_value, attr_name)) | |||
seen[key] = true | |||
end | |||
end | |||
end | |||
description = table.concat(desc_parts, '<br>') | |||
end | |||
local d_len = mw.ustring.len(description) | |||
local fontsize = '1em' | |||
if d_len > 60 and d_len < 71 then fontsize = '0.95rem' | |||
elseif d_len > 70 and d_len < 91 then fontsize = '0.875rem' | |||
elseif d_len > 90 then fontsize = '0.8rem' | |||
end | |||
local scaleHtml = '' | |||
if prop.Scale and prop.Scale.Type and prop.Scale.Value then | |||
scaleHtml = buildMultiplierHtml(prop.Scale.Type, commonutils.round_to_sig_fig(prop.Scale.Value, 3)) | |||
end | |||
return string.format( | |||
'<div style="display:flex; flex-direction:column; flex-grow: 1; flex-basis: 0; justify-content: flex-start; max-height: 140px; background-color: #555555; padding: 0 0 20px 0px; margin: 6px; text-align: center; box-shadow: 0 0 10px #2A2A2A; border-radius: 5px">' .. | |||
' <div style="padding: 2px 0 0 2px; font-family:Retail Demo bold; font-size: clamp(0.70em, 2vw, 1em); background-color: #121212; border-radius: 5px 5px 0 0; ">%s</div>' .. | |||
' <div style="padding: 10px 4px 4px 4px; font-size: clamp(0.70em, 2vw, %s);">%s</div>' .. | |||
' <div>%s</div>' .. | |||
'</div>', | |||
frame:expandTemplate{ title = 'Ap', args = { UPGRADE_COST_MAP[index], icon_size = '15px' } }, | |||
fontsize, | |||
frame:preprocess(description), | |||
scaleHtml | |||
) | |||
end | |||
return p | |||
Revision as of 17:11, 24 September 2025
Documentation for this module may be created at Module:Sandbox/LVL/doc
-- ====================================================================
-- Dependencies and Initial Setup
-- ====================================================================
local lang = require "Module:Lang"
local commonutils = require "Module:Utilities"
local utils = require "Module:Abilities/utils"
local p = {}
local data = mw.loadJsonData("Data:AbilityCards.json")
-- (ATTR_TYPE_ICON_MAP and helper functions remain the same)
local ATTR_TYPE_ICON_MAP = {
bullet_armor_up = {img='Bullet_Armor.png', link='Damage_Resistance', color = 'NoColor'},
bullet_armor_down = {img='Bullet_Armor.png', link='Damage_Resistance', color = 'NoColor'},
cast = {img='AttributeIconMaxChargesIncrease.png', link=''},
charges = {img='AttributeIconMaxChargesIncrease.png', link='', color = 'Purple'},
damage = {img='Damage_heart.png', link='', color = 'NoColor'},
bullet_damage = {img='Bullet_damage.png', link='Bullet_Damage', color = 'NoColor'},
fire_rate = {img='Fire Rate.png', link='Fire_Rate', color = 'Brown'},
healing = {img='Healing.png', link='Healing', color = 'Green'},
health = {img='Health.png', link='Health', color = 'Green'},
move_speed = {img='Move speed.png', link='Move Speed', color = 'Green'},
range = {img='CastRange.png', link='Ability_Range', color = 'Purple'},
tech_armor_up = {img='Spirit_Armor.png', link='Damage_Resistance', color = 'NoColor'},
tech_damage = {img='AttributeIconTechShieldHealth.png', link='Spirit_Damage', color = 'NoColor',size = '12px'},
distance = {img='AttributeIconTechRange.png', link='Ability_Range', color = 'Purple'},
duration = {img='AttributeIconTechDuration.png', link='Ability Duration', color = 'Purple'},
slow = {img='MoveSlow.png', link='', color = 'Purple'},
melee_damage = {img='Melee damage.png', link='Melee Damage'},
cooldown = {img='Cooldown Icon.png', link='Ability Cooldown', color = 'Purple'},
combat_barrier = {img='Barrier.png', link='Barrier', color = 'Green'},
time = {img='AttributeIconTechDuration.png', link='Ability Duration', color = 'Purple'},
}
function get_hero_key(hero_name) -- Unchanged
for i, hero in pairs(data) do
if hero["Name"] == hero_name then return i end
end
return nil
end
function get_icon(attr_type) -- Unchanged
local mappedAttr = ATTR_TYPE_ICON_MAP[attr_type]
local img = 'GenericProperty.png'
local link = ''
local size = ''
local color = 'Grey'
if mappedAttr then
img = mappedAttr.img
link = mappedAttr.link
size = mappedAttr.size
color = mappedAttr.color or color
end
return {img=img, link=link, color=color, size=size}
end
function get_attr_ss(attr) -- Unchanged
if not attr then return nil end
local scale = attr.Scale
if not scale or scale.Type ~= 'spirit' or scale.Value == 0 then return nil end
return commonutils.round_to_sig_fig(scale.Value, 3)
end
-- ====================================================================
-- HTML Builder Functions (With Final Fixes)
-- ====================================================================
local function buildMultiplierHtml(type, value, icon_size) -- Unchanged from last version
local config = {
spirit = { color = "#E3BDFA", bgColor = "#533669", icon = "Spirit scaling.png", link = "Spirit Power#Spirit Power Scaling", size = icon_size or "40px" },
melee = { color = "#cdb89e", bgColor = "#80550f", icon = "Melee scaling.png", link = "Melee Damage#Abilities", size = icon_size or "40px" },
boon = { color = "#bec1ac", bgColor = "#217a68", icon = "Boon scaling.png", link = "Level#Boons from Patron", size = icon_size or "25px" },
weapon_damage_increase = { color = "#cdb89e", bgColor = "#80550f", icon = "Weapon scaling.png", link = "Weapon Damage", size = icon_size or "40px" }
}
local c = config[type]
if not c or not value then return '' end
local icon = string.format('[[File:%s|link=%s|%s]]', c.icon, c.link, c.size)
return string.format(
'<span style="font-size: 0.8em; color: %s; white-space: nowrap;">%s<span style="background-color: %s; font-family: \'Retail Demo Regular\', \'PT Serif\', \'Palatino\'; font-weight:bold; width: auto; border-radius: 5px; padding: 2px 2px 0px 2px; margin-left: -2px;"><i>x</i>%s</span></span>',
c.color, icon, c.bgColor, value
)
end
--- FIXED: Replicates the original HeaderAttr template exactly.
local function buildHeaderAttrHtml(value, uom, ss, icon, style)
if not value or value == '' then return '' end
local frame = mw.getCurrentFrame()
-- The original template used {{Icon/Grey}}, which we can replicate with a span and filter for simplicity,
-- or just output the icon directly if the filter isn't critical. Let's keep it simple.
local mainBox = string.format('%s %s',
icon,
frame:expandTemplate{ title = "ValueAndUom", args = {
"'''" .. value .. "'''",
uom,
uom_style = "font-size: calc(1em - 2px); color: #B2B2B2"
}}
)
local spiritScale = ''
if ss and ss ~= '' then
spiritScale = string.format(
'<div style="position: relative;"><div style="position: absolute; top: -24px; right: -14px">%s</div></div>',
buildMultiplierHtml('spirit', ss, '28px')
)
end
return string.format(
'<div style="background-color: #2C2C2C; padding: 10px 6px 10px 6px; margin-right: 20px; white-space: nowrap; %s">%s</div>%s',
style or '', mainBox, spiritScale
)
end
--- FIXED: Removed "overflow: hidden;" to prevent clipping.
local function buildMainBoxHtml(prop, hero_key)
local frame = mw.getCurrentFrame()
local scale_type = prop.Scale and prop.Scale.Type
local scale_value = prop.Scale and commonutils.round_to_sig_fig(prop.Scale.Value, 3)
local scale_styles = {
melee = { grad = "radial-gradient(circle, rgba(36,37,36,1) 0%%, rgba(66,55,47,1) 100%%)", border = "#5b412b", shadow = "#3d2c4d" },
spirit = { grad = "radial-gradient(circle, rgba(42,41,43,1) 0%%, rgba(59,49,69,1) 100%%)", border = "#583D6F", shadow = "#3d2c4d" },
power_increase = { grad = "radial-gradient(circle, rgba(42,41,43,1) 0%%, rgba(61,78,70,1) 100%%)", border = "#48675a", shadow = "#3d2c4d" }
}
local current_style = scale_styles[scale_type] or { grad = "#2A2A2A", border = "#2A2A2A", shadow = "#2A2A2A" }
local multiplierHtml = buildMultiplierHtml(scale_type, scale_value)
local icon_data = get_icon(prop.Type)
local value = prop.Value
if scale_type == 'melee' then
local hero_data = mw.loadJsonData("Data:HeroData.json")
local hero = hero_data[hero_key]
if hero and hero.LightMeleeDamage and prop.Scale.Value then
value = prop.Value + hero.LightMeleeDamage * prop.Scale.Value
end
end
local iconHtml = frame:expandTemplate{ title = 'Icon/' .. (icon_data.color or 'Grey'), args = {
string.format('[[File:%s|%s|link=%s]]', icon_data.img, icon_data.size or '18px', icon_data.link),
frame:expandTemplate{ title = 'ValueAndUom', args = {
"'''" .. value .. "'''",
id = prop.Key,
frame:expandTemplate{ title = 'Lang', args = { key = prop.Key .. '_postfix' } },
uom_style = 'font-size: calc(1em - 2px); color: #B2B2B2'
}}
}}
return string.format(
-- REMOVED "overflow: hidden;" FROM THE FIRST DIV'S STYLE
'<div style="display:flex; flex-direction:column; flex-grow: 1; flex-basis: 0; max-height: 100%%; padding: 10px 0px 10px 0px; margin: 3px;">' ..
' %s' ..
' <div style="position: relative;"><div style="position: absolute; top: -20px; right: -2px">%s</div></div>' ..
' <div style="display:flex; flex-direction:column; flex-grow: 1; justify-content: space-between; background: %s; text-align: center; border: 3px solid %s; box-shadow: 0 0 10px %s; border-radius: 1px">' ..
' <div style="font-weight: bold;">%s</div>' ..
' <div style="font-size: 0.8rem; padding-bottom: 3px">%s</div>' ..
' </div>' ..
'</div>',
prop.Title and string.format('<div style="padding-bottom: 2px; font-size: 0.8rem; color: #9C9C9C; font-variant: small-caps;"><b>%s</b></div>', prop.Title) or '',
multiplierHtml,
current_style.grad,
current_style.border,
current_style.shadow,
iconHtml,
frame:expandTemplate{ title = 'Lang', args = { key = prop.Key .. '_label' }}
)
end
local buildAltBoxHtml = function(...) end -- Forward declare
local buildUpgradeBoxHtml = function(...) end -- Forward declare
-- ====================================================================
-- Main Entry Point (With Final Fixes)
-- ====================================================================
function p.get_ability_card(frame_args)
local frame = mw.getCurrentFrame()
local args = frame_args.args
local hero_name = args[1]
local ability_num = args[2]
local add_link = args[3] == 'true'
local notes = args[4]
local hero_key = get_hero_key(hero_name)
if not hero_key then return 'Hero with name "' .. tostring(hero_name) .. '" not found' end
local ability = utils.get_ability_card_data(hero_key, ability_num)
if not ability then return 'Ability data not found for hero ' .. tostring(hero_key) .. ' and num ' .. tostring(ability_num) end
local html = {}
local ability_name_localized = lang.get_string(ability.Key)
local name_link_target = add_link and ability_name_localized or nil
-- Header Section
local header_attrs_left = {}
local cast_time_data = ability.Cast and ability.Cast.AbilityChannelTime
table.insert(header_attrs_left, buildHeaderAttrHtml(cast_time_data and cast_time_data.Value, 's', get_attr_ss(cast_time_data), '[[File:AttributeIconMaxChargesIncrease.png|Channel Time|20px|link=]]'))
table.insert(header_attrs_left, buildHeaderAttrHtml(ability.AbilityCastRange and ability.AbilityCastRange.Value, 'm', get_attr_ss(ability.AbilityCastRange), '[[File:CastRange.png|Cast Range|20px|link=]]'))
table.insert(header_attrs_left, buildHeaderAttrHtml(ability.Radius and ability.Radius.Value, 'm', get_attr_ss(ability.Radius), '[[File:AttributeIconTechRange.png|Radius|20px|link=]]'))
table.insert(header_attrs_left, buildHeaderAttrHtml(ability.AbilityDuration and ability.AbilityDuration.Value, 's', get_attr_ss(ability.AbilityDuration), '[[File:AttributeIconTechDuration.png|Duration|20px|link=]]'))
local header_attrs_right_top = {}
table.insert(header_attrs_right_top, buildHeaderAttrHtml(ability.AbilityCharges and ability.AbilityCharges.Value, '', nil, '[[File:AttributeIconMaxChargesIncrease.png|Number of Charges|20px|link=]]', 'margin-right: 0;'))
table.insert(header_attrs_right_top, buildHeaderAttrHtml(ability.AbilityCooldownBetweenCharge and ability.AbilityCooldownBetweenCharge.Value, 's', get_attr_ss(ability.AbilityCooldownBetweenCharge), '[[File:AttributeIconTechCooldownBetweenChargeUses.png|Charge Cooldown|16px|link=]]', 'margin-left: 1px; margin-right: 0;'))
local header_attrs_right_bottom = buildHeaderAttrHtml(ability.AbilityCooldown and ability.AbilityCooldown.Value, 's', get_attr_ss(ability.AbilityCooldown), '[[File:AttributeIconTechCooldown.png|Cooldown|20px|link=]]')
table.insert(html, string.format(
'<div style="width: 100%%;"><div style="display:flex; flex-direction:row; justify-content: space-between; background: #121212; font-size: calc(1em - 2px); border-radius: 13px 13px 0 0; padding: 0 8px 14px 8px;">' ..
' <div style="display:flex; flex-direction:column;">' ..
' <div style="font-size: 1.3rem;"><span style="filter: brightness(0) saturate(100%%) invert(98%%) sepia(19%%) saturate(1458%%) hue-rotate(301deg) brightness(102%%) contrast(109%%); padding: 0 5px 0 7px">[[File:%s.png|45px|link=]]</span><span style="font-family:\'Retail Demo\',\'Open Sans\'; font-weight:bold">%s</span></div>' ..
' <div style="display:flex; flex-direction:row; margin-left: 10px; height: 40px;">%s</div>' ..
' </div>' ..
' <div style="display:flex; flex-direction:column; align-items: end; justify-content: flex-end;">' ..
' <div style="display:flex; flex-direction:row; justify-content: flex-end; margin-bottom: 5px; margin-right: 20px;">%s</div>' ..
' %s' ..
' </div>' ..
'</div></div>',
lang.get_string(ability.Key, 'en'),
name_link_target and string.format('[[%s|%s]]', name_link_target, ability_name_localized) or ability_name_localized,
table.concat(header_attrs_left),
table.concat(header_attrs_right_top),
header_attrs_right_bottom
))
-- Info Sections
table.insert(html, '<div style="margin: 5px 10px 0px 10px; padding: 3px 0 5px 0; width: calc(100%% - 18px); box-sizing: unset;">')
for i = 1, 3 do
local info_section = ability['Info' .. i]
if info_section then
local desc = (info_section.DescKey and lang.get_string(info_section.DescKey)) or ''
local main_boxes, alt_boxes = {}, {}
if info_section.Main and info_section.Main.Props then
for _, prop in ipairs(info_section.Main.Props) do
if prop.Value and prop.Value ~= 0 then
table.insert(main_boxes, buildMainBoxHtml(prop, hero_key))
end
end
end
if info_section.Alt then
for _, prop in ipairs(info_section.Alt) do
if prop.Value and prop.Value ~= 0 then
table.insert(alt_boxes, buildAltBoxHtml(prop))
end
end
end
if desc ~= '' or #main_boxes > 0 or #alt_boxes > 0 then
local alt_box_container = ''
if #alt_boxes > 0 then
-- FIXED: Added the missing wrapper div for alt boxes.
alt_box_container = string.format(
'<div style="display:flex; flex-direction:row; flex-wrap: wrap; justify-content: center; align-items: center; background-color: #2A2A2A; width: calc(97%% - 19px); min-width: 267px; padding: 0 4px 0 4px; column-gap: 15px">%s</div>',
table.concat(alt_boxes)
)
end
table.insert(html, string.format(
'<div style="display:flex; flex-direction:column; align-items: center; width: 100%%; padding-top: 8px;">' ..
' <div style="padding: 0 10px 10px 10px">%s</div>' ..
' <div style="display:flex; flex-direction:row; flex-wrap: wrap; justify-content: center; width: calc(97%% - 5px); min-width: 280px;">%s</div>' ..
' %s' ..
'</div>',
frame:preprocess(desc),
table.concat(main_boxes),
alt_box_container
))
end
end
end
table.insert(html, '</div>')
-- Upgrades Section
local upgrade_boxes = {}
if ability.Upgrades then
for i, prop in ipairs(ability.Upgrades) do
table.insert(upgrade_boxes, buildUpgradeBoxHtml(prop, i))
end
end
table.insert(html, string.format(
'<div style="display:flex; flex-direction:row; flex-wrap: wrap; margin-bottom: 5px; justify-content: center; width: calc(97%% - 16px); min-width: 280px;">%s</div>',
table.concat(upgrade_boxes)
))
-- Notes Section
if notes and notes ~= '' then
table.insert(html, string.format(
'<div style="align-self: flex-start; margin: 10px 0 10px 20px; font-size: 0.8rem;">'..
'<div><span style="font-size:1.15rem;">%s</span></div>' ..
'<div class="mw-collapsible mw-collapsed" style="margin-top: 5px;">' ..
'<div>%s</div>' ..
'</div></div>',
frame:callParserFunction( '#invoke', 'Dictionary', 'translate', 'Notes' ),
frame:preprocess(notes)
))
end
local final_card = string.format(
'{{anchor|%s}}<div style="font-size: 0.9rem; line-height: 1.4; font-family: \'Retail Demo\', \'Open Sans\', \'PT Serif\', serif; color: #FFEFD7; display:flex; flex-direction:column; background: linear-gradient(90deg, rgba(52,52,52,1) 0%%, rgba(66,66,66,1) 14%%, rgba(77,77,77,1) 100%%); align-items: center; width: 100%%; max-width: 500px; border-radius: 13px 13px 4px 4px;">%s</div>',
ability_name_localized,
table.concat(html, '\n')
)
-- FIXED: Preprocess the entire output to render the anchor and other wikitext.
return frame:preprocess(final_card)
end
-- We re-paste the remaining builder functions here so the module is complete in one block.
buildAltBoxHtml = function(prop)
local frame = mw.getCurrentFrame()
local scaleHtml = ''
if prop.Scale and prop.Scale.Type and prop.Scale.Value then
scaleHtml = ' ' .. buildMultiplierHtml(prop.Scale.Type, commonutils.round_to_sig_fig(prop.Scale.Value, 3), '35px')
end
local icon_data = get_icon(prop.Type)
local iconHtml = frame:expandTemplate{ title = 'Icon/' .. (icon_data.color or 'Grey'), args = {
string.format('[[File:%s|%s|link=%s]]', icon_data.img, icon_data.size or '18px', icon_data.link),
frame:expandTemplate{ title = 'ValueAndUom', args = {
"'''" .. prop.Value .. "'''",
id = prop.Key,
frame:expandTemplate{ title = 'Lang', args = { key = prop.Key .. '_postfix' } },
uom_style = 'font-size: 10px; color: #9C9C9C'
}}
}}
return string.format(
'<div style="display:flex; flex-direction:row; flex-grow: 1; flex-basis: 0; justify-content: left; background-color: #2A2A2A; font-size: 0.85em; margin: 3px; text-align: center; box-shadow: 0 0 10px #2A2A2A; border-radius: 1px; white-space: nowrap">' ..
' <span style="white-space: nowrap">%s <span style="font-size: 0.75rem; white-space: nowrap">%s%s</span></span>' ..
'</div>',
iconHtml,
frame:expandTemplate{ title = 'Lang', args = { key = prop.Key .. '_label' }},
scaleHtml
)
end
UPGRADE_COST_MAP = {1, 2, 5}
buildUpgradeBoxHtml = function(prop, index)
local frame = mw.getCurrentFrame()
local description = lang.get_string(prop.DescKey)
if (description == nil or description == '') then
local desc_parts = {}
local seen = {}
for k, v in pairs(prop) do
if type(v) ~= 'table' then
local formatted_value = utils.format_value_with_prepost(k, v, frame)
local attr_name = lang.get_string(k..'_label')
local key = formatted_value .. '|' .. attr_name
if not seen[key] then
table.insert(desc_parts, string.format('%s %s', formatted_value, attr_name))
seen[key] = true
end
end
end
description = table.concat(desc_parts, '<br>')
end
local d_len = mw.ustring.len(description)
local fontsize = '1em'
if d_len > 60 and d_len < 71 then fontsize = '0.95rem'
elseif d_len > 70 and d_len < 91 then fontsize = '0.875rem'
elseif d_len > 90 then fontsize = '0.8rem'
end
local scaleHtml = ''
if prop.Scale and prop.Scale.Type and prop.Scale.Value then
scaleHtml = buildMultiplierHtml(prop.Scale.Type, commonutils.round_to_sig_fig(prop.Scale.Value, 3))
end
return string.format(
'<div style="display:flex; flex-direction:column; flex-grow: 1; flex-basis: 0; justify-content: flex-start; max-height: 140px; background-color: #555555; padding: 0 0 20px 0px; margin: 6px; text-align: center; box-shadow: 0 0 10px #2A2A2A; border-radius: 5px">' ..
' <div style="padding: 2px 0 0 2px; font-family:Retail Demo bold; font-size: clamp(0.70em, 2vw, 1em); background-color: #121212; border-radius: 5px 5px 0 0; ">%s</div>' ..
' <div style="padding: 10px 4px 4px 4px; font-size: clamp(0.70em, 2vw, %s);">%s</div>' ..
' <div>%s</div>' ..
'</div>',
frame:expandTemplate{ title = 'Ap', args = { UPGRADE_COST_MAP[index], icon_size = '15px' } },
fontsize,
frame:preprocess(description),
scaleHtml
)
end
return p