973 lines
86 KiB
C#
973 lines
86 KiB
C#
// ─────────────────────────────────────────────────────────────────────────────
|
||
// WLStaffEarth.cs — 지팡이(현자의 지팡이 · 클래스 10302) 주력 · 각성 · 3타를 대지(Stylized Earth Skills) WL 방식으로 (#935 · 2026-10-18)
|
||
//
|
||
// PD 원문(#935):
|
||
// ③ 「지팡이 주력 스킬 시전 시 캐릭터의 주문 시전 모션에 모으기 이펙트를 재생시키고, Casual RPG VFX 에셋의 첨부한 스크린샷과 같은 이펙트를 시전한 캐릭터몸에 맞게 발생시켜줘.」
|
||
// ④ 「지팡이 주력 스킬 시전 시 타겟이 된 대상에게 Stylized Earth Skills에셋의 "FX_EarthSkill_Summon_04" 이펙트를 재생시켜 공격하고
|
||
// 타겟이 된 대상 및 주변의 적에게도 광역 피해 및 스턴 효과를 1.5초간 부여해줘」
|
||
// ⑤ 「지팡이 각성 스킬 시전 시간도 0.5초로 줄이고, 주력 스킬과 동일하게 모으기 이펙트 및 캐릭터 주변 시전 이펙트를 적용해. 그리고 시전한 스킬을
|
||
// FX_Earthskill_Path_03_1을 캐릭터를 중심으로 시전시키켜 범위 내 명중 된 적을 2m 띄운 후 떨어질 때 2초간 기절 시키게 해줘.」
|
||
// ⑥ 「지팡이 스킬 3타에는 타겟이 된 대상에게 FX_EarthSkill_Popup_04 이펙트를 재생시켜줘. 이 공격에 맞은 적은 피해를 받을 때 1.5m 띄워졌다가 0.5초 기절해야 해.」
|
||
//
|
||
// ■ 시전 — CombatEvents.SkillCast(시전 시작)에 castSkills 의 스킬이면 무기 끝에 모으기(chargeFx) · 발밑에 몸 기둥(auraFx · 몸 크기 비례) — 둘 다 따라가고
|
||
// 시전 시간 + life 뒤 fade 초 동안 흐려진다
|
||
// ■ 주력 · 각성 — 원본 Set() 의 훅 1줄(TakeOver)이 원본 진행(소용돌이 15 초 · 운석 3.75 초)을 건너뛴다: center(대상 · 시전자) 발밑에 fx →
|
||
// hitDelay(+ 중심에서 거리 / spreadSpeed) 뒤 radius 안의 적에게 원본 투사체 ProcessCollider(스킬 피해 · 피격 반응) + WLLaunch(띄우기 · 기절) →
|
||
// endAfter 뒤 원본 투사체 끄기(풀). 진행 중엔 원본 겉모습 · 판정 상자 · Update 를 끈다(끈 뒤 되돌림)
|
||
// ■ 3타 — 투사체 Set 끝 훅(WLProjectileVisual.OnSet → OnProjectileSet)이 쏘는 순간 애니메이터 현재 상태가 popup.stateName(attack3)이면 그 투사체를 표시 →
|
||
// 그 투사체의 피해 확정(HitConfirmed)에 맞은 적 발밑에 popup fx + WLLaunch(띄우기 · 내려온 뒤 기절)
|
||
// ■ 값 = Resources/WL/WLStaffEarth.json (C45) · enabled 0 = 구독 0 · 훅은 false(원본 그대로)
|
||
// 🔴 Debug.LogError 금지(원본 ErrorLogHook 이 팝업을 띄운다) — 예외는 LastLog 로만
|
||
// ─────────────────────────────────────────────────────────────────────────────
|
||
using System.Collections;
|
||
using System.Collections.Generic;
|
||
using System.Reflection;
|
||
using UnityEngine;
|
||
using WL.Combat.Core;
|
||
|
||
namespace WL.Combat.Weapon
|
||
{
|
||
[System.Serializable]
|
||
public class WLStaffEarthFx
|
||
{
|
||
public string prefab = "";
|
||
public float scale = 1f;
|
||
public int bodyScale; // 1 = scale × 시전자 몸 크기 비례(WLImpactFx.SizeFactor)
|
||
public float[] offset = new float[3]; // 시전자 정면 기준 (오른쪽, 위, 앞) m
|
||
public float[] euler = new float[3];
|
||
public float life = 2f; // 남는 시간(초) — 시전 이펙트는 시전 시간 + life
|
||
public float fade = 0.4f; // 0 보다 크면 마지막 fade 초 동안 흐려지며 사라진다(WLFxFade)
|
||
public int follow; // 1 = 무기 끝 · 시전자를 따라간다
|
||
public string socket = ""; // 모으기 무기 소켓 이름(빈 값 = 무기가 달린 켜진 첫 소켓)
|
||
public int head; // #963 1 = 무기 메시 머리(로컬 경계 긴 축 먼 쪽 끝 − headPull · 끝 빛과 같은 점) · 0 = 이전(소켓 아래 렌더러 월드 상자 꼭대기 · 기울면 머리에서 벗어남) · 2 = 무기 든 손 소켓 · 3 = 빈 손 뼈(#999 · 3타 모으기만)
|
||
public float headPull = 0.08f;
|
||
// #998 PD 「창 주력 스킬 이펙트 위치와 창의 위치가 맞지 않는 것 같아. 창을 내지른 방향에 맞게 이펙트위치를 수정해줘」 — alignWeapon 1 = 매 프레임 무기 축(자루 중심선 · 손 → 창끝)에 맞춰
|
||
// 돌리고 손에서 창 길이 × alignFraction 간 자리에 둔다(창 4타 찌르기와 같은 WLWeaponFollowFx · 음수 = 손 뒤 몸 쪽) · alignPitch 1 = 위아래 기울기도 따름 · toCamera = 카메라 쪽 m
|
||
public int alignWeapon;
|
||
public float alignFraction;
|
||
public int alignPitch = 1;
|
||
public float toCamera; // #999 무기 축 추종이 아니어도 카메라 쪽 m(플레이어보다 앞 = 가림 · WLFxToCamera)
|
||
// #999 PD 「창 주력 스킬 시전시 이펙트가 반복 재생되는 느낌인데 이펙트가 꺼지는 느낌이 들지 않도록 중간 프레임 부분을 돌진 지속시간 동안 반복재생되다가 돌진 끝나는 시점에 맞춰서 완전히 이펙트가 종료되도록」
|
||
// sustain 1 = (돌진 감싸기 · 대쉬) 한 번만 띄우고 시간당 방출 층(버스트 없는 층)은 반복 = 중간 구간 유지 · 끝 − sustainTail 초에 방출 멈춤 · 끝에 남은 입자 지움 · speed = 입자 시뮬레이션 속도(버스트형 짧은 층을 돌진 동안 늘림)
|
||
public int sustain;
|
||
public float sustainTail = 0.35f;
|
||
public float speed = 1f;
|
||
public float lightMul = 1f; // #1006 이펙트에 붙은 점광원 세기 배율(손이 낮게 있는 지팡이 마지막 타 = 땅에 밝은 웅덩이 → 낮춤)
|
||
}
|
||
|
||
[System.Serializable]
|
||
public class WLStaffEarthSkill
|
||
{
|
||
public string skill = ""; // 원본 스킬 클래스 이름(Skill_Hurricane · Skill_MeteoStorm)
|
||
public int classId; // #975 0 = 모든 직업 · > 0 = 시전자 직업이 이 값일 때만(같은 스킬 클래스를 직업별로 다르게 — 해머 기본 = 운석 소환 복사본 → 대지 줄기) · 직업이 맞는 행이 0 행보다 먼저
|
||
public string center = "target"; // target = 대상 발밑 · caster = 시전자 발밑
|
||
public WLStaffEarthFx fx = new WLStaffEarthFx();
|
||
public float hitDelay = 0.4f; // fx 뒤 판정까지(초)
|
||
public float spreadSpeed; // 0 보다 크면 중심에서 거리 / 이 속도(m/s)만큼 판정이 늦어진다(퍼져 나가는 바위 줄기)
|
||
public float radius = 2.5f; // 판정 반경(m · 적 몸 반경을 더함)
|
||
public float launchHeight; // 띄우기 높이(m · 0 = 띄우지 않음)
|
||
public float launchSeconds = 0.6f; // 떠 있는 시간(초)
|
||
public float stunSeconds; // 내려온 뒤 기절(초)
|
||
public float endAfter = 1f; // 마지막 판정 뒤 원본 투사체를 끄기까지(초)
|
||
public int noExtra; // #991 1 = 원본 스킬 부가 효과(화상 등) 없이 피해만
|
||
public int[] weaponIds = new int[0]; // #1000 비어 있지 않으면 장착 무기(그룹 번호)가 이 안일 때만(같은 직업 10303 = 정령의 셉터 9005 · 마법책 9009)
|
||
// #1000 PD 「마법책 각성 스킬 시전 시 AOE Explosions Pack 1 에셋의 첨부한 이펙트를 손바닥에서 전방으로 발사하는 형태로 구현해줘 (에셋은 하늘에서 떨어지는 형태이나, 손바닥에서 전방으로 날아가는 에너지 볼트 난사 컨셉) · 피격 이펙트는 일단 에셋에 있는 피격 이펙트로」
|
||
// boltCount 발을 boltInterval 초마다 빈 손(손바닥)에서 대상(없으면 정면 boltRange m) 쪽 ±boltSpread° 로 쏜다 · boltPrefab = 볼트 1발 사본(에셋 볼트 + 꼬리 · 수명 끝 = 착탄 하위 이미터)
|
||
// · 날아가는 시간 = 거리 / boltSpeed · 그 시각 도착 자리 boltRadius m 안 적에게 원본 투사체 피해(발마다 따로 셈)
|
||
public int boltCount;
|
||
public float boltInterval = 0.08f, boltSpeed = 15f, boltSpread = 10f, boltRange = 8f, boltScale = 0.35f, boltRadius = 1.0f;
|
||
public string boltPrefab = "";
|
||
// #1001 boltCurve = 휘는 정도(조절점 좌우 · 위 m) · boltHitFx = 도착 피격 이펙트 · boltMuzzleFx = 손바닥 발사 섬광(배율 따로)
|
||
public float boltCurve = 1.5f, boltHitScale = 0.15f, boltMuzzleScale = 0.15f;
|
||
public float boltTrailWidth, boltTrailTime, boltOwnTrail; // boltOwnTrail > 0 = 공 재질 리본 꼬리 너비(m) // #1001 꼬리 너비 배율(0 = boltScale) · 꼬리 시간(초 · 0 = 프리팹 값)
|
||
public string boltHitFx = "", boltMuzzleFx = "";
|
||
// #1001 PD 「창 각성 스킬에 명중 된 적은 1m 정도 회오리가 전진하는 방향으로 밀려나며 공중에 뜨도록」 — pushDistance > 0 이면 띄움(launchHeight · launchSeconds · stunSeconds)을 그 방향 밀기와 함께(WLLaunch.FlingDir)
|
||
public float pushDistance;
|
||
// #1001 PD 「대검 기본 스킬 회전하는 동안 검기 범위에 있는 적에게 최대 5회 연속 피격(피격 된 적이 조금씩 밀려야하고, 공중 회전 시 적이 밀리는 만큼 플레이어도 전진하며 회전) → 마지막 내려치기 공격 시 적이 1m정도 밀리며 0.5m 공중에 뜨게」
|
||
// spinHits 번 = 시전(SkillEvent = 점프 시작) 뒤 spinHitStart 초부터 spinHitInterval 초마다 PC 둘레 spinRadius m 적에게 원본 피해 × spinDamage + PC 정면으로 spinPush m · slamAt 초 = 착지 내려치기 정면 slamForward m 자리 radius m 적에게 원본 피해 + 정면 slamPush m · slamLaunch m 띄움(slamSeconds)
|
||
public int spinHits;
|
||
public float spinHitStart = 0.05f, spinHitInterval = 0.09f, spinRadius = 1.8f, spinPush = 0.25f, spinDamage = 0.15f, slamAt, slamForward = 1.0f, slamPush, slamLaunch, slamSeconds = 0.35f;
|
||
// #963 PD 「지팡이 각성 = Hyper Casual FX Pack Vol.2 메테오 — 타겟 대상 주위 5 m 에 운석 10개를 랜덤 낙하 · 낙하 자리의 적에게 피해 · 맞은 적 0.5 초 스턴」 —
|
||
// meteors > 0 = 메테오 방식: meteorInterval 마다 center 둘레 meteorArea(m) 원 안 무작위 자리에 fx(운석 1개 = 낙하 + 충돌)를 띄우고 meteorImpact 초 뒤(사본이 땅에 닿는 시각) 그 자리 radius 안 적에게 원본 투사체 피해 + 기절(stunSeconds) ·
|
||
// 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;
|
||
// #1006 PD 「마법책 기본 스킬과 대검 기본 스킬 사용 후 지면에 충격 흔적이 남도록 해줘. (해머 공격에 사용 된 지면 충격 효과를 참고할 것)」 — traceFx = 판정 순간(메테오 = 땅에 닿는 시각 · 그 밖 = hitDelay 뒤) 그 자리 지면에 남기는 흔적
|
||
public WLStaffEarthFx traceFx = new WLStaffEarthFx();
|
||
// #996 PD 「창 주력 = 찌르기 자세로 전방 돌진 · 돌진 이펙트로 몸을 감싼 채 4 m 전진 · 경로상 적을 밀치며 공격 · 돌진 중 방향 전환」 —
|
||
// rushDistance > 0 = 돌진 방식: rushHoldState 를 rushHoldNormalized 에 멈춘 자세로 rushSeconds 동안 rushDistance m(내비 안) · 가상패드로 rushTurnDegPerSec 회전 ·
|
||
// 몸 둘레 radius m 적 = rushHitInterval 마다 원본 투사체 피해 + 앞쪽 적은 같은 거리만큼 밀림(rushPush 배) · rushFx = 몸을 감싸는 이펙트(따라감)
|
||
public float rushDistance, rushSeconds = 0.6f, rushHitInterval = 0.2f, rushPush = 1f, rushTurnDegPerSec = 360f, rushHoldNormalized = 0.3f, rushSteerDeadzone = 0.2f;
|
||
public string rushHoldState = "";
|
||
public WLStaffEarthFx rushFx = new WLStaffEarthFx();
|
||
public WLStaffEarthFx rushFx2 = new WLStaffEarthFx(); // #997 몸 · 창 전체를 감싸는 이펙트(따라감) — rushFx2Every 초마다 다시 띄워 돌진 내내 보이게
|
||
public float rushFx2Every = 0.3f;
|
||
public WLStaffEarthFx rushFx3 = new WLStaffEarthFx(); // #1000 PD 「돌진하는 캐릭터가 흙먼지를 일으키며 돌진 · Hyper Casual FX Pack Vol.1 의 첨부한 돌진 이펙트」 — 발밑에 rushFx3Every 초마다(따라가지 않음 = 지나온 길에 남음)
|
||
public float rushFx3Every = 0.2f;
|
||
public float fanShrinkFrom = 1f; // #997 비행 진행 비율이 이 값을 넘으면 끝까지 크기를 0 으로 줄이며 사라짐(1 = 줄이지 않음)
|
||
// #996 PD 「창 각성 = 회오리를 전방 부채꼴 방향으로 3 m 가량 날아가다 소멸 · 명중 적 2 m 띄움 → 낙하 → 2 초 기절」 —
|
||
// fanCount > 0 = 부채꼴 방식: 시전자 정면(대상 쪽) ±fanAngle/2 에 fanCount 개 fx 를 fanStart m 에서 fanDistance m 까지 fanSeconds 동안 날리고 끝에서 지움 ·
|
||
// 지나는 자리 radius m 적 = 한 번씩 피해 + launchHeight · launchSeconds · stunSeconds(기존 칸)
|
||
public int fanCount;
|
||
public float fanAngle = 60f, fanStart = 0.6f, fanDistance = 3f, fanSeconds = 0.6f, fanFade = 0.25f;
|
||
// #1002 PD 「마법책 주력 스킬 사용 시 Stylized Earth Skills 이펙트가 발동 된 후 주위의 적들이 해당 위치로 몰리게 하고(중력에 이끌리듯) 해당 이펙트가 소멸하는 시점에
|
||
// 지면에 FX_EarthSkill_Energy_01로 주변에 피해를 주며 적을 1초간 기절시키며 골렘이 생성되게 · 골렘은 용암 컨셉 · 크기 2배」 —
|
||
// pullSeconds > 0 = 중력 방식: center 에 pullFx(pullSeconds 동안) · pullRadius m 안 적을 중심으로 끌어당김(속도 = pullAccel × 지난 초 · 최대 pullMaxSpeed · 중심에서 pullStop m 에서 멈춤) →
|
||
// 끝에 fx(지면) + hitDelay 뒤 radius 안 적 피해 + stunSeconds 기절 → 원본 Set 을 다시 불러 소환(summonPrefab = 원본 prefab 칸 대신 · 빈 값 = 원본) ·
|
||
// 소환체가 켜지면 center 로 옮기고 원본 배율 × summonFit(모델 키 맞춤) × summonScale
|
||
public float pullSeconds, pullRadius = 5f, pullAccel = 12f, pullMaxSpeed = 8f, pullStop = 0.6f;
|
||
public WLStaffEarthFx pullFx = new WLStaffEarthFx();
|
||
public string summonPrefab = "";
|
||
public float summonFit = 1f, summonScale = 1f;
|
||
// #1003 PD 「골렘의 공격 사거리를 근접처럼 줄이고, 몸 주변을 불타는 것처럼 · 2초에 한번 정도 공격 · 주위 0.5m이내 근접한 적에게 골렘 공격력의 1/10의 지속적인 피해」 — WLSummonMelee
|
||
public float summonMeleeGap = 0.3f, summonAttackInterval, summonAuraRadius = 0.5f, summonAuraRate, summonAuraTick = 0.5f;
|
||
public WLStaffEarthFx summonBurn = new WLStaffEarthFx(); // 몸 불꽃(골렘 발밑 · 따라감 · 소환 시간 + life)
|
||
// #1004 PD 「다가가 멈추는 거리를 줄이라는 지시가 아니라 공격 판정 범위를 줄이라고 한 지시 (팔이 늘어나 공격하는 거리가 길기 때문에 길게 늘어지지 않게)」 · 「골렘 주위 불꽃 이펙트를 파티클 느낌의 다른 이펙트로」 —
|
||
// summonMeleeGap < 0 = 다가가 멈추는 거리 원본 그대로 · summonHitReach > 0 = 골렘 공격 판정 상자 앞 끝(몸 중심에서 m · WLHitboxFit) · summonBurnBones = 불꽃을 붙일 뼈 경로(비면 발밑 1개)
|
||
public float summonHitReach;
|
||
public string[] summonBurnBones = new string[0];
|
||
}
|
||
|
||
/// <summary>#997 PD 「창 기본 · 각성 · 주력 스킬을 사용할 때 에너지가 모일 때 창을 빛나게」 — 이 스킬 · 직업 시전 시작부터 seconds 동안 무기 발광(rise 초 동안 켜짐 · 끝에서 fade 초 동안 꺼짐).</summary>
|
||
[System.Serializable]
|
||
public class WLSkillWeaponGlow
|
||
{
|
||
public string skill = ""; public int classId;
|
||
public float seconds = 0.6f, rise = 0.15f, fade = 0.25f, max = 1f, fill = 0.35f, rimPower = 2.5f;
|
||
public float[] color = { 1.6f, 0.75f, 0.08f };
|
||
}
|
||
|
||
/// <summary>#966 PD 「비수 각성 스킬에 시전 모션 및 시전 이펙트 — Hyper Casual FX Pack Vol.2 스크린샷 이펙트를 캐릭터에게 한번 재생시킨 후 발동」 — 이 원본 스킬(e_Skill)의 시전(SkillCast) 순간 시전자에게 fx(시전 시간 + life · follow 1 = 따라감)</summary>
|
||
[System.Serializable]
|
||
public class WLSkillCastFx
|
||
{
|
||
public string skill = "";
|
||
public int classId; // #996 0 = 모든 직업 · > 0 = 이 직업일 때만(같은 원본 스킬을 다른 직업 그릇으로 쓸 때)
|
||
public WLStaffEarthFx fx = new WLStaffEarthFx();
|
||
}
|
||
|
||
[System.Serializable]
|
||
public class WLStaffEarthPopup
|
||
{
|
||
public int classId = 10302;
|
||
public string stateName = "attack3"; // 쏘는 순간 애니메이터 현재 상태 이름(지팡이 콤보는 애니메이터 전환으로 이어져 CurAnim · ComboTracker 단계가 1 에 머문다)
|
||
public WLStaffEarthFx fx = new WLStaffEarthFx();
|
||
public float launchHeight = 1.5f, launchSeconds = 0.6f, stunSeconds = 0.5f;
|
||
// #967 PD 「지팡이 공격 마지막 타 — 기본 공격 투사체 대신 메테오(각성 스킬에 사용중인 이펙트) 한 발이 대상에게 떨어지도록」 — 1 = 3타 투사체를 쏘는 순간(겉모습을 붙인 뒤) 넘겨받아
|
||
// 숨기고 meteorRule(메테오 방식 · 운석 1개 · 대상 자리)로 떨어뜨려 땅에 닿는 순간 그 투사체 피해 · 이 경우 바위 솟음(fx)은 띄우지 않는다 · 0 = 이전(투사체 + 맞은 자리 바위)
|
||
public int meteor;
|
||
public WLStaffEarthSkill meteorRule = new WLStaffEarthSkill();
|
||
// #983 PD 「오브 "기본 공격" 마지막 타(3타)에서는 공격 직후 다시 모션을 캔슬하는 모습인데, 공격 상태의 손을 내지른 상태를 잠시 유지한 채 투사체를 날리지 말고 타겟중인 대상에게 번개가 1발을 낙하」 —
|
||
// holdSeconds > 0 = 넘겨받는 순간(발사 시점) holdDelay 뒤 애니메이터를 holdSeconds 동안 멈춤(자세 유지 · 다른 상태로 바뀌거나 다른 코드가 속도를 바꾸면 바로 손 뗌)
|
||
public float holdDelay, holdSeconds;
|
||
// #994 PD 「지팡이 마지막타 공격 전 짧게 에너지를 모으는 대기 시간과 연출을 부여」 — preChargeSeconds > 0 = 그 상태(stateName)로 넘어가는 첫 프레임(발사 이벤트 전 ·
|
||
// 그 상태 시각 preChargeMaxIn 초 안)에 애니메이터를 preChargeSeconds 동안 멈추고 무기 머리에 preChargeFx(시전 모으기와 같은 형식) — 다른 코드가 속도를 바꾸면 손 뗌
|
||
public float preChargeSeconds, preChargeMaxIn = 0.05f;
|
||
public WLStaffEarthFx preChargeFx = new WLStaffEarthFx();
|
||
public int[] weaponIds = new int[0]; // #1002 비어 있지 않으면 장착 무기(그룹 번호)가 이 안일 때만(같은 직업 10303 = 정령의 셉터 9005 · 마법책 9009 — PD 「마법책은 4타 공격 후 마지막 타에 FX_Splash_energy_01_floor」)
|
||
}
|
||
|
||
[System.Serializable]
|
||
public class WLStaffEarthJson
|
||
{
|
||
public int enabled = 0;
|
||
public string[] castSkills = new string[0];
|
||
public int[] castSkipClasses = new int[0]; // #1007 castSkills 이름이 같아도 이 직업은 chargeFx · auraFx 를 띄우지 않음(창 주력 = Skill_Hurricane → 창 끝에 지팡이 모으기 빛이 뭉침)
|
||
public WLStaffEarthFx chargeFx = new WLStaffEarthFx();
|
||
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();
|
||
public WLSkillCastFx[] skillCastFx = new WLSkillCastFx[0]; // #966 스킬별 시전 이펙트(시전자)
|
||
public WLSkillWeaponGlow[] weaponGlow = new WLSkillWeaponGlow[0]; // #997 스킬 시전(모으기) 동안 무기 발광(WLWeaponGlowFx · 대검 · 비수와 같은 재질)
|
||
public string weaponGlowMaterial = "WL/Fx/WL_Mat_WeaponGlow";
|
||
}
|
||
|
||
public static class WLStaffEarth
|
||
{
|
||
public const string ResourcesPath = "WL/WLStaffEarth";
|
||
public static int Casts, TakeOvers, Hits, Launches, Marked, Popups, Errors, Circles, Meteors, SkillCastFxShown, Attack3Meteors, NoExtra; // #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 Dictionary<int, WLStaffEarthPopup> s_marked = new Dictionary<int, WLStaffEarthPopup>();
|
||
static WLStaffEarthRunner s_runner;
|
||
static MethodInfo s_off;
|
||
|
||
static WLStaffEarthJson Cfg
|
||
{
|
||
get
|
||
{
|
||
if (s_cfg != null) return s_cfg;
|
||
s_cfg = new WLStaffEarthJson();
|
||
var ta = Resources.Load<TextAsset>(ResourcesPath);
|
||
if (ta == null) { LastLog = "설정 없음 " + ResourcesPath; return s_cfg; }
|
||
try { JsonUtility.FromJsonOverwrite(ta.text, s_cfg); } catch (System.Exception ex) { LastLog = "설정 파싱 실패 — " + ex.Message; }
|
||
return s_cfg;
|
||
}
|
||
}
|
||
|
||
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.AfterSceneLoad)]
|
||
static void Boot()
|
||
{
|
||
s_cfg = null; s_prefabs.Clear(); s_marked.Clear(); s_pendingMeteor = null; s_pendingPopup = null; s_preAn = null; s_preArmed = true;
|
||
CombatEvents.SkillCast.Remove(OnSkillCast); CombatEvents.HitConfirmed.Remove(OnHitConfirmed);
|
||
if (Cfg.enabled == 0) return;
|
||
CombatEvents.SkillCast.Add(OnSkillCast); CombatEvents.HitConfirmed.Add(OnHitConfirmed);
|
||
}
|
||
|
||
// ── 시전: 모으기(무기 끝) · 몸 기둥(발밑) ──────────────────────────────────────────
|
||
static void OnSkillCast(in SkillCastEvent e)
|
||
{
|
||
try
|
||
{
|
||
var c = Cfg; var pc = e.actor as PCActor;
|
||
if (pc == null || e.skill == null) return;
|
||
float cast = Mathf.Max(0f, e.skill.f_CastingTime);
|
||
if (pc.IsMainPC() && c.castCircleClasses != null && System.Array.IndexOf(c.castCircleClasses, pc.Get_ID()) >= 0 && Spawn(c.castCircle, pc, pc.transform, pc.Get_position(), cast) != null) Circles++; // #963 마법 시전 마법진
|
||
if (pc.IsMainPC() && c.skillCastFx != null) // #966 스킬별 시전 이펙트(비수 각성 = HCFX Energy_01 사본)
|
||
foreach (var sf in c.skillCastFx) if (sf != null && sf.skill == e.skill.e_Skill.ToString() && (sf.classId == 0 || sf.classId == pc.Get_ID()) && Spawn(sf.fx, pc, pc.transform, pc.Get_position(), cast) != null) SkillCastFxShown++;
|
||
if (pc.IsMainPC() && c.weaponGlow != null) // #997 무기 발광
|
||
foreach (var g in c.weaponGlow) if (g != null && g.skill == e.skill.e_Skill.ToString() && (g.classId == 0 || g.classId == pc.Get_ID())) { Runner().StartCoroutine(GlowRun(pc, g, c.weaponGlowMaterial)); Glows++; }
|
||
if (c.castSkills == null || System.Array.IndexOf(c.castSkills, e.skill.e_Skill.ToString()) < 0) return;
|
||
if (c.castSkipClasses != null && System.Array.IndexOf(c.castSkipClasses, pc.Get_ID()) >= 0) return; // #1007
|
||
var sock = Socket(pc, c.chargeFx.socket);
|
||
if (Spawn(c.chargeFx, pc, sock, sock != null ? Tip(sock, c.chargeFx) : pc.Get_Center_position(), cast) != null) Casts++;
|
||
Spawn(c.auraFx, pc, pc.transform, pc.Get_position(), cast);
|
||
}
|
||
catch (System.Exception ex) { Errors++; LastLog = "시전 이펙트 실패 — " + ex.Message; }
|
||
}
|
||
|
||
public static int Glows;
|
||
static WLStaffEarthRunner Runner()
|
||
{
|
||
if (s_runner == null) { var go = new GameObject("[WL935] StaffEarth"); go.hideFlags = HideFlags.HideInHierarchy; Object.DontDestroyOnLoad(go); s_runner = go.AddComponent<WLStaffEarthRunner>(); }
|
||
return s_runner;
|
||
}
|
||
static IEnumerator GlowRun(PCActor pc, WLSkillWeaponGlow g, string mat)
|
||
{
|
||
var sock = Socket(pc, ""); if (sock == null) yield break;
|
||
var fx = new WLWeaponGlowFx(); if (fx.Begin(sock, mat) <= 0) yield break;
|
||
var col = g.color != null && g.color.Length >= 3 ? new Color(g.color[0], g.color[1], g.color[2], 1f) : Color.white;
|
||
float t = 0f, total = Mathf.Max(0.05f, g.seconds);
|
||
try
|
||
{
|
||
while (t < total && pc != null && !pc.IsDead())
|
||
{
|
||
float k = Mathf.Min(1f, t / Mathf.Max(0.01f, g.rise)) * Mathf.Min(1f, (total - t) / Mathf.Max(0.01f, g.fade));
|
||
fx.Set(g.max * k, col, g.fill, g.rimPower);
|
||
yield return null; t += Time.deltaTime;
|
||
}
|
||
}
|
||
finally { fx.End(); }
|
||
}
|
||
|
||
/// <summary>무기 소켓 — 이름이 맞는 켜진 소켓, 없으면 무기(자식)가 달린 켜진 첫 소켓.</summary>
|
||
/// <summary>#999 PD 「여전히 지팡이 마지막타가 지팡이를 든 손이 빛나고 있어. 빈 손에서 에너지가 모여야 해」 — 빈 손 = Humanoid 왼손 · 오른손 뼈 중 무기 소켓에서 먼 쪽(이 PC 지팡이 = w_righthand → 왼손).</summary>
|
||
internal static Transform EmptyHand(PCActor pc)
|
||
{
|
||
var an = pc != null ? pc.m_animation : null; if (an == null || !an.isHuman) return null;
|
||
Transform l = an.GetBoneTransform(HumanBodyBones.LeftHand), r = an.GetBoneTransform(HumanBodyBones.RightHand), s = Socket(pc, "");
|
||
if (l == null || r == null) return l ?? r;
|
||
if (s == null) return l;
|
||
return Vector3.Distance(s.position, l.position) >= Vector3.Distance(s.position, r.position) ? l : r;
|
||
}
|
||
|
||
internal static Transform Socket(PCActor pc, string name) // #998 WLProjectileVisual fromSocket 도 쓴다(3타 모으기 빛과 같은 손)
|
||
{
|
||
if (pc == null || pc.tfs_weapon == null) return null;
|
||
Transform first = null;
|
||
foreach (var s in pc.tfs_weapon)
|
||
{
|
||
if (s == null || !s.gameObject.activeInHierarchy) continue;
|
||
if (!string.IsNullOrEmpty(name) && s.name == name) return s;
|
||
if (first == null && s.childCount > 0) first = s;
|
||
}
|
||
return first;
|
||
}
|
||
|
||
/// <summary>무기 끝(소켓 아래 렌더러 경계의 가장 높은 곳) — 렌더러가 없으면 소켓 자리.</summary>
|
||
static Vector3 Tip(Transform sock, WLStaffEarthFx fx)
|
||
{
|
||
if (fx != null && (fx.head == 2 || fx.head == 3)) return sock.position; // #997 2 = 소켓(무기 든 손) 자리 — PD 「지팡이가 아니라 손이 빛나게」 · #999 3 = 빈 손 뼈(부르는 쪽이 EmptyHand 를 넘김)
|
||
if (fx != null && fx.head != 0) // #963 무기 메시 머리(끝 빛과 같은 점) — 이전 방식은 기운 지팡이에서 월드 상자 꼭대기 · 가운데라 머리에서 떨어졌다(PD 「모으는 연출이 둥근 머리에 안 모인다」)
|
||
for (int i = 0; i < sock.childCount; i++)
|
||
{
|
||
var w = sock.GetChild(i); if (!w.gameObject.activeInHierarchy) continue;
|
||
Vector3 hl; if (WLWeaponTipGlow.HeadLocal(w, sock, fx.headPull, out hl)) return w.TransformPoint(hl);
|
||
}
|
||
bool any = false; Bounds b = new Bounds(sock.position, Vector3.zero);
|
||
foreach (var r in sock.GetComponentsInChildren<Renderer>(false))
|
||
{
|
||
if (r is ParticleSystemRenderer || !r.enabled) continue;
|
||
if (!any) { b = r.bounds; any = true; } else b.Encapsulate(r.bounds);
|
||
}
|
||
return any ? new Vector3(b.center.x, b.max.y, b.center.z) : sock.position;
|
||
}
|
||
|
||
static GameObject Spawn(WLStaffEarthFx fx, Actor actor, Transform parent, Vector3 pos, float extra)
|
||
{
|
||
var fwd = actor != null ? actor.transform.forward : Vector3.forward;
|
||
var go = SpawnAt(fx, actor, pos, fwd, extra);
|
||
if (go != null && fx.follow != 0 && parent != null) go.transform.SetParent(parent, true); // 월드 크기 · 자리 유지한 채 따라감
|
||
return go;
|
||
}
|
||
|
||
internal static GameObject SpawnAt(WLStaffEarthFx fx, Actor actor, Vector3 pos, Vector3 fwd, float extra)
|
||
{
|
||
if (fx == null || string.IsNullOrEmpty(fx.prefab)) return null;
|
||
var p = Prefab(fx.prefab); if (p == null) return null;
|
||
fwd.y = 0f; if (fwd.sqrMagnitude < 1e-4f) fwd = Vector3.forward;
|
||
var basis = Quaternion.LookRotation(fwd.normalized);
|
||
var go = Object.Instantiate(p, pos + basis * V(fx.offset), basis * Quaternion.Euler(V(fx.euler)));
|
||
float s = fx.scale * (fx.bodyScale != 0 && actor != null ? WL.Combat.Impact.WLImpactFx.SizeFactor(actor) : 1f);
|
||
foreach (var ps in go.GetComponentsInChildren<ParticleSystem>(true)) { var m = ps.main; m.scalingMode = ParticleSystemScalingMode.Hierarchy; }
|
||
go.transform.localScale = Vector3.one * s;
|
||
float total = Mathf.Max(0f, extra) + Mathf.Max(0.1f, fx.life);
|
||
if (fx.fade > 0f) WLFxFade.Attach(go, Mathf.Max(0f, total - fx.fade), fx.fade);
|
||
else Object.Destroy(go, total);
|
||
LastLog = "생성 " + fx.prefab + " @" + pos.ToString("F1") + " ×" + s.ToString("F2");
|
||
if (fx.alignWeapon != 0 && actor is PCActor apc) AlignToWeapon(go, apc, fx, s);
|
||
else if (fx.toCamera > 0f) go.AddComponent<WLFxToCamera>().dist = fx.toCamera;
|
||
if (!Mathf.Approximately(fx.speed, 1f) && fx.speed > 0f) foreach (var ps in go.GetComponentsInChildren<ParticleSystem>(true)) { var m = ps.main; m.simulationSpeed *= fx.speed; }
|
||
if (!Mathf.Approximately(fx.lightMul, 1f)) foreach (var l in go.GetComponentsInChildren<Light>(true)) l.intensity *= Mathf.Max(0f, fx.lightMul); // #1006 (WLFxFade 는 흐려지기 시작할 때의 세기를 기준으로 낮춤)
|
||
return go;
|
||
}
|
||
|
||
public static int Sustained, SustainLoops, Traces;
|
||
/// <summary>#999 시간당 방출 층(버스트 없는 층)만 반복으로 — 돌진 내내 중간 구간이 이어진다(버스트 층은 처음 1번 · 반복하면 번쩍임이 되풀이).</summary>
|
||
internal static void Sustain(GameObject go)
|
||
{
|
||
if (go == null) return; Sustained++;
|
||
foreach (var ps in go.GetComponentsInChildren<ParticleSystem>(true))
|
||
{
|
||
var em = ps.emission; if (!em.enabled || em.rateOverTime.constantMax < 5f) continue;
|
||
bool burst = false; for (int i = 0; i < em.burstCount; i++) if (em.GetBurst(i).count.constantMax > 0f) burst = true;
|
||
if (burst) continue;
|
||
var m = ps.main; m.loop = true; SustainLoops++;
|
||
}
|
||
}
|
||
/// <summary>#999 방출만 멈춤(살아 있는 입자는 수명대로 사라짐).</summary>
|
||
internal static void StopEmit(GameObject go) { if (go == null) return; foreach (var ps in go.GetComponentsInChildren<ParticleSystem>(true)) ps.Stop(false, ParticleSystemStopBehavior.StopEmitting); }
|
||
/// <summary>#999 남은 입자까지 지우고 없앤다(돌진 끝 = 이펙트 끝).</summary>
|
||
internal static void ClearEnd(GameObject go) { if (go == null) return; foreach (var ps in go.GetComponentsInChildren<ParticleSystem>(true)) ps.Stop(false, ParticleSystemStopBehavior.StopEmittingAndClear); Object.Destroy(go); }
|
||
|
||
public static int Aligned, AlignFailed, Dusts;
|
||
public static int BoltsShot;
|
||
static bool WeaponOk(WLStaffEarthSkill x, int wid) { return x.weaponIds == null || x.weaponIds.Length == 0 || System.Array.IndexOf(x.weaponIds, wid) >= 0; }
|
||
static int EquippedWeaponId() { var wi = WL.Equip.WLInventory.EquippedInSlot(WL.Equip.eFREquipmentType.Weapon); return wi != null && wi.itemJson != null ? wi.itemJson.GroupNo : 0; }
|
||
internal static bool SummonPass; // #1002 true 인 동안 TakeOver 는 원본에 넘긴다(중력 끝 소환)
|
||
public static int Pulled, PullFrames, Summons; public static string SummonLog = "";
|
||
static Actor s_sumOwner; static Vector3 s_sumAt; static Quaternion s_sumRot; static float s_sumMul = 1f, s_sumUntil; static WLStaffEarthSkill s_sumRule; static ProjectileData s_sumPd;
|
||
internal static SummonActor LastSummon;
|
||
internal static void ReserveSummon(Actor owner, Vector3 at, Quaternion rot, float mul, WLStaffEarthSkill rule, ProjectileData pd) { s_sumOwner = owner; s_sumAt = at; s_sumRot = rot; s_sumMul = mul; s_sumRule = rule; s_sumPd = pd; s_sumUntil = Time.time + 3f; LastSummon = null; }
|
||
/// <summary>#1002 원본 소환 콜백 훅(Skill_Golem 의 summon.Set 바로 뒤 1줄) — 중력 진행이 예약한 소환(주인 · 3 s 안)이면 보이기 전 같은 프레임에 자리 · 방향 · 배율을 바꾼다. 예약이 없으면 아무것도 안 함(원본 그대로).</summary>
|
||
public static void AfterSummon(SummonActor s)
|
||
{
|
||
if (s == null || s_sumOwner == null || Time.time > s_sumUntil || s.Get_Owner() != s_sumOwner) return;
|
||
s.Set_Warp(s_sumAt); s.transform.rotation = s_sumRot; s.transform.localScale *= s_sumMul;
|
||
WLSummonMelee.Arm(s, s_sumRule, s_sumPd != null ? s_sumPd.LifeTime : 30f); // #1003 근접 · 공격 간격 · 몸 불꽃 · 둘레 지속 피해
|
||
s_sumOwner = null; LastSummon = s;
|
||
}
|
||
/// <summary>#998 이펙트를 무기 축에 붙인다(WLWeaponFollowFx = 창 4타 찌르기 규칙과 같은 계산 · 손 → 창끝 · 자루 중심선).</summary>
|
||
static void AlignToWeapon(GameObject go, PCActor pc, WLStaffEarthFx fx, float scale)
|
||
{
|
||
var rule = new WL.Combat.Impact.WLSwingFxRule { alignWeapon = true, alignAxis = true, alignPitch = fx.alignPitch != 0, alignFraction = fx.alignFraction, toCamera = fx.toCamera, euler = fx.euler, life = fx.life, massLocal = new float[3] };
|
||
var fw = go.AddComponent<WL.Combat.Impact.WLWeaponFollowFx>();
|
||
if (fw.Arm(pc, rule, scale)) Aligned++; else { AlignFailed++; Object.Destroy(fw); }
|
||
}
|
||
|
||
// ── 주력 · 각성: 원본 Set() 훅 ─────────────────────────────────────────────────────
|
||
/// <summary>원본 Set() 훅(1줄) — 표에 있는 스킬이면 원본 진행을 건너뛰고 WL 진행을 시작한다(true). 꺼져 있거나 표에 없으면 false(원본 그대로).</summary>
|
||
public static bool TakeOver(ProjectileBase pb)
|
||
{
|
||
try
|
||
{
|
||
var c = Cfg; if (c.enabled == 0 || pb == null) return false;
|
||
var pd = pb.Get_ProjectTileData(); if (pd == null || DSUtil.CheckNull(pd.shooter)) return false;
|
||
WLStaffEarthSkill r = null; int cls = pd.shooter.Get_ID(); // #975 직업이 맞는 행 먼저 · 없으면 classId 0 행
|
||
if (SummonPass) return false; // #1002 중력 끝에 원본 소환을 다시 부르는 중
|
||
int wid = EquippedWeaponId(); // #1000 같은 직업 다른 무기 구분
|
||
if (c.skills != null) foreach (var x in c.skills) if (x != null && x.skill == pb.GetType().Name && x.classId == cls && WeaponOk(x, wid)) { r = x; break; }
|
||
if (r == null && c.skills != null) foreach (var x in c.skills) if (x != null && x.skill == pb.GetType().Name && x.classId == 0 && WeaponOk(x, wid)) { r = x; break; }
|
||
if (r == null) return false;
|
||
if (r.noExtra != 0) { pd.e_SkillExtraType = eSkillExtraType.None; pd.m_skillTypeConfigTable = null; pd.SetSkillExtraType = false; NoExtra++; } // #991 PD 「오브 기본 스킬 시전 시 몬스터에게 불타는 이펙트가 발생하지 않도록(화상 효과 X)」 — 이 발사의 원본 부가 효과(운석 소환 = Burn)를 지움 · Actor.Check_Extra 가 None 이면 바로 돌아간다(#977 활 멀티샷과 같은 방식)
|
||
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, r);
|
||
TakeOvers++;
|
||
return true;
|
||
}
|
||
catch (System.Exception ex) { Errors++; LastLog = "넘겨받기 실패 — " + ex.Message; return false; }
|
||
}
|
||
|
||
/// <summary>center 에서 reach(≤ radius) 안의 적(아직 안 맞은)에게 원본 투사체 피해 + 띄우기 · 기절.</summary>
|
||
internal static void HitAll(ProjectileBase pb, Actor shooter, Vector3 center, WLStaffEarthSkill r, float reach, HashSet<Actor> done, Vector3 pushDir = default(Vector3))
|
||
{
|
||
var info = ActorInfo.Ins; if (info == null || pb == null || DSUtil.CheckNull(shooter)) return;
|
||
var list = info.Get_EnemyActors(shooter.IsEnemy(), center, r.radius + 3f); if (list == null) return;
|
||
try
|
||
{
|
||
for (int k = 0; k < list.Count; k++)
|
||
{
|
||
var a = list[k]; if (a == null || a.IsDead() || done.Contains(a)) continue;
|
||
var v = a.Get_position() - center; v.y = 0f;
|
||
if (v.magnitude > Mathf.Min(reach, r.radius) + WL.Combat.WLCombatMotionSettings.BodyRadius(a)) continue;
|
||
var col = a.GetComponent<Collider>(); if (col == null || !col.enabled) continue;
|
||
done.Add(a);
|
||
if (!pb.ProcessCollider(col, false)) continue;
|
||
Hits++;
|
||
if (r.pushDistance > 0f && pushDir.sqrMagnitude > 1e-4f) { WLLaunch.FlingDir(a, pushDir, r.pushDistance, Mathf.Max(0.1f, r.launchSeconds), r.launchHeight, r.stunSeconds, false); Launches++; } // #1001 진행 방향으로 밀며 띄움
|
||
else if ((r.launchHeight > 0f || r.stunSeconds > 0f) && WLLaunch.Apply(a, r.launchHeight, r.launchSeconds, r.stunSeconds)) Launches++;
|
||
}
|
||
}
|
||
finally { info.Release_Actors(list); }
|
||
}
|
||
|
||
/// <summary>원본 투사체 끄기(풀 반납) — ProjectileBase.Off(bool, bool) 는 protected 라 리플렉션.</summary>
|
||
internal static void OffOriginal(ProjectileBase pb)
|
||
{
|
||
if (pb == null || !pb.gameObject.activeInHierarchy) return;
|
||
if (s_off == null) s_off = typeof(ProjectileBase).GetMethod("Off", BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public, null, new[] { typeof(bool), typeof(bool) }, null);
|
||
try { if (s_off != null) s_off.Invoke(pb, new object[] { true, false }); else pb.gameObject.SetActive(false); }
|
||
catch (System.Exception ex) { Errors++; LastLog = "Off 실패 — " + ex.Message; pb.gameObject.SetActive(false); }
|
||
}
|
||
|
||
// ── 3타: 쏘는 순간 표시 → 피해 확정에 바위 솟음 · 띄우기 · 기절 ─────────────────────
|
||
/// <summary>투사체 Set 끝 훅(WLProjectileVisual.OnSet 첫 줄) — 쏘는 순간 애니메이터 상태가 popup.stateName 이면 그 투사체(인스턴스)를 표시 · 아니면 지운다(풀 재사용).</summary>
|
||
public static void OnProjectileSet(ProjectileBase pb)
|
||
{
|
||
try
|
||
{
|
||
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;
|
||
WLStaffEarthPopup hit = null; int wid = EquippedWeaponId();
|
||
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;
|
||
if (p.weaponIds != null && p.weaponIds.Length > 0 && System.Array.IndexOf(p.weaponIds, wid) < 0) continue; // #1002 같은 직업 다른 무기
|
||
// #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; }
|
||
}
|
||
|
||
/// <summary>#967 투사체 Set 끝 훅(WLProjectileVisual.OnSet 마지막 · 겉모습 자식까지 붙은 뒤) — OnProjectileSet 이 고른 3타 투사체를 메테오 러너로 넘겨받는다(겉모습 · 판정 상자 · 이동을 끄고 대상 자리에 운석 1개).</summary>
|
||
public static void AfterProjectileSet(ProjectileBase pb)
|
||
{
|
||
if (pb == null || !ReferenceEquals(s_pendingMeteor, pb)) return;
|
||
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, 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; }
|
||
}
|
||
|
||
static void OnHitConfirmed(in HitConfirmedEvent e)
|
||
{
|
||
try
|
||
{
|
||
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++;
|
||
if ((p.launchHeight > 0f || p.stunSeconds > 0f) && WLLaunch.Apply(e.victim, p.launchHeight, p.launchSeconds, p.stunSeconds)) Launches++;
|
||
}
|
||
catch (System.Exception ex) { Errors++; LastLog = "3타 명중 실패 — " + ex.Message; }
|
||
}
|
||
|
||
// ── #994 3타 발사 전 모으기(WLStaffPreChargeTick 이 매 프레임 부른다) ─────────────────────
|
||
static Animator s_preAn; static float s_preUntil, s_preSpeed; static bool s_preArmed = true; static string s_preState = "";
|
||
public static int PreCharges; public static string PreChargeLog = "";
|
||
internal static void TickPreCharge()
|
||
{
|
||
try
|
||
{
|
||
var c = Cfg; if (c.enabled == 0) return;
|
||
var pc = MyValue.MyPC as PCActor; var an = pc != null ? pc.m_animation : null; if (an == null) return;
|
||
if (s_preAn != null) // 멈춰 둔 동안
|
||
{
|
||
if (s_preAn != an || an.speed != 0f) { s_preAn = null; PreChargeLog += " · 손 뗌(다른 코드)"; return; }
|
||
var sn = an.IsInTransition(0) ? an.GetNextAnimatorStateInfo(0) : an.GetCurrentAnimatorStateInfo(0);
|
||
bool moved = !sn.IsName(s_preState); // 구르기 등 다른 코드가 상태를 바꿈 = 바로 풀기
|
||
if (moved || Time.time >= s_preUntil) { an.speed = s_preSpeed > 0f ? s_preSpeed : 1f; s_preAn = null; PreChargeLog += moved ? " · 상태 바뀜 풀림" : " · 풀림"; }
|
||
return;
|
||
}
|
||
if (pc.Get_CurAnim() >= eAnim.Skill) { s_preArmed = false; return; } // 스킬(화염 광선이 attack3 자세를 씀) 중엔 안 함
|
||
WLStaffEarthPopup p = null;
|
||
int wid = EquippedWeaponId(); // #1003 같은 직업 다른 무기(마법책 9009 만 · 셉터 9005 는 없음)
|
||
for (int i = -1; i < (c.popups != null ? c.popups.Length : 0) && p == null; i++) { var x = i < 0 ? c.popup : c.popups[i]; if (x != null && x.preChargeSeconds > 0f && x.classId == pc.Get_ID() && (x.weaponIds == null || x.weaponIds.Length == 0 || System.Array.IndexOf(x.weaponIds, wid) >= 0)) p = x; }
|
||
if (p == null) return;
|
||
bool tr = an.IsInTransition(0); var st = tr ? an.GetNextAnimatorStateInfo(0) : an.GetCurrentAnimatorStateInfo(0);
|
||
if (!st.IsName(p.stateName)) { s_preArmed = true; return; } // 그 상태 밖 = 다음 진입에 다시
|
||
if (!s_preArmed) return;
|
||
s_preArmed = false;
|
||
float into = st.normalizedTime * st.length;
|
||
if (into > p.preChargeMaxIn) { PreChargeLog = "늦음 " + into.ToString("F2") + " s"; return; } // 이미 발사 이벤트 근처 = 이번엔 안 멈춤
|
||
s_preSpeed = an.speed; an.speed = 0f; s_preAn = an; s_preUntil = Time.time + p.preChargeSeconds; s_preState = p.stateName;
|
||
var sock = Socket(pc, p.preChargeFx.socket);
|
||
if (p.preChargeFx.head == 3) sock = EmptyHand(pc) ?? sock; // #999 빈 손 뼈에 붙여 따라감(자리 = 손 뼈)
|
||
Spawn(p.preChargeFx, pc, sock, sock != null ? Tip(sock, p.preChargeFx) : pc.Get_Center_position(), p.preChargeSeconds);
|
||
PreCharges++; PreChargeLog = "모으기 " + p.stateName + (tr ? " 전환 중" : "") + " " + into.ToString("F2") + " s · " + p.preChargeSeconds + " s" + (sock != null ? " · " + sock.name : "");
|
||
}
|
||
catch (System.Exception ex) { Errors++; LastLog = "3타 모으기 실패 — " + ex.Message; }
|
||
}
|
||
|
||
static GameObject Prefab(string path)
|
||
{
|
||
GameObject p;
|
||
if (s_prefabs.TryGetValue(path, out p) && p != null) return p;
|
||
p = Resources.Load<GameObject>(path);
|
||
if (p == null) LastLog = "이펙트 없음 " + path;
|
||
s_prefabs[path] = p;
|
||
return p;
|
||
}
|
||
|
||
static Vector3 V(float[] a) { return a != null && a.Length >= 3 ? new Vector3(a[0], a[1], a[2]) : Vector3.zero; }
|
||
}
|
||
|
||
/// <summary>#994 3타 발사 전 모으기 — 숨김 객체 1 개가 매 프레임 WLStaffEarth.TickPreCharge 를 부른다(애니메이터 갱신 전 Update).</summary>
|
||
public sealed class WLStaffPreChargeTick : MonoBehaviour
|
||
{
|
||
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.AfterSceneLoad)]
|
||
static void Boot()
|
||
{
|
||
if (FindFirstObjectByType<WLStaffPreChargeTick>() != null) return;
|
||
var go = new GameObject("[WL994] StaffPreCharge"); go.hideFlags = HideFlags.HideInHierarchy; DontDestroyOnLoad(go); go.AddComponent<WLStaffPreChargeTick>();
|
||
}
|
||
void Update() { WLStaffEarth.TickPreCharge(); }
|
||
}
|
||
|
||
/// <summary>#935 넘겨받은 원본 스킬 투사체 1 개의 진행(이펙트 → 판정 → 끄기). 숨김 러너 1 개가 코루틴을 돌린다(원본 투사체가 먼저 꺼져도 판정은 이어감).</summary>
|
||
public sealed class WLStaffEarthRunner : MonoBehaviour
|
||
{
|
||
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)
|
||
{
|
||
if (r.meteorImpact > 0f) yield return new WaitForSeconds(r.meteorImpact);
|
||
if (pb == null || DSUtil.CheckNull(shooter)) yield break;
|
||
if (WLStaffEarth.SpawnAt(r.traceFx, shooter, pt, shooter.transform.forward, 0f) != null) WLStaffEarth.Traces++; // #1006 지면 흔적
|
||
WLStaffEarth.HitAll(pb, shooter, pt, r, r.radius, new HashSet<Actor>());
|
||
}
|
||
|
||
IEnumerator Run(ProjectileBase pb, ProjectileData pd, WLStaffEarthSkill r)
|
||
{
|
||
// 원본 겉모습 · 판정 상자 · Update(대상 따라가기 · 대상 없으면 끄기) 끔 — 끈 뒤 되돌린다(풀 재사용)
|
||
var ren = new List<Renderer>(); foreach (var x in pb.GetComponentsInChildren<Renderer>(true)) if (x.enabled) { x.enabled = false; ren.Add(x); }
|
||
var col = new List<Collider>(); foreach (var x in pb.GetComponentsInChildren<Collider>(true)) if (x.enabled) { x.enabled = false; col.Add(x); }
|
||
foreach (var ps in pb.GetComponentsInChildren<ParticleSystem>(true)) ps.Stop(true, ParticleSystemStopBehavior.StopEmittingAndClear);
|
||
bool wasEnabled = pb.enabled; pb.enabled = false;
|
||
var shooter = pd.shooter; var target = pd.target;
|
||
if (r.pullSeconds > 0f) { yield return Gravity(pb, pd, shooter, target, r, ren, col, wasEnabled); yield break; } // #1002 마법책 주력(골렘 소환) — 끝에 원본 Set 을 다시 불러 소환 · 되돌림도 그 안에서
|
||
if (r.rushDistance > 0f || r.fanCount > 0 || r.boltCount > 0 || r.spinHits > 0 || r.slamAt > 0f) // #996 창 주력(돌진) · 각성(부채꼴 회오리) · #1000 마법책 각성(볼트 난사) · #1001 대검 기본(회전 연타 · 내려치기)
|
||
{
|
||
if (r.rushDistance > 0f) yield return Rush(pb, shooter as PCActor, r); else if (r.boltCount > 0) yield return Bolts(pb, shooter, target, r); else if (r.spinHits > 0 || r.slamAt > 0f) yield return JumpSpin(pb, shooter as PCActor, r); else yield return Fan(pb, shooter, target, r);
|
||
if (r.endAfter > 0f) yield return new WaitForSeconds(r.endAfter);
|
||
if (pb != null) { pb.enabled = wasEnabled; WLStaffEarth.OffOriginal(pb); }
|
||
foreach (var x in ren) if (x != null) x.enabled = true;
|
||
foreach (var x in col) if (x != null) x.enabled = true;
|
||
yield break;
|
||
}
|
||
if (r.meteors > 0) // #963 메테오 — 운석마다 무작위 자리 · 땅에 닿는 시각에 그 자리 판정 + 기절
|
||
{
|
||
// #964 PD 「지팡이 각성 스킬이 플레이어 주위가 아니라 타겟 된 적 중심으로 3 m 범위 내 랜덤」 — 투사체 대상이 비거나 죽었으면 PC 가 겨눈 대상(Get_Target)으로 · 그것도 없으면 이전처럼 시전자 앞 3 m
|
||
WLStaffEarth.MeteorSrc = !DSUtil.CheckNull(target) && !target.IsDead() ? "투사체 대상" : "투사체 대상 없음";
|
||
if ((DSUtil.CheckNull(target) || target.IsDead()) && shooter != null) { var t2 = shooter.Get_Target(); if (!DSUtil.CheckNull(t2) && !t2.IsDead()) { target = t2; WLStaffEarth.MeteorSrc += " → PC 대상"; } else WLStaffEarth.MeteorSrc += " → 시전자 앞"; }
|
||
WLStaffEarth.MeteorPts.Clear(); WLStaffEarth.MeteorCenters.Clear();
|
||
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 = 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 + "개 · " + (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;
|
||
foreach (var x in col) if (x != null) x.enabled = true;
|
||
yield break;
|
||
}
|
||
Vector3 center = r.center == "caster" || DSUtil.CheckNull(target) ? shooter.Get_position() : target.Get_position();
|
||
Vector3 fwd = center - shooter.Get_position(); if (r.center == "caster" || fwd.sqrMagnitude < 0.01f) fwd = shooter.transform.forward;
|
||
WLStaffEarth.SpawnAt(r.fx, shooter, center, fwd, 0f);
|
||
WLStaffEarth.LastLog = r.skill + " → " + r.fx.prefab + " @" + center.ToString("F1");
|
||
if (r.hitDelay > 0f) yield return new WaitForSeconds(r.hitDelay);
|
||
if (!DSUtil.CheckNull(shooter) && WLStaffEarth.SpawnAt(r.traceFx, shooter, center, fwd, 0f) != null) WLStaffEarth.Traces++; // #1006 지면 흔적
|
||
var done = new HashSet<Actor>();
|
||
if (r.spreadSpeed > 0f)
|
||
{
|
||
float t0 = Time.time, last = r.radius / r.spreadSpeed + 0.05f;
|
||
while (true)
|
||
{
|
||
float t = Time.time - t0;
|
||
WLStaffEarth.HitAll(pb, shooter, center, r, t * r.spreadSpeed, done);
|
||
if (t >= last) break;
|
||
yield return null;
|
||
}
|
||
}
|
||
else WLStaffEarth.HitAll(pb, shooter, center, r, r.radius, done);
|
||
if (r.endAfter > 0f) yield return new WaitForSeconds(r.endAfter);
|
||
if (pb != null) { pb.enabled = wasEnabled; WLStaffEarth.OffOriginal(pb); }
|
||
foreach (var x in ren) if (x != null) x.enabled = true;
|
||
foreach (var x in col) if (x != null) x.enabled = true;
|
||
}
|
||
|
||
/// <summary>#1002 중력 → 지면 폭발 → 원본 소환. center 에 pullFx 를 띄우고 pullSeconds 동안 pullRadius 안 적을 중심으로 끌어당긴다(NavMeshAgent.Move · 속도는 시간에 비례해 빨라짐 · 경직으로 붙잡음) →
|
||
/// 끝에 지면 fx + hitDelay 뒤 radius 안 적 피해 · 기절 → SummonPass 로 원본 Set 을 다시 불러(prefab 칸만 summonPrefab 으로 잠시 바꿈) 소환 → 새로 켜진 소환체를 center 로 옮기고 배율을 곱한다.</summary>
|
||
IEnumerator Gravity(ProjectileBase pb, ProjectileData pd, Actor shooter, Actor target, WLStaffEarthSkill r, List<Renderer> ren, List<Collider> col, bool wasEnabled)
|
||
{
|
||
if (DSUtil.CheckNull(shooter)) yield break;
|
||
// 소환 스킬 투사체의 대상은 시전자 자신이었다(1차 Play 실측 · 중심 = PC 옆 0.47 m) → 적이 아니면 PC 가 겨눈 대상 · 그것도 없으면 시전자 앞 3 m
|
||
if (DSUtil.CheckNull(target) || target.IsDead() || target == shooter || target.IsEnemy() == shooter.IsEnemy()) { var t2 = shooter.Get_Target(); target = !DSUtil.CheckNull(t2) && !t2.IsDead() && t2.IsEnemy() != shooter.IsEnemy() ? t2 : null; }
|
||
Vector3 center = r.center == "caster" ? shooter.Get_position() : !DSUtil.CheckNull(target) && !target.IsDead() ? target.Get_position() : shooter.Get_position() + shooter.transform.forward * 3f;
|
||
Vector3 fwd = center - shooter.Get_position(); fwd.y = 0f; if (fwd.sqrMagnitude < 0.01f) fwd = shooter.transform.forward;
|
||
WLStaffEarth.MeteorCenter = center;
|
||
WLStaffEarth.SpawnAt(r.pullFx, shooter, center, fwd, 0f);
|
||
var info = ActorInfo.Ins; var pulled = new HashSet<Actor>(); float t = 0f;
|
||
while (t < r.pullSeconds && info != null && !DSUtil.CheckNull(shooter))
|
||
{
|
||
float step = Mathf.Min(r.pullMaxSpeed, r.pullAccel * t) * Time.deltaTime;
|
||
var list = info.Get_EnemyActors(shooter.IsEnemy(), center, r.pullRadius + 3f);
|
||
if (list != null)
|
||
try
|
||
{
|
||
for (int k = 0; k < list.Count; k++)
|
||
{
|
||
var a = list[k]; if (a == null || a.IsDead() || a.IsSubRole(eSubRol.Boss)) continue;
|
||
var v = center - a.Get_position(); v.y = 0f; float d = v.magnitude;
|
||
if (d > r.pullRadius + WL.Combat.WLCombatMotionSettings.BodyRadius(a) || d <= r.pullStop) continue;
|
||
var ag = a.Get_Agent(); if (ag == null || !ag.enabled || !ag.isOnNavMesh) continue;
|
||
ag.Move(v / d * Mathf.Min(step, d - r.pullStop));
|
||
WL.Combat.Reaction.MobStagger.Extend(a, 0.1f);
|
||
if (pulled.Add(a)) WLStaffEarth.Pulled++;
|
||
}
|
||
}
|
||
finally { info.Release_Actors(list); }
|
||
WLStaffEarth.PullFrames++;
|
||
yield return null; t += Time.deltaTime;
|
||
}
|
||
WLStaffEarth.SpawnAt(r.fx, shooter, center, fwd, 0f);
|
||
if (r.hitDelay > 0f) yield return new WaitForSeconds(r.hitDelay);
|
||
if (pb != null && !DSUtil.CheckNull(shooter)) WLStaffEarth.HitAll(pb, shooter, center, r, r.radius, new HashSet<Actor>());
|
||
// 원본 소환 — 이 투사체를 되돌린 뒤 원본 Set 을 한 번 더(소환 콜백이 투사체를 끈다)
|
||
var fxBefore = new HashSet<int>(); foreach (var x in Object.FindObjectsByType<Transform>(FindObjectsSortMode.None)) if (x.name.StartsWith("Effect_Summoning_Golem")) fxBefore.Add(x.GetInstanceID());
|
||
// 자리 · 방향 · 배율은 원본 소환 콜백 안(AfterSummon 훅 · summon.Set 바로 뒤)에서 같은 프레임에 — 3차 Play 실측: 다음 프레임에 옮기면 원래 크기 골렘이 시전자 앞에 약 0.05 s 보였다
|
||
WLStaffEarth.ReserveSummon(shooter, center, Quaternion.LookRotation(fwd.normalized), Mathf.Max(0.01f, r.summonFit * r.summonScale), r, pd);
|
||
if (pb != null && !DSUtil.CheckNull(shooter))
|
||
{
|
||
pb.enabled = wasEnabled; foreach (var x in col) if (x != null) x.enabled = true;
|
||
var sg = pb as Skill_Golem; string keep = sg != null ? sg.prefab : null;
|
||
WLStaffEarth.SummonPass = true;
|
||
try { if (sg != null && !string.IsNullOrEmpty(r.summonPrefab)) sg.prefab = r.summonPrefab; pb.Set(pd); }
|
||
catch (System.Exception ex) { WLStaffEarth.Errors++; WLStaffEarth.LastLog = "소환 실패 — " + ex.Message; }
|
||
finally { WLStaffEarth.SummonPass = false; if (sg != null) sg.prefab = keep; }
|
||
}
|
||
float w = 0f;
|
||
while (WLStaffEarth.LastSummon == null && w < 3f) { yield return null; w += Time.deltaTime; }
|
||
foreach (var x in ren) if (x != null) x.enabled = true;
|
||
var got = WLStaffEarth.LastSummon;
|
||
if (got == null) { WLStaffEarth.SummonLog = "소환체 못 찾음 " + w.ToString("F1") + " s"; yield break; }
|
||
WLStaffEarth.Summons++;
|
||
WLStaffEarth.SummonLog = got.name + " @" + center.ToString("F1") + " ×" + got.transform.localScale.x.ToString("F2") + " · 끌림 " + pulled.Count + " · 소환 " + w.ToString("F2") + " s";
|
||
// 원본 소환 섬광(SummonActor.Set = 시전자 앞 1 m · 주소 로드라 몇 프레임 늦게 켜진다 — 2차 Play 실측 PC 자리에 남음)도 골렘 자리로
|
||
int moved = 0; float mv = 0f;
|
||
while (mv < 1f && got != null)
|
||
{
|
||
foreach (var x in Object.FindObjectsByType<Transform>(FindObjectsSortMode.None))
|
||
if (x.name.StartsWith("Effect_Summoning_Golem") && fxBefore.Add(x.GetInstanceID())) { x.position = got.Get_position(); moved++; }
|
||
yield return null; mv += Time.deltaTime;
|
||
}
|
||
WLStaffEarth.SummonLog += " · 소환 섬광 옮김 " + moved;
|
||
}
|
||
|
||
/// <summary>#1000 · #1001 볼트 난사 — 빈 손(손바닥)에서 1발씩 · 대상 쪽에서 좌우 · 위로 틀어 낸 조절점을 지나 대상의 지금 몸 중심으로 휘어 날아가(2차 베지어 · 끝점은 매 프레임 대상 = 유도) 도착하면 짝 피격 이펙트 + 그 자리 판정.
|
||
/// #1001 PD 「마법책 각성 스킬이 왜 에셋과 다르게 색상이 에너지 볼 같은 느낌이 나지 않는거지? … 불규칙한 궤적이지만 타겟을 향해 날아가는 에너지 볼트를 난사하는 스킬을 원했어」 · 「보유한 리소스 중 마법책 각성 스킬에 적합한 에셋이 없는지 체크해보고 적합한 투사체 이펙트로 교체해줘」</summary>
|
||
IEnumerator Bolts(ProjectileBase pb, Actor shooter, Actor target, WLStaffEarthSkill r)
|
||
{
|
||
var pc = shooter as PCActor; if (pc == null) yield break;
|
||
GameObject orb = LoadFx(r.boltPrefab), hit = LoadFx(r.boltHitFx), muz = LoadFx(r.boltMuzzleFx);
|
||
if ((DSUtil.CheckNull(target) || target.IsDead()) && shooter != null) { var t2 = shooter.Get_Target(); if (!DSUtil.CheckNull(t2) && !t2.IsDead()) target = t2; }
|
||
int hits0 = WLStaffEarth.Hits; var flying = new int[1]; float t0 = Time.time;
|
||
for (int i = 0; i < r.boltCount; i++)
|
||
{
|
||
if (pc == null || pc.IsDead()) break;
|
||
var hand = WLStaffEarth.EmptyHand(pc); Vector3 from = hand != null ? hand.position : pc.Get_Center_position();
|
||
Actor tg = !DSUtil.CheckNull(target) && !target.IsDead() ? target : null;
|
||
Vector3 aim = tg != null ? WL.Combat.Impact.WLImpactFx.BodyCenter(tg) : from + pc.transform.forward * r.boltRange;
|
||
var to = aim - from; float dist = Mathf.Clamp(to.magnitude, 0.3f, r.boltRange); var fwd = to.sqrMagnitude > 1e-4f ? to.normalized : pc.transform.forward;
|
||
var side = Vector3.Cross(Vector3.up, fwd); if (side.sqrMagnitude < 1e-4f) side = pc.transform.right; side.Normalize();
|
||
float lat = Random.Range(-1f, 1f); lat = Mathf.Sign(lat) * Mathf.Lerp(0.35f, 1f, Mathf.Abs(lat)); // 가운데로 몰리지 않게
|
||
Vector3 ctrl = from + fwd * dist * 0.4f + side * lat * r.boltCurve + Vector3.up * Random.Range(-0.15f, 0.6f) * r.boltCurve;
|
||
float fly = dist * 1.2f / Mathf.Max(0.5f, r.boltSpeed);
|
||
if (muz != null) { var m = Object.Instantiate(muz, from, Quaternion.LookRotation((ctrl - from).normalized)); m.transform.localScale = Vector3.one * r.boltMuzzleScale; Object.Destroy(m, 1.5f); }
|
||
flying[0]++; StartCoroutine(Fly(pb, shooter, r, orb, hit, from, ctrl, tg, aim, fly, flying));
|
||
float next = t0 + (i + 1) * Mathf.Max(0.01f, r.boltInterval);
|
||
while (Time.time < next) yield return null;
|
||
}
|
||
float guard = 0f; while (flying[0] > 0 && guard < 5f) { guard += Time.deltaTime; yield return null; }
|
||
WLStaffEarth.LastLog = r.skill + " 볼트 " + r.boltCount + "발 · " + r.boltInterval + " s · 명중 " + (WLStaffEarth.Hits - hits0) + (DSUtil.CheckNull(target) ? " · 대상 없음(정면)" : "");
|
||
}
|
||
static GameObject LoadFx(string path) { return string.IsNullOrEmpty(path) ? null : Resources.Load<GameObject>(path); }
|
||
IEnumerator Fly(ProjectileBase pb, Actor shooter, WLStaffEarthSkill r, GameObject orbP, GameObject hitP, Vector3 from, Vector3 ctrl, Actor tg, Vector3 end, float fly, int[] flying)
|
||
{
|
||
GameObject go = null;
|
||
if (orbP != null)
|
||
{
|
||
go = Object.Instantiate(orbP, from, Quaternion.LookRotation((ctrl - from).normalized)); go.transform.localScale = Vector3.one * r.boltScale;
|
||
foreach (var tr in go.GetComponentsInChildren<TrailRenderer>(true)) { tr.widthMultiplier *= r.boltTrailWidth > 0f ? r.boltTrailWidth : r.boltScale; if (r.boltTrailTime > 0f) tr.time = r.boltTrailTime; tr.Clear(); } // 꼬리 너비는 배율이 안 먹는다 · #1001 꼬리 = 궤적이 보이게 따로
|
||
if (r.boltOwnTrail > 0f) // #1001 궤적 리본 — 팩 꼬리(스크롤 셰이더 · 어두운 남색)가 밝은 바닥에서 안 보여(3회 캡처) 공에 보이는 보라 빛 재질로 끝이 가늘어지는 꼬리를 하나 더
|
||
{
|
||
var pr = go.GetComponent<ParticleSystemRenderer>(); var own = go.AddComponent<TrailRenderer>();
|
||
own.sharedMaterial = pr != null ? pr.sharedMaterial : null; own.time = r.boltTrailTime > 0f ? r.boltTrailTime : 0.35f; own.widthMultiplier = r.boltOwnTrail;
|
||
own.widthCurve = AnimationCurve.Linear(0f, 1f, 1f, 0f); own.minVertexDistance = 0.04f; own.alignment = LineAlignment.View; own.textureMode = LineTextureMode.Stretch; own.autodestruct = false;
|
||
var g = new Gradient(); g.SetKeys(new[] { new GradientColorKey(new Color(1f, 0.7f, 1f), 0f), new GradientColorKey(new Color(0.6f, 0.25f, 1f), 1f) }, new[] { new GradientAlphaKey(1f, 0f), new GradientAlphaKey(0f, 1f) }); own.colorGradient = g;
|
||
own.Clear();
|
||
}
|
||
WLStaffEarth.BoltsShot++;
|
||
}
|
||
float t = 0f; Vector3 prev = from;
|
||
while (t < fly)
|
||
{
|
||
yield return null; t += Time.deltaTime; float k = Mathf.Clamp01(t / fly), u = 1f - k;
|
||
if (tg != null && !tg.IsDead()) end = WL.Combat.Impact.WLImpactFx.BodyCenter(tg); // 유도 — 끝점 = 대상의 지금 몸 중심
|
||
var p = u * u * from + 2f * u * k * ctrl + k * k * end;
|
||
if (go != null) { go.transform.position = p; var d = p - prev; if (d.sqrMagnitude > 1e-6f) go.transform.rotation = Quaternion.LookRotation(d.normalized); }
|
||
prev = p;
|
||
}
|
||
if (hitP != null) { var h = Object.Instantiate(hitP, end, Quaternion.identity); h.transform.localScale = Vector3.one * r.boltHitScale; Object.Destroy(h, 2f); }
|
||
if (pb != null && !DSUtil.CheckNull(shooter))
|
||
{
|
||
WL.Combat.Impact.WLImpactFx.SuppressSkillHit = true; // 피격 이펙트 = 볼트 짝 명중 이펙트(공통 지팡이 스킬 피격 WL_Hit_StaffSkill 겹침 끔)
|
||
try { WLStaffEarth.HitAll(pb, shooter, end, r, r.boltRadius, new HashSet<Actor>()); }
|
||
finally { WL.Combat.Impact.WLImpactFx.SuppressSkillHit = false; }
|
||
}
|
||
if (go != null)
|
||
{
|
||
foreach (var ps in go.GetComponentsInChildren<ParticleSystem>(true)) ps.Stop(false, ParticleSystemStopBehavior.StopEmittingAndClear); // 공은 명중과 함께 사라지고 꼬리만 흐려짐
|
||
float trail = 0f; foreach (var tr in go.GetComponentsInChildren<TrailRenderer>(true)) { tr.emitting = false; trail = Mathf.Max(trail, tr.time); }
|
||
Object.Destroy(go, Mathf.Clamp(trail, 0.1f, 1.5f));
|
||
}
|
||
flying[0]--;
|
||
}
|
||
|
||
/// <summary>#1001 대검 기본 — 점프(WLJumpSlam 제자리 점프 · 시전 = 점프 시작) 동안 회전 연타 spinHits 번(피해 × spinDamage · PC 정면으로 spinPush m) → slamAt 초 착지 내려치기(원본 피해 · 정면 slamPush m · slamLaunch m 띄움).</summary>
|
||
IEnumerator JumpSpin(ProjectileBase pb, PCActor pc, WLStaffEarthSkill r)
|
||
{
|
||
if (pc == null) yield break; float t0 = Time.time; int hits0 = WLStaffEarth.Hits, spun = 0, landed = 0;
|
||
for (int n = 0; n < r.spinHits; n++)
|
||
{
|
||
while (Time.time - t0 < r.spinHitStart + n * r.spinHitInterval) yield return null;
|
||
if (pc == null || pc.IsDead()) yield break;
|
||
spun += Strike(pb, pc, pc.Get_position(), r.spinRadius, r.spinDamage, r.spinPush, 0.08f, 0f);
|
||
}
|
||
if (r.slamAt > 0f)
|
||
{
|
||
while (Time.time - t0 < r.slamAt) yield return null;
|
||
if (pc != null && !pc.IsDead()) { var f = pc.transform.forward; f.y = 0f; landed = Strike(pb, pc, pc.Get_position() + f.normalized * r.slamForward, r.radius, -1f, r.slamPush, r.slamSeconds, r.slamLaunch); }
|
||
}
|
||
WLStaffEarth.LastLog = r.skill + " 회전 연타 " + r.spinHits + "번 · 맞음 " + spun + " · 내려치기 맞음 " + landed + " · 명중 " + (WLStaffEarth.Hits - hits0);
|
||
}
|
||
/// <summary>#1001 center 둘레 reach m 적에게 원본 투사체 판정(scale ≥ 0 = 피해 배율 · WLStatus.DamageScale) + PC 정면으로 push m 밀기(height m 띄움).</summary>
|
||
static int Strike(ProjectileBase pb, PCActor pc, Vector3 center, float reach, float scale, float push, float seconds, float height)
|
||
{
|
||
var info = ActorInfo.Ins; if (info == null || pb == null) return 0;
|
||
var list = info.Get_EnemyActors(pc.IsEnemy(), center, reach + 3f); if (list == null) return 0; int n = 0;
|
||
var f = pc.transform.forward; f.y = 0f; if (f.sqrMagnitude < 1e-4f) f = Vector3.forward; f.Normalize();
|
||
try
|
||
{
|
||
for (int k = 0; k < list.Count; k++)
|
||
{
|
||
var a = list[k]; if (a == null || a.IsDead()) continue;
|
||
var v = a.Get_position() - center; v.y = 0f; if (v.magnitude > reach + WL.Combat.WLCombatMotionSettings.BodyRadius(a)) continue;
|
||
var col = a.GetComponent<Collider>(); if (col == null || !col.enabled) continue;
|
||
WL.Combat.Status.WLStatus.DamageScale = scale;
|
||
bool ok; try { ok = pb.ProcessCollider(col, false); } finally { WL.Combat.Status.WLStatus.DamageScale = -1f; }
|
||
if (!ok) continue;
|
||
WLStaffEarth.Hits++; n++;
|
||
if (push > 0f) WLLaunch.FlingDir(a, f, push, Mathf.Max(0.05f, seconds), height, 0f, false);
|
||
}
|
||
}
|
||
finally { info.Release_Actors(list); }
|
||
return n;
|
||
}
|
||
|
||
/// <summary>#996 돌진 — 찌르기 자세 고정 · 정면으로 rushDistance m · 가상패드 회전 · 몸 둘레 적 다단 피해 + 앞으로 밀기 · 몸 감싸는 이펙트.</summary>
|
||
IEnumerator Rush(ProjectileBase pb, PCActor pc, WLStaffEarthSkill r)
|
||
{
|
||
if (pc == null) yield break;
|
||
var tg = pc.Get_Target(); // 돌진 시작 = 대상 쪽(대상 없으면 지금 정면) · 이후는 가상패드로 회전
|
||
if (!DSUtil.CheckNull(tg) && !tg.IsDead()) { var to = tg.Get_position() - pc.Get_position(); to.y = 0f; if (to.sqrMagnitude > 1e-4f) pc.transform.rotation = Quaternion.LookRotation(to.normalized); }
|
||
var an = pc.m_animation; float sp0 = an != null && an.speed > 0f ? an.speed : 1f;
|
||
if (OptionInfo.Ins != null) OptionInfo.Ins.Set_PCMove(pc, false);
|
||
pc.Set_CurAnim(eAnim.Skill);
|
||
if (an != null && !string.IsNullOrEmpty(r.rushHoldState)) { an.speed = 0f; an.Play(r.rushHoldState, 0, Mathf.Clamp01(r.rushHoldNormalized)); an.Update(0f); }
|
||
var fx = WLStaffEarth.SpawnAt(r.rushFx, pc, pc.Get_position(), pc.transform.forward, r.rushSeconds);
|
||
if (fx != null) { fx.transform.SetParent(pc.transform, true); if (r.rushFx.sustain != 0) WLStaffEarth.Sustain(fx); }
|
||
GameObject wrap = null; bool fxStopped = false, wrapStopped = false; // #999 감싸기 1개를 돌진 내내(sustain)
|
||
float nextFx3 = 0f;
|
||
var agent = pc.GetComponent<UnityEngine.AI.NavMeshAgent>();
|
||
float speed = r.rushDistance / Mathf.Max(0.05f, r.rushSeconds), t = 0f, next = 0f, moved = 0f, nextFx2 = 0f; int hits0 = WLStaffEarth.Hits, pushes = 0, turns = 0; bool canceled = false;
|
||
while (t < r.rushSeconds)
|
||
{
|
||
if (pc == null || pc.IsDead()) { canceled = true; break; }
|
||
if (pc.Get_CurAnim() == eAnim.Dodge) { canceled = true; break; } // 구르기 = 취소(화염 광선과 같은 규칙)
|
||
// 방향 전환 — 가상패드 축(카메라 정면 기준) 쪽으로 몸을 돌린다
|
||
var a = WLStaffLaserRunner.PadAxis();
|
||
if (a.sqrMagnitude >= r.rushSteerDeadzone * r.rushSteerDeadzone)
|
||
{
|
||
var cam = Camera.main; var cf = cam != null ? cam.transform.forward : Vector3.forward; cf.y = 0f; if (cf.sqrMagnitude < 1e-4f) cf = Vector3.forward; cf.Normalize();
|
||
var want = cf * a.y + Vector3.Cross(Vector3.up, cf) * a.x;
|
||
if (want.sqrMagnitude > 1e-4f) { pc.transform.rotation = Quaternion.RotateTowards(pc.transform.rotation, Quaternion.LookRotation(want.normalized), r.rushTurnDegPerSec * Time.deltaTime); turns++; }
|
||
}
|
||
if (an != null && an.speed != 0f) an.speed = 0f;
|
||
var f = pc.transform.forward; f.y = 0f; if (f.sqrMagnitude < 1e-4f) f = Vector3.forward; f.Normalize();
|
||
float step = speed * Time.deltaTime; var p0 = pc.Get_position();
|
||
if (agent != null && agent.enabled && agent.isOnNavMesh) agent.Move(f * step); else pc.transform.position += f * step;
|
||
moved += Vector3.Distance(p0, pc.Get_position());
|
||
Carry(pc, f, step * r.rushPush, r.radius, ref pushes);
|
||
if (!string.IsNullOrEmpty(r.rushFx2.prefab) && t >= nextFx2)
|
||
{
|
||
if (r.rushFx2.sustain != 0) { nextFx2 = float.MaxValue; wrap = WLStaffEarth.SpawnAt(r.rushFx2, pc, pc.Get_position(), f, r.rushSeconds); if (wrap != null) { wrap.transform.SetParent(pc.transform, true); WLStaffEarth.Sustain(wrap); } }
|
||
else { nextFx2 += Mathf.Max(0.05f, r.rushFx2Every); var w = WLStaffEarth.SpawnAt(r.rushFx2, pc, pc.Get_position(), f, 0f); if (w != null) w.transform.SetParent(pc.transform, true); }
|
||
}
|
||
if (!string.IsNullOrEmpty(r.rushFx3.prefab) && t >= nextFx3) { nextFx3 += Mathf.Max(0.05f, r.rushFx3Every); if (WLStaffEarth.SpawnAt(r.rushFx3, pc, pc.Get_position(), f, 0f) != null) WLStaffEarth.Dusts++; } // #1000 흙먼지(지나온 길에 남김)
|
||
if (wrap != null && !wrapStopped && t >= r.rushSeconds - r.rushFx2.sustainTail) { wrapStopped = true; WLStaffEarth.StopEmit(wrap); } // 끝에 맞춰 사라지게
|
||
if (fx != null && r.rushFx.sustain != 0 && !fxStopped && t >= r.rushSeconds - r.rushFx.sustainTail) { fxStopped = true; WLStaffEarth.StopEmit(fx); }
|
||
if (t >= next) { next += Mathf.Max(0.05f, r.rushHitInterval); WLStaffEarth.HitAll(pb, pc, pc.Get_position() + f * (r.radius * 0.5f), r, r.radius, new HashSet<Actor>()); }
|
||
yield return null; t += Time.deltaTime;
|
||
}
|
||
if (fx != null && r.rushFx.sustain != 0) WLStaffEarth.ClearEnd(fx); // #999 돌진 끝 = 이펙트 끝
|
||
else if (fx != null) { foreach (var ps in fx.GetComponentsInChildren<ParticleSystem>(true)) ps.Stop(true, ParticleSystemStopBehavior.StopEmitting); fx.transform.SetParent(null, true); Destroy(fx, 0.4f); }
|
||
if (wrap != null) WLStaffEarth.ClearEnd(wrap);
|
||
if (an != null && an.speed == 0f) an.speed = sp0;
|
||
if (OptionInfo.Ins != null && pc != null) OptionInfo.Ins.Set_PCMove(pc, true);
|
||
if (!canceled && pc != null && !pc.IsDead()) pc.Play_Idle();
|
||
WLStaffEarth.LastLog = r.skill + " 돌진 " + (canceled ? "취소 " : "끝 ") + t.ToString("F2") + " s · 이동 " + moved.ToString("F2") + " m · 명중 " + (WLStaffEarth.Hits - hits0) + " · 밀침 " + pushes + " · 회전 프레임 " + turns;
|
||
}
|
||
|
||
/// <summary>#996 돌진 앞쪽(몸 반지름 + reach 안 · 정면 반원) 적을 같은 방향으로 함께 민다.</summary>
|
||
static void Carry(PCActor pc, Vector3 f, float d, float reach, ref int pushes)
|
||
{
|
||
var info = ActorInfo.Ins; if (info == null || d <= 0f) return;
|
||
var o = pc.Get_position(); var list = info.Get_EnemyActors(pc.IsEnemy(), o, reach + 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 v = a.Get_position() - o; v.y = 0f;
|
||
if (v.magnitude > reach + WL.Combat.WLCombatMotionSettings.BodyRadius(a) || Vector3.Dot(v, f) < 0f) continue;
|
||
var ag = a.GetComponent<UnityEngine.AI.NavMeshAgent>();
|
||
if (ag != null && ag.enabled && ag.isOnNavMesh) ag.Move(f * d); else a.transform.position += f * d;
|
||
pushes++;
|
||
}
|
||
}
|
||
finally { info.Release_Actors(list); }
|
||
}
|
||
|
||
/// <summary>#996 부채꼴 — 정면(대상 쪽) ±fanAngle/2 에 fanCount 개 fx 를 fanStart → fanDistance m 로 fanSeconds 동안 날리며 지나는 자리 radius 안 적을 한 번씩 피해 + 띄움 · 기절(HitAll).</summary>
|
||
IEnumerator Fan(ProjectileBase pb, Actor shooter, Actor target, WLStaffEarthSkill r)
|
||
{
|
||
if (DSUtil.CheckNull(shooter)) yield break;
|
||
var o = shooter.Get_position(); var fwd = !DSUtil.CheckNull(target) && !target.IsDead() ? target.Get_position() - o : shooter.transform.forward; fwd.y = 0f;
|
||
if (fwd.sqrMagnitude < 1e-4f) fwd = shooter.transform.forward; fwd.y = 0f; if (fwd.sqrMagnitude < 1e-4f) fwd = Vector3.forward; fwd.Normalize();
|
||
int n = Mathf.Max(1, r.fanCount); var dirs = new Vector3[n]; var fxs = new GameObject[n];
|
||
for (int i = 0; i < n; i++)
|
||
{
|
||
float ang = n == 1 ? 0f : -r.fanAngle * 0.5f + r.fanAngle * i / (n - 1);
|
||
dirs[i] = Quaternion.Euler(0f, ang, 0f) * fwd;
|
||
fxs[i] = WLStaffEarth.SpawnAt(r.fx, shooter, o + dirs[i] * r.fanStart, dirs[i], r.fanSeconds);
|
||
}
|
||
var done = new HashSet<Actor>(); float t = 0f; int hits0 = WLStaffEarth.Hits;
|
||
var s0 = new Vector3[n]; for (int i = 0; i < n; i++) s0[i] = fxs[i] != null ? fxs[i].transform.localScale : Vector3.one;
|
||
while (t <= r.fanSeconds)
|
||
{
|
||
float u = Mathf.Clamp01(t / Mathf.Max(0.05f, r.fanSeconds)), dist = Mathf.Lerp(r.fanStart, r.fanDistance, u);
|
||
float k = r.fanShrinkFrom < 1f && u > r.fanShrinkFrom ? 1f - Mathf.SmoothStep(0f, 1f, (u - r.fanShrinkFrom) / (1f - r.fanShrinkFrom)) : 1f; // #997 끝에서 점점 작아짐
|
||
for (int i = 0; i < n; i++)
|
||
{
|
||
var p = o + dirs[i] * dist;
|
||
if (fxs[i] != null) { fxs[i].transform.position = new Vector3(p.x, fxs[i].transform.position.y, p.z); fxs[i].transform.localScale = s0[i] * Mathf.Max(0.01f, k); }
|
||
WLStaffEarth.HitAll(pb, shooter, p, r, r.radius, done, dirs[i]); // #1001 회오리 진행 방향으로 밀며 띄움(pushDistance)
|
||
}
|
||
yield return null; t += Time.deltaTime;
|
||
}
|
||
foreach (var g in fxs) if (g != null) { foreach (var ps in g.GetComponentsInChildren<ParticleSystem>(true)) ps.Stop(true, ParticleSystemStopBehavior.StopEmitting); Destroy(g, Mathf.Max(0.05f, r.fanFade)); }
|
||
WLStaffEarth.LastLog = r.skill + " 부채꼴 " + n + "개 · " + r.fanAngle + "° · " + r.fanStart + " → " + r.fanDistance + " m · 명중 " + (WLStaffEarth.Hits - hits0) + " · 띄움 " + WLStaffEarth.Launches;
|
||
}
|
||
}
|
||
|
||
/// <summary>#999 PD 「이펙트가 플레이어보다 상위에 보이게해서 플레이어를 가려줘」 — 부모(PC) 기준 원래 자리에서 매 프레임 카메라 쪽으로 dist m(팩 셰이더에 깊이 검사 속성이 없어 위치로 · 창 4타 toCamera 와 같은 방식).</summary>
|
||
public sealed class WLFxToCamera : MonoBehaviour
|
||
{
|
||
public float dist; Vector3 _local; Transform _p; bool _got;
|
||
void LateUpdate()
|
||
{
|
||
var cam = Camera.main; if (cam == null) return;
|
||
if (!_got || _p != transform.parent) { _p = transform.parent; _local = _p != null ? _p.InverseTransformPoint(transform.position) : transform.position; _got = true; }
|
||
Vector3 b = _p != null ? _p.TransformPoint(_local) : _local;
|
||
transform.position = b + (cam.transform.position - b).normalized * dist;
|
||
}
|
||
}
|
||
}
|