|
|
<!DOCTYPE html>
|
|
|
<html lang="en">
|
|
|
<head>
|
|
|
<meta charset="UTF-8">
|
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
|
<title>PF2e Hit Points, Healing & Dying</title>
|
|
|
<link rel="stylesheet" href="global.css">
|
|
|
<script src="https://cdn.jsdelivr.net/npm/mermaid@10/dist/mermaid.min.js"></script>
|
|
|
<style>
|
|
|
:root {
|
|
|
--c-alive: #193a14;
|
|
|
--c-alive-hi: #5ec454;
|
|
|
--c-alive-border: #3a7c32;
|
|
|
|
|
|
--c-dying-1: #3e2e06;
|
|
|
--c-dying-1-hi: #f0c040;
|
|
|
--c-dying-1-border: #a07a10;
|
|
|
|
|
|
--c-dying-2: #3e1a06;
|
|
|
--c-dying-2-hi: #f07830;
|
|
|
--c-dying-2-border: #a04010;
|
|
|
|
|
|
--c-dying-3: #300c0c;
|
|
|
--c-dying-3-hi: #f04040;
|
|
|
--c-dying-3-border: #8a1010;
|
|
|
|
|
|
--c-dead: #1a0404;
|
|
|
--c-dead-hi: #c03030;
|
|
|
--c-dead-border: #600808;
|
|
|
|
|
|
--c-unconscious: #0e2234;
|
|
|
--c-unconscious-hi: #50a8e8;
|
|
|
--c-unconscious-border: #2060a0;
|
|
|
}
|
|
|
|
|
|
aside.open { width: 280px; min-width: 280px; border-right-width: 1px; }
|
|
|
.aside-inner { width: 280px; padding: 16px 14px; }
|
|
|
|
|
|
.diagram-wrap .mermaid { min-width: 700px; }
|
|
|
|
|
|
.ccard.alive { background: var(--c-alive); border-color: var(--c-alive-border); }
|
|
|
.ccard.alive h3 { color: var(--c-alive-hi); }
|
|
|
.ccard.alive .tag { background: #0d2a0a; color: var(--c-alive-hi); }
|
|
|
|
|
|
.ccard.dying { background: var(--c-dying-2); border-color: var(--c-dying-2-border); }
|
|
|
.ccard.dying h3 { color: var(--c-dying-2-hi); }
|
|
|
.ccard.dying .tag { background: #2a1004; color: var(--c-dying-2-hi); }
|
|
|
|
|
|
.ccard.unconscious { background: var(--c-unconscious); border-color: var(--c-unconscious-border); }
|
|
|
.ccard.unconscious h3 { color: var(--c-unconscious-hi); }
|
|
|
.ccard.unconscious .tag{ background: #081422; color: var(--c-unconscious-hi); }
|
|
|
|
|
|
.ccard.wounded { background: #201808; border-color: #806010; }
|
|
|
.ccard.wounded h3 { color: #d0a030; }
|
|
|
.ccard.wounded .tag{ background: #140e04; color: #d0a030; }
|
|
|
|
|
|
.ccard.doomed { background: #200808; border-color: #801010; }
|
|
|
.ccard.doomed h3 { color: #d04040; }
|
|
|
.ccard.doomed .tag { background: #140404; color: #d04040; }
|
|
|
|
|
|
.ccard.dead { background: var(--c-dead); border-color: var(--c-dead-border); }
|
|
|
.ccard.dead h3 { color: var(--c-dead-hi); }
|
|
|
.ccard.dead .tag { background: #100404; color: var(--c-dead-hi); }
|
|
|
|
|
|
.sim-start-row { display: flex; align-items: center; gap: 8px; margin-bottom: 14px; flex-wrap: wrap; }
|
|
|
|
|
|
.sim-chip.alive { color: var(--c-alive-hi); border-color: var(--c-alive-border); }
|
|
|
.sim-chip.dying-1 { color: var(--c-dying-1-hi); border-color: var(--c-dying-1-border); }
|
|
|
.sim-chip.dying-2 { color: var(--c-dying-2-hi); border-color: var(--c-dying-2-border); }
|
|
|
.sim-chip.dying-3 { color: var(--c-dying-3-hi); border-color: var(--c-dying-3-border); }
|
|
|
.sim-chip.unconscious { color: var(--c-unconscious-hi); border-color: var(--c-unconscious-border); }
|
|
|
|
|
|
.sim-cond-row { display: flex; align-items: center; gap: 16px; margin-bottom: 20px; flex-wrap: wrap; }
|
|
|
.sim-cond-item { display: flex; align-items: center; gap: 6px; }
|
|
|
.sim-cond-label { font-size: 0.78rem; color: var(--text-muted); text-transform: uppercase; letter-spacing: 0.06em; }
|
|
|
.sim-cond-val { font-size: 1rem; font-weight: bold; min-width: 24px; text-align: center; color: var(--text); }
|
|
|
.sim-cond-val.wounded-hi { color: #d0a030; }
|
|
|
.sim-cond-val.doomed-hi { color: #d04040; }
|
|
|
.sim-cond-val.dying-hi-1 { color: var(--c-dying-1-hi); }
|
|
|
.sim-cond-val.dying-hi-2 { color: var(--c-dying-2-hi); }
|
|
|
.sim-cond-val.dying-hi-3 { color: var(--c-dying-3-hi); }
|
|
|
.sim-cond-btn {
|
|
|
background: var(--surface2); border: 1px solid var(--border);
|
|
|
color: var(--text-muted); cursor: pointer; font-family: inherit;
|
|
|
font-size: 0.85rem; padding: 2px 8px; border-radius: 3px;
|
|
|
transition: color 0.15s, border-color 0.15s;
|
|
|
}
|
|
|
.sim-cond-btn:hover { color: var(--text); border-color: var(--text-muted); }
|
|
|
|
|
|
.sim-box.alive { background: var(--c-alive); border-color: var(--c-alive-border); }
|
|
|
.sim-box.dying-1 { background: var(--c-dying-1); border-color: var(--c-dying-1-border); }
|
|
|
.sim-box.dying-2 { background: var(--c-dying-2); border-color: var(--c-dying-2-border); }
|
|
|
.sim-box.dying-3 { background: var(--c-dying-3); border-color: var(--c-dying-3-border); }
|
|
|
.sim-box.dead { background: var(--c-dead); border-color: var(--c-dead-border); }
|
|
|
.sim-box.unconscious { background: var(--c-unconscious); border-color: var(--c-unconscious-border); }
|
|
|
|
|
|
.sim-box.alive .sim-state-name { color: var(--c-alive-hi); }
|
|
|
.sim-box.dying-1 .sim-state-name { color: var(--c-dying-1-hi); }
|
|
|
.sim-box.dying-2 .sim-state-name { color: var(--c-dying-2-hi); }
|
|
|
.sim-box.dying-3 .sim-state-name { color: var(--c-dying-3-hi); }
|
|
|
.sim-box.dead .sim-state-name { color: var(--c-dead-hi); }
|
|
|
.sim-box.unconscious .sim-state-name { color: var(--c-unconscious-hi); }
|
|
|
|
|
|
.sim-state-badges { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 8px; }
|
|
|
.sim-badge {
|
|
|
display: inline-block; padding: 3px 10px; border-radius: 4px;
|
|
|
font-size: 0.76rem; font-weight: bold; border: 1px solid; background: rgba(0,0,0,0.3);
|
|
|
}
|
|
|
.sim-box.dying-1 .sim-badge { color: var(--c-dying-1-hi); border-color: var(--c-dying-1-border); }
|
|
|
.sim-box.dying-2 .sim-badge { color: var(--c-dying-2-hi); border-color: var(--c-dying-2-border); }
|
|
|
.sim-box.dying-3 .sim-badge { color: var(--c-dying-3-hi); border-color: var(--c-dying-3-border); }
|
|
|
.sim-box.unconscious .sim-badge { color: var(--c-unconscious-hi); border-color: var(--c-unconscious-border); }
|
|
|
|
|
|
.sim-action {
|
|
|
padding: 9px 16px; background: var(--surface2); border: 1px solid var(--border);
|
|
|
color: var(--text); border-radius: 5px; cursor: pointer; font-family: inherit;
|
|
|
font-size: 0.83rem; text-align: left; max-width: 340px;
|
|
|
transition: border-color 0.15s, color 0.15s, background 0.15s;
|
|
|
}
|
|
|
.sim-outcomes-list { display: flex; flex-direction: column; gap: 6px; }
|
|
|
.sim-outcome {
|
|
|
padding: 8px 16px; background: var(--surface); border: 1px solid var(--border);
|
|
|
color: var(--text); border-radius: 4px; cursor: pointer; font-family: inherit;
|
|
|
font-size: 0.82rem; text-align: left; transition: border-color 0.15s, color 0.15s;
|
|
|
}
|
|
|
|
|
|
.sim-result-box.alive { background: var(--c-alive); border-color: var(--c-alive-border); }
|
|
|
.sim-result-box.dying-1 { background: var(--c-dying-1); border-color: var(--c-dying-1-border); }
|
|
|
.sim-result-box.dying-2 { background: var(--c-dying-2); border-color: var(--c-dying-2-border); }
|
|
|
.sim-result-box.dying-3 { background: var(--c-dying-3); border-color: var(--c-dying-3-border); }
|
|
|
.sim-result-box.dead { background: var(--c-dead); border-color: var(--c-dead-border); }
|
|
|
.sim-result-box.unconscious { background: var(--c-unconscious); border-color: var(--c-unconscious-border); }
|
|
|
|
|
|
.sim-result-box.alive .sim-result-new-state { color: var(--c-alive-hi); }
|
|
|
.sim-result-box.dying-1 .sim-result-new-state { color: var(--c-dying-1-hi); }
|
|
|
.sim-result-box.dying-2 .sim-result-new-state { color: var(--c-dying-2-hi); }
|
|
|
.sim-result-box.dying-3 .sim-result-new-state { color: var(--c-dying-3-hi); }
|
|
|
.sim-result-box.dead .sim-result-new-state { color: var(--c-dead-hi); }
|
|
|
.sim-result-box.unconscious .sim-result-new-state { color: var(--c-unconscious-hi); }
|
|
|
|
|
|
.sim-hist-state.alive { color: var(--c-alive-hi); }
|
|
|
.sim-hist-state.dying-1 { color: var(--c-dying-1-hi); }
|
|
|
.sim-hist-state.dying-2 { color: var(--c-dying-2-hi); }
|
|
|
.sim-hist-state.dying-3 { color: var(--c-dying-3-hi); }
|
|
|
.sim-hist-state.dead { color: var(--c-dead-hi); }
|
|
|
.sim-hist-state.unconscious { color: var(--c-unconscious-hi); }
|
|
|
</style>
|
|
|
</head>
|
|
|
<body>
|
|
|
|
|
|
<header>
|
|
|
<h1><a href="index.html">PF2e Hit Points, Healing & Dying</a></h1>
|
|
|
<p>Death & Recovery Reference — Player Core</p>
|
|
|
<button class="sidebar-toggle-btn" onclick="toggleSidebar()" title="Toggle Conditions Reference">
|
|
|
☰ Conditions
|
|
|
</button>
|
|
|
</header>
|
|
|
|
|
|
<div class="layout">
|
|
|
|
|
|
<!-- ── Sidebar ── -->
|
|
|
<aside id="sidebar">
|
|
|
<div class="aside-inner">
|
|
|
<div class="aside-header">
|
|
|
<h2>Conditions</h2>
|
|
|
<button class="aside-close" onclick="closeSidebar()" title="Close">×</button>
|
|
|
</div>
|
|
|
|
|
|
<div class="ccard alive">
|
|
|
<h3>Alive</h3>
|
|
|
<p>You have at least 1 Hit Point and are conscious. You act normally.</p>
|
|
|
<span class="tag">Normal play</span>
|
|
|
</div>
|
|
|
|
|
|
<div class="ccard dying">
|
|
|
<h3>Dying X</h3>
|
|
|
<p>You are bleeding out and unconscious. At the start of each turn attempt a flat check (DC 10 + dying value). If dying reaches 4—or less with Doomed—you die.</p>
|
|
|
<span class="tag">Recovery DC = 10 + dying value</span>
|
|
|
</div>
|
|
|
|
|
|
<div class="ccard unconscious">
|
|
|
<h3>Unconscious</h3>
|
|
|
<p>You can’t act. −4 status penalty to AC, Perception, and Reflex saves. You have the Blinded and Off-Guard conditions. You fall prone and drop held items.</p>
|
|
|
<span class="tag">−4 AC · −4 Perception · −4 Reflex</span>
|
|
|
</div>
|
|
|
|
|
|
<div class="ccard wounded">
|
|
|
<h3>Wounded X</h3>
|
|
|
<p>Gained whenever you lose the dying condition. Its value is added to every dying value increase. Cleared by a successful Treat Wounds or full HP restored with 10 min rest.</p>
|
|
|
<span class="tag">+X to all dying value increases</span>
|
|
|
</div>
|
|
|
|
|
|
<div class="ccard doomed">
|
|
|
<h3>Doomed X</h3>
|
|
|
<p>You die at dying (4−X) instead of dying 4. Doomed 1 means dying 3 kills you. If max dying is ever reduced to 0 you die instantly. Decreases by 1 per full night’s rest.</p>
|
|
|
<span class="tag">Die at dying (4 − X)</span>
|
|
|
</div>
|
|
|
|
|
|
<div class="ccard dead">
|
|
|
<h3>Dead</h3>
|
|
|
<p>You cannot act or be targeted by most spells. For all other purposes you are an object. Resurrection magic (<em>raise dead</em> spell or <em>resurrect</em> ritual) may restore life.</p>
|
|
|
<span class="tag">Terminal — requires special magic</span>
|
|
|
</div>
|
|
|
</div>
|
|
|
</aside>
|
|
|
<div class="sidebar-overlay" id="sidebar-overlay" onclick="closeSidebar()"></div>
|
|
|
|
|
|
<!-- ── Main ── -->
|
|
|
<main>
|
|
|
<div class="tab-bar">
|
|
|
<button class="tab-btn active" onclick="showTab('simulator', this)">Situation Simulator</button>
|
|
|
<button class="tab-btn" onclick="showTab('flowchart', this)">Flow Chart</button>
|
|
|
</div>
|
|
|
|
|
|
<!-- Flow Chart Tab -->
|
|
|
<div id="flowchart" class="tab-panel">
|
|
|
<h2>Dying Flow Chart</h2>
|
|
|
<p class="subtitle">State transitions for player characters — <strong>W</strong> = Wounded value added to every dying increase</p>
|
|
|
|
|
|
<div class="legend">
|
|
|
<div class="legend-item"><div class="legend-dot" style="background:var(--c-alive);border-color:var(--c-alive-hi)"></div> Alive</div>
|
|
|
<div class="legend-item"><div class="legend-dot" style="background:var(--c-dying-2);border-color:var(--c-dying-2-hi)"></div> Dying X</div>
|
|
|
<div class="legend-item"><div class="legend-dot" style="background:var(--c-unconscious);border-color:var(--c-unconscious-hi)"></div> Unconscious</div>
|
|
|
<div class="legend-item"><div class="legend-dot" style="background:var(--c-dead);border-color:var(--c-dead-hi)"></div> Dead</div>
|
|
|
</div>
|
|
|
|
|
|
<div class="diagram-wrap">
|
|
|
<div class="mermaid" id="dying-diagram">
|
|
|
%%{init: {'theme':'dark','themeVariables':{'background':'#161210','primaryColor':'#193a14','primaryTextColor':'#e8d5b0','primaryBorderColor':'#3a7c32','lineColor':'#9a7e56','secondaryColor':'#3e2e06','tertiaryColor':'#1a2840','edgeLabelBackground':'#211a13','clusterBkg':'#211a13'}}}%%
|
|
|
flowchart TD
|
|
|
|
|
|
classDef alive fill:#193a14,stroke:#3a7c32,color:#b8e8b0
|
|
|
classDef dying fill:#3e1a06,stroke:#a04010,color:#f0a868
|
|
|
classDef dead fill:#1a0404,stroke:#600808,color:#c06060
|
|
|
classDef unc fill:#0e2234,stroke:#2060a0,color:#90c8e8
|
|
|
|
|
|
ALIVE(["ALIVE · ≥1 HP"]):::alive
|
|
|
DX(["DYING X · Recovery DC 10+X"]):::dying
|
|
|
UNC(["UNCONSCIOUS · 0 HP"]):::unc
|
|
|
DEAD(["DEAD · dying ≥ threshold"]):::dead
|
|
|
|
|
|
ALIVE -->|"0 HP lethal · normal hit (+W)"| DX
|
|
|
ALIVE -->|"0 HP lethal · crit hit/fail (+W)"| DX
|
|
|
ALIVE -->|"0 HP nonlethal"| UNC
|
|
|
ALIVE -->|"Massive dmg or Death Effect"| DEAD
|
|
|
|
|
|
DX -->|"Crit Success · dying−2"| UNC
|
|
|
DX -->|"Success · dying−1"| UNC
|
|
|
DX -->|"Failure · dying+1+W"| DX
|
|
|
DX -->|"Crit Fail · dying+2+W"| DX
|
|
|
DX -->|"Take dmg · dying+1+W or +2+W"| DX
|
|
|
DX -->|"dying ≥ threshold"| DEAD
|
|
|
DX -->|"Healing 1+ HP"| ALIVE
|
|
|
DX -->|"Hero Points"| UNC
|
|
|
|
|
|
UNC -->|"Healing or natural recovery"| ALIVE
|
|
|
UNC -->|"Take lethal dmg to 0 HP"| DX
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
<div class="note-box">
|
|
|
<h3>Key Rules</h3>
|
|
|
<ul>
|
|
|
<li><strong>Recovery Check</strong> is a flat check (no modifiers) with DC = 10 + current dying value, made at the start of each turn while dying.</li>
|
|
|
<li><strong>Wounded</strong> is added to <em>every</em> dying value increase — being Wounded 2 and failing a recovery check means dying increases by 3 (1+2).</li>
|
|
|
<li><strong>Doomed X</strong> reduces your death threshold from 4 to (4−X). Doomed 1 means dying 3 kills you. Decreases by 1 per full night’s rest.</li>
|
|
|
<li><strong>Heroic Recovery:</strong> Spend all Hero Points at the start of your turn or when dying would increase. You lose the dying condition and stabilize at 0 HP. Wounded does <em>not</em> increase from this.</li>
|
|
|
<li><strong>Nonlethal KO:</strong> Reduced to 0 HP by a nonlethal attack causes Unconscious with no dying condition. You wake naturally after 10+ minutes.</li>
|
|
|
<li><strong>Massive Damage:</strong> Instant death if you take damage equal to or greater than <em>double your maximum HP</em> in one blow.</li>
|
|
|
<li><strong>Losing dying:</strong> Each time you lose the dying condition (except via Heroic Recovery) you gain Wounded 1 or increase Wounded by 1.</li>
|
|
|
<li><strong>Clearing Wounded:</strong> A successful Treat Wounds action, or restored to full HP and resting 10 minutes.</li>
|
|
|
</ul>
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
<!-- Simulator Tab -->
|
|
|
<div id="simulator" class="tab-panel active">
|
|
|
<h2>Situation Simulator</h2>
|
|
|
<p class="subtitle">Step through dying state transitions interactively — set conditions first, then choose what happens</p>
|
|
|
|
|
|
<div class="sim-start-row">
|
|
|
<span class="sim-start-label">Start as:</span>
|
|
|
<button class="sim-chip alive" onclick="simSetState('ALIVE', 0)">Alive</button>
|
|
|
<button class="sim-chip dying-1" onclick="simSetState('DYING', 1)">Dying 1</button>
|
|
|
<button class="sim-chip dying-2" onclick="simSetState('DYING', 2)">Dying 2</button>
|
|
|
<button class="sim-chip dying-3" onclick="simSetState('DYING', 3)">Dying 3</button>
|
|
|
<button class="sim-chip unconscious" onclick="simSetState('UNCONSCIOUS', 0)">Unconscious</button>
|
|
|
</div>
|
|
|
|
|
|
<div class="sim-cond-row">
|
|
|
<span class="sim-start-label">Conditions:</span>
|
|
|
<div class="sim-cond-item">
|
|
|
<span class="sim-cond-label">Dying</span>
|
|
|
<button class="sim-cond-btn" onclick="adjustDying(-1)">−</button>
|
|
|
<span class="sim-cond-val" id="sim-dying-val">0</span>
|
|
|
<button class="sim-cond-btn" onclick="adjustDying(1)">+</button>
|
|
|
</div>
|
|
|
<div class="sim-cond-item">
|
|
|
<span class="sim-cond-label">Wounded</span>
|
|
|
<button class="sim-cond-btn" onclick="adjustWounded(-1)">−</button>
|
|
|
<span class="sim-cond-val" id="sim-wounded-val">0</span>
|
|
|
<button class="sim-cond-btn" onclick="adjustWounded(1)">+</button>
|
|
|
</div>
|
|
|
<div class="sim-cond-item">
|
|
|
<span class="sim-cond-label">Doomed</span>
|
|
|
<button class="sim-cond-btn" onclick="adjustDoomed(-1)">−</button>
|
|
|
<span class="sim-cond-val" id="sim-doomed-val">0</span>
|
|
|
<button class="sim-cond-btn" onclick="adjustDoomed(1)">+</button>
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
<div id="sim-current-box" class="sim-box alive">
|
|
|
<div class="sim-box-label">Current State</div>
|
|
|
<div class="sim-state-name" id="sim-state-name">Alive</div>
|
|
|
<div class="sim-state-effects" id="sim-state-effects"></div>
|
|
|
</div>
|
|
|
|
|
|
<div id="sim-actions-section">
|
|
|
<div class="sim-section-head">What happens?</div>
|
|
|
<div class="sim-actions-list" id="sim-actions-list"></div>
|
|
|
</div>
|
|
|
|
|
|
<div id="sim-outcomes-section" style="display:none">
|
|
|
<div class="sim-outcomes-box">
|
|
|
<div class="sim-outcomes-head" id="sim-outcomes-head">Select the outcome:</div>
|
|
|
<div class="sim-outcomes-list" id="sim-outcomes-list"></div>
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
<div id="sim-result-container"></div>
|
|
|
|
|
|
<div class="sim-bottom-row">
|
|
|
<div class="sim-history-wrap">
|
|
|
<div class="sim-history-label">History</div>
|
|
|
<div class="sim-history-trail" id="sim-history-trail"></div>
|
|
|
</div>
|
|
|
<button class="sim-reset-btn" onclick="simReset()">↻ Reset</button>
|
|
|
</div>
|
|
|
</div>
|
|
|
</main>
|
|
|
</div>
|
|
|
|
|
|
<script>
|
|
|
mermaid.initialize({
|
|
|
startOnLoad: false,
|
|
|
theme: 'dark',
|
|
|
flowchart: { curve: 'basis', padding: 20, useMaxWidth: false },
|
|
|
securityLevel: 'loose',
|
|
|
});
|
|
|
|
|
|
document.addEventListener('DOMContentLoaded', async function() {
|
|
|
const diagrams = document.querySelectorAll('.mermaid');
|
|
|
for (let i = 0; i < diagrams.length; i++) {
|
|
|
const el = diagrams[i];
|
|
|
const id = el.id || ('mermaid-' + i);
|
|
|
try {
|
|
|
const { svg } = await mermaid.render(id + '-svg', el.textContent.trim());
|
|
|
el.innerHTML = svg;
|
|
|
} catch(e) {
|
|
|
console.error('Mermaid error in #' + id + ':', e);
|
|
|
el.innerHTML = '<pre style="color:#f07070;font-size:0.75rem;white-space:pre-wrap;padding:12px">Diagram error: ' + (e.message || e) + '</pre>';
|
|
|
}
|
|
|
}
|
|
|
simSetState('ALIVE', 0);
|
|
|
});
|
|
|
|
|
|
function showTab(name, btn) {
|
|
|
document.querySelectorAll('.tab-panel').forEach(function(p) { p.classList.remove('active'); });
|
|
|
document.querySelectorAll('.tab-btn').forEach(function(b) { b.classList.remove('active'); });
|
|
|
document.getElementById(name).classList.add('active');
|
|
|
btn.classList.add('active');
|
|
|
}
|
|
|
|
|
|
function toggleSidebar() {
|
|
|
document.getElementById('sidebar').classList.toggle('open');
|
|
|
document.getElementById('sidebar-overlay').classList.toggle('active');
|
|
|
}
|
|
|
|
|
|
function closeSidebar() {
|
|
|
document.getElementById('sidebar').classList.remove('open');
|
|
|
document.getElementById('sidebar-overlay').classList.remove('active');
|
|
|
}
|
|
|
|
|
|
// ── Simulator State ───────────────────────────────────────────
|
|
|
|
|
|
var simState = 'ALIVE';
|
|
|
var simDying = 0;
|
|
|
var simWounded = 0;
|
|
|
var simDoomed = 0;
|
|
|
var simHistory = [];
|
|
|
var simPending = null;
|
|
|
|
|
|
function deathThreshold() { return Math.max(1, 4 - simDoomed); }
|
|
|
|
|
|
function stateCssClass(state, dying) {
|
|
|
if (state === 'ALIVE') return 'alive';
|
|
|
if (state === 'DYING') return 'dying-' + Math.max(1, Math.min(3, dying));
|
|
|
if (state === 'UNCONSCIOUS') return 'unconscious';
|
|
|
if (state === 'DEAD') return 'dead';
|
|
|
return 'alive';
|
|
|
}
|
|
|
|
|
|
function stateLabel(state, dying) {
|
|
|
if (state === 'ALIVE') return 'Alive';
|
|
|
if (state === 'DYING') return 'Dying ' + dying;
|
|
|
if (state === 'UNCONSCIOUS') return 'Unconscious';
|
|
|
if (state === 'DEAD') return 'Dead';
|
|
|
return 'Alive';
|
|
|
}
|
|
|
|
|
|
function stateEffects(state, dying) {
|
|
|
if (state === 'ALIVE') {
|
|
|
return 'You are conscious and have at least 1 Hit Point. You can act normally.';
|
|
|
}
|
|
|
if (state === 'DYING') {
|
|
|
var dc = 10 + dying;
|
|
|
var threshold = deathThreshold();
|
|
|
var txt = 'You are unconscious and bleeding out. Attempt a <strong>flat check</strong> (DC ' + dc + ') at the start of each of your turns.';
|
|
|
txt += '<div class="sim-state-badges">';
|
|
|
txt += '<span class="sim-badge">Recovery DC: ' + dc + '</span>';
|
|
|
if (simDoomed > 0) {
|
|
|
txt += '<span class="sim-badge">Die at Dying ' + threshold + ' — Doomed ' + simDoomed + '</span>';
|
|
|
}
|
|
|
if (simWounded > 0) {
|
|
|
txt += '<span class="sim-badge">Wounded ' + simWounded + ' — all dying increases +' + simWounded + '</span>';
|
|
|
}
|
|
|
txt += '</div>';
|
|
|
return txt;
|
|
|
}
|
|
|
if (state === 'UNCONSCIOUS') {
|
|
|
var txt2 = 'You cannot act. You have the Blinded and Off-Guard conditions, and a −4 status penalty to AC, Perception, and Reflex saves. You have fallen prone.';
|
|
|
if (simWounded > 0) {
|
|
|
txt2 += '<div class="sim-state-badges"><span class="sim-badge">Wounded ' + simWounded + '</span></div>';
|
|
|
}
|
|
|
return txt2;
|
|
|
}
|
|
|
if (state === 'DEAD') {
|
|
|
return 'You have died. You cannot act or be targeted by most spells. Resurrection magic (<em>raise dead</em> or <em>resurrect</em> ritual) may restore you to life.';
|
|
|
}
|
|
|
return '';
|
|
|
}
|
|
|
|
|
|
// ── Transition Helpers ────────────────────────────────────────
|
|
|
|
|
|
function gotoNewDying(base) {
|
|
|
var startValue = base + simWounded;
|
|
|
var threshold = deathThreshold();
|
|
|
var wNote = simWounded > 0 ? ' (' + base + '+' + simWounded + 'W)' : '';
|
|
|
if (startValue >= threshold) {
|
|
|
simState = 'DEAD'; simDying = 0;
|
|
|
simHistory.push({ state: 'DEAD', dying: 0, via: 'Knocked Out' });
|
|
|
return { msg: 'You start at Dying ' + startValue + wNote + ', which meets or exceeds the death threshold of ' + threshold + '. You die instantly.' };
|
|
|
}
|
|
|
simState = 'DYING'; simDying = startValue;
|
|
|
simHistory.push({ state: 'DYING', dying: startValue, via: 'Knocked Out' });
|
|
|
return { msg: 'You are knocked unconscious with Dying ' + startValue + wNote + '. Attempt a recovery check (flat check DC ' + (10 + startValue) + ') at the start of each of your turns.' };
|
|
|
}
|
|
|
|
|
|
function recoveryImprove(amount) {
|
|
|
var old = simDying;
|
|
|
var newDying = old - amount;
|
|
|
if (newDying <= 0) {
|
|
|
simState = 'UNCONSCIOUS'; simDying = 0; simWounded += 1;
|
|
|
simHistory.push({ state: 'UNCONSCIOUS', dying: 0, via: 'Recovery Check' });
|
|
|
return { msg: 'Dying ' + old + ' − ' + amount + ' = 0. You lose the dying condition and stabilize at 0 HP. You gain the Wounded condition (now Wounded ' + simWounded + ').' };
|
|
|
}
|
|
|
simDying = newDying;
|
|
|
simHistory.push({ state: 'DYING', dying: newDying, via: 'Recovery Check' });
|
|
|
return { msg: 'Dying ' + old + ' − ' + amount + ' = Dying ' + newDying + '. Recovery DC is now ' + (10 + newDying) + '.' };
|
|
|
}
|
|
|
|
|
|
function recoveryWorsen(base) {
|
|
|
var old = simDying;
|
|
|
var total = base + simWounded;
|
|
|
var newDying = old + total;
|
|
|
var threshold = deathThreshold();
|
|
|
var wNote = simWounded > 0 ? ' (+' + simWounded + 'W = +' + total + ' total)' : '';
|
|
|
if (newDying >= threshold) {
|
|
|
simState = 'DEAD'; simDying = 0;
|
|
|
simHistory.push({ state: 'DEAD', dying: 0, via: 'Recovery Check Failed' });
|
|
|
return { msg: 'Dying ' + old + ' + ' + total + wNote + ' = Dying ' + newDying + ', reaching the death threshold of ' + threshold + '. You die.' };
|
|
|
}
|
|
|
simDying = newDying;
|
|
|
simHistory.push({ state: 'DYING', dying: newDying, via: 'Recovery Check Failed' });
|
|
|
return { msg: 'Dying ' + old + ' + ' + total + wNote + ' = Dying ' + newDying + '. Recovery DC is now ' + (10 + newDying) + '.' };
|
|
|
}
|
|
|
|
|
|
function dyingWorsen(base, label) {
|
|
|
var old = simDying;
|
|
|
var total = base + simWounded;
|
|
|
var newDying = old + total;
|
|
|
var threshold = deathThreshold();
|
|
|
var wNote = simWounded > 0 ? ' (+' + simWounded + 'W = +' + total + ' total)' : '';
|
|
|
if (newDying >= threshold) {
|
|
|
simState = 'DEAD'; simDying = 0;
|
|
|
simHistory.push({ state: 'DEAD', dying: 0, via: label });
|
|
|
return { msg: 'Dying ' + old + ' + ' + total + wNote + ' = Dying ' + newDying + ', reaching the death threshold of ' + threshold + '. You die.' };
|
|
|
}
|
|
|
simDying = newDying;
|
|
|
simHistory.push({ state: 'DYING', dying: newDying, via: label });
|
|
|
return { msg: 'Dying ' + old + ' + ' + total + wNote + ' = Dying ' + newDying + '.' };
|
|
|
}
|
|
|
|
|
|
// ── Actions ───────────────────────────────────────────────────
|
|
|
|
|
|
function getActions() {
|
|
|
var threshold = deathThreshold();
|
|
|
|
|
|
if (simState === 'ALIVE') return [
|
|
|
{
|
|
|
name: 'Reduced to 0 HP (lethal)',
|
|
|
hint: 'Hit by a lethal attack, spell, or effect',
|
|
|
needsRoll: true,
|
|
|
rollLabel: 'How were you reduced to 0 HP?',
|
|
|
getOutcomes: function() {
|
|
|
var nv = 1 + simWounded, cv = 2 + simWounded;
|
|
|
var wL = simWounded > 0 ? ' (+' + simWounded + 'W)' : '';
|
|
|
var dead = ' <strong style="color:var(--c-dead-hi)">\u2192 Instant Death!</strong>';
|
|
|
return [
|
|
|
{ label: 'Normal hit \u2014 Dying ' + nv + wL + (nv >= threshold ? dead : ''),
|
|
|
action: function() { return gotoNewDying(1); } },
|
|
|
{ label: 'Attacker\u2019s critical hit or your critical failure \u2014 Dying ' + cv + wL + (cv >= threshold ? dead : ''),
|
|
|
action: function() { return gotoNewDying(2); } },
|
|
|
];
|
|
|
},
|
|
|
},
|
|
|
{
|
|
|
name: 'Reduced to 0 HP (nonlethal)',
|
|
|
hint: 'Nonlethal attack or effect \u2014 no dying condition',
|
|
|
needsRoll: false,
|
|
|
action: function() {
|
|
|
simState = 'UNCONSCIOUS'; simDying = 0;
|
|
|
simHistory.push({ state: 'UNCONSCIOUS', dying: 0, via: 'Nonlethal KO' });
|
|
|
return { msg: 'You are knocked unconscious with 0 HP but <strong>no dying condition</strong>. You will naturally return to 1 HP and wake after sufficient time (minimum 10 minutes), or sooner with healing.' };
|
|
|
},
|
|
|
},
|
|
|
{
|
|
|
name: 'Massive Damage',
|
|
|
hint: 'Take damage \u2265 double your maximum HP in one blow',
|
|
|
needsRoll: false,
|
|
|
action: function() {
|
|
|
simState = 'DEAD'; simDying = 0;
|
|
|
simHistory.push({ state: 'DEAD', dying: 0, via: 'Massive Damage' });
|
|
|
return { msg: 'You die instantly. Taking damage equal to or greater than <strong>double your maximum Hit Points</strong> in a single blow causes immediate death, bypassing the dying condition entirely.' };
|
|
|
},
|
|
|
},
|
|
|
{
|
|
|
name: 'Death Effect',
|
|
|
hint: 'A spell or ability with the death trait \u2014 kills without reaching dying 4',
|
|
|
needsRoll: false,
|
|
|
action: function() {
|
|
|
simState = 'DEAD'; simDying = 0;
|
|
|
simHistory.push({ state: 'DEAD', dying: 0, via: 'Death Effect' });
|
|
|
return { msg: 'You are slain instantly by a death effect. These abilities bypass the normal dying progression — if they reduce you to 0 HP you die without reaching dying 4. Some kill outright without dealing damage at all.' };
|
|
|
},
|
|
|
},
|
|
|
{
|
|
|
name: 'Short Rest (10 min)',
|
|
|
hint: simWounded > 0 ? 'Treat Wounds \u2014 Medicine check to clear Wounded ' + simWounded : 'No Wounded condition to treat',
|
|
|
needsRoll: simWounded > 0,
|
|
|
rollLabel: 'Treat Wounds \u2014 Medicine check result:',
|
|
|
getOutcomes: function() {
|
|
|
var prev = simWounded;
|
|
|
return [
|
|
|
{ label: '✓ Success \u2014 Wounded ' + prev + ' clears',
|
|
|
action: function() {
|
|
|
simWounded = 0; renderCondVals();
|
|
|
simHistory.push({ state: 'ALIVE', dying: 0, via: 'Short Rest' });
|
|
|
return { msg: 'Treat Wounds succeeds. Your <strong>Wounded ' + prev + '</strong> condition is cleared.' };
|
|
|
}
|
|
|
},
|
|
|
{ label: '✗ Failure \u2014 Wounded ' + prev + ' remains',
|
|
|
action: function() {
|
|
|
simHistory.push({ state: 'ALIVE', dying: 0, via: 'Short Rest' });
|
|
|
return { msg: 'Treat Wounds fails. Your <strong>Wounded ' + prev + '</strong> condition remains.' };
|
|
|
}
|
|
|
},
|
|
|
];
|
|
|
},
|
|
|
action: function() {
|
|
|
simHistory.push({ state: 'ALIVE', dying: 0, via: 'Short Rest' });
|
|
|
return { msg: 'You rest for 10 minutes. You have no Wounded condition to treat.' };
|
|
|
},
|
|
|
},
|
|
|
{
|
|
|
name: 'Long Rest (full night)',
|
|
|
hint: 'Doomed \u22121; Wounded clears (restored to full HP during rest)',
|
|
|
needsRoll: false,
|
|
|
action: function() {
|
|
|
var parts = [];
|
|
|
if (simDoomed > 0) {
|
|
|
simDoomed -= 1;
|
|
|
parts.push('Doomed decreases by 1 (now <strong>Doomed ' + simDoomed + '</strong>).');
|
|
|
} else {
|
|
|
parts.push('Doomed is already 0.');
|
|
|
}
|
|
|
if (simWounded > 0) {
|
|
|
var prev = simWounded; simWounded = 0;
|
|
|
parts.push('Wounded ' + prev + ' clears (restored to full HP and rested).');
|
|
|
} else {
|
|
|
parts.push('No Wounded condition to clear.');
|
|
|
}
|
|
|
renderCondVals();
|
|
|
simHistory.push({ state: 'ALIVE', dying: 0, via: 'Long Rest' });
|
|
|
return { msg: parts.join(' ') };
|
|
|
},
|
|
|
},
|
|
|
];
|
|
|
|
|
|
if (simState === 'DYING') {
|
|
|
var dc = 10 + simDying;
|
|
|
return [
|
|
|
{
|
|
|
name: 'Recovery Check',
|
|
|
hint: 'Flat check at start of your turn \u2014 DC ' + dc,
|
|
|
needsRoll: true,
|
|
|
rollLabel: 'Recovery Check \u2014 flat check DC <strong>' + dc + '</strong>:',
|
|
|
getOutcomes: function() {
|
|
|
var t = deathThreshold();
|
|
|
var csR = simDying - 2, sR = simDying - 1;
|
|
|
var fR = simDying + 1 + simWounded, cfR = simDying + 2 + simWounded;
|
|
|
var wL = simWounded > 0 ? '+' + simWounded + 'W' : '';
|
|
|
var dead = ' <strong style="color:var(--c-dead-hi)">\u2192 Death</strong>';
|
|
|
var stable = ' <strong style="color:var(--c-alive-hi)">\u2192 Stabilize</strong>';
|
|
|
return [
|
|
|
{ label: '✓✓ Critical Success \u2014 dying−2 = Dying ' + Math.max(0,csR) + (csR <= 0 ? stable : ''),
|
|
|
action: function() { return recoveryImprove(2); } },
|
|
|
{ label: '✓ Success \u2014 dying−1 = Dying ' + Math.max(0,sR) + (sR <= 0 ? stable : ''),
|
|
|
action: function() { return recoveryImprove(1); } },
|
|
|
{ label: '✗ Failure \u2014 dying+1' + (wL ? '+' + wL : '') + ' = Dying ' + fR + (fR >= t ? dead : ''),
|
|
|
action: function() { return recoveryWorsen(1); } },
|
|
|
{ label: '✗✗ Critical Failure \u2014 dying+2' + (wL ? '+' + wL : '') + ' = Dying ' + cfR + (cfR >= t ? dead : ''),
|
|
|
action: function() { return recoveryWorsen(2); } },
|
|
|
];
|
|
|
},
|
|
|
},
|
|
|
{
|
|
|
name: 'Take Damage While Dying',
|
|
|
hint: 'Any damage while dying increases dying value (+W)',
|
|
|
needsRoll: true,
|
|
|
rollLabel: 'Was the hit a critical?',
|
|
|
getOutcomes: function() {
|
|
|
var t = deathThreshold();
|
|
|
var nR = simDying + 1 + simWounded, cR = simDying + 2 + simWounded;
|
|
|
var wL = simWounded > 0 ? '+' + simWounded + 'W' : '';
|
|
|
var dead = ' <strong style="color:var(--c-dead-hi)">\u2192 Death</strong>';
|
|
|
return [
|
|
|
{ label: 'Normal hit \u2014 dying+1' + (wL ? '+' + wL : '') + ' = Dying ' + nR + (nR >= t ? dead : ''),
|
|
|
action: function() { return dyingWorsen(1, 'Take Damage'); } },
|
|
|
{ label: 'Critical hit or critical failure \u2014 dying+2' + (wL ? '+' + wL : '') + ' = Dying ' + cR + (cR >= t ? dead : ''),
|
|
|
action: function() { return dyingWorsen(2, 'Critical Hit'); } },
|
|
|
];
|
|
|
},
|
|
|
},
|
|
|
{
|
|
|
name: 'Receive Healing (1+ HP)',
|
|
|
hint: 'A spell, potion, or ability restores at least 1 HP',
|
|
|
needsRoll: false,
|
|
|
action: function() {
|
|
|
simState = 'ALIVE'; simDying = 0; simWounded += 1;
|
|
|
simHistory.push({ state: 'ALIVE', dying: 0, via: 'Healing' });
|
|
|
return { msg: 'You regain Hit Points and wake up. You lose the dying condition automatically. You gain the Wounded condition (now <strong>Wounded ' + simWounded + '</strong>).' };
|
|
|
},
|
|
|
},
|
|
|
{
|
|
|
name: 'Heroic Recovery',
|
|
|
hint: 'Spend all Hero Points at start of turn or when dying would increase',
|
|
|
needsRoll: false,
|
|
|
action: function() {
|
|
|
simState = 'UNCONSCIOUS'; simDying = 0;
|
|
|
simHistory.push({ state: 'UNCONSCIOUS', dying: 0, via: 'Heroic Recovery' });
|
|
|
return { msg: 'You spend all your Hero Points. You lose the dying condition and stabilize at 0 HP. Your Wounded condition does <strong>not</strong> increase from this recovery.' + (simWounded > 0 ? ' You keep your existing Wounded ' + simWounded + '.' : '') };
|
|
|
},
|
|
|
},
|
|
|
];
|
|
|
}
|
|
|
|
|
|
if (simState === 'UNCONSCIOUS') return [
|
|
|
{
|
|
|
name: 'Receive Healing (1+ HP)',
|
|
|
hint: 'A spell, potion, or ability restores at least 1 HP',
|
|
|
needsRoll: false,
|
|
|
action: function() {
|
|
|
simState = 'ALIVE'; simDying = 0;
|
|
|
simHistory.push({ state: 'ALIVE', dying: 0, via: 'Healing' });
|
|
|
return { msg: 'You regain Hit Points and wake up. You lose the unconscious condition and can act normally on your next turn.' };
|
|
|
},
|
|
|
},
|
|
|
{
|
|
|
name: 'Natural Recovery (time passes)',
|
|
|
hint: 'At 0 HP \u2014 return to 1 HP after 10+ minutes',
|
|
|
needsRoll: false,
|
|
|
action: function() {
|
|
|
simState = 'ALIVE'; simDying = 0;
|
|
|
simHistory.push({ state: 'ALIVE', dying: 0, via: 'Natural Recovery' });
|
|
|
return { msg: 'Sufficient time has passed. You naturally return to 1 Hit Point and awaken. The GM determines how long this takes (minimum 10 minutes, up to several hours).' };
|
|
|
},
|
|
|
},
|
|
|
{
|
|
|
name: 'Woken Up',
|
|
|
hint: 'Loud noise, damage (not to 0 HP), healing, or Interact action',
|
|
|
needsRoll: false,
|
|
|
action: function() {
|
|
|
simState = 'ALIVE'; simDying = 0;
|
|
|
simHistory.push({ state: 'ALIVE', dying: 0, via: 'Woken Up' });
|
|
|
return { msg: 'You are awoken — by a loud noise (Perception check vs DC 5 for battle), an ally using an Interact action, healing, or damage that doesn\u2019t reduce you to 0 HP. You lose the unconscious condition.' };
|
|
|
},
|
|
|
},
|
|
|
{
|
|
|
name: 'Long Rest (full night)',
|
|
|
hint: 'Wake at full HP; Doomed \u22121; Wounded clears',
|
|
|
needsRoll: false,
|
|
|
action: function() {
|
|
|
simState = 'ALIVE'; simDying = 0;
|
|
|
var parts = ['You wake fully rested and restored to full HP.'];
|
|
|
if (simDoomed > 0) {
|
|
|
simDoomed -= 1;
|
|
|
parts.push('Doomed decreases by 1 (now <strong>Doomed ' + simDoomed + '</strong>).');
|
|
|
}
|
|
|
if (simWounded > 0) {
|
|
|
var prev = simWounded; simWounded = 0;
|
|
|
parts.push('Wounded ' + prev + ' clears.');
|
|
|
}
|
|
|
renderCondVals();
|
|
|
simHistory.push({ state: 'ALIVE', dying: 0, via: 'Long Rest' });
|
|
|
return { msg: parts.join(' ') };
|
|
|
},
|
|
|
},
|
|
|
{
|
|
|
name: 'Take Lethal Damage (to 0 HP)',
|
|
|
hint: 'Damage while at 0 HP \u2014 gain dying condition (+W)',
|
|
|
needsRoll: false,
|
|
|
action: function() {
|
|
|
var startValue = 1 + simWounded;
|
|
|
var threshold = deathThreshold();
|
|
|
var wNote = simWounded > 0 ? ' (+' + simWounded + 'W)' : '';
|
|
|
if (startValue >= threshold) {
|
|
|
simState = 'DEAD'; simDying = 0;
|
|
|
simHistory.push({ state: 'DEAD', dying: 0, via: 'Damage While Unconscious' });
|
|
|
return { msg: 'You take lethal damage at 0 HP and gain the dying condition at Dying ' + startValue + wNote + '. This meets the death threshold of ' + threshold + '. You die.' };
|
|
|
}
|
|
|
simState = 'DYING'; simDying = startValue;
|
|
|
simHistory.push({ state: 'DYING', dying: startValue, via: 'Damage While Unconscious' });
|
|
|
return { msg: 'You take lethal damage at 0 HP and gain the dying condition at Dying ' + startValue + wNote + '. Attempt a recovery check (flat check DC ' + (10 + startValue) + ') at the start of each turn.' };
|
|
|
},
|
|
|
},
|
|
|
];
|
|
|
|
|
|
if (simState === 'DEAD') return [
|
|
|
{
|
|
|
name: 'Resurrection Magic',
|
|
|
hint: 'raise dead spell, resurrect ritual, or similar',
|
|
|
needsRoll: false,
|
|
|
action: function() {
|
|
|
simState = 'ALIVE'; simDying = 0;
|
|
|
simHistory.push({ state: 'ALIVE', dying: 0, via: 'Resurrection' });
|
|
|
return { msg: 'Powerful magic restores you to life. Note: many resurrection methods impose the <strong>Doomed</strong> condition or other lasting effects, and rare artifacts or powers may block resurrection entirely.' };
|
|
|
},
|
|
|
},
|
|
|
];
|
|
|
|
|
|
return [];
|
|
|
}
|
|
|
|
|
|
// ── Condition Adjusters ───────────────────────────────────────
|
|
|
|
|
|
function adjustDying(delta) {
|
|
|
var current = (simState === 'DYING') ? simDying : 0;
|
|
|
var newVal = current + delta;
|
|
|
if (newVal <= 0) {
|
|
|
if (simState === 'DYING') { simState = 'UNCONSCIOUS'; }
|
|
|
simDying = 0;
|
|
|
} else if (newVal >= deathThreshold()) {
|
|
|
simState = 'DEAD'; simDying = 0;
|
|
|
} else {
|
|
|
simState = 'DYING'; simDying = newVal;
|
|
|
}
|
|
|
renderCondVals();
|
|
|
simRenderStateBox();
|
|
|
simRenderActions();
|
|
|
document.getElementById('sim-outcomes-section').style.display = 'none';
|
|
|
document.getElementById('sim-result-container').innerHTML = '';
|
|
|
}
|
|
|
|
|
|
function adjustWounded(delta) {
|
|
|
simWounded = Math.max(0, simWounded + delta);
|
|
|
renderCondVals();
|
|
|
simRenderStateBox();
|
|
|
simRenderActions();
|
|
|
}
|
|
|
|
|
|
function adjustDoomed(delta) {
|
|
|
simDoomed = Math.max(0, Math.min(3, simDoomed + delta));
|
|
|
if (simState === 'DYING' && simDying >= deathThreshold()) {
|
|
|
simState = 'DEAD'; simDying = 0;
|
|
|
simHistory.push({ state: 'DEAD', dying: 0, via: 'Doomed threshold' });
|
|
|
}
|
|
|
renderCondVals();
|
|
|
simRenderStateBox();
|
|
|
simRenderActions();
|
|
|
}
|
|
|
|
|
|
function renderCondVals() {
|
|
|
var dyEl = document.getElementById('sim-dying-val');
|
|
|
var wEl = document.getElementById('sim-wounded-val');
|
|
|
var dEl = document.getElementById('sim-doomed-val');
|
|
|
var dv = (simState === 'DYING') ? simDying : 0;
|
|
|
dyEl.textContent = dv;
|
|
|
dyEl.className = 'sim-cond-val' + (dv > 0 ? ' dying-hi-' + Math.min(3, dv) : '');
|
|
|
wEl.textContent = simWounded;
|
|
|
wEl.className = 'sim-cond-val' + (simWounded > 0 ? ' wounded-hi' : '');
|
|
|
dEl.textContent = simDoomed;
|
|
|
dEl.className = 'sim-cond-val' + (simDoomed > 0 ? ' doomed-hi' : '');
|
|
|
}
|
|
|
|
|
|
// ── Core Sim Functions ────────────────────────────────────────
|
|
|
|
|
|
function simSetState(state, dyingVal) {
|
|
|
if (state === 'DYING' && dyingVal >= deathThreshold()) {
|
|
|
simState = 'DEAD'; simDying = 0;
|
|
|
} else {
|
|
|
simState = state; simDying = dyingVal;
|
|
|
}
|
|
|
simHistory = [{ state: simState, dying: simDying }];
|
|
|
simPending = null;
|
|
|
renderCondVals();
|
|
|
simRender();
|
|
|
}
|
|
|
|
|
|
function simReset() {
|
|
|
simState = 'ALIVE'; simDying = 0; simWounded = 0; simDoomed = 0;
|
|
|
simHistory = [{ state: 'ALIVE', dying: 0 }];
|
|
|
simPending = null;
|
|
|
renderCondVals();
|
|
|
simRender();
|
|
|
}
|
|
|
|
|
|
function simSelectAction(idx) {
|
|
|
var actions = getActions();
|
|
|
var action = actions[idx];
|
|
|
if (!action) return;
|
|
|
simPending = action;
|
|
|
document.getElementById('sim-result-container').innerHTML = '';
|
|
|
if (!action.needsRoll) {
|
|
|
var result = action.action();
|
|
|
simRenderWithResult(result.msg);
|
|
|
} else {
|
|
|
simRenderActionHighlight(idx);
|
|
|
simRenderOutcomes(action);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
function simSelectOutcome(idx) {
|
|
|
if (!simPending || !simPending.getOutcomes) return;
|
|
|
var outcomes = simPending.getOutcomes();
|
|
|
var o = outcomes[idx];
|
|
|
if (!o) return;
|
|
|
var result = o.action();
|
|
|
simPending = null;
|
|
|
simRenderWithResult(result.msg);
|
|
|
}
|
|
|
|
|
|
function simRender() {
|
|
|
simRenderStateBox();
|
|
|
simRenderActions();
|
|
|
document.getElementById('sim-outcomes-section').style.display = 'none';
|
|
|
document.getElementById('sim-result-container').innerHTML = '';
|
|
|
simRenderHistory();
|
|
|
}
|
|
|
|
|
|
function simRenderWithResult(msg) {
|
|
|
renderCondVals();
|
|
|
simRenderStateBox();
|
|
|
simRenderActions();
|
|
|
document.getElementById('sim-outcomes-section').style.display = 'none';
|
|
|
var css = stateCssClass(simState, simDying);
|
|
|
var lbl = stateLabel(simState, simDying);
|
|
|
document.getElementById('sim-result-container').innerHTML =
|
|
|
'<div class="sim-result-box ' + css + '">' +
|
|
|
'<div class="sim-result-top">' +
|
|
|
'<span style="color:var(--text-muted);font-size:1.1rem;">\u2192</span>' +
|
|
|
'<span class="sim-result-new-state">' + lbl + '</span>' +
|
|
|
'</div>' +
|
|
|
'<div class="sim-result-msg">' + msg + '</div>' +
|
|
|
'</div>';
|
|
|
simRenderHistory();
|
|
|
}
|
|
|
|
|
|
function simRenderStateBox() {
|
|
|
var css = stateCssClass(simState, simDying);
|
|
|
var box = document.getElementById('sim-current-box');
|
|
|
box.className = 'sim-box ' + css;
|
|
|
document.getElementById('sim-state-name').textContent = stateLabel(simState, simDying);
|
|
|
document.getElementById('sim-state-effects').innerHTML = stateEffects(simState, simDying);
|
|
|
}
|
|
|
|
|
|
function simRenderActions() {
|
|
|
var actions = getActions();
|
|
|
document.getElementById('sim-actions-list').innerHTML = actions.map(function(a, i) {
|
|
|
return '<button class="sim-action" id="sim-act-' + i + '" onclick="simSelectAction(' + i + ')">' +
|
|
|
'<div class="sim-action-name">' + a.name + '</div>' +
|
|
|
'<div class="sim-action-hint">' + a.hint + '</div>' +
|
|
|
'</button>';
|
|
|
}).join('');
|
|
|
}
|
|
|
|
|
|
function simRenderActionHighlight(idx) {
|
|
|
document.querySelectorAll('.sim-action').forEach(function(b) { b.classList.remove('selected'); });
|
|
|
var btn = document.getElementById('sim-act-' + idx);
|
|
|
if (btn) btn.classList.add('selected');
|
|
|
}
|
|
|
|
|
|
function simRenderOutcomes(action) {
|
|
|
var section = document.getElementById('sim-outcomes-section');
|
|
|
section.style.display = 'block';
|
|
|
document.getElementById('sim-outcomes-head').innerHTML = action.rollLabel;
|
|
|
var outcomes = action.getOutcomes();
|
|
|
document.getElementById('sim-outcomes-list').innerHTML = outcomes.map(function(o, i) {
|
|
|
return '<button class="sim-outcome" onclick="simSelectOutcome(' + i + ')">' + o.label + '</button>';
|
|
|
}).join('');
|
|
|
}
|
|
|
|
|
|
function simRenderHistory() {
|
|
|
var trail = document.getElementById('sim-history-trail');
|
|
|
var html = '';
|
|
|
simHistory.forEach(function(entry, i) {
|
|
|
var css = stateCssClass(entry.state, entry.dying);
|
|
|
var lbl = stateLabel(entry.state, entry.dying);
|
|
|
if (i > 0 && entry.via) {
|
|
|
html += '<span class="sim-hist-via">\u2192 ' + entry.via + ' \u2192</span> ';
|
|
|
}
|
|
|
html += '<span class="sim-hist-state ' + css + '">' + lbl + '</span> ';
|
|
|
});
|
|
|
trail.innerHTML = html || '<span style="color:var(--text-muted);font-style:italic;font-size:0.78rem">No actions yet</span>';
|
|
|
}
|
|
|
</script>
|
|
|
</body>
|
|
|
</html>
|