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

254 lines
16 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// ─────────────────────────────────────────────────────────────────────────────
// WLTelegraphSettings.cs — 적 공격 예고(텔레그래프) 값의 단일 출처(SOT · C45)
//
// PD 지시 #815-5 「적의 공격을 확실히 유저가 인지하고 피할 수 있는 매커니즘」
// 발주서 WL-815f · 기준서 v1 §G-0~§G-4 · §G-7 1차(= ① 데칼 + ⓐ 속도 곡선 + ⓓ 펄스 + ③ 색 규약 + ④ SFX)
//
// 에셋 경로(고정): Assets/WL/Combat/Settings/Resources/WL/WLTelegraphSettings.asset
// → Resources.Load<WLTelegraphSettings>("WL/WLTelegraphSettings") 로 1회 로드 후 캐시.
// 에셋이 없거나 enabled = false 또는 RuntimeDisabled 면 Telegraph 가 아무 것도 하지 않는다
// (C8 · 애니메이터 속도 원본 · 데칼 0 · MPB 0 · 스케일 원본 = 원본 동작 100 %).
//
// 【기준서 §G-0 실측 요약 — 이 값들의 근거】
// · 원본에는 선딜 개념이 0 이다. 데미지는 공격 클립 안의 애니메이션 이벤트 `Projectile` 이 낸다.
// 즉 「예고 시간 = 클립 시작 ~ 이벤트 시각」이고 실측 0.081~0.228 s(잡몹·엘리트 전원).
// · 그래서 ⓐ 속도 곡선이 성립한다 — 애니메이터를 늦추면 **히트(=데미지)도 같이 늦어진다**(별도 동기화 0).
// · 공격 판정 범위의 출처 = 투사체 프리팹의 BoxCollider 다(근접도 투사체를 쏜다). 실측:
// 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
// → 데칼 크기를 코드 상수로 넣지 않고 **그 프리팹의 콜라이더를 읽어** 그린다 = 판정과 100 % 일치.
// · URP Decal Renderer Feature 가 없다(URP-Balanced-Renderer 의 피처 = SSAO 1개뿐) → DecalProjector 는 화면에 안 나온다.
// 기존 인디케이터 20종이 쓰는 **파티클/쿼드 방식**을 따른다(여기서는 런타임 쿼드 2장).
// · 보스(Anubis)는 코드가 3 s 차징을 이미 건다(Boss_Anubis.cs) → **보스는 건드리지 않는다**(bossUntouched).
//
// 🔴 어셈블리 주의: Assets/WL/Combat/ 에 .asmdef 를 만들지 말 것.
// ─────────────────────────────────────────────────────────────────────────────
using UnityEngine;
namespace WL.Combat.Telegraph
{
/// <summary>기준서 §G-3 위험 색 규약 「제안」. 빨강 = 못 피한다(막기) · 노랑 = 피할 수 있다(회피).</summary>
public enum TelegraphDanger { Avoidable = 0, Unavoidable = 1 }
/// <summary>데칼이 「채워지는」 방향.</summary>
public enum TelegraphFill { ForwardSweep = 0, CenterExpand = 1 }
[CreateAssetMenu(fileName = "WLTelegraphSettings", menuName = "WL/Telegraph Settings", order = 326)]
public sealed class WLTelegraphSettings : ScriptableObject
{
public const string ResourcesPath = "WL/WLTelegraphSettings";
static WLTelegraphSettings s_cached;
static bool s_lookupDone;
public static WLTelegraphSettings Instance
{
get
{
if (!s_lookupDone)
{
s_cached = Resources.Load<WLTelegraphSettings>(ResourcesPath);
s_lookupDone = true;
}
return s_cached;
}
}
/// <summary>진단·A/B 전용 런타임 스위치. 에셋을 건드리지 않고 예고만 끈다.</summary>
public static bool RuntimeDisabled;
public static bool Enabled { get { var i = Instance; return i != null && i.enabled && !RuntimeDisabled; } }
/// <summary>811gh ImpactTier 가 읽는다 — 예고 중인 몹에게 히트스톱을 걸지 않는다(기준서 §G-4).</summary>
public static bool SkipHitStop { get { var i = Instance; return i != null && i.enabled && !RuntimeDisabled && i.skipHitStopDuringTelegraph; } }
public static void ClearCache() { s_cached = null; s_lookupDone = false; }
// ─────────────────────────────────────────── 전체 스위치
[Header("전체 스위치")]
[Tooltip("끄면 속도 곡선·데칼·펄스·색·SFX 가 전부 비활성 = 원본 동작 100 %(C8 롤백 1순위).")]
public bool enabled = true;
[Tooltip("예고 시작/종료를 Console 에 남긴다(검증용 · GC 발생).")]
public bool verboseLog = false;
// ─────────────────────────────────────────── ⓐ 예고 시간 · 속도 곡선
[Header("ⓐ 예고 시간 (기준서 §G-6 표 · AttackStarted → HitboxSpawned 사이 목표 초)")]
[Tooltip("잡몹 근접 목표 예고 초. 기준서 §G-6 = 0.5 s(원본 실측 0.081~0.167).")]
public float telegraphSecondsNormal = 0.5f;
[Tooltip("잡몹 원거리(Batty_B/D 102·104) 목표 예고 초. 기준서 §G-6 = 0.6 s.")]
public float telegraphSecondsRanged = 0.6f;
[Tooltip("엘리트 목표 예고 초. 기준서 §G-6 = 0.8 s(첫 타만).")]
public float telegraphSecondsElite = 0.8f;
[Tooltip("🔴 보스는 건드리지 않는다 — 원본 코드가 이미 3 s 차징을 건다(Boss_Anubis.cs Skill1/2ChargingTime = 3).\n" +
"끄면 보스 평타에도 예고가 걸린다(발주 범위 밖 · 기본 켬 유지 권장).")]
public bool bossUntouched = true;
[Header("ⓐ 속도 곡선 (animator.speed = 히트시각 ÷ 목표 · 하한 있음)")]
[Tooltip("끄면 애니메이터 속도를 건드리지 않는다(데칼·펄스·색만 남는다).")]
public bool speedCurveEnabled = true;
[Tooltip("🔴 한계값. 원본 속도 대비 이 배수보다 더 느리게는 만들지 않는다.\n" +
"기준서 §G-2 ⓐ: k = 0.135 는 사실상 정지 화면이라 자연스러운 하한은 0.35 (Batty 예고 0.23 s).\n" +
"→ 속도 곡선 단독으로 0.5 s 는 안 된다. 데칼·펄스와 반드시 함께 쓴다.")]
public float minSpeedMultiplier = 0.35f;
[Tooltip("공격 클립의 히트 애니메이션 이벤트 함수명(이 이름의 이벤트 시각 = 원본 예고 시간).")]
public string[] hitEventNames = new[] { "Projectile", "BossProjectile2", "BossProjectile3" };
[Tooltip("클립에서 히트 이벤트를 못 찾았을 때 쓰는 기본 히트 시각(초). 기준서 실측 중앙값 ≈ 0.16.")]
public float defaultHitSeconds = 0.16f;
[Tooltip("첫 틱에서 실제 재생 중인 클립을 다시 읽어 히트 시각을 보정한다(이름 추정이 틀렸을 때 자가 치유 · 두 번째 공격부터 정확).")]
public bool correctFromLiveClip = true;
[Tooltip("HitboxSpawned 가 이 시간 안에 안 오면 강제로 원복한다(피격·사망으로 공격이 끊긴 경우 · 초).")]
public float maxTelegraphSeconds = 2.5f;
// ─────────────────────────────────────────── ① 지면 데칼
[Header("① 지면 데칼 (투사체 BoxCollider 를 읽어 같은 크기 · 런타임 쿼드 2장 · 풀링)")]
[Tooltip("끄면 판을 그리지 않는다.")]
public bool decalEnabled = true;
[Tooltip("동시에 켤 수 있는 판의 최대 수. 초과하면 그 예고는 판을 생략한다(811s 예산기 방식 · 군중에서 화면이 뭉개지는 것 방지).")]
public int maxConcurrent = 6;
[Tooltip("풀 예열 개수(첫 전투에서의 순간 할당을 없앤다). maxConcurrent 이상 권장.")]
public int poolWarmCount = 8;
[Tooltip("몹 발밑 기준 판의 높이(지면 Z-파이팅 방지 · m).")]
public float groundYOffset = 0.06f;
[Tooltip("발사 위치 규약(Actor.Get_CenterPositionFoward 의 기본값)과 같은 전방 거리(m). 원본 = 0.3.")]
public float forwardOffset = 0.3f;
[Tooltip("판 크기 = 콜라이더 크기 × 이 배수. 1 = 판정과 100 % 일치(기본값 · 바꾸지 말 것 권장).")]
public float sizeScale = 1f;
[Tooltip("투사체 프리팹을 못 읽었을 때의 기본 판 크기(기준서 실측 잡몹 근접 = 3×3×3).")]
public Vector3 defaultBoxSize = new Vector3(3f, 3f, 3f);
[Tooltip("투사체 수명(f_ProjectileLifeTime1)이 이 값 이상이면 원거리로 보고 직선 판을 쓴다.\n" +
"실측: 근접 0.1 s · 원거리 1.5 s(Batty_B/D 102·104 · Mummy_King 1024).")]
public float rangedLifetimeThreshold = 0.5f;
[Tooltip("원거리 직선 판의 최대 길이(m). 길이 = 투사체 속도 × 수명 이지만 화면을 넘지 않게 자른다.")]
public float rangedMaxLength = 12f;
[Tooltip("🔴 투사체 프리팹에는 속도 필드가 없다(실측) — 실제 속도는 런타임 ProjectileData.Speed 기본값이다.\n" +
"원본 ProjectileBase.cs:19 실측 = 5. Projectile_Strait.m_Speed 가 0 보다 크면 프리팹 값이 이긴다.")]
public float defaultProjectileSpeed = 5f;
[Tooltip("몹이 회전하면 판도 따라 돈다(원본 Play_Attack 이 transform.LookAt 으로 계속 돌린다).")]
public bool followRotation = true;
[Tooltip("채워지는 방향. ForwardSweep = 몹 쪽에서 앞으로 채워진다 · CenterExpand = 가운데서 퍼진다.")]
public TelegraphFill fillMode = TelegraphFill.ForwardSweep;
[Tooltip("바깥 판(범위)의 알파.")]
[Range(0f, 1f)] public float plateAlpha = 0.28f;
[Tooltip("채워지는 안쪽 판의 알파.")]
[Range(0f, 1f)] public float fillAlpha = 0.62f;
[Tooltip("판 셰이더 후보(앞에서부터 Shader.Find · 새 셰이더/머티리얼 에셋 0 · 811gh DashAfterImage 와 같은 방식).")]
public string[] decalShaderNames = new[] { "Universal Render Pipeline/Unlit", "Unlit/Transparent", "Sprites/Default" };
[Tooltip("판 색 프로퍼티 후보(앞에서부터 FindPropertyIndex).")]
public string[] decalColorProperties = new[] { "_BaseColor", "_Color", "_TintColor" };
[Tooltip("판의 렌더 큐(반투명 · 3000 이상). 지면 위·캐릭터 아래로 보이게 한다.")]
public int decalRenderQueue = 3050;
[Header("① IndicatorInfo.Show_Indicator 재사용 (원본 공개 API · 로드/풀링/자동 끄기 완비)")]
[Tooltip("비어 있지 않으면 Res_Addr/Obj/<이름>.prefab 인디케이터를 같이 띄운다(IndicatorInfo.Make_Indicator 로 풀에서 꺼내 WL 이 위치·회전·수명을 직접 준다).\n" +
"🔴 기본값이 빈 문자열인 이유 = 보유 인디케이터 20종은 전부 **원형 파티클**이라 사각 콜라이더와 크기를 100 % 맞출 수 없다.\n" +
"판정 일치는 위의 쿼드가 담당하고, 이 항목은 「연출 덧칠」이다. 예: Golem_Child_Skill1_Indicator.")]
public string indicatorPrefabName = "";
[Tooltip("인디케이터의 파티클 시작 크기를 실측해 콜라이더 최대 변에 맞춰 스케일을 잡는다(끄면 스케일 1).")]
public bool indicatorAutoScale = true;
// ─────────────────────────────────────────── ③ 색 규약
[Header("③ 위험 색 규약 (기준서 §G-3 「제안」 · 빨강 = 못 피함/막기 · 노랑 = 회피 가능)")]
[Tooltip("회피 가능(노랑) 판 색.")]
[ColorUsage(true, true)] public Color colorAvoidable = new Color(1f, 0.82f, 0.15f, 1f);
[Tooltip("회피 불가(빨강) 판 색.")]
[ColorUsage(true, true)] public Color colorUnavoidable = new Color(1f, 0.16f, 0.12f, 1f);
[Tooltip("잡몹 근접의 위험 등급. 기준서 §G-6 = 노랑(회피 가능).")]
public TelegraphDanger dangerNormalMelee = TelegraphDanger.Avoidable;
[Tooltip("잡몹 원거리의 위험 등급. 기준서 §G-6 = 노랑(회피 가능).")]
public TelegraphDanger dangerNormalRanged = TelegraphDanger.Avoidable;
[Tooltip("엘리트의 위험 등급. 기준서 §G-6 엘리트 연속기 = 「데칼 + 색(노랑)」.\n" +
"🔴 「막아야 하는 공격」을 도입할 때 Unavoidable(빨강)로 올리면 된다 = 값 한 줄.")]
public TelegraphDanger dangerElite = TelegraphDanger.Avoidable;
// ─────────────────────────────────────────── ⓓ 몹 색/펄스
[Header("ⓓ 몹 틴트 (MobHitFlash MPB 경로 재사용 · 813s 와 같은 소유자 = 머티리얼 인스턴스 0)")]
[Tooltip("끄면 몹 색을 건드리지 않는다.")]
public bool tintEnabled = true;
[Tooltip("회피 가능(노랑) 예고 중 알베도 색.")]
[ColorUsage(true, true)] public Color tintAlbedoAvoidable = new Color(1.20f, 1.05f, 0.60f, 1f);
[Tooltip("회피 가능(노랑) 예고 중 에미션 색.")]
[ColorUsage(true, true)] public Color tintEmissionAvoidable = new Color(0.55f, 0.34f, 0.02f, 1f);
[Tooltip("회피 불가(빨강) 예고 중 알베도 색.")]
[ColorUsage(true, true)] public Color tintAlbedoUnavoidable = new Color(1.30f, 0.60f, 0.55f, 1f);
[Tooltip("회피 불가(빨강) 예고 중 에미션 색.")]
[ColorUsage(true, true)] public Color tintEmissionUnavoidable = new Color(0.75f, 0.08f, 0.04f, 1f);
[Tooltip("히트 직전 흰 섬광을 넣는다.")]
public bool flashEnabled = true;
[Tooltip("히트까지 남은 시간이 이 값 이하가 되면 흰 섬광으로 바꾼다(초 · 「직전」).")]
public float flashLeadSeconds = 0.06f;
[Tooltip("흰 섬광이 유지되는 시간(초 · 60 fps 기준 0.05 ≈ 3프레임).")]
public float flashSeconds = 0.05f;
[ColorUsage(true, true)] public Color flashAlbedo = new Color(2.2f, 2.2f, 2.2f, 1f);
[ColorUsage(true, true)] public Color flashEmission = new Color(1.6f, 1.6f, 1.6f, 1f);
[Header("ⓓ 스케일 펄스 (813s EliteMarker 와 같은 localScale 경로 · 원 스케일 복원)")]
[Tooltip("끄면 크기를 건드리지 않는다.")]
public bool pulseEnabled = true;
[Tooltip("예고 끝(히트)에서의 최대 배수. 기준서 §G-2 ⓓ = 1.12.")]
public float pulseScale = 1.12f;
[Tooltip("원복 뒤 이 시간 동안 원 스케일을 다시 확인한다(811b WLHitFeel 스케일 펀치와 겹칠 때 크기가 남는 것 방지 · 초).")]
public float pulseRestoreGuardSeconds = 0.2f;
// ─────────────────────────────────────────── ④ SFX · 히트스톱
[Header("④ 예고 SFX (보유 사운드 재사용 · 동시 상한)")]
[Tooltip("끄면 소리를 내지 않는다.")]
public bool sfxEnabled = true;
[Tooltip("eSound 인덱스. 9 = s009_CastSpell(짧은 캐스팅 음). 프로브가 SoundInfo.arr_clip 실존을 확인한다.")]
public int sfxIndex = 9;
[Tooltip("최대 볼륨(거리 감쇠는 원본 Play_OneShot_byDistance 가 한다).")]
[Range(0f, 1f)] public float sfxVolume = 0.35f;
[Tooltip("기준서 §G-4 「동시 3발 상한」 — 이 창 안에서 이 개수까지만 낸다(군중에서 소리가 뭉개지는 것 방지).")]
public int sfxMaxConcurrent = 3;
[Tooltip("동시 상한을 세는 창(초).")]
public float sfxWindowSeconds = 0.35f;
[Header("④ 811gh 히트스톱과의 관계 (기준서 §G-4)")]
[Tooltip("🔴 예고 중인 몹을 때렸을 때 히트스톱을 걸지 않는다.\n" +
"이유: hitStopMs 가 예고 구간에 겹치면 timeScale 이 바뀌어 **예고 길이가 흔들린다**.\n" +
"끄면 811gh 기존 동작 그대로(히트스톱 유지).")]
public bool skipHitStopDuringTelegraph = true;
}
}