1105 lines
39 KiB
HTML
1105 lines
39 KiB
HTML
<!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 · 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} | Threshold: ${threshold} | Prob: ${selectedProb.label} | 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} ≤ 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} > 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} [${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} [${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('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>
|