Module:HatchMinistryDailyOps: Difference between revisions

From MicrasWiki
Jump to navigationJump to search
NewZimiaGov (talk | contribs)
No edit summary
NewZimiaGov (talk | contribs)
No edit summary
Line 5: Line 5:
-- Updates embedded:
-- Updates embedded:
--  • Added coastal cities: Sjøsborg, Sårensby, Storesund, Slevik
--  • Added coastal cities: Sjøsborg, Sårensby, Storesund, Slevik
--  • City flag icons now use a SAFE fallback to the Imperial Trade Union flag when missing
--  • City flag icons use SAFE fallback to the Imperial Trade Union flag when missing
--    (patterned after Module:WarLeagueDivisionOpsBVR logic)
--    (with caching to avoid “too many expensive function calls”)
--  • Integrated Ops Alert + Ops Impact from Module:BassaridiaForecast (if available):
--      - Adds Ops Alert column (badge + condition) to the table
--      - Orange/Red weather can bump severity and append a concise weather posture note
--      - Sea Closed / Air No-Go forces “weather delay / hold windows” language
--  • Everything else retained:
--  • Everything else retained:
--      - City flags in Home/Station columns
--      - City flags in Home/Station columns
--      - 20 tasks per pool (General / North / South / Central)
--      - 20 tasks per pool (General / North / South / Central) + privateering pool
--      - Row color-coding (Green/Yellow/Red)
--      - Row color-coding (Green/Yellow/Red)
--      - 10% daily chance per privateer of opportunistic privateering against “non-aligned” vessels
--      - 10% daily chance per privateer of opportunistic privateering against “non-aligned” vessels
--       (privateering pool = 20 entries)
--
-- Usage:
--  {{#invoke:HatchMinistryDailyOps|dailyReport}}
--  {{#invoke:HatchMinistryDailyOps|dailyReport|year=52|day=118}}
--  {{#invoke:HatchMinistryDailyOps|dailyReport|legend=no}}


local p = {}
local p = {}


local cal = require('Module:BassaridianCalendar')
local cal = require('Module:BassaridianCalendar')
local DAYS_IN_YEAR = 183
local DAYS_IN_YEAR = 183


Line 31: Line 38:


--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
-- FLAG FALLBACK (use ITU flag when missing)
-- (WX) OPTIONAL: integrate deterministic Ops Alert from Module:BassaridiaForecast
--------------------------------------------------------------------------------
local wxOk, wx = pcall(require, "Module:BassaridiaForecast")
if not wxOk then wx = nil end
 
local function wxColor(level)
  level = tonumber(level) or 0
  if level == 3 then return "#f8d7da" end
  if level == 2 then return "#ffe5cc" end
  if level == 1 then return "#fff3cd" end
  return "#d4edda"
end
 
local function wxBadge(level, label, cond)
  local bg = wxColor(level)
  label = tostring(label or "Green")
  cond  = tostring(cond or "")
  return string.format(
    "<span style='background:%s;padding:2px 6px;border:1px solid #ccc;border-radius:10px;white-space:nowrap;'>%s</span>%s",
    bg, label, (cond ~= "" and ("<br/><span style=\"font-size:90%;\">" .. cond .. "</span>") or "")
  )
end
 
local function wxImpactText(imp)
  imp = imp or {}
  return string.format("Gnd: %s; Sea: %s; Air: %s", imp.ground or "OK", imp.sea or "OK", imp.air or "OK")
end
 
local function getWx(cityName, year, dayOfYear)
  if not wx or type(wx.getOpsData) ~= "function" then return nil end
  return wx.getOpsData(cityName, year, dayOfYear)
end
 
local function applyWeatherOverlay(text, sev, station, year, dayOfYear)
  if not wx or type(wx.getOpsData) ~= "function" then return text, sev end
 
  local op = getWx(station, year, dayOfYear)
  if not op or not op.alert then return text, sev end
 
  local lvl = tonumber(op.alert.level) or 0
  if lvl < 2 then return text, sev end -- only Orange/Red modify ops
 
  local hazard  = tostring(op.alert.hazard or "Weather disruption")
  local impacts = op.impacts or {}
  local cond    = tostring(op.condition or "")
 
  -- severity bump
  if lvl == 2 and sev == "green" then sev = "yellow" end
  if lvl >= 3 then sev = "red" end
 
  local overlay = " Weather posture: " .. (op.alert.label or "Orange") ..
    " – " .. hazard .. " (" .. wxImpactText(impacts) .. ")."
 
  if impacts.sea == "Closed" or impacts.air == "No-Go" then
    overlay = overlay .. " Corridor hold-and-release windows enforced; noncritical boardings deferred; SAR/debris-clearance standby prioritized."
    sev = "red"
  elseif impacts.sea == "Restricted" then
    overlay = overlay .. " Escort buffers widened; boarding windows shifted to safer intervals; harbor entry metered."
    if sev == "green" then sev = "yellow" end
  elseif impacts.ground == "Hazard" or impacts.ground == "Slow" then
    overlay = overlay .. " Shore leave and pier movement controlled; queues stabilized at inspection points."
    if sev == "green" then sev = "yellow" end
  else
    overlay = overlay .. " Heightened messaging to prevent panic narratives and berth congestion."
  end
 
  -- keep overlays short for readability
  return text .. overlay, sev
end
 
--------------------------------------------------------------------------------
-- FLAG FALLBACK (use ITU flag when missing) + CACHING (avoid expensive limits)
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
local DEFAULT_FLAG = "Imperial_Trade_Union_flag.png"
local DEFAULT_FLAG = "Imperial_Trade_Union_flag.png"
Line 44: Line 122:
end
end


local fileExistsCache = {}
local function fileExists(fileName)
local function fileExists(fileName)
   local t = mw.title.new("File:" .. tostring(fileName or ""))
  fileName = tostring(fileName or "")
   return t and t.exists or false
  if fileName == "" then return false end
  if fileExistsCache[fileName] ~= nil then return fileExistsCache[fileName] end
   local t = mw.title.new("File:" .. fileName)
   local ok = (t and t.exists) or false
  fileExistsCache[fileName] = ok
  return ok
end
end


Line 55: Line 139:
end
end


local cityFlagCache = {}
local function cityFlagIcon(cityName, sizePx)
local function cityFlagIcon(cityName, sizePx)
   local size = tonumber(sizePx) or 20
   local size = tonumber(sizePx) or 20
  local key = tostring(cityName or "") .. "|" .. tostring(size)
  if cityFlagCache[key] then return cityFlagCache[key] end
   local candidates = {}
   local candidates = {}
   if cityName == "Skýrophos" then
   if cityName == "Skýrophos" then
     table.insert(candidates, "SkyrophosFlag.png")
     candidates = { "SkyrophosFlag.png" }
  elseif cityName == "Slevik" then
    -- known oddity on-wiki
    candidates = { "SlevikFlag.png.png", string.format("%sFlag.png", normalizeForFlag(cityName)) }
   else
   else
     local norm = normalizeForFlag(cityName)
     local norm = normalizeForFlag(cityName)
     table.insert(candidates, string.format("%sFlag.png", norm))
     candidates = { string.format("%sFlag.png", norm) }
 
    -- Known oddity sometimes used on-wiki
    if cityName == "Slevik" then
      table.insert(candidates, "SlevikFlag.png.png")
    end
   end
   end


   for _, f in ipairs(candidates) do
   for _, f in ipairs(candidates) do
     if fileExists(f) then
     if fileExists(f) then
       return flagIcon(f, size)
       cityFlagCache[key] = flagIcon(f, size)
      return cityFlagCache[key]
     end
     end
   end
   end
   return flagIcon(DEFAULT_FLAG, size)
 
   cityFlagCache[key] = flagIcon(DEFAULT_FLAG, size)
  return cityFlagCache[key]
end
end


Line 274: Line 362:
   T("Storm-surge response patrol supporting stranded craft, hauling wreckage clear and securing debris fields.","red"),
   T("Storm-surge response patrol supporting stranded craft, hauling wreckage clear and securing debris fields.","red"),
   T("Border-proximity deterrence demonstration near contested approaches, emphasizing corridor sovereignty by presence.","yellow"),
   T("Border-proximity deterrence demonstration near contested approaches, emphasizing corridor sovereignty by presence.","yellow"),
   T("Winter-route courier interception, targeting clandestine messages and route-maps carried by sled-ship tenders.","yellow"),
   T("Dockyard compliance audit for unregistered repairs and hidden compartments used for contraband staging.","yellow"),
   T("Harbor freeze management patrol, coordinating icebreaking assistance and preventing dock congestion escalation.","red"),
   T("Winterized escort for mail and courier traffic, enforcing strict timing windows through fog-prone waters.","green"),
   T("Recon of abandoned fortlets and cliff ladders used as clandestine landing infrastructure during past campaigns.","yellow"),
   T("Interception of runner craft using fjord-shadow routes to evade inspection, with pursuit and boarding discipline.","yellow"),
   T("Counter-insurgent sweep for hidden fuel caches, rope ladders, and cliff-staged supply bundles along rugged coasts.","yellow"),
   T("Harbor-mouth search pattern for missing fisher crews after sudden squall; SAR handoff to local authorities.","red"),
   T("Escort for refugee or evacuation flotillas under strict sequencing, preventing infiltration by hostile agents.","red"),
   T("Cold-weather medical readiness drill; hypothermia triage training paired with evacuation rehearsals.","yellow"),
   T("Mine-scare verification patrol, conducting lane checks after rumor events to restore merchant confidence.","red"),
   T("Counter-smuggling sweep focused on high-value furs, ore samples, and forged tax seals.","yellow"),
   T("Long-range patrol to discourage unaffiliated privateer predation in the far approaches, enforcing registry legitimacy.","yellow"),
   T("Coastal watchtower liaison: beacon confirmation, signal discipline, and false-alarm suppression.","green"),
   T("Cooperation drill with local militia garrisons, practicing shore-to-ship handover for detainees and seized cargo.","green"),
   T("Night shoreline patrol to prevent sabotage against lock-gates and fuel depots during high-wind periods.","red"),
   T("Watch for cult-linked winter rites that trigger crowd movement to the waterfront, providing calm enforcement presence.","yellow"),
   T("Escort for War League inspectors transiting northern ports; identity verification and berth control enforced.","yellow"),
   T("Signal interception and direction-finding patrol to locate clandestine transmitters along ridge or lighthouse ruins.","yellow"),
   T("Evidence transfer run under snow/fog conditions; custody chain maintained despite degraded visibility.","yellow"),
   T("Post-incident audit run, rechecking logs and witness statements after a northern skirmish to prevent rumor spirals.","yellow")
   T("Deterrence cruise through rough seas to signal sovereignty; safe-distance demonstration only.","green")
}
}


local tasks_south = {
local tasks_south = {
   T("Short-notice escort requested by port authorities after a threat bulletin, maintaining corridor credibility by response speed.","red"),
   T("Haifan corridor compliance sweep: routing verification, escort-buffer reinforcement, and random boarding windows.","yellow"),
   T("Southern straits checkpoint enforcement with ro-ro ramp inspections and explosive-risk screening of container traffic.","red"),
   T("Harbor-entry metering and berth assignment enforcement during peak merchant throughput.","green"),
   T("Interdiction of fast skiffs and disguised cargo dhows, prioritizing weapons leakage and clandestine courier movement.","yellow"),
   T("Contraband focus patrol targeting shrine-counterfeit cargo and unregistered ritual items.","yellow"),
   T("Escort for high-value shipments transiting the southern corridor under strict queue and inspection timing.","yellow"),
   T("VBSS series against suspect manifests tied to minor routing variances, emphasizing correction over disruption.","yellow"),
   T("Harbor perimeter patrol to prevent diver sabotage and underwater placement of charges near pylons and ramps.","red"),
   T("Shoreline patrol to deter small-craft runner networks staging from beach coves and canal mouths.","yellow"),
   T("Container seal audit and weigh-station verification to catch false tare weights, decoy loads, and swapped manifests.","yellow"),
   T("Escort for high-value diplomatic traffic; counter-ambush and surveillance checks at chokepoints.","yellow"),
   T("Crowd-control augmentation during market surges, keeping inspection lanes clear and preventing quay-side crush incidents.","red"),
   T("Rapid response to port unrest and rumor-driven crowd surges, coordinating with civic stewards.","red"),
   T("Escort of fuel barges and repair tenders supporting corridor craft, with anti-ambush screen along shallow approaches.","yellow"),
   T("Quarantine enforcement support for incoming vessel under inspection; controlled disembarkation and isolation lanes.","red"),
   T("Contraband sweep for dream-lure substances moving through coastal markets, enforcing seizure doctrine.","yellow"),
   T("Night interception of unlit craft attempting to shadow lawful hulls into berth lanes.","yellow"),
   T("Anti-bribery integrity patrol, rotating boarding teams and recording interactions to deter corrupt gatekeeping.","yellow"),
   T("Inspection of fishery convoys for hidden compartments and false seals; evidence packaged for clerks.","yellow"),
   T("Recon of mangrove or reed margins used for low-visibility transfers, with shallow pursuit and night optics.","yellow"),
   T("Counterfeit voucher crackdown: registry stamp verification and vendor interviews.","yellow"),
   T("VBSS focus on ro-ro decks and vehicle bays, inspecting hidden compartments, false bulkheads, and modified ramps.","yellow"),
   T("Harbor-mouth overwatch; spotters track loitering craft and false distress signals.","green"),
   T("Disruption of staged distress calls used to pull patrols off-lane, verifying authenticity before committing assets.","yellow"),
   T("Canal-approach patrol to prevent sabotage against locks and piers.","red"),
   T("Escort of pilgrim or festival traffic through the southern corridor, enforcing timed windows and calm procedure.","yellow"),
   T("Search-and-rescue readiness patrol in heavy rain periods; debris clearance standby.","red"),
   T("Coastal deterrence posture near known runner routes, signaling denial without escalation to open combat.","yellow"),
   T("Lane assurance: buoy and beacon checks to ensure safe passage and lawful routing.","green"),
   T("Port-rail interface security support, screening containers that transfer directly from quay to inland corridors.","yellow"),
   T("Interdiction against small-craft logistics supporting insurgent networks.","yellow"),
   T("Undercover observation run in nearshore traffic, tracking suspected broker craft coordinating smuggling rendezvous.","yellow"),
   T("Seizure-and-transfer convoy for detained contraband under chain-of-custody doctrine.","yellow"),
   T("EOD stand-by and cordon enforcement after discovery of suspect packages at ramps or quayside warehouses.","red"),
   T("Escort buffer widening during heightened risk periods; predictable passage enforced.","yellow"),
   T("Inspection of refrigerated cargo for false panels and hidden stowaways, coordinating safe medical screening.","yellow"),
   T("Training cruise: boarding compliance, medical triage procedure, detainee handling standards.","green"),
   T("Post-seizure handling and auction transfer escort, moving confiscated goods to authorized holdings under paperwork discipline.","yellow")
   T("Dockside liaison with Temple and civic screening teams; rumor containment and compliance reinforcement.","yellow")
}
}


local tasks_central = {
local tasks_central = {
   T("Canal and harbor security rotation supporting administrative traffic and registry enforcement near central corridor hubs.","green"),
   T("Canal-city inner-basin patrol: lock-gate oversight, pier security, and approach-lane order enforcement.","green"),
   T("Inspection coordination with port clerks and Temple auditors to reduce queue-time without relaxing compliance standards.","yellow"),
   T("VBSS rotation focused on paperwork integrity and counterfeit seal detection at high-throughput quays.","yellow"),
   T("Ritual cargo verification support with enforcement of pre-clearance doctrine and holding protocols.","yellow"),
   T("Interdiction against canal runner craft attempting to bypass inspection windows using side channels.","yellow"),
   T("Canal-lock security sweep to deter sabotage of gates, chains, and control houses serving high-traffic waterways.","red"),
   T("Harbor traffic control: queue metering, berth discipline, and stand-off enforcement for noncompliant hulls.","green"),
   T("Escort for administrative barges and registry couriers moving between major quays and oversight offices.","green"),
   T("Anti-smuggling sweep targeting disguised ritual cargo and forged Temple consignment manifests.","yellow"),
   T("Dockside patrol targeting petty theft rings and counterfeit stamp sellers preying on merchant queues.","yellow"),
   T("Rapid response support to civic authorities during unrest near the docks; dispersal corridors opened.","red"),
   T("Controlled berth allocation support during peak arrivals, enforcing order of entry and preventing berth disputes.","green"),
   T("Search-and-rescue standby near canal choke points; debris fields secured and cleared.","red"),
   T("Liaison patrol with Temple Bank clerks for high-value transfers, verifying seals and escorting to secure vault quays.","yellow"),
   T("Liaison with Temple auditors and port clerks: compliance reinforcement and morale discipline checks.","yellow"),
   T("Canal bridge and tunnel perimeter check, preventing illicit transfers from underpasses into restricted wharf zones.","yellow"),
   T("Night interception detail targeting unlit barges and covert tender rendezvous.","yellow"),
   T("Rapid-response run to a canal incident, establishing cordons, clearing debris, and restoring navigation sequencing.","red"),
   T("Evidence convoy run to War League intake point; chain-of-custody doctrine enforced.","yellow"),
   T("Inspection of passenger ferries and canal shuttles for clandestine couriers, false identities, and concealed satchels.","yellow"),
   T("Inspection of cold-chain and fishery cargo for hidden compartments and counterfeit seals.","yellow"),
   T("Citywatch coordination day, aligning waterfront patrol routes with civic policing to prevent gaps and overlaps.","green"),
   T("Anti-piracy deterrence sweep against unaffiliated raiders operating outside registry rules.","yellow"),
   T("Harbor-mouth overwatch with spotters to identify loitering craft attempting to time entries between inspection rotations.","green"),
   T("Boarding-team readiness cycle emphasizing non-lethal deck control and restraint discipline.","green"),
   T("Detainee transfer and intake coordination to a designated holding site, emphasizing calm procedure and record integrity.","yellow"),
   T("Escort for Temple couriers through canal junctions; counter-ambush screening at bridges.","yellow"),
   T("Compliance education pass, issuing warnings and standard notices to repeat offenders before escalatory seizure.","green"),
   T("Counterfeit voucher and registry-stamp crackdown in dockside markets.","yellow"),
   T("Audit of registry anomalies flagged by clerks, conducting targeted re-boards of vessels with inconsistent logs.","yellow"),
   T("Harbor-mouth overwatch with spotters to track suspicious loitering near approach lanes.","green"),
   T("Escort for repair barges and dredging crews, maintaining safety perimeters while channel work proceeds.","yellow"),
   T("Quarantine cordon support for suspected outbreak vessel; isolation lanes enforced.","red"),
   T("Canal-side rumor containment presence after an incident bulletin, stabilizing crowd behavior through visible order.","yellow"),
   T("Canal infrastructure protection: lock-gates and waterworks sabotage prevention.","red"),
   T("Verification of shrine-licensed ceremonial shipments, ensuring listed rites match cargo declarations and timing windows.","yellow"),
   T("Training cruise certifying boarding compliance and med-triage procedure for new crews.","green"),
   T("Training day for boarding teams in narrow-watercraft maneuvering, canal interdiction, and non-lethal deck control.","green")
   T("Routine patrol and compliance enforcement across primary canal approaches.","green")
}
}


local tasks_privateering = {
local tasks_privateering = {
   T("Privateering sortie against non-aligned vessels: boarding and prize-seizure operations against runners operating outside corridor protection.","yellow"),
   T("Privateering sortie against non-aligned vessels: inspection and seizure of contraband goods concealed in false bilge compartments.","yellow"),
   T("Privateering sortie against non-aligned vessels: interdiction of suspected smuggling hulls with forcible boarding and impoundment.","yellow"),
   T("Privateering sortie against non-aligned vessels: boarding action against a suspected pirate-supply barque, confiscating arms and fuel.","red"),
  T("Privateering sortie against non-aligned vessels: raid on a transient anchorage used by unaffiliated crews to stage cargo transfers.","red"),
   T("Privateering sortie against non-aligned vessels: capture of a runner dhow attempting corridor bypass under black sail.","yellow"),
  T("Privateering sortie against non-aligned vessels: pursuit of an unregistered convoy with capture mandates and escorted prize return.","red"),
   T("Privateering sortie against non-aligned vessels: interdiction of a false-distress decoy ship used to lure lawful traffic.","yellow"),
   T("Privateering sortie against non-aligned vessels: coercive inspection sweep to compel compliance and extract forfeiture from repeat offenders.","yellow"),
   T("Privateering sortie against non-aligned vessels: seizure of illicit currency and forged vouchers intended for dockside laundering.","yellow"),
  T("Privateering sortie against non-aligned vessels: night boarding operations targeting unlit craft attempting to bypass inspection windows.","red"),
   T("Privateering sortie against non-aligned vessels: raid on a coastal cache pier, dismantling ladders, crates, and mooring points.","red"),
   T("Privateering sortie against non-aligned vessels: enforcement raid on a suspected broker craft coordinating illicit rendezvous offshore.","red"),
   T("Privateering sortie against non-aligned vessels: pursuit and boarding under cover of fog, enforcing forfeiture for evasion.","red"),
   T("Privateering sortie against non-aligned vessels: demonstration seizure to reassert corridor monopoly over passage and escort.","yellow"),
   T("Privateering sortie against non-aligned vessels: escort of a captured prize through the harbor mouth under guard.","yellow"),
   T("Privateering sortie against non-aligned vessels: counter-raider action against unaffiliated privateers preying on registered traffic.","red"),
  T("Privateering sortie against non-aligned vessels: prize adjudication escort, moving seized hull and cargo under guard to authorized holding.","yellow"),
  T("Privateering sortie against non-aligned vessels: dawn boarding sweep on drift traffic loitering outside the inspection window.","yellow"),
  T("Privateering sortie against non-aligned vessels: seizure of a decoy cargo barge used to mask runner transfers along the reed margin.","red"),
   T("Privateering sortie against non-aligned vessels: interception of a false-flagged hull and forced re-identification under corridor doctrine.","yellow"),
  T("Privateering sortie against non-aligned vessels: pursuit of a fleeing skiff network, with coordinated cutoffs at known inlets and river mouths.","red"),
   T("Privateering sortie against non-aligned vessels: coercive audit of a ‘merchant-escort’ outfit suspected of acting as a raider screen.","yellow"),
   T("Privateering sortie against non-aligned vessels: boarding and confiscation of unregistered ritual cargo transported under counterfeit shrine seals.","red"),
   T("Privateering sortie against non-aligned vessels: boarding and confiscation of unregistered ritual cargo transported under counterfeit shrine seals.","red"),
   T("Privateering sortie against non-aligned vessels: interdiction of a night-transfer rendezvous, dispersing tenders and capturing the broker craft.","red"),
   T("Privateering sortie against non-aligned vessels: interdiction of a night-transfer rendezvous, dispersing tenders and capturing broker craft.","red"),
   T("Privateering sortie against non-aligned vessels: targeted inspection of hulls linked to prior paper anomalies, enforcing forfeiture for repeat fraud.","yellow"),
   T("Privateering sortie against non-aligned vessels: targeted inspection of hulls linked to prior paper anomalies, enforcing forfeiture for repeat fraud.","yellow"),
   T("Privateering sortie against non-aligned vessels: escort of captured prize through the harbor mouth under guard to prevent retaliatory recovery.","yellow"),
   T("Privateering sortie against non-aligned vessels: punitive raid on suspected resupply pier and fuel caches.","red"),
   T("Privateering sortie against non-aligned vessels: punitive raid on a suspected resupply pier, dismantling ladders, caches, and mooring points.","red")
  T("Privateering sortie against non-aligned vessels: seizure of contraband medicines and counterfeit labels; evidence packaged for clerks.","yellow"),
  T("Privateering sortie against non-aligned vessels: dismantle an illegal buoy line guiding ships off-lane; confiscate charts and signal codes.","yellow"),
  T("Privateering sortie against non-aligned vessels: detain a pirate recruiter crew; transport under restraint doctrine.","red"),
  T("Privateering sortie against non-aligned vessels: sweep of shadowed inlets for hidden tenders; tow seized craft to inspection berth.","yellow"),
   T("Privateering sortie against non-aligned vessels: strike deterrence demonstration and warning shots at standoff distance.","yellow"),
  T("Privateering sortie against non-aligned vessels: capture of a smuggling skiff convoy and destruction of caches.","red"),
  T("Privateering sortie against non-aligned vessels: enforce forfeiture on repeat-violator hull; escort to custody quay.","yellow"),
  T("Privateering sortie against non-aligned vessels: raid on ritual contraband broker and seizure of ledgers.","red"),
}
}


Line 393: Line 481:
   local cl = clusterForCity(home)
   local cl = clusterForCity(home)
   local localList = clusters[cl] or clusters.Central
   local localList = clusters[cl] or clusters.Central
  local r = rand01(nextInt)


  local r = rand01(nextInt)
   if r < 0.65 then
   if r < 0.65 then
     return home
     return home
Line 462: Line 550:
     end
     end
   end
   end
  -- WX overlay (Orange/Red only): weather posture + severity bump
  text, sev = applyWeatherOverlay(text, sev, station, year, dayOfYear)


   return {
   return {
Line 503: Line 594:


   table.insert(out, '{| class="wikitable sortable"')
   table.insert(out, '{| class="wikitable sortable"')
   table.insert(out, '! Privateer Flag !! Captain !! Home City !! Current Station !! Rotation !! Today\'s Activity')
   table.insert(out, '! Privateer Flag !! Captain !! Home City !! Current Station !! Ops Alert !! Rotation !! Today\'s Activity')


   for _, cap in ipairs(privateers) do
   for _, cap in ipairs(privateers) do
Line 515: Line 606:
     local stationCell = cityFlagIcon(a.station, 20) .. " " .. cityLink(a.station)
     local stationCell = cityFlagIcon(a.station, 20) .. " " .. cityLink(a.station)
     local rot = string.format("%d/%d", a.dayInBlock, a.blockLen)
     local rot = string.format("%d/%d", a.dayInBlock, a.blockLen)
    local opsCell = "n/a"
    if wx and type(wx.getOpsData) == "function" then
      local op = getWx(a.station, y, doy)
      if op and op.alert then
        opsCell = wxBadge(op.alert.level, op.alert.label, op.condition)
      end
    end


     table.insert(out, "|-" .. rowStyle)
     table.insert(out, "|-" .. rowStyle)
     table.insert(out, string.format('| %s || %s || %s || %s || %s || %s', pFlag, name, homeCell, stationCell, rot, a.task))
     table.insert(out, string.format('| %s || %s || %s || %s || %s || %s || %s',
      pFlag, name, homeCell, stationCell, opsCell, rot, a.task
    ))
   end
   end


Line 528: Line 629:
     table.insert(out, "<span style='background:#fff3cd;padding:2px 6px;border:1px solid #ffeeba;'>Yellow</span> = Elevated / Enforcement &nbsp; ")
     table.insert(out, "<span style='background:#fff3cd;padding:2px 6px;border:1px solid #ffeeba;'>Yellow</span> = Elevated / Enforcement &nbsp; ")
     table.insert(out, "<span style='background:#f8d7da;padding:2px 6px;border:1px solid #f5c6cb;'>Red</span> = Urgent / High-Risk / Incident")
     table.insert(out, "<span style='background:#f8d7da;padding:2px 6px;border:1px solid #f5c6cb;'>Red</span> = Urgent / High-Risk / Incident")
    table.insert(out, "<br/>Ops Alert badge reflects forecast-driven operational risk (Green/Yellow/Orange/Red) and aligns with War League / Missionary posture.")
     table.insert(out, "</div>")
     table.insert(out, "</div>")
   end
   end

Revision as of 21:56, 1 January 2026

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

-- Module:HatchMinistryDailyOps
-- Daily (PSSC-calendar keyed) ops schedule for Hatch Ministry privateers.
-- Deterministic output (no math.random / no os.time seeding).
--
-- Updates embedded:
--  • Added coastal cities: Sjøsborg, Sårensby, Storesund, Slevik
--  • City flag icons use SAFE fallback to the Imperial Trade Union flag when missing
--    (with caching to avoid “too many expensive function calls”)
--  • Integrated Ops Alert + Ops Impact from Module:BassaridiaForecast (if available):
--      - Adds Ops Alert column (badge + condition) to the table
--      - Orange/Red weather can bump severity and append a concise weather posture note
--      - Sea Closed / Air No-Go forces “weather delay / hold windows” language
--  • Everything else retained:
--      - City flags in Home/Station columns
--      - 20 tasks per pool (General / North / South / Central) + privateering pool
--      - Row color-coding (Green/Yellow/Red)
--      - 10% daily chance per privateer of opportunistic privateering against “non-aligned” vessels
--
-- Usage:
--   {{#invoke:HatchMinistryDailyOps|dailyReport}}
--   {{#invoke:HatchMinistryDailyOps|dailyReport|year=52|day=118}}
--   {{#invoke:HatchMinistryDailyOps|dailyReport|legend=no}}

local p = {}

local cal = require('Module:BassaridianCalendar')
local DAYS_IN_YEAR = 183

--------------------------------------------------------------------------------
-- 0) COASTAL CITIES (expanded)
--------------------------------------------------------------------------------
local cities = {
  "Aegirheim","Skýrophos","Bjornopolis","Norsolyra","Ephyra",
  "Symphonara","Delphica","Vaeringheim","Somniumpolis",
  "Keybir-Aviv","Sufriya","Jogi","Ardclach","Riddersborg",
  "Sjøsborg","Sårensby","Storesund","Slevik"
}

--------------------------------------------------------------------------------
-- (WX) OPTIONAL: integrate deterministic Ops Alert from Module:BassaridiaForecast
--------------------------------------------------------------------------------
local wxOk, wx = pcall(require, "Module:BassaridiaForecast")
if not wxOk then wx = nil end

local function wxColor(level)
  level = tonumber(level) or 0
  if level == 3 then return "#f8d7da" end
  if level == 2 then return "#ffe5cc" end
  if level == 1 then return "#fff3cd" end
  return "#d4edda"
end

local function wxBadge(level, label, cond)
  local bg = wxColor(level)
  label = tostring(label or "Green")
  cond  = tostring(cond or "")
  return string.format(
    "<span style='background:%s;padding:2px 6px;border:1px solid #ccc;border-radius:10px;white-space:nowrap;'>%s</span>%s",
    bg, label, (cond ~= "" and ("<br/><span style=\"font-size:90%;\">" .. cond .. "</span>") or "")
  )
end

local function wxImpactText(imp)
  imp = imp or {}
  return string.format("Gnd: %s; Sea: %s; Air: %s", imp.ground or "OK", imp.sea or "OK", imp.air or "OK")
end

local function getWx(cityName, year, dayOfYear)
  if not wx or type(wx.getOpsData) ~= "function" then return nil end
  return wx.getOpsData(cityName, year, dayOfYear)
end

local function applyWeatherOverlay(text, sev, station, year, dayOfYear)
  if not wx or type(wx.getOpsData) ~= "function" then return text, sev end

  local op = getWx(station, year, dayOfYear)
  if not op or not op.alert then return text, sev end

  local lvl = tonumber(op.alert.level) or 0
  if lvl < 2 then return text, sev end -- only Orange/Red modify ops

  local hazard  = tostring(op.alert.hazard or "Weather disruption")
  local impacts = op.impacts or {}
  local cond    = tostring(op.condition or "")

  -- severity bump
  if lvl == 2 and sev == "green" then sev = "yellow" end
  if lvl >= 3 then sev = "red" end

  local overlay = " Weather posture: " .. (op.alert.label or "Orange") ..
    " – " .. hazard .. " (" .. wxImpactText(impacts) .. ")."

  if impacts.sea == "Closed" or impacts.air == "No-Go" then
    overlay = overlay .. " Corridor hold-and-release windows enforced; noncritical boardings deferred; SAR/debris-clearance standby prioritized."
    sev = "red"
  elseif impacts.sea == "Restricted" then
    overlay = overlay .. " Escort buffers widened; boarding windows shifted to safer intervals; harbor entry metered."
    if sev == "green" then sev = "yellow" end
  elseif impacts.ground == "Hazard" or impacts.ground == "Slow" then
    overlay = overlay .. " Shore leave and pier movement controlled; queues stabilized at inspection points."
    if sev == "green" then sev = "yellow" end
  else
    overlay = overlay .. " Heightened messaging to prevent panic narratives and berth congestion."
  end

  -- keep overlays short for readability
  return text .. overlay, sev
end

--------------------------------------------------------------------------------
-- FLAG FALLBACK (use ITU flag when missing) + CACHING (avoid expensive limits)
--------------------------------------------------------------------------------
local DEFAULT_FLAG = "Imperial_Trade_Union_flag.png"

local function normalizeForFlag(name)
  -- Keep Nordic letters (ø/å) intact; just remove spacing/punctuation and normalize acute vowels.
  return (name or "")
    :gsub("[%s%-’]", "")
    :gsub("[Áá]", "A"):gsub("[Éé]", "E")
    :gsub("[Íí]", "I"):gsub("[Óó]", "O")
    :gsub("[Úú]", "U"):gsub("[Ýý]", "Y")
end

local fileExistsCache = {}
local function fileExists(fileName)
  fileName = tostring(fileName or "")
  if fileName == "" then return false end
  if fileExistsCache[fileName] ~= nil then return fileExistsCache[fileName] end
  local t = mw.title.new("File:" .. fileName)
  local ok = (t and t.exists) or false
  fileExistsCache[fileName] = ok
  return ok
end

local function flagIcon(fileName, sizePx)
  local size = tonumber(sizePx) or 20
  local f = (fileName and fileName ~= "") and fileName or DEFAULT_FLAG
  return string.format("[[File:%s|%dpx]]", f, size)
end

local cityFlagCache = {}
local function cityFlagIcon(cityName, sizePx)
  local size = tonumber(sizePx) or 20
  local key = tostring(cityName or "") .. "|" .. tostring(size)
  if cityFlagCache[key] then return cityFlagCache[key] end

  local candidates = {}
  if cityName == "Skýrophos" then
    candidates = { "SkyrophosFlag.png" }
  elseif cityName == "Slevik" then
    -- known oddity on-wiki
    candidates = { "SlevikFlag.png.png", string.format("%sFlag.png", normalizeForFlag(cityName)) }
  else
    local norm = normalizeForFlag(cityName)
    candidates = { string.format("%sFlag.png", norm) }
  end

  for _, f in ipairs(candidates) do
    if fileExists(f) then
      cityFlagCache[key] = flagIcon(f, size)
      return cityFlagCache[key]
    end
  end

  cityFlagCache[key] = flagIcon(DEFAULT_FLAG, size)
  return cityFlagCache[key]
end

local function cityLink(name)
  return string.format("[[List_of_cities_in_Bassaridia_Vaeringheim#%s|%s]]", name, name)
end

--------------------------------------------------------------------------------
-- 1) ACTIVE PRIVATEERS
--------------------------------------------------------------------------------
local privateers = {
  {first="Arion",     last="Theron"},
  {first="Cassius",   last="Valerus"},
  {first="Nefra",     last="Sekeri",    nick="Sekhem"},
  {first="Lysander",  last="Pallas"},
  {first="Marwan",    last="Othman"},
  {first="Octavia",   last="Marcellus"},
  {first="Amenhotep", last="Ankhu"},
  {first="Thalia",    last="Chrysos"},
  {first="Demetrius", last="Gravis"},
  {first="Selim",     last="Bey"},
  {first="Julius",    last="Drusus"},
  {first="Leyla",     last="Han"},
  {first="Marcus",    last="Flavianus"},
  {first="Fatima",    last="Pasha"},
  {first="Cornelia",  last="Urbanus"}, -- Minister-Captain
  {first="Eudora",    last="Merit"},
  {first="Perseus",   last="Phoebus"},
  {first="Khepri",    last="Sobek"},
  {first="Lucius",    last="Aurelian"},
  {first="Aisha",     last="Dinar"},
  {first="Gaius",     last="Cassianus"},
  {first="Selene",    last="Argus"},
  {first="Arsinoe",   last="Menkaure"},
  {first="Omar",      last="Zahir"},
  {first="Dorian",    last="Grimm"},
  {first="Quintus",   last="Nero"},
  {first="Zainab",    last="Sultan"},
  {first="Cassian",   last="Corinth"},
  {first="Valeria",   last="Maximus",   nick="Ferox"},
  {first="Thorne",    last="Noctis",    nick="Mainomenos"}
}

local function privateerFlag(last)
  return string.format("[[File:%s.png|20px]]", last)
end

local function privateerNameRow(c)
  if c.nick then
    return string.format("Captain %s ''‘%s’'' %s", c.first, c.nick, c.last)
  end
  return string.format("Captain %s %s", c.first, c.last)
end

--------------------------------------------------------------------------------
-- 2) CALENDAR PARSING
--------------------------------------------------------------------------------
local function parseCalendarString(s)
  if type(s) ~= "string" then return nil end
  local dayOfYear, month, year = s:match("^(%d+),%s+([%a]+)%s+%b()%s*,%s*(%d+)%s+PSSC")
  if dayOfYear and year then
    return tonumber(year), tonumber(dayOfYear), month
  end
  return nil
end

local function extractEventText(s)
  if type(s) ~= "string" then return "" end
  local ev = s:match("PSSC%s+–%s+(.-)%s+–%s+Proverb:")
  return ev or ""
end

local function getCurrentYDOY()
  local s = cal.getCurrentDate()
  local y, doy, month = parseCalendarString(s)
  if not y or not doy then
    y, doy, month = 0, 1, ""
    s = tostring(s or "")
  end
  return y, doy, month, s, extractEventText(s)
end

--------------------------------------------------------------------------------
-- 3) DETERMINISTIC HASH + RNG (NO bit32)
--------------------------------------------------------------------------------
local function hash31(str)
  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 = seed % 2147483647
  if s <= 0 then s = s + 2147483646 end
  return function()
    s = (1103515245 * s + 12345) % 2147483647
    return s
  end
end

local function rand01(nextInt)
  return nextInt() / 2147483647
end

local function pickFromList(nextInt, list, avoidValue)
  if #list == 0 then return nil end
  if #list == 1 then return list[1] end
  local tries = 0
  while tries < 12 do
    local idx = (nextInt() % #list) + 1
    local v = list[idx]
    if v ~= avoidValue then return v end
    tries = tries + 1
  end
  for _, v in ipairs(list) do
    if v ~= avoidValue then return v end
  end
  return list[1]
end

--------------------------------------------------------------------------------
-- 4) REGIONAL CLUSTERS (updated to include new coastal cities)
--------------------------------------------------------------------------------
local clusters = {
  North   = {"Aegirheim","Bjornopolis","Norsolyra","Ephyra","Ardclach","Riddersborg","Sjøsborg","Sårensby","Storesund","Slevik"},
  Central = {"Symphonara","Delphica","Vaeringheim","Somniumpolis"},
  South   = {"Keybir-Aviv","Sufriya","Jogi"},
  Jangsong= {"Skýrophos","Ardclach","Riddersborg"}
}

local function clusterForCity(city)
  for cname, list in pairs(clusters) do
    for _, v in ipairs(list) do
      if v == city then return cname end
    end
  end
  return "Central"
end

--------------------------------------------------------------------------------
-- 5) COLOR CODING (row background reflects urgency)
--------------------------------------------------------------------------------
local function sevStyle(sev)
  if sev == "red" then
    return ' style="background-color:#f8d7da;"'
  elseif sev == "yellow" then
    return ' style="background-color:#fff3cd;"'
  else
    return ' style="background-color:#d4edda;"'
  end
end

local function bumpSev(sev)
  if sev == "green" then return "yellow" end
  if sev == "yellow" then return "red" end
  return "red"
end

--------------------------------------------------------------------------------
-- 6) TASK LIBRARY (20 per pool + privateering pool)
--------------------------------------------------------------------------------
local function T(text, sev) return {text=text, sev=sev} end

local tasks_general = {
  T("Convoy escort and corridor clearing for registered merchant traffic, enforcing registry sequence and predictable passage.","green"),
  T("VBSS surge and manifest verification, with heightened scrutiny for cargo mislabeling, forged papers, and transshipment laundering.","yellow"),
  T("Interdiction patrol against small-craft logistics supporting anti-corridor insurgent networks, with evidence handling and detainee processing discipline.","yellow"),
  T("Lane assurance work in the approaches and harbor margins, including buoy checks, beacon verification, and channel safety enforcement.","green"),
  T("Counter-smuggling sweep focused on contraband arms, unregistered ritual cargo, and false-cleared Temple consignments.","yellow"),
  T("Rapid-response security support to local authorities for port unrest, quarantine enforcement, and rumor-driven crowd surges.","red"),
  T("Search-and-rescue readiness patrol with debris clearance coordination and temporary berth control during incidents.","red"),
  T("Liaison rotation with Temple and civic screening teams for morale discipline, rumor containment, and compliance reinforcement.","yellow"),
  T("Night interception detail targeting unlit runners and coastal-shadowing craft attempting to bypass inspection windows.","yellow"),
  T("Port-entry queue control and traffic metering to prevent berth chaos, including stand-off enforcement for noncompliant hulls.","green"),
  T("Evidence convoy to a War League intake point, transporting seized cargo under chain-of-custody doctrine.","yellow"),
  T("Inspection of fishery convoys and cold-chain cargo for hidden compartments, false ice loads, and counterfeit seals.","yellow"),
  T("Anti-piracy deterrence sweep against unaffiliated raiders operating outside the registry, with warning demonstrations and capture mandates.","yellow"),
  T("Boarding-team readiness cycle and small-arms drill day, with emphasis on non-lethal deck control and restraint discipline.","green"),
  T("Escort for diplomatic or Temple couriers transiting the corridor, with counter-ambush screening at chokepoints.","yellow"),
  T("Counterfeit voucher and registry-stamp crackdown, coordinating with port clerks to identify forged paperwork patterns.","yellow"),
  T("Harbor-mouth overwatch with spotters to track suspicious loitering and false distress signaling near approach lanes.","green"),
  T("Quarantine cordon support for a suspected outbreak vessel, enforcing isolation lanes and controlled disembarkation sequencing.","red"),
  T("Canal approach patrol to prevent sabotage against locks, piers, and water infrastructure serving major quay districts.","red"),
  T("Training cruise with junior crews to certify boarding compliance, medical triage procedure, and detainee handling standards.","green")
}

local tasks_north = {
  T("Cold-water frontier patrol emphasizing raider deterrence, route denial, and winter-route landing site reconnaissance.","yellow"),
  T("Joint boarding drills and seizure-readiness for difficult-weather VBSS operations near northern chokepoints.","green"),
  T("Coastal reconnaissance for hidden coves, improvised piers, and suspected insurgent resupply anchorages.","yellow"),
  T("Ice-margin escort for timber and ore traffic, enforcing safe-lane passage through fog and drift hazards.","green"),
  T("Night watch against ghost-ship imitation tactics, verifying hull identity and rejecting falsified light codes.","yellow"),
  T("Shoreline sweep for signal cairns and illicit beacon fires used to guide runners into protected inlets.","yellow"),
  T("Interdiction of smugglers using river-mouth approaches to bypass port control, with shallow-draft pursuit elements.","yellow"),
  T("Storm-surge response patrol supporting stranded craft, hauling wreckage clear and securing debris fields.","red"),
  T("Border-proximity deterrence demonstration near contested approaches, emphasizing corridor sovereignty by presence.","yellow"),
  T("Dockyard compliance audit for unregistered repairs and hidden compartments used for contraband staging.","yellow"),
  T("Winterized escort for mail and courier traffic, enforcing strict timing windows through fog-prone waters.","green"),
  T("Interception of runner craft using fjord-shadow routes to evade inspection, with pursuit and boarding discipline.","yellow"),
  T("Harbor-mouth search pattern for missing fisher crews after sudden squall; SAR handoff to local authorities.","red"),
  T("Cold-weather medical readiness drill; hypothermia triage training paired with evacuation rehearsals.","yellow"),
  T("Counter-smuggling sweep focused on high-value furs, ore samples, and forged tax seals.","yellow"),
  T("Coastal watchtower liaison: beacon confirmation, signal discipline, and false-alarm suppression.","green"),
  T("Night shoreline patrol to prevent sabotage against lock-gates and fuel depots during high-wind periods.","red"),
  T("Escort for War League inspectors transiting northern ports; identity verification and berth control enforced.","yellow"),
  T("Evidence transfer run under snow/fog conditions; custody chain maintained despite degraded visibility.","yellow"),
  T("Deterrence cruise through rough seas to signal sovereignty; safe-distance demonstration only.","green")
}

local tasks_south = {
  T("Haifan corridor compliance sweep: routing verification, escort-buffer reinforcement, and random boarding windows.","yellow"),
  T("Harbor-entry metering and berth assignment enforcement during peak merchant throughput.","green"),
  T("Contraband focus patrol targeting shrine-counterfeit cargo and unregistered ritual items.","yellow"),
  T("VBSS series against suspect manifests tied to minor routing variances, emphasizing correction over disruption.","yellow"),
  T("Shoreline patrol to deter small-craft runner networks staging from beach coves and canal mouths.","yellow"),
  T("Escort for high-value diplomatic traffic; counter-ambush and surveillance checks at chokepoints.","yellow"),
  T("Rapid response to port unrest and rumor-driven crowd surges, coordinating with civic stewards.","red"),
  T("Quarantine enforcement support for incoming vessel under inspection; controlled disembarkation and isolation lanes.","red"),
  T("Night interception of unlit craft attempting to shadow lawful hulls into berth lanes.","yellow"),
  T("Inspection of fishery convoys for hidden compartments and false seals; evidence packaged for clerks.","yellow"),
  T("Counterfeit voucher crackdown: registry stamp verification and vendor interviews.","yellow"),
  T("Harbor-mouth overwatch; spotters track loitering craft and false distress signals.","green"),
  T("Canal-approach patrol to prevent sabotage against locks and piers.","red"),
  T("Search-and-rescue readiness patrol in heavy rain periods; debris clearance standby.","red"),
  T("Lane assurance: buoy and beacon checks to ensure safe passage and lawful routing.","green"),
  T("Interdiction against small-craft logistics supporting insurgent networks.","yellow"),
  T("Seizure-and-transfer convoy for detained contraband under chain-of-custody doctrine.","yellow"),
  T("Escort buffer widening during heightened risk periods; predictable passage enforced.","yellow"),
  T("Training cruise: boarding compliance, medical triage procedure, detainee handling standards.","green"),
  T("Dockside liaison with Temple and civic screening teams; rumor containment and compliance reinforcement.","yellow")
}

local tasks_central = {
  T("Canal-city inner-basin patrol: lock-gate oversight, pier security, and approach-lane order enforcement.","green"),
  T("VBSS rotation focused on paperwork integrity and counterfeit seal detection at high-throughput quays.","yellow"),
  T("Interdiction against canal runner craft attempting to bypass inspection windows using side channels.","yellow"),
  T("Harbor traffic control: queue metering, berth discipline, and stand-off enforcement for noncompliant hulls.","green"),
  T("Anti-smuggling sweep targeting disguised ritual cargo and forged Temple consignment manifests.","yellow"),
  T("Rapid response support to civic authorities during unrest near the docks; dispersal corridors opened.","red"),
  T("Search-and-rescue standby near canal choke points; debris fields secured and cleared.","red"),
  T("Liaison with Temple auditors and port clerks: compliance reinforcement and morale discipline checks.","yellow"),
  T("Night interception detail targeting unlit barges and covert tender rendezvous.","yellow"),
  T("Evidence convoy run to War League intake point; chain-of-custody doctrine enforced.","yellow"),
  T("Inspection of cold-chain and fishery cargo for hidden compartments and counterfeit seals.","yellow"),
  T("Anti-piracy deterrence sweep against unaffiliated raiders operating outside registry rules.","yellow"),
  T("Boarding-team readiness cycle emphasizing non-lethal deck control and restraint discipline.","green"),
  T("Escort for Temple couriers through canal junctions; counter-ambush screening at bridges.","yellow"),
  T("Counterfeit voucher and registry-stamp crackdown in dockside markets.","yellow"),
  T("Harbor-mouth overwatch with spotters to track suspicious loitering near approach lanes.","green"),
  T("Quarantine cordon support for suspected outbreak vessel; isolation lanes enforced.","red"),
  T("Canal infrastructure protection: lock-gates and waterworks sabotage prevention.","red"),
  T("Training cruise certifying boarding compliance and med-triage procedure for new crews.","green"),
  T("Routine patrol and compliance enforcement across primary canal approaches.","green")
}

local tasks_privateering = {
  T("Privateering sortie against non-aligned vessels: inspection and seizure of contraband goods concealed in false bilge compartments.","yellow"),
  T("Privateering sortie against non-aligned vessels: boarding action against a suspected pirate-supply barque, confiscating arms and fuel.","red"),
  T("Privateering sortie against non-aligned vessels: capture of a runner dhow attempting corridor bypass under black sail.","yellow"),
  T("Privateering sortie against non-aligned vessels: interdiction of a false-distress decoy ship used to lure lawful traffic.","yellow"),
  T("Privateering sortie against non-aligned vessels: seizure of illicit currency and forged vouchers intended for dockside laundering.","yellow"),
  T("Privateering sortie against non-aligned vessels: raid on a coastal cache pier, dismantling ladders, crates, and mooring points.","red"),
  T("Privateering sortie against non-aligned vessels: pursuit and boarding under cover of fog, enforcing forfeiture for evasion.","red"),
  T("Privateering sortie against non-aligned vessels: escort of a captured prize through the harbor mouth under guard.","yellow"),
  T("Privateering sortie against non-aligned vessels: boarding and confiscation of unregistered ritual cargo transported under counterfeit shrine seals.","red"),
  T("Privateering sortie against non-aligned vessels: interdiction of a night-transfer rendezvous, dispersing tenders and capturing broker craft.","red"),
  T("Privateering sortie against non-aligned vessels: targeted inspection of hulls linked to prior paper anomalies, enforcing forfeiture for repeat fraud.","yellow"),
  T("Privateering sortie against non-aligned vessels: punitive raid on suspected resupply pier and fuel caches.","red"),
  T("Privateering sortie against non-aligned vessels: seizure of contraband medicines and counterfeit labels; evidence packaged for clerks.","yellow"),
  T("Privateering sortie against non-aligned vessels: dismantle an illegal buoy line guiding ships off-lane; confiscate charts and signal codes.","yellow"),
  T("Privateering sortie against non-aligned vessels: detain a pirate recruiter crew; transport under restraint doctrine.","red"),
  T("Privateering sortie against non-aligned vessels: sweep of shadowed inlets for hidden tenders; tow seized craft to inspection berth.","yellow"),
  T("Privateering sortie against non-aligned vessels: strike deterrence demonstration and warning shots at standoff distance.","yellow"),
  T("Privateering sortie against non-aligned vessels: capture of a smuggling skiff convoy and destruction of caches.","red"),
  T("Privateering sortie against non-aligned vessels: enforce forfeiture on repeat-violator hull; escort to custody quay.","yellow"),
  T("Privateering sortie against non-aligned vessels: raid on ritual contraband broker and seizure of ledgers.","red"),
}

local function pickTask(nextInt, station)
  local pool = {}
  for _, t in ipairs(tasks_general) do table.insert(pool, t) end

  local cl = clusterForCity(station)
  if cl == "North" or cl == "Jangsong" then
    for _, t in ipairs(tasks_north) do table.insert(pool, t) end
  elseif cl == "South" then
    for _, t in ipairs(tasks_south) do table.insert(pool, t) end
  else
    for _, t in ipairs(tasks_central) do table.insert(pool, t) end
  end

  return pickFromList(nextInt, pool, nil) or T("Standard patrol and compliance enforcement.","green")
end

--------------------------------------------------------------------------------
-- 7) ASSIGNMENT MODEL (station blocks of 7–14 days)
--------------------------------------------------------------------------------
local function homeCityForCaptain(cap)
  local key = (cap.last or "") .. "|" .. (cap.first or "")
  local idx = (hash31(key) % #cities) + 1
  return cities[idx]
end

local function blockLenForCaptain(cap)
  local key = "len|" .. (cap.last or "") .. "|" .. (cap.first or "")
  return 7 + (hash31(key) % 8) -- 7..14
end

local function stationForBlock(cap, blockIndex)
  local key = string.format("station|%s|%s|%d", cap.last or "", cap.first or "", blockIndex)
  local nextInt = lcg(hash31(key))

  local home = homeCityForCaptain(cap)
  local cl = clusterForCity(home)
  local localList = clusters[cl] or clusters.Central
  local r = rand01(nextInt)

  if r < 0.65 then
    return home
  elseif r < 0.92 then
    return pickFromList(nextInt, localList, home) or home
  else
    return pickFromList(nextInt, cities, home) or home
  end
end

local function maybePrivateering(cap, absDay)
  local key = string.format("priv|%s|%s|%d", cap.last or "", cap.first or "", absDay)
  local nextInt = lcg(hash31(key))
  return (rand01(nextInt) < 0.10), nextInt
end

local function todaysAssignment(cap, year, dayOfYear, eventText)
  local absDay = year * DAYS_IN_YEAR + dayOfYear

  local blockLen = blockLenForCaptain(cap)
  local offset = hash31("off|" .. (cap.last or "") .. "|" .. (cap.first or "")) % blockLen
  local blockIndex = math.floor((absDay + offset) / blockLen)
  local dayInBlock = ((absDay + offset) % blockLen) + 1

  local station = stationForBlock(cap, blockIndex)

  -- baseline task (deterministic)
  local nextInt = lcg(hash31(string.format("task|%s|%s|%d", cap.last or "", cap.first or "", absDay)))
  local taskEntry = pickTask(nextInt, station)

  local text = taskEntry.text
  local sev  = taskEntry.sev or "green"

  -- 10% chance: opportunistic privateering against non-aligned vessels
  local priv, privRng = maybePrivateering(cap, absDay)
  if priv then
    local pe = pickFromList(privRng, tasks_privateering, nil) or tasks_privateering[1]
    text = pe.text
    sev  = pe.sev or "yellow"
    if cap.last == "Urbanus" and cap.first == "Cornelia" then
      text = "Minister-Captain sanction: " .. text
      if sev == "green" then sev = "yellow" end
    end
  end

  -- movement flavor on block edges
  if dayInBlock == 1 then
    text = "Redeployment and patrol initiation: " .. text
  elseif dayInBlock == blockLen then
    text = "Handover and corridor reset: " .. text
  end

  -- If today’s calendar event tags the station in square brackets, bump urgency and add augmentation note
  if eventText and eventText ~= "" then
    local tag = "%[" .. station:gsub("(%W)","%%%1") .. "%]"
    if eventText:match(tag) then
      text = text .. " Festival and crowd-control augmentation in support of scheduled civic observances."
      sev = bumpSev(sev)
    end
  end

  -- Minister-Captain overlay (only if not already framed as sanction)
  if cap.last == "Urbanus" and cap.first == "Cornelia" then
    if not text:match("^Minister%-Captain") then
      text = "Minister-Captain coordination: liaison with port clerks and Temple auditors; " .. text
      if sev == "green" then sev = "yellow" end
    end
  end

  -- WX overlay (Orange/Red only): weather posture + severity bump
  text, sev = applyWeatherOverlay(text, sev, station, year, dayOfYear)

  return {
    home = homeCityForCaptain(cap),
    station = station,
    blockLen = blockLen,
    dayInBlock = dayInBlock,
    task = text,
    sev = sev
  }
end

--------------------------------------------------------------------------------
-- 8) OUTPUT
--------------------------------------------------------------------------------
function p.dailyReport(frame)
  local args = (frame and frame.args) or {}
  local y, doy, month, calStr, eventText = getCurrentYDOY()

  -- Optional overrides:
  -- {{#invoke:HatchMinistryDailyOps|dailyReport|year=52|day=118}}
  if args.year and args.day then
    local yy = tonumber(args.year)
    local dd = tonumber(args.day)
    if yy and dd and dd >= 1 and dd <= DAYS_IN_YEAR then
      y, doy = yy, dd
    end
  end

  local showLegend = true
  if args.legend ~= nil then
    local v = tostring(args.legend):lower()
    showLegend = not (v == "0" or v == "no" or v == "false")
  end

  local out = {}
  table.insert(out, string.format("'''Hatch Ministry Daily Operations – Day %d, Year %d PSSC'''", doy, y))
  if calStr and calStr ~= "" then
    table.insert(out, "''Calendar:'' " .. calStr)
  end

  table.insert(out, '{| class="wikitable sortable"')
  table.insert(out, '! Privateer Flag !! Captain !! Home City !! Current Station !! Ops Alert !! Rotation !! Today\'s Activity')

  for _, cap in ipairs(privateers) do
    local a = todaysAssignment(cap, y, doy, eventText)

    local rowStyle = sevStyle(a.sev)
    local pFlag = privateerFlag(cap.last)
    local name = privateerNameRow(cap)

    local homeCell = cityFlagIcon(a.home, 20) .. " " .. cityLink(a.home)
    local stationCell = cityFlagIcon(a.station, 20) .. " " .. cityLink(a.station)
    local rot = string.format("%d/%d", a.dayInBlock, a.blockLen)

    local opsCell = "n/a"
    if wx and type(wx.getOpsData) == "function" then
      local op = getWx(a.station, y, doy)
      if op and op.alert then
        opsCell = wxBadge(op.alert.level, op.alert.label, op.condition)
      end
    end

    table.insert(out, "|-" .. rowStyle)
    table.insert(out, string.format('| %s || %s || %s || %s || %s || %s || %s',
      pFlag, name, homeCell, stationCell, opsCell, rot, a.task
    ))
  end

  table.insert(out, "|}")

  if showLegend then
    table.insert(out, "\n<div style='margin-top:0.5em; font-size:90%;'>")
    table.insert(out, "<b>Legend:</b> ")
    table.insert(out, "<span style='background:#d4edda;padding:2px 6px;border:1px solid #c3e6cb;'>Green</span> = Routine / Standard &nbsp; ")
    table.insert(out, "<span style='background:#fff3cd;padding:2px 6px;border:1px solid #ffeeba;'>Yellow</span> = Elevated / Enforcement &nbsp; ")
    table.insert(out, "<span style='background:#f8d7da;padding:2px 6px;border:1px solid #f5c6cb;'>Red</span> = Urgent / High-Risk / Incident")
    table.insert(out, "<br/>Ops Alert badge reflects forecast-driven operational risk (Green/Yellow/Orange/Red) and aligns with War League / Missionary posture.")
    table.insert(out, "</div>")
  end

  return table.concat(out, "\n")
end

return p