Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4e795a38e9 | |||
| ce0faafb72 | |||
| 7f032571df | |||
| 2710d680b4 | |||
| cd634a9df5 | |||
| 760c77c083 | |||
| c64cda6e44 | |||
| 730bad3d74 |
@@ -633,7 +633,7 @@ AnimatorState:
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_Name: Swing3
|
||||
m_Speed: 1.1
|
||||
m_Speed: 0.88
|
||||
m_CycleOffset: 0
|
||||
m_Transitions:
|
||||
- {fileID: 7246856984932168808}
|
||||
@@ -779,7 +779,7 @@ AnimatorState:
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_Name: Swing2
|
||||
m_Speed: 1
|
||||
m_Speed: 0.85
|
||||
m_CycleOffset: 0
|
||||
m_Transitions:
|
||||
- {fileID: -7565464033697258994}
|
||||
@@ -1050,7 +1050,7 @@ AnimatorState:
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_Name: Swing1
|
||||
m_Speed: 1.25
|
||||
m_Speed: 1
|
||||
m_CycleOffset: 0
|
||||
m_Transitions:
|
||||
- {fileID: -1792799526728587842}
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,137 @@
|
||||
%YAML 1.1
|
||||
%TAG !u! tag:unity3d.com,2011:
|
||||
--- !u!21 &2100000
|
||||
Material:
|
||||
serializedVersion: 8
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_Name: M_Weapon_Preview
|
||||
m_Shader: {fileID: 4800000, guid: 933532a4fcc9baf4fa0491de14d08ed7, type: 3}
|
||||
m_Parent: {fileID: 0}
|
||||
m_ModifiedSerializedProperties: 0
|
||||
m_ValidKeywords: []
|
||||
m_InvalidKeywords: []
|
||||
m_LightmapFlags: 4
|
||||
m_EnableInstancingVariants: 0
|
||||
m_DoubleSidedGI: 0
|
||||
m_CustomRenderQueue: -1
|
||||
stringTagMap:
|
||||
RenderType: Opaque
|
||||
disabledShaderPasses:
|
||||
- MOTIONVECTORS
|
||||
m_LockedProperties:
|
||||
m_SavedProperties:
|
||||
serializedVersion: 3
|
||||
m_TexEnvs:
|
||||
- _BaseMap:
|
||||
m_Texture: {fileID: 2800000, guid: 48dd9a7736c11b5448a74711310e2ee6, type: 3}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _BumpMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _DetailAlbedoMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _DetailMask:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _DetailNormalMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _EmissionMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _MainTex:
|
||||
m_Texture: {fileID: 2800000, guid: 48dd9a7736c11b5448a74711310e2ee6, type: 3}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _MetallicGlossMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _OcclusionMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _ParallaxMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- _SpecGlossMap:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- unity_Lightmaps:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- unity_LightmapsInd:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
- unity_ShadowMasks:
|
||||
m_Texture: {fileID: 0}
|
||||
m_Scale: {x: 1, y: 1}
|
||||
m_Offset: {x: 0, y: 0}
|
||||
m_Ints: []
|
||||
m_Floats:
|
||||
- _AddPrecomputedVelocity: 0
|
||||
- _AlphaClip: 0
|
||||
- _AlphaToMask: 0
|
||||
- _Blend: 0
|
||||
- _BlendModePreserveSpecular: 1
|
||||
- _BumpScale: 1
|
||||
- _ClearCoatMask: 0
|
||||
- _ClearCoatSmoothness: 0
|
||||
- _Cull: 2
|
||||
- _Cutoff: 0.5
|
||||
- _DetailAlbedoMapScale: 1
|
||||
- _DetailNormalMapScale: 1
|
||||
- _DstBlend: 0
|
||||
- _DstBlendAlpha: 0
|
||||
- _EnvironmentReflections: 1
|
||||
- _GlossMapScale: 0
|
||||
- _Glossiness: 0
|
||||
- _GlossyReflections: 0
|
||||
- _Metallic: 0
|
||||
- _OcclusionStrength: 1
|
||||
- _Parallax: 0.005
|
||||
- _QueueOffset: 0
|
||||
- _ReceiveShadows: 1
|
||||
- _Smoothness: 0.5
|
||||
- _SmoothnessTextureChannel: 0
|
||||
- _SpecularHighlights: 1
|
||||
- _SrcBlend: 1
|
||||
- _SrcBlendAlpha: 1
|
||||
- _Surface: 0
|
||||
- _WorkflowMode: 1
|
||||
- _XRMotionVectorsPass: 1
|
||||
- _ZWrite: 1
|
||||
m_Colors:
|
||||
- _BaseColor: {r: 1, g: 1, b: 1, a: 1}
|
||||
- _Color: {r: 1, g: 1, b: 1, a: 1}
|
||||
- _EmissionColor: {r: 0, g: 0, b: 0, a: 1}
|
||||
- _SpecColor: {r: 0.19999996, g: 0.19999996, b: 0.19999996, a: 1}
|
||||
m_BuildTextureStacks: []
|
||||
m_AllowLocking: 1
|
||||
--- !u!114 &3355021883474362010
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 11
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 0}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: d0353a89b1f911e48b9e16bdc9f2e058, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier: Unity.RenderPipelines.Universal.Editor::UnityEditor.Rendering.Universal.AssetVersion
|
||||
version: 10
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b1208b4fab8c6c84c9655fcc3da69efe
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 2100000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -543,6 +543,96 @@ Transform:
|
||||
- {fileID: 8387096127486878932}
|
||||
m_Father: {fileID: 3285138630030188913}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
--- !u!1 &1276864559111187745
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
serializedVersion: 6
|
||||
m_Component:
|
||||
- component: {fileID: 7059114398337531242}
|
||||
- component: {fileID: 3316843276031498534}
|
||||
- component: {fileID: 195853059780659648}
|
||||
m_Layer: 0
|
||||
m_Name: WeaponGripAnchor
|
||||
m_TagString: EditorOnly
|
||||
m_Icon: {fileID: 0}
|
||||
m_NavMeshLayer: 0
|
||||
m_StaticEditorFlags: 0
|
||||
m_IsActive: 1
|
||||
--- !u!4 &7059114398337531242
|
||||
Transform:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 1276864559111187745}
|
||||
serializedVersion: 2
|
||||
m_LocalRotation: {x: 0, y: 0, z: -0.7071068, w: 0.7071068}
|
||||
m_LocalPosition: {x: 0, y: 0.06, z: 0.02}
|
||||
m_LocalScale: {x: 1.25, y: 1.25, z: 1.25}
|
||||
m_ConstrainProportionsScale: 0
|
||||
m_Children: []
|
||||
m_Father: {fileID: 7714500739160558408}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
--- !u!33 &3316843276031498534
|
||||
MeshFilter:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 1276864559111187745}
|
||||
m_Mesh: {fileID: 4300000, guid: 779990e366a12f94ebf06711efef57d3, type: 3}
|
||||
--- !u!23 &195853059780659648
|
||||
MeshRenderer:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 1276864559111187745}
|
||||
m_Enabled: 1
|
||||
m_CastShadows: 0
|
||||
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: b1208b4fab8c6c84c9655fcc3da69efe, 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 &1325769919043544559
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
@@ -833,7 +923,7 @@ Transform:
|
||||
- {fileID: 100450397525093210}
|
||||
- {fileID: 795319999662639885}
|
||||
- {fileID: 1063840860590240591}
|
||||
- {fileID: 136525282400229282}
|
||||
- {fileID: 1295007706386937831}
|
||||
m_Father: {fileID: 0}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
--- !u!114 &8290370249712404227
|
||||
@@ -2239,6 +2329,99 @@ Transform:
|
||||
- {fileID: 6528362243562002222}
|
||||
m_Father: {fileID: 2241067459902915878}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
--- !u!1 &4881940712018230872
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
serializedVersion: 6
|
||||
m_Component:
|
||||
- component: {fileID: 1295007706386937831}
|
||||
- component: {fileID: 1300809461631886519}
|
||||
m_Layer: 0
|
||||
m_Name: SM_Wep_Melee
|
||||
m_TagString: Untagged
|
||||
m_Icon: {fileID: 0}
|
||||
m_NavMeshLayer: 0
|
||||
m_StaticEditorFlags: 0
|
||||
m_IsActive: 1
|
||||
--- !u!4 &1295007706386937831
|
||||
Transform:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 4881940712018230872}
|
||||
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: 194292233036670670}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
--- !u!137 &1300809461631886519
|
||||
SkinnedMeshRenderer:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 4881940712018230872}
|
||||
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: 3
|
||||
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: 53abed4a039a2944498dc0d43258baf5, 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
|
||||
serializedVersion: 2
|
||||
m_Quality: 0
|
||||
m_UpdateWhenOffscreen: 0
|
||||
m_SkinnedMotionVectors: 1
|
||||
m_Mesh: {fileID: 4300000, guid: 0d76db2ce815146419fd52c745104909, type: 2}
|
||||
m_Bones:
|
||||
- {fileID: 7714500739160558408}
|
||||
m_BlendShapeWeights: []
|
||||
m_RootBone: {fileID: 7714500739160558408}
|
||||
m_AABB:
|
||||
m_Center: {x: 0.41444796, y: 0.059999853, z: 0.02000001}
|
||||
m_Extent: {x: 0.6914556, y: 0.07196632, z: 0.31731963}
|
||||
m_DirtyAABB: 0
|
||||
--- !u!1 &5369485156592443817
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
@@ -2949,99 +3132,6 @@ MeshRenderer:
|
||||
m_SortingOrder: 0
|
||||
m_MaskInteraction: 0
|
||||
m_AdditionalVertexStreams: {fileID: 0}
|
||||
--- !u!1 &7369922033552706299
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
serializedVersion: 6
|
||||
m_Component:
|
||||
- component: {fileID: 136525282400229282}
|
||||
- component: {fileID: 4540681531604194498}
|
||||
m_Layer: 0
|
||||
m_Name: SM_Wep_Melee
|
||||
m_TagString: Untagged
|
||||
m_Icon: {fileID: 0}
|
||||
m_NavMeshLayer: 0
|
||||
m_StaticEditorFlags: 0
|
||||
m_IsActive: 1
|
||||
--- !u!4 &136525282400229282
|
||||
Transform:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 7369922033552706299}
|
||||
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: 194292233036670670}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
--- !u!137 &4540681531604194498
|
||||
SkinnedMeshRenderer:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 7369922033552706299}
|
||||
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: 3
|
||||
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: 53abed4a039a2944498dc0d43258baf5, 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
|
||||
serializedVersion: 2
|
||||
m_Quality: 0
|
||||
m_UpdateWhenOffscreen: 0
|
||||
m_SkinnedMotionVectors: 1
|
||||
m_Mesh: {fileID: 4300000, guid: 0d76db2ce815146419fd52c745104909, type: 2}
|
||||
m_Bones:
|
||||
- {fileID: 7714500739160558408}
|
||||
m_BlendShapeWeights: []
|
||||
m_RootBone: {fileID: 7714500739160558408}
|
||||
m_AABB:
|
||||
m_Center: {x: 0.41444784, y: 0.060000006, z: 0.02000001}
|
||||
m_Extent: {x: 0.6914554, y: 0.071966305, z: 0.31731963}
|
||||
m_DirtyAABB: 0
|
||||
--- !u!1 &7432474647424217847
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
@@ -3488,6 +3578,7 @@ Transform:
|
||||
- {fileID: 133776596909499235}
|
||||
- {fileID: 8397794599406007036}
|
||||
- {fileID: 7135072282735691904}
|
||||
- {fileID: 7059114398337531242}
|
||||
m_Father: {fileID: 5022670406498921928}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
--- !u!1 &8130402508941367653
|
||||
|
||||
@@ -0,0 +1,153 @@
|
||||
#if UNITY_EDITOR
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using ProjectM.Simulation;
|
||||
using Unity.Collections;
|
||||
using Unity.Entities;
|
||||
using Unity.Mathematics;
|
||||
using Unity.NetCode;
|
||||
using Unity.Transforms;
|
||||
using UnityEngine;
|
||||
|
||||
namespace ProjectM.Client
|
||||
{
|
||||
/// <summary>
|
||||
/// 07-20 data-driven tuning: a MEASUREMENT probe for melee feel. In Play, spawns a huge-HP dummy in front of
|
||||
/// the local player, holds the (injected) Attack for a scripted chain, and samples the SERVER truth every
|
||||
/// editor frame: swing-start ticks, scheduled contact ticks, actual damage ticks + amounts. The report is
|
||||
/// MEASURED cadence / contact latency / DPS / buffer usage — objective numbers to tune against instead of
|
||||
/// eyeballs, valid under real tick-batching. Editor-only diagnostics (cross-world reads); drive from the
|
||||
/// DebugOverlay button or execute_code: <c>ProjectM.Client.CombatProbe.Run(6);</c>, read the console.
|
||||
/// </summary>
|
||||
public static class CombatProbe
|
||||
{
|
||||
public static bool Running { get; private set; }
|
||||
|
||||
struct SwingRec { public uint Start; public uint Scheduled; public uint DamageTickObserved; public float Damage; }
|
||||
|
||||
public static void Run(int swings = 6, float aimX = 1f, float aimZ = 0f)
|
||||
{
|
||||
if (!Application.isPlaying) { Debug.LogWarning("[CombatProbe] Play mode only."); return; }
|
||||
if (Running) { Debug.LogWarning("[CombatProbe] Already running."); return; }
|
||||
|
||||
World server = null;
|
||||
foreach (var w in World.All) if (w.Name == "ServerWorld") server = w;
|
||||
if (server == null) { Debug.LogWarning("[CombatProbe] No ServerWorld."); return; }
|
||||
var sem = server.EntityManager;
|
||||
|
||||
// The local player's server twin (the entity that carries the scheduled-cleave slot).
|
||||
var pq = sem.CreateEntityQuery(typeof(MeleeCleavePending), typeof(MeleeCombo), typeof(LocalTransform));
|
||||
var players = pq.ToEntityArray(Allocator.Temp);
|
||||
if (players.Length == 0) { players.Dispose(); Debug.LogWarning("[CombatProbe] No server player."); return; }
|
||||
var player = players[0];
|
||||
players.Dispose();
|
||||
var playerPos = sem.GetComponentData<LocalTransform>(player).Position;
|
||||
|
||||
// Dummy: any enemy prefab, parked inside the cone, HP no swing chain can finish.
|
||||
var prefQ = sem.CreateEntityQuery(new EntityQueryDesc
|
||||
{
|
||||
All = new ComponentType[] { typeof(EnemyTag), typeof(Prefab) },
|
||||
Options = EntityQueryOptions.IncludePrefab,
|
||||
});
|
||||
var prefabs = prefQ.ToEntityArray(Allocator.Temp);
|
||||
if (prefabs.Length == 0) { prefabs.Dispose(); Debug.LogWarning("[CombatProbe] No enemy prefab."); return; }
|
||||
var dummy = sem.Instantiate(prefabs[0]);
|
||||
prefabs.Dispose();
|
||||
var baked = sem.GetComponentData<LocalTransform>(dummy);
|
||||
var aim = math.normalizesafe(new float2(aimX, aimZ), new float2(1f, 0f));
|
||||
sem.SetComponentData(dummy, baked.WithPosition(playerPos + new float3(aim.x, 0f, aim.y) * 1.6f));
|
||||
sem.SetComponentData(dummy, new Health { Current = 100000f, Max = 100000f });
|
||||
|
||||
Running = true;
|
||||
int frame = 0;
|
||||
var recs = new List<SwingRec>();
|
||||
uint lastSwing = 0; float lastHp = 100000f; int buffered = 0; uint firstTick = 0, lastTick = 0;
|
||||
UnityEditor.EditorApplication.CallbackFunction cb = null;
|
||||
cb = () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
frame++;
|
||||
World sw = null;
|
||||
foreach (var w2 in World.All) if (w2.Name == "ServerWorld") sw = w2;
|
||||
if (sw == null) { Finish("server world gone"); return; }
|
||||
var em = sw.EntityManager;
|
||||
if (!em.Exists(player) || !em.Exists(dummy)) { Finish("probe entity despawned"); return; }
|
||||
|
||||
uint tick = 0;
|
||||
var ntq = em.CreateEntityQuery(typeof(NetworkTime));
|
||||
if (!ntq.IsEmpty) { var nt = ntq.GetSingleton<NetworkTime>(); if (nt.ServerTick.IsValid) tick = nt.ServerTick.TickIndexForValidTick; }
|
||||
if (firstTick == 0) firstTick = tick;
|
||||
lastTick = tick;
|
||||
|
||||
var mc = em.GetComponentData<MeleeCombo>(player);
|
||||
var pend = em.GetComponentData<MeleeCleavePending>(player);
|
||||
float hp = em.GetComponentData<Health>(dummy).Current;
|
||||
|
||||
if (mc.SwingStartTick != 0 && mc.SwingStartTick != lastSwing)
|
||||
{
|
||||
recs.Add(new SwingRec { Start = mc.SwingStartTick, Scheduled = pend.ResolveTick });
|
||||
lastSwing = mc.SwingStartTick;
|
||||
}
|
||||
else if (recs.Count > 0 && pend.ResolveTick != 0)
|
||||
{
|
||||
var r0 = recs[recs.Count - 1];
|
||||
if (r0.Scheduled == 0) { r0.Scheduled = pend.ResolveTick; recs[recs.Count - 1] = r0; }
|
||||
}
|
||||
if (hp < lastHp && recs.Count > 0)
|
||||
{
|
||||
var r1 = recs[recs.Count - 1];
|
||||
if (r1.DamageTickObserved == 0) { r1.DamageTickObserved = tick; r1.Damage = lastHp - hp; recs[recs.Count - 1] = r1; }
|
||||
}
|
||||
if (mc.BufferedAttackTick != 0) buffered++; // frames a buffer was armed (usage signal)
|
||||
lastHp = hp;
|
||||
|
||||
// Drive: hold the attack (re-issued) until the target swing count is reached — a mashing player.
|
||||
if (recs.Count < swings) { DebugInputInjectionSystem.SetAim(aim.x, aim.y); DebugInputInjectionSystem.Attack(3); }
|
||||
|
||||
bool lastResolved = recs.Count == 0 || recs[recs.Count - 1].DamageTickObserved != 0; // wait for the FINAL contact (a whiff falls through to the timeout)
|
||||
if ((recs.Count >= swings && lastResolved) || frame > 900) Finish(null);
|
||||
}
|
||||
catch (System.Exception ex) { Finish("exception: " + ex.Message); }
|
||||
};
|
||||
UnityEditor.EditorApplication.update += cb;
|
||||
Debug.Log($"[CombatProbe] Running: {swings} swings vs a 100k-HP dummy at +{1.6f:F1}m (aim {aim.x:F1},{aim.y:F1}).");
|
||||
|
||||
void Finish(string abort)
|
||||
{
|
||||
UnityEditor.EditorApplication.update -= cb;
|
||||
DebugInputInjectionSystem.Stop();
|
||||
Running = false;
|
||||
World sw2 = null;
|
||||
foreach (var w3 in World.All) if (w3.Name == "ServerWorld") sw2 = w3;
|
||||
if (sw2 != null && sw2.EntityManager.Exists(dummy)) sw2.EntityManager.DestroyEntity(dummy);
|
||||
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendLine("==== COMBAT PROBE REPORT (server-measured) ====");
|
||||
if (abort != null) sb.AppendLine(" ABORTED: " + abort);
|
||||
float totalDmg = 0f;
|
||||
for (int i = 0; i < recs.Count; i++)
|
||||
{
|
||||
var r = recs[i];
|
||||
long contact = r.Scheduled != 0 ? (long)r.Scheduled - r.Start : -1;
|
||||
long observed = r.DamageTickObserved != 0 ? (long)r.DamageTickObserved - r.Start : -1;
|
||||
long gap = i > 0 ? (long)r.Start - recs[i - 1].Start : 0;
|
||||
totalDmg += r.Damage;
|
||||
sb.AppendLine($" swing {i + 1}: start {r.Start}"
|
||||
+ (i > 0 ? $" (gap {gap}t = {gap / 60f:F2}s)" : "")
|
||||
+ $" | scheduled contact +{contact}t | damage {r.Damage:F0} observed +{observed}t (frame-sampled)");
|
||||
}
|
||||
if (recs.Count > 1)
|
||||
{
|
||||
float span = (recs[recs.Count - 1].Start - recs[0].Start) / 60f;
|
||||
sb.AppendLine($" MEASURED cadence: {span / (recs.Count - 1):F2}s/swing over {recs.Count} swings");
|
||||
}
|
||||
float dur = (lastTick - firstTick) / 60f;
|
||||
if (dur > 0.5f) sb.AppendLine($" MEASURED DPS vs dummy: {totalDmg / dur:F1} over {dur:F1}s (total {totalDmg:F0})");
|
||||
sb.AppendLine($" buffer armed on {buffered} sampled frames (mashing exercises the unlock-edge path)");
|
||||
Debug.Log(sb.ToString());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c20d0a512cd41f64191bc2457e059350
|
||||
@@ -117,6 +117,20 @@ namespace ProjectM.Client
|
||||
TuningRow("Cast turn deg", TuningKnob.CastTurnRateDeg, 60f, "0");
|
||||
TuningRow("Move sharp", TuningKnob.MoveSharpness, 0.5f, "0.0");
|
||||
|
||||
// 07-20 data-driven tuning: whole-gestalt FEEL PROFILES (sim knobs + FeelConfig in one JSON) --
|
||||
// A/B a feel with one click instead of dialing 30 scalars; Save captures the hand-dialed state.
|
||||
GUILayout.Space(6);
|
||||
GUILayout.Label("- Feel Profiles -");
|
||||
foreach (var prof in FeelProfileService.ListProfiles())
|
||||
if (GUILayout.Button(System.IO.Path.GetFileNameWithoutExtension(prof)))
|
||||
FeelProfileService.Apply(prof);
|
||||
GUILayout.BeginHorizontal();
|
||||
if (GUILayout.Button("Save current")) FeelProfileService.SaveCurrent();
|
||||
if (GUILayout.Button("Rescan")) FeelProfileService.Rescan();
|
||||
GUILayout.EndHorizontal();
|
||||
if (GUILayout.Button(CombatProbe.Running ? "Probe running..." : "Combat probe (6 swings)") && !CombatProbe.Running)
|
||||
CombatProbe.Run(6); // measured cadence/contact/DPS -> console
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,154 @@
|
||||
#if UNITY_EDITOR
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Reflection;
|
||||
using ProjectM.Simulation;
|
||||
using UnityEngine;
|
||||
|
||||
namespace ProjectM.Client
|
||||
{
|
||||
/// <summary>
|
||||
/// 07-20 data-driven tuning: a FEEL PROFILE is one JSON file holding a whole gestalt — sim knobs
|
||||
/// (<see cref="TuningKnob"/> names → values, applied through the authoritative SetTuning path exactly like an
|
||||
/// overlay nudge) + client feel statics (<see cref="FeelConfig"/> field names → values, set via reflection).
|
||||
/// Profiles are PARTIAL: only the keys a profile lists are touched, so a profile can be a 4-line experiment.
|
||||
/// A/B whole feels with one overlay click instead of twiddling 30 scalars; "save current" captures whatever
|
||||
/// was hand-dialed. EDITOR-ONLY (same family as DebugOverlay / DebugInputInjectionSystem); profiles live in
|
||||
/// Assets/_Project/Tuning/Profiles/*.json. When a profile wins, fold it into TuningConfig.Defaults() /
|
||||
/// FeelConfig.ResetDefaults() (code stays the single shipped truth; profiles are experiments).
|
||||
/// </summary>
|
||||
public static class FeelProfileService
|
||||
{
|
||||
public const string ProfileDir = "Assets/_Project/Tuning/Profiles";
|
||||
|
||||
[Serializable] public class Entry { public string k; public string v; }
|
||||
|
||||
[Serializable]
|
||||
public class ProfileData
|
||||
{
|
||||
public string name;
|
||||
public string notes;
|
||||
public List<Entry> sim = new List<Entry>();
|
||||
public List<Entry> feel = new List<Entry>();
|
||||
}
|
||||
|
||||
static string[] _cachedPaths;
|
||||
|
||||
/// <summary>Profile file paths (cached; <see cref="Rescan"/> after adding files).</summary>
|
||||
public static string[] ListProfiles()
|
||||
{
|
||||
if (_cachedPaths == null) Rescan();
|
||||
return _cachedPaths;
|
||||
}
|
||||
|
||||
public static void Rescan()
|
||||
{
|
||||
_cachedPaths = Directory.Exists(ProfileDir)
|
||||
? Directory.GetFiles(ProfileDir, "*.json")
|
||||
: Array.Empty<string>();
|
||||
Array.Sort(_cachedPaths);
|
||||
}
|
||||
|
||||
/// <summary>Apply a profile: sim knobs ride the authoritative SetTuning RPC + the optimistic local copy
|
||||
/// (identical to an overlay nudge; server clamps); feel fields are set by name. Play mode only for sim.</summary>
|
||||
public static void Apply(string path)
|
||||
{
|
||||
var data = Load(path);
|
||||
if (data == null) return;
|
||||
int simApplied = 0, feelApplied = 0;
|
||||
var knobByName = KnobByName();
|
||||
foreach (var e in data.sim)
|
||||
{
|
||||
if (!knobByName.TryGetValue(e.k, out byte knob))
|
||||
{ Debug.LogWarning($"[FeelProfile] Unknown sim knob '{e.k}' — skipped."); continue; }
|
||||
if (!float.TryParse(e.v, NumberStyles.Float, CultureInfo.InvariantCulture, out float val))
|
||||
{ Debug.LogWarning($"[FeelProfile] Bad value for {e.k}: '{e.v}'"); continue; }
|
||||
if (Application.isPlaying)
|
||||
{
|
||||
DebugCommandSendSystem.SetTuning(knob, val); // authoritative (server applies + broadcasts; clamped)
|
||||
TuningReadout.SetLocal(knob, val); // optimistic local (instant feel; clamped)
|
||||
}
|
||||
simApplied++;
|
||||
}
|
||||
foreach (var e in data.feel)
|
||||
{
|
||||
var f = typeof(FeelConfig).GetField(e.k, BindingFlags.Public | BindingFlags.Static);
|
||||
if (f == null) { Debug.LogWarning($"[FeelProfile] Unknown FeelConfig field '{e.k}' — skipped."); continue; }
|
||||
try { f.SetValue(null, ParseFeel(f.FieldType, e.v)); feelApplied++; }
|
||||
catch (Exception ex) { Debug.LogWarning($"[FeelProfile] {e.k}: {ex.Message}"); }
|
||||
}
|
||||
string simNote = Application.isPlaying ? simApplied.ToString() : simApplied + " (LISTED ONLY — sim knobs need Play mode)";
|
||||
Debug.Log($"[FeelProfile] Applied '{data.name}' ({Path.GetFileName(path)}): sim {simNote}, feel {feelApplied}. {data.notes}");
|
||||
}
|
||||
|
||||
/// <summary>Snapshot the CURRENT tuning (live knob readout + every FeelConfig field) as a new profile.
|
||||
/// Outside Play the feel statics are re-stamped to defaults first (they are zero after a domain reload).</summary>
|
||||
public static string SaveCurrent(string profileName = null)
|
||||
{
|
||||
if (!Application.isPlaying) FeelConfig.ResetDefaults();
|
||||
var data = new ProfileData
|
||||
{
|
||||
name = string.IsNullOrEmpty(profileName) ? "captured-" + DateTime.Now.ToString("MMdd-HHmm") : profileName,
|
||||
notes = "Captured from the live session " + DateTime.Now.ToString("yyyy-MM-dd HH:mm"),
|
||||
};
|
||||
foreach (var kv in KnobByName())
|
||||
data.sim.Add(new Entry { k = kv.Key, v = TuningConfig.Get(TuningReadout.Current, kv.Value).ToString(CultureInfo.InvariantCulture) });
|
||||
foreach (var f in typeof(FeelConfig).GetFields(BindingFlags.Public | BindingFlags.Static))
|
||||
data.feel.Add(new Entry { k = f.Name, v = FormatFeel(f.GetValue(null)) });
|
||||
Directory.CreateDirectory(ProfileDir);
|
||||
string path = Path.Combine(ProfileDir, data.name + ".json");
|
||||
File.WriteAllText(path, JsonUtility.ToJson(data, prettyPrint: true));
|
||||
UnityEditor.AssetDatabase.Refresh();
|
||||
Rescan();
|
||||
Debug.Log($"[FeelProfile] Saved current tuning to {path} ({data.sim.Count} sim, {data.feel.Count} feel).");
|
||||
return path;
|
||||
}
|
||||
|
||||
static ProfileData Load(string path)
|
||||
{
|
||||
try { return JsonUtility.FromJson<ProfileData>(File.ReadAllText(path)); }
|
||||
catch (Exception ex) { Debug.LogError($"[FeelProfile] Failed to read {path}: {ex.Message}"); return null; }
|
||||
}
|
||||
|
||||
static Dictionary<string, byte> KnobByName()
|
||||
{
|
||||
var map = new Dictionary<string, byte>();
|
||||
foreach (var f in typeof(TuningKnob).GetFields(BindingFlags.Public | BindingFlags.Static))
|
||||
if (f.IsLiteral && f.FieldType == typeof(byte) && f.Name != "Count")
|
||||
map[f.Name] = (byte)f.GetRawConstantValue();
|
||||
return map;
|
||||
}
|
||||
|
||||
static object ParseFeel(Type t, string v)
|
||||
{
|
||||
if (t == typeof(float)) return float.Parse(v, CultureInfo.InvariantCulture);
|
||||
if (t == typeof(int)) return int.Parse(v, CultureInfo.InvariantCulture);
|
||||
if (t == typeof(bool)) return bool.Parse(v);
|
||||
if (t == typeof(Color))
|
||||
{
|
||||
var p = v.Split(',');
|
||||
return new Color(
|
||||
float.Parse(p[0], CultureInfo.InvariantCulture), float.Parse(p[1], CultureInfo.InvariantCulture),
|
||||
float.Parse(p[2], CultureInfo.InvariantCulture), p.Length > 3 ? float.Parse(p[3], CultureInfo.InvariantCulture) : 1f);
|
||||
}
|
||||
throw new NotSupportedException($"unsupported field type {t.Name}");
|
||||
}
|
||||
|
||||
static string FormatFeel(object val)
|
||||
{
|
||||
switch (val)
|
||||
{
|
||||
case float f: return f.ToString(CultureInfo.InvariantCulture);
|
||||
case int i: return i.ToString(CultureInfo.InvariantCulture);
|
||||
case bool b: return b.ToString();
|
||||
case Color c: return string.Join(",",
|
||||
c.r.ToString(CultureInfo.InvariantCulture), c.g.ToString(CultureInfo.InvariantCulture),
|
||||
c.b.ToString(CultureInfo.InvariantCulture), c.a.ToString(CultureInfo.InvariantCulture));
|
||||
default: return val?.ToString() ?? "";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 696cc12527a12a74fbc5758a3e949bd5
|
||||
@@ -267,7 +267,7 @@ namespace ProjectM.Client
|
||||
|
||||
// Combat feel pass (2026-06)
|
||||
HitFlashBurstCount = 16;
|
||||
MeleeConnectFovKick = 0.8f;
|
||||
MeleeConnectFovKick = 1.1f; // 07-21 HEAVY LOCK: meatier bite
|
||||
MeleeConnectVolume = 0.55f;
|
||||
KillFlashBurstCount = 20;
|
||||
FootstepVolume = 0.30f; // 07-15: heavier, more present underwater thud
|
||||
@@ -287,9 +287,9 @@ namespace ProjectM.Client
|
||||
ShoulderLampIntensity = 8f; // 07-16e: the suit lamp actually lights (3 got lost in the murk ambient)
|
||||
ShoulderLampRange = 12f;
|
||||
CombatIdleHoldSec = 4f; // 07-18: Menacing01 combat-idle hold after the last swing
|
||||
MeleeArcIntensity = 1f; // 07-19: arc colors are pre-toned dim; this scales from there
|
||||
MeleeArcIntensity = 0.8f; // 07-21 HEAVY LOCK: the weapon is the read, the arc recedes further
|
||||
MeleeArcBubbles = 6;
|
||||
FinisherHoldFrames = 5; // 07-20 G5: the finisher's heavy beat (~83ms), capped well under the 8-frame ladder ceiling
|
||||
FinisherHoldFrames = 7; // 07-21 HEAVY LOCK: a fatter finisher beat (~117ms, still under the 8-frame ladder cap)
|
||||
|
||||
|
||||
RumbleEnabled = true;
|
||||
|
||||
@@ -674,9 +674,9 @@ namespace ProjectM.EditorTools
|
||||
// window. Hit-start seconds measured off the pack takes (= the WindUp take length).
|
||||
var swaps = new (string state, string fbxSub, string clip, float speed)[]
|
||||
{
|
||||
("Swing1", "Attack/LightCombo01/A_Attack_LightCombo01A_Sword", "A_Attack_LightCombo01A_Sword", 1.25f), // hit @0.33s -> ~0.27s (07-19 retime: 26-tick window)
|
||||
("Swing2", "Attack/LightCombo01/A_Attack_LightCombo01B_Sword", "A_Attack_LightCombo01B_Sword", 1.0f), // hit @0.17s natural speed
|
||||
("Swing3", "Attack/LightCombo01/A_Attack_LightCombo01C_Sword", "A_Attack_LightCombo01C_Sword", 1.1f), // finisher: hit @0.33s, heaviest of the chain
|
||||
("Swing1", "Attack/LightCombo01/A_Attack_LightCombo01A_Sword", "A_Attack_LightCombo01A_Sword", 1.0f), // 07-21 heavy lock: TRUE contact 0.333s = 20t (matches MeleeContactTicks 20)
|
||||
("Swing2", "Attack/LightCombo01/A_Attack_LightCombo01B_Sword", "A_Attack_LightCombo01B_Sword", 0.85f), // TRUE contact 0.196s = 12t (knob step2 = round(20*0.625) = 12)
|
||||
("Swing3", "Attack/LightCombo01/A_Attack_LightCombo01C_Sword", "A_Attack_LightCombo01C_Sword", 0.88f), // finisher: TRUE contact 0.417s = 25t (knob step3 = 25; 1 tick under CastFacingTicks)
|
||||
("SpecialSlam", "Attack/HeavyCombo01/A_Attack_HeavyCombo01A_Sword", "A_Attack_HeavyCombo01A_Slam_Sword", 1.5f), // custom take (skips ~2/3 of the windup); hit ~0.35s
|
||||
("Death", "Death/A_Death_B_01_Sword", "A_Death_B_01_Sword", 1.0f), // backward collapse, holds last frame
|
||||
};
|
||||
@@ -784,7 +784,22 @@ namespace ProjectM.EditorTools
|
||||
// Rigid rest matrices (GraftSmr conventions: scale-stripped rest, feet-at-0 prefab space).
|
||||
var hm = hand.localToWorldMatrix;
|
||||
var handRigid = Matrix4x4.TRS((Vector3)hm.GetColumn(3), hm.rotation, Vector3.one);
|
||||
var grip = Matrix4x4.TRS(WeaponGripPos, Quaternion.Euler(WeaponGripEuler), Vector3.one * WeaponScale); // uniform scale: normals keep their directions
|
||||
// 07-20 designer flow: the WeaponGripAnchor child of Hand_R (EditorOnly, carries a live preview of
|
||||
// the weapon mesh) is the AUTHORING source when present -- move/rotate/scale it with normal gizmos
|
||||
// in the prefab stage, then re-run this tool to bake. The consts are the fallback/seed only.
|
||||
Vector3 gripPos = WeaponGripPos;
|
||||
Quaternion gripRot = Quaternion.Euler(WeaponGripEuler);
|
||||
float gripScale = WeaponScale;
|
||||
var gripAnchor = hand.Find("WeaponGripAnchor");
|
||||
if (gripAnchor != null)
|
||||
{
|
||||
gripPos = gripAnchor.localPosition;
|
||||
gripRot = gripAnchor.localRotation;
|
||||
gripScale = gripAnchor.localScale.x; // uniform: gizmo-scaled non-uniformly -> x wins
|
||||
if (Mathf.Abs(gripAnchor.localScale.y - gripScale) > 1e-3f || Mathf.Abs(gripAnchor.localScale.z - gripScale) > 1e-3f)
|
||||
Debug.LogWarning("[PlayerRigTools] WeaponGripAnchor scale is non-uniform; using X for the rigid bake.");
|
||||
}
|
||||
var grip = Matrix4x4.TRS(gripPos, gripRot, Vector3.one * gripScale); // uniform scale: normals keep their directions
|
||||
var toWorld = handRigid * grip;
|
||||
|
||||
var verts = srcMesh.vertices;
|
||||
@@ -838,8 +853,79 @@ namespace ProjectM.EditorTools
|
||||
}
|
||||
finally { PrefabUtility.UnloadPrefabContents(root); }
|
||||
AssetDatabase.SaveAssets();
|
||||
Debug.Log($"[PlayerRigTools] Melee weapon attached to Hand_R ({srcMesh.vertexCount}v, grip pos {WeaponGripPos} euler {WeaponGripEuler}).");
|
||||
Debug.Log($"[PlayerRigTools] Melee weapon attached to Hand_R ({srcMesh.vertexCount}v). Grip source: WeaponGripAnchor if present (gizmo-authored), else the consts. Re-run after moving the anchor.");
|
||||
}
|
||||
|
||||
/// <summary>07-20 designer grip flow: create (or refresh the preview on) the WeaponGripAnchor under
|
||||
/// Hand_R in Player.prefab. The anchor CARRIES the actual weapon mesh as an edit-time preview (plain
|
||||
/// MeshRenderer + a URP/Lit material with the pack atlas), so what you grab with the move/rotate/scale
|
||||
/// gizmos in the prefab stage IS the axe at true size, live. Tagged <b>EditorOnly</b> -- the Entities
|
||||
/// baker skips it, so it can never leak an un-animated duplicate into the game; the baked rigid-skinned
|
||||
/// SMR stays the only runtime weapon. Until you re-run Attach Melee Weapon you'll see BOTH (preview at
|
||||
/// the new grip, baked axe at the old) -- a free before/after ghost; when they coincide, you're baked.
|
||||
/// Seeds from the consts ONLY on first creation; re-runs never stomp a hand-tuned anchor.</summary>
|
||||
[MenuItem("ProjectM/Animation/Player - Create Weapon Grip Anchor (LANTERN)")]
|
||||
public static void CreateWeaponGripAnchor()
|
||||
{
|
||||
const string weaponFbx = "Assets/Synty/PolygonDungeonRealms/Models/SM_Wep_Axe_Large_01.fbx";
|
||||
const string atlasPath = "Assets/Synty/PolygonDungeonRealms/Textures/Dungeons_2_Texture_01_A.png";
|
||||
const string previewMatPath = "Assets/_Project/Materials/M_Weapon_Preview.mat";
|
||||
const string output = "Assets/_Project/Prefabs/Player.prefab";
|
||||
|
||||
var src = AssetDatabase.LoadAssetAtPath<GameObject>(weaponFbx);
|
||||
var srcMf = src != null ? src.GetComponentInChildren<MeshFilter>(true) : null;
|
||||
if (srcMf == null || srcMf.sharedMesh == null) { Debug.LogError($"[PlayerRigTools] Weapon fbx/mesh missing: {weaponFbx}"); return; }
|
||||
|
||||
// Plain URP/Lit preview material (the runtime deformation shader won't render on a static MeshRenderer).
|
||||
var mat = AssetDatabase.LoadAssetAtPath<Material>(previewMatPath);
|
||||
if (mat == null)
|
||||
{
|
||||
var lit = Shader.Find("Universal Render Pipeline/Lit");
|
||||
if (lit == null) { Debug.LogError("[PlayerRigTools] URP/Lit shader not found."); return; }
|
||||
mat = new Material(lit) { name = "M_Weapon_Preview" };
|
||||
var atlas = AssetDatabase.LoadAssetAtPath<Texture>(atlasPath);
|
||||
if (atlas != null && mat.HasProperty("_BaseMap")) mat.SetTexture("_BaseMap", atlas);
|
||||
AssetDatabase.CreateAsset(mat, previewMatPath);
|
||||
}
|
||||
|
||||
var root = PrefabUtility.LoadPrefabContents(output);
|
||||
try
|
||||
{
|
||||
Transform hand = null;
|
||||
foreach (var t in root.GetComponentsInChildren<Transform>(true)) if (t.name == "Hand_R") { hand = t; break; }
|
||||
if (hand == null) { Debug.LogError("[PlayerRigTools] Hand_R not found on Player.prefab."); return; }
|
||||
|
||||
var anchor = hand.Find("WeaponGripAnchor");
|
||||
bool fresh = anchor == null;
|
||||
if (fresh)
|
||||
{
|
||||
var go = new GameObject("WeaponGripAnchor");
|
||||
anchor = go.transform;
|
||||
anchor.SetParent(hand, false);
|
||||
// Seed exactly where the current bake sits, so the preview and the baked axe start coincident.
|
||||
anchor.localPosition = WeaponGripPos;
|
||||
anchor.localRotation = Quaternion.Euler(WeaponGripEuler);
|
||||
anchor.localScale = Vector3.one * WeaponScale;
|
||||
}
|
||||
anchor.gameObject.tag = "EditorOnly"; // the Entities baker SKIPS EditorOnly -- preview never reaches runtime
|
||||
|
||||
var mf = anchor.GetComponent<MeshFilter>();
|
||||
if (mf == null) mf = anchor.gameObject.AddComponent<MeshFilter>();
|
||||
mf.sharedMesh = srcMf.sharedMesh;
|
||||
var mr = anchor.GetComponent<MeshRenderer>();
|
||||
if (mr == null) mr = anchor.gameObject.AddComponent<MeshRenderer>();
|
||||
mr.sharedMaterial = mat;
|
||||
mr.shadowCastingMode = UnityEngine.Rendering.ShadowCastingMode.Off;
|
||||
|
||||
PrefabUtility.SaveAsPrefabAsset(root, output);
|
||||
}
|
||||
finally { PrefabUtility.UnloadPrefabContents(root); }
|
||||
AssetDatabase.SaveAssets();
|
||||
Debug.Log("[PlayerRigTools] WeaponGripAnchor ready under Hand_R (EditorOnly, live axe preview). "
|
||||
+ "Open Player.prefab, search 'WeaponGripAnchor', move/rotate/scale it with normal gizmos, then run "
|
||||
+ "'Player - Attach Melee Weapon' to bake. Preview + baked axe overlap = you're in sync.");
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
using System.Text;
|
||||
using ProjectM.Simulation;
|
||||
using UnityEditor;
|
||||
using UnityEditor.Animations;
|
||||
using UnityEngine;
|
||||
|
||||
namespace ProjectM.EditorTools
|
||||
{
|
||||
/// <summary>
|
||||
/// 07-20 data-driven tuning: derive the melee timing TRUTH from the assets themselves instead of hand-copied
|
||||
/// constants — clip WindUp lengths / wired state speeds → the real per-step contact seconds/ticks, compared
|
||||
/// against what MeleeTiming + the MeleeContactTicks knob actually produce; the coupling invariants the design
|
||||
/// review flagged as un-clamped (contact < recover, contact ≤ CastFacingTicks); cadence/DPS math; and the
|
||||
/// weapon-vs-cone reach-honesty ratio measured off the BAKED weapon mesh. Run after any clip re-speed,
|
||||
/// wire-tool re-run, or knob-default change — drift shows up here before it shows up as jank.
|
||||
/// </summary>
|
||||
public static class TuningAuditTools
|
||||
{
|
||||
const string Controller = "Assets/_Project/Animation/AC_PlayerTopDown.controller";
|
||||
const string PackDir = "Assets/Synty/AnimationSwordCombat/Animations/Polygon/";
|
||||
const string WeaponMesh = "Assets/_Project/Art/Models/Rebased_SM_Wep_Melee.asset";
|
||||
|
||||
[MenuItem("ProjectM/Tuning/Audit Melee Timing + Reach")]
|
||||
public static void AuditMeleeTimingMenu() => AuditMeleeTiming();
|
||||
|
||||
public static string AuditMeleeTiming()
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendLine("==== MELEE TUNING AUDIT (derived from assets vs configured) ====");
|
||||
var t = TuningConfig.Defaults();
|
||||
float contactKnob = t.MeleeContactTicks;
|
||||
uint recover = (uint)Mathf.Max(1f, t.MeleeRecoverTicks);
|
||||
uint finRecover = (uint)Mathf.Max(1f, Mathf.Round(t.MeleeRecoverTicks * Mathf.Max(1f, t.MeleeFinisherMult)));
|
||||
|
||||
var ac = AssetDatabase.LoadAssetAtPath<AnimatorController>(Controller);
|
||||
if (ac == null) { Debug.LogError("[TuningAudit] Controller missing."); return "controller missing"; }
|
||||
var sm = ac.layers[0].stateMachine;
|
||||
|
||||
// (state, windup-take fbx, windup clip name, combo step, lock that gates this step's chain)
|
||||
var rows = new (string state, string fbx, string clip, byte step, uint stepRecover)[]
|
||||
{
|
||||
("Swing1", "Attack/LightCombo01/A_Attack_LightCombo01A_Sword", "A_Attack_LightCombo01A_WindUp_Sword", 1, recover),
|
||||
("Swing2", "Attack/LightCombo01/A_Attack_LightCombo01B_Sword", "A_Attack_LightCombo01B_WindUp_Sword", 2, recover),
|
||||
("Swing3", "Attack/LightCombo01/A_Attack_LightCombo01C_Sword", "A_Attack_LightCombo01C_WindUp_Sword", 3, finRecover),
|
||||
};
|
||||
bool anyDrift = false;
|
||||
foreach (var r in rows)
|
||||
{
|
||||
AnimatorState st = null;
|
||||
foreach (var cs in sm.states) if (cs.state.name == r.state) st = cs.state;
|
||||
if (st == null) { sb.AppendLine($" {r.state}: STATE MISSING"); continue; }
|
||||
AnimationClip windup = null;
|
||||
foreach (var o in AssetDatabase.LoadAllAssetsAtPath(PackDir + r.fbx + ".fbx"))
|
||||
if (o is AnimationClip c && c.name == r.clip) windup = c;
|
||||
if (windup == null) { sb.AppendLine($" {r.state}: windup take '{r.clip}' MISSING"); continue; }
|
||||
|
||||
float speed = Mathf.Max(0.01f, st.speed);
|
||||
float derivedSec = windup.length / speed; // the blade lands when the WindUp take ends
|
||||
uint derivedTicks = (uint)Mathf.Round(derivedSec * 60f);
|
||||
uint configured = MeleeTiming.ContactTicks(r.step, contactKnob);
|
||||
bool drift = derivedTicks != configured;
|
||||
anyDrift |= drift;
|
||||
sb.AppendLine($" {r.state}: clip windup {windup.length:F3}s @ speed {speed:F2} -> TRUE contact {derivedSec:F3}s = {derivedTicks}t | configured {configured}t {(drift ? " << DRIFT" : " OK")}");
|
||||
if (configured >= r.stepRecover) sb.AppendLine($" !! contact {configured}t >= step lock {r.stepRecover}t (flush-at-overwrite will fire it EARLY every chain)");
|
||||
if (configured > PlayerAimSystem.CastFacingTicks) sb.AppendLine($" !! contact {configured}t > CastFacingTicks {PlayerAimSystem.CastFacingTicks} (aim-steer + anim pulse end BEFORE the blade lands)");
|
||||
}
|
||||
if (anyDrift) sb.AppendLine(" -> DRIFT fix: retune MeleeContactTicks (base = Swing1's true ticks) and/or MeleeTiming's step fractions.");
|
||||
|
||||
// Cadence / DPS at the configured knobs (chain = light, light, finisher).
|
||||
float dmg = t.MeleeDamage;
|
||||
float chainSec = (2f * recover + finRecover) / 60f;
|
||||
float chainDmg = 2f * dmg + dmg * Mathf.Max(1f, t.MeleeFinisherMult);
|
||||
sb.AppendLine($" Cadence: light lock {recover}t ({recover / 60f:F2}s), finisher lock {finRecover}t ({finRecover / 60f:F2}s); full chain {chainSec:F2}s");
|
||||
sb.AppendLine($" DPS: per-light {dmg / (recover / 60f):F1} | full-chain {chainDmg / chainSec:F1} (dmg {dmg}, finisher x{t.MeleeFinisherMult})");
|
||||
sb.AppendLine($" Buffer: {t.MeleeBufferTicks}t window, unlock slack {MeleeTiming.BatchSlackTicks}t (batch-proof)");
|
||||
|
||||
// Reach honesty: the BAKED weapon mesh's max planar radius (rest pose, feet-at-0 space) vs the cone.
|
||||
var mesh = AssetDatabase.LoadAssetAtPath<Mesh>(WeaponMesh);
|
||||
if (mesh != null)
|
||||
{
|
||||
float maxR = 0f;
|
||||
var verts = mesh.vertices;
|
||||
for (int i = 0; i < verts.Length; i++)
|
||||
{
|
||||
float rr = new Vector2(verts[i].x, verts[i].z).magnitude;
|
||||
if (rr > maxR) maxR = rr;
|
||||
}
|
||||
float swingReach = maxR + 0.45f; // rest radius + typical arm extension at full swing (approx)
|
||||
float range = t.MeleeRange;
|
||||
float finRange = range * Mathf.Max(0.01f, t.MeleeFinisherRangeMult);
|
||||
sb.AppendLine($" Reach: weapon rest radius {maxR:F2}m (~{swingReach:F2}m swept) vs cone {range:F2}m (x{range / swingReach:F2}) | finisher {finRange:F2}m (x{finRange / swingReach:F2})");
|
||||
sb.AppendLine($" honesty budget: keep the ratio <= ~1.25x (guidelines G2; >1.4x reads as the old 'arc lies' complaint)");
|
||||
}
|
||||
else sb.AppendLine(" Reach: weapon mesh not found (run Attach Melee Weapon first)");
|
||||
|
||||
sb.AppendLine(" NOTE: SpecialSlam (cone socket) still damages AT FIRE, ~0.35s before its visual contact — the");
|
||||
sb.AppendLine(" same dishonesty melee just fixed; it rides the SOCKET pipeline (guidelines G6 follow-up).");
|
||||
Debug.Log(sb.ToString());
|
||||
return sb.ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f9457f271f6d69e4c92aa4a273af46fc
|
||||
@@ -120,14 +120,14 @@ namespace ProjectM.Server
|
||||
if (sweep)
|
||||
{
|
||||
foreach (var depenXform in SystemAPI.Query<RefRW<LocalTransform>>()
|
||||
.WithAll<EnemyTag>().WithNone<Dying, BossState>())
|
||||
.WithAll<EnemyTag>().WithNone<Dying, BossState>().WithNone<TargetDummyTag>())
|
||||
depenXform.ValueRW.Position = EnemyMoveUtil.Depenetrate(in physics, depenXform.ValueRO.Position, SweepRadius, envFilter);
|
||||
}
|
||||
|
||||
foreach (var (xform, stats, cooldown, knockback, windup, region) in
|
||||
SystemAPI.Query<RefRW<LocalTransform>, RefRO<EnemyStats>, RefRW<EnemyAttackCooldown>,
|
||||
RefRW<KnockbackState>, RefRW<AttackWindup>, RefRO<RegionTag>>()
|
||||
.WithAll<EnemyTag>().WithNone<LungeState, SpitterState, Dying>())
|
||||
.WithAll<EnemyTag>().WithNone<LungeState, SpitterState, Dying>().WithNone<TargetDummyTag>())
|
||||
{
|
||||
float3 pos = xform.ValueRO.Position;
|
||||
byte huskRegion = region.ValueRO.Region;
|
||||
@@ -398,7 +398,7 @@ namespace ProjectM.Server
|
||||
foreach (var (xform, stats, knockback, windup, spitter, region) in
|
||||
SystemAPI.Query<RefRW<LocalTransform>, RefRO<EnemyStats>, RefRW<KnockbackState>,
|
||||
RefRW<AttackWindup>, RefRW<SpitterState>, RefRO<RegionTag>>()
|
||||
.WithAll<EnemyTag, SpitterState>().WithNone<LungeState, Dying>())
|
||||
.WithAll<EnemyTag, SpitterState>().WithNone<LungeState, Dying>().WithNone<TargetDummyTag>())
|
||||
{
|
||||
float3 pos = xform.ValueRO.Position;
|
||||
byte sRegion = region.ValueRO.Region;
|
||||
@@ -503,7 +503,7 @@ namespace ProjectM.Server
|
||||
// Charger whose bit is currently DISABLED is still visited (Entities default-excludes disabled enableables).
|
||||
foreach (var (lunge, isLunging) in
|
||||
SystemAPI.Query<RefRO<LungeState>, EnabledRefRW<IsLunging>>()
|
||||
.WithAll<EnemyTag>().WithPresent<IsLunging>().WithNone<Dying>())
|
||||
.WithAll<EnemyTag>().WithPresent<IsLunging>().WithNone<Dying>().WithNone<TargetDummyTag>())
|
||||
{
|
||||
isLunging.ValueRW = lunge.ValueRO.UntilTick != 0u; // lunging iff a committed lunge is live this tick
|
||||
}
|
||||
@@ -591,7 +591,7 @@ namespace ProjectM.Server
|
||||
float nudgeStep = UnstickNudgeSpeed * dt;
|
||||
foreach (var (nxform, nstats, nav, nregion, nent) in
|
||||
SystemAPI.Query<RefRW<LocalTransform>, RefRO<EnemyStats>, RefRW<EnemyNavState>, RefRO<RegionTag>>()
|
||||
.WithAll<EnemyTag>().WithNone<SpitterState, BossState, Dying>().WithEntityAccess())
|
||||
.WithAll<EnemyTag>().WithNone<SpitterState, BossState, Dying>().WithNone<TargetDummyTag>().WithEntityAccess())
|
||||
{
|
||||
float3 npos = nxform.ValueRO.Position;
|
||||
byte nRegion = nregion.ValueRO.Region;
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
#if UNITY_EDITOR
|
||||
using ProjectM.Simulation;
|
||||
using Unity.Collections;
|
||||
using Unity.Entities;
|
||||
using Unity.Mathematics;
|
||||
using Unity.Transforms;
|
||||
|
||||
namespace ProjectM.Server
|
||||
{
|
||||
/// <summary>
|
||||
/// 07-20 sandbox combat target: keeps ONE planted <see cref="TargetDummyTag"/> dummy alive in the DevSandbox
|
||||
/// (scene-name gate — the established dev-script convention; GymSub bakes no WaveDirector, so this is the
|
||||
/// sandbox's enemy source by design). The dummy is a real enemy ghost instantiated from a baked prefab
|
||||
/// (grunt-shaped preferred), parked at the first player's position + a fixed offset, HP fat enough to eat a
|
||||
/// few chains; it dies through the NORMAL death path (Dying corpse window → destroy → kill pop on the client)
|
||||
/// and respawns at the same anchor shortly after — kill feedback stays testable on repeat. Editor-only.
|
||||
/// </summary>
|
||||
[WorldSystemFilter(WorldSystemFilterFlags.ServerSimulation)]
|
||||
public partial class TargetDummySpawnSystem : SystemBase
|
||||
{
|
||||
const float RespawnDelaySec = 1.5f;
|
||||
const float DummyHealth = 400f;
|
||||
static readonly float3 AnchorOffset = new float3(3f, 0f, 1.5f);
|
||||
|
||||
float _respawnTimer;
|
||||
float3 _anchor;
|
||||
bool _anchored;
|
||||
|
||||
protected override void OnUpdate()
|
||||
{
|
||||
if (UnityEngine.SceneManagement.SceneManager.GetActiveScene().name != "DevSandbox")
|
||||
return;
|
||||
|
||||
// A dummy exists (alive OR in its Dying corpse window) -> nothing to do; the corpse window
|
||||
// conveniently delays the respawn timer until the body is gone.
|
||||
var dummyQ = SystemAPI.QueryBuilder().WithAll<TargetDummyTag>().Build();
|
||||
if (!dummyQ.IsEmpty) { _respawnTimer = 0f; return; }
|
||||
|
||||
// Anchor once on the first player seen (spawn point + offset = a consistent training spot).
|
||||
if (!_anchored)
|
||||
{
|
||||
foreach (var xf in SystemAPI.Query<RefRO<LocalTransform>>().WithAll<PlayerTag>())
|
||||
{
|
||||
_anchor = xf.ValueRO.Position + AnchorOffset;
|
||||
_anchored = true;
|
||||
break;
|
||||
}
|
||||
if (!_anchored) return; // no player yet (world still connecting)
|
||||
}
|
||||
|
||||
_respawnTimer += SystemAPI.Time.DeltaTime; // wall-frame is fine: dev spawner in the plain server group
|
||||
if (_respawnTimer < RespawnDelaySec) return;
|
||||
_respawnTimer = 0f;
|
||||
|
||||
// Grunt-shaped prefab preferred (no lunge/spit machinery on a training dummy); fall back to any enemy.
|
||||
var prefQ = EntityManager.CreateEntityQuery(new EntityQueryDesc
|
||||
{
|
||||
All = new ComponentType[] { typeof(EnemyTag), typeof(Prefab) },
|
||||
None = new ComponentType[] { typeof(LungeState), typeof(SpitterState) },
|
||||
Options = EntityQueryOptions.IncludePrefab,
|
||||
});
|
||||
var prefabs = prefQ.ToEntityArray(Allocator.Temp);
|
||||
if (prefabs.Length == 0)
|
||||
{
|
||||
prefabs.Dispose();
|
||||
var anyQ = EntityManager.CreateEntityQuery(new EntityQueryDesc
|
||||
{
|
||||
All = new ComponentType[] { typeof(EnemyTag), typeof(Prefab) },
|
||||
Options = EntityQueryOptions.IncludePrefab,
|
||||
});
|
||||
prefabs = anyQ.ToEntityArray(Allocator.Temp);
|
||||
if (prefabs.Length == 0) { prefabs.Dispose(); return; } // no enemy ghosts in this world
|
||||
}
|
||||
var prefab = prefabs[0];
|
||||
prefabs.Dispose();
|
||||
|
||||
var inst = EntityManager.Instantiate(prefab);
|
||||
var baked = EntityManager.GetComponentData<LocalTransform>(prefab);
|
||||
EntityManager.SetComponentData(inst, baked.WithPosition(_anchor)); // WithPosition: never FromPosition (Scale reset)
|
||||
EntityManager.SetComponentData(inst, new Health { Current = DummyHealth, Max = DummyHealth });
|
||||
EntityManager.AddComponent<TargetDummyTag>(inst); // server-only: plants it (every AI pass excludes the tag)
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 036a5a62e96d4ac49b556c748135b478
|
||||
@@ -0,0 +1,14 @@
|
||||
using Unity.Entities;
|
||||
|
||||
namespace ProjectM.Simulation
|
||||
{
|
||||
/// <summary>07-20 sandbox combat testing: marks a server-spawned enemy ghost as a PLANTED target dummy —
|
||||
/// every EnemyAISystem pass excludes it (no move, no attack, no knockback integration, no depenetration
|
||||
/// nudge), so it stands at its anchor and just takes hits. Server-only (runtime-added at spawn, never
|
||||
/// replicated); the client sees a normal interpolated enemy, so health bars, hit flash, damage, kill pop
|
||||
/// and the death path all exercise the REAL pipeline. Spawned + respawned by TargetDummySpawnSystem in the
|
||||
/// DevSandbox (GymSub bakes no WaveDirector — the sandbox's combat target is this dummy, by design).
|
||||
/// ★ EnemyTag-reuse audit (CLAUDE.md): cleave/projectile damage MUST see it (the point); AI passes exclude
|
||||
/// it; wave/room cleared-checks would count it — acceptable ONLY because the spawner is sandbox-gated.</summary>
|
||||
public struct TargetDummyTag : IComponentData { }
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 013cf2990a041494fa876653dff1d059
|
||||
@@ -84,18 +84,18 @@ namespace ProjectM.Simulation
|
||||
ChargerLungeDurationTicks = 18f,
|
||||
ChargerWhiffStaggerTicks = 36f,
|
||||
GruntWindupTicks = Tuning.AttackWindupTicks, // canonical Grunt-windup source (TelegraphTests couples to it)
|
||||
MeleeDamage = 30f, // 07-19 heavy-weapon retime: bigger hit, slower cadence (DPS ~held: 18/0.27s -> 30/0.50s)
|
||||
MeleeDamage = 36f, // 07-21 HEAVY LOCK (operator A/B): DPS ~held at 60 (dmg = recover ticks)
|
||||
MeleeRange = 2.2f, // 07-20 G2.2 reach honesty: axe tip ~1.8m + a 0.4m player-favoring margin (was 2.6)
|
||||
MeleeConeHalfAngleRad = 0.9f, // ~51.5 deg half-angle (~103 deg cleave arc)
|
||||
MeleeRecoverTicks = 30f, // heavy-swing lock/recovery (~0.50s; 07-19 weighty-melee retime)
|
||||
MeleeChainGraceTicks = 24f, // window after the lock to chain the next hit (~0.4s)
|
||||
MeleeSwingMoveScale = 0.35f, // movement-commit while swinging (0..1)
|
||||
MeleeRecoverTicks = 36f, // 07-21 HEAVY LOCK: 0.60s light lock (finisher x1.5 = 54t)
|
||||
MeleeChainGraceTicks = 28f, // 07-21 HEAVY LOCK: ~0.47s chain window after the lock
|
||||
MeleeSwingMoveScale = 0.25f, // 07-21 HEAVY LOCK: deeper movement commit while swinging
|
||||
MeleeKnockbackSpeed = 6f,
|
||||
MeleeFinisherMult = 1.5f, // finisher (last hit) scales dmg/range/recover/knockback (07-19: 1.8 -> 1.5, recover was ballooning at the heavy cadence)
|
||||
MeleeComboLength = 3f, // light, light, finisher
|
||||
MeleeFinisherRangeMult = 1.25f, // 07-20 G2.2: finisher REACH (damage/recover/knockback keep MeleeFinisherMult)
|
||||
MeleeBufferTicks = 8f, // 07-20 G7: attack press remembered in the last ~0.13s of a lock (0 = off)
|
||||
MeleeContactTicks = 16f, // 07-20 G2.1: blade lands ~0.27s after swing start (per-step via MeleeTiming; 0 = legacy immediate)
|
||||
MeleeBufferTicks = 10f, // 07-21 HEAVY LOCK: wider buffer fits the slower cadence (0 = off)
|
||||
MeleeContactTicks = 20f, // 07-21 HEAVY LOCK: blade lands 20/12/25t per step -- clip speeds retuned to MATCH (wire tool 1.0/0.85/0.88; audit must show zero drift). Step-3 (25t) sits 1 tick under CastFacingTicks 26 -- do NOT raise without raising the cast window.
|
||||
StructureAggroWeight = 0.7f, // EB-1: <1 prefers structures (fortress aggro); live-tunable
|
||||
StaggerKnockbackSpeed = 7f, // B2 poise: kb.Speed >= this interrupts windups/lunges; below = nudge only
|
||||
SeparationMaxSpeed = 3f, // B1: max separation push (units/s) so soft-collision can't fling
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: dc29876a7efaad54da85c8c668fd3215
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c9422a862aa42ce49be71d2cab58f0b8
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"name": "baseline-heavy-0721",
|
||||
"notes": "THE SHIPPED DEFAULTS (heavy feel locked 07-21 from the operator A/B). Apply to RESET after experiments. DPS 60 held (dmg = recover ticks); clip speeds 1.0/0.85/0.88 match contact 20/12/25.",
|
||||
"sim": [
|
||||
{ "k": "MeleeDamage", "v": "36" },
|
||||
{ "k": "MeleeRange", "v": "2.2" },
|
||||
{ "k": "MeleeRecoverTicks", "v": "36" },
|
||||
{ "k": "MeleeChainGraceTicks", "v": "28" },
|
||||
{ "k": "MeleeFinisherMult", "v": "1.5" },
|
||||
{ "k": "MeleeFinisherRangeMult", "v": "1.25" },
|
||||
{ "k": "MeleeBufferTicks", "v": "10" },
|
||||
{ "k": "MeleeContactTicks", "v": "20" },
|
||||
{ "k": "MeleeSwingMoveScale", "v": "0.25" }
|
||||
],
|
||||
"feel": [
|
||||
{ "k": "MeleeArcIntensity", "v": "0.8" },
|
||||
{ "k": "MeleeArcBubbles", "v": "6" },
|
||||
{ "k": "FinisherHoldFrames", "v": "7" },
|
||||
{ "k": "MeleeConnectFovKick", "v": "1.1" },
|
||||
{ "k": "CombatIdleHoldSec", "v": "4" }
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d5f21d9955bf61d4ca0e4543712e8b46
|
||||
TextScriptImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,44 @@
|
||||
{
|
||||
"name": "heavy-committed",
|
||||
"notes": "LOCKED into code defaults 2026-07-21 (kept as the record of the winning A/B candidate). Slower, weightier: MonHun-lean. Longer lock + contact, bigger hit + hold, dimmer arc (the weapon is the read). DPS ~60 held.",
|
||||
"sim": [
|
||||
{
|
||||
"k": "MeleeDamage",
|
||||
"v": "36"
|
||||
},
|
||||
{
|
||||
"k": "MeleeRecoverTicks",
|
||||
"v": "36"
|
||||
},
|
||||
{
|
||||
"k": "MeleeChainGraceTicks",
|
||||
"v": "28"
|
||||
},
|
||||
{
|
||||
"k": "MeleeContactTicks",
|
||||
"v": "20"
|
||||
},
|
||||
{
|
||||
"k": "MeleeBufferTicks",
|
||||
"v": "10"
|
||||
},
|
||||
{
|
||||
"k": "MeleeSwingMoveScale",
|
||||
"v": "0.25"
|
||||
}
|
||||
],
|
||||
"feel": [
|
||||
{
|
||||
"k": "MeleeArcIntensity",
|
||||
"v": "0.8"
|
||||
},
|
||||
{
|
||||
"k": "FinisherHoldFrames",
|
||||
"v": "7"
|
||||
},
|
||||
{
|
||||
"k": "MeleeConnectFovKick",
|
||||
"v": "1.1"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 2ed6c17ceb8dc044f885d858b99e9a21
|
||||
TextScriptImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"name": "snappier",
|
||||
"notes": "Faster, Hades-lean: shorter lock + contact, lighter hits, freer movement mid-swing. DPS ~60 held. Contact 12 keeps step-3 (15t) under CastFacingTicks 26.",
|
||||
"sim": [
|
||||
{ "k": "MeleeDamage", "v": "24" },
|
||||
{ "k": "MeleeRecoverTicks", "v": "24" },
|
||||
{ "k": "MeleeChainGraceTicks", "v": "20" },
|
||||
{ "k": "MeleeContactTicks", "v": "12" },
|
||||
{ "k": "MeleeBufferTicks", "v": "8" },
|
||||
{ "k": "MeleeSwingMoveScale", "v": "0.45" }
|
||||
],
|
||||
"feel": [
|
||||
{ "k": "MeleeArcIntensity", "v": "1.1" },
|
||||
{ "k": "FinisherHoldFrames", "v": "4" },
|
||||
{ "k": "MeleeConnectFovKick", "v": "0.7" }
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b58b377cf031b0b489bd1e1234a1f3ec
|
||||
TextScriptImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,29 @@
|
||||
# 2026-07-20 (D) — Sandbox target dummy (planted combat-test target)
|
||||
|
||||
**Operator:** enemies don't work in the DevSandbox — replace with a target dummy for combat testing.
|
||||
**Diagnosis (recorded, not fixed per operator's "anyways"):** `GymSub.unity` bakes **no WaveDirector** — only the
|
||||
Game arena's `Gameplay.unity` has one — so nothing ever spawns enemies in the sandbox (and Force Next Wave has no
|
||||
director to force).
|
||||
|
||||
## What shipped
|
||||
|
||||
- **`TargetDummyTag`** (Simulation, server-only, never replicated): marks a real enemy ghost as a PLANTED dummy.
|
||||
Every `EnemyAISystem` pass (grunt move/attack, lunge, spitter, depenetration nudge, IsLunging enable — 5
|
||||
queries) excludes it via `WithNone<TargetDummyTag>`; since those passes are ALSO where `KnockbackState`
|
||||
integrates into movement, the dummy is planted for free (knockback stamps land but never move it).
|
||||
- **`TargetDummySpawnSystem`** (Server, editor-only, plain `SimulationSystemGroup`, `DevSandbox` scene-name gate —
|
||||
the established dev-script convention): keeps exactly ONE dummy alive at the first player's spawn + (3, 0, 1.5),
|
||||
400 HP (≈4 full chains), instantiated from a grunt-shaped baked enemy prefab (`WithPosition`, never
|
||||
`FromPosition`). It dies through the NORMAL path (`Dying` corpse window → destroy → kill pop) and respawns
|
||||
1.5 s after the corpse clears — the corpse window delays the timer for free. Client sees a normal interpolated
|
||||
enemy: health bar, hit flash, damage numbers, death feedback all exercise the real pipeline.
|
||||
- **★ EnemyTag-reuse audit** (the CLAUDE.md re-audit rule): damage paths MUST see the dummy (the point);
|
||||
AI excluded; markers/telemetry count it (cosmetic); wave/room cleared-checks WOULD count it — acceptable only
|
||||
because the spawner is sandbox-gated where no director runs. If a dummy ever ships outside the sandbox,
|
||||
re-audit cleared-checks first.
|
||||
|
||||
Composes with the tuning bench: the sandbox now always has a target for eyes-on swings; `CombatProbe` still
|
||||
spawns its own tracked 100k-HP dummy for measured runs (both can coexist — the probe reads its own entity).
|
||||
|
||||
**Validation:** console clean; suite deferred mid-session (operator entered Play while it queued — their live
|
||||
session doubles as the visual validation); dummy presence verified live server-side.
|
||||
@@ -0,0 +1,41 @@
|
||||
# 2026-07-20 (C) — Data-driven tuning bench (profiles + audit + probe)
|
||||
|
||||
**Operator:** "the knobs are a lot & not very intuitive — is there any way for you to do data-driven tuning?"
|
||||
The answer: raw scalars are bad for both of us — the operator can't intuit ticks/radians, and I can't feel the
|
||||
game. What I CAN do precisely is **derive and measure**. Three tools:
|
||||
|
||||
## 1. Feel profiles — whole gestalts as data
|
||||
`Assets/_Project/Tuning/Profiles/*.json` — one file = sim knobs (TuningKnob names) + FeelConfig fields, PARTIAL
|
||||
(only listed keys apply). `FeelProfileService` (Client/Debug, editor-only) applies via the existing authoritative
|
||||
SetTuning path + FeelConfig reflection; **"Save current"** captures a hand-dialed state back to a profile.
|
||||
DebugOverlay grew a Profiles section (one-click A/B) + the probe button. Starter set: `baseline-0720` (the shipped
|
||||
defaults / reset), `heavy-committed` (MonHun-lean), `snappier` (Hades-lean) — all DPS-held (~60) via dmg = recover.
|
||||
**Rule: code defaults stay the single shipped truth; profiles are experiments — a winner gets folded into
|
||||
`TuningConfig.Defaults()` / `FeelConfig.ResetDefaults()` and the profile archived.**
|
||||
|
||||
## 2. TuningAuditTools — the derived truth (edit-time, no eyes needed)
|
||||
`ProjectM/Tuning/Audit Melee Timing + Reach`: derives per-step contact from the ACTUAL clip WindUp takes ÷ wired
|
||||
state speeds and compares against `MeleeTiming` + the knob (drift detector — kills the hand-copied-constant class
|
||||
of bug); checks the un-clamped coupling invariants the design review flagged (contact < lock, ≤ CastFacingTicks);
|
||||
computes cadence/DPS; measures the reach-honesty ratio off the BAKED weapon mesh. First run: **zero drift**
|
||||
(16/10/20 == clip truth), DPS 60.0 flat, and **reach ratio ×0.93 base / ×1.16 finisher** — the "swings don't line
|
||||
up" complaint is now a measured number inside the ≤1.25 budget (was >2×).
|
||||
|
||||
## 3. CombatProbe — measured play numbers
|
||||
`CombatProbe.Run(n)` (Client/Debug, editor-only; overlay button): spawns a 100k-HP dummy in the cone, mash-drives
|
||||
a real injected attack chain, samples the SERVER per frame → per-swing start/scheduled-contact/damage-tick/amount,
|
||||
measured cadence + DPS + buffer usage. Demo run at defaults: gaps 30/30/45 exactly, contacts +16/+10/+20 exactly,
|
||||
damage 36/36/54 (class seed × finisher). Then **the full loop**: applied `snappier` → measured 24/24/36 gaps and
|
||||
+12/+8/+15 contacts immediately. Tuning = author JSON → apply → measure → compare. Probe fix during the demo: wait
|
||||
for the FINAL swing's contact before finishing (first run cut off swing 6).
|
||||
|
||||
## The workflow now
|
||||
- **Me (precise):** edit/author a profile → `Apply` → `CombatProbe` → read measured numbers vs targets → iterate.
|
||||
- **Operator (intuitive):** tap profile buttons in the overlay to A/B whole feels → nudge individual rows if
|
||||
desired → **Save current** → I fold the winner into code defaults.
|
||||
|
||||
Validation: console clean; audit + probe both demonstrated live; profile apply verified against measured sim
|
||||
output. No netcode surface (profiles ride the existing SetTuning RPC; probe is read-only diagnostics).
|
||||
|
||||
**Next:** operator A/Bs the three starter profiles in DevSandbox (DEV overlay → Feel Profiles); tell me which
|
||||
direction wins (or Save-current a custom) and I fold it into defaults + build the next profile iteration.
|
||||
@@ -0,0 +1,25 @@
|
||||
# 2026-07-20 (E) — Weapon grip anchor: gizmo-authored, WYSIWYG
|
||||
|
||||
**Operator:** why is the grip code-consts, and can the manipulation show the ACTUAL weapon at true rotation/size?
|
||||
|
||||
**Why it was consts:** DOTS + Rukhanka has no runtime bone Transforms — attachments are rigid-skinned SMRs whose
|
||||
grip is baked INTO the vertex data (the proven suit-kit path), so *something* must feed the bake; consts were the
|
||||
feel-pass shortcut. The fix is a designer-grade INPUT, not a different runtime model.
|
||||
|
||||
**What shipped (PlayerRigTools):**
|
||||
- **`Player - Create Weapon Grip Anchor (LANTERN)`** — creates `WeaponGripAnchor` under the `Hand_R` bone in
|
||||
`Player.prefab`, carrying the ACTUAL axe mesh as an edit-time preview (plain `MeshRenderer` + `M_Weapon_Preview`
|
||||
URP/Lit + the pack atlas — the runtime deformation shader won't render on a static MR). What you grab with
|
||||
move/rotate/scale gizmos in the prefab stage IS the axe, live, at true size. Seeded from the consts on first
|
||||
creation only — re-runs never stomp a hand-tuned anchor.
|
||||
- **`AttachMeleeWeapon` reads the anchor** (localPosition/localRotation/localScale.x) when present; consts are the
|
||||
fallback/seed. Non-uniform scale warns and takes X.
|
||||
- **Runtime safety:** the anchor is tagged **`EditorOnly`** — the Entities baker skips it, so the preview can never
|
||||
leak an un-animated duplicate. Play-verified: exactly one axe on the rig. Until rebake you see BOTH the preview
|
||||
(new grip) and the baked axe (old grip) in the prefab stage — a free before/after ghost; coincident = in sync.
|
||||
|
||||
**The designer loop:** open `Player.prefab` → search `WeaponGripAnchor` → move/rotate/scale with normal gizmos →
|
||||
`ProjectM/Animation/Player - Attach Melee Weapon` → the baked axe snaps to the preview, visible immediately in the
|
||||
prefab stage (no Play mode, no recompile). This is the in-Unity twin of the Blender nudge-and-bake workflow, and
|
||||
the pattern for future rigid attachments (weapon variants, kit pieces): gizmo anchors for spatial authoring,
|
||||
profiles for numbers, tools as the bake button.
|
||||
@@ -0,0 +1,42 @@
|
||||
# 2026-07-21 (F) — HEAVY FEEL LOCKED (the A/B winner folded into code defaults)
|
||||
|
||||
**Operator:** "lets lock the heavy feel, commit everything." The `heavy-committed` profile wins the A/B; per the
|
||||
bench rule the winner folds into code defaults and the profile stays as the record.
|
||||
|
||||
## The locked numbers (TuningConfig.Defaults / FeelConfig.ResetDefaults)
|
||||
|
||||
| Knob | Was | LOCKED | Note |
|
||||
|---|---|---|---|
|
||||
| MeleeRecoverTicks | 30 | **36** | 0.60s light lock; finisher x1.5 = 54t (0.90s) |
|
||||
| MeleeDamage | 30 | **36** | DPS 60 held (dmg = recover) |
|
||||
| MeleeContactTicks | 16 | **20** | blade lands 20/12/25t per step |
|
||||
| MeleeChainGraceTicks | 24 | **28** | |
|
||||
| MeleeBufferTicks | 8 | **10** | |
|
||||
| MeleeSwingMoveScale | 0.35 | **0.25** | deeper commit |
|
||||
| MeleeArcIntensity | 1.0 | **0.8** | the weapon is the read |
|
||||
| FinisherHoldFrames | 5 | **7** | ~117ms beat |
|
||||
| MeleeConnectFovKick | 0.8 | **1.1** | |
|
||||
|
||||
**Honesty correction while locking:** the profile as played had damage at 20/12/25t while the clips' VISUAL
|
||||
contacts sat at 16/10/20t (4–5t drift, the audit flagged it). The lock keeps the approved DAMAGE timing and
|
||||
retimes the clip state speeds to match — wire-tool swaps now **1.0 / 0.85 / 0.88** → audit shows **ZERO drift**
|
||||
(derived == configured at every step). Coupling note baked into the knob comment: step-3 contact (25t) sits ONE
|
||||
tick under CastFacingTicks (26) — never raise the contact knob without raising the cast window.
|
||||
|
||||
## Verified
|
||||
|
||||
- Audit: zero drift, DPS 60.0 per-light AND full-chain, buffer 10t + batch slack 4t.
|
||||
- Probe (server-measured, in play): gaps exactly 36/36/54/36/36, contacts +20/+12/+25 exactly, damage 42/42/63
|
||||
(base 36 x class seed, finisher x1.5).
|
||||
- 414/414 EditMode green (mechanics tests pin their own config — default drift can't break them, by design).
|
||||
- **The grip-anchor flow got its first real operator use**: the anchor moved from the seed (0, .06, .02) to the
|
||||
hand-tuned (0.115, .06, .061) and the baked mesh matches it exactly (expected-radius recompute == baked 1.53m).
|
||||
Reach with the tuned grip: swept ~1.98m vs 2.2m cone = **x1.11** base (in budget); finisher x1.39 — slightly
|
||||
over the 1.25 guideline, watch it in play (trim MeleeFinisherRangeMult toward ~1.15 if the finisher reads long).
|
||||
|
||||
Profiles: `baseline-0720.json` now carries the LOCKED defaults (display name `baseline-heavy-0721`, the reset
|
||||
button); `heavy-committed.json` marked as the locked record; `snappier.json` stays as the contrast experiment.
|
||||
|
||||
**Next:** hit-react pass (the last missing impact layer: pack React/Stagger clips + a damage-edge anim param +
|
||||
the Rukhanka-compatible victim-flinch that replaces the cut vibrate) · finisher-reach eyes-on (x1.39) ·
|
||||
G6 zone-socket telegraph + the SpecialSlam socket's damage-at-contact (same honesty fix melee got).
|
||||
Reference in New Issue
Block a user