WL #949 마법책(9009) PD 지시 4건 — ① 책이 떠 있는 동안 손 무기(셉터) 렌더러 숨김(WLFloatingBook hideHandWeapon · 폴링마다 재적용 · 직업 표 수정 행 방식은 Set_Obj 재부착이 로비 컨트롤러를 끼워 폐기) ② 마지막 타(attack2) = Mage@RangeAttack2 f16~46(오른손 내지른 채 약 0.5 s 고정 · 발사 0.35 · 재굽기 → pcanim_summoner) ③ 책 추적: 평소 = 카메라 기준 캐릭터 뒤(화면 위 · 먼쪽 0.45 · 옆 0.35 · 높이 0.72) · 시전 중(클립 Cast1/Cast2/BookCharge/BookCast) = PC 정면 0.5 m(cameraSpace · castStates · cast* 신설) ④ 스킬 시전 NRE(Actor.Use_Skill:756 ServerInfo 없음 · 테스트 씬) 가드 1줄

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
깃 관리자 2026-10-20 17:45:21 +09:00
parent 10c42cc28a
commit 4f262b3b8e
6 changed files with 113 additions and 13 deletions

View File

@ -694,6 +694,49 @@ ModelImporter:
maskType: 0
maskSource: {instanceID: 0}
additiveReferencePoseFrame: 0
- serializedVersion: 16
name: Cast2_S_10303
takeName: Take 001
internalID: -998496362607748373
firstFrame: 16
lastFrame: 46
wrapMode: 0
orientationOffsetY: 0
level: 0
cycleOffset: 0
loop: 0
hasAdditiveReferencePose: 0
loopTime: 0
loopBlend: 0
loopBlendOrientation: 1
loopBlendPositionY: 1
loopBlendPositionXZ: 0
keepOriginalOrientation: 1
keepOriginalPositionY: 1
keepOriginalPositionXZ: 1
heightFromFeet: 0
mirror: 0
bodyMask: 01000000010000000100000001000000010000000100000001000000010000000100000001000000010000000100000001000000
curves: []
events:
- time: 0
functionName: LookAtTarget
data:
objectReferenceParameter: {instanceID: 0}
floatParameter: 0
intParameter: 0
messageOptions: 0
- time: 0.35
functionName: Projectile
data: Projectile_10303
objectReferenceParameter: {instanceID: 0}
floatParameter: 0
intParameter: 0
messageOptions: 0
transformMask: []
maskType: 0
maskSource: {instanceID: 0}
additiveReferencePoseFrame: 0
isReadable: 1
meshes:
lODScreenPercentages: []

View File

@ -2349,7 +2349,7 @@ AnimatorState:
m_MirrorParameterActive: 0
m_CycleOffsetParameterActive: 0
m_TimeParameterActive: 0
m_Motion: {fileID: 6641085478485924818, guid: e0919932c9c179142ad06cf4ee11f204,
m_Motion: {fileID: -998496362607748373, guid: 7a1612ec6fd9fbf4b917575b7c1ecdcb,
type: 3}
m_Tag:
m_SpeedParameter:

View File

@ -753,7 +753,7 @@ public class Actor : MyCoroutine
if (!testskill) m_Stat.Set_Stat(false, eStat.MP, skilldata.n_MP);
else m_SkillData.SetSkillExtraType = true;
Set_MagicCoolTime(skilldata.Get_CoolTime(m_Stat.Get_Stat(eStat.COOLTIME_Multiplier)), index);
if (IsMainPC() && !testskill) ServerInfo.Ins.m_ServerData.Add_MissionCount(ePurpose.USE_SKILL_ALL);
if (IsMainPC() && !testskill && !DSUtil.CheckNull(ServerInfo.Ins) && ServerInfo.Ins.m_ServerData != null) ServerInfo.Ins.m_ServerData.Add_MissionCount(ePurpose.USE_SKILL_ALL); // WL #949 — 스킬 테스트 씬(서버 객체 없음)에서 NRE 로 스킬이 안 나가던 것(실측 Actor.cs:756) · 서버 있으면 원본 그대로
return Play_Skill(skilldata.s_Animation);
}
public virtual void Shoot_Skill()

View File

@ -1153,26 +1153,29 @@ MonoBehaviour:
intParameter: 0
floatParameter: 0
- stateName: attack2
sourceFbxPath: Assets/ExplosiveLLC/Mage Warrior Mecanim Animation Pack/Animations/Mage@Attack3_S.FBX
sourceFbxPath: Assets/ExplosiveLLC/Mage Warrior Mecanim Animation Pack/Animations/Mage@RangeAttack2.FBX
subClipName: Cast2_S_{CID}
sourceClipName:
sourceClipName:
incomingExitTime: 0.854
createIfMissing: 0
firstFrame: -1
lastFrame: -1
speed: 0
firstFrame: 16
lastFrame: 46
speed: 1
maskPath:
bakeRootY: -1
mirror: 0
events:
- functionName: LookAtTarget
time: 0
useNormalized: 1
normalizedTime: 0
stringParameter:
stringParameter:
intParameter: 0
floatParameter: 0
- functionName: Projectile
time: 0
useNormalized: 1
normalizedTime: 0.407
normalizedTime: 0.35
stringParameter: Projectile_{CID}
intParameter: 0
floatParameter: 0

View File

@ -7,9 +7,16 @@
"prefab": "WL/Book/WL_Book_Magic",
"count": 1,
"scale": 0.9,
"side": 0.55,
"height": 0.68,
"forward": 0.05,
"side": 0.35,
"height": 0.72,
"forward": 0.45,
"cameraSpace": 1,
"castStates": [ "Cast1_S", "Cast2_S", "BookCharge", "BookCast" ],
"castSide": 0.25,
"castHeight": 0.62,
"castForward": 0.5,
"hideHandWeapon": 1,
"_note_949": "#949 PD 「책이 캐릭터를 가리지 않게 가능한 캐릭터보다 뒤쪽에(추적 로직 수정) · 시전 동작 중엔 책이 캐릭터 모션 앞쪽으로 유지」 — 평소 자리 = 카메라 기준(cameraSpace 1 · forward + = 카메라에서 먼 쪽 = 화면 위 = 캐릭터 뒤 0.45 m · 옆 0.35 · 높이 0.72) · 시전 중(클립 이름 Cast1/Cast2/BookCharge/BookCast) = PC 정면 0.5 m · 옆 0.25 · 높이 0.62 · followLerp 로 오간다. 이전 = PC 오른쪽 0.55 · 앞 0.05(카메라에선 몸 앞을 가림)",
"euler": [
40.0,
0.0,

View File

@ -36,6 +36,11 @@ namespace WL.Combat.Weapon
public float animLoopStartSec, animLoopEndSec;// 책 클립 반복 구간(초 · 끝 ≤ 시작 = 반복 안 함)
public int matchPcLayer; // 1 = 책 레이어를 PC 와 같게(같은 카메라 경로로 그려지게)
public float pollSec = 0.25f;
// #949 PD 「책이 캐릭터를 가리지 않게 가능한 캐릭터보다 뒤쪽에 · 시전 동작 중엔 캐릭터 모션 앞쪽으로 유지」
public int cameraSpace; // 1 = 평소 자리(side · forward)를 카메라 yaw 기준으로 — forward + = 카메라에서 먼 쪽(화면 위 · 캐릭터 뒤) · 0 = PC 정면 기준
public string[] castStates = new string[0]; // PC 현재 클립 이름에 이 조각이 들면 「시전 중」 → castSide · castHeight · castForward(PC 정면 기준) 로
public float castSide, castHeight, castForward;
public int hideHandWeapon; // #949 1 = 책이 떠 있는 동안 PC 손 소켓(w_*) 아래 무기 렌더러를 끈다(지팡이·셉터 숨김 · 책이 사라지면 되돌림) — 직업 표(수정 행)로 비우면 Set_Obj 재부착이 로비 컨트롤러를 끼워 실패(실측)
public string note = "";
}
@ -98,6 +103,28 @@ namespace WL.Combat.Weapon
for (int i = 0; i < _books.Count; i++) if (_books[i] == null) { missing = true; break; }
if (_books.Count > 0 && (!want || missing || !ReferenceEquals(pc, _pc))) Clear(!want ? "조건 해제" : missing ? "씬 정리됨" : "PC 교체");
if (want && _books.Count == 0) Spawn(c, pc);
if (want && c.hideHandWeapon != 0 && _books.Count > 0) HideHandWeapon(pc); // 매 폴링 — 원본이 무기 모델을 다시 붙여도(Set_Obj) 다시 끈다
}
// #949 손 소켓(w_*) 아래 무기 렌더러 끄기 · 되돌리기
readonly List<Renderer> _hiddenWeapon = new List<Renderer>();
public static int WeaponHidden;
void HideHandWeapon(PCActor pc)
{
foreach (var t in pc.GetComponentsInChildren<Transform>(true))
{
if (!t.name.StartsWith("w_")) continue;
foreach (var r in t.GetComponentsInChildren<Renderer>(true))
{
if (r == null || !r.enabled || r is ParticleSystemRenderer) continue;
r.enabled = false; _hiddenWeapon.Add(r); WeaponHidden++;
}
}
}
void RestoreHandWeapon()
{
for (int i = 0; i < _hiddenWeapon.Count; i++) if (_hiddenWeapon[i] != null) _hiddenWeapon[i].enabled = true;
_hiddenWeapon.Clear();
}
/// <summary>장착 무기(FR GroupNo)가 weaponIds 에 들거나, 그 무기의 공격표 class_id 가 classIds 에 드는가.</summary>
@ -139,6 +166,7 @@ namespace WL.Combat.Weapon
if (_books.Count == 0) return;
for (int i = 0; i < _books.Count; i++) if (_books[i] != null) Destroy(_books[i].gameObject);
_books.Clear(); _anims.Clear(); _pc = null;
RestoreHandWeapon(); // #949
Cleared++; LastLog = "책 정리 — " + why;
}
@ -166,6 +194,12 @@ namespace WL.Combat.Weapon
Quaternion faceYaw = pcYaw;
if (c.faceCamera != 0) { var cam = Camera.main; if (cam != null) { var f = cam.transform.forward; f.y = 0f; if (f.sqrMagnitude > 1e-4f) faceYaw = Quaternion.LookRotation(f.normalized, Vector3.up); } }
var e = c.euler != null && c.euler.Length >= 3 ? Quaternion.Euler(c.euler[0], c.euler[1], c.euler[2]) : Quaternion.identity;
// #949 시전 중(현재 클립 이름에 castStates 조각) = PC 정면 기준 cast 자리 · 평소 = cameraSpace 면 카메라 yaw 기준(캐릭터 뒤 = 화면 위)
bool casting = IsCasting(c);
Casting = casting;
float side = casting ? c.castSide : c.side, height = casting ? c.castHeight : c.height, forward = casting ? c.castForward : c.forward;
Quaternion basis = casting || c.cameraSpace == 0 ? pcYaw : (c.faceCamera != 0 ? faceYaw : pcYaw);
if (!casting && c.cameraSpace != 0 && c.faceCamera == 0) { var cam = Camera.main; if (cam != null) { var f = cam.transform.forward; f.y = 0f; if (f.sqrMagnitude > 1e-4f) basis = Quaternion.LookRotation(f.normalized, Vector3.up); } }
int n = _books.Count;
float kp = snap || c.followLerp <= 0f ? 1f : 1f - Mathf.Exp(-c.followLerp * Time.deltaTime);
float kr = snap || c.turnLerp <= 0f ? 1f : 1f - Mathf.Exp(-c.turnLerp * Time.deltaTime);
@ -176,13 +210,26 @@ namespace WL.Combat.Weapon
var ring = Quaternion.Euler(0f, _orbit + 360f * share, 0f);
float bob = Mathf.Sin((t * c.bobHz + share) * Mathf.PI * 2f) * c.bobAmp;
float sw = Mathf.Sin((t * c.swayHz + share) * Mathf.PI * 2f);
var pos = pt.position + pcYaw * (ring * new Vector3(c.side, 0f, c.forward)) + Vector3.up * (c.height + bob);
var pos = pt.position + basis * (ring * new Vector3(side, 0f, forward)) + Vector3.up * (height + bob);
var rot = faceYaw * ring * e * Quaternion.Euler(0f, sw * c.swayYawDeg, sw * c.swayRollDeg);
if (kp >= 1f && kr >= 1f) b.SetPositionAndRotation(pos, rot);
else b.SetPositionAndRotation(Vector3.Lerp(b.position, pos, kp), Quaternion.Slerp(b.rotation, rot, kr));
}
}
public static bool Casting; // 프로브용 — 마지막 배치가 시전 자리였는가
/// <summary>#949 PC 애니메이터 0층 현재 클립 이름에 castStates 조각이 들면 시전 중.</summary>
bool IsCasting(WLFloatingBookJson c)
{
if (c.castStates == null || c.castStates.Length == 0 || _pc == null) return false;
var an = _pc.m_animation; if (an == null) return false;
var ci = an.GetCurrentAnimatorClipInfo(0); string name = ci.Length > 0 && ci[0].clip != null ? ci[0].clip.name : "";
if (name.Length == 0) return false;
for (int i = 0; i < c.castStates.Length; i++) if (!string.IsNullOrEmpty(c.castStates[i]) && name.Contains(c.castStates[i])) return true;
return false;
}
/// <summary>펼친 뒤 넘김 구간 반복 — 상태 시간이 끝 초를 넘으면 시작 초로 되돌린다.</summary>
void LoopAnim(WLFloatingBookJson c)
{