Module:VaeringheimMerchantLeagueInvitational
From MicrasWiki
Documentation for this module may be created at Module:VaeringheimMerchantLeagueInvitational/doc
-- Module:VaeringheimMerchantLeagueInvitational
-- Simulates the Vaeringheim Merchant League - Invitational Division.
-- Each season lasts one Bassaridian month: 61 days.
-- Seasons reset on Atosiel 1, Thalassiel 1, and Opsitheiel 1.
local p = {}
------------------------------------------------------------------------
-- 0) CALENDAR / OPTIONAL MARKET MODULES
------------------------------------------------------------------------
local DAYS_IN_YEAR = 183
local DAYS_IN_SEASON = 61
local DEFAULT_YEAR = 53
local MONTHS = {
[1] = "Atosiel",
[2] = "Thalassiel",
[3] = "Opsitheiel"
}
local MONTH_OF = {
["atosiel"] = 1,
["thalassiel"] = 2,
["opsitheiel"] = 3,
["1"] = 1,
["2"] = 2,
["3"] = 3
}
local okDate, date = pcall(require, "Module:BassaridianCalendar")
local okAg, agPrices = pcall(require, "Module:AutoUpdateAgPrices")
local okManf, manfPrices = pcall(require, "Module:AutoUpdateManfPrices")
local okServ, servPrices = pcall(require, "Module:AutoUpdateServPrices")
local okServData, servData = pcall(mw.loadData, "Module:AutoUpdateServPrices/Data")
local function clamp(x, lo, hi)
x = tonumber(x) or 0
if x < lo then return lo end
if x > hi then return hi end
return x
end
local function round(x)
return math.floor((tonumber(x) or 0) + 0.5)
end
local function currentPSSC()
local s = nil
if okDate and date then
if type(date.getCurrentDateNoProverb) == "function" then
s = date.getCurrentDateNoProverb()
elseif type(date.getCurrentDate) == "function" then
s = date.getCurrentDate()
end
end
local dayOfYear = 1
local year = DEFAULT_YEAR
if s then
dayOfYear = tonumber(tostring(s):match("^(%d+),")) or 1
year = tonumber(tostring(s):match("(%d+)%s*PSSC")) or DEFAULT_YEAR
else
local startDate = os.time({ year = 1999, month = 8, day = 6 })
local secondsInDay = 86400
local currentDate = os.time()
local totalDaysElapsed = math.floor((currentDate - startDate) / secondsInDay)
local yearFraction = totalDaysElapsed / DAYS_IN_YEAR
year = math.floor(yearFraction)
dayOfYear = math.floor((yearFraction - year) * DAYS_IN_YEAR) + 1
end
dayOfYear = ((dayOfYear - 1) % DAYS_IN_YEAR) + 1
local monthIndex, dayInMonth
if dayOfYear <= 61 then
monthIndex = 1
dayInMonth = dayOfYear
elseif dayOfYear <= 122 then
monthIndex = 2
dayInMonth = dayOfYear - 61
else
monthIndex = 3
dayInMonth = dayOfYear - 122
end
return {
year = year,
dayOfYear = dayOfYear,
monthIndex = monthIndex,
month = MONTHS[monthIndex],
day = dayInMonth
}
end
local function monthIndexFrom(v)
if v == nil or v == "" then return nil end
local key = mw.ustring.lower(tostring(v))
return MONTH_OF[key]
end
local function seasonArgs(frame)
local args = (frame and frame.args) or {}
local now = currentPSSC()
local year = tonumber(args.year or args.season or args[1] or "") or now.year
local monthIndex = monthIndexFrom(args.month or args.m or args[2]) or now.monthIndex
local day = tonumber(args.day or args.d or args[3] or "")
if not day then
if year == now.year and monthIndex == now.monthIndex then
day = now.day
else
day = DAYS_IN_SEASON
end
end
day = clamp(day, 1, DAYS_IN_SEASON)
return {
year = year,
monthIndex = monthIndex,
month = MONTHS[monthIndex],
day = day,
seasonKey = tostring(year) .. "|" .. tostring(monthIndex)
}
end
local function formatDate(ctx, day)
return tostring(day) .. "/" .. tostring(ctx.monthIndex) .. "/" .. tostring(ctx.year) .. " [[PSSC]]"
end
local function formatSeason(ctx)
return ctx.month .. " " .. tostring(ctx.year) .. " [[PSSC]]"
end
------------------------------------------------------------------------
-- 1) DETERMINISTIC RANDOM HELPERS
------------------------------------------------------------------------
local function hashNum(key)
key = tostring(key or "")
local h = mw.hash.hashValue("md5", key)
local n = 0
for i = 1, 8 do
local c = h:sub(i, i)
n = n * 16 + (tonumber(c, 16) or 0)
end
return n
end
local function randKey(key)
return (hashNum(key) % 1000000) / 1000000
end
local function pick(list, key)
if not list or #list == 0 then return nil end
local idx = math.floor(randKey(key) * #list) + 1
if idx < 1 then idx = 1 end
if idx > #list then idx = #list end
return list[idx]
end
local function chooseWeighted(list, key, weightFn)
if not list or #list == 0 then return nil end
local total = 0
local weights = {}
for i, item in ipairs(list) do
local w = tonumber(weightFn(item, i)) or 0
if w < 0 then w = 0 end
weights[i] = w
total = total + w
end
if total <= 0 then
return pick(list, key)
end
local r = randKey(key) * total
local acc = 0
for i, item in ipairs(list) do
acc = acc + weights[i]
if r <= acc then return item end
end
return list[#list]
end
------------------------------------------------------------------------
-- 2) MONEY FORMAT
------------------------------------------------------------------------
-- Internal simulation stores contract income in nil-coins:
-- 1 poli = 200 nil-coins; 1 sik = 10 nil-coins.
local function formatMoney(nilValue)
local n = math.max(0, round(nilValue or 0))
local poli = math.floor(n / 200)
local rem = n % 200
local sik = math.floor(rem / 10)
local nils = rem % 10
local parts = {}
if poli > 0 then table.insert(parts, tostring(poli) .. "₱̈") end
if sik > 0 then table.insert(parts, tostring(sik) .. "Ƨ̈") end
if nils > 0 then table.insert(parts, tostring(nils) .. "•") end
if #parts == 0 then return "0•" end
return table.concat(parts, " ")
end
------------------------------------------------------------------------
-- 3) MERCHANT HOUSES
------------------------------------------------------------------------
local HOUSES = {
{
id = "qamar",
merchant = "Asena Vel-Qamar",
house = "House Qamar",
origin = "Vaeringheim",
location = "Vaeringheim",
style = "Luxury shrine-road brokerage and ceremonial cargo.",
profile = "A polished house known for shrine consignments, sealed ledgers, and expensive clients.",
skill = 0.88,
reliability = 0.86,
risk = 0.46,
rep = 14,
stable = 5,
tags = { luxury = 4, ritual = 4, service = 2, sealed = 3 }
},
{
id = "eikskald",
merchant = "Jorunn Eikskald",
house = "Eikskald Factors",
origin = "Riddersborg",
location = "Riddersborg",
style = "Cold-road wool, timber, and northern freight.",
profile = "A hard-weather ledger with reliable northern carriers and a reputation for endurance.",
skill = 0.82,
reliability = 0.90,
risk = 0.38,
rep = 12,
stable = 4,
tags = { wool = 4, timber = 4, bulk = 3, cold = 4 }
},
{
id = "bluequay",
merchant = "Nami of the Blue Quay",
house = "Blue Quay Papers",
origin = "Mylecia",
location = "Mylecia",
style = "Water, medicine, ferry freight, and wetland routes.",
profile = "A careful wet-road broker whose clerks are trusted with water, medicine, and ferry claims.",
skill = 0.84,
reliability = 0.88,
risk = 0.42,
rep = 13,
stable = 3,
tags = { water = 4, medicine = 3, ferry = 4, perishable = 2 }
},
{
id = "aureli",
merchant = "Sadir al-Aureli",
house = "Aurelian Grain Desk",
origin = "Aurelia",
location = "Aurelia",
style = "Grain, lamp oil, staple food, and bulk provisions.",
profile = "A high-volume desk that wins by moving ordinary staples with unusual punctuality.",
skill = 0.81,
reliability = 0.84,
risk = 0.35,
rep = 11,
stable = 3,
tags = { grain = 5, oil = 3, staple = 5, bulk = 4 }
},
{
id = "arshir",
merchant = "Laleh Arshir",
house = "Ardashir Import Rooms",
origin = "Delphica",
location = "Delphica",
style = "Euranidom imports, fine horse stock, and prestige contracts.",
profile = "An import house with elegant accounts, costly preferences, and a taste for risky prestige tenders.",
skill = 0.86,
reliability = 0.78,
risk = 0.68,
rep = 13,
stable = 6,
tags = { luxury = 4, animal = 4, import = 4, prestige = 5 }
},
{
id = "salval",
merchant = "Brennan Salval",
house = "Salt-Valed Ledger",
origin = "Sufriya",
location = "Sufriya",
style = "Salt, fish, hard weather routes, and rough-margin tenders.",
profile = "A lean house that accepts poor weather, thin margins, and cargo other merchants avoid.",
skill = 0.79,
reliability = 0.80,
risk = 0.72,
rep = 9,
stable = 3,
tags = { salt = 5, fish = 4, cold = 3, rough = 5, ferry = 2 }
},
{
id = "thalmaen",
merchant = "Orvida Thal-Maen",
house = "Thal-Maen Water Desk",
origin = "Thal Qencha",
location = "Thal Qencha",
style = "Clean water, fish, mountain greens, and perishables.",
profile = "A mountain-water desk respected for inspection discipline and clean cask seals.",
skill = 0.83,
reliability = 0.89,
risk = 0.41,
rep = 12,
stable = 4,
tags = { water = 5, fish = 3, greens = 4, perishable = 5 }
},
{
id = "sanvei",
merchant = "Yusaru Sanvei",
house = "Sandveil Commissions",
origin = "Somniumpolis",
location = "Somniumpolis",
style = "Dream-city luxuries, sealed consignments, and dusk tenders.",
profile = "A theatrical and sometimes uncanny commission house with a talent for dusk deliveries.",
skill = 0.85,
reliability = 0.77,
risk = 0.70,
rep = 12,
stable = 5,
tags = { luxury = 4, sealed = 5, ritual = 2, service = 3, night = 5 }
},
{
id = "jorope",
merchant = "Mikel Jorope",
house = "Jogi Ropehouse",
origin = "Jogi",
location = "Jogi",
style = "Dock rope, reed mats, wet-road hauling, and marsh labor.",
profile = "A dockside house built on rope, reed, rough labor, and an intimate knowledge of marsh delay.",
skill = 0.78,
reliability = 0.82,
risk = 0.58,
rep = 10,
stable = 2,
tags = { rope = 5, reed = 5, ferry = 3, rough = 3, bulk = 3 }
},
{
id = "ourid",
merchant = "Tavri of Ourid",
house = "Ourid Shrine Accounts",
origin = "Ourid",
location = "Ourid",
style = "Pass-road courier work and shrine-route contracts.",
profile = "A shrine-route courier house that treats stamps, relic boxes, and pass ledgers as sacred matter.",
skill = 0.87,
reliability = 0.85,
risk = 0.52,
rep = 13,
stable = 4,
tags = { ritual = 5, courier = 5, sealed = 3, pass = 4 }
},
{
id = "siva",
merchant = "Nysa Siva",
house = "Velai Glass & Scrip",
origin = "Luminaria",
location = "Luminaria",
style = "Glassware, scrip, invoice brokerage, and social tenders.",
profile = "A nimble house whose glass consignments and scrip brokerage thrive on reputation.",
skill = 0.84,
reliability = 0.81,
risk = 0.47,
rep = 12,
stable = 3,
tags = { glass = 5, scrip = 5, service = 4, fragile = 4 }
},
{
id = "sinders",
merchant = "Haldor bin Sinders",
house = "Giid-Tally Yard",
origin = "Acheron",
location = "Acheron",
style = "Prestige caravan work, heavy hauling, and Giid-Lisea-drawn cargo.",
profile = "The heaviest yard in the division, feared for slow starts but massive hauling capacity.",
skill = 0.80,
reliability = 0.83,
risk = 0.45,
rep = 11,
stable = 7,
tags = { heavy = 5, prestige = 4, bulk = 5, animal = 2, timber = 3 }
}
}
local HOUSE_BY_ID = {}
for _, h in ipairs(HOUSES) do
HOUSE_BY_ID[h.id] = h
end
------------------------------------------------------------------------
-- 4) STABLE / HAULING PRESTIGE
------------------------------------------------------------------------
local STABLES = {
[1] = { name = "licensed porter crew", prestige = 10, capacity = 1.0 },
[2] = { name = "reed-cart pair", prestige = 22, capacity = 1.2 },
[3] = { name = "mule and wagon string", prestige = 36, capacity = 1.5 },
[4] = { name = "woolly horse caravan", prestige = 52, capacity = 1.8 },
[5] = { name = "sealed carriage account", prestige = 68, capacity = 1.6 },
[6] = { name = "prestige horse stock", prestige = 84, capacity = 1.7 },
[7] = { name = "Giid-Lisea heavy team", prestige = 105, capacity = 2.4 }
}
local function stableName(idx)
local s = STABLES[idx] or STABLES[1]
return s.name
end
local function stablePrestige(idx)
local s = STABLES[idx] or STABLES[1]
return s.prestige or 0
end
local function stableCapacity(idx)
local s = STABLES[idx] or STABLES[1]
return s.capacity or 1
end
------------------------------------------------------------------------
-- 5) CARGO POOL
------------------------------------------------------------------------
local CARGO = {
{ name = "Rice", sector = "Agriculture", base = 9.00, unit = "tons", company = "Gladeseed Farmers' Union", tags = { "grain", "staple", "bulk" }, risk = 0.08 },
{ name = "Agrithor Orys grain", sector = "Agriculture", base = 12.50, unit = "tons", company = "Wardens of the Fields of Callope", tags = { "grain", "staple", "bulk" }, risk = 0.10 },
{ name = "Lamp oil", sector = "Agriculture", base = 18.00, unit = "casks", company = "Wardens of the Fields of Callope", tags = { "oil", "ritual", "staple" }, risk = 0.18 },
{ name = "Clean water casks", sector = "Agriculture", base = 16.00, unit = "casks", company = "Thal-Maen Water Desk", tags = { "water", "staple", "perishable" }, risk = 0.12 },
{ name = "Mountain greens", sector = "Agriculture", base = 22.00, unit = "crates", company = "Lenaian Water Company", tags = { "greens", "perishable", "water" }, risk = 0.25 },
{ name = "Spring-pond trout", sector = "Agriculture", base = 26.00, unit = "ice crates", company = "Lenaian Water Company", tags = { "fish", "perishable", "water" }, risk = 0.30 },
{ name = "Salt fish", sector = "Agriculture", base = 14.00, unit = "barrels", company = "Salt-Valed Ledger", tags = { "fish", "salt", "rough" }, risk = 0.16 },
{ name = "Cistern melons", sector = "Agriculture", base = 20.00, unit = "hampers", company = "Pontic Esperance Homestead Association", tags = { "perishable", "staple", "desert" }, risk = 0.24 },
{ name = "Ferry mats", sector = "Manufacturing", base = 13.00, unit = "bundles", company = "Thyrsa Maena Harbor Cooperative", tags = { "reed", "rope", "ferry", "bulk" }, risk = 0.10 },
{ name = "Cargo padding", sector = "Manufacturing", base = 11.00, unit = "rolls", company = "Thyrsa Maena Harbor Cooperative", tags = { "reed", "rope", "bulk" }, risk = 0.08 },
{ name = "Cane-fiber dock twine", sector = "Manufacturing", base = 17.00, unit = "spools", company = "Thyrsa Maena Harbor Cooperative", tags = { "rope", "reed", "ferry" }, risk = 0.08 },
{ name = "Glassware crates", sector = "Manufacturing", base = 31.00, unit = "crates", company = "Velai Glass & Scrip", tags = { "glass", "fragile", "luxury" }, risk = 0.38 },
{ name = "Invoice scrip packets", sector = "Services", base = 27.00, unit = "sealed packets", company = "Velai Glass & Scrip", tags = { "scrip", "service", "sealed", "courier" }, risk = 0.22 },
{ name = "Temple incense and anointing oil", sector = "Agriculture", base = 34.00, unit = "sealed jars", company = "Temple Bank of the Reformed Stripping Path", tags = { "ritual", "oil", "sealed", "luxury" }, risk = 0.28 },
{ name = "Relic-container seals", sector = "Services", base = 40.00, unit = "sets", company = "Temple Bank of the Reformed Stripping Path", tags = { "ritual", "sealed", "courier" }, risk = 0.32 },
{ name = "Fine horse stock papers", sector = "Services", base = 46.00, unit = "ledgers", company = "Ardashir Import Rooms", tags = { "animal", "prestige", "import", "sealed" }, risk = 0.35 },
{ name = "Luxury shrine linens", sector = "Services", base = 38.00, unit = "chests", company = "House Qamar", tags = { "luxury", "ritual", "sealed" }, risk = 0.30 },
{ name = "Courier packets", sector = "Services", base = 21.00, unit = "packets", company = "Ourid Shrine Accounts", tags = { "courier", "sealed", "pass" }, risk = 0.20 },
{ name = "Timber beams", sector = "Manufacturing", base = 19.00, unit = "loads", company = "Eikskald Factors", tags = { "timber", "bulk", "heavy", "cold" }, risk = 0.18 },
{ name = "Heavy dock hardware", sector = "Manufacturing", base = 29.00, unit = "crates", company = "Giid-Tally Yard", tags = { "heavy", "bulk", "prestige" }, risk = 0.22 },
{ name = "Regulated guard equipment", sector = "Manufacturing", base = 42.00, unit = "sealed cases", company = "Baratar Corporation", tags = { "sealed", "prestige", "rough" }, risk = 0.48 },
{ name = "Skybridge courier satchels", sector = "Services", base = 36.00, unit = "satchels", company = "Lothayan Skybridge Airways", tags = { "courier", "service", "sealed" }, risk = 0.26 },
{ name = "Dream-city luxury parcels", sector = "Services", base = 44.00, unit = "parcels", company = "Sandveil Commissions", tags = { "luxury", "night", "sealed" }, risk = 0.34 }
}
------------------------------------------------------------------------
-- 6) MARKET SIGNALS
------------------------------------------------------------------------
local function marketAverage(module, label)
if module and type(module.getAverageData) == "function" then
local ok, data = pcall(module.getAverageData)
if ok and type(data) == "table" then
return {
label = data.label or label,
level = data.avgLevel or "Standard",
pct = tonumber(data.pctChange) or 0,
sym = data.avgSym or "•"
}
end
end
return { label = label, level = "Standard", pct = 0, sym = "•" }
end
local function marketSignals()
return {
Agriculture = marketAverage(okAg and agPrices or nil, "Agriculture"),
Manufacturing = marketAverage(okManf and manfPrices or nil, "Mining / Manufacturing / Energy"),
Services = marketAverage(okServ and servPrices or nil, "Services")
}
end
local function sectorPulse(sector, signals)
local s = signals and signals[sector]
if not s then return 1.0 end
-- In the General Port price ledgers, "High" means pressure/scarcity;
-- this increases tender value and demand.
if s.level == "High" then return 1.14 end
if s.level == "Low" then return 0.94 end
return 1.0
end
------------------------------------------------------------------------
-- 7) ROUTE NETWORK
------------------------------------------------------------------------
local ROUTES = {
{ from = "Vaeringheim", to = "Mylecia", mode = "skybridge", km = 1479, risk = 0.18 },
{ from = "Vaeringheim", to = "Luminaria", mode = "road", km = 320, risk = 0.20 },
{ from = "Vaeringheim", to = "Serena", mode = "ferry", km = 210, risk = 0.18 },
{ from = "Vaeringheim", to = "Delphica", mode = "road", km = 410, risk = 0.22 },
{ from = "Vaeringheim", to = "Somniumpolis", mode = "ferry", km = 520, risk = 0.30 },
{ from = "Mylecia", to = "Jogi", mode = "road", km = 310, risk = 0.36 },
{ from = "Mylecia", to = "Sufriya", mode = "ferry", km = 460, risk = 0.34 },
{ from = "Mylecia", to = "Hayr Chaskal", mode = "rail", km = 880, risk = 0.30 },
{ from = "Jogi", to = "Lykopolis", mode = "road", km = 240, risk = 0.38 },
{ from = "Sufriya", to = "Keybir-Aviv", mode = "ferry", km = 360, risk = 0.40 },
{ from = "Keybir-Aviv", to = "Lykopolis", mode = "road", km = 290, risk = 0.42 },
{ from = "Hayr Chaskal", to = "Saruq Thyrsa", mode = "road", km = 430, risk = 0.33 },
{ from = "Saruq Thyrsa", to = "Thal Qencha", mode = "rail", km = 520, risk = 0.28 },
{ from = "Thal Qencha", to = "Mirovia", mode = "rail", km = 690, risk = 0.31 },
{ from = "Mirovia", to = "Ayllu Nysara", mode = "road", km = 570, risk = 0.35 },
{ from = "Ayllu Nysara", to = "Mylecia", mode = "skybridge", km = 1480, risk = 0.24 },
{ from = "Aurelia", to = "Vaeringheim", mode = "road", km = 360, risk = 0.19 },
{ from = "Erythros", to = "Aurelia", mode = "road", km = 300, risk = 0.24 },
{ from = "Acheron", to = "Erythros", mode = "road", km = 440, risk = 0.30 },
{ from = "Acheron", to = "Ourid", mode = "pass-road", km = 510, risk = 0.37 },
{ from = "Luminaria", to = "Ourid", mode = "pass-road", km = 480, risk = 0.34 },
{ from = "Riddersborg", to = "Vaeringheim", mode = "rail", km = 760, risk = 0.24 },
{ from = "Riddersborg", to = "Acheron", mode = "road", km = 620, risk = 0.32 },
{ from = "Delphica", to = "Luminaria", mode = "road", km = 340, risk = 0.20 },
{ from = "Delphica", to = "Vaeringheim", mode = "road", km = 410, risk = 0.22 },
{ from = "Somniumpolis", to = "Vaeringheim", mode = "ferry", km = 520, risk = 0.30 }
}
local ROUTES_FROM = {}
local function addRoute(r)
ROUTES_FROM[r.from] = ROUTES_FROM[r.from] or {}
table.insert(ROUTES_FROM[r.from], r)
end
for _, r in ipairs(ROUTES) do
addRoute(r)
addRoute({
from = r.to,
to = r.from,
mode = r.mode,
km = r.km,
risk = r.risk + 0.01
})
end
local function routesFrom(city)
return ROUTES_FROM[city] or {}
end
local function pickRoute(rec, house, ctx, day, idx)
local opts = routesFrom(rec.location or house.location or house.origin)
if not opts or #opts == 0 then opts = ROUTES end
return chooseWeighted(opts, ctx.seasonKey .. "|route|" .. house.id .. "|" .. day .. "|" .. idx, function(r)
local w = 1.0
if house.tags.ferry and r.mode == "ferry" then w = w + house.tags.ferry * 0.45 end
if house.tags.pass and r.mode == "pass-road" then w = w + house.tags.pass * 0.40 end
if house.tags.courier and r.mode == "skybridge" then w = w + house.tags.courier * 0.18 end
if house.tags.rough and r.risk >= 0.35 then w = w + house.tags.rough * 0.25 end
if house.risk < 0.45 and r.risk > 0.36 then w = w * 0.72 end
return w
end)
end
------------------------------------------------------------------------
-- 8) CARGO SELECTION
------------------------------------------------------------------------
local function hasTag(cargo, tag)
for _, t in ipairs(cargo.tags or {}) do
if t == tag then return true end
end
return false
end
local function cargoWeightForHouse(house, cargo, signals)
local w = 1.0
for _, tag in ipairs(cargo.tags or {}) do
if house.tags[tag] then
w = w + house.tags[tag] * 0.70
end
end
w = w * sectorPulse(cargo.sector, signals)
if cargo.risk and house.risk then
if cargo.risk > 0.35 and house.risk > 0.60 then
w = w + 1.4
elseif cargo.risk > 0.35 and house.risk < 0.45 then
w = w * 0.65
end
end
return w
end
local function pickCargo(house, ctx, day, idx, signals)
return chooseWeighted(CARGO, ctx.seasonKey .. "|cargo|" .. house.id .. "|" .. day .. "|" .. idx, function(c)
return cargoWeightForHouse(house, c, signals)
end)
end
------------------------------------------------------------------------
-- 9) SIMULATION
------------------------------------------------------------------------
local INCIDENTS = {
"inspection delay",
"weather hold",
"ferry queue",
"shrine-road crowding",
"manifest challenge",
"stable injury",
"wet-road damage",
"customs reweighing",
"rumor panic",
"route patrol diversion"
}
local function emptyRecord(house)
return {
house = house,
location = house.location or house.origin,
contracts = 0,
late = 0,
earnings = 0,
trade = 0,
rep = house.rep or 0,
incidents = 0,
sealed = 0,
ritual = 0,
km = 0,
stable = house.stable or 1,
stablePrestige = stablePrestige(house.stable or 1),
cargoMap = {},
modeMap = {},
lastTender = nil,
score = 0
}
end
local function tendersForDay(house, ctx, day)
local r = randKey(ctx.seasonKey .. "|tenders|" .. house.id .. "|" .. day)
local active = clamp((house.skill or 0.8) + (house.risk or 0.5) * 0.12, 0.65, 1.05)
if r < 0.18 - active * 0.06 then return 0 end
if r < 0.76 then return 1 end
if r < 0.95 then return 2 end
return 3
end
local function routePayMultiplier(mode)
if mode == "skybridge" then return 1.26 end
if mode == "rail" then return 1.12 end
if mode == "ferry" then return 1.08 end
if mode == "pass-road" then return 1.18 end
return 1.00
end
local function routeSpeed(mode)
if mode == "skybridge" then return 140 end
if mode == "rail" then return 70 end
if mode == "ferry" then return 28 end
if mode == "pass-road" then return 18 end
return 24
end
local function simulateTender(rec, house, ctx, day, idx, signals)
local route = pickRoute(rec, house, ctx, day, idx)
local cargo = pickCargo(house, ctx, day, idx, signals)
local capacity = stableCapacity(rec.stable or house.stable or 1)
local qtyPulse = 0.65 + randKey(ctx.seasonKey .. "|qty|" .. house.id .. "|" .. day .. "|" .. idx) * 1.10
local quantity = math.max(1, round(qtyPulse * capacity * 4))
local basePay = 64
+ (tonumber(route.km) or 0) * 0.115
+ (tonumber(cargo.base) or 10) * 3.4
+ (tonumber(route.risk) or 0.2) * 190
+ (tonumber(cargo.risk) or 0.1) * 120
+ quantity * 12
basePay = basePay * routePayMultiplier(route.mode)
basePay = basePay * sectorPulse(cargo.sector, signals)
local sealed = hasTag(cargo, "sealed") or randKey(ctx.seasonKey .. "|sealed|" .. house.id .. "|" .. day .. "|" .. idx) < 0.16
local ritual = hasTag(cargo, "ritual")
if sealed then basePay = basePay * 1.08 end
if ritual then basePay = basePay * 1.10 end
local riskLoad = (route.risk or 0.2) * 0.55 + (cargo.risk or 0.1) * 0.45
local onTimeChance =
(house.skill or 0.8) * 0.48
+ (house.reliability or 0.8) * 0.40
+ (stableCapacity(rec.stable or house.stable or 1) - 1.0) * 0.055
- riskLoad * 0.24
if route.mode == "skybridge" then onTimeChance = onTimeChance + 0.05 end
if route.mode == "ferry" then onTimeChance = onTimeChance - 0.02 end
if hasTag(cargo, "perishable") then onTimeChance = onTimeChance - 0.04 end
if hasTag(cargo, "fragile") then onTimeChance = onTimeChance - 0.04 end
onTimeChance = clamp(onTimeChance, 0.35, 0.96)
local onTime = randKey(ctx.seasonKey .. "|ontime|" .. house.id .. "|" .. day .. "|" .. idx) <= onTimeChance
local incidentChance = clamp(
(route.risk or 0.2) * 0.12
+ (cargo.risk or 0.1) * 0.08
+ (1 - (house.reliability or 0.8)) * 0.06,
0.02,
0.30
)
local incident = nil
if randKey(ctx.seasonKey .. "|incident|" .. house.id .. "|" .. day .. "|" .. idx) < incidentChance then
incident = pick(INCIDENTS, ctx.seasonKey .. "|incidentPick|" .. house.id .. "|" .. day .. "|" .. idx)
end
if incident and randKey(ctx.seasonKey .. "|incidentLate|" .. house.id .. "|" .. day .. "|" .. idx) < 0.55 then
onTime = false
end
local pay = round(basePay)
local paid = onTime and pay or round(pay * 0.62)
local travelHours = (route.km or 0) / routeSpeed(route.mode)
if not onTime then travelHours = travelHours * 1.25 end
if incident then travelHours = travelHours * 1.12 end
local tender = {
day = day,
date = formatDate(ctx, day),
houseId = house.id,
merchant = house.merchant,
house = house.house,
from = route.from,
to = route.to,
mode = route.mode,
km = route.km,
cargo = cargo.name,
sector = cargo.sector,
company = cargo.company,
unit = cargo.unit,
quantity = quantity,
value = pay,
paid = paid,
onTime = onTime,
late = not onTime,
incident = incident,
sealed = sealed,
ritual = ritual,
travelHours = travelHours,
risk = riskLoad
}
rec.contracts = rec.contracts + 1
rec.earnings = rec.earnings + paid
rec.trade = rec.trade + round(paid * (0.20 + randKey(ctx.seasonKey .. "|trade|" .. house.id .. "|" .. day .. "|" .. idx) * 0.55))
rec.km = rec.km + (route.km or 0)
rec.location = route.to
rec.lastTender = tender
if not onTime then rec.late = rec.late + 1 end
if incident then rec.incidents = rec.incidents + 1 end
if sealed then rec.sealed = rec.sealed + 1 end
if ritual then rec.ritual = rec.ritual + 1 end
rec.rep = rec.rep + (onTime and 1.15 or -1.65)
if sealed and onTime then rec.rep = rec.rep + 0.45 end
if ritual and onTime then rec.rep = rec.rep + 0.55 end
if incident then rec.rep = rec.rep - 0.35 end
rec.rep = clamp(rec.rep, 0, 40)
rec.cargoMap[cargo.name] = (rec.cargoMap[cargo.name] or 0) + 1
rec.modeMap[route.mode] = (rec.modeMap[route.mode] or 0) + 1
return tender
end
local function maybeStableUpgrade(rec, house, ctx, day, notices)
local r = randKey(ctx.seasonKey .. "|stable|" .. house.id .. "|" .. day)
local threshold = 0.018 + (house.risk or 0.5) * 0.012
if r < threshold then
rec.stable = clamp((rec.stable or house.stable or 1) + 1, 1, #STABLES)
rec.stablePrestige = stablePrestige(rec.stable)
table.insert(notices, {
day = day,
type = "stable",
text = house.merchant .. " upgraded stable papers to a " .. stableName(rec.stable) .. "."
})
end
end
local function computeScore(rec)
local score =
rec.contracts * 70
+ rec.earnings * 0.30
+ rec.trade * 0.045
+ rec.rep * 25
+ rec.stablePrestige * 0.55
+ rec.sealed * 30
+ rec.ritual * 25
+ rec.km * 0.028
- rec.late * 70
- rec.incidents * 20
rec.score = round(score)
return rec.score
end
local function simulateBoard(ctx)
local signals = marketSignals()
local records = {}
local tendersByDay = {}
local notices = {}
for _, house in ipairs(HOUSES) do
records[house.id] = emptyRecord(house)
end
for day = 1, ctx.day do
tendersByDay[day] = {}
for _, house in ipairs(HOUSES) do
local rec = records[house.id]
local n = tendersForDay(house, ctx, day)
for i = 1, n do
local tender = simulateTender(rec, house, ctx, day, i, signals)
table.insert(tendersByDay[day], tender)
if tender.value >= 520 or tender.sealed or tender.ritual or tender.incident or tender.late then
local status = tender.onTime and "completed" or "settled late"
local extra = ""
if tender.incident then extra = " after " .. tender.incident end
table.insert(notices, {
day = day,
type = tender.late and "late" or (tender.incident and "incident" or "contract"),
text = tender.merchant .. " " .. status .. " a " .. tender.cargo .. " tender from " .. tender.from .. " to " .. tender.to .. extra .. "."
})
end
end
maybeStableUpgrade(rec, house, ctx, day, notices)
end
table.sort(tendersByDay[day], function(a, b)
if a.value ~= b.value then return a.value > b.value end
return a.merchant < b.merchant
end)
end
local list = {}
for _, rec in pairs(records) do
computeScore(rec)
table.insert(list, rec)
end
table.sort(list, function(a, b)
if a.score ~= b.score then return a.score > b.score end
if a.earnings ~= b.earnings then return a.earnings > b.earnings end
if a.contracts ~= b.contracts then return a.contracts > b.contracts end
return a.house.merchant < b.house.merchant
end)
for i, rec in ipairs(list) do
rec.rank = i
end
return {
ctx = ctx,
signals = signals,
records = records,
list = list,
tendersByDay = tendersByDay,
notices = notices
}
end
------------------------------------------------------------------------
-- 10) RENDER HELPERS
------------------------------------------------------------------------
local function topCargo(rec)
local bestName, bestCount = "—", 0
for name, count in pairs(rec.cargoMap or {}) do
if count > bestCount or (count == bestCount and name < bestName) then
bestName = name
bestCount = count
end
end
if bestCount <= 0 then return "—" end
return bestName .. " (" .. tostring(bestCount) .. ")"
end
local function modeSummary(rec)
local parts = {}
local order = { "road", "rail", "ferry", "skybridge", "pass-road" }
for _, mode in ipairs(order) do
local n = rec.modeMap and rec.modeMap[mode] or 0
if n > 0 then table.insert(parts, mode .. " " .. tostring(n)) end
end
if #parts == 0 then return "—" end
return table.concat(parts, "<br>")
end
local function renderSignalLine(signals)
local parts = {}
for _, sector in ipairs({ "Agriculture", "Manufacturing", "Services" }) do
local s = signals[sector]
if s then
table.insert(parts, (s.label or sector) .. ": " .. (s.level or "Standard") .. " " .. string.format("(%.2f%%)", s.pct or 0))
end
end
return table.concat(parts, " · ")
end
------------------------------------------------------------------------
-- 11) PUBLIC RENDER FUNCTIONS
------------------------------------------------------------------------
function p.renderSeasonInfo(frame)
local ctx = seasonArgs(frame)
local board = simulateBoard(ctx)
local leader = board.list[1]
local out = {}
out[#out+1] = '{| class="wikitable"'
out[#out+1] = '|-'
out[#out+1] = '! colspan="2" | Vaeringheim Merchant League - Invitational Division'
out[#out+1] = '|-'
out[#out+1] = '| Season || ' .. formatSeason(ctx)
out[#out+1] = '|-'
out[#out+1] = '| Season day || ' .. tostring(ctx.day) .. ' of 61'
out[#out+1] = '|-'
out[#out+1] = '| Board status || ' .. (ctx.day >= 61 and "Final table" or "Active competition")
out[#out+1] = '|-'
out[#out+1] = '| Current leader || ' .. (leader and (leader.house.merchant .. " (" .. leader.house.house .. ")") or "—")
out[#out+1] = '|-'
out[#out+1] = '| Market signals || ' .. renderSignalLine(board.signals)
out[#out+1] = '|}'
return table.concat(out, "\n")
end
function p.renderStandings(frame)
local ctx = seasonArgs(frame)
local board = simulateBoard(ctx)
local out = {}
out[#out+1] = '{| class="wikitable sortable"'
out[#out+1] = '|+ Vaeringheim Merchant League - Invitational Division standings, ' .. formatSeason(ctx) .. ' — day ' .. tostring(ctx.day) .. ' of 61'
out[#out+1] = '|-'
out[#out+1] = '! Pos !! Merchant !! House !! Origin !! Contracts !! Late !! Income !! Reputation !! Stable !! Top cargo !! Score'
for _, rec in ipairs(board.list) do
local style = ""
if rec.rank == 1 then
style = ' style="background-color:#fff2cc;"'
elseif rec.rank <= 3 then
style = ' style="background-color:#e6ffe6;"'
elseif rec.rank >= 11 then
style = ' style="background-color:#ffe6e6;"'
end
out[#out+1] = '|-' .. style
out[#out+1] = '| ' .. tostring(rec.rank)
.. ' || ' .. rec.house.merchant
.. ' || ' .. rec.house.house
.. ' || ' .. rec.house.origin
.. ' || ' .. tostring(rec.contracts)
.. ' || ' .. tostring(rec.late)
.. ' || ' .. formatMoney(rec.earnings)
.. ' || ' .. string.format("%.1f", rec.rep)
.. ' || ' .. stableName(rec.stable)
.. ' || ' .. topCargo(rec)
.. ' || ' .. tostring(rec.score)
end
out[#out+1] = '|}'
return table.concat(out, "\n")
end
p.standings = p.renderStandings
p.leaderboard = p.renderStandings
function p.renderSchedule(frame)
local args = (frame and frame.args) or {}
local ctx = seasonArgs(frame)
local startDay = tonumber(args.start or "") or 1
local endDay = tonumber(args["end"] or args.finish or "") or DAYS_IN_SEASON
startDay = clamp(startDay, 1, DAYS_IN_SEASON)
endDay = clamp(endDay, startDay, DAYS_IN_SEASON)
local out = {}
out[#out+1] = '{| class="wikitable sortable"'
out[#out+1] = '|+ Daily featured tender schedule, ' .. formatSeason(ctx)
out[#out+1] = '|-'
out[#out+1] = '! Day !! Date !! Featured board notice !! Likely route !! Leading claimant'
for day = startDay, endDay do
local fakeCtx = {
year = ctx.year,
monthIndex = ctx.monthIndex,
month = ctx.month,
day = day,
seasonKey = ctx.seasonKey
}
local house = pick(HOUSES, ctx.seasonKey .. "|scheduleHouse|" .. day)
local rec = emptyRecord(house)
local signals = marketSignals()
local route = pickRoute(rec, house, fakeCtx, day, 1)
local cargo = pickCargo(house, fakeCtx, day, 1, signals)
out[#out+1] = '|-'
out[#out+1] = '| ' .. tostring(day)
.. ' || ' .. formatDate(ctx, day)
.. ' || ' .. cargo.name
.. ' || ' .. route.from .. ' → ' .. route.to .. ' (' .. route.mode .. ')'
.. ' || ' .. house.merchant .. ', ' .. house.house
end
out[#out+1] = '|}'
return table.concat(out, "\n")
end
function p.renderTenderBoard(frame)
local ctx = seasonArgs(frame)
local board = simulateBoard(ctx)
local day = ctx.day
local tenders = board.tendersByDay[day] or {}
local limit = tonumber((frame and frame.args and frame.args.limit) or "") or 12
local out = {}
out[#out+1] = '{| class="wikitable sortable"'
out[#out+1] = '|+ Public tender board, ' .. formatDate(ctx, day)
out[#out+1] = '|-'
out[#out+1] = '! Rank !! Claimant !! Cargo !! Route !! Mode !! Quantity !! Posted value !! Result !! Incident'
for i = 1, math.min(limit, #tenders) do
local t = tenders[i]
out[#out+1] = '|-'
out[#out+1] = '| ' .. tostring(i)
.. ' || ' .. t.merchant .. '<br><small>' .. t.house .. '</small>'
.. ' || ' .. t.cargo .. '<br><small>' .. t.company .. '</small>'
.. ' || ' .. t.from .. ' → ' .. t.to
.. ' || ' .. t.mode
.. ' || ' .. tostring(t.quantity) .. ' ' .. t.unit
.. ' || ' .. formatMoney(t.value)
.. ' || ' .. (t.onTime and "On time" or "Late")
.. ' || ' .. (t.incident or "—")
end
if #tenders == 0 then
out[#out+1] = '|-'
out[#out+1] = '| colspan="9" | No public tenders were reconciled for this day.'
end
out[#out+1] = '|}'
return table.concat(out, "\n")
end
function p.renderTenderReport(frame)
local args = (frame and frame.args) or {}
local ctx = seasonArgs(frame)
local board = simulateBoard(ctx)
local day = tonumber(args.day or args.d or "") or ctx.day
day = clamp(day, 1, ctx.day)
local index = tonumber(args.tender or args.index or args[4] or "") or 1
local tenders = board.tendersByDay[day] or {}
local t = tenders[index] or tenders[1]
if not t then
return "'''No tender report is available for " .. formatDate(ctx, day) .. ".'''"
end
local out = {}
out[#out+1] = "'''Tender Report — " .. t.cargo .. " to " .. t.to .. "'''"
out[#out+1] = ""
out[#out+1] = t.merchant .. " of " .. t.house .. " claimed a " .. t.cargo .. " tender from " .. t.from .. " to " .. t.to .. ". The consignment moved by " .. t.mode .. " over approximately " .. tostring(round(t.km)) .. " km. The posted value was " .. formatMoney(t.value) .. ", with final settlement recorded at " .. formatMoney(t.paid) .. "."
out[#out+1] = ""
out[#out+1] = '{| class="wikitable"'
out[#out+1] = '|-'
out[#out+1] = '! Field !! Value'
out[#out+1] = '|-'
out[#out+1] = '| Date || ' .. formatDate(ctx, day)
out[#out+1] = '|-'
out[#out+1] = '| Merchant || ' .. t.merchant
out[#out+1] = '|-'
out[#out+1] = '| House || ' .. t.house
out[#out+1] = '|-'
out[#out+1] = '| Cargo || ' .. t.cargo
out[#out+1] = '|-'
out[#out+1] = '| Sector || ' .. t.sector
out[#out+1] = '|-'
out[#out+1] = '| Route || ' .. t.from .. ' → ' .. t.to
out[#out+1] = '|-'
out[#out+1] = '| Mode || ' .. t.mode
out[#out+1] = '|-'
out[#out+1] = '| Distance || ' .. tostring(round(t.km)) .. ' km'
out[#out+1] = '|-'
out[#out+1] = '| Quantity || ' .. tostring(t.quantity) .. ' ' .. t.unit
out[#out+1] = '|-'
out[#out+1] = '| Posted value || ' .. formatMoney(t.value)
out[#out+1] = '|-'
out[#out+1] = '| Settlement || ' .. formatMoney(t.paid)
out[#out+1] = '|-'
out[#out+1] = '| Result || ' .. (t.onTime and "On time" or "Late")
out[#out+1] = '|-'
out[#out+1] = '| Incident || ' .. (t.incident or "—")
out[#out+1] = '|-'
out[#out+1] = '| Sealed cargo || ' .. (t.sealed and "Yes" or "No")
out[#out+1] = '|-'
out[#out+1] = '| Ritual cargo || ' .. (t.ritual and "Yes" or "No")
out[#out+1] = '|}'
return table.concat(out, "\n")
end
function p.renderDailyNotices(frame)
local ctx = seasonArgs(frame)
local board = simulateBoard(ctx)
local args = (frame and frame.args) or {}
local limit = tonumber(args.limit or "") or 10
local filtered = {}
for i = #board.notices, 1, -1 do
local n = board.notices[i]
if n.day <= ctx.day then
table.insert(filtered, n)
end
if #filtered >= limit then break end
end
local out = {}
out[#out+1] = '{| class="wikitable"'
out[#out+1] = '|+ League notices, ' .. formatSeason(ctx) .. ' through day ' .. tostring(ctx.day)
out[#out+1] = '|-'
out[#out+1] = '! Day !! Type !! Notice'
for _, n in ipairs(filtered) do
out[#out+1] = '|-'
out[#out+1] = '| ' .. tostring(n.day)
.. ' || ' .. n.type
.. ' || ' .. n.text
end
if #filtered == 0 then
out[#out+1] = '|-'
out[#out+1] = '| colspan="3" | No major league notices have been posted yet.'
end
out[#out+1] = '|}'
return table.concat(out, "\n")
end
p.notices = p.renderDailyNotices
local function findHouse(query)
if not query or query == "" then return nil end
query = mw.ustring.lower(tostring(query))
for _, h in ipairs(HOUSES) do
local id = mw.ustring.lower(h.id or "")
local merchant = mw.ustring.lower(h.merchant or "")
local house = mw.ustring.lower(h.house or "")
if id == query or merchant:find(query, 1, true) or house:find(query, 1, true) then
return h
end
end
return nil
end
function p.renderHouseLedger(frame)
local args = (frame and frame.args) or {}
local ctx = seasonArgs(frame)
local board = simulateBoard(ctx)
local query = args.house or args.merchant or args[4] or args[1]
local h = findHouse(query)
if not h then
local options = {}
for _, x in ipairs(HOUSES) do
table.insert(options, x.house)
end
return "'''Unknown merchant house.''' Available houses: " .. table.concat(options, "; ")
end
local rec = board.records[h.id]
if not rec then return "'''No ledger record found for " .. h.house .. ".'''" end
local out = {}
out[#out+1] = '{| class="wikitable"'
out[#out+1] = '|+ Ledger profile — ' .. h.house .. ', ' .. formatSeason(ctx)
out[#out+1] = '|-'
out[#out+1] = '! Field !! Value'
out[#out+1] = '|-'
out[#out+1] = '| Merchant || ' .. h.merchant
out[#out+1] = '|-'
out[#out+1] = '| House || ' .. h.house
out[#out+1] = '|-'
out[#out+1] = '| Origin || ' .. h.origin
out[#out+1] = '|-'
out[#out+1] = '| Current location || ' .. rec.location
out[#out+1] = '|-'
out[#out+1] = '| Profile || ' .. h.profile
out[#out+1] = '|-'
out[#out+1] = '| Rank || #' .. tostring(rec.rank or "—")
out[#out+1] = '|-'
out[#out+1] = '| Score || ' .. tostring(rec.score)
out[#out+1] = '|-'
out[#out+1] = '| Contracts || ' .. tostring(rec.contracts)
out[#out+1] = '|-'
out[#out+1] = '| Late deliveries || ' .. tostring(rec.late)
out[#out+1] = '|-'
out[#out+1] = '| Income || ' .. formatMoney(rec.earnings)
out[#out+1] = '|-'
out[#out+1] = '| Trade turnover || ' .. formatMoney(rec.trade)
out[#out+1] = '|-'
out[#out+1] = '| Reputation || ' .. string.format("%.1f", rec.rep)
out[#out+1] = '|-'
out[#out+1] = '| Stable papers || ' .. stableName(rec.stable)
out[#out+1] = '|-'
out[#out+1] = '| Distance moved || ' .. tostring(round(rec.km)) .. ' km'
out[#out+1] = '|-'
out[#out+1] = '| Sealed tenders || ' .. tostring(rec.sealed)
out[#out+1] = '|-'
out[#out+1] = '| Ritual tenders || ' .. tostring(rec.ritual)
out[#out+1] = '|-'
out[#out+1] = '| Route modes || ' .. modeSummary(rec)
out[#out+1] = '|-'
out[#out+1] = '| Top cargo || ' .. topCargo(rec)
out[#out+1] = '|}'
return table.concat(out, "\n")
end
p.house = p.renderHouseLedger
function p.renderMarketSignals(frame)
local signals = marketSignals()
local out = {}
out[#out+1] = '{| class="wikitable sortable"'
out[#out+1] = '|+ General Port market signals used by the Invitational Division'
out[#out+1] = '|-'
out[#out+1] = '! Sector !! Signal !! Change !! League effect'
for _, sector in ipairs({ "Agriculture", "Manufacturing", "Services" }) do
local s = signals[sector]
local effect = "Standard tender weighting"
if s.level == "High" then
effect = "Higher scarcity pressure; tenders receive higher value weighting"
elseif s.level == "Low" then
effect = "Lower scarcity pressure; tenders receive lower value weighting"
end
out[#out+1] = '|-'
out[#out+1] = '| ' .. (s.label or sector)
.. ' || ' .. (s.level or "Standard") .. ' ' .. (s.sym or "•")
.. ' || ' .. string.format("%.2f%%", s.pct or 0)
.. ' || ' .. effect
end
out[#out+1] = '|}'
return table.concat(out, "\n")
end
function p.renderAwards(frame)
local ctx = seasonArgs(frame)
local board = simulateBoard(ctx)
local tenders = board.tendersByDay[ctx.day] or {}
local bestValue, bestOnTime, worstLate
for _, t in ipairs(tenders) do
if not bestValue or t.value > bestValue.value then bestValue = t end
if t.onTime and (not bestOnTime or t.paid > bestOnTime.paid) then bestOnTime = t end
if t.late and (not worstLate or t.value > worstLate.value) then worstLate = t end
end
local out = {}
out[#out+1] = '{| class="wikitable"'
out[#out+1] = '|+ Daily league distinctions, ' .. formatDate(ctx, ctx.day)
out[#out+1] = '|-'
out[#out+1] = '! Distinction !! Recipient !! Basis'
out[#out+1] = '|-'
out[#out+1] = '| Seal of the Board || ' .. (bestValue and bestValue.merchant or "—") .. ' || ' .. (bestValue and (bestValue.cargo .. " worth " .. formatMoney(bestValue.value)) or "No tender")
out[#out+1] = '|-'
out[#out+1] = '| Fleet-Footed Ledger || ' .. (bestOnTime and bestOnTime.merchant or "—") .. ' || ' .. (bestOnTime and ("On-time delivery to " .. bestOnTime.to) or "No on-time tender")
out[#out+1] = '|-'
out[#out+1] = '| Red Stamp of Delay || ' .. (worstLate and worstLate.merchant or "—") .. ' || ' .. (worstLate and ("Late " .. worstLate.cargo .. " tender") or "No late tender")
out[#out+1] = '|}'
return table.concat(out, "\n")
end
p.awards = p.renderAwards
function p.renderFull(frame)
local out = {}
out[#out+1] = p.renderSeasonInfo(frame)
out[#out+1] = ""
out[#out+1] = p.renderStandings(frame)
out[#out+1] = ""
out[#out+1] = p.renderAwards(frame)
out[#out+1] = ""
out[#out+1] = p.renderTenderBoard(frame)
out[#out+1] = ""
out[#out+1] = p.renderDailyNotices(frame)
return table.concat(out, "\n")
end
p.full = p.renderFull
function p.doc(frame)
return table.concat({
"'''Vaeringheim Merchant League - Invitational Division module'''",
"",
"Common invocations:",
"* <code>{{#invoke:VaeringheimMerchantLeagueInvitational|renderFull}}</code>",
"* <code>{{#invoke:VaeringheimMerchantLeagueInvitational|renderStandings}}</code>",
"* <code>{{#invoke:VaeringheimMerchantLeagueInvitational|renderStandings|year=54|month=Thalassiel|day=61}}</code>",
"* <code>{{#invoke:VaeringheimMerchantLeagueInvitational|renderSchedule|year=54|month=Atosiel}}</code>",
"* <code>{{#invoke:VaeringheimMerchantLeagueInvitational|renderTenderBoard|limit=20}}</code>",
"* <code>{{#invoke:VaeringheimMerchantLeagueInvitational|renderTenderReport|day=14|tender=1}}</code>",
"* <code>{{#invoke:VaeringheimMerchantLeagueInvitational|renderHouseLedger|house=Giid-Tally Yard}}</code>",
"* <code>{{#invoke:VaeringheimMerchantLeagueInvitational|renderMarketSignals}}</code>",
"",
"Seasons last 61 days and reset at the beginning of each PSSC month."
}, "\n")
end
return p