Editing Module:Sandbox/LVLGive feedback
The edit can be undone.
Please check the comparison below to verify that this is what you want to do, and then publish the changes below to finish undoing the edit.
| Latest revision | Your text | ||
| 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 p = {} | ||
local data = mw.loadJsonData("Data:AbilityCards.json") | |||
local | -- (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 | 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 | 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 | end | ||
-- | --- FIXED: Removed "overflow: hidden;" to prevent clipping. | ||
- | local function buildMainBoxHtml(prop, hero_key) | ||
local function | 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 | 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 | end | ||
return p | return p | ||