feat: 허수아비 DPS 벤치마크 씬 추가
- BalanceDummy 씬과 TrainingDummy 프리팹을 추가해 밸런싱용 허수아비 전투 공간을 구성 - TrainingDummyTarget과 DummyDpsBenchmarkRunner를 구현해 일정 시간 자동 시전 기반 DPS 측정을 지원 - 디버그 메뉴, 빌드 설정, 네트워크 프리팹 목록을 연결해 플레이 모드 검증 경로를 정리
This commit is contained in:
@@ -34,3 +34,8 @@ MonoBehaviour:
|
|||||||
SourcePrefabToOverride: {fileID: 0}
|
SourcePrefabToOverride: {fileID: 0}
|
||||||
SourceHashToOverride: 0
|
SourceHashToOverride: 0
|
||||||
OverridingTargetPrefab: {fileID: 0}
|
OverridingTargetPrefab: {fileID: 0}
|
||||||
|
- Override: 0
|
||||||
|
Prefab: {fileID: 3054181739271998841, guid: b2b53d2d232562a47995ca503090670e, type: 3}
|
||||||
|
SourcePrefabToOverride: {fileID: 0}
|
||||||
|
SourceHashToOverride: 0
|
||||||
|
OverridingTargetPrefab: {fileID: 0}
|
||||||
|
|||||||
13612
Assets/Scenes/BalanceDummy.unity
Normal file
13612
Assets/Scenes/BalanceDummy.unity
Normal file
File diff suppressed because it is too large
Load Diff
7
Assets/Scenes/BalanceDummy.unity.meta
Normal file
7
Assets/Scenes/BalanceDummy.unity.meta
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 90d10d723d37d174c94429aff781504d
|
||||||
|
DefaultImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
8
Assets/_Game/Prefabs/Combat.meta
Normal file
8
Assets/_Game/Prefabs/Combat.meta
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 769f8f2e72bd0d1419c1692789e7a251
|
||||||
|
folderAsset: yes
|
||||||
|
DefaultImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
225
Assets/_Game/Prefabs/Combat/Prefab_Object_TrainingDummy.prefab
Normal file
225
Assets/_Game/Prefabs/Combat/Prefab_Object_TrainingDummy.prefab
Normal file
@@ -0,0 +1,225 @@
|
|||||||
|
%YAML 1.1
|
||||||
|
%TAG !u! tag:unity3d.com,2011:
|
||||||
|
--- !u!1 &3054181739271998841
|
||||||
|
GameObject:
|
||||||
|
m_ObjectHideFlags: 0
|
||||||
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
|
m_PrefabInstance: {fileID: 0}
|
||||||
|
m_PrefabAsset: {fileID: 0}
|
||||||
|
serializedVersion: 6
|
||||||
|
m_Component:
|
||||||
|
- component: {fileID: 3603188469848606439}
|
||||||
|
- component: {fileID: 2004420943758656828}
|
||||||
|
- component: {fileID: 5182453241883187424}
|
||||||
|
- component: {fileID: 1285472408817410619}
|
||||||
|
- component: {fileID: 4754826070226246169}
|
||||||
|
- component: {fileID: 4771767181016103934}
|
||||||
|
- component: {fileID: 4198820995877550832}
|
||||||
|
- component: {fileID: 703472655594990954}
|
||||||
|
- component: {fileID: 2489901850308028945}
|
||||||
|
m_Layer: 6
|
||||||
|
m_Name: Prefab_TrainingDummy
|
||||||
|
m_TagString: Untagged
|
||||||
|
m_Icon: {fileID: 0}
|
||||||
|
m_NavMeshLayer: 0
|
||||||
|
m_StaticEditorFlags: 0
|
||||||
|
m_IsActive: 1
|
||||||
|
--- !u!4 &3603188469848606439
|
||||||
|
Transform:
|
||||||
|
m_ObjectHideFlags: 0
|
||||||
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
|
m_PrefabInstance: {fileID: 0}
|
||||||
|
m_PrefabAsset: {fileID: 0}
|
||||||
|
m_GameObject: {fileID: 3054181739271998841}
|
||||||
|
serializedVersion: 2
|
||||||
|
m_LocalRotation: {x: 0, y: 1, z: 0, w: -0.00000004371139}
|
||||||
|
m_LocalPosition: {x: 0, y: 1, z: 10}
|
||||||
|
m_LocalScale: {x: 1.5, y: 2.5, z: 1.5}
|
||||||
|
m_ConstrainProportionsScale: 0
|
||||||
|
m_Children: []
|
||||||
|
m_Father: {fileID: 0}
|
||||||
|
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||||
|
--- !u!33 &2004420943758656828
|
||||||
|
MeshFilter:
|
||||||
|
m_ObjectHideFlags: 0
|
||||||
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
|
m_PrefabInstance: {fileID: 0}
|
||||||
|
m_PrefabAsset: {fileID: 0}
|
||||||
|
m_GameObject: {fileID: 3054181739271998841}
|
||||||
|
m_Mesh: {fileID: 10208, guid: 0000000000000000e000000000000000, type: 0}
|
||||||
|
--- !u!136 &5182453241883187424
|
||||||
|
CapsuleCollider:
|
||||||
|
m_ObjectHideFlags: 0
|
||||||
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
|
m_PrefabInstance: {fileID: 0}
|
||||||
|
m_PrefabAsset: {fileID: 0}
|
||||||
|
m_GameObject: {fileID: 3054181739271998841}
|
||||||
|
m_Material: {fileID: 0}
|
||||||
|
m_IncludeLayers:
|
||||||
|
serializedVersion: 2
|
||||||
|
m_Bits: 0
|
||||||
|
m_ExcludeLayers:
|
||||||
|
serializedVersion: 2
|
||||||
|
m_Bits: 0
|
||||||
|
m_LayerOverridePriority: 0
|
||||||
|
m_IsTrigger: 0
|
||||||
|
m_ProvidesContacts: 0
|
||||||
|
m_Enabled: 1
|
||||||
|
serializedVersion: 2
|
||||||
|
m_Radius: 0.5
|
||||||
|
m_Height: 2
|
||||||
|
m_Direction: 1
|
||||||
|
m_Center: {x: 0, y: 0, z: 0}
|
||||||
|
--- !u!23 &1285472408817410619
|
||||||
|
MeshRenderer:
|
||||||
|
m_ObjectHideFlags: 0
|
||||||
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
|
m_PrefabInstance: {fileID: 0}
|
||||||
|
m_PrefabAsset: {fileID: 0}
|
||||||
|
m_GameObject: {fileID: 3054181739271998841}
|
||||||
|
m_Enabled: 1
|
||||||
|
m_CastShadows: 1
|
||||||
|
m_ReceiveShadows: 1
|
||||||
|
m_DynamicOccludee: 1
|
||||||
|
m_StaticShadowCaster: 0
|
||||||
|
m_MotionVectors: 1
|
||||||
|
m_LightProbeUsage: 1
|
||||||
|
m_ReflectionProbeUsage: 1
|
||||||
|
m_RayTracingMode: 2
|
||||||
|
m_RayTraceProcedural: 0
|
||||||
|
m_RayTracingAccelStructBuildFlagsOverride: 0
|
||||||
|
m_RayTracingAccelStructBuildFlags: 1
|
||||||
|
m_SmallMeshCulling: 1
|
||||||
|
m_ForceMeshLod: -1
|
||||||
|
m_MeshLodSelectionBias: 0
|
||||||
|
m_RenderingLayerMask: 1
|
||||||
|
m_RendererPriority: 0
|
||||||
|
m_Materials:
|
||||||
|
- {fileID: 2100000, guid: 31321ba15b8f8eb4c954353edc038b1d, type: 2}
|
||||||
|
m_StaticBatchInfo:
|
||||||
|
firstSubMesh: 0
|
||||||
|
subMeshCount: 0
|
||||||
|
m_StaticBatchRoot: {fileID: 0}
|
||||||
|
m_ProbeAnchor: {fileID: 0}
|
||||||
|
m_LightProbeVolumeOverride: {fileID: 0}
|
||||||
|
m_ScaleInLightmap: 1
|
||||||
|
m_ReceiveGI: 1
|
||||||
|
m_PreserveUVs: 1
|
||||||
|
m_IgnoreNormalsForChartDetection: 0
|
||||||
|
m_ImportantGI: 0
|
||||||
|
m_StitchLightmapSeams: 1
|
||||||
|
m_SelectedEditorRenderState: 3
|
||||||
|
m_MinimumChartSize: 4
|
||||||
|
m_AutoUVMaxDistance: 0.5
|
||||||
|
m_AutoUVMaxAngle: 89
|
||||||
|
m_LightmapParameters: {fileID: 0}
|
||||||
|
m_GlobalIlluminationMeshLod: 0
|
||||||
|
m_SortingLayerID: 0
|
||||||
|
m_SortingLayer: 0
|
||||||
|
m_SortingOrder: 0
|
||||||
|
m_MaskInteraction: 0
|
||||||
|
m_AdditionalVertexStreams: {fileID: 0}
|
||||||
|
--- !u!114 &4754826070226246169
|
||||||
|
MonoBehaviour:
|
||||||
|
m_ObjectHideFlags: 0
|
||||||
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
|
m_PrefabInstance: {fileID: 0}
|
||||||
|
m_PrefabAsset: {fileID: 0}
|
||||||
|
m_GameObject: {fileID: 3054181739271998841}
|
||||||
|
m_Enabled: 1
|
||||||
|
m_EditorHideFlags: 0
|
||||||
|
m_Script: {fileID: 11500000, guid: d5a57f767e5e46a458fc5d3c628d0cbb, type: 3}
|
||||||
|
m_Name:
|
||||||
|
m_EditorClassIdentifier: Unity.Netcode.Runtime::Unity.Netcode.NetworkObject
|
||||||
|
GlobalObjectIdHash: 3664463253
|
||||||
|
InScenePlacedSourceGlobalObjectIdHash: 0
|
||||||
|
DeferredDespawnTick: 0
|
||||||
|
Ownership: 1
|
||||||
|
AlwaysReplicateAsRoot: 0
|
||||||
|
SynchronizeTransform: 1
|
||||||
|
ActiveSceneSynchronization: 0
|
||||||
|
SceneMigrationSynchronization: 1
|
||||||
|
SpawnWithObservers: 1
|
||||||
|
DontDestroyWithOwner: 0
|
||||||
|
AutoObjectParentSync: 1
|
||||||
|
SyncOwnerTransformWhenParented: 1
|
||||||
|
AllowOwnerToParent: 0
|
||||||
|
--- !u!114 &4771767181016103934
|
||||||
|
MonoBehaviour:
|
||||||
|
m_ObjectHideFlags: 0
|
||||||
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
|
m_PrefabInstance: {fileID: 0}
|
||||||
|
m_PrefabAsset: {fileID: 0}
|
||||||
|
m_GameObject: {fileID: 3054181739271998841}
|
||||||
|
m_Enabled: 1
|
||||||
|
m_EditorHideFlags: 0
|
||||||
|
m_Script: {fileID: 11500000, guid: d1f7d13276f272b428bddd4d9aa5b3d8, type: 3}
|
||||||
|
m_Name:
|
||||||
|
m_EditorClassIdentifier: Colosseum.Game::Colosseum.Team
|
||||||
|
teamType: 2
|
||||||
|
--- !u!114 &4198820995877550832
|
||||||
|
MonoBehaviour:
|
||||||
|
m_ObjectHideFlags: 0
|
||||||
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
|
m_PrefabInstance: {fileID: 0}
|
||||||
|
m_PrefabAsset: {fileID: 0}
|
||||||
|
m_GameObject: {fileID: 3054181739271998841}
|
||||||
|
m_Enabled: 1
|
||||||
|
m_EditorHideFlags: 0
|
||||||
|
m_Script: {fileID: 11500000, guid: 234ffc560cedd8b4293c262e735a86b8, type: 3}
|
||||||
|
m_Name:
|
||||||
|
m_EditorClassIdentifier: Colosseum.Game::Colosseum.Combat.TrainingDummyTarget
|
||||||
|
ShowTopMostFoldoutHeaderGroup: 1
|
||||||
|
maxHealth: 50000
|
||||||
|
autoResetDelay: 3
|
||||||
|
resetImmediatelyOnDeath: 1
|
||||||
|
faceAttacker: 1
|
||||||
|
logSummaryOnReset: 1
|
||||||
|
currentHealth: 50000
|
||||||
|
accumulatedDamage: 0
|
||||||
|
peakHitDamage: 0
|
||||||
|
lastHitDamage: 0
|
||||||
|
averageDps: 0
|
||||||
|
lastAttackerName:
|
||||||
|
inCombat: 0
|
||||||
|
isDead: 0
|
||||||
|
--- !u!114 &703472655594990954
|
||||||
|
MonoBehaviour:
|
||||||
|
m_ObjectHideFlags: 0
|
||||||
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
|
m_PrefabInstance: {fileID: 0}
|
||||||
|
m_PrefabAsset: {fileID: 0}
|
||||||
|
m_GameObject: {fileID: 3054181739271998841}
|
||||||
|
m_Enabled: 1
|
||||||
|
m_EditorHideFlags: 0
|
||||||
|
m_Script: {fileID: 11500000, guid: fae0149926eea244dad932b67ee76f7b, type: 3}
|
||||||
|
m_Name:
|
||||||
|
m_EditorClassIdentifier: Colosseum.Game::Colosseum.Stats.CharacterStats
|
||||||
|
strength:
|
||||||
|
baseValue: 10
|
||||||
|
dexterity:
|
||||||
|
baseValue: 10
|
||||||
|
intelligence:
|
||||||
|
baseValue: 10
|
||||||
|
vitality:
|
||||||
|
baseValue: 10
|
||||||
|
wisdom:
|
||||||
|
baseValue: 10
|
||||||
|
spirit:
|
||||||
|
baseValue: 10
|
||||||
|
--- !u!114 &2489901850308028945
|
||||||
|
MonoBehaviour:
|
||||||
|
m_ObjectHideFlags: 0
|
||||||
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
|
m_PrefabInstance: {fileID: 0}
|
||||||
|
m_PrefabAsset: {fileID: 0}
|
||||||
|
m_GameObject: {fileID: 3054181739271998841}
|
||||||
|
m_Enabled: 1
|
||||||
|
m_EditorHideFlags: 0
|
||||||
|
m_Script: {fileID: 11500000, guid: 7a766b6ab825c1445a3385079bb32cc5, type: 3}
|
||||||
|
m_Name:
|
||||||
|
m_EditorClassIdentifier: Colosseum.Game::Colosseum.Abnormalities.AbnormalityManager
|
||||||
|
ShowTopMostFoldoutHeaderGroup: 1
|
||||||
|
characterStats: {fileID: 0}
|
||||||
|
networkController: {fileID: 0}
|
||||||
|
skillController: {fileID: 0}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: b2b53d2d232562a47995ca503090670e
|
||||||
|
PrefabImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
398
Assets/_Game/Scripts/Combat/DummyDpsBenchmarkRunner.cs
Normal file
398
Assets/_Game/Scripts/Combat/DummyDpsBenchmarkRunner.cs
Normal file
@@ -0,0 +1,398 @@
|
|||||||
|
using System.Collections;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
using Colosseum.Passives;
|
||||||
|
using Colosseum.Player;
|
||||||
|
using Colosseum.Skills;
|
||||||
|
|
||||||
|
namespace Colosseum.Combat
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 허수아비 대상 DPS 벤치마크를 자동으로 실행합니다.
|
||||||
|
/// 지정한 빌드를 적용한 뒤 일정 시간 슬롯 순환 시전을 반복하고 결과를 로그로 남깁니다.
|
||||||
|
/// </summary>
|
||||||
|
[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;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 현재 설정으로 허수아비 벤치마크를 시작합니다.
|
||||||
|
/// </summary>
|
||||||
|
[ContextMenu("Start Benchmark")]
|
||||||
|
public void StartBenchmark()
|
||||||
|
{
|
||||||
|
if (!Application.isPlaying)
|
||||||
|
{
|
||||||
|
Debug.LogWarning("[DummyBenchmark] 플레이 모드에서만 측정할 수 있습니다.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isRunning)
|
||||||
|
{
|
||||||
|
Debug.LogWarning("[DummyBenchmark] 이미 측정이 진행 중입니다.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
benchmarkRoutine = StartCoroutine(RunBenchmark());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 진행 중인 허수아비 벤치마크를 중지합니다.
|
||||||
|
/// </summary>
|
||||||
|
[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<PlayerNetworkController>() : null;
|
||||||
|
SkillController skillController = skillInput != null ? skillInput.GetComponent<SkillController>() : 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<PlayerSkillInput>(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<TrainingDummyTarget>();
|
||||||
|
}
|
||||||
|
|
||||||
|
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(']');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: bdd1385e2099b5c4b86889e3a2b5ac1b
|
||||||
258
Assets/_Game/Scripts/Combat/TrainingDummyTarget.cs
Normal file
258
Assets/_Game/Scripts/Combat/TrainingDummyTarget.cs
Normal file
@@ -0,0 +1,258 @@
|
|||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
using Unity.Netcode;
|
||||||
|
|
||||||
|
namespace Colosseum.Combat
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 밸런싱 테스트용 허수아비 타깃입니다.
|
||||||
|
/// 누적 피해를 기록하고 일정 시간 후 자동으로 체력을 복구합니다.
|
||||||
|
/// </summary>
|
||||||
|
[DisallowMultipleComponent]
|
||||||
|
public class TrainingDummyTarget : NetworkBehaviour, IDamageable
|
||||||
|
{
|
||||||
|
[Header("Settings")]
|
||||||
|
[Tooltip("허수아비 최대 체력")]
|
||||||
|
[Min(1f)] [SerializeField] private float maxHealth = 50000f;
|
||||||
|
|
||||||
|
[Tooltip("마지막 피격 이후 자동 리셋까지 대기 시간")]
|
||||||
|
[Min(0f)] [SerializeField] private float autoResetDelay = 3f;
|
||||||
|
|
||||||
|
[Tooltip("체력이 0이 되면 즉시 최대 체력으로 복구할지 여부")]
|
||||||
|
[SerializeField] private bool resetImmediatelyOnDeath = true;
|
||||||
|
|
||||||
|
[Tooltip("피격 시 공격자 방향을 바라볼지 여부")]
|
||||||
|
[SerializeField] private bool faceAttacker = true;
|
||||||
|
|
||||||
|
[Tooltip("리셋 시 누적 피해 요약 로그를 출력할지 여부")]
|
||||||
|
[SerializeField] private bool logSummaryOnReset = true;
|
||||||
|
|
||||||
|
[Header("Debug")]
|
||||||
|
[Tooltip("현재 체력")]
|
||||||
|
[SerializeField] private float currentHealth;
|
||||||
|
|
||||||
|
[Tooltip("최근 전투 구간 누적 피해")]
|
||||||
|
[SerializeField] private float accumulatedDamage;
|
||||||
|
|
||||||
|
[Tooltip("최근 전투 구간 최고 단일 피해")]
|
||||||
|
[SerializeField] private float peakHitDamage;
|
||||||
|
|
||||||
|
[Tooltip("마지막 단일 피해량")]
|
||||||
|
[SerializeField] private float lastHitDamage;
|
||||||
|
|
||||||
|
[Tooltip("최근 전투 구간 평균 DPS")]
|
||||||
|
[SerializeField] private float averageDps;
|
||||||
|
|
||||||
|
[Tooltip("마지막 공격자 이름")]
|
||||||
|
[SerializeField] private string lastAttackerName = string.Empty;
|
||||||
|
|
||||||
|
[Tooltip("전투 중 여부")]
|
||||||
|
[SerializeField] private bool inCombat;
|
||||||
|
|
||||||
|
[Tooltip("사망 여부")]
|
||||||
|
[SerializeField] private bool isDead;
|
||||||
|
|
||||||
|
private float combatStartTime = -1f;
|
||||||
|
private float lastHitTime = -1f;
|
||||||
|
private bool autoResetSuppressed;
|
||||||
|
|
||||||
|
public float CurrentHealth => currentHealth;
|
||||||
|
public float MaxHealth => maxHealth;
|
||||||
|
public bool IsDead => isDead;
|
||||||
|
public float AccumulatedDamage => accumulatedDamage;
|
||||||
|
public float PeakHitDamage => peakHitDamage;
|
||||||
|
public float LastHitDamage => lastHitDamage;
|
||||||
|
public float AverageDps => averageDps;
|
||||||
|
public float CombatDuration => inCombat ? Mathf.Max(0f, Time.time - combatStartTime) : 0f;
|
||||||
|
|
||||||
|
private void Awake()
|
||||||
|
{
|
||||||
|
ResetState(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void OnNetworkSpawn()
|
||||||
|
{
|
||||||
|
if (IsServer)
|
||||||
|
{
|
||||||
|
ResetState(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Update()
|
||||||
|
{
|
||||||
|
if (!IsServer || !Application.isPlaying)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (!inCombat)
|
||||||
|
return;
|
||||||
|
|
||||||
|
float combatDuration = Mathf.Max(0.01f, Time.time - combatStartTime);
|
||||||
|
averageDps = accumulatedDamage / combatDuration;
|
||||||
|
|
||||||
|
if (autoResetSuppressed || autoResetDelay <= 0f)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (Time.time - lastHitTime >= autoResetDelay)
|
||||||
|
{
|
||||||
|
ResetDummy();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 대미지를 적용합니다.
|
||||||
|
/// </summary>
|
||||||
|
public float TakeDamage(float damage, object source = null)
|
||||||
|
{
|
||||||
|
if (!IsServer || damage <= 0f)
|
||||||
|
return 0f;
|
||||||
|
|
||||||
|
GameObject sourceObject = ResolveSource(source);
|
||||||
|
float actualDamage = Mathf.Min(damage, currentHealth);
|
||||||
|
currentHealth = Mathf.Max(0f, currentHealth - actualDamage);
|
||||||
|
|
||||||
|
BeginOrRefreshCombat(sourceObject, actualDamage);
|
||||||
|
CombatBalanceTracker.RecordDamage(sourceObject, gameObject, actualDamage);
|
||||||
|
|
||||||
|
if (faceAttacker && sourceObject != null)
|
||||||
|
{
|
||||||
|
FaceTowards(sourceObject.transform.position);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentHealth <= 0f)
|
||||||
|
{
|
||||||
|
isDead = true;
|
||||||
|
|
||||||
|
if (resetImmediatelyOnDeath && !autoResetSuppressed)
|
||||||
|
{
|
||||||
|
ResetDummy();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return actualDamage;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 체력을 회복합니다.
|
||||||
|
/// </summary>
|
||||||
|
public float Heal(float amount)
|
||||||
|
{
|
||||||
|
if (!IsServer || amount <= 0f)
|
||||||
|
return 0f;
|
||||||
|
|
||||||
|
float missingHealth = Mathf.Max(0f, maxHealth - currentHealth);
|
||||||
|
float actualHeal = Mathf.Min(amount, missingHealth);
|
||||||
|
currentHealth += actualHeal;
|
||||||
|
|
||||||
|
if (currentHealth > 0f)
|
||||||
|
{
|
||||||
|
isDead = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return actualHeal;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 허수아비 상태를 초기화합니다.
|
||||||
|
/// </summary>
|
||||||
|
[ContextMenu("Reset Dummy")]
|
||||||
|
public void ResetDummy()
|
||||||
|
{
|
||||||
|
if (Application.isPlaying && !IsServer)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (logSummaryOnReset && accumulatedDamage > 0f)
|
||||||
|
{
|
||||||
|
Debug.Log(BuildSummary());
|
||||||
|
}
|
||||||
|
|
||||||
|
ResetState(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 현재 허수아비 전투 요약 문자열을 반환합니다.
|
||||||
|
/// </summary>
|
||||||
|
public string BuildSummary()
|
||||||
|
{
|
||||||
|
float combatDuration = inCombat ? Mathf.Max(0.01f, Time.time - combatStartTime) : 0f;
|
||||||
|
|
||||||
|
return $"[TrainingDummy] {name} | Damage={accumulatedDamage:0.##} | Peak={peakHitDamage:0.##} | DPS={averageDps:0.##} | Duration={combatDuration:0.##}s | LastAttacker={lastAttackerName}";
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 외부 벤치마크가 진행되는 동안 자동 리셋을 일시 중지합니다.
|
||||||
|
/// </summary>
|
||||||
|
public void SetAutoResetSuppressed(bool suppressed)
|
||||||
|
{
|
||||||
|
autoResetSuppressed = suppressed;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void BeginOrRefreshCombat(GameObject sourceObject, float actualDamage)
|
||||||
|
{
|
||||||
|
if (!inCombat)
|
||||||
|
{
|
||||||
|
inCombat = true;
|
||||||
|
combatStartTime = Time.time;
|
||||||
|
accumulatedDamage = 0f;
|
||||||
|
peakHitDamage = 0f;
|
||||||
|
averageDps = 0f;
|
||||||
|
}
|
||||||
|
|
||||||
|
accumulatedDamage += actualDamage;
|
||||||
|
peakHitDamage = Mathf.Max(peakHitDamage, actualDamage);
|
||||||
|
lastHitDamage = actualDamage;
|
||||||
|
lastHitTime = Time.time;
|
||||||
|
lastAttackerName = sourceObject != null ? sourceObject.name : "Unknown";
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ResetState(bool preserveEditorValues)
|
||||||
|
{
|
||||||
|
currentHealth = Mathf.Max(1f, maxHealth);
|
||||||
|
accumulatedDamage = 0f;
|
||||||
|
peakHitDamage = 0f;
|
||||||
|
lastHitDamage = 0f;
|
||||||
|
averageDps = 0f;
|
||||||
|
inCombat = false;
|
||||||
|
isDead = false;
|
||||||
|
combatStartTime = -1f;
|
||||||
|
lastHitTime = -1f;
|
||||||
|
|
||||||
|
if (!preserveEditorValues)
|
||||||
|
{
|
||||||
|
lastAttackerName = string.Empty;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void FaceTowards(Vector3 worldPosition)
|
||||||
|
{
|
||||||
|
Vector3 lookDirection = worldPosition - transform.position;
|
||||||
|
lookDirection.y = 0f;
|
||||||
|
|
||||||
|
if (lookDirection.sqrMagnitude <= 0.0001f)
|
||||||
|
return;
|
||||||
|
|
||||||
|
transform.rotation = Quaternion.LookRotation(lookDirection.normalized, Vector3.up);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static GameObject ResolveSource(object source)
|
||||||
|
{
|
||||||
|
return source switch
|
||||||
|
{
|
||||||
|
GameObject gameObject => gameObject,
|
||||||
|
Component component => component.gameObject,
|
||||||
|
_ => null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
#if UNITY_EDITOR
|
||||||
|
private void OnValidate()
|
||||||
|
{
|
||||||
|
maxHealth = Mathf.Max(1f, maxHealth);
|
||||||
|
|
||||||
|
if (!Application.isPlaying)
|
||||||
|
{
|
||||||
|
currentHealth = Mathf.Clamp(currentHealth <= 0f ? maxHealth : currentHealth, 0f, maxHealth);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
}
|
||||||
2
Assets/_Game/Scripts/Combat/TrainingDummyTarget.cs.meta
Normal file
2
Assets/_Game/Scripts/Combat/TrainingDummyTarget.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 234ffc560cedd8b4293c262e735a86b8
|
||||||
@@ -251,6 +251,44 @@ namespace Colosseum.Editor
|
|||||||
Debug.Log(summary);
|
Debug.Log(summary);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[MenuItem("Tools/Colosseum/Debug/Start Dummy DPS Benchmark")]
|
||||||
|
private static void StartDummyDpsBenchmark()
|
||||||
|
{
|
||||||
|
if (!EditorApplication.isPlaying)
|
||||||
|
{
|
||||||
|
Debug.LogWarning("[Debug] 플레이 모드에서만 사용할 수 있습니다.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
DummyDpsBenchmarkRunner benchmarkRunner = Object.FindFirstObjectByType<DummyDpsBenchmarkRunner>();
|
||||||
|
if (benchmarkRunner == null)
|
||||||
|
{
|
||||||
|
Debug.LogWarning("[Debug] DummyDpsBenchmarkRunner를 찾지 못했습니다.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
benchmarkRunner.StartBenchmark();
|
||||||
|
}
|
||||||
|
|
||||||
|
[MenuItem("Tools/Colosseum/Debug/Log Last Dummy DPS Benchmark")]
|
||||||
|
private static void LogLastDummyDpsBenchmark()
|
||||||
|
{
|
||||||
|
DummyDpsBenchmarkRunner benchmarkRunner = Object.FindFirstObjectByType<DummyDpsBenchmarkRunner>();
|
||||||
|
if (benchmarkRunner == null)
|
||||||
|
{
|
||||||
|
Debug.LogWarning("[Debug] DummyDpsBenchmarkRunner를 찾지 못했습니다.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(benchmarkRunner.LastSummary))
|
||||||
|
{
|
||||||
|
Debug.LogWarning("[Debug] 아직 완료된 허수아비 DPS 측정 결과가 없습니다.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Debug.Log(benchmarkRunner.LastSummary);
|
||||||
|
}
|
||||||
|
|
||||||
[MenuItem("Tools/Colosseum/Debug/Apply Local Stun")]
|
[MenuItem("Tools/Colosseum/Debug/Apply Local Stun")]
|
||||||
private static void ApplyLocalStun()
|
private static void ApplyLocalStun()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -14,6 +14,9 @@ EditorBuildSettings:
|
|||||||
- enabled: 1
|
- enabled: 1
|
||||||
path: Assets/Scenes/Test.unity
|
path: Assets/Scenes/Test.unity
|
||||||
guid: f727fa008df302a4f839260c2d345287
|
guid: f727fa008df302a4f839260c2d345287
|
||||||
|
- enabled: 1
|
||||||
|
path: Assets/Scenes/BalanceDummy.unity
|
||||||
|
guid: 90d10d723d37d174c94429aff781504d
|
||||||
m_configObjects:
|
m_configObjects:
|
||||||
com.unity.dt.app-ui: {fileID: 11400000, guid: 99f9c9493070a9d4c979a8fec7c5a8d3, type: 2}
|
com.unity.dt.app-ui: {fileID: 11400000, guid: 99f9c9493070a9d4c979a8fec7c5a8d3, type: 2}
|
||||||
com.unity.input.settings.actions: {fileID: -944628639613478452, guid: 052faaac586de48259a63d0c4782560b, type: 3}
|
com.unity.input.settings.actions: {fileID: -944628639613478452, guid: 052faaac586de48259a63d0c4782560b, type: 3}
|
||||||
|
|||||||
Reference in New Issue
Block a user