Skip to content

Stealth & Visibility — Gameplay Analysis

Status: Phases A + B shipped (echo-sense revived; gradual per-enemy detection + legibility). Phases C–D pending. Scope: the limited-visibility + stealth loop — fog of war, enemy perception, detection, route-planning, and the stealth-kill payoff. The question this doc answers: does the game currently let you read the field, plan a route, and stealth-kill enemies — and if not, why? Lens: casual mobile first (short sessions, touch, low micromanagement, readable at a glance), while preserving the Route-over-reflexes / Hide-and-kill pillars. Companion reading: PROJECT_CONTEXT.md (§2 pillars, §3 vision rules, §4 controls) and loot-inventory-redesign.md.


1. Why this doc exists

The design pillars (PROJECT_CONTEXT §2) put stealth at the center:

Hide-and-kill · Route over reflexes — "the skill is where you go… skirting enemy vision cones, striking only when safe."

For that to be the skill, three things must all be true at once:

  1. You can perceive the field beyond your own body — where enemies are, which way they face, where their cones sweep — far enough ahead to make a plan.
  2. Detection is legible and gradual — you can tell when you're safe, when you're risking it, and when you've been made, with enough warning to react (back off, duck, reroute).
  3. Acting on the plan is low-friction — you can commit to an approach and execute the stealth kill without fighting the controls.

This doc traces how the systems behave today, names what's strong (and must be preserved), and isolates the structural weaknesses blocking the stealth fantasy — the biggest of which is that the feature meant to deliver pillar #1 is currently dead code.


2. The reference landscape — what each touchstone teaches

The project already names its references; this maps each onto the stealth/visibility problem specifically, plus two genre touchstones worth borrowing from.

Reference What it nails Lesson for us
Arc Raiders (primary) PvEvP tension; enemies you hear before you see; sound as the core information currency; commit-to-extract risk. Information should arrive in layers — heard → glimpsed → seen. Noise is the first layer, not a side-system.
Hunter Assassin (controls) Tap-to-move stealth where the entire game is reading cones and timing a dash to a kill. Crucially: you can see every enemy and every cone on the board — the fog is short, but the threats are legible. The control scheme is right. But Hunter Assassin's stealth works because threats are always readable. Fog that hides the threats removes the puzzle.
Bullet Echo (fog option) Limited vision sphere + footstep/sound blips that reveal off-screen enemies as directional pings. You're never fully blind — you sense. Limited vision must be paired with a sensing layer, or it's just blindness. This is exactly our missing piece.
Zero Sievert (extraction) Fog + flashlight cone; enemies render as you sweep them; tension from not knowing what's past the treeline. Reveal-on-sweep is fine, but the player needs a reason to believe something is there (sound, tracks) so exploration is informed, not random.
Survivor.io (meta) Build-craft that visibly changes how a run plays. Stealth stats (detectability, sense range) should be buildable and felt, the way Stalker already hints at.
Commandos / Shadow Tactics (genre, not yet named) The green/red vision cone as a first-class, always-visible UI object; a gradual detection state (white → yellow "?" → red "!"); "shadow" tiles you route through. Cones and detection are UI, drawn always, not just when convenient. Detection has a middle state that creates the "back off in time" decision.
Mark of the Ninja (genre) Best-in-class stealth readability: your own sound radius is drawn as a ring; line-of-sight is explicit; you always know if you're hidden. Make the player's own footprint legible — show the noise you make and whether you're currently concealed.

Synthesis: every successful limited-vision stealth game pairs the fog with a sensing layer and a gradual, always-drawn detection read. Our design intends both (the how-to-play text promises echo-sense; the alert variable exists) but ships neither to the player. That gap is the whole problem.


3. How the systems actually work today

Traced from game.js / config.js.

System Implementation Anchors
Player vision / fog A visibility polygon (DDA raycast, 480 rays) clipped to a 300px radius and walls; redrawn when the player moves >4px. Terrain is always visible; the fog only gates actors/loot. drawFog, inVision, CFG.fog.radius
What reveals an enemy isVisibleToPlayer(en) → within fog.radius, line-of-sight clear (closed gate blocks), and not idle-in-a-bush past a cutoff. Sets en.lit. isVisibleToPlayer
Enemy perception perceive(en,tgt) → distance ≤ visR (bush shrinks it to ×0.36), inside the facing cone visA (ignored once alert≥1), LOS clear, and you're not in dodge i-frames. perceive
Detection / alert Binary-ish: on a successful perceive, en.alert = 1 instantly. Decays at 0.15/s when nothing is seen; a chasing mob floors it at 0.55. updateMob, CFG.combat.mobAlertDecay
Pack / noise spread A spotted mob broadcasts alert to others within radius every 1.2s; noises (shots, chests, deaths) push idle mobs to investigate a point. Backstabs are near-silent. alertPack, makeNoise
Stealth tools One: bushes (shrink detect radius, conceal while not alerted, +10% move perk). Plus Stalker set (−30% detectability) and dodge i-frame invuln window. CFG.stealth, setBonuses
Stealth kill Tap a currently-lit enemy → auto-path to it → melee; behind/side arc (≥0.55π from facing) = backstab crit, bypasses armor + Power check, silent. onTap, doMelee
Sensing layer (intended) drawSilhouette() — pulsing sonar blips + ping rings + ❗ for unseen mobs. Defined but never called. drawSilhouette (dead)
Route-planning UI Full-screen tactical map (tap to set a route). Enemies appear only if lit — unless devReveal (a debug flag, currently defaulted on) shows all. drawBigMap, devReveal

One-line summary: the enemy-side of stealth is rich and well-tuned (cones, graduated detection radius, packs, noise investigation). The player-side — perceiving threats and reading your own detection state — is mostly absent or switched off, so the careful route-planning the pillars promise has nothing to plan against.


4. Strengths — what's working and must be preserved

These are genuinely good and should survive any redesign:

  1. The enemy perception model is excellent. Directional cones (visA), distance falloff, LOS occlusion, bush detect-reduction, and the "cone gates initial detection but a fully alerted mob tracks by awareness" rule (perceive line ~1347) is a sophisticated, correct stealth model. The simulation is there.
  2. Noise is a real, systemic information economy. Shots/chests/deaths propagate with distinct radii, draw mobs to investigate, and backstabs are silent — exactly the Arc Raiders "sound is everything" lesson. This is a standout system.
  3. The backstab payoff is clean and readable. Behind/side arc → crit that bypasses armor and the Power check means stealth genuinely beats brute force, as designed. The rear weak-point marker + lock-on reticle make the execution legible.
  4. Layered concealment rules are thoughtful. Bushes conceal only while unalerted; an enemy in a fight won't vanish; close range sees through cover. These edge-cases prevent the cheap "hide mid-combat" exploit and show real stealth-design maturity.
  5. The fog itself is high-quality. Exact wall-face raycasting, distance-dimmed walls — it looks like a tense limited-vision game. The aesthetic promise is met.
  6. The pacing scaffold exists. Safe spawn → patrols drift in → restlessness at 7:00 → storm finale. The tension ramp is built; stealth just needs to be the verb that rides it.

Preserve: the perception simulation, the noise economy, the silent-backstab payoff, the concealment edge-cases. The fixes below add player-facing legibility on top of these — they do not touch the underlying model.


5. Weaknesses — the structural blockers

Ordered by impact on the core "plan a route, stealth-kill" loop.

W1 — You cannot perceive enemies beyond your own bubble ~~(critical)~~ ✅ FIXED (Phase A)

Resolved. drawSilhouette() is now called for echo-sensed mobs within senseRadius (see Phase A in §7). The original finding is kept below for context.

drawSilhouette() — the echo-sense sonar blips the how-to-play and PROJECT_CONTEXT both promise — was never called. The render loop only drew actors with en.lit (in direct sight, ≤300px, LOS clear). Outside that ~300px circle the field was truly blank.

Consequence: route-planning is impossible in principle. You can't "skirt a vision cone" you can't see; you discover enemies by walking into their sight range — which is often the same moment they discover you. The pillar verb ("where you go") has no information to act on. The fog stopped being tension and became blindness.

Severity is amplified because the fix is nearly free — the feature is written, just disconnected. See R1.

W2 — Detection is effectively binary; there is no "back off in time" window ~~(critical)~~ ✅ FIXED (Phase B)

Resolved. alert now fills over detectTime while you're in a creature's cone, with a suspect middle state (turns/creeps toward you, holds fire) before a full spot. See §7 Phase B.

alert jumped from 0 → 1 instantly on a successful perceive. There was no gradual fill, so there was no middle state where you're being noticed but not yet caught and can retreat. Every detection was a hard, immediate commit to combat.

Consequence: stealth has no tension curve and no counterplay. Commandos/MGS/Mark of the Ninja all live in the yellow "?" middle state — that's where the skill expression is. Without it, stealth is pass/fail coin-flips at the cone boundary, which reads as unfair on mobile (you got spotted "for no reason" because you couldn't see the cone — see W1).

W3 — You can only target/approach enemies you can already see (high)

onTap filters enemies by seenByPlayer (line ~744). You cannot tap a sensed-but-unlit enemy to queue an approach. Combined with W1, there's no way to say "go stealth-kill that one over there" — the very fantasy the controls were designed for (Hunter Assassin).

W4 — Your own detection state is not legible ~~(high)~~ ✅ FIXED (Phase B)

Resolved. A "being noticed" screen vignette now rises with G.playerDetect (amber while a creature fills on you, red + pulse once fully spotted), and each noticing mob shows a fill arc over its head. See §7 Phase B.

There was no "you are being seen / about to be seen" read. Cones were only drawn for lit enemies, and the cone is the enemy's, never projected onto you. The only self-state tell was the 🌿 "hidden" glyph in a bush. The player couldn't answer the most basic stealth question: am I safe right now?

W5 — Stealth has exactly one spatial tool: bushes (medium)

Concealment = bushes (plus the Stalker set and dodge i-frames). There's no light/shadow, no peeking/waiting verb, no distraction/lure to manipulate patrols. Route-planning is "is there a bush between me and it?" — thin for a game whose headline skill is routing.

W6 — The noise economy is invisible to the player as a planning tool (medium)

Noise drives enemy behavior beautifully, but the player only sees sonar rings for sounds others make. You can't see your own noise radius, and loud actions (cracking a chest, firing) aren't telegraphed as "this will be heard that far." Mark of the Ninja's own-sound ring is the model. Right now noise is a system that happens to you, not one you route around.

W7 — The tactical map leans on a debug cheat for legibility (medium)

The big map only shows lit enemies — except devReveal (a developer flag) currently defaults on and reveals everything. So the only reason the map is useful for planning is a cheat. With it off (shipping state), the map inherits W1's blindness. Planning needs a legitimate intel layer (sensed blips, last-known-position ghosts).

W8 — Minor friction & polish

  • Reveal-on-approach has no breadcrumb. Nothing hints "something is past that wall," so exploration is blind rather than informed (Zero Sievert gives you the treeline + sound).
  • No last-known-position memory. When an enemy de-lits you forget it existed; real stealth UIs leave a ghost marker at its last seen spot.
  • Backstab arc isn't previewed on approach. You learn if your angle worked only on contact; showing the rear arc on the targeted enemy would let you plan the side you strike from.

5.1 Root-cause map (symptom → cause)

  • "I get spotted out of nowhere" ← W1 (can't see the threat) + W2 (no gradual warning) + W4 (can't read own state). These three compound into the single worst feel-problem.
  • "There's nothing to plan" ← W1 (no intel) + W3 (can't act on intel) + W7 (map needs a cheat) + W5 (one tool).
  • "Stealth feels shallow" ← W2 (binary detection, no skill middle) + W5 (one verb) + W6 (noise isn't a player lever).

6. Recommendations — preserve the feel, add the legibility

Each recommendation states what, why it serves the pillar, the casual-mobile guardrail, and the config/anchors it implies. They are additive over the existing simulation.

R1 — Revive echo-sense as a deliberate "sense ring" (P0, near-free) — ✅ SHIPPED, see §7

Call drawSilhouette() for en.alive && !en.lit mobs within a sense radius (a new CFG.fog.senseRadius, e.g. 520px — larger than the 300px sight). Sensed enemies read as the existing pulsing blips: position + threat state, no health bar, identity only when close.

  • Why: restores pillar #1 — you perceive the field a screen ahead and can route. Turns fog from blindness back into tension (you know something's there, you just can't see it clearly).
  • Guardrail (as shipped): sense is LOS-gated — walls and the closed gate hide their occupants, so sealed rooms (the vault) keep their mystery; the noise system covers around-corner awareness. Blips show no precise cone (push/fall-back read only); a crisp cone + the backstab setup stay a reward for actually seeing the enemy. Tune senseRadius per enemy later (a loud Rocketeer senses farther than a Spider) — Arc's "hear the big ones first."
  • Anchors: drawSilhouette (exists), isSensed, render loop ~1990, CFG.fog.senseRadius. Resolved decision: sense mobs whenever in range+LOS; sense raiders only when loud (firing/cracking) — a quiet rival still stalks the fog unseen.

R2 — A gradual, per-enemy detection meter (P0) — ✅ SHIPPED (mobs), see §7

Replace the instant alert = 1 with a fill: while you're inside an enemy's perceive cone+range, alert ramps over detectTime (faster when closer / in the open, slower in a bush / at range). Crossing a suspicion threshold = the enemy turns to investigate; crossing spotted = full alert + combat. Decay when LOS breaks (already present, just slower at the top).

  • Why: creates the missing middle state — the "I'm being noticed, back off now" window that is the heart of cone-based stealth (Commandos/MGS). This is where route-skill lives.
  • Guardrail: keep detectTime generous for casual (e.g. 0.6–1.2s in the open) and make the meter loud and visual (see R3). Backstab still needs alert < spotted to land, so the meter is the stealth-kill timing game.
  • Anchors: perceive/updateMob alert logic, new CFG.stealth.detectTime, suspicionThreshold, reuse alertSpotted.

R3 — Make detection legible on both sides (P0/P1, pairs with R2) — ✅ SHIPPED (pip + vignette); cone-tint deferred

  • Enemy side: draw the detection meter as a small arc/pip over a sensing enemy (white → amber "?" → red "!"). Tint its cone amber while filling, red when spotted. (Note: cones are drawn only for enemies you can directly see, not for sensed blips — see the Phase A refinement. The pip/❗ on the blip carries the threat read for sensed enemies.)
  • Player side: a subtle screen-edge or under-token vignette that rises with the highest current detection on you ("you're being noticed"), going red on spotted. Answers "am I safe?" at a glance without numbers.
  • Why: W2's tension curve is only felt if it's drawn. This is the single biggest perceived-fairness upgrade.
  • Anchors: drawVisionCone (extend to sensed + amber state), drawActor (enemy pip), new player vignette in render.

R4 — Let the player commit to an approach on sensed enemies (P1)

Allow onTap to target seenByPlayer || sensed enemies (drop the lit-only filter once R1 ships). Tapping a sensed enemy queues the auto-path approach; the lock-on reticle shows even on a blip. Optionally preview the backstab arc on the target so you can pick the approach side (addresses W8).

  • Why: closes the loop — perceive (R1) → decide → act. Without this, intel is inert.
  • Anchors: onTap enemy filter ~744, drawTargetMark, backstab arc from doMelee.

R5 — Surface the player's own noise as a planning lever (P1)

Draw a faint ring at the player when you make noise (move-in-open vs sneak, fire, crack a chest) sized to that action's radius, and telegraph loud actions before commit ("cracking this chest will be heard ~300px"). Sneaking (slow move / in bush) shrinks the ring.

  • Why: turns the excellent-but-invisible noise economy (W6) into a routing tool — how you move and what you touch becomes a stealth decision, not just where.
  • Anchors: makeNoise, drawNoises (extend to own-noise ring), autoFirePlayer/openChest.

R6 — Broaden the stealth toolkit beyond bushes (P2)

Pick 1–2, lowest-friction first: - Wait/peek verb: holding still (no path) tightens your detectability and lets cones sweep past — rewards patience, the casual-friendly stealth skill. - Distraction: a throwable/lure (or repurpose a consumable) that makes a noise elsewhere to pull a patrol off your route — the classic patrol-manipulation verb. - Light/shadow tiles (bigger lift): dark tiles act as soft bushes; the lantern-lit POIs become the risky bright zones. Reinforces the concentric-risk map already in place.

  • Why: route-planning needs >1 spatial tool to be a real decision space (W5).

R7 — A legitimate intel layer for the tactical map (P2, ties off W7)

Once R1/R2 ship, the big map shows sensed blips (faded) + last-known-position ghosts for enemies that de-lit, instead of relying on devReveal. Turn devReveal back to a true debug-only default. Add a last-seen ghost marker in-world too (W8).

  • Why: makes pre-move planning legitimate and teaches object permanence — you plan against remembered intel, the hallmark of tactical stealth.
  • Anchors: drawBigMap enemy loop ~2766, devReveal default, new en.lastSeenByPlayer.

6.1 What NOT to change

  • Don't shrink/abolish the fog — limited vision is the genre. The fix is the sense layer, not more sight.
  • Don't make detection instant-and-forgiving by just lowering visR — that flattens the cone-routing puzzle. Keep cones meaningful; add the gradual read instead.
  • Don't touch the silent-backstab / armor-bypass / Power-bypass payoff — it's the correct reward and it's working.
  • Keep noise propagation exactly as tuned for enemies; only expose it (R5), don't retune it.

7. Suggested phased rollout

Sequenced so each phase is shippable and independently validates a hypothesis.

  • Phase A — "You can see to plan" (P0) — ✅ SHIPPED: R1 (revive echo-sense + senseRadius). Hypothesis: simply being able to perceive threats a screen ahead restores route-planning. Lowest cost, highest impact.
  • What shipped: CFG.fog.senseRadius (520px, > the 300px sight). isSensed(en) gates a new render pass: unlit enemies within sense range and along a clear line of sight draw as the existing drawSilhouette sonar blip. Rival raiders are sensed only when loud (_loudT, set for 0.7s on firing, or while _opening a chest/portal), so a quiet rival still stalks the fog unseen. Blips show no health and only name up close (≤360px).
  • Two deliberate refinements (from playtest feedback):
    • LOS-gated, not through-wall. Sense respects losTiles incl. the closed gate, so walls and sealed rooms keep their mystery — you don't see who guards the vault until you open it. Around-corner awareness is still carried by the noise system (loud creatures ping the map through walls); sense only adds silhouettes you have a line to. This reframes echo-sense as "making out movement at the dark edge of vision," not sonar-through-walls.
    • No cone on sensed enemies. A heard-not-seen blip shows position + threat state (❗) — enough to decide push or fall back — but not a precise gaze cone. Cone routing is a reward for actually seeing the enemy (within sight). drawVisionCone(en, dim) kept its dim param for Phase B's amber/sensed tinting, but sensed cones are not drawn today.
  • Not yet: tapping a sensed enemy to queue an approach (R4, Phase C) and the tactical-map intel layer (R7, Phase D) — sensing is currently in-world only.
  • Phase B — "You can feel the risk" (P0/P1) — ✅ SHIPPED: R2 (gradual detection) + the legibility half of R3 (per-enemy fill pip + player vignette). Hypothesis: the yellow middle state makes stealth a skill, not a coin-flip, and kills the "spotted for no reason" complaint.
  • What shipped (mobs only — see scope note):
    • Gradual fill: in updateMob, exposure inside a creature's cone fills alert over CFG.stealth.detectTime (0.9s mid-range) instead of snapping to 1; closer / more in-the-open fills faster (detectProxMin/Max scale by bestD/visR). Thresholds reuse the existing 0.5 line: <0.5 unnoticed · 0.5–1 suspect (turns toward + creeps in, holds fire) · ==1 fully spotted (locks on, mutual fire, only now rallies the pack via alertPack).
    • Escape window: breaking LOS / leaving the cone mid-fill decays alert (mobAlertDecay) and drops suspect → patrol at 0.5 — you can back off before being caught. An already committed chaser re-acquires instantly (no demotion). Validated by an isolated sim (mid-range full-spot ≈0.83s; point-blank ≈0.53s; LOS-break at 0.6 never spots).
    • Enemy-side read: drawDetArc draws a small amber→red fill ring over a mob that's noticing you; the red ❗ now means fully spotted (was: any alert>0.5). Sensed blips use the same 3-state colour (blue → amber → red) + arc.
    • Player-side read: G.playerDetect (max alert of any mob filling on you, or 1 if fully spotted) drives a screen-edge vignette — amber while you're being noticed, red + pulse when spotted. Answers "am I safe?" without a number.
    • Backstab interplay: since a suspect mob holds fire and seesPlayer stays false until a full spot, you can close in on a half-noticing creature for the silent backstab — the meter is the stealth-kill timing game. A non-lethal hit still fully alerts instantly (damage).
  • Scope note: applied to PvE mobs (the cone-routing puzzle). Rival raiders keep instant 360° detection — they have no cone to skirt, and their loop is a symmetric gunfight. Cone tinting (amber-while-filling) was left to a later polish pass; the arc pip carries the fill.
  • Phase C — "You can act and route" (P1): R4 (approach sensed enemies + arc preview) + R5 (own-noise legibility). Hypothesis: closing perceive→act and exposing noise deepens routing.
  • Phase D — "Stealth has depth" (P2): R6 (1–2 new verbs) + R7 (map intel + last-seen ghosts). Hypothesis: a real tool-set turns routing into expressive play.

A–B alone should move the core "plan a route, stealth-kill" experience from broken to good.


8. Open questions / decisions for the user

  1. Sense scope: mobs only, or rivals too (and only when loud)? (R1)
  2. Sense fidelity: position + facing hint (recommended), or position-only blip (harder)?
  3. Detection generosity: ~~target detectTime~~ — shipped at 0.9s mid-range (point-blank ≈0.53s). Live-tunable in the Perception tab; playtest to taste between 0.6s (twitchy) and 1.2s (very casual). (R2)
  4. Toolkit priority: which of wait/peek · distraction · light-shadow is most worth building first? (R6)
  5. Map intel: is the always-on tactical map (currently leaning on devReveal) part of the intended shipping design, or should sensing be primarily in-world with the map as a coarse overview? (R7)

Appendix — primary code anchors: perceive (enemy sight), isVisibleToPlayer / inVision (player sight), drawFog (visibility polygon), drawSilhouette (dead echo-sense), drawVisionCone / drawActor (enemy render), updateMob (alert/detection), makeNoise / drawNoises (noise economy), onTap (targeting), doMelee (backstab), CFG.fog / CFG.stealth / CFG.combat / CFG.noise (tuning).