chore: initial commit
This commit is contained in:
48
Assets/Scripts/Player/AbilityType.cs
Normal file
48
Assets/Scripts/Player/AbilityType.cs
Normal file
@@ -0,0 +1,48 @@
|
||||
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, // 快速旅行解锁
|
||||
|
||||
// ── 组合掩码 ─────────────────────────────────────────────────────
|
||||
AllMovement = WallCling | WallJump | Dash | AirDash | DoubleJump | SuperJump | Swim | Dive,
|
||||
AllSpells = Spell1 | Spell2 | Spell3,
|
||||
AllSpirit = SpiritForm | SpiritDash,
|
||||
}
|
||||
}
|
||||
11
Assets/Scripts/Player/AbilityType.cs.meta
Normal file
11
Assets/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/Scripts/Player/AbilityTypeEventChannelSO.cs
Normal file
15
Assets/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 = "Events/Player/AbilityType")]
|
||||
public class AbilityTypeEventChannelSO : BaseEventChannelSO<AbilityType> { }
|
||||
}
|
||||
11
Assets/Scripts/Player/AbilityTypeEventChannelSO.cs.meta
Normal file
11
Assets/Scripts/Player/AbilityTypeEventChannelSO.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 4c8eab760bd9b37468f12fa79d3d0693
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
23
Assets/Scripts/Player/BaseGames.Player.asmdef
Normal file
23
Assets/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/Scripts/Player/BaseGames.Player.asmdef.meta
Normal file
7
Assets/Scripts/Player/BaseGames.Player.asmdef.meta
Normal file
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a7ddc97eeb30a5a408e5fb7e472ff6fa
|
||||
AssemblyDefinitionImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
14
Assets/Scripts/Player/FormConfigSO.cs
Normal file
14
Assets/Scripts/Player/FormConfigSO.cs
Normal file
@@ -0,0 +1,14 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace BaseGames.Player
|
||||
{
|
||||
/// <summary>
|
||||
/// 形态切换配置 ScriptableObject。
|
||||
/// Phase 1 骨架 — Phase 2 FormController 实现三形态切换时补充字段。
|
||||
/// </summary>
|
||||
[CreateAssetMenu(menuName = "Player/FormConfig")]
|
||||
public class FormConfigSO : ScriptableObject
|
||||
{
|
||||
// Phase 2: 填入 Normal / Soul / Spirit 三形态参数
|
||||
}
|
||||
}
|
||||
11
Assets/Scripts/Player/FormConfigSO.cs.meta
Normal file
11
Assets/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:
|
||||
7
Assets/Scripts/Player/FormController.cs
Normal file
7
Assets/Scripts/Player/FormController.cs
Normal file
@@ -0,0 +1,7 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace BaseGames.Player
|
||||
{
|
||||
/// <summary>形态控制器。Phase 1 桩 — Phase 2 实现。</summary>
|
||||
public class FormController : MonoBehaviour { }
|
||||
}
|
||||
11
Assets/Scripts/Player/FormController.cs.meta
Normal file
11
Assets/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:
|
||||
44
Assets/Scripts/Player/PlayerAnimationConfigSO.cs
Normal file
44
Assets/Scripts/Player/PlayerAnimationConfigSO.cs
Normal file
@@ -0,0 +1,44 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace BaseGames.Player
|
||||
{
|
||||
[CreateAssetMenu(menuName = "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;
|
||||
|
||||
[Header("弹簧")]
|
||||
public AnimationClip UseSpring;
|
||||
|
||||
[Header("地面攻击(连招序列)")]
|
||||
public AnimationClip[] GroundAttacks;
|
||||
|
||||
[Header("空中攻击")]
|
||||
public AnimationClip AirAttack;
|
||||
public AnimationClip UpAttack;
|
||||
public AnimationClip DownAttack; // 戳击 (Pogo)
|
||||
|
||||
[Header("弹反")]
|
||||
public AnimationClip ParryStart;
|
||||
public AnimationClip ParrySuccess;
|
||||
|
||||
/// <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/Scripts/Player/PlayerAnimationConfigSO.cs.meta
Normal file
11
Assets/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:
|
||||
53
Assets/Scripts/Player/PlayerCombat.cs
Normal file
53
Assets/Scripts/Player/PlayerCombat.cs
Normal file
@@ -0,0 +1,53 @@
|
||||
using UnityEngine;
|
||||
using BaseGames.Combat;
|
||||
|
||||
namespace BaseGames.Player
|
||||
{
|
||||
/// <summary>
|
||||
/// 玩家战斗组件(Phase 1 实现)。
|
||||
/// 架构 05_PlayerModule §5:HitBox 直接挂在 Player Prefab 子节点上,不经过 WeaponInstance。
|
||||
/// 节点:[HitBoxGround]、[HitBoxUp]、[HitBoxDown]、[HitBoxAir]。
|
||||
/// </summary>
|
||||
public class PlayerCombat : MonoBehaviour
|
||||
{
|
||||
[SerializeField] private WeaponManager _weaponManager;
|
||||
|
||||
[Header("HitBox(Player Prefab 子节点)")]
|
||||
[SerializeField] private HitBox _hitBoxGround;
|
||||
[SerializeField] private HitBox _hitBoxUp;
|
||||
[SerializeField] private HitBox _hitBoxDown;
|
||||
[SerializeField] private HitBox _hitBoxAir;
|
||||
|
||||
// ── HitBox 激活(由 AttackState / Animancer 帧事件调用)─────────────
|
||||
public void EnableWeaponHitBox(AttackDirection dir)
|
||||
{
|
||||
var source = _weaponManager != null
|
||||
? _weaponManager.ActiveWeapon?.GetSourceByDir(dir)
|
||||
: null;
|
||||
GetHitBox(dir)?.Activate(source, transform);
|
||||
}
|
||||
|
||||
public void DisableWeaponHitBox(AttackDirection dir)
|
||||
=> GetHitBox(dir)?.Deactivate();
|
||||
|
||||
public void DisableAllWeaponHitBoxes()
|
||||
{
|
||||
_hitBoxGround?.Deactivate();
|
||||
_hitBoxUp?.Deactivate();
|
||||
_hitBoxDown?.Deactivate();
|
||||
_hitBoxAir?.Deactivate();
|
||||
}
|
||||
|
||||
/// <summary>命中回调(Phase 2 §2.3 补全:增加灵力)。</summary>
|
||||
internal void OnHitConfirmed(DamageInfo info) { /* Phase 2:增加灵力 */ }
|
||||
|
||||
private HitBox GetHitBox(AttackDirection dir) => dir switch
|
||||
{
|
||||
AttackDirection.Ground => _hitBoxGround,
|
||||
AttackDirection.Up => _hitBoxUp,
|
||||
AttackDirection.Down => _hitBoxDown,
|
||||
AttackDirection.Air => _hitBoxAir,
|
||||
_ => null,
|
||||
};
|
||||
}
|
||||
}
|
||||
11
Assets/Scripts/Player/PlayerCombat.cs.meta
Normal file
11
Assets/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:
|
||||
153
Assets/Scripts/Player/PlayerMovement.cs
Normal file
153
Assets/Scripts/Player/PlayerMovement.cs
Normal file
@@ -0,0 +1,153 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace BaseGames.Player
|
||||
{
|
||||
/// <summary>
|
||||
/// 玩家物理移动组件。封装 Rigidbody2D 操作,提供跑动、跳跃、击退等接口。
|
||||
/// Phase 2 功能(冲刺、单向平台)在此留存桩方法。
|
||||
/// </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;
|
||||
|
||||
// ── 运行时状态 ────────────────────────────────────────────────────────
|
||||
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;
|
||||
|
||||
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;
|
||||
|
||||
private void Awake() => _rb = GetComponent<Rigidbody2D>();
|
||||
|
||||
private void FixedUpdate()
|
||||
{
|
||||
CheckGrounded();
|
||||
CheckWalls();
|
||||
|
||||
if (_isGrounded)
|
||||
_coyoteTimer = _config != null ? _config.CoyoteTime : 0.12f;
|
||||
else
|
||||
_coyoteTimer = Mathf.Max(0f, _coyoteTimer - Time.fixedDeltaTime);
|
||||
}
|
||||
|
||||
// ── 移动 ──────────────────────────────────────────────────────────────
|
||||
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;
|
||||
float newX = Mathf.MoveTowards(current, target, accel * Time.fixedDeltaTime);
|
||||
_rb.velocity = new Vector2(newX, _rb.velocity.y);
|
||||
}
|
||||
|
||||
// ── 跳跃 ──────────────────────────────────────────────────────────────
|
||||
public void Jump(bool isVariable = true)
|
||||
{
|
||||
float force = _config != null ? _config.JumpForce : 18f;
|
||||
_rb.velocity = new Vector2(_rb.velocity.x, force);
|
||||
_coyoteTimer = 0f;
|
||||
}
|
||||
|
||||
public void CutJump()
|
||||
{
|
||||
if (_rb.velocity.y > 0f)
|
||||
_rb.velocity = new Vector2(_rb.velocity.x, _rb.velocity.y * 0.5f);
|
||||
}
|
||||
|
||||
// ── 重力 ──────────────────────────────────────────────────────────────
|
||||
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;
|
||||
Vector3 s = transform.localScale;
|
||||
transform.localScale = new Vector3(Mathf.Abs(s.x) * dir, s.y, s.z);
|
||||
}
|
||||
|
||||
// ── 取消窗口 ──────────────────────────────────────────────────────────
|
||||
public void SetCancelWindowOpen(bool open) => _cancelWindowOpen = open;
|
||||
|
||||
// ── Phase 2 桩 ────────────────────────────────────────────────────────
|
||||
/// <summary>Phase 2 实现冲刺。</summary>
|
||||
public void Dash(Vector2 direction, float speed) { }
|
||||
|
||||
/// <summary>Phase 2 实现单向平台穿透。</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.OverlapBox(origin, _groundCheckSize, 0f, _groundLayer);
|
||||
|
||||
if (_isGrounded && !wasGrounded)
|
||||
_coyoteTimer = _config != null ? _config.CoyoteTime : 0.12f;
|
||||
}
|
||||
|
||||
private void CheckWalls()
|
||||
{
|
||||
if (_config == null) return;
|
||||
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 OnDrawGizmosSelected()
|
||||
{
|
||||
if (_groundCheck == null) return;
|
||||
Gizmos.color = Color.green;
|
||||
Gizmos.DrawWireCube(_groundCheck.position, _groundCheckSize);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>当前所在地面类型(用于脚步声等反馈)。</summary>
|
||||
public enum SurfaceType
|
||||
{
|
||||
Ground,
|
||||
OneWayPlatform,
|
||||
Slope,
|
||||
Ice,
|
||||
}
|
||||
}
|
||||
11
Assets/Scripts/Player/PlayerMovement.cs.meta
Normal file
11
Assets/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:
|
||||
38
Assets/Scripts/Player/PlayerMovementConfigSO.cs
Normal file
38
Assets/Scripts/Player/PlayerMovementConfigSO.cs
Normal file
@@ -0,0 +1,38 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace BaseGames.Player
|
||||
{
|
||||
[CreateAssetMenu(menuName = "Player/MovementConfig")]
|
||||
public class PlayerMovementConfigSO : ScriptableObject
|
||||
{
|
||||
[Header("地面移动")]
|
||||
public float RunSpeed = 7f;
|
||||
public float Acceleration = 50f;
|
||||
public float Deceleration = 80f;
|
||||
|
||||
[Header("跳跃")]
|
||||
public float JumpForce = 18f;
|
||||
public float CoyoteTime = 0.12f;
|
||||
public float FallGravityMult = 2.5f;
|
||||
public float MaxFallSpeed = 20f;
|
||||
|
||||
[Header("冲刺")]
|
||||
public float DashSpeed = 20f;
|
||||
public float DashDuration = 0.18f;
|
||||
public float DashCooldown = 0.4f;
|
||||
public int MaxAerialDashes = 1;
|
||||
|
||||
[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;
|
||||
}
|
||||
}
|
||||
11
Assets/Scripts/Player/PlayerMovementConfigSO.cs.meta
Normal file
11
Assets/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:
|
||||
235
Assets/Scripts/Player/PlayerStats.cs
Normal file
235
Assets/Scripts/Player/PlayerStats.cs
Normal file
@@ -0,0 +1,235 @@
|
||||
using UnityEngine;
|
||||
using BaseGames.Core.Events;
|
||||
using BaseGames.Core.Save;
|
||||
|
||||
namespace BaseGames.Player
|
||||
{
|
||||
/// <summary>
|
||||
/// 玩家数值管理组件。负责 HP、灵魂、灵气、弹簧充能、Geo、能力解锁与存档读写。
|
||||
/// </summary>
|
||||
public class PlayerStats : MonoBehaviour, ISaveable, BaseGames.Core.IRestoreOnSave
|
||||
{
|
||||
[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 _onGeoChanged;
|
||||
[SerializeField] private AbilityTypeEventChannelSO _onAbilityUnlocked;
|
||||
[SerializeField] private VoidEventChannelSO _onPlayerDied;
|
||||
|
||||
// ── 运行时数值 ─────────────────────────────────────────────────────────
|
||||
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 CurrentGeo { 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 void Awake()
|
||||
{
|
||||
if (_config == null)
|
||||
{
|
||||
Debug.LogWarning("[PlayerStats] PlayerStatsSO not assigned.", this);
|
||||
return;
|
||||
}
|
||||
MaxHP = _config.MaxHP;
|
||||
CurrentHP = MaxHP;
|
||||
MaxSoulPower = _config.MaxSoulPower;
|
||||
MaxSpiritPower = _config.MaxSpiritPower;
|
||||
MaxSpringCharges = _config.MaxSpringCharges;
|
||||
CurrentSpringCharges = MaxSpringCharges;
|
||||
CurrentGeo = _config.InitialGeo;
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
float dt = Time.deltaTime;
|
||||
|
||||
if (_invincibleTimer > 0f)
|
||||
_invincibleTimer -= dt;
|
||||
|
||||
if (_config != null && _config.SpiritRegenRate > 0)
|
||||
{
|
||||
_spiritRegenTimer += dt;
|
||||
if (_spiritRegenTimer >= 1f)
|
||||
{
|
||||
_spiritRegenTimer -= 1f;
|
||||
AddSpiritPower(_config.SpiritRegenRate);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── HP ────────────────────────────────────────────────────────────────
|
||||
public void TakeDamage(int amount)
|
||||
{
|
||||
if (IsInvincible || !IsAlive || amount <= 0) return;
|
||||
CurrentHP = Mathf.Max(0, CurrentHP - amount);
|
||||
_onHPChanged?.Raise(CurrentHP);
|
||||
if (CurrentHP == 0)
|
||||
_onPlayerDied?.Raise();
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
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);
|
||||
if (_config != null)
|
||||
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)
|
||||
{
|
||||
if (_config == null) return;
|
||||
SpringKillPoints += points;
|
||||
if (SpringKillPoints >= _config.SpringKillThreshold)
|
||||
{
|
||||
SpringKillPoints = 0;
|
||||
RestoreSpringCharges(1);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Geo ───────────────────────────────────────────────────────────────
|
||||
public void AddGeo(int amount)
|
||||
{
|
||||
if (amount <= 0) return;
|
||||
CurrentGeo += amount;
|
||||
_onGeoChanged?.Raise(CurrentGeo);
|
||||
}
|
||||
|
||||
public bool SpendGeo(int amount)
|
||||
{
|
||||
if (CurrentGeo < amount) return false;
|
||||
CurrentGeo -= amount;
|
||||
_onGeoChanged?.Raise(CurrentGeo);
|
||||
return true;
|
||||
}
|
||||
|
||||
// ── Invincibility ─────────────────────────────────────────────────────
|
||||
public void BeginInvincibility(float duration = -1f)
|
||||
{
|
||||
float d = duration >= 0f ? duration : (_config != null ? _config.InvincibilityDuration : 0.6f);
|
||||
_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);
|
||||
}
|
||||
|
||||
public void LockAbility(AbilityType ability)
|
||||
=> _unlockedAbilities &= ~ability;
|
||||
|
||||
// ── ISaveable ─────────────────────────────────────────────────────────
|
||||
public void OnSave(SaveData saveData)
|
||||
{
|
||||
var p = saveData.Player;
|
||||
p.CurrentHP = CurrentHP;
|
||||
p.MaxHP = MaxHP;
|
||||
p.CurrentGeo = CurrentGeo;
|
||||
p.AbilityFlags = (uint)_unlockedAbilities;
|
||||
}
|
||||
|
||||
public void OnLoad(SaveData saveData)
|
||||
{
|
||||
var p = saveData.Player;
|
||||
MaxHP = p.MaxHP;
|
||||
CurrentHP = Mathf.Clamp(p.CurrentHP, 0, MaxHP);
|
||||
CurrentGeo = p.CurrentGeo;
|
||||
_unlockedAbilities = (AbilityType)p.AbilityFlags;
|
||||
|
||||
_onHPChanged?.Raise(CurrentHP);
|
||||
_onMaxHPChanged?.Raise(MaxHP);
|
||||
_onGeoChanged?.Raise(CurrentGeo);
|
||||
}
|
||||
}
|
||||
}
|
||||
11
Assets/Scripts/Player/PlayerStats.cs.meta
Normal file
11
Assets/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/Scripts/Player/PlayerStatsSO.cs
Normal file
29
Assets/Scripts/Player/PlayerStatsSO.cs
Normal file
@@ -0,0 +1,29 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace BaseGames.Player
|
||||
{
|
||||
[CreateAssetMenu(menuName = "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 InitialGeo = 0;
|
||||
}
|
||||
}
|
||||
11
Assets/Scripts/Player/PlayerStatsSO.cs.meta
Normal file
11
Assets/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:
|
||||
7
Assets/Scripts/Player/SkillManager.cs
Normal file
7
Assets/Scripts/Player/SkillManager.cs
Normal file
@@ -0,0 +1,7 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace BaseGames.Player
|
||||
{
|
||||
/// <summary>技能管理器。Phase 1 桩 — Phase 2 实现。</summary>
|
||||
public class SkillManager : MonoBehaviour { }
|
||||
}
|
||||
11
Assets/Scripts/Player/SkillManager.cs.meta
Normal file
11
Assets/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:
|
||||
7
Assets/Scripts/Player/SpringSystem.cs
Normal file
7
Assets/Scripts/Player/SpringSystem.cs
Normal file
@@ -0,0 +1,7 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace BaseGames.Player
|
||||
{
|
||||
/// <summary>治愈弹簧系统。Phase 1 桩 — Phase 1 Week 3 实现。</summary>
|
||||
public class SpringSystem : MonoBehaviour { }
|
||||
}
|
||||
11
Assets/Scripts/Player/SpringSystem.cs.meta
Normal file
11
Assets/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:
|
||||
8
Assets/Scripts/Player/States.meta
Normal file
8
Assets/Scripts/Player/States.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ef1d46e50aa9602449145ec8cfb71edf
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
0
Assets/Scripts/Player/States/.gitkeep
Normal file
0
Assets/Scripts/Player/States/.gitkeep
Normal file
65
Assets/Scripts/Player/States/AttackState.cs
Normal file
65
Assets/Scripts/Player/States/AttackState.cs
Normal file
@@ -0,0 +1,65 @@
|
||||
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 归一化时间事件驱动
|
||||
events.Add(0.3f,
|
||||
() => Owner.Combat?.EnableWeaponHitBox(AttackDirection.Ground));
|
||||
events.Add(0.6f,
|
||||
() => Owner.Combat?.DisableAllWeaponHitBoxes());
|
||||
}
|
||||
|
||||
private void OnClipEnd()
|
||||
{
|
||||
Input.AttackEvent -= OnAttackInput;
|
||||
Owner.Combat?.DisableAllWeaponHitBoxes();
|
||||
Owner.TryTransitionState(Owner.IdleState);
|
||||
}
|
||||
|
||||
private void OnAttackInput()
|
||||
{
|
||||
if (_comboIndex < 2)
|
||||
{
|
||||
_comboIndex++;
|
||||
PlayAttackClip();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
11
Assets/Scripts/Player/States/AttackState.cs.meta
Normal file
11
Assets/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:
|
||||
22
Assets/Scripts/Player/States/BaseGames.Player.States.asmdef
Normal file
22
Assets/Scripts/Player/States/BaseGames.Player.States.asmdef
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"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"
|
||||
],
|
||||
"autoReferenced": true,
|
||||
"overrideReferences": false,
|
||||
"includePlatforms": []
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c336a32eed62ced4280d1d4c9782ec91
|
||||
AssemblyDefinitionImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
53
Assets/Scripts/Player/States/FallState.cs
Normal file
53
Assets/Scripts/Player/States/FallState.cs
Normal file
@@ -0,0 +1,53 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace BaseGames.Player.States
|
||||
{
|
||||
/// <summary>下落状态。着地后转为 Idle 或 Run,持有郊狼时间允许跳跃。</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() && Move.HasCoyoteTime)
|
||||
{
|
||||
_owner.TransitionTo(_owner.JumpState);
|
||||
return;
|
||||
}
|
||||
|
||||
// 着地
|
||||
if (Move.IsGrounded)
|
||||
{
|
||||
Move.ZeroVelocity();
|
||||
if (Mathf.Abs(Input.MoveInput.x) > 0.1f)
|
||||
_owner.TransitionTo(_owner.RunState);
|
||||
else
|
||||
_owner.TransitionTo(_owner.IdleState);
|
||||
return;
|
||||
}
|
||||
|
||||
// 空中水平移动
|
||||
if (Mathf.Abs(Input.MoveInput.x) > 0.01f)
|
||||
Move.Move(Input.MoveInput.x * (Cfg != null ? Cfg.RunSpeed : 7f));
|
||||
}
|
||||
|
||||
public override void OnStateFixedUpdate()
|
||||
{
|
||||
// 增强下落重力
|
||||
if (Cfg != null && 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/Scripts/Player/States/FallState.cs.meta
Normal file
11
Assets/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:
|
||||
35
Assets/Scripts/Player/States/IdleState.cs
Normal file
35
Assets/Scripts/Player/States/IdleState.cs
Normal file
@@ -0,0 +1,35 @@
|
||||
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();
|
||||
}
|
||||
|
||||
public override void OnStateUpdate()
|
||||
{
|
||||
if (!Move.IsGrounded)
|
||||
{
|
||||
_owner.TransitionTo(_owner.FallState);
|
||||
return;
|
||||
}
|
||||
if (Buffer.ConsumeJump())
|
||||
{
|
||||
_owner.TransitionTo(_owner.JumpState);
|
||||
return;
|
||||
}
|
||||
if (Mathf.Abs(Input.MoveInput.x) > 0.1f)
|
||||
{
|
||||
_owner.TransitionTo(_owner.RunState);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
11
Assets/Scripts/Player/States/IdleState.cs.meta
Normal file
11
Assets/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:
|
||||
38
Assets/Scripts/Player/States/JumpState.cs
Normal file
38
Assets/Scripts/Player/States/JumpState.cs
Normal file
@@ -0,0 +1,38 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace BaseGames.Player.States
|
||||
{
|
||||
/// <summary>跳跃状态。在 OnStateEnter 触发跳跃,速度降为零时转为 FallState。</summary>
|
||||
public class JumpState : PlayerStateBase
|
||||
{
|
||||
public JumpState(PlayerController owner) : base(owner) { }
|
||||
|
||||
public override void OnStateEnter()
|
||||
{
|
||||
if (AnimCfg?.Jump != null)
|
||||
Anim.Play(AnimCfg.Jump);
|
||||
Move.Jump();
|
||||
Input.JumpCancelledEvent += OnJumpCancelled;
|
||||
}
|
||||
|
||||
public override void OnStateUpdate()
|
||||
{
|
||||
// 上升结束时转为下落
|
||||
if (Move.Rb.velocity.y <= 0f)
|
||||
{
|
||||
_owner.TransitionTo(_owner.FallState);
|
||||
return;
|
||||
}
|
||||
// 水平移动
|
||||
if (Mathf.Abs(Input.MoveInput.x) > 0.01f)
|
||||
Move.Move(Input.MoveInput.x * (Cfg != null ? Cfg.RunSpeed : 7f));
|
||||
}
|
||||
|
||||
public override void OnStateExit()
|
||||
{
|
||||
Input.JumpCancelledEvent -= OnJumpCancelled;
|
||||
}
|
||||
|
||||
private void OnJumpCancelled() => Move.CutJump();
|
||||
}
|
||||
}
|
||||
11
Assets/Scripts/Player/States/JumpState.cs.meta
Normal file
11
Assets/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:
|
||||
151
Assets/Scripts/Player/States/PlayerController.cs
Normal file
151
Assets/Scripts/Player/States/PlayerController.cs
Normal file
@@ -0,0 +1,151 @@
|
||||
using UnityEngine;
|
||||
using Animancer;
|
||||
using BaseGames.Core.Events;
|
||||
using BaseGames.Input;
|
||||
using BaseGames.Combat;
|
||||
using BaseGames.Parry;
|
||||
|
||||
namespace BaseGames.Player.States
|
||||
{
|
||||
/// <summary>
|
||||
/// 玩家主控制器(协调器)。位于 Player/States/ 程序集,以便引用所有具体状态类型。
|
||||
/// 实现 IDamageable:IsInvincible/Defense 委托 PlayerStats,TakeDamage 委托 _stats。
|
||||
/// 依赖注入:所有子系统通过 [SerializeField] 字段在 Inspector 中绑定。
|
||||
/// </summary>
|
||||
[DefaultExecutionOrder(-100)]
|
||||
[RequireComponent(typeof(InputBuffer))]
|
||||
public class PlayerController : MonoBehaviour, IDamageable
|
||||
{
|
||||
// ── 移动 & 数值 ───────────────────────────────────────────────────────
|
||||
[Header("核心组件")]
|
||||
[SerializeField] private PlayerMovement _movement;
|
||||
[SerializeField] private PlayerStats _stats;
|
||||
[SerializeField] 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:三形态切换参数
|
||||
// ── 战斗组件 ──────────────────────────────────────────────────────────
|
||||
[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;
|
||||
|
||||
// ── 事件频道 ──────────────────────────────────────────────────────────
|
||||
[Header("事件频道")]
|
||||
[SerializeField] private VoidEventChannelSO _onPlayerDied;
|
||||
[SerializeField] private IntEventChannelSO _onHPChanged;
|
||||
|
||||
// ── 运行时 ────────────────────────────────────────────────────────────
|
||||
private InputBuffer _inputBuffer;
|
||||
|
||||
// ── 状态实例 ──────────────────────────────────────────────────────────
|
||||
private PlayerStateBase _currentState;
|
||||
|
||||
private IdleState _idleState;
|
||||
private RunState _runState;
|
||||
private JumpState _jumpState;
|
||||
private FallState _fallState;
|
||||
private AttackState _attackState;
|
||||
|
||||
// ── IDamageable 实现 ──────────────────────────────────────────────────
|
||||
public bool IsInvincible => _stats != null && _stats.IsInvincible;
|
||||
public int Defense => 0; // Phase 2:从 PlayerStatsSO 读取
|
||||
|
||||
public void TakeDamage(DamageInfo info)
|
||||
{
|
||||
if (_stats == null) return;
|
||||
_stats.TakeDamage(info.FinalDamage);
|
||||
// Phase 2:若非 DashState,切换 HurtState
|
||||
}
|
||||
|
||||
// ── 公开属性(供状态类访问)──────────────────────────────────────────
|
||||
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 bool IsGrounded => _movement != null && _movement.IsGrounded;
|
||||
public int FacingDirection => _movement != null ? _movement.FacingDirection : 1;
|
||||
|
||||
// ── Unity Lifecycle ───────────────────────────────────────────────────
|
||||
private void Awake()
|
||||
{
|
||||
_inputBuffer = GetComponent<InputBuffer>();
|
||||
|
||||
// 注入 HurtBox 依赖(Phase 1:只注入护盾;弹反/霸体 Phase 2)
|
||||
if (_hurtBox != null && _shield != null)
|
||||
_hurtBox.SetShieldable(_shield);
|
||||
|
||||
InitializeStates();
|
||||
}
|
||||
|
||||
private void Start()
|
||||
{
|
||||
TransitionTo(_idleState);
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
_currentState?.OnStateUpdate();
|
||||
}
|
||||
|
||||
private void FixedUpdate()
|
||||
{
|
||||
_currentState?.OnStateFixedUpdate();
|
||||
}
|
||||
|
||||
private void LateUpdate()
|
||||
{
|
||||
_movement?.UpdateFacing();
|
||||
}
|
||||
|
||||
// ── 状态机 ────────────────────────────────────────────────────────────
|
||||
public void TransitionTo(PlayerStateBase newState)
|
||||
{
|
||||
_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);
|
||||
}
|
||||
|
||||
// ── 状态访问器 ────────────────────────────────────────────────────────
|
||||
public IdleState IdleState => _idleState;
|
||||
public RunState RunState => _runState;
|
||||
public JumpState JumpState => _jumpState;
|
||||
public FallState FallState => _fallState;
|
||||
public AttackState AttackState => _attackState;
|
||||
}
|
||||
}
|
||||
11
Assets/Scripts/Player/States/PlayerController.cs.meta
Normal file
11
Assets/Scripts/Player/States/PlayerController.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e20d2200567c4ca4d8fa1a047c7bbd58
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
32
Assets/Scripts/Player/States/PlayerStateBase.cs
Normal file
32
Assets/Scripts/Player/States/PlayerStateBase.cs
Normal file
@@ -0,0 +1,32 @@
|
||||
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() { }
|
||||
|
||||
// ── 便捷属性 ──────────────────────────────────────────────────────────
|
||||
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/Scripts/Player/States/PlayerStateBase.cs.meta
Normal file
11
Assets/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:
|
||||
42
Assets/Scripts/Player/States/RunState.cs
Normal file
42
Assets/Scripts/Player/States/RunState.cs
Normal file
@@ -0,0 +1,42 @@
|
||||
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);
|
||||
}
|
||||
|
||||
public override void OnStateUpdate()
|
||||
{
|
||||
if (!Move.IsGrounded)
|
||||
{
|
||||
_owner.TransitionTo(_owner.FallState);
|
||||
return;
|
||||
}
|
||||
if (Buffer.ConsumeJump())
|
||||
{
|
||||
_owner.TransitionTo(_owner.JumpState);
|
||||
return;
|
||||
}
|
||||
if (Mathf.Abs(Input.MoveInput.x) < 0.1f)
|
||||
{
|
||||
_owner.TransitionTo(_owner.IdleState);
|
||||
return;
|
||||
}
|
||||
|
||||
Move.Move(Input.MoveInput.x * (Cfg != null ? Cfg.RunSpeed : 7f));
|
||||
}
|
||||
|
||||
public override void OnStateFixedUpdate()
|
||||
{
|
||||
Move.Move(Input.MoveInput.x * (Cfg != null ? Cfg.RunSpeed : 7f));
|
||||
}
|
||||
}
|
||||
}
|
||||
11
Assets/Scripts/Player/States/RunState.cs.meta
Normal file
11
Assets/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:
|
||||
3
Assets/Scripts/Player/States/_Placeholder.cs
Normal file
3
Assets/Scripts/Player/States/_Placeholder.cs
Normal file
@@ -0,0 +1,3 @@
|
||||
// Placeholder to prevent asmdef-no-scripts warning.
|
||||
namespace BaseGames.Player.States { }
|
||||
|
||||
11
Assets/Scripts/Player/States/_Placeholder.cs.meta
Normal file
11
Assets/Scripts/Player/States/_Placeholder.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ee579de5ae36a9448b7463976699bc20
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
28
Assets/Scripts/Player/WeaponManager.cs
Normal file
28
Assets/Scripts/Player/WeaponManager.cs
Normal file
@@ -0,0 +1,28 @@
|
||||
using System;
|
||||
using UnityEngine;
|
||||
|
||||
namespace BaseGames.Player
|
||||
{
|
||||
/// <summary>
|
||||
/// 武器管理器(Phase 1 实现)。
|
||||
/// 架构 05_PlayerModule §7:ActiveWeapon(WeaponSO),OnWeaponChanged 事件。
|
||||
/// ⚠️ 无 Equip() 方法,无 WeaponInstance 类。
|
||||
/// Phase 2 §2.3:接入 FormController.OnFormChanged。
|
||||
/// </summary>
|
||||
public class WeaponManager : MonoBehaviour
|
||||
{
|
||||
[SerializeField] private WeaponSO _startingWeapon;
|
||||
|
||||
public WeaponSO ActiveWeapon { get; private set; }
|
||||
public event Action<WeaponSO> OnWeaponChanged;
|
||||
|
||||
private void Start()
|
||||
{
|
||||
if (_startingWeapon != null)
|
||||
{
|
||||
ActiveWeapon = _startingWeapon;
|
||||
OnWeaponChanged?.Invoke(ActiveWeapon);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
11
Assets/Scripts/Player/WeaponManager.cs.meta
Normal file
11
Assets/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:
|
||||
28
Assets/Scripts/Player/WeaponSO.cs
Normal file
28
Assets/Scripts/Player/WeaponSO.cs
Normal file
@@ -0,0 +1,28 @@
|
||||
using UnityEngine;
|
||||
using BaseGames.Combat;
|
||||
|
||||
namespace BaseGames.Player
|
||||
{
|
||||
/// <summary>
|
||||
/// 武器数据 SO(纯数据,不含 Prefab 引用)。
|
||||
/// 每个攻击方向对应一个 DamageSourceSO。
|
||||
/// </summary>
|
||||
[CreateAssetMenu(menuName = "Player/Weapon")]
|
||||
public class WeaponSO : ScriptableObject
|
||||
{
|
||||
public string WeaponName;
|
||||
public DamageSourceSO GroundSource;
|
||||
public DamageSourceSO AirSource;
|
||||
public DamageSourceSO UpSource;
|
||||
public DamageSourceSO DownSource;
|
||||
|
||||
public DamageSourceSO GetSourceByDir(AttackDirection dir) => dir switch
|
||||
{
|
||||
AttackDirection.Ground => GroundSource,
|
||||
AttackDirection.Air => AirSource,
|
||||
AttackDirection.Up => UpSource,
|
||||
AttackDirection.Down => DownSource,
|
||||
_ => GroundSource,
|
||||
};
|
||||
}
|
||||
}
|
||||
11
Assets/Scripts/Player/WeaponSO.cs.meta
Normal file
11
Assets/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