diff --git a/.serena/project.yml b/.serena/project.yml
index 9095265ac..2d83ffcdb 100644
--- a/.serena/project.yml
+++ b/.serena/project.yml
@@ -55,8 +55,8 @@ ignore_all_files_in_gitignore: true
# advanced configuration option allowing to configure language server-specific options.
# Maps the language key to the options.
-# Have a look at the docstring of the constructors of the LS implementations within solidlsp (e.g., for C# or PHP) to see which options are available.
-# No documentation on options means no options are available.
+# The settings are considered only if the project is trusted (see global configuration to define trusted projects).
+# See https://oraios.github.io/serena/02-usage/050_configuration.html#language-server-specific-settings
ls_specific_settings: {}
# list of additional workspace folder paths for cross-package reference support (e.g. in monorepos).
@@ -131,3 +131,16 @@ read_only_memory_patterns: []
# Extends the list from the global configuration, merging the two lists.
# Example: ["_archive/.*", "_episodes/.*"]
ignored_memory_patterns: []
+
+# optional shell command to run before the language backend (LSP or JetBrains) is initialised.
+# the command runs in the project root directory and is only executed if the project is trusted
+# (see trusted_project_path_patterns in the global configuration).
+# serena waits for the command to exit: a non-zero exit code is logged as an error but does not
+# abort activation. a per-project timeout (activation_command_timeout, default 180s) is the safety
+# backstop for non-terminating commands; on expiry the process is killed and activation continues.
+# example: activation_command: "npx nx run-many -t build"
+activation_command:
+
+# maximum time in seconds to wait for activation_command to complete before killing it (default 180s).
+# must be a positive number.
+activation_command_timeout: 180.0
diff --git a/Assets/Scenes/Game.unity b/Assets/Scenes/Game.unity
index f821303d9..767ea0179 100644
--- a/Assets/Scenes/Game.unity
+++ b/Assets/Scenes/Game.unity
@@ -7910,6 +7910,96 @@ Transform:
m_CorrespondingSourceObject: {fileID: 2973149261809905399, guid: f5ba9e2973c6d8d4cad295b79e2a7f45, type: 3}
m_PrefabInstance: {fileID: 275699048}
m_PrefabAsset: {fileID: 0}
+--- !u!1 &276249889
+GameObject:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ serializedVersion: 6
+ m_Component:
+ - component: {fileID: 276249890}
+ - component: {fileID: 276249892}
+ - component: {fileID: 276249891}
+ m_Layer: 0
+ m_Name: CoreCrystals
+ m_TagString: Untagged
+ m_Icon: {fileID: 0}
+ m_NavMeshLayer: 0
+ m_StaticEditorFlags: 4294967295
+ m_IsActive: 1
+--- !u!4 &276249890
+Transform:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 276249889}
+ 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: 2002840223}
+ m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
+--- !u!23 &276249891
+MeshRenderer:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 276249889}
+ 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: bc3bde0b4f16ba74aa27458eeac7042f, 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: 0
+ 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 &276249892
+MeshFilter:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 276249889}
+ m_Mesh: {fileID: 1214762786157944795, guid: e6f6a00e68d9de4489304ee8097054c3, type: 3}
--- !u!1001 &276342955
PrefabInstance:
m_ObjectHideFlags: 0
@@ -12307,8 +12397,8 @@ Transform:
- {fileID: 519877589}
- {fileID: 1646293828}
- {fileID: 1833012037}
- - {fileID: 1911983842}
- {fileID: 1282400926}
+ - {fileID: 2002840223}
m_Father: {fileID: 0}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!1001 &426724431
@@ -51293,88 +51383,6 @@ LODGroup:
- renderer: {fileID: 1078374115}
m_Enabled: 1
m_GlobalIlluminationLOD: 0
---- !u!1001 &1911983841
-PrefabInstance:
- m_ObjectHideFlags: 0
- serializedVersion: 2
- m_Modification:
- serializedVersion: 3
- m_TransformParent: {fileID: 423312854}
- m_Modifications:
- - target: {fileID: 100000, guid: d47dfef90b5bed241aa0ce186e8e5231, type: 3}
- propertyPath: m_Name
- value: EngineCore
- objectReference: {fileID: 0}
- - target: {fileID: 100000, guid: d47dfef90b5bed241aa0ce186e8e5231, type: 3}
- propertyPath: m_StaticEditorFlags
- value: 4294967295
- objectReference: {fileID: 0}
- - target: {fileID: 400000, guid: d47dfef90b5bed241aa0ce186e8e5231, type: 3}
- propertyPath: m_LocalScale.x
- value: 0.58309853
- objectReference: {fileID: 0}
- - target: {fileID: 400000, guid: d47dfef90b5bed241aa0ce186e8e5231, type: 3}
- propertyPath: m_LocalScale.y
- value: 0.61549294
- objectReference: {fileID: 0}
- - target: {fileID: 400000, guid: d47dfef90b5bed241aa0ce186e8e5231, type: 3}
- propertyPath: m_LocalScale.z
- value: 0.58309853
- objectReference: {fileID: 0}
- - target: {fileID: 400000, guid: d47dfef90b5bed241aa0ce186e8e5231, type: 3}
- propertyPath: m_LocalPosition.x
- value: 0
- objectReference: {fileID: 0}
- - target: {fileID: 400000, guid: d47dfef90b5bed241aa0ce186e8e5231, type: 3}
- propertyPath: m_LocalPosition.y
- value: 0
- objectReference: {fileID: 0}
- - target: {fileID: 400000, guid: d47dfef90b5bed241aa0ce186e8e5231, type: 3}
- propertyPath: m_LocalPosition.z
- value: 0
- objectReference: {fileID: 0}
- - target: {fileID: 400000, guid: d47dfef90b5bed241aa0ce186e8e5231, type: 3}
- propertyPath: m_LocalRotation.w
- value: 1
- objectReference: {fileID: 0}
- - target: {fileID: 400000, guid: d47dfef90b5bed241aa0ce186e8e5231, type: 3}
- propertyPath: m_LocalRotation.x
- value: -0
- objectReference: {fileID: 0}
- - target: {fileID: 400000, guid: d47dfef90b5bed241aa0ce186e8e5231, type: 3}
- propertyPath: m_LocalRotation.y
- value: -0
- objectReference: {fileID: 0}
- - target: {fileID: 400000, guid: d47dfef90b5bed241aa0ce186e8e5231, type: 3}
- propertyPath: m_LocalRotation.z
- value: -0
- objectReference: {fileID: 0}
- - target: {fileID: 400000, guid: d47dfef90b5bed241aa0ce186e8e5231, type: 3}
- propertyPath: m_LocalEulerAnglesHint.x
- value: 0
- objectReference: {fileID: 0}
- - target: {fileID: 400000, guid: d47dfef90b5bed241aa0ce186e8e5231, type: 3}
- propertyPath: m_LocalEulerAnglesHint.y
- value: 0
- objectReference: {fileID: 0}
- - target: {fileID: 400000, guid: d47dfef90b5bed241aa0ce186e8e5231, type: 3}
- propertyPath: m_LocalEulerAnglesHint.z
- value: 0
- objectReference: {fileID: 0}
- - target: {fileID: 2300000, guid: d47dfef90b5bed241aa0ce186e8e5231, type: 3}
- propertyPath: 'm_Materials.Array.data[0]'
- value:
- objectReference: {fileID: 2100000, guid: bc3bde0b4f16ba74aa27458eeac7042f, type: 2}
- m_RemovedComponents: []
- m_RemovedGameObjects: []
- m_AddedGameObjects: []
- m_AddedComponents: []
- m_SourcePrefab: {fileID: 100100000, guid: d47dfef90b5bed241aa0ce186e8e5231, type: 3}
---- !u!4 &1911983842 stripped
-Transform:
- m_CorrespondingSourceObject: {fileID: 400000, guid: d47dfef90b5bed241aa0ce186e8e5231, type: 3}
- m_PrefabInstance: {fileID: 1911983841}
- m_PrefabAsset: {fileID: 0}
--- !u!1001 &1912606105
PrefabInstance:
m_ObjectHideFlags: 0
@@ -52357,6 +52365,96 @@ Transform:
m_CorrespondingSourceObject: {fileID: 6509791800259593245, guid: e6680d59b8f4ae845ae51557281e8e53, type: 3}
m_PrefabInstance: {fileID: 1951217784}
m_PrefabAsset: {fileID: 0}
+--- !u!1 &1953443706
+GameObject:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ serializedVersion: 6
+ m_Component:
+ - component: {fileID: 1953443707}
+ - component: {fileID: 1953443709}
+ - component: {fileID: 1953443708}
+ m_Layer: 0
+ m_Name: CoreMachine
+ m_TagString: Untagged
+ m_Icon: {fileID: 0}
+ m_NavMeshLayer: 0
+ m_StaticEditorFlags: 4294967295
+ m_IsActive: 1
+--- !u!4 &1953443707
+Transform:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 1953443706}
+ 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: 2002840223}
+ m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
+--- !u!23 &1953443708
+MeshRenderer:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 1953443706}
+ 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: 4cee7a5983b187943adc589b9cb1f084, 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: 0
+ 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 &1953443709
+MeshFilter:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 1953443706}
+ m_Mesh: {fileID: -1751949791255435831, guid: e6f6a00e68d9de4489304ee8097054c3, type: 3}
--- !u!1001 &1965792741
PrefabInstance:
m_ObjectHideFlags: 0
@@ -53654,6 +53752,39 @@ Transform:
m_CorrespondingSourceObject: {fileID: 591573043354113928, guid: bafd1387c024a3a459afa35dd24cec25, type: 3}
m_PrefabInstance: {fileID: 1999454281}
m_PrefabAsset: {fileID: 0}
+--- !u!1 &2002840222
+GameObject:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ serializedVersion: 6
+ m_Component:
+ - component: {fileID: 2002840223}
+ m_Layer: 0
+ m_Name: AwakeningEngineCore
+ m_TagString: Untagged
+ m_Icon: {fileID: 0}
+ m_NavMeshLayer: 0
+ m_StaticEditorFlags: 4294967295
+ m_IsActive: 1
+--- !u!4 &2002840223
+Transform:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 2002840222}
+ 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:
+ - {fileID: 1953443707}
+ - {fileID: 276249890}
+ m_Father: {fileID: 423312854}
+ m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!1001 &2003593929
PrefabInstance:
m_ObjectHideFlags: 0
diff --git a/Assets/Screenshots/step14_boon_modal.png b/Assets/Screenshots/step14_boon_modal.png
new file mode 100644
index 000000000..07f2b1ce6
--- /dev/null
+++ b/Assets/Screenshots/step14_boon_modal.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:8aa22cdad645652f719e8efff1f804589a50e8c2380ae986d41205a26ca6ecca
+size 162953
diff --git a/Assets/Screenshots/step14_boon_modal.png.meta b/Assets/Screenshots/step14_boon_modal.png.meta
new file mode 100644
index 000000000..3b9411472
--- /dev/null
+++ b/Assets/Screenshots/step14_boon_modal.png.meta
@@ -0,0 +1,117 @@
+fileFormatVersion: 2
+guid: 2baac2a0d1e30d741883fde616060cea
+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/step14_route_panel.png b/Assets/Screenshots/step14_route_panel.png
new file mode 100644
index 000000000..e171f0314
--- /dev/null
+++ b/Assets/Screenshots/step14_route_panel.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:d487c28b55cf05d9c19dacee12c9fb17a52d5b51c9221e44e46b50e592481f56
+size 162894
diff --git a/Assets/Screenshots/step14_route_panel.png.meta b/Assets/Screenshots/step14_route_panel.png.meta
new file mode 100644
index 000000000..ba46f72bd
--- /dev/null
+++ b/Assets/Screenshots/step14_route_panel.png.meta
@@ -0,0 +1,117 @@
+fileFormatVersion: 2
+guid: 7efb157dfef14154684f62d0ea47deb0
+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/step14_route_panel2.png b/Assets/Screenshots/step14_route_panel2.png
new file mode 100644
index 000000000..bfd1cfb37
--- /dev/null
+++ b/Assets/Screenshots/step14_route_panel2.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:ef54b33c70ad0e7d6aefdcaa21b025470f7dfb85b0138536be06bddf28239f44
+size 162922
diff --git a/Assets/Screenshots/step14_route_panel2.png.meta b/Assets/Screenshots/step14_route_panel2.png.meta
new file mode 100644
index 000000000..2253cce8e
--- /dev/null
+++ b/Assets/Screenshots/step14_route_panel2.png.meta
@@ -0,0 +1,117 @@
+fileFormatVersion: 2
+guid: 94383215ba0e27c4193027b9ca5a0801
+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/step14_route_panel_final.png b/Assets/Screenshots/step14_route_panel_final.png
new file mode 100644
index 000000000..9a98361a9
--- /dev/null
+++ b/Assets/Screenshots/step14_route_panel_final.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:bfedb54dae7a13ad7025328a2d8a869bc67e8fe8c161bae17e69cf04521ff860
+size 280820
diff --git a/Assets/Screenshots/step14_route_panel_final.png.meta b/Assets/Screenshots/step14_route_panel_final.png.meta
new file mode 100644
index 000000000..68d4ac292
--- /dev/null
+++ b/Assets/Screenshots/step14_route_panel_final.png.meta
@@ -0,0 +1,117 @@
+fileFormatVersion: 2
+guid: a3d5062ca2d4c074688459d79f43f5ac
+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/step14_route_panel_live.png b/Assets/Screenshots/step14_route_panel_live.png
new file mode 100644
index 000000000..f4c4b875b
--- /dev/null
+++ b/Assets/Screenshots/step14_route_panel_live.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:2210ab810dfa0b1449b032699e204d101b1703f6ef399fb26ed8840dce65c18f
+size 151833
diff --git a/Assets/Screenshots/step14_route_panel_live.png.meta b/Assets/Screenshots/step14_route_panel_live.png.meta
new file mode 100644
index 000000000..8e6fa7400
--- /dev/null
+++ b/Assets/Screenshots/step14_route_panel_live.png.meta
@@ -0,0 +1,117 @@
+fileFormatVersion: 2
+guid: f95ae7500f290a449b0c5f61af5527a6
+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/step14_route_panel_live2.png b/Assets/Screenshots/step14_route_panel_live2.png
new file mode 100644
index 000000000..6782d2a91
--- /dev/null
+++ b/Assets/Screenshots/step14_route_panel_live2.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:8acab117db696f27bf5e68e49170083be885f3eb961e58496b0cef92fdaf7a15
+size 153910
diff --git a/Assets/Screenshots/step14_route_panel_live2.png.meta b/Assets/Screenshots/step14_route_panel_live2.png.meta
new file mode 100644
index 000000000..7f3116eea
--- /dev/null
+++ b/Assets/Screenshots/step14_route_panel_live2.png.meta
@@ -0,0 +1,117 @@
+fileFormatVersion: 2
+guid: c129c83b6ee3a80429dde83b9bed1290
+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/step14_staging_ready.png b/Assets/Screenshots/step14_staging_ready.png
new file mode 100644
index 000000000..6e9e371bc
--- /dev/null
+++ b/Assets/Screenshots/step14_staging_ready.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:e082a8b74ecb2e41c90cd6e7d3531a7ab0cf77e7c4307019cc3ff37c62f72024
+size 83587
diff --git a/Assets/Screenshots/step14_staging_ready.png.meta b/Assets/Screenshots/step14_staging_ready.png.meta
new file mode 100644
index 000000000..ece27df4f
--- /dev/null
+++ b/Assets/Screenshots/step14_staging_ready.png.meta
@@ -0,0 +1,117 @@
+fileFormatVersion: 2
+guid: 586c9080e97d9ba4c8d2606eae86cd69
+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/Art.meta b/Assets/_Project/Art.meta
new file mode 100644
index 000000000..c49c22a27
--- /dev/null
+++ b/Assets/_Project/Art.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 5532ca12b65632c4583c1be034e2393b
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/_Project/Art/Experiments.meta b/Assets/_Project/Art/Experiments.meta
new file mode 100644
index 000000000..ced7cf4b5
--- /dev/null
+++ b/Assets/_Project/Art/Experiments.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: aa9ffc37c66896e43ba6cd690104c528
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/_Project/Art/Experiments/GattlingGun_Twin.prefab b/Assets/_Project/Art/Experiments/GattlingGun_Twin.prefab
new file mode 100644
index 000000000..32ae89509
--- /dev/null
+++ b/Assets/_Project/Art/Experiments/GattlingGun_Twin.prefab
@@ -0,0 +1,87 @@
+%YAML 1.1
+%TAG !u! tag:unity3d.com,2011:
+--- !u!1001 &4916349407969455839
+PrefabInstance:
+ m_ObjectHideFlags: 0
+ serializedVersion: 2
+ m_Modification:
+ serializedVersion: 3
+ m_TransformParent: {fileID: 0}
+ m_Modifications:
+ - target: {fileID: -8679921383154817045, guid: 663f66b08f62e514fbb8ea9fdf609c1f, type: 3}
+ propertyPath: m_LocalPosition.x
+ value: -0
+ objectReference: {fileID: 0}
+ - target: {fileID: -8679921383154817045, guid: 663f66b08f62e514fbb8ea9fdf609c1f, type: 3}
+ propertyPath: m_LocalPosition.y
+ value: 0
+ objectReference: {fileID: 0}
+ - target: {fileID: -8679921383154817045, guid: 663f66b08f62e514fbb8ea9fdf609c1f, type: 3}
+ propertyPath: m_LocalPosition.z
+ value: 0
+ objectReference: {fileID: 0}
+ - target: {fileID: -8679921383154817045, guid: 663f66b08f62e514fbb8ea9fdf609c1f, type: 3}
+ propertyPath: m_LocalRotation.w
+ value: 0.7071067
+ objectReference: {fileID: 0}
+ - target: {fileID: -8679921383154817045, guid: 663f66b08f62e514fbb8ea9fdf609c1f, type: 3}
+ propertyPath: m_LocalRotation.x
+ value: 0.7071068
+ objectReference: {fileID: 0}
+ - target: {fileID: -8679921383154817045, guid: 663f66b08f62e514fbb8ea9fdf609c1f, type: 3}
+ propertyPath: m_LocalRotation.y
+ value: 0
+ objectReference: {fileID: 0}
+ - target: {fileID: -8679921383154817045, guid: 663f66b08f62e514fbb8ea9fdf609c1f, type: 3}
+ propertyPath: m_LocalRotation.z
+ value: -0
+ objectReference: {fileID: 0}
+ - target: {fileID: -8679921383154817045, guid: 663f66b08f62e514fbb8ea9fdf609c1f, type: 3}
+ propertyPath: m_LocalEulerAnglesHint.x
+ value: 0
+ objectReference: {fileID: 0}
+ - target: {fileID: -8679921383154817045, guid: 663f66b08f62e514fbb8ea9fdf609c1f, type: 3}
+ propertyPath: m_LocalEulerAnglesHint.y
+ value: 0
+ objectReference: {fileID: 0}
+ - target: {fileID: -8679921383154817045, guid: 663f66b08f62e514fbb8ea9fdf609c1f, type: 3}
+ propertyPath: m_LocalEulerAnglesHint.z
+ value: 0
+ objectReference: {fileID: 0}
+ - target: {fileID: -7511558181221131132, guid: 663f66b08f62e514fbb8ea9fdf609c1f, type: 3}
+ propertyPath: 'm_Materials.Array.data[0]'
+ value:
+ objectReference: {fileID: 2100000, guid: e600b338198602a449203a6e59c0e794, type: 2}
+ - target: {fileID: -5594369587634721024, guid: 663f66b08f62e514fbb8ea9fdf609c1f, type: 3}
+ propertyPath: 'm_Materials.Array.data[0]'
+ value:
+ objectReference: {fileID: 2100000, guid: e600b338198602a449203a6e59c0e794, type: 2}
+ - target: {fileID: -2416533905622979561, guid: 663f66b08f62e514fbb8ea9fdf609c1f, type: 3}
+ propertyPath: 'm_Materials.Array.data[0]'
+ value:
+ objectReference: {fileID: 2100000, guid: e600b338198602a449203a6e59c0e794, type: 2}
+ - target: {fileID: -2244577078095019901, guid: 663f66b08f62e514fbb8ea9fdf609c1f, type: 3}
+ propertyPath: 'm_Materials.Array.data[0]'
+ value:
+ objectReference: {fileID: 2100000, guid: e600b338198602a449203a6e59c0e794, type: 2}
+ - target: {fileID: -914913065822880002, guid: 663f66b08f62e514fbb8ea9fdf609c1f, type: 3}
+ propertyPath: 'm_Materials.Array.data[0]'
+ value:
+ objectReference: {fileID: 2100000, guid: e600b338198602a449203a6e59c0e794, type: 2}
+ - target: {fileID: 919132149155446097, guid: 663f66b08f62e514fbb8ea9fdf609c1f, type: 3}
+ propertyPath: m_Name
+ value: GattlingGun_Twin
+ objectReference: {fileID: 0}
+ - target: {fileID: 3903384658590035005, guid: 663f66b08f62e514fbb8ea9fdf609c1f, type: 3}
+ propertyPath: 'm_Materials.Array.data[0]'
+ value:
+ objectReference: {fileID: 2100000, guid: e600b338198602a449203a6e59c0e794, type: 2}
+ - target: {fileID: 6038234858806578497, guid: 663f66b08f62e514fbb8ea9fdf609c1f, type: 3}
+ propertyPath: 'm_Materials.Array.data[0]'
+ value:
+ objectReference: {fileID: 2100000, guid: e600b338198602a449203a6e59c0e794, type: 2}
+ m_RemovedComponents: []
+ m_RemovedGameObjects: []
+ m_AddedGameObjects: []
+ m_AddedComponents: []
+ m_SourcePrefab: {fileID: 100100000, guid: 663f66b08f62e514fbb8ea9fdf609c1f, type: 3}
diff --git a/Assets/_Project/Art/Experiments/GattlingGun_Twin.prefab.meta b/Assets/_Project/Art/Experiments/GattlingGun_Twin.prefab.meta
new file mode 100644
index 000000000..0469e66de
--- /dev/null
+++ b/Assets/_Project/Art/Experiments/GattlingGun_Twin.prefab.meta
@@ -0,0 +1,7 @@
+fileFormatVersion: 2
+guid: 0eb166271cf8ef747ba1236e70a8e46e
+PrefabImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/_Project/Art/Experiments/SM_Wep_GattlingGun_01_Twin.fbx b/Assets/_Project/Art/Experiments/SM_Wep_GattlingGun_01_Twin.fbx
new file mode 100644
index 000000000..8fc383e71
--- /dev/null
+++ b/Assets/_Project/Art/Experiments/SM_Wep_GattlingGun_01_Twin.fbx
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:f5d97d411f199ca5f29faa00a60139ecacc4fe422964ceb755e5e521aae6c0ac
+size 289212
diff --git a/Assets/_Project/Art/Experiments/SM_Wep_GattlingGun_01_Twin.fbx.meta b/Assets/_Project/Art/Experiments/SM_Wep_GattlingGun_01_Twin.fbx.meta
new file mode 100644
index 000000000..a5ad65544
--- /dev/null
+++ b/Assets/_Project/Art/Experiments/SM_Wep_GattlingGun_01_Twin.fbx.meta
@@ -0,0 +1,114 @@
+fileFormatVersion: 2
+guid: 663f66b08f62e514fbb8ea9fdf609c1f
+ModelImporter:
+ serializedVersion: 24501
+ internalIDToNameTable: []
+ externalObjects: {}
+ materials:
+ materialImportMode: 0
+ materialName: 0
+ materialSearch: 1
+ materialLocation: 1
+ searchTexturesGlobally: 0
+ animations:
+ legacyGenerateAnimations: 4
+ bakeSimulation: 0
+ resampleCurves: 1
+ optimizeGameObjects: 0
+ removeConstantScaleCurves: 0
+ motionNodeName:
+ animationImportErrors:
+ animationImportWarnings:
+ animationRetargetingWarnings:
+ animationDoRetargetingWarnings: 0
+ importAnimatedCustomProperties: 0
+ importConstraints: 0
+ animationCompression: 1
+ animationRotationError: 0.5
+ animationPositionError: 0.5
+ animationScaleError: 0.5
+ animationWrapMode: 0
+ extraExposedTransformPaths: []
+ extraUserProperties: []
+ clipAnimations: []
+ isReadable: 0
+ meshes:
+ lODScreenPercentages: []
+ globalScale: 1
+ meshCompression: 0
+ addColliders: 0
+ useSRGBMaterialColor: 1
+ sortHierarchyByName: 1
+ importPhysicalCameras: 1
+ importVisibility: 1
+ importBlendShapes: 1
+ importCameras: 1
+ importLights: 1
+ nodeNameCollisionStrategy: 1
+ fileIdsGeneration: 2
+ swapUVChannels: 0
+ generateSecondaryUV: 0
+ useFileUnits: 1
+ keepQuads: 0
+ weldVertices: 1
+ bakeAxisConversion: 0
+ preserveHierarchy: 0
+ skinWeightsMode: 0
+ maxBonesPerVertex: 4
+ minBoneWeight: 0.001
+ optimizeBones: 1
+ generateMeshLods: 0
+ meshLodGenerationFlags: 0
+ maximumMeshLod: -1
+ importUVs: -1
+ importVertexColors: 1
+ meshOptimizationFlags: -1
+ indexFormat: 0
+ secondaryUVAngleDistortion: 8
+ secondaryUVAreaDistortion: 15.000001
+ secondaryUVHardAngle: 88
+ secondaryUVMarginMethod: 1
+ secondaryUVMinLightmapResolution: 40
+ secondaryUVMinObjectScale: 1
+ secondaryUVPackMargin: 4
+ useFileScale: 1
+ strictVertexDataChecks: 0
+ tangentSpace:
+ normalSmoothAngle: 60
+ normalImportMode: 0
+ tangentImportMode: 3
+ normalCalculationMode: 4
+ legacyComputeAllNormalsFromSmoothingGroupsWhenMeshHasBlendShapes: 0
+ blendShapeNormalImportMode: 1
+ normalSmoothingSource: 0
+ calculateBlendshapeNormalsDeltaFromImportedNormals: 0
+ referencedClips: []
+ importAnimation: 1
+ humanDescription:
+ serializedVersion: 3
+ human: []
+ skeleton: []
+ armTwist: 0.5
+ foreArmTwist: 0.5
+ upperLegTwist: 0.5
+ legTwist: 0.5
+ armStretch: 0.05
+ legStretch: 0.05
+ feetSpacing: 0
+ globalScale: 1
+ rootMotionBoneName:
+ hasTranslationDoF: 0
+ hasExtraRoot: 0
+ skeletonHasParents: 1
+ lastHumanDescriptionAvatarSource: {instanceID: 0}
+ autoGenerateAvatarMappingIfUnspecified: 1
+ animationType: 2
+ humanoidOversampling: 1
+ avatarSetup: 0
+ addHumanoidExtraRootOnlyWhenUsingAvatar: 1
+ importBlendShapeDeformPercent: 1
+ remapMaterialsIfMaterialImportModeIsNone: 0
+ additionalBone: 0
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/_Project/Art/Structures.meta b/Assets/_Project/Art/Structures.meta
new file mode 100644
index 000000000..e26706dae
--- /dev/null
+++ b/Assets/_Project/Art/Structures.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 9e5f10ea2cf310a46956ebeb646c41e8
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/_Project/Art/Structures/SM_AwakeningCore_01.fbx b/Assets/_Project/Art/Structures/SM_AwakeningCore_01.fbx
new file mode 100644
index 000000000..a314c277c
--- /dev/null
+++ b/Assets/_Project/Art/Structures/SM_AwakeningCore_01.fbx
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:6dcd8ae03e028e98b6564d82dc03ec0b601ddb912a7b2bde9f3fccae75c7fa70
+size 185228
diff --git a/Assets/_Project/Art/Structures/SM_AwakeningCore_01.fbx.meta b/Assets/_Project/Art/Structures/SM_AwakeningCore_01.fbx.meta
new file mode 100644
index 000000000..27d01b303
--- /dev/null
+++ b/Assets/_Project/Art/Structures/SM_AwakeningCore_01.fbx.meta
@@ -0,0 +1,114 @@
+fileFormatVersion: 2
+guid: e6f6a00e68d9de4489304ee8097054c3
+ModelImporter:
+ serializedVersion: 24501
+ internalIDToNameTable: []
+ externalObjects: {}
+ materials:
+ materialImportMode: 0
+ materialName: 0
+ materialSearch: 1
+ materialLocation: 1
+ searchTexturesGlobally: 0
+ animations:
+ legacyGenerateAnimations: 4
+ bakeSimulation: 0
+ resampleCurves: 1
+ optimizeGameObjects: 0
+ removeConstantScaleCurves: 0
+ motionNodeName:
+ animationImportErrors:
+ animationImportWarnings:
+ animationRetargetingWarnings:
+ animationDoRetargetingWarnings: 0
+ importAnimatedCustomProperties: 0
+ importConstraints: 0
+ animationCompression: 1
+ animationRotationError: 0.5
+ animationPositionError: 0.5
+ animationScaleError: 0.5
+ animationWrapMode: 0
+ extraExposedTransformPaths: []
+ extraUserProperties: []
+ clipAnimations: []
+ isReadable: 0
+ meshes:
+ lODScreenPercentages: []
+ globalScale: 1
+ meshCompression: 0
+ addColliders: 0
+ useSRGBMaterialColor: 1
+ sortHierarchyByName: 1
+ importPhysicalCameras: 1
+ importVisibility: 1
+ importBlendShapes: 1
+ importCameras: 1
+ importLights: 1
+ nodeNameCollisionStrategy: 1
+ fileIdsGeneration: 2
+ swapUVChannels: 0
+ generateSecondaryUV: 0
+ useFileUnits: 1
+ keepQuads: 0
+ weldVertices: 1
+ bakeAxisConversion: 0
+ preserveHierarchy: 0
+ skinWeightsMode: 0
+ maxBonesPerVertex: 4
+ minBoneWeight: 0.001
+ optimizeBones: 1
+ generateMeshLods: 0
+ meshLodGenerationFlags: 0
+ maximumMeshLod: -1
+ importUVs: -1
+ importVertexColors: 1
+ meshOptimizationFlags: -1
+ indexFormat: 0
+ secondaryUVAngleDistortion: 8
+ secondaryUVAreaDistortion: 15.000001
+ secondaryUVHardAngle: 88
+ secondaryUVMarginMethod: 1
+ secondaryUVMinLightmapResolution: 40
+ secondaryUVMinObjectScale: 1
+ secondaryUVPackMargin: 4
+ useFileScale: 1
+ strictVertexDataChecks: 0
+ tangentSpace:
+ normalSmoothAngle: 60
+ normalImportMode: 0
+ tangentImportMode: 3
+ normalCalculationMode: 4
+ legacyComputeAllNormalsFromSmoothingGroupsWhenMeshHasBlendShapes: 0
+ blendShapeNormalImportMode: 1
+ normalSmoothingSource: 0
+ calculateBlendshapeNormalsDeltaFromImportedNormals: 0
+ referencedClips: []
+ importAnimation: 1
+ humanDescription:
+ serializedVersion: 3
+ human: []
+ skeleton: []
+ armTwist: 0.5
+ foreArmTwist: 0.5
+ upperLegTwist: 0.5
+ legTwist: 0.5
+ armStretch: 0.05
+ legStretch: 0.05
+ feetSpacing: 0
+ globalScale: 1
+ rootMotionBoneName:
+ hasTranslationDoF: 0
+ hasExtraRoot: 0
+ skeletonHasParents: 1
+ lastHumanDescriptionAvatarSource: {instanceID: 0}
+ autoGenerateAvatarMappingIfUnspecified: 1
+ animationType: 2
+ humanoidOversampling: 1
+ avatarSetup: 0
+ addHumanoidExtraRootOnlyWhenUsingAvatar: 1
+ importBlendShapeDeformPercent: 1
+ remapMaterialsIfMaterialImportModeIsNone: 0
+ additionalBone: 0
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/_Project/Art/Structures/SM_Fabricator_01.fbx b/Assets/_Project/Art/Structures/SM_Fabricator_01.fbx
new file mode 100644
index 000000000..99ba5be70
--- /dev/null
+++ b/Assets/_Project/Art/Structures/SM_Fabricator_01.fbx
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:12fa65b703e529c2bf39846fd415e3b4ac8e845dec93eb6921492f86923512f6
+size 75036
diff --git a/Assets/_Project/Art/Structures/SM_Fabricator_01.fbx.meta b/Assets/_Project/Art/Structures/SM_Fabricator_01.fbx.meta
new file mode 100644
index 000000000..a62d41a80
--- /dev/null
+++ b/Assets/_Project/Art/Structures/SM_Fabricator_01.fbx.meta
@@ -0,0 +1,114 @@
+fileFormatVersion: 2
+guid: c8c42eb35b8e2e649b185de6fd33e2e7
+ModelImporter:
+ serializedVersion: 24501
+ internalIDToNameTable: []
+ externalObjects: {}
+ materials:
+ materialImportMode: 0
+ materialName: 0
+ materialSearch: 1
+ materialLocation: 1
+ searchTexturesGlobally: 0
+ animations:
+ legacyGenerateAnimations: 4
+ bakeSimulation: 0
+ resampleCurves: 1
+ optimizeGameObjects: 0
+ removeConstantScaleCurves: 0
+ motionNodeName:
+ animationImportErrors:
+ animationImportWarnings:
+ animationRetargetingWarnings:
+ animationDoRetargetingWarnings: 0
+ importAnimatedCustomProperties: 0
+ importConstraints: 0
+ animationCompression: 1
+ animationRotationError: 0.5
+ animationPositionError: 0.5
+ animationScaleError: 0.5
+ animationWrapMode: 0
+ extraExposedTransformPaths: []
+ extraUserProperties: []
+ clipAnimations: []
+ isReadable: 0
+ meshes:
+ lODScreenPercentages: []
+ globalScale: 1
+ meshCompression: 0
+ addColliders: 0
+ useSRGBMaterialColor: 1
+ sortHierarchyByName: 1
+ importPhysicalCameras: 1
+ importVisibility: 1
+ importBlendShapes: 1
+ importCameras: 1
+ importLights: 1
+ nodeNameCollisionStrategy: 1
+ fileIdsGeneration: 2
+ swapUVChannels: 0
+ generateSecondaryUV: 0
+ useFileUnits: 1
+ keepQuads: 0
+ weldVertices: 1
+ bakeAxisConversion: 0
+ preserveHierarchy: 0
+ skinWeightsMode: 0
+ maxBonesPerVertex: 4
+ minBoneWeight: 0.001
+ optimizeBones: 1
+ generateMeshLods: 0
+ meshLodGenerationFlags: 0
+ maximumMeshLod: -1
+ importUVs: -1
+ importVertexColors: 1
+ meshOptimizationFlags: -1
+ indexFormat: 0
+ secondaryUVAngleDistortion: 8
+ secondaryUVAreaDistortion: 15.000001
+ secondaryUVHardAngle: 88
+ secondaryUVMarginMethod: 1
+ secondaryUVMinLightmapResolution: 40
+ secondaryUVMinObjectScale: 1
+ secondaryUVPackMargin: 4
+ useFileScale: 1
+ strictVertexDataChecks: 0
+ tangentSpace:
+ normalSmoothAngle: 60
+ normalImportMode: 0
+ tangentImportMode: 3
+ normalCalculationMode: 4
+ legacyComputeAllNormalsFromSmoothingGroupsWhenMeshHasBlendShapes: 0
+ blendShapeNormalImportMode: 1
+ normalSmoothingSource: 0
+ calculateBlendshapeNormalsDeltaFromImportedNormals: 0
+ referencedClips: []
+ importAnimation: 1
+ humanDescription:
+ serializedVersion: 3
+ human: []
+ skeleton: []
+ armTwist: 0.5
+ foreArmTwist: 0.5
+ upperLegTwist: 0.5
+ legTwist: 0.5
+ armStretch: 0.05
+ legStretch: 0.05
+ feetSpacing: 0
+ globalScale: 1
+ rootMotionBoneName:
+ hasTranslationDoF: 0
+ hasExtraRoot: 0
+ skeletonHasParents: 1
+ lastHumanDescriptionAvatarSource: {instanceID: 0}
+ autoGenerateAvatarMappingIfUnspecified: 1
+ animationType: 2
+ humanoidOversampling: 1
+ avatarSetup: 0
+ addHumanoidExtraRootOnlyWhenUsingAvatar: 1
+ importBlendShapeDeformPercent: 1
+ remapMaterialsIfMaterialImportModeIsNone: 0
+ additionalBone: 0
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/_Project/Art/Structures/SM_Turret_01.fbx b/Assets/_Project/Art/Structures/SM_Turret_01.fbx
new file mode 100644
index 000000000..0e5e8bdd7
--- /dev/null
+++ b/Assets/_Project/Art/Structures/SM_Turret_01.fbx
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:38caba977587668a21c06d130b4891bd8bb74726e7be552646895eb0bbddbc2e
+size 236124
diff --git a/Assets/_Project/Art/Structures/SM_Turret_01.fbx.meta b/Assets/_Project/Art/Structures/SM_Turret_01.fbx.meta
new file mode 100644
index 000000000..14f863eae
--- /dev/null
+++ b/Assets/_Project/Art/Structures/SM_Turret_01.fbx.meta
@@ -0,0 +1,114 @@
+fileFormatVersion: 2
+guid: 472a8342ece31c14f9d53ff7119b7857
+ModelImporter:
+ serializedVersion: 24501
+ internalIDToNameTable: []
+ externalObjects: {}
+ materials:
+ materialImportMode: 0
+ materialName: 0
+ materialSearch: 1
+ materialLocation: 1
+ searchTexturesGlobally: 0
+ animations:
+ legacyGenerateAnimations: 4
+ bakeSimulation: 0
+ resampleCurves: 1
+ optimizeGameObjects: 0
+ removeConstantScaleCurves: 0
+ motionNodeName:
+ animationImportErrors:
+ animationImportWarnings:
+ animationRetargetingWarnings:
+ animationDoRetargetingWarnings: 0
+ importAnimatedCustomProperties: 0
+ importConstraints: 0
+ animationCompression: 1
+ animationRotationError: 0.5
+ animationPositionError: 0.5
+ animationScaleError: 0.5
+ animationWrapMode: 0
+ extraExposedTransformPaths: []
+ extraUserProperties: []
+ clipAnimations: []
+ isReadable: 0
+ meshes:
+ lODScreenPercentages: []
+ globalScale: 1
+ meshCompression: 0
+ addColliders: 0
+ useSRGBMaterialColor: 1
+ sortHierarchyByName: 1
+ importPhysicalCameras: 1
+ importVisibility: 1
+ importBlendShapes: 1
+ importCameras: 1
+ importLights: 1
+ nodeNameCollisionStrategy: 1
+ fileIdsGeneration: 2
+ swapUVChannels: 0
+ generateSecondaryUV: 0
+ useFileUnits: 1
+ keepQuads: 0
+ weldVertices: 1
+ bakeAxisConversion: 0
+ preserveHierarchy: 0
+ skinWeightsMode: 0
+ maxBonesPerVertex: 4
+ minBoneWeight: 0.001
+ optimizeBones: 1
+ generateMeshLods: 0
+ meshLodGenerationFlags: 0
+ maximumMeshLod: -1
+ importUVs: -1
+ importVertexColors: 1
+ meshOptimizationFlags: -1
+ indexFormat: 0
+ secondaryUVAngleDistortion: 8
+ secondaryUVAreaDistortion: 15.000001
+ secondaryUVHardAngle: 88
+ secondaryUVMarginMethod: 1
+ secondaryUVMinLightmapResolution: 40
+ secondaryUVMinObjectScale: 1
+ secondaryUVPackMargin: 4
+ useFileScale: 1
+ strictVertexDataChecks: 0
+ tangentSpace:
+ normalSmoothAngle: 60
+ normalImportMode: 0
+ tangentImportMode: 3
+ normalCalculationMode: 4
+ legacyComputeAllNormalsFromSmoothingGroupsWhenMeshHasBlendShapes: 0
+ blendShapeNormalImportMode: 1
+ normalSmoothingSource: 0
+ calculateBlendshapeNormalsDeltaFromImportedNormals: 0
+ referencedClips: []
+ importAnimation: 1
+ humanDescription:
+ serializedVersion: 3
+ human: []
+ skeleton: []
+ armTwist: 0.5
+ foreArmTwist: 0.5
+ upperLegTwist: 0.5
+ legTwist: 0.5
+ armStretch: 0.05
+ legStretch: 0.05
+ feetSpacing: 0
+ globalScale: 1
+ rootMotionBoneName:
+ hasTranslationDoF: 0
+ hasExtraRoot: 0
+ skeletonHasParents: 1
+ lastHumanDescriptionAvatarSource: {instanceID: 0}
+ autoGenerateAvatarMappingIfUnspecified: 1
+ animationType: 2
+ humanoidOversampling: 1
+ avatarSetup: 0
+ addHumanoidExtraRootOnlyWhenUsingAvatar: 1
+ importBlendShapeDeformPercent: 1
+ remapMaterialsIfMaterialImportModeIsNone: 0
+ additionalBone: 0
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/_Project/Art/Structures/SM_Wall_01.fbx b/Assets/_Project/Art/Structures/SM_Wall_01.fbx
new file mode 100644
index 000000000..3738756ca
--- /dev/null
+++ b/Assets/_Project/Art/Structures/SM_Wall_01.fbx
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:3f74e76e7f2755e32eac27a682b009467795cf28aad86a55b4b7107835609c0a
+size 20684
diff --git a/Assets/_Project/Art/Structures/SM_Wall_01.fbx.meta b/Assets/_Project/Art/Structures/SM_Wall_01.fbx.meta
new file mode 100644
index 000000000..88f664bbf
--- /dev/null
+++ b/Assets/_Project/Art/Structures/SM_Wall_01.fbx.meta
@@ -0,0 +1,114 @@
+fileFormatVersion: 2
+guid: d5f5c8f8a3300c249a861a444f85e05d
+ModelImporter:
+ serializedVersion: 24501
+ internalIDToNameTable: []
+ externalObjects: {}
+ materials:
+ materialImportMode: 0
+ materialName: 0
+ materialSearch: 1
+ materialLocation: 1
+ searchTexturesGlobally: 0
+ animations:
+ legacyGenerateAnimations: 4
+ bakeSimulation: 0
+ resampleCurves: 1
+ optimizeGameObjects: 0
+ removeConstantScaleCurves: 0
+ motionNodeName:
+ animationImportErrors:
+ animationImportWarnings:
+ animationRetargetingWarnings:
+ animationDoRetargetingWarnings: 0
+ importAnimatedCustomProperties: 0
+ importConstraints: 0
+ animationCompression: 1
+ animationRotationError: 0.5
+ animationPositionError: 0.5
+ animationScaleError: 0.5
+ animationWrapMode: 0
+ extraExposedTransformPaths: []
+ extraUserProperties: []
+ clipAnimations: []
+ isReadable: 0
+ meshes:
+ lODScreenPercentages: []
+ globalScale: 1
+ meshCompression: 0
+ addColliders: 0
+ useSRGBMaterialColor: 1
+ sortHierarchyByName: 1
+ importPhysicalCameras: 1
+ importVisibility: 1
+ importBlendShapes: 1
+ importCameras: 1
+ importLights: 1
+ nodeNameCollisionStrategy: 1
+ fileIdsGeneration: 2
+ swapUVChannels: 0
+ generateSecondaryUV: 0
+ useFileUnits: 1
+ keepQuads: 0
+ weldVertices: 1
+ bakeAxisConversion: 0
+ preserveHierarchy: 0
+ skinWeightsMode: 0
+ maxBonesPerVertex: 4
+ minBoneWeight: 0.001
+ optimizeBones: 1
+ generateMeshLods: 0
+ meshLodGenerationFlags: 0
+ maximumMeshLod: -1
+ importUVs: -1
+ importVertexColors: 1
+ meshOptimizationFlags: -1
+ indexFormat: 0
+ secondaryUVAngleDistortion: 8
+ secondaryUVAreaDistortion: 15.000001
+ secondaryUVHardAngle: 88
+ secondaryUVMarginMethod: 1
+ secondaryUVMinLightmapResolution: 40
+ secondaryUVMinObjectScale: 1
+ secondaryUVPackMargin: 4
+ useFileScale: 1
+ strictVertexDataChecks: 0
+ tangentSpace:
+ normalSmoothAngle: 60
+ normalImportMode: 0
+ tangentImportMode: 3
+ normalCalculationMode: 4
+ legacyComputeAllNormalsFromSmoothingGroupsWhenMeshHasBlendShapes: 0
+ blendShapeNormalImportMode: 1
+ normalSmoothingSource: 0
+ calculateBlendshapeNormalsDeltaFromImportedNormals: 0
+ referencedClips: []
+ importAnimation: 1
+ humanDescription:
+ serializedVersion: 3
+ human: []
+ skeleton: []
+ armTwist: 0.5
+ foreArmTwist: 0.5
+ upperLegTwist: 0.5
+ legTwist: 0.5
+ armStretch: 0.05
+ legStretch: 0.05
+ feetSpacing: 0
+ globalScale: 1
+ rootMotionBoneName:
+ hasTranslationDoF: 0
+ hasExtraRoot: 0
+ skeletonHasParents: 1
+ lastHumanDescriptionAvatarSource: {instanceID: 0}
+ autoGenerateAvatarMappingIfUnspecified: 1
+ animationType: 2
+ humanoidOversampling: 1
+ avatarSetup: 0
+ addHumanoidExtraRootOnlyWhenUsingAvatar: 1
+ importBlendShapeDeformPercent: 1
+ remapMaterialsIfMaterialImportModeIsNone: 0
+ additionalBone: 0
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/_Project/Prefabs/Fabricator.prefab b/Assets/_Project/Prefabs/Fabricator.prefab
index 8f6e4c349..f01aeb50c 100644
--- a/Assets/_Project/Prefabs/Fabricator.prefab
+++ b/Assets/_Project/Prefabs/Fabricator.prefab
@@ -31,7 +31,7 @@ Transform:
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_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 0}
@@ -43,7 +43,7 @@ MeshFilter:
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 3885353946372160549}
- m_Mesh: {fileID: 4300000, guid: abc00000000010690097314383055197, type: 3}
+ m_Mesh: {fileID: -1810898345513765494, guid: c8c42eb35b8e2e649b185de6fd33e2e7, type: 3}
--- !u!23 &3320445911748035220
MeshRenderer:
m_ObjectHideFlags: 0
@@ -69,7 +69,7 @@ MeshRenderer:
m_RenderingLayerMask: 1
m_RendererPriority: 0
m_Materials:
- - {fileID: 2100000, guid: 53c360a5b4f92c04caad3273e9bb5bef, type: 2}
+ - {fileID: 2100000, guid: 4cee7a5983b187943adc589b9cb1f084, type: 2}
m_StaticBatchInfo:
firstSubMesh: 0
subMeshCount: 0
diff --git a/Assets/_Project/Prefabs/Turret.prefab b/Assets/_Project/Prefabs/Turret.prefab
index a301cd4e2..2428fd310 100644
--- a/Assets/_Project/Prefabs/Turret.prefab
+++ b/Assets/_Project/Prefabs/Turret.prefab
@@ -26,12 +26,11 @@ Transform:
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 2057690259992313649}
serializedVersion: 2
- m_LocalRotation: {x: 0, y: -0, z: -0, w: 1}
+ m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
- m_LocalScale: {x: 0.8, y: 0.8, z: 0.8}
+ m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
- m_Children:
- - {fileID: 8624793677999475166}
+ m_Children: []
m_Father: {fileID: 3572766465862231365}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!33 &3818890329194429404
@@ -41,7 +40,7 @@ MeshFilter:
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 2057690259992313649}
- m_Mesh: {fileID: 4300000, guid: b9f5d4937fcf552448a1757f00aec25a, type: 3}
+ m_Mesh: {fileID: -8131118695690612189, guid: 472a8342ece31c14f9d53ff7119b7857, type: 3}
--- !u!23 &6553709043537316589
MeshRenderer:
m_ObjectHideFlags: 0
@@ -67,282 +66,8 @@ MeshRenderer:
m_RenderingLayerMask: 1
m_RendererPriority: 0
m_Materials:
- - {fileID: 2100000, guid: 71e41e43b459a244abaf5acca76b89ee, 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: 0
- 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!1 &2187311308710316335
-GameObject:
- m_ObjectHideFlags: 0
- m_CorrespondingSourceObject: {fileID: 0}
- m_PrefabInstance: {fileID: 0}
- m_PrefabAsset: {fileID: 0}
- serializedVersion: 6
- m_Component:
- - component: {fileID: 1661254142448502089}
- - component: {fileID: 826915993840495878}
- - component: {fileID: 8045752627718943123}
- m_Layer: 0
- m_Name: SM_Wep_Ballista_Mounted_Bolt_01
- m_TagString: Untagged
- m_Icon: {fileID: 0}
- m_NavMeshLayer: 0
- m_StaticEditorFlags: 0
- m_IsActive: 1
---- !u!4 &1661254142448502089
-Transform:
- m_ObjectHideFlags: 0
- m_CorrespondingSourceObject: {fileID: 0}
- m_PrefabInstance: {fileID: 0}
- m_PrefabAsset: {fileID: 0}
- m_GameObject: {fileID: 2187311308710316335}
- serializedVersion: 2
- m_LocalRotation: {x: 0, y: -0, z: -0, w: 1}
- m_LocalPosition: {x: 0.000120390985, y: 0.07090128, z: 1.1123258}
- m_LocalScale: {x: 1, y: 1, z: 1}
- m_ConstrainProportionsScale: 0
- m_Children: []
- m_Father: {fileID: 4587729640460227676}
- m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
---- !u!33 &826915993840495878
-MeshFilter:
- m_ObjectHideFlags: 0
- m_CorrespondingSourceObject: {fileID: 0}
- m_PrefabInstance: {fileID: 0}
- m_PrefabAsset: {fileID: 0}
- m_GameObject: {fileID: 2187311308710316335}
- m_Mesh: {fileID: 4300010, guid: b9f5d4937fcf552448a1757f00aec25a, type: 3}
---- !u!23 &8045752627718943123
-MeshRenderer:
- m_ObjectHideFlags: 0
- m_CorrespondingSourceObject: {fileID: 0}
- m_PrefabInstance: {fileID: 0}
- m_PrefabAsset: {fileID: 0}
- m_GameObject: {fileID: 2187311308710316335}
- 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: 71e41e43b459a244abaf5acca76b89ee, 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: 0
- 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!1 &2457356283659831041
-GameObject:
- m_ObjectHideFlags: 0
- m_CorrespondingSourceObject: {fileID: 0}
- m_PrefabInstance: {fileID: 0}
- m_PrefabAsset: {fileID: 0}
- serializedVersion: 6
- m_Component:
- - component: {fileID: 8624793677999475166}
- - component: {fileID: 2206687939742515296}
- - component: {fileID: 2797104103422709025}
- m_Layer: 0
- m_Name: SM_Wep_Ballista_Mounted_Horizontal_01
- m_TagString: Untagged
- m_Icon: {fileID: 0}
- m_NavMeshLayer: 0
- m_StaticEditorFlags: 0
- m_IsActive: 1
---- !u!4 &8624793677999475166
-Transform:
- m_ObjectHideFlags: 0
- m_CorrespondingSourceObject: {fileID: 0}
- m_PrefabInstance: {fileID: 0}
- m_PrefabAsset: {fileID: 0}
- m_GameObject: {fileID: 2457356283659831041}
- serializedVersion: 2
- m_LocalRotation: {x: 0, y: -0, z: -0, w: 1}
- m_LocalPosition: {x: 0.00000019073485, y: 1.0745214, z: -0.0000007629394}
- m_LocalScale: {x: 1, y: 1, z: 1}
- m_ConstrainProportionsScale: 0
- m_Children:
- - {fileID: 4587729640460227676}
- m_Father: {fileID: 4449724123933675757}
- m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
---- !u!33 &2206687939742515296
-MeshFilter:
- m_ObjectHideFlags: 0
- m_CorrespondingSourceObject: {fileID: 0}
- m_PrefabInstance: {fileID: 0}
- m_PrefabAsset: {fileID: 0}
- m_GameObject: {fileID: 2457356283659831041}
- m_Mesh: {fileID: 4300002, guid: b9f5d4937fcf552448a1757f00aec25a, type: 3}
---- !u!23 &2797104103422709025
-MeshRenderer:
- m_ObjectHideFlags: 0
- m_CorrespondingSourceObject: {fileID: 0}
- m_PrefabInstance: {fileID: 0}
- m_PrefabAsset: {fileID: 0}
- m_GameObject: {fileID: 2457356283659831041}
- 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: 71e41e43b459a244abaf5acca76b89ee, 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: 0
- 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!1 &2839324981893915979
-GameObject:
- m_ObjectHideFlags: 0
- m_CorrespondingSourceObject: {fileID: 0}
- m_PrefabInstance: {fileID: 0}
- m_PrefabAsset: {fileID: 0}
- serializedVersion: 6
- m_Component:
- - component: {fileID: 4587729640460227676}
- - component: {fileID: 336643015455069546}
- - component: {fileID: 1156568749056329743}
- m_Layer: 0
- m_Name: SM_Wep_Ballista_Mounted_Vertical_01
- m_TagString: Untagged
- m_Icon: {fileID: 0}
- m_NavMeshLayer: 0
- m_StaticEditorFlags: 0
- m_IsActive: 1
---- !u!4 &4587729640460227676
-Transform:
- m_ObjectHideFlags: 0
- m_CorrespondingSourceObject: {fileID: 0}
- m_PrefabInstance: {fileID: 0}
- m_PrefabAsset: {fileID: 0}
- m_GameObject: {fileID: 2839324981893915979}
- serializedVersion: 2
- m_LocalRotation: {x: 0, y: -0, z: -0, w: 1}
- m_LocalPosition: {x: -0.000000055864938, y: 0.29948944, z: -0.0005771637}
- m_LocalScale: {x: 1, y: 1, z: 1}
- m_ConstrainProportionsScale: 0
- m_Children:
- - {fileID: 1661254142448502089}
- - {fileID: 2244160433493401752}
- - {fileID: 1648902957520227441}
- - {fileID: 4099830417809752389}
- m_Father: {fileID: 8624793677999475166}
- m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
---- !u!33 &336643015455069546
-MeshFilter:
- m_ObjectHideFlags: 0
- m_CorrespondingSourceObject: {fileID: 0}
- m_PrefabInstance: {fileID: 0}
- m_PrefabAsset: {fileID: 0}
- m_GameObject: {fileID: 2839324981893915979}
- m_Mesh: {fileID: 4300004, guid: b9f5d4937fcf552448a1757f00aec25a, type: 3}
---- !u!23 &1156568749056329743
-MeshRenderer:
- m_ObjectHideFlags: 0
- m_CorrespondingSourceObject: {fileID: 0}
- m_PrefabInstance: {fileID: 0}
- m_PrefabAsset: {fileID: 0}
- m_GameObject: {fileID: 2839324981893915979}
- 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: 71e41e43b459a244abaf5acca76b89ee, type: 2}
+ - {fileID: 2100000, guid: 4cee7a5983b187943adc589b9cb1f084, type: 2}
+ - {fileID: 2100000, guid: e600b338198602a449203a6e59c0e794, type: 2}
m_StaticBatchInfo:
firstSubMesh: 0
subMeshCount: 0
@@ -477,273 +202,3 @@ BoxCollider:
serializedVersion: 3
m_Size: {x: 0.8, y: 1.2, z: 0.8}
m_Center: {x: 0, y: 0.6, z: 0}
---- !u!1 &4051895978514069616
-GameObject:
- m_ObjectHideFlags: 0
- m_CorrespondingSourceObject: {fileID: 0}
- m_PrefabInstance: {fileID: 0}
- m_PrefabAsset: {fileID: 0}
- serializedVersion: 6
- m_Component:
- - component: {fileID: 2244160433493401752}
- - component: {fileID: 5785378423824416967}
- - component: {fileID: 774247800957589290}
- m_Layer: 0
- m_Name: SM_Wep_Ballista_Mounted_Handle_01
- m_TagString: Untagged
- m_Icon: {fileID: 0}
- m_NavMeshLayer: 0
- m_StaticEditorFlags: 0
- m_IsActive: 1
---- !u!4 &2244160433493401752
-Transform:
- m_ObjectHideFlags: 0
- m_CorrespondingSourceObject: {fileID: 0}
- m_PrefabInstance: {fileID: 0}
- m_PrefabAsset: {fileID: 0}
- m_GameObject: {fileID: 4051895978514069616}
- serializedVersion: 2
- m_LocalRotation: {x: 0, y: -0, z: -0, w: 1}
- m_LocalPosition: {x: 0.000000055864938, y: -0.011389092, z: -1.9009238}
- m_LocalScale: {x: 1, y: 1, z: 1}
- m_ConstrainProportionsScale: 0
- m_Children: []
- m_Father: {fileID: 4587729640460227676}
- m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
---- !u!33 &5785378423824416967
-MeshFilter:
- m_ObjectHideFlags: 0
- m_CorrespondingSourceObject: {fileID: 0}
- m_PrefabInstance: {fileID: 0}
- m_PrefabAsset: {fileID: 0}
- m_GameObject: {fileID: 4051895978514069616}
- m_Mesh: {fileID: 4300008, guid: b9f5d4937fcf552448a1757f00aec25a, type: 3}
---- !u!23 &774247800957589290
-MeshRenderer:
- m_ObjectHideFlags: 0
- m_CorrespondingSourceObject: {fileID: 0}
- m_PrefabInstance: {fileID: 0}
- m_PrefabAsset: {fileID: 0}
- m_GameObject: {fileID: 4051895978514069616}
- 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: 71e41e43b459a244abaf5acca76b89ee, 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: 0
- 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!1 &4368797453099486757
-GameObject:
- m_ObjectHideFlags: 0
- m_CorrespondingSourceObject: {fileID: 0}
- m_PrefabInstance: {fileID: 0}
- m_PrefabAsset: {fileID: 0}
- serializedVersion: 6
- m_Component:
- - component: {fileID: 4099830417809752389}
- - component: {fileID: 4089364109599762086}
- - component: {fileID: 5885630555293055191}
- m_Layer: 0
- m_Name: SM_Wep_Ballista_Mounted_String_01
- m_TagString: Untagged
- m_Icon: {fileID: 0}
- m_NavMeshLayer: 0
- m_StaticEditorFlags: 0
- m_IsActive: 1
---- !u!4 &4099830417809752389
-Transform:
- m_ObjectHideFlags: 0
- m_CorrespondingSourceObject: {fileID: 0}
- m_PrefabInstance: {fileID: 0}
- m_PrefabAsset: {fileID: 0}
- m_GameObject: {fileID: 4368797453099486757}
- serializedVersion: 2
- m_LocalRotation: {x: 0, y: -0, z: -0, w: 1}
- m_LocalPosition: {x: -0.00000013486994, y: 0.0375115, z: -0.07168248}
- m_LocalScale: {x: 1, y: 1, z: 1}
- m_ConstrainProportionsScale: 0
- m_Children: []
- m_Father: {fileID: 4587729640460227676}
- m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
---- !u!33 &4089364109599762086
-MeshFilter:
- m_ObjectHideFlags: 0
- m_CorrespondingSourceObject: {fileID: 0}
- m_PrefabInstance: {fileID: 0}
- m_PrefabAsset: {fileID: 0}
- m_GameObject: {fileID: 4368797453099486757}
- m_Mesh: {fileID: 4300012, guid: b9f5d4937fcf552448a1757f00aec25a, type: 3}
---- !u!23 &5885630555293055191
-MeshRenderer:
- m_ObjectHideFlags: 0
- m_CorrespondingSourceObject: {fileID: 0}
- m_PrefabInstance: {fileID: 0}
- m_PrefabAsset: {fileID: 0}
- m_GameObject: {fileID: 4368797453099486757}
- 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: 71e41e43b459a244abaf5acca76b89ee, 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: 0
- 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!1 &5096850821653549945
-GameObject:
- m_ObjectHideFlags: 0
- m_CorrespondingSourceObject: {fileID: 0}
- m_PrefabInstance: {fileID: 0}
- m_PrefabAsset: {fileID: 0}
- serializedVersion: 6
- m_Component:
- - component: {fileID: 1648902957520227441}
- - component: {fileID: 8575753145058782478}
- - component: {fileID: 1327063321531964727}
- m_Layer: 0
- m_Name: SM_Wep_Ballista_Mounted_Loader_01
- m_TagString: Untagged
- m_Icon: {fileID: 0}
- m_NavMeshLayer: 0
- m_StaticEditorFlags: 0
- m_IsActive: 1
---- !u!4 &1648902957520227441
-Transform:
- m_ObjectHideFlags: 0
- m_CorrespondingSourceObject: {fileID: 0}
- m_PrefabInstance: {fileID: 0}
- m_PrefabAsset: {fileID: 0}
- m_GameObject: {fileID: 5096850821653549945}
- serializedVersion: 2
- m_LocalRotation: {x: 0, y: -0, z: -0, w: 1}
- m_LocalPosition: {x: 0.000000055864938, y: 0.08274717, z: -1.2890174}
- m_LocalScale: {x: 1, y: 1, z: 1}
- m_ConstrainProportionsScale: 0
- m_Children: []
- m_Father: {fileID: 4587729640460227676}
- m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
---- !u!33 &8575753145058782478
-MeshFilter:
- m_ObjectHideFlags: 0
- m_CorrespondingSourceObject: {fileID: 0}
- m_PrefabInstance: {fileID: 0}
- m_PrefabAsset: {fileID: 0}
- m_GameObject: {fileID: 5096850821653549945}
- m_Mesh: {fileID: 4300006, guid: b9f5d4937fcf552448a1757f00aec25a, type: 3}
---- !u!23 &1327063321531964727
-MeshRenderer:
- m_ObjectHideFlags: 0
- m_CorrespondingSourceObject: {fileID: 0}
- m_PrefabInstance: {fileID: 0}
- m_PrefabAsset: {fileID: 0}
- m_GameObject: {fileID: 5096850821653549945}
- 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: 71e41e43b459a244abaf5acca76b89ee, 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: 0
- 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}
diff --git a/Assets/_Project/Prefabs/Wall.prefab b/Assets/_Project/Prefabs/Wall.prefab
index fae933c61..292654088 100644
--- a/Assets/_Project/Prefabs/Wall.prefab
+++ b/Assets/_Project/Prefabs/Wall.prefab
@@ -26,9 +26,9 @@ Transform:
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 551144932704427983}
serializedVersion: 2
- m_LocalRotation: {x: 0, y: -0, z: -0, w: 1}
+ m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
- m_LocalScale: {x: 1.2, y: 1.2, z: 1.2}
+ m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 3572766465862231365}
@@ -40,7 +40,7 @@ MeshFilter:
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 551144932704427983}
- m_Mesh: {fileID: 4300000, guid: d948b51d152cd2348b756856f220cc7e, type: 3}
+ m_Mesh: {fileID: 4754861833707048916, guid: d5f5c8f8a3300c249a861a444f85e05d, type: 3}
--- !u!23 &4489297219945009455
MeshRenderer:
m_ObjectHideFlags: 0
@@ -66,6 +66,7 @@ MeshRenderer:
m_RenderingLayerMask: 1
m_RendererPriority: 0
m_Materials:
+ - {fileID: 2100000, guid: 2a95f9ff80948c643a57b9e94a98eb3f, type: 2}
- {fileID: 2100000, guid: 53c360a5b4f92c04caad3273e9bb5bef, type: 2}
m_StaticBatchInfo:
firstSubMesh: 0
diff --git a/Assets/_Project/Resources/HudTheme.asset b/Assets/_Project/Resources/HudTheme.asset
index 379f13ada..57cc0e65b 100644
--- a/Assets/_Project/Resources/HudTheme.asset
+++ b/Assets/_Project/Resources/HudTheme.asset
@@ -37,6 +37,9 @@ MonoBehaviour:
HarvesterIcon: {fileID: 21300000, guid: 5a8c6e8575552cb4d96a8fe09227c6e2, type: 3}
FabricatorIcon: {fileID: 21300000, guid: ca43072da0d43f44fbdbf57c997414ba, type: 3}
ConveyorIcon: {fileID: 21300000, guid: ba4939cd85537454a93777a4cc9e8b38, type: 3}
+ TurretGhostMesh: {fileID: -8131118695690612189, guid: 472a8342ece31c14f9d53ff7119b7857, type: 3}
+ WallGhostMesh: {fileID: 4754861833707048916, guid: d5f5c8f8a3300c249a861a444f85e05d, type: 3}
+ FabricatorGhostMesh: {fileID: -1810898345513765494, guid: c8c42eb35b8e2e649b185de6fd33e2e7, type: 3}
KbmPlace: {fileID: 21300000, guid: 3fc063021eaa5eb4cb710e15a38ceb6c, type: 3}
KbmCancel: {fileID: 21300000, guid: 78d618b10af7f9b4db2600f77fdce06c, type: 3}
PadPlace: {fileID: 21300000, guid: 15a4f1d900c35fd4091f6970c5250a44, type: 3}
diff --git a/Assets/_Project/Scripts/Authoring/Combat/BoonCatalogAuthoring.cs b/Assets/_Project/Scripts/Authoring/Combat/BoonCatalogAuthoring.cs
new file mode 100644
index 000000000..9b1ae3dc6
--- /dev/null
+++ b/Assets/_Project/Scripts/Authoring/Combat/BoonCatalogAuthoring.cs
@@ -0,0 +1,77 @@
+using System;
+using System.Collections.Generic;
+using ProjectM.Simulation;
+using Unity.Collections;
+using Unity.Entities;
+using UnityEngine;
+
+namespace ProjectM.Authoring
+{
+ ///
+ /// Authoring for the boon-catalog config singleton (place ONE in the gameplay subscene). Designers can author
+ /// rows in the inspector; an EMPTY list bakes the code-default v1 table ()
+ /// verbatim — so the subscene object needs zero property assignment through the tooling (the
+ /// component_properties enum/array-drop hazard). Ids are APPEND-ONLY (they ride the replicated BoonOffer bytes
+ /// and, later, per-run analytics).
+ ///
+ public class BoonCatalogAuthoring : MonoBehaviour
+ {
+ [Serializable]
+ public struct BoonRow
+ {
+ public byte Id;
+ public StatTarget Target;
+ public ModOp Op;
+ public float Value;
+ [Tooltip("Rarity draw weight: common 100 / rare 30 / epic 10. 0 removes the boon from the pool.")]
+ public byte Weight;
+ [Tooltip("bit0 = Warrior, bit1 = Ranger, 3 = both.")]
+ public byte ClassMask;
+ public string Name;
+ public string Desc;
+ }
+
+ [Tooltip("Leave EMPTY to bake the code-default v1 table; fill to fully replace it.")]
+ public List Rows = new List();
+
+ private class BoonCatalogBaker : Baker
+ {
+ public override void Bake(BoonCatalogAuthoring authoring)
+ {
+ var entity = GetEntity(authoring, TransformUsageFlags.None);
+
+ BlobAssetReference blob;
+ if (authoring.Rows == null || authoring.Rows.Count == 0)
+ {
+ blob = BoonCatalogData.BuildDefault();
+ }
+ else
+ {
+ var builder = new BlobBuilder(Allocator.Temp);
+ ref var root = ref builder.ConstructRoot();
+ var defs = builder.Allocate(ref root.Defs, authoring.Rows.Count);
+ for (int i = 0; i < authoring.Rows.Count; i++)
+ {
+ var r = authoring.Rows[i];
+ defs[i] = new BoonDefBlob
+ {
+ Id = r.Id,
+ Target = (byte)r.Target,
+ Op = (byte)r.Op,
+ Value = r.Value,
+ Weight = r.Weight,
+ ClassMask = r.ClassMask,
+ Name = new FixedString64Bytes(r.Name ?? string.Empty),
+ Desc = new FixedString128Bytes(r.Desc ?? string.Empty),
+ };
+ }
+ blob = builder.CreateBlobAssetReference(Allocator.Persistent);
+ builder.Dispose();
+ }
+
+ AddBlobAsset(ref blob, out _);
+ AddComponent(entity, new BoonCatalog { Value = blob });
+ }
+ }
+ }
+}
diff --git a/Assets/_Project/Scripts/Authoring/Combat/BoonCatalogAuthoring.cs.meta b/Assets/_Project/Scripts/Authoring/Combat/BoonCatalogAuthoring.cs.meta
new file mode 100644
index 000000000..53d5ebbd4
--- /dev/null
+++ b/Assets/_Project/Scripts/Authoring/Combat/BoonCatalogAuthoring.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 075a570afb8ef9541b6307280b53f4e7
\ No newline at end of file
diff --git a/Assets/_Project/Scripts/Authoring/Economy/BaseFieldSpawnerAuthoring.cs b/Assets/_Project/Scripts/Authoring/Economy/BaseFieldSpawnerAuthoring.cs
deleted file mode 100644
index bd4eb792d..000000000
--- a/Assets/_Project/Scripts/Authoring/Economy/BaseFieldSpawnerAuthoring.cs
+++ /dev/null
@@ -1,51 +0,0 @@
-using ProjectM.Simulation;
-using Unity.Entities;
-using UnityEngine;
-
-namespace ProjectM.Authoring
-{
- ///
- /// Authoring for the home-base mining field (). Place ONE in the gameplay
- /// subscene. = the SAME ResourceNode ghost prefab the expedition uses; the server
- /// system overrides each instance to RegionTag{Base} + ResourceId.Ore and scatters them in the
- /// [, ] annulus around the base plot center. Defaults are
- /// sized to the baked 32x32 plot (square corner reach ~22.6) inside the ~28.7 boundary ring, so nodes form a
- /// reachable perimeter ring that never sits on a build cell.
- ///
- public class BaseFieldSpawnerAuthoring : MonoBehaviour
- {
- [Tooltip("Resource-node ghost prefab (ResourceNodeAuthoring + GhostAuthoring). Reuse the expedition node prefab.")]
- public GameObject NodePrefab;
-
- [Tooltip("Live base-node target; the field refills toward this each respawn pass.")]
- [Min(1)] public int TargetCount = 10;
-
- [Tooltip("Inner scatter radius — clears the build plot corner reach (~22.6) + spawn ring.")]
- [Min(0f)] public float InnerRadius = 23.5f;
-
- [Tooltip("Outer scatter radius — stays inside the walkable boundary ring (~28.7).")]
- [Min(0f)] public float OuterRadius = 27f;
-
- [Tooltip("Server ticks (@60) between top-up passes.")]
- [Min(1)] public int RespawnIntervalTicks = 600;
-
- private class BaseFieldSpawnerBaker : Baker
- {
- public override void Bake(BaseFieldSpawnerAuthoring authoring)
- {
- var entity = GetEntity(authoring, TransformUsageFlags.None);
- AddComponent(entity, new BaseFieldSpawner
- {
- Prefab = authoring.NodePrefab != null
- ? GetEntity(authoring.NodePrefab, TransformUsageFlags.Dynamic)
- : Entity.Null,
- TargetCount = authoring.TargetCount,
- InnerRadius = authoring.InnerRadius,
- OuterRadius = authoring.OuterRadius,
- RespawnIntervalTicks = authoring.RespawnIntervalTicks,
- });
- AddComponent(entity, new BaseFieldRuntime { Epoch = 0, NextSpawnTick = 0u });
- }
- }
- }
-}
diff --git a/Assets/_Project/Scripts/Authoring/Economy/BaseFieldSpawnerAuthoring.cs.meta b/Assets/_Project/Scripts/Authoring/Economy/BaseFieldSpawnerAuthoring.cs.meta
deleted file mode 100644
index adfe96de1..000000000
--- a/Assets/_Project/Scripts/Authoring/Economy/BaseFieldSpawnerAuthoring.cs.meta
+++ /dev/null
@@ -1,2 +0,0 @@
-fileFormatVersion: 2
-guid: c4055a6a779d06949ae23b16334b810e
\ No newline at end of file
diff --git a/Assets/_Project/Scripts/Authoring/Meta.meta b/Assets/_Project/Scripts/Authoring/Meta.meta
new file mode 100644
index 000000000..5f8fb91b2
--- /dev/null
+++ b/Assets/_Project/Scripts/Authoring/Meta.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: d0503ff85f3b39f49af750a451d44e00
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/_Project/Scripts/Authoring/Meta/MetaCatalogAuthoring.cs b/Assets/_Project/Scripts/Authoring/Meta/MetaCatalogAuthoring.cs
new file mode 100644
index 000000000..5b6ba8c8a
--- /dev/null
+++ b/Assets/_Project/Scripts/Authoring/Meta/MetaCatalogAuthoring.cs
@@ -0,0 +1,85 @@
+using System;
+using System.Collections.Generic;
+using ProjectM.Simulation;
+using Unity.Collections;
+using Unity.Entities;
+using UnityEngine;
+
+namespace ProjectM.Authoring
+{
+ ///
+ /// Authoring for the PERMANENT meta-upgrade catalog singleton (place ONE in the gameplay subscene — the
+ /// BoonCatalogAuthoring pattern). An EMPTY row list bakes the code-default v1 table
+ /// () verbatim, so the subscene object needs zero property assignment
+ /// through the tooling. Ids are APPEND-ONLY (persisted in SaveData v6; a removed id's saved rows are preserved
+ /// and skipped, never crashed on).
+ ///
+ public class MetaCatalogAuthoring : MonoBehaviour
+ {
+ [Serializable]
+ public struct MetaRow
+ {
+ public byte Id;
+ [Tooltip("bit0 = Warrior, bit1 = Ranger, 3 = both.")]
+ public byte ClassMask;
+ public StatTarget Target;
+ public ModOp Op;
+ public byte MaxTier;
+ public float ValuePerTier;
+ public int BaseCost;
+ public int CostGrowth;
+ [Tooltip("0xFF (255) = no prerequisite.")]
+ public byte PrereqId;
+ public byte PrereqTier;
+ public string Name;
+ public string Desc;
+ }
+
+ [Tooltip("Leave EMPTY to bake the code-default v1 table; fill to fully replace it.")]
+ public List Rows = new List();
+
+ private class MetaCatalogBaker : Baker
+ {
+ public override void Bake(MetaCatalogAuthoring authoring)
+ {
+ var entity = GetEntity(authoring, TransformUsageFlags.None);
+
+ BlobAssetReference blob;
+ if (authoring.Rows == null || authoring.Rows.Count == 0)
+ {
+ blob = MetaCatalogData.BuildDefault();
+ }
+ else
+ {
+ var builder = new BlobBuilder(Allocator.Temp);
+ ref var root = ref builder.ConstructRoot();
+ var defs = builder.Allocate(ref root.Defs, authoring.Rows.Count);
+ for (int i = 0; i < authoring.Rows.Count; i++)
+ {
+ var r = authoring.Rows[i];
+ defs[i] = new MetaUpgradeDefBlob
+ {
+ Id = r.Id,
+ ClassMask = r.ClassMask,
+ Target = (byte)r.Target,
+ Op = (byte)r.Op,
+ MaxTier = r.MaxTier,
+ ValuePerTier = r.ValuePerTier,
+ BaseCost = r.BaseCost,
+ CostGrowth = r.CostGrowth,
+ PrereqId = r.PrereqId,
+ PrereqTier = r.PrereqTier,
+ Name = new FixedString64Bytes(r.Name ?? string.Empty),
+ Desc = new FixedString128Bytes(r.Desc ?? string.Empty),
+ };
+ }
+ blob = builder.CreateBlobAssetReference(Allocator.Persistent);
+ builder.Dispose();
+ }
+
+ AddBlobAsset(ref blob, out _);
+ AddComponent(entity, new MetaUpgradeCatalog { Value = blob });
+ }
+ }
+ }
+}
diff --git a/Assets/_Project/Scripts/Authoring/Meta/MetaCatalogAuthoring.cs.meta b/Assets/_Project/Scripts/Authoring/Meta/MetaCatalogAuthoring.cs.meta
new file mode 100644
index 000000000..38bc113ff
--- /dev/null
+++ b/Assets/_Project/Scripts/Authoring/Meta/MetaCatalogAuthoring.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 4f007f7870c0afd4e93ea5b86fd21c8b
\ No newline at end of file
diff --git a/Assets/_Project/Scripts/Authoring/Player/PlayerAuthoring.cs b/Assets/_Project/Scripts/Authoring/Player/PlayerAuthoring.cs
index a0ee277f9..d9095496b 100644
--- a/Assets/_Project/Scripts/Authoring/Player/PlayerAuthoring.cs
+++ b/Assets/_Project/Scripts/Authoring/Player/PlayerAuthoring.cs
@@ -100,6 +100,11 @@ namespace ProjectM.Authoring
AddComponent(entity, new RespawnState { RespawnTick = 0, DelayTicks = authoring.RespawnDelayTicks, InvulnTicks = authoring.RespawnInvulnTicks });
AddComponent(entity, new RespawnInvuln { UntilTick = 0 });
+
+ // Expedition redesign (the ONE player-ghost re-bake, front-loaded): the send-to-all ready-check
+ // flag + the owner-only choice-of-3 boon offer (inert until Step 9's BoonOfferSystem lights it up).
+ AddComponent(entity);
+ AddComponent(entity);
}
}
}
diff --git a/Assets/_Project/Scripts/Authoring/World/CycleDirectorAuthoring.cs b/Assets/_Project/Scripts/Authoring/World/CycleDirectorAuthoring.cs
index bd06f5b8a..7e8ecad8a 100644
--- a/Assets/_Project/Scripts/Authoring/World/CycleDirectorAuthoring.cs
+++ b/Assets/_Project/Scripts/Authoring/World/CycleDirectorAuthoring.cs
@@ -78,6 +78,13 @@ namespace ProjectM.Authoring
// runtime-spawned director ghost (server + client bake the same prefab -> hash matches), like CoreIntegrity.
AddComponent(entity, new ExpeditionObjective { State = ExpeditionObjectiveState.Idle, Remaining = 0 });
+ // Expedition redesign: the replicated run-lifecycle FSM (RunInfo, 17 [GhostField]s) + the per-class
+ // permanent-meta tier buffer (MetaTierState) BOTH land in this ONE coordinated re-bake (front-loaded
+ // ghost layout — the writer systems arrive across Steps 2–13 while the state sits inert/default).
+ // Born Staging/empty; server RunDirectorSystem / MetaSpendSystem are the sole writers.
+ AddComponent(entity, new RunInfo { Lifecycle = RunLifecycle.Staging });
+ AddBuffer(entity);
+
AddComponent(entity, new ThreatConfig
{
diff --git a/Assets/_Project/Scripts/Authoring/World/ExpeditionGateAuthoring.cs b/Assets/_Project/Scripts/Authoring/World/ExpeditionGateAuthoring.cs
deleted file mode 100644
index 327a6678d..000000000
--- a/Assets/_Project/Scripts/Authoring/World/ExpeditionGateAuthoring.cs
+++ /dev/null
@@ -1,45 +0,0 @@
-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
deleted file mode 100644
index f52499f9a..000000000
--- a/Assets/_Project/Scripts/Authoring/World/ExpeditionGateAuthoring.cs.meta
+++ /dev/null
@@ -1,2 +0,0 @@
-fileFormatVersion: 2
-guid: 22f744b59ad23834abe28fc09b661005
\ No newline at end of file
diff --git a/Assets/_Project/Scripts/Client/Building/BuildSendSystem.cs b/Assets/_Project/Scripts/Client/Building/BuildSendSystem.cs
index 9d91c5c54..bcfad3d92 100644
--- a/Assets/_Project/Scripts/Client/Building/BuildSendSystem.cs
+++ b/Assets/_Project/Scripts/Client/Building/BuildSendSystem.cs
@@ -38,13 +38,14 @@ namespace ProjectM.Client
UnityEngine.Camera _camera; // cursor -> ground re-raycast for click-to-place (resolved lazily)
GameObject _ghost; // translucent ground preview cube
Material _ghostMat;
+ MeshFilter _ghostMf; // ghost mesh swaps to the selected structure's preview mesh (HudTheme)
+ MeshRenderer _ghostMr;
+ Mesh _cubeMesh; // procedural fallback (the original preview cube)
+ byte _ghostType = 255; // last-applied preview type (255 = unset)
byte _lastSelected; // skip placing on the frame a palette click changes the selection
- // DR-042 C6a: the ability-upgrade send is RUNTIME (the HUD Aether button calls UpgradeAbility); only the
- // execute_code PLACE statics stay editor-gated. Mirrors EquipSendSystem's unconditional queue + drain.
- static int s_PendingUpgrades = 0;
- /// Runtime hook (HUD Aether button) + execute_code: queue an ability-damage upgrade.
- public static void UpgradeAbility() => s_PendingUpgrades++;
+ // (Step 11: UpgradeAbility/s_PendingUpgrades RETIRED with the AbilityUpgradeRequest wire — in-run boons +
+ // the base meta-shop replaced the Aether damage upgrade.)des++;
#if UNITY_EDITOR
struct PendingBuild { public byte Type; public int CellX; public int CellZ; public byte Direction; }
@@ -113,17 +114,8 @@ namespace ProjectM.Client
foreach (var (key, type) in s_BuildHotkeys)
if (keyboard[key].wasPressedThisFrame && TryGetLocalPlayerCell(out int2 cell))
SendBuild(connection, type, cell.x, cell.y, type == StructureType.Conveyor ? s_ConveyorDir : (byte)0);
- if (keyboard.uKey.wasPressedThisFrame)
- SendUpgrade(connection);
}
- // DR-042 C6a: the ability-upgrade drain runs at RUNTIME (the HUD Aether button enqueues via UpgradeAbility);
- // only the execute_code PLACE drain stays editor-gated.
- while (s_PendingUpgrades > 0)
- {
- s_PendingUpgrades--;
- SendUpgrade(connection);
- }
#if UNITY_EDITOR
while (s_PendingBuild.Count > 0)
{
@@ -185,7 +177,7 @@ namespace ProjectM.Client
byte reason = BuildPreviewMath.Evaluate(anchor, targetCell, occupied, LedgerOre(), cost);
bool valid = reason == BuildPreviewMath.Valid;
- ShowGhost(BaseGridMath.CellToWorld(anchor, targetCell), anchor.CellSize, valid);
+ ShowGhost(BaseGridMath.CellToWorld(anchor, targetCell), anchor.CellSize, valid, sel);;
// Place on a left-click (valid, not the selecting click).
if (valid && !justSelected && mouse.leftButton.wasPressedThisFrame)
@@ -208,16 +200,44 @@ namespace ProjectM.Client
return int.MaxValue;
}
- // ---- Ground ghost preview (procedural translucent cube, like AimReticleSystem's reticle) ----
- void ShowGhost(float3 center, float cellSize, bool valid)
+ // ---- Ground ghost preview: the selected structure's REAL mesh (HudTheme, build-safe serialized refs)
+ // tinted translucent green/red; falls back to the original procedural cube when no mesh is authored. ----
+ void ShowGhost(float3 center, float cellSize, bool valid, byte type)
{
EnsureGhost();
- _ghost.transform.position = (Vector3)center + Vector3.up * 0.5f;
- _ghost.transform.localScale = new Vector3(cellSize * 0.9f, 1f, cellSize * 0.9f);
+ ApplyGhostMesh(type);
+ if (_ghostMf.sharedMesh == _cubeMesh)
+ {
+ _ghost.transform.position = (Vector3)center + Vector3.up * 0.5f;
+ _ghost.transform.localScale = new Vector3(cellSize * 0.9f, 1f, cellSize * 0.9f);
+ }
+ else
+ {
+ // preview meshes are authored real-size with a ground pivot (SM_Turret_01 / SM_Wall_01 / SM_Fabricator_01)
+ _ghost.transform.position = (Vector3)center;
+ _ghost.transform.localScale = Vector3.one;
+ }
_ghostMat.color = valid ? new Color(0.3f, 1f, 0.45f, 0.35f) : new Color(1f, 0.32f, 0.26f, 0.35f);
if (!_ghost.activeSelf) _ghost.SetActive(true);
}
+ // Swap the ghost's mesh when the palette selection changes (255 = unset sentinel; no selection is 0 -> cube).
+ void ApplyGhostMesh(byte type)
+ {
+ if (type == _ghostType) return;
+ _ghostType = type;
+ var theme = HudTheme.Get();
+ Mesh mesh = theme != null ? theme.StructureGhostMesh(type) : null;
+ if (mesh == null) mesh = _cubeMesh;
+ _ghostMf.sharedMesh = mesh;
+ if (_ghostMr.sharedMaterials.Length != mesh.subMeshCount)
+ {
+ var mats = new Material[mesh.subMeshCount]; // one translucent mat per submesh so the whole preview tints
+ for (int i = 0; i < mats.Length; i++) mats[i] = _ghostMat;
+ _ghostMr.sharedMaterials = mats;
+ }
+ }
+
void HideGhost()
{
if (_ghost != null && _ghost.activeSelf) _ghost.SetActive(false);
@@ -233,10 +253,13 @@ namespace ProjectM.Client
_ghost.name = "~BuildGhost";
var col = _ghost.GetComponent();
if (col != null) Object.Destroy(col);
- var mr = _ghost.GetComponent();
- mr.sharedMaterial = _ghostMat;
- mr.shadowCastingMode = UnityEngine.Rendering.ShadowCastingMode.Off;
- mr.receiveShadows = false;
+ _ghostMf = _ghost.GetComponent();
+ _cubeMesh = _ghostMf.sharedMesh;
+ _ghostType = 255;
+ _ghostMr = _ghost.GetComponent();
+ _ghostMr.sharedMaterial = _ghostMat;
+ _ghostMr.shadowCastingMode = UnityEngine.Rendering.ShadowCastingMode.Off;
+ _ghostMr.receiveShadows = false;
_ghost.SetActive(false);
}
@@ -270,12 +293,6 @@ namespace ProjectM.Client
EntityManager.AddComponentData(e, new BuildPlaceRequest { StructureType = type, CellX = cellX, CellZ = cellZ, Direction = direction });
EntityManager.AddComponentData(e, new SendRpcCommandRequest { TargetConnection = connection });
}
-
- void SendUpgrade(Entity connection)
- {
- var e = EntityManager.CreateEntity();
- EntityManager.AddComponentData(e, new AbilityUpgradeRequest());
- EntityManager.AddComponentData(e, new SendRpcCommandRequest { TargetConnection = connection });
- }
+ // (Step 11: the Aether ability-upgrade sender was RETIRED with AbilityUpgradeRequest — boons replaced it.)
}
}
diff --git a/Assets/_Project/Scripts/Client/Combat/BoonSendSystem.cs b/Assets/_Project/Scripts/Client/Combat/BoonSendSystem.cs
new file mode 100644
index 000000000..f92747cdd
--- /dev/null
+++ b/Assets/_Project/Scripts/Client/Combat/BoonSendSystem.cs
@@ -0,0 +1,49 @@
+using ProjectM.Simulation;
+using Unity.Entities;
+using Unity.NetCode;
+using UnityEngine;
+
+namespace ProjectM.Client
+{
+ ///
+ /// Client-side boon-pick sender: a static enqueue (the Step-14 3-card modal / execute_code) drained into
+ /// RPCs. Carries only the option INDEX — the server resolves it against the
+ /// sender's own authoritative BoonOffer and validates lifecycle/pending, so a stale or forged pick is
+ /// simply dropped. Statics reset on play-enter (the stale-bridge hazard).
+ ///
+ [WorldSystemFilter(WorldSystemFilterFlags.ClientSimulation)]
+ public partial class BoonSendSystem : SystemBase
+ {
+ static int s_Pending;
+ static byte s_PendingIndex;
+
+ [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)]
+ static void ResetStatics()
+ {
+ s_Pending = 0;
+ s_PendingIndex = 0;
+ }
+
+ /// Queue a boon pick (0/1/2). The HUD card click + execute_code drive this.
+ public static void PickBoon(byte optionIndex)
+ {
+ s_PendingIndex = optionIndex;
+ s_Pending++;
+ }
+
+ protected override void OnCreate()
+ {
+ RequireForUpdate();
+ }
+
+ protected override void OnUpdate()
+ {
+ while (s_Pending > 0)
+ {
+ s_Pending--;
+ var req = EntityManager.CreateEntity(typeof(BoonPickRequest), typeof(SendRpcCommandRequest));
+ EntityManager.SetComponentData(req, new BoonPickRequest { Index = s_PendingIndex });
+ }
+ }
+ }
+}
diff --git a/Assets/_Project/Scripts/Client/Combat/BoonSendSystem.cs.meta b/Assets/_Project/Scripts/Client/Combat/BoonSendSystem.cs.meta
new file mode 100644
index 000000000..ad7427a4f
--- /dev/null
+++ b/Assets/_Project/Scripts/Client/Combat/BoonSendSystem.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: d7e90bc7230614845817b81f0f7b70f0
\ No newline at end of file
diff --git a/Assets/_Project/Scripts/Client/Meta.meta b/Assets/_Project/Scripts/Client/Meta.meta
new file mode 100644
index 000000000..2e19226be
--- /dev/null
+++ b/Assets/_Project/Scripts/Client/Meta.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 60aece22e94371c468102dbf35c3fe47
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/_Project/Scripts/Client/Meta/MetaSpendSendSystem.cs b/Assets/_Project/Scripts/Client/Meta/MetaSpendSendSystem.cs
new file mode 100644
index 000000000..37262116f
--- /dev/null
+++ b/Assets/_Project/Scripts/Client/Meta/MetaSpendSendSystem.cs
@@ -0,0 +1,41 @@
+using System.Collections.Generic;
+using ProjectM.Simulation;
+using Unity.Entities;
+using Unity.NetCode;
+using UnityEngine;
+
+namespace ProjectM.Client
+{
+ ///
+ /// Client-side meta-purchase sender: a static enqueue (the Step-14 base meta-shop panel / execute_code) drained
+ /// into RPCs. Carries only the upgrade ID — the tier is server-computed and the
+ /// server re-validates everything (Staging gate, class mask, MaxTier, prereq, Aether affordability), so a stale
+ /// or forged request is simply dropped. A real QUEUE (unlike the single-slot boon pick) — two rapid clicks on
+ /// two different shop rows must both arrive. Statics reset on play-enter (the stale-bridge hazard).
+ ///
+ [WorldSystemFilter(WorldSystemFilterFlags.ClientSimulation)]
+ public partial class MetaSpendSendSystem : SystemBase
+ {
+ static readonly Queue s_Queue = new();
+
+ [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)]
+ static void ResetStatics() => s_Queue.Clear();
+
+ /// Queue a permanent-upgrade purchase by catalog id. The shop row click + execute_code drive this.
+ public static void RequestPurchase(byte upgradeId) => s_Queue.Enqueue(upgradeId);
+
+ protected override void OnCreate()
+ {
+ RequireForUpdate();
+ }
+
+ protected override void OnUpdate()
+ {
+ while (s_Queue.Count > 0)
+ {
+ var req = EntityManager.CreateEntity(typeof(MetaSpendRequest), typeof(SendRpcCommandRequest));
+ EntityManager.SetComponentData(req, new MetaSpendRequest { UpgradeId = s_Queue.Dequeue() });
+ }
+ }
+ }
+}
diff --git a/Assets/_Project/Scripts/Client/Meta/MetaSpendSendSystem.cs.meta b/Assets/_Project/Scripts/Client/Meta/MetaSpendSendSystem.cs.meta
new file mode 100644
index 000000000..06dc91328
--- /dev/null
+++ b/Assets/_Project/Scripts/Client/Meta/MetaSpendSendSystem.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 9f3197b4ede6dea41a6c4c93ffa51b42
\ No newline at end of file
diff --git a/Assets/_Project/Scripts/Client/Onboarding/OnboardingStepMath.cs b/Assets/_Project/Scripts/Client/Onboarding/OnboardingStepMath.cs
index 494d744ff..3623b2a22 100644
--- a/Assets/_Project/Scripts/Client/Onboarding/OnboardingStepMath.cs
+++ b/Assets/_Project/Scripts/Client/Onboarding/OnboardingStepMath.cs
@@ -102,9 +102,9 @@ namespace ProjectM.Client
case Mine: return attack + " — Attack the glowing Ore to mine it";
case Build: return build + " — open Build, then place a Turret by your Core";
case Fabricator: return "Build a Fabricator — turrets need Charge (Ore → ammo)";
- case Gate: return "Reach the Expedition Gate — clearing it charges the Engine";
- case Clear: return "Clear the zone — defeat every enemy";
- case Return: return "Return through the gate — bank your clear (+1 Engine charge)";
+ case Gate: return "Press T to READY UP — when everyone is ready, the run launches";
+ case Clear: return "Clear each room — pick a boon, choose your path, reach the boss";
+ case Return: return "Fell the boss — you return home and the Engine charges (+1)";;
case Defend: return "Defend the Core! — hold the line through the siege";
case Done: return "You've got it. Clear expeditions to fill the Engine and win.";
default: return "";
diff --git a/Assets/_Project/Scripts/Client/Onboarding/OnboardingSystem.cs b/Assets/_Project/Scripts/Client/Onboarding/OnboardingSystem.cs
index 2dc64f077..a89e6a0f0 100644
--- a/Assets/_Project/Scripts/Client/Onboarding/OnboardingSystem.cs
+++ b/Assets/_Project/Scripts/Client/Onboarding/OnboardingSystem.cs
@@ -215,13 +215,9 @@ namespace ProjectM.Client
}
return found;
}
- // base gate (go) lives in the base region; expedition gate (return) lives past the region split.
- bool wantBase = kind == OnboardingStepMath.PointerBaseGate;
- foreach (var lt in SystemAPI.Query>().WithAll())
- {
- var p = lt.ValueRO.Position;
- if ((p.x < ExpeditionRegionXMin) == wantBase) { target = p; return true; }
- }
+ // Step 11: the walk-in ExpeditionGate was RETIRED (the ready-check launches runs), so gate pointers
+ // have no world target — hide the arrow; the step text still teaches. Step 14's HUD pass re-points
+ // onboarding at the READY panel instead.
return false;
}
diff --git a/Assets/_Project/Scripts/Client/Presentation/HudSystem.cs b/Assets/_Project/Scripts/Client/Presentation/HudSystem.cs
index 5850181d3..9e0b1564f 100644
--- a/Assets/_Project/Scripts/Client/Presentation/HudSystem.cs
+++ b/Assets/_Project/Scripts/Client/Presentation/HudSystem.cs
@@ -68,8 +68,20 @@ namespace ProjectM.Client
// END-2: terminal win/loss banner (observes the replicated RunOutcome; latched server-side).
VisualElement _runBanner;
Label _runBannerText, _runBannerSub;
- // DR-042 C6a: the Aether ability-upgrade button (was U-key only) + its live affordability tint.
- Button _upgradeBtn;
+ // Step 14 (expedition redesign): the choice-of-3 boon modal + the route-choice panel. Both are
+ // observe-only readers of replicated state (BoonOffer via GhostOwnerIsLocal; RunInfo RouteOpt*); clicks
+ // enqueue through the client send-systems' statics. Built lazily on first show.
+ VisualElement _boonModal, _boonCardRow;
+ VisualElement _routePanel, _routeBtnRow;
+ Label _routeTitle;
+ byte _boonShownFor; // last (Option0^Option1^Option2 ^ room) signature the modal was built for
+ bool _boonModalBuilt, _routePanelBuilt;
+ // Step 14 (meta shop): Staging-only permanent-upgrade shop (replicated MetaTierState + ledger Aether;
+ // row clicks enqueue MetaSpendSendSystem.RequestPurchase — the server re-validates everything).
+ VisualElement _metaPanel, _metaRowsHost;
+ Label _metaShopTitle;
+ bool _metaShopBuilt;
+ int _metaShownFor; // last (class, tiers, aether) signature the shop rows were built for
readonly List _pips = new();
@@ -171,41 +183,97 @@ namespace ProjectM.Client
_cycleText.text = "";
}
- // ---- Location + gate hint (banner sub-line) ----
- var cam = Camera.main;
+ // ---- Location line (banner sub-line) — Step 14: driven by the replicated RunInfo lifecycle FSM ----
+ // (the old camera-X + walk-in-gate copy died with the gate; siege/final overrides below still win).
+ var cam = Camera.main; // camera-X region signal still feeds downstream panels (atmosphere/threat)
bool onExpedition = cam != null && cam.transform.position.x > ExpeditionRegionXMin;
- _locationText.text = onExpedition
- ? "ON EXPEDITION - carve the frontier, then return"
- : finalSiege
- ? "FINAL SIEGE - hold the Engine, this is the last stand"
- : siege
- ? "DEFEND THE BASE - hold the line"
- : "MINE THE CRYSTALS - any attack harvests Ore, then BUILD";
- _locationText.style.color = onExpedition ? new Color(1f, 0.8f, 0.4f)
- : finalSiege ? new Color(1f, 0.3f, 0.25f)
- : siege ? new Color(1f, 0.55f, 0.4f) : new Color(0.6f, 0.95f, 0.7f);
- // DR-042 C7 (gate prompt) + C7b (objective readout): the expedition is the win-driver, so signpost it.
- // Reads the REPLICATED ExpeditionObjective summary (cross-region safe). Lower priority than the siege /
- // cold-turret / overrun overrides below, which still win.
- if (SystemAPI.TryGetSingleton(out var obj))
+ bool haveRun = SystemAPI.TryGetSingleton(out var runInfo);
+ SystemAPI.TryGetSingleton(out var obj);
+ if (haveRun && !siege && !finalSiege)
{
- if (onExpedition)
+ switch (runInfo.Lifecycle)
{
- if (obj.State == ExpeditionObjectiveState.Cleared)
- { _locationText.text = "ZONE CLEARED - return to base to claim"; _locationText.style.color = new Color(0.5f, 1f, 0.6f); }
- else if (obj.State == ExpeditionObjectiveState.Active)
- { _locationText.text = "CLEAR THE ZONE - " + obj.Remaining + " enemies remaining"; _locationText.style.color = new Color(1f, 0.8f, 0.4f); }
- }
- else if (!siege)
- {
- if (obj.State == ExpeditionObjectiveState.Cleared)
- { _locationText.text = "EXPEDITION CLEARED - return to claim your reward"; _locationText.style.color = new Color(0.5f, 1f, 0.6f); }
- else if (obj.State == ExpeditionObjectiveState.Active)
- { _locationText.text = "EXPEDITION IN PROGRESS - " + obj.Remaining + " enemies remaining"; _locationText.style.color = new Color(1f, 0.8f, 0.4f); }
- else
- { _locationText.text = "GO TO THE EXPEDITION GATE - clear a sortie to advance the Engine"; _locationText.style.color = new Color(0.55f, 0.85f, 1f); }
+ case RunLifecycle.Staging:
+ {
+ // Client counts the party's replicated ready flags (send-to-all) for the N/M readout.
+ int total = 0, readyCount = 0;
+ foreach (var pr in SystemAPI.Query>().WithAll())
+ {
+ total++;
+ if (pr.ValueRO.Value != 0) readyCount++;
+ }
+ _locationText.text = "READY UP [T] - " + readyCount + "/" + Mathf.Max(total, 1)
+ + " ready - launch a run to advance the Engine";
+ _locationText.style.color = new Color(0.55f, 0.85f, 1f);
+ break;
+ }
+ case RunLifecycle.Launching:
+ {
+ uint nowTick = SystemAPI.TryGetSingleton(out var ntime) && ntime.ServerTick.IsValid
+ ? ntime.ServerTick.TickIndexForValidTick : 0u;
+ int secs = runInfo.LaunchTick != 0 && nowTick != 0
+ ? Mathf.Max(0, (int)((runInfo.LaunchTick - nowTick) / 60u) + 1) : 0;
+ _locationText.text = "LAUNCHING IN " + secs + " - un-ready [T] to abort";
+ _locationText.style.color = new Color(1f, 0.9f, 0.4f);
+ break;
+ }
+ case RunLifecycle.InRoom:
+ {
+ string room = "ROOM " + (runInfo.CurrentRoom + 1) + "/" + runInfo.RoomCount
+ + " " + RoomTypeLabel(runInfo.CurrentRoomType);
+ _locationText.text = obj.State == ExpeditionObjectiveState.Active
+ ? room + " - " + obj.Remaining + " enemies remaining"
+ : room + " - clear it to advance";
+ _locationText.style.color = new Color(1f, 0.8f, 0.4f);
+ break;
+ }
+ case RunLifecycle.RoomReward:
+ _locationText.text = "ROOM CLEARED - choose your boon";
+ _locationText.style.color = new Color(0.5f, 1f, 0.6f);
+ break;
+ case RunLifecycle.RouteSelect:
+ _locationText.text = "CHOOSE YOUR PATH";
+ _locationText.style.color = new Color(0.55f, 0.85f, 1f);
+ break;
+ case RunLifecycle.Returning:
+ _locationText.text = "RETURNING HOME...";
+ _locationText.style.color = new Color(0.7f, 0.9f, 1f);
+ break;
}
}
+ else if (!haveRun)
+ {
+ _locationText.text = finalSiege
+ ? "FINAL SIEGE - hold the Engine, this is the last stand"
+ : siege ? "DEFEND THE BASE - hold the line"
+ : "MINE THE CRYSTALS - any attack harvests Ore, then BUILD";
+ _locationText.style.color = finalSiege ? new Color(1f, 0.3f, 0.25f)
+ : siege ? new Color(1f, 0.55f, 0.4f) : new Color(0.6f, 0.95f, 0.7f);
+ }
+ else
+ {
+ _locationText.text = finalSiege
+ ? "FINAL SIEGE - hold the Engine, this is the last stand"
+ : "DEFEND THE BASE - hold the line";
+ _locationText.style.color = finalSiege ? new Color(1f, 0.3f, 0.25f) : new Color(1f, 0.55f, 0.4f);
+ }
+
+
+ // ---- Step 14: the choice-of-3 boon modal + the route-choice panel (observe replicated state; the
+ // card/button clicks enqueue through the client send-systems' statics) ----
+ BoonOffer localOffer = default;
+ bool hasOffer = false;
+ foreach (var off in SystemAPI.Query>().WithAll())
+ {
+ localOffer = off.ValueRO;
+ hasOffer = true;
+ break;
+ }
+ BlobAssetReference boonPool = default;
+ if (SystemAPI.TryGetSingleton(out var bcat))
+ boonPool = bcat.Value;
+ UpdateBoonModal(localOffer, hasOffer && localOffer.Pending == 1, boonPool);
+ UpdateRoutePanel(haveRun ? runInfo : default);
// ---- Goal (hex-pip meter, or a continuous bar for large targets) ----
if (SystemAPI.TryGetSingleton(out var goal))
@@ -255,9 +323,31 @@ namespace ProjectM.Client
_oreNum.text = ore.ToString();
_bioNum.text = bio.ToString();
_chargeNum.text = charge.ToString();
+
+ // ---- Step 14 (meta shop): Staging-only permanent-upgrade shop for the LOCAL class. Class derives from
+ // the replicated AbilityRef (tracks the dev class-switch; PlayerClass is server-only); tiers from the
+ // replicated MetaTierState record on the director ghost; Aether from the ledger read above. ----
+ byte localClass = ClassTraits.WarriorClass;
+ bool haveLocalPlayer = false;
+ foreach (var ar in SystemAPI.Query>().WithAll())
+ {
+ localClass = ClassTraits.ClassForAbility(ar.ValueRO.Id);
+ haveLocalPlayer = true;
+ break;
+ }
+ bool metaShow = false;
+ BlobAssetReference metaPool = default;
+ DynamicBuffer metaRecord = default;
+ if (haveRun && runInfo.Lifecycle == RunLifecycle.Staging && haveLocalPlayer && !siege
+ && SystemAPI.TryGetSingleton(out var metaCat) && metaCat.Value.IsCreated
+ && SystemAPI.TryGetSingletonBuffer(out metaRecord, true))
+ {
+ metaPool = metaCat.Value;
+ metaShow = true;
+ }
+ UpdateMetaShop(metaShow, localClass, aether, metaPool, metaRecord);
// DR-042 C6a: dim the Aether upgrade button when it isn't affordable (cost is a compile-time const).
- if (_upgradeBtn != null)
- _upgradeBtn.style.opacity = aether >= Tuning.AbilityUpgradeCostAmount ? 1f : 0.5f;
+ // (Step 11: upgrade-button affordability tint retired with the button.)
// EB-2 quiet-turret cue (GLOBAL, not per-turret, so the deterministic Charge split never reads as one
// broken turret): a dry base during a siege tells the player to build a Fabricator.
if (siege && charge == 0 && !onExpedition)
@@ -848,9 +938,8 @@ namespace ProjectM.Client
strip.Add(ResourceChip(null, ChargeViolet, "0", out _chargeNum, 26, 20)); // EB-2 turret ammo (flat violet, no icon)
// DR-042 C6a: the only Aether sink (ability-damage upgrade) gets a visible, clickable button (was U-key
// only). The Button element handles its own picking even though the HUD root Ignores clicks.
- _upgradeBtn = MenuUi.Button("UPGRADE DMG (" + Tuning.AbilityUpgradeCostAmount + " AETHER)", BuildSendSystem.UpgradeAbility);
- _upgradeBtn.style.marginLeft = 18;
- strip.Add(_upgradeBtn);
+ // (Step 11: the Aether UPGRADE-DMG button was RETIRED with AbilityUpgradeRequest — the choice-of-3
+ // boon modal + the base meta-shop (Step 14) replace it.)
root.Add(strip);
}
@@ -1138,5 +1227,243 @@ namespace ProjectM.Client
default: return "?";
}
}
+
+ // ==== Step 14: boon modal + route panel (lazy-built overlays; clicks -> client send statics) ====
+
+ byte _routeShownFor; // last (room ^ options) signature the route buttons were built for
+
+ static string RoomTypeLabel(byte roomType) => roomType == RoomTypeId.Boss ? "[BOSS]"
+ : roomType == RoomTypeId.Elite ? "[ELITE]"
+ : roomType == RoomTypeId.Reward ? "[REWARD]" : "[COMBAT]";
+
+ void UpdateBoonModal(BoonOffer offer, bool show, BlobAssetReference pool)
+ {
+ if (!show || !pool.IsCreated)
+ {
+ if (_boonModal != null) _boonModal.style.display = DisplayStyle.None;
+ _boonShownFor = 0;
+ return;
+ }
+ var root = _doc != null ? _doc.rootVisualElement : null;
+ if (root == null) return;
+ if (!_boonModalBuilt)
+ {
+ BuildBoonModal(root);
+ _boonModalBuilt = true;
+ }
+
+ // Rebuild the three cards only when the offer actually changes (a new room's deal).
+ byte sig = (byte)(offer.Option0 ^ (offer.Option1 * 3) ^ (offer.Option2 * 7));
+ if (sig == 0) sig = 1;
+ if (_boonShownFor != sig)
+ {
+ _boonCardRow.Clear();
+ ref var defs = ref pool.Value;
+ for (byte k = 0; k < 3; k++)
+ {
+ byte id = k == 2 ? offer.Option2 : k == 1 ? offer.Option1 : offer.Option0;
+ int idx = BoonMath.FindDef(ref defs, id);
+ string title = idx >= 0 ? defs.Defs[idx].Name.ToString() : ("BOON " + id);
+ string desc = idx >= 0 ? defs.Defs[idx].Desc.ToString() : "";
+ byte pick = k; // capture a COPY into the closure, never the loop variable
+ var card = MenuUi.Button(title + "\n" + desc, () => BoonSendSystem.PickBoon(pick));
+ card.style.width = 200;
+ card.style.height = 96;
+ card.style.marginLeft = 8;
+ card.style.marginRight = 8;
+ card.style.whiteSpace = WhiteSpace.Normal;
+ _boonCardRow.Add(card);
+ }
+ _boonShownFor = sig;
+ }
+ _boonModal.style.display = DisplayStyle.Flex;
+ }
+
+ void BuildBoonModal(VisualElement root)
+ {
+ _boonModal = new VisualElement { pickingMode = PickingMode.Ignore };
+ _boonModal.style.position = Position.Absolute;
+ _boonModal.style.left = 0; _boonModal.style.right = 0;
+ _boonModal.style.top = 0; _boonModal.style.bottom = 0;
+ _boonModal.style.alignItems = Align.Center;
+ _boonModal.style.justifyContent = Justify.Center;
+ _boonModal.style.display = DisplayStyle.None;
+
+ var box = new VisualElement();
+ box.style.backgroundColor = new Color(0.07f, 0.09f, 0.12f, 0.96f);
+ box.style.borderTopLeftRadius = 10; box.style.borderTopRightRadius = 10;
+ box.style.borderBottomLeftRadius = 10; box.style.borderBottomRightRadius = 10;
+ box.style.paddingLeft = 18; box.style.paddingRight = 18;
+ box.style.paddingTop = 14; box.style.paddingBottom = 16;
+ box.style.alignItems = Align.Center;
+
+ var title = new Label("ROOM CLEARED — CHOOSE A BOON");
+ title.style.color = new Color(0.6f, 1f, 0.7f);
+ title.style.fontSize = 18;
+ title.style.unityFontStyleAndWeight = FontStyle.Bold;
+ title.style.marginBottom = 12;
+ box.Add(title);
+
+ _boonCardRow = new VisualElement();
+ _boonCardRow.style.flexDirection = FlexDirection.Row;
+ box.Add(_boonCardRow);
+
+ _boonModal.Add(box);
+ root.Add(_boonModal);
+ }
+
+ void UpdateRoutePanel(RunInfo runInfo)
+ {
+ // Keyed on the LIFECYCLE (never RouteOptionCount alone — the review's D-F6 criterion).
+ bool show = runInfo.Lifecycle == RunLifecycle.RouteSelect && runInfo.RouteOptionCount > 0;
+ if (!show)
+ {
+ if (_routePanel != null) _routePanel.style.display = DisplayStyle.None;
+ _routeShownFor = 0;
+ return;
+ }
+ var root = _doc != null ? _doc.rootVisualElement : null;
+ if (root == null) return;
+ if (!_routePanelBuilt)
+ {
+ BuildRoutePanel(root);
+ _routePanelBuilt = true;
+ }
+
+ byte sig = (byte)((runInfo.CurrentRoom + 1) ^ (runInfo.RouteOpt0Type * 3)
+ ^ (runInfo.RouteOpt1Type * 5) ^ (runInfo.RouteOpt2Type * 7) ^ runInfo.RouteOptionCount);
+ if (sig == 0) sig = 1;
+ if (_routeShownFor != sig)
+ {
+ _routeBtnRow.Clear();
+ for (byte k = 0; k < runInfo.RouteOptionCount && k < 3; k++)
+ {
+ byte type = k == 2 ? runInfo.RouteOpt2Type : k == 1 ? runInfo.RouteOpt1Type : runInfo.RouteOpt0Type;
+ byte pick = k; // closure copy
+ var b = MenuUi.Button("→ " + RoomTypeLabel(type), () => RouteSendSystem.PickRoute(pick));
+ b.style.marginLeft = 6;
+ b.style.marginRight = 6;
+ _routeBtnRow.Add(b);
+ }
+ _routeTitle.text = "CHOOSE YOUR PATH — room " + (runInfo.CurrentRoom + 2) + "/" + runInfo.RoomCount;
+ _routeShownFor = sig;
+ }
+ _routePanel.style.display = DisplayStyle.Flex;
+ }
+
+ void BuildRoutePanel(VisualElement root)
+ {
+ _routePanel = new VisualElement { pickingMode = PickingMode.Ignore };
+ _routePanel.style.position = Position.Absolute;
+ _routePanel.style.left = 0; _routePanel.style.right = 0;
+ _routePanel.style.bottom = 120;
+ _routePanel.style.alignItems = Align.Center;
+ _routePanel.style.display = DisplayStyle.None;
+
+ var box = new VisualElement();
+ box.style.backgroundColor = new Color(0.07f, 0.09f, 0.12f, 0.94f);
+ box.style.borderTopLeftRadius = 10; box.style.borderTopRightRadius = 10;
+ box.style.borderBottomLeftRadius = 10; box.style.borderBottomRightRadius = 10;
+ box.style.paddingLeft = 16; box.style.paddingRight = 16;
+ box.style.paddingTop = 10; box.style.paddingBottom = 12;
+ box.style.alignItems = Align.Center;
+
+ _routeTitle = new Label("CHOOSE YOUR PATH");
+ _routeTitle.style.color = new Color(0.55f, 0.85f, 1f);
+ _routeTitle.style.fontSize = 15;
+ _routeTitle.style.unityFontStyleAndWeight = FontStyle.Bold;
+ _routeTitle.style.marginBottom = 8;
+ box.Add(_routeTitle);
+
+ _routeBtnRow = new VisualElement();
+ _routeBtnRow.style.flexDirection = FlexDirection.Row;
+ box.Add(_routeBtnRow);
+
+ _routePanel.Add(box);
+ root.Add(_routePanel);
+ }
+
+ void UpdateMetaShop(bool show, byte classId, int aether,
+ BlobAssetReference pool, DynamicBuffer record)
+ {
+ if (!show)
+ {
+ if (_metaPanel != null) _metaPanel.style.display = DisplayStyle.None;
+ _metaShownFor = 0;
+ return;
+ }
+ var root = _doc != null ? _doc.rootVisualElement : null;
+ if (root == null) return;
+ if (!_metaShopBuilt)
+ {
+ BuildMetaShop(root);
+ _metaShopBuilt = true;
+ }
+
+ // Rebuild the rows only when class / owned tiers / affordability actually change (Staging-only, <=8 rows).
+ int sig = classId * 131 ^ aether * 31;
+ for (int i = 0; i < record.Length; i++)
+ sig ^= (record[i].ClassId * 7 + record[i].UpgradeId * 13 + record[i].Tier) * (i + 3);
+ if (sig == 0) sig = 1;
+ if (_metaShownFor != sig)
+ {
+ _metaRowsHost.Clear();
+ _metaShopTitle.text = (classId == ClassTraits.RangerClass ? "RANGER" : "WARRIOR")
+ + " PERMANENT UPGRADES - AETHER " + aether;
+ ref var defs = ref pool.Value;
+ byte classBit = BoonMath.MaskFor(classId);
+ for (int d = 0; d < defs.Defs.Length; d++)
+ {
+ if ((defs.Defs[d].ClassMask & classBit) == 0) continue;
+ byte id = defs.Defs[d].Id;
+ byte owned = MetaMath.TierOf(record, classId, id);
+ bool maxed = owned >= defs.Defs[d].MaxTier;
+ int cost = MetaMath.CostForTier(in defs.Defs[d], owned);
+ string label = defs.Defs[d].Name.ToString() + " [" + owned + "/" + defs.Defs[d].MaxTier + "]"
+ + (maxed ? " MAXED" : " - " + cost + " Aether")
+ + "\n" + defs.Defs[d].Desc.ToString();
+ byte buyId = id; // closure copy, never the loop variable
+ var row = MenuUi.Button(label, () => MetaSpendSendSystem.RequestPurchase(buyId));
+ row.style.width = 290;
+ row.style.marginBottom = 4;
+ row.style.whiteSpace = WhiteSpace.Normal;
+ row.style.unityTextAlign = TextAnchor.MiddleLeft;
+ row.SetEnabled(!maxed && aether >= cost); // honest UI; the server re-validates everything anyway
+ _metaRowsHost.Add(row);
+ }
+ _metaShownFor = sig;
+ }
+ _metaPanel.style.display = DisplayStyle.Flex;
+ }
+
+ void BuildMetaShop(VisualElement root)
+ {
+ _metaPanel = new VisualElement { pickingMode = PickingMode.Ignore };
+ _metaPanel.style.position = Position.Absolute;
+ _metaPanel.style.right = 12;
+ _metaPanel.style.top = Length.Percent(22);
+ _metaPanel.style.alignItems = Align.FlexEnd;
+ _metaPanel.style.display = DisplayStyle.None;
+
+ var box = new VisualElement();
+ box.style.backgroundColor = new Color(0.07f, 0.09f, 0.12f, 0.92f);
+ box.style.borderTopLeftRadius = 10; box.style.borderTopRightRadius = 10;
+ box.style.borderBottomLeftRadius = 10; box.style.borderBottomRightRadius = 10;
+ box.style.paddingLeft = 12; box.style.paddingRight = 12;
+ box.style.paddingTop = 10; box.style.paddingBottom = 10;
+
+ _metaShopTitle = new Label("PERMANENT UPGRADES");
+ _metaShopTitle.style.color = AetherCyan;
+ _metaShopTitle.style.fontSize = 14;
+ _metaShopTitle.style.unityFontStyleAndWeight = FontStyle.Bold;
+ _metaShopTitle.style.marginBottom = 8;
+ box.Add(_metaShopTitle);
+
+ _metaRowsHost = new VisualElement();
+ box.Add(_metaRowsHost);
+
+ _metaPanel.Add(box);
+ root.Add(_metaPanel);
+ }
}
}
diff --git a/Assets/_Project/Scripts/Client/Presentation/WorldAtmosphereSystem.cs b/Assets/_Project/Scripts/Client/Presentation/WorldAtmosphereSystem.cs
index daa0f3214..a69deb415 100644
--- a/Assets/_Project/Scripts/Client/Presentation/WorldAtmosphereSystem.cs
+++ b/Assets/_Project/Scripts/Client/Presentation/WorldAtmosphereSystem.cs
@@ -37,6 +37,30 @@ namespace ProjectM.Client
Color expFog = cfg != null ? cfg.ExpeditionFogColor : new Color(0.851f, 0.549f, 0.227f, 1f);
float expDen = cfg != null ? cfg.ExpeditionFogDensity : 0.010f;
Color expAmb = cfg != null ? cfg.ExpeditionAmbientSky : new Color(0.910f, 0.769f, 0.604f, 1f);
+ // Step 14 (expedition redesign): the EXPEDITION-side palette keys on the replicated per-room biome
+ // (RunInfo.CurrentBiome) so every room re-themes — the camera-X blend still owns the base↔run fade.
+ // Palettes are code consts (cosmetic; per-biome config knobs can layer on later without churn).
+ if (SystemAPI.TryGetSingleton(out var runInfo)
+ && runInfo.Lifecycle != ProjectM.Simulation.RunLifecycle.Staging)
+ {
+ switch (runInfo.CurrentBiome)
+ {
+ case ProjectM.Simulation.RoomBiomeId.Meadow:
+ expFog = new Color(0.62f, 0.82f, 0.62f, 1f); expDen = 0.008f;
+ expAmb = new Color(0.68f, 0.88f, 0.70f, 1f);
+ break;
+ case ProjectM.Simulation.RoomBiomeId.Cavern:
+ expFog = new Color(0.42f, 0.48f, 0.60f, 1f); expDen = 0.014f;
+ expAmb = new Color(0.50f, 0.56f, 0.72f, 1f);
+ break;
+ case ProjectM.Simulation.RoomBiomeId.Blight:
+ expFog = new Color(0.55f, 0.42f, 0.62f, 1f); expDen = 0.016f;
+ expAmb = new Color(0.62f, 0.50f, 0.70f, 1f);
+ break;
+ // Arid keeps the config/default orange above.
+ }
+ }
+
float x = _cam.transform.position.x;
float t = Mathf.Clamp01((x - (boundary - half)) / (2f * half));
diff --git a/Assets/_Project/Scripts/Client/UI/HowToPlayPanel.cs b/Assets/_Project/Scripts/Client/UI/HowToPlayPanel.cs
index c3bbaf37a..23f0b2372 100644
--- a/Assets/_Project/Scripts/Client/UI/HowToPlayPanel.cs
+++ b/Assets/_Project/Scripts/Client/UI/HowToPlayPanel.cs
@@ -94,7 +94,7 @@ namespace ProjectM.Client
Body(c, "Turret (40 Ore) — auto-fires at enemies. Needs Charge as ammo.");
Body(c, "Fabricator (30 Ore) — converts Ore → Charge so turrets keep firing.");
Body(c, "Wall (Biomass) — a cheap barrier that blocks enemies.");
- Body(c, "Aether — spend it on UPGRADE DMG to boost your damage.");
+ Body(c, "Aether — rare; fuels PERMANENT class upgrades at the base between runs.");
Body(c, "Open Build with Tab (Y), pick a piece, click a green tile to place it.");
break;
case 3: // Threats
diff --git a/Assets/_Project/Scripts/Client/UI/HudTheme.cs b/Assets/_Project/Scripts/Client/UI/HudTheme.cs
index cafb16892..12b54c93e 100644
--- a/Assets/_Project/Scripts/Client/UI/HudTheme.cs
+++ b/Assets/_Project/Scripts/Client/UI/HudTheme.cs
@@ -55,6 +55,11 @@ namespace ProjectM.Client
public Sprite FabricatorIcon;
public Sprite ConveyorIcon;
+ [Header("Build-ghost preview meshes (authored real-size, ground pivot — BuildSendSystem)")]
+ public Mesh TurretGhostMesh;
+ public Mesh WallGhostMesh;
+ public Mesh FabricatorGhostMesh;
+
[Header("Build-mode control glyphs")]
public Sprite KbmPlace; // LMB
public Sprite KbmCancel; // RMB
@@ -91,6 +96,18 @@ namespace ProjectM.Client
}
}
+ /// Placement-ghost preview mesh for a byte (null → the cube fallback).
+ public Mesh StructureGhostMesh(byte type)
+ {
+ switch (type)
+ {
+ case StructureType.Turret: return TurretGhostMesh;
+ case StructureType.Wall: return WallGhostMesh;
+ case StructureType.Fabricator: return FabricatorGhostMesh;
+ default: return null;
+ }
+ }
+
// ---- cached SDF font definitions (one FontAsset per font, built once, reset per play session) ----
static FontAsset _displayFa, _bodyFa, _bodyLightFa;
static bool _displayTried, _bodyTried, _bodyLightTried;
diff --git a/Assets/_Project/Scripts/Client/UI/WorldLauncher.cs b/Assets/_Project/Scripts/Client/UI/WorldLauncher.cs
index 042261add..5f17104f5 100644
--- a/Assets/_Project/Scripts/Client/UI/WorldLauncher.cs
+++ b/Assets/_Project/Scripts/Client/UI/WorldLauncher.cs
@@ -120,7 +120,16 @@ namespace ProjectM.Client
if (data == null) return;
var em = server.EntityManager;
var e = em.CreateEntity();
- em.AddComponentData(e, new PendingSave { GoalCharge = data.GoalCharge, GoalTarget = data.GoalTarget, CoreCurrent = data.CoreCurrent, RunOutcome = (byte)data.RunOutcome, HasData = 1 });
+ // v5->v6 migration (operator-approved): an old save's Charge counted boss-cleared runs (DR-042), so a
+ // missing RunsCompleted floors to it — the HUD never shows "Charge 3/4" beside "Runs completed: 0".
+ int runsCompleted = data.RunsCompleted > data.GoalCharge ? data.RunsCompleted : data.GoalCharge;
+ em.AddComponentData(e, new PendingSave { GoalCharge = data.GoalCharge, GoalTarget = data.GoalTarget, CoreCurrent = data.CoreCurrent, RunOutcome = (byte)data.RunOutcome, RunsCompleted = runsCompleted, MaxDepthReached = data.MaxDepthReached, HasData = 1 });
+ // v6: stage the meta tiers UNCONDITIONALLY (empty OK — the Bursted spawn system GetBuffers it in the
+ // HasData block; a conditional buffer would throw on any v<=5 Continue). Rows verbatim, no clamping.
+ var mbuf = em.AddBuffer(e);
+ if (data.MetaUpgrades != null)
+ foreach (var mrow in data.MetaUpgrades)
+ mbuf.Add(new PendingMetaRow { ClassId = mrow.ClassId, UpgradeId = mrow.UpgradeId, Tier = mrow.Tier });
var buf = em.AddBuffer(e);
if (data.Ledger != null)
foreach (var row in data.Ledger)
@@ -167,6 +176,9 @@ namespace ProjectM.Client
var st = tq.GetSingleton().ServerTick;
if (st.IsValid) nowTick = st.TickIndexForValidTick;
}
+ // v6: the permanent-meta slice via the ONE shared collector — omitting it HERE (the most common
+ // exit path) would silently WIPE all meta progression on quit (the meta review's top blocker).
+ MetaSaveScan.Collect(em, dir, out var metaRows, out var runsCompleted, out var maxDepth);
SaveStructureScan.Collect(em, nowTick, out var structures, out var structureIo);
SaveService.Save(new SaveData
@@ -174,6 +186,9 @@ namespace ProjectM.Client
GoalCharge = goal.Charge,
GoalTarget = goal.Target,
CoreCurrent = core.Current,
+ RunsCompleted = runsCompleted,
+ MaxDepthReached = maxDepth,
+ MetaUpgrades = metaRows,
RunOutcome = outcome.Value,
Ledger = rows,
diff --git a/Assets/_Project/Scripts/Client/World.meta b/Assets/_Project/Scripts/Client/World.meta
new file mode 100644
index 000000000..deeb6a039
--- /dev/null
+++ b/Assets/_Project/Scripts/Client/World.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 23831c9502abad541a3b2a3dc65502c2
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/_Project/Scripts/Client/World/ReadySendSystem.cs b/Assets/_Project/Scripts/Client/World/ReadySendSystem.cs
new file mode 100644
index 000000000..b37857ecd
--- /dev/null
+++ b/Assets/_Project/Scripts/Client/World/ReadySendSystem.cs
@@ -0,0 +1,59 @@
+using ProjectM.Simulation;
+using Unity.Entities;
+using Unity.NetCode;
+using UnityEngine;
+
+namespace ProjectM.Client
+{
+ ///
+ /// Client-side ready-toggle sender: a static enqueue (HUD button at Step 14 / the T dev key / execute_code)
+ /// drained into RPC entities — the BuildSendSystem queue+drain idiom. The local
+ /// bool tracks only the toggle DIRECTION; the server-replicated is the truth the HUD
+ /// renders. Statics reset on play-enter (statics survive fast-enter-playmode reloads — the stale-bridge hazard).
+ ///
+ [WorldSystemFilter(WorldSystemFilterFlags.ClientSimulation)]
+ public partial class ReadySendSystem : SystemBase
+ {
+ static int s_Pending; // queued explicit sets
+ static byte s_PendingValue;
+ static bool s_LocalReady; // last requested state (toggle direction only, not authority)
+
+ [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)]
+ static void ResetStatics()
+ {
+ s_Pending = 0;
+ s_PendingValue = 0;
+ s_LocalReady = false;
+ }
+
+ /// Queue an explicit ready set (HUD button / execute_code).
+ public static void SetReady(bool ready)
+ {
+ s_PendingValue = (byte)(ready ? 1 : 0);
+ s_Pending++;
+ s_LocalReady = ready;
+ }
+
+ /// Queue a toggle of the last requested state (the T dev key; HUD replaces this at Step 14).
+ public static void ToggleReady() => SetReady(!s_LocalReady);
+
+ protected override void OnCreate()
+ {
+ RequireForUpdate();
+ }
+
+ protected override void OnUpdate()
+ {
+ var keyboard = UnityEngine.InputSystem.Keyboard.current;
+ if (keyboard != null && keyboard.tKey.wasPressedThisFrame && !PauseMenuController.Open)
+ ToggleReady();
+
+ while (s_Pending > 0)
+ {
+ s_Pending--;
+ var req = EntityManager.CreateEntity(typeof(ReadyToggleRequest), typeof(SendRpcCommandRequest));
+ EntityManager.SetComponentData(req, new ReadyToggleRequest { Ready = s_PendingValue });
+ }
+ }
+ }
+}
diff --git a/Assets/_Project/Scripts/Client/World/ReadySendSystem.cs.meta b/Assets/_Project/Scripts/Client/World/ReadySendSystem.cs.meta
new file mode 100644
index 000000000..47b3260f2
--- /dev/null
+++ b/Assets/_Project/Scripts/Client/World/ReadySendSystem.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 7047cb8f6861ba8498f33698c9948dc8
\ No newline at end of file
diff --git a/Assets/_Project/Scripts/Client/World/RouteSendSystem.cs b/Assets/_Project/Scripts/Client/World/RouteSendSystem.cs
new file mode 100644
index 000000000..3bcf2ae35
--- /dev/null
+++ b/Assets/_Project/Scripts/Client/World/RouteSendSystem.cs
@@ -0,0 +1,60 @@
+using ProjectM.Simulation;
+using Unity.Entities;
+using Unity.NetCode;
+using UnityEngine;
+
+namespace ProjectM.Client
+{
+ ///
+ /// Client-side route-pick sender: a static enqueue (the Step-14 map panel's option buttons / execute_code)
+ /// drained into RPCs. The request is stamped from the CLIENT's replicated
+ /// : ForRunEpoch = (int)RunSeed (the re-meaned run-identity token — the server-only
+ /// RunEpoch is not client-knowable) and ForLayer = CurrentRoom VERBATIM (during a gate that is still the
+ /// just-cleared layer — never +1). The server re-validates everything; a stale/possessed pick is simply dropped.
+ /// Statics reset on play-enter (the stale-bridge hazard).
+ ///
+ [WorldSystemFilter(WorldSystemFilterFlags.ClientSimulation)]
+ public partial class RouteSendSystem : SystemBase
+ {
+ static int s_Pending;
+ static byte s_PendingIndex;
+
+ [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)]
+ static void ResetStatics()
+ {
+ s_Pending = 0;
+ s_PendingIndex = 0;
+ }
+
+ /// Queue a route pick (0..RouteOptionCount-1). HUD map panel + execute_code drive this.
+ public static void PickRoute(byte optionIndex)
+ {
+ s_PendingIndex = optionIndex;
+ s_Pending++;
+ }
+
+ protected override void OnCreate()
+ {
+ RequireForUpdate();
+ RequireForUpdate();
+ }
+
+ protected override void OnUpdate()
+ {
+ if (s_Pending == 0)
+ return;
+ var runInfo = SystemAPI.GetSingleton();
+ while (s_Pending > 0)
+ {
+ s_Pending--;
+ var req = EntityManager.CreateEntity(typeof(RouteSelectRequest), typeof(SendRpcCommandRequest));
+ EntityManager.SetComponentData(req, new RouteSelectRequest
+ {
+ OptionIndex = s_PendingIndex,
+ ForRunEpoch = (int)runInfo.RunSeed, // the re-meaned replicated run token
+ ForLayer = runInfo.CurrentRoom, // the gate's un-incremented cleared layer
+ });
+ }
+ }
+ }
+}
diff --git a/Assets/_Project/Scripts/Client/World/RouteSendSystem.cs.meta b/Assets/_Project/Scripts/Client/World/RouteSendSystem.cs.meta
new file mode 100644
index 000000000..f0b761e07
--- /dev/null
+++ b/Assets/_Project/Scripts/Client/World/RouteSendSystem.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: c22921cccedc3564b86d12491d88488e
\ No newline at end of file
diff --git a/Assets/_Project/Scripts/Server/Building/AbilityUpgradeSystem.cs b/Assets/_Project/Scripts/Server/Building/AbilityUpgradeSystem.cs
deleted file mode 100644
index 72a116c33..000000000
--- a/Assets/_Project/Scripts/Server/Building/AbilityUpgradeSystem.cs
+++ /dev/null
@@ -1,92 +0,0 @@
-using ProjectM.Simulation;
-using Unity.Burst;
-using Unity.Collections;
-using Unity.Entities;
-using Unity.NetCode;
-
-namespace ProjectM.Server
-{
- ///
- /// Server-authoritative ability-damage upgrade (handles RPCs). Resolves
- /// the sender's player (SourceConnection -> NetworkId -> GhostOwner) and, if the global ledger affords the
- /// Aether cost, withdraws it IN-PLACE and grows a single damage on the player
- /// (replace-by-SourceId so the [InternalBufferCapacity(8)] buffer stays bounded — repeated upgrades grow one
- /// row's percent rather than appending). StatRecomputeSystem folds it into EffectiveAbilityStats.Damage on
- /// both worlds. Plain server SimulationSystemGroup (not predicted → applied once).
- ///
- [BurstCompile]
- [WorldSystemFilter(WorldSystemFilterFlags.ServerSimulation)]
- public partial struct AbilityUpgradeSystem : ISystem
- {
- const uint UpgradeSourceId = Tuning.AbilityUpgradeSourceId; // distinct sentinel so the upgrade modifier is found + grown
- const float TierStep = Tuning.AbilityUpgradeTierStep; // +25% damage per tier
- const int CostAmount = Tuning.AbilityUpgradeCostAmount; // Aether per tier
-
- [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 ledger = SystemAPI.GetBuffer(SystemAPI.GetSingletonEntity());
-
- 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 (receive, requestEntity) in
- SystemAPI.Query>().WithAll().WithEntityAccess())
- {
- var conn = receive.ValueRO.SourceConnection;
- if (SystemAPI.HasComponent(conn)
- && playerByConn.TryGetValue(SystemAPI.GetComponent(conn).Value, out var player))
- {
- int have = 0;
- for (int i = 0; i < ledger.Length; i++)
- if (ledger[i].ItemId == ResourceId.Aether) { have = ledger[i].Count; break; }
-
- if (have >= CostAmount)
- {
- StorageMath.Withdraw(ledger, ResourceId.Aether, CostAmount);
-
- var mods = SystemAPI.GetBuffer(player);
- bool grown = false;
- for (int i = 0; i < mods.Length; i++)
- {
- if (mods[i].SourceId == UpgradeSourceId && mods[i].Target == (byte)StatTarget.Damage)
- {
- var m = mods[i];
- m.Value += TierStep;
- mods[i] = m;
- grown = true;
- break;
- }
- }
- if (!grown)
- mods.Add(new StatModifier
- {
- Target = (byte)StatTarget.Damage,
- Op = (byte)ModOp.PercentAdd,
- Value = TierStep,
- SourceId = UpgradeSourceId,
- });
- }
- }
-
- ecb.DestroyEntity(requestEntity);
- }
-
- ecb.Playback(state.EntityManager);
- playerByConn.Dispose();
- }
- }
-}
diff --git a/Assets/_Project/Scripts/Server/Building/AbilityUpgradeSystem.cs.meta b/Assets/_Project/Scripts/Server/Building/AbilityUpgradeSystem.cs.meta
deleted file mode 100644
index 90855d758..000000000
--- a/Assets/_Project/Scripts/Server/Building/AbilityUpgradeSystem.cs.meta
+++ /dev/null
@@ -1,2 +0,0 @@
-fileFormatVersion: 2
-guid: ff2ed6b5fa37a174aa7413f4d2f5d6b3
\ No newline at end of file
diff --git a/Assets/_Project/Scripts/Server/Combat/BoonApplySystem.cs b/Assets/_Project/Scripts/Server/Combat/BoonApplySystem.cs
new file mode 100644
index 000000000..adaba71d6
--- /dev/null
+++ b/Assets/_Project/Scripts/Server/Combat/BoonApplySystem.cs
@@ -0,0 +1,133 @@
+using ProjectM.Simulation;
+using Unity.Burst;
+using Unity.Collections;
+using Unity.Entities;
+using Unity.NetCode;
+
+namespace ProjectM.Server
+{
+ ///
+ /// Server receiver for + the reward-grace AUTO-PICK backstop. A valid pick
+ /// (sender resolved, RunInfo.Lifecycle == RoomReward — the D-F4 gate — Pending == 1, index in
+ /// range, option id known to the catalog) appends ONE in the run-scoped BOON band
+ /// (Tuning.BoonSourceIdBase + BoonPickCounter++ — distinct rows, one range-strip clears the run) and
+ /// clears Pending; the buffer mutation is non-structural and folds through the unchanged
+ /// StatRecomputeSystem on both worlds (rollback-correct). When the reward grace elapses, every still-pending
+ /// EXPEDITION player is auto-dealt Option0 (the operator's default un-picked policy — a player always
+ /// gets something) so the run never stalls on an AFK picker.
+ ///
+ /// Ordering: [UpdateBefore(RunDirectorSystem)] — ALL RPC receivers sit before the director (the
+ /// ReadyToggle/RouteSelect symmetry). This closes the D-F4 straggler race STRUCTURALLY: on the tick the
+ /// director strips (Returning), a straggler pick is rejected here FIRST (lifecycle is already past RoomReward),
+ /// so nothing can append after the strip; and the auto-pick lands before the director's exit gate reads
+ /// Pending. Requests are ALWAYS destroyed. No CyclePhase edge (the room-chain hard rule).
+ ///
+ [BurstCompile]
+ [WorldSystemFilter(WorldSystemFilterFlags.ServerSimulation)]
+ [UpdateInGroup(typeof(SimulationSystemGroup))]
+ [UpdateBefore(typeof(RunDirectorSystem))]
+ public partial struct BoonApplySystem : ISystem
+ {
+ [BurstCompile]
+ public void OnCreate(ref SystemState state)
+ {
+ state.RequireForUpdate();
+ state.RequireForUpdate();
+ state.RequireForUpdate();
+ state.RequireForUpdate();
+ }
+
+ [BurstCompile]
+ public void OnUpdate(ref SystemState state)
+ {
+ var dirEntity = SystemAPI.GetSingletonEntity();
+ var info = SystemAPI.GetComponent(dirEntity);
+ var run = SystemAPI.GetComponent(dirEntity);
+ bool rewarding = info.Lifecycle == RunLifecycle.RoomReward;
+
+ var catalog = SystemAPI.GetComponent(SystemAPI.GetSingletonEntity());
+ if (!catalog.Value.IsCreated)
+ return;
+ ref var pool = ref catalog.Value.Value;
+
+ bool runDirty = false;
+
+ // ---- explicit picks (drained every tick so stale requests die even outside RoomReward) ----
+ 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 (receive, req, requestEntity) in
+ SystemAPI.Query, RefRO>().WithEntityAccess())
+ {
+ var conn = receive.ValueRO.SourceConnection;
+ if (rewarding
+ && req.ValueRO.Index < 3
+ && SystemAPI.HasComponent(conn)
+ && playerByConn.TryGetValue(SystemAPI.GetComponent(conn).Value, out var player))
+ {
+ var offer = SystemAPI.GetComponent(player);
+ if (offer.Pending == 1)
+ {
+ byte id = req.ValueRO.Index == 2 ? offer.Option2
+ : req.ValueRO.Index == 1 ? offer.Option1 : offer.Option0;
+ if (Apply(ref state, player, id, ref pool, ref run))
+ {
+ offer.Pending = 0;
+ SystemAPI.SetComponent(player, offer);
+ runDirty = true;
+ }
+ }
+ }
+ ecb.DestroyEntity(requestEntity);
+ }
+ ecb.Playback(state.EntityManager);
+ playerByConn.Dispose();
+
+ // ---- reward-grace auto-pick backstop (Option0 — the player always gets something) ----
+ if (rewarding && run.RewardGraceTick != 0u)
+ {
+ var serverTick = SystemAPI.GetSingleton().ServerTick;
+ if (serverTick.IsValid && !new NetworkTick(run.RewardGraceTick).IsNewerThan(serverTick))
+ {
+ foreach (var (offer, region, entity) in
+ SystemAPI.Query, RefRO>()
+ .WithAll().WithEntityAccess())
+ {
+ if (offer.ValueRO.Pending != 1 || region.ValueRO.Region != RegionId.Expedition)
+ continue;
+ if (Apply(ref state, entity, offer.ValueRO.Option0, ref pool, ref run))
+ runDirty = true;
+ offer.ValueRW.Pending = 0; // cleared even if the id was unknown — never wedge the gate
+ }
+ }
+ }
+
+ if (runDirty)
+ SystemAPI.SetComponent(dirEntity, run); // the documented BoonPickCounter co-write (band provenance)
+ }
+
+ /// Append the boon's StatModifier in the run-scoped band. False iff the id is unknown/zero.
+ static bool Apply(ref SystemState state, Entity player, byte boonId, ref BoonCatalogBlob pool, ref RunRuntime run)
+ {
+ if (boonId == 0)
+ return false;
+ int idx = BoonMath.FindDef(ref pool, boonId);
+ if (idx < 0)
+ return false; // unknown id (catalog drift) — preserve-and-skip, never throw
+
+ var mods = state.EntityManager.GetBuffer(player);
+ mods.Add(new StatModifier
+ {
+ Target = pool.Defs[idx].Target,
+ Op = pool.Defs[idx].Op,
+ Value = pool.Defs[idx].Value,
+ SourceId = Tuning.BoonSourceIdBase + (run.BoonPickCounter % Tuning.BoonSourceIdSpan),
+ });
+ run.BoonPickCounter += 1;
+ return true;
+ }
+ }
+}
diff --git a/Assets/_Project/Scripts/Server/Combat/BoonApplySystem.cs.meta b/Assets/_Project/Scripts/Server/Combat/BoonApplySystem.cs.meta
new file mode 100644
index 000000000..80a1a84dc
--- /dev/null
+++ b/Assets/_Project/Scripts/Server/Combat/BoonApplySystem.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 5749745bedc86ca4396b9a3911ef8773
\ No newline at end of file
diff --git a/Assets/_Project/Scripts/Server/Combat/BoonOfferSystem.cs b/Assets/_Project/Scripts/Server/Combat/BoonOfferSystem.cs
new file mode 100644
index 000000000..e5c56791b
--- /dev/null
+++ b/Assets/_Project/Scripts/Server/Combat/BoonOfferSystem.cs
@@ -0,0 +1,78 @@
+using ProjectM.Simulation;
+using Unity.Burst;
+using Unity.Entities;
+using Unity.NetCode;
+
+namespace ProjectM.Server
+{
+ ///
+ /// Server-only choice-of-3 boon dealer: once per (int-equality latch on
+ /// , attached beside the catalog singleton), when the run FSM enters RoomReward it
+ /// draws each EXPEDITION player's 3 distinct, rarity-weighted, class-filtered options via
+ /// — deterministically seeded from Hash(RunSeed, room, NetworkId) — and writes
+ /// the player's owner-only replicated (Pending=1). A base-region player (dead-respawned,
+ /// late joiner) gets NO offer and never holds the gate (RunDirector counts only Pending!=0). BoonApplySystem
+ /// (Step 10) consumes picks; the Returning-edge strip zeroes stragglers.
+ ///
+ /// Ordering: [UpdateAfter(RunDirectorSystem)] — on the RoomReward ENTRY tick this runs after the
+ /// transition, so offers exist BEFORE RunDirector's exit gate first evaluates (next tick). No CyclePhase edge.
+ ///
+ [BurstCompile]
+ [WorldSystemFilter(WorldSystemFilterFlags.ServerSimulation)]
+ [UpdateInGroup(typeof(SimulationSystemGroup))]
+ [UpdateAfter(typeof(RunDirectorSystem))]
+ public partial struct BoonOfferSystem : ISystem
+ {
+ [BurstCompile]
+ public void OnCreate(ref SystemState state)
+ {
+ state.RequireForUpdate();
+ state.RequireForUpdate();
+ state.RequireForUpdate();
+ }
+
+ [BurstCompile]
+ public void OnUpdate(ref SystemState state)
+ {
+ var catalogEntity = SystemAPI.GetSingletonEntity();
+
+ // One-shot: attach this system's latch beside the catalog singleton (the RoomFieldState idiom).
+ if (!SystemAPI.HasComponent(catalogEntity))
+ {
+ state.EntityManager.AddComponentData(catalogEntity, new BoonOfferState());
+ return; // structural change — clean re-read next tick
+ }
+
+ var dirEntity = SystemAPI.GetSingletonEntity();
+ var info = SystemAPI.GetComponent(dirEntity);
+ if (info.Lifecycle != RunLifecycle.RoomReward)
+ return;
+
+ var run = SystemAPI.GetComponent(dirEntity);
+ var offered = SystemAPI.GetComponent(catalogEntity);
+ if (offered.OfferedRoomEpoch == run.RoomEpoch)
+ return; // this room's offers are already dealt
+
+ var catalog = SystemAPI.GetComponent(catalogEntity);
+ if (!catalog.Value.IsCreated)
+ return;
+ ref var pool = ref catalog.Value.Value;
+
+ foreach (var (offer, owner, region, cls) in
+ SystemAPI.Query, RefRO, RefRO, RefRO>()
+ .WithAll())
+ {
+ if (region.ValueRO.Region != RegionId.Expedition)
+ continue; // home-bound players (dead-respawned, joiners) are dealt nothing
+
+ // Deterministic per-player draw: reconnect-stable per session, replay-reproducible per (seed, room).
+ uint offerSeed = RunMapMath.Hash(run.RunSeed, (uint)info.CurrentRoom, (uint)owner.ValueRO.NetworkId) | 1u;
+ BoonMath.PickBoons(offerSeed, cls.ValueRO.ClassId, ref pool, out byte o0, out byte o1, out byte o2);
+ offer.ValueRW = new BoonOffer { Pending = 1, Option0 = o0, Option1 = o1, Option2 = o2 };
+ }
+
+ offered.OfferedRoomEpoch = run.RoomEpoch;
+ SystemAPI.SetComponent(catalogEntity, offered);
+ }
+ }
+}
diff --git a/Assets/_Project/Scripts/Server/Combat/BoonOfferSystem.cs.meta b/Assets/_Project/Scripts/Server/Combat/BoonOfferSystem.cs.meta
new file mode 100644
index 000000000..aa8806809
--- /dev/null
+++ b/Assets/_Project/Scripts/Server/Combat/BoonOfferSystem.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 2d3715c60d2cc2348ac4ff7600006d23
\ No newline at end of file
diff --git a/Assets/_Project/Scripts/Server/Combat/RoomEnemyDirectorSystem.cs b/Assets/_Project/Scripts/Server/Combat/RoomEnemyDirectorSystem.cs
new file mode 100644
index 000000000..016bf1755
--- /dev/null
+++ b/Assets/_Project/Scripts/Server/Combat/RoomEnemyDirectorSystem.cs
@@ -0,0 +1,192 @@
+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 per-ROOM enemy director — the Step-6 successor of the presence-keyed ZoneEnemyDirectorSystem.
+ /// While the run FSM has a room active ( == InRoom) it seeds ONE wave per
+ /// (int-equality reseed) sized by indexed
+ /// on the room's (deeper rooms + Elite/Boss types skew heavier — the
+ /// grounded MC-2 mix bands are reused verbatim), drip-spawned one SLOT per cadence at the deterministic ring
+ /// around (base, ActiveSubSlot), under the same
+ /// "spawn-the-pack-only-if-it-fits-else-wait" relevancy guard. A
+ /// room spawns ONE beefed boss instead (health × ,
+ /// scale × — v1's boss is a scaled Charger). Every spawn keeps the full
+ /// stack — EnemyTag + RegionTag{Expedition} + — PLUS {room} (the
+ /// teardown contract). Scale preserved via baked.WithPosition.
+ ///
+ /// The room CLEAR edge surfaces ONLY through the replicated .State == Cleared
+ /// (wave fully spawned AND zero alive, latched per seeded epoch) — written FIRST, ABOVE every early-return
+ /// (snapshot-above-early-return) so the HUD never freezes; RunDirectorSystem consumes it one-tick-late (Step 7).
+ /// The old CycleRuntime.ClearedThisEpoch write is gone (the C4 collapse), and the old base-siege Calm gate is
+ /// deliberately DROPPED — a home retaliation siege no longer freezes a live sortie (the DR-042 latent gap).
+ ///
+ /// Ordering: [UpdateAfter(RunDirectorSystem)] ONLY — reads the freshly-advanced room state same-tick.
+ /// NO CyclePhase edge may ever return to the room chain (Play-only sort-cycle, invisible to EditMode).
+ ///
+ [BurstCompile]
+ [WorldSystemFilter(WorldSystemFilterFlags.ServerSimulation)]
+ [UpdateInGroup(typeof(SimulationSystemGroup))]
+ [UpdateAfter(typeof(RunDirectorSystem))]
+ public partial struct RoomEnemyDirectorSystem : ISystem
+ {
+ EntityQuery m_ZoneEnemies;
+
+ [BurstCompile]
+ public void OnCreate(ref SystemState state)
+ {
+ state.RequireForUpdate();
+ state.RequireForUpdate();
+ state.RequireForUpdate();
+ state.RequireForUpdate();
+ m_ZoneEnemies = 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 runEntity = SystemAPI.GetSingletonEntity();
+ var info = SystemAPI.GetComponent(runEntity);
+ var run = SystemAPI.GetComponent(runEntity);
+ bool roomActive = info.Lifecycle == RunLifecycle.InRoom;
+
+ var directorEntity = SystemAPI.GetSingletonEntity();
+ var dir = SystemAPI.GetComponent(directorEntity);
+ var zs = SystemAPI.GetComponent(directorEntity);
+
+ int aliveZone = m_ZoneEnemies.CalculateEntityCount();
+
+ // REPLICATED objective summary FIRST, above every early-return (snapshot-above-early-return): the HUD
+ // readout must never freeze stale. Cleared latches only for a wave seeded FOR THIS RoomEpoch.
+ if (SystemAPI.HasComponent(runEntity))
+ {
+ byte objState;
+ short objRemaining;
+ if (roomActive && (aliveZone > 0 || zs.RemainingToSpawn > 0))
+ {
+ objState = ExpeditionObjectiveState.Active;
+ objRemaining = (short)math.min(aliveZone + zs.RemainingToSpawn, short.MaxValue);
+ }
+ else if (roomActive && zs.SeededEpoch == run.RoomEpoch && zs.RemainingToSpawn == 0 && aliveZone == 0)
+ {
+ objState = ExpeditionObjectiveState.Cleared; // fully spawned + fully dead -> advance-ready
+ objRemaining = 0;
+ }
+ else
+ {
+ objState = ExpeditionObjectiveState.Idle;
+ objRemaining = 0;
+ }
+ SystemAPI.SetComponent(runEntity, new ExpeditionObjective { State = objState, Remaining = objRemaining });
+ }
+
+ if (!roomActive)
+ return;
+
+ var prefabs = SystemAPI.GetBuffer(directorEntity);
+ if (prefabs.Length == 0)
+ return;
+
+ // Single plan authority: the node RunDirector published — never re-derived here.
+ var map = RunMapMath.Generate(run.RunSeed);
+ var node = map.NodeAt(run.CurrentNodeId);
+ var plan = RoomLayoutMath.Plan(node, info.CurrentRoom, info.RoomCount);
+ byte room = (byte)(info.CurrentRoom & 0xFF);
+ bool bossRoom = plan.RoomType == RoomTypeId.Boss;
+
+ var bands = new MixBands
+ {
+ GruntBase = dir.GruntsPerWave,
+ ChargerBase = dir.ChargersPerWave,
+ SpitterBase = dir.SpitterBase,
+ SwarmerSlotBase = dir.SwarmerSlotBase,
+ ChargerPerEpoch = dir.ChargerPerEpoch,
+ SpitterPerEpoch = dir.SpitterPerEpoch,
+ SwarmerSlotPerEpoch = dir.SwarmerSlotPerEpoch,
+ SwarmerPackPerEpoch = dir.SwarmerPackPerEpoch,
+ };
+
+ // (Re)seed once per ROOM (its OWN counter, in SLOTS; a swarmer slot is one pack; a boss room is 1 slot).
+ if (zs.SeededEpoch != run.RoomEpoch)
+ {
+ zs.SeededEpoch = run.RoomEpoch;
+ zs.SpawnCounter = 0;
+ zs.RemainingToSpawn = bossRoom ? 1 : ZoneEnemyMath.WaveSlots(plan.DifficultyEpoch, bands);
+ zs.NextSpawnTick = TickUtil.NonZero(now); // first slot this tick
+ }
+
+ if (zs.RemainingToSpawn > 0)
+ {
+ bool dueNow = zs.NextSpawnTick == 0 || !new NetworkTick(zs.NextSpawnTick).IsNewerThan(serverTick);
+ if (dueNow)
+ {
+ int slot = (int)zs.SpawnCounter;
+ byte kind = bossRoom ? ZoneEnemyMath.KindCharger
+ : ZoneEnemyMath.KindForSlot(plan.DifficultyEpoch, slot, bands);
+ int packSize = !bossRoom && kind == ZoneEnemyMath.KindSwarmer
+ ? ZoneEnemyMath.PackSizeForSlot(plan.DifficultyEpoch, slot, bands, dir.SwarmerPackSize) : 1;
+
+ // MaxAlive counts ENTITIES; spawn the whole pack only if it fits (else WAIT — keep the slot).
+ if (aliveZone + packSize <= math.max(1, dir.MaxAlive))
+ {
+ float3 baseCenter = new float3(0f, 1f, 0f);
+ if (SystemAPI.TryGetSingleton(out var anchor))
+ baseCenter = BaseGridMath.PlotCenter(anchor);
+ float3 origin = RegionMath.ExpeditionRoomOrigin(baseCenter, run.ActiveSubSlot);
+ float3 center = bossRoom
+ ? origin // the boss anchors the room center
+ : EnemyAIMath.RingPosition(origin, slot, math.max(1, dir.RingSlots), dir.RingRadius);
+ center.y = origin.y;
+
+ int prefabIdx = kind;
+ if (prefabIdx >= prefabs.Length) prefabIdx = 0; // 4-entry buffer expected; clamp defensively
+ var prefab = prefabs[prefabIdx].Prefab;
+ var baked = state.EntityManager.GetComponentData(prefab);
+
+ var ecb = new EntityCommandBuffer(Allocator.Temp);
+ for (int k = 0; k < packSize; k++)
+ {
+ float3 pos = packSize > 1
+ ? EnemyAIMath.ClusterOffset(center, k, packSize, dir.ClusterTightRadius) : center;
+ pos.y = origin.y;
+ var enemy = ecb.Instantiate(prefab);
+ var xform = baked.WithPosition(pos); // preserve the baked [GhostField] Scale
+ if (bossRoom)
+ xform.Scale = baked.Scale * Tuning.BossScaleMultiplier;
+ ecb.SetComponent(enemy, xform);
+ ecb.AddComponent(enemy, new RegionTag { Region = RegionId.Expedition });
+ ecb.AddComponent(enemy);
+ ecb.AddComponent(enemy, new RoomTag { Room = room });
+ if (bossRoom && SystemAPI.HasComponent(prefab))
+ {
+ var hp = SystemAPI.GetComponent(prefab);
+ hp.Current *= Tuning.BossHealthMultiplier;
+ hp.Max *= Tuning.BossHealthMultiplier;
+ ecb.SetComponent(enemy, hp);
+ }
+ }
+ ecb.Playback(state.EntityManager);
+ ecb.Dispose();
+
+ zs.SpawnCounter += 1; // ONE slot consumed even for a pack
+ zs.RemainingToSpawn -= 1;
+ zs.NextSpawnTick = TickUtil.NonZero(now + (uint)math.max(1, dir.SpawnIntervalTicks));
+ }
+ }
+ }
+
+ SystemAPI.SetComponent(directorEntity, zs);
+ }
+ }
+}
diff --git a/Assets/_Project/Scripts/Server/Combat/RoomEnemyDirectorSystem.cs.meta b/Assets/_Project/Scripts/Server/Combat/RoomEnemyDirectorSystem.cs.meta
new file mode 100644
index 000000000..263acac0f
--- /dev/null
+++ b/Assets/_Project/Scripts/Server/Combat/RoomEnemyDirectorSystem.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 3204b510b450f384a93bd49902c65721
\ No newline at end of file
diff --git a/Assets/_Project/Scripts/Server/Combat/ZoneEnemyDirectorSystem.cs b/Assets/_Project/Scripts/Server/Combat/ZoneEnemyDirectorSystem.cs
deleted file mode 100644
index 9e51a31a3..000000000
--- a/Assets/_Project/Scripts/Server/Combat/ZoneEnemyDirectorSystem.cs
+++ /dev/null
@@ -1,189 +0,0 @@
-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 expedition zone-enemy director: while a player is OUT in the expedition region and the base is in
- /// , it seeds and drip-spawns one epoch-seeded combat wave around the expedition
- /// origin. The wave size + grunt/charger composition is pure of the
- /// (grunt-heavy -> charger-heavy as the epoch climbs), spawned one
- /// every at a deterministic ring, capped at
- /// concurrent (the v1 ghost-relevancy budget). Each enemy is the
- /// existing Husk ghost prefab + RegionTag{Expedition} + , so it reuses the whole
- /// combat/readability/AI stack (the per-region AI filter keeps it seeking the expedition player only). When the
- /// wave is fully spawned and every zone enemy is dead, it marks once —
- /// the gate's once-per-epoch Ore reward reads that on the player's return.
- ///
- /// DR-042 C7b: it ALSO writes the replicated summary every tick, ABOVE the
- /// presence early-return (snapshot-above-early-return), so the client HUD's "enemies remaining / cleared" readout
- /// never freezes stale even when nobody is out.
- ///
- /// Ordering: [UpdateAfter(ExpeditionFieldSystem)] ONLY. ExpeditionFieldSystem is itself
- /// [UpdateAfter(CyclePhaseSystem)], so ALSO declaring [UpdateBefore(CyclePhaseSystem)] here (as the
- /// v1 plan first sketched) would close a CyclePhase->Field->Zone->CyclePhase sort cycle that throws at Play
- /// world creation and is invisible to EditMode. Running after the field manager also reads the freshly-bumped
- /// epoch + current phase. Zone enemies are interpolated ghosts, moved server-only — no prediction.
- ///
- [BurstCompile]
- [WorldSystemFilter(WorldSystemFilterFlags.ServerSimulation)]
- [UpdateInGroup(typeof(SimulationSystemGroup))]
- [UpdateAfter(typeof(ExpeditionFieldSystem))]
- public partial struct ZoneEnemyDirectorSystem : ISystem
- {
- EntityQuery m_ZoneEnemies;
-
- [BurstCompile]
- public void OnCreate(ref SystemState state)
- {
- state.RequireForUpdate();
- state.RequireForUpdate();
- state.RequireForUpdate();
- m_ZoneEnemies = state.GetEntityQuery(ComponentType.ReadOnly());
- }
-
- [BurstCompile]
- public void OnUpdate(ref SystemState state)
- {
- var serverTick = SystemAPI.GetSingleton().ServerTick;
- if (!serverTick.IsValid)
- return;
- uint now = serverTick.TickIndexForValidTick;
-
- // Per-player presence: the SPAWNER only runs while someone is OUT in the expedition (mirrors
- // ExpeditionFieldSystem). The objective readout below is written FIRST, every tick, even when nobody's out.
- int expeditionPlayers = 0;
- foreach (var region in SystemAPI.Query>().WithAll())
- if (region.ValueRO.Region == RegionId.Expedition)
- expeditionPlayers++;
-
- var directorEntity = SystemAPI.GetSingletonEntity();
- var dir = SystemAPI.GetComponent(directorEntity);
- var zs = SystemAPI.GetComponent(directorEntity);
-
- var cycleEntity = SystemAPI.GetSingletonEntity();
- var cycle = SystemAPI.GetComponent(cycleEntity);
- var runtime = SystemAPI.GetComponent(cycleEntity);
- int epoch = runtime.ExpeditionEpoch;
-
- int aliveZone = m_ZoneEnemies.CalculateEntityCount();
-
- // DR-042 C7b: write the REPLICATED objective summary FIRST, above the early-returns (snapshot-above-
- // early-return) so the HUD never freezes stale. Rides the untagged CycleDirector ghost (cross-region safe).
- if (SystemAPI.HasComponent(cycleEntity))
- {
- byte objState;
- short objRemaining;
- if (runtime.ClearedThisEpoch != 0 && runtime.LastRewardedEpoch != runtime.ExpeditionEpoch)
- {
- objState = ExpeditionObjectiveState.Cleared; // cleared but not yet claimed -> "return to claim"
- objRemaining = 0;
- }
- else if (expeditionPlayers > 0 && (aliveZone > 0 || zs.RemainingToSpawn > 0))
- {
- objState = ExpeditionObjectiveState.Active;
- objRemaining = (short)math.min(aliveZone + zs.RemainingToSpawn, short.MaxValue);
- }
- else
- {
- objState = ExpeditionObjectiveState.Idle;
- objRemaining = 0;
- }
- SystemAPI.SetComponent(cycleEntity, new ExpeditionObjective { State = objState, Remaining = objRemaining });
- }
-
- if (expeditionPlayers == 0)
- return; // nobody out there: the field manager owns teardown, the spawner does nothing
-
- var prefabs = SystemAPI.GetBuffer(directorEntity);
- if (prefabs.Length == 0)
- return;
-
- // MC-2: build the 4-type weighted mix band from the director's baked weights (shared math with the base
- // siege). GruntsPerWave/ChargersPerWave are the Grunt/Charger base counts.
- var bands = new MixBands
- {
- GruntBase = dir.GruntsPerWave,
- ChargerBase = dir.ChargersPerWave,
- SpitterBase = dir.SpitterBase,
- SwarmerSlotBase = dir.SwarmerSlotBase,
- ChargerPerEpoch = dir.ChargerPerEpoch,
- SpitterPerEpoch = dir.SpitterPerEpoch,
- SwarmerSlotPerEpoch = dir.SwarmerSlotPerEpoch,
- SwarmerPackPerEpoch = dir.SwarmerPackPerEpoch,
- };
-
- // (Re)seed this epoch's wave once — its OWN counter (in SLOTS; a swarmer slot is one pack).
- if (zs.SeededEpoch != epoch)
- {
- zs.SeededEpoch = epoch;
- zs.SpawnCounter = 0;
- zs.RemainingToSpawn = ZoneEnemyMath.WaveSlots(epoch, bands);
- zs.NextSpawnTick = TickUtil.NonZero(now); // first slot this tick
- }
-
- if (zs.RemainingToSpawn > 0)
- {
- // Spawn only in Calm (a base Siege pauses the expedition wave), one SLOT per cadence, under the cap.
- bool calm = cycle.Phase == CyclePhase.Calm;
- bool dueNow = zs.NextSpawnTick == 0 || !new NetworkTick(zs.NextSpawnTick).IsNewerThan(serverTick);
- if (calm && dueNow)
- {
- int slot = (int)zs.SpawnCounter;
- byte kind = ZoneEnemyMath.KindForSlot(epoch, slot, bands);
- int packSize = kind == ZoneEnemyMath.KindSwarmer
- ? ZoneEnemyMath.PackSizeForSlot(epoch, slot, bands, dir.SwarmerPackSize) : 1;
-
- // MaxAlive counts ENTITIES; spawn the whole pack only if it fits (else WAIT — don't consume the slot).
- if (aliveZone + packSize <= math.max(1, dir.MaxAlive))
- {
- float3 baseCenter = new float3(0f, 1f, 0f);
- if (SystemAPI.TryGetSingleton(out var anchor))
- baseCenter = BaseGridMath.PlotCenter(anchor);
- float3 origin = RegionMath.RegionOrigin(RegionId.Expedition, baseCenter);
- float3 center = EnemyAIMath.RingPosition(origin, slot, math.max(1, dir.RingSlots), dir.RingRadius);
- center.y = origin.y;
-
- int prefabIdx = kind;
- if (prefabIdx >= prefabs.Length) prefabIdx = 0; // 4-entry buffer expected; clamp defensively
- var prefab = prefabs[prefabIdx].Prefab;
- // Preserve the prefab's baked Scale ([GhostField]) — FromPosition would reset Scale->1.
- var baked = state.EntityManager.GetComponentData(prefab);
-
- var ecb = new EntityCommandBuffer(Allocator.Temp);
- for (int k = 0; k < packSize; k++)
- {
- float3 pos = packSize > 1
- ? EnemyAIMath.ClusterOffset(center, k, packSize, dir.ClusterTightRadius) : center;
- pos.y = origin.y;
- var enemy = ecb.Instantiate(prefab);
- ecb.SetComponent(enemy, baked.WithPosition(pos));
- ecb.AddComponent(enemy, new RegionTag { Region = RegionId.Expedition });
- ecb.AddComponent(enemy);
- }
- ecb.Playback(state.EntityManager);
- ecb.Dispose();
-
- zs.SpawnCounter += 1; // ONE slot consumed even for a pack
- zs.RemainingToSpawn -= 1;
- zs.NextSpawnTick = TickUtil.NonZero(now + (uint)math.max(1, dir.SpawnIntervalTicks));
- }
- }
- }
- else if (aliveZone == 0 && runtime.ClearedThisEpoch == 0)
- {
- // Wave fully spawned AND every zone enemy dead -> a REAL clear. Mark once; the gate pays the
- // once-per-epoch Ore reward on the player's return to base.
- runtime.ClearedThisEpoch = 1;
- SystemAPI.SetComponent(cycleEntity, runtime);
- }
-
- SystemAPI.SetComponent(directorEntity, zs);
- }
- }
-}
diff --git a/Assets/_Project/Scripts/Server/Combat/ZoneEnemyDirectorSystem.cs.meta b/Assets/_Project/Scripts/Server/Combat/ZoneEnemyDirectorSystem.cs.meta
deleted file mode 100644
index 8b29587c0..000000000
--- a/Assets/_Project/Scripts/Server/Combat/ZoneEnemyDirectorSystem.cs.meta
+++ /dev/null
@@ -1,2 +0,0 @@
-fileFormatVersion: 2
-guid: 4dc121e99d0e53640b5e6815640a0bc6
\ No newline at end of file
diff --git a/Assets/_Project/Scripts/Server/Connection/GoInGameServerSystem.cs b/Assets/_Project/Scripts/Server/Connection/GoInGameServerSystem.cs
index fd6d554eb..55ff6c8aa 100644
--- a/Assets/_Project/Scripts/Server/Connection/GoInGameServerSystem.cs
+++ b/Assets/_Project/Scripts/Server/Connection/GoInGameServerSystem.cs
@@ -20,6 +20,8 @@ namespace ProjectM.Server
[WorldSystemFilter(WorldSystemFilterFlags.ServerSimulation)]
public partial struct GoInGameServerSystem : ISystem
{
+ bool _warnedMetaBlocked; // one-shot: a mis-authored subscene must not silently block spawns forever
+
[BurstCompile]
public void OnCreate(ref SystemState state)
{
@@ -33,6 +35,22 @@ namespace ProjectM.Server
[BurstCompile]
public void OnUpdate(ref SystemState state)
{
+ // Step 12a availability guard (review N2/L1-6): the born-correct meta seeding below needs the baked
+ // catalog + the live director's tier record. Both are per-tick-uniform, so guard ONCE at the TOP,
+ // BEFORE the ECB exists — a per-request continue would already have marked the connection in-game.
+ // Nothing is consumed; RequireForUpdate re-passes and the request retries next tick (a ≤1-tick window
+ // in practice — the director spawns at subscene-stream, before any GoInGame round-trip).
+ if (!SystemAPI.TryGetSingleton(out var metaCatalog) || !metaCatalog.Value.IsCreated
+ || !SystemAPI.TryGetSingletonBuffer(out var metaRecord, true))
+ {
+ if (!_warnedMetaBlocked)
+ {
+ UnityEngine.Debug.LogWarning("GoInGameServerSystem: player spawn waiting on the meta catalog/director (a mis-authored subscene would block spawns forever).");
+ _warnedMetaBlocked = true;
+ }
+ return;
+ }
+
var spawner = SystemAPI.GetSingleton();
// M5 home base: re-root the spawn ring on the baked BaseAnchor when present; fall back
@@ -61,6 +79,34 @@ namespace ProjectM.Server
byte classId = ClassTraits.Normalize(goReq.ValueRO.ClassId);
ecb.SetComponent(player, new AbilityRef { Id = ClassTraits.AbilityFor(classId) });
ClassTraits.AppendSeeds(classId, player, ecb);
+ // Expedition redesign: the server-only class anchor the meta systems key on (born-correct meta
+ // seeding at Step 12a + per-class spend at Step 13 resolve the tier record through this).
+ ecb.AddComponent(player, new PlayerClass { ClassId = classId });
+ // Step 12a: born-correct PERMANENT meta seeding — replay this class's persisted tiers as
+ // meta-band StatModifiers on the just-instantiated player (same ECB as Instantiate, the
+ // ClassTraits idiom). Skip tier 0 / unknown ids (preserve-don't-crash); CLAMP a saved tier above a
+ // rebalanced MaxTier (D-F5). Class gate via BoonMath.MaskFor (ClassId is the normalized
+ // CharacterId 2/3 — a raw 1<(out var metaCat) && metaCat.Value.IsCreated
+ && SystemAPI.TryGetSingletonBuffer(out var metaRecord, true))
+ {
+ ref var metaPool = ref metaCat.Value.Value;
+ byte metaBit = BoonMath.MaskFor(newClass);
+ for (int mi = 0; mi < metaRecord.Length; mi++)
+ {
+ if (metaRecord[mi].ClassId != newClass || metaRecord[mi].Tier == 0) continue;
+ int defIdx = MetaMath.FindDef(ref metaPool, metaRecord[mi].UpgradeId);
+ if (defIdx < 0) continue;
+ if ((metaPool.Defs[defIdx].ClassMask & metaBit) == 0) continue;
+ byte metaTier = metaRecord[mi].Tier < metaPool.Defs[defIdx].MaxTier
+ ? metaRecord[mi].Tier : metaPool.Defs[defIdx].MaxTier;
+ classMods.Add(new StatModifier
+ {
+ Target = metaPool.Defs[defIdx].Target,
+ Op = metaPool.Defs[defIdx].Op,
+ Value = metaPool.Defs[defIdx].ValuePerTier * metaTier,
+ SourceId = Tuning.MetaSourceIdBase + metaRecord[mi].UpgradeId,
+ });
+ }
+ }
+ if (SystemAPI.HasComponent(sender))
+ SystemAPI.SetComponent(sender, new PlayerClass { ClassId = newClass });
+
// Let the swapped Fire ability fire immediately (both abilities share one cooldown gate).
if (SystemAPI.HasComponent(sender))
SystemAPI.SetComponent(sender, new AbilityCooldown { NextFireTick = 0 }); // 0 = ready
diff --git a/Assets/_Project/Scripts/Server/Economy/BaseFieldSpawnSystem.cs b/Assets/_Project/Scripts/Server/Economy/BaseFieldSpawnSystem.cs
deleted file mode 100644
index d5d5f775d..000000000
--- a/Assets/_Project/Scripts/Server/Economy/BaseFieldSpawnSystem.cs
+++ /dev/null
@@ -1,101 +0,0 @@
-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 home-base mining-field manager. Keeps the live RegionTag{Base} ResourceNode count topped up to
- /// so the gather -> build -> survive loop lives AT the base (no
- /// expedition trip). Unlike (edge-triggered on player presence) this is a
- /// TICK-CADENCED top-up: every it counts live base nodes
- /// and instantiates (TargetCount - liveCount) more, scattered UNIFORMLY-IN-RADIUS (rad = inner + r*(outer-inner),
- /// NOT the area-weighted sqrt that piles nodes on the outer wall) in the [Inner,Outer] annulus around
- /// BaseGridMath.PlotCenter, each overridden via SetComponent (NOT Add — the prefab already bakes
- /// RegionTag{Expedition}) to RegionTag{Base} + ResourceId.Ore. The FIRST pass fires immediately
- /// (NextSpawnTick seeded 0) so the field seeds without waiting. Deterministic: the scatter RNG is seeded from
- /// a monotonic Epoch (never the tick); the cadence gate is wrap-safe NetworkTick math (TickUtil.NonZero +
- /// IsNewerThan), never raw uint. Runtime-spawned ghosts dodge the prespawn handshake. Plain server
- /// SimulationSystemGroup; server-only, never predicted.
- ///
- [BurstCompile]
- [WorldSystemFilter(WorldSystemFilterFlags.ServerSimulation)]
- [UpdateInGroup(typeof(SimulationSystemGroup))]
- public partial struct BaseFieldSpawnSystem : ISystem
- {
- [BurstCompile]
- public void OnCreate(ref SystemState state)
- {
- state.RequireForUpdate();
- 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 runtime = SystemAPI.GetComponent(spawnerEntity);
- if (spawner.Prefab == Entity.Null)
- return;
-
- // Cadence gate: first pass (NextSpawnTick == 0) fires immediately; thereafter every RespawnIntervalTicks.
- if (runtime.NextSpawnTick != 0u && new NetworkTick(runtime.NextSpawnTick).IsNewerThan(serverTick))
- return;
-
- // Count LIVE base-region nodes only (expedition nodes share the ResourceNode type; exclude by region).
- int liveBase = 0;
- foreach (var region in SystemAPI.Query>().WithAll())
- if (region.ValueRO.Region == RegionId.Base)
- liveBase++;
-
- int deficit = spawner.TargetCount - liveBase;
- if (deficit > 0)
- {
- var anchor = SystemAPI.GetSingleton();
- float3 center = BaseGridMath.PlotCenter(anchor);
- var baseXform = SystemAPI.GetComponent(spawner.Prefab);
- var prefabNode = SystemAPI.GetComponent(spawner.Prefab);
-
- runtime.Epoch += 1;
- var rng = new Random(((uint)runtime.Epoch * 747796405u) | 1u); // epoch-seeded (never the tick), nonzero
- float inner = math.max(0f, spawner.InnerRadius);
- float outer = math.max(inner + 0.01f, spawner.OuterRadius);
-
- var ecb = new EntityCommandBuffer(Allocator.Temp);
- for (int i = 0; i < deficit; i++)
- {
- var node = ecb.Instantiate(spawner.Prefab);
-
- float ang = rng.NextFloat(0f, math.PI * 2f);
- float rad = inner + rng.NextFloat(0f, 1f) * (outer - inner); // UNIFORM in radius
- var xform = baseXform;
- xform.Position = center + new float3(math.cos(ang) * rad, 0f, math.sin(ang) * rad);
- ecb.SetComponent(node, xform);
-
- // Override the baked RegionTag{Expedition} -> Base (else RegionRelevancy hides it from base
- // players) and force the resource to Ore (the build currency; base field stays Ore-only).
- ecb.SetComponent(node, new RegionTag { Region = RegionId.Base });
- var rn = prefabNode;
- rn.ResourceId = ResourceId.Ore;
- ecb.SetComponent(node, rn);
- }
- ecb.Playback(state.EntityManager);
- ecb.Dispose();
- }
-
- runtime.NextSpawnTick = TickUtil.NonZero(now + (uint)math.max(1, spawner.RespawnIntervalTicks));
- SystemAPI.SetComponent(spawnerEntity, runtime);
- }
- }
-}
diff --git a/Assets/_Project/Scripts/Server/Economy/BaseFieldSpawnSystem.cs.meta b/Assets/_Project/Scripts/Server/Economy/BaseFieldSpawnSystem.cs.meta
deleted file mode 100644
index 7e6c28cf4..000000000
--- a/Assets/_Project/Scripts/Server/Economy/BaseFieldSpawnSystem.cs.meta
+++ /dev/null
@@ -1,2 +0,0 @@
-fileFormatVersion: 2
-guid: 346e9c0fb92e7b94fa3761222fc2ff1e
\ No newline at end of file
diff --git a/Assets/_Project/Scripts/Server/Economy/ExpeditionFieldSystem.cs b/Assets/_Project/Scripts/Server/Economy/ExpeditionFieldSystem.cs
deleted file mode 100644
index 272f5a4a8..000000000
--- a/Assets/_Project/Scripts/Server/Economy/ExpeditionFieldSystem.cs
+++ /dev/null
@@ -1,145 +0,0 @@
-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. Re-keyed off PER-PLAYER PRESENCE (no global phase): it
- /// counts players whose server-only is the Expedition region, and on the
- /// empty->occupied edge (a new sortie) bumps and scatters
- /// resource-node ghosts (seeded by the epoch) around the expedition
- /// origin — PLUS, if a singleton is present,
- /// Blight-clutter ghosts (seeded DISTINCTLY so clutter and nodes don't
- /// co-locate, Variant round-robined), each RegionTag{Expedition}; on the occupied->empty edge (the LAST
- /// player left) it destroys every node AND every clutter piece. So the field lives as long as anyone is out
- /// there, not on a global timer. Plain server SimulationSystemGroup. Server-authoritative; clients despawn
- /// ghosts via GhostDespawnSystem. Per-epoch reproducible (the seed is the monotonic int epoch, compared by
- /// equality — never tick math, never 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 runtime = SystemAPI.GetComponent(cycleEntity);
- var spawner = SystemAPI.GetSingleton();
-
- // Per-player presence: is anyone currently out in the expedition region?
- int expeditionPlayers = 0;
- foreach (var region in SystemAPI.Query>().WithAll())
- if (region.ValueRO.Region == RegionId.Expedition)
- expeditionPlayers++;
- bool occupied = expeditionPlayers > 0;
- bool wasOccupied = runtime.PrevExpeditionOccupied != 0;
-
- // empty -> occupied: a new sortie begins; bump the epoch so the field reseeds fresh.
- if (occupied && !wasOccupied)
- {
- runtime.ExpeditionEpoch += 1;
- runtime.ClearedThisEpoch = 0; // a fresh sortie has not been cleared yet (gates the once-per-epoch reward)
- }
-
- 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: a player is out and this epoch has not been seeded yet.
- if (occupied
- && runtime.LastSpawnedEpoch != runtime.ExpeditionEpoch
- && spawner.Prefab != Entity.Null)
- {
- var baseXform = SystemAPI.GetComponent(spawner.Prefab);
- var prefabNode = SystemAPI.GetComponent(spawner.Prefab);
- var rng = new Random((uint)math.max(1, runtime.ExpeditionEpoch));
- 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);
- }
-
- // Blight clutter (OPTIONAL singleton): scatter alongside the nodes with a DISTINCT seed so the
- // two fields don't co-locate. Round-robin Variant for client visual variety.
- if (SystemAPI.TryGetSingleton(out var clutterSpawner)
- && clutterSpawner.Prefab != Entity.Null)
- {
- var clutterXform = SystemAPI.GetComponent(clutterSpawner.Prefab);
- var prefabClutter = SystemAPI.GetComponent(clutterSpawner.Prefab);
- var crng = new Random((uint)math.max(1, runtime.ExpeditionEpoch * 2 + 1));
- int ccount = math.max(1, clutterSpawner.Count);
- for (int i = 0; i < ccount; i++)
- {
- var piece = ecb.Instantiate(clutterSpawner.Prefab);
-
- float ang = crng.NextFloat(0f, math.PI * 2f);
- float rad = clutterSpawner.Radius * math.sqrt(crng.NextFloat(0f, 1f));
- var xform = clutterXform;
- xform.Position = origin + new float3(math.cos(ang) * rad, 0f, math.sin(ang) * rad);
- ecb.SetComponent(piece, xform);
-
- var bc = prefabClutter;
- bc.Variant = (byte)(i % 3);
- ecb.SetComponent(piece, bc);
- }
- }
-
- runtime.LastSpawnedEpoch = runtime.ExpeditionEpoch;
- }
-
- // DESTROY: the last player left the expedition — clear the whole field (nodes + clutter + zone enemies).
- if (wasOccupied && !occupied)
- {
- // Only EXPEDITION nodes — the base field is permanent RegionTag{Base} and must NOT be torn down here.
- foreach (var (rn, region, e) in
- SystemAPI.Query, RefRO>().WithEntityAccess())
- if (region.ValueRO.Region == RegionId.Expedition)
- ecb.DestroyEntity(e);
- // Blight clutter is Expedition-only today; guard by region defensively (DR-040 MINOR 1).
- foreach (var (region, e) in
- SystemAPI.Query>().WithAll().WithEntityAccess())
- if (region.ValueRO.Region == RegionId.Expedition)
- ecb.DestroyEntity(e);
- // Zone combat enemies share this single lifetime point (DR-040). EnemyTag + ZoneEnemyTag +
- // RegionTag{Expedition}; disjoint from the node/clutter queries, so no double-destroy.
- foreach (var (region, e) in
- SystemAPI.Query>().WithAll().WithEntityAccess())
- if (region.ValueRO.Region == RegionId.Expedition)
- ecb.DestroyEntity(e);
- }
-
- runtime.PrevExpeditionOccupied = (byte)(occupied ? 1 : 0);
- 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
deleted file mode 100644
index cbf23f296..000000000
--- a/Assets/_Project/Scripts/Server/Economy/ExpeditionFieldSystem.cs.meta
+++ /dev/null
@@ -1,2 +0,0 @@
-fileFormatVersion: 2
-guid: 9267d7809e68ea54caa55378f33e67f6
\ No newline at end of file
diff --git a/Assets/_Project/Scripts/Server/Economy/RoomFieldSystem.cs b/Assets/_Project/Scripts/Server/Economy/RoomFieldSystem.cs
new file mode 100644
index 000000000..80dda33cd
--- /dev/null
+++ b/Assets/_Project/Scripts/Server/Economy/RoomFieldSystem.cs
@@ -0,0 +1,146 @@
+using ProjectM.Simulation;
+using Unity.Burst;
+using Unity.Collections;
+using Unity.Entities;
+using Unity.Mathematics;
+using Unity.Transforms;
+
+namespace ProjectM.Server
+{
+ ///
+ /// Server-only per-ROOM field seeder — the Step-5 successor of the presence-keyed ExpeditionFieldSystem.
+ /// When the run FSM has a room active ( == InRoom) and
+ /// has advanced past the epoch this system last seeded (int equality, never
+ /// tick math), it resolves the active room's from the map node RunDirectorSystem published
+ /// ( — the single plan authority; NEVER re-derived here) and scatters
+ /// plan.NodeCount resource nodes — FLOORED by the run-wide scarcity budget
+ /// , which this system spends down (documented co-write: RunDirector
+ /// STAGES the budget at launch; this system only decrements it) — plus a light Blight-clutter dressing, all
+ /// inside the room's shape at (base, ActiveSubSlot). Every spawn is
+ /// stamped {room} (the teardown contract) on top of the prefab-baked RegionTag{Expedition};
+ /// Scale is preserved via baked.WithPosition (never FromPosition).
+ ///
+ /// Teardown: room-advance/return teardown belongs to RunDirectorSystem (RoomTeardown, Step 7). This system keeps
+ /// ONE defensive sweep — Staging with any alive → destroy them all (idempotent; covers
+ /// abort/disconnect edges). Untagged ghosts (base field, structures) are structurally untouchable.
+ ///
+ /// Ordering: [UpdateAfter(RunDirectorSystem)] so it reads the freshly-advanced room state same-tick.
+ /// The old inherited [UpdateAfter(CyclePhaseSystem)] is deliberately DROPPED and NO CyclePhase edge may
+ /// ever return to the room chain (the Play-only sort-cycle rule — invisible to EditMode).
+ ///
+ [BurstCompile]
+ [WorldSystemFilter(WorldSystemFilterFlags.ServerSimulation)]
+ [UpdateInGroup(typeof(SimulationSystemGroup))]
+ [UpdateAfter(typeof(RunDirectorSystem))]
+ public partial struct RoomFieldSystem : ISystem
+ {
+ /// Max clutter pieces per room — cosmetic ghosts still cost relevancy, keep the dressing light.
+ const int MaxClutterPerRoom = 6;
+
+ EntityQuery m_RoomTagged;
+
+ [BurstCompile]
+ public void OnCreate(ref SystemState state)
+ {
+ state.RequireForUpdate();
+ state.RequireForUpdate();
+ state.RequireForUpdate();
+ m_RoomTagged = state.GetEntityQuery(ComponentType.ReadOnly());
+ }
+
+ [BurstCompile]
+ public void OnUpdate(ref SystemState state)
+ {
+ var dirEntity = SystemAPI.GetSingletonEntity();
+ var info = SystemAPI.GetComponent(dirEntity);
+ var run = SystemAPI.GetComponent(dirEntity);
+
+ var spawnerEntity = SystemAPI.GetSingletonEntity();
+ var spawner = SystemAPI.GetComponent(spawnerEntity);
+
+ // One-shot: attach this system's server-only bookkeeping beside the baked spawner singleton.
+ if (!SystemAPI.HasComponent(spawnerEntity))
+ {
+ state.EntityManager.AddComponentData(spawnerEntity, new RoomFieldState());
+ return; // structural change — clean re-read next tick
+ }
+ var rf = SystemAPI.GetComponent(spawnerEntity);
+
+ var ecb = new EntityCommandBuffer(Allocator.Temp);
+
+ if (info.Lifecycle == RunLifecycle.InRoom)
+ {
+ if (rf.LastSpawnedRoomEpoch != run.RoomEpoch && spawner.Prefab != Entity.Null)
+ {
+ float3 baseCenter = new float3(0f, 1f, 0f);
+ if (SystemAPI.TryGetSingleton(out var anchor))
+ baseCenter = BaseGridMath.PlotCenter(anchor);
+ float3 origin = RegionMath.ExpeditionRoomOrigin(baseCenter, run.ActiveSubSlot);
+
+ // Single plan authority: the node RunDirector published — never re-derived from the col/path.
+ var map = RunMapMath.Generate(run.RunSeed);
+ var node = map.NodeAt(run.CurrentNodeId);
+ var plan = RoomLayoutMath.Plan(node, info.CurrentRoom, info.RoomCount);
+ byte room = (byte)(info.CurrentRoom & 0xFF);
+
+ // Scarcity: the run-wide budget floors this room's count and is spent down (never negative).
+ int count = math.min(plan.NodeCount, math.max(0, run.NodeBudgetRemaining));
+ if (count > 0)
+ {
+ var baked = SystemAPI.GetComponent(spawner.Prefab);
+ var prefabNode = SystemAPI.GetComponent(spawner.Prefab);
+ var rng = new Random(RunMapMath.Hash(run.RunSeed, (uint)run.CurrentNodeId, 0x0DEu) | 1u);
+ for (int i = 0; i < count; i++)
+ {
+ var e = ecb.Instantiate(spawner.Prefab);
+ float3 pos = RoomLayoutMath.ScatterInShape(plan.ShapeId, origin, i, count, ref rng);
+ ecb.SetComponent(e, baked.WithPosition(pos));
+ // Rarity-weighted resource type (Step 11): Ore 45% (building) / Biomass 40% (walls,
+ // fabricator) / AETHER 15% — the scarce permanent-meta currency, felt when it drops.
+ var rn = prefabNode;
+ int roll = rng.NextInt(0, 100);
+ rn.ResourceId = roll < 15 ? ResourceId.Aether : roll < 60 ? ResourceId.Ore : ResourceId.Biomass;
+ ecb.SetComponent(e, rn);
+ ecb.AddComponent(e, new RoomTag { Room = room });
+ }
+ run.NodeBudgetRemaining -= count;
+ SystemAPI.SetComponent(dirEntity, run); // the documented budget co-write (spend only)
+ }
+
+ // Clutter dressing (OPTIONAL singleton) — a DISTINCT seed so it never co-locates with nodes.
+ if (SystemAPI.TryGetSingleton(out var clutter)
+ && clutter.Prefab != Entity.Null)
+ {
+ var cBaked = SystemAPI.GetComponent(clutter.Prefab);
+ var cProto = SystemAPI.GetComponent(clutter.Prefab);
+ var crng = new Random(RunMapMath.Hash(run.RunSeed, (uint)run.CurrentNodeId, 0xC17u) | 1u);
+ int cCount = math.min(math.max(1, clutter.Count), MaxClutterPerRoom);
+ for (int i = 0; i < cCount; i++)
+ {
+ var e = ecb.Instantiate(clutter.Prefab);
+ float3 pos = RoomLayoutMath.ScatterInShape(plan.ShapeId, origin, i, cCount, ref crng);
+ ecb.SetComponent(e, cBaked.WithPosition(pos));
+ var bc = cProto;
+ bc.Variant = (byte)(i % 3);
+ ecb.SetComponent(e, bc);
+ ecb.AddComponent(e, new RoomTag { Room = room });
+ }
+ }
+
+ rf.LastSpawnedRoomEpoch = run.RoomEpoch;
+ SystemAPI.SetComponent(spawnerEntity, rf);
+ }
+ }
+ else if (info.Lifecycle == RunLifecycle.Staging && !m_RoomTagged.IsEmpty)
+ {
+ // Defensive sweep: no run active but room ghosts linger (abort/disconnect edge) — clear every room.
+ var ents = m_RoomTagged.ToEntityArray(Allocator.Temp);
+ for (int i = 0; i < ents.Length; i++)
+ ecb.DestroyEntity(ents[i]);
+ ents.Dispose();
+ }
+
+ ecb.Playback(state.EntityManager);
+ }
+ }
+}
diff --git a/Assets/_Project/Scripts/Server/Economy/RoomFieldSystem.cs.meta b/Assets/_Project/Scripts/Server/Economy/RoomFieldSystem.cs.meta
new file mode 100644
index 000000000..efb11a6da
--- /dev/null
+++ b/Assets/_Project/Scripts/Server/Economy/RoomFieldSystem.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: b2ba012b5e31bcc48b50dd14220c9fc5
\ No newline at end of file
diff --git a/Assets/_Project/Scripts/Server/Meta.meta b/Assets/_Project/Scripts/Server/Meta.meta
new file mode 100644
index 000000000..ab6d0f3d2
--- /dev/null
+++ b/Assets/_Project/Scripts/Server/Meta.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 032a0f4a47c8f1d459bd341f50d42054
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/_Project/Scripts/Server/Meta/MetaSpendSystem.cs b/Assets/_Project/Scripts/Server/Meta/MetaSpendSystem.cs
new file mode 100644
index 000000000..66438bf07
--- /dev/null
+++ b/Assets/_Project/Scripts/Server/Meta/MetaSpendSystem.cs
@@ -0,0 +1,147 @@
+using ProjectM.Simulation;
+using Unity.Burst;
+using Unity.Collections;
+using Unity.Entities;
+using Unity.NetCode;
+
+namespace ProjectM.Server
+{
+ ///
+ /// Server receiver for — the PERMANENT meta-upgrade purchase (Aether → tier).
+ /// Honored ONLY in Staging (N4: the base shop is a between-runs surface; mid-run Aether belongs to the run).
+ /// Per request, IN-LOOP against the live director buffers (the DR-014 placement idiom — two same-tick purchases
+ /// on barely-enough Aether cannot both pass): resolve sender → , validate catalog id /
+ /// class mask (, never raw 1<<ClassId) / MaxTier / prereq, price the NEXT tier
+ /// ( — tier is server-computed, never on the wire), then
+ /// pre-check BEFORE (Withdraw CLAMPS, it
+ /// never rejects), bump-or-append the row, and upsert the ABSOLUTE-value meta
+ /// StatModifier (R-F1: Value = ValuePerTier * newTier, keyed Tuning.MetaSourceIdBase + id) on every
+ /// pre-collected live player of that class (R-F2 — offline classmates get theirs born-correct at next spawn via
+ /// GoInGameServerSystem). Success raises so the tier is on disk before a crash.
+ /// Plain server group, before RunDirectorSystem (the receiver convention); requests are ALWAYS destroyed.
+ ///
+ [BurstCompile]
+ [WorldSystemFilter(WorldSystemFilterFlags.ServerSimulation)]
+ [UpdateInGroup(typeof(SimulationSystemGroup))]
+ [UpdateBefore(typeof(RunDirectorSystem))]
+ public partial struct MetaSpendSystem : ISystem
+ {
+ [BurstCompile]
+ public void OnCreate(ref SystemState state)
+ {
+ var builder = new EntityQueryBuilder(Allocator.Temp)
+ .WithAll();
+ state.RequireForUpdate(state.GetEntityQuery(builder));
+ state.RequireForUpdate();
+ state.RequireForUpdate();
+ state.RequireForUpdate();
+ }
+
+ [BurstCompile]
+ public void OnUpdate(ref SystemState state)
+ {
+ // N4 phase gate — hoisted (per-tick-uniform, like the ReadyToggle accept flag).
+ bool accept = SystemAPI.GetSingleton().Lifecycle == RunLifecycle.Staging;
+
+ var catalog = SystemAPI.GetSingleton();
+ var director = SystemAPI.GetSingletonEntity();
+ if (!catalog.Value.IsCreated || !SystemAPI.HasBuffer(director))
+ accept = false; // authoring hole: drop the requests below (no withdraw happened; nothing to roll back)
+
+ // Sender resolution (SourceConnection → NetworkId → GhostOwner → player, the ReadyToggle idiom).
+ var playerByConn = new NativeHashMap(8, Allocator.Temp);
+ // R-F2: pre-collect the live (player, class) pairs ONCE — a successful purchase upserts the modifier on
+ // every live member of the class, not just the buyer (shared per-class pool, operator default).
+ var classMembers = new NativeList(8, Allocator.Temp);
+ var classIds = new NativeList(8, Allocator.Temp);
+ foreach (var (owner, playerClass, entity) in
+ SystemAPI.Query, RefRO>()
+ .WithAll().WithEntityAccess())
+ {
+ playerByConn[owner.ValueRO.NetworkId] = entity;
+ classMembers.Add(entity);
+ classIds.Add(playerClass.ValueRO.ClassId);
+ }
+
+ var ecb = new EntityCommandBuffer(Allocator.Temp);
+ foreach (var (receive, req, requestEntity) in
+ SystemAPI.Query, RefRO>().WithEntityAccess())
+ {
+ ecb.DestroyEntity(requestEntity); // ALWAYS consumed, accepted or not
+ if (!accept) continue;
+
+ var conn = receive.ValueRO.SourceConnection;
+ if (!SystemAPI.HasComponent(conn)
+ || !playerByConn.TryGetValue(SystemAPI.GetComponent(conn).Value, out var buyer))
+ continue;
+ byte classId = SystemAPI.GetComponent(buyer).ClassId;
+
+ ref var pool = ref catalog.Value.Value;
+ int defIdx = MetaMath.FindDef(ref pool, req.ValueRO.UpgradeId);
+ if (defIdx < 0) continue; // unknown id — dropped (a forged/stale request, not a crash)
+ ref var def = ref pool.Defs[defIdx];
+ if ((def.ClassMask & BoonMath.MaskFor(classId)) == 0) continue;
+
+ // LIVE in-loop reads (no hoist — the previous request this tick may have bumped the tier or
+ // drained the ledger; hoisted copies would let both pass).
+ var record = SystemAPI.GetBuffer(director);
+ byte owned = MetaMath.TierOf(record, classId, req.ValueRO.UpgradeId);
+ if (owned >= def.MaxTier) continue;
+ if (def.PrereqId != 0xFF && MetaMath.TierOf(record, classId, def.PrereqId) < def.PrereqTier)
+ continue;
+
+ int cost = MetaMath.CostForTier(in def, owned);
+ var ledger = SystemAPI.GetBuffer(director);
+ if (StorageMath.TotalOf(ledger, ResourceId.Aether) < cost) continue; // pre-check: Withdraw CLAMPS
+ StorageMath.Withdraw(ledger, ResourceId.Aether, cost); // atomic commit (DR-014)
+
+ byte newTier = (byte)(owned + 1);
+ bool bumped = false;
+ for (int i = 0; i < record.Length; i++)
+ if (record[i].ClassId == classId && record[i].UpgradeId == req.ValueRO.UpgradeId)
+ {
+ record[i] = new MetaTierState { ClassId = classId, UpgradeId = req.ValueRO.UpgradeId, Tier = newTier };
+ bumped = true;
+ break;
+ }
+ if (!bumped)
+ record.Add(new MetaTierState { ClassId = classId, UpgradeId = req.ValueRO.UpgradeId, Tier = newTier });
+
+ // R-F1: ABSOLUTE-value upsert (Value = ValuePerTier * newTier) — never an incremental append; a
+ // second append would double-count in StatRecomputeSystem's sum.
+ uint sourceId = Tuning.MetaSourceIdBase + req.ValueRO.UpgradeId;
+ for (int p = 0; p < classMembers.Length; p++)
+ {
+ if (classIds[p] != classId) continue;
+ var mods = SystemAPI.GetBuffer(classMembers[p]);
+ bool upserted = false;
+ for (int m = 0; m < mods.Length; m++)
+ if (mods[m].SourceId == sourceId)
+ {
+ var row = mods[m];
+ row.Value = def.ValuePerTier * newTier;
+ mods[m] = row;
+ upserted = true;
+ break;
+ }
+ if (!upserted)
+ mods.Add(new StatModifier
+ {
+ Target = def.Target,
+ Op = def.Op,
+ Value = def.ValuePerTier * newTier,
+ SourceId = sourceId,
+ });
+ }
+
+ // Persist immediately — the tier is real money (Aether); a crash must not eat it.
+ if (SystemAPI.HasComponent(director))
+ SystemAPI.SetComponent(director, new SaveRequest { Pending = 1 });
+ }
+ ecb.Playback(state.EntityManager);
+ playerByConn.Dispose();
+ classMembers.Dispose();
+ classIds.Dispose();
+ }
+ }
+}
diff --git a/Assets/_Project/Scripts/Server/Meta/MetaSpendSystem.cs.meta b/Assets/_Project/Scripts/Server/Meta/MetaSpendSystem.cs.meta
new file mode 100644
index 000000000..69badb00a
--- /dev/null
+++ b/Assets/_Project/Scripts/Server/Meta/MetaSpendSystem.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 9b277eb9da63a054db9f9b3e041d582b
\ No newline at end of file
diff --git a/Assets/_Project/Scripts/Server/Persistence/SaveWriteSystem.cs b/Assets/_Project/Scripts/Server/Persistence/SaveWriteSystem.cs
index 838e20372..8794e30e6 100644
--- a/Assets/_Project/Scripts/Server/Persistence/SaveWriteSystem.cs
+++ b/Assets/_Project/Scripts/Server/Persistence/SaveWriteSystem.cs
@@ -55,6 +55,9 @@ namespace ProjectM.Server
// M7: also persist player-built structures + their production tick-state / inventory (single shared scan).
uint nowTick = SystemAPI.GetSingleton().ServerTick.TickIndexForValidTick;
SaveStructureScan.Collect(EntityManager, nowTick, out var structures, out var structureIo);
+ // v6: the permanent-meta slice via the ONE shared collector (drift-proof vs the quit-to-menu writer).
+ MetaSaveScan.Collect(EntityManager, dir, out var metaRows, out var runsCompleted, out var maxDepth);
+
SaveService.Save(new SaveData
{
@@ -62,6 +65,9 @@ namespace ProjectM.Server
GoalTarget = goal.Target,
CoreCurrent = core.Current,
RunOutcome = outcome.Value,
+ RunsCompleted = runsCompleted,
+ MaxDepthReached = maxDepth,
+ MetaUpgrades = metaRows,
Ledger = rows,
Structures = structures,
diff --git a/Assets/_Project/Scripts/Server/World/CycleDirectorSpawnSystem.cs b/Assets/_Project/Scripts/Server/World/CycleDirectorSpawnSystem.cs
index 34404a460..581c2ed41 100644
--- a/Assets/_Project/Scripts/Server/World/CycleDirectorSpawnSystem.cs
+++ b/Assets/_Project/Scripts/Server/World/CycleDirectorSpawnSystem.cs
@@ -62,6 +62,15 @@ namespace ProjectM.Server
// spawn like CycleRuntime/ThreatState (never on the ghost serializer). RunOutcome is baked on the prefab.
ecb.AddComponent(director, new RunPhase { Value = RunPhaseId.Normal });
+ // Expedition redesign: run-FSM working state + the co-op route first-commit latch + the persisted
+ // meta counters — ALL added UNCONDITIONALLY at spawn (the CycleRuntime/ThreatState/RunPhase idiom;
+ // D-F2: a New-Game boot must have the components the bank block reads; Continue restores VALUES only,
+ // inside the HasData block — Step 12b). HostSalt starts a fixed non-tick seed lineage (bumped per
+ // launch); SaveData v6 folds persisted RunsCompleted in at restore so cross-session runs diverge.
+ ecb.AddComponent(director, new RunRuntime { HostSalt = 0x5EED0001u });
+ ecb.AddComponent(director, default(RouteCommand));
+ ecb.AddComponent(director, default(MetaCounters));
+
// Born-correct load: if the menu staged a save (Continue), apply it AT SPAWN so the director
// DR-042 C6c: a NEW game seeds starting Ore below; a restored save (Continue) keeps its ledger.
bool restoredLedger = false;
@@ -98,6 +107,33 @@ namespace ProjectM.Server
// save / New Game = 0 -> InProgress). Independent of the Core -> NOT nested in the CoreIntegrity guard.
ecb.SetComponent(director, new RunOutcome { Value = pending.RunOutcome });
+ // v6: restore the permanent meta — counters (VALUES only; the component was added
+ // unconditionally above, D-F2), the tier record (SetBuffer replaces the baked-empty
+ // [GhostField] buffer pre-Playback — the StorageEntry idiom — rows VERBATIM incl. unknown
+ // ids), the born-correct RunInfo HUD mirror (the spawn-time exception to RunDirector's
+ // sole-writer rule, like CycleState/RunOutcome above), and the HostSalt fold (cross-session
+ // first-run maps diverge once you've banked clears — the promise at the RunRuntime add).
+ ecb.SetComponent(director, new MetaCounters
+ {
+ RunsCompleted = pending.RunsCompleted,
+ MaxDepthReached = pending.MaxDepthReached,
+ });
+ var metaSrc = SystemAPI.GetBuffer(pendingEntity);
+ var metaDst = ecb.SetBuffer(director);
+ for (int mi = 0; mi < metaSrc.Length; mi++)
+ metaDst.Add(new MetaTierState { ClassId = metaSrc[mi].ClassId, UpgradeId = metaSrc[mi].UpgradeId, Tier = metaSrc[mi].Tier });
+ if (SystemAPI.HasComponent(spawner.Prefab))
+ {
+ var runInfo = SystemAPI.GetComponent(spawner.Prefab); // baked Lifecycle=Staging
+ runInfo.RunsCompleted = pending.RunsCompleted;
+ runInfo.MaxDepthReached = pending.MaxDepthReached;
+ ecb.SetComponent(director, runInfo);
+ }
+ ecb.SetComponent(director, new RunRuntime
+ {
+ HostSalt = RunMapMath.Hash(0x5EED0001u, (uint)pending.RunsCompleted + 1u),
+ });
+
}
ecb.DestroyEntity(pendingEntity);
}
diff --git a/Assets/_Project/Scripts/Server/World/ExpeditionGateSystem.cs b/Assets/_Project/Scripts/Server/World/ExpeditionGateSystem.cs
deleted file mode 100644
index fcd31221a..000000000
--- a/Assets/_Project/Scripts/Server/World/ExpeditionGateSystem.cs
+++ /dev/null
@@ -1,122 +0,0 @@
-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 BASE signals the ThreatDirector (a completed expedition can draw a
- /// retaliation siege) by incrementing . Plain server
- /// SimulationSystemGroup, ordered BEFORE CyclePhaseSystem (Gate -> ThreatDirector -> RunState) so the return is
- /// consumed the same tick. Arrival points are offset from the destination gate so a transited player does not
- /// immediately re-trigger.
- ///
- [BurstCompile]
- [WorldSystemFilter(WorldSystemFilterFlags.ServerSimulation)]
- [UpdateInGroup(typeof(SimulationSystemGroup))]
- [UpdateBefore(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();
-
- // A player returned to base from an expedition -> signal the ThreatDirector (it sizes/arms any
- // retaliation siege). The gate teleports the returner out of its radius, so this fires once per return.
- if (returnedToBase)
- {
- if (SystemAPI.TryGetSingletonEntity(out var threatEntity))
- {
- var threat = SystemAPI.GetComponent(threatEntity);
- threat.PendingReturns += 1;
- threat.ExpeditionsCompleted += 1;
- SystemAPI.SetComponent(threatEntity, threat);
- }
-
- // Once-per-epoch zone-clear reward: a returner BANKS flat Ore to the shared ledger AND advances the
- // long-arc win meter (DR-042 — EXPEDITION CLEARS, not survived base sieges, are the win-driver:
- // CyclePhaseSystem no longer credits Charge, so this is the sole PRODUCTION writer of GoalProgress.Charge).
- // Resolved ONCE here (not per-returner) so two same-tick co-op returns pay exactly once (DR-040 BLOCKER 4)
- // and gate re-entry before a clear can't farm (MINOR 2). Ore + Charge share the SAME LastRewardedEpoch
- // latch so they always share fate (never one without the other). The Charge credit is guarded
- // independently of the ledger so it still lands in ledger-less worlds.
- if (SystemAPI.HasSingleton())
- {
- var cycleEntity = SystemAPI.GetSingletonEntity();
- var runtime = SystemAPI.GetComponent(cycleEntity);
- if (runtime.ClearedThisEpoch != 0 && runtime.LastRewardedEpoch != runtime.ExpeditionEpoch)
- {
- if (SystemAPI.TryGetSingleton(out var zoneDir)
- && SystemAPI.HasSingleton())
- {
- var ledger = SystemAPI.GetBuffer(SystemAPI.GetSingletonEntity());
- StorageMath.Deposit(ledger, (ushort)ResourceId.Ore, zoneDir.RewardOre);
- }
- if (SystemAPI.HasComponent(cycleEntity))
- {
- // +1 toward the goal per cleared expedition, CLAMPED to Target (single production writer).
- var goal = SystemAPI.GetComponent(cycleEntity);
- goal.Charge = math.min(goal.Charge + 1, goal.Target);
- SystemAPI.SetComponent(cycleEntity, goal);
- }
- // Checkpoint the hard-won clear (replaces the deleted survived-siege autosave in CyclePhaseSystem).
- if (SystemAPI.HasComponent(cycleEntity))
- SystemAPI.SetComponent(cycleEntity, new SaveRequest { Pending = 1 });
- runtime.LastRewardedEpoch = runtime.ExpeditionEpoch;
- SystemAPI.SetComponent(cycleEntity, runtime);
- }
- }
- }
- }
- }
-}
diff --git a/Assets/_Project/Scripts/Server/World/ExpeditionGateSystem.cs.meta b/Assets/_Project/Scripts/Server/World/ExpeditionGateSystem.cs.meta
deleted file mode 100644
index 90ce049a2..000000000
--- a/Assets/_Project/Scripts/Server/World/ExpeditionGateSystem.cs.meta
+++ /dev/null
@@ -1,2 +0,0 @@
-fileFormatVersion: 2
-guid: 4292536f663eb5c4d92688f6c5bb0368
\ No newline at end of file
diff --git a/Assets/_Project/Scripts/Server/World/ReadyToggleSystem.cs b/Assets/_Project/Scripts/Server/World/ReadyToggleSystem.cs
new file mode 100644
index 000000000..69750d111
--- /dev/null
+++ b/Assets/_Project/Scripts/Server/World/ReadyToggleSystem.cs
@@ -0,0 +1,61 @@
+using ProjectM.Simulation;
+using Unity.Burst;
+using Unity.Collections;
+using Unity.Entities;
+using Unity.NetCode;
+
+namespace ProjectM.Server
+{
+ ///
+ /// Server receiver for : resolves the sender (SourceConnection → NetworkId →
+ /// GhostOwner → player entity, the AbilityUpgradeSystem idiom) and SETS .
+ /// Honored ONLY while the run FSM is in Staging or Launching — an un-ready during the Launching countdown is the
+ /// launch-abort escape hatch (RunDirectorSystem reverts to Staging); toggles arriving mid-run are dropped (the
+ /// Returning edge clears every flag anyway). Ordered BEFORE RunDirectorSystem so a toggle lands the same tick the
+ /// ready-count is derived. Plain server group (one-off RPC effects never run in the predicted loop); the request
+ /// entity is ALWAYS destroyed.
+ ///
+ [BurstCompile]
+ [WorldSystemFilter(WorldSystemFilterFlags.ServerSimulation)]
+ [UpdateInGroup(typeof(SimulationSystemGroup))]
+ [UpdateBefore(typeof(RunDirectorSystem))]
+ public partial struct ReadyToggleSystem : ISystem
+ {
+ [BurstCompile]
+ public void OnCreate(ref SystemState state)
+ {
+ var builder = new EntityQueryBuilder(Allocator.Temp)
+ .WithAll();
+ state.RequireForUpdate(state.GetEntityQuery(builder));
+ state.RequireForUpdate();
+ }
+
+ [BurstCompile]
+ public void OnUpdate(ref SystemState state)
+ {
+ byte lifecycle = SystemAPI.GetSingleton().Lifecycle;
+ bool accept = lifecycle == RunLifecycle.Staging || lifecycle == RunLifecycle.Launching;
+
+ 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 (receive, req, requestEntity) in
+ SystemAPI.Query, RefRO