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>
10 KiB
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 Cardper ability above, each pointing at itsEnemyAbilityDefinition. - 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:
WaveGroupassets (TD/Run/Wave Group) — bags of weightedWaveDefinitionentries. Weights are relative 0–1 and default to 1 (equal odds). A zero weight warns inline and means the wave can never be drawn.PhaseDefinition(TD/Run/Phase Definition) —WaveGroups,BossGroups, andEnemyUpgradeGroups(the groups authored in §1).RunDefinition(TD/Run/Run Definition) —WavesPerCycle(5),CyclesPerPhase(3), and thePhasesarray. 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 itsabilitiesarray. ItsnoAbilityWeightfield 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.optionsarray 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
Wavesdata 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.MaxHpno longer exists. It'sHpMultipliernow — a deviation from budget, not hit points. All ten definitions have been re-authored into a 0.7–1.35 band by archetype (golems tanky/slow, ents fragile/quick, drake fastest and flimsiest). These are placeholder values — tune them.
MoveSpeedstays 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
- Author upgraded towers as ordinary
TowerDefinitionassets and add them toTowerPlacementManager.towerDefinitions(not tostartingDeck). - Wire the tree by filling each parent's
UpgradePathsarray with its children. - Each upgrade node's
GoldCostis its upgrade price — upgrade nodes are never placed directly, so the field is free to mean that. - 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.
- Create a
TD/Draft/Tower Upgrade Optionper node (BaseTower→UpgradedTower) and add it toDraftPool. 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
(
TowerCombatre-reads it every tick) but not the model — visual swap is an art task. - No 3-player map yet.
MatchRules.MaxPlayersis 3 and slot allocation caps there, but9Playeris 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.