feat: dedicated BRP dice pack; Day One uses it instead of coc-d100
Add brp-d100 rule-pack (Basic Roleplaying roll-under): fumble on 100, critical <= skill/20, special <= skill/5, success <= skill — bands transcribed from the Continuum Day One console's verdict() logic. Register it and switch day-one meta.system from coc-d100 to brp-d100. tests/dice/brp-d100.test.js covers the ladder, low-skill rounding, and the fumble-only-on-100 edge. 147 tests pass. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
9d0e6c7ec4
commit
49e9dcda46
@@ -53,7 +53,7 @@ Components are dumb renderers that emit events; the shell wires them together.
|
||||
| `src/core/scenario.js` | Scenario validator |
|
||||
| `src/core/format.js` | Elapsed + drift display formatting |
|
||||
| `src/dice/roller.js` | Seedable dice core (pools, modifiers) |
|
||||
| `src/dice/rulepacks/` | System interpreters (Year-Zero, CoC d100, VANITY d6, Panic & Glory, Dee Sanction) + registry |
|
||||
| `src/dice/rulepacks/` | System interpreters (Year-Zero, CoC d100, BRP d100, VANITY d6, Panic & Glory, Dee Sanction) + registry |
|
||||
| `src/npc/` | NPC generator (`generator.js`) + genre table packs (`packs/noir.js`) |
|
||||
| `src/art/` | Pencil-art library search (`search.js`) + tagged manifest |
|
||||
| `src/clues/safety-net.js` | Clue safety-net engine (essential-gap + solvability) |
|
||||
@@ -97,7 +97,7 @@ compress if you reach a beat behind schedule.
|
||||
3. **Slice 3** — dice rule-packs (CoC d100, VANITY d6-pool, Panic & Glory, Dee Sanction) + tray pack selector. ✅
|
||||
4. **Slice 4** — the clue **safety-net** (essential-clue gap tracker + fallbacks). ✅
|
||||
5. **Slice 5** — the convention **hub** (all slots, live "live now / up next / done", deep-links into each scenario). ✅ All **six** Continuum 2026 slots ported and scheduled with real times.
|
||||
6. **Later** — remaining tray tools (break timer, parking-lot note, wake-lock); online art "Generate"; a dedicated BRP dice pack (Day One currently borrows the d100 pack); markdown → scenario-data generator; native iPad wrapper.
|
||||
6. **Later** — remaining tray tools (break timer, parking-lot note, wake-lock); online art "Generate"; markdown → scenario-data generator; native iPad wrapper.
|
||||
|
||||
## Docs
|
||||
|
||||
|
||||
@@ -70,7 +70,10 @@ jsdom. No new dependencies. Fully offline.
|
||||
👥 rail chip. Reveal state is ephemeral component state. `tests/components/cast-tray.test.js` +
|
||||
`gm-shell-cast.test.js` cover it.
|
||||
|
||||
- **BRP dice pack built.** `brp-d100` (`src/dice/rulepacks/brp-d100.js`) — fumble on 100, critical
|
||||
≤ skill/20, special ≤ skill/5, success ≤ skill (bands transcribed from the Day One console). Day
|
||||
One's `meta.system` switched from `coc-d100` to `brp-d100`. `tests/dice/brp-d100.test.js` covers it.
|
||||
|
||||
## Still deferred (backlog)
|
||||
|
||||
- A dedicated **BRP** dice pack — Day One currently borrows `coc-d100` (d100 roll-under).
|
||||
- Break-timer / parking-lot / wake-lock tray tools; online art "Generate".
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
// Basic Roleplaying (BRP) roll-under d100 ladder vs a skill %.
|
||||
// Bands match the Continuum "Day One" console: fumble on 100, critical ≤ skill/20,
|
||||
// special ≤ skill/5, success ≤ skill (the fractions rounded up).
|
||||
export const brpD100 = {
|
||||
id: 'brp-d100',
|
||||
label: 'd100 (Basic Roleplaying)',
|
||||
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 === 100) level = 'fumble';
|
||||
else if (value <= Math.ceil(target / 20)) level = 'critical';
|
||||
else if (value <= Math.ceil(target / 5)) level = 'special';
|
||||
else if (value <= target) level = 'success';
|
||||
else level = 'failure';
|
||||
const success = ['critical', 'special', 'success'].includes(level);
|
||||
return { value, target, level, success };
|
||||
},
|
||||
summary(v) {
|
||||
return `${v.value} vs ${v.target}% → ${v.level.toUpperCase()}`;
|
||||
},
|
||||
};
|
||||
@@ -1,10 +1,11 @@
|
||||
import { yearZero } from './year-zero.js';
|
||||
import { cocD100 } from './coc-d100.js';
|
||||
import { brpD100 } from './brp-d100.js';
|
||||
import { vanityD6 } from './vanity-d6.js';
|
||||
import { panicGlory } from './panic-glory.js';
|
||||
import { deeSanction } from './dee-sanction.js';
|
||||
|
||||
const packs = new Map([[yearZero.id, yearZero], [cocD100.id, cocD100], [vanityD6.id, vanityD6], [panicGlory.id, panicGlory], [deeSanction.id, deeSanction]]);
|
||||
const packs = new Map([[yearZero.id, yearZero], [cocD100.id, cocD100], [brpD100.id, brpD100], [vanityD6.id, vanityD6], [panicGlory.id, panicGlory], [deeSanction.id, deeSanction]]);
|
||||
|
||||
export function getRulePack(id) { return packs.get(id) || null; }
|
||||
export function registerRulePack(pack) { packs.set(pack.id, pack); }
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
// DAY ONE — "London Falls" · Continuum 2026 · Fri Slot 1.
|
||||
// Timeline transcribed from gm-utility/day-one-console.html (SCHED array).
|
||||
// System is BRP; mapped to the d100 roller pack (no dedicated BRP pack yet).
|
||||
// System is BRP (Basic Roleplaying) — uses the dedicated brp-d100 rule-pack.
|
||||
export default {
|
||||
meta: { id: 'day-one', title: 'DAY ONE — London Falls', system: 'coc-d100', players: 6, playMinutes: 210, slot: 'Fri · Slot 1' },
|
||||
meta: { id: 'day-one', title: 'DAY ONE — London Falls', system: 'brp-d100', players: 6, playMinutes: 210, slot: 'Fri · Slot 1' },
|
||||
timeline: [
|
||||
{ id: 'intro', label: 'Intro done — six placed, alert fired, players converging', targetMin: 15 },
|
||||
{ id: 'market', label: 'MARKET BREAKS — vendor bitten in full view, stampede (force it now if not yet)', targetMin: 60, hardTrigger: true },
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { brpD100 } from '../../src/dice/rulepacks/brp-d100.js';
|
||||
import { getRulePack } from '../../src/dice/rulepacks/index.js';
|
||||
|
||||
const roll = (v) => [{ sides: 100, value: v }];
|
||||
|
||||
describe('brp-d100 rule-pack', () => {
|
||||
it('grades the ladder against a 50% skill (crit ≤3, special ≤10, success ≤50)', () => {
|
||||
expect(brpD100.interpret(roll(3), { target: 50 }).level).toBe('critical'); // ceil(50/20)=3
|
||||
expect(brpD100.interpret(roll(4), { target: 50 }).level).toBe('special');
|
||||
expect(brpD100.interpret(roll(10), { target: 50 }).level).toBe('special'); // ceil(50/5)=10
|
||||
expect(brpD100.interpret(roll(11), { target: 50 }).level).toBe('success');
|
||||
expect(brpD100.interpret(roll(50), { target: 50 }).level).toBe('success');
|
||||
expect(brpD100.interpret(roll(51), { target: 50 }).level).toBe('failure');
|
||||
});
|
||||
|
||||
it('fumbles only on 100 (a plain miss otherwise, even at 96–99)', () => {
|
||||
expect(brpD100.interpret(roll(100), { target: 50 }).level).toBe('fumble');
|
||||
expect(brpD100.interpret(roll(99), { target: 50 }).level).toBe('failure');
|
||||
expect(brpD100.interpret(roll(100), { target: 100 }).level).toBe('fumble'); // 100 fumbles even at skill 100
|
||||
});
|
||||
|
||||
it('rounds the crit/special fractions up for low skills', () => {
|
||||
// skill 25: crit ≤ ceil(25/20)=2, special ≤ ceil(25/5)=5
|
||||
expect(brpD100.interpret(roll(2), { target: 25 }).level).toBe('critical');
|
||||
expect(brpD100.interpret(roll(5), { target: 25 }).level).toBe('special');
|
||||
expect(brpD100.interpret(roll(6), { target: 25 }).level).toBe('success');
|
||||
});
|
||||
|
||||
it('marks crit/special/success as success, fumble/failure as not', () => {
|
||||
expect(brpD100.interpret(roll(3), { target: 50 }).success).toBe(true);
|
||||
expect(brpD100.interpret(roll(50), { target: 50 }).success).toBe(true);
|
||||
expect(brpD100.interpret(roll(51), { target: 50 }).success).toBe(false);
|
||||
expect(brpD100.interpret(roll(100), { target: 50 }).success).toBe(false);
|
||||
});
|
||||
|
||||
it('defaults skill to 50 and summarises', () => {
|
||||
const v = brpD100.interpret(roll(4));
|
||||
expect(v.target).toBe(50);
|
||||
expect(brpD100.summary(v)).toBe('4 vs 50% → SPECIAL');
|
||||
});
|
||||
|
||||
it('is registered', () => {
|
||||
expect(getRulePack('brp-d100')).toBe(brpD100);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user