Module:Item
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