From 7564cd420152f4828edf798225ffb996fec285c9 Mon Sep 17 00:00:00 2001 From: Tim Evans Date: Wed, 3 Jun 2026 23:25:04 +0100 Subject: [PATCH] feat: condition modifiers applied to ALL roll paths (v1.6.0) Every dice roll now checks active conditions before building its pool: - _onRoll (legacy .rollable attribute + skill paths) - _onRollEbbFormula (Empathy + Ebb pool) - _rollInitiative (bonus-successes roll pool + chat card label) - _onVitalSave (Health/Resolve saves, with auto-fail at 0 dice) Condition penalty is shown in the formula string on every chat card. Panicking (-2 all) and Shaken (-1 all) now silently reduce every roll until the GM clears the condition. --- system.json | 2 +- zero-engine.mjs | 57 ++++++++++++++++++++++++++++++++++++++----------- 2 files changed, 45 insertions(+), 14 deletions(-) diff --git a/system.json b/system.json index a7a4f47..fc309b9 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.9", + "version": "1.6.0", "compatibility": { "minimum": 13, "verified": "14.359" diff --git a/zero-engine.mjs b/zero-engine.mjs index 54d20b3..67c89b8 100644 --- a/zero-engine.mjs +++ b/zero-engine.mjs @@ -1157,16 +1157,19 @@ class ZeroEngineActorSheet extends foundry.appv1.sheets.ActorSheet { // Build dice pool: Empathy attribute + Ebb skill // Use parseInt() || 0 — robust against corrupt stored values ("3,3", "NaN", etc.) - const empathy = parseInt(this.actor.system.attributes?.empathy?.value) || 2; + const empathy = parseInt(this.actor.system.attributes?.empathy?.value) || 2; const ebbSkill = parseInt(this.actor.system.skills?.ebb?.value) || 0; - const totalDice = (empathy + ebbSkill) || 1; // fallback to 1 die minimum + const condMods = _getConditionModifiers(this.actor, { attribute: 'empathy', skill: 'ebb' }); + const totalDice = Math.max(0, empathy + ebbSkill + condMods.bonus) || 1; // fallback to 1 die minimum const requiredSuccesses = Number(formula.successes ?? 1); const disciplineLabel = formula.discipline ? formula.discipline.charAt(0).toUpperCase() + formula.discipline.slice(1) : "Ebb"; const label = `${item.name} [${disciplineLabel}]`; - const formulaStr = `Empathy ${empathy} + Ebb ${ebbSkill} = ${totalDice}d6 | ` + + const formulaStr = `Empathy ${empathy} + Ebb ${ebbSkill}` + + `${condMods.bonus !== 0 ? ` + Conditions ${condMods.bonus}` : ''}` + + ` = ${totalDice}d6 | ` + `Flux: ${currentFlux} → ${newFlux} (cost ${fluxCost}) | Need ${requiredSuccesses} suc`; const ebbInfo = { @@ -1247,8 +1250,28 @@ class ZeroEngineActorSheet extends foundry.appv1.sheets.ActorSheet { return; } - // Roll current value in d6 — no stress dice, no dialog - const roll = new Roll(`${current}d6`); + // Apply condition modifiers — conditions penalise all rolls including saves + const condMods = _getConditionModifiers(this.actor); + const pool = Math.max(0, current + condMods.bonus); + const condNote = condMods.bonus !== 0 + ? ` + Conditions ${condMods.bonus} = ${pool}d6` + : ''; + + if (pool <= 0) { + ui.notifications.warn(`${this.actor.name}: condition penalties reduce the ${label} pool to 0 — automatic failure. +1 Stress.`); + const curStress = this._getStressValue(this.actor); + const newStress = Math.min(10, curStress + 1); + await this.actor.update(this._buildStressUpdate(this.actor, newStress)); + await ChatMessage.create({ + speaker: ChatMessage.getSpeaker({ actor: this.actor }), + flavor: `${this.actor.name} — ${label}`, + content: `
AUTO-FAIL — 0 dice after condition penalties. +1 Stress (now ${newStress}).
`, + }); + return; + } + + // Roll the conditioned pool in d6 — no stress dice, no dialog + const roll = new Roll(`${pool}d6`); await roll.evaluate(); if (game.dice3d) await game.dice3d.showForRoll(roll, game.user, true); @@ -1276,7 +1299,7 @@ class ZeroEngineActorSheet extends foundry.appv1.sheets.ActorSheet { await ChatMessage.create({ speaker: ChatMessage.getSpeaker({ actor: this.actor }), - flavor: `${this.actor.name} — ${label}
${current}d6 (current ${saveType === 'health' ? 'Health' : 'Resolve'})`, + flavor: `${this.actor.name} — ${label}
${current}d6${condNote}`, content: `
${diceHtml}
${resultLine}
`, style: CONST.CHAT_MESSAGE_STYLES?.ROLL, type: CONST.CHAT_MESSAGE_TYPES?.ROLL, @@ -1398,12 +1421,14 @@ class ZeroEngineActorSheet extends foundry.appv1.sheets.ActorSheet { const attributeValue = this.actor.system.attributes[attribute].value || 2; const armorMods = this._getArmorRollModifiers(this.actor, { attribute }); - const drugMods = this._getDrugRollModifiers(this.actor, { attribute }); - const totalDice = Math.max(0, attributeValue + armorMods.total + drugMods.total); + const drugMods = this._getDrugRollModifiers(this.actor, { attribute }); + const condMods = _getConditionModifiers(this.actor, { attribute }); + const totalDice = Math.max(0, attributeValue + armorMods.total + drugMods.total + condMods.bonus); const label = attribute.charAt(0).toUpperCase() + attribute.slice(1); const formula = `${label} ${attributeValue}` + `${armorMods.statMod !== 0 ? ` + Armor Stat ${armorMods.statMod}` : ''}` + `${drugMods.statMod !== 0 ? ` + Drug Stat ${drugMods.statMod}` : ''}` + + `${condMods.bonus !== 0 ? ` + Conditions ${condMods.bonus}` : ''}` + ` = ${totalDice}d6`; return this._rollYZEDice(totalDice, label, formula, attribute); @@ -1424,12 +1449,13 @@ class ZeroEngineActorSheet extends foundry.appv1.sheets.ActorSheet { return; } - const skillValue = this.actor.system.skills[skill].value || 0; + const skillValue = this.actor.system.skills[skill].value || 0; const attributeValue = this.actor.system.attributes[attribute].value || 2; const armorMods = this._getArmorRollModifiers(this.actor, { attribute, skill }); - const drugMods = this._getDrugRollModifiers(this.actor, { attribute, skill }); - const totalDice = Math.max(0, attributeValue + skillValue + armorMods.total + drugMods.total); - const attrLabel = attribute.charAt(0).toUpperCase() + attribute.slice(1); + const drugMods = this._getDrugRollModifiers(this.actor, { attribute, skill }); + const condMods = _getConditionModifiers(this.actor, { attribute, skill }); + const totalDice = Math.max(0, attributeValue + skillValue + armorMods.total + drugMods.total + condMods.bonus); + const attrLabel = attribute.charAt(0).toUpperCase() + attribute.slice(1); const skillLabel = skill.charAt(0).toUpperCase() + skill.slice(1); const label = `${skillLabel} (${attribute.substring(0, 3).toUpperCase()})`; const formula = `${attrLabel} ${attributeValue} + ${skillLabel} ${skillValue}` + @@ -1437,6 +1463,7 @@ class ZeroEngineActorSheet extends foundry.appv1.sheets.ActorSheet { `${armorMods.skillMod !== 0 ? ` + Armor Skill ${armorMods.skillMod}` : ''}` + `${drugMods.statMod !== 0 ? ` + Drug Stat ${drugMods.statMod}` : ''}` + `${drugMods.skillMod !== 0 ? ` + Drug Skill ${drugMods.skillMod}` : ''}` + + `${condMods.bonus !== 0 ? ` + Conditions ${condMods.bonus}` : ''}` + ` = ${totalDice}d6`; return this._rollYZEDice(totalDice, label, formula, attribute); @@ -2856,8 +2883,11 @@ class ZeroEngineActorSheet extends foundry.appv1.sheets.ActorSheet { // Base score is fixed — attribute + skill value const baseScore = attrVal + skillVal; + // ── Condition penalties reduce the roll pool (conditions affect all rolls) ─ + const initCondMods = _getConditionModifiers(actor, { attribute: attrKey, skill: skillKey }); + // ── Roll the same pool for the bonus successes (no stress dice) ────────── - const pool = Math.max(1, baseScore); + const pool = Math.max(1, baseScore + initCondMods.bonus); const roll = new Roll(`${pool}d6`); await roll.evaluate(); @@ -2970,6 +3000,7 @@ class ZeroEngineActorSheet extends foundry.appv1.sheets.ActorSheet {
Roll (${pool}d6): ${diceHTML}
Successes: +${rollSuccesses}
${totalBonus !== 0 ? `
Bonuses: ${totalBonus > 0 ? '+' : ''}${totalBonus} (${bonusLine})
` : ''} + ${initCondMods.bonus !== 0 ? `
Conditions: ${initCondMods.bonus} (${initCondMods.breakdown.join(', ')})
` : ''}
Final Initiative: ${finalInit}
Tie-breaker d6: ${tieVal}
`;