Core Game Loop Additions

This commit is contained in:
2026-06-03 22:41:27 -07:00
parent 79ff06a7df
commit 8e9b4412ce
70 changed files with 3084 additions and 2 deletions
+575
View File
@@ -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}
Binary file not shown.
+117
View File
@@ -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:
Binary file not shown.
+117
View File
@@ -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:
Binary file not shown.
+117
View File
@@ -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:
@@ -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
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 6afcc12e55743ac45b265795f24238dd
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 2100000
userData:
assetBundleName:
assetBundleVariant:
@@ -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
@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 529ca7203da40f5489e9e3040ed1fc22
PrefabImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
+149
View File
@@ -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
@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 8565e5eb00679fb45b8b7dac1e2ae9f3
PrefabImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: c81ee1ba775463a4d940bb397a01f4e6
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,39 @@
using ProjectM.Simulation;
using Unity.Entities;
using UnityEngine;
namespace ProjectM.Authoring
{
/// <summary>
/// Authoring for the baked <see cref="ResourceFieldSpawner"/> 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.
/// </summary>
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<ResourceFieldSpawnerAuthoring>
{
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,
});
}
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: e9e840f8b95266140b0ac5bd4e81391b
@@ -0,0 +1,45 @@
using ProjectM.Simulation;
using Unity.Entities;
using UnityEngine;
namespace ProjectM.Authoring
{
/// <summary>
/// Authoring for a resource-node ghost prefab (ownerless interpolated — duplicate from UpgradePickup.prefab
/// so the GhostAuthoringComponent comes free). Bakes <see cref="ResourceNode"/> + <see cref="HitRadius"/>
/// (reused for the harvest hit test) + <see cref="RegionTag"/>{Expedition} so GhostRelevancy scopes the node
/// to expedition players. The field spawner overrides ResourceId (round-robin) and Position per instance.
/// </summary>
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<ResourceNodeAuthoring>
{
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 });
}
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 174563c586d9a0f4bb84cca191f4a1f0
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 4b2e77367c16a9a41943145e582954d1
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,34 @@
using ProjectM.Simulation;
using Unity.Entities;
using UnityEngine;
namespace ProjectM.Authoring
{
/// <summary>
/// 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 (<see cref="CycleState"/>) and the shared resource ledger
/// (a <see cref="StorageEntry"/> buffer marked by <see cref="ResourceLedger"/>). It is GLOBAL — it must
/// carry NO <see cref="RegionTag"/> 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.
/// </summary>
public class CycleDirectorAuthoring : MonoBehaviour
{
private class CycleDirectorBaker : Baker<CycleDirectorAuthoring>
{
public override void Bake(CycleDirectorAuthoring authoring)
{
var entity = GetEntity(authoring, TransformUsageFlags.Dynamic);
AddComponent(entity, new CycleState
{
Phase = CyclePhase.Expedition,
CycleNumber = 1,
PhaseEndTick = 0u,
});
AddComponent<ResourceLedger>(entity);
AddBuffer<StorageEntry>(entity);
}
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 843fd0d567f48ad46840dcce0ce84bbc
@@ -0,0 +1,31 @@
using ProjectM.Simulation;
using Unity.Entities;
using UnityEngine;
namespace ProjectM.Authoring
{
/// <summary>
/// Authoring for the baked <see cref="CycleDirectorSpawner"/> 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.
/// </summary>
public class CycleDirectorSpawnerAuthoring : MonoBehaviour
{
[Tooltip("Cycle-director ghost prefab. Must carry CycleDirectorAuthoring + a GhostAuthoringComponent (ownerless, interpolated).")]
public GameObject DirectorPrefab;
private class CycleDirectorSpawnerBaker : Baker<CycleDirectorSpawnerAuthoring>
{
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,
});
}
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 1f4d2cb1e17d6a1429525674969dd3f0
@@ -0,0 +1,45 @@
using ProjectM.Simulation;
using Unity.Entities;
using Unity.Mathematics;
using UnityEngine;
namespace ProjectM.Authoring
{
/// <summary>
/// Authoring for a walk-in <see cref="ExpeditionGate"/>. 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).
/// </summary>
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<ExpeditionGateAuthoring>
{
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),
});
}
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 22f744b59ad23834abe28fc09b661005
@@ -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<NetworkTime>(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<CycleState>(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<ResourceLedger>(out var ledgerE))
{
var buf = SystemAPI.GetBuffer<StorageEntry>(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 "";
}
}
}
}
@@ -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<CycleState>(out var cycle) && cycle.Phase != CyclePhase.Defend)
return;
var director = SystemAPI.GetSingleton<WaveDirector>();
var directorEntity = SystemAPI.GetSingletonEntity<WaveDirector>();
@@ -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();
@@ -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 });
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: c169eff521b1ff748b598a1b1a895196
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -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
{
/// <summary>
/// Server-only procedural expedition-field manager. Edge-triggered off the cycle phase (via the server-only
/// <see cref="CycleRuntime.PrevPhase"/>): on ENTERING Expedition for a not-yet-seeded cycle it scatters
/// <see cref="ResourceFieldSpawner.Count"/> resource-node ghosts (seeded by CycleNumber via
/// <see cref="Unity.Mathematics.Random"/>) around the expedition region origin, each
/// <see cref="RegionTag"/>{Expedition}; on LEAVING Expedition it destroys every node. Runs in the plain
/// server SimulationSystemGroup <c>[UpdateAfter(CyclePhaseSystem)]</c> 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).
/// </summary>
[BurstCompile]
[WorldSystemFilter(WorldSystemFilterFlags.ServerSimulation)]
[UpdateInGroup(typeof(SimulationSystemGroup))]
[UpdateAfter(typeof(CyclePhaseSystem))]
public partial struct ExpeditionFieldSystem : ISystem
{
[BurstCompile]
public void OnCreate(ref SystemState state)
{
state.RequireForUpdate<ResourceFieldSpawner>();
state.RequireForUpdate<CycleState>();
}
[BurstCompile]
public void OnUpdate(ref SystemState state)
{
var cycleEntity = SystemAPI.GetSingletonEntity<CycleState>();
var cycle = SystemAPI.GetComponent<CycleState>(cycleEntity);
var runtime = SystemAPI.GetComponent<CycleRuntime>(cycleEntity);
var spawner = SystemAPI.GetSingleton<ResourceFieldSpawner>();
float3 baseCenter = new float3(0f, 1f, 0f);
if (SystemAPI.TryGetSingleton<BaseAnchor>(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<LocalTransform>(spawner.Prefab);
var prefabNode = SystemAPI.GetComponent<ResourceNode>(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<RefRO<ResourceNode>>().WithEntityAccess())
ecb.DestroyEntity(e);
}
runtime.PrevPhase = cycle.Phase;
SystemAPI.SetComponent(cycleEntity, runtime);
ecb.Playback(state.EntityManager);
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 9267d7809e68ea54caa55378f33e67f6
@@ -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
{
/// <summary>
/// Server-only resource harvest: sweeps each surviving projectile's this-tick travel segment against
/// resource-node ghosts and deposits <see cref="ResourceNode.HarvestPerHit"/> of the node's
/// <see cref="ResourceNode.ResourceId"/> into the GLOBAL resource ledger (the CycleDirector's
/// <see cref="StorageEntry"/> buffer, resolved via <see cref="ResourceLedger"/> — NEVER
/// GetSingleton&lt;StorageEntry&gt;, which would collide with the base storage container). Runs in the plain
/// server SimulationSystemGroup <c>[UpdateAfter(PredictedSimulationSystemGroup)]</c> — after
/// ProjectileDamageSystem has already consumed Health-target hits and range-expired projectiles, so this
/// only sees true survivors. The swept segment is reconstructed from <see cref="Projectile.LastStep"/>
/// (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.
/// </summary>
[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<Projectile>();
state.RequireForUpdate<ResourceNode>();
state.RequireForUpdate<ResourceLedger>();
}
[BurstCompile]
public void OnUpdate(ref SystemState state)
{
var ledgerEntity = SystemAPI.GetSingletonEntity<ResourceLedger>();
var ledger = SystemAPI.GetBuffer<StorageEntry>(ledgerEntity);
// Snapshot all nodes once this tick.
var nodeEntities = new NativeList<Entity>(Allocator.Temp);
var nodePos = new NativeList<float2>(Allocator.Temp);
var nodeRadius = new NativeList<float>(Allocator.Temp);
var nodeRemaining = new NativeList<int>(Allocator.Temp);
var nodeResource = new NativeList<byte>(Allocator.Temp);
var nodePerHit = new NativeList<float>(Allocator.Temp);
foreach (var (xform, hr, node, e) in
SystemAPI.Query<RefRO<LocalTransform>, RefRO<HitRadius>, RefRO<ResourceNode>>().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<bool>(nodeEntities.Length, Allocator.Temp);
var ecb = new EntityCommandBuffer(Allocator.Temp);
foreach (var (xform, proj, projEntity) in
SystemAPI.Query<RefRO<LocalTransform>, RefRO<Projectile>>().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();
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 1e1ce72e524297a48a08085c68ff908e
@@ -43,6 +43,8 @@ namespace ProjectM.Server
var xform = SystemAPI.GetComponent<LocalTransform>(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.
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: ee7b17ccce78a094abf6f8008ecbef36
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -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
{
/// <summary>
/// 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 <see cref="CycleDirectorSpawner"/> + NetworkTime,
/// instantiates the ghost, initializes <see cref="CycleState"/> (Expedition, cycle 1, PhaseEndTick =
/// now + <see cref="CyclePhase.ExpeditionTicks"/>), adds the server-only <see cref="CycleRuntime"/>, 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.
/// </summary>
[BurstCompile]
[WorldSystemFilter(WorldSystemFilterFlags.ServerSimulation)]
public partial struct CycleDirectorSpawnSystem : ISystem
{
[BurstCompile]
public void OnCreate(ref SystemState state)
{
state.RequireForUpdate<CycleDirectorSpawner>();
state.RequireForUpdate<NetworkTime>();
}
[BurstCompile]
public void OnUpdate(ref SystemState state)
{
var serverTick = SystemAPI.GetSingleton<NetworkTime>().ServerTick;
if (!serverTick.IsValid)
return;
uint now = serverTick.TickIndexForValidTick;
var spawnerEntity = SystemAPI.GetSingletonEntity<CycleDirectorSpawner>();
var spawner = SystemAPI.GetComponent<CycleDirectorSpawner>(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<LocalTransform>(spawner.Prefab);
if (SystemAPI.TryGetSingleton<BaseAnchor>(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);
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: bdfd3e27e8a3e924c93d6af962e0df05
@@ -0,0 +1,93 @@
using ProjectM.Simulation;
using Unity.Burst;
using Unity.Entities;
using Unity.NetCode;
namespace ProjectM.Server
{
/// <summary>
/// Server-authoritative macro-loop director for "The Aether Cycle": Expedition (timed) -> Defend
/// (wave-driven) -> Build (timed) -> next cycle. Maintains the <see cref="CycleState"/> singleton and gates
/// <see cref="WaveSystem"/> so the base-defense wave only spawns during Defend. Runs in the plain server
/// SimulationSystemGroup (NOT prediction) before <see cref="WaveSystem"/>. All timing is wrap-safe
/// NetworkTick math (<see cref="TickUtil.NonZero"/> + <see cref="Unity.NetCode.NetworkTick.IsNewerThan"/>),
/// never raw uint compares. The CycleState/CycleRuntime live on the runtime-spawned CycleDirector ghost.
/// </summary>
[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<NetworkTime>();
state.RequireForUpdate<CycleState>();
m_AliveHusks = state.GetEntityQuery(ComponentType.ReadOnly<EnemyTag>());
}
[BurstCompile]
public void OnUpdate(ref SystemState state)
{
var serverTick = SystemAPI.GetSingleton<NetworkTime>().ServerTick;
if (!serverTick.IsValid)
return;
uint now = serverTick.TickIndexForValidTick;
var cycleEntity = SystemAPI.GetSingletonEntity<CycleState>();
var cycle = SystemAPI.GetComponent<CycleState>(cycleEntity);
var runtime = SystemAPI.GetComponent<CycleRuntime>(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<WaveState>(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<WaveState>(out var wave))
return false;
return wave.WaveNumber > defendStartWave
&& wave.RemainingToSpawn == 0
&& m_AliveHusks.CalculateEntityCount() == 0;
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: c325c252dce9fba4a938d5c8db903042
@@ -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
{
/// <summary>
/// Server-only walk-in gate transit: a player who walks within a gate's radius (and whose region matches the
/// gate's <see cref="ExpeditionGate.FromRegion"/>) is transited to the gate's ToRegion at its ArrivalPos
/// (RegionTag flipped + LocalTransform teleported — GhostRelevancy re-scopes their ghosts, as in
/// <c>RegionTransitSystem</c>). Returning to the BASE during the Expedition phase expires the Expedition
/// timer so Defend starts early ("timer cap + early return"). Plain server SimulationSystemGroup
/// <c>[UpdateAfter(CyclePhaseSystem)]</c>. Arrival points are offset from the destination gate so a transited
/// player does not immediately re-trigger.
/// </summary>
[BurstCompile]
[WorldSystemFilter(WorldSystemFilterFlags.ServerSimulation)]
[UpdateInGroup(typeof(SimulationSystemGroup))]
[UpdateAfter(typeof(CyclePhaseSystem))]
public partial struct ExpeditionGateSystem : ISystem
{
[BurstCompile]
public void OnCreate(ref SystemState state)
{
state.RequireForUpdate<ExpeditionGate>();
}
[BurstCompile]
public void OnUpdate(ref SystemState state)
{
// Snapshot gates once.
var gateFrom = new NativeList<byte>(Allocator.Temp);
var gateTo = new NativeList<byte>(Allocator.Temp);
var gateRadiusSq = new NativeList<float>(Allocator.Temp);
var gatePos = new NativeList<float2>(Allocator.Temp);
var gateArrival = new NativeList<float3>(Allocator.Temp);
foreach (var (gate, xform) in SystemAPI.Query<RefRO<ExpeditionGate>, RefRO<LocalTransform>>())
{
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<RegionTag>, RefRW<LocalTransform>>().WithAll<PlayerTag>())
{
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<CycleState>(out var cycleEntity))
{
var cs = SystemAPI.GetComponent<CycleState>(cycleEntity);
if (cs.Phase == CyclePhase.Expedition)
{
cs.PhaseEndTick = 1; // CyclePhaseSystem sees timedPhaseDue next tick -> Defend
SystemAPI.SetComponent(cycleEntity, cs);
}
}
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 4292536f663eb5c4d92688f6c5bb0368
@@ -0,0 +1,69 @@
using ProjectM.Simulation;
using Unity.Burst;
using Unity.Collections;
using Unity.Entities;
using Unity.NetCode;
namespace ProjectM.Server
{
/// <summary>
/// Server-authoritative per-connection ghost relevancy for the base/expedition region split. Each tick:
/// build a connection -&gt; 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
/// <see cref="GhostRelevancyMode.SetIsIrrelevant"/> so untagged/global ghosts (e.g. the cycle director)
/// stay relevant to everyone for free — only cross-region ghosts are hidden. Runs in the
/// <see cref="GhostSimulationSystemGroup"/> (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.
/// </summary>
[BurstCompile]
[WorldSystemFilter(WorldSystemFilterFlags.ServerSimulation)]
[UpdateInGroup(typeof(GhostSimulationSystemGroup))]
public partial struct RegionRelevancySystem : ISystem
{
[BurstCompile]
public void OnCreate(ref SystemState state)
{
state.RequireForUpdate<GhostRelevancy>();
}
[BurstCompile]
public void OnUpdate(ref SystemState state)
{
// Map each in-game connection (by NetworkId) to its player's region.
var connRegion = new NativeHashMap<int, byte>(8, Allocator.Temp);
foreach (var (owner, region) in
SystemAPI.Query<RefRO<GhostOwner>, RefRO<RegionTag>>().WithAll<PlayerTag>())
{
connRegion[owner.ValueRO.NetworkId] = region.ValueRO.Region;
}
ref var relevancy = ref SystemAPI.GetSingletonRW<GhostRelevancy>().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<GhostInstance>, RefRO<RegionTag>>())
{
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();
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: db2f33e6ce7ce9346b3dfbcd5e562ed6
@@ -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
{
/// <summary>
/// Server-authoritative handler for <see cref="RegionTransitRequest"/> RPCs. Resolves the sender's player
/// (via the source connection's <see cref="NetworkId"/> -&gt; <see cref="GhostOwner"/>), flips its
/// <see cref="RegionTag"/> to the requested region, and teleports it to that region's origin
/// (<see cref="RegionMath.RegionOrigin"/>, centered on the base via <see cref="BaseGridMath.PlotCenter"/>).
/// Runs in the default server SimulationSystemGroup (NOT the prediction loop) so the transit applies once;
/// the next snapshot reconciles the owner-predicted client and <see cref="RegionRelevancySystem"/> re-scopes
/// which region's ghosts the connection receives. Mirrors the <see cref="StorageOpReceiveSystem"/> RPC shape.
/// </summary>
[BurstCompile]
[WorldSystemFilter(WorldSystemFilterFlags.ServerSimulation)]
public partial struct RegionTransitSystem : ISystem
{
[BurstCompile]
public void OnCreate(ref SystemState state)
{
state.RequireForUpdate<BaseAnchor>();
var builder = new EntityQueryBuilder(Allocator.Temp)
.WithAll<RegionTransitRequest, ReceiveRpcCommandRequest>();
state.RequireForUpdate(state.GetEntityQuery(builder));
}
[BurstCompile]
public void OnUpdate(ref SystemState state)
{
var baseCenter = BaseGridMath.PlotCenter(SystemAPI.GetSingleton<BaseAnchor>());
// Map connection NetworkId -> player entity.
var playerByConn = new NativeHashMap<int, Entity>(8, Allocator.Temp);
foreach (var (owner, entity) in
SystemAPI.Query<RefRO<GhostOwner>>().WithAll<PlayerTag, RegionTag>().WithEntityAccess())
{
playerByConn[owner.ValueRO.NetworkId] = entity;
}
var ecb = new EntityCommandBuffer(Allocator.Temp);
foreach (var (request, receive, requestEntity) in
SystemAPI.Query<RefRO<RegionTransitRequest>, RefRO<ReceiveRpcCommandRequest>>().WithEntityAccess())
{
var connEntity = receive.ValueRO.SourceConnection;
if (SystemAPI.HasComponent<NetworkId>(connEntity))
{
int connId = SystemAPI.GetComponent<NetworkId>(connEntity).Value;
if (playerByConn.TryGetValue(connId, out var player))
{
byte target = request.ValueRO.TargetRegion;
SystemAPI.GetComponentRW<RegionTag>(player).ValueRW.Region = target;
SystemAPI.GetComponentRW<LocalTransform>(player).ValueRW.Position =
RegionMath.RegionOrigin(target, baseCenter);
}
}
ecb.DestroyEntity(requestEntity);
}
ecb.Playback(state.EntityManager);
playerByConn.Dispose();
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 44d6ce89f189d984c83cd213d86d4b02
@@ -35,5 +35,9 @@ namespace ProjectM.Simulation
/// <summary>Integrated distance travelled (predicted on client + authoritative on server). Not replicated.</summary>
public float DistanceTravelled;
/// <summary>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.</summary>
public float LastStep;
}
}
@@ -32,6 +32,7 @@ namespace ProjectM.Simulation
.WithAll<Simulate>())
{
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;
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 655041f384f27064e82ddc6dec87ce86
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,17 @@
using Unity.Entities;
namespace ProjectM.Simulation
{
/// <summary>
/// Tag marking the single GLOBAL shared-resource ledger — the entity whose [GhostField]
/// <see cref="StorageEntry"/> buffer holds harvested resources (Aether/ore/biomass) replicated to ALL
/// connections. It lives on the ownerless interpolated CycleDirector ghost, which carries NO
/// <see cref="RegionTag"/>, 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
/// <c>GetSingletonEntity&lt;ResourceLedger&gt;()</c> then <c>GetBuffer&lt;StorageEntry&gt;()</c> — NEVER
/// <c>GetSingleton&lt;StorageEntry&gt;</c> (the base container owns a second StorageEntry buffer, so a
/// buffer-typed singleton query would throw "multiple instances").
/// </summary>
public struct ResourceLedger : IComponentData { }
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 78ea3121ee0b2db4992b1a2c5dd8d34b
@@ -0,0 +1,21 @@
using Unity.Entities;
namespace ProjectM.Simulation
{
/// <summary>
/// Baked singleton holding the resource-node ghost prefab + field shape. ExpeditionFieldSystem reads it to
/// scatter <see cref="Count"/> nodes within <see cref="Radius"/> of the expedition region origin on each
/// Expedition phase entry (seeded by the cycle number). Mirrors <see cref="StorageSpawner"/>.
/// </summary>
public struct ResourceFieldSpawner : IComponentData
{
/// <summary>Baked resource-node ghost prefab to instantiate.</summary>
public Entity Prefab;
/// <summary>Number of nodes to scatter per expedition.</summary>
public int Count;
/// <summary>Scatter radius (world units) around the expedition region origin.</summary>
public float Radius;
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 120cf21540ce86640b32921fa224b974
@@ -0,0 +1,41 @@
using Unity.Entities;
using Unity.NetCode;
namespace ProjectM.Simulation
{
/// <summary>Resource-type ids for harvested materials (a byte, not an enum, per the cross-assembly enum-in-Burst hazard).</summary>
public static class ResourceId
{
/// <summary>Unused / empty sentinel (aligns with StorageMath's 0-itemId no-op).</summary>
public const byte None = 0;
/// <summary>Magic energy — powers abilities / charging.</summary>
public const byte Aether = 1;
/// <summary>Raw ore — structures / building.</summary>
public const byte Ore = 2;
/// <summary>Biomass — misc / crafting.</summary>
public const byte Biomass = 3;
}
/// <summary>
/// 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 <see cref="HarvestPerHit"/> of <see cref="ResourceId"/> into
/// the GLOBAL resource ledger and decrements <see cref="Remaining"/>; the node despawns at &lt;= 0.
/// ResourceId/Remaining are [GhostField] so clients can tint by type and (later) show depletion;
/// HarvestPerHit is baked, server-only.
/// </summary>
public struct ResourceNode : IComponentData
{
/// <summary>Which resource this node yields (see <see cref="ResourceId"/>).</summary>
[GhostField] public byte ResourceId;
/// <summary>Remaining resource units; the node despawns when this reaches 0.</summary>
[GhostField] public int Remaining;
/// <summary>Units yielded per projectile hit (baked; server-only).</summary>
public float HarvestPerHit;
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: d8504e2af2e9a694d9a7dc30c61cc69a
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: ee67f9c921637c84b92bfae0edb3d3e9
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,61 @@
using Unity.Entities;
using Unity.NetCode;
namespace ProjectM.Simulation
{
/// <summary>
/// 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).
/// </summary>
public struct CycleState : IComponentData
{
/// <summary>Current phase (see <see cref="CyclePhase"/>).</summary>
[GhostField] public byte Phase;
/// <summary>1-based cycle counter (increments when a new Expedition begins).</summary>
[GhostField] public int CycleNumber;
/// <summary>Server tick the current timed phase ends (Expedition/Build only; 0 in Defend).</summary>
[GhostField] public uint PhaseEndTick;
}
/// <summary>Phase constants for <see cref="CycleState.Phase"/> (a byte, not an enum, for trivial Burst/serialization).</summary>
public static class CyclePhase
{
/// <summary>Out in the procedural field gathering resources (timed).</summary>
public const byte Expedition = 0;
/// <summary>The base is under assault by a Husk wave (ends when the wave is cleared).</summary>
public const byte Defend = 1;
/// <summary>Calm at base: spend resources to build/upgrade (timed).</summary>
public const byte Build = 2;
/// <summary>Expedition phase duration in server ticks (SimulationTickRate = 60). Tunable; short for the M6 slice.</summary>
public const uint ExpeditionTicks = 3600; // ~60s cap (early return via the gate ends it sooner)
/// <summary>Build phase duration in server ticks.</summary>
public const uint BuildTicks = 1200; // ~20s
}
/// <summary>
/// Server-only bookkeeping for the cycle state machine that must NOT replicate (kept separate from the
/// replicated <see cref="CycleState"/>). 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".
/// </summary>
public struct CycleRuntime : IComponentData
{
/// <summary>WaveState.WaveNumber captured at the moment the current Defend phase started.</summary>
public int DefendStartWave;
/// <summary>Cycle phase from the previous tick — lets ExpeditionFieldSystem edge-detect entering/leaving Expedition.</summary>
public byte PrevPhase;
/// <summary>CycleNumber the expedition field was last seeded for (compared by int equality, never tick math).</summary>
public int LastSpawnedCycle;
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: ca714d222c4d2ed48aaaad7bbe6ec8fc
@@ -0,0 +1,16 @@
using Unity.Entities;
namespace ProjectM.Simulation
{
/// <summary>
/// Singleton baked into the gameplay subscene holding the cycle-director ghost prefab. A one-shot server
/// system (<c>CycleDirectorSpawnSystem</c>) instantiates the prefab — the GLOBAL <see cref="CycleState"/>
/// + shared resource-ledger ghost — exactly once, then destroys this singleton. Mirrors
/// <see cref="StorageSpawner"/>. Carries no transform; only the prefab needs one.
/// </summary>
public struct CycleDirectorSpawner : IComponentData
{
/// <summary>Baked cycle-director ghost prefab to instantiate.</summary>
public Entity Prefab;
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: e0fcb1bcbea82cc419d4f134c9589619
@@ -0,0 +1,28 @@
using Unity.Entities;
using Unity.Mathematics;
namespace ProjectM.Simulation
{
/// <summary>
/// A walk-in travel gate between world regions. A baked entity (visible mesh + this component) at a fixed
/// position; the server <c>ExpeditionGateSystem</c> transits a player who walks within <see cref="Radius"/>
/// and whose region matches <see cref="FromRegion"/> to <see cref="ToRegion"/>, placing them at
/// <see cref="ArrivalPos"/> (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).
/// </summary>
public struct ExpeditionGate : IComponentData
{
/// <summary>Region a player must currently be in for this gate to act on them (see <see cref="RegionId"/>).</summary>
public byte FromRegion;
/// <summary>Region the player is transited to.</summary>
public byte ToRegion;
/// <summary>Planar (XZ) trigger radius in world units.</summary>
public float Radius;
/// <summary>World position the player arrives at in the destination region.</summary>
public float3 ArrivalPos;
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: ed28d6b4a4f0b0844b851cecaadeb93f
@@ -0,0 +1,49 @@
using Unity.Entities;
using Unity.Mathematics;
namespace ProjectM.Simulation
{
/// <summary>
/// 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 <see cref="RegionId.Base"/> and
/// the procedurally-arranged <see cref="RegionId.Expedition"/> — 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).
/// </summary>
public struct RegionTag : IComponentData
{
/// <summary>Region id (see <see cref="RegionId"/>): 0 = base, 1 = expedition.</summary>
public byte Region;
}
/// <summary>Region ids for <see cref="RegionTag.Region"/> (a byte, not an enum, to keep server/Burst code trivial).</summary>
public static class RegionId
{
/// <summary>The persistent, shared home base.</summary>
public const byte Base = 0;
/// <summary>The procedural expedition field (offset far from the base on +X).</summary>
public const byte Expedition = 1;
}
/// <summary>
/// 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.
/// </summary>
public static class RegionMath
{
/// <summary>World-space X offset of the expedition region from the base region.</summary>
public const float ExpeditionOffsetX = 1000f;
/// <summary>World-space origin of <paramref name="region"/>, given the base center (BaseGridMath.PlotCenter).</summary>
public static float3 RegionOrigin(byte region, float3 baseCenter)
{
return region == RegionId.Expedition
? baseCenter + new float3(ExpeditionOffsetX, 0f, 0f)
: baseCenter;
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 09694e58455f41b439cdf791c3c421d8
@@ -0,0 +1,17 @@
using Unity.NetCode;
namespace ProjectM.Simulation
{
/// <summary>
/// Client -&gt; server request to move the sender's player between world regions (base &lt;-&gt; expedition).
/// A one-off action, so an RPC (not a per-tick predicted input), mirroring <see cref="StorageOpRequest"/>.
/// TargetRegion is a byte (see <see cref="RegionId"/>) to keep the generated serializer trivial. The
/// server teleports the sender's player to the region origin and flips its <see cref="RegionTag"/>, which
/// re-scopes GhostRelevancy so the client gains the target region's ghosts and drops the old region's.
/// </summary>
public struct RegionTransitRequest : IRpcCommand
{
/// <summary>Destination region id (see <see cref="RegionId"/>).</summary>
public byte TargetRegion;
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 32c76b46284ed1845a412ca030de2499
+310
View File
@@ -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}
+14
View File
@@ -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 01 done; 24 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<RelevantGhostForConnection,int> 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<CycleState>(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<T>` 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<StorageEntry>`** (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.
+1 -1
View File
@@ -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 02 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 34 (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.
@@ -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 01
## 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 (M1M5.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<StorageEntry>` "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<CycleState>`, 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 34 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).
@@ -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
M1M5.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 01 are implemented + runtime-validated; Stages 24 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<RelevantGhostForConnection,int> 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<CycleState>(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 24** (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 24** — 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<T>` 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]].