Compare commits
1 commit
main
...
map-pipeli
| Author | SHA1 | Date | |
|---|---|---|---|
| 2b1aee3e03 |
53 changed files with 5640 additions and 448 deletions
|
|
@ -14,5 +14,6 @@ MonoBehaviour:
|
|||
m_EditorClassIdentifier: Assembly-CSharp::TD.Gameplay.EnemyAbilities.BlinkAbilityDefinition
|
||||
DisplayName: Blink
|
||||
Description: Enemies occasionally teleport a short distance ahead.
|
||||
IntervalSeconds: 8
|
||||
BlinkDistanceMeters: 4
|
||||
IntervalSeconds: 4
|
||||
BlinkWaypoints: 2
|
||||
StartJitterSeconds: 1.5
|
||||
|
|
|
|||
|
|
@ -51,6 +51,16 @@ namespace TD.Levels
|
|||
[Tooltip("Optional author/credit text. Empty triggers a soft warning at bake time.")]
|
||||
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
|
||||
// draw always-on or only-when-selected.
|
||||
|
|
|
|||
|
|
@ -63,6 +63,17 @@ namespace TD.Levels
|
|||
public BakeOutcome LastBakeOutcome;
|
||||
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.
|
||||
// -------------------------------------------------------------------
|
||||
|
|
|
|||
|
|
@ -53,7 +53,7 @@ MonoBehaviour:
|
|||
m_Script: {fileID: 11500000, guid: d5a57f767e5e46a458fc5d3c628d0cbb, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier: Unity.Netcode.Runtime::Unity.Netcode.NetworkObject
|
||||
GlobalObjectIdHash: 3715663997
|
||||
GlobalObjectIdHash: 3037368120
|
||||
InScenePlacedSourceGlobalObjectIdHash: 0
|
||||
DeferredDespawnTick: 0
|
||||
Ownership: 1
|
||||
|
|
@ -125,7 +125,7 @@ MonoBehaviour:
|
|||
m_Name:
|
||||
m_EditorClassIdentifier: Assembly-CSharp::TD.Gameplay.EnemyHealth
|
||||
ShowTopMostFoldoutHeaderGroup: 1
|
||||
definition: {fileID: 11400000, guid: edc686813aef43743bb15d356b511dff, type: 2}
|
||||
definition: {fileID: 0}
|
||||
deathAnimator: {fileID: 8642767442976997525}
|
||||
deathAnimationDuration: 1.5
|
||||
sinkDuration: 1.5
|
||||
|
|
|
|||
|
|
@ -125,7 +125,7 @@ MonoBehaviour:
|
|||
m_Name:
|
||||
m_EditorClassIdentifier: Assembly-CSharp::TD.Gameplay.EnemyHealth
|
||||
ShowTopMostFoldoutHeaderGroup: 1
|
||||
definition: {fileID: 11400000, guid: fe31ccfc7a429b24fb8f57279ba96c95, type: 2}
|
||||
definition: {fileID: 0}
|
||||
deathAnimator: {fileID: 7317567842465160949}
|
||||
deathAnimationDuration: 1.5
|
||||
sinkDuration: 1.5
|
||||
|
|
|
|||
|
|
@ -126,7 +126,7 @@ MonoBehaviour:
|
|||
m_Name:
|
||||
m_EditorClassIdentifier: Assembly-CSharp::TD.Gameplay.EnemyHealth
|
||||
ShowTopMostFoldoutHeaderGroup: 1
|
||||
definition: {fileID: 11400000, guid: 21c69bd4b2c55cd4fa9cd9b551c6e3fe, type: 2}
|
||||
definition: {fileID: 0}
|
||||
deathAnimator: {fileID: 6910253555362952583}
|
||||
deathAnimationDuration: 1.5
|
||||
sinkDuration: 1.5
|
||||
|
|
|
|||
|
|
@ -125,7 +125,7 @@ MonoBehaviour:
|
|||
m_Name:
|
||||
m_EditorClassIdentifier: Assembly-CSharp::TD.Gameplay.EnemyHealth
|
||||
ShowTopMostFoldoutHeaderGroup: 1
|
||||
definition: {fileID: 11400000, guid: 52d9af0c680bca64a86d66fce2a73668, type: 2}
|
||||
definition: {fileID: 0}
|
||||
deathAnimator: {fileID: 5079596453572633521}
|
||||
deathAnimationDuration: 1.5
|
||||
sinkDuration: 1.5
|
||||
|
|
|
|||
|
|
@ -125,7 +125,7 @@ MonoBehaviour:
|
|||
m_Name:
|
||||
m_EditorClassIdentifier: Assembly-CSharp::TD.Gameplay.EnemyHealth
|
||||
ShowTopMostFoldoutHeaderGroup: 1
|
||||
definition: {fileID: 11400000, guid: 4b981d8b44098894197507508f655072, type: 2}
|
||||
definition: {fileID: 0}
|
||||
deathAnimator: {fileID: 162562342023078655}
|
||||
deathAnimationDuration: 1.5
|
||||
sinkDuration: 1.5
|
||||
|
|
|
|||
|
|
@ -125,7 +125,7 @@ MonoBehaviour:
|
|||
m_Name:
|
||||
m_EditorClassIdentifier: Assembly-CSharp::TD.Gameplay.EnemyHealth
|
||||
ShowTopMostFoldoutHeaderGroup: 1
|
||||
definition: {fileID: 11400000, guid: bed87a08deeb7e048ba06c8268b0f959, type: 2}
|
||||
definition: {fileID: 0}
|
||||
deathAnimator: {fileID: 6938810086909213800}
|
||||
deathAnimationDuration: 1.5
|
||||
sinkDuration: 1.5
|
||||
|
|
|
|||
|
|
@ -125,7 +125,7 @@ MonoBehaviour:
|
|||
m_Name:
|
||||
m_EditorClassIdentifier: Assembly-CSharp::TD.Gameplay.EnemyHealth
|
||||
ShowTopMostFoldoutHeaderGroup: 1
|
||||
definition: {fileID: 11400000, guid: 77f4545654c5b7b41b2760a580da6068, type: 2}
|
||||
definition: {fileID: 0}
|
||||
deathAnimator: {fileID: 2830498994423139172}
|
||||
deathAnimationDuration: 1.5
|
||||
sinkDuration: 1.5
|
||||
|
|
|
|||
|
|
@ -125,7 +125,7 @@ MonoBehaviour:
|
|||
m_Name:
|
||||
m_EditorClassIdentifier: Assembly-CSharp::TD.Gameplay.EnemyHealth
|
||||
ShowTopMostFoldoutHeaderGroup: 1
|
||||
definition: {fileID: 11400000, guid: 097d7ba78591cd443a06d637e0822a46, type: 2}
|
||||
definition: {fileID: 0}
|
||||
deathAnimator: {fileID: 471366370285235598}
|
||||
deathAnimationDuration: 1.5
|
||||
sinkDuration: 1.5
|
||||
|
|
|
|||
|
|
@ -125,7 +125,7 @@ MonoBehaviour:
|
|||
m_Name:
|
||||
m_EditorClassIdentifier: Assembly-CSharp::TD.Gameplay.EnemyHealth
|
||||
ShowTopMostFoldoutHeaderGroup: 1
|
||||
definition: {fileID: 11400000, guid: 1d9809612c00ed049848a955613a4619, type: 2}
|
||||
definition: {fileID: 0}
|
||||
deathAnimator: {fileID: 3345626123719039665}
|
||||
deathAnimationDuration: 1.5
|
||||
sinkDuration: 1.5
|
||||
|
|
|
|||
8
Assets/_Project/Prefabs/MatchCore.meta
Normal file
8
Assets/_Project/Prefabs/MatchCore.meta
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
fileFormatVersion: 2
|
||||
guid: cf34e1b2cedcd9a4fa7f938f8f0bf3c2
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
483
Assets/_Project/Prefabs/MatchCore/MatchNetwork.prefab
Normal file
483
Assets/_Project/Prefabs/MatchCore/MatchNetwork.prefab
Normal 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
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 7116de1968afffb4990ee24f709612fb
|
||||
PrefabImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
921
Assets/_Project/Prefabs/MatchCore/MatchServices.prefab
Normal file
921
Assets/_Project/Prefabs/MatchCore/MatchServices.prefab
Normal 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
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 13be8328f6275e14da787f25c94683ee
|
||||
PrefabImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
327
Assets/_Project/Scenes/Levels/3Player.asset
Normal file
327
Assets/_Project/Scenes/Levels/3Player.asset
Normal file
File diff suppressed because one or more lines are too long
8
Assets/_Project/Scenes/Levels/3Player.asset.meta
Normal file
8
Assets/_Project/Scenes/Levels/3Player.asset.meta
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 4ca1d0054e9d3744e8802817bf0b69e2
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 11400000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
1682
Assets/_Project/Scenes/Levels/3Player.unity
Normal file
1682
Assets/_Project/Scenes/Levels/3Player.unity
Normal file
File diff suppressed because it is too large
Load diff
7
Assets/_Project/Scenes/Levels/3Player.unity.meta
Normal file
7
Assets/_Project/Scenes/Levels/3Player.unity.meta
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 128de09101c3e3240b094d29d1eb9d22
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
BIN
Assets/_Project/Scenes/Levels/3Player_TerrainData.asset
Normal file
BIN
Assets/_Project/Scenes/Levels/3Player_TerrainData.asset
Normal file
Binary file not shown.
|
|
@ -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
BIN
Assets/_Project/Scenes/Levels/3Player_Thumbnail.png
(Stored with Git LFS)
Normal file
Binary file not shown.
117
Assets/_Project/Scenes/Levels/3Player_Thumbnail.png.meta
Normal file
117
Assets/_Project/Scenes/Levels/3Player_Thumbnail.png.meta
Normal 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:
|
||||
|
|
@ -638,6 +638,7 @@ MonoBehaviour:
|
|||
maps:
|
||||
- {fileID: 11400000, guid: e65cce766e92f604f85ab939cf374abd, type: 2}
|
||||
- {fileID: 11400000, guid: 9cc56fbc3ae460a4b862f8510fdf5f09, type: 2}
|
||||
- {fileID: 11400000, guid: 4ca1d0054e9d3744e8802817bf0b69e2, type: 2}
|
||||
--- !u!4 &976902011
|
||||
Transform:
|
||||
m_ObjectHideFlags: 0
|
||||
|
|
@ -203,6 +203,31 @@ namespace TD.Core
|
|||
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>
|
||||
/// Outcome of a bake operation, recorded on the baked <see cref="TD.Levels.LevelData"/> asset.
|
||||
/// </summary>
|
||||
|
|
|
|||
|
|
@ -3,8 +3,6 @@ using System.Collections.Generic;
|
|||
using Unity.Netcode;
|
||||
using UnityEngine;
|
||||
using TD.Gameplay.Draft;
|
||||
using TD.Gameplay.EnemyUpgrades;
|
||||
using TD.Gameplay.Waves;
|
||||
|
||||
namespace TD.Dev
|
||||
{
|
||||
|
|
@ -76,42 +74,5 @@ namespace TD.Dev
|
|||
Debug.LogWarning($"[DebugCommandRelay] '{option.DisplayName}' could not be applied " +
|
||||
$"to client {OwnerClientId}.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Force-apply the <see cref="EnemyUpgradeOption"/> at <paramref name="optionId"/> (its
|
||||
/// <see cref="EnemyUpgradePool"/> index) to the run's current wave slot — the same
|
||||
/// recording <c>WaveVote.ServerResolve</c> does for a vote winner, skipping the vote
|
||||
/// entirely. Not player-scoped: the effect lands on the run's current wave slot rather
|
||||
/// than the caller, so any connected client's relay can trigger it.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Takes effect the next time this wave slot's enemies spawn, not retroactively on
|
||||
/// enemies already alive — see <see cref="EnemyUpgradeOption"/>'s remarks on why
|
||||
/// applying is "record on the slot," not a direct mutation.
|
||||
/// </remarks>
|
||||
[Rpc(SendTo.Server, InvokePermission = RpcInvokePermission.Owner)]
|
||||
public void DebugGrantEnemyUpgradeRpc(int optionId)
|
||||
{
|
||||
var pool = EnemyUpgradePool.Instance;
|
||||
var option = pool != null ? pool.Get(optionId) : null;
|
||||
if (option == null)
|
||||
{
|
||||
Debug.LogWarning($"[DebugCommandRelay] Grant requested for unknown enemy upgrade id {optionId}.");
|
||||
return;
|
||||
}
|
||||
|
||||
var run = RunState.Instance;
|
||||
if (run == null)
|
||||
{
|
||||
Debug.LogWarning("[DebugCommandRelay] No RunState in this scene.");
|
||||
return;
|
||||
}
|
||||
|
||||
int slot = run.CurrentUpgradeSlot;
|
||||
run.ServerAddUpgrade(slot, optionId);
|
||||
option.ServerOnApplied(run, slot);
|
||||
|
||||
Debug.Log($"[DebugCommandRelay] Granted '{option.DisplayName}' to wave slot {slot}.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@ using UnityEngine.InputSystem;
|
|||
using UnityEngine.UIElements;
|
||||
using TD.UI;
|
||||
using TD.Gameplay.Draft;
|
||||
using TD.Gameplay.EnemyUpgrades;
|
||||
|
||||
namespace TD.Dev
|
||||
{
|
||||
|
|
@ -50,7 +49,13 @@ namespace TD.Dev
|
|||
"System (UnityEngine.InputSystem.Key). Backquote is the physical ` / ~ key.")]
|
||||
[SerializeField] private Key toggleKey = Key.Backquote;
|
||||
|
||||
private DebugConsoleUI ui;
|
||||
// Beyond this many candidates the list is truncated with a "+N more" row so a
|
||||
// large DraftPool can't grow the console bar off the top of the screen.
|
||||
private const int MaxVisibleSuggestions = 12;
|
||||
|
||||
private VisualElement consoleContainer;
|
||||
private TextField commandInput;
|
||||
private VisualElement suggestionList;
|
||||
private bool consoleOpen;
|
||||
|
||||
// Frame on which the console was opened or closed. The toggle key and Enter
|
||||
|
|
@ -93,23 +98,123 @@ namespace TD.Dev
|
|||
return;
|
||||
}
|
||||
|
||||
ui = new DebugConsoleUI(root);
|
||||
BuildConsoleUi(root);
|
||||
}
|
||||
|
||||
private void BuildConsoleUi(VisualElement uiRoot)
|
||||
{
|
||||
// Top-anchored bar, closed by default. Distinct position from the HUD's
|
||||
// bottom-left chat panel so the two never overlap.
|
||||
consoleContainer = new VisualElement();
|
||||
consoleContainer.pickingMode = PickingMode.Ignore;
|
||||
consoleContainer.style.position = Position.Absolute;
|
||||
consoleContainer.style.left = 0;
|
||||
consoleContainer.style.right = 0;
|
||||
consoleContainer.style.top = 0;
|
||||
consoleContainer.style.paddingTop = 8;
|
||||
consoleContainer.style.paddingBottom = 8;
|
||||
consoleContainer.style.paddingLeft = 12;
|
||||
consoleContainer.style.paddingRight = 12;
|
||||
// Fully opaque: the console is a text-entry overlay that has to stay readable
|
||||
// over the level, the HUD, and whatever else is on screen.
|
||||
consoleContainer.style.backgroundColor = new Color(0f, 0f, 0f, 1f);
|
||||
consoleContainer.style.display = DisplayStyle.None;
|
||||
|
||||
commandInput = new TextField();
|
||||
commandInput.style.minHeight = 28;
|
||||
commandInput.style.width = Length.Percent(100);
|
||||
commandInput.maxLength = 200;
|
||||
commandInput.isDelayed = false;
|
||||
StyleConsoleInput(commandInput);
|
||||
|
||||
// Focus tracking — contributes to the same flag HUDController's chat
|
||||
// input sets, so camera/builder/spell input all stay gated correctly
|
||||
// regardless of which text field currently has focus.
|
||||
ui.CommandInput.RegisterCallback<FocusInEvent>(_ => HUDController.IsTextInputActive = true);
|
||||
ui.CommandInput.RegisterCallback<FocusOutEvent>(_ => HUDController.IsTextInputActive = false);
|
||||
commandInput.RegisterCallback<FocusInEvent>(_ => HUDController.IsTextInputActive = true);
|
||||
commandInput.RegisterCallback<FocusOutEvent>(_ => HUDController.IsTextInputActive = false);
|
||||
|
||||
ui.CommandInput.RegisterValueChangedCallback(_ => RefreshSuggestions());
|
||||
commandInput.RegisterValueChangedCallback(_ => RefreshSuggestions());
|
||||
|
||||
// Tab and the arrows can't be polled from Keyboard.current the way Enter and
|
||||
// Escape are (below): UI Toolkit would still insert a tab character and move
|
||||
// the focus ring before Update runs. Intercept them on the field in the
|
||||
// trickle-down phase, before the text engine and the focus ring see them.
|
||||
ui.CommandInput.RegisterCallback<KeyDownEvent>(OnConsoleKeyDown, TrickleDown.TrickleDown);
|
||||
ui.CommandInput.RegisterCallback<NavigationMoveEvent>(OnConsoleNavigationMove,
|
||||
commandInput.RegisterCallback<KeyDownEvent>(OnConsoleKeyDown, TrickleDown.TrickleDown);
|
||||
commandInput.RegisterCallback<NavigationMoveEvent>(OnConsoleNavigationMove,
|
||||
TrickleDown.TrickleDown);
|
||||
|
||||
suggestionList = new VisualElement();
|
||||
suggestionList.pickingMode = PickingMode.Ignore;
|
||||
suggestionList.style.flexDirection = FlexDirection.Column;
|
||||
suggestionList.style.marginTop = 4;
|
||||
suggestionList.style.display = DisplayStyle.None;
|
||||
|
||||
consoleContainer.Add(commandInput);
|
||||
consoleContainer.Add(suggestionList);
|
||||
uiRoot.Add(consoleContainer);
|
||||
}
|
||||
|
||||
// Mirrors HUDController.StyleChatInputDark — dark background, white text,
|
||||
// a thin border, and inner padding so ascenders/descenders don't clip.
|
||||
// Green border (vs. chat's neutral gray) as a quick visual "this is the
|
||||
// debug console, not chat" cue.
|
||||
private static void StyleConsoleInput(TextField field)
|
||||
{
|
||||
var darkBg = new Color(0.05f, 0.05f, 0.05f, 1f);
|
||||
var borderClr = new Color(0.35f, 0.75f, 0.35f);
|
||||
|
||||
field.style.backgroundColor = darkBg;
|
||||
field.style.color = Color.white;
|
||||
field.style.borderTopWidth = field.style.borderBottomWidth =
|
||||
field.style.borderLeftWidth = field.style.borderRightWidth = 1;
|
||||
field.style.borderTopColor = field.style.borderBottomColor =
|
||||
field.style.borderLeftColor = field.style.borderRightColor = borderClr;
|
||||
|
||||
var inner = field.Q("unity-text-input");
|
||||
if (inner != null)
|
||||
{
|
||||
inner.style.backgroundColor = darkBg;
|
||||
inner.style.color = Color.white;
|
||||
inner.style.paddingTop = 4;
|
||||
inner.style.paddingBottom = 4;
|
||||
inner.style.paddingLeft = 6;
|
||||
inner.style.paddingRight = 6;
|
||||
}
|
||||
}
|
||||
|
||||
// One autocomplete row: the candidate token plus an optional dim hint. Rows are
|
||||
// pickingMode Ignore and keyboard-driven only — nothing here is clickable, which
|
||||
// avoids the recursive picking-mode problem documented in HUDController's chat
|
||||
// panel (clicks falling through to the game world via a ScrollView's internal
|
||||
// wrapper element).
|
||||
private static VisualElement BuildSuggestionRow(string text, string hint, bool selected)
|
||||
{
|
||||
var row = new VisualElement();
|
||||
row.pickingMode = PickingMode.Ignore;
|
||||
row.style.flexDirection = FlexDirection.Row;
|
||||
row.style.minHeight = 20;
|
||||
row.style.paddingLeft = 6;
|
||||
row.style.paddingRight = 6;
|
||||
row.style.backgroundColor = selected
|
||||
? new Color(0.16f, 0.38f, 0.16f, 1f) // matches the green console cue
|
||||
: new Color(0.05f, 0.05f, 0.05f, 1f);
|
||||
|
||||
var label = new Label(text);
|
||||
label.pickingMode = PickingMode.Ignore;
|
||||
label.style.color = Color.white;
|
||||
label.style.unityFontStyleAndWeight = selected ? FontStyle.Bold : FontStyle.Normal;
|
||||
row.Add(label);
|
||||
|
||||
if (!string.IsNullOrEmpty(hint))
|
||||
{
|
||||
var hintLabel = new Label(hint);
|
||||
hintLabel.pickingMode = PickingMode.Ignore;
|
||||
hintLabel.style.color = new Color(0.6f, 0.6f, 0.6f);
|
||||
hintLabel.style.marginLeft = 12;
|
||||
row.Add(hintLabel);
|
||||
}
|
||||
|
||||
return row;
|
||||
}
|
||||
|
||||
private void Update()
|
||||
|
|
@ -174,14 +279,15 @@ namespace TD.Dev
|
|||
private void Swallow(EventBase evt)
|
||||
{
|
||||
evt.StopImmediatePropagation();
|
||||
ui.CommandInput?.panel?.focusController?.IgnoreEvent(evt);
|
||||
commandInput?.panel?.focusController?.IgnoreEvent(evt);
|
||||
}
|
||||
|
||||
private void OpenConsole()
|
||||
{
|
||||
if (ui.CommandInput == null) return;
|
||||
ui.Show();
|
||||
ui.CommandInput.SetValueWithoutNotify(string.Empty);
|
||||
if (commandInput == null) return;
|
||||
consoleContainer.style.display = DisplayStyle.Flex;
|
||||
consoleContainer.pickingMode = PickingMode.Position;
|
||||
commandInput.SetValueWithoutNotify(string.Empty);
|
||||
RefreshSuggestions();
|
||||
|
||||
// Suppress the toggle key and Enter for this frame and the next so the
|
||||
|
|
@ -196,25 +302,33 @@ namespace TD.Dev
|
|||
private IEnumerator FocusNextFrame()
|
||||
{
|
||||
yield return null;
|
||||
if (consoleOpen && ui.CommandInput != null)
|
||||
if (consoleOpen && commandInput != null)
|
||||
{
|
||||
ui.CommandInput.Focus();
|
||||
ui.CommandInput.SelectAll();
|
||||
commandInput.Focus();
|
||||
commandInput.SelectAll();
|
||||
}
|
||||
}
|
||||
|
||||
private void CloseConsole()
|
||||
{
|
||||
if (ui.CommandInput != null)
|
||||
if (commandInput != null)
|
||||
{
|
||||
ui.CommandInput.SetValueWithoutNotify(string.Empty);
|
||||
ui.CommandInput.Blur();
|
||||
commandInput.SetValueWithoutNotify(string.Empty);
|
||||
commandInput.Blur();
|
||||
}
|
||||
if (consoleContainer != null)
|
||||
{
|
||||
consoleContainer.style.display = DisplayStyle.None;
|
||||
consoleContainer.pickingMode = PickingMode.Ignore;
|
||||
}
|
||||
ui.Hide();
|
||||
|
||||
suggestions.Clear();
|
||||
selectedIndex = 0;
|
||||
ui.ClearSuggestions();
|
||||
if (suggestionList != null)
|
||||
{
|
||||
suggestionList.Clear();
|
||||
suggestionList.style.display = DisplayStyle.None;
|
||||
}
|
||||
|
||||
consoleOpen = false;
|
||||
toggleSuppressFrame = Time.frameCount + 1;
|
||||
|
|
@ -232,7 +346,7 @@ namespace TD.Dev
|
|||
|
||||
private void SubmitCommand()
|
||||
{
|
||||
string text = ui.CommandInput?.value ?? string.Empty;
|
||||
string text = commandInput?.value ?? string.Empty;
|
||||
|
||||
// Enter doubles as "accept the highlighted suggestion" while the typed
|
||||
// command isn't runnable yet, so you can drive the whole thing with Enter
|
||||
|
|
@ -263,11 +377,11 @@ namespace TD.Dev
|
|||
public string Hint; // dim one-liner shown beside the token
|
||||
public Func<List<string>> Values; // non-null => this level takes a final value
|
||||
public bool Executable; // a complete command can end here
|
||||
public Action<string> Execute; // "Values" level: run the completed command
|
||||
public Type OptionType; // "get": the DraftOption subclass to match
|
||||
public readonly List<CommandNode> Children = new List<CommandNode>();
|
||||
}
|
||||
|
||||
private const string UsageHint = "Try: get <type> <name>, grant enemy <ability>";
|
||||
private const string UsageHint = "Try: get <type> <name>";
|
||||
|
||||
// Maps the type token in "get <type> <name>" to the DraftOption subclass it
|
||||
// should match against. Add an entry here whenever a new DraftOption subclass
|
||||
|
|
@ -298,34 +412,17 @@ namespace TD.Dev
|
|||
|
||||
foreach (var pair in DraftTypeAliases)
|
||||
{
|
||||
string typeToken = pair.Key;
|
||||
var optionType = pair.Value;
|
||||
get.Children.Add(new CommandNode
|
||||
{
|
||||
Token = typeToken,
|
||||
Token = pair.Key,
|
||||
Hint = "<name>",
|
||||
OptionType = optionType,
|
||||
Executable = true,
|
||||
Values = () => DraftOptionNames(optionType),
|
||||
Execute = name => ExecuteGet(typeToken, optionType, name),
|
||||
});
|
||||
}
|
||||
|
||||
var grant = new CommandNode
|
||||
{
|
||||
Token = "grant",
|
||||
Hint = "grant a buff to the enemies",
|
||||
};
|
||||
root.Children.Add(grant);
|
||||
|
||||
grant.Children.Add(new CommandNode
|
||||
{
|
||||
Token = "enemy",
|
||||
Hint = "<ability>",
|
||||
Executable = true,
|
||||
Values = EnemyUpgradeNames,
|
||||
Execute = ExecuteGrantEnemyAbility,
|
||||
});
|
||||
|
||||
return root;
|
||||
}
|
||||
|
||||
|
|
@ -357,35 +454,6 @@ namespace TD.Dev
|
|||
return names;
|
||||
}
|
||||
|
||||
// Display names of every pooled EnemyUpgradeOption, alphabetically. Mirrors
|
||||
// DraftOptionNames above — EnemyUpgradePool is the enemy-side equivalent of DraftPool,
|
||||
// and this is the same Count/Get surface ExecuteGrantEnemyAbility walks. Includes
|
||||
// non-ability cards too (e.g. the vote-meta "Double Dip") — DebugGrantEnemyUpgradeRpc
|
||||
// resolves through EnemyUpgradeOption.ServerOnApplied regardless of subclass, so
|
||||
// there's nothing this command can't actually grant.
|
||||
private static List<string> EnemyUpgradeNames()
|
||||
{
|
||||
var names = new List<string>();
|
||||
|
||||
var pool = EnemyUpgradePool.Instance;
|
||||
if (pool == null) return names;
|
||||
|
||||
for (int i = 0; i < pool.Count; i++)
|
||||
{
|
||||
var option = pool.Get(i);
|
||||
if (option == null) continue;
|
||||
|
||||
string name = option.DisplayName;
|
||||
if (string.IsNullOrWhiteSpace(name)) continue;
|
||||
if (names.Contains(name)) continue;
|
||||
|
||||
names.Add(name);
|
||||
}
|
||||
|
||||
names.Sort(StringComparer.OrdinalIgnoreCase);
|
||||
return names;
|
||||
}
|
||||
|
||||
// Valid literal tokens at a level, for error messages ("Expected one of: ...").
|
||||
private static string ChildTokens(CommandNode node)
|
||||
{
|
||||
|
|
@ -572,7 +640,7 @@ namespace TD.Dev
|
|||
|
||||
private void RefreshSuggestions()
|
||||
{
|
||||
if (ui.CommandInput == null) return;
|
||||
if (commandInput == null) return;
|
||||
|
||||
// Keep the highlight on the same candidate when it survives the new filter,
|
||||
// so typing another character doesn't silently move what Enter would accept.
|
||||
|
|
@ -580,7 +648,7 @@ namespace TD.Dev
|
|||
? suggestions[selectedIndex].Text
|
||||
: null;
|
||||
|
||||
suggestions = ComputeSuggestions(ui.CommandInput.value, out fragmentStart);
|
||||
suggestions = ComputeSuggestions(commandInput.value, out fragmentStart);
|
||||
|
||||
selectedIndex = 0;
|
||||
if (previouslySelected != null)
|
||||
|
|
@ -594,15 +662,38 @@ namespace TD.Dev
|
|||
}
|
||||
}
|
||||
|
||||
ui.RenderSuggestions(ToSuggestionRows(suggestions), selectedIndex);
|
||||
RebuildSuggestionRows();
|
||||
}
|
||||
|
||||
private static List<DebugConsoleUI.SuggestionRow> ToSuggestionRows(List<Suggestion> suggestions)
|
||||
private void RebuildSuggestionRows()
|
||||
{
|
||||
var rows = new List<DebugConsoleUI.SuggestionRow>(suggestions.Count);
|
||||
for (int i = 0; i < suggestions.Count; i++)
|
||||
rows.Add(new DebugConsoleUI.SuggestionRow(suggestions[i].Text, suggestions[i].Hint));
|
||||
return rows;
|
||||
if (suggestionList == null) return;
|
||||
|
||||
suggestionList.Clear();
|
||||
|
||||
if (suggestions.Count == 0)
|
||||
{
|
||||
suggestionList.style.display = DisplayStyle.None;
|
||||
return;
|
||||
}
|
||||
|
||||
suggestionList.style.display = DisplayStyle.Flex;
|
||||
|
||||
// Window the list around the highlight so arrowing past row 12 keeps the
|
||||
// selected row on screen instead of scrolling it out of the truncated view.
|
||||
int start = suggestions.Count > MaxVisibleSuggestions
|
||||
? Mathf.Clamp(selectedIndex - MaxVisibleSuggestions / 2, 0,
|
||||
suggestions.Count - MaxVisibleSuggestions)
|
||||
: 0;
|
||||
int end = Mathf.Min(start + MaxVisibleSuggestions, suggestions.Count);
|
||||
|
||||
for (int i = start; i < end; i++)
|
||||
suggestionList.Add(BuildSuggestionRow(suggestions[i].Text, suggestions[i].Hint,
|
||||
i == selectedIndex));
|
||||
|
||||
int hidden = suggestions.Count - (end - start);
|
||||
if (hidden > 0)
|
||||
suggestionList.Add(BuildSuggestionRow($"+{hidden} more", null, false));
|
||||
}
|
||||
|
||||
private void MoveSelection(int delta)
|
||||
|
|
@ -612,16 +703,16 @@ namespace TD.Dev
|
|||
selectedIndex = (selectedIndex + delta) % suggestions.Count;
|
||||
if (selectedIndex < 0) selectedIndex += suggestions.Count;
|
||||
|
||||
ui.RenderSuggestions(ToSuggestionRows(suggestions), selectedIndex);
|
||||
RebuildSuggestionRows();
|
||||
}
|
||||
|
||||
private bool AcceptSelectedSuggestion()
|
||||
{
|
||||
if (ui.CommandInput == null) return false;
|
||||
if (commandInput == null) return false;
|
||||
if (selectedIndex < 0 || selectedIndex >= suggestions.Count) return false;
|
||||
|
||||
var suggestion = suggestions[selectedIndex];
|
||||
string text = ui.CommandInput.value ?? string.Empty;
|
||||
string text = commandInput.value ?? string.Empty;
|
||||
int start = Mathf.Clamp(fragmentStart, 0, text.Length);
|
||||
|
||||
string completed = text.Substring(0, start) + suggestion.Text +
|
||||
|
|
@ -630,8 +721,8 @@ namespace TD.Dev
|
|||
// SetValueWithoutNotify + an explicit refresh instead of assigning value:
|
||||
// one deterministic recompute, and the caret placement below can't race the
|
||||
// change callback.
|
||||
ui.CommandInput.SetValueWithoutNotify(completed);
|
||||
ui.CommandInput.SelectRange(completed.Length, completed.Length);
|
||||
commandInput.SetValueWithoutNotify(completed);
|
||||
commandInput.SelectRange(completed.Length, completed.Length);
|
||||
RefreshSuggestions();
|
||||
return true;
|
||||
}
|
||||
|
|
@ -666,14 +757,14 @@ namespace TD.Dev
|
|||
}
|
||||
|
||||
string nameToken = string.Join(" ", walkTokens.GetRange(i, walkTokens.Count - i));
|
||||
node.Execute(nameToken);
|
||||
ExecuteGet(node.Token, node.OptionType, nameToken);
|
||||
}
|
||||
|
||||
// "get <type> <name>": grants any DraftOption of the given type whose DisplayName
|
||||
// matches <name>, via the local player's DebugCommandRelay. Reuses DraftOption's
|
||||
// existing type-agnostic ServerApply dispatch instead of hardcoding a grant path
|
||||
// per option type.
|
||||
private static void ExecuteGet(string typeToken, Type optionType, string nameToken)
|
||||
private void ExecuteGet(string typeToken, Type optionType, string nameToken)
|
||||
{
|
||||
var pool = DraftPool.Instance;
|
||||
if (pool == null)
|
||||
|
|
@ -704,41 +795,6 @@ namespace TD.Dev
|
|||
Debug.LogWarning($"[DebugConsole] No {typeToken} option named \"{nameToken}\" found.");
|
||||
}
|
||||
|
||||
// "grant enemy <ability>": the same effect as the post-wave vote picking that ability
|
||||
// card for the current wave slot — records it via RunState.ServerAddUpgrade and runs
|
||||
// its ServerOnApplied, mirroring WaveVote.ServerResolve exactly (see DebugCommandRelay.
|
||||
// DebugGrantEnemyUpgradeRpc). Takes effect the next time this wave slot's enemies
|
||||
// spawn, not retroactively on enemies already alive.
|
||||
private static void ExecuteGrantEnemyAbility(string nameToken)
|
||||
{
|
||||
var pool = EnemyUpgradePool.Instance;
|
||||
if (pool == null)
|
||||
{
|
||||
Debug.LogWarning("[DebugConsole] No EnemyUpgradePool in this scene.");
|
||||
return;
|
||||
}
|
||||
|
||||
string normalizedTarget = Normalize(nameToken);
|
||||
for (int i = 0; i < pool.Count; i++)
|
||||
{
|
||||
var option = pool.Get(i);
|
||||
if (option == null || Normalize(option.DisplayName) != normalizedTarget) continue;
|
||||
|
||||
var relay = DebugCommandRelay.Local;
|
||||
if (relay == null)
|
||||
{
|
||||
Debug.LogWarning("[DebugConsole] No local DebugCommandRelay found — " +
|
||||
"is it on the Player prefab?");
|
||||
return;
|
||||
}
|
||||
|
||||
relay.DebugGrantEnemyUpgradeRpc(i);
|
||||
return;
|
||||
}
|
||||
|
||||
Debug.LogWarning($"[DebugConsole] No enemy ability named \"{nameToken}\" found.");
|
||||
}
|
||||
|
||||
// DisplayName is player-facing draft card text (its spacing/casing is chosen for
|
||||
// presentation, not for typing), so command matching strips everything but letters
|
||||
// and digits before comparing — "Slow Area", "slow_area", and "SlowArea" all match
|
||||
|
|
|
|||
|
|
@ -1,185 +0,0 @@
|
|||
// Assets/_Project/Scripts/Dev/DebugConsoleUI.cs
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UIElements;
|
||||
|
||||
namespace TD.Dev
|
||||
{
|
||||
/// <summary>
|
||||
/// Builds and draws <see cref="DebugConsole"/>'s VisualElement tree: the input bar and
|
||||
/// its autocomplete list. Owns presentation only — element construction, styling, and
|
||||
/// row rendering — no command grammar, keybinding, or suggestion-selection logic; that
|
||||
/// stays in <see cref="DebugConsole"/>, which drives this class through its public
|
||||
/// members and reads/writes <see cref="CommandInput"/> directly for focus and value.
|
||||
/// </summary>
|
||||
internal sealed class DebugConsoleUI
|
||||
{
|
||||
// Beyond this many candidates the list is truncated with a "+N more" row so a
|
||||
// large DraftPool can't grow the console bar off the top of the screen.
|
||||
private const int MaxVisibleSuggestions = 12;
|
||||
|
||||
/// <summary>One row of the autocomplete list: a candidate token plus an optional dim hint.</summary>
|
||||
public readonly struct SuggestionRow
|
||||
{
|
||||
public readonly string Text;
|
||||
public readonly string Hint;
|
||||
|
||||
public SuggestionRow(string text, string hint)
|
||||
{
|
||||
Text = text;
|
||||
Hint = hint;
|
||||
}
|
||||
}
|
||||
|
||||
public TextField CommandInput { get; }
|
||||
|
||||
private readonly VisualElement consoleContainer;
|
||||
private readonly VisualElement suggestionList;
|
||||
|
||||
public DebugConsoleUI(VisualElement uiRoot)
|
||||
{
|
||||
// Top-anchored bar, closed by default. Distinct position from the HUD's
|
||||
// bottom-left chat panel so the two never overlap.
|
||||
consoleContainer = new VisualElement();
|
||||
consoleContainer.pickingMode = PickingMode.Ignore;
|
||||
consoleContainer.style.position = Position.Absolute;
|
||||
consoleContainer.style.left = 0;
|
||||
consoleContainer.style.right = 0;
|
||||
consoleContainer.style.top = 0;
|
||||
consoleContainer.style.paddingTop = 8;
|
||||
consoleContainer.style.paddingBottom = 8;
|
||||
consoleContainer.style.paddingLeft = 12;
|
||||
consoleContainer.style.paddingRight = 12;
|
||||
// Fully opaque: the console is a text-entry overlay that has to stay readable
|
||||
// over the level, the HUD, and whatever else is on screen.
|
||||
consoleContainer.style.backgroundColor = new Color(0f, 0f, 0f, 1f);
|
||||
consoleContainer.style.display = DisplayStyle.None;
|
||||
|
||||
CommandInput = new TextField();
|
||||
CommandInput.style.minHeight = 28;
|
||||
CommandInput.style.width = Length.Percent(100);
|
||||
CommandInput.maxLength = 200;
|
||||
CommandInput.isDelayed = false;
|
||||
StyleConsoleInput(CommandInput);
|
||||
|
||||
suggestionList = new VisualElement();
|
||||
suggestionList.pickingMode = PickingMode.Ignore;
|
||||
suggestionList.style.flexDirection = FlexDirection.Column;
|
||||
suggestionList.style.marginTop = 4;
|
||||
suggestionList.style.display = DisplayStyle.None;
|
||||
|
||||
consoleContainer.Add(CommandInput);
|
||||
consoleContainer.Add(suggestionList);
|
||||
uiRoot.Add(consoleContainer);
|
||||
}
|
||||
|
||||
// Mirrors HUDController.StyleChatInputDark — dark background, white text,
|
||||
// a thin border, and inner padding so ascenders/descenders don't clip.
|
||||
// Green border (vs. chat's neutral gray) as a quick visual "this is the
|
||||
// debug console, not chat" cue.
|
||||
private static void StyleConsoleInput(TextField field)
|
||||
{
|
||||
var darkBg = new Color(0.05f, 0.05f, 0.05f, 1f);
|
||||
var borderClr = new Color(0.35f, 0.75f, 0.35f);
|
||||
|
||||
field.style.backgroundColor = darkBg;
|
||||
field.style.color = Color.white;
|
||||
field.style.borderTopWidth = field.style.borderBottomWidth =
|
||||
field.style.borderLeftWidth = field.style.borderRightWidth = 1;
|
||||
field.style.borderTopColor = field.style.borderBottomColor =
|
||||
field.style.borderLeftColor = field.style.borderRightColor = borderClr;
|
||||
|
||||
var inner = field.Q("unity-text-input");
|
||||
if (inner != null)
|
||||
{
|
||||
inner.style.backgroundColor = darkBg;
|
||||
inner.style.color = Color.white;
|
||||
inner.style.paddingTop = 4;
|
||||
inner.style.paddingBottom = 4;
|
||||
inner.style.paddingLeft = 6;
|
||||
inner.style.paddingRight = 6;
|
||||
}
|
||||
}
|
||||
|
||||
// One autocomplete row: the candidate token plus an optional dim hint. Rows are
|
||||
// pickingMode Ignore and keyboard-driven only — nothing here is clickable, which
|
||||
// avoids the recursive picking-mode problem documented in HUDController's chat
|
||||
// panel (clicks falling through to the game world via a ScrollView's internal
|
||||
// wrapper element).
|
||||
private static VisualElement BuildSuggestionRow(string text, string hint, bool selected)
|
||||
{
|
||||
var row = new VisualElement();
|
||||
row.pickingMode = PickingMode.Ignore;
|
||||
row.style.flexDirection = FlexDirection.Row;
|
||||
row.style.minHeight = 20;
|
||||
row.style.paddingLeft = 6;
|
||||
row.style.paddingRight = 6;
|
||||
row.style.backgroundColor = selected
|
||||
? new Color(0.16f, 0.38f, 0.16f, 1f) // matches the green console cue
|
||||
: new Color(0.05f, 0.05f, 0.05f, 1f);
|
||||
|
||||
var label = new Label(text);
|
||||
label.pickingMode = PickingMode.Ignore;
|
||||
label.style.color = Color.white;
|
||||
label.style.unityFontStyleAndWeight = selected ? FontStyle.Bold : FontStyle.Normal;
|
||||
row.Add(label);
|
||||
|
||||
if (!string.IsNullOrEmpty(hint))
|
||||
{
|
||||
var hintLabel = new Label(hint);
|
||||
hintLabel.pickingMode = PickingMode.Ignore;
|
||||
hintLabel.style.color = new Color(0.6f, 0.6f, 0.6f);
|
||||
hintLabel.style.marginLeft = 12;
|
||||
row.Add(hintLabel);
|
||||
}
|
||||
|
||||
return row;
|
||||
}
|
||||
|
||||
public void Show()
|
||||
{
|
||||
consoleContainer.style.display = DisplayStyle.Flex;
|
||||
consoleContainer.pickingMode = PickingMode.Position;
|
||||
}
|
||||
|
||||
public void Hide()
|
||||
{
|
||||
consoleContainer.style.display = DisplayStyle.None;
|
||||
consoleContainer.pickingMode = PickingMode.Ignore;
|
||||
}
|
||||
|
||||
// Windows the list around selectedIndex so arrowing past MaxVisibleSuggestions
|
||||
// keeps the selected row on screen instead of scrolling it out of the truncated view.
|
||||
public void RenderSuggestions(List<SuggestionRow> rows, int selectedIndex)
|
||||
{
|
||||
suggestionList.Clear();
|
||||
|
||||
if (rows == null || rows.Count == 0)
|
||||
{
|
||||
suggestionList.style.display = DisplayStyle.None;
|
||||
return;
|
||||
}
|
||||
|
||||
suggestionList.style.display = DisplayStyle.Flex;
|
||||
|
||||
int start = rows.Count > MaxVisibleSuggestions
|
||||
? Mathf.Clamp(selectedIndex - MaxVisibleSuggestions / 2, 0,
|
||||
rows.Count - MaxVisibleSuggestions)
|
||||
: 0;
|
||||
int end = Mathf.Min(start + MaxVisibleSuggestions, rows.Count);
|
||||
|
||||
for (int i = start; i < end; i++)
|
||||
suggestionList.Add(BuildSuggestionRow(rows[i].Text, rows[i].Hint, i == selectedIndex));
|
||||
|
||||
int hidden = rows.Count - (end - start);
|
||||
if (hidden > 0)
|
||||
suggestionList.Add(BuildSuggestionRow($"+{hidden} more", null, false));
|
||||
}
|
||||
|
||||
public void ClearSuggestions()
|
||||
{
|
||||
suggestionList.Clear();
|
||||
suggestionList.style.display = DisplayStyle.None;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,2 +0,0 @@
|
|||
fileFormatVersion: 2
|
||||
guid: fce39b2f68ef0f3f9bec299aa46d353a
|
||||
|
|
@ -802,6 +802,20 @@ namespace TD.Levels.Editor
|
|||
}
|
||||
|
||||
// P5-8: zones with neither outgoing leak nor goal adjacency.
|
||||
//
|
||||
// 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.
|
||||
//
|
||||
// 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)
|
||||
{
|
||||
var zonesWithLeaks = new HashSet<PlayerSlot>();
|
||||
foreach (var l in ctx.LeakExitVolumes) zonesWithLeaks.Add(l.sourceZone);
|
||||
|
||||
|
|
@ -812,7 +826,9 @@ namespace TD.Levels.Editor
|
|||
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.");
|
||||
"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.MapDescription = ctx.Authoring.mapDescription;
|
||||
data.Author = ctx.Authoring.author;
|
||||
data.BuildAccess = ctx.Authoring.buildAccess;
|
||||
data.GridOriginTile = ctx.GridOriginTile;
|
||||
data.GridSize = ctx.GridSize;
|
||||
|
||||
|
|
@ -1152,7 +1169,10 @@ namespace TD.Levels.Editor
|
|||
sb.Append("playerCount=").Append(ctx.Authoring.playerCount.ToString(inv)).Append('|');
|
||||
sb.Append("expectedGoalCount=").Append(ctx.Authoring.expectedGoalCount.ToString(inv)).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.
|
||||
var rootTransform = ctx.Authoring.transform;
|
||||
|
|
@ -1354,6 +1374,7 @@ namespace TD.Levels.Editor
|
|||
target.PlayerCount = src.PlayerCount;
|
||||
target.MapDescription = src.MapDescription;
|
||||
target.Author = src.Author;
|
||||
target.BuildAccess = src.BuildAccess;
|
||||
target.ScenePath = src.ScenePath;
|
||||
target.AuthoringHash = src.AuthoringHash;
|
||||
target.LastBakeTimestamp = src.LastBakeTimestamp;
|
||||
|
|
|
|||
356
Assets/_Project/Scripts/Editor/Levels/MapCoreExtractor.cs
Normal file
356
Assets/_Project/Scripts/Editor/Levels/MapCoreExtractor.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
fileFormatVersion: 2
|
||||
guid: d1b948bcbbe8d8b4788f2056372280b3
|
||||
292
Assets/_Project/Scripts/Editor/Levels/MapTemplate.cs
Normal file
292
Assets/_Project/Scripts/Editor/Levels/MapTemplate.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
fileFormatVersion: 2
|
||||
guid: fb7355881928ea146a26471078f207c2
|
||||
578
Assets/_Project/Scripts/Editor/Levels/NewMapBuilder.cs
Normal file
578
Assets/_Project/Scripts/Editor/Levels/NewMapBuilder.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 5eda54745c5c78c489eb2cf9aa6265b7
|
||||
205
Assets/_Project/Scripts/Editor/Levels/NewMapWindow.cs
Normal file
205
Assets/_Project/Scripts/Editor/Levels/NewMapWindow.cs
Normal 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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 50a9cf35b1b63204da8774b8afe7003e
|
||||
|
|
@ -25,22 +25,26 @@ namespace TD.Gameplay.EnemyAbilities
|
|||
[Header("Blink")]
|
||||
[Tooltip("Seconds between blinks.")]
|
||||
[Min(0.1f)]
|
||||
public float IntervalSeconds = 8f;
|
||||
public float IntervalSeconds = 4f;
|
||||
|
||||
[Tooltip("World units to advance along the path per blink.")]
|
||||
[Min(0.1f)]
|
||||
public float BlinkDistanceMeters = 4f;
|
||||
[Tooltip("How many path waypoints to skip per blink. Note that path smoothing can make " +
|
||||
"one waypoint span several tiles, so this is a coarser dial than it looks.")]
|
||||
[Min(1)]
|
||||
public int BlinkWaypoints = 2;
|
||||
|
||||
[Tooltip("Random spread (seconds) added to each enemy's first blink so a wave doesn't " +
|
||||
"blink in one synchronized block. Applied once, at spawn.")]
|
||||
[Min(0f)]
|
||||
public float StartJitterSeconds = 1.5f;
|
||||
|
||||
public override void ServerOnSpawn(EnemyAbility instance, int abilityIndex)
|
||||
{
|
||||
// Seed each enemy's cooldown with a random point within the full interval, as if it
|
||||
// spawned already partway through a cooldown. A small jitter window near zero would
|
||||
// still let a burst of enemies spawned together sync up and blink as a visible cluster
|
||||
// every cycle; spreading across the whole interval avoids that regardless of how
|
||||
// IntervalSeconds is tuned. Done once, here, rather than on the first tick — a tick-time
|
||||
// Seed each enemy's cooldown with a different negative offset so the wave's first
|
||||
// blink is staggered. Done once, here, rather than on the first tick — a tick-time
|
||||
// check would have to distinguish "never started" from "just blinked", and both
|
||||
// states read as a zero timer.
|
||||
instance.TimerFor(abilityIndex) = -Random.Range(0f, IntervalSeconds);
|
||||
if (StartJitterSeconds > 0f)
|
||||
instance.TimerFor(abilityIndex) = -Random.Range(0f, StartJitterSeconds);
|
||||
}
|
||||
|
||||
public override void ServerTick(EnemyAbility instance, int abilityIndex, float dt)
|
||||
|
|
@ -50,7 +54,7 @@ namespace TD.Gameplay.EnemyAbilities
|
|||
if (timer < IntervalSeconds) return;
|
||||
|
||||
timer = 0f;
|
||||
instance.GetComponent<EnemyMovement>()?.ServerBlinkForward(BlinkDistanceMeters);
|
||||
instance.GetComponent<EnemyMovement>()?.ServerBlinkForward(BlinkWaypoints);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -286,70 +286,39 @@ namespace TD.Gameplay
|
|||
|
||||
/// <summary>
|
||||
/// Server-only: teleport this enemy forward along its existing path by up to
|
||||
/// <paramref name="meters"/> world units. Used by the Blink wave buff.
|
||||
/// <paramref name="tiles"/> waypoints. Used by the Blink wave buff.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <b>Follows the path rather than the straight line to the goal.</b> Jumping toward the
|
||||
/// goal as the crow flies would let a grounded enemy blink through walls, which is the
|
||||
/// flying buff's job, not this one. Walking the path keeps the enemy inside the maze —
|
||||
/// flying buff's job, not this one. Skipping waypoints keeps the enemy inside the maze —
|
||||
/// it cuts corners, it doesn't ignore them.
|
||||
///
|
||||
/// <para>Distance is measured along the path (waypoint to waypoint), consuming the
|
||||
/// budget one segment at a time, so the landing spot can fall mid-segment rather than
|
||||
/// snapping to a waypoint. This keeps the blink distance predictable regardless of how
|
||||
/// far apart the path's waypoints happen to be — unlike skipping a fixed waypoint count,
|
||||
/// where one blink could span a few tiles or the length of the whole map depending on
|
||||
/// path smoothing.</para>
|
||||
/// <para>Zone tracking still runs on the destination tile, so an enemy that blinks out of
|
||||
/// its origin zone still credits its owner a leak. Skipping that would make the buff a way
|
||||
/// to launder leaks past the no-leak bonus.</para>
|
||||
///
|
||||
/// <para>Zone tracking runs once, on the final landing tile, so an enemy that blinks out
|
||||
/// of its origin zone still credits its owner a leak. Skipping that would make the buff a
|
||||
/// way to launder leaks past the no-leak bonus.</para>
|
||||
///
|
||||
/// <para>Blinking onto (or past) the final waypoint resolves as a normal goal arrival,
|
||||
/// despawn and life cost included.</para>
|
||||
/// <para>Blinking onto the final waypoint resolves as a normal goal arrival, despawn and
|
||||
/// life cost included.</para>
|
||||
/// </remarks>
|
||||
public void ServerBlinkForward(float meters)
|
||||
public void ServerBlinkForward(int tiles)
|
||||
{
|
||||
if (!IsServer || meters <= 0f) return;
|
||||
if (!IsServer || tiles <= 0) return;
|
||||
if (hasReachedGoal || remainingPath.Count == 0) return;
|
||||
if (health != null && health.IsHeld) return;
|
||||
|
||||
float remaining = meters;
|
||||
Vector3 position = transform.position;
|
||||
|
||||
while (remaining > 0f && remainingPath.Count > 0)
|
||||
{
|
||||
Vector3 targetWorld = GridCoordinates.GridToWorld(remainingPath[0]);
|
||||
Vector3 toTarget = targetWorld - position;
|
||||
toTarget.y = 0f;
|
||||
float distance = toTarget.magnitude;
|
||||
|
||||
if (distance <= remaining)
|
||||
{
|
||||
// Reach this waypoint fully and keep walking the leftover budget
|
||||
// into the next segment.
|
||||
remaining -= distance;
|
||||
position = new Vector3(targetWorld.x, position.y, targetWorld.z);
|
||||
remainingPath.RemoveAt(0);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Budget runs out partway along this segment.
|
||||
position += toTarget.normalized * remaining;
|
||||
remaining = 0f;
|
||||
}
|
||||
}
|
||||
int skip = Mathf.Min(tiles, remainingPath.Count);
|
||||
Vector2Int destination = remainingPath[skip - 1];
|
||||
remainingPath.RemoveRange(0, skip);
|
||||
|
||||
// Preserve Y so flyers stay at altitude and grounded pivots don't sink.
|
||||
transform.position = position;
|
||||
Vector3 world = GridCoordinates.GridToWorld(destination);
|
||||
transform.position = new Vector3(world.x, transform.position.y, world.z);
|
||||
|
||||
CheckZoneTransition(destination);
|
||||
|
||||
if (remainingPath.Count == 0)
|
||||
{
|
||||
HandleGoalReached();
|
||||
return;
|
||||
}
|
||||
|
||||
CheckZoneTransition(GridCoordinates.WorldToGrid(position));
|
||||
}
|
||||
|
||||
// ----- Path invalidation ----------------------------------------------
|
||||
|
|
|
|||
|
|
@ -355,6 +355,39 @@ namespace TD.Gameplay
|
|||
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>
|
||||
/// 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
|
||||
|
|
|
|||
|
|
@ -353,7 +353,7 @@ namespace TD.Gameplay
|
|||
|
||||
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.IsOccupied(tile)) return false;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -217,8 +217,10 @@ namespace TD.Gameplay
|
|||
footprint.Add(tile);
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// Check 1: Ownership
|
||||
// Every footprint tile must be owned by the placing player's zone.
|
||||
// Check 1: Build permission
|
||||
// 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);
|
||||
if (placingSlot == PlayerSlot.None)
|
||||
|
|
@ -229,7 +231,7 @@ namespace TD.Gameplay
|
|||
|
||||
foreach (var tile in footprint)
|
||||
{
|
||||
if (loader.GetOwner(tile) != placingSlot)
|
||||
if (!loader.HasBuildPermission(tile, placingSlot))
|
||||
{
|
||||
Reject(req, PlacementRejectionReason.WrongOwner);
|
||||
return;
|
||||
|
|
@ -533,37 +535,67 @@ namespace TD.Gameplay
|
|||
// ----- Path-validity BFS ------------------------------------------
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if every spawner in <paramref name="placingSlot"/>'s zone can still
|
||||
/// reach an exit tile (any leak exit tile OR any goal tile) via walkable tiles,
|
||||
/// given the current state of <c>LevelLoader</c>'s runtime walkability grid.
|
||||
/// Returns true if the map's mazes are still solvable with the candidate footprint in
|
||||
/// place — every spawner can still reach an exit tile (any leak exit from its own zone,
|
||||
/// OR any goal tile) via walkable tiles, given <c>LevelLoader</c>'s runtime grid.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Mirrors bake-time P5-4. Runs a BFS per spawner against the runtime walkability
|
||||
/// grid. Reuses <see cref="bfsQueue"/> and <see cref="bfsVisited"/> scratch
|
||||
/// collections (cleared between BFS runs) to avoid GC allocation per call.
|
||||
/// <para>Mirrors bake-time P5-4. Reuses <see cref="bfsQueue"/> and <see cref="bfsVisited"/>
|
||||
/// scratch collections (cleared between BFS runs) to avoid GC allocation per call.</para>
|
||||
///
|
||||
/// <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>
|
||||
private bool CheckPathValidity(LevelLoader loader, PlayerSlot slot,
|
||||
private bool CheckPathValidity(LevelLoader loader, PlayerSlot placingSlot,
|
||||
HashSet<Vector2Int> virtualBlocked = null)
|
||||
{
|
||||
var levelData = loader.LevelData;
|
||||
if (levelData?.PlayerZones == null) return true;
|
||||
|
||||
// Find the PlayerZoneData for this slot.
|
||||
PlayerZoneData zoneData = null;
|
||||
if (loader.BuildAccess == BuildAccess.Shared)
|
||||
{
|
||||
foreach (var zone in levelData.PlayerZones)
|
||||
{
|
||||
if (zone.Owner == slot) { zoneData = zone; break; }
|
||||
if (!ZoneSpawnersCanReachExit(loader, levelData, zone, virtualBlocked))
|
||||
return false;
|
||||
}
|
||||
if (zoneData == null || zoneData.Spawners == null || zoneData.Spawners.Length == 0)
|
||||
return true;
|
||||
}
|
||||
|
||||
foreach (var zone in levelData.PlayerZones)
|
||||
{
|
||||
if (zone.Owner == placingSlot)
|
||||
return ZoneSpawnersCanReachExit(loader, levelData, zone, virtualBlocked);
|
||||
}
|
||||
|
||||
// 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.
|
||||
return true;
|
||||
}
|
||||
|
||||
// Build the exit tile set: union of all leak exit tiles and all goal tiles.
|
||||
var exitTiles = BuildExitTileSet(levelData, slot);
|
||||
// Build the exit tile set: this zone's leak exits, plus all goal tiles.
|
||||
var exitTiles = BuildExitTileSet(levelData, zoneData.Owner);
|
||||
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).");
|
||||
return true;
|
||||
}
|
||||
|
|
|
|||
39
Assets/_Project/Settings/MapTemplate.asset
Normal file
39
Assets/_Project/Settings/MapTemplate.asset
Normal 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
|
||||
8
Assets/_Project/Settings/MapTemplate.asset.meta
Normal file
8
Assets/_Project/Settings/MapTemplate.asset.meta
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
fileFormatVersion: 2
|
||||
guid: ac495796f7ea6a9478aa5512f162d264
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 11400000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
188
Docs/Map_Pipeline.md
Normal file
188
Docs/Map_Pipeline.md
Normal 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.
|
||||
|
|
@ -6,10 +6,10 @@ EditorBuildSettings:
|
|||
serializedVersion: 2
|
||||
m_Scenes:
|
||||
- enabled: 1
|
||||
path: Assets/_Project/Scenes/UI/MainMenu.unity
|
||||
path: Assets/_Project/Scenes/UI_Scenes/MainMenu.unity
|
||||
guid: 1f4cb4a6391f5e94285960890282b70e
|
||||
- enabled: 1
|
||||
path: Assets/_Project/Scenes/UI/Lobby.unity
|
||||
path: Assets/_Project/Scenes/UI_Scenes/Lobby.unity
|
||||
guid: 3a3639dca674a8049a570cb848bb69d2
|
||||
- enabled: 1
|
||||
path: Assets/_Project/Scenes/Levels/Main.unity
|
||||
|
|
@ -17,5 +17,8 @@ EditorBuildSettings:
|
|||
- enabled: 1
|
||||
path: Assets/_Project/Scenes/Levels/9Player.unity
|
||||
guid: 74fdeec477e257d4484b38108c283d84
|
||||
- enabled: 1
|
||||
path: Assets/_Project/Scenes/Levels/3Player.unity
|
||||
guid: 128de09101c3e3240b094d29d1eb9d22
|
||||
m_configObjects:
|
||||
com.unity.input.settings.actions: {fileID: -944628639613478452, guid: 052faaac586de48259a63d0c4782560b, type: 3}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue