diff --git a/Aetheria.State.Daemon.Smoke/Program.cs b/Aetheria.State.Daemon.Smoke/Program.cs index 8178b49e0..137c34968 100644 --- a/Aetheria.State.Daemon.Smoke/Program.cs +++ b/Aetheria.State.Daemon.Smoke/Program.cs @@ -13,6 +13,57 @@ internal sealed class AetheriaDaemonYmirSmokeChecks DaemonSimulationAppliesYmirHit(); MissingPhysicsOwnerCannotAdvanceProjectiles(); ThermalCellsUseFossilConductionAndRadiation(); + MultipleActorsUseTheSameMovementLever(); + } + + private static void MultipleActorsUseTheSameMovementLever() + { + var player = Entity(0, 0, "player"); + var agent = Entity(1, 0, "worker"); + var run = new AetheriaRuntimeRunCheckpointCommit + { + RunId = "shared-lever-smoke", + CurrentZoneIndex = 0, + CurrentEntityKey = "zone.0.entity.0", + Zones = [new AetheriaRuntimeZoneSnapshotCommit { ZoneIndex = 0, Entities = [player, agent] }] + }; + var commands = new[] + { + MovementCommand("player-move", "zone.0.entity.0", 1, 0), + MovementCommand("agent-move", "zone.0.entity.1", 0, 1) + }; + var operation = AetheriaRuntimeDaemonOperations.Execute(run, commands); + + RequireEqual(2, operation.Intents.Movements.Count, "movement intent must retain one lever position per actor"); + AetheriaRuntimeDaemonSimulation.Step( + run, + operation.Intents, + 0.1, + AetheriaRuntimeDaemonSimulationSettings.AetheriaDefault, + AetheriaRuntimeProjectilePhysicsUnavailable.Instance); + Require(player.VelocityX > 0 && Math.Abs(player.VelocityY) < 0.001, + "player command must drive its actor through the shared movement lever"); + Require(agent.VelocityY > 0 && Math.Abs(agent.VelocityX) < 0.001, + "agent command must drive its actor through the shared movement lever"); + } + + private static AetheriaRuntimeDaemonCommandDocument MovementCommand( + string commandId, + string actor, + double x, + double y) + { + var command = AetheriaRuntimeDaemonCommandDocument.Create( + AetheriaRuntimeDaemonCommandKinds.SetMoveVector, + "shared-control-smoke", + "shared-control-session", + 0, + actor); + command.CommandId = commandId; + command.DirectionX = x; + command.DirectionY = y; + command.ScalarValue = 1; + return command; } private static void ThermalCellsUseFossilConductionAndRadiation() diff --git a/Assets/Scripts/Tests/DaemonRuntimeDocumentTests.cs b/Assets/Scripts/Tests/DaemonRuntimeDocumentTests.cs index 0a642066b..5699a1d21 100644 --- a/Assets/Scripts/Tests/DaemonRuntimeDocumentTests.cs +++ b/Assets/Scripts/Tests/DaemonRuntimeDocumentTests.cs @@ -529,8 +529,8 @@ public class DaemonRuntimeDocumentTests Assert.AreEqual(2, result.OperationResult.AppliedCommandIds.Count); Assert.AreEqual(0, result.OperationResult.RejectedCommandIds.Count); Assert.AreEqual(1, run.Zones[0].Entities[0].TargetEntityIndex); - Assert.AreEqual("zone.0.entity.0", result.Intents.Movement.ActorEntityKey); - Assert.AreEqual(1.0, result.Intents.Movement.DirectionX, 0.0001); + Assert.AreEqual("zone.0.entity.0", result.Intents.Movements.Single().ActorEntityKey); + Assert.AreEqual(1.0, result.Intents.Movements.Single().DirectionX, 0.0001); Assert.AreEqual(12.5, result.Run.Zones[0].SimulationTimeSeconds, 0.0001); var frame = result.Frame; Assert.AreEqual("test-daemon", frame.DaemonId); @@ -4646,10 +4646,10 @@ public class DaemonRuntimeDocumentTests Assert.AreEqual(5, result.AppliedCommandIds.Count); Assert.IsTrue(result.Intents.HasAny); - Assert.AreEqual("zone.0.entity.0", result.Intents.Movement.ActorEntityKey); - Assert.AreEqual(0.25, result.Intents.Movement.DirectionX, 0.0001); - Assert.AreEqual(-0.5, result.Intents.Movement.DirectionY, 0.0001); - Assert.AreEqual(0.75, result.Intents.Movement.Magnitude, 0.0001); + Assert.AreEqual("zone.0.entity.0", result.Intents.Movements.Single().ActorEntityKey); + Assert.AreEqual(0.25, result.Intents.Movements.Single().DirectionX, 0.0001); + Assert.AreEqual(-0.5, result.Intents.Movements.Single().DirectionY, 0.0001); + Assert.AreEqual(0.75, result.Intents.Movements.Single().Magnitude, 0.0001); Assert.AreEqual(1, result.Intents.WeaponGroups.Count); Assert.IsTrue(result.Intents.WeaponGroups[0].Fire); Assert.AreEqual(1, result.Intents.WeaponGroups[0].WeaponGroup); diff --git a/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeDaemonIntentState.cs b/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeDaemonIntentState.cs index e5a60ba09..6d0253464 100644 --- a/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeDaemonIntentState.cs +++ b/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeDaemonIntentState.cs @@ -7,7 +7,8 @@ namespace GameCult.Aetheria.State.Verse { public sealed class AetheriaRuntimeDaemonIntentState { - public AetheriaRuntimeDaemonMovementIntent? Movement { get; set; } + public List Movements { get; } = + new List(); public List WeaponGroups { get; } = new List(); public List Behaviors { get; } = @@ -23,7 +24,7 @@ namespace GameCult.Aetheria.State.Verse public bool SensorPingRequested { get; set; } public bool HasAny => - Movement != null || + Movements.Count > 0 || SensorPingRequested || WeaponGroups.Count > 0 || Behaviors.Count > 0 || diff --git a/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeDaemonOperations.cs b/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeDaemonOperations.cs index 8782e061d..f2e917563 100644 --- a/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeDaemonOperations.cs +++ b/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeDaemonOperations.cs @@ -927,13 +927,15 @@ namespace GameCult.Aetheria.State.Verse return false; } - intents.Movement = new AetheriaRuntimeDaemonMovementIntent + intents.Movements.RemoveAll(intent => + string.Equals(intent.ActorEntityKey, actor, StringComparison.Ordinal)); + intents.Movements.Add(new AetheriaRuntimeDaemonMovementIntent { ActorEntityKey = actor, DirectionX = command.DirectionX, DirectionY = command.DirectionY, Magnitude = command.ScalarValue - }; + }); return true; } diff --git a/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeDaemonSimulation.cs b/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeDaemonSimulation.cs index 10d9a9d35..a0ff34435 100644 --- a/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeDaemonSimulation.cs +++ b/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeDaemonSimulation.cs @@ -38,7 +38,8 @@ namespace GameCult.Aetheria.State.Verse EnsureStats(entities, settings); foreach (var entity in entities) AetheriaRuntimeThermalSimulation.EnsureState(entity); - ApplyMovementIntent(run, entities, intents?.Movement, settings); + foreach (var movement in intents?.Movements ?? Enumerable.Empty()) + ApplyMovementIntent(run, entities, movement, settings); StepRaiderAi(entities); StepTargetPursuit(entities, settings); StepMovement(entities, deltaSeconds); @@ -86,7 +87,7 @@ namespace GameCult.Aetheria.State.Verse return; var entity = entities.FirstOrDefault(candidate => candidate.EntityIndex == entityIndex); - if (entity == null || !IsPlayerOwned(entity) || !IsAlive(entity)) + if (entity == null || !IsAlive(entity)) return; var magnitude = Clamp01(movement.Magnitude);