Project_WL/Assets/WL/Combat/Reaction/WLImpactTierSettings.cs

270 lines
18 KiB
C#
Raw Permalink Normal View History

// ─────────────────────────────────────────────────────────────────────────────
// WLImpactTierSettings.cs — 임팩트 티어(811h) · PC 피격 림라이트/넉백 · 대시 잔상(811g) 값의 단일 출처(SOT · C45)
//
// PD 지시 #813 · 발주서 WL-811gh §1 · 설계안 §C 811g·811h 행 · 기준서 §B 요소 2(타격감)·8-1(피격 가시성) (2026-09-09)
//
// 에셋 경로(고정): Assets/WL/Combat/Settings/Resources/WL/WLImpactTierSettings.asset
// → Resources.Load<WLImpactTierSettings>("WL/WLImpactTierSettings") 로 1회 로드 후 캐시.
// 에셋이 없거나 enabled = false 또는 RuntimeDisabled 면
// · WLHitFeel 은 **기존 2단(hitStopDuration/hitStopKillDuration · shakeKillMultiplier)** 그대로 돈다
// · MobHitFlash 의 점멸 배수는 1(= 기존 색·시간)
// · PcHitReaction · DashAfterImage 는 구독만 하고 아무 것도 하지 않는다
// = C8 롤백 1순위. 코드 무수정 롤백 = 이 에셋의 enabled 를 0 으로.
//
// ■ 티어 4단 (설계안 811h "평타 0 · 크리 1 · 킬/피니시 2" 를 보스 처치까지 4단으로 확장)
// Light 일반타
// Heavy 치명(critical) · 스킬 투사체(ProjectileData.m_SkillListTableData != null) · 최근 평균 × N 이상 피해
// Kill 이 타격으로 처치
// BossKill 보스(eSubRol.Boss) 처치 · 옵션으로 엘리트(eSubRol.Elite) 포함
//
// ■ 왜 값이 전부 여기 있나 — 코드 상수 0(조직 C45). 히트스톱 ms · 셰이크 배수 · 펀치 배수 · 점멸 배수 ·
// 티어별 초당 상한 · PC 피격 · 잔상 수치가 전부 배열/필드다. PD 튜닝은 이 에셋만 고친다.
//
// 🔴 어셈블리 주의: Assets/WL/Combat/ 에 .asmdef 를 만들지 말 것.
// ─────────────────────────────────────────────────────────────────────────────
using UnityEngine;
namespace WL.Combat.Reaction
{
/// <summary>임팩트 티어 4단. 배열 인덱스와 같다(Light 0 · Heavy 1 · Kill 2 · BossKill 3).</summary>
public enum ImpactTierKind { Light = 0, Heavy = 1, Kill = 2, BossKill = 3 }
/// <summary>클래스(직업) ID → 잔상 색. classId = Actor.Get_ID() = ClassConfigTableData.n_ClassID 실측.</summary>
[System.Serializable]
public struct ImpactClassColor { public int classId; [ColorUsage(true, true)] public Color color; }
/// <summary>스킬 추가효과(eSkillExtraType) → 잔상 색. extraType = (int)eSkillExtraType.</summary>
[System.Serializable]
public struct ImpactElementColor { public int extraType; [ColorUsage(true, true)] public Color color; }
[CreateAssetMenu(fileName = "WLImpactTierSettings", menuName = "WL/Impact Tier Settings", order = 324)]
public sealed class WLImpactTierSettings : ScriptableObject
{
public const string ResourcesPath = "WL/WLImpactTierSettings";
static WLImpactTierSettings s_cached;
static bool s_lookupDone;
public static WLImpactTierSettings Instance
{
get
{
if (!s_lookupDone)
{
s_cached = Resources.Load<WLImpactTierSettings>(ResourcesPath);
s_lookupDone = true;
}
return s_cached;
}
}
/// <summary>진단·A/B 전용 런타임 스위치. 에셋을 건드리지 않고 이 3기능만 끈다. Play 종료 시 자동 false.</summary>
public static bool RuntimeDisabled;
public static bool Enabled
{
get { var i = Instance; return i != null && i.enabled && !RuntimeDisabled; }
}
public static void ClearCache() { s_cached = null; s_lookupDone = false; }
// ─────────────────────────────────────────── 전체 스위치
[Header("전체 스위치")]
[Tooltip("끄면 WLHitFeel 이 기존 2단으로 돌아가고 점멸 배수 1 · PC 피격 연출 0 · 대시 잔상 0 = 원본 동작(C8). 롤백 1순위.")]
public bool enabled = true;
[Tooltip("티어 판정·연출 결정을 Console 에 남긴다(검증용 · 문자열 결합이라 GC 발생 · 배포 시 0).")]
public bool verboseLog = false;
// ─────────────────────────────────────────── ① 티어 판정
[Header("① 티어 판정 (HitConfirmed → Light/Heavy/Kill/BossKill)")]
[Tooltip("끄면 티어가 항상 Light 로 고정된다(값 배열의 0번만 쓴다).")]
public bool tierEnabled = true;
[Tooltip("메인 PC 가 때린 타격만 티어로 올린다. 끄면 펫·소환수 타격도 Heavy/Kill 로 올라간다.")]
public bool onlyByMainPC = true;
[Tooltip("치명타(HitConfirmedEvent.critical = DamageInfo.Critical != eStat.None)를 Heavy 로 본다.")]
public bool heavyOnCritical = true;
[Tooltip("스킬 투사체(ProjectileData.m_SkillListTableData != null)를 Heavy 로 본다.\n" +
"근접 평타도 0.02 s 박스 '투사체'라 projectile != null 만으로는 전부 Heavy 가 된다 → 스킬 테이블이 붙은 것만 본다(실측 CombatEvents.RaiseHitboxHit 의 isSkill 과 같은 식).")]
public bool heavyOnSkill = true;
[Tooltip("최근 평균 피해 × 이 배수 이상이면 Heavy. 0 이하면 이 판정을 쓰지 않는다.")]
public float heavyDamageAverageMultiplier = 1.8f;
[Tooltip("최근 평균을 내는 표본 수(링 버퍼 · 사전 할당 · GC 0). 이 수만큼 모이기 전에는 평균 판정을 쓰지 않는다.")]
[Range(2, 64)] public int damageAverageSamples = 16;
[Tooltip("보스 처치를 BossKill 로 본다(eSubRol.Boss 실측 · SpawnedEvent.isBoss 와 같은 식).")]
public bool bossKillEnabled = true;
[Tooltip("엘리트(eSubRol.Elite) 처치도 BossKill 로 본다.")]
public bool bossKillIncludesElite = true;
// ─────────────────────────────────────────── ② 티어별 값 (배열 인덱스 = 티어)
[Header("② 티어별 값 — 인덱스 0 Light · 1 Heavy · 2 Kill · 3 BossKill")]
[Tooltip("히트스톱 길이(밀리초 · unscaled). 0 = 그 티어는 히트스톱 없음.\n" +
"발주 제안 0 / 40 / 70 / 120. 콤보 간격 0.42~0.47 s 라 Heavy 40 ms 는 리듬을 깨지 않는다.\n" +
"실제 적용은 TimeScaleArbiter(Pause > Cinematic > HitStop · 초당 hitStopPerSecondMax 스로틀) 경유.")]
public float[] hitStopMs = new float[] { 0f, 40f, 70f, 120f };
[Tooltip("히트스톱 안전 상한(초). 티어 시스템이 켜져 있으면 WLHitFeelSettings.hitStopMax(0.08) 대신 이 값으로 자른다.\n" +
"BossKill 120 ms 를 허용하려면 0.12 이상이어야 한다.")]
public float hitStopMaxSeconds = 0.13f;
[Tooltip("카메라 셰이크 진폭 배수(WLHitFeelSettings.shakeAmplitude 에 곱한다). 0 = 그 티어는 셰이크 없음.\n" +
"발주 제안 0 / 1.0 / 1.4 / 2.2. 🔴 Light 0 은 '일반타에는 화면이 안 흔들린다' 는 뜻이다 —\n" +
"대비를 만드는 것이 티어 설계의 핵심이지만, 평타 피드백을 남기려면 여기만 0.4~0.6 으로 올리면 된다.")]
public float[] shakeMultiplier = new float[] { 0f, 1f, 1.4f, 2.2f };
[Tooltip("피격 몹 스케일 펀치 배수. 실제 펀치 = 1 + (WLHitFeelSettings.scalePunch - 1) × 이 값. 0 = 펀치 없음.\n" +
"Kill/BossKill 은 WLHitFeelSettings.skipPunchOnKill 이 켜져 있으면 애초에 펀치가 안 나간다.")]
public float[] punchMultiplier = new float[] { 1f, 1.4f, 1f, 1f };
[Tooltip("몹 점멸 색 배수(MobHitFlash 의 blinkColor·blinkEmission 의 RGB 에 곱한다 · 알파는 그대로). 1 = 기존 강도.")]
public float[] blinkColorMultiplier = new float[] { 1f, 1.35f, 1.7f, 2.2f };
[Tooltip("몹 점멸 시간 배수(MobHitFlash 의 blinkOnSeconds 에 곱한다). 1 = 기존 0.03 s.")]
public float[] blinkTimeMultiplier = new float[] { 1f, 1.3f, 1.8f, 2.4f };
[Tooltip("히트 이펙트 스케일 배수 — 【미적용 · 예약】 원본 히트 이펙트 스폰 지점이 Assets/Script/** 라\n" +
"'원본 훅 0' 제약에서 소비처가 없다. ImpactTier.HitEffectScale(tier) 로 읽을 수는 있다(후속 웨이브용).")]
public float[] hitEffectScale = new float[] { 1f, 1.15f, 1.3f, 1.6f };
[Tooltip("티어별 초당 발동 상한(회/초). 0 이하 = 제한 없음.\n" +
"히트스톱·셰이크는 구조상 동시 1개(TimeScaleArbiter 1층 · 셰이커 1개)라 '동시 상한' 은 초당 상한으로 실현한다.\n" +
"근접 스윙 1회가 군집 5~7 마리를 같은 프레임에 때리므로 Heavy 폭주를 여기서 막는다.")]
public float[] tierMaxPerSecond = new float[] { 0f, 4f, 3f, 0f };
// ─────────────────────────────────────────── ③ PC 피격 림라이트 (811g-①)
[Header("③ PC 피격 림라이트 (Damaged → MPB 에미션/색 · 셰이더 교체 0 · 머티리얼 인스턴스 0)")]
[Tooltip("PC 가 맞을 때 MPB 로 색·에미션을 올린다(MobHitFlash 와 같은 방식). 813tj HitVignette(UI)와 합쳐 기준서 8-1 「HP + 연출 2종」.")]
public bool pcHitFlashEnabled = true;
[Tooltip("무적(invincible=true)일 때는 연출하지 않는다. Q2 무적 유지 중에는 이 값이 1 이면 연출이 아예 안 보인다 —\n" +
"무적 해제(D-6) 전에 화면에서 보려면 0 으로 두고 QA 한다.")]
public bool pcHitSkipWhenInvincible = true;
[Tooltip("피해 0 은 연출하지 않는다.")]
public bool pcHitSkipWhenDamageZero = true;
[Tooltip("림/에미션이 켜져 있는 시간(초 · unscaled). 발주 규정 0.15.")]
public float pcHitFlashSeconds = 0.15f;
[Tooltip("연속 피격 시 다시 켜기까지의 최소 간격(초 · unscaled).")]
public float pcHitCooldownSeconds = 0.05f;
[Tooltip("PC 피격 색(HDR · 알베도 색 프로퍼티). 붉은 기 = '내가 맞았다' 가시성.")]
[ColorUsage(true, true)] public Color pcHitColor = new Color(1.9f, 0.55f, 0.5f, 1f);
[Tooltip("에미션도 함께 올린다(MK Toon 은 키워드 없이 가산 · 실측 _EmissionColor).")]
public bool pcHitUseEmission = true;
[Tooltip("PC 피격 에미션 색(HDR) — 림라이트처럼 보이게 하는 주 성분.")]
[ColorUsage(true, true)] public Color pcHitEmission = new Color(1.2f, 0.15f, 0.1f, 1f);
[Tooltip("알베도/베이스 색 프로퍼티 후보(앞에서부터 셰이더에 있는 첫 것 · Shader.FindPropertyIndex 실측).\n" +
"【실측 2026-09-09】 PC(Assets/Res_Addr/PC/Ai01.prefab)의 셰이더는 \"Toon/Toon\"(Unity Chan Toon Shader · UnityToon.shader:50) = _BaseColor 보유.\n" +
"몹(MK Toon)은 _AlbedoColor 라 둘 다 후보로 둔다 — 이 목록 하나로 PC·몹 양쪽을 덮는다.")]
public string[] pcHitColorProperties = new string[] { "_AlbedoColor", "_BaseColor" };
[Tooltip("에미션 색 프로퍼티 후보.\n" +
"【실측】 Toon/Toon 의 실제 에미션은 _Emissive_Color(HDR · CommonProperties.shaderblock:187)이고, _EmissionColor 도 프로퍼티로는 있으나\n" +
"셰이딩에 쓰이지 않는 잔재다 → _Emissive_Color 를 앞에 둔다.\n" +
"🔴 단, 이 셰이더의 에미션은 #pragma shader_feature _EMISSIVE_SIMPLE/_EMISSIVE_ANIMATION 로 키워드 게이팅된다.\n" +
" MPB 는 키워드를 켜지 못하므로, 에미션은 머티리얼이 이미 에미션을 쓰고 있을 때만 더해진다.\n" +
" PC 피격 연출의 **주 성분은 위 _BaseColor**(게이팅 없음)이다.")]
public string[] pcHitEmissionProperties = new string[] { "_Emissive_Color", "_EmissionColor" };
// ─────────────────────────────────────────── ④ PC 피격 넉백 (811g-①)
[Header("④ PC 피격 넉백 (피격 방향 반대로 짧게 · Set_Warp 금지 · NavMesh 밖 금지)")]
[Tooltip("PC 가 맞을 때 뒤로 살짝 밀린다.")]
public bool pcHitKnockbackEnabled = true;
[Tooltip("보스·엘리트에게 맞았을 때만 넉백(기본). 끄면 모든 피격에 넉백 = 잡몹 군집에서 계속 밀린다.")]
public bool pcHitKnockbackBossEliteOnly = true;
[Tooltip("넉백 거리(m). 발주 규정 0.15.")]
public float pcHitKnockbackDistance = 0.15f;
[Tooltip("넉백 시간(초 · unscaled). 발주 규정 0.1.")]
public float pcHitKnockbackSeconds = 0.1f;
[Tooltip("도착 지점이 NavMesh 위인지 검사할 반경(m). 검사에 실패하면 넉백을 아예 하지 않는다.")]
public float pcHitKnockbackNavSampleRadius = 0.5f;
[Tooltip("에이전트가 살아 있으면 NavMeshAgent.Move 로 민다(DashDriver·AttackStepDriver 선례 — 직접 대입은 에이전트 내부 위치와 어긋나\n" +
"다음 이동 명령에서 순간이동한다). 끄면 발주서 문구대로 transform.position 보간만 쓴다. Set_Warp 는 어느 쪽도 쓰지 않는다.")]
public bool pcHitKnockbackUseAgentMove = true;
[Tooltip("연속 넉백 최소 간격(초 · unscaled).")]
public float pcHitKnockbackCooldownSeconds = 0.25f;
// ─────────────────────────────────────────── ⑤ 대시 잔상 (811g-②)
[Header("⑤ 대시 잔상 (Dash began → SkinnedMeshRenderer.BakeMesh 고스트)")]
[Tooltip("대시 시작에 잔상을 남긴다. 새 셰이더/머티리얼 에셋 0 — 런타임 머티리얼 1개를 만들어 모든 고스트가 공유한다.")]
public bool ghostEnabled = true;
[Tooltip("대시 1회당 고스트 수. 발주 규정 3. BakeMesh 호출 수 = 이 값 × PC 의 SkinnedMeshRenderer 수.")]
[Range(0, 8)] public int ghostCount = 3;
[Tooltip("고스트 사이 간격(초 · unscaled). 발주 규정 0.06.")]
public float ghostIntervalSeconds = 0.06f;
[Tooltip("고스트 1개가 사라지는 데 걸리는 시간(초 · unscaled). 발주 규정 0.25.")]
public float ghostFadeSeconds = 0.25f;
[Tooltip("고스트 풀 크기(오브젝트+메시 재사용). 발주 규정 6. 풀이 비면 그 고스트는 건너뛴다(큐잉 없음).")]
[Range(1, 24)] public int ghostPoolSize = 6;
[Tooltip("PC 1명당 굽는 SkinnedMeshRenderer 최대 수(모바일 예산). 초과분은 무시한다.")]
[Range(1, 8)] public int ghostMaxRenderers = 3;
[Tooltip("고스트 머티리얼 셰이더 후보(앞에서부터 Shader.Find 로 찾은 첫 것). 전부 없으면 잔상을 만들지 않는다.\n" +
"실측: URP 17.3 패키지에 \"Universal Render Pipeline/Unlit\" 이 있다(Shaders/Unlit.shader).")]
public string[] ghostShaderNames = new string[] { "Universal Render Pipeline/Unlit", "Unlit/Color", "Sprites/Default" };
[Tooltip("고스트 색 프로퍼티 후보(앞에서부터 셰이더에 있는 첫 것). URP Unlit = _BaseColor · Unlit/Color·Sprites/Default = _Color.")]
public string[] ghostColorProperties = new string[] { "_BaseColor", "_Color" };
[Tooltip("고스트 렌더 큐(3000 = Transparent). 반투명으로 그리기 위한 값.")]
public int ghostRenderQueue = 3000;
[Tooltip("고스트 시작 알파(0~1). 여기서 0 까지 선형 감쇠한다.")]
[Range(0f, 1f)] public float ghostStartAlpha = 0.45f;
[Tooltip("클래스·원소 색을 못 찾았을 때 쓰는 기본 색(발주 규정: 없으면 흰색).")]
[ColorUsage(true, true)] public Color ghostColorDefault = new Color(1f, 1f, 1f, 1f);
[Tooltip("클래스(직업) ID → 잔상 색. classId = Actor.Get_ID()(PCActor.m_ID = ClassConfigTableData.n_ClassID 실측).\n" +
"비어 있으면 기본 색을 쓴다.")]
public ImpactClassColor[] ghostClassColors = new ImpactClassColor[0];
[Tooltip("마지막으로 시전한 스킬의 추가효과(eSkillExtraType) 색을 우선 쓴다(811i 와 같은 기준).")]
public bool ghostUseSkillElement = true;
[Tooltip("그 스킬 색을 유효하게 보는 시간(초). 이 시간이 지나면 클래스 색/기본 색으로 돌아간다.")]
public float ghostElementMemorySeconds = 4f;
[Tooltip("eSkillExtraType(int) → 잔상 색. 비어 있으면 원소 색을 쓰지 않는다.\n" +
"실측 enum 순서: 0 None · 1 Frost · 2 Cure · 3 Freezing · 4 Blood · 5 Poison · 6 Burn · 7 Stun · 8 Slow …")]
public ImpactElementColor[] ghostElementColors = new ImpactElementColor[0];
[Tooltip("화면(절두체) 밖이면 잔상을 만들지 않는다(EffectBudget.IsInView · viewMargin 은 코어 SO).")]
public bool ghostSkipOffscreen = true;
[Tooltip("EffectBudget(AfterImage 슬롯 · 코어 SO afterImageMax) 을 경유한다. 끄면 상한 없이 만든다.")]
public bool ghostUseEffectBudget = true;
[Tooltip("대시가 없는 클래스를 위해 공격 전진(AttackStepDriver) 시작에도 고스트 1개를 남긴다.\n" +
"AttackStepDriver.StepCount(정적 카운터) 변화를 폴링해서 감지한다 — 원본·기존 WL 파일 수정 0.")]
public bool ghostOnAttackStep = false;
[Tooltip("공격 전진 고스트 수(ghostOnAttackStep 이 켜졌을 때).")]
[Range(0, 4)] public int ghostAttackStepCount = 1;
}
}