Project_WL/AgentScripts/WL792_Apply.cs

380 lines
20 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// WL792_Apply.cs — #792 이펙트 프리팹 생성 · 기하 베이크 · 설정 적용 (에디터 · Edit 모드)
//
// unity command run_script --file AgentScripts/WL792_Apply.cs --entry WL792_Apply.BuildPrefabs
// unity command run_script --file AgentScripts/WL792_Apply.cs --entry WL792_Apply.BakeGeometry
// unity command run_script --file AgentScripts/WL792_Apply.cs --entry WL792_Apply.Configure
// unity command run_script --file AgentScripts/WL792_Apply.cs --entry WL792_Apply.DumpSettings
// unity command run_script --file AgentScripts/WL792_Apply.cs --entry WL792_Apply.BakeCalibrations (Play 뒤)
// unity command run_script --file AgentScripts/WL792_Apply.cs --entry WL792_Apply.Rollback (C8 원복)
//
// 설계 메모
// · 새 이펙트는 `Assets/Res_Addr/Effect/` 아래에 **프리팹 배리언트**로 만든다.
// 그 폴더는 Addressables `Effect` 그룹의 **폴더 엔트리**라 프리팹을 넣기만 하면
// `Assets/Res_Addr/Effect/{name}.prefab` 주소로 자동 등록된다 — 그룹 에셋을 손대지 않는다.
// 따라서 기존 InGameInfo 풀(dic_str_Effect + TurnOff_GO + Show_EffectEx)을 그대로 쓴다(C11).
// · 배리언트라 PD 가 NamuFX 원본(recolor)을 고치면 그대로 따라온다.
// · 루트를 **비활성**으로 저장한다 — Get_Effect 는 `!activeInHierarchy` 인 인스턴스를 재사용하므로
// 활성 상태로 저장하면 풀이 재사용되지 않고 스폰마다 새로 로드된다(누수).
// · 씬을 더럽히지 않는다 — 프리뷰 씬(EditorSceneManager.NewPreviewScene)에서만 작업한다.
using System.Collections.Generic;
using System.Text;
using UnityEditor;
using UnityEditor.SceneManagement;
using UnityEngine;
using WL.Combat;
public static class WL792_Apply
{
const string Out = "Screenshots_WL/combat6/wl792_apply.txt";
const string SrcSwing = "Assets/NamuFX/Simple Stylized Slash vol2/Prefabs/Slash_B_recolor_1 Variant.prefab";
const string SrcStab = "Assets/ResWork/EricWang/Game VFX - Cartoon Skill Collection Vol2/Prefabs/FX_Blue Stab.prefab";
const string DstSwing = "Assets/Res_Addr/Effect/Effect_WLSwingArc.prefab";
const string DstStab = "Assets/Res_Addr/Effect/Effect_WLStab.prefab";
const string SettingsPath = "Assets/WL/Settings/Resources/WL/SlashTrailSettings.asset";
// 찌르기에서 남길 이미터 — 실측(2026-09-07) 전체 10개 · 고유 머티리얼 9개는
// 모바일 예산(스윙당 추가 드로우 ≤ 3)을 크게 넘는다. 형태를 읽는 데 필요한 셋만 남긴다.
// lb(뒤로 흐르는 스피드 라인) · blue_flash(찌르기 스트리크) · head(선단 플래시)
static readonly string[] StabKeep = { "lb", "blue_flash", "head" };
// ─────────────────────────────────────────────────────────────────────────
public static void BuildPrefabs()
{
var sb = new StringBuilder();
sb.AppendLine("# WL #792 BuildPrefabs " + System.DateTime.Now.ToString("HH:mm:ss"));
Build(sb, SrcSwing, DstSwing, "Effect_WLSwingArc", false, 1.0f);
Build(sb, SrcStab, DstStab, "Effect_WLStab", true, 0.9f);
AssetDatabase.SaveAssets();
AssetDatabase.Refresh();
Write(sb);
}
static void Build(StringBuilder sb, string srcPath, string dstPath, string name, bool forceOffTime, float offTime)
{
var src = AssetDatabase.LoadAssetAtPath<GameObject>(srcPath);
if (src == null) { sb.AppendLine("!! 원본 없음 " + srcPath); return; }
var scene = EditorSceneManager.NewPreviewScene();
try
{
var inst = (GameObject)PrefabUtility.InstantiatePrefab(src, scene);
inst.name = name;
inst.transform.localPosition = Vector3.zero;
inst.transform.localRotation = Quaternion.identity;
inst.transform.localScale = Vector3.one;
var off = inst.GetComponent<TurnOff_GO>();
if (off == null) off = inst.AddComponent<TurnOff_GO>();
off.NoUseParticleTime = forceOffTime;
off.OffTime = offTime;
inst.SetActive(false);
var saved = PrefabUtility.SaveAsPrefabAsset(inst, dstPath);
sb.AppendLine(string.Format("생성 {0} ← {1} · TurnOff_GO(NoUseParticleTime={2} OffTime={3}) · 루트 비활성 · 배리언트={4}",
dstPath, System.IO.Path.GetFileName(srcPath), forceOffTime, offTime,
saved != null && PrefabUtility.GetPrefabAssetType(saved) == PrefabAssetType.Variant));
Object.DestroyImmediate(inst);
}
catch (System.Exception e) { sb.AppendLine("!! " + name + " 실패: " + e); }
finally { EditorSceneManager.ClosePreviewScene(scene); }
}
// ─────────────────────────────────────────────────────────────────────────
public static void BakeGeometry()
{
var sb = new StringBuilder();
sb.AppendLine("# WL #792 BakeGeometry " + System.DateTime.Now.ToString("HH:mm:ss"));
var so = AssetDatabase.LoadAssetAtPath<SlashTrailSettings>(SettingsPath);
if (so == null) { sb.AppendLine("!! 설정 에셋 없음 " + SettingsPath); Write(sb); return; }
BakeOne(sb, so, DstSwing, "Effect_WLSwingArc", SwingKind.Swing, null);
BakeOne(sb, so, DstStab, "Effect_WLStab", SwingKind.Stab, StabKeep);
EditorUtility.SetDirty(so);
AssetDatabase.SaveAssets();
Write(sb);
}
static void BakeOne(StringBuilder sb, SlashTrailSettings so, string path, string name, SwingKind kind, string[] keep)
{
var asset = AssetDatabase.LoadAssetAtPath<GameObject>(path);
if (asset == null) { sb.AppendLine("!! 없음 " + path); return; }
GameObject root = null;
try
{
root = PrefabUtility.LoadPrefabContents(path);
var g = so.FindGeometry(name);
if (g == null) { g = new SlashEffectGeometry { prefabName = name }; so.effectGeometry.Add(g); }
g.kind = kind;
g.bakedAt = System.DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss");
if (kind == SwingKind.Swing)
{
var f = SlashArcMeasure.MeasureCrescent(root, null, g.flipSweep);
g.arcValid = f.valid;
if (f.valid)
{
g.emitter = f.emitter;
g.arcNormal = f.normal;
g.arcCenter = f.center;
g.arcBisector = f.bisector;
g.arcStartDir = f.startDir;
g.arcRadiusOuter = f.radiusOuter;
g.arcRadiusInner = f.radiusInner;
g.arcSweepDeg = f.sweepDeg;
}
sb.AppendLine(string.Format("[호] {0} valid={1} emitter='{2}' n={3} c={4} bis={5} rOut={6:F4} rIn={7:F4} sweep={8:F2}°",
name, f.valid, f.emitter, V(f.normal), V(f.center), V(f.bisector), f.radiusOuter, f.radiusInner, f.sweepDeg));
}
else
{
var s = SlashArcMeasure.MeasureStab(root, g.flipAxis, keep);
g.stabValid = s.valid;
if (s.valid)
{
g.emitter = s.note;
g.stabAxis = s.axis;
g.stabStart = s.start;
g.stabLength = s.length;
g.stabPlaneNormal = s.planeNormal;
}
sb.AppendLine(string.Format("[찌르기] {0} valid={1} note='{2}' axis={3} start={4} len={5:F4} planeN={6}",
name, s.valid, s.note, V(s.axis), V(s.start), s.length, V(s.planeNormal)));
}
}
catch (System.Exception e) { sb.AppendLine("!! " + name + " 베이크 실패: " + e); }
finally { if (root != null) PrefabUtility.UnloadPrefabContents(root); }
}
// ─────────────────────────────────────────────────────────────────────────
public static void Configure()
{
var sb = new StringBuilder();
sb.AppendLine("# WL #792 Configure " + System.DateTime.Now.ToString("HH:mm:ss"));
var so = AssetDatabase.LoadAssetAtPath<SlashTrailSettings>(SettingsPath);
if (so == null) { sb.AppendLine("!! 설정 에셋 없음"); Write(sb); return; }
sb.AppendLine(string.Format("변경 전 — useMappedEffects={0} burstMode={1} drawRibbon={2} arcFollowRotation={3}",
so.useMappedEffects, so.burstMode, so.drawRibbon, so.arcFollowRotation));
so.useMappedEffects = true;
so.swingArcEffect = "Effect_WLSwingArc";
so.stabEffect = "Effect_WLStab";
so.stabKeepEmitterNames = StabKeep;
// 리본 OFF — PD #792 는 NamuFX 호를 쓰므로 리본이 겹치면 안 된다.
// 궤적 샘플링은 계속된다(평면·반지름 계산에 필요) · BladeTrail/램프 텍스처 자산은 보존한다(C8).
so.drawRibbon = false;
// 레거시 버스트 경로는 꺼 둔다(#792 경로가 대신한다).
so.burstMode = BurstMode.None;
so.arcFollowPlayer = true;
so.mappedFollowRotation = true;
so.arcFollowSeconds = 0f;
so.arcRadiusFit = ArcRadiusFit.Outer;
so.arcSizeScale = 1f;
so.arcPlaneOffset = 0f;
so.arcRadialOffset = 0f;
so.arcLifetimeSeconds = 0f; // 프리팹 파티클 duration(1.0 s) 을 그대로 쓴다
so.stabAnchor = StabAnchor.Start;
so.stabSizeScale = 1f;
so.stabFaceCamera = true;
so.stabLifetimeSeconds = 0.9f;
// 분류 임계값 — 10101·10501 실측(2026-09-07) 기준.
// 창 안 칼날 누적 회전량: Attack1 366~434° · Attack2 385~424° · Attack3 164°(짧은 창일 땐 67°)
// 150 은 그 간격 한가운데다. 칼끝/손잡이 경로 비 3.0 은 손목 스냅 스윙을 지켜 주는 보조 조건.
so.swingMinSweepDegrees = 150f;
so.swingMinTipHiltRatio = 3f;
so.swingTrimSpeedRatio = 0.2f;
so.stabMinStraightness = 0.86f;
so.stabMinForwardDot = 0.55f;
so.stabMinDistance = 0.18f;
so.verboseLog = true;
EditorUtility.SetDirty(so);
AssetDatabase.SaveAssets();
sb.AppendLine(string.Format("변경 후 — useMappedEffects={0} swing='{1}' stab='{2}' drawRibbon={3} burstMode={4} keep=[{5}]",
so.useMappedEffects, so.swingArcEffect, so.stabEffect, so.drawRibbon, so.burstMode, string.Join(",", StabKeep)));
Write(sb);
}
// ─────────────────────────────────────────────────────────────────────────
/// <summary>Play 중 런타임이 실측해 모아 둔 캘리브레이션을 SO 에 굽는다(첫 스윙부터 정확해진다).</summary>
public static void BakeCalibrations()
{
var sb = new StringBuilder();
sb.AppendLine("# WL #792 BakeCalibrations " + System.DateTime.Now.ToString("HH:mm:ss"));
var so = AssetDatabase.LoadAssetAtPath<SlashTrailSettings>(SettingsPath);
if (so == null) { sb.AppendLine("!! 설정 에셋 없음"); Write(sb); return; }
var rt = WeaponTrailDriver.RuntimeCalibrations;
sb.AppendLine("런타임 캐시 " + rt.Count + "행");
int added = 0, updated = 0;
foreach (var kv in rt)
{
var c = kv.Value;
if (c == null) continue;
var exist = so.FindCalibration(c.classId, c.clipName);
// 이미 구워 둔 행보다 품질(궤적 길이 × 원피팅 정합도)이 높을 때만 갈아 끼운다.
if (exist == null) { so.swingCalibrations.Add(Clone(c)); added++; }
else if (c.quality > exist.quality) { Copy(c, exist); updated++; }
else { sb.AppendLine(string.Format(" (유지) {0} {1} — 기존 q={2:F2} ≥ 새 q={3:F2}",
c.classId, c.clipName, exist.quality, c.quality)); continue; }
sb.AppendLine(string.Format(" {0,-6} {1,-24} {2,-5} sweep={3,6:F1}° tipR={4:F3} hiltR={5:F3} pivot={6} n={7} bis={8} | {9}",
c.classId, c.clipName, c.kind, c.sweepDeg, c.tipRadius, c.hiltRadius,
V(c.pivot), V(c.planeNormal), V(c.bisector), c.source));
}
sb.AppendLine(string.Format("추가 {0} · 갱신 {1} · 총 {2}행", added, updated, so.swingCalibrations.Count));
EditorUtility.SetDirty(so);
AssetDatabase.SaveAssets();
Write(sb);
}
static SwingCalibration Clone(SwingCalibration s)
{
var d = new SwingCalibration();
Copy(s, d);
d.classId = s.classId;
d.clipName = s.clipName;
return d;
}
static void Copy(SwingCalibration s, SwingCalibration d)
{
d.kind = s.kind;
d.planeNormal = s.planeNormal;
d.pivot = s.pivot;
d.bisector = s.bisector;
d.startDir = s.startDir;
d.tipRadius = s.tipRadius;
d.hiltRadius = s.hiltRadius;
d.sweepDeg = s.sweepDeg;
d.stabAxis = s.stabAxis;
d.stabStart = s.stabStart;
d.stabLength = s.stabLength;
d.quality = s.quality;
d.source = s.source;
}
// ─────────────────────────────────────────────────────────────────────────
public static void DumpSettings()
{
var sb = new StringBuilder();
sb.AppendLine("# WL #792 DumpSettings (디스크 대조) " + System.DateTime.Now.ToString("HH:mm:ss"));
var so = AssetDatabase.LoadAssetAtPath<SlashTrailSettings>(SettingsPath);
if (so == null) { sb.AppendLine("!! 없음"); Write(sb); return; }
sb.AppendLine(string.Format("useMappedEffects={0} swing='{1}' stab='{2}' drawRibbon={3} burstMode={4} burstTiming={5}",
so.useMappedEffects, so.swingArcEffect, so.stabEffect, so.drawRibbon, so.burstMode, so.burstTiming));
sb.AppendLine(string.Format("arcRadiusFit={0} arcSizeScale={1} arcPlaneOffset={2} arcRadialOffset={3} arcLifetime={4}",
so.arcRadiusFit, so.arcSizeScale, so.arcPlaneOffset, so.arcRadialOffset, so.arcLifetimeSeconds));
sb.AppendLine(string.Format("stabAnchor={0} stabSizeScale={1} stabFaceCamera={2} stabLifetime={3} keep=[{4}]",
so.stabAnchor, so.stabSizeScale, so.stabFaceCamera, so.stabLifetimeSeconds,
so.stabKeepEmitterNames != null ? string.Join(",", so.stabKeepEmitterNames) : ""));
sb.AppendLine(string.Format("분류 — 칼날회전≥{0}° 또는 칼끝/손잡이≥{1} → 휘두르기 · straight≥{2} minDist={3}m → 찌르기 · 속도트림 {4}",
so.swingMinSweepDegrees, so.swingMinTipHiltRatio, so.stabMinStraightness, so.stabMinDistance, so.swingTrimSpeedRatio));
sb.AppendLine(string.Format("추종 — arcFollowPlayer={0} mappedFollowRotation={1} seconds={2}",
so.arcFollowPlayer, so.mappedFollowRotation, so.arcFollowSeconds));
sb.AppendLine("── effectGeometry " + (so.effectGeometry != null ? so.effectGeometry.Count : 0) + "행");
if (so.effectGeometry != null)
foreach (var g in so.effectGeometry)
sb.AppendLine(string.Format(" {0,-20} kind={1,-5} arcValid={2} n={3} c={4} bis={5} rOut={6:F4} rIn={7:F4} sweep={8:F1}° | stabValid={9} axis={10} start={11} len={12:F4} planeN={13} | {14}",
g.prefabName, g.kind, g.arcValid, V(g.arcNormal), V(g.arcCenter), V(g.arcBisector),
g.arcRadiusOuter, g.arcRadiusInner, g.arcSweepDeg,
g.stabValid, V(g.stabAxis), V(g.stabStart), g.stabLength, V(g.stabPlaneNormal), g.bakedAt));
sb.AppendLine("── swingCalibrations " + (so.swingCalibrations != null ? so.swingCalibrations.Count : 0) + "행");
if (so.swingCalibrations != null)
foreach (var c in so.swingCalibrations)
sb.AppendLine(string.Format(" {0,-6} {1,-24} {2,-5} sweep={3,6:F1}° tipR={4:F3} hiltR={5:F3} n={6} pivot={7} bis={8} stab(axis={9} len={10:F3})",
c.classId, c.clipName, c.kind, c.sweepDeg, c.tipRadius, c.hiltRadius,
V(c.planeNormal), V(c.pivot), V(c.bisector), V(c.stabAxis), c.stabLength));
sb.AppendLine("── 프리팹 존재 여부");
foreach (var p in new[] { DstSwing, DstStab })
{
var a = AssetDatabase.LoadAssetAtPath<GameObject>(p);
sb.AppendLine(string.Format(" {0} = {1}{2}", p, a != null ? "OK" : "없음",
a != null ? " (activeSelf=" + a.activeSelf + ", TurnOff_GO=" + (a.GetComponent<TurnOff_GO>() != null) + ")" : ""));
}
Write(sb);
}
// ─────────────────────────────────────────────────────────────────────────
/// <summary>
/// 3타(Effect_Slash_{CID}_3)를 찌르기로 강제한다 — 찌르기 경로 검증 + PD 선택지 B.
/// 실측상 10101·10501 의 세 클립은 모두 칼날 회전량 164~434° 로 휘두르기로 분류된다.
/// PD 가 3타를 찌르기 연출로 원하면 이 SO 행 하나로 바꾼다 — 코드 변경 없음.
/// </summary>
public static void ForceStab() { SetStabRemap(true); }
public static void UnforceStab() { SetStabRemap(false); }
static void SetStabRemap(bool on)
{
var sb = new StringBuilder();
var so = AssetDatabase.LoadAssetAtPath<SlashTrailSettings>(SettingsPath);
if (so == null) { sb.AppendLine("!! 없음"); Write(sb); return; }
so.effectRemaps.RemoveAll(r => r != null && r.sourcePrefab != null && r.sourcePrefab.EndsWith("_3"));
if (on)
{
foreach (var cid in new[] { 10101, 10102, 10501, 10502 })
so.effectRemaps.Add(new SlashEffectRemap
{
sourcePrefab = "Effect_Slash_" + cid + "_3",
classId = cid,
kind = SwingKind.Stab
});
}
EditorUtility.SetDirty(so);
AssetDatabase.SaveAssets();
sb.AppendLine("#792 3타 찌르기 강제 = " + on + " · remap 행 " + so.effectRemaps.Count);
Write(sb);
}
/// <summary>C8 롤백 — #792 경로만 끈다. 프리팹·베이크 데이터는 남겨 둔다(다시 켜면 그대로 동작).</summary>
public static void Rollback()
{
var sb = new StringBuilder();
var so = AssetDatabase.LoadAssetAtPath<SlashTrailSettings>(SettingsPath);
if (so == null) { sb.AppendLine("!! 없음"); Write(sb); return; }
so.useMappedEffects = false;
so.drawRibbon = true;
so.burstMode = BurstMode.None;
EditorUtility.SetDirty(so);
AssetDatabase.SaveAssets();
sb.AppendLine("롤백 — useMappedEffects=false · drawRibbon=true · burstMode=None (#785 리본 상태로 복귀)");
Write(sb);
}
// ─────────────────────────────────────────────────────────────────────────
static string V(Vector3 v) { return string.Format("({0:F4},{1:F4},{2:F4})", v.x, v.y, v.z); }
static void Write(StringBuilder sb)
{
System.IO.Directory.CreateDirectory(System.IO.Path.GetDirectoryName(Out));
System.IO.File.AppendAllText(Out, sb.ToString() + "\n", new System.Text.UTF8Encoding(false));
Debug.Log(sb.ToString());
}
}