| Latest revision |
Your text |
| Line 1: |
Line 1: |
| local p = {}; | | local p = {}; |
|
| |
|
| local Icon = require('Module:Icon')
| |
| local Souls = require('Module:Souls')
| |
|
| |
| -- Loads the necessary data from the wiki's JSON pages.
| |
| local heroes_data = mw.loadJsonData("Data:HeroData.json") | | local heroes_data = mw.loadJsonData("Data:HeroData.json") |
| local soul_unlocks_data = mw.loadJsonData("Data:SoulUnlockData.json") | | local soul_unlocks_data = mw.loadJsonData("Data:SoulUnlockData.json") |
|
| |
|
| -- Helper to safely get stats that may be nested under Weapon
| |
| local function get_stat_value(data, key)
| |
| if data[key] ~= nil then return data[key] end
| |
| if data.Weapon and data.Weapon[key] ~= nil then return data.Weapon[key] end
| |
| return 0
| |
| end
| |
|
| |
| -- Defines human-readable names for the internal property keys used in headers.
| |
| local level_scaling_key_to_name = { | | local level_scaling_key_to_name = { |
| BulletDamage = "Bullet Damage", | | BulletDamage = "Bullet Damage", |
| Line 24: |
Line 12: |
| } | | } |
|
| |
|
| -- A helper function that returns a sort-friendly version of a name by removing leading articles like "The ". | | -- Not in HeroData.json although Shiv is just (bullet damage * 12) |
| local function get_sortable_name(name) | | local alt_fires = { |
| if string.sub(name, 1, 4) == "The " then | | Shiv = {base = 59.4, level = 2.64, suffix = "alt fire 12 pellets"}, |
| return string.sub(name, 5)
| | Viscous = {base = 63, level = 0.9, suffix = "alt fire AOE"}, |
| end | | Yamato = {base =63, level = 0.35, suffix = "alt fire AOE"} |
| return name
| | } |
| end
| |
|
| |
|
| --[[
| |
| -- @function p.write_power_increase_table
| |
| -- @description Generates a responsive and readable table showing hero stat scaling based on boons.
| |
| -- Includes a compact summary view and an expandable, detailed view for all levels.
| |
| -- Can optionally wrap the entire output in a theme-aware collapsible section.
| |
| -- @param {Frame} frame The MediaWiki frame object.
| |
| -- @param {string} frame.args[1] The internal property name to display (e.g., 'BulletDamage').
| |
| -- @param {string} [frame.args.title] An optional title. If provided, the table is wrapped in a collapsible section.
| |
| -- @return {string} The complete HTML for the table or collapsible section.
| |
| --]]
| |
| p.write_power_increase_table = function(frame) | | p.write_power_increase_table = function(frame) |
| -- Use the same smart sorting for this list.
| |
| local function hero_sort(h1, h2)
| |
| return get_sortable_name(h1.Name) < get_sortable_name(h2.Name)
| |
| end
| |
| -- Read and validate the arguments passed from the #invoke call.
| |
| local property = frame.args[1]
| |
| local title = frame.args.title
| |
|
| |
| if not property or property == '' then
| |
| return '<strong class="error">Error: Missing required property argument in #invoke call. Example: {{#invoke:ModuleName|write_power_increase_table|BulletDamage}}</strong>'
| |
| end
| |
|
| |
| local name = level_scaling_key_to_name[property]
| |
|
| |
| if not name then
| |
| return '<strong class="error">Error: Invalid property argument "' .. tostring(property) .. '". Must be one of: BulletDamage, MaxHealth, TechPower, LightMeleeDamage, HeavyMeleeDamage.</strong>'
| |
| end
| |
|
| |
| -- Create an ordered list of soul requirements for each power increase.
| |
| local pi_souls_list = {} | | local pi_souls_list = {} |
| for _, v in ipairs(soul_unlocks_data) do | | for _, v in ipairs(soul_unlocks_data) do |
| Line 68: |
Line 26: |
| end | | end |
| end | | end |
| | local level_count = 0 |
| | for _ in ipairs(pi_souls_list) do level_count = level_count + 1 end |
| | | |
| local max_boons = #pi_souls_list
| |
|
| |
| -- Define the milestones to display in the summary table.
| |
| local milestones = {10, 20, max_boons}
| |
|
| |
| -- Filter out disabled heroes from the data.
| |
| local filtered_heroes = {} | | local filtered_heroes = {} |
| for _, hero in pairs(heroes_data) do | | for _, hero in pairs(heroes_data) do |
| if (not hero["IsDisabled"] and not hero["InDevelopment"] and hero["Name"]) then | | if (not hero["IsDisabled"]) then |
| table.insert(filtered_heroes, hero) | | table.insert(filtered_heroes, hero) |
| end | | end |
| end | | end |
| | | |
| -- Sort the hero list alphabetically, ignoring "The " for proper sorting. | | local property = frame.args[1] --frame:getParent().args[1] |
| table.sort(filtered_heroes, hero_sort) | | local name = level_scaling_key_to_name[property] |
| | local suffix = property == "BulletDamage" and " (Full Shot)" or "" |
| | |
| | local create_table = mw.html.create("table"):addClass("wikitable sortable mw-collapsible mw-collapsed"):done() |
| | local title_header = mw.html.create("tr") |
| | title_header:tag("th"):attr("colspan", (level_count + 3)):wikitext("Base " .. name .. " based on PI"):done() |
| | create_table:node(title_header) |
| | | |
| -- Begin building the main data table.
| | local label_header = mw.html.create("tr") |
| local create_table = mw.html.create("table"):addClass("wikitable sortable") | | label_header |
| | :tag("th"):attr("rowspan", 2):wikitext("Hero"):done() |
| | :tag("th"):attr("rowspan", 2):wikitext("Base " .. name .. suffix):done() |
| | :tag("th"):attr("rowspan", 2):wikitext("PL Increase" .. suffix):done() |
| | :tag("th"):attr("colspan", level_count):wikitext("Souls Collected"):done() |
| | create_table:node(label_header) |
| | | |
| -- Create the header row for the summary table.
| | local souls_header = mw.html.create("tr") |
| local header_row = mw.html.create("tr") | | for _, v in ipairs(pi_souls_list) do |
| header_row
| | local souls_formatted = string.format("%.1fK", (v + 400) / 1000) |
| :tag("th"):wikitext("Hero"):done()
| | souls_formatted = frame:expandTemplate{ title = "Souls", args = {souls_formatted}} |
| :tag("th"):wikitext("Base " .. name):done()
| | souls_header:tag("th"):wikitext(souls_formatted):done() |
| :tag("th"):wikitext("Increase per Boon"):done()
| |
| | |
| -- Dynamically create milestone headers with both boon count and soul amount.
| |
| for _, count in ipairs(milestones) do | |
| local header_text = count .. " Boons"
| |
| if count == max_boons then header_text = header_text .. " (Max)" end
| |
| local required_souls = pi_souls_list[count]
| |
| local souls_formatted = string.format("%.1fK", (required_souls + 400) / 1000) | |
| local soul_display = Souls._render(souls_formatted) | |
| local full_header = header_text .. "<br /><small>(" .. soul_display .. ")</small>"
| |
| header_row:tag("th"):wikitext(full_header):done() | |
| end | | end |
| | create_table:node(souls_header) |
| | | |
| header_row:tag("th"):wikitext("Full Details"):done() | | for _, hero in ipairs(filtered_heroes) do |
| create_table:node(header_row)
| | local base = math.floor((hero[property] or 0) * 100) / 100 |
|
| | local level = math.floor(hero["LevelScaling"][property] * 100) / 100 |
| -- A helper function that generates the HTML for a single hero's row and its collapsible details.
| | local shots = hero["BulletsPerShot"] |
| local function addHeroRows(hero_data, is_alt_fire)
| | local burst = hero["BulletsPerBurst"] |
| local base, level, suffix | | local bullet_mult = shots * burst |
| local hero_icon_wikitext = Icon._render(hero_data['Name'])
| | local hero_icon = frame:expandTemplate{ title = "HeroIcon", args = {hero["Name"]}} |
| | | local hero_suffix = "" |
| -- Determine if this is a main weapon or an alt-fire and set the correct data.
| | if (property == "BulletDamage" and bullet_mult > 1) then |
| if is_alt_fire then
| | base = base * bullet_mult |
| base = hero_data.alt_fire.base
| | level = level * bullet_mult |
| level = hero_data.alt_fire.level
| | if (shots > 1) then hero_suffix = hero_suffix .. " (" .. shots .. " pellets)" end |
| suffix = " (" .. hero_data.alt_fire.suffix .. ")"
| | if (burst > 1) then hero_suffix = hero_suffix .. " (" .. burst .. " bullets)" end |
| else
| |
| base = get_stat_value(hero_data, property)
| |
| level = (hero_data["LevelScaling"] and hero_data["LevelScaling"][property] or 0)
| |
| suffix = ""
| |
|
| |
| local shots = get_stat_value(hero_data, "BulletsPerShot")
| |
| if shots == 0 then shots = 1 end
| |
| local burst = get_stat_value(hero_data, "BulletsPerBurst")
| |
| if burst == 0 then burst = 1 end
| |
| local hit_once = get_stat_value(hero_data, "HitOnceAcrossAllBullets")
| |
|
| |
| if (property == "BulletDamage" and (shots * burst) > 1) then
| |
| if (not hit_once) then
| |
| base = base * shots * burst
| |
| level = level * shots * burst
| |
| end
| |
| if (shots > 1) then suffix = suffix .. " (" .. shots .. " pellets)" end
| |
| if (burst > 1) then suffix = suffix .. " (" .. burst .. " bullets)" end
| |
| end
| |
| end | | end |
| | | local hero_row = mw.html.create("tr") |
| -- Build the main summary row with the hero's info and milestone values.
| | hero_row |
| local summary_row = mw.html.create("tr") | | :tag("td"):wikitext(hero_icon .. hero_suffix):done() |
| | :tag("td"):wikitext(base):done() |
| | :tag("td"):wikitext("+" .. level):done() |
| | | |
| local hero_cell_content = hero_icon_wikitext .. suffix | | for i, v in ipairs(pi_souls_list) do |
|
| | local total = (tonumber(base) ~= nil and base or 0) + (tonumber(level) ~= nil and level or 0) * i |
| summary_row:tag("td"):wikitext(hero_cell_content):done()
| | hero_row |
| summary_row
| | :tag("td"):wikitext(total):done() |
| :tag("td"):wikitext(string.format("%.2f", base)):done() | |
| :tag("td"):wikitext("+" .. string.format("%.2f", level)):done()
| |
|
| |
| for _, count in ipairs(milestones) do
| |
| local total = base + level * count
| |
| summary_row:tag("td"):wikitext(string.format("%.2f", total)):done() | |
| end | | end |
|
| |
| -- Create the main container for the collapsible "Full Details" section.
| |
| local details_id = "boon-details-" .. hero_data.Name .. "-" .. property .. (is_alt_fire and "-alt" or "")
| |
| local collapsible_container = mw.html.create("div")
| |
| :addClass("mw-collapsible mw-collapsed")
| |
| :attr("id", details_id)
| |
| :attr("style", "display: flex; flex-direction: column; align-items: flex-end; gap: 5px;")
| |
| | | |
| -- Style the "Show All" toggle to look like a theme-aware button. | | create_table:node(hero_row) |
| collapsible_container:tag("div")
| |
| :addClass("mw-collapsible-toggle")
| |
| :attr("style", "display: inline-block; padding: 2px 8px; border: 1px solid currentColor; border-radius: 1px; text-decoration: none;")
| |
| :wikitext("Show All")
| |
|
| |
| -- Create the container for the expanded content.
| |
| local content_div = mw.html.create("div"):addClass("mw-collapsible-content")
| |
| | | |
| -- Generate the three-column layout by distributing rows into mini-tables. | | if (property == "BulletDamage" and alt_fires[hero["Name"]] ~= nil) then |
| local column1_rows, column2_rows, column3_rows = {}, {}, {}
| | local alt_fire = alt_fires[hero["Name"]] |
| local rows_per_column = math.ceil(max_boons / 3)
| | local alt_base = alt_fire["base"] |
| | local alt_level = alt_fire["level"] |
| | local alt_suffix = " (" .. alt_fire["suffix"] .. ")" |
| | local alt_fire_row = mw.html.create("tr") |
| | alt_fire_row |
| | :tag("td"):wikitext(hero_icon .. alt_suffix):done() |
| | :tag("td"):wikitext(alt_base):done() |
| | :tag("td"):wikitext("+" .. alt_level):done() |
| | | |
| for i = 1, max_boons do
| | for i, v in ipairs(pi_souls_list) do |
| local total = base + level * i
| | local total = alt_base + alt_level * i |
| local required_souls = pi_souls_list[i]
| | alt_fire_row:tag("td"):wikitext(total):done() |
| local souls_formatted = string.format("%.1fK", (required_souls + 400) / 1000)
| |
| local soul_display = Souls._render(souls_formatted)
| |
|
| |
| local row_html = string.format("<tr><td>%s</td><td style='text-align:right;'>%.2f</td></tr>", soul_display, total)
| |
|
| |
| if i <= rows_per_column then
| |
| table.insert(column1_rows, row_html)
| |
| elseif i <= rows_per_column * 2 then
| |
| table.insert(column2_rows, row_html)
| |
| else
| |
| table.insert(column3_rows, row_html)
| |
| end | | end |
| end
| |
|
| |
| local table_style = "border:none; background:transparent; margin:0 10px; min-width:150px;"
| |
| local table1 = "<table style='" .. table_style .. "'>" .. table.concat(column1_rows) .. "</table>"
| |
| local table2 = "<table style='" .. table_style .. "'>" .. table.concat(column2_rows) .. "</table>"
| |
| local table3 = "<table style='" .. table_style .. "'>" .. table.concat(column3_rows) .. "</table>"
| |
|
| |
| -- Use a flexbox container to display the mini-tables side-by-side.
| |
| local flex_container_style = "display:flex; justify-content:space-around; padding:10px 0;"
| |
| content_div:wikitext("<div style='" .. flex_container_style .. "'>" .. table1 .. table2 .. table3 .. "</div>")
| |
|
| |
| collapsible_container:node(content_div)
| |
| summary_row:tag("td"):node(collapsible_container)
| |
| create_table:node(summary_row)
| |
| end
| |
|
| |
| -- Iterate through each hero to generate their main row and an alt-fire row if applicable.
| |
| for _, hero in ipairs(filtered_heroes) do
| |
| addHeroRows(hero, false)
| |
|
| |
| -- Dynamically add alt-fire rows if the data exists
| |
| if (property == "BulletDamage" and hero.Weapon and hero.Weapon.AltFire) then
| |
| local alt_fire_data = hero.Weapon.AltFire
| |
| local alt_fire_base = alt_fire_data.BulletDamage or 0
| |
|
| |
| if alt_fire_base > 0 then
| |
| -- Get scaling: prefer alt-fire specific key, fall back to main weapon scaling
| |
| local alt_fire_level = (hero.LevelScaling and
| |
| (hero.LevelScaling.BulletDamageAltFire or hero.LevelScaling[property])) or 0
| |
|
| |
| -- Apply same pellet/burst logic as main weapon
| |
| local shots = alt_fire_data.BulletsPerShot or 1
| |
| local burst = alt_fire_data.BulletsPerBurst or 1
| |
| local hit_once = alt_fire_data.HitOnceAcrossAllBullets or false
| |
|
| |
| local suffix = "alt fire"
| |
|
| |
| if (shots * burst) > 1 and not hit_once then
| |
| alt_fire_base = alt_fire_base * shots * burst
| |
| alt_fire_level = alt_fire_level * shots * burst
| |
| if shots > 1 then suffix = suffix .. " " .. shots .. " pellets" end
| |
| if burst > 1 then suffix = suffix .. " " .. burst .. " bullets" end
| |
| end
| |
|
| |
| -- Add AOE indicator (without radius)
| |
| if alt_fire_data.ExplosionRadius then
| |
| suffix = suffix .. " AOE"
| |
| end
| |
|
| |
| local alt_fire_hero_data = {
| |
| Name = hero.Name,
| |
| alt_fire = {
| |
| base = alt_fire_base,
| |
| level = alt_fire_level,
| |
| suffix = suffix
| |
| }
| |
| }
| |
| addHeroRows(alt_fire_hero_data, true)
| |
| end
| |
| end
| |
| end
| |
|
| |
| -- If a title was provided, wrap the completed table in a theme-aware collapsible container.
| |
| if title and title ~= '' then
| |
| local wrapper = mw.html.create('table')
| |
| :addClass('mw-collapsible mw-collapsed wikitable')
| |
| :css('width', '100%')
| |
| :css('margin-bottom', '1em')
| |
|
| |
| local header_row_wrapper = wrapper:tag('tr')
| |
|
| |
| header_row_wrapper:tag('th')
| |
| :css('text-align', 'center')
| |
| :wikitext('<span style="font-size: larger; font-weight: bold;">' .. title .. '</span><span class="mw-collapsible-toggle mw-collapsible-toggle-right">[Show]</span>')
| |
|
| |
| local content_row_wrapper = wrapper:tag('tr')
| |
|
| |
| content_row_wrapper:tag('td')
| |
| :addClass('mw-collapsible-content')
| |
| :node(create_table)
| |
| | | |
| return tostring(wrapper)
| | create_table:node(alt_fire_row) |
| else
| |
| -- If no title is provided, return the table as before.
| |
| return tostring(create_table)
| |
| end
| |
| end
| |
| | |
| --[[
| |
| -- @function p.write_multishot_stats
| |
| -- @description Outputs a wikitext bulleted list of heroes with spreadshot or burst fire weapons.
| |
| -- @param {Frame} frame The MediaWiki frame object.
| |
| -- @return {string} A formatted wikitext string.
| |
| --]]
| |
| p.write_multishot_stats = function(frame)
| |
| -- Use the same smart sorting for this list.
| |
| local function hero_sort(h1, h2)
| |
| return get_sortable_name(h1.Name) < get_sortable_name(h2.Name)
| |
| end
| |
| local spreadshot_heroes = {}
| |
| local burst_heroes = {}
| |
| for _, hero in pairs(heroes_data) do
| |
| local bullets_per_shot = get_stat_value(hero, "BulletsPerShot")
| |
| local bullets_per_burst = get_stat_value(hero, "BulletsPerBurst")
| |
|
| |
| if (bullets_per_shot > 1 and not hero["IsDisabled"] and not hero["InDevelopment"]) then
| |
| table.insert(spreadshot_heroes, hero)
| |
| elseif (bullets_per_burst > 1 and not hero["IsDisabled"] and not hero["InDevelopment"]) then
| |
| table.insert(burst_heroes, hero)
| |
| end | | end |
| end | | end |
| | | |
| table.sort(spreadshot_heroes, hero_sort) | | return tostring(create_table) |
| table.sort(burst_heroes, hero_sort)
| |
|
| |
| local multishot_str = "Weapons with multiple projectiles (Spreadshot):\n"
| |
| for _, hero in ipairs(spreadshot_heroes) do
| |
| local base = math.floor(get_stat_value(hero, "BulletDamage") * 100) / 100
| |
| local level = math.floor(hero["LevelScaling"]["BulletDamage"] * 100) / 100
| |
| local shots = get_stat_value(hero, "BulletsPerShot")
| |
| local hit_once = get_stat_value(hero, "HitOnceAcrossAllBullets")
| |
| local hero_icon = Icon._render(hero["Name"])
| |
|
| |
| -- Checks for the special property and changes the output text accordingly.
| |
| local stat_str
| |
| if hit_once then
| |
| stat_str = "* " .. hero_icon .. ": " .. shots .. " pellets, "
| |
| .. base .. " total damage (hits only once), +" .. level .. " total\n"
| |
| else
| |
| stat_str = "* " .. hero_icon .. ": " .. shots .. " pellets, "
| |
| .. base .. "/pellet, +" .. level .. "/pellet\n"
| |
| end
| |
| multishot_str = multishot_str .. stat_str
| |
| end
| |
|
| |
| multishot_str = multishot_str .. "Weapons with burst fire (Burst Fire):\n"
| |
| for _, hero in ipairs(burst_heroes) do
| |
| local base = math.floor(get_stat_value(hero, "BulletDamage") * 100) / 100
| |
| local level = math.floor(hero["LevelScaling"]["BulletDamage"] * 100) / 100
| |
| local shots = get_stat_value(hero, "BulletsPerBurst")
| |
| local hero_icon = Icon._render(hero["Name"])
| |
| local stat_str = "* " .. hero_icon .. ": " .. shots .. " rounds, "
| |
| .. base .. "/bullet, +" .. level .. "/bullet\n"
| |
| multishot_str = multishot_str .. stat_str
| |
| end
| |
|
| |
| return multishot_str
| |
| end | | end |
|
| |
|
| return p | | return p |