v1.7.3: NPC initiative button — pool max(5,AGI)+threatLevel d6, count 6s

This commit is contained in:
2026-06-10 19:17:43 +01:00
parent e20b9292db
commit 748b7bc7d9
3 changed files with 104 additions and 28 deletions
+30
View File
@@ -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;
+1 -1
View File
@@ -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"
+73 -27
View File
@@ -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 {
<label class="gnpc-stat-lbl">ARM<input class="gnpc-stat-input" type="number" data-npc-id="${npc.id}" data-field="armor" value="${npc.armor}" min="0"/></label>
<label class="gnpc-stat-lbl">DMG<input class="gnpc-stat-input" type="number" data-npc-id="${npc.id}" data-field="damage" value="${npc.damage}" min="1"/></label>
</div>
<div class="gnpc-init-row">
<button type="button" class="gnpc-btn gnpc-init-btn" data-npc-id="${npc.id}"
title="Roll Initiative — ${npc.initPool}d6, count 6s as successes">
<i class="fas fa-running"></i> INIT
</button>
<span class="gnpc-init-pool" title="Initiative dice pool">${npc.initPool}d6</span>
<span class="gnpc-init-result" data-npc-id="${npc.id}"></span>
</div>
${armorLine}
<div class="gnpc-weapons-block">${natHtml}${wepHtml}</div>
<div class="gnpc-abilities">${abils}</div>
@@ -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 =>
`<span class="gnpc-die${d === 6 ? ' gnpc-die-success' : ''}">${d}</span>`
).join('');
resultEl.innerHTML = `${diceHtml}<span class="gnpc-init-successes">${successes}✓</span>`;
}
// Post result to chat
const diceStr = rolls.map(d => d === 6 ? `<strong>${d}</strong>` : `${d}`).join(' ');
ChatMessage.create({
speaker: { alias: n.name },
content: `<div style="line-height:1.6">
<b><i class="fas fa-running"></i> ${n.name}</b> — Initiative (${pool}d6)<br>
<span style="font-size:12px">${diceStr}</span><br>
<b style="color:#44cc88">${successes} success${successes !== 1 ? 'es' : ''}</b>
</div>`,
type: CONST.CHAT_MESSAGE_TYPES?.OTHER ?? 0,
});
})
);
// Reroll one
root.querySelectorAll('.gnpc-reroll-btn').forEach(btn =>
btn.addEventListener('click', () => {