Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
25222fdc3a | ||
|
|
5a345534c7 |
+492
-21
@@ -457,6 +457,286 @@ const ARCHETYPES = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Carrien Salvaged Weapons & Attacks Database
|
||||||
|
// ============================================================================
|
||||||
|
const CARRIEN_WEAPONS = [
|
||||||
|
{ name: 'Feral Claws & Bite', damage: '1d10+3', ammo: 0, shots: 0, useAmmo: false, description: 'Sharp dirt-clogged nails and jagged teeth. Aggressive close-quarters rending.' },
|
||||||
|
{ name: 'Rust-Machete', damage: '1d8+3', ammo: 0, shots: 0, useAmmo: false, description: 'A heavy piece of salvaged structural plating ground into a brutal, jagged cleaver.' },
|
||||||
|
{ name: 'Scrap-Iron Club', damage: '1d8+4', ammo: 0, shots: 0, useAmmo: false, description: 'A rusted concrete-reinforcement bar (rebar). High blunt force damage.' },
|
||||||
|
{ name: 'Rusted Harpoon', damage: '1d10+2', ammo: 0, shots: 0, useAmmo: false, description: 'A sharp, barbed sewage tip wired to a long pole. Allows reach/thrust attacks.' },
|
||||||
|
{ name: 'Spiked Chain', damage: '1d8+2', ammo: 0, shots: 0, useAmmo: false, description: 'A heavy industrial drive chain wrapped with jagged scrap and rusted bolts.' },
|
||||||
|
{ name: 'Feral Acid Spit', damage: '1d6', ammo: 0, shots: 0, useAmmo: false, description: 'A burning chemical runoff secretion. Direct corrosive biological projectile.' },
|
||||||
|
{ name: 'Scrap Blunderbuss', damage: '2d6', ammo: 1, shots: 1, useAmmo: true, ammoType: 'Shotgun', description: 'A pipe jammed with gunpowder and packed with gravel, bolts, and broken glass.' },
|
||||||
|
{ name: 'Undercity Cleaver', damage: '1d6+4', ammo: 0, shots: 0, useAmmo: false, description: 'An enormous rusted butcher blade scavenged from abandoned sector packaging plants.' },
|
||||||
|
{ name: 'Feral Shock Prod', damage: '1d8', ammo: 5, shots: 1, useAmmo: true, ammoType: 'Battery', description: 'A broken sector perimeter baton held together with copper wire. Emits electrical discharge.' },
|
||||||
|
{ name: 'Sewer Crossbow', damage: '1d10', ammo: 1, shots: 1, useAmmo: true, ammoType: 'Bolt', description: 'A heavy leaf-spring mechanical bow firing sharp scrap iron bolts.' },
|
||||||
|
{ name: 'Pneumatic Nail-Gun', damage: '1d8', ammo: 10, shots: 1, useAmmo: true, ammoType: 'Nails', description: 'Repurposed industrial nail-gun powered by a scrap gas canister.' },
|
||||||
|
{ name: 'Pipe-Bomb', damage: '2d10', ammo: 1, shots: 1, useAmmo: true, ammoType: 'Grenade', description: 'A metal pipe capped at both ends, filled with fertilizer explosive and matchheads.' },
|
||||||
|
{ name: 'Scavenged GA Pistol', damage: '1d10+2', ammo: 15, shots: 1, useAmmo: true, ammoType: 'Pistol', description: 'A standard Shiver sidearm, dirty, worn, but functional.' },
|
||||||
|
{ name: 'Razor-Wire Whip', damage: '1d6+3', ammo: 0, shots: 0, useAmmo: false, description: 'Coils of rusty barbed wire wrapped around a handle.' }
|
||||||
|
];
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// SLA Industries Carrien Breeds Database
|
||||||
|
// ============================================================================
|
||||||
|
const CARRIEN_BREEDS = {
|
||||||
|
feral: {
|
||||||
|
name: 'Sewer Feral',
|
||||||
|
speciesLabel: 'Feral Carrien',
|
||||||
|
description: 'The standard, starved undercity scavenger. Highly aggressive and operates in packs.',
|
||||||
|
statMods: { strength: 0, speed: 0, intellect: 0, combat: 0, body: 0 },
|
||||||
|
skills: [{ name: 'Stealth', rank: 'Trained', bonus: 10 }],
|
||||||
|
allowedWeapons: ['Feral Claws & Bite', 'Scrap-Iron Club', 'Rust-Machete', 'Spiked Chain'],
|
||||||
|
dispositions: ['Feral', 'Starving', 'Aggressive', 'Territorial'],
|
||||||
|
threat: 'Moderate',
|
||||||
|
trait: {
|
||||||
|
name: 'Pack Tactics',
|
||||||
|
effects: 'Gain a bonus to melee attacks when another allied Carrien is close to the target.',
|
||||||
|
package: 'Carrien Breed Trait'
|
||||||
|
},
|
||||||
|
skulls: [
|
||||||
|
'a cracked pig skull tied with greasy wire',
|
||||||
|
'a yellowed dog skull with broken teeth',
|
||||||
|
'a mud-covered goat skull with one horn snapped off',
|
||||||
|
'a foul scavenger bird skull with trailing industrial ribbons'
|
||||||
|
],
|
||||||
|
heightRange: [1.5, 1.7],
|
||||||
|
weightRange: [50, 70],
|
||||||
|
ageRange: [3, 9]
|
||||||
|
},
|
||||||
|
scout: {
|
||||||
|
name: 'Undercity Scout',
|
||||||
|
speciesLabel: 'Feral Carrien',
|
||||||
|
description: 'A nimble, silent pack-runner specialized in exploring sector cracks and locating prey.',
|
||||||
|
statMods: { strength: -5, speed: 10, intellect: 2, combat: 5, body: -5 },
|
||||||
|
skills: [
|
||||||
|
{ name: 'Stealth', rank: 'Expert', bonus: 15 },
|
||||||
|
{ name: 'Observation', rank: 'Trained', bonus: 10 }
|
||||||
|
],
|
||||||
|
allowedWeapons: ['Sewer Crossbow', 'Feral Acid Spit', 'Rust-Machete'],
|
||||||
|
dispositions: ['Feral', 'Suspicious', 'Alert', 'Skittish'],
|
||||||
|
threat: 'Moderate',
|
||||||
|
trait: {
|
||||||
|
name: 'Undercity Camouflage',
|
||||||
|
effects: 'Virtually invisible in dark sewers and tunnels. Gain a +15 bonus to Stealth checks in dim light.',
|
||||||
|
package: 'Carrien Breed Trait'
|
||||||
|
},
|
||||||
|
skulls: [
|
||||||
|
'a sleek, elongated rodent skull',
|
||||||
|
'a small feral cat skull with copper wire whiskers',
|
||||||
|
'a bird skull with scrap industrial goggles wired over the eye sockets',
|
||||||
|
'a split badger skull bound in dirty insulation tape'
|
||||||
|
],
|
||||||
|
heightRange: [1.4, 1.6],
|
||||||
|
weightRange: [42, 60],
|
||||||
|
ageRange: [4, 10]
|
||||||
|
},
|
||||||
|
greater: {
|
||||||
|
name: 'Greater Hulk',
|
||||||
|
speciesLabel: 'Greater Carrien',
|
||||||
|
description: 'A massive, heavily muscled alpha breed that towers over standard feral pack-runners.',
|
||||||
|
statMods: { strength: 15, speed: -4, intellect: -2, combat: 10, body: 15 },
|
||||||
|
skills: [
|
||||||
|
{ name: 'Melee Combat', rank: 'Expert', bonus: 20 },
|
||||||
|
{ name: 'Intimidation', rank: 'Trained', bonus: 15 }
|
||||||
|
],
|
||||||
|
allowedWeapons: ['Undercity Cleaver', 'Rust-Machete', 'Rusted Harpoon', 'Scrap-Iron Club'],
|
||||||
|
dispositions: ['Aggressive', 'Hostile', 'Territorial', 'Feral'],
|
||||||
|
threat: 'High',
|
||||||
|
trait: {
|
||||||
|
name: 'Hulking Charge',
|
||||||
|
effects: 'If this creature moves at least 10 meters before a Melee hit, the target must make a Strength check or be knocked prone.',
|
||||||
|
package: 'Carrien Breed Trait'
|
||||||
|
},
|
||||||
|
skulls: [
|
||||||
|
'a huge, blood-stained bull skull with broad cracked horns',
|
||||||
|
'a massive armored boar skull with protruding rusted iron tusks',
|
||||||
|
'a thick-plated stormer head skull scavenged from a corporate landfill',
|
||||||
|
'a heavy industrial helmet welded over a split canine skull'
|
||||||
|
],
|
||||||
|
heightRange: [2.1, 2.4],
|
||||||
|
weightRange: [130, 185],
|
||||||
|
ageRange: [6, 12]
|
||||||
|
},
|
||||||
|
ursa: {
|
||||||
|
name: 'Ursa Raider',
|
||||||
|
speciesLabel: 'Ursa Carrien',
|
||||||
|
description: 'An evolved, highly intelligent strain of Carrien capable of complex tactics and mechanical salvage.',
|
||||||
|
statMods: { strength: 5, speed: 4, intellect: 12, combat: 8, body: 5 },
|
||||||
|
skills: [
|
||||||
|
{ name: 'Melee Combat', rank: 'Trained', bonus: 10 },
|
||||||
|
{ name: 'Firearms', rank: 'Trained', bonus: 10 }
|
||||||
|
],
|
||||||
|
allowedWeapons: ['Scrap Blunderbuss', 'Sewer Crossbow', 'Feral Shock Prod', 'Rust-Machete'],
|
||||||
|
dispositions: ['Cunning', 'Hostile', 'Disciplined', 'Aggressive'],
|
||||||
|
threat: 'High',
|
||||||
|
trait: {
|
||||||
|
name: 'Scrap Mastery',
|
||||||
|
effects: 'Expertly utilizes salvaged firearms. Weapons cannot jam due to critical failure, and reload speed is halved.',
|
||||||
|
package: 'Carrien Breed Trait'
|
||||||
|
},
|
||||||
|
skulls: [
|
||||||
|
'a human skull with SLA operative logo painted in white grease',
|
||||||
|
'a refined metal-reinforced dog skull with riveted copper plates',
|
||||||
|
'a split ram skull with thick coiled horns wrapped in barbed wire',
|
||||||
|
'a vintage industrial gas mask bolted directly to a pristine pig skull'
|
||||||
|
],
|
||||||
|
heightRange: [1.7, 1.9],
|
||||||
|
weightRange: [75, 95],
|
||||||
|
ageRange: [5, 14]
|
||||||
|
},
|
||||||
|
domesticated: {
|
||||||
|
name: 'Domesticated Operative',
|
||||||
|
speciesLabel: 'Advanced Carrien',
|
||||||
|
description: 'An advanced Carrien captured, trained, and conditioned by SLA to act as specialized contract support.',
|
||||||
|
statMods: { strength: 2, speed: 5, intellect: 15, combat: 10, body: 2 },
|
||||||
|
skills: [
|
||||||
|
{ name: 'Firearms', rank: 'Expert', bonus: 15 },
|
||||||
|
{ name: 'Unarmed Combat', rank: 'Trained', bonus: 10 }
|
||||||
|
],
|
||||||
|
allowedWeapons: ['Scavenged GA Pistol', 'Feral Shock Prod', 'Rust-Machete'],
|
||||||
|
dispositions: ['Disciplined', 'Professional', 'Cold', 'Alert'],
|
||||||
|
threat: 'Moderate',
|
||||||
|
trait: {
|
||||||
|
name: 'SLA Conditioning',
|
||||||
|
effects: 'Conditioned against mental fracture. Gain +15 to Resolve, and immune to Panic checks triggered by combat damage.',
|
||||||
|
package: 'Carrien Breed Trait'
|
||||||
|
},
|
||||||
|
skulls: [
|
||||||
|
'a sleek fiberglass replica skull branded with the SLA Industries seal',
|
||||||
|
'a pristine porcelain canine skull mask',
|
||||||
|
'a subdued tactical helmet with an integrated visor built into a small goat skull',
|
||||||
|
'no skull mask — wearing a standard tactical balaclava with carbon fiber cheek guards'
|
||||||
|
],
|
||||||
|
heightRange: [1.6, 1.8],
|
||||||
|
weightRange: [65, 85],
|
||||||
|
ageRange: [6, 16]
|
||||||
|
},
|
||||||
|
mutant: {
|
||||||
|
name: 'Mutant Scourge',
|
||||||
|
speciesLabel: 'Mutant Carrien',
|
||||||
|
description: 'A terrifying Carrien warped by high-level toxic sludge. Unpredictable and wildly dangerous.',
|
||||||
|
statMods: { strength: 10, speed: 6, intellect: -5, combat: 12, body: 10 },
|
||||||
|
skills: [
|
||||||
|
{ name: 'Melee Combat', rank: 'Expert', bonus: 15 },
|
||||||
|
{ name: 'Intimidation', rank: 'Expert', bonus: 15 }
|
||||||
|
],
|
||||||
|
allowedWeapons: ['Feral Claws & Bite', 'Feral Acid Spit', 'Feral Shock Prod', 'Spiked Chain'],
|
||||||
|
dispositions: ['Unstable', 'Hostile', 'Manic', 'Demented'],
|
||||||
|
threat: 'Extreme',
|
||||||
|
trait: {
|
||||||
|
name: 'Corrosive Backlash',
|
||||||
|
effects: 'When damaged by a melee slashing or piercing attack, deals 1d6 corrosive acid splash damage to the attacker.',
|
||||||
|
package: 'Carrien Breed Trait'
|
||||||
|
},
|
||||||
|
skulls: [
|
||||||
|
'a glowing, irradiated wolf skull showing weeping chemical pustules',
|
||||||
|
'a melted, half-fused mutant skull with wire staples',
|
||||||
|
'a hideous double-jawed dog skull mask',
|
||||||
|
'two bird skulls wired side-by-side to cover a bloated mutant face'
|
||||||
|
],
|
||||||
|
heightRange: [1.8, 2.1],
|
||||||
|
weightRange: [90, 130],
|
||||||
|
ageRange: [2, 6]
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Dossier & Biography generator for SLA Industries Sheet Integration
|
||||||
|
// ============================================================================
|
||||||
|
function buildDossierData(archetypeKey, species, name, background, breed = null) {
|
||||||
|
const cap = (s) => String(s ?? "").charAt(0).toUpperCase() + String(s ?? "").slice(1);
|
||||||
|
const speciesLabel = cap(species === 'human' && archetypeKey === 'carrien' ? 'Advanced Carrien' : species);
|
||||||
|
|
||||||
|
if (archetypeKey === 'carrien' && breed) {
|
||||||
|
return {
|
||||||
|
role: { value: breed.name },
|
||||||
|
archetype: { value: 'Carrien Threat' },
|
||||||
|
species: { value: breed.speciesLabel },
|
||||||
|
affiliation: { value: pick(['Hostile Sewer Swarm', 'Deep Undercity Feral Pack', 'Waste Sector Warrens', 'Undercity Marauders']) },
|
||||||
|
department: { value: pick(['Lower Sewer Grid', 'Pipeline Access Shafts', 'Abandoned Drainage Hubs']) },
|
||||||
|
sector: { value: pick(['Cannibal Sector 1', 'Cannibal Sector 2', 'Lower Sewer Levels', 'Deep Undercity']) },
|
||||||
|
disposition: { value: pick(breed.dispositions) },
|
||||||
|
threat: { value: breed.threat },
|
||||||
|
summary: { value: background }
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const roles = {
|
||||||
|
civilian: ['Hab-block Clerk', 'Street Tech Merchant', 'Recycling Loader', 'Transit Gatekeeper', 'Off-duty Laborer', 'Dossier Runner'],
|
||||||
|
shiver: ['Precinct Patrol Officer', 'Armed Response Trooper', 'Cordon Watch Sergeant', 'Sector Patrol Ranger', 'Security Escort Detail'],
|
||||||
|
carrien: ['Pack Stalker', 'Undercity Runner', 'Tunnel Ambusher', 'Nest Scout', 'Nest Alpha'],
|
||||||
|
gang: ['Street Turf Lookout', 'Block Crew Muscle', 'Corner Runner', 'Street Debt Collector', 'Back-alley Brawler'],
|
||||||
|
corporate: ['Departmental Fixer', 'Karma Lead Scientist', 'Sponsor Asset Overseer', 'Case Auditor', 'Blue Corp Special Envoy'],
|
||||||
|
mutant: ['Canker Street Carver', 'Block Eight Ripper', 'Substation Bleeder', 'Duct Work Phantom', 'Basement Surgeon']
|
||||||
|
}[archetypeKey] ?? ['Dossier Subject'];
|
||||||
|
|
||||||
|
const affiliations = {
|
||||||
|
civilian: ['Mort Civil Sector', 'Block Hab Committee', 'Public Transit Utility', 'Independent Trader'],
|
||||||
|
shiver: ['SLA Industries (Shiver Division)', 'Precinct 88 Wardens', 'Sector Cordon Detail'],
|
||||||
|
carrien: ['Hostile Sewer Swarm', 'Deep Undercity Feral Pack', 'Waste Sector Marauders'],
|
||||||
|
gang: ['Chrome Jacks', 'Sump Dogs', 'Red Wire Crew', 'Murder Mile Kings', 'Ash Runners'],
|
||||||
|
corporate: ['SLA Industries Operations', 'MediCore Containment Division', 'Karma Special Projects', 'White Corp Administration'],
|
||||||
|
mutant: ['Independent / Disavowed', 'Active Outcast / Hostile', 'Unregistered Threat Profile']
|
||||||
|
}[archetypeKey] ?? ['Independent'];
|
||||||
|
|
||||||
|
const departments = {
|
||||||
|
civilian: ['District Admin', 'Sub-Level Hab Blocks', 'Transit Operations', 'Sector Maintenance Detail'],
|
||||||
|
shiver: ['Beat Patrol Squad B', 'High-Risk Tactical Unit', 'Precinct Watch Desk', 'Anti-Crime Division'],
|
||||||
|
carrien: ['Lower Pumping Station Nest', 'Maintenance Pipeline Grid', 'Deep Waste Warrens'],
|
||||||
|
gang: ['East Cut Corner Crew', 'Recycling Stack Block', 'Downtown Turf Detail', 'Monorail Corridor Raiders'],
|
||||||
|
corporate: ['Oversight Compliance Unit', 'Asset Reclamation Office', 'Research Project Containment', 'External Affairs Desk'],
|
||||||
|
mutant: ['Mutated Containment Breaker', 'Active Pattern Serial Cell', 'Feral Underlevel Scourge']
|
||||||
|
}[archetypeKey] ?? ['None'];
|
||||||
|
|
||||||
|
const sectors = {
|
||||||
|
civilian: ['Mort City Central', 'Downtown Mort', 'SubUrban District 4', 'Sector Nine Residential'],
|
||||||
|
shiver: ['Mort City Cordon', 'Downtown Precincts', 'Lower Transit Junctions', 'Perimeter Gates'],
|
||||||
|
carrien: ['Cannibal Sectors', 'Transit Underlevels', 'Deep Waste District', 'Sewer Runoff Tunnels'],
|
||||||
|
gang: ['Cannibal Sectors', 'Transit Underlevels', 'Mort Lower Tunnels', 'Industrial Belt Edge'],
|
||||||
|
corporate: ['Corporate High-SCL Arc', 'Downtown Executive Plaza', 'Bonded Warehouse Sectors'],
|
||||||
|
mutant: ['Cannibal Sectors', 'Transit Underlevels', 'Abandoned Tenement Block 8', 'Waste District']
|
||||||
|
}[archetypeKey] ?? ['Mort City'];
|
||||||
|
|
||||||
|
const dispositions = {
|
||||||
|
civilian: ['Nervous', 'Suspicious', 'Quiet', 'Helpful', 'Terrified'],
|
||||||
|
shiver: ['Professional', 'Alert', 'Disciplined', 'Aggressive', 'Suspicious'],
|
||||||
|
carrien: ['Feral', 'Hostile', 'Territorial', 'Starving', 'Aggressive'],
|
||||||
|
gang: ['Aggressive', 'Hostile', 'Suspicious', 'Fanatical', 'Cornered'],
|
||||||
|
corporate: ['Professional', 'Cold', 'Polite', 'Ruthless', 'Calculating'],
|
||||||
|
mutant: ['Unstable', 'Hostile', 'Fanatical', 'Manic', 'Demented']
|
||||||
|
}[archetypeKey] ?? ['Neutral'];
|
||||||
|
|
||||||
|
const threats = {
|
||||||
|
civilian: ['Low'],
|
||||||
|
shiver: ['Moderate', 'High'],
|
||||||
|
carrien: ['Moderate', 'High'],
|
||||||
|
gang: ['Moderate'],
|
||||||
|
corporate: ['Moderate', 'High'],
|
||||||
|
mutant: ['High', 'Extreme']
|
||||||
|
}[archetypeKey] ?? ['Moderate'];
|
||||||
|
|
||||||
|
const role = pick(roles);
|
||||||
|
const affiliation = pick(affiliations);
|
||||||
|
const department = pick(departments);
|
||||||
|
const sector = pick(sectors);
|
||||||
|
const disposition = pick(dispositions);
|
||||||
|
const threat = pick(threats);
|
||||||
|
|
||||||
|
return {
|
||||||
|
role: { value: role },
|
||||||
|
archetype: { value: ARCHETYPES[archetypeKey]?.label ?? cap(archetypeKey) },
|
||||||
|
species: { value: speciesLabel },
|
||||||
|
affiliation: { value: affiliation },
|
||||||
|
department: { value: department },
|
||||||
|
sector: { value: sector },
|
||||||
|
disposition: { value: disposition },
|
||||||
|
threat: { value: threat },
|
||||||
|
summary: { value: background }
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
const SPECIES_OPTIONS = [
|
const SPECIES_OPTIONS = [
|
||||||
{ value: 'random', label: 'Random (archetype-appropriate)' },
|
{ value: 'random', label: 'Random (archetype-appropriate)' },
|
||||||
{ value: 'human', label: 'Human' },
|
{ value: 'human', label: 'Human' },
|
||||||
@@ -472,12 +752,18 @@ const SPECIES_OPTIONS = [
|
|||||||
// Name generation
|
// Name generation
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
function generateName(species, archetypeKey) {
|
function generateName(species, archetypeKey, breed = null) {
|
||||||
// Serial killers / mutants get a pre-built infamous identity
|
// Serial killers / mutants get a pre-built infamous identity
|
||||||
if (archetypeKey === 'mutant') {
|
if (archetypeKey === 'mutant') {
|
||||||
return pick(SERIAL_KILLER_NAMES);
|
return pick(SERIAL_KILLER_NAMES);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Carrien names are quick animalistic gutter tags
|
||||||
|
if (archetypeKey === 'carrien') {
|
||||||
|
const carrienNames = ['Gnash', 'Skitter', 'Slink', 'Bite', 'Rip', 'Tear', 'Gouge', 'Mire', 'Slime', 'Pox', 'Sewer', 'Drip', 'Claw', 'Feral', 'Snarl', 'Howl', 'Gore', 'Barrow', 'Sump', 'Drain', 'Grime', 'Scrap', 'Spit', 'Rust', 'Fang', 'Shred', 'Mangle', 'Carcass', 'Vulture', 'Rot'];
|
||||||
|
return pick(carrienNames);
|
||||||
|
}
|
||||||
|
|
||||||
// Stormer names are alphanumeric designations
|
// Stormer names are alphanumeric designations
|
||||||
if (species === 'stormer') {
|
if (species === 'stormer') {
|
||||||
const designations = ['Storm','Bravo','Vector','Apex','Delta','Echo','Grid','Havoc','Iron','Jackal','Kilo','Lima'];
|
const designations = ['Storm','Bravo','Vector','Apex','Delta','Echo','Grid','Havoc','Iron','Jackal','Kilo','Lima'];
|
||||||
@@ -569,12 +855,36 @@ async function generateNpc(archetypeKey, speciesChoice, manager, folderId) {
|
|||||||
? pick(def.defaultSpecies)
|
? pick(def.defaultSpecies)
|
||||||
: speciesChoice;
|
: speciesChoice;
|
||||||
|
|
||||||
|
// Resolve breed details if Carrien
|
||||||
|
let breed = null;
|
||||||
|
let carrienSkills = [...def.skills];
|
||||||
|
if (archetypeKey === 'carrien') {
|
||||||
|
const breedKey = pick(Object.keys(CARRIEN_BREEDS));
|
||||||
|
breed = CARRIEN_BREEDS[breedKey];
|
||||||
|
}
|
||||||
|
|
||||||
// Generate name
|
// Generate name
|
||||||
const name = generateName(species, archetypeKey);
|
const name = generateName(species, archetypeKey, breed);
|
||||||
|
|
||||||
// Generate stats
|
// Generate stats
|
||||||
const stats = generateStats(archetypeKey);
|
const stats = generateStats(archetypeKey);
|
||||||
|
|
||||||
|
// Apply breed-specific stat modifiers and skills if Carrien
|
||||||
|
if (breed) {
|
||||||
|
for (const [statKey, mod] of Object.entries(breed.statMods)) {
|
||||||
|
if (stats[statKey]) {
|
||||||
|
stats[statKey].value = Math.max(3, stats[statKey].value + mod);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (breed.skills) {
|
||||||
|
for (const bs of breed.skills) {
|
||||||
|
if (!carrienSkills.some(s => s.name === bs.name)) {
|
||||||
|
carrienSkills.push(bs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Health = body stat (simplified; Foundry system will recalculate in some builds)
|
// Health = body stat (simplified; Foundry system will recalculate in some builds)
|
||||||
const bodyVal = stats.body?.value ?? 30;
|
const bodyVal = stats.body?.value ?? 30;
|
||||||
const healthVal = Math.max(5, Math.floor(bodyVal / 2));
|
const healthVal = Math.max(5, Math.floor(bodyVal / 2));
|
||||||
@@ -615,8 +925,43 @@ async function generateNpc(archetypeKey, speciesChoice, manager, folderId) {
|
|||||||
portraitPath = portrait?.path ?? null;
|
portraitPath = portrait?.path ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Background
|
// Generate detailed dossier variables (gender, age, height, weight, SCL, credits, background description)
|
||||||
const background = pick(def.backgrounds);
|
let gender = pick(['Male', 'Female', 'Undetermined']);
|
||||||
|
let age = roll(18, 55);
|
||||||
|
let height = `${(roll(160, 195) / 100).toFixed(2)}m`;
|
||||||
|
let weight = `${roll(60, 95)}kg`;
|
||||||
|
let scl = '10';
|
||||||
|
let credits = roll(20, 150);
|
||||||
|
let background = pick(def.backgrounds);
|
||||||
|
|
||||||
|
if (archetypeKey === 'carrien' && breed) {
|
||||||
|
gender = pick(['Male', 'Female', 'Undetermined']);
|
||||||
|
age = roll(breed.ageRange[0], breed.ageRange[1]);
|
||||||
|
height = `${(roll(breed.heightRange[0] * 100, breed.heightRange[1] * 100) / 100).toFixed(2)}m`;
|
||||||
|
weight = `${roll(breed.weightRange[0], breed.weightRange[1])}kg`;
|
||||||
|
scl = 'None';
|
||||||
|
credits = 0;
|
||||||
|
|
||||||
|
const skull = pick(breed.skulls);
|
||||||
|
background = `A feral Carrien of the ${breed.name} breed. Standing roughly ${height} and weighing ${weight}, this creature represents the primal terror of Mort's underlevels. It wears ${skull} to mask its expressive, human-like features. It is currently acting with a ${breed.threat.toLowerCase()}-threat profile. Behavior is noted as extremely ${pick(breed.dispositions).toLowerCase()} and aggressive when cornered.`;
|
||||||
|
} else if (archetypeKey === 'shiver') {
|
||||||
|
scl = pick(['9', '9', '9', '8']); // 8 is Sergeant
|
||||||
|
credits = roll(100, 450);
|
||||||
|
} else if (archetypeKey === 'corporate') {
|
||||||
|
scl = pick(['7', '7', '6', '5']);
|
||||||
|
credits = roll(1000, 5000);
|
||||||
|
} else if (archetypeKey === 'mutant') {
|
||||||
|
scl = 'Outlaw';
|
||||||
|
credits = roll(0, 20);
|
||||||
|
height = `${(roll(180, 240) / 100).toFixed(2)}m`;
|
||||||
|
weight = `${roll(85, 150)}kg`;
|
||||||
|
} else if (archetypeKey === 'civilian') {
|
||||||
|
scl = '10';
|
||||||
|
credits = roll(10, 150);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate dossier data
|
||||||
|
const dossier = buildDossierData(archetypeKey, species, name, background, breed);
|
||||||
|
|
||||||
// Actor type: mutant/creature uses 'creature', everything else uses 'character'
|
// Actor type: mutant/creature uses 'creature', everything else uses 'character'
|
||||||
const actorType = isCreature ? 'creature' : 'character';
|
const actorType = isCreature ? 'creature' : 'character';
|
||||||
@@ -631,7 +976,42 @@ async function generateNpc(archetypeKey, speciesChoice, manager, folderId) {
|
|||||||
health: { value: healthVal, min: 0, max: healthVal },
|
health: { value: healthVal, min: 0, max: healthVal },
|
||||||
biography: background,
|
biography: background,
|
||||||
notes: `Archetype: ${def.label}\nSpecies: ${species}\nOperative Type: ${def.operativeType}`,
|
notes: `Archetype: ${def.label}\nSpecies: ${species}\nOperative Type: ${def.operativeType}`,
|
||||||
stats,
|
// System-compatible dossier parameters
|
||||||
|
details: {
|
||||||
|
age: String(age),
|
||||||
|
height: String(height),
|
||||||
|
weight: String(weight),
|
||||||
|
scl: String(scl),
|
||||||
|
credits: Number(credits)
|
||||||
|
},
|
||||||
|
bio: {
|
||||||
|
species: species.charAt(0).toUpperCase() + species.slice(1),
|
||||||
|
scl: String(scl),
|
||||||
|
background: background,
|
||||||
|
appearance: breed ? `Wears ${pick(breed.skulls)}. Slouched, feral posture.` : `Civilian clothing, typical of Lower Mort sectors.`,
|
||||||
|
notes: `Threat Profile: ${dossier.threat.value}`
|
||||||
|
},
|
||||||
|
finance: {
|
||||||
|
credits: Number(credits),
|
||||||
|
unis: 0,
|
||||||
|
lad: 0
|
||||||
|
},
|
||||||
|
gender: gender,
|
||||||
|
age: age,
|
||||||
|
height: height,
|
||||||
|
weight: weight,
|
||||||
|
wealth: Number(credits),
|
||||||
|
stats: {
|
||||||
|
...stats,
|
||||||
|
armor: { value: armorVal, mod: armorVal, damageReduction: archetypeKey === 'shiver' ? 1 : (archetypeKey === 'mutant' ? 2 : 0), enabled: true },
|
||||||
|
combat: { ...stats.combat, enabled: true },
|
||||||
|
instinct: { ...stats.instinct, enabled: true },
|
||||||
|
speed: { ...stats.speed, enabled: true },
|
||||||
|
strength: { ...stats.strength, enabled: true },
|
||||||
|
sanity: { ...stats.sanity, enabled: true },
|
||||||
|
fear: { ...stats.fear, enabled: true },
|
||||||
|
body: { ...stats.body, enabled: true }
|
||||||
|
},
|
||||||
other: {
|
other: {
|
||||||
stress: { value: 2, min: 2, max: stressMax },
|
stress: { value: 2, min: 2, max: stressMax },
|
||||||
resolve: { value: resolveMax, min: 0, max: resolveMax },
|
resolve: { value: resolveMax, min: 0, max: resolveMax },
|
||||||
@@ -639,7 +1019,10 @@ async function generateNpc(archetypeKey, speciesChoice, manager, folderId) {
|
|||||||
sla: {
|
sla: {
|
||||||
operativeType: { value: def.operativeType },
|
operativeType: { value: def.operativeType },
|
||||||
species: { value: species.charAt(0).toUpperCase() + species.slice(1) },
|
species: { value: species.charAt(0).toUpperCase() + species.slice(1) },
|
||||||
|
scl: String(scl),
|
||||||
|
credits: Number(credits)
|
||||||
},
|
},
|
||||||
|
slaNpc: dossier
|
||||||
},
|
},
|
||||||
prototypeToken: {
|
prototypeToken: {
|
||||||
name,
|
name,
|
||||||
@@ -658,11 +1041,91 @@ async function generateNpc(archetypeKey, speciesChoice, manager, folderId) {
|
|||||||
// Add armor stat update (armor is a stat on the character)
|
// Add armor stat update (armor is a stat on the character)
|
||||||
await actor.update({ 'system.stats.armor.value': armorVal });
|
await actor.update({ 'system.stats.armor.value': armorVal });
|
||||||
|
|
||||||
// Create embedded item documents (skills + weapons)
|
// Resolve weapons
|
||||||
const itemsToCreate = [
|
let npcWeapons = [...def.weapons];
|
||||||
...def.skills.map(s => makeSkillItemData(s)),
|
if (archetypeKey === 'carrien' && breed) {
|
||||||
...def.weapons.map(w => makeWeaponItemData(w)),
|
// Carriens get 1 to 2 random scrap weapons/attacks based on breed!
|
||||||
];
|
const allowed = breed.allowedWeapons;
|
||||||
|
const w1Name = pick(allowed);
|
||||||
|
const weapon1 = CARRIEN_WEAPONS.find(w => w.name === w1Name) || CARRIEN_WEAPONS[0];
|
||||||
|
|
||||||
|
let weapon2 = null;
|
||||||
|
if (Math.random() > 0.4 && allowed.length > 1) {
|
||||||
|
let w2Name = pick(allowed);
|
||||||
|
while (w2Name === w1Name) {
|
||||||
|
w2Name = pick(allowed);
|
||||||
|
}
|
||||||
|
weapon2 = CARRIEN_WEAPONS.find(w => w.name === w2Name);
|
||||||
|
}
|
||||||
|
npcWeapons = weapon2 ? [weapon1, weapon2] : [weapon1];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create embedded item documents (skills + weapons + breed traits!)
|
||||||
|
const validItemTypes = CONFIG.Item?.documentClass?.TYPES ?? [];
|
||||||
|
const hasType = (t) => validItemTypes.includes(t);
|
||||||
|
|
||||||
|
const itemsToCreate = [];
|
||||||
|
|
||||||
|
if (hasType('skill')) {
|
||||||
|
itemsToCreate.push(...carrienSkills.map(s => makeSkillItemData(s)));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasType('weapon')) {
|
||||||
|
itemsToCreate.push(...npcWeapons.map(w => makeWeaponItemData(w)));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (archetypeKey === 'carrien' && breed?.trait) {
|
||||||
|
// Zero-Engine specialty
|
||||||
|
if (hasType('specialty')) {
|
||||||
|
itemsToCreate.push({
|
||||||
|
name: breed.trait.name,
|
||||||
|
type: 'specialty',
|
||||||
|
img: 'icons/svg/star.svg',
|
||||||
|
system: {
|
||||||
|
effects: breed.trait.effects,
|
||||||
|
package: breed.trait.package,
|
||||||
|
category: 'general',
|
||||||
|
prerequisites: '',
|
||||||
|
healthMod: 0,
|
||||||
|
resolveMod: 0,
|
||||||
|
isActive: true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// S5S / Sla-Industries trait
|
||||||
|
if (hasType('trait')) {
|
||||||
|
itemsToCreate.push({
|
||||||
|
name: breed.trait.name,
|
||||||
|
type: 'trait',
|
||||||
|
img: 'icons/svg/statue.svg',
|
||||||
|
system: {
|
||||||
|
description: breed.trait.effects,
|
||||||
|
category: 'combat',
|
||||||
|
source: 'Carrien Breed'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// SLA-Mothership ability
|
||||||
|
if (hasType('ability')) {
|
||||||
|
itemsToCreate.push({
|
||||||
|
name: breed.trait.name,
|
||||||
|
type: 'ability',
|
||||||
|
img: 'icons/svg/bolt.svg',
|
||||||
|
system: {
|
||||||
|
text: breed.trait.effects,
|
||||||
|
roll: '',
|
||||||
|
sla: {
|
||||||
|
abilityType: 'special',
|
||||||
|
discipline: '',
|
||||||
|
fluxCost: 0,
|
||||||
|
tier: 'basic',
|
||||||
|
attack: false,
|
||||||
|
notes: [breed.trait.effects]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (itemsToCreate.length > 0) {
|
if (itemsToCreate.length > 0) {
|
||||||
await actor.createEmbeddedDocuments('Item', itemsToCreate);
|
await actor.createEmbeddedDocuments('Item', itemsToCreate);
|
||||||
@@ -720,6 +1183,10 @@ async function generateGangMembers(count, manager, folderId) {
|
|||||||
// Pick a random weapon from the gang weapons list
|
// Pick a random weapon from the gang weapons list
|
||||||
const weapon = pick(def.gangWeapons);
|
const weapon = pick(def.gangWeapons);
|
||||||
|
|
||||||
|
// Generate background & dossier data
|
||||||
|
const background = `Street gang member. No past worth mentioning.`;
|
||||||
|
const dossier = buildDossierData('gang', 'human', handle, background);
|
||||||
|
|
||||||
const actorData = {
|
const actorData = {
|
||||||
name: handle,
|
name: handle,
|
||||||
type: 'character',
|
type: 'character',
|
||||||
@@ -727,17 +1194,17 @@ async function generateGangMembers(count, manager, folderId) {
|
|||||||
folder: folderId ?? null,
|
folder: folderId ?? null,
|
||||||
system: {
|
system: {
|
||||||
health: { value: healthVal, min: 0, max: healthVal },
|
health: { value: healthVal, min: 0, max: healthVal },
|
||||||
biography: `Street gang member. No past worth mentioning.`,
|
biography: background,
|
||||||
notes: `Archetype: Street Gang\nArmor: ${armorVal}\nCombat: ${combatVal}`,
|
notes: `Archetype: Street Gang\nArmor: ${armorVal}\nCombat: ${combatVal}`,
|
||||||
stats: {
|
stats: {
|
||||||
strength: { value: strengthVal, mod: 0 },
|
strength: { value: strengthVal, mod: 0, enabled: true },
|
||||||
speed: { value: speedVal, mod: 0 },
|
speed: { value: speedVal, mod: 0, enabled: true },
|
||||||
intellect: { value: roll(15, 25), mod: 0 },
|
intellect: { value: roll(15, 25), mod: 0, enabled: true },
|
||||||
combat: { value: combatVal, mod: 0 },
|
combat: { value: combatVal, mod: 0, enabled: true },
|
||||||
sanity: { value: roll(12, 22), mod: 0 },
|
sanity: { value: roll(12, 22), mod: 0, enabled: true },
|
||||||
fear: { value: roll(12, 22), mod: 0 },
|
fear: { value: roll(12, 22), mod: 0, enabled: true },
|
||||||
body: { value: bodyVal, mod: 0 },
|
body: { value: bodyVal, mod: 0, enabled: true },
|
||||||
armor: { value: armorVal, mod: 0, damageReduction: 0 },
|
armor: { value: armorVal, mod: armorVal, damageReduction: 0, enabled: true },
|
||||||
},
|
},
|
||||||
other: {
|
other: {
|
||||||
stress: { value: 2, min: 2, max: roll(2, 4) },
|
stress: { value: 2, min: 2, max: roll(2, 4) },
|
||||||
@@ -747,6 +1214,7 @@ async function generateGangMembers(count, manager, folderId) {
|
|||||||
operativeType: { value: 'Gang Member' },
|
operativeType: { value: 'Gang Member' },
|
||||||
species: { value: 'Human' },
|
species: { value: 'Human' },
|
||||||
},
|
},
|
||||||
|
slaNpc: dossier
|
||||||
},
|
},
|
||||||
prototypeToken: {
|
prototypeToken: {
|
||||||
name,
|
name,
|
||||||
@@ -761,8 +1229,11 @@ async function generateGangMembers(count, manager, folderId) {
|
|||||||
const actor = await Actor.create(actorData);
|
const actor = await Actor.create(actorData);
|
||||||
if (!actor) continue;
|
if (!actor) continue;
|
||||||
|
|
||||||
// Embed the weapon item
|
// Embed the weapon item if valid for this system
|
||||||
await actor.createEmbeddedDocuments('Item', [ makeWeaponItemData(weapon) ]);
|
const validItemTypes = CONFIG.Item?.documentClass?.TYPES ?? [];
|
||||||
|
if (validItemTypes.includes('weapon')) {
|
||||||
|
await actor.createEmbeddedDocuments('Item', [ makeWeaponItemData(weapon) ]);
|
||||||
|
}
|
||||||
|
|
||||||
created.push(actor);
|
created.push(actor);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user