process TODO.md step2

This commit is contained in:
SepComet 2026-03-28 15:02:40 +08:00
parent a0a8c0a89d
commit 4a152e765b
17 changed files with 450 additions and 42 deletions

View File

@ -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);

View File

@ -0,0 +1,16 @@
{
"name": "NetworkFramework",
"rootNamespace": "",
"references": [
"GUID:d972d56d6b084684b5b0666f4856da75"
],
"includePlatforms": [],
"excludePlatforms": [],
"allowUnsafeCode": false,
"overrideReferences": false,
"precompiledReferences": [],
"autoReferenced": true,
"defineConstraints": [],
"versionDefines": [],
"noEngineReferences": false
}

View File

@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 25ab7ef51eeef1e4ea6e24413cbabdff
AssemblyDefinitionImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -13,17 +13,18 @@ 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()
{
Instance = this;
@ -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()

View File

@ -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()
{

View File

@ -4,7 +4,8 @@
"references": [
"Network.Runtime",
"UnityEngine.TestRunner",
"UnityEditor.TestRunner"
"UnityEditor.TestRunner",
"NetworkFramework"
],
"includePlatforms": [
"Editor"

View File

@ -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()
{

View File

@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-03-28

View File

@ -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?

View File

@ -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.

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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.

View File

@ -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

View File

@ -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

View File

@ -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