Project_WL/Assets/WL/Combat/Telegraph/TelegraphShape.cs

174 lines
9.7 KiB
C#
Raw Permalink Normal View History

// ─────────────────────────────────────────────────────────────────────────────
// TelegraphShape.cs — 예고 판의 크기를 「투사체 프리팹의 BoxCollider」에서 읽어 온다(신규 데이터 0)
//
// PD 지시 #815-5 · 발주서 WL-815f §1-2 · 기준서 v1 §G-0 「공격 판정의 출처」
//
// ■ 왜 콜라이더를 읽나
// 원본은 근접도 투사체를 쏜다(MobActor.Projectile → Shoot_Projectile(s_Porjectile1, …)).
// 따라서 **맞는 범위 = 그 투사체 프리팹의 BoxCollider** 다. 데칼 크기를 코드 상수로 넣으면 판정과 어긋난다.
// 실측(기준서 §G-0): Mob/P_*_Meele 3×3×3 center 0 · Elite/P_Golem_A 3×3×3 center z 1.5 · FieldBoss/Anubis_Attack 2×2×4 center z 2.
//
// ■ 어떻게 읽나 (이름 1개당 1회 · 결과는 캐시 · 정상 흐름 GC 0)
// ① 에디터(에디트 모드·플레이 모드 공통) = AssetDatabase 로 **동기** 로드 → 프로브가 Play 없이 실측할 수 있다.
// ② 빌드/플레이 = ProjectileInfo 풀(원본이 미리 로드해 둔 인스턴스)을 이름으로 훑어 콜라이더를 읽는다.
// ③ 그래도 없으면 Addressables 비동기 로드를 1회 걸고(중복 방지), 그 사이에는 SO 기본값을 쓴다.
//
// ■ 원거리 판(기준서 §G-6 「직선 데칼」)
// 길이 = 투사체 속도 × 수명. 속도는 프리팹의 ProjectileBase.Speed(= 기본 5) 이고
// Projectile_Strait.m_Speed 가 0 보다 크면 그 값이 이긴다(원본 Projectile_Strait.Set 실측).
// 수명은 몹 테이블의 f_ProjectileLifeTime1(근접 0.1 · 원거리 1.5 실측).
//
// 🔴 원본 무수정 — 읽기만 한다.
// ─────────────────────────────────────────────────────────────────────────────
using System.Collections.Generic;
using UnityEngine;
namespace WL.Combat.Telegraph
{
/// <summary>투사체 1종의 판정 상자(로컬 기준) + 이동 정보.</summary>
public struct TelegraphBox
{
public Vector3 size; // BoxCollider.size × 프리팹 lossyScale
public Vector3 center; // BoxCollider.center × 프리팹 lossyScale
public float speed; // 실효 이동 속도(m/s · 0 이면 제자리)
public bool resolved; // 진짜 프리팹에서 읽었는가(false = SO 기본값)
public string source; // 진단 문자열(어디서 읽었나)
}
public static class TelegraphShape
{
const string kPrefix = "Assets/Res_Addr/Projectile/";
const string kSuffix = ".prefab";
static readonly Dictionary<string, TelegraphBox> s_cache = new Dictionary<string, TelegraphBox>(32);
static readonly HashSet<string> s_inFlight = new HashSet<string>();
// ── 진단(프로브가 읽는다)
public static int ResolvedCount, FallbackCount, PoolHitCount, AssetDbHitCount, AddressablesRequestCount;
public static string LastSource = "";
public static int CacheCount { get { return s_cache.Count; } }
static WLTelegraphSettings St { get { return WLTelegraphSettings.Instance; } }
/// <summary>런타임 ProjectileData.Speed 의 기본값(원본 ProjectileBase.cs:19 실측 = 5). SO 로 바꿀 수 있다.</summary>
static float DefaultSpeed { get { var st = St; return st != null && st.defaultProjectileSpeed > 0f ? st.defaultProjectileSpeed : 5f; } }
/// <summary>이 투사체 이름의 판정 상자. 아직 못 읽었으면 SO 기본값(resolved = false)을 돌려주고 비동기 로드를 1회 건다.</summary>
public static TelegraphBox Get(string projectileName)
{
if (string.IsNullOrEmpty(projectileName) || projectileName == "None") return Fallback("(투사체 없음)");
TelegraphBox box;
if (s_cache.TryGetValue(projectileName, out box)) return box;
// ① 에디터 — 동기 로드(프로브가 Play 없이 실측한다)
#if UNITY_EDITOR
var asset = UnityEditor.AssetDatabase.LoadAssetAtPath<GameObject>(kPrefix + projectileName + kSuffix);
if (asset != null && ReadFrom(asset.transform, "AssetDatabase", out box))
{
s_cache[projectileName] = box; ResolvedCount++; AssetDbHitCount++; LastSource = box.source; return box;
}
#endif
// ② 원본 풀(ProjectileInfo 가 table_effectlist 로 미리 로드해 둔 인스턴스)
if (ProjectileInfo.isIns && ProjectileInfo.Ins != null)
{
var root = ProjectileInfo.Ins.transform;
string baseName = BaseName(projectileName);
for (int i = 0; i < root.childCount; i++)
{
var c = root.GetChild(i);
if (!NameMatches(c.name, baseName)) continue;
if (ReadFrom(c, "ProjectileInfo 풀", out box))
{
s_cache[projectileName] = box; ResolvedCount++; PoolHitCount++; LastSource = box.source; return box;
}
}
}
// ③ Addressables 비동기 로드 1회 — 이번 예고는 기본값으로 그리고 다음 예고부터 정확해진다
if (!s_inFlight.Contains(projectileName) && AddrResourceMgr.isIns && AddrResourceMgr.Ins != null)
{
s_inFlight.Add(projectileName);
AddressablesRequestCount++;
string key = projectileName;
AddrResourceMgr.Ins.LoadObject<GameObject>(kPrefix + key + kSuffix, handle =>
{
s_inFlight.Remove(key);
var go = handle.Result;
TelegraphBox b;
if (go != null && ReadFrom(go.transform, "Addressables", out b)) { s_cache[key] = b; ResolvedCount++; }
});
}
return Fallback(projectileName);
}
/// <summary>프로브/테스트용 — 실측값을 직접 주입한다(로드 경로 없이 크기 일치를 검증할 때).</summary>
public static void Prime(string projectileName, Vector3 size, Vector3 center, float speed, string source)
{
if (string.IsNullOrEmpty(projectileName)) return;
s_cache[projectileName] = new TelegraphBox { size = size, center = center, speed = speed, resolved = true, source = source };
}
public static void ClearCache() { s_cache.Clear(); s_inFlight.Clear(); }
public static void ResetDiagnostics()
{
ResolvedCount = FallbackCount = PoolHitCount = AssetDbHitCount = AddressablesRequestCount = 0;
LastSource = "";
}
// ───────────────────────────────────────── 내부
static TelegraphBox Fallback(string why)
{
FallbackCount++;
var st = St;
var size = st != null ? st.defaultBoxSize : new Vector3(3f, 3f, 3f);
return new TelegraphBox { size = size, center = Vector3.zero, speed = 0f, resolved = false, source = "기본값 " + why };
}
static string BaseName(string projectileName)
{
int slash = projectileName.LastIndexOf('/');
return slash >= 0 ? projectileName.Substring(slash + 1) : projectileName;
}
/// <summary>풀 인스턴스 이름은 "P_Batty_Meele" 또는 "P_Batty_Meele(Clone)" 이다. 접두 비교(부분 문자열 생성 0).</summary>
static bool NameMatches(string instanceName, string baseName)
{
if (instanceName == null || instanceName.Length < baseName.Length) return false;
return string.CompareOrdinal(instanceName, 0, baseName, 0, baseName.Length) == 0;
}
static bool ReadFrom(Transform root, string source, out TelegraphBox box)
{
box = default(TelegraphBox);
if (root == null) return false;
var bc = root.GetComponent<BoxCollider>();
if (bc == null) bc = root.GetComponentInChildren<BoxCollider>(true);
if (bc == null) return false;
// 프리팹 자체 스케일까지 반영한다(실측 = 전부 1 이지만 값이 바뀌어도 따라간다)
var ls = bc.transform.lossyScale;
var size = new Vector3(Mathf.Abs(bc.size.x * ls.x), Mathf.Abs(bc.size.y * ls.y), Mathf.Abs(bc.size.z * ls.z));
var center = new Vector3(bc.center.x * ls.x, bc.center.y * ls.y, bc.center.z * ls.z);
// 🔴 속도 실측 — 프리팹에는 Speed 필드가 없다(ProjectileBase 는 ColliderLayer/m_Pierce/str_HitEffect 만 직렬화).
// 실제 속도는 런타임 ProjectileData.Speed(기본 5)이고, Projectile_Strait.m_Speed 가 0 보다 크면 그 값이 이긴다
// (Projectile_Strait.Set: `if (m_Speed > 0f) m_ProjecTileData.Speed = m_Speed;`).
// ProjectileInfo.Shoot_Projectile 은 Speed 를 건드리지 않으므로 m_Speed = 0 인 프리팹은 기본값 5 로 난다.
float speed = 0f;
var strait = root.GetComponent<Projectile_Strait>();
if (strait == null) strait = root.GetComponentInChildren<Projectile_Strait>(true);
if (strait != null) speed = strait.m_Speed > 0f ? strait.m_Speed : DefaultSpeed;
box = new TelegraphBox { size = size, center = center, speed = speed, resolved = true, source = source };
return true;
}
}
}