WL 세션 3 (#803~#806 · PD 승인 2026-09-07) — 세로 전용 전환 · Attack3 검기 무기 정합(ShowEffect 0.25 · editbake 캘리브레이션 4행) · 타격감 midplus · 공용 팝업 세로 크기 · 검증 도구(SuppressPets 복원 수정 · ComboSpeed/Trace3 · 신규 WL803/804/806 도구)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
깃 관리자 2026-09-07 14:01:36 +09:00
parent b4a0977d4d
commit abcd038656
15 changed files with 799 additions and 64 deletions

View File

@ -117,10 +117,23 @@ public static class WL760_Seq
return t != null ? t.name : "(none)"; return t != null ? t.name : "(none)";
} }
// 펫이 있으면 같은 몹을 때려 데미지 귀속이 오염된다 → 런 동안 매 프레임 꺼 두고 끝나면 켠다(게임이 도중에 다시 켤 수 있음). // 펫이 있으면 같은 몹을 때려 데미지 귀속이 오염된다 → 런 동안 매 프레임 꺼 두고 끝나면 켠다(게임이 도중에 다시 켤 수 있음).
// #806(2026-09-07) 수정: 복원(suppress=false) 때 "이 도구가 끈 펫"만 되살린다.
// 이전 버전은 씬의 비활성 PetActor 를 전부 켜서, 게임이 의도적으로 꺼 둔 펫(PCInfo.Make_Pet 의 Off() 잔여 · 펫 메뉴 미리보기 모델)까지
// 되살렸다 → #792 검증 중 "펫 재등장" 의 유력 원인【추정】. 게임 코드 게이트(WLGameplaySettings.spawnPlayerPet)와 무관한 도구 결함.
static readonly System.Collections.Generic.HashSet<int> s_suppressed = new System.Collections.Generic.HashSet<int>();
static void SuppressPets(bool suppress) static void SuppressPets(bool suppress)
{ {
foreach (var p in UnityEngine.Object.FindObjectsByType<PetActor>(FindObjectsInactive.Include, FindObjectsSortMode.None)) if (suppress)
if (p.gameObject.activeSelf == suppress) p.gameObject.SetActive(!suppress); {
foreach (var p in UnityEngine.Object.FindObjectsByType<PetActor>(FindObjectsInactive.Include, FindObjectsSortMode.None))
if (p.gameObject.activeSelf) { p.gameObject.SetActive(false); s_suppressed.Add(p.GetInstanceID()); }
}
else
{
foreach (var p in UnityEngine.Object.FindObjectsByType<PetActor>(FindObjectsInactive.Include, FindObjectsSortMode.None))
if (s_suppressed.Contains(p.GetInstanceID()) && !p.gameObject.activeSelf) p.gameObject.SetActive(true);
s_suppressed.Clear();
}
} }
public IEnumerator Co(MyActor me, int classId, string tag, float dist, bool noPet) public IEnumerator Co(MyActor me, int classId, string tag, float dist, bool noPet)

View File

@ -29,10 +29,12 @@ public static class WL792_Verify
public static void Dirs(int classId) { Launch(h => h.Co_Dirs(classId), "dirs" + classId); } public static void Dirs(int classId) { Launch(h => h.Co_Dirs(classId), "dirs" + classId); }
public static void Combo(int classId) { Launch(h => h.Co_Combo(classId), "combo" + classId); } public static void Combo(int classId) { Launch(h => h.Co_Combo(classId), "combo" + classId); }
public static void ComboSpeed(int classId, float speed) { Launch(h => h.Co_ComboSpeed(classId, speed), "comboS" + classId); } // #804 클립 원속 재실측
public static void Moving(int classId) { Launch(h => h.Co_Moving(classId), "moving" + classId); } public static void Moving(int classId) { Launch(h => h.Co_Moving(classId), "moving" + classId); }
public static void Budget() { Launch(h => h.Co_Budget(), "budget"); } public static void Budget() { Launch(h => h.Co_Budget(), "budget"); }
public static void Regress() { Launch(h => h.Co_Regress(), "regress"); } public static void Regress() { Launch(h => h.Co_Regress(), "regress"); }
public static void Trace(int classId) { Launch(h => h.Co_Trace(classId), "trace" + classId); } public static void Trace(int classId) { Launch(h => h.Co_Trace(classId), "trace" + classId); }
public static void Trace3(int classId) { Launch(h => h.Co_Trace3(classId), "trace3_" + classId); } // #804 Attack3 한 클립 프레임 추적
/// <summary>런타임 캘리브레이션·기하 캐시를 비운다(설정/알고리즘을 바꾼 뒤 재실측).</summary> /// <summary>런타임 캘리브레이션·기하 캐시를 비운다(설정/알고리즘을 바꾼 뒤 재실측).</summary>
public static void Reset() public static void Reset()
@ -351,6 +353,94 @@ public static class WL792_Verify
Done(sb); Done(sb);
} }
// ── ②-S 콤보 연속 · 지정 공격 속도 (#804 · 2026-09-07) ──────────────────
// 런타임 공격 속도(FinalAttackSpeed 약 3.5)에서는 Attack3 스윙 창 안 샘플이 6~9 개뿐이라
// 캘리브레이션 품질이 낮다(실측 path 0.25 m · Stab 오판). 클립 원속(1.0)으로 돌려 샘플을 채운다.
// 베이크된 캘리브레이션은 이벤트 시점에 캐릭터 로컬로 펼치므로 공격 속도와 무관하게 재사용된다.
public IEnumerator Co_ComboSpeed(int classId, float speed)
{
var sb = new StringBuilder();
sb.AppendLine("# WL #804 콤보 연속(속도 " + speed.ToString("F2") + ") class=" + classId + " " + DateTime.Now.ToString("HH:mm:ss"));
yield return SetClass(sb, classId);
var me = Me();
if (me == null) { Done(sb); yield break; }
var anim = me.GetComponentInChildren<Animator>();
int before = WeaponTrailDriver.MappedSpawnCount;
int shots = 0;
float gap = 1.3f / Mathf.Max(speed, 0.1f);
float t0 = Time.time;
me.Play_Attack(0, speed);
float tEnd = Time.time + 6f * gap + 3f;
while (Time.time < tEnd && shots < 6)
{
yield return null;
if (WeaponTrailDriver.MappedSpawnCount > before)
{
before = WeaponTrailDriver.MappedSpawnCount;
var p = WeaponTrailDriver.LastPlacement;
sb.AppendLine(" " + Row("comboS" + shots + " " + ClipName(anim), p));
yield return new WaitForSeconds(0.05f);
yield return Cap(tag_ + "_seq" + shots);
shots++;
}
if (Time.time - t0 > gap && shots < 6)
{
me.Play_Attack(0, speed);
t0 = Time.time;
}
}
sb.AppendLine("캡처 " + shots + "장");
Done(sb);
}
// ── #804 Attack3 한 클립을 프레임마다 추적 — nt · 칼끝 이동 · 스윙 창 on/off · 배치 스폰 ─────
public IEnumerator Co_Trace3(int classId)
{
var sb = new StringBuilder();
sb.AppendLine("# WL #804 Attack3 프레임 추적 class=" + classId + " " + DateTime.Now.ToString("HH:mm:ss"));
yield return SetClass(sb, classId);
var me = Me();
if (me == null) { Done(sb); yield break; }
var anim = me.GetComponentInChildren<Animator>();
float aspd = (float)ASpd(me);
sb.AppendLine("FinalAttackSpeed=" + aspd.ToString("F3") + " animator.speed=" + anim.speed.ToString("F3"));
float t0 = Time.time; float tEnd = Time.time + 12f;
me.Play_Attack(0, aspd);
bool recorded = false;
var rows = new List<string>();
while (Time.time < tEnd && !recorded)
{
yield return null;
if (Time.time - t0 > 1.2f) { me.Play_Attack(0, aspd); t0 = Time.time; }
string c = ClipName(anim);
if (!c.Contains("Attack3")) continue;
// Attack3 진입 — 클립이 끝날 때까지 매 프레임 기록
var drv = me.GetComponent<WeaponTrailDriver>();
Vector3 prevTip = drv != null ? drv.GetTipWorld(0) : Vector3.zero;
int spawn0 = WeaponTrailDriver.MappedSpawnCount;
int fi = 0; float path = 0f;
while (ClipName(anim).Contains("Attack3") && fi < 200)
{
var st = anim.GetCurrentAnimatorStateInfo(0);
float nt = st.normalizedTime % 1f;
Vector3 tip = drv != null ? drv.GetTipWorld(0) : Vector3.zero;
float step = Vector3.Distance(tip, prevTip); if (fi > 0) path += step;
rows.Add(string.Format(" f{0,-3} nt={1,5:F3} inTrans={2} speed={3:F2} tipStep={4:F3} path={5:F2} swing={6} spawned={7} localTip={8}",
fi, nt, anim.IsInTransition(0) ? 1 : 0, anim.speed, step, path, drv != null && drv.IsSwinging ? 1 : 0,
WeaponTrailDriver.MappedSpawnCount - spawn0, me.transform.InverseTransformPoint(tip).ToString("F2")));
prevTip = tip; fi++;
yield return null;
}
recorded = true;
}
sb.AppendLine(recorded ? "Attack3 frames=" + rows.Count : "!! Attack3 not reached in 12 s");
foreach (var r in rows) sb.AppendLine(r);
Done(sb);
}
// ── ③ 이동 중 공격 ────────────────────────────────────────────────── // ── ③ 이동 중 공격 ──────────────────────────────────────────────────
public IEnumerator Co_Moving(int classId) public IEnumerator Co_Moving(int classId)
{ {

View File

@ -62,7 +62,7 @@ public static class WL797_MakeAsset
return sb.ToString(); return sb.ToString();
} }
/// <summary>세기 프리셋. args: "weak" | "mid" | "strong" | "off"</summary> /// <summary>세기 프리셋. args: "weak" | "mid" | "midplus" | "strong" | "off"</summary>
public static object Preset(string name) public static object Preset(string name)
{ {
var so = AssetDatabase.LoadAssetAtPath<WLHitFeelSettings>(Path); var so = AssetDatabase.LoadAssetAtPath<WLHitFeelSettings>(Path);
@ -85,6 +85,13 @@ public static class WL797_MakeAsset
so.shakeAmplitude = 0.10f; so.shakeDuration = 0.12f; so.shakeAmplitude = 0.10f; so.shakeDuration = 0.12f;
so.scalePunch = 1.12f; so.scalePunch = 1.12f;
break; break;
case "midplus": // B+ — PD 2026-09-07 "현재보다 미세하게 강하게" (mid 와 strong 의 중간)
case "mid+":
so.enabled = true;
so.hitStopDuration = 0.04f; so.hitStopKillDuration = 0.065f;
so.shakeAmplitude = 0.11f; so.shakeDuration = 0.12f;
so.scalePunch = 1.14f;
break;
case "strong": // C — 발주 상한 case "strong": // C — 발주 상한
so.enabled = true; so.enabled = true;
so.hitStopDuration = 0.05f; so.hitStopKillDuration = 0.08f; so.hitStopDuration = 0.05f; so.hitStopKillDuration = 0.08f;
@ -92,7 +99,7 @@ public static class WL797_MakeAsset
so.scalePunch = 1.16f; so.scalePunch = 1.16f;
break; break;
default: default:
return "UNKNOWN preset '" + name + "' (weak|mid|strong|off)"; return "UNKNOWN preset '" + name + "' (weak|mid|midplus|strong|off)";
} }
EditorUtility.SetDirty(so); EditorUtility.SetDirty(so);

View File

@ -0,0 +1,42 @@
// WL803_Portrait.cs — #803 세로(포트레이트) 전용 전환 (에디트 모드 · ProjectSettings.asset 만 수정 · PD 결정 2026-09-07)
// unity command run_script --file AgentScripts/WL803_Portrait.cs --entry WL803_Portrait.Dump
// unity command run_script --file AgentScripts/WL803_Portrait.cs --entry WL803_Portrait.Apply (세로 전용)
// unity command run_script --file AgentScripts/WL803_Portrait.cs --entry WL803_Portrait.Revert (가로 자동회전 원복 = 09-07 이전 값)
// 근거: 이전 값 defaultScreenOrientation 4(AutoRotation) · Portrait 0 · PortraitUpsideDown 0 · LandscapeRight 1 · LandscapeLeft 1.
// 코드의 방향 참조는 Assets/Script/Util/SafeArea.cs 하나(Portrait 기본 · 가로일 때만 분기) → 세로 전용에서 그대로 동작.
using UnityEditor;
using UnityEngine;
public static class WL803_Portrait
{
public static object Dump()
{
return string.Format("defaultInterfaceOrientation={0} · autorotate Portrait={1} PortraitUpsideDown={2} LandscapeLeft={3} LandscapeRight={4} · animatedAutorotation={5}",
PlayerSettings.defaultInterfaceOrientation,
PlayerSettings.allowedAutorotateToPortrait, PlayerSettings.allowedAutorotateToPortraitUpsideDown,
PlayerSettings.allowedAutorotateToLandscapeLeft, PlayerSettings.allowedAutorotateToLandscapeRight,
PlayerSettings.useAnimatedAutorotation);
}
public static object Apply()
{
PlayerSettings.defaultInterfaceOrientation = UIOrientation.Portrait;
PlayerSettings.allowedAutorotateToPortrait = true;
PlayerSettings.allowedAutorotateToPortraitUpsideDown = false;
PlayerSettings.allowedAutorotateToLandscapeLeft = false;
PlayerSettings.allowedAutorotateToLandscapeRight = false;
AssetDatabase.SaveAssets();
return "APPLIED portrait-only → " + Dump();
}
public static object Revert()
{
PlayerSettings.defaultInterfaceOrientation = UIOrientation.AutoRotation;
PlayerSettings.allowedAutorotateToPortrait = false;
PlayerSettings.allowedAutorotateToPortraitUpsideDown = false;
PlayerSettings.allowedAutorotateToLandscapeLeft = true;
PlayerSettings.allowedAutorotateToLandscapeRight = true;
AssetDatabase.SaveAssets();
return "REVERTED landscape-autorotate → " + Dump();
}
}

191
AgentScripts/WL804_Bake.cs Normal file
View File

@ -0,0 +1,191 @@
// WL804_Bake.cs — #804 Attack3 검기 캘리브레이션을 **에디트 모드 결정적 샘플링**으로 베이크 (PD 결정 2026-09-07 "3타 휘두르기 · 무기 방향에 정합")
// unity command run_script --file AgentScripts/WL804_Bake.cs --entry WL804_Bake.BakeAll --args "[false]" (dry · 수치만)
// unity command run_script --file AgentScripts/WL804_Bake.cs --entry WL804_Bake.BakeAll --args "[true]" (SlashTrailSettings 에 기록)
// unity command run_script --file AgentScripts/WL804_Bake.cs --entry WL804_Bake.Bake --args "[10101, 7.0, 12.0, 60.0, false]"
//
// 왜 에디트 모드 베이크인가 (실측 2026-09-07)
// · Attack3 클립(Knight@Attack3_S · 32f)의 실제 스윙은 f7~f12(nt 0.22~0.38)에서 칼끝이 약 3 m 움직이는 **가슴 높이 수평 스윙**
// (오른쪽 뒤 → 앞 → 왼쪽 앞)이다 — WL804_TipProbe(에디트 30fps) 와 WL792_Verify.Trace3(런타임 60fps) 두 실측이 일치.
// · 그런데 런타임 MeasureSwingCalibration 은 Attack3 에서만 path 0.20~0.25 m · 샘플 6~9 개를 돌려준다(Attack1/2 는 5 m · 25~29 개).
// 원인은 미규명(【미확인】 — 슬롯 선택/샘플 거리 필터 의심). 이벤트 시각을 f10→f8.5 로 앞당겨도 같았다.
// · 배치는 (클래스,클립) 캘리브레이션 행을 이벤트 시점에 캐릭터 로컬 → 월드로 펼치는 구조라, 행만 정확하면 런타임 실측과 무관하게 정합된다.
// 그래서 같은 모델·무기·클립을 프리뷰 씬에서 60 Hz 로 샘플해 MeasureSwingCalibration 과 **같은 수식**(SlashArcMeasure)으로 행을 만든다.
// · quality 는 런타임 재실측(BakeCalibrations 의 "더 좋을 때만 교체")이 이 행을 덮지 않도록 크게 둔다(source 에 editbake 표기).
using System.Linq;
using System.Text;
using UnityEditor;
using UnityEditor.SceneManagement;
using UnityEngine;
using WL.Combat;
public static class WL804_Bake
{
const string ModelPath = "Assets/Res_Addr/PC/Ai01.prefab";
const string SettingsPath = "Assets/WL/Settings/Resources/WL/SlashTrailSettings.asset";
const string ClassConfigJson = "Assets/ResWork/Table/Export/ClassConfig.json";
const string Fbx3 = "Assets/Res_Addr/Animations/Animation/OneHand/Knight@Attack3_S.FBX";
const float BakedQuality = 50f; // 런타임 q 는 2~6 → 이 행이 항상 우선
public static object BakeAll(bool apply)
{
var sb = new StringBuilder();
foreach (var cid in new[] { 10101, 10102, 10501, 10502 })
sb.Append(Bake(cid, 7f, 12f, 60f, apply)).AppendLine();
return sb.ToString();
}
/// <summary>frame0~frame1 (30fps 프레임 단위 · 원 클립 기준) 을 hz 로 샘플해 캘리브레이션을 만든다.</summary>
public static object Bake(int classId, float frame0, float frame1, float hz, bool apply)
{
// ── 클래스 → 무기 (ClassConfig.json · 런타임 table_classconfig 은 에디트 모드에 없다)
string weaponName = null; int socketIndex = -1;
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"]); }
break;
}
}
catch (System.Exception ex) { return "ClassConfig parse fail: " + ex.Message; }
if (string.IsNullOrEmpty(weaponName) || socketIndex < 0) return "class " + classId + " weapon not found";
var model = AssetDatabase.LoadAssetAtPath<GameObject>(ModelPath);
var clips = AssetDatabase.LoadAllAssetsAtPath(Fbx3).OfType<AnimationClip>().Where(c => !c.name.StartsWith("__preview")).ToArray();
var clip = clips.FirstOrDefault(c => c.name == "Attack3_S_" + classId) ?? clips.FirstOrDefault(c => c.name == "Attack3");
var weapon = AssetDatabase.LoadAssetAtPath<GameObject>("Assets/Res_Addr/Weapon/" + weaponName + ".prefab");
var settings = AssetDatabase.LoadAssetAtPath<SlashTrailSettings>(SettingsPath);
if (model == null || clip == null || weapon == null || settings == null)
return "load fail model=" + (model != null) + " clip=" + (clip != null) + " weapon=" + (weapon != null) + "(" + weaponName + ") settings=" + (settings != null);
var scene = EditorSceneManager.NewPreviewScene();
var sb = new StringBuilder();
bool animMode = false;
try
{
var go = (GameObject)PrefabUtility.InstantiatePrefab(model, scene);
go.transform.position = Vector3.zero; go.transform.rotation = Quaternion.identity;
var pc = go.GetComponentInChildren<PCActor>(true);
if (pc == null || pc.tfs_weapon == null || socketIndex >= pc.tfs_weapon.Length) return "tfs_weapon missing";
var socket = pc.tfs_weapon[socketIndex];
var w = (GameObject)PrefabUtility.InstantiatePrefab(weapon, scene);
w.transform.SetParent(socket, false);
w.transform.localPosition = Vector3.zero; w.transform.localRotation = Quaternion.identity; w.transform.localScale = Vector3.one;
// MeasureSlot 규칙과 동일 — 가장 긴 렌더러 바운딩 장축 양끝 · 소켓에 가까운 쪽 = 자루
Renderer best = null; Mesh bestMesh = null; float bestExtent = 0f;
foreach (var r in socket.GetComponentsInChildren<Renderer>(true))
{
var mf = r.GetComponent<MeshFilter>(); var smr = r as SkinnedMeshRenderer;
var mesh = smr != null ? smr.sharedMesh : (mf != null ? mf.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 "no 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);
var anim = go.GetComponentInChildren<Animator>();
var animGo = anim.gameObject; anim.applyRootMotion = false;
var root = go.transform; // 런타임 ToLocalPos/Dir 의 기준 = 액터 루트(위치+회전)
float t0 = frame0 / 30f, t1 = frame1 / 30f;
int n = Mathf.Max(3, Mathf.RoundToInt((t1 - t0) * hz) + 1);
var tips = new Vector3[n]; var hilts = new Vector3[n];
AnimationMode.StartAnimationMode(); animMode = true;
for (int i = 0; i < n; i++)
{
float t = t0 + (t1 - t0) * i / (n - 1);
AnimationMode.BeginSampling();
AnimationMode.SampleAnimationClip(animGo, clip, t);
AnimationMode.EndSampling();
tips[i] = root.InverseTransformPoint(socket.TransformPoint(tipLocal));
hilts[i] = root.InverseTransformPoint(socket.TransformPoint(hiltLocal));
}
AnimationMode.StopAnimationMode(); animMode = false;
// ── MeasureSwingCalibration 과 같은 수식 ─────────────────────────────
float fullPath = 0f, hiltPath = 0f, bladeAng = 0f;
Vector3 prevDir = (tips[0] - hilts[0]).normalized;
for (int i = 1; i < n; i++)
{
fullPath += Vector3.Distance(tips[i], tips[i - 1]);
hiltPath += Vector3.Distance(hilts[i], hilts[i - 1]);
var d = (tips[i] - hilts[i]).normalized; bladeAng += Vector3.Angle(prevDir, d); prevDir = d;
}
float tipPerHilt = fullPath / Mathf.Max(hiltPath, 1e-3f);
int kMax = 0; float dMax = 0f;
for (int i = 1; i < n; i++) { float d2 = (tips[i] - tips[0]).sqrMagnitude; if (d2 > dMax) { dMax = d2; kMax = i; } }
float chord = Mathf.Sqrt(dMax);
float pathToMax = 0f; for (int i = 1; i <= kMax; i++) pathToMax += Vector3.Distance(tips[i], tips[i - 1]);
float straight = pathToMax > 1e-5f ? chord / pathToMax : 0f;
bool swingShape = bladeAng >= settings.swingMinSweepDegrees || tipPerHilt >= settings.swingMinTipHiltRatio;
bool stabShape = chord >= settings.stabMinDistance && straight >= settings.stabMinStraightness;
var kind = swingShape ? SwingKind.Swing : (stabShape ? SwingKind.Stab : SwingKind.Swing);
var fit = SlashArcMeasure.FitArcOrdered(tips, n);
sb.AppendLine(string.Format("== class {0} weapon {1}@{2} clip {3} f{4}~f{5} @{6}Hz n={7} · path={8:F2} hiltPath={9:F2} tip/hilt={10:F2} bladeAng={11:F0}° chord={12:F2} straight={13:F2} → kind={14} · fit.valid={15} sweep={16:F1}° r={17:F3}",
classId, weaponName, socket.name, clip.name, frame0, frame1, hz, n, fullPath, hiltPath, tipPerHilt, bladeAng, chord, straight, kind, fit.valid, fit.sweepDeg, fit.radiusOuter));
if (!fit.valid) return sb.Append("!! 원 피팅 실패").ToString();
Vector3 nrm = fit.normal; Vector3 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 = "칼끝원";
bool bad = tipR < 1e-3f || tipR <= hiltR * 1.15f || dev > tipR * 0.25f;
if (bad)
{
Vector3 tipCentroid = SlashArcMeasure.CentroidOnPlane(tips, n, nrm, tips[0]);
pivot = SlashArcMeasure.CentroidOnPlane(hilts, n, nrm, tipCentroid);
tipR = SlashArcMeasure.MeanRadius(tips, n, pivot, nrm);
hiltR = SlashArcMeasure.MeanRadius(hilts, n, pivot, nrm);
pivotSrc = "손잡이중심";
}
Vector3 sd, bis; float sweep;
if (!SlashArcMeasure.RecomputeAboutPivot(tips, n, pivot, ref nrm, out sd, out bis, out sweep))
return sb.Append("!! RecomputeAboutPivot 실패").ToString();
float fitQ = Mathf.Clamp01(1f - (dev / Mathf.Max(tipR, 1e-3f)) * 2f);
float q = fullPath * Mathf.Max(fitQ, 0.05f);
sb.AppendLine(string.Format(" pivot({0})={1} tipR={2:F3} hiltR={3:F3} dev={4:F3} fitQ={5:F2} q={6:F2} · normal={7} bisector={8} startDir={9} sweep={10:F1}°{11}",
pivotSrc, V(pivot), tipR, hiltR, dev, fitQ, q, V(nrm), V(bis), V(sd), sweep, sweep < settings.swingMinSweepDegrees ? " (⚠ 런타임이면 칼날평면 폴백 구간 — 베이크는 원 피팅 그대로 기록)" : ""));
sb.AppendLine(" tips: " + string.Join(" ", tips.Select(V)));
if (!apply) return sb.Append(" (dry)").ToString();
var cal = new SwingCalibration
{
classId = classId, clipName = "Effect_Slash_" + classId + "_3", kind = SwingKind.Swing,
planeNormal = nrm, bisector = bis, startDir = sd, pivot = pivot, tipRadius = tipR, hiltRadius = hiltR, sweepDeg = sweep,
stabAxis = (tips[kMax] - tips[0]).normalized, stabStart = tips[0], stabLength = Mathf.Max(chord, 1e-3f),
quality = Mathf.Max(BakedQuality, q),
source = string.Format("editbake {0:yyyy-MM-dd HH:mm} {1} f{2}~f{3}@{4}Hz n={5} path={6:F2} bladeAng={7:F0}° sweep={8:F1}° pivot={9} tipR={10:F3} hiltR={11:F3} dev={12:F3} fitQ={13:F2} q={14:F2} (WL804_Bake · 런타임 재실측이 덮지 않도록 quality 고정)",
System.DateTime.Now, clip.name, frame0, frame1, hz, n, fullPath, bladeAng, sweep, pivotSrc, tipR, hiltR, dev, fitQ, q)
};
int removed = settings.swingCalibrations.RemoveAll(c => c != null && c.classId == classId && c.clipName == cal.clipName);
settings.swingCalibrations.Add(cal);
EditorUtility.SetDirty(settings);
AssetDatabase.SaveAssets();
sb.AppendLine(" APPLIED → " + cal.clipName + " (기존 " + removed + "행 교체 · 총 " + settings.swingCalibrations.Count + "행)");
}
finally
{
if (animMode) AnimationMode.StopAnimationMode();
EditorSceneManager.ClosePreviewScene(scene);
}
return sb.ToString();
}
static string V(Vector3 v) { return string.Format("({0:F2},{1:F2},{2:F2})", 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)); }
}

View File

@ -0,0 +1,130 @@
// WL804_TipProbe.cs — #804 공격 클립의 칼끝 궤적 실측 → ShowEffect 이벤트 시각을 "실제 스윙 구간"으로 옮기기 위한 근거 (에디트 모드 · 읽기 전용 · 프리뷰 씬)
// unity command run_script --file AgentScripts/WL804_TipProbe.cs --entry WL804_TipProbe.All
// unity command run_script --file AgentScripts/WL804_TipProbe.cs --entry WL804_TipProbe.Run --args "[\"Assets/Res_Addr/Animations/Animation/OneHand/Knight@Attack3_S.FBX\",\"Attack3\",\"Elven_Sword_01\",2,0.29305]"
// 방법: WL791_FootProbe 와 같은 프리뷰 씬 샘플링. Ai01.prefab(PCActor.tfs_weapon 소켓) 에 클래스 무기 프리팹(ClassConfig s_WeaponRHPrefab)을
// 런타임 PCActor.Make_Weapon 과 같은 방식(소켓 자식 · 로컬 0/identity/1)으로 붙이고, WeaponTrailDriver.MeasureSlot 과 같은 규칙
// (가장 긴 렌더러 바운딩의 장축 양끝 · 소켓에 가까운 쪽 = 자루)으로 칼끝/자루를 정한 뒤 30fps 프레임마다 루트 기준 위치를 기록한다.
// 출력: 프레임별 칼끝 이동(cm) · 칼날 방향 변화(deg) · 누적 경로 → "스윙 구간"(이동량 ≥ 최대의 30% 연속 구간) 과
// 현재 ShowEffect 시각 + 창(swingWindowNormalized 0.35) 이 스윙을 얼마나 덮는지(경로 비율).
using System.Linq;
using System.Text;
using UnityEditor;
using UnityEditor.SceneManagement;
using UnityEngine;
public static class WL804_TipProbe
{
const string ModelPath = "Assets/Res_Addr/PC/Ai01.prefab";
const float Window = 0.35f; // SlashTrailSettings.swingWindowNormalized (실측값 · 표시용)
public static object All()
{
var sb = new StringBuilder();
sb.Append(Run("Assets/Res_Addr/Animations/Animation/OneHand/Knight@Attack1_S.FBX", "Attack1", "Elven_Sword_01", 2, 0.29821f)).AppendLine();
sb.Append(Run("Assets/Res_Addr/Animations/Animation/OneHand/Knight@Attack2_S.FBX", "Attack2", "Elven_Sword_01", 2, 0.26643f)).AppendLine();
sb.Append(Run("Assets/Res_Addr/Animations/Animation/OneHand/Knight@Attack3_S.FBX", "Attack3", "Elven_Sword_01", 2, 0.29305f)).AppendLine();
return sb.ToString();
}
public static object Run(string fbxPath, string clipName, string weaponName, int socketIndex, float currentEventNorm)
{
var model = AssetDatabase.LoadAssetAtPath<GameObject>(ModelPath);
var clips = AssetDatabase.LoadAllAssetsAtPath(fbxPath).OfType<AnimationClip>().Where(c => !c.name.StartsWith("__preview")).ToArray();
var clip = clips.FirstOrDefault(c => c.name == clipName) ?? clips.FirstOrDefault(c => c.name.Contains(clipName));
var weapon = AssetDatabase.LoadAssetAtPath<GameObject>("Assets/Res_Addr/Weapon/" + weaponName + ".prefab");
if (model == null || clip == null || weapon == null)
return "load fail model=" + (model != null) + " clip=" + (clip != null) + " weapon=" + (weapon != null) + " clips=[" + string.Join(",", clips.Select(c => c.name)) + "]";
var scene = EditorSceneManager.NewPreviewScene();
var sb = new StringBuilder();
bool animMode = false;
try
{
var go = (GameObject)PrefabUtility.InstantiatePrefab(model, scene);
go.transform.position = Vector3.zero; go.transform.rotation = Quaternion.identity;
var pc = go.GetComponentInChildren<PCActor>(true);
if (pc == null || pc.tfs_weapon == null || socketIndex < 0 || socketIndex >= pc.tfs_weapon.Length) return "PCActor/tfs_weapon null or index out of range";
var socket = pc.tfs_weapon[socketIndex];
if (socket == null) return "socket null idx=" + socketIndex;
var w = (GameObject)PrefabUtility.InstantiatePrefab(weapon, scene);
w.transform.SetParent(socket, false);
w.transform.localPosition = Vector3.zero; w.transform.localRotation = Quaternion.identity; w.transform.localScale = Vector3.one;
// MeasureSlot 규칙
Renderer best = null; Mesh bestMesh = null; float bestExtent = 0f;
foreach (var r in socket.GetComponentsInChildren<Renderer>(true))
{
var mf = r.GetComponent<MeshFilter>(); var smr = r as SkinnedMeshRenderer;
var mesh = smr != null ? smr.sharedMesh : (mf != null ? mf.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 "no renderer under socket " + socket.name;
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);
float bladeLen = Vector3.Distance(hiltLocal, tipLocal);
var anim = go.GetComponentInChildren<Animator>();
if (anim == null) return "animator null";
var animGo = anim.gameObject; anim.applyRootMotion = false;
float fps = clip.frameRate; int n = Mathf.RoundToInt(clip.length * fps);
var T = new Vector3[n + 1]; var H = new Vector3[n + 1];
AnimationMode.StartAnimationMode(); animMode = true;
for (int i = 0; i <= n; i++)
{
AnimationMode.BeginSampling();
AnimationMode.SampleAnimationClip(animGo, clip, i / fps);
AnimationMode.EndSampling();
T[i] = animGo.transform.InverseTransformPoint(socket.TransformPoint(tipLocal));
H[i] = animGo.transform.InverseTransformPoint(socket.TransformPoint(hiltLocal));
}
sb.AppendLine("== " + clipName + " (" + clip.name + ") len=" + clip.length.ToString("F3") + "s frames=" + n + " fps=" + fps
+ " weapon=" + weaponName + " socket=" + socket.name + " blade=" + best.name + " len=" + bladeLen.ToString("F2") + "m · currentShowEffect=" + currentEventNorm.ToString("F3") + " window=" + Window + " ==");
sb.AppendLine(" f norm | tip x y z | dTip(cm) | dDir(deg) | cum(cm)");
var d = new float[n + 1]; float accum = 0f;
for (int i = 1; i <= n; i++)
{
d[i] = Vector3.Distance(T[i], T[i - 1]); accum += d[i];
float ang = Vector3.Angle(T[i] - H[i], T[i - 1] - H[i - 1]);
sb.AppendLine(string.Format(" {0,3} {1,5:F3} | {2,6:F2} {3,5:F2} {4,5:F2} | {5,7:F1} | {6,6:F1} | {7,7:F1}",
i, i / (float)n, T[i].x, T[i].y, T[i].z, d[i] * 100f, ang, accum * 100f));
}
float dmax = d.Max(); int imax = System.Array.IndexOf(d, dmax);
float thr = dmax * 0.30f;
int s = imax, e = imax;
while (s - 1 >= 1 && d[s - 1] >= thr) s--;
while (e + 1 <= n && d[e + 1] >= thr) e++;
float swingPath = 0f; for (int i = s; i <= e; i++) swingPath += d[i];
sb.AppendLine(string.Format("SWING(dTip >= 30% of max {0:F1}cm @f{1}) = f{2}~f{3} -> norm {4:F3}~{5:F3} · path {6:F2} m / total {7:F2} m",
dmax * 100f, imax, s, e, s / (float)n, e / (float)n, swingPath, accum));
float cover = Coverage(d, n, currentEventNorm, Window);
sb.AppendLine(string.Format("CURRENT ShowEffect {0:F3} + window {1:F2} (f{2}~f{3}) covers path {4:F2} m ({5:P0} of total)",
currentEventNorm, Window, Mathf.CeilToInt(currentEventNorm * n), Mathf.Min(n, Mathf.FloorToInt((currentEventNorm + Window) * n)), cover, accum > 0 ? cover / accum : 0f));
float rec = Mathf.Max(0f, (s - 1) / (float)n);
float recCover = Coverage(d, n, rec, Window);
sb.AppendLine(string.Format("RECOMMEND ShowEffect ~ {0:F3} (f{1}) -> window covers {2:F2} m ({3:P0}) · window end f{4}{5}",
rec, s - 1, recCover, accum > 0 ? recCover / accum : 0f, Mathf.Min(n, Mathf.FloorToInt((rec + Window) * n)), (rec + Window) * n < e ? " !! window shorter than swing end f" + e : ""));
}
finally
{
if (animMode) AnimationMode.StopAnimationMode();
EditorSceneManager.ClosePreviewScene(scene);
}
return sb.ToString();
}
static float Coverage(float[] d, int n, float startNorm, float window)
{
int a = Mathf.Max(1, Mathf.CeilToInt(startNorm * n)), z = Mathf.Min(n, Mathf.FloorToInt((startNorm + window) * n));
float c = 0f; for (int i = a; i <= z; i++) c += d[i]; return c;
}
static Vector3 Abs(Vector3 v) { return new Vector3(Mathf.Abs(v.x), Mathf.Abs(v.y), Mathf.Abs(v.z)); }
}

112
AgentScripts/WL804_Tools.cs Normal file
View File

@ -0,0 +1,112 @@
// WL804_Tools.cs — 2026-09-07 세션 보조 도구 (#804 검기 3타 · #806 후속 검증)
// unity command run_script --file AgentScripts/WL804_Tools.cs --entry WL804_Tools.Refresh (에디트 · AssetDatabase.Refresh + 매핑 Attack3 ShowEffect 값 확인)
// unity command run_script --file AgentScripts/WL804_Tools.cs --entry WL804_Tools.FbxEvents (에디트 · Knight@Attack3_S 서브클립 ShowEffect time 확인)
// unity command run_script --file AgentScripts/WL804_Tools.cs --entry WL804_Tools.RuntimeCal (Play · WeaponTrailDriver.RuntimeCalibrations 요약)
// unity command run_script --file AgentScripts/WL804_Tools.cs --entry WL804_Tools.PetCount (Play · PetActor 인스턴스 · ActorInfo 펫 수)
// unity command run_script --file AgentScripts/WL804_Tools.cs --entry WL804_Tools.PopupShow --args "[124]" (Play · 공용 Popup 표시)
// unity command run_script --file AgentScripts/WL804_Tools.cs --entry WL804_Tools.PopupClose (Play)
using System.Linq;
using System.Text;
using UnityEditor;
using UnityEngine;
public static class WL804_Tools
{
const string MappingPath = "Assets/WL/Combat/CombatAnimMapping.asset";
const string Fbx3 = "Assets/Res_Addr/Animations/Animation/OneHand/Knight@Attack3_S.FBX";
public static object Refresh()
{
AssetDatabase.Refresh(ImportAssetOptions.ForceSynchronousImport);
var so = AssetDatabase.LoadAssetAtPath<WL.Combat.CombatAnimMapping>(MappingPath);
if (so == null) return "mapping null";
var sb = new StringBuilder("refreshed · mapping Attack3 ShowEffect:\n");
foreach (var c in so.classes)
foreach (var st in c.states)
if (st.stateName == "Attack3")
foreach (var e in st.events)
if (e.functionName == "ShowEffect") sb.AppendLine(" " + c.classId + " " + st.stateName + " norm=" + e.normalizedTime + " useNorm=" + e.useNormalized);
return sb.ToString();
}
public static object FbxEvents()
{
var imp = AssetImporter.GetAtPath(Fbx3) as ModelImporter;
if (imp == null) return "importer null";
var sb = new StringBuilder();
foreach (var c in imp.clipAnimations)
{
sb.Append(c.name + " [" + c.firstFrame + "~" + c.lastFrame + "f]:");
foreach (var e in c.events) sb.Append(" " + e.functionName + "@" + e.time.ToString("F4"));
sb.AppendLine();
}
sb.AppendLine("motionNodeName='" + imp.motionNodeName + "'");
return sb.ToString();
}
public static object RuntimeCal()
{
var rt = WL.Combat.WeaponTrailDriver.RuntimeCalibrations;
if (rt == null) return "RuntimeCalibrations null";
var sb = new StringBuilder("runtime calibrations " + rt.Count + "\n");
foreach (var kv in rt)
{
var c = kv.Value; if (c == null) continue;
sb.AppendLine(string.Format(" {0} {1} kind={2} sweep={3:F1} q={4:F2} | {5}", c.classId, c.clipName, c.kind, c.sweepDeg, c.quality, c.source));
}
return sb.ToString();
}
public static object PetCount()
{
if (!Application.isPlaying) return "not playing";
var pets = UnityEngine.Object.FindObjectsByType<PetActor>(FindObjectsInactive.Include, FindObjectsSortMode.None);
var sb = new StringBuilder("PetActor instances=" + pets.Length + "\n");
foreach (var p in pets)
{
string path = p.name; var t = p.transform.parent; while (t != null) { path = t.name + "/" + path; t = t.parent; }
sb.AppendLine(" " + path + " active=" + p.gameObject.activeInHierarchy + " layer=" + LayerMask.LayerToName(p.gameObject.layer) + " pos=" + p.transform.position);
}
try
{
var list = ActorInfo.Ins.Get_Actors(eRole.Pet);
sb.AppendLine("ActorInfo eRole.Pet count=" + (list != null ? list.Count : -1));
var my = MyValue.MyPC != null ? MyValue.MyPC.Get_MyPet() : null;
sb.AppendLine("MyPC.Get_MyPet()=" + (my != null ? my.name : "null"));
}
catch (System.Exception ex) { sb.AppendLine("ActorInfo/MyValue err: " + ex.Message); }
sb.AppendLine("spawnPlayerPet=" + WL.Settings.WLGameplaySettings.SpawnPlayerPet);
return sb.ToString();
}
public static object PopupShow(int msgKey)
{
if (!Application.isPlaying) return "not playing";
var pop = UnityEngine.Object.FindFirstObjectByType<Popup>(FindObjectsInactive.Include);
if (pop == null) return "Popup instance not found";
pop.Set(ePopupType.One, msgKey);
var rt = pop.m_popup != null ? pop.m_popup.GetComponentInChildren<UnityEngine.UI.Button>(true) : null;
return "popup shown key=" + msgKey + " text='" + (pop.label_msg != null ? pop.label_msg.text : "") + "' btn=" + (rt != null ? ((RectTransform)rt.transform).rect.size.ToString() : "?")
+ " scale=" + (rt != null ? rt.transform.lossyScale.x.ToString("F4") : "?");
}
public static object PopupClose()
{
if (!Application.isPlaying) return "not playing";
var pop = UnityEngine.Object.FindFirstObjectByType<Popup>(FindObjectsInactive.Include);
if (pop == null) return "Popup instance not found";
pop.OnClick_OK();
return "popup closed";
}
/// <summary>에디트 모드 · SlashTrailSettings.swingCalibrations 에서 clipName 이 suffix 로 끝나는 행 제거 (재베이크 전 · 예: "_3")</summary>
public static object RemoveCal(string clipSuffix)
{
var so = AssetDatabase.LoadAssetAtPath<WL.Combat.SlashTrailSettings>("Assets/WL/Settings/Resources/WL/SlashTrailSettings.asset");
if (so == null || so.swingCalibrations == null) return "settings null";
var removed = new StringBuilder();
int n = so.swingCalibrations.RemoveAll(c => { bool r = c != null && c.clipName != null && c.clipName.EndsWith(clipSuffix); if (r) removed.Append(c.classId + "/" + c.clipName + " q=" + c.quality.ToString("F2") + "; "); return r; });
EditorUtility.SetDirty(so); AssetDatabase.SaveAssets();
return "removed " + n + " [" + removed + "] remaining " + so.swingCalibrations.Count;
}
}

101
AgentScripts/WL806_Popup.cs Normal file
View File

@ -0,0 +1,101 @@
// WL806_Popup.cs — 후속 B: SortOrder_5.prefab/PopupUI(공용 Popup 싱글톤) 를 세로 화면에서 터치·가독 가능한 크기로 (에디트 모드 · 프리팹 컨텐츠 · Play 불필요)
// unity command run_script --file AgentScripts/WL806_Popup.cs --entry WL806_Popup.Dump
// unity command run_script --file AgentScripts/WL806_Popup.cs --entry WL806_Popup.Apply
// unity command run_script --file AgentScripts/WL806_Popup.cs --entry WL806_Popup.Revert (2026-09-07 이전 원값)
// 배경(실측 Dump 2026-09-07): 캔버스 1920×1080 Expand → 세로 1080×1920 에서 scaleFactor 0.5625.
// 원값: bg 428×311 · msg 400×165 (0,30) fs20 · btn_ok 116×56 (0,-102) fs20 → 실화면 bg 241×175 px · 글자 11 px · 버튼 65×32 px (터치·가독 미달).
// 목표: TitleInfo 팝업(#794 · 1080 기준 bg 860×520 · msg 780×280 (0,70) · btn_ok 340×130 (0,-150)) 과 같은 실크기 → 1920 기준 유닛 = px / 0.5625.
// 글자: 로딩 화면(#800) 과 같은 실화면 ≈27 px → fs 48.
using System.Text;
using UnityEditor;
using UnityEngine;
using UnityEngine.UI;
public static class WL806_Popup
{
const string PrefabPath = "Assets/ResWork/UIPrefabs/Title/SortOrder_5.prefab";
// 1920×1080 기준 유닛 (px / 0.5625)
static readonly Vector2 BgSize = new Vector2(1529f, 924f);
static readonly Vector2 MsgSize = new Vector2(1387f, 498f); static readonly Vector2 MsgPos = new Vector2(0f, 124f);
static readonly Vector2 BtnSize = new Vector2(604f, 231f); static readonly Vector2 BtnPos = new Vector2(0f, -267f);
const float FontSize = 48f;
// 원값
static readonly Vector2 BgSize0 = new Vector2(428f, 311f);
static readonly Vector2 MsgSize0 = new Vector2(400f, 165f); static readonly Vector2 MsgPos0 = new Vector2(0f, 30f);
static readonly Vector2 BtnSize0 = new Vector2(116f, 56f); static readonly Vector2 BtnPos0 = new Vector2(0f, -102f);
const float FontSize0 = 20f;
static void Walk(Transform t, string path, StringBuilder sb, int depth)
{
var rt = t as RectTransform;
if (rt != null)
{
var txt = t.GetComponent<Text>();
var tmp = t.GetComponent<TMPro.TextMeshProUGUI>();
string font = txt != null ? (" Text fs=" + txt.fontSize + " bestFit=" + txt.resizeTextForBestFit)
: tmp != null ? (" TMP fs=" + tmp.fontSize + " auto=" + tmp.enableAutoSizing + " wrap=" + tmp.textWrappingMode + " overflow=" + tmp.overflowMode) : "";
var img = t.GetComponent<Image>();
string im = img != null ? (" Image=" + (img.sprite != null ? img.sprite.name : "null") + " type=" + img.type) : "";
var btn = t.GetComponent<Button>();
sb.AppendLine(new string(' ', depth * 2) + path + (t.gameObject.activeSelf ? "" : " (inactive)")
+ " | size=" + rt.sizeDelta + " pos=" + rt.anchoredPosition + " aMin=" + rt.anchorMin + " aMax=" + rt.anchorMax + " pivot=" + rt.pivot
+ (btn != null ? " [Button]" : "") + im + font);
}
for (int i = 0; i < t.childCount; i++) Walk(t.GetChild(i), path + "/" + t.GetChild(i).name, sb, depth + 1);
}
static RectTransform FindPopup(GameObject root)
{
foreach (var rt in root.GetComponentsInChildren<RectTransform>(true)) if (rt.name == "PopupUI") return rt;
return null;
}
public static object Dump()
{
var root = PrefabUtility.LoadPrefabContents(PrefabPath);
try
{
var sb = new StringBuilder();
var scaler = root.GetComponentInChildren<CanvasScaler>(true);
if (scaler != null) sb.AppendLine("CanvasScaler ref=" + scaler.referenceResolution + " mode=" + scaler.uiScaleMode + " match=" + scaler.matchWidthOrHeight + " screenMatch=" + scaler.screenMatchMode);
var popup = FindPopup(root);
if (popup == null) return "PopupUI not found";
Walk(popup, "PopupUI", sb, 0);
return sb.ToString();
}
finally { PrefabUtility.UnloadPrefabContents(root); }
}
static object Set(Vector2 bg, Vector2 msg, Vector2 msgPos, Vector2 btn, Vector2 btnPos, float fs, string tag)
{
var root = PrefabUtility.LoadPrefabContents(PrefabPath);
try
{
var popup = FindPopup(root);
if (popup == null) return "PopupUI not found";
var child = popup.Find("child");
if (child == null) return "PopupUI/child not found";
var bgRt = child.Find("bg") as RectTransform;
var msgRt = child.Find("msg") as RectTransform;
var btnRt = child.Find("btn_ok") as RectTransform;
var nameRt = btnRt != null ? btnRt.Find("btnName") : null;
if (bgRt == null || msgRt == null || btnRt == null) return "bg/msg/btn_ok missing";
var sb = new StringBuilder(tag + "\n");
sb.AppendLine("bg " + bgRt.sizeDelta + " -> " + bg); bgRt.sizeDelta = bg;
sb.AppendLine("msg " + msgRt.sizeDelta + msgRt.anchoredPosition + " -> " + msg + msgPos); msgRt.sizeDelta = msg; msgRt.anchoredPosition = msgPos;
sb.AppendLine("btn " + btnRt.sizeDelta + btnRt.anchoredPosition + " -> " + btn + btnPos); btnRt.sizeDelta = btn; btnRt.anchoredPosition = btnPos;
var msgT = msgRt.GetComponent<TMPro.TextMeshProUGUI>();
if (msgT != null) { sb.AppendLine("msg fs " + msgT.fontSize + " -> " + fs); msgT.fontSize = fs; }
var nameT = nameRt != null ? nameRt.GetComponent<TMPro.TextMeshProUGUI>() : null;
if (nameT != null) { sb.AppendLine("btnName fs " + nameT.fontSize + " -> " + fs); nameT.fontSize = fs; }
PrefabUtility.SaveAsPrefabAsset(root, PrefabPath);
AssetDatabase.SaveAssets();
return sb.ToString();
}
finally { PrefabUtility.UnloadPrefabContents(root); }
}
public static object Apply() { return Set(BgSize, MsgSize, MsgPos, BtnSize, BtnPos, FontSize, "APPLY portrait-size popup"); }
public static object Revert() { return Set(BgSize0, MsgSize0, MsgPos0, BtnSize0, BtnPos0, FontSize0, "REVERT original popup"); }
}

View File

@ -92,10 +92,23 @@ public static class WL778_Seq
if (mi == null || a == null) return double.NaN; if (mi == null || a == null) return double.NaN;
try { return Convert.ToDouble(mi.Invoke(a, null)); } catch { return double.NaN; } try { return Convert.ToDouble(mi.Invoke(a, null)); } catch { return double.NaN; }
} }
// #806(2026-09-07) 수정: 복원(suppress=false) 때 "이 도구가 끈 펫"만 되살린다.
// 이전 버전은 씬의 비활성 PetActor 를 전부 켜서, 게임이 의도적으로 꺼 둔 펫(PCInfo.Make_Pet 의 Off() 잔여 · 펫 메뉴 미리보기 모델)까지
// 되살렸다 → #792 검증 중 "펫 재등장" 의 유력 원인【추정】. 게임 코드 게이트(WLGameplaySettings.spawnPlayerPet)와 무관한 도구 결함.
static readonly System.Collections.Generic.HashSet<int> s_suppressed = new System.Collections.Generic.HashSet<int>();
static void SuppressPets(bool suppress) static void SuppressPets(bool suppress)
{ {
foreach (var p in UnityEngine.Object.FindObjectsByType<PetActor>(FindObjectsInactive.Include, FindObjectsSortMode.None)) if (suppress)
if (p.gameObject.activeSelf == suppress) p.gameObject.SetActive(!suppress); {
foreach (var p in UnityEngine.Object.FindObjectsByType<PetActor>(FindObjectsInactive.Include, FindObjectsSortMode.None))
if (p.gameObject.activeSelf) { p.gameObject.SetActive(false); s_suppressed.Add(p.GetInstanceID()); }
}
else
{
foreach (var p in UnityEngine.Object.FindObjectsByType<PetActor>(FindObjectsInactive.Include, FindObjectsSortMode.None))
if (s_suppressed.Contains(p.GetInstanceID()) && !p.gameObject.activeSelf) p.gameObject.SetActive(true);
s_suppressed.Clear();
}
} }
public IEnumerator Co(MyActor me, int classId, string tag, float dist, bool noPet, float seconds) public IEnumerator Co(MyActor me, int classId, string tag, float dist, bool noPet, float seconds)

View File

@ -3948,8 +3948,8 @@ MonoBehaviour:
m_faceColor: m_faceColor:
serializedVersion: 2 serializedVersion: 2
rgba: 4294967295 rgba: 4294967295
m_fontSize: 20 m_fontSize: 48
m_fontSizeBase: 20 m_fontSizeBase: 48
m_fontWeight: 400 m_fontWeight: 400
m_enableAutoSizing: 0 m_enableAutoSizing: 0
m_fontSizeMin: 10 m_fontSizeMin: 10
@ -6265,8 +6265,8 @@ RectTransform:
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0.5, y: 0.5} m_AnchorMin: {x: 0.5, y: 0.5}
m_AnchorMax: {x: 0.5, y: 0.5} m_AnchorMax: {x: 0.5, y: 0.5}
m_AnchoredPosition: {x: 0, y: -102} m_AnchoredPosition: {x: 0, y: -267}
m_SizeDelta: {x: 116, y: 56} m_SizeDelta: {x: 604, y: 231}
m_Pivot: {x: 0.5, y: 0.5} m_Pivot: {x: 0.5, y: 0.5}
--- !u!222 &7926492337561385874 --- !u!222 &7926492337561385874
CanvasRenderer: CanvasRenderer:
@ -8297,7 +8297,7 @@ RectTransform:
m_AnchorMin: {x: 0.5, y: 0.5} m_AnchorMin: {x: 0.5, y: 0.5}
m_AnchorMax: {x: 0.5, y: 0.5} m_AnchorMax: {x: 0.5, y: 0.5}
m_AnchoredPosition: {x: 0, y: 0} m_AnchoredPosition: {x: 0, y: 0}
m_SizeDelta: {x: 428, y: 311} m_SizeDelta: {x: 1529, y: 924}
m_Pivot: {x: 0.5, y: 0.5} m_Pivot: {x: 0.5, y: 0.5}
--- !u!222 &5685264383111004087 --- !u!222 &5685264383111004087
CanvasRenderer: CanvasRenderer:
@ -9266,8 +9266,8 @@ RectTransform:
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0.5, y: 0.5} m_AnchorMin: {x: 0.5, y: 0.5}
m_AnchorMax: {x: 0.5, y: 0.5} m_AnchorMax: {x: 0.5, y: 0.5}
m_AnchoredPosition: {x: 0, y: 30} m_AnchoredPosition: {x: 0, y: 124}
m_SizeDelta: {x: 400, y: 165} m_SizeDelta: {x: 1387, y: 498}
m_Pivot: {x: 0.5, y: 0.5} m_Pivot: {x: 0.5, y: 0.5}
--- !u!222 &1314091230975439951 --- !u!222 &1314091230975439951
CanvasRenderer: CanvasRenderer:
@ -9328,8 +9328,8 @@ MonoBehaviour:
m_faceColor: m_faceColor:
serializedVersion: 2 serializedVersion: 2
rgba: 4294967295 rgba: 4294967295
m_fontSize: 20 m_fontSize: 48
m_fontSizeBase: 20 m_fontSizeBase: 48
m_fontWeight: 400 m_fontWeight: 400
m_enableAutoSizing: 0 m_enableAutoSizing: 0
m_fontSizeMin: 10 m_fontSizeMin: 10

View File

@ -672,7 +672,7 @@ ModelImporter:
floatParameter: 0 floatParameter: 0
intParameter: 0 intParameter: 0
messageOptions: 0 messageOptions: 0
- time: 0.31258667 - time: 0.26666668
functionName: ShowEffect functionName: ShowEffect
data: Effect_Slash_10101_3 data: Effect_Slash_10101_3
objectReferenceParameter: {instanceID: 0} objectReferenceParameter: {instanceID: 0}
@ -729,7 +729,7 @@ ModelImporter:
floatParameter: 0 floatParameter: 0
intParameter: 0 intParameter: 0
messageOptions: 0 messageOptions: 0
- time: 0.31258667 - time: 0.26666668
functionName: ShowEffect functionName: ShowEffect
data: Effect_Slash_10102_3 data: Effect_Slash_10102_3
objectReferenceParameter: {instanceID: 0} objectReferenceParameter: {instanceID: 0}
@ -786,7 +786,7 @@ ModelImporter:
floatParameter: 0 floatParameter: 0
intParameter: 0 intParameter: 0
messageOptions: 0 messageOptions: 0
- time: 0.31258667 - time: 0.26666668
functionName: ShowEffect functionName: ShowEffect
data: Effect_Slash_10501_3 data: Effect_Slash_10501_3
objectReferenceParameter: {instanceID: 0} objectReferenceParameter: {instanceID: 0}
@ -843,7 +843,7 @@ ModelImporter:
floatParameter: 0 floatParameter: 0
intParameter: 0 intParameter: 0
messageOptions: 0 messageOptions: 0
- time: 0.31258667 - time: 0.26666668
functionName: ShowEffect functionName: ShowEffect
data: Effect_Slash_10502_3 data: Effect_Slash_10502_3
objectReferenceParameter: {instanceID: 0} objectReferenceParameter: {instanceID: 0}

View File

@ -101,7 +101,7 @@ MonoBehaviour:
- functionName: ShowEffect - functionName: ShowEffect
time: 0 time: 0
useNormalized: 1 useNormalized: 1
normalizedTime: 0.29305 normalizedTime: 0.25
stringParameter: 'Effect_Slash_{CID}_3' stringParameter: 'Effect_Slash_{CID}_3'
intParameter: 1 intParameter: 1
floatParameter: 0 floatParameter: 0
@ -204,7 +204,7 @@ MonoBehaviour:
- functionName: ShowEffect - functionName: ShowEffect
time: 0 time: 0
useNormalized: 1 useNormalized: 1
normalizedTime: 0.29305 normalizedTime: 0.25
stringParameter: 'Effect_Slash_{CID}_3' stringParameter: 'Effect_Slash_{CID}_3'
intParameter: 1 intParameter: 1
floatParameter: 0 floatParameter: 0
@ -307,7 +307,7 @@ MonoBehaviour:
- functionName: ShowEffect - functionName: ShowEffect
time: 0 time: 0
useNormalized: 1 useNormalized: 1
normalizedTime: 0.29305 normalizedTime: 0.25
stringParameter: 'Effect_Slash_{CID}_3' stringParameter: 'Effect_Slash_{CID}_3'
intParameter: 1 intParameter: 1
floatParameter: 0 floatParameter: 0
@ -410,7 +410,7 @@ MonoBehaviour:
- functionName: ShowEffect - functionName: ShowEffect
time: 0 time: 0
useNormalized: 1 useNormalized: 1
normalizedTime: 0.29305 normalizedTime: 0.25
stringParameter: 'Effect_Slash_{CID}_3' stringParameter: 'Effect_Slash_{CID}_3'
intParameter: 1 intParameter: 1
floatParameter: 0 floatParameter: 0

View File

@ -16,20 +16,20 @@ MonoBehaviour:
throttleSeconds: 0.08 throttleSeconds: 0.08
hitStopEnabled: 1 hitStopEnabled: 1
hitStopMode: 0 hitStopMode: 0
hitStopDuration: 0.03 hitStopDuration: 0.04
hitStopKillDuration: 0.05 hitStopKillDuration: 0.065
hitStopTimeScale: 0 hitStopTimeScale: 0
hitStopMax: 0.08 hitStopMax: 0.08
hitStopMinTimescale: 0.1 hitStopMinTimescale: 0.1
cameraShakeEnabled: 1 cameraShakeEnabled: 1
shakeAmplitude: 0.1 shakeAmplitude: 0.11
shakeDuration: 0.12 shakeDuration: 0.12
shakeFrequency: 32 shakeFrequency: 32
shakeAxisWeight: {x: 1, y: 0.35, z: 0.6} shakeAxisWeight: {x: 1, y: 0.35, z: 0.6}
shakeKillMultiplier: 1.35 shakeKillMultiplier: 1.35
suppressLegacyShake: 1 suppressLegacyShake: 1
scalePunchEnabled: 1 scalePunchEnabled: 1
scalePunch: 1.12 scalePunch: 1.14
scalePunchDuration: 0.1 scalePunchDuration: 0.1
skipPunchOnBoss: 1 skipPunchOnBoss: 1
skipPunchOnKill: 1 skipPunchOnKill: 1

View File

@ -425,24 +425,6 @@ MonoBehaviour:
\uCE7C\uB05D/\uC190\uC7A1\uC774=3.36 sweep=308.7\xB0 straight=0.60 fwd=-0.14 \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 chord=1.56 pivot=\uCE7C\uB05D\uC6D0 tipR=0.786 hiltR=0.229 dev=0.091 fitQ=0.77
q=6.48" q=6.48"
- classId: 10501
clipName: Effect_Slash_10501_3
kind: 1
planeNormal: {x: 0.028523665, y: -0.9994087, z: 0.019202197}
pivot: {x: -0.30667877, y: 0.6199608, z: -0.21064997}
bisector: {x: 0.8267103, y: 0.034384634, z: 0.5615762}
startDir: {x: 0.8696766, y: 0.015341387, z: -0.49338353}
tipRadius: 1.1213691
hiltRadius: 0.44792122
sweepDeg: 127.48248
stabAxis: {x: -0.56417817, y: -0.0007676195, z: 0.8256527}
stabStart: {x: 0.67300415, y: 0.63777435, z: -0.7664609}
stabLength: 2.0092373
quality: 2.4277308
source: "runtime 02:26:55 samples=5/11(trim 1~5) path=2.46 \uCE7C\uB0A0\uD68C\uC804=164\xB0
\uCE7C\uB05D/\uC190\uC7A1\uC774=4.09 sweep=127.5\xB0 straight=0.82 fwd=0.83
chord=2.01 pivot=\uCE7C\uB05D\uC6D0 tipR=1.121 hiltR=0.448 dev=0.008 fitQ=0.99
q=2.43"
- classId: 10501 - classId: 10501
clipName: Effect_Slash_10501_1 clipName: Effect_Slash_10501_1
kind: 1 kind: 1
@ -500,21 +482,75 @@ MonoBehaviour:
- classId: 10101 - classId: 10101
clipName: Effect_Slash_10101_3 clipName: Effect_Slash_10101_3
kind: 1 kind: 1
planeNormal: {x: 0.4579177, y: -0.8193166, z: 0.34500954} planeNormal: {x: 0.16853563, y: -0.9836209, z: -0.06391966}
pivot: {x: 0.03202251, y: 0.61713797, z: 0.0052795457} pivot: {x: 0.09357196, y: 1.2423787, z: -0.1380892}
bisector: {x: 0.57260495, y: 0.5686936, z: 0.5905178} bisector: {x: 0.94817513, y: 0.14405777, z: 0.28321597}
startDir: {x: 0.88574374, y: 0.45364287, z: -0.09831754} startDir: {x: 0.25327274, y: 0.10588382, z: -0.9615829}
tipRadius: 0.8851383 tipRadius: 0.95663345
hiltRadius: 0 hiltRadius: 0.40691212
sweepDeg: 90 sweepDeg: 181.94073
stabAxis: {x: -0.60237515, y: -0.0005457781, z: 0.798213} stabAxis: {x: -0.12657256, y: -0.108728364, z: 0.9859805}
stabStart: {x: 0.7911307, y: 0.68160576, z: -0.84916687} stabStart: {x: 0.3463071, y: 1.37269, z: -1.1111009}
stabLength: 2.3069606 stabLength: 2.0542197
quality: 2.8422709 quality: 50
source: "runtime 02:41:23 samples=5/12(trim 6~10) path=2.91 \uCE7C\uB0A0\uD68C\uC804=165\xB0 source: "editbake 2026-09-07 09:58 Attack3_S_10101 f7~f11@60Hz n=9 path=3.03
\uCE7C\uB05D/\uC190\uC7A1\uC774=4.60 sweep=135.3\xB0 straight=0.79 fwd=0.80 bladeAng=154\xB0 sweep=181.9\xB0 pivot=\uCE7C\uB05D\uC6D0 tipR=0.957 hiltR=0.407
chord=2.31 pivot=\uCE7C\uB05D\uC6D0 tipR=1.249 hiltR=0.418 dev=0.014 fitQ=0.98 dev=0.071 fitQ=0.85 q=2.58 (WL804_Bake \xB7 \uB7F0\uD0C0\uC784 \uC7AC\uC2E4\uCE21\uC774
q=2.84 [\uCE7C\uB0A0\uD3C9\uBA74 \uD3F4\uBC31 \xB7 \uCC3D\uC774 \uD1F4\uD654]" \uB36E\uC9C0 \uC54A\uB3C4\uB85D quality \uACE0\uC815)"
- 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
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)"
- 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
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)"
- 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
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)"
swingMinSweepDegrees: 150 swingMinSweepDegrees: 150
swingMinTipHiltRatio: 3 swingMinTipHiltRatio: 3
stabMinStraightness: 0.86 stabMinStraightness: 0.86

View File

@ -8,7 +8,7 @@ PlayerSettings:
AndroidProfiler: 0 AndroidProfiler: 0
AndroidFilterTouchesWhenObscured: 0 AndroidFilterTouchesWhenObscured: 0
AndroidEnableSustainedPerformanceMode: 0 AndroidEnableSustainedPerformanceMode: 0
defaultScreenOrientation: 4 defaultScreenOrientation: 0
targetDevice: 2 targetDevice: 2
useOnDemandResources: 0 useOnDemandResources: 0
accelerometerFrequency: 60 accelerometerFrequency: 60
@ -59,10 +59,10 @@ PlayerSettings:
iosShowActivityIndicatorOnLoading: -1 iosShowActivityIndicatorOnLoading: -1
androidShowActivityIndicatorOnLoading: -1 androidShowActivityIndicatorOnLoading: -1
iosUseCustomAppBackgroundBehavior: 0 iosUseCustomAppBackgroundBehavior: 0
allowedAutorotateToPortrait: 0 allowedAutorotateToPortrait: 1
allowedAutorotateToPortraitUpsideDown: 0 allowedAutorotateToPortraitUpsideDown: 0
allowedAutorotateToLandscapeRight: 1 allowedAutorotateToLandscapeRight: 0
allowedAutorotateToLandscapeLeft: 1 allowedAutorotateToLandscapeLeft: 0
useOSAutorotation: 1 useOSAutorotation: 1
use32BitDisplayBuffer: 1 use32BitDisplayBuffer: 1
preserveFramebufferAlpha: 0 preserveFramebufferAlpha: 0