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.
This commit is contained in:
@@ -61,6 +61,7 @@ namespace BaseGames.Combat
|
||||
_currentSource = source ?? _defaultSource;
|
||||
_attackerTransform = attacker ?? transform;
|
||||
_isActive = true;
|
||||
_collider.enabled = true;
|
||||
// 缓存宿主 Rigidbody2D(沿父层级向上查找)
|
||||
_ownerRigidbody = _attackerTransform.GetComponentInParent<Rigidbody2D>();
|
||||
// 每次激活清空当前激活期已命中目标集合(防止连击连段导致同一阶段多次命中目标)
|
||||
@@ -70,7 +71,8 @@ namespace BaseGames.Combat
|
||||
|
||||
public void Deactivate()
|
||||
{
|
||||
_isActive = false;
|
||||
_isActive = false;
|
||||
_collider.enabled = false;
|
||||
_hitThisActivation.Clear();
|
||||
_hitCooldownTimers.Clear();
|
||||
}
|
||||
@@ -87,6 +89,8 @@ namespace BaseGames.Combat
|
||||
_collider = GetComponent<Collider2D>();
|
||||
if (!_collider.isTrigger)
|
||||
Debug.LogWarning($"[HitBox] {name}: Collider2D.isTrigger 应为 true。", this);
|
||||
// 初始状态关闭碰撞体,防止未激活时产生物理检测
|
||||
_collider.enabled = false;
|
||||
// 缓存 IClashService:OnTriggerEnter2D 为物理热路径,避免每次调用 Dictionary 查找
|
||||
_clashService = ServiceLocator.GetOrDefault<IClashService>();
|
||||
// 缓存宿主投射物(仅 Projectile GameObject 上挂载的 HitBox 非 null)
|
||||
@@ -96,6 +100,7 @@ namespace BaseGames.Combat
|
||||
private void OnDisable()
|
||||
{
|
||||
_isActive = false;
|
||||
if (_collider != null) _collider.enabled = false;
|
||||
_hitThisActivation.Clear();
|
||||
_hitCooldownTimers.Clear();
|
||||
}
|
||||
@@ -180,116 +185,122 @@ namespace BaseGames.Combat
|
||||
{
|
||||
var col = GetComponent<Collider2D>();
|
||||
if (col == null) return;
|
||||
// HitBox:激活 = 鲜红,非激活 = 极淡红轮廓
|
||||
Gizmos.color = _isActive
|
||||
? new Color(1f, 0.15f, 0.15f, 1f)
|
||||
: new Color(1f, 0.15f, 0.15f, 0.2f);
|
||||
DrawCollider2DWire(col);
|
||||
Color fill = _isActive ? new Color(1f, 0.15f, 0.15f, 0.25f) : new Color(1f, 0.15f, 0.15f, 0.05f);
|
||||
Color outline = _isActive ? new Color(1f, 0.15f, 0.15f, 0.90f) : new Color(1f, 0.15f, 0.15f, 0.25f);
|
||||
DrawCollider2DFilled(col, fill, outline);
|
||||
}
|
||||
|
||||
// ────────────────────────────────────────────────────
|
||||
// Gizmo 辅助(内联,不依赖外部工具类)
|
||||
// ────────────────────────────────────────────────────
|
||||
/// <summary>
|
||||
/// 根据 Collider2D 类型绘制正确的 wire 轮廓。
|
||||
/// 使用 <see cref="Gizmos.matrix"/> 统一应用 Transform 的移动/旋转/缩放。
|
||||
/// </summary>
|
||||
public static void DrawCollider2DWire(Collider2D col)
|
||||
// ── Gizmo 辅助(填充 + 轮廓,不依赖外部工具类)──────────────────────────
|
||||
|
||||
/// <summary>根据 Collider2D 类型绘制带填充色和轮廓的 2D Gizmo(供 HurtBox 等复用)。</summary>
|
||||
public static void DrawCollider2DFilled(Collider2D col, Color fill, Color outline)
|
||||
{
|
||||
var prev = Gizmos.matrix;
|
||||
Gizmos.matrix = col.transform.localToWorldMatrix;
|
||||
var prevMatrix = UnityEditor.Handles.matrix;
|
||||
UnityEditor.Handles.matrix = col.transform.localToWorldMatrix;
|
||||
|
||||
switch (col)
|
||||
{
|
||||
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;
|
||||
|
||||
case CapsuleCollider2D caps:
|
||||
DrawWireCapsule2D(caps.offset, caps.size, caps.direction);
|
||||
DrawFilledCapsule2D(caps.offset, caps.size, caps.direction, fill, outline);
|
||||
break;
|
||||
|
||||
case PolygonCollider2D poly:
|
||||
for (int p = 0; p < poly.pathCount; p++)
|
||||
{
|
||||
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);
|
||||
DrawFilledPolygonPath2D(poly.GetPath(p), fill, outline);
|
||||
break;
|
||||
|
||||
default:
|
||||
// 其他类型回退到 bounds 近似(恢复矩阵后在世界空间绘制)
|
||||
Gizmos.matrix = prev;
|
||||
DrawWireRect2D(col.bounds.center, col.bounds.size);
|
||||
return;
|
||||
UnityEditor.Handles.matrix = Matrix4x4.identity;
|
||||
DrawFilledRect2D(col.bounds.center, col.bounds.size, fill, outline);
|
||||
break;
|
||||
}
|
||||
|
||||
Gizmos.matrix = prev;
|
||||
UnityEditor.Handles.matrix = prevMatrix;
|
||||
}
|
||||
|
||||
/// <summary>在当前 Gizmos 坐标系中绘制轴对齐矩形(2D 线框,兼容透视相机)。</summary>
|
||||
public static void DrawWireRect2D(Vector2 center, Vector2 size)
|
||||
/// <summary>向后兼容的线框接口(内部改调 DrawCollider2DFilled)。</summary>
|
||||
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;
|
||||
Vector3 tl = new Vector3(center.x - hx, center.y + hy, 0f);
|
||||
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++)
|
||||
var verts = new Vector3[]
|
||||
{
|
||||
float a = i * step * Mathf.Deg2Rad;
|
||||
Vector3 nextPt = center + new Vector3(Mathf.Cos(a) * radius, Mathf.Sin(a) * radius, 0f);
|
||||
Gizmos.DrawLine(prevPt, nextPt);
|
||||
prevPt = nextPt;
|
||||
}
|
||||
new Vector3(center.x - hx, center.y + hy, 0f),
|
||||
new Vector3(center.x + hx, center.y + hy, 0f),
|
||||
new Vector3(center.x + hx, center.y - hy, 0f),
|
||||
new Vector3(center.x - hx, center.y - hy, 0f),
|
||||
};
|
||||
UnityEditor.Handles.DrawSolidRectangleWithOutline(verts, fill, outline);
|
||||
}
|
||||
|
||||
/// <summary>绘制 2D 胶囊轮廓(在 col.transform 局部坐标系中)。</summary>
|
||||
private static void DrawWireCapsule2D(Vector2 offset, Vector2 size, CapsuleDirection2D dir)
|
||||
private static void DrawFilledCircle2D(Vector2 center, float radius, Color fill, Color outline)
|
||||
{
|
||||
bool vert = dir == CapsuleDirection2D.Vertical;
|
||||
float radius = vert ? size.x * 0.5f : size.y * 0.5f;
|
||||
float half = Mathf.Max(0f, (vert ? size.y : size.x) * 0.5f - radius);
|
||||
Vector2 axis = vert ? Vector2.up : Vector2.right;
|
||||
Vector2 perp = vert ? Vector2.right : Vector2.up;
|
||||
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);
|
||||
Vector3 c = new Vector3(center.x, center.y, 0f);
|
||||
UnityEditor.Handles.color = fill;
|
||||
UnityEditor.Handles.DrawSolidDisc(c, Vector3.back, radius);
|
||||
UnityEditor.Handles.color = outline;
|
||||
UnityEditor.Handles.DrawWireDisc(c, Vector3.back, radius);
|
||||
}
|
||||
|
||||
/// <summary>用多段直线近似绘制 2D 圆弧。</summary>
|
||||
private static void DrawArc2D(Vector2 c, float r, float startDeg, float spanDeg, int segs = 20)
|
||||
private static readonly System.Collections.Generic.List<Vector3> _capsuleVertsBuf
|
||||
= 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;
|
||||
var prev = c + new Vector2(Mathf.Cos(startDeg * Mathf.Deg2Rad) * r,
|
||||
Mathf.Sin(startDeg * Mathf.Deg2Rad) * r);
|
||||
for (int i = 1; i <= segs; i++)
|
||||
bool vert = dir == CapsuleDirection2D.Vertical;
|
||||
float radius = vert ? size.x * 0.5f : size.y * 0.5f;
|
||||
float half = Mathf.Max(0f, (vert ? size.y : size.x) * 0.5f - radius);
|
||||
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;
|
||||
var next = c + new Vector2(Mathf.Cos(a) * r, Mathf.Sin(a) * r);
|
||||
Gizmos.DrawLine(prev, next);
|
||||
prev = next;
|
||||
float a = baseA + (float)i / segs * Mathf.PI;
|
||||
_capsuleVertsBuf.Add(new Vector3(capA.x + Mathf.Cos(a) * radius,
|
||||
capA.y + Mathf.Sin(a) * radius, 0f));
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
@@ -123,13 +123,23 @@ namespace BaseGames.Combat
|
||||
{
|
||||
var col = GetComponent<Collider2D>();
|
||||
if (col == null) return;
|
||||
// HurtBox:正常激活 = 青色,无敌 = 黄色,非激活 = 极淡青
|
||||
Gizmos.color = !_isActive
|
||||
? new Color(0f, 0.85f, 1f, 0.15f)
|
||||
: _isHurtBoxInvincible
|
||||
? new Color(1f, 1f, 0f, 1f )
|
||||
: new Color(0f, 0.85f, 1f, 1f );
|
||||
HitBox.DrawCollider2DWire(col);
|
||||
Color fill, outline;
|
||||
if (!_isActive)
|
||||
{
|
||||
fill = new Color(0f, 0.85f, 1f, 0.05f);
|
||||
outline = new Color(0f, 0.85f, 1f, 0.20f);
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
@@ -13,7 +13,8 @@ namespace BaseGames.Editor
|
||||
/// 分组:
|
||||
/// 移动能力 — WallCling / WallJump / Dash / AirDash / DoubleJump / SuperJump / Swim / DownDash
|
||||
/// 法术能力 — Spell1 / Spell2 / Spell3
|
||||
/// 形态能力 — SpiritForm / SpiritDash
|
||||
/// 灵魄形态 — SpiritForm / SpiritDash
|
||||
/// 三魂形态 — FormTianHun / FormDiHun / FormMingHun
|
||||
/// 战斗能力 — Parry / ChargeAttack / DownSlash
|
||||
/// 互动能力 — Interact / FastTravel
|
||||
/// 能力强化 — InvincibleDash
|
||||
@@ -46,6 +47,12 @@ namespace BaseGames.Editor
|
||||
(AbilityType.SpiritForm, "灵魄形态"),
|
||||
(AbilityType.SpiritDash, "灵魄冲刺"),
|
||||
}),
|
||||
("三魂形态", new[]
|
||||
{
|
||||
(AbilityType.FormTianHun, "天魂"),
|
||||
(AbilityType.FormDiHun, "地魂"),
|
||||
(AbilityType.FormMingHun, "命魂"),
|
||||
}),
|
||||
("战斗能力", new[]
|
||||
{
|
||||
(AbilityType.Parry, "弹反"),
|
||||
|
||||
@@ -51,6 +51,7 @@ namespace BaseGames.Editor
|
||||
}),
|
||||
("法术能力", new[] { AbilityType.Spell1, AbilityType.Spell2, AbilityType.Spell3 }),
|
||||
("灵魄形态", new[] { AbilityType.SpiritForm, AbilityType.SpiritDash }),
|
||||
("三魂形态", new[] { AbilityType.FormTianHun, AbilityType.FormDiHun, AbilityType.FormMingHun }),
|
||||
("战斗能力", new[] { AbilityType.Parry, AbilityType.ChargeAttack, AbilityType.DownSlash }),
|
||||
("互动能力", new[] { AbilityType.Interact, AbilityType.FastTravel }),
|
||||
("能力强化", new[] { AbilityType.InvincibleDash }),
|
||||
@@ -58,12 +59,13 @@ namespace BaseGames.Editor
|
||||
|
||||
private static readonly string[] AbilityFlagNames =
|
||||
{
|
||||
"贴墙悬挂", "墙跳", "地面冲刺", "空中冲刺", "二段跳", "超级跳", "游泳", "下劈",
|
||||
"贴墙悬挂", "墙跳", "地面冲刺", "空中冲刺", "二段跳", "超级跳", "游泳", "下冲刺",
|
||||
"法术槽 1", "法术槽 2", "法术槽 3",
|
||||
"灵魄形态", "灵魄冲刺",
|
||||
"弹反", "蓄力攻击", "下斩",
|
||||
"互动", "快速旅行",
|
||||
"无敌冲刺",
|
||||
"天魂", "地魂", "命魂",
|
||||
};
|
||||
|
||||
// ── 样式(懒初始化)──────────────────────────────────────────────────
|
||||
@@ -469,7 +471,10 @@ namespace BaseGames.Editor
|
||||
AbilityType.DownSlash => "下斩",
|
||||
AbilityType.Interact => "互动",
|
||||
AbilityType.FastTravel => "快速旅行",
|
||||
AbilityType.InvincibleDash => "无敌冲刺",
|
||||
AbilityType.InvincibleDash => "无敌冲刺",
|
||||
AbilityType.FormTianHun => "天魂",
|
||||
AbilityType.FormDiHun => "地魂",
|
||||
AbilityType.FormMingHun => "命魂",
|
||||
_ => flag.ToString(),
|
||||
};
|
||||
|
||||
|
||||
@@ -301,13 +301,24 @@ namespace BaseGames.Enemies
|
||||
#if UNITY_EDITOR
|
||||
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;
|
||||
|
||||
// ── 攻击范围(淡红圆)───────────────────────────────────────
|
||||
Gizmos.color = new Color(1f, 0.15f, 0.15f, 0.22f);
|
||||
HitBox.DrawWireCircle2D(transform.position, _statsSO.AttackRange);
|
||||
UnityEditor.Handles.color = new Color(1f, 0.6f, 0.1f, 0.15f);
|
||||
UnityEditor.Handles.DrawSolidDisc(c, Vector3.back, _statsSO.DetectRange);
|
||||
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 连线 ────────────────────────────────────────
|
||||
if (!Application.isPlaying || _playerTransform == null) return;
|
||||
@@ -319,11 +330,10 @@ namespace BaseGames.Enemies
|
||||
|
||||
// 眼睛位置小圆点(金黄)
|
||||
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)
|
||||
{
|
||||
// 有 LOS → 橙色实线;在范围内但遮挡 → 灰色虚感(透明度低)
|
||||
Gizmos.color = _losResult
|
||||
? new Color(1f, 0.5f, 0f, 0.85f)
|
||||
: new Color(0.6f, 0.6f, 0.6f, 0.25f);
|
||||
@@ -338,11 +348,23 @@ namespace BaseGames.Enemies
|
||||
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);
|
||||
HitBox.DrawWireCircle2D(transform.position, _statsSO.AttackRange);
|
||||
UnityEditor.Handles.color = new Color(1f, 0.6f, 0.1f, 0.25f);
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,6 +45,7 @@ namespace BaseGames.Enemies
|
||||
public void PlayFootstep() { }
|
||||
public void TriggerPreset(string presetId) { }
|
||||
public void PlaySFXById(string sfxId) { }
|
||||
public void PlayFormSwitch(int formIndex) { } // 敌人无形态切换,空实现
|
||||
|
||||
// ── EnemyBase 语义方法 ────────────────────────────────────────────
|
||||
/// <summary>受到伤害时调用(由 EnemyBase 触发)。</summary>
|
||||
|
||||
@@ -44,6 +44,10 @@ namespace BaseGames.Feedback
|
||||
|
||||
/// <summary>通过 sfxId 触发单次音效(不带任何摄像机/振动反馈)。</summary>
|
||||
void PlaySFXById(string sfxId);
|
||||
|
||||
// ── 形态切换 ──────────────────────────────────────────────────────────────
|
||||
/// <summary>切换到指定形态时播放对应反馈(音效 + 粒子 + 震屏等)。formIndex 对应 FormType 枚举值。</summary>
|
||||
void PlayFormSwitch(int formIndex);
|
||||
}
|
||||
|
||||
/// <summary>命中力度。</summary>
|
||||
@@ -67,6 +71,7 @@ namespace BaseGames.Feedback
|
||||
public void PlayFootstep() { }
|
||||
public void TriggerPreset(string presetId) { }
|
||||
public void PlaySFXById(string sfxId) { }
|
||||
public void PlayFormSwitch(int formIndex) { }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -33,6 +33,10 @@ namespace BaseGames.Feedback
|
||||
[Header("脚步声材质检测")]
|
||||
[SerializeField] private BaseGames.Audio.FootstepSoundPlayer _footstepSoundPlayer;
|
||||
|
||||
[Header("形态切换反馈")]
|
||||
[Tooltip("索引与 FormType 枚举值对应:0=天魂, 1=地魂, 2=命魂")]
|
||||
[SerializeField] private MMF_Player[] _onFormSwitch = new MMF_Player[3];
|
||||
|
||||
[Header("命名预设(可选)")]
|
||||
[SerializeField] private NamedFeedback[] _namedPresets;
|
||||
|
||||
@@ -90,6 +94,12 @@ namespace BaseGames.Feedback
|
||||
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)
|
||||
{
|
||||
|
||||
@@ -47,9 +47,15 @@ namespace BaseGames.Player
|
||||
/// </summary>
|
||||
InvincibleDash = 1u << 18,
|
||||
|
||||
// ── 形态解锁(三魂形态)──────────────────────────────────────────────────
|
||||
FormTianHun = 1u << 19, // 天魂形态(默认初始解锁)
|
||||
FormDiHun = 1u << 20, // 地魂形态(游戏进程中解锁)
|
||||
FormMingHun = 1u << 21, // 命魂形态(游戏进程中解锁)
|
||||
|
||||
// ── 组合掩码 ─────────────────────────────────────────────────────────
|
||||
AllMovement = WallCling | WallJump | Dash | DoubleJump | SuperJump | Swim | DownDash | InvincibleDash,
|
||||
AllSpells = Spell1 | Spell2 | Spell3,
|
||||
AllSpirit = SpiritForm | SpiritDash,
|
||||
AllForms = FormTianHun | FormDiHun | FormMingHun,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,11 @@ namespace BaseGames.Player
|
||||
[Header("形态列表 (forms[0]=Sky, forms[1]=Earth, forms[2]=Death)")]
|
||||
public FormSO[] forms;
|
||||
|
||||
[Header("切换冷却")]
|
||||
[Tooltip("形态切换冷却时长(秒)。CD 内重复按键不生效。推荐 0.5。")]
|
||||
[Min(0f)]
|
||||
public float SwitchCooldown = 0.5f;
|
||||
|
||||
/// <summary>按形态类型查找对应 FormSO;找不到返回 null。</summary>
|
||||
public FormSO GetFormByType(FormType type)
|
||||
{
|
||||
|
||||
@@ -3,6 +3,7 @@ using UnityEngine;
|
||||
using BaseGames.Core;
|
||||
using BaseGames.Core.Save;
|
||||
using BaseGames.Core.Events;
|
||||
using BaseGames.Feedback;
|
||||
using BaseGames.Input;
|
||||
|
||||
namespace BaseGames.Player
|
||||
@@ -21,6 +22,9 @@ namespace BaseGames.Player
|
||||
[SerializeField] private FormConfigSO _config;
|
||||
[SerializeField] private InputReaderSO _input;
|
||||
|
||||
[Header("依赖")]
|
||||
[SerializeField] private PlayerStats _stats;
|
||||
|
||||
[Header("事件频道")]
|
||||
[SerializeField] private IntEventChannelSO _onFormChanged; // 广播当前形态索引(UI/Save)
|
||||
[SerializeField] private VoidEventChannelSO _onSkillSetChanged; // 通知 SkillHUD 刷新
|
||||
@@ -29,12 +33,21 @@ namespace BaseGames.Player
|
||||
public FormSO CurrentForm { get; private set; }
|
||||
public FormSO[] AllForms => _config.forms;
|
||||
|
||||
/// <summary>当前切换 CD 剩余时间(秒),0 表示可切换。</summary>
|
||||
public float SwitchCooldownRemaining => _switchCooldownTimer;
|
||||
|
||||
/// <summary>C# 事件,WeaponManager 在 OnEnable 自订阅(架构 05 §6)。</summary>
|
||||
public event Action OnFormChanged;
|
||||
|
||||
private float _switchCooldownTimer;
|
||||
private IFeedbackPlayer _feedback;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
Debug.Assert(_config != null, "[FormController] _config 未赋值,请在 Inspector 中指定 FormConfigSO。", this);
|
||||
if (_stats == null)
|
||||
_stats = GetComponent<PlayerStats>();
|
||||
_feedback = GetComponentInChildren<IFeedbackPlayer>() ?? NullFeedbackPlayer.Instance;
|
||||
}
|
||||
|
||||
private void OnEnable()
|
||||
@@ -61,16 +74,43 @@ namespace BaseGames.Player
|
||||
CurrentForm = _config.forms[0];
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
if (_switchCooldownTimer > 0f)
|
||||
_switchCooldownTimer -= Time.deltaTime;
|
||||
}
|
||||
|
||||
// ── 公共 API ────────────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>切换到指定形态类型。若已在目标形态则不操作。</summary>
|
||||
/// <summary>切换到指定形态类型。若已在目标形态、尚未解锁或 CD 未结束则不操作。</summary>
|
||||
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);
|
||||
if (newForm == null || newForm == CurrentForm) return;
|
||||
|
||||
CurrentForm = newForm;
|
||||
|
||||
// 启动切换 CD
|
||||
if (_config.SwitchCooldown > 0f)
|
||||
_switchCooldownTimer = _config.SwitchCooldown;
|
||||
|
||||
// 播放对应形态切换反馈
|
||||
_feedback.PlayFormSwitch((int)newFormType);
|
||||
|
||||
// 1. SO 事件广播索引(UI/Save)
|
||||
_onFormChanged?.Raise(_config.GetFormIndex(newForm));
|
||||
|
||||
|
||||
@@ -33,6 +33,10 @@ namespace BaseGames.Player
|
||||
[Header("弹簧")]
|
||||
public AnimationClip UseSpring;
|
||||
|
||||
[Header("下冲刺")]
|
||||
[Tooltip("向下冲刺动画。留空则复用 Fall 动画。")]
|
||||
public AnimationClip DownDash;
|
||||
|
||||
[Header("弹反")]
|
||||
public AnimationClip ParryStart;
|
||||
public AnimationClip ParrySuccess;
|
||||
|
||||
@@ -34,21 +34,25 @@ namespace BaseGames.Player
|
||||
{
|
||||
if (_weaponManager != null)
|
||||
_weaponManager.OnWeaponChanged -= HandleWeaponChanged;
|
||||
UnsubscribeDownHit();
|
||||
UnsubscribeHitEvents();
|
||||
}
|
||||
|
||||
private void HandleWeaponChanged(WeaponSO _)
|
||||
{
|
||||
UnsubscribeDownHit();
|
||||
UnsubscribeHitEvents();
|
||||
_currentHitBoxInstance = _weaponManager.ActiveHitBoxInstance;
|
||||
if (_currentHitBoxInstance != null)
|
||||
{
|
||||
_currentHitBoxInstance.OnDownHitConfirmed += HandleDownHitConfirmed;
|
||||
_currentHitBoxInstance.OnHitConfirmed += OnHitConfirmed;
|
||||
}
|
||||
}
|
||||
|
||||
private void UnsubscribeDownHit()
|
||||
private void UnsubscribeHitEvents()
|
||||
{
|
||||
if (_currentHitBoxInstance == null) return;
|
||||
_currentHitBoxInstance.OnDownHitConfirmed -= HandleDownHitConfirmed;
|
||||
_currentHitBoxInstance.OnHitConfirmed -= OnHitConfirmed;
|
||||
_currentHitBoxInstance = null;
|
||||
}
|
||||
|
||||
|
||||
@@ -292,6 +292,15 @@ namespace BaseGames.Player
|
||||
_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>
|
||||
/// 壁滑:将垂直速度限制为 -speed(每帧调用以约束最大下滑速度)。
|
||||
/// WallSlideState.OnStateFixedUpdate 根据正常/受限模式传入不同速度。
|
||||
@@ -422,7 +431,15 @@ namespace BaseGames.Player
|
||||
Gizmos.color = grounded
|
||||
? new Color(0.1f, 1f, 0.3f, 0.9f)
|
||||
: 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. 靠墙检测射线(碰墙橙色 / 未碰淡黄,带端点圆点)─────────
|
||||
@@ -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.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.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
|
||||
}
|
||||
|
||||
|
||||
@@ -54,6 +54,12 @@ namespace BaseGames.Player
|
||||
[Tooltip("无敌的独立冷却(秒)。CD 内再次冲刺不会获得无敌帧,防止连冲变相持续无敌(推荐 0.9s)。")]
|
||||
public float DashInvincibilityCooldown = 0.9f;
|
||||
|
||||
[Header("下冲刺")]
|
||||
[Tooltip("向下冲刺速度(单位/秒)。推荐 22,快速向下穿透空间。")]
|
||||
public float DownDashSpeed = 22f;
|
||||
[Tooltip("向下冲刺持续时长(秒)。推荐 0.25s。")]
|
||||
public float DownDashDuration = 0.25f;
|
||||
|
||||
[Header("抓墙 / 壁滑")]
|
||||
[Tooltip("受限抓墙时(高于 wallGrabY)的下滑速度(单位/秒)。推荐 2。")]
|
||||
public float WallSlideSpeed = 2f;
|
||||
|
||||
@@ -257,12 +257,29 @@ namespace BaseGames.Player
|
||||
}
|
||||
|
||||
// ── Spring ────────────────────────────────────────────────────────────
|
||||
public bool UseSpring()
|
||||
|
||||
/// <summary>
|
||||
/// 仅扣除灵泉充能(SpringState 进入前摇时调用)。
|
||||
/// 回血需前摇结束后调用 <see cref="ApplySpringHeal"/>。
|
||||
/// </summary>
|
||||
public bool ConsumeSpringCharge()
|
||||
{
|
||||
if (CurrentSpringCharges <= 0) return false;
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
@@ -28,9 +28,10 @@ namespace BaseGames.Player
|
||||
|
||||
[Header("初始已解锁能力")]
|
||||
[Tooltip("角色出生时默认持有的能力([Flags] 位掩码)。\n\n" +
|
||||
"FormTianHun:天魂形态默认解锁,请务必勾选。\n\n" +
|
||||
"Dash:地面与空中冲刺统一控制,勾选后即可使用 DashState。\n\n" +
|
||||
"DoubleJump:追加次数由 PlayerMovementConfigSO.MaxAirJumps 决定。\n" +
|
||||
"落地或 Pogo 命中后次数自动重置。")]
|
||||
public AbilityType InitialAbilities = AbilityType.None;
|
||||
public AbilityType InitialAbilities = AbilityType.FormTianHun;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -112,7 +112,10 @@ namespace BaseGames.Player.States
|
||||
var animState = Anim.Play(step.clip);
|
||||
animState.Speed *= spd; // 在 ClipTransition 自身速度基础上叠加攻速
|
||||
|
||||
// 每次重播同一 ClipTransition 会复用同一 AnimancerState,
|
||||
// 必须先清除旧事件再注册新事件,否则回调会累积叠加。
|
||||
var events = animState.Events(this);
|
||||
events.Clear();
|
||||
events.OnEnd = OnClipEnd;
|
||||
|
||||
// HitBox 时间窗口(capture step by value for closure safety)
|
||||
@@ -121,11 +124,24 @@ namespace BaseGames.Player.States
|
||||
Owner.Combat?.EnableWeaponHitBox(AttackDirection.Ground,
|
||||
capturedStep.hitBoxId, capturedStep.damageSource));
|
||||
events.Add(capturedStep.hitBoxExit, () => Owner.Combat?.DisableAllWeaponHitBoxes());
|
||||
|
||||
// 连击输入窗口
|
||||
if (capturedStep.comboInputOpen > 0f)
|
||||
events.Add(capturedStep.comboInputOpen, () => _comboWindowOpen = true);
|
||||
{
|
||||
events.Add(capturedStep.comboInputOpen, () =>
|
||||
{
|
||||
_comboWindowOpen = true;
|
||||
// 窗口刚开时,补检查 InputBuffer——玩家可能在窗口前就提前按键
|
||||
if (!_comboInputPending && Buffer.ConsumeAttack())
|
||||
_comboInputPending = true;
|
||||
});
|
||||
}
|
||||
else
|
||||
_comboWindowOpen = true; // 0 = 立即开放
|
||||
{
|
||||
_comboWindowOpen = true;
|
||||
if (!_comboInputPending && Buffer.ConsumeAttack())
|
||||
_comboInputPending = true;
|
||||
}
|
||||
|
||||
if (capturedStep.comboInputClose > 0f)
|
||||
events.Add(capturedStep.comboInputClose, () => _comboWindowOpen = false);
|
||||
@@ -141,11 +157,18 @@ namespace BaseGames.Player.States
|
||||
_comboWindowOpen = false;
|
||||
Move.SetCancelWindowOpen(false);
|
||||
|
||||
// 如果已有缓存输入,直接推进(零延迟连击)
|
||||
// 有缓存连击输入且还不是最后一段 → 零延迟推进到下一段
|
||||
if (_comboInputPending)
|
||||
{
|
||||
AdvanceCombo();
|
||||
return;
|
||||
_comboInputPending = false;
|
||||
int maxCombo = Owner.Weapon?.ActiveWeapon?.GroundComboCount ?? 1;
|
||||
if (_comboIndex < maxCombo - 1)
|
||||
{
|
||||
_comboIndex++;
|
||||
PlayAttackClip();
|
||||
return; // 新动画已开始,不进入等待阶段
|
||||
}
|
||||
// 已是最后一段:消耗掉多余输入,继续进入等待阶段(不 return)
|
||||
}
|
||||
|
||||
// 进入动画后等待阶段
|
||||
@@ -169,7 +192,7 @@ namespace BaseGames.Player.States
|
||||
}
|
||||
else
|
||||
{
|
||||
// 已是最后一段,忽略多余输入,等待超时
|
||||
// 已是最后一段,忽略多余输入,等待超时回 Idle
|
||||
_comboInputPending = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,6 +29,9 @@ namespace BaseGames.Player.States
|
||||
/// <summary>重置冲刺次数(落地或 Pogo 时由 IdleState/RunState/DownAttackState 调用)。</summary>
|
||||
public void ResetDashCharge() => _dashChargeUsed = false;
|
||||
|
||||
/// <summary>消耗空中冲刺次数(DownDashState 进入时调用,与普通空中冲刺共享次数上限)。</summary>
|
||||
public void ConsumeAirDashCharge() => _dashChargeUsed = true;
|
||||
|
||||
/// <summary>
|
||||
/// 无敌帧是否已冷却,即本次冲刺可以获得无敌。
|
||||
/// </summary>
|
||||
|
||||
62
Assets/_Game/Scripts/Player/States/DownDashState.cs
Normal file
62
Assets/_Game/Scripts/Player/States/DownDashState.cs
Normal 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>());
|
||||
}
|
||||
}
|
||||
}
|
||||
11
Assets/_Game/Scripts/Player/States/DownDashState.cs.meta
Normal file
11
Assets/_Game/Scripts/Player/States/DownDashState.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b5c828df0b1d49d4bb45ced1da093e7b
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -6,6 +6,7 @@ namespace BaseGames.Player.States
|
||||
/// 下落状态。
|
||||
/// - 郊狼跳:CoyoteTimer > 0 时按跳跃 → 一段跳(JumpState,使用 JumpForce)。
|
||||
/// - 空中跳跃:CoyoteTimer 耗尽后按跳跃且 AirJumpsLeft > 0 → JumpState(使用 DoubleJumpForce)。
|
||||
/// - 下冲刺:HasAbility(DownDash) && 下方向 + 冲刺键 → DownDashState(优先于普通冲刺)。
|
||||
/// - 冲刺:HasAbility(Dash) && DashState.CanDashMidAir → DashState(地面与空中统一,空中限一次)。
|
||||
/// - 抓墙:贴墙时按下朝向墙壁的方向键 → WallSlideState。
|
||||
/// - 增强下落重力(FallGravityMult)确保下落快于上升,手感紧实。
|
||||
@@ -53,9 +54,20 @@ namespace BaseGames.Player.States
|
||||
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)────────────────────────────
|
||||
// 先确认能力与冷却均满足,再消耗缓冲,避免无操作时静默吃掉输入
|
||||
var dashState = Owner.GetState<DashState>();
|
||||
if (dashState != null && dashState.CanDashMidAir
|
||||
&& Stats != null && Stats.HasAbility(AbilityType.Dash)
|
||||
&& Buffer.ConsumeDash())
|
||||
|
||||
@@ -52,9 +52,20 @@ namespace BaseGames.Player.States
|
||||
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,空中限一次,优先于二段跳:冲刺可保存二段跳机会)
|
||||
// 先确认能力与冷却均满足,再消耗缓冲,避免无操作时静默吃掉输入
|
||||
var dashState = Owner.GetState<DashState>();
|
||||
if (dashState != null && dashState.CanDashMidAir
|
||||
&& Stats != null && Stats.HasAbility(AbilityType.Dash)
|
||||
&& Buffer.ConsumeDash())
|
||||
|
||||
@@ -350,6 +350,7 @@ namespace BaseGames.Player.States
|
||||
_states[typeof(FallState)] = new FallState(this);
|
||||
_states[typeof(AttackState)] = new AttackState(this);
|
||||
_states[typeof(DashState)] = new DashState(this);
|
||||
_states[typeof(DownDashState)] = new DownDashState(this);
|
||||
_states[typeof(WallSlideState)] = new WallSlideState(this);
|
||||
_states[typeof(WallJumpState)] = new WallJumpState(this);
|
||||
_states[typeof(AirAttackState)] = new AirAttackState(this);
|
||||
|
||||
@@ -14,11 +14,11 @@ namespace BaseGames.Player.States
|
||||
|
||||
public override void OnStateEnter()
|
||||
{
|
||||
// 消耗灵泉充能并治疗(PlayerStats.UseSpring 内部回复 HP)
|
||||
bool used = Stats?.UseSpring() ?? false;
|
||||
// 前摇开始时只扣除充能,不立即回血;回血在前摇结束后的 OnSpringEnd 中执行。
|
||||
// 若前摇被打断(受伤 → HurtState),OnStateExit 被调用,充能已扣除但 OnSpringEnd 不会执行,回血失败。
|
||||
bool used = Stats?.ConsumeSpringCharge() ?? false;
|
||||
if (!used)
|
||||
{
|
||||
// 无充能时立即退出
|
||||
Owner.TransitionTo(Owner.GetState<IdleState>());
|
||||
return;
|
||||
}
|
||||
@@ -37,7 +37,7 @@ namespace BaseGames.Player.States
|
||||
}
|
||||
}
|
||||
|
||||
// 无动画则直接结束
|
||||
// 无动画配置则直接结束(视为前摇瞬间完成)
|
||||
OnSpringEnd();
|
||||
}
|
||||
|
||||
@@ -49,6 +49,8 @@ namespace BaseGames.Player.States
|
||||
|
||||
private void OnSpringEnd()
|
||||
{
|
||||
// 前摇正常结束 → 执行回血
|
||||
Stats?.ApplySpringHeal();
|
||||
Owner.TransitionTo(Owner.GetState<IdleState>());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,6 +32,9 @@ namespace BaseGames.Player
|
||||
/// <summary>下劈命中确认事件(供 DownAttackState Pogo 逻辑)。</summary>
|
||||
public event System.Action<DamageInfo> OnDownHitConfirmed;
|
||||
|
||||
/// <summary>任意 HitBox 命中确认事件(供 PlayerCombat 订阅通用命中反馈)。</summary>
|
||||
public event System.Action<DamageInfo> OnHitConfirmed;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
_allHitBoxes = GetComponentsInChildren<HitBox>(true);
|
||||
@@ -41,6 +44,7 @@ namespace BaseGames.Player
|
||||
|
||||
private void OnAnyHitConfirmed(DamageInfo info)
|
||||
{
|
||||
OnHitConfirmed?.Invoke(info);
|
||||
if (_activeDir == AttackDirection.Down)
|
||||
OnDownHitConfirmed?.Invoke(info);
|
||||
}
|
||||
|
||||
@@ -27,6 +27,9 @@ namespace BaseGames.Player
|
||||
// 护符注入的武器覆盖:Key = FormSO.formId,Value = 替换武器(架构 05 §7)
|
||||
private readonly Dictionary<string, WeaponSO> _overrides = new();
|
||||
|
||||
// 对象池:避免每次切换形态时 Instantiate/Destroy(Key = WeaponSO,Value = 已创建实例)
|
||||
private readonly Dictionary<WeaponSO, WeaponHitBoxInstance> _hitBoxPool = new();
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
if (_formController != null && _formController.CurrentForm != null)
|
||||
@@ -70,22 +73,33 @@ namespace BaseGames.Player
|
||||
|
||||
private void SetDirectWeapon(WeaponSO weapon)
|
||||
{
|
||||
var oldInstance = ActiveHitBoxInstance;
|
||||
// 归还旧实例到池(SetActive(false) 会触发 HitBox.OnDisable → Deactivate,自动关闭 Collider2D)
|
||||
ActiveHitBoxInstance?.gameObject.SetActive(false);
|
||||
|
||||
ActiveWeapon = weapon;
|
||||
ActiveHitBoxInstance = null;
|
||||
|
||||
if (weapon?.hitBoxPrefab != null && _weaponSocket != null)
|
||||
{
|
||||
var go = Instantiate(weapon.hitBoxPrefab, _weaponSocket);
|
||||
ActiveHitBoxInstance = go.GetComponent<WeaponHitBoxInstance>();
|
||||
if (!_hitBoxPool.TryGetValue(weapon, out var pooled) || pooled == null)
|
||||
{
|
||||
var go = Instantiate(weapon.hitBoxPrefab, _weaponSocket);
|
||||
pooled = go.GetComponent<WeaponHitBoxInstance>();
|
||||
_hitBoxPool[weapon] = pooled;
|
||||
}
|
||||
pooled.gameObject.SetActive(true);
|
||||
ActiveHitBoxInstance = pooled;
|
||||
}
|
||||
|
||||
// 先通知订阅者(使其有机会取消对旧实例的事件订阅),再销毁旧实例
|
||||
// 通知订阅者(PlayerCombat 取消旧实例事件订阅,订阅新实例)
|
||||
OnWeaponChanged?.Invoke(weapon);
|
||||
}
|
||||
|
||||
if (oldInstance != null)
|
||||
Destroy(oldInstance.gameObject);
|
||||
private void OnDestroy()
|
||||
{
|
||||
foreach (var inst in _hitBoxPool.Values)
|
||||
if (inst != null) Destroy(inst.gameObject);
|
||||
_hitBoxPool.Clear();
|
||||
}
|
||||
|
||||
// ── 护符 Override API(由 WeaponOverrideEffect 调用,架构 05 §7)──────
|
||||
|
||||
Reference in New Issue
Block a user