#if UNITY_EDITOR using ProjectM.Simulation; using UnityEngine; namespace ProjectM.Client { /// /// EDITOR-ONLY in-game dev-tools overlay (IMGUI dev overlay). Buttons enqueue /// RPCs through , so they drive the REAL /// server-authoritative paths (and work over a live connection too, not just in-editor). Drop it on a /// GameObject in the DevSandbox (or Game) scene. While open it forces the OS cursor visible /// () so its buttons stay clickable even while aiming. /// Stripped from player builds (#if UNITY_EDITOR). /// public class DebugOverlay : MonoBehaviour { bool _open = true; int _grantAmount = 50; bool _tuningOpen; Vector2 _scroll; void OnDisable() => AimPresentation.ForceCursorVisible = false; void OnGUI() { if (GUI.Button(new Rect(Screen.width - 96, 10, 86, 24), _open ? "DEV ▲" : "DEV ▼")) _open = !_open; AimPresentation.ForceCursorVisible = _open; if (!_open) return; float panelH = Mathf.Min(760f, Screen.height - 50f); GUILayout.BeginArea(new Rect(Screen.width - 232, 40, 222, panelH), GUI.skin.box); GUILayout.Label("DEV TOOLS"); _scroll = GUILayout.BeginScrollView(_scroll); GUILayout.Label("- World -"); if (GUILayout.Button("Force Next Wave")) DebugCommandSendSystem.SpawnWave(); if (GUILayout.Button("Stop Waves")) DebugCommandSendSystem.StopWaves(); if (GUILayout.Button("Clear Enemies")) DebugCommandSendSystem.ClearEnemies(); GUILayout.Space(6); GUILayout.Label("- Saturation (G4) -"); if (GUILayout.Button("Spawn 12 Drowners")) DebugCommandSendSystem.SpawnEnemy(0, 12); // gym roster kind 0; the standing worst-case load CombatStressDebug.StressAllyFx = GUILayout.Toggle(CombatStressDebug.StressAllyFx, "Stress ally FX (3 fake casters)"); GUILayout.Space(6); GUILayout.Label("- Resources -"); _grantAmount = IntField("Amount", _grantAmount); GUILayout.BeginHorizontal(); if (GUILayout.Button("Aether")) DebugCommandSendSystem.GrantResource(ResourceId.Aether, _grantAmount); if (GUILayout.Button("Ore")) DebugCommandSendSystem.GrantResource(ResourceId.Ore, _grantAmount); if (GUILayout.Button("Bio")) DebugCommandSendSystem.GrantResource(ResourceId.Biomass, _grantAmount); GUILayout.EndHorizontal(); if (GUILayout.Button("Grant Damage Upgrade")) DebugCommandSendSystem.GrantUpgrade(); GUILayout.Space(6); GUILayout.Label("- Player -"); GUILayout.BeginHorizontal(); if (GUILayout.Button("Heal")) DebugCommandSendSystem.Heal(); if (GUILayout.Button("Kill")) DebugCommandSendSystem.Kill(); if (GUILayout.Button("God")) DebugCommandSendSystem.ToggleGod(); GUILayout.EndHorizontal(); GUILayout.BeginHorizontal(); if (GUILayout.Button("Go Base")) DebugCommandSendSystem.Teleport(RegionId.Base); if (GUILayout.Button("Go Expedition")) DebugCommandSendSystem.Teleport(RegionId.Expedition); GUILayout.EndHorizontal(); GUILayout.Space(6); GUILayout.Label("- Class (F1/F2) -"); GUILayout.BeginHorizontal(); if (GUILayout.Button("Warrior")) DebugCommandSendSystem.SetWarrior(); if (GUILayout.Button("Ranger")) DebugCommandSendSystem.SetRanger(); GUILayout.EndHorizontal(); GUILayout.Space(6); GUILayout.Label("- Telemetry (MC-0) -"); if (DevTelemetryReadout.HasData) { var t = DevTelemetryReadout.Latest; GUILayout.Label($"tick {t.LastSampleTick} husks {t.LiveEnemyCount}"); GUILayout.Label($"dash neg {t.DashIFrameNegatedHits} / wasted {t.DashesWasted}"); GUILayout.Label($"whiff open {t.ChargerWhiffWindowsOpened} / punish {t.ChargerWhiffPunishesLanded}"); } else { GUILayout.Label("(waiting for server telemetry...)"); } GUILayout.Space(6); _tuningOpen = GUILayout.Toggle(_tuningOpen, "- Tuning (MC-0) -"); if (_tuningOpen) { TuningRow("Dash dist", TuningKnob.DashDistance, 0.5f, "0.0"); TuningRow("Dash iframe t", TuningKnob.IFrameWindowTicks, 1f, "0"); TuningRow("Dash recover t", TuningKnob.RecoverTailTicks, 1f, "0"); TuningRow("Dash cd t", TuningKnob.DashCooldownTicks, 1f, "0"); TuningRow("Dash sharp", TuningKnob.DashSharpness, 25f, "0"); TuningRow("Chgr windup t", TuningKnob.ChargerWindupTicks, 1f, "0"); TuningRow("Chgr lunge spd", TuningKnob.ChargerLungeSpeed, 1f, "0.0"); TuningRow("Chgr lunge t", TuningKnob.ChargerLungeDurationTicks, 1f, "0"); TuningRow("Chgr stagger t", TuningKnob.ChargerWhiffStaggerTicks, 1f, "0"); TuningRow("Grunt windup t", TuningKnob.GruntWindupTicks, 1f, "0"); GUILayout.Space(4); TuningRow("Melee dmg", TuningKnob.MeleeDamage, 1f, "0.0"); TuningRow("Melee range", TuningKnob.MeleeRange, 0.2f, "0.0"); TuningRow("Melee cone rad", TuningKnob.MeleeConeHalfAngleRad, 0.05f, "0.00"); TuningRow("Melee recover t", TuningKnob.MeleeRecoverTicks, 1f, "0"); TuningRow("Melee chain t", TuningKnob.MeleeChainGraceTicks, 1f, "0"); TuningRow("Melee move x", TuningKnob.MeleeSwingMoveScale, 0.05f, "0.00"); TuningRow("Melee knock spd", TuningKnob.MeleeKnockbackSpeed, 1f, "0.0"); TuningRow("Melee finish x", TuningKnob.MeleeFinisherMult, 0.1f, "0.0"); TuningRow("Melee combo len", TuningKnob.MeleeComboLength, 1f, "0"); // 07-20 feel forks: contact delay (0 = legacy same-tick cleave), input buffer (0 = off), reach-only finisher mult TuningRow("Melee contact t", TuningKnob.MeleeContactTicks, 1f, "0"); TuningRow("Melee buffer t", TuningKnob.MeleeBufferTicks, 1f, "0"); TuningRow("Melee finish rng x", TuningKnob.MeleeFinisherRangeMult, 0.05f, "0.00"); GUILayout.Space(4); TuningRow("Struct aggro w", TuningKnob.StructureAggroWeight, 0.1f, "0.00"); // EB-1: <1 prefers structures // 07-15 facing/underwater feel (0 = no override: authored stat / cast const / authored sharpness) TuningRow("Turn rate deg", TuningKnob.TurnRateDeg, 30f, "0"); TuningRow("Cast turn deg", TuningKnob.CastTurnRateDeg, 60f, "0"); TuningRow("Move sharp", TuningKnob.MoveSharpness, 0.5f, "0.0"); // 07-20 data-driven tuning: whole-gestalt FEEL PROFILES (sim knobs + FeelConfig in one JSON) -- // A/B a feel with one click instead of dialing 30 scalars; Save captures the hand-dialed state. GUILayout.Space(6); GUILayout.Label("- Feel Profiles -"); foreach (var prof in FeelProfileService.ListProfiles()) if (GUILayout.Button(System.IO.Path.GetFileNameWithoutExtension(prof))) FeelProfileService.Apply(prof); GUILayout.BeginHorizontal(); if (GUILayout.Button("Save current")) FeelProfileService.SaveCurrent(); if (GUILayout.Button("Rescan")) FeelProfileService.Rescan(); GUILayout.EndHorizontal(); if (GUILayout.Button(CombatProbe.Running ? "Probe running..." : "Combat probe (6 swings)") && !CombatProbe.Running) CombatProbe.Run(6); // measured cadence/contact/DPS -> console } GUILayout.EndScrollView(); GUILayout.EndArea(); } static int IntField(string label, int value) { GUILayout.BeginHorizontal(); GUILayout.Label(label, GUILayout.Width(70)); string s = GUILayout.TextField(value.ToString(), GUILayout.Width(60)); GUILayout.EndHorizontal(); return int.TryParse(s, out var v) ? v : value; } // One live-tuning row: shows the current value (from the readout) + step buttons. A nudge fires the // authoritative server RPC AND an optimistic local apply (so the tuner's own predicted dash uses it now). static void TuningRow(string label, byte knob, float step, string fmt) { float cur = ProjectM.Simulation.TuningConfig.Get(TuningReadout.Current, knob); GUILayout.BeginHorizontal(); GUILayout.Label(label, GUILayout.Width(92)); GUILayout.Label(cur.ToString(fmt), GUILayout.Width(40)); if (GUILayout.Button("-", GUILayout.Width(26))) Nudge(knob, cur - step); if (GUILayout.Button("+", GUILayout.Width(26))) Nudge(knob, cur + step); GUILayout.EndHorizontal(); } static void Nudge(byte knob, float value) { DebugCommandSendSystem.SetTuning(knob, value); // authoritative (server applies + broadcasts; clamped) TuningReadout.SetLocal(knob, value); // optimistic local (instant feel for the tuner; clamped) } } } #endif