Merge branch 'wl/systems/WL-816t-slash-fit'

This commit is contained in:
깃 관리자 2026-09-15 09:41:39 +09:00
commit 26abaff65d
5 changed files with 696 additions and 122 deletions

305
AgentScripts/WL816t_Bake.cs Normal file
View File

@ -0,0 +1,305 @@
// WL816t_Bake.cs — 검기 호 ↔ 무기 정합 실측·재베이크 (PD #816t)
// unity command run_script --file AgentScripts/WL816t_Bake.cs --entry WL816t_Bake.Probe --args "[10101, false]"
// unity command run_script --file AgentScripts/WL816t_Bake.cs --entry WL816t_Bake.Probe --args "[10101, true]" (SO 기록)
//
// WL804_Bake 와 같은 수식(SlashArcMeasure)·같은 프리뷰 샘플링을 쓰되 **런타임과 같은 캐릭터**로 잰다.
// · WL804_Bake : Assets/Res_Addr/PC/Ai01.prefab · root scale 1 · 무기 localScale 1 · root.InverseTransformPoint(스케일 포함)
// · 런타임 : WLCharacterSwapSettings.Resolve("Ai01") = LH_M05 · root scale = f_Scale × heightCompensation
// · 무기 = WLWeaponFitter.Fit(targetRatio 0.6) · ToLocalPos = 회전만(스케일 제외)
// 두 좌표계가 달라서 베이크 행이 현재 캐릭터의 칼끝 원과 어긋난다 — 그 편차를 재고 새 행을 만든다.
using System.Collections.Generic;
using System.Linq;
using System.Text;
using UnityEditor;
using UnityEditor.SceneManagement;
using UnityEngine;
using WL.Combat;
public static class WL816t_Bake
{
const string SettingsPath = "Assets/WL/Settings/Resources/WL/SlashTrailSettings.asset";
const string ClassConfigJson = "Assets/ResWork/Table/Export/ClassConfig.json";
const string AnimDir = "Assets/Res_Addr/Animations/Animation/OneHand/Knight@Attack{0}_S.FBX";
const float BakedQuality = 50f;
class Row
{
public int stage; public string clipName;
public Vector3 pivot, normal, bisector, startDir; public float tipR, hiltR, sweep, path, fitQ, q;
public Vector3 tipMid; // 스윙 중간 프레임의 칼끝(런타임 로컬)
public string src;
}
static readonly int[] Classes = { 10101, 10102, 10501, 10502 };
public static object Probe(int classId, bool apply) { return Run(classId, apply, true); }
public static object ProbeAll()
{
var sb = new StringBuilder();
foreach (var c in Classes) sb.AppendLine(Run(c, false, false));
return sb.ToString();
}
public static object ApplyAll()
{
var sb = new StringBuilder();
foreach (var c in Classes) sb.AppendLine(Run(c, true, false));
return sb.ToString();
}
/// <summary>공격 범위 정합 스위치 on/off + 배수. 되돌리기 = Set(false, 1)</summary>
public static object SetRangeFit(bool on, float rangeScale)
{
var s = AssetDatabase.LoadAssetAtPath<SlashTrailSettings>(SettingsPath);
if (s == null) return "설정 에셋 없음";
string before = string.Format("before: arcFitAttackRange={0} arcRangeScale={1:F2} arcSizeScale={2:F2}", s.arcFitAttackRange, s.arcRangeScale, s.arcSizeScale);
s.arcFitAttackRange = on; s.arcRangeScale = rangeScale;
EditorUtility.SetDirty(s); AssetDatabase.SaveAssets();
return before + string.Format(" → after: arcFitAttackRange={0} arcRangeScale={1:F2} arcSizeScale={2:F2}", s.arcFitAttackRange, s.arcRangeScale, s.arcSizeScale);
}
/// <summary>공격 범위(사거리) 정적 검산 — 런타임 ResolveAttackReach 와 같은 SOT.</summary>
public static object Reach(int classId)
{
float atk = 0f;
var arr = Newtonsoft.Json.Linq.JArray.Parse(System.IO.File.ReadAllText(ClassConfigJson));
foreach (var row in arr) { if ((string)row["n_ClassID"] == classId.ToString()) { float.TryParse((string)row["f_AttackRange"], out atk); break; } }
float margin = WL.Settings.WLTargetingSettings.ImmediateAttackMargin;
return string.Format("class {0} · f_AttackRange={1:F2} + AttackRange_Up(평시 0) + immediateAttackMargin={2:F2} = reach {3:F2} m", classId, atk, margin, atk + margin);
}
static string Run(int classId, bool apply, bool table)
{
var sb = new StringBuilder();
var settings = AssetDatabase.LoadAssetAtPath<SlashTrailSettings>(SettingsPath);
if (settings == null) return "설정 에셋 없음";
// ── ClassConfig (C45 · 상수 금지) ────────────────────────────────────
string weaponName = null; int socketIndex = -1; float fScale = 1f, atkRange = 0f;
try
{
var arr = Newtonsoft.Json.Linq.JArray.Parse(System.IO.File.ReadAllText(ClassConfigJson));
foreach (var row in arr)
{
if ((string)row["n_ClassID"] != classId.ToString()) continue;
weaponName = (string)row["s_WeaponRHPrefab"]; socketIndex = int.Parse((string)row["n_WeaponRHIndex"]);
if (string.IsNullOrEmpty(weaponName)) { weaponName = (string)row["s_WeaponLHPrefab"]; socketIndex = int.Parse((string)row["n_WeaponLHIndex"]); }
float.TryParse((string)row["f_Scale"], out fScale);
float.TryParse((string)row["f_AttackRange"], out atkRange);
break;
}
}
catch (System.Exception ex) { return "ClassConfig parse fail: " + ex.Message; }
if (string.IsNullOrEmpty(weaponName) || socketIndex < 0) return "class " + classId + " weapon not found";
// ── 런타임과 같은 PC 프리팹·스케일 ──────────────────────────────────
string pcName = WL.Character.WLCharacterSwapSettings.Resolve("Ai01");
float heightComp = WL.Character.WLCharacterSwapSettings.HeightCompensation;
string pcPath = "Assets/Res_Addr/PC/" + pcName + ".prefab";
var model = AssetDatabase.LoadAssetAtPath<GameObject>(pcPath);
var weaponPrefab = AssetDatabase.LoadAssetAtPath<GameObject>("Assets/Res_Addr/Weapon/" + weaponName + ".prefab");
if (model == null || weaponPrefab == null) return "load fail pc=" + pcPath + " weapon=" + weaponName;
float rootScale = Mathf.Max(fScale, 0.01f) * Mathf.Max(heightComp, 0.01f);
sb.AppendLine(string.Format("== class {0} · PC {1}(swap:{2}) rootScale={3:F4}(f_Scale {4:F2} × heightComp {5:F4}) · weapon {6}@{7} · f_AttackRange={8:F2}",
classId, pcName, pcName != "Ai01", rootScale, fScale, heightComp, weaponName, socketIndex, atkRange));
var scene = EditorSceneManager.NewPreviewScene();
bool animMode = false;
var rows = new List<Row>();
try
{
var go = (GameObject)PrefabUtility.InstantiatePrefab(model, scene);
go.transform.position = Vector3.zero; go.transform.rotation = Quaternion.identity;
go.transform.localScale = Vector3.one * rootScale;
var pc = go.GetComponentInChildren<PCActor>(true);
if (pc == null || pc.tfs_weapon == null || socketIndex >= pc.tfs_weapon.Length) return sb + "\ntfs_weapon missing";
var socket = pc.tfs_weapon[socketIndex];
var w = (GameObject)PrefabUtility.InstantiatePrefab(weaponPrefab, scene);
w.transform.SetParent(socket, false);
w.transform.localPosition = Vector3.zero; w.transform.localRotation = Quaternion.identity; w.transform.localScale = Vector3.one;
// ── 814p 무기 축소(런타임과 동일 경로) ──────────────────────────
var fitter = go.GetComponent<WL.Character.WLWeaponFitter>();
if (fitter == null) fitter = go.AddComponent<WL.Character.WLWeaponFitter>();
float wk = fitter.Fit(w.transform, socket);
sb.AppendLine(string.Format(" 무기 보정 ×{0:F4} (WLWeaponFitter.Fit · targetRatio={1:F2})", wk, WL.Character.WLWeaponFitSettings.ResolveRatio(w.name)));
// ── 칼자루/칼끝 로컬(WL804_Bake·MeasureSlot 과 같은 규칙) ────────
Renderer best = null; Mesh bestMesh = null; float bestExtent = 0f;
foreach (var r in socket.GetComponentsInChildren<Renderer>(true))
{
var mf2 = r.GetComponent<MeshFilter>(); var smr = r as SkinnedMeshRenderer;
var mesh = smr != null ? smr.sharedMesh : (mf2 != null ? mf2.sharedMesh : null);
if (mesh == null) continue;
var ws = Vector3.Scale(mesh.bounds.size, Abs(r.transform.lossyScale));
float longest = Mathf.Max(ws.x, Mathf.Max(ws.y, ws.z));
if (longest > bestExtent) { bestExtent = longest; best = r; bestMesh = mesh; }
}
if (best == null) return sb + "\nno renderer under socket";
var b = bestMesh.bounds; var size = Vector3.Scale(b.size, Abs(best.transform.lossyScale));
int axis = size.x >= size.y && size.x >= size.z ? 0 : (size.y >= size.z ? 1 : 2);
Vector3 axisDir = Vector3.zero; axisDir[axis] = 1f;
Vector3 eA = best.transform.TransformPoint(b.center - axisDir * b.extents[axis]);
Vector3 eB = best.transform.TransformPoint(b.center + axisDir * b.extents[axis]);
bool aIsHilt = (eA - socket.position).sqrMagnitude <= (eB - socket.position).sqrMagnitude;
Vector3 hiltLocal = socket.InverseTransformPoint(aIsHilt ? eA : eB);
Vector3 tipLocal = socket.InverseTransformPoint(aIsHilt ? eB : eA);
sb.AppendLine(string.Format(" 무기 월드 길이 {0:F3} m · 칼끝-자루 {1:F3} m", bestExtent,
Vector3.Distance(socket.TransformPoint(tipLocal), socket.TransformPoint(hiltLocal))));
var anim = go.GetComponentInChildren<Animator>();
var animGo = anim.gameObject; anim.applyRootMotion = false;
var root = go.transform;
AnimationMode.StartAnimationMode(); animMode = true;
for (int stage = 1; stage <= 3; stage++)
{
var fbx = string.Format(AnimDir, stage);
var clips = AssetDatabase.LoadAllAssetsAtPath(fbx).OfType<AnimationClip>().Where(c => !c.name.StartsWith("__preview")).ToArray();
var clip = clips.FirstOrDefault(c => c.name == "Attack" + stage + "_S_" + classId) ?? clips.FirstOrDefault(c => c.name.StartsWith("Attack" + stage));
if (clip == null) { sb.AppendLine(" [" + stage + "타] 클립 없음 " + fbx); continue; }
// 클립 전체를 60 Hz 로 훑고 런타임과 같은 속도 트림으로 스윙 구간만 남긴다
int n0 = Mathf.Max(8, Mathf.RoundToInt(clip.length * 60f) + 1);
var tAll = new Vector3[n0]; var hAll = new Vector3[n0];
for (int i = 0; i < n0; i++)
{
float t = clip.length * i / (n0 - 1);
AnimationMode.BeginSampling();
AnimationMode.SampleAnimationClip(animGo, clip, t);
AnimationMode.EndSampling();
// 🔴 런타임 ToLocalPos 와 동일 = 회전만(스케일 제외)
tAll[i] = Quaternion.Inverse(root.rotation) * (socket.TransformPoint(tipLocal) - root.position);
hAll[i] = Quaternion.Inverse(root.rotation) * (socket.TransformPoint(hiltLocal) - root.position);
}
int lo = 0, hi = n0 - 1;
float ratio = settings.swingTrimSpeedRatio;
if (ratio > 0f)
{
float maxStep = 0f; var step = new float[n0];
for (int i = 1; i < n0; i++) { step[i] = Vector3.Distance(tAll[i], tAll[i - 1]); if (step[i] > maxStep) maxStep = step[i]; }
float th = maxStep * ratio; int f = -1, l = -1;
for (int i = 1; i < n0; i++) if (step[i] >= th) { if (f < 0) f = i - 1; l = i; }
if (f >= 0 && l > f) { lo = f; hi = l; }
}
int n = hi - lo + 1;
if (n < 3) { lo = 0; hi = n0 - 1; n = n0; }
var tips = new Vector3[n]; var hilts = new Vector3[n];
for (int i = 0; i < n; i++) { tips[i] = tAll[lo + i]; hilts[i] = hAll[lo + i]; }
float fullPath = 0f; Vector3 pd = (tips[0] - hilts[0]).normalized; float bladeAng = 0f;
for (int i = 1; i < n; i++)
{
fullPath += Vector3.Distance(tips[i], tips[i - 1]);
var d = (tips[i] - hilts[i]).normalized; bladeAng += Vector3.Angle(pd, d); pd = d;
}
var fit = SlashArcMeasure.FitArcOrdered(tips, n);
if (!fit.valid) { sb.AppendLine(" [" + stage + "타] 원 피팅 실패 (n=" + n + ")"); continue; }
Vector3 nrm = fit.normal, pivot = fit.center;
float tipR = SlashArcMeasure.MeanRadius(tips, n, pivot, nrm);
float hiltR = SlashArcMeasure.MeanRadius(hilts, n, pivot, nrm);
float dev = SlashArcMeasure.RadiusDeviation(tips, n, pivot, nrm, tipR);
string pivotSrc = "칼끝원";
if (tipR < 1e-3f || tipR <= hiltR * 1.15f || dev > tipR * 0.25f)
{
Vector3 tc = SlashArcMeasure.CentroidOnPlane(tips, n, nrm, tips[0]);
pivot = SlashArcMeasure.CentroidOnPlane(hilts, n, nrm, tc);
tipR = SlashArcMeasure.MeanRadius(tips, n, pivot, nrm);
hiltR = SlashArcMeasure.MeanRadius(hilts, n, pivot, nrm);
pivotSrc = "손잡이중심";
}
Vector3 sd2, bis; float sweep;
if (!SlashArcMeasure.RecomputeAboutPivot(tips, n, pivot, ref nrm, out sd2, out bis, out sweep))
{ sb.AppendLine(" [" + stage + "타] RecomputeAboutPivot 실패"); continue; }
float fitQ = Mathf.Clamp01(1f - (dev / Mathf.Max(tipR, 1e-3f)) * 2f);
rows.Add(new Row
{
stage = stage, clipName = "Effect_Slash_" + classId + "_" + stage,
pivot = pivot, normal = nrm, bisector = bis, startDir = sd2,
tipR = tipR, hiltR = hiltR, sweep = sweep, path = fullPath, fitQ = fitQ,
q = fullPath * Mathf.Max(fitQ, 0.05f), tipMid = tips[n / 2],
src = string.Format("wl816t {0:yyyy-MM-dd HH:mm} {1} f{2}~f{3}/{4}@60Hz n={5} PC={6} rootScale={7:F4} weaponK={8:F4} path={9:F2} bladeAng={10:F0}° sweep={11:F1}° pivot={12} tipR={13:F3} hiltR={14:F3} dev={15:F3} fitQ={16:F2} (런타임 로컬=회전만)",
System.DateTime.Now, clip.name, lo, hi, n0 - 1, n, pcName, rootScale, wk, fullPath, bladeAng, sweep, pivotSrc, tipR, hiltR, dev, fitQ)
});
}
AnimationMode.StopAnimationMode(); animMode = false;
// ── 비교 + 8방향 편차 ───────────────────────────────────────────
sb.AppendLine("");
sb.AppendLine("clip | old pivot(로컬) tipR | new pivot(로컬) tipR | Δpivot(m) ΔtipR(m) Δnormal(°)");
var cams = CamBasis();
var devTable = new StringBuilder();
foreach (var r in rows)
{
var old = settings.FindCalibration(classId, r.clipName);
if (old == null) { sb.AppendLine(string.Format("{0,-23} | (없음) | {1} {2:F3} | -", r.clipName, V(r.pivot), r.tipR)); continue; }
float dPiv = Vector3.Distance(old.pivot, r.pivot);
float dTip = r.tipR - old.tipRadius;
float dNrm = Vector3.Angle(old.planeNormal, r.normal);
sb.AppendLine(string.Format("{0,-23} | {1} {2:F3} | {3} {4:F3} | {5:F3} {6:+0.000;-0.000} {7:F1}",
r.clipName, V(old.pivot), old.tipRadius, V(r.pivot), r.tipR, dPiv, dTip, dNrm));
// 8방향: 구 행으로 배치한 호의 중심/테두리 vs 실제 칼끝
if (!table) continue;
devTable.AppendLine(" " + r.clipName);
devTable.AppendLine(" yaw | Δ중심(m) 화면Δ가로(m) 화면Δ세로(m) | 칼끝-호테두리 반경차(m)");
for (int k = 0; k < 8; k++)
{
float yaw = k * 45f;
var rot = Quaternion.Euler(0f, yaw, 0f);
Vector3 oldPivW = rot * old.pivot; // root at origin
Vector3 newPivW = rot * r.pivot;
Vector3 tipW = rot * r.tipMid;
Vector3 d = oldPivW - newPivW;
float radialGap = Vector3.Distance(tipW, oldPivW) - old.tipRadius * Mathf.Max(settings.arcSizeScale, 0.01f);
devTable.AppendLine(string.Format(" {0,3:F0} | {1:F3} {2,8:+0.000;-0.000} {3,8:+0.000;-0.000} | {4,8:+0.000;-0.000}",
yaw, d.magnitude, Vector3.Dot(d, cams[0]), Vector3.Dot(d, cams[1]), radialGap));
}
}
sb.AppendLine("");
sb.AppendLine("8방향 편차(카메라 기준 · PD 구도 yaw45 내려보기 45°)");
sb.Append(devTable);
if (apply)
{
int removed = 0;
foreach (var r in rows)
{
removed += settings.swingCalibrations.RemoveAll(c => c != null && c.classId == classId && c.clipName == r.clipName);
settings.swingCalibrations.Add(new SwingCalibration
{
classId = classId, clipName = r.clipName, kind = SwingKind.Swing,
planeNormal = r.normal, bisector = r.bisector, startDir = r.startDir,
pivot = r.pivot, tipRadius = r.tipR, hiltRadius = r.hiltR, sweepDeg = r.sweep,
stabAxis = Vector3.forward, stabStart = r.pivot, stabLength = Mathf.Max(r.tipR, 1e-3f),
quality = Mathf.Max(BakedQuality, r.q), source = r.src
});
}
EditorUtility.SetDirty(settings);
AssetDatabase.SaveAssets();
sb.AppendLine(string.Format("APPLIED {0}행 (기존 {1}행 교체 · 총 {2}행)", rows.Count, removed, settings.swingCalibrations.Count));
}
else sb.AppendLine("(dry)");
}
finally
{
if (animMode) AnimationMode.StopAnimationMode();
EditorSceneManager.ClosePreviewScene(scene);
}
return sb.ToString();
}
/// <summary>PD 구도(비스듬히 내려보는 카메라)의 화면 가로·세로 축(월드).</summary>
static Vector3[] CamBasis()
{
var rot = Quaternion.Euler(45f, 45f, 0f);
return new[] { rot * Vector3.right, rot * Vector3.up };
}
static string V(Vector3 v) { return string.Format("({0,6:F3},{1,6:F3},{2,6:F3})", v.x, v.y, v.z); }
static Vector3 Abs(Vector3 v) { return new Vector3(Mathf.Abs(v.x), Mathf.Abs(v.y), Mathf.Abs(v.z)); }
}

156
AgentScripts/WL816t_Cap.cs Normal file
View File

@ -0,0 +1,156 @@
// WL816t_Cap.cs — 8방향 격자 · 3타 연속 캡처 (에디트 모드 · Play 없음)
// unity command run_script --file AgentScripts/WL816t_Cap.cs --entry WL816t_Cap.Grid8 --args "[10101, 2, \"Screenshots_WL/WL816t_before_8dir.png\"]"
// unity command run_script --file AgentScripts/WL816t_Cap.cs --entry WL816t_Cap.Combo3 --args "[10101, \"Screenshots_WL/WL816t_after_combo.png\"]"
// PlaceArc 와 같은 수식으로 실제 Effect_WLSwingArc 프리팹을 배치하고 ParticleSystem.Simulate 로 굽는다.
using System.Collections.Generic;
using System.Linq;
using UnityEditor;
using UnityEditor.SceneManagement;
using UnityEngine;
using UnityEngine.SceneManagement;
using WL.Combat;
public static class WL816t_Cap
{
const string SettingsPath = "Assets/WL/Settings/Resources/WL/SlashTrailSettings.asset";
const string ClassConfigJson = "Assets/ResWork/Table/Export/ClassConfig.json";
const string ArcPrefab = "Assets/Res_Addr/Effect/Effect_WLSwingArc.prefab";
public static object Grid8(int classId, int stage, string outPath) { return Build(classId, new[] { stage }, true, outPath); }
public static object Combo3(int classId, string outPath) { return Build(classId, new[] { 1, 2, 3 }, false, outPath); }
static string Build(int classId, int[] stages, bool grid8, string outPath)
{
AssetDatabase.Refresh(ImportAssetOptions.ForceUpdate);
var settings = AssetDatabase.LoadAssetAtPath<SlashTrailSettings>(SettingsPath);
var arcGo = AssetDatabase.LoadAssetAtPath<GameObject>(ArcPrefab);
if (settings == null || arcGo == null) return "load fail";
string weaponName = null; int socketIndex = -1; float fScale = 1f, atkRange = 0f;
var arr = Newtonsoft.Json.Linq.JArray.Parse(System.IO.File.ReadAllText(ClassConfigJson));
foreach (var row in arr)
{
if ((string)row["n_ClassID"] != classId.ToString()) continue;
weaponName = (string)row["s_WeaponRHPrefab"]; socketIndex = int.Parse((string)row["n_WeaponRHIndex"]);
if (string.IsNullOrEmpty(weaponName)) { weaponName = (string)row["s_WeaponLHPrefab"]; socketIndex = int.Parse((string)row["n_WeaponLHIndex"]); }
float.TryParse((string)row["f_Scale"], out fScale); float.TryParse((string)row["f_AttackRange"], out atkRange);
break;
}
string pcName = WL.Character.WLCharacterSwapSettings.Resolve("Ai01");
float rootScale = fScale * WL.Character.WLCharacterSwapSettings.HeightCompensation;
var model = AssetDatabase.LoadAssetAtPath<GameObject>("Assets/Res_Addr/PC/" + pcName + ".prefab");
var weaponPrefab = AssetDatabase.LoadAssetAtPath<GameObject>("Assets/Res_Addr/Weapon/" + weaponName + ".prefab");
if (model == null || weaponPrefab == null) return "pc/weapon load fail";
float reach = settings.arcFitAttackRange ? (atkRange + WL.Settings.WLTargetingSettings.ImmediateAttackMargin) * settings.arcRangeScale : 0f;
var scene = EditorSceneManager.NewScene(NewSceneSetup.EmptyScene, NewSceneMode.Single);
var log = new System.Text.StringBuilder();
try
{
var lightGo = new GameObject("Sun"); var lt = lightGo.AddComponent<Light>();
lt.type = LightType.Directional; lt.intensity = 1.2f; lightGo.transform.rotation = Quaternion.Euler(50f, -30f, 0f);
var ground = GameObject.CreatePrimitive(PrimitiveType.Plane);
ground.transform.localScale = Vector3.one * 5f; ground.name = "Ground";
int cols = grid8 ? 4 : stages.Length;
int cells = grid8 ? 8 : stages.Length;
float pitch = 3.2f;
var pss = new List<ParticleSystem>();
for (int i = 0; i < cells; i++)
{
float yaw = grid8 ? i * 45f : 0f;
int stage = grid8 ? stages[0] : stages[i];
var cal = settings.FindCalibration(classId, "Effect_Slash_" + classId + "_" + stage);
if (cal == null) { log.AppendLine("cal 없음 stage " + stage); continue; }
Vector3 cell = new Vector3((i % cols) * pitch - (cols - 1) * pitch * 0.5f, 0f, -(i / cols) * pitch);
var go = (GameObject)PrefabUtility.InstantiatePrefab(model);
go.transform.position = cell; go.transform.rotation = Quaternion.Euler(0f, yaw, 0f);
go.transform.localScale = Vector3.one * rootScale;
var pc = go.GetComponentInChildren<PCActor>(true);
var socket = pc.tfs_weapon[socketIndex];
var w = (GameObject)PrefabUtility.InstantiatePrefab(weaponPrefab);
w.transform.SetParent(socket, false);
w.transform.localPosition = Vector3.zero; w.transform.localRotation = Quaternion.identity; w.transform.localScale = Vector3.one;
var fitter = go.GetComponent<WL.Character.WLWeaponFitter>() ?? go.AddComponent<WL.Character.WLWeaponFitter>();
fitter.Fit(w.transform, socket);
FreezeAtSwingMid(go, classId, stage);
// ── PlaceArc 와 같은 수식 ────────────────────────────────────
var geo = settings.FindGeometry("Effect_WLSwingArc");
if (geo == null || !geo.arcValid) { log.AppendLine("geo 없음"); continue; }
var fx = (GameObject)PrefabUtility.InstantiatePrefab(arcGo);
Quaternion rootRot = go.transform.rotation; Vector3 rootPos = go.transform.position;
Vector3 nW = (rootRot * cal.planeNormal).normalized;
Vector3 bW = rootRot * cal.bisector; bW -= nW * Vector3.Dot(bW, nW); bW.Normalize();
Vector3 pivotW = rootPos + rootRot * cal.pivot;
float s = cal.tipRadius / geo.arcRadiusOuter;
if (reach > 0.01f) s = reach / geo.arcRadiusOuter;
s *= Mathf.Max(settings.arcSizeScale, 0.01f);
Quaternion frame = Quaternion.LookRotation(geo.arcNormal, geo.arcBisector);
Quaternion rot = Quaternion.LookRotation(nW, bW) * Quaternion.Inverse(frame);
fx.transform.rotation = rot;
fx.transform.localScale = Vector3.one * s;
fx.transform.position = pivotW + nW * settings.arcPlaneOffset + bW * settings.arcRadialOffset - rot * (geo.arcCenter * s);
fx.SetActive(true);
foreach (var tr in fx.GetComponentsInChildren<Transform>(true)) tr.gameObject.SetActive(true);
foreach (var r in fx.GetComponentsInChildren<Renderer>(true)) r.enabled = true;
foreach (var ps in fx.GetComponentsInChildren<ParticleSystem>(true)) pss.Add(ps);
if (i == 0) log.AppendLine(" fx: ps=" + fx.GetComponentsInChildren<ParticleSystem>(true).Length
+ " rend=" + fx.GetComponentsInChildren<Renderer>(true).Length
+ " mesh=" + fx.GetComponentsInChildren<MeshRenderer>(true).Length);
log.AppendLine(string.Format(" yaw {0,3:F0} stage {1} · s={2:F3} r={3:F3} pivotW={4}", yaw, stage, s, geo.arcRadiusOuter * s, pivotW.ToString("F2")));
}
int alive = 0;
foreach (var ps in pss) { ps.Simulate(0.12f, true, true, false); alive += ps.particleCount; }
log.AppendLine(" particles alive = " + alive + " (ps " + pss.Count + "개)");
// ── 카메라 (PD 구도 · 비스듬히 내려보기) ────────────────────────
var camGo = new GameObject("CapCam"); var cam = camGo.AddComponent<Camera>();
cam.clearFlags = CameraClearFlags.SolidColor; cam.backgroundColor = new Color(0.12f, 0.13f, 0.16f);
float span = grid8 ? 12f : 6f;
camGo.transform.rotation = Quaternion.Euler(38f, 0f, 0f);
Vector3 focus = new Vector3(0f, 0.9f, grid8 ? -pitch * 0.5f : 0f);
camGo.transform.position = focus - camGo.transform.forward * span;
cam.fieldOfView = 45f;
string full = System.IO.Path.Combine(System.IO.Directory.GetCurrentDirectory(), outPath);
System.IO.Directory.CreateDirectory(System.IO.Path.GetDirectoryName(full));
var rt = new RenderTexture(1600, 900, 24, RenderTextureFormat.ARGB32);
cam.targetTexture = rt; cam.Render();
RenderTexture.active = rt;
var tex = new Texture2D(1600, 900, TextureFormat.RGB24, false);
tex.ReadPixels(new Rect(0, 0, 1600, 900), 0, 0); tex.Apply();
RenderTexture.active = null; cam.targetTexture = null;
System.IO.File.WriteAllBytes(full, tex.EncodeToPNG());
Object.DestroyImmediate(tex); rt.Release(); Object.DestroyImmediate(rt);
log.AppendLine("SAVED " + full + " · reach=" + reach.ToString("F2") + " · arcFitAttackRange=" + settings.arcFitAttackRange);
}
finally { }
return log.ToString();
}
static void FreezeAtSwingMid(GameObject go, int classId, int stage)
{
var fbx = string.Format("Assets/Res_Addr/Animations/Animation/OneHand/Knight@Attack{0}_S.FBX", stage);
var clips = AssetDatabase.LoadAllAssetsAtPath(fbx).OfType<AnimationClip>().Where(c => !c.name.StartsWith("__preview")).ToArray();
var clip = clips.FirstOrDefault(c => c.name == "Attack" + stage + "_S_" + classId) ?? clips.FirstOrDefault(c => c.name.StartsWith("Attack" + stage));
if (clip == null) return;
var anim = go.GetComponentInChildren<Animator>();
if (anim == null) return;
anim.applyRootMotion = false;
var bones = go.GetComponentsInChildren<Transform>(true);
var trs = new Vector3[bones.Length]; var rots = new Quaternion[bones.Length]; var scl = new Vector3[bones.Length];
AnimationMode.StartAnimationMode();
AnimationMode.BeginSampling();
AnimationMode.SampleAnimationClip(anim.gameObject, clip, clip.length * 0.42f);
AnimationMode.EndSampling();
for (int i = 0; i < bones.Length; i++) { trs[i] = bones[i].localPosition; rots[i] = bones[i].localRotation; scl[i] = bones[i].localScale; }
AnimationMode.StopAnimationMode();
anim.enabled = false;
for (int i = 0; i < bones.Length; i++) { bones[i].localPosition = trs[i]; bones[i].localRotation = rots[i]; bones[i].localScale = scl[i]; }
}
}

View File

@ -788,6 +788,15 @@ namespace WL.Combat
[Tooltip("정합한 크기에 곱하는 배수. 1 = 칼끝 반경과 정확히 일치(기본 · C3-5)")]
[Range(0.4f, 2.5f)] public float arcSizeScale = 1f;
[Tooltip("■ #816t — 호 바깥 반지름을 칼끝 반경 대신 **플레이어 공격 범위**(사거리)에 맞춘다. " +
"출처 = NavMeshAgent.stoppingDistance 양수 최솟값(= ClassConfig f_AttackRange · PCActor.cs:381) " +
"+ eStat.AttackRange_Up + WLTargetingSettings.immediateAttackMargin — WLRangeRing 과 같은 SOT(상수 0개 · C45). " +
"끄면 즉시 이전(칼끝 반경 정합) 동작으로 돌아간다(C8 롤백)")]
public bool arcFitAttackRange = false;
[Tooltip("공격 범위에 곱하는 배수. 1 = 사거리와 정확히 일치")]
[Range(0.3f, 2f)] public float arcRangeScale = 1f;
[Tooltip("호를 스윙 평면에서 법선 방향으로 이만큼(m) 띄운다. 0 = 평면 위. " +
"무기 메시와 Z-파이팅이 보이면 아주 작은 값을 준다")]
public float arcPlaneOffset = 0f;

View File

@ -1602,6 +1602,13 @@ namespace WL.Combat
s = cal.tipRadius / mf.radiusOuter;
break;
}
// ── #816t 공격 범위 정합 — 호 바깥 반지름을 플레이어 사거리에 맞춘다(PD 지시) ──
// 출처는 WLRangeRing 과 같은 SOT(테이블 f_AttackRange + 버프 + 즉시공격 여유). 상수 0 개(C45).
// 사거리를 못 읽으면(로비·스탯 미초기화) 0 → 위의 칼끝 반경 정합을 그대로 쓴다.
float reach = settings.arcFitAttackRange ? ResolveAttackReach() : 0f;
if (reach > 0.01f && mf.radiusOuter > 1e-4f)
s = reach * Mathf.Max(settings.arcRangeScale, 0.01f) / mf.radiusOuter;
s *= Mathf.Max(settings.arcSizeScale, 0.01f);
// ── 회전 · 위치 ────────────────────────────────────────────────
@ -1740,6 +1747,34 @@ namespace WL.Combat
private Vector3 ToLocalPos(Vector3 world) { return Quaternion.Inverse(transform.rotation) * (world - transform.position); }
private Vector3 ToWorldPos(Vector3 local) { return transform.position + transform.rotation * local; }
// ── #816t 공격 범위(사거리) 해석 — WLRangeRing.Poll 과 같은 출처·같은 수식 ──────
// stoppingDistance 는 타겟 반경·크기 보정으로 커질 수 있어(MyActor.cs:207·212·213)
// **양수 최솟값**을 기억한다 = 보정 없는 평시 값 = ClassConfig f_AttackRange(PCActor.cs:381).
private float _baseStop;
/// <summary>마지막으로 해석한 공격 범위(m). 0 = 아직 못 읽음(검증용 읽기 전용 · 게임 로직은 쓰지 않는다).</summary>
public static float LastAttackReach;
private float ResolveAttackReach()
{
if (_pc == null) return 0f;
var agent = _pc.Get_Agent();
if (agent != null)
{
float sd = agent.stoppingDistance;
if (sd > 0.01f && (_baseStop <= 0f || sd < _baseStop)) _baseStop = sd;
}
if (_baseStop <= 0f) return 0f;
float up = 0f;
var stat = _pc.Get_StatInfo();
if (stat != null) up = (float)stat.Get_Stat(eStat.AttackRange_Up);
float reach = _baseStop + up + WL.Settings.WLTargetingSettings.ImmediateAttackMargin;
LastAttackReach = reach;
return reach;
}
// ─────────────────────────────────────────────────────────────────────────
// 수명 관리
// ─────────────────────────────────────────────────────────────────────────

View File

@ -407,150 +407,217 @@ MonoBehaviour:
flipAxis: 0
bakedAt: 2026-09-07 01:52:52
swingCalibrations:
- classId: 10501
clipName: Effect_Slash_10501_2
kind: 1
planeNormal: {x: 0.15228733, y: 0.9881525, z: -0.01905872}
pivot: {x: -0.01862258, y: 0.8349972, z: 0.039082136}
bisector: {x: 0.9480554, y: -0.14060435, z: 0.28534392}
startDir: {x: -0.9755819, y: 0.15338214, z: 0.15720595}
tipRadius: 0.7859197
hiltRadius: 0.2289177
sweepDeg: 308.74216
stabAxis: {x: 0.9878676, y: -0.07309146, z: -0.13702233}
stabStart: {x: -0.84236026, y: 0.7775105, z: 0.17080714}
stabLength: 1.5588787
quality: 6.4799576
source: "runtime 02:40:00 samples=30/33(trim 0~29) path=8.43 \uCE7C\uB0A0\uD68C\uC804=604\xB0
\uCE7C\uB05D/\uC190\uC7A1\uC774=3.36 sweep=308.7\xB0 straight=0.60 fwd=-0.14
chord=1.56 pivot=\uCE7C\uB05D\uC6D0 tipR=0.786 hiltR=0.229 dev=0.091 fitQ=0.77
q=6.48"
- classId: 10501
clipName: Effect_Slash_10501_1
kind: 1
planeNormal: {x: 0.09609319, y: -0.69287807, z: 0.7146231}
pivot: {x: 0.013419926, y: 0.6920113, z: -0.017135125}
bisector: {x: -0.46833384, y: 0.6020381, z: 0.64669436}
startDir: {x: 0.7837881, y: 0.49521428, z: 0.374752}
tipRadius: 0.8682612
hiltRadius: 0.19870256
sweepDeg: 160.0273
stabAxis: {x: -0.7661477, y: -0.63733035, z: -0.0826303}
stabStart: {x: 0.65847033, y: 1.292506, z: 0.12008436}
stabLength: 1.8009284
quality: 5.0141697
source: "runtime 02:26:58 samples=28/28(trim 0~27) path=5.21 \uCE7C\uB0A0\uD68C\uC804=366\xB0
\uCE7C\uB05D/\uC190\uC7A1\uC774=4.43 sweep=160.0\xB0 straight=0.36 fwd=-0.08
chord=1.80 pivot=\uCE7C\uB05D\uC6D0 tipR=0.868 hiltR=0.199 dev=0.016 fitQ=0.96
q=5.01"
- classId: 10101
clipName: Effect_Slash_10101_1
kind: 1
planeNormal: {x: 0.3078961, y: -0.636374, z: -0.70726806}
pivot: {x: -0.056650292, y: 0.74904346, z: 0.17702837}
bisector: {x: 0.897873, y: -0.05152121, z: 0.43722925}
startDir: {x: -0.6402187, y: 0.41131842, z: -0.6487967}
tipRadius: 1.0059098
hiltRadius: 0.20204122
sweepDeg: 303.2123
stabAxis: {x: 0.7060405, y: -0.4063584, z: 0.5799824}
stabStart: {x: -0.7590293, y: 1.3011718, z: -0.30031368}
stabLength: 1.9881575
quality: 5.966149
source: "runtime 02:41:31 samples=28/28(trim 0~27) path=6.15 \uCE7C\uB0A0\uD68C\uC804=363\xB0
\uCE7C\uB05D/\uC190\uC7A1\uC774=4.65 sweep=303.2\xB0 straight=0.65 fwd=0.58
chord=1.99 pivot=\uCE7C\uB05D\uC6D0 tipR=1.006 hiltR=0.202 dev=0.015 fitQ=0.97
q=5.97"
planeNormal: {x: 0.6155671, y: -0.7459609, z: 0.2542036}
pivot: {x: -0.44185022, y: 0.90176433, z: -0.14577419}
bisector: {x: -0.18893999, y: -0.452843, z: -0.8713409}
startDir: {x: 0.4323041, y: 0.5893181, z: 0.6825082}
tipRadius: 0.71769434
hiltRadius: 0.2310255
sweepDeg: 398.79218
stabAxis: {x: 0, y: 0, z: 1}
stabStart: {x: -0.44185022, y: 0.90176433, z: -0.14577419}
stabLength: 0.71769434
quality: 50
source: "wl816t 2026-09-15 09:35 Attack1_S_10101 f12~f35/50@60Hz n=24 PC=LH_M05
rootScale=1.0527 weaponK=0.5369 path=6.88 bladeAng=544\xB0 sweep=398.8\xB0
pivot=\uC190\uC7A1\uC774\uC911\uC2EC tipR=0.718 hiltR=0.231 dev=0.195 fitQ=0.46
(\uB7F0\uD0C0\uC784 \uB85C\uCEEC=\uD68C\uC804\uB9CC)"
- classId: 10101
clipName: Effect_Slash_10101_2
kind: 1
planeNormal: {x: 0.09728308, y: 0.9890535, z: -0.11094675}
pivot: {x: -0.071887866, y: 0.80780745, z: 0.034430865}
bisector: {x: 0.9841305, y: -0.078973934, z: 0.15890326}
startDir: {x: -0.76585925, y: 0.14558588, z: 0.6263101}
tipRadius: 0.9324841
hiltRadius: 0.2528293
sweepDeg: 263.46906
stabAxis: {x: 0.8400051, y: -0.09362712, z: -0.5344393}
stabStart: {x: -0.7618986, y: 0.83268386, z: 0.60202444}
stabLength: 1.7789476
quality: 5.7660966
source: "runtime 02:28:34 samples=29/31(trim 0~28) path=6.25 \uCE7C\uB0A0\uD68C\uC804=385\xB0
\uCE7C\uB05D/\uC190\uC7A1\uC774=3.35 sweep=263.5\xB0 straight=0.65 fwd=-0.53
chord=1.78 pivot=\uCE7C\uB05D\uC6D0 tipR=0.932 hiltR=0.253 dev=0.036 fitQ=0.92
q=5.77"
planeNormal: {x: 0.04436424, y: 0.9695876, z: 0.24068981}
pivot: {x: -0.07215572, y: 0.45991144, z: -0.0608187}
bisector: {x: -0.8473422, y: -0.09110173, z: 0.52317464}
startDir: {x: -0.20181037, y: 0.24465796, z: -0.9483749}
tipRadius: 0.65453726
hiltRadius: 0.3102
sweepDeg: 220.66309
stabAxis: {x: 0, y: 0, z: 1}
stabStart: {x: -0.07215572, y: 0.45991144, z: -0.0608187}
stabLength: 0.65453726
quality: 50
source: "wl816t 2026-09-15 09:35 Attack2_S_10101 f14~f44/52@60Hz n=31 PC=LH_M05
rootScale=1.0527 weaponK=0.5369 path=7.36 bladeAng=614\xB0 sweep=220.7\xB0
pivot=\uCE7C\uB05D\uC6D0 tipR=0.655 hiltR=0.310 dev=0.089 fitQ=0.73 (\uB7F0\uD0C0\uC784
\uB85C\uCEEC=\uD68C\uC804\uB9CC)"
- classId: 10101
clipName: Effect_Slash_10101_3
kind: 1
planeNormal: {x: 0.16853563, y: -0.9836209, z: -0.06391966}
pivot: {x: 0.09357196, y: 1.2423787, z: -0.1380892}
bisector: {x: 0.94817513, y: 0.14405777, z: 0.28321597}
startDir: {x: 0.25327274, y: 0.10588382, z: -0.9615829}
tipRadius: 0.95663345
hiltRadius: 0.40691212
sweepDeg: 181.94073
stabAxis: {x: -0.12657256, y: -0.108728364, z: 0.9859805}
stabStart: {x: 0.3463071, y: 1.37269, z: -1.1111009}
stabLength: 2.0542197
planeNormal: {x: -0.23372532, y: -0.96444863, z: -0.12333438}
pivot: {x: -0.37444147, y: 0.6109033, z: 0.22184418}
bisector: {x: 0.95191514, y: -0.20113623, z: -0.2310883}
startDir: {x: 0.61234134, y: -0.04747615, z: -0.7891667}
tipRadius: 0.71797794
hiltRadius: 0.51229453
sweepDeg: 78.42377
stabAxis: {x: 0, y: 0, z: 1}
stabStart: {x: -0.37444147, y: 0.6109033, z: 0.22184418}
stabLength: 0.71797794
quality: 50
source: "editbake 2026-09-07 09:58 Attack3_S_10101 f7~f11@60Hz n=9 path=3.03
bladeAng=154\xB0 sweep=181.9\xB0 pivot=\uCE7C\uB05D\uC6D0 tipR=0.957 hiltR=0.407
dev=0.071 fitQ=0.85 q=2.58 (WL804_Bake \xB7 \uB7F0\uD0C0\uC784 \uC7AC\uC2E4\uCE21\uC774
\uB36E\uC9C0 \uC54A\uB3C4\uB85D quality \uACE0\uC815)"
source: "wl816t 2026-09-15 09:35 Attack3_S_10101 f16~f54/64@60Hz n=39 PC=LH_M05
rootScale=1.0527 weaponK=0.5369 path=3.97 bladeAng=257\xB0 sweep=78.4\xB0 pivot=\uCE7C\uB05D\uC6D0
tipR=0.718 hiltR=0.512 dev=0.120 fitQ=0.66 (\uB7F0\uD0C0\uC784 \uB85C\uCEEC=\uD68C\uC804\uB9CC)"
- classId: 10102
clipName: Effect_Slash_10102_1
kind: 1
planeNormal: {x: 0.6155671, y: -0.7459609, z: 0.2542036}
pivot: {x: -0.44185022, y: 0.90176433, z: -0.14577419}
bisector: {x: -0.18893999, y: -0.452843, z: -0.8713409}
startDir: {x: 0.4323041, y: 0.5893181, z: 0.6825082}
tipRadius: 0.71769434
hiltRadius: 0.2310255
sweepDeg: 398.79218
stabAxis: {x: 0, y: 0, z: 1}
stabStart: {x: -0.44185022, y: 0.90176433, z: -0.14577419}
stabLength: 0.71769434
quality: 50
source: "wl816t 2026-09-15 09:35 Attack1_S_10102 f12~f35/50@60Hz n=24 PC=LH_M05
rootScale=1.0527 weaponK=0.5369 path=6.88 bladeAng=544\xB0 sweep=398.8\xB0
pivot=\uC190\uC7A1\uC774\uC911\uC2EC tipR=0.718 hiltR=0.231 dev=0.195 fitQ=0.46
(\uB7F0\uD0C0\uC784 \uB85C\uCEEC=\uD68C\uC804\uB9CC)"
- classId: 10102
clipName: Effect_Slash_10102_2
kind: 1
planeNormal: {x: 0.04436424, y: 0.9695876, z: 0.24068981}
pivot: {x: -0.07215572, y: 0.45991144, z: -0.0608187}
bisector: {x: -0.8473422, y: -0.09110173, z: 0.52317464}
startDir: {x: -0.20181037, y: 0.24465796, z: -0.9483749}
tipRadius: 0.65453726
hiltRadius: 0.3102
sweepDeg: 220.66309
stabAxis: {x: 0, y: 0, z: 1}
stabStart: {x: -0.07215572, y: 0.45991144, z: -0.0608187}
stabLength: 0.65453726
quality: 50
source: "wl816t 2026-09-15 09:35 Attack2_S_10102 f14~f44/52@60Hz n=31 PC=LH_M05
rootScale=1.0527 weaponK=0.5369 path=7.36 bladeAng=614\xB0 sweep=220.7\xB0
pivot=\uCE7C\uB05D\uC6D0 tipR=0.655 hiltR=0.310 dev=0.089 fitQ=0.73 (\uB7F0\uD0C0\uC784
\uB85C\uCEEC=\uD68C\uC804\uB9CC)"
- classId: 10102
clipName: Effect_Slash_10102_3
kind: 1
planeNormal: {x: 0.16853563, y: -0.9836209, z: -0.06391966}
pivot: {x: 0.09357196, y: 1.2423787, z: -0.1380892}
bisector: {x: 0.94817513, y: 0.14405777, z: 0.28321597}
startDir: {x: 0.25327274, y: 0.10588382, z: -0.9615829}
tipRadius: 0.95663345
hiltRadius: 0.40691212
sweepDeg: 181.94073
stabAxis: {x: -0.12657256, y: -0.108728364, z: 0.9859805}
stabStart: {x: 0.3463071, y: 1.37269, z: -1.1111009}
stabLength: 2.0542197
planeNormal: {x: -0.23372532, y: -0.96444863, z: -0.12333438}
pivot: {x: -0.37444147, y: 0.6109033, z: 0.22184418}
bisector: {x: 0.95191514, y: -0.20113623, z: -0.2310883}
startDir: {x: 0.61234134, y: -0.04747615, z: -0.7891667}
tipRadius: 0.71797794
hiltRadius: 0.51229453
sweepDeg: 78.42377
stabAxis: {x: 0, y: 0, z: 1}
stabStart: {x: -0.37444147, y: 0.6109033, z: 0.22184418}
stabLength: 0.71797794
quality: 50
source: "editbake 2026-09-07 09:58 Attack3_S_10102 f7~f11@60Hz n=9 path=3.03
bladeAng=154\xB0 sweep=181.9\xB0 pivot=\uCE7C\uB05D\uC6D0 tipR=0.957 hiltR=0.407
dev=0.071 fitQ=0.85 q=2.58 (WL804_Bake \xB7 \uB7F0\uD0C0\uC784 \uC7AC\uC2E4\uCE21\uC774
\uB36E\uC9C0 \uC54A\uB3C4\uB85D quality \uACE0\uC815)"
source: "wl816t 2026-09-15 09:35 Attack3_S_10102 f16~f54/64@60Hz n=39 PC=LH_M05
rootScale=1.0527 weaponK=0.5369 path=3.97 bladeAng=257\xB0 sweep=78.4\xB0 pivot=\uCE7C\uB05D\uC6D0
tipR=0.718 hiltR=0.512 dev=0.120 fitQ=0.66 (\uB7F0\uD0C0\uC784 \uB85C\uCEEC=\uD68C\uC804\uB9CC)"
- classId: 10501
clipName: Effect_Slash_10501_1
kind: 1
planeNormal: {x: 0.6043122, y: -0.7630855, z: 0.22914462}
pivot: {x: -0.5143255, y: 0.99384046, z: -0.13518894}
bisector: {x: -0.2910512, y: -0.47915196, z: -0.8280716}
startDir: {x: 0.60951185, y: 0.6279901, z: 0.48386335}
tipRadius: 0.7389534
hiltRadius: 0.23210584
sweepDeg: 416.96194
stabAxis: {x: 0, y: 0, z: 1}
stabStart: {x: -0.5143255, y: 0.99384046, z: -0.13518894}
stabLength: 0.7389534
quality: 50
source: "wl816t 2026-09-15 09:35 Attack1_S_10501 f12~f35/50@60Hz n=24 PC=LH_M05
rootScale=1.0527 weaponK=0.6587 path=7.35 bladeAng=545\xB0 sweep=417.0\xB0
pivot=\uC190\uC7A1\uC774\uC911\uC2EC tipR=0.739 hiltR=0.232 dev=0.204 fitQ=0.45
(\uB7F0\uD0C0\uC784 \uB85C\uCEEC=\uD68C\uC804\uB9CC)"
- classId: 10501
clipName: Effect_Slash_10501_2
kind: 1
planeNormal: {x: 0.04244417, y: 0.97287947, z: 0.2273852}
pivot: {x: -0.077203944, y: 0.43669456, z: -0.052627534}
bisector: {x: -0.81407905, y: -0.098262936, z: 0.5723808}
startDir: {x: -0.2501254, y: 0.23068981, z: -0.9403295}
tipRadius: 0.7294197
hiltRadius: 0.2821212
sweepDeg: 221.86562
stabAxis: {x: 0, y: 0, z: 1}
stabStart: {x: -0.077203944, y: 0.43669456, z: -0.052627534}
stabLength: 0.7294197
quality: 50
source: "wl816t 2026-09-15 09:35 Attack2_S_10501 f14~f44/52@60Hz n=31 PC=LH_M05
rootScale=1.0527 weaponK=0.6587 path=8.04 bladeAng=616\xB0 sweep=221.9\xB0
pivot=\uCE7C\uB05D\uC6D0 tipR=0.729 hiltR=0.282 dev=0.083 fitQ=0.77 (\uB7F0\uD0C0\uC784
\uB85C\uCEEC=\uD68C\uC804\uB9CC)"
- classId: 10501
clipName: Effect_Slash_10501_3
kind: 1
planeNormal: {x: 0.16179384, y: -0.98433506, z: -0.070051745}
pivot: {x: 0.06956807, y: 1.2031016, z: -0.17662044}
bisector: {x: 0.98678565, y: 0.16074902, z: 0.020341488}
startDir: {x: 0.019124245, y: 0.074101284, z: -0.99706733}
tipRadius: 0.797171
hiltRadius: 0.35618255
sweepDeg: 178.7966
stabAxis: {x: 0.11699972, y: -0.072316095, z: 0.99049556}
stabStart: {x: 0.08246852, y: 1.288136, z: -1.0311682}
stabLength: 1.7564142
planeNormal: {x: 0.043638147, y: -0.9978227, z: -0.049451575}
pivot: {x: -0.32562953, y: 0.5273627, z: 0.22276048}
bisector: {x: 0.96429247, y: 0.05501082, z: -0.25906354}
startDir: {x: 0.53950334, y: 0.06519729, z: -0.8394555}
tipRadius: 0.80060536
hiltRadius: 0.42106363
sweepDeg: 84.31582
stabAxis: {x: 0, y: 0, z: 1}
stabStart: {x: -0.32562953, y: 0.5273627, z: 0.22276048}
stabLength: 0.80060536
quality: 50
source: "editbake 2026-09-07 09:59 Attack3_S_10501 f7~f11@60Hz n=9 path=2.56
bladeAng=153\xB0 sweep=178.8\xB0 pivot=\uCE7C\uB05D\uC6D0 tipR=0.797 hiltR=0.356
dev=0.082 fitQ=0.79 q=2.03 (WL804_Bake \xB7 \uB7F0\uD0C0\uC784 \uC7AC\uC2E4\uCE21\uC774
\uB36E\uC9C0 \uC54A\uB3C4\uB85D quality \uACE0\uC815)"
source: "wl816t 2026-09-15 09:35 Attack3_S_10501 f16~f54/64@60Hz n=39 PC=LH_M05
rootScale=1.0527 weaponK=0.6587 path=4.20 bladeAng=254\xB0 sweep=84.3\xB0 pivot=\uCE7C\uB05D\uC6D0
tipR=0.801 hiltR=0.421 dev=0.120 fitQ=0.70 (\uB7F0\uD0C0\uC784 \uB85C\uCEEC=\uD68C\uC804\uB9CC)"
- classId: 10502
clipName: Effect_Slash_10502_1
kind: 1
planeNormal: {x: 0.6255864, y: -0.7492798, z: 0.21730489}
pivot: {x: -0.4383909, y: 0.96184254, z: -0.13046789}
bisector: {x: 0.32277644, y: 0.5021659, z: 0.80227476}
startDir: {x: 0.6897078, y: 0.6613513, z: 0.29481786}
tipRadius: 0.6846314
hiltRadius: 0.21322028
sweepDeg: 75.39421
stabAxis: {x: 0, y: 0, z: 1}
stabStart: {x: -0.4383909, y: 0.96184254, z: -0.13046789}
stabLength: 0.6846314
quality: 50
source: "wl816t 2026-09-15 09:35 Attack1_S_10502 f12~f35/50@60Hz n=24 PC=LH_M05
rootScale=1.0527 weaponK=0.5632 path=6.42 bladeAng=545\xB0 sweep=75.4\xB0 pivot=\uC190\uC7A1\uC774\uC911\uC2EC
tipR=0.685 hiltR=0.213 dev=0.167 fitQ=0.51 (\uB7F0\uD0C0\uC784 \uB85C\uCEEC=\uD68C\uC804\uB9CC)"
- classId: 10502
clipName: Effect_Slash_10502_2
kind: 1
planeNormal: {x: 0.047004227, y: 0.97822416, z: 0.20215859}
pivot: {x: -0.07373327, y: 0.44671315, z: -0.044609603}
bisector: {x: -0.7875351, y: -0.08820486, z: 0.609925}
startDir: {x: -0.28951094, y: 0.20703694, z: -0.9345154}
tipRadius: 0.6426512
hiltRadius: 0.30169082
sweepDeg: 222.2306
stabAxis: {x: 0, y: 0, z: 1}
stabStart: {x: -0.07373327, y: 0.44671315, z: -0.044609603}
stabLength: 0.6426512
quality: 50
source: "wl816t 2026-09-15 09:35 Attack2_S_10502 f14~f44/52@60Hz n=31 PC=LH_M05
rootScale=1.0527 weaponK=0.5632 path=7.04 bladeAng=616\xB0 sweep=222.2\xB0
pivot=\uCE7C\uB05D\uC6D0 tipR=0.643 hiltR=0.302 dev=0.076 fitQ=0.76 (\uB7F0\uD0C0\uC784
\uB85C\uCEEC=\uD68C\uC804\uB9CC)"
- classId: 10502
clipName: Effect_Slash_10502_3
kind: 1
planeNormal: {x: 0.16322699, y: -0.9838918, z: -0.07289651}
pivot: {x: 0.06879786, y: 1.1890548, z: -0.17932634}
bisector: {x: 0.9864896, y: 0.16381113, z: -0.0020673866}
startDir: {x: -0.012327602, y: 0.0718477, z: -0.9973394}
tipRadius: 0.74294686
hiltRadius: 0.5402583
sweepDeg: 179.8086
stabAxis: {x: 0.14303008, y: -0.07050567, z: 0.98720384}
stabStart: {x: 0.055376478, y: 1.2679739, z: -0.98118037}
stabLength: 1.6514131
planeNormal: {x: 0.035052184, y: -0.99825996, z: -0.047418203}
pivot: {x: -0.28646088, y: 0.51465774, z: 0.23871267}
bisector: {x: 0.9424084, y: 0.04880832, z: -0.330884}
startDir: {x: 0.4772259, y: 0.058407497, z: -0.8768375}
tipRadius: 0.70055455
hiltRadius: 0.39948177
sweepDeg: 84.072044
stabAxis: {x: 0, y: 0, z: 1}
stabStart: {x: -0.28646088, y: 0.51465774, z: 0.23871267}
stabLength: 0.70055455
quality: 50
source: "editbake 2026-09-07 09:59 Attack3_S_10502 f7~f11@60Hz n=9 path=2.41
bladeAng=153\xB0 sweep=179.8\xB0 pivot=\uCE7C\uB05D\uC6D0 tipR=0.743 hiltR=0.540
dev=0.083 fitQ=0.78 q=1.87 (WL804_Bake \xB7 \uB7F0\uD0C0\uC784 \uC7AC\uC2E4\uCE21\uC774
\uB36E\uC9C0 \uC54A\uB3C4\uB85D quality \uACE0\uC815)"
source: "wl816t 2026-09-15 09:35 Attack3_S_10502 f16~f54/64@60Hz n=39 PC=LH_M05
rootScale=1.0527 weaponK=0.5632 path=3.83 bladeAng=254\xB0 sweep=84.1\xB0 pivot=\uCE7C\uB05D\uC6D0
tipR=0.701 hiltR=0.399 dev=0.110 fitQ=0.69 (\uB7F0\uD0C0\uC784 \uB85C\uCEEC=\uD68C\uC804\uB9CC)"
swingMinSweepDegrees: 150
swingMinTipHiltRatio: 3
stabMinStraightness: 0.86
@ -559,6 +626,8 @@ MonoBehaviour:
swingTrimSpeedRatio: 0.2
arcRadiusFit: 0
arcSizeScale: 1
arcFitAttackRange: 1
arcRangeScale: 1
arcPlaneOffset: 0
arcRadialOffset: 0
arcLifetimeSeconds: 0