diff --git a/styles/zero-engine.css b/styles/zero-engine.css index ad15344..0e183c8 100644 --- a/styles/zero-engine.css +++ b/styles/zero-engine.css @@ -257,6 +257,15 @@ letter-spacing: 0.8px; } +.zero-engine .vital-save-label { + cursor: pointer; + transition: opacity 0.15s; +} +.zero-engine .vital-save-label:hover { + opacity: 0.75; + text-decoration: underline dotted; +} + .zero-engine .vital-values { display: flex; align-items: center; diff --git a/templates/actor/character-sheet.hbs b/templates/actor/character-sheet.hbs index ddb4fb4..7d97f22 100644 --- a/templates/actor/character-sheet.hbs +++ b/templates/actor/character-sheet.hbs @@ -109,7 +109,7 @@ {{!-- VITALS ROW: Health, Resolve, Stress, Ebb --}}
- +
/ @@ -117,7 +117,7 @@
- +
/ diff --git a/zero-engine.mjs b/zero-engine.mjs index f55dec5..d7caa4d 100644 --- a/zero-engine.mjs +++ b/zero-engine.mjs @@ -455,6 +455,9 @@ class ZeroEngineActorSheet extends foundry.appv1.sheets.ActorSheet { html.find('.manual-panic-button').click(this._onManualPanic.bind(this)); html.find('.manual-armor-button').click(this._onManualArmorCheck.bind(this)); + // Health / Resolve save rolls — click the vital label name + html.find('.vital-save-label').click(this._onVitalSave.bind(this)); + // Delete injury buttons html.find('.delete-injury-btn').click(this._onDeleteInjury.bind(this)); @@ -1214,6 +1217,64 @@ class ZeroEngineActorSheet extends foundry.appv1.sheets.ActorSheet { } } + /** + * Handle Health or Resolve save roll — clicking the vital label name. + * Rolls current value in plain d6 (no stress dice, no dialog). + * A failure (zero successes) adds +1 Stress. + * @param {Event} event + * @private + */ + async _onVitalSave(event) { + event.preventDefault(); + event.stopPropagation(); + + const saveType = event.currentTarget.dataset.saveType; // "health" or "resolve" + const label = saveType === "health" ? "Health Save" : "Resolve Save"; + const current = Number(this.actor.system.derivedStats?.[saveType]?.value) || 0; + + if (current <= 0) { + ui.notifications.warn(`${this.actor.name} has no ${saveType} remaining — cannot make a ${label}.`); + return; + } + + // Roll current value in d6 — no stress dice, no dialog + const roll = new Roll(`${current}d6`); + await roll.evaluate(); + + if (game.dice3d) await game.dice3d.showForRoll(roll, game.user, true); + + const results = roll.dice[0]?.results || []; + const successes = results.filter(r => r.result === 6).length; + const failed = successes === 0; + + // Build dice display using existing yze-die classes + const diceHtml = results.map(r => { + let cls = 'yze-die'; + if (r.result === 6) cls += ' yze-success'; + return `
${r.result}
`; + }).join(''); + + let resultLine; + if (failed) { + const curStress = this._getStressValue(this.actor); + const newStress = Math.min(10, curStress + 1); + await this.actor.update(this._buildStressUpdate(this.actor, newStress)); + resultLine = `
FAILED — no successes. +1 Stress (now ${newStress}).
`; + } else { + resultLine = `
SUCCESS — ${successes} success${successes !== 1 ? 'es' : ''}!
`; + } + + await ChatMessage.create({ + speaker: ChatMessage.getSpeaker({ actor: this.actor }), + flavor: `${this.actor.name} — ${label}
${current}d6 (current ${saveType === 'health' ? 'Health' : 'Resolve'})`, + content: `
${diceHtml}
${resultLine}
`, + style: CONST.CHAT_MESSAGE_STYLES?.ROLL, + type: CONST.CHAT_MESSAGE_TYPES?.ROLL, + roll: roll, + rollMode: game.settings.get('core', 'rollMode') + }); + } + /** * Handle Manual Panic button click * @param {Event} event