diff --git a/styles/zero-engine.css b/styles/zero-engine.css index 023e37e..069e614 100644 --- a/styles/zero-engine.css +++ b/styles/zero-engine.css @@ -4386,3 +4386,137 @@ body > .ze-pause-root .ze-sat-inner { background: rgba(255,200,0,0.35); box-shadow: 0 0 10px rgba(255,200,0,0.3); } + +/* ══════════════════════════════════════════════════════════════════════════════ + GROUP NPC GENERATOR + ══════════════════════════════════════════════════════════════════════════════ */ +.group-npc-tool .window-content { padding: 0; overflow: hidden; } +.group-npc-tool .gnpc-wrap { + display: flex; flex-direction: column; height: 100%; background: #1a1a1a; +} +/* Toolbar */ +.group-npc-tool .gnpc-toolbar { + padding: 8px 10px 6px; border-bottom: 1px solid #2a2a2a; + display: flex; flex-direction: column; gap: 6px; flex-shrink: 0; +} +.group-npc-tool .gnpc-type-row { display: flex; gap: 5px; flex-wrap: wrap; } +.group-npc-tool .gnpc-type-btn { + padding: 4px 12px; font-size: 12px; font-weight: bold; border-radius: 4px; + border: 1px solid; cursor: pointer; background: transparent; + transition: all 0.15s; white-space: nowrap; +} +.group-npc-tool .gnpc-type-btn:hover { opacity: 0.85; } +.group-npc-tool .gnpc-action-row { + display: flex; align-items: center; gap: 8px; flex-wrap: wrap; +} +.group-npc-tool .gnpc-count-lbl { + display: flex; align-items: center; gap: 5px; font-size: 12px; color: #aaa; +} +.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; +} +.group-npc-tool .gnpc-gen-btn { background:rgba(255,200,0,0.15); border:1px solid #ffcc00; color:#ffcc00; border-radius:3px; padding:3px 10px; font-size:12px; font-weight:bold; cursor:pointer; } +.group-npc-tool .gnpc-add-btn { background:rgba(0,180,100,0.12); border:1px solid #00bb66; color:#00bb66; border-radius:3px; padding:3px 10px; font-size:12px; cursor:pointer; } +.group-npc-tool .gnpc-clr-btn { background:rgba(200,50,50,0.1); border:1px solid #884444; color:#bb6666; border-radius:3px; padding:3px 10px; font-size:12px; cursor:pointer; margin-left:auto; } +.group-npc-tool .gnpc-gen-btn:hover { background:rgba(255,200,0,0.3); } +.group-npc-tool .gnpc-add-btn:hover { background:rgba(0,180,100,0.25); } +.group-npc-tool .gnpc-clr-btn:hover { background:rgba(200,50,50,0.25); } +/* Card list */ +.group-npc-tool .gnpc-list { + flex: 1; overflow-y: auto; padding: 8px 10px; + display: grid; grid-template-columns: repeat(auto-fill, minmax(360px, 1fr)); + gap: 8px; align-content: start; +} +.group-npc-tool .gnpc-empty { + text-align: center; padding: 40px 20px; color: #555; + font-style: italic; grid-column: 1 / -1; +} +/* NPC card */ +.group-npc-tool .gnpc-card { + background: rgba(255,255,255,0.03); border-radius: 5px; + padding: 8px 10px; display: flex; flex-direction: column; gap: 5px; + transition: opacity 0.2s; +} +.group-npc-tool .gnpc-card.gnpc-defeated { opacity: 0.4; } +.group-npc-tool .gnpc-card-top { + display: flex; align-items: center; gap: 6px; +} +.group-npc-tool .gnpc-name-input { + flex: 1; min-width: 0; background: rgba(255,255,255,0.06); + border: 1px solid #333; border-radius: 3px; color: #eee; + padding: 3px 6px; font-size: 13px; font-weight: bold; +} +.group-npc-tool .gnpc-name-input:focus { border-color: #555; outline: none; } +.group-npc-tool .gnpc-type-tag { + font-size: 10px; padding: 2px 7px; border-radius: 10px; border: 1px solid; + white-space: nowrap; flex-shrink: 0; +} +.group-npc-tool .gnpc-threat-tag { font-size: 11px; color: #666; white-space: nowrap; flex-shrink: 0; } +.group-npc-tool .gnpc-card-btns { display: flex; gap: 3px; flex-shrink: 0; } +.group-npc-tool .gnpc-btn { + background: transparent; border: 1px solid #333; border-radius: 3px; + color: #888; padding: 2px 5px; cursor: pointer; font-size: 11px; transition: all 0.15s; +} +.group-npc-tool .gnpc-btn:hover { background: rgba(255,255,255,0.08); color: #ccc; } +.group-npc-tool .gnpc-remove-btn:hover { border-color: #cc4444; color: #cc4444; } +/* Stats row */ +.group-npc-tool .gnpc-card-stats { + display: flex; align-items: center; gap: 7px; flex-wrap: wrap; font-size: 11px; +} +.group-npc-tool .gnpc-stat-lbl { + display: flex; align-items: center; gap: 3px; color: #777; +} +.group-npc-tool .gnpc-stat-lbl-plain { color: #777; font-size: 11px; } +.group-npc-tool .gnpc-stat-input { + width: 34px; text-align: center; background: rgba(255,255,255,0.07); + border: 1px solid #2a2a2a; border-radius: 3px; color: #ddd; + padding: 2px 2px; font-size: 12px; +} +.group-npc-tool .gnpc-sep { color: #333; } +.group-npc-tool .gnpc-hp-group { + display: flex; align-items: center; gap: 3px; +} +.group-npc-tool .gnpc-hp-val { width: 30px; color: #88eeaa; } +.group-npc-tool .gnpc-hp-max { width: 30px; color: #aaa; } +.group-npc-tool .gnpc-hp-sep { color: #444; font-size: 11px; } +.group-npc-tool .gnpc-hp-btn { + background: rgba(255,255,255,0.06); border: 1px solid #333; border-radius: 3px; + color: #888; padding: 0 4px; cursor: pointer; font-size: 13px; line-height: 1.4; + flex-shrink: 0; transition: all 0.1s; +} +.group-npc-tool .gnpc-hp-btn:hover { background: rgba(255,255,255,0.15); color: #eee; } +.group-npc-tool .gnpc-hp-bar-wrap { + width: 50px; height: 5px; background: #222; border-radius: 3px; + overflow: hidden; flex-shrink: 0; +} +.group-npc-tool .gnpc-hp-bar { height: 100%; border-radius: 3px; transition: width 0.2s, background 0.3s; } +/* Abilities */ +.group-npc-tool .gnpc-abilities { display: flex; flex-direction: column; gap: 1px; } +.group-npc-tool .gnpc-ability { font-size: 10.5px; color: #888; padding: 0 2px; line-height: 1.4; } +.group-npc-tool .gnpc-ability i { color: #555; margin-right: 3px; } +/* Notes */ +.group-npc-tool .gnpc-notes-input { + width: 100%; background: rgba(255,255,255,0.03); border: 1px solid #222; + border-radius: 3px; color: #888; font-size: 11px; padding: 3px 6px; + resize: vertical; box-sizing: border-box; font-family: inherit; +} +.group-npc-tool .gnpc-notes-input:focus { border-color: #444; outline: none; color: #bbb; } +/* Footer */ +.group-npc-tool .gnpc-footer { + display: flex; align-items: center; justify-content: space-between; + padding: 6px 10px; border-top: 1px solid #2a2a2a; flex-shrink: 0; +} +.group-npc-tool .gnpc-tally { font-size: 12px; color: #666; } +.group-npc-tool .gnpc-footer-btns { display: flex; gap: 8px; } +.group-npc-tool .gnpc-reset-hp-btn { + padding: 4px 12px; font-size: 11px; background: rgba(255,100,100,0.1); + border: 1px solid #884444; color: #cc8888; border-radius: 3px; cursor: pointer; +} +.group-npc-tool .gnpc-reset-hp-btn:hover { background: rgba(255,100,100,0.2); } +.group-npc-tool .gnpc-create-btn { + padding: 4px 12px; font-size: 11px; font-weight: bold; + background: rgba(255,200,0,0.15); border: 1px solid #ffcc00; + color: #ffcc00; border-radius: 3px; cursor: pointer; +} +.group-npc-tool .gnpc-create-btn:hover { background: rgba(255,200,0,0.3); } diff --git a/system.json b/system.json index 4ddbd76..0ce9e3a 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.5.1", + "version": "1.5.2", "compatibility": { "minimum": 13, "verified": "14.359" diff --git a/zero-engine.mjs b/zero-engine.mjs index ba7b0c3..c734b53 100644 --- a/zero-engine.mjs +++ b/zero-engine.mjs @@ -5710,6 +5710,420 @@ class SLAGMFinanceTool extends Application { } } +// ══════════════════════════════════════════════════════════════════════════════ +// SLA GROUP NPC GENERATOR +// GM tool: quickly generate and track groups of Carrions, Serial Killers, +// Monstarets or Machines. Compact editable stat cards, HP tracking, +// reroll per card, and one-click export to World actors. +// ══════════════════════════════════════════════════════════════════════════════ + +const SLA_NPC_GROUP_ARCHETYPES = { + carrion: { + label: 'Carrion', color: '#cc4422', icon: 'fas fa-biohazard', + nameParts: ['Bloated', 'Feral', 'Stalking', 'Gnawing', 'Lurching', 'Rabid', 'Rotting', 'Hulking'], + nameBase: ['Carrion', 'Carrion', 'Carrien', 'Feeder', 'Gnawer'], + stats: { str:[2,4], agi:[1,2], wit:[1,1], emp:[1,1] }, + health:[3,6], armor:[0,1], damage:[2,3], threat:[1,2], + abilities:[ + 'Pack Aggression — +1 die per Carrion adjacent to the same target', + 'Bite — 2 damage, ignores 1 armour point', + 'Mindless — immune to Panic and social effects', + 'Hard to Kill — rolls Physical Critical even at 0 HP (once per fight)', + 'Swarm — 3+ attack same target simultaneously: +2 damage total', + ] + }, + serialkiller: { + label: 'Serial Killer', color: '#882244', icon: 'fas fa-skull', + nameParts: ['The', 'The', 'The', ''], + nameBase: ['Butcher','Flenser','Ghost','Surgeon','Reaper','Cutter','Shadow','Wraith','Stalker'], + stats: { str:[2,4], agi:[2,4], wit:[2,3], emp:[1,2] }, + health:[3,5], armor:[0,2], damage:[3,4], threat:[3,4], + abilities:[ + 'Signature Weapon — +1 die with preferred weapon type', + 'Predator Focus — ignores cover when attacking chosen target', + 'Surprise Strike — first attack each combat deals +2 damage', + 'Psychotic Endurance — ignores one level of injury penalties', + 'Brutal Efficiency — 3+ successes on attack: target is Stunned', + 'Stealth — 3 free Stealth dice when ambushing from concealment', + ] + }, + monstaret: { + label: 'Monstaret', color: '#226622', icon: 'fas fa-dragon', + nameParts: ['Alpha','Juvenile','Mature','Mutant','Corrupted','Ancient','Feral'], + nameBase: ['Monstaret','Rend-Beast','Fang-Beast','Pit-Thing','Lurker','Crawler','Howler'], + stats: { str:[4,6], agi:[2,3], wit:[1,1], emp:[1,1] }, + health:[6,12], armor:[1,4], damage:[3,5], threat:[3,5], + abilities:[ + 'Natural Weapons — claws/fangs; always counts as armed', + 'Pack Hunter — flanking bonus: +1 die when 2+ present', + 'Toughened Hide — ignore first 1 damage from each attack', + 'Frenzy — at ≤ half HP, +2 damage on all attacks', + 'Leap — charge-attack targets up to 10m away', + 'Predatory Instinct — always acts in initiative phase 1', + ] + }, + machine: { + label: 'Machine', color: '#334488', icon: 'fas fa-robot', + nameParts: ['MK-','Unit-','Model-','DES-','SYS-'], + nameBase: ['7','12','19X','44','3B','Alpha','Delta','Sigma','Rho'], + stats: { str:[3,5], agi:[2,3], wit:[2,4], emp:[0,0] }, + health:[5,10], armor:[3,6], damage:[3,5], threat:[3,5], + abilities:[ + 'Targeting — re-roll one miss die per attack', + 'Weapon Array — attack 2 targets in one action (−1 die each)', + 'Armoured Shell — natural armour 3; stacks before equipped armour', + 'Diagnostic — regain 2 HP at start of turn if not attacked that round', + 'Overload — on death, 3 damage to all within 5m', + 'EMP Resistant — immune to electrical and shock effects', + ] + } +}; + +class SLAGroupNPCTool extends Application { + constructor(...args) { + super(...args); + this._npcs = []; + this._selectedType = 'carrion'; + this._count = 4; + this._root = null; + } + + static get defaultOptions() { + return foundry.utils.mergeObject(super.defaultOptions, { + id: 'sla-group-npc-tool', + title: 'Group NPC Generator', + width: 860, + height: 600, + resizable: true, + classes: ['zero-engine', 'group-npc-tool'] + }); + } + + // ── Utilities ───────────────────────────────────────────────────────────── + _rng(min, max) { return Math.floor(Math.random() * (max - min + 1)) + min; } + _pick(arr) { return arr[Math.floor(Math.random() * arr.length)]; } + _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'; + } + _esc(s) { return String(s).replace(/&/g,'&').replace(/"/g,'"').replace(/ Math.random() - 0.5); + return { + id: foundry.utils.randomID(), typeKey, + typeLabel: arch.label, color: arch.color, icon: arch.icon, + name, str, agi, wit, emp, + hp: hpMax, hpMax, armor, damage, threat, + abilities: pool.slice(0, 2), + notes: '', defeated: false + }; + } + + // ── Save in-progress edits before re-render ─────────────────────────────── + _saveState() { + const root = this._root; + if (!root) return; + root.querySelectorAll('.gnpc-name-input').forEach(el => { + const n = this._npcs.find(x => x.id === el.dataset.npcId); + if (n) n.name = el.value; + }); + root.querySelectorAll('.gnpc-stat-input').forEach(el => { + const n = this._npcs.find(x => x.id === el.dataset.npcId); + if (n && el.dataset.field) n[el.dataset.field] = parseInt(el.value) || 0; + }); + root.querySelectorAll('.gnpc-notes-input').forEach(el => { + const n = this._npcs.find(x => x.id === el.dataset.npcId); + if (n) n.notes = el.value; + }); + } + + // ── Build one NPC card ──────────────────────────────────────────────────── + _buildCard(npc) { + const rgb = this._hexRgb(npc.color); + const hpPct = npc.hpMax > 0 ? Math.max(0, Math.min(100, (npc.hp / npc.hpMax) * 100)) : 0; + const hpColor = hpPct > 60 ? '#44cc66' : hpPct > 30 ? '#ffaa00' : '#cc3333'; + const defeated = npc.defeated ? ' gnpc-defeated' : ''; + const abils = npc.abilities.map(a => + `