From 95833bd2c194e8d0c72ae472302f0689ee4a06aa Mon Sep 17 00:00:00 2001 From: Tim Evans Date: Mon, 20 Jul 2026 16:22:13 +0100 Subject: [PATCH] feat: VANITY d6-pool rule-pack --- src/dice/rulepacks/index.js | 3 ++- src/dice/rulepacks/vanity-d6.js | 31 +++++++++++++++++++++++++++ tests/dice/vanity-d6.test.js | 37 +++++++++++++++++++++++++++++++++ 3 files changed, 70 insertions(+), 1 deletion(-) create mode 100644 src/dice/rulepacks/vanity-d6.js create mode 100644 tests/dice/vanity-d6.test.js diff --git a/src/dice/rulepacks/index.js b/src/dice/rulepacks/index.js index aefe47f..bbb0d95 100644 --- a/src/dice/rulepacks/index.js +++ b/src/dice/rulepacks/index.js @@ -1,7 +1,8 @@ import { yearZero } from './year-zero.js'; import { cocD100 } from './coc-d100.js'; +import { vanityD6 } from './vanity-d6.js'; -const packs = new Map([[yearZero.id, yearZero], [cocD100.id, cocD100]]); +const packs = new Map([[yearZero.id, yearZero], [cocD100.id, cocD100], [vanityD6.id, vanityD6]]); export function getRulePack(id) { return packs.get(id) || null; } export function registerRulePack(pack) { packs.set(pack.id, pack); } diff --git a/src/dice/rulepacks/vanity-d6.js b/src/dice/rulepacks/vanity-d6.js new file mode 100644 index 0000000..049897c --- /dev/null +++ b/src/dice/rulepacks/vanity-d6.js @@ -0,0 +1,31 @@ +// VANITY (Draft 13) core roll: 5-6 = Success; Stumble = 0 successes with 2+ ones. +export const vanityD6 = { + id: 'vanity-d6', + label: 'd6 Pool (VANITY)', + params: [{ key: 'difficulty', label: 'Successes needed', default: 1 }], + interpret(results, params = {}) { + let successes = 0, ones = 0; + for (const r of results) { + if (r.value >= 5) successes += 1; + if (r.value === 1) ones += 1; + } + const difficulty = params.difficulty ?? 1; + const isSuccess = successes >= difficulty; + return { + successes, + ones, + difficulty, + isSuccess, + stumble: successes === 0 && ones >= 2, + twist: ones >= 1, + style: successes > difficulty, + canPush: true, + }; + }, + summary(v) { + const base = `${v.successes} success${v.successes === 1 ? '' : 'es'}`; + const verdict = v.isSuccess ? '' : ' — fail'; + const stumble = v.stumble ? ' · STUMBLE (+1 Bane)' : (v.twist ? ' · a 1 (GM twist)' : ''); + return `${base}${verdict}${stumble}`; + }, +}; diff --git a/tests/dice/vanity-d6.test.js b/tests/dice/vanity-d6.test.js new file mode 100644 index 0000000..994e4eb --- /dev/null +++ b/tests/dice/vanity-d6.test.js @@ -0,0 +1,37 @@ +import { describe, it, expect } from 'vitest'; +import { vanityD6 } from '../../src/dice/rulepacks/vanity-d6.js'; +import { getRulePack } from '../../src/dice/rulepacks/index.js'; + +const pool = (...vals) => vals.map((v) => ({ sides: 6, value: v })); + +describe('vanity-d6 rule-pack', () => { + it('counts 5s and 6s as successes', () => { + const v = vanityD6.interpret(pool(6, 5, 2), { difficulty: 1 }); + expect(v.successes).toBe(2); + expect(v.isSuccess).toBe(true); + expect(v.style).toBe(true); // more successes than needed + }); + it('stumbles on zero successes with two or more 1s', () => { + const v = vanityD6.interpret(pool(1, 1, 3), { difficulty: 1 }); + expect(v.successes).toBe(0); + expect(v.ones).toBe(2); + expect(v.stumble).toBe(true); + expect(v.isSuccess).toBe(false); + }); + it('a lone 1 is a GM twist, not a stumble', () => { + const v = vanityD6.interpret(pool(1, 4, 4), { difficulty: 1 }); + expect(v.twist).toBe(true); + expect(v.stumble).toBe(false); + }); + it('respects the difficulty (successes needed)', () => { + const v = vanityD6.interpret(pool(6, 2, 2), { difficulty: 2 }); + expect(v.successes).toBe(1); + expect(v.isSuccess).toBe(false); + }); + it('defaults difficulty to 1, summarises, and is registered', () => { + const v = vanityD6.interpret(pool(5, 3)); + expect(v.difficulty).toBe(1); + expect(vanityD6.summary(v)).toContain('1 success'); + expect(getRulePack('vanity-d6')).toBe(vanityD6); + }); +});