From 27bc43cea65f2b2077c66976becab34dd66dfb0e Mon Sep 17 00:00:00 2001 From: Tim Evans Date: Wed, 3 Jun 2026 23:50:05 +0100 Subject: [PATCH] fix+feat: button overrun, natural weapon rolls, Wraith Teeth & Claw (v1.6.1) CSS: morph-shift-btn now flex: 0 0 auto; max-width: 110px specialty-name gets flex: 1; min-width: 0 so it takes remaining space flex-wrap on specialty-name-row prevents overflow Natural weapons: - template.json: isNaturalWeapon/naturalWeaponDamage/AP/ROF fields on specialty - Morphic Strike: ROF 2, isNaturalWeapon: true - Wraith Teeth & Claw added to wraithraider racial abilities (DMG 2, AP 1, ROF 2) - _ensureRacialAbilities patches existing items with missing natural weapon fields - _onNaturalWeaponRoll: STR + Melee roll through full weapon chat card pipeline - STRIKE button appears on any specialty with isNaturalWeapon: true - All condition/drug/armor/racial mods applied to natural weapon rolls --- styles/zero-engine.css | 16 ++++- system.json | 2 +- template.json | 6 +- templates/actor/character-sheet.hbs | 7 +++ zero-engine.mjs | 91 ++++++++++++++++++++++++++--- 5 files changed, 110 insertions(+), 12 deletions(-) diff --git a/styles/zero-engine.css b/styles/zero-engine.css index d521e04..ed15c6e 100644 --- a/styles/zero-engine.css +++ b/styles/zero-engine.css @@ -971,6 +971,7 @@ align-items: center; gap: 8px; margin-bottom: 2px; + flex-wrap: wrap; } .zero-engine .specialty-name { @@ -978,6 +979,8 @@ font-size: 13px; color: #fff; cursor: pointer; + flex: 1; + min-width: 0; } .zero-engine .specialty-name:hover { @@ -4278,7 +4281,7 @@ body > .ze-pause-root .ze-sat-inner { padding: 2px 10px; font-size: 10px; font-weight: bold; text-transform: uppercase; letter-spacing: 0.5px; border-radius: 3px; cursor: pointer; transition: all 0.2s; background: rgba(0,180,255,0.1); border: 1px solid #0090cc; color: #44bbff; - white-space: nowrap; flex-shrink: 0; + white-space: nowrap; flex: 0 0 auto; max-width: 110px; } .zero-engine .morph-shift-btn:hover { background: rgba(0,180,255,0.25); } .zero-engine .morph-shift-btn.morph-shift-active { @@ -4286,6 +4289,17 @@ body > .ze-pause-root .ze-sat-inner { box-shadow: 0 0 8px rgba(0,200,255,0.3); } +/* Natural weapon STRIKE button */ +.zero-engine .natural-weapon-roll-btn { + padding: 2px 10px; font-size: 10px; font-weight: bold; text-transform: uppercase; + letter-spacing: 0.5px; border-radius: 3px; cursor: pointer; transition: all 0.2s; + background: rgba(255,80,0,0.15); border: 1px solid #cc4400; color: #ff7733; + white-space: nowrap; flex: 0 0 auto; +} +.zero-engine .natural-weapon-roll-btn:hover { + background: rgba(255,80,0,0.3); border-color: #ff6600; color: #ffaa55; +} + /* ══════════════════════════════════════════════════════════════════════════════ PLAYER CHARACTER FLAG — Bio Tab ══════════════════════════════════════════════════════════════════════════════ */ diff --git a/system.json b/system.json index fc309b9..74014ab 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.6.0", + "version": "1.6.1", "compatibility": { "minimum": 13, "verified": "14.359" diff --git a/template.json b/template.json index 2e0506c..79c34f9 100644 --- a/template.json +++ b/template.json @@ -310,7 +310,11 @@ "package": "", "healthMod": 0, "resolveMod": 0, - "isActive": true + "isActive": true, + "isNaturalWeapon": false, + "naturalWeaponDamage": 0, + "naturalWeaponAP": 0, + "naturalWeaponROF": 0 }, "ebb": { "description": "", diff --git a/templates/actor/character-sheet.hbs b/templates/actor/character-sheet.hbs index 13159f1..957f2b1 100644 --- a/templates/actor/character-sheet.hbs +++ b/templates/actor/character-sheet.hbs @@ -477,6 +477,13 @@
{{this.name}} {{this.system.package}} + {{!-- Natural weapon STRIKE button --}} + {{#if this.system.isNaturalWeapon}} + + {{/if}} {{!-- Morph form SHIFT button for Vevaphon forms --}} {{#if (eq this.system.prerequisites "Vevaphon")}} {{#unless (eq this.name "Morphic Strike")}} diff --git a/zero-engine.mjs b/zero-engine.mjs index 67c89b8..6d2de1e 100644 --- a/zero-engine.mjs +++ b/zero-engine.mjs @@ -460,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)); + // Natural weapon attack roll (Morphic Strike, Wraith Teeth & Claw, etc.) + html.find('.natural-weapon-roll-btn').click(this._onNaturalWeaponRoll.bind(this)); + // GM PC condition status report html.find('.pc-status-btn').click(ev => { ev.preventDefault(); _broadcastPCConditions(); }); @@ -1230,6 +1233,53 @@ class ZeroEngineActorSheet extends foundry.appv1.sheets.ActorSheet { } } + /** + * Handle a natural weapon attack roll from a specialty (Morphic Strike, Wraith Teeth & Claw, etc.) + * Uses STR + Melee, same pipeline as a melee weapon attack, with the weapon's own DMG/AP/ROF. + * @param {Event} event + * @private + */ + async _onNaturalWeaponRoll(event) { + event.preventDefault(); + event.stopPropagation(); + + const itemId = event.currentTarget.dataset.itemId; + const item = this.actor.items.get(itemId); + if (!item) return; + + const s = item.system; + const damage = Number(s.naturalWeaponDamage) || 2; + const ap = Number(s.naturalWeaponAP) || 1; + const rof = Number(s.naturalWeaponROF) || 2; + + const attrValue = parseInt(this.actor.system.attributes?.strength?.value) || 2; + const skillValue = parseInt(this.actor.system.skills?.melee?.value) || 0; + const armorMods = this._getArmorRollModifiers(this.actor, { attribute: 'strength', skill: 'melee' }); + const drugMods = this._getDrugRollModifiers(this.actor, { attribute: 'strength', skill: 'melee' }); + const racialMods = this._getRacialDiceBonus(this.actor, { skill: 'melee', attribute: 'strength' }); + const condMods = _getConditionModifiers(this.actor, { attribute: 'strength', skill: 'melee' }); + const baseDice = attrValue + skillValue + armorMods.total + drugMods.total + racialMods.bonus + condMods.bonus; + + const formulaStr = `STR ${attrValue} + Melee ${skillValue}` + + `${armorMods.total !== 0 ? ` + Armor ${armorMods.total}` : ''}` + + `${drugMods.total !== 0 ? ` + Drug ${drugMods.total}` : ''}` + + `${racialMods.bonus !== 0 ? ` + Racial ${racialMods.bonus}` : ''}` + + `${condMods.bonus !== 0 ? ` + Conditions ${condMods.bonus}` : ''}` + + ` = ${baseDice}d6 | DMG ${damage}, AP ${ap}, ROF ${rof}`; + + const weaponInfo = { + name: item.name, + damage, + ap, + range: 'Engaged', + rof, + category: 'melee', + weaponType: 'Natural' + }; + + return this._rollYZEDice(baseDice, item.name, formulaStr, 'strength', weaponInfo); + } + /** * Handle Health or Resolve save roll — clicking the vital label name. * Rolls current value in plain d6 (no stress dice, no dialog). @@ -6858,6 +6908,20 @@ const SLA_RACIAL_ABILITY_DATA = { prerequisites: "Wraith Raider", isActive: true, healthMod: 0, resolveMod: 0 } + }, + { + name: "Wraith Teeth & Claw", + type: "specialty", + img: "systems/zero-engine/assets/racial/natural-weapons.svg", + system: { + category: "racial", + description: "Bone-hard teeth and razor claws grown through decades of cold-world adaptation. Natural melee weapons — Damage 2, AP 1, ROF 2. Always counts as armed.", + effects: "Natural weapon: DMG 2, AP 1, ROF 2. Always armed.", + racialBonuses: [], + prerequisites: "Wraith Raider", + isActive: true, healthMod: 0, resolveMod: 0, + isNaturalWeapon: true, naturalWeaponDamage: 2, naturalWeaponAP: 1, naturalWeaponROF: 2 + } } ], @@ -6946,10 +7010,11 @@ const SLA_RACIAL_ABILITY_DATA = { name: "Morphic Strike", type: "specialty", img: "systems/zero-engine/assets/racial/natural-weapons.svg", system: { - category: "racial", description: "Natural biogenetic weapon formed from the Vevaphon's own mass. Damage 2, AP 1. Always available regardless of morph form. Counts as melee weapon for skill rolls.", - effects: "Natural weapon: DMG 2, AP 1. Always available.", + category: "racial", description: "Natural biogenetic weapon formed from the Vevaphon's own mass. Damage 2, AP 1, ROF 2. Always available regardless of morph form. Counts as melee weapon for skill rolls.", + effects: "Natural weapon: DMG 2, AP 1, ROF 2. Always available.", racialBonuses: [], - prerequisites: "Vevaphon", isActive: true, healthMod: 0, resolveMod: 0 + prerequisites: "Vevaphon", isActive: true, healthMod: 0, resolveMod: 0, + isNaturalWeapon: true, naturalWeaponDamage: 2, naturalWeaponAP: 1, naturalWeaponROF: 2 } } ] @@ -6970,20 +7035,28 @@ async function _ensureRacialAbilities(actor) { if (!abilities || !abilities.length) return; const toCreate = []; - const toUpdateImg = []; + const toUpdate = []; for (const ability of abilities) { const existing = actor.items.find( i => i.type === "specialty" && i.name === ability.name ); if (!existing) { toCreate.push(ability); - } else if (ability.img && existing.img !== ability.img) { - // Update stale icon on existing item - toUpdateImg.push({ _id: existing.id, img: ability.img }); + } else { + const patch = {}; + if (ability.img && existing.img !== ability.img) patch.img = ability.img; + // Patch natural weapon fields if missing or stale + if (ability.system?.isNaturalWeapon && !existing.system?.isNaturalWeapon) { + patch["system.isNaturalWeapon"] = true; + patch["system.naturalWeaponDamage"] = ability.system.naturalWeaponDamage ?? 2; + patch["system.naturalWeaponAP"] = ability.system.naturalWeaponAP ?? 1; + patch["system.naturalWeaponROF"] = ability.system.naturalWeaponROF ?? 2; + } + if (Object.keys(patch).length) toUpdate.push({ _id: existing.id, ...patch }); } } - if (toUpdateImg.length) { - try { await actor.updateEmbeddedDocuments("Item", toUpdateImg); } catch(_) {} + if (toUpdate.length) { + try { await actor.updateEmbeddedDocuments("Item", toUpdate); } catch(_) {} } if (!toCreate.length) return;