From 748b7bc7d9e32fb98940fe81056606aa009e9d0d Mon Sep 17 00:00:00 2001 From: Tim Evans Date: Wed, 10 Jun 2026 19:17:43 +0100 Subject: [PATCH] =?UTF-8?q?v1.7.3:=20NPC=20initiative=20button=20=E2=80=94?= =?UTF-8?q?=20pool=20max(5,AGI)+threatLevel=20d6,=20count=206s?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- styles/zero-engine.css | 30 +++++++++++++ system.json | 2 +- zero-engine.mjs | 100 ++++++++++++++++++++++++++++++----------- 3 files changed, 104 insertions(+), 28 deletions(-) diff --git a/styles/zero-engine.css b/styles/zero-engine.css index 7809002..26ab30b 100644 --- a/styles/zero-engine.css +++ b/styles/zero-engine.css @@ -4513,6 +4513,36 @@ body > .ze-pause-root .ze-sat-inner { border-radius: 10px; padding: 1px 6px; white-space: nowrap; flex-shrink: 0; text-shadow: 0 0 6px rgba(0,200,255,0.6); } +/* Initiative roll row */ +.group-npc-tool .gnpc-init-row { + display: flex; align-items: center; gap: 6px; margin: 3px 0 1px; +} +.group-npc-tool .gnpc-init-btn { + background: rgba(50,200,100,0.12) !important; border: 1px solid #33aa66 !important; + color: #44cc88 !important; font-weight: bold; font-size: 10px; padding: 2px 6px; + border-radius: 3px; cursor: pointer; flex-shrink: 0; +} +.group-npc-tool .gnpc-init-btn:hover { background: rgba(50,200,100,0.28) !important; } +.group-npc-tool .gnpc-init-pool { + font-size: 11px; color: #55bb77; font-family: monospace; flex-shrink: 0; +} +.group-npc-tool .gnpc-init-result { + display: flex; align-items: center; gap: 2px; flex-wrap: wrap; min-height: 18px; +} +.group-npc-tool .gnpc-die { + display: inline-flex; align-items: center; justify-content: center; + width: 15px; height: 15px; border-radius: 3px; + font-size: 9px; font-weight: bold; line-height: 1; + background: rgba(255,255,255,0.07); border: 1px solid #2a2a2a; color: #777; +} +.group-npc-tool .gnpc-die.gnpc-die-success { + background: rgba(50,200,100,0.22); border-color: #44cc88; color: #44ff88; + box-shadow: 0 0 5px rgba(68,204,136,0.45); +} +.group-npc-tool .gnpc-init-successes { + font-size: 11px; font-weight: bold; color: #44cc88; + font-family: monospace; margin-left: 4px; flex-shrink: 0; +} .group-npc-tool .gnpc-count-sel { background: rgba(255,255,255,0.08); border: 1px solid #444; color: #eee; border-radius: 3px; padding: 2px 4px; font-size: 12px; diff --git a/system.json b/system.json index bf2e641..523a036 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.7.2", + "version": "1.7.3", "compatibility": { "minimum": 13, "verified": "14.359" diff --git a/zero-engine.mjs b/zero-engine.mjs index f81cedc..fafba5e 100644 --- a/zero-engine.mjs +++ b/zero-engine.mjs @@ -6420,40 +6420,46 @@ class SLAGroupNPCTool extends Application { // ── Threat-level scaling ─────────────────────────────────────────────────── // Scales AGI (initiative) as the primary target; STR, HP, ARM, DMG also - // creep up at higher levels. Level 1 = no change; level 10 = elite tier. + // creep up at higher levels. Level 1 = baseline (no stat changes). + // initPool is ALWAYS set: max(5, agi) + threatLevel gives at least 6d6 at + // level 1 so every NPC can match players on initiative. _applyThreatBonus(npc) { const lvl = Math.max(1, Math.min(10, this._threatLevel ?? 1)); - if (lvl <= 1) return npc; + npc.threatBonus = lvl; - // Per-level bonus table: [agiBonus, strBonus, witBonus, hpPct, armorBonus, dmgBonus] - const TABLE = [ - [0, 0, 0, 0.00, 0, 0], // 1 — baseline - [1, 0, 0, 0.00, 0, 0], // 2 — quick - [1, 0, 1, 0.00, 0, 0], // 3 — alert - [2, 1, 1, 0.00, 0, 0], // 4 — dangerous - [2, 1, 1, 0.25, 0, 1], // 5 — veteran - [3, 2, 1, 0.25, 1, 1], // 6 — hardened - [3, 2, 2, 0.50, 1, 1], // 7 — elite - [4, 3, 2, 0.50, 1, 2], // 8 — apex - [4, 3, 3, 0.75, 2, 2], // 9 — legendary - [5, 4, 3, 1.00, 2, 3], // 10 — mythic - ]; - const [agiB, strB, witB, hpPct, armB, dmgB] = TABLE[lvl - 1]; + if (lvl > 1) { + // Per-level bonus table: [agiBonus, strBonus, witBonus, hpPct, armorBonus, dmgBonus] + const TABLE = [ + [0, 0, 0, 0.00, 0, 0], // 1 — baseline (unused; only entered for lvl > 1) + [1, 0, 0, 0.00, 0, 0], // 2 — quick + [1, 0, 1, 0.00, 0, 0], // 3 — alert + [2, 1, 1, 0.00, 0, 0], // 4 — dangerous + [2, 1, 1, 0.25, 0, 1], // 5 — veteran + [3, 2, 1, 0.25, 1, 1], // 6 — hardened + [3, 2, 2, 0.50, 1, 1], // 7 — elite + [4, 3, 2, 0.50, 1, 2], // 8 — apex + [4, 3, 3, 0.75, 2, 2], // 9 — legendary + [5, 4, 3, 1.00, 2, 3], // 10 — mythic + ]; + const [agiB, strB, witB, hpPct, armB, dmgB] = TABLE[lvl - 1]; - npc.agi = Math.min(8, npc.agi + agiB); - npc.str = Math.min(8, npc.str + strB); - npc.wit = Math.min(8, npc.wit + witB); - npc.armor = Math.min(8, npc.armor + armB); - npc.damage = Math.min(8, npc.damage + dmgB); + npc.agi = Math.min(8, npc.agi + agiB); + npc.str = Math.min(8, npc.str + strB); + npc.wit = Math.min(8, npc.wit + witB); + npc.armor = Math.min(8, npc.armor + armB); + npc.damage = Math.min(8, npc.damage + dmgB); - // HP: add a percentage of the base max (rounded up) - if (hpPct > 0) { - const extra = Math.ceil(npc.hpMax * hpPct); - npc.hpMax += extra; - npc.hp += extra; + if (hpPct > 0) { + const extra = Math.ceil(npc.hpMax * hpPct); + npc.hpMax += extra; + npc.hp += extra; + } } - npc.threatBonus = lvl; // store so the card can show a badge + // Initiative dice pool: base AGI (floor 5) + one bonus die per threat level. + // A level-1 Carrion (AGI 2) → max(2,5)+1 = 6d6; level-3 Carrion → max(3,5)+3 = 8d6. + // This guarantees NPCs can contest player initiative at all threat tiers. + npc.initPool = Math.max(5, npc.agi) + lvl; return npc; } @@ -6662,6 +6668,14 @@ class SLAGroupNPCTool extends Application { +
+ + ${npc.initPool}d6 + +
${armorLine}
${natHtml}${wepHtml}
${abils}
@@ -6798,6 +6812,38 @@ class SLAGroupNPCTool extends Application { }) ); + // Initiative roll — pool = max(5, agi) + threatLevel; count 6s as successes + root.querySelectorAll('.gnpc-init-btn').forEach(btn => + btn.addEventListener('click', () => { + const n = this._npcs.find(x => x.id === btn.dataset.npcId); + if (!n) return; + const pool = n.initPool ?? Math.max(5, n.agi) + (n.threatBonus ?? 1); + const rolls = Array.from({ length: pool }, () => Math.ceil(Math.random() * 6)); + const successes = rolls.filter(d => d === 6).length; + + // Update inline result display (no full re-render) + const resultEl = root.querySelector(`.gnpc-init-result[data-npc-id="${n.id}"]`); + if (resultEl) { + const diceHtml = rolls.map(d => + `${d}` + ).join(''); + resultEl.innerHTML = `${diceHtml}${successes}✓`; + } + + // Post result to chat + const diceStr = rolls.map(d => d === 6 ? `${d}` : `${d}`).join(' '); + ChatMessage.create({ + speaker: { alias: n.name }, + content: `
+ ${n.name} — Initiative (${pool}d6)
+ ${diceStr}
+ ${successes} success${successes !== 1 ? 'es' : ''} +
`, + type: CONST.CHAT_MESSAGE_TYPES?.OTHER ?? 0, + }); + }) + ); + // Reroll one root.querySelectorAll('.gnpc-reroll-btn').forEach(btn => btn.addEventListener('click', () => {