Updates Project_Context.md + Project_Roadmap.md for the tower-sell work (refund rules, coin VFX/SFX, refund excluded from income) and the PathfindingService budgeted re-path scheduler that removed the sell/placement frame hitch. Follows commit 0b4cde4 which landed the code + art.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
106 lines
14 KiB
Markdown
106 lines
14 KiB
Markdown
# 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 9 players.
|
||
|
||
- **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**.
|
||
- **Roguelike layer (now core to the design):** every player starts with the **same three towers** and builds out a personal "deck"/"build" through a **3-option draft** presented at match start and after every wave. Choices span new towers, systemic upgrades, builder abilities, enemy debuffs, and relic quests. 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`, `BuffDefinition`/`BuffCategory`, `DraftOption` (+ `NewTowerDraftOption`). Designers tune stats in assets, not code.
|
||
- **Per-player state pattern:** `NetworkBehaviour`s on the **Player prefab** with a static `GetForClient(clientId)` / `Local` registry. Current set: `PlayerGoldManager`, `PlayerMatchState`, `PlayerBuffManager`, `PlayerTowerDeck`, `PlayerDraft`.
|
||
- **Networked identifiers are catalog indices.** `TowerTypeId` indexes `TowerPlacementManager.towerDefinitions[]`; `DraftOptionId` indexes `DraftPool`. Stable **within a match**, not across sessions — cross-match persistence will need stable IDs (asset GUID or a serialized StableId).
|
||
- **Namespaces:** `TD.Core` (enums, palettes, grid math), `TD.Gameplay` (builder, placement, match/wave/pathfinding state, economy, enemies, deck), `TD.Gameplay.Draft` (draft system), `TD.Combat` (TowerCombat/Projectile), `TD.Levels` (in-engine authoring + bake), `TD.UI`, `TD.Net`.
|
||
- **Scenes:** `MainMenu` → `Lobby` → a Match level (`9Player`, `Main`).
|
||
|
||
### 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.)*
|