Compare commits

...

2 Commits

Author SHA1 Message Date
285ac46e31 优化 DiHun 武器和角色表单的属性,更新击打盒 ID 和颜色设置 2026-05-22 07:07:10 +08:00
47bdc67cdf feat: Implement DownDash ability and related systems
- Added DownDash ability with cooldown and speed configuration.
- Introduced DownDashState to handle down dashing mechanics, including gravity manipulation and animation playback.
- Updated PlayerMovement to support DownDash functionality.
- Enhanced PlayerStats to manage spring charge consumption and healing.
- Modified PlayerCombat and WeaponHitBoxInstance to support new hit confirmation events.
- Updated AbilityType to include new form types for character abilities.
- Improved Gizmos for better visualization of enemy detection and attack ranges.
- Added feedback systems for form switching in PlayerFeedback and IFeedbackPlayer.
- Refactored combat and movement states to accommodate new abilities and ensure smooth transitions.
2026-05-22 00:09:50 +08:00
33 changed files with 476 additions and 162 deletions

View File

@@ -28,13 +28,13 @@ MonoBehaviour:
_NormalizedStartTime: NaN _NormalizedStartTime: NaN
damageSource: {fileID: 11400000, guid: a0e13ba45445ebe408ef0353f5adcbfb, type: 2} damageSource: {fileID: 11400000, guid: a0e13ba45445ebe408ef0353f5adcbfb, type: 2}
hitBoxEnter: 0 hitBoxEnter: 0
hitBoxExit: 0 hitBoxExit: 0.092
comboInputOpen: 0 comboInputOpen: 0
comboInputClose: 0 comboInputClose: 1
cancelWindowOpen: 0 cancelWindowOpen: 0
recoveryTime: 0 recoveryTime: 0
comboTimeout: 0 comboTimeout: 0
hitBoxId: hitBoxId: ATK_Ground_1
- clip: - clip:
_FadeDuration: 0.25 _FadeDuration: 0.25
_Speed: 1 _Speed: 1
@@ -48,11 +48,11 @@ MonoBehaviour:
hitBoxEnter: 0.032 hitBoxEnter: 0.032
hitBoxExit: 0.669 hitBoxExit: 0.669
comboInputOpen: 0.057 comboInputOpen: 0.057
comboInputClose: 0.292 comboInputClose: 1
cancelWindowOpen: 0.061 cancelWindowOpen: 0.061
recoveryTime: 0 recoveryTime: 0
comboTimeout: 0 comboTimeout: 0
hitBoxId: hitBoxId: ATK_Ground_2
airComboSteps: airComboSteps:
- clip: - clip:
_FadeDuration: 0.25 _FadeDuration: 0.25
@@ -61,17 +61,17 @@ MonoBehaviour:
_NormalizedTimes: [] _NormalizedTimes: []
_Callbacks: [] _Callbacks: []
_Names: [] _Names: []
_Clip: {fileID: 0} _Clip: {fileID: 7400000, guid: bde6a54ec8fc04f45a424f9bcd7845df, type: 2}
_NormalizedStartTime: NaN _NormalizedStartTime: NaN
damageSource: {fileID: 0} damageSource: {fileID: 0}
hitBoxEnter: 0.1 hitBoxEnter: 0.1
hitBoxExit: 0.8 hitBoxExit: 0.8
comboInputOpen: 0 comboInputOpen: 0
comboInputClose: 0 comboInputClose: 1
cancelWindowOpen: 0 cancelWindowOpen: 0
recoveryTime: 0.05 recoveryTime: 0.05
comboTimeout: 0 comboTimeout: 0
hitBoxId: hitBoxId: ATK_Air_1
- clip: - clip:
_FadeDuration: 0.25 _FadeDuration: 0.25
_Speed: 1 _Speed: 1
@@ -79,17 +79,17 @@ MonoBehaviour:
_NormalizedTimes: [] _NormalizedTimes: []
_Callbacks: [] _Callbacks: []
_Names: [] _Names: []
_Clip: {fileID: 0} _Clip: {fileID: 7400000, guid: c0ef319f521c2a2448022cebcf9c9266, type: 2}
_NormalizedStartTime: NaN _NormalizedStartTime: NaN
damageSource: {fileID: 0} damageSource: {fileID: 0}
hitBoxEnter: 0.1 hitBoxEnter: 0.1
hitBoxExit: 0.8 hitBoxExit: 0.8
comboInputOpen: 0 comboInputOpen: 0.026
comboInputClose: 0 comboInputClose: 0.426
cancelWindowOpen: 0 cancelWindowOpen: 0.019
recoveryTime: 0.05 recoveryTime: 0.05
comboTimeout: 0 comboTimeout: 0
hitBoxId: hitBoxId: ATK_Air_2
upStep: upStep:
clip: clip:
_FadeDuration: 0.25 _FadeDuration: 0.25
@@ -98,7 +98,7 @@ MonoBehaviour:
_NormalizedTimes: [] _NormalizedTimes: []
_Callbacks: [] _Callbacks: []
_Names: [] _Names: []
_Clip: {fileID: 0} _Clip: {fileID: 7400000, guid: baae11fb27958444d911f8d637448184, type: 2}
_NormalizedStartTime: NaN _NormalizedStartTime: NaN
damageSource: {fileID: 0} damageSource: {fileID: 0}
hitBoxEnter: 0.2 hitBoxEnter: 0.2
@@ -108,7 +108,7 @@ MonoBehaviour:
cancelWindowOpen: 0 cancelWindowOpen: 0
recoveryTime: 0.05 recoveryTime: 0.05
comboTimeout: 0 comboTimeout: 0
hitBoxId: hitBoxId: ATK_Up
downStep: downStep:
clip: clip:
_FadeDuration: 0.25 _FadeDuration: 0.25
@@ -117,7 +117,7 @@ MonoBehaviour:
_NormalizedTimes: [] _NormalizedTimes: []
_Callbacks: [] _Callbacks: []
_Names: [] _Names: []
_Clip: {fileID: 0} _Clip: {fileID: 7400000, guid: d199f06821d6b92478f9fd7c3f0cd11d, type: 2}
_NormalizedStartTime: NaN _NormalizedStartTime: NaN
damageSource: {fileID: 0} damageSource: {fileID: 0}
hitBoxEnter: 0.1 hitBoxEnter: 0.1
@@ -127,7 +127,7 @@ MonoBehaviour:
cancelWindowOpen: 0 cancelWindowOpen: 0
recoveryTime: 0.05 recoveryTime: 0.05
comboTimeout: 0 comboTimeout: 0
hitBoxId: hitBoxId: ATK_Down
hitBoxPrefab: {fileID: 4821376343125962025, guid: 2fce42e66b8d4a042a471a661a05ee1b, type: 3} hitBoxPrefab: {fileID: 4821376343125962025, guid: 2fce42e66b8d4a042a471a661a05ee1b, type: 3}
vfxConfig: vfxConfig:
onEquipPresetId: onEquipPresetId:

View File

@@ -16,4 +16,4 @@ MonoBehaviour:
displayName: "\u5730\u9B42" displayName: "\u5730\u9B42"
formType: 1 formType: 1
defaultWeapon: {fileID: 11400000, guid: bde7d85bdf2d3e54da22d07b1f8d2901, type: 2} defaultWeapon: {fileID: 11400000, guid: bde7d85bdf2d3e54da22d07b1f8d2901, type: 2}
formAccentColor: {r: 1, g: 1, b: 1, a: 1} formAccentColor: {r: 0.9433962, g: 0.8825333, b: 0.15574935, a: 1}

View File

@@ -16,4 +16,4 @@ MonoBehaviour:
displayName: "\u547D\u9B42" displayName: "\u547D\u9B42"
formType: 2 formType: 2
defaultWeapon: {fileID: 11400000, guid: fbe1ff6f23c995541a5833b51c52dc01, type: 2} defaultWeapon: {fileID: 11400000, guid: fbe1ff6f23c995541a5833b51c52dc01, type: 2}
formAccentColor: {r: 1, g: 1, b: 1, a: 1} formAccentColor: {r: 0.9150943, g: 0.15970986, b: 0.15970986, a: 1}

View File

@@ -16,4 +16,4 @@ MonoBehaviour:
displayName: "\u5929\u9B42" displayName: "\u5929\u9B42"
formType: 0 formType: 0
defaultWeapon: {fileID: 11400000, guid: 533d4711e21d8584597a5d4569fe2eb0, type: 2} defaultWeapon: {fileID: 11400000, guid: 533d4711e21d8584597a5d4569fe2eb0, type: 2}
formAccentColor: {r: 1, g: 1, b: 1, a: 1} formAccentColor: {r: 0.23740657, g: 0.8709677, b: 0.9150943, a: 1}

View File

@@ -21,4 +21,4 @@ MonoBehaviour:
SpringKillThreshold: 4 SpringKillThreshold: 4
InvincibilityDuration: 0.6 InvincibilityDuration: 0.6
InitialLingZhu: 0 InitialLingZhu: 0
InitialAbilities: 0 InitialAbilities: 4194295

View File

@@ -92,10 +92,10 @@ MonoBehaviour:
m_EditorClassIdentifier: m_EditorClassIdentifier:
_defaultSource: {fileID: 0} _defaultSource: {fileID: 0}
_hitCooldown: 0.1 _hitCooldown: 0.1
_id: _id: ATK_Ground_1
_rivalHitBoxMask: _rivalHitBoxMask:
serializedVersion: 2 serializedVersion: 2
m_Bits: 32832 m_Bits: 134217792
--- !u!1 &1932889250901504761 --- !u!1 &1932889250901504761
GameObject: GameObject:
m_ObjectHideFlags: 0 m_ObjectHideFlags: 0
@@ -188,10 +188,10 @@ MonoBehaviour:
m_EditorClassIdentifier: m_EditorClassIdentifier:
_defaultSource: {fileID: 0} _defaultSource: {fileID: 0}
_hitCooldown: 0.1 _hitCooldown: 0.1
_id: _id: ATK_Down
_rivalHitBoxMask: _rivalHitBoxMask:
serializedVersion: 2 serializedVersion: 2
m_Bits: 0 m_Bits: 134217792
--- !u!1 &2584603199706918030 --- !u!1 &2584603199706918030
GameObject: GameObject:
m_ObjectHideFlags: 0 m_ObjectHideFlags: 0
@@ -284,10 +284,10 @@ MonoBehaviour:
m_EditorClassIdentifier: m_EditorClassIdentifier:
_defaultSource: {fileID: 0} _defaultSource: {fileID: 0}
_hitCooldown: 0.1 _hitCooldown: 0.1
_id: _id: ATK_Ground_2
_rivalHitBoxMask: _rivalHitBoxMask:
serializedVersion: 2 serializedVersion: 2
m_Bits: 32832 m_Bits: 134217792
--- !u!1 &4050057806632877121 --- !u!1 &4050057806632877121
GameObject: GameObject:
m_ObjectHideFlags: 0 m_ObjectHideFlags: 0
@@ -380,10 +380,10 @@ MonoBehaviour:
m_EditorClassIdentifier: m_EditorClassIdentifier:
_defaultSource: {fileID: 0} _defaultSource: {fileID: 0}
_hitCooldown: 0.1 _hitCooldown: 0.1
_id: _id: ATK_Air_2
_rivalHitBoxMask: _rivalHitBoxMask:
serializedVersion: 2 serializedVersion: 2
m_Bits: 0 m_Bits: 134217792
--- !u!1 &4335406389674002762 --- !u!1 &4335406389674002762
GameObject: GameObject:
m_ObjectHideFlags: 0 m_ObjectHideFlags: 0
@@ -476,10 +476,10 @@ MonoBehaviour:
m_EditorClassIdentifier: m_EditorClassIdentifier:
_defaultSource: {fileID: 0} _defaultSource: {fileID: 0}
_hitCooldown: 0.1 _hitCooldown: 0.1
_id: _id: ATK_Up
_rivalHitBoxMask: _rivalHitBoxMask:
serializedVersion: 2 serializedVersion: 2
m_Bits: 0 m_Bits: 134217792
--- !u!1 &4821376343125962025 --- !u!1 &4821376343125962025
GameObject: GameObject:
m_ObjectHideFlags: 0 m_ObjectHideFlags: 0
@@ -626,7 +626,7 @@ MonoBehaviour:
m_EditorClassIdentifier: m_EditorClassIdentifier:
_defaultSource: {fileID: 0} _defaultSource: {fileID: 0}
_hitCooldown: 0.1 _hitCooldown: 0.1
_id: _id: ATK_Air_1
_rivalHitBoxMask: _rivalHitBoxMask:
serializedVersion: 2 serializedVersion: 2
m_Bits: 0 m_Bits: 134217792

View File

@@ -61,6 +61,7 @@ namespace BaseGames.Combat
_currentSource = source ?? _defaultSource; _currentSource = source ?? _defaultSource;
_attackerTransform = attacker ?? transform; _attackerTransform = attacker ?? transform;
_isActive = true; _isActive = true;
_collider.enabled = true;
// 缓存宿主 Rigidbody2D沿父层级向上查找 // 缓存宿主 Rigidbody2D沿父层级向上查找
_ownerRigidbody = _attackerTransform.GetComponentInParent<Rigidbody2D>(); _ownerRigidbody = _attackerTransform.GetComponentInParent<Rigidbody2D>();
// 每次激活清空当前激活期已命中目标集合(防止连击连段导致同一阶段多次命中目标) // 每次激活清空当前激活期已命中目标集合(防止连击连段导致同一阶段多次命中目标)
@@ -70,7 +71,8 @@ namespace BaseGames.Combat
public void Deactivate() public void Deactivate()
{ {
_isActive = false; _isActive = false;
_collider.enabled = false;
_hitThisActivation.Clear(); _hitThisActivation.Clear();
_hitCooldownTimers.Clear(); _hitCooldownTimers.Clear();
} }
@@ -87,6 +89,8 @@ namespace BaseGames.Combat
_collider = GetComponent<Collider2D>(); _collider = GetComponent<Collider2D>();
if (!_collider.isTrigger) if (!_collider.isTrigger)
Debug.LogWarning($"[HitBox] {name}: Collider2D.isTrigger 应为 true。", this); Debug.LogWarning($"[HitBox] {name}: Collider2D.isTrigger 应为 true。", this);
// 初始状态关闭碰撞体,防止未激活时产生物理检测
_collider.enabled = false;
// 缓存 IClashServiceOnTriggerEnter2D 为物理热路径,避免每次调用 Dictionary 查找 // 缓存 IClashServiceOnTriggerEnter2D 为物理热路径,避免每次调用 Dictionary 查找
_clashService = ServiceLocator.GetOrDefault<IClashService>(); _clashService = ServiceLocator.GetOrDefault<IClashService>();
// 缓存宿主投射物(仅 Projectile GameObject 上挂载的 HitBox 非 null // 缓存宿主投射物(仅 Projectile GameObject 上挂载的 HitBox 非 null
@@ -96,6 +100,7 @@ namespace BaseGames.Combat
private void OnDisable() private void OnDisable()
{ {
_isActive = false; _isActive = false;
if (_collider != null) _collider.enabled = false;
_hitThisActivation.Clear(); _hitThisActivation.Clear();
_hitCooldownTimers.Clear(); _hitCooldownTimers.Clear();
} }
@@ -180,116 +185,122 @@ namespace BaseGames.Combat
{ {
var col = GetComponent<Collider2D>(); var col = GetComponent<Collider2D>();
if (col == null) return; if (col == null) return;
// HitBox激活 = 鲜红,非激活 = 极淡红轮廓 Color fill = _isActive ? new Color(1f, 0.15f, 0.15f, 0.25f) : new Color(1f, 0.15f, 0.15f, 0.05f);
Gizmos.color = _isActive Color outline = _isActive ? new Color(1f, 0.15f, 0.15f, 0.90f) : new Color(1f, 0.15f, 0.15f, 0.25f);
? new Color(1f, 0.15f, 0.15f, 1f) DrawCollider2DFilled(col, fill, outline);
: new Color(1f, 0.15f, 0.15f, 0.2f);
DrawCollider2DWire(col);
} }
// ──────────────────────────────────────────────────── // ── Gizmo 辅助(填充 + 轮廓,不依赖外部工具类)──────────────────────────
// Gizmo 辅助(内联,不依赖外部工具类)
// ──────────────────────────────────────────────────── /// <summary>根据 Collider2D 类型绘制带填充色和轮廓的 2D Gizmo供 HurtBox 等复用)。</summary>
/// <summary> public static void DrawCollider2DFilled(Collider2D col, Color fill, Color outline)
/// 根据 Collider2D 类型绘制正确的 wire 轮廓。
/// 使用 <see cref="Gizmos.matrix"/> 统一应用 Transform 的移动/旋转/缩放。
/// </summary>
public static void DrawCollider2DWire(Collider2D col)
{ {
var prev = Gizmos.matrix; var prevMatrix = UnityEditor.Handles.matrix;
Gizmos.matrix = col.transform.localToWorldMatrix; UnityEditor.Handles.matrix = col.transform.localToWorldMatrix;
switch (col) switch (col)
{ {
case BoxCollider2D box: case BoxCollider2D box:
DrawWireRect2D(box.offset, box.size); DrawFilledRect2D(box.offset, box.size, fill, outline);
break;
case CircleCollider2D circle:
DrawFilledCircle2D(circle.offset, circle.radius, fill, outline);
break; break;
case CapsuleCollider2D caps: case CapsuleCollider2D caps:
DrawWireCapsule2D(caps.offset, caps.size, caps.direction); DrawFilledCapsule2D(caps.offset, caps.size, caps.direction, fill, outline);
break; break;
case PolygonCollider2D poly: case PolygonCollider2D poly:
for (int p = 0; p < poly.pathCount; p++) for (int p = 0; p < poly.pathCount; p++)
{ DrawFilledPolygonPath2D(poly.GetPath(p), fill, outline);
var pts = poly.GetPath(p);
for (int i = 0; i < pts.Length; i++)
Gizmos.DrawLine(pts[i], pts[(i + 1) % pts.Length]);
}
break;
case CircleCollider2D circle:
DrawWireCircle2D(circle.offset, circle.radius);
break; break;
default: default:
// 其他类型回退到 bounds 近似(恢复矩阵后在世界空间绘制) UnityEditor.Handles.matrix = Matrix4x4.identity;
Gizmos.matrix = prev; DrawFilledRect2D(col.bounds.center, col.bounds.size, fill, outline);
DrawWireRect2D(col.bounds.center, col.bounds.size); break;
return;
} }
Gizmos.matrix = prev; UnityEditor.Handles.matrix = prevMatrix;
} }
/// <summary>在当前 Gizmos 坐标系中绘制轴对齐矩形2D 线框,兼容透视相机)。</summary> /// <summary>向后兼容的线框接口(内部改调 DrawCollider2DFilled)。</summary>
public static void DrawWireRect2D(Vector2 center, Vector2 size) public static void DrawCollider2DWire(Collider2D col)
=> DrawCollider2DFilled(col, new Color(1f, 1f, 1f, 0.05f), new Color(1f, 1f, 1f, 0.8f));
private static void DrawFilledRect2D(Vector2 center, Vector2 size, Color fill, Color outline)
{ {
float hx = size.x * 0.5f, hy = size.y * 0.5f; float hx = size.x * 0.5f, hy = size.y * 0.5f;
Vector3 tl = new Vector3(center.x - hx, center.y + hy, 0f); var verts = new Vector3[]
Vector3 tr = new Vector3(center.x + hx, center.y + hy, 0f);
Vector3 br = new Vector3(center.x + hx, center.y - hy, 0f);
Vector3 bl = new Vector3(center.x - hx, center.y - hy, 0f);
Gizmos.DrawLine(tl, tr);
Gizmos.DrawLine(tr, br);
Gizmos.DrawLine(br, bl);
Gizmos.DrawLine(bl, tl);
}
/// <summary>用线段近似绘制 2D 圆周(不使用 DrawWireSphere兼容透视相机。</summary>
public static void DrawWireCircle2D(Vector3 center, float radius, int segs = 32)
{
float step = 360f / segs;
Vector3 prevPt = center + new Vector3(radius, 0f, 0f);
for (int i = 1; i <= segs; i++)
{ {
float a = i * step * Mathf.Deg2Rad; new Vector3(center.x - hx, center.y + hy, 0f),
Vector3 nextPt = center + new Vector3(Mathf.Cos(a) * radius, Mathf.Sin(a) * radius, 0f); new Vector3(center.x + hx, center.y + hy, 0f),
Gizmos.DrawLine(prevPt, nextPt); new Vector3(center.x + hx, center.y - hy, 0f),
prevPt = nextPt; new Vector3(center.x - hx, center.y - hy, 0f),
} };
UnityEditor.Handles.DrawSolidRectangleWithOutline(verts, fill, outline);
} }
/// <summary>绘制 2D 胶囊轮廓(在 col.transform 局部坐标系中)。</summary> private static void DrawFilledCircle2D(Vector2 center, float radius, Color fill, Color outline)
private static void DrawWireCapsule2D(Vector2 offset, Vector2 size, CapsuleDirection2D dir)
{ {
bool vert = dir == CapsuleDirection2D.Vertical; Vector3 c = new Vector3(center.x, center.y, 0f);
float radius = vert ? size.x * 0.5f : size.y * 0.5f; UnityEditor.Handles.color = fill;
float half = Mathf.Max(0f, (vert ? size.y : size.x) * 0.5f - radius); UnityEditor.Handles.DrawSolidDisc(c, Vector3.back, radius);
Vector2 axis = vert ? Vector2.up : Vector2.right; UnityEditor.Handles.color = outline;
Vector2 perp = vert ? Vector2.right : Vector2.up; UnityEditor.Handles.DrawWireDisc(c, Vector3.back, radius);
Vector2 capA = offset + axis * half;
Vector2 capB = offset - axis * half;
Gizmos.DrawLine(capA + perp * radius, capB + perp * radius);
Gizmos.DrawLine(capA - perp * radius, capB - perp * radius);
DrawArc2D(capA, radius, vert ? 0f : -90f, 180f);
DrawArc2D(capB, radius, vert ? 180f : 90f, 180f);
} }
/// <summary>用多段直线近似绘制 2D 圆弧。</summary> private static readonly System.Collections.Generic.List<Vector3> _capsuleVertsBuf
private static void DrawArc2D(Vector2 c, float r, float startDeg, float spanDeg, int segs = 20) = new System.Collections.Generic.List<Vector3>(64);
private static void DrawFilledCapsule2D(Vector2 offset, Vector2 size,
CapsuleDirection2D dir, Color fill, Color outline)
{ {
float step = spanDeg / segs; bool vert = dir == CapsuleDirection2D.Vertical;
var prev = c + new Vector2(Mathf.Cos(startDeg * Mathf.Deg2Rad) * r, float radius = vert ? size.x * 0.5f : size.y * 0.5f;
Mathf.Sin(startDeg * Mathf.Deg2Rad) * r); float half = Mathf.Max(0f, (vert ? size.y : size.x) * 0.5f - radius);
for (int i = 1; i <= segs; i++) Vector2 axis = vert ? Vector2.up : Vector2.right;
Vector2 capA = offset + axis * half; // 顶部(竖向)或右端(横向)圆心
Vector2 capB = offset - axis * half; // 底部(竖向)或左端(横向)圆心
const int segs = 12;
_capsuleVertsBuf.Clear();
float baseA = vert ? 0f : -Mathf.PI * 0.5f;
for (int i = 0; i <= segs; i++)
{ {
float a = (startDeg + step * i) * Mathf.Deg2Rad; float a = baseA + (float)i / segs * Mathf.PI;
var next = c + new Vector2(Mathf.Cos(a) * r, Mathf.Sin(a) * r); _capsuleVertsBuf.Add(new Vector3(capA.x + Mathf.Cos(a) * radius,
Gizmos.DrawLine(prev, next); capA.y + Mathf.Sin(a) * radius, 0f));
prev = next;
} }
float baseB = vert ? Mathf.PI : Mathf.PI * 0.5f;
for (int i = 0; i <= segs; i++)
{
float a = baseB + (float)i / segs * Mathf.PI;
_capsuleVertsBuf.Add(new Vector3(capB.x + Mathf.Cos(a) * radius,
capB.y + Mathf.Sin(a) * radius, 0f));
}
var arr = _capsuleVertsBuf.ToArray();
UnityEditor.Handles.color = fill;
UnityEditor.Handles.DrawAAConvexPolygon(arr);
UnityEditor.Handles.color = outline;
for (int i = 0; i < arr.Length; i++)
UnityEditor.Handles.DrawLine(arr[i], arr[(i + 1) % arr.Length]);
}
private static void DrawFilledPolygonPath2D(Vector2[] path, Color fill, Color outline)
{
if (path == null || path.Length < 3) return;
var verts = System.Array.ConvertAll(path, p => new Vector3(p.x, p.y, 0f));
UnityEditor.Handles.color = fill;
UnityEditor.Handles.DrawAAConvexPolygon(verts); // 凹多边形仅轮廓准确,填充近似
UnityEditor.Handles.color = outline;
for (int i = 0; i < verts.Length; i++)
UnityEditor.Handles.DrawLine(verts[i], verts[(i + 1) % verts.Length]);
} }
#endif #endif
} }

View File

@@ -123,13 +123,23 @@ namespace BaseGames.Combat
{ {
var col = GetComponent<Collider2D>(); var col = GetComponent<Collider2D>();
if (col == null) return; if (col == null) return;
// HurtBox正常激活 = 青色,无敌 = 黄色,非激活 = 极淡青 Color fill, outline;
Gizmos.color = !_isActive if (!_isActive)
? new Color(0f, 0.85f, 1f, 0.15f) {
: _isHurtBoxInvincible fill = new Color(0f, 0.85f, 1f, 0.05f);
? new Color(1f, 1f, 0f, 1f ) outline = new Color(0f, 0.85f, 1f, 0.20f);
: new Color(0f, 0.85f, 1f, 1f ); }
HitBox.DrawCollider2DWire(col); else if (_isHurtBoxInvincible)
{
fill = new Color(1f, 1f, 0f, 0.25f);
outline = new Color(1f, 1f, 0f, 0.90f);
}
else
{
fill = new Color(0f, 0.85f, 1f, 0.20f);
outline = new Color(0f, 0.85f, 1f, 0.90f);
}
HitBox.DrawCollider2DFilled(col, fill, outline);
} }
#endif #endif
} }

View File

@@ -13,7 +13,8 @@ namespace BaseGames.Editor
/// 分组: /// 分组:
/// 移动能力 — WallCling / WallJump / Dash / AirDash / DoubleJump / SuperJump / Swim / DownDash /// 移动能力 — WallCling / WallJump / Dash / AirDash / DoubleJump / SuperJump / Swim / DownDash
/// 法术能力 — Spell1 / Spell2 / Spell3 /// 法术能力 — Spell1 / Spell2 / Spell3
/// 形态能力 — SpiritForm / SpiritDash /// 灵魄形态 — SpiritForm / SpiritDash
/// 三魂形态 — FormTianHun / FormDiHun / FormMingHun
/// 战斗能力 — Parry / ChargeAttack / DownSlash /// 战斗能力 — Parry / ChargeAttack / DownSlash
/// 互动能力 — Interact / FastTravel /// 互动能力 — Interact / FastTravel
/// 能力强化 — InvincibleDash /// 能力强化 — InvincibleDash
@@ -46,6 +47,12 @@ namespace BaseGames.Editor
(AbilityType.SpiritForm, "灵魄形态"), (AbilityType.SpiritForm, "灵魄形态"),
(AbilityType.SpiritDash, "灵魄冲刺"), (AbilityType.SpiritDash, "灵魄冲刺"),
}), }),
("三魂形态", new[]
{
(AbilityType.FormTianHun, "天魂"),
(AbilityType.FormDiHun, "地魂"),
(AbilityType.FormMingHun, "命魂"),
}),
("战斗能力", new[] ("战斗能力", new[]
{ {
(AbilityType.Parry, "弹反"), (AbilityType.Parry, "弹反"),

View File

@@ -51,6 +51,7 @@ namespace BaseGames.Editor
}), }),
("法术能力", new[] { AbilityType.Spell1, AbilityType.Spell2, AbilityType.Spell3 }), ("法术能力", new[] { AbilityType.Spell1, AbilityType.Spell2, AbilityType.Spell3 }),
("灵魄形态", new[] { AbilityType.SpiritForm, AbilityType.SpiritDash }), ("灵魄形态", new[] { AbilityType.SpiritForm, AbilityType.SpiritDash }),
("三魂形态", new[] { AbilityType.FormTianHun, AbilityType.FormDiHun, AbilityType.FormMingHun }),
("战斗能力", new[] { AbilityType.Parry, AbilityType.ChargeAttack, AbilityType.DownSlash }), ("战斗能力", new[] { AbilityType.Parry, AbilityType.ChargeAttack, AbilityType.DownSlash }),
("互动能力", new[] { AbilityType.Interact, AbilityType.FastTravel }), ("互动能力", new[] { AbilityType.Interact, AbilityType.FastTravel }),
("能力强化", new[] { AbilityType.InvincibleDash }), ("能力强化", new[] { AbilityType.InvincibleDash }),
@@ -58,12 +59,13 @@ namespace BaseGames.Editor
private static readonly string[] AbilityFlagNames = private static readonly string[] AbilityFlagNames =
{ {
"贴墙悬挂", "墙跳", "地面冲刺", "空中冲刺", "二段跳", "超级跳", "游泳", "下", "贴墙悬挂", "墙跳", "地面冲刺", "空中冲刺", "二段跳", "超级跳", "游泳", "下冲刺",
"法术槽 1", "法术槽 2", "法术槽 3", "法术槽 1", "法术槽 2", "法术槽 3",
"灵魄形态", "灵魄冲刺", "灵魄形态", "灵魄冲刺",
"弹反", "蓄力攻击", "下斩", "弹反", "蓄力攻击", "下斩",
"互动", "快速旅行", "互动", "快速旅行",
"无敌冲刺", "无敌冲刺",
"天魂", "地魂", "命魂",
}; };
// ── 样式(懒初始化)────────────────────────────────────────────────── // ── 样式(懒初始化)──────────────────────────────────────────────────
@@ -469,7 +471,10 @@ namespace BaseGames.Editor
AbilityType.DownSlash => "下斩", AbilityType.DownSlash => "下斩",
AbilityType.Interact => "互动", AbilityType.Interact => "互动",
AbilityType.FastTravel => "快速旅行", AbilityType.FastTravel => "快速旅行",
AbilityType.InvincibleDash => "无敌冲刺", AbilityType.InvincibleDash => "无敌冲刺",
AbilityType.FormTianHun => "天魂",
AbilityType.FormDiHun => "地魂",
AbilityType.FormMingHun => "命魂",
_ => flag.ToString(), _ => flag.ToString(),
}; };

View File

@@ -301,13 +301,24 @@ namespace BaseGames.Enemies
#if UNITY_EDITOR #if UNITY_EDITOR
if (_statsSO == null) return; if (_statsSO == null) return;
// ── 侦测范围(淡橙圆,始终可见)──────────────────────────── // ── 侦测范围(淡橙填充圆,始终可见)+ 攻击范围(淡红填充圆)────────
Gizmos.color = new Color(1f, 0.6f, 0.1f, 0.18f); {
HitBox.DrawWireCircle2D(transform.position, _statsSO.DetectRange); var c = new Vector3(transform.position.x, transform.position.y, 0f);
var prevM = UnityEditor.Handles.matrix;
UnityEditor.Handles.matrix = Matrix4x4.identity;
// ── 攻击范围(淡红圆)─────────────────────────────────────── UnityEditor.Handles.color = new Color(1f, 0.6f, 0.1f, 0.15f);
Gizmos.color = new Color(1f, 0.15f, 0.15f, 0.22f); UnityEditor.Handles.DrawSolidDisc(c, Vector3.back, _statsSO.DetectRange);
HitBox.DrawWireCircle2D(transform.position, _statsSO.AttackRange); UnityEditor.Handles.color = new Color(1f, 0.6f, 0.1f, 0.55f);
UnityEditor.Handles.DrawWireDisc(c, Vector3.back, _statsSO.DetectRange);
UnityEditor.Handles.color = new Color(1f, 0.15f, 0.15f, 0.15f);
UnityEditor.Handles.DrawSolidDisc(c, Vector3.back, _statsSO.AttackRange);
UnityEditor.Handles.color = new Color(1f, 0.15f, 0.15f, 0.55f);
UnityEditor.Handles.DrawWireDisc(c, Vector3.back, _statsSO.AttackRange);
UnityEditor.Handles.matrix = prevM;
}
// ── 运行时LOS 连线 ──────────────────────────────────────── // ── 运行时LOS 连线 ────────────────────────────────────────
if (!Application.isPlaying || _playerTransform == null) return; if (!Application.isPlaying || _playerTransform == null) return;
@@ -319,11 +330,10 @@ namespace BaseGames.Enemies
// 眼睛位置小圆点(金黄) // 眼睛位置小圆点(金黄)
Gizmos.color = new Color(1f, 0.9f, 0.2f, 0.85f); Gizmos.color = new Color(1f, 0.9f, 0.2f, 0.85f);
HitBox.DrawWireCircle2D(eyeWorld, 0.07f, 8); Gizmos.DrawWireSphere(eyeWorld, 0.07f);
if (inRange || _losResult) if (inRange || _losResult)
{ {
// 有 LOS → 橙色实线;在范围内但遮挡 → 灰色虚感(透明度低)
Gizmos.color = _losResult Gizmos.color = _losResult
? new Color(1f, 0.5f, 0f, 0.85f) ? new Color(1f, 0.5f, 0f, 0.85f)
: new Color(0.6f, 0.6f, 0.6f, 0.25f); : new Color(0.6f, 0.6f, 0.6f, 0.25f);
@@ -338,11 +348,23 @@ namespace BaseGames.Enemies
if (_statsSO == null) return; if (_statsSO == null) return;
// 选中时加亮范围圆 // 选中时加亮范围圆
Gizmos.color = new Color(1f, 0.6f, 0.1f, 0.6f); {
HitBox.DrawWireCircle2D(transform.position, _statsSO.DetectRange); var c = new Vector3(transform.position.x, transform.position.y, 0f);
var prevM = UnityEditor.Handles.matrix;
UnityEditor.Handles.matrix = Matrix4x4.identity;
Gizmos.color = new Color(1f, 0.2f, 0.2f, 0.7f); UnityEditor.Handles.color = new Color(1f, 0.6f, 0.1f, 0.25f);
HitBox.DrawWireCircle2D(transform.position, _statsSO.AttackRange); UnityEditor.Handles.DrawSolidDisc(c, Vector3.back, _statsSO.DetectRange);
UnityEditor.Handles.color = new Color(1f, 0.6f, 0.1f, 0.90f);
UnityEditor.Handles.DrawWireDisc(c, Vector3.back, _statsSO.DetectRange);
UnityEditor.Handles.color = new Color(1f, 0.2f, 0.2f, 0.25f);
UnityEditor.Handles.DrawSolidDisc(c, Vector3.back, _statsSO.AttackRange);
UnityEditor.Handles.color = new Color(1f, 0.2f, 0.2f, 0.90f);
UnityEditor.Handles.DrawWireDisc(c, Vector3.back, _statsSO.AttackRange);
UnityEditor.Handles.matrix = prevM;
}
#endif #endif
} }
} }

View File

@@ -45,6 +45,7 @@ namespace BaseGames.Enemies
public void PlayFootstep() { } public void PlayFootstep() { }
public void TriggerPreset(string presetId) { } public void TriggerPreset(string presetId) { }
public void PlaySFXById(string sfxId) { } public void PlaySFXById(string sfxId) { }
public void PlayFormSwitch(int formIndex) { } // 敌人无形态切换,空实现
// ── EnemyBase 语义方法 ──────────────────────────────────────────── // ── EnemyBase 语义方法 ────────────────────────────────────────────
/// <summary>受到伤害时调用(由 EnemyBase 触发)。</summary> /// <summary>受到伤害时调用(由 EnemyBase 触发)。</summary>

View File

@@ -44,6 +44,10 @@ namespace BaseGames.Feedback
/// <summary>通过 sfxId 触发单次音效(不带任何摄像机/振动反馈)。</summary> /// <summary>通过 sfxId 触发单次音效(不带任何摄像机/振动反馈)。</summary>
void PlaySFXById(string sfxId); void PlaySFXById(string sfxId);
// ── 形态切换 ──────────────────────────────────────────────────────────────
/// <summary>切换到指定形态时播放对应反馈(音效 + 粒子 + 震屏等。formIndex 对应 FormType 枚举值。</summary>
void PlayFormSwitch(int formIndex);
} }
/// <summary>命中力度。</summary> /// <summary>命中力度。</summary>
@@ -67,6 +71,7 @@ namespace BaseGames.Feedback
public void PlayFootstep() { } public void PlayFootstep() { }
public void TriggerPreset(string presetId) { } public void TriggerPreset(string presetId) { }
public void PlaySFXById(string sfxId) { } public void PlaySFXById(string sfxId) { }
public void PlayFormSwitch(int formIndex) { }
} }
} }

View File

@@ -33,6 +33,10 @@ namespace BaseGames.Feedback
[Header("脚步声材质检测")] [Header("脚步声材质检测")]
[SerializeField] private BaseGames.Audio.FootstepSoundPlayer _footstepSoundPlayer; [SerializeField] private BaseGames.Audio.FootstepSoundPlayer _footstepSoundPlayer;
[Header("形态切换反馈")]
[Tooltip("索引与 FormType 枚举值对应0=天魂, 1=地魂, 2=命魂")]
[SerializeField] private MMF_Player[] _onFormSwitch = new MMF_Player[3];
[Header("命名预设(可选)")] [Header("命名预设(可选)")]
[SerializeField] private NamedFeedback[] _namedPresets; [SerializeField] private NamedFeedback[] _namedPresets;
@@ -90,6 +94,12 @@ namespace BaseGames.Feedback
Debug.LogWarning($"[PlayerFeedback] 未找到 SFX 预设 '{sfxId}'。"); Debug.LogWarning($"[PlayerFeedback] 未找到 SFX 预设 '{sfxId}'。");
} }
public void PlayFormSwitch(int formIndex)
{
if (_onFormSwitch != null && formIndex >= 0 && formIndex < _onFormSwitch.Length)
_onFormSwitch[formIndex]?.PlayFeedbacks();
}
// ── 辅助 ───────────────────────────────────────────────────────────────── // ── 辅助 ─────────────────────────────────────────────────────────────────
private static Dictionary<string, MMF_Player> BuildMap(NamedFeedback[] entries) private static Dictionary<string, MMF_Player> BuildMap(NamedFeedback[] entries)
{ {

View File

@@ -47,9 +47,15 @@ namespace BaseGames.Player
/// </summary> /// </summary>
InvincibleDash = 1u << 18, InvincibleDash = 1u << 18,
// ── 形态解锁(三魂形态)──────────────────────────────────────────────────
FormTianHun = 1u << 19, // 天魂形态(默认初始解锁)
FormDiHun = 1u << 20, // 地魂形态(游戏进程中解锁)
FormMingHun = 1u << 21, // 命魂形态(游戏进程中解锁)
// ── 组合掩码 ───────────────────────────────────────────────────────── // ── 组合掩码 ─────────────────────────────────────────────────────────
AllMovement = WallCling | WallJump | Dash | DoubleJump | SuperJump | Swim | DownDash | InvincibleDash, AllMovement = WallCling | WallJump | Dash | DoubleJump | SuperJump | Swim | DownDash | InvincibleDash,
AllSpells = Spell1 | Spell2 | Spell3, AllSpells = Spell1 | Spell2 | Spell3,
AllSpirit = SpiritForm | SpiritDash, AllSpirit = SpiritForm | SpiritDash,
AllForms = FormTianHun | FormDiHun | FormMingHun,
} }
} }

View File

@@ -13,6 +13,11 @@ namespace BaseGames.Player
[Header("形态列表 (forms[0]=Sky, forms[1]=Earth, forms[2]=Death)")] [Header("形态列表 (forms[0]=Sky, forms[1]=Earth, forms[2]=Death)")]
public FormSO[] forms; public FormSO[] forms;
[Header("切换冷却")]
[Tooltip("形态切换冷却时长。CD 内重复按键不生效。推荐 0.5。")]
[Min(0f)]
public float SwitchCooldown = 0.5f;
/// <summary>按形态类型查找对应 FormSO找不到返回 null。</summary> /// <summary>按形态类型查找对应 FormSO找不到返回 null。</summary>
public FormSO GetFormByType(FormType type) public FormSO GetFormByType(FormType type)
{ {

View File

@@ -3,6 +3,7 @@ using UnityEngine;
using BaseGames.Core; using BaseGames.Core;
using BaseGames.Core.Save; using BaseGames.Core.Save;
using BaseGames.Core.Events; using BaseGames.Core.Events;
using BaseGames.Feedback;
using BaseGames.Input; using BaseGames.Input;
namespace BaseGames.Player namespace BaseGames.Player
@@ -21,6 +22,9 @@ namespace BaseGames.Player
[SerializeField] private FormConfigSO _config; [SerializeField] private FormConfigSO _config;
[SerializeField] private InputReaderSO _input; [SerializeField] private InputReaderSO _input;
[Header("依赖")]
[SerializeField] private PlayerStats _stats;
[Header("事件频道")] [Header("事件频道")]
[SerializeField] private IntEventChannelSO _onFormChanged; // 广播当前形态索引UI/Save [SerializeField] private IntEventChannelSO _onFormChanged; // 广播当前形态索引UI/Save
[SerializeField] private VoidEventChannelSO _onSkillSetChanged; // 通知 SkillHUD 刷新 [SerializeField] private VoidEventChannelSO _onSkillSetChanged; // 通知 SkillHUD 刷新
@@ -29,12 +33,21 @@ namespace BaseGames.Player
public FormSO CurrentForm { get; private set; } public FormSO CurrentForm { get; private set; }
public FormSO[] AllForms => _config.forms; public FormSO[] AllForms => _config.forms;
/// <summary>当前切换 CD 剩余时间0 表示可切换。</summary>
public float SwitchCooldownRemaining => _switchCooldownTimer;
/// <summary>C# 事件WeaponManager 在 OnEnable 自订阅(架构 05 §6。</summary> /// <summary>C# 事件WeaponManager 在 OnEnable 自订阅(架构 05 §6。</summary>
public event Action OnFormChanged; public event Action OnFormChanged;
private float _switchCooldownTimer;
private IFeedbackPlayer _feedback;
private void Awake() private void Awake()
{ {
Debug.Assert(_config != null, "[FormController] _config 未赋值,请在 Inspector 中指定 FormConfigSO。", this); Debug.Assert(_config != null, "[FormController] _config 未赋值,请在 Inspector 中指定 FormConfigSO。", this);
if (_stats == null)
_stats = GetComponent<PlayerStats>();
_feedback = GetComponentInChildren<IFeedbackPlayer>() ?? NullFeedbackPlayer.Instance;
} }
private void OnEnable() private void OnEnable()
@@ -61,16 +74,43 @@ namespace BaseGames.Player
CurrentForm = _config.forms[0]; CurrentForm = _config.forms[0];
} }
private void Update()
{
if (_switchCooldownTimer > 0f)
_switchCooldownTimer -= Time.deltaTime;
}
// ── 公共 API ──────────────────────────────────────────────────────────── // ── 公共 API ────────────────────────────────────────────────────────────
/// <summary>切换到指定形态类型。若已在目标形态则不操作。</summary> /// <summary>切换到指定形态类型。若已在目标形态、尚未解锁或 CD 未结束则不操作。</summary>
public void SwitchForm(FormType newFormType) public void SwitchForm(FormType newFormType)
{ {
// CD 检查
if (_switchCooldownTimer > 0f) return;
// 检查对应形态的解锁标志
AbilityType required = newFormType switch
{
FormType.TianHun => AbilityType.FormTianHun,
FormType.DiHun => AbilityType.FormDiHun,
FormType.MingHun => AbilityType.FormMingHun,
_ => AbilityType.None,
};
if (required != AbilityType.None && _stats != null && !_stats.HasAbility(required))
return;
FormSO newForm = _config.GetFormByType(newFormType); FormSO newForm = _config.GetFormByType(newFormType);
if (newForm == null || newForm == CurrentForm) return; if (newForm == null || newForm == CurrentForm) return;
CurrentForm = newForm; CurrentForm = newForm;
// 启动切换 CD
if (_config.SwitchCooldown > 0f)
_switchCooldownTimer = _config.SwitchCooldown;
// 播放对应形态切换反馈
_feedback.PlayFormSwitch((int)newFormType);
// 1. SO 事件广播索引UI/Save // 1. SO 事件广播索引UI/Save
_onFormChanged?.Raise(_config.GetFormIndex(newForm)); _onFormChanged?.Raise(_config.GetFormIndex(newForm));

View File

@@ -33,6 +33,10 @@ namespace BaseGames.Player
[Header("弹簧")] [Header("弹簧")]
public AnimationClip UseSpring; public AnimationClip UseSpring;
[Header("下冲刺")]
[Tooltip("向下冲刺动画。留空则复用 Fall 动画。")]
public AnimationClip DownDash;
[Header("弹反")] [Header("弹反")]
public AnimationClip ParryStart; public AnimationClip ParryStart;
public AnimationClip ParrySuccess; public AnimationClip ParrySuccess;

View File

@@ -34,21 +34,25 @@ namespace BaseGames.Player
{ {
if (_weaponManager != null) if (_weaponManager != null)
_weaponManager.OnWeaponChanged -= HandleWeaponChanged; _weaponManager.OnWeaponChanged -= HandleWeaponChanged;
UnsubscribeDownHit(); UnsubscribeHitEvents();
} }
private void HandleWeaponChanged(WeaponSO _) private void HandleWeaponChanged(WeaponSO _)
{ {
UnsubscribeDownHit(); UnsubscribeHitEvents();
_currentHitBoxInstance = _weaponManager.ActiveHitBoxInstance; _currentHitBoxInstance = _weaponManager.ActiveHitBoxInstance;
if (_currentHitBoxInstance != null) if (_currentHitBoxInstance != null)
{
_currentHitBoxInstance.OnDownHitConfirmed += HandleDownHitConfirmed; _currentHitBoxInstance.OnDownHitConfirmed += HandleDownHitConfirmed;
_currentHitBoxInstance.OnHitConfirmed += OnHitConfirmed;
}
} }
private void UnsubscribeDownHit() private void UnsubscribeHitEvents()
{ {
if (_currentHitBoxInstance == null) return; if (_currentHitBoxInstance == null) return;
_currentHitBoxInstance.OnDownHitConfirmed -= HandleDownHitConfirmed; _currentHitBoxInstance.OnDownHitConfirmed -= HandleDownHitConfirmed;
_currentHitBoxInstance.OnHitConfirmed -= OnHitConfirmed;
_currentHitBoxInstance = null; _currentHitBoxInstance = null;
} }

View File

@@ -292,6 +292,15 @@ namespace BaseGames.Player
_rb.velocity = new Vector2(direction.x * speed, 0f); _rb.velocity = new Vector2(direction.x * speed, 0f);
} }
/// <summary>
/// 施加向下冲刺速度DownDashState 调用)。
/// 冲刺期间调用 SetGravityScale(0) 可关闭重力;结束后恢复默认重力。
/// </summary>
public void DownDash(float speed)
{
_rb.velocity = new Vector2(0f, -speed);
}
/// <summary> /// <summary>
/// 壁滑:将垂直速度限制为 -speed每帧调用以约束最大下滑速度 /// 壁滑:将垂直速度限制为 -speed每帧调用以约束最大下滑速度
/// WallSlideState.OnStateFixedUpdate 根据正常/受限模式传入不同速度。 /// WallSlideState.OnStateFixedUpdate 根据正常/受限模式传入不同速度。
@@ -422,7 +431,15 @@ namespace BaseGames.Player
Gizmos.color = grounded Gizmos.color = grounded
? new Color(0.1f, 1f, 0.3f, 0.9f) ? new Color(0.1f, 1f, 0.3f, 0.9f)
: new Color(0.4f, 0.85f, 0.4f, 0.35f); : new Color(0.4f, 0.85f, 0.4f, 0.35f);
BaseGames.Combat.HitBox.DrawWireRect2D(_groundCheck.position, _groundCheckSize); // 绘制地面检测矩形
{
Vector3 c = _groundCheck.position;
float hx = _groundCheckSize.x * 0.5f, hy = _groundCheckSize.y * 0.5f;
Gizmos.DrawLine(new Vector3(c.x - hx, c.y + hy), new Vector3(c.x + hx, c.y + hy));
Gizmos.DrawLine(new Vector3(c.x + hx, c.y + hy), new Vector3(c.x + hx, c.y - hy));
Gizmos.DrawLine(new Vector3(c.x + hx, c.y - hy), new Vector3(c.x - hx, c.y - hy));
Gizmos.DrawLine(new Vector3(c.x - hx, c.y - hy), new Vector3(c.x - hx, c.y + hy));
}
} }
// ── 4. 靠墙检测射线(碰墙橙色 / 未碰淡黄,带端点圆点)───────── // ── 4. 靠墙检测射线(碰墙橙色 / 未碰淡黄,带端点圆点)─────────
@@ -436,11 +453,11 @@ namespace BaseGames.Player
Gizmos.color = lHit ? new Color(1f, 0.45f, 0f, 1f) : new Color(1f, 0.85f, 0.3f, 0.4f); Gizmos.color = lHit ? new Color(1f, 0.45f, 0f, 1f) : new Color(1f, 0.85f, 0.3f, 0.4f);
Gizmos.DrawRay(wallPos, Vector2.left * wLen); Gizmos.DrawRay(wallPos, Vector2.left * wLen);
BaseGames.Combat.HitBox.DrawWireCircle2D((Vector3)(wallPos + Vector2.left * wLen), 0.04f, 8); Gizmos.DrawWireSphere((Vector3)(wallPos + Vector2.left * wLen), 0.04f);
Gizmos.color = rHit ? new Color(1f, 0.45f, 0f, 1f) : new Color(1f, 0.85f, 0.3f, 0.4f); Gizmos.color = rHit ? new Color(1f, 0.45f, 0f, 1f) : new Color(1f, 0.85f, 0.3f, 0.4f);
Gizmos.DrawRay(wallPos, Vector2.right * wLen); Gizmos.DrawRay(wallPos, Vector2.right * wLen);
BaseGames.Combat.HitBox.DrawWireCircle2D((Vector3)(wallPos + Vector2.right * wLen), 0.04f, 8); Gizmos.DrawWireSphere((Vector3)(wallPos + Vector2.right * wLen), 0.04f);
#endif #endif
} }

View File

@@ -54,6 +54,12 @@ namespace BaseGames.Player
[Tooltip("无敌的独立冷却。CD 内再次冲刺不会获得无敌帧,防止连冲变相持续无敌(推荐 0.9s)。")] [Tooltip("无敌的独立冷却。CD 内再次冲刺不会获得无敌帧,防止连冲变相持续无敌(推荐 0.9s)。")]
public float DashInvincibilityCooldown = 0.9f; public float DashInvincibilityCooldown = 0.9f;
[Header("下冲刺")]
[Tooltip("向下冲刺速度(单位/秒)。推荐 22快速向下穿透空间。")]
public float DownDashSpeed = 22f;
[Tooltip("向下冲刺持续时长(秒)。推荐 0.25s。")]
public float DownDashDuration = 0.25f;
[Header("抓墙 / 壁滑")] [Header("抓墙 / 壁滑")]
[Tooltip("受限抓墙时(高于 wallGrabY的下滑速度单位/秒)。推荐 2。")] [Tooltip("受限抓墙时(高于 wallGrabY的下滑速度单位/秒)。推荐 2。")]
public float WallSlideSpeed = 2f; public float WallSlideSpeed = 2f;

View File

@@ -257,12 +257,29 @@ namespace BaseGames.Player
} }
// ── Spring ──────────────────────────────────────────────────────────── // ── Spring ────────────────────────────────────────────────────────────
public bool UseSpring()
/// <summary>
/// 仅扣除灵泉充能SpringState 进入前摇时调用)。
/// 回血需前摇结束后调用 <see cref="ApplySpringHeal"/>。
/// </summary>
public bool ConsumeSpringCharge()
{ {
if (CurrentSpringCharges <= 0) return false; if (CurrentSpringCharges <= 0) return false;
CurrentSpringCharges--; CurrentSpringCharges--;
_onSpringChargesChanged?.Raise(CurrentSpringCharges); _onSpringChargesChanged?.Raise(CurrentSpringCharges);
HealHP(_config.SpringHealAmount); return true;
}
/// <summary>
/// 执行灵泉回血SpringState 前摇动画结束时调用)。
/// </summary>
public void ApplySpringHeal() => HealHP(_config.SpringHealAmount);
/// <summary>扣除充能并立即回血(兼容旧调用路径)。</summary>
public bool UseSpring()
{
if (!ConsumeSpringCharge()) return false;
ApplySpringHeal();
return true; return true;
} }

View File

@@ -28,9 +28,10 @@ namespace BaseGames.Player
[Header("初始已解锁能力")] [Header("初始已解锁能力")]
[Tooltip("角色出生时默认持有的能力([Flags] \n\n" + [Tooltip("角色出生时默认持有的能力([Flags] \n\n" +
"FormTianHun天魂形态默认解锁请务必勾选。\n\n" +
"Dash地面与空中冲刺统一控制勾选后即可使用 DashState。\n\n" + "Dash地面与空中冲刺统一控制勾选后即可使用 DashState。\n\n" +
"DoubleJump追加次数由 PlayerMovementConfigSO.MaxAirJumps 决定。\n" + "DoubleJump追加次数由 PlayerMovementConfigSO.MaxAirJumps 决定。\n" +
"落地或 Pogo 命中后次数自动重置。")] "落地或 Pogo 命中后次数自动重置。")]
public AbilityType InitialAbilities = AbilityType.None; public AbilityType InitialAbilities = AbilityType.FormTianHun;
} }
} }

View File

@@ -112,7 +112,10 @@ namespace BaseGames.Player.States
var animState = Anim.Play(step.clip); var animState = Anim.Play(step.clip);
animState.Speed *= spd; // 在 ClipTransition 自身速度基础上叠加攻速 animState.Speed *= spd; // 在 ClipTransition 自身速度基础上叠加攻速
// 每次重播同一 ClipTransition 会复用同一 AnimancerState
// 必须先清除旧事件再注册新事件,否则回调会累积叠加。
var events = animState.Events(this); var events = animState.Events(this);
events.Clear();
events.OnEnd = OnClipEnd; events.OnEnd = OnClipEnd;
// HitBox 时间窗口capture step by value for closure safety // HitBox 时间窗口capture step by value for closure safety
@@ -121,11 +124,24 @@ namespace BaseGames.Player.States
Owner.Combat?.EnableWeaponHitBox(AttackDirection.Ground, Owner.Combat?.EnableWeaponHitBox(AttackDirection.Ground,
capturedStep.hitBoxId, capturedStep.damageSource)); capturedStep.hitBoxId, capturedStep.damageSource));
events.Add(capturedStep.hitBoxExit, () => Owner.Combat?.DisableAllWeaponHitBoxes()); events.Add(capturedStep.hitBoxExit, () => Owner.Combat?.DisableAllWeaponHitBoxes());
// 连击输入窗口 // 连击输入窗口
if (capturedStep.comboInputOpen > 0f) if (capturedStep.comboInputOpen > 0f)
events.Add(capturedStep.comboInputOpen, () => _comboWindowOpen = true); {
events.Add(capturedStep.comboInputOpen, () =>
{
_comboWindowOpen = true;
// 窗口刚开时,补检查 InputBuffer——玩家可能在窗口前就提前按键
if (!_comboInputPending && Buffer.ConsumeAttack())
_comboInputPending = true;
});
}
else else
_comboWindowOpen = true; // 0 = 立即开放 {
_comboWindowOpen = true;
if (!_comboInputPending && Buffer.ConsumeAttack())
_comboInputPending = true;
}
if (capturedStep.comboInputClose > 0f) if (capturedStep.comboInputClose > 0f)
events.Add(capturedStep.comboInputClose, () => _comboWindowOpen = false); events.Add(capturedStep.comboInputClose, () => _comboWindowOpen = false);
@@ -141,11 +157,18 @@ namespace BaseGames.Player.States
_comboWindowOpen = false; _comboWindowOpen = false;
Move.SetCancelWindowOpen(false); Move.SetCancelWindowOpen(false);
// 如果已有缓存输入,直接推进(零延迟连击) // 有缓存连击输入且还不是最后一段 → 零延迟推进到下一段
if (_comboInputPending) if (_comboInputPending)
{ {
AdvanceCombo(); _comboInputPending = false;
return; int maxCombo = Owner.Weapon?.ActiveWeapon?.GroundComboCount ?? 1;
if (_comboIndex < maxCombo - 1)
{
_comboIndex++;
PlayAttackClip();
return; // 新动画已开始,不进入等待阶段
}
// 已是最后一段:消耗掉多余输入,继续进入等待阶段(不 return
} }
// 进入动画后等待阶段 // 进入动画后等待阶段
@@ -169,7 +192,7 @@ namespace BaseGames.Player.States
} }
else else
{ {
// 已是最后一段,忽略多余输入,等待超时 // 已是最后一段,忽略多余输入,等待超时回 Idle
_comboInputPending = false; _comboInputPending = false;
} }
} }

View File

@@ -29,6 +29,9 @@ namespace BaseGames.Player.States
/// <summary>重置冲刺次数(落地或 Pogo 时由 IdleState/RunState/DownAttackState 调用)。</summary> /// <summary>重置冲刺次数(落地或 Pogo 时由 IdleState/RunState/DownAttackState 调用)。</summary>
public void ResetDashCharge() => _dashChargeUsed = false; public void ResetDashCharge() => _dashChargeUsed = false;
/// <summary>消耗空中冲刺次数DownDashState 进入时调用,与普通空中冲刺共享次数上限)。</summary>
public void ConsumeAirDashCharge() => _dashChargeUsed = true;
/// <summary> /// <summary>
/// 无敌帧是否已冷却,即本次冲刺可以获得无敌。 /// 无敌帧是否已冷却,即本次冲刺可以获得无敌。
/// </summary> /// </summary>

View File

@@ -0,0 +1,62 @@
using UnityEngine;
namespace BaseGames.Player.States
{
/// <summary>
/// 向下冲刺状态(空中下 + 冲刺触发)。
/// - 消耗空中冲刺次数(与普通冲刺共享,本次离地仅可用一次)。
/// - 关闭重力,施加向下速度,持续 DownDashDuration 秒。
/// - 提前着地或计时结束时退出。
/// - 需解锁 AbilityType.DownDash 才能进入FallState / JumpState 负责条件检查)。
/// </summary>
public class DownDashState : PlayerStateBase
{
private float _timer;
public DownDashState(PlayerController owner) : base(owner) { }
public override void OnStateEnter()
{
// 消耗空中冲刺次数(与普通空中冲刺互斥)
Owner.GetState<DashState>()?.ConsumeAirDashCharge();
_timer = Cfg.DownDashDuration;
// 关闭重力,施加向下速度
Move?.SetGravityScale(0f);
Move?.DownDash(Cfg.DownDashSpeed);
// 优先播放专属动画,未配置时回退到 Fall 动画
var clip = AnimCfg?.DownDash ?? AnimCfg?.Fall;
if (clip != null) Anim?.Play(clip);
}
public override void OnStateUpdate()
{
_timer -= Time.deltaTime;
if (_timer <= 0f || Move.IsGrounded)
EndDownDash();
}
public override void OnStateFixedUpdate()
{
// 持续保持向下速度(防止摩擦力减速)
if (_timer > 0f && !Move.IsGrounded)
Move?.DownDash(Cfg.DownDashSpeed);
}
public override void OnStateExit()
{
Move?.SetGravityScale(Cfg.DefaultGravityScale);
}
private void EndDownDash()
{
Move?.ZeroVelocity();
if (Move != null && Move.IsGrounded)
Owner.TransitionTo(Owner.GetState<IdleState>());
else
Owner.TransitionTo(Owner.GetState<FallState>());
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: b5c828df0b1d49d4bb45ced1da093e7b
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -6,6 +6,7 @@ namespace BaseGames.Player.States
/// 下落状态。 /// 下落状态。
/// - 郊狼跳CoyoteTimer > 0 时按跳跃 → 一段跳JumpState使用 JumpForce /// - 郊狼跳CoyoteTimer > 0 时按跳跃 → 一段跳JumpState使用 JumpForce
/// - 空中跳跃CoyoteTimer 耗尽后按跳跃且 AirJumpsLeft > 0 → JumpState使用 DoubleJumpForce /// - 空中跳跃CoyoteTimer 耗尽后按跳跃且 AirJumpsLeft > 0 → JumpState使用 DoubleJumpForce
/// - 下冲刺HasAbility(DownDash) &amp;&amp; 下方向 + 冲刺键 → DownDashState优先于普通冲刺
/// - 冲刺HasAbility(Dash) &amp;&amp; DashState.CanDashMidAir → DashState地面与空中统一空中限一次 /// - 冲刺HasAbility(Dash) &amp;&amp; DashState.CanDashMidAir → DashState地面与空中统一空中限一次
/// - 抓墙:贴墙时按下朝向墙壁的方向键 → WallSlideState。 /// - 抓墙:贴墙时按下朝向墙壁的方向键 → WallSlideState。
/// - 增强下落重力FallGravityMult确保下落快于上升手感紧实。 /// - 增强下落重力FallGravityMult确保下落快于上升手感紧实。
@@ -53,9 +54,20 @@ namespace BaseGames.Player.States
return; return;
} }
// ── 下冲刺(下 + 冲刺 → 向下冲刺,优先于普通冲刺)──────────────────────
// 按住下方向 + 冲刺键,且已解锁 DownDash 能力、空中冲刺次数未耗尽
var dashState = Owner.GetState<DashState>();
if (dashState != null && dashState.CanDashMidAir
&& Input.MoveInput.y < -0.5f
&& Stats != null && Stats.HasAbility(AbilityType.DownDash)
&& Buffer.ConsumeDash())
{
_owner.TransitionTo(_owner.GetState<DownDashState>());
return;
}
// ── 冲刺(地面/空中统一使用 DashState──────────────────────────── // ── 冲刺(地面/空中统一使用 DashState────────────────────────────
// 先确认能力与冷却均满足,再消耗缓冲,避免无操作时静默吃掉输入 // 先确认能力与冷却均满足,再消耗缓冲,避免无操作时静默吃掉输入
var dashState = Owner.GetState<DashState>();
if (dashState != null && dashState.CanDashMidAir if (dashState != null && dashState.CanDashMidAir
&& Stats != null && Stats.HasAbility(AbilityType.Dash) && Stats != null && Stats.HasAbility(AbilityType.Dash)
&& Buffer.ConsumeDash()) && Buffer.ConsumeDash())

View File

@@ -52,9 +52,20 @@ namespace BaseGames.Player.States
return; return;
} }
// ── 下冲刺(下 + 冲刺 → 向下冲刺,优先于普通冲刺)──────────────────────
// 按住下方向 + 冲刺键,且已解锁 DownDash 能力、空中冲刺次数未耗尽
var dashState = Owner.GetState<DashState>();
if (dashState != null && dashState.CanDashMidAir
&& Input.MoveInput.y < -0.5f
&& Stats != null && Stats.HasAbility(AbilityType.DownDash)
&& Buffer.ConsumeDash())
{
_owner.TransitionTo(_owner.GetState<DownDashState>());
return;
}
// 冲刺(地面/空中统一使用 DashState空中限一次优先于二段跳冲刺可保存二段跳机会 // 冲刺(地面/空中统一使用 DashState空中限一次优先于二段跳冲刺可保存二段跳机会
// 先确认能力与冷却均满足,再消耗缓冲,避免无操作时静默吃掉输入 // 先确认能力与冷却均满足,再消耗缓冲,避免无操作时静默吃掉输入
var dashState = Owner.GetState<DashState>();
if (dashState != null && dashState.CanDashMidAir if (dashState != null && dashState.CanDashMidAir
&& Stats != null && Stats.HasAbility(AbilityType.Dash) && Stats != null && Stats.HasAbility(AbilityType.Dash)
&& Buffer.ConsumeDash()) && Buffer.ConsumeDash())

View File

@@ -350,6 +350,7 @@ namespace BaseGames.Player.States
_states[typeof(FallState)] = new FallState(this); _states[typeof(FallState)] = new FallState(this);
_states[typeof(AttackState)] = new AttackState(this); _states[typeof(AttackState)] = new AttackState(this);
_states[typeof(DashState)] = new DashState(this); _states[typeof(DashState)] = new DashState(this);
_states[typeof(DownDashState)] = new DownDashState(this);
_states[typeof(WallSlideState)] = new WallSlideState(this); _states[typeof(WallSlideState)] = new WallSlideState(this);
_states[typeof(WallJumpState)] = new WallJumpState(this); _states[typeof(WallJumpState)] = new WallJumpState(this);
_states[typeof(AirAttackState)] = new AirAttackState(this); _states[typeof(AirAttackState)] = new AirAttackState(this);

View File

@@ -14,11 +14,11 @@ namespace BaseGames.Player.States
public override void OnStateEnter() public override void OnStateEnter()
{ {
// 消耗灵泉充能并治疗PlayerStats.UseSpring 内部回复 HP // 前摇开始时只扣除充能,不立即回血;回血在前摇结束后的 OnSpringEnd 中执行。
bool used = Stats?.UseSpring() ?? false; // 若前摇被打断(受伤 → HurtStateOnStateExit 被调用,充能已扣除但 OnSpringEnd 不会执行,回血失败。
bool used = Stats?.ConsumeSpringCharge() ?? false;
if (!used) if (!used)
{ {
// 无充能时立即退出
Owner.TransitionTo(Owner.GetState<IdleState>()); Owner.TransitionTo(Owner.GetState<IdleState>());
return; return;
} }
@@ -37,7 +37,7 @@ namespace BaseGames.Player.States
} }
} }
// 无动画则直接结束 // 无动画配置则直接结束(视为前摇瞬间完成)
OnSpringEnd(); OnSpringEnd();
} }
@@ -49,6 +49,8 @@ namespace BaseGames.Player.States
private void OnSpringEnd() private void OnSpringEnd()
{ {
// 前摇正常结束 → 执行回血
Stats?.ApplySpringHeal();
Owner.TransitionTo(Owner.GetState<IdleState>()); Owner.TransitionTo(Owner.GetState<IdleState>());
} }
} }

View File

@@ -32,6 +32,9 @@ namespace BaseGames.Player
/// <summary>下劈命中确认事件(供 DownAttackState Pogo 逻辑)。</summary> /// <summary>下劈命中确认事件(供 DownAttackState Pogo 逻辑)。</summary>
public event System.Action<DamageInfo> OnDownHitConfirmed; public event System.Action<DamageInfo> OnDownHitConfirmed;
/// <summary>任意 HitBox 命中确认事件(供 PlayerCombat 订阅通用命中反馈)。</summary>
public event System.Action<DamageInfo> OnHitConfirmed;
private void Awake() private void Awake()
{ {
_allHitBoxes = GetComponentsInChildren<HitBox>(true); _allHitBoxes = GetComponentsInChildren<HitBox>(true);
@@ -41,6 +44,7 @@ namespace BaseGames.Player
private void OnAnyHitConfirmed(DamageInfo info) private void OnAnyHitConfirmed(DamageInfo info)
{ {
OnHitConfirmed?.Invoke(info);
if (_activeDir == AttackDirection.Down) if (_activeDir == AttackDirection.Down)
OnDownHitConfirmed?.Invoke(info); OnDownHitConfirmed?.Invoke(info);
} }

View File

@@ -27,6 +27,9 @@ namespace BaseGames.Player
// 护符注入的武器覆盖Key = FormSO.formIdValue = 替换武器(架构 05 §7 // 护符注入的武器覆盖Key = FormSO.formIdValue = 替换武器(架构 05 §7
private readonly Dictionary<string, WeaponSO> _overrides = new(); private readonly Dictionary<string, WeaponSO> _overrides = new();
// 对象池:避免每次切换形态时 Instantiate/DestroyKey = WeaponSOValue = 已创建实例)
private readonly Dictionary<WeaponSO, WeaponHitBoxInstance> _hitBoxPool = new();
private void Awake() private void Awake()
{ {
if (_formController != null && _formController.CurrentForm != null) if (_formController != null && _formController.CurrentForm != null)
@@ -70,22 +73,33 @@ namespace BaseGames.Player
private void SetDirectWeapon(WeaponSO weapon) private void SetDirectWeapon(WeaponSO weapon)
{ {
var oldInstance = ActiveHitBoxInstance; // 归还旧实例到池SetActive(false) 会触发 HitBox.OnDisable → Deactivate自动关闭 Collider2D
ActiveHitBoxInstance?.gameObject.SetActive(false);
ActiveWeapon = weapon; ActiveWeapon = weapon;
ActiveHitBoxInstance = null; ActiveHitBoxInstance = null;
if (weapon?.hitBoxPrefab != null && _weaponSocket != null) if (weapon?.hitBoxPrefab != null && _weaponSocket != null)
{ {
var go = Instantiate(weapon.hitBoxPrefab, _weaponSocket); if (!_hitBoxPool.TryGetValue(weapon, out var pooled) || pooled == null)
ActiveHitBoxInstance = go.GetComponent<WeaponHitBoxInstance>(); {
var go = Instantiate(weapon.hitBoxPrefab, _weaponSocket);
pooled = go.GetComponent<WeaponHitBoxInstance>();
_hitBoxPool[weapon] = pooled;
}
pooled.gameObject.SetActive(true);
ActiveHitBoxInstance = pooled;
} }
// 通知订阅者(使其有机会取消旧实例事件订阅),再销毁旧实例 // 通知订阅者(PlayerCombat 取消旧实例事件订阅,订阅新实例
OnWeaponChanged?.Invoke(weapon); OnWeaponChanged?.Invoke(weapon);
}
if (oldInstance != null) private void OnDestroy()
Destroy(oldInstance.gameObject); {
foreach (var inst in _hitBoxPool.Values)
if (inst != null) Destroy(inst.gameObject);
_hitBoxPool.Clear();
} }
// ── 护符 Override API由 WeaponOverrideEffect 调用,架构 05 §7────── // ── 护符 Override API由 WeaponOverrideEffect 调用,架构 05 §7──────