diff --git a/src/npc/packs/index.js b/src/npc/packs/index.js new file mode 100644 index 0000000..2f857ea --- /dev/null +++ b/src/npc/packs/index.js @@ -0,0 +1,9 @@ +import { noir } from './noir.js'; + +const packs = new Map([[noir.id, noir]]); + +export function getGenrePack(id) { return packs.get(id) || null; } +export function registerGenrePack(pack) { packs.set(pack.id, pack); } +export function listGenrePacks() { + return [...packs.values()].map((p) => ({ id: p.id, label: p.label })); +} diff --git a/src/npc/packs/noir.js b/src/npc/packs/noir.js new file mode 100644 index 0000000..72008cd --- /dev/null +++ b/src/npc/packs/noir.js @@ -0,0 +1,51 @@ +export const noir = { + id: 'noir', + label: 'Noir / Blade Runner', + tables: { + firstNames: ['Otto', 'Mara', 'Cyrus', 'Dita', 'Lev', 'Priya', 'Sol', 'Neena', 'Bram', 'Vesna'], + surnames: ['Brandt', 'Okoye', 'Vance', 'Marlow', 'Sato', 'Kessler', 'Reyes', 'Dahl', 'Cole', 'Nyx'], + looks: [ + 'Heavyset, oil-stained cuffs, a rosary they keep touching', + 'Rail-thin, chrome tooth, coat two sizes too big', + 'Ex-military posture gone to seed, nicotine-yellow fingers', + 'Immaculate suit, one cracked lens in their glasses', + 'Burn scar down one wrist, always half in shadow', + 'Too-bright dye job, roots showing, tired eyes', + 'Synthetic-smooth skin that never quite reads as alive', + 'Missing a fingertip, hides the hand when they notice you looking', + ], + manners: [ + 'Over-friendly, laughs a beat too late', + 'Answers questions with questions', + 'Won\'t sit with their back to the door', + 'Counts their words like they cost money', + 'Flatters you, then watches to see if it landed', + 'Keeps checking a watch that isn\'t there', + 'Talks fast when lying, slow when it\'s worse', + ], + wants: [ + 'The debt gone before their shift ends', + 'A name - someone else to take the fall', + 'Out of the city on the last transport', + 'To be believed, just once', + 'The photograph back before anyone else sees it', + 'Protection they\'re too proud to ask for', + ], + secrets: [ + 'Signed off the container they never inspected', + 'Is already talking to the other side', + 'The person you\'re looking for is family', + 'Sold the access codes months ago', + 'Isn\'t who their papers say they are', + 'Watched it happen and did nothing', + ], + voices: [ + 'Says "you understand me?" after everything', + 'Never finishes the last word of a sentence', + 'Drops into a second language when rattled', + 'Calls everyone "friend" like a threat', + 'Over-enunciates, like reciting', + 'Laughs at their own lines before they land', + ], + }, +}; diff --git a/tests/npc/packs.test.js b/tests/npc/packs.test.js new file mode 100644 index 0000000..9a8440d --- /dev/null +++ b/tests/npc/packs.test.js @@ -0,0 +1,23 @@ +import { describe, it, expect } from 'vitest'; +import { noir } from '../../src/npc/packs/noir.js'; +import { getGenrePack, listGenrePacks } from '../../src/npc/packs/index.js'; + +const FIELDS = ['firstNames', 'surnames', 'looks', 'manners', 'wants', 'secrets', 'voices']; + +describe('noir genre pack', () => { + it('has an id, label, and all non-empty tables', () => { + expect(noir.id).toBe('noir'); + expect(noir.label).toMatch(/noir/i); + for (const f of FIELDS) { + expect(Array.isArray(noir.tables[f]), `${f} is array`).toBe(true); + expect(noir.tables[f].length, `${f} non-empty`).toBeGreaterThanOrEqual(6); + expect(noir.tables[f].every((x) => typeof x === 'string' && x.length > 0)).toBe(true); + } + }); + + it('is resolvable from the registry', () => { + expect(getGenrePack('noir')).toBe(noir); + expect(getGenrePack('missing')).toBe(null); + expect(listGenrePacks()).toEqual([{ id: 'noir', label: noir.label }]); + }); +});