diff --git a/src/components/dice-tray.js b/src/components/dice-tray.js new file mode 100644 index 0000000..141ff22 --- /dev/null +++ b/src/components/dice-tray.js @@ -0,0 +1,57 @@ +import { rollPool } from '../dice/roller.js'; +import { getRulePack } from '../dice/rulepacks/index.js'; + +const DICE = [4, 6, 8, 10, 12, 20, 100]; + +export class DiceTray extends HTMLElement { + constructor() { + super(); + this._sides = 8; + this._count = 3; + this._systemId = 'year-zero'; + this.rng = Math.random; + this.last = null; + } + get sides() { return this._sides; } + set sides(v) { this._sides = Number(v); this.render(); } + get count() { return this._count; } + set count(v) { this._count = Number(v); } + set systemId(v) { this._systemId = v; } + get systemId() { return this._systemId; } + + connectedCallback() { this.render(); } + + roll() { + const { results, total } = rollPool([{ sides: this._sides, count: this._count }], this.rng); + const pack = getRulePack(this._systemId); + const verdict = pack ? pack.interpret(results) : null; + this.last = { results, total, verdict }; + this.render(); + this.dispatchEvent(new CustomEvent('rolled', { detail: this.last, bubbles: true })); + } + + render() { + const picker = DICE.map((n) => + ``).join(''); + const felt = this.last + ? this.last.results.map((r) => `${r.value}`).join('') + : 'Tap Roll'; + const verdict = this.last?.verdict + ? `${this.last.verdict.successes} success${this.last.verdict.successes === 1 ? '' : 'es'}` + + (this.last.verdict.ones ? ` ยท ${this.last.verdict.ones} stress` : '') + : ''; + + this.innerHTML = ` +
+
${picker}
+
${felt}
+
${verdict}
+ +
`; + + this.querySelectorAll('[data-sides]').forEach((b) => + b.addEventListener('click', () => { this.sides = Number(b.dataset.sides); })); + this.querySelector('[data-role=roll]').addEventListener('click', () => this.roll()); + } +} +customElements.define('dice-tray', DiceTray); diff --git a/tests/components/dice-tray.test.js b/tests/components/dice-tray.test.js new file mode 100644 index 0000000..ad2c52a --- /dev/null +++ b/tests/components/dice-tray.test.js @@ -0,0 +1,36 @@ +import { describe, it, expect, beforeEach } from 'vitest'; +import '../../src/components/dice-tray.js'; + +function seq(values) { let i = 0; return () => values[i++ % values.length]; } + +describe('', () => { + let el; + beforeEach(() => { + document.body.innerHTML = ''; + el = document.createElement('dice-tray'); + el.systemId = 'year-zero'; + document.body.appendChild(el); + }); + + it('renders the polyhedral picker', () => { + expect(el.querySelector('[data-sides="20"]')).not.toBe(null); + expect(el.querySelector('[data-sides="4"]')).not.toBe(null); + }); + + it('rolls, interprets via the rule-pack, and reports successes', () => { + el.sides = 8; el.count = 3; + el.rng = seq([0.9, 0.0, 0.7]); // d8 -> 8, 1, 6 => 2 successes, 1 one + let detail = null; + el.addEventListener('rolled', (e) => { detail = e.detail; }); + el.roll(); + expect(el.last.verdict.successes).toBe(2); + expect(el.last.verdict.ones).toBe(1); + expect(detail.verdict.successes).toBe(2); + expect(el.querySelector('[data-role=verdict]').textContent).toContain('2 success'); + }); + + it('changes die type when a picker button is clicked', () => { + el.querySelector('[data-sides="20"]').click(); + expect(el.sides).toBe(20); + }); +});