Files
claudecode 736f744c03 feat: rework percentile roll into a stateful tens+ones flow
Tens and Ones now combine into a real 1-100 percentile result
(00+0 = 100) instead of rolling independently. Ones is disabled
until Tens is rolled, and a pending visual state shows the tens
value while waiting for the ones roll. Adds a Clear button to
reset the flow.
2026-07-01 00:34:47 -04:00

234 lines
7.7 KiB
JavaScript

// Mythic Oracle — Dice: pool builder, custom roll, percentile, ability score
// Pure frontend — no backend calls.
const dicePool = { 4: 0, 6: 0, 8: 0, 10: 0, 12: 0, 20: 0, 2: 0 };
const POOL_MAX = 25;
const DIE_ORDER = [4, 6, 8, 10, 12, 20, 2];
function addDie(sides) {
const total = DIE_ORDER.reduce((s, d) => s + dicePool[d], 0);
if (total >= POOL_MAX) return;
dicePool[sides]++;
updatePoolDisplay();
}
function clearPool() {
DIE_ORDER.forEach((d) => {
dicePool[d] = 0;
});
updatePoolDisplay();
const box = document.getElementById('poolResult');
box.className = 'dice-result-box';
box.innerHTML = '<span class="placeholder">Build a pool and roll.</span>';
}
function updatePoolDisplay() {
const total = DIE_ORDER.reduce((s, d) => s + dicePool[d], 0);
const display = document.getElementById('poolDisplay');
const capMsg = document.getElementById('poolCapMsg');
const parts = DIE_ORDER.filter((d) => dicePool[d] > 0).map((d) => `${dicePool[d]}d${d}`);
if (parts.length === 0) {
display.innerHTML = '<span class="placeholder">No dice in pool — click above to add.</span>';
} else {
display.textContent = parts.join(' + ');
}
const capped = total >= POOL_MAX;
capMsg.style.display = capped ? '' : 'none';
document.querySelectorAll('#diceGrid .die-btn').forEach((btn) => {
btn.disabled = capped;
});
}
function rollPool() {
const total = DIE_ORDER.reduce((s, d) => s + dicePool[d], 0);
const box = document.getElementById('poolResult');
if (total === 0) {
box.className = 'dice-result-box animate';
box.innerHTML = '<span class="placeholder">Add dice to the pool first.</span>';
void box.offsetWidth;
return;
}
const results = {};
let grandTotal = 0;
DIE_ORDER.forEach((sides) => {
if (dicePool[sides] > 0) {
results[sides] = [];
for (let i = 0; i < dicePool[sides]; i++) {
const roll = Math.floor(Math.random() * sides) + 1;
results[sides].push(roll);
grandTotal += roll;
}
}
});
const activeDice = DIE_ORDER.filter((s) => results[s]);
const groupedHTML = activeDice
.map((sides) => {
const rolls = results[sides];
const sum = rolls.reduce((a, b) => a + b, 0);
const rollStr = rolls.length > 1 ? `${rolls.join(' + ')} = ${sum}` : `${sum}`;
return `<div class="pool-result-group"><span class="pool-group-type">d${sides}</span>: ${rollStr}</div>`;
})
.join('');
box.className = 'dice-result-box animate';
box.innerHTML = `
<div class="dice-total">${grandTotal}</div>
<div class="dice-divider"></div>
<div class="pool-result-groups">${groupedHTML}</div>
`;
void box.offsetWidth;
}
function validateCustomRoll() {
const qty = document.getElementById('customQty').value;
const sides = document.getElementById('customSides').value;
const btn = document.getElementById('customRollBtn');
const isPosInt = (v) => v !== '' && Number.isInteger(Number(v)) && Number(v) > 0;
const valid = isPosInt(qty) && Number(qty) <= 25 && isPosInt(sides);
btn.disabled = !valid;
}
function rollCustom() {
const qty = Number(document.getElementById('customQty').value);
const sides = Number(document.getElementById('customSides').value);
const box = document.getElementById('customResult');
const rolls = [];
let total = 0;
for (let i = 0; i < qty; i++) {
const roll = Math.floor(Math.random() * sides) + 1;
rolls.push(roll);
total += roll;
}
box.className = 'dice-result-box animate';
box.innerHTML = `
<div class="dice-total">${total}</div>
<div class="dice-breakdown">${qty}d${sides} &nbsp;[${rolls.join(', ')}]</div>
`;
void box.offsetWidth;
}
function clearCustomRoll() {
const box = document.getElementById('customResult');
box.className = 'dice-result-box';
box.innerHTML = '<span class="placeholder">Enter a quantity and die size.</span>';
}
let percentileTens = null;
function updatePercentileButtons() {
document.getElementById('percentileTensBtn').disabled = percentileTens !== null;
document.getElementById('percentileOnesBtn').disabled = percentileTens === null;
}
function rollPercentileTens() {
percentileTens = Math.floor(Math.random() * 10) * 10; // 0, 10, 20 ... 90
updatePercentileButtons();
const box = document.getElementById('percentileResult');
box.className = 'dice-result-box animate pending';
box.innerHTML = `
<div class="dice-total pending-value">${percentileTens}</div>
<div class="dice-breakdown">Tens rolled — now roll ones.</div>
`;
void box.offsetWidth;
}
function rollPercentileOnes() {
if (percentileTens === null) return;
const ones = Math.floor(Math.random() * 10); // 0-9
const total = percentileTens + ones === 0 ? 100 : percentileTens + ones;
const box = document.getElementById('percentileResult');
box.className = 'dice-result-box animate';
box.innerHTML = `
<div class="dice-total">${total}</div>
<div class="dice-breakdown">Percentile &nbsp;[tens: ${percentileTens}, ones: ${ones}]</div>
`;
void box.offsetWidth;
percentileTens = null;
updatePercentileButtons();
}
function clearPercentile() {
percentileTens = null;
updatePercentileButtons();
const box = document.getElementById('percentileResult');
box.className = 'dice-result-box';
box.innerHTML = '<span class="placeholder">Roll tens to begin.</span>';
}
function quickRoll(sides, qty, mod, label, boxId) {
const rolls = [];
for (let i = 0; i < qty; i++) {
rolls.push(Math.floor(Math.random() * sides) + 1);
}
let total = rolls.reduce((a, b) => a + b, 0) + mod;
if (label.includes('drop lowest')) {
const sorted = [...rolls].sort((a, b) => a - b);
sorted.shift();
total = sorted.reduce((a, b) => a + b, 0);
}
const box = document.getElementById(boxId);
box.classList.add('animate');
box.innerHTML = `
<div class="dice-total">${total}</div>
<div class="dice-breakdown">${label} &nbsp;[${rolls.join(', ')}]</div>
`;
void box.offsetWidth;
}
function rollAbilityArray() {
const lines = [];
for (let i = 0; i < 6; i++) {
const rolls = [];
for (let j = 0; j < 4; j++) {
rolls.push(Math.floor(Math.random() * 6) + 1);
}
const sorted = [...rolls].sort((a, b) => a - b);
sorted.shift();
const total = sorted.reduce((a, b) => a + b, 0);
lines.push(`${rolls.join(', ')} &nbsp;→&nbsp; ${total}`);
}
const box = document.getElementById('abilityResult');
box.className = 'dice-result-box animate';
box.innerHTML = `<div class="dice-breakdown">${lines.join('<br>')}</div>`;
void box.offsetWidth;
}
function init() {
document.querySelectorAll('#diceGrid .die-btn').forEach((btn) => {
btn.addEventListener('click', () => addDie(Number(btn.dataset.sides)));
});
document.getElementById('poolRollBtn').addEventListener('click', rollPool);
document.getElementById('poolClearBtn').addEventListener('click', clearPool);
document.getElementById('customQty').addEventListener('input', validateCustomRoll);
document.getElementById('customSides').addEventListener('input', validateCustomRoll);
document.getElementById('customRollBtn').addEventListener('click', rollCustom);
document.getElementById('customClearBtn').addEventListener('click', clearCustomRoll);
document.getElementById('percentileTensBtn').addEventListener('click', rollPercentileTens);
document.getElementById('percentileOnesBtn').addEventListener('click', rollPercentileOnes);
document.getElementById('percentileClearBtn').addEventListener('click', clearPercentile);
updatePercentileButtons();
document.getElementById('ability4d6Btn').addEventListener('click', () => {
quickRoll(6, 4, -4, '4d6 drop lowest (approx)', 'abilityResult');
});
document.getElementById('abilityArrayBtn').addEventListener('click', rollAbilityArray);
}
document.addEventListener('DOMContentLoaded', init);