Module:Sandbox/User:MxFox/Spell cost table
Module documentation
This documentation is transcluded from Template:Module sandbox/doc. [edit] [history] [purge]
Module:Sandbox/User:MxFox/Spell cost table requires Module:Coins.
Module:Sandbox/User:MxFox/Spell cost table requires Module:Exchange.
This module is a sandbox for MxFox. It can be used to test changes to existing modules, prototype new modules, or just experimenting with lua features.
Invocations of this sandbox should be kept in userspace; if the module is intended for use in other namespaces, it should be moved out of the sandbox into a normal module and template.
This default documentation can be overridden by creating the /doc subpage of this module, as normal.
-- <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 mw.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