引入属性、伤害管线

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
@@ -0,0 +1,4 @@
using System.Runtime.CompilerServices;
[assembly: InternalsVisibleTo("Simulation.EditModeTests")]
[assembly: InternalsVisibleTo("Simulation.PlayModeTests")]
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 2d68a67948722984392ea42a1cec3594
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -24,7 +24,7 @@ public partial class GameEntry
public static EnemyManagerComponent EnemyManager { get; private set; }
public static SimulationWorld SimulationWorld { get; private set; }
public static ISimulationWorld SimulationWorld { get; private set; }
public static UIRouterComponent UIRouter { get; private set; }
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 715771c24b633ee49b200700baec66de
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,187 @@
using System;
using System.Collections.Generic;
using SepCore.Definition;
namespace SepCore.Battle
{
/// <summary>
/// 多维属性修饰器管线(Attribute Pipeline)。
/// 管理角色所有属性维度,提供三段修饰求值、脏标记缓存与按 Source 批量移除机制。
/// 纯 C# 类,无 MonoBehaviour 依赖。
/// </summary>
public sealed class AttributePipeline : IAttributeProvider
{
private readonly Dictionary<StatType, CharacterStat> _stats = new();
/// <summary>
/// 当某个属性的值发生变动(添加/移除修饰器、修改基础值)时触发。
/// 参数:(StatType 属性类型, float 新的当前值)。
/// </summary>
public event Action<StatType, float> OnStatChanged;
public AttributePipeline()
{
// 预初始化已定义的所有 StatType 枚举
Array values = Enum.GetValues(typeof(StatType));
for (int i = 0; i < values.Length; i++)
{
StatType statType = (StatType)values.GetValue(i);
_stats[statType] = new CharacterStat(statType, 0f);
}
}
/// <summary>
/// 获取指定类型的属性容器。
/// </summary>
public CharacterStat Get(StatType statType)
{
if (!_stats.TryGetValue(statType, out CharacterStat stat))
{
stat = new CharacterStat(statType, 0f);
_stats.Add(statType, stat);
}
return stat;
}
/// <summary>
/// 获取指定属性的当前最终计算值(带脏标记缓存与边界钳制)。
/// </summary>
public float GetValue(StatType statType)
{
return Get(statType).Value;
}
/// <summary>
/// 设置指定属性的基础值。
/// </summary>
public void SetBaseValue(StatType statType, float baseValue)
{
CharacterStat stat = Get(statType);
float oldValue = stat.Value;
stat.BaseValue = baseValue;
float newValue = stat.Value;
if (Math.Abs(oldValue - newValue) > 1e-6f)
{
OnStatChanged?.Invoke(statType, newValue);
}
}
/// <summary>
/// 添加属性修饰器。
/// </summary>
public void AddModifier(StatType statType, StatModifier modifier)
{
if (modifier == null)
{
throw new ArgumentNullException(nameof(modifier));
}
CharacterStat stat = Get(statType);
stat.AddModifier(modifier);
OnStatChanged?.Invoke(statType, stat.Value);
}
/// <summary>
/// 移除属性修饰器。
/// </summary>
public bool RemoveModifier(StatType statType, StatModifier modifier)
{
if (modifier == null)
{
return false;
}
CharacterStat stat = Get(statType);
bool removed = stat.RemoveModifier(modifier);
if (removed)
{
OnStatChanged?.Invoke(statType, stat.Value);
}
return removed;
}
/// <summary>
/// 添加 Luban 导出的 modifier(自动转换为 StatModifier)。
/// </summary>
public void AddModifier(StatModifierConfig lubanMod, object source = null)
{
if (lubanMod == null)
{
throw new ArgumentNullException(nameof(lubanMod));
}
StatModifier statMod = StatModifier.FromLuban(lubanMod, source);
AddModifier(lubanMod.StatType, statMod);
}
/// <summary>
/// 移除由 Luban 导出的 modifier 所生成的对应修饰器。
/// </summary>
public bool RemoveModifier(StatModifierConfig lubanMod, object source = null)
{
if (lubanMod == null)
{
return false;
}
CharacterStat stat = Get(lubanMod.StatType);
float targetValue = lubanMod.Value;
ModifierType targetType = lubanMod.ModifierType;
IReadOnlyList<StatModifier> modifiers = stat.Modifiers;
for (int i = modifiers.Count - 1; i >= 0; i--)
{
StatModifier m = modifiers[i];
if (m.Type == targetType &&
Math.Abs(m.Value - targetValue) < 1e-6f &&
(source == null || ReferenceEquals(m.Source, source) || Equals(m.Source, source)))
{
return RemoveModifier(lubanMod.StatType, m);
}
}
return false;
}
/// <summary>
/// 一键移除所有属性中指定来源(道具、武器、羁绊、Buff 等)的所有修饰器。
/// </summary>
/// <param name="source">来源对象引用</param>
/// <returns>移除的修饰器总数</returns>
public int RemoveAllModifiersFromSource(object source)
{
if (source == null)
{
return 0;
}
int totalRemoved = 0;
foreach (KeyValuePair<StatType, CharacterStat> pair in _stats)
{
int removed = pair.Value.RemoveAllModifiersFromSource(source);
if (removed > 0)
{
totalRemoved += removed;
OnStatChanged?.Invoke(pair.Key, pair.Value.Value);
}
}
return totalRemoved;
}
/// <summary>
/// 清空所有属性的修饰器并标记为脏。
/// </summary>
public void Clear()
{
foreach (KeyValuePair<StatType, CharacterStat> pair in _stats)
{
pair.Value.ClearModifiers();
OnStatChanged?.Invoke(pair.Key, pair.Value.Value);
}
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 312b0c5905f501449b3dc700f4ac0e5b
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,180 @@
using System;
using System.Collections.Generic;
using SepCore.Definition;
using UnityEngine;
namespace SepCore.Battle
{
/// <summary>
/// 单个属性实例容器。
/// 维护基础值、修饰器列表,通过脏标记(Dirty Flag)缓存计算结果,并基于 StatConfig 进行边界钳制。
/// </summary>
public sealed class CharacterStat
{
private readonly List<StatModifier> _modifiers = new();
private float _baseValue;
private bool _isDirty = true;
private float _cachedValue;
private StatConfig _config;
public StatType Type { get; }
public StatConfig Config
{
get
{
if (_config == null && GameEntry.Luban != null && GameEntry.Luban.IsReady)
{
_config = GameEntry.Luban.Get<StatConfig>((int)Type);
}
return _config;
}
set
{
_config = value;
_isDirty = true;
}
}
public float BaseValue
{
get => _baseValue;
set
{
if (Math.Abs(_baseValue - value) > 1e-6f)
{
_baseValue = value;
_isDirty = true;
}
}
}
public float Value
{
get
{
if (_isDirty)
{
_cachedValue = CalculateFinalValue();
_isDirty = false;
}
return _cachedValue;
}
}
public IReadOnlyList<StatModifier> Modifiers => _modifiers;
public CharacterStat(StatType type, float baseValue = 0f, StatConfig config = null)
{
Type = type;
_baseValue = baseValue;
_config = config;
_isDirty = true;
}
public void AddModifier(StatModifier modifier)
{
if (modifier == null)
{
throw new ArgumentNullException(nameof(modifier));
}
_modifiers.Add(modifier);
_isDirty = true;
}
public bool RemoveModifier(StatModifier modifier)
{
if (modifier == null)
{
return false;
}
if (_modifiers.Remove(modifier))
{
_isDirty = true;
return true;
}
return false;
}
public int RemoveAllModifiersFromSource(object source)
{
if (source == null)
{
return 0;
}
int removed = _modifiers.RemoveAll(m => ReferenceEquals(m.Source, source) || Equals(m.Source, source));
if (removed > 0)
{
_isDirty = true;
}
return removed;
}
public void ClearModifiers()
{
if (_modifiers.Count > 0)
{
_modifiers.Clear();
_isDirty = true;
}
}
public void MarkDirty()
{
_isDirty = true;
}
private float CalculateFinalValue()
{
float sumFlat = 0f;
float sumPercentAdd = 0f;
float prodPercentMult = 1f;
for (int i = 0; i < _modifiers.Count; i++)
{
StatModifier mod = _modifiers[i];
switch (mod.Type)
{
case ModifierType.Flat:
sumFlat += mod.Value;
break;
case ModifierType.PercentAdd:
sumPercentAdd += mod.Value;
break;
case ModifierType.PercentMult:
prodPercentMult *= mod.Value;
break;
}
}
float finalValue = ((_baseValue + sumFlat) * (1f + sumPercentAdd)) * prodPercentMult;
return ClampStat(finalValue);
}
private float ClampStat(float value)
{
StatConfig config = Config;
if (config != null)
{
if (config.HasMin && value < config.MinValue)
{
value = config.MinValue;
}
if (config.HasMax && value > config.MaxValue)
{
value = config.MaxValue;
}
}
return value;
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 67040be8b42ae744a81f961aa4ec5333
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,56 @@
using SepCore.Definition;
namespace SepCore.Battle
{
/// <summary>
/// 伤害结算输入上下文。
/// 封装攻击方与受击方的属性管线、武器基础伤害、加成属性、暴击等参数。
/// </summary>
public struct DamageContext
{
/// <summary>
/// 基础伤害(武器攻击力或敌人基础伤害)。
/// </summary>
public int BaseDamage;
/// <summary>
/// 伤害加成属性(近战伤害/远程伤害/元素伤害/None)。
/// </summary>
public StatType DamageScalingStat;
/// <summary>
/// 伤害加成系数,默认 1.0f。
/// </summary>
public float ScalingRatio;
/// <summary>
/// 基础暴击率(例如来自武器或全局配置)。
/// </summary>
public float BaseCritChance;
/// <summary>
/// 暴击伤害倍率(例如 1.5f 或 2.0f)。
/// </summary>
public float CritMultiplier;
/// <summary>
/// 攻击方属性修饰管线(可为 null,如普通怪物未挂载管线)。
/// </summary>
public IAttributeProvider AttackerPipeline;
/// <summary>
/// 防御方属性修饰管线(可为 null,如普通怪物未挂载管线)。
/// </summary>
public IAttributeProvider DefenderPipeline;
/// <summary>
/// 防御方备用护甲(当 DefenderPipeline 为 null 时生效)。
/// </summary>
public float FallbackDefenderArmor;
/// <summary>
/// 防御方备用闪避率(当 DefenderPipeline 为 null 时生效)。
/// </summary>
public float FallbackDefenderDodge;
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 8192a0b127344919ba1812836248cc01
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,134 @@
using SepCore.Definition;
using UnityEngine;
namespace SepCore.Battle
{
/// <summary>
/// 伤害结算管线(纯 C# 静态计算器)。
/// 实现武器类型加成、百分比放大、闪避判定、暴击判定、双曲护甲折减、保底伤害与生命偷取判定。
/// </summary>
public static class DamagePipeline
{
public const float ArmorDivisor = 15f; // Brotato 双曲护甲基数
public const float DefaultCritMultiplier = 1.5f;
public const float DefaultBaseCrit = 0.05f;
/// <summary>
/// 计算最终伤害结果。
/// </summary>
public static DamageResult Calculate(in DamageContext context)
{
// 1. 闪避判定 (Defender Dodge)
float dodgeChance = context.DefenderPipeline != null
? context.DefenderPipeline.GetValue(StatType.Dodge)
: context.FallbackDefenderDodge;
float maxDodge = 0.8f;
if (GameEntry.Luban != null && GameEntry.Luban.IsReady)
{
maxDodge = GameEntry.Luban.Global.MaxDodgeRate / 1000f;
}
dodgeChance = Mathf.Clamp(dodgeChance, 0f, maxDodge);
if (dodgeChance > 0f && Random.value < dodgeChance)
{
return new DamageResult
{
Damage = 0,
IsDodge = true,
IsCrit = false,
IsLifeSteal = false,
LifeStealAmount = 0
};
}
// 2. 武器类型伤害加成 (Melee / Ranged / Elemental)
float rawDamage = context.BaseDamage;
if (context.AttackerPipeline != null && context.DamageScalingStat != StatType.None)
{
float scalingBonus = context.AttackerPipeline.GetValue(context.DamageScalingStat);
float ratio = context.ScalingRatio > 0f ? context.ScalingRatio : 1f;
rawDamage += scalingBonus * ratio;
}
rawDamage = Mathf.Max(rawDamage, 1f);
// 3. 全局百分比增伤放大 (DamagePercent)
float damagePercent = context.AttackerPipeline != null
? context.AttackerPipeline.GetValue(StatType.DamagePercent)
: 0f;
float amplifiedDamage = rawDamage * (1f + damagePercent);
amplifiedDamage = Mathf.Max(amplifiedDamage, 1f);
// 4. 暴击判定 (CritChance & CritMultiplier)
float attackerCrit = context.AttackerPipeline != null
? context.AttackerPipeline.GetValue(StatType.CritChance)
: 0f;
float baseCrit = context.BaseCritChance;
if (baseCrit <= 0f && GameEntry.Luban != null && GameEntry.Luban.IsReady)
{
baseCrit = GameEntry.Luban.Global.WeaponBaseCrit;
}
float totalCritChance = Mathf.Clamp(baseCrit + attackerCrit, 0f, 0.9f);
bool isCrit = totalCritChance > 0f && Random.value < totalCritChance;
float critMultiplier = context.CritMultiplier;
if (critMultiplier <= 0f)
{
critMultiplier = GameEntry.Luban != null && GameEntry.Luban.IsReady
? GameEntry.Luban.Global.CritMultiplier
: DefaultCritMultiplier;
}
float damageAfterCrit = isCrit ? (amplifiedDamage * critMultiplier) : amplifiedDamage;
// 5. Brotato 标准双曲护甲折减 (Armor Mitigation)
float armor = context.DefenderPipeline != null
? context.DefenderPipeline.GetValue(StatType.Armor)
: context.FallbackDefenderArmor;
float mitigation;
if (armor >= 0f)
{
mitigation = 1f / (1f + armor / ArmorDivisor);
}
else
{
// 负护甲易伤放大: 2 - 1/(1 - armor/15)
mitigation = 2f - (1f / (1f - armor / ArmorDivisor));
}
float finalDamageFloat = damageAfterCrit * mitigation;
int minDamage = 1;
if (GameEntry.Luban != null && GameEntry.Luban.IsReady)
{
minDamage = GameEntry.Luban.Global.MinDamage;
}
int finalDamage = Mathf.Max(Mathf.CeilToInt(finalDamageFloat), minDamage);
// 6. 生命偷取判定 (LifeSteal)
bool isLifeSteal = false;
int lifeStealAmount = 0;
if (finalDamage > 0 && context.AttackerPipeline != null)
{
float lifeStealRate = context.AttackerPipeline.GetValue(StatType.LifeSteal);
if (lifeStealRate > 0f && Random.value < lifeStealRate)
{
isLifeSteal = true;
lifeStealAmount = 1;
}
}
return new DamageResult
{
Damage = finalDamage,
IsDodge = false,
IsCrit = isCrit,
IsLifeSteal = isLifeSteal,
LifeStealAmount = lifeStealAmount
};
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: a394c2d349564121dc3834058460ee23
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,33 @@
namespace SepCore.Battle
{
/// <summary>
/// 伤害计算输出结果。
/// </summary>
public struct DamageResult
{
/// <summary>
/// 最终扣除的生命值(若闪避则为 0)。
/// </summary>
public int Damage;
/// <summary>
/// 是否闪避。
/// </summary>
public bool IsDodge;
/// <summary>
/// 是否暴击。
/// </summary>
public bool IsCrit;
/// <summary>
/// 是否触发生命偷取。
/// </summary>
public bool IsLifeSteal;
/// <summary>
/// 偷取回复的生命值(通常为 1)。
/// </summary>
public int LifeStealAmount;
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 9283b1c238454920cb2923947359dd12
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -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);
}
}
}
@@ -28,12 +28,12 @@ namespace SepCore.DamageText
_instancePoolCapacity);
}
public void ShowDamage(Vector3 worldPosition, int damage)
public void ShowDamage(Vector3 worldPosition, int damage, bool isCrit = false, bool isDodge = false)
{
if (damage <= 0) return;
if (damage <= 0 && !isDodge) return;
DamageTextItem item = CreateDamageTextItem();
item.Show(worldPosition, damage, _canvas, OnItemComplete);
item.Show(worldPosition, damage, isCrit, isDodge, _canvas, OnItemComplete);
_activeDamageTextItems.Add(item);
}
@@ -12,6 +12,8 @@ namespace SepCore.DamageText
private const float FadeDelay = 0.2f;
private const float FadeDuration = 0.35f;
private static readonly Color NormalColor = new Color(1f, 0.32f, 0.23f, 1f);
private static readonly Color CritColor = new Color(1f, 0.85f, 0.15f, 1f);
private static readonly Color DodgeColor = new Color(0.75f, 0.75f, 0.75f, 1f);
[SerializeField] private RectTransform _cachedTransform = null;
@@ -22,7 +24,7 @@ namespace SepCore.DamageText
private Sequence _sequence;
private Action<DamageTextItem> _onComplete;
public void Show(Vector3 worldPosition, int damage, Canvas canvas, Action<DamageTextItem> onComplete)
public void Show(Vector3 worldPosition, int damage, bool isCrit, bool isDodge, Canvas canvas, Action<DamageTextItem> onComplete)
{
_onComplete = onComplete;
gameObject.SetActive(true);
@@ -30,8 +32,27 @@ namespace SepCore.DamageText
_canvasGroup.alpha = 1f;
_cachedTransform.localScale = Vector3.one;
_text.color = NormalColor;
_text.text = damage.ToString();
Vector3 punchScale;
if (isDodge)
{
_text.color = DodgeColor;
_text.text = "MISS";
punchScale = new Vector3(0.1f, 0.1f, 0f);
}
else if (isCrit)
{
_text.color = CritColor;
_text.text = damage.ToString() + "!";
_cachedTransform.localScale = new Vector3(1.3f, 1.3f, 1f);
punchScale = new Vector3(0.35f, 0.35f, 0f);
}
else
{
_text.color = NormalColor;
_text.text = damage.ToString();
punchScale = new Vector3(0.15f, 0.15f, 0f);
}
Vector3 screenPos = GameEntry.Scene.MainCamera.WorldToScreenPoint(worldPosition);
RectTransformUtility.ScreenPointToLocalPointInRectangle((RectTransform)canvas.transform, screenPos,
@@ -42,11 +63,16 @@ namespace SepCore.DamageText
_sequence = DOTween.Sequence();
_sequence.Append(_cachedTransform.DOAnchorPosY(localPos.y + RiseDistance, RiseDuration)
.SetEase(Ease.OutQuad));
_sequence.Join(_cachedTransform.DOPunchScale(new Vector3(0.15f, 0.15f, 0f), 0.18f, 6, 0.75f));
_sequence.Join(_cachedTransform.DOPunchScale(punchScale, 0.18f, 6, 0.75f));
_sequence.Join(_canvasGroup.DOFade(0f, FadeDuration).SetDelay(FadeDelay));
_sequence.OnComplete(() => _onComplete(this));
}
public void Show(Vector3 worldPosition, int damage, Canvas canvas, Action<DamageTextItem> onComplete)
{
Show(worldPosition, damage, false, false, canvas, onComplete);
}
public void ResetItem()
{
KillSequence();
@@ -35,7 +35,8 @@ namespace SepCore.CustomComponent
"tbspriteconfig",
"tbuiformconfig",
"tbuisoundconfig",
"tbweaponconfig"
"tbweaponconfig",
"tbstatconfig"
};
private sealed class TableAccessor
@@ -133,6 +134,11 @@ namespace SepCore.CustomComponent
Accessor(tables => id => tables.TbWeaponConfig.GetOrDefault((WeaponType)id),
tables => tables.TbWeaponConfig.DataList)
},
{
typeof(StatConfig),
Accessor(tables => id => tables.TbStatConfig.GetOrDefault((StatType)id),
tables => tables.TbStatConfig.DataList)
},
};
private static TableAccessor Accessor<TValue>(Func<Tables, Func<int, object>> rowGetterFactory,
@@ -1,3 +1,4 @@
using SepCore.Battle;
using SepCore.Components;
using SepCore.Debugger;
using SepCore.CustomUtility;
@@ -143,16 +144,22 @@ namespace SepCore.Simulation
return false;
}
damage = AIUtility.CalcDamageHP(sourceImpact.AttackBase, sourceImpact.AttackStat,
targetImpact.DefenseStat,
targetImpact.DodgeStat);
shouldDispatchPresentation = true;
if (damage <= 0)
DamageContext context = new DamageContext
{
return true;
}
BaseDamage = sourceImpact.AttackBase,
DamageScalingStat = sourceImpact.DamageType.ToScalingStat(),
ScalingRatio = sourceImpact.ScalingRatio,
BaseCritChance = sourceImpact.BaseCritChance,
CritMultiplier = sourceImpact.CritMultiplier,
AttackerPipeline = sourceImpact.AttributeProvider,
DefenderPipeline = targetImpact.AttributeProvider
};
target.ApplyDamage(attacker ?? sourceEntity ?? ownerEntity, damage);
DamageResult result = DamagePipeline.Calculate(in context);
damage = result.Damage;
shouldDispatchPresentation = true;
target.ApplyDamage(attacker ?? sourceEntity ?? ownerEntity, in result, hitPosition);
return true;
}
@@ -18,6 +18,7 @@ namespace SepCore.Simulation
[SerializeField]
private bool _projectileHitMarkerEnabled = true;
#pragma warning disable CS0414
[Tooltip("命中标记尺寸。")][SerializeField] private float _projectileHitMarkerSize = 0.2f;
[Tooltip("命中标记在目标上的高度偏移。")]
@@ -27,6 +28,7 @@ namespace SepCore.Simulation
[Tooltip("命中标记持续时间。")]
[SerializeField]
private float _projectileHitMarkerDuration = 0.15f;
#pragma warning restore CS0414
[Tooltip("命中标记颜色。")][SerializeField] private Color _projectileHitMarkerColor = new(1f, 0f, 0f, 0.95f);
@@ -1,3 +1,4 @@
using SepCore.Battle;
using SepCore.CustomUtility;
using SepCore.Definition;
using UnityEngine;
@@ -145,9 +146,19 @@ namespace SepCore.Entity
return;
}
int damage = AIUtility.CalcDamageHP(AttackDamage, null, targetImpactData.DefenseStat,
targetImpactData.DodgeStat);
_targetableTarget.ApplyDamage(this, damage);
DamageContext context = new DamageContext
{
BaseDamage = AttackDamage,
DamageScalingStat = StatType.None,
ScalingRatio = 1.0f,
BaseCritChance = 0f,
CritMultiplier = 0f,
AttackerPipeline = selfImpactData.AttributeProvider,
DefenderPipeline = targetImpactData.AttributeProvider
};
DamageResult result = DamagePipeline.Calculate(in context);
_targetableTarget.ApplyDamage(this, in result, _targetableTarget.CachedTransform.position);
}
}
}
@@ -169,7 +169,7 @@ namespace SepCore.Entity
AddWeaponAsync(EntityDataFactory.Create(GameEntry.Entity.NextId(), WeaponType.Knife, Id, _playerData.Camp)).Forget();
// StatComponent
foreach (var modifier in role.Modifiers)
foreach (var modifier in role.StatModifierConfigs)
{
_statComponent.AddModifier(modifier);
}
@@ -189,7 +189,7 @@ namespace SepCore.Entity
public override ImpactData GetImpactData()
{
return new ImpactData(_playerData.Camp, 0, null, _healthComponent.DefenseStat, _healthComponent.DodgeStat);
return new ImpactData(_playerData.Camp, 0, _statComponent.Pipeline);
}
protected override void OnInit(object userData)
@@ -1,4 +1,5 @@
using SepCore.Components;
using SepCore.Battle;
using SepCore.Components;
using SepCore.Definition;
using SepCore.CustomUtility;
using UnityEngine;
@@ -18,22 +19,54 @@ namespace SepCore.Entity
public abstract ImpactData GetImpactData();
public void ApplyDamage(EntityBase attacker, int damageHP)
public void ApplyDamage(EntityBase attacker, in DamageResult damageResult, Vector3? hitPosition = null)
{
if (damageResult.IsDodge)
{
_healthComponent.TakeDamage(0, false, true, hitPosition);
return;
}
float fromHPRatio = _healthComponent.HealthRatio;
_healthComponent.TakeDamage(damageHP);
_healthComponent.TakeDamage(damageResult.Damage, damageResult.IsCrit, false, hitPosition);
float toHPRatio = _healthComponent.HealthRatio;
if (fromHPRatio > toHPRatio)
{
GameEntry.HPBar.ShowHPBar(this, fromHPRatio, toHPRatio);
}
if (damageResult.IsLifeSteal && attacker != null)
{
HealthComponent attackerHealth = attacker.GetComponent<HealthComponent>();
if (attackerHealth == null && attacker is WeaponBase weapon)
{
attackerHealth = weapon.GetComponentInParent<HealthComponent>();
}
if (attackerHealth != null)
{
attackerHealth.TryTriggerLifeSteal(damageResult.LifeStealAmount);
}
}
if (_healthComponent.CurrentHealth <= 0)
{
OnDead(attacker);
}
}
public void ApplyDamage(EntityBase attacker, int damageHP)
{
ApplyDamage(attacker, new DamageResult
{
Damage = damageHP,
IsDodge = false,
IsCrit = false,
IsLifeSteal = false,
LifeStealAmount = 0
});
}
protected override void OnInit(object userData)
{
base.OnInit(userData);
@@ -43,13 +43,7 @@ namespace SepCore.Entity
return false;
}
var simulationWorld = GameEntry.SimulationWorld;
if (simulationWorld == null)
{
return false;
}
if (!simulationWorld.TryGetNearestEnemyEntityId(weapon.CachedTransform.position, maxSqrRange,
if (!GameEntry.SimulationWorld.TryGetNearestEnemyEntityId(weapon.CachedTransform.position, maxSqrRange,
out int entityId))
{
return false;
@@ -27,16 +27,33 @@ namespace SepCore.Entity
protected EntityBase _target;
private float _sqrRange;
public EntityBase Target => _target;
public float SqrRange => _sqrRange;
public float CurrentCooldown
{
get
{
if (WeaponData == null) return 0f;
float attackSpeed = _ownerAttributeProvider?.GetValue(StatType.AttackSpeed) ?? 0f;
float speedFactor = Mathf.Max(0.2f, 1f + attackSpeed);
return Mathf.Max(0.02f, WeaponData.Cooldown / speedFactor);
}
}
protected StatProperty AttackStat { get; private set; } = new();
public float CurrentAttackRange
{
get
{
if (WeaponData == null) return 0f;
float rangeBonus = _ownerAttributeProvider?.GetValue(StatType.Range) ?? 0f;
float effectiveBonus = rangeBonus >= 50f ? rangeBonus / 1000f : rangeBonus;
return Mathf.Max(0.5f, WeaponData.AttackRange + effectiveBonus);
}
}
private StatComponent _attackStatComponent;
private System.Action<modifier, bool> _attackStatCallback;
public float SqrRange => CurrentAttackRange * CurrentAttackRange;
private IAttributeProvider _ownerAttributeProvider;
private static float _rotateSpeed => GameEntry.Luban.Global.WeaponRotateSpeed;
@@ -63,8 +80,7 @@ namespace SepCore.Entity
return;
}
StartAttackCooldown(WeaponData.Cooldown);
_sqrRange = WeaponData.AttackRange * WeaponData.AttackRange;
StartAttackCooldown(CurrentCooldown);
_cachedRotation = CachedTransform.rotation;
if (WeaponData.OwnerCamp == CampType.Player)
@@ -136,14 +152,33 @@ namespace SepCore.Entity
public virtual ImpactData GetImpactData()
{
return new ImpactData(WeaponData.OwnerCamp, WeaponData.Attack, AttackStat);
float baseCrit = GameEntry.Luban != null && GameEntry.Luban.IsReady
? GameEntry.Luban.Global.WeaponBaseCrit
: 0.05f;
float critMult = GameEntry.Luban != null && GameEntry.Luban.IsReady
? GameEntry.Luban.Global.CritMultiplier
: 1.5f;
return new ImpactData(
WeaponData.OwnerCamp,
WeaponData.Attack,
_ownerAttributeProvider,
WeaponData.DamageType,
1.0f,
baseCrit,
critMult);
}
public abstract void Attack();
public void ResetAttackCooldown()
{
ResetAttackCooldown(CurrentCooldown);
}
public virtual void Check()
{
_target = SelectTarget(_sqrRange);
_target = SelectTarget(SqrRange);
}
public virtual void RotateToTarget(float elapseSeconds)
@@ -272,30 +307,18 @@ namespace SepCore.Entity
protected void BindAttackStatFromOwner(EntityLogic parentEntity)
{
ReleaseAttackStatSubscription();
AttackStat = new StatProperty();
if (parentEntity is not Player player) return;
var statComponent = player.GetComponent<StatComponent>();
if (statComponent == null) return;
AttackStat = statComponent.GetStat(StatType.Attack);
_attackStatComponent = statComponent;
_attackStatCallback = (modifier, isApply) =>
statComponent.UpdateStat(AttackStat, modifier, isApply);
statComponent.Subscribe(StatType.Attack, _attackStatCallback);
_ownerAttributeProvider = statComponent.Pipeline;
}
protected void ReleaseAttackStatSubscription()
{
if (_attackStatComponent != null && _attackStatCallback != null)
{
_attackStatComponent.Unsubscribe(StatType.Attack, _attackStatCallback);
}
_attackStatComponent = null;
_attackStatCallback = null;
AttackStat = new StatProperty();
_ownerAttributeProvider = null;
}
}
}
@@ -55,7 +55,7 @@ namespace SepCore.Entity
Vector3 fireOrigin = CachedTransform.TransformPoint(_fireOriginOffset);
Vector3 directionToTarget = targetPosition - fireOrigin;
float maxDistance = WeaponData.AttackRange;
float maxDistance = CurrentAttackRange;
if (directionToTarget.sqrMagnitude > Mathf.Epsilon &&
Physics.Raycast(fireOrigin, directionToTarget.normalized, out RaycastHit hit, maxDistance, _hitMask,
QueryTriggerInteraction.Collide))
@@ -15,7 +15,7 @@ namespace SepCore.Entity
public override void OnEnter()
{
_weapon.ResetAttackCooldown(_weapon.WeaponData.Cooldown);
_weapon.ResetAttackCooldown(_weapon.CurrentCooldown);
_weapon.Attack();
}
@@ -42,7 +42,7 @@ namespace SepCore.Entity
_attackCenter = _target != null ? _target.CachedTransform.position : CachedTransform.position;
CachedTransform.SetParent(null);
Vector3 targetPos = CachedTransform.position + CachedTransform.forward * WeaponData.AttackRange;
Vector3 targetPos = CachedTransform.position + CachedTransform.forward * CurrentAttackRange;
_attackSequence = DOTween.Sequence();
_attackSequence.Append(CachedTransform.DOMove(targetPos, _attackDuration).SetEase(Ease.OutQuad));
_attackSequence.AppendCallback(() =>
@@ -15,7 +15,7 @@ namespace SepCore.Entity
public override void OnEnter()
{
_weapon.ResetAttackCooldown(_weapon.WeaponData.Cooldown);
_weapon.ResetAttackCooldown(_weapon.CurrentCooldown);
_weapon.Attack();
}
@@ -15,7 +15,7 @@ namespace SepCore.Entity
public override void OnEnter()
{
_weapon.ResetAttackCooldown(_weapon.WeaponData.Cooldown);
_weapon.ResetAttackCooldown(_weapon.CurrentCooldown);
_weapon.Attack();
}
@@ -15,7 +15,7 @@ namespace SepCore.Entity
public override void OnEnter()
{
_weapon.ResetAttackCooldown(_weapon.WeaponData.Cooldown);
_weapon.ResetAttackCooldown(_weapon.CurrentCooldown);
_weapon.Attack();
}
@@ -57,7 +57,7 @@ namespace SepCore.Entity
CachedTransform.DORotateQuaternion(endRotation, _attackDuration).SetEase(Ease.OutSine));
_attackSequence.InsertCallback(_attackDuration * 0.5f, () =>
{
_attackEffect.Play(this, _attackCenter, _target, WeaponData.AttackRange);
_attackEffect.Play(this, _attackCenter, _target, CurrentAttackRange);
ApplySectorDamage();
});
_attackSequence.Append(CachedTransform.DORotateQuaternion(_cachedAttackRotation, _returnDuration)
@@ -71,7 +71,7 @@ namespace SepCore.Entity
private void ApplySectorDamage()
{
float attackRadius = WeaponData.AttackRange;
float attackRadius = CurrentAttackRange;
if (attackRadius <= 0f) return;
Vector3 forward = CachedTransform.forward;
@@ -15,7 +15,7 @@ namespace SepCore.Entity
public override void OnEnter()
{
_weapon.ResetAttackCooldown(_weapon.WeaponData.Cooldown);
_weapon.ResetAttackCooldown(_weapon.CurrentCooldown);
_weapon.Attack();
}
@@ -125,7 +125,7 @@ namespace SepCore.UI
private void ApplyReward(LevelUpRewardConfig reward)
{
_player.AddProp(new PropItem(reward.Modifiers, reward.Rarity, reward.Title, reward.Sprite));
_player.AddProp(new PropItem(reward.StatModifierConfigs, reward.Rarity, reward.Title, reward.Sprite));
}
private void BuildRewardRarityPools()
@@ -76,7 +76,7 @@ namespace SepCore.UI
selectedRoleData = new ShowAreaRawData
{
Name = selectedRole.Name,
InitialProperties = selectedRole.Modifiers
InitialProperties = selectedRole.StatModifierConfigs
};
}
}
@@ -6,6 +6,6 @@ namespace SepCore.UI
public class ShowAreaRawData
{
public string Name;
public List<modifier> InitialProperties;
public List<StatModifierConfig> InitialProperties;
}
}
@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using SepCore.Battle;
using SepCore.Definition;
using SepCore.Entity;
using UnityEngine;
@@ -209,10 +210,19 @@ namespace SepCore.CustomUtility
return;
}
int entityDamageHP = CalcDamageHP(projectileImpactData.AttackBase, projectileImpactData.AttackStat,
entityImpactData.DefenseStat, entityImpactData.DodgeStat);
DamageContext context = new DamageContext
{
BaseDamage = projectileImpactData.AttackBase,
DamageScalingStat = projectileImpactData.DamageType.ToScalingStat(),
ScalingRatio = projectileImpactData.ScalingRatio,
BaseCritChance = projectileImpactData.BaseCritChance,
CritMultiplier = projectileImpactData.CritMultiplier,
AttackerPipeline = projectileImpactData.AttributeProvider,
DefenderPipeline = entityImpactData.AttributeProvider
};
entity.ApplyDamage(enemyProjectile, entityDamageHP);
DamageResult result = DamagePipeline.Calculate(in context);
entity.ApplyDamage(enemyProjectile, in result, enemyProjectile.CachedTransform.position);
enemyProjectile.Expire();
return;
}
@@ -228,43 +238,23 @@ namespace SepCore.CustomUtility
return;
}
int entityDamageHP = CalcDamageHP(weaponImpactData.AttackBase, weaponImpactData.AttackStat,
entityImpactData.DefenseStat, entityImpactData.DodgeStat);
DamageContext context = new DamageContext
{
BaseDamage = weaponImpactData.AttackBase,
DamageScalingStat = weaponImpactData.DamageType.ToScalingStat(),
ScalingRatio = weaponImpactData.ScalingRatio,
BaseCritChance = weaponImpactData.BaseCritChance,
CritMultiplier = weaponImpactData.CritMultiplier,
AttackerPipeline = weaponImpactData.AttributeProvider,
DefenderPipeline = entityImpactData.AttributeProvider
};
entity.ApplyDamage(weapon, entityDamageHP);
DamageResult result = DamagePipeline.Calculate(in context);
entity.ApplyDamage(weapon, in result, entity.CachedTransform.position);
return;
}
}
public static int CalcDamageHP(int attack, StatProperty attackStat, StatProperty defenseStat,
StatProperty dodgeStat)
{
// 1. 处理闪避(从全局表读取闪避率上限)
float maxDodge = GameEntry.Luban.Global.MaxDodgeRate / 1000f;
if (dodgeStat != null)
{
if (Random.value < Mathf.Clamp(dodgeStat.Value, 0f, maxDodge)) return 0;
}
// 2. 处理攻击加成 最终伤害 = (基础伤害 + 伤害提升固定值) * 伤害提升率
float damage = attack;
if (attackStat != null)
{
damage = (attack + attackStat.Value) * attackStat.Percent;
}
// 3. 处理防御加成 最终伤害 = (基础伤害 - 防御提升固定值) / 防御提升率
if (defenseStat != null)
{
damage = (damage - defenseStat.Value) / defenseStat.Percent;
}
// 4. 将伤害限制为保底伤害,避免堆防御导致无法造成伤害
int minDamage = GameEntry.Luban.Global.MinDamage;
if (damage < minDamage) return minDamage;
return Mathf.CeilToInt(damage);
}
[StructLayout(LayoutKind.Auto)]
private struct CampPair
{