Module:Spell cost table

Module documentation
This documentation is transcluded from Template:No documentation/doc. [edit] [history] [purge]
This module does not have any documentation. Please consider adding documentation at Module:Spell cost table/doc. [edit]
Module:Spell cost table's function main is invoked by Template:Spell cost table.
Module:Spell cost table requires Module:Coins.
Module:Spell cost table requires Module:Exchange.

-- <pre>

local p = {}

local gep = require('Module:Exchange')._price
local coins = require('Module:Coins')._amount

local combo_runes = {
	['mist rune'] = { ['air rune'] = true, ['water rune'] = true },
	['dust rune'] = { ['air rune'] = true, ['earth rune'] = true },
	['mud rune'] = { ['water rune'] = true, ['earth rune'] = true },
	['smoke rune'] = { ['air rune'] = true, ['fire rune'] = true },
	['steam rune'] = { ['water rune'] = true, ['fire rune'] = true },
	['lava rune'] = { ['earth rune'] = true, ['fire rune'] = true }
}

local non_rune_items = {
	['banana'] = 1,
	['unpowered orb'] = true,
	['soft clay'] = true,
}
	
local staves = {
	--[[Weapon Structure
		Weapons are structured to allow specification of when and how they are included in the table. The structure for each entry is detailed below.
	
		<<Parameters>>
		name		 : The name of the item
		['_____']	 : The name of a fully provided rune (or item), should be set to true
		conditions   : (Optional) An array of conditions for item (or Modifier) inclusion, detailed below
		alternatives : (Optional) A list of alternative items, these will be listed in an explain subscript if they haven't been included in the table
		extras		 : (Optional Modifier) An array of additional items to the row, each item accepts conditions in addition to the parameters below
			item	 : The name of the item
			quantity : The amount of the item
		negations	 : (Optional Modifier) An array of negations (i.e.) a chance to save a rune, conditions should be used to specify things such as rune type or required arguments
			magnitude: The multiplier to negate by. Should be 1 - (% to save), e.g Kodai wand has 15% so magnitude is 1 - 0.15 = 0.85
			offset: an offset to subtract. Final rune cost is (base cost - offset)*magnitude.
		<<Conditions>>
		args		 : An array of {argument, (optional) value} checks for arg, if a value is included it requires that specific value
		runes		 : Array of rune types, should NOT be used at root level, i.e. should only be used as a condition for modifiers
		rune_count	 : A minimum number of matching runes to be included, should ONLY be used at root level, i.e. shouldn't be used as a condition for modifiers
	--]]
	{ name = 'staff of air', ['air rune'] = true, alternatives = {'mist battlestaff', 'smoke battlestaff', 'dust battlestaff'} },
	{ name = 'staff of water', ['water rune'] = true, alternatives = {'mist battlestaff', 'mud battlestaff', 'steam battlestaff', 'kodai wand'} },
	{ name = 'staff of earth', ['earth rune'] = true, alternatives = {'dust battlestaff', 'mud battlestaff', 'lava battlestaff'} },
	{ name = 'staff of fire', ['fire rune'] = true, alternatives = {'steam battlestaff', 'lava battlestaff', 'smoke battlestaff'} },
	
	--We don't want to including combination staves unless they're actually granting a benefit
	{ name = 'mud battlestaff', ['water rune'] = true, ['earth rune'] = true, conditions = { rune_count = 2 }},
	{ name = 'steam battlestaff', ['water rune'] = true, ['fire rune'] = true, conditions = { rune_count = 2 } },
	{ name = 'lava battlestaff', ['earth rune'] = true, ['fire rune'] = true, conditions = { rune_count = 2 } },
	{ name = 'smoke battlestaff', ['air rune'] = true, ['fire rune'] = true, conditions = { rune_count = 2 } },
	{ name = 'dust battlestaff', ['air rune'] = true, ['earth rune'] = true, conditions = { rune_count = 2 } },
	{ name = 'mist battlestaff', ['air rune'] = true, ['water rune'] = true, conditions = { rune_count = 2 } },
	
	--Staff of the dead and Kodai only need to be shown for offensive spells, thus we can condition their inclusion based on the is_offensive arg
	{ name = 'staff of the dead', conditions = {args = {{'is_offensive'}}}, alternatives = {'staff of light, staff of balance, or toxic variant'}, negations = {{magnitude = 0.857}} },
	{ name = 'kodai wand', ['water rune'] = true, conditions = {args = {{'is_offensive'}}}, negations = {{magnitude = 0.85}} },
	
	--Partial negation should be conditioned upon the rune(s), placed INSIDE the negation parameter
	{ name = "bryophyta's staff", negations = {{conditions = {runes = {'nature rune'}}, offset = (1/15)}} }
}

local offhands = {
	--Tome of Fire only sometimes uses pages, so we want to condition the extras to the uses_pages arg that
	{ name = 'tome of fire', ['fire rune'] = true, extras = {{conditions = {args = {{'uses_pages'}}}, item = 'Burnt page', quantity = 1/20 }} },
	{ name = 'tome of water', ['water rune'] = true, extras = {{conditions = {args = {{'uses_pages'}}}, item = 'Soaked page', quantity = 1/20}} },
	--(Following to be added on Sept 25th 2024)
	{ name = 'tome of earth', ['earth rune'] = true, extras = {{conditions = {args = {{'uses_pages'}}}, item = 'Soiled page', quantity = 1/20}} },
}

function p.main(frame)
	local args = frame:getParent().args
	
	--Parse the numbered rune arguments into an array
	local runes = {}
	for i=1,10 do
		if not args['Rune'..i] then
			break
		end
		
		local rune = string.lower(args['Rune'..i])
		
		-- Unless it's found in non-rune items, we assume that it's a rune and append " rune" to the end
		if not non_rune_items[rune] then
		    rune = rune..' rune'
		end
		    
		local num = tonumber(args['Rune'..i..'num'] or 1)
		table.insert(runes,{rune,num,{}})
	end
	
	return p.create_table(runes, args)
end

-- We want backwards compatibility for the old module, until things are moved over
function p._main(runes, no_staff, uses_pages)
	return p.create_table({['no_staff'] = no_staff, ['uses_pages'] = uses_pages})
end

function p.create_table(runes, args)
	-- Create the headers and insert the first row for basic runes
	local ret = mw.html.create('table')
					:addClass('wikitable')
					:tag('caption')
						:wikitext('Spell cost')
					:done()
					:tag('tr')
						:tag('th')
							:wikitext('Input')
						:done()
						:tag('th')
							:wikitext('Cost')
						:done()
					:done()
					:tag('tr')
						:tag('td')
							:wikitext(make_pics(runes))
						:done()
						:tag('td')
							:wikitext(total_price(runes))
						:done()
					:done()

	-- Decide what combo runes can be used in the spell
	local combos_used = {}
	for i, v in pairs(combo_runes) do
		local amtused = 0
		local runes_temp = {}
		for j, x in ipairs(runes) do
			if v[x[1]] then
				if x[2] > amtused then
				   amtused = x[2]
				end
			else
				table.insert(runes_temp, x)
			end
		end
		if amtused > 0 then
			table.insert(runes_temp,{i, amtused,{}})
			table.insert(combos_used,runes_temp)
		end
	end
	if #combos_used > 0 then
		ret:tag('tr')
			:tag('th')
				:attr('colspan','2')
				:wikitext('Combo runes')
			:done()
		:done()
		for _, v in ipairs(combos_used) do
			ret:tag('tr')
				:tag('td')
					:wikitext(make_pics(v))
				:done()
				:tag('td')
					:wikitext(total_price(v))
				:done()
			:done()
		end
	end
	
	-- add relevant main-hands to the weapons table
	local weapons = {}
	local relevant_staves = {}
	if (not args.nostaff and not args.no_staff) or (args.nostaff == 0 or args.no_staff == 0) then
		relevant_staves = composeWeapons(staves, runes, args)
		weapons = join (weapons, relevant_staves)
	end
	
	-- add relevant off-hands to the weapons table
	local relevant_offhands = {}
	if (not args.nooffhand and not args.no_offhand) and ((not args.nostaff and not args.no_staff) or (args.nostaff ~= 1 or args.no_staff ~= 1))  then
		relevant_offhands = composeWeapons(offhands, runes, args)
		weapons = join(weapons, relevant_offhands)
	end
	
	local relevant_combos = {}
	-- add relevant main-+off-hand combinations to the weapons table
	relevant_combos = compose_combinations(relevant_staves, relevant_offhands, runes, args)
	
	local offhand_header = 'Off-hands'
	if #relevant_combos > 0 then
		offhand_header = 'Main and off-hands'
	end
	
	if #relevant_staves > 0 then
		ret:tag('tr')
			:tag('th')
				:attr('colspan','2')
				:wikitext('Main-hands')
			:done()
		:done()
		ret = weapon_output(relevant_staves,runes,weapons,ret,args)
	end
	if #relevant_offhands > 0 then
		ret:tag('tr')
			:tag('th')
				:attr('colspan','2')
				:wikitext(offhand_header)
			:done()
		:done()
		ret = weapon_output(relevant_offhands,runes,weapons,ret,args)
		ret = weapon_output(relevant_combos,runes,weapons,ret,args)
	end
	return ret
end

-- Here we implement how conditions are checked.
function check_conditions(conditions, args, rune, rune_count)
	local ret = true --create a return variable
	
	if conditions.args then -- Scan through all args to make sure they match
		for _, condition in ipairs(conditions.args) do
			ret = ret and args[condition[1]] and ((not condition[2]) or (condition[2] == args[condition[1]]))
			-- If an arg has no listed value, it's assumed that anything but a nil value is valid, otherwise check
		end
	end

	if conditions.rune_count then ret = ret and rune_count >= conditions.rune_count end 
	
	if conditions.runes then -- Scan through all runes for which this condition applies, if a match is found, condition passes
		local rune_present = false
		for _, irune in ipairs(conditions.runes) do
			rune_present = rune_present or rune == irune
		end
		ret = ret and rune_present
	end
	
	return ret
end


function composeWeapons(list, runes, args)
	local a = {}
	for i, v in ipairs(list) do
		--Iterate through runes to search for a match on the staff's provided runes
		
		local total = 0
		local rune_count = 0
		local has_negation = false
		for j,k in ipairs(runes) do
			total = total + 1
			if v[k[1]] then
				rune_count = rune_count + 1
			else
				for ineg, negation in ipairs(v.negations or {}) do
					-- If the weapon has a negation and either fulfills its conditions OR has no conditions listed, apply it
					has_negation = has_negation or (not negation.conditions or check_conditions(negation.conditions, args, k[1])) 
				end
				
				-- No need to continue if negation is found
				if has_negation then break end
			end
		end
		
		if (not v.conditions or check_conditions(v.conditions, args, nil, rune_count)) and (has_negation or rune_count > 0) then
			table.insert(a, {v})
		end
	end
	return a
end

function compose_combinations(listA, listB, runes, args)
	local ret = {}
	
	for i_a, a in ipairs(listA) do
		for i_b, b in ipairs(listB) do
			-- eliminate redundancy e.g. fire staff + tome of fire is redundant
			for _, rune_data in ipairs(runes) do
				local rune_name = rune_data[1]
				if (not a[1][rune_name] and b[1][rune_name]) then
					table.insert(ret, {a[1], b[1]})
				else
					-- We want to check if the combined negations actually apply, so we calculate both negations then see if the combined is less than the staff's
					local negationMagA = 1
					for ineg, negation in ipairs(a[1].negations or {}) do
						-- If the weapon has a negation and either fulfills its conditions OR has no conditions listed, apply it
						if (not negation.conditions or check_conditions(negation.conditions, args, rune_data[1])) then
							negationMagA = negationMagA * (negation.magnitude or 1)
						end
					end
					local negationMagB = 1
					for ineg, negation in ipairs(b[1].negations or {}) do
						-- If the weapon has a negation and either fulfills its conditions OR has no conditions listed, apply it
						if (not negation.conditions or check_conditions(negation.conditions, args, rune_data[1])) then
							negationMagB = negationMagB * (negation.magnitude or 1)
						end
					end
					
					if negationMagA*negationMagB < negationMagA then table.insert(ret, {a[1], b[1]}) end
				end
			end
		end
	end
	
	return ret
end

function weapon_output(weapons, runes, all, ret, args)
	--For each weapon, scan through the runes and choose what to display
	for _, weapon_data in ipairs(weapons) do
		local tbl = {}
		for rune_i, rune_data in ipairs(runes) do
			if weapon_data[1][rune_data[1]] or (weapon_data[2] and weapon_data[2][rune_data[1]]) then
				-- do nothing
			else
				local negationMag = 1
				local negationOffset = 0
				local negations = weapon_data[1].negations or {}
				if weapon_data[2] then negations = join(negations, weapon_data[2].negations or {}) end
				
				for ineg, negation in ipairs(negations) do
					-- If the weapon has a negation and either fulfills its conditions OR has no conditions listed, apply it
					if (not negation.conditions or check_conditions(negation.conditions, args, rune_data[1])) then
						negationMag = negationMag * (negation.magnitude or 1)
						negationOffset = negationOffset + (negation.offset or 0)
					end
				end
				
				if negationMag == 0 then
					-- Do nothing
				elseif negationMag < 1 or negationOffset > 0 then
					table.insert(tbl, { rune_data[1], round( negationMag * (rune_data[2] - negationOffset), 2), {} })
				else
					table.insert(tbl, { rune_data[1], rune_data[2], {}} )
				end
			end
		end
		
		local extras = weapon_data[1].extras or {}
		if weapon_data[2] then extras = join(extras, weapon_data[2].extras or {}) end
		for i,v in ipairs(extras) do
			if not v.conditions or check_conditions(v.conditions, args, {}) then
				table.insert(tbl, {v.item, v.quantity, {}})
			end
		end
		
		for i,weapon in ipairs(weapon_data) do
			table.insert(tbl,{weapon.name, 0, weapon})
		end
		
		ret:tag('tr')
				:tag('td')
					:wikitext(make_pics(tbl, all))
				:done()
				:tag('td')
					:wikitext(total_price(tbl))
				:done()
			:done()
	end
	return ret
end

function join(tbl1, tbl2)
	local ret = tbl1
	
	for _, item in ipairs(tbl2 or {}) do
		table.insert(ret, item)
	end
	
	return ret
end

function round(n, digits)
	local working = math.pow(10, digits)
	local workingMod = math.fmod(n * working, 10)
	
	if workingMod >= 5 then
		return math.ceil(n * working)/working
	else
		return math.floor(n * working)/working
	end
end

function make_pics(arg, others)
	local runes = {}
	for _, v in ipairs(arg) do
		if type(v[1]) == 'table' then
			local _v = v[1]
			for _, w in ipairs(_v) do
				table.insert(runes, {w, v[2]})
			end
		else
			table.insert(runes, v)
		end
	end

	local ret = {}

	for _, v in ipairs(runes) do
		if v[2] > 0 then
			table.insert(ret,'<sup>'..v[2]..'</sup>')
		end
		table.insert(ret,'[[File:'..v[1]..'.png|link='..v[1]..']] ')

		local alts = ""
		local altNext = ""
		for _, alt in ipairs(v[3].alternatives or {}) do
			
			local weapon_present = false
			for i, weapon in ipairs(others) do
				if alt == weapon[1].name then
					weapon_present = true
					break
				end
			end
			
			if not weapon_present then 
				if #alts == 0 then
					alts = altNext
				else
				    alts = alts..", "..altNext
				end
				altNext = alt
			end
		end
		
		if #altNext ~= 0 then
			if #alts == 0 then alts = altNext
			else alts = alts..", or "..altNext	end
			
			table.insert(ret,"<sub class='explain' title='Alternatively, a "..alts.." can be used.' style='text-decoration:underline dotted'>Alt</sub>")
		end
	end
	
	return table.concat(ret)
end

function total_price(runes)
	local ret = 0
	for _, v in ipairs(runes) do
		if v[2] > 0 then
			ret = ret + gep(v[1]) * v[2]
		end
	end
	return coins(round(ret,0))
end

return p