Skip to content

Web → Unity Migration Plan

Porting the Shadow/Thornwood Raiders web prototype (game.js + config.js, single-file HTML5-canvas) to a maintainable Unity 6 / URP project. The web build is the design source of truth; this plan covers every validated mechanic, the target architecture, the toolset, and the order of work.


0. Status snapshot — 2026-06-29

The in-raid core loop is ported and faithful (verified vs game.js): zone/theme/vault map generation + spawning + portals + loot scatter; creature & rival-raider AI; hazards (fire/web/rocket/ bomb) + lunge; the vault (key/gate/Keykeeper); the keyRacer rival; the stage boss + Descent Portal; fog/vision/LOS/noise/stealth + backstab silent-kill; loot (chests, ground loot, spill, magnet, rarity rolls); status effects + channels; a config-tunable floater/banner feedback system; and the presentation layer — a pooled prefab-view system (ViewPool/—Views—) for floaters, name/loot labels, health bars and channel halos; a unified text-config (size/colour/bold + outline/shadow) on a baked Lato SDF font; minimap + fit-all tactical map; and off-screen threat arrows (Shapes triangles, gated to mobs/rivals actually hunting the player). Config is one GameConfig SO + per-thing defs (no magic numbers).

Completed since the 2026-06-27 snapshot

Minimap + tactical map (fit-all) + zone tint; pooled prefab-view system (floaters/labels/bars/halos, rent-on-vision); unified text config + Lato SDF; off-screen threat arrows (triangles, target-gated); salve→usable-potion + "+HP" heal floater + heal bar; 3-way damage-source threading (Health.LastAttacker) fixing mob aggro; tileset group tints. Then (2026-06-29 session): #1 Gear stats, #2 Meta/save/hub,

4 Biome ladder — see below.

DONE (verified vs game.js)

  1. Gear stats & loadoutGearStats/GearItem/Loadout/GearTables/Progression: computeStats, itemStats (per-level growth), sets (hunter/stalker/bulwark), perks (backstab/bush/mob-dmg/heal-on-kill), power. Player Power is gear-driven (671→1476 starter); LootRoller rolls gear on gear.rar*. The in-run inventory (Loot Station / field-equip / bag / Safe Pocket / corpse looting) is PARKED — see Remaining.
  2. Meta / save / hub — JSON SaveData (stash/loadout/biome ladder/currencies/records), endRun banking (gear→stash, valuable→gold, auto-equip-empty, run value), and a throwaway hub/loadout (equip/unequip/upgrade/scrap)/result UI (MetaHud) imitating the web lobby/inventory/result.
  3. Full multi-phase extraction (commits f25f1fb…6344c34) — call→incoming→open→board→close machine, ring-guards, rival multi-portal contention, Shapes escape/descend visuals. PARKED by default.
  4. Biome ladder — config-driven prog.*: per-(biome,floor) map size/zone-specs/vault gate, bDanger
  5. lootMul scaling, rarity zone tint (minimap), biome unlock + floor/biome select. MapDefinition reduced to a per-biome recipe; de-duplicated config↔asset. Endless-ladder extension (2026-07-10, Unity-only — web parked you on the final boss floor): save.biome is a virtual rung counting past the authored BiomeSet (ever-increasing analytics numbers); content clamps to the last authored biome while danger/spawn ramps keep climbing (MetaLoopController.AdvanceLadder, Progression.Danger(…, extraRungs), MapPopulator.Populate taking the virtual index). Authoring a real next biome later picks the same save index up automatically — no migration. Details in PROJECT_CONTEXT.md → "Unity port — current state".
  6. In-raid polish (cc026c9…0df86a0) — HP regen, projectile velocity inheritance, backstab crit + strict-arc auto-stab + range-gated 🗡 tell, world-event escalation + Wildmagic storm, enemy power-scaling, ⚡ threat nametags, PvP power-gap. Gear perks now wired (Phase A). Balance values verified web-exact.

REMAINING to migrate

  1. Real HUD / screens (MVC) — the in-raid HUD + hub/loadout/result are throwaway (TesterHud + MetaHud + PlayerVitalsView). Need real MVC: vitals/ammo/power/stamina HUD, inventory/loadout + (parked) loot-sheet screens, result screen — plus the pause menu (Resume/Retry/Tuning/Lobby), how-to/help screen, and the in-game Developer Tuning panel (none exist in Unity; the Inspector + GameConfigEditor cover authoring). Tactical map remaining: player-centred zoom, on-map noise rings, legend panel.
  2. Map authoring toolsMapDefinition custom Editor Window (params + live preview); the MapLayout fixed-layout asset + loader (prereq for FTUE). Gen works in code today.
  3. FTUE — the scripted tutorial stages (web 1-7: TUTORIAL table, buildTutorialMap, per-stage grants), on the MapLayout loader. The save carries a tutorial stub; no curriculum.

PARKED (intentionally off — matches web toggles; code absent in Unity)

  • In-run inventory (loot.inRunInventory=false) — Loot Station sheet, field-equip (equipNow), run-bag UI, Safe Pocket, corpse looting (loot.corpseMinItems). Live web = auto-collect → auto-equip (ported).
  • Shield / cells (player.shieldEnabled=false) — overshield pool + regen, shield cells (drop/channel/ button). Health has no shield pool.
  • Weak-point armor (combat.weakpointArmor=false) — directional front/rear armor multipliers.
  • Valuables (loot.valuableChance=0) — sellable trinkets (loot is consumables/equipment/blueprints/coins).

DEFERRED nuances — ✅ DONE (commit 3217a56)

  • stealth.silenceWindow — a backstab on an UNAWARE victim (alert < 0.5) stays quiet for the config window (was a hardcoded 0.05s; now EnemyController.MarkSilentKill).
  • stealth.bushSightCutoff — the player can't see an IDLE bushed enemy past 70px (EnemyController.Revealed).
  • combat.bandHysteresis + combat.stanceCommit — the PLAYER's melee↔ranged stance arbiter is now sticky (enter/exit hysteresis + commit timer, web updatePlayerAttacks); was a plain meleeBand single threshold.

Camera / screen shake — ✅ DONE (commit 3217a56)

The web G.shake system (was entirely missing from CameraRig): RaidState.Shake + AddShake; CameraRig decays it and offsets the ground plane. Wired at every web trigger — hit/crit/player-hit, chest (+legendary), explosion, boss/guard death, gate open, portal open (ritual beat), storm. Amounts in a shake config group (web magic numbers). Verified end-to-end (hit→0.2, decay 1.0→0).

Assets / polish (remaining)

TMP emoji font (🗡/🔑/⚡/👑 render as boxes — pending a fallback font asset); real entity sprites (emoji/placeholder art today); in-world 3D zone floor tint (minimap tint done); chest open-FX polish (Chest.cs TODO).

Maps/minimap design: docs/superpowers/specs/2026-06-27-minimap-tactical-map-design.md; pooled views: docs/superpowers/specs/2026-06-29-pooled-prefab-views-design.md.

(The phase plan + feature table below predate this snapshot — treat §0 as the live status.)


1. Principles

  • Faithful feel, native tech. Don't port the immediate-mode canvas renderer 1:1 — re-implement visuals with Unity-native tech (sprites, chunked meshes, Shapes). Port the math, formulas, and tuning values verbatim so the game feels identical.
  • Data-driven. Every magic number lives in a config asset (ScriptableObject), mirroring config.js/CFG.*. Keep the key names so balancing carries over.
  • Testable domain. Pure-C# simulation (pathing, detection, noise, combat math, loot rolls) lives in engine-agnostic classes with EditMode unit tests. The web has none; Unity should.
  • DI over globals. Replace the web's global G state with an injected RaidState and services (Zenject). No singletons-by-convenience.
  • MVC UI. Game logic never touches UI widgets directly; controllers mediate model ↔ view.
  • Mobile-first. Pooling, batching, the world-space fog + Shapes component-mode work already done.

2. Toolset

Concern Choice Notes
DI Zenject (Extenject) ProjectContext (config, save, signals) + per-scene SceneContext (raid systems, factories).
UI UI Toolkit + uGUI, mixed + MVC Use UI Toolkit where it maps cleanly (data/list-heavy panels, the eventual tuning panel — UXML≈HTML, USS≈CSS) and uGUI where it's simpler (world-space bars, quick HUD). Pick per screen; both drive through MVC.
Events Zenject SignalBus Decouples systems↔UI (HpChanged, NoiseEmitted, PlayerSpotted, LootCollected, RaidEnded, ExtractProgress…).
Config One aggregate GameConfig ScriptableObject A single SO holds everything tunable, as [Serializable] nested groups mirroring window.CONFIG (move, combat, stealth, noise, loot, …). Injected whole; systems read config.move.* etc. A runtime tuning panel is deferred (the Inspector covers it for now); when added it just walks this one SO like the web walks CONFIG.
Vector FX Shapes (component mode) Already integrated — route, click ripple, noise rings, UI rings. (Immediate mode is dead under URP Render Graph here.)
Rendering URP World-space fog-of-war (done), chunked tile mesh (done), sprite billboards for entities.
Input Unity Input System Already in use (tap / double-tap; add swipe-dodge).
Save JSON @ persistentDataPath via SaveService Replaces localStorage (thornwood_save_v1).
Pooling custom/Zenject MemoryPool Projectiles, floaters, effects, noise rings, enemies.
Tests NUnit EditMode Domain systems validated against values pulled from game.js.

3. Target architecture

Assets/Scripts/
  Config/          ScriptableObjects mirroring CFG groups (injected, designer-tunable)
  Domain/          pure C#, engine-agnostic, unit-tested
  World/           runtime entities + MonoBehaviours (the live raid)
  Presentation/    visuals only (read state, never mutate gameplay)
  UI/              MVC screens (UXML/USS + View/Controller, observable Models)
  Infrastructure/  DI installers, state machine, save, signals, factories, pooling
  • Config/ — one aggregate GameConfig ScriptableObject holding every tunable as [Serializable] nested groups that mirror window.CONFIG (move, stamina, combat, stealth, noise, loot, gear, extraction, map, progression, ui, plus enemies/weapons tables). Injected whole; systems read config.move.*. Parked toggles live here too (default off). TileSet and the new MapDefinition/MapLayout (see §8) are separate art/level assets, referenced from it or the scene.
  • Domain/Pathfinder ✅, Los ✅, DetectionModel (gradual alert), NoiseModel (propagation + recruit selection), CombatResolver (damage, armor/weakpoints, crit/backstab), LootRoller (type odds + rarity ladder), ExtractionStateMachine, WorldEventSchedule, StaminaModel. No UnityEngine deps where avoidable → unit-testable.
  • World/Player, Enemy (Mob + Raider behaviours), Chest, Gate, Portal, Projectile; plus the already-built MapGenerator ✅, FogOfWar/VisionField ✅, GazeCone ✅, NoisePing ✅, PathAgent ✅, CameraRig ✅. Entities query injected Domain services; the RaidState (entity lists, player ref, run timer, alarm level) replaces global G.
  • Presentation/EntityView (billboard sprite + above-head bars), FloaterSystem, EffectSystem (burst/dash/spark/shake), ThreatIndicators, TargetArrow, Minimap, TacticalMap; PathVisualizer ✅, ClickRipple ✅. Reads state, emits nothing gameplay.
  • UI/ — per screen Model (observable) / View (UXML binding) / Controller (subscribes to SignalBus + model, drives view): Hud, Inventory/LootStation, Loadout, Result, Menu, TuningPanel, PauseMenu.
  • Infrastructure/GameStateMachine (Boot → Menu → Raid → Result), SaveService, ProjectInstaller + RaidInstaller, SignalBus signal definitions, EnemyFactory/ ProjectileFactory (Zenject factories), ObjectPool.

Zenject wiring - ProjectInstaller: bind all config SOs, SaveService, SignalBus + signals, scene loader. - RaidInstaller (SceneContext): bind RaidState, all Domain systems, factories, presentation systems; install signals consumed in-raid. - Replace G.player/enemies/effects/... with RaidState injected into systems and entities.

4. Feature inventory (web → status → target)

Status: ✅ done · 🟡 partial · ⬜ todo. Section refs are game.js.

Web system Status Target module
Pathfinding (A* 8-dir + string pull) Domain/Pathfinder
LOS raycast (DDA) Domain/Los
Map generation — zone-based (colored zones + gray connective, POIs, doors) 🟡 basic random World/MapGenerator + MapDefinition SO authored via custom Editor Window (§8)
Fixed pre-made layouts (tutorial stages, verbatim) MapLayout asset + loader (§8) — prereq for FTUE
Tile art (variants, walls, bush cover) World/TileSet
Fog of war + player vision cone World/FogOfWar + VisionField
Movement: walk / sprint (double-tap) World/PathAgent + ClickToMove
Movement: dodge roll (swipe) + i-frames World/PlayerController + StaminaModel
Stamina (drain/regen, gating) Domain/StaminaModel
Camera: iso follow, dynamic zoom, sprint pull-back, look-ahead World/CameraRig
Enemy vision cones (gaze) — AI use 🟡 viz only Domain/DetectionModel + World/Enemy (GazeCone ✅ is viz)
Detection: gradual alert (suspicious→spotted), p.spotted, bush slow-fill Domain/DetectionModel
Noise: makeNoise, sonar rings, hearing range, recruit-nearest 🟡 rings ✅ Domain/NoiseModel (NoisePing ✅ is viz)
Stealth: bush cover mult, backstab crit, blind-spot tell, stealth-chase sprint, silence window Domain/Stealth + World/Player/Enemy
Ranged combat: archetypes (assault/smg/shotgun/sniper), projectiles, reload, auto-fire, pellets/spread Domain/CombatResolver + World/Projectile + WeaponTable
Melee: lunge (windup→dash→recovery), auto-melee range handoff, backstab crit, hitstop World/Player/Enemy + CombatConfig
Armor / weakpoints (front armor, rear flank) ⏸ parked deferred — see §7
Bombs / explosions / hazards World/Projectile + EffectSystem
Enemy AI — PvE creatures: smart patrol, pack broadcast, surround, flee, lastSeen search World/Enemy (Mob) + Domain
Enemy AI — rival raiders: loot, fight/fear, self-extract, carry real haul World/Enemy (Raider)
Loot types: gear / blueprint / coin / consumable (valuables parked) Domain/LootRoller + LootConfig
Chests: crack channel, rarity, tables, coin/consumable extras World/Chest + Domain/LootRoller
Inventory triage: Loot Station, field-equip, safe-pocket, dump, bag cap, corpse looting ⏸ parked deferred — see §7 (active model = auto-collect + auto-equip)
Vault gate: proximity/tap force-open channel, loud open World/Gate
Extraction: multi-phase Arc-style portals, safe vs progression exits, guard Domain/ExtractionStateMachine + World/Portal
World events / escalation (run timer, wildmagic storm) Domain/WorldEventSchedule
Render: iso sprites, above-head bars, threat-edge arrows, target arrow, floaters, effects 🟡 map only Presentation/*
Route line + reticle, click ripple Presentation/PathVisualizer + ClickRipple
Minimap + full tactical map Presentation/Minimap + TacticalMap
HUD / screens (vitals, ammo, result, menu) UI/* (MVC)
Meta/save: gold, blueprints, stash, loadout, upgrades, power, bestHaul Infrastructure/SaveService + UI/Loadout
FTUE: 6 scripted tutorial stages + coach driver + guided lobby spotlight data-driven StageDefs + TutorialController
Dev tuning panel (auto-built from CONFIG, live + persisted) ⬜ (inspector covers it short-term) UI/TuningPanel
Input: tap / double-tap / swipe 🟡 tap+double ✅ World/InputService
Frame cap (60) AppBootstrap

5. Implementation order

Phase 0 — Foundation (architecture spine).Done. Install Zenject; create ProjectInstaller + RaidInstaller; port config.js groups to ScriptableObjects (verbatim values/names); build GameStateMachine (Boot→Menu→Raid→Result) + SaveService; introduce RaidState and refactor the existing World scripts to take injected config instead of loose public fields where it makes sense. Stand up the EditMode test project. Deliverable: empty raid scene boots through DI with config-driven systems.

Phase 1 — Vertical slice (a playable raid).Core done (Raid_Slice scene). Pure-C# Domain (StaminaModel/DetectionModel/NoiseModel); one Mob that detects → chases → melees; player auto-fire + projectile + damage/death; one Chest (auto-collect); one extraction Portal → save bank; death → DOWNED. Verified end-to-end in play. Still owed before Phase 2 polish: dodge-roll + swipe input, camera zoom/look-ahead, projectile pooling, an entity sprite/EntityView pass (placeholder primitives for now). Deliverable met: move, fight/sneak past one enemy, loot a chest, extract.

Phase 2 — Stealth core (the pillar). 🟡 Core done.DetectionModel (gradual alert, suspicious/spotted, bush slow-fill, committed); enemy vision cones driving AI (correct visA/2 half-angle, alert>=1 sees 360°); NoiseSystem/NoiseModel (propagation, hearing range, recruit-nearest, sonar rings); bush cover detect-mult (model) + visual occlusion (map); enemy search/investigate (POI + give-up); IsInBlindSpot helper; ✅ silent backstab crit (player auto-backstab from a blind spot, CombatResolver). Owed (Phase 3 melee / Phase 7 UI): 🗡 blind-spot tell + stealth-chase sprint + silence-window mute. Mirrors the web's FTUE order.

Phase 3 — Combat depth. 🟡 In progress: CombatResolver ✅; unified player auto-melee (frontal + blind-spot silent backstab) ✅; damage→aggro ✅; weapon archetypes + pellets/spread + stance arbiter + magazine/reload ✅; symmetric stealth-fire (mutual perception) ✅; heal potion ✅. Todo: melee lunge windup/dash juice + hitstop; bombs/explosions/hazards. No front-armor/weak-point model (parked, §7).

Phase 4 — Enemy AI breadth. 🟡 In progress: EnemySpawner (Zenject-instantiated from EnemyDatabase, per-instance cones) ✅; damage→aggro ✅; enemy ranged attacks (mutual-gated, telegraphed direct fire + standoff) ✅; pack broadcast (a chaser rouses nearby pack-mates) ✅; smart patrol (calm mobs wander at half pace, web patrolMove) ✅; combat-weave (chasers hold range + strafe to open the rear, web combatMove) ✅. Todo: surround/flank, lastSeen-search refinement; rival raiders (loot, fight/fear mobs, self-extract, carry haul). Patrol zone-leash / portal-guard / loot-cache guarding deferred (need zones/extraction).

Phase 5 — Loot (active model only). Loot types (gear / blueprint / coin / heal potion — no valuables), LootRoller + chest tables, auto-collect into the uncapped bag, auto-equip into empty loadout slots on extract, result tally. No in-run Loot Station / field-equip / safe-pocket / dump (parked, §7). First MVC screens here are the Result + lobby Loadout (not an in-run inventory).

Phase 6 — Interactions, extraction & events. Chest-crack + vault-gate channels; multi-phase Arc portals + safe/progression exits + guard; world events / escalation (timer, wildmagic storm).

Phase 7 — UI/HUD polish (MVC). Vitals/ammo/stamina, minimap, full tactical map, threat-edge arrows, target arrow, floaters, effects.

Phase 8 — Meta / progression. Save; gold/blueprints; stash; loadout equip; gear upgrades (spend blueprints); power calc; bestHaul/raidsWon.

Phase 9 — FTUE. Data-driven tutorial stage defs + coach driver + guided lobby spotlight.

Phase 10 — Polish. Runtime tuning panel; audio; juice (shake/hitstop done); settings; build & perf pass (mobile).

6. Risks & guidance

  • Port formulas, not pixels. Lift detection/noise/combat/loot math directly from the cited game.js functions into Domain classes; unit-test against hand-checked values.
  • Kill the global. All shared run state goes in injected RaidState; entities never reach for a static.
  • Tuning parity. Each CFG.* value → a config-SO field with the same name, so the existing balance transfers and a future tuning panel maps 1:1.
  • Reuse what's done. Fog of war, map mesh, route/ripple/noise visuals, pathing, camera follow, frame cap are already migrated — build on them.
  • Don't over-abstract early. DI + layers yes; premature ECS/event-sourcing no. Vertical slice first (Phase 1) to validate the architecture before breadth.

7. Parked features — NOT migrated initially

The web keeps several systems behind "parked" master toggles (code present but inert). We do not port these in the first pass — add them later only if the design calls for them. Until then we mirror the web's active fallback behaviour.

Parked system Web flag Active behaviour we DO migrate
Shield / overshield + shield cells player.shieldEnabled = false No shield bar/cells; the only consumable is the heal potion.
In-run inventory: Loot Station, field-equip, bag UI, safe-pocket, dump, bag cap, corpse looting, rummage noise loot.inRunInventory = false Chests/kills auto-collect into an uncapped bag; gear auto-equips into empty loadout slots on extract (autoEquipEmpty). No triage UI.
Valuables (sellable 💎 trinkets) loot.valuableChance = 0 Loot rolls are gear / blueprint / coin / consumable only.
Front-armor / rear-weak-point combat.weakpointArmor = false Mobs take normal damage from all angles; the FTUE weak-point lesson is skipped.

Keep the toggles as fields on GameConfig (default off) so re-enabling later is a flip + the deferred code, exactly like the web. This trims Phase 3 (combat: no armor model) and Phase 5 (loot: no inventory UI — just auto-collect + auto-equip + result tally).

8. Map authoring — two paths, one loader

Map generation gets reworked around data assets, supporting both real raids (procedural) and the FTUE (hand-authored, fixed):

  • MapDefinition (ScriptableObject) — procedural raids. Holds the parameters: width/height, wall/bush density, loot %, enemy %, available zones/POIs, seed policy. Authored in a custom Editor Window (configure values → live preview of a sample generation → save to the SO). At runtime MapGenerator loads a MapDefinition and generates from it (replacing today's hardcoded generator).
  • MapLayout (ScriptableObject/asset) — fixed pre-made levels (tutorial stages). An explicit, hand-authored layout: the exact tile grid (walls/bush/floor) plus placed content (spawn, chests, scripted enemies, vault/gate, extraction points). Loaded verbatim — never regenerated on stage init, so a tutorial stage is identical every time. An FTUE stage = a MapLayout (static geometry + placements) + a stage script (coach steps, scripted triggers) layered on top.
  • One loader: MapGenerator takes either a MapDefinition (generate) or a MapLayout (load as-is) and produces the same MapData + chunk meshes downstream, so fog/pathing/vision are agnostic to which path built the map.

The custom Editor Window (preview + save-to-SO) is its own slice — fits in the Phase 1 "finish map gen" / Phase 6 area; the MapLayout path is a prerequisite for Phase 9 (FTUE).

9. Tuning & authoring tools (designer-facing)

Everything tunable is a designer-editable asset — no code edits to balance the game. The full set of authoring surfaces:

  • GameConfig SO — global balance. One asset, nested groups mirroring window.CONFIG (move, player, weapons, melee, combat, stealth, noise, fog, camera, ui, extraction, map, prog, zones, bot, enemyScale, loot, gear). [Tooltip]s carry the web's per-field blurbs. Edited in the Inspector now; a runtime tuning panel (deferred) later walks this same object like the web's dev panel walks CONFIG.
  • EnemyDef SOs + EnemyDatabase — per-creature tuning. Each enemies.* entry becomes one EnemyDef asset (hp/atk/speed/visR/visA/radius/melee/ranged/projectile/flags/art); an EnemyDatabase lists them for spawning. Add a creature = add an asset, no code.
  • Weapon archetypesweapons.* table as WeaponDef SOs (or a sub-table on GameConfig).
  • MapDefinition SO + custom Editor Window — procedural raid params with live preview → save (see §8).
  • MapLayout asset + StageDef — fixed FTUE layouts + their coach scripts (see §8). A simple layout inspector/painter can come with the FTUE phase.

Designer workflow goal: tune balance in GameConfig, add/adjust creatures via EnemyDef assets, author maps via the Map editor window, and lay out tutorials via MapLayout/StageDef — all in the Editor, all version-controlled assets.

10. Completeness check (audit)

Cross-checked against every game.js section and config.js group. Explicitly covered: map-gen/zones, pathing, LOS, fog, movement+stamina+dodge, camera (zoom/look-ahead), detection, noise+recruit, stealth/backstab/bush, ranged+melee combat, projectiles/bombs/hazards, PvE + rival AI, loot/chests (active model), interactions/gate, multi-phase extraction, world events, render (sprites/floaters/effects/threat arrows/target arrow), minimap + tactical map, HUD/screens, save + meta/progression + power calc, FTUE stages + lobby spotlight, input (tap/double-tap/swipe), frame cap. Explicitly deferred (§7): shield/cells, inventory triage, valuables, armor/weak-point. Explicitly dropped (web-only, not needed): the immediate-mode canvas renderer (replaced by Unity-native), the in-browser tuning panel (Inspector + later runtime panel), localStorage (→ JSON save). Dev-only devKit/sandbox and weapons.override map to Inspector/debug toggles, low priority.

11. Owed within active features (revisit)

Things that are implemented but partial — a faithful subset shipped, with the rest deferred (often blocked by a not-yet-ported system). Tracked here so they're not lost; revisit when the blocking piece lands.

  • Stealth (Phase 2): stealth-chase sprint (tap an unaware enemy from its blind spot → free auto-sprint at it until spotted) is now ported (PlayerController.SetHunt freeChase + _chaseSprint no-drain in the stamina block + spotted-ends-chase). The silence-window (a backstab on an unaware victim keeps the kill quiet for a beat) is still owed.
  • Enemy ranged (Phase 4): direct-fire works (mutual-gated, telegraphed, standoff). Deferred: bomb lob (bombardier, projSpeed=0) → needs a hazard/AoE system; rocket splash (rocketeer) → needs explosion radius; fire burn (emberball) → needs a burn DoT; spider web (atkWeb → slow) → needs the web/slow effect. combatMove weave/juke is ported (EnemyController.CombatMove — radial range-hold + perpendicular strafe, per-axis wall collision, A* fallback). Distinct bullet colour is ported (Projectile.Init(tint): player cyan #9fe8ff, mobs toxic-green #a6ff7a, boss/fire orange #ff8a3a — web fireWeapon/fireArcShot colours). Enemy gunshot noise is ported (EnemyController.FireRangedNoiseSystem.Emit(gunshot), throttled ~every noise.gunCd-th shot, web fireProjectile makeNoise) — recruits nearby mobs via the same Emit→Recruit→Investigate path the player's gunshot uses.
  • Noise sources (web makeNoise). Every noise whose feature exists in the slice is now wired: player gunshot + melee clash (frontal only — a backstab is silent), enemy gunshot, death (deathMob "big" loud kill — draws mobs to the hotspot — vs a near-silent backstab "shot" on a clean stealth kill, EnemyController.OnDied + MarkSilentKill), and hunter footsteps (hunterStep "step", recruit:false — a roused, moving, currently-UNSEEN mob pings through walls so a threat closing behind cover is audible; EnemyController.HuntNoise). The sonar rings render via NoisePing (lazily spawned by NoiseSystem). Still deferred with their features (no source exists yet): chest-crack/pop, gate force/open, loot rummage, bomb-lob/explosion, extraction/portal calls, and rival-raider chest/vault noises.
  • Mob tiers (web MOB_TIER) ✅. A danger TIER (1 harasser · 2 warden · 3 rocketeer/bombardier/sentry · 4 boss) on EnemyDef — a classification, NOT a stat multiplier (the web never scales hp/atk by tier). Drives lost-trail tenacity (tier >= combat.heavyTier=3 → decay × heavyTenacity 0.35, clings longer) — replaces the old hardcoded 1 — plus the lunge gate and (later) loot rarity.
  • Facing lerp (web updateMob + moveAlongPath turnRate). Mobs turn at a limited rate instead of snapping, in every mode: (a) a visible chase steers the cone toward the player via EnemyController.FaceLerp (web turn 6 melee / 3.4 ranged); (b) path movement — patrol, search, A*-fallback — lerps the facing toward the heading at combat.patrolTurn (web patrolMove passes it to moveAlongPath: "turn the head/cone gradually, not in one frame") via PathAgent.turnRate, set per-enemy; (c) ScanSpin sweeps INCREMENTALLY from the current facing (no absolute-yaw jump). PathAgent lerps from the LIVE transform.forward, so handing off between direct-steer combat and path-following never jump-cuts. The cone lags → circling to the unwatched rear works; the shot still aims straight at the target (facing is cosmetic to the aim).
  • Visual de-flicker (web _litHold). EnemyController.Shown holds the drawn state for fog.litHold (0.2s) after LOS drops, so an enemy peeking a corner / brushing sight's edge stays solid instead of strobing. Presentation (EntityView, HealthBarView, gaze cone, Perceived/ tap-targeting) reads Shown; the raw per-frame Revealed still gates the mutual fire window + footstep silence. (The fog.litHold knob existed but was previously unused.)
  • Melee: hitstop (the backstab-kill freeze-frame, web G.hitstop), the render-only attack lean (web setLean), and the windup→dash→recovery lunge (web meleeSequence:2539 + the chase arbitration :2761) are all ported (RaidController.Hitstop, EntityView.Lean, EnemyController.MeleeSequence/LungerChase/IsLunger). The lunge gates on IsLunger (melee && !ranged && !boss && tier >= melee.lungeMinTier, lungeMinTier=2). The skeleton is promoted to a tier-2 BRUTE (a deliberate slice tweak — web MOB_TIER had skeleton=1) so it's the slice's one visible lunger; the other melee stay tier-1 walk-in, and the tier-2 warden stays a non-lunger (it's ranged). Set lungeMinTier=1 to give every melee the lunge. A lunger's close-combat: IN REACH → quick simple attacks (atk·simpleMult) + weave; MID RANGE (off cd) → the telegraphed lunge (windup tracks → locks a destination just past the target → dash at lungeSpeed, per-axis wall-stop, plants on connect / overshoots on a dodge/whiff to bare its rear → recover); else walk in. Tier-1 melee keep the non-lunger path (Chase weave + point-blank hit).
  • Gaze cones / projectiles: destroyed on death, not pooled yet (pooling is Phase 10).
  • Potion: auto-used when critical as an interim; needs the HUD potion button + channel.