From Dark and Darker Wiki

(Fixed issue with tabid having overlapping codes)
m (Minor string syntax change. Replaced string library calls with the colon operator equivalent.)
 
(11 intermediate revisions by 2 users not shown)
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
Change code to handle props and loose loot as well.
]]--
local p = {}
local p = {}
local utils = require("Module:Utilities")
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 one two strings to a numeric code. Note that the returned 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)
-- 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)]


local line_divider = '\n<div class="line" style="margin:5px 0px 5px 0px; background-image:linear-gradient(to right,#0A0A0A,#646464,#0A0A0A)"></div>'
-- 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
 
 
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"}
}
-- Convert a numeric dungeon grade to its corresponding mode and map strings
-- - @param grade: string - integer corresponding to a specific mode and map
-- - @return: string, string - pair of strings corresponding to the mode and map
-- - - e.g. (0) -> "Default", "Default"; (4023) -> "HR 225+", "Inferno"
-- - - e.g. (5318008) -> "Default", "Default"; ("test") -> "Default", "Default"
local function interpret_dungeon_grade(dungeon_grade)
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 - ideally the dungeon grades are numericall ordered
-- - - Example: {1001, 2001, 4023}
-- - @return: table - a 2-level tree structure with order lists
-- - - Example: {["mode_order"] = {"PvE", "HR 225+""},
-- ["PvE"] = {["map_order"] = {"Goblin Caves", "Ice Cavern"}, ["Goblin Caves"] = true, ["Ice Cavern"] = true},
-- ["HR 225+"] = {["map_order"] = {"Ice Cavern", "Ice Abyss"}, ["Ice Cavern"] = true, ["Ice Abyss"] = true}}
local function get_dungeon_tree(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(tostring(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;">'
-- - - e.g. '\<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


local function create_div_tab(id, is_displayed)
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, title, 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..'" title="'..title..'">'..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..'" title="'..title..'">'..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 - list of content to be displayed for tab toggles.
-- - - e.g. {"PvE", "LR 0-124", "LR 125+", "HR 0-224", "HR 225+"}.
-- - @param tab_id: string - the id of the tab toggle group. Must be unique per tab toggle group.
-- - - e.g. "0" for the first level of tabs, or 11 for second level in the first group.
local function create_tab_toggles_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, tab, tab, true)
if interpretation == ''  then interpretation = tab end
else
 
tab_toggles_html = tab_toggles_html..create_tab_toggle(tab_id, tab, tab, tab, false)
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(("%."..(decimal_places or 0).."f"):format(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_drop_rate_wikitext(drop_rate_data, loot_drop_item_counts)
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>'
 
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
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
end
return tab_toggles_html
 
return droprate_table..'</table><br>'
end
end


local function dungeongrade_translate(dungeongrade)
local code = {"Default","Default"}


if string.find(dungeongrade, "40") then
-- Clean an ID string by removing the "I[dD]_..._" prefix and any trailing numbers
code[1] = "HR 225+"
local function clean_loot_table_id(loot_table_id)
elseif string.find(dungeongrade, "30") then
local cleaned_id = loot_table_id:gsub("I[dD]_%a*_","")
code[1] = "HR 0-224"
cleaned_id = cleaned_id:gsub("_%d+$", "")
elseif string.find(dungeongrade, "20") then
return cleaned_id
code[1] = "LR"
end
elseif string.find(dungeongrade, "10") then
 
code[1] = "PvE"
 
-- Get the loot table ids for a specific dungeon grade
-- - @param loot_tables: table - contains array of severl loot drop, drop rate, roll count pairs.
-- - @return: table - array of loot drop rate ids
local function get_loot_table_ids(loot_tables)
local drop_rate_ids = {}
for _, loot_table in ipairs(loot_tables) do
table.insert(drop_rate_ids, clean_loot_table_id(loot_table.drop_rate_id))
end
end
return drop_rate_ids
end


if string.find(dungeongrade, "001") then
 
code[2] = "Goblin Caves"
-- Get the luck grades for a specific drop rate table
elseif string.find(dungeongrade, "011") then
-- - @param drop_rate_table: table - contains drop rate data keyed by luck grade
code[2] = "Ice Caverns"
-- - @return: table - contains luck grades keyed by their numeric value
elseif string.find(dungeongrade, "012") then
local function get_luck_grades(drop_rate_table)
code[2] = "Ice Abyss"
local luck_grades = {}
elseif string.find(dungeongrade, "021") then
for i = 1,8 do
code[2] = "Ruins"
if drop_rate_table[i] then luck_grades[i]=true end
elseif string.find(dungeongrade, "022") then
code[2] = "Crypts"
elseif string.find(dungeongrade, "023") then
code[2] = "Inferno"
end
end
return luck_grades
end


return code
end


local function round(number, decimalPlaces)
-- Create a loot table from loot drop and drop rate data
return tonumber(string.format("%."..(decimalPlaces or 0).."f", number))
-- - @param items: table - contains item data keyed by item name
end
-- - - 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. {3=true, 5=true, 7=true, 8=true}
-- - @return: string - the HTML loot table
local function create_loot_drop_wikitext(items,luck_grades)
local wikitext = '<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>'
 
for item_name, item_data in pairs(items) do
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 -- this is incremented at the end of the loop's if block
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 function create_droprate_table(droprate_data, lootdrop_data, dungeongrade, resulting_table)
local rarity_name = utils.rarity_num_to_name(rarity_num)
-- Table header
if rarity_name == nil then return "rarity_num of '" .. rarity_num .. "' was converted to a nil rarity_name." end
resulting_table = resulting_table..'<table cellspacing="0" class="loottable stripedtable sortable jquery-tablesorter mw-collapsible" style="width:100%"><caption>Drop rates</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
local rowspan_td_cell = ""
for _, luckgrade in ipairs(droprate_data[dungeongrade]["luckgrade_order"]) do
-- if first, record's td must span all records rows and have an appropriate iconbox
local probability = droprate_data[dungeongrade]["luckgrade"][luckgrade]
if record_row_index == 1 then
if probability == nil then return "probability for '"..luckgrade.."' not found in droprate data for luckgrade dictionary." end
if rowspan > 1 then
rowspan_td_cell = "<td rowspan='" .. rowspan .. "'>"
else
rowspan_td_cell = "<td>" -- no rowspan needed for items with only one record
end
-- create the td element containing the iconbox
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.."|<b class=cr"..rarity_num..">"..item_name.."</b>]]"
.."</div>"
.."</td>"
end


local item_count = lootdrop_data["luckgrade"][luckgrade]
-- Add the row to the resulting table
if item_count == nil then return "item_count for '"..luckgrade.."' not found in lootdrop data." end
wikitext = wikitext
.."<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>"


-- Table row
record_row_index = record_row_index + 1
resulting_table = resulting_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
end


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


function p.create_droprate_tables(frame)
-- Create the wikitext containing loot tables specific to a monster/prop/loose loot
local droprate_filename = "Data:Droprate Monsters Bosses.json"
-- - @param LDG: table
local lootdrop_filename = "Data:Lootdrop GhostKing.json"
-- - @return: string - the wikitext containing tab toggles and loot tables
local function create_wikitext(LDG,dungeon_tree)
local wikitext = create_tab_toggles_from_list(dungeon_tree.order,"0") -- mode toggles
for i,mode in ipairs(dungeon_tree.order) do -- modes - 4 nodes
wikitext = wikitext
..create_div_header("0", mode, i == 1)
..create_tab_toggles_from_list(dungeon_tree[mode].order,"1"..i) -- map toggles


-- Load lootdrop_filename
for j,map in ipairs(dungeon_tree[mode].order) do -- maps - 1 to 6 nodes
local lootdrop_data = mw.loadJsonData(lootdrop_filename)
local dungeon_grade = tonumber(interpret_dungeon_strings(mode, map)) -- e.g. "1001" for "PvE", "Goblin Caves"
-- Check if lootdrop_data is nil, if so return an error message
if lootdrop_data == nil then return "Lootdrop data file '"..lootdrop_filename.."' could not be found." end


-- Load droprate_filename
wikitext = wikitext
local droprate_data = mw.loadJsonData(droprate_filename)
..create_div_header("1"..i, map, j == 1) -- prefix "1", the tab toggle's depth, to create a unique identifier
-- Check if droprate_data is nil, if so return an error message
..create_tab_toggles_from_list(get_loot_table_ids(LDG.loot_drop_groups.data[dungeon_grade]),"2"..i..j) -- loot table toggles
if droprate_data == nil then return "Droprate data file '"..droprate_filename.."' could not be found." end


local resulting_table = ''
for k,loot_table in ipairs(LDG.loot_drop_groups.data[dungeon_grade]) do -- 1 to 6 nodes
wikitext = wikitext
..create_div_header("2"..i..j, clean_loot_table_id(loot_table.drop_rate_id), k == 1)


-- The following nested code creates a series of divs with respective tab toggles for the different modes, maps, and lootdrops in the droprate data.
..'<span style="margin:20px 0px 20px 30px; font-size:24px">Rolls: '..loot_table.roll_count..'</span>'
-- The outermost div is for the entire table, and the innermost div is for each specific dungeongrade.
 
..create_drop_rate_wikitext(
LDG.drop_rates[loot_table.drop_rate_id],
LDG.loot_drops[loot_table.loot_drop_id].items_per_luck_grade)
 
..create_loot_drop_wikitext(
LDG.loot_drops[loot_table.loot_drop_id].items_data,
get_luck_grades(LDG.drop_rates[loot_table.drop_rate_id]))
 
..'</div>' -- close the content div
end
wikitext = wikitext..'</div>' -- close the map div
end
wikitext = wikitext..'</div>' -- close the mode div
end
 
return wikitext
end


-- Create the tabs for the modes in mode_order
resulting_table = resulting_table..create_tabs_from_list(droprate_data["mode_order"],0)
-- Create a div for each specific mode
for i,mode in ipairs(droprate_data["mode_order"]) do -- 4 nodes
resulting_table = resulting_table..create_div_tab(mode, i == 1)


-- Create the tabs for the maps in in map_order
-- Get the localized string and grade from a page name
resulting_table = resulting_table..create_tabs_from_list(droprate_data["map_order"][tonumber(mode)],tostring(1)..i)
local function get_localized_string_and_grade(page_name)
-- Create div for each specific map
-- Get the last word in the page name, which is expected to be the grade
for j,map in ipairs(droprate_data["map_order"][tonumber(mode)]) do -- 1 to 6 nodes
local grade = page_name:reverse():match("[^%s]+"):reverse()
resulting_table = resulting_table..create_div_tab(map, j == 1)
if grade == "Elite" or grade == "Nightmare" then
local localized_string,_ = page_name:gsub(" "..grade,"")
return localized_string, grade
else
return page_name, "Common"
end
end


-- Create the tabs for the lootdrop in lootdrop_order
resulting_table = resulting_table..create_tabs_from_list(droprate_data["lootdrop_order"][tonumber(mode)][tonumber(map)],tostring(3)..i..j)


resulting_table = resulting_table..'</div>'
-- #TODO make this function more generic, so it can be used for props and loose loot as well.
end
-- Currently only formatted for Data:Monster.json
resulting_table = resulting_table..'</div>'
local function get_object_data(page_name, id_type)
local data
if id_type == "Monster" then
data = mw.loadJsonData("Data:Monster.json")
elseif id_type == "Prop" then
data = mw.loadJsonData("Data:Prop.json")
elseif id_type == "Loose_loot" then
data = mw.loadJsonData("Data:Loose_loot.json")
else
return nil -- Invalid id_type
end
end


return resulting_table
local localized_string, grade = get_localized_string_and_grade(page_name)
local id = data.monster_localized_names[localized_string][grade]
 
return mw.loadJsonData("Data:"..data.monster_ids[id].loot_drop_group_id..".json"), get_dungeon_tree(data.monster_ids[id].dungeon_grade_order)
end
 
-- Create a loot table for a specific monster, prop, or loose loot
-- - @param frame: table
-- - - frame.args[1]: string - {{PAGENAME}} or localized string + grade
-- - - frame.args[2]: string - type of the object, either "Monster", "Prop", or "Loose_loot"
function p.loot_tables(frame)
-- -- frame.args[1] will always be the localized string of either a monster, prop, or loose loot.
local LDG, dungeon_tree = get_object_data(frame.args[1], frame.args[2])
if LDG == nil then return "LootDropGroup file could not be found." end
 
return create_wikitext(LDG, dungeon_tree)
end
end


return p
return p

Latest revision as of 01:28, 3 June 2025

HR 0-224
HR 225+
Inferno
Monsters_Bosses
Monster_GhostKing_Misc
QuestItemDefaultCommon
EventCurrency
Rolls: 3
Drop rates 
Luck gradeProbabilityProbability per itemItem count
655%0.9483%58
745%0.7759%58

Loot Table 
NameLuck GradeRarityItem Count
6Legend1
7Unique1
6Legend1
7Unique1
6Legend1
7Unique1
6Legend1
7Unique1
6Legend1
7Unique1
6Legend1
7Unique1
6Legend1
7Unique1
6Legend1
7Unique1
6Legend1
7Unique1
6Legend1
7Unique1
6Legend1
7Unique1
6Legend1
7Unique1
6Legend1
7Unique1
6Legend1
7Unique1
6Legend1
7Unique1
6Legend1
7Unique1
6Legend1
7Unique1
6Legend1
7Unique1
6Legend1
7Unique1
6Legend1
7Unique1
6Legend1
7Unique1
6Legend1
7Unique1
6Legend1
7Unique1
6Legend1
7Unique1
6Legend1
7Unique1
6Legend1
7Unique1
6Legend1
7Unique1
6Legend1
7Unique1
6Legend1
7Unique1
6Legend1
7Unique1
6Legend1
7Unique1
6Legend1
7Unique1
6Legend1
7Unique1
6Legend1
7Unique1
6Legend1
7Unique1
6Legend1
7Unique1
6Legend1
7Unique1
6Legend1
7Unique1
6Legend1
7Unique1
6Legend1
7Unique1
6Legend1
7Unique1
6Legend1
7Unique1
6Legend1
7Unique1
6Legend1
7Unique1
6Legend1
7Unique1
6Legend1
7Unique1
6Legend1
7Unique1
6Legend1
7Unique1
6Legend1
7Unique1
6Legend1
7Unique1
6Legend1
7Unique1
6Legend1
7Unique1
6Legend1
7Unique1
6Legend1
7Unique1
6Legend1
7Unique1
6Legend1
7Unique1
6Legend1
7Unique1
6Legend1
7Unique1


--[[
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
	Change code to handle props and loose loot as well.
]]--


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


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 one two strings to a numeric code. Note that the returned 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)
	-- 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


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"}
}
-- Convert a numeric dungeon grade to its corresponding mode and map strings
-- - @param grade: string - integer corresponding to a specific mode and map
-- - @return: string, string - pair of strings corresponding to the mode and map
-- - - e.g. (0) -> "Default", "Default"; (4023) -> "HR 225+", "Inferno"
-- - - e.g. (5318008) -> "Default", "Default"; ("test") -> "Default", "Default"
local function interpret_dungeon_grade(dungeon_grade)
	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 - ideally the dungeon grades are numericall ordered
-- - - Example: {1001, 2001, 4023}
-- - @return: table - a 2-level tree structure with order lists
-- - - Example: {["mode_order"] = {"PvE", "HR 225+""},
--				["PvE"] = {["map_order"] = {"Goblin Caves", "Ice Cavern"}, ["Goblin Caves"] = true, ["Ice Cavern"] = true},
--				["HR 225+"] = {["map_order"] = {"Ice Cavern", "Ice Abyss"}, ["Ice Cavern"] = true, ["Ice Abyss"] = true}}
local function get_dungeon_tree(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(tostring(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;">'
-- - - e.g. '\<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 - list of content to be displayed for tab toggles.
-- - - e.g. {"PvE", "LR 0-124", "LR 125+", "HR 0-224", "HR 225+"}.
-- - @param tab_id: string - the id of the tab toggle group. Must be unique per tab toggle group.
-- - - e.g. "0" for the first level of tabs, or 11 for second level in the first group.
local function create_tab_toggles_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(("%."..(decimal_places or 0).."f"):format(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_drop_rate_wikitext(drop_rate_data, loot_drop_item_counts)
	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>'

	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
			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


-- Clean an ID string by removing the "I[dD]_..._" prefix and any trailing numbers
local function clean_loot_table_id(loot_table_id)
	local cleaned_id = loot_table_id:gsub("I[dD]_%a*_","")
	cleaned_id = cleaned_id:gsub("_%d+$", "")
	return cleaned_id
end


-- Get the loot table ids for a specific dungeon grade
-- - @param loot_tables: table - contains array of severl loot drop, drop rate, roll count pairs.
-- - @return: table - array of loot drop rate ids
local function get_loot_table_ids(loot_tables)
	local drop_rate_ids = {}
	for _, loot_table in ipairs(loot_tables) do
		table.insert(drop_rate_ids, clean_loot_table_id(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)
	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. {3=true, 5=true, 7=true, 8=true}
-- - @return: string - the HTML loot table
local function create_loot_drop_wikitext(items,luck_grades)
	local wikitext = '<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>'

	for item_name, item_data in pairs(items) do
		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 -- this is incremented at the end of the loop's if block
		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

				local rowspan_td_cell = ""
				-- if first, record's td must span all records rows and have an appropriate iconbox
				if record_row_index == 1 then
					if rowspan > 1 then
						rowspan_td_cell = "<td rowspan='" .. rowspan .. "'>"
					else
						rowspan_td_cell = "<td>" -- no rowspan needed for items with only one record
					end
					-- create the td element containing the iconbox
					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.."|<b class=cr"..rarity_num..">"..item_name.."</b>]]"
							.."</div>"
						.."</td>"
				end

				-- Add the row to the resulting table
				wikitext = wikitext
					.."<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 wikitext..'</table><br>'
end

-- Create the wikitext containing loot tables specific to a monster/prop/loose loot
-- - @param LDG: table
-- - @return: string - the wikitext containing tab toggles and loot tables
local function create_wikitext(LDG,dungeon_tree)
	local wikitext = create_tab_toggles_from_list(dungeon_tree.order,"0") -- mode toggles
	for i,mode in ipairs(dungeon_tree.order) do -- modes - 4 nodes
		wikitext = wikitext
			..create_div_header("0", mode, i == 1)
			..create_tab_toggles_from_list(dungeon_tree[mode].order,"1"..i) -- map toggles

		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_tab_toggles_from_list(get_loot_table_ids(LDG.loot_drop_groups.data[dungeon_grade]),"2"..i..j) -- loot table toggles

			for k,loot_table in ipairs(LDG.loot_drop_groups.data[dungeon_grade]) do -- 1 to 6 nodes
				wikitext = wikitext
					..create_div_header("2"..i..j, clean_loot_table_id(loot_table.drop_rate_id), k == 1)

						..'<span style="margin:20px 0px 20px 30px; font-size:24px">Rolls: '..loot_table.roll_count..'</span>'

						..create_drop_rate_wikitext(
							LDG.drop_rates[loot_table.drop_rate_id],
							LDG.loot_drops[loot_table.loot_drop_id].items_per_luck_grade)

						..create_loot_drop_wikitext(
							LDG.loot_drops[loot_table.loot_drop_id].items_data,
							get_luck_grades(LDG.drop_rates[loot_table.drop_rate_id]))

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

	return wikitext
end


-- Get the localized string and grade from a page name
local function get_localized_string_and_grade(page_name)
	-- Get the last word in the page name, which is expected to be the grade
	local grade = page_name:reverse():match("[^%s]+"):reverse()
	if grade == "Elite" or grade == "Nightmare" then
		local localized_string,_ = page_name:gsub(" "..grade,"")
		return localized_string, grade
	else
		return page_name, "Common"
	end
end


-- #TODO make this function more generic, so it can be used for props and loose loot as well.
-- Currently only formatted for Data:Monster.json
local function get_object_data(page_name, id_type)
	local data
	if id_type == "Monster" then
		data = mw.loadJsonData("Data:Monster.json")
	elseif id_type == "Prop" then
		data = mw.loadJsonData("Data:Prop.json")
	elseif id_type == "Loose_loot" then
		data = mw.loadJsonData("Data:Loose_loot.json")
	else
		return nil -- Invalid id_type
	end

	local localized_string, grade = get_localized_string_and_grade(page_name)
	local id = data.monster_localized_names[localized_string][grade]

	return mw.loadJsonData("Data:"..data.monster_ids[id].loot_drop_group_id..".json"), get_dungeon_tree(data.monster_ids[id].dungeon_grade_order)
end

-- Create a loot table for a specific monster, prop, or loose loot
-- - @param frame: table
-- - - frame.args[1]: string - {{PAGENAME}} or localized string + grade
-- - - frame.args[2]: string - type of the object, either "Monster", "Prop", or "Loose_loot"
function p.loot_tables(frame)
	-- -- frame.args[1] will always be the localized string of either a monster, prop, or loose loot.
	local LDG, dungeon_tree = get_object_data(frame.args[1], frame.args[2])
	if LDG == nil then return "LootDropGroup file could not be found." end

	return create_wikitext(LDG, dungeon_tree)
end

return p