Radar-scope pause screen + Providence/Calamity critical events — v2.0.2
Pause: replace the stock spinner with an animated DG2 radar scope (centre- swept beam, phosphor trail, range rings, glinting blips) captioned MISSION PAUSED / ALL STATIONS HOLD. Home rule (criticalEvents automation setting, default on): when an Agent rolls a natural 01 on a percentile test, roll 1d6 on the PROVIDENCE table (1 = recover 1D4 SAN ... 6 = the whole cell gains +20%/1 WP); a natural 00 rolls on the CALAMITY table (1 = equipment failure ... 6 = everyone in the scene loses SAN as something notices the cell). Events post as dramatic gold/red dossier chat cards with the d6 attached (themed 3D dice); the Handler applies effects. Verified both paths end-to-end in isolated Foundry. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
+20
-7
@@ -225,14 +225,27 @@ Hooks.on("renderGamePause", function (_, html, options) {
|
||||
if (options.cssClass !== "paused") return;
|
||||
|
||||
try {
|
||||
const gamePausedOverrideText = game.i18n.translations.DG.MissionPaused;
|
||||
const caption = game.i18n.translations.DG?.MissionPaused ?? "Mission Paused";
|
||||
const sub = game.i18n.translations.DG2?.PausedSub ?? "ALL STATIONS HOLD // AWAITING HANDLER";
|
||||
|
||||
if (typeof gamePausedOverrideText === "undefined") {
|
||||
return;
|
||||
}
|
||||
|
||||
html.querySelector("figcaption").textContent = gamePausedOverrideText;
|
||||
html.querySelector("img").classList.remove("fa-spin"); // I don't like the logo spinning personally
|
||||
// Replace the default spinner with a DG2 radar-scope pause animation.
|
||||
html.classList.add("dg2-pause");
|
||||
html.innerHTML = `
|
||||
<div class="dg2-pause-scope" aria-hidden="true">
|
||||
<span class="dg2-pause-ring r1"></span>
|
||||
<span class="dg2-pause-ring r2"></span>
|
||||
<span class="dg2-pause-ring r3"></span>
|
||||
<span class="dg2-pause-cross"></span>
|
||||
<span class="dg2-pause-wedge"></span>
|
||||
<span class="dg2-pause-beam"></span>
|
||||
<span class="dg2-pause-hub"></span>
|
||||
<span class="dg2-pause-blip b1"></span>
|
||||
<span class="dg2-pause-blip b2"></span>
|
||||
</div>
|
||||
<figcaption class="dg2-pause-caption">
|
||||
<span class="dg2-pause-title">${caption}</span>
|
||||
<span class="dg2-pause-sub">${sub}</span>
|
||||
</figcaption>`;
|
||||
} catch {
|
||||
// noop
|
||||
}
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
/**
|
||||
* Delta Green 2 home rule — critical events.
|
||||
*
|
||||
* A natural 01 on any percentile test triggers PROVIDENCE: a d6 roll on an
|
||||
* escalating table of boons (1 = a small mercy, 6 = the whole cell benefits).
|
||||
* A natural 00 (100) triggers CALAMITY: a d6 roll on an escalating table of
|
||||
* misfortunes (1 = bad but not crippling, 6 = everyone at the table pays).
|
||||
*
|
||||
* Events are announced with a dramatic chat card; the Handler applies the
|
||||
* mechanical effects. Toggled by the `criticalEvents` automation setting.
|
||||
*/
|
||||
import { tagRollDice } from "../integrations/dice-so-nice.js";
|
||||
|
||||
const PROVIDENCE = [
|
||||
{
|
||||
title: "A MOMENT OF CLARITY",
|
||||
text: "For one impossible second, the world makes sense. The static in the Agent's head goes quiet. <b>Recover 1D4 SAN.</b>",
|
||||
},
|
||||
{
|
||||
title: "MUSCLE MEMORY",
|
||||
text: "Training the Agent forgot they had takes over completely. <b>Mark this skill for improvement</b> as if it had been earned the hard way — and it costs nothing tonight.",
|
||||
},
|
||||
{
|
||||
title: "ADRENALINE EDGE",
|
||||
text: "Time dilates. Every detail is crystal. The Agent gains <b>+20% to their next test this scene</b>, any skill.",
|
||||
},
|
||||
{
|
||||
title: "THE PROGRAM PROVIDES",
|
||||
text: "Against all odds, exactly the right thing is at hand — a spare magazine, a working flashlight, a door someone forgot to lock. <b>Recover 1D4 WP and gain +20% to the Agent's next test this scene.</b>",
|
||||
},
|
||||
{
|
||||
title: "UNTOUCHABLE",
|
||||
text: "Tonight, the dark looks elsewhere. <b>The next attack or hazard that would harm the Agent this scene misses, glances off, or simply… doesn't.</b>",
|
||||
},
|
||||
{
|
||||
title: "THE CELL IN PERFECT SYNC",
|
||||
text: "One flawless action locks the whole team into rhythm — hand signals, breath, heartbeat. <b>Every Agent in the scene recovers 1 WP and gains +20% to their next test.</b> For a moment, Delta Green is winning.",
|
||||
},
|
||||
];
|
||||
|
||||
const CALAMITY = [
|
||||
{
|
||||
title: "EQUIPMENT FAILURE",
|
||||
text: "The tool betrays its owner — the weapon jams, the lockpick snaps, the radio dies mid-word. <b>The item used is fouled or broken until repaired or reloaded.</b> Bad. Not crippling. Yet.",
|
||||
},
|
||||
{
|
||||
title: "BLOOD ON THE SNOW",
|
||||
text: "A misstep, a torn seam, an edge in the dark. <b>The Agent takes 1D4 damage</b> — nothing fatal, but it will be felt for the rest of the night.",
|
||||
},
|
||||
{
|
||||
title: "RATTLED",
|
||||
text: "The failure gets inside. Hands shake; the checklist scatters. <b>The Agent loses 1D4 WP and takes −20% on their next test this scene.</b>",
|
||||
},
|
||||
{
|
||||
title: "SOMETHING NOTICED",
|
||||
text: "In the wake of the blunder there is a pause — the sense of an ear turning. <b>The Agent makes a SAN test (0/1D4).</b> Whatever is out there now knows one of their names.",
|
||||
},
|
||||
{
|
||||
title: "THE PRICE IS PAID IN FULL",
|
||||
text: "Everything that could go wrong, does, at once. <b>The Agent takes 1D6 damage AND their equipped item is dropped or broken.</b> Somewhere, quietly, a door that was closed is now open.",
|
||||
},
|
||||
{
|
||||
title: "IT SEES ALL OF YOU",
|
||||
text: "The failure tears something open, and for a heartbeat every Agent stands in the light of it. <b>Everyone in the scene loses 0/1D4 SAN and takes −20% on their next test.</b> Radios whisper. Lights gutter. It knows the cell exists.",
|
||||
},
|
||||
];
|
||||
|
||||
/**
|
||||
* Check an evaluated percentile roll for a natural 01 / 00 and, if the
|
||||
* home rule is enabled and the roller is an Agent, post the event card.
|
||||
*
|
||||
* @param {DGPercentileRoll} roll - the evaluated roll (post-toChat).
|
||||
* @returns {Promise<ChatMessage|null>}
|
||||
*/
|
||||
export async function maybeTriggerCriticalEvent(roll) {
|
||||
try {
|
||||
if (!game.settings.get("deltagreen2", "criticalEvents")) return null;
|
||||
if (roll.actor?.type !== "agent") return null;
|
||||
if (roll.total !== 1 && roll.total !== 100) return null;
|
||||
|
||||
const providence = roll.total === 1;
|
||||
const d6 = new Roll("1d6");
|
||||
await d6.evaluate();
|
||||
tagRollDice(d6, providence ? "dg2-agency" : "dg2-unnatural");
|
||||
const entry = (providence ? PROVIDENCE : CALAMITY)[d6.total - 1];
|
||||
|
||||
const actorName = roll.actor?.name ?? "The Agent";
|
||||
const rollName = roll.localizedKey ?? "the test";
|
||||
|
||||
const strip = providence
|
||||
? { type: "PROVIDENCE — NATURAL 01", cls: "dg2-msg-providence", icon: "fa-star-of-life" }
|
||||
: { type: "CALAMITY — NATURAL 00", cls: "dg2-msg-calamity", icon: "fa-radiation" };
|
||||
|
||||
const lede = providence
|
||||
? `For one impossible moment, everything breaks <i>${actorName}'s</i> way on <b>${rollName}</b>.`
|
||||
: `On <b>${rollName}</b>, the night turns on <i>${actorName}</i>.`;
|
||||
|
||||
const content = `
|
||||
<div class="dg2-card dg2-crit-event">
|
||||
<div class="dg2-card-strip">
|
||||
<span class="dg2-card-type">${strip.type}</span>
|
||||
<span class="dg2-card-org">${game.i18n.localize("DG2.Chat.EyesOnly")}</span>
|
||||
</div>
|
||||
<div class="dg2-card-title">
|
||||
<span class="dg2-card-name">${entry.title}</span>
|
||||
<span class="dg2-chip"><i class="fas fa-dice-d6"></i> ${d6.total} / 6</span>
|
||||
</div>
|
||||
<p class="dg2-crit-lede">${lede}</p>
|
||||
<p class="dg2-crit-text">${entry.text}</p>
|
||||
</div>`;
|
||||
|
||||
return await ChatMessage.create({
|
||||
content,
|
||||
rolls: [d6],
|
||||
sound: CONFIG.sounds.dice,
|
||||
speaker: ChatMessage.getSpeaker({ actor: roll.actor }),
|
||||
flags: { deltagreen2: { variant: providence ? "providence" : "calamity" } },
|
||||
});
|
||||
} catch (ex) {
|
||||
console.warn("Delta Green 2 | critical event failed", ex);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
+5
-1
@@ -5,6 +5,7 @@ import {
|
||||
tagRollDice,
|
||||
colorsetForPercentileRoll,
|
||||
} from "../integrations/dice-so-nice.js";
|
||||
import { maybeTriggerCriticalEvent } from "../other/critical-events.js";
|
||||
|
||||
const { renderTemplate } = foundry.applications.handlebars;
|
||||
const { DialogV2 } = foundry.applications.api;
|
||||
@@ -410,10 +411,13 @@ export class DGPercentileRoll extends DGRoll {
|
||||
[keyForUpdate]: true,
|
||||
});
|
||||
|
||||
await maybeTriggerCriticalEvent(this);
|
||||
return message;
|
||||
}
|
||||
|
||||
return this.toMessage({ content: html });
|
||||
const message = await this.toMessage({ content: html });
|
||||
await maybeTriggerCriticalEvent(this);
|
||||
return message;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -220,6 +220,10 @@ class AutomationSettings extends SettingForm {
|
||||
default: true,
|
||||
type: Boolean,
|
||||
},
|
||||
criticalEvents: {
|
||||
default: true,
|
||||
type: Boolean,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user