diff --git a/Assets/DefaultNetworkPrefabs.asset b/Assets/DefaultNetworkPrefabs.asset index 219664e..abe922b 100644 --- a/Assets/DefaultNetworkPrefabs.asset +++ b/Assets/DefaultNetworkPrefabs.asset @@ -13,4 +13,9 @@ MonoBehaviour: m_Name: DefaultNetworkPrefabs m_EditorClassIdentifier: Unity.Netcode.Runtime::Unity.Netcode.NetworkPrefabsList IsDefault: 1 - List: [] + List: + - Override: 0 + Prefab: {fileID: 3493329038866903420, guid: 9a9c23b8584ab444aa5066a48579a9ec, type: 3} + SourcePrefabToOverride: {fileID: 0} + SourceHashToOverride: 0 + OverridingTargetPrefab: {fileID: 0} diff --git a/Assets/_Project/Levels/LevelAuthoring.cs b/Assets/_Project/Levels/LevelAuthoring.cs index 390fef0..8a5c349 100644 --- a/Assets/_Project/Levels/LevelAuthoring.cs +++ b/Assets/_Project/Levels/LevelAuthoring.cs @@ -69,6 +69,11 @@ namespace TD.Levels [Tooltip("If true, GoalVolume gizmos draw whether or not the volume is selected.")] public bool alwaysShowGoals = false; + [Tooltip("If true, MapAreaVolume gizmos draw whether or not the volume is selected. " + + "Defaults to true — the map boundary is generally useful to see while authoring " + + "the rest of the map, not just when the MapAreaVolume itself is selected.")] + public bool alwaysShowMapArea = true; + // ------------------------------------------------------------------- // Bake API // ------------------------------------------------------------------- @@ -242,4 +247,4 @@ namespace TD.Levels Gizmos.color = prev; } } -} +} \ No newline at end of file diff --git a/Assets/_Project/Levels/LevelData.cs b/Assets/_Project/Levels/LevelData.cs index df02d83..5e33677 100644 --- a/Assets/_Project/Levels/LevelData.cs +++ b/Assets/_Project/Levels/LevelData.cs @@ -81,6 +81,13 @@ namespace TD.Levels "Length = GridSize.x * GridSize.y.")] public PlayerSlot[] OwnerGrid; + [Tooltip("Per-tile map-area membership. True if the tile is part of the playable map " + + "(inside any MapAreaVolume); false for void tiles outside the map. This is the " + + "outermost gating layer — gameplay queries (placement, ownership, walkability) " + + "are only meaningful for tiles where MapAreaGrid is true. Length = GridSize.x * " + + "GridSize.y.")] + public bool[] MapAreaGrid; + // ------------------------------------------------------------------- // Per-zone and per-goal structures (populated by bake from volume data). // ------------------------------------------------------------------- @@ -141,4 +148,4 @@ namespace TD.Levels [Tooltip("Full tile coverage of the goal volume.")] public Vector2Int[] TileArea; } -} +} \ No newline at end of file diff --git a/Assets/_Project/Levels/MapAreaVolume.cs b/Assets/_Project/Levels/MapAreaVolume.cs new file mode 100644 index 0000000..14ec397 --- /dev/null +++ b/Assets/_Project/Levels/MapAreaVolume.cs @@ -0,0 +1,103 @@ +using UnityEngine; +using TD.Core; + +#if UNITY_EDITOR +using UnityEditor; +#endif + +namespace TD.Levels +{ + /// + /// Authoring volume defining the playable map area — the bounds the builder can move within + /// and the camera can pan to. Has no gameplay payload (no owner, no placement validity, no + /// spawner data, no leak target). Multiple instances are allowed; their union defines the + /// "map area" as referenced in the per-tile data layering spec. + /// + /// + /// Map area = where the player's attention can go. + /// Gameplay area = union of PlayerZone, Spawner, LeakExit, Goal volumes (where rules apply). + /// Buffer area = map area minus gameplay area (visible terrain, no gameplay, no building). + /// + /// Coverage rule: every gameplay volume's tiles must be a subset of the map area. The bake + /// validates this in Phase 5 (P5-12) and fails with a hard error if any gameplay volume + /// pokes outside the map area. + /// + /// Drawn as a thicker outline only (no fill) so it reads as a boundary marker rather than + /// as a translucent overlay on top of the gameplay volumes. The "always show" toggle on + /// LevelAuthoring defaults to true for this volume type because the map boundary is + /// generally useful to see during all authoring, not just when this volume is selected. + /// + public class MapAreaVolume : VolumeBase + { + // Map area draws BELOW player zones (which sit at 0.05). Just above the LevelAuthoring + // map-bounds line (0.01) so it doesn't z-fight with that wireframe. + private const float FillYLevel = 0.02f; + + protected override bool GetAlwaysShowToggle(LevelAuthoring authoring) + { + return authoring.alwaysShowMapArea; + } + + private void OnDrawGizmosSelected() + { + DrawGizmosCore(); + } + + private void OnDrawGizmos() + { + if (ShouldDrawAlwaysOn()) + { + DrawGizmosCore(); + } + } + + private void DrawGizmosCore() + { + Color baseColor = PlayerColors.MapArea; + DrawThickRectangularOutline(baseColor, yLevel: FillYLevel); + +#if UNITY_EDITOR + var col = Collider; + if (col != null) + { + Vector3 labelPos = new Vector3(col.bounds.center.x, FillYLevel + 0.1f, col.bounds.center.z); + Handles.Label(labelPos, "Map Area"); + } +#endif + } + + // Draws the volume's tight tile rectangle as a thicker outline. Unity's Gizmos.DrawLine + // has no width parameter, so we approximate thickness by drawing the rectangle three + // times with small lateral offsets. The offsets are in tile units; 0.04 reads as a + // visibly thicker line at typical scene-view zoom without looking like multiple lines. + private void DrawThickRectangularOutline(Color outlineColor, float yLevel) + { + if (!TryGetTightTileRect(out Vector2Int minTile, out Vector2Int maxTile)) return; + + const float thickness = 0.04f; + float halfTile = GridCoordinates.TILE_SIZE * 0.5f; + + // Three concentric rectangles: the original edge, slightly inset, slightly outset. + DrawOutlineAtInset(minTile, maxTile, halfTile, yLevel, 0f, outlineColor); + DrawOutlineAtInset(minTile, maxTile, halfTile, yLevel, +thickness, outlineColor); + DrawOutlineAtInset(minTile, maxTile, halfTile, yLevel, -thickness, outlineColor); + } + + private static void DrawOutlineAtInset(Vector2Int minTile, Vector2Int maxTile, float halfTile, + float yLevel, float inset, Color color) + { + Vector3 sw = new Vector3(minTile.x - halfTile - inset, yLevel, minTile.y - halfTile - inset); + Vector3 se = new Vector3(maxTile.x + halfTile + inset, yLevel, minTile.y - halfTile - inset); + Vector3 ne = new Vector3(maxTile.x + halfTile + inset, yLevel, maxTile.y + halfTile + inset); + Vector3 nw = new Vector3(minTile.x - halfTile - inset, yLevel, maxTile.y + halfTile + inset); + + Color prev = Gizmos.color; + Gizmos.color = color; + Gizmos.DrawLine(sw, se); + Gizmos.DrawLine(se, ne); + Gizmos.DrawLine(ne, nw); + Gizmos.DrawLine(nw, sw); + Gizmos.color = prev; + } + } +} \ No newline at end of file diff --git a/Assets/_Project/Levels/MapAreaVolume.cs.meta b/Assets/_Project/Levels/MapAreaVolume.cs.meta new file mode 100644 index 0000000..2c66f04 --- /dev/null +++ b/Assets/_Project/Levels/MapAreaVolume.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: bacb3662546735544bc6c56b5bf5830a \ No newline at end of file diff --git a/Assets/_Project/Prefabs/Player.meta b/Assets/_Project/Prefabs/Player.meta new file mode 100644 index 0000000..3f74df3 --- /dev/null +++ b/Assets/_Project/Prefabs/Player.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 36f79ad6231ec054a9ceed380b33ce11 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/_Project/Prefabs/Player/Player.prefab b/Assets/_Project/Prefabs/Player/Player.prefab new file mode 100644 index 0000000..827282d --- /dev/null +++ b/Assets/_Project/Prefabs/Player/Player.prefab @@ -0,0 +1,74 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!1 &3493329038866903420 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 8600750867913649879} + - component: {fileID: 2152427255203126265} + - component: {fileID: 2918837822014987993} + m_Layer: 0 + m_Name: Player + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!4 &8600750867913649879 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 3493329038866903420} + serializedVersion: 2 + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 36.397, y: 0.5, z: 6.33766} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 0} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!114 &2152427255203126265 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 3493329038866903420} + 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: 2568017024 + 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 &2918837822014987993 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 3493329038866903420} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 6b9796562d7cc274f832657f21a61cce, type: 3} + m_Name: + m_EditorClassIdentifier: Assembly-CSharp::TD.Gameplay.PlayerGoldManager + ShowTopMostFoldoutHeaderGroup: 1 + startingGold: 100 diff --git a/Assets/_Project/Prefabs/Player/Player.prefab.meta b/Assets/_Project/Prefabs/Player/Player.prefab.meta new file mode 100644 index 0000000..0cfc686 --- /dev/null +++ b/Assets/_Project/Prefabs/Player/Player.prefab.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 9a9c23b8584ab444aa5066a48579a9ec +PrefabImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/_Project/Scenes/Levels/Main.unity b/Assets/_Project/Scenes/Levels/Main.unity index 7f41077..7043dbf 100644 --- a/Assets/_Project/Scenes/Levels/Main.unity +++ b/Assets/_Project/Scenes/Levels/Main.unity @@ -237,78 +237,6 @@ Transform: m_Children: [] m_Father: {fileID: 0} m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} ---- !u!1 &239104687 -GameObject: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - serializedVersion: 6 - m_Component: - - component: {fileID: 239104690} - - component: {fileID: 239104688} - - component: {fileID: 239104689} - m_Layer: 0 - m_Name: GoldManager - m_TagString: Untagged - m_Icon: {fileID: 0} - m_NavMeshLayer: 0 - m_StaticEditorFlags: 0 - m_IsActive: 1 ---- !u!114 &239104688 -MonoBehaviour: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 239104687} - 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: 3224213014 - 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 &239104689 -MonoBehaviour: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 239104687} - m_Enabled: 1 - m_EditorHideFlags: 0 - m_Script: {fileID: 11500000, guid: d44ebdd0b2fc4144c8f8a181a714b738, type: 3} - m_Name: - m_EditorClassIdentifier: Assembly-CSharp::TD.Gameplay.GoldManager - ShowTopMostFoldoutHeaderGroup: 1 - startingGold: 100 ---- !u!4 &239104690 -Transform: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 239104687} - 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: 0} - m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} --- !u!1 &304575571 GameObject: m_ObjectHideFlags: 0 @@ -682,6 +610,7 @@ MonoBehaviour: alwaysShowSpawners: 1 alwaysShowLeakExits: 1 alwaysShowGoals: 1 + alwaysShowMapArea: 1 --- !u!4 &441239881 Transform: m_ObjectHideFlags: 0 @@ -701,6 +630,7 @@ Transform: - {fileID: 1078485324} - {fileID: 1064792476} - {fileID: 1360337263} + - {fileID: 923592499} m_Father: {fileID: 0} m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} --- !u!1 &832575517 @@ -752,6 +682,72 @@ Transform: m_Children: [] m_Father: {fileID: 0} m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!1 &923592498 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 923592499} + - component: {fileID: 923592501} + - component: {fileID: 923592500} + m_Layer: 0 + m_Name: MapArea + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!4 &923592499 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 923592498} + 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: 441239881} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!114 &923592500 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 923592498} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: bacb3662546735544bc6c56b5bf5830a, type: 3} + m_Name: + m_EditorClassIdentifier: Assembly-CSharp::TD.Levels.MapAreaVolume +--- !u!65 &923592501 +BoxCollider: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 923592498} + m_Material: {fileID: 0} + m_IncludeLayers: + serializedVersion: 2 + m_Bits: 0 + m_ExcludeLayers: + serializedVersion: 2 + m_Bits: 0 + m_LayerOverridePriority: 0 + m_IsTrigger: 0 + m_ProvidesContacts: 0 + m_Enabled: 1 + serializedVersion: 3 + m_Size: {x: 70, y: 0.5, z: 39} + m_Center: {x: 0.5, y: 0, z: 19} --- !u!1 &1064792475 GameObject: m_ObjectHideFlags: 0 @@ -1137,7 +1133,7 @@ MonoBehaviour: NetworkConfig: ProtocolVersion: 0 NetworkTransport: {fileID: 1682341400} - PlayerPrefab: {fileID: 0} + PlayerPrefab: {fileID: 3493329038866903420, guid: 9a9c23b8584ab444aa5066a48579a9ec, type: 3} Prefabs: NetworkPrefabsLists: - {fileID: 11400000, guid: 481ab1d7456efd044bc3e349aacd92ae, type: 2} @@ -1254,7 +1250,6 @@ SceneRoots: - {fileID: 410087041} - {fileID: 832575519} - {fileID: 1682341402} - - {fileID: 239104690} - {fileID: 441239881} - {fileID: 1464027364} - {fileID: 167151709} diff --git a/Assets/_Project/Scenes/Levels/TestLevel.asset b/Assets/_Project/Scenes/Levels/TestLevel.asset index ee59cd6..57f092c 100644 --- a/Assets/_Project/Scenes/Levels/TestLevel.asset +++ b/Assets/_Project/Scenes/Levels/TestLevel.asset @@ -18,15 +18,16 @@ MonoBehaviour: Author: Matt MapThumbnail: {fileID: 21300000, guid: d2e652d3e1c53454d80d3c1ec7888998, type: 3} ScenePath: Assets/_Project/Scenes/Levels/Main.unity - AuthoringHash: 521a1ef38caafd70be6e364f81e999f5da6c425332fe32933766854b8cfad413 - LastBakeTimestamp: 2026-04-30T19:05:42.7013062Z + AuthoringHash: de422400f5f1f75440a3d909f65f34569621293a1499b988881845ac0dc248a4 + LastBakeTimestamp: 2026-05-01T22:11:16.4327588Z LastBakeOutcome: 1 - LastBakeWarningCount: 1 - GridOriginTile: {x: 0, y: 0} - GridSize: {x: 68, y: 17} - PlacementGrid: 02020202020202010101010101010101010101010101010101010101010101010101010202010101010101010101010101010101010101010101010101010101010202020202020202020201010101010101010101010101010101010101010101010101010101020201010101010101010101010101010101010101010101010101010101020202020202020202020101010101010101010101010101010101010101010101010101010102020101010101010101010101010101010101010101010101010101010102020202020202020202010101010101010101010101010101010101010101010101010101010202010101010101010101010101010101010101010101010101010101010202020202020202020201010101010101010101010101010101010101010101010101010101020201010101010101010101010101010101010101010101010101010101020202020202020202020101010101010101010101010101010101010101010101010101010102020101010101010101010101010101010101010101010101010101010102020202020202020202010101010101010101010101010101010101010101010101010101010202010101010101010101010101010101010101010101010101010101010202020000000000000000000000000000000000000000000000000000000000000000000000000002020202020202000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000202020202020200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000020202020202020000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002020202020202000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000202020202020200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000020202020202020000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002020202020202000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000202020202020200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000020202020202020000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002020202020202000000000000000000000000000000000000000000000000 - WalkabilityGrid: 01010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010000000000000000000000000000000000000000000000000000000000000000000000000001010101010101000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000101010101010100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010101010101010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001010101010101000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000101010101010100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010101010101010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001010101010101000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000101010101010100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010101010101010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001010101010101000000000000000000000000000000000000000000000000 - OwnerGrid: 01010101010101010101010101010101010101010101010101010101010101010101010000020202020202020202020202020202020202020202020202020202020000000101010101010101010101010101010101010101010101010101010101010101010101000002020202020202020202020202020202020202020202020202020202000000010101010101010101010101010101010101010101010101010101010101010101010100000202020202020202020202020202020202020202020202020202020200000001010101010101010101010101010101010101010101010101010101010101010101010000020202020202020202020202020202020202020202020202020202020000000101010101010101010101010101010101010101010101010101010101010101010101000002020202020202020202020202020202020202020202020202020202000000010101010101010101010101010101010101010101010101010101010101010101010100000202020202020202020202020202020202020202020202020202020200000001010101010101010101010101010101010101010101010101010101010101010101010000020202020202020202020202020202020202020202020202020202020000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 + LastBakeWarningCount: 2 + GridOriginTile: {x: -1, y: -10} + GridSize: {x: 70, y: 39} + PlacementGrid: 000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002020202020202010101010101010101010101010101010101010101010101010101010202010101010101010101010101010101010101010101010101010101010202020000020202020202020101010101010101010101010101010101010101010101010101010102020101010101010101010101010101010101010101010101010101010102020200000202020202020201010101010101010101010101010101010101010101010101010101020201010101010101010101010101010101010101010101010101010101020202000002020202020202010101010101010101010101010101010101010101010101010101010202010101010101010101010101010101010101010101010101010101010202020000020202020202020101010101010101010101010101010101010101010101010101010102020101010101010101010101010101010101010101010101010101010102020200000202020202020201010101010101010101010101010101010101010101010101010101020201010101010101010101010101010101010101010101010101010101020202000002020202020202010101010101010101010101010101010101010101010101010101010202010101010101010101010101010101010101010101010101010101010202020000000000000000000000000000000000000000000000000000000000000000000000000000000202020202020200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002020202020202000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000020202020202020000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000202020202020200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002020202020202000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000020202020202020000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000202020202020200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002020202020202000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000020202020202020000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000202020202020200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 + WalkabilityGrid: 000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010000010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010100000101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101000001010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010000010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010100000101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101000001010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010000000000000000000000000000000000000000000000000000000000000000000000000000000101010101010100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001010101010101000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010101010101010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000101010101010100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001010101010101000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010101010101010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000101010101010100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001010101010101000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010101010101010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000101010101010100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 + OwnerGrid: 000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001010101010101010101010101010101010101010101010101010101010101010101010000020202020202020202020202020202020202020202020202020202020000000000010101010101010101010101010101010101010101010101010101010101010101010100000202020202020202020202020202020202020202020202020202020200000000000101010101010101010101010101010101010101010101010101010101010101010101000002020202020202020202020202020202020202020202020202020202000000000001010101010101010101010101010101010101010101010101010101010101010101010000020202020202020202020202020202020202020202020202020202020000000000010101010101010101010101010101010101010101010101010101010101010101010100000202020202020202020202020202020202020202020202020202020200000000000101010101010101010101010101010101010101010101010101010101010101010101000002020202020202020202020202020202020202020202020202020202000000000001010101010101010101010101010101010101010101010101010101010101010101010000020202020202020202020202020202020202020202020202020202020000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 + MapAreaGrid: 010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 PlayerZones: - Owner: 1 Spawners: diff --git a/Assets/_Project/Scenes/Levels/TestLevel_Thumbnail.png b/Assets/_Project/Scenes/Levels/TestLevel_Thumbnail.png index d469050..8f50996 100644 --- a/Assets/_Project/Scenes/Levels/TestLevel_Thumbnail.png +++ b/Assets/_Project/Scenes/Levels/TestLevel_Thumbnail.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9b13dafb78f7bc013cba5dd80719fd2e86d11f2601ad7103adc85bfd7aed067d -size 5198 +oid sha256:62cfc398409e4285b50681d79ae4767183db364b3f9feb9072ddf9dd60ec07d9 +size 12253 diff --git a/Assets/_Project/Scripts/Core/PlayerColors.cs b/Assets/_Project/Scripts/Core/PlayerColors.cs index d220f7f..3d74d31 100644 --- a/Assets/_Project/Scripts/Core/PlayerColors.cs +++ b/Assets/_Project/Scripts/Core/PlayerColors.cs @@ -19,19 +19,20 @@ namespace TD.Core { // Player colors. Hex values are RGB; alpha is set per-gizmo at draw time. // Values are tuned to be saturated enough to read against Unity's default scene background. - private static readonly Color Player1Red = HexRGB(0xE0, 0x3A, 0x3A); // red - private static readonly Color Player2Green = HexRGB(0x3A, 0xC0, 0x4A); // green - private static readonly Color Player3Blue = HexRGB(0x3A, 0x7A, 0xE0); // blue - private static readonly Color Player4Purple = HexRGB(0xA0, 0x4A, 0xC0); // purple - private static readonly Color Player5Yellow = HexRGB(0xE0, 0xC8, 0x3A); // yellow - private static readonly Color Player6Gray = HexRGB(0xB0, 0xB0, 0xB8); // gray (slightly cool) - private static readonly Color Player7Teal = HexRGB(0x3A, 0xC0, 0xB8); // teal - private static readonly Color Player8Olive = HexRGB(0x9A, 0x9A, 0x3A); // olive - private static readonly Color Player9DarkGray = HexRGB(0x60, 0x60, 0x68); // dark gray (slightly cool) + private static readonly Color Player1Red = HexRGB(0xE0, 0x3A, 0x3A); // red + private static readonly Color Player2Green = HexRGB(0x3A, 0xC0, 0x4A); // green + private static readonly Color Player3Blue = HexRGB(0x3A, 0x7A, 0xE0); // blue + private static readonly Color Player4Purple = HexRGB(0xA0, 0x4A, 0xC0); // purple + private static readonly Color Player5Yellow = HexRGB(0xE0, 0xC8, 0x3A); // yellow + private static readonly Color Player6Gray = HexRGB(0xB0, 0xB0, 0xB8); // gray (slightly cool) + private static readonly Color Player7Teal = HexRGB(0x3A, 0xC0, 0xB8); // teal + private static readonly Color Player8Olive = HexRGB(0x9A, 0x9A, 0x3A); // olive + private static readonly Color Player9DarkGray = HexRGB(0x60, 0x60, 0x68); // dark gray (slightly cool) // Non-player colors. - private static readonly Color GoalGold = HexRGB(0xE0, 0xB0, 0x20); // gold - private static readonly Color ErrorPink = HexRGB(0xFF, 0x4A, 0xC8); // diagnostic + private static readonly Color GoalGold = HexRGB(0xE0, 0xB0, 0x20); // gold + private static readonly Color MapAreaCyan = HexRGB(0xB0, 0xD0, 0xE0); // muted cyan (background) + private static readonly Color ErrorPink = HexRGB(0xFF, 0x4A, 0xC8); // diagnostic /// /// Returns the canonical color for a player slot. Returns the diagnostic error pink if @@ -60,6 +61,13 @@ namespace TD.Core /// The canonical color used for goal volumes. Not tied to any player. public static Color Goal => GoalGold; + /// + /// The canonical color used for MapAreaVolume gizmos. Muted cyan — distinct from any + /// player color and from goal gold. Intended to read as background context rather than + /// as a gameplay element. + /// + public static Color MapArea => MapAreaCyan; + /// Diagnostic color used when a volume has an invalid/missing owner. public static Color Error => ErrorPink; @@ -79,4 +87,4 @@ namespace TD.Core return new Color(r / 255f, g / 255f, b / 255f, 1f); } } -} +} \ No newline at end of file diff --git a/Assets/_Project/Scripts/Editor/Levels/LevelBakePipeline.cs b/Assets/_Project/Scripts/Editor/Levels/LevelBakePipeline.cs index 6b9dd6e..2c4dc05 100644 --- a/Assets/_Project/Scripts/Editor/Levels/LevelBakePipeline.cs +++ b/Assets/_Project/Scripts/Editor/Levels/LevelBakePipeline.cs @@ -89,7 +89,8 @@ namespace TD.Levels.Editor Debug.Log($"[LevelBake] {outcome} — '{ctx.Authoring.mapName}' " + $"({ctx.PlayerZoneVolumes.Count} player zones, {ctx.SpawnerVolumes.Count} spawners, " + - $"{ctx.LeakExitVolumes.Count} leak exits, {ctx.GoalVolumes.Count} goals; " + + $"{ctx.LeakExitVolumes.Count} leak exits, {ctx.GoalVolumes.Count} goals, " + + $"{ctx.MapAreaVolumes.Count} map areas; " + $"grid {ctx.GridSize.x}×{ctx.GridSize.y}; {report.WarningCount} warnings)"); if (report.WarningCount > 0) @@ -124,16 +125,18 @@ namespace TD.Levels.Editor var rootTransform = ctx.Authoring.transform; // Scoped scan: only volumes parented under _LevelAuthoring's transform are part of the bake. - ctx.PlayerZoneVolumes = rootTransform.GetComponentsInChildren(includeInactive: false).ToList(); - ctx.SpawnerVolumes = rootTransform.GetComponentsInChildren(includeInactive: false).ToList(); - ctx.LeakExitVolumes = rootTransform.GetComponentsInChildren(includeInactive: false).ToList(); - ctx.GoalVolumes = rootTransform.GetComponentsInChildren(includeInactive: false).ToList(); + ctx.PlayerZoneVolumes = rootTransform.GetComponentsInChildren(includeInactive: false).ToList(); + ctx.SpawnerVolumes = rootTransform.GetComponentsInChildren(includeInactive: false).ToList(); + ctx.LeakExitVolumes = rootTransform.GetComponentsInChildren(includeInactive: false).ToList(); + ctx.GoalVolumes = rootTransform.GetComponentsInChildren(includeInactive: false).ToList(); + ctx.MapAreaVolumes = rootTransform.GetComponentsInChildren(includeInactive: false).ToList(); ctx.AllVolumes = new List(); ctx.AllVolumes.AddRange(ctx.PlayerZoneVolumes); ctx.AllVolumes.AddRange(ctx.SpawnerVolumes); ctx.AllVolumes.AddRange(ctx.LeakExitVolumes); ctx.AllVolumes.AddRange(ctx.GoalVolumes); + ctx.AllVolumes.AddRange(ctx.MapAreaVolumes); // Full-scene scan: catch volumes that exist but aren't parented under _LevelAuthoring. var orphanCandidates = UnityEngine.Object.FindObjectsByType(FindObjectsInactive.Exclude); @@ -149,9 +152,10 @@ namespace TD.Levels.Editor // hash input ordering and stable iteration in subsequent phases. ctx.AllVolumes.Sort((a, b) => string.CompareOrdinal(CanonicalPath(a, rootTransform), CanonicalPath(b, rootTransform))); ctx.PlayerZoneVolumes.Sort((a, b) => string.CompareOrdinal(CanonicalPath(a, rootTransform), CanonicalPath(b, rootTransform))); - ctx.SpawnerVolumes.Sort((a, b) => string.CompareOrdinal(CanonicalPath(a, rootTransform), CanonicalPath(b, rootTransform))); - ctx.LeakExitVolumes.Sort((a, b) => string.CompareOrdinal(CanonicalPath(a, rootTransform), CanonicalPath(b, rootTransform))); - ctx.GoalVolumes.Sort((a, b) => string.CompareOrdinal(CanonicalPath(a, rootTransform), CanonicalPath(b, rootTransform))); + ctx.SpawnerVolumes.Sort((a, b) => string.CompareOrdinal(CanonicalPath(a, rootTransform), CanonicalPath(b, rootTransform))); + ctx.LeakExitVolumes.Sort((a, b) => string.CompareOrdinal(CanonicalPath(a, rootTransform), CanonicalPath(b, rootTransform))); + ctx.GoalVolumes.Sort((a, b) => string.CompareOrdinal(CanonicalPath(a, rootTransform), CanonicalPath(b, rootTransform))); + ctx.MapAreaVolumes.Sort((a, b) => string.CompareOrdinal(CanonicalPath(a, rootTransform), CanonicalPath(b, rootTransform))); } // Builds the canonical path string for a transform, relative to the root, with sibling @@ -214,6 +218,17 @@ namespace TD.Levels.Editor report.Error("P2-2", $"Found {ctx.GoalVolumes.Count} GoalVolume(s); LevelAuthoring expects {auth.expectedGoalCount}."); } + // P2-22: at least one MapAreaVolume is present. + // The map area is required (not inferred) so that gameplay bounds stay decoupled from + // visual geometry. See "Lessons Learned" — inferring playable bounds from terrain + // mesh extent was deliberately rejected. + if (ctx.MapAreaVolumes.Count == 0) + { + report.Error("P2-22", "No MapAreaVolume found. Every map must have at least one " + + "MapAreaVolume defining the playable bounds (where the builder " + + "can move and the camera can pan)."); + } + // P2-16: author non-empty (soft warning) if (string.IsNullOrWhiteSpace(auth.author)) { @@ -403,6 +418,7 @@ namespace TD.Levels.Editor ctx.PerVolumeTiles = new Dictionary>(); ctx.PerOwnerZoneTiles = new Dictionary>(); ctx.PerGoalTiles = new List>(); + ctx.MapAreaTiles = new HashSet(); bool initializedAggregate = false; int aggMinX = 0, aggMinY = 0, aggMaxX = 0, aggMaxY = 0; @@ -461,6 +477,14 @@ namespace TD.Levels.Editor { ctx.PerGoalTiles.Add(tiles); } + + // Map area: union all MapAreaVolume tiles into a single set. The map area is the + // union of all MapAreaVolumes, so a tile is "in the map" if ANY MapAreaVolume + // covers it. + if (v is MapAreaVolume) + { + foreach (var t in tiles) ctx.MapAreaTiles.Add(t); + } } if (!initializedAggregate) @@ -498,12 +522,27 @@ namespace TD.Levels.Editor // 2D arrays during composition; flattened to 1D in Phase 6. ctx.PlacementGrid2D = new PlacementState[width, height]; // defaults to Outside (0) ctx.WalkabilityGrid2D = new bool[width, height]; // defaults to false + ctx.MapAreaGrid2D = new bool[width, height]; // defaults to false + + // Map-area composition: a tile is "in map" iff any MapAreaVolume covers it. We have + // the union pre-computed in ctx.MapAreaTiles from Phase 3. + foreach (var t in ctx.MapAreaTiles) + { + int gx = t.x - ctx.GridOriginTile.x; + int gy = t.y - ctx.GridOriginTile.y; + ctx.MapAreaGrid2D[gx, gy] = true; + } // Volume-iterating algorithm: O(total tile-coverage) rather than O(grid * volumes). foreach (var v in ctx.AllVolumes) { if (!ctx.PerVolumeTiles.TryGetValue(v, out var tiles)) continue; + // MapAreaVolume contributes ONLY to the map-area grid (handled above) — not to + // walkability or placement. Buffer tiles (in-map but not gameplay) must remain + // non-walkable so enemies cannot path through them. + if (v is MapAreaVolume) continue; + bool isInvalid = IsInvalidValidity(v); bool isPlayerZone = v is PlayerZoneVolume; @@ -536,10 +575,10 @@ namespace TD.Levels.Editor switch (v) { case PlayerZoneVolume pz: return pz.placementValidity == PlacementValidity.Invalid; - case SpawnerVolume sv: return sv.placementValidity == PlacementValidity.Invalid; - case LeakExitVolume lv: return lv.placementValidity == PlacementValidity.Invalid; - case GoalVolume gv: return gv.placementValidity == PlacementValidity.Invalid; - default: return false; + case SpawnerVolume sv: return sv.placementValidity == PlacementValidity.Invalid; + case LeakExitVolume lv: return lv.placementValidity == PlacementValidity.Invalid; + case GoalVolume gv: return gv.placementValidity == PlacementValidity.Invalid; + default: return false; } } @@ -774,6 +813,45 @@ namespace TD.Levels.Editor report.Warning("P5-11", $"Walkability grid has {components} disconnected regions. " + "Some areas of the map are unreachable from others."); } + + // P5-12: every gameplay volume's tiles must be a subset of the map area. + // Coverage rule from the design spec — gameplay can't poke outside the playable map. + // Skipped if no MapAreaVolume exists (P2-22 already errored; this would just spam). + if (ctx.MapAreaTiles.Count > 0) + { + ValidateGameplayContainedInMapArea(ctx, report); + } + } + + // P5-12: each gameplay volume (PlayerZone, Spawner, LeakExit, Goal) must have all its + // tiles inside ctx.MapAreaTiles. Reports one error per offending volume with the count of + // out-of-map tiles to avoid drowning the report in per-tile errors. + private static void ValidateGameplayContainedInMapArea(BakeContext ctx, BakeReport report) + { + foreach (var v in ctx.AllVolumes) + { + if (v is MapAreaVolume) continue; // The map area itself doesn't need to contain itself. + if (!ctx.PerVolumeTiles.TryGetValue(v, out var tiles)) continue; + + int outsideCount = 0; + Vector2Int firstOutside = Vector2Int.zero; + foreach (var t in tiles) + { + if (!ctx.MapAreaTiles.Contains(t)) + { + if (outsideCount == 0) firstOutside = t; + outsideCount++; + } + } + + if (outsideCount > 0) + { + report.Error("P5-12", + $"{v.GetType().Name} '{v.name}' has {outsideCount} tile(s) outside the map area " + + $"(first offender: {firstOutside}). Every gameplay volume must be fully contained " + + "within the union of MapAreaVolumes."); + } + } } private static int CountWalkableComponents(HashSet walkable) @@ -830,6 +908,7 @@ namespace TD.Levels.Editor data.PlacementGrid = new PlacementState[total]; data.WalkabilityGrid = new bool[total]; data.OwnerGrid = new PlayerSlot[total]; // defaults to PlayerSlot.None (=0) + data.MapAreaGrid = new bool[total]; // defaults to false for (int x = 0; x < width; x++) { @@ -838,6 +917,7 @@ namespace TD.Levels.Editor int idx = y * width + x; data.PlacementGrid[idx] = ctx.PlacementGrid2D[x, y]; data.WalkabilityGrid[idx] = ctx.WalkabilityGrid2D[x, y]; + data.MapAreaGrid[idx] = ctx.MapAreaGrid2D[x, y]; } } @@ -984,26 +1064,26 @@ namespace TD.Levels.Editor // -- Hash computation ------------------------------------------------- - /// - /// Computes the authoring hash for a LevelAuthoring's scene state, - /// suitable for comparing against - /// to detect drift from baked data. - /// - /// - /// Delegates to the same hashing routine the bake itself uses, so equal - /// hashes mean the scene would bake to equivalent LevelData. Hashes - /// only Layer 1 authoring inputs (volumes + metadata); visual scene - /// content (terrain, lighting, decorations) is NOT included. - /// - /// Performs its own volume discovery and canonical sort. Does NOT run - /// pre-validation or rasterization, so it returns a hash even for scenes - /// that would fail the bake (e.g. missing zones, overlapping volumes). - /// That's intentional: the play-mode hook needs a hash even when the - /// scene is broken, so it can report "drift" as the actionable problem - /// rather than silently differing from the last successful bake. - /// - public static string ComputeAuthoringHash(LevelAuthoring authoring) - { + /// + /// Computes the authoring hash for a LevelAuthoring's scene state, + /// suitable for comparing against + /// to detect drift from baked data. + /// + /// + /// Delegates to the same hashing routine the bake itself uses, so equal + /// hashes mean the scene would bake to equivalent LevelData. Hashes + /// only Layer 1 authoring inputs (volumes + metadata); visual scene + /// content (terrain, lighting, decorations) is NOT included. + /// + /// Performs its own volume discovery and canonical sort. Does NOT run + /// pre-validation or rasterization, so it returns a hash even for scenes + /// that would fail the bake (e.g. missing zones, overlapping volumes). + /// That's intentional: the play-mode hook needs a hash even when the + /// scene is broken, so it can report "drift" as the actionable problem + /// rather than silently differing from the last successful bake. + /// + public static string ComputeAuthoringHash(LevelAuthoring authoring) + { if (authoring == null) return string.Empty; @@ -1018,13 +1098,15 @@ namespace TD.Levels.Editor var spawners = rootTransform.GetComponentsInChildren(includeInactive: false); var leakExits = rootTransform.GetComponentsInChildren(includeInactive: false); var goals = rootTransform.GetComponentsInChildren(includeInactive: false); + var mapAreas = rootTransform.GetComponentsInChildren(includeInactive: false); ctx.AllVolumes = new System.Collections.Generic.List( - playerZones.Length + spawners.Length + leakExits.Length + goals.Length); + playerZones.Length + spawners.Length + leakExits.Length + goals.Length + mapAreas.Length); ctx.AllVolumes.AddRange(playerZones); ctx.AllVolumes.AddRange(spawners); ctx.AllVolumes.AddRange(leakExits); ctx.AllVolumes.AddRange(goals); + ctx.AllVolumes.AddRange(mapAreas); // Match Phase 1's canonical sort exactly. The hash routine re-orders // by canonical path internally, but sorting AllVolumes here keeps @@ -1240,6 +1322,7 @@ namespace TD.Levels.Editor target.PlacementGrid = src.PlacementGrid; target.WalkabilityGrid = src.WalkabilityGrid; target.OwnerGrid = src.OwnerGrid; + target.MapAreaGrid = src.MapAreaGrid; target.PlayerZones = src.PlayerZones; target.Goals = src.Goals; @@ -1272,6 +1355,7 @@ namespace TD.Levels.Editor public List SpawnerVolumes; public List LeakExitVolumes; public List GoalVolumes; + public List MapAreaVolumes; // Phase 2 outputs public HashSet ExpectedOwners; @@ -1281,6 +1365,7 @@ namespace TD.Levels.Editor public Dictionary> PerVolumeTiles; public Dictionary> PerOwnerZoneTiles; public List> PerGoalTiles; + public HashSet MapAreaTiles; public Vector2Int MapMinTile; public Vector2Int MapMaxTile; @@ -1289,6 +1374,7 @@ namespace TD.Levels.Editor public Vector2Int GridSize; public PlacementState[,] PlacementGrid2D; public bool[,] WalkabilityGrid2D; + public bool[,] MapAreaGrid2D; // Phase 5 outputs public HashSet GoalAdjacentZones; @@ -1297,4 +1383,4 @@ namespace TD.Levels.Editor public LevelData AssembledData; public string ThumbnailPath; } -} +} \ No newline at end of file diff --git a/Assets/_Project/Scripts/Gameplay/GoldManager.cs b/Assets/_Project/Scripts/Gameplay/GoldManager.cs deleted file mode 100644 index 393ec0f..0000000 --- a/Assets/_Project/Scripts/Gameplay/GoldManager.cs +++ /dev/null @@ -1,147 +0,0 @@ -using Unity.Netcode; -using UnityEngine; - -namespace TD.Gameplay -{ - /// - /// GoldManager — canonical server-authoritative template for this project. - /// - /// Every gameplay system (towers, enemies, waves, damage) should follow - /// the same three-beat pattern demonstrated here: - /// 1. State lives in NetworkVariables, which only the server can write. - /// 2. Clients REQUEST changes via [Rpc(SendTo.Server, ...)] methods. - /// They never change state directly. - /// 3. The server VALIDATES the request before applying it. - /// Never trust the client. - /// - /// Cosmetic-only reactions (sounds, VFX, UI popups) can use - /// [Rpc(SendTo.ClientsAndHost)] or [Rpc(SendTo.NotServer)] to broadcast. - /// - public class GoldManager : NetworkBehaviour - { - // --- Tunables (editable in Inspector) ----------------------------- - - [Tooltip("How much gold every player starts with when the game begins.")] - [SerializeField] private int startingGold = 100; - - // --- Networked state ---------------------------------------------- - - // A NetworkVariable automatically syncs from server to clients. - // readPerm = Everyone: all clients can read the current value. - // writePerm = Server: only the server can change it. - private readonly NetworkVariable currentGold = new NetworkVariable( - value: 0, - readPerm: NetworkVariableReadPermission.Everyone, - writePerm: NetworkVariableWritePermission.Server - ); - - // Public read-only accessor for other scripts (UI, tower placement). - public int CurrentGold => currentGold.Value; - - // --- Lifecycle ---------------------------------------------------- - - /// - /// OnNetworkSpawn runs on every peer (server + all clients) when this - /// NetworkBehaviour becomes active on the network. Replaces Start() - /// for networked setup. - /// - public override void OnNetworkSpawn() - { - Debug.Log($"[GoldManager] OnNetworkSpawn ran. IsServer={IsServer}, IsClient={IsClient}, IsHost={IsHost}"); - - currentGold.OnValueChanged += HandleGoldChanged; - Debug.Log($"[GoldManager] Subscribed to OnValueChanged. Current value before init: {currentGold.Value}"); - - if (IsServer) - { - currentGold.Value = startingGold; - Debug.Log($"[GoldManager] Server initialized gold. Current value after set: {currentGold.Value}"); - } - } - - public override void OnNetworkDespawn() - { - // Always unsubscribe to avoid callback leaks. - currentGold.OnValueChanged -= HandleGoldChanged; - } - - private void HandleGoldChanged(int previous, int current) - { - // Fires on every peer whenever the value syncs. Use Log here so - // you can see syncing in the Console during development. - Debug.Log($"[GoldManager] Gold changed: {previous} -> {current}"); - } - - // --- Public API (called by client-side code) ---------------------- - - /// - /// Client-side entry point for spending gold. Called by gameplay code - /// like TowerPlacement when the local player clicks "build tower." - /// - /// The actual spending happens on the server via the Rpc. - /// - public void RequestSpendGold(int amount) - { - SpendGoldRpc(amount); - } - - /// - /// Server-side entry point for awarding gold (wave clear, enemy kill). - /// Not Rpc-wrapped — this is called directly by server game logic in - /// response to server-authoritative events. - /// - public void AwardGold(int amount) - { - if (!IsServer) - { - Debug.LogError("[GoldManager] AwardGold called on a client! " + - "Only server code should call this directly."); - return; - } - - if (amount <= 0) return; - currentGold.Value += amount; - } - - // --- Server-side RPC ---------------------------------------------- - - // [Rpc(SendTo.Server, ...)] means: a client calls this locally, but - // NGO routes the call and executes the method on the server. - // - // RequireOwnership = false lets any client call it (correct for a - // shared GoldManager). For per-player NetworkObjects you'd usually - // leave the default ownership requirement in place. - // - // Naming convention: methods with [Rpc] attributes must end with "Rpc". - // The source generator relies on this suffix. - [Rpc(SendTo.Server, RequireOwnership = false)] - private void SpendGoldRpc(int amount, RpcParams rpcParams = default) - { - // This method body runs on the server only. - // Validate everything — do not trust the client. - - // Validation 1: reject non-positive amounts. A negative amount - // would let a malicious client GAIN gold if we just subtracted. - if (amount <= 0) - { - Debug.LogWarning($"[GoldManager] Rejected spend of {amount} " + - $"from client {rpcParams.Receive.SenderClientId}: " + - $"amount must be positive."); - return; - } - - // Validation 2: can't spend more than current balance. - if (currentGold.Value < amount) - { - Debug.LogWarning($"[GoldManager] Rejected spend of {amount} " + - $"from client {rpcParams.Receive.SenderClientId}: " + - $"insufficient funds (have {currentGold.Value})."); - return; - } - - // Server applies the change. NetworkVariable syncs to clients - // automatically at the next network tick. - currentGold.Value -= amount; - } - } -} \ No newline at end of file diff --git a/Assets/_Project/Scripts/Gameplay/GoldManager.cs.meta b/Assets/_Project/Scripts/Gameplay/GoldManager.cs.meta deleted file mode 100644 index 53e1a6e..0000000 --- a/Assets/_Project/Scripts/Gameplay/GoldManager.cs.meta +++ /dev/null @@ -1,2 +0,0 @@ -fileFormatVersion: 2 -guid: d44ebdd0b2fc4144c8f8a181a714b738 \ No newline at end of file diff --git a/Assets/_Project/Scripts/Gameplay/LevelLoader.cs b/Assets/_Project/Scripts/Gameplay/LevelLoader.cs index 01c6969..b515909 100644 --- a/Assets/_Project/Scripts/Gameplay/LevelLoader.cs +++ b/Assets/_Project/Scripts/Gameplay/LevelLoader.cs @@ -264,6 +264,23 @@ namespace TD.Gameplay return x >= 0 && x < level.GridSize.x && y >= 0 && y < level.GridSize.y; } + /// + /// True if is part of the playable map area (inside any + /// MapAreaVolume at bake time). Returns false for out-of-bounds tiles and for in-bounds + /// "void" tiles outside the map area. This is the outermost gate — gameplay queries + /// (IsWalkable, GetPlacement, GetOwner) are only meaningful where IsInMap is true. + /// + /// Use this for: builder movement clamp, camera pan clamp, minimap rendering bounds. + /// + public bool IsInMap(Vector2Int tile) + { + if (!TryFlatIndex(tile, out int idx)) return false; + // Defensive: existing maps that haven't been re-baked since MapAreaGrid was added + // will have a null array. Treat that as "not in map" so callers don't false-positive. + if (level.MapAreaGrid == null || level.MapAreaGrid.Length == 0) return false; + return level.MapAreaGrid[idx]; + } + /// /// True if is currently walkable. Returns /// false for out-of-bounds tiles. Reflects the runtime walkability @@ -451,4 +468,4 @@ namespace TD.Gameplay } } } -} +} \ No newline at end of file diff --git a/Assets/_Project/Scripts/Gameplay/PlayerGoldManager.cs b/Assets/_Project/Scripts/Gameplay/PlayerGoldManager.cs new file mode 100644 index 0000000..65dfca6 --- /dev/null +++ b/Assets/_Project/Scripts/Gameplay/PlayerGoldManager.cs @@ -0,0 +1,178 @@ +using System.Collections.Generic; +using Unity.Netcode; +using UnityEngine; + +namespace TD.Gameplay +{ + /// + /// Per-player gold pool. One instance is spawned per connected client (via + /// ) and owned by that client. + /// + /// Replaces the earlier singleton-style GoldManager. Same server-authoritative + /// pattern (NetworkVariable + server-validated Rpc), but every player has their + /// own pool instead of sharing one. + /// + /// Three-beat pattern (unchanged from the original template): + /// 1. State lives in NetworkVariables (server-only writes). + /// 2. The owning client REQUESTS spends via a server-targeted Rpc. + /// 3. The server VALIDATES before applying. + /// + /// + /// Lookup ergonomics: each instance registers itself in a static dictionary + /// keyed by OwnerClientId during OnNetworkSpawn and removes itself during + /// OnNetworkDespawn. Server code that needs to award gold to a specific + /// player can call . + /// + /// This component is expected to live on the Player Prefab assigned to + /// NetworkManager. NGO handles spawn-on-connect and ownership assignment + /// automatically. + /// + public class PlayerGoldManager : NetworkBehaviour + { + // --- Static registry --------------------------------------------- + + // Keyed by OwnerClientId. Populated on every peer when an instance + // spawns; the same client can be looked up on the server (for awards) + // and on clients (for UI). Kept private — access goes through GetForClient. + private static readonly Dictionary s_byClientId + = new Dictionary(); + + /// + /// Returns the PlayerGoldManager owned by the given client, or null if + /// none is currently spawned. Safe to call on server or client. + /// + public static PlayerGoldManager GetForClient(ulong clientId) + { + s_byClientId.TryGetValue(clientId, out var manager); + return manager; + } + + /// + /// Convenience: the local client's own gold manager. Returns null on a + /// dedicated server or before the local player has spawned. + /// + public static PlayerGoldManager Local + { + get + { + var nm = NetworkManager.Singleton; + if (nm == null || !nm.IsClient) return null; + return GetForClient(nm.LocalClientId); + } + } + + // --- Tunables ---------------------------------------------------- + + [Tooltip("How much gold this player starts with when the match begins.")] + [SerializeField] private int startingGold = 100; + + // --- Networked state --------------------------------------------- + + // readPerm = Everyone so any client can read any other player's gold + // (needed for scoreboard / opponent UI). writePerm = Server keeps + // authority where it belongs. + private readonly NetworkVariable currentGold = new NetworkVariable( + value: 0, + readPerm: NetworkVariableReadPermission.Everyone, + writePerm: NetworkVariableWritePermission.Server + ); + + public int CurrentGold => currentGold.Value; + + // --- Lifecycle --------------------------------------------------- + + public override void OnNetworkSpawn() + { + Debug.Log($"[PlayerGoldManager] OnNetworkSpawn. OwnerClientId={OwnerClientId}, " + + $"IsOwner={IsOwner}, IsServer={IsServer}"); + + currentGold.OnValueChanged += HandleGoldChanged; + s_byClientId[OwnerClientId] = this; + + if (IsServer) + { + currentGold.Value = startingGold; + } + } + + public override void OnNetworkDespawn() + { + currentGold.OnValueChanged -= HandleGoldChanged; + + // Only remove if the entry still points to this instance — guards + // against the (unlikely) case where a new instance for the same + // client has already overwritten the slot. + if (s_byClientId.TryGetValue(OwnerClientId, out var registered) && registered == this) + { + s_byClientId.Remove(OwnerClientId); + } + } + + private void HandleGoldChanged(int previous, int current) + { + Debug.Log($"[PlayerGoldManager] Client {OwnerClientId} gold: {previous} -> {current}"); + } + + // --- Public API -------------------------------------------------- + + /// + /// Owning-client entry point for spending gold. Sends a server Rpc; + /// the server validates and applies. Calling this on a non-owning + /// client will be rejected by the Rpc's Owner permission. + /// + public void RequestSpendGold(int amount) + { + SpendGoldRpc(amount); + } + + /// + /// Server-side entry point for awarding gold (wave clear, enemy kill). + /// Direct call — not Rpc-wrapped — because awards always originate + /// from server-authoritative game events. + /// + public void AwardGold(int amount) + { + if (!IsServer) + { + Debug.LogError("[PlayerGoldManager] AwardGold called on a client. " + + "Only server code should call this directly."); + return; + } + + if (amount <= 0) return; + currentGold.Value += amount; + } + + // --- Server-side Rpc --------------------------------------------- + + // InvokePermission = Owner: only the client that owns this NetworkObject + // can invoke the Rpc. NGO will reject calls from anyone else, so a + // malicious client can't spend another player's gold. + // + // This replaces the old "RequireOwnership = false" + the implicit + // shared-pool semantics. The deprecation warning is gone. + [Rpc(SendTo.Server, InvokePermission = RpcInvokePermission.Owner)] + private void SpendGoldRpc(int amount, RpcParams rpcParams = default) + { + // Validation 1: positive amount. + if (amount <= 0) + { + Debug.LogWarning($"[PlayerGoldManager] Rejected spend of {amount} " + + $"from client {rpcParams.Receive.SenderClientId}: " + + $"amount must be positive."); + return; + } + + // Validation 2: sufficient funds. + if (currentGold.Value < amount) + { + Debug.LogWarning($"[PlayerGoldManager] Rejected spend of {amount} " + + $"from client {rpcParams.Receive.SenderClientId}: " + + $"insufficient funds (have {currentGold.Value})."); + return; + } + + currentGold.Value -= amount; + } + } +} diff --git a/Assets/_Project/Scripts/Gameplay/PlayerGoldManager.cs.meta b/Assets/_Project/Scripts/Gameplay/PlayerGoldManager.cs.meta new file mode 100644 index 0000000..53368a8 --- /dev/null +++ b/Assets/_Project/Scripts/Gameplay/PlayerGoldManager.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 6b9796562d7cc274f832657f21a61cce \ No newline at end of file