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.
125 lines
4.2 KiB
JavaScript
125 lines
4.2 KiB
JavaScript
// Mythic Oracle — Fate Check and Random Event Check
|
|
|
|
import { getActiveCampaign } from './app.js';
|
|
|
|
const NO_CAMPAIGN_MESSAGE = 'Select or create a campaign to use the oracle.';
|
|
|
|
// Mythic GME v2 probability matrix — base% -> thresholds indexed by chaos factor 1-9
|
|
const probTable = {
|
|
5: [1, 1, 2, 3, 4, 5, 6, 7, 8],
|
|
15: [1, 2, 4, 6, 8, 10, 12, 15, 18],
|
|
25: [2, 4, 7, 11, 15, 19, 23, 27, 32],
|
|
35: [3, 6, 10, 15, 20, 25, 30, 35, 41],
|
|
50: [5, 9, 14, 20, 26, 32, 38, 44, 50],
|
|
65: [9, 14, 20, 28, 36, 44, 52, 60, 68],
|
|
75: [11, 17, 24, 33, 42, 51, 60, 68, 76],
|
|
85: [14, 21, 30, 40, 50, 60, 70, 78, 85],
|
|
95: [18, 27, 38, 50, 62, 73, 82, 89, 95],
|
|
};
|
|
|
|
let selectedProb = { label: '50/50', base: 50 };
|
|
|
|
function initProbGrid() {
|
|
const buttons = document.querySelectorAll('#probGrid .prob-btn');
|
|
buttons.forEach((btn) => {
|
|
btn.addEventListener('click', () => {
|
|
buttons.forEach((b) => b.classList.remove('selected'));
|
|
btn.classList.add('selected');
|
|
selectedProb = { label: btn.dataset.label, base: Number(btn.dataset.base) };
|
|
});
|
|
});
|
|
}
|
|
|
|
function rollFate() {
|
|
const box = document.getElementById('fateResult');
|
|
const campaign = getActiveCampaign();
|
|
|
|
if (!campaign) {
|
|
box.className = 'result-box';
|
|
box.innerHTML = `<span class="placeholder">${NO_CAMPAIGN_MESSAGE}</span>`;
|
|
return;
|
|
}
|
|
|
|
const chaos = campaign.chaos_factor;
|
|
const roll = Math.floor(Math.random() * 100) + 1;
|
|
const thresholds = probTable[selectedProb.base];
|
|
const threshold = thresholds[chaos - 1];
|
|
|
|
// Exceptional results occur on doubles (11,22,33...) or within 10% of threshold
|
|
const isDouble = roll % 11 === 0 || (roll < 100 && Math.floor(roll / 10) === roll % 10);
|
|
const yesResult = roll <= threshold;
|
|
|
|
let resultClass, mainText, subText;
|
|
|
|
if (yesResult && isDouble) {
|
|
resultClass = 'exceptional-yes';
|
|
mainText = 'Exceptional Yes';
|
|
subText = 'Something beyond what was asked occurs in your favor.';
|
|
} else if (!yesResult && isDouble) {
|
|
resultClass = 'exceptional-no';
|
|
mainText = 'Exceptional No';
|
|
subText = 'Something beyond a simple no — complications arise.';
|
|
} else if (yesResult) {
|
|
resultClass = 'yes';
|
|
mainText = 'Yes';
|
|
subText = 'The answer is affirmative.';
|
|
} else {
|
|
resultClass = 'no';
|
|
mainText = 'No';
|
|
subText = 'The answer is negative.';
|
|
}
|
|
|
|
// Random event check bundled into fate check (Mythic v2 behavior)
|
|
if (roll <= chaos) {
|
|
subText += ' · Random event triggered.';
|
|
resultClass = yesResult ? resultClass : 'random-event';
|
|
}
|
|
|
|
const mainColorClass = yesResult ? 'yes-color' : resultClass === 'random-event' ? 'blue-color' : 'no-color';
|
|
|
|
box.className = `result-box animate ${resultClass}`;
|
|
box.innerHTML = `
|
|
<div class="result-main ${mainColorClass}">${mainText}</div>
|
|
<div class="result-sub">${subText}</div>
|
|
<div class="result-roll">Roll: ${roll} | Threshold: ${threshold} | Prob: ${selectedProb.label} | CF: ${chaos}</div>
|
|
`;
|
|
}
|
|
|
|
function rollEventCheck() {
|
|
const box = document.getElementById('eventResult');
|
|
const campaign = getActiveCampaign();
|
|
|
|
if (!campaign) {
|
|
box.className = 'event-result';
|
|
box.innerHTML = `<span class="placeholder">${NO_CAMPAIGN_MESSAGE}</span>`;
|
|
return;
|
|
}
|
|
|
|
const chaos = campaign.chaos_factor;
|
|
const roll = Math.floor(Math.random() * 10) + 1;
|
|
|
|
if (roll <= chaos) {
|
|
box.className = 'event-result animate';
|
|
box.style.borderColor = 'var(--info)';
|
|
box.innerHTML = `
|
|
<div class="result-main blue-color">Random Event!</div>
|
|
<div class="result-sub">Roll: ${roll} ≤ CF: ${chaos}</div>
|
|
`;
|
|
} else {
|
|
box.className = 'event-result animate';
|
|
box.style.borderColor = 'var(--border)';
|
|
box.innerHTML = `
|
|
<div class="result-main" style="color:var(--text-secondary);">No Event</div>
|
|
<div class="result-sub">Roll: ${roll} > CF: ${chaos} — scene proceeds as expected.</div>
|
|
`;
|
|
}
|
|
}
|
|
|
|
function init() {
|
|
initProbGrid();
|
|
document.getElementById('fateRollBtn').addEventListener('click', rollFate);
|
|
document.getElementById('eventRollBtn').addEventListener('click', rollEventCheck);
|
|
}
|
|
|
|
document.addEventListener('DOMContentLoaded', init);
|