Project_WL/AgentScripts/WL772_Maps.cs

246 lines
14 KiB
C#
Raw Permalink 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
// WL772_Maps.cs — PD 지시 #772 : LMHPOLY _Demo Scenes/Demo_02~Demo_10 을 사용 가능한 맵으로 일괄 생성
// 씬 실측 : unity command run_script --file AgentScripts/WL772_Maps.cs --entry WL772_Maps.ScanAll --timeout 600
// 1개 실측 : ... --entry WL772_Maps.Scan --args '["Assets/LMHPOLY/Low Poly Nature Bundle/_Demo Scenes/Demo_02.unity", 100.0]'
// 빌드 파라미터 출력(JSON) : ... --entry WL772_Maps.PlanAll --timeout 900
//
// 측정 전용 — 씬을 열기만 하고 ClearDirty 로 저장하지 않는다. 에셋 변경 0.
// 실제 맵 생성은 WL_StartMapBuilder.RunFor / Rebake, WL_MonsterSpawnerBuilder.RunFor,
// WL_NatureFinish.Lighting/Setup/Instancing 을 이 스캔 결과 수치로 호출해서 수행한다.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using UnityEngine;
#if UNITY_EDITOR
using UnityEditor;
using UnityEditor.SceneManagement;
#endif
public static class WL772_Maps
{
const string SceneDir = "Assets/LMHPOLY/Low Poly Nature Bundle/_Demo Scenes";
const string Out = "AgentScripts/staging/WL_Maps/out";
#if UNITY_EDITOR
static void W(string f, string s) { System.IO.Directory.CreateDirectory(Out); System.IO.File.WriteAllText(System.IO.Path.Combine(Out, f), s, new System.Text.UTF8Encoding(true)); }
public class Plan
{
public string scene, name;
public float minX, maxX, minZ, maxZ, minY, maxY;
public int prefabInstances, renderers, waterRenderers;
public float waterY = float.NaN;
public float startX, startY, startZ, centerX, centerZ, half, camYaw;
public float dWater = -1f, walkPct;
public string skybox = "", fog = "", sun = "", ambient = "";
public string note = "";
}
// ── 씬 1개 실측 + 시작 지점/영역 선정 ───────────────────────────────────
public static object Scan(string scenePath, float half)
{
var p = ScanCore(scenePath, half);
return Describe(p);
}
public static object ScanAll()
{
var sb = new StringBuilder();
var plans = new List<Plan>();
for (int i = 2; i <= 10; i++)
{
var sp = string.Format("{0}/Demo_{1:00}.unity", SceneDir, i);
var p = ScanCore(sp, 100f);
plans.Add(p);
sb.AppendLine(Describe(p));
sb.AppendLine();
}
// 표
sb.AppendLine("| 씬 | XZ 범위 | 높이 | 수면 y | 프리팹 인스턴스 | 렌더러 | 시작 지점 | 영역 중심 ±반폭 | 수면거리 | 보행가능 | camYaw |");
sb.AppendLine("|---|---|---|---|---|---|---|---|---|---|---|");
foreach (var p in plans)
sb.AppendLine(string.Format("| {0} | {1:F0}~{2:F0} x {3:F0}~{4:F0} | {5:F1}~{6:F1} | {7} | {8} | {9} | ({10:F1}, {11:F2}, {12:F1}) | ({13:F0}, {14:F0}) ±{15:F0} | {16:F1} | {17:F1}% | {18:F0} |",
p.name, p.minX, p.maxX, p.minZ, p.maxZ, p.minY, p.maxY,
float.IsNaN(p.waterY) ? "없음" : p.waterY.ToString("F2"),
p.prefabInstances, p.renderers, p.startX, p.startY, p.startZ, p.centerX, p.centerZ, p.half,
p.dWater, p.walkPct, p.camYaw));
var s = sb.ToString();
W("M1_scan.txt", s);
return s;
}
// 빌드에 필요한 인자만 CSV 로 (셸에서 파싱해 순차 호출)
public static object PlanAll()
{
var sb = new StringBuilder();
sb.AppendLine("idx,scene,startX,startY,startZ,camYaw,centerX,centerZ,half,navCY,navSY,waterY");
for (int i = 2; i <= 10; i++)
{
var sp = string.Format("{0}/Demo_{1:00}.unity", SceneDir, i);
var p = ScanCore(sp, 100f);
float navCY = (p.minY + p.maxY) * 0.5f;
float navSY = Mathf.Max(60f, (p.maxY - p.minY) + 40f);
sb.AppendLine(string.Format("{0},{1},{2:F3},{3:F3},{4:F3},{5:F1},{6:F1},{7:F1},{8:F1},{9:F2},{10:F1},{11}",
i, sp, p.startX, p.startY, p.startZ, p.camYaw, p.centerX, p.centerZ, p.half, navCY, navSY,
float.IsNaN(p.waterY) ? "nan" : p.waterY.ToString("F3")));
}
var s = sb.ToString();
W("M1_plan.csv", s);
return s;
}
static string Describe(Plan p)
{
return string.Format(
"## {0}\n 경로 {1}\n XZ {2:F1}~{3:F1} x {4:F1}~{5:F1} 높이 {6:F2}~{7:F2}\n 프리팹인스턴스 {8} · 렌더러 {9} · 수면렌더러 {10} · 수면y {11}\n 시작 ({12:F2}, {13:F2}, {14:F2}) camYaw {15:F0} · 영역중심 ({16:F1}, {17:F1}) ±{18:F0} · 수면거리 {19:F1} · 보행가능 {20:F1}%\n 스카이박스 {21}\n 안개 {22}\n 태양 {23}\n 앰비언트 {24}\n 비고 {25}",
p.name, p.scene, p.minX, p.maxX, p.minZ, p.maxZ, p.minY, p.maxY,
p.prefabInstances, p.renderers, p.waterRenderers, float.IsNaN(p.waterY) ? "없음" : p.waterY.ToString("F3"),
p.startX, p.startY, p.startZ, p.camYaw, p.centerX, p.centerZ, p.half, p.dWater, p.walkPct,
p.skybox, p.fog, p.sun, p.ambient, p.note);
}
static Plan ScanCore(string scenePath, float half)
{
var p = new Plan { scene = scenePath, name = System.IO.Path.GetFileNameWithoutExtension(scenePath), half = half };
var setup = EditorSceneManager.GetSceneManagerSetup();
var sc = EditorSceneManager.OpenScene(scenePath, OpenSceneMode.Single);
try
{
Physics.SyncTransforms();
var roots = sc.GetRootGameObjects();
var rends = new List<MeshRenderer>();
int pfi = 0;
foreach (var r in roots)
{
rends.AddRange(r.GetComponentsInChildren<MeshRenderer>(false));
foreach (var t in r.GetComponentsInChildren<Transform>(true))
if (PrefabUtility.IsAnyPrefabInstanceRoot(t.gameObject)) pfi++;
}
p.renderers = rends.Count; p.prefabInstances = pfi;
// 지형(콜라이더) 범위 = 실제로 서 있을 수 있는 곳
var cols = new List<Collider>();
foreach (var r in roots) cols.AddRange(r.GetComponentsInChildren<Collider>(false));
Bounds tb;
if (cols.Count > 0) { tb = cols[0].bounds; foreach (var c in cols) tb.Encapsulate(c.bounds); }
else if (rends.Count > 0) { tb = rends[0].bounds; foreach (var r in rends) tb.Encapsulate(r.bounds); }
else tb = new Bounds(Vector3.zero, Vector3.one * 100f);
p.minX = tb.min.x; p.maxX = tb.max.x; p.minZ = tb.min.z; p.maxZ = tb.max.z; p.minY = tb.min.y; p.maxY = tb.max.y;
// 수면
var waters = rends.Where(r => r.name.IndexOf("water", StringComparison.OrdinalIgnoreCase) >= 0
|| (r.sharedMaterial != null && r.sharedMaterial.name.IndexOf("water", StringComparison.OrdinalIgnoreCase) >= 0)).ToList();
p.waterRenderers = waters.Count;
var waterBounds = new List<Bounds>();
if (waters.Count > 0)
{
p.waterY = waters.Max(w => w.bounds.max.y);
foreach (var w in waters) waterBounds.Add(w.bounds);
}
// 환경
p.skybox = RenderSettings.skybox != null ? RenderSettings.skybox.name : "(none)";
p.fog = string.Format("enabled={0} mode={1} color={2} start={3:F1} end={4:F1} density={5:F4}",
RenderSettings.fog, RenderSettings.fogMode, ColorStr(RenderSettings.fogColor),
RenderSettings.fogStartDistance, RenderSettings.fogEndDistance, RenderSettings.fogDensity);
var lights = UnityEngine.Object.FindObjectsByType<Light>(FindObjectsInactive.Exclude, FindObjectsSortMode.None)
.Where(l => l.type == LightType.Directional).ToArray();
p.sun = lights.Length == 0 ? "(directional 없음)" : string.Join(" / ", lights.Select(l =>
string.Format("{0} rot({1:F2},{2:F2}) {3} i={4:F2} {5}", l.name, l.transform.eulerAngles.x, l.transform.eulerAngles.y, ColorStr(l.color), l.intensity, l.shadows)));
p.ambient = string.Format("mode={0} sky={1} intensity={2:F2}", RenderSettings.ambientMode, ColorStr(RenderSettings.ambientLight), RenderSettings.ambientIntensity);
// ── 시작 지점 선정 ────────────────────────────────────────────
// 조건: 경사 <= 15° · 수면 위(+0.5 m) · 반경 10 m 개활지 단차 <= 1.5 m · 지형 가장자리에서 여유
float pad = Mathf.Min(half * 0.5f, Mathf.Min(tb.size.x, tb.size.z) * 0.35f);
float x0 = tb.min.x + pad, x1 = tb.max.x - pad, z0 = tb.min.z + pad, z1 = tb.max.z - pad;
float step = Mathf.Max(4f, Mathf.Min(tb.size.x, tb.size.z) / 40f);
Vector3 best = Vector3.zero; float bestScore = float.NegativeInfinity, bestDW = -1f; bool found = false;
for (float x = x0; x <= x1; x += step)
for (float z = z0; z <= z1; z += step)
{
RaycastHit h;
if (!Physics.Raycast(new Vector3(x, tb.max.y + 50f, z), Vector3.down, out h, tb.size.y + 200f)) continue;
if (!float.IsNaN(p.waterY) && h.point.y < p.waterY + 0.5f) continue;
float slope = Vector3.Angle(h.normal, Vector3.up);
if (slope > 15f) continue;
// 반경 10 m 평탄도
float lo = h.point.y, hi = h.point.y; bool ok = true;
for (int a = 0; a < 8 && ok; a++)
{
float ang = a * Mathf.PI / 4f;
var q = new Vector3(x + Mathf.Cos(ang) * 10f, tb.max.y + 50f, z + Mathf.Sin(ang) * 10f);
RaycastHit h2;
if (!Physics.Raycast(q, Vector3.down, out h2, tb.size.y + 200f)) { ok = false; break; }
lo = Mathf.Min(lo, h2.point.y); hi = Mathf.Max(hi, h2.point.y);
}
if (!ok || hi - lo > 1.5f) continue;
// 수면까지 거리
float dw = float.MaxValue;
foreach (var wb in waterBounds)
{
var cp = wb.ClosestPoint(new Vector3(x, wb.center.y, z));
dw = Mathf.Min(dw, Vector2.Distance(new Vector2(x, z), new Vector2(cp.x, cp.z)));
}
if (waterBounds.Count == 0) dw = -1f;
// 점수: 평탄할수록 · 수면 6~40 m 근접 우선 · 지형 중앙 근접
float score = -(hi - lo) * 4f - slope * 0.2f;
if (dw >= 0f) score += (dw >= 5f && dw <= 45f) ? 20f - Mathf.Abs(dw - 20f) * 0.3f : -Mathf.Min(20f, Mathf.Abs(dw - 20f) * 0.2f);
score -= Vector2.Distance(new Vector2(x, z), new Vector2(tb.center.x, tb.center.z)) * 0.05f;
if (score > bestScore) { bestScore = score; best = h.point; bestDW = dw; found = true; }
}
if (!found)
{
RaycastHit hc;
if (Physics.Raycast(new Vector3(tb.center.x, tb.max.y + 50f, tb.center.z), Vector3.down, out hc, tb.size.y + 200f)) best = hc.point;
else best = new Vector3(tb.center.x, 0f, tb.center.z);
p.note += "적합 후보 없음 → 지형 중심 채택. ";
}
p.startX = best.x; p.startY = best.y; p.startZ = best.z; p.dWater = bestDW;
// 영역 반폭 = 지형에 맞춰 축소 (최대 100)
float maxHalf = Mathf.Min(100f, Mathf.Min(tb.size.x, tb.size.z) * 0.45f);
p.half = Mathf.Max(40f, maxHalf);
// 영역 중심 = 시작 지점을 지형 안으로 클램프
p.centerX = Mathf.Clamp(best.x, tb.min.x + p.half, tb.max.x - p.half);
p.centerZ = Mathf.Clamp(best.z, tb.min.z + p.half, tb.max.z - p.half);
if (tb.size.x < p.half * 2f) p.centerX = tb.center.x;
if (tb.size.z < p.half * 2f) p.centerZ = tb.center.z;
// camYaw = 수면 방향(없으면 영역 중심 방향)
Vector3 look;
if (waterBounds.Count > 0)
{
var nearest = waterBounds.OrderBy(b => Vector2.Distance(new Vector2(best.x, best.z), new Vector2(b.center.x, b.center.z))).First();
look = new Vector3(nearest.center.x - best.x, 0f, nearest.center.z - best.z);
}
else look = new Vector3(p.centerX - best.x, 0f, p.centerZ - best.z);
if (look.sqrMagnitude < 1f) look = Vector3.forward;
p.camYaw = Mathf.Repeat(Quaternion.LookRotation(look).eulerAngles.y, 360f);
// 영역 보행가능 비율(경사 45° 이하 · 수면 위)
int tot = 0, walk = 0;
for (float x = p.centerX - p.half; x <= p.centerX + p.half; x += 5f)
for (float z = p.centerZ - p.half; z <= p.centerZ + p.half; z += 5f)
{
tot++;
RaycastHit h;
if (!Physics.Raycast(new Vector3(x, tb.max.y + 50f, z), Vector3.down, out h, tb.size.y + 200f)) continue;
if (!float.IsNaN(p.waterY) && h.point.y < p.waterY) continue;
if (Vector3.Angle(h.normal, Vector3.up) > 45f) continue;
walk++;
}
p.walkPct = 100f * walk / Mathf.Max(1, tot);
}
finally
{
// 저장 없이 원상복귀 — 씬을 열기만 했으므로 dirty 를 만들지 않았다
if (setup != null && setup.Length > 0) EditorSceneManager.RestoreSceneManagerSetup(setup);
}
return p;
}
static string ColorStr(Color c) { return string.Format("RGBA({0:F3},{1:F3},{2:F3},{3:F3})", c.r, c.g, c.b, c.a); }
#endif
}