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

15 KiB
Raw Permalink Blame History

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, 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 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 — WaveGroupPhaseDefinitionRunDefinition, 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: NetworkBehaviours 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: MainMenuLobby → 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 TowerTypeIds. 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 TowerTypeIds 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.)