736f744c03
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.
234 lines
7.7 KiB
JavaScript
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} [${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 [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} [${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(', ')} → ${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);
|