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