Run Re-Do
This commit is contained in:
+15
-2
@@ -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
|
||||
|
||||
+214
-83
@@ -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
|
||||
|
||||
Binary file not shown.
@@ -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:
|
||||
Binary file not shown.
@@ -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:
|
||||
Binary file not shown.
@@ -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:
|
||||
Binary file not shown.
@@ -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:
|
||||
Binary file not shown.
@@ -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:
|
||||
Binary file not shown.
@@ -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:
|
||||
Binary file not shown.
@@ -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:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5532ca12b65632c4583c1be034e2393b
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: aa9ffc37c66896e43ba6cd690104c528
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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}
|
||||
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 0eb166271cf8ef747ba1236e70a8e46e
|
||||
PrefabImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Binary file not shown.
@@ -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:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 9e5f10ea2cf310a46956ebeb646c41e8
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Binary file not shown.
@@ -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:
|
||||
Binary file not shown.
@@ -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:
|
||||
Binary file not shown.
@@ -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:
|
||||
Binary file not shown.
@@ -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:
|
||||
@@ -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
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using ProjectM.Simulation;
|
||||
using Unity.Collections;
|
||||
using Unity.Entities;
|
||||
using UnityEngine;
|
||||
|
||||
namespace ProjectM.Authoring
|
||||
{
|
||||
/// <summary>
|
||||
/// 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 (<see cref="BoonCatalogData.BuildDefault"/>)
|
||||
/// 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).
|
||||
/// </summary>
|
||||
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<BoonRow> Rows = new List<BoonRow>();
|
||||
|
||||
private class BoonCatalogBaker : Baker<BoonCatalogAuthoring>
|
||||
{
|
||||
public override void Bake(BoonCatalogAuthoring authoring)
|
||||
{
|
||||
var entity = GetEntity(authoring, TransformUsageFlags.None);
|
||||
|
||||
BlobAssetReference<BoonCatalogBlob> 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<BoonCatalogBlob>();
|
||||
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<BoonCatalogBlob>(Allocator.Persistent);
|
||||
builder.Dispose();
|
||||
}
|
||||
|
||||
AddBlobAsset(ref blob, out _);
|
||||
AddComponent(entity, new BoonCatalog { Value = blob });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 075a570afb8ef9541b6307280b53f4e7
|
||||
@@ -1,51 +0,0 @@
|
||||
using ProjectM.Simulation;
|
||||
using Unity.Entities;
|
||||
using UnityEngine;
|
||||
|
||||
namespace ProjectM.Authoring
|
||||
{
|
||||
/// <summary>
|
||||
/// Authoring for the home-base mining field (<see cref="BaseFieldSpawner"/>). Place ONE in the gameplay
|
||||
/// subscene. <see cref="NodePrefab"/> = the SAME ResourceNode ghost prefab the expedition uses; the server
|
||||
/// system overrides each instance to RegionTag{Base} + ResourceId.Ore and scatters them in the
|
||||
/// [<see cref="InnerRadius"/>, <see cref="OuterRadius"/>] 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.
|
||||
/// </summary>
|
||||
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<BaseFieldSpawnerAuthoring>
|
||||
{
|
||||
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 });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c4055a6a779d06949ae23b16334b810e
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d0503ff85f3b39f49af750a451d44e00
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,85 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using ProjectM.Simulation;
|
||||
using Unity.Collections;
|
||||
using Unity.Entities;
|
||||
using UnityEngine;
|
||||
|
||||
namespace ProjectM.Authoring
|
||||
{
|
||||
/// <summary>
|
||||
/// 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
|
||||
/// (<see cref="MetaCatalogData.BuildDefault"/>) 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).
|
||||
/// </summary>
|
||||
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<MetaRow> Rows = new List<MetaRow>();
|
||||
|
||||
private class MetaCatalogBaker : Baker<MetaCatalogAuthoring>
|
||||
{
|
||||
public override void Bake(MetaCatalogAuthoring authoring)
|
||||
{
|
||||
var entity = GetEntity(authoring, TransformUsageFlags.None);
|
||||
|
||||
BlobAssetReference<MetaUpgradeCatalogBlob> 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<MetaUpgradeCatalogBlob>();
|
||||
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<MetaUpgradeCatalogBlob>(Allocator.Persistent);
|
||||
builder.Dispose();
|
||||
}
|
||||
|
||||
AddBlobAsset(ref blob, out _);
|
||||
AddComponent(entity, new MetaUpgradeCatalog { Value = blob });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 4f007f7870c0afd4e93ea5b86fd21c8b
|
||||
@@ -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<PlayerReady>(entity);
|
||||
AddComponent<BoonOffer>(entity);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<MetaTierState>(entity);
|
||||
|
||||
|
||||
AddComponent(entity, new ThreatConfig
|
||||
{
|
||||
|
||||
@@ -1,45 +0,0 @@
|
||||
using ProjectM.Simulation;
|
||||
using Unity.Entities;
|
||||
using Unity.Mathematics;
|
||||
using UnityEngine;
|
||||
|
||||
namespace ProjectM.Authoring
|
||||
{
|
||||
/// <summary>
|
||||
/// Authoring for a walk-in <see cref="ExpeditionGate"/>. Place on a visible gate object in the gameplay
|
||||
/// subscene; baked into both worlds at the gate's position (the server reads its LocalTransform for the
|
||||
/// overlap test, the client renders the mesh). Set From/To regions + the arrival point in the destination
|
||||
/// region (offset from that region's gate so the player doesn't immediately re-trigger).
|
||||
/// </summary>
|
||||
public class ExpeditionGateAuthoring : MonoBehaviour
|
||||
{
|
||||
public enum Region : byte { Base = 0, Expedition = 1 }
|
||||
|
||||
[Tooltip("Region a player must be in for this gate to act on them.")]
|
||||
public Region From = Region.Base;
|
||||
|
||||
[Tooltip("Region the player is transited to.")]
|
||||
public Region To = Region.Expedition;
|
||||
|
||||
[Min(0.5f)] public float Radius = 2.5f;
|
||||
|
||||
[Tooltip("Where the player arrives in the destination region (offset from that region's gate).")]
|
||||
public Vector3 ArrivalPos = new Vector3(1000f, 1f, 0f);
|
||||
|
||||
private class ExpeditionGateBaker : Baker<ExpeditionGateAuthoring>
|
||||
{
|
||||
public override void Bake(ExpeditionGateAuthoring authoring)
|
||||
{
|
||||
// Dynamic so the baked entity carries a LocalTransform the server can read for the overlap test.
|
||||
var entity = GetEntity(authoring, TransformUsageFlags.Dynamic);
|
||||
AddComponent(entity, new ExpeditionGate
|
||||
{
|
||||
FromRegion = (byte)authoring.From,
|
||||
ToRegion = (byte)authoring.To,
|
||||
Radius = authoring.Radius,
|
||||
ArrivalPos = new float3(authoring.ArrivalPos.x, authoring.ArrivalPos.y, authoring.ArrivalPos.z),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 22f744b59ad23834abe28fc09b661005
|
||||
@@ -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;
|
||||
/// <summary>Runtime hook (HUD Aether button) + execute_code: queue an ability-damage upgrade.</summary>
|
||||
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();
|
||||
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<Collider>();
|
||||
if (col != null) Object.Destroy(col);
|
||||
var mr = _ghost.GetComponent<MeshRenderer>();
|
||||
mr.sharedMaterial = _ghostMat;
|
||||
mr.shadowCastingMode = UnityEngine.Rendering.ShadowCastingMode.Off;
|
||||
mr.receiveShadows = false;
|
||||
_ghostMf = _ghost.GetComponent<MeshFilter>();
|
||||
_cubeMesh = _ghostMf.sharedMesh;
|
||||
_ghostType = 255;
|
||||
_ghostMr = _ghost.GetComponent<MeshRenderer>();
|
||||
_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.)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
using ProjectM.Simulation;
|
||||
using Unity.Entities;
|
||||
using Unity.NetCode;
|
||||
using UnityEngine;
|
||||
|
||||
namespace ProjectM.Client
|
||||
{
|
||||
/// <summary>
|
||||
/// Client-side boon-pick sender: a static enqueue (the Step-14 3-card modal / execute_code) drained into
|
||||
/// <see cref="BoonPickRequest"/> RPCs. Carries only the option INDEX — the server resolves it against the
|
||||
/// sender's own authoritative <c>BoonOffer</c> and validates lifecycle/pending, so a stale or forged pick is
|
||||
/// simply dropped. Statics reset on play-enter (the stale-bridge hazard).
|
||||
/// </summary>
|
||||
[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;
|
||||
}
|
||||
|
||||
/// <summary>Queue a boon pick (0/1/2). The HUD card click + execute_code drive this.</summary>
|
||||
public static void PickBoon(byte optionIndex)
|
||||
{
|
||||
s_PendingIndex = optionIndex;
|
||||
s_Pending++;
|
||||
}
|
||||
|
||||
protected override void OnCreate()
|
||||
{
|
||||
RequireForUpdate<NetworkId>();
|
||||
}
|
||||
|
||||
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 });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d7e90bc7230614845817b81f0f7b70f0
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 60aece22e94371c468102dbf35c3fe47
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,41 @@
|
||||
using System.Collections.Generic;
|
||||
using ProjectM.Simulation;
|
||||
using Unity.Entities;
|
||||
using Unity.NetCode;
|
||||
using UnityEngine;
|
||||
|
||||
namespace ProjectM.Client
|
||||
{
|
||||
/// <summary>
|
||||
/// Client-side meta-purchase sender: a static enqueue (the Step-14 base meta-shop panel / execute_code) drained
|
||||
/// into <see cref="MetaSpendRequest"/> 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).
|
||||
/// </summary>
|
||||
[WorldSystemFilter(WorldSystemFilterFlags.ClientSimulation)]
|
||||
public partial class MetaSpendSendSystem : SystemBase
|
||||
{
|
||||
static readonly Queue<byte> s_Queue = new();
|
||||
|
||||
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)]
|
||||
static void ResetStatics() => s_Queue.Clear();
|
||||
|
||||
/// <summary>Queue a permanent-upgrade purchase by catalog id. The shop row click + execute_code drive this.</summary>
|
||||
public static void RequestPurchase(byte upgradeId) => s_Queue.Enqueue(upgradeId);
|
||||
|
||||
protected override void OnCreate()
|
||||
{
|
||||
RequireForUpdate<NetworkId>();
|
||||
}
|
||||
|
||||
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() });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 9f3197b4ede6dea41a6c4c93ffa51b42
|
||||
@@ -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 "";
|
||||
|
||||
@@ -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<RefRO<LocalTransform>>().WithAll<ExpeditionGate>())
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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<VisualElement> _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
|
||||
bool haveRun = SystemAPI.TryGetSingleton<RunInfo>(out var runInfo);
|
||||
SystemAPI.TryGetSingleton<ExpeditionObjective>(out var obj);
|
||||
if (haveRun && !siege && !finalSiege)
|
||||
{
|
||||
switch (runInfo.Lifecycle)
|
||||
{
|
||||
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<RefRO<PlayerReady>>().WithAll<PlayerTag>())
|
||||
{
|
||||
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<NetworkTime>(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"
|
||||
: 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)
|
||||
_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);
|
||||
// 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<ExpeditionObjective>(out var obj))
|
||||
{
|
||||
if (onExpedition)
|
||||
{
|
||||
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); }
|
||||
{
|
||||
_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<RefRO<BoonOffer>>().WithAll<PlayerTag, GhostOwnerIsLocal>())
|
||||
{
|
||||
localOffer = off.ValueRO;
|
||||
hasOffer = true;
|
||||
break;
|
||||
}
|
||||
BlobAssetReference<BoonCatalogBlob> boonPool = default;
|
||||
if (SystemAPI.TryGetSingleton<BoonCatalog>(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<GoalProgress>(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<RefRO<AbilityRef>>().WithAll<PlayerTag, GhostOwnerIsLocal>())
|
||||
{
|
||||
localClass = ClassTraits.ClassForAbility(ar.ValueRO.Id);
|
||||
haveLocalPlayer = true;
|
||||
break;
|
||||
}
|
||||
bool metaShow = false;
|
||||
BlobAssetReference<MetaUpgradeCatalogBlob> metaPool = default;
|
||||
DynamicBuffer<MetaTierState> metaRecord = default;
|
||||
if (haveRun && runInfo.Lifecycle == RunLifecycle.Staging && haveLocalPlayer && !siege
|
||||
&& SystemAPI.TryGetSingleton<MetaUpgradeCatalog>(out var metaCat) && metaCat.Value.IsCreated
|
||||
&& SystemAPI.TryGetSingletonBuffer<MetaTierState>(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<BoonCatalogBlob> 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<MetaUpgradeCatalogBlob> pool, DynamicBuffer<MetaTierState> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<ProjectM.Simulation.RunInfo>(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));
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Placement-ghost preview mesh for a <see cref="StructureType"/> byte (null → the cube fallback).</summary>
|
||||
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;
|
||||
|
||||
@@ -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<PendingMetaRow>(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<PendingSaveLedgerRow>(e);
|
||||
if (data.Ledger != null)
|
||||
foreach (var row in data.Ledger)
|
||||
@@ -167,6 +176,9 @@ namespace ProjectM.Client
|
||||
var st = tq.GetSingleton<NetworkTime>().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,
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 23831c9502abad541a3b2a3dc65502c2
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,59 @@
|
||||
using ProjectM.Simulation;
|
||||
using Unity.Entities;
|
||||
using Unity.NetCode;
|
||||
using UnityEngine;
|
||||
|
||||
namespace ProjectM.Client
|
||||
{
|
||||
/// <summary>
|
||||
/// Client-side ready-toggle sender: a static enqueue (HUD button at Step 14 / the T dev key / execute_code)
|
||||
/// drained into <see cref="ReadyToggleRequest"/> RPC entities — the BuildSendSystem queue+drain idiom. The local
|
||||
/// bool tracks only the toggle DIRECTION; the server-replicated <see cref="PlayerReady"/> is the truth the HUD
|
||||
/// renders. Statics reset on play-enter (statics survive fast-enter-playmode reloads — the stale-bridge hazard).
|
||||
/// </summary>
|
||||
[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;
|
||||
}
|
||||
|
||||
/// <summary>Queue an explicit ready set (HUD button / execute_code).</summary>
|
||||
public static void SetReady(bool ready)
|
||||
{
|
||||
s_PendingValue = (byte)(ready ? 1 : 0);
|
||||
s_Pending++;
|
||||
s_LocalReady = ready;
|
||||
}
|
||||
|
||||
/// <summary>Queue a toggle of the last requested state (the T dev key; HUD replaces this at Step 14).</summary>
|
||||
public static void ToggleReady() => SetReady(!s_LocalReady);
|
||||
|
||||
protected override void OnCreate()
|
||||
{
|
||||
RequireForUpdate<NetworkId>();
|
||||
}
|
||||
|
||||
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 });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7047cb8f6861ba8498f33698c9948dc8
|
||||
@@ -0,0 +1,60 @@
|
||||
using ProjectM.Simulation;
|
||||
using Unity.Entities;
|
||||
using Unity.NetCode;
|
||||
using UnityEngine;
|
||||
|
||||
namespace ProjectM.Client
|
||||
{
|
||||
/// <summary>
|
||||
/// Client-side route-pick sender: a static enqueue (the Step-14 map panel's option buttons / execute_code)
|
||||
/// drained into <see cref="RouteSelectRequest"/> RPCs. The request is stamped from the CLIENT's replicated
|
||||
/// <see cref="RunInfo"/>: <c>ForRunEpoch = (int)RunSeed</c> (the re-meaned run-identity token — the server-only
|
||||
/// RunEpoch is not client-knowable) and <c>ForLayer = CurrentRoom</c> 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).
|
||||
/// </summary>
|
||||
[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;
|
||||
}
|
||||
|
||||
/// <summary>Queue a route pick (0..RouteOptionCount-1). HUD map panel + execute_code drive this.</summary>
|
||||
public static void PickRoute(byte optionIndex)
|
||||
{
|
||||
s_PendingIndex = optionIndex;
|
||||
s_Pending++;
|
||||
}
|
||||
|
||||
protected override void OnCreate()
|
||||
{
|
||||
RequireForUpdate<NetworkId>();
|
||||
RequireForUpdate<RunInfo>();
|
||||
}
|
||||
|
||||
protected override void OnUpdate()
|
||||
{
|
||||
if (s_Pending == 0)
|
||||
return;
|
||||
var runInfo = SystemAPI.GetSingleton<RunInfo>();
|
||||
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
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c22921cccedc3564b86d12491d88488e
|
||||
@@ -1,92 +0,0 @@
|
||||
using ProjectM.Simulation;
|
||||
using Unity.Burst;
|
||||
using Unity.Collections;
|
||||
using Unity.Entities;
|
||||
using Unity.NetCode;
|
||||
|
||||
namespace ProjectM.Server
|
||||
{
|
||||
/// <summary>
|
||||
/// Server-authoritative ability-damage upgrade (handles <see cref="AbilityUpgradeRequest"/> 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 <see cref="StatModifier"/> 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).
|
||||
/// </summary>
|
||||
[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<ResourceLedger>();
|
||||
var builder = new EntityQueryBuilder(Allocator.Temp)
|
||||
.WithAll<AbilityUpgradeRequest, ReceiveRpcCommandRequest>();
|
||||
state.RequireForUpdate(state.GetEntityQuery(builder));
|
||||
}
|
||||
|
||||
[BurstCompile]
|
||||
public void OnUpdate(ref SystemState state)
|
||||
{
|
||||
var ledger = SystemAPI.GetBuffer<StorageEntry>(SystemAPI.GetSingletonEntity<ResourceLedger>());
|
||||
|
||||
var playerByConn = new NativeHashMap<int, Entity>(8, Allocator.Temp);
|
||||
foreach (var (owner, entity) in
|
||||
SystemAPI.Query<RefRO<GhostOwner>>().WithAll<PlayerTag, StatModifier>().WithEntityAccess())
|
||||
playerByConn[owner.ValueRO.NetworkId] = entity;
|
||||
|
||||
var ecb = new EntityCommandBuffer(Allocator.Temp);
|
||||
|
||||
foreach (var (receive, requestEntity) in
|
||||
SystemAPI.Query<RefRO<ReceiveRpcCommandRequest>>().WithAll<AbilityUpgradeRequest>().WithEntityAccess())
|
||||
{
|
||||
var conn = receive.ValueRO.SourceConnection;
|
||||
if (SystemAPI.HasComponent<NetworkId>(conn)
|
||||
&& playerByConn.TryGetValue(SystemAPI.GetComponent<NetworkId>(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<StatModifier>(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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ff2ed6b5fa37a174aa7413f4d2f5d6b3
|
||||
@@ -0,0 +1,133 @@
|
||||
using ProjectM.Simulation;
|
||||
using Unity.Burst;
|
||||
using Unity.Collections;
|
||||
using Unity.Entities;
|
||||
using Unity.NetCode;
|
||||
|
||||
namespace ProjectM.Server
|
||||
{
|
||||
/// <summary>
|
||||
/// Server receiver for <see cref="BoonPickRequest"/> + the reward-grace AUTO-PICK backstop. A valid pick
|
||||
/// (sender resolved, <c>RunInfo.Lifecycle == RoomReward</c> — the D-F4 gate — <c>Pending == 1</c>, index in
|
||||
/// range, option id known to the catalog) appends ONE <see cref="StatModifier"/> in the run-scoped BOON band
|
||||
/// (<c>Tuning.BoonSourceIdBase + BoonPickCounter++</c> — distinct rows, one range-strip clears the run) and
|
||||
/// clears <c>Pending</c>; 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 <c>Option0</c> (the operator's default un-picked policy — a player always
|
||||
/// gets something) so the run never stalls on an AFK picker.
|
||||
///
|
||||
/// Ordering: <c>[UpdateBefore(RunDirectorSystem)]</c> — 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).
|
||||
/// </summary>
|
||||
[BurstCompile]
|
||||
[WorldSystemFilter(WorldSystemFilterFlags.ServerSimulation)]
|
||||
[UpdateInGroup(typeof(SimulationSystemGroup))]
|
||||
[UpdateBefore(typeof(RunDirectorSystem))]
|
||||
public partial struct BoonApplySystem : ISystem
|
||||
{
|
||||
[BurstCompile]
|
||||
public void OnCreate(ref SystemState state)
|
||||
{
|
||||
state.RequireForUpdate<BoonCatalog>();
|
||||
state.RequireForUpdate<RunInfo>();
|
||||
state.RequireForUpdate<RunRuntime>();
|
||||
state.RequireForUpdate<NetworkTime>();
|
||||
}
|
||||
|
||||
[BurstCompile]
|
||||
public void OnUpdate(ref SystemState state)
|
||||
{
|
||||
var dirEntity = SystemAPI.GetSingletonEntity<RunInfo>();
|
||||
var info = SystemAPI.GetComponent<RunInfo>(dirEntity);
|
||||
var run = SystemAPI.GetComponent<RunRuntime>(dirEntity);
|
||||
bool rewarding = info.Lifecycle == RunLifecycle.RoomReward;
|
||||
|
||||
var catalog = SystemAPI.GetComponent<BoonCatalog>(SystemAPI.GetSingletonEntity<BoonCatalog>());
|
||||
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<int, Entity>(8, Allocator.Temp);
|
||||
foreach (var (owner, entity) in
|
||||
SystemAPI.Query<RefRO<GhostOwner>>().WithAll<PlayerTag, BoonOffer, StatModifier>().WithEntityAccess())
|
||||
playerByConn[owner.ValueRO.NetworkId] = entity;
|
||||
|
||||
var ecb = new EntityCommandBuffer(Allocator.Temp);
|
||||
foreach (var (receive, req, requestEntity) in
|
||||
SystemAPI.Query<RefRO<ReceiveRpcCommandRequest>, RefRO<BoonPickRequest>>().WithEntityAccess())
|
||||
{
|
||||
var conn = receive.ValueRO.SourceConnection;
|
||||
if (rewarding
|
||||
&& req.ValueRO.Index < 3
|
||||
&& SystemAPI.HasComponent<NetworkId>(conn)
|
||||
&& playerByConn.TryGetValue(SystemAPI.GetComponent<NetworkId>(conn).Value, out var player))
|
||||
{
|
||||
var offer = SystemAPI.GetComponent<BoonOffer>(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<NetworkTime>().ServerTick;
|
||||
if (serverTick.IsValid && !new NetworkTick(run.RewardGraceTick).IsNewerThan(serverTick))
|
||||
{
|
||||
foreach (var (offer, region, entity) in
|
||||
SystemAPI.Query<RefRW<BoonOffer>, RefRO<RegionTag>>()
|
||||
.WithAll<PlayerTag, StatModifier>().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)
|
||||
}
|
||||
|
||||
/// <summary>Append the boon's StatModifier in the run-scoped band. False iff the id is unknown/zero.</summary>
|
||||
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<StatModifier>(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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5749745bedc86ca4396b9a3911ef8773
|
||||
@@ -0,0 +1,78 @@
|
||||
using ProjectM.Simulation;
|
||||
using Unity.Burst;
|
||||
using Unity.Entities;
|
||||
using Unity.NetCode;
|
||||
|
||||
namespace ProjectM.Server
|
||||
{
|
||||
/// <summary>
|
||||
/// Server-only choice-of-3 boon dealer: once per <see cref="RunRuntime.RoomEpoch"/> (int-equality latch on
|
||||
/// <see cref="BoonOfferState"/>, 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
|
||||
/// <see cref="BoonMath.PickBoons"/> — deterministically seeded from Hash(RunSeed, room, NetworkId) — and writes
|
||||
/// the player's owner-only replicated <see cref="BoonOffer"/> (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: <c>[UpdateAfter(RunDirectorSystem)]</c> — 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.
|
||||
/// </summary>
|
||||
[BurstCompile]
|
||||
[WorldSystemFilter(WorldSystemFilterFlags.ServerSimulation)]
|
||||
[UpdateInGroup(typeof(SimulationSystemGroup))]
|
||||
[UpdateAfter(typeof(RunDirectorSystem))]
|
||||
public partial struct BoonOfferSystem : ISystem
|
||||
{
|
||||
[BurstCompile]
|
||||
public void OnCreate(ref SystemState state)
|
||||
{
|
||||
state.RequireForUpdate<BoonCatalog>();
|
||||
state.RequireForUpdate<RunInfo>();
|
||||
state.RequireForUpdate<RunRuntime>();
|
||||
}
|
||||
|
||||
[BurstCompile]
|
||||
public void OnUpdate(ref SystemState state)
|
||||
{
|
||||
var catalogEntity = SystemAPI.GetSingletonEntity<BoonCatalog>();
|
||||
|
||||
// One-shot: attach this system's latch beside the catalog singleton (the RoomFieldState idiom).
|
||||
if (!SystemAPI.HasComponent<BoonOfferState>(catalogEntity))
|
||||
{
|
||||
state.EntityManager.AddComponentData(catalogEntity, new BoonOfferState());
|
||||
return; // structural change — clean re-read next tick
|
||||
}
|
||||
|
||||
var dirEntity = SystemAPI.GetSingletonEntity<RunInfo>();
|
||||
var info = SystemAPI.GetComponent<RunInfo>(dirEntity);
|
||||
if (info.Lifecycle != RunLifecycle.RoomReward)
|
||||
return;
|
||||
|
||||
var run = SystemAPI.GetComponent<RunRuntime>(dirEntity);
|
||||
var offered = SystemAPI.GetComponent<BoonOfferState>(catalogEntity);
|
||||
if (offered.OfferedRoomEpoch == run.RoomEpoch)
|
||||
return; // this room's offers are already dealt
|
||||
|
||||
var catalog = SystemAPI.GetComponent<BoonCatalog>(catalogEntity);
|
||||
if (!catalog.Value.IsCreated)
|
||||
return;
|
||||
ref var pool = ref catalog.Value.Value;
|
||||
|
||||
foreach (var (offer, owner, region, cls) in
|
||||
SystemAPI.Query<RefRW<BoonOffer>, RefRO<GhostOwner>, RefRO<RegionTag>, RefRO<PlayerClass>>()
|
||||
.WithAll<PlayerTag>())
|
||||
{
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 2d3715c60d2cc2348ac4ff7600006d23
|
||||
@@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// Server-only per-ROOM enemy director — the Step-6 successor of the presence-keyed <c>ZoneEnemyDirectorSystem</c>.
|
||||
/// While the run FSM has a room active (<see cref="RunInfo.Lifecycle"/> == InRoom) it seeds ONE wave per
|
||||
/// <see cref="RunRuntime.RoomEpoch"/> (int-equality reseed) sized by <see cref="ZoneEnemyMath.WaveSlots"/> indexed
|
||||
/// on the room's <see cref="RoomPlan.DifficultyEpoch"/> (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 <see cref="RegionMath.ExpeditionRoomOrigin"/>(base, ActiveSubSlot), under the same
|
||||
/// <see cref="ZoneEnemyDirector.MaxAlive"/> "spawn-the-pack-only-if-it-fits-else-wait" relevancy guard. A
|
||||
/// <see cref="RoomTypeId.Boss"/> room spawns ONE beefed boss instead (health × <see cref="Tuning.BossHealthMultiplier"/>,
|
||||
/// scale × <see cref="Tuning.BossScaleMultiplier"/> — v1's boss is a scaled Charger). Every spawn keeps the full
|
||||
/// stack — EnemyTag + RegionTag{Expedition} + <see cref="ZoneEnemyTag"/> — PLUS <see cref="RoomTag"/>{room} (the
|
||||
/// teardown contract). Scale preserved via <c>baked.WithPosition</c>.
|
||||
///
|
||||
/// The room CLEAR edge surfaces ONLY through the replicated <see cref="ExpeditionObjective"/>.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: <c>[UpdateAfter(RunDirectorSystem)]</c> 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).
|
||||
/// </summary>
|
||||
[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<NetworkTime>();
|
||||
state.RequireForUpdate<RunInfo>();
|
||||
state.RequireForUpdate<RunRuntime>();
|
||||
state.RequireForUpdate<ZoneEnemyDirector>();
|
||||
m_ZoneEnemies = state.GetEntityQuery(ComponentType.ReadOnly<ZoneEnemyTag>());
|
||||
}
|
||||
|
||||
[BurstCompile]
|
||||
public void OnUpdate(ref SystemState state)
|
||||
{
|
||||
var serverTick = SystemAPI.GetSingleton<NetworkTime>().ServerTick;
|
||||
if (!serverTick.IsValid)
|
||||
return;
|
||||
uint now = serverTick.TickIndexForValidTick;
|
||||
|
||||
var runEntity = SystemAPI.GetSingletonEntity<RunInfo>();
|
||||
var info = SystemAPI.GetComponent<RunInfo>(runEntity);
|
||||
var run = SystemAPI.GetComponent<RunRuntime>(runEntity);
|
||||
bool roomActive = info.Lifecycle == RunLifecycle.InRoom;
|
||||
|
||||
var directorEntity = SystemAPI.GetSingletonEntity<ZoneEnemyDirector>();
|
||||
var dir = SystemAPI.GetComponent<ZoneEnemyDirector>(directorEntity);
|
||||
var zs = SystemAPI.GetComponent<ZoneEnemyState>(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<ExpeditionObjective>(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<ZoneEnemyPrefab>(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<BaseAnchor>(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<LocalTransform>(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<ZoneEnemyTag>(enemy);
|
||||
ecb.AddComponent(enemy, new RoomTag { Room = room });
|
||||
if (bossRoom && SystemAPI.HasComponent<Health>(prefab))
|
||||
{
|
||||
var hp = SystemAPI.GetComponent<Health>(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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 3204b510b450f384a93bd49902c65721
|
||||
@@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// Server-only expedition zone-enemy director: while a player is OUT in the expedition region and the base is in
|
||||
/// <see cref="CyclePhase.Calm"/>, it seeds and drip-spawns one epoch-seeded combat wave around the expedition
|
||||
/// origin. The wave size + grunt/charger composition is pure <see cref="ZoneEnemyMath"/> of the
|
||||
/// <see cref="CycleRuntime.ExpeditionEpoch"/> (grunt-heavy -> charger-heavy as the epoch climbs), spawned one
|
||||
/// every <see cref="ZoneEnemyDirector.SpawnIntervalTicks"/> at a deterministic ring, capped at
|
||||
/// <see cref="ZoneEnemyDirector.MaxAlive"/> concurrent (the v1 ghost-relevancy budget). Each enemy is the
|
||||
/// existing Husk ghost prefab + <c>RegionTag{Expedition}</c> + <see cref="ZoneEnemyTag"/>, 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 <see cref="CycleRuntime.ClearedThisEpoch"/> once —
|
||||
/// the gate's once-per-epoch Ore reward reads that on the player's return.
|
||||
///
|
||||
/// DR-042 C7b: it ALSO writes the replicated <see cref="ExpeditionObjective"/> 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: <c>[UpdateAfter(ExpeditionFieldSystem)]</c> ONLY. ExpeditionFieldSystem is itself
|
||||
/// <c>[UpdateAfter(CyclePhaseSystem)]</c>, so ALSO declaring <c>[UpdateBefore(CyclePhaseSystem)]</c> 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.
|
||||
/// </summary>
|
||||
[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<NetworkTime>();
|
||||
state.RequireForUpdate<CycleState>();
|
||||
state.RequireForUpdate<ZoneEnemyDirector>();
|
||||
m_ZoneEnemies = state.GetEntityQuery(ComponentType.ReadOnly<ZoneEnemyTag>());
|
||||
}
|
||||
|
||||
[BurstCompile]
|
||||
public void OnUpdate(ref SystemState state)
|
||||
{
|
||||
var serverTick = SystemAPI.GetSingleton<NetworkTime>().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<RefRO<RegionTag>>().WithAll<PlayerTag>())
|
||||
if (region.ValueRO.Region == RegionId.Expedition)
|
||||
expeditionPlayers++;
|
||||
|
||||
var directorEntity = SystemAPI.GetSingletonEntity<ZoneEnemyDirector>();
|
||||
var dir = SystemAPI.GetComponent<ZoneEnemyDirector>(directorEntity);
|
||||
var zs = SystemAPI.GetComponent<ZoneEnemyState>(directorEntity);
|
||||
|
||||
var cycleEntity = SystemAPI.GetSingletonEntity<CycleState>();
|
||||
var cycle = SystemAPI.GetComponent<CycleState>(cycleEntity);
|
||||
var runtime = SystemAPI.GetComponent<CycleRuntime>(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<ExpeditionObjective>(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<ZoneEnemyPrefab>(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<BaseAnchor>(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<LocalTransform>(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<ZoneEnemyTag>(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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 4dc121e99d0e53640b5e6815640a0bc6
|
||||
@@ -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<MetaUpgradeCatalog>(out var metaCatalog) || !metaCatalog.Value.IsCreated
|
||||
|| !SystemAPI.TryGetSingletonBuffer<MetaTierState>(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<PlayerSpawner>();
|
||||
|
||||
// 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<<ClassId would compute bits 2/3 and silently skip everything).
|
||||
{
|
||||
ref var metaPool = ref metaCatalog.Value.Value;
|
||||
byte classBit = BoonMath.MaskFor(classId);
|
||||
for (int m = 0; m < metaRecord.Length; m++)
|
||||
{
|
||||
if (metaRecord[m].ClassId != classId || metaRecord[m].Tier == 0) continue;
|
||||
int defIdx = MetaMath.FindDef(ref metaPool, metaRecord[m].UpgradeId);
|
||||
if (defIdx < 0) continue; // unknown id (catalog drift) — preserved on disk, skipped live
|
||||
if ((metaPool.Defs[defIdx].ClassMask & classBit) == 0) continue;
|
||||
byte tier = metaRecord[m].Tier < metaPool.Defs[defIdx].MaxTier
|
||||
? metaRecord[m].Tier : metaPool.Defs[defIdx].MaxTier;
|
||||
ecb.AppendToBuffer(player, new StatModifier
|
||||
{
|
||||
Target = metaPool.Defs[defIdx].Target,
|
||||
Op = metaPool.Defs[defIdx].Op,
|
||||
Value = metaPool.Defs[defIdx].ValuePerTier * tier,
|
||||
SourceId = Tuning.MetaSourceIdBase + metaRecord[m].UpgradeId,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Auto-despawn the player when its owning connection is removed.
|
||||
ecb.AppendToBuffer(connection, new LinkedEntityGroup { Value = player });
|
||||
|
||||
@@ -187,6 +187,39 @@ namespace ProjectM.Server
|
||||
ClassTraits.Reapply(newClass, classMods);
|
||||
SystemAPI.SetComponent(sender, new AbilityRef { Id = ClassTraits.AbilityFor(newClass) });
|
||||
|
||||
// Expedition redesign (dev fork, operator-approved): keep the PERMANENT meta channel in
|
||||
// sync with the swap. Reapply only strips the CLASS-seed band, so the OLD class's meta
|
||||
// rows would survive — strip the meta band, replay the NEW class's persisted tiers (the
|
||||
// GoInGame skip/clamp rules), and repoint the server-only PlayerClass anchor so a later
|
||||
// MetaSpendRequest buys against the right class record. Runs BEFORE the heal below so the
|
||||
// refill folds the new class's meta MaxHealth too.
|
||||
TimedModifierUtil.RemoveBySourceIdRange(classMods, Tuning.MetaSourceIdBase,
|
||||
Tuning.MetaSourceIdBase + Tuning.MetaSourceIdSpan);
|
||||
if (SystemAPI.TryGetSingleton<MetaUpgradeCatalog>(out var metaCat) && metaCat.Value.IsCreated
|
||||
&& SystemAPI.TryGetSingletonBuffer<MetaTierState>(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<PlayerClass>(sender))
|
||||
SystemAPI.SetComponent(sender, new PlayerClass { ClassId = newClass });
|
||||
|
||||
// Let the swapped Fire ability fire immediately (both abilities share one cooldown gate).
|
||||
if (SystemAPI.HasComponent<AbilityCooldown>(sender))
|
||||
SystemAPI.SetComponent(sender, new AbilityCooldown { NextFireTick = 0 }); // 0 = ready
|
||||
|
||||
@@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// Server-only home-base mining-field manager. Keeps the live RegionTag{Base} ResourceNode count topped up to
|
||||
/// <see cref="BaseFieldSpawner.TargetCount"/> so the gather -> build -> survive loop lives AT the base (no
|
||||
/// expedition trip). Unlike <see cref="ExpeditionFieldSystem"/> (edge-triggered on player presence) this is a
|
||||
/// TICK-CADENCED top-up: every <see cref="BaseFieldSpawner.RespawnIntervalTicks"/> 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.
|
||||
/// </summary>
|
||||
[BurstCompile]
|
||||
[WorldSystemFilter(WorldSystemFilterFlags.ServerSimulation)]
|
||||
[UpdateInGroup(typeof(SimulationSystemGroup))]
|
||||
public partial struct BaseFieldSpawnSystem : ISystem
|
||||
{
|
||||
[BurstCompile]
|
||||
public void OnCreate(ref SystemState state)
|
||||
{
|
||||
state.RequireForUpdate<NetworkTime>();
|
||||
state.RequireForUpdate<BaseFieldSpawner>();
|
||||
state.RequireForUpdate<BaseAnchor>();
|
||||
}
|
||||
|
||||
[BurstCompile]
|
||||
public void OnUpdate(ref SystemState state)
|
||||
{
|
||||
var serverTick = SystemAPI.GetSingleton<NetworkTime>().ServerTick;
|
||||
if (!serverTick.IsValid)
|
||||
return;
|
||||
uint now = serverTick.TickIndexForValidTick;
|
||||
|
||||
var spawnerEntity = SystemAPI.GetSingletonEntity<BaseFieldSpawner>();
|
||||
var spawner = SystemAPI.GetComponent<BaseFieldSpawner>(spawnerEntity);
|
||||
var runtime = SystemAPI.GetComponent<BaseFieldRuntime>(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<RefRO<RegionTag>>().WithAll<ResourceNode>())
|
||||
if (region.ValueRO.Region == RegionId.Base)
|
||||
liveBase++;
|
||||
|
||||
int deficit = spawner.TargetCount - liveBase;
|
||||
if (deficit > 0)
|
||||
{
|
||||
var anchor = SystemAPI.GetSingleton<BaseAnchor>();
|
||||
float3 center = BaseGridMath.PlotCenter(anchor);
|
||||
var baseXform = SystemAPI.GetComponent<LocalTransform>(spawner.Prefab);
|
||||
var prefabNode = SystemAPI.GetComponent<ResourceNode>(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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 346e9c0fb92e7b94fa3761222fc2ff1e
|
||||
@@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// Server-only procedural expedition-field manager. Re-keyed off PER-PLAYER PRESENCE (no global phase): it
|
||||
/// counts players whose server-only <see cref="RegionTag"/> is the Expedition region, and on the
|
||||
/// empty->occupied edge (a new sortie) bumps <see cref="CycleRuntime.ExpeditionEpoch"/> and scatters
|
||||
/// <see cref="ResourceFieldSpawner.Count"/> resource-node ghosts (seeded by the epoch) around the expedition
|
||||
/// origin — PLUS, if a <see cref="ClutterFieldSpawner"/> singleton is present,
|
||||
/// <see cref="ClutterFieldSpawner.Count"/> 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).
|
||||
/// </summary>
|
||||
[BurstCompile]
|
||||
[WorldSystemFilter(WorldSystemFilterFlags.ServerSimulation)]
|
||||
[UpdateInGroup(typeof(SimulationSystemGroup))]
|
||||
[UpdateAfter(typeof(CyclePhaseSystem))]
|
||||
public partial struct ExpeditionFieldSystem : ISystem
|
||||
{
|
||||
[BurstCompile]
|
||||
public void OnCreate(ref SystemState state)
|
||||
{
|
||||
state.RequireForUpdate<ResourceFieldSpawner>();
|
||||
state.RequireForUpdate<CycleState>();
|
||||
}
|
||||
|
||||
[BurstCompile]
|
||||
public void OnUpdate(ref SystemState state)
|
||||
{
|
||||
var cycleEntity = SystemAPI.GetSingletonEntity<CycleState>();
|
||||
var runtime = SystemAPI.GetComponent<CycleRuntime>(cycleEntity);
|
||||
var spawner = SystemAPI.GetSingleton<ResourceFieldSpawner>();
|
||||
|
||||
// Per-player presence: is anyone currently out in the expedition region?
|
||||
int expeditionPlayers = 0;
|
||||
foreach (var region in SystemAPI.Query<RefRO<RegionTag>>().WithAll<PlayerTag>())
|
||||
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<BaseAnchor>(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<LocalTransform>(spawner.Prefab);
|
||||
var prefabNode = SystemAPI.GetComponent<ResourceNode>(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<ClutterFieldSpawner>(out var clutterSpawner)
|
||||
&& clutterSpawner.Prefab != Entity.Null)
|
||||
{
|
||||
var clutterXform = SystemAPI.GetComponent<LocalTransform>(clutterSpawner.Prefab);
|
||||
var prefabClutter = SystemAPI.GetComponent<BlightClutter>(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<ResourceNode>, RefRO<RegionTag>>().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<RefRO<RegionTag>>().WithAll<BlightClutter>().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<RefRO<RegionTag>>().WithAll<ZoneEnemyTag>().WithEntityAccess())
|
||||
if (region.ValueRO.Region == RegionId.Expedition)
|
||||
ecb.DestroyEntity(e);
|
||||
}
|
||||
|
||||
runtime.PrevExpeditionOccupied = (byte)(occupied ? 1 : 0);
|
||||
SystemAPI.SetComponent(cycleEntity, runtime);
|
||||
|
||||
ecb.Playback(state.EntityManager);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 9267d7809e68ea54caa55378f33e67f6
|
||||
@@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// Server-only per-ROOM field seeder — the Step-5 successor of the presence-keyed <c>ExpeditionFieldSystem</c>.
|
||||
/// When the run FSM has a room active (<see cref="RunInfo.Lifecycle"/> == InRoom) and
|
||||
/// <see cref="RunRuntime.RoomEpoch"/> has advanced past the epoch this system last seeded (int equality, never
|
||||
/// tick math), it resolves the active room's <see cref="RoomPlan"/> from the map node RunDirectorSystem published
|
||||
/// (<see cref="RunRuntime.CurrentNodeId"/> — the single plan authority; NEVER re-derived here) and scatters
|
||||
/// <c>plan.NodeCount</c> resource nodes — FLOORED by the run-wide scarcity budget
|
||||
/// <see cref="RunRuntime.NodeBudgetRemaining"/>, 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 <see cref="RegionMath.ExpeditionRoomOrigin"/>(base, ActiveSubSlot). Every spawn is
|
||||
/// stamped <see cref="RoomTag"/>{room} (the teardown contract) on top of the prefab-baked RegionTag{Expedition};
|
||||
/// Scale is preserved via <c>baked.WithPosition</c> (never FromPosition).
|
||||
///
|
||||
/// Teardown: room-advance/return teardown belongs to RunDirectorSystem (RoomTeardown, Step 7). This system keeps
|
||||
/// ONE defensive sweep — Staging with any <see cref="RoomTag"/> alive → destroy them all (idempotent; covers
|
||||
/// abort/disconnect edges). Untagged ghosts (base field, structures) are structurally untouchable.
|
||||
///
|
||||
/// Ordering: <c>[UpdateAfter(RunDirectorSystem)]</c> so it reads the freshly-advanced room state same-tick.
|
||||
/// The old inherited <c>[UpdateAfter(CyclePhaseSystem)]</c> is deliberately DROPPED and NO CyclePhase edge may
|
||||
/// ever return to the room chain (the Play-only sort-cycle rule — invisible to EditMode).
|
||||
/// </summary>
|
||||
[BurstCompile]
|
||||
[WorldSystemFilter(WorldSystemFilterFlags.ServerSimulation)]
|
||||
[UpdateInGroup(typeof(SimulationSystemGroup))]
|
||||
[UpdateAfter(typeof(RunDirectorSystem))]
|
||||
public partial struct RoomFieldSystem : ISystem
|
||||
{
|
||||
/// <summary>Max clutter pieces per room — cosmetic ghosts still cost relevancy, keep the dressing light.</summary>
|
||||
const int MaxClutterPerRoom = 6;
|
||||
|
||||
EntityQuery m_RoomTagged;
|
||||
|
||||
[BurstCompile]
|
||||
public void OnCreate(ref SystemState state)
|
||||
{
|
||||
state.RequireForUpdate<ResourceFieldSpawner>();
|
||||
state.RequireForUpdate<RunInfo>();
|
||||
state.RequireForUpdate<RunRuntime>();
|
||||
m_RoomTagged = state.GetEntityQuery(ComponentType.ReadOnly<RoomTag>());
|
||||
}
|
||||
|
||||
[BurstCompile]
|
||||
public void OnUpdate(ref SystemState state)
|
||||
{
|
||||
var dirEntity = SystemAPI.GetSingletonEntity<RunInfo>();
|
||||
var info = SystemAPI.GetComponent<RunInfo>(dirEntity);
|
||||
var run = SystemAPI.GetComponent<RunRuntime>(dirEntity);
|
||||
|
||||
var spawnerEntity = SystemAPI.GetSingletonEntity<ResourceFieldSpawner>();
|
||||
var spawner = SystemAPI.GetComponent<ResourceFieldSpawner>(spawnerEntity);
|
||||
|
||||
// One-shot: attach this system's server-only bookkeeping beside the baked spawner singleton.
|
||||
if (!SystemAPI.HasComponent<RoomFieldState>(spawnerEntity))
|
||||
{
|
||||
state.EntityManager.AddComponentData(spawnerEntity, new RoomFieldState());
|
||||
return; // structural change — clean re-read next tick
|
||||
}
|
||||
var rf = SystemAPI.GetComponent<RoomFieldState>(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<BaseAnchor>(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<LocalTransform>(spawner.Prefab);
|
||||
var prefabNode = SystemAPI.GetComponent<ResourceNode>(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<ClutterFieldSpawner>(out var clutter)
|
||||
&& clutter.Prefab != Entity.Null)
|
||||
{
|
||||
var cBaked = SystemAPI.GetComponent<LocalTransform>(clutter.Prefab);
|
||||
var cProto = SystemAPI.GetComponent<BlightClutter>(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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b2ba012b5e31bcc48b50dd14220c9fc5
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 032a0f4a47c8f1d459bd341f50d42054
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,147 @@
|
||||
using ProjectM.Simulation;
|
||||
using Unity.Burst;
|
||||
using Unity.Collections;
|
||||
using Unity.Entities;
|
||||
using Unity.NetCode;
|
||||
|
||||
namespace ProjectM.Server
|
||||
{
|
||||
/// <summary>
|
||||
/// Server receiver for <see cref="MetaSpendRequest"/> — 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 → <see cref="PlayerClass"/>, validate catalog id /
|
||||
/// class mask (<see cref="BoonMath.MaskFor"/>, never raw 1<<ClassId) / MaxTier / prereq, price the NEXT tier
|
||||
/// (<see cref="MetaMath.CostForTier"/> — tier is server-computed, never on the wire), then
|
||||
/// <see cref="StorageMath.TotalOf"/> pre-check BEFORE <see cref="StorageMath.Withdraw"/> (Withdraw CLAMPS, it
|
||||
/// never rejects), bump-or-append the <see cref="MetaTierState"/> row, and upsert the ABSOLUTE-value meta
|
||||
/// StatModifier (R-F1: Value = ValuePerTier * newTier, keyed <c>Tuning.MetaSourceIdBase + id</c>) on every
|
||||
/// pre-collected live player of that class (R-F2 — offline classmates get theirs born-correct at next spawn via
|
||||
/// GoInGameServerSystem). Success raises <see cref="SaveRequest"/> so the tier is on disk before a crash.
|
||||
/// Plain server group, before RunDirectorSystem (the receiver convention); requests are ALWAYS destroyed.
|
||||
/// </summary>
|
||||
[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<MetaSpendRequest, ReceiveRpcCommandRequest>();
|
||||
state.RequireForUpdate(state.GetEntityQuery(builder));
|
||||
state.RequireForUpdate<RunInfo>();
|
||||
state.RequireForUpdate<MetaUpgradeCatalog>();
|
||||
state.RequireForUpdate<ResourceLedger>();
|
||||
}
|
||||
|
||||
[BurstCompile]
|
||||
public void OnUpdate(ref SystemState state)
|
||||
{
|
||||
// N4 phase gate — hoisted (per-tick-uniform, like the ReadyToggle accept flag).
|
||||
bool accept = SystemAPI.GetSingleton<RunInfo>().Lifecycle == RunLifecycle.Staging;
|
||||
|
||||
var catalog = SystemAPI.GetSingleton<MetaUpgradeCatalog>();
|
||||
var director = SystemAPI.GetSingletonEntity<ResourceLedger>();
|
||||
if (!catalog.Value.IsCreated || !SystemAPI.HasBuffer<MetaTierState>(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<int, Entity>(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<Entity>(8, Allocator.Temp);
|
||||
var classIds = new NativeList<byte>(8, Allocator.Temp);
|
||||
foreach (var (owner, playerClass, entity) in
|
||||
SystemAPI.Query<RefRO<GhostOwner>, RefRO<PlayerClass>>()
|
||||
.WithAll<PlayerTag, StatModifier>().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<ReceiveRpcCommandRequest>, RefRO<MetaSpendRequest>>().WithEntityAccess())
|
||||
{
|
||||
ecb.DestroyEntity(requestEntity); // ALWAYS consumed, accepted or not
|
||||
if (!accept) continue;
|
||||
|
||||
var conn = receive.ValueRO.SourceConnection;
|
||||
if (!SystemAPI.HasComponent<NetworkId>(conn)
|
||||
|| !playerByConn.TryGetValue(SystemAPI.GetComponent<NetworkId>(conn).Value, out var buyer))
|
||||
continue;
|
||||
byte classId = SystemAPI.GetComponent<PlayerClass>(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<MetaTierState>(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<StorageEntry>(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<StatModifier>(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<SaveRequest>(director))
|
||||
SystemAPI.SetComponent(director, new SaveRequest { Pending = 1 });
|
||||
}
|
||||
ecb.Playback(state.EntityManager);
|
||||
playerByConn.Dispose();
|
||||
classMembers.Dispose();
|
||||
classIds.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 9b277eb9da63a054db9f9b3e041d582b
|
||||
@@ -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<NetworkTime>().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,
|
||||
|
||||
@@ -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<PendingMetaRow>(pendingEntity);
|
||||
var metaDst = ecb.SetBuffer<MetaTierState>(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<RunInfo>(spawner.Prefab))
|
||||
{
|
||||
var runInfo = SystemAPI.GetComponent<RunInfo>(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);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// Server-only walk-in gate transit: a player who walks within a gate's radius (and whose region matches the
|
||||
/// gate's <see cref="ExpeditionGate.FromRegion"/>) is transited to the gate's ToRegion at its ArrivalPos
|
||||
/// (RegionTag flipped + LocalTransform teleported — GhostRelevancy re-scopes their ghosts, as in
|
||||
/// <c>RegionTransitSystem</c>). Returning to BASE signals the ThreatDirector (a completed expedition can draw a
|
||||
/// retaliation siege) by incrementing <see cref="ProjectM.Simulation.ThreatState.PendingReturns"/>. 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.
|
||||
/// </summary>
|
||||
[BurstCompile]
|
||||
[WorldSystemFilter(WorldSystemFilterFlags.ServerSimulation)]
|
||||
[UpdateInGroup(typeof(SimulationSystemGroup))]
|
||||
[UpdateBefore(typeof(CyclePhaseSystem))]
|
||||
public partial struct ExpeditionGateSystem : ISystem
|
||||
{
|
||||
[BurstCompile]
|
||||
public void OnCreate(ref SystemState state)
|
||||
{
|
||||
state.RequireForUpdate<ExpeditionGate>();
|
||||
}
|
||||
|
||||
[BurstCompile]
|
||||
public void OnUpdate(ref SystemState state)
|
||||
{
|
||||
// Snapshot gates once.
|
||||
var gateFrom = new NativeList<byte>(Allocator.Temp);
|
||||
var gateTo = new NativeList<byte>(Allocator.Temp);
|
||||
var gateRadiusSq = new NativeList<float>(Allocator.Temp);
|
||||
var gatePos = new NativeList<float2>(Allocator.Temp);
|
||||
var gateArrival = new NativeList<float3>(Allocator.Temp);
|
||||
foreach (var (gate, xform) in SystemAPI.Query<RefRO<ExpeditionGate>, RefRO<LocalTransform>>())
|
||||
{
|
||||
gateFrom.Add(gate.ValueRO.FromRegion);
|
||||
gateTo.Add(gate.ValueRO.ToRegion);
|
||||
gateRadiusSq.Add(gate.ValueRO.Radius * gate.ValueRO.Radius);
|
||||
gatePos.Add(xform.ValueRO.Position.xz);
|
||||
gateArrival.Add(gate.ValueRO.ArrivalPos);
|
||||
}
|
||||
|
||||
bool returnedToBase = false;
|
||||
foreach (var (region, xform) in
|
||||
SystemAPI.Query<RefRW<RegionTag>, RefRW<LocalTransform>>().WithAll<PlayerTag>())
|
||||
{
|
||||
byte r = region.ValueRO.Region;
|
||||
float2 pp = xform.ValueRO.Position.xz;
|
||||
for (int i = 0; i < gateFrom.Length; i++)
|
||||
{
|
||||
if (gateFrom[i] != r) continue;
|
||||
if (math.distancesq(pp, gatePos[i]) > gateRadiusSq[i]) continue;
|
||||
region.ValueRW.Region = gateTo[i];
|
||||
xform.ValueRW.Position = gateArrival[i];
|
||||
if (gateTo[i] == RegionId.Base)
|
||||
returnedToBase = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
gateFrom.Dispose();
|
||||
gateTo.Dispose();
|
||||
gateRadiusSq.Dispose();
|
||||
gatePos.Dispose();
|
||||
gateArrival.Dispose();
|
||||
|
||||
// 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<ThreatState>(out var threatEntity))
|
||||
{
|
||||
var threat = SystemAPI.GetComponent<ThreatState>(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<CycleState>())
|
||||
{
|
||||
var cycleEntity = SystemAPI.GetSingletonEntity<CycleState>();
|
||||
var runtime = SystemAPI.GetComponent<CycleRuntime>(cycleEntity);
|
||||
if (runtime.ClearedThisEpoch != 0 && runtime.LastRewardedEpoch != runtime.ExpeditionEpoch)
|
||||
{
|
||||
if (SystemAPI.TryGetSingleton<ZoneEnemyDirector>(out var zoneDir)
|
||||
&& SystemAPI.HasSingleton<ResourceLedger>())
|
||||
{
|
||||
var ledger = SystemAPI.GetBuffer<StorageEntry>(SystemAPI.GetSingletonEntity<ResourceLedger>());
|
||||
StorageMath.Deposit(ledger, (ushort)ResourceId.Ore, zoneDir.RewardOre);
|
||||
}
|
||||
if (SystemAPI.HasComponent<GoalProgress>(cycleEntity))
|
||||
{
|
||||
// +1 toward the goal per cleared expedition, CLAMPED to Target (single production writer).
|
||||
var goal = SystemAPI.GetComponent<GoalProgress>(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<SaveRequest>(cycleEntity))
|
||||
SystemAPI.SetComponent(cycleEntity, new SaveRequest { Pending = 1 });
|
||||
runtime.LastRewardedEpoch = runtime.ExpeditionEpoch;
|
||||
SystemAPI.SetComponent(cycleEntity, runtime);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 4292536f663eb5c4d92688f6c5bb0368
|
||||
@@ -0,0 +1,61 @@
|
||||
using ProjectM.Simulation;
|
||||
using Unity.Burst;
|
||||
using Unity.Collections;
|
||||
using Unity.Entities;
|
||||
using Unity.NetCode;
|
||||
|
||||
namespace ProjectM.Server
|
||||
{
|
||||
/// <summary>
|
||||
/// Server receiver for <see cref="ReadyToggleRequest"/>: resolves the sender (SourceConnection → NetworkId →
|
||||
/// GhostOwner → player entity, the AbilityUpgradeSystem idiom) and SETS <see cref="PlayerReady.Value"/>.
|
||||
/// 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.
|
||||
/// </summary>
|
||||
[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<ReadyToggleRequest, ReceiveRpcCommandRequest>();
|
||||
state.RequireForUpdate(state.GetEntityQuery(builder));
|
||||
state.RequireForUpdate<RunInfo>();
|
||||
}
|
||||
|
||||
[BurstCompile]
|
||||
public void OnUpdate(ref SystemState state)
|
||||
{
|
||||
byte lifecycle = SystemAPI.GetSingleton<RunInfo>().Lifecycle;
|
||||
bool accept = lifecycle == RunLifecycle.Staging || lifecycle == RunLifecycle.Launching;
|
||||
|
||||
var playerByConn = new NativeHashMap<int, Entity>(8, Allocator.Temp);
|
||||
foreach (var (owner, entity) in
|
||||
SystemAPI.Query<RefRO<GhostOwner>>().WithAll<PlayerTag, PlayerReady>().WithEntityAccess())
|
||||
playerByConn[owner.ValueRO.NetworkId] = entity;
|
||||
|
||||
var ecb = new EntityCommandBuffer(Allocator.Temp);
|
||||
foreach (var (receive, req, requestEntity) in
|
||||
SystemAPI.Query<RefRO<ReceiveRpcCommandRequest>, RefRO<ReadyToggleRequest>>().WithEntityAccess())
|
||||
{
|
||||
var conn = receive.ValueRO.SourceConnection;
|
||||
if (accept
|
||||
&& SystemAPI.HasComponent<NetworkId>(conn)
|
||||
&& playerByConn.TryGetValue(SystemAPI.GetComponent<NetworkId>(conn).Value, out var player))
|
||||
{
|
||||
SystemAPI.SetComponent(player, new PlayerReady { Value = (byte)(req.ValueRO.Ready != 0 ? 1 : 0) });
|
||||
}
|
||||
ecb.DestroyEntity(requestEntity);
|
||||
}
|
||||
ecb.Playback(state.EntityManager);
|
||||
playerByConn.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7392579cf8b92e64f9686b58da99f7c2
|
||||
@@ -0,0 +1,96 @@
|
||||
using ProjectM.Simulation;
|
||||
using Unity.Burst;
|
||||
using Unity.Collections;
|
||||
using Unity.Entities;
|
||||
using Unity.NetCode;
|
||||
|
||||
namespace ProjectM.Server
|
||||
{
|
||||
/// <summary>
|
||||
/// Server receiver for <see cref="RouteSelectRequest"/> — the co-op route choice (any-player-first-commits, the
|
||||
/// operator's locked authority model). Validates each pick server-authoritatively:
|
||||
/// <c>Lifecycle == RouteSelect</c> · run identity <c>(uint)ForRunEpoch == RunRuntime.RunSeed</c> (the Step-8
|
||||
/// review re-mean: the replicated seed IS the run token; the server-only RunEpoch is not client-knowable) ·
|
||||
/// <c>ForLayer == RunInfo.CurrentRoom</c> (the gate's un-incremented cleared layer) · <c>OptionIndex</c> within
|
||||
/// the replicated <c>RouteOptionCount</c> · the SENDER's <see cref="RegionTag"/> is Expedition (N3 — a
|
||||
/// base-bound joiner cannot commit the party's route) · nothing accepted yet this gate.
|
||||
///
|
||||
/// FIRST-COMMIT LATCH: the accepted pick is written to the server-only <see cref="RouteCommand"/> via an
|
||||
/// IMMEDIATE in-place <c>SystemAPI.SetComponent</c> INSIDE the drain loop plus a local accepted flag (the DR-014
|
||||
/// atomicity idiom) — two same-tick picks can never both observe an open gate; a hoisted read would re-create
|
||||
/// the exact N1 race the design review killed. <see cref="RouteCommand.ForRunEpoch"/> is stamped from the TRUE
|
||||
/// server-only epoch (never the client-echoed value). Requests are ALWAYS destroyed. This system writes ONLY
|
||||
/// RouteCommand — RunDirectorSystem stays the sole RunInfo/RunRuntime writer and consumes the latch
|
||||
/// (abort → pick → grace, in that order). Ordered before it so a pick can land the same tick it is consumed;
|
||||
/// NO CyclePhase edge (the room-chain hard rule).
|
||||
/// </summary>
|
||||
[BurstCompile]
|
||||
[WorldSystemFilter(WorldSystemFilterFlags.ServerSimulation)]
|
||||
[UpdateInGroup(typeof(SimulationSystemGroup))]
|
||||
[UpdateBefore(typeof(RunDirectorSystem))]
|
||||
public partial struct RouteSelectSystem : ISystem
|
||||
{
|
||||
[BurstCompile]
|
||||
public void OnCreate(ref SystemState state)
|
||||
{
|
||||
var builder = new EntityQueryBuilder(Allocator.Temp)
|
||||
.WithAll<RouteSelectRequest, ReceiveRpcCommandRequest>();
|
||||
state.RequireForUpdate(state.GetEntityQuery(builder));
|
||||
state.RequireForUpdate<RunInfo>();
|
||||
state.RequireForUpdate<RunRuntime>();
|
||||
state.RequireForUpdate<RouteCommand>();
|
||||
}
|
||||
|
||||
[BurstCompile]
|
||||
public void OnUpdate(ref SystemState state)
|
||||
{
|
||||
var dirEntity = SystemAPI.GetSingletonEntity<RunInfo>();
|
||||
var info = SystemAPI.GetComponent<RunInfo>(dirEntity);
|
||||
var run = SystemAPI.GetComponent<RunRuntime>(dirEntity);
|
||||
bool gateOpen = info.Lifecycle == RunLifecycle.RouteSelect;
|
||||
|
||||
// Sender-region lookup (N3): connection NetworkId -> the player's CURRENT region. A player entity
|
||||
// without RegionTag simply never enters the map -> its pick is a clean reject, never a throw.
|
||||
var regionByConn = new NativeHashMap<int, byte>(8, Allocator.Temp);
|
||||
foreach (var (owner, region) in
|
||||
SystemAPI.Query<RefRO<GhostOwner>, RefRO<RegionTag>>().WithAll<PlayerTag>())
|
||||
regionByConn[owner.ValueRO.NetworkId] = region.ValueRO.Region;
|
||||
|
||||
// Local accepted flag beside the in-place write = the first-commit latch (nothing else writes
|
||||
// RouteCommand mid-loop; RunDirector's gate-entry clear ran on a previous tick by construction).
|
||||
bool accepted = SystemAPI.GetComponent<RouteCommand>(dirEntity).HasPick != 0;
|
||||
|
||||
var ecb = new EntityCommandBuffer(Allocator.Temp);
|
||||
foreach (var (receive, req, requestEntity) in
|
||||
SystemAPI.Query<RefRO<ReceiveRpcCommandRequest>, RefRO<RouteSelectRequest>>().WithEntityAccess())
|
||||
{
|
||||
var conn = receive.ValueRO.SourceConnection;
|
||||
bool valid = gateOpen
|
||||
&& !accepted
|
||||
&& (uint)req.ValueRO.ForRunEpoch == run.RunSeed
|
||||
&& req.ValueRO.ForLayer == info.CurrentRoom
|
||||
&& req.ValueRO.OptionIndex < info.RouteOptionCount
|
||||
&& SystemAPI.HasComponent<NetworkId>(conn)
|
||||
&& regionByConn.TryGetValue(SystemAPI.GetComponent<NetworkId>(conn).Value, out byte senderRegion)
|
||||
&& senderRegion == RegionId.Expedition;
|
||||
|
||||
if (valid)
|
||||
{
|
||||
// IMMEDIATE in-place commit (never an ECB-deferred write) + the local flag: first pick wins.
|
||||
SystemAPI.SetComponent(dirEntity, new RouteCommand
|
||||
{
|
||||
HasPick = 1,
|
||||
OptionIndex = req.ValueRO.OptionIndex,
|
||||
ForRunEpoch = run.RunEpoch, // the TRUE server epoch — never echo the client value
|
||||
ForLayer = req.ValueRO.ForLayer,
|
||||
});
|
||||
accepted = true;
|
||||
}
|
||||
|
||||
ecb.DestroyEntity(requestEntity);
|
||||
}
|
||||
ecb.Playback(state.EntityManager);
|
||||
regionByConn.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f56898976ef03af499c200e0d6f43b0d
|
||||
@@ -0,0 +1,409 @@
|
||||
using ProjectM.Simulation;
|
||||
using Unity.Burst;
|
||||
using Unity.Collections;
|
||||
using Unity.Entities;
|
||||
using Unity.Mathematics;
|
||||
using Unity.NetCode;
|
||||
using Unity.Transforms;
|
||||
|
||||
namespace ProjectM.Server
|
||||
{
|
||||
/// <summary>
|
||||
/// SOLE writer of the replicated run-lifecycle FSM (<see cref="RunInfo"/>) and its server-only working state
|
||||
/// (<see cref="RunRuntime"/>) — the expedition redesign's counterpart of CyclePhaseSystem's single-writer
|
||||
/// discipline (that system stays the sole writer of the BASE Calm↔Siege posture; the two FSMs are distinct).
|
||||
///
|
||||
/// Step-7 = the REAL LINEAR traversal: Staging (ready-check) → Launching (3-2-1 telegraph, un-ready aborts) →
|
||||
/// InRoom (fight; the clear edge arrives as the replicated <see cref="ExpeditionObjective"/>.State == Cleared,
|
||||
/// consumed ONE-TICK-LATE by construction — RoomEnemyDirectorSystem writes it after this system each tick, so no
|
||||
/// system-ordering back-edge exists) → RoomReward (cleared room TORN DOWN at entry via <see cref="RoomTeardown"/>;
|
||||
/// boon picks gate the exit from Step 10, all-Pending==0 today) → advance (bump room/epoch, flip the ping-pong
|
||||
/// sub-slot, teleport — teardown-at-entry + spawn-on-advance guarantees ≥1 empty tick and exactly ONE room alive)
|
||||
/// → … → Boss clear → Returning (teleport home + the CLEAR-GATED terminal bank) → Staging. Branching route
|
||||
/// choice (RouteSelect) replaces the fixed col-0 advance at Step 8.
|
||||
///
|
||||
/// The terminal bank (once per RunEpoch, equality-latched): ALWAYS records the honest depth
|
||||
/// (max(MaxDepthReached, RoomsClearedThisRun) — never the planned RoomCount) and re-stages; ONLY a genuine
|
||||
/// boss-clear terminal (<see cref="RunRuntime.LastTerminalCleared"/>) credits the win meter
|
||||
/// (<see cref="GoalProgress"/>.Charge, clamped), RunsCompleted, the retaliation inputs
|
||||
/// (<see cref="ThreatState"/>.PendingReturns/ExpeditionsCompleted — carried from the retired gate, C7) and
|
||||
/// requests a save. An abort/wipe banks NOTHING but the depth high-water (D-F3).
|
||||
///
|
||||
/// Ordering: <c>[UpdateBefore(CyclePhaseSystem)]</c> ONLY (GoalReachedSystem is [UpdateAfter(CyclePhaseSystem)] —
|
||||
/// transitively after this system, so the Charge credit lands before it reads the edge). Per the hard rule,
|
||||
/// NOTHING in the room chain adds another CyclePhase edge (a sort cycle is invisible to EditMode and throws only
|
||||
/// at Play world creation).
|
||||
/// </summary>
|
||||
[BurstCompile]
|
||||
[WorldSystemFilter(WorldSystemFilterFlags.ServerSimulation)]
|
||||
[UpdateInGroup(typeof(SimulationSystemGroup))]
|
||||
[UpdateBefore(typeof(CyclePhaseSystem))]
|
||||
public partial struct RunDirectorSystem : ISystem
|
||||
{
|
||||
/// <summary>"All ready → 3-2-1 → go" telegraph (~3 s @ 60). An un-ready during the countdown aborts.</summary>
|
||||
const uint LaunchCountdownTicks = 180;
|
||||
|
||||
/// <summary>Boon-pick grace (~30 s @ 60): RoomReward advances when every survivor picked OR this elapses
|
||||
/// (the AFK/disconnect backstop; the un-picked-offer policy lands with the boons at Step 10).</summary>
|
||||
const uint RewardGraceTicks = 1800;
|
||||
|
||||
/// <summary>Route-choice grace (~30 s @ 60): the gate auto-picks the LOWEST-INDEX reachable option when it
|
||||
/// elapses (the AFK backstop; an accepted pick always beats a same-tick expiry — review F2).</summary>
|
||||
const uint RouteGraceTicks = 1800;
|
||||
|
||||
EntityQuery m_RoomTagged;
|
||||
|
||||
[BurstCompile]
|
||||
public void OnCreate(ref SystemState state)
|
||||
{
|
||||
state.RequireForUpdate<NetworkTime>();
|
||||
state.RequireForUpdate<RunInfo>();
|
||||
state.RequireForUpdate<RunRuntime>();
|
||||
m_RoomTagged = state.GetEntityQuery(ComponentType.ReadOnly<RoomTag>());
|
||||
}
|
||||
|
||||
[BurstCompile]
|
||||
public void OnUpdate(ref SystemState state)
|
||||
{
|
||||
var serverTick = SystemAPI.GetSingleton<NetworkTime>().ServerTick;
|
||||
if (!serverTick.IsValid)
|
||||
return;
|
||||
uint now = serverTick.TickIndexForValidTick;
|
||||
|
||||
var dirEntity = SystemAPI.GetSingletonEntity<RunInfo>();
|
||||
var info = SystemAPI.GetComponent<RunInfo>(dirEntity);
|
||||
var run = SystemAPI.GetComponent<RunRuntime>(dirEntity);
|
||||
|
||||
float3 baseCenter = new float3(0f, 1f, 0f);
|
||||
if (SystemAPI.TryGetSingleton<BaseAnchor>(out var anchor))
|
||||
baseCenter = BaseGridMath.PlotCenter(anchor);
|
||||
|
||||
// Ready-check + party-presence derivation, shared across the states below. The party is co-located at
|
||||
// base while Staging (the N7 co-location invariant), so live PlayerTag ghosts ARE the roster; a
|
||||
// disconnect drops the counts (LinkedEntityGroup despawn) and the checks re-derive clean.
|
||||
int totalPlayers = 0, readyPlayers = 0, expeditionPlayers = 0;
|
||||
foreach (var (ready, region) in
|
||||
SystemAPI.Query<RefRO<PlayerReady>, RefRO<RegionTag>>().WithAll<PlayerTag>())
|
||||
{
|
||||
totalPlayers++;
|
||||
if (ready.ValueRO.Value != 0) readyPlayers++;
|
||||
if (region.ValueRO.Region == RegionId.Expedition) expeditionPlayers++;
|
||||
}
|
||||
bool allReady = totalPlayers > 0 && readyPlayers == totalPlayers;
|
||||
|
||||
switch (info.Lifecycle)
|
||||
{
|
||||
case RunLifecycle.Staging:
|
||||
{
|
||||
// F2 cross-FSM launch guard: no new run while a final siege arms/runs or the outcome latched.
|
||||
// Guards default OPEN when the server-only markers are absent (EditMode worlds).
|
||||
bool launchAllowed =
|
||||
(!SystemAPI.HasComponent<RunPhase>(dirEntity)
|
||||
|| SystemAPI.GetComponent<RunPhase>(dirEntity).Value == RunPhaseId.Normal)
|
||||
&& (!SystemAPI.HasComponent<RunOutcome>(dirEntity)
|
||||
|| SystemAPI.GetComponent<RunOutcome>(dirEntity).Value == RunOutcomeId.InProgress);
|
||||
|
||||
if (allReady && run.WasAllReady == 0 && launchAllowed)
|
||||
{
|
||||
// Rising edge → Launching. Seed the run: monotonic epoch + per-playthrough salt lineage,
|
||||
// never a tick, never 0, equality-compared downstream.
|
||||
run.RunEpoch += 1;
|
||||
run.HostSalt = RunMapMath.Hash(run.HostSalt, (uint)run.RunEpoch);
|
||||
run.RunSeed = math.max(1u, RunMapMath.Hash((uint)run.RunEpoch, run.HostSalt));
|
||||
run.NodeBudgetRemaining = Tuning.ExpeditionNodeBudget;
|
||||
run.RoomsClearedThisRun = 0;
|
||||
run.BoonPickCounter = 0; // fresh boon-band provenance per run
|
||||
run.LastTerminalCleared = 0;
|
||||
|
||||
var map = RunMapMath.Generate(run.RunSeed);
|
||||
info.RunSeed = run.RunSeed;
|
||||
info.RoomCount = map.LayerCount;
|
||||
info.LaunchTick = TickUtil.NonZero(now + LaunchCountdownTicks);
|
||||
info.Lifecycle = RunLifecycle.Launching;
|
||||
}
|
||||
run.WasAllReady = (byte)(allReady ? 1 : 0);
|
||||
break;
|
||||
}
|
||||
|
||||
case RunLifecycle.Launching:
|
||||
{
|
||||
// Un-ready during the countdown aborts back to Staging (the telegraph's escape hatch).
|
||||
if (!allReady)
|
||||
{
|
||||
info.LaunchTick = 0u;
|
||||
info.Lifecycle = RunLifecycle.Staging;
|
||||
run.WasAllReady = 0;
|
||||
break;
|
||||
}
|
||||
|
||||
bool due = info.LaunchTick == 0u || !new NetworkTick(info.LaunchTick).IsNewerThan(serverTick);
|
||||
if (due)
|
||||
{
|
||||
// Enter room 0 (the guaranteed Combat landing at column 0, sub-slot 0).
|
||||
var map = RunMapMath.Generate(run.RunSeed);
|
||||
EnterRoom(ref state, ref info, ref run, in map, layer: 0, col: 0, baseCenter, bumpEpoch: true);
|
||||
info.LaunchTick = 0u;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case RunLifecycle.InRoom:
|
||||
{
|
||||
// All expedition players gone (disconnect/death-warp edge) → clean abort, no credit.
|
||||
if (expeditionPlayers == 0)
|
||||
{
|
||||
run.LastTerminalCleared = 0;
|
||||
info.Lifecycle = RunLifecycle.Returning;
|
||||
break;
|
||||
}
|
||||
|
||||
// The room clear edge — the replicated objective RoomEnemyDirectorSystem computed LAST tick
|
||||
// (one-tick-late by construction; no ordering back-edge). Teardown happens AT THIS ENTRY, the
|
||||
// next room spawns on the advance tick → ≥1 empty tick, exactly one room alive.
|
||||
if (SystemAPI.HasComponent<ExpeditionObjective>(dirEntity)
|
||||
&& SystemAPI.GetComponent<ExpeditionObjective>(dirEntity).State == ExpeditionObjectiveState.Cleared)
|
||||
{
|
||||
var ecb = new EntityCommandBuffer(Allocator.Temp);
|
||||
RoomTeardown.DestroyRoom(m_RoomTagged, ecb, (byte)(info.CurrentRoom & 0xFF));
|
||||
ecb.Playback(state.EntityManager);
|
||||
ecb.Dispose();
|
||||
|
||||
run.RoomsClearedThisRun += 1;
|
||||
if (info.CurrentRoom >= info.RoomCount - 1)
|
||||
run.LastTerminalCleared = 1; // the Boss fell — a genuine terminal clear
|
||||
run.RewardGraceTick = TickUtil.NonZero(now + RewardGraceTicks);
|
||||
info.Lifecycle = RunLifecycle.RoomReward;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case RunLifecycle.RoomReward:
|
||||
{
|
||||
if (expeditionPlayers == 0 && run.LastTerminalCleared == 0)
|
||||
{
|
||||
info.Lifecycle = RunLifecycle.Returning;
|
||||
break;
|
||||
}
|
||||
|
||||
// Exit gate: every SURVIVING player has picked (BoonOffer.Pending==0 — inert until Step 10)
|
||||
// OR the grace elapsed (wrap-safe IsNewerThan, never raw uint — F4).
|
||||
bool anyPending = false;
|
||||
foreach (var offer in SystemAPI.Query<RefRO<BoonOffer>>().WithAll<PlayerTag>())
|
||||
if (offer.ValueRO.Pending != 0) { anyPending = true; break; }
|
||||
bool graceElapsed = run.RewardGraceTick == 0u
|
||||
|| !new NetworkTick(run.RewardGraceTick).IsNewerThan(serverTick);
|
||||
if (anyPending && !graceElapsed)
|
||||
break;
|
||||
|
||||
run.RewardGraceTick = 0u;
|
||||
if (run.LastTerminalCleared != 0)
|
||||
{
|
||||
info.Lifecycle = RunLifecycle.Returning; // boss cleared — go home a winner
|
||||
}
|
||||
else
|
||||
{
|
||||
// Open the ROUTE GATE (Step 8 — the branching choice): publish the AUTHORITATIVE reachable
|
||||
// options (the client map panel is regen-for-display; the clickable buttons bind to these
|
||||
// bytes). The cleared room is already gone — RouteSelect IS the teardown gap; the next room
|
||||
// materializes only when the choice commits.
|
||||
var map = RunMapMath.Generate(run.RunSeed);
|
||||
int optionCount = RunMapMath.ReachableOptions(in map, info.CurrentRoom, info.CurrentCol,
|
||||
out var cols);
|
||||
if (optionCount == 0)
|
||||
{
|
||||
// Unreachable by construction (every non-terminal node has an out-edge) — a future
|
||||
// generator regression must abort CLEANLY, never wedge on stale options (review F4).
|
||||
info.RouteOptionCount = 0;
|
||||
info.Lifecycle = RunLifecycle.Returning;
|
||||
}
|
||||
else
|
||||
{
|
||||
int nextLayer = info.CurrentRoom + 1;
|
||||
info.RouteOptionCount = (byte)math.min(optionCount, 3);
|
||||
info.RouteOpt0Col = cols.Length > 0 ? cols[0] : (byte)0;
|
||||
info.RouteOpt1Col = cols.Length > 1 ? cols[1] : (byte)0;
|
||||
info.RouteOpt2Col = cols.Length > 2 ? cols[2] : (byte)0;
|
||||
info.RouteOpt0Type = cols.Length > 0 ? map.Node(nextLayer, cols[0]).RoomType : (byte)0;
|
||||
info.RouteOpt1Type = cols.Length > 1 ? map.Node(nextLayer, cols[1]).RoomType : (byte)0;
|
||||
info.RouteOpt2Type = cols.Length > 2 ? map.Node(nextLayer, cols[2]).RoomType : (byte)0;
|
||||
run.RouteGraceTick = TickUtil.NonZero(now + RouteGraceTicks);
|
||||
// Entry-clear: any accepted pick provably belongs to THIS gate (RouteSelectSystem runs
|
||||
// BEFORE this system, so it cannot accept on the entry tick).
|
||||
if (SystemAPI.HasComponent<RouteCommand>(dirEntity))
|
||||
SystemAPI.SetComponent(dirEntity, default(RouteCommand));
|
||||
info.Lifecycle = RunLifecycle.RouteSelect;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case RunLifecycle.RouteSelect:
|
||||
{
|
||||
// Predicate order is LOAD-BEARING (review F2): abort → pick-consume → grace. A same-tick pick
|
||||
// from a vanishing party must never resurrect the run (EnterRoom would conscript base players);
|
||||
// an accepted pick must beat a same-tick grace expiry (the player was told "committed").
|
||||
if (expeditionPlayers == 0)
|
||||
{
|
||||
info.RouteOptionCount = 0; // close the gate ON the abort edge itself (review F3)
|
||||
run.LastTerminalCleared = 0;
|
||||
info.Lifecycle = RunLifecycle.Returning;
|
||||
break;
|
||||
}
|
||||
|
||||
var cmd = SystemAPI.HasComponent<RouteCommand>(dirEntity)
|
||||
? SystemAPI.GetComponent<RouteCommand>(dirEntity)
|
||||
: default;
|
||||
bool routeGraceElapsed = run.RouteGraceTick == 0u
|
||||
|| !new NetworkTick(run.RouteGraceTick).IsNewerThan(serverTick);
|
||||
|
||||
if (cmd.HasPick != 0)
|
||||
{
|
||||
// The party's committed choice (first-accepted-wins latch; any-player-first-commits).
|
||||
byte chosenCol = cmd.OptionIndex == 2 ? info.RouteOpt2Col
|
||||
: cmd.OptionIndex == 1 ? info.RouteOpt1Col : info.RouteOpt0Col;
|
||||
if (SystemAPI.HasComponent<RouteCommand>(dirEntity))
|
||||
SystemAPI.SetComponent(dirEntity, default(RouteCommand));
|
||||
run.RouteGraceTick = 0u;
|
||||
var map = RunMapMath.Generate(run.RunSeed);
|
||||
EnterRoom(ref state, ref info, ref run, in map, info.CurrentRoom + 1, chosenCol, baseCenter, bumpEpoch: true);
|
||||
}
|
||||
else if (routeGraceElapsed)
|
||||
{
|
||||
// AFK backstop: deterministic LOWEST-INDEX reachable option (RouteOpt0 is ascending-first).
|
||||
run.RouteGraceTick = 0u;
|
||||
var map = RunMapMath.Generate(run.RunSeed);
|
||||
EnterRoom(ref state, ref info, ref run, in map, info.CurrentRoom + 1, info.RouteOpt0Col, baseCenter, bumpEpoch: true);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case RunLifecycle.Returning:
|
||||
{
|
||||
// Party teleport HOME + region flip back to Base.
|
||||
int idx = 0;
|
||||
foreach (var (region, xform) in
|
||||
SystemAPI.Query<RefRW<RegionTag>, RefRW<LocalTransform>>().WithAll<PlayerTag>())
|
||||
{
|
||||
region.ValueRW.Region = RegionId.Base;
|
||||
var p = baseCenter;
|
||||
p.x += 1.5f * idx;
|
||||
p.y = xform.ValueRO.Position.y;
|
||||
xform.ValueRW.Position = p;
|
||||
idx++;
|
||||
}
|
||||
|
||||
// THE terminal bank — once per RunEpoch (equality latch, F7), CLEAR-GATED (D-F3).
|
||||
if (run.LastBankedRunEpoch != run.RunEpoch)
|
||||
{
|
||||
run.LastBankedRunEpoch = run.RunEpoch;
|
||||
|
||||
// Always: the honest depth high-water (actual rooms cleared, never the planned count).
|
||||
if (SystemAPI.HasComponent<MetaCounters>(dirEntity))
|
||||
{
|
||||
var meta = SystemAPI.GetComponent<MetaCounters>(dirEntity);
|
||||
meta.MaxDepthReached = math.max(meta.MaxDepthReached, run.RoomsClearedThisRun);
|
||||
if (run.LastTerminalCleared != 0)
|
||||
meta.RunsCompleted += 1;
|
||||
SystemAPI.SetComponent(dirEntity, meta);
|
||||
info.RunsCompleted = meta.RunsCompleted; // HUD mirror
|
||||
info.MaxDepthReached = meta.MaxDepthReached; // HUD mirror
|
||||
}
|
||||
|
||||
// Boss-clear only: the win meter, the retaliation inputs (C7), and a save checkpoint.
|
||||
if (run.LastTerminalCleared != 0)
|
||||
{
|
||||
if (SystemAPI.HasComponent<GoalProgress>(dirEntity))
|
||||
{
|
||||
var goal = SystemAPI.GetComponent<GoalProgress>(dirEntity);
|
||||
goal.Charge = math.min(goal.Charge + 1, goal.Target);
|
||||
SystemAPI.SetComponent(dirEntity, goal);
|
||||
}
|
||||
if (SystemAPI.HasComponent<ThreatState>(dirEntity))
|
||||
{
|
||||
var threat = SystemAPI.GetComponent<ThreatState>(dirEntity);
|
||||
threat.PendingReturns += 1;
|
||||
threat.ExpeditionsCompleted += 1;
|
||||
SystemAPI.SetComponent(dirEntity, threat);
|
||||
}
|
||||
if (SystemAPI.HasComponent<SaveRequest>(dirEntity))
|
||||
SystemAPI.SetComponent(dirEntity, new SaveRequest { Pending = 1 });
|
||||
}
|
||||
}
|
||||
|
||||
// TWO-CHANNEL strip (DR-037): run boons EXPIRE at home — one range-strip clears every
|
||||
// boon-band StatModifier (replicates via the [GhostField] buffer; StatRecompute reverts the
|
||||
// effective stats on both worlds) and zeroes any straggler offer. Class/meta/equip bands are
|
||||
// disjoint and survive. Idempotent — safe on every Returning tick.
|
||||
foreach (var (mods, offer) in
|
||||
SystemAPI.Query<DynamicBuffer<StatModifier>, RefRW<BoonOffer>>().WithAll<PlayerTag>())
|
||||
{
|
||||
TimedModifierUtil.RemoveBySourceIdRange(mods, Tuning.BoonSourceIdBase,
|
||||
Tuning.BoonSourceIdBase + Tuning.BoonSourceIdSpan);
|
||||
offer.ValueRW = default;
|
||||
}
|
||||
|
||||
// Clear EVERY ready flag — the next run needs a fresh, deliberate ready-check from everyone.
|
||||
foreach (var ready in SystemAPI.Query<RefRW<PlayerReady>>().WithAll<PlayerTag>())
|
||||
ready.ValueRW.Value = 0;
|
||||
|
||||
run.RewardGraceTick = 0u;
|
||||
run.RouteGraceTick = 0u; // gate hygiene (review F5): no stale grace into the next run
|
||||
if (SystemAPI.HasComponent<RouteCommand>(dirEntity))
|
||||
SystemAPI.SetComponent(dirEntity, default(RouteCommand)); // no leftover latch either
|
||||
run.WasAllReady = 0;
|
||||
run.LastTerminalCleared = 0;
|
||||
|
||||
info.CurrentRoom = 0;
|
||||
info.RouteOptionCount = 0;
|
||||
info.LaunchTick = 0u;
|
||||
info.Lifecycle = RunLifecycle.Staging;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Single write-back point — RunInfo/RunRuntime are ALWAYS published (F12: the HUD readout can never
|
||||
// freeze stale behind a branch's early-break).
|
||||
SystemAPI.SetComponent(dirEntity, info);
|
||||
SystemAPI.SetComponent(dirEntity, run);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Enter room (<paramref name="layer"/>, <paramref name="col"/>): publish the node as the single plan
|
||||
/// authority (<see cref="RunRuntime.CurrentNodeId"/>/<c>CurrentRoomType</c> — the field/enemy directors NEVER
|
||||
/// re-derive it), flip the ping-pong sub-slot, bump <see cref="RunRuntime.RoomEpoch"/> so the room systems
|
||||
/// reseed, and teleport the party onto the new origin (Position write in place — never FromPosition).
|
||||
/// </summary>
|
||||
void EnterRoom(ref SystemState state, ref RunInfo info, ref RunRuntime run, in RunMap map,
|
||||
int layer, int col, float3 baseCenter, bool bumpEpoch)
|
||||
{
|
||||
var node = map.Node(layer, col);
|
||||
run.ActiveSubSlot = (byte)(layer & 1);
|
||||
run.CurrentNodeId = RunMap.NodeId(layer, col);
|
||||
run.CurrentCol = (byte)col;
|
||||
run.CurrentRoomType = node.RoomType;
|
||||
if (bumpEpoch)
|
||||
run.RoomEpoch += 1;
|
||||
|
||||
info.CurrentRoom = layer;
|
||||
info.CurrentCol = (byte)col;
|
||||
info.CurrentRoomType = node.RoomType;
|
||||
info.CurrentBiome = node.Biome;
|
||||
info.RouteOptionCount = 0;
|
||||
|
||||
float3 roomOrigin = RegionMath.ExpeditionRoomOrigin(baseCenter, run.ActiveSubSlot);
|
||||
int idx = 0;
|
||||
foreach (var (region, xform) in
|
||||
SystemAPI.Query<RefRW<RegionTag>, RefRW<LocalTransform>>().WithAll<PlayerTag>())
|
||||
{
|
||||
region.ValueRW.Region = RegionId.Expedition;
|
||||
var p = roomOrigin;
|
||||
p.x += 1.5f * idx; // small spread so kinematic capsules don't stack
|
||||
p.y = xform.ValueRO.Position.y;
|
||||
xform.ValueRW.Position = p;
|
||||
idx++;
|
||||
}
|
||||
|
||||
info.Lifecycle = RunLifecycle.InRoom;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b7c36de338378264e9aa0ad2c2512e7f
|
||||
@@ -11,8 +11,9 @@ namespace ProjectM.Server
|
||||
/// Server-only composite ThreatDirector — the data-driven base-attack SCHEDULER. It owns the decision of WHEN
|
||||
/// and HOW BIG a siege is; <see cref="CyclePhaseSystem"/> owns the Calm↔Siege transition. The single documented
|
||||
/// hand-off is <see cref="ThreatState.PendingSiegeSize"/> (this system sets it; CyclePhaseSystem consumes it).
|
||||
/// This slice wires ONE source — POST-EXPEDITION retaliation: a player returning to base (counted as
|
||||
/// <see cref="ThreatState.PendingReturns"/> by <see cref="ExpeditionGateSystem"/>) arms a siege of
|
||||
/// This slice wires ONE source — POST-EXPEDITION retaliation: a completed RUN (banked on the boss-clear
|
||||
/// return by <see cref="RunDirectorSystem"/> into <see cref="ThreatState.PendingReturns"/> — the retired
|
||||
/// walk-in ExpeditionGateSystem's carry, Step 11) arms a siege ofe of
|
||||
/// <see cref="ThreatConfig.SizeBase"/> Husks after a <see cref="ThreatConfig.PostExpeditionDelayTicks"/>
|
||||
/// telegraph. The Heat/Schedule sources are reserved (config baked-but-inert) so they drop in additively with
|
||||
/// no re-bake. It also enforces a BOUNDED siege lifetime (<see cref="ThreatConfig.SiegeTimeoutTicks"/>): an
|
||||
@@ -24,7 +25,7 @@ namespace ProjectM.Server
|
||||
[BurstCompile]
|
||||
[WorldSystemFilter(WorldSystemFilterFlags.ServerSimulation)]
|
||||
[UpdateInGroup(typeof(SimulationSystemGroup))]
|
||||
[UpdateAfter(typeof(ExpeditionGateSystem))]
|
||||
[UpdateAfter(typeof(RunDirectorSystem))]
|
||||
[UpdateBefore(typeof(CyclePhaseSystem))]
|
||||
public partial struct ThreatDirectorSystem : ISystem
|
||||
{
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
using Unity.NetCode;
|
||||
|
||||
namespace ProjectM.Simulation
|
||||
{
|
||||
/// <summary>
|
||||
/// Client -> server request to upgrade the sender's ability damage one tier, spending Aether from the
|
||||
/// shared ledger. A one-off RPC. The server grows a single damage <see cref="StatModifier"/> on the
|
||||
/// player (replace-by-SourceId so the buffer stays bounded), which StatRecomputeSystem folds into
|
||||
/// EffectiveAbilityStats.Damage on both worlds — no new replicated component.
|
||||
/// </summary>
|
||||
public struct AbilityUpgradeRequest : IRpcCommand { }
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 1236d3751a5740741a4a10e0a653565f
|
||||
@@ -0,0 +1,180 @@
|
||||
using Unity.Collections;
|
||||
using Unity.Entities;
|
||||
|
||||
namespace ProjectM.Simulation
|
||||
{
|
||||
/// <summary>
|
||||
/// One authored boon in the catalog blob: a thin wrapper over the existing stat pipeline —
|
||||
/// <see cref="Target"/>/<see cref="Op"/>/<see cref="Value"/> map 1:1 onto a <see cref="StatModifier"/> row
|
||||
/// (bytes, never enums, on the baked path). <see cref="Id"/> is the stable APPEND-ONLY key the replicated
|
||||
/// <c>BoonOffer</c> options and pick RPC carry. <see cref="Weight"/> is the rarity draw weight
|
||||
/// (common 100 / rare 30 / epic 10). <see cref="ClassMask"/> gates by class: bit0 = Warrior (classId 0),
|
||||
/// bit1 = Ranger (classId 1), 3 = both.
|
||||
/// </summary>
|
||||
public struct BoonDefBlob
|
||||
{
|
||||
public byte Id;
|
||||
public byte Target; // StatTarget as byte
|
||||
public byte Op; // ModOp as byte
|
||||
public float Value;
|
||||
public byte Weight;
|
||||
public byte ClassMask;
|
||||
public FixedString64Bytes Name;
|
||||
public FixedString128Bytes Desc;
|
||||
}
|
||||
|
||||
/// <summary>The baked boon pool (config blob, both worlds, NOT replicated — the AbilityDatabase pattern).</summary>
|
||||
public struct BoonCatalogBlob
|
||||
{
|
||||
public BlobArray<BoonDefBlob> Defs;
|
||||
}
|
||||
|
||||
/// <summary>Singleton component carrying the baked catalog (place ONE BoonCatalogAuthoring in the subscene).</summary>
|
||||
public struct BoonCatalog : IComponentData
|
||||
{
|
||||
public BlobAssetReference<BoonCatalogBlob> Value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Server-only bookkeeping for <c>BoonOfferSystem</c>, attached at runtime beside the catalog singleton (the
|
||||
/// RoomFieldState idiom): the RoomEpoch offers were last drawn for — int equality, one offer set per room.
|
||||
/// </summary>
|
||||
public struct BoonOfferState : IComponentData
|
||||
{
|
||||
public int OfferedRoomEpoch;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Pure, deterministic boon selection math — integer-hash only (<see cref="RunMapMath.Hash(uint,uint)"/> chain,
|
||||
/// no RNG state), so an offer is a reproducible function of (runSeed, room, player). EditMode-tested.
|
||||
/// </summary>
|
||||
public static class BoonMath
|
||||
{
|
||||
/// <summary>Class-mask bit for a wire class id (0 = Warrior, 1 = Ranger).</summary>
|
||||
public static byte MaskFor(byte classId) => (byte)(1 << (classId & 1));
|
||||
|
||||
/// <summary>
|
||||
/// Draw 3 DISTINCT, rarity-weighted, class-filtered boon ids from the pool. Deterministic per
|
||||
/// <paramref name="offerSeed"/>. If the class-legal pool has fewer than 3 entries the tail repeats the
|
||||
/// last-drawn candidates (a catalog authoring smell, not a crash). Returns the number of distinct ids.
|
||||
/// </summary>
|
||||
public static int PickBoons(uint offerSeed, byte classId, ref BoonCatalogBlob pool,
|
||||
out byte o0, out byte o1, out byte o2)
|
||||
{
|
||||
byte classBit = MaskFor(classId);
|
||||
|
||||
// Class-legal candidate indices + the total weight.
|
||||
var candidates = new FixedList128Bytes<byte>();
|
||||
int totalWeight = 0;
|
||||
for (int i = 0; i < pool.Defs.Length && candidates.Length < candidates.Capacity; i++)
|
||||
{
|
||||
if ((pool.Defs[i].ClassMask & classBit) == 0) continue;
|
||||
if (pool.Defs[i].Weight == 0) continue;
|
||||
candidates.Add((byte)i);
|
||||
totalWeight += pool.Defs[i].Weight;
|
||||
}
|
||||
|
||||
o0 = o1 = o2 = 0;
|
||||
if (candidates.Length == 0)
|
||||
return 0;
|
||||
|
||||
var picked = new FixedList32Bytes<byte>(); // picked catalog indices
|
||||
uint salt = 0;
|
||||
while (picked.Length < 3 && picked.Length < candidates.Length)
|
||||
{
|
||||
// Weighted draw with rejection on duplicates (bounded; falls through to a linear fill).
|
||||
uint roll = RunMapMath.Hash(offerSeed, (uint)picked.Length, salt) % (uint)totalWeight;
|
||||
byte drawn = candidates[candidates.Length - 1];
|
||||
int acc = 0;
|
||||
for (int c = 0; c < candidates.Length; c++)
|
||||
{
|
||||
acc += pool.Defs[candidates[c]].Weight;
|
||||
if (roll < (uint)acc) { drawn = candidates[c]; break; }
|
||||
}
|
||||
|
||||
bool dup = false;
|
||||
for (int p = 0; p < picked.Length; p++)
|
||||
if (picked[p] == drawn) { dup = true; break; }
|
||||
|
||||
if (!dup)
|
||||
{
|
||||
picked.Add(drawn);
|
||||
salt = 0;
|
||||
}
|
||||
else if (++salt > 16)
|
||||
{
|
||||
// Rejection budget spent — take the first unpicked candidate (still deterministic).
|
||||
for (int c = 0; c < candidates.Length; c++)
|
||||
{
|
||||
bool used = false;
|
||||
for (int p = 0; p < picked.Length; p++)
|
||||
if (picked[p] == candidates[c]) { used = true; break; }
|
||||
if (!used) { picked.Add(candidates[c]); break; }
|
||||
}
|
||||
salt = 0;
|
||||
}
|
||||
}
|
||||
|
||||
o0 = picked.Length > 0 ? pool.Defs[picked[0]].Id : (byte)0;
|
||||
o1 = picked.Length > 1 ? pool.Defs[picked[1]].Id : o0;
|
||||
o2 = picked.Length > 2 ? pool.Defs[picked[2]].Id : o1;
|
||||
return picked.Length;
|
||||
}
|
||||
|
||||
/// <summary>Find a def index by its stable id (-1 when absent — callers preserve-and-skip unknown ids).</summary>
|
||||
public static int FindDef(ref BoonCatalogBlob pool, byte id)
|
||||
{
|
||||
for (int i = 0; i < pool.Defs.Length; i++)
|
||||
if (pool.Defs[i].Id == id) return i;
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The DEFAULT v1 boon table + the blob builder the baker AND EditMode tests share (single source — the
|
||||
/// authoring bakes this table verbatim when its designer-row list is empty). Append-only ids.
|
||||
/// </summary>
|
||||
public static class BoonCatalogData
|
||||
{
|
||||
/// <summary>Build the default catalog blob (caller owns/disposes the reference).</summary>
|
||||
public static BlobAssetReference<BoonCatalogBlob> BuildDefault(Allocator allocator = Allocator.Persistent)
|
||||
{
|
||||
var builder = new BlobBuilder(Allocator.Temp);
|
||||
ref var root = ref builder.ConstructRoot<BoonCatalogBlob>();
|
||||
var defs = builder.Allocate(ref root.Defs, 12);
|
||||
int i = 0;
|
||||
// id, target, op, value, weight, mask(1=Warrior,2=Ranger,3=both), name, desc
|
||||
defs[i++] = Make(1, StatTarget.Damage, ModOp.PercentAdd, 0.20f, 100, 3, "Honed Edge", "+20% ability damage");
|
||||
defs[i++] = Make(2, StatTarget.CooldownTicks, ModOp.PercentMult, -0.15f, 100, 3, "Swift Hands", "-15% ability cooldown");
|
||||
defs[i++] = Make(3, StatTarget.Range, ModOp.PercentAdd, 0.25f, 100, 2, "Long Reach", "+25% projectile range");
|
||||
defs[i++] = Make(4, StatTarget.MoveSpeed, ModOp.PercentAdd, 0.12f, 100, 3, "Fleet Foot", "+12% move speed");
|
||||
defs[i++] = Make(5, StatTarget.MaxHealth, ModOp.Flat, 25f, 100, 3, "Iron Constitution", "+25 max health");
|
||||
defs[i++] = Make(6, StatTarget.MeleeDamage, ModOp.PercentAdd, 0.25f, 100, 1, "Heavy Blows", "+25% melee damage");
|
||||
defs[i++] = Make(7, StatTarget.MeleeRange, ModOp.PercentAdd, 0.20f, 60, 1, "Extended Haft", "+20% melee reach");
|
||||
defs[i++] = Make(8, StatTarget.ProjectileSpeed, ModOp.PercentAdd, 0.25f, 60, 2, "Swift Bolts", "+25% projectile speed");
|
||||
defs[i++] = Make(9, StatTarget.AutoTargetRange, ModOp.PercentAdd, 0.20f, 60, 3, "Keen Instinct", "+20% auto-target range");
|
||||
defs[i++] = Make(10, StatTarget.CooldownTicks, ModOp.PercentMult, -0.25f, 30, 3, "Berserker's Pace", "-25% ability cooldown");
|
||||
defs[i++] = Make(11, StatTarget.MaxHealth, ModOp.Flat, 60f, 30, 3, "Titan's Vigor", "+60 max health");
|
||||
defs[i++] = Make(12, StatTarget.Damage, ModOp.PercentAdd, 0.50f, 10, 3, "Executioner", "+50% ability damage");
|
||||
var blob = builder.CreateBlobAssetReference<BoonCatalogBlob>(allocator);
|
||||
builder.Dispose();
|
||||
return blob;
|
||||
}
|
||||
|
||||
static BoonDefBlob Make(byte id, StatTarget target, ModOp op, float value, byte weight, byte mask,
|
||||
string name, string desc)
|
||||
{
|
||||
return new BoonDefBlob
|
||||
{
|
||||
Id = id,
|
||||
Target = (byte)target,
|
||||
Op = (byte)op,
|
||||
Value = value,
|
||||
Weight = weight,
|
||||
ClassMask = mask,
|
||||
Name = new FixedString64Bytes(name),
|
||||
Desc = new FixedString128Bytes(desc),
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 00909311d983d7a43afc195595aff217
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user