feat: dice-tray rule-pack selector + per-pack param inputs
This commit is contained in:
@@ -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) =>
|
||||
`<button class="pack ${p.id === this._systemId ? 'on' : ''}" data-pack-id="${p.id}">${p.label}</button>`).join('');
|
||||
const active = getRulePack(this._systemId);
|
||||
const paramInputs = (active && active.params ? active.params : []).map((p) =>
|
||||
`<label class="param">${p.label}<input type="number" data-param="${p.key}" value="${this.params[p.key] ?? p.default}" /></label>`).join('');
|
||||
|
||||
this.innerHTML = `
|
||||
<div class="dice-tray">
|
||||
<div class="pack-bar">${packBar}</div>
|
||||
<div class="picker">${picker}</div>
|
||||
<div class="params">${paramInputs}</div>
|
||||
<div class="felt">${felt}</div>
|
||||
<div class="verdict" data-role="verdict">${verdict}</div>
|
||||
<button class="roll" data-role="roll">Roll</button>
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -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('<dice-tray> 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('');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user