Major pipeline changes to map building, introducing 3player map, and adding Shared build mode to level authoring options

This commit is contained in:
Matt F 2026-08-04 22:50:33 -07:00
parent 749c9203de
commit 2b1aee3e03
37 changed files with 5418 additions and 30 deletions

188
Docs/Map_Pipeline.md Normal file
View file

@ -0,0 +1,188 @@
# Map Pipeline
How to make a new map, and what the generator puts in the scene for you.
---
## One-time setup
Open **`9Player`** and run **`TD → Map Pipeline → Build Core Prefabs From Open Scene`**.
This reads the fully-wired reference scene and writes:
| Output | What it holds |
|---|---|
| `Assets/_Project/Prefabs/MatchCore/MatchServices.prefab` | Every map-independent, non-networked service |
| `Assets/_Project/Prefabs/MatchCore/MatchNetwork.prefab` | The six scene-placed `NetworkBehaviour`s |
| `Assets/_Project/Settings/MapTemplate.asset` | Prefab references, terrain defaults, starter-layout proportions |
It **copies live components** rather than rebuilding them from an asset scan. `AssetDatabase.FindAssets`
order isn't guaranteed stable across machines or re-imports, and several of these arrays double as
network identity — `EnemyUpgradePool.options` indices *are* card network ids, and
`TowerPlacementManager.towerDefinitions` indices *are* `TowerTypeId`. Two people generating the same
map could commit different orders, and *that* desyncs a host from a client. Copying sidesteps it, and
gives each new map the same known-good content as the reference.
> Maps having **different** catalogs from each other is fine and supported — every peer loads the same
> scene, and `PlayerTowerDeck.ServerInitialize` clears the deck at match start. Per-map enemy rosters
> are a matter of assigning a different `RunDefinition` on `_MatchNetwork/RunState`.
> The reference scene's contents aren't modified, but Unity marks it dirty. **Don't save it.**
Re-run this any time the shared boilerplate changes in a way the prefab should pick up.
---
## Making a map
**`TD → New Map...`**
Fill in name, player count, and size. The dialog validates the starter layout live and tells you the
exact width or height to use if the shape can't hold one.
On Create it does all of this:
1. Creates and saves the scene in `Assets/_Project/Scenes/Levels/`
2. Instantiates `_MatchServices` (**linked** prefab instance) and `_MatchNetwork` (**unpacked**)
3. Creates the `LevelData` asset and assigns it to **both** `LevelAuthoring.targetAsset` *and*
`LevelLoader.level`
4. Generates a bakeable starter volume layout
5. Creates a `Terrain` on the `TerrainGeometry` layer, sized to the map plus a skirt
6. Registers the scene in **Build Settings**
7. Appends the `LevelData` to the **`MapRegistry`** in `MainMenu.unity`
8. Runs the bake
Steps 3, 6 and 7 are the ones that used to fail silently — a map missing any of them looks fine in
the editor and breaks only at match start, or just never appears in the lobby.
A generated map bakes with **zero warnings** as long as you filled in author and description.
---
## What you get, and where to edit it
Four roots instead of the ~30 the hand-authored maps carry:
```
_LevelAuthoring ← yours: shape the volumes here
_MatchServices ← shared prefab: don't edit per-map (except LevelLoader.level)
_MatchNetwork ← the six NetworkBehaviours, unpacked
Terrain ← yours: sculpt here
```
`_MatchServices` is a **linked prefab instance**, so fixing something in
`MatchServices.prefab` fixes it in every map at once. The one field that is legitimately a
per-instance override is `LevelLoader.level`.
**Never press "Apply All" on the `_MatchServices` instance** — that would push this map's
`LevelData` into the shared prefab.
`_MatchNetwork` is unpacked deliberately: NGO treats in-scene placed `NetworkObject`s that are prefab
instances differently from loose ones, and unpacking keeps generated scenes structurally identical to
hand-authored ones. The cost is that changes to `MatchNetwork.prefab` don't propagate to maps already
created — there are only three asset references in it (`RunDefinition`, `GoldConfig`,
`EnemyScalingConfig`), so fix those by hand if they ever change.
---
## The starter layout
Player bands stacked south → north, full map width:
```
┌─────────── Goal ───────────┐ north
│ Player N band │
│ ····· leak gate ····· │
│ Player N-1 band │
│ ····· leak gate ····· │
│ Player 1 band │
│ [Player 1 spawner] │ south
└── MapArea (margin border) ─┘
```
Enemies spawn at the south edge of their owner's band and walk north. Each band leaks through a
one-tile gate into the next; the northernmost band is goal-adjacent instead of having a leak.
Proportions (margin, band depth, gate width, goal size, terrain skirt and carve depth) all live on
**`MapTemplate.asset`** — tune them there, not in code.
---
## Build Access: one maze each, or one maze together
`_LevelAuthoring → Rules → Build Access` is a per-map choice. Also offered in the New Map dialog.
| Mode | Meaning |
|---|---|
| `OwnerOnly` | Default. A player builds only inside their own `PlayerZoneVolume`. |
| `Shared` | Any player builds on any Buildable tile — the whole map is one cooperative maze. |
**Ownership doesn't disappear in Shared mode.** `OwnerGrid` still drives leak attribution, camera
start position, builder spawn point, and minimap tinting; zones still own their spawners and leak
exits. The only thing that lifts is the placement restriction. That's why this is a per-map enum and
not a change to how maps are authored — a Shared map is authored exactly like an OwnerOnly one.
**Towers still belong to whoever paid.** Sell, upgrade and paint stay owner-gated in both modes, so a
teammate can't sell your tower out of the lane you put it in.
Both modes route through `LevelLoader.HasBuildPermission`, which the client's ghost preview and the
server's authoritative check both call — they can't drift apart and show a white ghost that then gets
rejected.
> **Shared mode widens path validation.** Under `OwnerOnly` a player can only strand their own
> spawners, so `TowerPlacementManager.CheckPathValidity` checks one zone. Under `Shared` anyone can
> wall off anyone's lane, so it checks *every* zone's spawners — otherwise a player could leave a
> teammate's enemies with no route to any exit. Cost is one BFS per spawner per placement attempt
> rather than one per placement; placements are already throttled to `requestsPerFrame`.
Changing the mode on an existing map just needs a re-bake.
### Leak exits are optional on Shared maps
Bake rule **P5-8** ("every zone must have an outgoing leak or be goal-adjacent") is **skipped on
Shared maps**, so you can delete the `LeakExitVolume`s entirely. Every other leak rule is conditional
on a leak existing, so they're vacuously satisfied at zero.
That's safe because leak exits do almost nothing at runtime. `PathfindingService.ComputePath` takes no
destination — enemies always path to `LevelData.Goals`, and the route through a gate is *emergent*
from walkability, never directed. `LeakExitData.NormalizedWeight` is written by the bake and read
nowhere. Leak *attribution* runs off `OwnerGrid`, not leak volumes, so it keeps working. The safety
property P5-8 protected is enforced more strictly by **P5-4**, which BFSes every spawner to the exit
set — and with no leaks present that set is the goal tiles, which is exactly the right requirement.
> **Zones must then be directly adjacent.** A `LeakExitVolume`'s tiles are walkable; a bare gap
> between two zones is not. Delete the leaks without closing the gap and the bands become
> disconnected — P5-4 hard-errors and P5-11 warns, so the bake catches it loudly rather than shipping
> a map whose enemies can't move.
New Map handles this for you: choosing **Shared** generates bands butted directly together with no
gate rows and no leak volumes. Choosing **OwnerOnly** generates the one-tile gates.
---
## Changing the Quick Play map
Quick Start skips the lobby and loads **`MapRegistry.Default`**, which is the *first valid entry* in
the `Maps` array — not a separate setting.
Open `MainMenu`, select the `MapRegistry` GameObject, and drag the map you want to element **0**.
Entries with an empty `MapName` or `ScenePath` are skipped, so "first" means first *usable*.
New maps are **appended** to the array, so creating one never silently changes what Quick Play loads.
Reordering is deliberate.
---
## Things worth knowing
- **Walkability is the union of the gameplay volumes**, not something you paint. Walls are the
*absence* of a `PlayerZone` / `Spawner` / `LeakExit` / `Goal` volume. `MapAreaVolume` tiles are
deliberately **not** walkable — that volume only bounds camera pan and builder movement.
- **Terrain must stay on the `TerrainGeometry` layer.** The builder's ground raycast and spell
targeting both filter on it; on `Default` the builder silently walks at Y=0.
- **Terrain visuals have no relationship to the grid.** Nothing validates that the hills you sculpt
match the volumes you placed. That correspondence is yours to keep.
- **Volumes must be axis-aligned and unit-scaled.** Rotation is a hard bake error; non-unit
`Transform.Scale` is a warning. Size volumes with `BoxCollider.Size`, or use the
**Resize Volume Edge** scene tool.
- `MatchRules.MaxPlayers` is **3**. Larger player counts bake fine but no lobby can fill them yet.