chore: initial commit

This commit is contained in:
2026-05-08 11:04:00 +08:00
commit f55d2a57c3
6278 changed files with 866081 additions and 0 deletions

View 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,
}
}

View File

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

View File

@@ -0,0 +1,15 @@
using BaseGames.Core.Events;
using UnityEngine;
namespace BaseGames.Player
{
/// <summary>
/// 能力解锁事件频道(内部类型保留)。
/// ⚠️ EVT_AbilityUnlocked 资产实际使用 StringEventChannelSOabilityId 字符串),
/// 本频道供需要类型安全枚举的内部系统(如 FormController使用。
/// 发布AbilityManager.UnlockAbility()
/// 订阅PlayerController刷新可用动作集、HUDController更新技能面板
/// </summary>
[CreateAssetMenu(menuName = "Events/Player/AbilityType")]
public class AbilityTypeEventChannelSO : BaseEventChannelSO<AbilityType> { }
}

View File

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

View 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": []
}

View File

@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: a7ddc97eeb30a5a408e5fb7e472ff6fa
AssemblyDefinitionImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View 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 三形态参数
}
}

View File

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

View File

@@ -0,0 +1,7 @@
using UnityEngine;
namespace BaseGames.Player
{
/// <summary>形态控制器。Phase 1 桩 — Phase 2 实现。</summary>
public class FormController : MonoBehaviour { }
}

View File

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

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

View File

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

View File

@@ -0,0 +1,53 @@
using UnityEngine;
using BaseGames.Combat;
namespace BaseGames.Player
{
/// <summary>
/// 玩家战斗组件Phase 1 实现)。
/// 架构 05_PlayerModule §5HitBox 直接挂在 Player Prefab 子节点上,不经过 WeaponInstance。
/// 节点:[HitBoxGround]、[HitBoxUp]、[HitBoxDown]、[HitBoxAir]。
/// </summary>
public class PlayerCombat : MonoBehaviour
{
[SerializeField] private WeaponManager _weaponManager;
[Header("HitBoxPlayer 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,
};
}
}

View File

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

View 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,
}
}

View File

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

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

View File

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

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

View File

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

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

View File

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

View File

@@ -0,0 +1,7 @@
using UnityEngine;
namespace BaseGames.Player
{
/// <summary>技能管理器。Phase 1 桩 — Phase 2 实现。</summary>
public class SkillManager : MonoBehaviour { }
}

View File

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

View File

@@ -0,0 +1,7 @@
using UnityEngine;
namespace BaseGames.Player
{
/// <summary>治愈弹簧系统。Phase 1 桩 — Phase 1 Week 3 实现。</summary>
public class SpringSystem : MonoBehaviour { }
}

View File

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

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: ef1d46e50aa9602449145ec8cfb71edf
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

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

View File

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

View 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": []
}

View File

@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: c336a32eed62ced4280d1d4c9782ec91
AssemblyDefinitionImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

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

View File

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

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

View File

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

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

View File

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

View 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/ 程序集,以便引用所有具体状态类型。
/// 实现 IDamageableIsInvincible/Defense 委托 PlayerStatsTakeDamage 委托 _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;
}
}

View File

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

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

View File

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

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

View File

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

View File

@@ -0,0 +1,3 @@
// Placeholder to prevent asmdef-no-scripts warning.
namespace BaseGames.Player.States { }

View File

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

View File

@@ -0,0 +1,28 @@
using System;
using UnityEngine;
namespace BaseGames.Player
{
/// <summary>
/// 武器管理器Phase 1 实现)。
/// 架构 05_PlayerModule §7ActiveWeaponWeaponSOOnWeaponChanged 事件。
/// ⚠️ 无 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);
}
}
}
}

View File

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

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

View File

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