diff --git a/slazero2.mjs b/slazero2.mjs index 7c1db86..96a0a34 100644 --- a/slazero2.mjs +++ b/slazero2.mjs @@ -3950,6 +3950,7 @@ Hooks.on("getSceneControlButtons", (controls) => { { name: "sla-threat", title: "NPC Threat Board", icon: "fas fa-skull", fn: () => SLANPCThreatBoard.open() }, { name: "sla-bpn", title: "BPN Tracker", icon: "fas fa-clipboard-check", fn: () => SLABPNTracker.open() }, { name: "sla-condapp", title: "Condition Applicator", icon: "fas fa-bolt", fn: () => SLAConditionApplicator.open() }, + { name: "sla-bpn-gen", title: "Generate BPN", icon: "fas fa-file-signature", fn: () => SLABPNGenerator.open() }, ]; // Foundry v14: controls is a plain object keyed by group name @@ -10589,3 +10590,241 @@ Conditions tick automatically. When Health or Resolve breaks, the d66 critical r await game.settings.set("slazero2", FLAG_KEY, true); }); + +// ══════════════════════════════════════════════════════════════════════════════ +// SLA ZERO TWO — CORE BPN GENERATOR (v2.1.0) +// Blueprint News files generated in-system: colour logic, code, contact, +// objectives, complication, reward. Creates a briefing journal + chat post. +// (The standalone sla-blueprint-news-zero module targets the original +// zero-engine system; SLA Zero Two carries its own generator in core.) +// ══════════════════════════════════════════════════════════════════════════════ + +class SLABPNGenerator { + static COLOURS = { + blue: { + label: "Blue", tint: "#3399ff", brief: "civic order / security", + objectives: [ + "Escort a Shiver patrol through {sector} and return with all bodies breathing", + "Guard the {place} against gang reprisal for one full shift", + "Recover a stolen SLA property crate before it crosses {sector}", + "Suppress a street disturbance in {sector} without collateral on camera" + ], + reward: [180, 320], complication: [ + "The gang has a sponsor with cameras of its own", + "One of the Shivers is on the take", + "The property crate is leaking something", + "A rival squad has the same contract" + ] + }, + green: { + label: "Green", tint: "#44cc66", brief: "investigation / intelligence", + objectives: [ + "Trace the source of counterfeit KickStart flooding {sector}", + "Locate a missing DAC researcher last seen near {place}", + "Document serial-killer activity in {sector} and identify the pattern", + "Verify or refute Carrien sightings below {place}" + ], + reward: [250, 450], complication: [ + "The trail leads somewhere SLA does not want filmed", + "The witness will only talk to an Ebon", + "The evidence keeps being bought before you arrive", + "Someone is running the same investigation — better funded" + ] + }, + red: { + label: "Red", tint: "#cc2222", brief: "hostile suppression", + objectives: [ + "Clear the {place} of Carrien infestation — flame recommended", + "Terminate the gang leadership of {sector} — collateral discouraged, not forbidden", + "Put down a rogue Stormer unit gone feral near {place}", + "Break the siege of the {place} and hold until relieved" + ], + reward: [400, 700], complication: [ + "They are dug in deeper than the brief admits", + "Half of them used to be SLA", + "The extraction window is a lie", + "Something bigger is eating the gang from below" + ] + }, + silver: { + label: "Silver", tint: "#aaaacc", brief: "corporate / escort", + objectives: [ + "Escort a Dept. of Propaganda film crew through {sector} — keep the talent alive", + "Bodyguard a corporate negotiator at the {place} summit", + "Recover a defecting executive from {sector} before their employer does", + "Secure the {place} for a sponsorship gala" + ], + reward: [350, 600], complication: [ + "The principal wants to be filmed in danger", + "The other side bid on your contract too", + "The defector is carrying more than data", + "The gala guest list includes someone who wants you dead" + ] + }, + gold: { + label: "Gold", tint: "#e3c300", brief: "high value / high oversight", + objectives: [ + "Retrieve a prototype Sleight asset from a crash site in {sector}", + "Extract a Dark Finder informant alive from {place}", + "Recover LAD-flagged remains before a rival house does", + "Deliver a sealed case to {place}. Do not open the case." + ], + reward: [700, 1200], complication: [ + "Head Office is watching this one live", + "The asset has opinions about being retrieved", + "The case is ticking", + "Success means someone important owes you — and hates it" + ] + }, + black: { + label: "Black", tint: "#555555", brief: "deniable", + objectives: [ + "Erase all evidence of an operation that never happened in {sector}", + "Retire a decommissioned operative quietly — no cameras, no LAD ping", + "Plant evidence at the {place} and be gone before the Shivers arrive", + "Escort a thing that does not appear on any manifest out of {sector}" + ], + reward: [600, 1000], complication: [ + "There is no debrief for this one", + "Your own footage is the loose end", + "The target knows you are coming and has prepared a confession", + "Payment routes through an account that does not exist" + ] + } + }; + + static SECTORS = ["Downtown", "Suburbia", "Lower Downtown", "Cannibal Sector 1 fringe", "Sector 41", "the Fen Street corridor", "Grid Square 19", "the Pipe District"]; + static PLACES = ["Karma clinic", "Third Eye News relay", "abandoned DAC lab", "Shiver station 9", "monorail interchange", "Meat Market", "sponsorship arena", "sub-level archive"]; + + static _pick(arr) { return arr[Math.floor(Math.random() * arr.length)]; } + + static _fill(text) { + return text.replace("{sector}", this._pick(this.SECTORS)).replace("{place}", this._pick(this.PLACES)); + } + + static _code(colourKey) { + const digits = String(Math.floor(Math.random() * 900) + 100); + const letter = "ABCDEFGHJKLMNPRSTVWX"[Math.floor(Math.random() * 20)]; + return `${colourKey.charAt(0).toUpperCase()}${digits}${letter}`; + } + + static async generate(colourKey) { + const c = this.COLOURS[colourKey]; + if (!c) return; + const code = this._code(colourKey); + const objective = this._fill(this._pick(c.objectives)); + const complication = this._pick(c.complication); + const reward = Math.round((c.reward[0] + Math.random() * (c.reward[1] - c.reward[0])) / 10) * 10; + const contact = _slaRandomName("human"); + + const briefHtml = ` +
${c.brief}
+ISSUING CONTACT: ${contact}, SLA Industries
+OBJECTIVE: ${objective}.
+REWARD: ${reward}c per operative on completion and debrief.
+NOTES: Standard rules of engagement. Ammunition is billed. The cameras are always on.
+COMPLICATION: ${complication}.
+SUGGESTED OPPOSITION: use the Group NPC Generator at threat ${colourKey === "red" || colourKey === "gold" ? "4-6" : colourKey === "black" ? "5-7" : "2-4"}.
+