Compare commits

...
Sign in to create a new pull request.

1 commit

37 changed files with 5418 additions and 30 deletions

View file

@ -51,6 +51,16 @@ namespace TD.Levels
[Tooltip("Optional author/credit text. Empty triggers a soft warning at bake time.")] [Tooltip("Optional author/credit text. Empty triggers a soft warning at bake time.")]
public string author = ""; public string author = "";
[Header("Rules")]
[Tooltip("OwnerOnly: players can only build inside their own PlayerZoneVolume — the classic " +
"one-maze-each layout.\n\n" +
"Shared: any player can build on any Buildable tile, so the whole map is one " +
"cooperative maze. Zones still define territory for leak attribution, camera " +
"start, builder spawn and minimap tinting — only the build restriction lifts.\n\n" +
"Towers always belong to whoever paid for them; sell and upgrade stay owner-gated " +
"under both modes.")]
public BuildAccess buildAccess = BuildAccess.OwnerOnly;
// ------------------------------------------------------------------- // -------------------------------------------------------------------
// Gizmo visibility toggles. Each volume type reads its own toggle to decide whether to // Gizmo visibility toggles. Each volume type reads its own toggle to decide whether to
// draw always-on or only-when-selected. // draw always-on or only-when-selected.

View file

@ -63,6 +63,17 @@ namespace TD.Levels
public BakeOutcome LastBakeOutcome; public BakeOutcome LastBakeOutcome;
public int LastBakeWarningCount; public int LastBakeWarningCount;
// -------------------------------------------------------------------
// Match rules.
// -------------------------------------------------------------------
[Header("Rules")]
[Tooltip("Whether players are restricted to building in their own zone (OwnerOnly) or can " +
"build anywhere on the map's buildable area (Shared). Affects placement permission " +
"only — OwnerGrid still drives leak attribution, camera start, builder spawn and " +
"minimap tinting under both modes.")]
public BuildAccess BuildAccess;
// ------------------------------------------------------------------- // -------------------------------------------------------------------
// Grid metadata. // Grid metadata.
// ------------------------------------------------------------------- // -------------------------------------------------------------------

View file

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: cf34e1b2cedcd9a4fa7f938f8f0bf3c2
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View file

@ -0,0 +1,483 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!1 &1348868032911320215
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 2098889495380348335}
- component: {fileID: 8734966915826773517}
- component: {fileID: 6131651964009722688}
m_Layer: 0
m_Name: TowerPlacementManager
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!4 &2098889495380348335
Transform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1348868032911320215}
serializedVersion: 2
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 5251454257324784754}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!114 &8734966915826773517
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1348868032911320215}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: d5a57f767e5e46a458fc5d3c628d0cbb, type: 3}
m_Name:
m_EditorClassIdentifier: Unity.Netcode.Runtime::Unity.Netcode.NetworkObject
GlobalObjectIdHash: 3734927306
InScenePlacedSourceGlobalObjectIdHash: 0
DeferredDespawnTick: 0
Ownership: 1
AlwaysReplicateAsRoot: 0
SynchronizeTransform: 1
ActiveSceneSynchronization: 0
SceneMigrationSynchronization: 0
SpawnWithObservers: 1
DontDestroyWithOwner: 0
AutoObjectParentSync: 1
SyncOwnerTransformWhenParented: 1
AllowOwnerToParent: 0
--- !u!114 &6131651964009722688
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1348868032911320215}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: d369496ba7887b844b1c220b524a507d, type: 3}
m_Name:
m_EditorClassIdentifier: Assembly-CSharp::TD.Gameplay.TowerPlacementManager
ShowTopMostFoldoutHeaderGroup: 1
requestsPerFrame: 3
towerDefinitions:
- {fileID: 0}
- {fileID: 11400000, guid: d5b2e8fa3c7e9b5f4a1d0c2e7f6b3a91, type: 2}
- {fileID: 11400000, guid: 0f693e29ca953e1439e10cb8f12e4b30, type: 2}
- {fileID: 11400000, guid: 86d4c3ab00d4c654eb4c255280ee5701, type: 2}
- {fileID: 11400000, guid: 04219895bbea4fb9a94fd62fef35bf2a, type: 2}
- {fileID: 11400000, guid: e3f1a9c2b5d84e76a0c3f1e8d27b4905, type: 2}
startingDeck:
- {fileID: 11400000, guid: d5b2e8fa3c7e9b5f4a1d0c2e7f6b3a91, type: 2}
- {fileID: 11400000, guid: 0f693e29ca953e1439e10cb8f12e4b30, type: 2}
--- !u!1 &5626802001211498962
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 3648445760525794609}
- component: {fileID: 4563436515829454556}
- component: {fileID: 8936124320851695101}
m_Layer: 0
m_Name: WaveManager
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!4 &3648445760525794609
Transform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 5626802001211498962}
serializedVersion: 2
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 5251454257324784754}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!114 &4563436515829454556
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 5626802001211498962}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: d5a57f767e5e46a458fc5d3c628d0cbb, type: 3}
m_Name:
m_EditorClassIdentifier: Unity.Netcode.Runtime::Unity.Netcode.NetworkObject
GlobalObjectIdHash: 3957247190
InScenePlacedSourceGlobalObjectIdHash: 0
DeferredDespawnTick: 0
Ownership: 1
AlwaysReplicateAsRoot: 0
SynchronizeTransform: 1
ActiveSceneSynchronization: 0
SceneMigrationSynchronization: 0
SpawnWithObservers: 1
DontDestroyWithOwner: 0
AutoObjectParentSync: 1
SyncOwnerTransformWhenParented: 1
AllowOwnerToParent: 0
--- !u!114 &8936124320851695101
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 5626802001211498962}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 81d8e215d8419404ea4d959196cd9cc3, type: 3}
m_Name:
m_EditorClassIdentifier: Assembly-CSharp::TD.Gameplay.WaveManager
ShowTopMostFoldoutHeaderGroup: 1
startingLives: 40
draftTimeSeconds: 30
voteTimeSeconds: 25
goldConfig: {fileID: 11400000, guid: d5e01c919f14a1e4888ad494a159241b, type: 2}
enemyScalingConfig: {fileID: 11400000, guid: ae9fde0b564e20440ac2e20861fa1ca6, type: 2}
--- !u!1 &6226139915412633039
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 8547990667945757531}
- component: {fileID: 4703036714451320211}
- component: {fileID: 2895480021437825520}
m_Layer: 0
m_Name: MatchState
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!4 &8547990667945757531
Transform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 6226139915412633039}
serializedVersion: 2
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 5251454257324784754}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!114 &4703036714451320211
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 6226139915412633039}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: d5a57f767e5e46a458fc5d3c628d0cbb, type: 3}
m_Name:
m_EditorClassIdentifier: Unity.Netcode.Runtime::Unity.Netcode.NetworkObject
GlobalObjectIdHash: 1618207171
InScenePlacedSourceGlobalObjectIdHash: 0
DeferredDespawnTick: 0
Ownership: 1
AlwaysReplicateAsRoot: 0
SynchronizeTransform: 1
ActiveSceneSynchronization: 0
SceneMigrationSynchronization: 0
SpawnWithObservers: 1
DontDestroyWithOwner: 0
AutoObjectParentSync: 1
SyncOwnerTransformWhenParented: 1
AllowOwnerToParent: 0
--- !u!114 &2895480021437825520
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 6226139915412633039}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: a34585be59d49e94b811d96be51c5088, type: 3}
m_Name:
m_EditorClassIdentifier: Assembly-CSharp::TD.Gameplay.MatchState
ShowTopMostFoldoutHeaderGroup: 1
--- !u!1 &8213601217886697539
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 1789974153803072425}
- component: {fileID: 5759999417597858157}
- component: {fileID: 8805989965283647635}
m_Layer: 0
m_Name: RunState
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!4 &1789974153803072425
Transform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 8213601217886697539}
serializedVersion: 2
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 38.30481, y: 0.5, z: 155.93135}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 5251454257324784754}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!114 &5759999417597858157
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 8213601217886697539}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: d5a57f767e5e46a458fc5d3c628d0cbb, type: 3}
m_Name:
m_EditorClassIdentifier: Unity.Netcode.Runtime::Unity.Netcode.NetworkObject
GlobalObjectIdHash: 3660389945
InScenePlacedSourceGlobalObjectIdHash: 0
DeferredDespawnTick: 0
Ownership: 1
AlwaysReplicateAsRoot: 0
SynchronizeTransform: 1
ActiveSceneSynchronization: 0
SceneMigrationSynchronization: 0
SpawnWithObservers: 1
DontDestroyWithOwner: 0
AutoObjectParentSync: 1
SyncOwnerTransformWhenParented: 1
AllowOwnerToParent: 0
--- !u!114 &8805989965283647635
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 8213601217886697539}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: a293cf6dca91dd2478c9f4e799cf9e0d, type: 3}
m_Name:
m_EditorClassIdentifier: Assembly-CSharp::TD.Gameplay.Waves.RunState
ShowTopMostFoldoutHeaderGroup: 1
runDefinition: {fileID: 11400000, guid: 1bb81a966192c344d94cce2628c1832b, type: 2}
--- !u!1 &8429081419068942390
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 5251454257324784754}
m_Layer: 0
m_Name: MatchNetwork
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!4 &5251454257324784754
Transform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 8429081419068942390}
serializedVersion: 2
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children:
- {fileID: 2182889149551291466}
- {fileID: 8547990667945757531}
- {fileID: 1789974153803072425}
- {fileID: 2098889495380348335}
- {fileID: 3648445760525794609}
- {fileID: 2625678984567986094}
m_Father: {fileID: 0}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!1 &9094815948999422851
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 2182889149551291466}
- component: {fileID: 8307773927879075679}
- component: {fileID: 3604854611554599773}
m_Layer: 0
m_Name: ChatService
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!4 &2182889149551291466
Transform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 9094815948999422851}
serializedVersion: 2
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 5251454257324784754}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!114 &8307773927879075679
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 9094815948999422851}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: d5a57f767e5e46a458fc5d3c628d0cbb, type: 3}
m_Name:
m_EditorClassIdentifier: Unity.Netcode.Runtime::Unity.Netcode.NetworkObject
GlobalObjectIdHash: 1681065116
InScenePlacedSourceGlobalObjectIdHash: 0
DeferredDespawnTick: 0
Ownership: 1
AlwaysReplicateAsRoot: 0
SynchronizeTransform: 1
ActiveSceneSynchronization: 0
SceneMigrationSynchronization: 0
SpawnWithObservers: 1
DontDestroyWithOwner: 0
AutoObjectParentSync: 1
SyncOwnerTransformWhenParented: 1
AllowOwnerToParent: 0
--- !u!114 &3604854611554599773
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 9094815948999422851}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 816890bb20c416b419e38d3d4b91ffca, type: 3}
m_Name:
m_EditorClassIdentifier: Assembly-CSharp::TD.Gameplay.ChatService
ShowTopMostFoldoutHeaderGroup: 1
--- !u!1 &9150709944481815107
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 2625678984567986094}
- component: {fileID: 1287556557852043940}
- component: {fileID: 4386956365137913543}
m_Layer: 0
m_Name: WaveVote
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!4 &2625678984567986094
Transform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 9150709944481815107}
serializedVersion: 2
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 38.30481, y: 0.5, z: 155.93135}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 5251454257324784754}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!114 &1287556557852043940
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 9150709944481815107}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: d5a57f767e5e46a458fc5d3c628d0cbb, type: 3}
m_Name:
m_EditorClassIdentifier: Unity.Netcode.Runtime::Unity.Netcode.NetworkObject
GlobalObjectIdHash: 3520766009
InScenePlacedSourceGlobalObjectIdHash: 0
DeferredDespawnTick: 0
Ownership: 1
AlwaysReplicateAsRoot: 0
SynchronizeTransform: 1
ActiveSceneSynchronization: 0
SceneMigrationSynchronization: 0
SpawnWithObservers: 1
DontDestroyWithOwner: 0
AutoObjectParentSync: 1
SyncOwnerTransformWhenParented: 1
AllowOwnerToParent: 0
--- !u!114 &4386956365137913543
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 9150709944481815107}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: a18321dc2d138034fbdb5f705f19f893, type: 3}
m_Name:
m_EditorClassIdentifier: Assembly-CSharp::TD.Gameplay.EnemyUpgrades.WaveVote
ShowTopMostFoldoutHeaderGroup: 1
optionsPerVote: 3

View file

@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 7116de1968afffb4990ee24f709612fb
PrefabImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View file

@ -0,0 +1,921 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!1 &216401704418653358
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 2369632775351055227}
- component: {fileID: 5469845921758518005}
- component: {fileID: 1431563384465486267}
m_Layer: 0
m_Name: HUD
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!4 &2369632775351055227
Transform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 216401704418653358}
serializedVersion: 2
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 3833381251955063088}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!114 &5469845921758518005
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 216401704418653358}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 19102, guid: 0000000000000000e000000000000000, type: 0}
m_Name:
m_EditorClassIdentifier: UnityEngine.dll::UnityEngine.UIElements.UIDocument
m_PanelSettings: {fileID: 11400000, guid: 6aa0af71585acea4db4995c3931dc946, type: 2}
m_ParentUI: {fileID: 0}
sourceAsset: {fileID: 9197481963319205126, guid: eba711c1def2b194584102ceb5517c48, type: 3}
m_SortingOrder: 0
m_Position: 0
m_WorldSpaceSizeMode: 1
m_WorldSpaceWidth: 1920
m_WorldSpaceHeight: 1080
m_PivotReferenceSize: 0
m_Pivot: 0
m_WorldSpaceCollider: {fileID: 0}
--- !u!114 &1431563384465486267
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 216401704418653358}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 1c69cd177f09f0c419288f73b49df886, type: 3}
m_Name:
m_EditorClassIdentifier: Assembly-CSharp::TD.UI.HUDController
placementController: {fileID: 7706260725108150093}
paintController: {fileID: 559798155559411408}
placementManager: {fileID: 0}
cameraController: {fileID: 5042310339586537117}
rejectionMessageDuration: 2.5
menuButtonIcon: {fileID: 0}
chatMaxHeight: 280
chatMaxMessages: 2147483647
chatSystemColor: {r: 1, g: 0.7, b: 0.2, a: 1}
chatPlayerColor: {r: 0.92, g: 0.92, b: 0.92, a: 1}
--- !u!1 &1445952844252628864
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 3833381251955063088}
- component: {fileID: 6850685008819507485}
- component: {fileID: 3524271020287320838}
- component: {fileID: 6600478213912852129}
- component: {fileID: 191939159250915621}
- component: {fileID: 7706260725108150093}
- component: {fileID: 559798155559411408}
- component: {fileID: 1988948222073846279}
- component: {fileID: 2077830364583566817}
- component: {fileID: 8797807118405570736}
- component: {fileID: 3273128039403554245}
- component: {fileID: 1661838386639436151}
- component: {fileID: 8864891455444378183}
- component: {fileID: 7594921744928559756}
- component: {fileID: 1678455881203478607}
- component: {fileID: 8637796564860235120}
- component: {fileID: 4396752746502665908}
- component: {fileID: 9162204174128098398}
- component: {fileID: 4044629536480845444}
- component: {fileID: 1951038390230828916}
m_Layer: 0
m_Name: MatchServices
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!4 &3833381251955063088
Transform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1445952844252628864}
serializedVersion: 2
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children:
- {fileID: 2369632775351055227}
- {fileID: 2545698982756849348}
- {fileID: 9174177271844320236}
- {fileID: 7648787753941018277}
m_Father: {fileID: 0}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!114 &6850685008819507485
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1445952844252628864}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: a303b690faebb0e4e930d1714afa424e, type: 3}
m_Name:
m_EditorClassIdentifier: Assembly-CSharp::TD.Gameplay.LevelLoader
level: {fileID: 0}
buildablePlaneLayerName: BuildablePlane
drawGridBounds: 1
drawWalkability: 1
drawBuildablePlane: 1
drawOwnerBorders: 1
--- !u!114 &3524271020287320838
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1445952844252628864}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: de7d013503af0f74c950f215f8dae1c0, type: 3}
m_Name:
m_EditorClassIdentifier: Assembly-CSharp::TD.Gameplay.PathfindingService
recomputeBudgetMs: 1.5
--- !u!114 &6600478213912852129
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1445952844252628864}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: dc307e7e94967894584e8e6050fc38cf, type: 3}
m_Name:
m_EditorClassIdentifier: Assembly-CSharp::TD.Gameplay.SelectionState
--- !u!114 &191939159250915621
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1445952844252628864}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: e28d10549c8d2ac4585eded3ad8d2198, type: 3}
m_Name:
m_EditorClassIdentifier: Assembly-CSharp::TD.Gameplay.SelectionVisualizer
selectionRingPrefab: {fileID: 8648063358813763958, guid: 74757e379bac0e444aabab5e388e17c6, type: 3}
yOffset: 0.02
projectionLayerMask:
serializedVersion: 2
m_Bits: 960
--- !u!114 &7706260725108150093
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1445952844252628864}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 124253f2cbc2d9f46befb6e1763cd6b9, type: 3}
m_Name:
m_EditorClassIdentifier: Assembly-CSharp::TD.Gameplay.TowerPlacementController
settings: {fileID: 11400000, guid: 18f99b4b94d13eb429d4dfb9b0b37b4b, type: 2}
buildablePlaneLayerMask:
serializedVersion: 2
m_Bits: 64
raycastMaxDistance: 500
--- !u!114 &559798155559411408
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1445952844252628864}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 6b81770471b644b3aa0e58d4a108dddd, type: 3}
m_Name:
m_EditorClassIdentifier: Assembly-CSharp::TD.Gameplay.TowerPaintController
raycastMaxDistance: 500
cursorSize: 28
--- !u!114 &1988948222073846279
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1445952844252628864}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: cc99a2b3d3e8d8178a74e1d68a34553c, type: 3}
m_Name:
m_EditorClassIdentifier: Assembly-CSharp::TD.Gameplay.BuilderSpellCastController
groundLayerMask:
serializedVersion: 2
m_Bits: 128
raycastMaxDistance: 500
aimDecal: {fileID: 0}
pointReticleDiameter: 1.5
projectionDepth: 50
--- !u!114 &2077830364583566817
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1445952844252628864}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: a31401869def6d64c87e645f1da1aa2a, type: 3}
m_Name:
m_EditorClassIdentifier: Assembly-CSharp::TD.UI.FloatingTextSpawner
prefab: {fileID: 4844901775730304629, guid: e5002bdc91043d24994f0b1cb578de56, type: 3}
goldColor: {r: 1, g: 0.84, b: 0.2, a: 1}
livesColor: {r: 0.95, g: 0.25, b: 0.25, a: 1}
verticalOffset: 2
positionJitter: 0.4
verticalJitter: 0.2
--- !u!114 &8797807118405570736
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1445952844252628864}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: e2708c6bac8136f45b48c899d3d40630, type: 3}
m_Name:
m_EditorClassIdentifier: Assembly-CSharp::TD.VFX.SellEffectSpawner
coinBurstPrefab: {fileID: 2313150535691989867, guid: 31f8d8c320b42c54d886d610fb13a1e7, type: 3}
sellSound:
clip: {fileID: 8300000, guid: 778b3d5981f4c3045a475794435f43cc, type: 3}
volume: 1
minPitch: 0.95
maxPitch: 1.05
verticalOffset: 0.75
--- !u!114 &3273128039403554245
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1445952844252628864}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 9ec21c01769ef58d1ac26ba89668eeaa, type: 3}
m_Name:
m_EditorClassIdentifier: Assembly-CSharp::TD.Audio.AudioManager
categories:
- category: 0
maxVoices: 6
volume: 0.5
- category: 1
maxVoices: 4
volume: 0.5
- category: 2
maxVoices: 4
volume: 0.5
--- !u!82 &1661838386639436151
AudioSource:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1445952844252628864}
m_Enabled: 1
serializedVersion: 4
OutputAudioMixerGroup: {fileID: 0}
m_audioClip: {fileID: 0}
m_Resource: {fileID: 0}
m_PlayOnAwake: 1
m_Volume: 1
m_Pitch: 1
Loop: 0
Mute: 0
Spatialize: 0
SpatializePostEffects: 0
Priority: 128
DopplerLevel: 1
MinDistance: 1
MaxDistance: 500
Pan2D: 0
rolloffMode: 0
BypassEffects: 0
BypassListenerEffects: 0
BypassReverbZones: 0
rolloffCustomCurve:
serializedVersion: 2
m_Curve:
- serializedVersion: 3
time: 0
value: 1
inSlope: 0
outSlope: 0
tangentMode: 0
weightedMode: 0
inWeight: 0.33333334
outWeight: 0.33333334
- serializedVersion: 3
time: 1
value: 0
inSlope: 0
outSlope: 0
tangentMode: 0
weightedMode: 0
inWeight: 0.33333334
outWeight: 0.33333334
m_PreInfinity: 2
m_PostInfinity: 2
m_RotationOrder: 4
panLevelCustomCurve:
serializedVersion: 2
m_Curve:
- serializedVersion: 3
time: 0
value: 0
inSlope: 0
outSlope: 0
tangentMode: 0
weightedMode: 0
inWeight: 0.33333334
outWeight: 0.33333334
m_PreInfinity: 2
m_PostInfinity: 2
m_RotationOrder: 4
spreadCustomCurve:
serializedVersion: 2
m_Curve:
- serializedVersion: 3
time: 0
value: 0
inSlope: 0
outSlope: 0
tangentMode: 0
weightedMode: 0
inWeight: 0.33333334
outWeight: 0.33333334
m_PreInfinity: 2
m_PostInfinity: 2
m_RotationOrder: 4
reverbZoneMixCustomCurve:
serializedVersion: 2
m_Curve:
- serializedVersion: 3
time: 0
value: 1
inSlope: 0
outSlope: 0
tangentMode: 0
weightedMode: 0
inWeight: 0.33333334
outWeight: 0.33333334
m_PreInfinity: 2
m_PostInfinity: 2
m_RotationOrder: 4
--- !u!114 &8864891455444378183
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1445952844252628864}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 292ad37b9e4d1d7c28e59cf4dc1e99fa, type: 3}
m_Name:
m_EditorClassIdentifier: Assembly-CSharp::TD.Audio.MusicPlayer
clip: {fileID: 8300000, guid: d4626563ad3c90179b247207e7b9f0be, type: 3}
volume: 0.632
--- !u!114 &7594921744928559756
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1445952844252628864}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 6226c6e672c5b358a9cdc2a1bee47daf, type: 3}
m_Name:
m_EditorClassIdentifier: Assembly-CSharp::TD.Gameplay.BuilderEffects.BuilderEffectPool
effects:
- {fileID: 11400000, guid: 419fac0976a3e0a20827beff81826161, type: 2}
--- !u!114 &1678455881203478607
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1445952844252628864}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 36a7be1b3824682b8a183fbcf8564652, type: 3}
m_Name:
m_EditorClassIdentifier: Assembly-CSharp::TD.Gameplay.BuilderSpells.BuilderSpellPool
spells:
- {fileID: 11400000, guid: 051c48f416e5366b59d49b0e062333ac, type: 2}
- {fileID: 11400000, guid: d893f46b037536b4ba91307e184cecfe, type: 2}
- {fileID: 11400000, guid: 364edf6abc3c31e33b53d0a01142b8ab, type: 2}
--- !u!114 &8637796564860235120
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1445952844252628864}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 4409eb25a8be555de8dc3a73d4ed95db, type: 3}
m_Name:
m_EditorClassIdentifier: Assembly-CSharp::TD.Gameplay.EnemyAbilities.EnemyAbilityPool
abilities:
- {fileID: 11400000, guid: 0305468bc8f912d429a8b20bf23f947c, type: 2}
- {fileID: 11400000, guid: 030f10c9a264e544ea3e0f4fdb4171bd, type: 2}
- {fileID: 11400000, guid: cb2ec6bb70f57d0429b99b7cd4c7db73, type: 2}
- {fileID: 11400000, guid: a18691a20d75f17479a83b3e171759e7, type: 2}
- {fileID: 11400000, guid: c64c7121b0456bb4a873acec9e0abf91, type: 2}
--- !u!114 &4396752746502665908
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1445952844252628864}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: c4d98a01d9b9a7b438a6f91b0b7f498f, type: 3}
m_Name:
m_EditorClassIdentifier: Assembly-CSharp::TD.Gameplay.EnemyUpgrades.EnemyUpgradePool
options:
- {fileID: 11400000, guid: eef28ca7b8844504e9d75ff903a3bb61, type: 2}
- {fileID: 11400000, guid: 46e2a6a59c0a3034f9fa44681d9ba422, type: 2}
- {fileID: 11400000, guid: 80fb851b702ff304183cb6e89c4aa245, type: 2}
- {fileID: 11400000, guid: a67aa3644d2ad4847a47040f53fea26f, type: 2}
- {fileID: 11400000, guid: 37d761966a349ba4cb4141f913224d5c, type: 2}
- {fileID: 11400000, guid: f6bfb033a15b6d0419587b80a8286bd1, type: 2}
--- !u!114 &9162204174128098398
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1445952844252628864}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 5a7e1f3c9d2b4867a1c0e4f8b3d6792a, type: 3}
m_Name:
m_EditorClassIdentifier: Assembly-CSharp::TD.Gameplay.Draft.DraftPool
options:
- {fileID: 11400000, guid: 6b1d3f8a2c5e4097b8a1d0c3e6f9b240, type: 2}
- {fileID: 11400000, guid: f4a2b8d1c6e93f05a7b2d4c8e1f6a930, type: 2}
- {fileID: 11400000, guid: 0b9f4905752751f44b9959b84b620266, type: 2}
- {fileID: 11400000, guid: 65ff25c1c8a89f7df8e88f71968c1c98, type: 2}
- {fileID: 11400000, guid: 5f390de69ba43b2a6a0d89cf09580320, type: 2}
- {fileID: 11400000, guid: 29b35841f7e5db454903798cb5d83434, type: 2}
- {fileID: 11400000, guid: d180030b68a9a5e1da497bf6f83451d4, type: 2}
--- !u!114 &4044629536480845444
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1445952844252628864}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 9d3a7c5e1b8f42606a2d4e9f1c7b3850, type: 3}
m_Name:
m_EditorClassIdentifier: Assembly-CSharp::TD.Gameplay.Draft.DraftService
optionsPerDraft: 3
rerollCost: 50
--- !u!114 &1951038390230828916
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1445952844252628864}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 468de2ce61e73ce4595ba4792874b076, type: 3}
m_Name:
m_EditorClassIdentifier: Assembly-CSharp::TD.Dev.DevWaveControls
hotkey: 94
grantTowerHotkey: 101
skipToBossHotkey: 100
--- !u!1 &2434986514412567376
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 9174177271844320236}
- component: {fileID: 5042310339586537117}
m_Layer: 0
m_Name: CameraRig
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!4 &9174177271844320236
Transform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 2434986514412567376}
serializedVersion: 2
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children:
- {fileID: 7042465335914098461}
m_Father: {fileID: 3833381251955063088}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!114 &5042310339586537117
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 2434986514412567376}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 1e670a36689801e428873c5712ea3679, type: 3}
m_Name:
m_EditorClassIdentifier: Assembly-CSharp::TD.Gameplay.CameraController
cameraChild: {fileID: 5566698628747533439}
basePanSpeed: 12
edgePanMarginPixels: 16
edgePanEnabled: 1
minDollyDistance: 5
maxDollyDistance: 50
startDollyDistance: 25
zoomSpeed: 3
cursorAnchoredZoom: 1
minPitchDegrees: 30
maxPitchDegrees: 75
startPitchDegrees: 50
pitchSpeed: 4
--- !u!1 &2936049029850371745
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 7042465335914098461}
- component: {fileID: 5566698628747533439}
- component: {fileID: 9062733689301865828}
- component: {fileID: 7984992879034746998}
m_Layer: 0
m_Name: Main Camera
m_TagString: MainCamera
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!4 &7042465335914098461
Transform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 2936049029850371745}
serializedVersion: 2
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 9174177271844320236}
m_LocalEulerAnglesHint: {x: 45, y: -90, z: 0}
--- !u!20 &5566698628747533439
Camera:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 2936049029850371745}
m_Enabled: 1
serializedVersion: 2
m_ClearFlags: 1
m_BackGroundColor: {r: 0.19215687, g: 0.3019608, b: 0.4745098, a: 0}
m_projectionMatrixMode: 1
m_GateFitMode: 2
m_FOVAxisMode: 0
m_Iso: 200
m_ShutterSpeed: 0.005
m_Aperture: 16
m_FocusDistance: 10
m_FocalLength: 50
m_BladeCount: 5
m_Curvature: {x: 2, y: 11}
m_BarrelClipping: 0.25
m_Anamorphism: 0
m_SensorSize: {x: 36, y: 24}
m_LensShift: {x: 0, y: 0}
m_NormalizedViewPortRect:
serializedVersion: 2
x: 0
y: 0
width: 1
height: 1
near clip plane: 0.3
far clip plane: 1000
field of view: 60
orthographic: 0
orthographic size: 5
m_Depth: -1
m_CullingMask:
serializedVersion: 2
m_Bits: 4294967295
m_RenderingPath: -1
m_TargetTexture: {fileID: 0}
m_TargetDisplay: 0
m_TargetEye: 3
m_HDR: 1
m_AllowMSAA: 1
m_AllowDynamicResolution: 0
m_ForceIntoRT: 0
m_OcclusionCulling: 1
m_StereoConvergence: 10
m_StereoSeparation: 0.022
--- !u!81 &9062733689301865828
AudioListener:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 2936049029850371745}
m_Enabled: 1
--- !u!114 &7984992879034746998
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 2936049029850371745}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: a79441f348de89743a2939f4d699eac1, type: 3}
m_Name:
m_EditorClassIdentifier:
m_RenderShadows: 1
m_RequiresDepthTextureOption: 2
m_RequiresOpaqueTextureOption: 2
m_CameraType: 0
m_Cameras: []
m_RendererIndex: -1
m_VolumeLayerMask:
serializedVersion: 2
m_Bits: 1
m_VolumeTrigger: {fileID: 0}
m_VolumeFrameworkUpdateModeOption: 2
m_RenderPostProcessing: 1
m_Antialiasing: 0
m_AntialiasingQuality: 2
m_StopNaN: 0
m_Dithering: 0
m_ClearDepth: 1
m_AllowXRRendering: 1
m_AllowHDROutput: 1
m_UseScreenCoordOverride: 0
m_ScreenSizeOverride: {x: 0, y: 0, z: 0, w: 0}
m_ScreenCoordScaleBias: {x: 0, y: 0, z: 0, w: 0}
m_RequiresDepthTexture: 0
m_RequiresColorTexture: 0
m_TaaSettings:
m_Quality: 3
m_FrameInfluence: 0.1
m_JitterScale: 1
m_MipBias: 0
m_VarianceClampScale: 0.9
m_ContrastAdaptiveSharpening: 0
m_Version: 2
--- !u!1 &3584612494616813666
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 2545698982756849348}
- component: {fileID: 9037991996416952042}
- component: {fileID: 6848311735055501230}
m_Layer: 0
m_Name: DebugConsole
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!4 &2545698982756849348
Transform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 3584612494616813666}
serializedVersion: 2
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: -52.51635, y: 0, z: -27.6311}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 3833381251955063088}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!114 &9037991996416952042
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 3584612494616813666}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 19102, guid: 0000000000000000e000000000000000, type: 0}
m_Name:
m_EditorClassIdentifier: UnityEngine.dll::UnityEngine.UIElements.UIDocument
m_PanelSettings: {fileID: 11400000, guid: 6aa0af71585acea4db4995c3931dc946, type: 2}
m_ParentUI: {fileID: 0}
sourceAsset: {fileID: 0}
m_SortingOrder: 1
m_Position: 0
m_WorldSpaceSizeMode: 1
m_WorldSpaceWidth: 1920
m_WorldSpaceHeight: 1080
m_PivotReferenceSize: 0
m_Pivot: 0
m_WorldSpaceCollider: {fileID: 0}
--- !u!114 &6848311735055501230
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 3584612494616813666}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 7c6a808e8291552d0b64e36a210879c4, type: 3}
m_Name:
m_EditorClassIdentifier: Assembly-CSharp::TD.Dev.DebugConsole
toggleKey: 4
--- !u!1 &5697898299420166755
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 7648787753941018277}
- component: {fileID: 4271007794402033964}
- component: {fileID: 1710569614611447900}
m_Layer: 0
m_Name: Directional Light
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!4 &7648787753941018277
Transform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 5697898299420166755}
serializedVersion: 2
m_LocalRotation: {x: 0.0000014901161, y: 0.9063078, z: -0.42261824, w: 0.00000295043}
m_LocalPosition: {x: -17, y: 3, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 3833381251955063088}
m_LocalEulerAnglesHint: {x: 50, y: 180, z: 0}
--- !u!108 &4271007794402033964
Light:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 5697898299420166755}
m_Enabled: 1
serializedVersion: 13
m_Type: 1
m_Color: {r: 0.8156863, g: 0.9098039, b: 1, a: 1}
m_Intensity: 0.65
m_Range: 10
m_SpotAngle: 30
m_InnerSpotAngle: 21.80208
m_CookieSize2D: {x: 10, y: 10}
m_Shadows:
m_Type: 2
m_Resolution: -1
m_CustomResolution: -1
m_Strength: 1
m_Bias: 0.05
m_NormalBias: 0.4
m_NearPlane: 0.2
m_CullingMatrixOverride:
e00: 1
e01: 0
e02: 0
e03: 0
e10: 0
e11: 1
e12: 0
e13: 0
e20: 0
e21: 0
e22: 1
e23: 0
e30: 0
e31: 0
e32: 0
e33: 1
m_UseCullingMatrixOverride: 0
m_Cookie: {fileID: 0}
m_DrawHalo: 0
m_Flare: {fileID: 0}
m_RenderMode: 0
m_CullingMask:
serializedVersion: 2
m_Bits: 4294967295
m_RenderingLayerMask: 1
m_Lightmapping: 4
m_LightShadowCasterMode: 0
m_AreaSize: {x: 1, y: 1}
m_BounceIntensity: 1
m_ColorTemperature: 7191
m_UseColorTemperature: 1
m_BoundingSphereOverride: {x: 0, y: 0, z: 0, w: 0}
m_UseBoundingSphereOverride: 0
m_UseViewFrustumForShadowCasterCull: 1
m_ForceVisible: 0
m_ShapeRadius: 0
m_ShadowAngle: 0
m_LightUnit: 1
m_LuxAtDistance: 1
m_EnableSpotReflector: 1
--- !u!114 &1710569614611447900
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 5697898299420166755}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 474bcb49853aa07438625e644c072ee6, type: 3}
m_Name:
m_EditorClassIdentifier:
m_UsePipelineSettings: 1
m_AdditionalLightsShadowResolutionTier: 2
m_CustomShadowLayers: 0
m_LightCookieSize: {x: 1, y: 1}
m_LightCookieOffset: {x: 0, y: 0}
m_SoftShadowQuality: 1
m_RenderingLayersMask:
serializedVersion: 0
m_Bits: 1
m_ShadowRenderingLayersMask:
serializedVersion: 0
m_Bits: 1
m_Version: 4
m_LightLayerMask: 1
m_ShadowLayerMask: 1
m_RenderingLayers: 1
m_ShadowRenderingLayers: 1

View file

@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 13be8328f6275e14da787f25c94683ee
PrefabImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 4ca1d0054e9d3744e8802817bf0b69e2
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 11400000
userData:
assetBundleName:
assetBundleVariant:

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 128de09101c3e3240b094d29d1eb9d22
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View file

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: ee9f119ba54c4b14d83c6818dc2f0ef8
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 15600000
userData:
assetBundleName:
assetBundleVariant:

BIN
Assets/_Project/Scenes/Levels/3Player_Thumbnail.png (Stored with Git LFS) Normal file

Binary file not shown.

View file

@ -0,0 +1,117 @@
fileFormatVersion: 2
guid: 7e33bd8556d93f04eb4f5c38006a7880
TextureImporter:
internalIDToNameTable: []
externalObjects: {}
serializedVersion: 13
mipmaps:
mipMapMode: 0
enableMipMap: 0
sRGBTexture: 1
linearTexture: 0
fadeOut: 0
borderMipMap: 0
mipMapsPreserveCoverage: 0
alphaTestReferenceValue: 0.5
mipMapFadeDistanceStart: 1
mipMapFadeDistanceEnd: 3
bumpmap:
convertToNormalMap: 0
externalNormalMap: 0
heightScale: 0.25
normalMapFilter: 0
flipGreenChannel: 0
isReadable: 0
streamingMipmaps: 0
streamingMipmapsPriority: 0
vTOnly: 0
ignoreMipmapLimit: 0
grayScaleToAlpha: 0
generateCubemap: 6
cubemapConvolution: 0
seamlessCubemap: 0
textureFormat: 1
maxTextureSize: 2048
textureSettings:
serializedVersion: 2
filterMode: 1
aniso: 1
mipBias: 0
wrapU: 1
wrapV: 1
wrapW: 1
nPOTScale: 0
lightmap: 0
compressionQuality: 50
spriteMode: 1
spriteExtrude: 1
spriteMeshType: 1
alignment: 0
spritePivot: {x: 0.5, y: 0.5}
spritePixelsToUnits: 100
spriteBorder: {x: 0, y: 0, z: 0, w: 0}
spriteGenerateFallbackPhysicsShape: 1
alphaUsage: 1
alphaIsTransparency: 1
spriteTessellationDetail: -1
textureType: 8
textureShape: 1
singleChannelComponent: 0
flipbookRows: 1
flipbookColumns: 1
maxTextureSizeSet: 0
compressionQualitySet: 0
textureFormatSet: 0
ignorePngGamma: 0
applyGammaDecoding: 0
swizzle: 50462976
cookieLightType: 0
platformSettings:
- serializedVersion: 4
buildTarget: DefaultTexturePlatform
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 4
buildTarget: Standalone
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
spriteSheet:
serializedVersion: 2
sprites: []
outline: []
customData:
physicsShape: []
bones: []
spriteID: 5e97eb03825dee720800000000000000
internalID: 0
vertices: []
indices:
edges: []
weights: []
secondaryTextures: []
spriteCustomMetadata:
entries: []
nameFileIdTable: {}
mipmapLimitGroupName:
pSDRemoveMatte: 0
userData:
assetBundleName:
assetBundleVariant:

View file

@ -638,6 +638,7 @@ MonoBehaviour:
maps: maps:
- {fileID: 11400000, guid: e65cce766e92f604f85ab939cf374abd, type: 2} - {fileID: 11400000, guid: e65cce766e92f604f85ab939cf374abd, type: 2}
- {fileID: 11400000, guid: 9cc56fbc3ae460a4b862f8510fdf5f09, type: 2} - {fileID: 11400000, guid: 9cc56fbc3ae460a4b862f8510fdf5f09, type: 2}
- {fileID: 11400000, guid: 4ca1d0054e9d3744e8802817bf0b69e2, type: 2}
--- !u!4 &976902011 --- !u!4 &976902011
Transform: Transform:
m_ObjectHideFlags: 0 m_ObjectHideFlags: 0

View file

@ -203,6 +203,31 @@ namespace TD.Core
Restricted = 2, Restricted = 2,
} }
/// <summary>
/// Whether a map restricts tower placement to each player's own zone, or lets everyone build
/// anywhere on the map's buildable area.
/// </summary>
/// <remarks>
/// <para>This governs placement PERMISSION only. Zone ownership keeps doing everything else it
/// does today under both modes — leak attribution, camera start position, builder spawn point,
/// minimap tinting. A shared-build map still has distinct player territories; they are simply
/// all buildable by everyone.</para>
///
/// <para>Backed by byte with <c>OwnerOnly = 0</c> so <c>LevelData</c> assets baked before this
/// existed deserialize to the pre-existing behaviour without needing a re-bake.</para>
///
/// <para>Tower OWNERSHIP is unaffected: a tower belongs to whoever paid for it, and sell /
/// upgrade stay owner-gated in both modes.</para>
/// </remarks>
public enum BuildAccess : byte
{
/// <summary>A player may only build on tiles covered by their own PlayerZoneVolume.</summary>
OwnerOnly = 0,
/// <summary>Any player may build on any Buildable tile, whichever zone owns it.</summary>
Shared = 1,
}
/// <summary> /// <summary>
/// Outcome of a bake operation, recorded on the baked <see cref="TD.Levels.LevelData"/> asset. /// Outcome of a bake operation, recorded on the baked <see cref="TD.Levels.LevelData"/> asset.
/// </summary> /// </summary>

View file

@ -802,17 +802,33 @@ namespace TD.Levels.Editor
} }
// P5-8: zones with neither outgoing leak nor goal adjacency. // P5-8: zones with neither outgoing leak nor goal adjacency.
var zonesWithLeaks = new HashSet<PlayerSlot>(); //
foreach (var l in ctx.LeakExitVolumes) zonesWithLeaks.Add(l.sourceZone); // OwnerOnly maps only. The rule asserts the lane-chain topology those maps are built
// around: each player's zone either hands off to the next or is the final defender.
foreach (var owner in ctx.DeclaredOwners) //
// On a Shared map that assertion is wrong, not merely unnecessary. Zones there are
// typically directly adjacent with no gate between them, so most have no outgoing leak
// and are not goal-adjacent — and that is the intended shape, because the whole map is
// one contiguous maze. The property P5-8 actually protects ("enemies from this zone have
// somewhere to go") is enforced more strictly by P5-4 above, which runs a real BFS from
// every spawner to the exit set — and with no leak volumes present, that exit set is the
// goal tiles, which is exactly the right requirement. Zones with no spawners can't
// strand anything: WaveManager skips them.
if (ctx.Authoring.buildAccess == BuildAccess.OwnerOnly)
{ {
bool hasLeak = zonesWithLeaks.Contains(owner); var zonesWithLeaks = new HashSet<PlayerSlot>();
bool isGoalAdj = goalAdjacentZones.Contains(owner); foreach (var l in ctx.LeakExitVolumes) zonesWithLeaks.Add(l.sourceZone);
if (!hasLeak && !isGoalAdj)
foreach (var owner in ctx.DeclaredOwners)
{ {
report.Error("P5-8", $"Player zone {owner} has no outgoing leak exit AND is not goal-adjacent. " + bool hasLeak = zonesWithLeaks.Contains(owner);
"Every zone must either lead somewhere or be a final defender."); bool isGoalAdj = goalAdjacentZones.Contains(owner);
if (!hasLeak && !isGoalAdj)
{
report.Error("P5-8", $"Player zone {owner} has no outgoing leak exit AND is not goal-adjacent. " +
"Every zone must either lead somewhere or be a final defender. " +
"(This rule is skipped on Shared-build maps.)");
}
} }
} }
@ -919,6 +935,7 @@ namespace TD.Levels.Editor
data.PlayerCount = ctx.Authoring.playerCount; data.PlayerCount = ctx.Authoring.playerCount;
data.MapDescription = ctx.Authoring.mapDescription; data.MapDescription = ctx.Authoring.mapDescription;
data.Author = ctx.Authoring.author; data.Author = ctx.Authoring.author;
data.BuildAccess = ctx.Authoring.buildAccess;
data.GridOriginTile = ctx.GridOriginTile; data.GridOriginTile = ctx.GridOriginTile;
data.GridSize = ctx.GridSize; data.GridSize = ctx.GridSize;
@ -1152,7 +1169,10 @@ namespace TD.Levels.Editor
sb.Append("playerCount=").Append(ctx.Authoring.playerCount.ToString(inv)).Append('|'); sb.Append("playerCount=").Append(ctx.Authoring.playerCount.ToString(inv)).Append('|');
sb.Append("expectedGoalCount=").Append(ctx.Authoring.expectedGoalCount.ToString(inv)).Append('|'); sb.Append("expectedGoalCount=").Append(ctx.Authoring.expectedGoalCount.ToString(inv)).Append('|');
sb.Append("mapDescription=").Append(ctx.Authoring.mapDescription ?? "").Append('|'); sb.Append("mapDescription=").Append(ctx.Authoring.mapDescription ?? "").Append('|');
sb.Append("author=").Append(ctx.Authoring.author ?? "").Append('\n'); sb.Append("author=").Append(ctx.Authoring.author ?? "").Append('|');
// By NAME, matching the per-volume enum convention below — a future reordering of the
// enum must not silently produce a matching hash for different rules.
sb.Append("buildAccess=").Append(ctx.Authoring.buildAccess.ToString()).Append('\n');
// Volume blocks, ordered by canonical path. // Volume blocks, ordered by canonical path.
var rootTransform = ctx.Authoring.transform; var rootTransform = ctx.Authoring.transform;
@ -1354,6 +1374,7 @@ namespace TD.Levels.Editor
target.PlayerCount = src.PlayerCount; target.PlayerCount = src.PlayerCount;
target.MapDescription = src.MapDescription; target.MapDescription = src.MapDescription;
target.Author = src.Author; target.Author = src.Author;
target.BuildAccess = src.BuildAccess;
target.ScenePath = src.ScenePath; target.ScenePath = src.ScenePath;
target.AuthoringHash = src.AuthoringHash; target.AuthoringHash = src.AuthoringHash;
target.LastBakeTimestamp = src.LastBakeTimestamp; target.LastBakeTimestamp = src.LastBakeTimestamp;

View file

@ -0,0 +1,356 @@
// Assets/_Project/Scripts/Editor/Levels/MapCoreExtractor.cs
using System.Collections.Generic;
using UnityEditor;
using UnityEditor.SceneManagement;
using UnityEditorInternal;
using UnityEngine;
using UnityEngine.SceneManagement;
using TD.Audio;
using TD.Dev;
using TD.Gameplay;
using TD.Gameplay.BuilderEffects;
using TD.Gameplay.BuilderSpells;
using TD.Gameplay.Draft;
using TD.Gameplay.EnemyAbilities;
using TD.Gameplay.EnemyUpgrades;
using TD.Gameplay.Waves;
using TD.UI;
using TD.VFX;
namespace TD.Levels.Editor
{
/// <summary>
/// One-time (re-runnable) utility that lifts every map-independent scene object out of a
/// reference match scene and bakes it into the two prefabs <c>TD/New Map…</c> instantiates.
/// </summary>
/// <remarks>
/// <para><b>Why extract instead of generating from scratch?</b> Several of these components hold
/// ORDER-SENSITIVE arrays that double as network identity — <c>EnemyUpgradePool.options</c>
/// indices are card network ids, and <c>TowerPlacementManager.towerDefinitions</c> indices are
/// <c>TowerTypeId</c>. Re-deriving those from an AssetDatabase scan would produce a different
/// order than the shipping map and desync peers. Copying the live components preserves them
/// exactly.</para>
///
/// <para><b>Two prefabs, two lifetimes.</b> <c>MatchServices</c> contains no NetworkObject, so
/// new maps keep it as a LINKED prefab instance and later edits propagate everywhere.
/// <c>MatchNetwork</c> holds the six scene-placed NetworkBehaviours and is UNPACKED on use, so
/// what NGO sees in a generated scene is indistinguishable from a hand-authored one.</para>
///
/// <para><b>This does not modify the reference scene's contents</b> — temporary objects are
/// created and destroyed — but Unity still marks the scene dirty. Don't save it afterwards.</para>
/// </remarks>
public static class MapCoreExtractor
{
private const string PrefabFolder = "Assets/_Project/Prefabs/MatchCore";
private const string ServicesPrefabPath = PrefabFolder + "/MatchServices.prefab";
private const string NetworkPrefabPath = PrefabFolder + "/MatchNetwork.prefab";
// Plain (non-networked, no UIDocument) singleton services, collapsed onto the
// _MatchServices ROOT GameObject in this order.
//
// Order matters in exactly one place: AudioSource must precede MusicPlayer, whose
// [RequireComponent(typeof(AudioSource))] would otherwise auto-add a second, default-valued
// AudioSource before we get a chance to copy the authored one.
private static readonly System.Type[] RootServiceTypes =
{
typeof(LevelLoader),
typeof(PathfindingService),
typeof(SelectionState),
typeof(SelectionVisualizer),
typeof(TowerPlacementController),
typeof(TowerPaintController),
typeof(BuilderSpellCastController),
typeof(FloatingTextSpawner),
typeof(SellEffectSpawner),
typeof(AudioManager),
typeof(AudioSource), // must come before MusicPlayer — see note above
typeof(MusicPlayer),
typeof(BuilderEffectPool),
typeof(BuilderSpellPool),
typeof(EnemyAbilityPool),
typeof(EnemyUpgradePool),
typeof(DraftPool),
typeof(DraftService),
typeof(DevWaveControls),
};
// Scene-placed NetworkBehaviours. Each keeps its own GameObject and its own NetworkObject —
// they are only re-parented under a shared root, so network topology is unchanged.
private static readonly System.Type[] NetworkServiceTypes =
{
typeof(ChatService),
typeof(MatchState),
typeof(RunState),
typeof(TowerPlacementManager),
typeof(WaveManager),
typeof(WaveVote),
};
// -------------------------------------------------------------------
// Menu entry
// -------------------------------------------------------------------
[MenuItem("TD/Map Pipeline/Build Core Prefabs From Open Scene", false, 200)]
public static void BuildCorePrefabs()
{
Scene scene = EditorSceneManager.GetActiveScene();
if (!scene.IsValid() || string.IsNullOrEmpty(scene.path))
{
EditorUtility.DisplayDialog(
"No saved scene open",
"Open a fully-wired match scene (9Player) before running this. The extractor " +
"reads its components to build the core prefabs.",
"OK");
return;
}
// Pre-scan so a missing service aborts before we create anything.
if (!TryResolveSources(scene, out var rootSources, out var missing))
{
EditorUtility.DisplayDialog(
"Reference scene is incomplete",
$"'{scene.name}' is missing {missing.Count} component(s) the core prefabs need:\n\n" +
string.Join("\n", missing) +
"\n\nOpen a complete match scene (9Player) and try again.",
"OK");
return;
}
bool proceed = EditorUtility.DisplayDialog(
"Build core prefabs",
$"Read '{scene.name}' and (re)write:\n\n" +
$" {ServicesPrefabPath}\n" +
$" {NetworkPrefabPath}\n\n" +
"The scene's own contents are not changed, but Unity will mark it dirty. " +
"Don't save it afterwards.\n\nProceed?",
"Build Prefabs",
"Cancel");
if (!proceed) return;
MapTemplate.EnsureFolder(PrefabFolder);
GameObject servicesPrefab = BuildServicesPrefab(scene, rootSources);
GameObject networkPrefab = BuildNetworkPrefab(scene);
var template = MapTemplate.LoadOrCreate();
template.matchServicesPrefab = servicesPrefab;
template.matchNetworkPrefab = networkPrefab;
CaptureTerrainDefaults(scene, template);
EditorUtility.SetDirty(template);
AssetDatabase.SaveAssets();
AssetDatabase.Refresh();
Debug.Log(
$"[MapPipeline] Core prefabs built from '{scene.name}'.\n" +
$" MatchServices: {RootServiceTypes.Length} components on the root + HUD / DebugConsole / " +
$"CameraRig / lighting children.\n" +
$" MatchNetwork: {NetworkServiceTypes.Length} NetworkBehaviours under one root.\n" +
$" Template: {MapTemplate.AssetPath}\n" +
$" '{scene.name}' is now marked dirty by the temporary objects — do NOT save it.");
EditorGUIUtility.PingObject(template);
}
// -------------------------------------------------------------------
// Source resolution
// -------------------------------------------------------------------
// Resolves every root-service component up front. Returns false (with a human-readable list)
// if anything is absent, so we never write a half-populated prefab.
private static bool TryResolveSources(Scene scene, out List<Component> rootSources,
out List<string> missing)
{
rootSources = new List<Component>(RootServiceTypes.Length);
missing = new List<string>();
// AudioSource is resolved from MusicPlayer's GameObject rather than "first in scene",
// so we copy the music source's authored volume rather than some unrelated emitter.
var musicPlayer = FindOne(scene, typeof(MusicPlayer)) as MusicPlayer;
foreach (var type in RootServiceTypes)
{
Component found = type == typeof(AudioSource)
? (musicPlayer != null ? musicPlayer.GetComponent<AudioSource>() : null)
: FindOne(scene, type);
if (found == null) missing.Add(" • " + type.Name);
rootSources.Add(found);
}
foreach (var type in NetworkServiceTypes)
{
if (FindOne(scene, type) == null) missing.Add(" • " + type.Name);
}
if (FindOne(scene, typeof(HUDController)) == null) missing.Add(" • HUDController");
if (FindOne(scene, typeof(CameraController)) == null) missing.Add(" • CameraController");
return missing.Count == 0;
}
// Scans the scene's root objects rather than Object.FindObjectsByType: the FindObjectsByType
// overload set has churned across Unity 6.x (FindObjectsSortMode deprecation), and walking
// roots is stable across all of them.
private static Component FindOne(Scene scene, System.Type type)
{
foreach (var root in scene.GetRootGameObjects())
{
var found = root.GetComponentInChildren(type, true);
if (found != null) return found;
}
return null;
}
private static Light FindDirectionalLight(Scene scene)
{
foreach (var root in scene.GetRootGameObjects())
{
foreach (var light in root.GetComponentsInChildren<Light>(true))
{
if (light.type == LightType.Directional) return light;
}
}
return null;
}
// -------------------------------------------------------------------
// MatchServices prefab
// -------------------------------------------------------------------
private static GameObject BuildServicesPrefab(Scene scene, List<Component> rootSources)
{
var root = new GameObject("_MatchServices");
// Flatten every plain service onto the root. CopyComponent/PasteComponentAsNew carries
// all serialized values, including the order-sensitive asset arrays.
for (int i = 0; i < rootSources.Count; i++)
{
var src = rootSources[i];
if (src == null) continue;
ComponentUtility.CopyComponent(src);
ComponentUtility.PasteComponentAsNew(root);
}
// Components that need their own GameObject: UIDocument is [DisallowMultipleComponent],
// so HUD and DebugConsole can't share one; CameraRig needs an independently-moving
// transform with the Camera as its child.
GameObject hud = CloneUnder(FindOne(scene, typeof(HUDController)), root, "HUD");
CloneUnder(FindOne(scene, typeof(DebugConsole)), root, "DebugConsole");
GameObject cameraRig = CloneUnder(FindOne(scene, typeof(CameraController)), root, "CameraRig");
CloneUnder(FindDirectionalLight(scene), root, "Directional Light");
// Post-processing volume is optional — 9Player doesn't have one, Main does.
CloneUnder(FindOne(scene, typeof(UnityEngine.Rendering.Volume)), root, "Global Volume");
RewireHudReferences(hud, root, cameraRig);
ClearBakedLevelReference(root);
var prefab = PrefabUtility.SaveAsPrefabAsset(root, ServicesPrefabPath);
Object.DestroyImmediate(root);
return prefab;
}
// Instantiate() remaps references that point INSIDE the copied hierarchy but leaves
// references that point outside it aimed at the original scene objects. Saving a prefab
// silently nulls those, so re-aim them at the prefab-internal equivalents first.
private static void RewireHudReferences(GameObject hud, GameObject root, GameObject cameraRig)
{
if (hud == null) return;
var controller = hud.GetComponent<HUDController>();
if (controller == null) return;
var so = new SerializedObject(controller);
SetRef(so, "placementController", root.GetComponent<TowerPlacementController>());
SetRef(so, "paintController", root.GetComponent<TowerPaintController>());
SetRef(so, "cameraController",
cameraRig != null ? cameraRig.GetComponent<CameraController>() : null);
// TowerPlacementManager lives in the OTHER prefab (it's a NetworkBehaviour), so this
// reference cannot be satisfied here. HUDController already falls back to
// TowerPlacementManager.Instance and retries per-frame until the object spawns, so
// leaving it null is the correct wiring, not a gap.
SetRef(so, "placementManager", null);
so.ApplyModifiedPropertiesWithoutUndo();
}
// The reference scene's LevelLoader points at that map's LevelData. The prefab is map-
// independent, so blank it — NewMapBuilder sets it per-instance as a prefab override.
private static void ClearBakedLevelReference(GameObject root)
{
var loader = root.GetComponent<LevelLoader>();
if (loader == null) return;
var so = new SerializedObject(loader);
SetRef(so, "level", null);
so.ApplyModifiedPropertiesWithoutUndo();
}
// -------------------------------------------------------------------
// MatchNetwork prefab
// -------------------------------------------------------------------
private static GameObject BuildNetworkPrefab(Scene scene)
{
var root = new GameObject("_MatchNetwork");
foreach (var type in NetworkServiceTypes)
{
CloneUnder(FindOne(scene, type), root, type.Name);
}
var prefab = PrefabUtility.SaveAsPrefabAsset(root, NetworkPrefabPath);
Object.DestroyImmediate(root);
return prefab;
}
// -------------------------------------------------------------------
// Terrain defaults
// -------------------------------------------------------------------
private static void CaptureTerrainDefaults(Scene scene, MapTemplate template)
{
var terrain = FindOne(scene, typeof(Terrain)) as Terrain;
if (terrain == null) return;
if (terrain.materialTemplate != null) template.terrainMaterial = terrain.materialTemplate;
if (terrain.terrainData != null && terrain.terrainData.terrainLayers != null &&
terrain.terrainData.terrainLayers.Length > 0)
{
template.terrainLayers = terrain.terrainData.terrainLayers;
}
}
// -------------------------------------------------------------------
// Helpers
// -------------------------------------------------------------------
// Clones the component's whole GameObject subtree and parents it under root.
// Returns null (silently) when source is null — callers treat those as optional.
private static GameObject CloneUnder(Component source, GameObject root, string name)
{
if (source == null) return null;
var clone = Object.Instantiate(source.gameObject);
clone.name = name;
clone.transform.SetParent(root.transform, worldPositionStays: false);
return clone;
}
private static void SetRef(SerializedObject so, string propertyPath, Object value)
{
var prop = so.FindProperty(propertyPath);
if (prop == null)
{
Debug.LogWarning($"[MapPipeline] Serialized field '{propertyPath}' not found on " +
$"{so.targetObject.GetType().Name}. It was probably renamed — " +
"update MapCoreExtractor.");
return;
}
prop.objectReferenceValue = value;
}
}
}

View file

@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: d1b948bcbbe8d8b4788f2056372280b3

View file

@ -0,0 +1,292 @@
// Assets/_Project/Scripts/Editor/Levels/MapTemplate.cs
using UnityEditor;
using UnityEngine;
namespace TD.Levels.Editor
{
/// <summary>
/// Editor-only settings asset that drives <c>TD/New Map…</c>. Holds the two core prefabs
/// produced by <see cref="MapCoreExtractor"/> plus every default the new-map generator needs,
/// so tuning the starter layout is an inspector edit rather than a code change.
/// </summary>
/// <remarks>
/// Lives in the Editor assembly deliberately — nothing at runtime reads it, and keeping the
/// type editor-only stops it from being pulled into a player build.
///
/// The asset is created on demand at <see cref="AssetPath"/>. If it goes missing, both
/// <c>TD/New Map…</c> and the extractor recreate it with defaults; only the prefab references
/// and captured terrain settings are lost, and re-running the extractor restores those.
/// </remarks>
public class MapTemplate : ScriptableObject
{
public const string AssetPath = "Assets/_Project/Settings/MapTemplate.asset";
// -------------------------------------------------------------------
// Core prefabs. Populated by MapCoreExtractor; consumed by NewMapBuilder.
// -------------------------------------------------------------------
[Header("Core Prefabs (built by TD/Map Pipeline/Build Core Prefabs)")]
[Tooltip("Every map-independent, non-networked service collapsed onto one GameObject " +
"(plus HUD / DebugConsole / CameraRig / light children). Instantiated as a LINKED " +
"prefab instance so edits here propagate to every map.")]
public GameObject matchServicesPrefab;
[Tooltip("The six scene-placed NetworkBehaviours, parented under one root. Instantiated " +
"and immediately UNPACKED so the scene contains plain GameObjects — network " +
"topology stays byte-identical to hand-authored maps.")]
public GameObject matchNetworkPrefab;
// -------------------------------------------------------------------
// Output locations.
// -------------------------------------------------------------------
[Header("Output Locations")]
[Tooltip("Folder new map scenes, their LevelData, and their TerrainData are written to. " +
"Drag the folder itself here — an asset reference follows renames and moves, " +
"which a path string does not.")]
public DefaultAsset sceneFolderAsset;
[Tooltip("Scene holding the MapRegistry that the lobby's map browser reads. New maps are " +
"appended to its Maps array automatically.")]
public SceneAsset mainMenuScene;
// Legacy path strings, superseded by the asset references above. Kept so an existing
// template migrates itself (see MigratePathsToAssets) and as a last resort if a reference
// is cleared. Hidden so there's exactly one obvious place to edit.
[HideInInspector] public string sceneFolder = "Assets/_Project/Scenes/Levels";
[HideInInspector] public string mainMenuScenePath = "Assets/_Project/Scenes/UI/MainMenu.unity";
/// <summary>
/// Folder new maps are written to. Prefers <see cref="sceneFolderAsset"/>; falls back to the
/// legacy path string if the reference is unset or doesn't point at a folder.
/// </summary>
public string SceneFolderPath
{
get
{
if (sceneFolderAsset != null)
{
string path = AssetDatabase.GetAssetPath(sceneFolderAsset);
if (AssetDatabase.IsValidFolder(path)) return path;
Debug.LogWarning($"[MapPipeline] MapTemplate's Scene Folder Asset points at " +
$"'{path}', which is not a folder. Falling back to '{sceneFolder}'.");
}
return sceneFolder;
}
}
/// <summary>
/// Scene containing the MapRegistry. Prefers <see cref="mainMenuScene"/>; falls back to the
/// legacy path string. Returns empty if neither resolves.
/// </summary>
public string MainMenuScenePath
{
get
{
if (mainMenuScene != null)
{
string path = AssetDatabase.GetAssetPath(mainMenuScene);
if (!string.IsNullOrEmpty(path)) return path;
}
return mainMenuScenePath ?? string.Empty;
}
}
// -------------------------------------------------------------------
// Terrain defaults. Captured from the reference scene by the extractor.
// -------------------------------------------------------------------
[Header("Terrain Defaults")]
[Tooltip("Material applied to new terrain. Captured from the reference scene's Terrain.")]
public Material terrainMaterial;
[Tooltip("Terrain layers (splat textures) applied to new terrain.")]
public TerrainLayer[] terrainLayers;
[Tooltip("Heightmap resolution. Must be 2^n + 1 (129, 257, 513, 1025…).")]
public int terrainHeightmapResolution = 513;
[Tooltip("Total vertical range of the terrain in world units.")]
public float terrainHeight = 60f;
[Tooltip("How far the terrain extends past the map bounds on each side, in tiles. Gives " +
"the camera something to look at beyond the playable area.")]
public int terrainSkirtTiles = 20;
[Tooltip("Terrain is created flattened to this height above its own base, and its transform " +
"is dropped by the same amount — so the flat surface lands exactly on the buildable " +
"plane (Y=0) while leaving headroom to carve DOWN as well as up. Set 0 to match the " +
"older 9Player setup (surface at the very bottom of the height range).")]
public float terrainCarveDepth = 5f;
// -------------------------------------------------------------------
// Starter layout. All values in tiles.
// -------------------------------------------------------------------
[Header("Starter Layout (tiles)")]
[Tooltip("Default map width for the New Map dialog.")]
public int defaultMapWidth = 60;
[Tooltip("Default map height for the New Map dialog.")]
public int defaultMapHeight = 70;
[Tooltip("Gap between the map edge (MapAreaVolume) and the gameplay volumes. The map area " +
"must fully contain every gameplay volume, and the buffer gives the camera room " +
"to pan past the playfield.")]
public int playAreaMargin = 4;
[Tooltip("Depth (north-south) of the GoalVolume.")]
public int goalDepth = 5;
[Tooltip("Width (east-west) of the GoalVolume.")]
public int goalWidth = 10;
[Tooltip("Depth of each SpawnerVolume, measured from the south edge of its owner's band.")]
public int spawnerDepth = 3;
[Tooltip("Width of each SpawnerVolume.")]
public int spawnerWidth = 10;
[Tooltip("Width of the one-tile-deep LeakExitVolume gate between adjacent player bands.")]
public int leakGateWidth = 8;
[Tooltip("Smallest acceptable north-south depth for a player band. The New Map dialog " +
"refuses to generate a layout thinner than this and tells you the map height needed.")]
public int minBandDepth = 8;
// -------------------------------------------------------------------
// Loading
// -------------------------------------------------------------------
/// <summary>
/// Returns the template asset, creating it with defaults if it doesn't exist yet.
/// Never returns null.
/// </summary>
public static MapTemplate LoadOrCreate()
{
var existing = AssetDatabase.LoadAssetAtPath<MapTemplate>(AssetPath);
if (existing != null)
{
existing.MigratePathsToAssets();
return existing;
}
EnsureFolder(System.IO.Path.GetDirectoryName(AssetPath).Replace("\\", "/"));
var created = CreateInstance<MapTemplate>();
AssetDatabase.CreateAsset(created, AssetPath);
created.MigratePathsToAssets();
AssetDatabase.SaveAssets();
Debug.Log($"[MapPipeline] Created default map template at '{AssetPath}'.");
return created;
}
/// <summary>
/// Upgrades a template that still stores locations as path strings, resolving each to an
/// asset reference. Idempotent and cheap — safe to call on every load.
/// </summary>
/// <remarks>
/// If the recorded path no longer resolves (the folder was renamed out from under it, which
/// is exactly the failure the asset references exist to prevent), falls back to searching
/// the project for a uniquely-named match. An ambiguous match is left unresolved on purpose:
/// guessing which of several same-named scenes holds the MapRegistry would be worse than
/// asking.
/// </remarks>
private void MigratePathsToAssets()
{
bool changed = false;
if (sceneFolderAsset == null && !string.IsNullOrEmpty(sceneFolder)
&& AssetDatabase.IsValidFolder(sceneFolder))
{
sceneFolderAsset = AssetDatabase.LoadAssetAtPath<DefaultAsset>(sceneFolder);
changed |= sceneFolderAsset != null;
}
if (mainMenuScene == null && !string.IsNullOrEmpty(mainMenuScenePath))
{
var scene = AssetDatabase.LoadAssetAtPath<SceneAsset>(mainMenuScenePath);
if (scene == null)
{
string wanted = System.IO.Path.GetFileNameWithoutExtension(mainMenuScenePath);
scene = FindUniqueSceneNamed(wanted);
if (scene != null)
{
Debug.Log($"[MapPipeline] MapTemplate's recorded Main Menu path " +
$"'{mainMenuScenePath}' no longer exists; relinked to " +
$"'{AssetDatabase.GetAssetPath(scene)}'.");
}
}
if (scene != null)
{
mainMenuScene = scene;
changed = true;
}
}
if (changed)
{
EditorUtility.SetDirty(this);
AssetDatabase.SaveAssets();
}
}
// Returns the one SceneAsset whose file name matches exactly, or null if there are zero or
// several. Name search only — the caller has already failed to resolve by path.
private static SceneAsset FindUniqueSceneNamed(string sceneName)
{
if (string.IsNullOrEmpty(sceneName)) return null;
string[] guids = AssetDatabase.FindAssets($"{sceneName} t:SceneAsset");
SceneAsset first = null;
int matches = 0;
foreach (string guid in guids)
{
string path = AssetDatabase.GUIDToAssetPath(guid);
if (!string.Equals(System.IO.Path.GetFileNameWithoutExtension(path), sceneName,
System.StringComparison.OrdinalIgnoreCase))
{
continue; // FindAssets does substring matching; we want an exact file name.
}
matches++;
if (first == null) first = AssetDatabase.LoadAssetAtPath<SceneAsset>(path);
}
if (matches > 1)
{
Debug.LogWarning($"[MapPipeline] Found {matches} scenes named '{sceneName}'. " +
"Assign Main Menu Scene on MapTemplate manually.");
return null;
}
return first;
}
/// <summary>
/// Creates <paramref name="folder"/> and any missing ancestors. Path is asset-relative and
/// uses forward slashes ("Assets/_Project/Prefabs/MatchCore"). No-op if it already exists.
/// </summary>
public static void EnsureFolder(string folder)
{
if (string.IsNullOrEmpty(folder) || AssetDatabase.IsValidFolder(folder)) return;
string[] parts = folder.Split('/');
// parts[0] is always "Assets" for a valid asset path; walk down creating as needed.
string running = parts[0];
for (int i = 1; i < parts.Length; i++)
{
string next = running + "/" + parts[i];
if (!AssetDatabase.IsValidFolder(next))
{
AssetDatabase.CreateFolder(running, parts[i]);
}
running = next;
}
}
}
}

View file

@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: fb7355881928ea146a26471078f207c2

View file

@ -0,0 +1,578 @@
// Assets/_Project/Scripts/Editor/Levels/NewMapBuilder.cs
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
using UnityEditor;
using UnityEditor.SceneManagement;
using UnityEngine;
using UnityEngine.SceneManagement;
using TD.Core;
using TD.Gameplay;
namespace TD.Levels.Editor
{
/// <summary>
/// Everything the <c>TD/New Map…</c> command does once the designer presses Create: build the
/// scene, generate a starter volume layout that bakes clean, create and wire the LevelData, and
/// perform the three registration steps that otherwise fail silently (Build Settings, the
/// LevelLoader reference, and the lobby's MapRegistry).
/// </summary>
/// <remarks>
/// The generated scene has four roots — <c>_LevelAuthoring</c>, <c>_MatchServices</c>,
/// <c>_MatchNetwork</c>, <c>Terrain</c> — replacing the ~30 the hand-authored maps carry.
/// </remarks>
public static class NewMapBuilder
{
// The physics layer terrain must sit on. Builder.SampleTerrainY and
// BuilderSpellCastController both raycast against it; terrain left on Default silently
// falls back to Y=0 and the builder walks through hills instead of over them.
private const string TerrainLayerName = "TerrainGeometry";
// Vertical size of every authoring volume's BoxCollider. Any value works so long as the
// bounds straddle Y=0 (bake rule P2-18); 1 unit centred on zero matches 9Player.
private const float VolumeHeight = 1f;
private static readonly Regex ValidName = new Regex(@"^[A-Za-z0-9_]+$");
// -------------------------------------------------------------------
// Request
// -------------------------------------------------------------------
public class Request
{
public string MapName = "NewMap";
public int PlayerCount = 3;
public int MapWidth = 60;
public int MapHeight = 70;
public string Author = "";
public string Description = "";
public BuildAccess BuildAccess = BuildAccess.OwnerOnly;
public bool CreateTerrain = true;
public bool AddToBuildSettings = true;
public bool AddToMapRegistry = true;
public bool BakeImmediately = true;
}
// -------------------------------------------------------------------
// Starter layout
// -------------------------------------------------------------------
/// <summary>
/// Tile rectangles for a generated starter map. Zones are stacked south-to-north as
/// full-width bands; enemies spawn at the south edge of their owner's band and travel north
/// through a one-tile leak gate into the next player's band, with the goal north of the
/// final band.
/// </summary>
public struct Layout
{
public RectInt MapArea;
public RectInt[] Zones; // index 0 = Player1
public RectInt[] Spawners; // parallel to Zones
public RectInt[] Leaks; // Zones.Length - 1 entries; Leaks[i] runs zone i -> zone i+1
public RectInt Goal;
public int BandDepth;
}
/// <summary>
/// Computes the starter layout, or explains why the requested dimensions can't hold one.
/// Pure — safe to call every OnGUI frame for live validation.
/// </summary>
public static bool TryComputeLayout(MapTemplate template, int mapWidth, int mapHeight,
int playerCount, BuildAccess buildAccess,
out Layout layout, out string error)
{
layout = default;
error = null;
// OwnerOnly maps get a one-tile leak gate between bands: it is the only walkable
// connection, which is what forces enemies through a chokepoint and makes the handoff
// between lanes legible. Shared maps skip gates entirely and butt the bands together —
// the whole map is one maze, so a gate would just be an arbitrary pinch, and with no
// LeakExitVolume the gap row would not be walkable at all.
bool useLeakGates = buildAccess == BuildAccess.OwnerOnly;
int gapRows = useLeakGates ? playerCount - 1 : 0;
int margin = Mathf.Max(0, template.playAreaMargin);
int playX0 = margin;
int playY0 = margin;
int playW = mapWidth - margin * 2;
int playH = mapHeight - margin * 2;
int widestFeature = Mathf.Max(template.goalWidth,
Mathf.Max(template.spawnerWidth, template.leakGateWidth));
if (playW < widestFeature)
{
error = $"Map is too narrow. The play area is {playW} tiles wide after the " +
$"{margin}-tile margin, but the widest starter feature needs {widestFeature}. " +
$"Use a width of at least {widestFeature + margin * 2}.";
return false;
}
// Vertical budget: N bands + the gate rows (if any) + the goal.
int budget = playH - gapRows - template.goalDepth;
int bandDepth = playerCount > 0 ? budget / playerCount : 0;
int minBand = Mathf.Max(template.minBandDepth, template.spawnerDepth + 1);
if (bandDepth < minBand)
{
int required = playerCount * minBand + gapRows
+ template.goalDepth + margin * 2;
error = $"Map is too shallow for {playerCount} player band(s). Each band would be " +
$"{Mathf.Max(bandDepth, 0)} tiles deep; the minimum is {minBand}. " +
$"Use a height of at least {required}.";
return false;
}
layout.MapArea = new RectInt(0, 0, mapWidth, mapHeight);
layout.BandDepth = bandDepth;
layout.Zones = new RectInt[playerCount];
layout.Spawners = new RectInt[playerCount];
layout.Leaks = new RectInt[useLeakGates ? Mathf.Max(0, playerCount - 1) : 0];
int y = playY0;
for (int i = 0; i < playerCount; i++)
{
layout.Zones[i] = new RectInt(playX0, y, playW, bandDepth);
// Spawner sits at the south edge of its own band, so enemies always have the full
// band depth to walk through before reaching the exit (bake rule P5-5 wants it
// inside the owner's zone).
int spawnW = Mathf.Min(template.spawnerWidth, playW);
layout.Spawners[i] = new RectInt(
CenteredStart(playX0, playW, spawnW), y, spawnW, template.spawnerDepth);
y += bandDepth;
if (useLeakGates && i < playerCount - 1)
{
// One-tile gate wedged between two bands: its south neighbours are band i and
// its north neighbours are band i+1, satisfying both adjacency rules (P5-6/P5-7).
int gateW = Mathf.Min(template.leakGateWidth, playW);
layout.Leaks[i] = new RectInt(
CenteredStart(playX0, playW, gateW), y, gateW, 1);
y += 1;
}
}
int goalW = Mathf.Min(template.goalWidth, playW);
layout.Goal = new RectInt(
CenteredStart(playX0, playW, goalW), y, goalW, template.goalDepth);
return true;
}
private static int CenteredStart(int areaStart, int areaWidth, int featureWidth)
{
return areaStart + (areaWidth - featureWidth) / 2;
}
// -------------------------------------------------------------------
// Creation
// -------------------------------------------------------------------
/// <summary>
/// Creates the map. Returns false with a human-readable reason on any precondition failure;
/// nothing is written to disk in that case.
/// </summary>
public static bool Create(Request request, out string error)
{
error = null;
var template = MapTemplate.LoadOrCreate();
if (!ValidatePreconditions(request, template, out error)) return false;
if (!TryComputeLayout(template, request.MapWidth, request.MapHeight,
request.PlayerCount, request.BuildAccess,
out Layout layout, out error))
{
return false;
}
string folder = template.SceneFolderPath;
string scenePath = $"{folder}/{request.MapName}.unity";
string dataPath = $"{folder}/{request.MapName}.asset";
// Give the designer a chance to save whatever they had open — NewScene(Single) would
// otherwise discard it.
if (!EditorSceneManager.SaveCurrentModifiedScenesIfUserWantsTo())
{
error = "Cancelled while saving the currently open scene.";
return false;
}
MapTemplate.EnsureFolder(folder);
Scene scene = EditorSceneManager.NewScene(NewSceneSetup.EmptyScene, NewSceneMode.Single);
// ---- Core objects -------------------------------------------
var services = (GameObject)PrefabUtility.InstantiatePrefab(template.matchServicesPrefab);
services.name = "_MatchServices";
var network = (GameObject)PrefabUtility.InstantiatePrefab(template.matchNetworkPrefab);
network.name = "_MatchNetwork";
// Unpack so the six NetworkObjects are plain scene objects. NGO treats in-scene placed
// NetworkObjects that are prefab instances differently from loose ones; unpacking keeps
// generated maps structurally identical to the hand-authored ones.
PrefabUtility.UnpackPrefabInstance(
network, PrefabUnpackMode.Completely, InteractionMode.AutomatedAction);
// ---- LevelData ----------------------------------------------
var levelData = ScriptableObject.CreateInstance<LevelData>();
AssetDatabase.CreateAsset(levelData, dataPath);
// ---- Authoring hierarchy ------------------------------------
LevelAuthoring authoring = BuildAuthoring(request, layout, levelData);
// ---- Terrain -------------------------------------------------
if (request.CreateTerrain)
{
CreateTerrain(request, template);
}
// ---- Wire the runtime loader ---------------------------------
// LevelLoader lives inside the linked MatchServices prefab; assigning here records a
// per-instance override, which is exactly the intent — the asset differs per map.
WireLevelLoader(services, levelData);
// ---- Save + register -----------------------------------------
EditorSceneManager.SaveScene(scene, scenePath);
var notes = new List<string>();
if (request.AddToBuildSettings) notes.Add(RegisterInBuildSettings(scenePath));
if (request.AddToMapRegistry) notes.Add(RegisterInMapRegistry(template, levelData));
// Bake last: it stamps LevelData.ScenePath from the ACTIVE scene, so the scene has to
// be saved (and therefore have a path) before this runs.
if (request.BakeImmediately)
{
notes.Add(LevelBakePipeline.Bake(authoring)
? "Baked LevelData (see console for the bake report)."
: "Bake FAILED — see console. The scene and assets were still created.");
}
AssetDatabase.SaveAssets();
AssetDatabase.Refresh();
Selection.activeGameObject = authoring.gameObject;
if (SceneView.lastActiveSceneView != null) SceneView.lastActiveSceneView.FrameSelected();
Debug.Log(
$"[MapPipeline] Created map '{request.MapName}' " +
$"({request.PlayerCount} player(s), {request.MapWidth}×{request.MapHeight} tiles, " +
$"band depth {layout.BandDepth}).\n" +
$" Scene: {scenePath}\n" +
$" LevelData: {dataPath}\n" +
string.Join("\n", notes.Where(n => !string.IsNullOrEmpty(n)).Select(n => " " + n)));
return true;
}
private static bool ValidatePreconditions(Request request, MapTemplate template,
out string error)
{
error = null;
if (string.IsNullOrWhiteSpace(request.MapName) || !ValidName.IsMatch(request.MapName))
{
error = "Map name must be letters, digits and underscores only — it becomes the " +
"scene file name and the string NGO uses to load the scene.";
return false;
}
if (template.matchServicesPrefab == null || template.matchNetworkPrefab == null)
{
error = "Core prefabs are missing. Open the 9Player scene and run " +
"TD → Map Pipeline → Build Core Prefabs From Open Scene first.";
return false;
}
string folder = template.SceneFolderPath;
if (string.IsNullOrEmpty(folder))
{
error = "MapTemplate has no output folder. Assign Scene Folder Asset on " +
MapTemplate.AssetPath + ".";
return false;
}
string scenePath = $"{folder}/{request.MapName}.unity";
if (System.IO.File.Exists(scenePath))
{
error = $"A scene already exists at '{scenePath}'. Pick a different name.";
return false;
}
string dataPath = $"{folder}/{request.MapName}.asset";
if (System.IO.File.Exists(dataPath))
{
error = $"A LevelData asset already exists at '{dataPath}'. Pick a different name.";
return false;
}
return true;
}
// -------------------------------------------------------------------
// Authoring hierarchy
// -------------------------------------------------------------------
private static LevelAuthoring BuildAuthoring(Request request, Layout layout,
LevelData levelData)
{
var rootGO = new GameObject("_LevelAuthoring");
var authoring = rootGO.AddComponent<LevelAuthoring>();
authoring.targetAsset = levelData;
authoring.mapName = request.MapName;
authoring.playerCount = request.PlayerCount;
authoring.expectedGoalCount = 1;
authoring.mapDescription = request.Description ?? "";
authoring.author = request.Author ?? "";
authoring.buildAccess = request.BuildAccess;
// Default every gizmo category on. A designer opening a fresh map wants to SEE the
// starter layout without hunting for toggles; turning them off is the deliberate act.
authoring.alwaysShowPlayerZones = true;
authoring.alwaysShowSpawners = true;
authoring.alwaysShowLeakExits = true;
authoring.alwaysShowGoals = true;
authoring.alwaysShowMapArea = true;
CreateVolume<MapAreaVolume>("MapArea", rootGO.transform, layout.MapArea);
var goal = CreateVolume<GoalVolume>("Goal", rootGO.transform, layout.Goal);
goal.GetComponent<GoalVolume>().placementValidity = PlacementValidity.Invalid;
for (int i = 0; i < request.PlayerCount; i++)
{
int playerNumber = i + 1;
var slot = (PlayerSlot)(byte)playerNumber;
var group = new GameObject($"Player {playerNumber}");
group.transform.SetParent(rootGO.transform, worldPositionStays: false);
var zone = CreateVolume<PlayerZoneVolume>(
$"Player{playerNumber}Zone", group.transform, layout.Zones[i]);
var zoneVolume = zone.GetComponent<PlayerZoneVolume>();
zoneVolume.owner = slot;
zoneVolume.placementValidity = PlacementValidity.Allowed;
var spawn = CreateVolume<SpawnerVolume>(
$"Player{playerNumber}Spawn", group.transform, layout.Spawners[i]);
var spawnVolume = spawn.GetComponent<SpawnerVolume>();
spawnVolume.owner = slot;
spawnVolume.spawnerIdInZone = 0;
spawnVolume.spawnFacing = Direction.North; // bands run south -> north
spawnVolume.placementValidity = PlacementValidity.Invalid;
// The northernmost band has no leak — it's the final defender, and its zone is
// goal-adjacent instead (bake rule P5-8).
if (i < layout.Leaks.Length)
{
var leak = CreateVolume<LeakExitVolume>(
$"Player{playerNumber}Leak", group.transform, layout.Leaks[i]);
var leakVolume = leak.GetComponent<LeakExitVolume>();
leakVolume.sourceZone = slot;
leakVolume.target = (PlayerSlot)(byte)(playerNumber + 1);
leakVolume.weight = 1f;
leakVolume.placementValidity = PlacementValidity.Invalid;
}
}
return authoring;
}
// Builds one authoring volume covering the given tile rectangle. Tiles are edge-aligned, so
// a rect spanning tiles [x0..x1] occupies world X [x0, x1+1] — which is exactly
// RectInt.xMin..RectInt.xMax. Geometry goes on the BoxCollider (centre zero, size = extent)
// with the transform at the volume's centre, matching the existing maps and keeping
// VolumeEditTool's edge-drag behaviour predictable.
private static GameObject CreateVolume<T>(string name, Transform parent, RectInt tiles)
where T : VolumeBase
{
var go = new GameObject(name);
go.transform.SetParent(parent, worldPositionStays: false);
go.transform.localPosition = new Vector3(
(tiles.xMin + tiles.xMax) * 0.5f * GridCoordinates.TILE_SIZE,
0f,
(tiles.yMin + tiles.yMax) * 0.5f * GridCoordinates.TILE_SIZE);
// Added before the volume component so VolumeBase's [RequireComponent] doesn't add a
// second, default-sized collider.
var collider = go.AddComponent<BoxCollider>();
collider.center = Vector3.zero;
collider.size = new Vector3(
tiles.width * GridCoordinates.TILE_SIZE,
VolumeHeight,
tiles.height * GridCoordinates.TILE_SIZE);
go.AddComponent<T>();
return go;
}
// -------------------------------------------------------------------
// Terrain
// -------------------------------------------------------------------
private static void CreateTerrain(Request request, MapTemplate template)
{
int skirt = Mathf.Max(0, template.terrainSkirtTiles);
float carve = Mathf.Clamp(template.terrainCarveDepth, 0f, template.terrainHeight);
var data = new TerrainData { name = request.MapName + "_TerrainData" };
// Resolution before size: changing heightmapResolution resets size.
data.heightmapResolution = template.terrainHeightmapResolution;
data.size = new Vector3(
request.MapWidth + skirt * 2f,
Mathf.Max(1f, template.terrainHeight),
request.MapHeight + skirt * 2f);
if (template.terrainLayers != null && template.terrainLayers.Length > 0)
{
data.terrainLayers = template.terrainLayers;
}
// Start the surface partway up its own height range and drop the transform to match, so
// the flat ground lands on the buildable plane (Y=0) with room to carve downwards.
if (carve > 0f)
{
int res = data.heightmapResolution;
float normalized = carve / Mathf.Max(1f, template.terrainHeight);
var heights = new float[res, res];
for (int y = 0; y < res; y++)
{
for (int x = 0; x < res; x++) heights[y, x] = normalized;
}
data.SetHeights(0, 0, heights);
}
AssetDatabase.CreateAsset(
data, $"{template.SceneFolderPath}/{request.MapName}_TerrainData.asset");
var terrainGO = Terrain.CreateTerrainGameObject(data);
terrainGO.name = "Terrain";
terrainGO.transform.position = new Vector3(-skirt, -carve, -skirt);
int layer = LayerMask.NameToLayer(TerrainLayerName);
if (layer >= 0)
{
terrainGO.layer = layer;
}
else
{
Debug.LogWarning(
$"[MapPipeline] Layer '{TerrainLayerName}' does not exist, so the new terrain was " +
"left on Default. The builder's ground raycast and spell targeting both filter " +
"on that layer and will ignore this terrain until it's fixed.");
}
if (template.terrainMaterial != null)
{
terrainGO.GetComponent<Terrain>().materialTemplate = template.terrainMaterial;
}
}
// -------------------------------------------------------------------
// Wiring and registration
// -------------------------------------------------------------------
private static void WireLevelLoader(GameObject services, LevelData levelData)
{
var loader = services.GetComponentInChildren<LevelLoader>(true);
if (loader == null)
{
Debug.LogError("[MapPipeline] MatchServices prefab has no LevelLoader — the map " +
"will not load at runtime. Re-run Build Core Prefabs.");
return;
}
var so = new SerializedObject(loader);
var prop = so.FindProperty("level");
if (prop == null)
{
Debug.LogError("[MapPipeline] LevelLoader has no serialized field 'level'. It was " +
"renamed — update NewMapBuilder.");
return;
}
prop.objectReferenceValue = levelData;
so.ApplyModifiedPropertiesWithoutUndo();
}
private static string RegisterInBuildSettings(string scenePath)
{
var scenes = EditorBuildSettings.scenes.ToList();
if (scenes.Any(s => s.path == scenePath))
{
return "Already in Build Settings.";
}
scenes.Add(new EditorBuildSettingsScene(scenePath, true));
EditorBuildSettings.scenes = scenes.ToArray();
return $"Added to Build Settings (index {scenes.Count - 1}).";
}
// MapRegistry lives in the MainMenu scene, not the map scene — the one registration step
// that isn't visible from where the designer is working, and therefore the one most often
// forgotten. Open it additively, append, save, close.
private static string RegisterInMapRegistry(MapTemplate template, LevelData levelData)
{
string path = template.MainMenuScenePath;
if (string.IsNullOrEmpty(path) || !System.IO.File.Exists(path))
{
return $"SKIPPED MapRegistry — no scene at '{path}'. Assign Main Menu Scene on " +
$"{MapTemplate.AssetPath}, then add this LevelData to the registry by hand.";
}
Scene menu = default;
bool opened = false;
try
{
menu = EditorSceneManager.OpenScene(path, OpenSceneMode.Additive);
opened = true;
MapRegistry registry = null;
foreach (var root in menu.GetRootGameObjects())
{
registry = root.GetComponentInChildren<MapRegistry>(true);
if (registry != null) break;
}
if (registry == null)
{
return $"SKIPPED MapRegistry — none found in '{menu.name}'. Add the LevelData by hand.";
}
var so = new SerializedObject(registry);
var maps = so.FindProperty("maps");
if (maps == null)
{
return "SKIPPED MapRegistry — serialized field 'maps' not found (renamed?).";
}
for (int i = 0; i < maps.arraySize; i++)
{
if (maps.GetArrayElementAtIndex(i).objectReferenceValue == levelData)
{
return "Already in MapRegistry.";
}
}
maps.InsertArrayElementAtIndex(maps.arraySize);
maps.GetArrayElementAtIndex(maps.arraySize - 1).objectReferenceValue = levelData;
so.ApplyModifiedPropertiesWithoutUndo();
EditorSceneManager.MarkSceneDirty(menu);
EditorSceneManager.SaveScene(menu);
return $"Added to MapRegistry in '{menu.name}' (slot {maps.arraySize - 1}).";
}
finally
{
if (opened && menu.IsValid())
{
EditorSceneManager.CloseScene(menu, removeScene: true);
}
}
}
}
}

View file

@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 5eda54745c5c78c489eb2cf9aa6265b7

View file

@ -0,0 +1,205 @@
// Assets/_Project/Scripts/Editor/Levels/NewMapWindow.cs
using UnityEditor;
using UnityEngine;
using TD.Core;
namespace TD.Levels.Editor
{
/// <summary>
/// The <c>TD/New Map…</c> dialog. Collects the handful of decisions only a designer can make,
/// validates the resulting layout live, and hands off to <see cref="NewMapBuilder"/>.
/// </summary>
public class NewMapWindow : EditorWindow
{
// Mirrors LevelBakePipeline's accepted set — anything else is a hard bake error (P2-15),
// so there's no point offering it here.
private static readonly int[] PlayerCountValues = { 1, 2, 3, 4, 5, 9 };
private static readonly string[] PlayerCountLabels =
{ "1 player", "2 players", "3 players", "4 players", "5 players", "9 players" };
private const string AuthorPrefKey = "TD.MapPipeline.LastAuthor";
private readonly NewMapBuilder.Request request = new NewMapBuilder.Request();
private bool initialized;
private Vector2 scroll;
[MenuItem("TD/New Map...", false, 100)]
public static void Open()
{
var window = GetWindow<NewMapWindow>(utility: true, title: "New Map", focus: true);
window.minSize = new Vector2(420f, 460f);
window.Show();
}
private void Initialize()
{
if (initialized) return;
initialized = true;
var template = MapTemplate.LoadOrCreate();
request.MapWidth = template.defaultMapWidth;
request.MapHeight = template.defaultMapHeight;
request.Author = EditorPrefs.GetString(AuthorPrefKey, "");
}
private void OnGUI()
{
Initialize();
var template = MapTemplate.LoadOrCreate();
scroll = EditorGUILayout.BeginScrollView(scroll);
bool prefabsReady = template.matchServicesPrefab != null
&& template.matchNetworkPrefab != null;
if (!prefabsReady)
{
EditorGUILayout.HelpBox(
"Core prefabs haven't been built yet.\n\n" +
"Open the 9Player scene, then run:\n" +
"TD → Map Pipeline → Build Core Prefabs From Open Scene",
MessageType.Error);
EditorGUILayout.Space(6);
}
EditorGUILayout.LabelField("Identity", EditorStyles.boldLabel);
request.MapName = EditorGUILayout.TextField(
new GUIContent("Map Name",
"Becomes the scene file name, the LevelData asset name, and the string NGO " +
"uses to load the scene. Letters, digits and underscores only."),
request.MapName);
request.Author = EditorGUILayout.TextField("Author", request.Author);
EditorGUILayout.LabelField(new GUIContent("Description",
"Shown in the lobby's map browser. Empty produces a bake warning."));
request.Description = EditorGUILayout.TextArea(
request.Description, GUILayout.Height(38f));
EditorGUILayout.Space(8);
EditorGUILayout.LabelField("Shape", EditorStyles.boldLabel);
request.PlayerCount = EditorGUILayout.IntPopup(
"Players", request.PlayerCount, PlayerCountLabels, PlayerCountValues);
request.MapWidth = Mathf.Max(8, EditorGUILayout.IntField(
new GUIContent("Width (tiles)", "1 tile = 1 world unit."), request.MapWidth));
request.MapHeight = Mathf.Max(8, EditorGUILayout.IntField(
new GUIContent("Height (tiles)", "1 tile = 1 world unit."), request.MapHeight));
request.BuildAccess = (BuildAccess)EditorGUILayout.EnumPopup(
new GUIContent("Build Access",
"OwnerOnly: each player builds only in their own zone.\n" +
"Shared: everyone builds anywhere on the map's buildable area.\n\n" +
"Changeable later on _LevelAuthoring — it just needs a re-bake."),
request.BuildAccess);
if (request.BuildAccess == BuildAccess.Shared)
{
EditorGUILayout.HelpBox(
"Shared: every band forms one cooperative maze. Zones still define " +
"territory for leak attribution, camera start and builder spawn. Towers stay " +
"owned by whoever paid — only that player can sell or upgrade them.",
MessageType.None);
}
EditorGUILayout.Space(8);
EditorGUILayout.LabelField("On Create", EditorStyles.boldLabel);
request.CreateTerrain = EditorGUILayout.Toggle(
new GUIContent("Create Terrain",
"Adds a Terrain sized to the map plus a skirt, on the TerrainGeometry layer, " +
"flattened onto the buildable plane."),
request.CreateTerrain);
request.AddToBuildSettings = EditorGUILayout.Toggle(
new GUIContent("Add to Build Settings",
"Required — NGO cannot load a scene that isn't in the build list."),
request.AddToBuildSettings);
request.AddToMapRegistry = EditorGUILayout.Toggle(
new GUIContent("Add to MapRegistry",
"Opens the MainMenu scene, appends the LevelData to the lobby's map browser, " +
"and saves it."),
request.AddToMapRegistry);
request.BakeImmediately = EditorGUILayout.Toggle(
new GUIContent("Bake immediately",
"Runs the bake on the generated starter layout so the map is valid and " +
"playable the moment it's created."),
request.BakeImmediately);
// Registry wiring is the one step that reaches into a scene the designer isn't looking
// at, so a broken reference should be visible BEFORE Create, not in the summary after.
if (request.AddToMapRegistry && !System.IO.File.Exists(template.MainMenuScenePath))
{
EditorGUILayout.HelpBox(
"Main Menu Scene isn't resolving, so the new map won't be added to the lobby's " +
"map browser. Assign it on MapTemplate, or add the LevelData to MapRegistry.Maps " +
"by hand afterwards.",
MessageType.Warning);
}
EditorGUILayout.Space(8);
DrawLayoutPreview(template);
EditorGUILayout.EndScrollView();
EditorGUILayout.Space(6);
bool canCreate = prefabsReady &&
NewMapBuilder.TryComputeLayout(
template, request.MapWidth, request.MapHeight,
request.PlayerCount, request.BuildAccess, out _, out _);
using (new EditorGUI.DisabledScope(!canCreate))
{
if (GUILayout.Button("Create Map", GUILayout.Height(30f)))
{
CreateMap();
}
}
}
private void DrawLayoutPreview(MapTemplate template)
{
EditorGUILayout.LabelField("Starter Layout", EditorStyles.boldLabel);
if (!NewMapBuilder.TryComputeLayout(template, request.MapWidth, request.MapHeight,
request.PlayerCount, request.BuildAccess,
out var layout, out string error))
{
EditorGUILayout.HelpBox(error, MessageType.Error);
return;
}
string routing = layout.Leaks.Length > 0
? $"through {layout.Leaks.Length} one-tile leak gate(s) " +
$"{Mathf.Min(template.leakGateWidth, layout.Zones[0].width)} tiles wide"
: "across directly-adjacent band borders (no leak gates)";
EditorGUILayout.HelpBox(
$"{request.PlayerCount} full-width band(s), {layout.BandDepth} tiles deep each, " +
$"stacked south to north.\n" +
$"Each band gets a spawner at its south edge; enemies travel north " +
$"{routing} to the goal.\n\n" +
$"Grid will bake to {layout.MapArea.width}×{layout.MapArea.height} with its " +
$"south-west corner at tile (0, 0).",
MessageType.Info);
EditorGUILayout.LabelField(
"Tune the starter proportions on the MapTemplate asset:",
EditorStyles.miniLabel);
if (GUILayout.Button("Select MapTemplate", EditorStyles.miniButton))
{
Selection.activeObject = template;
EditorGUIUtility.PingObject(template);
}
}
private void CreateMap()
{
EditorPrefs.SetString(AuthorPrefKey, request.Author ?? "");
if (NewMapBuilder.Create(request, out string error))
{
Close();
return;
}
EditorUtility.DisplayDialog("Could not create map", error, "OK");
}
}
}

View file

@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 50a9cf35b1b63204da8774b8afe7003e

View file

@ -355,6 +355,39 @@ namespace TD.Gameplay
return level.OwnerGrid[idx]; return level.OwnerGrid[idx];
} }
/// <summary>
/// This map's build-permission rule. <see cref="BuildAccess.OwnerOnly"/> for an unloaded
/// or unbaked level, which is the safe default (it can only ever reject more).
/// </summary>
public BuildAccess BuildAccess => level != null ? level.BuildAccess : BuildAccess.OwnerOnly;
/// <summary>
/// True if <paramref name="slot"/> is permitted to build on <paramref name="tile"/> under
/// this map's <see cref="BuildAccess"/> rule.
/// </summary>
/// <remarks>
/// <para>Permission ONLY — the caller still has to check <see cref="GetPlacement"/> and
/// <see cref="IsOccupied"/>. Keeping them separate is deliberate: each produces a different
/// <c>PlacementRejectionReason</c>, and collapsing them would cost the player a useful
/// message.</para>
///
/// <para>Both the client's ghost preview and the server's authoritative check call this, so
/// they cannot drift apart and show a white ghost that then gets rejected.</para>
///
/// <para>Under <see cref="BuildAccess.Shared"/> the tile must still belong to SOME player
/// zone. In practice every Buildable tile does, but stating it here keeps the method
/// meaningful on its own rather than relying on a caller's later check.</para>
/// </remarks>
public bool HasBuildPermission(Vector2Int tile, PlayerSlot slot)
{
if (slot == PlayerSlot.None) return false;
PlayerSlot owner = GetOwner(tile); // PlayerSlot.None when out of bounds
return BuildAccess == BuildAccess.Shared
? owner != PlayerSlot.None
: owner == slot;
}
/// <summary> /// <summary>
/// True if <paramref name="tile"/> is currently occupied by a placed tower footprint. /// True if <paramref name="tile"/> is currently occupied by a placed tower footprint.
/// Returns false for out-of-bounds tiles. Unlike <see cref="IsWalkable"/>, this grid /// Returns false for out-of-bounds tiles. Unlike <see cref="IsWalkable"/>, this grid

View file

@ -353,7 +353,7 @@ namespace TD.Gameplay
foreach (var tile in GridCoordinates.GetFootprintTiles(anchor, def.FootprintSize)) foreach (var tile in GridCoordinates.GetFootprintTiles(anchor, def.FootprintSize))
{ {
if (loader.GetOwner(tile) != localSlot) return false; if (!loader.HasBuildPermission(tile, localSlot)) return false;
if (loader.GetPlacement(tile) != PlacementState.Buildable) return false; if (loader.GetPlacement(tile) != PlacementState.Buildable) return false;
if (loader.IsOccupied(tile)) return false; if (loader.IsOccupied(tile)) return false;
} }

View file

@ -217,8 +217,10 @@ namespace TD.Gameplay
footprint.Add(tile); footprint.Add(tile);
// ------------------------------------------------------------------ // ------------------------------------------------------------------
// Check 1: Ownership // Check 1: Build permission
// Every footprint tile must be owned by the placing player's zone. // On an OwnerOnly map every footprint tile must be owned by the placing
// player's zone; on a Shared map any zone's tile will do. LevelLoader owns
// that rule so the client's ghost preview can't disagree with us.
// ------------------------------------------------------------------ // ------------------------------------------------------------------
PlayerSlot placingSlot = ClientIdToPlayerSlot(req.SenderClientId); PlayerSlot placingSlot = ClientIdToPlayerSlot(req.SenderClientId);
if (placingSlot == PlayerSlot.None) if (placingSlot == PlayerSlot.None)
@ -229,7 +231,7 @@ namespace TD.Gameplay
foreach (var tile in footprint) foreach (var tile in footprint)
{ {
if (loader.GetOwner(tile) != placingSlot) if (!loader.HasBuildPermission(tile, placingSlot))
{ {
Reject(req, PlacementRejectionReason.WrongOwner); Reject(req, PlacementRejectionReason.WrongOwner);
return; return;
@ -533,37 +535,67 @@ namespace TD.Gameplay
// ----- Path-validity BFS ------------------------------------------ // ----- Path-validity BFS ------------------------------------------
/// <summary> /// <summary>
/// Returns true if every spawner in <paramref name="placingSlot"/>'s zone can still /// Returns true if the map's mazes are still solvable with the candidate footprint in
/// reach an exit tile (any leak exit tile OR any goal tile) via walkable tiles, /// place — every spawner can still reach an exit tile (any leak exit from its own zone,
/// given the current state of <c>LevelLoader</c>'s runtime walkability grid. /// OR any goal tile) via walkable tiles, given <c>LevelLoader</c>'s runtime grid.
/// </summary> /// </summary>
/// <remarks> /// <remarks>
/// Mirrors bake-time P5-4. Runs a BFS per spawner against the runtime walkability /// <para>Mirrors bake-time P5-4. Reuses <see cref="bfsQueue"/> and <see cref="bfsVisited"/>
/// grid. Reuses <see cref="bfsQueue"/> and <see cref="bfsVisited"/> scratch /// scratch collections (cleared between BFS runs) to avoid GC allocation per call.</para>
/// collections (cleared between BFS runs) to avoid GC allocation per call. ///
/// <para><b>Scope depends on the map's build rule.</b> Under
/// <see cref="BuildAccess.OwnerOnly"/> a player can only build inside their own zone, so
/// their footprint can only ever strand their OWN spawners — checking
/// <paramref name="placingSlot"/>'s zone alone is both sufficient and cheapest. Under
/// <see cref="BuildAccess.Shared"/> anyone can wall off anyone's lane, so every zone has to
/// be checked or a player could strand a teammate's spawner and leave its enemies with no
/// route at all.</para>
/// </remarks> /// </remarks>
private bool CheckPathValidity(LevelLoader loader, PlayerSlot slot, private bool CheckPathValidity(LevelLoader loader, PlayerSlot placingSlot,
HashSet<Vector2Int> virtualBlocked = null) HashSet<Vector2Int> virtualBlocked = null)
{ {
var levelData = loader.LevelData; var levelData = loader.LevelData;
if (levelData?.PlayerZones == null) return true;
if (loader.BuildAccess == BuildAccess.Shared)
{
foreach (var zone in levelData.PlayerZones)
{
if (!ZoneSpawnersCanReachExit(loader, levelData, zone, virtualBlocked))
return false;
}
return true;
}
// Find the PlayerZoneData for this slot.
PlayerZoneData zoneData = null;
foreach (var zone in levelData.PlayerZones) foreach (var zone in levelData.PlayerZones)
{ {
if (zone.Owner == slot) { zoneData = zone; break; } if (zone.Owner == placingSlot)
return ZoneSpawnersCanReachExit(loader, levelData, zone, virtualBlocked);
} }
if (zoneData == null || zoneData.Spawners == null || zoneData.Spawners.Length == 0)
// Placing player has no zone — nothing of theirs to strand.
return true;
}
/// <summary>
/// Runs the reachability BFS for every spawner in one zone. Returns true if they can all
/// still reach an exit (or the zone has no spawners to strand).
/// </summary>
private bool ZoneSpawnersCanReachExit(LevelLoader loader, LevelData levelData,
PlayerZoneData zoneData,
HashSet<Vector2Int> virtualBlocked)
{
if (zoneData?.Spawners == null || zoneData.Spawners.Length == 0)
{ {
// No spawners means nothing to block — treat as valid. // No spawners means nothing to block — treat as valid.
return true; return true;
} }
// Build the exit tile set: union of all leak exit tiles and all goal tiles. // Build the exit tile set: this zone's leak exits, plus all goal tiles.
var exitTiles = BuildExitTileSet(levelData, slot); var exitTiles = BuildExitTileSet(levelData, zoneData.Owner);
if (exitTiles.Count == 0) if (exitTiles.Count == 0)
{ {
Debug.LogWarning($"[TowerPlacementManager] Zone {slot} has no exit tiles. " + Debug.LogWarning($"[TowerPlacementManager] Zone {zoneData.Owner} has no exit tiles. " +
$"This should have been caught at bake time (P5-8)."); $"This should have been caught at bake time (P5-8).");
return true; return true;
} }

View file

@ -0,0 +1,39 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!114 &11400000
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: fb7355881928ea146a26471078f207c2, type: 3}
m_Name: MapTemplate
m_EditorClassIdentifier: Assembly-CSharp-Editor::TD.Levels.Editor.MapTemplate
matchServicesPrefab: {fileID: 1445952844252628864, guid: 13be8328f6275e14da787f25c94683ee, type: 3}
matchNetworkPrefab: {fileID: 8429081419068942390, guid: 7116de1968afffb4990ee24f709612fb, type: 3}
sceneFolderAsset: {fileID: 102900000, guid: 1ac41b242a484844a85bb02ca1ae9f1f, type: 3}
mainMenuScene: {fileID: 102900000, guid: 1f4cb4a6391f5e94285960890282b70e, type: 3}
sceneFolder: Assets/_Project/Scenes/Levels
mainMenuScenePath: Assets/_Project/Scenes/UI_Scenes/MainMenu.unity
terrainMaterial: {fileID: 2100000, guid: 594ea882c5a793440b60ff72d896021e, type: 2}
terrainLayers:
- {fileID: 8574412962073106934, guid: f72a840f9f0991747a2d10c3c220bd27, type: 2}
- {fileID: 8574412962073106934, guid: 445b935ebf050f74a96e491cb1596964, type: 2}
- {fileID: 8574412962073106934, guid: faa5ef4eade0336408d27e4e78ebf1a6, type: 2}
- {fileID: 8574412962073106934, guid: 79a46a4f0f2c8a04a9f50c26dc17dc48, type: 2}
terrainHeightmapResolution: 513
terrainHeight: 60
terrainSkirtTiles: 20
terrainCarveDepth: 5
defaultMapWidth: 100
defaultMapHeight: 100
playAreaMargin: 4
goalDepth: 5
goalWidth: 10
spawnerDepth: 5
spawnerWidth: 10
leakGateWidth: 8
minBandDepth: 8

View file

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: ac495796f7ea6a9478aa5512f162d264
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 11400000
userData:
assetBundleName:
assetBundleVariant:

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.

View file

@ -6,10 +6,10 @@ EditorBuildSettings:
serializedVersion: 2 serializedVersion: 2
m_Scenes: m_Scenes:
- enabled: 1 - enabled: 1
path: Assets/_Project/Scenes/UI/MainMenu.unity path: Assets/_Project/Scenes/UI_Scenes/MainMenu.unity
guid: 1f4cb4a6391f5e94285960890282b70e guid: 1f4cb4a6391f5e94285960890282b70e
- enabled: 1 - enabled: 1
path: Assets/_Project/Scenes/UI/Lobby.unity path: Assets/_Project/Scenes/UI_Scenes/Lobby.unity
guid: 3a3639dca674a8049a570cb848bb69d2 guid: 3a3639dca674a8049a570cb848bb69d2
- enabled: 1 - enabled: 1
path: Assets/_Project/Scenes/Levels/Main.unity path: Assets/_Project/Scenes/Levels/Main.unity
@ -17,5 +17,8 @@ EditorBuildSettings:
- enabled: 1 - enabled: 1
path: Assets/_Project/Scenes/Levels/9Player.unity path: Assets/_Project/Scenes/Levels/9Player.unity
guid: 74fdeec477e257d4484b38108c283d84 guid: 74fdeec477e257d4484b38108c283d84
- enabled: 1
path: Assets/_Project/Scenes/Levels/3Player.unity
guid: 128de09101c3e3240b094d29d1eb9d22
m_configObjects: m_configObjects:
com.unity.input.settings.actions: {fileID: -944628639613478452, guid: 052faaac586de48259a63d0c4782560b, type: 3} com.unity.input.settings.actions: {fileID: -944628639613478452, guid: 052faaac586de48259a63d0c4782560b, type: 3}