From cc2e7ad95e794b48c68e45c89ee6d25d4a458af1 Mon Sep 17 00:00:00 2001 From: Luis Gonzalez Date: Tue, 7 Jul 2026 20:51:18 -0700 Subject: [PATCH] Polishes --- .../Authoring/Combat/EnemyAuthoring.cs | 1 + .../Client/Debug/PixelArtDevControls.cs | 205 ++++++++++++++++++ .../Client/Debug/PixelArtDevControls.cs.meta | 2 + .../Client/Presentation/EnemyMarkerSystem.cs | 184 ++++++++++++++++ .../Presentation/EnemyMarkerSystem.cs.meta | 2 + .../Scripts/Client/Presentation/FeelConfig.cs | 21 ++ .../Client/Presentation/NodeFeedbackSystem.cs | 103 +++++++++ .../Presentation/NodeFeedbackSystem.cs.meta | 2 + .../Presentation/WorldFeedbackSystem.cs | 11 +- .../Client/Presentation/WorldFeelConfig.cs | 27 ++- .../Scripts/Server/Combat/EnemyAISystem.cs | 89 ++++++++ .../Scripts/Server/Combat/EnemyMoveUtil.cs | 69 ++++-- .../Scripts/Simulation/Combat/EnemyAIMath.cs | 8 +- .../Simulation/Combat/EnemyNavState.cs | 28 +++ .../Simulation/Combat/EnemyNavState.cs.meta | 2 + Assets/_Project/Shaders/PixelOutline.mat | 9 +- Assets/_Project/Shaders/PixelOutline.shader | 6 + .../Tests/EditMode/EnemyAIMathTests.cs | 11 +- CLAUDE.md | 2 +- ...Expedition_Enemy_Stuck_Harvest_Feedback.md | 47 ++++ 20 files changed, 794 insertions(+), 35 deletions(-) create mode 100644 Assets/_Project/Scripts/Client/Debug/PixelArtDevControls.cs create mode 100644 Assets/_Project/Scripts/Client/Debug/PixelArtDevControls.cs.meta create mode 100644 Assets/_Project/Scripts/Client/Presentation/EnemyMarkerSystem.cs create mode 100644 Assets/_Project/Scripts/Client/Presentation/EnemyMarkerSystem.cs.meta create mode 100644 Assets/_Project/Scripts/Client/Presentation/NodeFeedbackSystem.cs create mode 100644 Assets/_Project/Scripts/Client/Presentation/NodeFeedbackSystem.cs.meta create mode 100644 Assets/_Project/Scripts/Simulation/Combat/EnemyNavState.cs create mode 100644 Assets/_Project/Scripts/Simulation/Combat/EnemyNavState.cs.meta create mode 100644 Docs/Vault/07_Sessions/2026/2026-07-07_Expedition_Enemy_Stuck_Harvest_Feedback.md diff --git a/Assets/_Project/Scripts/Authoring/Combat/EnemyAuthoring.cs b/Assets/_Project/Scripts/Authoring/Combat/EnemyAuthoring.cs index 901fd6078..c81cbb4b2 100644 --- a/Assets/_Project/Scripts/Authoring/Combat/EnemyAuthoring.cs +++ b/Assets/_Project/Scripts/Authoring/Combat/EnemyAuthoring.cs @@ -51,6 +51,7 @@ namespace ProjectM.Authoring }); AddComponent(entity, new EnemyAttackCooldown { NextAttackTick = 0 }); AddComponent(entity); // server-only recoil state (zero = not knocked) + AddComponent(entity, new EnemyNavState { LastPos = float.MaxValue }); // server-only anti-stuck nav state (not replicated); sentinel LastPos forces a first-tick reset AddComponent(entity); // replicated telegraph signal (zero = not winding up) // Slice 1 (Feature C): client-safe baked telegraph metadata. EnemyBaker is the SOLE writer of // EnemyTelegraph even on a Charger (the prefab composes both authorings on one entity); reading the diff --git a/Assets/_Project/Scripts/Client/Debug/PixelArtDevControls.cs b/Assets/_Project/Scripts/Client/Debug/PixelArtDevControls.cs new file mode 100644 index 000000000..7ff1da357 --- /dev/null +++ b/Assets/_Project/Scripts/Client/Debug/PixelArtDevControls.cs @@ -0,0 +1,205 @@ +#if UNITY_EDITOR +using UnityEngine; +using UnityEditor; + +namespace ProjectM.Client +{ + /// + /// EDITOR-ONLY live tuner for the "3D pixel art" full-screen render style — the + /// Hidden/ProjectM/PixelOutline shader driven by PixelOutline.mat via the + /// PC_Renderer FullScreenPassRendererFeature. Self-spawns on Play (no scene wiring), + /// so it works in Game.unity and DevSandbox.unity alike. Toggle the panel with F3. + /// + /// It writes straight to the shared material ASSET, so every drag applies to the running + /// game instantly AND sticks after you exit Play mode (material assets aren't reverted the + /// way scene objects are). "Save Asset" force-writes to disk now; "Reset" restores the + /// shader's shipped defaults. Stripped from player builds (#if UNITY_EDITOR). + /// + public class PixelArtDevControls : MonoBehaviour + { + const string MaterialPath = "Assets/_Project/Shaders/PixelOutline.mat"; + + static PixelArtDevControls _instance; + + [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.AfterSceneLoad)] + static void Bootstrap() + { + if (_instance != null) + return; + var go = new GameObject("~PixelArtDevControls") { hideFlags = HideFlags.HideAndDontSave }; + DontDestroyOnLoad(go); + _instance = go.AddComponent(); + } + + Material _mat; + bool _open; + Vector2 _scroll; + GUIStyle _wrap; + + Material Mat + { + get + { + if (_mat == null) + _mat = AssetDatabase.LoadAssetAtPath(MaterialPath); + return _mat; + } + } + + GUIStyle Wrap => _wrap ??= new GUIStyle(GUI.skin.label) { wordWrap = true, fontSize = 10 }; + + void OnDisable() => AimPresentation.ForceCursorVisible = false; + + void Update() + { + if (Input.GetKeyDown(KeyCode.F3)) + _open = !_open; + } + + void OnGUI() + { + if (GUI.Button(new Rect(10, 10, 128, 24), _open ? "PIXEL ▲ (F3)" : "PIXEL ▼ (F3)")) + _open = !_open; + + if (!_open) + return; + + AimPresentation.ForceCursorVisible = true; + + var mat = Mat; + if (mat == null) + { + GUILayout.BeginArea(new Rect(10, 40, 270, 64), GUI.skin.box); + GUILayout.Label("PixelOutline.mat not found at\n" + MaterialPath, Wrap); + GUILayout.EndArea(); + return; + } + + float panelH = Mathf.Min(660f, Screen.height - 60f); + GUILayout.BeginArea(new Rect(10, 40, 272, panelH), GUI.skin.box); + GUILayout.Label("PIXEL-ART RENDER STYLE (live)"); + _scroll = GUILayout.BeginScrollView(_scroll); + + Toggle(mat, "_MasterEnabled", "MASTER (whole effect on/off)"); + GUILayout.Space(6); + + GUILayout.Label("- Toggles -"); + Toggle(mat, "_PixelateEnabled", "Pixelate"); + Toggle(mat, "_PosterizeEnabled", "Posterize (banded color)"); + Toggle(mat, "_DepthEdgesEnabled", "Depth edges (silhouette)"); + Toggle(mat, "_NormalEdgesEnabled", "Normal edges (creases)"); + + GUILayout.Space(6); + GUILayout.Label("- Pixelation -"); + Slider(mat, "_PixelHeight", "Grid height (px)", 40f, 720f, "0"); + + GUILayout.Space(6); + GUILayout.Label("- Color / tone -"); + Slider(mat, "_ColorLevels", "Posterize levels", 2f, 64f, "0"); + Slider(mat, "_Brightness", "Brightness", 0.5f, 2f, "0.00"); + Slider(mat, "_Contrast", "Contrast", 0.5f, 2f, "0.00"); + Slider(mat, "_Saturation", "Saturation", 0f, 2f, "0.00"); + + GUILayout.Space(6); + GUILayout.Label("- Outlines -"); + Slider(mat, "_DepthThreshold", "Depth threshold", 0f, 1f, "0.000"); + Slider(mat, "_NormalThreshold", "Normal threshold", 0f, 4f, "0.00"); + Slider(mat, "_OutlineThickness", "Thickness (texels)", 0.25f, 4f, "0.00"); + Slider(mat, "_EdgeStrength", "Edge strength", 0f, 1f, "0.00"); + ColorRow(mat, "_OutlineColorOuter", "Silhouette color"); + ColorRow(mat, "_OutlineColorInner", "Crease color"); + + GUILayout.Space(8); + GUILayout.BeginHorizontal(); + if (GUILayout.Button("Reset defaults")) + ResetDefaults(mat); + if (GUILayout.Button("Save Asset")) + SaveAsset(mat); + GUILayout.EndHorizontal(); + GUILayout.Label("Drags apply live and persist after Play. 'Save Asset' writes to disk now.", Wrap); + + GUILayout.EndScrollView(); + GUILayout.EndArea(); + } + + static void Toggle(Material mat, string prop, string label) + { + bool cur = mat.GetFloat(prop) > 0.5f; + bool now = GUILayout.Toggle(cur, " " + label); + if (now != cur) + { + mat.SetFloat(prop, now ? 1f : 0f); + EditorUtility.SetDirty(mat); + } + } + + static void Slider(Material mat, string prop, string label, float min, float max, string fmt) + { + float cur = mat.GetFloat(prop); + GUILayout.BeginHorizontal(); + GUILayout.Label(label, GUILayout.Width(126)); + GUILayout.Label(cur.ToString(fmt), GUILayout.Width(48)); + GUILayout.EndHorizontal(); + float now = GUILayout.HorizontalSlider(cur, min, max); + if (!Mathf.Approximately(now, cur)) + { + mat.SetFloat(prop, now); + EditorUtility.SetDirty(mat); + } + } + + static void ColorRow(Material mat, string prop, string label) + { + Color c = mat.GetColor(prop); + GUILayout.Label(label); + Color n = c; + n.r = ChannelSlider("R", c.r); + n.g = ChannelSlider("G", c.g); + n.b = ChannelSlider("B", c.b); + if (n != c) + { + n.a = 1f; + mat.SetColor(prop, n); + EditorUtility.SetDirty(mat); + } + } + + static float ChannelSlider(string label, float v) + { + GUILayout.BeginHorizontal(); + GUILayout.Label(label, GUILayout.Width(14)); + float r = GUILayout.HorizontalSlider(v, 0f, 1f); + GUILayout.EndHorizontal(); + return r; + } + + // Shipped defaults from PixelOutline.shader / .mat. + static void ResetDefaults(Material mat) + { + mat.SetFloat("_MasterEnabled", 1f); + mat.SetFloat("_PixelHeight", 200f); + mat.SetFloat("_PosterizeEnabled", 1f); + mat.SetFloat("_ColorLevels", 12f); + mat.SetFloat("_Brightness", 1.1f); + mat.SetFloat("_Contrast", 1.05f); + mat.SetFloat("_Saturation", 1.35f); + mat.SetFloat("_DepthThreshold", 0.08f); + mat.SetFloat("_NormalThreshold", 1f); + mat.SetFloat("_OutlineThickness", 1f); + mat.SetFloat("_EdgeStrength", 1f); + mat.SetFloat("_PixelateEnabled", 1f); + mat.SetFloat("_DepthEdgesEnabled", 1f); + mat.SetFloat("_NormalEdgesEnabled", 1f); + mat.SetColor("_OutlineColorOuter", Color.black); + mat.SetColor("_OutlineColorInner", Color.black); + EditorUtility.SetDirty(mat); + } + + static void SaveAsset(Material mat) + { + EditorUtility.SetDirty(mat); + AssetDatabase.SaveAssetIfDirty(mat); + } + } +} +#endif diff --git a/Assets/_Project/Scripts/Client/Debug/PixelArtDevControls.cs.meta b/Assets/_Project/Scripts/Client/Debug/PixelArtDevControls.cs.meta new file mode 100644 index 000000000..d64bf60bd --- /dev/null +++ b/Assets/_Project/Scripts/Client/Debug/PixelArtDevControls.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: e197a9add2f5e9845bac0bddd950232a \ No newline at end of file diff --git a/Assets/_Project/Scripts/Client/Presentation/EnemyMarkerSystem.cs b/Assets/_Project/Scripts/Client/Presentation/EnemyMarkerSystem.cs new file mode 100644 index 000000000..a63d039d0 --- /dev/null +++ b/Assets/_Project/Scripts/Client/Presentation/EnemyMarkerSystem.cs @@ -0,0 +1,184 @@ +using System.Collections.Generic; +using ProjectM.Simulation; +using Unity.Entities; +using Unity.Mathematics; +using Unity.NetCode; +using Unity.Transforms; +using UnityEngine; +using UnityEngine.UIElements; + +namespace ProjectM.Client +{ + /// + /// Client-only ENEMY VISIBILITY markers — makes living enemies (especially the last one) easy to find so a room + /// never devolves into hunting an unseen straggler. Observe-only presentation in + /// (same shape as / : + /// reads replicated state, never mutates the sim, never destroys a ghost). Owns its own runtime UIDocument + /// (sortingOrder 48 — just under the HUD's 50) and a POOL of marker glyphs: each living enemy within + /// of the local player gets either a clamped EDGE ARROW when off-screen + /// (rotated toward it — the "it's over here" cue, reusing OnboardingSystem's WorldToScreen edge math) or a subtle + /// overhead PIP when on-screen. The pips fade toward as the on-screen + /// enemy count rises (declutter a swarm); off-screen arrows always stay full so a straggler is never missed. The + /// boss is excluded (it has its own HUD presence bar); corpses () are excluded. + /// + [WorldSystemFilter(WorldSystemFilterFlags.ClientSimulation)] + [UpdateInGroup(typeof(PresentationSystemGroup))] + public partial class EnemyMarkerSystem : SystemBase + { + const int MaxMarkers = 32; // pool cap (a room never fields this many; when few remain, all are marked) + const float Margin = 48f; // screen-edge inset for off-screen arrows (panel units) + + GameObject _go; + UIDocument _doc; + bool _built; + readonly List