feat: 젬 테스트 경로 및 보스 기절 디버그 추가

- 다중 젬 슬롯용 타입을 별도 스크립트로 분리하고 테스트 젬/로드아웃 자산 생성 경로를 정리

- 젬 테스트 전용 공격 스킬과 분리된 애니메이션 자산을 추가해 베이스 스킬 검증 경로를 마련

- PlayerSkillDebugMenu와 MPP 디버그 메뉴를 보강해 젬 프리셋 적용, 원격 테스트, 보스 기절 디버그 메뉴를 추가

- BossCombatBehaviorContext와 공통 BT 액션이 기절 상태를 존중하도록 수정해 보스 추적과 패턴 실행을 중단

- Unity 리프레시와 외부 빌드 통과를 확인하고 드로그전 및 MPP 기준 젬 프리셋 적용 흐름을 검증
This commit is contained in:
2026-03-25 18:38:12 +09:00
parent 35a5b272cb
commit 24b284ad7e
39 changed files with 4443 additions and 463 deletions

View File

@@ -47,6 +47,12 @@ public abstract partial class BossPatternActionBase : Action
if (!IsReady())
return Status.Failure;
if (combatBehaviorContext.IsBehaviorSuppressed)
{
StopMovement();
return Status.Failure;
}
if (bossEnemy.IsDead || bossEnemy.IsTransitioning)
return Status.Failure;
@@ -71,6 +77,12 @@ public abstract partial class BossPatternActionBase : Action
if (!IsReady() || activePattern == null)
return Status.Failure;
if (combatBehaviorContext.IsBehaviorSuppressed)
{
StopMovement();
return Status.Failure;
}
if (bossEnemy.IsDead || bossEnemy.IsTransitioning)
return Status.Failure;

View File

@@ -1,4 +1,5 @@
using System;
using Colosseum.Enemy;
using Unity.Behavior;
using UnityEngine;
using Action = Unity.Behavior.Action;
@@ -22,6 +23,12 @@ public partial class ChaseTargetAction : Action
protected override Status OnStart()
{
BossCombatBehaviorContext context = GameObject.GetComponent<BossCombatBehaviorContext>();
if (context != null && context.IsBehaviorSuppressed)
{
return Status.Failure;
}
if (Target.Value == null)
{
return Status.Failure;
@@ -47,6 +54,15 @@ public partial class ChaseTargetAction : Action
protected override Status OnUpdate()
{
BossCombatBehaviorContext context = GameObject.GetComponent<BossCombatBehaviorContext>();
if (context != null && context.IsBehaviorSuppressed)
{
if (agent != null)
agent.isStopped = true;
return Status.Failure;
}
if (Target.Value == null)
{
return Status.Failure;

View File

@@ -25,6 +25,9 @@ public abstract partial class CheckPatternReadyActionBase : Action
if (context == null)
return Status.Failure;
if (context.IsBehaviorSuppressed)
return Status.Failure;
BossPatternData pattern = context.GetPattern(PatternRole);
return UsePatternAction.IsPatternReady(GameObject, pattern) ? Status.Success : Status.Failure;
}

View File

@@ -21,6 +21,9 @@ public partial class CheckSignaturePatternReadyAction : Action
protected override Status OnStart()
{
BossCombatBehaviorContext context = GameObject.GetComponent<BossCombatBehaviorContext>();
if (context != null && context.IsBehaviorSuppressed)
return Status.Failure;
return context != null && context.IsSignaturePatternReady()
? Status.Success
: Status.Failure;

View File

@@ -24,6 +24,10 @@ public partial class RefreshPrimaryTargetAction : Action
protected override Status OnStart()
{
BossCombatBehaviorContext context = GameObject.GetComponent<BossCombatBehaviorContext>();
if (context != null && context.IsBehaviorSuppressed)
return Status.Failure;
EnemyBase enemyBase = GameObject.GetComponent<EnemyBase>();
if (enemyBase == null)
return Status.Failure;
@@ -34,7 +38,6 @@ public partial class RefreshPrimaryTargetAction : Action
if (resolvedTarget == null)
{
BossCombatBehaviorContext context = GameObject.GetComponent<BossCombatBehaviorContext>();
resolvedTarget = context != null ? context.FindNearestLivingTarget() : null;
}

View File

@@ -0,0 +1,356 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text;
using System.IO;
using Process = System.Diagnostics.Process;
using UnityEditor;
using UnityEngine;
namespace Colosseum.Editor
{
/// <summary>
/// Multiplayer Play Mode 관련 상태와 리플렉션 정보를 점검하는 디버그 메뉴입니다.
/// </summary>
public static class MultiplayerPlayModeDebugMenu
{
private const string MultiplayerManagerAssetPath = "ProjectSettings/MultiplayerManager.asset";
private const string DiagnosticsDirectory = "Temp/MPP";
private const string VirtualProjectsRoot = "Library/VP";
[MenuItem("Tools/Colosseum/Multiplayer/Log Play Mode Module Types")]
private static void LogPlayModeModuleTypes()
{
Assembly playModeAssembly = typeof(UnityEditor.PlayModeStateChange).Assembly;
Type[] types = playModeAssembly
.GetTypes()
.Where(type => type.FullName != null &&
(type.FullName.Contains("PlayMode", StringComparison.OrdinalIgnoreCase) ||
type.FullName.Contains("Scenario", StringComparison.OrdinalIgnoreCase) ||
type.FullName.Contains("Multiplayer", StringComparison.OrdinalIgnoreCase)))
.OrderBy(type => type.FullName)
.ToArray();
StringBuilder builder = new StringBuilder();
builder.AppendLine("[MPP] PlayModeModule 타입 목록");
for (int i = 0; i < types.Length; i++)
{
builder.Append("- ");
builder.AppendLine(types[i].FullName);
}
string diagnosticsPath = EnsureDiagnosticsFilePath("PlayModeModuleTypes.txt");
File.WriteAllText(diagnosticsPath, builder.ToString(), Encoding.UTF8);
Debug.Log($"[MPP] PlayModeModule 타입 목록을 저장했습니다. {diagnosticsPath}");
}
[MenuItem("Tools/Colosseum/Multiplayer/Log Play Mode User Settings")]
private static void LogPlayModeUserSettings()
{
Type settingsType = Type.GetType("Unity.PlayMode.Editor.PlayModeUserSettings, UnityEditor.PlayModeModule");
if (settingsType == null)
{
Debug.LogWarning("[MPP] Unity.PlayMode.Editor.PlayModeUserSettings 타입을 찾지 못했습니다.");
return;
}
string diagnosticsPath = EnsureDiagnosticsFilePath("PlayModeUserSettings.txt");
MethodInfo getOrCreateMethod = settingsType.GetMethod(
"GetOrCreateSettings",
BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic);
object settings = getOrCreateMethod?.Invoke(null, null);
if (settings == null)
{
StringBuilder nullBuilder = new StringBuilder();
nullBuilder.AppendLine("[MPP] PlayModeUserSettings 인스턴스를 가져오지 못했습니다.");
nullBuilder.AppendLine($"Type: {settingsType.FullName}");
nullBuilder.AppendLine("Static Members:");
AppendStaticMembers(nullBuilder, settingsType);
File.WriteAllText(diagnosticsPath, nullBuilder.ToString(), Encoding.UTF8);
Debug.LogWarning($"[MPP] PlayModeUserSettings 인스턴스를 가져오지 못했습니다. 진단 파일: {diagnosticsPath}");
return;
}
StringBuilder builder = new StringBuilder();
builder.AppendLine("[MPP] PlayModeUserSettings");
AppendMembers(builder, settingsType, settings);
AppendStaticMembers(builder, settingsType);
File.WriteAllText(diagnosticsPath, builder.ToString(), Encoding.UTF8);
Debug.Log($"[MPP] PlayModeUserSettings 정보를 저장했습니다. {diagnosticsPath}");
}
[MenuItem("Tools/Colosseum/Multiplayer/Enable Local Deployment")]
private static void EnableLocalDeployment()
{
SerializedObject multiplayerManager = GetMultiplayerManagerSerializedObject();
if (multiplayerManager == null)
{
return;
}
SerializedProperty localDeployment = multiplayerManager.FindProperty("m_EnablePlayModeLocalDeployment");
if (localDeployment == null)
{
Debug.LogWarning("[MPP] m_EnablePlayModeLocalDeployment 속성을 찾지 못했습니다.");
return;
}
localDeployment.intValue = 1;
multiplayerManager.ApplyModifiedPropertiesWithoutUndo();
AssetDatabase.SaveAssets();
Debug.Log("[MPP] 로컬 Play Mode 배포를 활성화했습니다.");
}
[MenuItem("Tools/Colosseum/Multiplayer/Disable Local Deployment")]
private static void DisableLocalDeployment()
{
SerializedObject multiplayerManager = GetMultiplayerManagerSerializedObject();
if (multiplayerManager == null)
{
return;
}
SerializedProperty localDeployment = multiplayerManager.FindProperty("m_EnablePlayModeLocalDeployment");
if (localDeployment == null)
{
Debug.LogWarning("[MPP] m_EnablePlayModeLocalDeployment 속성을 찾지 못했습니다.");
return;
}
localDeployment.intValue = 0;
multiplayerManager.ApplyModifiedPropertiesWithoutUndo();
AssetDatabase.SaveAssets();
Debug.Log("[MPP] 로컬 Play Mode 배포를 비활성화했습니다.");
}
[MenuItem("Tools/Colosseum/Multiplayer/Log Multiplayer Manager Settings")]
private static void LogMultiplayerManagerSettings()
{
SerializedObject multiplayerManager = GetMultiplayerManagerSerializedObject();
if (multiplayerManager == null)
{
return;
}
SerializedProperty roles = multiplayerManager.FindProperty("m_EnableMultiplayerRoles");
SerializedProperty localDeployment = multiplayerManager.FindProperty("m_EnablePlayModeLocalDeployment");
SerializedProperty remoteDeployment = multiplayerManager.FindProperty("m_EnablePlayModeRemoteDeployment");
Debug.Log(
$"[MPP] MultiplayerManager | Roles={roles?.intValue ?? -1} | " +
$"LocalDeployment={localDeployment?.intValue ?? -1} | " +
$"RemoteDeployment={remoteDeployment?.intValue ?? -1}");
}
[MenuItem("Tools/Colosseum/Multiplayer/Log Virtual Player Clones")]
private static void LogVirtualPlayerClones()
{
string[] cloneDirectories = GetVirtualPlayerCloneDirectories();
if (cloneDirectories.Length == 0)
{
Debug.LogWarning("[MPP] Library/VP 아래에 가상 플레이어 복제본을 찾지 못했습니다.");
return;
}
StringBuilder builder = new StringBuilder();
builder.AppendLine("[MPP] 가상 플레이어 복제본 목록");
for (int i = 0; i < cloneDirectories.Length; i++)
{
builder.Append("- ");
builder.AppendLine(Path.GetFullPath(cloneDirectories[i]));
}
Debug.Log(builder.ToString());
}
[MenuItem("Tools/Colosseum/Multiplayer/Launch First Virtual Player Clone")]
private static void LaunchFirstVirtualPlayerClone()
{
string[] cloneDirectories = GetVirtualPlayerCloneDirectories();
if (cloneDirectories.Length == 0)
{
Debug.LogWarning("[MPP] 실행할 가상 플레이어 복제본이 없습니다.");
return;
}
string cloneProjectPath = Path.GetFullPath(cloneDirectories[0]);
Process.Start(new System.Diagnostics.ProcessStartInfo
{
FileName = EditorApplication.applicationPath,
Arguments = $"-projectPath \"{cloneProjectPath}\"",
UseShellExecute = true,
});
Debug.Log($"[MPP] 가상 플레이어 복제본을 실행했습니다. {cloneProjectPath}");
}
private static SerializedObject GetMultiplayerManagerSerializedObject()
{
UnityEngine.Object[] assets = AssetDatabase.LoadAllAssetsAtPath(MultiplayerManagerAssetPath);
if (assets == null || assets.Length == 0 || assets[0] == null)
{
Debug.LogWarning("[MPP] MultiplayerManager.asset를 찾지 못했습니다.");
return null;
}
return new SerializedObject(assets[0]);
}
private static void AppendMembers(StringBuilder builder, Type settingsType, object settings)
{
const BindingFlags flags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic;
List<PropertyInfo> properties = settingsType.GetProperties(flags)
.Where(property => property.GetIndexParameters().Length == 0)
.OrderBy(property => property.Name)
.ToList();
for (int i = 0; i < properties.Count; i++)
{
PropertyInfo property = properties[i];
object value = null;
bool success = true;
try
{
value = property.GetValue(settings);
}
catch (Exception exception)
{
success = false;
value = exception.GetType().Name;
}
builder.Append("- Property ");
builder.Append(property.Name);
builder.Append(" = ");
builder.AppendLine(success ? FormatValue(value) : $"<error: {value}>");
}
List<FieldInfo> fields = settingsType.GetFields(flags)
.OrderBy(field => field.Name)
.ToList();
for (int i = 0; i < fields.Count; i++)
{
FieldInfo field = fields[i];
object value = field.GetValue(settings);
builder.Append("- Field ");
builder.Append(field.Name);
builder.Append(" = ");
builder.AppendLine(FormatValue(value));
}
}
private static void AppendStaticMembers(StringBuilder builder, Type settingsType)
{
const BindingFlags flags = BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic;
List<PropertyInfo> properties = settingsType.GetProperties(flags)
.Where(property => property.GetIndexParameters().Length == 0)
.OrderBy(property => property.Name)
.ToList();
for (int i = 0; i < properties.Count; i++)
{
PropertyInfo property = properties[i];
object value = null;
bool success = true;
try
{
value = property.GetValue(null);
}
catch (Exception exception)
{
success = false;
value = exception.GetType().Name;
}
builder.Append("- Static Property ");
builder.Append(property.Name);
builder.Append(" = ");
builder.AppendLine(success ? FormatValue(value) : $"<error: {value}>");
}
List<FieldInfo> fields = settingsType.GetFields(flags)
.OrderBy(field => field.Name)
.ToList();
for (int i = 0; i < fields.Count; i++)
{
FieldInfo field = fields[i];
object value = field.GetValue(null);
builder.Append("- Static Field ");
builder.Append(field.Name);
builder.Append(" = ");
builder.AppendLine(FormatValue(value));
}
List<MethodInfo> methods = settingsType.GetMethods(flags)
.Where(method => !method.IsSpecialName)
.OrderBy(method => method.Name)
.ToList();
for (int i = 0; i < methods.Count; i++)
{
MethodInfo method = methods[i];
string parameterSummary = string.Join(
", ",
method.GetParameters().Select(parameter => $"{parameter.ParameterType.Name} {parameter.Name}"));
builder.Append("- Static Method ");
builder.Append(method.ReturnType.Name);
builder.Append(' ');
builder.Append(method.Name);
builder.Append('(');
builder.Append(parameterSummary);
builder.AppendLine(")");
}
}
private static string EnsureDiagnosticsFilePath(string fileName)
{
Directory.CreateDirectory(DiagnosticsDirectory);
return Path.Combine(DiagnosticsDirectory, fileName);
}
private static string[] GetVirtualPlayerCloneDirectories()
{
if (!Directory.Exists(VirtualProjectsRoot))
{
return Array.Empty<string>();
}
return Directory
.GetDirectories(VirtualProjectsRoot, "mppm*")
.OrderBy(path => path)
.ToArray();
}
private static string FormatValue(object value)
{
if (value == null)
{
return "null";
}
if (value is string stringValue)
{
return stringValue;
}
if (value is IEnumerable<object> enumerable)
{
return "[" + string.Join(", ", enumerable.Select(FormatValue)) + "]";
}
return value.ToString();
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: eb18c83f7c6efff429f59061e7f0b07b

View File

@@ -23,6 +23,7 @@ namespace Colosseum.Editor
private const string ShieldSkillPath = "Assets/_Game/Data/Skills/Data_Skill_Player_보호막.asset";
private const string SlashSkillPath = "Assets/_Game/Data/Skills/Data_Skill_Player_베기.asset";
private const string PierceSkillPath = "Assets/_Game/Data/Skills/Data_Skill_Player_찌르기.asset";
private const string GemTestSkillPath = "Assets/_Game/Data/Skills/Data_Skill_Player_젬테스트공격.asset";
private const string SpinSkillPath = "Assets/_Game/Data/Skills/Data_Skill_Player_회전베기.asset";
private const string DashSkillPath = "Assets/_Game/Data/Skills/Data_Skill_Player_돌진.asset";
private const string ProjectileSkillPath = "Assets/_Game/Data/Skills/Data_Skill_Player_투사체.asset";
@@ -33,6 +34,14 @@ namespace Colosseum.Editor
private const string StunAbnormalityPath = "Assets/_Game/Data/Abnormalities/Data_Abnormality_Player_Stun.asset";
private const string SilenceAbnormalityPath = "Assets/_Game/Data/Abnormalities/Data_Abnormality_Player_Silence.asset";
private const string MarkAbnormalityPath = "Assets/_Game/Data/Abnormalities/Data_Abnormality_Player_집행자의낙인.asset";
private const string SkillGemFolderPath = "Assets/_Game/Data/SkillGems";
private const string LoadoutPresetFolderPath = "Assets/_Game/Data/Loadouts";
private const string CrushGemPath = SkillGemFolderPath + "/Data_SkillGem_Player_파쇄.asset";
private const string ChallengerGemPath = SkillGemFolderPath + "/Data_SkillGem_Player_도전자.asset";
private const string GuardianGemPath = SkillGemFolderPath + "/Data_SkillGem_Player_수호.asset";
private const string TankGemPresetPath = LoadoutPresetFolderPath + "/Data_LoadoutPreset_Player_탱커_젬테스트.asset";
private const string SupportGemPresetPath = LoadoutPresetFolderPath + "/Data_LoadoutPreset_Player_지원_젬테스트.asset";
private const string DpsGemPresetPath = LoadoutPresetFolderPath + "/Data_LoadoutPreset_Player_딜러_젬테스트.asset";
[MenuItem("Tools/Colosseum/Debug/Cast Local Skill 3")]
private static void CastLocalSkill3()
@@ -40,6 +49,12 @@ namespace Colosseum.Editor
CastLocalSkill(2);
}
[MenuItem("Tools/Colosseum/Debug/Cast Local Skill R")]
private static void CastLocalSkillR()
{
CastLocalSkill(1);
}
[MenuItem("Tools/Colosseum/Debug/Cast Local Skill 4")]
private static void CastLocalSkill4()
{
@@ -52,6 +67,42 @@ namespace Colosseum.Editor
CastLocalSkill(4);
}
[MenuItem("Tools/Colosseum/Debug/Cast Local Skill 6")]
private static void CastLocalSkill6()
{
CastLocalSkill(5);
}
[MenuItem("Tools/Colosseum/Debug/Cast Client1 Skill R")]
private static void CastClient1SkillR()
{
CastOwnedPlayerSkillAsServer(1, 1);
}
[MenuItem("Tools/Colosseum/Debug/Cast Client1 Skill 1")]
private static void CastClient1Skill1()
{
CastOwnedPlayerSkillAsServer(1, 2);
}
[MenuItem("Tools/Colosseum/Debug/Cast Client1 Skill 2")]
private static void CastClient1Skill2()
{
CastOwnedPlayerSkillAsServer(1, 3);
}
[MenuItem("Tools/Colosseum/Debug/Cast Client1 Skill 3")]
private static void CastClient1Skill3()
{
CastOwnedPlayerSkillAsServer(1, 4);
}
[MenuItem("Tools/Colosseum/Debug/Cast Client1 Skill 4")]
private static void CastClient1Skill4()
{
CastOwnedPlayerSkillAsServer(1, 5);
}
[MenuItem("Tools/Colosseum/Debug/Cast Local Heal")]
private static void CastLocalHeal()
{
@@ -134,14 +185,14 @@ namespace Colosseum.Editor
continue;
if (builder.Length > 0)
builder.AppendLine().AppendLine();
builder.Append(" || ");
builder.Append(enemy.name);
builder.Append(" : ");
builder.Append(enemy.GetThreatDebugSummary().Replace("\r\n", " | ").Replace("\n", " | "));
}
Debug.Log($"[Debug] 보스 위협 요약\n{builder}");
Debug.Log($"[Debug] 보스 위협 요약 | {builder}");
}
[MenuItem("Tools/Colosseum/Debug/Apply Local Stun")]
@@ -162,6 +213,33 @@ namespace Colosseum.Editor
ApplyLocalAbnormality(MarkAbnormalityPath);
}
[MenuItem("Tools/Colosseum/Debug/Apply Boss Stun")]
private static void ApplyBossStun()
{
if (!EditorApplication.isPlaying)
{
Debug.LogWarning("[Debug] 플레이 모드에서만 사용할 수 있습니다.");
return;
}
AbnormalityManager abnormalityManager = FindBossAbnormalityManager();
if (abnormalityManager == null)
{
Debug.LogWarning("[Debug] 보스 AbnormalityManager를 찾지 못했습니다.");
return;
}
AbnormalityData abnormality = AssetDatabase.LoadAssetAtPath<AbnormalityData>(StunAbnormalityPath);
if (abnormality == null)
{
Debug.LogWarning($"[Debug] 기절 이상상태 에셋을 찾지 못했습니다: {StunAbnormalityPath}");
return;
}
abnormalityManager.ApplyAbnormality(abnormality, abnormalityManager.gameObject);
Debug.Log($"[Debug] 보스에게 기절 적용 | Target={abnormalityManager.gameObject.name} | Abnormality={abnormality.abnormalityName}");
}
[MenuItem("Tools/Colosseum/Debug/Log HUD Abnormality Summary")]
private static void LogHudAbnormalitySummary()
{
@@ -223,6 +301,131 @@ namespace Colosseum.Editor
EvadeSkillPath);
}
[MenuItem("Tools/Colosseum/Debug/Apply Tank Gem Loadout")]
private static void ApplyTankGemLoadout()
{
ApplyLoadoutPreset(TankGemPresetPath, "탱커 젬");
}
[MenuItem("Tools/Colosseum/Debug/Apply Support Gem Loadout")]
private static void ApplySupportGemLoadout()
{
ApplyLoadoutPreset(SupportGemPresetPath, "지원 젬");
}
[MenuItem("Tools/Colosseum/Debug/Apply DPS Gem Loadout")]
private static void ApplyDpsGemLoadout()
{
ApplyLoadoutPreset(DpsGemPresetPath, "딜러 젬");
}
[MenuItem("Tools/Colosseum/Setup/Create or Update Test Skill Gems")]
public static void CreateOrUpdateTestSkillGems()
{
EnsureFolder("Assets/_Game/Data", "SkillGems");
SkillEffect damageEffect = AssetDatabase.LoadAssetAtPath<SkillEffect>("Assets/_Game/Data/Skills/Effects/Data_SkillEffect_Player_찌르기_0_데미지.asset");
SkillEffect tauntEffect = AssetDatabase.LoadAssetAtPath<SkillEffect>("Assets/_Game/Data/Skills/Effects/Data_SkillEffect_Player_도발_0_도발.asset");
SkillEffect shieldEffect = AssetDatabase.LoadAssetAtPath<SkillEffect>("Assets/_Game/Data/Skills/Effects/Data_SkillEffect_Player_보호막_0_보호막.asset");
CreateOrUpdateGemAsset(
CrushGemPath,
"파쇄",
"고위력 기술의 단일 피해를 강화하는 테스트용 젬",
1.15f,
1.1f,
damageEffect);
CreateOrUpdateGemAsset(
ChallengerGemPath,
"도전자",
"고위력 기술에 위협 선점 기능을 얹는 테스트용 젬",
1f,
1f,
tauntEffect);
CreateOrUpdateGemAsset(
GuardianGemPath,
"수호",
"고위력 기술에 보호막 보조를 얹는 테스트용 젬",
1.05f,
1.1f,
shieldEffect);
AssetDatabase.SaveAssets();
AssetDatabase.Refresh();
Debug.Log("[Debug] 테스트용 젬 자산 생성/갱신 완료");
}
[MenuItem("Tools/Colosseum/Setup/Create or Update Test Loadout Presets")]
public static void CreateOrUpdateTestLoadoutPresets()
{
EnsureFolder("Assets/_Game/Data", "Loadouts");
CreateOrUpdateTestSkillGems();
SkillData slashSkill = AssetDatabase.LoadAssetAtPath<SkillData>(SlashSkillPath);
SkillData tauntSkill = AssetDatabase.LoadAssetAtPath<SkillData>(TauntSkillPath);
SkillData guardSkill = AssetDatabase.LoadAssetAtPath<SkillData>(GuardSkillPath);
SkillData dashSkill = AssetDatabase.LoadAssetAtPath<SkillData>(DashSkillPath);
SkillData ironWallSkill = AssetDatabase.LoadAssetAtPath<SkillData>(IronWallSkillPath);
SkillData pierceSkill = AssetDatabase.LoadAssetAtPath<SkillData>(PierceSkillPath);
SkillData gemTestSkill = AssetDatabase.LoadAssetAtPath<SkillData>(GemTestSkillPath);
SkillData healSkill = AssetDatabase.LoadAssetAtPath<SkillData>(HealSkillPath);
SkillData areaHealSkill = AssetDatabase.LoadAssetAtPath<SkillData>(AreaHealSkillPath);
SkillData shieldSkill = AssetDatabase.LoadAssetAtPath<SkillData>(ShieldSkillPath);
SkillData projectileSkill = AssetDatabase.LoadAssetAtPath<SkillData>(ProjectileSkillPath);
SkillData spinSkill = AssetDatabase.LoadAssetAtPath<SkillData>(SpinSkillPath);
SkillData evadeSkill = AssetDatabase.LoadAssetAtPath<SkillData>(EvadeSkillPath);
SkillGemData crushGem = AssetDatabase.LoadAssetAtPath<SkillGemData>(CrushGemPath);
SkillGemData challengerGem = AssetDatabase.LoadAssetAtPath<SkillGemData>(ChallengerGemPath);
SkillGemData guardianGem = AssetDatabase.LoadAssetAtPath<SkillGemData>(GuardianGemPath);
CreateOrUpdatePresetAsset(
TankGemPresetPath,
"탱커 젬 테스트",
"도전자 젬을 사용하는 탱커 검증 프리셋",
CreateLoadoutEntries(
CreateEntry(slashSkill),
CreateEntry(tauntSkill),
CreateEntry(guardSkill),
CreateEntry(dashSkill),
CreateEntry(ironWallSkill),
CreateEntry(gemTestSkill, challengerGem),
CreateEntry(evadeSkill)));
CreateOrUpdatePresetAsset(
SupportGemPresetPath,
"지원 젬 테스트",
"수호 젬을 사용하는 지원 검증 프리셋",
CreateLoadoutEntries(
CreateEntry(slashSkill),
CreateEntry(healSkill),
CreateEntry(areaHealSkill),
CreateEntry(shieldSkill),
CreateEntry(dashSkill),
CreateEntry(gemTestSkill, guardianGem),
CreateEntry(evadeSkill)));
CreateOrUpdatePresetAsset(
DpsGemPresetPath,
"딜러 젬 테스트",
"파쇄 젬을 사용하는 딜러 검증 프리셋",
CreateLoadoutEntries(
CreateEntry(slashSkill),
CreateEntry(pierceSkill),
CreateEntry(spinSkill),
CreateEntry(dashSkill),
CreateEntry(projectileSkill),
CreateEntry(gemTestSkill, crushGem),
CreateEntry(evadeSkill)));
AssetDatabase.SaveAssets();
AssetDatabase.Refresh();
Debug.Log("[Debug] 테스트용 젬 프리셋 생성/갱신 완료");
}
[MenuItem("Tools/Colosseum/Debug/Log Local Skill Loadout")]
private static void LogLocalSkillLoadout()
{
@@ -245,9 +448,11 @@ namespace Colosseum.Editor
for (int i = 0; i < slotOrder.Length; i++)
{
SkillData skill = localSkillInput.GetSkill(slotOrder[i]);
SkillLoadoutEntry loadoutEntry = localSkillInput.GetSkillLoadout(slotOrder[i]);
builder.Append(slotNames[i]);
builder.Append(": ");
builder.Append(skill != null ? skill.SkillName : "(비어 있음)");
AppendGemSummary(builder, loadoutEntry);
if (i < slotOrder.Length - 1)
builder.Append(" | ");
@@ -291,6 +496,20 @@ namespace Colosseum.Editor
return localNetworkController.GetComponent<AbnormalityManager>();
}
private static AbnormalityManager FindBossAbnormalityManager()
{
BossEnemy activeBoss = BossEnemy.ActiveBoss;
if (activeBoss != null)
{
AbnormalityManager activeManager = activeBoss.GetComponent<AbnormalityManager>();
if (activeManager != null)
return activeManager;
}
BossEnemy bossEnemy = Object.FindFirstObjectByType<BossEnemy>();
return bossEnemy != null ? bossEnemy.GetComponent<AbnormalityManager>() : null;
}
private static void CastLocalSkill(int slotIndex)
{
if (!EditorApplication.isPlaying)
@@ -391,5 +610,197 @@ namespace Colosseum.Editor
localSkillInput.SetSkills(skills);
Debug.Log($"[Debug] {loadoutName} 프리셋을 적용했습니다.");
}
private static void ApplyLoadoutPreset(string presetPath, string presetLabel)
{
if (!EditorApplication.isPlaying)
{
Debug.LogWarning("[Debug] 플레이 모드에서만 사용할 수 있습니다.");
return;
}
PlayerSkillInput localSkillInput = FindLocalSkillInput();
if (localSkillInput == null)
{
Debug.LogWarning("[Debug] 로컬 PlayerSkillInput을 찾지 못했습니다.");
return;
}
PlayerLoadoutPreset preset = AssetDatabase.LoadAssetAtPath<PlayerLoadoutPreset>(presetPath);
if (preset == null)
{
Debug.LogWarning($"[Debug] 프리셋 에셋을 찾지 못했습니다: {presetPath}");
return;
}
localSkillInput.ApplyLoadoutPreset(preset);
Debug.Log($"[Debug] {presetLabel} 프리셋을 적용했습니다.");
}
private static void EnsureFolder(string parentFolder, string childFolderName)
{
string combined = $"{parentFolder}/{childFolderName}";
if (AssetDatabase.IsValidFolder(combined))
return;
AssetDatabase.CreateFolder(parentFolder, childFolderName);
}
private static SkillLoadoutEntry[] CreateLoadoutEntries(params SkillLoadoutEntry[] entries)
{
return entries;
}
private static SkillLoadoutEntry CreateEntry(SkillData skill, params SkillGemData[] gems)
{
SkillLoadoutEntry entry = SkillLoadoutEntry.CreateTemporary(skill);
if (gems == null)
return entry;
for (int i = 0; i < gems.Length; i++)
{
entry.SetGem(i, gems[i]);
}
return entry;
}
private static void CreateOrUpdateGemAsset(string assetPath, string gemName, string description, float manaCostMultiplier, float cooldownMultiplier, SkillEffect triggeredEffect)
{
SkillGemData gem = AssetDatabase.LoadAssetAtPath<SkillGemData>(assetPath);
if (gem == null)
{
if (AssetDatabase.LoadMainAssetAtPath(assetPath) != null)
{
AssetDatabase.DeleteAsset(assetPath);
}
gem = ScriptableObject.CreateInstance<SkillGemData>();
AssetDatabase.CreateAsset(gem, assetPath);
}
SerializedObject serializedGem = new SerializedObject(gem);
serializedGem.FindProperty("gemName").stringValue = gemName;
serializedGem.FindProperty("description").stringValue = description;
serializedGem.FindProperty("manaCostMultiplier").floatValue = manaCostMultiplier;
serializedGem.FindProperty("cooldownMultiplier").floatValue = cooldownMultiplier;
SerializedProperty castStartEffectsProperty = serializedGem.FindProperty("castStartEffects");
castStartEffectsProperty.arraySize = 0;
SerializedProperty triggeredEffectsProperty = serializedGem.FindProperty("triggeredEffects");
triggeredEffectsProperty.arraySize = triggeredEffect != null ? 1 : 0;
if (triggeredEffect != null)
{
SerializedProperty triggeredEntry = triggeredEffectsProperty.GetArrayElementAtIndex(0);
triggeredEntry.FindPropertyRelative("triggerIndex").intValue = 0;
SerializedProperty effectArray = triggeredEntry.FindPropertyRelative("effects");
effectArray.arraySize = 1;
effectArray.GetArrayElementAtIndex(0).objectReferenceValue = triggeredEffect;
}
serializedGem.ApplyModifiedPropertiesWithoutUndo();
EditorUtility.SetDirty(gem);
}
private static void CreateOrUpdatePresetAsset(string assetPath, string presetName, string description, IReadOnlyList<SkillLoadoutEntry> entries)
{
PlayerLoadoutPreset preset = AssetDatabase.LoadAssetAtPath<PlayerLoadoutPreset>(assetPath);
if (preset == null)
{
if (AssetDatabase.LoadMainAssetAtPath(assetPath) != null)
{
AssetDatabase.DeleteAsset(assetPath);
}
preset = ScriptableObject.CreateInstance<PlayerLoadoutPreset>();
AssetDatabase.CreateAsset(preset, assetPath);
}
SerializedObject serializedPreset = new SerializedObject(preset);
serializedPreset.FindProperty("presetName").stringValue = presetName;
serializedPreset.FindProperty("description").stringValue = description;
SerializedProperty slotsProperty = serializedPreset.FindProperty("slots");
slotsProperty.arraySize = entries != null ? entries.Count : 0;
for (int i = 0; i < slotsProperty.arraySize; i++)
{
SkillLoadoutEntry entry = entries[i] != null ? entries[i].CreateCopy() : new SkillLoadoutEntry();
SerializedProperty slotProperty = slotsProperty.GetArrayElementAtIndex(i);
slotProperty.FindPropertyRelative("baseSkill").objectReferenceValue = entry.BaseSkill;
SerializedProperty gemsProperty = slotProperty.FindPropertyRelative("socketedGems");
gemsProperty.arraySize = entry.SocketedGems.Count;
for (int j = 0; j < gemsProperty.arraySize; j++)
{
gemsProperty.GetArrayElementAtIndex(j).objectReferenceValue = entry.GetGem(j);
}
}
serializedPreset.ApplyModifiedPropertiesWithoutUndo();
EditorUtility.SetDirty(preset);
}
private static void AppendGemSummary(StringBuilder builder, SkillLoadoutEntry loadoutEntry)
{
if (builder == null || loadoutEntry == null || loadoutEntry.SocketedGems == null)
return;
bool hasGem = false;
StringBuilder gemBuilder = new StringBuilder();
for (int i = 0; i < loadoutEntry.SocketedGems.Count; i++)
{
SkillGemData gem = loadoutEntry.SocketedGems[i];
if (gem == null)
continue;
if (hasGem)
gemBuilder.Append(", ");
gemBuilder.Append(gem.GemName);
hasGem = true;
}
if (!hasGem)
return;
builder.Append(" [");
builder.Append(gemBuilder);
builder.Append("]");
}
private static void CastOwnedPlayerSkillAsServer(ulong ownerClientId, int slotIndex)
{
if (!EditorApplication.isPlaying)
{
Debug.LogWarning("[Debug] 플레이 모드에서만 사용할 수 있습니다.");
return;
}
PlayerSkillInput playerSkillInput = FindPlayerSkillInputByOwner(ownerClientId);
if (playerSkillInput == null)
{
Debug.LogWarning($"[Debug] OwnerClientId={ownerClientId} 인 PlayerSkillInput을 찾지 못했습니다.");
return;
}
bool executed = playerSkillInput.DebugExecuteSkillAsServer(slotIndex);
Debug.Log($"[Debug] 원격 스킬 실행 요청 | OwnerClientId={ownerClientId} | Slot={slotIndex} | Success={executed}");
}
private static PlayerSkillInput FindPlayerSkillInputByOwner(ulong ownerClientId)
{
PlayerSkillInput[] skillInputs = Object.FindObjectsByType<PlayerSkillInput>(FindObjectsInactive.Exclude, FindObjectsSortMode.None);
for (int i = 0; i < skillInputs.Length; i++)
{
PlayerSkillInput skillInput = skillInputs[i];
if (skillInput != null && skillInput.OwnerClientId == ownerClientId)
return skillInput;
}
return null;
}
}
}

View File

@@ -27,6 +27,7 @@ namespace Colosseum.Enemy
[SerializeField] protected BossEnemy bossEnemy;
[SerializeField] protected EnemyBase enemyBase;
[SerializeField] protected SkillController skillController;
[SerializeField] protected AbnormalityManager abnormalityManager;
[SerializeField] protected UnityEngine.AI.NavMeshAgent navMeshAgent;
[SerializeField] protected BehaviorGraphAgent behaviorGraphAgent;
@@ -192,6 +193,11 @@ namespace Colosseum.Enemy
/// </summary>
public bool DebugModeEnabled => debugMode;
/// <summary>
/// 기절 등으로 인해 보스 전투 로직을 진행할 수 없는 상태인지 여부
/// </summary>
public bool IsBehaviorSuppressed => abnormalityManager != null && abnormalityManager.IsStunned;
/// <summary>
/// 현재 보스 패턴 페이즈
/// </summary>
@@ -238,6 +244,12 @@ namespace Colosseum.Enemy
if (bossEnemy.IsDead || bossEnemy.IsTransitioning)
return;
if (IsBehaviorSuppressed)
{
StopMovement();
return;
}
if (!disableBehaviorGraph)
return;
@@ -468,6 +480,9 @@ namespace Colosseum.Enemy
if (!IsServer || bossEnemy == null || skillController == null)
return false;
if (IsBehaviorSuppressed)
return false;
if (CurrentPatternPhase < signatureMinPhase)
return false;
@@ -699,6 +714,9 @@ namespace Colosseum.Enemy
if (skillController == null)
skillController = GetComponent<SkillController>();
if (abnormalityManager == null)
abnormalityManager = GetComponent<AbnormalityManager>();
if (navMeshAgent == null)
navMeshAgent = GetComponent<UnityEngine.AI.NavMeshAgent>();

View File

@@ -7,6 +7,13 @@ using System.Collections.Generic;
using Colosseum.Skills;
using Colosseum.Weapons;
#if UNITY_EDITOR
using System.IO;
using System.Text.RegularExpressions;
using UnityEditor;
#endif
namespace Colosseum.Player
{
/// <summary>
@@ -18,9 +25,46 @@ namespace Colosseum.Player
{
private const int ExpectedSkillSlotCount = 7;
#if UNITY_EDITOR
private static readonly string[] TankLoadoutPaths =
{
"Assets/_Game/Data/Skills/Data_Skill_Player_베기.asset",
"Assets/_Game/Data/Skills/Data_Skill_Player_도발.asset",
"Assets/_Game/Data/Skills/Data_Skill_Player_방어태세.asset",
"Assets/_Game/Data/Skills/Data_Skill_Player_돌진.asset",
"Assets/_Game/Data/Skills/Data_Skill_Player_철벽.asset",
"Assets/_Game/Data/Skills/Data_Skill_Player_찌르기.asset",
"Assets/_Game/Data/Skills/Data_Skill_Player_구르기.asset",
};
private static readonly string[] SupportLoadoutPaths =
{
"Assets/_Game/Data/Skills/Data_Skill_Player_베기.asset",
"Assets/_Game/Data/Skills/Data_Skill_Player_치유.asset",
"Assets/_Game/Data/Skills/Data_Skill_Player_광역치유.asset",
"Assets/_Game/Data/Skills/Data_Skill_Player_보호막.asset",
"Assets/_Game/Data/Skills/Data_Skill_Player_돌진.asset",
"Assets/_Game/Data/Skills/Data_Skill_Player_투사체.asset",
"Assets/_Game/Data/Skills/Data_Skill_Player_구르기.asset",
};
private static readonly string[] DpsLoadoutPaths =
{
"Assets/_Game/Data/Skills/Data_Skill_Player_베기.asset",
"Assets/_Game/Data/Skills/Data_Skill_Player_찌르기.asset",
"Assets/_Game/Data/Skills/Data_Skill_Player_회전베기.asset",
"Assets/_Game/Data/Skills/Data_Skill_Player_돌진.asset",
"Assets/_Game/Data/Skills/Data_Skill_Player_투사체.asset",
"Assets/_Game/Data/Skills/Data_Skill_Player_보호막.asset",
"Assets/_Game/Data/Skills/Data_Skill_Player_구르기.asset",
};
#endif
[Header("Skill Slots")]
[Tooltip("각 슬롯에 등록할 스킬 데이터 (6개 + 추가 슬롯)")]
[SerializeField] private SkillData[] skillSlots = new SkillData[ExpectedSkillSlotCount];
[Tooltip("각 슬롯의 베이스 스킬 + 젬 조합")]
[SerializeField] private SkillLoadoutEntry[] skillLoadoutEntries = new SkillLoadoutEntry[ExpectedSkillSlotCount];
[Header("References")]
[Tooltip("SkillController (없으면 자동 검색)")]
@@ -35,6 +79,7 @@ namespace Colosseum.Player
private InputSystem_Actions inputActions;
public SkillData[] SkillSlots => skillSlots;
public SkillLoadoutEntry[] SkillLoadoutEntries => skillLoadoutEntries;
/// <summary>
/// 스킬 슬롯 구성이 변경되었을 때 호출됩니다.
@@ -44,6 +89,10 @@ namespace Colosseum.Player
public override void OnNetworkSpawn()
{
EnsureSkillSlotCapacity();
EnsureSkillLoadoutCapacity();
SyncLegacySkillsToLoadoutEntries();
EnsureRuntimeReferences();
ApplyEditorMultiplayerLoadoutIfNeeded();
if (!IsOwner)
{
@@ -51,35 +100,6 @@ namespace Colosseum.Player
return;
}
// SkillController 참조 확인
if (skillController == null)
{
skillController = GetComponent<SkillController>();
if (skillController == null)
{
Debug.LogError("PlayerSkillInput: SkillController not found!");
enabled = false;
return;
}
}
// PlayerNetworkController 참조 확인
if (networkController == null)
{
networkController = GetComponent<PlayerNetworkController>();
}
// WeaponEquipment 참조 확인
if (weaponEquipment == null)
{
weaponEquipment = GetComponent<WeaponEquipment>();
}
if (actionState == null)
{
actionState = GetOrCreateActionState();
}
InitializeInputActions();
}
@@ -113,11 +133,15 @@ namespace Colosseum.Player
private void Awake()
{
EnsureSkillSlotCapacity();
EnsureSkillLoadoutCapacity();
SyncLegacySkillsToLoadoutEntries();
}
private void OnValidate()
{
EnsureSkillSlotCapacity();
EnsureSkillLoadoutCapacity();
SyncLegacySkillsToLoadoutEntries();
}
private void OnEnable()
@@ -150,6 +174,63 @@ namespace Colosseum.Player
skillSlots = resizedSlots;
}
/// <summary>
/// 슬롯별 로드아웃 엔트리 배열을 보정합니다.
/// </summary>
private void EnsureSkillLoadoutCapacity()
{
if (skillLoadoutEntries == null || skillLoadoutEntries.Length != ExpectedSkillSlotCount)
{
SkillLoadoutEntry[] resizedEntries = new SkillLoadoutEntry[ExpectedSkillSlotCount];
if (skillLoadoutEntries != null)
{
int copyCount = Mathf.Min(skillLoadoutEntries.Length, resizedEntries.Length);
for (int i = 0; i < copyCount; i++)
{
resizedEntries[i] = skillLoadoutEntries[i];
}
}
skillLoadoutEntries = resizedEntries;
}
for (int i = 0; i < skillLoadoutEntries.Length; i++)
{
if (skillLoadoutEntries[i] == null)
skillLoadoutEntries[i] = new SkillLoadoutEntry();
skillLoadoutEntries[i].EnsureGemSlotCapacity();
}
}
/// <summary>
/// 기존 SkillData 직렬화와 새 로드아웃 엔트리 구조를 동기화합니다.
/// </summary>
private void SyncLegacySkillsToLoadoutEntries()
{
EnsureSkillSlotCapacity();
EnsureSkillLoadoutCapacity();
for (int i = 0; i < skillSlots.Length; i++)
{
SkillLoadoutEntry entry = skillLoadoutEntries[i];
SkillData legacySkill = skillSlots[i];
if (entry.BaseSkill == null && legacySkill != null)
{
entry.SetBaseSkill(legacySkill);
}
else if (legacySkill == null && entry.BaseSkill != null)
{
skillSlots[i] = entry.BaseSkill;
}
else if (entry.BaseSkill != legacySkill)
{
skillSlots[i] = entry.BaseSkill;
}
}
}
private void CleanupInputActions()
{
if (inputActions != null)
@@ -166,7 +247,8 @@ namespace Colosseum.Player
if (slotIndex < 0 || slotIndex >= skillSlots.Length)
return;
SkillData skill = skillSlots[slotIndex];
SkillLoadoutEntry loadoutEntry = GetSkillLoadout(slotIndex);
SkillData skill = loadoutEntry != null ? loadoutEntry.BaseSkill : null;
if (skill == null)
{
Debug.Log($"Skill slot {slotIndex + 1} is empty");
@@ -191,7 +273,7 @@ namespace Colosseum.Player
}
// 마나 비용 체크 (무기 배율 적용)
float actualManaCost = GetActualManaCost(skill);
float actualManaCost = GetActualManaCost(loadoutEntry);
if (networkController != null && networkController.Mana < actualManaCost)
{
Debug.Log($"Not enough mana for skill: {skill.SkillName}");
@@ -211,7 +293,8 @@ namespace Colosseum.Player
if (slotIndex < 0 || slotIndex >= skillSlots.Length)
return;
SkillData skill = skillSlots[slotIndex];
SkillLoadoutEntry loadoutEntry = GetSkillLoadout(slotIndex);
SkillData skill = loadoutEntry != null ? loadoutEntry.BaseSkill : null;
if (skill == null) return;
// 서버에서 다시 검증
@@ -222,7 +305,7 @@ namespace Colosseum.Player
return;
// 마나 비용 체크 (무기 배율 적용)
float actualManaCost = GetActualManaCost(skill);
float actualManaCost = GetActualManaCost(loadoutEntry);
if (networkController != null && networkController.Mana < actualManaCost)
return;
@@ -245,21 +328,22 @@ namespace Colosseum.Player
if (slotIndex < 0 || slotIndex >= skillSlots.Length)
return;
SkillData skill = skillSlots[slotIndex];
SkillLoadoutEntry loadoutEntry = GetSkillLoadout(slotIndex);
SkillData skill = loadoutEntry != null ? loadoutEntry.BaseSkill : null;
if (skill == null) return;
// 모든 클라이언트에서 스킬 실행 (애니메이션 포함)
skillController.ExecuteSkill(skill);
skillController.ExecuteSkill(loadoutEntry);
}
/// <summary>
/// 무기 마나 배율이 적용된 실제 마나 비용 계산
/// </summary>
private float GetActualManaCost(SkillData skill)
private float GetActualManaCost(SkillLoadoutEntry loadoutEntry)
{
if (skill == null) return 0f;
if (loadoutEntry == null || loadoutEntry.BaseSkill == null) return 0f;
float baseCost = skill.ManaCost;
float baseCost = loadoutEntry.GetResolvedManaCost();
float multiplier = weaponEquipment != null ? weaponEquipment.ManaCostMultiplier : 1f;
return baseCost * multiplier;
@@ -271,10 +355,27 @@ namespace Colosseum.Player
public SkillData GetSkill(int slotIndex)
{
EnsureSkillSlotCapacity();
EnsureSkillLoadoutCapacity();
SyncLegacySkillsToLoadoutEntries();
if (slotIndex < 0 || slotIndex >= skillSlots.Length)
return null;
return skillSlots[slotIndex];
return skillLoadoutEntries[slotIndex] != null ? skillLoadoutEntries[slotIndex].BaseSkill : skillSlots[slotIndex];
}
/// <summary>
/// 슬롯 엔트리 접근자
/// </summary>
public SkillLoadoutEntry GetSkillLoadout(int slotIndex)
{
EnsureSkillSlotCapacity();
EnsureSkillLoadoutCapacity();
SyncLegacySkillsToLoadoutEntries();
if (slotIndex < 0 || slotIndex >= skillLoadoutEntries.Length)
return null;
return skillLoadoutEntries[slotIndex];
}
/// <summary>
@@ -288,6 +389,7 @@ namespace Colosseum.Player
return;
skillSlots[slotIndex] = skill;
skillLoadoutEntries[slotIndex].SetBaseSkill(skill);
OnSkillSlotsChanged?.Invoke();
}
@@ -305,16 +407,92 @@ namespace Colosseum.Player
for (int i = 0; i < count; i++)
{
skillSlots[i] = skills[i];
skillLoadoutEntries[i].SetBaseSkill(skills[i]);
}
for (int i = count; i < skillSlots.Length; i++)
{
skillSlots[i] = null;
skillLoadoutEntries[i].SetBaseSkill(null);
}
OnSkillSlotsChanged?.Invoke();
}
/// <summary>
/// 슬롯 엔트리를 직접 설정합니다.
/// </summary>
public void SetSkillLoadout(int slotIndex, SkillLoadoutEntry loadoutEntry)
{
EnsureSkillSlotCapacity();
EnsureSkillLoadoutCapacity();
if (slotIndex < 0 || slotIndex >= skillLoadoutEntries.Length)
return;
skillLoadoutEntries[slotIndex] = loadoutEntry != null ? loadoutEntry.CreateCopy() : new SkillLoadoutEntry();
skillLoadoutEntries[slotIndex].EnsureGemSlotCapacity();
skillSlots[slotIndex] = skillLoadoutEntries[slotIndex].BaseSkill;
OnSkillSlotsChanged?.Invoke();
}
/// <summary>
/// 특정 슬롯의 특정 젬 슬롯을 갱신합니다.
/// </summary>
public void SetSkillGem(int slotIndex, int gemSlotIndex, SkillGemData gem)
{
EnsureSkillSlotCapacity();
EnsureSkillLoadoutCapacity();
if (slotIndex < 0 || slotIndex >= skillLoadoutEntries.Length)
return;
skillLoadoutEntries[slotIndex].SetGem(gemSlotIndex, gem);
skillSlots[slotIndex] = skillLoadoutEntries[slotIndex].BaseSkill;
OnSkillSlotsChanged?.Invoke();
}
/// <summary>
/// 슬롯 엔트리 전체를 한 번에 갱신합니다.
/// </summary>
public void SetSkillLoadouts(IReadOnlyList<SkillLoadoutEntry> loadouts)
{
EnsureSkillSlotCapacity();
EnsureSkillLoadoutCapacity();
if (loadouts == null)
return;
int count = Mathf.Min(skillLoadoutEntries.Length, loadouts.Count);
for (int i = 0; i < count; i++)
{
skillLoadoutEntries[i] = loadouts[i] != null ? loadouts[i].CreateCopy() : new SkillLoadoutEntry();
skillLoadoutEntries[i].EnsureGemSlotCapacity();
skillSlots[i] = skillLoadoutEntries[i].BaseSkill;
}
for (int i = count; i < skillLoadoutEntries.Length; i++)
{
skillLoadoutEntries[i] = new SkillLoadoutEntry();
skillLoadoutEntries[i].EnsureGemSlotCapacity();
skillSlots[i] = null;
}
OnSkillSlotsChanged?.Invoke();
}
/// <summary>
/// 프리셋 기반으로 슬롯 엔트리를 일괄 적용합니다.
/// </summary>
public void ApplyLoadoutPreset(PlayerLoadoutPreset preset)
{
if (preset == null)
return;
preset.EnsureSlotCapacity();
SetSkillLoadouts(preset.Slots);
}
/// <summary>
/// 남은 쿨타임 조회
/// </summary>
@@ -331,6 +509,8 @@ namespace Colosseum.Player
/// </summary>
public bool CanUseSkill(int slotIndex)
{
EnsureRuntimeReferences();
SkillData skill = GetSkill(slotIndex);
if (skill == null) return false;
@@ -352,6 +532,44 @@ namespace Colosseum.Player
OnSkillInput(slotIndex);
}
/// <summary>
/// 서버 권한에서 특정 슬롯 스킬을 강제로 실행합니다.
/// 멀티플레이 테스트 시 원격 플레이어 스킬을 호스트에서 검증할 때 사용합니다.
/// </summary>
public bool DebugExecuteSkillAsServer(int slotIndex)
{
if (!IsServer)
return false;
EnsureRuntimeReferences();
if (slotIndex < 0 || slotIndex >= skillSlots.Length)
return false;
SkillLoadoutEntry loadoutEntry = GetSkillLoadout(slotIndex);
SkillData skill = loadoutEntry != null ? loadoutEntry.BaseSkill : null;
if (skill == null)
return false;
if (actionState != null && !actionState.CanStartSkill(skill))
return false;
if (skillController == null || skillController.IsExecutingSkill || skillController.IsOnCooldown(skill))
return false;
float actualManaCost = GetActualManaCost(loadoutEntry);
if (networkController != null && networkController.Mana < actualManaCost)
return false;
if (networkController != null && actualManaCost > 0f)
{
networkController.UseManaRpc(actualManaCost);
}
BroadcastSkillExecutionRpc(slotIndex);
return true;
}
private void OnSkill1Performed(InputAction.CallbackContext context) => OnSkillInput(0);
private void OnSkill2Performed(InputAction.CallbackContext context) => OnSkillInput(1);
@@ -374,5 +592,123 @@ namespace Colosseum.Player
return gameObject.AddComponent<PlayerActionState>();
}
/// <summary>
/// 로컬/원격 여부와 관계없이 런타임 참조를 보정합니다.
/// 서버에서 원격 플레이어 스킬을 디버그 실행할 때도 동일한 검증 경로를 쓰기 위해 필요합니다.
/// </summary>
private void EnsureRuntimeReferences()
{
if (skillController == null)
{
skillController = GetComponent<SkillController>();
}
if (networkController == null)
{
networkController = GetComponent<PlayerNetworkController>();
}
if (weaponEquipment == null)
{
weaponEquipment = GetComponent<WeaponEquipment>();
}
if (actionState == null)
{
actionState = GetOrCreateActionState();
}
}
#if UNITY_EDITOR
/// <summary>
/// MPP 환경에서는 메인 에디터에 탱커, 가상 플레이어 복제본에 지원 프리셋을 자동 적용합니다.
/// </summary>
private void ApplyEditorMultiplayerLoadoutIfNeeded()
{
if (!ShouldApplyMppmLoadout())
return;
string[] loadoutPaths = GetMppmLoadoutPathsForOwner();
List<SkillData> loadout = LoadSkillAssets(loadoutPaths);
if (loadout == null || loadout.Count == 0)
return;
SetSkills(loadout);
Debug.Log($"[MPP] 자동 프리셋 적용: {GetMppmLoadoutLabel()} (OwnerClientId={OwnerClientId})");
}
private static bool ShouldApplyMppmLoadout()
{
string systemDataPath = GetMppmSystemDataPath();
if (string.IsNullOrEmpty(systemDataPath) || !File.Exists(systemDataPath))
return false;
string json = File.ReadAllText(systemDataPath);
if (!json.Contains("\"IsMppmActive\": true", StringComparison.Ordinal))
return false;
return Regex.Matches(json, "\"Active\"\\s*:\\s*true").Count > 1;
}
private static string GetMppmSystemDataPath()
{
string projectRoot = Path.GetFullPath(Path.Combine(Application.dataPath, ".."));
if (IsVirtualProjectCloneInEditor())
return Path.GetFullPath(Path.Combine(projectRoot, "..", "SystemData.json"));
return Path.Combine(projectRoot, "Library", "VP", "SystemData.json");
}
private static bool IsVirtualProjectCloneInEditor()
{
string[] arguments = Environment.GetCommandLineArgs();
for (int i = 0; i < arguments.Length; i++)
{
if (string.Equals(arguments[i], "--virtual-project-clone", StringComparison.OrdinalIgnoreCase))
return true;
}
return false;
}
private static List<SkillData> LoadSkillAssets(IReadOnlyList<string> assetPaths)
{
List<SkillData> skills = new List<SkillData>(assetPaths.Count);
for (int i = 0; i < assetPaths.Count; i++)
{
SkillData skill = AssetDatabase.LoadAssetAtPath<SkillData>(assetPaths[i]);
if (skill == null)
{
Debug.LogWarning($"[MPP] 스킬 에셋을 찾지 못했습니다: {assetPaths[i]}");
return null;
}
skills.Add(skill);
}
return skills;
}
private string[] GetMppmLoadoutPathsForOwner()
{
return OwnerClientId switch
{
0 => TankLoadoutPaths,
1 => SupportLoadoutPaths,
_ => DpsLoadoutPaths,
};
}
private string GetMppmLoadoutLabel()
{
return OwnerClientId switch
{
0 => "탱커",
1 => "지원",
_ => "딜러",
};
}
#endif
}
}

View File

@@ -0,0 +1,69 @@
using System.Collections.Generic;
using UnityEngine;
namespace Colosseum.Skills
{
/// <summary>
/// 플레이어가 슬롯별로 사용할 스킬/젬 조합 프리셋입니다.
/// </summary>
[CreateAssetMenu(fileName = "NewPlayerLoadoutPreset", menuName = "Colosseum/Player Loadout Preset")]
public class PlayerLoadoutPreset : ScriptableObject
{
private const int DefaultSlotCount = 7;
[Header("기본 정보")]
[SerializeField] private string presetName;
[TextArea(2, 4)]
[SerializeField] private string description;
[Header("슬롯 구성")]
[SerializeField] private SkillLoadoutEntry[] slots = new SkillLoadoutEntry[DefaultSlotCount];
public string PresetName => presetName;
public string Description => description;
public IReadOnlyList<SkillLoadoutEntry> Slots => slots;
private void OnValidate()
{
EnsureSlotCapacity();
}
public void EnsureSlotCapacity(int slotCount = DefaultSlotCount)
{
slotCount = Mathf.Max(0, slotCount);
if (slots != null && slots.Length == slotCount)
{
EnsureGemSlots();
return;
}
SkillLoadoutEntry[] resized = new SkillLoadoutEntry[slotCount];
if (slots != null)
{
int copyCount = Mathf.Min(slots.Length, resized.Length);
for (int i = 0; i < copyCount; i++)
{
resized[i] = slots[i];
}
}
slots = resized;
EnsureGemSlots();
}
private void EnsureGemSlots()
{
if (slots == null)
return;
for (int i = 0; i < slots.Length; i++)
{
if (slots[i] == null)
slots[i] = new SkillLoadoutEntry();
slots[i].EnsureGemSlotCapacity();
}
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 26d5895a89de4f24aade1ea4b5f7644e

View File

@@ -53,6 +53,9 @@ namespace Colosseum.Skills
// 현재 실행 중인 스킬
private SkillData currentSkill;
private SkillLoadoutEntry currentLoadoutEntry;
private readonly List<SkillEffect> currentCastStartEffects = new();
private readonly Dictionary<int, List<SkillEffect>> currentTriggeredEffects = new();
private bool skillEndRequested; // OnSkillEnd 이벤트 호출 여부
private bool waitingForEndAnimation; // EndAnimation 종료 대기 중
@@ -66,6 +69,7 @@ namespace Colosseum.Skills
public bool UsesRootMotion => currentSkill != null && currentSkill.UseRootMotion;
public bool IgnoreRootMotionY => currentSkill != null && currentSkill.IgnoreRootMotionY;
public SkillData CurrentSkill => currentSkill;
public SkillLoadoutEntry CurrentLoadoutEntry => currentLoadoutEntry;
public Animator Animator => animator;
public SkillCancelReason LastCancelReason => lastCancelReason;
public string LastCancelledSkillName => lastCancelledSkillName;
@@ -131,6 +135,15 @@ namespace Colosseum.Skills
/// </summary>
public bool ExecuteSkill(SkillData skill)
{
return ExecuteSkill(SkillLoadoutEntry.CreateTemporary(skill));
}
/// <summary>
/// 슬롯 엔트리 기준으로 스킬 시전
/// </summary>
public bool ExecuteSkill(SkillLoadoutEntry loadoutEntry)
{
SkillData skill = loadoutEntry != null ? loadoutEntry.BaseSkill : null;
if (skill == null)
{
Debug.LogWarning("Skill is null!");
@@ -157,17 +170,19 @@ namespace Colosseum.Skills
return false;
}
currentLoadoutEntry = loadoutEntry != null ? loadoutEntry.CreateCopy() : SkillLoadoutEntry.CreateTemporary(skill);
currentSkill = skill;
skillEndRequested = false;
waitingForEndAnimation = false;
lastCancelReason = SkillCancelReason.None;
BuildResolvedEffects(currentLoadoutEntry);
if (debugMode) Debug.Log($"[Skill] Cast: {skill.SkillName}");
// 쿨타임 시작
StartCooldown(skill);
StartCooldown(skill, currentLoadoutEntry.GetResolvedCooldown());
TriggerCastStartEffects(skill);
TriggerCastStartEffects();
// 스킬 애니메이션 재생
if (skill.SkillClip != null && animator != null)
@@ -176,7 +191,7 @@ namespace Colosseum.Skills
PlaySkillClip(skill.SkillClip);
}
TriggerImmediateSelfEffectsIfNeeded(skill);
TriggerImmediateSelfEffectsIfNeeded();
return true;
}
@@ -185,17 +200,17 @@ namespace Colosseum.Skills
/// 시전 시작 즉시 발동하는 효과를 실행합니다.
/// 서버 권한으로만 처리해 실제 게임플레이 효과가 한 번만 적용되게 합니다.
/// </summary>
private void TriggerCastStartEffects(SkillData skill)
private void TriggerCastStartEffects()
{
if (skill == null || skill.CastStartEffects == null || skill.CastStartEffects.Count == 0)
if (currentSkill == null || currentCastStartEffects.Count == 0)
return;
if (NetworkManager.Singleton != null && !NetworkManager.Singleton.IsServer)
return;
for (int i = 0; i < skill.CastStartEffects.Count; i++)
for (int i = 0; i < currentCastStartEffects.Count; i++)
{
SkillEffect effect = skill.CastStartEffects[i];
SkillEffect effect = currentCastStartEffects[i];
if (effect == null)
continue;
@@ -208,20 +223,23 @@ namespace Colosseum.Skills
/// 애니메이션 이벤트가 없는 자기 자신 대상 효과는 시전 즉시 발동합니다.
/// 버프/무적 같은 자기 강화 스킬이 이벤트 누락으로 동작하지 않는 상황을 막기 위한 보정입니다.
/// </summary>
private void TriggerImmediateSelfEffectsIfNeeded(SkillData skill)
private void TriggerImmediateSelfEffectsIfNeeded()
{
if (skill == null || skill.Effects == null || skill.Effects.Count == 0)
if (currentSkill == null || currentTriggeredEffects.Count == 0)
return;
if (NetworkManager.Singleton != null && !NetworkManager.Singleton.IsServer)
return;
if (skill.SkillClip != null && skill.SkillClip.events != null && skill.SkillClip.events.Length > 0)
if (currentSkill.SkillClip != null && currentSkill.SkillClip.events != null && currentSkill.SkillClip.events.Length > 0)
return;
for (int i = 0; i < skill.Effects.Count; i++)
if (!currentTriggeredEffects.TryGetValue(0, out List<SkillEffect> effectsAtZero))
return;
for (int i = 0; i < effectsAtZero.Count; i++)
{
SkillEffect effect = skill.Effects[i];
SkillEffect effect = effectsAtZero[i];
if (effect == null || effect.TargetType != TargetType.Self)
continue;
@@ -230,6 +248,21 @@ namespace Colosseum.Skills
}
}
/// <summary>
/// 현재 슬롯 엔트리 기준으로 시전 시작/트리거 효과를 합성합니다.
/// </summary>
private void BuildResolvedEffects(SkillLoadoutEntry loadoutEntry)
{
currentCastStartEffects.Clear();
currentTriggeredEffects.Clear();
if (loadoutEntry == null)
return;
loadoutEntry.CollectCastStartEffects(currentCastStartEffects);
loadoutEntry.CollectTriggeredEffects(currentTriggeredEffects);
}
/// <summary>
/// 스킬 클립으로 Override Controller 생성 후 재생
/// </summary>
@@ -354,23 +387,28 @@ namespace Colosseum.Skills
return;
}
var effects = currentSkill.Effects;
if (index < 0 || index >= effects.Count)
if (!currentTriggeredEffects.TryGetValue(index, out List<SkillEffect> effects) || effects == null || effects.Count == 0)
{
if (debugMode) Debug.LogWarning($"[Effect] Invalid index: {index}");
return;
}
var effect = effects[index];
if (debugMode) Debug.Log($"[Effect] {effect.name} (index {index})");
// 공격 범위 시각화
if (showAreaDebug)
for (int i = 0; i < effects.Count; i++)
{
effect.DrawDebugRange(gameObject, debugDrawDuration);
}
SkillEffect effect = effects[i];
if (effect == null)
continue;
effect.ExecuteOnCast(gameObject);
if (debugMode) Debug.Log($"[Effect] {effect.name} (index {index})");
// 공격 범위 시각화
if (showAreaDebug)
{
effect.DrawDebugRange(gameObject, debugDrawDuration);
}
effect.ExecuteOnCast(gameObject);
}
}
/// <summary>
@@ -408,6 +446,9 @@ namespace Colosseum.Skills
RestoreBaseController();
currentSkill = null;
currentLoadoutEntry = null;
currentCastStartEffects.Clear();
currentTriggeredEffects.Clear();
skillEndRequested = false;
waitingForEndAnimation = false;
return true;
@@ -430,9 +471,9 @@ namespace Colosseum.Skills
return Mathf.Max(0f, remaining);
}
private void StartCooldown(SkillData skill)
private void StartCooldown(SkillData skill, float cooldownDuration)
{
cooldownTracker[skill] = Time.time + skill.Cooldown;
cooldownTracker[skill] = Time.time + cooldownDuration;
}
public void ResetCooldown(SkillData skill)

View File

@@ -1,6 +1,8 @@
using UnityEngine;
using System;
using System.Collections.Generic;
using UnityEngine;
namespace Colosseum.Skills
{
/// <summary>
@@ -43,6 +45,10 @@ namespace Colosseum.Skills
[Min(0f)] [SerializeField] private float cooldown = 1f;
[Min(0f)] [SerializeField] private float manaCost = 0f;
[Header("젬 슬롯")]
[Tooltip("이 스킬에 장착 가능한 젬 슬롯 수")]
[Min(0)] [SerializeField] private int maxGemSlotCount = 2;
[Header("효과 목록")]
[Tooltip("시전 시작 즉시 발동하는 효과 목록. 시전 보호 버프 등에 사용됩니다.")]
[SerializeField] private List<SkillEffect> castStartEffects = new List<SkillEffect>();
@@ -60,6 +66,7 @@ namespace Colosseum.Skills
public float AnimationSpeed => animationSpeed;
public float Cooldown => cooldown;
public float ManaCost => manaCost;
public int MaxGemSlotCount => maxGemSlotCount;
public bool UseRootMotion => useRootMotion;
public bool IgnoreRootMotionY => ignoreRootMotionY;
public bool JumpToTarget => jumpToTarget;

View File

@@ -0,0 +1,55 @@
using System;
using System.Collections.Generic;
using UnityEngine;
namespace Colosseum.Skills
{
/// <summary>
/// 젬 효과가 발동될 애니메이션 이벤트 인덱스와 효과 목록입니다.
/// </summary>
[Serializable]
public class SkillGemTriggeredEffectEntry
{
[Tooltip("OnEffect(index)와 매칭되는 애니메이션 이벤트 인덱스")]
[Min(0)] [SerializeField] private int triggerIndex = 0;
[Tooltip("해당 인덱스에서 함께 실행할 추가 효과")]
[SerializeField] private List<SkillEffect> effects = new();
public int TriggerIndex => triggerIndex;
public IReadOnlyList<SkillEffect> Effects => effects;
}
/// <summary>
/// 스킬의 기반 효과 위에 추가 동작을 덧붙이는 젬 데이터입니다.
/// </summary>
[CreateAssetMenu(fileName = "NewSkillGem", menuName = "Colosseum/Skill Gem")]
public class SkillGemData : ScriptableObject
{
[Header("기본 정보")]
[SerializeField] private string gemName;
[TextArea(2, 4)]
[SerializeField] private string description;
[SerializeField] private Sprite icon;
[Header("기본 수치 보정")]
[Tooltip("장착 시 마나 비용 배율")]
[Min(0f)] [SerializeField] private float manaCostMultiplier = 1f;
[Tooltip("장착 시 쿨타임 배율")]
[Min(0f)] [SerializeField] private float cooldownMultiplier = 1f;
[Header("추가 효과")]
[Tooltip("시전 시작 시 즉시 발동하는 추가 효과")]
[SerializeField] private List<SkillEffect> castStartEffects = new();
[Tooltip("애니메이션 이벤트 인덱스별로 발동하는 추가 효과")]
[SerializeField] private List<SkillGemTriggeredEffectEntry> triggeredEffects = new();
public string GemName => gemName;
public string Description => description;
public Sprite Icon => icon;
public float ManaCostMultiplier => manaCostMultiplier;
public float CooldownMultiplier => cooldownMultiplier;
public IReadOnlyList<SkillEffect> CastStartEffects => castStartEffects;
public IReadOnlyList<SkillGemTriggeredEffectEntry> TriggeredEffects => triggeredEffects;
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: e81a62ae7c7624847ab572ff37789bb8

View File

@@ -0,0 +1,226 @@
using System.Collections.Generic;
using UnityEngine;
namespace Colosseum.Skills
{
/// <summary>
/// 단일 슬롯에서 사용할 스킬과 장착된 젬 조합입니다.
/// </summary>
[System.Serializable]
public class SkillLoadoutEntry
{
private const int DefaultGemSlotCount = 2;
[Tooltip("이 슬롯의 기반 스킬")]
[SerializeField] private SkillData baseSkill;
[Tooltip("기반 스킬에 장착된 젬")]
[SerializeField] private SkillGemData[] socketedGems = new SkillGemData[DefaultGemSlotCount];
public SkillData BaseSkill => baseSkill;
public IReadOnlyList<SkillGemData> SocketedGems => socketedGems;
public static SkillLoadoutEntry CreateTemporary(SkillData skill)
{
SkillLoadoutEntry entry = new SkillLoadoutEntry();
entry.SetBaseSkill(skill);
entry.EnsureGemSlotCapacity();
return entry;
}
public SkillLoadoutEntry CreateCopy()
{
SkillLoadoutEntry copy = new SkillLoadoutEntry();
copy.baseSkill = baseSkill;
copy.socketedGems = new SkillGemData[socketedGems != null ? socketedGems.Length : DefaultGemSlotCount];
if (socketedGems != null)
{
for (int i = 0; i < socketedGems.Length; i++)
{
copy.socketedGems[i] = socketedGems[i];
}
}
return copy;
}
public void EnsureGemSlotCapacity(int slotCount = -1)
{
if (slotCount < 0)
{
slotCount = baseSkill != null ? baseSkill.MaxGemSlotCount : DefaultGemSlotCount;
}
slotCount = Mathf.Max(0, slotCount);
if (socketedGems != null && socketedGems.Length == slotCount)
return;
SkillGemData[] resized = new SkillGemData[slotCount];
if (socketedGems != null)
{
int copyCount = Mathf.Min(socketedGems.Length, resized.Length);
for (int i = 0; i < copyCount; i++)
{
resized[i] = socketedGems[i];
}
}
socketedGems = resized;
}
public void SetBaseSkill(SkillData skill)
{
baseSkill = skill;
EnsureGemSlotCapacity();
}
public void SetGem(int slotIndex, SkillGemData gem)
{
EnsureGemSlotCapacity();
if (slotIndex < 0 || slotIndex >= socketedGems.Length)
return;
socketedGems[slotIndex] = gem;
}
public SkillGemData GetGem(int slotIndex)
{
EnsureGemSlotCapacity();
if (slotIndex < 0 || slotIndex >= socketedGems.Length)
return null;
return socketedGems[slotIndex];
}
public float GetResolvedManaCost()
{
if (baseSkill == null)
return 0f;
float resolved = baseSkill.ManaCost;
if (socketedGems == null)
return resolved;
for (int i = 0; i < socketedGems.Length; i++)
{
SkillGemData gem = socketedGems[i];
if (gem == null)
continue;
resolved *= gem.ManaCostMultiplier;
}
return resolved;
}
public float GetResolvedCooldown()
{
if (baseSkill == null)
return 0f;
float resolved = baseSkill.Cooldown;
if (socketedGems == null)
return resolved;
for (int i = 0; i < socketedGems.Length; i++)
{
SkillGemData gem = socketedGems[i];
if (gem == null)
continue;
resolved *= gem.CooldownMultiplier;
}
return resolved;
}
public void CollectCastStartEffects(List<SkillEffect> destination)
{
if (destination == null)
return;
if (baseSkill != null && baseSkill.CastStartEffects != null)
{
for (int i = 0; i < baseSkill.CastStartEffects.Count; i++)
{
SkillEffect effect = baseSkill.CastStartEffects[i];
if (effect != null)
destination.Add(effect);
}
}
if (socketedGems == null)
return;
for (int i = 0; i < socketedGems.Length; i++)
{
SkillGemData gem = socketedGems[i];
if (gem == null || gem.CastStartEffects == null)
continue;
for (int j = 0; j < gem.CastStartEffects.Count; j++)
{
SkillEffect effect = gem.CastStartEffects[j];
if (effect != null)
destination.Add(effect);
}
}
}
public void CollectTriggeredEffects(Dictionary<int, List<SkillEffect>> destination)
{
if (destination == null)
return;
if (baseSkill != null && baseSkill.Effects != null)
{
for (int i = 0; i < baseSkill.Effects.Count; i++)
{
SkillEffect effect = baseSkill.Effects[i];
if (effect == null)
continue;
AddTriggeredEffect(destination, i, effect);
}
}
if (socketedGems == null)
return;
for (int i = 0; i < socketedGems.Length; i++)
{
SkillGemData gem = socketedGems[i];
if (gem == null || gem.TriggeredEffects == null)
continue;
for (int j = 0; j < gem.TriggeredEffects.Count; j++)
{
SkillGemTriggeredEffectEntry entry = gem.TriggeredEffects[j];
if (entry == null || entry.Effects == null)
continue;
for (int k = 0; k < entry.Effects.Count; k++)
{
SkillEffect effect = entry.Effects[k];
if (effect == null)
continue;
AddTriggeredEffect(destination, entry.TriggerIndex, effect);
}
}
}
}
private static void AddTriggeredEffect(Dictionary<int, List<SkillEffect>> destination, int triggerIndex, SkillEffect effect)
{
if (!destination.TryGetValue(triggerIndex, out List<SkillEffect> effectList))
{
effectList = new List<SkillEffect>();
destination.Add(triggerIndex, effectList);
}
effectList.Add(effect);
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: d06556806f8c370429a54ca7af7e2a34

View File

@@ -1,3 +1,5 @@
using System;
using UnityEngine;
using Unity.Netcode;
using Unity.Netcode.Transports.UTP;
@@ -34,7 +36,10 @@ namespace Colosseum.UI
#if UNITY_EDITOR
if (autoStartHostInEditor && NetworkManager.Singleton != null && !NetworkManager.Singleton.IsListening)
{
StartHost();
if (IsVirtualProjectClone())
StartClient();
else
StartHost();
}
#endif
}
@@ -107,5 +112,20 @@ namespace Colosseum.UI
UpdateTransportSettings();
}
}
/// <summary>
/// MPP 가상 플레이어 복제본 에디터인지 확인합니다.
/// </summary>
private static bool IsVirtualProjectClone()
{
string[] arguments = Environment.GetCommandLineArgs();
for (int i = 0; i < arguments.Length; i++)
{
if (string.Equals(arguments[i], "--virtual-project-clone", StringComparison.OrdinalIgnoreCase))
return true;
}
return false;
}
}
}