NPC Desk: archetype accents, live preview card, badge icons, custom SVG emblems

- sla-npc-generator.js: archetype-specific accent styling for Shiver, DarkNight, Tek, Ebb, Media, Internal Affairs, gangs, civilians, predators, corp assets; live preview card (identity, strapline, disposition/threat posture, stat readout, loadout kit) updating on archetype/quality/species/disposition/threat/loadout/affiliation change; badge icon helper switched from Font Awesome classes to custom SVG path mapping
- sla-npc-generator.html: preview card markup with badge header; SVG badge renderer replacing icon-class approach
- mosh.css: archetype accent strip styles; preview card layout; image-based badge styling; badge set rules under npc-badges
- icons/ui/npc-badges/: 10 new custom SVGs — shiver, darknight, tek, ebb, internal, media, gang, predator, civilian, corp
This commit is contained in:
2026-04-15 18:27:24 +01:00
parent 9269348bf4
commit 24f513baf8
13 changed files with 532 additions and 35 deletions
+162
View File
@@ -3017,6 +3017,160 @@ p {
color: #11161b;
}
.mosh .sla-npc-preview-panel {
position: relative;
overflow: hidden;
border-radius: 14px;
border: 1px solid rgba(20, 27, 33, 0.18);
}
.mosh .sla-npc-preview-panel::before {
content: "";
position: absolute;
inset: 0 0 auto 0;
height: 3px;
background: var(--sla-npc-accent, linear-gradient(90deg, #74a7d3, #d7e4f2));
}
.mosh .sla-npc-preview-header {
display: flex;
justify-content: space-between;
gap: 12px;
align-items: flex-start;
margin-bottom: 12px;
}
.mosh .sla-npc-preview-heading {
display: flex;
gap: 12px;
align-items: flex-start;
}
.mosh .sla-npc-preview-badge {
position: relative;
width: 56px;
height: 56px;
flex: 0 0 56px;
border-radius: 16px;
background: rgba(18, 24, 30, 0.9);
color: #f3f6f8;
border: 1px solid rgba(255, 255, 255, 0.12);
display: inline-flex;
align-items: center;
justify-content: center;
box-shadow: inset 0 1px 0 rgba(255,255,255,0.08), 0 8px 20px rgba(0,0,0,0.12);
overflow: hidden;
}
.mosh .sla-npc-preview-badge::before {
content: "";
position: absolute;
inset: 0 0 auto 0;
height: 3px;
background: var(--sla-npc-accent, linear-gradient(90deg, #74a7d3, #d7e4f2));
}
.mosh .sla-npc-preview-icon {
width: 26px;
height: 26px;
display: block;
object-fit: contain;
opacity: 0.96;
filter: drop-shadow(0 1px 0 rgba(255,255,255,0.08));
}
.mosh .sla-npc-preview-badge span {
position: absolute;
right: 6px;
bottom: 4px;
font-size: 0.52rem;
font-family: "Josefin Sans", "Roboto", sans-serif;
letter-spacing: 0.08em;
font-weight: 700;
color: rgba(243, 246, 248, 0.82);
}
.mosh .sla-npc-preview-pill {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 88px;
padding: 6px 10px;
border-radius: 999px;
background: rgba(17, 22, 28, 0.08);
border: 1px solid rgba(17, 22, 28, 0.14);
color: #11161b;
font-family: "Josefin Sans", "Roboto", sans-serif;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.08em;
}
.mosh .sla-npc-preview-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 10px;
}
.mosh .sla-npc-preview-block {
padding: 10px;
border-radius: 12px;
background: rgba(255, 255, 255, 0.48);
border: 1px solid rgba(18, 24, 30, 0.08);
min-width: 0;
}
.mosh .sla-npc-preview-wide {
grid-column: span 2;
}
.mosh .sla-npc-generator .sla-tag,
.mosh .sla-npc-generator .sla-note-pill {
background: rgba(18, 24, 30, 0.06);
border-color: rgba(18, 24, 30, 0.12);
color: #151a1f;
}
.mosh .sla-npc-generator.sla-npc-accent-shiver {
--sla-npc-accent: linear-gradient(90deg, #5ca3d9, #d9edf9);
}
.mosh .sla-npc-generator.sla-npc-accent-darknight {
--sla-npc-accent: linear-gradient(90deg, #7a2238, #d59baa);
}
.mosh .sla-npc-generator.sla-npc-accent-tek {
--sla-npc-accent: linear-gradient(90deg, #4ea5b7, #d0f0f4);
}
.mosh .sla-npc-generator.sla-npc-accent-ebb {
--sla-npc-accent: linear-gradient(90deg, #6f63d9, #d9d7ff);
}
.mosh .sla-npc-generator.sla-npc-accent-media {
--sla-npc-accent: linear-gradient(90deg, #d09a34, #f6e2b0);
}
.mosh .sla-npc-generator.sla-npc-accent-internal {
--sla-npc-accent: linear-gradient(90deg, #9b2f2f, #f1b7b7);
}
.mosh .sla-npc-generator.sla-npc-accent-gang {
--sla-npc-accent: linear-gradient(90deg, #b04c34, #f0c2a8);
}
.mosh .sla-npc-generator.sla-npc-accent-predator {
--sla-npc-accent: linear-gradient(90deg, #7a8b38, #dfe8b1);
}
.mosh .sla-npc-generator.sla-npc-accent-civilian {
--sla-npc-accent: linear-gradient(90deg, #7f8b93, #dde3e8);
}
.mosh .sla-npc-generator.sla-npc-accent-corp {
--sla-npc-accent: linear-gradient(90deg, #6c8aa6, #d8e4ef);
}
.mosh .sla-creature-dossier {
margin: 10px 12px 14px;
padding: 12px;
@@ -3060,6 +3214,14 @@ p {
}
@media (max-width: 900px) {
.mosh .sla-npc-preview-grid {
grid-template-columns: 1fr;
}
.mosh .sla-npc-preview-wide {
grid-column: span 1;
}
.mosh .sla-creature-dossier-grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
+5
View File
@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" fill="none">
<rect x="14" y="12" width="36" height="40" rx="6" stroke="#F4F7FA" stroke-width="4"/>
<circle cx="24" cy="27" r="5" stroke="#F4F7FA" stroke-width="4"/>
<path d="M19 40c2-4 5-6 9-6s7 2 9 6M36 24h8M36 32h8M36 40h8" stroke="#F4F7FA" stroke-width="4" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 358 B

+5
View File
@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" fill="none">
<path d="M20 50V18l12-8 12 8v32" stroke="#F4F7FA" stroke-width="4" stroke-linejoin="round"/>
<path d="M26 24h3M35 24h3M26 31h3M35 31h3M26 38h3M35 38h3" stroke="#F4F7FA" stroke-width="4" stroke-linecap="round"/>
<path d="M16 50h32" stroke="#F4F7FA" stroke-width="4" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 376 B

+4
View File
@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" fill="none">
<path d="M39 10c-8 4-13 12-13 22 0 10 5 18 13 22-13-1.5-24-11.8-24-27S26 11.5 39 10Z" fill="#F4F7FA"/>
<path d="m40 21 8 11-8 11" stroke="#F4F7FA" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 297 B

+5
View File
@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" fill="none">
<path d="M10 32c5.7-9 13.1-13.5 22-13.5S48.3 23 54 32c-5.7 9-13.1 13.5-22 13.5S15.7 41 10 32Z" stroke="#F4F7FA" stroke-width="4" stroke-linejoin="round"/>
<circle cx="32" cy="32" r="7" stroke="#F4F7FA" stroke-width="4"/>
<circle cx="32" cy="32" r="2.5" fill="#F4F7FA"/>
</svg>

After

Width:  |  Height:  |  Size: 356 B

+6
View File
@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" fill="none">
<path d="M23 18h18l5 8-3 15H21l-3-15 5-8Z" stroke="#F4F7FA" stroke-width="4" stroke-linejoin="round"/>
<circle cx="26" cy="29" r="2.5" fill="#F4F7FA"/>
<circle cx="38" cy="29" r="2.5" fill="#F4F7FA"/>
<path d="M26 37c2 2 4 3 6 3s4-1 6-3" stroke="#F4F7FA" stroke-width="4" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 385 B

+4
View File
@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" fill="none">
<path d="M18 12h20l8 8v28a4 4 0 0 1-4 4H18a4 4 0 0 1-4-4V16a4 4 0 0 1 4-4Z" stroke="#F4F7FA" stroke-width="4" stroke-linejoin="round"/>
<path d="M38 12v10h10M24 30h12M24 38h16" stroke="#F4F7FA" stroke-width="4" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 320 B

+4
View File
@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" fill="none">
<path d="M32 16v28M24 44h16" stroke="#F4F7FA" stroke-width="4" stroke-linecap="round"/>
<path d="M22 22c2.5-3.5 6-5 10-5s7.5 1.5 10 5M16 16c4.3-5.3 9.6-8 16-8s11.7 2.7 16 8M16 28c4.3 5.3 9.6 8 16 8s11.7-2.7 16-8" stroke="#F4F7FA" stroke-width="4" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 356 B

+4
View File
@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" fill="none">
<path d="M18 48 28 16M30 50l8-26M42 48l6-18" stroke="#F4F7FA" stroke-width="5" stroke-linecap="round"/>
<path d="M18 48c2-4 6-7 10-8M30 50c2-4 5-7 8-8M42 48c1.5-3 4-5.5 7-6.5" stroke="#F4F7FA" stroke-width="3.5" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 321 B

+4
View File
@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" fill="none">
<path d="M32 8 48 14v14c0 11-6.4 20.8-16 28-9.6-7.2-16-17-16-28V14L32 8Z" stroke="#F4F7FA" stroke-width="4" stroke-linejoin="round"/>
<path d="M32 18v24M22 30h20" stroke="#F4F7FA" stroke-width="4" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 306 B

+5
View File
@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" fill="none">
<path d="M24 10h16l8 14-8 14H24l-8-14 8-14Z" stroke="#F4F7FA" stroke-width="4" stroke-linejoin="round"/>
<circle cx="32" cy="24" r="4" fill="#F4F7FA"/>
<path d="M32 28v18M32 38h10M32 46H22" stroke="#F4F7FA" stroke-width="4" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 335 B

+280 -32
View File
@@ -83,9 +83,57 @@ const NPC_ARCHETYPES = {
"Field Agent": ["FEN 209 Machine Pistol", "Shiver Flak Vest", "Klippo Multi-Band Communicator"]
},
defaultLoadout: "Field Agent",
defaultAffiliationType: "Corporation",
defaultRole: "Corporate Operative",
defaultAffiliation: "SLA Industries",
defaultDepartment: "Operations"
defaultDepartment: "Operations",
defaultSector: "Mort City"
},
Media: {
summary: "Reporters, propaganda staff, image handlers, and camera teams who shape the public story while staying very close to the danger.",
stats: { combat: 20, instinct: 42, speed: 28, loyalty: 44, sanity: 42, armor: 1, damageReduction: 0, health: 9, hits: 1 },
enabled: { combat: true, instinct: true, speed: true, loyalty: true, sanity: true, armor: true },
skills: [
{ name: "Persuade", bonus: 30 },
{ name: "Fast Talk", bonus: 25 },
{ name: "SLA Info", bonus: 25 },
{ name: "Spot Hidden", bonus: 20 },
{ name: "Intimidate", bonus: 15 }
],
loadouts: {
None: [],
Press: ["Klippo Multi-Band Communicator", "UV / Multi-Spectrum Torch", "Street Clothes / Leather Jacket"],
"Embedded Crew": ["FEN 603 Auto-Pistol", "Klippo Multi-Band Communicator", "Street Clothes / Leather Jacket"]
},
defaultLoadout: "Press",
defaultAffiliationType: "Corporation",
defaultRole: "Media Correspondent",
defaultAffiliation: "SLA Media",
defaultDepartment: "Editorial / Field Unit",
defaultSector: "Downtown Mort"
},
"Internal Affairs": {
summary: "Cold, suspicious investigators and auditors used for leak hunts, corruption probes, and quiet pressure on operatives or departments.",
stats: { combat: 34, instinct: 48, speed: 28, loyalty: 56, sanity: 44, armor: 2, damageReduction: 0, health: 10, hits: 2 },
enabled: { combat: true, instinct: true, speed: true, loyalty: true, sanity: true, armor: true },
skills: [
{ name: "Bureaucracy", bonus: 30 },
{ name: "Psychology", bonus: 25 },
{ name: "Intimidate", bonus: 25 },
{ name: "Command", bonus: 20 },
{ name: "Firearm (Pistol)", bonus: 20 }
],
loadouts: {
None: [],
Audit: ["FEN 603 Auto-Pistol", "Klippo Multi-Band Communicator", "Street Clothes / Leather Jacket"],
Pressure: ["FEN 401 Shiver Pistol", "Shiver Flak Vest", "Klippo Multi-Band Communicator"]
},
defaultLoadout: "Audit",
defaultAffiliationType: "Department",
defaultRole: "Internal Affairs Investigator",
defaultAffiliation: "SLA Industries",
defaultDepartment: "Internal Affairs",
defaultSector: "Cannibal Sectors"
},
"DarkNight Agent": {
summary: "Hard, covert, and unpleasant professionals for pressure scenes, black-bag work, and very bad conversations.",
@@ -104,9 +152,11 @@ const NPC_ARCHETYPES = {
Breach: ["FEN AR Assault Rifle", "Fragmentation Grenade", "Crackshot Armour"]
},
defaultLoadout: "Strike Agent",
defaultAffiliationType: "Squad",
defaultRole: "DarkNight Agent",
defaultAffiliation: "DarkNight",
defaultDepartment: "Black Unit"
defaultDepartment: "Black Unit",
defaultSector: "Cannibal Sectors"
},
"Tek Specialist": {
summary: "Engineers, locks-and-systems staff, data handlers, and machine-facing support assets.",
@@ -125,9 +175,11 @@ const NPC_ARCHETYPES = {
"Field Tek": ["FEN 603 Auto-Pistol", "BOOPA Medical Kit (Standard)", "Klippo Multi-Band Communicator"]
},
defaultLoadout: "Utility",
defaultAffiliationType: "Department",
defaultRole: "Tek Specialist",
defaultAffiliation: "Tek Division",
defaultDepartment: "Systems"
defaultDepartment: "Systems",
defaultSector: "Industrial Edge"
},
"Ebb Operative": {
summary: "Psychic intervention assets, occult handlers, and dangerous minds kept near the edge of tolerable doctrine.",
@@ -145,9 +197,11 @@ const NPC_ARCHETYPES = {
Focus: ["FEN 603 Auto-Pistol", "BOOPA Medical Kit (Standard)", "Klippo Multi-Band Communicator"]
},
defaultLoadout: "Focus",
defaultAffiliationType: "Department",
defaultRole: "Ebb Handler",
defaultAffiliation: "SLA Industries",
defaultDepartment: "Ebb / Black"
defaultDepartment: "Ebb / Black",
defaultSector: "Mort City"
},
"Predator / Beast": {
summary: "Mutants, monsters, feral biogen threats, or engineered horrors that need only enough structure to fight and stalk.",
@@ -163,9 +217,11 @@ const NPC_ARCHETYPES = {
None: []
},
defaultLoadout: "None",
defaultAffiliationType: "Independent",
defaultRole: "Hunting Threat",
defaultAffiliation: "Hostile Specimen",
defaultDepartment: "Wild / Uncontrolled"
defaultDepartment: "Wild / Uncontrolled",
defaultSector: "Waste District"
}
};
@@ -186,6 +242,8 @@ const ROLE_SNIPPETS = {
"Street Ganger": ["Lookout", "Enforcer", "Runner", "Debt collector", "Back-alley bruiser"],
"Shiver Patrol": ["Patrol officer", "Checkpoint lead", "Riot response cop", "Beat sergeant", "Scene controller"],
"Corp Operative": ["Case handler", "Recovery agent", "Sponsor fixer", "Quiet escort", "Internal security operative"],
Media: ["Field correspondent", "Embedded camera lead", "Image handler", "Narrative producer", "Live scene reporter"],
"Internal Affairs": ["Audit investigator", "Leak hunter", "Corruption examiner", "Interrogation lead", "Compliance agent"],
"DarkNight Agent": ["Pressure asset", "Black-bag specialist", "Interdiction hunter", "Silent breach lead", "Cleanup professional"],
"Tek Specialist": ["Systems breach tech", "Maintenance lead", "Field engineer", "Data recovery tech", "Signals operator"],
"Ebb Operative": ["Control asset", "Psi observer", "Occult investigator", "Flux-sensitive handler", "Discreet interventionist"],
@@ -207,6 +265,8 @@ const DEPARTMENT_SNIPPETS = {
"Street Ganger": ["East Cut", "Underline", "Recycling Stack", "Canal Row", "Tunnel Nine"],
"Shiver Patrol": ["Precinct 88", "Riot Control", "Street Division", "Transit Policing", "Evidence Escort"],
"Corp Operative": ["Internal Security", "Investigations", "Sponsor Liaison", "Logistics Recovery", "Operations"],
Media: ["Editorial Desk", "Live Scene Unit", "Mort Broadcast", "Image Control", "Public Feed Team"],
"Internal Affairs": ["Compliance Review", "Leak Office", "Audit Section", "Disciplinary Unit", "Special Oversight"],
"DarkNight Agent": ["Silent Entry", "Black Archive", "Pressure Cell", "Removal Team", "Interdiction"],
"Tek Specialist": ["Signals", "Maintenance", "Surveillance", "Field Systems", "Machine Control"],
"Ebb Operative": ["Occult Oversight", "Black Psi", "Flux Watch", "Intervention Cell", "Containment"],
@@ -247,6 +307,7 @@ export class SLANpcGeneratorApp extends FormApplication {
return {
state,
accentKey: getArchetypeAccentKey(state.archetype),
archetypes: Object.entries(NPC_ARCHETYPES).map(([name, entry]) => ({ name, summary: entry.summary })),
speciesOptions,
qualityOptions: Object.keys(QUALITY_MODS),
@@ -254,7 +315,8 @@ export class SLANpcGeneratorApp extends FormApplication {
threatOptions: THREAT_TIERS,
affiliationTypes: AFFILIATION_TYPES,
loadoutOptions,
summary: archetype.summary
summary: archetype.summary,
preview: buildNpcPreview(state, archetype)
};
}
@@ -266,11 +328,11 @@ export class SLANpcGeneratorApp extends FormApplication {
archetype,
quality: "Standard",
species: speciesOptions.includes("Human") ? "Human" : (speciesOptions[0] ?? "Human"),
affiliationType: "Gang",
affiliationType: config.defaultAffiliationType ?? "Gang",
affiliation: config.defaultAffiliation,
department: config.defaultDepartment,
role: config.defaultRole,
sector: "Mort City",
sector: config.defaultSector ?? "Mort City",
disposition: "Hostile",
threat: "Moderate",
loadout: config.defaultLoadout ?? "None",
@@ -295,18 +357,26 @@ export class SLANpcGeneratorApp extends FormApplication {
});
html.find("[name='archetype']").on("change", async () => {
const next = this.collectState(html);
const config = NPC_ARCHETYPES[next.archetype] ?? NPC_ARCHETYPES["Street Ganger"];
next.role = next.role || config.defaultRole;
next.affiliation = next.affiliation || config.defaultAffiliation;
next.department = next.department || config.defaultDepartment;
next.loadout = (config.loadouts?.[next.loadout] ? next.loadout : config.defaultLoadout) ?? "None";
if (next.archetype === "Ebb Operative" && !SLAMothershipGenerator.isEbbSpecies(next.species)) {
next.species = "Ebon";
}
this.formState = next;
const current = this.collectState(html);
this.formState = this.rethemeStateForArchetype(current, current.archetype);
await this.render(false);
});
const rerenderSelectors = [
"[name='quality']",
"[name='species']",
"[name='disposition']",
"[name='threat']",
"[name='loadout']",
"[name='affiliationType']"
];
for (const selector of rerenderSelectors) {
html.find(selector).on("change", async () => {
this.formState = this.collectState(html);
await this.render(false);
});
}
}
collectState(html) {
@@ -333,21 +403,31 @@ export class SLANpcGeneratorApp extends FormApplication {
const state = { ...current };
const archetypeName = state.archetype || randomFrom(Object.keys(NPC_ARCHETYPES)) || "Street Ganger";
const archetype = NPC_ARCHETYPES[archetypeName] ?? NPC_ARCHETYPES["Street Ganger"];
state.name = SLAMothershipGenerator.randomName();
state.role = randomFrom(ROLE_SNIPPETS[archetypeName] ?? [archetype.defaultRole]) ?? archetype.defaultRole;
state.affiliationType = state.affiliationType || "Department";
state.affiliation = randomFrom(AFFILIATION_SNIPPETS[state.affiliationType] ?? [archetype.defaultAffiliation]) ?? archetype.defaultAffiliation;
state.department = randomFrom(DEPARTMENT_SNIPPETS[archetypeName] ?? [archetype.defaultDepartment]) ?? archetype.defaultDepartment;
state.sector = randomFrom(["Mort City", "Cannibal Sectors", "Downtown Mort", "Transit Underlevels", "Industrial Edge", "Waste District"]) ?? "Mort City";
state.disposition = randomFrom(DISPOSITIONS) ?? "Neutral";
state.threat = randomFrom(THREAT_TIERS) ?? "Moderate";
state.loadout = randomFrom(Object.keys(archetype.loadouts ?? { None: [] })) ?? "None";
if (archetypeName === "Ebb Operative") {
state.species = randomFrom(["Ebon", "Brain Waster"]) ?? "Ebon";
} else if (archetypeName === "Predator / Beast") {
state.species = randomFrom(["Advanced Carrien", "Stormer 313 Malice", "Stormer 711 Xeno", "Wraith Raider"]) ?? "Advanced Carrien";
const themed = this.rethemeStateForArchetype(state, archetypeName);
themed.affiliation = randomFrom(AFFILIATION_SNIPPETS[themed.affiliationType] ?? [archetype.defaultAffiliation]) ?? archetype.defaultAffiliation;
themed.department = randomFrom(DEPARTMENT_SNIPPETS[archetypeName] ?? [archetype.defaultDepartment]) ?? archetype.defaultDepartment;
themed.role = randomFrom(ROLE_SNIPPETS[archetypeName] ?? [archetype.defaultRole]) ?? archetype.defaultRole;
themed.sector = randomFrom(getSectorOptionsForArchetype(archetypeName, archetype.defaultSector)) ?? (archetype.defaultSector ?? "Mort City");
themed.disposition = randomFrom(DISPOSITIONS) ?? "Neutral";
themed.threat = randomFrom(THREAT_TIERS) ?? "Moderate";
themed.loadout = randomFrom(Object.keys(archetype.loadouts ?? { None: [] })) ?? "None";
themed.name = SLAMothershipGenerator.randomName();
return themed;
}
return state;
rethemeStateForArchetype(current, archetypeName) {
const archetype = NPC_ARCHETYPES[archetypeName] ?? NPC_ARCHETYPES["Street Ganger"];
const next = { ...current, archetype: archetypeName };
next.affiliationType = archetype.defaultAffiliationType ?? current.affiliationType ?? "Department";
next.affiliation = archetype.defaultAffiliation ?? "";
next.department = archetype.defaultDepartment ?? "";
next.role = archetype.defaultRole ?? "";
next.sector = archetype.defaultSector ?? "Mort City";
next.loadout = archetype.defaultLoadout ?? "None";
next.disposition = getDefaultDispositionForArchetype(archetypeName);
next.threat = getDefaultThreatForArchetype(archetypeName);
next.species = getDefaultSpeciesForArchetype(archetypeName, current.species);
return next;
}
async _updateObject(_event, formData) {
@@ -556,6 +636,30 @@ function buildNpcBiography({ state, archetype, speciesName }) {
`.trim();
}
function buildNpcPreview(state, archetype) {
const quality = QUALITY_MODS[state.quality] ?? QUALITY_MODS.Standard;
const loadoutNames = archetype.loadouts?.[state.loadout] ?? [];
const statline = applyQualityToStats(archetype.stats, quality);
return {
iconPath: getArchetypeIconPath(state.archetype),
iconLabel: getArchetypeBadgeShort(state.archetype),
badge: getArchetypeBadge(state.archetype),
strap: getArchetypeStrapline(state.archetype),
identity: `${state.role || archetype.defaultRole} / ${state.species}`,
brief: `${state.affiliation || archetype.defaultAffiliation}${state.department ? ` / ${state.department}` : ""} operating in ${state.sector || archetype.defaultSector || "Mort City"}.`,
posture: `${state.disposition || "Neutral"} disposition with ${state.threat || "Moderate"} threat expectations.`,
statline: [
`Combat ${statline.combat}`,
`Instinct ${statline.instinct}`,
`Speed ${statline.speed}`,
`Health ${statline.health}`,
`Wounds ${statline.hits}`
],
loadout: loadoutNames.length ? loadoutNames : ["No preset issue kit selected."],
notes: archetype.summary
};
}
function clamp98(value) {
return Math.max(1, Math.min(98, Number(value ?? 0) || 0));
}
@@ -595,6 +699,69 @@ function randomFrom(list = []) {
return list[Math.floor(Math.random() * list.length)] ?? null;
}
function getDefaultSpeciesForArchetype(archetypeName, currentSpecies = "Human") {
if (archetypeName === "Ebb Operative") return "Ebon";
if (archetypeName === "Predator / Beast") return "Advanced Carrien";
return currentSpecies || "Human";
}
function getDefaultDispositionForArchetype(archetypeName) {
switch (archetypeName) {
case "Civilian Witness": return "Nervous";
case "Street Ganger": return "Hostile";
case "Shiver Patrol": return "Professional";
case "Corp Operative": return "Professional";
case "Media": return "Corrupt";
case "Internal Affairs": return "Professional";
case "DarkNight Agent": return "Hostile";
case "Tek Specialist": return "Professional";
case "Ebb Operative": return "Cornered";
case "Predator / Beast": return "Fanatical";
default: return "Neutral";
}
}
function getDefaultThreatForArchetype(archetypeName) {
switch (archetypeName) {
case "Civilian Witness": return "Low";
case "Street Ganger": return "Moderate";
case "Shiver Patrol": return "Moderate";
case "Corp Operative": return "Moderate";
case "Media": return "Low";
case "Internal Affairs": return "Moderate";
case "DarkNight Agent": return "High";
case "Tek Specialist": return "Moderate";
case "Ebb Operative": return "High";
case "Predator / Beast": return "Extreme";
default: return "Moderate";
}
}
function getSectorOptionsForArchetype(archetypeName, fallback = "Mort City") {
switch (archetypeName) {
case "Street Ganger":
return ["Cannibal Sectors", "Transit Underlevels", "Waste District", "Mort City"];
case "Shiver Patrol":
return ["Mort City", "Downtown Mort", "Transit Underlevels", "Cannibal Sectors"];
case "Corp Operative":
return ["Mort City", "Downtown Mort", "Industrial Edge", "Cannibal Sectors"];
case "Media":
return ["Downtown Mort", "Mort City", "Cannibal Sectors", "Transit Underlevels"];
case "Internal Affairs":
return ["Cannibal Sectors", "Mort City", "Downtown Mort", "Industrial Edge"];
case "DarkNight Agent":
return ["Cannibal Sectors", "Transit Underlevels", "Mort City", "Waste District"];
case "Tek Specialist":
return ["Industrial Edge", "Mort City", "Transit Underlevels", "Downtown Mort"];
case "Ebb Operative":
return ["Mort City", "Cannibal Sectors", "Downtown Mort"];
case "Predator / Beast":
return ["Waste District", "Cannibal Sectors", "Transit Underlevels", "Industrial Edge"];
default:
return [fallback, "Mort City", "Downtown Mort"];
}
}
function escapeHtml(value) {
return String(value ?? "")
.replaceAll("&", "&amp;")
@@ -603,3 +770,84 @@ function escapeHtml(value) {
.replaceAll('"', "&quot;")
.replaceAll("'", "&#39;");
}
function getArchetypeAccentKey(archetypeName) {
switch (archetypeName) {
case "Shiver Patrol": return "shiver";
case "DarkNight Agent": return "darknight";
case "Tek Specialist": return "tek";
case "Ebb Operative": return "ebb";
case "Media": return "media";
case "Internal Affairs": return "internal";
case "Street Ganger": return "gang";
case "Predator / Beast": return "predator";
case "Civilian Witness": return "civilian";
case "Corp Operative": return "corp";
default: return "corp";
}
}
function getArchetypeBadge(archetypeName) {
switch (archetypeName) {
case "Shiver Patrol": return "Shiver Patrol";
case "DarkNight Agent": return "DarkNight";
case "Tek Specialist": return "Tek Division";
case "Ebb Operative": return "Ebb Control";
case "Media": return "Media Feed";
case "Internal Affairs": return "Internal Affairs";
case "Street Ganger": return "Street Threat";
case "Predator / Beast": return "Hostile Specimen";
case "Civilian Witness": return "Civilian Contact";
case "Corp Operative": return "Corporate Asset";
default: return archetypeName;
}
}
function getArchetypeBadgeShort(archetypeName) {
switch (archetypeName) {
case "Shiver Patrol": return "SV";
case "DarkNight Agent": return "DN";
case "Tek Specialist": return "TK";
case "Ebb Operative": return "EB";
case "Media": return "MD";
case "Internal Affairs": return "IA";
case "Street Ganger": return "SG";
case "Predator / Beast": return "HX";
case "Civilian Witness": return "CV";
case "Corp Operative": return "CO";
default: return "NPC";
}
}
function getArchetypeIconPath(archetypeName) {
const root = "systems/sla-mothership/icons/ui/npc-badges";
switch (archetypeName) {
case "Shiver Patrol": return `${root}/shiver.svg`;
case "DarkNight Agent": return `${root}/darknight.svg`;
case "Tek Specialist": return `${root}/tek.svg`;
case "Ebb Operative": return `${root}/ebb.svg`;
case "Media": return `${root}/media.svg`;
case "Internal Affairs": return `${root}/internal.svg`;
case "Street Ganger": return `${root}/gang.svg`;
case "Predator / Beast": return `${root}/predator.svg`;
case "Civilian Witness": return `${root}/civilian.svg`;
case "Corp Operative": return `${root}/corp.svg`;
default: return `${root}/corp.svg`;
}
}
function getArchetypeStrapline(archetypeName) {
switch (archetypeName) {
case "Shiver Patrol": return "Precinct issue / patrol doctrine / fast response";
case "DarkNight Agent": return "Black operations / pressure asset / covert response";
case "Tek Specialist": return "Systems breach / maintenance / machine-facing support";
case "Ebb Operative": return "Occult intervention / flux risk / psychic control";
case "Media": return "Image control / live coverage / narrative pressure";
case "Internal Affairs": return "Audit pressure / disciplinary review / quiet threat";
case "Street Ganger": return "Turf violence / local intimidation / disposable muscle";
case "Predator / Beast": return "Ferality / pursuit / close-quarters threat";
case "Civilian Witness": return "Collateral witness / local knowledge / brittle pressure";
case "Corp Operative": return "Corporate recovery / sponsor discipline / field oversight";
default: return "Operational contact / generated dossier";
}
}
+43 -2
View File
@@ -1,7 +1,7 @@
<form class="{{cssClass}} sla-npc-generator" autocomplete="off">
<form class="{{cssClass}} sla-npc-generator sla-npc-accent-{{accentKey}}" autocomplete="off">
<p>Create lightweight SLA NPCs with enough identity for play, then open the creature sheet to adjust portrait, items, weapons, skills, and notes.</p>
<div class="sla-card" style="padding: 12px; margin-bottom: 12px;">
<div class="sla-card sla-npc-brief-card" style="padding: 12px; margin-bottom: 12px;">
<div class="grid grid-2col" style="gap: 10px;">
<div class="resource">
<label class="resource-label">Name</label>
@@ -38,6 +38,47 @@
<div class="sla-card-copy" style="margin-top: 10px;">{{summary}}</div>
</div>
<section class="sla-card sla-npc-preview-panel" style="margin-bottom: 12px;">
<div class="sla-npc-preview-header">
<div class="sla-npc-preview-heading">
<div class="sla-npc-preview-badge" aria-hidden="true">
<img class="sla-npc-preview-icon" src="{{preview.iconPath}}" alt="" />
<span>{{preview.iconLabel}}</span>
</div>
<div>
<div class="sla-kicker">{{preview.badge}}</div>
<div class="sla-title">{{state.name}}</div>
<div class="sla-card-copy">{{preview.strap}}</div>
</div>
</div>
<div class="sla-npc-preview-pill">{{state.quality}}</div>
</div>
<div class="sla-npc-preview-grid">
<div class="sla-npc-preview-block">
<div class="sla-card-label">Profile</div>
<div class="sla-card-copy"><strong>{{preview.identity}}</strong></div>
<div class="sla-card-copy">{{preview.brief}}</div>
<div class="sla-card-copy">{{preview.posture}}</div>
</div>
<div class="sla-npc-preview-block">
<div class="sla-card-label">Field Readout</div>
<div class="sla-tags">
{{#each preview.statline}}
<span class="sla-tag">{{this}}</span>
{{/each}}
</div>
</div>
<div class="sla-npc-preview-block sla-npc-preview-wide">
<div class="sla-card-label">Issue Kit</div>
<div class="sla-note-list">
{{#each preview.loadout}}
<span class="sla-note-pill">{{this}}</span>
{{/each}}
</div>
</div>
</div>
</section>
<div class="grid grid-2col" style="gap: 10px;">
<div class="resource">
<label class="resource-label">Affiliation Type</label>