UnityTowerDefense/Docs/2.0_Setup_Checklist.md
Matt F 16706a1ecf Scale difficulty by run position; wipe towers at phase end
Follow-up pass on the 2.0 refactor, closing the gap between "waves are drawn
at random" and "difficulty still lives in the enemy assets".

- Enemy health is no longer authored per enemy type. New EnemyScalingConfig
  scales a per-zone HP *budget* by encounter number; per-enemy health is that
  budget divided by the wave's enemy count. Scaling the total rather than the
  per-enemy value keeps enemy count a texture knob (few tanks vs many swarmers)
  instead of a second, uncontrolled difficulty axis. EnemyDefinition.MaxHp
  becomes HpMultiplier, a deviation around 1.0. Speed stays archetype-only and
  unscaled -- escalating it compounds with health and invalidates tower balance
  mid-run.
- GoldConfig entries no longer reference a WaveDefinition. Payout is a property
  of run position, not of which wave got drawn: WaveGoldEntry ->
  EncounterGoldEntry, Waves -> Encounters, keyed by global encounter number.
  Its inspector labels elements "Encounter N" and projects cumulative earnings.
- The wave's voted buffs now show as icon badges in the top bar, read from the
  same RunState slot the spawn path uses so the display can't drift from what
  the enemies actually carry. Hover names the buff; unillustrated cards fall
  back to a lettered badge rather than vanishing.
- Clearing a phase's boss destroys every built tower, unrefunded. Queued build
  jobs still refund -- those towers were never delivered. Runs with the field
  empty, so the walkability churn doesn't hit the re-path scheduler.
- Fixed an RPC codegen break: [ClientRpc] requires a ClientRpc suffix, unlike
  the newer [Rpc(SendTo...)] style this file doesn't use.
- Setup checklist reordered into dependency order; it previously asked for a
  RunDefinition two sections before creating one.

Also carries the editor-side asset reorganisation into Definitions/RunDefinitions
and the sprite move into Enemy/Player draft icon folders.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-31 00:32:33 -07:00

10 KiB
Raw Blame History

2.0 Refactor — Editor Setup Checklist

The 2.0 code landed on 2.0_design_refactor and compiles clean, but the new systems are data- and scene-driven. Nothing runs until the assets below exist and the scene objects are wired. Work top to bottom — each section depends on the ones above it.

Sections are in dependency order — every asset a step references has been created by an earlier step. Scene wiring comes last, once there is something to assign.


0. Assets vs. scene objects

The single most useful distinction when hunting for a field:

  • ScriptableObject → a file in the Project window. WaveDefinition, WaveGroup, PhaseDefinition, RunDefinition, every ability, every card, every upgrade group.
  • MonoBehaviour → a component on a scene GameObject. Only four: RunState, WaveVote, EnemyUpgradePool, EnemyAbilityPool.

Scene components hold references to assets, never the other way round — RunState in the scene points at the RunDefinition asset on disk.

Where the assets live, under Assets/_Project/Definitions/:

Asset type Folder
Abilities RunDefinitions/EnemyAbilities/
Enemy-buff cards + upgrade groups RunDefinitions/Draft/EnemyDraft/
Player draft options RunDefinitions/Draft/PlayerDraft/
Wave groups, phases, run RunDefinitions/RunStructure/
Individual waves RunDefinitions/Waves/
Enemy types Enemies/
Gold + enemy scaling configs RunDefinitions/

1. Enemy-buff cards

Two asset layers, because network identity and phase gating are separate concerns.

Abilities (TD/Enemy Abilities/…) — what a buffed enemy does:

  • Split On Death — already exists; re-check its fields, they changed. It now has SplitCount, HpPercent, SpeedPercent, ScalePercent, LivesCostPercent, FlightHeightPercent, ScatterRadius.
  • Flight, Blink, No Bounty, Gold Theft — new, need assets creating.

Cards (TD/Enemy Upgrades/…) — what players vote on:

  • One Ability Card per ability above, each pointing at its EnemyAbilityDefinition.
  • One Double Up Card (vote-meta; grants no ability).

Groups (TD/Enemy Upgrades/Upgrade Group) — the unit of phase gating:

Cut groups by when a card should become available, not by what it does, since a group is what you drag between phases: EUG_Phase1_Basics, EUG_Phase2_Nasty, and so on. Availability accumulates — phase 2 lists both Basics and Nasty. For MVP, one group holding every card is fine.

A card must be in both the pool (§4, network identity) and a group (phase gating) to ever be offered. Pooled-but-ungrouped is silently never offered — that's how you park an authored card you aren't ready to use. Grouped-but-unpooled logs a warning and is skipped.

Keep a card in only one group per phase. Two referenced groups holding the same card give it two independent shots at the draw, effectively doubling its rate.


2. Run structure

Build bottom-up:

  1. WaveGroup assets (TD/Run/Wave Group) — bags of weighted WaveDefinition entries. Weights are relative 01 and default to 1 (equal odds). A zero weight warns inline and means the wave can never be drawn.
  2. PhaseDefinition (TD/Run/Phase Definition) — WaveGroups, BossGroups, and EnemyUpgradeGroups (the groups authored in §1).
  3. RunDefinition (TD/Run/Run Definition) — WavesPerCycle (5), CyclesPerPhase (3), and the Phases array. MVP needs one phase.

The RunDefinition inspector shows a live validation panel and a per-phase capacity readout. Get it green before pressing play.

The constraint that will bite

A phase must supply WavesPerCycle waves with DISTINCT enemy types. Enemy buffs are keyed to a wave slot, so two slots sharing an enemy type would make "which wave did this buff apply to" ambiguous. Ten waves in a pool is not enough if they only use three enemies. The inspector reports distinct-type count per phase; the draw fails loudly at match start otherwise.

Ten wave definitions and ten enemy definitions already exist, so the content floor is met — but check the type spread.


3. Scene objects (both 9Player and Main)

Everything these reference now exists. Two new GameObjects need a NetworkObject; one doesn't.

GameObject Component NetworkObject? Assign
RunState RunState Yes The RunDefinition asset from §2.
WaveVote WaveVote Yes Nothing — Options Per Vote defaults to 3.
EnemyUpgradePool EnemyUpgradePool No Every card asset from §1 into options.

Also update the pool that already exists:

  • EnemyAbilityPool — add every ability asset from §1 to its abilities array. Its noAbilityWeight field is gone (abilities are voted in now, not rolled per enemy).

WaveManager no longer has a waveDefinitions array — that field is gone. Its two new serialized fields are Draft Time Seconds (30) and Vote Time Seconds (25).

Both new NetworkBehaviours are scene objects, so they must be present before the host starts.

EnemyUpgradePool.options array order is each card's network id. Appending is safe; inserting or reordering desyncs a host from clients running a different build.


4. Gold config

GoldConfig.Waves is now GoldConfig.Encounters, indexed by global encounter number and counted continuously across the whole run — cycles do not reset it. One phase of 5 waves × 3 cycles + a boss needs 16 entries. Missing entries pay zero for that encounter.

Entries no longer reference a WaveDefinition. Payout is a property of run position, not of which wave was drawn — encounter 3 pays encounter 3's rates whatever spawns in it. The inspector labels each element Encounter N and projects per-encounter and cumulative earnings against PreviewEnemiesPerEncounter, a preview-only figure that nothing at runtime reads.

The existing asset's Waves data migrates automatically on first import ([FormerlySerializedAs]) — but it holds 10 entries and one phase needs 16, so the last six encounters (including the boss) pay nothing until they're authored.


5. Enemy scaling

Create an EnemyScalingConfig (TD/Enemy Scaling Config) and assign it to WaveManager alongside the GoldConfig. Without it every enemy spawns at a flat 100 HP and difficulty never progresses — the run is playable but meaningless.

Three numbers, no per-encounter table:

Field Default Meaning
BaseHpBudget 5000 Total enemy health encounter 1 presents, per player zone
GrowthPerEncounter 1.18 +18% per encounter → budget doubles roughly every 4
BossHpMultiplier 6 Extra multiplier on a boss encounter

Health is a budget, divided. Per-enemy HP is the encounter's budget split across the wave's enemy count, then skewed by the type's HpMultiplier. This is what lets waves be drawn at random: a 150-strong swarm and an 8-strong elite pack in the same slot present the same total threat. Enemy count becomes a texture knob, not a difficulty one.

EnemyDefinition.MaxHp no longer exists. It's HpMultiplier now — a deviation from budget, not hit points. All ten definitions have been re-authored into a 0.71.35 band by archetype (golems tanky/slow, ents fragile/quick, drake fastest and flimsiest). These are placeholder values — tune them.

MoveSpeed stays absolute and is deliberately never scaled by run progress. Escalating speed compounds with escalating health and invalidates tower balance mid-run; fast-vs-slow is a fixed character trait. Escalation is carried by health and the voted buffs.


6. Player prefab

Add PlayerTowerUpgrades to Player.prefab, alongside PlayerTowerDeck / PlayerDraft. Without it, tower-upgrade draft options can't apply and the Upgrade buttons never appear.


7. Enemy prefabs

Any enemy that should be able to carry wave buffs needs an EnemyAbility component on its root. WaveManager logs a warning naming the prefab if a buffed wave spawns an enemy without one.

SplitOnDeath and any size-changing buff need the prefab's NetworkTransform to sync scale, or minions render full-size on remote peers.


8. Tower upgrade trees

  1. Author upgraded towers as ordinary TowerDefinition assets and add them to TowerPlacementManager.towerDefinitions (not to startingDeck).
  2. Wire the tree by filling each parent's UpgradePaths array with its children.
  3. Each upgrade node's GoldCost is its upgrade price — upgrade nodes are never placed directly, so the field is free to mean that.
  4. Footprint must match the parent. A differently-sized upgrade is rejected: the tower's occupied/unwalkable tiles are stamped at its current size, and converting would leave the grid describing a shape the tower no longer has.
  5. Create a TD/Draft/Tower Upgrade Option per node (BaseTowerUpgradedTower) and add it to DraftPool. Prerequisites need no explicit list — a node is offered only once its parent is reachable (in the deck, or itself unlocked).

Existing TowerUpgradeDraftOption assets changed meaning: they used to swap the deck entry, they now unlock an upgrade node. Re-check any that exist.


Phase boundaries reset the board

When a boss dies and a new phase is drawn, every built tower is destroyed with no refund — players rebuild their maze from scratch each phase. Nothing to wire; it runs automatically off RunAdvance.NextPhase.

  • Queued and in-progress builds are refunded — those towers were never delivered, so eating the gold would just punish having a build queued when the boss happened to die.
  • Shelved build sites are cleared too, so no ghosts outlive the maze.
  • A system message names the loss in chat, since an entire maze vanishing needs a stated cause.
  • The final phase's boss ends the run instead, so no wipe happens there.

Known gaps

  • Upgraded towers keep the parent's mesh. Switching the definition changes stats immediately (TowerCombat re-reads it every tick) but not the model — visual swap is an art task.
  • No 3-player map yet. MatchRules.MaxPlayers is 3 and slot allocation caps there, but 9Player is still the only authored level.
  • Tower vulnerability is not built — Savage, Mind Control, AOE EMP, Fart Miasma and Dummy all need towers to be damageable/disableable, which doesn't exist. Deferred by agreement.
  • Relics untouched, as agreed.