Module:DropTable: Difference between revisions

From Modded Wiki
Jump to navigation Jump to search
Carter57 (talk | contribs)
Attempted fix
Carter57 (talk | contribs)
Undo revision 14095 by Carter57 (talk)
Tag: Undo
 
Line 755: Line 755:
args = { group = i18n.refgroup }
args = { group = i18n.refgroup }
}))
}))
end
local function setBucketData(datas, args)
args.name = args.name or mw.title.getCurrentTitle().text
local image, link = getImageAndLink(args, 'mob')
for _, data in ipairs(datas) do
local jsonData = mw.text.jsonEncode({
name = args.name,
image = image,
linktext = link,
java = data.java,
bedrock = data.bedrock,
notes = data.notes
}, mw.text.JSON_PRESERVE_KEYS)
local bucketObject = {
item = data.name,
json = jsonData
}
bucket(i18n.bucket).put(bucketObject)
end
end
end



Latest revision as of 02:49, 2 June 2026

This module unfortunately does not work.

"Lua error at line 825: attempt to call global 'setBucketData' (a nil value)."


local i18n = {
	imageSprite = {
		item = 'Invicon',
		mob = 'EntitySprite'
	},
	headerThing = {
		item = 'Item',
		mob = 'Mob'
	},
	headerWithoutLooting = 'Default',
	headerLooting1 = 'Looting I',
	headerLooting2 = 'Looting II',
	headerLooting3 = 'Looting III',
	headerOverview = 'Quantity / Chance / Average',
	headerDropAmount = 'Amount',
	headerDistribution = 'Probability',
	headerAverage = 'Average',
	headerTotal = {
		item = 'Killed',
		mob = 'Drops'
	},
	headerExpectation = {
		item = 'Expected Drops',
		mob = 'Expected Number of Kills'
	},
	tabOverviewDecimal = 'Decimal',
	tabOverviewFraction = 'Fraction',
	tabDistribution = 'Distribution',
	tabCalculator = 'Expectation',
	titlejava = "''[[Java Edition]]'':",
	titlebedrock = "''[[Bedrock Edition]]'':",
	notePlayerOrPet = 'Only when killed by a [[player]] or a tamed [[wolf]].',
	notePlayerOnly = 'Only when killed by a [[player]].',
	noteBurnOrFireAspect = 'Only when on fire or killed with a weapon enchanted with [[Fire Aspect]].',
	noteBurnOnly = 'Only when on fire.',
	noteNotBurnOrFireAspect = "Only when ''not'' on fire and ''not'' killed with a weapon enchanted with [[Fire Aspect]].",
	noteNotBurnOnly = "Only when ''not'' on fire.",
	refgroup = 'FN', -- [[MediaWiki:Cite link label group-FN]]
	style = 'DropTable/styles.css', -- [[Template:DropTable/styles.css]]
	bucket = 'droptable', -- [[Bucket:droptable]]
}

------------------

local Frac = {}

-- Defined types:
-- * frac: { numerator = numerator, denominator = denominator }, used on Frac.*( ..., fraction, ... )

local function GCD( x, y )	-- get Greatest Common Divisor (GCD)
	-- Test
	-- gcd( 0, 0 ) = undef
	if type( x ) ~= 'number' or type( y ) ~= 'number' or ( x == 0 and y == 0 ) then
		return nil
	end

	x = math.abs( x )
	y = math.abs( y )

	-- Improve precision
	local multiplier = 1
	while x % 1 ~= 0 or y % 1 ~= 0 do
		x = x * 10
		y = y * 10
		multiplier = multiplier * 10
	end

	local function euclidean( x, y )
		return y == 0 and x or euclidean( y, x % y )
	end

	return euclidean( x, y ) / multiplier
end

local function LCM( x, y )	-- get Least Common Multiple (LCM)
	-- Test
	local GCD = GCD( x, y )
	if not GCD then
		return nil
	end

	return x * y / GCD
end

-- Start of Frac: Fraction Library
local fracMetatable = {
	__index    = Frac,
	__add      = function ( fraction1, fraction2 ) return Frac.add( fraction1, fraction2 )      end,
	__sub      = function ( fraction1, fraction2 ) return Frac.sub( fraction1, fraction2 )      end,
	__mul      = function ( fraction1, fraction2 ) return Frac.mul( fraction1, fraction2 )      end,
	__div      = function ( fraction1, fraction2 ) return Frac.div( fraction1, fraction2 )      end,
	__pow      = function ( fraction, n )          return Frac.pow( fraction, n )               end,
	__unm      = function ( fraction )             return Frac.neg( fraction )                  end,
	__eq       = function ( fraction1, fraction2 ) return Frac.cmp( fraction1, fraction2 ) == 0 end,
	__lt       = function ( fraction1, fraction2 ) return Frac.cmp( fraction1, fraction2 ) < 0  end,
	__le       = function ( fraction1, fraction2 ) return Frac.cmp( fraction1, fraction2 ) <= 0 end,
	__tostring = function ( fraction )             return Frac.display( fraction )              end,
}

-- Tests
function Frac.isFrac( fraction )	-- Check if it is a valid fraction
	return not ( type( fraction ) ~= 'table' or not fraction.numerator or not fraction.denominator or fraction.denominator <= 0 or fraction.numerator % 1 ~= 0 or fraction.denominator % 1 ~= 0 )
end

-- Create a fraction directly
function Frac.new( numerator, denominator )
	if type( numerator ) ~= 'number' or denominator and ( type( denominator ) ~= 'number' or denominator == 0 ) then
		return nil
	end
	local fraction = denominator and ( denominator < 0 and { numerator = - numerator, denominator = - denominator } or { numerator = numerator, denominator = denominator } ) or Frac.fromDec( numerator )
	if Frac.isFrac( fraction ) == false then
		return nil
	end
	setmetatable( fraction, fracMetatable )
	return fraction
end

function Frac.fromDec( decimal )	-- Convert decimal to fraction
	if type( decimal ) ~= 'number' then
		return nil
	end

	local numerator = decimal
	local denominator = 1
	while #tostring(numerator % 1) ~= 1 do
		numerator = numerator * 10
		denominator = denominator * 10
	end

	return Frac.reduce( Frac.new( math.floor(numerator + 0.5), denominator ) )
end

function Frac.toDec( fraction )	-- Convert fraction to decimal
	if Frac.isFrac( fraction ) == false then
		return nil
	end

	return fraction.numerator / fraction.denominator
end

-- Basic calculations
function Frac.neg( fraction )	-- Take the negative fraction
	if Frac.isFrac( fraction ) == false then
		return nil
	end

	return Frac.new( - fraction.numerator, fraction.denominator )
end

function Frac.inv( fraction )	-- Take the inversed fraction
	if Frac.isFrac( fraction ) == false then
		return nil
	end

	if fraction.numerator == 0 then
		return nil
	end

	return ( fraction.numerator < 0 ) and Frac.new( - fraction.denominator, - fraction.numerator ) or Frac.new( fraction.denominator, fraction.numerator )
end

function Frac.mul( fraction1, fraction2 )	-- Fractions multiplication
	fraction1 = Frac.isFrac( fraction1 ) and fraction1 or Frac.new( fraction1 )
	fraction2 = Frac.isFrac( fraction2 ) and fraction2 or Frac.new( fraction2 )
	if Frac.isFrac( fraction1 ) == false or Frac.isFrac( fraction2 ) == false then
		return nil
	end

	return Frac.new( fraction1.numerator * fraction2.numerator, fraction1.denominator * fraction2.denominator )
end

function Frac.div( fraction1, fraction2 )	-- Fractions division
	fraction2 = Frac.isFrac( fraction2 ) and fraction2 or Frac.new( fraction2 )
	return Frac.mul( fraction1, Frac.inv( fraction2 ) )
end

function Frac.pow( fraction, n )	-- Fractions exponentiation
	if Frac.isFrac( fraction ) == false or n % 1 ~= 0 then
		return nil
	end

	return ( n < 0 ) and Frac.new( fraction.denominator ^ ( - n ), fraction.numerator ^ ( -n ) ) or Frac.new( fraction.numerator ^ n, fraction.denominator ^ n )
end

-- Advanced calculation
function Frac.expand( fraction, newDenominator )	-- Fractions expansion
	if Frac.isFrac( fraction ) == false or type( newDenominator ) ~= 'number' or ( type( newDenominator ) == 'number' and newDenominator % 1 ~= 0 ) or newDenominator <= 0 then
		return nil
	end

	local newNumerator = fraction.numerator * newDenominator / fraction.denominator
	if newNumerator % 1 ~= 0 then
		return nil
	end

	return Frac.new( newNumerator, newDenominator )
end

function Frac.reduce( fraction )	-- Fractions reduction (to simplest)
	if Frac.isFrac( fraction ) == false then
		return nil
	end

	return Frac.expand( fraction, fraction.denominator / GCD( fraction.denominator, fraction.numerator ) )
end

function Frac.common( fractionList )	-- Get fractions with the common denominator
	if type( fractionList ) ~= 'table' then
		return nil
	end

	local candidates = {}
	local denominators = {}
	for _, fraction in pairs( fractionList ) do
		if Frac.isFrac( fraction ) == false then
			return nil
		else
			table.insert( candidates, fraction )
			table.insert( denominators, fraction.denominator )
		end
	end
	if #candidates == 1 then
		return candidates
	end

	local lcm = 1
	for _, denominator in pairs( denominators ) do
		lcm = LCM( denominator, lcm )
	end

	local results = {}
	for _, fraction in pairs( candidates ) do
		table.insert( results, Frac.expand( fraction, lcm ) )
	end

	return results
end

-- Basic calculations, with dependencies
function Frac.add( fraction1, fraction2 )	-- Fractions addition
	fraction1 = Frac.isFrac( fraction1 ) and fraction1 or Frac.new( fraction1 )
	fraction2 = Frac.isFrac( fraction2 ) and fraction2 or Frac.new( fraction2 )
	if Frac.isFrac( fraction1 ) == false or Frac.isFrac( fraction2 ) == false then
		return nil
	end

	local fractions = Frac.common{ fraction1, fraction2 }
	return Frac.new( fractions[ 1 ].numerator + fractions[ 2 ].numerator, fractions[ 1 ].denominator )
end

function Frac.sub( fraction1, fraction2 )	-- Fractions subtraction
	fraction2 = Frac.isFrac( fraction2 ) and fraction2 or Frac.new( fraction2 )
	return Frac.add( fraction1, Frac.neg( fraction2 ) )
end

function Frac.cmp( fraction1, fraction2 )	-- Compare fractions
	return Frac.sub( fraction1, fraction2 ).numerator
end

-- Formattings to human readable
function Frac.display( fraction, isReduced, isMixed )	-- Convert fractions to human-readable style
	if Frac.isFrac( fraction ) == false then
		return '' 
	end

	if fraction.numerator == 0 then
		return '0'
	end

	local prefix = ''
	if fraction.numerator < 0 then
		prefix = prefix .. '-'
		fraction = Frac.neg( fraction )
	end

	if isReduced then
		fraction = Frac.reduce( fraction )
	end

	local numerator = fraction.numerator
	local denominator = fraction.denominator
	if denominator == 1 then
		return prefix .. tostring( numerator )
	end
	if numerator == denominator then
		return prefix .. '1'
	end
	if isMixed and numerator > denominator then
		prefix = prefix .. math.floor( numerator / denominator )
		numerator = numerator % denominator
		if numerator == 0 then
			return prefix
		end
	end

	return string.format( '%s<sup>%s</sup>&frasl;<sub>%s</sub>', prefix, numerator, denominator )
end

------------------

local Distr = {}
Distr.mt = {}
function Distr.mt.__index( t, k )
	if type( k ) == 'number' then return Frac.new( 0 ) end
	return Distr[ k ]
end

function Distr.mt.__newindex( t, k, v )
	v = Frac.isFrac( v ) and v or Frac.new( v )
	if type( k ) ~= 'number' or v == nil then error() end
	if v == Frac.new( 0 ) then return end
	return rawset( t, k, v )
end

function Distr.new( t )
	t = t or {}
	local distribution = {}
	setmetatable( distribution, Distr.mt )
	for k, v in pairs( t ) do
		distribution[ k ] = v
	end
	return distribution
end

function Distr.shift( distribution, s )
	local newDistribution = Distr.new()
	for i, v in pairs( distribution ) do
		newDistribution[ i + s ] = v
	end
	return newDistribution
end

function Distr.limitRange( distribution, min, max )
	local newDistribution = Distr.new()
	for i, v in pairs( distribution ) do
		if max and i >= max then
			newDistribution[ max ] = newDistribution[ max ] + v
		elseif min and i <= min then
			newDistribution[ min ] = newDistribution[ min ] + v
		else
			newDistribution[ i ] = v
		end
	end
	return newDistribution
end

function Distr.add( distribution1, distribution2 )
	local newDistribution = distribution1:new()
	for i, v in pairs( distribution2 ) do
		newDistribution[ i ] = newDistribution[ i ] + v
	end
	return newDistribution
end

function Distr.multiply( distribution, x )
	local newDistribution = Distr.new()
	for i, v in pairs( distribution ) do
		newDistribution[ i ] = v * x
	end
	return newDistribution
end


function Distr.expectation( distribution )
	local expectation = Frac.new( 0 )
	for i, v in pairs( distribution ) do
		expectation = expectation + v * i
	end
	return expectation
end


function Distr.times( distribution, n )
	local newDistribution = Distr.new({[0] = 1})
	for i = 1, n do
		newDistribution = newDistribution:addIncrease( distribution )
	end
	return newDistribution
end

function Distr.addIncrease( distribution1, distribution2 )
	local newDistribution = Distr.new()
	for i, v in pairs( distribution2 ) do
		newDistribution = newDistribution:add( distribution1:shift( i ):multiply( v ) )
	end
	return newDistribution
end

function Distr.rolls( distribution1, distribution2 )
	local newDistribution = Distr.new()
	for i, v in pairs( distribution2 ) do
		newDistribution = newDistribution:add( distribution1:times( i ):multiply( v ) )
	end
	return newDistribution
end

function Distr.getUniform( min, max )
	local value = Frac.new( 1, max - min + 1 )
	local distribution = Distr.new()
	for i = min, max do
		distribution[ i ] = value
	end
	return distribution
end

function Distr.getLootingIncrease( level, min, max )
	min = min or 0
	max = max or min + 1
	min = min * level
	max = max * level
	if max - min < 2 then
		return Distr.getUniform( min, max )
	end
	local distribution = Distr.new()
	local value = Frac.new( 1, max - min )
	for i = min, max do
		distribution[ i ] = value
	end
	distribution[ min ] = value / 2
	distribution[ max ] = value / 2
	return distribution
end

function Distr.addLootingIncrease( distribution1, distribution2, edition )
	local base = distribution1:limitRange(0, nil)
	local backup = base[ 0 ]
	if edition == 'bedrock' then
		base[ 0 ] = 0 -- MCPE-35307: Looting doesn't work when the mob drops nothing
	end
	local newDistribution = base:addIncrease(distribution2)
	if edition == 'bedrock' then
		newDistribution[ 0 ] = newDistribution[ 0 ] + backup
	end
	return newDistribution
end

------------------

local p = {}

local function calculateDistributions( args, edition )
	local function parseRange(str)
		local n = tonumber(str)
		if n then return {min = n, max = n} end
		local min, max = str:match('^%s*(%-?%d+)%s*%-%s*(%-?%d+)%s*$')
		min = tonumber(min)
		max = tonumber(max)
		if min and max and min <= max then return {min = min, max = max} end
		error(string.format("'%s' isn't a range!", str))
	end
	local function parseProbability(str)
		local n = Frac.fromDec(tonumber(str)) or Frac.div(tonumber(str:match('(.*)%%%s*$')), 100)
		if n then return n end
		local numerator, denominator = str:match('^%s*(%-?%d+)%s*/%s*(%-?%d+)%s*$')
		numerator = tonumber(numerator)
		denominator = tonumber(denominator)
		local frac = Frac.new(numerator, denominator)
		if frac then return frac end
		error(string.format("'%s' isn't a probability!", str))
	end
	local quantity = parseRange(args.quantity or 1)
	local lootingquantity = parseRange(args.lootingquantity or 0)
	local dropchance = parseProbability(args.dropchance or 1)
	local lootingchance = parseProbability(args.lootingchance or 0)
	local multiplychance = parseProbability(args.multiplychance or 1)
	local rolls = parseRange(args.rolls or 1)
	local limit = tonumber(args.limit)

	if edition == 'bedrock' then
		limit = nil
	end

	local independent = {}
	for _, sub in ipairs(args.independent or {}) do
		table.insert(independent, calculateDistributions(sub, edition))
	end
	local mutuallyexclusive = {}
	for _, sub in ipairs(args.mutuallyexclusive or {}) do
		table.insert(mutuallyexclusive, calculateDistributions(sub, edition))
	end

	local distributions = {}
	for level = 0, 3 do
		local lootingIncrease = Distr.getLootingIncrease(level, lootingquantity.min, lootingquantity.max)
		local chance = (dropchance + lootingchance * level) * multiplychance
		local distribution = Distr.getUniform(quantity.min, quantity.max)
			:addLootingIncrease(lootingIncrease, edition)
			:rolls(Distr.new{[0] = 1 - chance, [1] = chance})
			:limitRange(0, limit)
			:rolls(Distr.getUniform(rolls.min, rolls.max))
		for _, sub in ipairs(independent) do
			distribution = distribution:addIncrease(sub[level])
		end
		for _, sub in ipairs(mutuallyexclusive) do
			local nodropchance1 = distribution[0]
			local nodropchance2 = sub[level][0]
			distribution = distribution:add(sub[level])
			distribution[0] = nodropchance1 + nodropchance2 - Frac.new(1)
		end
		distributions[level] = distribution
	end
	return distributions
end

local function getImageAndLink(args, mode)
	local link = args.link or args.name
	local image = require([[Module:SpriteFile]]).sprite{
		args.image or args.name,
		name = args.spritetype or i18n.imageSprite[mode],
		link = link,
		size = 32,
		align = 'middle',
		keepcase = mode == 'item'
	}
	local displayname = args.displayname or args.name
	local linktext = string.format('[[%s|%s]]', link, displayname)
	return image, linktext
end

local function getDropData(args, edition)
	local data = {}
	for level, distribution in pairs(calculateDistributions(args, edition)) do
		local quantity = {}
		for k, v in pairs(distribution) do
			if v ~= Frac.new(0) then table.insert(quantity, k) end
		end
		table.sort(quantity)
		local min = quantity[1]
		local max = quantity[#quantity]
		local from = min
		local to
		local quantitytext = ''
		local function range()
			quantitytext = quantitytext .. (from == min and '' or ' / ')
				.. (from == to and to or (from .. '&ndash;' .. to))
		end
		for _, num in ipairs(quantity) do
			if to and to ~= num - 1 then
				range()
				from = num
			end
			to = num
		end
		range()
		data[level] = {
			distribution = distribution,
			average = distribution:expectation():reduce(),
			dropchance = (1 - distribution[0]):reduce(),
			min = min,
			max = max,
			quantitytext = quantitytext
		}
	end
	return data
end

local function linesToDatas(lines, notes, edition)
	local datas = {}
	for _, line in ipairs(lines) do
		local args = mw.text.jsonDecode(line)
		local data = { name = args.name }
		data.image, data.linktext = getImageAndLink(args, 'item')
		if edition ~= 'bedrock' and args.edition ~= 'bedrock' then
			data.java = getDropData(args, 'java')
		end
		if edition ~= 'java' and args.edition ~= 'java' then
			data.bedrock = getDropData(args, 'bedrock')
		end
		local notenames = args.notes and mw.text.split(args.notes, ',') or {}
		data.notes = {}
		for _, name in ipairs(notenames) do
			table.insert(data.notes, notes[name])
		end
		table.insert(datas, data)
	end
	return datas
end

local function createOverviewTable(format, datas, noteTexts, mode, edition)
	local wikitable = mw.html.create('table'):addClass('wikitable')
		:tag('tr')
			:tag('th'):attr('colspan', '2'):attr('rowspan', '2'):wikitext(i18n.headerThing[mode]):done()
			:tag('th'):attr('colspan', '12'):wikitext(i18n.headerOverview):done()
			:done()
		:tag('tr')
			:tag('th'):attr('colspan', '3'):wikitext(i18n.headerWithoutLooting):done()
			:tag('th'):attr('colspan', '3'):wikitext(i18n.headerLooting1):done()
			:tag('th'):attr('colspan', '3'):wikitext(i18n.headerLooting2):done()
			:tag('th'):attr('colspan', '3'):wikitext(i18n.headerLooting3):done()
			:done()

	for i, data in ipairs(datas) do
		if data[edition] then
			local row = mw.html.create('tr')
				:tag('td'):wikitext(data.image):done()
				:tag('td'):wikitext(data.linktext .. noteTexts[i]):done()

			for level = 0, 3 do
				local info = data[edition][level]
				row:tag('td'):css('text-wrap-mode', 'nowrap'):wikitext(info.quantitytext):done()
				if format == 'decimal' then
					row:tag('td'):wikitext(string.format('%.2f%%', Frac.toDec(info.dropchance) * 100)):done()
					row:tag('td'):wikitext(string.format('%.2f', Frac.toDec(info.average))):done()
				else
					row:tag('td'):wikitext(Frac.display(info.dropchance)):done()
					row:tag('td'):wikitext(Frac.display(info.average)):done()
				end
			end
			wikitable:node(row)
		end
	end
	return tostring(wikitable)
end

local function createDistributionsTable(datas, noteTexts, mode, edition)
	local wikitable = mw.html.create('table'):addClass('wikitable')
		:tag('tr')
			:tag('th'):attr('colspan', '2'):attr('rowspan', '2'):wikitext(i18n.headerThing[mode]):done()
			:tag('th'):attr('rowspan', '2'):wikitext(i18n.headerDropAmount):done()
			:tag('th'):attr('colspan', '4'):wikitext(i18n.headerDistribution):done()
			:done()
		:tag('tr')
			:tag('th'):wikitext(i18n.headerWithoutLooting):done()
			:tag('th'):wikitext(i18n.headerLooting1):done()
			:tag('th'):wikitext(i18n.headerLooting2):done()
			:tag('th'):wikitext(i18n.headerLooting3):done()
			:done()
	for i, data in ipairs(datas) do
		if data[edition] then
			local min = math.huge
			local max = -math.huge
			for level = 0, 3 do
				min = math.min(min, data[edition][level].min)
				max = math.max(max, data[edition][level].max)
			end
			local row = mw.html.create('tr')
				:tag('td'):attr('rowspan', max - min + 2):wikitext(data.image):done()
				:tag('td'):attr('rowspan', max - min + 2):wikitext(data.linktext .. noteTexts[i]):done()

			for drops = min, max do
				row:tag('td'):wikitext(tostring(drops)):done()
				for level = 0, 3 do
					local probability = data[edition][level].distribution[drops] or Frac.new(0)
					row:tag('td'):wikitext(
						probability == Frac.new(0) and '0' or
						string.format('%s (%.2f%%)', Frac.display(probability, true), Frac.toDec(probability) * 100)
					):done()
				end
				wikitable:node(row)
				row = mw.html.create('tr')
			end
			row:tag('th'):wikitext(i18n.headerAverage):done()
			for level = 0, 3 do
				local average = data[edition][level].average
				row:tag('th'):wikitext(string.format('%s (%.2f)', Frac.display(average), Frac.toDec(average))):done()
			end
			wikitable:node(row)
		end
	end
	return tostring(wikitable)
end

local function createCalculatorTable(datas, noteTexts, mode, edition)
	local wikitable = mw.html.create('table'):addClass('wikitable'):addClass('calculator-container')
		:tag('tr')
			:tag('th')
				:attr('colspan', '2')
				:wikitext(mw.getCurrentFrame():expandTemplate{ title = 'Simplecalc label', args = {
					label = i18n.headerTotal[mode],
					['for'] = 'count'
				}})
				:done()
			:tag('td')
				:attr('colspan', '4')
				:wikitext(mw.getCurrentFrame():expandTemplate{ title = 'simplecalc', args = {
					type = 'number',
					id = 'count',
					min = 0,
					step = 1,
					default = 1
				}})
				:done()
			:done()
		:tag('tr')
			:tag('th'):attr('colspan', '2'):attr('rowspan', '2'):wikitext(i18n.headerThing[mode]):done()
			:tag('th'):attr('colspan', '4'):wikitext(i18n.headerExpectation[mode]):done()
			:done()
		:tag('tr')
			:tag('th'):wikitext(i18n.headerWithoutLooting):done()
			:tag('th'):wikitext(i18n.headerLooting1):done()
			:tag('th'):wikitext(i18n.headerLooting2):done()
			:tag('th'):wikitext(i18n.headerLooting3):done()
			:done()

	for i, data in ipairs(datas) do
		if data[edition] then
			local row = mw.html.create('tr')
				:tag('td'):wikitext(data.image):done()
				:tag('td'):wikitext(data.linktext .. noteTexts[i]):done()

			for level = 0, 3 do
				local average = data[edition][level].average
				if mode == 'mob' then
					average = Frac.inv(average)
				end
				row:tag('td')
					:wikitext(mw.getCurrentFrame():expandTemplate{ title = 'simplecalc', args = {
						type = 'plain',
						formula = string.format('count*(%s/%s)', average.numerator, average.denominator),
						decimals = 2,
						default = string.format('%.2f', Frac.toDec(average))
					}})
					:done()
			end
			wikitable:node(row)
		end
	end
	return tostring(wikitable)
end

local function createTabs (datas, mode, edition)
	local f = mw.getCurrentFrame()
	local noteTexts = {}
	for i, data in pairs(datas) do
		local noteText = ''
		if data[edition] then
			for _, note in ipairs(data.notes) do
				local ref = note.name and note or note[edition]
				if ref then
					noteText = noteText .. f:extensionTag{
						name = 'ref',
						content = ref.content,
						args = { name = ref.name, group = i18n.refgroup }
					}
				end
			end
		end
		noteTexts[i] = noteText
	end
	local tabber = f:extensionTag('tabber', string.format(
		'%s=%s|-|%s=%s|-|%s=%s|-|%s=%s',
		i18n.tabOverviewDecimal,
		createOverviewTable('decimal', datas, noteTexts, mode, edition),
		i18n.tabOverviewFraction,
		createOverviewTable('fraction', datas, noteTexts, mode, edition),
		i18n.tabDistribution,
		createDistributionsTable(datas, noteTexts, mode, edition),
		i18n.tabCalculator,
		createCalculatorTable(datas, noteTexts, mode, edition)
	)) 
	return require('Module:TSLoader').call(i18n.style)
		.. tostring(mw.html.create('div'):addClass('droptable-tabber'):wikitext(tabber))
		.. tostring(mw.html.create('div'):addClass('droptable-references'):wikitext(f:extensionTag{
			name = 'references',
			args = { group = i18n.refgroup }
		}))
end

local function setBucketData(datas, args)
	args.name = args.name or mw.title.getCurrentTitle().text
	local image, link = getImageAndLink(args, 'mob')
	for _, data in ipairs(datas) do
		local jsonData = mw.text.jsonEncode({
			name = args.name,
			image = image,
			linktext = link,
			java = data.java,
			bedrock = data.bedrock,
			notes = data.notes
		}, mw.text.JSON_PRESERVE_KEYS)

		local bucketObject = {
			item = data.name,
			json = jsonData
		}
		bucket(i18n.bucket).put(bucketObject)
	end
end

local function getBucketData(args)
	local name = args.name or args[1] or mw.title.getCurrentTitle().text
	local data = bucket(i18n.bucket)
		.select('json')
		.where('item', name)
		.run()
	local jsons = {}
	for _, object in ipairs(data) do
		local jsondata = object['json']
		table.insert(jsons, mw.text.jsonDecode(jsondata, mw.text.JSON_PRESERVE_KEYS))
	end
	return jsons
end

local function parseNotes(defines)
	local notes = {
		burn_only = { name = 'burn_only', content = i18n.noteBurnOnly },
		burn_or_fire_aspect = { name = 'burn_or_fire_aspect', content = i18n.noteBurnOrFireAspect },
		not_burn_only = { name = 'not_burn_only', content = i18n.noteNotBurnOnly },
		not_burn_or_fire_aspect = { name = 'not_burn_or_fire_aspect', content = i18n.noteNotBurnOrFireAspect },
		player_only = { name = 'player_only', content = i18n.notePlayerOnly },
		player_or_pet = { name = 'player_or_pet', content = i18n.notePlayerOrPet }
	}
	notes.player = { java = notes.player_or_pet, bedrock = notes.player_only }
	notes.burn = { java = notes.burn_or_fire_aspect, bedrock = notes.burn_only }
	notes.not_burn = { java = notes.not_burn_or_fire_aspect, bedrock = notes.not_burn_only }

	for _, str in ipairs(defines and mw.text.split(defines, '\n') or {}) do
		local key, text = str:match('^%s*([^=]-)%s*=%s*(.-)%s*$')
		if key and text then
			local name, edition = key:match('^(.-)%.(.*)$')
			if edition then
				notes[name] = notes[name] or {}
				local redirect = text:match('^>%s*(.-)$')
				if redirect and notes[redirect].name then
					notes[name][edition] = notes[redirect]
				else
					notes[name][edition] = { name = name, content = text }
				end
			else
				notes[key] = { name = key, content = text }
			end
		end
	end
	return notes
end

function p.line( frame )
	local args = require( 'Module:ProcessArgs' ).norm( frame:getParent().args )
	for _, name in pairs({'independent', 'mutuallyexclusive'}) do
		if args[name] then
			args[name] = mw.text.jsonDecode('[' .. args[name] .. ']')
		end
	end
	return mw.text.jsonEncode(args)
end

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

function p._main( args )
	local notes = parseNotes(args.notes)
	local edition = args.edition and mw.text.trim(args.edition)
	local datas = linesToDatas(args, notes, edition)
	if not args.nosource and mw.title.getCurrentTitle().isContentPage then
		setBucketData(datas, args)
	end
	if edition then
		return createTabs(datas, 'item', edition)
	end
	return i18n.titlejava .. createTabs(datas, 'item', 'java')
		.. i18n.titlebedrock .. createTabs(datas, 'item', 'bedrock')
end

function p.source( frame )
	return p._source( frame:getParent().args )
end

function p._source( args )
	local datas = getBucketData( args )
	local edition = args.edition and mw.text.trim(args.edition)
	if edition then
		return createTabs(datas, 'mob', edition)
	end
	return i18n.titlejava .. createTabs(datas, 'mob', 'java')
		.. i18n.titlebedrock .. createTabs(datas, 'mob', 'bedrock')
end

return p