using System; using System.Collections.Generic; using System.IO; using UnityEditor; using UnityEngine; using Colosseum.Abnormalities; using Colosseum.AI; using Colosseum.Skills; using Colosseum.Skills.Effects; namespace Colosseum.Editor { /// /// 드로그 기획/패턴 문서를 기준으로 스킬/이펙트/패턴 플레이스홀더 자산을 재구성합니다. /// 애니메이션이 아직 확정되지 않은 단계에서도 BT와 데이터 연결을 먼저 맞추는 용도입니다. /// public static class RebuildDrogCombatAssets { private const string AnimationsFolder = "Assets/_Game/Animations"; private const string SkillsFolder = "Assets/_Game/Data/Skills"; private const string PatternsFolder = "Assets/_Game/Data/Patterns"; private const string EffectsFolder = "Assets/_Game/Data/Skills/Effects"; private const string ExecutionTelegraphAbnormalityPath = "Assets/_Game/Data/Abnormalities/Data_Abnormality_Drog_집행준비.asset"; private const string LightCombo01BSourcePath = "Assets/External/Animations/AnimationSwordCombat/Animations/Sidekick/Attack/LightCombo01/A_MOD_SWD_Attack_LightCombo01B_RM_Neut.fbx"; private const string HeavyCombo01BSourcePath = "Assets/External/Animations/AnimationSwordCombat/Animations/Sidekick/Attack/HeavyCombo01/A_MOD_SWD_Attack_HeavyCombo01B_RM_Neut.fbx"; private const string HeavyCombo01CSourcePath = "Assets/External/Animations/AnimationSwordCombat/Animations/Sidekick/Attack/HeavyCombo01/A_MOD_SWD_Attack_HeavyCombo01C_RM_Neut.fbx"; private const string ZweihanderAttack013SourcePath = "Assets/External/Animations/Knight_Zweihander_Animset/Animation/Attack/Root/Zweihander_Attack01_3_Root.FBX"; private const string PunchLSourcePath = "Assets/External/Animations/Mixamo/펀치L.fbx"; private const string PunchRSourcePath = "Assets/External/Animations/Mixamo/펀치R.fbx"; private const string KickRSourcePath = "Assets/External/Animations/Mixamo/킥R.fbx"; [MenuItem("Tools/Colosseum/Rebuild Drog Combat Assets")] private static void Rebuild() { try { EnsureFolder("Assets/_Game"); EnsureFolder("Assets/_Game/Animations"); EnsureFolder("Assets/_Game/Data"); EnsureFolder("Assets/_Game/Data/Skills"); EnsureFolder("Assets/_Game/Data/Patterns"); EnsureFolder("Assets/_Game/Data/Skills/Effects"); DeleteLegacyComboAssets(); AbnormalityData executionTelegraph = AssetDatabase.LoadAssetAtPath(ExecutionTelegraphAbnormalityPath); if (executionTelegraph == null) { Debug.LogError($"[DrogCombatAssets] 집행 전조 이상상태를 찾을 수 없습니다: {ExecutionTelegraphAbnormalityPath}"); return; } AnimationClip comboBasic1Hit1Clip0 = EnsureClipFromSource($"{AnimationsFolder}/Anim_Drog_콤보-기본기1_1_0.anim", $"{AnimationsFolder}/Anim_Drog_평타1R_0.anim"); AnimationClip comboBasic1Hit1Clip1 = EnsureClipFromSource($"{AnimationsFolder}/Anim_Drog_콤보-기본기1_1_1.anim", $"{AnimationsFolder}/Anim_Drog_평타1R_1.anim"); AnimationClip comboBasic1Hit2Clip = EnsureClipFromSource($"{AnimationsFolder}/Anim_Drog_콤보-기본기1_2_0.anim", LightCombo01BSourcePath, "A_MOD_SWD_Attack_LightCombo01B_RM_Neut"); AnimationClip comboBasic2Hit1Clip0 = EnsureClipFromSource($"{AnimationsFolder}/Anim_Drog_콤보-기본기2_1_0.anim", $"{AnimationsFolder}/Anim_Drog_평타2R_0.anim"); AnimationClip comboBasic2Hit1Clip1 = EnsureClipFromSource($"{AnimationsFolder}/Anim_Drog_콤보-기본기2_1_1.anim", $"{AnimationsFolder}/Anim_Drog_평타2R_1.anim"); AnimationClip comboBasic2Hit1Clip2 = EnsureClipFromSource($"{AnimationsFolder}/Anim_Drog_콤보-기본기2_1_2.anim", $"{AnimationsFolder}/Anim_Drog_평타2R_2.anim"); AnimationClip comboBasic2Hit2Clip = EnsureClipFromSource($"{AnimationsFolder}/Anim_Drog_콤보-기본기2_2_0.anim", HeavyCombo01CSourcePath, "A_MOD_SWD_Attack_HeavyCombo01C_RM_Neut"); AnimationClip comboBasic3Hit1Clip = EnsureClipFromSource($"{AnimationsFolder}/Anim_Drog_콤보-기본기3_1_0.anim", PunchLSourcePath, "mixamo.com"); AnimationClip comboBasic3Hit2Clip = EnsureClipFromSource($"{AnimationsFolder}/Anim_Drog_콤보-기본기3_2_0.anim", PunchRSourcePath, "mixamo.com"); AnimationClip comboBasic3Hit3Clip = EnsureClipFromSource($"{AnimationsFolder}/Anim_Drog_콤보-기본기3_3_0.anim", KickRSourcePath, "mixamo.com"); AnimationClip comboSlamHit1Clip = EnsureClipFromSource($"{AnimationsFolder}/Anim_Drog_콤보-강타_1_0.anim", HeavyCombo01BSourcePath, "A_MOD_SWD_Attack_HeavyCombo01B_RM_Neut"); AnimationClip comboSlamHit2Clip = EnsureClipFromSource($"{AnimationsFolder}/Anim_Drog_콤보-강타_2_0.anim", LightCombo01BSourcePath, "A_MOD_SWD_Attack_LightCombo01B_RM_Neut"); AnimationClip slamClip = EnsureClipFromSource($"{AnimationsFolder}/Anim_Drog_강타_0.anim", $"{AnimationsFolder}/Anim_Drog_강타R_0.anim"); AnimationClip comboStompHit1Clip = EnsureClipFromSource($"{AnimationsFolder}/Anim_Drog_콤보-발구르기_1_0.anim", ZweihanderAttack013SourcePath, "Zweihander_Attack01_3_Root"); AnimationClip comboStompHit2Clip = EnsureClipFromSource($"{AnimationsFolder}/Anim_Drog_콤보-발구르기_2_0.anim", HeavyCombo01CSourcePath, "A_MOD_SWD_Attack_HeavyCombo01C_RM_Neut"); AnimationClip stompClip = EnsureClipFromSource($"{AnimationsFolder}/Anim_Drog_발구르기_0.anim", $"{AnimationsFolder}/Anim_Drog_발구르기_0.anim"); AnimationClip leapPrepareClip = EnsurePlaceholderClip($"{AnimationsFolder}/Anim_Drog_도약_준비_0.anim"); AnimationClip leapAirClip = EnsurePlaceholderClip($"{AnimationsFolder}/Anim_Drog_도약_공중_0.anim"); AnimationClip leapLandingClip = EnsurePlaceholderClip($"{AnimationsFolder}/Anim_Drog_도약_착지_0.anim"); AnimationClip stepClip = EnsurePlaceholderClip($"{AnimationsFolder}/Anim_Drog_밟기_0.anim"); AnimationClip throwClip = EnsurePlaceholderClip($"{AnimationsFolder}/Anim_Drog_투척_0.anim"); AnimationClip roarClip = EnsurePlaceholderClip($"{AnimationsFolder}/Anim_Drog_포효_0.anim"); AnimationClip executionReadyClip = EnsurePlaceholderClip($"{AnimationsFolder}/Anim_Drog_집행_준비_0.anim"); AnimationClip executionHit1Clip = EnsurePlaceholderClip($"{AnimationsFolder}/Anim_Drog_집행_연타1_0.anim"); AnimationClip executionHit2Clip = EnsurePlaceholderClip($"{AnimationsFolder}/Anim_Drog_집행_연타2_0.anim"); AnimationClip executionHit3Clip = EnsurePlaceholderClip($"{AnimationsFolder}/Anim_Drog_집행_연타3_0.anim"); DamageEffect comboBasic1Hit1Damage = CreateDamageEffect( $"{EffectsFolder}/Data_SkillEffect_Drog_콤보-기본기1_1_0_데미지.asset", 22f, DamageType.Physical, 0.65f, AreaShapeType.Fan, 3.25f, 1.25f, 3.25f, 42f, AreaCenterType.Caster); DamageEffect comboBasic1Hit2Damage = CreateDamageEffect( $"{EffectsFolder}/Data_SkillEffect_Drog_콤보-기본기1_2_0_데미지.asset", 16f, DamageType.Physical, 0.5f, AreaShapeType.Fan, 3.3f, 1.25f, 3.3f, 40f, AreaCenterType.Caster); DamageEffect comboBasic2Hit1Damage = CreateDamageEffect( $"{EffectsFolder}/Data_SkillEffect_Drog_콤보-기본기2_1_0_데미지.asset", 26f, DamageType.Physical, 0.8f, AreaShapeType.Fan, 3.5f, 1.35f, 3.5f, 46f, AreaCenterType.Caster); DamageEffect comboBasic2Hit2Damage = CreateDamageEffect( $"{EffectsFolder}/Data_SkillEffect_Drog_콤보-기본기2_2_0_데미지.asset", 20f, DamageType.Physical, 0.6f, AreaShapeType.Fan, 3.6f, 1.35f, 3.6f, 42f, AreaCenterType.Caster); DamageEffect slamDamage = CreateDamageEffect( $"{EffectsFolder}/Data_SkillEffect_Drog_강타_0_데미지.asset", 48f, DamageType.Physical, 1.15f, AreaShapeType.Fan, 3.4f, 1.2f, 3.4f, 32f, AreaCenterType.Caster); DownEffect slamDown = CreateDownEffect( $"{EffectsFolder}/Data_SkillEffect_Drog_강타_1_다운.asset", 1.8f, AreaShapeType.Fan, 3.4f, 1.2f, 3.4f, 32f, AreaCenterType.Caster); DamageEffect comboBasic3Hit1Damage = CreateDamageEffect( $"{EffectsFolder}/Data_SkillEffect_Drog_콤보-기본기3_1_0_데미지.asset", 12f, DamageType.Physical, 0.35f, AreaShapeType.Fan, 2.6f, 1.1f, 2.6f, 55f, AreaCenterType.Caster); DamageEffect comboBasic3Hit2Damage = CreateDamageEffect( $"{EffectsFolder}/Data_SkillEffect_Drog_콤보-기본기3_2_0_데미지.asset", 12f, DamageType.Physical, 0.35f, AreaShapeType.Fan, 2.6f, 1.1f, 2.6f, 55f, AreaCenterType.Caster); DamageEffect comboBasic3Hit3Damage = CreateDamageEffect( $"{EffectsFolder}/Data_SkillEffect_Drog_콤보-기본기3_3_0_데미지.asset", 18f, DamageType.Physical, 0.55f, AreaShapeType.Fan, 3.1f, 1.15f, 3.1f, 68f, AreaCenterType.Caster); DamageEffect comboSlamHit1Damage = CreateDamageEffect( $"{EffectsFolder}/Data_SkillEffect_Drog_콤보-강타_1_0_데미지.asset", 20f, DamageType.Physical, 0.6f, AreaShapeType.Fan, 3.6f, 1.3f, 3.6f, 52f, AreaCenterType.Caster); DamageEffect comboSlamHit2Damage = CreateDamageEffect( $"{EffectsFolder}/Data_SkillEffect_Drog_콤보-강타_2_0_데미지.asset", 16f, DamageType.Physical, 0.45f, AreaShapeType.Fan, 3.2f, 1.15f, 3.2f, 42f, AreaCenterType.Caster); DamageEffect comboStompHit1Damage = CreateDamageEffect( $"{EffectsFolder}/Data_SkillEffect_Drog_콤보-발구르기_1_0_데미지.asset", 22f, DamageType.Physical, 0.65f, AreaShapeType.Fan, 3.9f, 1.35f, 3.9f, 60f, AreaCenterType.Caster); DamageEffect comboStompHit2Damage = CreateDamageEffect( $"{EffectsFolder}/Data_SkillEffect_Drog_콤보-발구르기_2_0_데미지.asset", 18f, DamageType.Physical, 0.5f, AreaShapeType.Fan, 3.6f, 1.25f, 3.6f, 44f, AreaCenterType.Caster); DamageEffect stompDamage = CreateDamageEffect( $"{EffectsFolder}/Data_SkillEffect_Drog_발구르기_0_데미지.asset", 22f, DamageType.Physical, 0.65f, AreaShapeType.Sphere, 4.75f, 1f, 4.75f, 180f, AreaCenterType.Caster); KnockbackEffect stompKnockback = CreateKnockbackEffect( $"{EffectsFolder}/Data_SkillEffect_Drog_발구르기_1_넉백.asset", 6f, 1.5f, 0.2f, AreaShapeType.Sphere, 4.75f, 1f, 4.75f, 180f, AreaCenterType.Caster); DamageEffect leapLandingDamage = CreateDamageEffect( $"{EffectsFolder}/Data_SkillEffect_Drog_도약_착지_0_데미지.asset", 34f, DamageType.Physical, 0.95f, AreaShapeType.Sphere, 4.2f, 1f, 4.2f, 180f, AreaCenterType.Caster); KnockbackEffect leapLandingKnockback = CreateKnockbackEffect( $"{EffectsFolder}/Data_SkillEffect_Drog_도약_착지_1_넉백.asset", 8f, 2f, 0.25f, AreaShapeType.Sphere, 4.2f, 1f, 4.2f, 180f, AreaCenterType.Caster); HitReactionDamageEffect stepDamage = CreateHitReactionDamageEffect( $"{EffectsFolder}/Data_SkillEffect_Drog_밟기_0_피격가중데미지.asset", 52f, DamageType.Physical, 1.1f, 1.6f, AreaShapeType.Sphere, 2.8f, 1f, 2.8f, 180f, AreaCenterType.Caster); KnockbackEffect stepKnockback = CreateKnockbackEffect( $"{EffectsFolder}/Data_SkillEffect_Drog_밟기_1_넉백.asset", 5f, 1f, 0.18f, AreaShapeType.Sphere, 2.8f, 1f, 2.8f, 180f, AreaCenterType.Caster); DamageEffect throwDamage = CreateDamageEffect( $"{EffectsFolder}/Data_SkillEffect_Drog_투척_0_데미지.asset", 28f, DamageType.Physical, 0.7f, AreaShapeType.Beam, 12f, 1.2f, 0.75f, 0f, AreaCenterType.Caster); DamageEffect executionHit1 = CreateDamageEffect( $"{EffectsFolder}/Data_SkillEffect_Drog_집행_연타1_0_데미지.asset", 14f, DamageType.Physical, 0.35f, AreaShapeType.Sphere, 8.5f, 1f, 8.5f, 180f, AreaCenterType.Caster); DamageEffect executionHit2 = CreateDamageEffect( $"{EffectsFolder}/Data_SkillEffect_Drog_집행_연타2_0_데미지.asset", 17f, DamageType.Physical, 0.4f, AreaShapeType.Sphere, 8.5f, 1f, 8.5f, 180f, AreaCenterType.Caster); DamageEffect executionHit3 = CreateDamageEffect( $"{EffectsFolder}/Data_SkillEffect_Drog_집행_연타3_0_데미지.asset", 20f, DamageType.Physical, 0.45f, AreaShapeType.Sphere, 8.5f, 1f, 8.5f, 180f, AreaCenterType.Caster); SkillData comboBasic1Hit1Skill = CreateSkill( $"{SkillsFolder}/Data_Skill_Drog_콤보-기본기1_1.asset", "콤보-기본기1 1타", "기본기 콤보1의 첫 타격입니다.", new[] { comboBasic1Hit1Clip0, comboBasic1Hit1Clip1 }, 1f, SkillCastTargetTrackingMode.FaceTarget, true, true, false, comboBasic1Hit1Damage); SkillData comboBasic1Hit2Skill = CreateSkill( $"{SkillsFolder}/Data_Skill_Drog_콤보-기본기1_2.asset", "콤보-기본기1 2타", "기본기 콤보1의 후속 타격입니다.", new[] { comboBasic1Hit2Clip }, 1f, SkillCastTargetTrackingMode.FaceTarget, true, true, false, comboBasic1Hit2Damage); SkillData comboBasic2Hit1Skill = CreateSkill( $"{SkillsFolder}/Data_Skill_Drog_콤보-기본기2_1.asset", "콤보-기본기2 1타", "기본기 콤보2의 시작 타격입니다.", new[] { comboBasic2Hit1Clip0, comboBasic2Hit1Clip1, comboBasic2Hit1Clip2 }, 1f, SkillCastTargetTrackingMode.FaceTarget, true, true, false, comboBasic2Hit1Damage); SkillData comboBasic2Hit2Skill = CreateSkill( $"{SkillsFolder}/Data_Skill_Drog_콤보-기본기2_2.asset", "콤보-기본기2 2타", "기본기 콤보2의 마무리 타격입니다.", new[] { comboBasic2Hit2Clip }, 1f, SkillCastTargetTrackingMode.FaceTarget, true, true, false, comboBasic2Hit2Damage); SkillData slamSkill = CreateSkill( $"{SkillsFolder}/Data_Skill_Drog_강타.asset", "강타", "정면 관리 실패를 응징하는 강한 일격입니다.", new[] { slamClip }, 1f, SkillCastTargetTrackingMode.FaceTarget, true, true, false, slamDamage, slamDown); SkillData comboBasic3Hit1Skill = CreateSkill( $"{SkillsFolder}/Data_Skill_Drog_콤보-기본기3_1.asset", "콤보-기본기3 1타", "기본기 콤보3의 첫 번째 타격입니다.", new[] { comboBasic3Hit1Clip }, 1f, SkillCastTargetTrackingMode.FaceTarget, true, true, false, comboBasic3Hit1Damage); SkillData comboBasic3Hit2Skill = CreateSkill( $"{SkillsFolder}/Data_Skill_Drog_콤보-기본기3_2.asset", "콤보-기본기3 2타", "기본기 콤보3의 두 번째 타격입니다.", new[] { comboBasic3Hit2Clip }, 1f, SkillCastTargetTrackingMode.FaceTarget, true, true, false, comboBasic3Hit2Damage); SkillData comboBasic3Hit3Skill = CreateSkill( $"{SkillsFolder}/Data_Skill_Drog_콤보-기본기3_3.asset", "콤보-기본기3 3타", "기본기 콤보3의 발차기 마무리입니다.", new[] { comboBasic3Hit3Clip }, 1f, SkillCastTargetTrackingMode.FaceTarget, true, true, false, comboBasic3Hit3Damage); SkillData comboSlamHit1Skill = CreateSkill( $"{SkillsFolder}/Data_Skill_Drog_콤보-강타_1.asset", "콤보-강타 1타", "강타 콤보의 첫 선행 타격입니다.", new[] { comboSlamHit1Clip }, 1f, SkillCastTargetTrackingMode.FaceTarget, true, true, false, comboSlamHit1Damage); SkillData comboSlamHit2Skill = CreateSkill( $"{SkillsFolder}/Data_Skill_Drog_콤보-강타_2.asset", "콤보-강타 2타", "강타로 이어지는 두 번째 선행 타격입니다.", new[] { comboSlamHit2Clip }, 1f, SkillCastTargetTrackingMode.FaceTarget, true, true, false, comboSlamHit2Damage); SkillData comboStompHit1Skill = CreateSkill( $"{SkillsFolder}/Data_Skill_Drog_콤보-발구르기_1.asset", "콤보-발구르기 1타", "발구르기 콤보의 첫 선행 타격입니다.", new[] { comboStompHit1Clip }, 1f, SkillCastTargetTrackingMode.FaceTarget, true, true, false, comboStompHit1Damage); SkillData comboStompHit2Skill = CreateSkill( $"{SkillsFolder}/Data_Skill_Drog_콤보-발구르기_2.asset", "콤보-발구르기 2타", "발구르기로 연결되는 두 번째 선행 타격입니다.", new[] { comboStompHit2Clip }, 1f, SkillCastTargetTrackingMode.FaceTarget, true, true, false, comboStompHit2Damage); SkillData stompSkill = CreateSkill( $"{SkillsFolder}/Data_Skill_Drog_발구르기.asset", "발구르기", "근접 측후방 전체를 흔드는 광역 압박입니다.", new[] { stompClip }, 1f, SkillCastTargetTrackingMode.None, true, true, false, stompDamage, stompKnockback); SkillData leapPrepareSkill = CreateSkill( $"{SkillsFolder}/Data_Skill_Drog_도약_준비.asset", "도약 준비", "원거리 이탈 대상에게 시선을 고정합니다.", new[] { leapPrepareClip }, 1f, SkillCastTargetTrackingMode.FaceTarget, false, true, false); SkillData leapAirSkill = CreateSkill( $"{SkillsFolder}/Data_Skill_Drog_도약_공중.asset", "도약 공중", "대상 위치로 도약하는 이동 스텝입니다.", new[] { leapAirClip }, 1f, SkillCastTargetTrackingMode.MoveTowardTarget, true, false, true); SkillData leapLandingSkill = CreateSkill( $"{SkillsFolder}/Data_Skill_Drog_도약_착지.asset", "도약 착지", "도약 종료 시 주변에 피해와 넉백을 줍니다.", new[] { leapLandingClip }, 1f, SkillCastTargetTrackingMode.None, false, true, false, leapLandingDamage, leapLandingKnockback); SkillData stepSkill = CreateSkill( $"{SkillsFolder}/Data_Skill_Drog_밟기.asset", "밟기", "다운된 대상을 후속 압박으로 처벌합니다.", new[] { stepClip }, 1f, SkillCastTargetTrackingMode.FaceTarget, false, true, false, stepDamage, stepKnockback); SkillData throwSkill = CreateSkill( $"{SkillsFolder}/Data_Skill_Drog_투척.asset", "투척", "부활 시전자나 원거리 대상을 견제하는 유틸리티 공격입니다.", new[] { throwClip }, 1f, SkillCastTargetTrackingMode.FaceTarget, false, true, false, throwDamage); SkillData roarSkill = CreateSkill( $"{SkillsFolder}/Data_Skill_Drog_포효.asset", "포효", "Phase 3 진입을 알리는 전환 신호입니다.", new[] { roarClip }, 0.9f, SkillCastTargetTrackingMode.None, false, true, false); SkillData executionReadySkill = CreateSkill( $"{SkillsFolder}/Data_Skill_Drog_집행_준비.asset", "집행 준비", "집행 돌입 전 자세를 고정합니다.", new[] { executionReadyClip }, 0.85f, SkillCastTargetTrackingMode.None, false, true, false); SkillData executionHit1Skill = CreateSkill( $"{SkillsFolder}/Data_Skill_Drog_집행_연타1.asset", "집행 연타1", "집행의 첫 압박 타격입니다.", new[] { executionHit1Clip }, 1f, SkillCastTargetTrackingMode.None, false, true, false, executionHit1); SkillData executionHit2Skill = CreateSkill( $"{SkillsFolder}/Data_Skill_Drog_집행_연타2.asset", "집행 연타2", "집행의 두 번째 압박 타격입니다.", new[] { executionHit2Clip }, 1.1f, SkillCastTargetTrackingMode.None, false, true, false, executionHit2); SkillData executionHit3Skill = CreateSkill( $"{SkillsFolder}/Data_Skill_Drog_집행_연타3.asset", "집행 연타3", "집행의 세 번째 압박 타격입니다.", new[] { executionHit3Clip }, 1.2f, SkillCastTargetTrackingMode.None, false, true, false, executionHit3); CreatePattern( $"{PatternsFolder}/Data_Pattern_Drog_콤보-기본기1.asset", "콤보-기본기1", PatternCategory.Basic, false, true, TargetResolveMode.HighestThreat, 2.5f, 1, false, PatternStepDefinition.CreateSkillStep(comboBasic1Hit1Skill), PatternStepDefinition.CreateSkillStep(comboBasic1Hit2Skill)); CreatePattern( $"{PatternsFolder}/Data_Pattern_Drog_콤보-기본기2.asset", "콤보-기본기2", PatternCategory.Basic, false, true, TargetResolveMode.HighestThreat, 3f, 1, false, PatternStepDefinition.CreateSkillStep(comboBasic2Hit1Skill), PatternStepDefinition.CreateSkillStep(comboBasic2Hit2Skill)); CreatePattern( $"{PatternsFolder}/Data_Pattern_Drog_콤보-기본기3.asset", "콤보-기본기3", PatternCategory.Basic, false, true, TargetResolveMode.HighestThreat, 3.25f, 1, false, PatternStepDefinition.CreateSkillStep(comboBasic3Hit1Skill), PatternStepDefinition.CreateSkillStep(comboBasic3Hit2Skill), PatternStepDefinition.CreateSkillStep(comboBasic3Hit3Skill)); CreatePattern( $"{PatternsFolder}/Data_Pattern_Drog_콤보-강타.asset", "콤보-강타", PatternCategory.Basic, false, true, TargetResolveMode.HighestThreat, 4.5f, 1, false, PatternStepDefinition.CreateSkillStep(comboSlamHit1Skill), PatternStepDefinition.CreateSkillStep(comboSlamHit2Skill), PatternStepDefinition.CreateWaitStep(0.1f), PatternStepDefinition.CreateSkillStep(slamSkill)); CreatePattern( $"{PatternsFolder}/Data_Pattern_Drog_콤보-발구르기.asset", "콤보-발구르기", PatternCategory.Basic, false, true, TargetResolveMode.HighestThreat, 5f, 1, false, PatternStepDefinition.CreateSkillStep(comboStompHit1Skill), PatternStepDefinition.CreateSkillStep(comboStompHit2Skill), PatternStepDefinition.CreateWaitStep(0.1f), PatternStepDefinition.CreateSkillStep(stompSkill)); CreatePattern( $"{PatternsFolder}/Data_Pattern_Drog_밟기.asset", "밟기", PatternCategory.Punish, false, false, TargetResolveMode.HighestThreat, 2.5f, 2, false, PatternStepDefinition.CreateSkillStep(stepSkill)); CreatePattern( $"{PatternsFolder}/Data_Pattern_Drog_도약.asset", "도약", PatternCategory.Big, false, false, TargetResolveMode.Mobility, 8f, 2, false, PatternStepDefinition.CreateSkillStep(leapPrepareSkill), PatternStepDefinition.CreateWaitStep(0.1f), PatternStepDefinition.CreateSkillStep(leapAirSkill), PatternStepDefinition.CreateWaitStep(0.1f), PatternStepDefinition.CreateSkillStep(leapLandingSkill)); CreatePattern( $"{PatternsFolder}/Data_Pattern_Drog_투척.asset", "투척", PatternCategory.Basic, false, false, TargetResolveMode.Utility, 10f, 2, false, PatternStepDefinition.CreateSkillStep(throwSkill)); CreatePattern( $"{PatternsFolder}/Data_Pattern_Drog_집행.asset", "집행", PatternCategory.Big, true, false, TargetResolveMode.HighestThreat, 45f, 3, false, PatternStepDefinition.CreateSkillStep(executionReadySkill), PatternStepDefinition.CreateChargeWaitStep(2.25f, executionTelegraph, 0.1f, 2f), PatternStepDefinition.CreateSkillStep(executionHit1Skill), PatternStepDefinition.CreateWaitStep(0.65f), PatternStepDefinition.CreateSkillStep(executionHit2Skill), PatternStepDefinition.CreateWaitStep(0.45f), PatternStepDefinition.CreateSkillStep(executionHit3Skill)); AssetDatabase.SaveAssets(); AssetDatabase.Refresh(); Debug.Log("[DrogCombatAssets] 드로그 스킬/이펙트/패턴 플레이스홀더 자산 재구성이 완료되었습니다."); } catch (Exception exception) { Debug.LogException(exception); } } /// /// 폴더가 없으면 생성합니다. /// private static void EnsureFolder(string path) { if (AssetDatabase.IsValidFolder(path)) return; string parent = Path.GetDirectoryName(path)?.Replace('\\', '/'); if (string.IsNullOrEmpty(parent)) return; EnsureFolder(parent); AssetDatabase.CreateFolder(parent, Path.GetFileName(path)); } /// /// 이전 단일 스킬 기반 콤보 자산을 제거합니다. /// private static void DeleteLegacyComboAssets() { string[] legacyPaths = { $"{AnimationsFolder}/Anim_Drog_콤보-기본기1_0.anim", $"{AnimationsFolder}/Anim_Drog_콤보-기본기2_0.anim", $"{AnimationsFolder}/Anim_Drog_콤보-기본기3_0.anim", $"{AnimationsFolder}/Anim_Drog_콤보-발구르기_선행_0.anim", $"{SkillsFolder}/Data_Skill_Drog_콤보-기본기1.asset", $"{SkillsFolder}/Data_Skill_Drog_콤보-기본기2.asset", $"{SkillsFolder}/Data_Skill_Drog_콤보-기본기3.asset", $"{SkillsFolder}/Data_Skill_Drog_콤보-발구르기_선행.asset", $"{EffectsFolder}/Data_SkillEffect_Drog_콤보-기본기1_0_데미지.asset", $"{EffectsFolder}/Data_SkillEffect_Drog_콤보-기본기2_0_데미지.asset", $"{EffectsFolder}/Data_SkillEffect_Drog_콤보-기본기3_0_데미지.asset", $"{EffectsFolder}/Data_SkillEffect_Drog_콤보-발구르기_선행_0_데미지.asset", }; for (int i = 0; i < legacyPaths.Length; i++) { if (AssetDatabase.LoadMainAssetAtPath(legacyPaths[i]) == null) continue; AssetDatabase.DeleteAsset(legacyPaths[i]); } } /// /// 지정 경로의 ScriptableObject를 읽거나 새로 생성합니다. /// private static T LoadOrCreateAsset(string path) where T : ScriptableObject { T asset = AssetDatabase.LoadAssetAtPath(path); if (asset != null) return asset; asset = ScriptableObject.CreateInstance(); asset.name = Path.GetFileNameWithoutExtension(path); AssetDatabase.CreateAsset(asset, path); return asset; } /// /// 드로그 스킬 플레이스홀더용 빈 애니메이션 클립을 보장합니다. /// private static AnimationClip EnsurePlaceholderClip(string path) { AnimationClip clip = AssetDatabase.LoadAssetAtPath(path); if (clip != null) return clip; clip = new AnimationClip { name = Path.GetFileNameWithoutExtension(path), frameRate = 60f, }; AssetDatabase.CreateAsset(clip, path); return clip; } /// /// 소스 클립을 독립 .anim 자산으로 복제하거나 기존 자산을 갱신합니다. /// private static AnimationClip EnsureClipFromSource(string targetPath, string sourcePath, string sourceClipName = null) { AnimationClip sourceClip = LoadClipFromPath(sourcePath, sourceClipName); if (sourceClip == null) { Debug.LogWarning($"[DrogCombatAssets] 소스 클립을 찾지 못해 플레이스홀더를 유지합니다: {sourcePath} ({sourceClipName})"); return EnsurePlaceholderClip(targetPath); } AnimationClip clonedClip = CloneClip(sourceClip, Path.GetFileNameWithoutExtension(targetPath)); AnimationClip existingClip = AssetDatabase.LoadAssetAtPath(targetPath); if (existingClip == null) { AssetDatabase.CreateAsset(clonedClip, targetPath); return clonedClip; } EditorUtility.CopySerialized(clonedClip, existingClip); existingClip.name = Path.GetFileNameWithoutExtension(targetPath); EditorUtility.SetDirty(existingClip); UnityEngine.Object.DestroyImmediate(clonedClip); return existingClip; } /// /// 경로에서 AnimationClip을 읽습니다. FBX인 경우 지정 이름의 서브 클립을 우선 사용합니다. /// private static AnimationClip LoadClipFromPath(string path, string preferredClipName = null) { AnimationClip directClip = AssetDatabase.LoadAssetAtPath(path); if (directClip != null && (string.IsNullOrEmpty(preferredClipName) || directClip.name == preferredClipName)) return directClip; UnityEngine.Object[] subAssets = AssetDatabase.LoadAllAssetsAtPath(path); AnimationClip fallbackClip = null; for (int i = 0; i < subAssets.Length; i++) { if (subAssets[i] is not AnimationClip clip) continue; if (!string.IsNullOrEmpty(preferredClipName) && clip.name == preferredClipName) return clip; if (fallbackClip == null && !clip.name.StartsWith("__preview__", StringComparison.Ordinal)) fallbackClip = clip; } return fallbackClip; } /// /// 소스 클립의 커브와 이벤트를 복사한 독립 AnimationClip을 생성합니다. /// private static AnimationClip CloneClip(AnimationClip sourceClip, string targetName) { AnimationClip clonedClip = new AnimationClip { name = targetName, frameRate = sourceClip.frameRate, legacy = sourceClip.legacy, wrapMode = sourceClip.wrapMode, localBounds = sourceClip.localBounds, }; EditorCurveBinding[] bindings = AnimationUtility.GetCurveBindings(sourceClip); for (int i = 0; i < bindings.Length; i++) { AnimationCurve curve = AnimationUtility.GetEditorCurve(sourceClip, bindings[i]); if (curve == null || curve.keys.Length == 0) continue; clonedClip.SetCurve(bindings[i].path, bindings[i].type, bindings[i].propertyName, curve); } AnimationEvent[] sourceEvents = AnimationUtility.GetAnimationEvents(sourceClip); if (sourceEvents.Length > 0) { var clonedEvents = new AnimationEvent[sourceEvents.Length]; for (int i = 0; i < sourceEvents.Length; i++) { clonedEvents[i] = new AnimationEvent { time = sourceEvents[i].time, functionName = sourceEvents[i].functionName, floatParameter = sourceEvents[i].floatParameter, intParameter = sourceEvents[i].intParameter, stringParameter = sourceEvents[i].stringParameter, objectReferenceParameter = sourceEvents[i].objectReferenceParameter, messageOptions = sourceEvents[i].messageOptions, }; } AnimationUtility.SetAnimationEvents(clonedClip, clonedEvents); } return clonedClip; } /// /// Effect/Skill/Pattern의 공통 Object 리스트를 설정합니다. /// private static void SetObjectList(SerializedObject serializedObject, string propertyName, IReadOnlyList values) { SerializedProperty listProperty = serializedObject.FindProperty(propertyName); listProperty.arraySize = values != null ? values.Count : 0; for (int i = 0; i < listProperty.arraySize; i++) { listProperty.GetArrayElementAtIndex(i).objectReferenceValue = values[i]; } } /// /// 범위형 효과의 공통 판정 설정을 적용합니다. /// private static void ConfigureAreaEffect( SerializedObject serializedObject, AreaShapeType areaShape, float areaRadius, float fanOriginDistance, float fanRadius, float fanHalfAngle, AreaCenterType areaCenter) { serializedObject.FindProperty("targetType").enumValueIndex = (int)TargetType.Area; serializedObject.FindProperty("targetTeam").enumValueIndex = (int)TargetTeam.Enemy; serializedObject.FindProperty("areaCenter").enumValueIndex = (int)areaCenter; serializedObject.FindProperty("areaShape").enumValueIndex = (int)areaShape; serializedObject.FindProperty("includeCasterInArea").boolValue = false; serializedObject.FindProperty("areaRadius").floatValue = areaRadius; serializedObject.FindProperty("fanOriginDistance").floatValue = fanOriginDistance; serializedObject.FindProperty("fanRadius").floatValue = fanRadius; serializedObject.FindProperty("fanHalfAngle").floatValue = fanHalfAngle; } /// /// 범위 피해 효과를 생성하거나 갱신합니다. /// private static DamageEffect CreateDamageEffect( string path, float baseDamage, DamageType damageType, float statScaling, AreaShapeType areaShape, float areaRadius, float fanOriginDistance, float fanRadius, float fanHalfAngle, AreaCenterType areaCenter) { DamageEffect effect = LoadOrCreateAsset(path); SerializedObject serializedObject = new SerializedObject(effect); ConfigureAreaEffect(serializedObject, areaShape, areaRadius, fanOriginDistance, fanRadius, fanHalfAngle, areaCenter); serializedObject.FindProperty("baseDamage").floatValue = baseDamage; serializedObject.FindProperty("damageType").enumValueIndex = (int)damageType; serializedObject.FindProperty("statScaling").floatValue = statScaling; serializedObject.ApplyModifiedPropertiesWithoutUndo(); EditorUtility.SetDirty(effect); return effect; } /// /// 범위 다운 효과를 생성하거나 갱신합니다. /// private static DownEffect CreateDownEffect( string path, float duration, AreaShapeType areaShape, float areaRadius, float fanOriginDistance, float fanRadius, float fanHalfAngle, AreaCenterType areaCenter) { DownEffect effect = LoadOrCreateAsset(path); SerializedObject serializedObject = new SerializedObject(effect); ConfigureAreaEffect(serializedObject, areaShape, areaRadius, fanOriginDistance, fanRadius, fanHalfAngle, areaCenter); serializedObject.FindProperty("duration").floatValue = duration; serializedObject.ApplyModifiedPropertiesWithoutUndo(); EditorUtility.SetDirty(effect); return effect; } /// /// 범위 넉백 효과를 생성하거나 갱신합니다. /// private static KnockbackEffect CreateKnockbackEffect( string path, float force, float upwardForce, float duration, AreaShapeType areaShape, float areaRadius, float fanOriginDistance, float fanRadius, float fanHalfAngle, AreaCenterType areaCenter) { KnockbackEffect effect = LoadOrCreateAsset(path); SerializedObject serializedObject = new SerializedObject(effect); ConfigureAreaEffect(serializedObject, areaShape, areaRadius, fanOriginDistance, fanRadius, fanHalfAngle, areaCenter); serializedObject.FindProperty("force").floatValue = force; serializedObject.FindProperty("upwardForce").floatValue = upwardForce; serializedObject.FindProperty("duration").floatValue = duration; serializedObject.ApplyModifiedPropertiesWithoutUndo(); EditorUtility.SetDirty(effect); return effect; } /// /// 다운 대상 추가 피해 효과를 생성하거나 갱신합니다. /// private static HitReactionDamageEffect CreateHitReactionDamageEffect( string path, float baseDamage, DamageType damageType, float statScaling, float downedDamageMultiplier, AreaShapeType areaShape, float areaRadius, float fanOriginDistance, float fanRadius, float fanHalfAngle, AreaCenterType areaCenter) { HitReactionDamageEffect effect = LoadOrCreateAsset(path); SerializedObject serializedObject = new SerializedObject(effect); ConfigureAreaEffect(serializedObject, areaShape, areaRadius, fanOriginDistance, fanRadius, fanHalfAngle, areaCenter); serializedObject.FindProperty("baseDamage").floatValue = baseDamage; serializedObject.FindProperty("damageType").enumValueIndex = (int)damageType; serializedObject.FindProperty("statScaling").floatValue = statScaling; serializedObject.FindProperty("bonusAgainstDownedTarget").boolValue = true; serializedObject.FindProperty("downedDamageMultiplier").floatValue = downedDamageMultiplier; serializedObject.ApplyModifiedPropertiesWithoutUndo(); EditorUtility.SetDirty(effect); return effect; } /// /// 빈 애니메이션 상태에서도 즉시 발동 가능한 드로그 스킬 플레이스홀더를 생성하거나 갱신합니다. /// private static SkillData CreateSkill( string path, string skillName, string description, IReadOnlyList clips, float animationSpeed, SkillCastTargetTrackingMode trackingMode, bool useRootMotion, bool ignoreRootMotionY, bool jumpToTarget, params SkillEffect[] castStartEffects) { SkillData skill = LoadOrCreateAsset(path); SerializedObject serializedObject = new SerializedObject(skill); serializedObject.FindProperty("skillName").stringValue = skillName; serializedObject.FindProperty("description").stringValue = description; serializedObject.FindProperty("skillRole").enumValueIndex = (int)SkillRoleType.Attack; serializedObject.FindProperty("activationType").enumValueIndex = (int)SkillActivationType.Instant; serializedObject.FindProperty("baseTypes").intValue = (int)SkillBaseType.Attack; serializedObject.FindProperty("animationSpeed").floatValue = animationSpeed; serializedObject.FindProperty("useRootMotion").boolValue = useRootMotion; serializedObject.FindProperty("ignoreRootMotionY").boolValue = ignoreRootMotionY; serializedObject.FindProperty("jumpToTarget").boolValue = jumpToTarget; serializedObject.FindProperty("blockMovementWhileCasting").boolValue = true; serializedObject.FindProperty("blockJumpWhileCasting").boolValue = true; serializedObject.FindProperty("blockOtherSkillsWhileCasting").boolValue = true; serializedObject.FindProperty("castTargetTrackingMode").enumValueIndex = (int)trackingMode; serializedObject.FindProperty("castTargetRotationSpeed").floatValue = 12f; serializedObject.FindProperty("castTargetStopDistance").floatValue = 2.5f; serializedObject.FindProperty("cooldown").floatValue = 0f; serializedObject.FindProperty("manaCost").floatValue = 0f; serializedObject.FindProperty("maxGemSlotCount").intValue = 0; serializedObject.FindProperty("triggeredEffects").arraySize = 0; var clipObjects = new List(); if (clips != null) { for (int i = 0; i < clips.Count; i++) { if (clips[i] != null) clipObjects.Add(clips[i]); } } SetObjectList(serializedObject, "animationClips", clipObjects); var effectObjects = new List(); if (castStartEffects != null) { for (int i = 0; i < castStartEffects.Length; i++) { if (castStartEffects[i] != null) effectObjects.Add(castStartEffects[i]); } } SetObjectList(serializedObject, "castStartEffects", effectObjects); serializedObject.ApplyModifiedPropertiesWithoutUndo(); skill.RefreshAnimationClips(); EditorUtility.SetDirty(skill); return skill; } /// /// 드로그 보스 패턴 자산을 생성하거나 갱신합니다. /// private static BossPatternData CreatePattern( string path, string patternName, PatternCategory category, bool isSignature, bool isMelee, TargetResolveMode targetMode, float cooldown, int minPhase, bool skipJumpStepOnNoTarget, params PatternStepDefinition[] stepDefinitions) { BossPatternData pattern = LoadOrCreateAsset(path); SerializedObject serializedObject = new SerializedObject(pattern); serializedObject.FindProperty("patternName").stringValue = patternName; serializedObject.FindProperty("category").enumValueIndex = (int)category; serializedObject.FindProperty("isSignature").boolValue = isSignature; serializedObject.FindProperty("isMelee").boolValue = isMelee; serializedObject.FindProperty("targetMode").enumValueIndex = (int)targetMode; serializedObject.FindProperty("cooldown").floatValue = cooldown; serializedObject.FindProperty("minPhase").intValue = minPhase; serializedObject.FindProperty("skipJumpStepOnNoTarget").boolValue = skipJumpStepOnNoTarget; SerializedProperty stepsProperty = serializedObject.FindProperty("steps"); stepsProperty.arraySize = stepDefinitions != null ? stepDefinitions.Length : 0; for (int i = 0; i < stepsProperty.arraySize; i++) { ConfigurePatternStep(stepsProperty.GetArrayElementAtIndex(i), stepDefinitions[i]); } serializedObject.ApplyModifiedPropertiesWithoutUndo(); EditorUtility.SetDirty(pattern); return pattern; } /// /// 단일 패턴 스텝 데이터를 SerializedProperty에 기록합니다. /// private static void ConfigurePatternStep(SerializedProperty stepProperty, PatternStepDefinition definition) { stepProperty.FindPropertyRelative("Type").enumValueIndex = (int)definition.StepType; stepProperty.FindPropertyRelative("Skill").objectReferenceValue = definition.Skill; stepProperty.FindPropertyRelative("Duration").floatValue = definition.Duration; SerializedProperty chargeDataProperty = stepProperty.FindPropertyRelative("ChargeData"); if (chargeDataProperty == null) return; chargeDataProperty.FindPropertyRelative("requiredDamageRatio").floatValue = definition.RequiredDamageRatio; chargeDataProperty.FindPropertyRelative("telegraphAbnormality").objectReferenceValue = definition.TelegraphAbnormality; chargeDataProperty.FindPropertyRelative("staggerDuration").floatValue = definition.StaggerDuration; } /// /// 패턴 스텝 정의를 간단히 구성하기 위한 헬퍼입니다. /// private sealed class PatternStepDefinition { public PatternStepType StepType { get; private set; } public SkillData Skill { get; private set; } public float Duration { get; private set; } public AbnormalityData TelegraphAbnormality { get; private set; } public float RequiredDamageRatio { get; private set; } public float StaggerDuration { get; private set; } public static PatternStepDefinition CreateSkillStep(SkillData skill) { return new PatternStepDefinition { StepType = PatternStepType.Skill, Skill = skill, Duration = 0f, }; } public static PatternStepDefinition CreateWaitStep(float duration) { return new PatternStepDefinition { StepType = PatternStepType.Wait, Duration = duration, }; } public static PatternStepDefinition CreateChargeWaitStep(float duration, AbnormalityData telegraphAbnormality, float requiredDamageRatio, float staggerDuration) { return new PatternStepDefinition { StepType = PatternStepType.ChargeWait, Duration = duration, TelegraphAbnormality = telegraphAbnormality, RequiredDamageRatio = requiredDamageRatio, StaggerDuration = staggerDuration, }; } } } }