@jjlmoya/utils-tabletop 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +65 -0
- package/scripts/postinstall.mjs +27 -0
- package/src/category/TabletopCategorySEO.astro +14 -0
- package/src/category/i18n/de.ts +27 -0
- package/src/category/i18n/en.ts +27 -0
- package/src/category/i18n/es.ts +27 -0
- package/src/category/i18n/fr.ts +27 -0
- package/src/category/i18n/id.ts +27 -0
- package/src/category/i18n/it.ts +27 -0
- package/src/category/i18n/ja.ts +27 -0
- package/src/category/i18n/ko.ts +27 -0
- package/src/category/i18n/nl.ts +27 -0
- package/src/category/i18n/pl.ts +27 -0
- package/src/category/i18n/pt.ts +27 -0
- package/src/category/i18n/ru.ts +27 -0
- package/src/category/i18n/sv.ts +27 -0
- package/src/category/i18n/tr.ts +27 -0
- package/src/category/i18n/zh.ts +27 -0
- package/src/category/index.ts +27 -0
- package/src/components/PreviewNavSidebar.astro +116 -0
- package/src/components/PreviewToolbar.astro +143 -0
- package/src/data.ts +10 -0
- package/src/entries.ts +10 -0
- package/src/env.d.ts +5 -0
- package/src/index.ts +21 -0
- package/src/layouts/PreviewLayout.astro +117 -0
- package/src/pages/[locale]/[slug].astro +161 -0
- package/src/pages/[locale].astro +251 -0
- package/src/pages/index.astro +4 -0
- package/src/tests/faq_count.test.ts +18 -0
- package/src/tests/i18n_coverage.test.ts +36 -0
- package/src/tests/locale_completeness.test.ts +29 -0
- package/src/tests/mocks/astro_mock.js +2 -0
- package/src/tests/no_en_dash.test.ts +41 -0
- package/src/tests/no_h1_in_components.test.ts +48 -0
- package/src/tests/schemas_fulfillment.test.ts +23 -0
- package/src/tests/seo_length.test.ts +22 -0
- package/src/tests/shared-test-helpers.ts +56 -0
- package/src/tests/slug_language_code_format.test.ts +23 -0
- package/src/tests/slug_uniqueness.test.ts +81 -0
- package/src/tests/title_quality.test.ts +55 -0
- package/src/tests/tool_exports.test.ts +34 -0
- package/src/tests/tool_validation.test.ts +16 -0
- package/src/tool/board-game-timer/bibliography.astro +16 -0
- package/src/tool/board-game-timer/bibliography.ts +12 -0
- package/src/tool/board-game-timer/board-game-timer.css +1615 -0
- package/src/tool/board-game-timer/client.ts +17 -0
- package/src/tool/board-game-timer/component.astro +22 -0
- package/src/tool/board-game-timer/components/DuelTimer.astro +75 -0
- package/src/tool/board-game-timer/components/MultiplayerTimer.astro +58 -0
- package/src/tool/board-game-timer/components/SetupPanel.astro +155 -0
- package/src/tool/board-game-timer/components/StatsModal.astro +52 -0
- package/src/tool/board-game-timer/entry.ts +76 -0
- package/src/tool/board-game-timer/i18n/de.ts +303 -0
- package/src/tool/board-game-timer/i18n/en.ts +304 -0
- package/src/tool/board-game-timer/i18n/es.ts +303 -0
- package/src/tool/board-game-timer/i18n/fr.ts +303 -0
- package/src/tool/board-game-timer/i18n/id.ts +303 -0
- package/src/tool/board-game-timer/i18n/it.ts +303 -0
- package/src/tool/board-game-timer/i18n/ja.ts +306 -0
- package/src/tool/board-game-timer/i18n/ko.ts +303 -0
- package/src/tool/board-game-timer/i18n/nl.ts +303 -0
- package/src/tool/board-game-timer/i18n/pl.ts +303 -0
- package/src/tool/board-game-timer/i18n/pt.ts +303 -0
- package/src/tool/board-game-timer/i18n/ru.ts +303 -0
- package/src/tool/board-game-timer/i18n/sv.ts +303 -0
- package/src/tool/board-game-timer/i18n/tr.ts +303 -0
- package/src/tool/board-game-timer/i18n/zh.ts +303 -0
- package/src/tool/board-game-timer/index.ts +9 -0
- package/src/tool/board-game-timer/logic.test.ts +282 -0
- package/src/tool/board-game-timer/logic.ts +6 -0
- package/src/tool/board-game-timer/modules/ColorHelper.ts +26 -0
- package/src/tool/board-game-timer/modules/DuelViewManager.ts +120 -0
- package/src/tool/board-game-timer/modules/FormatHelper.ts +12 -0
- package/src/tool/board-game-timer/modules/GameRunner.ts +136 -0
- package/src/tool/board-game-timer/modules/GameState.ts +48 -0
- package/src/tool/board-game-timer/modules/MultiplayerViewManager.ts +163 -0
- package/src/tool/board-game-timer/modules/SetupPanelManager.ts +209 -0
- package/src/tool/board-game-timer/modules/SoundManager.ts +109 -0
- package/src/tool/board-game-timer/modules/SpeechManager.ts +1 -0
- package/src/tool/board-game-timer/modules/StatsManager.ts +87 -0
- package/src/tool/board-game-timer/modules/StatsModalManager.ts +74 -0
- package/src/tool/board-game-timer/modules/TimerEngine.ts +175 -0
- package/src/tool/board-game-timer/modules/TimerEngineBase.ts +49 -0
- package/src/tool/board-game-timer/modules/TimerEngineClock.ts +127 -0
- package/src/tool/board-game-timer/modules/TimerStateHelper.ts +55 -0
- package/src/tool/board-game-timer/modules/particles.ts +108 -0
- package/src/tool/board-game-timer/seo.astro +16 -0
- package/src/tool/board-game-timer/types.ts +97 -0
- package/src/tool/dice-roller-simulator/bibliography.astro +16 -0
- package/src/tool/dice-roller-simulator/bibliography.ts +12 -0
- package/src/tool/dice-roller-simulator/client.ts +273 -0
- package/src/tool/dice-roller-simulator/component.astro +75 -0
- package/src/tool/dice-roller-simulator/components/DiceGrid.astro +55 -0
- package/src/tool/dice-roller-simulator/components/RollHistorySection.astro +26 -0
- package/src/tool/dice-roller-simulator/components/RollResultBanner.astro +10 -0
- package/src/tool/dice-roller-simulator/components/StatsPanel.astro +36 -0
- package/src/tool/dice-roller-simulator/dice-roller-simulator.css +1319 -0
- package/src/tool/dice-roller-simulator/entry.ts +39 -0
- package/src/tool/dice-roller-simulator/i18n/de.ts +305 -0
- package/src/tool/dice-roller-simulator/i18n/en.ts +305 -0
- package/src/tool/dice-roller-simulator/i18n/es.ts +305 -0
- package/src/tool/dice-roller-simulator/i18n/fr.ts +305 -0
- package/src/tool/dice-roller-simulator/i18n/id.ts +305 -0
- package/src/tool/dice-roller-simulator/i18n/it.ts +305 -0
- package/src/tool/dice-roller-simulator/i18n/ja.ts +305 -0
- package/src/tool/dice-roller-simulator/i18n/ko.ts +305 -0
- package/src/tool/dice-roller-simulator/i18n/nl.ts +305 -0
- package/src/tool/dice-roller-simulator/i18n/pl.ts +305 -0
- package/src/tool/dice-roller-simulator/i18n/pt.ts +305 -0
- package/src/tool/dice-roller-simulator/i18n/ru.ts +305 -0
- package/src/tool/dice-roller-simulator/i18n/sv.ts +305 -0
- package/src/tool/dice-roller-simulator/i18n/tr.ts +305 -0
- package/src/tool/dice-roller-simulator/i18n/zh.ts +305 -0
- package/src/tool/dice-roller-simulator/index.ts +9 -0
- package/src/tool/dice-roller-simulator/modules/chart.ts +32 -0
- package/src/tool/dice-roller-simulator/modules/history.ts +87 -0
- package/src/tool/dice-roller-simulator/modules/particles.ts +22 -0
- package/src/tool/dice-roller-simulator/seo.astro +16 -0
- package/src/tools.ts +9 -0
- package/src/types.ts +70 -0
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
import { spawnParticle, spawnRollPop } from './modules/particles';
|
|
2
|
+
import { drawDistributionChart } from './modules/chart';
|
|
3
|
+
import { getHistory, saveHistory, renderHistory } from './modules/history';
|
|
4
|
+
|
|
5
|
+
const rollButton = document.getElementById('roll-button') as HTMLButtonElement;
|
|
6
|
+
const clearHistoryBtn = document.getElementById('clear-history-btn') as HTMLElement;
|
|
7
|
+
const clearPoolBtn = document.getElementById('clear-pool-btn') as HTMLElement;
|
|
8
|
+
|
|
9
|
+
const modifierInput = document.getElementById('modifier-input') as HTMLInputElement;
|
|
10
|
+
const modDecBtn = document.getElementById('mod-dec-btn') as HTMLElement;
|
|
11
|
+
const modIncBtn = document.getElementById('mod-inc-btn') as HTMLElement;
|
|
12
|
+
|
|
13
|
+
const rollArena = document.getElementById('roll-arena') as HTMLElement;
|
|
14
|
+
const trayRim = document.querySelector('.tray-rim') as HTMLElement;
|
|
15
|
+
const arenaPlaceholder = document.getElementById('arena-placeholder') as HTMLElement;
|
|
16
|
+
const diceResultsContainer = document.getElementById('dice-results-container') as HTMLElement;
|
|
17
|
+
|
|
18
|
+
const rollResultBanner = document.getElementById('roll-result-banner') as HTMLElement;
|
|
19
|
+
const resultTotalNumber = document.getElementById('result-total-number') as HTMLElement;
|
|
20
|
+
const resultBreakdownDisplay = document.getElementById('result-breakdown-display') as HTMLElement;
|
|
21
|
+
|
|
22
|
+
const totalRollsVal = document.getElementById('total-rolls-val') as HTMLElement;
|
|
23
|
+
const averageRollVal = document.getElementById('average-roll-val') as HTMLElement;
|
|
24
|
+
const lastRollVal = document.getElementById('last-roll-val') as HTMLElement;
|
|
25
|
+
const totalRollsBadge = document.getElementById('total-rolls-badge') as HTMLElement;
|
|
26
|
+
|
|
27
|
+
const historyList = document.getElementById('history-list') as HTMLElement;
|
|
28
|
+
const noHistoryMsg = document.getElementById('no-history-msg') as HTMLElement;
|
|
29
|
+
const distributionBarsContainer = document.getElementById('distribution-chart-bars') as HTMLElement;
|
|
30
|
+
|
|
31
|
+
const activePool = new Map<string, number>([['D4', 0], ['D6', 0], ['D8', 0], ['D10', 0], ['D12', 0], ['D20', 0], ['D100', 0]]);
|
|
32
|
+
const maxValues = new Map<string, number>([['D4', 4], ['D6', 6], ['D8', 8], ['D10', 10], ['D12', 12], ['D20', 20], ['D100', 100]]);
|
|
33
|
+
|
|
34
|
+
const extraTabButtons = document.querySelectorAll('.extra-tab-btn');
|
|
35
|
+
const extraTabPanes = document.querySelectorAll('.extra-tab-pane');
|
|
36
|
+
|
|
37
|
+
extraTabButtons.forEach(btn => {
|
|
38
|
+
btn.addEventListener('click', () => {
|
|
39
|
+
extraTabButtons.forEach(b => b.classList.remove('active'));
|
|
40
|
+
extraTabPanes.forEach(p => p.classList.remove('active'));
|
|
41
|
+
btn.classList.add('active');
|
|
42
|
+
const id = btn.getAttribute('data-extra-tab');
|
|
43
|
+
if (id) document.getElementById(id)?.classList.add('active');
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
function updateRollButtonState() {
|
|
48
|
+
let totalCount = 0;
|
|
49
|
+
activePool.forEach(count => totalCount += count);
|
|
50
|
+
rollButton.disabled = totalCount === 0;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function pulseElement(el: HTMLElement) {
|
|
54
|
+
if (!el) return;
|
|
55
|
+
el.classList.remove('pulse-value');
|
|
56
|
+
void el.offsetWidth;
|
|
57
|
+
el.classList.add('pulse-value');
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function bounceModifierInput() {
|
|
61
|
+
if (!modifierInput) return;
|
|
62
|
+
modifierInput.classList.remove('bounce-effect');
|
|
63
|
+
void modifierInput.offsetWidth;
|
|
64
|
+
modifierInput.classList.add('bounce-effect');
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function callRenderHistory() {
|
|
68
|
+
const refs = {
|
|
69
|
+
list: historyList,
|
|
70
|
+
emptyMsg: noHistoryMsg,
|
|
71
|
+
totalVal: totalRollsVal,
|
|
72
|
+
averageVal: averageRollVal,
|
|
73
|
+
lastVal: lastRollVal,
|
|
74
|
+
badge: totalRollsBadge,
|
|
75
|
+
};
|
|
76
|
+
renderHistory(refs, (totals) => {
|
|
77
|
+
drawDistributionChart(distributionBarsContainer, totals);
|
|
78
|
+
});
|
|
79
|
+
if (totalRollsVal) pulseElement(totalRollsVal);
|
|
80
|
+
if (averageRollVal) pulseElement(averageRollVal);
|
|
81
|
+
if (lastRollVal) pulseElement(lastRollVal);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function attachDieControl(row: HTMLElement, btn: HTMLElement | null, offset: number) {
|
|
85
|
+
if (!btn) return;
|
|
86
|
+
const dieType = row.getAttribute('data-die-type') || '';
|
|
87
|
+
const decBtn = row.querySelector('.dec-btn') as HTMLButtonElement;
|
|
88
|
+
const badge = row.querySelector('.die-counter-badge') as HTMLElement;
|
|
89
|
+
btn.addEventListener('click', (e) => {
|
|
90
|
+
e.stopPropagation();
|
|
91
|
+
const curr = Math.max(0, (activePool.get(dieType) || 0) + offset);
|
|
92
|
+
activePool.set(dieType, curr);
|
|
93
|
+
badge.textContent = curr.toString();
|
|
94
|
+
decBtn.disabled = curr === 0;
|
|
95
|
+
badge.classList.toggle('hidden', curr === 0);
|
|
96
|
+
updateRollButtonState();
|
|
97
|
+
const rect = row.getBoundingClientRect();
|
|
98
|
+
const txt = offset > 0 ? `+${offset}` : `${offset}`;
|
|
99
|
+
spawnParticle(row, txt, e.clientX - rect.left, e.clientY - rect.top);
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function setupDieRow(row: Element) {
|
|
104
|
+
const rowEl = row as HTMLElement;
|
|
105
|
+
const decBtn = rowEl.querySelector('.dec-btn') as HTMLElement;
|
|
106
|
+
const incBtn = rowEl.querySelector('.inc-btn') as HTMLElement;
|
|
107
|
+
const mainBtn = rowEl.querySelector('.die-main-button') as HTMLElement;
|
|
108
|
+
attachDieControl(rowEl, decBtn, -1);
|
|
109
|
+
attachDieControl(rowEl, incBtn, 1);
|
|
110
|
+
attachDieControl(rowEl, mainBtn, 1);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
document.querySelectorAll('.die-control-row').forEach(row => {
|
|
114
|
+
setupDieRow(row);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
clearPoolBtn.addEventListener('click', () => {
|
|
118
|
+
activePool.forEach((_, key) => activePool.set(key, 0));
|
|
119
|
+
document.querySelectorAll('.die-control-row').forEach(row => {
|
|
120
|
+
const decBtn = row.querySelector('.dec-btn') as HTMLButtonElement;
|
|
121
|
+
const countBadge = row.querySelector('.die-counter-badge') as HTMLElement;
|
|
122
|
+
countBadge.textContent = '0';
|
|
123
|
+
countBadge.classList.add('hidden');
|
|
124
|
+
decBtn.disabled = true;
|
|
125
|
+
});
|
|
126
|
+
updateRollButtonState();
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
modDecBtn.addEventListener('click', (e) => {
|
|
130
|
+
const val = parseInt(modifierInput.value) || 0;
|
|
131
|
+
modifierInput.value = (val - 1).toString();
|
|
132
|
+
bounceModifierInput();
|
|
133
|
+
const rect = modifierInput.parentElement?.getBoundingClientRect();
|
|
134
|
+
if (rect) {
|
|
135
|
+
spawnParticle(modifierInput.parentElement as HTMLElement, '-1', e.clientX - rect.left, e.clientY - rect.top);
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
modIncBtn.addEventListener('click', (e) => {
|
|
140
|
+
const val = parseInt(modifierInput.value) || 0;
|
|
141
|
+
modifierInput.value = (val + 1).toString();
|
|
142
|
+
bounceModifierInput();
|
|
143
|
+
const rect = modifierInput.parentElement?.getBoundingClientRect();
|
|
144
|
+
if (rect) {
|
|
145
|
+
spawnParticle(modifierInput.parentElement as HTMLElement, '+1', e.clientX - rect.left, e.clientY - rect.top);
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
function triggerFeltImpact() {
|
|
150
|
+
if (trayRim) {
|
|
151
|
+
trayRim.classList.remove('felt-impact');
|
|
152
|
+
void trayRim.offsetWidth;
|
|
153
|
+
trayRim.classList.add('felt-impact');
|
|
154
|
+
setTimeout(() => trayRim.classList.remove('felt-impact'), 300);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function updateBanner(rolls: { type: string; value: number }[], modifier: number, total: number) {
|
|
159
|
+
if (!rollResultBanner || !resultTotalNumber || !resultBreakdownDisplay) return;
|
|
160
|
+
resultTotalNumber.textContent = total.toString();
|
|
161
|
+
const diceBreakdownHtml = rolls.map(r => {
|
|
162
|
+
const typeLower = r.type.toLowerCase();
|
|
163
|
+
const maxVal = maxValues.get(r.type) || 6;
|
|
164
|
+
let extraClass = '';
|
|
165
|
+
if (r.value === maxVal) {
|
|
166
|
+
extraClass = ' die-critical';
|
|
167
|
+
} else if (r.value === 1) {
|
|
168
|
+
extraClass = ' die-fumble';
|
|
169
|
+
}
|
|
170
|
+
return `<span class="breakdown-die-tag${extraClass}" data-die-type="${typeLower}">${r.value}</span>`;
|
|
171
|
+
}).join(' <span class="breakdown-operator">+</span> ');
|
|
172
|
+
let finalHtml = diceBreakdownHtml;
|
|
173
|
+
if (modifier !== 0) {
|
|
174
|
+
const sign = modifier >= 0 ? '+' : '-';
|
|
175
|
+
finalHtml += ` <span class="breakdown-operator">${sign}</span> <span class="breakdown-modifier-tag">${Math.abs(modifier)}</span>`;
|
|
176
|
+
}
|
|
177
|
+
resultBreakdownDisplay.innerHTML = finalHtml;
|
|
178
|
+
rollResultBanner.classList.remove('hidden');
|
|
179
|
+
rollResultBanner.classList.remove('banner-pop-effect');
|
|
180
|
+
void rollResultBanner.offsetWidth;
|
|
181
|
+
rollResultBanner.classList.add('banner-pop-effect');
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function rollDiceTumble(selectedDice: { type: string; max: number }[], visualDiceEls: HTMLElement[]): Promise<{ type: string; value: number }[]> {
|
|
185
|
+
return new Promise((resolve) => {
|
|
186
|
+
const intervalId = setInterval(() => {
|
|
187
|
+
visualDiceEls.forEach((el, index) => {
|
|
188
|
+
const d = selectedDice[index];
|
|
189
|
+
el.textContent = (Math.floor(Math.random() * d.max) + 1).toString();
|
|
190
|
+
});
|
|
191
|
+
}, 60);
|
|
192
|
+
setTimeout(() => {
|
|
193
|
+
clearInterval(intervalId);
|
|
194
|
+
const results = visualDiceEls.map((el, index) => {
|
|
195
|
+
const d = selectedDice[index];
|
|
196
|
+
el.classList.remove('rolling-pulse');
|
|
197
|
+
const val = Math.floor(Math.random() * d.max) + 1;
|
|
198
|
+
if (val === d.max) {
|
|
199
|
+
el.classList.add('die-critical');
|
|
200
|
+
el.innerHTML = `<span class="die-val">${val}</span><svg class="die-svg-icon-crit" viewBox="0 0 24 24"><path fill="currentColor" d="M12,17.27L18.18,21L16.54,13.97L22,9.24L14.81,8.62L12,2L9.19,8.62L2,9.24L7.45,13.97L5.82,21L12,17.27Z"/></svg>`;
|
|
201
|
+
} else if (val === 1) {
|
|
202
|
+
el.classList.add('die-fumble');
|
|
203
|
+
el.innerHTML = `<span class="die-val">${val}</span><svg class="die-svg-icon-fumble" viewBox="0 0 24 24"><path fill="currentColor" d="M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2M12,4A8,8 0 0,1 20,12A8,8 0 0,1 12,20A8,8 0 0,1 4,12A8,8 0 0,1 12,4M11,7V13H13V7H11M11,15V17H13V15H11Z"/></svg>`;
|
|
204
|
+
} else {
|
|
205
|
+
el.textContent = val.toString();
|
|
206
|
+
}
|
|
207
|
+
return { type: d.type, value: val };
|
|
208
|
+
});
|
|
209
|
+
resolve(results);
|
|
210
|
+
}, 500);
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function createVisualDice(selectedDice: { type: string; max: number }[]): HTMLElement[] {
|
|
215
|
+
return selectedDice.map((d) => {
|
|
216
|
+
const el = document.createElement('div');
|
|
217
|
+
el.className = 'visual-die rolling-pulse';
|
|
218
|
+
el.setAttribute('data-die-type', d.type.toLowerCase());
|
|
219
|
+
el.textContent = '?';
|
|
220
|
+
diceResultsContainer.appendChild(el);
|
|
221
|
+
return el;
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function saveRollLog(rolls: { type: string; value: number }[], modifier: number, total: number) {
|
|
226
|
+
const history = getHistory();
|
|
227
|
+
history.push({
|
|
228
|
+
id: 'roll_' + Date.now(),
|
|
229
|
+
timestamp: new Date().toISOString(),
|
|
230
|
+
dice: rolls.map(r => `${r.type.toLowerCase()}:${r.value}`),
|
|
231
|
+
modifier,
|
|
232
|
+
total,
|
|
233
|
+
});
|
|
234
|
+
saveHistory(history);
|
|
235
|
+
callRenderHistory();
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
async function onRoll() {
|
|
239
|
+
const selectedDice: { type: string; max: number }[] = [];
|
|
240
|
+
activePool.forEach((count, type) => {
|
|
241
|
+
const max = maxValues.get(type) || 6;
|
|
242
|
+
for (let i = 0; i < count; i++) {
|
|
243
|
+
selectedDice.push({ type, max });
|
|
244
|
+
}
|
|
245
|
+
});
|
|
246
|
+
if (selectedDice.length === 0) return;
|
|
247
|
+
arenaPlaceholder.style.display = 'none';
|
|
248
|
+
diceResultsContainer.innerHTML = '';
|
|
249
|
+
if (rollResultBanner) rollResultBanner.classList.add('hidden');
|
|
250
|
+
rollButton.disabled = true;
|
|
251
|
+
const visualDiceEls = createVisualDice(selectedDice);
|
|
252
|
+
const rolls = await rollDiceTumble(selectedDice, visualDiceEls);
|
|
253
|
+
const modifier = parseInt(modifierInput.value) || 0;
|
|
254
|
+
const total = rolls.reduce((acc, r) => acc + r.value, 0) + modifier;
|
|
255
|
+
triggerFeltImpact();
|
|
256
|
+
spawnRollPop(rollArena, total);
|
|
257
|
+
updateBanner(rolls, modifier, total);
|
|
258
|
+
saveRollLog(rolls, modifier, total);
|
|
259
|
+
updateRollButtonState();
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
rollButton.addEventListener('click', onRoll);
|
|
263
|
+
|
|
264
|
+
clearHistoryBtn.addEventListener('click', () => {
|
|
265
|
+
saveHistory([]);
|
|
266
|
+
callRenderHistory();
|
|
267
|
+
arenaPlaceholder.style.display = 'block';
|
|
268
|
+
diceResultsContainer.innerHTML = '';
|
|
269
|
+
rollResultBanner?.classList.add('hidden');
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
updateRollButtonState();
|
|
273
|
+
callRenderHistory();
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
---
|
|
2
|
+
import DiceGrid from './components/DiceGrid.astro';
|
|
3
|
+
import RollHistorySection from './components/RollHistorySection.astro';
|
|
4
|
+
import StatsPanel from './components/StatsPanel.astro';
|
|
5
|
+
import RollResultBanner from './components/RollResultBanner.astro';
|
|
6
|
+
|
|
7
|
+
interface Props {
|
|
8
|
+
ui: Record<string, string>;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const { ui } = Astro.props;
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
<div class="tool-main-card" data-ui={JSON.stringify(ui)}>
|
|
15
|
+
<div class="tabletop-gaming-dashboard">
|
|
16
|
+
<div class="forge-and-tray-grid">
|
|
17
|
+
<DiceGrid labels={ui} />
|
|
18
|
+
|
|
19
|
+
<div class="dashboard-center">
|
|
20
|
+
<div class="rolling-tray-container">
|
|
21
|
+
<div class="tray-rim">
|
|
22
|
+
<div class="tray-felt" id="roll-arena">
|
|
23
|
+
<div class="felt-placeholder" id="arena-placeholder">
|
|
24
|
+
<svg viewBox="0 0 100 100" class="felt-crest-svg" aria-hidden="true">
|
|
25
|
+
<circle cx="50" cy="50" r="45" fill="none" stroke="currentColor" stroke-opacity="0.15" stroke-width="1" />
|
|
26
|
+
<circle cx="50" cy="50" r="40" fill="none" stroke="currentColor" stroke-opacity="0.08" stroke-width="1" stroke-dasharray="3,3" />
|
|
27
|
+
|
|
28
|
+
<polygon points="50,15 80,32 80,68 50,85 20,68 20,32" fill="currentColor" fill-opacity="0.03" stroke="currentColor" stroke-width="2" />
|
|
29
|
+
|
|
30
|
+
<polygon points="50,38 72,50 50,75 28,50" fill="none" stroke="currentColor" stroke-width="1.5" />
|
|
31
|
+
<line x1="50" y1="15" x2="50" y2="38" stroke="currentColor" stroke-width="1.5" />
|
|
32
|
+
<line x1="20" y1="32" x2="28" y2="50" stroke="currentColor" stroke-width="1.5" />
|
|
33
|
+
<line x1="80" y1="32" x2="72" y2="50" stroke="currentColor" stroke-width="1.5" />
|
|
34
|
+
<line x1="20" y1="68" x2="28" y2="50" stroke="currentColor" stroke-width="1.5" />
|
|
35
|
+
<line x1="80" y1="68" x2="72" y2="50" stroke="currentColor" stroke-width="1.5" />
|
|
36
|
+
<line x1="50" y1="85" x2="50" y2="75" stroke="currentColor" stroke-width="1.5" />
|
|
37
|
+
<line x1="28" y1="50" x2="50" y2="75" stroke="currentColor" stroke-width="1.5" />
|
|
38
|
+
<line x1="72" y1="50" x2="50" y2="75" stroke="currentColor" stroke-width="1.5" />
|
|
39
|
+
|
|
40
|
+
<text x="50" y="58" font-size="12" font-weight="900" text-anchor="middle" fill="currentColor" font-family="'Outfit', sans-serif">20</text>
|
|
41
|
+
</svg>
|
|
42
|
+
<p class="felt-hint-text">Add dice to roll</p>
|
|
43
|
+
</div>
|
|
44
|
+
<div class="felt-dice-container" id="dice-results-container"></div>
|
|
45
|
+
</div>
|
|
46
|
+
</div>
|
|
47
|
+
</div>
|
|
48
|
+
|
|
49
|
+
<RollResultBanner />
|
|
50
|
+
|
|
51
|
+
<button type="button" id="roll-button" class="btn-roll-hero" disabled>
|
|
52
|
+
ROLL DICE
|
|
53
|
+
</button>
|
|
54
|
+
</div>
|
|
55
|
+
</div>
|
|
56
|
+
|
|
57
|
+
<div class="tabletop-extra-tabs">
|
|
58
|
+
<div class="tabs-nav">
|
|
59
|
+
<button type="button" class="extra-tab-btn active" data-extra-tab="logs-panel">Roll Logs</button>
|
|
60
|
+
<button type="button" class="extra-tab-btn" data-extra-tab="stats-panel">Statistics</button>
|
|
61
|
+
</div>
|
|
62
|
+
|
|
63
|
+
<div class="extra-tabs-content">
|
|
64
|
+
<div class="extra-tab-pane active" id="logs-panel">
|
|
65
|
+
<RollHistorySection labels={ui} />
|
|
66
|
+
</div>
|
|
67
|
+
<div class="extra-tab-pane" id="stats-panel">
|
|
68
|
+
<StatsPanel labels={ui} />
|
|
69
|
+
</div>
|
|
70
|
+
</div>
|
|
71
|
+
</div>
|
|
72
|
+
</div>
|
|
73
|
+
</div>
|
|
74
|
+
|
|
75
|
+
<script src="./client.ts"></script>
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
---
|
|
2
|
+
import { Icon } from 'astro-icon/components';
|
|
3
|
+
|
|
4
|
+
interface Props {
|
|
5
|
+
labels: Record<string, string>;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const { labels } = Astro.props;
|
|
9
|
+
|
|
10
|
+
const diceTypes = [
|
|
11
|
+
{ name: 'D4', max: 4, icon: 'mdi:triangle' },
|
|
12
|
+
{ name: 'D6', max: 6, icon: 'mdi:square' },
|
|
13
|
+
{ name: 'D8', max: 8, icon: 'mdi:octagram' },
|
|
14
|
+
{ name: 'D10', max: 10, icon: 'mdi:rhombus' },
|
|
15
|
+
{ name: 'D12', max: 12, icon: 'mdi:hexagon-slice-6' },
|
|
16
|
+
{ name: 'D20', max: 20, icon: 'mdi:hexagon-multiple' },
|
|
17
|
+
{ name: 'D100', max: 100, icon: 'mdi:circle-slice-8' },
|
|
18
|
+
];
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
<div class="dice-forge-panel">
|
|
22
|
+
<div class="panel-header">
|
|
23
|
+
<h3>{labels.selectDice || "Dice Selection"}</h3>
|
|
24
|
+
<button type="button" id="clear-pool-btn" class="btn-text-action">Clear Pool</button>
|
|
25
|
+
</div>
|
|
26
|
+
|
|
27
|
+
<div class="dice-selectors-column">
|
|
28
|
+
{diceTypes.map((die) => (
|
|
29
|
+
<div class="die-control-row" data-die-type={die.name} data-die-max={die.max}>
|
|
30
|
+
<button type="button" class="die-adjust-btn dec-btn" disabled aria-label="Decrease">-</button>
|
|
31
|
+
|
|
32
|
+
<div class="die-main-button" title="Click to add die">
|
|
33
|
+
<div class="die-visual-icon">
|
|
34
|
+
<Icon name={die.icon} width="36" height="36" />
|
|
35
|
+
<span class="die-counter-badge hidden">0</span>
|
|
36
|
+
</div>
|
|
37
|
+
<span class="die-name-label">{die.name}</span>
|
|
38
|
+
</div>
|
|
39
|
+
|
|
40
|
+
<button type="button" class="die-adjust-btn inc-btn" aria-label="Increase">+</button>
|
|
41
|
+
</div>
|
|
42
|
+
))}
|
|
43
|
+
</div>
|
|
44
|
+
|
|
45
|
+
<div class="modifier-row-panel">
|
|
46
|
+
<div class="modifier-box">
|
|
47
|
+
<span class="config-label">{labels.modifierLabel || "Modifier"}</span>
|
|
48
|
+
<div class="modifier-input-wrapper">
|
|
49
|
+
<button type="button" id="mod-dec-btn" class="mod-adjust">-</button>
|
|
50
|
+
<input type="number" id="modifier-input" class="mod-val-input" value="0" readonly />
|
|
51
|
+
<button type="button" id="mod-inc-btn" class="mod-adjust">+</button>
|
|
52
|
+
</div>
|
|
53
|
+
</div>
|
|
54
|
+
</div>
|
|
55
|
+
</div>
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
---
|
|
2
|
+
import { Icon } from 'astro-icon/components';
|
|
3
|
+
|
|
4
|
+
interface Props {
|
|
5
|
+
labels: Record<string, string>;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const { labels } = Astro.props;
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
<div class="tabletop-history-panel">
|
|
12
|
+
<div class="panel-header">
|
|
13
|
+
<h3>{labels.historyTitle || "Roll Logs"}</h3>
|
|
14
|
+
<button type="button" id="clear-history-btn" class="btn-clear-all" title={labels.clearButton || "Clear History"}>
|
|
15
|
+
<Icon name="mdi:trash-can-outline" width="18" height="18" />
|
|
16
|
+
</button>
|
|
17
|
+
</div>
|
|
18
|
+
|
|
19
|
+
<div class="history-scroll-viewport">
|
|
20
|
+
<div id="no-history-msg" class="history-empty-message">
|
|
21
|
+
<Icon name="mdi:dice-multiple-outline" width="48" height="48" class="empty-icon" />
|
|
22
|
+
<p>{labels.noHistory || "Roll dice to populate log history."}</p>
|
|
23
|
+
</div>
|
|
24
|
+
<div id="history-list" class="history-cards-list"></div>
|
|
25
|
+
</div>
|
|
26
|
+
</div>
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
---
|
|
2
|
+
---
|
|
3
|
+
|
|
4
|
+
<div id="roll-result-banner" class="roll-result-banner hidden">
|
|
5
|
+
<div class="banner-total">
|
|
6
|
+
<span class="banner-total-label">Result</span>
|
|
7
|
+
<span id="result-total-number" class="banner-total-value">0</span>
|
|
8
|
+
</div>
|
|
9
|
+
<div id="result-breakdown-display" class="banner-breakdown">0</div>
|
|
10
|
+
</div>
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
---
|
|
2
|
+
interface Props {
|
|
3
|
+
labels: Record<string, string>;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
const { labels } = Astro.props;
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
<div class="stats-showcase-panel">
|
|
10
|
+
<div class="stats-glow-grid">
|
|
11
|
+
<div class="stat-glass-card">
|
|
12
|
+
<span class="stat-card-title">{labels.totalRolls || "Total Rolls"}</span>
|
|
13
|
+
<span id="total-rolls-val" class="stat-card-number">0</span>
|
|
14
|
+
</div>
|
|
15
|
+
<div class="stat-glass-card">
|
|
16
|
+
<span class="stat-card-title">{labels.averageRoll || "Average"}</span>
|
|
17
|
+
<span id="average-roll-val" class="stat-card-number">--</span>
|
|
18
|
+
</div>
|
|
19
|
+
<div class="stat-glass-card">
|
|
20
|
+
<span class="stat-card-title">{labels.lastRoll || "Last Roll"}</span>
|
|
21
|
+
<span id="last-roll-val" class="stat-card-number">--</span>
|
|
22
|
+
</div>
|
|
23
|
+
</div>
|
|
24
|
+
|
|
25
|
+
<div class="distribution-chart-wrapper">
|
|
26
|
+
<div class="chart-header">
|
|
27
|
+
<h4>Roll Outcomes Distribution</h4>
|
|
28
|
+
<span class="chart-subtitle">Frequency of totals</span>
|
|
29
|
+
</div>
|
|
30
|
+
<div class="css-chart-viewport">
|
|
31
|
+
<div id="distribution-chart-bars" class="distribution-bars-container">
|
|
32
|
+
<div class="chart-empty-state">No rolls recorded yet</div>
|
|
33
|
+
</div>
|
|
34
|
+
</div>
|
|
35
|
+
</div>
|
|
36
|
+
</div>
|