diff --git a/styles/zero-engine.css b/styles/zero-engine.css index eadd5df..7809002 100644 --- a/styles/zero-engine.css +++ b/styles/zero-engine.css @@ -4481,6 +4481,38 @@ body > .ze-pause-root .ze-sat-inner { .group-npc-tool .gnpc-count-lbl { display: flex; align-items: center; gap: 5px; font-size: 12px; color: #aaa; } +/* Threat level slider row */ +.group-npc-tool .gnpc-threat-lbl { + display: flex; align-items: center; gap: 6px; + font-size: 12px; color: #aaa; flex-shrink: 0; +} +.group-npc-tool .gnpc-threat-slider { + -webkit-appearance: none; appearance: none; + width: 110px; height: 4px; border-radius: 2px; + background: linear-gradient(to right, #00ccff 0%, #00ccff 20%, #333 20%, #333 100%); + outline: none; cursor: pointer; +} +.group-npc-tool .gnpc-threat-slider::-webkit-slider-thumb { + -webkit-appearance: none; appearance: none; + width: 12px; height: 12px; border-radius: 50%; + background: #00ccff; border: 2px solid #0088aa; cursor: pointer; + box-shadow: 0 0 4px rgba(0,200,255,0.5); +} +.group-npc-tool .gnpc-threat-slider::-moz-range-thumb { + width: 12px; height: 12px; border-radius: 50%; + background: #00ccff; border: 2px solid #0088aa; cursor: pointer; +} +.group-npc-tool .gnpc-threat-val { + font-size: 11px; color: #00ccff; font-family: monospace; + white-space: nowrap; min-width: 90px; +} +/* Threat level badge on NPC cards */ +.group-npc-tool .gnpc-tlvl-tag { + font-size: 11px; font-weight: bold; color: #00ccff; + background: rgba(0,200,255,0.12); border: 1px solid rgba(0,200,255,0.35); + border-radius: 10px; padding: 1px 6px; white-space: nowrap; flex-shrink: 0; + text-shadow: 0 0 6px rgba(0,200,255,0.6); +} .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 e4d428d..bf2e641 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.1", + "version": "1.7.2", "compatibility": { "minimum": 13, "verified": "14.359" diff --git a/zero-engine.mjs b/zero-engine.mjs index 0f77b9f..f81cedc 100644 --- a/zero-engine.mjs +++ b/zero-engine.mjs @@ -6312,6 +6312,7 @@ class SLAGroupNPCTool extends Application { this._selectedType = 'carrion'; this._count = 4; this._root = null; + this._threatLevel = 3; // 1 = baseline, 10 = elite. Scale: AGI/initiative focus. } static get defaultOptions() { @@ -6328,6 +6329,10 @@ class SLAGroupNPCTool extends Application { // ── Utilities ───────────────────────────────────────────────────────────── _rng(min, max) { return Math.floor(Math.random() * (max - min + 1)) + min; } _pick(arr) { return arr[Math.floor(Math.random() * arr.length)]; } + _threatLevelLabel(lvl) { + const names = ['','Baseline','Quick','Alert','Dangerous','Veteran','Hardened','Elite','Apex','Legendary','Mythic']; + return `${lvl} — ${names[lvl] ?? lvl}`; + } _hexRgb(hex) { const r = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex); return r ? `${parseInt(r[1],16)},${parseInt(r[2],16)},${parseInt(r[3],16)}` : '128,128,128'; @@ -6401,7 +6406,7 @@ class SLAGroupNPCTool extends Application { } } - return { + const npc = { id: foundry.utils.randomID(), typeKey, typeLabel: arch.label, color: arch.color, icon: arch.icon, @@ -6410,6 +6415,46 @@ class SLAGroupNPCTool extends Application { weapons, naturalWeapons, armorDesc, abilities, notes: '', defeated: false }; + return this._applyThreatBonus(npc); + } + + // ── 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. + _applyThreatBonus(npc) { + const lvl = Math.max(1, Math.min(10, this._threatLevel ?? 1)); + if (lvl <= 1) return npc; + + // 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]; + + 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; + } + + npc.threatBonus = lvl; // store so the card can show a badge + return npc; } // ── Icon helpers — SLA-specific art ───────────────────────────────────── @@ -6584,6 +6629,7 @@ class SLAGroupNPCTool extends Application { ${npc.typeLabel} T${npc.threat} + ${(npc.threatBonus ?? 1) > 1 ? `⚡${npc.threatBonus}` : ''}
@@ -6696,6 +6747,22 @@ class SLAGroupNPCTool extends Application { this._count = Math.max(1, Math.min(10, parseInt(e.target.value) || 1)); }); + // Threat level slider (1–10) — live label + track-fill update, no re-render + const threatSlider = root.querySelector('.gnpc-threat-slider'); + const _updateSliderFill = (el, lvl) => { + const pct = ((lvl - 1) / 9 * 100).toFixed(1); + el.style.background = `linear-gradient(to right, #00ccff 0%, #00ccff ${pct}%, #333 ${pct}%, #333 100%)`; + }; + if (threatSlider) { + _updateSliderFill(threatSlider, this._threatLevel); + threatSlider.addEventListener('input', e => { + this._threatLevel = Math.max(1, Math.min(10, parseInt(e.target.value) || 1)); + _updateSliderFill(e.target, this._threatLevel); + const valEl = root.querySelector('.gnpc-threat-val'); + if (valEl) valEl.textContent = this._threatLevelLabel(this._threatLevel); + }); + } + // Generate (replace all) root.querySelector('.gnpc-gen-btn')?.addEventListener('click', () => { this._npcs = [];