diff --git a/src/components/dice-tray.js b/src/components/dice-tray.js index f080334..594e3a3 100644 --- a/src/components/dice-tray.js +++ b/src/components/dice-tray.js @@ -1,5 +1,5 @@ import { rollPool } from '../dice/roller.js'; -import { getRulePack } from '../dice/rulepacks/index.js'; +import { getRulePack, listRulePacks } from '../dice/rulepacks/index.js'; const DICE = [4, 6, 8, 10, 12, 20, 100]; @@ -17,10 +17,23 @@ export class DiceTray extends HTMLElement { 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; } + set systemId(v) { this._systemId = v; this.render(); } get systemId() { return this._systemId; } - connectedCallback() { this.render(); } + connectedCallback() { this.render(); this.syncParams(); } + + syncParams() { + const pack = getRulePack(this._systemId); + const next = {}; + if (pack && pack.params) { + for (const p of pack.params) { + const input = this.querySelector(`[data-param="${p.key}"]`); + const raw = input ? input.value : ''; + next[p.key] = raw === '' ? p.default : Number(raw); + } + } + this.params = next; + } roll() { const { results, total } = rollPool([{ sides: this._sides, count: this._count }], this.rng); @@ -42,9 +55,18 @@ export class DiceTray extends HTMLElement { ? (pack && pack.summary ? pack.summary(this.last.verdict) : '') : ''; + const packs = listRulePacks(); + const packBar = [{ id: '', label: 'Any die' }, ...packs].map((p) => + ``).join(''); + const active = getRulePack(this._systemId); + const paramInputs = (active && active.params ? active.params : []).map((p) => + ``).join(''); + this.innerHTML = `
+
${packBar}
${picker}
+
${paramInputs}
${felt}
${verdict}
@@ -52,6 +74,10 @@ export class DiceTray extends HTMLElement { this.querySelectorAll('[data-sides]').forEach((b) => b.addEventListener('click', () => { this.sides = Number(b.dataset.sides); })); + this.querySelectorAll('[data-pack-id]').forEach((b) => + b.addEventListener('click', () => { this._systemId = b.dataset.packId; this.syncParams(); this.render(); })); + this.querySelectorAll('[data-param]').forEach((inp) => + inp.addEventListener('input', () => this.syncParams())); this.querySelector('[data-role=roll]').addEventListener('click', () => this.roll()); } } diff --git a/src/styles.css b/src/styles.css index a877c95..16fae5d 100644 --- a/src/styles.css +++ b/src/styles.css @@ -55,3 +55,9 @@ gm-shell { display: block; } .prop-overlay { position: fixed; inset: 0; background: #000000ee; display: flex; align-items: center; justify-content: center; z-index: 100; } .prop-img { max-width: 96vw; max-height: 96vh; object-fit: contain; } prop-viewer[hidden] { display: none; } +.pack-bar { display: flex; gap: 6px; flex-wrap: wrap; margin-bottom: 8px; } +.pack { font-size: 11px; padding: 5px 10px; border-radius: 20px; border: 1px solid #2b3644; background: #161d27; color: #9fb0c2; } +.pack.on { background: #1e3a52; border-color: #3f7fb0; color: #dcefff; } +.params { display: flex; gap: 10px; flex-wrap: wrap; margin: 8px 0; } +.param { font-size: 11px; color: #9fb0c2; display: flex; flex-direction: column; gap: 3px; } +.param input { width: 80px; background: #161d27; border: 1px solid #2a3542; border-radius: 6px; padding: 6px 8px; color: #e7eef6; } diff --git a/tests/components/dice-tray-packs.test.js b/tests/components/dice-tray-packs.test.js new file mode 100644 index 0000000..dbfaf80 --- /dev/null +++ b/tests/components/dice-tray-packs.test.js @@ -0,0 +1,46 @@ +import { describe, it, expect, beforeEach } from 'vitest'; +import '../../src/components/dice-tray.js'; + +const seq = (vals) => { let i = 0; return () => vals[i++ % vals.length]; }; + +describe(' pack selector + params', () => { + let el; + beforeEach(() => { + document.body.innerHTML = ''; + el = document.createElement('dice-tray'); + document.body.appendChild(el); + }); + + it('renders a button for each registered pack plus Any die', () => { + expect(el.querySelector('[data-pack-id="coc-coc-d100"]') || el.querySelector('[data-pack-id="coc-d100"]')).not.toBe(null); + expect(el.querySelector('[data-pack-id="vanity-d6"]')).not.toBe(null); + expect(el.querySelector('[data-pack-id=""]')).not.toBe(null); // Any die + }); + + it('selecting a pack updates systemId and shows its param inputs', () => { + el.querySelector('[data-pack-id="coc-d100"]').click(); + expect(el.systemId).toBe('coc-d100'); + expect(el.querySelector('[data-param="target"]')).not.toBe(null); + }); + + it('feeds param inputs into interpret and renders the pack summary', () => { + el.querySelector('[data-pack-id="coc-d100"]').click(); + el.sides = 100; el.count = 1; + el.querySelector('[data-param="target"]').value = '40'; + el.querySelector('[data-param="target"]').dispatchEvent(new Event('input')); + el.rng = seq([0.95]); // d100 -> floor(0.95*100)+1 = 96 + el.roll(); + expect(el.last.verdict.target).toBe(40); + expect(el.last.verdict.level).toBe('fumble'); // 96 with target 40 + expect(el.querySelector('[data-role=verdict]').textContent).toContain('FUMBLE'); + }); + + it('Any die selection clears the rule-pack (blank verdict)', () => { + el.querySelector('[data-pack-id=""]').click(); + expect(el.systemId).toBe(''); + el.sides = 20; el.count = 1; el.rng = seq([0.5]); + el.roll(); + expect(el.last.verdict).toBe(null); + expect(el.querySelector('[data-role=verdict]').textContent).toBe(''); + }); +});