diff --git a/.claude/worktrees/dazzling-hoover-3d45a2/.claude/settings.local.json b/.claude/worktrees/dazzling-hoover-3d45a2/.claude/settings.local.json index b5a1a86..9a16201 100644 --- a/.claude/worktrees/dazzling-hoover-3d45a2/.claude/settings.local.json +++ b/.claude/worktrees/dazzling-hoover-3d45a2/.claude/settings.local.json @@ -6,7 +6,8 @@ "Bash(findstr /v \"Library\\\\PackageCache\")", "Bash(dir \"C:\\\\Users\\\\catos\\\\UnityTowerDefense\" /a-d)", "Bash(mkdir -p \"C:\\\\Users\\\\catos\\\\UnityTowerDefense\\\\Assets\\\\_Project\\\\UI\")", - "Bash(mkdir -p \"C:\\\\Users\\\\catos\\\\UnityTowerDefense\\\\Assets\\\\_Project\\\\Scripts\\\\UI\")" + "Bash(mkdir -p \"C:\\\\Users\\\\catos\\\\UnityTowerDefense\\\\Assets\\\\_Project\\\\Scripts\\\\UI\")", + "Bash(mkdir -p \"C:/Users/catos/UnityTowerDefense/Assets/_Project/Scripts/UI/Minimap\")" ] } } diff --git a/Assets/_Project/Art/Textures/MinimapRT.renderTexture b/Assets/_Project/Art/Textures/MinimapRT.renderTexture deleted file mode 100644 index 5f7d9fb..0000000 --- a/Assets/_Project/Art/Textures/MinimapRT.renderTexture +++ /dev/null @@ -1,39 +0,0 @@ -%YAML 1.1 -%TAG !u! tag:unity3d.com,2011: ---- !u!84 &8400000 -RenderTexture: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_Name: MinimapRT - m_ImageContentsHash: - serializedVersion: 2 - Hash: 00000000000000000000000000000000 - m_IsAlphaChannelOptional: 0 - serializedVersion: 6 - m_Width: 256 - m_Height: 256 - m_AntiAliasing: 1 - m_MipCount: -1 - m_DepthStencilFormat: 94 - m_ColorFormat: 8 - m_MipMap: 0 - m_GenerateMips: 1 - m_SRGB: 0 - m_UseDynamicScale: 0 - m_UseDynamicScaleExplicit: 0 - m_BindMS: 0 - m_EnableCompatibleFormat: 1 - m_EnableRandomWrite: 0 - m_TextureSettings: - serializedVersion: 2 - m_FilterMode: 1 - m_Aniso: 0 - m_MipBias: 0 - m_WrapU: 1 - m_WrapV: 1 - m_WrapW: 1 - m_Dimension: 2 - m_VolumeDepth: 1 - m_ShadowSamplingMode: 2 diff --git a/Assets/_Project/Scenes/Levels/Main.unity b/Assets/_Project/Scenes/Levels/Main.unity index 69b7952..d8a2e6d 100644 --- a/Assets/_Project/Scenes/Levels/Main.unity +++ b/Assets/_Project/Scenes/Levels/Main.unity @@ -237,134 +237,6 @@ Transform: m_Children: [] m_Father: {fileID: 0} m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} ---- !u!1 &260379225 -GameObject: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - serializedVersion: 6 - m_Component: - - component: {fileID: 260379229} - - component: {fileID: 260379228} - - component: {fileID: 260379226} - m_Layer: 0 - m_Name: MinimapCamera - m_TagString: Untagged - m_Icon: {fileID: 0} - m_NavMeshLayer: 0 - m_StaticEditorFlags: 0 - m_IsActive: 1 ---- !u!114 &260379226 -MonoBehaviour: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 260379225} - m_Enabled: 1 - m_EditorHideFlags: 0 - m_Script: {fileID: 11500000, guid: a79441f348de89743a2939f4d699eac1, type: 3} - m_Name: - m_EditorClassIdentifier: Unity.RenderPipelines.Universal.Runtime::UnityEngine.Rendering.Universal.UniversalAdditionalCameraData - 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: 0 - 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!20 &260379228 -Camera: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 260379225} - 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: 1 - orthographic size: 80 - m_Depth: 0 - m_CullingMask: - serializedVersion: 2 - m_Bits: 983 - m_RenderingPath: -1 - m_TargetTexture: {fileID: 8400000, guid: e640b4de7a1dee049b38712e7eff82ea, type: 2} - 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!4 &260379229 -Transform: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 260379225} - serializedVersion: 2 - m_LocalRotation: {x: 0.7071068, y: 0, z: 0, w: 0.7071068} - m_LocalPosition: {x: 14, y: 100, z: 39} - m_LocalScale: {x: 1, y: 1, z: 1} - m_ConstrainProportionsScale: 0 - m_Children: [] - m_Father: {fileID: 0} - m_LocalEulerAnglesHint: {x: 90, y: 0, z: 0} --- !u!1 &304575571 GameObject: m_ObjectHideFlags: 0 @@ -748,7 +620,7 @@ Transform: m_GameObject: {fileID: 441239879} serializedVersion: 2 m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} - m_LocalPosition: {x: 1, y: 0, z: 2} + m_LocalPosition: {x: 5, y: 0, z: 3} m_LocalScale: {x: 1, y: 1, z: 1} m_ConstrainProportionsScale: 0 m_Children: @@ -867,7 +739,7 @@ Transform: m_GameObject: {fileID: 611926972} serializedVersion: 2 m_LocalRotation: {x: -0, y: 0.70710576, z: -0, w: 0.70710784} - m_LocalPosition: {x: 39, y: 2, z: 40} + m_LocalPosition: {x: 43, y: 2, z: 41} m_LocalScale: {x: 100, y: 5, z: 20} m_ConstrainProportionsScale: 0 m_Children: [] @@ -979,7 +851,7 @@ Transform: m_GameObject: {fileID: 643505902} serializedVersion: 2 m_LocalRotation: {x: -0, y: 0.70710576, z: -0, w: 0.70710784} - m_LocalPosition: {x: 4, y: 2, z: 41} + m_LocalPosition: {x: 8, y: 2, z: 42} m_LocalScale: {x: 8, y: 5, z: 12.6126} m_ConstrainProportionsScale: 0 m_Children: [] @@ -1098,8 +970,8 @@ BoxCollider: m_ProvidesContacts: 0 m_Enabled: 1 serializedVersion: 3 - m_Size: {x: 32, y: 0.5, z: 87} - m_Center: {x: 14.5, y: 0, z: 41} + m_Size: {x: 36, y: 0.5, z: 88} + m_Center: {x: 12.5, y: 0, z: 40.5} --- !u!1 &1058315973 GameObject: m_ObjectHideFlags: 0 @@ -1132,7 +1004,7 @@ MonoBehaviour: m_EditorClassIdentifier: Assembly-CSharp::TD.UI.HUDController placementController: {fileID: 1597884411} placementManager: {fileID: 1507514108} - minimapRenderTexture: {fileID: 8400000, guid: e640b4de7a1dee049b38712e7eff82ea, type: 2} + cameraController: {fileID: 1239994223} rejectionMessageDuration: 2.5 --- !u!114 &1058315975 MonoBehaviour: @@ -1479,8 +1351,8 @@ BoxCollider: m_ProvidesContacts: 0 m_Enabled: 1 serializedVersion: 3 - m_Size: {x: 7, y: 1, z: 2} - m_Center: {x: 0, y: 0, z: 2.5} + m_Size: {x: 7, y: 1, z: 4} + m_Center: {x: 0, y: 0, z: 1.5} --- !u!1 &1464027360 GameObject: m_ObjectHideFlags: 0 @@ -1588,7 +1460,7 @@ Transform: m_GameObject: {fileID: 1464027360} serializedVersion: 2 m_LocalRotation: {x: 0, y: 0.7071068, z: 0, w: 0.7071068} - m_LocalPosition: {x: 14, y: 0, z: 39} + m_LocalPosition: {x: 14, y: 0, z: 41} m_LocalScale: {x: 10, y: 1, z: 5} m_ConstrainProportionsScale: 0 m_Children: [] @@ -1990,7 +1862,7 @@ Transform: m_GameObject: {fileID: 1949204941} serializedVersion: 2 m_LocalRotation: {x: -0, y: 0.70710576, z: -0, w: 0.70710784} - m_LocalPosition: {x: -11, y: 2, z: 40} + m_LocalPosition: {x: -7, y: 2, z: 41} m_LocalScale: {x: 100, y: 5, z: 20} m_ConstrainProportionsScale: 0 m_Children: [] @@ -2170,7 +2042,7 @@ Transform: m_GameObject: {fileID: 2024858685} serializedVersion: 2 m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} - m_LocalPosition: {x: 14, y: 2, z: 89} + m_LocalPosition: {x: 18, y: 2, z: 90} m_LocalScale: {x: 50, y: 5, z: 5} m_ConstrainProportionsScale: 0 m_Children: [] @@ -2282,7 +2154,7 @@ Transform: m_GameObject: {fileID: 2105067734} serializedVersion: 2 m_LocalRotation: {x: -0, y: 0.70710576, z: -0, w: 0.70710784} - m_LocalPosition: {x: 24, y: 2, z: 41} + m_LocalPosition: {x: 28, y: 2, z: 42} m_LocalScale: {x: 8, y: 5, z: 12.6126} m_ConstrainProportionsScale: 0 m_Children: [] @@ -2309,4 +2181,3 @@ SceneRoots: - {fileID: 611926976} - {fileID: 1222526238} - {fileID: 1058315976} - - {fileID: 260379229} diff --git a/Assets/_Project/Scenes/Levels/TestLevel.asset b/Assets/_Project/Scenes/Levels/TestLevel.asset index 5ab171e..169ec4f 100644 --- a/Assets/_Project/Scenes/Levels/TestLevel.asset +++ b/Assets/_Project/Scenes/Levels/TestLevel.asset @@ -18,208 +18,222 @@ MonoBehaviour: Author: Matt MapThumbnail: {fileID: 21300000, guid: d2e652d3e1c53454d80d3c1ec7888998, type: 3} ScenePath: Assets/_Project/Scenes/Levels/Main.unity - AuthoringHash: 613b9977ac3706b8fe14b6bc0e37256512c8c8d890d3051a3969d210e802649c - LastBakeTimestamp: 2026-05-09T00:16:03.7794768Z + AuthoringHash: 23fbb3b3dc3b0e03aa6f52cd2607c08275b33919120b2d96ea68b69ade0656f9 + LastBakeTimestamp: 2026-05-11T05:20:39.4889478Z LastBakeOutcome: 1 LastBakeWarningCount: 2 GridOriginTile: {x: 0, y: 0} - GridSize: {x: 32, y: 87} - PlacementGrid: 000000000000000000000000000000000000000000000000000000000000000000000000000000000000000202020202020200000000000000000000000000000000000000000000000000020202020202020000000000000000000000000000010101010101010101010101010101010101010101010101010101010101000001010101010101010101010101010101010101010101010101010101010100000101010101010101010101010101010101010101010101010101010101010000010101010101010101010101010101010101010101010101010101010101000001010101010101010101010101010101010101010101010101010101010100000101010101010101010101010101010101010101010101010101010101010000010101010101010101010101010101010101010101010101010101010101000001010101010101010101010101010101010101010101010101010101010100000101010101010101010101010101010101010101010101010101010101010000010101010101010101010101010101010101010101010101010101010101000001010101010101010101010101010101010101010101010101010101010100000101010101010101010101010101010101010101010101010101010101010000010101010101010101010101010101010101010101010101010101010101000001010101010101010101010101010101010101010101010101010101010100000101010101010101010101010101010101010101010101010101010101010000010101010101010101010101010101010101010101010101010101010101000001010101010101010101010101010101010101010101010101010101010100000101010101010101010101010101010101010101010101010101010101010000010101010101010101010101010101010101010101010101010101010101000001010101010101010101010101010101010101010101010101010101010100000101010101010101010101010101010101010101010101010101010101010000010101010101010101010101010101010101010101010101010101010101000001010101010101010101010101010101010101010101010101010101010100000101010101010101010101010101010101010101010101010101010101010000010101010101010101010101010101010101010101010101010101010101000001010101010101010101010101010101010101010101010101010101010100000101010101010101010101010101010101010101010101010101010101010000010101010101010101010101010101010101010101010101010101010101000001010101010101010101010101010101010101010101010101010101010100000101010101010101010101010101010101010101010101010101010101010000010101010101010101010101010101010101010101010101010101010101000001010101010101010101010101010101010101010101010101010101010100000101010101010101010101010101010101010101010101010101010101010000010101010101010101010101010101010101010101010101010101010101000000000000000000000000000202020202020200000000000000000000000000000000000000000000000000020202020202020000000000000000000000000000000000000000000000000002020202020202000000000000000000000000000000000000000000000000000202020202020200000000000000000000000000000000000000000000000000020202020202020000000000000000000000000000000000000000000000000002020202020202000000000000000000000000000000000000000000000000000202020202020200000000000000000000000000000000000000000000000000020202020202020000000000000000000000000000000000000000000000000002020202020202000000000000000000000000000001010101010101010101010101010101010101010101010101010101010000000101010101010101010101010101010101010101010101010101010101000000010101010101010101010101010101010101010101010101010101010100000001010101010101010101010101010101010101010101010101010101010000000101010101010101010101010101010101010101010101010101010101000000010101010101010101010101010101010101010101010101010101010100000001010101010101010101010101010101010101010101010101010101010000000101010101010101010101010101010101010101010101010101010101000000010101010101010101010101010101010101010101010101010101010100000001010101010101010101010101010101010101010101010101010101010000000101010101010101010101010101010101010101010101010101010101000000010101010101010101010101010101010101010101010101010101010100000001010101010101010101010101010101010101010101010101010101010000000101010101010101010101010101010101010101010101010101010101000000010101010101010101010101010101010101010101010101010101010100000001010101010101010101010101010101010101010101010101010101010000000101010101010101010101010101010101010101010101010101010101000000010101010101010101010101010101010101010101010101010101010100000001010101010101010101010101010101010101010101010101010101010000000101010101010101010101010101010101010101010101010101010101000000010101010101010101010101010101010101010101010101010101010100000001010101010101010101010101010101010101010101010101010101010000000101010101010101010101010101010101010101010101010101010101000000010101010101010101010101010101010101010101010101010101010100000001010101010101010101010101010101010101010101010101010101010000000101010101010101010101010101010101010101010101010101010101000000010101010101010101010101010101010101010101010101010101010100000001010101010101010101010101010101010101010101010101010101010000000101010101010101010101010101010101010101010101010101010101000000010101010101010101010101010101010101010101010101010101010100000001010101010101010101010101010101010101010101010101010101010000000101010101010101010101010101010101010101010101010101010101000000010101010101010101010101010101010101010101010101010101010100000001010101010101010101010101010101010101010101010101010101010000000000000000000000000000020202020202020000000000000000000000000000000000000000000000000002020202020202000000000000000000000000000000000000000000000000000202020202020200000000000000000000000000000000000000000000000000020202020202020000000000000000000000000000000000000000000000000002020202020202000000000000000000000000000000000000000000000000000202020202020200000000000000000000000000000000000000000000000000020202020202020000000000000000000000000000 - WalkabilityGrid: 000000000000000000000000000000000000000000000000000000000000000000000000000000000000000101010101010100000000000000000000000000000000000000000000000000010101010101010000000000000000000000000000010101010101010101010101010101010101010101010101010101010101000001010101010101010101010101010101010101010101010101010101010100000101010101010101010101010101010101010101010101010101010101010000010101010101010101010101010101010101010101010101010101010101000001010101010101010101010101010101010101010101010101010101010100000101010101010101010101010101010101010101010101010101010101010000010101010101010101010101010101010101010101010101010101010101000001010101010101010101010101010101010101010101010101010101010100000101010101010101010101010101010101010101010101010101010101010000010101010101010101010101010101010101010101010101010101010101000001010101010101010101010101010101010101010101010101010101010100000101010101010101010101010101010101010101010101010101010101010000010101010101010101010101010101010101010101010101010101010101000001010101010101010101010101010101010101010101010101010101010100000101010101010101010101010101010101010101010101010101010101010000010101010101010101010101010101010101010101010101010101010101000001010101010101010101010101010101010101010101010101010101010100000101010101010101010101010101010101010101010101010101010101010000010101010101010101010101010101010101010101010101010101010101000001010101010101010101010101010101010101010101010101010101010100000101010101010101010101010101010101010101010101010101010101010000010101010101010101010101010101010101010101010101010101010101000001010101010101010101010101010101010101010101010101010101010100000101010101010101010101010101010101010101010101010101010101010000010101010101010101010101010101010101010101010101010101010101000001010101010101010101010101010101010101010101010101010101010100000101010101010101010101010101010101010101010101010101010101010000010101010101010101010101010101010101010101010101010101010101000001010101010101010101010101010101010101010101010101010101010100000101010101010101010101010101010101010101010101010101010101010000010101010101010101010101010101010101010101010101010101010101000001010101010101010101010101010101010101010101010101010101010100000101010101010101010101010101010101010101010101010101010101010000010101010101010101010101010101010101010101010101010101010101000000000000000000000000000101010101010100000000000000000000000000000000000000000000000000010101010101010000000000000000000000000000000000000000000000000001010101010101000000000000000000000000000000000000000000000000000101010101010100000000000000000000000000000000000000000000000000010101010101010000000000000000000000000000000000000000000000000001010101010101000000000000000000000000000000000000000000000000000101010101010100000000000000000000000000000000000000000000000000010101010101010000000000000000000000000000000000000000000000000001010101010101000000000000000000000000000001010101010101010101010101010101010101010101010101010101010000000101010101010101010101010101010101010101010101010101010101000000010101010101010101010101010101010101010101010101010101010100000001010101010101010101010101010101010101010101010101010101010000000101010101010101010101010101010101010101010101010101010101000000010101010101010101010101010101010101010101010101010101010100000001010101010101010101010101010101010101010101010101010101010000000101010101010101010101010101010101010101010101010101010101000000010101010101010101010101010101010101010101010101010101010100000001010101010101010101010101010101010101010101010101010101010000000101010101010101010101010101010101010101010101010101010101000000010101010101010101010101010101010101010101010101010101010100000001010101010101010101010101010101010101010101010101010101010000000101010101010101010101010101010101010101010101010101010101000000010101010101010101010101010101010101010101010101010101010100000001010101010101010101010101010101010101010101010101010101010000000101010101010101010101010101010101010101010101010101010101000000010101010101010101010101010101010101010101010101010101010100000001010101010101010101010101010101010101010101010101010101010000000101010101010101010101010101010101010101010101010101010101000000010101010101010101010101010101010101010101010101010101010100000001010101010101010101010101010101010101010101010101010101010000000101010101010101010101010101010101010101010101010101010101000000010101010101010101010101010101010101010101010101010101010100000001010101010101010101010101010101010101010101010101010101010000000101010101010101010101010101010101010101010101010101010101000000010101010101010101010101010101010101010101010101010101010100000001010101010101010101010101010101010101010101010101010101010000000101010101010101010101010101010101010101010101010101010101000000010101010101010101010101010101010101010101010101010101010100000001010101010101010101010101010101010101010101010101010101010000000101010101010101010101010101010101010101010101010101010101000000010101010101010101010101010101010101010101010101010101010100000001010101010101010101010101010101010101010101010101010101010000000000000000000000000000010101010101010000000000000000000000000000000000000000000000000001010101010101000000000000000000000000000000000000000000000000000101010101010100000000000000000000000000000000000000000000000000010101010101010000000000000000000000000000000000000000000000000001010101010101000000000000000000000000000000000000000000000000000101010101010100000000000000000000000000000000000000000000000000010101010101010000000000000000000000000000 - OwnerGrid: 000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000020202020202020202020202020202020202020202020202020202020202000002020202020202020202020202020202020202020202020202020202020200000202020202020202020202020202020202020202020202020202020202020000020202020202020202020202020202020202020202020202020202020202000002020202020202020202020202020202020202020202020202020202020200000202020202020202020202020202020202020202020202020202020202020000020202020202020202020202020202020202020202020202020202020202000002020202020202020202020202020202020202020202020202020202020200000202020202020202020202020202020202020202020202020202020202020000020202020202020202020202020202020202020202020202020202020202000002020202020202020202020202020202020202020202020202020202020200000202020202020202020202020202020202020202020202020202020202020000020202020202020202020202020202020202020202020202020202020202000002020202020202020202020202020202020202020202020202020202020200000202020202020202020202020202020202020202020202020202020202020000020202020202020202020202020202020202020202020202020202020202000002020202020202020202020202020202020202020202020202020202020200000202020202020202020202020202020202020202020202020202020202020000020202020202020202020202020202020202020202020202020202020202000002020202020202020202020202020202020202020202020202020202020200000202020202020202020202020202020202020202020202020202020202020000020202020202020202020202020202020202020202020202020202020202000002020202020202020202020202020202020202020202020202020202020200000202020202020202020202020202020202020202020202020202020202020000020202020202020202020202020202020202020202020202020202020202000002020202020202020202020202020202020202020202020202020202020200000202020202020202020202020202020202020202020202020202020202020000020202020202020202020202020202020202020202020202020202020202000002020202020202020202020202020202020202020202020202020202020200000202020202020202020202020202020202020202020202020202020202020000020202020202020202020202020202020202020202020202020202020202000002020202020202020202020202020202020202020202020202020202020200000202020202020202020202020202020202020202020202020202020202020000020202020202020202020202020202020202020202020202020202020202000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001010101010101010101010101010101010101010101010101010101010000000101010101010101010101010101010101010101010101010101010101000000010101010101010101010101010101010101010101010101010101010100000001010101010101010101010101010101010101010101010101010101010000000101010101010101010101010101010101010101010101010101010101000000010101010101010101010101010101010101010101010101010101010100000001010101010101010101010101010101010101010101010101010101010000000101010101010101010101010101010101010101010101010101010101000000010101010101010101010101010101010101010101010101010101010100000001010101010101010101010101010101010101010101010101010101010000000101010101010101010101010101010101010101010101010101010101000000010101010101010101010101010101010101010101010101010101010100000001010101010101010101010101010101010101010101010101010101010000000101010101010101010101010101010101010101010101010101010101000000010101010101010101010101010101010101010101010101010101010100000001010101010101010101010101010101010101010101010101010101010000000101010101010101010101010101010101010101010101010101010101000000010101010101010101010101010101010101010101010101010101010100000001010101010101010101010101010101010101010101010101010101010000000101010101010101010101010101010101010101010101010101010101000000010101010101010101010101010101010101010101010101010101010100000001010101010101010101010101010101010101010101010101010101010000000101010101010101010101010101010101010101010101010101010101000000010101010101010101010101010101010101010101010101010101010100000001010101010101010101010101010101010101010101010101010101010000000101010101010101010101010101010101010101010101010101010101000000010101010101010101010101010101010101010101010101010101010100000001010101010101010101010101010101010101010101010101010101010000000101010101010101010101010101010101010101010101010101010101000000010101010101010101010101010101010101010101010101010101010100000001010101010101010101010101010101010101010101010101010101010000000101010101010101010101010101010101010101010101010101010101000000010101010101010101010101010101010101010101010101010101010100000001010101010101010101010101010101010101010101010101010101010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 - MapAreaGrid: 010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 + GridSize: {x: 36, y: 88} + PlacementGrid: 000000000000000000000000000000020202020202020000000000000000000000000000000000000000000000000000000000020202020202020000000000000000000000000000000000000000000000000000000000020202020202020000000000000000000000000000000000000000000000000000000000020202020202020000000000000000000000000000000000000101010101010101010101010101010101010101010101010101010101010000000000000101010101010101010101010101010101010101010101010101010101010000000000000101010101010101010101010101010101010101010101010101010101010000000000000101010101010101010101010101010101010101010101010101010101010000000000000101010101010101010101010101010101010101010101010101010101010000000000000101010101010101010101010101010101010101010101010101010101010000000000000101010101010101010101010101010101010101010101010101010101010000000000000101010101010101010101010101010101010101010101010101010101010000000000000101010101010101010101010101010101010101010101010101010101010000000000000101010101010101010101010101010101010101010101010101010101010000000000000101010101010101010101010101010101010101010101010101010101010000000000000101010101010101010101010101010101010101010101010101010101010000000000000101010101010101010101010101010101010101010101010101010101010000000000000101010101010101010101010101010101010101010101010101010101010000000000000101010101010101010101010101010101010101010101010101010101010000000000000101010101010101010101010101010101010101010101010101010101010000000000000101010101010101010101010101010101010101010101010101010101010000000000000101010101010101010101010101010101010101010101010101010101010000000000000101010101010101010101010101010101010101010101010101010101010000000000000101010101010101010101010101010101010101010101010101010101010000000000000101010101010101010101010101010101010101010101010101010101010000000000000101010101010101010101010101010101010101010101010101010101010000000000000101010101010101010101010101010101010101010101010101010101010000000000000101010101010101010101010101010101010101010101010101010101010000000000000101010101010101010101010101010101010101010101010101010101010000000000000101010101010101010101010101010101010101010101010101010101010000000000000101010101010101010101010101010101010101010101010101010101010000000000000101010101010101010101010101010101010101010101010101010101010000000000000101010101010101010101010101010101010101010101010101010101010000000000000101010101010101010101010101010101010101010101010101010101010000000000000101010101010101010101010101010101010101010101010101010101010000000000000101010101010101010101010101010101010101010101010101010101010000000000000101010101010101010101010101010101010101010101010101010101010000000000000101010101010101010101010101010101010101010101010101010101010000000000000000000000000000000000020202020202020000000000000000000000000000000000000000000000000000000000020202020202020000000000000000000000000000000000000000000000000000000000020202020202020000000000000000000000000000000000000000000000000000000000020202020202020000000000000000000000000000000000000000000000000000000000020202020202020000000000000000000000000000000000000000000000000000000000020202020202020000000000000000000000000000000000000000000000000000000000020202020202020000000000000000000000000000000000000000000000000000000000020202020202020000000000000000000000000000000000000000000000000000000000020202020202020000000000000000000000000000000000000101010101010101010101010101010101010101010101010101010101000000000000000101010101010101010101010101010101010101010101010101010101000000000000000101010101010101010101010101010101010101010101010101010101000000000000000101010101010101010101010101010101010101010101010101010101000000000000000101010101010101010101010101010101010101010101010101010101000000000000000101010101010101010101010101010101010101010101010101010101000000000000000101010101010101010101010101010101010101010101010101010101000000000000000101010101010101010101010101010101010101010101010101010101000000000000000101010101010101010101010101010101010101010101010101010101000000000000000101010101010101010101010101010101010101010101010101010101000000000000000101010101010101010101010101010101010101010101010101010101000000000000000101010101010101010101010101010101010101010101010101010101000000000000000101010101010101010101010101010101010101010101010101010101000000000000000101010101010101010101010101010101010101010101010101010101000000000000000101010101010101010101010101010101010101010101010101010101000000000000000101010101010101010101010101010101010101010101010101010101000000000000000101010101010101010101010101010101010101010101010101010101000000000000000101010101010101010101010101010101010101010101010101010101000000000000000101010101010101010101010101010101010101010101010101010101000000000000000101010101010101010101010101010101010101010101010101010101000000000000000101010101010101010101010101010101010101010101010101010101000000000000000101010101010101010101010101010101010101010101010101010101000000000000000101010101010101010101010101010101010101010101010101010101000000000000000101010101010101010101010101010101010101010101010101010101000000000000000101010101010101010101010101010101010101010101010101010101000000000000000101010101010101010101010101010101010101010101010101010101000000000000000101010101010101010101010101010101010101010101010101010101000000000000000101010101010101010101010101010101010101010101010101010101000000000000000101010101010101010101010101010101010101010101010101010101000000000000000101010101010101010101010101010101010101010101010101010101000000000000000101010101010101010101010101010101010101010101010101010101000000000000000101010101010101010101010101010101010101010101010101010101000000000000000101010101010101010101010101010101010101010101010101010101000000000000000101010101010101010101010101010101010101010101010101010101000000000000000000000000000000000000020202020202020000000000000000000000000000000000000000000000000000000000020202020202020000000000000000000000000000000000000000000000000000000000020202020202020000000000000000000000000000000000000000000000000000000000020202020202020000000000000000000000000000000000000000000000000000000000020202020202020000000000000000000000000000000000000000000000000000000000020202020202020000000000000000000000000000000000000000000000000000000000020202020202020000000000000000000000000000 + WalkabilityGrid: 000000000000000000000000000000010101010101010000000000000000000000000000000000000000000000000000000000010101010101010000000000000000000000000000000000000000000000000000000000010101010101010000000000000000000000000000000000000000000000000000000000010101010101010000000000000000000000000000000000000101010101010101010101010101010101010101010101010101010101010000000000000101010101010101010101010101010101010101010101010101010101010000000000000101010101010101010101010101010101010101010101010101010101010000000000000101010101010101010101010101010101010101010101010101010101010000000000000101010101010101010101010101010101010101010101010101010101010000000000000101010101010101010101010101010101010101010101010101010101010000000000000101010101010101010101010101010101010101010101010101010101010000000000000101010101010101010101010101010101010101010101010101010101010000000000000101010101010101010101010101010101010101010101010101010101010000000000000101010101010101010101010101010101010101010101010101010101010000000000000101010101010101010101010101010101010101010101010101010101010000000000000101010101010101010101010101010101010101010101010101010101010000000000000101010101010101010101010101010101010101010101010101010101010000000000000101010101010101010101010101010101010101010101010101010101010000000000000101010101010101010101010101010101010101010101010101010101010000000000000101010101010101010101010101010101010101010101010101010101010000000000000101010101010101010101010101010101010101010101010101010101010000000000000101010101010101010101010101010101010101010101010101010101010000000000000101010101010101010101010101010101010101010101010101010101010000000000000101010101010101010101010101010101010101010101010101010101010000000000000101010101010101010101010101010101010101010101010101010101010000000000000101010101010101010101010101010101010101010101010101010101010000000000000101010101010101010101010101010101010101010101010101010101010000000000000101010101010101010101010101010101010101010101010101010101010000000000000101010101010101010101010101010101010101010101010101010101010000000000000101010101010101010101010101010101010101010101010101010101010000000000000101010101010101010101010101010101010101010101010101010101010000000000000101010101010101010101010101010101010101010101010101010101010000000000000101010101010101010101010101010101010101010101010101010101010000000000000101010101010101010101010101010101010101010101010101010101010000000000000101010101010101010101010101010101010101010101010101010101010000000000000101010101010101010101010101010101010101010101010101010101010000000000000101010101010101010101010101010101010101010101010101010101010000000000000101010101010101010101010101010101010101010101010101010101010000000000000000000000000000000000010101010101010000000000000000000000000000000000000000000000000000000000010101010101010000000000000000000000000000000000000000000000000000000000010101010101010000000000000000000000000000000000000000000000000000000000010101010101010000000000000000000000000000000000000000000000000000000000010101010101010000000000000000000000000000000000000000000000000000000000010101010101010000000000000000000000000000000000000000000000000000000000010101010101010000000000000000000000000000000000000000000000000000000000010101010101010000000000000000000000000000000000000000000000000000000000010101010101010000000000000000000000000000000000000101010101010101010101010101010101010101010101010101010101000000000000000101010101010101010101010101010101010101010101010101010101000000000000000101010101010101010101010101010101010101010101010101010101000000000000000101010101010101010101010101010101010101010101010101010101000000000000000101010101010101010101010101010101010101010101010101010101000000000000000101010101010101010101010101010101010101010101010101010101000000000000000101010101010101010101010101010101010101010101010101010101000000000000000101010101010101010101010101010101010101010101010101010101000000000000000101010101010101010101010101010101010101010101010101010101000000000000000101010101010101010101010101010101010101010101010101010101000000000000000101010101010101010101010101010101010101010101010101010101000000000000000101010101010101010101010101010101010101010101010101010101000000000000000101010101010101010101010101010101010101010101010101010101000000000000000101010101010101010101010101010101010101010101010101010101000000000000000101010101010101010101010101010101010101010101010101010101000000000000000101010101010101010101010101010101010101010101010101010101000000000000000101010101010101010101010101010101010101010101010101010101000000000000000101010101010101010101010101010101010101010101010101010101000000000000000101010101010101010101010101010101010101010101010101010101000000000000000101010101010101010101010101010101010101010101010101010101000000000000000101010101010101010101010101010101010101010101010101010101000000000000000101010101010101010101010101010101010101010101010101010101000000000000000101010101010101010101010101010101010101010101010101010101000000000000000101010101010101010101010101010101010101010101010101010101000000000000000101010101010101010101010101010101010101010101010101010101000000000000000101010101010101010101010101010101010101010101010101010101000000000000000101010101010101010101010101010101010101010101010101010101000000000000000101010101010101010101010101010101010101010101010101010101000000000000000101010101010101010101010101010101010101010101010101010101000000000000000101010101010101010101010101010101010101010101010101010101000000000000000101010101010101010101010101010101010101010101010101010101000000000000000101010101010101010101010101010101010101010101010101010101000000000000000101010101010101010101010101010101010101010101010101010101000000000000000101010101010101010101010101010101010101010101010101010101000000000000000000000000000000000000010101010101010000000000000000000000000000000000000000000000000000000000010101010101010000000000000000000000000000000000000000000000000000000000010101010101010000000000000000000000000000000000000000000000000000000000010101010101010000000000000000000000000000000000000000000000000000000000010101010101010000000000000000000000000000000000000000000000000000000000010101010101010000000000000000000000000000000000000000000000000000000000010101010101010000000000000000000000000000 + OwnerGrid: 000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000202020202020202020202020202020202020202020202020202020202020000000000000202020202020202020202020202020202020202020202020202020202020000000000000202020202020202020202020202020202020202020202020202020202020000000000000202020202020202020202020202020202020202020202020202020202020000000000000202020202020202020202020202020202020202020202020202020202020000000000000202020202020202020202020202020202020202020202020202020202020000000000000202020202020202020202020202020202020202020202020202020202020000000000000202020202020202020202020202020202020202020202020202020202020000000000000202020202020202020202020202020202020202020202020202020202020000000000000202020202020202020202020202020202020202020202020202020202020000000000000202020202020202020202020202020202020202020202020202020202020000000000000202020202020202020202020202020202020202020202020202020202020000000000000202020202020202020202020202020202020202020202020202020202020000000000000202020202020202020202020202020202020202020202020202020202020000000000000202020202020202020202020202020202020202020202020202020202020000000000000202020202020202020202020202020202020202020202020202020202020000000000000202020202020202020202020202020202020202020202020202020202020000000000000202020202020202020202020202020202020202020202020202020202020000000000000202020202020202020202020202020202020202020202020202020202020000000000000202020202020202020202020202020202020202020202020202020202020000000000000202020202020202020202020202020202020202020202020202020202020000000000000202020202020202020202020202020202020202020202020202020202020000000000000202020202020202020202020202020202020202020202020202020202020000000000000202020202020202020202020202020202020202020202020202020202020000000000000202020202020202020202020202020202020202020202020202020202020000000000000202020202020202020202020202020202020202020202020202020202020000000000000202020202020202020202020202020202020202020202020202020202020000000000000202020202020202020202020202020202020202020202020202020202020000000000000202020202020202020202020202020202020202020202020202020202020000000000000202020202020202020202020202020202020202020202020202020202020000000000000202020202020202020202020202020202020202020202020202020202020000000000000202020202020202020202020202020202020202020202020202020202020000000000000202020202020202020202020202020202020202020202020202020202020000000000000202020202020202020202020202020202020202020202020202020202020000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000101010101010101010101010101010101010101010101010101010101000000000000000101010101010101010101010101010101010101010101010101010101000000000000000101010101010101010101010101010101010101010101010101010101000000000000000101010101010101010101010101010101010101010101010101010101000000000000000101010101010101010101010101010101010101010101010101010101000000000000000101010101010101010101010101010101010101010101010101010101000000000000000101010101010101010101010101010101010101010101010101010101000000000000000101010101010101010101010101010101010101010101010101010101000000000000000101010101010101010101010101010101010101010101010101010101000000000000000101010101010101010101010101010101010101010101010101010101000000000000000101010101010101010101010101010101010101010101010101010101000000000000000101010101010101010101010101010101010101010101010101010101000000000000000101010101010101010101010101010101010101010101010101010101000000000000000101010101010101010101010101010101010101010101010101010101000000000000000101010101010101010101010101010101010101010101010101010101000000000000000101010101010101010101010101010101010101010101010101010101000000000000000101010101010101010101010101010101010101010101010101010101000000000000000101010101010101010101010101010101010101010101010101010101000000000000000101010101010101010101010101010101010101010101010101010101000000000000000101010101010101010101010101010101010101010101010101010101000000000000000101010101010101010101010101010101010101010101010101010101000000000000000101010101010101010101010101010101010101010101010101010101000000000000000101010101010101010101010101010101010101010101010101010101000000000000000101010101010101010101010101010101010101010101010101010101000000000000000101010101010101010101010101010101010101010101010101010101000000000000000101010101010101010101010101010101010101010101010101010101000000000000000101010101010101010101010101010101010101010101010101010101000000000000000101010101010101010101010101010101010101010101010101010101000000000000000101010101010101010101010101010101010101010101010101010101000000000000000101010101010101010101010101010101010101010101010101010101000000000000000101010101010101010101010101010101010101010101010101010101000000000000000101010101010101010101010101010101010101010101010101010101000000000000000101010101010101010101010101010101010101010101010101010101000000000000000101010101010101010101010101010101010101010101010101010101000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 + MapAreaGrid: 010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 PlayerZones: - Owner: 1 Spawners: - SpawnerIdInZone: 0 - TilePosition: {x: 14, y: 83} + TilePosition: {x: 18, y: 84} TileArea: - - {x: 11, y: 80} - - {x: 12, y: 80} - - {x: 13, y: 80} - - {x: 14, y: 80} - - {x: 15, y: 80} - - {x: 16, y: 80} - - {x: 17, y: 80} - - {x: 11, y: 81} - - {x: 12, y: 81} - - {x: 13, y: 81} - - {x: 14, y: 81} - {x: 15, y: 81} - {x: 16, y: 81} - {x: 17, y: 81} - - {x: 11, y: 82} - - {x: 12, y: 82} - - {x: 13, y: 82} - - {x: 14, y: 82} + - {x: 18, y: 81} + - {x: 19, y: 81} + - {x: 20, y: 81} + - {x: 21, y: 81} - {x: 15, y: 82} - {x: 16, y: 82} - {x: 17, y: 82} - - {x: 11, y: 83} - - {x: 12, y: 83} - - {x: 13, y: 83} - - {x: 14, y: 83} + - {x: 18, y: 82} + - {x: 19, y: 82} + - {x: 20, y: 82} + - {x: 21, y: 82} - {x: 15, y: 83} - {x: 16, y: 83} - {x: 17, y: 83} - - {x: 11, y: 84} - - {x: 12, y: 84} - - {x: 13, y: 84} - - {x: 14, y: 84} + - {x: 18, y: 83} + - {x: 19, y: 83} + - {x: 20, y: 83} + - {x: 21, y: 83} - {x: 15, y: 84} - {x: 16, y: 84} - {x: 17, y: 84} - - {x: 11, y: 85} - - {x: 12, y: 85} - - {x: 13, y: 85} - - {x: 14, y: 85} + - {x: 18, y: 84} + - {x: 19, y: 84} + - {x: 20, y: 84} + - {x: 21, y: 84} - {x: 15, y: 85} - {x: 16, y: 85} - {x: 17, y: 85} - - {x: 11, y: 86} - - {x: 12, y: 86} - - {x: 13, y: 86} - - {x: 14, y: 86} + - {x: 18, y: 85} + - {x: 19, y: 85} + - {x: 20, y: 85} + - {x: 21, y: 85} - {x: 15, y: 86} - {x: 16, y: 86} - {x: 17, y: 86} + - {x: 18, y: 86} + - {x: 19, y: 86} + - {x: 20, y: 86} + - {x: 21, y: 86} + - {x: 15, y: 87} + - {x: 16, y: 87} + - {x: 17, y: 87} + - {x: 18, y: 87} + - {x: 19, y: 87} + - {x: 20, y: 87} + - {x: 21, y: 87} Facing: 1 LeakExits: - Target: 2 TileArea: - - {x: 11, y: 37} - - {x: 12, y: 37} - - {x: 13, y: 37} - - {x: 14, y: 37} - - {x: 15, y: 37} - - {x: 16, y: 37} - - {x: 17, y: 37} - - {x: 11, y: 38} - - {x: 12, y: 38} - - {x: 13, y: 38} - - {x: 14, y: 38} - {x: 15, y: 38} - {x: 16, y: 38} - {x: 17, y: 38} - - {x: 11, y: 39} - - {x: 12, y: 39} - - {x: 13, y: 39} - - {x: 14, y: 39} + - {x: 18, y: 38} + - {x: 19, y: 38} + - {x: 20, y: 38} + - {x: 21, y: 38} - {x: 15, y: 39} - {x: 16, y: 39} - {x: 17, y: 39} - - {x: 11, y: 40} - - {x: 12, y: 40} - - {x: 13, y: 40} - - {x: 14, y: 40} + - {x: 18, y: 39} + - {x: 19, y: 39} + - {x: 20, y: 39} + - {x: 21, y: 39} - {x: 15, y: 40} - {x: 16, y: 40} - {x: 17, y: 40} - - {x: 11, y: 41} - - {x: 12, y: 41} - - {x: 13, y: 41} - - {x: 14, y: 41} + - {x: 18, y: 40} + - {x: 19, y: 40} + - {x: 20, y: 40} + - {x: 21, y: 40} - {x: 15, y: 41} - {x: 16, y: 41} - {x: 17, y: 41} - - {x: 11, y: 42} - - {x: 12, y: 42} - - {x: 13, y: 42} - - {x: 14, y: 42} + - {x: 18, y: 41} + - {x: 19, y: 41} + - {x: 20, y: 41} + - {x: 21, y: 41} - {x: 15, y: 42} - {x: 16, y: 42} - {x: 17, y: 42} - - {x: 11, y: 43} - - {x: 12, y: 43} - - {x: 13, y: 43} - - {x: 14, y: 43} + - {x: 18, y: 42} + - {x: 19, y: 42} + - {x: 20, y: 42} + - {x: 21, y: 42} - {x: 15, y: 43} - {x: 16, y: 43} - {x: 17, y: 43} - - {x: 11, y: 44} - - {x: 12, y: 44} - - {x: 13, y: 44} - - {x: 14, y: 44} + - {x: 18, y: 43} + - {x: 19, y: 43} + - {x: 20, y: 43} + - {x: 21, y: 43} - {x: 15, y: 44} - {x: 16, y: 44} - {x: 17, y: 44} - - {x: 11, y: 45} - - {x: 12, y: 45} - - {x: 13, y: 45} - - {x: 14, y: 45} + - {x: 18, y: 44} + - {x: 19, y: 44} + - {x: 20, y: 44} + - {x: 21, y: 44} - {x: 15, y: 45} - {x: 16, y: 45} - {x: 17, y: 45} + - {x: 18, y: 45} + - {x: 19, y: 45} + - {x: 20, y: 45} + - {x: 21, y: 45} + - {x: 15, y: 46} + - {x: 16, y: 46} + - {x: 17, y: 46} + - {x: 18, y: 46} + - {x: 19, y: 46} + - {x: 20, y: 46} + - {x: 21, y: 46} NormalizedWeight: 1 - Owner: 2 Spawners: - SpawnerIdInZone: 0 - TilePosition: {x: 14, y: 40} + TilePosition: {x: 18, y: 41} TileArea: - - {x: 11, y: 37} - - {x: 12, y: 37} - - {x: 13, y: 37} - - {x: 14, y: 37} - - {x: 15, y: 37} - - {x: 16, y: 37} - - {x: 17, y: 37} - - {x: 11, y: 38} - - {x: 12, y: 38} - - {x: 13, y: 38} - - {x: 14, y: 38} - {x: 15, y: 38} - {x: 16, y: 38} - {x: 17, y: 38} - - {x: 11, y: 39} - - {x: 12, y: 39} - - {x: 13, y: 39} - - {x: 14, y: 39} + - {x: 18, y: 38} + - {x: 19, y: 38} + - {x: 20, y: 38} + - {x: 21, y: 38} - {x: 15, y: 39} - {x: 16, y: 39} - {x: 17, y: 39} - - {x: 11, y: 40} - - {x: 12, y: 40} - - {x: 13, y: 40} - - {x: 14, y: 40} + - {x: 18, y: 39} + - {x: 19, y: 39} + - {x: 20, y: 39} + - {x: 21, y: 39} - {x: 15, y: 40} - {x: 16, y: 40} - {x: 17, y: 40} - - {x: 11, y: 41} - - {x: 12, y: 41} - - {x: 13, y: 41} - - {x: 14, y: 41} + - {x: 18, y: 40} + - {x: 19, y: 40} + - {x: 20, y: 40} + - {x: 21, y: 40} - {x: 15, y: 41} - {x: 16, y: 41} - {x: 17, y: 41} - - {x: 11, y: 42} - - {x: 12, y: 42} - - {x: 13, y: 42} - - {x: 14, y: 42} + - {x: 18, y: 41} + - {x: 19, y: 41} + - {x: 20, y: 41} + - {x: 21, y: 41} - {x: 15, y: 42} - {x: 16, y: 42} - {x: 17, y: 42} - - {x: 11, y: 43} - - {x: 12, y: 43} - - {x: 13, y: 43} - - {x: 14, y: 43} + - {x: 18, y: 42} + - {x: 19, y: 42} + - {x: 20, y: 42} + - {x: 21, y: 42} - {x: 15, y: 43} - {x: 16, y: 43} - {x: 17, y: 43} + - {x: 18, y: 43} + - {x: 19, y: 43} + - {x: 20, y: 43} + - {x: 21, y: 43} + - {x: 15, y: 44} + - {x: 16, y: 44} + - {x: 17, y: 44} + - {x: 18, y: 44} + - {x: 19, y: 44} + - {x: 20, y: 44} + - {x: 21, y: 44} Facing: 1 LeakExits: [] Goals: - TileArea: - - {x: 11, y: 1} - - {x: 12, y: 1} - - {x: 13, y: 1} - - {x: 14, y: 1} + - {x: 15, y: 0} + - {x: 16, y: 0} + - {x: 17, y: 0} + - {x: 18, y: 0} + - {x: 19, y: 0} + - {x: 20, y: 0} + - {x: 21, y: 0} - {x: 15, y: 1} - {x: 16, y: 1} - {x: 17, y: 1} - - {x: 11, y: 2} - - {x: 12, y: 2} - - {x: 13, y: 2} - - {x: 14, y: 2} + - {x: 18, y: 1} + - {x: 19, y: 1} + - {x: 20, y: 1} + - {x: 21, y: 1} - {x: 15, y: 2} - {x: 16, y: 2} - {x: 17, y: 2} + - {x: 18, y: 2} + - {x: 19, y: 2} + - {x: 20, y: 2} + - {x: 21, y: 2} + - {x: 15, y: 3} + - {x: 16, y: 3} + - {x: 17, y: 3} + - {x: 18, y: 3} + - {x: 19, y: 3} + - {x: 20, y: 3} + - {x: 21, y: 3} diff --git a/Assets/_Project/Scenes/Levels/TestLevel_Thumbnail.png b/Assets/_Project/Scenes/Levels/TestLevel_Thumbnail.png index 69e9589..5677440 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:57a63c71a6e777bc1d8852d8ba23b96474946cb7aa9b424f35a64cf7fd3149b4 -size 8744 +oid sha256:af5c029677d941da474a8bf47c7bafbf704308b7bde5335f4f311d0da621316c +size 9770 diff --git a/Assets/_Project/Scripts/Core/PlayerColors.cs b/Assets/_Project/Scripts/Core/PlayerColors.cs index 3d74d31..3fe5280 100644 --- a/Assets/_Project/Scripts/Core/PlayerColors.cs +++ b/Assets/_Project/Scripts/Core/PlayerColors.cs @@ -6,10 +6,11 @@ namespace TD.Core /// Canonical color palette for player slots, used by editor gizmos to color volumes by owner. /// /// - /// Color names follow the 9-player map design doc (red, green, blue, purple, yellow, gray, - /// teal, olive, dark gray). The exact RGB values have been tuned for readability when drawn - /// as translucent gizmos against Unity's default scene-view background. Tweak the constants - /// below if any color reads poorly in practice. + /// Color names follow the 9-player map design doc (orange, green, blue, purple, yellow, gray, + /// teal, olive, dark gray). Red is reserved for enemy units on the minimap, so Player 1 uses + /// orange instead. The exact RGB values have been tuned for readability when drawn as + /// translucent gizmos against Unity's default scene-view background. Tweak the constants below + /// if any color reads poorly in practice. /// /// The "ErrorPink" color is reserved for diagnostic use: if a volume's owner is /// (which should never happen on a valid volume), the gizmo @@ -19,7 +20,7 @@ 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 Player1Orange = HexRGB(0xE0, 0x7A, 0x2E); // orange (red reserved for enemies) 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 @@ -43,7 +44,7 @@ namespace TD.Core { switch (slot) { - case PlayerSlot.Player1: return Player1Red; + case PlayerSlot.Player1: return Player1Orange; case PlayerSlot.Player2: return Player2Green; case PlayerSlot.Player3: return Player3Blue; case PlayerSlot.Player4: return Player4Purple; diff --git a/Assets/_Project/Scripts/Gameplay/Builder.cs b/Assets/_Project/Scripts/Gameplay/Builder.cs index 6c6346f..18ca781 100644 --- a/Assets/_Project/Scripts/Gameplay/Builder.cs +++ b/Assets/_Project/Scripts/Gameplay/Builder.cs @@ -4,6 +4,7 @@ using Unity.Netcode; using UnityEngine; using TD.Core; using TD.Towers; +using TD.UI.Minimap; namespace TD.Gameplay { @@ -45,7 +46,7 @@ namespace TD.Gameplay /// traversal. /// [RequireComponent(typeof(NetworkObject))] - public class Builder : NetworkBehaviour + public class Builder : NetworkBehaviour, IMinimapEntity { // ----- Static registry -------------------------------------------- @@ -188,6 +189,7 @@ namespace TD.Gameplay public override void OnNetworkSpawn() { s_byClientId[OwnerClientId] = this; + MinimapEntityRegistry.Register(this); if (IsServer) { @@ -217,6 +219,7 @@ namespace TD.Gameplay { if (s_byClientId.TryGetValue(OwnerClientId, out var registered) && registered == this) s_byClientId.Remove(OwnerClientId); + MinimapEntityRegistry.Deregister(this); // Server-only cleanup: despawn any remaining build-site visuals so they // don't leak when a player disconnects mid-construction. @@ -233,6 +236,32 @@ namespace TD.Gameplay // (NetworkList is owned by NGO; no manual Dispose needed in NGO 2.x.) + // ----- IMinimapEntity --------------------------------------------- + // + // Read every minimap refresh tick. Position is read live (NetworkTransform + // interpolation handles remote-client smoothing). Color comes from the same + // OwnerClientId → PlayerSlot stub mapping used by ApplyOwnerColor; both will pick + // up the real mapping when MatchState lands. + + Vector3 IMinimapEntity.WorldPosition => transform.position; + + Color IMinimapEntity.MinimapColor + { + get + { + byte slotByte = (byte)(OwnerClientId + 1); + PlayerSlot slot = (slotByte >= 1 && slotByte <= 9) + ? (PlayerSlot)slotByte : PlayerSlot.None; + return PlayerColors.Get(slot); + } + } + + MinimapIconKind IMinimapEntity.IconKind => MinimapIconKind.Builder; + + // Diameter of the builder dot in world units. The view enforces a pixel-size floor so + // builders remain visible at full zoom-out regardless of this value. + float IMinimapEntity.MinimapWorldSize => 0.6f; + // ----- Owner color tinting ---------------------------------------- // Lazily allocated; reused across renderers. Construction in a field initializer diff --git a/Assets/_Project/Scripts/Gameplay/CameraController.cs b/Assets/_Project/Scripts/Gameplay/CameraController.cs index 9b56e65..8504ec0 100644 --- a/Assets/_Project/Scripts/Gameplay/CameraController.cs +++ b/Assets/_Project/Scripts/Gameplay/CameraController.cs @@ -3,6 +3,7 @@ using UnityEngine; using UnityEngine.InputSystem; using TD.Core; using TD.Levels; +using TD.UI; namespace TD.Gameplay { @@ -218,6 +219,12 @@ namespace TD.Gameplay var kb = Keyboard.current; if (mouse == null) return; + // If the cursor is over an interactive HUD element (e.g., the minimap, which has + // its own scroll-wheel zoom), don't also drive the world camera. UI Toolkit's + // WheelEvent.StopPropagation only blocks UI-side bubbling — it has no effect on + // raw Input System polling, so we have to gate here explicitly. + if (HUDController.IsPointerOverInteractiveHud(mouse.position.ReadValue())) return; + // Scroll wheel values are inconsistent across platforms and devices: // - Windows click-wheel mice: ±120 per click (legacy Win32 convention) // - Free-spin wheels and trackpads: small fractional values per tick diff --git a/Assets/_Project/Scripts/Gameplay/TowerInstance.cs b/Assets/_Project/Scripts/Gameplay/TowerInstance.cs index ba5a535..9a47002 100644 --- a/Assets/_Project/Scripts/Gameplay/TowerInstance.cs +++ b/Assets/_Project/Scripts/Gameplay/TowerInstance.cs @@ -4,6 +4,7 @@ using Unity.Netcode; using UnityEngine; using TD.Core; using TD.Towers; +using TD.UI.Minimap; namespace TD.Gameplay { @@ -42,7 +43,7 @@ namespace TD.Gameplay /// TowerCombat component added to the same prefab. /// [RequireComponent(typeof(NetworkObject))] - public class TowerInstance : NetworkBehaviour + public class TowerInstance : NetworkBehaviour, IMinimapEntity { // ----- Networked state ------------------------------------------------ @@ -167,6 +168,9 @@ namespace TD.Gameplay // Apply owner color to the mesh renderer. ApplyOwnerColor(); + // Register for minimap rendering. + MinimapEntityRegistry.Register(this); + if (resolvedDefinition != null) { Debug.Log($"[TowerInstance] Spawned '{resolvedDefinition.DisplayName}' " + @@ -180,6 +184,33 @@ namespace TD.Gameplay // Un-stamp the footprint when the tower is destroyed (sold, wave end, etc.) // so the tiles become walkable and buildable again. StampFootprint(walkable: true, occupied: false); + + MinimapEntityRegistry.Deregister(this); + } + + // ----- IMinimapEntity ------------------------------------------------- + // + // Towers are static, so WorldPosition is cheap (no movement to track). Color reflects + // the replicated ownerSlot; reads safely on every client because ownerSlot is set in + // OnNetworkSpawn before this entity is added to the registry. + + Vector3 IMinimapEntity.WorldPosition => transform.position; + Color IMinimapEntity.MinimapColor => PlayerColors.Get(ownerSlot.Value); + MinimapIconKind IMinimapEntity.IconKind => MinimapIconKind.Tower; + + // Tower footprint in world units. Uses the larger axis if the footprint isn't square, + // so an Nx1 tower still occupies its full long-side on the minimap. + // Falls back to one tile if the definition hasn't resolved yet (transient, harmless). + float IMinimapEntity.MinimapWorldSize + { + get + { + if (resolvedDefinition == null) return GridCoordinates.TILE_SIZE; + int extent = Mathf.Max( + resolvedDefinition.FootprintSize.x, + resolvedDefinition.FootprintSize.y); + return extent * GridCoordinates.TILE_SIZE; + } } // ----- Private helpers ------------------------------------------------ diff --git a/Assets/_Project/Scripts/UI/HUDController.cs b/Assets/_Project/Scripts/UI/HUDController.cs index 179aafd..616917c 100644 --- a/Assets/_Project/Scripts/UI/HUDController.cs +++ b/Assets/_Project/Scripts/UI/HUDController.cs @@ -4,6 +4,7 @@ using UnityEngine; using UnityEngine.UIElements; using TD.Gameplay; using TD.Towers; +using TD.UI.Minimap; namespace TD.UI { @@ -24,9 +25,9 @@ namespace TD.UI [Tooltip("The TowerPlacementManager NetworkObject in the scene.")] [SerializeField] private TowerPlacementManager placementManager; - [Header("Minimap")] - [Tooltip("RenderTexture that the minimap camera renders into.")] - [SerializeField] private RenderTexture minimapRenderTexture; + [Tooltip("The local client's CameraController. Used by the minimap for click-to-jump " + + "and drag-to-pan.")] + [SerializeField] private CameraController cameraController; [Header("Settings")] [SerializeField] private float rejectionMessageDuration = 2.5f; @@ -48,6 +49,52 @@ namespace TD.UI private Coroutine rejectionFadeCoroutine; private bool gridPopulated; private bool uiInitialized; + private MinimapView minimapView; + private IPanel myPanel; // tracked separately so OnDestroy only clears the static if it still points at us + + // ----- Static hit-test probe -------------------------------------- + + // Set when InitializeUI succeeds; cleared on OnDestroy. Non-UI systems (camera, + // input handlers) can query IsPointerOverInteractiveHud without taking a direct + // reference to HUDController. + private static IPanel s_hudPanel; + + /// + /// True if falls over an interactive (non-ignore) + /// HUD element. Non-UI systems that consume mouse input (camera scroll-zoom, edge-pan) + /// should gate their handling on this so a cursor over the minimap, command grid, or + /// any other interactive HUD region doesn't drive both the HUD and the world at once. + /// + /// + /// Convention: uses Unity Input System screen coords + /// (origin bottom-left, y up). Returns false before the HUD has initialized; safe to + /// call from any system at any time. + /// + public static bool IsPointerOverInteractiveHud(Vector2 screenMousePos) + { + if (s_hudPanel == null) return false; + + // Coord convention rabbit hole: + // - Screen mouse position: origin bottom-left, y up (Unity Input System). + // - UI Toolkit panel coords: origin top-left, y down. + // + // RuntimePanelUtils.ScreenToPanel converts the SCALE (e.g., reference resolution + // vs. actual resolution) but does NOT flip Y. We flip manually using the visual + // tree's height so the result works regardless of PanelSettings scale mode. + // + // Subtle: visualTree.worldBound height may be 0 for one frame on the very first + // layout pass. The caller (CameraController) checks the result against "is over + // interactive HUD"; a one-frame false positive (camera zooms when it shouldn't) + // is harmless and self-corrects the next frame. + Vector2 scaled = RuntimePanelUtils.ScreenToPanel(s_hudPanel, screenMousePos); + float panelHeight = s_hudPanel.visualTree.worldBound.height; + Vector2 panelPos = new Vector2(scaled.x, panelHeight - scaled.y); + + // panel.Pick returns null when the topmost element under the point has + // PickingMode.Ignore (or there's no element there at all). Non-null means an + // interactive HUD element is under the cursor. + return s_hudPanel.Pick(panelPos) != null; + } // ----- Lifecycle -------------------------------------------------- @@ -101,15 +148,24 @@ namespace TD.UI SetEnabled(root, "upgrade-btn", false); SetEnabled(root, "sell-btn", false); - // Minimap RenderTexture. - if (minimapRenderTexture != null) + // Minimap. The MinimapView owns the two sub-elements (terrain + entity overlay) + // and drives them; we just hand it the host container and the camera controller. + // Bake is deferred until LevelLoader is ready — view tries each frame in Tick(). + var minimapContainer = root.Q("minimap"); + if (minimapContainer != null) { - var minimap = root.Q("minimap"); - if (minimap != null) - minimap.style.backgroundImage = - Background.FromRenderTexture(minimapRenderTexture); + if (cameraController == null) + Debug.LogWarning("[HUDController] cameraController not assigned. " + + "Click-to-jump and drag-to-pan on the minimap will be disabled."); + minimapView = new MinimapView(minimapContainer, cameraController); } + // Publish the panel so non-UI systems can query "is pointer over the HUD". + // Stored on `myPanel` too so OnDestroy only clears the static if it still + // points at this instance (defensive against re-creation overlap). + myPanel = root.panel; + s_hudPanel = myPanel; + uiInitialized = true; } @@ -131,6 +187,17 @@ namespace TD.UI TryPopulateCommandGrid(); RefreshGoldDisplay(); + minimapView?.Tick(); + } + + private void OnDestroy() + { + minimapView?.Dispose(); + minimapView = null; + + if (s_hudPanel != null && s_hudPanel == myPanel) + s_hudPanel = null; + myPanel = null; } // ----- Gold display ----------------------------------------------- diff --git a/Assets/_Project/Art/Textures/MinimapRT.renderTexture.meta b/Assets/_Project/Scripts/UI/Minimap.meta similarity index 52% rename from Assets/_Project/Art/Textures/MinimapRT.renderTexture.meta rename to Assets/_Project/Scripts/UI/Minimap.meta index 0d58917..64f6505 100644 --- a/Assets/_Project/Art/Textures/MinimapRT.renderTexture.meta +++ b/Assets/_Project/Scripts/UI/Minimap.meta @@ -1,8 +1,8 @@ fileFormatVersion: 2 -guid: e640b4de7a1dee049b38712e7eff82ea -NativeFormatImporter: +guid: dd886c53adc748f4391bc440c289d0ea +folderAsset: yes +DefaultImporter: externalObjects: {} - mainObjectFileID: 8400000 userData: assetBundleName: assetBundleVariant: diff --git a/Assets/_Project/Scripts/UI/Minimap/MinimapEntityRegistry.cs b/Assets/_Project/Scripts/UI/Minimap/MinimapEntityRegistry.cs new file mode 100644 index 0000000..9c14508 --- /dev/null +++ b/Assets/_Project/Scripts/UI/Minimap/MinimapEntityRegistry.cs @@ -0,0 +1,94 @@ +// Assets/_Project/Scripts/UI/Minimap/MinimapEntityRegistry.cs +using System; +using System.Collections.Generic; +using UnityEngine; + +namespace TD.UI.Minimap +{ + /// + /// Visual category for a minimap entity. Determines the shape and size of the icon drawn + /// by . Color is supplied separately by the entity itself so the + /// view does not need to know about ownership / faction mapping. + /// + public enum MinimapIconKind : byte + { + Enemy, + Tower, + Builder, + } + + /// + /// Anything that wants to appear on the minimap as a dynamic icon implements this. The view + /// reads every getter on each refresh tick, so values may change between reads (movement is + /// fine — no caching needed). + /// + public interface IMinimapEntity + { + Vector3 WorldPosition { get; } + Color MinimapColor { get; } + MinimapIconKind IconKind { get; } + + /// + /// The entity's diameter (or footprint extent) in world units. The view converts this + /// to pixels using the current minimap zoom, so a 2×2 tower will appear as a 2-tile + /// square on the minimap and adjacent towers will visually touch — matching their + /// world-space relationship. For point-like entities (builders, enemies) the view + /// also enforces a minimum pixel size so they stay visible when fully zoomed out. + /// + float MinimapWorldSize { get; } + } + + /// + /// Static registry of every entity currently visible on the minimap. Entities register + /// themselves on spawn and deregister on despawn; the iterates the + /// set on each refresh. + /// + /// + /// Backing store is a so duplicate Register calls are no-ops and + /// Deregister is O(1). Iteration uses the struct enumerator via to + /// avoid the boxing that would happen if we exposed an . + /// + /// Static lifetime is intentional: the registry survives scene transitions only if entities + /// remember to deregister. NGO's OnNetworkDespawn covers that for towers / builders / + /// enemies. Manual is provided for tests and for explicit reset on match + /// teardown if needed. + /// + public static class MinimapEntityRegistry + { + private static readonly HashSet s_entities = new HashSet(); + + /// Number of currently-registered entities. Cheap, no allocation. + public static int Count => s_entities.Count; + + /// + /// Registers for minimap rendering. No-op if already registered. + /// Safe to call from any thread that Unity allows (i.e., the main thread). + /// + public static void Register(IMinimapEntity entity) + { + if (entity == null) return; + s_entities.Add(entity); + } + + /// Removes from the registry. No-op if not present. + public static void Deregister(IMinimapEntity entity) + { + if (entity == null) return; + s_entities.Remove(entity); + } + + /// + /// Invokes once per registered entity. Uses HashSet's struct + /// enumerator so iteration is allocation-free (the only allocation is the closure that + /// may carry, which is the caller's choice). + /// + public static void ForEach(Action action) + { + if (action == null) return; + foreach (var e in s_entities) action(e); + } + + /// Drops every registered entity. Tests and explicit match-teardown use only. + public static void Clear() => s_entities.Clear(); + } +} diff --git a/Assets/_Project/Scripts/UI/Minimap/MinimapEntityRegistry.cs.meta b/Assets/_Project/Scripts/UI/Minimap/MinimapEntityRegistry.cs.meta new file mode 100644 index 0000000..11c129d --- /dev/null +++ b/Assets/_Project/Scripts/UI/Minimap/MinimapEntityRegistry.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 683a64c92b1c7a144b7f9717b854ef38 \ No newline at end of file diff --git a/Assets/_Project/Scripts/UI/Minimap/MinimapTerrainBaker.cs b/Assets/_Project/Scripts/UI/Minimap/MinimapTerrainBaker.cs new file mode 100644 index 0000000..61864b7 --- /dev/null +++ b/Assets/_Project/Scripts/UI/Minimap/MinimapTerrainBaker.cs @@ -0,0 +1,247 @@ +// Assets/_Project/Scripts/UI/Minimap/MinimapTerrainBaker.cs +using UnityEngine; +using TD.Core; +using TD.Gameplay; +using TD.Levels; + +namespace TD.UI.Minimap +{ + /// + /// Builds the static terrain layer of the WC3-style minimap as a + /// from the baked . One pixel per tile; the UI element scales the + /// texture up with Point filtering so the result reads as crisp, chunky map abstraction + /// rather than a tiny blurred photograph. + /// + /// + /// The terrain layer represents what cannot change during a match: zone ownership, spawner + /// locations, goal locations, and the map shape itself. Towers and enemies are drawn on a + /// separate dynamic overlay by , so this texture is baked once at + /// level load and never re-baked. + /// + public static class MinimapTerrainBaker + { + // ----- Palette ---------------------------------------------------- + // Picked for legibility at low resolution. Aim: each tile category reads as a distinct + // color block when scaled up 4–8×, even on a Steam Deck screen at running brightness. + + /// Tiles outside the playable map area. Fully transparent so the panel + /// background shows through. + private static readonly Color32 OutOfMap = new Color32(0, 0, 0, 0); + + /// In-map tiles that are not owned by any player and not part of a goal / + /// spawner. Dark neutral grey — reads as "playable but unallocated". + private static readonly Color32 NeutralInMap = new Color32(46, 46, 51, 255); + + /// Spawner tiles. Bright cyan — pops against every player zone color. + private static readonly Color32 SpawnerTile = new Color32(102, 217, 255, 255); + + /// Goal tiles. Matches (gold). + private static readonly Color32 GoalTile = new Color32(224, 176, 32, 255); + + // ----- Public API ------------------------------------------------- + + /// + /// Bakes the current level into a Texture2D and returns it. Returns null if the loader + /// is not ready. The returned texture has FilterMode.Point and is non-readable after + /// bake (CPU-side pixel data is released). + /// + /// + /// Caller is responsible for the texture lifetime: assign it to the minimap element and + /// destroy it when no longer needed. Currently holds the only + /// reference and lets it live for the duration of the match. + /// + public static Texture2D Bake(LevelLoader loader) + { + if (loader == null || !loader.IsLoaded) + { + Debug.LogWarning("[MinimapTerrainBaker] LevelLoader not ready; cannot bake terrain."); + return null; + } + + var data = loader.LevelData; + int w = data.GridSize.x; + int h = data.GridSize.y; + if (w <= 0 || h <= 0) + { + Debug.LogWarning($"[MinimapTerrainBaker] Invalid grid size {data.GridSize}; cannot bake."); + return null; + } + + // RGBA32 supports alpha for out-of-map transparency. mipChain off — the texture + // is rendered at near-1:1 in the UI and mipmaps would only blur the abstract look. + var tex = new Texture2D(w, h, TextureFormat.RGBA32, mipChain: false, linear: false) + { + filterMode = FilterMode.Point, + wrapMode = TextureWrapMode.Clamp, + name = "MinimapTerrain" + }; + + var pixels = new Color32[w * h]; + + // Pass 1: base tile color from MapArea + Owner. + // Texture y=0 is the BOTTOM row in Unity convention. World z and grid y both + // increase northward, so a tile at grid-y=0 should land at the bottom of the + // texture. That's exactly what `pixels[y * w + x] = ...` gives us, since SetPixels32 + // treats index 0 as bottom-left. No flip needed during bake — the flip happens at + // display time (UI y-axis points down, world z points up; MinimapView handles it). + for (int y = 0; y < h; y++) + { + for (int x = 0; x < w; x++) + { + int idx = y * w + x; + pixels[idx] = ResolveBaseColor(data, idx); + } + } + + // Pass 2: stamp special tile categories on top — spawners and goals override base. + // PlayerZones[].LeakExits intentionally NOT painted: leak exits are conceptually + // part of the source player's zone (an enemy boundary, not a player-visible + // landmark), and visually highlighting them would clutter the minimap. + StampZoneSpawners(pixels, data); + StampGoals(pixels, data); + + tex.SetPixels32(pixels); + tex.Apply(updateMipmaps: false, makeNoLongerReadable: true); + + LogBakeSummary(data, w, h); + return tex; + } + + // ----- Diagnostics ------------------------------------------------ + + /// + /// One-shot log of the bake breakdown. Useful for diagnosing apparent asymmetries in + /// the rendered minimap — a symmetric authoring should produce mirrored counts on + /// opposite edges. Per-player min/max columns reveal exactly where each zone starts + /// and ends across the grid. + /// + private static void LogBakeSummary(LevelData data, int w, int h) + { + int outOfMap = 0, neutralInMap = 0, spawnerTiles = 0, goalTiles = 0; + // Per-slot owned-tile bounds (1..9). + int[] ownedCount = new int[10]; + int[] minX = new int[10]; int[] maxX = new int[10]; + int[] minY = new int[10]; int[] maxY = new int[10]; + for (int i = 0; i < 10; i++) + { + minX[i] = int.MaxValue; minY[i] = int.MaxValue; + maxX[i] = int.MinValue; maxY[i] = int.MinValue; + } + + for (int y = 0; y < h; y++) + for (int x = 0; x < w; x++) + { + int idx = y * w + x; + bool inMap = data.MapAreaGrid != null && data.MapAreaGrid[idx]; + if (!inMap) { outOfMap++; continue; } + + PlayerSlot s = data.OwnerGrid != null ? data.OwnerGrid[idx] : PlayerSlot.None; + if (s == PlayerSlot.None) { neutralInMap++; continue; } + + int si = (int)s; + if (si < 0 || si > 9) continue; + ownedCount[si]++; + if (x < minX[si]) minX[si] = x; + if (x > maxX[si]) maxX[si] = x; + if (y < minY[si]) minY[si] = y; + if (y > maxY[si]) maxY[si] = y; + } + + if (data.PlayerZones != null) + foreach (var z in data.PlayerZones) + if (z?.Spawners != null) + foreach (var sp in z.Spawners) + if (sp?.TileArea != null) spawnerTiles += sp.TileArea.Length; + if (data.Goals != null) + foreach (var g in data.Goals) + if (g?.TileArea != null) goalTiles += g.TileArea.Length; + + var sb = new System.Text.StringBuilder(256); + sb.AppendFormat("[MinimapTerrainBaker] '{0}' baked. Grid {1}×{2} origin {3}. ", + data.MapName, w, h, data.GridOriginTile); + sb.AppendFormat("Tiles: outOfMap={0}, neutralInMap={1}, spawner={2}, goal={3}.", + outOfMap, neutralInMap, spawnerTiles, goalTiles); + for (int i = 1; i <= 9; i++) + { + if (ownedCount[i] == 0) continue; + sb.AppendFormat(" P{0}={1} (x:{2}–{3}, y:{4}–{5})", + i, ownedCount[i], minX[i], maxX[i], minY[i], maxY[i]); + } + Debug.Log(sb.ToString()); + } + + // ----- Pass implementations --------------------------------------- + + private static Color32 ResolveBaseColor(LevelData data, int idx) + { + // MapAreaGrid may be null on levels baked before that field existed; treat as + // "not in map" so we render transparent rather than a misleading neutral block. + bool inMap = data.MapAreaGrid != null + && idx < data.MapAreaGrid.Length + && data.MapAreaGrid[idx]; + if (!inMap) return OutOfMap; + + PlayerSlot owner = (data.OwnerGrid != null && idx < data.OwnerGrid.Length) + ? data.OwnerGrid[idx] + : PlayerSlot.None; + + if (owner == PlayerSlot.None) return NeutralInMap; + return ToZoneColor(PlayerColors.Get(owner)); + } + + private static void StampZoneSpawners(Color32[] pixels, LevelData data) + { + if (data.PlayerZones == null) return; + foreach (var zone in data.PlayerZones) + { + if (zone == null || zone.Spawners == null) continue; + foreach (var spawner in zone.Spawners) + { + if (spawner == null || spawner.TileArea == null) continue; + foreach (var tile in spawner.TileArea) + PaintTile(pixels, data, tile, SpawnerTile); + } + } + } + + private static void StampGoals(Color32[] pixels, LevelData data) + { + if (data.Goals == null) return; + foreach (var goal in data.Goals) + { + if (goal == null || goal.TileArea == null) continue; + foreach (var tile in goal.TileArea) + PaintTile(pixels, data, tile, GoalTile); + } + } + + private static void PaintTile(Color32[] pixels, LevelData data, Vector2Int worldTile, Color32 color) + { + int x = worldTile.x - data.GridOriginTile.x; + int y = worldTile.y - data.GridOriginTile.y; + if (x < 0 || x >= data.GridSize.x || y < 0 || y >= data.GridSize.y) return; + pixels[y * data.GridSize.x + x] = color; + } + + // ----- Color helpers ---------------------------------------------- + + /// + /// Translates a saturated player gizmo color into the dimmer, slightly desaturated tone + /// used for zone fill on the minimap. The full-saturation original is reserved for the + /// player's icons (towers, builders) so units pop against their own zone tint. + /// + private static Color32 ToZoneColor(Color full) + { + // Desaturate ~45% toward equal-luminance grey, then darken to ~70% brightness. + float lum = full.r * 0.299f + full.g * 0.587f + full.b * 0.114f; + float r = Mathf.Lerp(full.r, lum, 0.45f) * 0.70f; + float g = Mathf.Lerp(full.g, lum, 0.45f) * 0.70f; + float b = Mathf.Lerp(full.b, lum, 0.45f) * 0.70f; + return new Color32( + (byte)Mathf.RoundToInt(r * 255f), + (byte)Mathf.RoundToInt(g * 255f), + (byte)Mathf.RoundToInt(b * 255f), + 255); + } + } +} diff --git a/Assets/_Project/Scripts/UI/Minimap/MinimapTerrainBaker.cs.meta b/Assets/_Project/Scripts/UI/Minimap/MinimapTerrainBaker.cs.meta new file mode 100644 index 0000000..7cbb17e --- /dev/null +++ b/Assets/_Project/Scripts/UI/Minimap/MinimapTerrainBaker.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 3a3122ca07726044f8b63bbfb8f0230c \ No newline at end of file diff --git a/Assets/_Project/Scripts/UI/Minimap/MinimapView.cs b/Assets/_Project/Scripts/UI/Minimap/MinimapView.cs new file mode 100644 index 0000000..cd5ba5a --- /dev/null +++ b/Assets/_Project/Scripts/UI/Minimap/MinimapView.cs @@ -0,0 +1,502 @@ +// Assets/_Project/Scripts/UI/Minimap/MinimapView.cs +using UnityEngine; +using UnityEngine.UIElements; +using TD.Core; +using TD.Gameplay; +using TD.Levels; + +namespace TD.UI.Minimap +{ + /// + /// UI Toolkit minimap controller. Manages two stacked sub-elements (terrain + entities) + /// inside a host , handles click-to-jump, drag-to-pan, + /// right-click-to-move-builder, and scroll-wheel zoom against a + /// and the local . Refreshes the entity overlay at a throttled + /// rate so 500+ enemies don't dominate frame time. + /// + /// + /// Coordinate spaces. Three are in play: + /// + /// World — XZ plane positions of game-side entities. Z+ is north. + /// UI local — pixel coordinates within the minimap container. Y+ is DOWN. + /// Texture — handled entirely by the baker; not exposed here. + /// + /// World↔UI conversion uses the currently-visible world rectangle, which is the full map + /// at zoom 1 and a smaller rect centered on at zoom > 1. + /// + /// Zoom model. Zoom is anchored to the cursor position: the world point under + /// the cursor before the scroll stays under the cursor after, so the user can dial in on a + /// region by hovering over it and scrolling. Zoom defaults to 1 (fully zoomed out) and is + /// clamped to . When zoom returns to 1 the visible rect snaps back to + /// the full map. + /// + /// Terrain rendering. The terrain sub-element is sized and positioned so the + /// currently-visible world rect maps to the container's contentRect. At zoom 1 the element + /// fills the container exactly; at higher zoom it grows (in pixels) past the container and + /// the container's overflow: hidden clips the off-view portion. The texture itself + /// is baked once and never re-baked. + /// + public class MinimapView + { + // ----- Tuning ----------------------------------------------------- + + /// Entity overlay refresh rate. 20Hz handles 500+ enemies comfortably without + /// burning the frame budget on Painter2D. Revisit if testing shows it's not enough. + private const float EntityRepaintHz = 20f; + + private const float MinZoom = 1f; + private const float MaxZoom = 4f; + /// Multiplicative factor applied per scroll-wheel tick. 1.2 ≈ 6 ticks to span + /// 1x → 4x, which feels neither sluggish nor twitchy in playtest. + private const float ZoomStepFactor = 1.2f; + + // Pixel-size floor for point-like icons (builders, enemies). Ensures they stay visible + // even at full zoom-out on a large map. Towers use their actual footprint instead and + // do not need a floor — they're always at least a couple of pixels by virtue of being + // 2×2 or larger. + private const float BuilderMinRadiusPx = 2.4f; + private const float EnemyMinRadiusPx = 1.5f; + + // Outline added to builder icons so they read against same-color zone fill. + private static readonly Color BuilderOutline = new Color(1f, 1f, 1f, 0.85f); + + // ----- Refs ------------------------------------------------------- + + private readonly VisualElement container; + private readonly VisualElement terrainLayer; + private readonly VisualElement entityLayer; + private readonly CameraController cameraController; + + // ----- Bake state ------------------------------------------------- + + // World-space rectangle the texture covers (the full map). Cached once at bake. + private Vector2 worldMin; + private Vector2 worldMax; + + // Owned texture — kept so we can destroy it cleanly. Null until first successful bake. + private Texture2D bakedTerrain; + + // True once terrain has been baked successfully. + private bool ready; + + // ----- View state ------------------------------------------------- + + // Current zoom in [MinZoom, MaxZoom]. 1 = full map visible. + private float zoom = 1f; + + // World-XZ point the visible rect is centered on. At zoom 1 this is forced to the + // world center; at higher zoom the user's scrolling determines it (clamped so the + // visible rect never leaves the map). + private Vector2 viewCenter; + + // Visible world rect, derived from zoom + viewCenter in ApplyView(). + private Vector2 visibleWorldMin; + private Vector2 visibleWorldMax; + + // ----- Input state ------------------------------------------------ + + private bool isDragging; + private int dragPointerId; + + // ----- Repaint throttle ------------------------------------------- + + private float lastEntityRepaintTime; + + // ----- Construction ----------------------------------------------- + + public MinimapView(VisualElement container, CameraController cameraController) + { + this.container = container; + this.cameraController = cameraController; + + // Two stacked sub-elements. Terrain is painted via background-image; the entity + // overlay uses generateVisualContent + Painter2D. Both are PickingMode.Ignore so + // pointer events bubble back to the container, where we capture them. + terrainLayer = new VisualElement { name = "minimap-terrain" }; + terrainLayer.AddToClassList("minimap-terrain"); + terrainLayer.pickingMode = PickingMode.Ignore; + container.Add(terrainLayer); + + entityLayer = new VisualElement { name = "minimap-entities" }; + entityLayer.AddToClassList("minimap-entities"); + entityLayer.pickingMode = PickingMode.Ignore; + entityLayer.generateVisualContent += DrawEntities; + container.Add(entityLayer); + + // Pointer events on the container drive click-to-jump, drag-to-pan, right-click-move. + container.RegisterCallback(OnPointerDown); + container.RegisterCallback(OnPointerMove); + container.RegisterCallback(OnPointerUp); + container.RegisterCallback(OnPointerCaptureOut); + container.RegisterCallback(OnWheel); + + // Recompute terrain layout when the container resizes — the absolute pixel + // positions we set on terrainLayer are container-relative. + container.RegisterCallback(_ => { if (ready) ApplyView(); }); + } + + // ----- Lifecycle -------------------------------------------------- + + /// + /// Called from HUDController.Update. Performs lazy bake on the first frame + /// after finishes loading, then triggers throttled entity + /// overlay repaints. + /// + public void Tick() + { + if (!ready) + { + TryBake(); + if (!ready) return; + } + + float now = Time.unscaledTime; + if (now - lastEntityRepaintTime >= 1f / EntityRepaintHz) + { + lastEntityRepaintTime = now; + entityLayer.MarkDirtyRepaint(); + } + } + + /// Releases the baked texture. Call from HUDController.OnDestroy. + public void Dispose() + { + if (bakedTerrain != null) + { + Object.Destroy(bakedTerrain); + bakedTerrain = null; + } + ready = false; + } + + // ----- Bake ------------------------------------------------------- + + private void TryBake() + { + var loader = LevelLoader.Instance; + if (loader == null || !loader.IsLoaded) return; + + bakedTerrain = MinimapTerrainBaker.Bake(loader); + if (bakedTerrain == null) return; + + terrainLayer.style.backgroundImage = + new StyleBackground(Background.FromTexture2D(bakedTerrain)); + + // World extents of the baked rectangle. Tile (n) covers world n - 0.5 to n + 0.5. + var data = loader.LevelData; + float halfTile = GridCoordinates.TILE_SIZE * 0.5f; + float minX = data.GridOriginTile.x * GridCoordinates.TILE_SIZE - halfTile; + float maxX = (data.GridOriginTile.x + data.GridSize.x) * GridCoordinates.TILE_SIZE - halfTile; + float minZ = data.GridOriginTile.y * GridCoordinates.TILE_SIZE - halfTile; + float maxZ = (data.GridOriginTile.y + data.GridSize.y) * GridCoordinates.TILE_SIZE - halfTile; + worldMin = new Vector2(minX, minZ); + worldMax = new Vector2(maxX, maxZ); + + // Default view: centered, fully zoomed out. + zoom = MinZoom; + viewCenter = (worldMin + worldMax) * 0.5f; + ApplyView(); + + ready = true; + } + + // ----- View math -------------------------------------------------- + + /// + /// Recomputes / from the + /// current and , clamps the view to stay + /// within the map, and resizes/positions the terrain sub-element so its rendered + /// region matches the visible world rect. + /// + private void ApplyView() + { + var rect = container.contentRect; + if (rect.width <= 0f || rect.height <= 0f) return; + + // Half-extents of the visible world rect. + float halfWorldX = (worldMax.x - worldMin.x) * 0.5f / zoom; + float halfWorldZ = (worldMax.y - worldMin.y) * 0.5f / zoom; + + // Clamp viewCenter so the visible rect doesn't leave the map. At zoom 1 the + // clamp range collapses to a single point (the world center), forcing the view. + viewCenter.x = Mathf.Clamp(viewCenter.x, + worldMin.x + halfWorldX, worldMax.x - halfWorldX); + viewCenter.y = Mathf.Clamp(viewCenter.y, + worldMin.y + halfWorldZ, worldMax.y - halfWorldZ); + + visibleWorldMin = new Vector2(viewCenter.x - halfWorldX, viewCenter.y - halfWorldZ); + visibleWorldMax = new Vector2(viewCenter.x + halfWorldX, viewCenter.y + halfWorldZ); + + // Position the terrain element so visibleWorldMin maps to container (0, 0) — in + // UI coords where y is top-down and the texture is flipped to put north at top. + // Terrain at zoom z is rect.size * z big, and we offset it so the visible window + // lands inside the container. + float worldRangeX = worldMax.x - worldMin.x; + float worldRangeZ = worldMax.y - worldMin.y; + + float terrainW = rect.width * zoom; + float terrainH = rect.height * zoom; + float left = -(visibleWorldMin.x - worldMin.x) / worldRangeX * terrainW; + // Top: container's top corresponds to visibleWorldMax.z. Distance from texture's + // top (worldMax.z) to visibleWorldMax.z, as a fraction of world range, times height. + float top = -(worldMax.y - visibleWorldMax.y) / worldRangeZ * terrainH; + + terrainLayer.style.width = terrainW; + terrainLayer.style.height = terrainH; + terrainLayer.style.left = left; + terrainLayer.style.top = top; + terrainLayer.style.right = StyleKeyword.Auto; + terrainLayer.style.bottom = StyleKeyword.Auto; + + // Entity overlay needs a redraw to reflect the new visible rect. + entityLayer.MarkDirtyRepaint(); + } + + // ----- Pointer handling ------------------------------------------- + + private void OnPointerDown(PointerDownEvent evt) + { + if (!ready) return; + + if (evt.button == 0) + { + // Left button: jump camera + start drag-to-pan. + if (cameraController == null) return; + isDragging = true; + dragPointerId = evt.pointerId; + container.CapturePointer(evt.pointerId); + cameraController.BeginDrag(); + cameraController.JumpTo(UIToWorld(evt.localPosition)); + evt.StopPropagation(); + } + else if (evt.button == 1) + { + // Right button: move-and-pause command on the selected builder. Fires + // immediately; no drag/capture semantics (single-shot like an RTS). + HandleRightClickMove(evt.localPosition); + evt.StopPropagation(); + } + } + + private void OnPointerMove(PointerMoveEvent evt) + { + if (!isDragging || evt.pointerId != dragPointerId) return; + cameraController.JumpTo(UIToWorld(evt.localPosition)); + evt.StopPropagation(); + } + + private void OnPointerUp(PointerUpEvent evt) + { + if (!isDragging || evt.pointerId != dragPointerId) return; + EndDragging(evt.pointerId); + evt.StopPropagation(); + } + + // Lost capture — end drag cleanly so we don't leave CameraController stuck. + private void OnPointerCaptureOut(PointerCaptureOutEvent evt) + { + if (!isDragging || evt.pointerId != dragPointerId) return; + EndDragging(evt.pointerId); + } + + private void EndDragging(int pointerId) + { + isDragging = false; + cameraController?.EndDrag(); + if (container.HasPointerCapture(pointerId)) + container.ReleasePointer(pointerId); + } + + private void HandleRightClickMove(Vector2 uiLocal) + { + var selection = SelectionState.Instance; + if (selection == null || !selection.HasSelection) return; + + Vector3 worldTarget = UIToWorld(uiLocal); + // Same RPC the world right-click uses; server validates and side-effects the queue. + selection.SelectedBuilder.RequestMoveAndPauseRpc(worldTarget); + } + + // ----- Zoom ------------------------------------------------------- + + private void OnWheel(WheelEvent evt) + { + if (!ready) return; + + // Cursor-anchored zoom: capture the world point under the cursor before the + // zoom change, then move viewCenter so that same world point lands at the same + // UI position afterward. Result: the cursor "drills in" on whatever it's over. + Vector3 cursorWorldBefore = UIToWorld(evt.localMousePosition); + + // WheelEvent.delta.y: positive = scroll down (typically zoom out), negative = up. + // Multiplicative steps feel natural and keep the rate consistent across zoom levels. + float steps = -evt.delta.y; + float newZoom = Mathf.Clamp(zoom * Mathf.Pow(ZoomStepFactor, steps), MinZoom, MaxZoom); + if (Mathf.Approximately(newZoom, zoom)) + { + evt.StopPropagation(); + return; + } + + zoom = newZoom; + + // Compute viewCenter that keeps cursorWorldBefore under the cursor at the new zoom. + // visibleWorldMin = viewCenter - halfWorld; visibleWorldMax = viewCenter + halfWorld + // UIToWorld(uiLocal) = visibleWorldMin + frac * (visibleWorldMax - visibleWorldMin) + // = viewCenter - halfWorld + frac * 2 * halfWorld + // = viewCenter + (2*frac - 1) * halfWorld + // Solve viewCenter from desired cursorWorldBefore at the cursor's frac. + var rect = container.contentRect; + float fx = Mathf.Clamp01(evt.localMousePosition.x / rect.width); + float fyTopDown = Mathf.Clamp01(evt.localMousePosition.y / rect.height); + float fzBottomUp = 1f - fyTopDown; + + float halfWorldX = (worldMax.x - worldMin.x) * 0.5f / zoom; + float halfWorldZ = (worldMax.y - worldMin.y) * 0.5f / zoom; + viewCenter.x = cursorWorldBefore.x - (2f * fx - 1f) * halfWorldX; + viewCenter.y = cursorWorldBefore.z - (2f * fzBottomUp - 1f) * halfWorldZ; + + ApplyView(); // clamps viewCenter and updates terrain element + evt.StopPropagation(); + } + + // ----- Coordinate transforms -------------------------------------- + + // UI local (y down) → world (z up). Clamps to visible map rect so dragging past the + // edge doesn't fly the camera off the map. + private Vector3 UIToWorld(Vector2 uiLocal) + { + var rect = container.contentRect; + if (rect.width <= 0f || rect.height <= 0f) return Vector3.zero; + + float fx = Mathf.Clamp01(uiLocal.x / rect.width); + float fyTopDown = Mathf.Clamp01(uiLocal.y / rect.height); + float fzBottomUp = 1f - fyTopDown; + + float worldX = Mathf.Lerp(visibleWorldMin.x, visibleWorldMax.x, fx); + float worldZ = Mathf.Lerp(visibleWorldMin.y, visibleWorldMax.y, fzBottomUp); + return new Vector3(worldX, GridCoordinates.BUILDABLE_PLANE_Y, worldZ); + } + + // World → UI local. Used for placing entity icons. + // + // IMPORTANT: this must NOT clamp. Mathf.InverseLerp clamps to [0,1], which would + // pin off-screen entities to the minimap edge — making zoomed-in views show + // ghost icons stuck against every border. We compute the fraction manually so + // out-of-view entities get UI coords outside the container's rect, where the + // bounds check in DrawOneEntity culls them. + private Vector2 WorldToUI(Vector3 world) + { + var rect = container.contentRect; + float rangeX = visibleWorldMax.x - visibleWorldMin.x; + float rangeZ = visibleWorldMax.y - visibleWorldMin.y; + if (rangeX <= 0.0001f || rangeZ <= 0.0001f) return Vector2.zero; + + float fx = (world.x - visibleWorldMin.x) / rangeX; + float fzBottomUp = (world.z - visibleWorldMin.y) / rangeZ; + float fyTopDown = 1f - fzBottomUp; + return new Vector2(fx * rect.width, fyTopDown * rect.height); + } + + /// Pixels per world unit at the current zoom. Used to scale entity icons so + /// e.g. a 2×2 tower footprint reads as a 2-tile square on the minimap. + private float PixelsPerWorldUnit + { + get + { + var rect = container.contentRect; + float visibleWorldWidth = visibleWorldMax.x - visibleWorldMin.x; + return visibleWorldWidth > 0.0001f ? rect.width / visibleWorldWidth : 1f; + } + } + + // ----- Entity overlay drawing ------------------------------------- + + private void DrawEntities(MeshGenerationContext mgc) + { + if (!ready) return; + var painter = mgc.painter2D; + float pxPerWorld = PixelsPerWorldUnit; + + // The local player's builder always draws on top. Cache the reference once + // (Builder.Local does a dictionary lookup) and cast to the interface so the + // ReferenceEquals compare against registry entries works without boxing. + IMinimapEntity localBuilder = Builder.Local; + + // Pass 1: every entity except the local builder. Order within this pass is + // arbitrary (registry is a HashSet); revisit if cross-entity layering between + // non-local entities ever matters. + MinimapEntityRegistry.ForEach(e => + { + if (ReferenceEquals(e, localBuilder)) return; + DrawOneEntity(painter, e, pxPerWorld); + }); + + // Pass 2: local builder on top. Skipped if the local builder isn't spawned + // (e.g., on a dedicated server or before the local client's builder arrives). + if (localBuilder != null) + DrawOneEntity(painter, localBuilder, pxPerWorld); + } + + private void DrawOneEntity(Painter2D p, IMinimapEntity entity, float pxPerWorld) + { + if (entity == null) return; + + Vector2 ui = WorldToUI(entity.WorldPosition); + + // Skip if obviously off-screen — overflow:hidden clips it anyway, but skipping + // saves Painter2D work for entities far outside the zoomed-in window. + var rect = container.contentRect; + if (ui.x < -8f || ui.y < -8f || ui.x > rect.width + 8f || ui.y > rect.height + 8f) return; + + Color col = entity.MinimapColor; col.a = 1f; + p.fillColor = col; + + switch (entity.IconKind) + { + case MinimapIconKind.Enemy: + { + float r = Mathf.Max(entity.MinimapWorldSize * 0.5f * pxPerWorld, EnemyMinRadiusPx); + DrawCircle(p, ui, r); + break; + } + case MinimapIconKind.Tower: + { + // Tower size follows footprint exactly — no pixel floor. Adjacent towers + // visually touch on the minimap because their drawn squares span their + // full footprint extent in world units. + float halfPx = entity.MinimapWorldSize * 0.5f * pxPerWorld; + DrawSquare(p, ui, halfPx); + break; + } + case MinimapIconKind.Builder: + { + float r = Mathf.Max(entity.MinimapWorldSize * 0.5f * pxPerWorld, BuilderMinRadiusPx); + DrawCircle(p, ui, r); + p.strokeColor = BuilderOutline; + p.lineWidth = 1f; + p.Stroke(); + break; + } + } + } + + private static void DrawCircle(Painter2D p, Vector2 center, float radius) + { + p.BeginPath(); + p.Arc(center, radius, new Angle(0f, AngleUnit.Degree), new Angle(360f, AngleUnit.Degree)); + p.Fill(); + } + + private static void DrawSquare(Painter2D p, Vector2 center, float halfSize) + { + p.BeginPath(); + p.MoveTo(new Vector2(center.x - halfSize, center.y - halfSize)); + p.LineTo(new Vector2(center.x + halfSize, center.y - halfSize)); + p.LineTo(new Vector2(center.x + halfSize, center.y + halfSize)); + p.LineTo(new Vector2(center.x - halfSize, center.y + halfSize)); + p.ClosePath(); + p.Fill(); + } + } +} diff --git a/Assets/_Project/Scripts/UI/Minimap/MinimapView.cs.meta b/Assets/_Project/Scripts/UI/Minimap/MinimapView.cs.meta new file mode 100644 index 0000000..5c0228d --- /dev/null +++ b/Assets/_Project/Scripts/UI/Minimap/MinimapView.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 0462e237a5a906242a62de1740fc4365 \ No newline at end of file diff --git a/Assets/_Project/UI/HUD.uss b/Assets/_Project/UI/HUD.uss index 52d31c8..e9520f8 100644 --- a/Assets/_Project/UI/HUD.uss +++ b/Assets/_Project/UI/HUD.uss @@ -165,13 +165,28 @@ border-top-color: rgba(90, 74, 16, 1); } -/* Minimap — RenderTexture applied as background-image at runtime */ +/* Minimap — host container. MinimapView injects two stacked sub-elements + (.minimap-terrain and .minimap-entities) at runtime. */ .minimap { width: 110px; flex-shrink: 0; - background-color: rgb(10, 26, 10); + background-color: rgb(8, 10, 12); border-right-width: 1px; border-right-color: rgba(42, 58, 26, 1); + overflow: hidden; +} + +/* Static baked terrain. Background-image assigned by MinimapView. */ +.minimap-terrain { + position: absolute; + top: 0; left: 0; right: 0; bottom: 0; + -unity-background-scale-mode: stretch-to-fill; +} + +/* Dynamic entity overlay. Drawn via Painter2D in generateVisualContent. */ +.minimap-entities { + position: absolute; + top: 0; left: 0; right: 0; bottom: 0; } /* Portrait */