v2.1.0 — post-evaluation features: core BPN Generator + rolltable provisioning

- SLABPNGenerator: full Blueprint News generation in core — six colour lines
  (blue/green/red/silver/gold/black) with colour-appropriate objectives,
  complications, contacts, codes and reward bands. Creates a two-page journal
  (player Brief + GM-only Dossier) and posts a dispatch card to chat.
  Registered as a GM scene-control tool. (The standalone BPN module targets
  the original zero-engine system; SLA Zero Two is self-contained.)
- Rolltable provisioning: the engine draws 'Ranged/Melee Weapon Critical Fail
  (2-12)' tables by name but fresh worlds had none — all five system tables
  (panic, d66 phys/mental crits, both crit-fails) are now auto-created from
  packs/tables-data.json in any world where they're missing.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-14 16:10:39 +01:00
co-authored by Claude Fable 5
parent 772facb187
commit 58b8c0a2e9
2 changed files with 240 additions and 1 deletions
+239
View File
@@ -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 = `
<div style="font-family:'SLA Borg UI',sans-serif;">
<h1 style="color:${c.tint};margin-bottom:2px;">BPN ${code} — ${c.label.toUpperCase()}</h1>
<p style="color:#8a8a82;margin-top:0;"><em>${c.brief}</em></p>
<hr/>
<p><strong>ISSUING CONTACT:</strong> ${contact}, SLA Industries</p>
<p><strong>OBJECTIVE:</strong> ${objective}.</p>
<p><strong>REWARD:</strong> ${reward}c per operative on completion and debrief.</p>
<p><strong>NOTES:</strong> Standard rules of engagement. Ammunition is billed. The cameras are always on.</p>
</div>`;
const gmHtml = `
<div style="font-family:'SLA Borg UI',sans-serif;">
<h2 style="color:#cc1111;">GM DOSSIER — what the brief doesn't say</h2>
<p><strong>COMPLICATION:</strong> ${complication}.</p>
<p><strong>SUGGESTED OPPOSITION:</strong> use the Group NPC Generator at threat ${colourKey === "red" || colourKey === "gold" ? "4-6" : colourKey === "black" ? "5-7" : "2-4"}.</p>
</div>`;
const journal = await JournalEntry.create({
name: `BPN ${code} — ${c.label}`,
pages: [
{ name: "Operative Brief", type: "text", text: { format: 1, content: briefHtml } },
{ name: "GM Dossier", type: "text", text: { format: 1, content: gmHtml },
ownership: { default: CONST.DOCUMENT_OWNERSHIP_LEVELS.NONE } }
]
});
await ChatMessage.create({
speaker: { alias: "SLA Industries — Dispatch" },
content: `<div class="yze-initiative-card" style="border-left-color:${c.tint}">
<div class="yze-initiative-title" style="color:${c.tint}">📠 BPN ${code} — ${c.label.toUpperCase()}</div>
<div>${objective}.</div>
<div style="margin-top:4px;color:#8a8a82;">Reward ${reward}c/operative · contact ${contact} · brief filed in Journal</div>
</div>`
});
journal.sheet.render(true);
return journal;
}
static open() {
const buttons = Object.entries(this.COLOURS).map(([key, c]) =>
`<button type="button" class="bpn-gen-colour" data-colour="${key}" style="
display:block;width:100%;margin:3px 0;padding:8px;text-align:left;
background:linear-gradient(180deg,#1d1d1d,#0a0a0a);
border:1px solid ${c.tint};border-left:5px solid ${c.tint};
color:${c.tint};font-family:'SLA Borg UI',sans-serif;letter-spacing:1px;cursor:pointer;">
${c.label.toUpperCase()} <span style="color:#8a8a82;float:right">${c.brief}</span>
</button>`).join("");
const dlg = new Dialog({
title: "Generate BPN",
content: `<div style="padding:4px 2px;">${buttons}</div>`,
buttons: {
random: {
icon: '<i class="fas fa-dice"></i>', label: "Random Colour",
callback: () => this.generate(this._pick(Object.keys(this.COLOURS)))
},
cancel: { icon: '<i class="fas fa-times"></i>', label: "Cancel" }
},
default: "random",
render: (html) => {
const root = html instanceof HTMLElement ? html : html[0];
root.querySelectorAll(".bpn-gen-colour").forEach(btn => {
btn.addEventListener("click", () => { dlg.close(); this.generate(btn.dataset.colour); });
});
}
}, { classes: ["slazero2", "dialog"], width: 400 });
dlg.render(true);
}
}
// ══════════════════════════════════════════════════════════════════════════════
// SLA ZERO TWO — ROLLTABLE PROVISIONING (v2.1.0)
// The dice engine draws "Ranged/Melee Weapon Critical Fail (2-12)" rolltables
// by name. Ship them (plus Panic and the d66 crit references) into any world
// where they're missing. Idempotent by table name.
// ══════════════════════════════════════════════════════════════════════════════
Hooks.once("ready", async () => {
if (!game.user?.isGM) return;
try {
const resp = await fetch("systems/slazero2/packs/tables-data.json");
if (!resp.ok) return;
const data = await resp.json();
let created = 0;
for (const key of Object.keys(data)) {
const t = data[key];
if (!t?.name || game.tables.getName(t.name)) continue;
await RollTable.create({
name: t.name,
description: t.description || "",
formula: (t.formula || "1d6").replace("@stress", "0"),
replacement: true,
displayRoll: true,
results: (t.results || []).map(r => ({
type: CONST.TABLE_RESULT_TYPES?.TEXT ?? 0,
text: r.text,
range: r.range,
weight: 1
}))
});
created++;
}
if (created > 0) {
console.log(`SLA Zero Two | Created ${created} system rolltable(s)`);
ui.notifications.info(`SLA Zero Two: ${created} rules rolltable(s) added to this world.`);
}
} catch (err) {
console.warn("SLA Zero Two | Rolltable provisioning failed:", err);
}
});
+1 -1
View File
@@ -2,7 +2,7 @@
"id": "slazero2",
"title": "SLA Zero Two",
"description": "SLA Zero Two \u2014 the sequel system for SLA Industries on the Zero Engine ruleset. All the mechanics of Zero Engine v1.7.6 with a full presentation overhaul: custom dice, redesigned sheets, and cinematic chat cards.",
"version": "2.0.0",
"version": "2.1.0",
"compatibility": {
"minimum": 13,
"verified": "14.359"