WL #985 PD 지시 3건 — ① 활 투사체에 시위 화살과 같은 화살 메시(WL_NockArrow 0.7 m · 촉 0.15 m 앞)를 자식으로(WLProjectileVisual arrow · 10401 · 10402 · 실측 진행 방향과 0.0° · 경계 0.70 m · 3타 강화 화살에도 보임) ② 비수 기본 수리검 = 몸 둘레 0.4~0.9 m 에서 0.3 s 안에 12개가 11 m/s 로 퍼짐(제 축 회전 1.8 배) · 궤적 되살림 = 경로 호를 날아가는 수리검 자리에서 뿜음(pathFollow) · 작아질 때 녹색 터짐(PunchHitGreen 사본) · 지나간 적 몸에 녹색 별 섬광(ShurikensHit 사본) + 카메라 흔들림 · 명중 경직 0.15 s(WLSkillHitFeel) · 실측 0.25 s 11개 0.86~2.77 m → 0.45 s 12개 2.52~4.05 m · 수리검마다 끝 터짐 1 ③ 오브 기본 번개 = 돔 투명도 0.65 → 0.29 · 별 · 줄기 0.6~0.9 배 · 검은 바닥 균열 · 바닥 번개 무늬 끔 · 한손검 각성(돌진 베기) 하늘색 충격파 고리(shock02 두 층 · 6 배) · 맞은 대상 검은 바위 폭발(WL_Hit_StaffSkill) → 번개 피격(LightingHit 사본 · 오브 기본만) — 리컴파일 오류 0 · Play 2회 콘솔 오류 0

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
깃 관리자 2026-10-26 01:06:21 +09:00
parent 2b6bf964e9
commit 945aafa843
13 changed files with 93262 additions and 34 deletions

File diff suppressed because it is too large Load Diff

View File

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

View File

@ -108,6 +108,17 @@
"offsetCompensate": true, "offsetCompensate": true,
"_note_967": "#967 PD 「지팡이 각성 메테오가 떨어진 후 폭파 연기가 너무 짙은 검은색에 가까운데 자연스러운 흙먼지와 같은 반투명한 브라운 계열로」 — Play 끄기 실측(967_diag2 · 캡처 967h_k1_*): 검은 연기 · 검은 돌 = 운석마다 대상 몸에 뜨는 아래 행 WL_Hit_StaffSkill(먼지 회색 0.42→0.21 · 돌 0.28~0.36) → 메테오만 갈색 사본(먼지 반투명 갈색 α0.6→0.45 · 돌 갈색 · 만들기 scripts/wl/probes/967_meteorhit.cs) · 오브 · 마법책 · 지팡이 주력 피격은 그대로" "_note_967": "#967 PD 「지팡이 각성 메테오가 떨어진 후 폭파 연기가 너무 짙은 검은색에 가까운데 자연스러운 흙먼지와 같은 반투명한 브라운 계열로」 — Play 끄기 실측(967_diag2 · 캡처 967h_k1_*): 검은 연기 · 검은 돌 = 운석마다 대상 몸에 뜨는 아래 행 WL_Hit_StaffSkill(먼지 회색 0.42→0.21 · 돌 0.28~0.36) → 메테오만 갈색 사본(먼지 반투명 갈색 α0.6→0.45 · 돌 갈색 · 만들기 scripts/wl/probes/967_meteorhit.cs) · 오브 · 마법책 · 지팡이 주력 피격은 그대로"
}, },
{
"classIds": [
10301
],
"skills": [ "Skill_MeteoStorm" ],
"prefab": "WL_Hit_OrbLightning",
"scale": 1.0,
"fit": 1.4,
"offsetCompensate": true,
"_note_985": "#985 PD 「오브 기본 스킬 이펙트가 … 지면의 바닥 충격파 이미지는 기존 이펙트와 달라서 이질감이 커」 — Play 캡처: 오브 기본(운석 소환 직업 맞춤 복사본) 번개가 맞힌 대상 몸에 아래 행 WL_Hit_StaffSkill(검은 연기 · 검은 돌)이 떠 번개와 따로 노는 어두운 폭발 → 오브 기본만 번개 피격(MasterStylizedFX Lightnings/LightingHit 사본 · 흰 별 + 전기 줄기) · 오브 다른 스킬 · 마법책 · 지팡이는 그대로"
},
{ {
"classIds": [ "classIds": [
10301, 10301,

View File

@ -1,6 +1,7 @@
{ {
"enabled": 1, "enabled": 1,
"rules": [ "rules": [
{ "skill": "Skill_BrutalSlash", "pushScale": 0.05, "knockbackScale": 0.0, "staggerSeconds": 0.15, "_note_985": "#985 PD 「비수 기본 스킬 … 좀 더 화려하고 타격감을 느껴질 수 있게」 — 무자비한 일격 명중 반응 = 다른 비수 스킬(그림자 베기 · 환영 난무)과 같은 경직 0.15 s · 밀림 0.05(PM 값)" },
{ "skill": "Skill_DarkAmbush", "pushScale": 0.0, "knockbackScale": 0.0, "staggerSeconds": 0.15, "_note_967": "#967 PD 「비수 각성 스킬 사용 시 적이 밀려나는 현상이 없도록」 — 각성(환영 난무) 판정 = 원본 Skill_DarkAmbush 충돌체(0.25 s 뒤 1.75 s) · 맞을 때마다 원본 넉백(f_KnockbackTime + AttackKnockBack) + 피격 밀기(MobStagger)가 걸렸다 → 넉백 시작 안 함 · 밀기 × 0 · 맞는 반응은 경직 0.15 s" }, { "skill": "Skill_DarkAmbush", "pushScale": 0.0, "knockbackScale": 0.0, "staggerSeconds": 0.15, "_note_967": "#967 PD 「비수 각성 스킬 사용 시 적이 밀려나는 현상이 없도록」 — 각성(환영 난무) 판정 = 원본 Skill_DarkAmbush 충돌체(0.25 s 뒤 1.75 s) · 맞을 때마다 원본 넉백(f_KnockbackTime + AttackKnockBack) + 피격 밀기(MobStagger)가 걸렸다 → 넉백 시작 안 함 · 밀기 × 0 · 맞는 반응은 경직 0.15 s" },
{ "skill": "Skill_ShadowSlash", "pushScale": 0.05, "knockbackScale": 0.0, "staggerSeconds": 0.15, "_note_962": "#962 PD 「비수 주력 사용 시 판정이 발생되지 않는다」 — Play 실측: 판정은 나지만 맞을 때마다 원본 넉백 + 피격 밀기로 더미가 2 s 동안 1.0 → 3.4 m 밀려 제자리 난사 이펙트 밖으로 나가고 4.0 m 를 넘으면 빗나감 → 넉백 끔 · 밀기 × 0.05 · 맞을 때마다 경직 0.15 s(맞는 반응이 보이게)" }, { "skill": "Skill_ShadowSlash", "pushScale": 0.05, "knockbackScale": 0.0, "staggerSeconds": 0.15, "_note_962": "#962 PD 「비수 주력 사용 시 판정이 발생되지 않는다」 — Play 실측: 판정은 나지만 맞을 때마다 원본 넉백 + 피격 밀기로 더미가 2 s 동안 1.0 → 3.4 m 밀려 제자리 난사 이펙트 밖으로 나가고 4.0 m 를 넘으면 빗나감 → 넉백 끔 · 밀기 × 0.05 · 맞을 때마다 경직 0.15 s(맞는 반응이 보이게)" },
{ {

File diff suppressed because it is too large Load Diff

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

@ -66,6 +66,11 @@
"chargedScale": 1.0, "chargedScale": 1.0,
"chargedReplaces": true, "chargedReplaces": true,
"hideMesh": true, "hideMesh": true,
"arrow": "WL/Fx/WL_NockArrow",
"arrowLength": 0.7,
"arrowWidth": 1.0,
"arrowTip": 0.15,
"_note_985": "#985 PD 「활 투사체에 화살이 같이 보이는게 좋을 것 같아. (화살과 전혀 동떨어진 투사체로 보여서 어색해)」 — 날아가는 투사체에 시위 화살(WLNockArrow)과 같은 화살 메시 WL_NockArrow(FX_Shoot_Arrow 화살 · #979)를 자식으로(길이 0.7 m = 시위 화살과 같음 · 촉이 투사체 자리 0.15 m 앞 · 빛 줄기(visual) · 3타 강화 겉모습은 그대로 · 원본 화살 메시는 계속 숨김)(PM 값)",
"hitFx": "WL/Impact/WL_Hit_Bow", "hitFx": "WL/Impact/WL_Hit_Bow",
"hitScale": 0.28, "hitScale": 0.28,
"hitLife": 1.5 "hitLife": 1.5
@ -83,6 +88,11 @@
"chargedScale": 1.0, "chargedScale": 1.0,
"chargedReplaces": true, "chargedReplaces": true,
"hideMesh": true, "hideMesh": true,
"arrow": "WL/Fx/WL_NockArrow",
"arrowLength": 0.7,
"arrowWidth": 1.0,
"arrowTip": 0.15,
"_note_985": "#985 PD 「활 투사체에 화살이 같이 보이는게 좋을 것 같아. (화살과 전혀 동떨어진 투사체로 보여서 어색해)」 — 날아가는 투사체에 시위 화살(WLNockArrow)과 같은 화살 메시 WL_NockArrow(FX_Shoot_Arrow 화살 · #979)를 자식으로(길이 0.7 m = 시위 화살과 같음 · 촉이 투사체 자리 0.15 m 앞 · 빛 줄기(visual) · 3타 강화 겉모습은 그대로 · 원본 화살 메시는 계속 숨김)(PM 값)",
"hitFx": "WL/Impact/WL_Hit_Bow", "hitFx": "WL/Impact/WL_Hit_Bow",
"hitScale": 0.28, "hitScale": 0.28,
"hitLife": 1.5 "hitLife": 1.5

View File

@ -13,24 +13,41 @@
"riderScale": 0.4, "riderScale": 0.4,
"rimFraction": 1.0, "rimFraction": 1.0,
"maxSeconds": 2.5, "maxSeconds": 2.5,
"hidePath": 1, "hidePath": 0,
"minHeight": 0.6, "minHeight": 0.6,
"mode": "scatter", "mode": "scatter",
"scatterCount": 12, "scatterCount": 12,
"scatterWindow": 0.5, "scatterWindow": 0.3,
"scatterMinRadius": 0.8, "scatterMinRadius": 0.4,
"scatterMaxRadius": 4.0, "scatterMaxRadius": 0.9,
"scatterMinHeight": 0.6, "scatterMinHeight": 0.7,
"scatterMaxHeight": 2.0, "scatterMaxHeight": 1.4,
"scatterMinSeconds": 0.55, "scatterMinSeconds": 0.45,
"scatterMaxSeconds": 0.85, "scatterMaxSeconds": 0.6,
"scatterSpeed": 4.0, "scatterSpeed": 11.0,
"scatterSpeedEnd": 0.3, "scatterSpeedEnd": 0.35,
"scatterTurnStart": 90.0, "scatterTurnStart": 30.0,
"scatterTurnEnd": 1080.0, "scatterTurnEnd": 720.0,
"scatterJitter": 40.0, "scatterJitter": 25.0,
"scatterRise": 0.25, "scatterRise": 0.2,
"scatterShrinkFrom": 0.35, "scatterShrinkFrom": 0.75,
"pathFollow": 1,
"pathRate": 30.0,
"pathSizeMin": 0.5,
"pathSizeMax": 1.0,
"endFx": "WL/Fx/WL_Fx_ShurikenEnd",
"endFxScale": 0.25,
"endFxLife": 1.0,
"hitFx": "WL/Fx/WL_Fx_ShurikenHit",
"hitFxScale": 0.12,
"hitFxLife": 1.0,
"hitRadius": 0.6,
"releaseShake": 0.08,
"releaseShakeSeconds": 0.15,
"hitShake": 0.05,
"hitShakeSeconds": 0.06,
"spinMul": 1.8,
"note_985": "#985 PD 「비수 기본 스킬 시전 시 비수가 너무 느리게 동작하는 것 같고, 갑자기 사라지는 느낌이야. (캐릭터 주위로 펴져나가야 해) 비수 위치를 고려해 궤적 연출을 다시 살리고 좀 더 화려하고 타격감을 느껴질 수 있게 연출해줘.」 — scatter 를 시전자 몸 둘레 0.4~0.9 m 에서 0.3 s 안에 12개가 나와 바깥으로 11 m/s(끝 0.35 배)로 퍼지며 꺾임 30 → 720°/s 로 끝에서 말려 들고 수명 0.45~0.6 s(이전 4 m/s · 0.55~0.85 s · 0.8~4 m 무작위 자리) · 제 축 회전 1.8 배 · 궤적 되살림 = 경로 호(Multiple Slashes)를 원래 무작위 자리 대신 날아가는 수리검 자리에서 수리검마다 초당 30개(크기 0.5~1.0 · hidePath 0 · pathFollow 1) · 75 % 에서 작아지기 시작할 때 그 자리 녹색 터짐(MasterStylizedFX PunchHitGreen 사본 WL_Fx_ShurikenEnd 0.25 배) · 지나가며 적 몸(반지름 + 0.6 m)에 닿으면 녹색 별 섬광(MasterStylizedFX ShurikensHit 사본 WL_Fx_ShurikenHit 0.12 배 · 수리검 · 적 쌍마다 1번 · 피해는 원본 그대로) + 카메라 흔들림 0.05 · 처음 퍼지는 순간 흔들림 0.08(PM 값)",
"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)", "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, "spiralCount": 10,
"spiralArms": 1, "spiralArms": 1,

View File

@ -34,6 +34,10 @@ namespace WL.Combat.Weapon
public bool chargedReplaces; public bool chargedReplaces;
// #917 PD 「화살 투사체 오브젝트 및 활 히트 이펙트를 교체(Casual RPG VFX)」 — 원래 화살 메시를 숨기고(hideMesh) · 이 투사체의 명중 이펙트(hitFx · 몸 중심 · 몸 크기 비례 hitScale) // #917 PD 「화살 투사체 오브젝트 및 활 히트 이펙트를 교체(Casual RPG VFX)」 — 원래 화살 메시를 숨기고(hideMesh) · 이 투사체의 명중 이펙트(hitFx · 몸 중심 · 몸 크기 비례 hitScale)
public bool hideMesh; public bool hideMesh;
// #985 PD 「활 투사체에 화살이 같이 보이는게 좋을 것 같아. (화살과 전혀 동떨어진 투사체로 보여서 어색해)」 — arrow = 날아가는 투사체에 시위 화살과 같은 화살 메시(WL_NockArrow · +Z = 촉)를 자식으로
// 길이 arrowLength(m · 월드) · 굵기 배율 arrowWidth · 촉 = 투사체 자리에서 앞으로 arrowTip(m) · 강화(3타) 겉모습으로 바뀌어도 그대로 보인다
public string arrow = "";
public float arrowLength = 0.7f, arrowWidth = 1f, arrowTip = 0f;
// #934 PD 「지팡이 기본 공격 투사체에 세로로 긴 이펙트가 숨겨져 있는 것 같아. 세로로 긴 화살형 이펙트는 보이지 않게 해줘.」 — 원본 투사체 안 자식(투사체 기준 경로)의 렌더러를 끈다(풀 재사용에도 꺼진 채) // #934 PD 「지팡이 기본 공격 투사체에 세로로 긴 이펙트가 숨겨져 있는 것 같아. 세로로 긴 화살형 이펙트는 보이지 않게 해줘.」 — 원본 투사체 안 자식(투사체 기준 경로)의 렌더러를 끈다(풀 재사용에도 꺼진 채)
public string[] hideNames = new string[0]; public string[] hideNames = new string[0];
public string hitFx = ""; public string hitFx = "";
@ -211,6 +215,7 @@ namespace WL.Combat.Weapon
// #948 — 덧씌움 자식(~WL_Visual/…)도 hideNames 로 끌 수 있게 붙인 뒤 한 번 더(위 첫 패스는 덧씌움이 생기기 전이라 원본 자식만 잡는다) // #948 — 덧씌움 자식(~WL_Visual/…)도 hideNames 로 끌 수 있게 붙인 뒤 한 번 더(위 첫 패스는 덧씌움이 생기기 전이라 원본 자식만 잡는다)
if (rule.hideNames != null) foreach (var hn in rule.hideNames) { var ht = string.IsNullOrEmpty(hn) ? null : pb.transform.Find(hn); if (ht != null) foreach (var hr in ht.GetComponentsInChildren<Renderer>(true)) { if (hr.enabled) { hr.enabled = false; hid++; } } } if (rule.hideNames != null) foreach (var hn in rule.hideNames) { var ht = string.IsNullOrEmpty(hn) ? null : pb.transform.Find(hn); if (ht != null) foreach (var hr in ht.GetComponentsInChildren<Renderer>(true)) { if (hr.enabled) { hr.enabled = false; hid++; } } }
} }
if (!string.IsNullOrEmpty(rule.arrow)) AttachArrow(pb, rule); // #985 화살 메시
// #839 — 강화 화살: 글로우 자식(꺼둠) + WLChargedMark(켜질 때 창 열려 있으면 적용) // #839 — 강화 화살: 글로우 자식(꺼둠) + WLChargedMark(켜질 때 창 열려 있으면 적용)
if (!string.IsNullOrEmpty(rule.chargedVisual)) if (!string.IsNullOrEmpty(rule.chargedVisual))
{ {
@ -227,6 +232,24 @@ namespace WL.Combat.Weapon
return true; return true;
} }
public static int Arrows; public static string LastArrow = "";
/// <summary>#985 화살 메시 자식 — 메시 경계로 길이(꼬리 → 촉)를 재서 월드 길이 arrowLength 가 되게(투사체 루트 배율 보정) · 촉을 투사체 자리 + arrowTip 에.</summary>
static void AttachArrow(ProjectileBase pb, WLProjectileVisualRule rule)
{
var go = InstantiateVisual(rule.arrow, pb.transform, 1f, "~WL_Arrow"); if (go == null) { LastArrow = "화살 프리팹 없음 " + rule.arrow; return; }
float zmin = float.MaxValue, zmax = float.MinValue;
foreach (var mf in go.GetComponentsInChildren<MeshFilter>(true))
{
if (mf.sharedMesh == null) continue; var b = mf.sharedMesh.bounds; var m = go.transform.worldToLocalMatrix * mf.transform.localToWorldMatrix;
for (int i = 0; i < 8; i++) { var v = m.MultiplyPoint3x4(new Vector3((i & 1) == 0 ? b.min.x : b.max.x, (i & 2) == 0 ? b.min.y : b.max.y, (i & 4) == 0 ? b.min.z : b.max.z)); zmin = Mathf.Min(zmin, v.z); zmax = Mathf.Max(zmax, v.z); }
}
float len = zmin < zmax ? zmax - zmin : 1f, s = rule.arrowLength / Mathf.Max(1e-3f, len); var ls = pb.transform.lossyScale;
float lx = Mathf.Max(1e-4f, Mathf.Abs(ls.x)), ly = Mathf.Max(1e-4f, Mathf.Abs(ls.y)), lz = Mathf.Max(1e-4f, Mathf.Abs(ls.z));
go.transform.localScale = new Vector3(s * rule.arrowWidth / lx, s * rule.arrowWidth / ly, s / lz);
go.transform.localPosition = new Vector3(0f, 0f, (rule.arrowTip - s * (zmin < zmax ? zmax : 0f)) / lz); // 촉(zmax)이 투사체 자리 + arrowTip
Arrows++; LastArrow = pb.name + " 화살 " + rule.arrowLength + " m (원본 " + len.ToString("F2") + " · 배율 " + s.ToString("F3") + ")";
}
public static int Thinned; public static int Thinned;
/// <summary>#943-② 굵기만 t 배 — 입자 시작 크기(3D 면 X·Y 만) · 늘림 빌보드는 길이 보정(lengthScale ÷ t) · 입자 궤적 폭 · TrailRenderer/LineRenderer 폭. 길이(진행 방향)는 그대로.</summary> /// <summary>#943-② 굵기만 t 배 — 입자 시작 크기(3D 면 X·Y 만) · 늘림 빌보드는 길이 보정(lengthScale ÷ t) · 입자 궤적 폭 · TrailRenderer/LineRenderer 폭. 길이(진행 방향)는 그대로.</summary>
static void ApplyThickness(GameObject go, float t) static void ApplyThickness(GameObject go, float t)

View File

@ -55,6 +55,17 @@ namespace WL.Combat.Weapon
public float scatterJitter = 40f; // 바깥 방향에서 흔들림(± °) public float scatterJitter = 40f; // 바깥 방향에서 흔들림(± °)
public float scatterRise = 0.25f; // 날며 오르는 높이(m) public float scatterRise = 0.25f; // 날며 오르는 높이(m)
public float scatterShrinkFrom = 0.35f; // 이 비율부터 크기가 줄어 끝에 0(나선 끝에서 사라짐) public float scatterShrinkFrom = 0.35f; // 이 비율부터 크기가 줄어 끝에 0(나선 끝에서 사라짐)
// #985 PD 「비수가 너무 느리게 동작 · 갑자기 사라지는 느낌 · 캐릭터 주위로 퍼져나가야 · 비수 위치를 고려해 궤적 연출을 다시 살리고 · 좀 더 화려하고 타격감」
public int pathFollow = 0; // 1 = 경로 호(layer)를 원래 무작위 자리 대신 날아가는 수리검 자리에서 뿜음(호 자체 방출은 끄고 끝나면 되살림 · hidePath 0 과 함께)
public float pathRate = 30f; // 수리검 1개당 초당 호 수
public float pathSizeMin = 0.6f, pathSizeMax = 1.2f; // 뿜는 호 크기(원본 시작 크기 1~3)
public string endFx = ""; // 수리검이 작아지기 시작할 때(scatterShrinkFrom) 그 자리에 터지는 이펙트(Resources) — 「갑자기 사라짐」 대신 터지며 사라짐
public float endFxScale = 0.2f, endFxLife = 1f;
public string hitFx = ""; // 수리검이 적 몸 hitRadius 안을 지날 때 그 적 몸에(수리검 · 적 쌍마다 1번 · 보이는 타격 · 피해는 원본 스킬 그대로)
public float hitFxScale = 0.2f, hitFxLife = 1f, hitRadius = 0.8f;
public float releaseShake = 0f, releaseShakeSeconds = 0.15f; // 수리검이 처음 나오는 순간 카메라 흔들림(진폭 · 0 = 없음 · 주파수 · 축 = WLHitFeelSettings)
public float hitShake = 0f, hitShakeSeconds = 0.08f; // 명중 섬광마다 카메라 흔들림
public float spinMul = 1f; // 수리검 제 축 회전(사본 AutoRotate) 배율
public string note = ""; public string note = "";
} }
@ -64,8 +75,9 @@ namespace WL.Combat.Weapon
public static int Begun, MaxShown; public static string LastLog = ""; public static int Begun, MaxShown; public static string LastLog = "";
public static float MinY = float.MaxValue, MaxY = float.MinValue; // #979 측정용 — 수리검 높이(시전자 발 기준) 최소 · 최대 public static float MinY = float.MaxValue, MaxY = float.MinValue; // #979 측정용 — 수리검 높이(시전자 발 기준) 최소 · 최대
public static int Scattered; public static float MinR = float.MaxValue, MaxR; // #983 측정용 — scatter 로 나타난 수 · 나타난 자리 반지름 최소 · 최대 public static int Scattered; public static float MinR = float.MaxValue, MaxR; // #983 측정용 — scatter 로 나타난 수 · 나타난 자리 반지름 최소 · 최대
public static int PathEmitted, EndBursts, HitBursts, Shakes; public static float MaxReach; // #985 측정용 — 수리검 자리에서 뿜은 호 · 끝 터짐 · 명중 섬광 · 흔들림 · 시전자에서 가장 먼 거리
static WLShurikenRiderJson s_cfg; static WLShurikenRiderJson s_cfg;
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)] static void Reset() { s_cfg = null; MinY = float.MaxValue; MaxY = float.MinValue; Scattered = 0; MinR = float.MaxValue; MaxR = 0f; } [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)] static void Reset() { s_cfg = null; MinY = float.MaxValue; MaxY = float.MinValue; Scattered = 0; MinR = float.MaxValue; MaxR = 0f; PathEmitted = EndBursts = HitBursts = Shakes = 0; MaxReach = 0f; }
internal static WLShurikenRiderJson Cfg internal static WLShurikenRiderJson Cfg
{ {
@ -94,7 +106,7 @@ namespace WL.Combat.Weapon
if (path == null) { LastLog = "경로 층 없음 " + c.layer; return; } if (path == null) { LastLog = "경로 층 없음 " + c.layer; return; }
var run = pb.GetComponent<WLShurikenRiderRunner>(); if (run == null) run = pb.gameObject.AddComponent<WLShurikenRiderRunner>(); var run = pb.GetComponent<WLShurikenRiderRunner>(); if (run == null) run = pb.gameObject.AddComponent<WLShurikenRiderRunner>();
bool has = pd != null && !DSUtil.CheckNull(pd.shooter); bool has = pd != null && !DSUtil.CheckNull(pd.shooter);
run.Begin(path, c, has ? pd.shooter.Get_position().y : pb.transform.position.y, has ? pd.shooter.transform : pb.transform); Begun++; run.Begin(path, c, has ? pd.shooter.Get_position().y : pb.transform.position.y, has ? pd.shooter.transform : pb.transform, has ? pd.shooter : null); Begun++;
} }
catch (System.Exception ex) { LastLog = "실패 — " + ex.Message; } catch (System.Exception ex) { LastLog = "실패 — " + ex.Message; }
} }
@ -104,17 +116,31 @@ namespace WL.Combat.Weapon
{ {
ParticleSystem _ps; WLShurikenRiderJson _c; ParticleSystem.Particle[] _buf = new ParticleSystem.Particle[256]; ParticleSystem _ps; WLShurikenRiderJson _c; ParticleSystem.Particle[] _buf = new ParticleSystem.Particle[256];
readonly List<GameObject> _riders = new List<GameObject>(); readonly Dictionary<uint, int> _bySeed = new Dictionary<uint, int>(); readonly List<GameObject> _riders = new List<GameObject>(); readonly Dictionary<uint, int> _bySeed = new Dictionary<uint, int>();
float _t0, _rim = 0.5f, _ground; bool _on; Transform _center; float _t0, _rim = 0.5f, _ground; bool _on; Transform _center; Actor _shooter;
bool _pathEmOff, _pathEmWas, _shook; float[] _emitAcc; bool[] _endDone; HashSet<long> _hitPairs = new HashSet<long>();
public void Begin(ParticleSystem ps, WLShurikenRiderJson c, float ground, Transform center) public void Begin(ParticleSystem ps, WLShurikenRiderJson c, float ground, Transform center, Actor shooter)
{ {
_ps = ps; _c = c; _t0 = Time.time; _on = true; _bySeed.Clear(); _ground = ground; _center = center; _ps = ps; _c = c; _t0 = Time.time; _on = true; _bySeed.Clear(); _ground = ground; _center = center; _shooter = shooter; _shook = false; _hitPairs.Clear();
var r = ps.GetComponent<ParticleSystemRenderer>(); var m = r != null ? r.mesh : null; var r = ps.GetComponent<ParticleSystemRenderer>(); var m = r != null ? r.mesh : null;
_pathRenderer = c.hidePath != 0 && r != null && r.enabled ? r : null; if (_pathRenderer != null) _pathRenderer.enabled = false; _pathRenderer = c.hidePath != 0 && r != null && r.enabled ? r : null; if (_pathRenderer != null) _pathRenderer.enabled = false;
if (c.mode == "scatter" && c.pathFollow != 0) // #985 경로 호를 수리검 자리에서만 — 원래 방출(구 반지름 4 m 무작위)은 끄고 이미 나온 것도 지운다(끝나면 되살림)
{
var em = ps.emission; _pathEmWas = em.enabled; em.enabled = false; _pathEmOff = true; ps.Clear(false);
}
_rim = m != null ? Mathf.Max(m.bounds.extents.x, m.bounds.extents.y) : 0.5f; // 호 메시 반지름(입자 크기 1 기준) _rim = m != null ? Mathf.Max(m.bounds.extents.x, m.bounds.extents.y) : 0.5f; // 호 메시 반지름(입자 크기 1 기준)
var p = Resources.Load<GameObject>(c.prefab); var p = Resources.Load<GameObject>(c.prefab);
int need = c.mode == "spiral" ? Mathf.Max(c.maxRiders, c.spiralCount) : c.mode == "scatter" ? Mathf.Max(c.maxRiders, c.scatterCount) : 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); } while (p != null && _riders.Count < need)
{
var g = Instantiate(p); g.SetActive(false); _riders.Add(g);
if (!Mathf.Approximately(c.spinMul, 1f)) // #985 제 축 회전(사본의 팩 AutoRotate.RotateAngle) 배율 — 만들 때 1번
foreach (var mb in g.GetComponentsInChildren<MonoBehaviour>(true))
{
if (mb == null || mb.GetType().Name != "AutoRotate") continue; var f = mb.GetType().GetField("RotateAngle"); if (f == null) continue;
f.SetValue(mb, (Vector3)f.GetValue(mb) * c.spinMul);
}
}
foreach (var g in _riders) if (g != null) { g.SetActive(false); g.transform.localScale = Vector3.one * c.riderScale; } 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 (p == null) WLShurikenRider.LastLog = "수리검 프리팹 없음 " + c.prefab;
if (c.mode == "scatter") PlanScatter(); if (c.mode == "scatter") PlanScatter();
@ -125,7 +151,7 @@ namespace WL.Combat.Weapon
Scat[] _sc; Scat[] _sc;
void PlanScatter() void PlanScatter()
{ {
int n = Mathf.Min(Mathf.Max(1, _c.scatterCount), _riders.Count); _sc = new Scat[n]; int n = Mathf.Min(Mathf.Max(1, _c.scatterCount), _riders.Count); _sc = new Scat[n]; _emitAcc = new float[n]; _endDone = new bool[n];
var order = new int[n]; for (int i = 0; i < n; i++) order[i] = i; 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; } 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); float a0 = Random.Range(0f, 360f), r0 = Mathf.Min(_c.scatterMinRadius, _c.scatterMaxRadius), r1 = Mathf.Max(_c.scatterMinRadius, _c.scatterMaxRadius);
@ -222,12 +248,66 @@ namespace WL.Combat.Weapon
g.transform.position = _sc[i].pos; g.transform.localScale = Vector3.one * (_c.riderScale * k); 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); 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); WLShurikenRider.MinY = Mathf.Min(WLShurikenRider.MinY, _sc[i].pos.y - _ground); WLShurikenRider.MaxY = Mathf.Max(WLShurikenRider.MaxY, _sc[i].pos.y - _ground);
var fl = _sc[i].pos - c0; fl.y = 0f; WLShurikenRider.MaxReach = Mathf.Max(WLShurikenRider.MaxReach, fl.magnitude);
if (_pathEmOff && _ps != null && _c.pathRate > 0f && u < _c.scatterShrinkFrom) // #985 수리검 자리에서 경로 호(궤적)를 뿜는다 — 지나온 길에 남아 원래 수명대로 사라짐
{
_emitAcc[i] += _c.pathRate * dt; if (_emitAcc[i] >= 1f && !_ps.isPlaying) _ps.Play(false); // 호 층 길이(0.5 s)가 지나 멈췄으면 다시 켬(방출은 꺼 둔 채)
while (_emitAcc[i] >= 1f)
{
_emitAcc[i] -= 1f; var ep = new ParticleSystem.EmitParams { applyShapeToPosition = false, startSize = Random.Range(_c.pathSizeMin, _c.pathSizeMax) };
ep.position = _ps.main.simulationSpace == ParticleSystemSimulationSpace.World ? _sc[i].pos : _ps.transform.InverseTransformPoint(_sc[i].pos);
_ps.Emit(ep, 1); WLShurikenRider.PathEmitted++;
}
}
if (!_endDone[i] && u >= _c.scatterShrinkFrom) { _endDone[i] = true; if (Burst(_c.endFx, _sc[i].pos, _c.endFxScale, _c.endFxLife)) WLShurikenRider.EndBursts++; } // #985 작아지기 시작할 때 터짐
shown++; shown++;
} }
if (shown > 0 && !_shook && _c.releaseShake > 0f) { _shook = true; Shake(_c.releaseShake, _c.releaseShakeSeconds); } // #985 퍼지는 순간 흔들림
if (shown > 0 && !string.IsNullOrEmpty(_c.hitFx)) HitPass();
if (shown > WLShurikenRider.MaxShown) WLShurikenRider.MaxShown = shown; if (shown > WLShurikenRider.MaxShown) WLShurikenRider.MaxShown = shown;
if (!pending) Stop(); if (!pending) Stop();
} }
// #985 날아가는 수리검이 적 몸(반지름 + hitRadius) 안을 지나면 그 적 몸 앞에 명중 섬광 + 흔들림(수리검 · 적 쌍마다 1번 · 보이는 타격만 · 피해는 원본 스킬 그대로)
void HitPass()
{
var info = ActorInfo.Ins; if (info == null || DSUtil.CheckNull(_shooter)) return;
var c0 = _shooter.Get_position(); var list = info.Get_EnemyActors(_shooter.IsEnemy(), c0, WLShurikenRider.MaxReach + _c.hitRadius + 3f); if (list == null) return;
try
{
for (int k = 0; k < list.Count; k++)
{
var a = list[k]; if (a == null || a.IsDead()) continue; var ap = a.Get_position(); float rad = WL.Combat.WLCombatMotionSettings.BodyRadius(a) + _c.hitRadius;
for (int i = 0; i < _sc.Length; i++)
{
if (!_sc[i].on || _sc[i].done) continue; var v = _sc[i].pos - ap; v.y = 0f; if (v.magnitude > rad) continue;
long key = ((long)a.GetInstanceID() << 8) | (long)(i & 0xff); if (!_hitPairs.Add(key)) continue;
var at = WL.Combat.Impact.WLImpactFx.BodyCenter(a) + WL.Combat.Impact.WLImpactFx.TowardCamera(a);
if (Burst(_c.hitFx, at, _c.hitFxScale * WL.Combat.Impact.WLImpactFx.SizeFactor(a), _c.hitFxLife)) WLShurikenRider.HitBursts++;
if (_c.hitShake > 0f) Shake(_c.hitShake, _c.hitShakeSeconds);
}
}
}
finally { info.Release_Actors(list); }
}
static readonly Dictionary<string, GameObject> s_fx = new Dictionary<string, GameObject>();
static bool Burst(string path, Vector3 pos, float scale, float life)
{
if (string.IsNullOrEmpty(path)) return false; GameObject p;
if (!s_fx.TryGetValue(path, out p) || p == null) { p = Resources.Load<GameObject>(path); s_fx[path] = p; }
if (p == null) { WLShurikenRider.LastLog = "이펙트 없음 " + path; return false; }
var g = Instantiate(p, pos, Quaternion.identity); g.transform.localScale = p.transform.localScale * scale;
foreach (var ps in g.GetComponentsInChildren<ParticleSystem>(true)) { var m = ps.main; m.scalingMode = ParticleSystemScalingMode.Hierarchy; }
Destroy(g, Mathf.Max(0.2f, life)); return true;
}
static void Shake(float amp, float seconds)
{
var st = WL.Feel.WLHitFeel.Settings; var sh = WL.Feel.WLHitFeel.GetShaker(true); if (st == null || sh == null) return;
sh.Shake(amp, seconds, st.shakeFrequency, st.shakeAxisWeight); WLShurikenRider.Shakes++;
}
// 수리검 면(사본 루트 +Y)을 카메라 쪽으로 · 앞(+Z) = 카메라 위쪽 — 위에서 내려다보는 카메라에서 납작한 원판이 정면으로 보이고 제 축(AutoRotate)으로 돈다 · tilt° 만큼 카메라 오른쪽 축으로 눕힘 // 수리검 면(사본 루트 +Y)을 카메라 쪽으로 · 앞(+Z) = 카메라 위쪽 — 위에서 내려다보는 카메라에서 납작한 원판이 정면으로 보이고 제 축(AutoRotate)으로 돈다 · tilt° 만큼 카메라 오른쪽 축으로 눕힘
static Quaternion Face(float tilt, Vector3 pos) static Quaternion Face(float tilt, Vector3 pos)
{ {
@ -241,7 +321,7 @@ namespace WL.Combat.Weapon
int FreeSlot() { for (int i = 0; i < _riders.Count; i++) if (!_bySeed.ContainsValue(i)) return i; return -1; } int FreeSlot() { for (int i = 0; i < _riders.Count; i++) if (!_bySeed.ContainsValue(i)) return i; return -1; }
ParticleSystemRenderer _pathRenderer; ParticleSystemRenderer _pathRenderer;
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 Stop() { _on = false; _bySeed.Clear(); _sc = null; if (_pathEmOff) { _pathEmOff = false; if (_ps != null) { var em = _ps.emission; em.enabled = _pathEmWas; } } 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 OnDisable() { Stop(); }
void OnDestroy() { foreach (var g in _riders) if (g != null) Destroy(g); } void OnDestroy() { foreach (var g in _riders) if (g != null) Destroy(g); }
} }