- 빌드 입력, 룰셋, 회전 정책, 결과/리포트 모델을 포함한 데미지 계산 시뮬레이터 기반을 추가 - 단일 실행 창과 배치 전수 조사 창, 플레이어 데미지 스윕 메뉴를 추가 - DamageEffect 계산값 접근자를 열어 기존 전투 공식을 시뮬레이터에서 재사용하도록 정리
608 lines
25 KiB
C#
608 lines
25 KiB
C#
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);
|
|
}
|
|
}
|
|
}
|