You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
731 lines
32 KiB
731 lines
32 KiB
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>PF2e Perception & Stealth</title>
|
|
<link rel="stylesheet" href="global.css">
|
|
<script src="https://cdn.jsdelivr.net/npm/mermaid@10/dist/mermaid.min.js"></script>
|
|
<style>
|
|
:root {
|
|
--c-observed: #193a14;
|
|
--c-observed-hi: #5ec454;
|
|
--c-observed-border: #3a7c32;
|
|
|
|
--c-hidden: #3e2e06;
|
|
--c-hidden-hi: #f0c040;
|
|
--c-hidden-border: #a07a10;
|
|
|
|
--c-undetected: #3e1a06;
|
|
--c-undetected-hi: #f07830;
|
|
--c-undetected-border: #a04010;
|
|
|
|
--c-unnoticed: #300c0c;
|
|
--c-unnoticed-hi: #f04040;
|
|
--c-unnoticed-border: #8a1010;
|
|
|
|
--c-concealed: #0e2234;
|
|
--c-concealed-hi: #50a8e8;
|
|
--c-concealed-border: #2060a0;
|
|
|
|
--c-invisible: #180e30;
|
|
--c-invisible-hi: #9070e0;
|
|
--c-invisible-border: #5030b0;
|
|
}
|
|
|
|
aside.open { width: 270px; min-width: 270px; border-right-width: 1px; }
|
|
.aside-inner { width: 270px; padding: 16px 14px; }
|
|
|
|
.diagram-wrap .mermaid { min-width: 600px; }
|
|
|
|
.cover-box {
|
|
margin-top: 14px; padding: 10px 12px;
|
|
background: var(--surface2); border: 1px solid var(--border); border-radius: 5px;
|
|
font-size: 0.76rem; line-height: 1.7;
|
|
}
|
|
.cover-box h3 { color: var(--accent); font-size: 0.75rem; text-transform: uppercase; letter-spacing: 0.1em; margin-bottom: 6px; }
|
|
.cover-box strong { color: var(--accent); }
|
|
|
|
.ccard.observed { background: var(--c-observed); border-color: var(--c-observed-border); }
|
|
.ccard.observed h3{ color: var(--c-observed-hi); }
|
|
.ccard.observed .tag { background: #0d2a0a; color: var(--c-observed-hi); }
|
|
|
|
.ccard.hidden { background: var(--c-hidden); border-color: var(--c-hidden-border); }
|
|
.ccard.hidden h3 { color: var(--c-hidden-hi); }
|
|
.ccard.hidden .tag{ background: #2a1e04; color: var(--c-hidden-hi); }
|
|
|
|
.ccard.undetected { background: var(--c-undetected); border-color: var(--c-undetected-border); }
|
|
.ccard.undetected h3 { color: var(--c-undetected-hi); }
|
|
.ccard.undetected .tag{ background: #2a1004; color: var(--c-undetected-hi); }
|
|
|
|
.ccard.unnoticed { background: var(--c-unnoticed); border-color: var(--c-unnoticed-border); }
|
|
.ccard.unnoticed h3 { color: var(--c-unnoticed-hi); }
|
|
.ccard.unnoticed .tag{ background: #200808; color: var(--c-unnoticed-hi); }
|
|
|
|
.ccard.concealed { background: var(--c-concealed); border-color: var(--c-concealed-border); }
|
|
.ccard.concealed h3 { color: var(--c-concealed-hi); }
|
|
.ccard.concealed .tag{ background: #081422; color: var(--c-concealed-hi); }
|
|
|
|
.ccard.invisible { background: var(--c-invisible); border-color: var(--c-invisible-border); }
|
|
.ccard.invisible h3 { color: var(--c-invisible-hi); }
|
|
.ccard.invisible .tag{ background: #100820; color: var(--c-invisible-hi); }
|
|
|
|
.sim-start-row { display: flex; align-items: center; gap: 8px; margin-bottom: 20px; flex-wrap: wrap; }
|
|
|
|
.sim-chip.observed { color: var(--c-observed-hi); border-color: var(--c-observed-border); }
|
|
.sim-chip.hidden { color: var(--c-hidden-hi); border-color: var(--c-hidden-border); }
|
|
.sim-chip.undetected { color: var(--c-undetected-hi); border-color: var(--c-undetected-border); }
|
|
.sim-chip.unnoticed { color: var(--c-unnoticed-hi); border-color: var(--c-unnoticed-border); }
|
|
|
|
.sim-box.observed { background: var(--c-observed); border-color: var(--c-observed-border); }
|
|
.sim-box.hidden { background: var(--c-hidden); border-color: var(--c-hidden-border); }
|
|
.sim-box.undetected { background: var(--c-undetected); border-color: var(--c-undetected-border); }
|
|
.sim-box.unnoticed { background: var(--c-unnoticed); border-color: var(--c-unnoticed-border); }
|
|
|
|
.sim-box.observed .sim-state-name { color: var(--c-observed-hi); }
|
|
.sim-box.hidden .sim-state-name { color: var(--c-hidden-hi); }
|
|
.sim-box.undetected .sim-state-name { color: var(--c-undetected-hi); }
|
|
.sim-box.unnoticed .sim-state-name { color: var(--c-unnoticed-hi); }
|
|
|
|
.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: 300px;
|
|
transition: border-color 0.15s, color 0.15s, background 0.15s;
|
|
}
|
|
.sim-outcomes-list { display: flex; flex-wrap: wrap; gap: 8px; }
|
|
.sim-outcome {
|
|
padding: 7px 16px; background: var(--surface); border: 1px solid var(--border);
|
|
color: var(--text); border-radius: 4px; cursor: pointer; font-family: inherit; font-size: 0.82rem;
|
|
transition: border-color 0.15s, color 0.15s;
|
|
}
|
|
|
|
.sim-result-box.observed { background: var(--c-observed); border-color: var(--c-observed-border); }
|
|
.sim-result-box.hidden { background: var(--c-hidden); border-color: var(--c-hidden-border); }
|
|
.sim-result-box.undetected { background: var(--c-undetected); border-color: var(--c-undetected-border); }
|
|
.sim-result-box.unnoticed { background: var(--c-unnoticed); border-color: var(--c-unnoticed-border); }
|
|
|
|
.sim-result-box.observed .sim-result-new-state { color: var(--c-observed-hi); }
|
|
.sim-result-box.hidden .sim-result-new-state { color: var(--c-hidden-hi); }
|
|
.sim-result-box.undetected .sim-result-new-state { color: var(--c-undetected-hi); }
|
|
.sim-result-box.unnoticed .sim-result-new-state { color: var(--c-unnoticed-hi); }
|
|
|
|
.sim-hist-state.observed { color: var(--c-observed-hi); }
|
|
.sim-hist-state.hidden { color: var(--c-hidden-hi); }
|
|
.sim-hist-state.undetected { color: var(--c-undetected-hi); }
|
|
.sim-hist-state.unnoticed { color: var(--c-unnoticed-hi); }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
|
|
<header>
|
|
<h1>PF2e Perception & Stealth</h1>
|
|
<p>Flow of Control Reference — Player Core</p>
|
|
<button class="sidebar-toggle-btn" onclick="toggleSidebar()" title="Toggle Detection Conditions">
|
|
☰ Conditions
|
|
</button>
|
|
</header>
|
|
|
|
<div class="layout">
|
|
|
|
<!-- ── Sidebar ── -->
|
|
<aside id="sidebar">
|
|
<div class="aside-inner">
|
|
<div class="aside-header">
|
|
<h2>Detection Conditions</h2>
|
|
<button class="aside-close" onclick="closeSidebar()" title="Close">×</button>
|
|
</div>
|
|
|
|
<div class="ccard observed">
|
|
<h3>Observed</h3>
|
|
<p>You perceive the creature clearly with a precise sense. You can target it normally.</p>
|
|
<span class="tag">No flat check to target</span>
|
|
</div>
|
|
|
|
<div class="ccard concealed">
|
|
<h3>Concealed</h3>
|
|
<p>In mist, dim light, or similar. Still <em>observed</em>, but harder to affect. Does not change the main detection category.</p>
|
|
<span class="tag">DC 5 flat check to affect</span>
|
|
</div>
|
|
|
|
<div class="ccard hidden">
|
|
<h3>Hidden</h3>
|
|
<p>You know the creature’s space but can’t see it. You are off-guard to it. The creature cannot take non-unobtrusive actions without being observed.</p>
|
|
<span class="tag">DC 11 flat check to affect</span>
|
|
</div>
|
|
|
|
<div class="ccard undetected">
|
|
<h3>Undetected</h3>
|
|
<p>You don’t know which space it’s in. You are off-guard. You may know it is nearby (undetected) or have no clue at all (unnoticed).</p>
|
|
<span class="tag">Pick a square — GM rolls secretly</span>
|
|
</div>
|
|
|
|
<div class="ccard unnoticed">
|
|
<h3>Unnoticed</h3>
|
|
<p>You have no idea the creature even exists nearby. Certain abilities only work on totally unaware targets.</p>
|
|
<span class="tag">Cannot target at all</span>
|
|
</div>
|
|
|
|
<div class="ccard invisible">
|
|
<h3>Invisible</h3>
|
|
<p>Automatically <em>undetected</em> against creatures relying on sight alone. Precise senses other than vision still work. Seek can reveal location, making it hidden.</p>
|
|
<span class="tag">Undetected to sight-only perceivers</span>
|
|
</div>
|
|
|
|
<div class="cover-box">
|
|
<h3>Cover Bonuses (Stealth)</h3>
|
|
<strong>Standard Cover:</strong> +2 to Stealth<br>
|
|
<strong>Greater Cover:</strong> +4 to Stealth
|
|
</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('stealth', this)">Stealth Actions</button>
|
|
<button class="tab-btn" onclick="showTab('detection', this)">Detection & Targeting</button>
|
|
</div>
|
|
|
|
<!-- Stealth Tab -->
|
|
<div id="stealth" class="tab-panel">
|
|
<h2>Stealth Actions Flow</h2>
|
|
<p class="subtitle">From the <strong>stealther’s</strong> perspective — how detection states change based on actions you take</p>
|
|
|
|
<div class="legend">
|
|
<div class="legend-item"><div class="legend-dot" style="background:var(--c-observed);border-color:var(--c-observed-hi)"></div> Observed</div>
|
|
<div class="legend-item"><div class="legend-dot" style="background:var(--c-hidden);border-color:var(--c-hidden-hi)"></div> Hidden</div>
|
|
<div class="legend-item"><div class="legend-dot" style="background:var(--c-undetected);border-color:var(--c-undetected-hi)"></div> Undetected</div>
|
|
<div class="legend-item"><div class="legend-dot" style="background:var(--c-unnoticed);border-color:var(--c-unnoticed-hi)"></div> Unnoticed</div>
|
|
<div class="legend-item"><div class="legend-dot" style="background:#1a2840;border-color:#4080b0"></div> Action / Roll</div>
|
|
</div>
|
|
|
|
<div class="diagram-wrap">
|
|
<div class="mermaid" id="stealth-diagram">
|
|
%%{init: {'theme':'dark','themeVariables':{'background':'#161210','primaryColor':'#193a14','primaryTextColor':'#e8d5b0','primaryBorderColor':'#3a7c32','lineColor':'#9a7e56','secondaryColor':'#3e2e06','tertiaryColor':'#1a2840','edgeLabelBackground':'#211a13','clusterBkg':'#211a13'}}}%%
|
|
flowchart LR
|
|
|
|
classDef obs fill:#193a14,stroke:#3a7c32,color:#b8e8b0,rx:30
|
|
classDef hid fill:#3e2e06,stroke:#a07a10,color:#f0e060
|
|
classDef und fill:#3e1a06,stroke:#a04010,color:#f0a868
|
|
classDef unn fill:#300c0c,stroke:#8a1010,color:#f07070
|
|
classDef act fill:#1a2840,stroke:#4080b0,color:#90c0e8
|
|
classDef roll fill:#201828,stroke:#6040a0,color:#c0a0e8
|
|
|
|
OBS(["OBSERVED"]):::obs
|
|
HID(["HIDDEN"]):::hid
|
|
UND(["UNDETECTED"]):::und
|
|
UNN(["UNNOTICED"]):::unn
|
|
|
|
HIDE["HIDE
|
|
- needs cover or concealment
|
|
- secret Stealth roll vs Perception DC
|
|
- Standard cover +2
|
|
- Greater cover +4"]:::act
|
|
|
|
SNEAK["SNEAK
|
|
- cover at START and END
|
|
- move ≤ half Speed
|
|
- secret Stealth roll vs Perception DC"]:::act
|
|
|
|
OBS --> HIDE
|
|
HIDE -->|"✓ Success"| HID
|
|
HIDE -->|"✗ Failure"| OBS
|
|
|
|
HID --> SNEAK
|
|
HID -->|"Non-stealthy action
|
|
or Strike"| OBS
|
|
HID -->|"STEP
|
|
with cover / concealment"| HID
|
|
HID -->|"Unobtrusive action
|
|
GM may require Stealth"| HID
|
|
|
|
SNEAK -->|"✓ Success"| UND
|
|
SNEAK -->|"✗ Failure"| HID
|
|
SNEAK -->|"✗✗ Crit Failure
|
|
(not invisible)"| OBS
|
|
|
|
UND -->|"SNEAK ✓ Success"| UND
|
|
UND -->|"Speak or
|
|
make loud noise"| OBS
|
|
|
|
UNN -.->|"may also be"| UND
|
|
UNN -->|"Speak, loud noise
|
|
or non-stealthy action"| OBS
|
|
</div>
|
|
</div>
|
|
|
|
<div class="note-box">
|
|
<h3>Key Rules</h3>
|
|
<ul>
|
|
<li><strong>HIDE</strong> only rolls against creatures that are currently <em>observing</em> you. If a creature already considers you hidden or undetected, you simply retain that condition — HIDE does not improve your state from undetected.</li>
|
|
<li><strong>SNEAK</strong> requires cover or concealment at <em>both the start and end</em> of your movement. You move up to half your Speed.</li>
|
|
<li>Taking any non-<em>unobtrusive</em> action while hidden immediately makes you <strong>Observed</strong>.</li>
|
|
<li>A <strong>Strike</strong> from a hidden or undetected position makes you <strong>Observed</strong> after the attack (target now knows your location).</li>
|
|
<li>An <strong>Invisible</strong> creature that critically fails a Sneak roll becomes <strong>Hidden</strong> rather than Observed, since it cannot be seen.</li>
|
|
<li><strong>AVOID NOTICE</strong> (Exploration): Roll Stealth to start an encounter Hidden or Undetected rather than Observed.</li>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Detection Tab -->
|
|
<div id="detection" class="tab-panel">
|
|
<h2>Detection & Targeting Flow</h2>
|
|
<p class="subtitle">From the <strong>detector’s</strong> perspective — how to determine a creature’s condition and what you can do about it</p>
|
|
|
|
<div class="legend">
|
|
<div class="legend-item"><div class="legend-dot" style="background:var(--c-observed);border-color:var(--c-observed-hi)"></div> Observed</div>
|
|
<div class="legend-item"><div class="legend-dot" style="background:var(--c-hidden);border-color:var(--c-hidden-hi)"></div> Hidden</div>
|
|
<div class="legend-item"><div class="legend-dot" style="background:var(--c-undetected);border-color:var(--c-undetected-hi)"></div> Undetected</div>
|
|
<div class="legend-item"><div class="legend-dot" style="background:var(--c-unnoticed);border-color:var(--c-unnoticed-hi)"></div> Unnoticed</div>
|
|
<div class="legend-item"><div class="legend-dot" style="background:#1a2840;border-color:#4080b0"></div> Action / Roll</div>
|
|
<div class="legend-item"><div class="legend-dot" style="background:#201828;border-color:#6040a0"></div> Decision</div>
|
|
</div>
|
|
|
|
<div class="diagram-wrap">
|
|
<div class="mermaid" id="detection-diagram">
|
|
%%{init: {'theme':'dark','themeVariables':{'background':'#161210','primaryColor':'#193a14','primaryTextColor':'#e8d5b0','primaryBorderColor':'#8b6914','lineColor':'#a08060','secondaryColor':'#3e2e06','tertiaryColor':'#1a2840','edgeLabelBackground':'#211a13','clusterBkg':'#211a13'}}}%%
|
|
flowchart TD
|
|
|
|
classDef obs fill:#193a14,stroke:#3a7c32,color:#b8e8b0
|
|
classDef hid fill:#3e2e06,stroke:#a07a10,color:#f0e060
|
|
classDef und fill:#3e1a06,stroke:#a04010,color:#f0a868
|
|
classDef unn fill:#300c0c,stroke:#8a1010,color:#f07070
|
|
classDef act fill:#1a2840,stroke:#4080b0,color:#90c0e8
|
|
classDef dec fill:#2a2010,stroke:#806040,color:#d0c0a0
|
|
|
|
START(["What can you perceive?"]):::dec
|
|
Q1["Can your precise sense detect it?"]:::dec
|
|
Q2["Do you know it is nearby?"]:::dec
|
|
Q3["Do you know which space?"]:::dec
|
|
|
|
OBS(["OBSERVED
|
|
Target normally
|
|
If concealed: DC 5 flat check"]):::obs
|
|
|
|
HID(["HIDDEN
|
|
You know the space
|
|
You are off-guard to it
|
|
DC 11 flat check to target"]):::hid
|
|
|
|
UND(["UNDETECTED
|
|
You do not know the space
|
|
You are off-guard to it
|
|
Guess a square - GM rolls secretly"]):::und
|
|
|
|
UNN(["UNNOTICED
|
|
No idea it exists
|
|
Cannot target at all"]):::unn
|
|
|
|
SEEK_HID["SEEK from Hidden
|
|
Perception vs Stealth DC
|
|
rolled secretly by GM"]:::act
|
|
|
|
SEEK_UND["SEEK from Undetected
|
|
Perception vs Stealth DC
|
|
rolled secretly by GM"]:::act
|
|
|
|
START --> Q1
|
|
Q1 -->|YES| OBS
|
|
Q1 -->|NO| Q2
|
|
Q2 -->|NO| UNN
|
|
Q2 -->|YES| Q3
|
|
Q3 -->|YES| HID
|
|
Q3 -->|NO| UND
|
|
|
|
HID -->|Use SEEK| SEEK_HID
|
|
UND -->|Use SEEK| SEEK_UND
|
|
|
|
SEEK_HID -->|Critical Success| OBS
|
|
SEEK_HID -->|Success precise sense| OBS
|
|
SEEK_HID -->|Success imprecise or invisible| HID
|
|
SEEK_HID -->|Failure| HID
|
|
|
|
SEEK_UND -->|Critical Success| OBS
|
|
SEEK_UND -->|Success| HID
|
|
SEEK_UND -->|Failure| UND
|
|
</div>
|
|
</div>
|
|
|
|
<div class="note-box">
|
|
<h3>Targeting Summary</h3>
|
|
<ul>
|
|
<li><strong>Observed:</strong> Attack normally. If also <em>concealed</em>, attempt a DC 5 flat check before rolling — fail means no effect.</li>
|
|
<li><strong>Hidden:</strong> Attempt a DC 11 flat check before rolling. On a fail, your actions (and spell slots, resources) are wasted. You remain off-guard to the creature regardless.</li>
|
|
<li><strong>Undetected:</strong> Declare a square to attack. The GM secretly rolls both the flat check and your attack roll. The GM will not say whether you missed due to the flat check, the attack roll, or choosing the wrong square.</li>
|
|
<li><strong>Unnoticed:</strong> You cannot target the creature directly. Area effects still work normally against undetected creatures.</li>
|
|
<li><strong>Invisible creature</strong> starts as <em>Hidden</em> if you were already observing it when it turned invisible (you last know where it was). It can then Sneak to become Undetected.</li>
|
|
<li><strong>Off-guard:</strong> When a creature is hidden or undetected from you, <em>you</em> are off-guard to <em>it</em> — you take a −2 circumstance penalty to your AC against its attacks. The hidden/undetected creature is not automatically off-guard to you.</li>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Simulator Tab -->
|
|
<div id="simulator" class="tab-panel active">
|
|
<h2>Situation Simulator</h2>
|
|
<p class="subtitle">Step through detection state changes interactively — choose an action to see what happens next</p>
|
|
|
|
<!-- Start-as state picker -->
|
|
<div class="sim-start-row">
|
|
<span class="sim-start-label">Start as:</span>
|
|
<button class="sim-chip observed" onclick="simSetState('OBSERVED')">Observed</button>
|
|
<button class="sim-chip hidden" onclick="simSetState('HIDDEN')">Hidden</button>
|
|
<button class="sim-chip undetected" onclick="simSetState('UNDETECTED')">Undetected</button>
|
|
<button class="sim-chip unnoticed" onclick="simSetState('UNNOTICED')">Unnoticed</button>
|
|
</div>
|
|
|
|
<!-- Current state display -->
|
|
<div id="sim-current-box" class="sim-box observed">
|
|
<div class="sim-box-label">Current State</div>
|
|
<div class="sim-state-name" id="sim-state-name">Observed</div>
|
|
<div class="sim-state-effects" id="sim-state-effects"></div>
|
|
</div>
|
|
|
|
<!-- Action buttons -->
|
|
<div id="sim-actions-section">
|
|
<div class="sim-section-head">What happens?</div>
|
|
<div class="sim-actions-list" id="sim-actions-list"></div>
|
|
</div>
|
|
|
|
<!-- Roll outcomes (shown when action needs a roll) -->
|
|
<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>
|
|
|
|
<!-- Result of last transition -->
|
|
<div id="sim-result-container"></div>
|
|
|
|
<!-- History trail + Reset -->
|
|
<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 in #' + id + ':\n' + (e.message || e) + '</pre>';
|
|
}
|
|
}
|
|
simSetState('OBSERVED');
|
|
});
|
|
|
|
function showTab(name, btn) {
|
|
document.querySelectorAll('.tab-panel').forEach(p => p.classList.remove('active'));
|
|
document.querySelectorAll('.tab-btn').forEach(b => b.classList.remove('active'));
|
|
document.getElementById(name).classList.add('active');
|
|
btn.classList.add('active');
|
|
}
|
|
|
|
// ── Situation Simulator ─────────────────────────────────────
|
|
|
|
const SIM_STATES = {
|
|
OBSERVED: {
|
|
label: 'Observed',
|
|
cssClass: 'observed',
|
|
effects: 'Opponents perceive you clearly. You can be targeted normally. If also <strong>Concealed</strong>, they need a DC 5 flat check.',
|
|
},
|
|
HIDDEN: {
|
|
label: 'Hidden',
|
|
cssClass: 'hidden',
|
|
effects: 'Opponents know your general location but not your exact square. You are <strong>off-guard</strong> to them. They need a DC 11 flat check to affect you.',
|
|
},
|
|
UNDETECTED: {
|
|
label: 'Undetected',
|
|
cssClass: 'undetected',
|
|
effects: 'No opponent knows which square you occupy. You are <strong>off-guard</strong> to them. They must guess a square; the GM rolls secretly.',
|
|
},
|
|
UNNOTICED: {
|
|
label: 'Unnoticed',
|
|
cssClass: 'unnoticed',
|
|
effects: 'No opponent even knows you exist nearby. You cannot be targeted at all.',
|
|
},
|
|
};
|
|
|
|
const SIM_ACTIONS = {
|
|
OBSERVED: [
|
|
{
|
|
id: 'hide',
|
|
name: 'Use HIDE',
|
|
hint: 'Requires cover or concealment',
|
|
needsRoll: true,
|
|
rollLabel: 'HIDE — select the outcome (Stealth vs Perception DC):',
|
|
outcomes: [
|
|
{ label: '✓ Success', newState: 'HIDDEN', msg: 'You slip out of sight. Opponents know your general area but not your exact square.' },
|
|
{ label: '✗ Failure', newState: 'OBSERVED', msg: 'You fail to conceal yourself. You remain fully observed.' },
|
|
],
|
|
},
|
|
],
|
|
HIDDEN: [
|
|
{
|
|
id: 'sneak',
|
|
name: 'Use SNEAK',
|
|
hint: 'Move ≤ half Speed, cover at start & end',
|
|
needsRoll: true,
|
|
rollLabel: 'SNEAK — select the outcome (Stealth vs Perception DC):',
|
|
outcomes: [
|
|
{ label: '✓ Success', newState: 'UNDETECTED', msg: 'You move silently. No opponent knows your new location.' },
|
|
{ label: '✗ Failure', newState: 'HIDDEN', msg: 'You make some noise but stay in the same known area. You remain hidden.' },
|
|
{ label: '✗✗ Crit Fail', newState: 'OBSERVED', msg: 'You give away your position. You are spotted! (If invisible, you become Hidden instead.)' },
|
|
],
|
|
},
|
|
{
|
|
id: 'seek',
|
|
name: 'Someone SEEKs me',
|
|
hint: 'Opponent rolls Perception vs your Stealth DC',
|
|
needsRoll: true,
|
|
rollLabel: 'SEEK — select the outcome (their Perception vs your Stealth DC):',
|
|
outcomes: [
|
|
{ label: '✓✓ Crit Success', newState: 'OBSERVED', msg: 'The seeker pinpoints you precisely with a precise sense. You are now fully observed.' },
|
|
{ label: '✓ Success (precise sense)', newState: 'OBSERVED', msg: 'The seeker locates you precisely. You are observed.' },
|
|
{ label: '✓ Success (imprecise / you\'re invisible)', newState: 'HIDDEN', msg: 'The seeker narrows down your area but cannot see you precisely. You remain hidden.' },
|
|
{ label: '✗ Failure', newState: 'HIDDEN', msg: 'The seeker finds nothing new. You remain hidden.' },
|
|
],
|
|
},
|
|
{
|
|
id: 'nonstealthy',
|
|
name: 'Non-stealthy action / Strike',
|
|
hint: 'Attack, cast a spell, or act openly',
|
|
needsRoll: false,
|
|
newState: 'OBSERVED',
|
|
msg: 'Acting openly reveals your position. You are now observed.',
|
|
},
|
|
{
|
|
id: 'step',
|
|
name: 'STEP with cover / concealment',
|
|
hint: 'One square while remaining covered',
|
|
needsRoll: false,
|
|
newState: 'HIDDEN',
|
|
msg: 'You step carefully while staying concealed. You remain hidden.',
|
|
},
|
|
{
|
|
id: 'unobtrusive',
|
|
name: 'Unobtrusive action',
|
|
hint: 'Draw an item, open a door — GM may call for Stealth',
|
|
needsRoll: false,
|
|
newState: 'HIDDEN',
|
|
msg: 'You act quietly. You remain hidden.',
|
|
},
|
|
],
|
|
UNDETECTED: [
|
|
{
|
|
id: 'sneak',
|
|
name: 'Use SNEAK',
|
|
hint: 'Move ≤ half Speed, cover at start & end',
|
|
needsRoll: true,
|
|
rollLabel: 'SNEAK — select the outcome (Stealth vs Perception DC):',
|
|
outcomes: [
|
|
{ label: '✓ Success', newState: 'UNDETECTED', msg: 'You move silently. You remain undetected.' },
|
|
{ label: '✗ Failure', newState: 'HIDDEN', msg: 'You make some noise. Opponents now know your general area but not your exact square.' },
|
|
{ label: '✗✗ Crit Fail', newState: 'OBSERVED', msg: 'You completely give away your position. You are spotted! (If invisible, you become Hidden instead.)' },
|
|
],
|
|
},
|
|
{
|
|
id: 'seek',
|
|
name: 'Someone SEEKs me',
|
|
hint: 'Opponent rolls Perception vs your Stealth DC (GM rolls secretly)',
|
|
needsRoll: true,
|
|
rollLabel: 'SEEK — select the outcome (their Perception vs your Stealth DC):',
|
|
outcomes: [
|
|
{ label: '✓✓ Crit Success', newState: 'OBSERVED', msg: 'The seeker pinpoints you precisely. You are now fully observed.' },
|
|
{ label: '✓ Success', newState: 'HIDDEN', msg: 'The seeker narrows down your location. You are now hidden — they know the general area.' },
|
|
{ label: '✗ Failure', newState: 'UNDETECTED', msg: 'The seeker fails to find you. You remain undetected.' },
|
|
],
|
|
},
|
|
{
|
|
id: 'speak',
|
|
name: 'Speak or make a loud noise',
|
|
hint: 'Anything that reveals your presence',
|
|
needsRoll: false,
|
|
newState: 'OBSERVED',
|
|
msg: 'Sound reveals your location. You are now observed.',
|
|
},
|
|
],
|
|
UNNOTICED: [
|
|
{
|
|
id: 'seek',
|
|
name: 'Someone SEEKs the area',
|
|
hint: 'Rare — seeker sweeps without knowing you\'re there',
|
|
needsRoll: true,
|
|
rollLabel: 'SEEK — select the outcome (their Perception vs your Stealth DC):',
|
|
outcomes: [
|
|
{ label: '✓✓ Crit Success', newState: 'OBSERVED', msg: 'The seeker detects you completely. You are now observed.' },
|
|
{ label: '✓ Success', newState: 'HIDDEN', msg: 'The seeker notices something. You are now hidden — they know the general area.' },
|
|
{ label: '✗ Failure', newState: 'UNNOTICED', msg: 'The seeker notices nothing unusual. You remain unnoticed.' },
|
|
],
|
|
},
|
|
{
|
|
id: 'speak',
|
|
name: 'Speak or make a loud noise',
|
|
hint: 'Anything that reveals your existence',
|
|
needsRoll: false,
|
|
newState: 'OBSERVED',
|
|
msg: 'Sound reveals your presence. You are now observed.',
|
|
},
|
|
{
|
|
id: 'nonstealthy',
|
|
name: 'Non-stealthy action',
|
|
hint: 'Attack, cast a spell, or act openly',
|
|
needsRoll: false,
|
|
newState: 'OBSERVED',
|
|
msg: 'Acting openly reveals you. You are now observed.',
|
|
},
|
|
],
|
|
};
|
|
|
|
let simCurrentState = 'OBSERVED';
|
|
let simHistory = [];
|
|
let simPendingAction = null;
|
|
|
|
function simSetState(state) {
|
|
simCurrentState = state;
|
|
simHistory = [{ state }];
|
|
simPendingAction = null;
|
|
simRender();
|
|
}
|
|
|
|
function simReset() { simSetState('OBSERVED'); }
|
|
|
|
function toggleSidebar() {
|
|
const sidebar = document.getElementById('sidebar');
|
|
const overlay = document.getElementById('sidebar-overlay');
|
|
sidebar.classList.toggle('open');
|
|
overlay.classList.toggle('active');
|
|
}
|
|
|
|
function closeSidebar() {
|
|
document.getElementById('sidebar').classList.remove('open');
|
|
document.getElementById('sidebar-overlay').classList.remove('active');
|
|
}
|
|
|
|
function simSelectAction(id) {
|
|
const action = SIM_ACTIONS[simCurrentState].find(a => a.id === id);
|
|
if (!action) return;
|
|
simPendingAction = action;
|
|
document.getElementById('sim-result-container').innerHTML = '';
|
|
|
|
if (!action.needsRoll) {
|
|
simApplyTransition(action.newState, action.name, action.msg);
|
|
} else {
|
|
simRenderActionHighlight(id);
|
|
simRenderOutcomes(action);
|
|
}
|
|
}
|
|
|
|
function simSelectOutcome(idx) {
|
|
if (!simPendingAction) return;
|
|
const o = simPendingAction.outcomes[idx];
|
|
simApplyTransition(o.newState, simPendingAction.name + ' \u2014 ' + o.label.replace(/&#x[\da-f]+;/gi, s => { const t = document.createElement('textarea'); t.innerHTML = s; return t.value; }), o.msg);
|
|
}
|
|
|
|
function simApplyTransition(newState, actionLabel, msg) {
|
|
simCurrentState = newState;
|
|
simHistory.push({ state: newState, via: actionLabel });
|
|
simPendingAction = null;
|
|
simRenderWithResult(newState, msg);
|
|
}
|
|
|
|
function simRender() {
|
|
simRenderStateBox();
|
|
simRenderActions();
|
|
document.getElementById('sim-outcomes-section').style.display = 'none';
|
|
document.getElementById('sim-result-container').innerHTML = '';
|
|
simRenderHistory();
|
|
}
|
|
|
|
function simRenderWithResult(newState, msg) {
|
|
simRenderStateBox();
|
|
simRenderActions();
|
|
document.getElementById('sim-outcomes-section').style.display = 'none';
|
|
|
|
const info = SIM_STATES[newState];
|
|
document.getElementById('sim-result-container').innerHTML =
|
|
'<div class="sim-result-box ' + info.cssClass + '">' +
|
|
'<div class="sim-result-top">' +
|
|
'<span style="color:var(--text-muted);font-size:1.1rem;">\u2192</span>' +
|
|
'<span class="sim-result-new-state">' + info.label + '</span>' +
|
|
'</div>' +
|
|
'<div class="sim-result-msg">' + msg + '</div>' +
|
|
'</div>';
|
|
|
|
simRenderHistory();
|
|
}
|
|
|
|
function simRenderStateBox() {
|
|
const info = SIM_STATES[simCurrentState];
|
|
const box = document.getElementById('sim-current-box');
|
|
box.className = 'sim-box ' + info.cssClass;
|
|
document.getElementById('sim-state-name').textContent = info.label;
|
|
document.getElementById('sim-state-effects').innerHTML = info.effects;
|
|
}
|
|
|
|
function simRenderActions() {
|
|
const actions = SIM_ACTIONS[simCurrentState];
|
|
document.getElementById('sim-actions-list').innerHTML = actions.map(a =>
|
|
'<button class="sim-action" id="sim-act-' + a.id + '" onclick="simSelectAction(\'' + a.id + '\')">' +
|
|
'<div class="sim-action-name">' + a.name + '</div>' +
|
|
'<div class="sim-action-hint">' + a.hint + '</div>' +
|
|
'</button>'
|
|
).join('');
|
|
}
|
|
|
|
function simRenderActionHighlight(id) {
|
|
document.querySelectorAll('.sim-action').forEach(b => b.classList.remove('selected'));
|
|
const btn = document.getElementById('sim-act-' + id);
|
|
if (btn) btn.classList.add('selected');
|
|
}
|
|
|
|
function simRenderOutcomes(action) {
|
|
const section = document.getElementById('sim-outcomes-section');
|
|
section.style.display = 'block';
|
|
document.getElementById('sim-outcomes-head').innerHTML = action.rollLabel;
|
|
document.getElementById('sim-outcomes-list').innerHTML = action.outcomes.map((o, i) =>
|
|
'<button class="sim-outcome" onclick="simSelectOutcome(' + i + ')">' + o.label + '</button>'
|
|
).join('');
|
|
}
|
|
|
|
function simRenderHistory() {
|
|
const trail = document.getElementById('sim-history-trail');
|
|
let html = '';
|
|
simHistory.forEach((entry, i) => {
|
|
const info = SIM_STATES[entry.state];
|
|
if (i > 0 && entry.via) {
|
|
const shortVia = entry.via.split('\u2014')[0].trim();
|
|
html += '<span class="sim-hist-via">\u2192 ' + shortVia + ' \u2192</span> ';
|
|
}
|
|
html += '<span class="sim-hist-state ' + info.cssClass + '">' + info.label + '</span> ';
|
|
});
|
|
trail.innerHTML = html || '<span style="color:var(--text-muted);font-style:italic;font-size:0.78rem;">No actions yet</span>';
|
|
}
|
|
|
|
</script>
|
|
</body>
|
|
</html>
|