Module:Minion

local p = {}

local getArgs = require('Module:Arguments').getArgs local mergeArgsSyntax = require('Module:Arguments').mergeArgsSyntax local loader = require('Module:Loader') local Apidata = require('Module:Item/ApiData')

local string, table, yesno, inventoryslot, bazaar, colorModule, currency, itemModule, ui = loader.require('String', 'Table', 'Yesno', 'Inventory slot', 'Bazaar', 'Color', 'Currency', 'Item', 'UI') local minionData, collectionData, minionAliases = loader.loadData('Minion/Data', 'Collection/Data', 'Minion/Aliases')

local _error = string._error

local slot = inventoryslot.slot -- 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'], false) local fullwidth = yesno(args['fullwidth'] or args['noscroll'], false) return p._minionPerDayTable( minion, amount, item, split, fullwidth ) end

function p._minionPerDayTable( minion, amount, item, split, fullwidth ) if not minionData[minion] then error('Invalid minion '..minion) end local HALF_ROW = 6 local actionsPerDay, 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 fullwidth 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 fullwidth 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(days * 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 fullwidth and tostring(_table) or scrollable end

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

function p.minionPerDayTableCombined( frame ) local args = getArgs(frame) local lang = mw.language.getContentLanguage local split = yesno(args['split'], false) local fullwidth = yesno(args['fullwidth'] or args['noscroll'], false) -- 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, fullwidth) end

function p._minionPerDayTableCombined(entries, split, fullwidth) 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, fullwidth))			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, fullwidth)) end end result = ' ' .. table.concat(result) .. ' '	local frame = mw.getCurrentFrame return frame:preprocess(result) 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 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 = crafting and (crafting.B3 and 7 or 8) or nil 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 = ('article-msTable-l%d article-msTable-r1 article-row-main'):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 = ('article-msTable-l%d article-msTable-r2 article-row-bound'):format(table.length(unique)) row = wikitable:tag('tr'):addClass(class_to_add) -- Total cost row:tag('td') :tag('div'):addClass('article-msTable-cumulative'):wikitext(' '..cumulativeDisplay) -- Bazaar Total Cost if showBazaarCost then row:tag('td') :tag('div'):addClass('article-msTable-cumulative'):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 with ) 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'] local selltobazaar = yesno(args['selltobazaar'] or args['bazsell'], true) return p._minionProfit(minion, tier, diam_spread, fuel, crystal, bonus, expander, web, sc3000, input_time, selltobazaar, true) end

function p._minionProfit(minion, tier, diam_spread, fuel, crystal, bonus, expander, web, sc3000, input_time, selltobazaar, coin) local data = minionData[string.ucfirst(minion)] if not data 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 = data.items local tba = data.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(data.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 = data.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 = data.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] elseif yesno(selltobazaar, false) then bazaarValues[#bazaarValues+1] = {item.name, item.num} else item.name = item.name:gsub('%s+', '_') item.name = item.name:upper local response = itemModule._api(item.name) if response then bazaarValues[#bazaarValues+1] = item.num * (response.npc_sell_price) else error(('Item API search: Invalid item name %s'):format(item.name)) end end end --finish return bazaar._calcMaterialBuyPrices(bazaarValues, 'sell', coin); end

- -- Template:Minions page profit table -- -- 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

-- DEBUG function p.getPrice(minion, tier) return { p._minionProfit(minion, tier, diam_spread, fuel, crystal, bonus, expander, web, sc3000, input_time, false, false), p._minionProfit(minion, tier, diam_spread, fuel, crystal, bonus, expander, web, sc3000, input_time, true, false) } 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, false) or 'None', p._minionProfit(minion, tier, diam_spread, fuel, crystal, bonus, expander, web, sc3000, input_time, true, false) or 'None' }	end -- Store as a list first so we can sort it	local rowData = {} for minion,_ in pairs(minionData) do		local minionText = table.concat{ mw.getCurrentFrame:expandTemplate{ title = 'MinionName', args = {minion, short='y'} }, minion == 'Chicken' and 'ⓘ' or minion == 'Gravel' and 'ⓘ' or '', }		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][1]) == 'string' or type(b[6][1]) == 'string' then			return false		else			return a[6][1] > b[6][1]		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		local row = wikitable:tag('tr') :tag('td'):wikitext(data[1]):done for count = 2, 7 do			row:tag('td'):wikitext(('%s (%s: %s) '):format( makeCoin(data[count][1]), string.makeTitle('BZP', 'Price when sold to bazaar.'), makeCoin(data[count][2]) )):done end row: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(, 'sell', true))):done row:tag('td'):wikitext(tostring(bazaar._calcMaterialBuyPrices(, 'sell', 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