ec28933623
Ports the four core Mythic GME tools from reference/index.html into the modular structure. Fate Check and Random Event Check preserve the exact probability matrix and roll logic, now reading chaos factor live from the active campaign instead of local state. Meaning and UNE tables are served from data/tables/ via a new tables route. UNE motivation uses the published verb+noun tables instead of the reference's flattened phrase list. Dice (pool builder, custom roll, percentile, ability score) is ported verbatim as pure frontend logic.
190 lines
6.3 KiB
JavaScript
190 lines
6.3 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, 100: 0, 2: 0 };
|
|
const POOL_MAX = 25;
|
|
const DIE_ORDER = [4, 6, 8, 10, 12, 20, 100, 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>';
|
|
}
|
|
|
|
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', () => {
|
|
quickRoll(10, 1, 0, 'Percentile (tens)', 'percentileResult');
|
|
});
|
|
document.getElementById('percentileOnesBtn').addEventListener('click', () => {
|
|
quickRoll(10, 1, 0, 'Percentile (ones)', 'percentileResult');
|
|
});
|
|
|
|
document.getElementById('ability4d6Btn').addEventListener('click', () => {
|
|
quickRoll(6, 4, -4, '4d6 drop lowest (approx)', 'abilityResult');
|
|
});
|
|
document.getElementById('abilityArrayBtn').addEventListener('click', rollAbilityArray);
|
|
}
|
|
|
|
document.addEventListener('DOMContentLoaded', init);
|