Module:Minion

local p = {}

local getArgs = require('Module:Arguments').getArgs local mergeArgsSyntax = require('Module:Arguments').mergeArgsSyntax local loader = require('Module:Loader') local string, table, yesno = loader.require('String', 'Table', 'Yesno') local _error = string._error

local bazaar, colorModule, currency, itemModule, ui = loader.require('Bazaar', 'Color', 'Currency', 'Item', 'UI') local slot = require('Module:Inventory slot').slot local minionData, collectionData, minionAliases = loader.loadData('Minion/Data', 'Collection/Data', 'Minion/Aliases')

-- not all minions have all 12 tiers; use #TIERS to cover all tiers, instead of using '12' local TIERS = { "I", "II", "III", "IV", "V", "VI", "VII", "VIII", "IX", "X", "XI", "XII" }

-- NPC text for p.minionUI local NPCtext = { ["Bulvar"] = {name="&5Bulvar", loc="&2Dwarven Mines"}, ["Terry"] = {name="&5Terry", loc="&bTrappers Den"}, }

-- merchant sell pricess for non-bazaar items, for profit calculation local nonBazaarMerchantSellPrices = { ['Poisonous Potato'] = 10, ['White Wool'] = 2, ['Clownfish'] = 20, }

-- Produces that aren't recorded as items in Minion/Data -- Used in p._minionPerDayTableCombined (DUMC) and _getStatTableBaseCost local differentList = { ['dandelion'] = {'Flower'}, ['poppy'] = {'Flower'}, }

local function _actionsPerMinute(minion, tier) -- TBA*2 = SECONDS_PER_FULL_ACTIONS : Each action either picks up or places, so 2 actions are needed for every collection. -- 60/SECONDS_PER_FULL_ACTIONS = ACTIONS_PER_MINION : This gives us a number that can be used as a multiplier to find all other action/time related questions. return 60/(minionData[minion].stats[tier].tba*2) end

local lang = mw.getContentLanguage local title = mw.title.getCurrentTitle.text

-- utility: peakStat -- not all minions have all 12 tiers; this function returns the values of --				the highest tier and the highest craftable tier of a minion local function peakStat(name) if not minionData[name] then error('Invalid minion: '..name) end highestTier = table.length(minionData[name].stats) for x = #TIERS, 1, -1 do		if minionData[name].stats[x] and minionData[name].stats[x].crafting then highestCraftable = x		end if highestCraftable then break end end return highestTier, highestCraftable end

p.peakStat = peakStat

-- Template:GetMinionData -- -- Returns a specified peice of data from the /Data module based on inputs

function p.getMinionData(frame) local args = getArgs(frame) local minion = args[1] or args["minion"] or args["m"] local tier = args["tier"] or args["t"] local data = args[2] or args["data"] or args["d"] local item = args[3] or args["i"] or args["item"] if not minion then return _error('Inputs expected, got none') end if not data then return _error('Invalid Argument to #2: paramater "data" is required.') end if not tier and minion:match('(.+) ([%dIVXivx]+)') then minion, tier = minion:match('(.+) ([%dIVXivx]+)') else tier = tier minion = minion end return p._getMinionData(minion, data, tier, item) end

function p._getMinionData(minionName, data, tier, item) local minionData = minionData[minionName] local aliases = minionAliases.minionDataAliases local data = aliases[data:lower] local tier = tonumber(string._toArabic(tier or 0)) local item = tonumber(item) if data == "storage" then ret = minionData.stats[tier].storage elseif data == "time between actions" then ret = minionData.stats[tier].tba elseif data == "crafting item" then ret = minionData.stats[tier].crafting.item elseif data == "crafting number" then ret = minionData.stats[tier].crafting.num elseif data == "description" then ret = minionData.description elseif data == "average item" then ret = minionData.items[item].item elseif data == "average number" then ret = minionData.items[item].avg else ret = _error("Invalid Argument to \"data\": unknown data type (help)") end return ret end

- -- Template:Days using minions -- -- Makes a table with how long it would take a single minion to output a certain amount of items based on it's tier - function p.minionPerDayTable( frame ) --local parameters = frame.args local args = getArgs(frame) local minion = args["minion"] or args[1] or title local amount = args["amount"] or args[2] local item = args["item"] or (amount == args[2] and args[3] or args[2]) local split = yesno(args["split"], true) local noscroll = yesno(args["noscroll"], false) return p._minionPerDayTable( minion, amount, item, split, noscroll ) end

function p._minionPerDayTable( minion, amount, item, split, noscroll ) if not minionData[minion] then error('Invalid minion '..minion) end local HALF_ROW = 6 local actionsPerDay local days local itemIndex = 1 if item then for key,value in ipairs(minionData[minion].items) do			if value['item'] == item then itemIndex = key end end end local _table = mw.html.create('table'):addClass('wikitable'):css({['text-align']="center", ['table-layout']="fixed"}) if noscroll and not split then _table:css({['min-width']="100%"}) end _table:tag('caption'):wikitext(string.format( 'Days to acquire %s using %s %s Minion (by tier, no fuel) If the player has multiple minions, divide the number of days by how many minions will be used. ',		itemModule._resourceDisplay(amount..' '..minionData[minion].items[itemIndex].item, true), (minion:lower:match('^([aeiouy])') and 'an' or 'a'), minion )	)	local row, row_data, row2, row_data2 = _table:tag("tr"), _table:tag("tr"), _table:tag("tr"), _table:tag("tr") for i, tier in ipairs(TIERS) do		if minionData[minion].stats[i] then if split and i > HALF_ROW then row2:tag("th"):wikitext(tier):done else if noscroll and not split then row:tag("th"):wikitext(tier):done else row:tag("th"):wikitext(tier):css({["min-width"]="3.8em"}):done end end else if split then row2:tag("td"):wikitext(string.blankCell):attr({rowspan=2}) end end end for i, tier in ipairs(TIERS) do		if minionData[minion].stats[i] then actionsPerDay = _actionsPerMinute(minion, i) * 60 * 24 days = amount/actionsPerDay/minionData[minion].items[itemIndex].avg -- Show appropriate number of decimals (smaller the number = more) days = days < 1 and math.floor(day * 100) / 100 or				days < 10 and math.floor(days * 10) / 10 or				math.floor(days) if split and i > HALF_ROW then row_data2:tag("td"):wikitext( days ):done else row_data:tag("td"):wikitext( days ):done end end end row:done row2:done row_data:done row_data2:done _table:done scrollable = string.wrapHtml(tostring(_table), 'div', {		class = 'article-scrollable',	}) return noscroll and tostring(_table) end

- -- Template:Days Using Minions Combined -- Template:DUMC -- -- Displays Days Using Minions with tabbers -

function p.minionPerDayTableCombined( frame ) --local parameters = frame.args local args = getArgs(frame) local lang = mw.language.getContentLanguage local split = yesno(args["split"], true) -- TODO: Fix noscroll currently only working with true -- local noscroll = yesno(args["noscroll"], false) local noscroll = true -- first combine both methods of entering data into one list local resources = mergeArgsSyntax(args) local items = {} for _, item in ipairs(resources) do		local numOrNil = tonumber(lang:parseFormattedNumber(item)) if numOrNil then items[#items+1] = numOrNil else local num, name = string.match(item, "([%d,%.]+)x? %[?%[?([%l%s%u%,%'%-%d_]*)%|?.*%]?%]?") if not num then -- also test for format name, num = string.match(item, "([%l%s%u%,%'%-_]*),([%d]+)") if not num then return _error('Invalid item format: '..item) end end items[#items+1] = {name, tonumber(lang:parseFormattedNumber(num))} end end return p._minionPerDayTableCombined(items, split, noscroll) end

function p._minionPerDayTableCombined(entries, split, noscroll) local result = {} local function addtolist(key, val) local stor = differentList[key:lower] if stor then table.merge(stor, val) else differentList[key:lower] = {val} end end for mn, dt in pairs(minionData) do		-- Adds each item that is in Minion/Data into differentList table.each(dt.items, function(i)			i = i.item			if i ==  then return end			addtolist(i, mn)			if i:match('^Raw ') then				local foo = i:gsub('^Raw ',)				addtolist(foo, mn)			end		end) end for i, entry in pairs(entries) do		minionList = differentList[entry[1]:lower] if minionList then table.push(result, ('|-|%s='):format(entry[1])) table.each(minionList, function(_minion)				local frame = mw.getCurrentFrame				table.push(result, p._minionPerDayTable(_minion, entry[2], entry[1], split, noscroll))			end) else local frame = mw.getCurrentFrame table.push(result, ('|-|%s='):format(entry[1])) table.push(result, p._minionPerDayTable(entry[1], entry[2], entry[1], split, noscroll)) end end result = " " .. table.concat(result) .. " "	local frame = mw.getCurrentFrame return noscroll and frame:preprocess(result) or scrollable end

- -- Template:Minion recipe gallery -- -- Makes a table of to form the in-game ideal layout for a minion - function p.minionRecipeGallery( frame ) --local parameters = frame.args local args = getArgs(frame) local minion = args["minion"] or args[1] if not minion then minion = title end return p._minionRecipeGallery( minion ) end

function p._minionRecipeGallery( minionName ) local minion = minionData[minionName] -- Get a list of the desired tiers local tiers = {} local start = minion.stats[1].crafting.info and 2 or 1 local _, nCraftable = peakStat(minionName) for i = start, nCraftable do tiers[#tiers+1] = i end -- Generate crafting tables local tables = {} for _,tier in ipairs(tiers) do		local crafting = minion.stats[tier].crafting local recipeBase = crafting.item..","..crafting.num local recipeB3 = crafting.B3 or crafting.item..","..crafting.num local recipeMinion = crafting.base or minionName.." Minion "..TIERS[tier-1] local recipeOutput = minionName.." Minion "..TIERS[tier] tables[#tables+1] = ui.craftingTable{ A1 = recipeBase, B1 = recipeBase,	C1 = recipeBase, A2 = recipeBase, B2 = recipeMinion, C2 = recipeBase, A3 = recipeBase, B3 = recipeB3,	 C3 = recipeBase, Output = recipeOutput }	end -- Display all crafting tables in a grid local grid = mw.html.create('table') local row for i, ct in pairs(tables) do		if (i-1)%3 == 0 then if row then row:done end row = grid:tag("tr") end row:tag("td"):wikitext(ct):done end row:done grid:done return tostring(grid) end

- -- Template:Minion animated crafting table -- -- Make an animated of all potential minion crafting recipes - function p.minionAnimatedCraftingTable( frame ) --local parameters = frame.args local args = getArgs(frame) local minion = args["minion"] or args[1] or title local item = args["item"] local from = args["from"] local to = args["to"] return p._minionAnimatedCraftingTable( minion, item, from, to ) end function p._minionAnimatedCraftingTable( minionName, item, from, to ) local minion = minionData[minionName] local highestTier, highestCraftable = peakStat(minionName) -- Get a list of the desired tiers local tiers = {} if item then for i=1,highestCraftable,1 do			local crafting = minion.stats[i].crafting if crafting.item == item and not crafting.info then tiers[#tiers+1] = i			end end --ignore .info elseif from and to then if not tonumber(from) or not tonumber(to) then error("Range is not correct") end from, to = tonumber(from), tonumber(to) if not minion.stats[from] or not minion.stats[to] then error("Range is not correct") end if from > to then from, to = to, from end for i=from,to,1 do tiers[#tiers+1] = i end else local start = minion.stats[1].crafting.info and 2 or 1 for i=start,highestCraftable,1 do tiers[#tiers+1] = i end end -- Use tier list to generate the animated crafting table local recipeBase = {} local recipeB3 = {} local recipeMinion = {} local recipeOutput = {} for key,tier in pairs(tiers) do		local crafting = minion.stats[tier].crafting recipeBase[#recipeBase+1] = crafting.item..","..crafting.num recipeB3[#recipeB3+1] = crafting.B3 or crafting.item..","..crafting.num recipeMinion[#recipeMinion+1] = crafting.base or minionName.." Minion "..TIERS[tier-1] recipeOutput[#recipeOutput+1] = minionName.." Minion "..TIERS[tier] -- special case for mushroom, since all elements must have 2 states, since the "Mushroom" item cycles between 2 states if minionName == "Mushroom" then recipeMinion[#recipeBase] = recipeMinion[#recipeBase]..";"..recipeMinion[#recipeBase] recipeOutput[#recipeBase] = recipeOutput[#recipeBase]..";"..recipeOutput[#recipeBase] end end -- now return the crafting table recipeBase = table.concat(recipeBase,";") recipeB3 = table.concat(recipeB3,";") recipeMinion = table.concat(recipeMinion,";") recipeOutput = table.concat(recipeOutput,";") return ui.craftingTable{ A1 = recipeBase, B1 = recipeBase, C1 = recipeBase, A2 = recipeBase, B2 = recipeMinion, C2 = recipeBase, A3 = recipeBase, B3 = recipeB3, C3 = recipeBase, Output = recipeOutput } end

- -- Template:Minion stats table -- -- Takes all the data from /Data and displays it nicely in a table on the minion page. - function p.minionStatsTable( frame ) --local parameters = frame.args local args = getArgs(frame) local minion = args["minion"] or args[1] or title return frame:preprocess(p._minionStatsTable( minion )) end

function p._minionStatsTable( minionName ) local minion = minionData[minionName] -- B3 has non-standard info for slayer minions local B3Exists = minion.stats[2].crafting.B3	local showBazaarCost = minionName:lower ~= "flower" local totalsBazaarCost = 0 -- We want the cost of cumulative enchanted version local wikitable = mw.html.create('table'):addClass('wikitable') local cumulativeDisplay = string.wrapHtml(string.makeTitle('CUMU','This result is cumulative.'),'sup') -- Header row local row = wikitable:tag("tr") :tag("th"):attr({ rowspan=2 }):wikitext("Tier"):done :tag("th"):attr({ rowspan=2 }):wikitext("Info"):done :tag("th"):wikitext("Total Upgrade Cost"):done if showBazaarCost then row:tag("th"):wikitext("Bazaar Upgrade Cost"):done end row:tag("th"):attr({ colspan=B3Exists and 3 or 2 }):wikitext("Recipe"):done -- Header row (second row) row = wikitable:tag("tr") row:tag("th"):wikitext("Total Cumulative Cost") if showBazaarCost then row:tag("th"):wikitext("Bazaar Cumulat. Cost") end row:tag("th"):wikitext(" Base ") if B3Exists then row:tag("th"):wikitext(" Item (x7) ") row:tag("th"):wikitext(" Extra ") else row:tag("th"):wikitext(" Item (x8) ") end -- Display non-header rows local baseCraftingItem = minion.stats[2].crafting.item local items = minion.items local flexCSS = { display = 'flex', ['justify-content'] = 'space-between', ['align-items'] = 'center' } local function getInfo(i) local stats = minion.stats[i] local crafting = stats.crafting local mainItem = crafting and crafting.item or stats.trade[1].item return stats, crafting, mainItem end for i = 1,#TIERS,1 do		if not minion.stats[i] then break end local stats, crafting, mainItem = getInfo(i) local multi8 = B3Exists and 7 or 8 local actionsPerDay = _actionsPerMinute(minionName, i)*60*24 local unique = {} for j = i,1,-1 do			local stats, crafting, mainItem = getInfo(j) unique[mainItem] = true end local cellTopBrdr = { ["border-top-width"]="2px" } --		-- Calculate info for below --		--Calculate Item Costs local tierCosts = {} if crafting then tierCosts[#tierCosts+1] = (crafting.num*multi8).." "..crafting.item.."" if crafting.base then tierCosts[#tierCosts+1] = "1x "..crafting.base.."" end if crafting.B3 then tierCosts[#tierCosts+1] = "1x "..crafting.B3.."" end elseif stats.trade then for _,itemData in ipairs(stats.trade) do				if itemData.item:lower == "coin" then tierCosts[#tierCosts+1] = itemData.num.." coins" elseif itemData.item:lower == "pelt" then tierCosts[#tierCosts+1] = itemData.num.." pelts" else tierCosts[#tierCosts+1] = itemData.num.."x "..itemData.item.."" end end end -- Find totals for items needed for all tiers up to this one local totals = {} local totalsObj = _getStatTableCumulativeTotal(minionName, i)		for key,value in pairs(totalsObj) do totals[#totals+1] = string._formatNum(math.ceil(value)).." "..key end --Calculate Bazaar Costs local bazaarCost = 0 if showBazaarCost then -- Since crafting and trade data have different formats, list format is needed local items = {} if crafting then items[#items+1] = { item=crafting.item, num=crafting.num*multi8 } if crafting.base then items[#items+1] = { item=crafting.base, num=1 } end elseif stats.trade then items = stats.trade end for _,itemData in ipairs(items) do				local craftNum, craftItem = itemData.num, itemData.item -- Hardcoded workaround for Silver Fang not being purchasable on Bazaar if craftItem == "Silver Fang" then craftNum = craftNum*25 craftItem = "Enchanted Ghast Tear" elseif craftItem == "Melon (block)" then craftNum = craftNum*9 craftItem = "Melon" end -- Handle special items if craftItem:lower == "coin" then -- just increment cost by coin amount bazaarCost = bazaarCost + craftNum elseif craftItem:lower == "pelt" then -- skip else local calculatedPrice = bazaar._calcMaterialBuyPrices({ { craftItem, tonumber(craftNum) } }) bazaarCost = bazaarCost + (tonumber(calculatedPrice) or 0) end end --Totals totalsBazaarCost = totalsBazaarCost + bazaarCost end --		-- First row for this tier --		local class_to_add = ('minion-stat-table-l%d minion-stat-table-r1'):format(table.length(unique)) local row = wikitable:tag("tr"):addClass(class_to_add):attr('id', TIERS[i]) -- Tier + Icon row:tag('th'):attr({ rowspan=2 }):css(cellTopBrdr):wikitext(TIERS[i]..' '..slot{ minionName..' Minion '..TIERS[i] }):done -- Info row:tag("td") :attr({ rowspan=2 }) :css(cellTopBrdr) :wikitext("Cooldown: "..colorModule._colorTemplates("Green", stats.tba.."s")) :wikitext(" Storage: "..colorModule._colorTemplates("Yellow", stats.storage)) :done -- Cost row:tag("td"):css(cellTopBrdr):wikitext(""):done -- Bazaar Cost if showBazaarCost then row:tag("td"):css(cellTopBrdr):wikitext("") end -- Recipe if stats.trade then -- If bought via trade, list data local slots,list = { slot{ minionName.." Minion "..TIERS[i-1] } },{} for _,trd in ipairs(stats.trade) do				if trd.item:lower == "coin" then slots[#slots+1] = ui.slot{trd.num..' coins'} elseif trd.item:lower == "pelt" then slots[#slots+1] = ui.slot{trd.num..' pelts'} else slots[#slots+1] = ui.slot{ trd.item..","..trd.num } end end row:tag("td"):attr({ rowspan=2, colspan=B3Exists and 3 or 2 }):css(cellTopBrdr):css('text-align','center') :wikitext("Merchant: ") :tag("div"):wikitext(table.concat(slots, "")):done :tag("ul"):addClass('lowmargin'):wikitext(""..table.concat(list, "")..""):done elseif crafting.info then -- If there is no crafting recipe, show the details on why row:tag("td"):attr({ rowspan=2, colspan=B3Exists and 3 or 2 }):css(cellTopBrdr):css('text-align','center'):wikitext(crafting.info):done else -- Recipe exists row:tag("td"):attr({ rowspan=2 }):css(cellTopBrdr):css('text-align','center'):wikitext(crafting.base and slot{ crafting.base } or slot{ minionName.." Minion "..TIERS[i-1] }):done local itemColspan = B3Exists and not crafting.B3 and 2 or 1 row:tag("td"):attr({ rowspan=2, colspan=itemColspan }):css(cellTopBrdr):css('text-align','center'):wikitext(slot{ crafting.item..","..crafting.num }):wikitext(itemColspan==2 and " x8" or ""):done if crafting.B3 then row:tag("td"):attr({ rowspan=2 }):css(cellTopBrdr):css('text-align','center'):wikitext(slot{ crafting.B3 }):done end end --		-- Second row for this tier --		class_to_add = ('minion-stat-table-l%d minion-stat-table-r2'):format(table.length(unique)) row = wikitable:tag("tr"):addClass(class_to_add) -- Total cost row:tag("td") :tag('div'):css(flexCSS):wikitext(" "..cumulativeDisplay) -- Bazaar Total Cost if showBazaarCost then row:tag("td") :tag('div'):css(flexCSS):wikitext(" "..cumulativeDisplay) end end return tostring(wikitable) end

-- Find the total number of the base minion items -- returns { {item,num} } function _getStatTableBaseCost(minionName, item, num) local minion = minionData[minionName] local recipes = minion.recipes if not recipes[item] then return (bazaar._getProduct(item) or not not differentList[item:lower]) and { {item, num} } or {} end local items = {} for key,value in ipairs(recipes[item]) do		local subitems = _getStatTableBaseCost(minionName, value[1], value[2]*num) for key2,value2 in ipairs(subitems) do			items[#items+1]=value2 end end return items end

-- Find total of base minion items for all tiers up to the one passed in function _getStatTableCumulativeTotal(minionName, tier) local minion = minionData[minionName] local totals = {} for i = 1,tier,1 do		local stats = minion.stats[i] local items = {} if stats.crafting then local crafting = stats.crafting local multi8 = crafting.B3 and 7 or 8 items = { { item=crafting.item, num=crafting.num*multi8 } } if crafting.base then items[2] = { item=crafting.base, num=1 } end elseif stats.trade then items = stats.trade end local cost = {} for _, itemData in ipairs(items) do			cost = table.merge(cost, _getStatTableBaseCost(minionName, itemData.item, itemData.num)) end for _, value in ipairs(cost) do			local item = value[1] totals[item] = (totals[item] or 0) + value[2] end end return totals end

- -- Template:MinionProfitTable -- -- Shows a table with resources collected by the minion alongside their bazaar sell prices - function p.minionProfitTable(frame) local args = getArgs(frame) local minion = args[1] or args["minion"] or title if not minion then minion = mw.title.getCurrentTitle.text end minion = minion:gsub('%s*[Mm]inion', '') return frame:preprocess(p._minionProfitTable(minion)) end

function p._minionProfitTable(minion, isOffline) local produce = minionData[minion].items local tba = minionData[minion].stats local items = {} --get drops produced by the minion for i=1,25,1 do --25 because some minions like flower minion have very large amount of different drops if produce[i] and produce[i].condition == nil then items[i] = produce[i] elseif produce[i] and produce[i].condition == "Auto Smelter" and produce[i].item ~= "Cactus Green" then items[i] = produce[i] table.remove(items, i-1) elseif produce[i] and produce[i].condition == "Flint Shovel" then items[i] = produce[i] table.remove(items, i-1) end end --compose the header of the table local wikitable = mw.html.create('table'):addClass('wikitable') wikitable:tag('caption'):wikitext("Listed below are the profits of this minion when its items are sold to Bazaar. No fuel and no diamond spreading are used.", ' ', 'Values are shown when the player is online.') wikitable:tag('tr') :tag('th'):wikitext('Tier'):attr({ rowspan=2 }):done :tag('th'):wikitext('Harvests per minute '):attr({ rowspan=2 }):done :tag('th'):wikitext('Per hour'):attr ({ colspan=2 }):done :tag('th'):wikitext('Per day'):attr ({ colspan=2 }):done :done wikitable:tag('tr') :tag('th'):wikitext('Items'):done :tag('th'):wikitext('Bazaar Profit'):done :tag('th'):wikitext('Items'):done :tag('th'):wikitext('Bazaar Profit'):done :done -- add minion produce and profits to the table for i=1,#TIERS,1 do 		if not minionData[minion].stats[i] then return tostring(wikitable) end local tba = minionData[minion].stats[i].tba --list items not possible to sell at bazaar local non_bazaar_items = { ["Poisonous Potato"] = 10, ['White Wool'] = 2, ['Clownfish'] = 20, }		--create the string displayed in the "produce" column (compatible with ) local resourceStr = {} --get first item resourceStr[1] = "*"..math.floor(p._producePerMinute(items[1].avg, tba)*60).." "..items[1].item --get rest of items, if applicable for j=2,#items,1 do resourceStr[#resourceStr+1] = "\n*"..math.floor(p._producePerMinute(items[j].avg, tba)*60).." "..items[j].item end resourceStr = table.concat(resourceStr) --create string for profit calculation (compatible woth ) local bazaarStr = {} --get first item bazaarStr[1] = "*"..(p._producePerMinute(items[1].avg, tba)*60).." "..items[1].item --get rest of items, if applicable for j=2,#items,1 do			if non_bazaar_items[items[j].item] then bazaarStr[#bazaarStr+1] = "\n*"..(p._producePerMinute(items[j].avg, tba)*60)*non_bazaar_items[items[j].item] else bazaarStr[#bazaarStr+1] = "\n*"..(p._producePerMinute(items[j].avg, tba)*60).." "..items[j].item end end bazaarStr = table.concat(bazaarStr) --same thing as above but for the 24 hour section local resourceStr_24 = {} resourceStr_24[1] = "*"..math.floor(p._producePerMinute(items[1].avg, tba)*60*24).." "..items[1].item for j=2,#items,1 do resourceStr_24[#resourceStr_24+1] = "\n*"..math.floor(p._producePerMinute(items[j].avg, tba)*60*24).." "..items[j].item end resourceStr_24 = table.concat(resourceStr_24) local bazaarStr_24 = {} bazaarStr_24[1] = "*"..(p._producePerMinute(items[1].avg, tba)*60*24).." "..items[1].item for j=2,#items,1 do			if non_bazaar_items[items[j].item] then bazaarStr_24[#bazaarStr_24+1] = "\n*"..(p._producePerMinute(items[j].avg, tba)*60*24)*non_bazaar_items[items[j].item] else bazaarStr_24[#bazaarStr_24+1] = "\n*"..(p._producePerMinute(items[j].avg, tba)*60*24).." "..items[j].item end end bazaarStr_24 = table.concat(bazaarStr_24) --get harvests per minute local hpm if minion == "Pumpkin" or minion == "Melon" or minion == "Fishing" then hpm = string.roundNumber(60/(tba), 2) else hpm = string.roundNumber(60/(tba*2), 2) end --add the table cells and fill them with the data from above wikitable:tag('tr') :tag('th'):wikitext(string._toRoman(i)..' '..ui.slot{ minion..' Minion '..string._toRoman(i) }):done :tag('td'):wikitext(' "):done			:tag('td'):wikitext(""):done			:tag('td'):wikitext(""):done			:tag('td'):wikitext(""):done			:tag('td'):wikitext(""):done		:done	end	return tostring(wikitable) end

function p._producePerMinute(amount, tba) -- ACTIONS_PER_MINUTE = 60/TBA*2 -- PRODUCE_PER_MINUTE = ACTIONS_PER_MINUTE * AVG_AMOUNT return 60/(tba*2)*amount end

- -- Template:Minion Profit -- -- Shows the average bazaar sell price for a specific minion setup - function p.minionProfit(frame) local args = getArgs(frame) local minion = args[1] or args["minion"] or title local tier = args["tier"] or args["t"] local diam_spread = yesno(args["diamond_spreading"] or args["ds"], false) local fuel = args["fuel"] or args["f"] local crystal = yesno(args["crystal"] or args["c"]) local bonus = args["bonus"] or args["other"] local expander = args["expander"] or args["e"] local web = args["flycatcher"] or args["fly"] or args["web"] local sc3000 = yesno(args["sc3000"], false) local input_time = args["input_time"] or args["time"] return p._minionProfit(minion, tier, diam_spread, fuel, crystal, bonus, expander, web, sc3000, input_time, true) end

function p._minionProfit(minion, tier, diam_spread, fuel, crystal, bonus, expander, web, sc3000, input_time, coin) local minionData = require('Module:Minion/Data') if not minionData[minion] then return _error('Invalid minion: '.. minion) end --remove "Minion" from minion name if minion then minion = minion:gsub("(%w)(%w+)", function(t1, t2) return t1:upper..t2:lower end) minion = minion:gsub("%s+?[Mm]inion", "") end local produce = minionData[minion].items local tba = minionData[minion].stats --set time to 1h if not specified if not input_time then input_time = 1 end --set tier to max tier if not specified. convert tier to arabic local maxTier = table.length(minionData[minion].stats) if tier then if tonumber(tier) then tier = tonumber(tier) if tier > maxTier then return nil end else tier = string._toArabic(tier) end else tier = maxTier end local fuels = minionAliases.fuelData if fuel then fuel = minionAliases.fuelAliases[fuel:lower] or error('Invalid fuel "' .. fuel .. '"') end -- 'fuel' gets the fuel multiplier fuel = fuels[fuel] or 0 --check if some other params exist; if not set to default if not expander then expander = 0 else expander = tonumber(expander) end if not web then web = 0 else web = tonumber(web) end if not bonus then bonus = 0 else bonus = tonumber(bonus) end -- if not crystal then crystal = 0 else crystal = 0.1 end if input_time then input_time = tonumber(input_time) or error('Invalid time "'..input_time..'"') end

--get items produced by minion local items = {} for i=1,25,1 do --25 because some minions like flower minion have very large amount of different drops if produce[i] and produce[i].condition == nil then items[i] = produce[i] elseif produce[i] and produce[i].condition == 'Auto Smelter' and produce[i].item ~= 'Cactus Green' then items[i] = produce[i] table.remove(items, i-1) elseif produce[i] and produce[i].condition == 'Flint Shovel' then items[i] = produce[i] table.remove(items, i-1) end end local function getMultiplier(multiplier, boost) -- note: 'boost' should be passed as a multiple, in other words, (1+increase%) return boost == 0 and multiplier or multiplier * boost end --get minion tba local tba, multiplier = minionData[minion].stats[tier].tba, 1 --calculate new tba multiplier = getMultiplier(multiplier, fuel) for i = 1, expander>2 and 2 or expander<0 and 0 or expander, 1 do		multiplier = getMultiplier(multiplier, 1.1) end for i = 1, web>2 and 2 or web<0 and 0 or web, 1 do		multiplier = getMultiplier(multiplier, 1.1) end if crystal then -- check if minion can use crystal local validMinions = minionAliases.crystalValid if table.find(validMinions, minion:lower) then multiplier = getMultiplier(multiplier, 1.1) end end if bonus then multiplier = getMultiplier(multiplier, bonus) end --calculate final produce collected local totalProduce = {} for i,item in pairs(items) do		totalProduce[#totalProduce+1] = { name = item.item, num = p._producePerMinute(item.avg, tba/multiplier)*60*input_time }	end --add diamond spreading diamonds if necessary if diam_spread then totalProduce[#totalProduce+1] = { name = "Diamond", num = p._producePerMinute(1, tba/multiplier)*60*input_time/10 }	end -- If a sc3000 is being used, use enchanted forms for more accurate price if sc3000 then local recipes = minionData[minion].recipes local ingr, numToBeCompacted for i,item in pairs(totalProduce) do			-- Go down recipe list and see if this item can be upgraded for rName,rIngrList in pairs(recipes) do				-- If recipe has more than one ingredient sc3000 doesn't work on it					-- Some items (such as Silver Fang) are ignored due to not having Bazaar support if #rIngrList == 1 and (rName ~= "Silver Fang" and rName ~= "Melon (block)") then ingr = rIngrList[1] if ingr[1] == item.name and item.num >= ingr[2] then -- integer division, since we only want a whole number of craftable amount numToBeCompacted = math.floor(item.num / ingr[2]) -- if item can be compacted then add new item to list and subtract count away from original recipe ingredient local n = #totalProduce+1 totalProduce[n] = { name=rName, num=numToBeCompacted } item.num = item.num % ingr[2] -- update list with the remainder item = totalProduce[n] end end end end end --get merchant sell pricess for non-bazaar items local non_bazaar_items = nonBazaarMerchantSellPrices if minion == 'Flower' then non_bazaar_items[''] = 1 end --make a string that can be used in 	local bazaarValues = {} for i,item in pairs(totalProduce) do		if non_bazaar_items[item.name] then -- If not in the bazaar calculate the shop sell price bazaarValues[#bazaarValues+1] = item.num*non_bazaar_items[item.name] else bazaarValues[#bazaarValues+1] = {item.name, item.num} end end --finish return bazaar._calcMaterialBuyPrices(bazaarValues, "buy", coin); end

- -- invoked directly -- -- more or less p.minionProfit, just in table for each minion - function p.minionsPageProfitTable(frame) local args = getArgs(frame) local diam_spread = yesno(args["diamond_spreading"] or args["ds"], false) local fuel = args["fuel"] or args["f"] local crystal = yesno(args["crystal"] or args["c"]) local bonus = args["bonus"] or args["other"] local expander = args["expander"] or args["e"] local web = args["flycatcher"] or args["fly"] or args["web"] -- These two have defaults different from normal profit code due to minion page local sc3000 = yesno(args["sc3000"], true) local input_time = args["input_time"] or args["time"] or 24 return p._minionsPageProfitTable(diam_spread, fuel, crystal, bonus, expander, web, sc3000, input_time) end

function p._minionsPageProfitTable(diam_spread, fuel, crystal, bonus, expander, web, sc3000, input_time) -- Needed due to calculator if fuel == "[none]" or fuel == "none" then fuel = nil end local wikitable = mw.html.create('table'):addClass('wikitable sortable searchable') wikitable:tag("caption"):wikitext("Profit per "..input_time.." hours") wikitable:tag("tr") :tag("th"):wikitext("Minion"):done :tag("th"):attr( 'data-sort-type', 'number' ):wikitext("Tier 1"):done :tag("th"):attr( 'data-sort-type', 'number' ):wikitext("Tier 3"):done :tag("th"):attr( 'data-sort-type', 'number' ):wikitext("Tier 5"):done :tag("th"):attr( 'data-sort-type', 'number' ):wikitext("Tier 7"):done :tag("th"):attr( 'data-sort-type', 'number' ):wikitext("Tier 9"):done :tag("th"):attr( 'data-sort-type', 'number' ):wikitext("Tier 11"):done :tag("th"):attr( 'data-sort-type', 'number' ):wikitext("Tier "..#TIERS):done local function getPrice(minion, tier) return p._minionProfit(minion, tier, diam_spread, fuel, crystal, bonus, expander, web, sc3000, input_time, false) end -- Store as a list first so we can sort it	local rowData = {} for minion,_ in pairs(minionData) do		local minionText = minion if minion=="Chicken" then minionText = minionText..'ⓘ' end if minion=="Gravel" then minionText = minionText..'ⓘ' end rowData[#rowData+1] = { minionText, getPrice(minion, 1), getPrice(minion, 3), getPrice(minion, 5), getPrice(minion, 7), getPrice(minion, 9), getPrice(minion, 11), getPrice(minion, #TIERS) } end -- Now sort minion from best to worst in terms of profit table.sort(rowData, function(a,b) 		if type(a[6]) == 'string' or type(b[6]) == 'string' then			return false		else			return a[6] > b[6]		end	end) local function makeCoin(num) if tonumber(num) == nil then return string.blankCell end return tostring(currency._coins(math.ceil(num))) end -- now add rows to the table for _,data in ipairs(rowData) do		wikitable:tag("tr") :tag("td"):wikitext(data[1]):done :tag("td"):wikitext(makeCoin(data[2])):done :tag("td"):wikitext(makeCoin(data[3])):done :tag("td"):wikitext(makeCoin(data[4])):done :tag("td"):wikitext(makeCoin(data[5])):done :tag("td"):wikitext(makeCoin(data[6])):done :tag("td"):wikitext(makeCoin(data[7])):done :tag("td"):wikitext(data[8] and makeCoin(data[8]) or string.blankCell):done end return tostring(wikitable) end

- -- Template: Minion Drops Table -- -- Returns a table with minion drops - function p.minionDropsTable(frame) local args = getArgs(frame) local minion = args.minion or args.m or args[1] or title return p._minionDropsTable(minion) end

function p._minionDropsTable(minion) local notBazaarItems = { ['Cactus Green'] = 1, ['White Wool'] = 2, ['Egg'] = 3, ['Poisonous Potato'] = 10, ['Iron Ore'] = 3, ['Gold Ore'] = 3, }	minion = minion:lower minion = minion:match('(.+) minion') or minion local needsToBeDividedByTwo = minion == "fishing" and true or false local dropData = minionData[string._ucfirst(minion)].items local hasNotBazaarItem, hasCondition = {false, false} for _,v in pairs(dropData) do		if notBazaarItems[v.item] then hasNotBazaarItem = true end if v.condition then hasCondition = true	 end end local wikitable = mw.html.create('table'):addClass('wikitable') local row = wikitable:tag('tr') row:tag('th'):attr({rowspan=2, colspan=2}):wikitext('Items'):done row:tag('th'):attr({rowspan=2}):wikitext('Chance'):done row:tag('th'):attr({rowspan=2}):wikitext('Average per harvest'):done if hasCondition then row:tag('th'):attr({rowspan=2}):wikitext('Condition'):done end row:tag('th'):attr({rowspan=2}):wikitext('Experience per Item'):done row:tag('th'):attr({colspan=2}):wikitext(hasNotBazaarItem and 'Bazaar/Merchant Sell Price' or 'Bazaar Sell Price'):done row:done row = wikitable:tag('tr') row:tag('th'):wikitext('1 Item'):done row:tag('th'):wikitext('Stack'):done row:done for _,v in pairs(dropData) do		row = wikitable:tag('tr') row:tag('td'):wikitext(ui.slot{ v.item }):done row:tag('td'):wikitext(..v.item..):done local chance = '100%' if v.avg < 1 then if needsToBeDividedByTwo then chance = (v.avg/2*100)..'%' else chance = (v.avg*100)..'%' end end row:tag('td'):wikitext(chance):done row:tag('td'):wikitext(v.avg):done if v.condition then row:tag('td'):wikitext(itemModule._itemDisplay(v.condition)):done elseif hasCondition then row:tag('td'):wikitext(string.blankCell):done end if v.exp then row:tag('td'):wikitext(v.exp):done else row:tag('td'):wikitext('?'):done end if notBazaarItems[v.item] then row:tag('td'):wikitext(tostring(currency._coins(notBazaarItems[v.item]))):done row:tag('td'):wikitext(tostring(currency._coins(notBazaarItems[v.item]*64))):done else row:tag('td'):wikitext(tostring(bazaar._calcMaterialBuyPrices(, 'buy', true))):done row:tag('td'):wikitext(tostring(bazaar._calcMaterialBuyPrices(, 'buy', true))):done end row:done end return tostring(wikitable) end

-- Template:Days using minions collection -- -- Shows a table with days to acquire each tier of a collection with a minion

function p.minionCollectionDaysTable( frame ) --local parameters = frame.args local args = getArgs(frame) local coll = args["collection"] or args["coll"] or args[1] local minion = args["minion"] return p._minionCollectionDaysTable( coll, minion ) end

function p._minionCollectionDaysTable( coll, minion ) local function handleMinion local mins = {} for c,dt in pairs(collectionData) do			if dt.minion and dt.minion == (minion or coll) then table.push(mins, c)			end end if #mins == 0 then return else return mins end end if minion then local t = handleMinion if t then return table.concat(table.map(t, function(m) return p._minionCollectionDaysTable(m) end),'\n') end end coll = coll or minion if not collectionData[string.ucfirst(coll)] then error('Invalid collection or minion: '..coll) end local minion = collectionData[string.ucfirst(coll)].minion if not minion then error('Collection '..coll..' cannot be obtained by minions.') end local wikitable = mw.html.create('table'):addClass('article-table') wikitable:tag('caption'):wikitext(string.format( 'Time needed to acquire each tier of the %s Collection using a %s Minion (by tier, no fuel) If you have multiple minions, just divide the number of days by how many minions you have. ',		string.ucfirst(coll), minion)) local highestTier, _ = peakStat(minion) local row = wikitable:tag('th'):wikitext('Minion Tier'):done for i = 1,highestTier,1 do		row:tag('th'):cssText('font: bold 100% times new roman;'):wikitext(string._toRoman(i)):done end for i,ctier in ipairs(collectionData[coll]) do		local row = wikitable:tag('tr') row:tag('th'):wikitext(			string.wrapHtml(('%s %s'):format(string.ucfirst(coll), string._toRoman(i)),'span', {style = 'font: bold 100% times new roman;'})			..' '			..string.wrapHtml(('(%s)'):format(ctier.required),'font', {style = 'color: #55FF55;'} )		):done for mtier = 1,highestTier,1 do			local actPerHour = _actionsPerMinute(minion, mtier)*60 local durationHr = ctier.required/actPerHour if durationHr < 24 then row:tag('td'):wikitext(('%.2f'):format(durationHr)..'h') else row:tag('td'):wikitext(('%.2f'):format(durationHr/24)..'d') end end end return tostring(wikitable) end

- -- Template: Minion Recipes UI -- -- Creates a minion recipes UI - function p.minionRecipesUI(frame) return p._minionRecipesUI(table.deepCopy(getArgs(frame))) end

- -- function: minionRewardsUI(args?: table|frame) -- -- Invoked by. - function p._minionRecipesUI(args) checkType(1, args, 'table') local col = (args["collection"] or args[1] or titleObj.rootText):gsub('%s[Mm]inion', '') return ui._collectionRewardsUI{ col, args["reward_tier"] or args[2] or 1, pipeline(		*%s Minion $n	, string.dedent, col, string.format, 11, string._repeat):gsub('(%d+)', function(m) return string._toRoman(m) end), hide=yesno(args['hide'], true), id=args['id'], return_id=args['return_id'] or args['return-id'], return_text=args['return_text'], } end

- -- Template:Minion ideal layout -- -- Makes a table of to form the in-game ideal layout for a minion - function p.minionIdealLayoutTable( frame ) --local parameters = frame.args local args = getArgs(frame) local minion = args["minion"] or args[1] or title local ideal = args["ideal"] local border = args["border"] local size = 5; local slots = {} local rowLetter = { 'A', 'B', 'C', 'D', 'E', } for y = 1,size,1 do		slots[y] = {} for x = 1,size,1 do			slots[y][x] = args[rowLetter[x]..y]		end end return p.minionIdealLayout( minion, size, ideal, border, slots ) end

function p.minionIdealLayout( minion, size, ideal, border, customSlots ) if border then size = size + 2 end local table = mw.html.create('table'):addClass('mcui mcui-Crafting_Table pixel-image'):css({['margin']='0 auto', ['display']='table'}) for y = 1,size,1 do		local row = table:tag("tr"):addClass('mcui-row') for x = 1,size,1 do			local img if border and (y==1 or y==size or x==1 or x==size) then img = border elseif x==math.ceil(size/2) and y==math.ceil(size/2) then img = "["..minion.."]"..minion.." I"				if customSlots[y][x] then img = img..";"..customSlots[y][x] end else img = border and (customSlots[y-1][x-1] or ideal) or (customSlots[y][x] or ideal) end row:tag("td"):wikitext( slot{ img } ):done end row:done end table:done return tostring(table) end

- -- Template:Minion UI -- -- Makes a table of to form the in-game UI of a minion - function p.minionUI( frame ) --local parameters = frame.args local args = getArgs(frame) local minion = args["minion"] or args[1] or title return p._minionUI( minion ) end

function p._minionUI( minion ) local name, tier = minion:match('(.-)%s?[Mm]?[Ii]?[Nn]?[Ii]?[Oo]?[Nn]?%s([%divxIVX]+)') if tier then tier = tonumber(tier) or string._toArabic(tier) else name = minion:gsub('%s*[Mm][Ii][Nn][Ii][Oo][Nn]', '') tier = 1 end if not minionData[name] then error("Minion name "..name.." is not valid") end if not minionData[name].stats[tier] then error(name.."Minion has no such tier (Tier "..tier..")") end local highestTier, highestCraftable = peakStat(name) local storageTier = {} for slotno = 1,15,1 do -- Minions have 15 total slots for y = 1,10,1 do -- All slots unlocked at tier 10 local avail = minionData[name].stats[y].storage / 64 storageTier[slotno] = y			if avail >= slotno then break end end end local function available( num ) if tier >= storageTier[num] then return '' -- empty slot else return ('White Stained Glass Pane, none, &eStorage unlocked at tier %s, none'):format(string._toRoman(storageTier[num])) end end local title = name..' Minion '..string._toRoman(tier) local titleLink = name..' Minion#'..string._toRoman(tier) local description = minionData[name].description or 'No Description' local upgradeMessage = '' local upgradeMessageView = '' local upgradeMessageUpgrade = '' if tier == highestTier then upgradeMessage = '&aHighest tier has been reached!' upgradeMessageView = 'The highest tier of this minion/has been reached//'..upgradeMessage upgradeMessageUpgrade = "&cThis minion has reached the/maximum tier." elseif tier == highestCraftable then local tradenpc = minionData[name].stats[tier+1].tradeNpc upgradeMessage = '&aHighest craftable tier has been/reached!' upgradeMessageView = 'The highest craftable tier of/this minion has been reached/Talk to '..(NPCtext[tradenpc] and NPCtext[tradenpc].name or tradenpc)..' &rin the/'..(NPCtext[tradenpc] and NPCtext[tradenpc].loc or '(unknown)')..' &rto unlock the/next tier!//'..upgradeMessage upgradeMessageUpgrade = "&cThis minion has reached the/maximum tier." elseif minionData[name].stats[tier + 1].storage > minionData[name].stats[tier].storage then upgradeMessage = '&7Time Between Actions: &e'..minionData[name].stats[tier].tba..'s/&7Max Storage: &8'..minionData[name].stats[tier].storage..' ➜ &a'..minionData[name].stats[tier + 1].storage upgradeMessageView = '&7View the items required to/upgrade this minion to the/next tier.//'..upgradeMessage..'//&eClick to view!' upgradeMessageUpgrade = upgradeMessage..'//&eClick to upgrade!' else upgradeMessage = '&7Time Between Actions: &8'..minionData[name].stats[tier].tba..'s ➜ &a'..minionData[name].stats[tier + 1].tba..'s/&7Max Storage: &e'..minionData[name].stats[tier].storage upgradeMessageView = '&7View the items required to/upgrade this minion to the/next tier.//'..upgradeMessage..'//&eClick to view!' upgradeMessageUpgrade = upgradeMessage..'//&eClick to upgrade!' end local uitable = { title, arrow = 'none', close = 'none', dnl = true, ['1,4'] = 'Redstone Torch, none, &aIdeal Layout, &7View the most effecient spot for/this minion to be placed in.', ['1,5'] = ('%s, none;%s, &9%s, &7%s//&7Time Between Action: &a%ss/&7Max Storage: &e%s/&7Resources Generated: &b0'):format(			title, titleLink, title, description,			minionData[name].stats[tier].tba, minionData[name].stats[tier].storage		), ['1,6'] = 'Gold Ingot, none, &aNext Tier, '..upgradeMessageView, ['2,2'] = 'Lime Stained Glass Pane, none, &aMinion Skin Slot, &7You can insert a Minion Skin/here to change the appearance of/your minion.', ['3,2'] = 'Orange Stained Glass Pane, none, &aFuel, &7Increase the speed of your/minion by adding minion fuel/items here.//&cNote: &7You can\'t take/fuel back out after you/place it here!', ['4,2'] = 'Blue Stained Glass Pane, none, &aAutomated Shipping, Add a &bBudget Hopper&7,/&bEnchanted Hopper&7 or a/&bPerfect Hopper&7 here to make/your minion automatically sell/generated items after its/inventory is full.', ['5,2'] = 'Yellow Stained Glass Pane, none, &aUpgrade Slot, &7You can improve your minion by/adding a minion upgrade item/here.', ['6,2'] = 'Yellow Stained Glass Pane, none, &aUpgrade Slot, &7You can improve your minion by/adding a minion upgrade item/here.', ['6,4'] = 'Chest, none, &aCollect All, &eClick to Collect all items!', ['6,6'] = 'Diamond, none, &aQuick-Upgrade Minion, &7Click here to upgrade your/minion to the next tier.//'..upgradeMessageUpgrade, ['6,9'] = 'Bedrock, none, &aPickup Minion, &eClick to pickup!', }	for i = 0,14,1 do		uitable[('%d,%d'):format(math.floor(i/5)+3, i%5+4)] = available(i+1) end return tostring(ui.ui(uitable)) end

-- Finish Module -- return p