fix: Group NPC — embed weapons/armor/abilities on created actors

- Weapon items created as embedded weapon Items with proper schema
  (melee vs ranged category, damage, AP, range, ROF from note field)
- Armor items created for NPCs with armour description (skips
  Built-in for machines — they use the armor stat directly)
- Ability items created as specialty Items showing in Stats tab
- Biography formatted with armour, weapons, abilities sections

- Card backgrounds now tinted by type colour (very subtle 7% opacity)
  so Carrion/Gang/Monstaret/Machine are visually distinct at a glance

- Count selector replaced with number input (1–10) instead of dropdown
This commit is contained in:
2026-06-03 17:51:54 +01:00
parent bf5c82359b
commit 5482acd2f9
3 changed files with 94 additions and 13 deletions
+8
View File
@@ -4538,3 +4538,11 @@ body > .ze-pause-root .ze-sat-inner {
font-size: 10.5px; color: #aaa88a; padding: 1px 2px;
}
.group-npc-tool .gnpc-armor-row i { margin-right: 4px; color: #887755; }
/* Group NPC — count as number input */
.group-npc-tool .gnpc-count-sel {
width: 44px; text-align: center;
background: rgba(255,255,255,0.08); border: 1px solid #555;
color: #eee; border-radius: 3px; padding: 3px 4px; font-size: 13px;
font-weight: bold;
}
+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.5.3",
"version": "1.5.4",
"compatibility": {
"minimum": 13,
"verified": "14.359"
+85 -12
View File
@@ -6137,7 +6137,8 @@ class SLAGroupNPCTool extends Application {
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 defeated = npc.defeated ? ' gnpc-defeated' : '';
const bgStyle = `border-left:3px solid ${npc.color};background:rgba(${rgb},0.07)`;
// Natural weapons (creatures only)
const natHtml = (npc.naturalWeapons ?? []).map(w => {
@@ -6160,7 +6161,7 @@ class SLAGroupNPCTool extends Application {
? `<div class="gnpc-armor-row"><i class="fas fa-shield-alt"></i> ${this._esc(npc.armorDesc)}</div>` : '';
return `
<div class="gnpc-card${defeated}" data-npc-id="${npc.id}" style="border-left:3px solid ${npc.color}">
<div class="gnpc-card${defeated}" data-npc-id="${npc.id}" style="${bgStyle}">
<div class="gnpc-card-top">
<input class="gnpc-name-input" type="text" data-npc-id="${npc.id}" value="${this._esc(npc.name)}" />
<span class="gnpc-type-tag" style="background:rgba(${rgb},0.15);color:${npc.color};border-color:${npc.color}80">
@@ -6219,9 +6220,7 @@ class SLAGroupNPCTool extends Application {
<i class="${a.icon}"></i> ${a.label}</button>`;
}).join('');
const countOpts = [1,2,3,4,5,6,8,10].map(n =>
`<option value="${n}"${n === this._count ? ' selected' : ''}>${n}</option>`
).join('');
const countVal = this._count;
const alive = this._npcs.filter(n => !n.defeated).length;
const defeated = this._npcs.filter(n => n.defeated).length;
@@ -6250,7 +6249,7 @@ class SLAGroupNPCTool extends Application {
<div class="gnpc-toolbar">
<div class="gnpc-type-row">${typeBtns}</div>
<div class="gnpc-action-row">
<label class="gnpc-count-lbl">Count <select class="gnpc-count-sel">${countOpts}</select></label>
<label class="gnpc-count-lbl">Count <input type="number" class="gnpc-count-sel" min="1" max="10" value="${countVal}" /></label>
<button type="button" class="gnpc-gen-btn"><i class="fas fa-random"></i> Generate</button>
<button type="button" class="gnpc-add-btn"><i class="fas fa-plus"></i> Add More</button>
<button type="button" class="gnpc-clr-btn"><i class="fas fa-trash-alt"></i> Clear All</button>
@@ -6276,9 +6275,9 @@ class SLAGroupNPCTool extends Application {
})
);
// Count selector
root.querySelector('.gnpc-count-sel')?.addEventListener('change', e => {
this._count = parseInt(e.target.value) || 4;
// Count selector (number input, 1–10)
root.querySelector('.gnpc-count-sel')?.addEventListener('input', e => {
this._count = Math.max(1, Math.min(10, parseInt(e.target.value) || 1));
});
// Generate (replace all)
@@ -6396,17 +6395,33 @@ class SLAGroupNPCTool extends Application {
})
);
// Create as World Actors
// Create as World Actors — with embedded weapon, armor, and specialty items
root.querySelector('.gnpc-create-btn')?.addEventListener('click', async () => {
if (!game.user?.isGM) return;
this._saveState();
let created = 0;
for (const npc of this._npcs) {
try {
await Actor.create({
// Formatted biography
const bio = [];
bio.push(`Type: ${npc.typeLabel} | Threat: ${npc.threat}`);
if (npc.armorDesc && npc.armorDesc !== 'None') bio.push(`Armour: ${npc.armorDesc}`);
const allWep = [...(npc.naturalWeapons ?? []), ...(npc.weapons ?? [])];
if (allWep.length) {
bio.push('\nWeapons:');
allWep.forEach(w => bio.push(` ${w.name} — DMG ${w.damage}${w.ap ? ` AP ${w.ap}` : ''}${w.range ? ` [${w.range}]` : ''}${w.note ? ` — ${w.note}` : ''}`));
}
if (npc.abilities?.length) {
bio.push('\nAbilities:');
npc.abilities.forEach(a => bio.push(` ${a}`));
}
if (npc.notes) bio.push(`\nNotes:\n ${npc.notes}`);
// Create base actor
const actorDoc = await Actor.create({
name: npc.name, type: 'npc',
system: {
biography: `Type: ${npc.typeLabel}\n\nAbilities:\n${npc.abilities.join('\n')}${npc.notes ? '\n\nNotes:\n' + npc.notes : ''}`,
biography: bio.join('\n'),
threat: npc.threat,
attributes: { strength: npc.str, agility: npc.agi, wits: npc.wit, empathy: npc.emp },
health: { value: npc.hp, max: npc.hpMax },
@@ -6414,6 +6429,64 @@ class SLAGroupNPCTool extends Application {
damage: npc.damage
}
});
if (!actorDoc) continue;
// Build embedded items
const embeds = [];
// Weapon items (natural + conventional/integrated)
for (const w of allWep) {
const isRanged = w.range && w.range !== 'Engaged';
const rofMatch = w.note?.match(/ROF\s*(\d+)/i);
embeds.push({
name: w.name, type: 'weapon',
img: isRanged
? 'icons/weapons/guns/gun-pistol-flintlock-metal.webp'
: 'icons/weapons/daggers/dagger-straight-sharp.webp',
system: {
description: w.note || '', weaponType: w.name,
category: isRanged ? 'ranged' : 'melee',
damage: w.damage, range: w.range || 'Engaged', ap: w.ap || 0,
gearBonus: 0, rof: rofMatch ? parseInt(rofMatch[1]) : 1,
magazine: isRanged ? 12 : 0, ammo: isRanged ? 12 : 0,
fireModes: isRanged ? ['single'] : [], equipped: true,
ammoType: 'standard', autoAmmoUse: 8, initiativeMod: 0, ammoEmpty: false
}
});
}
// Armor item (skip Built-in; machines use their armor stat directly)
if (npc.armor > 0 && npc.armorDesc && npc.armorDesc !== 'None' && !npc.armorDesc.startsWith('Built-in')) {
const armorName = npc.armorDesc.replace(/\s*\(ARM \d+\)/, '').trim() || 'Armour';
embeds.push({
name: armorName, type: 'armor',
img: 'icons/equipment/chest/vest-simple-leather-brown.webp',
system: {
description: npc.armorDesc, armorDice: npc.armor, armorAuto: 0,
armorRating: npc.armor, statMod: 0, skillMod: 0, statModTarget: '',
skillModTarget: '', healthMod: 0, resolveMod: 0, initiativeMod: 0, equipped: true
}
});
}
// Specialty items for abilities
for (const ability of (npc.abilities ?? [])) {
const title = ability.match(/^([^—\-]+)/)?.[1]?.trim() ?? ability.slice(0, 30);
embeds.push({
name: title, type: 'specialty',
img: 'icons/skills/social/intimidation-impressing.webp',
system: {
description: ability, effects: ability, category: 'general',
package: npc.typeLabel, prerequisites: '', isActive: true,
healthMod: 0, resolveMod: 0
}
});
}
if (embeds.length) {
try { await actorDoc.createEmbeddedDocuments('Item', embeds); }
catch(e) { console.warn(`Zero Engine | Group NPC: embed failed for "${npc.name}"`, e); }
}
created++;
} catch(err) {
console.error(`Zero Engine | Group NPC: failed to create "${npc.name}"`, err);