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:
+239
@@ -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
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user