diff --git a/Assets/Scripts/Gameplay/ActionBarSlot.cs b/Assets/Scripts/Gameplay/ActionBarSlot.cs index 1c51faf82..5c28357e9 100644 --- a/Assets/Scripts/Gameplay/ActionBarSlot.cs +++ b/Assets/Scripts/Gameplay/ActionBarSlot.cs @@ -75,7 +75,7 @@ public class ActionBarSlot : MonoBehaviour this, entity.Equipment[savedActionBarGearBinding.EquipmentIndex], entity.Equipment[savedActionBarGearBinding.EquipmentIndex] - .Behaviors[savedActionBarGearBinding.BehaviorIndex] as IActivatedBehavior), + .Behaviors[savedActionBarGearBinding.BehaviorIndex] as ActivatedBehavior), SavedActionBarWeaponGroupBinding savedActionBarWeaponGroupBinding => new ActionBarWeaponGroupBinding(entity, this, savedActionBarWeaponGroupBinding.Group), _ => null @@ -136,11 +136,11 @@ public class ActionBarConsumableBinding : ActionBarBinding public class ActionBarGearBinding : ActionBarBinding { public EquippedItem Item { get; } - public IActivatedBehavior Behavior { get; } + public ActivatedBehavior Behavior { get; } public bool Active; - public ActionBarGearBinding(Entity entity, ActionBarSlot slot, EquippedItem item, IActivatedBehavior behavior) : base(entity, slot) + public ActionBarGearBinding(Entity entity, ActionBarSlot slot, EquippedItem item, ActivatedBehavior behavior) : base(entity, slot) { Item = item; var data = Entity.ItemManager.GetData(item.EquippableItem); diff --git a/Assets/Scripts/Gameplay/ActionGameManager.cs b/Assets/Scripts/Gameplay/ActionGameManager.cs index 61a20308f..6fa564d92 100644 --- a/Assets/Scripts/Gameplay/ActionGameManager.cs +++ b/Assets/Scripts/Gameplay/ActionGameManager.cs @@ -396,39 +396,23 @@ public class ActionGameManager : MonoBehaviour ActionBar.gameObject.SetActive(!_uiHidden); }; - Input.Player.OverrideShutdown.performed += context => - { - CurrentEntity.OverrideShutdown = !CurrentEntity.OverrideShutdown; - }; - - Input.Player.Ping.performed += context => - { - CurrentEntity.Sensor?.Ping(); - }; - - Input.Player.ToggleHeatsinks.performed += context => - { - CurrentEntity.HeatsinksEnabled = !CurrentEntity.HeatsinksEnabled; - // TODO: SFX: Success/Fail - }; - - Input.Player.ToggleShield.performed += context => - { - if (CurrentEntity.Shield != null) - { - CurrentEntity.Shield.Item.Enabled.Value = !CurrentEntity.Shield.Item.Enabled.Value; - // TODO: SFX: Success/Fail - } - }; + // The switches and the ping are presses on the entity's command, flipped at the next step. + Input.Player.OverrideShutdown.performed += context => CurrentEntity.Command.ToggleOverrideShutdown(); + Input.Player.Ping.performed += context => CurrentEntity.Command.Ping(); + // TODO: SFX: Success/Fail + Input.Player.ToggleHeatsinks.performed += context => CurrentEntity.Command.ToggleHeatsinks(); + // TODO: SFX: Success/Fail + Input.Player.ToggleShield.performed += context => CurrentEntity.Command.ToggleShield(); // Flips the player's own stance on the current target between hostile and neutral. Going // neutral mid-fight safes weapons (Weapon.StanceAllowsFire); an unmarked/undetected target - // reads as non-hostile here, so the first press declares hostile. + // reads as non-hostile, so the first press declares hostile. The flip reads the stance when applied, so two + // presses in a step cancel. Input.Player.ToggleStance.performed += context => { var target = CurrentEntity.Target.Value.Entity; if (target == null) return; - CurrentEntity.Command.Iff(target, !CurrentEntity.IsHostileTo(target)); + CurrentEntity.Command.ToggleStance(target); }; #region Targeting @@ -483,7 +467,7 @@ public class ActionGameManager : MonoBehaviour switch (dragAction) { case EquippedItemDragObject equippedItemDragAction: - var trigger = equippedItemDragAction.EquippedItem.GetBehavior(); + var trigger = equippedItemDragAction.EquippedItem.GetBehavior(); if (trigger == null) return false; slot.Binding = new ActionBarGearBinding(CurrentEntity, slot, equippedItemDragAction.EquippedItem, trigger); return true; diff --git a/Assets/Scripts/ServerShared/Behaviors/Behaviors.cs b/Assets/Scripts/ServerShared/Behaviors/Behaviors.cs index 0cbe11186..7150e47a1 100644 --- a/Assets/Scripts/ServerShared/Behaviors/Behaviors.cs +++ b/Assets/Scripts/ServerShared/Behaviors/Behaviors.cs @@ -87,10 +87,15 @@ public abstract class Behavior } } -public interface IActivatedBehavior +// A behaviour its pilot triggers. Only PilotApplier pulls and releases it, from the pilot's PilotCommand (Pull, Release), +// so Activate and Deactivate are internal to the simulation: no script outside it triggers one in place. +public abstract class ActivatedBehavior : Behavior { - void Activate(); - void Deactivate(); + protected ActivatedBehavior(BehaviorData data, EquippedItem item) : base(data, item) { } + protected ActivatedBehavior(BehaviorData data, ConsumableItemEffect consumable) : base(data, consumable) { } + + internal abstract void Activate(); + internal abstract void Deactivate(); } public interface IEventBehavior diff --git a/Assets/Scripts/ServerShared/Behaviors/ChargedWeapon.cs b/Assets/Scripts/ServerShared/Behaviors/ChargedWeapon.cs index 8d048083d..4d2bb5647 100644 --- a/Assets/Scripts/ServerShared/Behaviors/ChargedWeapon.cs +++ b/Assets/Scripts/ServerShared/Behaviors/ChargedWeapon.cs @@ -167,7 +167,7 @@ public class ChargedWeapon : InstantWeapon return base.Execute(dt); } - public override void Activate() + internal override void Activate() { // CanFire also holds a weapon whose released charge has a burst pending: its cooldown starts at its first round, // so a charge started before then would overlap it. @@ -180,7 +180,7 @@ public class ChargedWeapon : InstantWeapon } } - public override void Deactivate() + internal override void Deactivate() { if (_charging) { diff --git a/Assets/Scripts/ServerShared/Behaviors/ConstantWeapon.cs b/Assets/Scripts/ServerShared/Behaviors/ConstantWeapon.cs index cc0015dbe..a2fc9f802 100644 --- a/Assets/Scripts/ServerShared/Behaviors/ConstantWeapon.cs +++ b/Assets/Scripts/ServerShared/Behaviors/ConstantWeapon.cs @@ -198,7 +198,7 @@ public class ConstantWeapon : Weapon, IProgressBehavior, IEventBehavior, IPowerC return true; } - public override void Activate() + internal override void Activate() { // A refused beam never starts (operator ruling 2026-09-30): OnStartFiring drives the beam's visuals and // audio, so starting it only for Execute to stop it a tick later would flash a beam that fires nothing. @@ -209,7 +209,7 @@ public class ConstantWeapon : Weapon, IProgressBehavior, IEventBehavior, IPowerC } } - public override void Deactivate() + internal override void Deactivate() { if (_firing) { diff --git a/Assets/Scripts/ServerShared/Behaviors/InstantWeapon.cs b/Assets/Scripts/ServerShared/Behaviors/InstantWeapon.cs index 043df920c..a6b3dcd2f 100644 --- a/Assets/Scripts/ServerShared/Behaviors/InstantWeapon.cs +++ b/Assets/Scripts/ServerShared/Behaviors/InstantWeapon.cs @@ -286,7 +286,7 @@ public class InstantWeapon : Weapon, IProgressBehavior, IEventBehavior, IPowerCo return true; } - public override void Activate() + internal override void Activate() { if(CanFire) Trigger(); diff --git a/Assets/Scripts/ServerShared/Behaviors/Sensor.cs b/Assets/Scripts/ServerShared/Behaviors/Sensor.cs index cf9d161f0..d98437e59 100644 --- a/Assets/Scripts/ServerShared/Behaviors/Sensor.cs +++ b/Assets/Scripts/ServerShared/Behaviors/Sensor.cs @@ -136,7 +136,7 @@ public class Sensor : Behavior, IEventBehavior, IPowerConsumer // Cut 5 (docs/stats-and-power-cut.md ยง1.3, PowerTiers.cs): Utility -- everything else. public int DefaultPowerTier => PowerTiers.Utility; - public void Ping() + internal void Ping() { // Whole-or-nothing against the ping's own input capacitor -- fires only at full charge (operator // ruling: no fraction of a shot). A consumable-hosted instance (Item == null) has no PowerBus entry, so diff --git a/Assets/Scripts/ServerShared/Behaviors/Switch.cs b/Assets/Scripts/ServerShared/Behaviors/Switch.cs index 47990c4b2..dc5711e2f 100644 --- a/Assets/Scripts/ServerShared/Behaviors/Switch.cs +++ b/Assets/Scripts/ServerShared/Behaviors/Switch.cs @@ -20,11 +20,11 @@ public class SwitchData : BehaviorData } } -public class Switch : Behavior, IActivatedBehavior +public class Switch : ActivatedBehavior { private SwitchData _data; - public bool Activated { get; set; } + public bool Activated { get; private set; } public Switch(SwitchData data, EquippedItem item) : base(data, item) { @@ -40,12 +40,12 @@ public class Switch : Behavior, IActivatedBehavior return Activated; } - public void Activate() + internal override void Activate() { Activated = true; } - public void Deactivate() + internal override void Deactivate() { Activated = false; } diff --git a/Assets/Scripts/ServerShared/Behaviors/Trigger.cs b/Assets/Scripts/ServerShared/Behaviors/Trigger.cs index be7c9b9dc..ddc58ada9 100644 --- a/Assets/Scripts/ServerShared/Behaviors/Trigger.cs +++ b/Assets/Scripts/ServerShared/Behaviors/Trigger.cs @@ -20,9 +20,9 @@ public class TriggerData : BehaviorData } } -public class Trigger : Behavior, IActivatedBehavior +public class Trigger : ActivatedBehavior { - public bool _pulled; + private bool _pulled; public Trigger(TriggerData data, EquippedItem item) : base(data, item) { } public Trigger(TriggerData data, ConsumableItemEffect item) : base(data, item) { } @@ -38,12 +38,12 @@ public class Trigger : Behavior, IActivatedBehavior return false; } - public void Activate() + internal override void Activate() { _pulled = true; } - public void Deactivate() + internal override void Deactivate() { } } \ No newline at end of file diff --git a/Assets/Scripts/ServerShared/Behaviors/Weapon.cs b/Assets/Scripts/ServerShared/Behaviors/Weapon.cs index 6a01f81ca..12166d46c 100644 --- a/Assets/Scripts/ServerShared/Behaviors/Weapon.cs +++ b/Assets/Scripts/ServerShared/Behaviors/Weapon.cs @@ -100,7 +100,7 @@ public abstract class WeaponData : BehaviorData public float RatedRange => Range.Max; } -public abstract class Weapon : Behavior, IActivatedBehavior +public abstract class Weapon : ActivatedBehavior { private WeaponData _data; @@ -179,12 +179,12 @@ public abstract class Weapon : Behavior, IActivatedBehavior return true; } - public virtual void Activate() + internal override void Activate() { _firing = true; } - public virtual void Deactivate() + internal override void Deactivate() { _firing = false; } diff --git a/Assets/Scripts/ServerShared/Entity.cs b/Assets/Scripts/ServerShared/Entity.cs index 42b30cf4f..6b1eab8c9 100644 --- a/Assets/Scripts/ServerShared/Entity.cs +++ b/Assets/Scripts/ServerShared/Entity.cs @@ -173,7 +173,7 @@ public abstract class Entity public bool HeatsinksEnabled { get => _heatsinksEnabled; - set + internal set { if (value == _heatsinksEnabled) return; _heatsinksEnabled = value; @@ -186,7 +186,7 @@ public abstract class Entity public EntitySettings Settings { get; } - public bool OverrideShutdown { get; set; } + public bool OverrideShutdown { get; internal set; } // The pilot's tractor command (0..1); the Pickup behaviour spins its beam toward it. public float TractorCommand { get; internal set; } @@ -2170,7 +2170,7 @@ public class EquippedItem : IStatContext public ReactiveProperty ThermalOnline { get; } = new ReactiveProperty(false); public ReactiveProperty DurabilityOnline { get; } = new ReactiveProperty(false); public ReadOnlyReactiveProperty Online { get; } - public ReactiveProperty Enabled { get; } = new ReactiveProperty(true); + internal ReactiveProperty Enabled { get; } = new ReactiveProperty(true); public ReadOnlyReactiveProperty Active { get; } public ItemManager ItemManager { get; } diff --git a/Assets/Scripts/ServerShared/Pilot.cs b/Assets/Scripts/ServerShared/Pilot.cs index be1e9cd7e..576191b7d 100644 --- a/Assets/Scripts/ServerShared/Pilot.cs +++ b/Assets/Scripts/ServerShared/Pilot.cs @@ -53,27 +53,35 @@ public sealed class PilotCommand internal readonly List Selections = new List(); internal readonly List Hails = new List(); internal readonly List Uses = new List(); - internal readonly List<(IActivatedBehavior Behavior, bool Pull)> Triggers = new List<(IActivatedBehavior, bool)>(); - internal readonly List<(Entity Other, bool? Hostile)> Iffs = new List<(Entity, bool?)>(); + internal readonly List<(ActivatedBehavior Behavior, bool Pull)> Triggers = new List<(ActivatedBehavior, bool)>(); + internal readonly List<(Entity Other, bool? Hostile, bool Toggle)> Iffs = new List<(Entity, bool?, bool)>(); internal TargetVerb TargetVerb; internal int AimCycles; + internal int ShutdownOverrideToggles, HeatsinkToggles, ShieldToggles, Pings; public void Designate(TargetRef target) => Designations.Add(target); public void SelectItem(EquippedItem item) => Selections.Add(item); public void Hail(string message) => Hails.Add(message); public void Use(ConsumableItemData consumable) => Uses.Add(consumable); - public void Pull(IActivatedBehavior behavior) => Triggers.Add((behavior, true)); - public void Release(IActivatedBehavior behavior) => Triggers.Add((behavior, false)); + public void Pull(ActivatedBehavior behavior) => Triggers.Add((behavior, true)); + public void Release(ActivatedBehavior behavior) => Triggers.Add((behavior, false)); // The last verb in a step wins. public void Verb(TargetVerb verb) => TargetVerb = verb; public void CycleAim() => AimCycles++; - public void Iff(Entity other, bool? hostile) => Iffs.Add((other, hostile)); + public void Iff(Entity other, bool? hostile) => Iffs.Add((other, hostile, false)); + // A press that flips the stance on `other` as it stands when applied, in order with Iff, so two in a step cancel. + public void ToggleStance(Entity other) => Iffs.Add((other, null, true)); + // The entity's own switches, pressed: each press flips its switch when applied, so two in a step cancel. + public void ToggleOverrideShutdown() => ShutdownOverrideToggles++; + public void ToggleHeatsinks() => HeatsinkToggles++; + public void ToggleShield() => ShieldToggles++; + public void Ping() => Pings++; // The pilot's own pending decisions, which it reads back instead of the ship (that is last step's applied state). public TargetRef? Designated => Designations.Count > 0 ? Designations[Designations.Count - 1] : (TargetRef?) null; // Whether the last trigger this pilot commanded for `behavior` this step is a pull. - public bool Pulls(IActivatedBehavior behavior) + public bool Pulls(ActivatedBehavior behavior) { for (var i = Triggers.Count - 1; i >= 0; i--) if (ReferenceEquals(Triggers[i].Behavior, behavior)) return Triggers[i].Pull; @@ -90,6 +98,7 @@ public sealed class PilotCommand Iffs.Clear(); TargetVerb = TargetVerb.None; AimCycles = 0; + ShutdownOverrideToggles = HeatsinkToggles = ShieldToggles = Pings = 0; } } @@ -112,7 +121,12 @@ public static class PilotApplier } foreach (var item in command.Selections) entity.TrySelectTargetItem(item); for (var i = 0; i < command.AimCycles; i++) entity.CycleAim(); - foreach (var (other, hostile) in command.Iffs) entity.SetIff(other, hostile); + foreach (var (other, hostile, toggle) in command.Iffs) entity.SetIff(other, toggle ? !entity.IsHostileTo(other) : hostile); + for (var i = 0; i < command.ShutdownOverrideToggles; i++) entity.OverrideShutdown = !entity.OverrideShutdown; + for (var i = 0; i < command.HeatsinkToggles; i++) entity.HeatsinksEnabled = !entity.HeatsinksEnabled; + if (entity.Shield != null) + for (var i = 0; i < command.ShieldToggles; i++) entity.Shield.Item.Enabled.Value = !entity.Shield.Item.Enabled.Value; + for (var i = 0; i < command.Pings; i++) entity.Sensor?.Ping(); foreach (var hail in command.Hails) entity.SetMessage(hail); foreach (var consumable in command.Uses) entity.TryActivateConsumable(consumable); if (command.Aim is float3 aim) entity.Aim = aim; diff --git a/docs/headless-playground-cut.md b/docs/headless-playground-cut.md index 8e52bdaf1..bea30aa46 100644 --- a/docs/headless-playground-cut.md +++ b/docs/headless-playground-cut.md @@ -72,7 +72,7 @@ combat state cannot run without Unity.** - Player-only inputs, all plain entity writes that an agent could make: - `TractorPower` (`ActionGameManager.cs:1214-1216`); - `OverrideShutdown`, `HeatsinksEnabled`, shield `Enabled`, `Sensor.Ping` (`:339-362`); - - gear `IActivatedBehavior` (`ActionBarSlot.cs:159-168`); + - gear `ActivatedBehavior` (`ActionBarSlot.cs:159-168`); - consumables (`ActionBarSlot.cs:119-122`, `Entity.TryActivateConsumable`). - Player-only lifecycle actions. These are the orchestration this map moves: - interact, which means warp, dock or undock (`ActionGameManager.cs:298-320`); @@ -1002,7 +1002,7 @@ mean we want to delete all the weapon types we created". | `move ` / `look ` / `tractor <0..1>` | pilot entity fields, as `ActionGameManager.cs:1189-1216` | | `target nearest\|none\|` | `Pilot.Target.Value` | | `fire on\|off` | `WeaponGroups[g].weapons` Activate/Deactivate, as `ActionBarSlot.cs:190-204` | -| `gear on\|off` | `IActivatedBehavior` Activate/Deactivate | +| `gear on\|off` | `ActivatedBehavior` Activate/Deactivate | | `pilot manual\|minion\|patrol\|goto station \|goto wormhole ` | `Run.PilotAgent` | | `dock` / `undock` / `board ` / `warp` / `tow` | `Run.TryDock` / `Undock` / `Board` / `TryEnterWormhole` / `Tow` | | `hit [hardpoint ]` | `Entity.ApplyHit` (Cut 2) | diff --git a/tests/Aetheria.Shared.Tests/PilotSeamPlayerTests.cs b/tests/Aetheria.Shared.Tests/PilotSeamPlayerTests.cs index e656c1212..afb95836f 100644 --- a/tests/Aetheria.Shared.Tests/PilotSeamPlayerTests.cs +++ b/tests/Aetheria.Shared.Tests/PilotSeamPlayerTests.cs @@ -287,11 +287,12 @@ public sealed partial class RunStartTests } // A trigger that counts its edges: a semi-auto fires once per Activate. - private sealed class SeamTrigger : IActivatedBehavior + private sealed class SeamTrigger : ActivatedBehavior { public int Pulled, Released; - public void Activate() => Pulled++; - public void Deactivate() => Released++; + public SeamTrigger(EquippedItem item) : base(null, item) { } + internal override void Activate() => Pulled++; + internal override void Deactivate() => Released++; } // A press is one pull at the next step and the button held after it pulls nothing more, so a semi-auto fires once; @@ -300,7 +301,7 @@ public sealed partial class RunStartTests public void PlayerTriggerIsAnEdge() { var (arena, east, _) = SeamPair(false); - var trigger = new SeamTrigger(); + var trigger = new SeamTrigger(SeamGun(east).Item); east.Command.Pull(trigger); Assert.Equal(0, trigger.Pulled); for (var step = 0; step < 10; step++) arena.Update(SeamDt);