7b65912f30
07-18/19/20 arc: LightCombo01/HeavyCombo01 clips onto Swing1-3/Slam at ~natural speed (26-tick window, 30-tick recover, DPS-held damage retune), Menacing01 combat idle (InCombat), SM_Wep_Axe_Large_01 rigid-skinned to Hand_R at x1.25, LANTERN FX retone + blade-smear ribbon. Forks 1-6 per Combat_Attack_Feel_Guidelines (design review wf_000bc247): cleave resolves at the CONTACT tick (MeleeCleavePending schedule-and-consume, knob 31, 0=legacy; connect cues moved to contact), MeleeRange 2.2 + reach-only finisher mult 1.25 (knob 30), BufferedAttackTick GhostField input buffer (knob 29, unlock-edge validity), dash refused pre-contact (lookup-based). Tests: MeleeComboTests pin legacy knobs; +3 fork tests (414 green); live server proof: HP drop exactly at swing+16 under tick batching. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
121 lines
6.5 KiB
C#
121 lines
6.5 KiB
C#
#if UNITY_EDITOR
|
|
using ProjectM.Simulation;
|
|
using Unity.Entities;
|
|
using Unity.Mathematics;
|
|
using Unity.NetCode;
|
|
|
|
namespace ProjectM.Client
|
|
{
|
|
/// <summary>
|
|
/// EDITOR-ONLY validation hook for driving the local player's <see cref="PlayerInput"/> without a
|
|
/// real input device or a focused Game view. The Unity Input System ignores injected/real device
|
|
/// input while the Game view is unfocused, which makes headless (MCP <c>execute_code</c>) or
|
|
/// automated fire/move validation impossible through <see cref="PlayerInputGatherSystem"/> alone.
|
|
/// <para>
|
|
/// This system runs in <see cref="GhostInputSystemGroup"/> immediately AFTER the real gather and,
|
|
/// when <see cref="Active"/> is set, overwrites the locally-owned player's input from static fields
|
|
/// you can poke from a debugger / <c>execute_code</c> / an editor button. Because it writes the same
|
|
/// <see cref="PlayerInput"/> the gather does, it drives the authentic command → prediction →
|
|
/// AbilityFireSystem pipeline (not a shortcut), so it validates the real fire/move/auto-target path.
|
|
/// </para>
|
|
/// <para>
|
|
/// Entirely wrapped in <c>#if UNITY_EDITOR</c>: it does not exist in player builds. Pair with
|
|
/// <c>Application.runInBackground = true</c> so the unfocused editor keeps ticking. Usage from
|
|
/// <c>execute_code</c>: <c>ProjectM.Client.DebugInputInjectionSystem.Fire();</c> (one shot),
|
|
/// <c>...SetMove(0f, 1f);</c> (hold a move heading), <c>...SetAim(1f, 0f);</c>, <c>...Stop();</c>.
|
|
/// </para>
|
|
/// </summary>
|
|
[UpdateInGroup(typeof(GhostInputSystemGroup))]
|
|
[UpdateAfter(typeof(PlayerInputGatherSystem))]
|
|
[WorldSystemFilter(WorldSystemFilterFlags.ClientSimulation)]
|
|
public partial class DebugInputInjectionSystem : SystemBase
|
|
{
|
|
/// <summary>While true, this system overrides the local player's gathered input each frame.</summary>
|
|
public static bool Active;
|
|
|
|
/// <summary>Movement heading applied to PlayerInput.Move while <see cref="Active"/>.</summary>
|
|
public static float2 Move;
|
|
|
|
/// <summary>Aim vector applied to PlayerInput.Aim while <see cref="Active"/> (zero = face movement).</summary>
|
|
public static float2 Aim;
|
|
|
|
/// <summary>Frames remaining to hold the Fire event. Each held frame raises Fire.Set(); holding
|
|
/// across several frames spans multiple network ticks so the one-shot event reliably reaches the
|
|
/// command buffer (a single-frame pulse can be lost across the frame→tick boundary). 0 = idle.</summary>
|
|
public static int FireFrames;
|
|
|
|
/// <summary>Input scheme stamped onto injected input (<see cref="InputSchemeId"/>). Defaults to Gamepad
|
|
/// so injected aim still receives the server auto-target assist (the pre-scheme behaviour), keeping the
|
|
/// editor fire / auto-target validation path intact.</summary>
|
|
public static byte Scheme = InputSchemeId.Gamepad;
|
|
|
|
/// <summary>Convenience: hold Fire for the next <paramref name="frames"/> frames (also enables
|
|
/// override). The ability cooldown still gates how many shots actually result.</summary>
|
|
public static void Fire(int frames = 10) { Active = true; FireFrames = math.max(FireFrames, frames); }
|
|
|
|
/// <summary>Frames remaining to hold the melee Attack event (07-19: melee swing validation).</summary>
|
|
public static int AttackFrames;
|
|
|
|
/// <summary>Convenience: hold the melee Attack for the next <paramref name="frames"/> frames (also enables override).</summary>
|
|
public static void Attack(int frames = 10) { Active = true; AttackFrames = math.max(AttackFrames, frames); }
|
|
|
|
/// <summary>Per-socket fire holds (LANTERN 4-socket kit). Each held frame raises that socket's fire event.</summary>
|
|
public static readonly int[] SocketFrames = new int[SocketId.Count];
|
|
|
|
/// <summary>Hold socket <paramref name="i"/>'s fire for the next <paramref name="frames"/> frames (also enables override).</summary>
|
|
public static void FireSocket(int i, int frames = 10) { Active = true; if ((uint)i < (uint)SocketId.Count) SocketFrames[i] = math.max(SocketFrames[i], frames); }
|
|
|
|
/// <summary>Convenience: hold a planar move heading (also enables override).</summary>
|
|
public static void SetMove(float x, float z) { Active = true; Move = new float2(x, z); }
|
|
|
|
/// <summary>Convenience: hold an aim direction (also enables override).</summary>
|
|
public static void SetAim(float x, float z) { Active = true; Aim = new float2(x, z); }
|
|
|
|
/// <summary>Convenience: stop overriding and clear all injected input.</summary>
|
|
public static void Stop() { Active = false; Move = default; Aim = default; FireFrames = 0; AttackFrames = 0; for (int i = 0; i < SocketId.Count; i++) SocketFrames[i] = 0; }
|
|
|
|
protected override void OnCreate()
|
|
{
|
|
RequireForUpdate<PlayerInput>();
|
|
}
|
|
|
|
protected override void OnUpdate()
|
|
{
|
|
if (!Active)
|
|
return;
|
|
|
|
bool fire = FireFrames > 0;
|
|
if (FireFrames > 0) FireFrames--;
|
|
bool attack = AttackFrames > 0;
|
|
if (AttackFrames > 0) AttackFrames--;
|
|
// LANTERN 4-socket kit: per-socket fire holds, decremented once per frame.
|
|
bool s0 = SocketFrames[0] > 0; if (SocketFrames[0] > 0) SocketFrames[0]--;
|
|
bool s1 = SocketFrames[1] > 0; if (SocketFrames[1] > 0) SocketFrames[1]--;
|
|
bool s2 = SocketFrames[2] > 0; if (SocketFrames[2] > 0) SocketFrames[2]--;
|
|
bool s3 = SocketFrames[3] > 0; if (SocketFrames[3] > 0) SocketFrames[3]--;
|
|
float2 move = Move;
|
|
float2 aim = Aim;
|
|
|
|
// Keep the presentation bridge in sync with the injected scheme so the reticle/cursor match the
|
|
// input the sim is actually using (the gather published a value earlier this frame; we override it).
|
|
AimPresentation.Scheme = Scheme;
|
|
|
|
foreach (var input in SystemAPI.Query<RefRW<PlayerInput>>().WithAll<GhostOwnerIsLocal>())
|
|
{
|
|
input.ValueRW.Move = move;
|
|
input.ValueRW.Aim = aim;
|
|
input.ValueRW.Scheme = Scheme;
|
|
if (fire)
|
|
input.ValueRW.Fire.Set();
|
|
if (attack)
|
|
input.ValueRW.Attack.Set();
|
|
if (s0) input.ValueRW.Socket0.Set();
|
|
if (s1) input.ValueRW.Socket1.Set();
|
|
if (s2) input.ValueRW.Socket2.Set();
|
|
if (s3) input.ValueRW.Socket3.Set();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
#endif
|