diff --git a/styles/zero-engine.css b/styles/zero-engine.css index 0e183c8..d521e04 100644 --- a/styles/zero-engine.css +++ b/styles/zero-engine.css @@ -3973,15 +3973,47 @@ box-shadow: inset 0 0 12px rgba(220,80,0,0.08); } +/* ── LAD Account locked expense row ─────────────────────── */ +.zero-engine .lad-expense-row label { + color: #888; font-style: italic; +} +.zero-engine .fin-amount-locked input { + background: #1a1a1a; color: #666; cursor: not-allowed; + border-color: #333; opacity: 0.7; +} +.zero-engine .fin-amount-locked span { + color: #666; +} + +/* ── PC Status button (GM only) ─────────────────────────── */ +.zero-engine .pc-status-btn { + display: block; width: 100%; margin-top: 4px; + background: rgba(80,120,180,0.2); border: 1px solid rgba(80,120,200,0.4); + color: #88aadd; border-radius: 3px; padding: 3px 6px; + font-size: 10px; cursor: pointer; text-transform: uppercase; letter-spacing: 0.5px; +} +.zero-engine .pc-status-btn:hover { + background: rgba(80,120,180,0.4); color: #aaccff; +} + /* ── Active Conditions Row ──────────────────────────────── */ .zero-engine .active-conditions-row { display: flex; flex-wrap: wrap; gap: 5px; margin-top: 6px; padding: 5px 0; } .zero-engine .condition-tag { - display: flex; align-items: center; gap: 4px; + display: flex; align-items: center; gap: 6px; background: rgba(200,80,0,0.15); border: 1px solid rgba(200,100,0,0.4); - border-radius: 3px; padding: 3px 7px; font-size: 11px; color: #e0a060; - font-family: "SLA Borg UI", sans-serif; text-transform: uppercase; letter-spacing: 0.5px; + border-radius: 3px; padding: 4px 8px; font-size: 11px; color: #e0a060; + font-family: "SLA Borg UI", sans-serif; +} +.zero-engine .condition-tag-body { + display: flex; flex-direction: column; gap: 1px; +} +.zero-engine .condition-label { + text-transform: uppercase; letter-spacing: 0.5px; font-weight: bold; +} +.zero-engine .condition-penalty { + font-size: 10px; opacity: 0.85; font-style: italic; text-transform: none; letter-spacing: 0; } .zero-engine .condition-tag.sla-bleeding { border-color: #aa2222; background: rgba(160,20,20,0.2); color: #ff6666; } .zero-engine .condition-tag.sla-stunned { border-color: #aaaa22; background: rgba(140,140,20,0.18); color: #ffff88; } diff --git a/system.json b/system.json index 2a407af..a7a4f47 100644 --- a/system.json +++ b/system.json @@ -2,7 +2,7 @@ "id": "zero-engine", "title": "Zero Engine (SLA Industries)", "description": "A basic game system for SLA Industries using the Zero Engine rules.", - "version": "1.5.8", + "version": "1.5.9", "compatibility": { "minimum": 13, "verified": "14.359" diff --git a/template.json b/template.json index 2096463..2e0506c 100644 --- a/template.json +++ b/template.json @@ -12,7 +12,8 @@ "weight": "", "scl": 10, "credits": 500, - "isPlayerCharacter": false + "isPlayerCharacter": false, + "ladAccount": false }, "race": [], "training": [], diff --git a/templates/actor/character-sheet.hbs b/templates/actor/character-sheet.hbs index 7d97f22..13159f1 100644 --- a/templates/actor/character-sheet.hbs +++ b/templates/actor/character-sheet.hbs @@ -138,6 +138,11 @@ + {{#if isGM}} + + {{/if}} {{#if isEbbUser}}
@@ -213,9 +218,12 @@ {{#if activeConditions.length}}
{{#each activeConditions}} -
- - {{this.label}} +
+
+ + {{this.label}} + {{this.description}} +
{{/each}} @@ -890,6 +898,12 @@
c
c
c
+ {{#if system.details.ladAccount}} +
+ +
c
+
+ {{/if}}
c
{{system.finances.expenses.bulletTax}}c
c
@@ -965,6 +979,13 @@ When ticked, this actor appears in the GM Finance Ledger.
+
+ + Life After Death media subscription — costs 50c per week (auto-added to Finances). +

Background

diff --git a/zero-engine.mjs b/zero-engine.mjs index d7caa4d..54d20b3 100644 --- a/zero-engine.mjs +++ b/zero-engine.mjs @@ -297,6 +297,8 @@ class ZeroEngineActorSheet extends foundry.appv1.sheets.ActorSheet { context.equippedArmorName = context.armors[0]?.name || null; // Finances summary (used by both Mk1 and Mk2 sheets) + context.isGM = game.user?.isGM ?? false; + const ladCost = (this.actor.system.details?.ladAccount === true) ? 50 : 0; if (!this.actor.system.finances) { context.weeklyIncome = 0; context.weeklyExpenses = 0; context.weeklyNet = 0; context.weeklyNetSign = '+'; context.weeklyNetColor = '#44cc66'; @@ -304,7 +306,7 @@ class ZeroEngineActorSheet extends foundry.appv1.sheets.ActorSheet { const _inc = this.actor.system.finances?.income || {}; const _exp = this.actor.system.finances?.expenses || {}; context.weeklyIncome = (_inc.salary||0) + (_inc.bpnReward||0) + (_inc.other||0); - context.weeklyExpenses = (_exp.accommodation||0) + (_exp.drugs||0) + (_exp.subscriptions||0) + (_exp.other||0) + (_exp.bulletTax||0); + context.weeklyExpenses = (_exp.accommodation||0) + (_exp.drugs||0) + (_exp.subscriptions||0) + (_exp.other||0) + (_exp.bulletTax||0) + ladCost; context.weeklyNet = context.weeklyIncome - context.weeklyExpenses; context.weeklyNetSign = context.weeklyNet >= 0 ? '+' : ''; context.weeklyNetColor = context.weeklyNet >= 0 ? '#44cc66' : '#cc1111'; @@ -458,6 +460,9 @@ class ZeroEngineActorSheet extends foundry.appv1.sheets.ActorSheet { // Health / Resolve save rolls — click the vital label name html.find('.vital-save-label').click(this._onVitalSave.bind(this)); + // GM PC condition status report + html.find('.pc-status-btn').click(ev => { ev.preventDefault(); _broadcastPCConditions(); }); + // Delete injury buttons html.find('.delete-injury-btn').click(this._onDeleteInjury.bind(this)); @@ -663,9 +668,14 @@ class ZeroEngineActorSheet extends foundry.appv1.sheets.ActorSheet { if (!condId) return; try { await this.actor.toggleStatusEffect(condId, { active: false }); - ui.notifications.info(`Condition cleared: ${condId.replace('sla-','')} on ${this.actor.name}`); } catch(e) { - console.warn('Zero Engine | Could not clear condition:', e); + // Fallback: find and delete the matching ActiveEffect directly + console.warn(`Zero Engine | toggleStatusEffect(false) failed for "${condId}", trying direct delete:`, e); + const effect = this.actor.effects.find(ef => ef.statuses?.has(condId)); + if (effect) await effect.delete(); + } + if (!this.actor.statuses?.has(condId)) { + ui.notifications.info(`Condition cleared: ${condId.replace('sla-','')} on ${this.actor.name}`); } }); @@ -1832,10 +1842,8 @@ class ZeroEngineActorSheet extends foundry.appv1.sheets.ActorSheet { notes.push('Reverted to base form'); } if (entry.applyCondition) { - try { - await actor.toggleStatusEffect(entry.applyCondition, { active: true }); - notes.push(`Condition applied: ${entry.applyCondition.replace('sla-','')}`); - } catch(_) {} + await _applyStatusCondition(actor, entry.applyCondition); + notes.push(`Condition applied: ${entry.applyCondition.replace('sla-','')}`); } if (Object.keys(updates).length > 0) { @@ -2810,16 +2818,14 @@ class ZeroEngineActorSheet extends foundry.appv1.sheets.ActorSheet { }); // Apply condition based on panic severity - try { - const panicLevel = total || 0; - if (panicLevel >= 10) { - await actor.toggleStatusEffect('sla-panicking', { active: true }); - ui.notifications.warn(`${actor.name} is PANICKING! (-2 dice to all rolls)`); - } else if (panicLevel >= 7) { - await actor.toggleStatusEffect('sla-shaken', { active: true }); - ui.notifications.warn(`${actor.name} is SHAKEN! (-1 die to all rolls)`); - } - } catch(_) {} + const panicLevel = total || 0; + if (panicLevel >= 10) { + await _applyStatusCondition(actor, 'sla-panicking', 'Panicking', 'icons/svg/terror.svg'); + ui.notifications.warn(`${actor.name} is PANICKING! (-2 dice to all rolls)`); + } else if (panicLevel >= 7) { + await _applyStatusCondition(actor, 'sla-shaken', 'Shaken', 'icons/svg/terror.svg'); + ui.notifications.warn(`${actor.name} is SHAKEN! (-1 die to all rolls)`); + } if (total > 6) { const panicEffect = this._getPanicEffect(total); @@ -3279,10 +3285,7 @@ class ZeroEngineActorSheet extends foundry.appv1.sheets.ActorSheet { }; const mCondIds = MENTAL_CRIT_CONDITIONS[key] || []; for (const condId of mCondIds) { - try { - const already = actor.statuses?.has(condId); - if (!already) await actor.toggleStatusEffect(condId, { active: true }); - } catch(_) {} + await _applyStatusCondition(actor, condId); } // If instant death @@ -3392,10 +3395,7 @@ class ZeroEngineActorSheet extends foundry.appv1.sheets.ActorSheet { }; const condIds = PHYS_CRIT_CONDITIONS[key] || []; for (const condId of condIds) { - try { - const already = actor.statuses?.has(condId); - if (!already) await actor.toggleStatusEffect(condId, { active: true }); - } catch(_) {} + await _applyStatusCondition(actor, condId); } // If instant death @@ -3563,6 +3563,11 @@ Hooks.once("setup", () => { Hooks.once('ready', function() { console.log('Zero Engine | Ready'); + // Hourly PC condition report — whispered to GM (fires once per hour while Foundry is running) + if (game.user?.isGM) { + setInterval(() => _broadcastPCConditions(), 60 * 60 * 1000); + } + // ── ONE-TIME DATA SANITIZATION ──────────────────────────────────────── // Fix actors whose skill/flux values were corrupted by duplicate form inputs // (stored as "X,X" string). Safe to run every boot — skips clean actors. @@ -7616,6 +7621,113 @@ const SLA_CONDITIONS = [ // ─── CONDITION MODIFIER HELPER ──────────────────────────────────────────────── +/** + * Post a whispered GM report of every PC's current conditions, HP, Resolve, and Stress. + * Called hourly from the ready hook and on demand from the STATUS button on any sheet. + */ +async function _broadcastPCConditions() { + if (!game.user?.isGM) return; + + const pcs = (game.actors ?? []).filter(a => + a.type === 'character' && a.system?.details?.isPlayerCharacter === true + ); + + if (pcs.length === 0) { + ui.notifications.info('Zero Engine | No PCs found for condition report.'); + return; + } + + const condLabels = Object.fromEntries( + SLA_CONDITIONS.map(c => [c.id, { label: c.label, desc: c.description }]) + ); + + let rows = ''; + for (const pc of pcs) { + const hp = Number(pc.system.derivedStats?.health?.value ?? 0); + const hpMax = Number(pc.system.derivedStats?.health?.max ?? 0); + const res = Number(pc.system.derivedStats?.resolve?.value ?? 0); + const resMax = Number(pc.system.derivedStats?.resolve?.max ?? 0); + const stress = Number(pc.system.derivedStats?.stress ?? 0); + + const hpColour = hp <= 0 ? '#ff2222' : hp <= hpMax * 0.3 ? '#ff8800' : '#44cc66'; + const resColour = res <= 0 ? '#ff2222' : res <= resMax * 0.3 ? '#ff8800' : '#44ccff'; + const stressColour = stress >= 8 ? '#ff2222' : stress >= 5 ? '#ff8800' : '#aaaaaa'; + + const activeConds = [...(pc.statuses ?? new Set())] + .filter(id => condLabels[id]) + .map(id => `${condLabels[id].label} (${condLabels[id].desc})`) + .join(', '); + + rows += ` +
+
${pc.name}
+
+ ❤ HP ${hp}/${hpMax}   + 🧠 Resolve ${res}/${resMax}   + ⚡ Stress ${stress} +
+
+ ${activeConds || 'No active conditions'} +
+
`; + } + + const now = new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); + + await ChatMessage.create({ + speaker: { alias: '⚠ Zero Engine — PC Status Report' }, + content: `
+
Status Report — ${now} — ${pcs.length} PC${pcs.length !== 1 ? 's' : ''}
+ ${rows} +
`, + whisper: [game.user.id] + }); +} + +/** + * Apply a status condition to an actor reliably. + * Tries actor.toggleStatusEffect first (Foundry v14 API). If that fails or + * the condition still isn't in actor.statuses afterward, falls back to creating + * an ActiveEffect directly so conditions always land even if toggleStatusEffect + * is unavailable or throws. + * @param {Actor} actor + * @param {string} condId e.g. "sla-panicking" + * @param {string} [label] display name fallback + * @param {string} [icon] icon path fallback + */ +async function _applyStatusCondition(actor, condId, label, icon) { + // Skip if already active + if (actor.statuses?.has(condId)) return; + + // Try the standard API first + let applied = false; + try { + await actor.toggleStatusEffect(condId, { active: true }); + applied = actor.statuses?.has(condId) ?? false; + } catch(e) { + console.warn(`Zero Engine | toggleStatusEffect failed for "${condId}":`, e); + } + + if (!applied) { + // Fallback: look up from SLA_CONDITIONS or use provided label/icon + const condDef = SLA_CONDITIONS.find(c => c.id === condId); + const effectName = condDef?.label ?? label ?? condId; + const effectIcon = condDef?.icon ?? icon ?? 'icons/svg/aura.svg'; + try { + await actor.createEmbeddedDocuments('ActiveEffect', [{ + name: effectName, + img: effectIcon, + icon: effectIcon, + statuses: [condId], + disabled: false + }]); + console.log(`Zero Engine | Applied "${condId}" via fallback ActiveEffect on ${actor.name}`); + } catch(e2) { + console.error(`Zero Engine | Could not apply condition "${condId}" on ${actor.name}:`, e2); + } + } +} + /** * Returns dice-pool penalty from any active SLA conditions on this actor. * Checks actor.statuses (Foundry v14 Set) and system.derivedStats.broken.