WL #960 PD 지시 4건(비수 · 테스트 씬) — 잔상 검정 · 각성 착지 옆 진입 양손 베기 · 주력 10회 판정 · TestMap Play = 스킬 테스트 씬
- ① 잔상 색: 주력 난사 잔상(#959 teal) · 각성 진입 그림자 → 검정(0,0,0,0.5) · 4타 순간이동 · 시전 · 착지 잔상은 원래 거의 검정(0.07,0.03,0.12) - ② 비수 각성 착지: 낙하로 보인 원인 = 모델이 드러난 뒤 ghost_land(Platform Sub_HeroLanding = 공중에서 내려앉는 클립) 재생 + 옆 진입 동안 실제 모델 숨김 → 착지 상태 = 4타 양손 낮게 가르기(DualBlades_DodgeAttack · 이벤트 끔) · 실제 모델이 보인 채 옆 1.2 m 에서 0.2 s 진입 · 0.04 s 마다 검은 지금 자세 잔상 · 끝 잔상 3 = 양손 베기 자세 · 착지 동안 공격 전진 표 막음(AttackRootMotion.Gate · 실측 이동 0.75 → 0.11 m) - ③ 비수 주력: 원본 판정(시전 0.5 s 뒤 0.1 s 간격 6회 · 난사와 무관) 끄고(go_effect Loop 0) 난사 틱 0.24 → 0.2 s · 휘두름마다 0.1 s 뒤 원본 한 대 피해 1회 × 10(Skill_ShadowSlash.ProcessCollider · 1회 피해 그대로 → 총량 원본의 10/6) · 범위 PC 발 3.5 m + 몸 반지름 · 대상 쪽 ±110° · 판정 시각 대기열(끊김에도 휘두름마다) · 실측 3회 모두 10/10 · 0.2 s 간격 - ④ 테스트 씬 미실행: PD 가 Play 한 씬 = Assets/Scenes/TestMap.unity(원본 맵 조각 · LoadMapMgr.Awake NRE · #957 과 같음) → WLSkillTestPlayRedirect(에디터) = WLSkillTest.json redirectScenes 의 씬을 연 채 Play 하면 Play 시작 씬을 WLSkillTest.unity 로(playModeStartScene · 끝나면 원래 씬) · 실측 TestMap 에서 Play → ready · 콘솔 오류 0 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
a6c2bf0809
commit
e04e380307
|
|
@ -140,6 +140,7 @@ namespace WL.Combat
|
|||
if (_agent == null || !_agent.enabled || !_agent.isOnNavMesh) return false;
|
||||
if (!_pc.WL_CanAttackStep()) return false;
|
||||
if (DashDriver.IsDashingNow(_pc)) return false;
|
||||
if (WL.Combat.Weapon.WLGhostStrike.IsLanding(_pc)) return false; // #960 비수 각성 착지(양손 베기 = 4타 클립)는 옆 진입 뒤 멈춰야 한다 — 4타 전진 표가 착지 뒤 0.65 m 더 밀었다(실측 이동 0.75 m)
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -42,7 +42,11 @@
|
|||
"jumpForward": 0.1,
|
||||
"jumpSide": 1.0,
|
||||
"jumpSeconds": 0.25,
|
||||
"landState": "ghost_land",
|
||||
"landState": "DualBlades_DodgeAttack",
|
||||
"landStateStart": 0.0,
|
||||
"landNoEvents": 1,
|
||||
"entryGhostInterval": 0.04,
|
||||
"_note_960": "#960 PD 「각성 마지막 랜딩 때 그림자는 옆에서 등장하는데 캐릭터는 공중에서 낙하 → 캐릭터를 양손 베기 모션 상태로 그림자처럼 옆에서 등장하며 랜딩」 · 「비수 공격 잔상 색 = 검은색」 — 낙하로 보인 원인 = 착지 상태 ghost_land(Platform Sub_HeroLanding = 공중에서 내려앉는 클립)를 모델이 드러난 뒤 재생 · 옆 진입 동안은 실제 모델을 숨기고 반투명 teal 그림자만 그렸음 → 착지 상태 = 4타 양손 낮게 가르기(DualBlades_DodgeAttack = Stander@DualBlades_Attack3 · 처음부터 · 애니메이션 이벤트 끔 = 4타 순간이동·카메라·타격 안 섞임) · 실제 모델이 보인 채 옆 1.2 m 에서 0.2 s 감속 진입 · 지나온 길에 0.04 s 마다 지금 자세 검은 잔상(landGhostColor 0,0,0,0.5 · 유지 0.03 · 소멸 0.18) · 끝 잔상 3 = 양손 베기 자세",
|
||||
"landHeight": 0.0,
|
||||
"landFallSeconds": 0.2,
|
||||
"entrySide": 1.2,
|
||||
|
|
@ -57,11 +61,11 @@
|
|||
"dustScale": 0.15,
|
||||
"keepCamera": 1,
|
||||
"fallForward": 0.0,
|
||||
"landPose": "ghost_land@0.25",
|
||||
"landHideModel": 1,
|
||||
"landGhostColor": [ 0.55, 0.95, 0.85, 0.45 ],
|
||||
"landPose": "DualBlades_DodgeAttack@0.3",
|
||||
"landHideModel": 0,
|
||||
"landGhostColor": [ 0.0, 0.0, 0.0, 0.5 ],
|
||||
"landGhostHold": 0.03,
|
||||
"landGhostFade": 0.1,
|
||||
"landGhostFade": 0.18,
|
||||
"popGhosts": 3,
|
||||
"_note_955": "#955 PD 「너무 높은 데서 떨어짐 → 공중 낙하가 아니라 0.5 m 쯤 살짝 낮은 공간에서 순간이동하듯 등장하며 착지 · 수직 랜딩 뒤 미끄러짐이 아니라 전진하듯 살짝 미끄러지는 연출」 — 높이 0.5 → 0.2 · 낙하 0.15 → 0.06 s(거의 즉시 등장) · 등장 뒤 전진 미끄러짐 0.45 m / 0.3 s 감속 · 잔상 3",
|
||||
"_note_954": "#954 PD 「각성 뒤 너무 높은 위치에서 튀어나옴 → 0.5 m 정도 낮은 높이에서 반투명 상태로 갑자기 이펙트와 함께 튀어나오며 랜딩 · 랜딩 시간 짧게 · 랜딩 중 공격 불가」 — 높이 1.2 → 0.5 · 낙하 0.25 → 0.15 s · 전체 1.0 → 0.45 s · 미끄러짐 0.4/0.35 → 0.3/0.25 · 낙하 중 실제 모델 숨김 + 착지 자세(ghost_land@0.25)의 반투명 teal 캐릭터를 매 프레임(유지 0.02 · 소멸 0.08) · 착지 순간 모델 보임 + 잔상 2 + 먼지 · 공격 금지 = IsLanding(하네스 공격 키) + IsStop(자동 전투)",
|
||||
|
|
|
|||
|
|
@ -17,7 +17,13 @@
|
|||
"fxPrefabWidth": 5.26,
|
||||
"hideOriginalFx": 1,
|
||||
"slashStates": [ "attack", "DualBlades_Attack2" ],
|
||||
"slashInterval": 0.24,
|
||||
"slashInterval": 0.2,
|
||||
"slashHits": 10,
|
||||
"slashHitDelay": 0.1,
|
||||
"slashHitRadius": 3.5,
|
||||
"slashHitRangeMul": 1.6,
|
||||
"slashHitHalfAngle": 110,
|
||||
"_note_960": "#960 PD 「비수 주력 스킬 공격 시 총 10회 타격이 가능하도록 애니메이션 재생 타이밍에 적절하게 판정」 · 「비수 공격 잔상 색 = 검은색」 — 원본 판정 = 시전 0.5 s 뒤 go_effect 가 0.1 s 간격 6회(Loop 6 · 난사와 무관) → 끄고(Loop 0) 난사 휘두름 틱 0.24 → 0.2 s(2.0 s 에 10번 휘두름 · 1.6 배속 exit 0.31 s 보다 짧음) 마다 0.1 s 뒤(1·2타 클립의 칼이 지나가는 순간) 1회씩 10회 = 원본 한 대 피해(1회 피해 그대로 → 총량 원본 6회의 10/6) · 범위 = PC 발 기준 3.5 m + 몸 반지름 · 대상 쪽 ±110°(맞을 때마다 더미가 밀려나 공격 범위 × 1.6 = 1.6 m 로는 10회 중 7회 · 2.5 m 로도 3.0 m 까지 밀려 빗나감 실측 · 원본 판정 상자는 앞 약 4.4 m 까지) · 판정 시각은 대기열(프레임 끊김에도 휘두름마다 1회) · 정면 사본 반복도 0.2 s(휘두름마다) · 잔상 색 teal → 검정(0,0,0,0.5)",
|
||||
"slashSpeed": 1.6,
|
||||
"slashSeconds": 2.0,
|
||||
"fxMain": 1,
|
||||
|
|
@ -27,14 +33,14 @@
|
|||
"fxSlashFraction": 0.6,
|
||||
"fxSlashYOffset": 0.0,
|
||||
"fxSlashFollow": 1,
|
||||
"fxRepeatInterval": 0.24,
|
||||
"fxRepeatInterval": 0.2,
|
||||
"fxRepeatSeconds": 0,
|
||||
"slashHardRestart": 1,
|
||||
"slashGhosts": 1,
|
||||
"slashGhostInterval": 0.07,
|
||||
"slashGhostHold": 0.03,
|
||||
"slashGhostFade": 0.14,
|
||||
"slashGhostColor": [ 0.55, 0.95, 0.85, 0.4 ],
|
||||
"slashGhostColor": [ 0.0, 0.0, 0.0, 0.5 ],
|
||||
"_note_959": "#959 PD 「이펙트 크기 2배 더 · 플레이어 동작에 맞게 재생시간을 늘려 모션만 보이지 않게(캐릭터 전진값 고려해 위치도) · 공격 모션에 잔상(빠르게 공격하는 느낌)」 — 폭 = 공격 범위 × 1.6 → 3.2(배율 0.304 → 0.608) · 정면 사본을 0.5 s 부터 0.28 s 마다 난사가 끝나는 2.0 s 까지 되풀이(매번 그때 PC 위치·대상 방향 = 전진 반영 · 초승달 수명 0.3 s 라 1회로는 0.6 s 만 보였음) · 난사 상태는 Animator.Play 로 강제(Play 실측: 배속 1.8 에선 2타→3타 exit 전이가 틱보다 먼저 와 3타 회전·4타 돌진 0.4 m 가 섞였음 = PD 가 본 「전진」) · 난사 2.0 s 동안 0.07 s 마다 지금 자세 잔상(teal · 유지 0.03 · 소멸 0.14) · 틱 0.28 → 0.24 s · 배속 1.8 → 1.6(2타가 #959 원본 DualBlades_Attack2 16~36 f 0.61 s · exit 0.8 이 되어 1.8 배속이면 exit 0.27 s 가 틱보다 먼저 와 3타 회전이 샘 → 1.6 배속 exit 0.31 s > 0.24 s)",
|
||||
"_note_958": "#958 PD 「주력 이펙트가 양손에 작게 하나씩 → 무기 방향 전방에 다른 무기 궤적처럼 하나만 · 전방의 적이 있는 방향에 1개 · 플레이어가 타겟하려는 대상의 무기 공격 범위가 중점」 — 양손 난사 사본 끔(fxPerSlash 0) · 정면 사본 1개(fxMain 1 · 0.5 s) · 방향 = 대상 쪽(fxAimTarget 1) · 중심 = PC 몸 중심 + 공격 범위(WL_AttackRange) × 0.5(대상이 더 가까우면 대상 자리) · 폭 = 공격 범위 × 1.6(배율 = 폭 ÷ 프리팹 단위 초승달 폭 5.26 m · #956 편집 모드 실측 · fxScale 무시) · 높이 0 = 몸 중심",
|
||||
"_note_956": "#956 PD 「주력 이펙트가 너무 먼 위치(정면 1.2 m)에 재생 → 플레이어가 휘두르는 무기 위치에 맞게(휘두르는 느낌)」 — 정면 1회 사본은 끔(fxMain 0 · 켜면 fxForward 0.45 · 손 높이 0.3) · 대신 난사 루프 0.28 s 마다 같은 프리팹의 작은 사본(0.35 · 편집 모드 실측 프리팹 단위 초승달 AABB 5.26 m → 약 1.8 m · 초승달 수명 0.30 s · 2번째 초승달 시차)을 양손 소켓(w_bladedancer_l/r) 손잡이→날 끝 0.6 자리에 번갈아 띄우고 0.6 s 동안 날 방향(수평)·위치를 따라가게",
|
||||
|
|
|
|||
|
|
@ -60,6 +60,11 @@ namespace WL.Combat.Weapon
|
|||
public float[] landGhostColor; // 떨어지는 반투명 캐릭터 색(r,g,b,a) · 비우면 잔상 색
|
||||
public float landGhostHold = 0.02f, landGhostFade = 0.08f; // 매 프레임 띄우는 반투명 캐릭터의 유지·소멸(초)
|
||||
public int popGhosts = 0; // 착지 순간 착지 자세로 터뜨리는 잔상 수(잔상 색 · 기본 유지·소멸)
|
||||
// #960 PD 「각성 마지막 랜딩 때 그림자는 옆에서 등장하는데 캐릭터는 공중에서 낙하 → 캐릭터를 양손 베기 모션 상태로 그림자처럼 옆에서 등장하며 랜딩」 —
|
||||
// landStateStart = 착지 상태를 이 정규화 시각부터(양손 베기 클립의 베는 구간이 옆 진입 동안 보이게) · landNoEvents 1 = 착지 동안 애니메이션 이벤트 끔(4타 클립의 순간이동 · 카메라 고정 · 타격이 착지에 끼지 않게) ·
|
||||
// entryGhostInterval > 0 = 옆에서 들어오는 동안(실제 모델이 보일 때) 이 간격으로 지금 자세 잔상(landGhostColor · landGhostHold · landGhostFade) = 그림자처럼 지나온 길
|
||||
public float landStateStart = 0f, entryGhostInterval = 0f;
|
||||
public int landNoEvents = 1;
|
||||
public string poseSource = "clips"; // clips = 숨은 꼭두각시에 poses 를 걸어 굽기 · live = 시전 순간 PC 자세 1 개
|
||||
public string[] poses = new string[0]; // "애니메이터 상태 이름@정규화 시각" — 차례로 돌려 쓴다(시작은 무작위)
|
||||
}
|
||||
|
|
@ -106,6 +111,7 @@ namespace WL.Combat.Weapon
|
|||
// #951 도약 잔상 · 착지
|
||||
public int jumpDone; public float jumpNext = -1f; public bool wasHidden, sawAir, landing, landed, dustDone, hideDuringFall; public float landT0, modelBaseY, modelY = float.NaN; public int landGhostsDone, hideUntilFrame;
|
||||
public Vector3 landPos, landFwd, landRight; public float entryMoved; public Transform model; public UnityEngine.AI.NavMeshAgent agent; public object stopWas; // #959 landRight · entryMoved = 옆에서 들어오기
|
||||
public bool eventsWas = true, eventsOff; public float nextEntryGhost; // #960 착지 동안 애니메이션 이벤트 끔 · 옆 진입 잔상 다음 시각
|
||||
public readonly List<SkinnedMeshRenderer> smrs = new List<SkinnedMeshRenderer>();
|
||||
public readonly List<MeshFilter> weapons = new List<MeshFilter>();
|
||||
public readonly List<string> smrPaths = new List<string>();
|
||||
|
|
@ -357,6 +363,14 @@ namespace WL.Combat.Weapon
|
|||
var lp = cast.model.localPosition; cast.model.localPosition = new Vector3(lp.x, cast.modelY, lp.z);
|
||||
}
|
||||
|
||||
public static int EntryGhosts; // #960 옆 진입 잔상 수(검증용)
|
||||
/// <summary>#960 착지 동안 끈 애니메이션 이벤트를 되돌린다(한 번만).</summary>
|
||||
static void RestoreEvents(Cast cast)
|
||||
{
|
||||
if (!cast.eventsOff) return; cast.eventsOff = false;
|
||||
try { if (!DSUtil.CheckNull(cast.shooter) && cast.shooter.m_animation != null) cast.shooter.m_animation.fireEvents = cast.eventsWas; } catch (System.Exception) { }
|
||||
}
|
||||
|
||||
/// <summary>시전자 모델(살갗 메시 + 무기 메시) 보이기·숨기기 — 원본 Model_OnOff 와 같은 렌더러를 쓰되 한 프레임용.</summary>
|
||||
static void SetVisible(Cast cast, bool on)
|
||||
{
|
||||
|
|
@ -371,7 +385,7 @@ namespace WL.Combat.Weapon
|
|||
if (cast.landing)
|
||||
{
|
||||
if (cast.model != null) { var lp = cast.model.localPosition; cast.model.localPosition = new Vector3(lp.x, cast.modelBaseY, lp.z); }
|
||||
SetVisible(cast, true); SetStop(a, cast.stopWas);
|
||||
SetVisible(cast, true); SetStop(a, cast.stopWas); RestoreEvents(cast); // #960
|
||||
}
|
||||
cast.landing = false; cast.landed = true; cast.modelY = float.NaN; cast.hideUntilFrame = 0; cast.hideDuringFall = false; s_cast = null; LandLog += " · 취소(직업 전환)";
|
||||
}
|
||||
|
|
@ -406,7 +420,17 @@ namespace WL.Combat.Weapon
|
|||
var from = cast.landPos - cast.landRight * r.entrySide;
|
||||
try { if (pc != null) pc.Set_Warp(from); else cast.shooter.transform.position = from; } catch (System.Exception) { cast.shooter.transform.position = from; }
|
||||
}
|
||||
try { if (cast.shooter.m_animation != null) { cast.shooter.m_animation.speed = 1f; cast.shooter.m_animation.CrossFade(r.landState, 0f, 0, 0f); } } catch (System.Exception ex) { LandLog = "착지 클립 실패 — " + ex.Message; } // #952-② 섞임 0 = 서 있는 자세에서 번지지 않게
|
||||
try
|
||||
{
|
||||
var an = cast.shooter.m_animation;
|
||||
if (an != null)
|
||||
{
|
||||
if (r.landNoEvents != 0) { cast.eventsWas = an.fireEvents; an.fireEvents = false; cast.eventsOff = true; } // #960 양손 베기(4타) 클립의 이벤트(순간이동 · 카메라 · 타격)가 착지에 끼지 않게
|
||||
an.speed = 1f; an.CrossFade(r.landState, 0f, 0, Mathf.Clamp01(r.landStateStart));
|
||||
}
|
||||
}
|
||||
catch (System.Exception ex) { LandLog = "착지 클립 실패 — " + ex.Message; } // #952-② 섞임 0 = 서 있는 자세에서 번지지 않게
|
||||
cast.nextEntryGhost = now; // #960 옆 진입 잔상 첫 장 = 바로
|
||||
if (cast.model != null) cast.model.localPosition = new Vector3(cast.model.localPosition.x, cast.modelBaseY + r.landHeight, cast.model.localPosition.z);
|
||||
Landings++; LandLog = "착지 시작 t+" + (now - cast.t0).ToString("F2") + " @" + cast.landPos.ToString("F2") + (cast.agent != null ? " agent" : " agent 없음");
|
||||
}
|
||||
|
|
@ -432,6 +456,11 @@ namespace WL.Combat.Weapon
|
|||
float step = Mathf.Min(total * (2f / fall) * (1f - kk) * Time.deltaTime, Mathf.Max(0f, total - cast.entryMoved));
|
||||
var dir = cast.landRight * Mathf.Sign(r.entrySide); cast.entryMoved += step;
|
||||
if (cast.agent != null && cast.agent.enabled && cast.agent.isOnNavMesh) cast.agent.Move(dir * step); else cast.shooter.transform.position += dir * step;
|
||||
if (r.entryGhostInterval > 0f && !cast.hideDuringFall && now >= cast.nextEntryGhost) // #960 실제 캐릭터가 들어오는 길에 그림자 잔상
|
||||
{
|
||||
cast.nextEntryGhost = now + r.entryGhostInterval;
|
||||
if (LiveGhost(cast.shooter, r.landGhostColor, r.landGhostHold, r.landGhostFade)) EntryGhosts++;
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
|
@ -468,6 +497,7 @@ namespace WL.Combat.Weapon
|
|||
cast.landing = false; cast.landed = true; cast.modelY = float.NaN;
|
||||
if (cast.model != null) { var lp = cast.model.localPosition; cast.model.localPosition = new Vector3(lp.x, cast.modelBaseY, lp.z); }
|
||||
var pc = cast.shooter as PCActor;
|
||||
RestoreEvents(cast); // #960
|
||||
SetStop(cast.shooter, cast.stopWas);
|
||||
try { if (pc != null) pc.Play_Idle(); } catch (System.Exception) { }
|
||||
LandLog += " · 끝 " + t.ToString("F2") + " s · 이동 " + Vector3.Distance(cast.landPos, cast.shooter.Get_position()).ToString("F2") + " m";
|
||||
|
|
|
|||
|
|
@ -54,6 +54,11 @@ namespace WL.Combat.Weapon
|
|||
public float fxRepeatInterval = 0f, fxRepeatSeconds = 0f;
|
||||
public int slashHardRestart = 0, slashGhosts = 0;
|
||||
public float slashGhostInterval = 0.07f, slashGhostHold = 0.03f, slashGhostFade = 0.14f; public float[] slashGhostColor = new float[0];
|
||||
// #960 PD 「비수 주력 스킬 공격 시 총 10회 타격이 가능하도록 애니메이션 재생 타이밍에 적절하게 판정」 — slashHits > 0 = 원본 베기 판정(시전 0.5 s 뒤 0.1 s 간격 6회 · go_effect Loop 6 · 난사와 무관)을 끄고
|
||||
// 난사 휘두름(slashInterval 틱)마다 slashHitDelay 초 뒤(칼이 지나가는 순간) 1회씩 slashHits 회까지 원본 한 대 피해(Skill_ShadowSlash.ProcessCollider · 1회 피해 · 명중 이펙트 · 피격 반응 그대로) ·
|
||||
// 범위 = PC 발 기준 slashHitRadius(0 = 공격 범위 × slashHitRangeMul) + 적 몸 반지름 · 대상(없으면 정면) 쪽 ±slashHitHalfAngle°
|
||||
public int slashHits = 0;
|
||||
public float slashHitDelay = 0.1f, slashHitRadius = 0f, slashHitRangeMul = 1.6f, slashHitHalfAngle = 110f;
|
||||
}
|
||||
|
||||
public static class WLShadowSlash
|
||||
|
|
@ -79,7 +84,7 @@ namespace WL.Combat.Weapon
|
|||
}
|
||||
|
||||
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.AfterSceneLoad)]
|
||||
static void Boot() { s_cfg = null; Casts = Shown = Scaled = Errors = 0; SlashLoops = SlashLeaks = FxShown = FxRepeats = FxSlashes = SlashGhostCount = 0; LastLog = SlashLog = FxRepeatLog = ""; }
|
||||
static void Boot() { s_cfg = null; Casts = Shown = Scaled = Errors = 0; SlashLoops = SlashLeaks = FxShown = FxRepeats = FxSlashes = SlashGhostCount = SlashHits = SlashHitTargets = 0; LastLog = SlashLog = FxRepeatLog = SlashHitLog = ""; }
|
||||
|
||||
public static string Describe() { return "casts " + Casts + " · shown " + Shown + " · scaled " + Scaled + " · err " + Errors + " · fx " + FxShown + "(반복 " + FxRepeats + ") · 잔상 " + SlashGhostCount + " · " + LastLog; }
|
||||
|
||||
|
|
@ -107,7 +112,8 @@ namespace WL.Combat.Weapon
|
|||
if (c.hideOriginalFx != 0 && skill.go_effect != null) foreach (var rd in skill.go_effect.GetComponentsInChildren<Renderer>(true)) rd.enabled = false; // 풀 인스턴스라 한 번 끄면 유지
|
||||
if (c.fxMain != 0) Runner.StartCoroutine(CoFx(shooter, c)); // #956 fxMain 0 = 정면 사본 없이 난사 사본(fxPerSlash)만
|
||||
}
|
||||
if (c.slashStates != null && c.slashStates.Length > 0 && !DSUtil.CheckNull(shooter) && shooter.m_animation != null) Runner.StartCoroutine(CoSlash(shooter, c)); // #955
|
||||
HitMode(skill, c); // #960 원본 6회 판정 끄기(slashHits > 0) / 되돌리기
|
||||
if (c.slashStates != null && c.slashStates.Length > 0 && !DSUtil.CheckNull(shooter) && shooter.m_animation != null) Runner.StartCoroutine(CoSlash(shooter, c, skill)); // #955 · #960 휘두름마다 판정
|
||||
if (c.slashGhosts != 0 && !DSUtil.CheckNull(shooter)) Runner.StartCoroutine(CoGhosts(shooter, c)); // #959 난사 잔상
|
||||
LastLog = "cast " + Casts + " · shown " + Shown + " · scaled " + Scaled;
|
||||
}
|
||||
|
|
@ -169,12 +175,52 @@ namespace WL.Combat.Weapon
|
|||
}
|
||||
}
|
||||
|
||||
public static int SlashHits, SlashHitTargets; public static string SlashHitLog = "", LastMissInfo = "";
|
||||
/// <summary>#960 원본 베기 판정(go_effect · Projectile_StopAndColliderControl Loop 6 · 0.1 s)을 slashHits > 0 이면 끄고(Loop 0) · 0 이면 처음 값으로(풀 인스턴스라 값이 남는다).</summary>
|
||||
static void HitMode(Skill_ShadowSlash skill, WLShadowSlashJson c)
|
||||
{
|
||||
var ge = skill != null ? skill.go_effect : null; if (ge == null) return;
|
||||
var mark = ge.GetComponent<WLShadowSlashHits>();
|
||||
if (c.slashHits > 0)
|
||||
{
|
||||
if (mark == null) { mark = ge.gameObject.AddComponent<WLShadowSlashHits>(); mark.loop0 = ge.Loop; mark.loopTime0 = ge.LoopTime; }
|
||||
ge.Loop = 0; ge.LoopTime = 0f;
|
||||
}
|
||||
else if (mark != null) { ge.Loop = mark.loop0; ge.LoopTime = mark.loopTime0; Object.Destroy(mark); }
|
||||
}
|
||||
|
||||
/// <summary>#960 휘두름 1회 판정 — PC 발 기준 범위 + 적 몸 반지름 · 대상 쪽 ±각 안의 적마다 원본 한 대 피해(대검 각성 WLBladeStorm.Hit 와 같은 방식) · 맞은 수.</summary>
|
||||
static int SlashHit(ProjectileBase pb, Actor shooter, WLShadowSlashJson c)
|
||||
{
|
||||
var info = ActorInfo.Ins; if (info == null || pb == null || DSUtil.CheckNull(shooter)) return 0;
|
||||
var origin = shooter.Get_position();
|
||||
var fwd = shooter.transform.forward; fwd.y = 0f; var tgt = shooter.Get_Target();
|
||||
if (!DSUtil.CheckNull(tgt) && !tgt.IsDead()) { var d = tgt.Get_position() - origin; d.y = 0f; if (d.sqrMagnitude > 1e-4f) fwd = d; }
|
||||
if (fwd.sqrMagnitude < 1e-4f) fwd = Vector3.forward; fwd.Normalize();
|
||||
var pcA = shooter as PCActor; float range = c.slashHitRadius > 0f ? c.slashHitRadius : (pcA != null ? pcA.WL_AttackRange : 1f) * Mathf.Max(0.1f, c.slashHitRangeMul);
|
||||
var list = info.Get_EnemyActors(shooter.IsEnemy(), origin, range + 3f); if (list == null) return 0;
|
||||
int n = 0; LastMissInfo = "";
|
||||
for (int k = 0; k < list.Count; k++)
|
||||
{
|
||||
var a = list[k]; if (a == null || a.IsDead()) continue;
|
||||
var v = a.Get_position() - origin; v.y = 0f;
|
||||
if (k == 0) LastMissInfo = "d" + v.magnitude.ToString("F2") + "/" + (range + WL.Combat.WLCombatMotionSettings.BodyRadius(a)).ToString("F2") + " a" + (v.sqrMagnitude > 1e-4f ? Vector3.Angle(fwd, v) : 0f).ToString("F0");
|
||||
if (v.magnitude > range + WL.Combat.WLCombatMotionSettings.BodyRadius(a)) continue;
|
||||
if (v.sqrMagnitude > 1e-4f && Vector3.Angle(fwd, v) > c.slashHitHalfAngle) continue;
|
||||
var col = a.GetComponent<Collider>(); if (col == null || !col.enabled) continue;
|
||||
if (pb.ProcessCollider(col, false)) n++;
|
||||
}
|
||||
info.Release_Actors(list);
|
||||
return n;
|
||||
}
|
||||
|
||||
public static int SlashLoops, SlashLeaks; public static string SlashLog = "";
|
||||
/// <summary>#955 제자리 난사 — slashSeconds 동안 slashStates 를 번갈아 CrossFade(이벤트 끔 · 배속) · 스킬 상태를 벗어나면 중단 · 끝에 이벤트·배속 원상.</summary>
|
||||
static System.Collections.IEnumerator CoSlash(Actor shooter, WLShadowSlashJson c)
|
||||
/// <summary>#955 제자리 난사 — slashSeconds 동안 slashStates 를 번갈아 CrossFade(이벤트 끔 · 배속) · 스킬 상태를 벗어나면 중단 · 끝에 이벤트·배속 원상. #960 휘두름마다 판정(slashHits).</summary>
|
||||
static System.Collections.IEnumerator CoSlash(Actor shooter, WLShadowSlashJson c, ProjectileBase pb)
|
||||
{
|
||||
var an = shooter.m_animation; if (an == null) yield break;
|
||||
bool prevEvents = an.fireEvents; an.fireEvents = false; float t0 = Time.time; int i = -1, cur = 0; float interval = Mathf.Max(0.05f, c.slashInterval), tickAt = t0;
|
||||
int hits = 0, targets = 0, scheduled = 0; var hitQ = new System.Collections.Generic.Queue<float>(4); var hitTimes = new System.Text.StringBuilder(); // #960 휘두름마다 판정 시각을 쌓는다(프레임 끊김으로 틱 둘이 한 프레임에 와도 빠지지 않게)
|
||||
try
|
||||
{
|
||||
while (Time.time - t0 < c.slashSeconds)
|
||||
|
|
@ -187,6 +233,7 @@ namespace WL.Combat.Weapon
|
|||
if (an.HasState(0, cur)) { if (c.slashHardRestart != 0) an.Play(cur, 0, 0f); else an.CrossFadeInFixedTime(cur, 0.05f, 0, 0f); an.speed = Mathf.Max(0.1f, c.slashSpeed); SlashLoops++; }
|
||||
else SlashLog = "상태 없음 " + st;
|
||||
if (c.fxPerSlash != 0 && !string.IsNullOrEmpty(c.fxPrefab)) SlashFx(shooter, c, i); // #956 휘두르는 손의 날 자리에 작은 사본
|
||||
if (c.slashHits > 0 && scheduled < c.slashHits) { hitQ.Enqueue(Time.time + Mathf.Max(0f, c.slashHitDelay)); scheduled++; } // #960 이 휘두름의 칼이 지나가는 순간
|
||||
}
|
||||
else if (c.slashHardRestart != 0)
|
||||
{
|
||||
|
|
@ -194,11 +241,24 @@ namespace WL.Combat.Weapon
|
|||
bool foreign = an.GetCurrentAnimatorStateInfo(0).shortNameHash != cur || (an.IsInTransition(0) && an.GetNextAnimatorStateInfo(0).shortNameHash != cur);
|
||||
if (foreign) { an.Play(cur, 0, 0f); an.speed = Mathf.Max(0.1f, c.slashSpeed); SlashLeaks++; }
|
||||
}
|
||||
while (hitQ.Count > 0 && Time.time >= hitQ.Peek()) // #960 판정(이번 프레임까지 온 것 모두)
|
||||
{
|
||||
hitQ.Dequeue(); hits++; SlashHits++; int n = SlashHit(pb, shooter, c); targets += n; SlashHitTargets += n;
|
||||
hitTimes.Append((Time.time - t0).ToString("F2")).Append(n > 0 ? "" : "(빗나감 " + LastMissInfo + ")").Append(' ');
|
||||
}
|
||||
yield return null;
|
||||
}
|
||||
// 끝 — 남은 판정 + (끊김으로 휘두름이 모자랐으면) 모자란 만큼
|
||||
if (c.slashHits > 0 && !DSUtil.CheckNull(shooter) && !shooter.IsDead())
|
||||
while (hits < c.slashHits && (hitQ.Count > 0 || scheduled < c.slashHits))
|
||||
{
|
||||
if (hitQ.Count > 0) hitQ.Dequeue(); else scheduled++;
|
||||
hits++; SlashHits++; int n = SlashHit(pb, shooter, c); targets += n; SlashHitTargets += n; hitTimes.Append((Time.time - t0).ToString("F2")).Append("(끝) ");
|
||||
}
|
||||
}
|
||||
finally { if (!DSUtil.CheckNull(shooter) && shooter.m_animation != null) { shooter.m_animation.fireEvents = prevEvents; shooter.m_animation.speed = 1f; } }
|
||||
SlashLog = "난사 " + (i + 1) + "회 · " + (Time.time - t0).ToString("F2") + " s · 되돌림 " + SlashLeaks;
|
||||
if (c.slashHits > 0) SlashHitLog = "판정 " + hits + "회 · 맞힌 적 합 " + targets + " · 시각 " + hitTimes;
|
||||
}
|
||||
|
||||
public static int FxSlashes; public static string FxSlashLog = "";
|
||||
|
|
@ -260,6 +320,8 @@ namespace WL.Combat.Weapon
|
|||
public sealed class WLShadowSlashBase : MonoBehaviour { public Vector3 scale, size, center; public bool hasBox; }
|
||||
/// <summary>#954 코루틴 숙주(씬 밖 · 숨김).</summary>
|
||||
public sealed class WLShadowSlashRunner : MonoBehaviour { }
|
||||
/// <summary>#960 원본 베기 판정을 끈 표식 — 처음 Loop · LoopTime(풀 인스턴스에 남으므로 slashHits 0 으로 되돌릴 때 쓴다).</summary>
|
||||
public sealed class WLShadowSlashHits : MonoBehaviour { public int loop0; public float loopTime0; }
|
||||
/// <summary>#956 난사 사본 추종 — 자리 = 소켓(손잡이) → 날 끝 사이 fraction + 위 yOffset · 방향 = 날 방향의 수평 성분(없으면 PC 정면) · follow 면 매 프레임(LateUpdate · 애니메이션 평가 뒤) 다시 맞춘다.</summary>
|
||||
public sealed class WLShadowSlashFxFollow : MonoBehaviour
|
||||
{
|
||||
|
|
|
|||
|
|
@ -0,0 +1,59 @@
|
|||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// WLSkillTestPlayRedirect.cs — TestMap 을 연 채 Play 하면 스킬 테스트 씬으로 시작 (PD #960 · 2026-10-21)
|
||||
//
|
||||
// PD 원문(#960): 「스킬 테스트 용 씬이 또 실행되지 않는거 같아 확인해보고 수정해」 (#957 에도 같은 증상)
|
||||
// ■ 실측: 두 번 모두 PD 가 Play 한 씬 = Assets/Scenes/TestMap.unity(원본 2025 맵 조각 · 루트 LoadMap[LoadMapMgr] 1개).
|
||||
// 이 씬은 게임 흐름(Title 부팅)이 만든 AddrResourceMgr 로 맵 프리팹을 불러오는 조각이라 혼자 Play 하면 LoadMapMgr.Awake NRE(LoadMapMgr.cs:16)로 아무것도 안 뜬다.
|
||||
// 스킬 테스트 씬은 Assets/WL/SkillTest/WLSkillTest.unity(#947 하네스) — 이 씬은 정상(play_test.sh ready).
|
||||
// ■ 처리: 편집기에서 활성 씬이 WLSkillTest.json redirectScenes 에 든 씬이면 Play 시작 씬을 스킬 테스트 씬으로 바꾼다
|
||||
// (EditorSceneManager.playModeStartScene · Play 가 끝나면 원래 씬이 그대로 열려 있다 · 목록 밖 씬은 영향 없음 · 에디터 전용).
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
using UnityEditor;
|
||||
using UnityEditor.SceneManagement;
|
||||
using UnityEngine;
|
||||
|
||||
namespace WL.SkillTest.Editor
|
||||
{
|
||||
[InitializeOnLoad]
|
||||
public static class WLSkillTestPlayRedirect
|
||||
{
|
||||
public static string LastLog = "";
|
||||
static bool s_set;
|
||||
|
||||
static WLSkillTestPlayRedirect()
|
||||
{
|
||||
EditorApplication.playModeStateChanged -= OnPlayMode;
|
||||
EditorApplication.playModeStateChanged += OnPlayMode;
|
||||
}
|
||||
|
||||
static string[] RedirectScenes()
|
||||
{
|
||||
var ta = Resources.Load<TextAsset>(WLSkillTestHarness.ResourcesPath); if (ta == null) return new string[0];
|
||||
try { var j = JsonUtility.FromJson<WLSkillTestJson>(ta.text); return j != null && j.redirectScenes != null ? j.redirectScenes : new string[0]; }
|
||||
catch (System.Exception) { return new string[0]; }
|
||||
}
|
||||
|
||||
static void OnPlayMode(PlayModeStateChange s)
|
||||
{
|
||||
if (s == PlayModeStateChange.ExitingEditMode)
|
||||
{
|
||||
var active = EditorSceneManager.GetActiveScene().path;
|
||||
if (System.Array.IndexOf(RedirectScenes(), active) >= 0)
|
||||
{
|
||||
var sa = AssetDatabase.LoadAssetAtPath<SceneAsset>(WLSkillTestSceneBuilder.ScenePath);
|
||||
if (sa != null)
|
||||
{
|
||||
EditorSceneManager.playModeStartScene = sa; s_set = true;
|
||||
LastLog = "Play 시작 씬 " + active + " → " + WLSkillTestSceneBuilder.ScenePath;
|
||||
Debug.Log("[WL960] " + active + " 은 원본 맵 조각(혼자 실행 불가)이라 스킬 테스트 씬으로 Play 합니다 — " + WLSkillTestSceneBuilder.ScenePath);
|
||||
}
|
||||
}
|
||||
else if (s_set) { EditorSceneManager.playModeStartScene = null; s_set = false; }
|
||||
}
|
||||
else if (s == PlayModeStateChange.EnteredEditMode && s_set)
|
||||
{
|
||||
EditorSceneManager.playModeStartScene = null; s_set = false; // 다음 Play 는 다시 그때 활성 씬 기준
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
fileFormatVersion: 2
|
||||
guid: af821b628ec287549b6635dd0432509a
|
||||
|
|
@ -22,6 +22,8 @@
|
|||
"numberKeysWeapon": 1,
|
||||
"allAwakened": 1,
|
||||
"manualSkills": 1,
|
||||
"redirectScenes": [ "Assets/Scenes/TestMap.unity" ],
|
||||
"_note_960": "#960 PD 「스킬 테스트 용 씬이 또 실행되지 않는 거 같아」 — 실측(#957 · #960 두 번 다): PD 가 Play 한 씬 = Assets/Scenes/TestMap.unity(원본 맵 조각 · AddrResourceMgr 가 Title 부팅에서만 생겨 LoadMapMgr.Awake NRE) → 이 씬을 연 채 Play 하면 Play 시작 씬을 Assets/WL/SkillTest/WLSkillTest.unity 로 바꾼다(WLSkillTestPlayRedirect · Play 끝나면 TestMap 그대로)",
|
||||
"_note_958": "#958 PD — numberKeysWeapon 1 = 숫자 키 1~9 · 0(10번)이 그 번호 무기로 즉시 교체(번호 = O/P 순환과 같은 종류 순 · 화면 왼쪽 위 목록 · 등급 치트는 이 씬에서 끔) · allAwakened 1 = 모든 무기 각성 상태(주력 · 각성 해금 등급 무시) · manualSkills 1 = 자동 스킬 시전 끔(Q · E 치트키로만 · 그 밖엔 기본 공격만)",
|
||||
"label": 1,
|
||||
"labelFontScale": 0.018,
|
||||
|
|
|
|||
|
|
@ -74,6 +74,7 @@ namespace WL.SkillTest
|
|||
public int numberKeysWeapon = 1; // #958 1 = 숫자 키 1~9 · 0(=10) 이 그 번호 무기로 즉시 교체(번호 = O/P 순환과 같은 종류 순 · 화면 왼쪽 위 목록) · 등급 치트는 이 씬에서 끔
|
||||
public int allAwakened = 1; // #958 1 = 모든 무기 각성 상태(등급과 무관하게 주력 · 각성 해금)
|
||||
public int manualSkills = 1; // #958 1 = 자동 스킬 시전 끔(Q · E 치트키로만 스킬 · 그 밖엔 기본 공격만)
|
||||
public string[] redirectScenes = new string[0]; // #960 이 씬을 연 채 Play 하면 스킬 테스트 씬으로 시작(WLSkillTestPlayRedirect · 원본 맵 조각 TestMap 등)
|
||||
public int label = 1;
|
||||
public float labelFontScale = 0.018f;
|
||||
public float groundSize = 100f;
|
||||
|
|
|
|||
Loading…
Reference in New Issue