From 605b107c7707836c0f36200f0e1f982ecbea77e9 Mon Sep 17 00:00:00 2001 From: Tim Evans Date: Mon, 20 Jul 2026 16:18:27 +0100 Subject: [PATCH] feat: CoC 7e d100 rule-pack --- src/dice/rulepacks/coc-d100.js | 22 ++++++++++++++++++++++ src/dice/rulepacks/index.js | 3 ++- tests/dice/coc-d100.test.js | 32 ++++++++++++++++++++++++++++++++ 3 files changed, 56 insertions(+), 1 deletion(-) create mode 100644 src/dice/rulepacks/coc-d100.js create mode 100644 tests/dice/coc-d100.test.js diff --git a/src/dice/rulepacks/coc-d100.js b/src/dice/rulepacks/coc-d100.js new file mode 100644 index 0000000..173802b --- /dev/null +++ b/src/dice/rulepacks/coc-d100.js @@ -0,0 +1,22 @@ +// Call of Cthulhu 7e success ladder vs a skill %. +export const cocD100 = { + id: 'coc-d100', + label: 'd100 (Call of Cthulhu)', + params: [{ key: 'target', label: 'Skill %', default: 50 }], + interpret(results, params = {}) { + const value = results[0]?.value ?? 0; + const target = params.target ?? 50; + let level; + if (value === 1) level = 'critical'; + else if (value <= Math.floor(target / 5)) level = 'extreme'; + else if (value <= Math.floor(target / 2)) level = 'hard'; + else if (value <= target) level = 'success'; + else if ((target < 50 && value >= 96) || value === 100) level = 'fumble'; + else level = 'failure'; + const success = ['critical', 'extreme', 'hard', 'success'].includes(level); + return { value, target, level, success }; + }, + summary(v) { + return `${v.value} vs ${v.target}% → ${v.level.toUpperCase()}`; + }, +}; diff --git a/src/dice/rulepacks/index.js b/src/dice/rulepacks/index.js index 8432605..aefe47f 100644 --- a/src/dice/rulepacks/index.js +++ b/src/dice/rulepacks/index.js @@ -1,6 +1,7 @@ import { yearZero } from './year-zero.js'; +import { cocD100 } from './coc-d100.js'; -const packs = new Map([[yearZero.id, yearZero]]); +const packs = new Map([[yearZero.id, yearZero], [cocD100.id, cocD100]]); export function getRulePack(id) { return packs.get(id) || null; } export function registerRulePack(pack) { packs.set(pack.id, pack); } diff --git a/tests/dice/coc-d100.test.js b/tests/dice/coc-d100.test.js new file mode 100644 index 0000000..a4f5e07 --- /dev/null +++ b/tests/dice/coc-d100.test.js @@ -0,0 +1,32 @@ +import { describe, it, expect } from 'vitest'; +import { cocD100 } from '../../src/dice/rulepacks/coc-d100.js'; +import { getRulePack } from '../../src/dice/rulepacks/index.js'; + +const roll = (v) => [{ sides: 100, value: v }]; + +describe('coc-d100 rule-pack', () => { + it('grades a roll against a 50% skill', () => { + expect(cocD100.interpret(roll(1), { target: 50 }).level).toBe('critical'); + expect(cocD100.interpret(roll(10), { target: 50 }).level).toBe('extreme'); // <= 10 + expect(cocD100.interpret(roll(25), { target: 50 }).level).toBe('hard'); // <= 25 + expect(cocD100.interpret(roll(50), { target: 50 }).level).toBe('success'); + expect(cocD100.interpret(roll(51), { target: 50 }).level).toBe('failure'); + expect(cocD100.interpret(roll(100), { target: 50 }).level).toBe('fumble'); + }); + it('fumbles on 96+ when the skill is under 50', () => { + expect(cocD100.interpret(roll(96), { target: 40 }).level).toBe('fumble'); + expect(cocD100.interpret(roll(41), { target: 40 }).level).toBe('failure'); + }); + it('marks the four passing bands as success', () => { + expect(cocD100.interpret(roll(10), { target: 50 }).success).toBe(true); + expect(cocD100.interpret(roll(51), { target: 50 }).success).toBe(false); + }); + it('defaults target to 50 and summarises', () => { + const v = cocD100.interpret(roll(5)); + expect(v.target).toBe(50); + expect(cocD100.summary(v)).toBe('5 vs 50% → EXTREME'); + }); + it('is registered', () => { + expect(getRulePack('coc-d100')).toBe(cocD100); + }); +});