UnityTowerDefense/Project_Context.md
Matt F 4892d7253d First pass at full refactor to 2.0 design
Restructures the game around the cyclical run loop from Game Design Doc V2:
5 waves = a cycle, 3 cycles = a phase, each phase ends in a boss.

- New TD.Gameplay.Waves: WaveGroup / PhaseDefinition / RunDefinition author the
  run as draggable weighted pools; RunState owns phase/cycle position, the drawn
  wave slots, and the per-slot enemy buff sets. WaveManager's flat wave array is
  gone -- it now only runs the encounter RunState points at.
- New TD.Gameplay.EnemyUpgrades: the post-wave enemy-buff vote, with public
  live-replicated ballots so the HUD can show who voted for what.
- Inter-wave flow is now strictly sequential: draft -> vote -> build, each stage
  ending early once every player has acted.
- Enemy abilities inverted from per-instance random rolls to deterministic,
  stacking per-wave-slot sets. Six cards ship: Split (reworked), Flight, Blink,
  No Bounty, Gold Theft, Double Up.
- Tower upgrades are a two-step tree: a draft pick unlocks a node, gold converts
  an already-placed tower in place.
- Boss encounters flag their enemies and drive a boss HP bar.
- Player cap reduced to 3 via MatchRules.MaxPlayers.
- GoldConfig is now keyed by global encounter number rather than wave index.

Compiles clean; NOT yet verified in-engine. Editor wiring still required --
see Docs/2.0_Setup_Checklist.md.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-30 17:45:11 -07:00

109 lines
15 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Unity Tower Defense — Project Context
## Purpose
A snapshot of **where the project is and how it works** — the authoritative reference for current architecture, implemented systems, conventions, and known debt. It pairs with [`Project_Roadmap.md`](Project_Roadmap.md), which is the forward-looking plan. When the two disagree, this document describes *what exists today*; the roadmap describes *what's planned next*.
Last substantial update: 2026-07-14.
---
## Game overview
A **co-op tower-defense / roguelike hybrid** for up to **3 players** (reduced from 9 in the 2.0 design as a scope cut — see `TD.Core.MatchRules.MaxPlayers`; the `PlayerSlot` enum still runs to 9 so baked `LevelData` owner grids stay valid).
- **Maze defense (Wintermaul-style):** players build towers to force enemies along a longer path through their own zone. Lives are a **shared pool**; gold is **per-player**.
- **Cyclical run structure (2.0):** 5 waves = a **cycle**, 3 cycles = a **phase**, each phase ends in a **boss**, 3 phases wins the run. A phase draws 5 waves with **distinct enemy types** and replays them every cycle. Beating the last phase's boss is Victory.
- **Roguelike layer (core):** every player starts with the same three towers and grows a personal deck through a **3-option draft**. After each wave the sequence is: personal draft → shared **enemy-buff vote** (open ballots, visible per-player) → build countdown. The vote permanently buffs the wave just cleared, so it comes back harder next cycle; buffs are wiped when a new phase draws fresh waves. See [`Project_Roadmap.md`](Project_Roadmap.md) for the full design.
- **Target platform:** Steam (Windows / Linux / Steam Deck).
- **Visual direction (aspirational):** "painted tabletop miniature" look with Spider-Verse-style stepped (on-2s) enemy animation. Current visuals are **placeholder** (primitive meshes / cones, sourced creature models).
## Engine & tech
Unity **6.4 (6000.4.4f1)**, URP, IL2CPP, .NET Standard 2.1, Linear color space, new Input System, Force Text serialization. Netcode: **Netcode for GameObjects (NGO) 2.x**.
## Repository & collaboration
- Self-hosted **Forgejo**: `https://git.marlboro-bc.duckdns.org/yeahweregames/UnityTowerDefense` (git remote **`origin`**). A legacy GitHub mirror exists as remote **`github`**.
- **Multiple contributors** work in parallel on feature branches merged to `main` via PRs. Expect `main` to move between sessions; rebase/branch off the latest.
- **IP guardrail:** the prototype uses Games-Workshop-adjacent placeholder content (race names, sourced models). The repo stays **private**, no public builds/demos, until that content is replaced. Maintain a plain-text asset manifest of IP-derived assets to swap before any public release.
---
## Architecture & conventions
- **Server-authoritative gameplay; local-only UI/visual state.** Only gameplay-meaningful state is networked. Selection, placement ghosts, animation, and the paint cursor are client-local.
- **Data-driven via ScriptableObjects:** `TowerDefinition`, `EnemyDefinition`, `WaveDefinition`, `RaceDefinition`, `GoldConfig`, `DraftOption` (+ subclasses), `EnemyAbilityDefinition` (+ subclasses), `EnemyUpgradeOption` (+ subclasses), and the run structure — `WaveGroup``PhaseDefinition``RunDefinition`, plus `EnemyUpgradeGroup`. Designers tune stats in assets, not code.
- **Pools are authored as draggable groups.** Wave and enemy-buff pools are lists of *group* assets, so rebalancing a phase means dragging a group between phases rather than re-authoring entries. Draw weights are **relative** 01 values on each entry (`[PoolWeight]`), normalized at draw time; a zero weight means "never drawn" and the inspector warns inline.
- **Per-player state pattern:** `NetworkBehaviour`s on the **Player prefab** with a static `GetForClient(clientId)` / `Local` registry. Current set: `PlayerGoldManager`, `PlayerMatchState`, `PlayerTowerDeck`, `PlayerTowerUpgrades`, `PlayerDraft`, `PlayerSpellLoadout`, `BuilderUpgradeManager`.
- **Networked identifiers are catalog indices.** `TowerTypeId` indexes `TowerPlacementManager.towerDefinitions[]`; `DraftOptionId` indexes `DraftPool`; `EnemyUpgradeOptionId` indexes `EnemyUpgradePool`; wave ids index a flat list `RunDefinition` builds by walking its phases/groups/entries in declaration order (identical on every peer). Stable **within a match**, not across sessions.
- **Run progression lives in `RunState`, not `WaveManager`.** `RunState` owns phase/cycle position, the drawn wave slots, and the per-slot enemy-buff sets; `WaveManager` only runs the encounter it is pointed at and calls `ServerAdvance()` when the field clears.
- **Namespaces:** `TD.Core` (enums, palettes, grid math, match rules), `TD.Gameplay` (builder, placement, match/pathfinding state, economy, enemies, deck), `TD.Gameplay.Waves` (run structure + progression), `TD.Gameplay.Draft` (per-player draft), `TD.Gameplay.EnemyAbilities` (what buffed enemies *do*), `TD.Gameplay.EnemyUpgrades` (the vote + its cards), `TD.Combat` (TowerCombat/Projectile), `TD.Levels` (in-engine authoring + bake), `TD.UI`, `TD.Net`.
- **Scenes:** `MainMenu``Lobby` → a Match level (`9Player`, `Main`). `9Player` is **legacy** at 3-player scope — a new 3-player map is pending.
### Engineering principles (carried across sessions)
- **Debug to root cause.** No defensive workarounds or per-frame state-correcting hacks — find and fix the actual cause.
- **Restate design intent before coding.** Confirmed valuable repeatedly; prevents rework.
- **Server owns gameplay; clients render.** Authoritative state on the server, visualization derived locally on each peer.
---
## Implemented systems
### Match foundation
- `MatchState` (NetworkBehaviour scene singleton): phase (`Lobby`/`CountDown`/`Playing`/`Victory`/`Defeat`), current wave, shared lives. `PlayerMatchState` (per-player): slot allocation, race selection, ready state. Authoritative client-id → `PlayerSlot` mapping.
### Combat & towers
- `TowerCombat` (server targeting loop; `OnTargetAcquired`/`OnTargetLost`/`OnFire`/`OnAreaFired` events), `Projectile`, hitscan + projectile paths, target types **Single/Splash/Chain/AllInRange**, damage types incl. **Electric**, range indicator. **Air-targeting** done (`GroundedOnly` skips flyers). Range checks are **3D**.
- `TowerPlacementManager` (server-validated placement queue) — `SpawnTower` now **seats towers flush on the ground from their mesh bounds** (`SeatOnGround`), so any pivot works (replaced the old hard-coded y=0.5). `Builder` build queue with staged construction/pause/cancel/refund, RTS selection, owner tinting.
- **Construction-phase build visuals:** `BuildSiteVisual` swaps between an ordered set of phase prefabs as a tower builds — `ConstructionPhaseSet` SO + optional per-tower `TowerDefinition.ConstructionPhases`, with a project-default set and a legacy cube Y-scale fallback. `BuildSiteVisual` resolves its def via `TowerTypeId` (catalog), like `TowerInstance`.
- **Tesla Coil tower:** Electric, `AllInRange` (zaps every enemy in range each 0.5s tick). `TeslaArcVisual` draws arced, crackling, HDR `LineRenderer` bolts that flicker, plus **real point lights** (emitter flash + per-impact lights at struck enemies, capped) so the electricity actually illuminates the scene — emission/bloom alone don't cast light.
- **Tower Sell** (branch `tower-sell`, verified in-engine 2026-07-14): selecting an owned, completed tower shows a **Sell** action in the command grid's **bottom-right** slot (owner-gated; a non-owner sees it disabled). `TowerInstance.RequestSellServerRpc` (owner-validated, like paint) refunds `TowerDefinition.SellRefundPercent` (default 0.75) of the tower's replicated `goldInvested` (placement + any future upgrade spend), except a `FullRefundIfUnupgraded` tower (the **Wall**) refunds 100% while `upgradeCount == 0`. The refund is **not** counted as round income — `PlayerGoldManager.AwardGold(amount, countAsEarned:false)`. Selling despawns the tower (`OnNetworkDespawn` un-stamps the footprint as a **batch** and clears selection) and broadcasts a coin **VFX + rustle SFX** through `SellEffectSpawner` (scene singleton mirroring `FloatingTextSpawner`) via `TowerPlacementManager.PlaySellEffectRpc` (routed off a persistent object because the tower despawns the same frame). `TowerInstance.ServerAddUpgradeInvestment` is the seam the future upgrade system writes to. The coin VFX is an **artist-authored** prefab (`FX_SellTower`: a Shuriken **mesh-particle** burst of the Blender `Coin` model with an **emissive** gold material — emissive so it survives the grimdark grade); `CoinBurstVfx` is the zero-art code-gen fallback, and new reusable `TransientEffect` self-destructs one-shot effect prefabs (for VFX-Graph effects that lack a Stop Action).
### Enemies & pathfinding
- `EnemyHealth` (replicated HP, damage types, `IsFlying`, held state), `EnemyStatus` (lingering effects: slow/DoT), `EnemyMovement` (A* path following, zone-leak attribution, death/sink sequence).
- `PathfindingService` (A* on the runtime walkability grid, octile heuristic, corner-cut prevention, line-of-sight path smoothing). **Re-paths via a budgeted scheduler:** grounded enemies `RegisterMover`/`UnregisterMover`; on a maze change the service enqueues them and recomputes under a per-frame `recomputeBudgetMs` (default 1.5 ms) drain, spreading the work across frames so a placement/sell with a full wave present no longer spikes a frame (was ~0.5 s). Safe because selling only opens the maze and placement is BFS-guaranteed to leave a route, so a few frames of slightly-stale path is invisible.
- **Flying enemies:** path on the **baked terrain grid** (`LevelLoader.IsBaseWalkable`) so they soar over towers; compute once, never re-path; spawn elevated by `EnemyDefinition.FlightHeight`.
- Content: ~10 enemy definitions/prefabs (Crystal Golem / Cyclops / Ent variants, Undead Drake = the flying test enemy), 10 wave definitions.
### Lobby, connection & races
- `MainMenu` + `Lobby` scenes; Direct-IP via `NetworkBootstrap` (the single seam for the deferred Steam swap) + `UnityTransport`; `LobbyService`, `SessionFlow`; **Quick Start** dev shortcut.
- Race data + selection UI: `RaceDefinition`, `RaceRegistry`, 16-slot `RaceId`, `RaceSelectionOverlay`. Two placeholder races (both use the default builder). **Note:** "Race" is the code term; the roguelike design calls this concept "Builder" — naming reconciliation is an open decision.
### Economy
- `PlayerGoldManager` (per-player, server-validated spend/award), `GoldConfig` (starting gold, per-wave kill rewards, completion + no-leak bonuses), `WaveManager` orchestration (prep countdown, per-zone spawning, kill-gold attribution, lives pool, Victory/Defeat).
### Roguelike — tower deck & draft (the current focus)
- **`PlayerTowerDeck`** (per-player): the growable set of unlocked `TowerTypeId`s. Starts at the base set (seeded by `WaveManager` from `TowerPlacementManager.startingDeck`); placement is gated server-side (`TowerNotInDeck` rejection); the HUD build grid reads the local deck and rebuilds live on change. **Merged to `main`.**
- **Draft system (Slice 1 — spine + "new tower"):** `DraftOption` (abstract SO) / `NewTowerDraftOption`, `DraftPool` (scene singleton catalog), `PlayerDraft` (per-player offered set + pick/buy-roll RPCs), `DraftService` (server weighted-random generation + offer/auto-resolve). The **prep phase is the draft window** (wave 1's prep = the match-start draft); unpicked drafts auto-resolve at prep end. Gold **"Buy Roll"** purchases an extra roll any time. Non-modal HUD overlay. **On branch `feature/draft-system`, verified working in-engine; pending commit/merge as of session end.**
- **Draft Slice 1 is merged** (it's in `main`/the active branch line, not just `feature/draft-system`). Tower content available to draft: **Basic Arrow** (ground+air single), **Siege Cannon** (ground-only splash), **Wall** (2×2 maze-marker), **Tesla Coil** (Electric AllInRange). Arrow/Siege have cone meshes; Tesla is a ProBuilder mesh. So the "new tower" draft finally has real content — starting deck vs. draftable pool is a live tuning knob.
### Paint system (PAUSED)
- In-match paint (R/G/B + Reset) recolors owned towers and drives effects (Red=Splash, Green=Poison, Blue=Cold), server-authoritative. **Frozen** pending the roguelike reconciliation decision (it overlaps with systemic damage-type upgrades).
### Level authoring
- In-engine `TD.Levels` authoring volumes (player zones, spawners, leak exits, goals) + `LevelData` + a bake pipeline producing walkability/placement/owner grids.
### Rendering & post-processing
- URP with **6 quality-tier RP assets** in `Assets/_Project/Settings/Render Settings/` (Mobile, PC, URP-Low/Medium/High/Ultra). **HDR** must be on per-asset (Quality section) for bloom/glow.
- **Project-wide post-processing** via the `DefaultVolumeProfile` (Edit→Project Settings→Graphics→Default Profile) — a **grimdark grade** (ACES tonemapping, desaturate/contrast, Split Toning, Bloom, Vignette, Film Grain). It has **no per-property checkboxes** (it's the always-on baseline). **Gotcha:** scene Volumes (e.g. the URP-template `SampleSceneProfile` in `9Player`) override the default — remove/redirect them so the default applies everywhere.
### HUD & dev tools
- `HUDController` (UI Toolkit): gold/wave/lives/leaks, scoreboard, minimap, selection portrait + context panel, build/paint command grid, build progress, match-end overlay, chat, buff menu, and the new **draft overlay**.
- `DevWaveControls`: **F9 / "Force Next Wave"**, **F8 / "Grant Next Tower"** (dev stand-in for deck growth).
---
## Known placeholders & technical debt
- **Visuals are placeholder** throughout (primitive/cone towers, sourced enemy models). See roadmap art TODOs.
- **Enemies lack idle animation support** — needs adding across all enemy prefabs.
- **Tower footprint visuals don't fill their 2×2 space** — meshes read smaller than the tile area they occupy; should visually reflect the full footprint.
- **Grimdark grade is in, but the scene fights it** — the post-processing reads correctly, yet the bright/flat-lit ice map (`9Player`) undercuts the mood. Grimdark needs a **lighting pass** (low ambient, hard moody key) and ultimately darker level art; post alone can't carry it.
- **URP additional-lights budget** now matters — Tesla flash + per-impact lights stack across towers; watch the URP asset's Additional Lights "Per Object Limit" / Per-Pixel setting.
- **Tower catalog is now a single source of truth:** `TowerPlacementManager.towerDefinitions[]` (by-index, = `TowerTypeId`). `TowerInstance` replicates that index and resolves via `TowerPlacementManager.GetDefinition`. *(The old by-name `TowerRegistry` was removed 2026-06-24 — add new towers only to `towerDefinitions` + `DraftPool`.)*
- **Catalog index 0 is a reserved sentinel** (valid `TowerTypeId`s start at 1) — easy to forget when wiring the catalog in the inspector.
- **Catalog-index identifiers aren't session-stable** — blocks cross-match persistence until a stable ID is added.
- **Paint system frozen**; **Race vs Builder** naming unresolved; the gold **"Buy Roll available any time"** rule is provisional and may change.
- **Stubbed/unbuilt:** tower **Upgrade** action (HUD button still disabled; the `TowerInstance.ServerAddUpgradeInvestment` seam is ready for it), enemy resistances/weaknesses, in-match race-pick countdown. *(Tower **Sell** is now done — see Combat & towers.)*