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.

549 lines
19 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 &amp; Stealth</title>
<script src="https://cdn.jsdelivr.net/npm/mermaid@10/dist/mermaid.min.js"></script>
<style>
:root {
--bg: #161210;
--surface: #211a13;
--surface2: #2e2318;
--border: #7a5c18;
--text: #e8d5b0;
--text-muted: #9a7e56;
--accent: #c89030;
--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;
}
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
body {
background: var(--bg);
color: var(--text);
font-family: Georgia, 'Times New Roman', serif;
min-height: 100vh;
display: flex;
flex-direction: column;
}
/* ── Header ── */
header {
background: var(--surface);
border-bottom: 2px solid var(--border);
padding: 14px 24px;
display: flex;
align-items: baseline;
gap: 16px;
flex-shrink: 0;
}
header h1 {
font-size: 1.5rem;
color: var(--accent);
letter-spacing: 0.06em;
}
header p {
color: var(--text-muted);
font-size: 0.85rem;
font-style: italic;
}
/* ── Body layout ── */
.layout {
display: flex;
flex: 1;
overflow: hidden;
}
/* ── Sidebar ── */
aside {
width: 270px;
min-width: 270px;
background: var(--surface);
border-right: 1px solid var(--border);
overflow-y: auto;
padding: 16px 14px;
}
aside h2 {
color: var(--accent);
font-size: 0.75rem;
text-transform: uppercase;
letter-spacing: 0.12em;
margin-bottom: 12px;
}
.ccard {
border-radius: 5px;
padding: 10px 12px;
margin-bottom: 9px;
border: 1px solid;
}
.ccard h3 {
font-size: 0.8rem;
text-transform: uppercase;
letter-spacing: 0.07em;
margin-bottom: 5px;
}
.ccard p {
font-size: 0.76rem;
line-height: 1.55;
color: var(--text);
}
.ccard .tag {
display: inline-block;
margin-top: 6px;
font-size: 0.72rem;
font-weight: bold;
padding: 2px 7px;
border-radius: 3px;
}
.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); }
.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); }
/* ── Main content ── */
main {
flex: 1;
overflow: auto;
padding: 20px 24px;
}
/* ── Tabs ── */
.tab-bar {
display: flex;
gap: 6px;
margin-bottom: 0;
}
.tab-btn {
padding: 8px 22px;
background: var(--surface2);
border: 1px solid var(--border);
border-bottom-color: var(--border);
color: var(--text-muted);
cursor: pointer;
font-family: inherit;
font-size: 0.9rem;
border-radius: 6px 6px 0 0;
transition: color 0.15s, background 0.15s;
}
.tab-btn.active {
background: var(--surface);
color: var(--accent);
border-color: var(--accent);
border-bottom-color: var(--surface);
position: relative;
z-index: 1;
}
.tab-btn:hover:not(.active) { color: var(--text); }
.tab-panel {
display: none;
background: var(--surface);
border: 1px solid var(--accent);
border-radius: 0 6px 6px 6px;
padding: 24px;
}
.tab-panel.active { display: block; }
.tab-panel h2 {
color: var(--accent);
font-size: 1.1rem;
margin-bottom: 6px;
}
.tab-panel .subtitle {
color: var(--text-muted);
font-size: 0.82rem;
font-style: italic;
margin-bottom: 22px;
}
/* ── Mermaid container ── */
.diagram-wrap {
overflow-x: auto;
padding: 10px 0;
}
.diagram-wrap .mermaid {
display: flex;
justify-content: center;
min-width: 600px;
}
/* legend strip */
.legend {
display: flex;
flex-wrap: wrap;
gap: 10px;
margin-bottom: 18px;
font-size: 0.77rem;
}
.legend-item {
display: flex;
align-items: center;
gap: 6px;
}
.legend-dot {
width: 13px;
height: 13px;
border-radius: 50%;
border: 1px solid;
flex-shrink: 0;
}
.note-box {
background: var(--surface2);
border: 1px solid var(--border);
border-radius: 5px;
padding: 12px 16px;
margin-top: 20px;
font-size: 0.8rem;
line-height: 1.7;
color: var(--text);
}
.note-box h3 {
color: var(--accent);
font-size: 0.78rem;
text-transform: uppercase;
letter-spacing: 0.1em;
margin-bottom: 8px;
}
.note-box ul {
padding-left: 16px;
}
.note-box li { margin-bottom: 4px; }
.note-box strong { color: var(--accent); }
</style>
</head>
<body>
<header>
<h1>PF2e Perception &amp; Stealth</h1>
<p>Flow of Control Reference &mdash; Player Core</p>
</header>
<div class="layout">
<!-- ── Sidebar ── -->
<aside>
<h2>Detection Conditions</h2>
<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&rsquo;s space but can&rsquo;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&rsquo;t know which space it&rsquo;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 &mdash; 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>
</aside>
<!-- ── Main ── -->
<main>
<div class="tab-bar">
<button class="tab-btn active" onclick="showTab('stealth', this)">Stealth Actions</button>
<button class="tab-btn" onclick="showTab('detection', this)">Detection &amp; Targeting</button>
</div>
<!-- Stealth Tab -->
<div id="stealth" class="tab-panel active">
<h2>Stealth Actions Flow</h2>
<p class="subtitle">From the <strong>stealther&rsquo;s</strong> perspective &mdash; 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 &amp; Targeting Flow</h2>
<p class="subtitle">From the <strong>detector&rsquo;s</strong> perspective &mdash; how to determine a creature&rsquo;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&nbsp;5 flat check before rolling — fail means no effect.</li>
<li><strong>Hidden:</strong> Attempt a DC&nbsp;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 &minus;2 circumstance penalty to your AC against its attacks. The hidden/undetected creature is not automatically off-guard to you.</li>
</ul>
</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>';
}
}
});
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');
}
</script>
</body>
</html>