Project_WL/AgentScripts/WL778_Seq.cs

345 lines
20 KiB
C#
Raw Normal View History

WL 리뉴얼 세션 2026-09-06~07 (#760~#800) — 전투·배경·UI·타격감 (조직 PD 로그 #746~#800 · 대화로그 §14~§24) - 전투: Knight@Attack1~3_S 3콤보(4 컨트롤러·클래스별 서브클립) · 타겟팅 4규칙(정면 우선·재타겟·즉시 공격·어그로 금지 · WLTargetingSettings) · 대쉬 후 공격(Stander@Chase_Start · DashDriver · 5 m · 사정거리+0.8 · 근거리 적 우선) · 공격 이동 FrameTable(발 접지 실측 표 · AttackRootMotion) · 충돌 반경/투사체 0.3 배(WLCollisionTuning) · 무적(임시)·펫 금지(WLGameplaySettings) - 검기: NamuFX Slash_B 배리언트 Effect_WLSwingArc 원 피팅 정합 배치 + 캘리브레이션(SlashArcMeasure · SlashTrailSettings) · 찌르기 Effect_WLStab 대기 · 램프 리본(BladeTrail · WL_BladeRibbon.shader · T_WL_BladeRibbonRamp) 보존(drawRibbon 0) - 타격감: Assets/WL/Feel(WLHitFeel · 히트스톱 0.03 · 셰이크 0.10 m · 몹 펀치 1.12 · Actor.Get_Damage 훅 1줄 · 원본 RealCamera 셰이크 결함 대체) - 배경/맵: LMHPOLY Demo_01~10 → WL_Nature01~10(프리팹·씬·NavMesh·스포너·BattleMapConfig) · 물(ToonWaterU) · 포스트 블룸 0.9/0.3 · 잔디(BruteForce·드레싱) 제거 · 마젠타 머티리얼 URP 변환 - UI: 세로 HUD(WL_HUD · 하단 5메뉴 폭 전체 · 채팅/전투 패드 숨김 · WLIngameUiOverride) · Title/TitleInfo 1080×1920 Expand + 배경 높이 fit(WLBackgroundFit) · 로딩 SortOrder_5(WLRawImageAspectSync · 초점표 8장) · Loading1~8 ASTC 6×6 - 도구: AgentScripts/*(LightProbe · WL_MapSwitch · WL760~WL800 프로브/집행/검증 · 상단 사용법 주석) · 에디터 락 프로토콜 파일(staging) - 제외(별도 커밋 예정): Assets/LMHPOLY(703 MB) · Assets/Feel(422 MB) · Assets/Shinabro(300 MB) — 에셋 스토어/구 WL 팩 원본 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-06 23:24:25 +00:00
// WL778_Seq.cs — PD 지시 #777 + #778 런타임 실측 (Play 중 · Assets 무수정 · 서버 쓰기 없음)
// unity command run_script --file AgentScripts/WL778_Seq.cs --entry WL778_Seq.RunAt --args '[10501, "before_10501", 1.5, 1, 4.5]'
// unity command run_script --file AgentScripts/WL778_Seq.cs --entry WL778_Seq.Report --args '["before_10501"]'
// unity command run_script --file AgentScripts/WL778_Seq.cs --entry WL778_Seq.SetFollow --args '[1, 0, 0]'
// unity command run_script --file AgentScripts/WL778_Seq.cs --entry WL778_Seq.Cfg
//
// #777 : 슬래시 이펙트 인스턴스의 월드 위치 vs PC 위치 vs 검(자루·검끝) 위치를 매 프레임 기록해
// 「스폰 후 PC 이동량」과 「이펙트가 무기에서 벌어진 거리」를 수치로 낸다.
// #778 : Animator 상태 진입/전이 시각 · 타격(몹 HP 감소) 시각 · 스윙(ShowEffect) 시각을 기록해
// 타격 간 간격과 콤보 사이클 길이를 낸다.
// 결과 = Screenshots_WL/combat4/seq_<tag>.txt + 캡처 PNG (모두 Assets 밖).
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text;
using UnityEngine;
using UnityEngine.AI;
public static class WL778_Seq
{
const string OutDir = "Screenshots_WL/combat4";
const BindingFlags BF = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance;
public static object RunAt(int classId, string tag, float dist, int noPet, float seconds)
{
if (!Application.isPlaying) return "not playing";
var me = UnityEngine.Object.FindObjectsByType<MyActor>(FindObjectsInactive.Exclude, FindObjectsSortMode.None).FirstOrDefault();
if (me == null) return "MyActor none";
if (GameObject.Find("__wl778seq") != null) return "sequence already running";
var host = new GameObject("__wl778seq").AddComponent<SeqHost>();
host.StartCoroutine(host.Co(me, classId, tag, dist, noPet != 0, seconds <= 0f ? 4.5f : seconds));
return "started " + tag + " class=" + classId + " dist=" + dist + " noPet=" + noPet + " sec=" + seconds;
}
public static object Report(string tag)
{
var p = System.IO.Path.Combine(OutDir, "seq_" + tag + ".txt");
return System.IO.File.Exists(p) ? System.IO.File.ReadAllText(p) : "(no report yet) " + p;
}
/// <summary>#777 추종 설정을 런타임에 바꾼다(메모리만 · 디스크 저장 없음 · a/b 비교용).</summary>
public static object SetFollow(int follow, int followRotation, float seconds)
{
var st = WL.Combat.SlashTrailSettings.Instance;
if (st == null) return "settings null";
st.arcFollowPlayer = follow != 0;
st.arcFollowRotation = followRotation != 0;
st.arcFollowSeconds = seconds;
return Cfg();
}
public static object Cfg()
{
var st = WL.Combat.SlashTrailSettings.Instance;
var mo = WL.Combat.WLCombatMotionSettings.Instance;
var sb = new StringBuilder();
sb.AppendLine(st == null ? "SlashTrailSettings=null"
: string.Format("Slash: enable={0} burstMode={1} timing={2} suppress={3} drawRibbon={4} autoOrient={5} fit={6} scale={7} anchor={8} | FOLLOW player={9} rot={10} sec={11}",
st.enableWeaponTrail, st.burstMode, st.burstTiming, st.suppressMode, st.drawRibbon,
st.arcAutoOrient, st.arcFitToBlade, st.arcScale, st.arcAnchor,
st.arcFollowPlayer, st.arcFollowRotation, st.arcFollowSeconds));
sb.AppendLine(mo == null ? "WLCombatMotionSettings=null"
: string.Format("Motion: enable={0} sec={1} scale={2} max={3} easeOut={4} stopDist={5}",
mo.enableAttackStep, mo.attackStepSeconds, mo.attackStepScale, mo.attackStepMaxDistance,
mo.attackStepEaseOut, mo.attackStepStopDistance));
sb.AppendLine("follower: attaches=" + WL.Combat.SlashArcFollower.AttachCount + " last=" + (WL.Combat.SlashArcFollower.LastInfo ?? "(none)"));
sb.AppendLine("lastArc=" + (WL.Combat.WeaponTrailDriver.LastArcInfo ?? "(none)"));
return sb.ToString();
}
/// <summary>스폰된 슬래시 인스턴스의 파티클 시뮬레이션 공간을 덤프한다(#777 잔상 위험 실측).</summary>
public static object SimSpace()
{
var sb = new StringBuilder();
foreach (var m in UnityEngine.Object.FindObjectsByType<WL.Combat.SlashCrescentMask>(FindObjectsInactive.Include, FindObjectsSortMode.None))
{
sb.Append(m.gameObject.name).Append(" active=").Append(m.gameObject.activeInHierarchy).Append(" :: ");
foreach (var ps in m.GetComponentsInChildren<ParticleSystem>(true))
sb.Append(ps.gameObject.name).Append('=').Append(ps.main.simulationSpace).Append(' ');
sb.AppendLine();
}
return sb.Length == 0 ? "(스폰된 슬래시 인스턴스 없음 — 공격 후 다시 호출)" : sb.ToString();
}
class SeqHost : MonoBehaviour
{
static double HP(Actor a)
{
var mi = typeof(Actor).GetMethod("Get_HP", BF);
if (mi == null || a == null) return double.NaN;
try { return Convert.ToDouble(mi.Invoke(a, null)); } catch { return double.NaN; }
}
static void SuppressPets(bool suppress)
{
foreach (var p in UnityEngine.Object.FindObjectsByType<PetActor>(FindObjectsInactive.Include, FindObjectsSortMode.None))
if (p.gameObject.activeSelf == suppress) p.gameObject.SetActive(!suppress);
}
public IEnumerator Co(MyActor me, int classId, string tag, float dist, bool noPet, float seconds)
{
var sb = new StringBuilder();
System.IO.Directory.CreateDirectory(OutDir);
if (noPet) SuppressPets(true);
var pc = me as PCActor;
if (classId > 0 && pc != null)
{
var data = table_classconfig.Ins.Get_Data_orNull(classId);
if (data == null) { sb.AppendLine("class data null " + classId); Done(tag, sb); yield break; }
pc.Change_Class(data, true);
yield return new WaitForSeconds(1.5f);
}
var anim = me.GetComponentInChildren<Animator>();
sb.AppendLine("class=" + classId + " ctrl=" + (anim != null && anim.runtimeAnimatorController != null ? anim.runtimeAnimatorController.name : "(null)")
+ " scene=" + UnityEngine.SceneManagement.SceneManager.GetActiveScene().name
+ " fixedDt=" + Time.fixedDeltaTime.ToString("F4"));
sb.Append(Cfg());
var mobs = UnityEngine.Object.FindObjectsByType<MobActor>(FindObjectsInactive.Exclude, FindObjectsSortMode.None)
.Where(m => HP(m) > 0).OrderBy(m => Vector3.Distance(m.transform.position, me.transform.position)).ToArray();
if (mobs.Length == 0) { sb.AppendLine("no live MobActor"); Done(tag, sb); yield break; }
var t = mobs[0];
var dir = t.transform.position - me.transform.position; dir.y = 0f;
if (dir.sqrMagnitude > 1e-4f && Mathf.Abs(dir.magnitude - dist) > 0.1f)
{
var pos = t.transform.position - dir.normalized * dist;
var na = me.GetComponent<NavMeshAgent>();
if (na != null && na.enabled && na.isOnNavMesh) na.Warp(pos); else me.transform.position = pos;
}
me.transform.LookAt(new Vector3(t.transform.position.x, me.transform.position.y, t.transform.position.z));
var setT = typeof(Actor).GetMethod("Change_Target", BF);
if (setT != null) { try { setT.Invoke(me, new object[] { false, (Actor)t, false }); } catch { } }
yield return null;
var watch = UnityEngine.Object.FindObjectsByType<MobActor>(FindObjectsInactive.Exclude, FindObjectsSortMode.None)
.Where(m => HP(m) > 0 && Vector3.Distance(m.transform.position, me.transform.position) < 8f).ToList();
var lastHpAll = watch.ToDictionary(m => m, m => HP(m));
Vector3 pos0 = me.transform.position;
sb.AppendLine("target=" + t.name + " hp0=" + HP(t).ToString("F0")
+ " d=" + Vector3.Distance(pos0, t.transform.position).ToString("F2")
+ " pcPos0=" + pos0.ToString("F2") + " watched=" + watch.Count);
double aspd = 1.0;
try
{
var fi = typeof(Actor).GetField("m_Stat", BF);
var stat = fi != null ? fi.GetValue(me) : null;
var gm = stat != null ? stat.GetType().GetMethod("Get_Stat", new[] { typeof(eStat) }) : null;
if (gm != null) aspd = Convert.ToDouble(gm.Invoke(stat, new object[] { eStat.FinalAttackSpeed }));
}
catch { }
var drv = me.GetComponent<WL.Combat.WeaponTrailDriver>();
me.Play_Attack(0, (float)aspd);
sb.AppendLine("Play_Attack(0," + aspd.ToString("F2") + ") animSpeed=" + (anim != null ? anim.speed : 0f).ToString("F2"));
float t0 = Time.time;
int lastStateHash = 0; bool lastInTr = false;
int lastSwing = drv != null ? drv.SwingCount : 0;
int lastStep = WL.Combat.AttackStepDriver.StepCount;
int lastAttach = WL.Combat.SlashArcFollower.AttachCount;
string lastArc = WL.Combat.WeaponTrailDriver.LastArcInfo;
var stateLog = new List<string>(); var hitLog = new List<string>();
var swingLog = new List<string>(); var stepLog = new List<string>();
var fxLog = new List<string>(); var capLog = new List<string>();
// #777 — 추적 중인 이펙트 인스턴스
Transform fx = null; Vector3 fxSpawnPos = Vector3.zero, pcAtSpawn = Vector3.zero;
float fxSpawnT = -1f, fxMaxPcMove = 0f, fxMaxDrift = 0f, fxMaxHiltDist = 0f, fxMinHiltDist = 999f;
int fxSamples = 0; string fxName = "";
var fxRows = new List<string>();
var fxSummaries = new List<string>();
float capNext = -1f; int caps = 0; float capInterval = 0.1f; int capMax = 8;
var hitTimes = new List<float>();
while (Time.time - t0 < seconds)
{
if (noPet) SuppressPets(true);
float el = Time.time - t0;
// ── #778 상태·전이
if (anim != null && anim.layerCount > 0)
{
var cur = anim.GetCurrentAnimatorStateInfo(0);
bool inTr = anim.IsInTransition(0);
if (cur.fullPathHash != lastStateHash)
{
lastStateHash = cur.fullPathHash;
string cn = "(?)";
var ci = anim.GetCurrentAnimatorClipInfo(0);
if (ci.Length > 0 && ci[0].clip != null) cn = ci[0].clip.name + " len=" + ci[0].clip.length.ToString("F3");
stateLog.Add(string.Format("{0:F3}s STATE {1} n={2:F3}", el, cn, cur.normalizedTime));
}
if (inTr != lastInTr)
{
lastInTr = inTr;
if (inTr)
{
var ti = anim.GetAnimatorTransitionInfo(0);
var nx = anim.GetNextAnimatorStateInfo(0);
string nn = "(?)";
var nci = anim.GetNextAnimatorClipInfo(0);
if (nci.Length > 0 && nci[0].clip != null) nn = nci[0].clip.name;
stateLog.Add(string.Format("{0:F3}s TRANS-BEGIN dur={1:F3}s -> {2} (src n={3:F3})",
el, ti.duration, nn, cur.normalizedTime));
}
else stateLog.Add(string.Format("{0:F3}s TRANS-END (now n={1:F3})", el, cur.normalizedTime));
}
}
// ── 스윙(ShowEffect) · 전진(Move) · 추종 부착
if (drv != null && drv.SwingCount != lastSwing)
{
lastSwing = drv.SwingCount;
swingLog.Add(string.Format("{0:F3}s SWING#{1}", el, lastSwing));
}
if (WL.Combat.AttackStepDriver.StepCount != lastStep)
{
lastStep = WL.Combat.AttackStepDriver.StepCount;
stepLog.Add(string.Format("{0:F3}s STEP#{1} planned={2:F2}m", el, lastStep, WL.Combat.AttackStepDriver.LastPlannedDistance));
}
// ── #777 이펙트 스폰 감지 → 추적 시작
string arcNow = WL.Combat.WeaponTrailDriver.LastArcInfo;
bool spawned = (arcNow != lastArc) || (WL.Combat.SlashArcFollower.AttachCount != lastAttach);
if (spawned)
{
lastArc = arcNow; lastAttach = WL.Combat.SlashArcFollower.AttachCount;
if (fx != null) fxSummaries.Add(FxSummary(fxName, fxSpawnT, fxSamples, fxMaxPcMove, fxMaxDrift, fxMinHiltDist, fxMaxHiltDist));
var mask = WL.Combat.WeaponTrailDriver.LastMask;
fx = mask != null ? mask.transform : null;
fxName = mask != null ? mask.gameObject.name : "(none)";
fxSpawnT = el; fxSpawnPos = fx != null ? fx.position : Vector3.zero; pcAtSpawn = me.transform.position;
fxMaxPcMove = 0f; fxMaxDrift = 0f; fxMaxHiltDist = 0f; fxMinHiltDist = 999f; fxSamples = 0;
fxLog.Add(string.Format("{0:F3}s SPAWN {1} fxPos={2} pcPos={3} follower={4} | {5}",
el, fxName, fxSpawnPos.ToString("F2"), pcAtSpawn.ToString("F2"),
fx != null && fx.GetComponent<WL.Combat.SlashArcFollower>() != null
&& fx.GetComponent<WL.Combat.SlashArcFollower>().IsFollowing, arcNow));
if (capNext < 0f) capNext = el; // 첫 스폰부터 캡처 시퀀스 시작
}
// ── #777 프레임 시계열
if (fx != null && fx.gameObject.activeInHierarchy)
{
Vector3 pcNow = me.transform.position;
Vector3 fxNow = fx.position;
float pcMove = Vector3.Distance(pcAtSpawn, pcNow);
// 「떨어진 느낌」의 크기 = PC 대비 상대 위치가 스폰 시점에서 얼마나 벌어졌는가
float drift = Vector3.Distance(fxNow - pcNow, fxSpawnPos - pcAtSpawn);
float hiltD = (drv != null && drv.ActiveSlotCount > 0)
? Vector3.Distance(fxNow, drv.GetHiltWorld(0)) : -1f;
if (pcMove > fxMaxPcMove) fxMaxPcMove = pcMove;
if (drift > fxMaxDrift) fxMaxDrift = drift;
if (hiltD >= 0f) { if (hiltD > fxMaxHiltDist) fxMaxHiltDist = hiltD; if (hiltD < fxMinHiltDist) fxMinHiltDist = hiltD; }
fxSamples++;
if (fxRows.Count < 220)
fxRows.Add(string.Format("{0:F3}s [{1}] +{2:F3}s fx={3} pc={4} pcMove={5:F3} drift={6:F3} hiltD={7:F3}",
el, fxName, el - fxSpawnT, fxNow.ToString("F2"), pcNow.ToString("F2"), pcMove, drift, hiltD));
}
else if (fx != null)
{
fxSummaries.Add(FxSummary(fxName, fxSpawnT, fxSamples, fxMaxPcMove, fxMaxDrift, fxMinHiltDist, fxMaxHiltDist));
fxLog.Add(string.Format("{0:F3}s OFF {1} (수명 {2:F3}s)", el, fxName, el - fxSpawnT));
fx = null;
}
// ── 타격
foreach (var m in watch)
{
if (m == null) continue;
double hp = HP(m), prev = lastHpAll[m];
if (Math.Abs(hp - prev) > 0.01)
{
hitLog.Add(string.Format("{0:F3}s HIT {1} {2:F0} -> {3:F0}", el, m.name, prev, hp));
hitTimes.Add(el);
lastHpAll[m] = hp;
}
}
// ── 캡처 시퀀스 (0.1 s 간격)
if (capNext >= 0f && caps < capMax && el >= capNext)
{
capNext = el + capInterval;
yield return new WaitForEndOfFrame();
Texture2D tex = null;
try { tex = ScreenCapture.CaptureScreenshotAsTexture(); } catch { }
if (tex != null)
{
var path = System.IO.Path.Combine(OutDir, string.Format("seq_{0}_{1:00}_{2:F2}s.png", tag, ++caps, el));
System.IO.File.WriteAllBytes(path, tex.EncodeToPNG());
UnityEngine.Object.Destroy(tex);
capLog.Add(string.Format("{0:F3}s CAP {1}", el, path));
}
continue;
}
// 🔴 반드시 프레임 끝에서 샘플링한다 — SlashArcFollower 는 LateUpdate 에서
// 좌표를 갱신하므로 Update 단계(yield return null)에서 읽으면 이펙트 위치가
// 한 프레임 뒤처져 보이고, 그 차이가 그 프레임의 PC 이동량으로 잘못 잡힌다.
yield return new WaitForEndOfFrame();
}
if (fx != null) fxSummaries.Add(FxSummary(fxName, fxSpawnT, fxSamples, fxMaxPcMove, fxMaxDrift, fxMinHiltDist, fxMaxHiltDist));
sb.AppendLine("== #778 states/transitions (" + stateLog.Count + ") =="); foreach (var s in stateLog) sb.AppendLine(" " + s);
sb.AppendLine("== swings ShowEffect (" + swingLog.Count + ") =="); foreach (var s in swingLog) sb.AppendLine(" " + s);
sb.AppendLine("== hits (" + hitLog.Count + ") =="); foreach (var s in hitLog) sb.AppendLine(" " + s);
if (hitTimes.Count >= 2)
{
var gaps = new StringBuilder();
for (int i = 1; i < hitTimes.Count; i++) gaps.Append((hitTimes[i] - hitTimes[i - 1]).ToString("F3")).Append("s ");
sb.AppendLine(" hit gaps: " + gaps + " | first->last = " + (hitTimes[hitTimes.Count - 1] - hitTimes[0]).ToString("F3") + "s");
}
sb.AppendLine("== attack steps (" + stepLog.Count + ") =="); foreach (var s in stepLog) sb.AppendLine(" " + s);
sb.AppendLine("== #777 fx events (" + fxLog.Count + ") =="); foreach (var s in fxLog) sb.AppendLine(" " + s);
sb.AppendLine("== #777 fx summary (" + fxSummaries.Count + ") =="); foreach (var s in fxSummaries) sb.AppendLine(" " + s);
sb.AppendLine("== #777 fx timeline (" + fxRows.Count + ") =="); foreach (var s in fxRows) sb.AppendLine(" " + s);
sb.AppendLine("== captures (" + capLog.Count + ") =="); foreach (var s in capLog) sb.AppendLine(" " + s);
Vector3 pend = me.transform.position;
sb.AppendLine("final pcPos=" + pend.ToString("F2") + " totalΔ=" + Vector3.Distance(pos0, pend).ToString("F3") + "m"
+ " swings=" + (drv != null ? drv.SwingCount : 0) + " steps=" + WL.Combat.AttackStepDriver.StepCount
+ " attaches=" + WL.Combat.SlashArcFollower.AttachCount);
if (noPet) SuppressPets(false);
Done(tag, sb);
}
static string FxSummary(string name, float t, int n, float pcMove, float drift, float minH, float maxH)
{
return string.Format("{0} spawn@{1:F3}s frames={2} PC최대이동={3:F3}m 이펙트상대이탈최대={4:F3}m 자루거리 {5:F3}~{6:F3}m",
name, t, n, pcMove, drift, minH > 900f ? 0f : minH, maxH);
}
void Done(string tag, StringBuilder sb)
{
System.IO.File.WriteAllText(System.IO.Path.Combine(OutDir, "seq_" + tag + ".txt"), sb.ToString());
Destroy(gameObject);
}
}
}