UnityTowerDefense/Project_Context.md

12 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-06-24.


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 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: NetworkBehaviours 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: MainMenuLobby → 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.

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 on tower placement/removal).
  • 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/Sell actions (HUD buttons disabled), enemy resistances/weaknesses, in-match race-pick countdown.