# Endgate Architecture

> **Doc index:** [`README.md`](README.md).
> **Target loop:** see [`systems/gd-wave-loop.md`](systems/gd-wave-loop.md).

Endgate is a server-authoritative SECS v2 prototype for a top-down village
defense map. The C# host owns simulation, combat, wave state, deterministic map
generation, and the authoritative snapshot. The browser is a tactical shell that
renders that snapshot and sends high-level commands back to the server.

## Current runtime shape

- `Content/` holds the authored `.secs` shell for low-cardinality durable state.
- `Generated/Endgate.Generated.csproj` runs the SECS compiler over `Content/`
  and compiles the schema-owned generated output consumed by the runtime.
  Retained `Generated/` files are explicit non-schema host/catalog helpers.
- `Host/EndgateGameHost` owns the hot loop and durable run payload. Goblins and
  characters are SECS entities (`EnemyRoot` and `CharacterRoot` scopes) advanced
  through the activity pipeline; the host keeps thin `EnemyRuntime` /
  `CharacterRuntime` wrappers for live position and cooldowns inside a recycling
  enemy pool. The host also owns combat, activity-driven movement, progression,
  between-wave save/load, retry restoration, and seeded map generation.
- `Server/` hosts the SignalR hub, command/query routing, session snapshot
  cache, snapshot projection, and the authoritative tick loop.
- `Client/` is a React/Vite tactical shell. It renders static map geometry,
  village parcels, characters, enemy bodies, corpse marks, combat effects, and
  same-map outcome banners through the Three.js map surface.
- `Benchmarks/` exercises the same query, command, and tick paths used by the
  live server.

**Target gameplay loop:** the system docs under [`systems/`](systems/) describe
both shipped baseline and target design per feature. The detailed draft specs
are preserved in [`history/`](history/).

## Authority flow

1. The browser connects to `/gamehub` and reads `state.live` to get the first
   authoritative snapshot. Queries and commands reuse that same SignalR hub
   instead of creating per-command transports.
2. UI actions send high-level commands such as `run.start`, `wave.start_next`,
   `character.move_to`, `village.zone.purchase`, upgrade purchases, `save.load`,
   and `map.regenerate`, or query server-owned surfaces such as `save.export`.
3. `GameOperationRouter` validates the request and mutates `EndgateGameHost`.
4. `GameService` runs a 50 ms heartbeat. Each heartbeat reads the current
   simulation speed and advances that many host ticks.
5. `GameSession` projects `EndgateSimulationState` into `EndgateRootSnapshot`
   and broadcasts `LiveChanged` to every connected client.
6. The browser redraws from the new snapshot. It does not simulate locally.

That flow is the current authority boundary. Combat, wave progression, base
damage, character movement, upgrade purchases, save/load validation, and map
regeneration all resolve on the server.

## Map and wave model

- The current map is a deterministic 1600 x 900 meter-scale top-down horde
  battlefield. `1 gameplay meter = 1 authoritative coordinate unit`; the
  renderer may exaggerate sprites/rings for readability but must keep
  collision, building footprints, wall cells, and command targets in gameplay
  meters.
- The active village contract starts at 3 x 3 owned 32m zones inside a 9 x 7
  active zone envelope. Repeated zone purchases expand the active zone radii up
  to the server cap while preserving the meter contract and pushing the enemy
  perimeter outward.
- A whole-number seed drives open battlefield terrain, visual roads, blocked
  scenery, terrain movement cost, perimeter spawn arcs, and
  spawn-to-objective routing.
- The village has a square 32m zone grid centered on the Endgate. The core
  3 x 3 zones start owned; adjacent zones can be bought with energy between
  waves. The server projects owned/buildable, owned/reserved, purchasable,
  unaffordable, locked, wilderness, and blocked zone states with reason codes,
  strategic traits, threat exposure, and perimeter impact. Buying zones expands
  buildable territory on the same battlefield map; terrain-blocked or
  non-adjacent zones remain server-rejected.
- Owned parcels project explicit interior build slots. Non-wall structures use
  server-owned `zone_slot` placement domains, slot classes, occupied/reserved
  state, and snapped multi-meter footprints. Walls and gates are freeform 1x1
  build-cell buildings (`wall_cell` placement domain) placed on any owned cell;
  towers use the `wall_tower` placement domain on or beside a wall cell.
  Placement, relocation, placed rotation, wall/gate/tower changes, upgrade,
  repair, and salvage are server commands, not client-side decoration.
- Wall defenses are submitted through `building.wall_chain.place`,
  `building.wall_gate.toggle`, `building.wall_tower.place`,
  `building.wall_run.upgrade`, and `building.wall_run.demolish`. The host
  validates owned cell ownership, run connectivity, phase, energy, and cell
  occupancy before creating wall, gate, or tower bodies; tier upgrade and
  demolish address the flood-filled connected run that contains the target cell.
- Each wall cell projects its grid coordinate, autotile connection mask, tier,
  HP, blocker strength, gate passability, breach state, and render geometry as a
  `WallPieceSnapshot` (keyed by `cellId` `wall:x:y`). The renderer draws the wall
  line solely from the projected `wallPieces` and never owns wall adjacency,
  collision, or pathing.
- There are no player-facing fixed defense locations. The map is the command
  surface: selected characters receive server-validated world-space movement.
  Desktop control is RTS-style: left-click selects and right-click sends
  `character.move_to`; explicit assign mode remains the touch/mobile
  left-click command fallback. Build and `zone_buy` modes do not send character
  movement from empty-ground clicks. In `zone_buy`, left-clicking a highlighted
  zone sends `village.zone.purchase`; the panel may explain decisions but does
  not replace the map interaction.
- The character roster is a server-owned progression surface. The main
  character starts unlocked and remains special; guard, archer, mage, and healer
  definitions are host-owned unlockable roster entries. Character upgrades
  mutate max HP, damage, cadence, range, movement speed, armor, target priority,
  special cooldown, and revive cost only on the host.
- Downed characters do not auto-revive between waves. They remain downed,
  cannot move or attack, and require an energy-backed revive command. When the
  main character is downed, the host projects the visible penalty and increases
  horde pressure against the Endgate.
- Enemy entry is a server-owned perimeter model. During combat the snapshot
  projects active `edgeSpawnArcs`; during planning the telegraph is empty.
  Enemies sample deterministic valid perimeter cells inside those arcs, then
  move through the server cost field. Zone expansion reduces the projected enemy
  perimeter inset so later waves originate from a wider outer layer. Roads are
  visual planning terrain and do not give monsters a pathing discount.
- Waves are authored by the host wave director. The host keeps exact script
  identity, budget, cadence, rewards, spawn arcs, and mix internally, then
  projects `waveIntel` according to server-owned scouting level. At low intel
  `wavePlan` deliberately carries no descriptive script id, exact enemy count,
  target policy, rewards, cadence, or weighted mix. Full scouting reveals the
  exact `wavePlan`. The map and minimap use `waveIntel.knownSpawnArcs` during
  planning and vague edge pressure when arcs are still hidden; active combat
  uses server-projected `map.edgeSpawnArcs`.
- The command panel starts the server-selected planned wave with
  `wave.start_next` and no descriptive script id. The 1,000, 5,000, and 10,000
  body buttons are explicit stress drills: they send `waveSize` and `spawnRate`
  (validated in [1, 512]) so the host can scale the authored encounter without
  making the browser choose hidden wave identity.
- Active enemies are SECS entities on the `EnemyRoot` scope, dispatched through
  the `HostileMovement` and `HostileBehavior` policies. They project an
  archetype byte that identifies one of the five goblin templates
  (`GoblinScout`, `GoblinWarrior`, `GoblinRaider`, `GoblinSapper`,
  `GoblinChief`). The root snapshot also projects the server-authored goblin
  template catalog so the UI can explain target policy and readability without
  owning enemy behavior. Deaths project bounded corpse/fade rows, while attacks,
  special pulses, base damage, building damage, and healing project bounded
  combat-event rows.
- Wave identity advances only when a wave is cleared. If the Endgate falls, the
  host pays failure energy, sets `wave_failed_retry_pending`, increments
  `waveAttempt`, and keeps `currentWave` on the failed wave. Retry preserves the
  failed wave script, including stress-drill scale, cadence, edge arcs,
  archetype mix, and rewards; the next `wave.start_next` command starts that
  saved script instead of accepting a new script or wave size. Projected retry
  script identity and exact retry count/cadence are still redacted unless
  scouting level permits them.
- First-session guidance is also server-projected. The host derives the current
  step, primary map mode, command id, focus point, and milestone completion from
  authoritative run/progression state so the browser can display the next
  readable loop affordance without owning tutorial state.
- Save/load is a planning-state persistence surface. `save.export` returns the
  host-authored run payload only when no wave has queued or active enemies.
  `save.load` validates the payload version and restores seed, tick, wave
  attempt, retry script, energy, scouting level, village zones, slot occupancy,
  buildings and wall pieces, moved/rotated building state, village upgrades,
  roster unlocks, character upgrades, downed state, and building damage before
  broadcasting a fresh root snapshot. Active
  enemy, corpse, and combat-event buffers are runtime feedback and are not
  serialized.
- The active enemy pool holds `EndgateHostOptions.MaxLiveEnemies` simultaneous
  bodies (default 128) and that live-concurrency cap is exposed in the snapshot
  as `activeEnemyCapacity`. It is scalable: raising `MaxLiveEnemies` widens the
  live window for stress probes. A wave budget larger than the cap is honored in
  full and streams through the pool over ticks as freed slots recycle, so if a
  wave queues faster than active bodies clear, the queued count remains visible.

## Why the browser is a renderer, not a game simulation

The client receives a full root snapshot with run state, objective state, map
projection, character roster, progression state, one compact enemy render
batch, and bounded visual-feedback batches. That snapshot already contains the
information the browser needs to paint the current map, enemy positions, death
marks, combat flashes, and outcome banners. The browser only keeps UI-local
state such as the selected character, transport status, and the current seed
draft.

This keeps the current prototype honest about authority: one simulation, one map
generator, one combat model, and one source of truth. Local command feedback is
transient UI state only: pending, accepted, and rejected movement targets mirror
the latest server command result and do not move characters without an
authoritative snapshot.

## Current UX position

The current Endgate shell is the tactical map at `/endgate/`. The older
dashboard-style shell is not the intended final Endgate UX. This prototype is
focused on the authoritative defense map, not on a final surrounding product
shell. Desktop and landscape viewports use a 1920 x 1080 logical canvas;
narrow portrait viewports use a 900 x 1600 logical canvas. Camera reset frames
the 9 x 7 active village envelope around the Endgate by default, while pan and
zoom still allow full-battlefield inspection. The renderer classifies the
orthographic view into strategic, tactical, and build tiers so the full 1m grid
is suppressed outside close build contexts, while the minimap keeps owned
territory, camera viewport, known pressure, walls, structures, and the Endgate
visible at all zoom levels.
