WL #983 ③~⑥ — ③ 비수 기본 수리검 scatter(숨긴 경로 호처럼 시전자 둘레 0.8~4.0 m 무작위 자리 · 0.5 s 안 무작위 순서로 12개 → 바깥으로 날며 꺾임 90 → 1080°/s 나선으로 말려 들어가며 작아져 사라짐 · 실측 0.45 s 11개 1.61~5.02 m) ④ 오브 기본(Q) 「전진 낙뢰」(이름 임시 · 무기 표 1057 칸 3 = 운석 소환 703305 직업 맞춤 복사본 · WLStaffEarth 10301 행 line) = 대상 쪽 1.5 · 3.3 · 5.1 m 에 0.3 s 간격 번개 3발 · 각 반경 1.6 m 피해(실측 1.50/3.30/5.09 m · 0.32 s 간격 · 더미 자리 번개 명중) ⑤ 오브 3타 = 투사체 숨기고 대상에 번개 1발(반경 1.2 m) + 손 내지른 자세 0.4 s 유지(WLStaffEarth popups · holdSeconds · 실측 OrbSlam3 n0.66 에서 0.40 s 멈춤) ⑥ 번개 = MasterStylizedFX NewExplosions/LightningExplosion 사본 WL_Fx_OrbLightning(스크린샷 = 위에서 꽂히는 번개 + 파란 돔 + 별 + 불꽃 · 루트 템플릿 층 끔) — 리컴파일 오류 0 · Play 3회 콘솔 오류 0

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
깃 관리자 2026-10-25 13:53:08 +09:00
parent f6d24b8b3a
commit ea972ad350
7 changed files with 73711 additions and 21 deletions

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 77a37a1dd52dcf34db5c05e4b39304c0
PrefabImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -15,7 +15,23 @@
"maxSeconds": 2.5,
"hidePath": 1,
"minHeight": 0.6,
"mode": "spiral",
"mode": "scatter",
"scatterCount": 12,
"scatterWindow": 0.5,
"scatterMinRadius": 0.8,
"scatterMaxRadius": 4.0,
"scatterMinHeight": 0.6,
"scatterMaxHeight": 2.0,
"scatterMinSeconds": 0.55,
"scatterMaxSeconds": 0.85,
"scatterSpeed": 4.0,
"scatterSpeedEnd": 0.3,
"scatterTurnStart": 90.0,
"scatterTurnEnd": 1080.0,
"scatterJitter": 40.0,
"scatterRise": 0.25,
"scatterShrinkFrom": 0.35,
"note_983": "#983 PD 「비수 기본 스킬의 컨셉이 내가 원했던 모습이 아니야. 기존에 숨긴 궤적과 같이 수리검들이 화면을 불규칙하게 생성되어 날아가며 이때 나선형에 가깝게 회전하듯 사라지는 모습을 원했던거야.」 — mode scatter: 숨긴 경로 호(Multiple Slashes = 구 반지름 4 m 안 · 0.5 s 동안 초당 200 · 수명 0.2~0.75 s 실측)처럼 수리검 12개가 시전자 둘레 0.8~4.0 m · 발 위 0.6~2.0 m 무작위 자리(30° 구간마다 1개)에 0.5 s 동안 무작위 순서로 나타나 · 바깥쪽(± 40°)으로 4 m/s 로 날며 꺾임 90 → 1080°/s(제곱)로 커져 나선으로 말려 들어가고 · 속도 0.3 배까지 줄며 · 수명 0.55~0.85 s 의 35 % 부터 크기가 0 으로(나선 끝에서 사라짐) · 경로 호 숨김(hidePath 1) · 수리검 사본 · 70° 눕힘 그대로(PM 값 · 이전 방사형 = mode spiral)",
"spiralCount": 10,
"spiralArms": 1,
"spiralStartRadius": 0.8,

View File

@ -150,6 +150,40 @@
"meteorYaw": 30,
"_note_966": "#966 PD 「메테오 바위의 크기가 너무 작아서 메테오스럽지 않아 1.5배」 — fx 배율 0.3 → 0.45(운석 전체 = 낙하체 · 폭발 함께 1.5 배 · 낙하 시작 높이도 1.5 배 · 충돌 시각은 같음 = 위치·속도가 함께 커짐(Hierarchy) · 판정 반경 1.6 m 그대로) · 폭발 짙은 연기 = impact_stone(짙은 회색 0.13~0.29)을 투사체 연기와 같은 재질·색(알파 0.36)으로(966_apply.cs)",
"_note_963": "#963 PD 「지팡이 각성 = Hyper Casual FX Pack Vol.2 스크린샷 같은 메테오 — 플레이어가 타겟한 대상 주위 5 m 범위에 운석 10개 랜덤 낙하 · 낙하 자리의 적에게 피해 · 맞은 적 0.5 초 스턴」 — 팩 메테오 4종 색 대조(주황 불꽃 + 검은 연기 = 01 · 02 파랑 · 03 빠른 선 · 04 보라) → 사본 WL_Fx_Meteor01(충돌 = 루트 높이 평면 · 바닥 콜라이더 무관 · 편집 모드 실측 0.67 s 에 그 자리 충돌) · 0.12 s 간격 10개 · 대상 둘레 5 m 원 안 무작위(대상이 움직이면 따라감) · 땅에 닿는 0.67 s 에 반경 1.6 m(+ 몸 반지름) 적마다 원본 운석 소환 피해(ProcessCollider · 운석마다 따로) + 기절 0.5 s(WLLaunch · 띄움 0) · 운석은 카메라 정면 오른쪽 30° 쪽 위에서 떨어짐 · 배율 0.3(시작 높이 6 m)"
},
{
"skill": "Skill_MeteoStorm",
"classId": 10301,
"center": "target",
"fx": {
"prefab": "WL/Fx/WL_Fx_OrbLightning",
"scale": 0.45,
"bodyScale": 0,
"offset": [ 0, 0, 0 ],
"euler": [ 0, 0, 0 ],
"life": 1.6,
"fade": 0.3,
"follow": 0,
"socket": "",
"head": 0,
"headPull": 0.08
},
"hitDelay": 0.0,
"spreadSpeed": 0,
"radius": 1.6,
"launchHeight": 0,
"launchSeconds": 0.6,
"stunSeconds": 0,
"endAfter": 0.5,
"meteors": 3,
"meteorArea": 0.0,
"meteorInterval": 0.3,
"meteorImpact": 0.05,
"meteorYaw": 0,
"meteorPattern": "line",
"meteorStart": 1.5,
"meteorStep": 1.8,
"_note_983": "#983 PD 「오브 \"기본 스킬\" 시전 시 플레이어의 전방(공격 대상이 있는 방향)으로 번개 3발이 순차적으로 낙하하며 번개 피해가 전진하며 피해를 입히는 형태로 스킬을 구현해줘. (쾅, 쾅, 쾅! 0.3초 간격으로 전진 된 위치에 순차 낙하하는 번개)」 + 「오브에 사용 할 번개 이펙트는 \"Master Stylized FX\" 에셋의 번개 이펙트를 사용하면 되며, 스크린샷을 참고할 것.」 — 오브 기본(무기 표 1057 칸 3 = 운석 소환 703305 직업 맞춤 복사본 · 해머 기본과 같은 방식) · 이 행 = 직업 10301 만 · 시전 순간 시전자 자리에서 대상 쪽(없으면 정면) 직선 1.5 · 3.3 · 5.1 m 에 0.3 s 간격 번개 3발(meteorPattern line) · 각 번개 0.05 s 뒤 반경 1.6 m(+ 몸 반지름) 적마다 원본 운석 소환 피해(ProcessCollider · 번개마다 따로) · 기절 0 · 번개 = MasterStylizedFX NewExplosions/LightningExplosion 사본 WL_Fx_OrbLightning(위에서 꽂히는 파란 번개 + 파란 구 · 퍼지는 고리 = 바닥에 가려 돔 + 흰 별 + 사방 불꽃 = 스크린샷 · 편집 모드 대조표 Lightnings 5종 · GroundBlast · Explosions 중 선택 · 루트 템플릿 층 끔) · 배율 0.45(PM 값)"
}
],
"popup": {
@ -210,6 +244,59 @@
"_note_967": "#967 PD 「지팡이 공격 마지막 타 — 포즈 지속시간 0.5초 늘리고 · 기본 공격 투사체 대신 메테오(각성 스킬에 사용중인 이펙트) 한 발이 대상에게 떨어지도록」 — meteor 1 = 3타 투사체를 쏘는 순간 넘겨받아 숨기고 대상 자리(meteorArea 0)에 각성과 같은 운석(WL_Fx_Meteor01 · 배율 0.45 · 낙하 0.67 s) 1개 · 땅에 닿는 순간 반경 1.2 m(공격표 3타 aoe_radius_m 과 같은 값) 안 적에게 그 투사체 피해(띄움은 WLStageHitReaction 10302 3행 그대로) · 기절 0 · 바위 솟음(fx)은 메테오 3타에선 띄우지 않음(PM 해석 — 각성 메테오와 같은 겉모습) · 포즈 = CombatAnimMapping 10302 attack3 속도 0.25 → 0.1225(상태 0.53 → 1.09 s · idle 전환 시작 0.48 → 0.98 s · 발사 시각 0.08 s 유지)",
"_note_948": "#948 PD 「3타 바위 크기 절반」 fx.scale 0.3 → 0.15 · 띄움·기절은 WLStageHitReaction 10302 3행(바위 등장 0.2 s 뒤 Fling)으로 옮겨 0 · 갈라짐(crack) 검정 → 브라운은 프리팹 입자 시작색"
},
"popups": [
{
"classId": 10301,
"stateName": "attack3",
"fx": {
"prefab": "",
"scale": 1.0,
"bodyScale": 0,
"offset": [ 0, 0, 0 ],
"euler": [ 0, 0, 0 ],
"life": 1.0,
"fade": 0.0,
"follow": 0,
"socket": ""
},
"launchHeight": 0.0,
"launchSeconds": 0.6,
"stunSeconds": 0.0,
"meteor": 1,
"meteorRule": {
"skill": "OrbAttack3Bolt",
"center": "target",
"fx": {
"prefab": "WL/Fx/WL_Fx_OrbLightning",
"scale": 0.4,
"bodyScale": 0,
"offset": [ 0, 0, 0 ],
"euler": [ 0, 0, 0 ],
"life": 1.6,
"fade": 0.3,
"follow": 0,
"socket": "",
"head": 0,
"headPull": 0.08
},
"hitDelay": 0.0,
"spreadSpeed": 0,
"radius": 1.2,
"launchHeight": 0,
"launchSeconds": 0.6,
"stunSeconds": 0,
"endAfter": 0.3,
"meteors": 1,
"meteorArea": 0.0,
"meteorInterval": 0.0,
"meteorImpact": 0.05,
"meteorYaw": 0
},
"holdDelay": 0.0,
"holdSeconds": 0.4,
"_note_983": "#983 PD 「오브 \"기본 공격\" 마지막 타(3타)에서는 공격 직후 다시 모션을 캔슬하는 모습인데, 공격 상태의 손을 내지른 상태를 잠시 유지한 채 투사체를 날리지 말고 타겟중인 대상에게 번개가 1발을 낙하시켜 줘.」 — 오브(10301) attack3(Mage Jump-Attack1 0~24 프레임 · 발사 이벤트 n 0.66) 투사체를 쏘는 순간 넘겨받아 숨기고(투사체 없음) 대상 자리에 번개 1발(WL_Fx_OrbLightning 0.4 배 · 0.05 s 뒤 반경 1.2 m 그 투사체 피해) + 그 순간 애니메이터를 0.4 s 멈춰 손 내지른 자세 유지(holdSeconds · 그 사이 이동 · 구르기 · 스킬로 상태가 바뀌면 바로 풂)(PM 값)"
}
],
"castCircleClasses": [
10301,
10302,

View File

@ -12,6 +12,7 @@ weapon_id,weapon_name,slot,skill_id,skill_name,skill_class,anim,next_anim,unlock
1071,현자의 지팡이,2,703305,운석 소환,Skill_MeteoStorm,casting,casting_end,7,18,0,10,8.0,Burn,WST:1006,Effect_Burn,5,M:skill_id;skill_name;skill_class;anim;next_anim;range_m;awaken2_extra;vfx;src_cool_ref;weapon_id;weapon_name;fr_ref,,,,
1057,맹약,1,703303,에너지볼 난사,Skill_EnergyBall,casting,casting_end,5,7,0,10,3.6,Burn,WST:1005,Effect_Burn,5,M:skill_id;skill_name;skill_class;anim;next_anim;range_m;awaken2_extra;vfx;src_cool_ref;weapon_id;weapon_name;fr_ref,,,,
1057,맹약,2,703301,블랙홀,Skill_BlackHole,casting,casting_end,7,18,0,10,7.5,Burn,WST:1005,Effect_Burn,5,M:skill_id;skill_name;skill_class;anim;next_anim;range_m;awaken2_extra;vfx;src_cool_ref;weapon_id;weapon_name;fr_ref,,,,
1057,맹약,3,703305,전진 낙뢰,Skill_MeteoStorm,casting,casting_end,3,18,0,6,8.0,Burn,WST:1005,Effect_Burn,5,PD#983 기본 스킬(전방 = 대상 쪽 번개 3발 0.3 s 간격 전진 낙하 · 운석 소환 703305 직업 맞춤 복사본 + WLStaffEarth 10301 행(line · 번개 = MasterStylizedFX LightningExplosion 사본) · 쿨 · 피해 = 지팡이 각성 값 그대로(해머 기본 #975 와 같은 규칙) · 사거리 10 → 6 = 번개 줄 끝 5.1 m · 이름 임시),,,,
9001,수호자의 철퇴,1,703505,십자 가르기,Skill_CrossSlash,crossslash,,5,6,0,2,2.4,Stun,,Effect_Stun,6,M:skill_id;skill_name;skill_class;anim;next_anim;range_m;awaken2_extra;vfx;src_cool_ref,,,,
9001,수호자의 철퇴,2,703506,회전 베기,Skill_SpinSlash,spinslash,,7,16,0,12,6.0,Stun,,Effect_Stun,8,M:skill_id;skill_name;skill_class;anim;next_anim;range_m;awaken2_extra;vfx;src_cool_ref,,,,
9002,들꽃 단궁,1,703401,제멋대로 화살,Skill_UnpredictableArrow,skill1_start,,5,5,0,10,2.0,Slow,,Effect_Slow,5,M:skill_id;skill_name;skill_class;anim;next_anim;range_m;awaken2_extra;vfx;src_cool_ref,,,,

1 weapon_id weapon_name slot skill_id skill_name skill_class anim next_anim unlock_tier cool_s mp range_m dmg_ratio_base awaken2_extra fr_ref vfx src_cool_ref src wl_skill trigger dash_cast first_cast
12 1071 현자의 지팡이 2 703305 운석 소환 Skill_MeteoStorm casting casting_end 7 18 0 10 8.0 Burn WST:1006 Effect_Burn 5 M:skill_id;skill_name;skill_class;anim;next_anim;range_m;awaken2_extra;vfx;src_cool_ref;weapon_id;weapon_name;fr_ref
13 1057 맹약 1 703303 에너지볼 난사 Skill_EnergyBall casting casting_end 5 7 0 10 3.6 Burn WST:1005 Effect_Burn 5 M:skill_id;skill_name;skill_class;anim;next_anim;range_m;awaken2_extra;vfx;src_cool_ref;weapon_id;weapon_name;fr_ref
14 1057 맹약 2 703301 블랙홀 Skill_BlackHole casting casting_end 7 18 0 10 7.5 Burn WST:1005 Effect_Burn 5 M:skill_id;skill_name;skill_class;anim;next_anim;range_m;awaken2_extra;vfx;src_cool_ref;weapon_id;weapon_name;fr_ref
15 1057 맹약 3 703305 전진 낙뢰 Skill_MeteoStorm casting casting_end 3 18 0 6 8.0 Burn WST:1005 Effect_Burn 5 PD#983 기본 스킬(전방 = 대상 쪽 번개 3발 0.3 s 간격 전진 낙하 · 운석 소환 703305 직업 맞춤 복사본 + WLStaffEarth 10301 행(line · 번개 = MasterStylizedFX LightningExplosion 사본) · 쿨 · 피해 = 지팡이 각성 값 그대로(해머 기본 #975 와 같은 규칙) · 사거리 10 → 6 = 번개 줄 끝 5.1 m · 이름 임시)
16 9001 수호자의 철퇴 1 703505 십자 가르기 Skill_CrossSlash crossslash 5 6 0 2 2.4 Stun Effect_Stun 6 M:skill_id;skill_name;skill_class;anim;next_anim;range_m;awaken2_extra;vfx;src_cool_ref
17 9001 수호자의 철퇴 2 703506 회전 베기 Skill_SpinSlash spinslash 7 16 0 12 6.0 Stun Effect_Stun 8 M:skill_id;skill_name;skill_class;anim;next_anim;range_m;awaken2_extra;vfx;src_cool_ref
18 9002 들꽃 단궁 1 703401 제멋대로 화살 Skill_UnpredictableArrow skill1_start 5 5 0 10 2.0 Slow Effect_Slow 5 M:skill_id;skill_name;skill_class;anim;next_anim;range_m;awaken2_extra;vfx;src_cool_ref

View File

@ -7,6 +7,8 @@
// ■ 원본 Skill_BrutalSlash.Co_Update 가 베기 이펙트를 켜는 줄 다음 훅 1줄(Begin) → 그 투사체의 경로 층(layer) 입자 중 every 번째(randomSeed)마다
// 수리검 1개를 붙여 매 LateUpdate 호의 가장자리(입자 위치 + 3D 회전 × 오른쪽 × 메시 반지름 × 크기 × rimFraction)로 — 호가 돌면 수리검이 호를 따라 돈다 · 입자가 사라지면 숨김
// ■ #980 mode "spiral" = 시전자 둘레 나선(회전 · 휘몰아치며 퍼져 나감 · 경로 입자 무관) · faceCamera = 수리검 면이 카메라를 봄
// ■ #983 mode "scatter" = 숨긴 경로 호(구 반지름 4 m 안 무작위 자리 · 0.5 s 동안 불규칙 생성)처럼 시전자 둘레 무작위 자리 · 무작위 시각에 하나씩 나타나
// 바깥쪽으로 날며 점점 세게 꺾여 나선으로 말려 들어가고 크기가 줄며 사라진다(나타난 자리 기준 · 시전자 이동 무관)
// ■ 값 = Resources/WL/WLShurikenRider.json (C45) · 끄기 = enabled 0
// 🔴 Debug.LogError 금지(원본 팝업).
// ─────────────────────────────────────────────────────────────────────────────
@ -42,6 +44,17 @@ namespace WL.Combat.Weapon
public float spiralFadeIn = 0.08f, spiralFadeOut = 0.2f; // 처음 · 끝에 크기 0 ↔ 1(s)
public int faceCamera = 0; // #980 PD 「수리검 이펙트 방향은 카메라 시점 기준 가장 자연스럽게」 — 1 = 수리검 면이 카메라를 봄(제 축으로 회전)
public float faceTilt = 0f; // #980 PD 「스크린샷과 같은 느낌」 — 카메라 정면에서 카메라 오른쪽 축으로 눕히는 각(°) · 70 ≈ 데모처럼 옆에서 본 납작한 원판 + 초승달
// #983 PD 「기존에 숨긴 궤적과 같이 수리검들이 화면을 불규칙하게 생성되어 날아가며 이때 나선형에 가깝게 회전하듯 사라지는 모습」 — mode "scatter"
public int scatterCount = 12; // 수리검 수(각 구간에 1개씩 = 한쪽에 몰리지 않게 · 구간 안 무작위)
public float scatterWindow = 0.5f; // 나타나는 시각 범위(s · 순서 무작위)
public float scatterMinRadius = 0.8f, scatterMaxRadius = 4f; // 나타나는 자리(시전자 기준 수평 m · 넓이 고르게)
public float scatterMinHeight = 0.6f, scatterMaxHeight = 2f; // 나타나는 높이(발 위 m)
public float scatterMinSeconds = 0.55f, scatterMaxSeconds = 0.85f; // 한 수리검이 나타나서 사라지기까지(s · 무작위)
public float scatterSpeed = 4f, scatterSpeedEnd = 0.3f; // 처음 속도(m/s) · 끝 속도 배율
public float scatterTurnStart = 90f, scatterTurnEnd = 1080f; // 꺾임(°/s) 처음 → 끝(제곱으로 커짐) — 끝이 클수록 촘촘한 나선으로 말려 들어감 · 방향 = spiralDir
public float scatterJitter = 40f; // 바깥 방향에서 흔들림(± °)
public float scatterRise = 0.25f; // 날며 오르는 높이(m)
public float scatterShrinkFrom = 0.35f; // 이 비율부터 크기가 줄어 끝에 0(나선 끝에서 사라짐)
public string note = "";
}
@ -50,8 +63,9 @@ namespace WL.Combat.Weapon
public const string ResourcesPath = "WL/WLShurikenRider";
public static int Begun, MaxShown; public static string LastLog = "";
public static float MinY = float.MaxValue, MaxY = float.MinValue; // #979 측정용 — 수리검 높이(시전자 발 기준) 최소 · 최대
public static int Scattered; public static float MinR = float.MaxValue, MaxR; // #983 측정용 — scatter 로 나타난 수 · 나타난 자리 반지름 최소 · 최대
static WLShurikenRiderJson s_cfg;
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)] static void Reset() { s_cfg = null; MinY = float.MaxValue; MaxY = float.MinValue; }
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)] static void Reset() { s_cfg = null; MinY = float.MaxValue; MaxY = float.MinValue; Scattered = 0; MinR = float.MaxValue; MaxR = 0f; }
internal static WLShurikenRiderJson Cfg
{
@ -99,10 +113,31 @@ namespace WL.Combat.Weapon
_pathRenderer = c.hidePath != 0 && r != null && r.enabled ? r : null; if (_pathRenderer != null) _pathRenderer.enabled = false;
_rim = m != null ? Mathf.Max(m.bounds.extents.x, m.bounds.extents.y) : 0.5f; // 호 메시 반지름(입자 크기 1 기준)
var p = Resources.Load<GameObject>(c.prefab);
int need = c.mode == "spiral" ? Mathf.Max(c.maxRiders, c.spiralCount) : c.maxRiders;
int need = c.mode == "spiral" ? Mathf.Max(c.maxRiders, c.spiralCount) : c.mode == "scatter" ? Mathf.Max(c.maxRiders, c.scatterCount) : c.maxRiders;
while (p != null && _riders.Count < need) { var g = Instantiate(p); g.SetActive(false); _riders.Add(g); }
foreach (var g in _riders) if (g != null) { g.SetActive(false); g.transform.localScale = Vector3.one * c.riderScale; }
if (p == null) WLShurikenRider.LastLog = "수리검 프리팹 없음 " + c.prefab;
if (c.mode == "scatter") PlanScatter();
}
// #983 scatter — 수리검마다 나타날 시각 · 자리(각도 · 반지름 · 높이) · 수명 · 첫 방향 흔들림을 미리 정한다(각도는 구간마다 1개 · 시각 순서는 섞음 → 자리와 시각이 따로 논다)
struct Scat { public float t0, ang, r, h, life, jit; public bool on, done; public Vector3 pos; public float head, y0; }
Scat[] _sc;
void PlanScatter()
{
int n = Mathf.Min(Mathf.Max(1, _c.scatterCount), _riders.Count); _sc = new Scat[n];
var order = new int[n]; for (int i = 0; i < n; i++) order[i] = i;
for (int i = n - 1; i > 0; i--) { int j = Random.Range(0, i + 1); int k = order[i]; order[i] = order[j]; order[j] = k; }
float a0 = Random.Range(0f, 360f), r0 = Mathf.Min(_c.scatterMinRadius, _c.scatterMaxRadius), r1 = Mathf.Max(_c.scatterMinRadius, _c.scatterMaxRadius);
for (int i = 0; i < n; i++)
{
_sc[i].ang = a0 + 360f / n * (i + Random.Range(0.15f, 0.85f));
_sc[i].r = Mathf.Sqrt(Mathf.Lerp(r0 * r0, r1 * r1, Random.value)); // 넓이 고르게(가운데 몰림 없음)
_sc[i].h = Random.Range(_c.scatterMinHeight, _c.scatterMaxHeight);
_sc[i].t0 = _c.scatterWindow * (order[i] + Random.value) / n;
_sc[i].life = Mathf.Max(0.05f, Random.Range(_c.scatterMinSeconds, _c.scatterMaxSeconds));
_sc[i].jit = Random.Range(-_c.scatterJitter, _c.scatterJitter);
}
}
void LateUpdate()
@ -110,6 +145,7 @@ namespace WL.Combat.Weapon
if (!_on) return;
if (_ps == null || !_ps.gameObject.activeInHierarchy || Time.time - _t0 > _c.maxSeconds) { Stop(); return; }
if (_c.mode == "spiral") { Spiral(); return; }
if (_c.mode == "scatter") { Scatter(); return; }
int n = _ps.GetParticles(_buf); var alive = new HashSet<uint>(); int shown = 0;
for (int i = 0; i < n; i++)
{
@ -159,6 +195,39 @@ namespace WL.Combat.Weapon
if (!pending) Stop();
}
// #983 scatter — 나타날 시각이 되면 그때 시전자 둘레 자리에 켜고 · 바깥쪽(+ 흔들림)으로 날며 꺾임이 처음 → 끝(제곱)으로 커져 나선으로 말려 들어가고 ·
// 속도는 끝 배율까지 줄고 · scatterShrinkFrom 부터 크기가 0 으로(처음 spiralFadeIn 은 0 → 1) · 다 끝나면 멈춤
void Scatter()
{
if (_sc == null) PlanScatter();
float t = Time.time - _t0, dt = Time.deltaTime; bool pending = false; int shown = 0;
var c0 = _center != null ? _center.position : transform.position; c0.y = _ground; var useFace = _c.faceCamera != 0;
for (int i = 0; i < _sc.Length; i++)
{
var g = _riders[i]; if (g == null || _sc[i].done) continue; pending = true;
float ti = t - _sc[i].t0; if (ti < 0f) continue;
if (!_sc[i].on)
{
float a = _sc[i].ang * Mathf.Deg2Rad; var dir = new Vector3(Mathf.Cos(a), 0f, Mathf.Sin(a));
_sc[i].pos = c0 + dir * _sc[i].r + Vector3.up * _sc[i].h; _sc[i].y0 = _sc[i].pos.y; _sc[i].head = _sc[i].ang + _sc[i].jit; _sc[i].on = true; g.SetActive(true);
WLShurikenRider.Scattered++; WLShurikenRider.MaxR = Mathf.Max(WLShurikenRider.MaxR, _sc[i].r); WLShurikenRider.MinR = Mathf.Min(WLShurikenRider.MinR, _sc[i].r);
}
float u = Mathf.Clamp01(ti / _sc[i].life);
if (ti > _sc[i].life) { _sc[i].done = true; g.SetActive(false); continue; }
float turn = Mathf.Lerp(_c.scatterTurnStart, _c.scatterTurnEnd, u * u), spd = _c.scatterSpeed * Mathf.Lerp(1f, _c.scatterSpeedEnd, u);
_sc[i].head += _c.spiralDir * turn * dt; // 각도 증가 = 위에서 볼 때 반시계(spiral 과 같은 규칙)
float h = _sc[i].head * Mathf.Deg2Rad; var vel = new Vector3(Mathf.Cos(h), 0f, Mathf.Sin(h)) * spd;
_sc[i].pos += vel * dt; _sc[i].pos.y = _sc[i].y0 + _c.scatterRise * u;
float k = Mathf.Min(1f, ti / Mathf.Max(0.01f, _c.spiralFadeIn)) * (1f - Mathf.SmoothStep(0f, 1f, Mathf.InverseLerp(_c.scatterShrinkFrom, 1f, u)));
g.transform.position = _sc[i].pos; g.transform.localScale = Vector3.one * (_c.riderScale * k);
g.transform.rotation = useFace ? Face(_c.faceTilt, _sc[i].pos) : Quaternion.LookRotation(vel.sqrMagnitude > 1e-6f ? vel : Vector3.forward);
WLShurikenRider.MinY = Mathf.Min(WLShurikenRider.MinY, _sc[i].pos.y - _ground); WLShurikenRider.MaxY = Mathf.Max(WLShurikenRider.MaxY, _sc[i].pos.y - _ground);
shown++;
}
if (shown > WLShurikenRider.MaxShown) WLShurikenRider.MaxShown = shown;
if (!pending) Stop();
}
// 수리검 면(사본 루트 +Y)을 카메라 쪽으로 · 앞(+Z) = 카메라 위쪽 — 위에서 내려다보는 카메라에서 납작한 원판이 정면으로 보이고 제 축(AutoRotate)으로 돈다 · tilt° 만큼 카메라 오른쪽 축으로 눕힘
static Quaternion Face(float tilt, Vector3 pos)
{
@ -172,7 +241,7 @@ namespace WL.Combat.Weapon
int FreeSlot() { for (int i = 0; i < _riders.Count; i++) if (!_bySeed.ContainsValue(i)) return i; return -1; }
ParticleSystemRenderer _pathRenderer;
void Stop() { _on = false; _bySeed.Clear(); if (_pathRenderer != null) { _pathRenderer.enabled = true; _pathRenderer = null; } foreach (var g in _riders) if (g != null) g.SetActive(false); WLShurikenRider.LastLog = "끝 · 최대 동시 " + WLShurikenRider.MaxShown; }
void Stop() { _on = false; _bySeed.Clear(); _sc = null; if (_pathRenderer != null) { _pathRenderer.enabled = true; _pathRenderer = null; } foreach (var g in _riders) if (g != null) g.SetActive(false); WLShurikenRider.LastLog = "끝 · 최대 동시 " + WLShurikenRider.MaxShown; }
void OnDisable() { Stop(); }
void OnDestroy() { foreach (var g in _riders) if (g != null) Destroy(g); }
}

View File

@ -62,6 +62,10 @@ namespace WL.Combat.Weapon
// meteorYaw = 운석이 날아오는 쪽(카메라 수평 정면을 이 각만큼 돌린 방향 · 사본 로컬 +Z 위쪽 앞에서 떨어진다)
public int meteors;
public float meteorArea = 5f, meteorInterval = 0.12f, meteorImpact = 0.67f, meteorYaw = 30f;
// #983 PD 「오브 기본 스킬 시전 시 플레이어의 전방(공격 대상이 있는 방향)으로 번개 3발이 순차적으로 낙하하며 번개 피해가 전진하며 피해를 입히는 형태 (쾅, 쾅, 쾅! 0.3초 간격으로 전진 된 위치에 순차 낙하하는 번개)」 —
// meteorPattern "line" = 무작위 대신 시전 순간 시전자 자리에서 대상 쪽(없으면 정면) 직선 위 meteorStart + meteorStep × i(m) 자리에 차례로(이펙트 앞 = 그 방향)
public string meteorPattern = "";
public float meteorStart = 1.5f, meteorStep = 1.8f;
}
/// <summary>#966 PD 「비수 각성 스킬에 시전 모션 및 시전 이펙트 — Hyper Casual FX Pack Vol.2 스크린샷 이펙트를 캐릭터에게 한번 재생시킨 후 발동」 — 이 원본 스킬(e_Skill)의 시전(SkillCast) 순간 시전자에게 fx(시전 시간 + life · follow 1 = 따라감)</summary>
@ -83,6 +87,9 @@ namespace WL.Combat.Weapon
// 숨기고 meteorRule(메테오 방식 · 운석 1개 · 대상 자리)로 떨어뜨려 땅에 닿는 순간 그 투사체 피해 · 이 경우 바위 솟음(fx)은 띄우지 않는다 · 0 = 이전(투사체 + 맞은 자리 바위)
public int meteor;
public WLStaffEarthSkill meteorRule = new WLStaffEarthSkill();
// #983 PD 「오브 "기본 공격" 마지막 타(3타)에서는 공격 직후 다시 모션을 캔슬하는 모습인데, 공격 상태의 손을 내지른 상태를 잠시 유지한 채 투사체를 날리지 말고 타겟중인 대상에게 번개가 1발을 낙하」 —
// holdSeconds > 0 = 넘겨받는 순간(발사 시점) holdDelay 뒤 애니메이터를 holdSeconds 동안 멈춤(자세 유지 · 다른 상태로 바뀌거나 다른 코드가 속도를 바꾸면 바로 손 뗌)
public float holdDelay, holdSeconds;
}
[System.Serializable]
@ -94,6 +101,7 @@ namespace WL.Combat.Weapon
public WLStaffEarthFx auraFx = new WLStaffEarthFx();
public WLStaffEarthSkill[] skills = new WLStaffEarthSkill[0];
public WLStaffEarthPopup popup = new WLStaffEarthPopup();
public WLStaffEarthPopup[] popups = new WLStaffEarthPopup[0]; // #983 popup 과 같은 규칙을 다른 직업에(오브 10301 3타 = 번개 1발 · 자세 유지) — 직업 · 상태가 맞는 첫 규칙(popup 먼저)
// #963 PD 「플레이어가 마법을 시전할 때는 Hyper Casual FX Pack Vol.1 의 마법진을 플레이어가 있는 바닥에 재생되었다가 시전(주황색 계열)」 — castCircleClasses 직업의 스킬 시전(SkillCast)마다 발밑에 castCircle(시전 시간 + life · 따라감)
public int[] castCircleClasses = new int[0];
public WLStaffEarthFx castCircle = new WLStaffEarthFx();
@ -105,12 +113,14 @@ namespace WL.Combat.Weapon
public const string ResourcesPath = "WL/WLStaffEarth";
public static int Casts, TakeOvers, Hits, Launches, Marked, Popups, Errors, Circles, Meteors, SkillCastFxShown, Attack3Meteors; // #967 Attack3Meteors = 3타 투사체를 메테오로 넘겨받은 수
static ProjectileBase s_pendingMeteor; // #967 OnProjectileSet 이 고른 3타 투사체 — 겉모습 훅이 끝난 뒤(AfterProjectileSet) 넘겨받는다
static WLStaffEarthPopup s_pendingPopup; // #983 그 투사체에 맞은 3타 규칙(popup · popups)
public static int Holds; public static string HoldLog = ""; // #983 3타 자세 유지 횟수 · 마지막 기록
public static string MeteorSrc = ""; public static Vector3 MeteorCenter; public static readonly List<Vector3> MeteorPts = new List<Vector3>(), MeteorCenters = new List<Vector3>(); // #964 검증용(메테오 중심 출처 · 낙하 자리)
public static string LastLog = "";
static WLStaffEarthJson s_cfg;
static readonly Dictionary<string, GameObject> s_prefabs = new Dictionary<string, GameObject>();
static readonly HashSet<int> s_marked = new HashSet<int>();
static readonly Dictionary<int, WLStaffEarthPopup> s_marked = new Dictionary<int, WLStaffEarthPopup>();
static WLStaffEarthRunner s_runner;
static MethodInfo s_off;
@ -130,7 +140,7 @@ namespace WL.Combat.Weapon
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.AfterSceneLoad)]
static void Boot()
{
s_cfg = null; s_prefabs.Clear(); s_marked.Clear(); s_pendingMeteor = null;
s_cfg = null; s_prefabs.Clear(); s_marked.Clear(); s_pendingMeteor = null; s_pendingPopup = null;
CombatEvents.SkillCast.Remove(OnSkillCast); CombatEvents.HitConfirmed.Remove(OnHitConfirmed);
if (Cfg.enabled == 0) return;
CombatEvents.SkillCast.Add(OnSkillCast); CombatEvents.HitConfirmed.Add(OnHitConfirmed);
@ -272,12 +282,17 @@ namespace WL.Combat.Weapon
var c = Cfg; if (c.enabled == 0 || pb == null) return;
var pd = pb.Get_ProjectTileData(); var sh = pd != null ? pd.shooter as PCActor : null;
var an = sh != null ? sh.m_animation : null;
// #967 3타 발사 이벤트(attack3 0.08 s)가 attack2 → attack3 전환 중에 나와 현재 상태가 아직 attack2 였다(Play 실측 3타 표시 0) → 전환 중이면 다음 상태도 본다
bool inState = an != null && (an.GetCurrentAnimatorStateInfo(0).IsName(c.popup.stateName) || (an.IsInTransition(0) && an.GetNextAnimatorStateInfo(0).IsName(c.popup.stateName)));
bool mark = sh != null && !pd.IsSkill() && sh.Get_ID() == c.popup.classId && inState;
bool meteor = mark && c.popup.meteor != 0 && c.popup.meteorRule != null && c.popup.meteorRule.meteors > 0;
if (meteor) { s_pendingMeteor = pb; mark = false; } // #967 3타 = 메테오(바위 솟음 표시 안 함)
if (mark) { s_marked.Add(pb.GetInstanceID()); Marked++; } else s_marked.Remove(pb.GetInstanceID());
WLStaffEarthPopup hit = null;
if (sh != null && !pd.IsSkill() && an != null)
for (int i = -1; i < (c.popups != null ? c.popups.Length : 0) && hit == null; i++) // #983 popup 먼저 · 이어서 popups(직업별)
{
var p = i < 0 ? c.popup : c.popups[i]; if (p == null || sh.Get_ID() != p.classId) continue;
// #967 3타 발사 이벤트(attack3 0.08 s)가 attack2 → attack3 전환 중에 나와 현재 상태가 아직 attack2 였다(Play 실측 3타 표시 0) → 전환 중이면 다음 상태도 본다
if (an.GetCurrentAnimatorStateInfo(0).IsName(p.stateName) || (an.IsInTransition(0) && an.GetNextAnimatorStateInfo(0).IsName(p.stateName))) hit = p;
}
bool meteor = hit != null && hit.meteor != 0 && hit.meteorRule != null && hit.meteorRule.meteors > 0;
if (meteor) { s_pendingMeteor = pb; s_pendingPopup = hit; } // #967 3타 = 메테오(바위 솟음 표시 안 함)
if (hit != null && !meteor) { s_marked[pb.GetInstanceID()] = hit; Marked++; } else s_marked.Remove(pb.GetInstanceID());
}
catch (System.Exception ex) { Errors++; LastLog = "3타 표시 실패 — " + ex.Message; }
}
@ -286,12 +301,14 @@ namespace WL.Combat.Weapon
public static void AfterProjectileSet(ProjectileBase pb)
{
if (pb == null || !ReferenceEquals(s_pendingMeteor, pb)) return;
s_pendingMeteor = null;
var rule = s_pendingPopup; s_pendingMeteor = null; s_pendingPopup = null;
try
{
var c = Cfg; var pd = pb.Get_ProjectTileData(); if (pd == null || DSUtil.CheckNull(pd.shooter)) return;
if (rule == null) rule = c.popup;
if (s_runner == null) { var go = new GameObject("[WL935] StaffEarth"); go.hideFlags = HideFlags.HideInHierarchy; Object.DontDestroyOnLoad(go); s_runner = go.AddComponent<WLStaffEarthRunner>(); }
s_runner.Begin(pb, pd, c.popup.meteorRule);
s_runner.Begin(pb, pd, rule.meteorRule);
if (rule.holdSeconds > 0f) s_runner.Hold(pd.shooter as PCActor, rule); // #983 손 내지른 자세 유지
Attack3Meteors++;
}
catch (System.Exception ex) { Errors++; LastLog = "3타 메테오 실패 — " + ex.Message; }
@ -301,9 +318,8 @@ namespace WL.Combat.Weapon
{
try
{
var pb = e.projectile != null ? e.projectile.m_ProjectileBase : null;
if (pb == null || DSUtil.CheckNull(e.victim) || !s_marked.Contains(pb.GetInstanceID())) return;
var p = Cfg.popup;
var pb = e.projectile != null ? e.projectile.m_ProjectileBase : null; WLStaffEarthPopup p;
if (pb == null || DSUtil.CheckNull(e.victim) || !s_marked.TryGetValue(pb.GetInstanceID(), out p) || p == null) return;
var fwd = !DSUtil.CheckNull(e.attacker) ? e.victim.Get_position() - e.attacker.Get_position() : e.victim.transform.forward;
SpawnAt(p.fx, e.attacker, e.victim.Get_position(), fwd, 0f);
Popups++;
@ -330,6 +346,28 @@ namespace WL.Combat.Weapon
{
public void Begin(ProjectileBase pb, ProjectileData pd, WLStaffEarthSkill r) { StartCoroutine(Run(pb, pd, r)); }
/// <summary>#983 3타 자세 유지 — holdDelay 뒤 그 상태(stateName)면 애니메이터를 holdSeconds 동안 멈춘다. 그 사이 상태가 바뀌거나 전환이 시작되거나 다른 코드가 속도를 바꾸면 바로 손 뗀다(내가 멈춘 그대로일 때만 되돌림).</summary>
public void Hold(PCActor pc, WLStaffEarthPopup p) { if (pc != null && pc.m_animation != null && p != null) StartCoroutine(HoldRun(pc, p)); }
IEnumerator HoldRun(PCActor pc, WLStaffEarthPopup p)
{
if (p.holdDelay > 0f) yield return new WaitForSeconds(p.holdDelay);
var an = pc != null ? pc.m_animation : null; if (an == null) yield break;
var st = an.GetCurrentAnimatorStateInfo(0);
if (!st.IsName(p.stateName) || an.IsInTransition(0)) { WLStaffEarth.HoldLog = "안 멈춤(상태 " + (st.IsName(p.stateName) ? "전환 중" : "다름") + ")"; yield break; }
float sp = an.speed, n0 = st.normalizedTime, t = 0f; string why = "끝";
an.speed = 0f; WLStaffEarth.Holds++;
while (t < p.holdSeconds)
{
yield return null; t += Time.deltaTime;
if (pc == null || an == null) { why = "PC 없음"; break; }
if (an.speed != 0f) { why = "다른 코드가 속도 바꿈"; break; }
if (an.IsInTransition(0) || !an.GetCurrentAnimatorStateInfo(0).IsName(p.stateName)) { why = "상태 바뀜"; break; }
}
if (an != null && an.speed == 0f) an.speed = sp > 0f ? sp : 1f;
WLStaffEarth.HoldLog = "멈춤 n" + n0.ToString("F2") + " · " + t.ToString("F2") + " s · " + why;
}
/// <summary>#963 운석 1개 — 땅에 닿는 시각(meteorImpact)에 그 자리 radius 안 적(운석마다 따로 셈)에게 원본 투사체 피해 + 기절.</summary>
IEnumerator MeteorHit(ProjectileBase pb, Actor shooter, Vector3 pt, WLStaffEarthSkill r)
{
@ -355,18 +393,22 @@ namespace WL.Combat.Weapon
Vector3 last = DSUtil.CheckNull(target) ? shooter.Get_position() + shooter.transform.forward * 3f : target.Get_position();
var cam = Camera.main; Vector3 cf = cam != null ? cam.transform.forward : shooter.transform.forward; cf.y = 0f; if (cf.sqrMagnitude < 1e-4f) cf = Vector3.forward;
Vector3 from = Quaternion.Euler(0f, r.meteorYaw, 0f) * cf.normalized;
// #983 직선(오브 기본) — 시전 순간 시전자 자리 · 대상 쪽 방향을 고정하고 그 위로 전진(대상 없으면 시전자 정면)
bool line = r.meteorPattern == "line"; Vector3 lineO = shooter.Get_position(), lineD = !DSUtil.CheckNull(target) && !target.IsDead() ? target.Get_position() - lineO : shooter.transform.forward;
lineD.y = 0f; if (lineD.sqrMagnitude < 1e-4f) { lineD = shooter.transform.forward; lineD.y = 0f; } if (lineD.sqrMagnitude < 1e-4f) lineD = Vector3.forward; lineD.Normalize();
if (line) from = lineD;
for (int i = 0; i < r.meteors; i++)
{
if (r.center != "caster" && !DSUtil.CheckNull(target) && !target.IsDead()) last = target.Get_position();
Vector3 c0 = r.center == "caster" ? shooter.Get_position() : last;
Vector2 d = Random.insideUnitCircle * Mathf.Max(0f, r.meteorArea);
Vector3 pt = new Vector3(c0.x + d.x, c0.y, c0.z + d.y);
Vector3 c0 = line ? lineO : r.center == "caster" ? shooter.Get_position() : last;
Vector2 d = line ? Vector2.zero : Random.insideUnitCircle * Mathf.Max(0f, r.meteorArea);
Vector3 pt = line ? lineO + lineD * (r.meteorStart + r.meteorStep * i) : new Vector3(c0.x + d.x, c0.y, c0.z + d.y);
WLStaffEarth.MeteorPts.Add(pt); WLStaffEarth.MeteorCenters.Add(c0); WLStaffEarth.MeteorCenter = c0; // #964 검증용(낙하 자리 · 그때 중심)
WLStaffEarth.SpawnAt(r.fx, shooter, pt, from, 0f); WLStaffEarth.Meteors++;
StartCoroutine(MeteorHit(pb, shooter, pt, r));
if (r.meteorInterval > 0f && i < r.meteors - 1) yield return new WaitForSeconds(r.meteorInterval);
}
WLStaffEarth.LastLog = r.skill + " 메테오 " + r.meteors + "개 · 반경 " + r.meteorArea + " m";
WLStaffEarth.LastLog = r.skill + " 메테오 " + r.meteors + "개 · " + (line ? "직선 " + r.meteorStart + " + " + r.meteorStep + " m 씩" : "반경 " + r.meteorArea + " m");
yield return new WaitForSeconds(Mathf.Max(0f, r.meteorImpact) + Mathf.Max(0f, r.endAfter));
if (pb != null) { pb.enabled = wasEnabled; WLStaffEarth.OffOriginal(pb); }
foreach (var x in ren) if (x != null) x.enabled = true;