diff --git a/Assets/Scripts/MovementComponent.cs b/Assets/Scripts/MovementComponent.cs index 7bcdcfe..db4a661 100644 --- a/Assets/Scripts/MovementComponent.cs +++ b/Assets/Scripts/MovementComponent.cs @@ -4,6 +4,54 @@ using Network.NetworkApplication; using UnityEngine; using Vector3 = UnityEngine.Vector3; +public static class ClientGameplayInputFlow +{ + public static bool HasPlanarInput(Vector3 input) + { + return new Vector2(input.x, input.z).sqrMagnitude > 0f; + } + + public static bool TryCreateMoveInput(string playerId, long tick, Vector3 input, bool stopMessagePending, out MoveInput message) + { + if (!HasPlanarInput(input) && !stopMessagePending) + { + message = null; + return false; + } + + message = new MoveInput + { + PlayerId = playerId, + Tick = tick, + MoveX = input.x, + MoveY = input.z + }; + return true; + } + + public static ShootInput CreateShootInput(string playerId, long tick, Vector3 aimDirection, string targetId = "") + { + var planarDirection = new Vector3(aimDirection.x, 0f, aimDirection.z); + if (planarDirection.sqrMagnitude <= 0f) + { + planarDirection = Vector3.forward; + } + else + { + planarDirection.Normalize(); + } + + return new ShootInput + { + PlayerId = playerId, + Tick = tick, + DirX = planarDirection.x, + DirY = planarDirection.z, + TargetId = targetId ?? string.Empty + }; + } +} + public class MovementComponent : MonoBehaviour { [SerializeField] private float _sendInterval = 0.05f; @@ -26,7 +74,10 @@ public class MovementComponent : MonoBehaviour private Vector3 _currentPos; private float _lerpTime; [SerializeField] private float _lerpRate = 0.1f; - private MoveInput _cachedInput; + private Vector3 _cachedMoveInput; + private Vector3 _lastAimDirection = Vector3.forward; + private bool _wasMovingLastFrame; + private bool _stopMessagePending; public void Init(bool isControlled, Player master, int speed = 0, long serverTick = 0) { @@ -44,13 +95,33 @@ public class MovementComponent : MonoBehaviour { if (_isControlled) { - _cachedInput = CaptureInput(); + _cachedMoveInput = CaptureMovement(); + var hasMovement = ClientGameplayInputFlow.HasPlanarInput(_cachedMoveInput); + if (hasMovement) + { + _lastAimDirection = _cachedMoveInput; + _stopMessagePending = false; + } + else if (_wasMovingLastFrame) + { + _stopMessagePending = true; + } + + _wasMovingLastFrame = hasMovement; + + var shootInput = CaptureShootInput(); + if (shootInput != null) + { + NetworkManager.Instance.SendShootInput(shootInput); + } if (Time.time - _lastSendTime > _sendInterval) { - if (_cachedInput != null) + if (ClientGameplayInputFlow.TryCreateMoveInput(_master.PlayerId, Tick, _cachedMoveInput, _stopMessagePending, out var moveInput)) { - NetworkManager.Instance.SendMoveInput(_cachedInput); + NetworkManager.Instance.SendMoveInput(moveInput); + _predictionBuffer.Record(moveInput); + _stopMessagePending = false; } _lastSendTime = Time.time; @@ -72,11 +143,7 @@ public class MovementComponent : MonoBehaviour _hasServerState = false; } - Simulate(_cachedInput); - if (_cachedInput != null) - { - _predictionBuffer.Record(_cachedInput); - } + Simulate(_cachedMoveInput); } else { @@ -98,27 +165,36 @@ public class MovementComponent : MonoBehaviour ReplayPendingInputs(replayInputs); } - private MoveInput CaptureInput() + private Vector3 CaptureMovement() { - var input = new Vector3(Input.GetAxisRaw("Horizontal"), 0, Input.GetAxisRaw("Vertical")); - if (input == Vector3.zero) + return new Vector3(Input.GetAxisRaw("Horizontal"), 0f, Input.GetAxisRaw("Vertical")); + } + + private ShootInput CaptureShootInput() + { + if (!Input.GetMouseButtonDown(0)) { return null; } - return new MoveInput - { - PlayerId = _master.PlayerId, - Tick = Tick, - MoveX = input.x, - MoveY = input.z - }; + return ClientGameplayInputFlow.CreateShootInput(_master.PlayerId, Tick, ResolveAimDirection()); } - private void Simulate(MoveInput input) + private Vector3 ResolveAimDirection() { - var dir = input == null ? Vector3.zero : new Vector3(input.MoveX, 0f, input.MoveY); - _rigid.velocity = _speed * dir; + if (ClientGameplayInputFlow.HasPlanarInput(_lastAimDirection)) + { + return _lastAimDirection; + } + + var forward = _master != null ? _master.transform.forward : transform.forward; + var planarForward = new Vector3(forward.x, 0f, forward.z); + return ClientGameplayInputFlow.HasPlanarInput(planarForward) ? planarForward : Vector3.forward; + } + + private void Simulate(Vector3 input) + { + _rigid.velocity = _speed * input; if (_isControlled) { MainUI.Instance.OnClientPosChanged(_rigid.position); @@ -182,4 +258,4 @@ public class MovementComponent : MonoBehaviour MainUI.Instance.OnClientPosChanged(_rigid.position); } } -} \ No newline at end of file +} diff --git a/Assets/Scripts/NetworkFramework.asmdef b/Assets/Scripts/NetworkFramework.asmdef new file mode 100644 index 0000000..3ee1fa9 --- /dev/null +++ b/Assets/Scripts/NetworkFramework.asmdef @@ -0,0 +1,16 @@ +{ + "name": "NetworkFramework", + "rootNamespace": "", + "references": [ + "GUID:d972d56d6b084684b5b0666f4856da75" + ], + "includePlatforms": [], + "excludePlatforms": [], + "allowUnsafeCode": false, + "overrideReferences": false, + "precompiledReferences": [], + "autoReferenced": true, + "defineConstraints": [], + "versionDefines": [], + "noEngineReferences": false +} \ No newline at end of file diff --git a/Assets/Scripts/NetworkFramework.asmdef.meta b/Assets/Scripts/NetworkFramework.asmdef.meta new file mode 100644 index 0000000..79c6562 --- /dev/null +++ b/Assets/Scripts/NetworkFramework.asmdef.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 25ab7ef51eeef1e4ea6e24413cbabdff +AssemblyDefinitionImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/NetworkManager.cs b/Assets/Scripts/NetworkManager.cs index 3e6eddc..dd17f3f 100644 --- a/Assets/Scripts/NetworkManager.cs +++ b/Assets/Scripts/NetworkManager.cs @@ -13,16 +13,17 @@ public class NetworkManager : MonoBehaviour private const int DefaultReliablePort = 8080; private const int DefaultSyncPort = 8081; - public static NetworkManager Instance; - private SharedNetworkRuntime _networkRuntime; - private IPEndPoint _serverPoint; - private uint _sequence = 0; - private Task _networkDrainTask = Task.CompletedTask; [SerializeField] private GameObject _wrongWindow; [SerializeField] private bool _enableNetworkDiagnosticsOverlay = true; [SerializeField] private string _serverIp = DefaultServerIp; [SerializeField] private int _reliablePort = DefaultReliablePort; [SerializeField] private int _syncPort = DefaultSyncPort; + + public static NetworkManager Instance; + private SharedNetworkRuntime _networkRuntime; + private IPEndPoint _serverPoint; + private uint _sequence = 0; + private Task _networkDrainTask = Task.CompletedTask; private void Awake() { @@ -197,24 +198,23 @@ public class NetworkManager : MonoBehaviour Debug.Log($"[NetworkManager] Session {lifecycleEvent.PreviousState} -> {lifecycleEvent.CurrentState} ({lifecycleEvent.Kind}) {lifecycleEvent.Reason}"); } - public void SendMoveInput(string playerId, Vector3 input) - { - var message = new MoveInput - { - PlayerId = playerId, - MoveX = input.x, - MoveY = input.z - }; - _networkRuntime.MessageManager.SendMessage(message, MessageType.MoveInput); - Debug.Log($"PlayerMoveSeq: {_sequence++}"); - } - public void SendMoveInput(MoveInput message) { _networkRuntime.MessageManager.SendMessage(message, MessageType.MoveInput); Debug.Log($"PlayerMoveSeq: {_sequence++}"); } + public void SendShootInput(string playerId, Vector3 direction, long tick = 0, string targetId = "") + { + SendShootInput(ClientGameplayInputFlow.CreateShootInput(playerId, tick, direction, targetId)); + } + + public void SendShootInput(ShootInput message) + { + _networkRuntime.MessageManager.SendMessage(message, MessageType.ShootInput); + Debug.Log($"PlayerShootSeq: {_sequence++}"); + } + public void SendLoginRequest(string playerId, int speed) { var request = new LoginRequest() diff --git a/Assets/Tests/EditMode/Network/MessageManagerTests.cs b/Assets/Tests/EditMode/Network/MessageManagerTests.cs index c53e011..b273025 100644 --- a/Assets/Tests/EditMode/Network/MessageManagerTests.cs +++ b/Assets/Tests/EditMode/Network/MessageManagerTests.cs @@ -86,6 +86,38 @@ namespace Tests.EditMode.Network Assert.That(parsed.MoveY, Is.EqualTo(-1)); } + [Test] + public void SendMessage_ZeroVectorMoveInput_UsesSyncLanePolicy() + { + var reliableTransport = new FakeTransport(); + var syncTransport = new FakeTransport(); + var manager = new MessageManager( + reliableTransport, + new MainThreadNetworkDispatcher(), + new DefaultMessageDeliveryPolicyResolver(), + syncTransport); + var message = new MoveInput + { + PlayerId = "player-1", + Tick = 13, + MoveX = 0f, + MoveY = 0f + }; + + manager.SendMessage(message, MessageType.MoveInput); + + Assert.That(reliableTransport.SendCallCount, Is.EqualTo(0)); + Assert.That(syncTransport.SendCallCount, Is.EqualTo(1)); + + var envelope = Envelope.Parser.ParseFrom(syncTransport.LastSentData); + var parsed = MoveInput.Parser.ParseFrom(envelope.Payload); + Assert.That(envelope.Type, Is.EqualTo((int)MessageType.MoveInput)); + Assert.That(parsed.PlayerId, Is.EqualTo("player-1")); + Assert.That(parsed.Tick, Is.EqualTo(13)); + Assert.That(parsed.MoveX, Is.EqualTo(0f)); + Assert.That(parsed.MoveY, Is.EqualTo(0f)); + } + [Test] public void SendMessage_ShootInput_UsesReliableLanePolicy() { diff --git a/Assets/Tests/EditMode/Network/Network.EditMode.Tests.asmdef b/Assets/Tests/EditMode/Network/Network.EditMode.Tests.asmdef index 9e24742..a50e3c6 100644 --- a/Assets/Tests/EditMode/Network/Network.EditMode.Tests.asmdef +++ b/Assets/Tests/EditMode/Network/Network.EditMode.Tests.asmdef @@ -4,7 +4,8 @@ "references": [ "Network.Runtime", "UnityEngine.TestRunner", - "UnityEditor.TestRunner" + "UnityEditor.TestRunner", + "NetworkFramework" ], "includePlatforms": [ "Editor" diff --git a/Assets/Tests/EditMode/Network/SyncStrategyTests.cs b/Assets/Tests/EditMode/Network/SyncStrategyTests.cs index f307676..b55029e 100644 --- a/Assets/Tests/EditMode/Network/SyncStrategyTests.cs +++ b/Assets/Tests/EditMode/Network/SyncStrategyTests.cs @@ -6,11 +6,54 @@ using Network.NetworkApplication; using Network.NetworkHost; using Network.NetworkTransport; using NUnit.Framework; +using UnityEngine; +using Vector3 = UnityEngine.Vector3; namespace Tests.EditMode.Network { public class SyncStrategyTests { + [Test] + public void ClientGameplayInputFlow_StopTransition_EmitsSingleZeroVectorMoveInput() + { + var released = ClientGameplayInputFlow.TryCreateMoveInput( + "player-1", + 8, + Vector3.zero, + true, + out var stopInput); + var continuedIdle = ClientGameplayInputFlow.TryCreateMoveInput( + "player-1", + 9, + Vector3.zero, + false, + out var idleInput); + + Assert.That(released, Is.True); + Assert.That(stopInput, Is.Not.Null); + Assert.That(stopInput.PlayerId, Is.EqualTo("player-1")); + Assert.That(stopInput.Tick, Is.EqualTo(8)); + Assert.That(stopInput.MoveX, Is.EqualTo(0f)); + Assert.That(stopInput.MoveY, Is.EqualTo(0f)); + Assert.That(continuedIdle, Is.False); + Assert.That(idleInput, Is.Null); + } + + [Test] + public void ClientGameplayInputFlow_CreateShootInput_UsesSplitShootMessageFields() + { + var shootInput = ClientGameplayInputFlow.CreateShootInput( + "player-1", + 21, + new Vector3(2f, 0f, 0f)); + + Assert.That(shootInput.PlayerId, Is.EqualTo("player-1")); + Assert.That(shootInput.Tick, Is.EqualTo(21)); + Assert.That(shootInput.DirX, Is.EqualTo(1f)); + Assert.That(shootInput.DirY, Is.EqualTo(0f)); + Assert.That(shootInput.TargetId, Is.EqualTo(string.Empty)); + } + [Test] public void ClientPredictionBuffer_AuthoritativeState_PrunesAcknowledgedMoveInputs() { @@ -149,4 +192,4 @@ namespace Tests.EditMode.Network } } } -} \ No newline at end of file +} diff --git a/openspec/changes/archive/2026-03-28-align-client-input-flow-with-mvp/.openspec.yaml b/openspec/changes/archive/2026-03-28-align-client-input-flow-with-mvp/.openspec.yaml new file mode 100644 index 0000000..65bf7c9 --- /dev/null +++ b/openspec/changes/archive/2026-03-28-align-client-input-flow-with-mvp/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-03-28 diff --git a/openspec/changes/archive/2026-03-28-align-client-input-flow-with-mvp/design.md b/openspec/changes/archive/2026-03-28-align-client-input-flow-with-mvp/design.md new file mode 100644 index 0000000..2f4c69f --- /dev/null +++ b/openspec/changes/archive/2026-03-28-align-client-input-flow-with-mvp/design.md @@ -0,0 +1,70 @@ +## Context + +The repository already has the shared MVP message split in place: `MoveInput` exists for high-frequency movement and `ShootInput` exists for reliable firing intent. The current Unity client path has only implemented part of that contract. `MovementComponent` captures movement every send interval, but it returns `null` for zero input, which means releasing input does not emit an authoritative stop update. The same component also drives immediate local prediction for the controlled player, while `NetworkManager` only exposes `SendMoveInput(...)` and has no shooting send path. + +This change is cross-cutting enough to justify a design document because it touches Unity input capture, local prediction timing, message selection, and delivery-lane expectations together. It also needs to preserve the shared/client boundary already established by the networking architecture: Unity input polling stays in Unity-side scripts, while the shared networking layer continues to own message transport policy. + +## Goals / Non-Goals + +**Goals:** +- Define an MVP-safe client input flow that sends movement and shooting through `MoveInput` and `ShootInput` only. +- Preserve immediate local movement prediction for the controlled player. +- Ensure movement release emits one explicit zero-vector `MoveInput` so the server can stop authoritative motion. +- Add a `NetworkManager.SendShootInput(...)` API that keeps shooting traffic on the reliable lane through existing delivery-policy resolution. +- Keep local shooting feedback optional and cosmetic so authoritative combat remains server-driven. + +**Non-Goals:** +- Redesign the shared transport, message envelope, or delivery-policy system. +- Introduce `UnityEngine` dependencies into shared code under `Assets/Scripts/Network/`. +- Define full authoritative combat-result handling or client-side rollback for rejected shots. +- Rework remote-player interpolation or the later authoritative `PlayerState` application steps from the TODO. + +## Decisions + +### Decision: Keep gameplay input ownership in Unity-side controlled-player components +Movement capture, release detection, and shooting input polling will stay in Unity-facing scripts such as `MovementComponent` or an adjacent controlled-player helper, with `NetworkManager` acting as the send boundary into shared networking code. + +This keeps Unity polling (`Input.*`) out of shared networking code and matches the current architecture, where `MovementComponent` already owns local prediction and `NetworkManager` already owns message submission. + +Alternative considered: move gameplay input orchestration into shared networking services. Rejected because shared code cannot depend on Unity input APIs and the TODO step only needs client-side MVP wiring, not a new host-agnostic input abstraction. + +### Decision: Represent input release as an edge-triggered zero-vector `MoveInput` +The client will continue sampling movement every send interval, but it must distinguish between "still idle" and "just released input." On the transition from non-zero movement to idle, it will send one final `MoveInput` whose vector is zero. Continued idle frames will not keep spamming zero messages unless a later movement input starts and stops again. + +This satisfies the authoritative-stop acceptance criteria without bloating sync traffic or changing the meaning of `MoveInput`. + +Alternative considered: infer stop on the server from missing movement packets. Rejected because it couples stop timing to packet cadence and conflicts with the TODO requirement for an explicit stop message. + +### Decision: Keep local movement prediction immediate and independent from send-path branching +The controlled player should continue applying local movement immediately from the latest captured input, including the zero-vector case on release, while the network send path decides whether that captured input should be transmitted this frame. + +This preserves current feel and keeps prediction behavior stable even as the send rules become more explicit. + +Alternative considered: simulate only after a message is queued for send. Rejected because it would delay local response and entangle presentation with networking cadence. + +### Decision: Add a narrow `SendShootInput(...)` API and leave shot presentation outside authoritative gameplay +`NetworkManager` will gain a dedicated method for sending `ShootInput`, mirroring the current movement send API. The client capture path will construct `ShootInput` from local fire intent and rely on the existing delivery-policy resolver so the message remains on the reliable lane. Any local muzzle flash, animation, or other feedback remains optional and must not decide damage, hit confirmation, or death state. + +This keeps the MVP send surface explicit and ensures the client no longer needs legacy broad gameplay messages for firing. + +Alternative considered: reuse a generic "send gameplay action" entry point. Rejected because it weakens the split-message contract and leaves room for `PlayerAction`-style payloads to creep back into client code. + +## Risks / Trade-offs + +- [Risk] Edge-triggered stop detection can regress if input state tracking mixes "captured this frame" with "sent this frame." → Mitigation: define the release rule around the last non-zero captured movement state and cover it with regression tests. +- [Risk] Shooting capture may depend on an existing Unity scene/input setup that is less formalized than movement. → Mitigation: keep the MVP contract narrow, allow optional cosmetic feedback, and leave targeting details minimal when no authoritative target is selected. +- [Risk] Client scripts could accidentally reintroduce legacy gameplay messages while adding fire input. → Mitigation: codify the split-message-only rule in specs and tests around `NetworkManager` send APIs. +- [Risk] Overloading `MovementComponent` with too many responsibilities could make later TODO steps harder to implement. → Mitigation: keep this step focused on input capture/send semantics and defer broader controller refactors unless implementation friction proves they are necessary. + +## Migration Plan + +1. Update the specs so the MVP client-input contract, message selection rules, and lane expectations are explicit. +2. Implement Unity-side input changes for movement release detection, local prediction continuity, and shooting capture. +3. Add `NetworkManager.SendShootInput(...)` and remove any remaining client gameplay dependence on legacy broad messages. +4. Add regression coverage for stop-message emission, reliable shooting dispatch, and split-message-only gameplay sends. +5. Roll back, if needed, by removing the shooting send entry point and returning to movement-only capture while preserving the existing shared message definitions. + +## Open Questions + +- Which existing Unity input source should drive the initial `ShootInput` direction in MVP: pointer-derived aim, avatar facing, or a simpler fixed forward fallback? +- Should the first implementation keep shooting capture inside `MovementComponent`, or is a small adjacent controlled-player input script clearer once fire intent is added? diff --git a/openspec/changes/archive/2026-03-28-align-client-input-flow-with-mvp/proposal.md b/openspec/changes/archive/2026-03-28-align-client-input-flow-with-mvp/proposal.md new file mode 100644 index 0000000..5aa8843 --- /dev/null +++ b/openspec/changes/archive/2026-03-28-align-client-input-flow-with-mvp/proposal.md @@ -0,0 +1,26 @@ +## Why + +The MVP protocol has already been split into `MoveInput` and `ShootInput`, but the client input loop still only emits non-zero movement updates and has no shooting send path. Step 2 is needed now so the client actually drives gameplay through the split message contract before later authoritative state and combat-result work depends on it. + +## What Changes + +- Define the MVP client gameplay-input flow for movement capture, stop signaling, local prediction, and shooting input capture. +- Require the controlled player to send an explicit zero-vector `MoveInput` when local movement input is released so authoritative movement can stop cleanly. +- Add a `NetworkManager.SendShootInput(...)` path and require client gameplay actions to be sent only as `MoveInput` or `ShootInput`. +- Preserve immediate local movement prediction for the controlled player while keeping local shooting presentation optional and purely cosmetic. +- Add regression coverage for the client-side input flow and the delivery-lane expectations it depends on. + +## Capabilities + +### New Capabilities +- `client-gameplay-input`: Defines how the MVP client captures movement and shooting intent, predicts local movement, and sends gameplay actions through split message types. + +### Modified Capabilities +- `network-gameplay-message-types`: Tighten the gameplay message contract so client gameplay-action send paths use `MoveInput` and `ShootInput` directly instead of legacy broad messages such as `PlayerAction`. +- `network-sync-strategy`: Clarify that explicit zero-vector `MoveInput` updates remain valid sync-lane traffic while `ShootInput` continues to use the reliable lane from the client send path. + +## Impact + +- Affected Unity-side client code in `Assets/Scripts/MovementComponent.cs`, `Assets/Scripts/NetworkManager.cs`, and related local input/presentation scripts. +- Affected shared or integration expectations for gameplay message selection and delivery-lane behavior. +- Edit-mode regression tests under `Assets/Tests/EditMode/Network/`, plus any focused client-side tests needed for input-flow coverage. diff --git a/openspec/changes/archive/2026-03-28-align-client-input-flow-with-mvp/specs/client-gameplay-input/spec.md b/openspec/changes/archive/2026-03-28-align-client-input-flow-with-mvp/specs/client-gameplay-input/spec.md new file mode 100644 index 0000000..62de543 --- /dev/null +++ b/openspec/changes/archive/2026-03-28-align-client-input-flow-with-mvp/specs/client-gameplay-input/spec.md @@ -0,0 +1,30 @@ +## ADDED Requirements + +### Requirement: Controlled client movement input preserves immediate prediction and explicit stop signaling +The MVP client SHALL capture movement intent for the controlled player in Unity-side input code, apply local movement prediction immediately, and send `MoveInput` updates through the networking boundary. When movement input transitions from non-zero to idle, the client MUST send one final zero-vector `MoveInput` so authoritative movement can stop cleanly. + +#### Scenario: Controlled player moves locally without waiting for the network +- **WHEN** the controlled player provides non-zero movement input +- **THEN** the client applies local movement prediction immediately for presentation +- **THEN** the client submits a `MoveInput` carrying the current player id, tick, and planar movement vector through the networking send path + +#### Scenario: Releasing movement emits an explicit stop update +- **WHEN** the controlled player releases movement input after previously providing non-zero movement +- **THEN** the client sends exactly one final `MoveInput` whose movement vector is zero +- **THEN** local predicted movement also stops immediately without waiting for authoritative correction + +### Requirement: Controlled client captures shooting intent as a dedicated gameplay input +The MVP client SHALL capture local fire intent separately from movement and translate that intent into `ShootInput` messages rather than overloading movement or generic gameplay-action payloads. + +#### Scenario: Firing produces a shoot input message +- **WHEN** the controlled player triggers a fire action +- **THEN** the client constructs a `ShootInput` containing the current player id, tick, and aim direction used by the MVP client flow +- **THEN** the message is sent through a dedicated shooting send path instead of a legacy generic gameplay-action message + +### Requirement: Local shooting presentation remains cosmetic +The MVP client SHALL treat any immediate local shooting feedback as optional cosmetic presentation and MUST NOT use it to finalize authoritative combat outcomes. + +#### Scenario: Cosmetic firing feedback does not decide gameplay truth +- **WHEN** the client plays local muzzle flash, animation, or similar fire feedback before server confirmation +- **THEN** that feedback does not apply authoritative damage, hit confirmation, or death resolution locally +- **THEN** gameplay truth remains dependent on authoritative server messages diff --git a/openspec/changes/archive/2026-03-28-align-client-input-flow-with-mvp/specs/network-gameplay-message-types/spec.md b/openspec/changes/archive/2026-03-28-align-client-input-flow-with-mvp/specs/network-gameplay-message-types/spec.md new file mode 100644 index 0000000..c71ec46 --- /dev/null +++ b/openspec/changes/archive/2026-03-28-align-client-input-flow-with-mvp/specs/network-gameplay-message-types/spec.md @@ -0,0 +1,14 @@ +## ADDED Requirements + +### Requirement: Client gameplay actions use split gameplay messages directly +The client-facing gameplay send path SHALL express MVP gameplay actions directly as `MoveInput` and `ShootInput`. Controlled-player movement and firing MUST NOT depend on legacy broad gameplay messages such as `PlayerAction`. + +#### Scenario: Client movement uses MoveInput directly +- **WHEN** the controlled client sends gameplay movement intent +- **THEN** the send path uses `MoveInput` +- **THEN** the client does not wrap that movement intent in `PlayerAction` or another broad gameplay payload + +#### Scenario: Client firing uses ShootInput directly +- **WHEN** the controlled client sends gameplay firing intent +- **THEN** the send path uses `ShootInput` +- **THEN** the client does not wrap that firing intent in `PlayerAction` or another broad gameplay payload diff --git a/openspec/changes/archive/2026-03-28-align-client-input-flow-with-mvp/specs/network-sync-strategy/spec.md b/openspec/changes/archive/2026-03-28-align-client-input-flow-with-mvp/specs/network-sync-strategy/spec.md new file mode 100644 index 0000000..ad9cd6f --- /dev/null +++ b/openspec/changes/archive/2026-03-28-align-client-input-flow-with-mvp/specs/network-sync-strategy/spec.md @@ -0,0 +1,14 @@ +## ADDED Requirements + +### Requirement: Client gameplay input preserves movement and shooting lane semantics +The client gameplay-input flow SHALL preserve the MVP delivery-lane contract when sending gameplay actions. Explicit zero-vector `MoveInput` updates generated on input release MUST remain valid high-frequency sync traffic, and `ShootInput` generated from local fire intent MUST use the reliable ordered lane. + +#### Scenario: Stop movement update remains sync traffic +- **WHEN** the controlled client sends a zero-vector `MoveInput` after releasing movement input +- **THEN** the message is still treated as `MoveInput` for delivery-policy resolution +- **THEN** the networking stack routes it through the high-frequency sync lane when one is configured + +#### Scenario: Shoot input remains reliable traffic +- **WHEN** the controlled client sends `ShootInput` from the MVP fire-input path +- **THEN** the networking stack resolves that message to the reliable ordered lane +- **THEN** firing intent does not share the latest-wins sync delivery behavior used for movement updates diff --git a/openspec/changes/archive/2026-03-28-align-client-input-flow-with-mvp/tasks.md b/openspec/changes/archive/2026-03-28-align-client-input-flow-with-mvp/tasks.md new file mode 100644 index 0000000..5800866 --- /dev/null +++ b/openspec/changes/archive/2026-03-28-align-client-input-flow-with-mvp/tasks.md @@ -0,0 +1,17 @@ +## 1. Update Controlled-Player Input Capture + +- [x] 1.1 Refine the controlled-player movement input flow so releasing input emits one zero-vector `MoveInput` while continued idle frames do not spam duplicate stop packets. +- [x] 1.2 Preserve immediate local movement prediction for the controlled player across both non-zero movement and release-to-stop transitions. +- [x] 1.3 Add an MVP shooting input capture path in the Unity-side controlled-player flow, including direction/target defaults that fit the current scene setup. + +## 2. Align Network Send Boundaries With Split Gameplay Messages + +- [x] 2.1 Add `NetworkManager.SendShootInput(...)` and route the new client fire path through `ShootInput`. +- [x] 2.2 Ensure client gameplay actions are sent only as `MoveInput` and `ShootInput`, with no remaining controlled-player dependence on legacy broad gameplay messages such as `PlayerAction`. +- [x] 2.3 Keep any local shooting feedback optional and cosmetic so authoritative combat still depends on server-driven messages. + +## 3. Verify MVP Input-Flow Regressions + +- [x] 3.1 Add or update tests covering explicit stop-message emission and the split-message-only gameplay send path. +- [x] 3.2 Add or update tests covering that `ShootInput` from the client path resolves to the reliable lane while `MoveInput` stop updates remain sync-lane traffic. +- [x] 3.3 Run `dotnet build Network.EditMode.Tests.csproj -v minimal` and `dotnet test Network.EditMode.Tests.csproj --no-build -v minimal` after implementation. diff --git a/openspec/specs/client-gameplay-input/spec.md b/openspec/specs/client-gameplay-input/spec.md new file mode 100644 index 0000000..0bf51df --- /dev/null +++ b/openspec/specs/client-gameplay-input/spec.md @@ -0,0 +1,34 @@ +# client-gameplay-input Specification + +## Purpose +Define how the controlled Unity client captures MVP gameplay intent, preserves immediate local prediction, and sends movement and shooting through split gameplay messages. + +## Requirements +### Requirement: Controlled client movement input preserves immediate prediction and explicit stop signaling +The MVP client SHALL capture movement intent for the controlled player in Unity-side input code, apply local movement prediction immediately, and send `MoveInput` updates through the networking boundary. When movement input transitions from non-zero to idle, the client MUST send one final zero-vector `MoveInput` so authoritative movement can stop cleanly. + +#### Scenario: Controlled player moves locally without waiting for the network +- **WHEN** the controlled player provides non-zero movement input +- **THEN** the client applies local movement prediction immediately for presentation +- **THEN** the client submits a `MoveInput` carrying the current player id, tick, and planar movement vector through the networking send path + +#### Scenario: Releasing movement emits an explicit stop update +- **WHEN** the controlled player releases movement input after previously providing non-zero movement +- **THEN** the client sends exactly one final `MoveInput` whose movement vector is zero +- **THEN** local predicted movement also stops immediately without waiting for authoritative correction + +### Requirement: Controlled client captures shooting intent as a dedicated gameplay input +The MVP client SHALL capture local fire intent separately from movement and translate that intent into `ShootInput` messages rather than overloading movement or generic gameplay-action payloads. + +#### Scenario: Firing produces a shoot input message +- **WHEN** the controlled player triggers a fire action +- **THEN** the client constructs a `ShootInput` containing the current player id, tick, and aim direction used by the MVP client flow +- **THEN** the message is sent through a dedicated shooting send path instead of a legacy generic gameplay-action message + +### Requirement: Local shooting presentation remains cosmetic +The MVP client SHALL treat any immediate local shooting feedback as optional cosmetic presentation and MUST NOT use it to finalize authoritative combat outcomes. + +#### Scenario: Cosmetic firing feedback does not decide gameplay truth +- **WHEN** the client plays local muzzle flash, animation, or similar fire feedback before server confirmation +- **THEN** that feedback does not apply authoritative damage, hit confirmation, or death resolution locally +- **THEN** gameplay truth remains dependent on authoritative server messages diff --git a/openspec/specs/network-gameplay-message-types/spec.md b/openspec/specs/network-gameplay-message-types/spec.md index 56d3894..7e3f207 100644 --- a/openspec/specs/network-gameplay-message-types/spec.md +++ b/openspec/specs/network-gameplay-message-types/spec.md @@ -38,3 +38,16 @@ The shared networking contract SHALL define the MVP payload fields for gameplay - **WHEN** client or server code constructs or parses `CombatEvent` - **THEN** the message exposes `tick`, `eventType`, `attackerId`, `targetId`, `damage`, and `hitPosition` - **THEN** `CombatEventType` provides explicit combat-result categories for interpreting that event payload + +### Requirement: Client gameplay actions use split gameplay messages directly +The client-facing gameplay send path SHALL express MVP gameplay actions directly as `MoveInput` and `ShootInput`. Controlled-player movement and firing MUST NOT depend on legacy broad gameplay messages such as `PlayerAction`. + +#### Scenario: Client movement uses MoveInput directly +- **WHEN** the controlled client sends gameplay movement intent +- **THEN** the send path uses `MoveInput` +- **THEN** the client does not wrap that movement intent in `PlayerAction` or another broad gameplay payload + +#### Scenario: Client firing uses ShootInput directly +- **WHEN** the controlled client sends gameplay firing intent +- **THEN** the send path uses `ShootInput` +- **THEN** the client does not wrap that firing intent in `PlayerAction` or another broad gameplay payload diff --git a/openspec/specs/network-sync-strategy/spec.md b/openspec/specs/network-sync-strategy/spec.md index 17e32f6..bc0cbf8 100644 --- a/openspec/specs/network-sync-strategy/spec.md +++ b/openspec/specs/network-sync-strategy/spec.md @@ -71,3 +71,16 @@ The networking stack SHALL route sync-designated traffic through the sync transp #### Scenario: Dedicated sync transport unavailable - **WHEN** sync-designated traffic is sent from a session whose integration wiring does not include a sync transport - **THEN** the traffic SHALL be dispatched on the primary reliable transport without failing session operation + +### Requirement: Client gameplay input preserves movement and shooting lane semantics +The client gameplay-input flow SHALL preserve the MVP delivery-lane contract when sending gameplay actions. Explicit zero-vector `MoveInput` updates generated on input release MUST remain valid high-frequency sync traffic, and `ShootInput` generated from local fire intent MUST use the reliable ordered lane. + +#### Scenario: Stop movement update remains sync traffic +- **WHEN** the controlled client sends a zero-vector `MoveInput` after releasing movement input +- **THEN** the message is still treated as `MoveInput` for delivery-policy resolution +- **THEN** the networking stack routes it through the high-frequency sync lane when one is configured + +#### Scenario: Shoot input remains reliable traffic +- **WHEN** the controlled client sends `ShootInput` from the MVP fire-input path +- **THEN** the networking stack resolves that message to the reliable ordered lane +- **THEN** firing intent does not share the latest-wins sync delivery behavior used for movement updates