摄像机区域的架构改动
This commit is contained in:
56
Assets/_Game/Scripts/Player/AbilityType.cs
Normal file
56
Assets/_Game/Scripts/Player/AbilityType.cs
Normal file
@@ -0,0 +1,56 @@
|
||||
using System;
|
||||
|
||||
namespace BaseGames.Player
|
||||
{
|
||||
/// <summary>
|
||||
/// 玩家能力解锁标志位枚举([Flags] uint)。
|
||||
/// 每个 bit 对应一项可解锁能力,支持组合查询。
|
||||
/// 对应 Progression 模块中 AbilityManager 的解锁状态。
|
||||
/// </summary>
|
||||
[Flags]
|
||||
public enum AbilityType : uint
|
||||
{
|
||||
None = 0,
|
||||
|
||||
// ── 移动能力 ──────────────────────────────────────────────────────
|
||||
WallCling = 1u << 0, // 贴墙悬挂
|
||||
WallJump = 1u << 1, // 墙跳
|
||||
Dash = 1u << 2, // 地面冲刺
|
||||
AirDash = 1u << 3, // 空中冲刺(二段冲刺)
|
||||
DoubleJump = 1u << 4, // 二段跳
|
||||
SuperJump = 1u << 5, // 超级跳(聚气跳)
|
||||
Swim = 1u << 6, // 游泳(液体中自由移动)
|
||||
Dive = 1u << 7, // 下劈(空中下突)
|
||||
|
||||
// ── 法术能力 ──────────────────────────────────────────────────────
|
||||
Spell1 = 1u << 8, // 法术槽 1(策划自定义)
|
||||
Spell2 = 1u << 9, // 法术槽 2
|
||||
Spell3 = 1u << 10, // 法术槽 3
|
||||
|
||||
// ── 灵魄形态(泽灵特有)──────────────────────────────────────────
|
||||
SpiritForm = 1u << 11, // 灵魄形态切换
|
||||
SpiritDash = 1u << 12, // 灵魄冲刺(穿透地形)
|
||||
|
||||
// ── 战斗能力 ──────────────────────────────────────────────────────
|
||||
Parry = 1u << 13, // 格挡/弹反
|
||||
ChargeAttack = 1u << 14, // 蓄力攻击
|
||||
DownSlash = 1u << 15, // 下斩
|
||||
|
||||
// ── 互动能力 ──────────────────────────────────────────────────────
|
||||
Interact = 1u << 16, // 互动(NPC/机关)
|
||||
FastTravel = 1u << 17, // 快速旅行解锁
|
||||
|
||||
// ── 能力强化 ──────────────────────────────────────────────────────────
|
||||
/// <summary>
|
||||
/// 无敌冲刺强化(类比空洞骑士暗影披风)。
|
||||
/// 仅持有 Dash 时:冲刺无无敌帧。
|
||||
/// 解锁 InvincibleDash 后:冲刺期间完全无敌(地面 DashState + 空中 AerialDashState)。
|
||||
/// </summary>
|
||||
InvincibleDash = 1u << 18,
|
||||
|
||||
// ── 组合掩码 ─────────────────────────────────────────────────────────
|
||||
AllMovement = WallCling | WallJump | Dash | AirDash | DoubleJump | SuperJump | Swim | Dive | InvincibleDash,
|
||||
AllSpells = Spell1 | Spell2 | Spell3,
|
||||
AllSpirit = SpiritForm | SpiritDash,
|
||||
}
|
||||
}
|
||||
11
Assets/_Game/Scripts/Player/AbilityType.cs.meta
Normal file
11
Assets/_Game/Scripts/Player/AbilityType.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ecb590541d6e9c54f9a4d0524ef78292
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
15
Assets/_Game/Scripts/Player/AbilityTypeEventChannelSO.cs
Normal file
15
Assets/_Game/Scripts/Player/AbilityTypeEventChannelSO.cs
Normal file
@@ -0,0 +1,15 @@
|
||||
using BaseGames.Core.Events;
|
||||
using UnityEngine;
|
||||
|
||||
namespace BaseGames.Player
|
||||
{
|
||||
/// <summary>
|
||||
/// 能力解锁事件频道(内部类型保留)。
|
||||
/// ⚠️ EVT_AbilityUnlocked 资产实际使用 StringEventChannelSO(abilityId 字符串),
|
||||
/// 本频道供需要类型安全枚举的内部系统(如 FormController)使用。
|
||||
/// 发布:AbilityManager.UnlockAbility()
|
||||
/// 订阅:PlayerController(刷新可用动作集)、HUDController(更新技能面板)
|
||||
/// </summary>
|
||||
[CreateAssetMenu(menuName = "BaseGames/Events/Player/AbilityType")]
|
||||
public class AbilityTypeEventChannelSO : BaseEventChannelSO<AbilityType> { }
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 4c8eab760bd9b37468f12fa79d3d0693
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
23
Assets/_Game/Scripts/Player/BaseGames.Player.asmdef
Normal file
23
Assets/_Game/Scripts/Player/BaseGames.Player.asmdef
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"excludePlatforms": [],
|
||||
"allowUnsafeCode": false,
|
||||
"precompiledReferences": [],
|
||||
"name": "BaseGames.Player",
|
||||
"defineConstraints": [],
|
||||
"noEngineReferences": false,
|
||||
"versionDefines": [],
|
||||
"rootNamespace": "BaseGames.Player",
|
||||
"references": [
|
||||
"BaseGames.Core",
|
||||
"BaseGames.Core.Save",
|
||||
"BaseGames.Core.Events",
|
||||
"BaseGames.Input",
|
||||
"BaseGames.Combat",
|
||||
"BaseGames.Parry",
|
||||
"BaseGames.Feedback",
|
||||
"Kybernetik.Animancer"
|
||||
],
|
||||
"autoReferenced": true,
|
||||
"overrideReferences": false,
|
||||
"includePlatforms": []
|
||||
}
|
||||
7
Assets/_Game/Scripts/Player/BaseGames.Player.asmdef.meta
Normal file
7
Assets/_Game/Scripts/Player/BaseGames.Player.asmdef.meta
Normal file
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a7ddc97eeb30a5a408e5fb7e472ff6fa
|
||||
AssemblyDefinitionImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
34
Assets/_Game/Scripts/Player/FormConfigSO.cs
Normal file
34
Assets/_Game/Scripts/Player/FormConfigSO.cs
Normal file
@@ -0,0 +1,34 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace BaseGames.Player
|
||||
{
|
||||
/// <summary>
|
||||
/// 形态切换配置 ScriptableObject。
|
||||
/// 单一资产,forms[0..2] = Sky / Earth / Death(架构 05 §18)。
|
||||
/// ⚠️ 只有 1 个 FormConfigSO 资产;FormSO × 3 存于 forms[] 数组中,不分开建立。
|
||||
/// </summary>
|
||||
[CreateAssetMenu(menuName = "BaseGames/Player/FormConfig")]
|
||||
public class FormConfigSO : ScriptableObject
|
||||
{
|
||||
[Header("形态列表 (forms[0]=Sky, forms[1]=Earth, forms[2]=Death)")]
|
||||
public FormSO[] forms;
|
||||
|
||||
/// <summary>按形态类型查找对应 FormSO;找不到返回 null。</summary>
|
||||
public FormSO GetFormByType(FormType type)
|
||||
{
|
||||
if (forms == null) return null;
|
||||
foreach (var f in forms)
|
||||
if (f != null && f.formType == type) return f;
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>返回指定 FormSO 在 forms 数组中的索引;找不到返回 -1。</summary>
|
||||
public int GetFormIndex(FormSO form)
|
||||
{
|
||||
if (forms == null || form == null) return -1;
|
||||
for (int i = 0; i < forms.Length; i++)
|
||||
if (forms[i] == form) return i;
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
11
Assets/_Game/Scripts/Player/FormConfigSO.cs.meta
Normal file
11
Assets/_Game/Scripts/Player/FormConfigSO.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 59dd9c303ae12724085e79b1e9b55645
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
71
Assets/_Game/Scripts/Player/FormController.cs
Normal file
71
Assets/_Game/Scripts/Player/FormController.cs
Normal file
@@ -0,0 +1,71 @@
|
||||
using System;
|
||||
using UnityEngine;
|
||||
using BaseGames.Core.Events;
|
||||
|
||||
namespace BaseGames.Player
|
||||
{
|
||||
/// <summary>
|
||||
/// 形态控制器。
|
||||
/// 管理天魂/地魂/命魂三形态切换,依次触发:
|
||||
/// 1. _onFormChanged SO 事件(UI/Save 用)
|
||||
/// 2. OnFormChanged C# 事件(WeaponManager 订阅)
|
||||
/// 3. _onSkillSetChanged SO 事件(SkillHUD 刷新)
|
||||
/// 架构 05_PlayerModule §6。
|
||||
/// </summary>
|
||||
public class FormController : MonoBehaviour
|
||||
{
|
||||
[Header("配置")]
|
||||
[SerializeField] private FormConfigSO _config;
|
||||
|
||||
[Header("事件频道")]
|
||||
[SerializeField] private IntEventChannelSO _onFormChanged; // 广播当前形态索引(UI/Save)
|
||||
[SerializeField] private VoidEventChannelSO _onSkillSetChanged; // 通知 SkillHUD 刷新
|
||||
|
||||
// ── 运行时 ─────────────────────────────────────────────────────────────
|
||||
public FormSO CurrentForm { get; private set; }
|
||||
public FormSO[] AllForms => _config.forms;
|
||||
|
||||
/// <summary>C# 事件,WeaponManager 在 OnEnable 自订阅(架构 05 §6)。</summary>
|
||||
public event Action OnFormChanged;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
Debug.Assert(_config != null, "[FormController] _config 未赋值,请在 Inspector 中指定 FormConfigSO。", this);
|
||||
}
|
||||
|
||||
private void Start()
|
||||
{
|
||||
if (_config.forms != null && _config.forms.Length > 0)
|
||||
CurrentForm = _config.forms[0];
|
||||
}
|
||||
|
||||
// ── 公共 API ────────────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>切换到指定形态类型。若已在目标形态则不操作。</summary>
|
||||
public void SwitchForm(FormType newFormType)
|
||||
{
|
||||
FormSO newForm = _config.GetFormByType(newFormType);
|
||||
if (newForm == null || newForm == CurrentForm) return;
|
||||
|
||||
CurrentForm = newForm;
|
||||
|
||||
// 1. SO 事件广播索引(UI/Save)
|
||||
_onFormChanged?.Raise(_config.GetFormIndex(newForm));
|
||||
|
||||
// 2. C# 事件(WeaponManager 等订阅者)
|
||||
OnFormChanged?.Invoke();
|
||||
|
||||
// 3. SkillHUD 刷新事件
|
||||
_onSkillSetChanged?.Raise();
|
||||
}
|
||||
|
||||
/// <summary>通过数组索引切换形态。</summary>
|
||||
public void SwitchToFormByIndex(int index)
|
||||
{
|
||||
if (_config?.forms == null || index < 0 || index >= _config.forms.Length) return;
|
||||
var form = _config.forms[index];
|
||||
if (form != null)
|
||||
SwitchForm(form.formType);
|
||||
}
|
||||
}
|
||||
}
|
||||
11
Assets/_Game/Scripts/Player/FormController.cs.meta
Normal file
11
Assets/_Game/Scripts/Player/FormController.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: cbcc6974256e3fb40879694b4bf2d2dc
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
24
Assets/_Game/Scripts/Player/FormSO.cs
Normal file
24
Assets/_Game/Scripts/Player/FormSO.cs
Normal file
@@ -0,0 +1,24 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace BaseGames.Player
|
||||
{
|
||||
/// <summary>
|
||||
/// 单一形态数据 ScriptableObject(架构 05_PlayerModule §18)。
|
||||
/// 存储于 FormConfigSO.forms[] 数组中,不单独作为 FormConfigSO 资产。
|
||||
/// ⚠️ 补充 formType 字段(架构 05 §18 遗漏,以架构 18 §10 ApplyPalette(FormType) 为准)。
|
||||
/// </summary>
|
||||
[CreateAssetMenu(menuName = "BaseGames/Player/Form")]
|
||||
public class FormSO : ScriptableObject
|
||||
{
|
||||
[Header("基础信息")]
|
||||
public string formId; // 全局唯一 ID,如 "Form_Sky"
|
||||
public string displayName; // 显示名,如 "天魂"
|
||||
public FormType formType; // 对应枚举值,供 ApplyPalette / 条件判断使用
|
||||
|
||||
[Header("武器")]
|
||||
public WeaponSO defaultWeapon; // 此形态的默认武器(护符可通过 Override 覆盖)
|
||||
|
||||
[Header("外观")]
|
||||
public Color formAccentColor = Color.white; // 调色盘主色(仅供参考,VFX 以 formType 枚举为准)
|
||||
}
|
||||
}
|
||||
11
Assets/_Game/Scripts/Player/FormSO.cs.meta
Normal file
11
Assets/_Game/Scripts/Player/FormSO.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 62cea04b1d7bfe64ea7acf4a501a44d3
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
13
Assets/_Game/Scripts/Player/FormType.cs
Normal file
13
Assets/_Game/Scripts/Player/FormType.cs
Normal file
@@ -0,0 +1,13 @@
|
||||
namespace BaseGames.Player
|
||||
{
|
||||
/// <summary>
|
||||
/// 玩家形态枚举。对应三形态切换系统(天魂/地魂/命魂)。
|
||||
/// ApplyPalette(FormType) 以此为参数(架构 18_VFXFeedbackModule §10)。
|
||||
/// </summary>
|
||||
public enum FormType
|
||||
{
|
||||
TianHun = 0, // 天魂形态:裂空刃,高频轻击
|
||||
DiHun = 1, // 地魂形态:地震锤,低频重击
|
||||
MingHun = 2, // 命魂形态:命镰,穿透直线斩
|
||||
}
|
||||
}
|
||||
11
Assets/_Game/Scripts/Player/FormType.cs.meta
Normal file
11
Assets/_Game/Scripts/Player/FormType.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7daccda5fa8aaac429b688d919201c82
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
64
Assets/_Game/Scripts/Player/PlayerAnimationConfigSO.cs
Normal file
64
Assets/_Game/Scripts/Player/PlayerAnimationConfigSO.cs
Normal file
@@ -0,0 +1,64 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace BaseGames.Player
|
||||
{
|
||||
[CreateAssetMenu(menuName = "BaseGames/Player/AnimationConfig")]
|
||||
public class PlayerAnimationConfigSO : ScriptableObject
|
||||
{
|
||||
[Header("移动")]
|
||||
public AnimationClip Idle;
|
||||
public AnimationClip Run;
|
||||
public AnimationClip Jump;
|
||||
public AnimationClip Fall;
|
||||
public AnimationClip Dash;
|
||||
|
||||
[Header("墙")]
|
||||
public AnimationClip WallSlide;
|
||||
|
||||
[Header("受伤 / 死亡")]
|
||||
public AnimationClip Hurt;
|
||||
public AnimationClip Dead;
|
||||
[Tooltip("受击硬直最短持续时间(秒),动画早于此时间结束也不会提前退出)")]
|
||||
public float HurtDuration = 0.4f;
|
||||
|
||||
[Header("弹簧")]
|
||||
public AnimationClip UseSpring;
|
||||
|
||||
[Header("地面攻击(连招序列)")]
|
||||
public AnimationClip[] GroundAttacks;
|
||||
|
||||
/// <summary>每段连击的 HitBox 开启/关闭时间点(归一化 0-1),与 GroundAttacks 索引对应。</summary>
|
||||
[System.Serializable]
|
||||
public struct AttackTimings
|
||||
{
|
||||
[Tooltip("HitBox 开启时间点(归一化 0-1)")]
|
||||
[UnityEngine.Range(0f, 1f)] public float HitBoxEnter;
|
||||
[Tooltip("HitBox 关闭时间点(归一化 0-1)")]
|
||||
[UnityEngine.Range(0f, 1f)] public float HitBoxExit;
|
||||
}
|
||||
|
||||
[Header("地面攻击 HitBox 激活窗口(归一化时间 0-1)")]
|
||||
[Tooltip("每段连击 HitBox 开启/关闭时间点,与 GroundAttacks 索引对应")]
|
||||
public AttackTimings[] GroundAttackTimings = { new AttackTimings { HitBoxEnter = 0.3f, HitBoxExit = 0.6f } };
|
||||
|
||||
[Header("空中攻击")]
|
||||
public AnimationClip AirAttack;
|
||||
public AnimationClip UpAttack;
|
||||
public AnimationClip DownAttack; // 戳击 (Pogo)
|
||||
|
||||
[Header("弹反")]
|
||||
public AnimationClip ParryStart;
|
||||
public AnimationClip ParrySuccess;
|
||||
|
||||
[Header("游泳")]
|
||||
public AnimationClip SwimIdle;
|
||||
public AnimationClip SwimMove;
|
||||
|
||||
/// <summary>按连招步骤取地面攻击动画,越界自动取最后一个。</summary>
|
||||
public AnimationClip GetAttackClip(int step)
|
||||
{
|
||||
if (GroundAttacks == null || GroundAttacks.Length == 0) return null;
|
||||
return step < GroundAttacks.Length ? GroundAttacks[step] : GroundAttacks[^1];
|
||||
}
|
||||
}
|
||||
}
|
||||
11
Assets/_Game/Scripts/Player/PlayerAnimationConfigSO.cs.meta
Normal file
11
Assets/_Game/Scripts/Player/PlayerAnimationConfigSO.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5ec15df6b0d345c4f92ba459e89dc02f
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
94
Assets/_Game/Scripts/Player/PlayerCombat.cs
Normal file
94
Assets/_Game/Scripts/Player/PlayerCombat.cs
Normal file
@@ -0,0 +1,94 @@
|
||||
using UnityEngine;
|
||||
using BaseGames.Combat;
|
||||
|
||||
namespace BaseGames.Player
|
||||
{
|
||||
/// <summary>
|
||||
/// 玩家战斗组件。
|
||||
/// HitBox 随装备武器动态实例化到 [WeaponSocket] 子节点,通过 WeaponManager.ActiveHitBoxInstance 访问。
|
||||
/// </summary>
|
||||
public class PlayerCombat : MonoBehaviour
|
||||
{
|
||||
[SerializeField] private WeaponManager _weaponManager;
|
||||
|
||||
private PlayerStats _stats;
|
||||
private WeaponHitBoxInstance _currentHitBoxInstance;
|
||||
|
||||
/// <summary>下劈 HitBox 命中确认事件(供 DownAttackState 订阅 pogo 弹跳逻辑)。</summary>
|
||||
public event System.Action<DamageInfo> OnDownHitConfirmed;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
_stats = GetComponentInParent<PlayerStats>();
|
||||
}
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
if (_weaponManager != null)
|
||||
_weaponManager.OnWeaponChanged += HandleWeaponChanged;
|
||||
}
|
||||
|
||||
private void OnDisable()
|
||||
{
|
||||
if (_weaponManager != null)
|
||||
_weaponManager.OnWeaponChanged -= HandleWeaponChanged;
|
||||
UnsubscribeDownHit();
|
||||
}
|
||||
|
||||
private void HandleWeaponChanged(WeaponSO _)
|
||||
{
|
||||
UnsubscribeDownHit();
|
||||
_currentHitBoxInstance = _weaponManager.ActiveHitBoxInstance;
|
||||
if (_currentHitBoxInstance != null)
|
||||
_currentHitBoxInstance.OnDownHitConfirmed += HandleDownHitConfirmed;
|
||||
}
|
||||
|
||||
private void UnsubscribeDownHit()
|
||||
{
|
||||
if (_currentHitBoxInstance == null) return;
|
||||
_currentHitBoxInstance.OnDownHitConfirmed -= HandleDownHitConfirmed;
|
||||
_currentHitBoxInstance = null;
|
||||
}
|
||||
|
||||
private void HandleDownHitConfirmed(DamageInfo info) => OnDownHitConfirmed?.Invoke(info);
|
||||
|
||||
// ── 连击段伤害来源切换 ────────────────────────────────────────────────
|
||||
/// <summary>
|
||||
/// 根据当前连招段切换 HitBox 的 DamageSource(由 AttackState 在每段开始时调用)。
|
||||
/// </summary>
|
||||
public void SetComboSegmentSource(int comboIndex)
|
||||
{
|
||||
WeaponSO w = _weaponManager?.ActiveWeapon;
|
||||
if (w == null) return;
|
||||
|
||||
DamageSourceSO src = comboIndex switch
|
||||
{
|
||||
0 => w.attack1Source,
|
||||
1 => w.attack2Source,
|
||||
2 => w.attack3Source,
|
||||
_ => w.attack1Source,
|
||||
};
|
||||
_weaponManager.ActiveHitBoxInstance?.SetDamageSource(AttackDirection.Ground, src);
|
||||
}
|
||||
|
||||
// ── HitBox 激活(由 State / AnimationEvent 调用)─────────────────────
|
||||
public void EnableWeaponHitBox(AttackDirection dir)
|
||||
{
|
||||
var source = _weaponManager?.ActiveWeapon?.GetSourceByDir(dir);
|
||||
_weaponManager?.ActiveHitBoxInstance?.Activate(dir, source, transform);
|
||||
}
|
||||
|
||||
public void DisableWeaponHitBox(AttackDirection dir)
|
||||
=> _weaponManager?.ActiveHitBoxInstance?.Deactivate(dir);
|
||||
|
||||
public void DisableAllWeaponHitBoxes()
|
||||
=> _weaponManager?.ActiveHitBoxInstance?.DeactivateAll();
|
||||
|
||||
/// <summary>命中确认回调:增加灵力(由 HurtBox.ReceiveDamage 步骤 7 的 HitConfirmed 事件订阅)。</summary>
|
||||
public void OnHitConfirmed(DamageInfo info)
|
||||
{
|
||||
int gain = _weaponManager?.ActiveWeapon?.soulPowerGain ?? 10;
|
||||
_stats?.AddSoulPower(gain);
|
||||
}
|
||||
}
|
||||
}
|
||||
11
Assets/_Game/Scripts/Player/PlayerCombat.cs.meta
Normal file
11
Assets/_Game/Scripts/Player/PlayerCombat.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 9d978725c6a901c4da85041223e2b0ee
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
271
Assets/_Game/Scripts/Player/PlayerMovement.cs
Normal file
271
Assets/_Game/Scripts/Player/PlayerMovement.cs
Normal file
@@ -0,0 +1,271 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace BaseGames.Player
|
||||
{
|
||||
/// <summary>
|
||||
/// 玩家物理移动组件。封装 Rigidbody2D 操作,提供跑动、跳跃、击退等接口。
|
||||
|
||||
/// </summary>
|
||||
[RequireComponent(typeof(Rigidbody2D))]
|
||||
public class PlayerMovement : MonoBehaviour
|
||||
{
|
||||
[Header("配置")]
|
||||
[SerializeField] private PlayerMovementConfigSO _config;
|
||||
|
||||
[Header("地面检测")]
|
||||
[SerializeField] private Transform _groundCheck;
|
||||
[SerializeField] private Vector2 _groundCheckSize = new Vector2(0.8f, 0.05f);
|
||||
[SerializeField] private LayerMask _groundLayer;
|
||||
|
||||
[Header("朝向")]
|
||||
[SerializeField] private SpriteRenderer _spriteRenderer;
|
||||
|
||||
// ── 运行时状态 ────────────────────────────────────────────────────────
|
||||
private Rigidbody2D _rb;
|
||||
private float _coyoteTimer;
|
||||
private bool _isGrounded;
|
||||
private bool _isWallLeft;
|
||||
private bool _isWallRight;
|
||||
private bool _onOneWayPlatform;
|
||||
private int _facingDirection = 1;
|
||||
private bool _cancelWindowOpen;
|
||||
private SurfaceType _currentSurface = SurfaceType.Ground;
|
||||
private readonly Collider2D[] _groundBuffer = new Collider2D[4];
|
||||
|
||||
public bool IsGrounded => _isGrounded;
|
||||
public bool HasCoyoteTime => _coyoteTimer > 0f;
|
||||
public bool IsWallLeft => _isWallLeft;
|
||||
public bool IsWallRight => _isWallRight;
|
||||
public bool OnOneWayPlatform => _onOneWayPlatform;
|
||||
public int FacingDirection => _facingDirection;
|
||||
public Rigidbody2D Rb => _rb;
|
||||
public bool CancelWindowOpen => _cancelWindowOpen;
|
||||
public SurfaceType CurrentSurface => _currentSurface;
|
||||
/// <summary>垂直速度大于零(处于上升弧段)。供 WallJumpState 判断起跳是否结束。</summary>
|
||||
public bool IsRising => _rb.velocity.y > 0f;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
Debug.Assert(_config != null, "[PlayerMovement] _config 未赋值,请在 Inspector 中指定 PlayerMovementConfigSO。", this);
|
||||
_rb = GetComponent<Rigidbody2D>();
|
||||
if (_spriteRenderer == null)
|
||||
_spriteRenderer = GetComponentInChildren<SpriteRenderer>();
|
||||
}
|
||||
|
||||
private void FixedUpdate()
|
||||
{
|
||||
CheckGrounded();
|
||||
CheckWalls();
|
||||
|
||||
if (_isGrounded)
|
||||
_coyoteTimer = _config.CoyoteTime;
|
||||
else
|
||||
_coyoteTimer = Mathf.Max(0f, _coyoteTimer - Time.fixedDeltaTime);
|
||||
}
|
||||
|
||||
// ── 移动 ──────────────────────────────────────────────────────────────
|
||||
public void Move(float speedX)
|
||||
{
|
||||
float target = speedX;
|
||||
float current = _rb.velocity.x;
|
||||
float accel = Mathf.Abs(speedX) > 0.01f ? _config.Acceleration : _config.Deceleration;
|
||||
float newX = Mathf.MoveTowards(current, target, accel * Time.fixedDeltaTime);
|
||||
_rb.velocity = new Vector2(newX, _rb.velocity.y);
|
||||
}
|
||||
|
||||
// ── 跳跃 ──────────────────────────────────────────────────────────────
|
||||
public void Jump(bool isVariable = true)
|
||||
{
|
||||
_rb.velocity = new Vector2(_rb.velocity.x, _config.JumpForce);
|
||||
_coyoteTimer = 0f;
|
||||
}
|
||||
|
||||
public void CutJump()
|
||||
{
|
||||
if (_rb.velocity.y > 0f)
|
||||
_rb.velocity = new Vector2(_rb.velocity.x, _rb.velocity.y * _config.JumpCutMultiplier);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 二段跳(Monarch Wings 等效)。覆盖当前垂直速度为 DoubleJumpForce。
|
||||
/// FallState / JumpState 在检测到 HasAbility(DoubleJump) && AirJumpsLeft > 0 时调用。
|
||||
/// </summary>
|
||||
public void DoubleJump()
|
||||
{
|
||||
_rb.velocity = new Vector2(_rb.velocity.x, _config.DoubleJumpForce);
|
||||
_coyoteTimer = 0f;
|
||||
}
|
||||
|
||||
// ── 重力 ──────────────────────────────────────────────────────────────
|
||||
public void SetGravityScale(float scale) => _rb.gravityScale = scale;
|
||||
|
||||
// ── 击退 ──────────────────────────────────────────────────────────────
|
||||
public void ApplyKnockback(Vector2 direction, float force)
|
||||
=> _rb.velocity = direction.normalized * force;
|
||||
|
||||
// ── 速度控制 ──────────────────────────────────────────────────────────
|
||||
public void ZeroVelocity() => _rb.velocity = Vector2.zero;
|
||||
public void ZeroHorizontalVelocity() => _rb.velocity = new Vector2(0f, _rb.velocity.y);
|
||||
|
||||
// ── 朝向 ──────────────────────────────────────────────────────────────
|
||||
public void UpdateFacing()
|
||||
{
|
||||
float vx = _rb.velocity.x;
|
||||
if (Mathf.Abs(vx) < 0.1f) return;
|
||||
int dir = vx > 0f ? 1 : -1;
|
||||
if (dir == _facingDirection) return;
|
||||
_facingDirection = dir;
|
||||
if (_spriteRenderer != null)
|
||||
_spriteRenderer.flipX = dir < 0;
|
||||
}
|
||||
|
||||
// ── 取消窗口 ──────────────────────────────────────────────────────────
|
||||
public void SetCancelWindowOpen(bool open) => _cancelWindowOpen = open;
|
||||
|
||||
// ── 冲刺 ──────────────────────────────────────────────────────────────
|
||||
/// <summary>
|
||||
/// 施加冲刺速度(DashState/AerialDashState 调用)。
|
||||
/// direction 应为归一化水平向量(±1, 0)。
|
||||
/// 冲刺期间调用 SetGravityScale(0) 可关闭重力;结束后恢复默认重力。
|
||||
/// </summary>
|
||||
public void Dash(Vector2 direction, float speed)
|
||||
{
|
||||
_rb.velocity = new Vector2(direction.x * speed, 0f);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 壁滑:将垂直速度限制为 -WallSlideSpeed(向下缓慢滑动)。
|
||||
/// WallSlideState.OnStateFixedUpdate 每帧调用。
|
||||
/// </summary>
|
||||
public void ApplyWallSlide()
|
||||
{
|
||||
float targetY = -_config.WallSlideSpeed;
|
||||
if (_rb.velocity.y < targetY)
|
||||
_rb.velocity = new Vector2(_rb.velocity.x, targetY);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 蹬墙跳:对墙方向施加相反水平力 + 向上力。
|
||||
/// wallDir = +1 (右墙) 或 -1 (左墙),跳跃方向与之相反。
|
||||
/// </summary>
|
||||
public void WallJump(int wallDir)
|
||||
{
|
||||
float forceX = -wallDir * _config.WallJumpForceX;
|
||||
float forceY = _config.WallJumpForceY;
|
||||
_rb.velocity = new Vector2(forceX, forceY);
|
||||
_coyoteTimer = 0f;
|
||||
}
|
||||
|
||||
/// <summary>单向平台穿透(输入下行 + 跳跃键时触发)。</summary>
|
||||
public void DropThroughPlatform() { }
|
||||
|
||||
// ── Physics 检测 ──────────────────────────────────────────────────────
|
||||
private void CheckGrounded()
|
||||
{
|
||||
bool wasGrounded = _isGrounded;
|
||||
Vector2 origin = _groundCheck != null
|
||||
? (Vector2)_groundCheck.position
|
||||
: (Vector2)transform.position + Vector2.down * 0.5f;
|
||||
|
||||
_isGrounded = Physics2D.OverlapBoxNonAlloc(origin, _groundCheckSize, 0f, _groundBuffer, _groundLayer) > 0;
|
||||
|
||||
if (_isGrounded && !wasGrounded)
|
||||
_coyoteTimer = _config.CoyoteTime;
|
||||
}
|
||||
|
||||
private void CheckWalls()
|
||||
{
|
||||
float len = _config.WallRayLength;
|
||||
float offY = _config.WallRayOffsetY;
|
||||
Vector2 pos = (Vector2)transform.position + Vector2.up * offY;
|
||||
|
||||
_isWallLeft = Physics2D.Raycast(pos, Vector2.left, len, _groundLayer);
|
||||
_isWallRight = Physics2D.Raycast(pos, Vector2.right, len, _groundLayer);
|
||||
}
|
||||
|
||||
private void OnDrawGizmos()
|
||||
{
|
||||
#if UNITY_EDITOR
|
||||
// ── 1. 角色物理轮廓(浅绿,始终可见)────────────────────────────
|
||||
Gizmos.color = new Color(0.4f, 1f, 0.4f, 0.65f);
|
||||
foreach (var col in GetComponents<Collider2D>())
|
||||
{
|
||||
if (col.isTrigger) continue;
|
||||
BaseGames.Combat.HitBox.DrawCollider2DWire(col);
|
||||
}
|
||||
|
||||
// ── 2. 朝向箭头(金黄)──────────────────────────────────────────
|
||||
Vector3 center = transform.position;
|
||||
Vector3 arrowEnd = center + new Vector3(_facingDirection * 0.55f, 0f, 0f);
|
||||
DrawArrow2D(center, arrowEnd, new Color(1f, 0.85f, 0.1f, 0.95f));
|
||||
|
||||
// ── 3. 站立检测框(落地亮绿 / 未落地淡绿)──────────────────────
|
||||
Vector2 gOrigin = _groundCheck != null
|
||||
? (Vector2)_groundCheck.position
|
||||
: (Vector2)transform.position + Vector2.down * 0.5f;
|
||||
bool grounded = Application.isPlaying && _isGrounded;
|
||||
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(gOrigin, _groundCheckSize);
|
||||
|
||||
// ── 4. 靠墙检测射线(碰墙橙色 / 未碰淡黄,带端点圆点)─────────
|
||||
if (_config == null) return;
|
||||
float wLen = _config.WallRayLength;
|
||||
float wOffY = _config.WallRayOffsetY;
|
||||
Vector2 wallPos = (Vector2)transform.position + Vector2.up * wOffY;
|
||||
|
||||
bool lHit = Application.isPlaying && _isWallLeft;
|
||||
bool rHit = Application.isPlaying && _isWallRight;
|
||||
|
||||
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.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);
|
||||
#endif
|
||||
}
|
||||
|
||||
private void OnDrawGizmosSelected()
|
||||
{
|
||||
#if UNITY_EDITOR
|
||||
// 运行时:用青色箭头显示速度向量(仅选中时,按比例缩放)
|
||||
if (!Application.isPlaying || _rb == null) return;
|
||||
Vector2 vel = _rb.velocity;
|
||||
if (vel.sqrMagnitude < 0.01f) return;
|
||||
DrawArrow2D(transform.position, transform.position + (Vector3)(vel * 0.12f),
|
||||
new Color(0.2f, 0.9f, 1f, 0.9f), 0.1f);
|
||||
#endif
|
||||
}
|
||||
|
||||
// 在 Gizmos 空间绘制带箭头的 2D 有向线段
|
||||
private static void DrawArrow2D(Vector3 from, Vector3 to, Color color, float headLen = 0.15f)
|
||||
{
|
||||
Vector3 dir = to - from;
|
||||
if (dir.sqrMagnitude < 0.0001f) return;
|
||||
dir = dir.normalized;
|
||||
Gizmos.color = color;
|
||||
Gizmos.DrawLine(from, to);
|
||||
|
||||
// 将 -dir 分别旋转 ±35° 得到箭头两翼
|
||||
float cos = 0.8192f; // Mathf.Cos(35° in radians)
|
||||
float sin = 0.5736f; // Mathf.Sin(35°)
|
||||
float bx = -dir.x, by = -dir.y;
|
||||
Vector3 wing1 = new Vector3(bx * cos - by * sin, bx * sin + by * cos, 0f) * headLen;
|
||||
Vector3 wing2 = new Vector3(bx * cos + by * sin, -bx * sin + by * cos, 0f) * headLen;
|
||||
Gizmos.DrawLine(to, to + wing1);
|
||||
Gizmos.DrawLine(to, to + wing2);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>当前所在地面类型(用于脚步声等反馈)。</summary>
|
||||
public enum SurfaceType
|
||||
{
|
||||
Ground,
|
||||
OneWayPlatform,
|
||||
Slope,
|
||||
Ice,
|
||||
}
|
||||
}
|
||||
11
Assets/_Game/Scripts/Player/PlayerMovement.cs.meta
Normal file
11
Assets/_Game/Scripts/Player/PlayerMovement.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 263a07a0eb148924cbcf284def379a3f
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
62
Assets/_Game/Scripts/Player/PlayerMovementConfigSO.cs
Normal file
62
Assets/_Game/Scripts/Player/PlayerMovementConfigSO.cs
Normal file
@@ -0,0 +1,62 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace BaseGames.Player
|
||||
{
|
||||
[CreateAssetMenu(menuName = "BaseGames/Player/MovementConfig")]
|
||||
public class PlayerMovementConfigSO : ScriptableObject
|
||||
{
|
||||
[Header("地面移动")]
|
||||
public float RunSpeed = 7f;
|
||||
public float Acceleration = 50f;
|
||||
public float Deceleration = 80f;
|
||||
|
||||
[Header("跳跃(对齐空洞骑士手感)")]
|
||||
[Tooltip("一段跳初速度。HK 约 18-20,对应 ~4-5 格高度。")]
|
||||
public float JumpForce = 19f;
|
||||
[Tooltip("按住跳跃键可保持的郊狼时间。HK ~0.12s。")]
|
||||
public float CoyoteTime = 0.12f;
|
||||
[Tooltip("下落阶段额外重力倍率。HK ~3.5,使下落比上升更快、手感更紧实。")]
|
||||
public float FallGravityMult = 3.5f;
|
||||
[Tooltip("最大下落速度(终端速度)。HK ~22。")]
|
||||
public float MaxFallSpeed = 22f;
|
||||
[Tooltip("松开跳跃键时速度保留比例(变高跳)。HK ~0.45,越小跳跃越低。")]
|
||||
[Range(0f, 1f)]
|
||||
public float JumpCutMultiplier = 0.45f;
|
||||
|
||||
[Header("二段跳(Monarch Wings 等效)")]
|
||||
[Tooltip("二段跳初速度。设为与 JumpForce 相同可获得等高二段跳(HK 风格)。")]
|
||||
public float DoubleJumpForce = 19f;
|
||||
|
||||
[Header("冲刺(对齐空洞骑士 Mothwing Cloak 手感)")]
|
||||
[Tooltip("冲刺速度(单位/秒)。HK ~25,在 0.35s 内约穿越 7-8 格。")]
|
||||
public float DashSpeed = 25f;
|
||||
[Tooltip("冲刺持续时长(秒)。HK ~0.35s。")]
|
||||
public float DashDuration = 0.35f;
|
||||
[Tooltip("冲刺冷却时长(秒)。HK ~0.6s,落地后才可再次冲刺。")]
|
||||
public float DashCooldown = 0.6f;
|
||||
[Tooltip("每次腾空可使用的最大空中冲刺次数。HK = 1(Mothwing Cloak)。")]
|
||||
public int MaxAerialDashes = 1;
|
||||
|
||||
[Header("冲刺无敌帧(对齐空洞骑士:窗口 < 冲刺时长,且有独立 CD)")]
|
||||
[Tooltip("冲刺无敌窗口时长(秒)。仅为冲刺前段;窗口结束后即使仍在冲刺中也可受伤被打断(HK ~0.20s)。")]
|
||||
public float DashInvincibilityDuration = 0.20f;
|
||||
[Tooltip("无敌的独立冷却(秒)。CD 内再次冲刺不会获得无敌帧,防止连冲变相持续无敌(HK ~0.9s)。")]
|
||||
public float DashInvincibilityCooldown = 0.9f;
|
||||
|
||||
[Header("蹬墙 / 壁滑")]
|
||||
public float WallSlideSpeed = 2f;
|
||||
public float WallJumpForceX = 12f;
|
||||
public float WallJumpForceY = 16f;
|
||||
public float WallRayLength = 0.55f;
|
||||
public float WallRayOffsetY = 0.2f;
|
||||
public float WallGrabMaxHeightGain = 0.5f;
|
||||
public float WallGrabReleaseDelay = 0.08f;
|
||||
public float WallJumpBackForceX = 14f;
|
||||
public float WallJumpAwayForceX = 10f;
|
||||
public float WallJumpAwayForceY = 18f;
|
||||
public float WallJumpInputLockDuration = 0.15f;
|
||||
|
||||
[Header("重力")]
|
||||
public float DefaultGravityScale = 3f;
|
||||
}
|
||||
}
|
||||
11
Assets/_Game/Scripts/Player/PlayerMovementConfigSO.cs.meta
Normal file
11
Assets/_Game/Scripts/Player/PlayerMovementConfigSO.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 81da55e0fcf99d34693cbc5a348225c3
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
314
Assets/_Game/Scripts/Player/PlayerStats.cs
Normal file
314
Assets/_Game/Scripts/Player/PlayerStats.cs
Normal file
@@ -0,0 +1,314 @@
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
using BaseGames.Core;
|
||||
using BaseGames.Core.Events;
|
||||
using BaseGames.Core.Save;
|
||||
|
||||
namespace BaseGames.Player
|
||||
{
|
||||
/// <summary>
|
||||
/// 玩家数值管理组件。负责 HP、灵魂、灵气、弹簧充能、LingZhu、能力解锁与存档读写。
|
||||
/// 实现 <see cref="IRewardTarget"/> 供 RewardSO 使用,避免 Quest 程序集直接依赖 Player 程序集。
|
||||
/// </summary>
|
||||
public class PlayerStats : MonoBehaviour, ISaveable, BaseGames.Core.IRestoreOnSave, IRewardTarget
|
||||
{
|
||||
[Header("配置")]
|
||||
[SerializeField] private PlayerStatsSO _config;
|
||||
|
||||
[Header("事件频道")]
|
||||
[SerializeField] private IntEventChannelSO _onHPChanged;
|
||||
[SerializeField] private IntEventChannelSO _onMaxHPChanged;
|
||||
[SerializeField] private IntEventChannelSO _onSoulPowerChanged;
|
||||
[SerializeField] private IntEventChannelSO _onSpiritPowerChanged;
|
||||
[SerializeField] private IntEventChannelSO _onSpringChargesChanged;
|
||||
[SerializeField] private IntEventChannelSO _onLingZhuChanged;
|
||||
[SerializeField] private AbilityTypeEventChannelSO _onAbilityUnlocked;
|
||||
[SerializeField] private DifficultyChangedEventChannel _onDifficultyChanged;
|
||||
|
||||
// ── 运行时数值 ─────────────────────────────────────────────────────────
|
||||
public int CurrentHP { get; private set; }
|
||||
public int MaxHP { get; private set; }
|
||||
public int CurrentSoulPower { get; private set; }
|
||||
public int MaxSoulPower { get; private set; }
|
||||
public int CurrentSpiritPower { get; private set; }
|
||||
public int MaxSpiritPower { get; private set; }
|
||||
public int CurrentSpringCharges { get; private set; }
|
||||
public int MaxSpringCharges { get; private set; }
|
||||
public int SpringKillPoints { get; private set; }
|
||||
public int CurrentLingZhu { get; private set; }
|
||||
|
||||
public bool IsInvincible => _invincibleTimer > 0f;
|
||||
public bool IsAlive => CurrentHP > 0;
|
||||
|
||||
private float _invincibleTimer;
|
||||
private float _spiritRegenTimer;
|
||||
private AbilityType _unlockedAbilities = AbilityType.None;
|
||||
private bool _isGodMode;
|
||||
private readonly CompositeDisposable _subs = new();
|
||||
|
||||
// ── 护符属性修改器 ─────────────────────────────────────────────────────────
|
||||
private readonly Dictionary<StatType, float> _flatModifiers = new();
|
||||
private readonly Dictionary<StatType, float> _percentModifiers = new();
|
||||
private float _animatorSpeedBonus = 0f;
|
||||
private int _soulCostReduction = 0;
|
||||
|
||||
/// <summary>动画速度倍率(AttackSpeedEffect 调节,初始 1.0)。</summary>
|
||||
public float AnimatorSpeedMultiplier => 1f + _animatorSpeedBonus;
|
||||
|
||||
/// <summary>法术灵力消耗减免(SoulSpellEffect 叠加)。</summary>
|
||||
public int SoulCostReduction => _soulCostReduction;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
Debug.Assert(_config != null, "[PlayerStats] _config 未赋值,请在 Inspector 中指定 PlayerStatsSO。", this);
|
||||
MaxHP = _config.MaxHP;
|
||||
CurrentHP = MaxHP;
|
||||
MaxSoulPower = _config.MaxSoulPower;
|
||||
MaxSpiritPower = _config.MaxSpiritPower;
|
||||
MaxSpringCharges = _config.MaxSpringCharges;
|
||||
CurrentSpringCharges = MaxSpringCharges;
|
||||
CurrentLingZhu = _config.InitialLingZhu;
|
||||
}
|
||||
|
||||
private void OnEnable() => _onDifficultyChanged?.Subscribe(HandleDifficultyChanged).AddTo(_subs);
|
||||
private void OnDisable() => _subs.Clear();
|
||||
|
||||
private void HandleDifficultyChanged(DifficultyLevel _)
|
||||
{
|
||||
var scaler = ServiceLocator.GetOrDefault<IDifficultyService>()?.CurrentScaler;
|
||||
if (scaler == null) return;
|
||||
// 按比例缩放当前 HP(架构 19 §5:难度切换时保持 HP 比例)
|
||||
float hpRatio = MaxHP > 0 ? (float)CurrentHP / MaxHP : 1f;
|
||||
MaxHP = Mathf.Max(1, Mathf.RoundToInt(_config.MaxHP * scaler.PlayerMaxHPMultiplier));
|
||||
CurrentHP = Mathf.Clamp(Mathf.RoundToInt(MaxHP * hpRatio), IsAlive ? 1 : 0, MaxHP);
|
||||
_onMaxHPChanged?.Raise(MaxHP);
|
||||
_onHPChanged?.Raise(CurrentHP);
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
float dt = Time.deltaTime;
|
||||
|
||||
if (_invincibleTimer > 0f)
|
||||
_invincibleTimer -= dt;
|
||||
|
||||
if (_config.SpiritRegenRate > 0)
|
||||
{
|
||||
_spiritRegenTimer += dt;
|
||||
if (_spiritRegenTimer >= 1f)
|
||||
{
|
||||
_spiritRegenTimer -= 1f;
|
||||
AddSpiritPower(_config.SpiritRegenRate);
|
||||
}
|
||||
}
|
||||
}
|
||||
// ── 护符修改器 API ─────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>叠加属性修改器(护符效果装备时调用)。</summary>
|
||||
public void AddModifier(StatType stat, float flat, float percent)
|
||||
{
|
||||
_flatModifiers[stat] = _flatModifiers.GetValueOrDefault(stat) + flat;
|
||||
_percentModifiers[stat] = _percentModifiers.GetValueOrDefault(stat) + percent;
|
||||
if (stat == StatType.MaxHP) RecalcMaxHP();
|
||||
}
|
||||
|
||||
/// <summary>移除属性修改器(护符效果卸下时调用)。</summary>
|
||||
public void RemoveModifier(StatType stat, float flat, float percent)
|
||||
{
|
||||
_flatModifiers[stat] = _flatModifiers.GetValueOrDefault(stat) - flat;
|
||||
_percentModifiers[stat] = _percentModifiers.GetValueOrDefault(stat) - percent;
|
||||
if (stat == StatType.MaxHP) RecalcMaxHP();
|
||||
}
|
||||
|
||||
/// <summary>查询指定属性的固定加成(由 PlayerMovement、战斗等外部系统查询)。</summary>
|
||||
public float GetFlatModifier(StatType stat) => _flatModifiers.GetValueOrDefault(stat);
|
||||
|
||||
/// <summary>查询指定属性的百分比加成。</summary>
|
||||
public float GetPercentModifier(StatType stat) => _percentModifiers.GetValueOrDefault(stat);
|
||||
|
||||
/// <summary>加载动画速度加成(AttackSpeedEffect 装备时调用)。</summary>
|
||||
public void AddAnimatorSpeedBonus(float delta) => _animatorSpeedBonus += delta;
|
||||
|
||||
/// <summary>移除动画速度加成(AttackSpeedEffect 卸下时调用)。</summary>
|
||||
public void RemoveAnimatorSpeedBonus(float delta) => _animatorSpeedBonus -= delta;
|
||||
|
||||
/// <summary>增加灵力消耗减免(SoulSpellEffect 装备时调用)。</summary>
|
||||
public void AddSoulCostReduction(int amount) => _soulCostReduction += amount;
|
||||
|
||||
/// <summary>移除灵力消耗减免(SoulSpellEffect 卸下时调用)。</summary>
|
||||
public void RemoveSoulCostReduction(int amount) => _soulCostReduction = Mathf.Max(0, _soulCostReduction - amount);
|
||||
|
||||
private void RecalcMaxHP()
|
||||
{
|
||||
float flat = _flatModifiers.GetValueOrDefault(StatType.MaxHP);
|
||||
float percent = _percentModifiers.GetValueOrDefault(StatType.MaxHP);
|
||||
int newMax = Mathf.Max(1, Mathf.RoundToInt(_config.MaxHP * (1f + percent) + flat));
|
||||
SetMaxHP(newMax);
|
||||
}
|
||||
// ── HP ────────────────────────────────────────────────────────────────
|
||||
/// <summary>受到伤害时触发(已扣血,不包含无敌/死亡情况)。</summary>
|
||||
public event System.Action OnDamaged;
|
||||
|
||||
/// <summary>Debug:开启/关闭无敌模式(不计入无敌帧,永久生效直至关闭)。</summary>
|
||||
public void SetGodMode(bool v) { _isGodMode = v; }
|
||||
|
||||
public void TakeDamage(int amount)
|
||||
{
|
||||
if (_isGodMode || IsInvincible || !IsAlive || amount <= 0) return;
|
||||
CurrentHP = Mathf.Max(0, CurrentHP - amount);
|
||||
_onHPChanged?.Raise(CurrentHP);
|
||||
OnDamaged?.Invoke();
|
||||
}
|
||||
|
||||
public void FullHeal()
|
||||
{
|
||||
if (!IsAlive) return;
|
||||
CurrentHP = MaxHP;
|
||||
_onHPChanged?.Raise(CurrentHP);
|
||||
}
|
||||
|
||||
// ── IRestoreOnSave ────────────────────────────────────────────────────
|
||||
void BaseGames.Core.IRestoreOnSave.FullRestore() => FullHeal();
|
||||
void BaseGames.Core.IRestoreOnSave.RestoreSpring() => RestoreSpringCharges();
|
||||
|
||||
public void HealHP(int amount)
|
||||
{
|
||||
if (!IsAlive || amount <= 0) return;
|
||||
CurrentHP = Mathf.Min(MaxHP, CurrentHP + amount);
|
||||
_onHPChanged?.Raise(CurrentHP);
|
||||
}
|
||||
|
||||
public void SetMaxHP(int newMax)
|
||||
{
|
||||
MaxHP = Mathf.Max(1, newMax);
|
||||
CurrentHP = Mathf.Min(CurrentHP, MaxHP);
|
||||
_onMaxHPChanged?.Raise(MaxHP);
|
||||
_onHPChanged?.Raise(CurrentHP);
|
||||
}
|
||||
|
||||
// ── Soul Power ────────────────────────────────────────────────────────
|
||||
public void AddSoulPower(int amount)
|
||||
{
|
||||
if (amount <= 0) return;
|
||||
CurrentSoulPower = Mathf.Min(MaxSoulPower, CurrentSoulPower + amount);
|
||||
_onSoulPowerChanged?.Raise(CurrentSoulPower);
|
||||
}
|
||||
|
||||
/// <summary>AddSoul 是 AddSoulPower 的别名(架构 06_CombatModule §8 ParrySystem 使用此名称)。</summary>
|
||||
public void AddSoul(int amount) => AddSoulPower(amount);
|
||||
|
||||
public bool ConsumeSoulPower(int amount)
|
||||
{
|
||||
if (CurrentSoulPower < amount) return false;
|
||||
CurrentSoulPower -= amount;
|
||||
_onSoulPowerChanged?.Raise(CurrentSoulPower);
|
||||
return true;
|
||||
}
|
||||
|
||||
// ── Spirit Power ──────────────────────────────────────────────────────
|
||||
public void AddSpiritPower(int amount)
|
||||
{
|
||||
if (amount <= 0) return;
|
||||
CurrentSpiritPower = Mathf.Min(MaxSpiritPower, CurrentSpiritPower + amount);
|
||||
_onSpiritPowerChanged?.Raise(CurrentSpiritPower);
|
||||
}
|
||||
|
||||
public bool ConsumeSpiritPower(int amount)
|
||||
{
|
||||
if (CurrentSpiritPower < amount) return false;
|
||||
CurrentSpiritPower -= amount;
|
||||
_onSpiritPowerChanged?.Raise(CurrentSpiritPower);
|
||||
return true;
|
||||
}
|
||||
|
||||
// ── Spring ────────────────────────────────────────────────────────────
|
||||
public bool UseSpring()
|
||||
{
|
||||
if (CurrentSpringCharges <= 0) return false;
|
||||
CurrentSpringCharges--;
|
||||
_onSpringChargesChanged?.Raise(CurrentSpringCharges);
|
||||
HealHP(_config.SpringHealAmount);
|
||||
return true;
|
||||
}
|
||||
|
||||
public void RestoreSpringCharges(int amount = -1)
|
||||
{
|
||||
if (amount < 0) amount = MaxSpringCharges;
|
||||
CurrentSpringCharges = Mathf.Min(MaxSpringCharges, CurrentSpringCharges + amount);
|
||||
_onSpringChargesChanged?.Raise(CurrentSpringCharges);
|
||||
}
|
||||
|
||||
public void AddKillPoints(int points = 1)
|
||||
{
|
||||
SpringKillPoints += points;
|
||||
if (SpringKillPoints >= _config.SpringKillThreshold)
|
||||
{
|
||||
SpringKillPoints = 0;
|
||||
RestoreSpringCharges(1);
|
||||
}
|
||||
}
|
||||
|
||||
// ── LingZhu ─────────────────────────────────────────────────────────────────────
|
||||
public void AddLingZhu(int amount)
|
||||
{
|
||||
if (amount <= 0) return;
|
||||
CurrentLingZhu += amount;
|
||||
_onLingZhuChanged?.Raise(CurrentLingZhu);
|
||||
}
|
||||
|
||||
public bool SpendLingZhu(int amount)
|
||||
{
|
||||
if (CurrentLingZhu < amount) return false;
|
||||
CurrentLingZhu -= amount;
|
||||
_onLingZhuChanged?.Raise(CurrentLingZhu);
|
||||
return true;
|
||||
}
|
||||
|
||||
// ── Invincibility ─────────────────────────────────────────────────────
|
||||
public void BeginInvincibility(float duration = -1f)
|
||||
{
|
||||
float d = duration >= 0f ? duration : _config.InvincibilityDuration;
|
||||
_invincibleTimer = Mathf.Max(_invincibleTimer, d);
|
||||
}
|
||||
|
||||
// ── Abilities ─────────────────────────────────────────────────────────
|
||||
public bool HasAbility(AbilityType ability)
|
||||
=> (_unlockedAbilities & ability) == ability;
|
||||
|
||||
public void UnlockAbility(AbilityType ability)
|
||||
{
|
||||
if (HasAbility(ability)) return;
|
||||
_unlockedAbilities |= ability;
|
||||
_onAbilityUnlocked?.Raise(ability);
|
||||
}
|
||||
|
||||
/// <summary>IRewardTarget 实现:以 uint 位掩码解锁能力(避免跨程序集枚举引用)。</summary>
|
||||
void IRewardTarget.UnlockAbilityFlag(uint abilityFlag) => UnlockAbility((AbilityType)abilityFlag);
|
||||
|
||||
public void LockAbility(AbilityType ability)
|
||||
=> _unlockedAbilities &= ~ability;
|
||||
|
||||
// ── ISaveable ─────────────────────────────────────────────────────────
|
||||
public void OnSave(SaveData saveData)
|
||||
{
|
||||
var p = saveData.Player;
|
||||
p.CurrentHP = CurrentHP;
|
||||
p.MaxHP = MaxHP;
|
||||
p.CurrentLingZhu = CurrentLingZhu;
|
||||
p.AbilityFlags = (uint)_unlockedAbilities;
|
||||
}
|
||||
|
||||
public void OnLoad(SaveData saveData)
|
||||
{
|
||||
var p = saveData.Player;
|
||||
MaxHP = p.MaxHP;
|
||||
CurrentHP = Mathf.Clamp(p.CurrentHP, 0, MaxHP);
|
||||
CurrentLingZhu = p.CurrentLingZhu;
|
||||
_unlockedAbilities = (AbilityType)p.AbilityFlags;
|
||||
|
||||
_onHPChanged?.Raise(CurrentHP);
|
||||
_onMaxHPChanged?.Raise(MaxHP);
|
||||
_onLingZhuChanged?.Raise(CurrentLingZhu);
|
||||
}
|
||||
}
|
||||
}
|
||||
11
Assets/_Game/Scripts/Player/PlayerStats.cs.meta
Normal file
11
Assets/_Game/Scripts/Player/PlayerStats.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: edd28a350d3cebe46a72e7550ffb1b93
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
29
Assets/_Game/Scripts/Player/PlayerStatsSO.cs
Normal file
29
Assets/_Game/Scripts/Player/PlayerStatsSO.cs
Normal file
@@ -0,0 +1,29 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace BaseGames.Player
|
||||
{
|
||||
[CreateAssetMenu(menuName = "BaseGames/Player/Stats")]
|
||||
public class PlayerStatsSO : ScriptableObject
|
||||
{
|
||||
[Header("HP")]
|
||||
public int MaxHP = 5;
|
||||
|
||||
[Header("Soul")]
|
||||
public int MaxSoulPower = 100;
|
||||
|
||||
[Header("Spirit")]
|
||||
public int MaxSpiritPower = 100;
|
||||
public int SpiritRegenRate = 5; // 每秒回复量
|
||||
|
||||
[Header("Spring (治愈弹簧)")]
|
||||
public int MaxSpringCharges = 3;
|
||||
public int SpringHealAmount = 2;
|
||||
public int SpringKillThreshold = 4; // 击杀数触发弹簧恢复
|
||||
|
||||
[Header("无敌帧")]
|
||||
public float InvincibilityDuration = 0.6f;
|
||||
|
||||
[Header("初始货币")]
|
||||
public int InitialLingZhu = 0;
|
||||
}
|
||||
}
|
||||
11
Assets/_Game/Scripts/Player/PlayerStatsSO.cs.meta
Normal file
11
Assets/_Game/Scripts/Player/PlayerStatsSO.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 31a9f22bef1315643bf5a49f2a6edd2b
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
70
Assets/_Game/Scripts/Player/PlayerWallDetector.cs
Normal file
70
Assets/_Game/Scripts/Player/PlayerWallDetector.cs
Normal file
@@ -0,0 +1,70 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace BaseGames.Player
|
||||
{
|
||||
/// <summary>
|
||||
/// 独立墙壁检测组件(架构 05_PlayerModule §13)。
|
||||
/// ⚠️ 不嵌入 PlayerMovement,以保持单一职责。
|
||||
/// 每侧发两根射线(Top + Bottom),两根均命中才视为接触墙壁(防卡角误判)。
|
||||
/// WallSlideState / WallJumpState 通过 PlayerController.WallDetector 访问。
|
||||
/// </summary>
|
||||
[RequireComponent(typeof(PlayerMovement))]
|
||||
public class PlayerWallDetector : MonoBehaviour
|
||||
{
|
||||
[SerializeField] private PlayerMovementConfigSO _config;
|
||||
|
||||
[Header("墙壁 Layer(默认使用 \"Wall\" + \"Ground\")")]
|
||||
[SerializeField] private LayerMask _wallLayer;
|
||||
|
||||
/// <summary>当前是否正在触碰墙壁。</summary>
|
||||
public bool IsTouchingWall { get; private set; }
|
||||
|
||||
/// <summary>触碰到的墙壁方向:+1 = 右墙,-1 = 左墙,0 = 无墙。</summary>
|
||||
public int WallDirection { get; private set; }
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
Debug.Assert(_config != null, "[PlayerWallDetector] _config 未赋值,请在 Inspector 中指定 PlayerMovementConfigSO。", this);
|
||||
}
|
||||
|
||||
private void FixedUpdate()
|
||||
{
|
||||
bool rightWall = CheckSide(Vector2.right);
|
||||
bool leftWall = CheckSide(Vector2.left);
|
||||
|
||||
IsTouchingWall = rightWall || leftWall;
|
||||
WallDirection = rightWall ? 1 : (leftWall ? -1 : 0);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 每侧发两根射线(TopRay + BottomRay),两根均命中才返回 true。
|
||||
/// </summary>
|
||||
private bool CheckSide(Vector2 dir)
|
||||
{
|
||||
Vector2 center = transform.position;
|
||||
float len = _config.WallRayLength;
|
||||
float oy = _config.WallRayOffsetY;
|
||||
int layer = _wallLayer != 0 ? (int)_wallLayer : LayerMask.GetMask("Wall", "Ground");
|
||||
|
||||
bool top = Physics2D.Raycast(center + Vector2.up * oy, dir, len, layer);
|
||||
bool bot = Physics2D.Raycast(center + Vector2.down * oy, dir, len, layer);
|
||||
return top && bot;
|
||||
}
|
||||
|
||||
private void OnDrawGizmosSelected()
|
||||
{
|
||||
if (_config == null) return;
|
||||
float len = _config.WallRayLength;
|
||||
float oy = _config.WallRayOffsetY;
|
||||
Vector2 center = transform.position;
|
||||
|
||||
Gizmos.color = IsTouchingWall ? Color.red : Color.cyan;
|
||||
// 右侧两根射线
|
||||
Gizmos.DrawRay(center + Vector2.up * oy, Vector2.right * len);
|
||||
Gizmos.DrawRay(center + Vector2.down * oy, Vector2.right * len);
|
||||
// 左侧两根射线
|
||||
Gizmos.DrawRay(center + Vector2.up * oy, Vector2.left * len);
|
||||
Gizmos.DrawRay(center + Vector2.down * oy, Vector2.left * len);
|
||||
}
|
||||
}
|
||||
}
|
||||
11
Assets/_Game/Scripts/Player/PlayerWallDetector.cs.meta
Normal file
11
Assets/_Game/Scripts/Player/PlayerWallDetector.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 03bafd68d1793e44f9650296d658a4aa
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
2
Assets/_Game/Scripts/Player/SkillManager.cs
Normal file
2
Assets/_Game/Scripts/Player/SkillManager.cs
Normal file
@@ -0,0 +1,2 @@
|
||||
// SkillManager 定义位于 Assets/Scripts/Skills/SkillManager.cs (BaseGames.Skills)。
|
||||
namespace BaseGames.Player { }
|
||||
11
Assets/_Game/Scripts/Player/SkillManager.cs.meta
Normal file
11
Assets/_Game/Scripts/Player/SkillManager.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 204560c2bc6841f44b27b50f3ff51fbc
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
14
Assets/_Game/Scripts/Player/SpringSystem.cs
Normal file
14
Assets/_Game/Scripts/Player/SpringSystem.cs
Normal file
@@ -0,0 +1,14 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace BaseGames.Player
|
||||
{
|
||||
/// <summary>
|
||||
/// 治愈弹簧系统(架构 05_PlayerModule §7)。
|
||||
/// PlayerStats 中已预留 CurrentSpringCharges / MaxSpringCharges / SpringKillPoints 字段。
|
||||
/// TODO: 实现弹簧充能逻辑:
|
||||
/// - 击杀敌人时调用 PlayerStats.AddSpringKillPoint() 积累充能
|
||||
/// - 按下治愈键时消耗充能槽并恢复玩家 HP
|
||||
/// - 充能满格时触发特殊强化状态(视设计而定)
|
||||
/// </summary>
|
||||
public class SpringSystem : MonoBehaviour { }
|
||||
}
|
||||
11
Assets/_Game/Scripts/Player/SpringSystem.cs.meta
Normal file
11
Assets/_Game/Scripts/Player/SpringSystem.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 1f57bdff3327d2d478779d844b114c83
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
16
Assets/_Game/Scripts/Player/StatType.cs
Normal file
16
Assets/_Game/Scripts/Player/StatType.cs
Normal file
@@ -0,0 +1,16 @@
|
||||
namespace BaseGames.Player
|
||||
{
|
||||
/// <summary>
|
||||
/// 玩家属性类型(护符效果 StatModifierEffect 使用)。
|
||||
/// 存放于 BaseGames.Player 程序集,供 PlayerStats 与 Equipment 共同引用。
|
||||
/// </summary>
|
||||
public enum StatType
|
||||
{
|
||||
MaxHP,
|
||||
AttackDamage,
|
||||
MoveSpeed,
|
||||
JumpHeight,
|
||||
SoulGain,
|
||||
Defense
|
||||
}
|
||||
}
|
||||
11
Assets/_Game/Scripts/Player/StatType.cs.meta
Normal file
11
Assets/_Game/Scripts/Player/StatType.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 2b459997f45624944acc00acc6e62ed1
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
8
Assets/_Game/Scripts/Player/States.meta
Normal file
8
Assets/_Game/Scripts/Player/States.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ef1d46e50aa9602449145ec8cfb71edf
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
84
Assets/_Game/Scripts/Player/States/AerialDashState.cs
Normal file
84
Assets/_Game/Scripts/Player/States/AerialDashState.cs
Normal file
@@ -0,0 +1,84 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace BaseGames.Player.States
|
||||
{
|
||||
/// <summary>
|
||||
/// 空中冲刺状态(架构 05_PlayerModule §12)。
|
||||
/// 与地面 DashState 独立,消耗 MaxAerialDashes 次数;
|
||||
/// 空中冲刺可向任意方向(使用移动输入方向,无输入则使用朝向)。
|
||||
/// </summary>
|
||||
public class AerialDashState : PlayerStateBase
|
||||
{
|
||||
private float _timer;
|
||||
private int _aerialDashesLeft;
|
||||
private int _facingDir;
|
||||
|
||||
public bool HasAerialDash => _aerialDashesLeft > 0;
|
||||
|
||||
// ── IsInvincible 不再在状态层硬编码,与 DashState 保持一致:
|
||||
// 实际无敌用 Stats.BeginInvincibility(DashInvincibilityDuration) 面题。
|
||||
// PlayerController.TakeDamage 已将 Stats.IsInvincible 纳入硬直判断。
|
||||
|
||||
public AerialDashState(PlayerController owner) : base(owner)
|
||||
{
|
||||
_aerialDashesLeft = 1;
|
||||
}
|
||||
|
||||
public override void OnStateEnter()
|
||||
{
|
||||
_aerialDashesLeft = Mathf.Max(0, _aerialDashesLeft - 1);
|
||||
_facingDir = Owner.FacingDirection;
|
||||
_timer = Cfg.DashDuration;
|
||||
|
||||
// 无敌帧:与地面冲刺共享同一无敌 CD(DashState._invincibilityCooldownTimer)
|
||||
// 条件 1:已解锁 InvincibleDash
|
||||
// 条件 2:共享无敌冷却已就绪
|
||||
var dashState = Owner.GetState<DashState>();
|
||||
if (Stats != null && Stats.HasAbility(AbilityType.InvincibleDash)
|
||||
&& dashState != null && dashState.CanGrantInvincibility)
|
||||
{
|
||||
Stats.BeginInvincibility(Cfg.DashInvincibilityDuration);
|
||||
dashState.ResetInvincibilityCooldown(Cfg.DashInvincibilityCooldown);
|
||||
}
|
||||
|
||||
// 关闭重力,施加冲刺速度(空中冲刺不改变垂直速度)
|
||||
Move?.SetGravityScale(0f);
|
||||
float dir = Input.MoveInput.x != 0 ? Mathf.Sign(Input.MoveInput.x) : _facingDir;
|
||||
Move?.Dash(new Vector2(dir, 0f), Cfg.DashSpeed);
|
||||
|
||||
// 播放冲刺动画(复用地面冲刺动画)
|
||||
if (AnimCfg?.Dash != null) Anim?.Play(AnimCfg.Dash);
|
||||
}
|
||||
|
||||
public override void OnStateUpdate()
|
||||
{
|
||||
_timer -= Time.deltaTime;
|
||||
if (_timer <= 0f)
|
||||
{
|
||||
Move?.SetGravityScale(Cfg.DefaultGravityScale);
|
||||
Owner.TransitionTo(Owner.GetState<FallState>());
|
||||
}
|
||||
}
|
||||
|
||||
public override void OnStateExit()
|
||||
{
|
||||
Move?.SetGravityScale(Cfg.DefaultGravityScale);
|
||||
}
|
||||
|
||||
public override void OnStateFixedUpdate()
|
||||
{
|
||||
// 冲刺期间锁定速度
|
||||
if (_timer > 0f)
|
||||
{
|
||||
float dir = Input.MoveInput.x != 0 ? Mathf.Sign(Input.MoveInput.x) : _facingDir;
|
||||
Move?.Dash(new Vector2(dir, 0f), Cfg.DashSpeed);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>着地时重置空中冲刺次数(由 PlayerController 在着地时调用)。</summary>
|
||||
public void ResetAerialDashes()
|
||||
{
|
||||
_aerialDashesLeft = Cfg.MaxAerialDashes;
|
||||
}
|
||||
}
|
||||
}
|
||||
11
Assets/_Game/Scripts/Player/States/AerialDashState.cs.meta
Normal file
11
Assets/_Game/Scripts/Player/States/AerialDashState.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 38830e1649a9eb548b20540a737bdf09
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
46
Assets/_Game/Scripts/Player/States/AirAttackState.cs
Normal file
46
Assets/_Game/Scripts/Player/States/AirAttackState.cs
Normal file
@@ -0,0 +1,46 @@
|
||||
using BaseGames.Combat;
|
||||
|
||||
namespace BaseGames.Player.States
|
||||
{
|
||||
/// <summary>
|
||||
/// 空中攻击状态(架构 05_PlayerModule §2)。
|
||||
/// 由 FallState / JumpState 接收攻击输入后转入;
|
||||
/// Animancer 帧事件驱动 HitBoxAir 激活;结束后回到 FallState。
|
||||
/// </summary>
|
||||
public class AirAttackState : PlayerStateBase
|
||||
{
|
||||
public AirAttackState(PlayerController owner) : base(owner) { }
|
||||
|
||||
public override void OnStateEnter()
|
||||
{
|
||||
Owner.Combat?.SetComboSegmentSource(0);
|
||||
Owner.Combat?.EnableWeaponHitBox(AttackDirection.Air);
|
||||
|
||||
var clip = Owner.Weapon?.ActiveWeapon?.airAttackClip;
|
||||
if (clip != null && clip.Clip != null)
|
||||
{
|
||||
var state = Anim.Play(clip);
|
||||
state.Events(this).OnEnd = OnClipEnd;
|
||||
}
|
||||
else if (AnimCfg?.AirAttack != null)
|
||||
{
|
||||
var state = Anim.Play(AnimCfg.AirAttack);
|
||||
state.Events(this).OnEnd = OnClipEnd;
|
||||
}
|
||||
else
|
||||
{
|
||||
OnClipEnd();
|
||||
}
|
||||
}
|
||||
|
||||
public override void OnStateExit()
|
||||
{
|
||||
Owner.Combat?.DisableAllWeaponHitBoxes();
|
||||
}
|
||||
|
||||
private void OnClipEnd()
|
||||
{
|
||||
Owner.TransitionTo(Owner.GetState<FallState>());
|
||||
}
|
||||
}
|
||||
}
|
||||
11
Assets/_Game/Scripts/Player/States/AirAttackState.cs.meta
Normal file
11
Assets/_Game/Scripts/Player/States/AirAttackState.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 0414d90465115124bbbfb05141a405f4
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
69
Assets/_Game/Scripts/Player/States/AttackState.cs
Normal file
69
Assets/_Game/Scripts/Player/States/AttackState.cs
Normal file
@@ -0,0 +1,69 @@
|
||||
using BaseGames.Combat;
|
||||
|
||||
namespace BaseGames.Player.States
|
||||
{
|
||||
/// <summary>
|
||||
/// 地面攻击状态(3 段连击)。
|
||||
/// 由 PlayerController 实例化,AttackEvent 触发切换。
|
||||
/// 通过 Animancer 帧事件驱动 HitBox 激活/关闭。
|
||||
/// </summary>
|
||||
public class AttackState : PlayerStateBase
|
||||
{
|
||||
private int _comboIndex;
|
||||
|
||||
public AttackState(PlayerController owner) : base(owner) { }
|
||||
|
||||
public override void OnStateEnter()
|
||||
{
|
||||
_comboIndex = 0;
|
||||
PlayAttackClip();
|
||||
Input.AttackEvent += OnAttackInput;
|
||||
}
|
||||
|
||||
public override void OnStateExit()
|
||||
{
|
||||
Input.AttackEvent -= OnAttackInput;
|
||||
Owner.Combat?.DisableAllWeaponHitBoxes();
|
||||
}
|
||||
|
||||
public override void OnStateUpdate() { }
|
||||
public override void OnStateFixedUpdate() { }
|
||||
|
||||
// ── 内部 ──────────────────────────────────────────────────────────────
|
||||
|
||||
private void PlayAttackClip()
|
||||
{
|
||||
// ⚠️ 字段名 GroundAttacks(非 AttackChainClips)
|
||||
var clip = AnimCfg.GroundAttacks[_comboIndex];
|
||||
var state = Anim.Play(clip);
|
||||
var events = state.Events(this);
|
||||
events.OnEnd = OnClipEnd;
|
||||
|
||||
// HitBox 由 Animancer 归一化时间事件驱动(时间点配置于 PlayerAnimationConfigSO)
|
||||
var timings = AnimCfg?.GroundAttackTimings;
|
||||
float enterTime = timings != null && _comboIndex < timings.Length
|
||||
? timings[_comboIndex].HitBoxEnter : 0.3f;
|
||||
float exitTime = timings != null && _comboIndex < timings.Length
|
||||
? timings[_comboIndex].HitBoxExit : 0.6f;
|
||||
events.Add(enterTime, () => Owner.Combat?.EnableWeaponHitBox(AttackDirection.Ground));
|
||||
events.Add(exitTime, () => Owner.Combat?.DisableAllWeaponHitBoxes());
|
||||
}
|
||||
|
||||
private void OnClipEnd()
|
||||
{
|
||||
Owner.Combat?.DisableAllWeaponHitBoxes();
|
||||
Owner.TransitionTo(Owner.GetState<IdleState>());
|
||||
}
|
||||
|
||||
private void OnAttackInput()
|
||||
{
|
||||
// 连击段数从配置 SO 动态读取,无须修改代码即可支持任意段数连击
|
||||
int maxCombo = AnimCfg?.GroundAttacks?.Length ?? 1;
|
||||
if (_comboIndex < maxCombo - 1)
|
||||
{
|
||||
_comboIndex++;
|
||||
PlayAttackClip();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
11
Assets/_Game/Scripts/Player/States/AttackState.cs.meta
Normal file
11
Assets/_Game/Scripts/Player/States/AttackState.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f7d976cdcc6a9c44ba569bff0147f6c7
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"excludePlatforms": [],
|
||||
"allowUnsafeCode": false,
|
||||
"precompiledReferences": [],
|
||||
"name": "BaseGames.Player.States",
|
||||
"defineConstraints": [],
|
||||
"noEngineReferences": false,
|
||||
"versionDefines": [],
|
||||
"rootNamespace": "BaseGames.Player.States",
|
||||
"references": [
|
||||
"BaseGames.Player",
|
||||
"BaseGames.Core.Events",
|
||||
"BaseGames.Input",
|
||||
"BaseGames.Combat",
|
||||
"BaseGames.Parry",
|
||||
"BaseGames.Feedback",
|
||||
"Kybernetik.Animancer",
|
||||
"BaseGames.World",
|
||||
"BaseGames.Skills"
|
||||
],
|
||||
"autoReferenced": true,
|
||||
"overrideReferences": false,
|
||||
"includePlatforms": []
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c336a32eed62ced4280d1d4c9782ec91
|
||||
AssemblyDefinitionImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
101
Assets/_Game/Scripts/Player/States/DashState.cs
Normal file
101
Assets/_Game/Scripts/Player/States/DashState.cs
Normal file
@@ -0,0 +1,101 @@
|
||||
using UnityEngine;
|
||||
using System.Collections;
|
||||
|
||||
namespace BaseGames.Player.States
|
||||
{
|
||||
/// <summary>
|
||||
/// 地面冲刺状态(架构 05_PlayerModule §2 + §12)。
|
||||
/// 施加水平位移 + 无敌帧;冲刺期间重力归零,结束后恢复;
|
||||
/// 需解锁 AbilityType.Dash 才能进入(PlayerController 负责条件检查)。
|
||||
/// </summary>
|
||||
public class DashState : PlayerStateBase
|
||||
{
|
||||
private float _timer;
|
||||
private float _cooldownTimer;
|
||||
/// <summary>
|
||||
/// 无敌的独立冷却计时器。由 TickCooldown 每帧减少;
|
||||
/// 地面冲刺和空中冲刺共享此计时器(AerialDashState 通过 GetState<DashState>() 读写)。
|
||||
/// </summary>
|
||||
private float _invincibilityCooldownTimer;
|
||||
private int _facingDir;
|
||||
|
||||
public bool CanDash => _cooldownTimer <= 0f;
|
||||
|
||||
/// <summary>
|
||||
/// 无敌帧是否已冷却,即本次冲刺可以获得无敌。
|
||||
/// DashState 和 AerialDashState 共享同一实例计时器实现 "spam 冲刺不能持续无敌" 机制。
|
||||
/// </summary>
|
||||
public bool CanGrantInvincibility => _invincibilityCooldownTimer <= 0f;
|
||||
|
||||
/// <summary>重置无敌冷却(DashState/AerialDashState 驾用无敌后调用)。</summary>
|
||||
public void ResetInvincibilityCooldown(float cd) => _invincibilityCooldownTimer = cd;
|
||||
|
||||
// ── IsInvincible 不再在状态层硬编码,改为全面依赖 Stats._invincibleTimer ────────────
|
||||
// 冲刺无敌窗口 = BeginInvincibility(DashInvincibilityDuration) 设定的时间限 Stats._invincibleTimer
|
||||
// PlayerController.TakeDamage 已改为将 Stats.IsInvincible 纳入硬直判断,居间无敌窗口自然失效。
|
||||
// 不再需要 override IsInvincible。
|
||||
|
||||
public DashState(PlayerController owner) : base(owner) { }
|
||||
|
||||
public override void OnStateEnter()
|
||||
{
|
||||
Debug.Assert(Cfg != null, "[DashState] MovementConfig 未配置,请在 PlayerController Inspector 中绑定 MovementConfig SO。", _owner);
|
||||
|
||||
_facingDir = Owner.FacingDirection;
|
||||
_timer = Cfg.DashDuration;
|
||||
|
||||
// 无敌帧:
|
||||
// 条件 1:已解锁 InvincibleDash
|
||||
// 条件 2:无敌冷却已就绪(防止 spam 冲刺连序无敌)
|
||||
// 窗口时长 = DashInvincibilityDuration < DashDuration,冲刺后段无保护(对齐 HK)
|
||||
if (Stats != null && Stats.HasAbility(AbilityType.InvincibleDash) && CanGrantInvincibility)
|
||||
{
|
||||
Stats.BeginInvincibility(Cfg.DashInvincibilityDuration);
|
||||
_invincibilityCooldownTimer = Cfg.DashInvincibilityCooldown;
|
||||
}
|
||||
|
||||
// 关闭重力,施加冲刺速度
|
||||
Move?.SetGravityScale(0f);
|
||||
Move?.Dash(new Vector2(_facingDir, 0f), Cfg.DashSpeed);
|
||||
|
||||
// 播放冲刺动画
|
||||
if (AnimCfg?.Dash != null) Anim?.Play(AnimCfg.Dash);
|
||||
}
|
||||
|
||||
public override void OnStateUpdate()
|
||||
{
|
||||
_timer -= Time.deltaTime;
|
||||
if (_timer <= 0f)
|
||||
EndDash();
|
||||
}
|
||||
|
||||
public override void OnStateExit()
|
||||
{
|
||||
// 恢复默认重力
|
||||
Move?.SetGravityScale(Cfg.DefaultGravityScale);
|
||||
_cooldownTimer = Cfg.DashCooldown;
|
||||
}
|
||||
|
||||
public override void OnStateFixedUpdate()
|
||||
{
|
||||
// 冲刺期间保持冲刺速度(防止摩擦力减速)
|
||||
if (_timer > 0f)
|
||||
Move?.Dash(new Vector2(_facingDir, 0f), Cfg.DashSpeed);
|
||||
}
|
||||
|
||||
private void EndDash()
|
||||
{
|
||||
if (Move != null && Move.IsGrounded)
|
||||
Owner.TransitionTo(Owner.GetState<IdleState>());
|
||||
else
|
||||
Owner.TransitionTo(Owner.GetState<FallState>());
|
||||
}
|
||||
|
||||
/// <summary>每帧减少冒小帮冷却(由 PlayerController.Update 调用)。</summary>
|
||||
public void TickCooldown(float dt)
|
||||
{
|
||||
if (_cooldownTimer > 0f) _cooldownTimer -= dt;
|
||||
if (_invincibilityCooldownTimer > 0f) _invincibilityCooldownTimer -= dt;
|
||||
}
|
||||
}
|
||||
}
|
||||
11
Assets/_Game/Scripts/Player/States/DashState.cs.meta
Normal file
11
Assets/_Game/Scripts/Player/States/DashState.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 907f53feec688524b87e472f34d46ab3
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
39
Assets/_Game/Scripts/Player/States/DeadState.cs
Normal file
39
Assets/_Game/Scripts/Player/States/DeadState.cs
Normal file
@@ -0,0 +1,39 @@
|
||||
namespace BaseGames.Player.States
|
||||
{
|
||||
/// <summary>
|
||||
/// 死亡状态(架构 05_PlayerModule §2)。
|
||||
/// 冻结物理(ZeroVelocity + 关闭重力),播放 Dead 动画;
|
||||
/// 不自动转出 — 由 DeathRespawnSystem 通过 EVT_PlayerRespawned 触发重置。
|
||||
/// </summary>
|
||||
public class DeadState : PlayerStateBase
|
||||
{
|
||||
public DeadState(PlayerController owner) : base(owner) { }
|
||||
|
||||
public override void OnStateEnter()
|
||||
{
|
||||
// 冻结物理
|
||||
Move?.ZeroVelocity();
|
||||
Move?.SetGravityScale(0f);
|
||||
|
||||
// 禁用 HurtBox(防止重复受击)
|
||||
if (Owner.HurtBox != null)
|
||||
Owner.HurtBox.SetActive(false);
|
||||
|
||||
// 播放死亡动画
|
||||
if (AnimCfg?.Dead != null)
|
||||
Anim?.Play(AnimCfg.Dead);
|
||||
}
|
||||
|
||||
public override void OnStateExit()
|
||||
{
|
||||
// 复活时恢复重力和 HurtBox
|
||||
Move?.SetGravityScale(Owner.MovConfig.DefaultGravityScale);
|
||||
if (Owner.HurtBox != null)
|
||||
Owner.HurtBox.SetActive(true);
|
||||
}
|
||||
|
||||
// 死亡状态不接受任何输入或状态转换
|
||||
public override void OnStateUpdate() { }
|
||||
public override void OnStateFixedUpdate() { }
|
||||
}
|
||||
}
|
||||
11
Assets/_Game/Scripts/Player/States/DeadState.cs.meta
Normal file
11
Assets/_Game/Scripts/Player/States/DeadState.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 1761709fa9be931418e37b2387899077
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
83
Assets/_Game/Scripts/Player/States/DownAttackState.cs
Normal file
83
Assets/_Game/Scripts/Player/States/DownAttackState.cs
Normal file
@@ -0,0 +1,83 @@
|
||||
using UnityEngine;
|
||||
using BaseGames.Combat;
|
||||
|
||||
namespace BaseGames.Player.States
|
||||
{
|
||||
/// <summary>
|
||||
/// 下劈(踩踏 Pogo)状态(架构 05_PlayerModule §2)。
|
||||
/// 需解锁 AbilityType.DownSlash 才能进入;
|
||||
/// 激活 HitBoxDown,着地或命中后弹跳。
|
||||
/// </summary>
|
||||
public class DownAttackState : PlayerStateBase
|
||||
{
|
||||
private bool _hasHitEnemy;
|
||||
private bool _exited;
|
||||
|
||||
public DownAttackState(PlayerController owner) : base(owner) { }
|
||||
|
||||
public override void OnStateEnter()
|
||||
{
|
||||
_hasHitEnemy = false;
|
||||
_exited = false;
|
||||
|
||||
if (Owner.Combat != null)
|
||||
Owner.Combat.OnDownHitConfirmed += OnDownHitConfirmed;
|
||||
|
||||
Owner.Combat?.EnableWeaponHitBox(AttackDirection.Down);
|
||||
|
||||
var clip = Owner.Weapon?.ActiveWeapon?.downAttackClip;
|
||||
if (clip != null && clip.Clip != null)
|
||||
{
|
||||
var state = Anim.Play(clip);
|
||||
state.Events(this).OnEnd = OnClipEnd;
|
||||
}
|
||||
else if (AnimCfg?.DownAttack != null)
|
||||
{
|
||||
var state = Anim.Play(AnimCfg.DownAttack);
|
||||
state.Events(this).OnEnd = OnClipEnd;
|
||||
}
|
||||
else
|
||||
{
|
||||
OnClipEnd();
|
||||
}
|
||||
|
||||
// 施加向下的速度(下劈冲击)
|
||||
if (Move?.Rb != null)
|
||||
Move.Rb.velocity = new Vector2(Move.Rb.velocity.x, -18f);
|
||||
}
|
||||
|
||||
public override void OnStateExit()
|
||||
{
|
||||
_exited = true;
|
||||
if (Owner.Combat != null)
|
||||
Owner.Combat.OnDownHitConfirmed -= OnDownHitConfirmed;
|
||||
Owner.Combat?.DisableAllWeaponHitBoxes();
|
||||
}
|
||||
|
||||
private void OnDownHitConfirmed(BaseGames.Combat.DamageInfo _)
|
||||
{
|
||||
if (_hasHitEnemy) return;
|
||||
_hasHitEnemy = true;
|
||||
// Pogo 弹跳:命中敌人后向上弹起
|
||||
Move.Jump();
|
||||
}
|
||||
|
||||
public override void OnStateUpdate()
|
||||
{
|
||||
// 着地时回到 Idle / Run
|
||||
if (Move.IsGrounded)
|
||||
{
|
||||
Owner.TransitionTo(Owner.GetState<IdleState>());
|
||||
}
|
||||
}
|
||||
|
||||
private void OnClipEnd()
|
||||
{
|
||||
if (_exited) return;
|
||||
if (!Move.IsGrounded)
|
||||
Owner.TransitionTo(Owner.GetState<FallState>());
|
||||
else
|
||||
Owner.TransitionTo(Owner.GetState<IdleState>());
|
||||
}
|
||||
}
|
||||
}
|
||||
11
Assets/_Game/Scripts/Player/States/DownAttackState.cs.meta
Normal file
11
Assets/_Game/Scripts/Player/States/DownAttackState.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 35c14ecd9a2db5c48a95c015ac6128fd
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
84
Assets/_Game/Scripts/Player/States/FallState.cs
Normal file
84
Assets/_Game/Scripts/Player/States/FallState.cs
Normal file
@@ -0,0 +1,84 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace BaseGames.Player.States
|
||||
{
|
||||
/// <summary>
|
||||
/// 下落状态(对齐空洞骑士手感)。
|
||||
/// - 郊狼跳:CoyoteTimer > 0 时按跳跃 → 一段跳(JumpState,使用 JumpForce)。
|
||||
/// - 二段跳:CoyoteTimer 耗尽后按跳跃且 AirJumpsLeft > 0 → JumpState(使用 DoubleJumpForce)。
|
||||
/// - 空中冲刺:HasAbility(AirDash) && HasAerialDash → AerialDashState。
|
||||
/// - 增强下落重力(FallGravityMult)确保下落快于上升,手感紧实。
|
||||
/// </summary>
|
||||
public class FallState : PlayerStateBase
|
||||
{
|
||||
public FallState(PlayerController owner) : base(owner) { }
|
||||
|
||||
public override void OnStateEnter()
|
||||
{
|
||||
if (AnimCfg?.Fall != null)
|
||||
Anim.Play(AnimCfg.Fall);
|
||||
}
|
||||
|
||||
public override void OnStateUpdate()
|
||||
{
|
||||
// ── 跳跃输入(郊狼跳 / 二段跳)────────────────────────────────────
|
||||
if (Buffer.ConsumeJump())
|
||||
{
|
||||
if (Move.HasCoyoteTime)
|
||||
{
|
||||
// 郊狼跳:一段跳,使用 JumpForce
|
||||
_owner.TransitionTo(_owner.GetState<JumpState>());
|
||||
return;
|
||||
}
|
||||
if (Owner.AirJumpsLeft > 0)
|
||||
{
|
||||
// 二段跳:通过 SetDoubleJump 标记 JumpState 使用 DoubleJumpForce
|
||||
Owner.UseAirJump();
|
||||
Owner.GetState<JumpState>()?.SetDoubleJump(true);
|
||||
_owner.TransitionTo(_owner.GetState<JumpState>());
|
||||
return;
|
||||
}
|
||||
// 无跳跃机会:输入已消耗,静默忽略(HK 相同行为)
|
||||
}
|
||||
|
||||
// ── 空中冲刺────────────────────────────────────────────────────────
|
||||
if (Buffer.ConsumeDash()
|
||||
&& Stats != null && Stats.HasAbility(AbilityType.AirDash))
|
||||
{
|
||||
var aerialDash = Owner.GetState<AerialDashState>();
|
||||
if (aerialDash != null && aerialDash.HasAerialDash)
|
||||
{
|
||||
_owner.TransitionTo(aerialDash);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// ── 着地──────────────────────────────────────────────────────────
|
||||
if (Move.IsGrounded)
|
||||
{
|
||||
Move.ZeroVelocity();
|
||||
if (Mathf.Abs(Input.MoveInput.x) > 0.1f)
|
||||
_owner.TransitionTo(_owner.GetState<RunState>());
|
||||
else
|
||||
_owner.TransitionTo(_owner.GetState<IdleState>());
|
||||
return;
|
||||
}
|
||||
|
||||
// 空中水平移动
|
||||
if (Mathf.Abs(Input.MoveInput.x) > 0.01f)
|
||||
Move.Move(Input.MoveInput.x * Cfg.RunSpeed);
|
||||
}
|
||||
|
||||
public override void OnStateFixedUpdate()
|
||||
{
|
||||
// 增强下落重力(FallGravityMult 对齐 HK:下落比上升更快)
|
||||
if (Move.Rb.velocity.y < 0f)
|
||||
{
|
||||
float extraGrav = Physics2D.gravity.y * (Cfg.FallGravityMult - 1f) * Time.fixedDeltaTime;
|
||||
Move.Rb.velocity = new Vector2(
|
||||
Move.Rb.velocity.x,
|
||||
Mathf.Max(Move.Rb.velocity.y + extraGrav, -Cfg.MaxFallSpeed));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
11
Assets/_Game/Scripts/Player/States/FallState.cs.meta
Normal file
11
Assets/_Game/Scripts/Player/States/FallState.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d81f2601a90beeb4382680e53e18be63
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
63
Assets/_Game/Scripts/Player/States/HurtState.cs
Normal file
63
Assets/_Game/Scripts/Player/States/HurtState.cs
Normal file
@@ -0,0 +1,63 @@
|
||||
using UnityEngine;
|
||||
using BaseGames.Combat;
|
||||
|
||||
namespace BaseGames.Player.States
|
||||
{
|
||||
/// <summary>
|
||||
/// 受击硬直状态(架构 05_PlayerModule §2)。
|
||||
/// 由 PlayerController.TakeDamage 在玩家非无敌时触发;
|
||||
/// 播放 Hurt 动画,施加击退,结束后回到 Idle 或 Fall。
|
||||
/// </summary>
|
||||
public class HurtState : PlayerStateBase
|
||||
{
|
||||
private float _timer;
|
||||
private bool _ended;
|
||||
|
||||
public HurtState(PlayerController owner) : base(owner) { }
|
||||
|
||||
public void Initialize(BaseGames.Combat.DamageInfo info)
|
||||
{
|
||||
// 由 PlayerController.TakeDamage 传入伤害信息
|
||||
if (info.KnockbackForce > 0.01f)
|
||||
Move?.ApplyKnockback(info.KnockbackDirection, info.KnockbackForce);
|
||||
}
|
||||
|
||||
public override void OnStateEnter()
|
||||
{
|
||||
// 持续时长从 PlayerAnimationConfigSO 读取,不同攻击可设置不同硬直
|
||||
_timer = Owner.AnimConfig?.HurtDuration ?? 0.4f;
|
||||
_ended = false;
|
||||
Stats?.BeginInvincibility();
|
||||
|
||||
if (AnimCfg?.Hurt != null)
|
||||
{
|
||||
var state = Anim?.Play(AnimCfg.Hurt);
|
||||
if (state != null)
|
||||
state.Events(this).OnEnd = OnHurtEnd;
|
||||
}
|
||||
}
|
||||
|
||||
public override void OnStateUpdate()
|
||||
{
|
||||
_timer -= Time.deltaTime;
|
||||
if (_timer <= 0f)
|
||||
OnHurtEnd();
|
||||
}
|
||||
|
||||
private void OnHurtEnd()
|
||||
{
|
||||
if (_ended) return;
|
||||
_ended = true;
|
||||
if (Stats != null && !Stats.IsAlive)
|
||||
{
|
||||
Owner.TransitionTo(Owner.GetState<DeadState>());
|
||||
return;
|
||||
}
|
||||
|
||||
if (Move.IsGrounded)
|
||||
Owner.TransitionTo(Owner.GetState<IdleState>());
|
||||
else
|
||||
Owner.TransitionTo(Owner.GetState<FallState>());
|
||||
}
|
||||
}
|
||||
}
|
||||
11
Assets/_Game/Scripts/Player/States/HurtState.cs.meta
Normal file
11
Assets/_Game/Scripts/Player/States/HurtState.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f7192bcf89ed1f5419d40fa182ac66ba
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
46
Assets/_Game/Scripts/Player/States/IdleState.cs
Normal file
46
Assets/_Game/Scripts/Player/States/IdleState.cs
Normal file
@@ -0,0 +1,46 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace BaseGames.Player.States
|
||||
{
|
||||
/// <summary>闲置状态。默认入口状态,播放 Idle 动画。</summary>
|
||||
public class IdleState : PlayerStateBase
|
||||
{
|
||||
public IdleState(PlayerController owner) : base(owner) { }
|
||||
|
||||
public override void OnStateEnter()
|
||||
{
|
||||
if (AnimCfg?.Idle != null)
|
||||
Anim.Play(AnimCfg.Idle);
|
||||
Move?.ZeroHorizontalVelocity();
|
||||
// 落地时重置空中能力计数器
|
||||
Owner.GetState<AerialDashState>()?.ResetAerialDashes();
|
||||
Owner.ResetAirJumps();
|
||||
}
|
||||
|
||||
public override void OnStateUpdate()
|
||||
{
|
||||
if (!Move.IsGrounded)
|
||||
{
|
||||
_owner.TransitionTo(_owner.GetState<FallState>());
|
||||
return;
|
||||
}
|
||||
if (Buffer.ConsumeJump())
|
||||
{
|
||||
_owner.TransitionTo(_owner.GetState<JumpState>());
|
||||
return;
|
||||
}
|
||||
// 地面冲刺:需解锁 Dash 能力,且冲刺不在冷却
|
||||
if (Buffer.ConsumeDash()
|
||||
&& Stats != null && Stats.HasAbility(AbilityType.Dash)
|
||||
&& Owner.GetState<DashState>() is { CanDash: true } dashState)
|
||||
{
|
||||
_owner.TransitionTo(dashState);
|
||||
return;
|
||||
}
|
||||
if (Mathf.Abs(Input.MoveInput.x) > 0.1f)
|
||||
{
|
||||
_owner.TransitionTo(_owner.GetState<RunState>());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
11
Assets/_Game/Scripts/Player/States/IdleState.cs.meta
Normal file
11
Assets/_Game/Scripts/Player/States/IdleState.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 84b6b9fa502d3d34a8d7284831404d75
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
84
Assets/_Game/Scripts/Player/States/JumpState.cs
Normal file
84
Assets/_Game/Scripts/Player/States/JumpState.cs
Normal file
@@ -0,0 +1,84 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace BaseGames.Player.States
|
||||
{
|
||||
/// <summary>
|
||||
/// 跳跃状态(对齐空洞骑士手感)。
|
||||
/// - 一段跳 / 郊狼跳:OnStateEnter 时调用 Move.Jump()。
|
||||
/// - 二段跳(Monarch Wings 等效):上升或下落途中再按跳跃且 AirJumpsLeft > 0,
|
||||
/// 调用 Move.DoubleJump(),重播跳跃动画,不离开本状态(保持速度截断逻辑)。
|
||||
/// - 空中冲刺:上升途中按冲刺且 HasAbility(AirDash) → AerialDashState。
|
||||
/// - 变高跳:松开跳跃键触发 JumpCancelledEvent → CutJump()(系数 = JumpCutMultiplier)。
|
||||
/// - _isDoubleJump:由 FallState 在转换前通过 SetDoubleJump(true) 预设,
|
||||
/// 使 OnStateEnter 对二段跳调用 Move.DoubleJump() 而非 Move.Jump()。
|
||||
/// </summary>
|
||||
public class JumpState : PlayerStateBase
|
||||
{
|
||||
private bool _isDoubleJump;
|
||||
|
||||
public JumpState(PlayerController owner) : base(owner) { }
|
||||
|
||||
/// <summary>
|
||||
/// 由 FallState 在转换到 JumpState 前调用,标记本次进入为二段跳,
|
||||
/// 以便 OnStateEnter 使用 DoubleJumpForce 而非 JumpForce。
|
||||
/// </summary>
|
||||
public void SetDoubleJump(bool isDouble) => _isDoubleJump = isDouble;
|
||||
|
||||
public override void OnStateEnter()
|
||||
{
|
||||
if (AnimCfg?.Jump != null)
|
||||
Anim.Play(AnimCfg.Jump);
|
||||
|
||||
if (_isDoubleJump)
|
||||
Move.DoubleJump();
|
||||
else
|
||||
Move.Jump();
|
||||
|
||||
_isDoubleJump = false; // 消耗标记
|
||||
Input.JumpCancelledEvent += OnJumpCancelled;
|
||||
}
|
||||
|
||||
public override void OnStateUpdate()
|
||||
{
|
||||
// 上升结束时转为下落
|
||||
if (Move.Rb.velocity.y <= 0f)
|
||||
{
|
||||
_owner.TransitionTo(_owner.GetState<FallState>());
|
||||
return;
|
||||
}
|
||||
|
||||
// 空中冲刺(优先于二段跳:冲刺可保存二段跳机会)
|
||||
if (Buffer.ConsumeDash()
|
||||
&& Stats != null && Stats.HasAbility(AbilityType.AirDash))
|
||||
{
|
||||
var aerialDash = Owner.GetState<AerialDashState>();
|
||||
if (aerialDash != null && aerialDash.HasAerialDash)
|
||||
{
|
||||
_owner.TransitionTo(aerialDash);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 二段跳:上升阶段即可触发(类比 HK Monarch Wings,随时可二段跳)
|
||||
if (Buffer.ConsumeJump() && Owner.AirJumpsLeft > 0)
|
||||
{
|
||||
Owner.UseAirJump();
|
||||
Move.DoubleJump();
|
||||
// 留在 JumpState:速度截断(JumpCancelledEvent)和落地检测逻辑继续生效
|
||||
if (AnimCfg?.Jump != null) Anim?.Play(AnimCfg.Jump);
|
||||
return;
|
||||
}
|
||||
|
||||
// 水平移动(HK 空中控制:与跑步同速)
|
||||
if (Mathf.Abs(Input.MoveInput.x) > 0.01f)
|
||||
Move.Move(Input.MoveInput.x * Cfg.RunSpeed);
|
||||
}
|
||||
|
||||
public override void OnStateExit()
|
||||
{
|
||||
Input.JumpCancelledEvent -= OnJumpCancelled;
|
||||
}
|
||||
|
||||
private void OnJumpCancelled() => Move.CutJump();
|
||||
}
|
||||
}
|
||||
11
Assets/_Game/Scripts/Player/States/JumpState.cs.meta
Normal file
11
Assets/_Game/Scripts/Player/States/JumpState.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 643f44aee61bd684ebd893779e1122aa
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
46
Assets/_Game/Scripts/Player/States/ParryState.cs
Normal file
46
Assets/_Game/Scripts/Player/States/ParryState.cs
Normal file
@@ -0,0 +1,46 @@
|
||||
namespace BaseGames.Player.States
|
||||
{
|
||||
/// <summary>
|
||||
/// 弹反预备状态(架构 05_PlayerModule §2,完整实现在 Week 6)。
|
||||
/// 开启 ParrySystem 弹反窗口,播放 ParryStart 动画;
|
||||
/// 成功弹反后 ParrySystem.ConsumeParry() 返回 true,HurtBox 不处理该次伤害。
|
||||
/// </summary>
|
||||
public class ParryState : PlayerStateBase
|
||||
{
|
||||
public ParryState(PlayerController owner) : base(owner) { }
|
||||
|
||||
public override void OnStateEnter()
|
||||
{
|
||||
// 开启弹反窗口(HurtBox.ReceiveDamage 步骤 2 查询)
|
||||
Owner.Parry?.OpenParryWindow();
|
||||
|
||||
// 停止移动
|
||||
Move?.ZeroHorizontalVelocity();
|
||||
|
||||
// 播放弹反预备动画
|
||||
if (AnimCfg?.ParryStart != null)
|
||||
{
|
||||
var state = Anim?.Play(AnimCfg.ParryStart);
|
||||
if (state != null)
|
||||
{
|
||||
state.Events(this).OnEnd = OnParryEnd;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 无动画则立即结束
|
||||
OnParryEnd();
|
||||
}
|
||||
|
||||
public override void OnStateExit()
|
||||
{
|
||||
// 确保弹反窗口关闭(无论是否成功)
|
||||
Owner.Parry?.CloseParryWindow();
|
||||
}
|
||||
|
||||
private void OnParryEnd()
|
||||
{
|
||||
Owner.TransitionTo(Owner.GetState<IdleState>());
|
||||
}
|
||||
}
|
||||
}
|
||||
11
Assets/_Game/Scripts/Player/States/ParryState.cs.meta
Normal file
11
Assets/_Game/Scripts/Player/States/ParryState.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 29962783977c7e345bd6b2716c59c39b
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
343
Assets/_Game/Scripts/Player/States/PlayerController.cs
Normal file
343
Assets/_Game/Scripts/Player/States/PlayerController.cs
Normal file
@@ -0,0 +1,343 @@
|
||||
using UnityEngine;
|
||||
using System.Linq;
|
||||
using Animancer;
|
||||
using BaseGames.Core.Events;
|
||||
using BaseGames.Input;
|
||||
using BaseGames.Combat;
|
||||
using BaseGames.Parry;
|
||||
using BaseGames.Skills;
|
||||
|
||||
namespace BaseGames.Player.States
|
||||
{
|
||||
/// <summary>
|
||||
/// 玩家主控制器(协调器)。位于 Player/States/ 程序集,以便引用所有具体状态类型。
|
||||
/// 实现 IDamageable + IPoiseSource(架构 06_CombatModule §13)。
|
||||
/// 依赖注入:同节点组件由 RequireComponent + Awake 自动获取;跨节点引用通过 [SerializeField] 绑定。
|
||||
/// </summary>
|
||||
[DefaultExecutionOrder(-100)]
|
||||
[RequireComponent(typeof(InputBuffer))]
|
||||
[RequireComponent(typeof(PlayerMovement))]
|
||||
[RequireComponent(typeof(PlayerStats))]
|
||||
[RequireComponent(typeof(AnimancerComponent))]
|
||||
public class PlayerController : MonoBehaviour, IDamageable, IPoiseSource
|
||||
{
|
||||
// ── 同节点组件(由 RequireComponent 保证存在,Awake 中自动获取)─────
|
||||
private PlayerMovement _movement;
|
||||
private PlayerStats _stats;
|
||||
private AnimancerComponent _animancer;
|
||||
|
||||
// ── 配置 SO ───────────────────────────────────────────────────────────
|
||||
[Header("配置")]
|
||||
[SerializeField] private PlayerMovementConfigSO _movementConfig;
|
||||
[SerializeField] private PlayerAnimationConfigSO _animConfig;
|
||||
[SerializeField] private InputReaderSO _inputReader;
|
||||
[SerializeField] private FormConfigSO _formConfig;
|
||||
|
||||
// ── 战斗组件 ──────────────────────────────────────────────────────────
|
||||
[Header("战斗")]
|
||||
[SerializeField] private PlayerCombat _combat;
|
||||
[SerializeField] private FormController _formController;
|
||||
[SerializeField] private WeaponManager _weaponManager;
|
||||
[SerializeField] private SkillManager _skillManager;
|
||||
[SerializeField] private SpringSystem _springSystem;
|
||||
[SerializeField] private ParrySystem _parrySystem;
|
||||
[SerializeField] private HurtBox _hurtBox;
|
||||
[SerializeField] private ShieldComponent _shield;
|
||||
[SerializeField] private PlayerWallDetector _wallDetector;
|
||||
|
||||
// ── 事件频道 ──────────────────────────────────────────────────────────
|
||||
[Header("事件频道")]
|
||||
[SerializeField] private VoidEventChannelSO _onPlayerDied;
|
||||
/// <summary>
|
||||
/// Start() 时广播玩家 Transform(EnemyBase / ProjectileManager 等订阅此频道)。
|
||||
/// 替代每个敌人在 Awake 中独立 FindWithTag 的 O(n) 全场景扫描。
|
||||
/// </summary>
|
||||
[SerializeField] private TransformEventChannelSO _onPlayerSpawned;
|
||||
// ── 运行时 ────────────────────────────────────────────────────────────
|
||||
private InputBuffer _inputBuffer;
|
||||
private bool _missingDependencyLogged;
|
||||
private bool _dependenciesReady;
|
||||
/// <summary>
|
||||
/// 当前腾空可用的额外跳跃次数(二段跳)。
|
||||
/// 由 IdleState/RunState.OnStateEnter 落地时通过 ResetAirJumps() 重置;
|
||||
/// JumpState/FallState 判断 HasAbility(DoubleJump) 后消耗。
|
||||
/// </summary>
|
||||
private int _airJumpsLeft;
|
||||
#if UNITY_EDITOR
|
||||
[Header("调试")]
|
||||
[SerializeField] private bool _debugValidateTransitions = true;
|
||||
#endif
|
||||
|
||||
// Overlay Layer(Layer 1):供 SpringState / SoulSkill 等叠加层动画使用
|
||||
// Base Layer(Layer 0):移动/攻击/受伤/死亡等全身状态动画
|
||||
private AnimancerLayer _overlayLayer;
|
||||
|
||||
// ── 状态实例 ──────────────────────────────────────────────────────────
|
||||
private PlayerStateBase _currentState;
|
||||
private readonly System.Collections.Generic.Dictionary<System.Type, PlayerStateBase> _states = new();
|
||||
|
||||
// ── IDamageable 实现 ──────────────────────────────────────────────────
|
||||
public bool IsAlive => _stats != null && _stats.IsAlive;
|
||||
public bool IsInvincible => _stats != null && _stats.IsInvincible;
|
||||
public int Defense => 0;
|
||||
|
||||
public void TakeDamage(DamageInfo info)
|
||||
{
|
||||
if (_stats == null) return;
|
||||
_stats.TakeDamage(info.FinalDamage);
|
||||
|
||||
// 当前状态标记为无敌(如旧版冲刺状态),或 Stats 层无敌窗口仍激活
|
||||
// (冲刺无敌帧 DashInvincibilityDuration 内:跳过受击硬直;窗口过期后可被打断)
|
||||
if (_currentState?.IsInvincible == true || (_stats != null && _stats.IsInvincible)) return;
|
||||
|
||||
if (_stats.IsAlive)
|
||||
{
|
||||
GetState<HurtState>()?.Initialize(info);
|
||||
TransitionTo(GetState<HurtState>());
|
||||
}
|
||||
else
|
||||
{
|
||||
TransitionTo(GetState<DeadState>());
|
||||
_onPlayerDied?.Raise();
|
||||
}
|
||||
}
|
||||
|
||||
// ── IPoiseSource 实现(架构 06_CombatModule §13)─────────────────────
|
||||
/// <summary>
|
||||
/// 玩家不拥有霸体,始终返回 <see cref="PoiseLevel.None"/>。
|
||||
/// 设计决策:类似 Hollow Knight,玩家依靠走位和弹反规避伤害,而非硬吃。
|
||||
/// 若未来需要临时霸体(如特定技能动作),请通过独立的覆盖标记实现,
|
||||
/// 而非在此处引入状态,以保持接口语义清晰。
|
||||
/// </summary>
|
||||
public PoiseLevel GetCurrentPoiseLevel() => PoiseLevel.None;
|
||||
|
||||
// ── 公开属性(供状态类访问)──────────────────────────────────────────
|
||||
public PlayerMovement Movement => _movement;
|
||||
public PlayerStats Stats => _stats;
|
||||
public AnimancerComponent Animancer => _animancer;
|
||||
public PlayerMovementConfigSO MovConfig => _movementConfig;
|
||||
public PlayerAnimationConfigSO AnimConfig => _animConfig;
|
||||
public InputReaderSO Input => _inputReader;
|
||||
public InputBuffer Buffer => _inputBuffer;
|
||||
|
||||
public PlayerCombat Combat => _combat;
|
||||
public FormController Form => _formController;
|
||||
public WeaponManager Weapon => _weaponManager;
|
||||
public SkillManager Skill => _skillManager;
|
||||
public SpringSystem Spring => _springSystem;
|
||||
public ParrySystem Parry => _parrySystem;
|
||||
public HurtBox HurtBox => _hurtBox;
|
||||
public ShieldComponent Shield => _shield;
|
||||
public PlayerWallDetector WallDetector => _wallDetector;
|
||||
|
||||
public bool IsGrounded => _movement != null && _movement.IsGrounded;
|
||||
public int FacingDirection => _movement != null ? _movement.FacingDirection : 1;
|
||||
|
||||
// ── 空中跳跃 API(二段跳)────────────────────────────────────────────
|
||||
public int AirJumpsLeft => _airJumpsLeft;
|
||||
public void UseAirJump() => _airJumpsLeft = Mathf.Max(0, _airJumpsLeft - 1);
|
||||
/// <summary>
|
||||
/// 落地时重置空中跳跃次数(由 IdleState/RunState.OnStateEnter 调用)。
|
||||
/// 若解锁 DoubleJump 则重置为 1,否则为 0。
|
||||
/// </summary>
|
||||
public void ResetAirJumps() =>
|
||||
_airJumpsLeft = _stats != null && _stats.HasAbility(AbilityType.DoubleJump) ? 1 : 0;
|
||||
|
||||
// ── Overlay Layer API(供 SpringState / SoulSkill 等叠加动画使用)─────
|
||||
/// <summary>
|
||||
/// 在 Overlay Layer(Layer 1)播放动画,叠加于当前 Base Layer 动画之上。
|
||||
/// 适用于灵泉使用、魂技能等需要与移动动画并行的上半身动作。
|
||||
/// </summary>
|
||||
public AnimancerState PlayOnOverlay(AnimationClip clip)
|
||||
=> _overlayLayer?.Play(clip);
|
||||
|
||||
/// <summary>停止 Overlay Layer 动画,淡出回 Base Layer。</summary>
|
||||
public void StopOverlay(float fadeDuration = 0.1f)
|
||||
=> _overlayLayer?.StartFade(0f, fadeDuration);
|
||||
|
||||
// ── Unity Lifecycle ───────────────────────────────────────────────────
|
||||
private void Awake()
|
||||
{
|
||||
Debug.Assert(_movementConfig != null, "[PlayerController] _movementConfig 未赋值,请在 Inspector 中指定 PlayerMovementConfigSO。", this);
|
||||
ResolveDependencies();
|
||||
|
||||
// 初始化 Animancer 双层动画:Layer 0 = Base(全身状态),Layer 1 = Overlay(叠加层)
|
||||
if (_animancer != null)
|
||||
{
|
||||
// 确保 Layer 1 存在;AnimancerComponent 默认只有 Layer 0
|
||||
while (_animancer.Layers.Count <= 1)
|
||||
_animancer.Layers.Add();
|
||||
_overlayLayer = _animancer.Layers[1];
|
||||
}
|
||||
|
||||
// 注入 HurtBox 依赖
|
||||
if (_hurtBox != null)
|
||||
{
|
||||
if (_shield != null) _hurtBox.SetShieldable(_shield);
|
||||
if (_parrySystem != null) _hurtBox.SetParrySystem(_parrySystem);
|
||||
_hurtBox.SetPoiseSource(this);
|
||||
}
|
||||
|
||||
// 将唯一配置点(_inputReader)注入到 ParrySystem。
|
||||
// ParrySystem 不再需要在 Inspector 单独配置 InputReaderSO。
|
||||
if (_parrySystem != null)
|
||||
_parrySystem.SetInputReader(_inputReader);
|
||||
|
||||
InitializeStates();
|
||||
|
||||
// 订阅 ParrySystem C# 事件
|
||||
if (_parrySystem != null)
|
||||
{
|
||||
_parrySystem.OnParryActivated += OnParryActivated;
|
||||
_parrySystem.OnParryConsumed += OnParryConsumedHandler;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnDestroy()
|
||||
{
|
||||
if (_parrySystem != null)
|
||||
{
|
||||
_parrySystem.OnParryActivated -= OnParryActivated;
|
||||
_parrySystem.OnParryConsumed -= OnParryConsumedHandler;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>弹反输入激活时由 ParrySystem 触发 → 转换到 ParryState。</summary>
|
||||
private void OnParryActivated()
|
||||
{
|
||||
if (_states.ContainsKey(typeof(ParryState)))
|
||||
TransitionTo(GetState<ParryState>());
|
||||
}
|
||||
|
||||
/// <summary>弹反命中成功时由 ParrySystem 触发 → 发放灵力并恢复护盾。</summary>
|
||||
private void OnParryConsumedHandler(BaseGames.Parry.ParryInfo info)
|
||||
{
|
||||
_stats?.AddSoul(info.SoulGained);
|
||||
_shield?.OnParrySuccess();
|
||||
}
|
||||
|
||||
private void Start()
|
||||
{
|
||||
if (!HasRequiredStateDependencies())
|
||||
return;
|
||||
|
||||
// 广播玩家 Transform:EnemyBase / ProjectileManager 等订阅者将通过事件接收引用
|
||||
// (必须在 Start 中调用,确保所有 Awake/OnEnable 订阅已就绪)
|
||||
_onPlayerSpawned?.Raise(transform);
|
||||
|
||||
TransitionTo(GetState<IdleState>());
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
if (!HasRequiredStateDependencies())
|
||||
return;
|
||||
|
||||
// 冲刺冷却计时
|
||||
GetState<DashState>()?.TickCooldown(Time.deltaTime);
|
||||
|
||||
_currentState?.OnStateUpdate();
|
||||
}
|
||||
|
||||
private void FixedUpdate()
|
||||
{
|
||||
_currentState?.OnStateFixedUpdate();
|
||||
}
|
||||
|
||||
private void LateUpdate()
|
||||
{
|
||||
_movement?.UpdateFacing();
|
||||
}
|
||||
|
||||
// ── 状态机 ────────────────────────────────────────────────────────────
|
||||
public void TransitionTo(PlayerStateBase newState)
|
||||
{
|
||||
#if UNITY_EDITOR
|
||||
if (_debugValidateTransitions && _currentState != null && newState != null)
|
||||
{
|
||||
var allowed = _currentState.ValidTransitions;
|
||||
if (allowed.Count > 0 && !allowed.Contains(newState.GetType()))
|
||||
Debug.LogWarning(
|
||||
$"[PlayerController] 非预期转换: {_currentState.GetType().Name} → {newState.GetType().Name}",
|
||||
this);
|
||||
}
|
||||
#endif
|
||||
_currentState?.OnStateExit();
|
||||
_currentState = newState;
|
||||
_currentState?.OnStateEnter();
|
||||
}
|
||||
|
||||
private void InitializeStates()
|
||||
{
|
||||
_states[typeof(IdleState)] = new IdleState(this);
|
||||
_states[typeof(RunState)] = new RunState(this);
|
||||
_states[typeof(JumpState)] = new JumpState(this);
|
||||
_states[typeof(FallState)] = new FallState(this);
|
||||
_states[typeof(AttackState)] = new AttackState(this);
|
||||
_states[typeof(DashState)] = new DashState(this);
|
||||
_states[typeof(AerialDashState)] = new AerialDashState(this);
|
||||
_states[typeof(WallSlideState)] = new WallSlideState(this);
|
||||
_states[typeof(WallJumpState)] = new WallJumpState(this);
|
||||
_states[typeof(AirAttackState)] = new AirAttackState(this);
|
||||
_states[typeof(DownAttackState)] = new DownAttackState(this);
|
||||
_states[typeof(UpAttackState)] = new UpAttackState(this);
|
||||
_states[typeof(HurtState)] = new HurtState(this);
|
||||
_states[typeof(DeadState)] = new DeadState(this);
|
||||
_states[typeof(SpringState)] = new SpringState(this);
|
||||
_states[typeof(ParryState)] = new ParryState(this);
|
||||
_states[typeof(SwimState)] = new SwimState(this);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 按类型获取状态实例。未注册时返回 null(供可选状态调用方安全使用)。
|
||||
/// </summary>
|
||||
public T GetState<T>() where T : PlayerStateBase
|
||||
=> _states.TryGetValue(typeof(T), out var s) ? (T)s : null;
|
||||
|
||||
private void ResolveDependencies()
|
||||
{
|
||||
if (_movement == null)
|
||||
_movement = GetComponent<PlayerMovement>();
|
||||
if (_stats == null)
|
||||
_stats = GetComponent<PlayerStats>();
|
||||
if (_animancer == null)
|
||||
_animancer = GetComponent<AnimancerComponent>();
|
||||
if (_inputBuffer == null)
|
||||
_inputBuffer = GetComponent<InputBuffer>();
|
||||
|
||||
// 将唯一配置点(_inputReader)注入到同一 GameObject 上的 InputBuffer。
|
||||
// InputBuffer 不再需要在 Inspector 单独配置 InputReaderSO。
|
||||
if (_inputBuffer != null)
|
||||
_inputBuffer.Init(_inputReader);
|
||||
}
|
||||
|
||||
private bool HasRequiredStateDependencies()
|
||||
{
|
||||
if (_dependenciesReady) return true;
|
||||
|
||||
bool ok = _movement != null && _animancer != null && _inputBuffer != null && _inputReader != null;
|
||||
if (ok)
|
||||
{
|
||||
_dependenciesReady = true;
|
||||
_missingDependencyLogged = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!_missingDependencyLogged)
|
||||
{
|
||||
Debug.LogError($"[PlayerController] Missing required dependencies. " +
|
||||
$"Movement={(_movement != null ? "OK" : "NULL")}, " +
|
||||
$"Animancer={(_animancer != null ? "OK" : "NULL")}, " +
|
||||
$"InputBuffer={(_inputBuffer != null ? "OK" : "NULL")}, " +
|
||||
$"InputReader={(_inputReader != null ? "OK" : "NULL")}");
|
||||
_missingDependencyLogged = true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
// ── 状态访问器 ────────────────────────────────────────────────────────
|
||||
// 使用 GetState<T>() 按类型获取任意状态实例,不再暴露具名属性。
|
||||
}
|
||||
}
|
||||
11
Assets/_Game/Scripts/Player/States/PlayerController.cs.meta
Normal file
11
Assets/_Game/Scripts/Player/States/PlayerController.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e20d2200567c4ca4d8fa1a047c7bbd58
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: -100
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
48
Assets/_Game/Scripts/Player/States/PlayerStateBase.cs
Normal file
48
Assets/_Game/Scripts/Player/States/PlayerStateBase.cs
Normal file
@@ -0,0 +1,48 @@
|
||||
using Animancer;
|
||||
using BaseGames.Input;
|
||||
using BaseGames.Player;
|
||||
|
||||
namespace BaseGames.Player.States
|
||||
{
|
||||
/// <summary>
|
||||
/// 所有玩家状态的抽象基类。持有 PlayerController 引用并提供便捷属性访问。
|
||||
/// 状态不继承 MonoBehaviour,生命周期由 PlayerController 驱动。
|
||||
/// </summary>
|
||||
public abstract class PlayerStateBase
|
||||
{
|
||||
protected PlayerController _owner;
|
||||
|
||||
protected PlayerStateBase(PlayerController owner) => _owner = owner;
|
||||
|
||||
public virtual void OnStateEnter() { }
|
||||
public virtual void OnStateUpdate() { }
|
||||
public virtual void OnStateFixedUpdate() { }
|
||||
public virtual void OnStateExit() { }
|
||||
public virtual PlayerStateBase GetNextState() => null;
|
||||
|
||||
/// <summary>
|
||||
/// 此状态期间是否应视为无敌(忽略伤害)。
|
||||
/// 冲刺等状态 override 为 true,PlayerController.TakeDamage 据此判断是否进入受击。
|
||||
/// </summary>
|
||||
public virtual bool IsInvincible => false;
|
||||
|
||||
#if UNITY_EDITOR
|
||||
/// <summary>
|
||||
/// 此状态允许转换到的目标类型白名单(仅 Editor 调试用)。
|
||||
/// 返回空列表表示不限制任何转换。子状态按需 override 来声明合法出口。
|
||||
/// </summary>
|
||||
public virtual System.Collections.Generic.IReadOnlyList<System.Type> ValidTransitions
|
||||
=> System.Array.Empty<System.Type>();
|
||||
#endif
|
||||
|
||||
// ── 便捷属性 ──────────────────────────────────────────────────────────
|
||||
protected PlayerController Owner => _owner;
|
||||
protected InputReaderSO Input => _owner.Input;
|
||||
protected InputBuffer Buffer => _owner.Buffer;
|
||||
protected PlayerMovement Move => _owner.Movement;
|
||||
protected PlayerStats Stats => _owner.Stats;
|
||||
protected AnimancerComponent Anim => _owner.Animancer;
|
||||
protected PlayerMovementConfigSO Cfg => _owner.MovConfig;
|
||||
protected PlayerAnimationConfigSO AnimCfg => _owner.AnimConfig;
|
||||
}
|
||||
}
|
||||
11
Assets/_Game/Scripts/Player/States/PlayerStateBase.cs.meta
Normal file
11
Assets/_Game/Scripts/Player/States/PlayerStateBase.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d0e192113c871fe44ba2d9d56c95c27e
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
51
Assets/_Game/Scripts/Player/States/RunState.cs
Normal file
51
Assets/_Game/Scripts/Player/States/RunState.cs
Normal file
@@ -0,0 +1,51 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace BaseGames.Player.States
|
||||
{
|
||||
/// <summary>跑步状态。播放 Run 动画并驱动水平移动。</summary>
|
||||
public class RunState : PlayerStateBase
|
||||
{
|
||||
public RunState(PlayerController owner) : base(owner) { }
|
||||
|
||||
public override void OnStateEnter()
|
||||
{
|
||||
if (AnimCfg?.Run != null)
|
||||
Anim.Play(AnimCfg.Run);
|
||||
// 落地时重置空中能力计数器(绝大多数情况被 IdleState 覆盖,但水平落地直接进入 RunState 时也需要)
|
||||
Owner.GetState<AerialDashState>()?.ResetAerialDashes();
|
||||
Owner.ResetAirJumps();
|
||||
}
|
||||
|
||||
public override void OnStateUpdate()
|
||||
{
|
||||
if (!Move.IsGrounded)
|
||||
{
|
||||
_owner.TransitionTo(_owner.GetState<FallState>());
|
||||
return;
|
||||
}
|
||||
if (Buffer.ConsumeJump())
|
||||
{
|
||||
_owner.TransitionTo(_owner.GetState<JumpState>());
|
||||
return;
|
||||
}
|
||||
// 地面冲刺:需解锁 Dash 能力,且冲刺不在冷却
|
||||
if (Buffer.ConsumeDash()
|
||||
&& Stats != null && Stats.HasAbility(AbilityType.Dash)
|
||||
&& Owner.GetState<DashState>() is { CanDash: true } dashState)
|
||||
{
|
||||
_owner.TransitionTo(dashState);
|
||||
return;
|
||||
}
|
||||
if (Mathf.Abs(Input.MoveInput.x) < 0.1f)
|
||||
{
|
||||
_owner.TransitionTo(_owner.GetState<IdleState>());
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
public override void OnStateFixedUpdate()
|
||||
{
|
||||
Move.Move(Input.MoveInput.x * Cfg.RunSpeed);
|
||||
}
|
||||
}
|
||||
}
|
||||
11
Assets/_Game/Scripts/Player/States/RunState.cs.meta
Normal file
11
Assets/_Game/Scripts/Player/States/RunState.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f695c81f3a6b0bc4eafc75125bbf47aa
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
55
Assets/_Game/Scripts/Player/States/SpringState.cs
Normal file
55
Assets/_Game/Scripts/Player/States/SpringState.cs
Normal file
@@ -0,0 +1,55 @@
|
||||
namespace BaseGames.Player.States
|
||||
{
|
||||
/// <summary>
|
||||
/// 使用灵泉(治疗)状态(架构 05_PlayerModule §2)。
|
||||
/// 消耗 SpringCharge,播放治愈动画;动画结束后回到 Idle。
|
||||
/// 需在地面且有充能才能进入(PlayerController 负责条件检查)。
|
||||
///
|
||||
/// 动画在 Overlay Layer(Layer 1)播放,叠加于 Base Layer 的 Idle/Run 之上,
|
||||
/// 符合架构 05_PlayerModule §14 双层动画设计。
|
||||
/// </summary>
|
||||
public class SpringState : PlayerStateBase
|
||||
{
|
||||
public SpringState(PlayerController owner) : base(owner) { }
|
||||
|
||||
public override void OnStateEnter()
|
||||
{
|
||||
// 消耗灵泉充能并治疗(PlayerStats.UseSpring 内部回复 HP)
|
||||
bool used = Stats?.UseSpring() ?? false;
|
||||
if (!used)
|
||||
{
|
||||
// 无充能时立即退出
|
||||
Owner.TransitionTo(Owner.GetState<IdleState>());
|
||||
return;
|
||||
}
|
||||
|
||||
// 停止移动
|
||||
Move?.ZeroHorizontalVelocity();
|
||||
|
||||
// 在 Overlay Layer(Layer 1)播放灵泉动画,叠加于 Base Layer 的 Idle 之上
|
||||
if (AnimCfg?.UseSpring != null)
|
||||
{
|
||||
var state = Owner.PlayOnOverlay(AnimCfg.UseSpring);
|
||||
if (state != null)
|
||||
{
|
||||
state.Events(this).OnEnd = OnSpringEnd;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 无动画则直接结束
|
||||
OnSpringEnd();
|
||||
}
|
||||
|
||||
public override void OnStateExit()
|
||||
{
|
||||
// 淡出叠加层,恢复纯 Base Layer 动画
|
||||
Owner.StopOverlay(fadeDuration: 0.1f);
|
||||
}
|
||||
|
||||
private void OnSpringEnd()
|
||||
{
|
||||
Owner.TransitionTo(Owner.GetState<IdleState>());
|
||||
}
|
||||
}
|
||||
}
|
||||
11
Assets/_Game/Scripts/Player/States/SpringState.cs.meta
Normal file
11
Assets/_Game/Scripts/Player/States/SpringState.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e0a4d3517cee2b24da050f85ffe13fdd
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
85
Assets/_Game/Scripts/Player/States/SwimState.cs
Normal file
85
Assets/_Game/Scripts/Player/States/SwimState.cs
Normal file
@@ -0,0 +1,85 @@
|
||||
// Assets/Scripts/Player/States/SwimState.cs
|
||||
// 游泳状态:玩家在液体中时使用(Architecture 21_LiquidPuzzleModule §5)
|
||||
// ⚠️ 遵循 PlayerStateBase 构造函数注入模式(非 MonoBehaviour)
|
||||
// ⚠️ 输入通过 Input.MoveInput / Input.JumpStartedEvent 访问(项目实际 API)
|
||||
using BaseGames.Player;
|
||||
using BaseGames.World.Liquid;
|
||||
using UnityEngine;
|
||||
|
||||
namespace BaseGames.Player.States
|
||||
{
|
||||
/// <summary>
|
||||
/// 游泳状态:玩家在液体中时使用。
|
||||
/// 需要 AbilityType.Swim 已解锁;若未解锁则由 WaterDangerState 触发溺水流程。
|
||||
/// 由 PlayerController 在收到 EVT_LiquidEntered 后切换进入。
|
||||
/// </summary>
|
||||
public class SwimState : PlayerStateBase
|
||||
{
|
||||
private LiquidPhysicsConfigSO _currentPhysics;
|
||||
private float _originalGravity;
|
||||
|
||||
public SwimState(PlayerController owner) : base(owner) { }
|
||||
|
||||
/// <summary>由 PlayerController 在切换状态前调用,注入当前液体区域的物理配置。</summary>
|
||||
public void SetPhysicsConfig(LiquidPhysicsConfigSO config) => _currentPhysics = config;
|
||||
|
||||
public override void OnStateEnter()
|
||||
{
|
||||
_originalGravity = Move.Rb.gravityScale;
|
||||
Move.SetGravityScale(_currentPhysics?.GravityScale ?? 0.3f);
|
||||
|
||||
if (AnimCfg?.SwimIdle != null)
|
||||
Anim?.Play(AnimCfg.SwimIdle);
|
||||
|
||||
Input.JumpStartedEvent += OnJumpStarted;
|
||||
}
|
||||
|
||||
public override void OnStateExit()
|
||||
{
|
||||
Input.JumpStartedEvent -= OnJumpStarted;
|
||||
Move.SetGravityScale(_originalGravity);
|
||||
}
|
||||
|
||||
public override void OnStateUpdate()
|
||||
{
|
||||
var input = Input.MoveInput;
|
||||
var maxSpeed = _currentPhysics?.MaxSwimSpeed ?? 4f;
|
||||
var accel = _currentPhysics?.SwimAcceleration ?? 8f;
|
||||
var drag = _currentPhysics?.DragCoefficient ?? 3f;
|
||||
var rb = Move.Rb;
|
||||
|
||||
if (input != Vector2.zero)
|
||||
{
|
||||
var targetVel = input * maxSpeed;
|
||||
rb.velocity = Vector2.MoveTowards(rb.velocity, targetVel, accel * Time.deltaTime);
|
||||
|
||||
if (AnimCfg?.SwimMove != null)
|
||||
Anim?.Play(AnimCfg.SwimMove);
|
||||
}
|
||||
else
|
||||
{
|
||||
// 水下浮力(持续向上的微弱力)
|
||||
rb.AddForce(Vector2.up * (_currentPhysics?.BuoyancyForce ?? 0.5f), ForceMode2D.Force);
|
||||
|
||||
if (AnimCfg?.SwimIdle != null)
|
||||
Anim?.Play(AnimCfg.SwimIdle);
|
||||
}
|
||||
|
||||
// 施加水阻
|
||||
rb.velocity *= 1f - drag * Time.deltaTime;
|
||||
}
|
||||
|
||||
public override PlayerStateBase GetNextState()
|
||||
{
|
||||
// 离开液体区域由 PlayerController 订阅 EVT_LiquidExited 后切换到 FallState
|
||||
return null;
|
||||
}
|
||||
|
||||
private void OnJumpStarted()
|
||||
{
|
||||
// 跳跃键 = 跃出水面冲量
|
||||
Move.Rb.AddForce(Vector2.up * (_currentPhysics?.SurfaceExitSpeed ?? 5f),
|
||||
ForceMode2D.Impulse);
|
||||
}
|
||||
}
|
||||
}
|
||||
11
Assets/_Game/Scripts/Player/States/SwimState.cs.meta
Normal file
11
Assets/_Game/Scripts/Player/States/SwimState.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 199e23e0415d2d04ebc19174f11f3231
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
47
Assets/_Game/Scripts/Player/States/UpAttackState.cs
Normal file
47
Assets/_Game/Scripts/Player/States/UpAttackState.cs
Normal file
@@ -0,0 +1,47 @@
|
||||
using BaseGames.Combat;
|
||||
|
||||
namespace BaseGames.Player.States
|
||||
{
|
||||
/// <summary>
|
||||
/// 上劈状态(架构 05_PlayerModule §2)。
|
||||
/// 激活 HitBoxUp;结束后回到 Idle(地面)或 FallState(空中)。
|
||||
/// </summary>
|
||||
public class UpAttackState : PlayerStateBase
|
||||
{
|
||||
public UpAttackState(PlayerController owner) : base(owner) { }
|
||||
|
||||
public override void OnStateEnter()
|
||||
{
|
||||
Owner.Combat?.EnableWeaponHitBox(AttackDirection.Up);
|
||||
|
||||
var clip = Owner.Weapon?.ActiveWeapon?.upAttackClip;
|
||||
if (clip != null && clip.Clip != null)
|
||||
{
|
||||
var state = Anim.Play(clip);
|
||||
state.Events(this).OnEnd = OnClipEnd;
|
||||
}
|
||||
else if (AnimCfg?.UpAttack != null)
|
||||
{
|
||||
var state = Anim.Play(AnimCfg.UpAttack);
|
||||
state.Events(this).OnEnd = OnClipEnd;
|
||||
}
|
||||
else
|
||||
{
|
||||
OnClipEnd();
|
||||
}
|
||||
}
|
||||
|
||||
public override void OnStateExit()
|
||||
{
|
||||
Owner.Combat?.DisableAllWeaponHitBoxes();
|
||||
}
|
||||
|
||||
private void OnClipEnd()
|
||||
{
|
||||
if (Move.IsGrounded)
|
||||
Owner.TransitionTo(Owner.GetState<IdleState>());
|
||||
else
|
||||
Owner.TransitionTo(Owner.GetState<FallState>());
|
||||
}
|
||||
}
|
||||
}
|
||||
11
Assets/_Game/Scripts/Player/States/UpAttackState.cs.meta
Normal file
11
Assets/_Game/Scripts/Player/States/UpAttackState.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 3917a3a7a101db249ab296e784afea24
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
58
Assets/_Game/Scripts/Player/States/WallJumpState.cs
Normal file
58
Assets/_Game/Scripts/Player/States/WallJumpState.cs
Normal file
@@ -0,0 +1,58 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace BaseGames.Player.States
|
||||
{
|
||||
/// <summary>
|
||||
/// 蹬墙跳状态(架构 05_PlayerModule §2)。
|
||||
/// 从 WallSlideState 进入;施加背墙方向的水平 + 垂直速度;
|
||||
/// 短暂锁定水平输入后转为 FallState。
|
||||
/// </summary>
|
||||
public class WallJumpState : PlayerStateBase
|
||||
{
|
||||
private float _inputLockTimer;
|
||||
private int _wallDir;
|
||||
|
||||
public WallJumpState(PlayerController owner) : base(owner) { }
|
||||
|
||||
public override void OnStateEnter()
|
||||
{
|
||||
// 记录墙壁方向(跳跃反向)
|
||||
_wallDir = Owner.WallDetector != null ? Owner.WallDetector.WallDirection : 0;
|
||||
if (_wallDir == 0) _wallDir = -Owner.FacingDirection;
|
||||
|
||||
// 施加蹬墙跳速度
|
||||
Move?.WallJump(_wallDir);
|
||||
|
||||
// 锁定水平输入
|
||||
_inputLockTimer = Cfg.WallJumpInputLockDuration;
|
||||
|
||||
// 播放跳跃动画(复用跳跃动画)
|
||||
if (AnimCfg?.Jump != null) Anim?.Play(AnimCfg.Jump);
|
||||
|
||||
Input.JumpCancelledEvent += OnJumpCancelled;
|
||||
}
|
||||
|
||||
public override void OnStateExit()
|
||||
{
|
||||
Input.JumpCancelledEvent -= OnJumpCancelled;
|
||||
}
|
||||
|
||||
public override void OnStateUpdate()
|
||||
{
|
||||
_inputLockTimer -= Time.deltaTime;
|
||||
|
||||
// 上升结束 → 下落
|
||||
if (!Move.IsRising)
|
||||
{
|
||||
Owner.TransitionTo(Owner.GetState<FallState>());
|
||||
return;
|
||||
}
|
||||
|
||||
// 输入锁结束后允许水平控制
|
||||
if (_inputLockTimer <= 0f && Mathf.Abs(Input.MoveInput.x) > 0.01f)
|
||||
Move?.Move(Input.MoveInput.x * Cfg.RunSpeed);
|
||||
}
|
||||
|
||||
private void OnJumpCancelled() => Move?.CutJump();
|
||||
}
|
||||
}
|
||||
11
Assets/_Game/Scripts/Player/States/WallJumpState.cs.meta
Normal file
11
Assets/_Game/Scripts/Player/States/WallJumpState.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 351b042e66111474d8b152878313dc92
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
55
Assets/_Game/Scripts/Player/States/WallSlideState.cs
Normal file
55
Assets/_Game/Scripts/Player/States/WallSlideState.cs
Normal file
@@ -0,0 +1,55 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace BaseGames.Player.States
|
||||
{
|
||||
/// <summary>
|
||||
/// 壁滑状态(架构 05_PlayerModule §2)。
|
||||
/// 需有 PlayerWallDetector.IsTouchingWall == true 才能进入;
|
||||
/// 限制下落速度为 WallSlideSpeed;按跳跃则切换到 WallJumpState。
|
||||
/// </summary>
|
||||
public class WallSlideState : PlayerStateBase
|
||||
{
|
||||
public WallSlideState(PlayerController owner) : base(owner) { }
|
||||
|
||||
public override void OnStateEnter()
|
||||
{
|
||||
if (AnimCfg?.WallSlide != null)
|
||||
Anim?.Play(AnimCfg.WallSlide);
|
||||
|
||||
Input.JumpStartedEvent += OnJumpPressed;
|
||||
}
|
||||
|
||||
public override void OnStateExit()
|
||||
{
|
||||
Input.JumpStartedEvent -= OnJumpPressed;
|
||||
}
|
||||
|
||||
public override void OnStateUpdate()
|
||||
{
|
||||
// 离开墙壁 → 下落
|
||||
if (Owner.WallDetector == null || !Owner.WallDetector.IsTouchingWall)
|
||||
{
|
||||
Owner.TransitionTo(Owner.GetState<FallState>());
|
||||
return;
|
||||
}
|
||||
|
||||
// 着地 → 闲置
|
||||
if (Move.IsGrounded)
|
||||
{
|
||||
Owner.TransitionTo(Owner.GetState<IdleState>());
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
public override void OnStateFixedUpdate()
|
||||
{
|
||||
// 限制下落速度(壁滑缓慢下落)
|
||||
Move?.ApplyWallSlide();
|
||||
}
|
||||
|
||||
private void OnJumpPressed()
|
||||
{
|
||||
Owner.TransitionTo(Owner.GetState<WallJumpState>());
|
||||
}
|
||||
}
|
||||
}
|
||||
11
Assets/_Game/Scripts/Player/States/WallSlideState.cs.meta
Normal file
11
Assets/_Game/Scripts/Player/States/WallSlideState.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7578dea22a1b02d44970bcac667b1401
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
69
Assets/_Game/Scripts/Player/WeaponHitBoxInstance.cs
Normal file
69
Assets/_Game/Scripts/Player/WeaponHitBoxInstance.cs
Normal file
@@ -0,0 +1,69 @@
|
||||
using UnityEngine;
|
||||
using BaseGames.Combat;
|
||||
|
||||
namespace BaseGames.Player
|
||||
{
|
||||
/// <summary>
|
||||
/// 武器 HitBox 实例。
|
||||
/// 由 WeaponManager 在切换武器时实例化到角色的 [WeaponSocket] 子节点下,
|
||||
/// 武器卸下(切换)时销毁。
|
||||
///
|
||||
/// Prefab 内部层级示例:
|
||||
/// [WPN_SkyBlade_HitBox]
|
||||
/// ├── [HitBox_Ground] ← BoxCollider2D, Layer = PlayerHitBox
|
||||
/// ├── [HitBox_Up]
|
||||
/// ├── [HitBox_Down]
|
||||
/// └── [HitBox_Air]
|
||||
///
|
||||
/// 命名规范:Assets/Prefabs/Weapons/WPN_{weaponId}_HitBox.prefab
|
||||
/// </summary>
|
||||
public class WeaponHitBoxInstance : MonoBehaviour
|
||||
{
|
||||
[SerializeField] private HitBox _hitBoxGround;
|
||||
[SerializeField] private HitBox _hitBoxUp;
|
||||
[SerializeField] private HitBox _hitBoxDown;
|
||||
[SerializeField] private HitBox _hitBoxAir;
|
||||
|
||||
/// <summary>下劈 HitBox 命中确认事件(供 PlayerCombat 转发给 DownAttackState pogo 逻辑)。</summary>
|
||||
public event System.Action<DamageInfo> OnDownHitConfirmed;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
if (_hitBoxDown != null)
|
||||
_hitBoxDown.OnHitConfirmed += info => OnDownHitConfirmed?.Invoke(info);
|
||||
}
|
||||
|
||||
// ── 公共 API ──────────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>激活指定方向的 HitBox。</summary>
|
||||
public void Activate(AttackDirection dir, DamageSourceSO source, Transform attacker)
|
||||
=> GetHitBox(dir)?.Activate(source, attacker);
|
||||
|
||||
/// <summary>停用指定方向的 HitBox。</summary>
|
||||
public void Deactivate(AttackDirection dir)
|
||||
=> GetHitBox(dir)?.Deactivate();
|
||||
|
||||
/// <summary>停用所有方向的 HitBox。</summary>
|
||||
public void DeactivateAll()
|
||||
{
|
||||
_hitBoxGround?.Deactivate();
|
||||
_hitBoxUp?.Deactivate();
|
||||
_hitBoxDown?.Deactivate();
|
||||
_hitBoxAir?.Deactivate();
|
||||
}
|
||||
|
||||
/// <summary>切换连击段伤害源(不改变激活状态,供 PlayerCombat.SetComboSegmentSource 调用)。</summary>
|
||||
public void SetDamageSource(AttackDirection dir, DamageSourceSO source)
|
||||
=> GetHitBox(dir)?.SetDamageSource(source);
|
||||
|
||||
/// <summary>按方向查询对应 HitBox 组件。</summary>
|
||||
public HitBox GetHitBox(AttackDirection dir) => dir switch
|
||||
{
|
||||
AttackDirection.Ground => _hitBoxGround,
|
||||
AttackDirection.Up => _hitBoxUp,
|
||||
AttackDirection.Down => _hitBoxDown,
|
||||
AttackDirection.Air => _hitBoxAir,
|
||||
_ => null,
|
||||
};
|
||||
}
|
||||
}
|
||||
11
Assets/_Game/Scripts/Player/WeaponHitBoxInstance.cs.meta
Normal file
11
Assets/_Game/Scripts/Player/WeaponHitBoxInstance.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ec12dacf2519f58429dd3c59da8f93b0
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
125
Assets/_Game/Scripts/Player/WeaponManager.cs
Normal file
125
Assets/_Game/Scripts/Player/WeaponManager.cs
Normal file
@@ -0,0 +1,125 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
namespace BaseGames.Player
|
||||
{
|
||||
/// <summary>
|
||||
/// 武器管理器。
|
||||
/// 监听 FormController.OnFormChanged,依据当前形态切换 ActiveWeapon。
|
||||
/// 支持护符 Override:护符调用 SetOverride() 临时替换特定形态的武器。
|
||||
/// 架构 05_PlayerModule §7。
|
||||
/// </summary>
|
||||
public class WeaponManager : MonoBehaviour
|
||||
{
|
||||
[SerializeField] private FormController _formController;
|
||||
[SerializeField] private WeaponSO _startingWeapon; // 无 FormController 时的回退武器
|
||||
|
||||
[Header("HitBox 挂载点")]
|
||||
[Tooltip("武器 HitBox Prefab 实例化的父节点,应为 Player 层级下的 [WeaponSocket] 子节点。")]
|
||||
[SerializeField] private Transform _weaponSocket;
|
||||
|
||||
public WeaponSO ActiveWeapon { get; private set; }
|
||||
public WeaponHitBoxInstance ActiveHitBoxInstance { get; private set; }
|
||||
|
||||
public event Action<WeaponSO> OnWeaponChanged;
|
||||
|
||||
// 护符注入的武器覆盖:Key = FormSO.formId,Value = 替换武器(架构 05 §7)
|
||||
private readonly Dictionary<string, WeaponSO> _overrides = new();
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
if (_formController != null && _formController.CurrentForm != null)
|
||||
ApplyWeapon(_formController.CurrentForm);
|
||||
else if (_startingWeapon != null)
|
||||
SetDirectWeapon(_startingWeapon);
|
||||
}
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
if (_formController != null)
|
||||
_formController.OnFormChanged += HandleFormChanged;
|
||||
}
|
||||
|
||||
private void OnDisable()
|
||||
{
|
||||
if (_formController != null)
|
||||
_formController.OnFormChanged -= HandleFormChanged;
|
||||
}
|
||||
|
||||
private void Start()
|
||||
{
|
||||
// 若 FormController 在 Awake 时 CurrentForm 尚未初始化,在 Start 重试
|
||||
if (ActiveWeapon == null && _formController != null && _formController.CurrentForm != null)
|
||||
ApplyWeapon(_formController.CurrentForm);
|
||||
else if (ActiveWeapon == null && _startingWeapon != null)
|
||||
SetDirectWeapon(_startingWeapon);
|
||||
}
|
||||
|
||||
// ── 内部切换 ───────────────────────────────────────────────────────────
|
||||
|
||||
private void HandleFormChanged() => ApplyWeapon(_formController.CurrentForm);
|
||||
|
||||
private void ApplyWeapon(FormSO form)
|
||||
{
|
||||
if (form == null) return;
|
||||
WeaponSO next = _overrides.TryGetValue(form.formId, out var ov) ? ov : form.defaultWeapon;
|
||||
if (next == ActiveWeapon) return;
|
||||
SetDirectWeapon(next);
|
||||
}
|
||||
|
||||
private void SetDirectWeapon(WeaponSO weapon)
|
||||
{
|
||||
var oldInstance = ActiveHitBoxInstance;
|
||||
|
||||
ActiveWeapon = weapon;
|
||||
ActiveHitBoxInstance = null;
|
||||
|
||||
if (weapon?.hitBoxPrefab != null && _weaponSocket != null)
|
||||
{
|
||||
var go = Instantiate(weapon.hitBoxPrefab, _weaponSocket);
|
||||
ActiveHitBoxInstance = go.GetComponent<WeaponHitBoxInstance>();
|
||||
}
|
||||
|
||||
// 先通知订阅者(使其有机会取消对旧实例的事件订阅),再销毁旧实例
|
||||
OnWeaponChanged?.Invoke(weapon);
|
||||
|
||||
if (oldInstance != null)
|
||||
Destroy(oldInstance.gameObject);
|
||||
}
|
||||
|
||||
// ── 护符 Override API(由 WeaponOverrideEffect 调用,架构 05 §7)──────
|
||||
|
||||
/// <summary>为指定形态设置武器覆盖。formId 为空 = 覆盖所有形态。</summary>
|
||||
public void SetOverride(string formId, WeaponSO weapon)
|
||||
{
|
||||
if (string.IsNullOrEmpty(formId))
|
||||
{
|
||||
if (_formController != null)
|
||||
foreach (var f in _formController.AllForms)
|
||||
_overrides[f.formId] = weapon;
|
||||
}
|
||||
else
|
||||
{
|
||||
_overrides[formId] = weapon;
|
||||
}
|
||||
if (_formController?.CurrentForm != null)
|
||||
ApplyWeapon(_formController.CurrentForm);
|
||||
}
|
||||
|
||||
/// <summary>移除覆盖,恢复默认武器。formId 为空 = 移除所有形态覆盖。</summary>
|
||||
public void ClearOverride(string formId)
|
||||
{
|
||||
if (string.IsNullOrEmpty(formId))
|
||||
{
|
||||
_overrides.Clear();
|
||||
}
|
||||
else
|
||||
{
|
||||
_overrides.Remove(formId);
|
||||
}
|
||||
if (_formController?.CurrentForm != null)
|
||||
ApplyWeapon(_formController.CurrentForm);
|
||||
}
|
||||
}
|
||||
}
|
||||
11
Assets/_Game/Scripts/Player/WeaponManager.cs.meta
Normal file
11
Assets/_Game/Scripts/Player/WeaponManager.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 6620d87234b5a9b4c811905861cd32fd
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
91
Assets/_Game/Scripts/Player/WeaponSO.cs
Normal file
91
Assets/_Game/Scripts/Player/WeaponSO.cs
Normal file
@@ -0,0 +1,91 @@
|
||||
using System;
|
||||
using UnityEngine;
|
||||
using Animancer;
|
||||
using BaseGames.Combat;
|
||||
using BaseGames.Feedback;
|
||||
|
||||
namespace BaseGames.Player
|
||||
{
|
||||
/// <summary>
|
||||
/// 武器数据 SO(纯数据,不含 Prefab 引用)。
|
||||
/// 每个攻击方向对应一个 DamageSourceSO 和 ClipTransition。
|
||||
/// HitBox 直接挂载在 Player Prefab 上(架构 05_PlayerModule §7)。
|
||||
/// </summary>
|
||||
[CreateAssetMenu(menuName = "BaseGames/Player/Weapon")]
|
||||
public class WeaponSO : ScriptableObject
|
||||
{
|
||||
[Header("基础信息")]
|
||||
public string weaponId; // 全局唯一 ID,如 "Weapon_SkyBlade"
|
||||
public string displayName; // 显示名,如 "天裂刃"
|
||||
public Sprite icon;
|
||||
public WeaponType weaponType;
|
||||
|
||||
[Header("连击动画(Animancer ClipTransition)")]
|
||||
public ClipTransition attack1Clip;
|
||||
public ClipTransition attack2Clip;
|
||||
public ClipTransition attack3Clip;
|
||||
public ClipTransition airAttackClip;
|
||||
public ClipTransition upAttackClip;
|
||||
public ClipTransition downAttackClip;
|
||||
|
||||
[Header("伤害来源(每段独立 DamageSourceSO)")]
|
||||
public DamageSourceSO attack1Source;
|
||||
public DamageSourceSO attack2Source;
|
||||
public DamageSourceSO attack3Source;
|
||||
public DamageSourceSO airAttackSource;
|
||||
public DamageSourceSO upAttackSource;
|
||||
public DamageSourceSO downAttackSource;
|
||||
|
||||
[Header("HitBox Prefab")]
|
||||
[Tooltip("武器专属 HitBox Prefab,内含 WeaponHitBoxInstance。\nWeaponManager 在切换武器时实例化于 [WeaponSocket] 下,武器卸下时销毁。\n命名规范:Assets/Prefabs/Weapons/WPN_{weaponId}_HitBox.prefab")]
|
||||
public GameObject hitBoxPrefab;
|
||||
|
||||
[Header("武器特效")]
|
||||
public WeaponVFXConfig vfxConfig;
|
||||
|
||||
[Header("战斗参数")]
|
||||
[Tooltip("命中确认时增加的灵力值(覆盖 PlayerCombat 默认值 10)")]
|
||||
[Min(0)]
|
||||
public int soulPowerGain = 10;
|
||||
|
||||
// ── 方向查询 ──────────────────────────────────────────────────────────
|
||||
public DamageSourceSO GetSourceByDir(AttackDirection dir) => dir switch
|
||||
{
|
||||
AttackDirection.Ground => attack1Source,
|
||||
AttackDirection.Up => upAttackSource,
|
||||
AttackDirection.Down => downAttackSource,
|
||||
AttackDirection.Air => airAttackSource,
|
||||
_ => attack1Source,
|
||||
};
|
||||
|
||||
public ClipTransition GetClipByCombo(int comboIndex) => comboIndex switch
|
||||
{
|
||||
0 => attack1Clip,
|
||||
1 => attack2Clip,
|
||||
2 => attack3Clip,
|
||||
_ => attack1Clip,
|
||||
};
|
||||
}
|
||||
|
||||
// ── 武器类型枚举(架构 05 §7)──────────────────────────────────────────────
|
||||
public enum WeaponType
|
||||
{
|
||||
SkyBlade, // 天魂:裂空刃(高频轻击)
|
||||
EarthHammer, // 地魂:地震锤(低频重击,范围大)
|
||||
LifeScythe, // 命魂:命镰(穿透,直线斩)
|
||||
Custom, // 护符替换或未来扩展武器
|
||||
}
|
||||
|
||||
// ── 武器特效配置([Serializable] 内嵌,架构 05 §7)────────────────────────
|
||||
[Serializable]
|
||||
public class WeaponVFXConfig
|
||||
{
|
||||
[Tooltip("切换到此武器时播放的特效预设 ID(对应 IFeedbackPlayer.TriggerPreset)")]
|
||||
public string onEquipPresetId;
|
||||
|
||||
[Tooltip("武器挥斩拖尾 Prefab(null = 不显示拖尾)")]
|
||||
public GameObject weaponTrailPrefab;
|
||||
|
||||
public Color trailColor = Color.white;
|
||||
}
|
||||
}
|
||||
11
Assets/_Game/Scripts/Player/WeaponSO.cs.meta
Normal file
11
Assets/_Game/Scripts/Player/WeaponSO.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d2443d04d1c179d4d8a4f36e7ca7156e
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Reference in New Issue
Block a user