diff --git a/Assets/Scenes/SampleScene.unity b/Assets/Scenes/SampleScene.unity
index d8251947b..d03b1eb1f 100644
--- a/Assets/Scenes/SampleScene.unity
+++ b/Assets/Scenes/SampleScene.unity
@@ -910,6 +910,120 @@ Transform:
m_CorrespondingSourceObject: {fileID: 4134353195883994, guid: 28f2883c42945194b9a7df1e5c8544c5, type: 3}
m_PrefabInstance: {fileID: 320265325}
m_PrefabAsset: {fileID: 0}
+--- !u!1 &330081557
+GameObject:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ serializedVersion: 6
+ m_Component:
+ - component: {fileID: 330081561}
+ - component: {fileID: 330081560}
+ - component: {fileID: 330081559}
+ - component: {fileID: 330081558}
+ m_Layer: 0
+ m_Name: ExpedPillar1
+ m_TagString: Untagged
+ m_Icon: {fileID: 0}
+ m_NavMeshLayer: 0
+ m_StaticEditorFlags: 0
+ m_IsActive: 1
+--- !u!23 &330081558
+MeshRenderer:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 330081557}
+ m_Enabled: 1
+ m_CastShadows: 1
+ m_ReceiveShadows: 1
+ m_DynamicOccludee: 1
+ m_StaticShadowCaster: 0
+ m_MotionVectors: 1
+ m_LightProbeUsage: 1
+ m_ReflectionProbeUsage: 1
+ m_RayTracingMode: 2
+ m_RayTraceProcedural: 0
+ m_RayTracingAccelStructBuildFlagsOverride: 0
+ m_RayTracingAccelStructBuildFlags: 1
+ m_SmallMeshCulling: 1
+ m_ForceMeshLod: -1
+ m_MeshLodSelectionBias: 0
+ m_RenderingLayerMask: 1
+ m_RendererPriority: 0
+ m_Materials:
+ - {fileID: 2100000, guid: 69964ae457ecfd245af12cd95fa5eaed, type: 2}
+ m_StaticBatchInfo:
+ firstSubMesh: 0
+ subMeshCount: 0
+ m_StaticBatchRoot: {fileID: 0}
+ m_ProbeAnchor: {fileID: 0}
+ m_LightProbeVolumeOverride: {fileID: 0}
+ m_ScaleInLightmap: 1
+ m_ReceiveGI: 1
+ m_PreserveUVs: 1
+ m_IgnoreNormalsForChartDetection: 0
+ m_ImportantGI: 0
+ m_StitchLightmapSeams: 1
+ m_SelectedEditorRenderState: 3
+ m_MinimumChartSize: 4
+ m_AutoUVMaxDistance: 0.5
+ m_AutoUVMaxAngle: 89
+ m_LightmapParameters: {fileID: 0}
+ m_GlobalIlluminationMeshLod: 0
+ m_SortingLayerID: 0
+ m_SortingLayer: 0
+ m_SortingOrder: 0
+ m_MaskInteraction: 0
+ m_AdditionalVertexStreams: {fileID: 0}
+--- !u!136 &330081559
+CapsuleCollider:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 330081557}
+ m_Material: {fileID: 0}
+ m_IncludeLayers:
+ serializedVersion: 2
+ m_Bits: 0
+ m_ExcludeLayers:
+ serializedVersion: 2
+ m_Bits: 0
+ m_LayerOverridePriority: 0
+ m_IsTrigger: 0
+ m_ProvidesContacts: 0
+ m_Enabled: 1
+ serializedVersion: 2
+ m_Radius: 0.5
+ m_Height: 2
+ m_Direction: 1
+ m_Center: {x: 0, y: 0, z: 0}
+--- !u!33 &330081560
+MeshFilter:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 330081557}
+ m_Mesh: {fileID: 10206, guid: 0000000000000000e000000000000000, type: 0}
+--- !u!4 &330081561
+Transform:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 330081557}
+ serializedVersion: 2
+ m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
+ m_LocalPosition: {x: 1026, y: 5, z: 6}
+ m_LocalScale: {x: 2.5, y: 6, z: 2.5}
+ m_ConstrainProportionsScale: 0
+ m_Children: []
+ m_Father: {fileID: 0}
+ m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!1 &330585543
GameObject:
m_ObjectHideFlags: 0
@@ -1069,11 +1183,126 @@ MonoBehaviour:
OrthoSize: 10
FollowSharpness: 8
FallbackTarget: {x: 3, y: 0, z: 4}
+ AimLeadDistance: 2.5
--- !u!4 &331593291 stripped
Transform:
m_CorrespondingSourceObject: {fileID: 4855556370681302, guid: 65f76c5f923964045833f09a3f767a16, type: 3}
m_PrefabInstance: {fileID: 232019258}
m_PrefabAsset: {fileID: 0}
+--- !u!1 &384552424
+GameObject:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ serializedVersion: 6
+ m_Component:
+ - component: {fileID: 384552428}
+ - component: {fileID: 384552427}
+ - component: {fileID: 384552426}
+ - component: {fileID: 384552425}
+ m_Layer: 0
+ m_Name: ExpedPillar4
+ m_TagString: Untagged
+ m_Icon: {fileID: 0}
+ m_NavMeshLayer: 0
+ m_StaticEditorFlags: 0
+ m_IsActive: 1
+--- !u!23 &384552425
+MeshRenderer:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 384552424}
+ m_Enabled: 1
+ m_CastShadows: 1
+ m_ReceiveShadows: 1
+ m_DynamicOccludee: 1
+ m_StaticShadowCaster: 0
+ m_MotionVectors: 1
+ m_LightProbeUsage: 1
+ m_ReflectionProbeUsage: 1
+ m_RayTracingMode: 2
+ m_RayTraceProcedural: 0
+ m_RayTracingAccelStructBuildFlagsOverride: 0
+ m_RayTracingAccelStructBuildFlags: 1
+ m_SmallMeshCulling: 1
+ m_ForceMeshLod: -1
+ m_MeshLodSelectionBias: 0
+ m_RenderingLayerMask: 1
+ m_RendererPriority: 0
+ m_Materials:
+ - {fileID: 2100000, guid: 69964ae457ecfd245af12cd95fa5eaed, type: 2}
+ m_StaticBatchInfo:
+ firstSubMesh: 0
+ subMeshCount: 0
+ m_StaticBatchRoot: {fileID: 0}
+ m_ProbeAnchor: {fileID: 0}
+ m_LightProbeVolumeOverride: {fileID: 0}
+ m_ScaleInLightmap: 1
+ m_ReceiveGI: 1
+ m_PreserveUVs: 1
+ m_IgnoreNormalsForChartDetection: 0
+ m_ImportantGI: 0
+ m_StitchLightmapSeams: 1
+ m_SelectedEditorRenderState: 3
+ m_MinimumChartSize: 4
+ m_AutoUVMaxDistance: 0.5
+ m_AutoUVMaxAngle: 89
+ m_LightmapParameters: {fileID: 0}
+ m_GlobalIlluminationMeshLod: 0
+ m_SortingLayerID: 0
+ m_SortingLayer: 0
+ m_SortingOrder: 0
+ m_MaskInteraction: 0
+ m_AdditionalVertexStreams: {fileID: 0}
+--- !u!136 &384552426
+CapsuleCollider:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 384552424}
+ m_Material: {fileID: 0}
+ m_IncludeLayers:
+ serializedVersion: 2
+ m_Bits: 0
+ m_ExcludeLayers:
+ serializedVersion: 2
+ m_Bits: 0
+ m_LayerOverridePriority: 0
+ m_IsTrigger: 0
+ m_ProvidesContacts: 0
+ m_Enabled: 1
+ serializedVersion: 2
+ m_Radius: 0.5
+ m_Height: 2
+ m_Direction: 1
+ m_Center: {x: 0, y: 0, z: 0}
+--- !u!33 &384552427
+MeshFilter:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 384552424}
+ m_Mesh: {fileID: 10206, guid: 0000000000000000e000000000000000, type: 0}
+--- !u!4 &384552428
+Transform:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 384552424}
+ serializedVersion: 2
+ m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
+ m_LocalPosition: {x: 994, y: 4, z: -26}
+ m_LocalScale: {x: 2, y: 5, z: 2}
+ m_ConstrainProportionsScale: 0
+ m_Children: []
+ m_Father: {fileID: 0}
+ m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!1001 &390565387
PrefabInstance:
m_ObjectHideFlags: 0
@@ -1553,6 +1782,120 @@ PrefabInstance:
m_AddedGameObjects: []
m_AddedComponents: []
m_SourcePrefab: {fileID: 100100000, guid: 0af7253adfe9a6c4ca7be0a1573f3c77, type: 3}
+--- !u!1 &476566464
+GameObject:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ serializedVersion: 6
+ m_Component:
+ - component: {fileID: 476566468}
+ - component: {fileID: 476566467}
+ - component: {fileID: 476566466}
+ - component: {fileID: 476566465}
+ m_Layer: 0
+ m_Name: ExpedPillar2
+ m_TagString: Untagged
+ m_Icon: {fileID: 0}
+ m_NavMeshLayer: 0
+ m_StaticEditorFlags: 0
+ m_IsActive: 1
+--- !u!23 &476566465
+MeshRenderer:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 476566464}
+ m_Enabled: 1
+ m_CastShadows: 1
+ m_ReceiveShadows: 1
+ m_DynamicOccludee: 1
+ m_StaticShadowCaster: 0
+ m_MotionVectors: 1
+ m_LightProbeUsage: 1
+ m_ReflectionProbeUsage: 1
+ m_RayTracingMode: 2
+ m_RayTraceProcedural: 0
+ m_RayTracingAccelStructBuildFlagsOverride: 0
+ m_RayTracingAccelStructBuildFlags: 1
+ m_SmallMeshCulling: 1
+ m_ForceMeshLod: -1
+ m_MeshLodSelectionBias: 0
+ m_RenderingLayerMask: 1
+ m_RendererPriority: 0
+ m_Materials:
+ - {fileID: 2100000, guid: 69964ae457ecfd245af12cd95fa5eaed, type: 2}
+ m_StaticBatchInfo:
+ firstSubMesh: 0
+ subMeshCount: 0
+ m_StaticBatchRoot: {fileID: 0}
+ m_ProbeAnchor: {fileID: 0}
+ m_LightProbeVolumeOverride: {fileID: 0}
+ m_ScaleInLightmap: 1
+ m_ReceiveGI: 1
+ m_PreserveUVs: 1
+ m_IgnoreNormalsForChartDetection: 0
+ m_ImportantGI: 0
+ m_StitchLightmapSeams: 1
+ m_SelectedEditorRenderState: 3
+ m_MinimumChartSize: 4
+ m_AutoUVMaxDistance: 0.5
+ m_AutoUVMaxAngle: 89
+ m_LightmapParameters: {fileID: 0}
+ m_GlobalIlluminationMeshLod: 0
+ m_SortingLayerID: 0
+ m_SortingLayer: 0
+ m_SortingOrder: 0
+ m_MaskInteraction: 0
+ m_AdditionalVertexStreams: {fileID: 0}
+--- !u!136 &476566466
+CapsuleCollider:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 476566464}
+ m_Material: {fileID: 0}
+ m_IncludeLayers:
+ serializedVersion: 2
+ m_Bits: 0
+ m_ExcludeLayers:
+ serializedVersion: 2
+ m_Bits: 0
+ m_LayerOverridePriority: 0
+ m_IsTrigger: 0
+ m_ProvidesContacts: 0
+ m_Enabled: 1
+ serializedVersion: 2
+ m_Radius: 0.5
+ m_Height: 2
+ m_Direction: 1
+ m_Center: {x: 0, y: 0, z: 0}
+--- !u!33 &476566467
+MeshFilter:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 476566464}
+ m_Mesh: {fileID: 10206, guid: 0000000000000000e000000000000000, type: 0}
+--- !u!4 &476566468
+Transform:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 476566464}
+ serializedVersion: 2
+ m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
+ m_LocalPosition: {x: 974, y: 5, z: -6}
+ m_LocalScale: {x: 2.5, y: 6, z: 2.5}
+ m_ConstrainProportionsScale: 0
+ m_Children: []
+ m_Father: {fileID: 0}
+ m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!1001 &515680011
PrefabInstance:
m_ObjectHideFlags: 0
@@ -3448,6 +3791,119 @@ PrefabInstance:
m_AddedGameObjects: []
m_AddedComponents: []
m_SourcePrefab: {fileID: 100100000, guid: 0af7253adfe9a6c4ca7be0a1573f3c77, type: 3}
+--- !u!1 &1541455365
+GameObject:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ serializedVersion: 6
+ m_Component:
+ - component: {fileID: 1541455369}
+ - component: {fileID: 1541455368}
+ - component: {fileID: 1541455367}
+ - component: {fileID: 1541455366}
+ m_Layer: 0
+ m_Name: ExpeditionGround
+ m_TagString: Untagged
+ m_Icon: {fileID: 0}
+ m_NavMeshLayer: 0
+ m_StaticEditorFlags: 0
+ m_IsActive: 1
+--- !u!23 &1541455366
+MeshRenderer:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 1541455365}
+ m_Enabled: 1
+ m_CastShadows: 1
+ m_ReceiveShadows: 1
+ m_DynamicOccludee: 1
+ m_StaticShadowCaster: 0
+ m_MotionVectors: 1
+ m_LightProbeUsage: 1
+ m_ReflectionProbeUsage: 1
+ m_RayTracingMode: 2
+ m_RayTraceProcedural: 0
+ m_RayTracingAccelStructBuildFlagsOverride: 0
+ m_RayTracingAccelStructBuildFlags: 1
+ m_SmallMeshCulling: 1
+ m_ForceMeshLod: -1
+ m_MeshLodSelectionBias: 0
+ m_RenderingLayerMask: 1
+ m_RendererPriority: 0
+ m_Materials:
+ - {fileID: 2100000, guid: 6afcc12e55743ac45b265795f24238dd, type: 2}
+ m_StaticBatchInfo:
+ firstSubMesh: 0
+ subMeshCount: 0
+ m_StaticBatchRoot: {fileID: 0}
+ m_ProbeAnchor: {fileID: 0}
+ m_LightProbeVolumeOverride: {fileID: 0}
+ m_ScaleInLightmap: 1
+ m_ReceiveGI: 1
+ m_PreserveUVs: 1
+ m_IgnoreNormalsForChartDetection: 0
+ m_ImportantGI: 0
+ m_StitchLightmapSeams: 1
+ m_SelectedEditorRenderState: 3
+ m_MinimumChartSize: 4
+ m_AutoUVMaxDistance: 0.5
+ m_AutoUVMaxAngle: 89
+ m_LightmapParameters: {fileID: 0}
+ m_GlobalIlluminationMeshLod: 0
+ m_SortingLayerID: 0
+ m_SortingLayer: 0
+ m_SortingOrder: 0
+ m_MaskInteraction: 0
+ m_AdditionalVertexStreams: {fileID: 0}
+--- !u!64 &1541455367
+MeshCollider:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 1541455365}
+ m_Material: {fileID: 0}
+ m_IncludeLayers:
+ serializedVersion: 2
+ m_Bits: 0
+ m_ExcludeLayers:
+ serializedVersion: 2
+ m_Bits: 0
+ m_LayerOverridePriority: 0
+ m_IsTrigger: 0
+ m_ProvidesContacts: 0
+ m_Enabled: 1
+ serializedVersion: 5
+ m_Convex: 0
+ m_CookingOptions: 30
+ m_Mesh: {fileID: 10209, guid: 0000000000000000e000000000000000, type: 0}
+--- !u!33 &1541455368
+MeshFilter:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 1541455365}
+ m_Mesh: {fileID: 10209, guid: 0000000000000000e000000000000000, type: 0}
+--- !u!4 &1541455369
+Transform:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 1541455365}
+ serializedVersion: 2
+ m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
+ m_LocalPosition: {x: 1000, y: 0, z: 0}
+ m_LocalScale: {x: 8, y: 1, z: 8}
+ m_ConstrainProportionsScale: 0
+ m_Children: []
+ m_Father: {fileID: 0}
+ m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!1001 &1577269770
PrefabInstance:
m_ObjectHideFlags: 0
@@ -4283,6 +4739,120 @@ PrefabInstance:
m_AddedGameObjects: []
m_AddedComponents: []
m_SourcePrefab: {fileID: 100100000, guid: c037f4cd64354fd428bbf6b6007c235d, type: 3}
+--- !u!1 &2009335283
+GameObject:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ serializedVersion: 6
+ m_Component:
+ - component: {fileID: 2009335287}
+ - component: {fileID: 2009335286}
+ - component: {fileID: 2009335285}
+ - component: {fileID: 2009335284}
+ m_Layer: 0
+ m_Name: ExpedPillar3
+ m_TagString: Untagged
+ m_Icon: {fileID: 0}
+ m_NavMeshLayer: 0
+ m_StaticEditorFlags: 0
+ m_IsActive: 1
+--- !u!23 &2009335284
+MeshRenderer:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 2009335283}
+ m_Enabled: 1
+ m_CastShadows: 1
+ m_ReceiveShadows: 1
+ m_DynamicOccludee: 1
+ m_StaticShadowCaster: 0
+ m_MotionVectors: 1
+ m_LightProbeUsage: 1
+ m_ReflectionProbeUsage: 1
+ m_RayTracingMode: 2
+ m_RayTraceProcedural: 0
+ m_RayTracingAccelStructBuildFlagsOverride: 0
+ m_RayTracingAccelStructBuildFlags: 1
+ m_SmallMeshCulling: 1
+ m_ForceMeshLod: -1
+ m_MeshLodSelectionBias: 0
+ m_RenderingLayerMask: 1
+ m_RendererPriority: 0
+ m_Materials:
+ - {fileID: 2100000, guid: 69964ae457ecfd245af12cd95fa5eaed, type: 2}
+ m_StaticBatchInfo:
+ firstSubMesh: 0
+ subMeshCount: 0
+ m_StaticBatchRoot: {fileID: 0}
+ m_ProbeAnchor: {fileID: 0}
+ m_LightProbeVolumeOverride: {fileID: 0}
+ m_ScaleInLightmap: 1
+ m_ReceiveGI: 1
+ m_PreserveUVs: 1
+ m_IgnoreNormalsForChartDetection: 0
+ m_ImportantGI: 0
+ m_StitchLightmapSeams: 1
+ m_SelectedEditorRenderState: 3
+ m_MinimumChartSize: 4
+ m_AutoUVMaxDistance: 0.5
+ m_AutoUVMaxAngle: 89
+ m_LightmapParameters: {fileID: 0}
+ m_GlobalIlluminationMeshLod: 0
+ m_SortingLayerID: 0
+ m_SortingLayer: 0
+ m_SortingOrder: 0
+ m_MaskInteraction: 0
+ m_AdditionalVertexStreams: {fileID: 0}
+--- !u!136 &2009335285
+CapsuleCollider:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 2009335283}
+ m_Material: {fileID: 0}
+ m_IncludeLayers:
+ serializedVersion: 2
+ m_Bits: 0
+ m_ExcludeLayers:
+ serializedVersion: 2
+ m_Bits: 0
+ m_LayerOverridePriority: 0
+ m_IsTrigger: 0
+ m_ProvidesContacts: 0
+ m_Enabled: 1
+ serializedVersion: 2
+ m_Radius: 0.5
+ m_Height: 2
+ m_Direction: 1
+ m_Center: {x: 0, y: 0, z: 0}
+--- !u!33 &2009335286
+MeshFilter:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 2009335283}
+ m_Mesh: {fileID: 10206, guid: 0000000000000000e000000000000000, type: 0}
+--- !u!4 &2009335287
+Transform:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 2009335283}
+ serializedVersion: 2
+ m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
+ m_LocalPosition: {x: 1006, y: 4, z: 26}
+ m_LocalScale: {x: 2, y: 5, z: 2}
+ m_ConstrainProportionsScale: 0
+ m_Children: []
+ m_Father: {fileID: 0}
+ m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!1001 &2017077544
PrefabInstance:
m_ObjectHideFlags: 0
@@ -4619,3 +5189,8 @@ SceneRoots:
- {fileID: 228729546}
- {fileID: 1736670160}
- {fileID: 1116745582}
+ - {fileID: 1541455369}
+ - {fileID: 330081561}
+ - {fileID: 476566468}
+ - {fileID: 2009335287}
+ - {fileID: 384552428}
diff --git a/Assets/Screenshots/m6_base_gate.png b/Assets/Screenshots/m6_base_gate.png
new file mode 100644
index 000000000..9d044ec7d
--- /dev/null
+++ b/Assets/Screenshots/m6_base_gate.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:4fc3d79f50c4ec2c7fb866946ea29d9278b2db770fb751254d90ecedee5c204e
+size 135317
diff --git a/Assets/Screenshots/m6_base_gate.png.meta b/Assets/Screenshots/m6_base_gate.png.meta
new file mode 100644
index 000000000..f5a887c3d
--- /dev/null
+++ b/Assets/Screenshots/m6_base_gate.png.meta
@@ -0,0 +1,117 @@
+fileFormatVersion: 2
+guid: ddd381e1325c5ab488ff60c7e7371a70
+TextureImporter:
+ internalIDToNameTable: []
+ externalObjects: {}
+ serializedVersion: 13
+ mipmaps:
+ mipMapMode: 0
+ enableMipMap: 1
+ sRGBTexture: 1
+ linearTexture: 0
+ fadeOut: 0
+ borderMipMap: 0
+ mipMapsPreserveCoverage: 0
+ alphaTestReferenceValue: 0.5
+ mipMapFadeDistanceStart: 1
+ mipMapFadeDistanceEnd: 3
+ bumpmap:
+ convertToNormalMap: 0
+ externalNormalMap: 0
+ heightScale: 0.25
+ normalMapFilter: 0
+ flipGreenChannel: 0
+ isReadable: 0
+ streamingMipmaps: 0
+ streamingMipmapsPriority: 0
+ vTOnly: 0
+ ignoreMipmapLimit: 0
+ grayScaleToAlpha: 0
+ generateCubemap: 6
+ cubemapConvolution: 0
+ seamlessCubemap: 0
+ textureFormat: 1
+ maxTextureSize: 2048
+ textureSettings:
+ serializedVersion: 2
+ filterMode: 1
+ aniso: 1
+ mipBias: 0
+ wrapU: 0
+ wrapV: 0
+ wrapW: 0
+ nPOTScale: 1
+ lightmap: 0
+ compressionQuality: 50
+ spriteMode: 0
+ spriteExtrude: 1
+ spriteMeshType: 1
+ alignment: 0
+ spritePivot: {x: 0.5, y: 0.5}
+ spritePixelsToUnits: 100
+ spriteBorder: {x: 0, y: 0, z: 0, w: 0}
+ spriteGenerateFallbackPhysicsShape: 1
+ alphaUsage: 1
+ alphaIsTransparency: 0
+ spriteTessellationDetail: -1
+ textureType: 0
+ textureShape: 1
+ singleChannelComponent: 0
+ flipbookRows: 1
+ flipbookColumns: 1
+ maxTextureSizeSet: 0
+ compressionQualitySet: 0
+ textureFormatSet: 0
+ ignorePngGamma: 0
+ applyGammaDecoding: 0
+ swizzle: 50462976
+ cookieLightType: 0
+ platformSettings:
+ - serializedVersion: 4
+ buildTarget: DefaultTexturePlatform
+ maxTextureSize: 2048
+ resizeAlgorithm: 0
+ textureFormat: -1
+ textureCompression: 1
+ compressionQuality: 50
+ crunchedCompression: 0
+ allowsAlphaSplitting: 0
+ overridden: 0
+ ignorePlatformSupport: 0
+ androidETC2FallbackOverride: 0
+ forceMaximumCompressionQuality_BC6H_BC7: 0
+ - serializedVersion: 4
+ buildTarget: Standalone
+ maxTextureSize: 2048
+ resizeAlgorithm: 0
+ textureFormat: -1
+ textureCompression: 1
+ compressionQuality: 50
+ crunchedCompression: 0
+ allowsAlphaSplitting: 0
+ overridden: 0
+ ignorePlatformSupport: 0
+ androidETC2FallbackOverride: 0
+ forceMaximumCompressionQuality_BC6H_BC7: 0
+ spriteSheet:
+ serializedVersion: 2
+ sprites: []
+ outline: []
+ customData:
+ physicsShape: []
+ bones: []
+ spriteID:
+ internalID: 0
+ vertices: []
+ indices:
+ edges: []
+ weights: []
+ secondaryTextures: []
+ spriteCustomMetadata:
+ entries: []
+ nameFileIdTable: {}
+ mipmapLimitGroupName:
+ pSDRemoveMatte: 0
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/Screenshots/m6_expedition.png b/Assets/Screenshots/m6_expedition.png
new file mode 100644
index 000000000..5aa0ae151
--- /dev/null
+++ b/Assets/Screenshots/m6_expedition.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:95d91df128c4db2226fb6ebbe54b8b468d94e7d085dd73b053cfece42ce10e57
+size 103662
diff --git a/Assets/Screenshots/m6_expedition.png.meta b/Assets/Screenshots/m6_expedition.png.meta
new file mode 100644
index 000000000..f2d517203
--- /dev/null
+++ b/Assets/Screenshots/m6_expedition.png.meta
@@ -0,0 +1,117 @@
+fileFormatVersion: 2
+guid: 7b4a5ded5202520459e9961d37e871c8
+TextureImporter:
+ internalIDToNameTable: []
+ externalObjects: {}
+ serializedVersion: 13
+ mipmaps:
+ mipMapMode: 0
+ enableMipMap: 1
+ sRGBTexture: 1
+ linearTexture: 0
+ fadeOut: 0
+ borderMipMap: 0
+ mipMapsPreserveCoverage: 0
+ alphaTestReferenceValue: 0.5
+ mipMapFadeDistanceStart: 1
+ mipMapFadeDistanceEnd: 3
+ bumpmap:
+ convertToNormalMap: 0
+ externalNormalMap: 0
+ heightScale: 0.25
+ normalMapFilter: 0
+ flipGreenChannel: 0
+ isReadable: 0
+ streamingMipmaps: 0
+ streamingMipmapsPriority: 0
+ vTOnly: 0
+ ignoreMipmapLimit: 0
+ grayScaleToAlpha: 0
+ generateCubemap: 6
+ cubemapConvolution: 0
+ seamlessCubemap: 0
+ textureFormat: 1
+ maxTextureSize: 2048
+ textureSettings:
+ serializedVersion: 2
+ filterMode: 1
+ aniso: 1
+ mipBias: 0
+ wrapU: 0
+ wrapV: 0
+ wrapW: 0
+ nPOTScale: 1
+ lightmap: 0
+ compressionQuality: 50
+ spriteMode: 0
+ spriteExtrude: 1
+ spriteMeshType: 1
+ alignment: 0
+ spritePivot: {x: 0.5, y: 0.5}
+ spritePixelsToUnits: 100
+ spriteBorder: {x: 0, y: 0, z: 0, w: 0}
+ spriteGenerateFallbackPhysicsShape: 1
+ alphaUsage: 1
+ alphaIsTransparency: 0
+ spriteTessellationDetail: -1
+ textureType: 0
+ textureShape: 1
+ singleChannelComponent: 0
+ flipbookRows: 1
+ flipbookColumns: 1
+ maxTextureSizeSet: 0
+ compressionQualitySet: 0
+ textureFormatSet: 0
+ ignorePngGamma: 0
+ applyGammaDecoding: 0
+ swizzle: 50462976
+ cookieLightType: 0
+ platformSettings:
+ - serializedVersion: 4
+ buildTarget: DefaultTexturePlatform
+ maxTextureSize: 2048
+ resizeAlgorithm: 0
+ textureFormat: -1
+ textureCompression: 1
+ compressionQuality: 50
+ crunchedCompression: 0
+ allowsAlphaSplitting: 0
+ overridden: 0
+ ignorePlatformSupport: 0
+ androidETC2FallbackOverride: 0
+ forceMaximumCompressionQuality_BC6H_BC7: 0
+ - serializedVersion: 4
+ buildTarget: Standalone
+ maxTextureSize: 2048
+ resizeAlgorithm: 0
+ textureFormat: -1
+ textureCompression: 1
+ compressionQuality: 50
+ crunchedCompression: 0
+ allowsAlphaSplitting: 0
+ overridden: 0
+ ignorePlatformSupport: 0
+ androidETC2FallbackOverride: 0
+ forceMaximumCompressionQuality_BC6H_BC7: 0
+ spriteSheet:
+ serializedVersion: 2
+ sprites: []
+ outline: []
+ customData:
+ physicsShape: []
+ bones: []
+ spriteID:
+ internalID: 0
+ vertices: []
+ indices:
+ edges: []
+ weights: []
+ secondaryTextures: []
+ spriteCustomMetadata:
+ entries: []
+ nameFileIdTable: {}
+ mipmapLimitGroupName:
+ pSDRemoveMatte: 0
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/Screenshots/m6_expedition2.png b/Assets/Screenshots/m6_expedition2.png
new file mode 100644
index 000000000..7e76e76e1
--- /dev/null
+++ b/Assets/Screenshots/m6_expedition2.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:0feb1132b00f207a9ffce6656947ba7a09b34a9778d2c2ec2ca46f30c2bd7400
+size 177848
diff --git a/Assets/Screenshots/m6_expedition2.png.meta b/Assets/Screenshots/m6_expedition2.png.meta
new file mode 100644
index 000000000..a09be96be
--- /dev/null
+++ b/Assets/Screenshots/m6_expedition2.png.meta
@@ -0,0 +1,117 @@
+fileFormatVersion: 2
+guid: 9155a3ec3c4385a4d87c148d86cfaa97
+TextureImporter:
+ internalIDToNameTable: []
+ externalObjects: {}
+ serializedVersion: 13
+ mipmaps:
+ mipMapMode: 0
+ enableMipMap: 1
+ sRGBTexture: 1
+ linearTexture: 0
+ fadeOut: 0
+ borderMipMap: 0
+ mipMapsPreserveCoverage: 0
+ alphaTestReferenceValue: 0.5
+ mipMapFadeDistanceStart: 1
+ mipMapFadeDistanceEnd: 3
+ bumpmap:
+ convertToNormalMap: 0
+ externalNormalMap: 0
+ heightScale: 0.25
+ normalMapFilter: 0
+ flipGreenChannel: 0
+ isReadable: 0
+ streamingMipmaps: 0
+ streamingMipmapsPriority: 0
+ vTOnly: 0
+ ignoreMipmapLimit: 0
+ grayScaleToAlpha: 0
+ generateCubemap: 6
+ cubemapConvolution: 0
+ seamlessCubemap: 0
+ textureFormat: 1
+ maxTextureSize: 2048
+ textureSettings:
+ serializedVersion: 2
+ filterMode: 1
+ aniso: 1
+ mipBias: 0
+ wrapU: 0
+ wrapV: 0
+ wrapW: 0
+ nPOTScale: 1
+ lightmap: 0
+ compressionQuality: 50
+ spriteMode: 0
+ spriteExtrude: 1
+ spriteMeshType: 1
+ alignment: 0
+ spritePivot: {x: 0.5, y: 0.5}
+ spritePixelsToUnits: 100
+ spriteBorder: {x: 0, y: 0, z: 0, w: 0}
+ spriteGenerateFallbackPhysicsShape: 1
+ alphaUsage: 1
+ alphaIsTransparency: 0
+ spriteTessellationDetail: -1
+ textureType: 0
+ textureShape: 1
+ singleChannelComponent: 0
+ flipbookRows: 1
+ flipbookColumns: 1
+ maxTextureSizeSet: 0
+ compressionQualitySet: 0
+ textureFormatSet: 0
+ ignorePngGamma: 0
+ applyGammaDecoding: 0
+ swizzle: 50462976
+ cookieLightType: 0
+ platformSettings:
+ - serializedVersion: 4
+ buildTarget: DefaultTexturePlatform
+ maxTextureSize: 2048
+ resizeAlgorithm: 0
+ textureFormat: -1
+ textureCompression: 1
+ compressionQuality: 50
+ crunchedCompression: 0
+ allowsAlphaSplitting: 0
+ overridden: 0
+ ignorePlatformSupport: 0
+ androidETC2FallbackOverride: 0
+ forceMaximumCompressionQuality_BC6H_BC7: 0
+ - serializedVersion: 4
+ buildTarget: Standalone
+ maxTextureSize: 2048
+ resizeAlgorithm: 0
+ textureFormat: -1
+ textureCompression: 1
+ compressionQuality: 50
+ crunchedCompression: 0
+ allowsAlphaSplitting: 0
+ overridden: 0
+ ignorePlatformSupport: 0
+ androidETC2FallbackOverride: 0
+ forceMaximumCompressionQuality_BC6H_BC7: 0
+ spriteSheet:
+ serializedVersion: 2
+ sprites: []
+ outline: []
+ customData:
+ physicsShape: []
+ bones: []
+ spriteID:
+ internalID: 0
+ vertices: []
+ indices:
+ edges: []
+ weights: []
+ secondaryTextures: []
+ spriteCustomMetadata:
+ entries: []
+ nameFileIdTable: {}
+ mipmapLimitGroupName:
+ pSDRemoveMatte: 0
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/_Project/Materials/M_ExpeditionGround.mat b/Assets/_Project/Materials/M_ExpeditionGround.mat
new file mode 100644
index 000000000..372fc19fb
--- /dev/null
+++ b/Assets/_Project/Materials/M_ExpeditionGround.mat
@@ -0,0 +1,137 @@
+%YAML 1.1
+%TAG !u! tag:unity3d.com,2011:
+--- !u!21 &2100000
+Material:
+ serializedVersion: 8
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_Name: M_ExpeditionGround
+ m_Shader: {fileID: 4800000, guid: 933532a4fcc9baf4fa0491de14d08ed7, type: 3}
+ m_Parent: {fileID: 0}
+ m_ModifiedSerializedProperties: 0
+ m_ValidKeywords: []
+ m_InvalidKeywords: []
+ m_LightmapFlags: 4
+ m_EnableInstancingVariants: 0
+ m_DoubleSidedGI: 0
+ m_CustomRenderQueue: -1
+ stringTagMap:
+ RenderType: Opaque
+ disabledShaderPasses:
+ - MOTIONVECTORS
+ m_LockedProperties:
+ m_SavedProperties:
+ serializedVersion: 3
+ m_TexEnvs:
+ - _BaseMap:
+ m_Texture: {fileID: 0}
+ m_Scale: {x: 1, y: 1}
+ m_Offset: {x: 0, y: 0}
+ - _BumpMap:
+ m_Texture: {fileID: 0}
+ m_Scale: {x: 1, y: 1}
+ m_Offset: {x: 0, y: 0}
+ - _DetailAlbedoMap:
+ m_Texture: {fileID: 0}
+ m_Scale: {x: 1, y: 1}
+ m_Offset: {x: 0, y: 0}
+ - _DetailMask:
+ m_Texture: {fileID: 0}
+ m_Scale: {x: 1, y: 1}
+ m_Offset: {x: 0, y: 0}
+ - _DetailNormalMap:
+ m_Texture: {fileID: 0}
+ m_Scale: {x: 1, y: 1}
+ m_Offset: {x: 0, y: 0}
+ - _EmissionMap:
+ m_Texture: {fileID: 0}
+ m_Scale: {x: 1, y: 1}
+ m_Offset: {x: 0, y: 0}
+ - _MainTex:
+ m_Texture: {fileID: 0}
+ m_Scale: {x: 1, y: 1}
+ m_Offset: {x: 0, y: 0}
+ - _MetallicGlossMap:
+ m_Texture: {fileID: 0}
+ m_Scale: {x: 1, y: 1}
+ m_Offset: {x: 0, y: 0}
+ - _OcclusionMap:
+ m_Texture: {fileID: 0}
+ m_Scale: {x: 1, y: 1}
+ m_Offset: {x: 0, y: 0}
+ - _ParallaxMap:
+ m_Texture: {fileID: 0}
+ m_Scale: {x: 1, y: 1}
+ m_Offset: {x: 0, y: 0}
+ - _SpecGlossMap:
+ m_Texture: {fileID: 0}
+ m_Scale: {x: 1, y: 1}
+ m_Offset: {x: 0, y: 0}
+ - unity_Lightmaps:
+ m_Texture: {fileID: 0}
+ m_Scale: {x: 1, y: 1}
+ m_Offset: {x: 0, y: 0}
+ - unity_LightmapsInd:
+ m_Texture: {fileID: 0}
+ m_Scale: {x: 1, y: 1}
+ m_Offset: {x: 0, y: 0}
+ - unity_ShadowMasks:
+ m_Texture: {fileID: 0}
+ m_Scale: {x: 1, y: 1}
+ m_Offset: {x: 0, y: 0}
+ m_Ints: []
+ m_Floats:
+ - _AddPrecomputedVelocity: 0
+ - _AlphaClip: 0
+ - _AlphaToMask: 0
+ - _Blend: 0
+ - _BlendModePreserveSpecular: 1
+ - _BumpScale: 1
+ - _ClearCoatMask: 0
+ - _ClearCoatSmoothness: 0
+ - _Cull: 2
+ - _Cutoff: 0.5
+ - _DetailAlbedoMapScale: 1
+ - _DetailNormalMapScale: 1
+ - _DstBlend: 0
+ - _DstBlendAlpha: 0
+ - _EnvironmentReflections: 1
+ - _GlossMapScale: 0
+ - _Glossiness: 0
+ - _GlossyReflections: 0
+ - _Metallic: 0
+ - _OcclusionStrength: 1
+ - _Parallax: 0.005
+ - _QueueOffset: 0
+ - _ReceiveShadows: 1
+ - _Smoothness: 0.5
+ - _SmoothnessTextureChannel: 0
+ - _SpecularHighlights: 1
+ - _SrcBlend: 1
+ - _SrcBlendAlpha: 1
+ - _Surface: 0
+ - _WorkflowMode: 1
+ - _XRMotionVectorsPass: 1
+ - _ZWrite: 1
+ m_Colors:
+ - _BaseColor: {r: 0.33, g: 0.28, b: 0.48, a: 1}
+ - _Color: {r: 0.32999998, g: 0.27999997, b: 0.47999996, a: 1}
+ - _EmissionColor: {r: 0, g: 0, b: 0, a: 1}
+ - _SpecColor: {r: 0.19999996, g: 0.19999996, b: 0.19999996, a: 1}
+ m_BuildTextureStacks: []
+ m_AllowLocking: 1
+--- !u!114 &7570948517623919954
+MonoBehaviour:
+ m_ObjectHideFlags: 11
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 0}
+ m_Enabled: 1
+ m_EditorHideFlags: 0
+ m_Script: {fileID: 11500000, guid: d0353a89b1f911e48b9e16bdc9f2e058, type: 3}
+ m_Name:
+ m_EditorClassIdentifier: Unity.RenderPipelines.Universal.Editor::UnityEditor.Rendering.Universal.AssetVersion
+ version: 10
diff --git a/Assets/_Project/Materials/M_ExpeditionGround.mat.meta b/Assets/_Project/Materials/M_ExpeditionGround.mat.meta
new file mode 100644
index 000000000..1888fef6b
--- /dev/null
+++ b/Assets/_Project/Materials/M_ExpeditionGround.mat.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 6afcc12e55743ac45b265795f24238dd
+NativeFormatImporter:
+ externalObjects: {}
+ mainObjectFileID: 2100000
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/_Project/Prefabs/CycleDirector.prefab b/Assets/_Project/Prefabs/CycleDirector.prefab
new file mode 100644
index 000000000..ceb22eaf0
--- /dev/null
+++ b/Assets/_Project/Prefabs/CycleDirector.prefab
@@ -0,0 +1,86 @@
+%YAML 1.1
+%TAG !u! tag:unity3d.com,2011:
+--- !u!1 &3885353946372160549
+GameObject:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ serializedVersion: 6
+ m_Component:
+ - component: {fileID: 3572766465862231365}
+ - component: {fileID: 9053853372340598254}
+ - component: {fileID: 6834786618115927220}
+ - component: {fileID: 5858859957262695065}
+ m_Layer: 0
+ m_Name: CycleDirector
+ m_TagString: Untagged
+ m_Icon: {fileID: 0}
+ m_NavMeshLayer: 0
+ m_StaticEditorFlags: 0
+ m_IsActive: 1
+--- !u!4 &3572766465862231365
+Transform:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 3885353946372160549}
+ serializedVersion: 2
+ m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
+ m_LocalPosition: {x: 0, y: 0, z: 0}
+ m_LocalScale: {x: 2.5, y: 2.5, z: 2.5}
+ m_ConstrainProportionsScale: 0
+ m_Children: []
+ m_Father: {fileID: 0}
+ m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
+--- !u!114 &9053853372340598254
+MonoBehaviour:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 3885353946372160549}
+ m_Enabled: 1
+ m_EditorHideFlags: 0
+ m_Script: {fileID: 11500000, guid: c16549610bfe4458aa9389201d072bb6, type: 3}
+ m_Name:
+ m_EditorClassIdentifier: Unity.Entities.Hybrid::Unity.Entities.Hybrid.Baking.LinkedEntityGroupAuthoring
+--- !u!114 &6834786618115927220
+MonoBehaviour:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 3885353946372160549}
+ m_Enabled: 1
+ m_EditorHideFlags: 0
+ m_Script: {fileID: 11500000, guid: 7c79d771cedb4794bf100ce60df5f764, type: 3}
+ m_Name:
+ m_EditorClassIdentifier: Unity.NetCode.Authoring.Hybrid::Unity.NetCode.GhostAuthoringComponent
+ HasOwner: 0
+ SupportAutoCommandTarget: 1
+ TrackInterpolationDelay: 0
+ GhostGroup: 0
+ UsePreSerialization: 0
+ UseSingleBaseline: 0
+ RollbackPredictedSpawnedGhostState: 0
+ RollbackPredictionOnStructuralChanges: 1
+ DefaultGhostMode: 0
+ SupportedGhostModes: 3
+ OptimizationMode: 0
+ Importance: 1
+ MaxSendRate: 0
+ prefabId:
+--- !u!114 &5858859957262695065
+MonoBehaviour:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 3885353946372160549}
+ m_Enabled: 1
+ m_EditorHideFlags: 0
+ m_Script: {fileID: 11500000, guid: 843fd0d567f48ad46840dcce0ce84bbc, type: 3}
+ m_Name:
+ m_EditorClassIdentifier: ProjectM.Authoring::ProjectM.Authoring.CycleDirectorAuthoring
diff --git a/Assets/_Project/Prefabs/CycleDirector.prefab.meta b/Assets/_Project/Prefabs/CycleDirector.prefab.meta
new file mode 100644
index 000000000..00929cf61
--- /dev/null
+++ b/Assets/_Project/Prefabs/CycleDirector.prefab.meta
@@ -0,0 +1,7 @@
+fileFormatVersion: 2
+guid: 529ca7203da40f5489e9e3040ed1fc22
+PrefabImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/_Project/Prefabs/ResourceNode.prefab b/Assets/_Project/Prefabs/ResourceNode.prefab
new file mode 100644
index 000000000..43b285976
--- /dev/null
+++ b/Assets/_Project/Prefabs/ResourceNode.prefab
@@ -0,0 +1,149 @@
+%YAML 1.1
+%TAG !u! tag:unity3d.com,2011:
+--- !u!1 &3885353946372160549
+GameObject:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ serializedVersion: 6
+ m_Component:
+ - component: {fileID: 3572766465862231365}
+ - component: {fileID: 3909651526955663392}
+ - component: {fileID: 3320445911748035220}
+ - component: {fileID: 9053853372340598254}
+ - component: {fileID: 6834786618115927220}
+ - component: {fileID: 88785505736215562}
+ m_Layer: 0
+ m_Name: ResourceNode
+ m_TagString: Untagged
+ m_Icon: {fileID: 0}
+ m_NavMeshLayer: 0
+ m_StaticEditorFlags: 0
+ m_IsActive: 1
+--- !u!4 &3572766465862231365
+Transform:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 3885353946372160549}
+ serializedVersion: 2
+ m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
+ m_LocalPosition: {x: 0, y: 0, z: 0}
+ m_LocalScale: {x: 2.5, y: 2.5, z: 2.5}
+ m_ConstrainProportionsScale: 0
+ m_Children: []
+ m_Father: {fileID: 0}
+ m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
+--- !u!33 &3909651526955663392
+MeshFilter:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 3885353946372160549}
+ m_Mesh: {fileID: 4300000, guid: abc00000000010690097314383055197, type: 3}
+--- !u!23 &3320445911748035220
+MeshRenderer:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 3885353946372160549}
+ m_Enabled: 1
+ m_CastShadows: 1
+ m_ReceiveShadows: 1
+ m_DynamicOccludee: 1
+ m_StaticShadowCaster: 0
+ m_MotionVectors: 1
+ m_LightProbeUsage: 1
+ m_ReflectionProbeUsage: 1
+ m_RayTracingMode: 2
+ m_RayTraceProcedural: 0
+ m_RayTracingAccelStructBuildFlagsOverride: 0
+ m_RayTracingAccelStructBuildFlags: 1
+ m_SmallMeshCulling: 1
+ m_ForceMeshLod: -1
+ m_MeshLodSelectionBias: 0
+ m_RenderingLayerMask: 1
+ m_RendererPriority: 0
+ m_Materials:
+ - {fileID: 2100000, guid: ba23fa98368bb4a4997bfd08547c83ee, type: 2}
+ m_StaticBatchInfo:
+ firstSubMesh: 0
+ subMeshCount: 0
+ m_StaticBatchRoot: {fileID: 0}
+ m_ProbeAnchor: {fileID: 0}
+ m_LightProbeVolumeOverride: {fileID: 0}
+ m_ScaleInLightmap: 1
+ m_ReceiveGI: 1
+ m_PreserveUVs: 1
+ m_IgnoreNormalsForChartDetection: 0
+ m_ImportantGI: 0
+ m_StitchLightmapSeams: 1
+ m_SelectedEditorRenderState: 3
+ m_MinimumChartSize: 4
+ m_AutoUVMaxDistance: 0.5
+ m_AutoUVMaxAngle: 89
+ m_LightmapParameters: {fileID: 0}
+ m_GlobalIlluminationMeshLod: 0
+ m_SortingLayerID: 0
+ m_SortingLayer: 0
+ m_SortingOrder: 0
+ m_MaskInteraction: 0
+ m_AdditionalVertexStreams: {fileID: 0}
+--- !u!114 &9053853372340598254
+MonoBehaviour:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 3885353946372160549}
+ m_Enabled: 1
+ m_EditorHideFlags: 0
+ m_Script: {fileID: 11500000, guid: c16549610bfe4458aa9389201d072bb6, type: 3}
+ m_Name:
+ m_EditorClassIdentifier: Unity.Entities.Hybrid::Unity.Entities.Hybrid.Baking.LinkedEntityGroupAuthoring
+--- !u!114 &6834786618115927220
+MonoBehaviour:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 3885353946372160549}
+ m_Enabled: 1
+ m_EditorHideFlags: 0
+ m_Script: {fileID: 11500000, guid: 7c79d771cedb4794bf100ce60df5f764, type: 3}
+ m_Name:
+ m_EditorClassIdentifier: Unity.NetCode.Authoring.Hybrid::Unity.NetCode.GhostAuthoringComponent
+ HasOwner: 0
+ SupportAutoCommandTarget: 1
+ TrackInterpolationDelay: 0
+ GhostGroup: 0
+ UsePreSerialization: 0
+ UseSingleBaseline: 0
+ RollbackPredictedSpawnedGhostState: 0
+ RollbackPredictionOnStructuralChanges: 1
+ DefaultGhostMode: 0
+ SupportedGhostModes: 3
+ OptimizationMode: 0
+ Importance: 1
+ MaxSendRate: 0
+ prefabId: 8565e5eb00679fb45b8b7dac1e2ae9f3
+--- !u!114 &88785505736215562
+MonoBehaviour:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 3885353946372160549}
+ m_Enabled: 1
+ m_EditorHideFlags: 0
+ m_Script: {fileID: 11500000, guid: 174563c586d9a0f4bb84cca191f4a1f0, type: 3}
+ m_Name:
+ m_EditorClassIdentifier: ProjectM.Authoring::ProjectM.Authoring.ResourceNodeAuthoring
+ Kind: 1
+ Amount: 30
+ HarvestPerHit: 5
+ HitRadius: 1.2
diff --git a/Assets/_Project/Prefabs/ResourceNode.prefab.meta b/Assets/_Project/Prefabs/ResourceNode.prefab.meta
new file mode 100644
index 000000000..625f64736
--- /dev/null
+++ b/Assets/_Project/Prefabs/ResourceNode.prefab.meta
@@ -0,0 +1,7 @@
+fileFormatVersion: 2
+guid: 8565e5eb00679fb45b8b7dac1e2ae9f3
+PrefabImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/_Project/Scripts/Authoring/Economy.meta b/Assets/_Project/Scripts/Authoring/Economy.meta
new file mode 100644
index 000000000..036ab1a7f
--- /dev/null
+++ b/Assets/_Project/Scripts/Authoring/Economy.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: c81ee1ba775463a4d940bb397a01f4e6
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/_Project/Scripts/Authoring/Economy/ResourceFieldSpawnerAuthoring.cs b/Assets/_Project/Scripts/Authoring/Economy/ResourceFieldSpawnerAuthoring.cs
new file mode 100644
index 000000000..7e01deb7c
--- /dev/null
+++ b/Assets/_Project/Scripts/Authoring/Economy/ResourceFieldSpawnerAuthoring.cs
@@ -0,0 +1,39 @@
+using ProjectM.Simulation;
+using Unity.Entities;
+using UnityEngine;
+
+namespace ProjectM.Authoring
+{
+ ///
+ /// Authoring for the baked singleton (mirrors StorageSpawnerAuthoring).
+ /// Place once in the gameplay subscene and assign the resource-node ghost prefab; ExpeditionFieldSystem
+ /// scatters the field each Expedition. Carries no transform.
+ ///
+ public class ResourceFieldSpawnerAuthoring : MonoBehaviour
+ {
+ [Tooltip("Resource-node ghost prefab. Must carry ResourceNodeAuthoring + a GhostAuthoringComponent (ownerless, interpolated).")]
+ public GameObject NodePrefab;
+
+ [Tooltip("Number of nodes per expedition.")]
+ [Min(1)] public int Count = 8;
+
+ [Tooltip("Scatter radius (world units) around the expedition origin.")]
+ [Min(1f)] public float Radius = 12f;
+
+ private class ResourceFieldSpawnerBaker : Baker
+ {
+ public override void Bake(ResourceFieldSpawnerAuthoring authoring)
+ {
+ var entity = GetEntity(authoring, TransformUsageFlags.None);
+ AddComponent(entity, new ResourceFieldSpawner
+ {
+ Prefab = authoring.NodePrefab != null
+ ? GetEntity(authoring.NodePrefab, TransformUsageFlags.Dynamic)
+ : Entity.Null,
+ Count = authoring.Count,
+ Radius = authoring.Radius,
+ });
+ }
+ }
+ }
+}
diff --git a/Assets/_Project/Scripts/Authoring/Economy/ResourceFieldSpawnerAuthoring.cs.meta b/Assets/_Project/Scripts/Authoring/Economy/ResourceFieldSpawnerAuthoring.cs.meta
new file mode 100644
index 000000000..26b74b081
--- /dev/null
+++ b/Assets/_Project/Scripts/Authoring/Economy/ResourceFieldSpawnerAuthoring.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: e9e840f8b95266140b0ac5bd4e81391b
\ No newline at end of file
diff --git a/Assets/_Project/Scripts/Authoring/Economy/ResourceNodeAuthoring.cs b/Assets/_Project/Scripts/Authoring/Economy/ResourceNodeAuthoring.cs
new file mode 100644
index 000000000..65023e5f7
--- /dev/null
+++ b/Assets/_Project/Scripts/Authoring/Economy/ResourceNodeAuthoring.cs
@@ -0,0 +1,45 @@
+using ProjectM.Simulation;
+using Unity.Entities;
+using UnityEngine;
+
+namespace ProjectM.Authoring
+{
+ ///
+ /// Authoring for a resource-node ghost prefab (ownerless interpolated — duplicate from UpgradePickup.prefab
+ /// so the GhostAuthoringComponent comes free). Bakes +
+ /// (reused for the harvest hit test) + {Expedition} so GhostRelevancy scopes the node
+ /// to expedition players. The field spawner overrides ResourceId (round-robin) and Position per instance.
+ ///
+ public class ResourceNodeAuthoring : MonoBehaviour
+ {
+ public enum ResourceKind : byte { Aether = 1, Ore = 2, Biomass = 3 }
+
+ [Tooltip("Default resource type (the spawner round-robins this per node).")]
+ public ResourceKind Kind = ResourceKind.Aether;
+
+ [Tooltip("Total resource units in the node before it depletes.")]
+ [Min(1)] public int Amount = 30;
+
+ [Tooltip("Units harvested per projectile hit.")]
+ [Min(1f)] public float HarvestPerHit = 5f;
+
+ [Tooltip("Hit radius (world units) for the harvest sweep.")]
+ [Min(0f)] public float HitRadius = 1.2f;
+
+ private class ResourceNodeBaker : Baker
+ {
+ public override void Bake(ResourceNodeAuthoring authoring)
+ {
+ var entity = GetEntity(authoring, TransformUsageFlags.Dynamic);
+ AddComponent(entity, new ResourceNode
+ {
+ ResourceId = (byte)authoring.Kind,
+ Remaining = authoring.Amount,
+ HarvestPerHit = authoring.HarvestPerHit,
+ });
+ AddComponent(entity, new HitRadius { Value = authoring.HitRadius });
+ AddComponent(entity, new RegionTag { Region = RegionId.Expedition });
+ }
+ }
+ }
+}
diff --git a/Assets/_Project/Scripts/Authoring/Economy/ResourceNodeAuthoring.cs.meta b/Assets/_Project/Scripts/Authoring/Economy/ResourceNodeAuthoring.cs.meta
new file mode 100644
index 000000000..da4c7c522
--- /dev/null
+++ b/Assets/_Project/Scripts/Authoring/Economy/ResourceNodeAuthoring.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 174563c586d9a0f4bb84cca191f4a1f0
\ No newline at end of file
diff --git a/Assets/_Project/Scripts/Authoring/World.meta b/Assets/_Project/Scripts/Authoring/World.meta
new file mode 100644
index 000000000..3f1a8a5f3
--- /dev/null
+++ b/Assets/_Project/Scripts/Authoring/World.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 4b2e77367c16a9a41943145e582954d1
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/_Project/Scripts/Authoring/World/CycleDirectorAuthoring.cs b/Assets/_Project/Scripts/Authoring/World/CycleDirectorAuthoring.cs
new file mode 100644
index 000000000..e4a5c18a8
--- /dev/null
+++ b/Assets/_Project/Scripts/Authoring/World/CycleDirectorAuthoring.cs
@@ -0,0 +1,34 @@
+using ProjectM.Simulation;
+using Unity.Entities;
+using UnityEngine;
+
+namespace ProjectM.Authoring
+{
+ ///
+ /// Authoring for the GLOBAL cycle-director ghost prefab: an ownerless INTERPOLATED ghost (the
+ /// GhostAuthoringComponent is inherited when this prefab is duplicated from UpgradePickup.prefab) that
+ /// carries the replicated macro-loop state () and the shared resource ledger
+ /// (a buffer marked by ). It is GLOBAL — it must
+ /// carry NO so GhostRelevancy keeps it relevant to every connection regardless of
+ /// region. The server CycleDirectorSpawnSystem overrides the baked CycleState at spawn (real PhaseEndTick)
+ /// and adds the server-only CycleRuntime.
+ ///
+ public class CycleDirectorAuthoring : MonoBehaviour
+ {
+ private class CycleDirectorBaker : Baker
+ {
+ public override void Bake(CycleDirectorAuthoring authoring)
+ {
+ var entity = GetEntity(authoring, TransformUsageFlags.Dynamic);
+ AddComponent(entity, new CycleState
+ {
+ Phase = CyclePhase.Expedition,
+ CycleNumber = 1,
+ PhaseEndTick = 0u,
+ });
+ AddComponent(entity);
+ AddBuffer(entity);
+ }
+ }
+ }
+}
diff --git a/Assets/_Project/Scripts/Authoring/World/CycleDirectorAuthoring.cs.meta b/Assets/_Project/Scripts/Authoring/World/CycleDirectorAuthoring.cs.meta
new file mode 100644
index 000000000..a47726361
--- /dev/null
+++ b/Assets/_Project/Scripts/Authoring/World/CycleDirectorAuthoring.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 843fd0d567f48ad46840dcce0ce84bbc
\ No newline at end of file
diff --git a/Assets/_Project/Scripts/Authoring/World/CycleDirectorSpawnerAuthoring.cs b/Assets/_Project/Scripts/Authoring/World/CycleDirectorSpawnerAuthoring.cs
new file mode 100644
index 000000000..58f112b5e
--- /dev/null
+++ b/Assets/_Project/Scripts/Authoring/World/CycleDirectorSpawnerAuthoring.cs
@@ -0,0 +1,31 @@
+using ProjectM.Simulation;
+using Unity.Entities;
+using UnityEngine;
+
+namespace ProjectM.Authoring
+{
+ ///
+ /// Authoring for the baked singleton (mirrors StorageSpawnerAuthoring).
+ /// Place once in the gameplay subscene; the server-only CycleDirectorSpawnSystem reads it, instantiates the
+ /// global cycle-director ghost, then destroys the singleton so it fires exactly once. Carries no transform.
+ ///
+ public class CycleDirectorSpawnerAuthoring : MonoBehaviour
+ {
+ [Tooltip("Cycle-director ghost prefab. Must carry CycleDirectorAuthoring + a GhostAuthoringComponent (ownerless, interpolated).")]
+ public GameObject DirectorPrefab;
+
+ private class CycleDirectorSpawnerBaker : Baker
+ {
+ public override void Bake(CycleDirectorSpawnerAuthoring authoring)
+ {
+ var entity = GetEntity(authoring, TransformUsageFlags.None);
+ AddComponent(entity, new CycleDirectorSpawner
+ {
+ Prefab = authoring.DirectorPrefab != null
+ ? GetEntity(authoring.DirectorPrefab, TransformUsageFlags.Dynamic)
+ : Entity.Null,
+ });
+ }
+ }
+ }
+}
diff --git a/Assets/_Project/Scripts/Authoring/World/CycleDirectorSpawnerAuthoring.cs.meta b/Assets/_Project/Scripts/Authoring/World/CycleDirectorSpawnerAuthoring.cs.meta
new file mode 100644
index 000000000..7bf72ae45
--- /dev/null
+++ b/Assets/_Project/Scripts/Authoring/World/CycleDirectorSpawnerAuthoring.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 1f4d2cb1e17d6a1429525674969dd3f0
\ No newline at end of file
diff --git a/Assets/_Project/Scripts/Authoring/World/ExpeditionGateAuthoring.cs b/Assets/_Project/Scripts/Authoring/World/ExpeditionGateAuthoring.cs
new file mode 100644
index 000000000..327a6678d
--- /dev/null
+++ b/Assets/_Project/Scripts/Authoring/World/ExpeditionGateAuthoring.cs
@@ -0,0 +1,45 @@
+using ProjectM.Simulation;
+using Unity.Entities;
+using Unity.Mathematics;
+using UnityEngine;
+
+namespace ProjectM.Authoring
+{
+ ///
+ /// Authoring for a walk-in . Place on a visible gate object in the gameplay
+ /// subscene; baked into both worlds at the gate's position (the server reads its LocalTransform for the
+ /// overlap test, the client renders the mesh). Set From/To regions + the arrival point in the destination
+ /// region (offset from that region's gate so the player doesn't immediately re-trigger).
+ ///
+ public class ExpeditionGateAuthoring : MonoBehaviour
+ {
+ public enum Region : byte { Base = 0, Expedition = 1 }
+
+ [Tooltip("Region a player must be in for this gate to act on them.")]
+ public Region From = Region.Base;
+
+ [Tooltip("Region the player is transited to.")]
+ public Region To = Region.Expedition;
+
+ [Min(0.5f)] public float Radius = 2.5f;
+
+ [Tooltip("Where the player arrives in the destination region (offset from that region's gate).")]
+ public Vector3 ArrivalPos = new Vector3(1000f, 1f, 0f);
+
+ private class ExpeditionGateBaker : Baker
+ {
+ public override void Bake(ExpeditionGateAuthoring authoring)
+ {
+ // Dynamic so the baked entity carries a LocalTransform the server can read for the overlap test.
+ var entity = GetEntity(authoring, TransformUsageFlags.Dynamic);
+ AddComponent(entity, new ExpeditionGate
+ {
+ FromRegion = (byte)authoring.From,
+ ToRegion = (byte)authoring.To,
+ Radius = authoring.Radius,
+ ArrivalPos = new float3(authoring.ArrivalPos.x, authoring.ArrivalPos.y, authoring.ArrivalPos.z),
+ });
+ }
+ }
+ }
+}
diff --git a/Assets/_Project/Scripts/Authoring/World/ExpeditionGateAuthoring.cs.meta b/Assets/_Project/Scripts/Authoring/World/ExpeditionGateAuthoring.cs.meta
new file mode 100644
index 000000000..f52499f9a
--- /dev/null
+++ b/Assets/_Project/Scripts/Authoring/World/ExpeditionGateAuthoring.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 22f744b59ad23834abe28fc09b661005
\ No newline at end of file
diff --git a/Assets/_Project/Scripts/Client/Presentation/HudSystem.cs b/Assets/_Project/Scripts/Client/Presentation/HudSystem.cs
index 206a3ea39..1d0fe552f 100644
--- a/Assets/_Project/Scripts/Client/Presentation/HudSystem.cs
+++ b/Assets/_Project/Scripts/Client/Presentation/HudSystem.cs
@@ -24,6 +24,9 @@ namespace ProjectM.Client
RectTransform _cooldownFill;
Text _healthText;
Text _threatText;
+ Text _phaseText;
+ Text _resourceText;
+ Text _locationText;
GameObject _respawnOverlay;
EntityQuery _huskQuery;
@@ -48,6 +51,55 @@ namespace ProjectM.Client
bool haveTick = SystemAPI.TryGetSingleton(out var nt);
+ // Macro-loop HUD (phase + cycle + countdown + location), read before the per-player early-out so it persists pre-spawn.
+ bool haveCycle = SystemAPI.TryGetSingleton(out var cyc);
+ if (_phaseText != null && haveCycle)
+ {
+ var endTick = new NetworkTick(cyc.PhaseEndTick);
+ string detail;
+ if (cyc.Phase == CyclePhase.Defend)
+ detail = _huskQuery.CalculateEntityCount() + " HUSKS";
+ else if (haveTick && cyc.PhaseEndTick != 0 && endTick.IsValid && endTick.IsNewerThan(nt.ServerTick))
+ detail = (endTick.TicksSince(nt.ServerTick) / 60) + "s";
+ else
+ detail = "";
+ _phaseText.text = PhaseLabel(cyc.Phase) + (detail.Length > 0 ? " - " + detail : "") + " CYCLE " + cyc.CycleNumber;
+ _phaseText.color = PhaseColor(cyc.Phase);
+ }
+ else if (_phaseText != null)
+ {
+ _phaseText.text = "";
+ }
+
+ if (_locationText != null)
+ {
+ var cam = Camera.main;
+ bool onExpedition = cam != null && cam.transform.position.x > 500f;
+ _locationText.text = onExpedition
+ ? "ON EXPEDITION - return through the gate"
+ : "AT BASE" + (haveCycle && cyc.Phase == CyclePhase.Expedition ? " - step into the gate to deploy" : "");
+ _locationText.color = onExpedition ? new Color(1f, 0.8f, 0.4f) : new Color(0.6f, 0.85f, 1f);
+ }
+
+ if (_resourceText != null)
+ {
+ string res = "";
+ if (SystemAPI.TryGetSingletonEntity(out var ledgerE))
+ {
+ var buf = SystemAPI.GetBuffer(ledgerE);
+ int aether = 0, ore = 0, bio = 0;
+ for (int i = 0; i < buf.Length; i++)
+ {
+ var en = buf[i];
+ if (en.ItemId == ResourceId.Aether) aether = en.Count;
+ else if (en.ItemId == ResourceId.Ore) ore = en.Count;
+ else if (en.ItemId == ResourceId.Biomass) bio = en.Count;
+ }
+ res = "AETHER " + aether + " ORE " + ore + " BIO " + bio;
+ }
+ _resourceText.text = res;
+ }
+
bool found = false;
float hp = 0f, maxHp = 1f, cdFrac = 1f;
bool dead = false, shielded = false;
@@ -77,7 +129,7 @@ namespace ProjectM.Client
break;
}
- _canvas.enabled = found;
+ _canvas.enabled = found || haveCycle;
if (!found) return;
float frac = maxHp > 0f ? Mathf.Clamp01(hp / maxHp) : 0f;
@@ -139,6 +191,27 @@ namespace ProjectM.Client
trt.anchorMin = new Vector2(1, 1); trt.anchorMax = new Vector2(1, 1); trt.pivot = new Vector2(1, 1);
trt.anchoredPosition = new Vector2(-40, -30); trt.sizeDelta = new Vector2(380, 50);
+ // Cycle phase + number (top-center).
+ _phaseText = MakeText("PhaseText", _canvas.transform, "EXPEDITION CYCLE 1", 34, TextAnchor.UpperCenter,
+ new Color(0.55f, 0.9f, 1f), font);
+ var prt = _phaseText.rectTransform;
+ prt.anchorMin = new Vector2(0.5f, 1f); prt.anchorMax = new Vector2(0.5f, 1f); prt.pivot = new Vector2(0.5f, 1f);
+ prt.anchoredPosition = new Vector2(0, -24); prt.sizeDelta = new Vector2(600, 50);
+
+ // Resource ledger counts (top-center, below phase).
+ _resourceText = MakeText("ResourceText", _canvas.transform, "", 24, TextAnchor.UpperCenter,
+ new Color(0.7f, 0.95f, 0.8f), font);
+ var rrt = _resourceText.rectTransform;
+ rrt.anchorMin = new Vector2(0.5f, 1f); rrt.anchorMax = new Vector2(0.5f, 1f); rrt.pivot = new Vector2(0.5f, 1f);
+ rrt.anchoredPosition = new Vector2(0, -64); rrt.sizeDelta = new Vector2(600, 40);
+
+ // Location + gate hint (top-center, below resources).
+ _locationText = MakeText("LocationText", _canvas.transform, "", 22, TextAnchor.UpperCenter,
+ new Color(0.6f, 0.85f, 1f), font);
+ var lrt = _locationText.rectTransform;
+ lrt.anchorMin = new Vector2(0.5f, 1f); lrt.anchorMax = new Vector2(0.5f, 1f); lrt.pivot = new Vector2(0.5f, 1f);
+ lrt.anchoredPosition = new Vector2(0, -96); lrt.sizeDelta = new Vector2(760, 36);
+
// Downed / respawning overlay (full screen, toggled by Dead).
_respawnOverlay = new GameObject("RespawnOverlay", typeof(RectTransform));
_respawnOverlay.transform.SetParent(_canvas.transform, false);
@@ -211,5 +284,27 @@ namespace ProjectM.Client
if (f == null) f = Font.CreateDynamicFontFromOSFont(new[] { "Arial", "Liberation Sans", "DejaVu Sans" }, 28);
return f;
}
+
+ static Color PhaseColor(byte phase)
+ {
+ switch (phase)
+ {
+ case CyclePhase.Expedition: return new Color(0.45f, 0.85f, 1f);
+ case CyclePhase.Defend: return new Color(1f, 0.5f, 0.3f);
+ case CyclePhase.Build: return new Color(0.45f, 0.95f, 0.6f);
+ default: return Color.white;
+ }
+ }
+
+ static string PhaseLabel(byte phase)
+ {
+ switch (phase)
+ {
+ case CyclePhase.Expedition: return "EXPEDITION";
+ case CyclePhase.Defend: return "DEFEND";
+ case CyclePhase.Build: return "BUILD";
+ default: return "";
+ }
+ }
}
}
diff --git a/Assets/_Project/Scripts/Server/Combat/WaveSystem.cs b/Assets/_Project/Scripts/Server/Combat/WaveSystem.cs
index 5e278c149..7a1eaf772 100644
--- a/Assets/_Project/Scripts/Server/Combat/WaveSystem.cs
+++ b/Assets/_Project/Scripts/Server/Combat/WaveSystem.cs
@@ -41,6 +41,9 @@ namespace ProjectM.Server
if (!serverTick.IsValid)
return;
uint now = serverTick.TickIndexForValidTick;
+ // M6 Aether Cycle: the base-defense wave only runs during the Defend phase.
+ if (SystemAPI.TryGetSingleton(out var cycle) && cycle.Phase != CyclePhase.Defend)
+ return;
var director = SystemAPI.GetSingleton();
var directorEntity = SystemAPI.GetSingletonEntity();
@@ -84,6 +87,8 @@ namespace ProjectM.Server
var ecb = new EntityCommandBuffer(Allocator.Temp);
var husk = ecb.Instantiate(prefabs[prefabIdx].Prefab);
ecb.SetComponent(husk, LocalTransform.FromPosition(pos));
+ // Husks belong to the base region (hidden from expedition players by relevancy).
+ ecb.AddComponent(husk, new RegionTag { Region = RegionId.Base });
ecb.Playback(state.EntityManager);
ecb.Dispose();
diff --git a/Assets/_Project/Scripts/Server/Connection/GoInGameServerSystem.cs b/Assets/_Project/Scripts/Server/Connection/GoInGameServerSystem.cs
index 58e8a67a8..a5aacee2d 100644
--- a/Assets/_Project/Scripts/Server/Connection/GoInGameServerSystem.cs
+++ b/Assets/_Project/Scripts/Server/Connection/GoInGameServerSystem.cs
@@ -53,6 +53,8 @@ namespace ProjectM.Server
var player = ecb.Instantiate(spawner.PlayerPrefab);
ecb.SetComponent(player, LocalTransform.FromPosition(center + PlayerSpawnMath.SpawnOffset(networkId.Value, spawner.SpawnRingRadius, spawner.RingSlots)));
ecb.SetComponent(player, new GhostOwner { NetworkId = networkId.Value });
+ // Tag the player into the base region (M6 region/relevancy split).
+ ecb.AddComponent(player, new RegionTag { Region = RegionId.Base });
// Auto-despawn the player when its owning connection is removed.
ecb.AppendToBuffer(connection, new LinkedEntityGroup { Value = player });
diff --git a/Assets/_Project/Scripts/Server/Economy.meta b/Assets/_Project/Scripts/Server/Economy.meta
new file mode 100644
index 000000000..2f20c1446
--- /dev/null
+++ b/Assets/_Project/Scripts/Server/Economy.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: c169eff521b1ff748b598a1b1a895196
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/_Project/Scripts/Server/Economy/ExpeditionFieldSystem.cs b/Assets/_Project/Scripts/Server/Economy/ExpeditionFieldSystem.cs
new file mode 100644
index 000000000..4c558cb0e
--- /dev/null
+++ b/Assets/_Project/Scripts/Server/Economy/ExpeditionFieldSystem.cs
@@ -0,0 +1,88 @@
+using ProjectM.Simulation;
+using Unity.Burst;
+using Unity.Collections;
+using Unity.Entities;
+using Unity.Mathematics;
+using Unity.Transforms;
+
+namespace ProjectM.Server
+{
+ ///
+ /// Server-only procedural expedition-field manager. Edge-triggered off the cycle phase (via the server-only
+ /// ): on ENTERING Expedition for a not-yet-seeded cycle it scatters
+ /// resource-node ghosts (seeded by CycleNumber via
+ /// ) around the expedition region origin, each
+ /// {Expedition}; on LEAVING Expedition it destroys every node. Runs in the plain
+ /// server SimulationSystemGroup [UpdateAfter(CyclePhaseSystem)] so the phase edge is observed the
+ /// same tick. Server-authoritative; clients despawn nodes via GhostDespawnSystem. Per-cycle reproducible
+ /// (the seed is the monotonic int CycleNumber, compared by equality — never tick math; never seed 0).
+ ///
+ [BurstCompile]
+ [WorldSystemFilter(WorldSystemFilterFlags.ServerSimulation)]
+ [UpdateInGroup(typeof(SimulationSystemGroup))]
+ [UpdateAfter(typeof(CyclePhaseSystem))]
+ public partial struct ExpeditionFieldSystem : ISystem
+ {
+ [BurstCompile]
+ public void OnCreate(ref SystemState state)
+ {
+ state.RequireForUpdate();
+ state.RequireForUpdate();
+ }
+
+ [BurstCompile]
+ public void OnUpdate(ref SystemState state)
+ {
+ var cycleEntity = SystemAPI.GetSingletonEntity();
+ var cycle = SystemAPI.GetComponent(cycleEntity);
+ var runtime = SystemAPI.GetComponent(cycleEntity);
+ var spawner = SystemAPI.GetSingleton();
+
+ float3 baseCenter = new float3(0f, 1f, 0f);
+ if (SystemAPI.TryGetSingleton(out var anchor))
+ baseCenter = BaseGridMath.PlotCenter(anchor);
+ float3 origin = RegionMath.RegionOrigin(RegionId.Expedition, baseCenter);
+
+ var ecb = new EntityCommandBuffer(Allocator.Temp);
+
+ // SPAWN edge: entered Expedition for a cycle we have not seeded yet.
+ if (cycle.Phase == CyclePhase.Expedition
+ && runtime.LastSpawnedCycle != cycle.CycleNumber
+ && spawner.Prefab != Entity.Null)
+ {
+ var baseXform = SystemAPI.GetComponent(spawner.Prefab);
+ var prefabNode = SystemAPI.GetComponent(spawner.Prefab);
+ var rng = new Random((uint)math.max(1, cycle.CycleNumber));
+ int count = math.max(1, spawner.Count);
+ for (int i = 0; i < count; i++)
+ {
+ var node = ecb.Instantiate(spawner.Prefab);
+
+ float ang = rng.NextFloat(0f, math.PI * 2f);
+ float rad = spawner.Radius * math.sqrt(rng.NextFloat(0f, 1f));
+ var xform = baseXform;
+ xform.Position = origin + new float3(math.cos(ang) * rad, 0f, math.sin(ang) * rad);
+ ecb.SetComponent(node, xform);
+
+ // Round-robin the resource type (Aether / Ore / Biomass) over the prefab's baked node.
+ var rn = prefabNode;
+ rn.ResourceId = (byte)(ResourceId.Aether + (byte)(i % 3));
+ ecb.SetComponent(node, rn);
+ }
+ runtime.LastSpawnedCycle = cycle.CycleNumber;
+ }
+
+ // DESTROY edge: left Expedition — clear the whole field.
+ if (runtime.PrevPhase == CyclePhase.Expedition && cycle.Phase != CyclePhase.Expedition)
+ {
+ foreach (var (rn, e) in SystemAPI.Query>().WithEntityAccess())
+ ecb.DestroyEntity(e);
+ }
+
+ runtime.PrevPhase = cycle.Phase;
+ SystemAPI.SetComponent(cycleEntity, runtime);
+
+ ecb.Playback(state.EntityManager);
+ }
+ }
+}
diff --git a/Assets/_Project/Scripts/Server/Economy/ExpeditionFieldSystem.cs.meta b/Assets/_Project/Scripts/Server/Economy/ExpeditionFieldSystem.cs.meta
new file mode 100644
index 000000000..cbf23f296
--- /dev/null
+++ b/Assets/_Project/Scripts/Server/Economy/ExpeditionFieldSystem.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 9267d7809e68ea54caa55378f33e67f6
\ No newline at end of file
diff --git a/Assets/_Project/Scripts/Server/Economy/ResourceHarvestSystem.cs b/Assets/_Project/Scripts/Server/Economy/ResourceHarvestSystem.cs
new file mode 100644
index 000000000..e7ff1d46a
--- /dev/null
+++ b/Assets/_Project/Scripts/Server/Economy/ResourceHarvestSystem.cs
@@ -0,0 +1,134 @@
+using ProjectM.Simulation;
+using Unity.Burst;
+using Unity.Collections;
+using Unity.Entities;
+using Unity.Mathematics;
+using Unity.NetCode;
+using Unity.Transforms;
+
+namespace ProjectM.Server
+{
+ ///
+ /// Server-only resource harvest: sweeps each surviving projectile's this-tick travel segment against
+ /// resource-node ghosts and deposits of the node's
+ /// into the GLOBAL resource ledger (the CycleDirector's
+ /// buffer, resolved via — NEVER
+ /// GetSingleton<StorageEntry>, which would collide with the base storage container). Runs in the plain
+ /// server SimulationSystemGroup [UpdateAfter(PredictedSimulationSystemGroup)] — after
+ /// ProjectileDamageSystem has already consumed Health-target hits and range-expired projectiles, so this
+ /// only sees true survivors. The swept segment is reconstructed from
+ /// (written by ProjectileMoveSystem in the fixed-step group), so it is tunnelling-safe WITHOUT depending on
+ /// this plain group's variable-frame DeltaTime. A node hit by two projectiles in one tick deposits twice
+ /// but is destroyed exactly once. Relies on the asserted ~1000-unit base/expedition coordinate gap so a
+ /// base projectile can never geometrically reach an expedition node.
+ ///
+ [BurstCompile]
+ [WorldSystemFilter(WorldSystemFilterFlags.ServerSimulation)]
+ [UpdateInGroup(typeof(SimulationSystemGroup))]
+ [UpdateAfter(typeof(PredictedSimulationSystemGroup))]
+ public partial struct ResourceHarvestSystem : ISystem
+ {
+ const float k_ProjectileRadius = 0.2f;
+
+ [BurstCompile]
+ public void OnCreate(ref SystemState state)
+ {
+ state.RequireForUpdate();
+ state.RequireForUpdate();
+ state.RequireForUpdate();
+ }
+
+ [BurstCompile]
+ public void OnUpdate(ref SystemState state)
+ {
+ var ledgerEntity = SystemAPI.GetSingletonEntity();
+ var ledger = SystemAPI.GetBuffer(ledgerEntity);
+
+ // Snapshot all nodes once this tick.
+ var nodeEntities = new NativeList(Allocator.Temp);
+ var nodePos = new NativeList(Allocator.Temp);
+ var nodeRadius = new NativeList(Allocator.Temp);
+ var nodeRemaining = new NativeList(Allocator.Temp);
+ var nodeResource = new NativeList(Allocator.Temp);
+ var nodePerHit = new NativeList(Allocator.Temp);
+
+ foreach (var (xform, hr, node, e) in
+ SystemAPI.Query, RefRO, RefRO>().WithEntityAccess())
+ {
+ nodeEntities.Add(e);
+ nodePos.Add(xform.ValueRO.Position.xz);
+ nodeRadius.Add(hr.ValueRO.Value);
+ nodeRemaining.Add(node.ValueRO.Remaining);
+ nodeResource.Add(node.ValueRO.ResourceId);
+ nodePerHit.Add(node.ValueRO.HarvestPerHit);
+ }
+
+ var destroyed = new NativeArray(nodeEntities.Length, Allocator.Temp);
+ var ecb = new EntityCommandBuffer(Allocator.Temp);
+
+ foreach (var (xform, proj, projEntity) in
+ SystemAPI.Query, RefRO>().WithEntityAccess())
+ {
+ float3 cur = xform.ValueRO.Position;
+ float2 segEnd = cur.xz;
+ float2 segStart = segEnd - proj.ValueRO.Direction * proj.ValueRO.LastStep;
+ float2 seg = segEnd - segStart;
+ float segLenSq = math.lengthsq(seg);
+
+ int bestIdx = -1;
+ float bestT = float.MaxValue;
+ for (int i = 0; i < nodeEntities.Length; i++)
+ {
+ if (destroyed[i]) continue;
+ float2 tp = nodePos[i];
+ float t = segLenSq > 1e-8f ? math.saturate(math.dot(tp - segStart, seg) / segLenSq) : 0f;
+ float2 closest = segStart + t * seg;
+ float hitDist = nodeRadius[i] + k_ProjectileRadius;
+ if (math.distancesq(tp, closest) <= hitDist * hitDist && t < bestT)
+ {
+ bestT = t;
+ bestIdx = i;
+ }
+ }
+
+ if (bestIdx < 0)
+ continue;
+
+ int amount = (int)nodePerHit[bestIdx];
+ StorageMath.Deposit(ledger, nodeResource[bestIdx], amount);
+ int rem = nodeRemaining[bestIdx] - amount;
+ nodeRemaining[bestIdx] = rem;
+ ecb.DestroyEntity(projEntity);
+
+ if (rem <= 0)
+ {
+ if (!destroyed[bestIdx])
+ {
+ destroyed[bestIdx] = true;
+ ecb.DestroyEntity(nodeEntities[bestIdx]);
+ }
+ }
+ else
+ {
+ // Persist the decremented Remaining (replicated GhostField) so depletion carries across ticks.
+ SystemAPI.SetComponent(nodeEntities[bestIdx], new ResourceNode
+ {
+ ResourceId = nodeResource[bestIdx],
+ Remaining = rem,
+ HarvestPerHit = nodePerHit[bestIdx],
+ });
+ }
+ }
+
+ ecb.Playback(state.EntityManager);
+ ecb.Dispose();
+ destroyed.Dispose();
+ nodeEntities.Dispose();
+ nodePos.Dispose();
+ nodeRadius.Dispose();
+ nodeRemaining.Dispose();
+ nodeResource.Dispose();
+ nodePerHit.Dispose();
+ }
+ }
+}
diff --git a/Assets/_Project/Scripts/Server/Economy/ResourceHarvestSystem.cs.meta b/Assets/_Project/Scripts/Server/Economy/ResourceHarvestSystem.cs.meta
new file mode 100644
index 000000000..1a616c96a
--- /dev/null
+++ b/Assets/_Project/Scripts/Server/Economy/ResourceHarvestSystem.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 1e1ce72e524297a48a08085c68ff908e
\ No newline at end of file
diff --git a/Assets/_Project/Scripts/Server/HomeBase/SharedStorageSpawnSystem.cs b/Assets/_Project/Scripts/Server/HomeBase/SharedStorageSpawnSystem.cs
index 7d988bcf5..6f69c3212 100644
--- a/Assets/_Project/Scripts/Server/HomeBase/SharedStorageSpawnSystem.cs
+++ b/Assets/_Project/Scripts/Server/HomeBase/SharedStorageSpawnSystem.cs
@@ -43,6 +43,8 @@ namespace ProjectM.Server
var xform = SystemAPI.GetComponent(spawner.Prefab);
xform.Position = position;
ecb.SetComponent(container, xform);
+ // M6: scope the shared storage to the base region for ghost relevancy.
+ ecb.AddComponent(container, new RegionTag { Region = RegionId.Base });
}
// One-shot: remove the spawner so RequireForUpdate fails and the system idles.
diff --git a/Assets/_Project/Scripts/Server/World.meta b/Assets/_Project/Scripts/Server/World.meta
new file mode 100644
index 000000000..6066370dd
--- /dev/null
+++ b/Assets/_Project/Scripts/Server/World.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: ee7b17ccce78a094abf6f8008ecbef36
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/_Project/Scripts/Server/World/CycleDirectorSpawnSystem.cs b/Assets/_Project/Scripts/Server/World/CycleDirectorSpawnSystem.cs
new file mode 100644
index 000000000..dc33fc951
--- /dev/null
+++ b/Assets/_Project/Scripts/Server/World/CycleDirectorSpawnSystem.cs
@@ -0,0 +1,69 @@
+using ProjectM.Simulation;
+using Unity.Burst;
+using Unity.Collections;
+using Unity.Entities;
+using Unity.NetCode;
+using Unity.Transforms;
+
+namespace ProjectM.Server
+{
+ ///
+ /// Server-only, one-shot spawner for the GLOBAL cycle-director ghost (mirrors SharedStorageSpawnSystem,
+ /// but MINUS the RegionTag — the director must stay global so GhostRelevancy keeps it relevant to every
+ /// region). On its first update it reads the baked + NetworkTime,
+ /// instantiates the ghost, initializes (Expedition, cycle 1, PhaseEndTick =
+ /// now + ), adds the server-only , and
+ /// places it at the base center (preserving the prefab's baked LocalTransform scale — FromPosition would
+ /// reset the replicated Scale GhostField), then destroys the spawner so it idles.
+ ///
+ [BurstCompile]
+ [WorldSystemFilter(WorldSystemFilterFlags.ServerSimulation)]
+ public partial struct CycleDirectorSpawnSystem : ISystem
+ {
+ [BurstCompile]
+ public void OnCreate(ref SystemState state)
+ {
+ state.RequireForUpdate();
+ state.RequireForUpdate();
+ }
+
+ [BurstCompile]
+ public void OnUpdate(ref SystemState state)
+ {
+ var serverTick = SystemAPI.GetSingleton().ServerTick;
+ if (!serverTick.IsValid)
+ return;
+ uint now = serverTick.TickIndexForValidTick;
+
+ var spawnerEntity = SystemAPI.GetSingletonEntity();
+ var spawner = SystemAPI.GetComponent(spawnerEntity);
+
+ var ecb = new EntityCommandBuffer(Allocator.Temp);
+
+ if (spawner.Prefab != Entity.Null)
+ {
+ var director = ecb.Instantiate(spawner.Prefab);
+
+ // Place at the base center, preserving the prefab's baked scale/rotation.
+ var xform = SystemAPI.GetComponent(spawner.Prefab);
+ if (SystemAPI.TryGetSingleton(out var anchor))
+ xform.Position = BaseGridMath.PlotCenter(anchor);
+ ecb.SetComponent(director, xform);
+
+ // Override the baked CycleState with the real start tick; add server-only bookkeeping.
+ ecb.SetComponent(director, new CycleState
+ {
+ Phase = CyclePhase.Expedition,
+ CycleNumber = 1,
+ PhaseEndTick = TickUtil.NonZero(now + CyclePhase.ExpeditionTicks),
+ });
+ ecb.AddComponent(director, new CycleRuntime { DefendStartWave = 0 });
+ }
+
+ // One-shot: remove the spawner so RequireForUpdate fails and the system idles.
+ ecb.DestroyEntity(spawnerEntity);
+
+ ecb.Playback(state.EntityManager);
+ }
+ }
+}
diff --git a/Assets/_Project/Scripts/Server/World/CycleDirectorSpawnSystem.cs.meta b/Assets/_Project/Scripts/Server/World/CycleDirectorSpawnSystem.cs.meta
new file mode 100644
index 000000000..dbf764f18
--- /dev/null
+++ b/Assets/_Project/Scripts/Server/World/CycleDirectorSpawnSystem.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: bdfd3e27e8a3e924c93d6af962e0df05
\ No newline at end of file
diff --git a/Assets/_Project/Scripts/Server/World/CyclePhaseSystem.cs b/Assets/_Project/Scripts/Server/World/CyclePhaseSystem.cs
new file mode 100644
index 000000000..daf93473c
--- /dev/null
+++ b/Assets/_Project/Scripts/Server/World/CyclePhaseSystem.cs
@@ -0,0 +1,93 @@
+using ProjectM.Simulation;
+using Unity.Burst;
+using Unity.Entities;
+using Unity.NetCode;
+
+namespace ProjectM.Server
+{
+ ///
+ /// Server-authoritative macro-loop director for "The Aether Cycle": Expedition (timed) -> Defend
+ /// (wave-driven) -> Build (timed) -> next cycle. Maintains the singleton and gates
+ /// so the base-defense wave only spawns during Defend. Runs in the plain server
+ /// SimulationSystemGroup (NOT prediction) before . All timing is wrap-safe
+ /// NetworkTick math ( + ),
+ /// never raw uint compares. The CycleState/CycleRuntime live on the runtime-spawned CycleDirector ghost.
+ ///
+ [BurstCompile]
+ [WorldSystemFilter(WorldSystemFilterFlags.ServerSimulation)]
+ [UpdateInGroup(typeof(SimulationSystemGroup))]
+ [UpdateBefore(typeof(WaveSystem))]
+ public partial struct CyclePhaseSystem : ISystem
+ {
+ EntityQuery m_AliveHusks;
+
+ [BurstCompile]
+ public void OnCreate(ref SystemState state)
+ {
+ state.RequireForUpdate();
+ state.RequireForUpdate();
+ m_AliveHusks = state.GetEntityQuery(ComponentType.ReadOnly());
+ }
+
+ [BurstCompile]
+ public void OnUpdate(ref SystemState state)
+ {
+ var serverTick = SystemAPI.GetSingleton().ServerTick;
+ if (!serverTick.IsValid)
+ return;
+ uint now = serverTick.TickIndexForValidTick;
+
+ var cycleEntity = SystemAPI.GetSingletonEntity();
+
+ var cycle = SystemAPI.GetComponent(cycleEntity);
+ var runtime = SystemAPI.GetComponent(cycleEntity);
+
+ bool timedPhaseDue =
+ cycle.PhaseEndTick != 0 && !new NetworkTick(cycle.PhaseEndTick).IsNewerThan(serverTick);
+
+ switch (cycle.Phase)
+ {
+ case CyclePhase.Expedition:
+ if (timedPhaseDue)
+ {
+ cycle.Phase = CyclePhase.Defend;
+ cycle.PhaseEndTick = 0; // Defend is wave-driven, not timed.
+ runtime.DefendStartWave =
+ SystemAPI.TryGetSingleton(out var w) ? w.WaveNumber : 0;
+ }
+ break;
+
+ case CyclePhase.Defend:
+ if (DefendCleared(ref state, runtime.DefendStartWave))
+ {
+ cycle.Phase = CyclePhase.Build;
+ cycle.PhaseEndTick = TickUtil.NonZero(now + CyclePhase.BuildTicks);
+ }
+ break;
+
+ case CyclePhase.Build:
+ if (timedPhaseDue)
+ {
+ cycle.Phase = CyclePhase.Expedition;
+ cycle.CycleNumber += 1;
+ cycle.PhaseEndTick = TickUtil.NonZero(now + CyclePhase.ExpeditionTicks);
+ }
+ break;
+ }
+
+ SystemAPI.SetComponent(cycleEntity, cycle);
+ SystemAPI.SetComponent(cycleEntity, runtime);
+ }
+
+ // The Defend wave has run for this phase (WaveNumber advanced past the captured start), is fully
+ // spawned, and no Husks remain alive.
+ bool DefendCleared(ref SystemState state, int defendStartWave)
+ {
+ if (!SystemAPI.TryGetSingleton(out var wave))
+ return false;
+ return wave.WaveNumber > defendStartWave
+ && wave.RemainingToSpawn == 0
+ && m_AliveHusks.CalculateEntityCount() == 0;
+ }
+ }
+}
diff --git a/Assets/_Project/Scripts/Server/World/CyclePhaseSystem.cs.meta b/Assets/_Project/Scripts/Server/World/CyclePhaseSystem.cs.meta
new file mode 100644
index 000000000..963b88f00
--- /dev/null
+++ b/Assets/_Project/Scripts/Server/World/CyclePhaseSystem.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: c325c252dce9fba4a938d5c8db903042
\ No newline at end of file
diff --git a/Assets/_Project/Scripts/Server/World/ExpeditionGateSystem.cs b/Assets/_Project/Scripts/Server/World/ExpeditionGateSystem.cs
new file mode 100644
index 000000000..6b6aaa5e1
--- /dev/null
+++ b/Assets/_Project/Scripts/Server/World/ExpeditionGateSystem.cs
@@ -0,0 +1,85 @@
+using ProjectM.Simulation;
+using Unity.Burst;
+using Unity.Collections;
+using Unity.Entities;
+using Unity.Mathematics;
+using Unity.Transforms;
+
+namespace ProjectM.Server
+{
+ ///
+ /// Server-only walk-in gate transit: a player who walks within a gate's radius (and whose region matches the
+ /// gate's ) is transited to the gate's ToRegion at its ArrivalPos
+ /// (RegionTag flipped + LocalTransform teleported — GhostRelevancy re-scopes their ghosts, as in
+ /// RegionTransitSystem). Returning to the BASE during the Expedition phase expires the Expedition
+ /// timer so Defend starts early ("timer cap + early return"). Plain server SimulationSystemGroup
+ /// [UpdateAfter(CyclePhaseSystem)]. Arrival points are offset from the destination gate so a transited
+ /// player does not immediately re-trigger.
+ ///
+ [BurstCompile]
+ [WorldSystemFilter(WorldSystemFilterFlags.ServerSimulation)]
+ [UpdateInGroup(typeof(SimulationSystemGroup))]
+ [UpdateAfter(typeof(CyclePhaseSystem))]
+ public partial struct ExpeditionGateSystem : ISystem
+ {
+ [BurstCompile]
+ public void OnCreate(ref SystemState state)
+ {
+ state.RequireForUpdate();
+ }
+
+ [BurstCompile]
+ public void OnUpdate(ref SystemState state)
+ {
+ // Snapshot gates once.
+ var gateFrom = new NativeList(Allocator.Temp);
+ var gateTo = new NativeList(Allocator.Temp);
+ var gateRadiusSq = new NativeList(Allocator.Temp);
+ var gatePos = new NativeList(Allocator.Temp);
+ var gateArrival = new NativeList(Allocator.Temp);
+ foreach (var (gate, xform) in SystemAPI.Query, RefRO>())
+ {
+ gateFrom.Add(gate.ValueRO.FromRegion);
+ gateTo.Add(gate.ValueRO.ToRegion);
+ gateRadiusSq.Add(gate.ValueRO.Radius * gate.ValueRO.Radius);
+ gatePos.Add(xform.ValueRO.Position.xz);
+ gateArrival.Add(gate.ValueRO.ArrivalPos);
+ }
+
+ bool returnedToBase = false;
+ foreach (var (region, xform) in
+ SystemAPI.Query, RefRW>().WithAll())
+ {
+ byte r = region.ValueRO.Region;
+ float2 pp = xform.ValueRO.Position.xz;
+ for (int i = 0; i < gateFrom.Length; i++)
+ {
+ if (gateFrom[i] != r) continue;
+ if (math.distancesq(pp, gatePos[i]) > gateRadiusSq[i]) continue;
+ region.ValueRW.Region = gateTo[i];
+ xform.ValueRW.Position = gateArrival[i];
+ if (gateTo[i] == RegionId.Base)
+ returnedToBase = true;
+ break;
+ }
+ }
+
+ gateFrom.Dispose();
+ gateTo.Dispose();
+ gateRadiusSq.Dispose();
+ gatePos.Dispose();
+ gateArrival.Dispose();
+
+ // Early return: a player came back to base mid-Expedition -> expire the Expedition timer (-> Defend).
+ if (returnedToBase && SystemAPI.TryGetSingletonEntity(out var cycleEntity))
+ {
+ var cs = SystemAPI.GetComponent(cycleEntity);
+ if (cs.Phase == CyclePhase.Expedition)
+ {
+ cs.PhaseEndTick = 1; // CyclePhaseSystem sees timedPhaseDue next tick -> Defend
+ SystemAPI.SetComponent(cycleEntity, cs);
+ }
+ }
+ }
+ }
+}
diff --git a/Assets/_Project/Scripts/Server/World/ExpeditionGateSystem.cs.meta b/Assets/_Project/Scripts/Server/World/ExpeditionGateSystem.cs.meta
new file mode 100644
index 000000000..90ce049a2
--- /dev/null
+++ b/Assets/_Project/Scripts/Server/World/ExpeditionGateSystem.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 4292536f663eb5c4d92688f6c5bb0368
\ No newline at end of file
diff --git a/Assets/_Project/Scripts/Server/World/RegionRelevancySystem.cs b/Assets/_Project/Scripts/Server/World/RegionRelevancySystem.cs
new file mode 100644
index 000000000..fdfb90681
--- /dev/null
+++ b/Assets/_Project/Scripts/Server/World/RegionRelevancySystem.cs
@@ -0,0 +1,69 @@
+using ProjectM.Simulation;
+using Unity.Burst;
+using Unity.Collections;
+using Unity.Entities;
+using Unity.NetCode;
+
+namespace ProjectM.Server
+{
+ ///
+ /// Server-authoritative per-connection ghost relevancy for the base/expedition region split. Each tick:
+ /// build a connection -> region map from the region-tagged player ghosts, then mark every region-tagged
+ /// ghost IRRELEVANT to each connection whose player is in a DIFFERENT region. Uses
+ /// so untagged/global ghosts (e.g. the cycle director)
+ /// stay relevant to everyone for free — only cross-region ghosts are hidden. Runs in the
+ /// (before GhostSendSystem reads the set). The set holds
+ /// (connection, ghost) pairs for the CURRENT simulated tick only, so it is cleared and repopulated every
+ /// update. A connection with no spawned player yet is absent from the map and simply sees everything.
+ ///
+ [BurstCompile]
+ [WorldSystemFilter(WorldSystemFilterFlags.ServerSimulation)]
+ [UpdateInGroup(typeof(GhostSimulationSystemGroup))]
+ public partial struct RegionRelevancySystem : ISystem
+ {
+ [BurstCompile]
+ public void OnCreate(ref SystemState state)
+ {
+ state.RequireForUpdate();
+ }
+
+ [BurstCompile]
+ public void OnUpdate(ref SystemState state)
+ {
+ // Map each in-game connection (by NetworkId) to its player's region.
+ var connRegion = new NativeHashMap(8, Allocator.Temp);
+ foreach (var (owner, region) in
+ SystemAPI.Query, RefRO>().WithAll())
+ {
+ connRegion[owner.ValueRO.NetworkId] = region.ValueRO.Region;
+ }
+
+ ref var relevancy = ref SystemAPI.GetSingletonRW().ValueRW;
+ relevancy.GhostRelevancyMode = GhostRelevancyMode.SetIsIrrelevant;
+ var set = relevancy.GhostRelevancySet;
+ set.Clear();
+
+ if (!connRegion.IsEmpty)
+ {
+ var conns = connRegion.GetKeyValueArrays(Allocator.Temp);
+ foreach (var (ghost, region) in
+ SystemAPI.Query, RefRO>())
+ {
+ int ghostId = ghost.ValueRO.ghostId;
+ if (ghostId == 0)
+ continue; // ghost id not assigned yet this tick
+
+ byte ghostRegion = region.ValueRO.Region;
+ for (int i = 0; i < conns.Keys.Length; i++)
+ {
+ if (conns.Values[i] != ghostRegion)
+ set.Add(new RelevantGhostForConnection { Connection = conns.Keys[i], Ghost = ghostId }, 1);
+ }
+ }
+ conns.Dispose();
+ }
+
+ connRegion.Dispose();
+ }
+ }
+}
diff --git a/Assets/_Project/Scripts/Server/World/RegionRelevancySystem.cs.meta b/Assets/_Project/Scripts/Server/World/RegionRelevancySystem.cs.meta
new file mode 100644
index 000000000..09a586f05
--- /dev/null
+++ b/Assets/_Project/Scripts/Server/World/RegionRelevancySystem.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: db2f33e6ce7ce9346b3dfbcd5e562ed6
\ No newline at end of file
diff --git a/Assets/_Project/Scripts/Server/World/RegionTransitSystem.cs b/Assets/_Project/Scripts/Server/World/RegionTransitSystem.cs
new file mode 100644
index 000000000..e32069e92
--- /dev/null
+++ b/Assets/_Project/Scripts/Server/World/RegionTransitSystem.cs
@@ -0,0 +1,71 @@
+using ProjectM.Simulation;
+using Unity.Burst;
+using Unity.Collections;
+using Unity.Entities;
+using Unity.NetCode;
+using Unity.Transforms;
+
+namespace ProjectM.Server
+{
+ ///
+ /// Server-authoritative handler for RPCs. Resolves the sender's player
+ /// (via the source connection's -> ), flips its
+ /// to the requested region, and teleports it to that region's origin
+ /// (, centered on the base via ).
+ /// Runs in the default server SimulationSystemGroup (NOT the prediction loop) so the transit applies once;
+ /// the next snapshot reconciles the owner-predicted client and re-scopes
+ /// which region's ghosts the connection receives. Mirrors the RPC shape.
+ ///
+ [BurstCompile]
+ [WorldSystemFilter(WorldSystemFilterFlags.ServerSimulation)]
+ public partial struct RegionTransitSystem : ISystem
+ {
+ [BurstCompile]
+ public void OnCreate(ref SystemState state)
+ {
+ state.RequireForUpdate();
+
+ var builder = new EntityQueryBuilder(Allocator.Temp)
+ .WithAll();
+ state.RequireForUpdate(state.GetEntityQuery(builder));
+ }
+
+ [BurstCompile]
+ public void OnUpdate(ref SystemState state)
+ {
+ var baseCenter = BaseGridMath.PlotCenter(SystemAPI.GetSingleton());
+
+ // Map connection NetworkId -> player entity.
+ var playerByConn = new NativeHashMap(8, Allocator.Temp);
+ foreach (var (owner, entity) in
+ SystemAPI.Query>().WithAll().WithEntityAccess())
+ {
+ playerByConn[owner.ValueRO.NetworkId] = entity;
+ }
+
+ var ecb = new EntityCommandBuffer(Allocator.Temp);
+
+ foreach (var (request, receive, requestEntity) in
+ SystemAPI.Query, RefRO>().WithEntityAccess())
+ {
+ var connEntity = receive.ValueRO.SourceConnection;
+ if (SystemAPI.HasComponent(connEntity))
+ {
+ int connId = SystemAPI.GetComponent(connEntity).Value;
+ if (playerByConn.TryGetValue(connId, out var player))
+ {
+ byte target = request.ValueRO.TargetRegion;
+ SystemAPI.GetComponentRW(player).ValueRW.Region = target;
+ SystemAPI.GetComponentRW(player).ValueRW.Position =
+ RegionMath.RegionOrigin(target, baseCenter);
+ }
+ }
+
+ ecb.DestroyEntity(requestEntity);
+ }
+
+ ecb.Playback(state.EntityManager);
+ playerByConn.Dispose();
+ }
+ }
+}
diff --git a/Assets/_Project/Scripts/Server/World/RegionTransitSystem.cs.meta b/Assets/_Project/Scripts/Server/World/RegionTransitSystem.cs.meta
new file mode 100644
index 000000000..24e0f66ef
--- /dev/null
+++ b/Assets/_Project/Scripts/Server/World/RegionTransitSystem.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 44d6ce89f189d984c83cd213d86d4b02
\ No newline at end of file
diff --git a/Assets/_Project/Scripts/Simulation/Combat/Projectile.cs b/Assets/_Project/Scripts/Simulation/Combat/Projectile.cs
index 255d618e7..3d735ea4b 100644
--- a/Assets/_Project/Scripts/Simulation/Combat/Projectile.cs
+++ b/Assets/_Project/Scripts/Simulation/Combat/Projectile.cs
@@ -35,5 +35,9 @@ namespace ProjectM.Simulation
/// Integrated distance travelled (predicted on client + authoritative on server). Not replicated.
public float DistanceTravelled;
+
+ /// This tick's travel step (Speed*dt), written by ProjectileMoveSystem so a plain-group harvest
+ /// sweep is tunnelling-safe without depending on its own variable-frame clock. Server-local; not replicated.
+ public float LastStep;
}
}
diff --git a/Assets/_Project/Scripts/Simulation/Combat/ProjectileMoveSystem.cs b/Assets/_Project/Scripts/Simulation/Combat/ProjectileMoveSystem.cs
index a22525512..d6ce99b1d 100644
--- a/Assets/_Project/Scripts/Simulation/Combat/ProjectileMoveSystem.cs
+++ b/Assets/_Project/Scripts/Simulation/Combat/ProjectileMoveSystem.cs
@@ -32,6 +32,7 @@ namespace ProjectM.Simulation
.WithAll())
{
float step = projectile.ValueRO.Speed * dt;
+ projectile.ValueRW.LastStep = step;
float3 dir = new float3(projectile.ValueRO.Direction.x, 0f, projectile.ValueRO.Direction.y);
transform.ValueRW.Position += dir * step;
diff --git a/Assets/_Project/Scripts/Simulation/Economy.meta b/Assets/_Project/Scripts/Simulation/Economy.meta
new file mode 100644
index 000000000..0fcc83861
--- /dev/null
+++ b/Assets/_Project/Scripts/Simulation/Economy.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 655041f384f27064e82ddc6dec87ce86
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/_Project/Scripts/Simulation/Economy/ResourceComponents.cs b/Assets/_Project/Scripts/Simulation/Economy/ResourceComponents.cs
new file mode 100644
index 000000000..28f16d349
--- /dev/null
+++ b/Assets/_Project/Scripts/Simulation/Economy/ResourceComponents.cs
@@ -0,0 +1,17 @@
+using Unity.Entities;
+
+namespace ProjectM.Simulation
+{
+ ///
+ /// Tag marking the single GLOBAL shared-resource ledger — the entity whose [GhostField]
+ /// buffer holds harvested resources (Aether/ore/biomass) replicated to ALL
+ /// connections. It lives on the ownerless interpolated CycleDirector ghost, which carries NO
+ /// , so GhostRelevancy (SetIsIrrelevant) keeps it relevant to players in EVERY
+ /// region — base AND expedition — unlike the region-tagged base storage container (which relevancy hides
+ /// from expedition players). Server systems resolve the ledger via
+ /// GetSingletonEntity<ResourceLedger>() then GetBuffer<StorageEntry>() — NEVER
+ /// GetSingleton<StorageEntry> (the base container owns a second StorageEntry buffer, so a
+ /// buffer-typed singleton query would throw "multiple instances").
+ ///
+ public struct ResourceLedger : IComponentData { }
+}
diff --git a/Assets/_Project/Scripts/Simulation/Economy/ResourceComponents.cs.meta b/Assets/_Project/Scripts/Simulation/Economy/ResourceComponents.cs.meta
new file mode 100644
index 000000000..57793aafb
--- /dev/null
+++ b/Assets/_Project/Scripts/Simulation/Economy/ResourceComponents.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 78ea3121ee0b2db4992b1a2c5dd8d34b
\ No newline at end of file
diff --git a/Assets/_Project/Scripts/Simulation/Economy/ResourceFieldSpawner.cs b/Assets/_Project/Scripts/Simulation/Economy/ResourceFieldSpawner.cs
new file mode 100644
index 000000000..1bc3cba37
--- /dev/null
+++ b/Assets/_Project/Scripts/Simulation/Economy/ResourceFieldSpawner.cs
@@ -0,0 +1,21 @@
+using Unity.Entities;
+
+namespace ProjectM.Simulation
+{
+ ///
+ /// Baked singleton holding the resource-node ghost prefab + field shape. ExpeditionFieldSystem reads it to
+ /// scatter nodes within of the expedition region origin on each
+ /// Expedition phase entry (seeded by the cycle number). Mirrors .
+ ///
+ public struct ResourceFieldSpawner : IComponentData
+ {
+ /// Baked resource-node ghost prefab to instantiate.
+ public Entity Prefab;
+
+ /// Number of nodes to scatter per expedition.
+ public int Count;
+
+ /// Scatter radius (world units) around the expedition region origin.
+ public float Radius;
+ }
+}
diff --git a/Assets/_Project/Scripts/Simulation/Economy/ResourceFieldSpawner.cs.meta b/Assets/_Project/Scripts/Simulation/Economy/ResourceFieldSpawner.cs.meta
new file mode 100644
index 000000000..605943bed
--- /dev/null
+++ b/Assets/_Project/Scripts/Simulation/Economy/ResourceFieldSpawner.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 120cf21540ce86640b32921fa224b974
\ No newline at end of file
diff --git a/Assets/_Project/Scripts/Simulation/Economy/ResourceNode.cs b/Assets/_Project/Scripts/Simulation/Economy/ResourceNode.cs
new file mode 100644
index 000000000..34305a816
--- /dev/null
+++ b/Assets/_Project/Scripts/Simulation/Economy/ResourceNode.cs
@@ -0,0 +1,41 @@
+using Unity.Entities;
+using Unity.NetCode;
+
+namespace ProjectM.Simulation
+{
+ /// Resource-type ids for harvested materials (a byte, not an enum, per the cross-assembly enum-in-Burst hazard).
+ public static class ResourceId
+ {
+ /// Unused / empty sentinel (aligns with StorageMath's 0-itemId no-op).
+ public const byte None = 0;
+
+ /// Magic energy — powers abilities / charging.
+ public const byte Aether = 1;
+
+ /// Raw ore — structures / building.
+ public const byte Ore = 2;
+
+ /// Biomass — misc / crafting.
+ public const byte Biomass = 3;
+ }
+
+ ///
+ /// A harvestable resource node in the procedural expedition field — an ownerless INTERPOLATED ghost
+ /// (region-tagged Expedition) that clients see and shoot. The server-only ResourceHarvestSystem sweeps
+ /// projectiles against it; each hit deposits of into
+ /// the GLOBAL resource ledger and decrements ; the node despawns at <= 0.
+ /// ResourceId/Remaining are [GhostField] so clients can tint by type and (later) show depletion;
+ /// HarvestPerHit is baked, server-only.
+ ///
+ public struct ResourceNode : IComponentData
+ {
+ /// Which resource this node yields (see ).
+ [GhostField] public byte ResourceId;
+
+ /// Remaining resource units; the node despawns when this reaches 0.
+ [GhostField] public int Remaining;
+
+ /// Units yielded per projectile hit (baked; server-only).
+ public float HarvestPerHit;
+ }
+}
diff --git a/Assets/_Project/Scripts/Simulation/Economy/ResourceNode.cs.meta b/Assets/_Project/Scripts/Simulation/Economy/ResourceNode.cs.meta
new file mode 100644
index 000000000..ba762beea
--- /dev/null
+++ b/Assets/_Project/Scripts/Simulation/Economy/ResourceNode.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: d8504e2af2e9a694d9a7dc30c61cc69a
\ No newline at end of file
diff --git a/Assets/_Project/Scripts/Simulation/World.meta b/Assets/_Project/Scripts/Simulation/World.meta
new file mode 100644
index 000000000..b3d77f9c2
--- /dev/null
+++ b/Assets/_Project/Scripts/Simulation/World.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: ee67f9c921637c84b92bfae0edb3d3e9
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/_Project/Scripts/Simulation/World/CycleComponents.cs b/Assets/_Project/Scripts/Simulation/World/CycleComponents.cs
new file mode 100644
index 000000000..3cc6636a0
--- /dev/null
+++ b/Assets/_Project/Scripts/Simulation/World/CycleComponents.cs
@@ -0,0 +1,61 @@
+using Unity.Entities;
+using Unity.NetCode;
+
+namespace ProjectM.Simulation
+{
+ ///
+ /// Macro-loop state for "The Aether Cycle": which phase the run is in, the cycle number, and the server
+ /// tick the current (timed) phase ends. Server-authoritative, maintained by CyclePhaseSystem. Currently a
+ /// server-side singleton; the [GhostField]s below are inert until it is moved onto the runtime-spawned
+ /// CycleDirector ghost (when the client HUD is wired), at which point the same struct replicates unchanged.
+ /// The Defend phase is NOT timed — it ends when the base-defense wave is cleared — so PhaseEndTick is only
+ /// meaningful in Expedition/Build (0 during Defend).
+ ///
+ public struct CycleState : IComponentData
+ {
+ /// Current phase (see ).
+ [GhostField] public byte Phase;
+
+ /// 1-based cycle counter (increments when a new Expedition begins).
+ [GhostField] public int CycleNumber;
+
+ /// Server tick the current timed phase ends (Expedition/Build only; 0 in Defend).
+ [GhostField] public uint PhaseEndTick;
+ }
+
+ /// Phase constants for (a byte, not an enum, for trivial Burst/serialization).
+ public static class CyclePhase
+ {
+ /// Out in the procedural field gathering resources (timed).
+ public const byte Expedition = 0;
+
+ /// The base is under assault by a Husk wave (ends when the wave is cleared).
+ public const byte Defend = 1;
+
+ /// Calm at base: spend resources to build/upgrade (timed).
+ public const byte Build = 2;
+
+ /// Expedition phase duration in server ticks (SimulationTickRate = 60). Tunable; short for the M6 slice.
+ public const uint ExpeditionTicks = 3600; // ~60s cap (early return via the gate ends it sooner)
+
+ /// Build phase duration in server ticks.
+ public const uint BuildTicks = 1200; // ~20s
+ }
+
+ ///
+ /// Server-only bookkeeping for the cycle state machine that must NOT replicate (kept separate from the
+ /// replicated ). Records the wave number captured when the Defend phase began so
+ /// the director can detect "this Defend's wave has now been spawned and cleared".
+ ///
+ public struct CycleRuntime : IComponentData
+ {
+ /// WaveState.WaveNumber captured at the moment the current Defend phase started.
+ public int DefendStartWave;
+
+ /// Cycle phase from the previous tick — lets ExpeditionFieldSystem edge-detect entering/leaving Expedition.
+ public byte PrevPhase;
+
+ /// CycleNumber the expedition field was last seeded for (compared by int equality, never tick math).
+ public int LastSpawnedCycle;
+ }
+}
diff --git a/Assets/_Project/Scripts/Simulation/World/CycleComponents.cs.meta b/Assets/_Project/Scripts/Simulation/World/CycleComponents.cs.meta
new file mode 100644
index 000000000..81d52d7b1
--- /dev/null
+++ b/Assets/_Project/Scripts/Simulation/World/CycleComponents.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: ca714d222c4d2ed48aaaad7bbe6ec8fc
\ No newline at end of file
diff --git a/Assets/_Project/Scripts/Simulation/World/CycleDirectorSpawner.cs b/Assets/_Project/Scripts/Simulation/World/CycleDirectorSpawner.cs
new file mode 100644
index 000000000..7f742afac
--- /dev/null
+++ b/Assets/_Project/Scripts/Simulation/World/CycleDirectorSpawner.cs
@@ -0,0 +1,16 @@
+using Unity.Entities;
+
+namespace ProjectM.Simulation
+{
+ ///
+ /// Singleton baked into the gameplay subscene holding the cycle-director ghost prefab. A one-shot server
+ /// system (CycleDirectorSpawnSystem) instantiates the prefab — the GLOBAL
+ /// + shared resource-ledger ghost — exactly once, then destroys this singleton. Mirrors
+ /// . Carries no transform; only the prefab needs one.
+ ///
+ public struct CycleDirectorSpawner : IComponentData
+ {
+ /// Baked cycle-director ghost prefab to instantiate.
+ public Entity Prefab;
+ }
+}
diff --git a/Assets/_Project/Scripts/Simulation/World/CycleDirectorSpawner.cs.meta b/Assets/_Project/Scripts/Simulation/World/CycleDirectorSpawner.cs.meta
new file mode 100644
index 000000000..624a2af04
--- /dev/null
+++ b/Assets/_Project/Scripts/Simulation/World/CycleDirectorSpawner.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: e0fcb1bcbea82cc419d4f134c9589619
\ No newline at end of file
diff --git a/Assets/_Project/Scripts/Simulation/World/ExpeditionGate.cs b/Assets/_Project/Scripts/Simulation/World/ExpeditionGate.cs
new file mode 100644
index 000000000..a50cb085c
--- /dev/null
+++ b/Assets/_Project/Scripts/Simulation/World/ExpeditionGate.cs
@@ -0,0 +1,28 @@
+using Unity.Entities;
+using Unity.Mathematics;
+
+namespace ProjectM.Simulation
+{
+ ///
+ /// A walk-in travel gate between world regions. A baked entity (visible mesh + this component) at a fixed
+ /// position; the server ExpeditionGateSystem transits a player who walks within
+ /// and whose region matches to , placing them at
+ /// (offset from the destination gate so they do not immediately re-trigger).
+ /// Returning to the base during the Expedition phase also starts Defend early (the "timer cap + early
+ /// return" pacing).
+ ///
+ public struct ExpeditionGate : IComponentData
+ {
+ /// Region a player must currently be in for this gate to act on them (see ).
+ public byte FromRegion;
+
+ /// Region the player is transited to.
+ public byte ToRegion;
+
+ /// Planar (XZ) trigger radius in world units.
+ public float Radius;
+
+ /// World position the player arrives at in the destination region.
+ public float3 ArrivalPos;
+ }
+}
diff --git a/Assets/_Project/Scripts/Simulation/World/ExpeditionGate.cs.meta b/Assets/_Project/Scripts/Simulation/World/ExpeditionGate.cs.meta
new file mode 100644
index 000000000..b87506ae6
--- /dev/null
+++ b/Assets/_Project/Scripts/Simulation/World/ExpeditionGate.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: ed28d6b4a4f0b0844b851cecaadeb93f
\ No newline at end of file
diff --git a/Assets/_Project/Scripts/Simulation/World/RegionComponents.cs b/Assets/_Project/Scripts/Simulation/World/RegionComponents.cs
new file mode 100644
index 000000000..0b479b7a5
--- /dev/null
+++ b/Assets/_Project/Scripts/Simulation/World/RegionComponents.cs
@@ -0,0 +1,49 @@
+using Unity.Entities;
+using Unity.Mathematics;
+
+namespace ProjectM.Simulation
+{
+ ///
+ /// Identifies which world REGION an entity belongs to. M6 splits the single server world into two
+ /// spatial regions at a large coordinate offset — the persistent home and
+ /// the procedurally-arranged — and uses per-connection GhostRelevancy
+ /// to replicate each region only to the connections whose player is currently in it. Server-side only
+ /// (NOT a [GhostField]; the server makes all relevancy decisions). Added to players on spawn and to
+ /// every region-scoped ghost the server spawns. Untagged ghosts are global (relevant to everyone).
+ ///
+ public struct RegionTag : IComponentData
+ {
+ /// Region id (see ): 0 = base, 1 = expedition.
+ public byte Region;
+ }
+
+ /// Region ids for (a byte, not an enum, to keep server/Burst code trivial).
+ public static class RegionId
+ {
+ /// The persistent, shared home base.
+ public const byte Base = 0;
+
+ /// The procedural expedition field (offset far from the base on +X).
+ public const byte Expedition = 1;
+ }
+
+ ///
+ /// Deterministic mapping of a region id to its world-space origin. The base region keeps the existing
+ /// home-base coordinates; the expedition region lives at a large +X offset so the two never overlap in
+ /// the single shared PhysicsWorld. Pure (no RNG/wall-clock) — server-authoritative teleports and field
+ /// spawners resolve region positions through here.
+ ///
+ public static class RegionMath
+ {
+ /// World-space X offset of the expedition region from the base region.
+ public const float ExpeditionOffsetX = 1000f;
+
+ /// World-space origin of , given the base center (BaseGridMath.PlotCenter).
+ public static float3 RegionOrigin(byte region, float3 baseCenter)
+ {
+ return region == RegionId.Expedition
+ ? baseCenter + new float3(ExpeditionOffsetX, 0f, 0f)
+ : baseCenter;
+ }
+ }
+}
diff --git a/Assets/_Project/Scripts/Simulation/World/RegionComponents.cs.meta b/Assets/_Project/Scripts/Simulation/World/RegionComponents.cs.meta
new file mode 100644
index 000000000..cc2c1ee79
--- /dev/null
+++ b/Assets/_Project/Scripts/Simulation/World/RegionComponents.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 09694e58455f41b439cdf791c3c421d8
\ No newline at end of file
diff --git a/Assets/_Project/Scripts/Simulation/World/RegionTransitRequest.cs b/Assets/_Project/Scripts/Simulation/World/RegionTransitRequest.cs
new file mode 100644
index 000000000..a60848951
--- /dev/null
+++ b/Assets/_Project/Scripts/Simulation/World/RegionTransitRequest.cs
@@ -0,0 +1,17 @@
+using Unity.NetCode;
+
+namespace ProjectM.Simulation
+{
+ ///
+ /// Client -> server request to move the sender's player between world regions (base <-> expedition).
+ /// A one-off action, so an RPC (not a per-tick predicted input), mirroring .
+ /// TargetRegion is a byte (see ) to keep the generated serializer trivial. The
+ /// server teleports the sender's player to the region origin and flips its , which
+ /// re-scopes GhostRelevancy so the client gains the target region's ghosts and drops the old region's.
+ ///
+ public struct RegionTransitRequest : IRpcCommand
+ {
+ /// Destination region id (see ).
+ public byte TargetRegion;
+ }
+}
diff --git a/Assets/_Project/Scripts/Simulation/World/RegionTransitRequest.cs.meta b/Assets/_Project/Scripts/Simulation/World/RegionTransitRequest.cs.meta
new file mode 100644
index 000000000..7f65c13de
--- /dev/null
+++ b/Assets/_Project/Scripts/Simulation/World/RegionTransitRequest.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 32c76b46284ed1845a412ca030de2499
\ No newline at end of file
diff --git a/Assets/_Project/Subscenes/Gameplay.unity b/Assets/_Project/Subscenes/Gameplay.unity
index fc5ce4183..9bb12cc68 100644
--- a/Assets/_Project/Subscenes/Gameplay.unity
+++ b/Assets/_Project/Subscenes/Gameplay.unity
@@ -119,6 +119,160 @@ NavMeshSettings:
debug:
m_Flags: 0
m_NavMeshData: {fileID: 0}
+--- !u!1 &17637045
+GameObject:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ serializedVersion: 6
+ m_Component:
+ - component: {fileID: 17637047}
+ - component: {fileID: 17637046}
+ m_Layer: 0
+ m_Name: ResourceFieldSpawner
+ m_TagString: Untagged
+ m_Icon: {fileID: 0}
+ m_NavMeshLayer: 0
+ m_StaticEditorFlags: 0
+ m_IsActive: 1
+--- !u!114 &17637046
+MonoBehaviour:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 17637045}
+ m_Enabled: 1
+ m_EditorHideFlags: 0
+ m_Script: {fileID: 11500000, guid: e9e840f8b95266140b0ac5bd4e81391b, type: 3}
+ m_Name:
+ m_EditorClassIdentifier: ProjectM.Authoring::ProjectM.Authoring.ResourceFieldSpawnerAuthoring
+ NodePrefab: {fileID: 3885353946372160549, guid: 8565e5eb00679fb45b8b7dac1e2ae9f3, type: 3}
+ Count: 8
+ Radius: 12
+--- !u!4 &17637047
+Transform:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 17637045}
+ serializedVersion: 2
+ m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
+ m_LocalPosition: {x: 0, y: 0, z: 0}
+ m_LocalScale: {x: 1, y: 1, z: 1}
+ m_ConstrainProportionsScale: 0
+ m_Children: []
+ m_Father: {fileID: 0}
+ m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
+--- !u!1 &236770150
+GameObject:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ serializedVersion: 6
+ m_Component:
+ - component: {fileID: 236770154}
+ - component: {fileID: 236770153}
+ - component: {fileID: 236770152}
+ - component: {fileID: 236770151}
+ m_Layer: 0
+ m_Name: ReturnGate
+ m_TagString: Untagged
+ m_Icon: {fileID: 0}
+ m_NavMeshLayer: 0
+ m_StaticEditorFlags: 0
+ m_IsActive: 1
+--- !u!114 &236770151
+MonoBehaviour:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 236770150}
+ m_Enabled: 1
+ m_EditorHideFlags: 0
+ m_Script: {fileID: 11500000, guid: 22f744b59ad23834abe28fc09b661005, type: 3}
+ m_Name:
+ m_EditorClassIdentifier: ProjectM.Authoring::ProjectM.Authoring.ExpeditionGateAuthoring
+ From: 1
+ To: 0
+ Radius: 2.5
+ ArrivalPos: {x: 0, y: 1, z: 0}
+--- !u!23 &236770152
+MeshRenderer:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 236770150}
+ m_Enabled: 1
+ m_CastShadows: 1
+ m_ReceiveShadows: 1
+ m_DynamicOccludee: 1
+ m_StaticShadowCaster: 0
+ m_MotionVectors: 1
+ m_LightProbeUsage: 1
+ m_ReflectionProbeUsage: 1
+ m_RayTracingMode: 2
+ m_RayTraceProcedural: 0
+ m_RayTracingAccelStructBuildFlagsOverride: 0
+ m_RayTracingAccelStructBuildFlags: 1
+ m_SmallMeshCulling: 1
+ m_ForceMeshLod: -1
+ m_MeshLodSelectionBias: 0
+ m_RenderingLayerMask: 1
+ m_RendererPriority: 0
+ m_Materials:
+ - {fileID: 2100000, guid: 175e5bfb2ad02704caa3d1753aad499d, type: 2}
+ m_StaticBatchInfo:
+ firstSubMesh: 0
+ subMeshCount: 0
+ m_StaticBatchRoot: {fileID: 0}
+ m_ProbeAnchor: {fileID: 0}
+ m_LightProbeVolumeOverride: {fileID: 0}
+ m_ScaleInLightmap: 1
+ m_ReceiveGI: 1
+ m_PreserveUVs: 1
+ m_IgnoreNormalsForChartDetection: 0
+ m_ImportantGI: 0
+ m_StitchLightmapSeams: 1
+ m_SelectedEditorRenderState: 3
+ m_MinimumChartSize: 4
+ m_AutoUVMaxDistance: 0.5
+ m_AutoUVMaxAngle: 89
+ m_LightmapParameters: {fileID: 0}
+ m_GlobalIlluminationMeshLod: 0
+ m_SortingLayerID: 0
+ m_SortingLayer: 0
+ m_SortingOrder: 0
+ m_MaskInteraction: 0
+ m_AdditionalVertexStreams: {fileID: 0}
+--- !u!33 &236770153
+MeshFilter:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 236770150}
+ m_Mesh: {fileID: 10206, guid: 0000000000000000e000000000000000, type: 0}
+--- !u!4 &236770154
+Transform:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 236770150}
+ serializedVersion: 2
+ m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
+ m_LocalPosition: {x: 1000, y: 1, z: 8}
+ m_LocalScale: {x: 1.5, y: 2.5, z: 1.5}
+ m_ConstrainProportionsScale: 0
+ m_Children: []
+ m_Father: {fileID: 0}
+ m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!1 &409538537
GameObject:
m_ObjectHideFlags: 0
@@ -327,6 +481,113 @@ Transform:
m_Children: []
m_Father: {fileID: 0}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
+--- !u!1 &1192434514
+GameObject:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ serializedVersion: 6
+ m_Component:
+ - component: {fileID: 1192434518}
+ - component: {fileID: 1192434517}
+ - component: {fileID: 1192434516}
+ - component: {fileID: 1192434515}
+ m_Layer: 0
+ m_Name: BaseGate
+ m_TagString: Untagged
+ m_Icon: {fileID: 0}
+ m_NavMeshLayer: 0
+ m_StaticEditorFlags: 0
+ m_IsActive: 1
+--- !u!114 &1192434515
+MonoBehaviour:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 1192434514}
+ m_Enabled: 1
+ m_EditorHideFlags: 0
+ m_Script: {fileID: 11500000, guid: 22f744b59ad23834abe28fc09b661005, type: 3}
+ m_Name:
+ m_EditorClassIdentifier: ProjectM.Authoring::ProjectM.Authoring.ExpeditionGateAuthoring
+ From: 0
+ To: 1
+ Radius: 2.5
+ ArrivalPos: {x: 1000, y: 1, z: 0}
+--- !u!23 &1192434516
+MeshRenderer:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 1192434514}
+ m_Enabled: 1
+ m_CastShadows: 1
+ m_ReceiveShadows: 1
+ m_DynamicOccludee: 1
+ m_StaticShadowCaster: 0
+ m_MotionVectors: 1
+ m_LightProbeUsage: 1
+ m_ReflectionProbeUsage: 1
+ m_RayTracingMode: 2
+ m_RayTraceProcedural: 0
+ m_RayTracingAccelStructBuildFlagsOverride: 0
+ m_RayTracingAccelStructBuildFlags: 1
+ m_SmallMeshCulling: 1
+ m_ForceMeshLod: -1
+ m_MeshLodSelectionBias: 0
+ m_RenderingLayerMask: 1
+ m_RendererPriority: 0
+ m_Materials:
+ - {fileID: 2100000, guid: 175e5bfb2ad02704caa3d1753aad499d, type: 2}
+ m_StaticBatchInfo:
+ firstSubMesh: 0
+ subMeshCount: 0
+ m_StaticBatchRoot: {fileID: 0}
+ m_ProbeAnchor: {fileID: 0}
+ m_LightProbeVolumeOverride: {fileID: 0}
+ m_ScaleInLightmap: 1
+ m_ReceiveGI: 1
+ m_PreserveUVs: 1
+ m_IgnoreNormalsForChartDetection: 0
+ m_ImportantGI: 0
+ m_StitchLightmapSeams: 1
+ m_SelectedEditorRenderState: 3
+ m_MinimumChartSize: 4
+ m_AutoUVMaxDistance: 0.5
+ m_AutoUVMaxAngle: 89
+ m_LightmapParameters: {fileID: 0}
+ m_GlobalIlluminationMeshLod: 0
+ m_SortingLayerID: 0
+ m_SortingLayer: 0
+ m_SortingOrder: 0
+ m_MaskInteraction: 0
+ m_AdditionalVertexStreams: {fileID: 0}
+--- !u!33 &1192434517
+MeshFilter:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 1192434514}
+ m_Mesh: {fileID: 10206, guid: 0000000000000000e000000000000000, type: 0}
+--- !u!4 &1192434518
+Transform:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 1192434514}
+ serializedVersion: 2
+ m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
+ m_LocalPosition: {x: 0, y: 1, z: 8}
+ m_LocalScale: {x: 1.5, y: 2.5, z: 1.5}
+ m_ConstrainProportionsScale: 0
+ m_Children: []
+ m_Father: {fileID: 0}
+ m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!1 &1301940439
GameObject:
m_ObjectHideFlags: 0
@@ -843,6 +1104,51 @@ Transform:
m_Children: []
m_Father: {fileID: 0}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
+--- !u!1 &2038854530
+GameObject:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ serializedVersion: 6
+ m_Component:
+ - component: {fileID: 2038854532}
+ - component: {fileID: 2038854531}
+ m_Layer: 0
+ m_Name: CycleDirectorSpawner
+ m_TagString: Untagged
+ m_Icon: {fileID: 0}
+ m_NavMeshLayer: 0
+ m_StaticEditorFlags: 0
+ m_IsActive: 1
+--- !u!114 &2038854531
+MonoBehaviour:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 2038854530}
+ m_Enabled: 1
+ m_EditorHideFlags: 0
+ m_Script: {fileID: 11500000, guid: 1f4d2cb1e17d6a1429525674969dd3f0, type: 3}
+ m_Name:
+ m_EditorClassIdentifier: ProjectM.Authoring::ProjectM.Authoring.CycleDirectorSpawnerAuthoring
+ DirectorPrefab: {fileID: 3885353946372160549, guid: 529ca7203da40f5489e9e3040ed1fc22, type: 3}
+--- !u!4 &2038854532
+Transform:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 2038854530}
+ serializedVersion: 2
+ m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
+ m_LocalPosition: {x: 0, y: 0, z: 0}
+ m_LocalScale: {x: 1, y: 1, z: 1}
+ m_ConstrainProportionsScale: 0
+ m_Children: []
+ m_Father: {fileID: 0}
+ m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!1 &2143686865
GameObject:
m_ObjectHideFlags: 0
@@ -907,3 +1213,7 @@ SceneRoots:
- {fileID: 874705788}
- {fileID: 1930969067}
- {fileID: 1301940441}
+ - {fileID: 2038854532}
+ - {fileID: 17637047}
+ - {fileID: 1192434518}
+ - {fileID: 236770154}
diff --git a/CLAUDE.md b/CLAUDE.md
index 3f47645d8..5e1613a1c 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -126,6 +126,20 @@ The KBM/gamepad aim rework is [[DR-012_Aim_Controls_Cursor_Gamepad]] / [[2026-06
- **A static presentation bridge must reset on play-enter.** `AimPresentation.Scheme` (mirrors `PrototypeCameraRig`/`VFXConfig` statics) needs `[UnityEngine.RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)]` to reset — statics survive **fast-enter-playmode** domain reloads, and a stale value flashes the wrong cursor/reticle for the first frames (caught by the adversarial review).
- **Cursor/reticle = client `PresentationSystemGroup` `SystemBase` (`AimReticleSystem`) that OBSERVES, never mutates.** A flat world-space ground ring (primitive quad, `Sprites/Default` with a null-guard fallback, procedural ring texture) is the aim indicator for BOTH schemes — KBM at the cursor's ground-projection point, gamepad a fixed distance ahead along replicated `PlayerFacing`. The hardware cursor is **hidden while aiming + focused** (`Application.isFocused`-gated) and restored on focus-loss / `OnDestroy`. A radial **dead-zone** (`AimMath.PlanarAimFromRay` `deadZoneRadius`) holds facing when the cursor is over the character. **The KBM ground point is re-raycast INSIDE `AimReticleSystem`** (PresentationSystemGroup runs after the follow-cam's LateUpdate), not latched from the gather (`GhostInputSystemGroup`, before the move) — latching there drifts the ring a frame behind the cursor under the moving camera. Optional camera **aim look-ahead** (`PrototypeCameraRig.AimLeadDistance`, tunable) leads the framed point toward `PlayerFacing` (not the live cursor projection, to avoid a feedback loop). Headless validation: drive `DebugInputInjectionSystem` (now stamps `Scheme`) + force `AimPresentation.Scheme`; the **real cursor / live device-switch needs a focused Game view** (the unfocused editor can't inject mouse position).
+### Build gotchas (learned — M6 Aether Cycle core loop, 2026-06-03)
+
+The M6 core-loop slice (Expedition→Defend→Build) + the base/expedition world split. See [[DR-013_M6_Aether_Cycle_Region_Split]] / [[2026-06-03_M6_Aether_Cycle_CoreLoop]]. **Stages 0–1 done; 2–4 are the continuation.**
+
+- **Base/expedition split = coordinate-region + per-connection `GhostRelevancy`, NOT `SceneSystem` streaming** (supersedes DR-008's framing). One server world; the expedition lives at `base + (1000,0,0)`; a server `RegionRelevancySystem` in `GhostSimulationSystemGroup` (before `GhostSendSystem`) sets `GhostRelevancyMode.SetIsIrrelevant` and each tick marks region-tagged ghosts irrelevant to connections whose player is in a different region. **Use `SetIsIrrelevant` (not `SetIsRelevant`)** so untagged/global ghosts (the future cycle director) stay relevant to everyone for free — you only enumerate cross-region ghosts to hide. Verify the API on the installed Netcode (1.13.2) with `unity_reflect`: `GhostRelevancy` singleton has `GhostRelevancyMode` + `NativeParallelHashMap GhostRelevancySet`; `RelevantGhostForConnection{ int Connection; int Ghost }` (`Connection`=`NetworkId.Value`, `Ghost`=`GhostInstance.ghostId`). `RegionTag{byte Region}` is **server-only (NOT a `[GhostField]`)** — the server makes all relevancy decisions; the client just gains/loses ghosts. Reuses the runtime-ghost spawn path verbatim (no baked ghosts → no prespawn handshake), no async-load race; co-op drop-in is free.
+- **Region transit + cycle phase use the established byte-RPC + tick-safe + server-`SimulationSystemGroup` patterns.** `RegionTransitRequest{byte TargetRegion}` (resolve sender via `ReceiveRpcCommandRequest.SourceConnection`→`NetworkId`→`GhostOwner`, flip `RegionTag`, teleport `LocalTransform`). The macro loop is a server-only `CycleState` singleton ([GhostField]-pre-annotated for the later CycleDirector ghost) driven by `CyclePhaseSystem` (`[UpdateBefore(WaveSystem)]`); it **gates `WaveSystem`** with a one-line `if (TryGetSingleton(out var c) && c.Phase != CyclePhase.Defend) return;`. All phase timers are wrap-safe `NetworkTick` (`TickUtil.NonZero` + `IsNewerThan`), never raw `uint <`.
+- **Editing an existing `[BurstCompile]` ISystem's SystemAPI query set on an UNFOCUSED editor can leave a STALE Burst binary** (managed assembly recompiles with shifted source-gen query indices, Burst's async recompile doesn't finish) → runtime `InvalidOperationException: "… required component type was not declared in the EntityQuery"` thrown from an *unrelated* `GetSingleton` in that system. **Tell:** the Burst stack reports the *old* line number for the failing call. Same family as the M2 Burst-cache gotcha. **Workaround:** `Jobs ▸ Burst ▸ Enable Compilation` OFF for the session (verify `BurstCompiler.Options.EnableBurstCompilation==false`) — everything runs the fresh managed source-gen. **Permanent fix = restart Unity** to clear the cache, then re-enable Burst. Prefer a **focused** editor for Burst-affecting edits.
+- **Shared GLOBAL game-state (cycle phase, resource ledger, goal meter) rides an UNTAGGED ghost, never a region-tagged one** — `SetIsIrrelevant` hides a region-tagged ghost (e.g. the base storage) from players in the *other* region. The M6 resource **ledger** is a `StorageEntry` buffer on the global `CycleDirector` ghost, resolved via a distinct `ResourceLedger` tag — **never `GetSingleton`** (the base storage container owns a second `StorageEntry` buffer → "multiple instances" throw). Runtime-proven: the director stays relevant to an expedition player while the base storage despawns.
+- **A hit/area sweep that runs in the PLAIN `SimulationSystemGroup` must NOT use `SystemAPI.Time.DeltaTime`** — that group sees the variable *wall-frame* delta, not the fixed tick step, so a `cur - dir*Speed*dt` segment is wrong. Store the per-tick step on the projectile (`Projectile.LastStep`, written by `ProjectileMoveSystem` in the fixed-step predicted group) and reconstruct the swept segment as `cur - dir*LastStep` — tunnel-safe with zero dependence on the consuming system's clock. `ResourceHarvestSystem` runs `[UpdateAfter(PredictedSimulationSystemGroup)]` so it only sees projectiles that survived `ProjectileDamageSystem` (relies on the ~1000u base/expedition coordinate gap so a base shot can't reach a node). A node hit by N projectiles in one tick: deposit per hit but `ecb.DestroyEntity` **at-most-once** (destroyed-bitset + local Remaining copy — a double destroy throws at Playback); persist the decremented `[GhostField] Remaining` via `SetComponent` so depletion carries across ticks.
+- **New ghost prefab recipe (proven M6):** `manage_asset duplicate` UpgradePickup.prefab → `manage_prefabs modify_contents` (swap the authoring MonoBehaviour; **strip MeshFilter+MeshRenderer for an invisible state-holder**, keep them for a visible node). Wire the baked spawner into the gameplay subscene: `manage_scene load additive` → `set_active_scene Gameplay` → `manage_gameobject create` (+ `manage_components set_property` for the prefab ref, verify via `mcpforunity://scene/gameobject/{id}/component/...`) → `save` → `set_active_scene SampleScene` → `close_scene` (re-bakes on Play).
+- **Run an adversarial design-review Workflow (3 critics: netcode/relevancy, determinism/prediction, reuse/scope → synthesize) BEFORE coding a netcode-heavy slice** — for M6 Stage 2 it caught every one of the above pre-implementation (relevancy trap, singleton collision, dt-trap, double-destroy, lazy-create hazard).
+- **`manage_gameobject create` `component_properties` SILENTLY DROPS enum + Vector3 fields** (it set object-refs and simple scalars, but baked authoring enums/`Vector3` stayed at their C# defaults — two gates baked identical, one worked only by coincidence). **Always set those via a follow-up `manage_components set_property` (with a `properties` dict) and VERIFY through the `mcpforunity://scene/gameobject/{id}/component/{Type}` resource** (or, for a ghost, by reading the baked component in `execute_code` after Play). Same caveat applies to `manage_prefabs modify_contents` `component_properties`. Per-renderer color via `manage_material set_renderer_color` defaults to a runtime **PropertyBlock that does NOT persist into Play** — create a material asset (`manage_material create`) and `assign_material_to_renderer`, or use a prefab-stage assign, for colors that survive a domain reload.
+- **Walk-in region gates (M6 visibility pass):** a baked `ExpeditionGate{FromRegion,ToRegion,Radius,ArrivalPos}` entity (visible primitive, collider stripped so you pass through) + a server `ExpeditionGateSystem` (plain group, `[UpdateAfter(CyclePhaseSystem)]`) proximity-transits a player whose `RegionTag` matches `FromRegion` (flip RegionTag + teleport to `ArrivalPos`, offset from the destination gate so no re-trigger). Returning to base mid-Expedition expires the cycle timer → Defend ("timer cap + early return"). The expedition is a *place* = cosmetic ground/pillars in **SampleScene** at the +1000 offset (classic URP, like SyntyWorld), not the DOTS subscene; gameplay nodes/gates are the baked subscene entities.
+
## Bootstrap & worlds
- `ProjectM.Simulation.GameBootstrap : ClientServerBootstrap` → overrides `Initialize`, sets `AutoConnectPort = 7979` (in-editor auto-connect over IPC; set in M1 — was 0), calls `CreateDefaultClientServerWorlds()`. Entering Play Mode creates separate `ServerWorld` (`WorldFlags.GameServer`) and `ClientWorld` (`WorldFlags.GameClient`) — verified.
diff --git a/Docs/Vault/06_Roadmap/Milestones.md b/Docs/Vault/06_Roadmap/Milestones.md
index 26066f39a..13009ff8d 100644
--- a/Docs/Vault/06_Roadmap/Milestones.md
+++ b/Docs/Vault/06_Roadmap/Milestones.md
@@ -19,7 +19,7 @@ permalink: gamevault/06-roadmap/milestones
| **M5.5 — Game feel & identity** | Bridge "tech-demo → game": the **Husk** enemy (server AI, interpolated ghost), player death/respawn, combat juice (damage numbers/VFX/SFX/camera shake), a core HUD, and a sci-fi look pass — under the new fiction ([[Identity]], sci-fi frontier colony) | ✅ Done 2026-06-02 — runtime-validated on 6.4.7: Husks spawn(6)+replicate+chase+strike; death→respawn loop; HUD (health/cooldown/threat/downed); emissive dark-sci-fi look. EditMode **74/74**. ctx7-verified APIs. **Deepened same day:** auto-target on Husks, replicated respawn-invulnerability, and a `WaveSystem` threat director (escalating waves of 3 Husk variants — Grunt/Swarmer/Brute) replacing the flat sustain — runtime-validated (wave 1→2 escalation 4→6, distinct maxHP 30/15/80). [[DR-009_GameFeel_Identity_FirstBlood]], [[2026-06-02_GameFeel_Identity]], [[2026-06-02_GameFeel_Deepening]] |
| **— 2026-06-03 Visual & controls polish —** | Non-milestone polish layered on M5.5 (no mechanical rework): HDRP→URP art import + reusable converter; a cohesive **Synty** sci-fi colony world (cosmetic SampleScene GameObjects) + **GabrielAguiar** combat VFX; **KBM mouse-cursor aim + gamepad aim** with last-actuation device auto-switch (rides the existing `PlayerInput.Aim` ghost field). | ✅ Done 2026-06-03 — [[DR-010_Art_Import_URP_Conversion_Visual_Upgrade]], [[DR-011_Synty_World_VFX_Integration]], [[DR-012_Aim_Controls_Cursor_Gamepad]] |
| **— 2026-06-03 Pre-M6 cleanup —** | Loose-ends pass before M6: vault roadmap reconcile, Unity-template + orphaned-material removal, rate-limited turning, console/runtime health gate. | ✅ Done 2026-06-03 — [[2026-06-03_Pre_M6_Cleanup]] |
-| **M6 — Build/placement** | Server-authoritative grid build placement via RPC | ⬜ |
+| **M6 — The Aether Cycle (core loop)** | Reframed from "grid build placement" into the first vertical slice of the **core game loop**: Expedition (gather) → Defend (wave) → Build/Charge (spend), persistent base + procedural sorties, escalating toward a goal. Build placement is now Stage 3 of this milestone. | 🚧 In progress 2026-06-03 — **Stages 0–2 done + runtime-validated** on 6.4.7: **base/expedition split via coordinate-region + `GhostRelevancy`** (player transit despawns/re-grants the other region's ghosts; server==client); a **server phase-director** (Expedition→Defend→Build→Expedition auto-cycle, cycle 1→2, Husk `WaveSystem` only in Defend, escalation 4→6); and **resources + harvest** — a **global CycleDirector ghost** carrying the replicated `CycleState` + a shared resource **ledger** (relevant in every region, unlike the base storage), a procedural **expedition field** (8 resource-node ghosts seeded per cycle, region-scoped), and a tunnel-safe **harvest** sweep depositing into the ledger; client **HUD** shows phase + resource counts. Supersedes DR-008's "split requires streaming" framing. **Stages 3–4 (build placement/turret/ability-tiers, persistence/goal) = continuation.** — [[DR-013_M6_Aether_Cycle_Region_Split]], [[2026-06-03_M6_Aether_Cycle_CoreLoop]] |
| **M7 — Automation** | Self-running tick-based production chains (deterministic offline catch-up) | ⬜ |
Promote items from [[Backlog]] here when committed.
\ No newline at end of file
diff --git a/Docs/Vault/07_Sessions/2026/2026-06-03_M6_Aether_Cycle_CoreLoop.md b/Docs/Vault/07_Sessions/2026/2026-06-03_M6_Aether_Cycle_CoreLoop.md
new file mode 100644
index 000000000..2f14139c7
--- /dev/null
+++ b/Docs/Vault/07_Sessions/2026/2026-06-03_M6_Aether_Cycle_CoreLoop.md
@@ -0,0 +1,106 @@
+---
+date: 2026-06-03
+type: session
+tags: [session, m6, core-loop, netcode, ghost-relevancy, design]
+---
+
+# Session 2026-06-03 — M6 "The Aether Cycle": core-loop research, plan, and Stages 0–1
+
+## Goal
+
+Re-align M6 from the narrow "grid build placement via RPC" into **the first vertical slice of the actual core
+game loop**. Operator brief: a magic + sci-fi premise (start weak/amnesiac, a voice toward "THEM", harvest
+magical energy to build a base + charge abilities, procedural expeditions, periodic base-defense). Research
+good directions and **determine the core game loop in this slice**.
+
+## Done
+
+**Research + design (`/dots-dev`).** Three-stream research: codebase foundation map (M1–M5.5), co-op
+roguelite-base-defense loop precedent (Dome Keeper, Deep Rock, Hades, Risk of Rain, Vampire Survivors, Drill
+Core…), and magic+sci-fi narrative/progression (Warframe/Control/Bastion/Noita/Returnal). Synthesised **"The
+Aether Cycle"** — a Dome-Keeper two-phase loop: **Expedition** (gather, soft incursion timer) → **Defend** (a
+wave hits the base) → **Build/Charge** (spend resources on structures *and* ability tiers, one shared economy)
+→ repeat, escalating, toward a goal meter. Four operator decisions: persistent base + sorties; separate
+base/expedition scenes; multiple resource types; "The Awakening Engine" narrative lean. Plan approved.
+
+**Stage 0 — region-relevancy world split (the netcode crux; DONE + validated).**
+- `RegionTag{byte Region}` + `RegionId`/`RegionMath` (`Simulation/World/RegionComponents.cs`); transit RPC
+ (`RegionTransitRequest.cs`).
+- `RegionRelevancySystem` (Server, `GhostSimulationSystemGroup`): per-connection `GhostRelevancyMode.SetIsIrrelevant`,
+ hides cross-region ghosts each tick (global/untagged ghosts stay visible for free). API verified on Netcode
+ 1.13.2 via `unity_reflect`.
+- `RegionTransitSystem` (Server): RPC → resolve player via `SourceConnection`→`NetworkId`→`GhostOwner`, flip
+ `RegionTag`, teleport to region origin (expedition = base + (1000,0,0)).
+- Tagged players (`GoInGameServerSystem`), storage (`SharedStorageSpawnSystem`), Husks (`WaveSystem`) → Base.
+- **Validated headless:** transit→Expedition teleports the player to X=1000 and **despawns the base storage
+ ghost on the client** (relevancy); transit→Base **re-grants** it; server==client, console clean.
+
+**Stage 1 — macro phase director + wave gating (DONE + validated).**
+- `CycleState` ([GhostField] Phase/CycleNumber/PhaseEndTick — pre-annotated for the future CycleDirector ghost)
+ + `CyclePhase` consts + `CycleRuntime` (server-only) in `Simulation/World/CycleComponents.cs`.
+- `CyclePhaseSystem` (Server, `[UpdateBefore(WaveSystem)]`): Expedition (timed) → Defend (wave-driven) → Build
+ (timed) → next cycle, all wrap-safe `NetworkTick` math. Gates `WaveSystem` via a one-line `CycleState` check.
+- **Validated headless:** full **Expedition→Defend→Build→Expedition** auto-cycle; **CycleNumber 1→2**; wave
+ spawns **only** in Defend (husks=0 in Expedition); **escalation across cycles 4→6 Husks**; Husks tagged Base.
+
+**Stage 2 — resources + harvest + cycle replication/HUD (DONE + validated).** Adversarially design-reviewed
+first via a 3-critic + synthesis workflow (it caught real bugs pre-code: the base-storage-ledger relevancy
+trap, a `GetSingleton` "multiple instances" collision, the harvest variable-frame dt-trap, a
+node double-destroy, the CycleState lazy-create hazard). Split into **2a** (CycleDirector global ghost +
+migrate `CycleState` onto it + HUD phase readout) and **2b** (resources/nodes/harvest/ledger + HUD counts).
+- **2a:** `CycleDirector.prefab` (dup UpgradePickup, mesh stripped, no RegionTag → global) carries `CycleState`
+ + a `StorageEntry` ledger buffer + `ResourceLedger` tag; `CycleDirectorSpawnSystem` (one-shot) spawns it;
+ `CyclePhaseSystem` refactored atomically (`RequireForUpdate`, lazy-create deleted). **Validated:**
+ exactly one `CycleState`, replicates to client, HUD shows `"DEFEND CYCLE 1"`; the global director stays
+ relevant to an expedition player while the base storage despawns (the global-ledger thesis proven).
+- **2b:** `ResourceId` + `ResourceNode` ghost (`ResourceNode.prefab`, RegionTag{Expedition}); `ExpeditionFieldSystem`
+ edge-spawns/despawns a seeded field per cycle; `ResourceHarvestSystem` (plain group after the predicted group)
+ sweeps projectiles via the new `Projectile.LastStep` (written by `ProjectileMoveSystem`) → deposits to the
+ global ledger (`StorageMath` reused). **Validated headless:** 8 nodes seed in expedition (round-robin A/O/B,
+ invisible to the base player by relevancy); a hit deposits 5 Aether + decrements the node; full depletion
+ despawns the node; 5 same-tick hits deposit all + destroy once (double-destroy safe); **tunnelling sweep**
+ catches a node 3u past the projectile (point test would miss); field despawns on leaving Expedition; HUD reads
+ `"AETHER 30 ORE 5 BIO 0"`. Burst ON, console clean.
+
+**Visibility + playability pass (DONE + validated, operator-requested).** The systems worked but were invisible/
+unplayable in a real session (no in-world travel, void expedition, timer-only phases). Added: **walk-in gates**
+(`ExpeditionGate` baked entity + `ExpeditionGateSystem` server proximity transit — a glowing gate at the base
+deploys you to the expedition, a return gate brings you back) with **timer cap + early return** pacing
+(Expedition cap lengthened to ~60s; returning to base via the gate expires the timer → Defend); a **visible
+expedition place** (indigo ground plane + dark pillars at the expedition region in SampleScene); **bright glowing
+resource nodes** (M_Projectile material); and a **HUD clarity pass** (color-coded phase + countdown, BASE/ON-
+EXPEDITION location + gate hint, resource counts). **Validated via screenshots + headless:** stepping into the
+base gate deploys to the expedition (camera follows, 8 glowing nodes become visible by relevancy); stepping into
+the return gate comes back to base AND starts Defend early; HUD reads phase/countdown/cycle/resources/location.
+*Tooling gotcha:* `manage_gameobject create` `component_properties` silently dropped the enum/Vector3 fields
+(both gates baked with authoring defaults — the BaseGate worked only by coincidence); fixed with
+`manage_components set_property` + verified via the `mcpforunity://scene/gameobject/{id}/component/...` resource.
+
+See [[DR-013_M6_Aether_Cycle_Region_Split]] for the full architecture + validated evidence.
+
+## Decisions
+
+- [[DR-013_M6_Aether_Cycle_Region_Split]] — M6 = "The Aether Cycle"; base/expedition split via coordinate-region
+ + `GhostRelevancy` (supersedes DR-008's streaming framing); server-authoritative phase director gating the
+ Husk wave. Region-relevancy + phase-machine implemented and runtime-validated.
+
+## Open / deferred (the Stage 3–4 continuation)
+
+- ✅ **Stages 2a/2b done** (see above): client HUD + `CycleState` replication on the global CycleDirector ghost;
+ multi-type resources + procedural harvest field + global ledger + HUD resource counts. Optional hardening:
+ extract the harvest sweep into a pure `HarvestMath` + EditMode tunnelling/reproducibility tests (currently
+ runtime-validated, consistent with how `ProjectileDamageSystem`'s sweep is covered).
+- **Stage 3 — Build placement + turret + ability tiers:** `BuildPlaceRequest` RPC + occupancy + `BuildPlacementMath`
+ (unit-tested) + grid-snap preview; `Turret`+`TurretFireSystem` (auto-defend, reuse projectile path);
+ `AbilityUpgradeRequest` spending the ledger. (The original M6 build slice.)
+- **Stage 4 — Persistence + goal meter:** host JSON save/restore (replayed through build/upgrade paths — new
+ scope vs. DR-008); `GoalProgress` ghost ticked per cycle.
+- **Fiction reconciliation:** adopt Aether/Awakening-Engine naming into [[Identity]] (operator sign-off).
+- **Burst re-enable + editor restart:** Burst is currently **disabled** for the session (workaround for a stale
+ Burst-cache exception when editing `WaveSystem` on an unfocused editor — see DR-013 gotcha). Restart Unity to
+ clear the cache, then re-enable `Jobs ▸ Burst ▸ Enable Compilation` to restore full performance.
+
+## Next
+
+Checkpoint for operator feedback on the working core-loop skeleton, then continue Stage 2 (resources + harvest)
+— the gather half of the economy — followed by build placement (Stage 3) and persistence/goal (Stage 4).
diff --git a/Docs/Vault/07_Sessions/_Decisions/DR-013_M6_Aether_Cycle_Region_Split.md b/Docs/Vault/07_Sessions/_Decisions/DR-013_M6_Aether_Cycle_Region_Split.md
new file mode 100644
index 000000000..ee63a0b25
--- /dev/null
+++ b/Docs/Vault/07_Sessions/_Decisions/DR-013_M6_Aether_Cycle_Region_Split.md
@@ -0,0 +1,73 @@
+---
+id: DR-013
+title: M6 — "The Aether Cycle" core loop + base/expedition region split via GhostRelevancy
+status: accepted
+date: 2026-06-03
+tags:
+- decision
+- netcode
+- ghost-relevancy
+- world-architecture
+- core-loop
+- m6
+permalink: gamevault/07-sessions/decisions/dr-013-m6-aether-cycle-region-split
+---
+
+# DR-013 — M6 "The Aether Cycle" core loop + base/expedition region split
+
+## Context
+
+M1–M5.5 + polish delivered a systems-complete prototype (predicted player, data-driven combat, co-op,
+physics/CC, home-base grid + shared storage, escalating Husk `WaveSystem`, death/respawn, VFX/HUD, Synty
+world, KBM/gamepad aim) — but with **no game loop**. The roadmap scoped M6 narrowly as "server-authoritative
+grid build placement via RPC."
+
+A `/dots-dev` research + design pass (Dome Keeper, Deep Rock, Hades, Risk of Rain, Vampire Survivors, Noita,
+Warframe/Control/Bastion) reframed M6 as **the first vertical slice of the actual core game loop** — *The
+Aether Cycle* — reusing the ~70% of loop systems that already exist. The operator chose four directions:
+**persistent base + procedural sorties** (death = respawn, not wipe); **separate base/expedition scenes**;
+**multiple resource types** (Aether/ore/biomass); narrative lean **"The Awakening Engine"** (base = recharging
+hibernation-pod, Aether restores memory+power, a guiding voice toward "THEM"). Full plan: see
+[[2026-06-03_M6_Aether_Cycle_CoreLoop]].
+
+The core loop is a **Dome-Keeper two-phase rhythm**: **Expedition** (gather resources from a procedural field,
+soft incursion timer) → **Defend** (a wave assaults the base; you + built structures hold) → **Build/Charge**
+(spend resources to place/upgrade structures *and* ability tiers — one shared economy) → repeat, escalating,
+with a long-arc goal meter toward THEM. Mechanically this is pure fulfilment of the locked [[Pillars]] (action
+ARPG + co-op base + automation + *persistent base + instanced/procedural expeditions*) — **no pillar changes**;
+only the fiction skin evolves (Aether/Awakening-Engine vs. [[Identity]]'s industrial-colony/Blight framing),
+which is reconcilable and tracked as a follow-up, not a mechanical rework.
+
+## Decision
+
+**The milestone is staged to de-risk the netcode world-split FIRST. Stages 0–1 are implemented + runtime-validated; Stages 2–4 are scoped for the continuation.**
+
+1. **Base/expedition split = ONE server world, two spatial REGIONS at a coordinate offset, scoped per-connection by `GhostRelevancy` — NOT `SceneSystem` streaming.** This **supersedes DR-008's framing** that the split requires Option-C async subscene streaming (which DR-008 deferred). `RegionTag { byte Region }` (server-only, NOT a `[GhostField]`; `RegionId.Base=0` / `Expedition=1`); `RegionMath.RegionOrigin` puts the expedition at `baseCenter + (1000,0,0)`. A server `RegionRelevancySystem` (in `GhostSimulationSystemGroup`, before `GhostSendSystem`) sets `GhostRelevancyMode.SetIsIrrelevant` and, each tick, marks every region-tagged ghost **irrelevant to each connection whose player is in a different region**. `SetIsIrrelevant` (not `SetIsRelevant`) was chosen so **untagged/global ghosts stay relevant to everyone for free** — only cross-region ghosts are hidden. Verified API shape on the installed Netcode **1.13.2** via `unity_reflect` (the 1.10 published docs were closest): `GhostRelevancy` singleton with `GhostRelevancyMode` + `NativeParallelHashMap GhostRelevancySet`; `RelevantGhostForConnection { int Connection; int Ghost }` (`Connection` = `NetworkId.Value`, `Ghost` = `GhostInstance.ghostId`).
+
+2. **Region transit = `RegionTransitRequest { byte TargetRegion }` `IRpcCommand`** (mirrors the `StorageOpRequest` recipe — byte code, plain blittable, applied in the plain server `SimulationSystemGroup`, NOT the predicted loop). `RegionTransitSystem` resolves the sender's player via `ReceiveRpcCommandRequest.SourceConnection` → `NetworkId` → `GhostOwner`, flips its `RegionTag`, and teleports its `LocalTransform.Position` to the region origin. Players are tagged `RegionTag{Base}` on spawn (`GoInGameServerSystem`); the shared storage ghost (`SharedStorageSpawnSystem`) and Husks (`WaveSystem`) are tagged `RegionTag{Base}`.
+
+3. **Macro-loop director = a server-authoritative state machine.** `CycleState { [GhostField] byte Phase; [GhostField] int CycleNumber; [GhostField] uint PhaseEndTick }` (+ `CyclePhase` byte consts Expedition/Defend/Build) — currently a **server-only singleton**, pre-annotated with `[GhostField]`s so it drops onto the runtime-spawned CycleDirector ghost unchanged when the client HUD is wired. `CyclePhaseSystem` (plain server `SimulationSystemGroup`, `[UpdateBefore(WaveSystem)]`) advances **Expedition (timed) → Defend (wave-driven) → Build (timed) → next cycle**, all on wrap-safe `NetworkTick` math (`TickUtil.NonZero` + `IsNewerThan`, never raw `uint <`). It **gates `WaveSystem`**: a one-line early-out (`if (TryGetSingleton(out var c) && c.Phase != CyclePhase.Defend) return;`) so the base-defense wave only spawns during Defend. Defend ends when the wave has run for this phase (`WaveState.WaveNumber > DefendStartWave`), is fully spawned, and no Husks remain (`CycleRuntime.DefendStartWave` is server-only bookkeeping, kept off the replicated struct).
+
+4. **No new asmdefs.** New code under `…/World/` in Simulation (`RegionComponents`, `RegionTransitRequest`, `CycleComponents`) and Server (`RegionRelevancySystem`, `RegionTransitSystem`, `CyclePhaseSystem`); two one-line edits each to `GoInGameServerSystem` (tag player Base) and `WaveSystem` (Defend gate + tag Husk Base). The Server asmdef already references `Unity.NetCode` → `GhostRelevancy`/`GhostInstance` reachable with no asmdef edit.
+
+## Consequences
+
+- **Validated at runtime on 6.4.7 (single in-editor client), headless via `execute_code`:**
+ - **Stage 0 (relevancy):** player spawns `RegionTag{Base}`; sends `RegionTransitRequest{Expedition}` → server player teleports to X=1000, region flips, **the base-region storage ghost despawns on the client** (`clientStorageGhosts` 1→0); transit back → storage **re-granted** (0→1), server==client position (no desync). Clean console (only the known tick-batching warning).
+ - **Stage 1 (cycle):** full loop **Expedition → Defend → Build → Expedition** auto-advances on timers; **CycleNumber increments 1→2**; the wave spawns **only** in Defend (husks=0 in Expedition); **escalation persists across cycles** (wave 1 = 4 Husks → wave 2 = 6); Husks carry `RegionTag{Base}`.
+- **Delivers the "instanced/procedural expeditions" pillar without the streaming machinery DR-008 deferred** — region-relevancy reuses the existing runtime-ghost spawn path verbatim (no baked/prespawned ghosts → no prespawn section-ack/CRC handshake), and relevancy is a per-tick server-only write with no async-load race. Co-op drop-in is trivial (a connection without a spawned player is simply absent from the map and sees everything).
+- **Foundation for Stages 2–4** (the continuation): resources + harvest (multi-type, into the generalised `StorageEntry` ledger), build placement + turret + ability tiers (the original M6 RPC), persistence (host JSON, new scope vs. DR-008), goal meter — plus wiring the deferred **client HUD + cycle-state replication** (move `CycleState` onto a runtime-spawned CycleDirector ghost; the `[GhostField]`s are already in place).
+
+## Open / deferred
+
+- **Client HUD + `CycleState` replication** — deferred from Stage 1; bundle with the Stage 2 HUD (resource ledger readout). Needs the CycleDirector ghost prefab (duplicate `UpgradePickup.prefab`) + a baked spawner.
+- **Stages 2–4** — resources/harvest, build placement/turret/ability-tiers, persistence/goal. See the plan in the session log.
+- **Fiction reconciliation** — adopt the Awakening-Engine/Aether naming into [[Identity]] (currently industrial-colony/Blight); operator sign-off before rewriting the Identity doc. Light-touch in M6.
+- **Disk persistence** — the persistent-base run model now requires the save/load DR-008 deferred (Stage 4 = new scope).
+- **Teleport fidelity** — transit lands the CC character near (not exactly on) the region origin (collide-and-slide settling); the real game will land players on a designated pad/ring slot.
+
+## Build gotcha recorded this session
+
+- **Editing an existing `[BurstCompile]` ISystem's SystemAPI query set on an UNFOCUSED editor can leave a stale Burst binary** while the managed assembly recompiles with shifted source-gen query indices → a runtime `InvalidOperationException: "… required component type was not declared in the EntityQuery"` from an *unrelated* `GetSingleton` in that system (the Burst stack reports the *old* line number — the tell). Root cause: Burst's async recompile doesn't complete on an unfocused editor (same family as the [[CLAUDE]] M2 Burst-cache gotcha). **Workaround used:** disable Burst for the session (`Jobs ▸ Burst ▸ Enable Compilation`; verified `BurstCompiler.Options.EnableBurstCompilation == false`) so every system runs the fresh managed source-gen. **Permanent fix = restart Unity** (clears the Burst cache) then re-enable Burst. Prefer a **focused** editor for Burst-affecting edits.
+
+Mirrors the server-authoritative + deterministic + co-op pillars from [[Pillars]]; supersedes the streaming framing in [[DR-008_M5_HomeBase_BaseLayer_Storage]]; reuses the byte-RPC + runtime-ghost + tick-safe patterns from [[DR-008_M5_HomeBase_BaseLayer_Storage]] / [[DR-009_GameFeel_Identity_FirstBlood]].