feat: 허수아비 계산 시뮬레이터 추가
- 빌드 입력, 룰셋, 회전 정책, 결과/리포트 모델을 포함한 데미지 계산 시뮬레이터 기반을 추가 - 단일 실행 창과 배치 전수 조사 창, 플레이어 데미지 스윕 메뉴를 추가 - DamageEffect 계산값 접근자를 열어 기존 전투 공식을 시뮬레이터에서 재사용하도록 정리
This commit is contained in:
607
Assets/_Game/Scripts/Combat/Simulation/BuildSimulationEngine.cs
Normal file
607
Assets/_Game/Scripts/Combat/Simulation/BuildSimulationEngine.cs
Normal file
@@ -0,0 +1,607 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
using UnityEngine;
|
||||
|
||||
using Colosseum.Passives;
|
||||
using Colosseum.Skills;
|
||||
using Colosseum.Skills.Effects;
|
||||
using Colosseum.Stats;
|
||||
using Colosseum.Weapons;
|
||||
|
||||
namespace Colosseum.Combat.Simulation
|
||||
{
|
||||
/// <summary>
|
||||
/// 허수아비 계산 시뮬레이터의 단일 대상 피해 계산 엔진입니다.
|
||||
/// </summary>
|
||||
public static class BuildSimulationEngine
|
||||
{
|
||||
private const float MinimumActionDuration = 0.01f;
|
||||
private const string UnsupportedEffectsWarning = "현재 MVP는 DamageEffect만 계산합니다.";
|
||||
|
||||
private sealed class SimulationContext
|
||||
{
|
||||
public GameObject actor;
|
||||
public CharacterStats stats;
|
||||
public PassiveRuntimeController passiveController;
|
||||
public float currentMana;
|
||||
public float maxMana;
|
||||
public float totalDamage;
|
||||
public float totalManaUsed;
|
||||
public float currentTime;
|
||||
public float firstCycleEndTime = -1f;
|
||||
}
|
||||
|
||||
private sealed class SkillRuntimeSnapshot
|
||||
{
|
||||
public int slotIndex;
|
||||
public SkillLoadoutEntry loadoutEntry;
|
||||
public SkillData skill;
|
||||
public string skillName;
|
||||
public float castDuration;
|
||||
public float cooldown;
|
||||
public float manaCost;
|
||||
public List<DamageEventSnapshot> damageEvents = new List<DamageEventSnapshot>();
|
||||
public bool containsUnsupportedEffects;
|
||||
}
|
||||
|
||||
private sealed class DamageEventSnapshot
|
||||
{
|
||||
public float relativeTime;
|
||||
public float damageAmount;
|
||||
}
|
||||
|
||||
private sealed class SkillMetrics
|
||||
{
|
||||
public string skillName;
|
||||
public int castCount;
|
||||
public float totalDamage;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 입력 조건에 따라 허수아비 계산 시뮬레이션을 실행합니다.
|
||||
/// </summary>
|
||||
public static SimulationResult Run(
|
||||
BuildSimulationInput input,
|
||||
SimulationRuleSet ruleSet,
|
||||
RotationPolicy rotationPolicy)
|
||||
{
|
||||
SimulationResult result = new SimulationResult();
|
||||
List<string> warnings = new List<string>();
|
||||
|
||||
if (input == null)
|
||||
{
|
||||
warnings.Add("BuildSimulationInput이 없습니다.");
|
||||
result.FinalizeResult("입력 없음", string.Empty, string.Empty, 0f, 0f, 0f, -1f, new List<SimulationSkillBreakdown>(), warnings);
|
||||
return result;
|
||||
}
|
||||
|
||||
IReadOnlyList<SkillLoadoutEntry> resolvedEntries = input.ResolveLoadoutEntries();
|
||||
if (resolvedEntries == null || resolvedEntries.Count == 0 || !input.HasAnyResolvedSkill())
|
||||
{
|
||||
warnings.Add("유효한 스킬 슬롯이 없습니다.");
|
||||
result.FinalizeResult(input.BuildLabel, ruleSet != null ? ruleSet.RuleName : string.Empty, rotationPolicy != null ? rotationPolicy.PolicyName : string.Empty, 0f, 0f, 0f, -1f, new List<SimulationSkillBreakdown>(), warnings);
|
||||
return result;
|
||||
}
|
||||
|
||||
ruleSet ??= new SimulationRuleSet();
|
||||
rotationPolicy ??= new RotationPolicy();
|
||||
|
||||
SimulationContext context = CreateContext(input, warnings);
|
||||
try
|
||||
{
|
||||
SkillRuntimeSnapshot[] skillSnapshots = BuildSnapshots(input, context, ruleSet, warnings);
|
||||
Dictionary<int, float> cooldownReadyTimes = new Dictionary<int, float>();
|
||||
Dictionary<int, SkillMetrics> metricsBySlot = new Dictionary<int, SkillMetrics>();
|
||||
HashSet<int> cycleCompletedSlots = new HashSet<int>();
|
||||
List<int> validPrioritySlots = CollectValidPrioritySlots(rotationPolicy, skillSnapshots);
|
||||
|
||||
while (context.currentTime < ruleSet.DurationSeconds)
|
||||
{
|
||||
SkillRuntimeSnapshot nextSkill = TrySelectSkill(skillSnapshots, cooldownReadyTimes, context, rotationPolicy);
|
||||
if (nextSkill == null)
|
||||
{
|
||||
if (!TryAdvanceIdleTime(skillSnapshots, cooldownReadyTimes, context, ruleSet, rotationPolicy, warnings))
|
||||
break;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
float castStartTime = context.currentTime;
|
||||
context.currentMana = Mathf.Max(0f, context.currentMana - nextSkill.manaCost);
|
||||
context.totalManaUsed += nextSkill.manaCost;
|
||||
cooldownReadyTimes[nextSkill.slotIndex] = castStartTime + nextSkill.cooldown;
|
||||
|
||||
if (!metricsBySlot.TryGetValue(nextSkill.slotIndex, out SkillMetrics skillMetrics))
|
||||
{
|
||||
skillMetrics = new SkillMetrics
|
||||
{
|
||||
skillName = nextSkill.skillName,
|
||||
};
|
||||
metricsBySlot.Add(nextSkill.slotIndex, skillMetrics);
|
||||
}
|
||||
|
||||
skillMetrics.castCount++;
|
||||
|
||||
for (int i = 0; i < nextSkill.damageEvents.Count; i++)
|
||||
{
|
||||
DamageEventSnapshot damageEvent = nextSkill.damageEvents[i];
|
||||
float eventTime = castStartTime + damageEvent.relativeTime;
|
||||
if (eventTime > ruleSet.DurationSeconds)
|
||||
continue;
|
||||
|
||||
context.totalDamage += damageEvent.damageAmount;
|
||||
skillMetrics.totalDamage += damageEvent.damageAmount;
|
||||
}
|
||||
|
||||
if (validPrioritySlots.Contains(nextSkill.slotIndex))
|
||||
{
|
||||
cycleCompletedSlots.Add(nextSkill.slotIndex);
|
||||
if (context.firstCycleEndTime < 0f && cycleCompletedSlots.Count >= validPrioritySlots.Count)
|
||||
{
|
||||
context.firstCycleEndTime = Mathf.Min(ruleSet.DurationSeconds, castStartTime + nextSkill.castDuration);
|
||||
}
|
||||
}
|
||||
|
||||
AdvanceTime(context, Mathf.Min(ruleSet.DurationSeconds, castStartTime + nextSkill.castDuration), ruleSet);
|
||||
}
|
||||
|
||||
result.FinalizeResult(
|
||||
input.BuildLabel,
|
||||
ruleSet.RuleName,
|
||||
rotationPolicy.PolicyName,
|
||||
ruleSet.DurationSeconds,
|
||||
context.totalDamage,
|
||||
context.totalManaUsed,
|
||||
context.firstCycleEndTime,
|
||||
BuildBreakdowns(metricsBySlot),
|
||||
warnings);
|
||||
|
||||
return result;
|
||||
}
|
||||
finally
|
||||
{
|
||||
DestroyContext(context);
|
||||
}
|
||||
}
|
||||
|
||||
private static SimulationContext CreateContext(BuildSimulationInput input, List<string> warnings)
|
||||
{
|
||||
SimulationContext context = new SimulationContext
|
||||
{
|
||||
actor = new GameObject("BuildSimulationActor")
|
||||
{
|
||||
hideFlags = HideFlags.HideAndDontSave,
|
||||
},
|
||||
};
|
||||
|
||||
context.stats = context.actor.AddComponent<CharacterStats>();
|
||||
context.passiveController = context.actor.AddComponent<PassiveRuntimeController>();
|
||||
context.passiveController.Initialize(context.stats);
|
||||
|
||||
ApplyBaseStats(context.stats, input);
|
||||
ApplyWeaponStats(context.stats, input.Weapon);
|
||||
|
||||
PassiveTreeData resolvedTree = input.ResolvePassiveTree();
|
||||
List<string> selectedNodeIds = input.BuildSelectedPassiveNodeIdList();
|
||||
if (resolvedTree != null && selectedNodeIds.Count > 0)
|
||||
{
|
||||
bool applied = context.passiveController.TryApplySelection(
|
||||
resolvedTree,
|
||||
selectedNodeIds,
|
||||
input.PassivePreset != null ? input.PassivePreset.PresetName : "직접구성",
|
||||
out string reason);
|
||||
|
||||
if (!applied && !string.IsNullOrWhiteSpace(reason))
|
||||
warnings.Add($"패시브 적용 실패: {reason}");
|
||||
}
|
||||
|
||||
context.maxMana = context.stats.MaxMana;
|
||||
context.currentMana = context.maxMana;
|
||||
return context;
|
||||
}
|
||||
|
||||
private static void ApplyBaseStats(CharacterStats stats, BuildSimulationInput input)
|
||||
{
|
||||
stats.Strength.BaseValue = input.Strength;
|
||||
stats.Dexterity.BaseValue = input.Dexterity;
|
||||
stats.Intelligence.BaseValue = input.Intelligence;
|
||||
stats.Vitality.BaseValue = input.Vitality;
|
||||
stats.Wisdom.BaseValue = input.Wisdom;
|
||||
stats.Spirit.BaseValue = input.Spirit;
|
||||
}
|
||||
|
||||
private static void ApplyWeaponStats(CharacterStats stats, WeaponData weapon)
|
||||
{
|
||||
if (stats == null || weapon == null)
|
||||
return;
|
||||
|
||||
foreach (StatType statType in Enum.GetValues(typeof(StatType)))
|
||||
{
|
||||
int bonus = weapon.GetStatBonus(statType);
|
||||
if (bonus == 0)
|
||||
continue;
|
||||
|
||||
CharacterStat stat = stats.GetStat(statType);
|
||||
stat?.AddModifier(new StatModifier(bonus, StatModType.Flat, weapon));
|
||||
}
|
||||
}
|
||||
|
||||
private static SkillRuntimeSnapshot[] BuildSnapshots(
|
||||
BuildSimulationInput input,
|
||||
SimulationContext context,
|
||||
SimulationRuleSet ruleSet,
|
||||
List<string> warnings)
|
||||
{
|
||||
IReadOnlyList<SkillLoadoutEntry> slots = input.ResolveLoadoutEntries();
|
||||
SkillRuntimeSnapshot[] snapshots = new SkillRuntimeSnapshot[slots.Count];
|
||||
float weaponDamageMultiplier = input.Weapon != null ? input.Weapon.DamageMultiplier : 1f;
|
||||
float weaponManaMultiplier = input.Weapon != null ? input.Weapon.ManaCostMultiplier : 1f;
|
||||
|
||||
for (int slotIndex = 0; slotIndex < slots.Count; slotIndex++)
|
||||
{
|
||||
SkillLoadoutEntry sourceEntry = slots[slotIndex];
|
||||
SkillData skill = sourceEntry != null ? sourceEntry.BaseSkill : null;
|
||||
if (sourceEntry == null || skill == null)
|
||||
continue;
|
||||
|
||||
SkillLoadoutEntry loadoutEntry = sourceEntry.CreateCopy();
|
||||
SkillRuntimeSnapshot snapshot = new SkillRuntimeSnapshot
|
||||
{
|
||||
slotIndex = slotIndex,
|
||||
loadoutEntry = loadoutEntry,
|
||||
skill = skill,
|
||||
skillName = string.IsNullOrWhiteSpace(skill.SkillName) ? $"Slot {slotIndex}" : skill.SkillName,
|
||||
cooldown = loadoutEntry.GetResolvedCooldown(),
|
||||
manaCost = loadoutEntry.GetResolvedManaCost() * weaponManaMultiplier * context.passiveController.GetManaCostMultiplier(skill),
|
||||
};
|
||||
|
||||
float resolvedAnimationSpeed = loadoutEntry.GetResolvedAnimationSpeed();
|
||||
float mainClipDuration = ResolveClipDuration(skill.SkillClip, resolvedAnimationSpeed);
|
||||
float endClipDuration = ResolveClipDuration(skill.EndClip, 1f);
|
||||
int repeatCount = loadoutEntry.GetResolvedRepeatCount();
|
||||
snapshot.castDuration = Mathf.Max(MinimumActionDuration, (mainClipDuration * repeatCount) + endClipDuration + ruleSet.MovementLossSecondsPerCast);
|
||||
|
||||
Dictionary<int, List<SkillEffect>> effectMap = new Dictionary<int, List<SkillEffect>>();
|
||||
loadoutEntry.CollectTriggeredEffects(effectMap);
|
||||
|
||||
BuildDamageEvents(snapshot, effectMap, context, weaponDamageMultiplier, ruleSet, mainClipDuration, resolvedAnimationSpeed, repeatCount, warnings);
|
||||
snapshots[slotIndex] = snapshot;
|
||||
}
|
||||
|
||||
return snapshots;
|
||||
}
|
||||
|
||||
private static void BuildDamageEvents(
|
||||
SkillRuntimeSnapshot snapshot,
|
||||
Dictionary<int, List<SkillEffect>> effectMap,
|
||||
SimulationContext context,
|
||||
float weaponDamageMultiplier,
|
||||
SimulationRuleSet ruleSet,
|
||||
float mainClipDuration,
|
||||
float resolvedAnimationSpeed,
|
||||
int repeatCount,
|
||||
List<string> warnings)
|
||||
{
|
||||
if (snapshot == null || effectMap == null || effectMap.Count == 0)
|
||||
return;
|
||||
|
||||
List<AnimationEvent> effectEvents = new List<AnimationEvent>();
|
||||
AnimationClip clip = snapshot.skill.SkillClip;
|
||||
if (clip != null)
|
||||
{
|
||||
AnimationEvent[] clipEvents = clip.events;
|
||||
for (int i = 0; i < clipEvents.Length; i++)
|
||||
{
|
||||
if (string.Equals(clipEvents[i].functionName, "OnEffect", StringComparison.Ordinal))
|
||||
effectEvents.Add(clipEvents[i]);
|
||||
}
|
||||
}
|
||||
|
||||
effectEvents.Sort((left, right) => left.time.CompareTo(right.time));
|
||||
float passiveDamageMultiplier = context.passiveController.GetDamageMultiplier(snapshot.skill);
|
||||
|
||||
for (int iteration = 0; iteration < repeatCount; iteration++)
|
||||
{
|
||||
float iterationOffset = mainClipDuration * iteration;
|
||||
|
||||
for (int eventIndex = 0; eventIndex < effectEvents.Count; eventIndex++)
|
||||
{
|
||||
AnimationEvent animationEvent = effectEvents[eventIndex];
|
||||
if (!effectMap.TryGetValue(animationEvent.intParameter, out List<SkillEffect> effectsAtIndex) || effectsAtIndex == null)
|
||||
continue;
|
||||
|
||||
float relativeTime = iterationOffset + (animationEvent.time / Mathf.Max(0.05f, resolvedAnimationSpeed));
|
||||
|
||||
for (int effectListIndex = 0; effectListIndex < effectsAtIndex.Count; effectListIndex++)
|
||||
{
|
||||
SkillEffect effect = effectsAtIndex[effectListIndex];
|
||||
if (effect is DamageEffect damageEffect)
|
||||
{
|
||||
float damageAmount = CalculateDamage(damageEffect, context.stats, snapshot.loadoutEntry, passiveDamageMultiplier, weaponDamageMultiplier);
|
||||
damageAmount *= ResolveTargetCount(effect, ruleSet);
|
||||
|
||||
snapshot.damageEvents.Add(new DamageEventSnapshot
|
||||
{
|
||||
relativeTime = relativeTime,
|
||||
damageAmount = damageAmount,
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
snapshot.containsUnsupportedEffects = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (snapshot.containsUnsupportedEffects)
|
||||
warnings.Add($"{snapshot.skillName}: {UnsupportedEffectsWarning}");
|
||||
|
||||
if (effectEvents.Count == 0 && effectMap.Count > 0)
|
||||
warnings.Add($"{snapshot.skillName}: OnEffect 애니메이션 이벤트가 없어 트리거 효과를 계산하지 못했습니다.");
|
||||
}
|
||||
|
||||
private static float CalculateDamage(
|
||||
DamageEffect effect,
|
||||
CharacterStats stats,
|
||||
SkillLoadoutEntry loadoutEntry,
|
||||
float passiveDamageMultiplier,
|
||||
float weaponDamageMultiplier)
|
||||
{
|
||||
if (effect == null || stats == null)
|
||||
return 0f;
|
||||
|
||||
if (effect.DamageKind == DamageType.True)
|
||||
{
|
||||
return effect.BaseDamage * loadoutEntry.GetResolvedDamageMultiplier() * passiveDamageMultiplier;
|
||||
}
|
||||
|
||||
float statDamage = effect.DamageKind switch
|
||||
{
|
||||
DamageType.Physical => stats.PhysicalDamage,
|
||||
DamageType.Magical => stats.MagicDamage,
|
||||
DamageType.Ranged => stats.Dexterity.FinalValue * 2f,
|
||||
_ => 0f,
|
||||
};
|
||||
|
||||
float baseTotal = effect.BaseDamage + (statDamage * effect.StatScaling);
|
||||
return baseTotal * weaponDamageMultiplier * loadoutEntry.GetResolvedDamageMultiplier() * passiveDamageMultiplier;
|
||||
}
|
||||
|
||||
private static int ResolveTargetCount(SkillEffect effect, SimulationRuleSet ruleSet)
|
||||
{
|
||||
if (effect == null)
|
||||
return 1;
|
||||
|
||||
return effect.TargetType == TargetType.Area ? ruleSet.TargetCount : 1;
|
||||
}
|
||||
|
||||
private static SkillRuntimeSnapshot TrySelectSkill(
|
||||
SkillRuntimeSnapshot[] snapshots,
|
||||
Dictionary<int, float> cooldownReadyTimes,
|
||||
SimulationContext context,
|
||||
RotationPolicy rotationPolicy)
|
||||
{
|
||||
SkillRuntimeSnapshot snapshot = TrySelectFromSlots(rotationPolicy.PrioritySlots, snapshots, cooldownReadyTimes, context, rotationPolicy);
|
||||
if (snapshot != null)
|
||||
return snapshot;
|
||||
|
||||
if (!rotationPolicy.UseFallbackSlot)
|
||||
return null;
|
||||
|
||||
return TrySelectFromSlots(new[] { rotationPolicy.FallbackSlotIndex }, snapshots, cooldownReadyTimes, context, rotationPolicy);
|
||||
}
|
||||
|
||||
private static SkillRuntimeSnapshot TrySelectFromSlots(
|
||||
int[] slotOrder,
|
||||
SkillRuntimeSnapshot[] snapshots,
|
||||
Dictionary<int, float> cooldownReadyTimes,
|
||||
SimulationContext context,
|
||||
RotationPolicy rotationPolicy)
|
||||
{
|
||||
if (slotOrder == null || snapshots == null)
|
||||
return null;
|
||||
|
||||
for (int i = 0; i < slotOrder.Length; i++)
|
||||
{
|
||||
int slotIndex = slotOrder[i];
|
||||
if (slotIndex < 0 || slotIndex >= snapshots.Length)
|
||||
continue;
|
||||
|
||||
SkillRuntimeSnapshot snapshot = snapshots[slotIndex];
|
||||
if (snapshot == null)
|
||||
continue;
|
||||
|
||||
if (rotationPolicy.DelayHighPowerSkillUntilTime &&
|
||||
slotIndex == rotationPolicy.HighPowerSlotIndex &&
|
||||
context.currentTime < rotationPolicy.HighPowerFirstUseTime &&
|
||||
!cooldownReadyTimes.ContainsKey(slotIndex))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (cooldownReadyTimes.TryGetValue(slotIndex, out float readyTime) && readyTime > context.currentTime)
|
||||
continue;
|
||||
|
||||
if (snapshot.manaCost > context.currentMana)
|
||||
continue;
|
||||
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static bool TryAdvanceIdleTime(
|
||||
SkillRuntimeSnapshot[] snapshots,
|
||||
Dictionary<int, float> cooldownReadyTimes,
|
||||
SimulationContext context,
|
||||
SimulationRuleSet ruleSet,
|
||||
RotationPolicy rotationPolicy,
|
||||
List<string> warnings)
|
||||
{
|
||||
float nextReadyTime = float.PositiveInfinity;
|
||||
bool hasActionCandidate = false;
|
||||
|
||||
for (int i = 0; i < snapshots.Length; i++)
|
||||
{
|
||||
SkillRuntimeSnapshot snapshot = snapshots[i];
|
||||
if (snapshot == null || !IsSlotRelevantToRotation(snapshot.slotIndex, rotationPolicy))
|
||||
continue;
|
||||
|
||||
hasActionCandidate = true;
|
||||
if (rotationPolicy.DelayHighPowerSkillUntilTime &&
|
||||
snapshot.slotIndex == rotationPolicy.HighPowerSlotIndex &&
|
||||
context.currentTime < rotationPolicy.HighPowerFirstUseTime &&
|
||||
!cooldownReadyTimes.ContainsKey(snapshot.slotIndex))
|
||||
{
|
||||
nextReadyTime = Mathf.Min(nextReadyTime, rotationPolicy.HighPowerFirstUseTime);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (cooldownReadyTimes.TryGetValue(snapshot.slotIndex, out float readyTime))
|
||||
{
|
||||
if (readyTime > context.currentTime)
|
||||
nextReadyTime = Mathf.Min(nextReadyTime, readyTime);
|
||||
}
|
||||
else if (snapshot.manaCost <= context.currentMana)
|
||||
{
|
||||
warnings.Add("대기 없이 사용할 수 있는 스킬을 찾지 못해 시뮬레이션을 중단했습니다.");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (!hasActionCandidate)
|
||||
{
|
||||
warnings.Add("유효한 스킬이 없어 시뮬레이션을 종료했습니다.");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (ruleSet.ManaRegenPerSecond > 0f)
|
||||
{
|
||||
float nextManaReadyTime = ResolveNextManaReadyTime(snapshots, context, ruleSet, rotationPolicy);
|
||||
nextReadyTime = Mathf.Min(nextReadyTime, nextManaReadyTime);
|
||||
}
|
||||
|
||||
if (float.IsPositiveInfinity(nextReadyTime))
|
||||
{
|
||||
warnings.Add("더 이상 실행 가능한 스킬이 없어 시뮬레이션을 조기 종료했습니다.");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (nextReadyTime <= context.currentTime + 0.0001f)
|
||||
{
|
||||
warnings.Add("다음 행동 시점을 계산하지 못해 시뮬레이션을 중단했습니다.");
|
||||
return false;
|
||||
}
|
||||
|
||||
AdvanceTime(context, Mathf.Min(ruleSet.DurationSeconds, nextReadyTime), ruleSet);
|
||||
return context.currentTime < ruleSet.DurationSeconds;
|
||||
}
|
||||
|
||||
private static float ResolveNextManaReadyTime(
|
||||
SkillRuntimeSnapshot[] snapshots,
|
||||
SimulationContext context,
|
||||
SimulationRuleSet ruleSet,
|
||||
RotationPolicy rotationPolicy)
|
||||
{
|
||||
float nextTime = float.PositiveInfinity;
|
||||
for (int i = 0; i < snapshots.Length; i++)
|
||||
{
|
||||
SkillRuntimeSnapshot snapshot = snapshots[i];
|
||||
if (snapshot == null ||
|
||||
!IsSlotRelevantToRotation(snapshot.slotIndex, rotationPolicy) ||
|
||||
snapshot.manaCost <= context.currentMana)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
float shortage = snapshot.manaCost - context.currentMana;
|
||||
nextTime = Mathf.Min(nextTime, context.currentTime + (shortage / ruleSet.ManaRegenPerSecond));
|
||||
}
|
||||
|
||||
return nextTime;
|
||||
}
|
||||
|
||||
private static void AdvanceTime(SimulationContext context, float nextTime, SimulationRuleSet ruleSet)
|
||||
{
|
||||
float clampedNextTime = Mathf.Max(context.currentTime, nextTime);
|
||||
float deltaTime = clampedNextTime - context.currentTime;
|
||||
if (deltaTime <= 0f)
|
||||
return;
|
||||
|
||||
context.currentTime = clampedNextTime;
|
||||
if (ruleSet.ManaRegenPerSecond > 0f)
|
||||
{
|
||||
context.currentMana = Mathf.Min(context.maxMana, context.currentMana + (ruleSet.ManaRegenPerSecond * deltaTime));
|
||||
}
|
||||
}
|
||||
|
||||
private static float ResolveClipDuration(AnimationClip clip, float speed)
|
||||
{
|
||||
if (clip == null)
|
||||
return 0f;
|
||||
|
||||
return clip.length / Mathf.Max(0.05f, speed);
|
||||
}
|
||||
|
||||
private static List<int> CollectValidPrioritySlots(RotationPolicy rotationPolicy, SkillRuntimeSnapshot[] snapshots)
|
||||
{
|
||||
List<int> validSlots = new List<int>();
|
||||
int[] prioritySlots = rotationPolicy.PrioritySlots;
|
||||
for (int i = 0; i < prioritySlots.Length; i++)
|
||||
{
|
||||
int slotIndex = prioritySlots[i];
|
||||
if (slotIndex < 0 || slotIndex >= snapshots.Length)
|
||||
continue;
|
||||
|
||||
if (snapshots[slotIndex] == null || validSlots.Contains(slotIndex))
|
||||
continue;
|
||||
|
||||
validSlots.Add(slotIndex);
|
||||
}
|
||||
|
||||
return validSlots;
|
||||
}
|
||||
|
||||
private static bool IsSlotRelevantToRotation(int slotIndex, RotationPolicy rotationPolicy)
|
||||
{
|
||||
int[] prioritySlots = rotationPolicy.PrioritySlots;
|
||||
for (int i = 0; i < prioritySlots.Length; i++)
|
||||
{
|
||||
if (prioritySlots[i] == slotIndex)
|
||||
return true;
|
||||
}
|
||||
|
||||
return rotationPolicy.UseFallbackSlot && rotationPolicy.FallbackSlotIndex == slotIndex;
|
||||
}
|
||||
|
||||
private static List<SimulationSkillBreakdown> BuildBreakdowns(Dictionary<int, SkillMetrics> metricsBySlot)
|
||||
{
|
||||
List<SimulationSkillBreakdown> breakdowns = new List<SimulationSkillBreakdown>();
|
||||
foreach (KeyValuePair<int, SkillMetrics> pair in metricsBySlot)
|
||||
{
|
||||
SkillMetrics metrics = pair.Value;
|
||||
breakdowns.Add(new SimulationSkillBreakdown(metrics.skillName, metrics.castCount, metrics.totalDamage));
|
||||
}
|
||||
|
||||
breakdowns.Sort((left, right) => right.TotalDamage.CompareTo(left.TotalDamage));
|
||||
return breakdowns;
|
||||
}
|
||||
|
||||
private static void DestroyContext(SimulationContext context)
|
||||
{
|
||||
if (context == null || context.actor == null)
|
||||
return;
|
||||
|
||||
if (Application.isPlaying)
|
||||
{
|
||||
UnityEngine.Object.Destroy(context.actor);
|
||||
return;
|
||||
}
|
||||
|
||||
UnityEngine.Object.DestroyImmediate(context.actor);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user