diff --git a/src/dice/rulepacks/index.js b/src/dice/rulepacks/index.js index bbb0d95..62b8391 100644 --- a/src/dice/rulepacks/index.js +++ b/src/dice/rulepacks/index.js @@ -1,8 +1,9 @@ import { yearZero } from './year-zero.js'; import { cocD100 } from './coc-d100.js'; import { vanityD6 } from './vanity-d6.js'; +import { panicGlory } from './panic-glory.js'; -const packs = new Map([[yearZero.id, yearZero], [cocD100.id, cocD100], [vanityD6.id, vanityD6]]); +const packs = new Map([[yearZero.id, yearZero], [cocD100.id, cocD100], [vanityD6.id, vanityD6], [panicGlory.id, panicGlory]]); export function getRulePack(id) { return packs.get(id) || null; } export function registerRulePack(pack) { packs.set(pack.id, pack); } diff --git a/src/dice/rulepacks/panic-glory.js b/src/dice/rulepacks/panic-glory.js new file mode 100644 index 0000000..b018945 --- /dev/null +++ b/src/dice/rulepacks/panic-glory.js @@ -0,0 +1,25 @@ +// Continuum house mechanic: 1d6 + (20 - current SAN), six bands. +const BANDS = [ + { max: 6, key: 'glory-unleashed', label: 'GLORY UNLEASHED', effect: '2D10 temp HP · ignore SAN loss 3 rounds · auto-succeed one pulp feat · +1 Glory' }, + { max: 9, key: 'heroic-surge', label: 'HEROIC SURGE', effect: '1D6 temp HP · +20% next 2 rolls · +1 Glory' }, + { max: 13, key: 'focused-fury', label: 'FOCUSED FURY', effect: 'reroll next fail, then −1D3 SAN' }, + { max: 16, key: 'wavering-resolve', label: 'WAVERING RESOLVE', effect: '−20% next action · POW×5 or panic' }, + { max: 19, key: 'desperate-flinch', label: 'DESPERATE FLINCH', effect: 'drop weapon / freeze · lose action' }, + { max: Infinity, key: 'full-panic', label: 'FULL PANIC', effect: '−1D10 SAN · flee, freeze, or act irrationally' }, +]; + +export const panicGlory = { + id: 'panic-glory', + label: 'Panic & Glory', + params: [{ key: 'san', label: 'Current SAN', default: 20 }], + interpret(results, params = {}) { + const roll = results[0]?.value ?? 0; + const san = params.san ?? 20; + const total = roll + (20 - san); + const band = BANDS.find((b) => total <= b.max); + return { d6: roll, san, total, band, gloryPoint: total <= 9 }; + }, + summary(v) { + return `${v.total} → ${v.band.label}`; + }, +}; diff --git a/tests/dice/panic-glory.test.js b/tests/dice/panic-glory.test.js new file mode 100644 index 0000000..b3cade4 --- /dev/null +++ b/tests/dice/panic-glory.test.js @@ -0,0 +1,30 @@ +import { describe, it, expect } from 'vitest'; +import { panicGlory } from '../../src/dice/rulepacks/panic-glory.js'; +import { getRulePack } from '../../src/dice/rulepacks/index.js'; + +const d6 = (v) => [{ sides: 6, value: v }]; + +describe('panic-glory rule-pack', () => { + it('adds (20 - SAN) to the d6', () => { + const v = panicGlory.interpret(d6(3), { san: 20 }); + expect(v.total).toBe(3); + expect(v.band.key).toBe('glory-unleashed'); + expect(v.gloryPoint).toBe(true); + }); + it('bands a mid result correctly', () => { + const v = panicGlory.interpret(d6(4), { san: 10 }); // 4 + 10 = 14 + expect(v.total).toBe(14); + expect(v.band.key).toBe('wavering-resolve'); + expect(v.gloryPoint).toBe(false); + }); + it('caps into full panic at 20+', () => { + const v = panicGlory.interpret(d6(5), { san: 5 }); // 5 + 15 = 20 + expect(v.band.key).toBe('full-panic'); + }); + it('defaults SAN to 20, summarises, and is registered', () => { + const v = panicGlory.interpret(d6(6)); + expect(v.total).toBe(6); + expect(panicGlory.summary(v)).toBe('6 → GLORY UNLEASHED'); + expect(getRulePack('panic-glory')).toBe(panicGlory); + }); +});