diff --git a/index.html b/index.html
index 5ec44f0..f6255db 100644
--- a/index.html
+++ b/index.html
@@ -274,6 +274,209 @@
}
.note-box li { margin-bottom: 4px; }
.note-box strong { color: var(--accent); }
+
+ /* ── Simulator Tab ── */
+ .sim-start-row {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ margin-bottom: 20px;
+ flex-wrap: wrap;
+ }
+ .sim-start-label {
+ font-size: 0.78rem;
+ text-transform: uppercase;
+ letter-spacing: 0.08em;
+ color: var(--text-muted);
+ }
+ .sim-chip {
+ padding: 5px 14px;
+ border-radius: 20px;
+ border: 1px solid;
+ background: transparent;
+ cursor: pointer;
+ font-family: inherit;
+ font-size: 0.78rem;
+ font-weight: bold;
+ letter-spacing: 0.05em;
+ transition: opacity 0.15s;
+ }
+ .sim-chip:hover { opacity: 0.7; }
+ .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 {
+ border-radius: 7px;
+ border: 2px solid;
+ padding: 18px 22px;
+ margin-bottom: 22px;
+ transition: background 0.25s, border-color 0.25s;
+ }
+ .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-label {
+ font-size: 0.7rem;
+ text-transform: uppercase;
+ letter-spacing: 0.12em;
+ color: var(--text-muted);
+ margin-bottom: 4px;
+ }
+ .sim-state-name {
+ font-size: 1.7rem;
+ font-weight: bold;
+ letter-spacing: 0.06em;
+ text-transform: uppercase;
+ margin-bottom: 6px;
+ }
+ .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-state-effects {
+ font-size: 0.8rem;
+ color: var(--text);
+ line-height: 1.6;
+ }
+
+ .sim-section-head {
+ font-size: 0.72rem;
+ text-transform: uppercase;
+ letter-spacing: 0.1em;
+ color: var(--text-muted);
+ margin-bottom: 10px;
+ }
+ .sim-actions-list {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 8px;
+ margin-bottom: 22px;
+ }
+ .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-action:hover { border-color: var(--accent); color: var(--accent); }
+ .sim-action.selected { border-color: var(--accent); background: #1e1608; color: var(--accent); }
+ .sim-action-name { font-weight: bold; margin-bottom: 3px; }
+ .sim-action-hint { font-size: 0.7rem; color: var(--text-muted); }
+ .sim-action.selected .sim-action-hint { color: #906c38; }
+
+ .sim-outcomes-box {
+ background: var(--surface2);
+ border: 1px solid var(--border);
+ border-radius: 6px;
+ padding: 14px 18px;
+ margin-bottom: 22px;
+ }
+ .sim-outcomes-head {
+ font-size: 0.78rem;
+ color: var(--text-muted);
+ margin-bottom: 10px;
+ }
+ .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-outcome:hover { border-color: var(--accent); color: var(--accent); }
+
+ .sim-result-box {
+ border-radius: 6px;
+ border: 1px solid;
+ padding: 14px 18px;
+ margin-bottom: 22px;
+ animation: simFadeIn 0.25s ease;
+ }
+ @keyframes simFadeIn {
+ from { opacity: 0; transform: translateY(5px); }
+ to { opacity: 1; transform: translateY(0); }
+ }
+ .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-top { display: flex; align-items: center; gap: 10px; margin-bottom: 6px; }
+ .sim-result-new-state {
+ font-size: 1.05rem;
+ font-weight: bold;
+ text-transform: uppercase;
+ letter-spacing: 0.06em;
+ }
+ .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-result-msg { font-size: 0.8rem; color: var(--text); line-height: 1.6; }
+
+ .sim-bottom-row {
+ display: flex;
+ align-items: flex-start;
+ justify-content: space-between;
+ gap: 16px;
+ flex-wrap: wrap;
+ margin-top: 4px;
+ }
+ .sim-history-wrap { flex: 1; min-width: 0; }
+ .sim-history-label {
+ font-size: 0.7rem;
+ text-transform: uppercase;
+ letter-spacing: 0.1em;
+ color: var(--text-muted);
+ margin-bottom: 6px;
+ }
+ .sim-history-trail {
+ display: flex;
+ flex-wrap: wrap;
+ align-items: center;
+ gap: 4px;
+ font-size: 0.78rem;
+ min-height: 1.4em;
+ }
+ .sim-hist-state { font-weight: bold; }
+ .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); }
+ .sim-hist-via { color: var(--text-muted); font-size: 0.7rem; font-style: italic; }
+
+ .sim-reset-btn {
+ padding: 7px 18px;
+ background: var(--surface2);
+ border: 1px solid var(--border);
+ color: var(--text-muted);
+ border-radius: 4px;
+ cursor: pointer;
+ font-family: inherit;
+ font-size: 0.8rem;
+ white-space: nowrap;
+ transition: color 0.15s, border-color 0.15s;
+ }
+ .sim-reset-btn:hover { color: var(--text); border-color: var(--text-muted); }
@@ -337,6 +540,7 @@
+
@@ -511,6 +715,54 @@ flowchart TD
+
+
+
+
Situation Simulator
+
Step through detection state changes interactively — choose an action to see what happens next
+
+
+
+ Start as:
+
+
+
+
+
+
+
+
+
Current State
+
Observed
+
+
+
+
+
+
+
+
+
+
Select the outcome:
+
+
+
+
+
+
+
+
+
+
@@ -536,6 +788,7 @@ flowchart TD
'Diagram error in #' + id + ':\n' + (e.message || e) + '';
}
}
+ simSetState('OBSERVED');
});
function showTab(name, btn) {
@@ -544,6 +797,276 @@ flowchart TD
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 Concealed, 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 off-guard 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 off-guard 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 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(/[\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 =
+ '' +
+ '
' +
+ '\u2192' +
+ '' + info.label + '' +
+ '
' +
+ '
' + msg + '
' +
+ '
';
+
+ 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 =>
+ ''
+ ).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) =>
+ ''
+ ).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 += '\u2192 ' + shortVia + ' \u2192 ';
+ }
+ html += '' + info.label + ' ';
+ });
+ trail.innerHTML = html || 'No actions yet';
+ }
+