From Dark and Darker Wiki

(Making loottable data ids unique per mode/map)
(Merged loot drop and drop rate tables into one. Using static data file strings at the moment. That still needs work.)
Line 1: Line 1:
--[[
Terminology:
- Dungeon: A specific area in the game where players can explore and fight monsters.  Synonymous with "queue".
- Dungeon grade: A numeric code representing a specific dungeon mode and map combination.
- Mode: The type of dungeon, such as PvE or LR or HR.
- Map: The specific area within the dungeon, such as Goblin Caves or Ice Cavern.
- Loot table: A combination of loot drop and drop rate data and roll count
- Loot drop table: A collection of items and the rarity therein that can be rolled for a loot table.
- Drop rate table: Contains the rates at which certain luck grades are rolled.
- Luck grade: A property which determines which items are rolled from within the loot drop table.
- Roll count: The number of times the loot table is rolled.
- Loot drop group: table of all the loot tables for each specific dungeon grade. It also contains loot drop tables and drop rate tables.
For an easier time following the code, the functions each have a docstring.
Use Lua by Sumneko on VSCode to view workspace function signatures and type hints.
#TODO
Build proper pipeline for going from {{PAGENAME}} to what is currently in p.main()
Currently spawner files are assumed rather than being determiend through module arguments
Need to add functions to hanfle Monster.json -> Spawner... -> LootDropGroup
Clean up the code in p.main().  It can use some compartmentalization.  Don't think it needs to be split into multiple functions, but at least better comments.
]]--
local p = {}
local p = {}
local utils = require("Module:Utilities")


local line_divider = '\n<div class="line" style="margin:5px 0px 5px 0px; background-image:linear-gradient(to right,#0A0A0A,#646464,#0A0A0A)"></div>'


local function translate_dungeon_code(code)
-- Convert one two strings to a numeric code. Note that the strings are concatenated, so the order matters
local codes = {
-- - @param a: string, optional, associated with either a mode or a map string
[40] = "HR 225+",
-- - @param b: string, optional, associated with either a mode or a map string
[30] = "HR 0-224",
-- - @return: string, number representing the dungeon mode or map or both
[20] = "LR",
-- - - e.g. ("") -> ""
[10] = "PvE",
-- - - e.g. ("Gibberish") -> ""
[1] = "Goblin Caves",
-- - - e.g. ("PvE") -> "10"
[11] = "Ice Cavern",
-- - - e.g. ("Goblin Caves") -> "01"
[12] = "Ice Abyss",
-- - - e.g. ("PvE","Goblin Caves") -> "1001"
[21] = "Ruins",
local function interpret_dungeon_strings(a,b)
[22] = "Crypts",
local modes = {
[23] = "Inferno"
["Default"] = "0",
["PvE"] = "10",
["LR 0-124"] = "20",
["LR 125+"] = "21",
["HR 0-224"] = "30",
["HR 225+"] = "40"
}
}
if codes[code] == nil then
local maps = {
return code
["Default"] = "0",
["Goblin Caves"] = "01",
["Ice Cavern"] = "11",
["Ice Abyss"] = "12",
["Ruins"] = "21",
["Crypts"] = "22",
["Inferno"] = "23"
}
 
-- Convert the input strings to their corresponding numeric codes, if they exist
a = modes[tostring(a)] or maps[tostring(a)]
b = modes[tostring(b)] or maps[tostring(b)]
 
-- If strings weren't matched, default to empty strings instead of nil
if a == nil then a = '' end
if b == nil then b = '' end
 
-- Default cases cannot be paired with other modes or maps, so return "0" if either is "Default"
if a == "Default" or b == "Default" then
return "0"
else
else
return codes[code]
return a..b
end
end
 
 
-- Convert a numeric dungeon grade to its corresponding mode and map strings
-- - @param grade: integer - represents a specific map of a dungeon mode
-- - @return: string, string - pair of strings corresponding to the mode and map
-- - - e.g. (0) -> "Default", "Default"
-- - - e.g. (4023) -> "HR 225+", "Inferno"
local function interpret_dungeon_grade(dungeon_grade)
local dungeon_grades = {
["0"] = {"Default","Default"},
 
["1001"] = {"PvE","Goblin Caves"},
["1011"] = {"PvE","Ice Cavern"},
["1012"] = {"PvE","Ice Abyss"},
["1021"] = {"PvE","Ruins"},
["1022"] = {"PvE","Crypts"},
["1023"] = {"PvE","Inferno"},
 
["2001"] = {"LR 0-124","Goblin Caves"},
["2011"] = {"LR 0-124","Ice Cavern"},
["2012"] = {"LR 0-124","Ice Abyss"},
["2021"] = {"LR 0-124","Ruins"},
["2022"] = {"LR 0-124","Crypts"},
["2023"] = {"LR 0-124","Inferno"},
 
["2101"] = {"LR 125+","Goblin Caves"},
["2111"] = {"LR 125+","Ice Cavern"},
["2112"] = {"LR 125+","Ice Abyss"},
["2121"] = {"LR 125+","Ruins"},
["2122"] = {"LR 125+","Crypts"},
["2123"] = {"LR 125+","Inferno"},
 
["3001"] = {"HR 0-224","Goblin Caves"},
["3011"] = {"HR 0-224","Ice Cavern"},
["3012"] = {"HR 0-224","Ice Abyss"},
["3021"] = {"HR 0-224","Ruins"},
["3022"] = {"HR 0-224","Crypts"},
["3023"] = {"HR 0-224","Inferno"},
 
["4001"] = {"HR 225+","Goblin Caves"},
["4011"] = {"HR 225+","Ice Cavern"},
["4012"] = {"HR 225+","Ice Abyss"},
["4021"] = {"HR 225+","Ruins"},
["4022"] = {"HR 225+","Crypts"},
["4023"] = {"HR 225+","Inferno"}
}
 
if dungeon_grade == nil or dungeon_grades[dungeon_grade] == nil then
return dungeon_grades["0"][1], dungeon_grades["0"][2]
end
end
return dungeon_grades[dungeon_grade][1], dungeon_grades[dungeon_grade][2]
end
end


local function create_div_tab(id, is_displayed)
 
-- Interpret a list of dungeon grades and return the corresponding tree
-- - @param dungeon_grades: table of numeric grades representing dungeon strings
-- - - Example: {1001, 2001, 4023}
-- - @return: table, a tree structure with modes and maps
-- - - Example: {["mode_order"] = {"PvE"}, ["PvE"] = {["map_order"] = {"Goblin Caves", "Ice Cavern"}, ["Goblin Caves"] = true, ["Ice Cavern"] = true}}
local function interpret_dungeon_grades(ordered_dungeon_grades)
if ordered_dungeon_grades == nil then return {} end
 
local tree = {}
tree.order = {}
 
for _,dungeon_grade in ipairs(ordered_dungeon_grades) do
local mode,map = interpret_dungeon_grade(dungeon_grade)
if tree[mode] == nil then
tree[mode] = {}
tree[mode].order = {}
table.insert(tree.order,mode)
end
if tree[mode][map] == nil then
tree[mode][map] = true
table.insert(tree[mode].order,map)
end
end
 
return tree
end
 
 
-- Create a div header with a class based on the id and an optional display argument
-- - @param id: string, represents the id of the div
-- - @param is_displayed: boolean, indicates whether the div should be displayed or hidden
-- - @return: string, the HTML div header
-- - - e.g. '\<div class="0-data" style="display:none;">' or '\<div class="0-data">
local function create_div_header(id_prefix, id, is_displayed)
local interpretation = interpret_dungeon_strings(id)
if interpretation == ''  then interpretation = id end
 
if is_displayed then
if is_displayed then
return '<div class="'.. id ..'-data">'
return '<div class="'..id_prefix..interpretation..'-data">'
else
else
return '<div class="'.. id ..'-data" style="display:none;">'
return '<div class="'..id_prefix..interpretation..'-data" style="display:none;">'
end
end
end
end


local function create_tab_toggle(tab_id, tab_number, content, selected)
 
if selected then
-- Create a tab toggle element with a specific id, number, and content
return '<div class="selected-tab tab-toggle tab" data-tabid="'..tab_id..'" data-tab="'..tab_number..'">'..content..'</div>'
-- - @param data_tabid: string, determines which class to toggle when the toggle is clicked
-- - @param data_tab: string, determines the toggle group, among which only one can be selected at a time
-- - @param content: string, the content of the tab toggle
-- - @param selected_tab: boolean, if true, the tab will be displayed as selected
-- - @return: string, the HTML tab toggle element
-- - - Example: \<div class="selected-tab tab-toggle tab" data-tabid="0" data-tab="PvE">PvE\</div>
local function create_tab_toggle(data_tab_id, data_tab, content, selected_tab)
if selected_tab then
return '<div class="selected-tab tab-toggle tab" data-tabid="'..data_tab_id..'" data-tab="'..data_tab..'">'..content..'</div>'
else
else
return '<div class="tab-toggle tab" data-tabid="'..tab_id..'" data-tab="'..tab_number..'">'..content..'</div>'
return '<div class="tab-toggle tab" data-tabid="'..data_tab_id..'" data-tab="'..data_tab..'">'..content..'</div>'
end
end
end
end


local function create_tabs_from_list(tab_list, tab_id)
 
-- Create tab toggles from a list of tabs.
-- - @param tab_list: table containing the content of the tabs.
-- - - e.g. {"PvE", "LR 0-124", "LR 125+", "HR 0-224", "HR 225+"}.
-- - - e.g. {"Goblin Caves", "Ice Cavern", "Ice Abyss", "Ruins", "Crypts", "Inferno"}.
-- - @param tab_id: string, the id of the tab toggle group.
-- - - e.g. "0" for the first level of tabs.
-- - - e.g. "1" for the second level, or 11 for second level in the first group.
local function create_tabtoggles_from_list(tab_list, tab_id)
if tab_list == nil then return '' end
if tab_list == nil then return '' end


local tab_toggles_html = ''
local wikitext = ''
for i,tab in ipairs(tab_list) do
for i,tab in ipairs(tab_list) do
if i == 1 then
local interpretation = interpret_dungeon_strings(tab)
tab_toggles_html = tab_toggles_html..create_tab_toggle(tab_id, tab_id..tab, translate_dungeon_code(tab), true)
if interpretation == ''  then interpretation = tab end
else
 
tab_toggles_html = tab_toggles_html..create_tab_toggle(tab_id, tab_id..tab, translate_dungeon_code(tab), false)
wikitext = wikitext..create_tab_toggle(tab_id, tab_id..interpretation, tab, i==1)
end
end
end
return tab_toggles_html
 
return wikitext
end
end


local function round(number, decimalPlaces)
 
return tonumber(string.format("%."..(decimalPlaces or 0).."f", number))
-- Round a number to a specified decimal place value
local function round(number, decimal_places)
return tonumber(string.format("%."..(decimal_places or 0).."f", number))
end
end


local function create_droprate_table(droprate_data, lootdrop_data, dungeongrade)
if droprate_data[dungeongrade] == nil then return '' end


-- Create a drop rate table from drop rate data and loot drop item counts
-- - @param drop_rate_data: table, contains drop rate data keyed by luck grade
-- - @param loot_drop_item_counts: table, contains item counts keyed by luck grade
-- - @return: string, the HTML drop rate table
local function create_droprate_table(drop_rate_data, loot_drop_item_counts)
-- Table header
-- Table header
local droprate_table = '<table cellspacing="0" class="loottable stripedtable sortable jquery-tablesorter mw-collapsible" style="width:100%"><caption>Drop rates</caption>'
local droprate_table = '<table cellspacing="0" class="loottable stripedtable sortable jquery-tablesorter mw-collapsible" style="width:100%"><caption>Drop rates&nbsp;</caption>'
..'<tr><th style="width:5%">Luck grade</th><th style="width:5%">Probability</th><th style="width:5%">Probability per item</th><th style="width:5%">Item count</th></tr>'
..'<tr><th style="width:5%">Luck grade</th><th style="width:5%">Probability</th><th style="width:5%">Probability per item</th><th style="width:5%">Item count</th></tr>'


-- Table body
-- Table body
for _, luckgrade in ipairs(droprate_data[dungeongrade]["luckgrade_order"]) do
for luckgrade = 1,8 do
local probability = droprate_data[dungeongrade]["luckgrade"][luckgrade]
local probability = drop_rate_data[luckgrade]
local item_count = lootdrop_data["luckgrade"][luckgrade]
local item_count = loot_drop_item_counts[luckgrade]
-- Table row
if probability ~= nil and item_count ~= nil then
droprate_table = droprate_table.."<tr class='cr"..luckgrade.."'><td><b>"..luckgrade.."</b></td><td><b>".. 100*probability.."%</b></td><td><b>"..round(100*probability/item_count,4).."%</b></td><td><b>"..item_count.."</b></td></tr>"
-- Table row
droprate_table = droprate_table
.."<tr class='cr"..luckgrade.."'>"
.."<td><b>"..luckgrade.."</b></td>"
.."<td><b>".. 100*probability.."%</b></td>"
.."<td><b>"..round(100*probability/item_count,4).."%</b></td>"
.."<td><b>"..item_count.."</b></td>"
.."</tr>"
end
end
end


return droprate_table..'</table>'
return droprate_table..'</table><br>'
end
end


function p.create_droprate_tables(frame)
local droprate_filename = "Data:Droprate Monsters Bosses.json"
local lootdrop_filename = "Data:Lootdrop GhostKing.json"


-- Load lootdrop_filename
-- Get the loot drop rate ids for a specific dungeon grade
local lootdrop_data = mw.loadJsonData(lootdrop_filename)
-- - @param loot_drop_groups: table, contains loot drop groups keyed by dungeon grade
-- Check if lootdrop_data is nil, if so return an error message
-- - @param dungeon_grade: integer, represents the dungeon grade
if lootdrop_data == nil then return "Lootdrop data file '"..lootdrop_filename.."' could not be found." end
-- - @return: table, contains loot drop rate ids keyed by numerical order
local function get_drop_rate_ids(loot_drop_groups, dungeon_grade)
-- Get the loot table ids for a specific dungeon code
local drop_rate_ids = {}
for _, loot_table in ipairs(loot_drop_groups[dungeon_grade]) do
table.insert(drop_rate_ids, loot_table.drop_rate_id)
end
return drop_rate_ids
end


-- Load droprate_filename
local droprate_data = mw.loadJsonData(droprate_filename)
-- Check if droprate_data is nil, if so return an error message
if droprate_data == nil then return "Droprate data file '"..droprate_filename.."' could not be found." end


-- The following nested code creates a series of divs with respective tab toggles for the different modes, maps, and lootdrops in the droprate data.
-- Get the luck grades for a specific drop rate table
-- The outermost div is for the entire table, and the innermost div is for each specific dungeongrade.
-- - @param drop_rate_table: table, contains drop rate data keyed by luck grade
-- - @return: table, contains luck grades keyed by their numeric value
local function get_luck_grades(drop_rate_table)
-- Get the luck grades for a specific drop rate table
local luck_grades = {}
for i = 1,8 do
if drop_rate_table[i] then
luck_grades[i]=true
end
end
return luck_grades
end


-- Create the tabs for the modes in mode_order
 
local resulting_table = create_tabs_from_list(droprate_data["mode_order"],"0")
-- Create a loot table from loot drop and drop rate data
for i,mode in ipairs(droprate_data["mode_order"]) do -- 4 nodes
-- - @param items: table, contains item data keyed by item name
-- Create a div and tabs for each specific mode
-- - - e.g. {["Unobtainium Ore"] = {{luck_grade = 9, rarity = 9, count = 2}, {luck_grade = 9, rarity = 9, count = 10}},  
resulting_table = resulting_table..create_div_tab("0"..mode, i == 1)..create_tabs_from_list(droprate_data["map_order"][mode],"1"..i)
--          ["Mithril Ore"] = {{luck_grade = 8, rarity = 8, count = 1}, {luck_grade = 8, rarity = 8, count = 8}}}
for j,map in ipairs(droprate_data["map_order"][mode]) do -- 1 to 6 nodes
-- - @param luck_grades: table, contains luck grades keyed by their numeric value
-- Create div and tabs for each specific map
-- - - e.g. {1=true, 2=true, 3=true, 4=true, 5=true, 6=true, 7=true, 8=true}
resulting_table = resulting_table..create_div_tab("1"..i..map, j == 1)..create_tabs_from_list(droprate_data["lootdrop_order"][mode][map],"2"..i..j)
-- - @return: string, the HTML loot table
for k,dungeongrade in ipairs(droprate_data["lootdrop_order"][mode][map]) do -- 1 to 6 nodes, example data only has 1 node atm
local function create_loot_table(items,luck_grades)
-- Create a div and table for each specific dungeongrade
local resulting_table = '<table cellspacing="0" class="loottable stripedtable sortable jquery-tablesorter mw-collapsible" style="width:100%">'
resulting_table = resulting_table..create_div_tab("2"..i..j..dungeongrade, k == 1)..create_droprate_table(droprate_data, lootdrop_data, dungeongrade)..'</div>'
..'<caption>Loot Table&nbsp;</caption>'
..'<tr><th style="width:5%">Name</th><th style="width:5%">Luck Grade</th><th style="width:5%">Rarity</th><th style="width:5%">Item Count</th></tr>'
 
-- Create body of table
-- For each item: name, luck grade, rarity, count
for item_name, item_data in pairs(items) do
-- Count the number of records for this item
local rowspan = 0
for _,item_record in ipairs(item_data) do
if luck_grades[item_record.luck_grade] then rowspan = rowspan + 1 end
end
-- Track how many rows have been created for this item; this is typically a subset of the entire record array
local record_row_index = 1
-- Iterate each record in the item data
for _, item_record in ipairs(item_data) do
if luck_grades[item_record.luck_grade] then
local luck_grade = item_record.luck_grade
local rarity_num = item_record.rarity
local item_count = item_record.count
 
local rarity_name = utils.rarity_num_to_name(rarity_num)
if rarity_name == nil then return "rarity_num of '" .. rarity_num .. "' was converted to a nil rarity_name." end
 
-- the first record's td must span all records rows
local rowspan_td_cell = ""
if record_row_index == 1 and rowspan > 1 then
rowspan_td_cell = "<td rowspan='" .. rowspan .. "'>"
elseif record_row_index == 1 then
rowspan_td_cell = "<td>" -- no rowspan needed for items with only one record
end
-- If this is the first record, create the td element containing the iconbox
if record_row_index == 1 then
rowspan_td_cell = rowspan_td_cell
.."<div class='iconbox'>"
.."<div class='rarity"..rarity_num.." rounded relative'>"
.."[[File:"..item_name..".png|x80px|link="..item_name.."]]"
.."</div>"
.."[["..item_name.."]]"
.."</div>"
.."</td>"
end
 
resulting_table = resulting_table
.."<tr>"
..rowspan_td_cell
.."<td class='cr"..luck_grade.."'><b>"..luck_grade.."</b></td>"
.."<td class='cr"..rarity_num.."'><b>"..rarity_name.."</b></td>"
.."<td>"..item_count.."</td>"
.."</tr>"
 
record_row_index = record_row_index + 1
end
end
resulting_table = resulting_table..'</div>'
end
end
resulting_table = resulting_table..'</div>'
end
end


return resulting_table
return resulting_table..'</table><br>'
end
 
 
-- Create tables of drop rates for a specific loot drop group.
function p.main(frame)
-- #TODO Make this dynamic
local monster_id = "Id_Monster_GhostKing"
local spawner_filename = "Data:Id_Spawner_New_Monster_GhostKing.json"
local loot_drop_group_filename = "Data:Id_LootDropGroup_GhostKing.json"
 
-- #TODO potentially move the code below to a separate function, it's kinda ugly
 
-- Store the loot drop group data in relevant tables
local loot_drop_group_data = mw.loadJsonData(loot_drop_group_filename)
if loot_drop_group_data == nil then return "LootDropGroup data file '"..loot_drop_group_filename.."' could not be found." end
local loot_drop_groups = loot_drop_group_data.loot_drop_groups -- contains data keyed by  dungeongrade, "4023", for example
local loot_drops_data = loot_drop_group_data.loot_drops -- contains data keyed by "ID_Lootdrop_Drop_GhostKing", for example
local drop_rates_data = loot_drop_group_data.drop_rates -- contains data keyed by "ID_Droprate_Monsters_Bosses", for example
 
local spawner_data = mw.loadJsonData(spawner_filename)
if spawner_data == nil then return "Spawner data file '"..spawner_filename.."' could not be found." end
local monster_spawner_order = spawner_data[monster_id].order -- contains ordered list of dungeon grades in which <monster_id> appears
local monster_spawner_data = spawner_data[monster_id].data -- contains loot drop group ids and odds of each group appearing
 
local dungeon_tree = interpret_dungeon_grades(monster_spawner_order)
 
local wikitext = create_tabtoggles_from_list(dungeon_tree.order,"0")
for i,mode in ipairs(dungeon_tree.order) do -- modes - 4 nodes
wikitext = wikitext
..create_div_header("0", mode, i == 1) -- tab toggle id, and boolean for if it should be displayed
..create_tabtoggles_from_list(dungeon_tree[mode].order,"1"..i)
 
for j,map in ipairs(dungeon_tree[mode].order) do -- maps - 1 to 6 nodes
local dungeon_grade = tonumber(interpret_dungeon_strings(mode, map)) -- e.g. "1001" for "PvE", "Goblin Caves"
 
wikitext = wikitext
..create_div_header("1"..i, map, j == 1) -- prefix "1", the tab toggle's depth, to create a unique identifier
..create_tabtoggles_from_list(get_drop_rate_ids(loot_drop_groups, dungeon_grade),"2"..i..j)
 
for k,loot_table in ipairs(loot_drop_groups[dungeon_grade]) do -- 1 to 6 nodes
wikitext = wikitext
..create_div_header("2"..i..j, loot_table.drop_rate_id, k == 1)
..'<span style="margin:20px 0px 20px 30px; font-size:24px">Rolls: '..loot_table.roll_count..'</span>'
..create_droprate_table(
drop_rates_data[loot_table.drop_rate_id],
loot_drops_data[loot_table.loot_drop_id].items_per_luck_grade)
..create_loot_table(
loot_drops_data[loot_table.loot_drop_id].items,
get_luck_grades(drop_rates_data[loot_table.drop_rate_id]))
..'</div>' -- close the droprate table div
end
 
wikitext = wikitext..'</div>' -- close the map div
end
 
wikitext = wikitext..'</div>' -- close the mode div
end
 
return wikitext
end
end


return p
return p

Revision as of 06:00, 25 May 2025

Script error: The function "loot_tables" does not exist.


--[[
Terminology:
- Dungeon: A specific area in the game where players can explore and fight monsters.  Synonymous with "queue".
- Dungeon grade: A numeric code representing a specific dungeon mode and map combination.
	- Mode: The type of dungeon, such as PvE or LR or HR.
	- Map: The specific area within the dungeon, such as Goblin Caves or Ice Cavern.
- Loot table: A combination of loot drop and drop rate data and roll count
	- Loot drop table: A collection of items and the rarity therein that can be rolled for a loot table.
	- Drop rate table: Contains the rates at which certain luck grades are rolled.
		- Luck grade: A property which determines which items are rolled from within the loot drop table.
	- Roll count: The number of times the loot table is rolled.
- Loot drop group: table of all the loot tables for each specific dungeon grade. It also contains loot drop tables and drop rate tables.

For an easier time following the code, the functions each have a docstring.
Use Lua by Sumneko on VSCode to view workspace function signatures and type hints.

#TODO
	Build proper pipeline for going from {{PAGENAME}} to what is currently in p.main()
		Currently spawner files are assumed rather than being determiend through module arguments
		Need to add functions to hanfle Monster.json -> Spawner... -> LootDropGroup
	Clean up the code in p.main().  It can use some compartmentalization.  Don't think it needs to be split into multiple functions, but at least better comments.
]]--


local p = {}
local utils = require("Module:Utilities")


-- Convert one two strings to a numeric code. Note that the strings are concatenated, so the order matters
-- - @param a: string, optional, associated with either a mode or a map string
-- - @param b: string, optional, associated with either a mode or a map string
-- - @return: string, number representing the dungeon mode or map or both
-- - - e.g. ("") -> ""
-- - - e.g. ("Gibberish") -> ""
-- - - e.g. ("PvE") -> "10"
-- - - e.g. ("Goblin Caves") -> "01"
-- - - e.g. ("PvE","Goblin Caves") -> "1001"
local function interpret_dungeon_strings(a,b)
	local modes = {
		["Default"] = "0",
		["PvE"] = "10",
		["LR 0-124"] = "20",
		["LR 125+"] = "21",
		["HR 0-224"] = "30",
		["HR 225+"] = "40"
	}
	local maps = {
		["Default"] = "0",
		["Goblin Caves"] = "01",
		["Ice Cavern"] = "11",
		["Ice Abyss"] = "12",
		["Ruins"] = "21",
		["Crypts"] = "22",
		["Inferno"] = "23"
	}

	-- Convert the input strings to their corresponding numeric codes, if they exist
	a = modes[tostring(a)] or maps[tostring(a)]
	b = modes[tostring(b)] or maps[tostring(b)]

	-- If strings weren't matched, default to empty strings instead of nil
	if a == nil then a = '' end
	if b == nil then b = '' end

	-- Default cases cannot be paired with other modes or maps, so return "0" if either is "Default"
	if a == "Default" or b == "Default" then
		return "0"
	else
		return a..b
	end
end


-- Convert a numeric dungeon grade to its corresponding mode and map strings
-- - @param grade: integer - represents a specific map of a dungeon mode
-- - @return: string, string - pair of strings corresponding to the mode and map
-- - - e.g. (0) -> "Default", "Default"
-- - - e.g. (4023) -> "HR 225+", "Inferno"
local function interpret_dungeon_grade(dungeon_grade)
	local dungeon_grades = {
		["0"] = {"Default","Default"},

		["1001"] = {"PvE","Goblin Caves"},
		["1011"] = {"PvE","Ice Cavern"},
		["1012"] = {"PvE","Ice Abyss"},
		["1021"] = {"PvE","Ruins"},
		["1022"] = {"PvE","Crypts"},
		["1023"] = {"PvE","Inferno"},

		["2001"] = {"LR 0-124","Goblin Caves"},
		["2011"] = {"LR 0-124","Ice Cavern"},
		["2012"] = {"LR 0-124","Ice Abyss"},
		["2021"] = {"LR 0-124","Ruins"},
		["2022"] = {"LR 0-124","Crypts"},
		["2023"] = {"LR 0-124","Inferno"},

		["2101"] = {"LR 125+","Goblin Caves"},
		["2111"] = {"LR 125+","Ice Cavern"},
		["2112"] = {"LR 125+","Ice Abyss"},
		["2121"] = {"LR 125+","Ruins"},
		["2122"] = {"LR 125+","Crypts"},
		["2123"] = {"LR 125+","Inferno"},

		["3001"] = {"HR 0-224","Goblin Caves"},
		["3011"] = {"HR 0-224","Ice Cavern"},
		["3012"] = {"HR 0-224","Ice Abyss"},
		["3021"] = {"HR 0-224","Ruins"},
		["3022"] = {"HR 0-224","Crypts"},
		["3023"] = {"HR 0-224","Inferno"},

		["4001"] = {"HR 225+","Goblin Caves"},
		["4011"] = {"HR 225+","Ice Cavern"},
		["4012"] = {"HR 225+","Ice Abyss"},
		["4021"] = {"HR 225+","Ruins"},
		["4022"] = {"HR 225+","Crypts"},
		["4023"] = {"HR 225+","Inferno"}
	}

	if dungeon_grade == nil or dungeon_grades[dungeon_grade] == nil then
		return dungeon_grades["0"][1], dungeon_grades["0"][2]
	end

	return dungeon_grades[dungeon_grade][1], dungeon_grades[dungeon_grade][2]
end


-- Interpret a list of dungeon grades and return the corresponding tree
-- - @param dungeon_grades: table of numeric grades representing dungeon strings
-- - - Example: {1001, 2001, 4023}
-- - @return: table, a tree structure with modes and maps
-- - - Example: {["mode_order"] = {"PvE"}, ["PvE"] = {["map_order"] = {"Goblin Caves", "Ice Cavern"}, ["Goblin Caves"] = true, ["Ice Cavern"] = true}}
local function interpret_dungeon_grades(ordered_dungeon_grades)
	if ordered_dungeon_grades == nil then return {} end

	local tree = {}
	tree.order = {}

	for _,dungeon_grade in ipairs(ordered_dungeon_grades) do
		local mode,map = interpret_dungeon_grade(dungeon_grade)
		
		if tree[mode] == nil then
			tree[mode] = {}
			tree[mode].order = {}
			table.insert(tree.order,mode)
		end
		if tree[mode][map] == nil then
			tree[mode][map] = true
			table.insert(tree[mode].order,map)
		end
	end

	return tree
end


-- Create a div header with a class based on the id and an optional display argument
-- - @param id: string, represents the id of the div
-- - @param is_displayed: boolean, indicates whether the div should be displayed or hidden
-- - @return: string, the HTML div header
-- - - e.g. '\<div class="0-data" style="display:none;">' or '\<div class="0-data">
local function create_div_header(id_prefix, id, is_displayed)
	local interpretation = interpret_dungeon_strings(id)
	if interpretation == ''  then interpretation = id end

	if is_displayed then
		return '<div class="'..id_prefix..interpretation..'-data">'
	else
		return '<div class="'..id_prefix..interpretation..'-data" style="display:none;">'
	end
end


-- Create a tab toggle element with a specific id, number, and content
-- - @param data_tabid: string, determines which class to toggle when the toggle is clicked
-- - @param data_tab: string, determines the toggle group, among which only one can be selected at a time
-- - @param content: string, the content of the tab toggle
-- - @param selected_tab: boolean, if true, the tab will be displayed as selected
-- - @return: string, the HTML tab toggle element
-- - - Example: \<div class="selected-tab tab-toggle tab" data-tabid="0" data-tab="PvE">PvE\</div>
local function create_tab_toggle(data_tab_id, data_tab, content, selected_tab)
	if selected_tab then
		return '<div class="selected-tab tab-toggle tab" data-tabid="'..data_tab_id..'" data-tab="'..data_tab..'">'..content..'</div>'
	else
		return '<div class="tab-toggle tab" data-tabid="'..data_tab_id..'" data-tab="'..data_tab..'">'..content..'</div>'
	end
end


-- Create tab toggles from a list of tabs.
-- - @param tab_list: table containing the content of the tabs.
-- - - e.g. {"PvE", "LR 0-124", "LR 125+", "HR 0-224", "HR 225+"}.
-- - - e.g. {"Goblin Caves", "Ice Cavern", "Ice Abyss", "Ruins", "Crypts", "Inferno"}.
-- - @param tab_id: string, the id of the tab toggle group.
-- - - e.g. "0" for the first level of tabs.
-- - - e.g. "1" for the second level, or 11 for second level in the first group.
local function create_tabtoggles_from_list(tab_list, tab_id)
	if tab_list == nil then return '' end

	local wikitext = ''
	for i,tab in ipairs(tab_list) do
		local interpretation = interpret_dungeon_strings(tab)
		if interpretation == ''  then interpretation = tab end

		wikitext = wikitext..create_tab_toggle(tab_id, tab_id..interpretation, tab, i==1)
	end

	return wikitext
end


-- Round a number to a specified decimal place value
local function round(number, decimal_places)
	return tonumber(string.format("%."..(decimal_places or 0).."f", number))
end


-- Create a drop rate table from drop rate data and loot drop item counts
-- - @param drop_rate_data: table, contains drop rate data keyed by luck grade
-- - @param loot_drop_item_counts: table, contains item counts keyed by luck grade
-- - @return: string, the HTML drop rate table
local function create_droprate_table(drop_rate_data, loot_drop_item_counts)
	-- Table header
	local droprate_table = '<table cellspacing="0" class="loottable stripedtable sortable jquery-tablesorter mw-collapsible" style="width:100%"><caption>Drop rates&nbsp;</caption>'
		..'<tr><th style="width:5%">Luck grade</th><th style="width:5%">Probability</th><th style="width:5%">Probability per item</th><th style="width:5%">Item count</th></tr>'

	-- Table body
	for luckgrade = 1,8 do
		local probability = drop_rate_data[luckgrade]
		local item_count = loot_drop_item_counts[luckgrade]
		if probability ~= nil and item_count ~= nil then
			-- Table row
			droprate_table = droprate_table
				.."<tr class='cr"..luckgrade.."'>"
					.."<td><b>"..luckgrade.."</b></td>"
					.."<td><b>".. 100*probability.."%</b></td>"
					.."<td><b>"..round(100*probability/item_count,4).."%</b></td>"
					.."<td><b>"..item_count.."</b></td>"
				.."</tr>"
		end
	end

	return droprate_table..'</table><br>'
end


-- Get the loot drop rate ids for a specific dungeon grade
-- - @param loot_drop_groups: table, contains loot drop groups keyed by dungeon grade
-- - @param dungeon_grade: integer, represents the dungeon grade
-- - @return: table, contains loot drop rate ids keyed by numerical order
local function get_drop_rate_ids(loot_drop_groups, dungeon_grade)
	-- Get the loot table ids for a specific dungeon code
	local drop_rate_ids = {}
	for _, loot_table in ipairs(loot_drop_groups[dungeon_grade]) do
		table.insert(drop_rate_ids, loot_table.drop_rate_id)
	end
	return drop_rate_ids
end


-- Get the luck grades for a specific drop rate table
-- - @param drop_rate_table: table, contains drop rate data keyed by luck grade
-- - @return: table, contains luck grades keyed by their numeric value
local function get_luck_grades(drop_rate_table)
	-- Get the luck grades for a specific drop rate table
	local luck_grades = {}
	for i = 1,8 do
		if drop_rate_table[i] then
			luck_grades[i]=true
		end
	end
	return luck_grades
end


-- Create a loot table from loot drop and drop rate data
-- - @param items: table, contains item data keyed by item name
-- - - e.g. {["Unobtainium Ore"] = {{luck_grade = 9, rarity = 9, count = 2}, {luck_grade = 9, rarity = 9, count = 10}}, 
--           ["Mithril Ore"] = {{luck_grade = 8, rarity = 8, count = 1}, {luck_grade = 8, rarity = 8, count = 8}}}
-- - @param luck_grades: table, contains luck grades keyed by their numeric value
-- - - e.g. {1=true, 2=true, 3=true, 4=true, 5=true, 6=true, 7=true, 8=true}
-- - @return: string, the HTML loot table
local function create_loot_table(items,luck_grades)
	local resulting_table = '<table cellspacing="0" class="loottable stripedtable sortable jquery-tablesorter mw-collapsible" style="width:100%">'
		..'<caption>Loot Table&nbsp;</caption>'
		..'<tr><th style="width:5%">Name</th><th style="width:5%">Luck Grade</th><th style="width:5%">Rarity</th><th style="width:5%">Item Count</th></tr>'

	-- Create body of table
	-- For each item: name, luck grade, rarity, count
	for item_name, item_data in pairs(items) do
		-- Count the number of records for this item
		local rowspan = 0
		for _,item_record in ipairs(item_data) do
			if luck_grades[item_record.luck_grade] then rowspan = rowspan + 1 end
		end
		-- Track how many rows have been created for this item; this is typically a subset of the entire record array
		local record_row_index = 1
		-- Iterate each record in the item data
		for _, item_record in ipairs(item_data) do
			if luck_grades[item_record.luck_grade] then
				local luck_grade = item_record.luck_grade
				local rarity_num = item_record.rarity
				local item_count = item_record.count

				local rarity_name = utils.rarity_num_to_name(rarity_num)
				if rarity_name == nil then return "rarity_num of '" .. rarity_num .. "' was converted to a nil rarity_name." end

				-- the first record's td must span all records rows
				local rowspan_td_cell = ""
				if record_row_index == 1 and rowspan > 1 then
					rowspan_td_cell = "<td rowspan='" .. rowspan .. "'>"
				elseif record_row_index == 1 then
					rowspan_td_cell = "<td>" -- no rowspan needed for items with only one record
				end
				-- If this is the first record, create the td element containing the iconbox
				if record_row_index == 1 then
					rowspan_td_cell = rowspan_td_cell
							.."<div class='iconbox'>"
								.."<div class='rarity"..rarity_num.." rounded relative'>"
									.."[[File:"..item_name..".png|x80px|link="..item_name.."]]"
								.."</div>"
								.."[["..item_name.."]]"
							.."</div>"
						.."</td>"
				end

				resulting_table = resulting_table
					.."<tr>"
						..rowspan_td_cell
						.."<td class='cr"..luck_grade.."'><b>"..luck_grade.."</b></td>"
						.."<td class='cr"..rarity_num.."'><b>"..rarity_name.."</b></td>"
						.."<td>"..item_count.."</td>"
					.."</tr>"

				record_row_index = record_row_index + 1
			end
		end
	end

	return resulting_table..'</table><br>'
end


-- Create tables of drop rates for a specific loot drop group.
function p.main(frame)
	-- #TODO Make this dynamic
	local monster_id = "Id_Monster_GhostKing"
	local spawner_filename = "Data:Id_Spawner_New_Monster_GhostKing.json"
	local loot_drop_group_filename = "Data:Id_LootDropGroup_GhostKing.json"

	-- #TODO potentially move the code below to a separate function, it's kinda ugly

	-- Store the loot drop group data in relevant tables
	local loot_drop_group_data = mw.loadJsonData(loot_drop_group_filename)
	if loot_drop_group_data == nil then return "LootDropGroup data file '"..loot_drop_group_filename.."' could not be found." end
	local loot_drop_groups = loot_drop_group_data.loot_drop_groups -- contains data keyed by  dungeongrade, "4023", for example
	local loot_drops_data = loot_drop_group_data.loot_drops -- contains data keyed by "ID_Lootdrop_Drop_GhostKing", for example
	local drop_rates_data = loot_drop_group_data.drop_rates -- contains data keyed by "ID_Droprate_Monsters_Bosses", for example

	local spawner_data = mw.loadJsonData(spawner_filename)
	if spawner_data == nil then return "Spawner data file '"..spawner_filename.."' could not be found." end
	local monster_spawner_order = spawner_data[monster_id].order -- contains ordered list of dungeon grades in which <monster_id> appears
	local monster_spawner_data = spawner_data[monster_id].data -- contains loot drop group ids and odds of each group appearing

	local dungeon_tree = interpret_dungeon_grades(monster_spawner_order)

	local wikitext = create_tabtoggles_from_list(dungeon_tree.order,"0")
	for i,mode in ipairs(dungeon_tree.order) do -- modes - 4 nodes
		wikitext = wikitext
			..create_div_header("0", mode, i == 1) -- tab toggle id, and boolean for if it should be displayed
			..create_tabtoggles_from_list(dungeon_tree[mode].order,"1"..i)

		for j,map in ipairs(dungeon_tree[mode].order) do -- maps - 1 to 6 nodes
			local dungeon_grade = tonumber(interpret_dungeon_strings(mode, map)) -- e.g. "1001" for "PvE", "Goblin Caves"

			wikitext = wikitext
				..create_div_header("1"..i, map, j == 1) -- prefix "1", the tab toggle's depth, to create a unique identifier
				..create_tabtoggles_from_list(get_drop_rate_ids(loot_drop_groups, dungeon_grade),"2"..i..j)

			for k,loot_table in ipairs(loot_drop_groups[dungeon_grade]) do -- 1 to 6 nodes
				wikitext = wikitext
					..create_div_header("2"..i..j, loot_table.drop_rate_id, k == 1)
						..'<span style="margin:20px 0px 20px 30px; font-size:24px">Rolls: '..loot_table.roll_count..'</span>'
						..create_droprate_table(
							drop_rates_data[loot_table.drop_rate_id],
							loot_drops_data[loot_table.loot_drop_id].items_per_luck_grade)
						..create_loot_table(
							loot_drops_data[loot_table.loot_drop_id].items,
							get_luck_grades(drop_rates_data[loot_table.drop_rate_id]))
					..'</div>' -- close the droprate table div
			end

			wikitext = wikitext..'</div>' -- close the map div
		end

		wikitext = wikitext..'</div>' -- close the mode div
	end

	return wikitext
end

return p