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

270 lines
18 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.

// ─────────────────────────────────────────────────────────────────────────────
// 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;
}
}