Files
DeltaGreen2/module/deltagreen.js
T
tevansandClaude Fable 5 7bbe36c29e Deterministic pause overlay via GamePause subclass — v2.0.4
The v2.0.3 pauseGame-hook re-injection raced Foundry's own pause re-render:
on slower clients the default GAME PAUSED markup landed after our overlay,
so pressing Space after an unpause showed the stock screen. Register a
DG2GamePause subclass (CONFIG.ui.pause) that applies the radar-scope
overlay inside _onRender - part of every render, no timing window. Hook
fallbacks retained (with retries) for cores lacking the AppV2 GamePause.
Verified with five rapid unpause/pause cycles at 150ms settle.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-16 22:27:50 +01:00

323 lines
10 KiB
JavaScript

// Import Modules
import DG from "./config.js";
import DeltaGreenActor from "./actor/actor.js";
import DGAgentSheet from "./sheets/agent-sheet.js";
import DeltaGreenItem from "./item/item.js";
import DeltaGreenItemSheet from "./item/item-sheet.js";
import * as DGRolls from "./roll/roll.js";
import registerSystemSettings from "./settings.js";
import preloadHandlebarsTemplates from "./templates.js";
import registerHandlebarsHelpers from "./other/register-helpers.js";
import ParseDeltaGreenStatBlock from "./other/stat-parser-macro.js";
import {
createDeltaGreenMacro,
rollItemMacro,
rollItemSkillCheckMacro,
rollSkillMacro,
rollSkillTestAndDamageForOwnedItem,
} from "./other/macro-functions.js";
import { handleInlineActions } from "./other/inline.js";
import { registerDG2DiceSoNice } from "./integrations/dice-so-nice.js";
import DGNPCSheet from "./sheets/npc-sheet.js";
import DGUnnaturalSheet from "./sheets/unnatural-sheet.js";
import DGVehicleSheet from "./sheets/vehicle-sheet.js";
import DGAgentSheetV2 from "./sheets/agent-sheet-v2.js";
import AgentData from "./data/actor/agent.js";
import UnnaturalData from "./data/actor/unnatural.js";
import NPCData from "./data/actor/npc.js";
import VehicleData from "./data/actor/vehicle.js";
import WeaponItemData from "./data/item/weapon.js";
import ArmorItemData from "./data/item/armor.js";
import MotivationItemData from "./data/item/motivation.js";
import BondItemData from "./data/item/bond.js";
import GearItemData from "./data/item/gear.js";
import TomeItemData from "./data/item/tome.js";
import RitualItemData from "./data/item/ritual.js";
const { Actors, Items } = foundry.documents.collections;
Hooks.once("init", async () => {
// Own the pause UI: applying our overlay inside _onRender means it is part
// of EVERY render of the pause application - no hook/timing races.
try {
const BasePause = foundry.applications?.ui?.GamePause;
if (BasePause) {
class DG2GamePause extends BasePause {
/** @override */
async _onRender(context, options) {
await super._onRender?.(context, options);
applyDG2PauseOverlay(this.element);
}
}
CONFIG.ui.pause = DG2GamePause;
}
} catch (ex) {
console.warn("Delta Green 2 | could not register pause subclass", ex);
}
Object.assign(CONFIG.Actor.dataModels, {
// The keys are the types defined in system.json documentTypes.Actor
agent: AgentData,
unnatural: UnnaturalData,
npc: NPCData,
vehicle: VehicleData,
});
Object.assign(CONFIG.Item.dataModels, {
weapon: WeaponItemData,
armor: ArmorItemData,
motivation: MotivationItemData,
bond: BondItemData,
gear: GearItemData,
tome: TomeItemData,
ritual: RitualItemData,
});
game.deltagreen = {
DeltaGreenActor,
DeltaGreenItem,
rollItemMacro,
rollItemSkillCheckMacro,
rollSkillMacro,
ParseDeltaGreenStatBlock,
rollSkillTestAndDamageForOwnedItem,
};
/**
* Set an initiative formula for the system
* @type {String}
*/
CONFIG.Combat.initiative = {
formula: "@statistics.dex.value",
decimals: 0,
};
// Register custom dice rolls
Object.values(DGRolls).forEach((cls) => CONFIG.Dice.rolls.push(cls));
// Register System Settings
registerSystemSettings();
// Define custom Entity classes
CONFIG.Actor.documentClass = DeltaGreenActor;
CONFIG.Item.documentClass = DeltaGreenItem;
// Unregister core sheets.
Actors.unregisterSheet("core", foundry.appv1.sheets.ActorSheet);
Items.unregisterSheet("core", foundry.appv1.sheets.ItemSheet);
// Register all actor sheets.
const sheetClassMap = {
agent: DGAgentSheet,
npc: DGNPCSheet,
unnatural: DGUnnaturalSheet,
vehicle: DGVehicleSheet,
};
Object.entries(sheetClassMap).forEach(([actorType, SheetClass]) => {
Actors.registerSheet(DG.ID, SheetClass, {
makeDefault: true,
themes: null,
label: `DG.Sheet.class.${actorType}`,
types: [actorType],
});
});
// Don't register new sheet yet.
// Actors.registerSheet(DG.ID, DGAgentSheetV2, {
// makeDefault: false,
// themes: null,
// label: `Agent Sheet V2`,
// types: ["agent"],
// });
// Register item sheet.
Items.registerSheet(DG.ID, DeltaGreenItemSheet, {
makeDefault: true,
label: "DG.Sheet.class.item",
themes: null,
});
// Preload Handlebars Templates
preloadHandlebarsTemplates();
// Add Handlebars helpers
registerHandlebarsHelpers();
});
Hooks.once("ready", async () => {
// Wait to register hotbar drop hook on ready so that modules could register earlier if they want to
// TODO: Fix eslint issue on next line
// eslint-disable-next-line consistent-return
Hooks.on("hotbarDrop", (bar, data, slot) => {
// note - if we don't limit this logic to just items, we'll break re-ording macros on the bar
if (data.type === "Item") {
// this is async!!
createDeltaGreenMacro(data, slot);
// this must return false here! This stops the default logic on item drop from occuring,
// which is a macro to open an item sheet
return false;
}
});
});
Hooks.on("preCreateItem", (item) => {
if (item.img === "icons/svg/item-bag.svg") {
if (item.type === "bond") {
item.updateSource({
img: "systems/deltagreen2/assets/icons/person-black-bg.svg",
});
} else if (item.type === "tome") {
item.updateSource({
img: "systems/deltagreen2/assets/icons/book-cover.svg",
});
} else if (item.type === "ritual") {
item.updateSource({
img: "systems/deltagreen2/assets/icons/bookmarklet.svg",
});
} else {
item.updateSource({
img: "systems/deltagreen2/assets/icons/swap-bag-black-bg.svg",
});
}
}
});
// Hook into the render call for the Actors Directory to add an extra button
Hooks.on("renderActorDirectory", (app, html, data) => {
if (!game.user.isGM) {
return;
}
if (html.querySelector("#statParserButton")) {
return;
}
const button = document.createElement("button");
button.id = "statParserButton";
const icon = document.createElement("i");
icon.classList.add("fas", "fa-file-import");
button.appendChild(icon);
button.append("Delta Green Stat Block Parser");
html.querySelector(".directory-footer").appendChild(button);
if (button) {
button.addEventListener("click", function (event) {
ParseDeltaGreenStatBlock();
});
}
});
// Note - this event is fired on ALL connected clients...
Hooks.on("createActor", async (actor, options, userId) => {
try {
// use this to trap on if this hook is firing for the same user that triggered the create
// can put logic specific to a particular user session below
if (userId !== game.user.id) return;
if (actor === null) return;
if (actor.type === "agent") {
// throw on an unarmed strike item for convenience
actor.AddUnarmedAttackItemIfMissing();
} else if (actor.type === "vehicle") {
actor.AddBaseVehicleItemsIfMissing();
}
} catch (ex) {
console.log(ex);
}
});
// Register themed Delta Green 2 dice with Dice So Nice, if present.
Hooks.once("diceSoNiceReady", (dice3d) => {
try {
registerDG2DiceSoNice(dice3d);
} catch (ex) {
console.warn("Delta Green 2 | Failed to register Dice So Nice theme", ex);
}
});
/**
* Replace the stock pause spinner with the DG2 radar-scope animation.
* Idempotent: safe to call on every render/toggle; skips if already applied.
* Wired to BOTH renderGamePause and pauseGame so the scope appears no matter
* which path Foundry takes when the pause state changes.
*/
function applyDG2PauseOverlay(element) {
try {
const el = element ?? document.getElementById("pause");
if (!el || !game.paused) return;
if (el.querySelector(".dg2-pause-scope")) return; // already applied
const caption = game.i18n.translations.DG?.MissionPaused ?? "Mission Paused";
const sub =
game.i18n.translations.DG2?.PausedSub ??
"All stations hold // awaiting Handler";
el.classList.add("dg2-pause");
el.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 (ex) {
console.warn("Delta Green 2 | pause overlay failed", ex);
}
}
Hooks.on("renderGamePause", (_app, html) => {
const el = html instanceof HTMLElement ? html : html?.[0];
applyDG2PauseOverlay(el);
});
Hooks.on("pauseGame", () => {
// Fallback for cores where the pause subclass isn't registered: re-apply
// after Foundry's own render, with retries to outlast a slow re-render.
for (const delay of [50, 250, 700]) {
setTimeout(() => applyDG2PauseOverlay(), delay);
}
});
Hooks.once("ready", () => applyDG2PauseOverlay());
function addEventListenerToChatMessage(element) {
element.addEventListener("click", (event) => {
const btnWithAction = event.target.closest("button[data-action]");
const message = event.target.closest("li[data-message-id]");
const { messageId } = message?.dataset || {};
if (btnWithAction && messageId) {
handleInlineActions(btnWithAction, messageId);
}
});
}
Hooks.on("renderChatLog", async (app, element) => {
addEventListenerToChatMessage(element);
});
Hooks.on("renderChatMessageHTML", async (app, element, context) => {
// Style DG2 roll messages as dossier cards (variant set in DGRoll.toMessage).
const variant = app.getFlag?.("deltagreen2", "variant");
if (variant) {
element.classList.add("dg2-msg", `dg2-msg-${variant}`);
}
// ignore non chat card notifications
if (!context.canClose) return;
addEventListenerToChatMessage(element);
});