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,17 @@
{
"excludePlatforms": [],
"allowUnsafeCode": false,
"precompiledReferences": [],
"name": "BaseGames.Combat",
"defineConstraints": [],
"noEngineReferences": false,
"versionDefines": [],
"rootNamespace": "BaseGames.Combat",
"references": [
"BaseGames.Core.Events",
"BaseGames.Parry"
],
"autoReferenced": true,
"overrideReferences": false,
"includePlatforms": []
}

View File

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

View File

@@ -0,0 +1,82 @@
using System;
namespace BaseGames.Combat
{
// ── 元素/物理属性 ───────────────────────────────────────────────────────
public enum DamageType { Normal, True, Fire, Poison, Ice, Lightning, Void }
// ── 来源分类 ────────────────────────────────────────────────────────────
public enum DamageCategory
{
NormalAttack = 0,
SoulSkill = 1,
SpiritSkill = 2,
Projectile = 3,
EnvironmentTrap = 4,
StatusEffect = 5,
FallDamage = 6,
Reflected = 7,
}
// ── 行为标志 ────────────────────────────────────────────────────────────
[Flags]
public enum DamageFlags
{
None = 0,
Unblockable = 1 << 0,
CanBeParried = 1 << 1,
IgnoreIFrame = 1 << 2,
PerfectParryOnly = 1 << 3,
IsProjectile = 1 << 4,
CanClash = 1 << 5,
ForceBreak = 1 << 6,
NoKnockback = 1 << 7,
}
// ── 交互标签 ────────────────────────────────────────────────────────────
[Flags]
public enum DamageTags : uint
{
None = 0,
MeleeHit = 1 << 0,
RangedHit = 1 << 1,
SkillHit = 1 << 2,
ElementFire = 1 << 3,
ElementPoison = 1 << 4,
ElementVoid = 1 << 5,
AfterParry = 1 << 6,
ChargedAttack = 1 << 7,
SkyFormOnly = 1 << 8,
EarthFormOnly = 1 << 9,
DeathFormOnly = 1 << 10,
BreakLight = 1 << 11,
BreakMedium = 1 << 12,
BreakHeavy = 1 << 13,
BreakBreaker = 1 << 14,
}
public enum HitFxType { Spark, Slash, Blood, Magic, Heavy, Crit, Void, Heal, Parry, Fire, Ice }
// ── 攻击方打断等级 ──────────────────────────────────────────────────────
public enum BreakLevel
{
None = 0,
Light = 1,
Medium = 2,
Heavy = 3,
Breaker = 4,
}
// ── 承受方霸体等级 ──────────────────────────────────────────────────────
public enum PoiseLevel
{
None = 0,
Light = 1,
Medium = 2,
Heavy = 3,
Unbreakable = 100,
}
// ── 攻击方向PlayerCombat / WeaponSO 使用)────────────────────────────
public enum AttackDirection { Ground, Up, Down, Air }
}

View File

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

View File

@@ -0,0 +1,48 @@
namespace BaseGames.Combat
{
/// <summary>
/// 可受击实体接口。PlayerController 和 EnemyBase 实现此接口。
/// HurtBox.Awake 通过 GetComponentInParent&lt;IDamageable&gt;() 注入。
/// </summary>
public interface IDamageable
{
bool IsInvincible { get; }
int Defense { get; }
void TakeDamage(DamageInfo info);
}
/// <summary>
/// 可持有霸体的实体接口。HurtBox 在 ReceiveDamage 中做等级比较。
/// </summary>
public interface IPoiseSource
{
PoiseLevel GetCurrentPoiseLevel();
}
/// <summary>
/// 护盾接口(玩家专属)。由 PlayerController.Awake() 注入 HurtBox。
/// AbsorbDamage 返回穿透量0 = 全部吸收,>0 = 穿透量继续走 TakeDamage 流程)。
/// </summary>
public interface IShieldable
{
bool HasShield { get; }
int AbsorbDamage(int amount);
}
/// <summary>
/// 可破坏机关/障碍物接口。HitBox 在命中非 HurtBox 对象时尝试调用。
/// </summary>
public interface IBreakable
{
void TryInteract(DamageInfo info);
}
/// <summary>
/// 可施加状态效果的实体接口(避免 Combat 直接引用 StatusEffects 程序集)。
/// StatusEffectManager 实现此接口HurtBox.ReceiveDamage 步骤 8 通过此接口调用。
/// </summary>
public interface IStatusEffectable
{
void ApplyStatusEffect(DamageType type);
}
}

View File

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

View File

@@ -0,0 +1,82 @@
using UnityEngine;
using BaseGames.Core.Events;
namespace BaseGames.Combat
{
/// <summary>
/// 单次伤害信息。流水线RawDamage → Amount护盾修改→ FinalDamage防御减免后
/// ⚠️ 非 readonly struct — Builder 就地写入字段。
/// </summary>
[System.Serializable]
public struct DamageInfo
{
public int RawDamage; // HitBox 设定的原始值Builder.SetRaw 写入一次)
public int Amount; // 流水线中被护盾/防御修改
public int FinalDamage; // HurtBox 写入,最终 HP 扣除量
public Vector2 KnockbackDirection;
public float KnockbackForce;
public float HitStunDuration;
public DamageType Type;
public DamageCategory Category;
public DamageFlags Flags;
public DamageTags Tags;
public Vector2 SourcePosition;
public int SourceLayer;
public HitFxType FxType;
public BreakLevel Break;
public string SourceId;
public string SkillId;
// ── Builder ──────────────────────────────────────────────────────────
public class Builder
{
private DamageInfo _d;
public Builder() { }
// SetRaw 同步初始化 AmountAmount 始终以 RawDamage 为起点)
public Builder SetRaw(int v) { _d.RawDamage = v; _d.Amount = v; return this; }
public Builder SetType(DamageType v) { _d.Type = v; return this; }
public Builder SetCategory(DamageCategory v){ _d.Category = v; return this; }
public Builder SetFlags(DamageFlags v) { _d.Flags = v; return this; }
public Builder SetTags(DamageTags v) { _d.Tags = v; return this; }
public Builder SetSkillId(string v) { _d.SkillId = v; return this; }
public Builder SetSourceId(string v) { _d.SourceId = v; return this; }
public Builder SetKnockback(Vector2 dir, float force)
{ _d.KnockbackDirection = dir; _d.KnockbackForce = force; return this; }
public Builder SetStun(float dur) { _d.HitStunDuration = dur; return this; }
public Builder SetFx(HitFxType v) { _d.FxType = v; return this; }
public Builder SetBreak(BreakLevel v) { _d.Break = v; return this; }
public Builder SetSourcePos(Vector2 v) { _d.SourcePosition = v; return this; }
public Builder SetLayer(int v) { _d.SourceLayer = v; return this; }
public DamageInfo Build() => _d;
}
/// <summary>
/// ⚡ 零堆分配工厂(热路径首选)。直接从 DamageSourceSO 填入基础字段。
/// KnockbackDirection / SourcePosition / SourceLayer 等运行时字段由调用方就地赋值。
/// </summary>
public static DamageInfo From(DamageSourceSO so)
{
int baseAmt = Mathf.RoundToInt(so.BaseDamage * so.DamageMultiplier);
return new DamageInfo
{
RawDamage = baseAmt,
Amount = baseAmt,
Type = so.Type,
Category = so.Category,
Flags = so.Flags,
Tags = so.Tags,
HitStunDuration = so.HitStunDuration,
FxType = so.FxType,
Break = so.BreakLevel,
SourceId = so.sourceId,
SkillId = so.skillId,
};
}
}
/// <summary>伤害事件频道EVT_DamageDealt。</summary>
[UnityEngine.CreateAssetMenu(menuName = "Events/DamageDealt")]
public class DamageInfoEventChannelSO : BaseEventChannelSO<DamageInfo> { }
}

View File

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

View File

@@ -0,0 +1,52 @@
using UnityEngine;
namespace BaseGames.Combat
{
/// <summary>
/// 攻击数据源 SO。描述单次攻击的基础伤害参数。
/// ⚡ 热路径使用零分配工厂DamageInfo.From(sourceSO)。
/// 仅需链式覆盖多字段时才使用 CreateBuilder()。
/// </summary>
[CreateAssetMenu(menuName = "Combat/DamageSource")]
public class DamageSourceSO : ScriptableObject
{
[Header("Identity")]
public string sourceId;
public string skillId;
[Header("Base")]
public int BaseDamage = 10;
public float DamageMultiplier = 1.0f;
public DamageType Type = DamageType.Normal;
public DamageCategory Category = DamageCategory.NormalAttack;
public DamageFlags Flags = DamageFlags.CanBeParried;
public DamageTags Tags = DamageTags.MeleeHit;
[Header("Physics")]
public float KnockbackForce = 5f;
public float HitStunDuration = 0.1f;
public BreakLevel BreakLevel = BreakLevel.Light;
[Header("FX")]
public HitFxType FxType = HitFxType.Slash;
[Header("Combo")]
public float ComboWindowDuration = 0.4f;
public float CancelWindowEnd = 0.5f;
/// <summary>
/// 链式 Builder特殊场景使用热路径改用 DamageInfo.From(this))。
/// </summary>
public DamageInfo.Builder CreateBuilder() => new DamageInfo.Builder()
.SetRaw(Mathf.RoundToInt(BaseDamage * DamageMultiplier))
.SetType(Type)
.SetCategory(Category)
.SetFlags(Flags)
.SetTags(Tags)
.SetStun(HitStunDuration)
.SetFx(FxType)
.SetBreak(BreakLevel)
.SetSourceId(sourceId)
.SetSkillId(skillId);
}
}

View File

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

View File

@@ -0,0 +1,96 @@
using System.Collections.Generic;
using UnityEngine;
namespace BaseGames.Combat
{
/// <summary>
/// 攻击判定盒。挂载在武器 Prefab 或技能 HitBox Prefab 的子节点上。
/// Phase 1 简化:直接挂在 Player Prefab 子节点 [HitBoxGround/Up/Down/Air]。
/// Collider2D 需设 IsTrigger = trueLayer = PlayerHitBox 或 EnemyHitBox。
/// </summary>
[RequireComponent(typeof(Collider2D))]
public class HitBox : MonoBehaviour
{
[SerializeField] private DamageSourceSO _defaultSource;
[SerializeField] private float _hitCooldown = 0.1f;
private DamageSourceSO _currentSource;
private Transform _attackerTransform;
private bool _isActive;
/// <summary>命中确认委托PlayerCombat / EnemyCombat 订阅)。</summary>
public System.Action<DamageInfo> OnHitConfirmed;
/// <summary>
/// 激活 HitBox。source/attacker 均可选,未传则使用 Inspector 默认值。
/// ⚠️ 不存在 Activate(float duration) 重载。
/// </summary>
public void Activate(DamageSourceSO source = null, Transform attacker = null)
{
_currentSource = source ?? _defaultSource;
_attackerTransform = attacker ?? transform;
_isActive = true;
}
public void Deactivate() => _isActive = false;
private void Awake()
{
// 确保 Collider2D 是 Trigger
var col = GetComponent<Collider2D>();
if (!col.isTrigger)
Debug.LogWarning($"[HitBox] {name}: Collider2D.isTrigger 应为 true。", this);
}
private void OnDisable()
{
_isActive = false;
_hitCooldownTimers.Clear();
}
private void OnTriggerEnter2D(Collider2D other)
{
if (!_isActive) return;
if (_currentSource == null)
{
Debug.LogWarning($"[HitBox] {name}: 无 DamageSourceSO跳过命中。", this);
return;
}
if (!CheckCooldown(other)) return;
Vector2 knockDir = ((Vector2)other.bounds.center
- (Vector2)_attackerTransform.position).normalized;
// ⚡ 零 GCstruct 工厂,就地赋值运行时字段
var info = DamageInfo.From(_currentSource);
info.KnockbackDirection = knockDir;
info.KnockbackForce = _currentSource.KnockbackForce;
info.SourcePosition = _attackerTransform.position;
info.SourceLayer = _attackerTransform.gameObject.layer;
// ① 命中 HurtBox
var hurtBox = other.GetComponent<HurtBox>();
if (hurtBox != null)
{
hurtBox.ReceiveDamage(info);
OnHitConfirmed?.Invoke(info);
return;
}
// ② 命中 IBreakable机关/障碍物)
other.GetComponent<IBreakable>()?.TryInteract(info);
}
// ── 同目标多帧命中冷却 ────────────────────────────────────────────────
private readonly Dictionary<Collider2D, float> _hitCooldownTimers = new();
private bool CheckCooldown(Collider2D other)
{
float now = Time.time;
if (_hitCooldownTimers.TryGetValue(other, out float last) && now - last < _hitCooldown)
return false;
_hitCooldownTimers[other] = now;
return true;
}
}
}

View File

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

View File

@@ -0,0 +1,8 @@
using BaseGames.Core.Events;
using UnityEngine;
namespace BaseGames.Combat
{
[CreateAssetMenu(menuName = "Events/HitConfirmed")]
public class HitConfirmedEventChannelSO : BaseEventChannelSO<HitInfo> { }
}

View File

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

View File

@@ -0,0 +1,17 @@
using UnityEngine;
using BaseGames.Core.Events;
namespace BaseGames.Combat
{
/// <summary>
/// 命中信息HurtBox.ReceiveDamage 广播给 VFX/Audio/Feedback
/// </summary>
public struct HitInfo
{
public DamageInfo DamageInfo;
public Vector3 HitPoint;
public Vector3 HitNormal;
public Transform HitTransform;
}
}

View File

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

View File

@@ -0,0 +1,106 @@
using UnityEngine;
using BaseGames.Parry;
namespace BaseGames.Combat
{
/// <summary>
/// 受击盒组件。实现完整 8 步伤害流水线(架构 06_CombatModule §5
/// 挂载在角色根节点或指定子节点上Collider2D 需设 IsTrigger = true
/// Layer = PlayerHurtBox 或 EnemyHurtBox。
/// </summary>
[RequireComponent(typeof(Collider2D))]
public class HurtBox : MonoBehaviour
{
// ── 伤害接受方Awake 注入)──────────────────────────────────────────
private IDamageable _owner;
private IShieldable _shieldable; // 由 PlayerController.Awake() 注入
private ParrySystem _parrySystem; // Phase 2 由 PlayerController.Awake() 注入
private IPoiseSource _poiseSource; // Phase 2 由 EnemyBase.Awake() 注入
private bool _isHurtBoxInvincible;
private bool _isActive = true;
// ── 事件频道 ──────────────────────────────────────────────────────────
[SerializeField] private DamageInfoEventChannelSO _onDamageDealt;
[SerializeField] private HitConfirmedEventChannelSO _onHitConfirmed;
// ── 注入接口 ──────────────────────────────────────────────────────────
public void SetShieldable(IShieldable shieldable) => _shieldable = shieldable;
public void SetParrySystem(ParrySystem ps) => _parrySystem = ps;
public void SetPoiseSource(IPoiseSource src) => _poiseSource = src;
public void SetInvincible(bool value) => _isHurtBoxInvincible = value;
public void SetActive(bool value) => _isActive = value;
private void Awake()
{
_owner = GetComponentInParent<IDamageable>();
if (_owner == null)
Debug.LogWarning($"[HurtBox] {name}: 父节点中未找到 IDamageable 实现。", this);
}
/// <summary>
/// 接受伤害(由 HitBox.OnTriggerEnter2D 直接调用)。
/// ⚠️ 方法名必须为 ReceiveDamage。
/// </summary>
public void ReceiveDamage(DamageInfo info)
{
if (!_isActive || _owner == null) return;
// 1. 无敌帧检查
if ((_owner.IsInvincible || _isHurtBoxInvincible)
&& !info.Flags.HasFlag(DamageFlags.IgnoreIFrame)) return;
// 2. 弹反检查Phase 1 _parrySystem == null 跳过)
// ParrySystem 只暴露窗口状态,伤害数据留在 Combat 层,无跨程序集数据依赖。
if (_parrySystem != null && info.Flags.HasFlag(DamageFlags.CanBeParried))
if (_parrySystem.ConsumeParry()) return;
// 3. 霸体检查Phase 1 _poiseSource == null 跳过)
if (!info.Flags.HasFlag(DamageFlags.ForceBreak) && _poiseSource != null)
{
PoiseLevel curPoise = _poiseSource.GetCurrentPoiseLevel();
if (curPoise == PoiseLevel.Unbreakable) return;
if ((int)info.Break < (int)curPoise)
{
_onHitConfirmed?.Raise(new HitInfo
{
DamageInfo = info,
HitPoint = transform.position,
});
return;
}
}
// 4. 护盾层拦截(玩家专属,在防御减免前)
if (_shieldable != null && _shieldable.HasShield)
{
int passThrough = _shieldable.AbsorbDamage(info.Amount);
if (passThrough <= 0) return;
info.Amount = passThrough;
}
// 5. 计算 FinalDamage防御减免最低 1
int finalDamage = UnityEngine.Mathf.Max(1, info.Amount - _owner.Defense);
info.Amount = finalDamage;
info.FinalDamage = finalDamage;
// 6. 调用 _owner.TakeDamage
_owner.TakeDamage(info);
// 7. 全局广播
_onDamageDealt?.Raise(info);
_onHitConfirmed?.Raise(new HitInfo
{
DamageInfo = info,
HitPoint = transform.position,
});
// 8. 状态效果触发DoT — Fire / Poison
// 使用接口避免对 StatusEffects 程序集的直接依赖
if (_owner is UnityEngine.MonoBehaviour mb)
{
mb.GetComponent<IStatusEffectable>()?.ApplyStatusEffect(info.Type);
}
}
}
}

View File

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

View File

@@ -0,0 +1,20 @@
using UnityEngine;
namespace BaseGames.Combat
{
/// <summary>
/// 护盾组件Phase 1 存根)。实现 IShieldable 接口供 HurtBox 注入。
/// Phase 2 实现完整护盾逻辑(护盾值、再生、破盾事件)。
/// </summary>
public class ShieldComponent : MonoBehaviour, IShieldable
{
public bool HasShield { get; private set; }
/// <summary>
/// 尝试以护盾吸收伤害。
/// 返回穿透量0 = 全部吸收,>0 = 穿透量继续走 TakeDamage 流程)。
/// Phase 1护盾不存在全量穿透。
/// </summary>
public int AbsorbDamage(int amount) => amount;
}
}

View File

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

View File

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

View File

@@ -0,0 +1,16 @@
{
"excludePlatforms": [],
"allowUnsafeCode": false,
"precompiledReferences": [],
"name": "BaseGames.Combat.StatusEffects",
"defineConstraints": [],
"noEngineReferences": false,
"versionDefines": [],
"rootNamespace": "BaseGames.Combat.StatusEffects",
"references": [
"BaseGames.Combat"
],
"autoReferenced": true,
"overrideReferences": false,
"includePlatforms": []
}

View File

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

View File

@@ -0,0 +1,20 @@
using UnityEngine;
using BaseGames.Combat;
namespace BaseGames.Combat.StatusEffects
{
/// <summary>
/// 状态效果管理器Phase 1 桩)。
/// 实现 IStatusEffectable 接口,由 HurtBox 通过接口调用,避免程序集循环依赖。
/// Phase 2 实现完整的效果叠加、持续时间、DoT 伤害计算。
/// </summary>
public class StatusEffectManager : MonoBehaviour, IStatusEffectable
{
// Phase 1空实现
public void ApplyStatusEffect(DamageType type) { }
}
// ── Phase 1 占位效果类型 ──────────────────────────────────────────────────
public class FireEffect { }
public class PoisonEffect { }
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 708938b7c3d75b244abcbd30ed589461
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.Combat.StatusEffects { }

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 1dfc988231a6ac14a9aa035ba1719ab0
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.Combat { }

View File

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