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('');
+ });
+});