Module:CultRelationsDailyBVR: Difference between revisions
From MicrasWiki
Jump to navigationJump to search
NewZimiaGov (talk | contribs) No edit summary |
NewZimiaGov (talk | contribs) No edit summary |
||
| Line 615: | Line 615: | ||
city = trim(city) | city = trim(city) | ||
return string.format("[[List of cities in Bassaridia Vaeringheim#%s|%s]]", city, city) | return string.format("[[List of cities in Bassaridia Vaeringheim#%s|%s]]", city, city) | ||
end | |||
local function eventCityLink(event) | |||
if not event then | |||
return "—" | |||
end | |||
local cultName = normalizeCultName(event.a) | |||
local cult = cultByName[cultName] | |||
if not cult or not cult.city or cult.city == "" then | |||
return "—" | |||
end | |||
return cityLink(cult.city) | |||
end | end | ||
| Line 1,411: | Line 1,426: | ||
table.insert(out, string.format("'''Daily cult-relations event log — %s'''", data.date.longText)) | table.insert(out, string.format("'''Daily cult-relations event log — %s'''", data.date.longText)) | ||
table.insert(out, '{| class="wikitable sortable" style="width:100%; text-align:left;"') | table.insert(out, '{| class="wikitable sortable" style="width:100%; text-align:left;"') | ||
table.insert(out, '! Event !! Severity !! Cults !! Effect !! Notes') | table.insert(out, '! Event !! Severity !! Location !! Cults !! Effect !! Notes') | ||
for _, e in ipairs(data.events or {}) do | for _, e in ipairs(data.events or {}) do | ||
table.insert(out, string.format( | |||
"%s at %s: %s ↔ %s (%s)", | |||
e.title, | |||
eventCityLink(e), | |||
e.a, | |||
e.b, | |||
signed(e.delta) | |||
)) | |||
end | end | ||
Revision as of 21:05, 1 June 2026
Documentation for this module may be created at Module:CultRelationsDailyBVR/doc
-- Module:CultRelationsDailyBVR
-- Dynamic cult-relations index for the Reformed Stripping Path.
--
-- This module:
-- * Uses [[Module:BassaridianCalendar]] for the current PSSC date.
-- * Stores a fixed roster of cults/mysteries from Planetary Devotion.
-- * Starts from baseline alliances and rivalries.
-- * Applies deterministic daily events so relations change automatically by day.
-- * Provides table, pair, matrix, event-log, and raw-data functions.
--
-- Suggested invocations:
-- {{#invoke:CultRelationsDailyBVR|makeTable}}
-- {{#invoke:CultRelationsDailyBVR|makeTable|limit=30}}
-- {{#invoke:CultRelationsDailyBVR|makeTable|year=53|day=95}}
-- {{#invoke:CultRelationsDailyBVR|makePair|a=Harmony Sanctum|b=Ignis Aeternum}}
-- {{#invoke:CultRelationsDailyBVR|eventLog}}
-- {{#invoke:CultRelationsDailyBVR|makeMatrix}}
-- {{#invoke:CultRelationsDailyBVR|legend}}
--
-- Optional bridge from [[Module:MissionaryDailyOpsBVR]]:
-- local cultRel = require("Module:CultRelationsDailyBVR")
-- cultRel.makeTable(frame)
local p = {}
local calOk, cal = pcall(require, "Module:BassaridianCalendar")
if not calOk then
cal = nil
end
local DAYS_IN_YEAR = 183
--------------------------------------------------------------------------------
-- 1. BASIC UTILITIES
--------------------------------------------------------------------------------
local function trim(s)
s = tostring(s or "")
return (s:gsub("^%s+", ""):gsub("%s+$", ""))
end
local function lower(s)
return mw.ustring.lower(trim(s))
end
local function clamp(n, lo, hi)
n = tonumber(n) or 0
if n < lo then return lo end
if n > hi then return hi end
return n
end
local function sortedPair(a, b)
a, b = trim(a), trim(b)
if a < b then
return a .. "||" .. b
end
return b .. "||" .. a
end
local function splitPairKey(k)
local a, b = tostring(k or ""):match("^(.-)%|%|(.*)$")
return a or "", b or ""
end
local function tableContains(t, value)
for _, v in ipairs(t or {}) do
if v == value then
return true
end
end
return false
end
local function shallowCopy(t)
local out = {}
for k, v in pairs(t or {}) do
out[k] = v
end
return out
end
local function hash31(str)
str = tostring(str or "")
local h = 0
for i = 1, #str do
h = (h * 131 + str:byte(i)) % 2147483647
end
return h
end
local function lcg(seed)
local s = tonumber(seed) or 1
s = s % 2147483647
if s <= 0 then
s = s + 2147483646
end
return function()
s = (1103515245 * s + 12345) % 2147483647
return s
end
end
local function pick(nextInt, list)
if not list or #list == 0 then
return nil
end
return list[(nextInt() % #list) + 1]
end
local function pickWeighted(nextInt, list)
local total = 0
for _, item in ipairs(list or {}) do
total = total + (tonumber(item.w) or 0)
end
if total <= 0 then
return list and list[1] or nil
end
local r = nextInt() % total
local acc = 0
for _, item in ipairs(list) do
acc = acc + (tonumber(item.w) or 0)
if r < acc then
return item
end
end
return list[#list]
end
--------------------------------------------------------------------------------
-- 2. DATE HANDLING
--------------------------------------------------------------------------------
local monthNames = {
[1] = "Atosiel",
[2] = "Thalassiel",
[3] = "Opsitheiel"
}
local zodiacByDay = {
[1] = {
{12, "Atosien"},
{24, "Eosena"},
{36, "Micrasha"},
{48, "Pyreska"},
{61, "Indomin"}
},
[2] = {
{12, "Chrysen"},
{24, "Thalassian"},
{36, "Nephelia"},
{48, "Glinaeus"},
{61, "Noctien"}
},
[3] = {
{12, "Opsithia"},
{24, "Stygian"},
{36, "Faunian"},
{48, "Silenian"},
{61, "Catosien"}
}
}
local function doyToMonthDom(doy)
doy = clamp(doy, 1, DAYS_IN_YEAR)
if doy <= 61 then
return 1, doy
elseif doy <= 122 then
return 2, doy - 61
else
return 3, doy - 122
end
end
local function monthDomToDoy(month, dom)
month = tonumber(month) or 1
dom = tonumber(dom) or 1
if month == 1 then
return clamp(dom, 1, 61)
elseif month == 2 then
return 61 + clamp(dom, 1, 61)
else
return 122 + clamp(dom, 1, 61)
end
end
local function zodiacFor(monthNum, dom)
local rows = zodiacByDay[monthNum] or zodiacByDay[1]
for _, row in ipairs(rows) do
if dom <= row[1] then
return row[2]
end
end
return rows[#rows][2]
end
local function parseCalendarString(raw)
raw = tostring(raw or "")
-- Expected from getCurrentDateNoProverb:
-- "95, Thalassiel (Noctien), 53 PSSC"
local doy, month, zodiac, year = raw:match("^(%d+),%s*([%a]+)%s*%(([^%)]+)%),%s*(%d+)%s*PSSC")
if doy and month and year then
local d = tonumber(doy)
local y = tonumber(year)
local m = 1
if month == "Thalassiel" then
m = 2
elseif month == "Opsitheiel" then
m = 3
end
local mn, dom = doyToMonthDom(d)
return {
year = y,
dayOfYear = d,
monthNum = mn,
month = monthNames[mn],
dom = dom,
zodiac = zodiac or zodiacFor(mn, dom),
raw = raw,
text = string.format("%d/%d/%d PSSC", dom, mn, y),
longText = string.format("%d, %s (%s), %d PSSC", d, monthNames[mn], zodiac or zodiacFor(mn, dom), y)
}
end
-- Accept a simpler "day/month/year PSSC" pattern.
local dom2, month2, year2 = raw:match("^(%d+)/(%d+)/(%d+)%s*PSSC")
if dom2 and month2 and year2 then
local mn = clamp(tonumber(month2), 1, 3)
local dm = clamp(tonumber(dom2), 1, 61)
local y = tonumber(year2)
local d = monthDomToDoy(mn, dm)
local z = zodiacFor(mn, dm)
return {
year = y,
dayOfYear = d,
monthNum = mn,
month = monthNames[mn],
dom = dm,
zodiac = z,
raw = raw,
text = string.format("%d/%d/%d PSSC", dm, mn, y),
longText = string.format("%d, %s (%s), %d PSSC", d, monthNames[mn], z, y)
}
end
return nil
end
local function getDateInfo(args)
args = args or {}
local y = tonumber(args.year or args.psscyear)
local doy = tonumber(args.day or args.dayofyear or args.doy)
if y and doy then
doy = clamp(doy, 1, DAYS_IN_YEAR)
local mn, dom = doyToMonthDom(doy)
local z = zodiacFor(mn, dom)
return {
year = y,
dayOfYear = doy,
monthNum = mn,
month = monthNames[mn],
dom = dom,
zodiac = z,
raw = string.format("%d, %s (%s), %d PSSC", doy, monthNames[mn], z, y),
text = string.format("%d/%d/%d PSSC", dom, mn, y),
longText = string.format("%d, %s (%s), %d PSSC", doy, monthNames[mn], z, y)
}
end
local raw = ""
if cal and type(cal.getCurrentDateNoProverb) == "function" then
raw = cal.getCurrentDateNoProverb()
elseif cal and type(cal.getCurrentDate) == "function" then
raw = cal.getCurrentDate()
end
local parsed = parseCalendarString(raw)
if parsed then
return parsed
end
-- Stable fallback if the calendar module is unavailable.
local fallbackYear = 53
local fallbackDay = 1
local mn, dom = doyToMonthDom(fallbackDay)
local z = zodiacFor(mn, dom)
return {
year = fallbackYear,
dayOfYear = fallbackDay,
monthNum = mn,
month = monthNames[mn],
dom = dom,
zodiac = z,
raw = "Calendar unavailable",
text = string.format("%d/%d/%d PSSC", dom, mn, fallbackYear),
longText = string.format("%d, %s (%s), %d PSSC", fallbackDay, monthNames[mn], z, fallbackYear)
}
end
--------------------------------------------------------------------------------
-- 3. CULT ROSTER
-- Based on Planetary Devotion / Mystery table.
--------------------------------------------------------------------------------
local cults = {
{
key = "OSI",
name = "Ordo Solis Invicti",
divine = "Atos",
city = "Vaeringheim",
page = "Atos (Reformed Stripping Path)",
anchor = "Ordo Solis Invicti",
logo = "AtosSygil.png",
tendency = {"order", "measure", "light", "state"}
},
{
key = "AUR",
name = "Order of Aurora Mystica",
divine = "Eos",
city = "Luminaria",
page = "Eos (Reformed Stripping Path)",
anchor = "Order of Aurora Mystica",
logo = "Auroramystica.png",
tendency = {"dawn", "renewal", "illumination", "mediation"}
},
{
key = "HAR",
name = "Harmony Sanctum",
divine = "Micras",
city = "Serena",
page = "Micras (Reformed Stripping Path)",
anchor = "Harmony Sanctum",
logo = "Harmonysanctum.png",
tendency = {"balance", "wisdom", "mediation", "order"}
},
{
key = "IGN",
name = "Ignis Aeternum",
divine = "Pyros",
city = "Pyralis",
page = "Pyros (Reformed Stripping Path)",
anchor = "Ignis Aeternum",
logo = "Ignis Aeternum.png",
tendency = {"fire", "renewal", "passion", "destruction"}
},
{
key = "CHS",
name = "Celestial Harmony Sect",
divine = "Indigo and Momiji",
city = "Symphonara",
page = "Indigo (Reformed Stripping Path)",
anchor = "Celestial Harmony Sect",
logo = "HarmonySect.png",
tendency = {"duality", "change", "mediation", "ambiguity"}
},
{
key = "GGS",
name = "Guild of Golden Shadows",
divine = "Chrysos",
city = "Aurelia",
page = "Chrysos (Reformed Stripping Path)",
anchor = "Guild of Golden Shadows",
logo = "GoldenShadows.png",
tendency = {"wealth", "trade", "secrecy", "espionage"}
},
{
key = "AZU",
name = "Azure Sentinel Sect",
divine = "Thalassa",
city = "Vaeringheim",
page = "Thalassa (Reformed Stripping Path)",
anchor = "Azure Sentinel Sect",
logo = "AzureSentinel.png",
tendency = {"sea", "war", "depth", "mystery"}
},
{
key = "RNB",
name = "Reverie Nebulous",
divine = "Nephele",
city = "Somniumpolis",
page = "Nephele (Reformed Stripping Path)",
anchor = "Reverie Nebulous",
logo = "RevNeb.png",
tendency = {"dreams", "clouds", "imagination", "omens"}
},
{
key = "EOF",
name = "Eon Fellowship",
divine = "Glinos",
city = "Nexa",
page = "Glinos (Reformed Stripping Path)",
anchor = "Eon Fellowship",
logo = "EonFel.png",
tendency = {"winter", "death", "cycle", "endurance"}
},
{
key = "UMB",
name = "Order of the Umbral Oracle",
divine = "Noctis",
city = "Lunalis Sancta",
page = "Noctis (Reformed Stripping Path)",
anchor = "Order of the Umbral Oracle",
logo = "Umbral.png",
tendency = {"night", "secrecy", "truth", "introspection"}
},
{
key = "VDE",
name = "Mystery of the Verdant Embrace",
divine = "Opsithe",
city = "Sylvapolis",
page = "Opsithe (Reformed Stripping Path)",
anchor = "Mystery of the Verdant Embrace",
logo = "Verdant.png",
tendency = {"harvest", "fertility", "nature", "stability"}
},
{
key = "CON",
name = "Conclave Illuminara",
divine = "Tarsica",
city = "Luminaria",
page = "Tarsica (Reformed Stripping Path)",
anchor = "Conclave Illuminara",
logo = "Conclave.png",
tendency = {"illumination", "wisdom", "moon", "eclipse"}
},
{
key = "SVI",
name = "Sanctum Vitalis",
divine = "Plateau",
city = "Saluria",
page = "Plateau (Reformed Stripping Path)",
anchor = "Sanctum Vitalis",
logo = "Vitalis.png",
tendency = {"health", "medicine", "relief", "stability"}
},
{
key = "ALB",
name = "Temple Alabaster",
divine = "Ivory",
city = "Aetherium",
page = "Ivory (Reformed Stripping Path)",
anchor = "Temple Alabaster",
logo = "Alabaster.png",
tendency = {"purity", "serenity", "neutrality", "healing"}
},
{
key = "CICL",
name = "Court of the Ironclad",
divine = "Fenrir",
city = "Ferrum Citadel",
page = "Fenrir (Reformed Stripping Path)",
anchor = "Court of the Ironclad",
logo = "Ironclad.png",
tendency = {"strength", "tenacity", "security", "militancy"}
},
{
key = "ACC",
name = "Accord Concordia",
divine = "Lukedu",
city = "Symphonara",
page = "Lukedu (Reformed Stripping Path)",
anchor = "Accord Concordia",
logo = "Concordia.png",
tendency = {"unity", "peace", "mediation", "diplomacy"}
},
{
key = "STY",
name = "Mystery of the Stygian Veil",
divine = "Styx",
city = "Acheron",
page = "Styx (Reformed Stripping Path)",
anchor = "Mystery of the Stygian Veil",
logo = "StygianVe.png",
tendency = {"transformation", "rebirth", "death", "threshold"}
},
{
key = "SYL",
name = "Sylvan Fellowship",
divine = "Faun",
city = "Sylvapolis",
page = "Faun (Reformed Stripping Path)",
anchor = "Sylvan Fellowship",
logo = "Sylvan.png",
tendency = {"nature", "fertility", "wilds", "balance"}
},
{
key = "MIR",
name = "Mystery of Red Mirth",
divine = "Silenus",
city = "Erythros",
page = "Silenus (Reformed Stripping Path)",
anchor = "Mystery of Red Mirth",
logo = "MirthFlag.png",
tendency = {"merriment", "festival", "wine", "release"}
},
{
key = "CAT",
name = "Rex Catonis",
divine = "Cato",
city = "Catonis Atrium",
page = "Cato (Reformed Stripping Path)",
anchor = "Rex Catonis",
logo = "Catonis.png",
tendency = {"discipline", "order", "law", "judgment"}
},
{
key = "SDE",
name = "Sanctum Delphica",
divine = "Sisera",
city = "Delphica",
page = "Sisera (Reformed Stripping Path)",
anchor = "Sanctum Delphica",
logo = "SanctumDel.png",
tendency = {"reflection", "introspection", "records", "truth"}
},
{
key = "AMI",
name = "Ordo Amicitia",
divine = "Erasmus",
city = "Koinonía",
page = "Erasmus (Reformed Stripping Path)",
anchor = "Ordo Amicitia",
logo = "OrdoFl.png",
tendency = {"charity", "wisdom", "relief", "mercy"}
},
{
key = "ILL",
name = "Temple Illuminata",
divine = "Nikolaj",
city = "Aureum",
page = "Nikolaj (Reformed Stripping Path)",
anchor = "Temple Illuminata",
logo = "AureumFlag.png",
tendency = {"epiphany", "mystery", "cosmic", "insight"}
}
}
local cultByName = {}
local cultByKey = {}
local cultAliases = {}
for _, c in ipairs(cults) do
cultByName[c.name] = c
cultByKey[c.key] = c
cultAliases[lower(c.name)] = c.name
cultAliases[lower(c.key)] = c.name
cultAliases[lower(c.divine)] = c.name
end
-- Common shorthand aliases.
cultAliases["aurora"] = "Order of Aurora Mystica"
cultAliases["aurora mystica"] = "Order of Aurora Mystica"
cultAliases["harmony"] = "Harmony Sanctum"
cultAliases["ignis"] = "Ignis Aeternum"
cultAliases["golden shadows"] = "Guild of Golden Shadows"
cultAliases["azure"] = "Azure Sentinel Sect"
cultAliases["reverie"] = "Reverie Nebulous"
cultAliases["eon"] = "Eon Fellowship"
cultAliases["umbral"] = "Order of the Umbral Oracle"
cultAliases["verdant"] = "Mystery of the Verdant Embrace"
cultAliases["illuminara"] = "Conclave Illuminara"
cultAliases["vitalis"] = "Sanctum Vitalis"
cultAliases["alabaster"] = "Temple Alabaster"
cultAliases["ironclad"] = "Court of the Ironclad"
cultAliases["concordia"] = "Accord Concordia"
cultAliases["stygian veil"] = "Mystery of the Stygian Veil"
cultAliases["sylvan"] = "Sylvan Fellowship"
cultAliases["red mirth"] = "Mystery of Red Mirth"
cultAliases["catonis"] = "Rex Catonis"
cultAliases["delphica"] = "Sanctum Delphica"
cultAliases["amicitia"] = "Ordo Amicitia"
cultAliases["illuminata"] = "Temple Illuminata"
local function normalizeCultName(name)
name = trim(name)
if cultByName[name] then
return name
end
return cultAliases[lower(name)] or name
end
local function cultLink(name)
name = normalizeCultName(name)
local c = cultByName[name]
if not c then
return "[[" .. name .. "]]"
end
return string.format("[[%s#%s|%s]]", c.page, c.anchor, c.name)
end
local function cultMark(name)
name = normalizeCultName(name)
local c = cultByName[name]
if not c then
return cultLink(name)
end
local logo = ""
if c.logo and c.logo ~= "" then
logo = string.format("[[File:%s|18px]] ", c.logo)
end
return logo .. cultLink(name)
end
local function cityLink(city)
city = trim(city)
return string.format("[[List of cities in Bassaridia Vaeringheim#%s|%s]]", city, city)
end
local function eventCityLink(event)
if not event then
return "—"
end
local cultName = normalizeCultName(event.a)
local cult = cultByName[cultName]
if not cult or not cult.city or cult.city == "" then
return "—"
end
return cityLink(cult.city)
end
--------------------------------------------------------------------------------
-- 4. BASELINE RELATIONS
--
-- Scale:
-- +75 covenantal alliance
-- +55 strong alliance
-- +35 standing alliance
-- +15 mild affinity
-- 0 neutral
-- -15 mild tension
-- -35 standing rivalry
-- -55 hostile rivalry
-- -75 sectarian enmity
--------------------------------------------------------------------------------
local baselineRows = {
["Ordo Solis Invicti"] = {
allies = {"Rex Catonis", "Harmony Sanctum", "Order of Aurora Mystica"},
enemies = {},
note = "solar order, measure, and public adjudication"
},
["Order of Aurora Mystica"] = {
allies = {"Eon Fellowship", "Conclave Illuminara", "Harmony Sanctum", "Ordo Solis Invicti"},
enemies = {"Celestial Harmony Sect", "Ignis Aeternum"},
note = "dawn rites, renewal, and anti-destruction cooperation"
},
["Harmony Sanctum"] = {
allies = {"Order of Aurora Mystica", "Mystery of the Verdant Embrace", "Guild of Golden Shadows", "Conclave Illuminara", "Accord Concordia", "Ordo Solis Invicti"},
enemies = {"Ignis Aeternum", "Azure Sentinel Sect", "Court of the Ironclad", "Sylvan Fellowship"},
note = "balance-oriented center of the faith"
},
["Ignis Aeternum"] = {
allies = {"Azure Sentinel Sect", "Sylvan Fellowship"},
enemies = {"Eon Fellowship", "Harmony Sanctum", "Reverie Nebulous", "Mystery of the Verdant Embrace", "Order of Aurora Mystica", "Court of the Ironclad", "Guild of Golden Shadows"},
note = "purifying flame and destructive renewal"
},
["Celestial Harmony Sect"] = {
allies = {"Order of the Umbral Oracle", "Accord Concordia", "Sanctum Delphica"},
enemies = {"Order of Aurora Mystica", "Conclave Illuminara", "Rex Catonis"},
note = "duality, change, and mediating ambiguity"
},
["Guild of Golden Shadows"] = {
allies = {"Harmony Sanctum", "Reverie Nebulous", "Court of the Ironclad", "Sanctum Delphica"},
enemies = {"Ignis Aeternum", "Azure Sentinel Sect"},
note = "trade, intelligence, brokerage, and covert influence"
},
["Azure Sentinel Sect"] = {
allies = {"Ignis Aeternum", "Reverie Nebulous"},
enemies = {"Eon Fellowship", "Harmony Sanctum", "Guild of Golden Shadows"},
note = "sea, war, depth, and temple defense"
},
["Reverie Nebulous"] = {
allies = {"Guild of Golden Shadows", "Azure Sentinel Sect", "Sanctum Delphica"},
enemies = {"Ignis Aeternum", "Eon Fellowship", "Court of the Ironclad"},
note = "dreams, imagination, omen-craft, and social volatility"
},
["Eon Fellowship"] = {
allies = {"Order of Aurora Mystica", "Court of the Ironclad", "Temple Illuminata"},
enemies = {"Ignis Aeternum", "Azure Sentinel Sect", "Sylvan Fellowship", "Reverie Nebulous"},
note = "winter, endurance, death, and cyclical renewal"
},
["Order of the Umbral Oracle"] = {
allies = {"Harmony Sanctum", "Ordo Amicitia", "Celestial Harmony Sect", "Sanctum Delphica"},
enemies = {},
note = "night, hidden truth, introspection, and oracle-work"
},
["Mystery of the Verdant Embrace"] = {
allies = {"Harmony Sanctum", "Ordo Amicitia", "Sanctum Vitalis", "Sylvan Fellowship"},
enemies = {"Ignis Aeternum", "Court of the Ironclad"},
note = "harvest, fertility, cultivated nature, and anti-destruction practice"
},
["Conclave Illuminara"] = {
allies = {"Order of Aurora Mystica", "Harmony Sanctum", "Ordo Solis Invicti"},
enemies = {"Celestial Harmony Sect"},
note = "Tarsican illumination, eclipse interpretation, and wisdom rites"
},
["Sanctum Vitalis"] = {
allies = {"Temple Alabaster", "Ordo Amicitia", "Mystery of the Verdant Embrace", "Harmony Sanctum"},
enemies = {},
note = "health, medicine, relief, and recovery"
},
["Temple Alabaster"] = {
allies = {"Court of the Ironclad", "Ordo Amicitia", "Sanctum Vitalis", "Rex Catonis"},
enemies = {},
note = "purity, serenity, neutrality, and protected precincts"
},
["Court of the Ironclad"] = {
allies = {"Eon Fellowship", "Guild of Golden Shadows", "Temple Alabaster", "Rex Catonis"},
enemies = {"Ignis Aeternum", "Harmony Sanctum", "Reverie Nebulous", "Mystery of the Verdant Embrace"},
note = "strength, tenacity, armed discipline, and crisis response"
},
["Accord Concordia"] = {
allies = {"Sanctum Delphica", "Harmony Sanctum", "Celestial Harmony Sect", "Ordo Amicitia"},
enemies = {},
note = "unity, diplomacy, and inter-cult mediation"
},
["Mystery of the Stygian Veil"] = {
allies = {"Temple Illuminata", "Order of the Umbral Oracle", "Sanctum Vitalis"},
enemies = {"Ordo Amicitia"},
note = "transformation, rebirth, death, and threshold rites"
},
["Sylvan Fellowship"] = {
allies = {"Ignis Aeternum", "Mystery of the Verdant Embrace"},
enemies = {"Eon Fellowship", "Harmony Sanctum"},
note = "wild fertility, natural balance, and resistance to over-ordering"
},
["Mystery of Red Mirth"] = {
allies = {"Rex Catonis", "Accord Concordia", "Ordo Amicitia"},
enemies = {},
note = "celebration, release, and ritual merriment"
},
["Rex Catonis"] = {
allies = {"Mystery of Red Mirth", "Court of the Ironclad", "Temple Alabaster", "Ordo Solis Invicti"},
enemies = {"Celestial Harmony Sect"},
note = "discipline, order, law, and moral clarity"
},
["Sanctum Delphica"] = {
allies = {"Accord Concordia", "Reverie Nebulous", "Order of the Umbral Oracle", "Guild of Golden Shadows", "Celestial Harmony Sect"},
enemies = {},
note = "reflection, records, introspection, and arbitration"
},
["Ordo Amicitia"] = {
allies = {"Order of the Umbral Oracle", "Mystery of the Verdant Embrace", "Temple Alabaster", "Sanctum Vitalis", "Accord Concordia", "Mystery of Red Mirth"},
enemies = {"Mystery of the Stygian Veil"},
note = "charity, wisdom, aid distribution, and mercy"
},
["Temple Illuminata"] = {
allies = {"Mystery of the Stygian Veil", "Eon Fellowship", "Conclave Illuminara"},
enemies = {},
note = "epiphanies, cosmic insight, and mystery"
}
}
local mythicAffinities = {
{"Order of Aurora Mystica", "Eon Fellowship", 15, "Eos–Glinos mythic affinity"},
{"Order of Aurora Mystica", "Conclave Illuminara", 15, "Eos–Tarsica mythic affinity"},
{"Harmony Sanctum", "Mystery of the Verdant Embrace", 15, "Micras–Opsithe mythic affinity"},
{"Guild of Golden Shadows", "Reverie Nebulous", 15, "Chrysos–Nephele mythic affinity"},
{"Guild of Golden Shadows", "Accord Concordia", 8, "Chrysos–Lukedu lineage affinity"},
{"Azure Sentinel Sect", "Ignis Aeternum", 15, "Thalassa–Pyros mythic affinity"},
{"Order of the Umbral Oracle", "Ordo Amicitia", 12, "Noctis–Erasmus affinity"},
{"Accord Concordia", "Sanctum Delphica", 15, "Lukedu–Sisera mythic affinity"},
{"Mystery of Red Mirth", "Rex Catonis", 15, "Silenus–Cato mythic affinity"},
{"Mystery of the Stygian Veil", "Temple Illuminata", 15, "Styx–Nikolaj mythic affinity"},
{"Court of the Ironclad", "Temple Alabaster", 12, "Fenrir–Ivory mythic affinity"},
{"Sanctum Vitalis", "Mystery of the Verdant Embrace", 10, "Plateau–Opsithe lineage affinity"},
{"Sylvan Fellowship", "Mystery of the Verdant Embrace", 10, "Faun–Opsithe natural affinity"},
{"Ignis Aeternum", "Harmony Sanctum", -15, "Pyros–Micras mythic tension"},
{"Ignis Aeternum", "Mystery of the Verdant Embrace", -15, "Pyros–Opsithe mythic tension"},
{"Ignis Aeternum", "Reverie Nebulous", -12, "Pyros–Nephele mythic tension"},
{"Azure Sentinel Sect", "Harmony Sanctum", -10, "Thalassa–Micras mythic tension"},
{"Order of Aurora Mystica", "Celestial Harmony Sect", -10, "Eos–Indigo/Momiji mythic tension"},
{"Conclave Illuminara", "Celestial Harmony Sect", -10, "Tarsica–Indigo/Momiji mythic tension"},
{"Eon Fellowship", "Azure Sentinel Sect", -10, "Glinos–Thalassa mythic tension"},
{"Eon Fellowship", "Sylvan Fellowship", -8, "winter–wild fertility tension"},
{"Mystery of the Stygian Veil", "Ordo Amicitia", -8, "Styx–Nyssa/Erasmus secrecy tension"}
}
local function buildBaseScores()
local scores = {}
local notes = {}
local function add(a, b, delta, note)
a, b = normalizeCultName(a), normalizeCultName(b)
if a == "" or b == "" or a == b then
return
end
local k = sortedPair(a, b)
scores[k] = clamp((scores[k] or 0) + delta, -95, 95)
notes[k] = notes[k] or {}
table.insert(notes[k], note)
end
for name, row in pairs(baselineRows) do
for _, ally in ipairs(row.allies or {}) do
add(name, ally, 35, "standing alliance")
end
for _, enemy in ipairs(row.enemies or {}) do
add(name, enemy, -35, "standing rivalry")
end
end
for _, rel in ipairs(mythicAffinities) do
add(rel[1], rel[2], rel[3], rel[4])
end
return scores, notes
end
--------------------------------------------------------------------------------
-- 5. DAILY EVENT POOL
--------------------------------------------------------------------------------
local eventPool = {
{
w = 9,
type = "relief",
severity = "green",
delta = 8,
title = "Joint relief distribution",
text = "{A} and {B} jointly distribute food, medicine, or shelter stipends through Temple Bank channels.",
cults = {"Ordo Amicitia", "Sanctum Vitalis", "Temple Alabaster", "Harmony Sanctum", "Mystery of the Verdant Embrace"}
},
{
w = 8,
type = "mediation",
severity = "green",
delta = 7,
title = "Mediation circuit",
text = "{A} and {B} conduct a mediation circuit in a tense district, improving working confidence.",
cults = {"Harmony Sanctum", "Accord Concordia", "Order of Aurora Mystica", "Celestial Harmony Sect", "Sanctum Delphica"}
},
{
w = 7,
type = "audit",
severity = "green",
delta = 6,
title = "Temple Bank audit clears both sides",
text = "A Temple Bank audit clears {A} and {B} of misconduct allegations, reducing public suspicion.",
cults = {"Rex Catonis", "Sanctum Delphica", "Temple Alabaster", "Guild of Golden Shadows", "Ordo Solis Invicti"}
},
{
w = 6,
type = "festival",
severity = "green",
delta = 9,
title = "Shared festival stewardship",
text = "{A} and {B} jointly steward a festival route without disorder.",
cults = {"Mystery of Red Mirth", "Rex Catonis", "Order of Aurora Mystica", "Conclave Illuminara", "Accord Concordia", "Ordo Amicitia"}
},
{
w = 6,
type = "calendar",
severity = "green",
delta = 6,
title = "Calendar synchronization",
text = "{A} and {B} agree on calendar timing for public rites and registry deadlines.",
cults = {"Ordo Solis Invicti", "Conclave Illuminara", "Order of Aurora Mystica", "Rex Catonis", "Sanctum Delphica"}
},
{
w = 6,
type = "omen",
severity = "yellow",
delta = -7,
title = "Disputed omen",
text = "{A} and {B} issue conflicting interpretations of a public omen.",
cults = {"Conclave Illuminara", "Order of the Umbral Oracle", "Temple Illuminata", "Reverie Nebulous", "Celestial Harmony Sect"}
},
{
w = 7,
type = "doctrine",
severity = "yellow",
delta = -8,
title = "Doctrinal protest",
text = "{A} denounces the ritual language used by {B}.",
cults = {"Ignis Aeternum", "Harmony Sanctum", "Eon Fellowship", "Azure Sentinel Sect", "Mystery of the Verdant Embrace", "Order of Aurora Mystica"}
},
{
w = 6,
type = "trade",
severity = "yellow",
delta = -6,
title = "Trade-route accusation",
text = "{A} accuses {B} of shielding merchants, brokers, or shrine vendors from proper review.",
cults = {"Guild of Golden Shadows", "Azure Sentinel Sect", "Rex Catonis", "Temple Alabaster", "Sanctum Delphica", "Ordo Solis Invicti"}
},
{
w = 5,
type = "security",
severity = "yellow",
delta = -7,
title = "Shrine security dispute",
text = "{A} objects to the security posture adopted by {B} near a shrine precinct.",
cults = {"Court of the Ironclad", "Azure Sentinel Sect", "Rex Catonis", "Harmony Sanctum", "Order of Aurora Mystica", "Temple Alabaster"}
},
{
w = 5,
type = "dream",
severity = "yellow",
delta = -6,
title = "Dream-prophecy disagreement",
text = "{A} and {B} dispute whether a recurring dream report should be treated as prophecy, warning, or rumor.",
cults = {"Reverie Nebulous", "Order of the Umbral Oracle", "Sanctum Delphica", "Conclave Illuminara", "Temple Illuminata"}
},
{
w = 4,
type = "panic",
severity = "red",
delta = -12,
title = "Rumor panic",
text = "Panic narratives spread between communities aligned with {A} and {B}.",
cults = {"Reverie Nebulous", "Order of the Umbral Oracle", "Temple Illuminata", "Sanctum Delphica", "Eon Fellowship"}
},
{
w = 4,
type = "violence",
severity = "red",
delta = -16,
title = "Sectarian violence",
text = "Street violence attributed to extremists worsens relations between {A} and {B}.",
cults = {"Ignis Aeternum", "Court of the Ironclad", "Azure Sentinel Sect", "Harmony Sanctum", "Reverie Nebulous", "Mystery of the Verdant Embrace"}
},
{
w = 5,
type = "mourning",
severity = "green",
delta = 7,
title = "Shared mourning rites",
text = "{A} and {B} cooperate during mourning rites after a local death, disaster, or battlefield recovery.",
cults = {"Mystery of the Stygian Veil", "Ordo Amicitia", "Order of the Umbral Oracle", "Sanctum Vitalis", "Temple Alabaster"}
},
{
w = 5,
type = "ecology",
severity = "green",
delta = 6,
title = "Ecological repair compact",
text = "{A} and {B} sign a temporary compact for ecological repair and pilgrimage safety.",
cults = {"Mystery of the Verdant Embrace", "Sylvan Fellowship", "Harmony Sanctum", "Sanctum Vitalis"}
},
{
w = 4,
type = "purification",
severity = "red",
delta = -14,
title = "Purification controversy",
text = "{A} is blamed for encouraging dangerous purification language against {B}.",
cults = {"Ignis Aeternum", "Temple Alabaster", "Harmony Sanctum", "Mystery of the Verdant Embrace", "Order of Aurora Mystica"}
},
{
w = 5,
type = "registry",
severity = "green",
delta = 5,
title = "Registry reconciliation",
text = "{A} and {B} reconcile conflicting cult ledgers through formal registry review.",
cults = {"Rex Catonis", "Sanctum Delphica", "Guild of Golden Shadows", "Ordo Amicitia", "Accord Concordia", "Ordo Solis Invicti"}
},
{
w = 5,
type = "port",
severity = "green",
delta = 6,
title = "General Port membership drive",
text = "{A} and {B} support a General Port membership drive tied to shrine investment and community stabilization.",
cults = {"Guild of Golden Shadows", "Ordo Amicitia", "Harmony Sanctum", "Accord Concordia", "Sanctum Delphica"}
},
{
w = 4,
type = "privateer",
severity = "yellow",
delta = -7,
title = "Privateer blessing dispute",
text = "{A} and {B} disagree over whether a privateer commission should receive ritual blessing.",
cults = {"Azure Sentinel Sect", "Guild of Golden Shadows", "Rex Catonis", "Court of the Ironclad", "Order of the Umbral Oracle"}
},
{
w = 4,
type = "missionary",
severity = "green",
delta = 6,
title = "Missionary escort compact",
text = "{A} and {B} agree to escort missionaries through a contested road or canal route.",
cults = {"Ordo Amicitia", "Court of the Ironclad", "Temple Alabaster", "Harmony Sanctum", "Azure Sentinel Sect"}
}
}
local festivalPressures = {
Atosien = {
positive = {"Ordo Solis Invicti", "Conclave Illuminara", "Rex Catonis"},
negative = {}
},
Eosena = {
positive = {"Order of Aurora Mystica", "Conclave Illuminara", "Accord Concordia"},
negative = {"Celestial Harmony Sect"}
},
Micrasha = {
positive = {"Harmony Sanctum", "Mystery of the Verdant Embrace", "Accord Concordia"},
negative = {"Ignis Aeternum"}
},
Pyreska = {
positive = {"Ignis Aeternum", "Azure Sentinel Sect", "Sylvan Fellowship"},
negative = {"Harmony Sanctum", "Mystery of the Verdant Embrace"}
},
Indomin = {
positive = {"Celestial Harmony Sect", "Sanctum Delphica", "Accord Concordia"},
negative = {"Order of Aurora Mystica", "Conclave Illuminara", "Rex Catonis"}
},
Chrysen = {
positive = {"Guild of Golden Shadows", "Reverie Nebulous", "Sanctum Delphica"},
negative = {"Azure Sentinel Sect"}
},
Thalassian = {
positive = {"Azure Sentinel Sect", "Ignis Aeternum", "Reverie Nebulous"},
negative = {"Eon Fellowship", "Harmony Sanctum"}
},
Nephelia = {
positive = {"Reverie Nebulous", "Guild of Golden Shadows", "Sanctum Delphica"},
negative = {"Ignis Aeternum", "Court of the Ironclad"}
},
Glinaeus = {
positive = {"Eon Fellowship", "Court of the Ironclad", "Order of Aurora Mystica"},
negative = {"Ignis Aeternum", "Azure Sentinel Sect"}
},
Noctien = {
positive = {"Order of the Umbral Oracle", "Ordo Amicitia", "Sanctum Delphica"},
negative = {}
},
Opsithia = {
positive = {"Mystery of the Verdant Embrace", "Sanctum Vitalis", "Sylvan Fellowship"},
negative = {"Ignis Aeternum"}
},
Stygian = {
positive = {"Mystery of the Stygian Veil", "Temple Illuminata", "Order of the Umbral Oracle"},
negative = {"Ordo Amicitia"}
},
Faunian = {
positive = {"Sylvan Fellowship", "Mystery of the Verdant Embrace", "Sanctum Vitalis"},
negative = {"Eon Fellowship"}
},
Silenian = {
positive = {"Mystery of Red Mirth", "Rex Catonis", "Accord Concordia"},
negative = {}
},
Catosien = {
positive = {"Rex Catonis", "Ordo Solis Invicti", "Temple Alabaster"},
negative = {"Celestial Harmony Sect"}
}
}
local function pickTwoDistinct(nextInt, list)
if not list or #list < 2 then
return nil, nil
end
local a = pick(nextInt, list)
local b = pick(nextInt, list)
local tries = 0
while a == b and tries < 20 do
b = pick(nextInt, list)
tries = tries + 1
end
if a == b then
return list[1], list[2]
end
return a, b
end
local function makeEventText(template, a, b)
local s = tostring(template or "")
s = s:gsub("{A}", cultLink(a))
s = s:gsub("{B}", cultLink(b))
return s
end
local function generateDailyEvents(dateInfo, args)
args = args or {}
local count = tonumber(args.events or args.eventcount or args.relationEvents) or 5
count = clamp(count, 1, 12)
local seed = hash31("CultRelationsDailyBVR:" .. tostring(dateInfo.year) .. ":" .. tostring(dateInfo.dayOfYear))
local nextInt = lcg(seed)
local events = {}
local used = {}
local tries = 0
while #events < count and tries < 100 do
tries = tries + 1
local e = pickWeighted(nextInt, eventPool)
local a, b = pickTwoDistinct(nextInt, e.cults)
if a and b then
a, b = normalizeCultName(a), normalizeCultName(b)
local key = sortedPair(a, b) .. "::" .. tostring(e.title)
if not used[key] then
used[key] = true
table.insert(events, {
title = e.title,
type = e.type,
severity = e.severity,
a = a,
b = b,
delta = tonumber(e.delta) or 0,
text = makeEventText(e.text, a, b)
})
end
end
end
-- Zodiac-season pressure. This makes the current zodiac period matter.
local fp = festivalPressures[dateInfo.zodiac]
if fp then
local pa, pb = pickTwoDistinct(nextInt, fp.positive)
if pa and pb then
table.insert(events, {
title = dateInfo.zodiac .. " ritual alignment",
type = "zodiac",
severity = "green",
a = pa,
b = pb,
delta = 5,
text = cultLink(pa) .. " and " .. cultLink(pb) .. " benefit from shared " .. dateInfo.zodiac .. " ritual emphasis."
})
end
if fp.negative and #fp.negative > 0 and fp.positive and #fp.positive > 0 then
local na = pick(nextInt, fp.negative)
local nb = pick(nextInt, fp.positive)
if na and nb and na ~= nb then
table.insert(events, {
title = dateInfo.zodiac .. " doctrinal strain",
type = "zodiac",
severity = "yellow",
a = na,
b = nb,
delta = -4,
text = cultLink(na) .. " experiences doctrinal strain with " .. cultLink(nb) .. " during the " .. dateInfo.zodiac .. " period."
})
end
end
end
-- Recurring calendar-cycle pressure.
if dateInfo.dayOfYear % 17 == 0 then
table.insert(events, {
title = "Bacchanalia review",
type = "governance",
severity = "green",
a = "Harmony Sanctum",
b = "Accord Concordia",
delta = 8,
text = cultLink("Harmony Sanctum") .. " and " .. cultLink("Accord Concordia") .. " improve relations during a formal review of mediation protocols."
})
end
if dateInfo.dayOfYear % 29 == 0 then
table.insert(events, {
title = "Fire-renewal dispute",
type = "doctrine",
severity = "yellow",
a = "Ignis Aeternum",
b = "Mystery of the Verdant Embrace",
delta = -10,
text = cultLink("Ignis Aeternum") .. " and " .. cultLink("Mystery of the Verdant Embrace") .. " clash over fire-renewal language near cultivated shrine land."
})
end
if dateInfo.dayOfYear % 41 == 0 then
table.insert(events, {
title = "Lunar interpretation compact",
type = "omen",
severity = "green",
a = "Conclave Illuminara",
b = "Order of Aurora Mystica",
delta = 9,
text = cultLink("Conclave Illuminara") .. " and " .. cultLink("Order of Aurora Mystica") .. " publish a shared lunar interpretation compact."
})
end
if dateInfo.dayOfYear % 53 == 0 then
table.insert(events, {
title = "Order-and-mirth procession",
type = "festival",
severity = "green",
a = "Rex Catonis",
b = "Mystery of Red Mirth",
delta = 8,
text = cultLink("Rex Catonis") .. " and " .. cultLink("Mystery of Red Mirth") .. " coordinate disciplined festival processions without arrests."
})
end
return events
end
--------------------------------------------------------------------------------
-- 6. DAILY RELATION CALCULATION
--------------------------------------------------------------------------------
local function addDelta(scores, notes, a, b, delta, note)
a, b = normalizeCultName(a), normalizeCultName(b)
if a == "" or b == "" or a == b then
return
end
local k = sortedPair(a, b)
scores[k] = clamp((scores[k] or 0) + (tonumber(delta) or 0), -100, 100)
notes[k] = notes[k] or {}
table.insert(notes[k], note)
end
local function applyEvent(scores, notes, e)
addDelta(scores, notes, e.a, e.b, e.delta, e.title)
-- Severe events harden each participant's pre-existing enemies.
if e.severity == "red" then
for _, actor in ipairs({e.a, e.b}) do
local row = baselineRows[actor]
for _, enemy in ipairs((row and row.enemies) or {}) do
addDelta(scores, notes, actor, enemy, -3, "crisis hardening")
end
end
end
-- Cooperative events improve spillover relations with standing allies.
if e.severity == "green" then
for _, actor in ipairs({e.a, e.b}) do
local row = baselineRows[actor]
for _, ally in ipairs((row and row.allies) or {}) do
addDelta(scores, notes, actor, ally, 2, "cooperation spillover")
end
end
end
-- Yellow events add mild distrust to factions already in rivalry.
if e.severity == "yellow" then
for _, actor in ipairs({e.a, e.b}) do
local row = baselineRows[actor]
for _, enemy in ipairs((row and row.enemies) or {}) do
addDelta(scores, notes, actor, enemy, -1, "suspicion spillover")
end
end
end
end
local function buildDailyScores(dateInfo, args)
local scores, notes = buildBaseScores()
local events = generateDailyEvents(dateInfo, args)
for _, e in ipairs(events) do
applyEvent(scores, notes, e)
end
return scores, notes, events
end
function p.getDailyRelations(args)
args = args or {}
local dateInfo = getDateInfo(args)
local scores, notes, events = buildDailyScores(dateInfo, args)
return {
date = dateInfo,
scores = scores,
notes = notes,
events = events,
cults = cults,
baseline = baselineRows
}
end
--------------------------------------------------------------------------------
-- 7. DISPLAY HELPERS
--------------------------------------------------------------------------------
local function relationLabel(score)
score = tonumber(score) or 0
if score >= 75 then return "Covenantal alliance" end
if score >= 55 then return "Strong alliance" end
if score >= 35 then return "Standing alliance" end
if score >= 15 then return "Working cooperation" end
if score >= 5 then return "Mild cooperation" end
if score > -5 then return "Neutral / watchful" end
if score > -15 then return "Minor tension" end
if score > -35 then return "Rivalry" end
if score > -55 then return "Hostile rivalry" end
if score > -75 then return "Sectarian hostility" end
return "Sectarian enmity"
end
local function scoreStyle(score)
score = tonumber(score) or 0
if score >= 55 then
return "background:#cfe8cf;"
elseif score >= 15 then
return "background:#e8f5e9;"
elseif score > -15 then
return "background:#f8f9fa;"
elseif score > -55 then
return "background:#fff3cd;"
else
return "background:#f8d7da;"
end
end
local function severityBadge(severity)
if severity == "red" then
return '<span style="background:#f8d7da;border:1px solid #c99;padding:1px 5px;border-radius:8px;">Red</span>'
elseif severity == "yellow" then
return '<span style="background:#fff3cd;border:1px solid #cc9;padding:1px 5px;border-radius:8px;">Yellow</span>'
end
return '<span style="background:#d4edda;border:1px solid #9c9;padding:1px 5px;border-radius:8px;">Green</span>'
end
local function signed(n)
n = tonumber(n) or 0
if n > 0 then
return "+" .. tostring(n)
end
return tostring(n)
end
local function getPairScore(data, a, b)
a, b = normalizeCultName(a), normalizeCultName(b)
return data.scores[sortedPair(a, b)] or 0
end
local function topPairs(data, limit, mode)
limit = tonumber(limit) or 25
mode = trim(mode or "important")
local rows = {}
for k, score in pairs(data.scores or {}) do
local a, b = splitPairKey(k)
if cultByName[a] and cultByName[b] then
local item = {
a = a,
b = b,
score = score,
abs = math.abs(score)
}
if mode == "positive" then
if score > 0 then
table.insert(rows, item)
end
elseif mode == "negative" then
if score < 0 then
table.insert(rows, item)
end
else
table.insert(rows, item)
end
end
end
table.sort(rows, function(x, y)
if mode == "positive" then
if x.score == y.score then return x.a .. x.b < y.a .. y.b end
return x.score > y.score
elseif mode == "negative" then
if x.score == y.score then return x.a .. x.b < y.a .. y.b end
return x.score < y.score
else
if x.abs == y.abs then return x.a .. x.b < y.a .. y.b end
return x.abs > y.abs
end
end)
local out = {}
for i = 1, math.min(limit, #rows) do
table.insert(out, rows[i])
end
return out
end
local function noteText(data, a, b)
local k = sortedPair(normalizeCultName(a), normalizeCultName(b))
local list = data.notes[k] or {}
if #list == 0 then
return "neutral baseline"
end
local seen = {}
local out = {}
for _, note in ipairs(list) do
if not seen[note] then
seen[note] = true
table.insert(out, note)
end
end
return table.concat(out, "; ")
end
--------------------------------------------------------------------------------
-- 8. PUBLIC DISPLAY FUNCTIONS
--------------------------------------------------------------------------------
function p.eventLog(frame)
local args = frame and frame.args or {}
local data = p.getDailyRelations(args)
local out = {}
table.insert(out, string.format("'''Daily cult-relations event log — %s'''", data.date.longText))
table.insert(out, '{| class="wikitable sortable" style="width:100%; text-align:left;"')
table.insert(out, '! Event !! Severity !! Location !! Cults !! Effect !! Notes')
for _, e in ipairs(data.events or {}) do
table.insert(out, string.format(
"%s at %s: %s ↔ %s (%s)",
e.title,
eventCityLink(e),
e.a,
e.b,
signed(e.delta)
))
end
table.insert(out, "|}")
return table.concat(out, "\n")
end
function p.makePair(frame)
local args = frame and frame.args or {}
local a = normalizeCultName(args.a or args[1])
local b = normalizeCultName(args.b or args[2])
if a == "" or b == "" then
return "Specify two cults with <code>|a=</code> and <code>|b=</code>."
end
local data = p.getDailyRelations(args)
local score = getPairScore(data, a, b)
local out = {}
table.insert(out, '{| class="wikitable" style="width:100%; text-align:left;"')
table.insert(out, string.format("|+ Daily cult relation — %s", data.date.longText))
table.insert(out, "! Cult A !! Cult B !! Score !! Status !! Basis")
table.insert(out, "|-")
table.insert(out, string.format(
"| %s || %s || style='%s text-align:center; font-weight:bold;' | %d || %s || %s",
cultMark(a),
cultMark(b),
scoreStyle(score),
score,
relationLabel(score),
noteText(data, a, b)
))
table.insert(out, "|}")
return table.concat(out, "\n")
end
function p.makeTable(frame)
local args = frame and frame.args or {}
local data = p.getDailyRelations(args)
local limit = tonumber(args.limit or args.relationsLimit) or 25
local mode = trim(args.mode or "important")
local showEvents = lower(args.showEvents or "yes") ~= "no"
local compact = lower(args.compact or "no") == "yes"
local rows = topPairs(data, limit, mode)
local out = {}
table.insert(out, string.format(
"<div style='border:1px solid #aaa;background:#f8f9fa;padding:8px;margin:6px 0;'>" ..
"'''Reformed Stripping Path daily cult-relations index'''<br/>" ..
"Date: '''%s'''<br/>" ..
"Current zodiac period: '''%s'''<br/>" ..
"Relations are recalculated from standing alliances, standing rivalries, mythic affinities, zodiac pressure, and embedded daily events. " ..
"Positive scores indicate cooperation; negative scores indicate rivalry or hostility." ..
"</div>",
data.date.longText,
data.date.zodiac
))
table.insert(out, '{| class="wikitable sortable" style="width:100%; text-align:left;"')
table.insert(out, "! Cult A !! Cult B !! Score !! Status")
if not compact then
table.insert(out, "!! Current basis")
end
for _, r in ipairs(rows) do
table.insert(out, "|-")
local row = string.format(
"| %s || %s || style='%s text-align:center; font-weight:bold;' | %d || %s",
cultMark(r.a),
cultMark(r.b),
scoreStyle(r.score),
r.score,
relationLabel(r.score)
)
if not compact then
row = row .. " || " .. noteText(data, r.a, r.b)
end
table.insert(out, row)
end
table.insert(out, "|}")
if showEvents then
table.insert(out, "\n" .. p.eventLog({args = args}))
end
return table.concat(out, "\n")
end
function p.makeMatrix(frame)
local args = frame and frame.args or {}
local data = p.getDailyRelations(args)
local out = {}
table.insert(out, string.format("'''Cult-relations matrix — %s'''", data.date.longText))
table.insert(out, '{| class="wikitable" style="font-size:85%; text-align:center;"')
local header = {"! Cult"}
for _, c in ipairs(cults) do
table.insert(header, "!" .. c.key)
end
table.insert(out, table.concat(header, "\n"))
for _, a in ipairs(cults) do
local row = {"|-\n! " .. cultMark(a.name)}
for _, b in ipairs(cults) do
if a.name == b.name then
table.insert(row, "| style='background:#eee;' | —")
else
local score = getPairScore(data, a.name, b.name)
table.insert(row, string.format("| style='%s' | %d", scoreStyle(score), score))
end
end
table.insert(out, table.concat(row, "\n"))
end
table.insert(out, "|}")
return table.concat(out, "\n")
end
function p.cultList(frame)
local args = frame and frame.args or {}
local out = {}
table.insert(out, '{| class="wikitable sortable" style="width:100%; text-align:left;"')
table.insert(out, "! Key !! Cult / Mystery !! Divine !! Holy city !! General tendency")
for _, c in ipairs(cults) do
table.insert(out, "|-")
table.insert(out, string.format(
"| %s || %s || [[%s]] || %s || %s",
c.key,
cultMark(c.name),
c.divine,
cityLink(c.city),
table.concat(c.tendency or {}, ", ")
))
end
table.insert(out, "|}")
return table.concat(out, "\n")
end
function p.legend(frame)
return table.concat({
'{| class="wikitable" style="width:60%;"',
'! Score range !! Meaning',
'|-',
'| 75 to 100 || Covenantal alliance',
'|-',
'| 55 to 74 || Strong alliance',
'|-',
'| 35 to 54 || Standing alliance',
'|-',
'| 15 to 34 || Working cooperation',
'|-',
'| 5 to 14 || Mild cooperation',
'|-',
'| -4 to 4 || Neutral / watchful',
'|-',
'| -14 to -5 || Minor tension',
'|-',
'| -34 to -15 || Rivalry',
'|-',
'| -54 to -35 || Hostile rivalry',
'|-',
'| -74 to -55 || Sectarian hostility',
'|-',
'| -100 to -75 || Sectarian enmity',
'|}'
}, "\n")
end
--------------------------------------------------------------------------------
-- 9. RAW / DEBUG FUNCTIONS
--------------------------------------------------------------------------------
function p.rawDate(frame)
local args = frame and frame.args or {}
local d = getDateInfo(args)
return string.format(
"dayOfYear=%s; month=%s; day=%s; zodiac=%s; year=%s; raw=%s",
tostring(d.dayOfYear),
tostring(d.month),
tostring(d.dom),
tostring(d.zodiac),
tostring(d.year),
tostring(d.raw)
)
end
function p.rawScore(frame)
local args = frame and frame.args or {}
local a = normalizeCultName(args.a or args[1])
local b = normalizeCultName(args.b or args[2])
if a == "" or b == "" then
return "0"
end
local data = p.getDailyRelations(args)
return tostring(getPairScore(data, a, b))
end
function p.rawLabel(frame)
local args = frame and frame.args or {}
local a = normalizeCultName(args.a or args[1])
local b = normalizeCultName(args.b or args[2])
if a == "" or b == "" then
return "Neutral / watchful"
end
local data = p.getDailyRelations(args)
return relationLabel(getPairScore(data, a, b))
end
function p.rawEvents(frame)
local args = frame and frame.args or {}
local data = p.getDailyRelations(args)
local out = {}
for _, e in ipairs(data.events or {}) do
table.insert(out, string.format(
"%s: %s ↔ %s (%s)",
e.title,
e.a,
e.b,
signed(e.delta)
))
end
return table.concat(out, "; ")
end
return p