Module:TradeTable

From Downtime Wiki
Revision as of 04:45, 25 April 2025 by Orashgle (talk | contribs) (1 revision imported)
(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)

Documentation for this module may be created at Module:TradeTable/doc

local p = {}
local namespace = mw.title.getCurrentTitle().nsText
local sprite = require('Module:SpriteFile')
local rowspans = {} -- Count of rowspans for each level
local rowspans_slot = {} -- count of rowspans for each slot
local outside_html -- html outside of table
local html -- html table
local lastLevel = ''
local lastSlot
local profession = ''
local refUsed = false
local level_trades = {} -- How many different trades are available in the pool per level
local slot_trades = {} -- How many different trades are available in the pool per slot (bedrock)
local given_note_list = {} -- A table of note names -> note text
local wanted_note_list = {} -- A table of note names -> note text

local colors = {'White', 'Light Gray', 'Gray', 'Black', 'Brown', 'Red', 'Orange', 'Yellow', 'Lime', 'Green', 'Cyan', 'Light Blue', 'Blue', 'Purple', 'Magenta', 'Pink'}

-- Maps alternate input param names to a standardized name
function normalizeInput(line)
	return {
		level = line["level"] or line["lvl"],
		trade_slot = line["slot"] or '-',
		wanted_item = line["want"] or line["want1"] or 'Emerald',
		wanted_quant = line["wantQuant"] or line["wantQuant1"] or '1',
		wanted_sprite = line["wantSprite"] or line["wantSprite1"],
		wanted_item_2 = line["want2"],
		wanted_sprite_2 = line["wantSprite2"],
		wanted_quant_2 = line["wantQuant2"] or '1',
		wanted_note = line["wantNote"],
		wanted_note_text = line["wantNoteText"],
		price_multiplier = line["multi"],
		given_item = line["give"] or 'Emerald',
		given_sprite = line["giveSprite"],
		given_quant = line["giveQuant"] or '1',
		given_note = line["giveNote"],
		given_note_text = line["giveNoteText"],
		max_trades = line["maxTrades"],
		villager_xp_gain = line["xpGain"],
		weight = line["weight"] or 1
	}
end

-- Takes all args and puts them into a json string
function p.serialize(frame)
	return mw.text.jsonEncode(frame:getParent().args)
end

-- Undo the serialization for parsing
function deserialize(args)
	local e
	local out = {}
	for k, v in pairs(args) do
		if type(k) == "number" then -- only deserialize unnamed params
			e, out[k] = pcall( mw.text.jsonDecode, v )
			if not e then
				out[k] = { '', 'unknown', v..' <strong class="error">Lua error: '..out[k].. ' Input:'.. v ..'</strong>' }
			else
				out[k] = normalizeInput(out[k])
			end
		end
	end
	return out
end

function printHeader(headerText)
	outside_html = mw.html.create('div')
	html = outside_html:tag('table')
		:addClass('wikitable')
		:cssText('text-align:center')
	
	if headerText ~= '' then
		html:tag('th')
			:attr('colspan', 9)
			:attr('data-description', headerText)
			:wikitext(headerText)
	end
	
	local tr = html:tag('tr')
	tr:tag('th'):wikitext('Level'):attr('rowspan', 2)
	
	tr:tag('th'):wikitext("''[[Bedrock Edition]]''"):attr('colspan', 2)
	tr:tag('th'):wikitext("''[[Java Edition]]''")

	tr:tag('th'):wikitext('Item wanted'):attr('rowspan', 2)
		:tag('th'):wikitext('Item given'):attr('rowspan', 2)
		:tag('th'):wikitext('Trades in<br>stock'):attr('rowspan', 2)
		:tag('th'):wikitext('Price multiplier'):attr('rowspan', 2)
		:tag('th'):wikitext('Villager XP'):attr('rowspan', 2)
	
	tr = tr:tag('tr')
	tr:tag('th'):wikitext('Slot')
	tr:tag('th'):wikitext('Probability')
	tr:tag('th'):wikitext('Probability')

end

function p.formatItemOutput(item, quant, item2, quant2, _sprite, _sprite2)
	local text, qtyText = '', ''
	
	local item_display_text = item:gsub(' %(item%)',''):gsub('Any color','')
	
	_sprite = _sprite or item
	if quant ~= '1' then
		quant = quant:gsub('-', '&ndash;')
		qtyText = quant..' × '
	end
	if item:find('Enchanted') then
		text = text..qtyText..'[[File:'.._sprite..' (item).gif|16x16px|link='..item..']] [['..item..']]'
	elseif item:find('Any color') then
		-- Show all color sprites
		local base_item = _sprite:gsub('Any color', '')
		for i, color in pairs(colors) do
			text = text..sprite.link({text='',id=color..' '..base_item,name='InvSprite',keepcase=1})
			if i == 8 then
				text = text..'<br>'
			end
		end
		text = text..'<br>'..qtyText..'[['..item_display_text..'|'..item..']]'
	else
		text = text..qtyText..sprite.link({text=item_display_text,id=_sprite,link=item,name="InvSprite", keepcase=1})
	end
	
	if quant2 and item2 then
		text = text .. '<br>+ ' .. p.formatItemOutput(item2, quant2, nil, nil, _sprite2)
	end
	return text
end

-- from WP
function choose(n, k)
	if k < 0 or k > n then
		return 0
	end
	if k == 0 or k == n then
		return 1
	end
	k = math.min(k, n-k) -- symmetry
	local c = 1
	for i = 0, k-1 do
		c = c * (n-i) / (k - i)
	end
	return c
end

function formatProbability(line, use_slot)
	local weight = line.weight
	local group_weight = level_trades[line.level]
	local draws = 2
	if use_slot and line.trade_slot then
		if line.trade_slot == '-' then
			return '-'
		end
		draws = 1
		group_weight = slot_trades[line.trade_slot]
	end
	
	if group_weight - weight == 0 then
		return 100	
	end
	
	local probability = (1 - choose(group_weight - weight, draws) / choose(group_weight, draws)) * 100
	return math.floor(probability+0.5)
end

function printLine(line)
	local tr = html:tag('tr')
	local cssText = ''
	if line.level ~= lastLevel then
		-- Don't do thick border for first level
		if lastLevel ~= '' then
			cssText = 'border-top-width:2px'
		end
		tr:tag('th')
			:attr('rowspan', rowspans[line.level])
			:cssText(cssText)
			:wikitext((line.level:gsub("^%l", string.upper))) --uppercase first letter for nice display
	end
	lastLevel = line.level
	
	if line.trade_slot == '-' or line.trade_slot ~= lastSlot then
		tr:tag('th')
			:attr('rowspan', rowspans_slot[line.trade_slot])
			:cssText(cssText)
			:wikitext(line.trade_slot)
	end
	lastSlot = line.trade_slot
	
	local bedrock_probability = formatProbability(line, true)
	if line.trade_slot ~= '-' then
		bedrock_probability = bedrock_probability .. '%'	
	end
	
	tr:tag('td'):cssText(cssText):wikitext(bedrock_probability) -- Bedrock probability
	tr:tag('td'):cssText(cssText):wikitext(formatProbability(line, false), '%') -- Java probability
	
	local wanted = tr:tag('td'):cssText(cssText):wikitext(p.formatItemOutput(line.wanted_item, line.wanted_quant, line.wanted_item_2, line.wanted_quant_2, line.wanted_sprite, line.wanted_sprite_2))
	if line.wanted_note_text ~= nil or line.wanted_note ~= nil then
		wanted:wikitext(mw.getCurrentFrame():extensionTag{ name='ref', content=line.wanted_note_text, args = {name=line.wanted_note, group='t'}})
		refUsed = true -- Used to set references at footer
	end
	local given = tr:tag('td'):cssText(cssText):wikitext(p.formatItemOutput(line.given_item, line.given_quant, nil, nil, line.given_sprite)):wikitext()
	if line.given_note_text ~= nil or line.given_note ~= nil then
		given:wikitext(mw.getCurrentFrame():extensionTag{ name='ref', content=line.given_note_text, args = {name=line.given_note, group='t'}})
		refUsed = true -- Used to set references at footer
	end
	tr:tag('td'):cssText(cssText):wikitext(line.max_trades)
	
	local price_multiplier_text = line.price_multiplier
	
	if line.price_multiplier == "0.05" then 
		price_multiplier_text = "Low"
	elseif line.price_multiplier == "0.2" then 
		price_multiplier_text = "High"
	end
	tr:tag('td'):cssText(cssText):wikitext(price_multiplier_text)
	tr:tag('td'):cssText(cssText):wikitext(line.villager_xp_gain)
end

function storeLine(line)
	-- Put data into SMW
	-- The only top level params that are needed is stuff that would be used as selectors
	-- everything else is put into a json blob for flexibility
	if not (namespace == '' or namespace == 'Minecraft_Wiki') then
		 return -- only store data if we are in main or Minecraft_Wiki namespaces
	end
	
	local subname = 'TRADE'..'_'..line.level..'_'..line.wanted_item..'_'..line.given_item
	subname = subname:gsub(' ', '_')
	
	local smw_json = {
		['profession'] = profession,
		['level'] = line.level,
		['java_probability'] = formatProbability(line, false),
		['bedrock_probability'] = formatProbability(line, true),
		['given_item'] = line.given_item,
		['given_quant'] = line.given_quant,
		['given_sprite'] = line.given_sprite,
		['wanted_item'] = line.wanted_item,
		['wanted_quant'] = line.wanted_quant,
		['wanted_sprite'] = line.wanted_sprite,
		['wanted_item_2'] = line.wanted_item_2,
		['wanted_quant_2'] = line.wanted_quant_2,
		['wanted_sprite_2'] = line.wanted_sprite_2,
		['given_note'] = line.given_note,
		['given_note_text'] = given_note_list[line.given_note] or line.given_note_text,
		['wanted_note'] = line.wanted_note,
		['wanted_note_text'] = wanted_note_list[line.wanted_note] or line.wanted_note_text
	}
	local smw_sub = { -- the actual SMW sub-object
		['Given item'] = line.given_item,
		['Wanted item'] = {line.wanted_item, line.wanted_item_2},
		['Trade JSON'] = mw.text.jsonEncode(smw_json)
	}
	local result = mw.smw.subobject(smw_sub, subname)
	mw.logObject(result, subname)
	if result == true then
		return ''
	else
		return 'Error saving smw data: ' .. result.error
	end
end

function p.main(frame)
	return p._main(frame:getParent().args)
end

function p._main(args)
	local headerText = args.title or ''
	local ignoreUsage = args.ignoreUsage or nil
	local lines = deserialize(args)
	
	-- First loop counts the rowspans for column 1
	for i,line in ipairs(lines) do
		level_trades[line.level] = (level_trades[line.level] or 0) + line.weight
		rowspans[line.level] = (rowspans[line.level] or 0) + 1
		rowspans_slot[line.trade_slot] = (rowspans_slot[line.trade_slot] or 0) + 1
		slot_trades[line.trade_slot] = (slot_trades[line.trade_slot] or 0) + line.weight
		
		--Save the note text so it can be used by SMW to display the notes on other pages
		if line.given_note and line.given_note_text and given_note_list[line.given_note] == nil then
			given_note_list[line.given_note] = line.given_note_text
		end
		if line.wanted_note and line.wanted_note_text and wanted_note_list[line.wanted_note] == nil then
			wanted_note_list[line.wanted_note] = line.wanted_note_text
		end
	end
	profession = args.profession or headerText
	printHeader(headerText)
	
	rowspans_slot['-'] = 1 -- Special case missing slot to not combine
	
	local smw_info = {}
	for i, line in ipairs(lines) do
		printLine(line)
		if not ignoreUsage then
			local result = storeLine(line)
			if result ~= '' then
				table.insert(smw_info, result)
			end
		end
	end
	-- Add a warning popup with any errors saving smw data
	if #smw_info > 0 then
		outside_html:wikitext(mw.smw.info(table.concat(smw_info, '\n'), 'warning'))
	end
	
	if refUsed then
		outside_html:wikitext(mw.getCurrentFrame():extensionTag{ name='references', args = {group='t'}})
	end
	
	return outside_html
end

return p