feat: player permissions enforcement, CSS overhaul, actor/chat template updates
- Enforce sidebar directory restrictions for non-GM players across actors, scenes, journal, tables, compendium, and combat - Major CSS refactor in mosh.css (layout, sizing, colour passes) - Actor sheet JS: extended item handling and sheet render logic - Actor data model updates in actor.js - Item sheet additions in item-sheet.js - Updated chat templates: itemRoll, modifyActor, modifyItem, reload, rollCheck, rollTable for improved formatting and clarity - Actor/creature sheet HTML restructured - Compendium pack DB files rotated (LevelDB log/manifest housekeeping) Co-authored-by: slaguru666 <timevans666@gmail.com>
This commit is contained in:
+126
-10
@@ -141,6 +141,30 @@ function decorateSkillWithTraitPreview(skill, traitRows = []) {
|
||||
return skill;
|
||||
}
|
||||
|
||||
function getPurchasableItemTotalCost(itemData = {}) {
|
||||
const system = itemData?.system ?? itemData?.data ?? {};
|
||||
const unitCost = Number(system.cost ?? 0) || 0;
|
||||
const quantity = Math.max(1, Number(system.quantity ?? 1) || 1);
|
||||
return Math.round(unitCost * quantity * 100) / 100;
|
||||
}
|
||||
|
||||
function isAmmoPurchaseItem(itemData = {}) {
|
||||
const system = itemData?.system ?? itemData?.data ?? {};
|
||||
const sla = system?.sla ?? {};
|
||||
const name = String(itemData?.name ?? "").trim().toLowerCase();
|
||||
const category = String(sla.category ?? "").trim().toLowerCase();
|
||||
const calibre = String(sla.calibre ?? "").trim();
|
||||
const ammoTag = String(sla.ammoTag ?? "").trim();
|
||||
return category === "ammunition" || Boolean((calibre && ammoTag) || name.startsWith("ammo:"));
|
||||
}
|
||||
|
||||
function formatSlaTimestamp() {
|
||||
return new Intl.DateTimeFormat(undefined, {
|
||||
dateStyle: "short",
|
||||
timeStyle: "short"
|
||||
}).format(new Date());
|
||||
}
|
||||
|
||||
const SLA_SPECIES_RULES = {
|
||||
Human: [
|
||||
{ key: "adaptable", label: "Adaptable", summary: "Once per session, reroll one Stat check and take the better result.", optional: false, default: true }
|
||||
@@ -677,19 +701,26 @@ export class MothershipActorSheet extends foundry.appv1.sheets.ActorSheet {
|
||||
//Quantity adjuster
|
||||
html.on('mousedown', '.item-quantity', ev => {
|
||||
const li = ev.currentTarget.closest(".item");
|
||||
//const item = duplicate(this.actor.getEmbeddedDocument("Item", li.dataset.itemId))
|
||||
var item;
|
||||
item = foundry.utils.duplicate(this.actor.getEmbeddedDocument("Item", li.dataset.itemId));
|
||||
|
||||
let amount = item.system.quantity;
|
||||
const sourceItem = this.actor.getEmbeddedDocument("Item", li.dataset.itemId);
|
||||
if (!sourceItem) return;
|
||||
const item = foundry.utils.duplicate(sourceItem);
|
||||
const amount = Math.max(0, Number(item.system?.quantity ?? 0) || 0);
|
||||
let delta = 0;
|
||||
|
||||
if (event.button == 0) {
|
||||
item.system.quantity = Number(amount) + 1;
|
||||
} else if (event.button == 2) {
|
||||
item.system.quantity = Number(amount) - 1;
|
||||
if (ev.button === 0) {
|
||||
delta = 1;
|
||||
} else if (ev.button === 2) {
|
||||
delta = -1;
|
||||
}
|
||||
|
||||
this.actor.updateEmbeddedDocuments('Item', [item]);
|
||||
if (!delta) return;
|
||||
item.system.quantity = Math.max(0, amount + delta);
|
||||
|
||||
this.actor.updateEmbeddedDocuments('Item', [item]).then(async () => {
|
||||
if (delta > 0) {
|
||||
await this._applySlaQuantityIncreaseDebit(sourceItem, delta);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
//Severity adjuster
|
||||
@@ -1197,6 +1228,91 @@ export class MothershipActorSheet extends foundry.appv1.sheets.ActorSheet {
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
async _onDrop(event) {
|
||||
const data = TextEditor.getDragEventData(event);
|
||||
if (data?.type !== "Item") return super._onDrop(event);
|
||||
|
||||
const droppedItem = await Item.implementation.fromDropData(data).catch(() => null);
|
||||
if (!droppedItem) return super._onDrop(event);
|
||||
|
||||
if (droppedItem.parent?.uuid === this.actor.uuid) {
|
||||
return super._onDrop(event);
|
||||
}
|
||||
|
||||
const shouldDebitPurchase = !droppedItem.parent || droppedItem.parent.documentName !== "Actor";
|
||||
const itemData = droppedItem.toObject();
|
||||
delete itemData._id;
|
||||
delete itemData.folder;
|
||||
const created = await this.actor.createEmbeddedDocuments("Item", [itemData], {
|
||||
slaPurchaseDebit: true,
|
||||
slaPurchaseSource: data.uuid ?? droppedItem.uuid ?? ""
|
||||
});
|
||||
if (shouldDebitPurchase) {
|
||||
await this._applySlaPurchaseDebit(created);
|
||||
}
|
||||
return created;
|
||||
}
|
||||
|
||||
async _applySlaPurchaseDebit(createdDocs = []) {
|
||||
const documents = Array.isArray(createdDocs) ? createdDocs : [createdDocs];
|
||||
const total = Math.round(documents.reduce((sum, doc) => sum + getPurchasableItemTotalCost(doc?.toObject?.() ?? doc), 0) * 100) / 100;
|
||||
if (!total) return;
|
||||
|
||||
const currentCredits = Number(this.actor.system?.credits?.value ?? 0) || 0;
|
||||
const newCredits = Math.round((currentCredits - total) * 100) / 100;
|
||||
const updateData = { "system.credits.value": newCredits };
|
||||
|
||||
const itemNames = documents.map((doc) => doc?.name).filter(Boolean).join(", ");
|
||||
const ammoDocs = documents
|
||||
.map((doc) => doc?.toObject?.() ?? doc)
|
||||
.filter((doc) => isAmmoPurchaseItem(doc));
|
||||
if (ammoDocs.length) {
|
||||
const stamp = formatSlaTimestamp();
|
||||
const currentLedger = String(this.actor.system?.sla?.ammoLedger?.value ?? "").trim();
|
||||
const purchaseEntries = ammoDocs.map((doc) => {
|
||||
const system = doc?.system ?? doc?.data ?? {};
|
||||
const quantity = Math.max(1, Number(system.quantity ?? 1) || 1);
|
||||
const unitCost = Number(system.cost ?? 0) || 0;
|
||||
const lineCost = Math.round(unitCost * quantity * 100) / 100;
|
||||
return `[${stamp}] PURCHASE | ${doc?.name ?? "Ammo"} | ${quantity} round${quantity === 1 ? "" : "s"} | -${lineCost} cR | balance ${newCredits} cR`;
|
||||
});
|
||||
const ledgerLines = [...purchaseEntries, ...currentLedger.split("\n").filter(Boolean)].slice(0, 60);
|
||||
updateData["system.sla.ammoLedger.value"] = ledgerLines.join("\n");
|
||||
}
|
||||
|
||||
await this.actor.update(updateData);
|
||||
|
||||
ui.notifications.info(`${this.actor.name}: deducted ${total} cR for ${itemNames || "purchased item"}. Credits now ${newCredits} cR.`);
|
||||
await this.actor.addSlaActivityLog?.(`Purchased ${itemNames || "item"} for ${total} cR. Credits now ${newCredits} cR.`);
|
||||
}
|
||||
|
||||
async _applySlaQuantityIncreaseDebit(itemDoc, increase = 0) {
|
||||
const added = Math.max(0, Number(increase ?? 0) || 0);
|
||||
if (!added || !itemDoc) return;
|
||||
|
||||
const unitCost = Number(itemDoc.system?.cost ?? 0) || 0;
|
||||
const total = Math.round(unitCost * added * 100) / 100;
|
||||
if (!total) return;
|
||||
|
||||
const currentCredits = Number(this.actor.system?.credits?.value ?? 0) || 0;
|
||||
const newCredits = Math.round((currentCredits - total) * 100) / 100;
|
||||
const updateData = { "system.credits.value": newCredits };
|
||||
|
||||
if (isAmmoPurchaseItem(itemDoc)) {
|
||||
const stamp = formatSlaTimestamp();
|
||||
const currentLedger = String(this.actor.system?.sla?.ammoLedger?.value ?? "").trim();
|
||||
const entry = `[${stamp}] PURCHASE | ${itemDoc.name ?? "Ammo"} | ${added} round${added === 1 ? "" : "s"} | -${total} cR | balance ${newCredits} cR`;
|
||||
const ledgerLines = [entry, ...currentLedger.split("\n").filter(Boolean)].slice(0, 60);
|
||||
updateData["system.sla.ammoLedger.value"] = ledgerLines.join("\n");
|
||||
}
|
||||
|
||||
await this.actor.update(updateData);
|
||||
ui.notifications.info(`${this.actor.name}: deducted ${total} cR for ${added} more ${itemDoc.name ?? "item"}. Credits now ${newCredits} cR.`);
|
||||
await this.actor.addSlaActivityLog?.(`Purchased +${added} ${itemDoc.name ?? "item"} for ${total} cR. Credits now ${newCredits} cR.`);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Handle creating a new Owned Item for the actor using initial data defined in the HTML dataset
|
||||
* @param {Event} event The originating click event
|
||||
|
||||
+177
-129
@@ -402,14 +402,6 @@ export class MothershipActor extends Actor {
|
||||
// Prepare Character type specific data
|
||||
_deriveCharacter() {
|
||||
const system = this.system;
|
||||
//clamp stress.value to stress.min so stale stored values never show below the floor
|
||||
if (system.other?.stress) {
|
||||
const stressMin = Number(system.other.stress.min ?? 0);
|
||||
const stressVal = Number(system.other.stress.value ?? 0);
|
||||
if (Number.isFinite(stressMin) && Number.isFinite(stressVal) && stressVal < stressMin) {
|
||||
system.other.stress.value = stressMin;
|
||||
}
|
||||
}
|
||||
//armor points + damage reduction
|
||||
//init vars
|
||||
let armorPoints = 0;
|
||||
@@ -497,44 +489,11 @@ export class MothershipActor extends Actor {
|
||||
}
|
||||
|
||||
async useSlaDrug(itemId) {
|
||||
const result = await SLADrugSystem.useDrug({ actor: this, itemId });
|
||||
if (result?.ok !== false) {
|
||||
const item = this.items.get(itemId);
|
||||
if (item) await this.addSlaActivityLog(`Drug used: ${item.name}.`);
|
||||
}
|
||||
return result;
|
||||
return SLADrugSystem.useDrug({ actor: this, itemId });
|
||||
}
|
||||
|
||||
async closeSlaDrug(itemId = "", drug = "") {
|
||||
const result = await SLADrugSystem.closeDrug({ actor: this, itemId, drug });
|
||||
if (result?.ok !== false) {
|
||||
const drugName = drug || (itemId ? (this.items.get(itemId)?.name ?? "Unknown drug") : "Unknown drug");
|
||||
await this.addSlaActivityLog(`Drug effect ended: ${drugName}.`);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Append a timestamped line to the operative's activity log.
|
||||
* Keeps newest entries at the top and trims to ~10 KB.
|
||||
*/
|
||||
async addSlaActivityLog(entry) {
|
||||
try {
|
||||
const now = new Date();
|
||||
const ts = now.toLocaleTimeString("en-GB", { hour: "2-digit", minute: "2-digit" });
|
||||
const line = `[${ts}] ${entry}`;
|
||||
const current = String(this.system?.sla?.activityLog?.value ?? "");
|
||||
const combined = current ? `${line}\n${current}` : line;
|
||||
// Trim oldest lines until under 10 KB
|
||||
const MAX = 10240;
|
||||
if (combined.length > MAX) {
|
||||
const lines = combined.split("\n");
|
||||
while (lines.join("\n").length > MAX && lines.length > 1) lines.pop();
|
||||
await this.update({ "system.sla.activityLog.value": lines.join("\n") });
|
||||
} else {
|
||||
await this.update({ "system.sla.activityLog.value": combined });
|
||||
}
|
||||
} catch (_) { /* log failures are non-fatal */ }
|
||||
return SLADrugSystem.closeDrug({ actor: this, itemId, drug });
|
||||
}
|
||||
|
||||
//central flavor text library for all chat messages
|
||||
@@ -640,9 +599,123 @@ export class MothershipActor extends Actor {
|
||||
return rollStringParsed;
|
||||
}
|
||||
|
||||
clampSlaRollTarget(rollTarget, rollString = "") {
|
||||
const numericTarget = Number(rollTarget);
|
||||
if (!Number.isFinite(numericTarget)) return rollTarget;
|
||||
const usesD100 = String(rollString ?? "").includes("1d100");
|
||||
if (!usesD100) return numericTarget;
|
||||
return Math.min(98, numericTarget);
|
||||
}
|
||||
|
||||
isSlaAutomaticFailure(total, rollString = "") {
|
||||
const numericTotal = Number(total);
|
||||
if (!Number.isFinite(numericTotal)) return false;
|
||||
const usesD100 = String(rollString ?? "").includes("1d100");
|
||||
return usesD100 && numericTotal === 99;
|
||||
}
|
||||
|
||||
getSlaChatVariant(context = {}) {
|
||||
const actorClass = String(this.system?.class?.value ?? "").trim();
|
||||
const employer = String(this.system?.sla?.employer?.value ?? "").trim();
|
||||
const sector = String(this.system?.sla?.sector?.value ?? "").trim();
|
||||
const skill = String(context?.skill ?? "").trim();
|
||||
const skillCategory = String(context?.skillCategory ?? "").trim();
|
||||
const tableName = String(context?.tableName ?? "").trim();
|
||||
const header = String(context?.msgHeader ?? "").trim();
|
||||
const weaponName = String(context?.weapon?.name ?? "").trim();
|
||||
const itemName = String(context?.item?.name ?? "").trim();
|
||||
const extra = String(context?.extra ?? "").trim();
|
||||
|
||||
const haystack = [
|
||||
actorClass,
|
||||
employer,
|
||||
sector,
|
||||
skill,
|
||||
skillCategory,
|
||||
tableName,
|
||||
header,
|
||||
weaponName,
|
||||
itemName,
|
||||
extra
|
||||
].join(" ").toLowerCase();
|
||||
|
||||
if (context?.isSlaEbbSkill || /\bebb\b|ebon|brain\swaste|flux|telekinesis|blast|awareness/.test(haystack)) {
|
||||
return { key: "ebb", label: "Ebb", emblem: "EBB" };
|
||||
}
|
||||
if (/\bdark\s?night\b/.test(haystack)) {
|
||||
return { key: "darknight", label: "DarkNight", emblem: "DN" };
|
||||
}
|
||||
if (/\bshiver\b/.test(haystack)) {
|
||||
return { key: "shiver", label: "Shiver", emblem: "SV" };
|
||||
}
|
||||
if (/\bmort\b|mortuary|undertaker|corpse|cadaver/.test(haystack)) {
|
||||
return { key: "mort", label: "Mort", emblem: "MT" };
|
||||
}
|
||||
if (/\btek\b|technical|engineer|engineering|medic|medical|science|research/.test(haystack)) {
|
||||
return { key: "tek", label: "Tek", emblem: "TK" };
|
||||
}
|
||||
if (/\bops\b|operations|opscon|ops\scon|operative|squad/.test(haystack)) {
|
||||
return { key: "ops", label: "Ops", emblem: "OP" };
|
||||
}
|
||||
return { key: "sla", label: "SLA", emblem: "SLA" };
|
||||
}
|
||||
|
||||
getSlaSeverityMeta({ severity = null, tableName = "", specialRoll = "", tableResultNumber = null } = {}) {
|
||||
const rawSeverity = Number(severity);
|
||||
if (Number.isFinite(rawSeverity)) {
|
||||
if (rawSeverity >= 5) return { key: "critical", label: "Critical", cssClass: "sla-severity-critical" };
|
||||
if (rawSeverity >= 3) return { key: "severe", label: "Severe", cssClass: "sla-severity-severe" };
|
||||
return { key: "minor", label: "Minor", cssClass: "sla-severity-minor" };
|
||||
}
|
||||
|
||||
const name = String(tableName ?? "").toLowerCase();
|
||||
const special = String(specialRoll ?? "").toLowerCase();
|
||||
const numericResult = Number(tableResultNumber);
|
||||
|
||||
if (!Number.isFinite(numericResult)) {
|
||||
return { key: "minor", label: "Minor", cssClass: "sla-severity-minor" };
|
||||
}
|
||||
|
||||
const isPanic = special === "paniccheck" || name.includes("panic");
|
||||
const isWound = name.includes("wound");
|
||||
|
||||
if (isPanic) {
|
||||
if (numericResult >= 16) return { key: "critical", label: "Catastrophic", cssClass: "sla-severity-critical" };
|
||||
if (numericResult >= 9) return { key: "severe", label: "Severe", cssClass: "sla-severity-severe" };
|
||||
return { key: "minor", label: "Minor", cssClass: "sla-severity-minor" };
|
||||
}
|
||||
|
||||
if (isWound) {
|
||||
if (numericResult >= 8) return { key: "critical", label: "Critical", cssClass: "sla-severity-critical" };
|
||||
if (numericResult >= 4) return { key: "severe", label: "Severe", cssClass: "sla-severity-severe" };
|
||||
return { key: "minor", label: "Minor", cssClass: "sla-severity-minor" };
|
||||
}
|
||||
|
||||
return { key: "minor", label: "Minor", cssClass: "sla-severity-minor" };
|
||||
}
|
||||
|
||||
async addSlaActivityLog(entry, { maxEntries = 40 } = {}) {
|
||||
const text = String(entry ?? "").trim();
|
||||
if (!text || this.type !== "character") return null;
|
||||
|
||||
const timestamp = new Date().toLocaleString("en-GB", {
|
||||
day: "2-digit",
|
||||
month: "2-digit",
|
||||
year: "2-digit",
|
||||
hour: "2-digit",
|
||||
minute: "2-digit"
|
||||
});
|
||||
const currentLog = String(this.system?.sla?.activityLog?.value ?? "").trim();
|
||||
const existingLines = currentLog ? currentLog.split("\n").filter(Boolean) : [];
|
||||
const nextLines = [`[${timestamp}] ${text}`, ...existingLines].slice(0, Math.max(1, maxEntries));
|
||||
await this.update({ "system.sla.activityLog.value": nextLines.join("\n") });
|
||||
return nextLines[0];
|
||||
}
|
||||
|
||||
//central roll parsing function | TAKES '1d100',[Foundry roll object],true,true,41,'<' | RETURNS enriched Foundry roll object
|
||||
parseRollResult(rollString, rollResult, zeroBased, checkCrit, rollTarget, comparison, specialRoll) {
|
||||
//init vars
|
||||
rollTarget = this.clampSlaRollTarget(rollTarget, rollString);
|
||||
let doubles = new Set([0, 11, 22, 33, 44, 55, 66, 77, 88, 99]);
|
||||
let enrichedRollResult = rollResult;
|
||||
let rollFormula = enrichedRollResult.formula;
|
||||
@@ -711,29 +784,29 @@ export class MothershipActor extends Actor {
|
||||
if (rollString.includes("[")) {
|
||||
//set whether each die succeeded
|
||||
//die 0
|
||||
if (comparison === '<' && die0value < rollTarget && die0value < 90) {
|
||||
if (comparison === '<' && die0value < rollTarget && !this.isSlaAutomaticFailure(die0value, rollString)) {
|
||||
die0success = true;
|
||||
}
|
||||
if (comparison === '<=' && die0value <= rollTarget && die0value < 90) {
|
||||
if (comparison === '<=' && die0value <= rollTarget && !this.isSlaAutomaticFailure(die0value, rollString)) {
|
||||
die0success = true;
|
||||
}
|
||||
if (comparison === '>' && die0value > rollTarget && die0value < 90) {
|
||||
if (comparison === '>' && die0value > rollTarget && !this.isSlaAutomaticFailure(die0value, rollString)) {
|
||||
die0success = true;
|
||||
}
|
||||
if (comparison === '>=' && die0value >= rollTarget && die0value < 90) {
|
||||
if (comparison === '>=' && die0value >= rollTarget && !this.isSlaAutomaticFailure(die0value, rollString)) {
|
||||
die0success = true;
|
||||
}
|
||||
//die 1
|
||||
if (comparison === '<' && die1value < rollTarget && die1value < 90) {
|
||||
if (comparison === '<' && die1value < rollTarget && !this.isSlaAutomaticFailure(die1value, rollString)) {
|
||||
die1success = true;
|
||||
}
|
||||
if (comparison === '<=' && die1value <= rollTarget && die1value < 90) {
|
||||
if (comparison === '<=' && die1value <= rollTarget && !this.isSlaAutomaticFailure(die1value, rollString)) {
|
||||
die1success = true;
|
||||
}
|
||||
if (comparison === '>' && die1value > rollTarget && die1value < 90) {
|
||||
if (comparison === '>' && die1value > rollTarget && !this.isSlaAutomaticFailure(die1value, rollString)) {
|
||||
die1success = true;
|
||||
}
|
||||
if (comparison === '>=' && die1value >= rollTarget && die1value < 90) {
|
||||
if (comparison === '>=' && die1value >= rollTarget && !this.isSlaAutomaticFailure(die1value, rollString)) {
|
||||
die1success = true;
|
||||
}
|
||||
//set whether each die are a crit
|
||||
@@ -875,8 +948,8 @@ export class MothershipActor extends Actor {
|
||||
//add data point: detect success/failure
|
||||
if (rollTarget || rollTarget === 0) {
|
||||
//check for auto failure
|
||||
if (enrichedRollResult.total >= 90) {
|
||||
//result >= 90 is a failure
|
||||
if (this.isSlaAutomaticFailure(enrichedRollResult.total, rollString)) {
|
||||
// result 99 on d100 is always a failure
|
||||
enrichedRollResult.success = false;
|
||||
} else {
|
||||
//compare values based on compararison setting
|
||||
@@ -951,7 +1024,7 @@ export class MothershipActor extends Actor {
|
||||
compareIcon = '<i class="fas fa-greater-than-equal"></i>';
|
||||
}
|
||||
//prepare formula
|
||||
if (rollTarget) {
|
||||
if (rollTarget || rollTarget === 0) {
|
||||
//show dice against target
|
||||
diceFormula = rollString + ' ' + compareIcon + ' ' + rollTarget;
|
||||
} else {
|
||||
@@ -1376,6 +1449,12 @@ export class MothershipActor extends Actor {
|
||||
}
|
||||
//generate chat message
|
||||
//prepare data
|
||||
const severityMeta = this.getSlaSeverityMeta({
|
||||
tableName,
|
||||
specialRoll,
|
||||
tableResultNumber
|
||||
});
|
||||
|
||||
let messageData = {
|
||||
actor: this,
|
||||
tableResult: tableResult,
|
||||
@@ -1390,7 +1469,14 @@ export class MothershipActor extends Actor {
|
||||
woundText: woundText,
|
||||
secondRoll: secondRoll,
|
||||
parsedRollResult2: parsedRollResult2,
|
||||
specialRoll: specialRoll
|
||||
specialRoll: specialRoll,
|
||||
severityMeta: severityMeta,
|
||||
chatStyle: this.getSlaChatVariant({
|
||||
tableName,
|
||||
msgHeader: tableName,
|
||||
isSlaEbbSkill: false,
|
||||
extra: specialRoll
|
||||
})
|
||||
};
|
||||
//prepare template
|
||||
messageTemplate = 'systems/sla-mothership/templates/chat/rollTable.html';
|
||||
@@ -1409,6 +1495,7 @@ export class MothershipActor extends Actor {
|
||||
}, {
|
||||
keepId: true
|
||||
});
|
||||
await this.addSlaActivityLog?.(`${tableName}: ${parsedRollResult.success ? "success" : "failure"} on ${rollString}${tableResultNumber !== null ? ` -> ${tableResultNumber}` : ""}.`);
|
||||
if (game.modules.get("dice-so-nice") && game.modules.get("dice-so-nice").active) {
|
||||
//log what was done
|
||||
slaDebug(`Rolled on table ID: ${tableId}, with: rollString:${rollString}, aimFor:${aimFor}, zeroBased:${zeroBased}, checkCrit:${checkCrit}, rollAgainst:${rollAgainst}, comparison:${comparison}`);
|
||||
@@ -1691,7 +1778,7 @@ export class MothershipActor extends Actor {
|
||||
window: {title: dlgTitle},
|
||||
classes: ["macro-popup-dialog"],
|
||||
position: {width: 600},
|
||||
content: skillHeader + `<div class="skill-dialog-scroll">${skillList}</div>` + buttonDesc,
|
||||
content: skillHeader + skillList + buttonDesc,
|
||||
buttons: []
|
||||
};
|
||||
//add adv/normal/dis buttons if we need a rollString
|
||||
@@ -2513,7 +2600,13 @@ export class MothershipActor extends Actor {
|
||||
targetingResult: targetingResult,
|
||||
needsDesc: needsDesc,
|
||||
woundEffect: woundEffect,
|
||||
specialRoll: specialRoll
|
||||
specialRoll: specialRoll,
|
||||
chatStyle: this.getSlaChatVariant({
|
||||
weapon,
|
||||
msgHeader,
|
||||
isSlaEbbSkill: false,
|
||||
extra: ammoContext?.label ?? ""
|
||||
})
|
||||
};
|
||||
let chatData = {
|
||||
user: game.user.id,
|
||||
@@ -2528,6 +2621,7 @@ export class MothershipActor extends Actor {
|
||||
const content = await foundry.applications.handlebars.renderTemplate(template, messageData);
|
||||
chatData.content = content;
|
||||
await ChatMessage.create(chatData);
|
||||
await this.addSlaActivityLog?.(`${weapon.name}: damage roll resolved.${targetingResult ? ` ${targetingResult.targetName} took ${targetingResult.finalDamage} damage.` : ""}`);
|
||||
//log what was done
|
||||
console.log(`Rolled damage on:${weapon.name}`);
|
||||
//return messageData
|
||||
@@ -2545,6 +2639,13 @@ export class MothershipActor extends Actor {
|
||||
} else {
|
||||
rollTarget = rollTargetOverride;
|
||||
}
|
||||
rollTarget = this.clampSlaRollTarget(rollTarget, rollString);
|
||||
if (effectiveRollBreakdown && (String(rollString ?? "").includes("1d100"))) {
|
||||
effectiveRollBreakdown = {
|
||||
...effectiveRollBreakdown,
|
||||
total: rollTarget
|
||||
};
|
||||
}
|
||||
//roll the dice
|
||||
//parse the roll string
|
||||
let parsedRollString = this.parseRollString(rollString, aimFor);
|
||||
@@ -2707,9 +2808,9 @@ export class MothershipActor extends Actor {
|
||||
}
|
||||
if (ammoUsage) {
|
||||
const roundLabel = ammoUsage.shots === 1 ? 'round' : 'rounds';
|
||||
const ammoPrefix = `<strong>${ammoUsage.label}</strong> fire with <strong>${ammoUsage.ammoLabel}</strong> spent <strong>${ammoUsage.shots}</strong> ${roundLabel}. Magazine <strong>${ammoUsage.loaded}/${weapon.system.shots}</strong>. Reserve <strong>${ammoUsage.reserve}</strong>. Ammo cost <strong>${ammoUsage.totalCost}</strong> cR. ${ammoUsage.ammoRule}`;
|
||||
const ammoPrefix = `<strong>${ammoUsage.label}</strong> fire with <strong>${ammoUsage.ammoLabel}</strong> spent <strong>${ammoUsage.shots}</strong> ${roundLabel}. Magazine <strong>${ammoUsage.loaded}/${weapon.system.shots}</strong>. Reserve <strong>${ammoUsage.reserve}</strong>. Firing spend <strong>${ammoUsage.totalCost}</strong> cR. ${ammoUsage.ammoRule}`;
|
||||
const accountingText = ammoAccounting
|
||||
? ` Credits now <strong>${ammoAccounting.newCredits}</strong> cR. Running ammo spend <strong>${ammoAccounting.spendTotal}</strong> cR.`
|
||||
? ` Credits now <strong>${ammoAccounting.newCredits}</strong> cR. Running firing spend <strong>${ammoAccounting.spendTotal}</strong> cR.`
|
||||
: "";
|
||||
flavorText = flavorText ? `${ammoPrefix}${accountingText}<br>${flavorText}` : `${ammoPrefix}${accountingText}`;
|
||||
}
|
||||
@@ -2999,7 +3100,14 @@ export class MothershipActor extends Actor {
|
||||
targetingResult: targetingResult,
|
||||
firstEdition: game.settings.get('sla-mothership', 'firstEdition'),
|
||||
useCalm: game.settings.get('sla-mothership', 'useCalm'),
|
||||
androidPanic: game.settings.get('sla-mothership', 'androidPanic')
|
||||
androidPanic: game.settings.get('sla-mothership', 'androidPanic'),
|
||||
chatStyle: this.getSlaChatVariant({
|
||||
weapon,
|
||||
skill,
|
||||
skillCategory: String(attackContext?.skillCategory ?? "").trim(),
|
||||
msgHeader,
|
||||
isSlaEbbSkill
|
||||
})
|
||||
};
|
||||
//prepare template
|
||||
messageTemplate = 'systems/sla-mothership/templates/chat/rollCheck.html';
|
||||
@@ -3018,6 +3126,11 @@ export class MothershipActor extends Actor {
|
||||
}, {
|
||||
keepId: true
|
||||
});
|
||||
const skillLabel = String(skill ?? "").trim();
|
||||
const actionLabel = weapon
|
||||
? weapon.name
|
||||
: (skillLabel ? `${attributeLabel} / ${skillLabel}` : attributeLabel);
|
||||
await this.addSlaActivityLog?.(`${actionLabel}: ${parsedRollResult.success ? "success" : "failure"} (${parsedRollResult.total} vs ${rollTarget}).`);
|
||||
//is DSN active?
|
||||
if (game.modules.get("dice-so-nice") && game.modules.get("dice-so-nice").active) {
|
||||
//log what was done
|
||||
@@ -3040,18 +3153,6 @@ export class MothershipActor extends Actor {
|
||||
await this.rollTable("panicCheck", null, null, null, null, null, null);
|
||||
}
|
||||
}
|
||||
// Activity log
|
||||
if (this.type === "character" && specialRoll !== "damage") {
|
||||
try {
|
||||
const rollLabel = skill && skill !== "none" ? skill : (attribute ? String(attribute).charAt(0).toUpperCase() + String(attribute).slice(1) : "Roll");
|
||||
const resultNum = Number(parsedRollResult?.total ?? 0);
|
||||
const outcome = parsedRollResult?.success
|
||||
? (parsedRollResult.critical ? "Critical success" : "Pass")
|
||||
: (parsedRollResult?.critical ? "Critical failure" : "Fail");
|
||||
const critTag = parsedRollResult?.critical ? " ★" : "";
|
||||
await this.addSlaActivityLog(`${rollLabel}: rolled ${resultNum} vs ${rollTarget} — ${outcome}${critTag}`);
|
||||
} catch (_) {}
|
||||
}
|
||||
return [messageData];
|
||||
}
|
||||
|
||||
@@ -4812,57 +4913,12 @@ export class MothershipActor extends Actor {
|
||||
|
||||
getSlaSpeciesMinimumWounds() {
|
||||
const species = String(this.system?.sla?.species?.value ?? "").trim();
|
||||
if (["Frother", "Shaktar", "Stormer 313 Malice", "Stormer 711 Xeno", "Stormer Vevaphon"].includes(species)) {
|
||||
if (["Frother", "Shaktar", "Stormer 313 Malice", "Stormer 711 Xeno"].includes(species)) {
|
||||
return 3;
|
||||
}
|
||||
return 2;
|
||||
}
|
||||
|
||||
isSlaVevaphon() {
|
||||
const species = String(this.system?.sla?.species?.value ?? "").trim();
|
||||
return species === "Stormer Vevaphon";
|
||||
}
|
||||
|
||||
getSlaVevaphonInstability() {
|
||||
return Math.max(0, Math.min(12, Number(this.system?.sla?.vevaphonInstability?.value ?? 0) || 0));
|
||||
}
|
||||
|
||||
getSlaVevaphonMorphForm() {
|
||||
return String(this.system?.sla?.morphForm?.value ?? "").trim() || "Brute Form";
|
||||
}
|
||||
|
||||
async setSlaVevaphonMorphForm(formName) {
|
||||
if (!this.isSlaVevaphon()) return null;
|
||||
const validForms = ["Brute Form", "Stalker Form", "Raptor Form"];
|
||||
const resolved = validForms.find((entry) => entry === formName) ?? formName;
|
||||
const currentInstability = this.getSlaVevaphonInstability();
|
||||
const newInstability = Math.min(12, currentInstability + 1);
|
||||
await this.update({
|
||||
"system.sla.morphForm.value": resolved,
|
||||
"system.sla.vevaphonInstability.value": newInstability
|
||||
});
|
||||
const instabilityWarning = newInstability >= 10
|
||||
? " ⚠ Instability critical — Morph Panic risk on failed Sanity save."
|
||||
: newInstability >= 6
|
||||
? " Instability elevated — minor morph effects active."
|
||||
: "";
|
||||
ui.notifications.info(`${this.name} shifts to ${resolved}. Instability: ${newInstability}/12.${instabilityWarning}`);
|
||||
return { morphForm: resolved, instability: newInstability };
|
||||
}
|
||||
|
||||
async adjustSlaVevaphonInstability(delta = 1) {
|
||||
if (!this.isSlaVevaphon()) return null;
|
||||
const current = this.getSlaVevaphonInstability();
|
||||
const next = Math.max(0, Math.min(12, current + delta));
|
||||
await this.update({ "system.sla.vevaphonInstability.value": next });
|
||||
if (next >= 12) {
|
||||
ui.notifications.warn(`${this.name} has reached maximum Instability (12). The Vevaphon is lost — immediate retirement or death.`);
|
||||
} else if (next >= 10) {
|
||||
ui.notifications.warn(`${this.name} — Instability ${next}/12. Morph Panic risk: failed Sanity save triggers a Morph Panic event.`);
|
||||
}
|
||||
return next;
|
||||
}
|
||||
|
||||
async enforceSlaSpeciesWounds() {
|
||||
if (this.type !== "character") return null;
|
||||
const minimumWounds = this.getSlaSpeciesMinimumWounds();
|
||||
@@ -4961,14 +5017,7 @@ export class MothershipActor extends Actor {
|
||||
return this.items
|
||||
.filter((item) => {
|
||||
const name = String(item.name ?? "").trim();
|
||||
if (item.type === "ability") {
|
||||
// Vevaphon morph form abilities are NOT Ebb items — exclude them
|
||||
if (item.system?.sla?.morphForm === true) return false;
|
||||
// Also exclude by name so hand-placed morph abilities are safe even without the flag
|
||||
const morphNames = ["Brute Form", "Stalker Form", "Raptor Form"];
|
||||
if (morphNames.includes(name)) return false;
|
||||
return true;
|
||||
}
|
||||
if (item.type === "ability") return true;
|
||||
if (item.type !== "skill") return false;
|
||||
return name === "Formulate" || name === "Biofeedback" || name === "Ebb (Core)" || name.startsWith("Ebb ");
|
||||
})
|
||||
@@ -5344,8 +5393,6 @@ export class MothershipActor extends Actor {
|
||||
if (shouldPanic) {
|
||||
await this.rollTable("panicCheck", null, null, null, null, null, null);
|
||||
}
|
||||
const saveOutcome = saveResult ? (saveResult.success ? "Pass" : "Fail") : "n/a";
|
||||
await this.addSlaActivityLog(`Flux ${forcePanic ? "Panic forced" : "Save"} — Fear save ${saveOutcome}. Flux ${this.system?.sla?.flux?.value ?? 0}/${this.system?.sla?.flux?.max ?? 0}.${shouldPanic ? " Panic triggered." : ""}`);
|
||||
return { saveResult, state, panicTriggered: shouldPanic };
|
||||
}
|
||||
|
||||
@@ -5531,7 +5578,8 @@ export class MothershipActor extends Actor {
|
||||
insert: rollInsert,
|
||||
onlyDesc: true,
|
||||
swapNameDesc,
|
||||
swapName
|
||||
swapName,
|
||||
severityMeta: this.getSlaSeverityMeta({ severity: item?.system?.severity })
|
||||
};
|
||||
|
||||
const chatData = {
|
||||
|
||||
@@ -1,5 +1,22 @@
|
||||
import { SLADrugSystem } from "../sla-drug-system.js";
|
||||
|
||||
function isAmmoPurchaseItem(itemData = {}) {
|
||||
const system = itemData?.system ?? {};
|
||||
const sla = system?.sla ?? {};
|
||||
const name = String(itemData?.name ?? "").trim().toLowerCase();
|
||||
const category = String(sla.category ?? "").trim().toLowerCase();
|
||||
const calibre = String(sla.calibre ?? "").trim();
|
||||
const ammoTag = String(sla.ammoTag ?? "").trim();
|
||||
return category === "ammunition" || Boolean((calibre && ammoTag) || name.startsWith("ammo:"));
|
||||
}
|
||||
|
||||
function formatSlaTimestamp() {
|
||||
return new Intl.DateTimeFormat(undefined, {
|
||||
dateStyle: "short",
|
||||
timeStyle: "short"
|
||||
}).format(new Date());
|
||||
}
|
||||
|
||||
/**
|
||||
* Extend the basic ItemSheet with some very simple modifications
|
||||
* @extends {ItemSheet}
|
||||
@@ -128,11 +145,38 @@ export class MothershipItemSheet extends foundry.appv1.sheets.ItemSheet {
|
||||
|
||||
async _updateObject(event, formData) {
|
||||
const updateData = foundry.utils.expandObject(formData);
|
||||
const currentQty = Math.max(0, Number(this.item.system?.quantity ?? 0) || 0);
|
||||
if (Object.prototype.hasOwnProperty.call(updateData?.system ?? {}, "weight")) {
|
||||
const parsed = Number(updateData.system.weight ?? 0) || 0;
|
||||
updateData.system.weight = Math.round(parsed * 10) / 10;
|
||||
}
|
||||
await this.item.update(updateData, { diff: false });
|
||||
|
||||
const nextQtyRaw = updateData?.system?.quantity;
|
||||
const nextQty = nextQtyRaw === undefined ? currentQty : Math.max(0, Number(nextQtyRaw ?? 0) || 0);
|
||||
const increase = Math.max(0, nextQty - currentQty);
|
||||
const actor = this.item.parent;
|
||||
if (!increase || actor?.documentName !== "Actor") return;
|
||||
|
||||
const unitCost = Number(updateData?.system?.cost ?? this.item.system?.cost ?? 0) || 0;
|
||||
const total = Math.round(unitCost * increase * 100) / 100;
|
||||
if (!total) return;
|
||||
|
||||
const currentCredits = Number(actor.system?.credits?.value ?? 0) || 0;
|
||||
const newCredits = Math.round((currentCredits - total) * 100) / 100;
|
||||
const updateActorData = { "system.credits.value": newCredits };
|
||||
|
||||
if (isAmmoPurchaseItem(this.item.toObject())) {
|
||||
const stamp = formatSlaTimestamp();
|
||||
const currentLedger = String(actor.system?.sla?.ammoLedger?.value ?? "").trim();
|
||||
const entry = `[${stamp}] PURCHASE | ${this.item.name ?? "Ammo"} | ${increase} round${increase === 1 ? "" : "s"} | -${total} cR | balance ${newCredits} cR`;
|
||||
const ledgerLines = [entry, ...currentLedger.split("\n").filter(Boolean)].slice(0, 60);
|
||||
updateActorData["system.sla.ammoLedger.value"] = ledgerLines.join("\n");
|
||||
}
|
||||
|
||||
await actor.update(updateActorData);
|
||||
ui.notifications.info(`${actor.name}: deducted ${total} cR for ${increase} more ${this.item.name ?? "item"}. Credits now ${newCredits} cR.`);
|
||||
await actor.addSlaActivityLog?.(`Purchased +${increase} ${this.item.name ?? "item"} for ${total} cR. Credits now ${newCredits} cR.`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+170
-84
@@ -175,16 +175,37 @@ Hooks.once('init', async function () {
|
||||
|
||||
Hooks.on("renderActorDirectory", (app, html) => {
|
||||
installSlaSidebarButtons(app, html);
|
||||
if (!game.user?.isGM) enforceSlaPlayerDirectoryRestrictions("actors", html);
|
||||
});
|
||||
|
||||
Hooks.on("renderSidebarTab", (app, html) => {
|
||||
const isActorsTab = app?.tabName === "actors" || app?.options?.id === "actors";
|
||||
if (!isActorsTab) return;
|
||||
installSlaSidebarButtons(app, html);
|
||||
if (isActorsTab) installSlaSidebarButtons(app, html);
|
||||
if (!game.user?.isGM) {
|
||||
hideSlaRestrictedSidebarTabs();
|
||||
enforceSlaPlayerSidebarTabRestrictions(app, html);
|
||||
}
|
||||
});
|
||||
|
||||
Hooks.on("renderSceneDirectory", (_app, html) => {
|
||||
if (!game.user?.isGM) enforceSlaPlayerDirectoryRestrictions("scenes", html);
|
||||
});
|
||||
|
||||
Hooks.on("renderJournalDirectory", (_app, html) => {
|
||||
if (!game.user?.isGM) enforceSlaPlayerDirectoryRestrictions("journal", html);
|
||||
});
|
||||
|
||||
Hooks.on("renderRollTableDirectory", (_app, html) => {
|
||||
if (!game.user?.isGM) enforceSlaPlayerDirectoryRestrictions("tables", html);
|
||||
});
|
||||
|
||||
Hooks.on("renderCompendiumDirectory", (_app, html) => {
|
||||
if (!game.user?.isGM) enforceSlaPlayerDirectoryRestrictions("compendium", html);
|
||||
});
|
||||
|
||||
Hooks.on("renderCombatTracker", (app, html) => {
|
||||
installSlaCombatTrackerControls(app, html);
|
||||
if (!game.user?.isGM) enforceSlaPlayerDirectoryRestrictions("combat", html);
|
||||
});
|
||||
|
||||
Hooks.on("updateCombat", async (combat, changed) => {
|
||||
@@ -247,6 +268,7 @@ Hooks.once("ready", async function () {
|
||||
}
|
||||
|
||||
patchSlaInitiativeSystem();
|
||||
if (!game.user?.isGM) hideSlaRestrictedSidebarTabs();
|
||||
|
||||
});
|
||||
|
||||
@@ -1254,33 +1276,19 @@ export function formatCreditsNumber(num) {
|
||||
// SLA Industries Pause Screen Animation
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
let slaPauseRefreshHandle = null;
|
||||
|
||||
function injectSlaPauseAnimation() {
|
||||
const pauseEl = document.querySelector('#pause');
|
||||
if (!pauseEl) return;
|
||||
|
||||
// Don't inject twice
|
||||
if (pauseEl.querySelector('.sla-pause-root')) return;
|
||||
|
||||
// Aggressively hide ALL of Foundry's default pause content
|
||||
// Hide every direct child that isn't our injected element
|
||||
Array.from(pauseEl.children).forEach(child => {
|
||||
child.style.setProperty('display', 'none', 'important');
|
||||
});
|
||||
// Also kill any descendant with animation or image content
|
||||
pauseEl.querySelectorAll('figure, img, svg, h3, p, .fas, .fa-cog, canvas').forEach(el => {
|
||||
el.style.setProperty('display', 'none', 'important');
|
||||
});
|
||||
document.body?.classList.add("sla-pause-active");
|
||||
if (document.querySelector('.sla-pause-root')) {
|
||||
return;
|
||||
}
|
||||
|
||||
const svgHTML = `
|
||||
<div class="sla-pause-root" aria-label="SLA Industries – Game Paused">
|
||||
<svg
|
||||
class="sla-pause-svg"
|
||||
viewBox="-120 -120 240 260"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
role="img"
|
||||
>
|
||||
<div class="sla-pause-stage">
|
||||
<svg class="sla-pause-svg" viewBox="-120 -120 240 260" xmlns="http://www.w3.org/2000/svg" role="img">
|
||||
<defs>
|
||||
<!-- Red glow filter for the logo and satellites -->
|
||||
<filter id="sla-glow-red" x="-50%" y="-50%" width="200%" height="200%">
|
||||
<feGaussianBlur stdDeviation="3.5" result="blur"/>
|
||||
<feMerge>
|
||||
@@ -1288,7 +1296,6 @@ function injectSlaPauseAnimation() {
|
||||
<feMergeNode in="SourceGraphic"/>
|
||||
</feMerge>
|
||||
</filter>
|
||||
<!-- Subtle glow for rings -->
|
||||
<filter id="sla-glow-ring" x="-30%" y="-30%" width="160%" height="160%">
|
||||
<feGaussianBlur stdDeviation="1.5" result="blur"/>
|
||||
<feMerge>
|
||||
@@ -1296,23 +1303,17 @@ function injectSlaPauseAnimation() {
|
||||
<feMergeNode in="SourceGraphic"/>
|
||||
</feMerge>
|
||||
</filter>
|
||||
<!-- Radial gradient for background disc -->
|
||||
<radialGradient id="sla-bg-grad" cx="50%" cy="50%" r="50%">
|
||||
<stop offset="0%" stop-color="#1a0000" stop-opacity="0.95"/>
|
||||
<stop offset="100%" stop-color="#000000" stop-opacity="0.0"/>
|
||||
</radialGradient>
|
||||
<!-- Clip for the inner content -->
|
||||
<clipPath id="sla-clip-outer">
|
||||
<circle cx="0" cy="0" r="108"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
|
||||
<!-- Ambient background disc -->
|
||||
<circle cx="0" cy="0" r="108" fill="url(#sla-bg-grad)"/>
|
||||
|
||||
<!-- ── Outer ring (clockwise) ── -->
|
||||
<circle cx="0" cy="0" r="108" fill="url(#sla-bg-grad)" class="sla-pause-core-glow"/>
|
||||
<g class="sla-ring-outer">
|
||||
<!-- Dashed orbit path -->
|
||||
<animateTransform attributeName="transform" type="rotate" from="0 0 0" to="360 0 0" dur="18s" repeatCount="indefinite"/>
|
||||
<circle cx="0" cy="0" r="90"
|
||||
fill="none"
|
||||
stroke="#8b1a1a"
|
||||
@@ -1321,11 +1322,9 @@ function injectSlaPauseAnimation() {
|
||||
opacity="0.7"
|
||||
filter="url(#sla-glow-ring)"
|
||||
/>
|
||||
<!-- 3 red satellite dots at 0°, 120°, 240° -->
|
||||
<circle cx="0" cy="-90" r="5" fill="#cc2222" filter="url(#sla-glow-red)" class="sla-sat"/>
|
||||
<circle cx="77.9" cy="45" r="5" fill="#cc2222" filter="url(#sla-glow-red)" class="sla-sat"/>
|
||||
<circle cx="-77.9" cy="45" r="5" fill="#cc2222" filter="url(#sla-glow-red)" class="sla-sat"/>
|
||||
<!-- Thin tick marks every 30° on outer ring -->
|
||||
<line x1="0" y1="-86" x2="0" y2="-94" stroke="#8b1a1a" stroke-width="1" transform="rotate(0)"/>
|
||||
<line x1="0" y1="-86" x2="0" y2="-94" stroke="#8b1a1a" stroke-width="1" transform="rotate(30)"/>
|
||||
<line x1="0" y1="-86" x2="0" y2="-94" stroke="#8b1a1a" stroke-width="1" transform="rotate(60)"/>
|
||||
@@ -1339,9 +1338,8 @@ function injectSlaPauseAnimation() {
|
||||
<line x1="0" y1="-86" x2="0" y2="-94" stroke="#8b1a1a" stroke-width="1" transform="rotate(300)"/>
|
||||
<line x1="0" y1="-86" x2="0" y2="-94" stroke="#8b1a1a" stroke-width="1" transform="rotate(330)"/>
|
||||
</g>
|
||||
|
||||
<!-- ── Inner ring (counter-clockwise) ── -->
|
||||
<g class="sla-ring-inner">
|
||||
<animateTransform attributeName="transform" type="rotate" from="360 0 0" to="0 0 0" dur="13s" repeatCount="indefinite"/>
|
||||
<circle cx="0" cy="0" r="62"
|
||||
fill="none"
|
||||
stroke="#6b1010"
|
||||
@@ -1350,15 +1348,11 @@ function injectSlaPauseAnimation() {
|
||||
opacity="0.6"
|
||||
filter="url(#sla-glow-ring)"
|
||||
/>
|
||||
<!-- 3 smaller orange-red dots at 60°, 180°, 300° offset for variety -->
|
||||
<circle cx="53.7" cy="-31" r="3.5" fill="#dd4422" filter="url(#sla-glow-red)" class="sla-sat-inner"/>
|
||||
<circle cx="-53.7" cy="-31" r="3.5" fill="#dd4422" filter="url(#sla-glow-red)" class="sla-sat-inner"/>
|
||||
<circle cx="0" cy="62" r="3.5" fill="#dd4422" filter="url(#sla-glow-red)" class="sla-sat-inner"/>
|
||||
</g>
|
||||
|
||||
<!-- ── Central "SLA" logotype ── -->
|
||||
<g filter="url(#sla-glow-red)">
|
||||
<!-- Hexagonal backing plate -->
|
||||
<g filter="url(#sla-glow-red)" class="sla-pause-logo-core">
|
||||
<polygon
|
||||
points="0,-38 33,-19 33,19 0,38 -33,19 -33,-19"
|
||||
fill="#1a0000"
|
||||
@@ -1366,7 +1360,6 @@ function injectSlaPauseAnimation() {
|
||||
stroke-width="1.2"
|
||||
opacity="0.9"
|
||||
/>
|
||||
<!-- SLA text -->
|
||||
<text
|
||||
x="0" y="14"
|
||||
text-anchor="middle"
|
||||
@@ -1378,35 +1371,27 @@ function injectSlaPauseAnimation() {
|
||||
class="sla-pause-logo-text"
|
||||
>SLA</text>
|
||||
</g>
|
||||
|
||||
<!-- ── Thin separator lines ── -->
|
||||
<line x1="-70" y1="48" x2="70" y2="48" stroke="#8b1a1a" stroke-width="0.5" opacity="0.7"/>
|
||||
|
||||
<!-- ── "STAND BY" label ── -->
|
||||
<text
|
||||
x="0" y="70"
|
||||
x="0" y="68"
|
||||
text-anchor="middle"
|
||||
font-family="'Oswald', 'Courier New', monospace"
|
||||
font-size="13"
|
||||
font-weight="400"
|
||||
letter-spacing="5"
|
||||
fill="#cc4444"
|
||||
font-size="11.5"
|
||||
font-weight="500"
|
||||
letter-spacing="3.2"
|
||||
fill="#ff7b7b"
|
||||
class="sla-pause-standby"
|
||||
>STAND BY</text>
|
||||
|
||||
<!-- ── "SLA INDUSTRIES" footer text ── -->
|
||||
>PLEASE STAND BY</text>
|
||||
<text
|
||||
x="0" y="95"
|
||||
x="0" y="94"
|
||||
text-anchor="middle"
|
||||
font-family="'Oswald', 'Arial Narrow', sans-serif"
|
||||
font-size="7"
|
||||
font-size="8"
|
||||
font-weight="300"
|
||||
letter-spacing="3"
|
||||
fill="#661111"
|
||||
opacity="0.85"
|
||||
fill="#b13b3b"
|
||||
opacity="0.92"
|
||||
>SLA INDUSTRIES</text>
|
||||
|
||||
<!-- ── Scanning line sweep ── -->
|
||||
<line
|
||||
x1="0" y1="-90" x2="0" y2="0"
|
||||
stroke="#ff2222"
|
||||
@@ -1415,37 +1400,138 @@ function injectSlaPauseAnimation() {
|
||||
class="sla-pause-scan"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>`;
|
||||
document.body?.insertAdjacentHTML('beforeend', svgHTML);
|
||||
}
|
||||
|
||||
// Inject directly into the pause element (not into figure, which we hid)
|
||||
pauseEl.insertAdjacentHTML('beforeend', svgHTML);
|
||||
function removeSlaPauseAnimation() {
|
||||
document.querySelector(".sla-pause-root")?.remove();
|
||||
document.body?.classList.remove("sla-pause-active");
|
||||
}
|
||||
|
||||
function queueSlaPauseRefresh(delay = 0) {
|
||||
window.clearTimeout(slaPauseRefreshHandle);
|
||||
slaPauseRefreshHandle = window.setTimeout(() => {
|
||||
if (!game?.paused) return;
|
||||
injectSlaPauseAnimation();
|
||||
}, delay);
|
||||
}
|
||||
|
||||
Hooks.once('ready', () => {
|
||||
// Inject if game is already paused on load
|
||||
if (game.paused) injectSlaPauseAnimation();
|
||||
});
|
||||
|
||||
// Use the pauseGame hook exclusively — don't use a MutationObserver
|
||||
// which could fight Foundry's own show/hide logic on the #pause element.
|
||||
Hooks.on('pauseGame', (paused) => {
|
||||
if (paused) {
|
||||
// Two-frame delay — Foundry renders the pause figure asynchronously
|
||||
requestAnimationFrame(() => requestAnimationFrame(() => injectSlaPauseAnimation()));
|
||||
} else {
|
||||
// On unpause: remove our injected content so it can be re-injected cleanly next time
|
||||
const slaRoot = document.querySelector('#pause .sla-pause-root');
|
||||
if (slaRoot) slaRoot.remove();
|
||||
// Restore Foundry's original pause children (they were hidden inline, unhide them)
|
||||
const pauseEl = document.querySelector('#pause');
|
||||
if (pauseEl) {
|
||||
pauseEl.querySelectorAll(':scope > *:not(.sla-pause-root)').forEach(child => {
|
||||
child.style.removeProperty('display');
|
||||
});
|
||||
}
|
||||
if (game.paused) {
|
||||
queueSlaPauseRefresh(0);
|
||||
queueSlaPauseRefresh(60);
|
||||
queueSlaPauseRefresh(180);
|
||||
}
|
||||
});
|
||||
|
||||
Hooks.on('pauseGame', (paused) => {
|
||||
if (paused) {
|
||||
requestAnimationFrame(() => requestAnimationFrame(() => injectSlaPauseAnimation()));
|
||||
queueSlaPauseRefresh(0);
|
||||
queueSlaPauseRefresh(60);
|
||||
queueSlaPauseRefresh(180);
|
||||
queueSlaPauseRefresh(360);
|
||||
} else {
|
||||
removeSlaPauseAnimation();
|
||||
}
|
||||
});
|
||||
|
||||
function getSlaHtmlRoot(html) {
|
||||
return html instanceof HTMLElement ? html : html?.[0] ?? null;
|
||||
}
|
||||
|
||||
function hideSlaRestrictedSidebarTabs() {
|
||||
[
|
||||
"#sidebar-tabs [data-tab='tables']",
|
||||
"#sidebar-tabs [aria-controls='tables']",
|
||||
"#sidebar-tabs [data-tab='compendium']",
|
||||
"#sidebar-tabs [aria-controls='compendium']"
|
||||
].forEach((selector) => {
|
||||
document.querySelectorAll(selector).forEach((el) => {
|
||||
el.closest("li")?.remove();
|
||||
if (el instanceof HTMLElement) el.remove();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function removeSlaElements(root, selectors = []) {
|
||||
selectors.forEach((selector) => {
|
||||
root.querySelectorAll(selector).forEach((el) => el.remove());
|
||||
});
|
||||
}
|
||||
|
||||
function pruneEmptySlaFolders(root) {
|
||||
const folders = Array.from(root.querySelectorAll(".directory-item.folder"));
|
||||
folders.reverse().forEach((folder) => {
|
||||
const visibleDocuments = Array.from(folder.querySelectorAll(".directory-item.document, li.document")).filter((node) => node.offsetParent !== null);
|
||||
const visibleFolders = Array.from(folder.querySelectorAll(":scope > .directory-list > .directory-item.folder, :scope > ol > li.folder")).filter((node) => node.offsetParent !== null);
|
||||
if (!visibleDocuments.length && !visibleFolders.length) folder.remove();
|
||||
});
|
||||
}
|
||||
|
||||
function shouldShowSlaPlayerJournal(entry) {
|
||||
if (!entry) return false;
|
||||
const name = String(entry.name ?? "").trim();
|
||||
if (/^operative handbook$/i.test(name)) return true;
|
||||
if (entry.getFlag?.("sla-mothership", "playerVisible") === true) return true;
|
||||
return entry.testUserPermission?.(game.user, CONST.DOCUMENT_OWNERSHIP_LEVELS.OWNER) ?? false;
|
||||
}
|
||||
|
||||
function filterSlaJournalDirectory(root) {
|
||||
const rows = Array.from(root.querySelectorAll("[data-entry-id], [data-document-id]"));
|
||||
rows.forEach((row) => {
|
||||
const id = row.dataset.entryId ?? row.dataset.documentId;
|
||||
if (!id) return;
|
||||
const entry = game.journal?.get?.(id);
|
||||
if (!shouldShowSlaPlayerJournal(entry)) row.remove();
|
||||
});
|
||||
pruneEmptySlaFolders(root);
|
||||
}
|
||||
|
||||
function enforceSlaPlayerDirectoryRestrictions(kind, html) {
|
||||
const root = getSlaHtmlRoot(html);
|
||||
if (!root || game.user?.isGM) return;
|
||||
|
||||
removeSlaElements(root, [
|
||||
".directory-header .header-actions",
|
||||
".directory-header .action-buttons",
|
||||
".sla-sidebar-actions",
|
||||
".qbm-scene-directory-actions",
|
||||
".qbm-launch",
|
||||
".sla-world-tools-launch",
|
||||
".sla-create-operative",
|
||||
".sla-create-random-operative",
|
||||
".sla-refresh-content",
|
||||
".sla-gear-cache-open",
|
||||
".sla-briefing-desk-open",
|
||||
".sla-bpn-dispatch-open",
|
||||
".sla-npc-director-open",
|
||||
".sla-npc-director-actor-open",
|
||||
".sla-ops-clock-open",
|
||||
".npp-generate-sidebar-btn"
|
||||
]);
|
||||
|
||||
if (kind === "journal") {
|
||||
filterSlaJournalDirectory(root);
|
||||
}
|
||||
|
||||
if (kind === "tables" || kind === "compendium") {
|
||||
root.innerHTML = "";
|
||||
}
|
||||
}
|
||||
|
||||
function enforceSlaPlayerSidebarTabRestrictions(app, html) {
|
||||
const tabName = app?.tabName ?? app?.options?.id ?? "";
|
||||
if (tabName === "actors") return enforceSlaPlayerDirectoryRestrictions("actors", html);
|
||||
if (tabName === "scenes") return enforceSlaPlayerDirectoryRestrictions("scenes", html);
|
||||
if (tabName === "journal") return enforceSlaPlayerDirectoryRestrictions("journal", html);
|
||||
if (tabName === "tables") return enforceSlaPlayerDirectoryRestrictions("tables", html);
|
||||
if (tabName === "compendium") return enforceSlaPlayerDirectoryRestrictions("compendium", html);
|
||||
if (tabName === "combat") return enforceSlaPlayerDirectoryRestrictions("combat", html);
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// SLA Industries — UI Icon Injection
|
||||
// Replaces Foundry's Font Awesome icons in the sidebar and scene controls
|
||||
|
||||
Reference in New Issue
Block a user