Files
tevansandClaude Opus 4.8 9a55991ad4 Blueprint News 0 Engine v0.5.0 — BPN generator + Zero Engine NPC actors
Based on sla-blueprint-news. On Create, in a zero-engine world, also spawns
playable Zero Engine npc actors for the briefing: a threat group (named leader
+ mooks) matched to the BPN opposition (gangs, Carrien, corpsec, Stormers,
black-ops...) plus the named cast — each with attributes, hit points, armour,
damage, threat and embedded weapons. Stat blocks mirror the system's own NPC
Creator (SLA_NPC_TYPES / _slaWeaponData). De-collided ids/globals/buttons so it
coexists with the base module.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 19:00:30 +01:00

172 lines
11 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Zero Engine adapter for Blueprint News 0 Engine.
*
* Turns a generated BPN into real Zero Engine (system id `zero-engine`) NPC
* actors — gangs, Carrien, corp security, and the named cast — with
* attributes, hit points, armour, damage, threat level and embedded weapons.
*
* The actor + weapon shapes mirror the Zero Engine system's own NPC Creator
* (SLA_NPC_TYPES / _slaWeaponData / _createNPC in zero-engine.mjs) so the
* results are identical to what the system would build itself. Initiative is
* rolled by the Zero Engine in combat (driven by Agility + Mobility), which
* these actors have.
*/
/* Weapon presets — copied from the Zero Engine system's _slaWeaponData. */
const ZERO_WEAPONS = {
pistol: { name: "Blitzer 10mm Pistol", weaponType: "Pistol", category: "ranged", damage: 2, range: "Short", rof: 2, ap: 0, magazine: 15, ammo: 15, fireModes: ["single"], cost: 300, img: "icons/weapons/guns/gun-pistol-flintlock-metal.webp" },
rifle: { name: "M300 Assault Rifle", weaponType: "Rifle", category: "ranged", damage: 3, range: "Long", rof: 3, ap: 1, magazine: 30, ammo: 30, fireModes: ["single", "burst", "auto"], cost: 600, img: "icons/weapons/guns/rifle-bayonet.webp" },
smg: { name: "Blitzer SMG", weaponType: "SMG", category: "ranged", damage: 2, range: "Short", rof: 3, ap: 0, magazine: 30, ammo: 30, fireModes: ["single", "auto"], cost: 450, img: "icons/weapons/guns/gun-pistol-flintlock-metal.webp" },
heavy_melee: { name: "Power Blade", weaponType: "Blade", category: "melee", damage: 3, range: "Engaged", rof: 1, ap: 2, magazine: 0, ammo: 0, fireModes: [], cost: 400, img: "icons/weapons/swords/greatsword.webp" },
knife: { name: "Combat Knife", weaponType: "Knife", category: "melee", damage: 2, range: "Engaged", rof: 2, ap: 0, magazine: 0, ammo: 0, fireModes: [], cost: 80, img: "icons/weapons/daggers/dagger-straight-sharp.webp" },
baton: { name: "Shiver Baton", weaponType: "Baton", category: "melee", damage: 2, range: "Engaged", rof: 2, ap: 0, magazine: 0, ammo: 0, fireModes: [], cost: 50, img: "icons/weapons/clubs/club-simple-black.webp" },
claws: { name: "Claws", weaponType: "Natural", category: "melee", damage: 3, range: "Engaged", rof: 2, ap: 0, magazine: 0, ammo: 0, fireModes: [], cost: 0, img: "icons/skills/melee/unarmed-punch-fist.webp" },
blade: { name: "Mono-Edge Blade", weaponType: "Blade", category: "melee", damage: 3, range: "Engaged", rof: 2, ap: 2, magazine: 0, ammo: 0, fireModes: [], cost: 350, img: "icons/weapons/swords/sword-katana-gleaming.webp" },
heavy_weapon: { name: "Heavy Autocannon", weaponType: "HW", category: "ranged", damage: 5, range: "Long", rof: 2, ap: 3, magazine: 20, ammo: 20, fireModes: ["burst", "auto"], cost: 2500, img: "icons/weapons/guns/gun-blunderbuss.webp" }
};
function weaponItem(type) {
const base = ZERO_WEAPONS[type] || ZERO_WEAPONS.pistol;
const { img, ...sys } = base;
return {
name: base.name,
type: "weapon",
img: img || "icons/svg/item.svg",
system: { ...sys, description: "", equipped: true, gearBonus: 0, initiativeMod: 0, ammoType: "standard", autoAmmoUse: 8 }
};
}
/* Stat profiles — the Zero Engine SLA_NPC_TYPES set, plus a Stormer profile
the base set lacks. attrs clamp 2–5, armour 0–6, hp scales with threat. */
const ZERO_PROFILES = {
civilian: { label: "Civilian", threat: 1, attrs: { strength: 2, agility: 2, wits: 2, empathy: 2 }, skills: { observation: 1 }, hp: 4, armor: 0, weapons: [] },
gang_grunt: { label: "Gang Grunt", threat: 1, attrs: { strength: 3, agility: 2, wits: 2, empathy: 2 }, skills: { melee: 2, force: 1 }, hp: 5, armor: 1, weapons: ["pistol", "knife"] },
shiver: { label: "Shiver (Patrol)", threat: 2, attrs: { strength: 3, agility: 3, wits: 2, empathy: 2 }, skills: { marksmanship: 2, melee: 1, observation: 1 }, hp: 6, armor: 2, weapons: ["pistol", "baton"] },
operative_green: { label: "SLA Operative (Green)", threat: 2, attrs: { strength: 2, agility: 3, wits: 2, empathy: 2 }, skills: { marksmanship: 2, mobility: 1, observation: 1 }, hp: 5, armor: 2, weapons: ["pistol", "knife"] },
carrien: { label: "Carrien", threat: 2, attrs: { strength: 3, agility: 4, wits: 1, empathy: 1 }, skills: { melee: 3, mobility: 2 }, hp: 7, armor: 1, weapons: ["claws"] },
operative_veteran: { label: "SLA Operative (Vet)", threat: 3, attrs: { strength: 3, agility: 3, wits: 3, empathy: 2 }, skills: { marksmanship: 3, melee: 2, stealth: 2, observation: 2 }, hp: 8, armor: 3, weapons: ["rifle", "pistol", "knife"] },
stormer: { label: "Stormer", threat: 3, attrs: { strength: 4, agility: 3, wits: 2, empathy: 1 }, skills: { melee: 3, force: 2, stamina: 2 }, hp: 11, armor: 3, weapons: ["heavy_melee", "smg"] },
dark_finder: { label: "Dark Finder", threat: 4, attrs: { strength: 4, agility: 3, wits: 3, empathy: 2 }, skills: { marksmanship: 4, melee: 3, stealth: 3, observation: 3 }, hp: 10, armor: 4, weapons: ["rifle", "pistol", "blade"] },
manchine: { label: "Manchine", threat: 5, attrs: { strength: 5, agility: 4, wits: 3, empathy: 1 }, skills: { melee: 4, marksmanship: 3, force: 4, stamina: 4 }, hp: 14, armor: 5, weapons: ["heavy_weapon", "claws"] }
};
/* BPN threat archetype → encounter group (a leader + a number of mooks). */
const THREAT_GROUPS = {
ganger: { leader: "gang_grunt", mook: "gang_grunt", mooks: [3, 5] },
raider: { leader: "operative_green", mook: "gang_grunt", mooks: [3, 5] },
killer: { leader: "operative_veteran", mook: "operative_green", mooks: [1, 2] },
corpsec: { leader: "operative_veteran", mook: "shiver", mooks: [3, 4] },
security: { leader: "shiver", mook: "shiver", mooks: [3, 4] },
riot: { leader: "gang_grunt", mook: "gang_grunt", mooks: [5, 6] },
blackops: { leader: "dark_finder", mook: "operative_veteran", mooks: [2, 3] },
stormer: { leader: "manchine", mook: "stormer", mooks: [1, 2] },
carrien: { leader: "carrien", mook: "carrien", mooks: [3, 5] },
"retrieval-crew": { leader: "operative_veteran", mook: "operative_green", mooks: [2, 3] }
};
/* Map a cast member's role text to a sensible non-combat profile. */
function contactProfile(role = "") {
const r = String(role).toLowerCase();
if (r.includes("shiver") || r.includes("security") || r.includes("enforcer")) return "shiver";
if (r.includes("operative") || r.includes("fixer") || r.includes("runner") || r.includes("financier")) return "operative_green";
if (r.includes("scientist") || r.includes("tech") || r.includes("medic") || r.includes("clerk") || r.includes("controller")) return "civilian";
return "civilian";
}
/** Build a Zero Engine npc actor payload from a profile, scaled to a target
* threat. Mirrors the system's _createNPC (derivedStats, skills, flux). */
function npcActorData(profileKey, { threat, name, biography } = {}) {
const profile = ZERO_PROFILES[profileKey] || ZERO_PROFILES.gang_grunt;
const t = Math.max(1, Math.min(6, threat ?? profile.threat));
const delta = t - profile.threat;
const attrs = {};
for (const [k, v] of Object.entries(profile.attrs)) attrs[k] = Math.max(2, Math.min(5, v + delta));
const hp = Math.max(1, profile.hp + delta * 2);
const armor = Math.max(0, Math.min(6, profile.armor + delta));
const resolve = Math.max(2, attrs.wits + attrs.empathy);
return {
actor: {
name: name || `${profile.label} #${Math.floor(Math.random() * 900 + 100)}`,
type: "npc",
system: {
biography: biography || "",
threat: t,
attributes: { ...attrs },
health: { value: hp, max: hp },
derivedStats: { health: { value: hp, max: hp }, resolve: { value: resolve, max: resolve } },
skills: { ...profile.skills },
armor,
damage: Math.max(2, attrs.strength),
flux: profile.flux ? { value: profile.flux, max: profile.flux } : { value: 0, max: 0 }
}
},
// 'claws' is a natural attack reflected by the damage stat — not an item.
weapons: (profile.weapons || []).filter((w) => w !== "claws").map(weaponItem),
label: profile.label
};
}
function randInt(lo, hi) { return Math.floor(Math.random() * (hi - lo + 1)) + lo; }
/** True when actor creation is possible (running under the Zero Engine). */
export function zeroEngineActive() {
return game.system?.id === "zero-engine";
}
/**
* Create the BPN's NPC actors in a folder. Returns { created, folder } or
* { created: 0, skipped: true } when not on the Zero Engine.
*/
export async function createZeroActors(bpn) {
if (!zeroEngineActive()) return { created: 0, skipped: true };
const base = `${bpn.colour.label} BPN — ${bpn.shortTitle}`;
const folder = await Folder.create({ name: base, type: "Actor", color: bpn.colour.accent });
const fid = folder.id;
const builds = [];
// 1) The threat group: a named leader plus a number of mooks.
const threatKey = bpn.npcSeam?.threatArchetype || "ganger";
const group = THREAT_GROUPS[threatKey] || THREAT_GROUPS.ganger;
const leaderProfile = ZERO_PROFILES[group.leader] || ZERO_PROFILES.gang_grunt;
const leaderName = bpn.gm?.threatLead || `${leaderProfile.label} Leader`;
builds.push(npcActorData(group.leader, {
threat: leaderProfile.threat + 1,
name: leaderName,
biography: `Threat lead for ${base}.\n${bpn.opposition}\n${bpn.gm?.threatCountLabel ?? ""}`.trim()
}));
const mookCount = randInt(group.mooks[0], group.mooks[1]);
const mookProfile = ZERO_PROFILES[group.mook] || ZERO_PROFILES.gang_grunt;
for (let i = 0; i < mookCount; i++) {
builds.push(npcActorData(group.mook, {
name: `${mookProfile.label} ${i + 1}`,
biography: `Part of the opposition for ${base}.`
}));
}
// 2) The named cast (skip the threat lead — already built above as leader).
for (const npc of (bpn.cast || [])) {
if (npc.role === "Designated threat") continue;
builds.push(npcActorData(contactProfile(npc.role), {
name: npc.name,
biography: `${npc.role} · ${npc.disposition}\n${npc.description}\nSignificance: ${npc.significance}`
}));
}
// Create actors, then embed their weapons.
let created = 0;
for (const b of builds) {
try {
const actor = await Actor.create({ ...b.actor, folder: fid });
if (actor && b.weapons.length) await actor.createEmbeddedDocuments("Item", b.weapons);
created++;
} catch (err) {
console.warn("Blueprint News 0E | actor create failed:", err);
}
}
return { created, folder, threatLabel: leaderProfile.label, mookCount, mookLabel: mookProfile.label };
}