[WL] 가격판 겹침·오류 수정 · 예고 섬광 20 % · 넉백 거리 절반 · 히트 하이라이트 매 타격
- 가격판: FI Purchaser 가 활성 씬(InGame)에 생겨 섬 씬이 내려가도 남던 옛 판(파괴된 섬에 묶임)을 섬 씬 언로드 순간 제거 + 섬 준비 때 한 번 더 실측: 던전 입장 때 1개 제거 · 복귀 후 판 1개 · 겹침 0 · 오류 0 - 공격 예고 흰 섬광: 알베도 2.2/에미션 1.6(완전 흰색) → 캐릭터 덧그리기 패스 _HitFlash 20 %(flashOverlayOpacity) · 실측 완전 흰색 0 - 넉백 거리 배율(MobKnockbackScale · mobKnockbackDistanceScale 0.5): 새 넉백이 시작된 틱에 원본 남은 시간을 줄여 총 거리 절반 실측 평균 0.203 m(38회) → 0.102~0.103 m(84회) · 넉백 상태 멈춤 0(최솟값을 남기면 끊긴 넉백이 영영 안 풀리던 결함은 수정 전 실측 · 멈춘 넉백 풀기 안전장치 포함) - 히트 하이라이트: 꺼진 뒤 0.2 s 쉼 동안 다음 점멸을 막아 빠른 연타에서 첫 타만 번쩍 → 타격마다 다시 켬(retriggerEveryHit) 실측(0.15 s 간격 6타): 종전 3/6 → 6/6 · 실제 전투 화면 캡처로 1~3타 모두 표시 확인 - 원본 0줄 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
1371982fec
commit
b95f043696
|
|
@ -36,9 +36,13 @@ namespace WL.Combat.Reaction
|
|||
public MaterialPropertyBlock[] blocks;
|
||||
public Resolved[] resolved;
|
||||
public bool blinkOn, tint;
|
||||
// 2026-09-18 — 공격 예고 섬광(Telegraph)을 덮기 방식으로 그릴 때의 불투명도(0 = 없음)와 색
|
||||
public float tellFlash;
|
||||
public Color tellColor;
|
||||
public float blinkOffAt, blinkReadyAt, tintHdr, tintRange;
|
||||
// WL #816zb — 이번 점멸이 처음 켜진 시각. 연장(ExtendCount)이 blinkMaxSeconds 를 넘지 못하게 하는 기준점.
|
||||
public float blinkStartAt;
|
||||
public int blinkStartFrame = -1; // 2026-09-18 — 같은 프레임 다중 히트(스윙 1회 여러 타)는 한 번으로 묶는다
|
||||
public int activeIndex = -1;
|
||||
// WL #811gh — 임팩트 티어별 점멸 강도. 1 = 기존 동작(티어 시스템 off 이면 항상 1).
|
||||
public float blinkColorMul = 1f, blinkTimeMul = 1f;
|
||||
|
|
@ -59,7 +63,7 @@ namespace WL.Combat.Reaction
|
|||
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 int OverlayBlinks, LegacyBlinks, RetriggerCount;
|
||||
|
||||
// ── 진단(프로브가 읽는다)
|
||||
public static bool Subscribed;
|
||||
|
|
@ -110,6 +114,24 @@ namespace WL.Combat.Reaction
|
|||
float colorMul, timeMul;
|
||||
ImpactTier.GetBlinkModifiers(victim, out colorMul, out timeMul);
|
||||
float now = Time.unscaledTime;
|
||||
if (st.retriggerEveryHit)
|
||||
{
|
||||
// 🔴 2026-09-18 PD 「히트 하이라이트가 최초 공격 1회에만 발생 → 타격될 때마다」 — 실측: 매 타격 값은 들어갔지만(58/58),
|
||||
// 꺼진 뒤 blinkOffSeconds(0.2 s) 동안 다음 점멸을 막고 켜진 중엔 연장만 해서 빠른 연타·연속 피해에서는 첫 타격만 번쩍였다.
|
||||
// → 타격마다 처음부터 다시 켠다(쉼·연장 상한 없음). 같은 프레임의 여러 타는 한 번으로 묶는다.
|
||||
if (s.blinkOn && s.blinkStartFrame == Time.frameCount) { ExtendCount++; return; }
|
||||
if (s.blinkOn) RetriggerCount++;
|
||||
if (s.tint && !victim.IsDead()) s.tint = false;
|
||||
WLReactionRunner.Ensure();
|
||||
s.blinkColorMul = colorMul; s.blinkTimeMul = timeMul;
|
||||
s.blinkOn = true; s.blinkStartAt = now; s.blinkStartFrame = Time.frameCount;
|
||||
s.blinkOffAt = now + st.blinkOnSeconds * timeMul;
|
||||
Apply(s, st);
|
||||
Activate(s);
|
||||
OnCount++;
|
||||
LastInfo = "on";
|
||||
return;
|
||||
}
|
||||
// WL #816zb — 연장도 '처음 켜진 시각 + blinkMaxSeconds' 를 넘지 못한다(PD: 몹이 계속 하얗게 보이는 현상).
|
||||
if (s.blinkOn) { s.blinkOffAt = CapOffAt(st, s.blinkStartAt, now + st.blinkOnSeconds * timeMul); ExtendCount++; return; }
|
||||
if (now < s.blinkReadyAt) { ThrottledCount++; return; }
|
||||
|
|
@ -152,8 +174,8 @@ namespace WL.Combat.Reaction
|
|||
if (e.actor == null) return;
|
||||
State s;
|
||||
if (!s_states.TryGetValue(e.actor, out s)) return;
|
||||
if (!s.blinkOn && !s.tint) return;
|
||||
s.blinkOn = false; s.tint = false; s.blinkReadyAt = 0f;
|
||||
if (!s.blinkOn && !s.tint && s.tellFlash <= 0f) return;
|
||||
s.blinkOn = false; s.tint = false; s.tellFlash = 0f; s.blinkReadyAt = 0f;
|
||||
var st = St;
|
||||
// 리뷰 Minor-8ⓒ: st == null(에셋 자체가 없음)이면 Apply 를 못 부르지만, 그 상태에서는 Enabled 가 false 라 애초에 MPB 를 쓴 적이 없다
|
||||
// (블록이 있다면 RestoreAll 의 st == null 경로가 SetPropertyBlock(null) 로 뗀다) → 잔류 0. ⓑ s_states 는 씬 전환(ClearAll)까지 유지 = 풀 크기 상한.
|
||||
|
|
@ -258,13 +280,14 @@ namespace WL.Combat.Reaction
|
|||
if (eres.hasColor) mpb.SetColor(eres.colorId, s.eliteAlbedo);
|
||||
if (eres.hasEmission) mpb.SetColor(eres.emissionId, s.eliteEmission);
|
||||
}
|
||||
bool overlayHere = WL.Look.Character.WLCharacterBacklight.HasOverlay(r);
|
||||
float ov = 0f; Color ovColor = st.overlayColor;
|
||||
if (s.blinkOn)
|
||||
{
|
||||
if (st.overlayMode && WL.Look.Character.WLCharacterBacklight.HasOverlay(r))
|
||||
if (st.overlayMode && overlayHere)
|
||||
{
|
||||
// 흰색을 overlayOpacity 만큼만 덮는다(본체가 비친다) — 임팩트 티어 배수는 1.5 배까지만
|
||||
mpb.SetFloat(kHitFlashId, Mathf.Clamp01(st.overlayOpacity * Mathf.Min(s.blinkColorMul, 1.5f)));
|
||||
mpb.SetColor(kHitFlashColorId, st.overlayColor);
|
||||
ov = Mathf.Clamp01(st.overlayOpacity * Mathf.Min(s.blinkColorMul, 1.5f));
|
||||
if (i == 0) OverlayBlinks++;
|
||||
}
|
||||
else
|
||||
|
|
@ -275,6 +298,13 @@ namespace WL.Combat.Reaction
|
|||
if (i == 0) LegacyBlinks++;
|
||||
}
|
||||
}
|
||||
// 2026-09-18 PD 「예고 섬광도 투명도 20 %」 — 피격 하이라이트와 같은 칸에 더 진한 쪽을 쓴다
|
||||
if (s.tellFlash > ov && overlayHere) { ov = s.tellFlash; ovColor = s.tellColor; }
|
||||
if (ov > 0f)
|
||||
{
|
||||
mpb.SetFloat(kHitFlashId, ov);
|
||||
mpb.SetColor(kHitFlashColorId, ovColor);
|
||||
}
|
||||
if (s.tint)
|
||||
{
|
||||
if (s_dissolveHasHdr && s.tintHdr >= 0f) mpb.SetFloat(s_edgeHdrId, s.tintHdr);
|
||||
|
|
@ -351,8 +381,8 @@ namespace WL.Combat.Reaction
|
|||
foreach (var kv in s_states)
|
||||
{
|
||||
var s = kv.Value;
|
||||
if (!s.blinkOn && !s.tint && !s.elite) continue;
|
||||
s.blinkOn = false; s.tint = false; s.elite = false;
|
||||
if (!s.blinkOn && !s.tint && !s.elite && s.tellFlash <= 0f) continue;
|
||||
s.blinkOn = false; s.tint = false; s.elite = false; s.tellFlash = 0f;
|
||||
if (st != null) Apply(s, st);
|
||||
else for (int i = 0; i < s.renderers.Length; i++) if (s.renderers[i] != null) { s.blocks[i].Clear(); s.renderers[i].SetPropertyBlock(null); }
|
||||
}
|
||||
|
|
@ -388,6 +418,26 @@ namespace WL.Combat.Reaction
|
|||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 2026-09-18 PD 「예고 섬광도 투명도 20 %」 — 공격 예고의 흰 섬광을 덮기 방식(덧그리기 패스 `_HitFlash`)으로 건다. opacity 0 = 끔.
|
||||
/// 덧그리기 패스가 아직 없으면 붙이고, 그래도 없으면(룩 시스템 꺼짐) false = 호출자가 예전 방식으로 그린다.
|
||||
/// </summary>
|
||||
public static bool SetTellFlash(Actor actor, float opacity, Color color)
|
||||
{
|
||||
var st = St;
|
||||
if (actor == null || st == null) return false;
|
||||
var s = GetState(actor, st);
|
||||
if (s == null || s.renderers.Length == 0) return false;
|
||||
if (opacity > 0f && !WL.Look.Character.WLCharacterBacklight.HasOverlay(s.renderers[0]))
|
||||
{
|
||||
WL.Look.Character.WLCharacterBacklight.ApplyTier2(actor.gameObject);
|
||||
if (!WL.Look.Character.WLCharacterBacklight.HasOverlay(s.renderers[0])) return false;
|
||||
}
|
||||
s.tellFlash = Mathf.Clamp01(opacity); s.tellColor = color;
|
||||
Apply(s, st);
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>엘리트 상시 색을 뗀다(처치·정리). 점멸/디졸브 상태는 건드리지 않는다.</summary>
|
||||
public static void ClearEliteTint(Actor actor)
|
||||
{
|
||||
|
|
@ -419,6 +469,7 @@ namespace WL.Combat.Reaction
|
|||
public static void ResetDiagnostics()
|
||||
{
|
||||
OnCount = OffCount = ExtendCount = ThrottledCount = TintCount = ClearCount = SkippedFilter = SkippedNoProperty = StatesCreated = RendererFieldViaReflection = 0;
|
||||
RetriggerCount = 0;
|
||||
LastInfo = "";
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,183 @@
|
|||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// MobKnockbackScale.cs — 몹 넉백 이동 거리 배율 · 원본 넉백 코루틴 0줄 · 코루틴 0 · GC 0(추적 목록은 풀 크기 상한)
|
||||
//
|
||||
// 2026-09-18 PD 「몬스터 넉백 거리가 너무 멀어. 우선은 절반 정도로 줄이고,
|
||||
// 추후에는 캐릭터 테이블에서 공격 타수 별 넉백 거리를 조절할 수 있도록 설계해줘.」
|
||||
//
|
||||
// ■ 원본 넉백 2종(Actor.cs)
|
||||
// KnockBack(time, speed 4) → Co_KnockBack : eCC.KnockBack 의 CCTime 이 0 이 될 때까지 매 프레임 뒤로 speed × dt
|
||||
// Knockback_byHitter(pos, time, 10) → Co_Knockback_byHitter : eCC.KnockBack_byHitter 의 CCTime 동안 때린 쪽 반대로 speed × dt
|
||||
// 둘 다 「남은 시간 × 속도」만큼 움직이므로, 새 넉백이 시작된 직후 남은 시간을 줄이면 총 거리가 같은 비율로 준다.
|
||||
// ■ 검출 = 피격(HitConfirmed)된 몹만 추적 · 매 틱 CCTime 이 직전 값보다 커지면 「새 넉백 시작」.
|
||||
// 그 사이 원본이 이미 한 프레임(dt) 밀었으므로 남은 시간 = (현재 + dt) × 배율 − dt → 총 거리 = 원래 × 배율.
|
||||
// ■ 값 = WLHitFeelSettings.mobKnockbackDistanceScale(0.5) · mobKnockbackScaleByHitter · 0 이하 = 기능 끔(원본 그대로).
|
||||
// ■ 진단: 종류별 배율 적용 수 · 실제 이동 거리(넉백 직전 위치 → 끝 위치 수평 거리) 합·최근값. 원래 거리는 같은 세션에서 배율 1 로 재서 비교한다.
|
||||
// 🔴 Debug.LogError / Debug.LogException 을 쓰지 않는다(813z 규칙).
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Reflection;
|
||||
using UnityEngine;
|
||||
using WL.Combat.Core;
|
||||
|
||||
namespace WL.Combat.Reaction
|
||||
{
|
||||
public static class MobKnockbackScale
|
||||
{
|
||||
sealed class Track
|
||||
{
|
||||
public Actor actor;
|
||||
public Dictionary<eCC, CCData> cc;
|
||||
public double lastBack, lastHitter;
|
||||
public bool movingBack, movingHitter;
|
||||
public int stillBack, stillHitter;
|
||||
public Vector3 restPos, startBack, startHitter;
|
||||
public float lastActiveAt;
|
||||
}
|
||||
|
||||
/// <summary>넉백 값이 이 틱 수 동안(시간이 흐르는 프레임 기준) 전혀 줄지 않으면 원본 코루틴이 없는 것 = 몹이 영영 묶인다 → 0 으로 풀어 준다.</summary>
|
||||
const int StuckTicks = 10;
|
||||
/// <summary>배율 계산에 쓰는 프레임 시간 상한(판 재시작·로딩 순간의 긴 프레임이 계산을 음수로 만들지 않게).</summary>
|
||||
const float MaxStepSeconds = 0.05f;
|
||||
|
||||
static readonly Dictionary<Actor, Track> s_tracks = new Dictionary<Actor, Track>(64);
|
||||
static readonly List<Track> s_list = new List<Track>(64);
|
||||
static FieldInfo s_ccField;
|
||||
static bool s_ccLooked;
|
||||
|
||||
// ── 진단(프로브가 읽는다)
|
||||
public static bool Subscribed;
|
||||
public static int ScaledBack, ScaledHitter, EndedBack, EndedHitter, StuckCleared;
|
||||
public static float SumMovedBack, SumMovedHitter, LastMovedBack, LastMovedHitter, LastScale = -1f;
|
||||
public static int TrackCount { get { return s_list.Count; } }
|
||||
|
||||
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.AfterAssembliesLoaded)]
|
||||
static void Register()
|
||||
{
|
||||
CombatEvents.HitConfirmed.Add(OnHitConfirmed);
|
||||
Subscribed = true;
|
||||
}
|
||||
|
||||
static WLHitFeelSettings St { get { return WL.Feel.WLHitFeel.Settings; } }
|
||||
|
||||
public static bool Enabled
|
||||
{
|
||||
get { var st = St; return st != null && st.enabled && st.mobKnockbackDistanceScale > 0f && !WL.Feel.WLHitFeel.RuntimeDisabled; }
|
||||
}
|
||||
|
||||
static void OnHitConfirmed(in HitConfirmedEvent e)
|
||||
{
|
||||
if (!Enabled) return;
|
||||
var victim = e.victim;
|
||||
if (victim == null || !(victim is MobActor) || victim.IsDead()) return;
|
||||
Track t;
|
||||
if (!s_tracks.TryGetValue(victim, out t))
|
||||
{
|
||||
if (!s_ccLooked)
|
||||
{
|
||||
s_ccLooked = true;
|
||||
s_ccField = typeof(Actor).GetField("dic_ccData", BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public);
|
||||
}
|
||||
var dict = s_ccField != null ? s_ccField.GetValue(victim) as Dictionary<eCC, CCData> : null;
|
||||
if (dict == null) return;
|
||||
// 직전 값은 0(넉백 아님)으로 시작한다 — 이 피격이 방금 건 넉백도 다음 틱에 「새 넉백」으로 잡힌다
|
||||
t = new Track { actor = victim, cc = dict, restPos = victim.transform.position };
|
||||
s_tracks.Add(victim, t);
|
||||
s_list.Add(t);
|
||||
}
|
||||
t.lastActiveAt = Time.unscaledTime;
|
||||
WLReactionRunner.Ensure();
|
||||
}
|
||||
|
||||
internal static void Tick(float now)
|
||||
{
|
||||
if (s_list.Count == 0) return;
|
||||
var st = St;
|
||||
bool on = Enabled;
|
||||
float scale = on ? Mathf.Clamp01(st.mobKnockbackDistanceScale) : 1f;
|
||||
bool byHitter = on && st.mobKnockbackScaleByHitter;
|
||||
float dt = Time.deltaTime;
|
||||
LastScale = scale;
|
||||
|
||||
for (int i = s_list.Count - 1; i >= 0; i--)
|
||||
{
|
||||
var t = s_list[i];
|
||||
var a = t.actor;
|
||||
if (a == null || !a.isActiveAndEnabled || a.IsDead() || (now - t.lastActiveAt > 5f && !t.movingBack && !t.movingHitter))
|
||||
{
|
||||
if (!ReferenceEquals(a, null)) s_tracks.Remove(a);
|
||||
s_list.RemoveAt(i);
|
||||
continue;
|
||||
}
|
||||
var tr = a.transform;
|
||||
CCData d;
|
||||
bool idle = true;
|
||||
if (t.cc.TryGetValue(eCC.KnockBack, out d))
|
||||
{
|
||||
Step(t, d, ref t.lastBack, ref t.movingBack, ref t.startBack, ref t.stillBack, tr, scale, dt, ref ScaledBack, ref EndedBack, ref SumMovedBack, ref LastMovedBack);
|
||||
if (t.movingBack) idle = false;
|
||||
}
|
||||
if (t.cc.TryGetValue(eCC.KnockBack_byHitter, out d))
|
||||
{
|
||||
Step(t, d, ref t.lastHitter, ref t.movingHitter, ref t.startHitter, ref t.stillHitter, tr, byHitter ? scale : 1f, dt, ref ScaledHitter, ref EndedHitter, ref SumMovedHitter, ref LastMovedHitter);
|
||||
if (t.movingHitter) idle = false;
|
||||
}
|
||||
if (idle) t.restPos = tr.position; // 넉백 직전 위치(다음 넉백의 시작점)
|
||||
else t.lastActiveAt = now;
|
||||
}
|
||||
}
|
||||
|
||||
static void Step(Track t, CCData d, ref double last, ref bool moving, ref Vector3 start, ref int still, Transform tr, float scale, float dt,
|
||||
ref int scaledCount, ref int endedCount, ref float sumMoved, ref float lastMoved)
|
||||
{
|
||||
double cc = d.CCTime;
|
||||
if (cc > last + 0.0001)
|
||||
{
|
||||
// 새 넉백 시작(또는 진행 중 재시작) — 진행 중이었다면 지금까지 움직인 거리를 먼저 마감
|
||||
if (moving) { lastMoved = Flat(start, tr.position); sumMoved += lastMoved; endedCount++; start = tr.position; }
|
||||
else start = t.restPos;
|
||||
moving = true;
|
||||
still = 0;
|
||||
// 🔴 직전 틱에 넉백이 아니었을 때만(= 원본 코루틴이 방금 시작) 줄인다. 계산이 0 이하면 0 — 양수 최솟값을 남기면
|
||||
// 코루틴이 없는 경우(판 재시작으로 끊긴 넉백 등) 그 값이 영영 안 줄어 몹이 넉백 상태에 묶였다(2026-09-18 실측 · 0.0001 고정).
|
||||
if (scale < 0.999f && last <= 0.0)
|
||||
{
|
||||
float step = Mathf.Min(dt, MaxStepSeconds);
|
||||
double adj = (cc + step) * scale - step;
|
||||
d.CCTime = adj > 0.0 ? adj : 0.0;
|
||||
cc = d.CCTime;
|
||||
scaledCount++;
|
||||
}
|
||||
}
|
||||
else if (cc > 0.0)
|
||||
{
|
||||
// 줄지 않는 넉백 = 원본 코루틴이 없다(끊긴 채 값만 남음) → 몹이 영영 묶이지 않게 풀어 준다(시간이 멈춘 히트스톱 프레임은 세지 않는다)
|
||||
if (dt > 0f && System.Math.Abs(cc - last) < 1e-9)
|
||||
{
|
||||
if (++still >= StuckTicks) { d.CCTime = 0.0; cc = 0.0; still = 0; StuckCleared++; }
|
||||
}
|
||||
else still = 0;
|
||||
}
|
||||
if (moving && cc <= 0.0)
|
||||
{
|
||||
lastMoved = Flat(start, tr.position);
|
||||
sumMoved += lastMoved;
|
||||
endedCount++;
|
||||
moving = false;
|
||||
}
|
||||
last = cc;
|
||||
}
|
||||
|
||||
static float Flat(Vector3 a, Vector3 b) { a.y = 0f; b.y = 0f; return Vector3.Distance(a, b); }
|
||||
|
||||
/// <summary>씬 전환: 추적 목록(파괴된 Actor 참조)을 버린다.</summary>
|
||||
public static void ClearAll() { s_tracks.Clear(); s_list.Clear(); }
|
||||
|
||||
/// <summary>프로브용 카운터 초기화.</summary>
|
||||
public static void ResetDiagnostics()
|
||||
{
|
||||
ScaledBack = ScaledHitter = EndedBack = EndedHitter = StuckCleared = 0;
|
||||
SumMovedBack = SumMovedHitter = LastMovedBack = LastMovedHitter = 0f;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 99cbc94ae8b7d334aa9275de30b417f9
|
||||
|
|
@ -97,6 +97,10 @@ namespace WL.Combat.Reaction
|
|||
// 원인(값 계산): 켜짐 최대 0.06 s / 쉼 0.03 s 라 연속 피격 중엔 최대 66 % 시간이 흰색 + 알베도 HDR 3배 = 완전 흰색.
|
||||
// 🔴 하얗게 **오래** 남던 주원인은 이 점멸이 아니라 공격 예고의 흰 섬광이 끊긴 공격에 남은 것(실측 · `WLTelegraphSettings.endOnAttackInterrupted`).
|
||||
// → 켜짐 0.07(상한 0.09) · 쉼 0.2 s(= 연타 중에도 또렷이 끊어지는 점멸) · 그리기는 캐릭터 덧그리기 패스(`WL/CharBacklight`)의 `_HitFlash` 로 **흰색을 20 % 만 덮는다**.
|
||||
[Tooltip("true = 타격마다 점멸을 처음부터 다시 켠다(아래 blinkOffSeconds 쉼·blinkMaxSeconds 연장 상한을 쓰지 않음 · 같은 프레임 여러 타는 1회).\n" +
|
||||
"2026-09-18 PD 「히트 하이라이트가 최초 공격 1회에만 발생 → 타격될 때마다」. false = 종전(쉼 동안 다음 점멸 생략).")]
|
||||
public bool retriggerEveryHit = true;
|
||||
|
||||
[Tooltip("true = 덧그리기 패스가 붙은 렌더러는 흰색을 overlayOpacity 만큼만 덮는다(본체 색이 비친다). false 또는 패스가 없으면 예전 방식(알베도 HDR = 완전 흰색).")]
|
||||
public bool overlayMode = true;
|
||||
[Tooltip("덮는 정도(0~1). PD 2026-09-17 = 0.2.")]
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@ namespace WL.Combat.Reaction
|
|||
DamageAggregator.ClearAll();
|
||||
MobHitFlash.ClearAll();
|
||||
MobStagger.ClearAll(); // WL #816zb — 씬을 넘기며 speed 0 인 몹이 남지 않게
|
||||
MobKnockbackScale.ClearAll();
|
||||
}
|
||||
|
||||
void Update()
|
||||
|
|
@ -42,6 +43,7 @@ namespace WL.Combat.Reaction
|
|||
DamageAggregator.Tick(now);
|
||||
MobHitFlash.Tick(now);
|
||||
MobStagger.Tick(now); // WL #816zb — 피격 경직 해제
|
||||
MobKnockbackScale.Tick(now); // 2026-09-18 — 넉백 거리 배율
|
||||
}
|
||||
|
||||
void OnDisable() { MobHitFlash.RestoreAll(); MobStagger.RestoreAll(); KillChain.ReleaseBurst(); }
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ MonoBehaviour:
|
|||
blinkOnSeconds: 0.07
|
||||
blinkMaxSeconds: 0.09
|
||||
blinkOffSeconds: 0.2
|
||||
retriggerEveryHit: 1
|
||||
overlayMode: 1
|
||||
overlayOpacity: 0.2
|
||||
overlayColor: {r: 1, g: 1, b: 1, a: 1}
|
||||
|
|
|
|||
|
|
@ -69,6 +69,8 @@ MonoBehaviour:
|
|||
flashSeconds: 0.05
|
||||
flashAlbedo: {r: 2.2, g: 2.2, b: 2.2, a: 1}
|
||||
flashEmission: {r: 1.6, g: 1.6, b: 1.6, a: 1}
|
||||
flashOverlayOpacity: 0.2
|
||||
flashOverlayColor: {r: 1, g: 1, b: 1, a: 1}
|
||||
pulseEnabled: 1
|
||||
pulseScale: 1.12
|
||||
pulseRestoreGuardSeconds: 0.2
|
||||
|
|
|
|||
|
|
@ -45,6 +45,7 @@ namespace WL.Combat.Telegraph
|
|||
public float startTime, plannedSeconds, hitSeconds, origSpeed, appliedSpeed, targetSeconds;
|
||||
public float fadeUntil, guardUntil;
|
||||
public bool speedApplied, tinted, pulsed, flashed, wasElite, corrected;
|
||||
public bool overlayFlash; // 2026-09-18 — 섬광을 덮기 방식(MobHitFlash.SetTellFlash)으로 걸었는가
|
||||
public Vector3 baseScale;
|
||||
public int decalIdx;
|
||||
public int attackIndex;
|
||||
|
|
@ -294,9 +295,8 @@ namespace WL.Combat.Telegraph
|
|||
// 히트 직전 흰 섬광
|
||||
if (st.flashEnabled && !s.flashed && (planned - elapsed) <= st.flashLeadSeconds)
|
||||
{
|
||||
if (WL.Combat.Reaction.MobHitFlash.SetEliteTint(s.actor, st.flashAlbedo, st.flashEmission))
|
||||
{ s.flashed = true; s.tinted = true; FlashCount++; }
|
||||
else s.flashed = true;
|
||||
if (SetFlash(s, st)) FlashCount++;
|
||||
s.flashed = true;
|
||||
}
|
||||
}
|
||||
else if (s.phase == Phase.Fading)
|
||||
|
|
@ -377,7 +377,7 @@ namespace WL.Combat.Telegraph
|
|||
|
||||
static void EndInternal(State s, bool byHit, bool forced)
|
||||
{
|
||||
if (s.phase == Phase.None && !s.speedApplied && !s.tinted && !s.pulsed && s.decalIdx < 0) return;
|
||||
if (s.phase == Phase.None && !s.speedApplied && !s.tinted && !s.overlayFlash && !s.pulsed && s.decalIdx < 0) return;
|
||||
|
||||
var st = St;
|
||||
float now = TelegraphRunner.Now;
|
||||
|
|
@ -401,8 +401,7 @@ namespace WL.Combat.Telegraph
|
|||
|
||||
if (byHit && !forced && st != null && st.flashEnabled && st.flashSeconds > 0f && s.actor != null)
|
||||
{
|
||||
if (!s.flashed && WL.Combat.Reaction.MobHitFlash.SetEliteTint(s.actor, st.flashAlbedo, st.flashEmission))
|
||||
{ s.tinted = true; FlashCount++; }
|
||||
if (!s.flashed && SetFlash(s, st)) FlashCount++;
|
||||
s.flashed = true;
|
||||
s.phase = Phase.Fading;
|
||||
s.fadeUntil = now + st.flashSeconds;
|
||||
|
|
@ -418,8 +417,23 @@ namespace WL.Combat.Telegraph
|
|||
if (s.guardUntil <= now && s.phase == Phase.None) Deactivate(s);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 흰 섬광을 건다. 2026-09-18 PD 「투명도 20 % 반투명으로」 — flashOverlayOpacity > 0 이면 피격 하이라이트와 같은 덮기 방식
|
||||
/// (본체 색·예고 색이 비친다). 덧그리기 패스를 못 쓰면 예전 방식(알베도·에미션 = 완전 흰색).
|
||||
/// </summary>
|
||||
static bool SetFlash(State s, WLTelegraphSettings st)
|
||||
{
|
||||
if (s.actor == null) return false;
|
||||
if (st.flashOverlayOpacity > 0f && WL.Combat.Reaction.MobHitFlash.SetTellFlash(s.actor, st.flashOverlayOpacity, st.flashOverlayColor))
|
||||
{ s.overlayFlash = true; return true; }
|
||||
if (WL.Combat.Reaction.MobHitFlash.SetEliteTint(s.actor, st.flashAlbedo, st.flashEmission))
|
||||
{ s.tinted = true; return true; }
|
||||
return false;
|
||||
}
|
||||
|
||||
static void ClearTint(State s, WLTelegraphSettings st)
|
||||
{
|
||||
if (s.overlayFlash) { s.overlayFlash = false; if (s.actor != null) WL.Combat.Reaction.MobHitFlash.SetTellFlash(s.actor, 0f, Color.white); }
|
||||
if (!s.tinted || s.actor == null) { s.tinted = false; return; }
|
||||
s.tinted = false;
|
||||
|
||||
|
|
@ -435,7 +449,7 @@ namespace WL.Combat.Telegraph
|
|||
static void HardClear(State s)
|
||||
{
|
||||
if (s.decalIdx >= 0) { TelegraphDecal.Release(s.decalIdx); s.decalIdx = -1; }
|
||||
s.phase = Phase.None; s.tinted = false; s.pulsed = false; s.speedApplied = false; s.guardUntil = 0f;
|
||||
s.phase = Phase.None; s.tinted = false; s.overlayFlash = false; s.pulsed = false; s.speedApplied = false; s.guardUntil = 0f;
|
||||
Deactivate(s);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -222,9 +222,15 @@ namespace WL.Combat.Telegraph
|
|||
[Tooltip("흰 섬광이 유지되는 시간(초 · 60 fps 기준 0.05 ≈ 3프레임).")]
|
||||
public float flashSeconds = 0.05f;
|
||||
|
||||
[Tooltip("예전 방식(아래 덮기 불투명도 0 또는 덧그리기 패스 없음)의 알베도 색 — 완전 흰색.")]
|
||||
[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);
|
||||
|
||||
// 2026-09-18 PD 「(예고 섬광도) 투명도 20 % 반투명으로 낮춰」 — 피격 하이라이트(WLHitBlinkSettings.overlayOpacity)와 같은 덮기 방식
|
||||
[Tooltip("흰 섬광을 캐릭터 덧그리기 패스로 이만큼만 덮는다(0~1 · 본체·예고 색이 비친다). 0 = 예전 방식(flashAlbedo · 완전 흰색).")]
|
||||
[Range(0f, 1f)] public float flashOverlayOpacity = 0.2f;
|
||||
public Color flashOverlayColor = Color.white;
|
||||
|
||||
[Header("ⓓ 스케일 펄스 (813s EliteMarker 와 같은 localScale 경로 · 원 스케일 복원)")]
|
||||
[Tooltip("끄면 크기를 건드리지 않는다.")]
|
||||
public bool pulseEnabled = true;
|
||||
|
|
|
|||
|
|
@ -46,4 +46,6 @@ MonoBehaviour:
|
|||
mobStaggerBlockAttack: 1
|
||||
mobStaggerFreezeDelay: 0.1
|
||||
mobStaggerReplayInterval: 0.2
|
||||
mobKnockbackDistanceScale: 0.5
|
||||
mobKnockbackScaleByHitter: 1
|
||||
logHits: 0
|
||||
|
|
|
|||
|
|
@ -172,6 +172,14 @@ public class WLHitFeelSettings : ScriptableObject
|
|||
[Tooltip("연타 중 피격 모션을 다시 트는 최소 간격(초). 너무 짧으면 첫 프레임만 반복돼 떨려 보인다.")]
|
||||
public float mobStaggerReplayInterval = 0.2f;
|
||||
|
||||
// ── 2026-09-18 PD 「몬스터 넉백 거리가 너무 멀어. 우선은 절반 정도로 줄이고, 추후 캐릭터 테이블에서 공격 타수별 넉백 거리를 조절」
|
||||
// 원본 넉백 = 남은 시간(CCTime) × 속도로 민다(뒤로 밀기 초속 4 m · 때린 쪽 반대로 밀기 초속 10 m). 새 넉백이 시작된 프레임에
|
||||
// **남은 시간을 줄여** 총 이동 거리를 배율만큼으로 만든다(MobKnockbackScale · 원본 코드 0줄). 타수별 테이블 값은 설계서(인계 델타 추가분 35) 참조.
|
||||
[Tooltip("몹 넉백 이동 거리 배율. 1 = 원본 · 0.5 = 절반. 0 이하 = 기능 끔(원본 그대로).")]
|
||||
[Range(0f, 1f)] public float mobKnockbackDistanceScale = 0.5f;
|
||||
[Tooltip("때린 쪽 반대로 미는 넉백(스킬 추가효과 KnockBack_byHitter · 초속 10 m)에도 같은 배율을 쓴다.")]
|
||||
public bool mobKnockbackScaleByHitter = true;
|
||||
|
||||
// ─────────────────────────────────────────── 진단
|
||||
|
||||
[Header("진단 (검증용 · 배포 시 false)")]
|
||||
|
|
|
|||
|
|
@ -873,6 +873,60 @@ namespace WL.Island
|
|||
|
||||
static void OnTerrainBaked() { if (s_cfgForBaked != null) RefitPlaced(s_cfgForBaked); }
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────
|
||||
// 🔴 2026-09-18 PD 「가격판 겹침 오류 수정」 — 실측: FI `Purchaser` 는 부모 없이 Instantiate 돼 **활성 씬(InGame)** 에 생기고,
|
||||
// 던전에 들어갈 때 섬 씬만 내려가 섬(`Island`)은 파괴되고 판은 남았다 → 섬에 돌아오면 새 판과 같은 자리에 겹치고,
|
||||
// 옛 판에서 구매가 끝나면 코인을 뺀 뒤 `island.Activate()` 가 파괴된 섬에 접근(MissingReferenceException → 원본 오류 창 · 「0」 판 잔존).
|
||||
// → 섬 씬이 내려가는 순간 「파괴된 섬에 묶인 판」을 지운다(구매 진행분은 FI 세이브 `PurchaserData.Price` 에 이미 남아 새 판이 이어받는다).
|
||||
// 섬이 준비될 때(Apply)도 한 번 더 훑는다(안전장치). 섬 참조가 비어 있는 판(Init 전 · 우리 발판)은 건드리지 않는다.
|
||||
// ─────────────────────────────────────────────────────────────────
|
||||
static System.Reflection.FieldInfo s_purchIslandField;
|
||||
static bool s_purchIslandLooked;
|
||||
public static int StaleRemoved;
|
||||
|
||||
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.AfterAssembliesLoaded)]
|
||||
static void HookSceneUnload()
|
||||
{
|
||||
SceneManager.sceneUnloaded -= OnSceneUnloaded;
|
||||
SceneManager.sceneUnloaded += OnSceneUnloaded;
|
||||
}
|
||||
|
||||
static void OnSceneUnloaded(Scene scene)
|
||||
{
|
||||
var cfg = WLIslandSettings.Instance;
|
||||
if (cfg == null || string.IsNullOrEmpty(cfg.islandSceneName) || scene.name != cfg.islandSceneName) return;
|
||||
RemoveStalePurchasers(cfg, "섬 씬 내려감");
|
||||
}
|
||||
|
||||
/// <summary>파괴된 섬에 묶인 FI 확장 가격판을 지운다. 반환 = 지운 수.</summary>
|
||||
public static int RemoveStalePurchasers(WLIslandSettings cfg, string reason)
|
||||
{
|
||||
if (!s_purchIslandLooked)
|
||||
{
|
||||
s_purchIslandLooked = true;
|
||||
s_purchIslandField = typeof(FIPurchaser).GetField("island", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Public);
|
||||
}
|
||||
if (s_purchIslandField == null) return 0;
|
||||
var all = Object.FindObjectsByType<FIPurchaser>(FindObjectsInactive.Include, FindObjectsSortMode.None);
|
||||
int n = 0;
|
||||
for (int i = 0; i < all.Length; i++)
|
||||
{
|
||||
var p = all[i];
|
||||
if (p == null) continue;
|
||||
var isl = s_purchIslandField.GetValue(p) as Object;
|
||||
if (ReferenceEquals(isl, null) || isl != null) continue; // 묶인 적 없음 · 섬이 살아 있음
|
||||
Object.Destroy(p.gameObject);
|
||||
n++;
|
||||
}
|
||||
if (n > 0)
|
||||
{
|
||||
StaleRemoved += n;
|
||||
LastLog = "옛 확장 가격판 " + n + "개 제거(" + reason + " · 파괴된 섬에 묶인 판)";
|
||||
if (cfg != null) WLIslandBridge.Log(cfg, LastLog);
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
static IEnumerator Co_RefitNextFrame(WLIslandSettings cfg) { yield return null; RefitPlaced(cfg); }
|
||||
|
||||
/// <summary>이미 옮긴 판의 높이·기울기만 지형에 다시 맞춘다(자리는 그대로).</summary>
|
||||
|
|
@ -925,6 +979,7 @@ namespace WL.Island
|
|||
{
|
||||
if (cfg == null || cfg.gateFlowEnabled == 0 || cfg.expansionAfterFirstWin == 0) return;
|
||||
|
||||
RemoveStalePurchasers(cfg, "섬 준비"); // 2026-09-18 — 던전 복귀 뒤 옛 판(파괴된 섬에 묶임) 안전장치
|
||||
Watch(cfg, scene); // 칸이 열리는 프레임에 바로 옮기기 · 지형 재굽기 뒤 높이 재맞춤
|
||||
PlacePurchasers(cfg, scene); // 자리 옮기기(숨김 여부와 무관 · 1회)
|
||||
if (IsUnlocked(cfg))
|
||||
|
|
|
|||
Loading…
Reference in New Issue