Phase 16: advantage/disadvantage applies to any single die
applyRollMode (src/lib/dice/notation.ts) now rewrites the first single-die term of any size (d4/d6/d20/...) to 2dN kh1/kl1, not just d20. Multi-die pools (2d6) and terms with an existing keep/drop (4d6kh3) are left untouched. Dice page copy updated; unit tests for d4/d6/d8 + skip cases; e2e toggles Advantage and rolls a d4 → 2d4kh1. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -11,9 +11,18 @@ describe('applyRollMode', () => {
|
||||
expect(applyRollMode('2d6+3', 'advantage')).toBe('2d6+3');
|
||||
expect(applyRollMode('1d20+5', 'normal')).toBe('1d20+5');
|
||||
});
|
||||
it('only transforms the first d20 term', () => {
|
||||
it('only transforms the first die term', () => {
|
||||
expect(applyRollMode('1d20+1d20', 'advantage')).toBe('2d20kh1+1d20');
|
||||
});
|
||||
it('applies to any single die, not just d20', () => {
|
||||
expect(applyRollMode('1d4', 'advantage')).toBe('2d4kh1');
|
||||
expect(applyRollMode('d6', 'disadvantage')).toBe('2d6kl1');
|
||||
expect(applyRollMode('1d8+3', 'advantage')).toBe('2d8kh1+3');
|
||||
});
|
||||
it('does not touch multi-die pools or terms that already keep/drop', () => {
|
||||
expect(applyRollMode('2d6+3', 'advantage')).toBe('2d6+3');
|
||||
expect(applyRollMode('4d6kh3', 'advantage')).toBe('4d6kh3');
|
||||
});
|
||||
});
|
||||
|
||||
describe('parseDice', () => {
|
||||
|
||||
@@ -51,18 +51,21 @@ export class DiceParseError extends Error {}
|
||||
export type RollMode = 'normal' | 'advantage' | 'disadvantage';
|
||||
|
||||
/**
|
||||
* Apply advantage/disadvantage to a d20 expression by turning the first d20 term
|
||||
* into 2d20kh1 / 2d20kl1. Non-d20 expressions are returned unchanged, so the
|
||||
* toggle only affects d20 checks (and keeps any modifiers, e.g. 1d20+5).
|
||||
* Apply advantage/disadvantage by turning the first single-die term (any dN —
|
||||
* d4, d6, d20, …) into 2dN kh1 / 2dN kl1, keeping any modifiers (e.g. 1d20+5).
|
||||
* Only single-die terms (count 1 or omitted) are affected, since "advantage" on
|
||||
* a pool like 2d6 isn't well-defined; terms that already carry a keep/drop
|
||||
* modifier (e.g. 4d6kh3) are left untouched so we never double-apply.
|
||||
*/
|
||||
export function applyRollMode(expression: string, mode: RollMode): string {
|
||||
if (mode === 'normal') return expression;
|
||||
const keep = mode === 'advantage' ? 'kh1' : 'kl1';
|
||||
let replaced = false;
|
||||
return expression.replace(/(^|[+\-\s])\d*d20\b/i, (m, pre: string) => {
|
||||
if (replaced) return m;
|
||||
replaced = true;
|
||||
return `${pre}2d20${keep}`;
|
||||
let done = false;
|
||||
return expression.replace(/(^|[+\-\s])(\d*)d(\d+)((?:kh|kl|dh|dl)\d+)?/gi, (m, pre: string, count: string, sides: string, mod: string | undefined) => {
|
||||
if (done || mod) return m;
|
||||
if (count !== '' && count !== '1') return m; // only single-die terms
|
||||
done = true;
|
||||
return `${pre}2d${sides}${keep}`;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user