[WL] 전투 타격감 3건 — 던전 풀 자글자글(지형 그늘 최소 밝기 = 풀잎) · 몬스터 경직(피격 모션 0.25 s · 공격 끊기/막기) · 흰색 하이라이트(피격 점멸 20 % 덮기 0.07 s + 끊긴 공격의 예고 흰 섬광 즉시 종료)
- 던전 풀: 814t 대비 복제가 지형 _MinimumDarkness 만 0.12 로 낮춰 풀잎(0.20)이 그늘마다 밝은 점으로 도드라짐 → 지형을 풀잎 값으로(dungeonTerrainMatchGrassDarkness) - 경직: 피격 모션 재생(일반 몹은 공격 취소 · 엘리트 설정 · 보스/스킬/사망 제외) → 0.1 s 뒤 굳힘 → 경직 끝까지 공격 쿨타임 붙잡기 · 일반 0.25 / 엘리트 0.18 s · 끊긴 몹은 풀리면 애니 속도 1 - 피격 점멸: 켜짐 0.07(상한 0.09) · 쉼 0.2 s · 덧그리기 패스 _HitFlash 0.2(본체가 비치는 흰색 20 %) - 하얗게 남던 주원인(실측) = 공격 예고(Telegraph) 흰 섬광이 경직·넉백으로 끊긴 공격에 최대 2.5 s 남음 → CurAnim 이 공격 밖이면 즉시 종료 + 예정 히트 +0.3 s 안전장치 같은 세션 A/B(던전 1 · 3판씩): 끊긴 공격에 남은 흰색 47/80 측정 → 0/41 - 원본 0줄(Assets/Script · FarmingIsland 무수정) Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
a0634ad735
commit
1371982fec
|
|
@ -58,6 +58,8 @@ namespace WL.Combat.Reaction
|
|||
static bool s_dissolveResolved, s_dissolveHasHdr, s_dissolveHasRange;
|
||||
static int s_edgeHdrId, s_edgeRangeId;
|
||||
static readonly List<Renderer> s_tmpRenderers = new List<Renderer>(16);
|
||||
static readonly int kHitFlashId = Shader.PropertyToID("_HitFlash"), kHitFlashColorId = Shader.PropertyToID("_HitFlashColor");
|
||||
public static int OverlayBlinks, LegacyBlinks;
|
||||
|
||||
// ── 진단(프로브가 읽는다)
|
||||
public static bool Subscribed;
|
||||
|
|
@ -101,6 +103,9 @@ namespace WL.Combat.Reaction
|
|||
|
||||
var s = GetState(victim, st);
|
||||
if (s == null) return;
|
||||
// 2026-09-17 — 덮기 방식은 덧그리기 패스가 붙어 있어야 한다. 막 나온 몹(등급 검색 1.5 s 전)이면 지금 붙인다.
|
||||
if (st.overlayMode && s.renderers.Length > 0 && !WL.Look.Character.WLCharacterBacklight.HasOverlay(s.renderers[0]))
|
||||
WL.Look.Character.WLCharacterBacklight.ApplyTier2(victim.gameObject);
|
||||
// WL #811gh — 임팩트 티어별 점멸 색/시간 배수. 티어 시스템이 꺼져 있으면 (1, 1) = 기존 동작 그대로.
|
||||
float colorMul, timeMul;
|
||||
ImpactTier.GetBlinkModifiers(victim, out colorMul, out timeMul);
|
||||
|
|
@ -255,15 +260,28 @@ namespace WL.Combat.Reaction
|
|||
}
|
||||
if (s.blinkOn)
|
||||
{
|
||||
var res = s.resolved[i];
|
||||
if (res.hasColor) mpb.SetColor(res.colorId, ScaleRgb(st.blinkColor, s.blinkColorMul));
|
||||
if (st.useEmission && res.hasEmission) mpb.SetColor(res.emissionId, ScaleRgb(st.blinkEmission, s.blinkColorMul));
|
||||
if (st.overlayMode && WL.Look.Character.WLCharacterBacklight.HasOverlay(r))
|
||||
{
|
||||
// 흰색을 overlayOpacity 만큼만 덮는다(본체가 비친다) — 임팩트 티어 배수는 1.5 배까지만
|
||||
mpb.SetFloat(kHitFlashId, Mathf.Clamp01(st.overlayOpacity * Mathf.Min(s.blinkColorMul, 1.5f)));
|
||||
mpb.SetColor(kHitFlashColorId, st.overlayColor);
|
||||
if (i == 0) OverlayBlinks++;
|
||||
}
|
||||
else
|
||||
{
|
||||
var res = s.resolved[i];
|
||||
if (res.hasColor) mpb.SetColor(res.colorId, ScaleRgb(st.blinkColor, s.blinkColorMul));
|
||||
if (st.useEmission && res.hasEmission) mpb.SetColor(res.emissionId, ScaleRgb(st.blinkEmission, s.blinkColorMul));
|
||||
if (i == 0) LegacyBlinks++;
|
||||
}
|
||||
}
|
||||
if (s.tint)
|
||||
{
|
||||
if (s_dissolveHasHdr && s.tintHdr >= 0f) mpb.SetFloat(s_edgeHdrId, s.tintHdr);
|
||||
if (s_dissolveHasRange && s.tintRange >= 0f) mpb.SetFloat(s_edgeRangeId, s.tintRange);
|
||||
}
|
||||
// 2등급 본체 그늘 색은 이 블록에 같이 쓴다(블록 소유자 = 이 클래스 하나 · 따로 쓰면 맞을 때마다 지워진다)
|
||||
WL.Look.Character.WLCharacterBacklight.WriteTier2Props(r, mpb);
|
||||
// 빈 블록을 써도 HasPropertyBlock 은 true 로 남는다(2026-09-09 Title Play 실측) → 오버라이드가 없으면 null 로 블록을 떼어 SRP Batcher 로 복귀
|
||||
if (mpb.isEmpty) r.SetPropertyBlock(null); else r.SetPropertyBlock(mpb);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,7 +7,9 @@
|
|||
// ■ 무엇을 하나 (HitConfirmed / Killed / Spawned 구독 · Assets/Script 무수정)
|
||||
// HitConfirmed(몹 · 피해 > 0 · 이 타격으로 죽지 않음)
|
||||
// → 그 몹의 NavMeshAgent.speed 를 0 으로 잠그고 velocity 를 지우고 Animator.speed 를 0 으로 세운다.
|
||||
// 등급별 시간(일반 0.18 · 엘리트 0.12 · 보스 0.08 s · WLHitFeelSettings)이 지나면 원래 값으로 되돌린다.
|
||||
// 등급별 시간(일반 0.25 · 엘리트 0.18 · 보스 0.08 s · WLHitFeelSettings)이 지나면 원래 값으로 되돌린다.
|
||||
// → 2026-09-17 PD 「히트모션 상태로 0.2~0.3초 멈추었다가 공격」: 피격 모션을 틀고(일반 몹은 공격도 끊음) 0.1 s 뒤 그 자세로 굳히며,
|
||||
// 경직이 끝날 때까지 원본 공격 쿨타임을 붙잡아 새 공격을 막는다. 끊긴 공격의 예고는 Telegraph 가 바로 끝낸다(endOnAttackInterrupted).
|
||||
// → 다시 맞으면 시간이 갱신된다(연타 중에는 계속 붙잡혀 있다 = 히트 락).
|
||||
// Killed / Spawned(풀 재사용) → 즉시 원복하고 상태를 놓는다.
|
||||
//
|
||||
|
|
@ -49,8 +51,15 @@ namespace WL.Combat.Reaction
|
|||
public float savedSpeed, savedAnimSpeed;
|
||||
public bool speedWritten, animWritten;
|
||||
public int activeIndex = -1;
|
||||
// 2026-09-17 — 피격 모션 · 자세 굳히기 · 공격 막기
|
||||
public float freezeAt, lastHitAnimAt = -99f;
|
||||
public bool hitAnimPlayed;
|
||||
}
|
||||
|
||||
static System.Reflection.FieldInfo s_coolField;
|
||||
static bool s_coolFieldLooked;
|
||||
static int s_hitStateHash;
|
||||
|
||||
static readonly Dictionary<Actor, State> s_states = new Dictionary<Actor, State>(64);
|
||||
static State[] s_active = new State[32];
|
||||
static int s_activeCount;
|
||||
|
|
@ -58,6 +67,7 @@ namespace WL.Combat.Reaction
|
|||
// ── 진단(프로브가 읽는다)
|
||||
public static bool Subscribed;
|
||||
public static int StaggerCount, RefreshCount, ReleasedByCcCount, HoldCount, SkippedFilter, SkippedNoAgent, EndedCount;
|
||||
public static int HitAnimPlays, AttackInterrupts, AttackBlocks, AttacksDuringStagger;
|
||||
public static float LastDuration;
|
||||
public static int ActiveCount { get { return s_activeCount; } }
|
||||
public static int StateCount { get { return s_states.Count; } }
|
||||
|
|
@ -68,9 +78,17 @@ namespace WL.Combat.Reaction
|
|||
CombatEvents.HitConfirmed.Add(OnHitConfirmed);
|
||||
CombatEvents.Killed.Add(OnKilled);
|
||||
CombatEvents.Spawned.Add(OnSpawned);
|
||||
CombatEvents.AttackStarted.Add(OnAttackStarted);
|
||||
Subscribed = true;
|
||||
}
|
||||
|
||||
/// <summary>진단 — 경직 중인 몹이 공격을 시작했는가(0 이어야 한다).</summary>
|
||||
static void OnAttackStarted(in AttackStartedEvent e)
|
||||
{
|
||||
State s;
|
||||
if (e.actor != null && !e.isMainPC && s_states.TryGetValue(e.actor, out s) && s.activeIndex >= 0) AttacksDuringStagger++;
|
||||
}
|
||||
|
||||
static WLHitFeelSettings St { get { return WL.Feel.WLHitFeel.Settings; } }
|
||||
|
||||
/// <summary>설정 에셋이 없거나 전체 스위치·경직 스위치가 꺼져 있으면 이 시스템은 통째로 비활성.</summary>
|
||||
|
|
@ -104,10 +122,45 @@ namespace WL.Combat.Reaction
|
|||
LastDuration = dur;
|
||||
|
||||
WLReactionRunner.Ensure();
|
||||
Freeze(s);
|
||||
PlayHitMotion(s, st, victim, now);
|
||||
Freeze(s, now);
|
||||
Activate(s);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 🔴 2026-09-17 PD 「히트모션 상태로 멈추었다가 공격을 시도」 — 피격 모션을 튼다. 원본 `MobActor.Play_Hit` 는 공격 중이면 그냥 돌아가므로
|
||||
/// (공격이 끊기지 않는다) 일반 몹은 공개 API(`Set_CurAnim`·`AnimationPlay`)로 직접 피격 상태로 넘긴다 = 공격 취소. 엘리트는 설정, 보스·스킬·사망은 끊지 않는다.
|
||||
/// </summary>
|
||||
static void PlayHitMotion(State s, WLHitFeelSettings st, Actor victim, float now)
|
||||
{
|
||||
s.hitAnimPlayed = false;
|
||||
var an = s.anim;
|
||||
if (an == null) { s.freezeAt = now; return; }
|
||||
var cur = victim.Get_CurAnim();
|
||||
bool attacking = cur >= eAnim.Attack && cur <= eAnim.Attack4;
|
||||
bool boss = victim.IsSubRole(eSubRol.Boss), elite = victim.IsSubRole(eSubRol.Elite);
|
||||
bool can = cur != eAnim.Die && cur != eAnim.Skill && !boss;
|
||||
if (can && attacking) can = elite ? st.mobStaggerInterruptElite : st.mobStaggerInterruptAttack;
|
||||
if (!can) { s.freezeAt = now; return; } // 끊지 않는 대상 = 종전처럼 그 자세로 바로 멈춘다(히트스톱)
|
||||
|
||||
if (s_hitStateHash == 0) s_hitStateHash = Animator.StringToHash(MyValue.Get_AnimName(eAnim.Hit));
|
||||
if (!an.HasState(0, s_hitStateHash)) { s.freezeAt = now; return; } // 피격 상태가 없는 애니메이터
|
||||
|
||||
if (now - s.lastHitAnimAt >= st.mobStaggerReplayInterval)
|
||||
{
|
||||
// 피격 모션은 원본 Play_Hit 처럼 속도 1 로 튼다 → 경직이 풀리면 1 로 돌아간다(끊긴 공격·예고가 늦춰 둔 속도를 이어받지 않는다)
|
||||
s.savedAnimSpeed = 1f;
|
||||
an.speed = 1f;
|
||||
victim.Set_CurAnim(eAnim.Hit);
|
||||
victim.AnimationPlay(MyValue.Get_AnimName(eAnim.Hit));
|
||||
s.lastHitAnimAt = now;
|
||||
HitAnimPlays++;
|
||||
if (attacking) AttackInterrupts++;
|
||||
s.freezeAt = st.mobStaggerFreezeDelay >= 0f ? now + st.mobStaggerFreezeDelay : float.MaxValue;
|
||||
}
|
||||
s.hitAnimPlayed = true;
|
||||
}
|
||||
|
||||
static void OnKilled(in KilledEvent e) { Release(e.victim); }
|
||||
|
||||
static void OnSpawned(in SpawnedEvent e) { Release(e.actor); }
|
||||
|
|
@ -138,8 +191,8 @@ namespace WL.Combat.Reaction
|
|||
return s;
|
||||
}
|
||||
|
||||
/// <summary>speed 0 · velocity 0 · Animator.speed 0. 원래 값은 처음 얼릴 때 1회만 기억한다.</summary>
|
||||
static void Freeze(State s)
|
||||
/// <summary>speed 0 · velocity 0 · (freezeAt 이후) Animator.speed 0 · 공격 쿨타임 붙잡기. 원래 값은 처음 얼릴 때 1회만 기억한다.</summary>
|
||||
static void Freeze(State s, float now)
|
||||
{
|
||||
var a = s.agent;
|
||||
if (a != null)
|
||||
|
|
@ -150,11 +203,29 @@ namespace WL.Combat.Reaction
|
|||
if (a.enabled && a.isOnNavMesh) a.velocity = Vector3.zero;
|
||||
}
|
||||
var an = s.anim;
|
||||
if (an != null)
|
||||
if (an != null && now >= s.freezeAt)
|
||||
{
|
||||
if (!s.animWritten) { s.savedAnimSpeed = an.speed; s.animWritten = true; }
|
||||
if (!s.animWritten) { if (!s.hitAnimPlayed) s.savedAnimSpeed = an.speed; s.animWritten = true; }
|
||||
an.speed = 0f;
|
||||
}
|
||||
BlockAttack(s, now);
|
||||
}
|
||||
|
||||
/// <summary>경직이 끝날 때까지 새 공격을 못 시작하게 원본의 공격 쿨타임(`Actor.m_attackCoolTime` · protected double)을 붙잡는다(리플렉션 읽기/쓰기 · 원본 코드 0줄).</summary>
|
||||
static void BlockAttack(State s, float now)
|
||||
{
|
||||
var st = St;
|
||||
if (st == null || !st.mobStaggerBlockAttack || s.actor == null) return;
|
||||
if (!s_coolFieldLooked)
|
||||
{
|
||||
s_coolFieldLooked = true;
|
||||
s_coolField = typeof(Actor).GetField("m_attackCoolTime", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Public);
|
||||
}
|
||||
if (s_coolField == null) return;
|
||||
double remain = (s.until - now) + 0.05;
|
||||
if (remain <= 0.0) return;
|
||||
double cur = (double)s_coolField.GetValue(s.actor);
|
||||
if (cur < remain) { s_coolField.SetValue(s.actor, remain); AttackBlocks++; }
|
||||
}
|
||||
|
||||
/// <summary>우리가 쓴 0 이 그대로 남아 있을 때만 원래 값으로 되돌린다(버프·사망·리스폰이 바꿨으면 손대지 않는다).</summary>
|
||||
|
|
@ -169,7 +240,7 @@ namespace WL.Combat.Reaction
|
|||
if (s.animWritten)
|
||||
{
|
||||
var an = s.anim;
|
||||
if (an != null && an.speed == 0f) an.speed = s.savedAnimSpeed;
|
||||
if (an != null && an.speed == 0f) an.speed = s.savedAnimSpeed > 0f ? s.savedAnimSpeed : 1f; // 0 을 기억했다면(다른 정지 중에 맞음) 1 로
|
||||
s.animWritten = false;
|
||||
}
|
||||
}
|
||||
|
|
@ -225,7 +296,7 @@ namespace WL.Combat.Reaction
|
|||
Restore(s); Deactivate(s); EndedCount++; continue;
|
||||
}
|
||||
|
||||
Freeze(s); // AI 상태 전이가 speed 를 되돌려도 매 틱 다시 0 으로 잠근다
|
||||
Freeze(s, now); // AI 상태 전이가 speed 를 되돌려도 매 틱 다시 0 으로 잠근다
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -255,6 +326,7 @@ namespace WL.Combat.Reaction
|
|||
public static void ResetDiagnostics()
|
||||
{
|
||||
StaggerCount = RefreshCount = ReleasedByCcCount = HoldCount = SkippedFilter = SkippedNoAgent = EndedCount = 0;
|
||||
HitAnimPlays = AttackInterrupts = AttackBlocks = AttacksDuringStagger = 0;
|
||||
LastDuration = 0f;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -83,15 +83,25 @@ namespace WL.Combat.Reaction
|
|||
[ColorUsage(true, true)] public Color blinkEmission = new Color(0.9f, 0.9f, 0.9f, 1f);
|
||||
|
||||
[Tooltip("켜져 있는 시간(초 · unscaled). Feel 프리셋 Blink On 0.03.")]
|
||||
public float blinkOnSeconds = 0.03f;
|
||||
public float blinkOnSeconds = 0.07f;
|
||||
|
||||
[Tooltip("한 번 켜진 점멸이 이어질 수 있는 **총 상한**(초 · unscaled · 0 이하면 상한 없음).\n" +
|
||||
"PD #816zb — 몹이 계속 하얗게 보이던 원인은 ① 임팩트 티어 시간 배수(최대 2.4 → 0.072 s)와\n" +
|
||||
"② 점멸 중 추가 타격마다 blinkOffAt 을 다시 미루는 연장 경로였다. 연장까지 포함해 첫 점멸 시작 시각 + 이 값에서 끊는다. 0.06 = 발주 규정값.")]
|
||||
public float blinkMaxSeconds = 0.06f;
|
||||
public float blinkMaxSeconds = 0.09f;
|
||||
|
||||
[Tooltip("꺼진 뒤 다시 켤 수 있기까지의 최소 간격(초 · unscaled). Feel 프리셋 Blink Off 0.03 — 같은 프레임 다중 히트(스윙 1회 5~7타)를 1회 점멸로 묶는다.")]
|
||||
public float blinkOffSeconds = 0.03f;
|
||||
public float blinkOffSeconds = 0.2f;
|
||||
|
||||
// ── 2026-09-17 PD 「히트된 몬스터가 하얗게 하이라이트된 상태로 유지되는 경우가 많다 → 피격 시점에 짧게(0.1초 미만) · 완전히 흰색으로 덮지 말고 투명도 20 % 정도」
|
||||
// 원인(값 계산): 켜짐 최대 0.06 s / 쉼 0.03 s 라 연속 피격 중엔 최대 66 % 시간이 흰색 + 알베도 HDR 3배 = 완전 흰색.
|
||||
// 🔴 하얗게 **오래** 남던 주원인은 이 점멸이 아니라 공격 예고의 흰 섬광이 끊긴 공격에 남은 것(실측 · `WLTelegraphSettings.endOnAttackInterrupted`).
|
||||
// → 켜짐 0.07(상한 0.09) · 쉼 0.2 s(= 연타 중에도 또렷이 끊어지는 점멸) · 그리기는 캐릭터 덧그리기 패스(`WL/CharBacklight`)의 `_HitFlash` 로 **흰색을 20 % 만 덮는다**.
|
||||
[Tooltip("true = 덧그리기 패스가 붙은 렌더러는 흰색을 overlayOpacity 만큼만 덮는다(본체 색이 비친다). false 또는 패스가 없으면 예전 방식(알베도 HDR = 완전 흰색).")]
|
||||
public bool overlayMode = true;
|
||||
[Tooltip("덮는 정도(0~1). PD 2026-09-17 = 0.2.")]
|
||||
[Range(0f, 1f)] public float overlayOpacity = 0.2f;
|
||||
public Color overlayColor = Color.white;
|
||||
|
||||
[Tooltip("알베도/베이스 색 프로퍼티 후보(앞에서부터 셰이더에 있는 첫 것을 쓴다 · Shader.FindPropertyIndex 실측). 없으면 그 렌더러는 건너뜀.")]
|
||||
public string[] colorProperties = new string[] { "_AlbedoColor", "_BaseColor" };
|
||||
|
|
|
|||
|
|
@ -20,9 +20,12 @@ MonoBehaviour:
|
|||
blinkColor: {r: 3, g: 3, b: 3, a: 1}
|
||||
useEmission: 1
|
||||
blinkEmission: {r: 0.9, g: 0.9, b: 0.9, a: 1}
|
||||
blinkOnSeconds: 0.03
|
||||
blinkMaxSeconds: 0.06
|
||||
blinkOffSeconds: 0.03
|
||||
blinkOnSeconds: 0.07
|
||||
blinkMaxSeconds: 0.09
|
||||
blinkOffSeconds: 0.2
|
||||
overlayMode: 1
|
||||
overlayOpacity: 0.2
|
||||
overlayColor: {r: 1, g: 1, b: 1, a: 1}
|
||||
colorProperties:
|
||||
- _AlbedoColor
|
||||
- _BaseColor
|
||||
|
|
|
|||
|
|
@ -27,6 +27,8 @@ MonoBehaviour:
|
|||
defaultHitSeconds: 0.16
|
||||
correctFromLiveClip: 1
|
||||
maxTelegraphSeconds: 2.5
|
||||
endOnAttackInterrupted: 1
|
||||
lateHitGraceSeconds: 0.3
|
||||
decalEnabled: 0
|
||||
maxConcurrent: 6
|
||||
poolWarmCount: 8
|
||||
|
|
|
|||
|
|
@ -78,7 +78,7 @@ namespace WL.Combat.Telegraph
|
|||
public static bool Subscribed;
|
||||
public static int StartCount, EndCount, SkippedBoss, SkippedNotMob, SkippedDisabled, SkippedNoTable, TimeoutCount,
|
||||
SpeedApplied, SpeedClamped, SpeedRestored, TintApplied, TintRestoredElite, PulseApplied, PulseRestored,
|
||||
FlashCount, SfxPlayed, SfxThrottled, SfxNoManager, DecalOn, DecalSkipped, HitTimeFallback, HitTimeCorrected;
|
||||
FlashCount, SfxPlayed, SfxThrottled, SfxNoManager, DecalOn, DecalSkipped, HitTimeFallback, HitTimeCorrected, InterruptedCount, LateCount;
|
||||
public static float LastPlannedSeconds = -1f, LastHitSeconds = -1f, LastOrigSpeed = -1f, LastAppliedSpeed = -1f, LastTargetSeconds = -1f;
|
||||
public static bool LastWasRanged, LastWasClamped;
|
||||
public static string LastInfo = "";
|
||||
|
|
@ -265,9 +265,21 @@ namespace WL.Combat.Telegraph
|
|||
if (elapsed > Mathf.Max(0.1f, st.maxTelegraphSeconds) || !s.actor.gameObject.activeInHierarchy)
|
||||
{ TimeoutCount++; EndInternal(s, false, true); continue; }
|
||||
|
||||
// 2026-09-17 — 공격이 끊겼으면(원본 CurAnim 이 공격 밖) 기다리지 않고 바로 끝낸다. 종전엔 흰 섬광이 위 시간(2.5 s)까지 남았다.
|
||||
if (st.endOnAttackInterrupted)
|
||||
{
|
||||
var cur = s.actor.Get_CurAnim();
|
||||
if (cur < eAnim.Attack || cur > eAnim.Attack4) { InterruptedCount++; EndInternal(s, false, true); continue; }
|
||||
}
|
||||
|
||||
if (!s.corrected) CorrectFromLiveClip(s, st);
|
||||
|
||||
float planned = Mathf.Max(0.0001f, s.plannedSeconds);
|
||||
|
||||
// 2026-09-17 — 예정 히트 시각이 지나고도 히트가 안 오면(CurAnim 이 늦게 바뀌는 경우) 흰 섬광을 오래 두지 않는다
|
||||
if (st.lateHitGraceSeconds > 0f && elapsed > planned + st.lateHitGraceSeconds)
|
||||
{ LateCount++; EndInternal(s, false, true); continue; }
|
||||
|
||||
float t = Mathf.Clamp01(elapsed / planned);
|
||||
|
||||
if (s.decalIdx >= 0) { TelegraphDecal.Follow(s.decalIdx, s.actor); TelegraphDecal.SetFill(s.decalIdx, t); }
|
||||
|
|
@ -594,7 +606,7 @@ namespace WL.Combat.Telegraph
|
|||
{
|
||||
StartCount = EndCount = SkippedBoss = SkippedNotMob = SkippedDisabled = SkippedNoTable = TimeoutCount = 0;
|
||||
SpeedApplied = SpeedClamped = SpeedRestored = TintApplied = TintRestoredElite = PulseApplied = PulseRestored = 0;
|
||||
FlashCount = SfxPlayed = SfxThrottled = SfxNoManager = DecalOn = DecalSkipped = HitTimeFallback = HitTimeCorrected = 0;
|
||||
FlashCount = SfxPlayed = SfxThrottled = SfxNoManager = DecalOn = DecalSkipped = HitTimeFallback = HitTimeCorrected = InterruptedCount = LateCount = 0;
|
||||
LastPlannedSeconds = LastHitSeconds = LastOrigSpeed = LastAppliedSpeed = LastTargetSeconds = -1f;
|
||||
LastWasRanged = LastWasClamped = false;
|
||||
LastInfo = "";
|
||||
|
|
|
|||
|
|
@ -108,6 +108,14 @@ namespace WL.Combat.Telegraph
|
|||
[Tooltip("HitboxSpawned 가 이 시간 안에 안 오면 강제로 원복한다(피격·사망으로 공격이 끊긴 경우 · 초).")]
|
||||
public float maxTelegraphSeconds = 2.5f;
|
||||
|
||||
// 2026-09-17 PD 「히트 된 몬스터가 하얗게 하이라이트 된 상태로 유지되는 경우가 많다」 실측 원인 = 끊긴 공격의 예고가 위 시간까지 남아
|
||||
// 흰 섬광(flashAlbedo 2.2 · flashEmission 1.6)이 최대 2.5 s 유지됐다(경직이 공격을 끊으면서 더 잦아짐).
|
||||
[Tooltip("true = 몹이 더 이상 공격 모션이 아니면(피격 모션·넉백·CC·타깃 상실 = 원본 CurAnim 이 Attack~Attack4 밖) 그 즉시 예고를 끝낸다(색·섬광·속도·크기 원복).\nfalse = 종전처럼 maxTelegraphSeconds 까지 기다린다.")]
|
||||
public bool endOnAttackInterrupted = true;
|
||||
|
||||
[Tooltip("예정 히트 시각이 이만큼(초) 지나도 히트가 안 오면 예고를 끝낸다(원본 CurAnim 이 늦게 바뀌는 경우의 안전장치 · 흰 섬광이 남는 시간의 상한).\n0 이하 = 끄기(maxTelegraphSeconds 까지 기다림).")]
|
||||
public float lateHitGraceSeconds = 0.3f;
|
||||
|
||||
// ─────────────────────────────────────────── ① 지면 데칼
|
||||
[Header("① 지면 데칼 (투사체 BoxCollider 를 읽어 같은 크기 · 런타임 쿼드 2장 · 풀링)")]
|
||||
[Tooltip("끄면 판을 그리지 않는다.")]
|
||||
|
|
|
|||
|
|
@ -38,7 +38,12 @@ MonoBehaviour:
|
|||
flashDuration: 0.06
|
||||
hapticEnabled: 0
|
||||
mobStaggerEnabled: 1
|
||||
mobStaggerSeconds: 0.18
|
||||
mobStaggerSecondsElite: 0.12
|
||||
mobStaggerSeconds: 0.25
|
||||
mobStaggerSecondsElite: 0.18
|
||||
mobStaggerSecondsBoss: 0.08
|
||||
mobStaggerInterruptAttack: 1
|
||||
mobStaggerInterruptElite: 0
|
||||
mobStaggerBlockAttack: 1
|
||||
mobStaggerFreezeDelay: 0.1
|
||||
mobStaggerReplayInterval: 0.2
|
||||
logHits: 0
|
||||
|
|
|
|||
|
|
@ -151,14 +151,27 @@ public class WLHitFeelSettings : ScriptableObject
|
|||
|
||||
[Tooltip("일반 몹 경직 길이(초 · unscaled). 콤보 간격 0.42~0.47 s 의 절반 이하로 둔다 — 0.18 = 발주 규정값.\n" +
|
||||
"넉백 중에 맞으면 넉백이 끝난 시점부터 이 길이를 1회 다시 준다(밀리다 말고 곧장 걷지 않게).")]
|
||||
public float mobStaggerSeconds = 0.18f;
|
||||
public float mobStaggerSeconds = 0.25f;
|
||||
|
||||
[Tooltip("엘리트 경직 길이(초). 일반보다 짧게 — 압박감을 유지한다.")]
|
||||
public float mobStaggerSecondsElite = 0.12f;
|
||||
public float mobStaggerSecondsElite = 0.18f;
|
||||
|
||||
[Tooltip("보스 경직 길이(초). 가장 짧게 — 보스 패턴이 끊기면 안 된다.")]
|
||||
public float mobStaggerSecondsBoss = 0.08f;
|
||||
|
||||
// ── 2026-09-17 PD 「몬스터가 피격 후 경직되지 않아 타격감이 부족 → 밀린 상태에서 경직 시간을 주어 히트모션 상태로 0.2~0.3초 멈추었다가 공격을 시도」
|
||||
// 종전 = 이동 속도 0 + 애니메이터 정지(0.18 s)뿐이라 ① 공격 중인 몹은 공격 모션 그대로 얼었다 풀려 그대로 때렸고 ② 새 공격도 경직 중에 시작됐다.
|
||||
[Tooltip("true = 공격 중인 **일반** 몹도 피격 모션으로 끊는다(공격 취소 → 경직 뒤 다시 시도). 원본 Play_Hit 는 공격 중이면 피격 모션을 안 튼다.")]
|
||||
public bool mobStaggerInterruptAttack = true;
|
||||
[Tooltip("true = 엘리트도 공격을 끊는다. 기본 false(압박감 유지 · 엘리트는 모션만 잠깐 멈춘다). 보스는 항상 끊지 않는다.")]
|
||||
public bool mobStaggerInterruptElite = false;
|
||||
[Tooltip("true = 경직 중에는 새 공격을 시작하지 못하게 공격 쿨타임을 경직이 끝날 때까지 붙잡아 둔다(원본 필드 읽기/쓰기만 · 원본 코드 0줄).")]
|
||||
public bool mobStaggerBlockAttack = true;
|
||||
[Tooltip("피격 모션을 이만큼(초) 재생한 뒤 그 자세로 멈춘다(움찔한 자세로 굳는다). 음수 = 멈추지 않고 모션을 끝까지 재생.")]
|
||||
public float mobStaggerFreezeDelay = 0.1f;
|
||||
[Tooltip("연타 중 피격 모션을 다시 트는 최소 간격(초). 너무 짧으면 첫 프레임만 반복돼 떨려 보인다.")]
|
||||
public float mobStaggerReplayInterval = 0.2f;
|
||||
|
||||
// ─────────────────────────────────────────── 진단
|
||||
|
||||
[Header("진단 (검증용 · 배포 시 false)")]
|
||||
|
|
|
|||
|
|
@ -54,6 +54,10 @@ Shader "WL/CharBacklight"
|
|||
_FaceStrength ("Face Light Strength (0 = 끔)", Float) = 0.3
|
||||
_FaceWrap ("Face Light Wrap (0 = 또렷 · 1 = 둥글게 감싸는 빛)", Range(0, 1)) = 0.5
|
||||
_FillStrength ("Body Fill Strength (위치 없는 채움빛 · 2등급용)", Float) = 0
|
||||
|
||||
[Header(Hit flash overlay)]
|
||||
_HitFlash ("Hit Flash Opacity (렌더러별 프로퍼티 블록이 쓴다 · 0 = 없음)", Range(0, 1)) = 0
|
||||
_HitFlashColor ("Hit Flash Color", Color) = (1, 1, 1, 1)
|
||||
}
|
||||
|
||||
SubShader
|
||||
|
|
@ -107,6 +111,8 @@ Shader "WL/CharBacklight"
|
|||
float _FaceStrength;
|
||||
float _FaceWrap;
|
||||
float _FillStrength;
|
||||
float _HitFlash;
|
||||
float4 _HitFlashColor;
|
||||
CBUFFER_END
|
||||
|
||||
// xyz = 표면에서 가상 역광 쪽을 가리키는 월드 방향(정규화) · w = 1 이면 유효(0 = 아직 안 들어옴 → 위쪽)
|
||||
|
|
@ -206,7 +212,12 @@ Shader "WL/CharBacklight"
|
|||
}
|
||||
|
||||
float3 col = _RimColor.rgb * (rim * _RimStrength) + _WrapColor.rgb * wrap + _SssColor.rgb * sss + _FaceColor.rgb * face;
|
||||
return half4(col, rim * saturate(_RimOpacity));
|
||||
float a = rim * saturate(_RimOpacity);
|
||||
// ⑤ 피격 하이라이트 — 본체 위에 흰색을 _HitFlash 만큼만 덮는다(완전히 하얗게 되지 않는다 · 2026-09-17 PD 「투명도 20 % 정도」)
|
||||
float hf = saturate(_HitFlash);
|
||||
col = col * (1.0 - hf) + _HitFlashColor.rgb * hf;
|
||||
a = 1.0 - (1.0 - a) * (1.0 - hf);
|
||||
return half4(col, a);
|
||||
}
|
||||
ENDHLSL
|
||||
}
|
||||
|
|
|
|||
|
|
@ -108,6 +108,7 @@ namespace WL.Look.Character
|
|||
r.sharedMaterials = s_tmp.ToArray();
|
||||
BrightenBase(null, r, s_tmp.Count > 0 ? s_tmp[0] : null, false); // 우리가 붙였던 렌더러만 본체 값도 되돌린다
|
||||
RestoreMkTint(r, s_tmp.Count > 0 ? s_tmp[0] : null);
|
||||
s_overlayRends.Remove(r);
|
||||
}
|
||||
}
|
||||
LastLog = "역광 패스 제거 @" + root.name;
|
||||
|
|
@ -164,6 +165,29 @@ namespace WL.Look.Character
|
|||
// ─────────────────────────────────────────────────────────────────
|
||||
public static int Tier2Roots, Tier2Renderers, Tier3Renderers, Tier3Materials;
|
||||
static readonly int kGoochDark = Shader.PropertyToID("_GoochDarkColor");
|
||||
static readonly HashSet<Renderer> s_overlayRends = new HashSet<Renderer>();
|
||||
|
||||
/// <summary>이 렌더러에 우리 덧그리기 패스가 붙어 있는가(피격 하이라이트가 덮기 방식으로 그릴 수 있는가).</summary>
|
||||
public static bool HasOverlay(Renderer r) { return r != null && s_overlayRends.Contains(r); }
|
||||
|
||||
/// <summary>
|
||||
/// 몹 피격 점멸(`MobHitFlash`)이 렌더러의 프로퍼티 블록을 통째로 다시 쓸 때 불러, 2등급 본체 그늘 색을 **같은 블록**에 같이 써 넣는다
|
||||
/// (따로 쓰면 맞을 때마다 지워져 그늘 색이 깜빡인다 · 블록의 소유자는 MobHitFlash 하나).
|
||||
/// </summary>
|
||||
public static void WriteTier2Props(Renderer r, MaterialPropertyBlock mpb)
|
||||
{
|
||||
if (r == null || mpb == null || !s_overlayRends.Contains(r)) return;
|
||||
var cfg = WLCharacterBacklightSettings.Instance;
|
||||
if (cfg == null || cfg.enabled_ == 0 || cfg.tiersEnabled == 0) return;
|
||||
var baseMat = r.sharedMaterial;
|
||||
if (baseMat == null) return;
|
||||
if (baseMat.HasProperty(kGoochDark)) mpb.SetColor(kGoochDark, cfg.tier2MkShadowColor);
|
||||
if (baseMat.HasProperty(kMinDark) && baseMat.HasProperty(kShadowCol))
|
||||
{
|
||||
mpb.SetFloat(kMinDark, Mathf.Clamp01(baseMat.GetFloat(kMinDark) + cfg.tier2MinimumDarknessAdd));
|
||||
mpb.SetColor(kShadowCol, Color.Lerp(baseMat.GetColor(kShadowCol), cfg.tier2ShadowColor, cfg.tier2ShadowBlend));
|
||||
}
|
||||
}
|
||||
|
||||
public static void ApplyTier2(GameObject root)
|
||||
{
|
||||
|
|
@ -197,8 +221,10 @@ namespace WL.Look.Character
|
|||
r.sharedMaterials = s_tmp.ToArray();
|
||||
}
|
||||
TintTier2Base(cfg, r, baseMat);
|
||||
s_overlayRends.Add(r);
|
||||
n++;
|
||||
}
|
||||
if (s_overlayRends.Count > 512) s_overlayRends.RemoveWhere(x => x == null);
|
||||
Tier2Renderers += n;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -94,6 +94,7 @@ MonoBehaviour:
|
|||
shoreSprayLineBreak: 0.6
|
||||
dungeonGrassMatchIsland: 1
|
||||
dungeonLightMatchIsland: 1
|
||||
dungeonTerrainMatchGrassDarkness: 1
|
||||
shoreFoamMaterial: {fileID: 2100000, guid: d24a4c5ce16646c409631c112b57f2cf, type: 2}
|
||||
shoreFoamWidth: 1.6
|
||||
shoreFoamInner: 0.5
|
||||
|
|
|
|||
|
|
@ -31,6 +31,19 @@ namespace WL.Look.Farm
|
|||
// ── 진단(프로브가 읽는다)
|
||||
public static int Instancers, EntriesScaled, MaterialsTuned, TerrainsTuned, LightsMatched;
|
||||
public static string LastLog = "";
|
||||
public static float TerrainDarknessMatched = -1f;
|
||||
|
||||
static float GrassDarkness(DemoLayer layer, Mesh grassMesh)
|
||||
{
|
||||
if (layer == null || layer.Settings == null) return -1f;
|
||||
for (int i = 0; i < layer.Settings.Length; i++)
|
||||
{
|
||||
var e = layer.Settings[i];
|
||||
if (e == null || e.Material == null || !e.Material.HasProperty("_MinimumDarkness")) continue;
|
||||
if (grassMesh == null || e.Mesh == grassMesh) return e.Material.GetFloat("_MinimumDarkness");
|
||||
}
|
||||
return -1f;
|
||||
}
|
||||
|
||||
static readonly Dictionary<Material, Material> s_tuned = new Dictionary<Material, Material>();
|
||||
|
||||
|
|
@ -43,6 +56,7 @@ namespace WL.Look.Farm
|
|||
int insts = 0, scaled = 0, terrains = 0, lights = 0; int tunedBefore = s_tuned.Count;
|
||||
|
||||
var roots = scene.GetRootGameObjects();
|
||||
float grassMd = -1f;
|
||||
for (int r = 0; r < roots.Length; r++)
|
||||
{
|
||||
var list = roots[r].GetComponentsInChildren<DemoInstancer>(true);
|
||||
|
|
@ -57,13 +71,26 @@ namespace WL.Look.Farm
|
|||
scaled += TuneLayer(cfg, inst.FourthLayer, grassMesh, flowerMesh);
|
||||
if (was) inst.enabled = true; // OnEnable → 새 값으로 다시 심는다
|
||||
insts++;
|
||||
if (grassMd < 0f) grassMd = GrassDarkness(inst.FirstLayer, grassMesh);
|
||||
}
|
||||
}
|
||||
|
||||
// 🔴 2026-09-17 PD 「전투맵의 풀이 여전히 자글자글(섬에서 지적한 현상과 동일)」 실측(풀만 있는 구역의 밝기 기울기):
|
||||
// SSAO·후처리·그림자·풀 외곽선은 무관. 814t 대비 복제가 **지형** 그늘 최소 밝기를 0.12 로 낮춰 풀잎(0.2)보다 그늘이 어두워져
|
||||
// 구름 그늘·명암 단계마다 풀잎이 하나씩 밝은 점으로 도드라졌다(0.57). 지형을 풀잎과 같은 값으로 맞추면 0.11(섬과 같은 매끈함).
|
||||
for (int r = 0; r < roots.Length; r++)
|
||||
{
|
||||
var ts = roots[r].GetComponentsInChildren<Terrain>(true);
|
||||
for (int i = 0; i < ts.Length; i++)
|
||||
{
|
||||
var t = ts[i]; if (t == null || t.materialTemplate == null) continue;
|
||||
var m = Tuned(cfg, t.materialTemplate);
|
||||
if (cfg.dungeonTerrainMatchGrassDarkness != 0 && grassMd >= 0f && m.HasProperty("_MinimumDarkness"))
|
||||
{
|
||||
if (m == t.materialTemplate && !s_tuned.ContainsValue(m)) { var c = new Material(m) { name = m.name + " (WL dungeon=island)" }; s_tuned[m] = c; m = c; }
|
||||
m.SetFloat("_MinimumDarkness", grassMd);
|
||||
TerrainDarknessMatched = grassMd;
|
||||
}
|
||||
if (m != t.materialTemplate) { t.materialTemplate = m; terrains++; }
|
||||
}
|
||||
|
||||
|
|
@ -84,7 +111,7 @@ namespace WL.Look.Farm
|
|||
|
||||
Instancers = insts; EntriesScaled = scaled; TerrainsTuned = terrains; LightsMatched = lights; MaterialsTuned = s_tuned.Count;
|
||||
LastLog = "던전 풀밭 = 섬 값 — 인스턴서 " + insts + " · 크기 바꾼 항목 " + scaled + " · 재질 복사본 " + s_tuned.Count + "(새로 " + (s_tuned.Count - tunedBefore) + ")"
|
||||
+ " · 터레인 " + terrains + " · 태양 " + lights + " @" + scene.name;
|
||||
+ " · 터레인 " + terrains + "(그늘 최소 밝기 = 풀잎 " + TerrainDarknessMatched.ToString("F2") + ") · 태양 " + lights + " @" + scene.name;
|
||||
cfg.Log(LastLog);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -316,6 +316,8 @@ namespace WL.Look.Farm
|
|||
public int dungeonGrassMatchIsland = 1;
|
||||
[Tooltip("1 = 던전의 디렉셔널 라이트도 섬의 색·세기·그림자 진하기(`dirLight*`)로 맞춘다.")]
|
||||
public int dungeonLightMatchIsland = 1;
|
||||
[Tooltip("1 = 던전 지형 재질의 그늘 최소 밝기(_MinimumDarkness)를 풀잎 재질과 같은 값으로 맞춘다. 실측: 814t 대비 복제가 지형만 0.12 로 낮춰 풀잎(0.2)이 그늘마다 밝은 점으로 도드라졌다(자글자글).")]
|
||||
public int dungeonTerrainMatchGrassDarkness = 1;
|
||||
|
||||
// ───────────────────────────────── §816zf 부드러운 흙 데칼
|
||||
[Header("§816zf — 흙 자리를 데모처럼 「둥글게 번지게」 (PD 2026-09-15)")]
|
||||
|
|
|
|||
Loading…
Reference in New Issue