Module:Swatches

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

Module:Swatches

Lists cosmetic swatches from Module:Swatches/data and renders them using Template:SwatchInfobox.

The rendered infobox cards are wrapped in a flex container with the following styling: display:flex; flex-wrap:wrap; overflow-x:auto; align-items:flex-start; gap:1em;

Usage

List all swatches

If no scope is provided, all swatches in the dataset are shown:

{{#invoke:Swatches|list}}

List swatches for a scope page

Scope can be specified to show all swatches that are compatible with at least one item in that scope:

{{#invoke:Swatches|list|scope=<SCOPE>}}

List swatches for an item page

Item can additionally be specified to show only swatches that are compatible with that item:

{{#invoke:Swatches|list|scope=<SCOPE>|item=<ITEM>}}

Parameters

scope (optional)
Compatibility scope to filter swatches. Accepted values:
  • Vehicle
  • Weapon
  • Garment
  • Placeable
item (optional)
Name of item within the specified scope, to further filter swatch compatibility. This must match the item name used in onlyItems tokens in Module:Swatches/data.
{{PAGENAME}} can be used if the page title matches the dataset naming.

Notes

  • Swatch compatibility rules are defined in Module:Swatches/data using the scopes and optional onlyItems fields.
  • If scope is omitted, item is ignored and all swatches are shown.
  • If a swatch uses scopes={"All"}, it will appear for any scope/item filter.



local p = {}
local data = require("Module:Swatches/data")

-- ---------- Helpers ----------

local function trim(s)
  return mw.text.trim(tostring(s or ""))
end

local function lowerTrim(s)
  return mw.ustring.lower(trim(s))
end

local function escapeTemplateParam(s)
  -- Prevent breaking template calls if odd characters appear
  s = tostring(s or "")
  s = s:gsub("|", "&#124;")
  return s
end

local function normalizeScope(s)
  -- Accommodate plural forms of scopes
  s = lowerTrim(s)
  if s == "vehicles" then return "vehicle" end
  if s == "weapons" then return "weapon" end
  if s == "garments" then return "garment" end
  if s == "placeables" then return "placeable" end
  return s
end

local function listContains(list, token, normalizeFn)
  if type(list) ~= "table" then return false end

  -- defaults to lowerTrim if not comparing scopes
  normalizeFn = normalizeFn or lowerTrim

  token = normalizeFn(token)
  for _, v in ipairs(list) do
    if normalizeFn(v) == token then return true end
  end
  return false
end

local function sortedSwatches()
  local swatches = {}
  for _, s in ipairs(data) do
    swatches[#swatches + 1] = s
  end
  table.sort(swatches, function(a, b)
    return lowerTrim(a.name) < lowerTrim(b.name)
  end)
  return swatches
end

-- ---------- Compatibility ----------

local function swatchAppliesTo(swatch, scope, item)
  scope = normalizeScope(scope or "")
  item = trim(item or "")

  -- If no scope provided, show all swatches
  if scope == "" then return true end

  -- If 'scopes' not provided, default to All
  local swatchScopes = swatch.scopes or { "All" }

  -- Swatches with an "All" scope match all filters
  if listContains(swatchScopes, "All", normalizeScope) then return true end

  -- Otherwise must match the requested scope
  if not listContains(swatchScopes, scope, normalizeScope) then return false end

  -- If no item specified, scope match is enough
  if item == "" then return true end

  -- If 'onlyItems' is not present, applies to all items in this scope
  if type(swatch.onlyItems) ~= "table" or #swatch.onlyItems == 0 then return true end

  -- Otherwise, must match one of the "Scope:Item" tokens
  local token = scope .. ":" .. item
  return listContains(swatch.onlyItems, token)
end

local function getCompatibilityDisplay(swatch)
  -- Derive the Compatibility value to display, based on 'scopes' and 'onlyItems' variables:
  -- - If 'scopes' includes "All": display "All"
  -- - Else if 'onlyItems' is present: list item names (without the "Scope:" prefix)
  -- - Else, swatch applies to all items within its scopes in 'scopes':
  --   * One scope  -> "All Vehicles"
  --   * Many scopes -> "All Vehicles • Weapons • Garments" (etc.)

  local swatchScopes = swatch.scopes or { "All" }

  if listContains(swatchScopes, "All", normalizeScope) then return "All" end

  if type(swatch.onlyItems) == "table" and #swatch.onlyItems > 0 then
    local items = {}
    for _, tok in ipairs(swatch.onlyItems) do
      local t = trim(tok)
      -- Split on first colon: "Scope:Item" -> "Item"
      local scopePart, itemPart = t:match("^%s*([^:]+)%s*:%s*(.+)%s*$")
      if itemPart then
        items[#items + 1] = itemPart
      else
        -- Display warning if malformed token (missing "Scope:Item")
        items[#items + 1] = "[INVALID onlyItems token: " .. t .. "]"
      end

    end
  
    -- One item: return plain text
    if #items == 1 then return items[1] end
  
    -- Many items: return bullet list
    local out = {}
    for _, item in ipairs(items) do
      out[#out + 1] = "* " .. item
    end
    return "\n" .. table.concat(out, "\n")
  end

  -- Convert scopes into display phrases like "Vehicles"
  local scopes = {}
  for _, t in ipairs(swatchScopes) do
    local s = normalizeScope(t)
    if s == "vehicle" then
      scopes[#scopes + 1] = "Vehicles"
    elseif s == "weapon" then
      scopes[#scopes + 1] = "Weapons"
    elseif s == "garment" then
      scopes[#scopes + 1] = "Garments"
    elseif s == "placeable" then
      scopes[#scopes + 1] = "Placeables"
    else
      -- Fallback: use the scope as-is
      scopes[#scopes + 1] = trim(t)
    end
  end

  -- Sort for stable output
  table.sort(scopes, function(a, b)
    return lowerTrim(a) < lowerTrim(b)
  end)

  if #scopes == 0 then return "All" end

  return "All " .. table.concat(scopes, " • ")
end

-- ---------- Rendering ----------

local function renderSwatchInfoboxCall(swatch)
  local title = escapeTemplateParam(swatch.name or "")

  local image = escapeTemplateParam(swatch.image or "Placeholder.jpg")
  local link = escapeTemplateParam(swatch.link or "")

  local c1 = escapeTemplateParam((swatch.colors and swatch.colors[1]) or "")
  local c2 = escapeTemplateParam((swatch.colors and swatch.colors[2]) or "")
  local c3 = escapeTemplateParam((swatch.colors and swatch.colors[3]) or "")
  local c4 = escapeTemplateParam((swatch.colors and swatch.colors[4]) or "")

  local compat = escapeTemplateParam(getCompatibilityDisplay(swatch) or "All")

  local out = {}
  out[#out + 1] = "{{SwatchInfobox"
  out[#out + 1] = "|title=" .. title
  out[#out + 1] = "|image=" .. image
  if link ~= "" then
    out[#out + 1] = "|link=" .. link
  end
  out[#out + 1] = "|color1=" .. c1
  out[#out + 1] = "|color2=" .. c2
  out[#out + 1] = "|color3=" .. c3
  out[#out + 1] = "|color4=" .. c4
  out[#out + 1] = "|compatibility=" .. compat
  out[#out + 1] = "}}"
  return table.concat(out, "\n")
end

function p.list(frame)
  local args = frame.args
  local scope = args.scope or ""
  local item = args.item or ""

  local out = {}
  out[#out + 1] = '<div style="display:flex; flex-wrap:wrap; overflow-x:auto; align-items:flex-start; gap:1em;">'
  for _, swatch in ipairs(sortedSwatches()) do
    if swatchAppliesTo(swatch, scope, item) then
      out[#out + 1] = renderSwatchInfoboxCall(swatch)
    end
  end
  out[#out + 1] = "</div>"
  return frame:preprocess(table.concat(out, "\n"))
end

return p