Module:Item

From Dune: Awakening Community Wiki
Jump to navigation Jump to search

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

-- Module:Item
-- =============================================================================
-- The main entry point for the item system. This is the module that
-- Template:Item actually calls. It looks up an item by name, figures out
-- what category it belongs to, and dispatches to the right renderer.
-- =============================================================================

	local ItemData  = require("Module:ItemData")
	local ItemUtils = require("Module:ItemUtils")

	local p = {}


	-- =============================================================================
	-- BONUS ORDER CONFIGS
	-- =============================================================================
	-- Per-category, per-type ordered field definitions. Each entry defines:
	--   key      — the field name on the item table
	--   label    — display label
	--   format   — "plain" | "suffix" | "pct" | "pct-colour"
	--   negative — for pct-colour only: true means "lower is better" (colours accordingly)
	--   suffix   — for suffix format only: unit string appended to the value
	--   header   — alternatively, a row can just be {header = "Section Title"}
	-- =============================================================================

	local weaponBonusOrder = {}

	local garmentBonusOrder = {
		["Stillsuit"] = {
			{ key = "heat_protection",           label = "Heat Protection",   format = "plain" },
			{ key = "catchpocket_size",          label = "Catchpocket Size",  format = "plain" },
			{ key = "garment_hydration_capture", label = "Hydration Capture", format = "pct-colour", negative = false },
			{ key = "garment_sun_stroke_rate",   label = "Sun Stroke Rate",   format = "pct-colour", negative = true  },
			{ key = "dash_stamina_cost",         label = "Dash Stamina Cost", format = "pct-colour", negative = true  },
		},
		["Light Armor"] = {
			{ key = "dash_stamina_cost",         label = "Dash Stamina Cost", format = "pct-colour", negative = true  },
			{ key = "heat_protection",           label = "Heat Protection",   format = "plain" },
			{ key = "blade_mitigation",          label = "Blade Mitigation",   format = "pct-colour" },
			{ key = "light_dart_mitigation",     label = "Light Dart Mitigation",   format = "pct-colour" },
			{ key = "heavy_dart_mitigation",     label = "Heavy Dart Mitigation",   format = "pct-colour" },
			{ key = "energy_mitigation",     label = "Energy Mitigation",   format = "pct-colour" },
			{ key = "fire_mitigation",     label = "Fire Mitigation",   format = "pct-colour" },
			{ key = "poison_mitigation",     label = "Poison Mitigation",   format = "pct-colour" },
			{ key = "garment_suspensor_power_usage",     label = "Suspensor Power Usage",   format = "pct-colour", negative = true },
			{ key = "worm_threat_scalar",     label = "Worm Threat",   format = "pct-colour", negative = true },
			{ key = "garment_dew_reap_yield",     label = "Dew Reaper Yield",   format = "pct-colour" },
			{ key = "garment_climbing_drain",     label = "Climbing Drain",   format = "pct-colour", negative = true },
			{ key = "handheld_scanner_range_bonus",     label = "Scanner Range",   format = "pct-colour" },

		},
		["Heavy Armor"] = {
			{ key = "dash_stamina_cost",         label = "Dash Stamina Cost", format = "pct-colour", negative = true  },
			{ key = "heat_protection",           label = "Heat Protection",   format = "plain" },
			{ key = "blade_mitigation",          label = "Blade Mitigation",   format = "pct-colour" },
			{ key = "light_dart_mitigation",     label = "Light Dart Mitigation",   format = "pct-colour" },
			{ key = "heavy_dart_mitigation",     label = "Heavy Dart Mitigation",   format = "pct-colour" },
			{ key = "concussive_mitigation",     label = "Concussive Mitigation",   format = "pct-colour" },
			{ key = "energy_mitigation",     label = "Energy Mitigation",   format = "pct-colour" },
			{ key = "garment_power_usage",     label = "Power Usage",   format = "pct-colour", negative = true },
			{ key = "garment_maximum_power",     label = "Maximum Power",   format = "pct-colour" },

		},
		["Utility"] = {
			{ key = "heat_protection",           label = "Heat Protection",   format = "plain" },
			{ key = "fire_mitigation",     label = "Fire Mitigation",   format = "pct-colour" },
			{ key = "radiation_mitigation",     label = "Radiation Mitigation",   format = "pct-colour" },

		},
	}

	local utilityBonusOrder = {

		["Blood Extractor"] = {
			{ key = "power_consumption",           label = "Power Consumption",     format = "plain"       },
			{ key = "blood_extraction_efficiency", label = "Extraction Efficiency", format = "pct-colour", negative = false },
		},

		["Blood Sack"] = {
			{ key = "container_capacity", label = "Container Capacity", format = "suffix", suffix = " mL" },
		},

		["Cutteray"] = {
			{ key = "power_consumption",    label = "Power Consumption", format = "plain" },
			{ header = "Mining Yield" },
			{ key = "mining_tier1_minerals", label = "Copper, Granite, Salvaged Metal", format = "plain" },
			{ key = "mining_tier2_minerals", label = "Iron",                            format = "plain" },
			{ key = "mining_tier3_minerals", label = "Carbon and Erythrite",            format = "plain" },
			{ key = "mining_tier4_minerals", label = "Basalt and Aluminium",            format = "plain" },
			{ key = "mining_tier5_minerals", label = "Jasmium",                         format = "plain" },
			{ key = "mining_tier6_minerals", label = "Titanium and Stravidium",         format = "plain" },
		},

		["Dew Harvester"] = {
			{ key = "power_consumption",    label = "Power Consumption", format = "plain"                },
			{ key = "dew_reap_area",        label = "Dew Reap Area",     format = "suffix", suffix = " m²" },
			{ key = "dew_reap_water_yield", label = "Water Yield",       format = "plain"                },
		},

		["Fuel Container"] = {
			{ key = "fuel_capacity", label = "Fuel Capacity", format = "plain" },
		},

		["Power Pack"] = {
			{ key = "powerpack_power_pool",       label = "Power Pool",       format = "plain" },
			{ key = "powerpack_regen_per_second", label = "Regen per Second", format = "plain" },
		},

		["Scanner"] = {
			{ key = "power_consumption", label = "Power Consumption", format = "plain"                },
			{ key = "scanner_fov",       label = "FOV",               format = "plain"                },
			{ key = "scanner_range",     label = "Range",             format = "suffix", suffix = " m" },
		},

		["Shield"] = {
			{ key = "shield_refresh_time", label = "Shield Refresh Time", format = "suffix", suffix = "s" },
			{ key = "shield_power_drain",  label = "Power Drain",         format = "pct-colour", negative = true },
		},

		["Static Compactor"] = {
			{ key = "power_consumption",     label = "Power Consumption", format = "plain" },
			{ key = "compactor_gather_rate", label = "Gather Rate",       format = "plain" },
		},

		["Suspensor Belt"] = {
			{ key = "suspensor_power_drain",     label = "Power Drain",     format = "plain" },
			{ key = "worm_attraction_intensity", label = "Worm Attraction", format = "plain" },
		},

		["Vehicle Tool"] = {
			{ key = "power_consumption",           label = "Power Consumption", format = "plain"       },
			{ key = "welding_torch_detach_speed",  label = "Detach Speed",      format = "plain"       },
			{ key = "welding_torch_repair_quality", label = "[[Welding Torch#Repair Quality|Repair Quality]]",   format = "pct", negative = false },
		},

		["Water Container"] = {
			{ key = "container_capacity", label = "Container Capacity", format = "suffix", suffix = " mL" },
		},

	}

	local miscBonusOrder   = {}
	local vehicleBonusOrder = {}


	-- =============================================================================
	-- SHARED HELPERS
	-- =============================================================================
	-- Logic every renderer needs — vendor price, unique schematic, percentage
	-- formatting, stat bars, and the bonus-row dispatcher. Written once here
	-- instead of copy-pasted into four renderers and slowly drifting apart
	-- from each other, which is exactly what was happening before.
	-- =============================================================================

	-- Builds the vendor price row with the currency icon, or nil if the item
	-- is free/priceless. The > 1 guard matches ItemInfo's existing behaviour.
	local function getVendorPrice(item, frame)
		local priceVal = item.base_vendor_price or item.market_price
		if priceVal and tonumber(priceVal) and tonumber(priceVal) > 1 then
			return frame:expandTemplate{
				title = "CurrencyTag",
				args  = { "Solari", tostring(priceVal) }
			}
		end
		return nil
	end

	-- Resolves the unique schematic field into either a link to the schematic
	-- page or a plain "No". Returns nil if the field isn't set at all.
	local function getUniqueSchematic(item)
		if not item.unique_schematic then return nil end
		local val = tostring(item.unique_schematic):lower()
		if val == "yes" or val == "true" then
			return "[[" .. item.name .. "/Schematic|Yes]]"
		end
		return "No"
	end

	-- Coloured percentage via {{ItemInfo/Percentage}} — red/green logic lives
	-- in the template itself. negative=true means "lower is better" so the
	-- template colours a decrease green instead of red.
	local function pctColour(value, negative, frame)
		if not value then return nil end
		local args = { tostring(value) }
		if negative then args.negative = "1" end
		return frame:expandTemplate{ title = "ItemInfo/Percentage", args = args }
	end

	-- Plain percentage — just the number and a %, no template call, no colour.
	-- Use for stats where colour-coding would be noise rather than signal.
	local function pctPlain(value)
		if not value then return nil end
		return tostring(value) .. "%"
	end

	-- Calls {{ItemInfo/StatRow}} for the progress-bar display used by weapons
	-- and vehicle modules. Returns nil if value is absent so the row
	-- disappears gracefully rather than rendering an empty bar.
	local function statRow(label, value, max, round, unit, frame)
		if not value then return nil end
		local args = {
			label = label,
			value = tostring(value),
			max   = tostring(max),
		}
		if round then args.round = round end
		if unit  then args.unit  = unit  end
		return frame:expandTemplate{ title = "ItemInfo/StatRow", args = args }
	end

	-- Formats a single bonus value according to its declared format type.
	-- This is the one place "pct" vs "pct-colour" vs "suffix" vs "plain"
	-- gets decided — every renderer's bonus loop calls this instead of
	-- reimplementing the branch itself.
	local function formatBonusValue(bonus, value, frame)
		if bonus.format == "pct-colour" then
			return pctColour(value, bonus.negative, frame)
		elseif bonus.format == "pct" then
			return pctPlain(value)
		elseif bonus.format == "suffix" then
			return tostring(value) .. (bonus.suffix or "")
		else
			-- "plain" or anything unrecognised — display as-is
			return tostring(value)
		end
	end

	-- Iterates a bonus order list against an item, inserting rows (and any
	-- header entries) directly into the given rows table. Shared by every
	-- renderer that uses a bonus-order config (Garments, Utility, and any
	-- future category that adopts the pattern).
	local function applyBonusOrder(rows, bonusOrder, item, frame)
		for _, bonus in ipairs(bonusOrder) do
			if bonus.header then
				table.insert(rows, { header = bonus.header })
			else
				local value = item[bonus.key]
				if value ~= nil then
					table.insert(rows, { bonus.label, formatBonusValue(bonus, value, frame) })
				end
			end
		end
	end


	-- =============================================================================
	-- RENDERERS
	-- =============================================================================
	-- Each renderer takes (item, frame) — frame is needed for any renderer
	-- that calls expandTemplate() for things like CurrencyTag or StatRow.
	-- =============================================================================
	local renderers = {

		-- -------------------------------------------------------------------------
		-- Weapons
		-- Branches on item.weapon_type (ranged vs melee) and renders the
		-- appropriate stat rows via the shared statRow() helper.
		-- -------------------------------------------------------------------------
		Weapons = function(item, frame)

			local vendorPrice     = getVendorPrice(item, frame)
			local uniqueSchematic = getUniqueSchematic(item)

			-- Weapon-type-specific stat rows — ranged and melee have completely
			-- different stats so we branch rather than having a wall of nils.
			local weaponRows = {}

			if item.weapon_type == "ranged" then
				weaponRows = {
					{header = "Weapon Stats"},
					{"Fire Mode",       item.fire_mode},
					{"ADS Mode",        item.ads_mode},
					{"Damage Type",     item.damage_type},
					{full = statRow("Damage Per Shot", item.damage_per_shot, 50,    "round1", nil,   frame)},
					{full = statRow("Rate of Fire",    item.rate_of_fire,    600,   "0",      " RPM", frame)},
					{full = statRow("Clip Size",       item.clip_size,       50,    nil,      nil,   frame)},
					{full = statRow("Reload Speed",    item.reload_speed,    1.3,   "round1", " s",  frame)},
					{full = statRow("Effective Range", item.effective_range, 360,   nil,      " m",  frame)},
					{full = statRow("Accuracy",        item.accuracy,        0.666, "2",      nil,   frame)},
					{full = statRow("Stability",       item.stability,       20,    "round1", nil,   frame)},
				}
			elseif item.weapon_type == "melee" then
				weaponRows = {
					{header = "Weapon Stats"},
					{"Weapon Type",  item.melee_weapon_type},
					{"Damage Type",  item.damage_type},
					{full = statRow("Damage Per Hit", item.damage_per_hit, 50,  nil, nil, frame)},
					{full = statRow("Attack Speed",   item.attack_speed,   600, "0", nil, frame)},
				}
			else
				weaponRows = {
					{header = "Weapon Stats"},
					{"Damage Type",  item.damage_type},
				}
			end

			local rows = {
				{"Volume",           item.volume},
				{"Location",         item.location},
				{"Dropped on Death", item.dropped_on_death},
				{"Unique Schematic", uniqueSchematic},
				{"Vendor Price",     vendorPrice},
			}

			for _, row in ipairs(weaponRows) do
				table.insert(rows, row)
			end

			return ItemUtils.buildInfobox({
				title     = item.name,
				image     = item.image,
				subheader = item.short_desc,
				item_id   = item.item_id,
				added     = item.added,
				rows      = rows,
			})
		end,


		-- -------------------------------------------------------------------------
		-- Garments
		-- Covers stillsuits, light armour, and heavy armour. Bonus stats are
		-- driven entirely by garmentBonusOrder via the shared applyBonusOrder().
		-- -------------------------------------------------------------------------
		Garments = function(item, frame)

			local vendorPrice     = getVendorPrice(item, frame)
			local uniqueSchematic = getUniqueSchematic(item)

			-- Look up the display order for this garment type.
			-- Falls back to an empty table if garment_type is unrecognised,
			-- which renders no bonus rows rather than exploding dramatically.
			local bonusOrder = garmentBonusOrder[item.garment_type] or {}

			local rows = {
				{"Volume",           item.volume},
				{"Location",         item.location},
				{"Dropped on Death", item.dropped_on_death},
				{"Unique Schematic", uniqueSchematic},
				{"Vendor Price",     vendorPrice},
				{header = "Garment Stats"},
				{"Garment Type",     "[[" .. item.garment_type .. "]]"},
				{"Slot",             item.equip_slot},
				{"Armor Value",      item.armor_value},
			}

			applyBonusOrder(rows, bonusOrder, item, frame)

			return ItemUtils.buildInfobox({
				title     = item.name,
				image     = item.image,
				subheader = item.short_desc,
				item_id   = item.item_id,
				added     = item.added,
				rows      = rows,
			})
		end,


		-- -------------------------------------------------------------------------
		-- Utility
		-- The miscellaneous drawer — tools, consumables, scanners, shields,
		-- thumpers, compactors, and anything else that doesn't fit neatly
		-- somewhere else. Driven by utilityBonusOrder via applyBonusOrder(),
		-- including header rows (looking at you, Cutteray mining yield).
		-- -------------------------------------------------------------------------
		Utility = function(item, frame)

			local vendorPrice     = getVendorPrice(item, frame)
			local uniqueSchematic = getUniqueSchematic(item)

			-- Look up the display order for this utility type.
			-- Falls back to an empty table if utility_type is unrecognised.
			local bonusOrder = utilityBonusOrder[item.utility_type] or {}

			local rows = {
				{"Stack Size",       item.max_stack},
				{"Volume",           item.volume},
				{"Location",         item.location},
				{"Dropped on Death", item.dropped_on_death},
				{"Unique Schematic", uniqueSchematic},
				{"Vendor Price",     vendorPrice},
				{header = "Tool Stats"},
				{"Tool Type", "[[" .. item.utility_type .. "]]"},
			}

			applyBonusOrder(rows, bonusOrder, item, frame)

			return ItemUtils.buildInfobox({
				title     = item.name,
				image     = item.image,
				subheader = item.short_desc,
				item_id   = item.item_id,
				added     = item.added,
				rows      = rows,
			})
		end,


		-- -------------------------------------------------------------------------
		-- Miscellaneous
		-- Catch-all for anything that doesn't warrant its own category yet.
		-- -------------------------------------------------------------------------
		Miscellaneous = function(item, frame)
			return ItemUtils.buildInfobox({
				title     = item.name,
				image     = item.image,
				subheader = item.short_desc,
				item_id   = item.item_id,
				added     = item.added,
				rows = {
					{"Item Type",        item.subtype},
					{"Vendor Price",     getVendorPrice(item, frame)},
					{"Stack Size",       item.stack_size},
					{"Volume",           item.volume},
					{"Dropped on Death", item.dropped_on_death},
				}
			})
		end,


		-- -------------------------------------------------------------------------
		-- VehicleModules
		-- Chassis, engines, boosters, treads, storage. Uses the shared
		-- statRow() helper for boost rating and speed.
		-- -------------------------------------------------------------------------
		VehicleModules = function(item, frame)

			local vendorPrice     = getVendorPrice(item, frame)
			local uniqueSchematic = getUniqueSchematic(item)

			local rows = {
				{"Stack Size",       item.max_stack},
				{"Volume",           item.volume},
				{"Location",         item.location},
				{"Dropped on Death", item.dropped_on_death},
				{"Unique Schematic", uniqueSchematic},
				{"Vendor Price",     vendorPrice},

				{header = "Vehicle Module Stats"},
				{"Module Type",          item.vehicle_module_type},
				{"Assembly Requirement", item.assembly_requirement},
				{"Armor",                item.vehicle_module_armor},
				{"Fuel Capacity",        item.fuel_capacity},
				{"Grip Rating",          item.treads_steering},
				{"Vibration Level",      item.sandworm_threat_moving_vibration},
				{"Boost Type",           item.boost_type},
				{"Acceleration Type",    item.acceleration_type},
				{"Extra Heat",           item.extra_heat},
				{"Max Volume",           item.inventory_volume},
				{"Item Slots",           item.inventory_slots},
				{"Seats",                item.hull_seats},
				{"Utility Slots",        item.utility_slots},
				{"Scanner FOV",          item.scanner_fov},
				{"Scanner Range",        item.scanner_range},
				{"Heat Increase/s",      item.heat_increase_second},
				{"Heat Increase/use",    item.heat_increase_added_instant},
				{"Power Use/s",          item.power_consumption_per_second},
				{"Power Use/use",        item.power_consumption_added_instant},

				{full = statRow("Boost Rating", item.boost_rating, 12,  "round1", nil,     frame)},
				{full = statRow("Speed",        item.speed,        123, "round0", " km/h", frame)},

				-- Mining yield — orphaned-header guard, only shows if at least
				-- one tier has a value.
				(item.mining_tier1_minerals or
				 item.mining_tier2_minerals or
				 item.mining_tier3_minerals or
				 item.mining_tier4_minerals or
				 item.mining_tier5_minerals or
				 item.mining_tier6_minerals)
				and {header = "Mining Yield"} or nil,
				{"Copper, Granite, Salvaged Metal", item.mining_tier1_minerals},
				{"Iron",                            item.mining_tier2_minerals},
				{"Carbon and Erythrite",            item.mining_tier3_minerals},
				{"Basalt and Aluminium",            item.mining_tier4_minerals},
				{"Jasmium",                         item.mining_tier5_minerals},
				{"Titanium and Stravidium",         item.mining_tier6_minerals},
			}

			return ItemUtils.buildInfobox({
				title     = item.name,
				image     = item.image,
				subheader = item.short_desc,
				item_id   = item.item_id,
				added     = item.added,
				rows      = rows,
			})
		end,

	}


	-- =============================================================================
	-- p.main
	-- =============================================================================
	-- Called by Template:Item (or directly via #invoke). Looks up the item,
	-- picks a renderer, and returns the infobox.
	-- =============================================================================
	function p.main(frame)

		local itemName = mw.text.trim(frame.args[1] or "")

		if itemName == "" then
			return ItemUtils.error("No item name provided. Usage: {{Item|Item Name}}")
		end

		local item = ItemData[itemName]

		if not item then
			return ItemUtils.error(
				'Item "' .. itemName .. '" not found in Module:ItemData. ' ..
				'Either add it to the relevant data submodule, or check for typos. ' ..
				'Capitalisation matters.'
			)
		end

		item.name = itemName

		local renderer = renderers[item.category]

		if not renderer then
			return ItemUtils.error(
				'No renderer found for category "' .. tostring(item.category) .. '". ' ..
				'Add one to the renderers table in Module:Item.'
			)
		end

		return renderer(item, frame)
	end


	-- =============================================================================
	-- p.render
	-- =============================================================================
	-- Convenience wrapper for calling from another Lua module directly.
	-- =============================================================================
	function p.render(itemName)
		return p.main({ args = { itemName } })
	end


	-- Still mandatory. Still easy to forget.
	return p