多轮审查和修复

This commit is contained in:
2026-05-12 15:34:08 +08:00
parent f55d2a57c3
commit ebbbb7332e
805 changed files with 838724 additions and 1905 deletions

View File

@@ -4,11 +4,31 @@ namespace BaseGames.Player
{
/// <summary>
/// 形态切换配置 ScriptableObject。
/// Phase 1 骨架 — Phase 2 FormController 实现三形态切换时补充字段
/// 单一资产forms[0..2] = Sky / Earth / Death架构 05 §18
/// ⚠️ 只有 1 个 FormConfigSO 资产FormSO × 3 存于 forms[] 数组中,不分开建立。
/// </summary>
[CreateAssetMenu(menuName = "Player/FormConfig")]
public class FormConfigSO : ScriptableObject
{
// Phase 2: 填入 Normal / Soul / Spirit 三形态参数
[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;
}
}
}

View File

@@ -1,7 +1,71 @@
using System;
using UnityEngine;
using BaseGames.Core.Events;
namespace BaseGames.Player
{
/// <summary>形态控制器。Phase 1 桩 — Phase 2 实现。</summary>
public class FormController : MonoBehaviour { }
/// <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);
}
}
}

View 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 = "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 枚举为准)
}
}

View File

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

View File

@@ -0,0 +1,13 @@
namespace BaseGames.Player
{
/// <summary>
/// 玩家形态枚举。对应三形态切换系统(天魂/地魂/命魂)。
/// ApplyPalette(FormType) 以此为参数(架构 18_VFXFeedbackModule §10
/// </summary>
public enum FormType
{
Sky = 0, // 天魂形态:裂空刃,高频轻击
Earth = 1, // 地魂形态:地震锤,低频重击
Death = 2, // 命魂形态:命镰,穿透直线斩
}
}

View File

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

View File

@@ -18,6 +18,8 @@ namespace BaseGames.Player
[Header("受伤 / 死亡")]
public AnimationClip Hurt;
public AnimationClip Dead;
[Tooltip("受击硬直最短持续时间(秒),动画早于此时间结束也不会提前退出)")]
public float HurtDuration = 0.4f;
[Header("弹簧")]
public AnimationClip UseSpring;
@@ -25,6 +27,20 @@ namespace BaseGames.Player
[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;
@@ -34,6 +50,10 @@ namespace BaseGames.Player
public AnimationClip ParryStart;
public AnimationClip ParrySuccess;
[Header("游泳")]
public AnimationClip SwimIdle;
public AnimationClip SwimMove;
/// <summary>按连招步骤取地面攻击动画,越界自动取最后一个。</summary>
public AnimationClip GetAttackClip(int step)
{

View File

@@ -4,7 +4,7 @@ using BaseGames.Combat;
namespace BaseGames.Player
{
/// <summary>
/// 玩家战斗组件Phase 1 实现)
/// 玩家战斗组件。
/// 架构 05_PlayerModule §5HitBox 直接挂在 Player Prefab 子节点上,不经过 WeaponInstance。
/// 节点:[HitBoxGround]、[HitBoxUp]、[HitBoxDown]、[HitBoxAir]。
/// </summary>
@@ -18,12 +18,53 @@ namespace BaseGames.Player
[SerializeField] private HitBox _hitBoxDown;
[SerializeField] private HitBox _hitBoxAir;
// ── HitBox 激活(由 AttackState / Animancer 帧事件调用)─────────────
private PlayerStats _stats;
private void Awake()
{
_stats = GetComponentInParent<PlayerStats>();
}
private void OnEnable()
{
if (_weaponManager != null)
_weaponManager.OnWeaponChanged += RefreshWeaponData;
}
private void OnDisable()
{
if (_weaponManager != null)
_weaponManager.OnWeaponChanged -= RefreshWeaponData;
}
private void RefreshWeaponData(WeaponSO weapon)
{
// 武器切换时可在此更新 HitBox 默认尺寸等
}
// ── 连击段伤害来源切换 ────────────────────────────────────────────────
/// <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,
};
_hitBoxGround?.SetDamageSource(src);
}
// ── HitBox 激活(由 State / AnimationEvent 调用)─────────────────────
public void EnableWeaponHitBox(AttackDirection dir)
{
var source = _weaponManager != null
? _weaponManager.ActiveWeapon?.GetSourceByDir(dir)
: null;
var source = _weaponManager?.ActiveWeapon?.GetSourceByDir(dir);
GetHitBox(dir)?.Activate(source, transform);
}
@@ -38,8 +79,12 @@ namespace BaseGames.Player
_hitBoxAir?.Deactivate();
}
/// <summary>命中回调Phase 2 §2.3 补全:增加灵力)。</summary>
internal void OnHitConfirmed(DamageInfo info) { /* Phase 2增加灵力 */ }
/// <summary>命中确认回调:增加灵力(由 HurtBox.ReceiveDamage 步骤 7 的 HitConfirmed 事件订阅)。</summary>
public void OnHitConfirmed(DamageInfo info)
{
int gain = _weaponManager?.ActiveWeapon?.soulPowerGain ?? 10;
_stats?.AddSoulPower(gain);
}
private HitBox GetHitBox(AttackDirection dir) => dir switch
{

View File

@@ -4,7 +4,7 @@ namespace BaseGames.Player
{
/// <summary>
/// 玩家物理移动组件。封装 Rigidbody2D 操作,提供跑动、跳跃、击退等接口。
/// Phase 2 功能(冲刺、单向平台)在此留存桩方法。
/// </summary>
[RequireComponent(typeof(Rigidbody2D))]
public class PlayerMovement : MonoBehaviour
@@ -17,6 +17,9 @@ namespace BaseGames.Player
[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;
@@ -27,6 +30,7 @@ namespace BaseGames.Player
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;
@@ -37,8 +41,16 @@ namespace BaseGames.Player
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() => _rb = GetComponent<Rigidbody2D>();
private void Awake()
{
Debug.Assert(_config != null, "[PlayerMovement] _config 未赋值,请在 Inspector 中指定 PlayerMovementConfigSO。", this);
_rb = GetComponent<Rigidbody2D>();
if (_spriteRenderer == null)
_spriteRenderer = GetComponentInChildren<SpriteRenderer>();
}
private void FixedUpdate()
{
@@ -46,7 +58,7 @@ namespace BaseGames.Player
CheckWalls();
if (_isGrounded)
_coyoteTimer = _config != null ? _config.CoyoteTime : 0.12f;
_coyoteTimer = _config.CoyoteTime;
else
_coyoteTimer = Mathf.Max(0f, _coyoteTimer - Time.fixedDeltaTime);
}
@@ -54,7 +66,6 @@ namespace BaseGames.Player
// ── 移动 ──────────────────────────────────────────────────────────────
public void Move(float speedX)
{
if (_config == null) return;
float target = speedX;
float current = _rb.velocity.x;
float accel = Mathf.Abs(speedX) > 0.01f ? _config.Acceleration : _config.Deceleration;
@@ -65,8 +76,7 @@ namespace BaseGames.Player
// ── 跳跃 ──────────────────────────────────────────────────────────────
public void Jump(bool isVariable = true)
{
float force = _config != null ? _config.JumpForce : 18f;
_rb.velocity = new Vector2(_rb.velocity.x, force);
_rb.velocity = new Vector2(_rb.velocity.x, _config.JumpForce);
_coyoteTimer = 0f;
}
@@ -95,18 +105,48 @@ namespace BaseGames.Player
int dir = vx > 0f ? 1 : -1;
if (dir == _facingDirection) return;
_facingDirection = dir;
Vector3 s = transform.localScale;
transform.localScale = new Vector3(Mathf.Abs(s.x) * dir, s.y, s.z);
if (_spriteRenderer != null)
_spriteRenderer.flipX = dir < 0;
}
// ── 取消窗口 ──────────────────────────────────────────────────────────
public void SetCancelWindowOpen(bool open) => _cancelWindowOpen = open;
// ── Phase 2 桩 ────────────────────────────────────────────────────────
/// <summary>Phase 2 实现冲刺。</summary>
public void Dash(Vector2 direction, float speed) { }
// ── 冲刺 ──────────────────────────────────────────────────────────────
/// <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>Phase 2 实现单向平台穿透。</summary>
/// <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 检测 ──────────────────────────────────────────────────────
@@ -117,15 +157,14 @@ namespace BaseGames.Player
? (Vector2)_groundCheck.position
: (Vector2)transform.position + Vector2.down * 0.5f;
_isGrounded = Physics2D.OverlapBox(origin, _groundCheckSize, 0f, _groundLayer);
_isGrounded = Physics2D.OverlapBoxNonAlloc(origin, _groundCheckSize, 0f, _groundBuffer, _groundLayer) > 0;
if (_isGrounded && !wasGrounded)
_coyoteTimer = _config != null ? _config.CoyoteTime : 0.12f;
_coyoteTimer = _config.CoyoteTime;
}
private void CheckWalls()
{
if (_config == null) return;
float len = _config.WallRayLength;
float offY = _config.WallRayOffsetY;
Vector2 pos = (Vector2)transform.position + Vector2.up * offY;

View File

@@ -34,5 +34,8 @@ namespace BaseGames.Player
public float WallJumpAwayForceX = 10f;
public float WallJumpAwayForceY = 18f;
public float WallJumpInputLockDuration = 0.15f;
[Header("重力")]
public float DefaultGravityScale = 3f;
}
}

View File

@@ -1,4 +1,6 @@
using System.Collections.Generic;
using UnityEngine;
using BaseGames.Core;
using BaseGames.Core.Events;
using BaseGames.Core.Save;
@@ -21,6 +23,7 @@ namespace BaseGames.Player
[SerializeField] private IntEventChannelSO _onGeoChanged;
[SerializeField] private AbilityTypeEventChannelSO _onAbilityUnlocked;
[SerializeField] private VoidEventChannelSO _onPlayerDied;
[SerializeField] private DifficultyChangedEventChannel _onDifficultyChanged;
// ── 运行时数值 ─────────────────────────────────────────────────────────
public int CurrentHP { get; private set; }
@@ -40,14 +43,24 @@ namespace BaseGames.Player
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()
{
if (_config == null)
{
Debug.LogWarning("[PlayerStats] PlayerStatsSO not assigned.", this);
return;
}
Debug.Assert(_config != null, "[PlayerStats] _config 未赋值,请在 Inspector 中指定 PlayerStatsSO。", this);
MaxHP = _config.MaxHP;
CurrentHP = MaxHP;
MaxSoulPower = _config.MaxSoulPower;
@@ -57,6 +70,21 @@ namespace BaseGames.Player
CurrentGeo = _config.InitialGeo;
}
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;
@@ -64,7 +92,7 @@ namespace BaseGames.Player
if (_invincibleTimer > 0f)
_invincibleTimer -= dt;
if (_config != null && _config.SpiritRegenRate > 0)
if (_config.SpiritRegenRate > 0)
{
_spiritRegenTimer += dt;
if (_spiritRegenTimer >= 1f)
@@ -74,13 +102,62 @@ namespace BaseGames.Player
}
}
}
// ── 护符修改器 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 (IsInvincible || !IsAlive || amount <= 0) return;
if (_isGodMode || IsInvincible || !IsAlive || amount <= 0) return;
CurrentHP = Mathf.Max(0, CurrentHP - amount);
_onHPChanged?.Raise(CurrentHP);
OnDamaged?.Invoke();
if (CurrentHP == 0)
_onPlayerDied?.Raise();
}
@@ -119,6 +196,9 @@ namespace BaseGames.Player
_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;
@@ -149,8 +229,7 @@ namespace BaseGames.Player
if (CurrentSpringCharges <= 0) return false;
CurrentSpringCharges--;
_onSpringChargesChanged?.Raise(CurrentSpringCharges);
if (_config != null)
HealHP(_config.SpringHealAmount);
HealHP(_config.SpringHealAmount);
return true;
}
@@ -163,7 +242,6 @@ namespace BaseGames.Player
public void AddKillPoints(int points = 1)
{
if (_config == null) return;
SpringKillPoints += points;
if (SpringKillPoints >= _config.SpringKillThreshold)
{
@@ -191,7 +269,7 @@ namespace BaseGames.Player
// ── Invincibility ─────────────────────────────────────────────────────
public void BeginInvincibility(float duration = -1f)
{
float d = duration >= 0f ? duration : (_config != null ? _config.InvincibilityDuration : 0.6f);
float d = duration >= 0f ? duration : _config.InvincibilityDuration;
_invincibleTimer = Mathf.Max(_invincibleTimer, d);
}

View 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);
}
}
}

View File

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

View File

@@ -1,7 +1,2 @@
using UnityEngine;
namespace BaseGames.Player
{
/// <summary>技能管理器。Phase 1 桩 — Phase 2 实现。</summary>
public class SkillManager : MonoBehaviour { }
}
// SkillManager 定义位于 Assets/Scripts/Skills/SkillManager.cs (BaseGames.Skills)。
namespace BaseGames.Player { }

View File

@@ -2,6 +2,13 @@ using UnityEngine;
namespace BaseGames.Player
{
/// <summary>治愈弹簧系统。Phase 1 桩 — Phase 1 Week 3 实现。</summary>
/// <summary>
/// 治愈弹簧系统(架构 05_PlayerModule §7
/// PlayerStats 中已预留 CurrentSpringCharges / MaxSpringCharges / SpringKillPoints 字段。
/// TODO: 实现弹簧充能逻辑:
/// - 击杀敌人时调用 PlayerStats.AddSpringKillPoint() 积累充能
/// - 按下治愈键时消耗充能槽并恢复玩家 HP
/// - 充能满格时触发特殊强化状态(视设计而定)
/// </summary>
public class SpringSystem : MonoBehaviour { }
}

View File

@@ -0,0 +1,16 @@
namespace BaseGames.Player
{
/// <summary>
/// 玩家属性类型(护符效果 StatModifierEffect 使用)。
/// 存放于 BaseGames.Player 程序集,供 PlayerStats 与 Equipment 共同引用。
/// </summary>
public enum StatType
{
MaxHP,
AttackDamage,
MoveSpeed,
JumpHeight,
SoulGain,
Defense
}
}

View File

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

View File

@@ -0,0 +1,72 @@
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;
public AerialDashState(PlayerController owner) : base(owner)
{
_aerialDashesLeft = 1;
}
public override void OnStateEnter()
{
_aerialDashesLeft = Mathf.Max(0, _aerialDashesLeft - 1);
_facingDir = Owner.FacingDirection;
_timer = Cfg.DashDuration;
// 无敌帧
Stats?.BeginInvincibility(Cfg.DashDuration + 0.05f);
// 关闭重力,施加冲刺速度(空中冲刺不改变垂直速度)
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;
}
}
}

View File

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

View 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>());
}
}
}

View File

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

View File

@@ -39,23 +39,27 @@ namespace BaseGames.Player.States
var events = state.Events(this);
events.OnEnd = OnClipEnd;
// HitBox 由 Animancer 归一化时间事件驱动
events.Add(0.3f,
() => Owner.Combat?.EnableWeaponHitBox(AttackDirection.Ground));
events.Add(0.6f,
() => Owner.Combat?.DisableAllWeaponHitBoxes());
// 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()
{
Input.AttackEvent -= OnAttackInput;
Owner.Combat?.DisableAllWeaponHitBoxes();
Owner.TryTransitionState(Owner.IdleState);
Owner.TransitionTo(Owner.GetState<IdleState>());
}
private void OnAttackInput()
{
if (_comboIndex < 2)
// 连击段数从配置 SO 动态读取,无须修改代码即可支持任意段数连击
int maxCombo = AnimCfg?.GroundAttacks?.Length ?? 1;
if (_comboIndex < maxCombo - 1)
{
_comboIndex++;
PlayAttackClip();

View File

@@ -14,7 +14,9 @@
"BaseGames.Combat",
"BaseGames.Parry",
"BaseGames.Feedback",
"Kybernetik.Animancer"
"Kybernetik.Animancer",
"BaseGames.World",
"BaseGames.Skills"
],
"autoReferenced": true,
"overrideReferences": false,

View File

@@ -0,0 +1,77 @@
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;
private int _facingDir;
public bool CanDash => _cooldownTimer <= 0f;
/// <summary>冲刺状态期间应视为无敌PlayerController 据此跳过受击硬直。</summary>
public override bool IsInvincible => true;
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;
// 无敌帧
Stats?.BeginInvincibility(Cfg.DashDuration + 0.05f);
// 关闭重力,施加冲刺速度
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;
}
}
}

View File

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

View 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() { }
}
}

View File

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

View File

@@ -0,0 +1,70 @@
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;
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;
Owner.Combat?.DisableAllWeaponHitBoxes();
}
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>());
}
}
}

View File

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

View File

@@ -18,7 +18,7 @@ namespace BaseGames.Player.States
// 郊狼时间跳跃
if (Buffer.ConsumeJump() && Move.HasCoyoteTime)
{
_owner.TransitionTo(_owner.JumpState);
_owner.TransitionTo(_owner.GetState<JumpState>());
return;
}
@@ -27,21 +27,21 @@ namespace BaseGames.Player.States
{
Move.ZeroVelocity();
if (Mathf.Abs(Input.MoveInput.x) > 0.1f)
_owner.TransitionTo(_owner.RunState);
_owner.TransitionTo(_owner.GetState<RunState>());
else
_owner.TransitionTo(_owner.IdleState);
_owner.TransitionTo(_owner.GetState<IdleState>());
return;
}
// 空中水平移动
if (Mathf.Abs(Input.MoveInput.x) > 0.01f)
Move.Move(Input.MoveInput.x * (Cfg != null ? Cfg.RunSpeed : 7f));
Move.Move(Input.MoveInput.x * Cfg.RunSpeed);
}
public override void OnStateFixedUpdate()
{
// 增强下落重力
if (Cfg != null && Move.Rb.velocity.y < 0f)
if (Move.Rb.velocity.y < 0f)
{
float extraGrav = Physics2D.gravity.y * (Cfg.FallGravityMult - 1f) * Time.fixedDeltaTime;
Move.Rb.velocity = new Vector2(

View 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>());
}
}
}

View File

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

View File

@@ -12,23 +12,25 @@ namespace BaseGames.Player.States
if (AnimCfg?.Idle != null)
Anim.Play(AnimCfg.Idle);
Move?.ZeroHorizontalVelocity();
// 着地时重置空中冲刺次数
Owner.GetState<AerialDashState>()?.ResetAerialDashes();
}
public override void OnStateUpdate()
{
if (!Move.IsGrounded)
{
_owner.TransitionTo(_owner.FallState);
_owner.TransitionTo(_owner.GetState<FallState>());
return;
}
if (Buffer.ConsumeJump())
{
_owner.TransitionTo(_owner.JumpState);
_owner.TransitionTo(_owner.GetState<JumpState>());
return;
}
if (Mathf.Abs(Input.MoveInput.x) > 0.1f)
{
_owner.TransitionTo(_owner.RunState);
_owner.TransitionTo(_owner.GetState<RunState>());
}
}
}

View File

@@ -20,12 +20,12 @@ namespace BaseGames.Player.States
// 上升结束时转为下落
if (Move.Rb.velocity.y <= 0f)
{
_owner.TransitionTo(_owner.FallState);
_owner.TransitionTo(_owner.GetState<FallState>());
return;
}
// 水平移动
if (Mathf.Abs(Input.MoveInput.x) > 0.01f)
Move.Move(Input.MoveInput.x * (Cfg != null ? Cfg.RunSpeed : 7f));
Move.Move(Input.MoveInput.x * Cfg.RunSpeed);
}
public override void OnStateExit()

View File

@@ -0,0 +1,46 @@
namespace BaseGames.Player.States
{
/// <summary>
/// 弹反预备状态(架构 05_PlayerModule §2完整实现在 Week 6
/// 开启 ParrySystem 弹反窗口,播放 ParryStart 动画;
/// 成功弹反后 ParrySystem.ConsumeParry() 返回 trueHurtBox 不处理该次伤害。
/// </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>());
}
}
}

View File

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

View File

@@ -1,112 +1,219 @@
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/ 程序集,以便引用所有具体状态类型。
/// 实现 IDamageableIsInvincible/Defense 委托 PlayerStatsTakeDamage 委托 _stats
/// 依赖注入:所有子系统通过 [SerializeField] 字段在 Inspector 中绑定。
/// 实现 IDamageable + IPoiseSource架构 06_CombatModule §13
/// 依赖注入:同节点组件由 RequireComponent + Awake 自动获取;跨节点引用通过 [SerializeField] 绑定。
/// </summary>
[DefaultExecutionOrder(-100)]
[RequireComponent(typeof(InputBuffer))]
public class PlayerController : MonoBehaviour, IDamageable
[RequireComponent(typeof(PlayerMovement))]
[RequireComponent(typeof(PlayerStats))]
[RequireComponent(typeof(AnimancerComponent))]
public class PlayerController : MonoBehaviour, IDamageable, IPoiseSource
{
// ── 移动 & 数值 ───────────────────────────────────────────────────────
[Header("核心组件")]
[SerializeField] private PlayerMovement _movement;
[SerializeField] private PlayerStats _stats;
[SerializeField] private AnimancerComponent _animancer;
// ── 同节点组件(由 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 PlayerStatsSO _statsConfig; // 数值基准HP/弹簧等初始化用)
[SerializeField] private FormConfigSO _formConfig; // Phase 2三形态切换参数
[SerializeField] private InputReaderSO _inputReader;
[SerializeField] private PlayerStatsSO _statsConfig;
[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 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;
[SerializeField] private IntEventChannelSO _onHPChanged;
[SerializeField] private IntEventChannelSO _onHPChanged; /// <summary>
/// Start() 时广播玩家 TransformEnemyBase / ProjectileManager 等订阅此频道)。
/// 替代每个敌人在 Awake 中独立 FindWithTag 的 O(n) 全场景扫描。
/// </summary>
[SerializeField] private TransformEventChannelSO _onPlayerSpawned;
// ── 运行时 ────────────────────────────────────────────────────────────
private InputBuffer _inputBuffer;
private bool _missingDependencyLogged;
private bool _dependenciesReady;
#if UNITY_EDITOR
[Header("调试")]
[SerializeField] private bool _debugValidateTransitions = true;
#endif
// Overlay LayerLayer 1供 SpringState / SoulSkill 等叠加层动画使用
// Base LayerLayer 0移动/攻击/受伤/死亡等全身状态动画
private AnimancerLayer _overlayLayer;
// ── 状态实例 ──────────────────────────────────────────────────────────
private PlayerStateBase _currentState;
private IdleState _idleState;
private RunState _runState;
private JumpState _jumpState;
private FallState _fallState;
private AttackState _attackState;
private readonly System.Collections.Generic.Dictionary<System.Type, PlayerStateBase> _states = new();
// ── IDamageable 实现 ──────────────────────────────────────────────────
public bool IsInvincible => _stats != null && _stats.IsInvincible;
public int Defense => 0; // Phase 2从 PlayerStatsSO 读取
public int Defense => 0;
public void TakeDamage(DamageInfo info)
{
if (_stats == null) return;
_stats.TakeDamage(info.FinalDamage);
// Phase 2若非 DashState切换 HurtState
// 当前状态标记为无敌(如冲刺)则跳过受击硬直
if (_currentState?.IsInvincible == true) return;
if (_stats.IsAlive)
{
GetState<HurtState>()?.Initialize(info);
TransitionTo(GetState<HurtState>());
}
else
{
TransitionTo(GetState<DeadState>());
_onPlayerDied?.Raise();
}
}
// ── 公开属性(供状态类访问)──────────────────────────────────────────
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;
// ── IPoiseSource 实现(架构 06_CombatModule §13─────────────────────
/// <summary>
/// 玩家不拥有霸体,始终返回 <see cref="PoiseLevel.None"/>。
/// 设计决策:类似 Hollow Knight玩家依靠走位和弹反规避伤害而非硬吃。
/// 若未来需要临时霸体(如特定技能动作),请通过独立的覆盖标记实现,
/// 而非在此处引入状态,以保持接口语义清晰。
/// </summary>
public PoiseLevel GetCurrentPoiseLevel() => PoiseLevel.None;
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 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;
// ── Overlay Layer API供 SpringState / SoulSkill 等叠加动画使用)─────
/// <summary>
/// 在 Overlay LayerLayer 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()
{
_inputBuffer = GetComponent<InputBuffer>();
Debug.Assert(_movementConfig != null, "[PlayerController] _movementConfig 未赋值,请在 Inspector 中指定 PlayerMovementConfigSO。", this);
ResolveDependencies();
// 注入 HurtBox 依赖Phase 1只注入护盾弹反/霸体 Phase 2
if (_hurtBox != null && _shield != null)
_hurtBox.SetShieldable(_shield);
// 初始化 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);
}
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()
{
TransitionTo(_idleState);
if (!HasRequiredStateDependencies())
return;
// 广播玩家 TransformEnemyBase / ProjectileManager 等订阅者将通过事件接收引用
// (必须在 Start 中调用,确保所有 Awake/OnEnable 订阅已就绪)
_onPlayerSpawned?.Raise(transform);
TransitionTo(GetState<IdleState>());
}
private void Update()
{
if (!HasRequiredStateDependencies())
return;
// 冲刺冷却计时
GetState<DashState>()?.TickCooldown(Time.deltaTime);
_currentState?.OnStateUpdate();
}
@@ -123,29 +230,89 @@ namespace BaseGames.Player.States
// ── 状态机 ────────────────────────────────────────────────────────────
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();
}
/// <summary>尝试切换状态(供状态内部的条件转换使用)。</summary>
public void TryTransitionState(PlayerStateBase newState)
=> TransitionTo(newState);
private void InitializeStates()
{
_idleState = new IdleState(this);
_runState = new RunState(this);
_jumpState = new JumpState(this);
_fallState = new FallState(this);
_attackState = new AttackState(this);
_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 必须在 Inspector 中赋值SerializeField
// 已移除 Resources.FindObjectsOfTypeAll 全资产扫描回退
}
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;
}
// ── 状态访问器 ────────────────────────────────────────────────────────
public IdleState IdleState => _idleState;
public RunState RunState => _runState;
public JumpState JumpState => _jumpState;
public FallState FallState => _fallState;
public AttackState AttackState => _attackState;
// 使用 GetState<T>() 按类型获取任意状态实例,不再暴露具名属性。
}
}

View File

@@ -4,7 +4,7 @@ MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
executionOrder: -100
icon: {instanceID: 0}
userData:
assetBundleName:

View File

@@ -18,6 +18,22 @@ namespace BaseGames.Player.States
public virtual void OnStateUpdate() { }
public virtual void OnStateFixedUpdate() { }
public virtual void OnStateExit() { }
public virtual PlayerStateBase GetNextState() => null;
/// <summary>
/// 此状态期间是否应视为无敌(忽略伤害)。
/// 冲刺等状态 override 为 truePlayerController.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;

View File

@@ -17,26 +17,26 @@ namespace BaseGames.Player.States
{
if (!Move.IsGrounded)
{
_owner.TransitionTo(_owner.FallState);
_owner.TransitionTo(_owner.GetState<FallState>());
return;
}
if (Buffer.ConsumeJump())
{
_owner.TransitionTo(_owner.JumpState);
_owner.TransitionTo(_owner.GetState<JumpState>());
return;
}
if (Mathf.Abs(Input.MoveInput.x) < 0.1f)
{
_owner.TransitionTo(_owner.IdleState);
_owner.TransitionTo(_owner.GetState<IdleState>());
return;
}
Move.Move(Input.MoveInput.x * (Cfg != null ? Cfg.RunSpeed : 7f));
Move.Move(Input.MoveInput.x * Cfg.RunSpeed);
}
public override void OnStateFixedUpdate()
{
Move.Move(Input.MoveInput.x * (Cfg != null ? Cfg.RunSpeed : 7f));
Move.Move(Input.MoveInput.x * Cfg.RunSpeed);
}
}
}

View File

@@ -0,0 +1,55 @@
namespace BaseGames.Player.States
{
/// <summary>
/// 使用灵泉(治疗)状态(架构 05_PlayerModule §2
/// 消耗 SpringCharge播放治愈动画动画结束后回到 Idle。
/// 需在地面且有充能才能进入PlayerController 负责条件检查)。
///
/// 动画在 Overlay LayerLayer 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 LayerLayer 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>());
}
}
}

View File

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

View 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);
}
}
}

View File

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

View 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>());
}
}
}

View File

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

View 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();
}
}

View File

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

View 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>());
}
}
}

View File

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

View File

@@ -1,28 +1,105 @@
using System;
using System.Collections.Generic;
using UnityEngine;
namespace BaseGames.Player
{
/// <summary>
/// 武器管理器Phase 1 实现)
/// 架构 05_PlayerModule §7ActiveWeaponWeaponSOOnWeaponChanged 事件
/// ⚠️ 无 Equip() 方法,无 WeaponInstance 类
/// Phase 2 §2.3:接入 FormController.OnFormChanged
/// 武器管理器。
/// 监听 FormController.OnFormChanged依据当前形态切换 ActiveWeapon
/// 支持护符 Override护符调用 SetOverride() 临时替换特定形态的武器
/// 架构 05_PlayerModule §7
/// </summary>
public class WeaponManager : MonoBehaviour
{
[SerializeField] private WeaponSO _startingWeapon;
[SerializeField] private FormController _formController;
[SerializeField] private WeaponSO _startingWeapon; // 无 FormController 时的回退武器
public WeaponSO ActiveWeapon { get; private set; }
public event Action<WeaponSO> OnWeaponChanged;
// 护符注入的武器覆盖Key = FormSO.formIdValue = 替换武器(架构 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()
{
if (_startingWeapon != null)
// 若 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)
{
ActiveWeapon = weapon;
OnWeaponChanged?.Invoke(weapon);
}
// ── 护符 Override API由 WeaponOverrideEffect 调用,架构 05 §7──────
/// <summary>为指定形态设置武器覆盖。formId 为空 = 覆盖所有形态。</summary>
public void SetOverride(string formId, WeaponSO weapon)
{
if (string.IsNullOrEmpty(formId))
{
ActiveWeapon = _startingWeapon;
OnWeaponChanged?.Invoke(ActiveWeapon);
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);
}
}
}

View File

@@ -1,28 +1,91 @@
using System;
using UnityEngine;
using Animancer;
using BaseGames.Combat;
using BaseGames.Feedback;
namespace BaseGames.Player
{
/// <summary>
/// 武器数据 SO纯数据不含 Prefab 引用)。
/// 每个攻击方向对应一个 DamageSourceSO。
/// 每个攻击方向对应一个 DamageSourceSO 和 ClipTransition
/// HitBox 直接挂载在 Player Prefab 上(架构 05_PlayerModule §7
/// </summary>
[CreateAssetMenu(menuName = "Player/Weapon")]
public class WeaponSO : ScriptableObject
{
public string WeaponName;
public DamageSourceSO GroundSource;
public DamageSourceSO AirSource;
public DamageSourceSO UpSource;
public DamageSourceSO DownSource;
[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 覆盖(零向量 = 使用 PlayerCombat 默认尺寸)")]
public Vector2 hitBoxSizeOverride;
public Vector2 hitBoxOffsetOverride;
[Header("武器特效")]
public WeaponVFXConfig vfxConfig;
[Header("战斗参数")]
[Tooltip("命中确认时增加的灵力值(覆盖 PlayerCombat 默认值 10")]
[Min(0)]
public int soulPowerGain = 10;
// ── 方向查询 ──────────────────────────────────────────────────────────
public DamageSourceSO GetSourceByDir(AttackDirection dir) => dir switch
{
AttackDirection.Ground => GroundSource,
AttackDirection.Air => AirSource,
AttackDirection.Up => UpSource,
AttackDirection.Down => DownSource,
_ => GroundSource,
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("武器挥斩拖尾 Prefabnull = 不显示拖尾)")]
public GameObject weaponTrailPrefab;
public Color trailColor = Color.white;
}
}