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
+
+
+ + +
+
What happens?
+
+
+ + + + + +
+ + +
+
+
History
+
+
+ +
+
@@ -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(/&#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 = + '
' + + '
' + + '\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'; + } +