引入属性、伤害管线
This commit is contained in:
@@ -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:
|
||||
Reference in New Issue
Block a user