Files
2026-06-30 21:43:32 -04:00

1105 lines
39 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Mythic Oracle</title>
<link href="https://fonts.googleapis.com/css2?family=Cinzel:wght@400;600;700&family=Crimson+Pro:ital,wght@0,300;0,400;1,300;1,400&display=swap" rel="stylesheet">
<style>
:root {
--bg: #1b1f18;
--bg2: #232820;
--bg3: #2a2f26;
--border: #3a4035;
--text: #d3c9a8;
--text-dim: #7a7560;
--text-faint:#4a4840;
--gold: #c9a84c;
--gold-dim: #7a6530;
--green: #83a374;
--red: #c4705a;
--blue: #7a9fb5;
--accent: #9db88a;
--shadow: rgba(0,0,0,0.6);
}
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
background: var(--bg);
color: var(--text);
font-family: 'Crimson Pro', Georgia, serif;
font-size: 17px;
min-height: 100vh;
background-image:
radial-gradient(ellipse at 20% 0%, rgba(201,168,76,0.04) 0%, transparent 60%),
radial-gradient(ellipse at 80% 100%, rgba(131,163,116,0.03) 0%, transparent 60%);
}
/* ── Header ── */
header {
text-align: center;
padding: 2rem 1rem 0;
position: relative;
}
header::after {
content: '';
display: block;
margin: 1.2rem auto 0;
width: 60%;
height: 1px;
background: linear-gradient(90deg, transparent, var(--gold-dim), transparent);
}
header h1 {
font-family: 'Cinzel', serif;
font-size: 1.6rem;
font-weight: 400;
letter-spacing: 0.25em;
color: var(--gold);
text-transform: uppercase;
}
header p {
font-style: italic;
color: var(--text-dim);
font-size: 0.9rem;
margin-top: 0.3rem;
letter-spacing: 0.05em;
}
/* ── Tabs ── */
.tabs {
display: flex;
justify-content: center;
gap: 0;
margin: 1.5rem auto 0;
max-width: 700px;
border-bottom: 1px solid var(--border);
padding: 0 1rem;
}
.tab {
font-family: 'Cinzel', serif;
font-size: 0.75rem;
letter-spacing: 0.15em;
text-transform: uppercase;
color: var(--text-dim);
padding: 0.7rem 1.4rem;
cursor: pointer;
border-bottom: 2px solid transparent;
transition: color 0.2s, border-color 0.2s;
user-select: none;
}
.tab:hover { color: var(--text); }
.tab.active {
color: var(--gold);
border-bottom-color: var(--gold);
}
/* ── Panels ── */
.panel {
display: none;
max-width: 700px;
margin: 0 auto;
padding: 2rem 1.5rem 3rem;
}
.panel.active { display: block; }
/* ── Cards ── */
.card {
background: var(--bg2);
border: 1px solid var(--border);
border-radius: 4px;
padding: 1.5rem;
margin-bottom: 1.2rem;
position: relative;
}
.card-title {
font-family: 'Cinzel', serif;
font-size: 0.7rem;
letter-spacing: 0.2em;
text-transform: uppercase;
color: var(--text-dim);
margin-bottom: 1rem;
}
/* ── Chaos Tracker ── */
.chaos-display {
display: flex;
align-items: center;
justify-content: center;
gap: 1.5rem;
}
.chaos-btn {
width: 2.2rem;
height: 2.2rem;
border-radius: 50%;
border: 1px solid var(--border);
background: var(--bg3);
color: var(--text);
font-size: 1.2rem;
cursor: pointer;
display: flex; align-items: center; justify-content: center;
transition: border-color 0.15s, background 0.15s;
line-height: 1;
}
.chaos-btn:hover { border-color: var(--gold); background: var(--bg3); color: var(--gold); }
.chaos-number {
font-family: 'Cinzel', serif;
font-size: 3.5rem;
font-weight: 600;
color: var(--gold);
min-width: 3rem;
text-align: center;
line-height: 1;
}
.chaos-label {
font-style: italic;
color: var(--text-dim);
font-size: 0.85rem;
text-align: center;
margin-top: 0.5rem;
}
/* ── Fate Check ── */
.prob-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 0.5rem;
margin-bottom: 1rem;
}
.prob-btn {
padding: 0.5rem 0.3rem;
border: 1px solid var(--border);
background: var(--bg3);
color: var(--text-dim);
font-family: 'Crimson Pro', serif;
font-size: 0.85rem;
cursor: pointer;
border-radius: 3px;
transition: all 0.15s;
text-align: center;
}
.prob-btn:hover { border-color: var(--gold-dim); color: var(--text); }
.prob-btn.selected { border-color: var(--gold); color: var(--gold); background: rgba(201,168,76,0.08); }
.roll-btn {
width: 100%;
padding: 0.75rem;
background: var(--bg3);
border: 1px solid var(--gold-dim);
color: var(--gold);
font-family: 'Cinzel', serif;
font-size: 0.75rem;
letter-spacing: 0.2em;
text-transform: uppercase;
cursor: pointer;
border-radius: 3px;
transition: all 0.2s;
margin-top: 0.5rem;
}
.roll-btn:hover { background: rgba(201,168,76,0.1); border-color: var(--gold); }
.roll-btn:active { transform: scale(0.98); }
/* ── Result Display ── */
.result-box {
margin-top: 1rem;
padding: 1.2rem;
border-radius: 3px;
text-align: center;
border: 1px solid var(--border);
background: var(--bg3);
min-height: 4rem;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
transition: all 0.3s;
}
.result-box.yes { border-color: var(--green); }
.result-box.no { border-color: var(--red); }
.result-box.exceptional-yes { border-color: var(--gold); background: rgba(201,168,76,0.06); }
.result-box.exceptional-no { border-color: var(--red); background: rgba(196,112,90,0.06); }
.result-box.random-event { border-color: var(--blue); background: rgba(122,159,181,0.06); }
.result-main {
font-family: 'Cinzel', serif;
font-size: 1.4rem;
font-weight: 600;
letter-spacing: 0.1em;
}
.result-main.yes-color { color: var(--green); }
.result-main.no-color { color: var(--red); }
.result-main.gold-color { color: var(--gold); }
.result-main.blue-color { color: var(--blue); }
.result-sub {
font-style: italic;
color: var(--text-dim);
font-size: 0.85rem;
margin-top: 0.3rem;
}
.result-roll {
color: var(--text-faint);
font-size: 0.8rem;
margin-top: 0.4rem;
}
/* ── Random Event Check ── */
.event-result {
margin-top: 1rem;
padding: 1rem;
border-radius: 3px;
background: var(--bg3);
border: 1px solid var(--border);
text-align: center;
min-height: 3rem;
display: flex; align-items: center; justify-content: center;
flex-direction: column;
}
/* ── Meaning Tables ── */
.meaning-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 1rem;
}
.meaning-result {
padding: 1rem;
background: var(--bg3);
border: 1px solid var(--border);
border-radius: 3px;
text-align: center;
min-height: 3.5rem;
display: flex; align-items: center; justify-content: center;
flex-direction: column;
}
.meaning-label {
font-family: 'Cinzel', serif;
font-size: 0.6rem;
letter-spacing: 0.2em;
text-transform: uppercase;
color: var(--text-faint);
margin-bottom: 0.4rem;
}
.meaning-word {
font-size: 1.2rem;
color: var(--blue);
font-style: italic;
}
/* ── UNE ── */
.une-result {
padding: 1.2rem;
background: var(--bg3);
border: 1px solid var(--border);
border-radius: 3px;
margin-top: 1rem;
}
.une-row {
display: flex;
justify-content: space-between;
align-items: baseline;
padding: 0.4rem 0;
border-bottom: 1px solid var(--bg);
}
.une-row:last-child { border-bottom: none; }
.une-key {
font-family: 'Cinzel', serif;
font-size: 0.65rem;
letter-spacing: 0.15em;
text-transform: uppercase;
color: var(--text-dim);
min-width: 6rem;
}
.une-val {
font-style: italic;
color: var(--accent);
font-size: 1rem;
text-align: right;
}
/* ── Dice ── */
.dice-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 0.6rem;
margin-bottom: 1rem;
}
.die-btn {
padding: 0.8rem 0.4rem;
border: 1px solid var(--border);
background: var(--bg3);
color: var(--text-dim);
font-family: 'Cinzel', serif;
font-size: 0.8rem;
letter-spacing: 0.1em;
cursor: pointer;
border-radius: 3px;
transition: all 0.15s;
text-align: center;
}
.die-btn:hover { border-color: var(--gold-dim); color: var(--text); }
.die-btn.selected { border-color: var(--gold); color: var(--gold); background: rgba(201,168,76,0.08); }
.die-btn:disabled { opacity: 0.3; cursor: not-allowed; }
.pool-display {
background: var(--bg3);
border: 1px solid var(--border);
border-radius: 3px;
padding: 0.6rem 1rem;
text-align: center;
margin: 0.8rem 0 0.4rem;
font-family: 'Cinzel', serif;
font-size: 0.95rem;
color: var(--gold);
letter-spacing: 0.05em;
min-height: 2.4rem;
display: flex;
align-items: center;
justify-content: center;
}
.pool-cap-msg {
text-align: center;
color: var(--red);
font-size: 0.8rem;
font-style: italic;
margin-bottom: 0.3rem;
}
.clear-btn {
padding: 0.75rem 1rem;
background: var(--bg3);
border: 1px solid var(--border);
color: var(--text-dim);
font-family: 'Cinzel', serif;
font-size: 0.75rem;
letter-spacing: 0.15em;
text-transform: uppercase;
cursor: pointer;
border-radius: 3px;
transition: all 0.2s;
white-space: nowrap;
}
.clear-btn:hover { border-color: var(--red); color: var(--red); }
.clear-btn:active { transform: scale(0.98); }
.pool-result-groups {
text-align: center;
font-size: 0.85rem;
color: var(--text-dim);
font-style: italic;
width: 100%;
}
.pool-result-group { padding: 0.15rem 0; }
.pool-group-type {
font-family: 'Cinzel', serif;
font-size: 0.65rem;
letter-spacing: 0.12em;
text-transform: uppercase;
color: var(--text-dim);
}
.pool-result-individual {
font-size: 0.75rem;
color: var(--text-faint);
margin-top: 0.35rem;
}
.custom-roll-row {
display: flex;
align-items: stretch;
gap: 0.6rem;
margin-bottom: 0.8rem;
}
.custom-roll-input {
flex: 1;
width: 100%;
padding: 0.6rem 0.8rem;
background: var(--bg3);
border: 1px solid var(--border);
border-radius: 3px;
color: var(--text);
font-family: 'Crimson Pro', serif;
font-size: 0.95rem;
line-height: 1.2;
text-align: center;
box-sizing: border-box;
}
.custom-roll-input:focus { outline: none; border-color: var(--gold-dim); }
.custom-die-input {
flex: 1;
display: flex;
align-items: center;
gap: 0.4rem;
}
.custom-die-prefix {
color: var(--text-dim);
font-family: 'Cinzel', serif;
font-size: 0.9rem;
line-height: 1.2;
}
.roll-btn:disabled { opacity: 0.35; cursor: not-allowed; }
.roll-btn:disabled:hover { background: var(--bg3); border-color: var(--gold-dim); }
.dice-result-box {
background: var(--bg3);
border: 1px solid var(--border);
border-radius: 3px;
padding: 1.2rem;
text-align: center;
min-height: 4rem;
display: flex; flex-direction: column; align-items: center; justify-content: center;
}
.dice-total {
font-family: 'Cinzel', serif;
font-size: 2.5rem;
font-weight: 600;
color: var(--gold);
line-height: 1;
}
.dice-breakdown {
font-size: 0.8rem;
color: var(--text-dim);
margin-top: 0.4rem;
font-style: italic;
}
/* ── Divider ── */
.divider {
height: 1px;
background: linear-gradient(90deg, transparent, var(--border), transparent);
margin: 1.2rem 0;
}
/* ── Placeholder text ── */
.placeholder {
font-style: italic;
color: var(--text-faint);
font-size: 0.9rem;
}
/* ── Animations ── */
@keyframes fadeIn {
from { opacity: 0; transform: translateY(4px); }
to { opacity: 1; transform: translateY(0); }
}
.animate { animation: fadeIn 0.25s ease forwards; }
/* ── Scrollbar ── */
::-webkit-scrollbar { width: 6px; }
::-webkit-scrollbar-track { background: var(--bg); }
::-webkit-scrollbar-thumb { background: var(--border); border-radius: 3px; }
</style>
</head>
<body>
<header>
<h1>Mythic Oracle</h1>
<p>Mythic GME v2 &middot; Solo Play Companion</p>
</header>
<nav class="tabs">
<div class="tab active" onclick="showTab('oracle')">Oracle</div>
<div class="tab" onclick="showTab('meaning')">Meaning</div>
<div class="tab" onclick="showTab('une')">UNE</div>
<div class="tab" onclick="showTab('dice')">Dice</div>
</nav>
<!-- ═══════════════════════════════════════════════ ORACLE TAB -->
<div id="tab-oracle" class="panel active">
<!-- Chaos Tracker -->
<div class="card">
<div class="card-title">Chaos Factor</div>
<div class="chaos-display">
<button class="chaos-btn" onclick="changeChaos(-1)"></button>
<div>
<div class="chaos-number" id="chaos-num">5</div>
<div class="chaos-label" id="chaos-desc">Moderate tension</div>
</div>
<button class="chaos-btn" onclick="changeChaos(1)">+</button>
</div>
</div>
<!-- Fate Check -->
<div class="card">
<div class="card-title">Fate Check</div>
<div class="prob-grid">
<button class="prob-btn" onclick="selectProb(this, 'impossible', 'Impossible', 5)">Impossible</button>
<button class="prob-btn" onclick="selectProb(this, 'no-way', 'No Way', 15)">No Way</button>
<button class="prob-btn" onclick="selectProb(this, 'very-unlikely', 'Very Unlikely', 25)">Very Unlikely</button>
<button class="prob-btn" onclick="selectProb(this, 'unlikely', 'Unlikely', 35)">Unlikely</button>
<button class="prob-btn selected" onclick="selectProb(this, 'fifty-fifty', '50/50', 50)">50 / 50</button>
<button class="prob-btn" onclick="selectProb(this, 'somewhat-likely', 'Somewhat Likely', 65)">Somewhat Likely</button>
<button class="prob-btn" onclick="selectProb(this, 'likely', 'Likely', 75)">Likely</button>
<button class="prob-btn" onclick="selectProb(this, 'very-likely', 'Very Likely', 85)">Very Likely</button>
<button class="prob-btn" onclick="selectProb(this, 'near-certain', 'Near Certain', 95)">Near Certain</button>
</div>
<button class="roll-btn" onclick="rollFate()">Ask the Fates</button>
<div class="result-box" id="fate-result">
<span class="placeholder">Select a probability and ask.</span>
</div>
</div>
<!-- Random Event Check -->
<div class="card">
<div class="card-title">Random Event Check</div>
<p style="font-style:italic; color:var(--text-dim); font-size:0.9rem; margin-bottom:0.8rem;">
Roll at the start of each scene. A random event occurs if the result is equal to or under the chaos factor.
</p>
<button class="roll-btn" onclick="rollEventCheck()">Check for Random Event</button>
<div class="event-result" id="event-result">
<span class="placeholder">Roll to check.</span>
</div>
</div>
</div>
<!-- ═══════════════════════════════════════════════ MEANING TAB -->
<div id="tab-meaning" class="panel">
<div class="card">
<div class="card-title">Meaning Tables</div>
<p style="font-style:italic; color:var(--text-dim); font-size:0.9rem; margin-bottom:1rem;">
Use these to interpret random events, oracle results, or whenever you need narrative inspiration.
</p>
<button class="roll-btn" onclick="rollMeaning()">Roll Action + Subject</button>
<div class="meaning-grid" style="margin-top:1rem;">
<div class="meaning-result">
<div class="meaning-label">Action</div>
<div class="meaning-word" id="meaning-action"><span class="placeholder"></span></div>
</div>
<div class="meaning-result">
<div class="meaning-label">Subject</div>
<div class="meaning-word" id="meaning-subject"><span class="placeholder"></span></div>
</div>
</div>
</div>
<div class="card">
<div class="card-title">Individual Tables</div>
<div style="display:grid; grid-template-columns:1fr 1fr; gap:0.6rem;">
<button class="roll-btn" onclick="rollAction()">Roll Action</button>
<button class="roll-btn" onclick="rollSubject()">Roll Subject</button>
</div>
</div>
</div>
<!-- ═══════════════════════════════════════════════ UNE TAB -->
<div id="tab-une" class="panel">
<div class="card">
<div class="card-title">NPC Generator — UNE</div>
<p style="font-style:italic; color:var(--text-dim); font-size:0.9rem; margin-bottom:1rem;">
Generate an NPC's core traits. Drop the results into your NPC note.
</p>
<button class="roll-btn" onclick="rollUNE()">Generate NPC</button>
<div class="une-result" id="une-result" style="display:none;">
<div class="une-row">
<span class="une-key">Motivation</span>
<span class="une-val" id="une-motivation"></span>
</div>
<div class="une-row">
<span class="une-key">Demeanor</span>
<span class="une-val" id="une-demeanor"></span>
</div>
<div class="une-row">
<span class="une-key">Character</span>
<span class="une-val" id="une-character"></span>
</div>
</div>
</div>
<div class="card">
<div class="card-title">NPC Relationship</div>
<p style="font-style:italic; color:var(--text-dim); font-size:0.9rem; margin-bottom:0.8rem;">
How does this NPC feel about the PC?
</p>
<button class="roll-btn" onclick="rollRelationship()">Roll Relationship</button>
<div class="event-result" id="relationship-result">
<span class="placeholder">Roll to find out.</span>
</div>
</div>
</div>
<!-- ═══════════════════════════════════════════════ DICE TAB -->
<div id="tab-dice" class="panel">
<div class="card">
<div class="card-title">Dice Pool Builder</div>
<div class="dice-grid">
<button class="die-btn" onclick="addDie(4)">d4</button>
<button class="die-btn" onclick="addDie(6)">d6</button>
<button class="die-btn" onclick="addDie(8)">d8</button>
<button class="die-btn" onclick="addDie(10)">d10</button>
<button class="die-btn" onclick="addDie(12)">d12</button>
<button class="die-btn" onclick="addDie(20)">d20</button>
<button class="die-btn" onclick="addDie(100)">d100</button>
<button class="die-btn" onclick="addDie(2)">d2</button>
</div>
<div class="pool-display" id="pool-display">
<span class="placeholder">No dice in pool — click above to add.</span>
</div>
<div class="pool-cap-msg" id="pool-cap-msg" style="display:none;">Pool is full — 25 dice maximum.</div>
<div style="display:flex; gap:0.6rem; margin-top:0.5rem;">
<button class="roll-btn" onclick="rollPool()" style="margin-top:0; flex:1;">Roll Pool</button>
<button class="clear-btn" onclick="clearPool()">Clear</button>
</div>
<div class="dice-result-box" style="margin-top:1rem;" id="dice-result">
<span class="placeholder">Build a pool and roll.</span>
</div>
</div>
<div class="card">
<div class="card-title">Custom Roll</div>
<div class="custom-roll-row">
<input type="number" id="custom-qty" class="custom-roll-input" placeholder="Qty" min="1" step="1" oninput="validateCustomRoll()">
<div class="custom-die-input">
<span class="custom-die-prefix">d</span>
<input type="number" id="custom-sides" class="custom-roll-input" placeholder="sides" min="1" step="1" oninput="validateCustomRoll()">
</div>
</div>
<div style="display:flex; gap:0.6rem;">
<button class="roll-btn" id="custom-roll-btn" onclick="rollCustom()" style="margin-top:0; flex:1;" disabled>Roll</button>
<button class="clear-btn" onclick="clearCustomRoll()">Clear</button>
</div>
<div class="dice-result-box" style="margin-top:1rem;" id="custom-dice-result">
<span class="placeholder">Enter a quantity and die size.</span>
</div>
</div>
<div class="card">
<div class="card-title">Percentile</div>
<div style="display:flex; gap:0.6rem;">
<button class="roll-btn" style="margin-top:0; flex:1;" onclick="quickRoll(10,1,0,'Percentile (tens)','percentile-dice-result')">Tens</button>
<button class="roll-btn" style="margin-top:0; flex:1;" onclick="quickRoll(10,1,0,'Percentile (ones)','percentile-dice-result')">Ones</button>
</div>
<div class="dice-result-box" style="margin-top:1rem;" id="percentile-dice-result">
<span class="placeholder">Roll tens or ones.</span>
</div>
</div>
<div class="card">
<div class="card-title">Ability Score</div>
<button class="roll-btn" style="margin-top:0;" onclick="quickRoll(6,4,-4,'4d6 drop lowest (approx)','ability-dice-result')">4d6 dl</button>
<button class="roll-btn" onclick="rollAbilityArray()">Full Array</button>
<div class="dice-result-box" style="margin-top:1rem;" id="ability-dice-result">
<span class="placeholder">Roll for an ability score.</span>
</div>
</div>
</div>
<script>
// ══════════════════════════════════════════════════════
// STATE
// ══════════════════════════════════════════════════════
let chaos = 5;
let selectedProb = { label: '50/50', base: 50 };
// ══════════════════════════════════════════════════════
// TABS
// ══════════════════════════════════════════════════════
function showTab(name) {
document.querySelectorAll('.panel').forEach(p => p.classList.remove('active'));
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
document.getElementById('tab-' + name).classList.add('active');
event.target.classList.add('active');
}
// ══════════════════════════════════════════════════════
// CHAOS
// ══════════════════════════════════════════════════════
const chaosLabels = {
1: 'Almost none', 2: 'Very low', 3: 'Low', 4: 'Below average',
5: 'Moderate tension', 6: 'Above average', 7: 'High', 8: 'Very high', 9: 'Frantic'
};
function changeChaos(delta) {
chaos = Math.max(1, Math.min(9, chaos + delta));
document.getElementById('chaos-num').textContent = chaos;
document.getElementById('chaos-desc').textContent = chaosLabels[chaos];
}
// ══════════════════════════════════════════════════════
// FATE CHECK
// Mythic GME v2 probability matrix adjusted by chaos factor
// ══════════════════════════════════════════════════════
const probTable = {
// base% -> [cf1..cf9] thresholds
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],
};
function selectProb(el, id, label, base) {
document.querySelectorAll('.prob-btn').forEach(b => b.classList.remove('selected'));
el.classList.add('selected');
selectedProb = { label, base };
}
function rollFate() {
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)
let eventNote = '';
if (roll <= chaos) {
eventNote = ' · Random event triggered.';
subText += eventNote;
resultClass = yesResult ? resultClass : 'random-event';
}
const box = document.getElementById('fate-result');
box.className = 'result-box animate ' + resultClass;
box.innerHTML = `
<div class="result-main ${yesResult ? 'yes-color' : (resultClass === 'random-event' ? 'blue-color' : 'no-color')}">${mainText}</div>
<div class="result-sub">${subText}</div>
<div class="result-roll">Roll: ${roll} &nbsp;|&nbsp; Threshold: ${threshold} &nbsp;|&nbsp; Prob: ${selectedProb.label} &nbsp;|&nbsp; CF: ${chaos}</div>
`;
void box.offsetWidth;
}
// ══════════════════════════════════════════════════════
// RANDOM EVENT CHECK
// ══════════════════════════════════════════════════════
function rollEventCheck() {
const roll = Math.floor(Math.random() * 10) + 1;
const box = document.getElementById('event-result');
box.innerHTML = '';
if (roll <= chaos) {
box.className = 'event-result animate';
box.style.borderColor = 'var(--blue)';
box.innerHTML = `
<div class="result-main blue-color">Random Event!</div>
<div class="result-sub">Roll: ${roll} &nbsp;≤&nbsp; CF: ${chaos}</div>
`;
} else {
box.className = 'event-result animate';
box.style.borderColor = 'var(--border)';
box.innerHTML = `
<div class="result-main" style="color:var(--text-dim);">No Event</div>
<div class="result-sub">Roll: ${roll} &nbsp;&gt;&nbsp; CF: ${chaos} — scene proceeds as expected.</div>
`;
}
void box.offsetWidth;
}
// ══════════════════════════════════════════════════════
// MEANING TABLES (Mythic GME v2)
// ══════════════════════════════════════════════════════
const actions = [
"Abandon","Accomplish","Acquire","Advance","Affect","Antagonize","Approach","Arrive",
"Attain","Attract","Balance","Befriend","Betray","Block","Break","Build","Capture",
"Change","Communicate","Complete","Conceal","Conflict","Confront","Create","Damage",
"Deceive","Decrease","Delay","Deny","Destroy","Discover","Dispute","Dominate","Encourage",
"Endure","Escape","Examine","Expose","Fight","Follow","Fulfill","Harm","Help","Hide",
"Hinder","Ignore","Imprison","Improve","Inform","Investigate","Journey","Kill","Lead",
"Leave","Lose","Manipulate","Move","Oppose","Overcome","Persevere","Possess","Prevent",
"Proceed","Protect","Pursue","Realize","Release","Remove","Resist","Restore","Reveal",
"Ruin","Search","Seek","Separate","Stop","Struggle","Support","Take","Transform","Travel",
"Trick","Understand","Unite","Weaken","Work","Wound"
];
const subjects = [
"A burden","A conflict","A dream","A enemy","A fear","A friend","A goal","A group",
"A hidden truth","A journey","A leader","A lie","A loss","A message","A mission",
"A mistake","A mystery","A need","A opportunity","A plan","A power","A problem",
"A relationship","A resource","A rumor","A secret","A skill","A stranger","A threat",
"A truth","A weapon","An agreement","An ally","An ambition","An authority","An emotion",
"An enemy","An event","An idea","An obstacle","Beauty","Chaos","Comfort","Community",
"Competition","Corruption","Courage","Creation","Danger","Death","Destiny","Discord",
"Dominance","Duty","Emotion","Energy","Evil","Failure","Faith","Fame","Fate","Fear",
"Freedom","Good","Greed","Hatred","Health","History","Honor","Hope","Identity","Knowledge",
"Law","Liberty","Life","Love","Loyalty","Magic","Nature","Order","Pain","Peace","Power",
"Pride","Protection","Purpose","Reason","Revenge","Safety","Strength","Success","Survival",
"Time","Victory","Violence","Wealth","Wisdom"
];
function rollAction() {
const word = actions[Math.floor(Math.random() * actions.length)];
const el = document.getElementById('meaning-action');
el.classList.add('animate');
el.innerHTML = `<span style="font-style:italic;color:var(--blue)">${word}</span>`;
void el.offsetWidth;
}
function rollSubject() {
const word = subjects[Math.floor(Math.random() * subjects.length)];
const el = document.getElementById('meaning-subject');
el.classList.add('animate');
el.innerHTML = `<span style="font-style:italic;color:var(--blue)">${word}</span>`;
void el.offsetWidth;
}
function rollMeaning() {
rollAction();
rollSubject();
}
// ══════════════════════════════════════════════════════
// UNE — Universal NPC Emulator
// ══════════════════════════════════════════════════════
const uneMotivations = [
"Acquire knowledge","Advance ambitions","Attain glory","Avenge a wrong","Build a legacy",
"Complete a quest","Conceal a secret","Corrupt an institution","Create something new",
"Defend a belief","Destroy an enemy","Discover the truth","Dominate others",
"Escape a fate","Establish order","Find belonging","Fulfill an obligation",
"Gain power","Gain wealth","Harm an enemy","Help the downtrodden","Hide from the past",
"Honor a vow","Influence events","Maintain balance","Obtain an item","Overthrow authority",
"Possess something","Preserve tradition","Prevent a disaster","Protect a person",
"Pursue justice","Pursue pleasure","Redeem themselves","Restore something lost",
"Reveal a secret","Seek absolution","Seek revenge","Serve a master","Spread a belief",
"Survive at any cost","Teach others","Undermine authority","Unite a group","Win approval"
];
const uneDemeanors = [
"Aggressive","Aloof","Ambitious","Amiable","Apathetic","Arrogant","Bitter","Calm",
"Cautious","Charming","Cold","Competitive","Confident","Conflicted","Curious","Cynical",
"Deceptive","Defensive","Desperate","Devoted","Dignified","Earnest","Eccentric",
"Elusive","Erratic","Fearful","Fierce","Formal","Friendly","Guarded","Humble",
"Imperious","Impulsive","Insecure","Intense","Jovial","Manipulative","Melancholic",
"Mercurial","Methodical","Nervous","Neutral","Obsequious","Paranoid","Passionate",
"Patient","Pragmatic","Righteous","Secretive","Serious","Suspicious","Threatening",
"Timid","Unpredictable","Wary","Weary","Zealous"
];
const uneCharacters = [
"A betrayed idealist","A burned-out soldier","A charlatan with a conscience",
"A desperate parent","A disgraced noble","A disillusioned priest","A failed hero",
"A fallen guardian","A fanatic true believer","A hardened survivor","A haunted veteran",
"A hidden manipulator","A jaded mercenary","A loyal servant","A opportunistic thief",
"A principled outlaw","A reluctant leader","A ruthless pragmatist","A scarred wanderer",
"A scholar obsessed","A secret keeper","A self-made merchant","A shattered idealist",
"A struggling reformer","A tortured artist","A true believer","A vengeful survivor",
"A weary traveler","A willful contrarian","An ambitious upstart","An ancient relic",
"An unlikely prophet","Someone hiding grief","Someone hiding guilt","Someone seeking peace"
];
const uneRelationships = [
"Hostile — actively works against the PC",
"Unfriendly — suspicious and unhelpful",
"Neutral — no strong feelings either way",
"Neutral — cautious but open",
"Friendly — willing to help if it costs little",
"Friendly — genuinely likes the PC",
"Helpful — goes out of their way for the PC",
"Devoted — would take serious risks for the PC"
];
function rollUNE() {
const motivation = uneMotivations[Math.floor(Math.random() * uneMotivations.length)];
const demeanor = uneDemeanors[Math.floor(Math.random() * uneDemeanors.length)];
const character = uneCharacters[Math.floor(Math.random() * uneCharacters.length)];
document.getElementById('une-motivation').textContent = motivation;
document.getElementById('une-demeanor').textContent = demeanor;
document.getElementById('une-character').textContent = character;
const result = document.getElementById('une-result');
result.style.display = 'block';
result.classList.add('animate');
void result.offsetWidth;
}
function rollRelationship() {
const rel = uneRelationships[Math.floor(Math.random() * uneRelationships.length)];
const box = document.getElementById('relationship-result');
box.classList.add('animate');
box.innerHTML = `<div style="font-style:italic;color:var(--accent);font-size:1rem;">${rel}</div>`;
void box.offsetWidth;
}
// ══════════════════════════════════════════════════════
// DICE POOL BUILDER
// ══════════════════════════════════════════════════════
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('dice-result');
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('pool-display');
const capMsg = document.getElementById('pool-cap-msg');
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('.die-btn').forEach(btn => { btn.disabled = capped; });
}
function rollPool() {
const total = DIE_ORDER.reduce((s, d) => s + dicePool[d], 0);
const box = document.getElementById('dice-result');
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 style="height:1px;background:var(--border);width:70%;margin:0.55rem auto;"></div>
<div class="pool-result-groups">${groupedHTML}</div>
`;
void box.offsetWidth;
}
function validateCustomRoll() {
const qty = document.getElementById('custom-qty').value;
const sides = document.getElementById('custom-sides').value;
const btn = document.getElementById('custom-roll-btn');
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('custom-qty').value);
const sides = Number(document.getElementById('custom-sides').value);
const box = document.getElementById('custom-dice-result');
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('custom-dice-result');
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} &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('ability-dice-result');
box.className = 'dice-result-box animate';
box.innerHTML = `<div class="dice-breakdown">${lines.join('<br>')}</div>`;
void box.offsetWidth;
}
</script>
</body>
</html>