Module:Engineers

Current answers. Practical procedures. Reliable reference.
Revision as of 20:26, 19 August 2026 by Sythan (talk | contribs)
Jump to navigation Jump to search

Documentation for this module may be created at Module:Engineers/doc

-- Module:Engineers
-- Reads canonical Engineer facts from Module:Data/Engineers/<Name> (JSON
-- content-model pages) and formats them for multiple consumers:
--
--   * Engineer infoboxes via Template:Engineer infobox
--   * List of Engineers
--   * Engineer Capability Comparison
--
-- This module adds appropriate internal wiki links when rendering content.
--
-- Adding a new Engineer:
--   1. Create Module:Data/Engineers/<Name>
--   2. Use the same JSON schema as the existing Engineer records
--   3. Append the Engineer name to Module:Data/Engineers/index
--
-- See Elite Dangerous Field Manual:Adding Structured Data.

local p = {}

-- Field order and labels shared by both the structured-data path and the
-- manual fallback path.
local FIELD_ORDER = {
	{ key = 'engineer_type', label = 'Engineer type', group = 'ENGINEER' },
	{ key = 'system', label = 'System' },
	{ key = 'body', label = 'Body' },
	{ key = 'facility', label = 'Facility' },
	{ key = 'region', label = 'Region' },
	{ key = 'allegiance', label = 'Allegiance' },
	{ key = 'specialties', label = 'Specialties' },
	{ key = 'max_grade', label = 'Maximum grade' },

	{ key = 'prerequisite_engineer', label = 'Prerequisite', group = 'ACCESS' },
	{ key = 'discovery_requirement', label = 'Discovery' },
	{ key = 'invitation_requirement', label = 'Invitation' },
	{ key = 'unlock_requirement', label = 'Unlock' },
}


------------------------------------------------------------------------
-- Utility functions
------------------------------------------------------------------------

-- Converts a plain structured-data value into an internal wiki link.
--
-- Examples:
--
--   wikilink('Deciat')
--     -> [[Deciat]]
--
--   wikilink('Farseer Inc')
--     -> [[Farseer Inc]]
--
-- Existing wiki links are left untouched so explicit/manual overrides
-- remain safe.
local function wikilink(value, target)
	if value == nil or value == '' then
		return value
	end

	if type(value) ~= 'string' then
		return value
	end

	-- Already contains a wiki link.
	if value:find('%[%[') then
		return value
	end

	target = target or value

	if target == value then
		return '[[' .. value .. ']]'
	end

	return '[[' .. target .. '|' .. value .. ']]'
end


------------------------------------------------------------------------
-- Data loading
------------------------------------------------------------------------

local function loadRecord(name)
	local title = mw.title.new('Data/Engineers/' .. name, 'Module')

	if not title then
		return nil
	end

	local content = title:getContent()

	if not content then
		return nil
	end

	local ok, data = pcall(mw.text.jsonDecode, content)

	if not ok then
		return nil
	end

	return data
end


local function loadIndex()
	local title = mw.title.new('Data/Engineers/index', 'Module')
	local content = title and title:getContent()

	if not content then
		return {}
	end

	local ok, data = pcall(mw.text.jsonDecode, content)

	if not ok then
		return {}
	end

	return data
end


------------------------------------------------------------------------
-- Specialty formatting
------------------------------------------------------------------------

-- Structured Engineer records should preferably store specialties as an
-- array:
--
-- "specialties": [
--     "Frame Shift Drive",
--     "Power Plant",
--     "Thrusters"
-- ]
--
-- This allows each module name to become its own internal link.
local function joinSpecialties(d, linked)
	if not d or not d.specialties then
		return nil
	end

	if type(d.specialties) == 'table' then
		local values = {}

		for _, specialty in ipairs(d.specialties) do
			if linked then
				table.insert(values, wikilink(specialty))
			else
				table.insert(values, specialty)
			end
		end

		return table.concat(values, ', ')
	end

	-- Preserve legacy/string values unchanged. These may contain descriptive
	-- prose rather than a single module name, so automatically linking the
	-- entire string would not always be appropriate.
	return d.specialties
end


------------------------------------------------------------------------
-- Unlock formatting
------------------------------------------------------------------------

-- Supports a newer structured unlock format while retaining compatibility
-- with the existing unlock_requirement prose field.
--
-- Example JSON:
--
-- "unlock": {
--     "action": "Deliver",
--     "quantity": 1,
--     "item": "Meta-Alloys"
-- }
--
-- Produces:
--
-- Deliver 1 unit of [[Meta-Alloys]]
--
-- Optional fields:
--
-- "unit": "unit"
-- "unit_plural": "units"
-- "item_target": "Meta-Alloys"
--
-- For unlock requirements that do not fit this structure cleanly, continue
-- using unlock_requirement as a display string.
local function formatUnlock(d)
	if not d then
		return nil
	end

	if type(d.unlock) == 'table' and d.unlock.item then
		local action = d.unlock.action or 'Deliver'
		local quantity = d.unlock.quantity or 1
		local quantityNumber = tonumber(quantity)

		local singularUnit = d.unlock.unit or 'unit'
		local pluralUnit = d.unlock.unit_plural or (singularUnit .. 's')

		local unit

		if quantityNumber == 1 then
			unit = singularUnit
		else
			unit = pluralUnit
		end

		local item = wikilink(
			d.unlock.item,
			d.unlock.item_target or d.unlock.item
		)

		return string.format(
			'%s %s %s of %s',
			action,
			tostring(quantity),
			unit,
			item
		)
	end

	-- Backward compatibility with existing records.
	return d.unlock_requirement
end


------------------------------------------------------------------------
-- Field construction
------------------------------------------------------------------------

-- Builds the display values used by an Engineer infobox.
--
-- Structured data supplies the defaults. Explicit parameters supplied by
-- an article override those defaults.
local function buildFields(d, pargs)
	local fields = {}

	if d then
		-- Start with the canonical values.
		for _, f in ipairs(FIELD_ORDER) do
			fields[f.key] = d[f.key]
		end

		--------------------------------------------------------------------
		-- Automatically linked structured fields
		--------------------------------------------------------------------

		fields.system = wikilink(d.system)
		fields.body = wikilink(d.body)
		fields.facility = wikilink(d.facility)
		fields.region = wikilink(d.region)
		fields.allegiance = wikilink(d.allegiance)

		-- Each specialty/module gets its own internal link.
		fields.specialties = joinSpecialties(d, true)

		--------------------------------------------------------------------
		-- Prerequisite Engineer
		--------------------------------------------------------------------

		if d.prerequisite_engineer then
			local note = ''

			if d.prerequisite_engineer_note
				and d.prerequisite_engineer_note ~= '' then
				note = ' ' .. d.prerequisite_engineer_note
			end

			fields.prerequisite_engineer =
				wikilink(d.prerequisite_engineer) .. note
		else
			fields.prerequisite_engineer =
				d.prerequisite_engineer_display
				or 'None — available from the start'
		end

		--------------------------------------------------------------------
		-- Unlock
		--------------------------------------------------------------------

		fields.unlock_requirement = formatUnlock(d)

	else
		-- No structured record exists yet. Fall back entirely to manually
		-- supplied template parameters.
		for _, f in ipairs(FIELD_ORDER) do
			fields[f.key] = pargs[f.key]
		end
	end

	------------------------------------------------------------------------
	-- Explicit article overrides
	------------------------------------------------------------------------
	--
	-- This preserves:
	--
	-- {{Engineer infobox
	--  |image = DifferentPortrait.png
	-- }}
	--
	-- as a valid way to override one structured value without duplicating
	-- the rest of the Engineer record.
	--
	-- Empty parameters do not erase structured defaults.
	for _, f in ipairs(FIELD_ORDER) do
		if pargs[f.key] ~= nil and pargs[f.key] ~= '' then
			fields[f.key] = pargs[f.key]
		end
	end

	return fields
end


------------------------------------------------------------------------
-- Engineer infobox
------------------------------------------------------------------------

-- {{#invoke:Engineers|infobox}}
--
-- Called by Template:Engineer infobox.
--
-- If a canonical JSON record exists, it supplies the defaults.
-- Explicit template parameters override structured values.
-- If no JSON record exists, manual parameters are used instead.
function p.infobox(frame)
	local parent = frame:getParent()
	local pargs = parent and parent.args or {}

	local name = pargs.name or mw.title.getCurrentTitle().text
	local d = loadRecord(name)

	local fields = buildFields(d, pargs)

	------------------------------------------------------------------------
	-- Image
	------------------------------------------------------------------------

	local image = pargs.image

	if (image == nil or image == '')
		and d
		and d.image
		and d.image ~= '' then
		image = d.image
	end

	------------------------------------------------------------------------
	-- Build Template:Infobox parameters
	------------------------------------------------------------------------

	local args = {
		name = name,
		image = image
	}

	local n = 0

	for _, f in ipairs(FIELD_ORDER) do
		local value = fields[f.key]

		if value ~= nil and value ~= '' then
			n = n + 1

			if f.group then
				args['group' .. n] = f.group
			end

			args['label' .. n] = f.label
			args['row' .. n] = value
		end
	end

	return frame:expandTemplate {
		title = 'Infobox',
		args = args
	}
end


------------------------------------------------------------------------
-- Engineer list
------------------------------------------------------------------------

-- {{#invoke:Engineers|list}}
--
-- Generates a sortable Engineer table from the canonical index.
--
-- Engineer names, systems, and specialties are linked automatically.
function p.list()
	local rows = {}

	for _, name in ipairs(loadIndex()) do
		local d = loadRecord(name)

		if d then
			table.insert(
				rows,
				string.format(
					'|-\n| %s || %s || %s || %s',
					wikilink(d.name or name),
					wikilink(d.system) or '—',
					joinSpecialties(d, true) or '—',
					d.max_grade or 'Not yet confirmed'
				)
			)
		end
	end

	return
		'{| class="wikitable sortable"\n'
		.. '! Engineer !! System !! Specialties !! Maximum grade\n'
		.. table.concat(rows, '\n')
		.. '\n|}'
end


------------------------------------------------------------------------
-- Engineer capability comparison
------------------------------------------------------------------------

-- {{#invoke:Engineers|comparison}}
--
-- Generates a comparison table from canonical Engineer data.
--
-- Engineer names, specialties, prerequisite Engineers, and structured
-- unlock items are linked automatically.
function p.comparison()
	local rows = {}

	for _, name in ipairs(loadIndex()) do
		local d = loadRecord(name)

		if d then
			local prereq

			if d.prerequisite_engineer then
				prereq = wikilink(d.prerequisite_engineer)
			else
				prereq =
					d.prerequisite_engineer_display
					or 'None'
			end

			table.insert(
				rows,
				string.format(
					'|-\n| %s || %s || %s || %s',
					wikilink(d.name or name),
					joinSpecialties(d, true) or '—',
					prereq,
					formatUnlock(d) or 'Not yet confirmed'
				)
			)
		end
	end

	return
		'{| class="wikitable sortable"\n'
		.. '! Engineer !! Specialties !! Prerequisite Engineer !! Unlock requirement\n'
		.. table.concat(rows, '\n')
		.. '\n|}'
end


------------------------------------------------------------------------
-- Engineer count
------------------------------------------------------------------------

-- {{#invoke:Engineers|count}}
--
-- Returns the number of canonical Engineer records currently present in
-- Module:Data/Engineers/index.
function p.count()
	return tostring(#loadIndex())
end


return p