引入属性、伤害管线
This commit is contained in:
@@ -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,
|
||||
|
||||
+15
-8
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
+2
@@ -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);
|
||||
|
||||
+1
-7
@@ -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))
|
||||
|
||||
+1
-1
@@ -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(() =>
|
||||
|
||||
+1
-1
@@ -15,7 +15,7 @@ namespace SepCore.Entity
|
||||
|
||||
public override void OnEnter()
|
||||
{
|
||||
_weapon.ResetAttackCooldown(_weapon.WeaponData.Cooldown);
|
||||
_weapon.ResetAttackCooldown(_weapon.CurrentCooldown);
|
||||
_weapon.Attack();
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -15,7 +15,7 @@ namespace SepCore.Entity
|
||||
|
||||
public override void OnEnter()
|
||||
{
|
||||
_weapon.ResetAttackCooldown(_weapon.WeaponData.Cooldown);
|
||||
_weapon.ResetAttackCooldown(_weapon.CurrentCooldown);
|
||||
_weapon.Attack();
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -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;
|
||||
|
||||
+1
-1
@@ -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
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user