diff --git a/Assets/_Game/Data/Combat/Weapons/WPN_DiHun.asset b/Assets/_Game/Data/Combat/Weapons/WPN_DiHun.asset index 5bd19d7..d2bf091 100644 --- a/Assets/_Game/Data/Combat/Weapons/WPN_DiHun.asset +++ b/Assets/_Game/Data/Combat/Weapons/WPN_DiHun.asset @@ -18,7 +18,7 @@ MonoBehaviour: weaponType: 1 groundComboSteps: - clip: - _FadeDuration: 0.25 + _FadeDuration: 0 _Speed: 1 _Events: _NormalizedTimes: [] @@ -30,13 +30,13 @@ MonoBehaviour: hitBoxEnter: 0 hitBoxExit: 0.092 comboInputOpen: 0 - comboInputClose: 1 + comboInputClose: 0.07 cancelWindowOpen: 0 recoveryTime: 0 - comboTimeout: 0 + comboTimeout: 0.2 hitBoxId: ATK_Ground_1 - clip: - _FadeDuration: 0.25 + _FadeDuration: 0 _Speed: 1 _Events: _NormalizedTimes: [] diff --git a/Assets/_Game/Scenes/Testings/TestRoomA.unity b/Assets/_Game/Scenes/Testings/TestRoomA.unity index 805b3d2..cbcc7eb 100644 --- a/Assets/_Game/Scenes/Testings/TestRoomA.unity +++ b/Assets/_Game/Scenes/Testings/TestRoomA.unity @@ -2842,83 +2842,6 @@ PolygonCollider2D: - {x: 3.5, y: 6.5} - {x: -15.5, y: 6.5} m_UseDelaunayMesh: 0 ---- !u!1 &147475844 -GameObject: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - serializedVersion: 6 - m_Component: - - component: {fileID: 147475846} - - component: {fileID: 147475845} - m_Layer: 0 - m_Name: PassengerSensor - m_TagString: Untagged - m_Icon: {fileID: 0} - m_NavMeshLayer: 0 - m_StaticEditorFlags: 0 - m_IsActive: 1 ---- !u!61 &147475845 -BoxCollider2D: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 147475844} - m_Enabled: 1 - m_Density: 1 - m_Material: {fileID: 0} - m_IncludeLayers: - serializedVersion: 2 - m_Bits: 0 - m_ExcludeLayers: - serializedVersion: 2 - m_Bits: 0 - m_LayerOverridePriority: 0 - m_ForceSendLayers: - serializedVersion: 2 - m_Bits: 4294967295 - m_ForceReceiveLayers: - serializedVersion: 2 - m_Bits: 4294967295 - m_ContactCaptureLayers: - serializedVersion: 2 - m_Bits: 4294967295 - m_CallbackLayers: - serializedVersion: 2 - m_Bits: 4294967295 - m_IsTrigger: 1 - m_UsedByEffector: 0 - m_UsedByComposite: 0 - m_Offset: {x: -0.06424427, y: 0.33} - m_SpriteTilingProperty: - border: {x: 0, y: 0, z: 0, w: 0} - pivot: {x: 0, y: 0} - oldSize: {x: 0, y: 0} - newSize: {x: 0, y: 0} - adaptiveTilingThreshold: 0 - drawMode: 0 - adaptiveTiling: 0 - m_AutoTiling: 0 - serializedVersion: 2 - m_Size: {x: 1.7309284, y: 0.25} - m_EdgeRadius: 0 ---- !u!4 &147475846 -Transform: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 147475844} - 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: 1077748573} - m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} --- !u!1 &148166884 GameObject: m_ObjectHideFlags: 0 @@ -7952,37 +7875,6 @@ MonoBehaviour: _noiseFrequency: 1 _dedicatedCamera: {fileID: 1059402937} _dedicatedPriority: 20 ---- !u!1 &405300252 -GameObject: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - serializedVersion: 6 - m_Component: - - component: {fileID: 405300253} - m_Layer: 0 - m_Name: WaypointB - m_TagString: Untagged - m_Icon: {fileID: 0} - m_NavMeshLayer: 0 - m_StaticEditorFlags: 0 - m_IsActive: 1 ---- !u!4 &405300253 -Transform: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 405300252} - serializedVersion: 2 - m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} - m_LocalPosition: {x: -13.74, y: 4.27, z: 0} - m_LocalScale: {x: 1, y: 1, z: 1} - m_ConstrainProportionsScale: 0 - m_Children: [] - m_Father: {fileID: 0} - m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} --- !u!1 &406872835 GameObject: m_ObjectHideFlags: 0 @@ -18171,224 +18063,6 @@ Transform: - {fileID: 194069122} m_Father: {fileID: 113908853} m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} ---- !u!1 &1077748568 -GameObject: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - serializedVersion: 6 - m_Component: - - component: {fileID: 1077748573} - - component: {fileID: 1077748572} - - component: {fileID: 1077748571} - - component: {fileID: 1077748570} - - component: {fileID: 1077748569} - - component: {fileID: 1077748575} - - component: {fileID: 1077748574} - m_Layer: 8 - m_Name: MovingPlatform - m_TagString: Untagged - m_Icon: {fileID: 0} - m_NavMeshLayer: 0 - m_StaticEditorFlags: 0 - m_IsActive: 1 ---- !u!114 &1077748569 -MonoBehaviour: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 1077748568} - m_Enabled: 1 - m_EditorHideFlags: 0 - m_Script: {fileID: 11500000, guid: ae997ac2469ff6b4cb58cf825ed67397, type: 3} - m_Name: - m_EditorClassIdentifier: - _moveType: 0 - _wayPoints: - - {fileID: 1112983964} - - {fileID: 405300253} - _speed: 3 - _waitAtEndpoint: 0.5 - _activationChannel: {fileID: 0} - _passengerSensor: {fileID: 147475845} - _passengerLayer: - serializedVersion: 2 - m_Bits: 8704 ---- !u!212 &1077748570 -SpriteRenderer: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 1077748568} - m_Enabled: 0 - m_CastShadows: 0 - m_ReceiveShadows: 0 - m_DynamicOccludee: 1 - m_StaticShadowCaster: 0 - m_MotionVectors: 1 - m_LightProbeUsage: 1 - m_ReflectionProbeUsage: 1 - m_RayTracingMode: 0 - m_RayTraceProcedural: 0 - m_RenderingLayerMask: 1 - m_RendererPriority: 0 - m_Materials: - - {fileID: 2100000, guid: a97c105638bdf8b4a8650670310a4cd3, 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: 0 - m_MinimumChartSize: 4 - m_AutoUVMaxDistance: 0.5 - m_AutoUVMaxAngle: 89 - m_LightmapParameters: {fileID: 0} - m_SortingLayerID: 0 - m_SortingLayer: 0 - m_SortingOrder: 0 - m_Sprite: {fileID: 7482667652216324306, guid: 311925a002f4447b3a28927169b83ea6, type: 3} - m_Color: {r: 1, g: 1, b: 1, a: 1} - m_FlipX: 0 - m_FlipY: 0 - m_DrawMode: 0 - m_Size: {x: 1, y: 1} - m_AdaptiveModeThreshold: 0.5 - m_SpriteTileMode: 0 - m_WasSpriteAssigned: 1 - m_MaskInteraction: 0 - m_SpriteSortPoint: 0 ---- !u!61 &1077748571 -BoxCollider2D: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 1077748568} - m_Enabled: 1 - m_Density: 1 - m_Material: {fileID: 0} - m_IncludeLayers: - serializedVersion: 2 - m_Bits: 0 - m_ExcludeLayers: - serializedVersion: 2 - m_Bits: 0 - m_LayerOverridePriority: 0 - m_ForceSendLayers: - serializedVersion: 2 - m_Bits: 4294967295 - m_ForceReceiveLayers: - serializedVersion: 2 - m_Bits: 4294967295 - m_ContactCaptureLayers: - serializedVersion: 2 - m_Bits: 4294967295 - m_CallbackLayers: - serializedVersion: 2 - m_Bits: 4294967295 - m_IsTrigger: 0 - m_UsedByEffector: 0 - m_UsedByComposite: 0 - m_Offset: {x: 0, y: 0} - m_SpriteTilingProperty: - border: {x: 0, y: 0, z: 0, w: 0} - pivot: {x: 0.5, y: 0.5} - oldSize: {x: 1, y: 1} - newSize: {x: 1, y: 1} - adaptiveTilingThreshold: 0.5 - drawMode: 0 - adaptiveTiling: 0 - m_AutoTiling: 0 - serializedVersion: 2 - m_Size: {x: 4, y: 0.4} - m_EdgeRadius: 0 ---- !u!50 &1077748572 -Rigidbody2D: - serializedVersion: 4 - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 1077748568} - m_BodyType: 1 - m_Simulated: 1 - m_UseFullKinematicContacts: 0 - m_UseAutoMass: 0 - m_Mass: 1 - m_LinearDrag: 0 - m_AngularDrag: 0.05 - m_GravityScale: 1 - m_Material: {fileID: 0} - m_IncludeLayers: - serializedVersion: 2 - m_Bits: 0 - m_ExcludeLayers: - serializedVersion: 2 - m_Bits: 0 - m_Interpolate: 1 - m_SleepingMode: 1 - m_CollisionDetection: 0 - m_Constraints: 4 ---- !u!4 &1077748573 -Transform: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 1077748568} - serializedVersion: 2 - m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} - m_LocalPosition: {x: -16.74, y: 4.27, z: 0} - m_LocalScale: {x: 1, y: 1, z: 1} - m_ConstrainProportionsScale: 0 - m_Children: - - {fileID: 147475846} - m_Father: {fileID: 0} - m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} ---- !u!114 &1077748574 -MonoBehaviour: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 1077748568} - m_Enabled: 0 - m_EditorHideFlags: 0 - m_Script: {fileID: 11500000, guid: c1483f09f23da6b469f288d63b2f52b5, type: 3} - m_Name: - m_EditorClassIdentifier: - _allowDropThrough: 1 - _dropDisableDuration: 0.3 ---- !u!251 &1077748575 -PlatformEffector2D: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 1077748568} - m_Enabled: 0 - m_UseColliderMask: 1 - m_ColliderMask: - serializedVersion: 2 - m_Bits: 4294967295 - m_RotationalOffset: 0 - m_UseOneWay: 1 - m_UseOneWayGrouping: 0 - m_SurfaceArc: 150.47 - m_UseSideFriction: 0 - m_UseSideBounce: 0 - m_SideArc: 1 --- !u!1 &1078913098 GameObject: m_ObjectHideFlags: 0 @@ -18770,37 +18444,6 @@ Transform: m_Children: [] m_Father: {fileID: 1658705856} m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} ---- !u!1 &1112983963 -GameObject: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - serializedVersion: 6 - m_Component: - - component: {fileID: 1112983964} - m_Layer: 0 - m_Name: WaypointA - m_TagString: Untagged - m_Icon: {fileID: 0} - m_NavMeshLayer: 0 - m_StaticEditorFlags: 0 - m_IsActive: 1 ---- !u!4 &1112983964 -Transform: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 1112983963} - serializedVersion: 2 - m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} - m_LocalPosition: {x: -19.74, y: 4.27, z: 0} - m_LocalScale: {x: 1, y: 1, z: 1} - m_ConstrainProportionsScale: 0 - m_Children: [] - m_Father: {fileID: 0} - m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} --- !u!1 &1113400486 GameObject: m_ObjectHideFlags: 0 @@ -22019,7 +21662,7 @@ Transform: m_GameObject: {fileID: 1354690325} serializedVersion: 2 m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} - m_LocalPosition: {x: -5.02, y: 12.9783, z: 0} + m_LocalPosition: {x: -26.800001, y: 7.27, z: 0} m_LocalScale: {x: 1, y: 12.316664, z: 1} m_ConstrainProportionsScale: 0 m_Children: [] @@ -30214,7 +29857,7 @@ Transform: m_GameObject: {fileID: 1865796628} serializedVersion: 2 m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} - m_LocalPosition: {x: -9.4, y: 12.9783, z: 0} + m_LocalPosition: {x: -31.18, y: 7.27, z: 0} m_LocalScale: {x: 1, y: 12.316664, z: 1} m_ConstrainProportionsScale: 0 m_Children: [] @@ -32254,6 +31897,7 @@ MonoBehaviour: m_EditorClassIdentifier: _config: {fileID: 11400000, guid: 8f33c49c9bd20ac47a6867f1f19f24a9, type: 2} _input: {fileID: 11400000, guid: 88fadef8bc554e04483edd7418d20aa2, type: 2} + _stats: {fileID: 0} _onFormChanged: {fileID: 11400000, guid: 9e1af94c61bb1c04ebc829f3838714f4, type: 2} _onSkillSetChanged: {fileID: 11400000, guid: 249630b7ebe7a094299413cc67c4f3d1, type: 2} --- !u!114 &1995033583 @@ -35358,6 +35002,3 @@ SceneRoots: - {fileID: 783576435} - {fileID: 1813702095} - {fileID: 1995033591} - - {fileID: 1077748573} - - {fileID: 1112983964} - - {fileID: 405300253} diff --git a/Assets/_Game/Scripts/Editor/Scene/SceneObjectPlacerTool.cs b/Assets/_Game/Scripts/Editor/Scene/SceneObjectPlacerTool.cs index b03e8f6..a7809a4 100644 --- a/Assets/_Game/Scripts/Editor/Scene/SceneObjectPlacerTool.cs +++ b/Assets/_Game/Scripts/Editor/Scene/SceneObjectPlacerTool.cs @@ -775,9 +775,13 @@ namespace BaseGames.Editor { var report = new List(); - GameObject go = new GameObject("MovingPlatform"); - Undo.RegisterCreatedObjectUndo(go, "Place Moving Platform"); - go.transform.position = GetDropPosition(); + // 根节点:平台实体 + 路径点都挂在此节点下,路径点不随平台本体移动 + GameObject root = new GameObject("MovingPlatform_Root"); + Undo.RegisterCreatedObjectUndo(root, "Place Moving Platform"); + root.transform.position = GetDropPosition(); + + // 平台实体:作为 root 子节点 + GameObject go = GetOrCreateChild(root.transform, "MovingPlatform").gameObject; SetLayer(go, "Platform", report); Rigidbody2D rb = GetOrAddComponent(go); @@ -796,22 +800,23 @@ namespace BaseGames.Editor sensorCol.size = new Vector2(3.8f, 0.25f); sensorCol.offset = new Vector2(0f, 0.33f); - // Waypoint markers (LinearAB mode end points) - Transform wpA = GetOrCreateChild(go.transform, "WaypointA"); - Transform wpB = GetOrCreateChild(go.transform, "WaypointB"); - wpA.localPosition = new Vector3(-3f, 0f, 0f); - wpB.localPosition = new Vector3(3f, 0f, 0f); + // 路径点:挂在 root 下而非平台下,平台移动时路径点位置不变 + Transform wpA = GetOrCreateChild(root.transform, "WaypointA"); + Transform wpB = GetOrCreateChild(root.transform, "WaypointB"); + wpA.position = root.transform.position + new Vector3(-3f, 0f, 0f); + wpB.position = root.transform.position + new Vector3( 3f, 0f, 0f); MovingPlatform platform = GetOrAddComponent(go); AssignReference(platform, "_passengerSensor", sensorCol, report); AssignLayerMask(platform, "_passengerLayer", new[] { "Player", "Enemy" }, report); AssignObjectArray(platform, "_wayPoints", new Object[] { wpA, wpB }, report); - report.Add("WaypointA / WaypointB 为移动端点,可将其拖出平台并在场景中调整位置。"); + report.Add("WaypointA / WaypointB 已挂在 MovingPlatform_Root 下(非平台子节点),平台移动时路径点保持原位。"); + report.Add("在场景中调整 WaypointA / WaypointB 的世界位置即可设置移动端点。"); report.Add("如需触发激活,改 _moveType = TriggeredLinear 并将 VoidEventChannelSO 拖入 _activationChannel。"); - Selection.activeGameObject = go; - MarkDirtyAndLog("Moving Platform", go, report); + Selection.activeGameObject = root; + MarkDirtyAndLog("Moving Platform", root, report); } [MenuItem("BaseGames/Scene/Place/Tilemap Ground", priority = 160)] diff --git a/Assets/_Game/Scripts/Input/InputReaderSO.cs b/Assets/_Game/Scripts/Input/InputReaderSO.cs index f6c25a9..d75f33b 100644 --- a/Assets/_Game/Scripts/Input/InputReaderSO.cs +++ b/Assets/_Game/Scripts/Input/InputReaderSO.cs @@ -30,6 +30,7 @@ namespace BaseGames.Input public event Action SpiritSkill2CancelledEvent; public event Action SpellCastEvent; public event Action InteractEvent; + public event Action InteractCancelledEvent; // ── UI Events ───────────────────────────────────────────────────────── public event Action PauseEvent; @@ -177,7 +178,8 @@ namespace BaseGames.Input BindStarted(_gameplay, "SpiritSkill2", () => SpiritSkill2StartedEvent?.Invoke()); BindCanceled(_gameplay, "SpiritSkill2", () => SpiritSkill2CancelledEvent?.Invoke()); BindStarted(_gameplay, "Spell", () => SpellCastEvent?.Invoke()); - BindStarted(_gameplay, "Interact", () => InteractEvent?.Invoke()); + BindStarted(_gameplay, "Interact", () => InteractEvent?.Invoke()); + BindCanceled(_gameplay, "Interact", () => InteractCancelledEvent?.Invoke()); BindStarted(_gameplay, "Pause", HandlePause); diff --git a/Assets/_Game/Scripts/Player/PlayerMovement.cs b/Assets/_Game/Scripts/Player/PlayerMovement.cs index 8cc36d4..7429d73 100644 --- a/Assets/_Game/Scripts/Player/PlayerMovement.cs +++ b/Assets/_Game/Scripts/Player/PlayerMovement.cs @@ -43,6 +43,7 @@ namespace BaseGames.Player private bool _isWallRight; private bool _onOneWayPlatform; private int _facingDirection = 1; + private bool _facingLocked; // 为 true 时 UpdateFacing() 不覆盖朝向 private bool _cancelWindowOpen; private SurfaceType _currentSurface = SurfaceType.Ground; private readonly Collider2D[] _groundBuffer = new Collider2D[4]; @@ -213,6 +214,7 @@ namespace BaseGames.Player // ── 朝向 ────────────────────────────────────────────────────────────── public void UpdateFacing() { + if (_facingLocked) return; // 读取玩家输入速度(不含平台分量),避免平台横向运动驱动朝向翻转。 float vx = _inputVelocityX; if (Mathf.Abs(vx) < 0.1f) return; @@ -247,6 +249,13 @@ namespace BaseGames.Player transform.localScale = new Vector3(dir, 1f, 1f); } + /// + /// 锁定/解锁自动朝向(UpdateFacing)。 + /// 传入 true 后 UpdateFacing 不再根据输入速度覆盖朝向, + /// 直到传入 false 解锁。适用于抓墙、蹬墙跳等需要手动控制朝向的状态。 + /// + public void LockFacing(bool locked) => _facingLocked = locked; + // ── 取消窗口 ────────────────────────────────────────────────────────── public void SetCancelWindowOpen(bool open) => _cancelWindowOpen = open; @@ -318,7 +327,8 @@ namespace BaseGames.Player /// public void WallJumpAway(int wallDir) { - _rb.velocity = new Vector2(-wallDir * _config.WallJumpAwayForceX, _config.WallJumpAwayForceY); + _inputVelocityX = -wallDir * _config.WallJumpAwayForceX; + _rb.velocity = new Vector2(_inputVelocityX, _config.WallJumpAwayForceY); _coyoteTimer = 0f; } @@ -328,7 +338,8 @@ namespace BaseGames.Player /// public void WallJumpToward(int wallDir) { - _rb.velocity = new Vector2(wallDir * _config.WallJumpTowardForceX, _config.WallJumpTowardForceY); + _inputVelocityX = wallDir * _config.WallJumpTowardForceX; + _rb.velocity = new Vector2(_inputVelocityX, _config.WallJumpTowardForceY); _coyoteTimer = 0f; } diff --git a/Assets/_Game/Scripts/Player/PlayerStats.cs b/Assets/_Game/Scripts/Player/PlayerStats.cs index f945fc2..d6fccad 100644 --- a/Assets/_Game/Scripts/Player/PlayerStats.cs +++ b/Assets/_Game/Scripts/Player/PlayerStats.cs @@ -195,6 +195,18 @@ namespace BaseGames.Player OnDamaged?.Invoke(); } + /// + /// 强制即死,无视无敌帧(危险区域、深渊等环境击杀专用)。 + /// GodMode 下仍然豁免。 + /// + public void Kill() + { + if (_isGodMode || !IsAlive) return; + CurrentHP = 0; + _onHPChanged?.Raise(CurrentHP); + OnDamaged?.Invoke(); + } + public void FullHeal() { if (!IsAlive) return; diff --git a/Assets/_Game/Scripts/Player/States/WallJumpState.cs b/Assets/_Game/Scripts/Player/States/WallJumpState.cs index f9eac87..f799656 100644 --- a/Assets/_Game/Scripts/Player/States/WallJumpState.cs +++ b/Assets/_Game/Scripts/Player/States/WallJumpState.cs @@ -38,6 +38,9 @@ namespace BaseGames.Player.States public override void OnStateEnter() { + // 蹬墙时解锁自动朝向(由 WallSlideState.OnStateExit 已解锁,这里保险再做一次) + Move?.LockFacing(false); + // 施加对应类型的速度 if (_isAwayJump) { diff --git a/Assets/_Game/Scripts/Player/States/WallSlideState.cs b/Assets/_Game/Scripts/Player/States/WallSlideState.cs index 322cde8..36e9a9f 100644 --- a/Assets/_Game/Scripts/Player/States/WallSlideState.cs +++ b/Assets/_Game/Scripts/Player/States/WallSlideState.cs @@ -62,6 +62,11 @@ namespace BaseGames.Player.States _lastGrabDir = _wallDir; } + // 锁定自动朝向,防止 LateUpdate 的 UpdateFacing 覆盖手动设置的朝向 + Move?.LockFacing(true); + // 抓墙时始终面朝墙壁,确保背墙跳的 FlipFacing(-_wallDir) 能正确翻转朝向 + Move?.FlipFacing(_wallDir); + // 计算当前是否处于正常模式 UpdateCanJump(); @@ -76,6 +81,7 @@ namespace BaseGames.Player.States public override void OnStateExit() { + Move?.LockFacing(false); Input.JumpStartedEvent -= OnJumpPressed; } @@ -98,6 +104,25 @@ namespace BaseGames.Player.States return; } + // ── 抓墙攻击:优先于方向键脱离检测,朝离墙方向翻转后进入空中攻击(无土狼时间)── + if (Buffer.ConsumeAttack()) + { + Move.FlipFacing(-_wallDir); + Owner.TransitionTo(Owner.GetState()); + return; + } + + // ── 抓墙冲刺:优先于方向键脱离检测,朝离墙方向翻转后冲出(无土狼时间)────────── + var ds = Owner.GetState(); + if (ds != null && ds.CanDashMidAir + && Stats != null && Stats.HasAbility(AbilityType.Dash) + && Buffer.ConsumeDash()) + { + Move.FlipFacing(-_wallDir); + Owner.TransitionTo(ds); + return; + } + // 按下方向键 → 启动墙壁土狼时间后主动脱离,自然下落 if (Input.MoveInput.y < -0.5f) { @@ -107,7 +132,6 @@ namespace BaseGames.Player.States } // 按反方向键 → 启动墙壁土狼时间后脱离 - // wall coyote 存在时,离墙后短窗口内仍可触发蹬墙跳,不会误双跳 float mx = Input.MoveInput.x; if (Mathf.Abs(mx) > 0.1f && (mx > 0f ? 1 : -1) != _wallDir) { diff --git a/Assets/_Game/Scripts/World/CrumblePlatform.cs b/Assets/_Game/Scripts/World/CrumblePlatform.cs index 77dcde9..50a1e34 100644 --- a/Assets/_Game/Scripts/World/CrumblePlatform.cs +++ b/Assets/_Game/Scripts/World/CrumblePlatform.cs @@ -18,6 +18,11 @@ namespace BaseGames.World [SerializeField] private SceneFeedback _crumbleFeedback; // 预警震动 + 碎裂粒子 + 音效 [SerializeField] private BoxCollider2D _passengerSensor; // IsTrigger,检测玩家踩踏 + [Header("持久化(_isOneShot = true 时生效)")] + [Tooltip("平台唯一 ID。_isOneShot=true 时碎裂状态写入 WorldStateRegistry,重载场景后不复原。留空则不持久化。")] + [SerializeField] private string _platformId; + [SerializeField] private WorldStateRegistry _worldState; + private BoxCollider2D _col; private SpriteRenderer _sr; private bool _isCrumbling; @@ -28,6 +33,18 @@ namespace BaseGames.World _sr = GetComponent(); } + private void Start() + { + // 读档恢复:_isOneShot 平台已碎裂则直接禁用,无需等待触发 + if (!_isOneShot || string.IsNullOrEmpty(_platformId) || _worldState == null) return; + if (!_worldState.HasFlag("crumble_" + _platformId)) return; + + _isCrumbling = true; + _col.enabled = false; + _sr.enabled = false; + if (_passengerSensor != null) _passengerSensor.enabled = false; + } + private void OnTriggerEnter2D(Collider2D other) { if (_isCrumbling) return; @@ -53,7 +70,12 @@ namespace BaseGames.World _passengerSensor.enabled = false; if (_isOneShot || _respawnDelay <= 0f) - yield break; // 永久消失 + { + // 持久化一次性碎裂状态,场景重载后不复原 + if (_isOneShot && !string.IsNullOrEmpty(_platformId)) + _worldState?.SetFlag("crumble_" + _platformId); + yield break; + } // 4. Respawn yield return new WaitForSeconds(_respawnDelay); diff --git a/Assets/_Game/Scripts/World/DirectionalInteractable.cs b/Assets/_Game/Scripts/World/DirectionalInteractable.cs index 34fde4b..7b9add3 100644 --- a/Assets/_Game/Scripts/World/DirectionalInteractable.cs +++ b/Assets/_Game/Scripts/World/DirectionalInteractable.cs @@ -51,10 +51,15 @@ namespace BaseGames.World // ── Physics Triggers ────────────────────────────────────────────────── + /// + /// 判断碰撞体是否为有效触发来源。子类可覆写以扩展触发主体(如幻影身体)。 + /// + protected virtual bool IsValidTriggerBody(Collider2D col) => col.CompareTag("Player"); + private void OnTriggerEnter2D(Collider2D other) { if (_triggerCondition != TriggerCondition.PlayerBody) return; - if (!other.CompareTag("Player")) return; + if (!IsValidTriggerBody(other)) return; if (!CheckSide(other.transform.position)) return; TryActivate(); } @@ -62,7 +67,7 @@ namespace BaseGames.World private void OnTriggerExit2D(Collider2D other) { if (_triggerCondition != TriggerCondition.PlayerBody) return; - if (!other.CompareTag("Player") || _isOneShot) return; + if (!IsValidTriggerBody(other) || _isOneShot) return; _activated = false; _deactivationChannel?.Raise(); } @@ -106,13 +111,14 @@ namespace BaseGames.World private void Start() { - // 读档恢复:若机关已激活则静默还原 + // 读档恢复:仅标记 _activated = true,不广播激活事件。 + // 下游组件(PuzzleReceiver / 动画门等)已通过各自的 WorldStateRegistry 检查在 Start 中自行恢复, + // 重复广播会导致它们再次播放开门动画等副作用。 if (_isOneShot && !string.IsNullOrEmpty(_interactableId) && _worldState != null && _worldState.HasFlag("mechanism_" + _interactableId)) { _activated = true; - _activationChannel?.Raise(); } } } diff --git a/Assets/_Game/Scripts/World/HazardZone.cs b/Assets/_Game/Scripts/World/HazardZone.cs index 21cf253..9bf3593 100644 --- a/Assets/_Game/Scripts/World/HazardZone.cs +++ b/Assets/_Game/Scripts/World/HazardZone.cs @@ -4,36 +4,67 @@ using UnityEngine; namespace BaseGames.World { /// - /// 危险区域。玩家进入时触发即死或持续伤害。 + /// 危险区域。玩家进入时触发即死(调用 Kill(),无视无敌帧)或持续伤害。 /// [RequireComponent(typeof(Collider2D))] public class HazardZone : MonoBehaviour { - [SerializeField] private bool _isInstantKill = true; - [SerializeField] private int _damage = 9999; + [SerializeField] private bool _isInstantKill = true; + [SerializeField] private int _damage = 1; + [Tooltip("持续伤害模式下每次造成伤害的间隔(秒)。仅 _isInstantKill = false 时生效。")] + [SerializeField] private float _damageInterval = 0.5f; + + private PlayerStats _cachedStats; + private float _damageTimer; + private bool _playerInside; private void OnTriggerEnter2D(Collider2D other) { if (!other.CompareTag("Player")) return; + _cachedStats = other.GetComponentInParent(); + if (_cachedStats == null) return; - var stats = other.GetComponentInParent(); - if (stats == null) return; + _playerInside = true; + ApplyDamage(); + _damageTimer = _damageInterval; + } + private void OnTriggerExit2D(Collider2D other) + { + if (!other.CompareTag("Player")) return; + _playerInside = false; + _cachedStats = null; + } + + private void Update() + { + if (!_playerInside || _isInstantKill || _cachedStats == null) return; + + _damageTimer -= Time.deltaTime; + if (_damageTimer <= 0f) + { + ApplyDamage(); + _damageTimer = _damageInterval; + } + } + + private void ApplyDamage() + { + if (_cachedStats == null) return; if (_isInstantKill) - stats.TakeDamage(stats.MaxHP * 2); // 确保即死(超过最大血量) + _cachedStats.Kill(); else - stats.TakeDamage(_damage); + _cachedStats.TakeDamage(_damage); } private void OnDrawGizmos() { - Gizmos.color = new Color(1f, 0f, 0f, 0.3f); var col = GetComponent(); - if (col != null) - Gizmos.DrawCube(transform.position, col.bounds.size); + if (col == null) return; + Gizmos.color = new Color(1f, 0f, 0f, 0.3f); + Gizmos.DrawCube(transform.position, col.bounds.size); Gizmos.color = new Color(1f, 0f, 0f, 0.8f); - if (col != null) - Gizmos.DrawWireCube(transform.position, col.bounds.size); + Gizmos.DrawWireCube(transform.position, col.bounds.size); } } } diff --git a/Assets/_Game/Scripts/World/InteractableDetector.cs b/Assets/_Game/Scripts/World/InteractableDetector.cs index bbbabde..63109d6 100644 --- a/Assets/_Game/Scripts/World/InteractableDetector.cs +++ b/Assets/_Game/Scripts/World/InteractableDetector.cs @@ -1,3 +1,4 @@ +using System.Collections.Generic; using BaseGames.Core.Events; using BaseGames.Input; using UnityEngine; @@ -22,8 +23,19 @@ namespace BaseGames.World // 预分配检测缓冲区,避免 OverlapCircleAll 每帧 GC 分配 private readonly Collider2D[] _overlapBuffer = new Collider2D[16]; - private void OnEnable() => _inputReader.InteractEvent += TryInteract; - private void OnDisable() => _inputReader.InteractEvent -= TryInteract; + // Collider → IInteractable 缓存,避免 FindNearest 每帧重复 GetComponentInParent + private readonly Dictionary _componentCache = new(); + + private void OnEnable() + { + _inputReader.InteractEvent += TryInteract; + } + + private void OnDisable() + { + _inputReader.InteractEvent -= TryInteract; + _componentCache.Clear(); // 清理缓存,防止跨场景持有旧引用 + } private void Update() { @@ -57,20 +69,29 @@ namespace BaseGames.World private IInteractable FindNearest(Collider2D[] hits, int count) { - IInteractable best = null; - float bestDist = float.MaxValue; + IInteractable best = null; + float bestSqrDist = float.MaxValue; for (int i = 0; i < count; i++) { var col = hits[i]; - var interactable = col.GetComponentInParent(); + if (col == null) continue; + + // 查缓存,未命中时才调用 GetComponentInParent(避免每帧反射开销) + if (!_componentCache.TryGetValue(col, out var interactable)) + { + interactable = col.GetComponentInParent(); + _componentCache[col] = interactable; + } + if (interactable == null || !interactable.CanInteract) continue; - float dist = Vector2.Distance(transform.position, col.transform.position); - if (dist < bestDist) + // 用 sqrMagnitude 比较距离,省去 Distance 的 sqrt 开销 + float sqrDist = ((Vector2)transform.position - (Vector2)col.transform.position).sqrMagnitude; + if (sqrDist < bestSqrDist) { - bestDist = dist; - best = interactable; + bestSqrDist = sqrDist; + best = interactable; } } diff --git a/Assets/_Game/Scripts/World/MovingPlatform.cs b/Assets/_Game/Scripts/World/MovingPlatform.cs index 26faded..75f0829 100644 --- a/Assets/_Game/Scripts/World/MovingPlatform.cs +++ b/Assets/_Game/Scripts/World/MovingPlatform.cs @@ -146,6 +146,7 @@ namespace BaseGames.World if (_moveType == MoveType.LinearAB) { + if (_wayPoints.Length < 2) return; // 至少需要两个路径点 _movingForward = !_movingForward; _waypointIndex = _movingForward ? 1 : 0; } diff --git a/Assets/_Game/Scripts/World/PhantomInteractable.cs b/Assets/_Game/Scripts/World/PhantomInteractable.cs index 1a0b49e..b9754f7 100644 --- a/Assets/_Game/Scripts/World/PhantomInteractable.cs +++ b/Assets/_Game/Scripts/World/PhantomInteractable.cs @@ -4,6 +4,7 @@ namespace BaseGames.World { /// /// 幻影可交互机关。继承 DirectionalInteractable,额外响应 PhantomBody 层(太虚斩形态)。 + /// 通过覆写 IsValidTriggerBody 扩展触发主体,_triggerCondition 条件检查由父类统一处理。 /// public class PhantomInteractable : DirectionalInteractable { @@ -11,13 +12,7 @@ namespace BaseGames.World private void Awake() => _phantomBodyLayer = LayerMask.NameToLayer("PhantomBody"); - private void OnTriggerEnter2D(Collider2D other) - { - bool isPlayer = other.CompareTag("Player"); - bool isPhantom = other.gameObject.layer == _phantomBodyLayer; - - if (!isPlayer && !isPhantom) return; - TryActivate(); - } + protected override bool IsValidTriggerBody(Collider2D col) + => col.CompareTag("Player") || col.gameObject.layer == _phantomBodyLayer; } } diff --git a/Assets/_Game/Scripts/World/Puzzle/PuzzleSwitch.cs b/Assets/_Game/Scripts/World/Puzzle/PuzzleSwitch.cs index 2840ff5..8dae4c6 100644 --- a/Assets/_Game/Scripts/World/Puzzle/PuzzleSwitch.cs +++ b/Assets/_Game/Scripts/World/Puzzle/PuzzleSwitch.cs @@ -2,6 +2,7 @@ using System; using Animancer; using BaseGames.Feedback; +using BaseGames.Input; using BaseGames.World; using UnityEngine; @@ -38,6 +39,9 @@ namespace BaseGames.Puzzle [Header("持久化(SO 注入,非 Instance 单例)")] [SerializeField] private WorldStateRegistry _worldState; + [Header("Hold 模式输入(SwitchTriggerMode.Hold 时必填)")] + [SerializeField] private InputReaderSO _inputReader; + private bool _isActive; public bool IsActive => _isActive; @@ -59,16 +63,41 @@ namespace BaseGames.Puzzle // ── IInteractable ──────────────────────────────────────────────────── public string InteractPrompt => _mode == SwitchTriggerMode.Hold ? "按住交互" : "交互"; - public bool CanInteract => true; + + /// + /// 压板模式不需要交互提示(物理触发);InteractOnce 已激活后隐藏提示。 + /// + public bool CanInteract => _mode switch + { + SwitchTriggerMode.InteractOnce => !_isActive, + SwitchTriggerMode.InteractToggle => true, + SwitchTriggerMode.Hold => true, + SwitchTriggerMode.Pressure => false, + _ => false, + }; public void Interact(Transform player) { + // Hold 模式通过 OnPlayerEnterRange 订阅输入事件处理,此处不响应 + if (_mode == SwitchTriggerMode.Hold) return; if (_mode == SwitchTriggerMode.InteractOnce && _isActive) return; SetState(!_isActive); } - public void OnPlayerEnterRange(Transform player) { } - public void OnPlayerExitRange() { } + public void OnPlayerEnterRange(Transform player) + { + if (_mode != SwitchTriggerMode.Hold || _inputReader == null) return; + _inputReader.InteractEvent += OnHoldStarted; + _inputReader.InteractCancelledEvent += OnHoldCancelled; + } + + public void OnPlayerExitRange() + { + UnsubscribeHold(); + // 玩家离开范围时停用 Hold 开关 + if (_mode == SwitchTriggerMode.Hold && _isActive) + SetState(false); + } // ── ISwitchable ────────────────────────────────────────────────────── public void ForceState(bool active) => SetState(active); @@ -106,5 +135,19 @@ namespace BaseGames.Puzzle else _worldState?.ClearFlag("switch_" + _switchId); } } + + // ── Hold 模式辅助 ────────────────────────────────────────────────────── + + private void OnHoldStarted() => SetState(true); + private void OnHoldCancelled() => SetState(false); + + private void UnsubscribeHold() + { + if (_inputReader == null) return; + _inputReader.InteractEvent -= OnHoldStarted; + _inputReader.InteractCancelledEvent -= OnHoldCancelled; + } + + private void OnDestroy() => UnsubscribeHold(); } }