From Dark and Darker Wiki

(Some comment style changes.)
(Added section header)
 
(22 intermediate revisions by the same user not shown)
Line 16: Line 16:


#TODO
#TODO
Build proper pipeline for going from {{PAGENAME}} to what is currently in p.loot_table()
Change code to handle props and loose loot as well.
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.loot_table().  It can use some compartmentalization.  Don't think it needs to be split into multiple functions, but at least better comments.
]]--
]]--


Line 27: Line 24:




local modes = {
local MODES = {
["Default"] = "0",
["Default"] = "0",
["PvE"] = "10",
["PvE"] = "10",
["LR 0-124"] = "20",
["LR"] = "20",
["LR 125+"] = "21",
["HR"] = "30",
["HR 0-224"] = "30",
["Squire to Riches"] = "40",
["HR 225+"] = "40"
["???"] = "50"
}
}
local maps = {
local MAPS = {
["Default"] = "0",
["Default"] = "0",
["Goblin Caves"] = "01",
["Goblin Caves"] = "01",
["Ice Cavern"] = "11",
["Firedeep"] = "02",
["Ice Abyss"] = "12",
["Ice Cavern"] = "11",
["Ruins"] = "21",
["Ice Abyss"] = "12",
["Crypts"] = "22",
["Ruins"] = "21",
["Inferno"] = "23"
["Crypts"] = "22",
["Inferno"] = "23",
["Ship Graveyard"] ="31"
}
}
-- Convert one two strings to a numeric code. Note that the returned strings are concatenated, so the order matters.
-- Convert one two strings to a numeric code. Note that the returned strings are concatenated, so the order matters.
Line 55: Line 54:
local function interpret_dungeon_strings(a,b)
local function interpret_dungeon_strings(a,b)
-- Convert the input strings to their corresponding numeric codes, if they exist
-- Convert the input strings to their corresponding numeric codes, if they exist
a = modes[tostring(a)] or maps[tostring(a)]
a = MODES[tostring(a)] or MAPS[tostring(a)]
b = modes[tostring(b)] or maps[tostring(b)]
b = MODES[tostring(b)] or MAPS[tostring(b)]


-- If strings weren't matched, default to empty strings instead of nil
-- If strings weren't matched, default to empty strings instead of nil
Line 71: Line 70:




local dungeon_grades = {
local DUNGEON_GRADES = {
["0"] = {"Default","Default"},
["0"]   = {mode = "Default", map = "Default"},


["1001"] = {"PvE","Goblin Caves"},
["1001"] = {mode = "PvE", map = "Goblin Caves"},
["1011"] = {"PvE","Ice Cavern"},
["1002"] = {mode = "PvE", map = "Firedeep"},
["1012"] = {"PvE","Ice Abyss"},
["1011"] = {mode = "PvE", map = "Ice Cavern"},
["1021"] = {"PvE","Ruins"},
["1012"] = {mode = "PvE", map = "Ice Abyss"},
["1022"] = {"PvE","Crypts"},
["1021"] = {mode = "PvE", map = "Ruins"},
["1023"] = {"PvE","Inferno"},
["1022"] = {mode = "PvE", map = "Crypts"},
["1023"] = {mode = "PvE", map = "Inferno"},
["1031"] = {mode = "PvE", map = "Ship Graveyard"},


["2001"] = {"LR 0-124","Goblin Caves"},
["2001"] = {mode = "LR", map = "Goblin Caves"},
["2011"] = {"LR 0-124","Ice Cavern"},
["2002"] = {mode = "LR", map = "Firedeep"},
["2012"] = {"LR 0-124","Ice Abyss"},
["2011"] = {mode = "LR", map = "Ice Cavern"},
["2021"] = {"LR 0-124","Ruins"},
["2012"] = {mode = "LR", map = "Ice Abyss"},
["2022"] = {"LR 0-124","Crypts"},
["2021"] = {mode = "LR", map = "Ruins"},
["2023"] = {"LR 0-124","Inferno"},
["2022"] = {mode = "LR", map = "Crypts"},
["2023"] = {mode = "LR", map = "Inferno"},
["2031"] = {mode = "LR", map = "Ship Graveyard"},


["2101"] = {"LR 125+","Goblin Caves"},
["3001"] = {mode = "HR", map = "Goblin Caves"},
["2111"] = {"LR 125+","Ice Cavern"},
["3002"] = {mode = "HR", map = "Firedeep"},
["2112"] = {"LR 125+","Ice Abyss"},
["3011"] = {mode = "HR", map = "Ice Cavern"},
["2121"] = {"LR 125+","Ruins"},
["3012"] = {mode = "HR", map = "Ice Abyss"},
["2122"] = {"LR 125+","Crypts"},
["3021"] = {mode = "HR", map = "Ruins"},
["2123"] = {"LR 125+","Inferno"},
["3022"] = {mode = "HR", map = "Crypts"},
["3023"] = {mode = "HR", map = "Inferno"},
["3031"] = {mode = "HR", map = "Ship Graveyard"},


["3001"] = {"HR 0-224","Goblin Caves"},
["4001"] = {mode = "StR", map = "Goblin Caves"},
["3011"] = {"HR 0-224","Ice Cavern"},
["4002"] = {mode = "StR", map = "Firedeep"},
["3012"] = {"HR 0-224","Ice Abyss"},
["4011"] = {mode = "StR", map = "Ice Cavern"},
["3021"] = {"HR 0-224","Ruins"},
["4012"] = {mode = "StR", map = "Ice Abyss"},
["3022"] = {"HR 0-224","Crypts"},
["4021"] = {mode = "StR", map = "Ruins"},
["3023"] = {"HR 0-224","Inferno"},
["4022"] = {mode = "StR", map = "Crypts"},
["4023"] = {mode = "StR", map = "Inferno"},
["4031"] = {mode = "StR", map = "Ship Graveyard"},


["4001"] = {"HR 225+","Goblin Caves"},
-- ["5001"] = {mode = "???", map = "Goblin Caves"},
["4011"] = {"HR 225+","Ice Cavern"},
-- ["5002"] = {mode = "???", map = "Firedeep"},
["4012"] = {"HR 225+","Ice Abyss"},
-- ["5011"] = {mode = "???", map = "Ice Cavern"},
["4021"] = {"HR 225+","Ruins"},
-- ["5012"] = {mode = "???", map = "Ice Abyss"},
["4022"] = {"HR 225+","Crypts"},
-- ["5021"] = {mode = "???", map = "Ruins"},
["4023"] = {"HR 225+","Inferno"}
-- ["5022"] = {mode = "???", map = "Crypts"},
-- ["5023"] = {mode = "???", map = "Inferno"},
-- ["5031"] = {mode = "???", map = "Ship Graveyardo"}
}
}
-- Convert a numeric dungeon grade to its corresponding mode and map strings
-- Convert a numeric dungeon grade to its corresponding mode and map strings
-- - @param grade: string - integer corresponding to a specific mode and map
-- - @param grade: string - integer corresponding to a specific mode and map
-- - @return: string, string - pair of strings corresponding to the mode and map
-- - @return: string, string - pair of strings corresponding to the mode and map
-- - - e.g. (0) -> "Default", "Default"
-- - - e.g. (0) -> "Default", "Default"; (4023) -> "HR 225+", "Inferno"
-- - - e.g. (4023) -> "HR 225+", "Inferno"
-- - - e.g. (5318008) -> "Default", "Default"; ("test") -> "Default", "Default"
local function interpret_dungeon_grade(dungeon_grade)
local function interpret_dungeon_grade(dungeon_grade)
if dungeon_grade == nil or dungeon_grades[dungeon_grade] == nil then
if dungeon_grade == nil or DUNGEON_GRADES[dungeon_grade] == nil then
return dungeon_grades["0"][1], dungeon_grades["0"][2]
return "Improper dungeon grade code",""
end
end


return dungeon_grades[dungeon_grade][1], dungeon_grades[dungeon_grade][2]
return DUNGEON_GRADES[dungeon_grade].mode, DUNGEON_GRADES[dungeon_grade].map
end
end


-- Interpret a list of dungeon grades and return the corresponding tree
-- Interpret a list of dungeon grades and return the corresponding tree
Line 130: Line 138:
-- ["PvE"] = {["map_order"] = {"Goblin Caves", "Ice Cavern"}, ["Goblin Caves"] = true, ["Ice Cavern"] = true},
-- ["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}}
-- ["HR 225+"] = {["map_order"] = {"Ice Cavern", "Ice Abyss"}, ["Ice Cavern"] = true, ["Ice Abyss"] = true}}
local function interpret_dungeon_grades(ordered_dungeon_grades)
local function get_dungeon_tree(ordered_dungeon_grades)
if ordered_dungeon_grades == nil then return {} end
if ordered_dungeon_grades == nil then return {} end


Line 137: Line 145:


for _,dungeon_grade in ipairs(ordered_dungeon_grades) do
for _,dungeon_grade in ipairs(ordered_dungeon_grades) do
local mode,map = interpret_dungeon_grade(dungeon_grade)
local mode,map = interpret_dungeon_grade(tostring(dungeon_grade))
-- if dungeon grade cannot be matched, skip it
if tree[mode] == nil then
if map ~= "" then
tree[mode] = {}
if tree[mode] == nil then
tree[mode].order = {}
tree[mode] = {}
table.insert(tree.order,mode)
tree[mode].order = {}
end
table.insert(tree.order,mode)
if tree[mode][map] == nil then
end
tree[mode][map] = true
if tree[mode][map] == nil then
table.insert(tree[mode].order,map)
tree[mode][map] = true
table.insert(tree[mode].order,map)
end
end
end
end
end
Line 158: Line 168:
-- - @param is_displayed: boolean - indicates whether the div should be displayed or hidden
-- - @param is_displayed: boolean - indicates whether the div should be displayed or hidden
-- - @return: string - the HTML div header
-- - @return: string - the HTML div header
-- - - e.g. '\<div class="0-data" style="display:none;">' or '\<div class="0-data">
-- - - e.g. '\<div class="0-data" style="display:none;">'
local function create_div_header(id_prefix, id, is_displayed)
-- - - e.g. '\<div class="0-data">
local function create_div_header(id_prefix, id, is_displayed, disambiguation)
local interpretation = interpret_dungeon_strings(id)
local interpretation = interpret_dungeon_strings(id)
if interpretation == ''  then interpretation = id end
if interpretation == ''  then interpretation = id end
local wt = '<div class="'..id_prefix..interpretation
if disambiguation then
wt = wt..disambiguation
end


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


Line 192: Line 211:
-- - @param tab_id: string - the id of the tab toggle group. Must be unique per tab toggle group.
-- - @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.
-- - - e.g. "0" for the first level of tabs, or 11 for second level in the first group.
local function create_tabtoggles_from_list(tab_list, tab_id)
local function create_tab_toggles_from_list(tab_list, tab_id, is_differentiated)
if tab_list == nil then return '' end
if tab_list == nil then return '' end


Line 200: Line 219:
if interpretation == ''  then interpretation = tab end
if interpretation == ''  then interpretation = tab end


wikitext = wikitext..create_tab_toggle(tab_id, tab_id..interpretation, tab, i==1)
if is_differentiated then
wikitext = wikitext..create_tab_toggle(tab_id, tab_id..interpretation..tostring(i), tab, i==1)
else
wikitext = wikitext..create_tab_toggle(tab_id, tab_id..interpretation, tab, i==1)
end
end
end


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


Line 217: Line 240:
-- - @param loot_drop_item_counts: table - contains item counts keyed by luck grade
-- - @param loot_drop_item_counts: table - contains item counts keyed by luck grade
-- - @return: string - the HTML drop rate table
-- - @return: string - the HTML drop rate table
local function create_droprate_table(drop_rate_data, loot_drop_item_counts)
local function create_drop_rate_wikitext(drop_rate_data, loot_drop_item_counts)
-- Table header
local droprate_table = '<table cellspacing="0" class="loottable stripedtable sortable jquery-tablesorter mw-collapsible" style="min-width:30%"><caption>Drop rates&nbsp;</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>Luck grade</th><th>Probability</th><th>Probability per item</th><th>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
for luckgrade = 1,8 do
for luckgrade = 1,8 do
local probability = drop_rate_data[luckgrade]
local probability = drop_rate_data[luckgrade]
local item_count = loot_drop_item_counts[luckgrade]
local item_count = loot_drop_item_counts[luckgrade]
if probability ~= nil and item_count ~= nil then
if probability ~= nil and item_count ~= nil then
-- Table row
droprate_table = droprate_table
droprate_table = droprate_table
.."<tr class='cr"..luckgrade.."'>"
.."<tr class='cr"..luckgrade.."'>"
Line 242: Line 262:




-- Get the loot drop rate ids for a specific dungeon grade
-- Clean an ID string by removing the "I[dD]_..._" prefix and any trailing numbers
-- - @param loot_drop_groups: table - contains loot drop groups keyed by dungeon grade
local function clean_loot_table_id(loot_table_id)
-- - @param dungeon_grade: integer - represents the dungeon grade
local cleaned_id = loot_table_id:gsub("I[dD]_%a*_","")
-- - @return: table - contains loot drop rate ids keyed by numerical order
cleaned_id = cleaned_id:gsub("_%d+$", "")
local function get_drop_rate_ids(loot_drop_groups, dungeon_grade)
return cleaned_id
-- Get the loot table ids for a specific dungeon code
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 = {}
local drop_rate_ids = {}
for _, loot_table in ipairs(loot_drop_groups[dungeon_grade]) do
for _, loot_table in ipairs(loot_tables) do
table.insert(drop_rate_ids, loot_table.drop_rate_id)
table.insert(drop_rate_ids, clean_loot_table_id(loot_table.drop_rate_id))
end
end
return drop_rate_ids
return drop_rate_ids
Line 260: Line 286:
-- - @return: table - contains luck grades keyed by their numeric value
-- - @return: table - contains luck grades keyed by their numeric value
local function get_luck_grades(drop_rate_table)
local function get_luck_grades(drop_rate_table)
-- Get the luck grades for a specific drop rate table
local luck_grades = {}
local luck_grades = {}
for i = 1,8 do
for i = 1,8 do
if drop_rate_table[i] then
if drop_rate_table[i] then luck_grades[i]=true end
luck_grades[i]=true
end
end
end
return luck_grades
return luck_grades
Line 276: Line 299:
--          ["Mithril Ore"] = {{luck_grade = 8, rarity = 8, count = 1}, {luck_grade = 8, rarity = 8, count = 8}}}
--          ["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
-- - @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}
-- - - e.g. {3=true, 5=true, 7=true, 8=true}
-- - @return: string - the HTML loot table
-- - @return: string - the HTML loot table
local function create_loot_table(items,luck_grades)
local function create_loot_drop_wikitext(items,luck_grades)
local resulting_table = '<table cellspacing="0" class="loottable stripedtable sortable jquery-tablesorter mw-collapsible" style="width:100%">'
local wikitext = '<table cellspacing="0" class="loottable stripedtable sortable jquery-tablesorter mw-collapsible" style="min-width:30%"><caption>Loot Table&nbsp;</caption>'
..'<caption>Loot Table&nbsp;</caption>'
..'<tr><th>Name</th><th>Luck Grade</th><th>Rarity</th><th>Item Count</th></tr>'
..'<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
for item_name, item_data in pairs(items) do
-- Count the number of records for this item
local rowspan = 0
local rowspan = 0
for _,item_record in ipairs(item_data) do
for _,item_record in ipairs(item_data) do
if luck_grades[item_record.luck_grade] then rowspan = rowspan + 1 end
if luck_grades[item_record.luck_grade] then rowspan = rowspan + 1 end
end
end
-- Track how many rows have been created for this item; this is typically a subset of the entire record array
-- 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
local record_row_index = 1 -- this is incremented at the end of the loop's if block
-- Iterate each record in the item data
for _, item_record in ipairs(item_data) do
for _, item_record in ipairs(item_data) do
if luck_grades[item_record.luck_grade] then
if luck_grades[item_record.luck_grade] then
Line 303: Line 322:
if rarity_name == nil then return "rarity_num of '" .. rarity_num .. "' was converted to a nil rarity_name." end
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 = ""
local rowspan_td_cell = ""
if record_row_index == 1 and rowspan > 1 then
-- if first, record's td must span all records rows and have an appropriate iconbox
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
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
rowspan_td_cell = rowspan_td_cell
.."<div class='iconbox'>"
.."<div class='iconbox'>"
Line 322: Line 341:
end
end


resulting_table = resulting_table
-- Add the row to the resulting table
wikitext = wikitext
.."<tr>"
.."<tr>"
..rowspan_td_cell
..rowspan_td_cell
Line 335: Line 355:
end
end


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


 
-- Create the wikitext containing loot tables specific to a monster/prop/loose loot
-- Create loot tables for the loot drop group data relevant to the monster/prop.
local function create_wikitext(LDG,dungeon_tree,grade)
function p.loot_table(frame)
local wikitext = create_tab_toggles_from_list(dungeon_tree.order,grade.."0") -- mode toggles
-- #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
for i,mode in ipairs(dungeon_tree.order) do -- modes - 4 nodes
wikitext = wikitext
wikitext = wikitext
..create_div_header("0", mode, i == 1) -- tab toggle id, and boolean for if it should be displayed
..create_div_header(grade.."0", mode, i == 1)
..create_tabtoggles_from_list(dungeon_tree[mode].order,"1"..i)
..create_tab_toggles_from_list(dungeon_tree[mode].order,grade.."1"..i) -- map toggles


for j,map in ipairs(dungeon_tree[mode].order) do -- maps - 1 to 6 nodes
for j,map in ipairs(dungeon_tree[mode].order) do -- maps - 1 to 6 nodes
if not LDG.loot_drop_groups.data[0] and not LDG.loot_drop_groups.data[1] then return "No items drop from this source." end
local dungeon_grade = tonumber(interpret_dungeon_strings(mode, map)) -- e.g. "1001" for "PvE", "Goblin Caves"
local dungeon_grade = tonumber(interpret_dungeon_strings(mode, map)) -- e.g. "1001" for "PvE", "Goblin Caves"
-- json decoder forces 0 -> 1 for arrays, so we must rely on a manual fix
if dungeon_grade == 0 and LDG.loot_drop_groups.data[dungeon_grade] == nil then dungeon_grade = 1 end


wikitext = wikitext
wikitext = wikitext
..create_div_header("1"..i, map, j == 1) -- prefix "1", the tab toggle's depth, to create a unique identifier
..create_div_header(grade.."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)
..create_tab_toggles_from_list(get_loot_table_ids(LDG.loot_drop_groups.data[dungeon_grade]),grade.."2"..i..j,true) -- loot table toggles


for k,loot_table in ipairs(loot_drop_groups[dungeon_grade]) do -- 1 to 6 nodes
for k,loot_table in ipairs(LDG.loot_drop_groups.data[dungeon_grade]) do -- 1 to 6 nodes
wikitext = wikitext
wikitext = wikitext
..create_div_header("2"..i..j, loot_table.drop_rate_id, k == 1)
..create_div_header(grade.."2"..i..j, clean_loot_table_id(loot_table.drop_rate_id), k == 1,k) -- prefix "2", the tab toggle's depth, to create a unique identifier
..'<span style="margin:20px 0px 20px 30px; font-size:24px">Rolls: '..loot_table.roll_count..'</span>'
..'<span style="margin:20px 0px 20px 30px; font-size:24px">Rolls: '..loot_table.roll_count..'</span>'
..create_droprate_table(
..create_drop_rate_wikitext(
drop_rates_data[loot_table.drop_rate_id],
LDG.drop_rates[loot_table.drop_rate_id],
loot_drops_data[loot_table.loot_drop_id].items_per_luck_grade)
LDG.loot_drops[loot_table.loot_drop_id].items_per_luck_grade)
..create_loot_table(
..create_loot_drop_wikitext(
loot_drops_data[loot_table.loot_drop_id].items,
LDG.loot_drops[loot_table.loot_drop_id].items_data,
get_luck_grades(drop_rates_data[loot_table.drop_rate_id]))
get_luck_grades(LDG.drop_rates[loot_table.drop_rate_id]))
..'</div>' -- close the droprate table div
 
..'</div>' -- close the content div
end
end
wikitext = wikitext..'</div>' -- close the map div
wikitext = wikitext..'</div>' -- close the map div
end
end
wikitext = wikitext..'</div>' -- close the mode div
wikitext = wikitext..'</div>' -- close the mode div
end
end


return wikitext
return wikitext
end
local GRADE_ORDER = {"Common","Elite","Nightmare"}
-- Some monsters don't have all three grades, so we filter them here
local function get_grades(ML,page_name)
local grades = {}
for _,grade in ipairs(GRADE_ORDER) do
if ML.localized_name[page_name][grade] ~= nil then
grades[#grades+1] = grade
end
end
return grades
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)
local wt = {}
local ML = mw.loadJsonData("Loot:Monster.json")
local pagename = frame.args.name
local LDG_id = ""
local LDG = {}
wt[#wt+1] = "<h2 style='width:100%'>Loot Tables</h2>"
local grades = get_grades(ML,pagename)
wt[#wt+1] = create_tab_toggles_from_list(grades,"Grade")
for i, grade in ipairs(grades) do
local id = ML.localized_name[pagename][grade]
if LDG_id ~= "Loot:"..ML.id[id].loot..".json" then -- only load as needed
LDG_id = "Loot:"..ML.id[id].loot..".json"
LDG = mw.loadJsonData(LDG_id)
end
if LDG == nil then return "LootDropGroup file not found." end
wt[#wt+1] = create_div_header("Grade", grade, i == 1)
wt[#wt+1] = create_wikitext(LDG,get_dungeon_tree(ML.id[id].dungeon_grade_order),grade)
wt[#wt+1] = "</div>"
end
return table.concat(wt)
end
end


return p
return p
--[=[
mw.log(p.loot_tables({args={name="Mermaid",category="Monster"}}))
mw.log(p.loot_tables({args={name="Ghost King",category="Monster"}}))
]=]

Latest revision as of 08:38, 7 May 2026

Overview

Functions for making loot table. Data comes from Loot:Monster.json, Loot:Prop.json, Loot:Loose Loot.json, and the Loot: namespace.

Functions

loot_tables

Creates a table of drops

  • name - Name of the object
  • category - Monster, Prop, Loose Loot

{{#invoke:Droprate|loot_tables|name=Lich|category=Monster}}

Loot Tables

Common
Elite
Default
PvE
LR
Default
Monsters_Bosses
Key_High
Key_Med
QuestItemDefaultCommon
QuestItemDefaultCommon
EventCurrency
Rolls: 3
Drop rates 
Luck gradeProbabilityProbability per itemItem count
420%0.5%40
580%2%40

Loot Table 
NameLuck GradeRarityItem Count
4Rare1
5Epic1
4Rare1
5Epic1
4Rare1
5Epic1
4Rare1
5Epic1
4Rare1
5Epic1
4Rare1
5Epic1
4Rare1
5Epic1
4Rare1
5Epic1
4Rare1
5Epic1
4Rare1
5Epic1
4Rare1
5Epic1
4Rare1
5Epic1
4Rare1
5Epic1
4Rare1
5Epic1
4Rare1
5Epic1
4Rare1
5Epic1
4Rare1
5Epic1
4Rare1
5Epic1
4Rare1
5Epic1
4Rare1
5Epic1
4Rare1
5Epic1
4Rare1
5Epic1
4Rare1
5Epic1
4Rare1
5Epic1
4Rare1
5Epic1
4Rare1
5Epic1
4Rare1
5Epic1
4Rare1
5Epic1
4Rare1
5Epic1
4Rare1
5Epic1
4Rare1
5Epic1
4Rare1
5Epic1
4Rare1
5Epic1
4Rare1
5Epic1
4Rare1
5Epic1
4Rare1
5Epic1
4Rare1
5Epic1
4Rare1
5Epic1
4Rare1
5Epic1
4Rare1
5Epic1


--[[
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"] = "20",
	["HR"] = "30",
	["Squire to Riches"] = "40",
	["???"] = "50"
}
local MAPS = {
	["Default"] =		"0",
	["Goblin Caves"] =	"01",
	["Firedeep"] =		"02",
	["Ice Cavern"] =	"11",
	["Ice Abyss"] =		"12",
	["Ruins"] =			"21",
	["Crypts"] =		"22",
	["Inferno"] =		"23",
	["Ship Graveyard"] ="31"
}
-- 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"]    = {mode = "Default",	map = "Default"},

	["1001"] = {mode = "PvE",		map = "Goblin Caves"},
	["1002"] = {mode = "PvE",		map = "Firedeep"},
	["1011"] = {mode = "PvE",		map = "Ice Cavern"},
	["1012"] = {mode = "PvE",		map = "Ice Abyss"},
	["1021"] = {mode = "PvE",		map = "Ruins"},
	["1022"] = {mode = "PvE",		map = "Crypts"},
	["1023"] = {mode = "PvE",		map = "Inferno"},
	["1031"] = {mode = "PvE",		map = "Ship Graveyard"},

	["2001"] = {mode = "LR",		map = "Goblin Caves"},
	["2002"] = {mode = "LR",		map = "Firedeep"},
	["2011"] = {mode = "LR",		map = "Ice Cavern"},
	["2012"] = {mode = "LR",		map = "Ice Abyss"},
	["2021"] = {mode = "LR",		map = "Ruins"},
	["2022"] = {mode = "LR",		map = "Crypts"},
	["2023"] = {mode = "LR",		map = "Inferno"},
	["2031"] = {mode = "LR",		map = "Ship Graveyard"},

	["3001"] = {mode = "HR",		map = "Goblin Caves"},
	["3002"] = {mode = "HR",		map = "Firedeep"},
	["3011"] = {mode = "HR",		map = "Ice Cavern"},
	["3012"] = {mode = "HR",		map = "Ice Abyss"},
	["3021"] = {mode = "HR",		map = "Ruins"},
	["3022"] = {mode = "HR",		map = "Crypts"},
	["3023"] = {mode = "HR",		map = "Inferno"},
	["3031"] = {mode = "HR",		map = "Ship Graveyard"},

	["4001"] = {mode = "StR",		map = "Goblin Caves"},
	["4002"] = {mode = "StR",		map = "Firedeep"},
	["4011"] = {mode = "StR",		map = "Ice Cavern"},
	["4012"] = {mode = "StR",		map = "Ice Abyss"},
	["4021"] = {mode = "StR",		map = "Ruins"},
	["4022"] = {mode = "StR",		map = "Crypts"},
	["4023"] = {mode = "StR",		map = "Inferno"},
	["4031"] = {mode = "StR",		map = "Ship Graveyard"},

	-- ["5001"] = {mode = "???",		map = "Goblin Caves"},
	-- ["5002"] = {mode = "???",		map = "Firedeep"},
	-- ["5011"] = {mode = "???",		map = "Ice Cavern"},
	-- ["5012"] = {mode = "???",		map = "Ice Abyss"},
	-- ["5021"] = {mode = "???",		map = "Ruins"},
	-- ["5022"] = {mode = "???",		map = "Crypts"},
	-- ["5023"] = {mode = "???",		map = "Inferno"},
	-- ["5031"] = {mode = "???",		map = "Ship Graveyardo"}
}
-- 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 "Improper dungeon grade code",""
	end

	return DUNGEON_GRADES[dungeon_grade].mode, DUNGEON_GRADES[dungeon_grade].map
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 dungeon grade cannot be matched, skip it
		if map ~= "" then
			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
	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, disambiguation)
	local interpretation = interpret_dungeon_strings(id)
	if interpretation == ''  then interpretation = id end

	local wt = '<div class="'..id_prefix..interpretation

	if disambiguation then
		wt = wt..disambiguation
	end

	if is_displayed then
		wt = wt..'-data">'
	else
		wt = wt..'-data" style="display:none;">'
	end

	return wt
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, is_differentiated)
	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

		if is_differentiated then
			wikitext = wikitext..create_tab_toggle(tab_id, tab_id..interpretation..tostring(i), tab, i==1)
		else
			wikitext = wikitext..create_tab_toggle(tab_id, tab_id..interpretation, tab, i==1)
		end
	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="min-width:30%"><caption>Drop rates&nbsp;</caption>'
		..'<tr><th>Luck grade</th><th>Probability</th><th>Probability per item</th><th>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="min-width:30%"><caption>Loot Table&nbsp;</caption>'
		..'<tr><th>Name</th><th>Luck Grade</th><th>Rarity</th><th>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
local function create_wikitext(LDG,dungeon_tree,grade)
	local wikitext = create_tab_toggles_from_list(dungeon_tree.order,grade.."0") -- mode toggles
	for i,mode in ipairs(dungeon_tree.order) do -- modes - 4 nodes
		wikitext = wikitext
			..create_div_header(grade.."0", mode, i == 1)
			..create_tab_toggles_from_list(dungeon_tree[mode].order,grade.."1"..i) -- map toggles

		for j,map in ipairs(dungeon_tree[mode].order) do -- maps - 1 to 6 nodes
			if not LDG.loot_drop_groups.data[0] and not LDG.loot_drop_groups.data[1] then return "No items drop from this source." end

			local dungeon_grade = tonumber(interpret_dungeon_strings(mode, map)) -- e.g. "1001" for "PvE", "Goblin Caves"
			-- json decoder forces 0 -> 1 for arrays, so we must rely on a manual fix
			if dungeon_grade == 0 and LDG.loot_drop_groups.data[dungeon_grade] == nil then dungeon_grade = 1 end

			wikitext = wikitext
				..create_div_header(grade.."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]),grade.."2"..i..j,true) -- 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(grade.."2"..i..j, clean_loot_table_id(loot_table.drop_rate_id), k == 1,k) -- prefix "2", the tab toggle's depth, to create a unique identifier
						..'<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


local GRADE_ORDER = {"Common","Elite","Nightmare"}


-- Some monsters don't have all three grades, so we filter them here
local function get_grades(ML,page_name)
	local grades = {}

	for _,grade in ipairs(GRADE_ORDER) do
		if ML.localized_name[page_name][grade] ~= nil then
			grades[#grades+1] = grade
		end
	end

	return grades
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)
	local wt = {}
	local ML = mw.loadJsonData("Loot:Monster.json")
	local pagename = frame.args.name
	local LDG_id = ""
	local LDG = {}

	wt[#wt+1] = "<h2 style='width:100%'>Loot Tables</h2>"

	local grades = get_grades(ML,pagename)
	wt[#wt+1] = create_tab_toggles_from_list(grades,"Grade")
	for i, grade in ipairs(grades) do
		local id = ML.localized_name[pagename][grade]

		if LDG_id ~= "Loot:"..ML.id[id].loot..".json" then -- only load as needed
			LDG_id = "Loot:"..ML.id[id].loot..".json"
			LDG = mw.loadJsonData(LDG_id)
		end
		if LDG == nil then return "LootDropGroup file not found." end

		wt[#wt+1] = create_div_header("Grade", grade, i == 1)
		wt[#wt+1] = create_wikitext(LDG,get_dungeon_tree(ML.id[id].dungeon_grade_order),grade)
		wt[#wt+1] = "</div>"
	end

	return table.concat(wt)
end

return p



--[=[
mw.log(p.loot_tables({args={name="Mermaid",category="Monster"}}))
mw.log(p.loot_tables({args={name="Ghost King",category="Monster"}}))
]=]