引入属性、伤害管线

This commit is contained in:
2026-09-18 21:43:22 +08:00
parent 92e1ea0d55
commit ba25ec1926
108 changed files with 2920 additions and 720 deletions
@@ -1,5 +1,3 @@
using System;
using SepCore.Definition;
using SepCore.Entity;
using UnityEngine;
@@ -7,85 +5,32 @@ namespace SepCore.Components
{
public class AbsorbComponent : MonoBehaviour
{
[SerializeField] private int _maxDetectCount = 64;
private Player _player;
private Transform _target;
private StatComponent _statComponent;
private Collider[] _buffer;
private StatProperty _absorbRangeStat;
private Action<modifier, bool> _absorbRangeStatCallback;
private float AbsorbRangeBase => GameEntry.Luban.Global.DefaultAbsorbRange / 1000f;
private float AbsorbSpeed => GameEntry.Luban.Global.AbsorbSpeed / 1000f;
private float CollectDistance => GameEntry.Luban.Global.CollectDistance / 1000f;
private float AbsorbRange => Mathf.Max(0f, (AbsorbRangeBase + _absorbRangeStat.Value) * _absorbRangeStat.Percent);
private float AbsorbRange => AbsorbRangeBase;
public void OnInit(Player player, StatComponent statComponent)
{
_player = player;
_target = player.CachedTransform;
_statComponent = statComponent;
_buffer = new Collider[Mathf.Max(1, _maxDetectCount)];
_absorbRangeStat = _statComponent.GetStat(StatType.AbsorbRange);
_absorbRangeStatCallback = (modifier, isApply) =>
_statComponent.UpdateStat(_absorbRangeStat, modifier, isApply);
_statComponent.Subscribe(StatType.AbsorbRange, _absorbRangeStatCallback);
}
public void OnUpdate(float elapseSeconds, float realElapseSeconds)
{
if (!_player.Available) return;
int hitCount = Physics.OverlapSphereNonAlloc(
_target.position,
AbsorbRange,
_buffer,
Constant.Layer.DropLayerMask
);
for (int i = 0; i < hitCount; i++)
{
Collider hit = _buffer[i];
if (hit == null) continue;
CoinEntity coin = hit.GetComponent<CoinEntity>();
if (coin != null && coin.Available)
{
AbsorbDrop(coin.CachedTransform, elapseSeconds);
coin.TryCollect(_player, CollectDistance);
continue;
}
ExpEntity exp = hit.GetComponent<ExpEntity>();
if (exp != null && exp.Available)
{
AbsorbDrop(exp.CachedTransform, elapseSeconds);
exp.TryCollect(_player, CollectDistance);
}
}
GameEntry.SimulationWorld.SyncPlayerAbsorbSettings(AbsorbRange, AbsorbSpeed, CollectDistance);
}
public void OnReset()
{
if (_statComponent != null)
{
_statComponent.Unsubscribe(StatType.AbsorbRange, _absorbRangeStatCallback);
_absorbRangeStatCallback = null;
}
_statComponent = null;
_target = null;
_player = null;
_buffer = null;
}
private void AbsorbDrop(Transform drop, float deltaTime)
{
drop.position = Vector3.MoveTowards(drop.position, _target.position, AbsorbSpeed * deltaTime);
}
}
}
@@ -13,23 +13,17 @@ namespace SepCore.Components
private StatComponent _statComponent;
private StatProperty _maxHealthStat;
private Action<modifier, bool> _maxHealthStatCallback;
public StatProperty MaxHealthStat => _maxHealthStat;
private StatProperty _defenseStat;
private Action<modifier, bool> _defenseStatCallback;
public StatProperty DefenseStat => _defenseStat;
private StatProperty _dodgeStat;
private Action<modifier, bool> _dodgeStatCallback;
public StatProperty DodgeStat => _dodgeStat;
private Action<StatType, float> _onStatChangedAction;
private float _lastLifeStealTime = -1f;
public const float LifeStealIcd = 0.1f;
private bool _isPlayer => _statComponent != null;
public float HealthRatio => MaxHealth > 0 ? (float)CurrentHealth / MaxHealth : 0f;
public int MaxHealth => Mathf.CeilToInt((_maxHealthBase + _maxHealthStat.Value) * _maxHealthStat.Percent);
public int MaxHealth => _statComponent != null
? Mathf.CeilToInt(_statComponent.GetStatValue(StatType.MaxHp))
: _maxHealthBase;
public int CurrentHealth
{
@@ -46,82 +40,115 @@ namespace SepCore.Components
_statComponent = statComponent;
if (_statComponent != null)
{
_maxHealthStat = _statComponent.GetStat(StatType.MaxHealth);
_maxHealthStatCallback = (modifier, isApply) =>
{
_statComponent.UpdateStat(_maxHealthStat, modifier, isApply);
GameEntry.Event.Fire(this, PlayerHealthChangeEventArgs.Create(0, CurrentHealth, MaxHealth));
};
_statComponent.Subscribe(StatType.MaxHealth, _maxHealthStatCallback);
_defenseStat = _statComponent.GetStat(StatType.Defense);
_defenseStatCallback = (modifier, isApply) =>
_statComponent.UpdateStat(_defenseStat, modifier, isApply);
_statComponent.Subscribe(StatType.Defense, _defenseStatCallback);
_dodgeStat = _statComponent.GetStat(StatType.Dodge);
_dodgeStatCallback = (modifier, isApply) =>
_statComponent.UpdateStat(_dodgeStat, modifier, isApply);
_statComponent.Subscribe(StatType.Dodge, _dodgeStatCallback);
}
else
{
_maxHealthStat = new StatProperty();
_defenseStat = new StatProperty();
_dodgeStat = new StatProperty();
_statComponent.Pipeline.SetBaseValue(StatType.MaxHp, maxHealth);
_onStatChangedAction = OnStatChanged;
_statComponent.Pipeline.OnStatChanged += _onStatChangedAction;
}
}
public void OnReset()
{
if (_statComponent != null)
if (_statComponent != null && _onStatChangedAction != null)
{
_statComponent.Unsubscribe(StatType.MaxHealth, _maxHealthStatCallback);
_maxHealthStatCallback = null;
_statComponent.Unsubscribe(StatType.Defense, _defenseStatCallback);
_defenseStatCallback = null;
_statComponent.Unsubscribe(StatType.Dodge, _dodgeStatCallback);
_dodgeStatCallback = null;
_statComponent.Pipeline.OnStatChanged -= _onStatChangedAction;
_onStatChangedAction = null;
}
_statComponent = null;
_lastLifeStealTime = -1f;
}
private void OnStatChanged(StatType statType, float newValue)
{
if (statType == StatType.MaxHp)
{
if (_currentHealth > MaxHealth)
{
_currentHealth = MaxHealth;
}
if (GameEntry.Event != null)
{
GameEntry.Event.Fire(this, PlayerHealthChangeEventArgs.Create(0, CurrentHealth, MaxHealth));
}
}
}
#endregion
public void TakeDamage(int damage)
public void TakeDamage(int damage, bool isCrit = false, bool isDodge = false, Vector3? hitPosition = null)
{
if (_isDead) return;
float maxDodge = GameEntry.Luban.Global.MaxDodgeRate / 1000f;
if (UnityEngine.Random.value < Mathf.Clamp(_dodgeStat.Value, 0f, maxDodge))
if (isDodge)
{
damage = 0;
}
else
{
int defenseValue = (int)_defenseStat.Value;
float defensePercent = _defenseStat.Percent;
damage -= defenseValue;
damage = Mathf.CeilToInt(damage / defensePercent);
int minDamage = GameEntry.Luban.Global.MinDamage;
if (damage < minDamage) damage = minDamage;
float yOffset = GameEntry.Luban != null && GameEntry.Luban.IsReady
? GameEntry.Luban.Global.DamageTextYOffset / 1000f
: 1f;
Vector3 pos = hitPosition ?? (transform.position + Vector3.up * yOffset);
if (GameEntry.DamageText != null)
{
GameEntry.DamageText.ShowDamage(pos, 0, false, true);
}
return;
}
_currentHealth -= damage;
CauseDamage(damage);
if (_isPlayer)
if (damage <= 0) return;
_currentHealth = Mathf.Max(0, _currentHealth - damage);
CauseDamage(damage, isCrit, hitPosition);
if (_isPlayer && GameEntry.Event != null)
{
GameEntry.Event.Fire(this, PlayerHealthChangeEventArgs.Create(damage, CurrentHealth, MaxHealth));
}
}
private void CauseDamage(int damage)
public void TakeDamage(int damage)
{
TakeDamage(damage, false, false, null);
}
public void Heal(int healAmount)
{
if (_isDead || healAmount <= 0) return;
int oldHealth = _currentHealth;
_currentHealth = Mathf.Min(_currentHealth + healAmount, MaxHealth);
int healed = _currentHealth - oldHealth;
if (healed > 0 && _isPlayer && GameEntry.Event != null)
{
GameEntry.Event.Fire(this, PlayerHealthChangeEventArgs.Create(-healed, CurrentHealth, MaxHealth));
}
}
public bool TryTriggerLifeSteal(int healAmount, float? customTime = null)
{
if (_isDead || healAmount <= 0) return false;
float currentTime = customTime ?? Time.time;
if (_lastLifeStealTime >= 0f && currentTime - _lastLifeStealTime < LifeStealIcd)
{
return false;
}
_lastLifeStealTime = currentTime;
Heal(healAmount);
return true;
}
private void CauseDamage(int damage, bool isCrit = false, Vector3? hitPosition = null)
{
if (damage <= 0) return;
float yOffset = GameEntry.Luban.Global.DamageTextYOffset / 1000f;
GameEntry.DamageText.ShowDamage(transform.position + Vector3.up * yOffset, damage);
float yOffset = GameEntry.Luban != null && GameEntry.Luban.IsReady
? GameEntry.Luban.Global.DamageTextYOffset / 1000f
: 1f;
Vector3 pos = hitPosition ?? (transform.position + Vector3.up * yOffset);
if (GameEntry.DamageText != null)
{
GameEntry.DamageText.ShowDamage(pos, damage, isCrit, false);
}
}
}
}
@@ -16,7 +16,7 @@ namespace SepCore.Components
[SerializeField] private float _enemyBodyRadius;
[SerializeField] private int _separationIterations;
public float Speed => (_speedBase + _movementStat.Value) * _movementStat.Percent;
public float Speed => _statComponent != null ? _statComponent.GetStatValue(StatType.MoveSpeed) : _speedBase;
public bool AvoidEnemyOverlap => _avoidEnemyOverlap;
public float EnemyBodyRadius => _enemyBodyRadius;
public int SeparationIterations => _separationIterations;
@@ -28,9 +28,7 @@ namespace SepCore.Components
private StatComponent _statComponent;
private Player _player;
private EnemyBase _enemy;
private StatProperty _movementStat;
private Action<modifier, bool> _movementStatCallback;
private Action<StatType, float> _onStatChangedAction;
public void OnInit(float speed, Transform target, StatComponent statComponent = null,
bool avoidEnemyOverlap = false, float? enemyBodyRadius = null, int? separationIterations = null)
@@ -39,8 +37,15 @@ namespace SepCore.Components
_cachedTransform = target;
_direction = Vector3.forward;
_avoidEnemyOverlap = avoidEnemyOverlap;
_enemyBodyRadius = Mathf.Max(0.01f, enemyBodyRadius ?? (GameEntry.Luban.Global.EnemyBodyRadius / 1000f));
_separationIterations = Mathf.Max(1, separationIterations ?? GameEntry.Luban.Global.EnemySeparationIterations);
float defaultRadius = GameEntry.Luban != null && GameEntry.Luban.IsReady
? GameEntry.Luban.Global.EnemyBodyRadius / 1000f
: 0.5f;
int defaultIterations = GameEntry.Luban != null && GameEntry.Luban.IsReady
? GameEntry.Luban.Global.EnemySeparationIterations
: 4;
_enemyBodyRadius = Mathf.Max(0.01f, enemyBodyRadius ?? defaultRadius);
_separationIterations = Mathf.Max(1, separationIterations ?? defaultIterations);
TryGetComponent(out _player);
TryGetComponent(out _enemy);
@@ -48,17 +53,9 @@ namespace SepCore.Components
_statComponent = statComponent;
if (_statComponent != null)
{
_movementStat = _statComponent.GetStat(StatType.MovementSpeed);
_movementStatCallback = (modifier, isApply) =>
{
_statComponent.UpdateStat(_movementStat, modifier, isApply);
SyncToSimulationWorld();
};
_statComponent.Subscribe(StatType.MovementSpeed, _movementStatCallback);
}
else
{
_movementStat = new StatProperty();
_statComponent.Pipeline.SetBaseValue(StatType.MoveSpeed, speed);
_onStatChangedAction = OnStatChanged;
_statComponent.Pipeline.OnStatChanged += _onStatChangedAction;
}
SyncToSimulationWorld();
@@ -73,25 +70,32 @@ namespace SepCore.Components
{
Transform transformToUnregister = _cachedTransform;
float defaultRadius = GameEntry.Luban != null && GameEntry.Luban.IsReady
? GameEntry.Luban.Global.EnemyBodyRadius / 1000f
: 0.5f;
int defaultIterations = GameEntry.Luban != null && GameEntry.Luban.IsReady
? GameEntry.Luban.Global.EnemySeparationIterations
: 4;
_speedBase = 0;
_cachedTransform = null;
_direction = Vector3.zero;
_isMoving = false;
_avoidEnemyOverlap = false;
_enemyBodyRadius = GameEntry.Luban.Global.EnemyBodyRadius / 1000f;
_separationIterations = GameEntry.Luban.Global.EnemySeparationIterations;
_enemyBodyRadius = defaultRadius;
_separationIterations = defaultIterations;
_player = null;
_enemy = null;
if (_statComponent != null)
if (_statComponent != null && _onStatChangedAction != null)
{
_statComponent.Unsubscribe(StatType.MovementSpeed, _movementStatCallback);
_movementStatCallback = null;
_statComponent.Pipeline.OnStatChanged -= _onStatChangedAction;
_onStatChangedAction = null;
}
_statComponent = null;
if (transformToUnregister != null)
if (transformToUnregister != null && GameEntry.SimulationWorld != null)
{
GameEntry.SimulationWorld.UnregisterPlayerMovement(transformToUnregister);
}
@@ -113,7 +117,7 @@ namespace SepCore.Components
{
using (CustomProfilerMarker.Movement_Update.Auto())
{
if (_cachedTransform == null)
if (_cachedTransform == null || GameEntry.SimulationWorld == null)
{
return;
}
@@ -138,5 +142,13 @@ namespace SepCore.Components
}
}
}
private void OnStatChanged(StatType statType, float newValue)
{
if (statType == StatType.MoveSpeed)
{
SyncToSimulationWorld();
}
}
}
}
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using SepCore.Battle;
using SepCore.Definition;
using UnityEngine;
@@ -7,8 +8,12 @@ namespace SepCore.Components
{
public class StatComponent : MonoBehaviour
{
private readonly Dictionary<StatType, List<modifier>> _statModifiers = new();
private readonly Dictionary<StatType, Action<modifier, bool>> _statModifierCallbacks = new();
private readonly AttributePipeline _pipeline = new();
/// <summary>
/// 纯逻辑属性修饰器管线。
/// </summary>
public AttributePipeline Pipeline => _pipeline;
public void OnInit()
{
@@ -16,82 +21,42 @@ namespace SepCore.Components
public void OnReset()
{
_statModifiers.Clear();
_statModifierCallbacks.Clear();
_pipeline.Clear();
}
public void AddModifier(modifier modifier)
public void AddModifier(StatType statType, StatModifier modifier)
{
if (!_statModifiers.TryGetValue(modifier.StatType, out var modifiers))
{
modifiers = new List<modifier>();
_statModifiers.Add(modifier.StatType, modifiers);
}
modifiers.Add(modifier);
if (_statModifierCallbacks.TryGetValue(modifier.StatType, out var callbacks))
callbacks?.Invoke(modifier, true);
_pipeline.AddModifier(statType, modifier);
}
public void RemoveModifier(modifier modifier)
public bool RemoveModifier(StatType statType, StatModifier modifier)
{
if (_statModifiers.TryGetValue(modifier.StatType, out var modifiers))
{
modifiers.Remove(modifier);
}
if (_statModifierCallbacks.TryGetValue(modifier.StatType, out var callbacks))
callbacks?.Invoke(modifier, false);
return _pipeline.RemoveModifier(statType, modifier);
}
public void Subscribe(StatType statType, Action<modifier, bool> callback)
public void AddModifier(StatModifierConfig statModifierConfig, object source = null)
{
if (!_statModifierCallbacks.TryAdd(statType, callback))
{
_statModifierCallbacks[statType] += callback;
}
_pipeline.AddModifier(statModifierConfig, source);
}
public void Unsubscribe(StatType statType, Action<modifier, bool> callback)
public void RemoveModifier(StatModifierConfig statModifierConfig, object source = null)
{
if (!_statModifierCallbacks.TryGetValue(statType, out var callbacks))
{
return;
}
callbacks -= callback;
if (callbacks == null)
{
_statModifierCallbacks.Remove(statType);
}
_pipeline.RemoveModifier(statModifierConfig, source);
}
public StatProperty GetStat(StatType statType)
public int RemoveAllModifiersFromSource(object source)
{
var result = new StatProperty();
if (!_statModifiers.TryGetValue(statType, out var modifiers)) return result;
foreach (var modifier in modifiers)
{
if (modifier.IsPercent) result.Percent += modifier.RealValue;
else result.Value += modifier.RealValue;
}
return result;
return _pipeline.RemoveAllModifiersFromSource(source);
}
public void UpdateStat(StatProperty stat, modifier modifier, bool isApply)
public float GetStatValue(StatType statType)
{
float coef = isApply ? 1f : -1f;
if (modifier.IsPercent)
{
stat.Percent += modifier.RealValue * coef;
}
else
{
stat.Value += modifier.RealValue * coef;
}
return _pipeline.GetValue(statType);
}
public CharacterStat GetCharacterStat(StatType statType)
{
return _pipeline.Get(statType);
}
}
}