feat: 아군 타게팅 시스템 구현 — SingleAlly 투사체형 치유/보호막
- 치유/보호막 스킬을 즉발 자가시전에서 투사체형 아군 1인 타겟팅으로 전환 - TargetType.SingleAlly 추가, targetOverride 매개변수로 외부 타겟 주입 지원 - PlayerSkillInput: 카메라 레이캐스트 기반 아군 탐지, 서버 검증, RPC 타겟 ID 전달 - AllyTargetIndicator: 호버 아군 위에 디스크 인디케이터 표시, 사거리/초과 색상 변경 - SpawnEffect: 타겟 방향 회전 보정 - 투사체 스폰 이펙트 에셋 생성 (치유/보호막 각각) - 인디케이터 프리팹 + URP/Unlit 머티리얼 생성 - Player 프리팹에 AllyTargetIndicator 컴포넌트 추가 및 설정 - Input.mousePosition → Mouse.current.position.ReadValue() 수정 (Input System 호환)
This commit is contained in:
@@ -87,13 +87,18 @@ namespace Colosseum.Skills.Effects
|
||||
|
||||
private Quaternion GetSpawnRotation(GameObject caster, GameObject target)
|
||||
{
|
||||
if (target != null && (spawnLocation == SpawnLocation.Target || spawnLocation == SpawnLocation.CasterForward))
|
||||
// target이 있으면 항상 target 방향 우선 (SingleAlly 타게팅 지원)
|
||||
if (target != null && target != caster)
|
||||
{
|
||||
Vector3 lookDirection = target.transform.position - caster.transform.position;
|
||||
if (lookDirection.sqrMagnitude > 0.0001f)
|
||||
return Quaternion.LookRotation(lookDirection);
|
||||
}
|
||||
|
||||
// target이 없으면 spawnLocation 기준
|
||||
if (spawnLocation == SpawnLocation.Target)
|
||||
return caster.transform.rotation;
|
||||
|
||||
return caster.transform.rotation;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,6 +65,7 @@ namespace Colosseum.Skills
|
||||
private bool waitingForEndAnimation; // EndAnimation 종료 대기 중
|
||||
private int currentRepeatCount = 1;
|
||||
private int currentIterationIndex = 0;
|
||||
private GameObject currentTargetOverride;
|
||||
|
||||
// 쿨타임 추적
|
||||
private Dictionary<SkillData, float> cooldownTracker = new Dictionary<SkillData, float>();
|
||||
@@ -80,6 +81,7 @@ namespace Colosseum.Skills
|
||||
public Animator Animator => animator;
|
||||
public SkillCancelReason LastCancelReason => lastCancelReason;
|
||||
public string LastCancelledSkillName => lastCancelledSkillName;
|
||||
public GameObject CurrentTargetOverride => currentTargetOverride;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
@@ -152,6 +154,25 @@ namespace Colosseum.Skills
|
||||
/// 슬롯 엔트리 기준으로 스킬 시전
|
||||
/// </summary>
|
||||
public bool ExecuteSkill(SkillLoadoutEntry loadoutEntry)
|
||||
{
|
||||
currentTargetOverride = null;
|
||||
return ExecuteSkillInternal(loadoutEntry);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 타겟 오버라이드와 함께 스킬 시전.
|
||||
/// SingleAlly 타입 효과에서 외부 타겟을 사용할 때 호출합니다.
|
||||
/// </summary>
|
||||
public bool ExecuteSkill(SkillLoadoutEntry loadoutEntry, GameObject targetOverride)
|
||||
{
|
||||
currentTargetOverride = targetOverride;
|
||||
return ExecuteSkillInternal(loadoutEntry);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 스킬 시전 공통 로직
|
||||
/// </summary>
|
||||
private bool ExecuteSkillInternal(SkillLoadoutEntry loadoutEntry)
|
||||
{
|
||||
SkillData skill = loadoutEntry != null ? loadoutEntry.BaseSkill : null;
|
||||
if (skill == null)
|
||||
@@ -217,7 +238,7 @@ namespace Colosseum.Skills
|
||||
continue;
|
||||
|
||||
if (debugMode) Debug.Log($"[Skill] Cast start effect: {effect.name} (index {i})");
|
||||
effect.ExecuteOnCast(gameObject);
|
||||
effect.ExecuteOnCast(gameObject, currentTargetOverride);
|
||||
}
|
||||
|
||||
if (currentCastStartAbnormalities.Count <= 0)
|
||||
@@ -266,7 +287,7 @@ namespace Colosseum.Skills
|
||||
continue;
|
||||
|
||||
if (debugMode) Debug.Log($"[Skill] Immediate self effect: {effect.name} (index {i})");
|
||||
effect.ExecuteOnCast(gameObject);
|
||||
effect.ExecuteOnCast(gameObject, currentTargetOverride);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -478,7 +499,7 @@ namespace Colosseum.Skills
|
||||
effect.DrawDebugRange(gameObject, debugDrawDuration);
|
||||
}
|
||||
|
||||
effect.ExecuteOnCast(gameObject);
|
||||
effect.ExecuteOnCast(gameObject, currentTargetOverride);
|
||||
}
|
||||
|
||||
ApplyTriggeredAbnormalities(index, effects);
|
||||
@@ -564,6 +585,7 @@ namespace Colosseum.Skills
|
||||
currentCastStartAbnormalities.Clear();
|
||||
currentTriggeredAbnormalities.Clear();
|
||||
currentTriggeredTargetsBuffer.Clear();
|
||||
currentTargetOverride = null;
|
||||
waitingForEndAnimation = false;
|
||||
currentRepeatCount = 1;
|
||||
currentIterationIndex = 0;
|
||||
@@ -589,7 +611,7 @@ namespace Colosseum.Skills
|
||||
if (effect == null || effect.TargetType == TargetType.Self)
|
||||
continue;
|
||||
|
||||
effect.CollectTargets(gameObject, currentTriggeredTargetsBuffer);
|
||||
effect.CollectTargets(gameObject, currentTriggeredTargetsBuffer, currentTargetOverride);
|
||||
}
|
||||
|
||||
if (currentTriggeredTargetsBuffer.Count == 0)
|
||||
|
||||
@@ -13,7 +13,7 @@ namespace Colosseum.Skills
|
||||
public abstract class SkillEffect : ScriptableObject
|
||||
{
|
||||
[Header("대상 설정")]
|
||||
[Tooltip("Self: 시전자, Area: 범위 내 대상")]
|
||||
[Tooltip("Self: 시전자, Area: 범위 내 대상, SingleAlly: 아군 1인 (외부에서 타겟 주입)")]
|
||||
[SerializeField] protected TargetType targetType = TargetType.Self;
|
||||
|
||||
[Header("Area Settings (TargetType이 Area일 때)")]
|
||||
@@ -49,10 +49,10 @@ namespace Colosseum.Skills
|
||||
/// <summary>
|
||||
/// 스킬 시전 시 호출
|
||||
/// </summary>
|
||||
public void ExecuteOnCast(GameObject caster)
|
||||
public void ExecuteOnCast(GameObject caster, GameObject targetOverride = null)
|
||||
{
|
||||
List<GameObject> targets = new List<GameObject>();
|
||||
CollectTargets(caster, targets);
|
||||
CollectTargets(caster, targets, targetOverride);
|
||||
|
||||
for (int i = 0; i < targets.Count; i++)
|
||||
{
|
||||
@@ -64,7 +64,7 @@ namespace Colosseum.Skills
|
||||
/// 현재 효과가 영향을 줄 대상 목록을 수집합니다.
|
||||
/// 젬의 적중 이상상태 적용 등에서 동일한 타겟 해석을 재사용하기 위한 경로입니다.
|
||||
/// </summary>
|
||||
public void CollectTargets(GameObject caster, List<GameObject> destination)
|
||||
public void CollectTargets(GameObject caster, List<GameObject> destination, GameObject targetOverride = null)
|
||||
{
|
||||
if (caster == null || destination == null)
|
||||
return;
|
||||
@@ -78,6 +78,13 @@ namespace Colosseum.Skills
|
||||
case TargetType.Area:
|
||||
CollectAreaTargets(caster, destination);
|
||||
break;
|
||||
|
||||
case TargetType.SingleAlly:
|
||||
if (targetOverride != null && IsCorrectTeam(caster, targetOverride))
|
||||
AddUniqueTarget(destination, targetOverride);
|
||||
else
|
||||
AddUniqueTarget(destination, caster);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -275,7 +282,8 @@ namespace Colosseum.Skills
|
||||
public enum TargetType
|
||||
{
|
||||
Self,
|
||||
Area
|
||||
Area,
|
||||
SingleAlly
|
||||
}
|
||||
|
||||
public enum TargetTeam
|
||||
|
||||
@@ -489,6 +489,60 @@ namespace Colosseum.Skills
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 이 로드아웃에 지정한 TargetType을 사용하는 효과가 있는지 확인합니다.
|
||||
/// 기반 스킬과 장착된 젬의 모든 효과를 검사합니다.
|
||||
/// </summary>
|
||||
public bool HasEffectWithTargetType(TargetType type)
|
||||
{
|
||||
if (baseSkill != null)
|
||||
{
|
||||
if (baseSkill.CastStartEffects != null && CheckEffectsForTargetType(baseSkill.CastStartEffects, type))
|
||||
return true;
|
||||
if (baseSkill.Effects != null && CheckEffectsForTargetType(baseSkill.Effects, type))
|
||||
return true;
|
||||
}
|
||||
|
||||
if (socketedGems == null)
|
||||
return false;
|
||||
|
||||
for (int i = 0; i < socketedGems.Length; i++)
|
||||
{
|
||||
SkillGemData gem = socketedGems[i];
|
||||
if (gem == null) continue;
|
||||
|
||||
if (gem.CastStartEffects != null && CheckEffectsForTargetType(gem.CastStartEffects, type))
|
||||
return true;
|
||||
|
||||
if (gem.TriggeredEffects != null)
|
||||
{
|
||||
for (int j = 0; j < gem.TriggeredEffects.Count; j++)
|
||||
{
|
||||
SkillGemTriggeredEffectEntry entry = gem.TriggeredEffects[j];
|
||||
if (entry == null || entry.Effects == null) continue;
|
||||
|
||||
if (CheckEffectsForTargetType(entry.Effects, type))
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool CheckEffectsForTargetType(IReadOnlyList<SkillEffect> effects, TargetType type)
|
||||
{
|
||||
if (effects == null) return false;
|
||||
|
||||
for (int i = 0; i < effects.Count; i++)
|
||||
{
|
||||
if (effects[i] != null && effects[i].TargetType == type)
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private float GetResolvedScalarMultiplier(System.Func<SkillGemData, float> selector)
|
||||
{
|
||||
if (baseSkill == null)
|
||||
|
||||
Reference in New Issue
Block a user