using System.Collections; using System.Text; using UnityEngine; using Colosseum.Passives; using Colosseum.Player; using Colosseum.Skills; namespace Colosseum.Combat { /// /// 허수아비 대상 DPS 벤치마크를 자동으로 실행합니다. /// 지정한 빌드를 적용한 뒤 일정 시간 슬롯 순환 시전을 반복하고 결과를 로그로 남깁니다. /// [DisallowMultipleComponent] public class DummyDpsBenchmarkRunner : MonoBehaviour { [Header("Targets")] [Tooltip("측정 대상 허수아비 (비어 있으면 자동 검색)")] [SerializeField] private TrainingDummyTarget targetDummy; [Tooltip("측정할 플레이어 OwnerClientId")] [Min(0)] [SerializeField] private int targetOwnerClientId = 0; [Header("Build Override")] [Tooltip("벤치마크 시작 시 적용할 로드아웃 프리셋 (비어 있으면 현재 빌드 유지)")] [SerializeField] private PlayerLoadoutPreset loadoutPreset; [Tooltip("벤치마크 시작 시 적용할 패시브 프리셋 (비어 있으면 현재 패시브 유지)")] [SerializeField] private PassivePresetData passivePreset; [Header("Benchmark")] [Tooltip("측정 시간")] [Min(1f)] [SerializeField] private float benchmarkDuration = 10f; [Tooltip("허수아비와 유지할 기준 거리")] [Min(0.5f)] [SerializeField] private float benchmarkDistance = 2.2f; [Tooltip("시작 전에 플레이어를 기준 위치로 정렬할지 여부")] [SerializeField] private bool snapPlayerToBenchmarkLane = true; [Tooltip("측정 중 가능한 한 허수아비를 바라보도록 유지할지 여부")] [SerializeField] private bool keepFacingDummy = true; [Tooltip("측정 중 수동 입력을 잠시 막을지 여부")] [SerializeField] private bool disableManualInputDuringBenchmark = true; [Tooltip("측정 슬롯 순서 (0 기반 인덱스)")] [SerializeField] private int[] rotationSlots = new[] { 0, 1, 2, 3, 4, 5 }; [Header("Session")] [Tooltip("완료 후 로그를 자동 출력할지 여부")] [SerializeField] private bool logSummaryOnComplete = true; [Tooltip("현재 측정 진행 중 여부")] [SerializeField] private bool isRunning; [Tooltip("마지막 측정 요약")] [SerializeField] private string lastSummary = string.Empty; [Tooltip("마지막 측정 총 피해량")] [SerializeField] private float lastTotalDamage; [Tooltip("마지막 측정 DPS")] [SerializeField] private float lastDps; [Tooltip("마지막 빌드 라벨")] [SerializeField] private string lastBuildLabel = string.Empty; private Coroutine benchmarkRoutine; public bool IsRunning => isRunning; public string LastSummary => lastSummary; public float LastTotalDamage => lastTotalDamage; public float LastDps => lastDps; public string LastBuildLabel => lastBuildLabel; /// /// 현재 설정으로 허수아비 벤치마크를 시작합니다. /// [ContextMenu("Start Benchmark")] public void StartBenchmark() { if (!Application.isPlaying) { Debug.LogWarning("[DummyBenchmark] 플레이 모드에서만 측정할 수 있습니다."); return; } if (isRunning) { Debug.LogWarning("[DummyBenchmark] 이미 측정이 진행 중입니다."); return; } benchmarkRoutine = StartCoroutine(RunBenchmark()); } /// /// 진행 중인 허수아비 벤치마크를 중지합니다. /// [ContextMenu("Stop Benchmark")] public void StopBenchmark() { if (benchmarkRoutine != null) { StopCoroutine(benchmarkRoutine); benchmarkRoutine = null; } isRunning = false; RestoreTargetDummyState(); } private IEnumerator RunBenchmark() { PlayerSkillInput skillInput = FindTargetSkillInput(); PlayerNetworkController networkController = skillInput != null ? skillInput.GetComponent() : null; SkillController skillController = skillInput != null ? skillInput.GetComponent() : null; targetDummy = ResolveDummy(); if (skillInput == null || networkController == null || skillController == null || targetDummy == null) { Debug.LogWarning("[DummyBenchmark] 플레이어/허수아비 참조를 찾지 못해 측정을 시작할 수 없습니다."); yield break; } if (!networkController.IsServer) { Debug.LogWarning("[DummyBenchmark] 호스트/서버 권한에서만 자동 측정을 실행할 수 있습니다."); yield break; } if (rotationSlots == null || rotationSlots.Length <= 0) { Debug.LogWarning("[DummyBenchmark] rotationSlots가 비어 있습니다."); yield break; } isRunning = true; lastSummary = string.Empty; lastTotalDamage = 0f; lastDps = 0f; targetDummy.SetAutoResetSuppressed(true); targetDummy.ResetDummy(); CombatBalanceTracker.Reset(); if (loadoutPreset != null) { skillInput.ApplyLoadoutPreset(loadoutPreset); } if (passivePreset != null) { networkController.DebugApplyPassivePreset(passivePreset, true); } else { networkController.Respawn(); } if (disableManualInputDuringBenchmark) { skillInput.SetGameplayInputEnabled(false); } skillController.CancelSkill(SkillCancelReason.Manual); if (snapPlayerToBenchmarkLane) { SnapPlayerToBenchmark(skillInput.transform); } yield return null; float benchmarkStartTime = Time.time; int rotationIndex = 0; while (Time.time - benchmarkStartTime < benchmarkDuration) { if (!skillController.IsExecutingSkill && snapPlayerToBenchmarkLane) { SnapPlayerToBenchmark(skillInput.transform); } if (keepFacingDummy) { FaceDummy(skillInput.transform); } TryExecuteRotationStep(skillInput, ref rotationIndex); yield return null; } lastTotalDamage = targetDummy.AccumulatedDamage; lastDps = lastTotalDamage / Mathf.Max(0.01f, benchmarkDuration); lastBuildLabel = BuildBuildLabel(skillInput, networkController); lastSummary = BuildBenchmarkSummary(networkController, targetDummy); if (logSummaryOnComplete) { Debug.Log(lastSummary); } if (disableManualInputDuringBenchmark) { skillInput.SetGameplayInputEnabled(true); } targetDummy.SetAutoResetSuppressed(false); benchmarkRoutine = null; isRunning = false; } private bool TryExecuteRotationStep(PlayerSkillInput skillInput, ref int rotationIndex) { if (skillInput == null || rotationSlots == null || rotationSlots.Length <= 0) return false; int slotIndex = rotationSlots[Mathf.Clamp(rotationIndex, 0, rotationSlots.Length - 1)]; bool executed = skillInput.DebugExecuteSkillAsServer(slotIndex); if (executed) { rotationIndex = (rotationIndex + 1) % rotationSlots.Length; } return executed; } private string BuildBenchmarkSummary(PlayerNetworkController networkController, TrainingDummyTarget dummy) { StringBuilder builder = new StringBuilder(); builder.Append("[DummyBenchmark] "); builder.Append(lastBuildLabel); builder.Append(" | TotalDamage="); builder.Append(lastTotalDamage.ToString("0.##")); builder.Append(" | DPS="); builder.Append(lastDps.ToString("0.##")); builder.Append(" | Duration="); builder.Append(benchmarkDuration.ToString("0.##")); builder.Append("s"); if (networkController != null) { builder.Append(" | Passive="); builder.Append(string.IsNullOrWhiteSpace(networkController.CurrentPassivePresetName) ? "미적용" : networkController.CurrentPassivePresetName); } builder.AppendLine(); builder.Append(dummy != null ? dummy.BuildSummary() : "[TrainingDummy] 없음"); builder.AppendLine(); builder.Append(CombatBalanceTracker.BuildSummary()); return builder.ToString(); } private string BuildBuildLabel(PlayerSkillInput skillInput, PlayerNetworkController networkController) { StringBuilder builder = new StringBuilder(); if (loadoutPreset != null && !string.IsNullOrWhiteSpace(loadoutPreset.PresetName)) { builder.Append(loadoutPreset.PresetName); } else { builder.Append("현재 빌드"); } if (passivePreset != null && !string.IsNullOrWhiteSpace(passivePreset.PresetName)) { builder.Append(" + "); builder.Append(passivePreset.PresetName); } else if (networkController != null && !string.IsNullOrWhiteSpace(networkController.CurrentPassivePresetName)) { builder.Append(" + "); builder.Append(networkController.CurrentPassivePresetName); } if (skillInput == null) return builder.ToString(); builder.Append(" | Rotation="); bool hasSlot = false; for (int i = 0; i < rotationSlots.Length; i++) { int slotIndex = rotationSlots[i]; SkillLoadoutEntry loadoutEntry = skillInput.GetSkillLoadout(slotIndex); SkillData skill = loadoutEntry != null ? loadoutEntry.BaseSkill : null; if (skill == null) continue; if (hasSlot) { builder.Append(" -> "); } builder.Append(slotIndex + 1); builder.Append(':'); builder.Append(skill.SkillName); AppendGemSummary(builder, loadoutEntry); hasSlot = true; } return builder.ToString(); } private void SnapPlayerToBenchmark(Transform playerTransform) { if (playerTransform == null || targetDummy == null) return; Vector3 dummyForward = targetDummy.transform.forward; dummyForward.y = 0f; if (dummyForward.sqrMagnitude <= 0.0001f) { dummyForward = Vector3.back; } else { dummyForward.Normalize(); } Vector3 targetPosition = targetDummy.transform.position + dummyForward * benchmarkDistance; playerTransform.position = new Vector3(targetPosition.x, playerTransform.position.y, targetPosition.z); FaceDummy(playerTransform); } private void FaceDummy(Transform playerTransform) { if (playerTransform == null || targetDummy == null) return; Vector3 lookDirection = targetDummy.transform.position - playerTransform.position; lookDirection.y = 0f; if (lookDirection.sqrMagnitude <= 0.0001f) return; playerTransform.rotation = Quaternion.LookRotation(lookDirection.normalized, Vector3.up); } private PlayerSkillInput FindTargetSkillInput() { PlayerSkillInput[] players = FindObjectsByType(FindObjectsInactive.Exclude, FindObjectsSortMode.None); for (int i = 0; i < players.Length; i++) { PlayerSkillInput skillInput = players[i]; if (skillInput != null && skillInput.OwnerClientId == (ulong)Mathf.Max(0, targetOwnerClientId)) return skillInput; } return null; } private TrainingDummyTarget ResolveDummy() { if (targetDummy != null) return targetDummy; return FindFirstObjectByType(); } private void RestoreTargetDummyState() { if (targetDummy != null) { targetDummy.SetAutoResetSuppressed(false); } } private static void AppendGemSummary(StringBuilder builder, SkillLoadoutEntry loadoutEntry) { if (builder == null || loadoutEntry == null || loadoutEntry.SocketedGems == null) return; bool hasGem = false; for (int i = 0; i < loadoutEntry.SocketedGems.Count; i++) { SkillGemData gem = loadoutEntry.SocketedGems[i]; if (gem == null) continue; builder.Append(hasGem ? ", " : "["); builder.Append(gem.GemName); hasGem = true; } if (hasGem) { builder.Append(']'); } } } }