]> git.smokeofanarchy.ru Git - space-station-14.git/commitdiff
Sentry turrets - Part 4: The sentry turret and its primary systems (#35123)
authorchromiumboy <50505512+chromiumboy@users.noreply.github.com>
Sat, 29 Mar 2025 17:55:58 +0000 (12:55 -0500)
committerGitHub <noreply@github.com>
Sat, 29 Mar 2025 17:55:58 +0000 (12:55 -0500)
* Initial commit

* Removed mention of StationAiTurretComponent (for now)

* Prep for moving out of draft

* Fixing merge conflict

* Re-added new net frequencies to AI turrets

* Removed turret control content

* Removed unintended change

* Final tweaks

* Fixed incorrect file name

* Improvement to fire mode handling

* Addressed review comments

* Updated how turret wire panel auto-closing is handled

* Ranged NPCs no longer waste shots on stunned targets

* Fixed bug in tracking broken state

* Addressed review comments

* Bug fix

* Removed unnecessary event call

24 files changed:
Content.Client/Turrets/DeployableTurretSystem.cs [new file with mode: 0644]
Content.Server/Destructible/DestructibleComponent.cs
Content.Server/Destructible/DestructibleSystem.cs
Content.Server/NPC/Queries/Considerations/TargetIsStunnedCon.cs [new file with mode: 0644]
Content.Server/NPC/Systems/NPCUtilitySystem.cs
Content.Server/Turrets/DeployableTurretSystem.cs [new file with mode: 0644]
Content.Shared/Damage/Systems/DamageableSystem.cs
Content.Shared/Power/Components/ApcPowerReceiverBatteryComponent.cs [moved from Content.Shared/Power/Components/ApcPowerReceiverBatteryChangedEvent.cs with 100% similarity]
Content.Shared/Turrets/DeployableTurretComponent.cs [new file with mode: 0644]
Content.Shared/Turrets/SharedDeployableTurretSystem.cs [new file with mode: 0644]
Content.Shared/Weapons/Ranged/Components/BatteryWeaponFireModesComponent.cs
Content.Shared/Weapons/Ranged/Systems/BatteryWeaponFireModesSystem.cs
Resources/Locale/en-US/construction/steps/arbitrary-insert-construction-graph-step.ftl
Resources/Locale/en-US/devices/device-network.ftl
Resources/Locale/en-US/weapons/ranged/turrets.ftl [new file with mode: 0644]
Resources/Locale/en-US/wires/wire-names.ftl
Resources/Prototypes/Device/devicenet_frequencies.yml
Resources/Prototypes/Entities/Objects/Devices/Circuitboards/Machine/turrets.yml [new file with mode: 0644]
Resources/Prototypes/Entities/Objects/Weapons/Guns/Battery/battery_guns.yml
Resources/Prototypes/Entities/Objects/Weapons/Guns/Turrets/turrets_base.yml
Resources/Prototypes/Entities/Objects/Weapons/Guns/Turrets/turrets_energy.yml [new file with mode: 0644]
Resources/Prototypes/NPCs/utility_queries.yml
Resources/Prototypes/Wires/layouts.yml
Resources/Prototypes/tags.yml

diff --git a/Content.Client/Turrets/DeployableTurretSystem.cs b/Content.Client/Turrets/DeployableTurretSystem.cs
new file mode 100644 (file)
index 0000000..a839974
--- /dev/null
@@ -0,0 +1,121 @@
+using Content.Client.Power;
+using Content.Shared.Turrets;
+using Robust.Client.Animations;
+using Robust.Client.GameObjects;
+
+namespace Content.Client.Turrets;
+
+public sealed partial class DeployableTurretSystem : SharedDeployableTurretSystem
+{
+    [Dependency] private readonly AppearanceSystem _appearance = default!;
+    [Dependency] private readonly AnimationPlayerSystem _animation = default!;
+
+    public override void Initialize()
+    {
+        base.Initialize();
+
+        SubscribeLocalEvent<DeployableTurretComponent, ComponentInit>(OnComponentInit);
+        SubscribeLocalEvent<DeployableTurretComponent, AnimationCompletedEvent>(OnAnimationCompleted);
+        SubscribeLocalEvent<DeployableTurretComponent, AppearanceChangeEvent>(OnAppearanceChange);
+    }
+
+    private void OnComponentInit(Entity<DeployableTurretComponent> ent, ref ComponentInit args)
+    {
+        ent.Comp.DeploymentAnimation = new Animation
+        {
+            Length = TimeSpan.FromSeconds(ent.Comp.DeploymentLength),
+            AnimationTracks = {
+                new AnimationTrackSpriteFlick() {
+                    LayerKey = DeployableTurretVisuals.Turret,
+                    KeyFrames = {new AnimationTrackSpriteFlick.KeyFrame(ent.Comp.DeployingState, 0f)}
+                },
+            }
+        };
+
+        ent.Comp.RetractionAnimation = new Animation
+        {
+            Length = TimeSpan.FromSeconds(ent.Comp.RetractionLength),
+            AnimationTracks = {
+                new AnimationTrackSpriteFlick() {
+                    LayerKey = DeployableTurretVisuals.Turret,
+                    KeyFrames = {new AnimationTrackSpriteFlick.KeyFrame(ent.Comp.RetractingState, 0f)}
+                },
+            }
+        };
+    }
+
+    private void OnAnimationCompleted(Entity<DeployableTurretComponent> ent, ref AnimationCompletedEvent args)
+    {
+        if (args.Key != DeployableTurretComponent.AnimationKey)
+            return;
+
+        if (!TryComp<SpriteComponent>(ent, out var sprite))
+            return;
+
+        if (!_appearance.TryGetData<DeployableTurretState>(ent, DeployableTurretVisuals.Turret, out var state))
+            state = ent.Comp.VisualState;
+
+        // Convert to terminal state
+        var targetState = state & DeployableTurretState.Deployed;
+
+        UpdateVisuals(ent, targetState, sprite, args.AnimationPlayer);
+    }
+
+    private void OnAppearanceChange(Entity<DeployableTurretComponent> ent, ref AppearanceChangeEvent args)
+    {
+        if (args.Sprite == null)
+            return;
+
+        if (!TryComp<AnimationPlayerComponent>(ent, out var animPlayer))
+            return;
+
+        if (!_appearance.TryGetData<DeployableTurretState>(ent, DeployableTurretVisuals.Turret, out var state, args.Component))
+            state = DeployableTurretState.Retracted;
+
+        UpdateVisuals(ent, state, args.Sprite, animPlayer);
+    }
+
+    private void UpdateVisuals(Entity<DeployableTurretComponent> ent, DeployableTurretState state, SpriteComponent sprite, AnimationPlayerComponent? animPlayer = null)
+    {
+        if (!Resolve(ent, ref animPlayer))
+            return;
+
+        if (_animation.HasRunningAnimation(ent, animPlayer, DeployableTurretComponent.AnimationKey))
+            return;
+
+        if (state == ent.Comp.VisualState)
+            return;
+
+        var targetState = state & DeployableTurretState.Deployed;
+        var destinationState = ent.Comp.VisualState & DeployableTurretState.Deployed;
+
+        if (targetState != destinationState)
+            targetState = targetState | DeployableTurretState.Retracting;
+
+        ent.Comp.VisualState = state;
+
+        // Toggle layer visibility
+        sprite.LayerSetVisible(DeployableTurretVisuals.Weapon, (targetState & DeployableTurretState.Deployed) > 0);
+        sprite.LayerSetVisible(PowerDeviceVisualLayers.Powered, HasAmmo(ent) && targetState == DeployableTurretState.Retracted);
+
+        // Change the visual state
+        switch (targetState)
+        {
+            case DeployableTurretState.Deploying:
+                _animation.Play((ent, animPlayer), (Animation)ent.Comp.DeploymentAnimation, DeployableTurretComponent.AnimationKey);
+                break;
+
+            case DeployableTurretState.Retracting:
+                _animation.Play((ent, animPlayer), (Animation)ent.Comp.RetractionAnimation, DeployableTurretComponent.AnimationKey);
+                break;
+
+            case DeployableTurretState.Deployed:
+                sprite.LayerSetState(DeployableTurretVisuals.Turret, ent.Comp.DeployedState);
+                break;
+
+            case DeployableTurretState.Retracted:
+                sprite.LayerSetState(DeployableTurretVisuals.Turret, ent.Comp.RetractedState);
+                break;
+        }
+    }
+}
index 5c593fb083c102b3e1f9582a2a733cc6a53d814c..d154811c78e26fee0ab060b14c4fb4b01e8eb5f4 100644 (file)
@@ -9,8 +9,17 @@ namespace Content.Server.Destructible
     [RegisterComponent]
     public sealed partial class DestructibleComponent : Component
     {
-        [DataField("thresholds")]
+        /// <summary>
+        /// A list of damage thresholds for the entity;
+        /// includes their triggers and resultant behaviors
+        /// </summary>
+        [DataField]
         public List<DamageThreshold> Thresholds = new();
 
+        /// <summary>
+        /// Specifies whether the entity has passed a damage threshold that causes it to break
+        /// </summary>
+        [DataField]
+        public bool IsBroken = false;
     }
 }
index 48b38e9d0194d3463894f3bff3471152baae1a5a..ca7f975e608f377df9e43593cbdd5d6cc18bddd1 100644 (file)
@@ -57,6 +57,8 @@ namespace Content.Server.Destructible
         /// </summary>
         public void Execute(EntityUid uid, DestructibleComponent component, DamageChangedEvent args)
         {
+            component.IsBroken = false;
+
             foreach (var threshold in component.Thresholds)
             {
                 if (threshold.Reached(args.Damageable, this))
@@ -96,6 +98,12 @@ namespace Content.Server.Destructible
                     threshold.Execute(uid, this, EntityManager, args.Origin);
                 }
 
+                if (threshold.OldTriggered)
+                {
+                    component.IsBroken |= threshold.Behaviors.Any(b => b is DoActsBehavior doActsBehavior &&
+                        (doActsBehavior.HasAct(ThresholdActs.Breakage) || doActsBehavior.HasAct(ThresholdActs.Destruction)));
+                }
+
                 // if destruction behavior (or some other deletion effect) occurred, don't run other triggers.
                 if (EntityManager.IsQueuedForDeletion(uid) || Deleted(uid))
                     return;
diff --git a/Content.Server/NPC/Queries/Considerations/TargetIsStunnedCon.cs b/Content.Server/NPC/Queries/Considerations/TargetIsStunnedCon.cs
new file mode 100644 (file)
index 0000000..6188ae9
--- /dev/null
@@ -0,0 +1,10 @@
+namespace Content.Server.NPC.Queries.Considerations;
+
+/// <summary>
+/// Returns 1f if the target has the <see cref="StunnedComponent"/>
+/// </summary>
+public sealed partial class TargetIsStunnedCon : UtilityConsideration
+{
+
+}
+
index b5d3ac3cbde377434e9b9dea96a5c6bdb250366f..eff4f2772b44f0305acc756216a49380993af6ed 100644 (file)
@@ -19,6 +19,7 @@ using Content.Shared.Mobs.Systems;
 using Content.Shared.NPC.Systems;
 using Content.Shared.Nutrition.Components;
 using Content.Shared.Nutrition.EntitySystems;
+using Content.Shared.Stunnable;
 using Content.Shared.Tools.Systems;
 using Content.Shared.Turrets;
 using Content.Shared.Weapons.Melee;
@@ -360,6 +361,10 @@ public sealed class NPCUtilitySystem : EntitySystem
                         return 1f;
                     return 0f;
                 }
+            case TargetIsStunnedCon:
+                {
+                    return HasComp<StunnedComponent>(targetUid) ? 1f : 0f;
+                }
             case TurretTargetingCon:
                 {
                     if (!TryComp<TurretTargetSettingsComponent>(owner, out var turretTargetSettings) ||
diff --git a/Content.Server/Turrets/DeployableTurretSystem.cs b/Content.Server/Turrets/DeployableTurretSystem.cs
new file mode 100644 (file)
index 0000000..359d91f
--- /dev/null
@@ -0,0 +1,175 @@
+using Content.Server.Destructible;
+using Content.Server.DeviceNetwork;
+using Content.Server.DeviceNetwork.Components;
+using Content.Server.DeviceNetwork.Systems;
+using Content.Server.NPC.HTN;
+using Content.Server.NPC.HTN.PrimitiveTasks.Operators.Combat.Ranged;
+using Content.Server.Power.Components;
+using Content.Server.Repairable;
+using Content.Shared.Destructible;
+using Content.Shared.DeviceNetwork;
+using Content.Shared.Power;
+using Content.Shared.Turrets;
+using Content.Shared.Weapons.Ranged.Events;
+using Robust.Shared.Audio;
+using Robust.Shared.Audio.Systems;
+using Robust.Shared.Timing;
+
+namespace Content.Server.Turrets;
+
+public sealed partial class DeployableTurretSystem : SharedDeployableTurretSystem
+{
+    [Dependency] private readonly HTNSystem _htn = default!;
+    [Dependency] private readonly SharedAppearanceSystem _appearance = default!;
+    [Dependency] private readonly SharedAudioSystem _audio = default!;
+    [Dependency] private readonly DeviceNetworkSystem _deviceNetwork = default!;
+    [Dependency] private readonly IGameTiming _timing = default!;
+
+    public override void Initialize()
+    {
+        base.Initialize();
+
+        SubscribeLocalEvent<DeployableTurretComponent, AmmoShotEvent>(OnAmmoShot);
+        SubscribeLocalEvent<DeployableTurretComponent, ChargeChangedEvent>(OnChargeChanged);
+        SubscribeLocalEvent<DeployableTurretComponent, PowerChangedEvent>(OnPowerChanged);
+        SubscribeLocalEvent<DeployableTurretComponent, BreakageEventArgs>(OnBroken);
+        SubscribeLocalEvent<DeployableTurretComponent, RepairedEvent>(OnRepaired);
+        SubscribeLocalEvent<DeployableTurretComponent, BeforeBroadcastAttemptEvent>(OnBeforeBroadcast);
+    }
+
+    private void OnAmmoShot(Entity<DeployableTurretComponent> ent, ref AmmoShotEvent args)
+    {
+        UpdateAmmoStatus(ent);
+    }
+
+    private void OnChargeChanged(Entity<DeployableTurretComponent> ent, ref ChargeChangedEvent args)
+    {
+        UpdateAmmoStatus(ent);
+    }
+
+    private void OnPowerChanged(Entity<DeployableTurretComponent> ent, ref PowerChangedEvent args)
+    {
+        UpdateAmmoStatus(ent);
+    }
+
+    private void OnBroken(Entity<DeployableTurretComponent> ent, ref BreakageEventArgs args)
+    {
+        if (TryComp<AppearanceComponent>(ent, out var appearance))
+            _appearance.SetData(ent, DeployableTurretVisuals.Broken, true, appearance);
+
+        SetState(ent, false);
+    }
+
+    private void OnRepaired(Entity<DeployableTurretComponent> ent, ref RepairedEvent args)
+    {
+        if (TryComp<AppearanceComponent>(ent, out var appearance))
+            _appearance.SetData(ent, DeployableTurretVisuals.Broken, false, appearance);
+    }
+
+    private void OnBeforeBroadcast(Entity<DeployableTurretComponent> ent, ref BeforeBroadcastAttemptEvent args)
+    {
+        if (!TryComp<DeviceNetworkComponent>(ent, out var deviceNetwork))
+            return;
+
+        var recipientDeviceNetworks = new HashSet<DeviceNetworkComponent>();
+
+        // Only broadcast to connected devices
+        foreach (var recipient in deviceNetwork.DeviceLists)
+        {
+            if (!TryComp<DeviceNetworkComponent>(recipient, out var recipientDeviceNetwork))
+                continue;
+
+            recipientDeviceNetworks.Add(recipientDeviceNetwork);
+        }
+
+        if (recipientDeviceNetworks.Count > 0)
+            args.ModifiedRecipients = recipientDeviceNetworks;
+    }
+
+    private void SendStateUpdateToDeviceNetwork(Entity<DeployableTurretComponent> ent)
+    {
+        if (!TryComp<DeviceNetworkComponent>(ent, out var device))
+            return;
+
+        var payload = new NetworkPayload
+        {
+            [DeviceNetworkConstants.Command] = DeviceNetworkConstants.CmdUpdatedState,
+            [DeviceNetworkConstants.CmdUpdatedState] = GetTurretState(ent)
+        };
+
+        _deviceNetwork.QueuePacket(ent, null, payload, device: device);
+    }
+
+    protected override void SetState(Entity<DeployableTurretComponent> ent, bool enabled, EntityUid? user = null)
+    {
+        if (ent.Comp.Enabled == enabled)
+            return;
+
+        base.SetState(ent, enabled, user);
+        DirtyField(ent, ent.Comp, nameof(DeployableTurretComponent.Enabled));
+
+        // Determine how much time is remaining in the current animation and the one next in queue
+        var animTimeRemaining = MathF.Max((float)(ent.Comp.AnimationCompletionTime - _timing.CurTime).TotalSeconds, 0f);
+        var animTimeNext = ent.Comp.Enabled ? ent.Comp.DeploymentLength : ent.Comp.RetractionLength;
+
+        // End/restart any tasks the NPC was doing
+        // Delay the resumption of any tasks based on the total animation length (plus a buffer)
+        var planCooldown = animTimeRemaining + animTimeNext + 0.5f;
+
+        if (TryComp<HTNComponent>(ent, out var htn))
+            _htn.SetHTNEnabled((ent, htn), ent.Comp.Enabled, planCooldown);
+
+        // Play audio
+        _audio.PlayPvs(ent.Comp.Enabled ? ent.Comp.DeploymentSound : ent.Comp.RetractionSound, ent, new AudioParams { Volume = -10f });
+    }
+
+    private void UpdateAmmoStatus(Entity<DeployableTurretComponent> ent)
+    {
+        if (!HasAmmo(ent))
+            SetState(ent, false);
+    }
+
+    private DeployableTurretState GetTurretState(Entity<DeployableTurretComponent> ent, DestructibleComponent? destructable = null, HTNComponent? htn = null)
+    {
+        Resolve(ent, ref destructable, ref htn);
+
+        if (destructable?.IsBroken == true)
+            return DeployableTurretState.Broken;
+
+        if (htn == null || !HasAmmo(ent))
+            return DeployableTurretState.Disabled;
+
+        if (htn.Plan?.CurrentTask.Operator is GunOperator)
+            return DeployableTurretState.Firing;
+
+        if (ent.Comp.AnimationCompletionTime > _timing.CurTime)
+            return ent.Comp.Enabled ? DeployableTurretState.Deploying : DeployableTurretState.Retracting;
+
+        return ent.Comp.Enabled ? DeployableTurretState.Deployed : DeployableTurretState.Retracted;
+    }
+
+    public override void Update(float frameTime)
+    {
+        base.Update(frameTime);
+
+        var query = EntityQueryEnumerator<DeployableTurretComponent, DestructibleComponent, HTNComponent>();
+        while (query.MoveNext(out var uid, out var deployableTurret, out var destructible, out var htn))
+        {
+            // Check if the turret state has changed since the last update,
+            // and if it has, inform the device network
+            var ent = new Entity<DeployableTurretComponent>(uid, deployableTurret);
+            var newState = GetTurretState(ent, destructible, htn);
+
+            if (newState != deployableTurret.CurrentState)
+            {
+                deployableTurret.CurrentState = newState;
+                DirtyField(uid, deployableTurret, nameof(DeployableTurretComponent.CurrentState));
+
+                SendStateUpdateToDeviceNetwork(ent);
+
+                if (TryComp<AppearanceComponent>(ent, out var appearance))
+                    _appearance.SetData(ent, DeployableTurretVisuals.Turret, newState, appearance);
+            }
+        }
+    }
+}
index 8557e5623fdc35a5b4f568f41b90caa90883a222..fb55a6184ee335ed8af27b279d644788d90661d6 100644 (file)
@@ -296,7 +296,7 @@ namespace Content.Shared.Damage
             DamageChanged(uid, component, new DamageSpecifier());
         }
 
-        public void SetDamageModifierSetId(EntityUid uid, string damageModifierSetId, DamageableComponent? comp = null)
+        public void SetDamageModifierSetId(EntityUid uid, string? damageModifierSetId, DamageableComponent? comp = null)
         {
             if (!_damageableQuery.Resolve(uid, ref comp))
                 return;
diff --git a/Content.Shared/Turrets/DeployableTurretComponent.cs b/Content.Shared/Turrets/DeployableTurretComponent.cs
new file mode 100644 (file)
index 0000000..a23b4ec
--- /dev/null
@@ -0,0 +1,161 @@
+using Content.Shared.Damage.Prototypes;
+using Robust.Shared.Audio;
+using Robust.Shared.GameStates;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Serialization;
+
+namespace Content.Shared.Turrets;
+
+/// <summary>
+/// Attached to turrets that can be toggled between an inactive and active state
+/// </summary>
+[RegisterComponent, NetworkedComponent, AutoGenerateComponentState(fieldDeltas: true), AutoGenerateComponentPause]
+[Access(typeof(SharedDeployableTurretSystem))]
+public sealed partial class DeployableTurretComponent : Component
+{
+    /// <summary>
+    /// Whether the turret is toggled 'on' or 'off'
+    /// </summary>
+    [DataField, AutoNetworkedField]
+    public bool Enabled = false;
+
+    /// <summary>
+    /// The current state of the turret. Used to inform the device network. 
+    /// </summary>
+    [DataField, AutoNetworkedField]
+    public DeployableTurretState CurrentState = DeployableTurretState.Retracted;
+
+    /// <summary>
+    /// The visual state of the turret. Used on the client-side. 
+    /// </summary>
+    [DataField]
+    public DeployableTurretState VisualState = DeployableTurretState.Retracted;
+
+    /// <summary>
+    /// The physics fixture that will have its collisions disabled when the turret is retracted.
+    /// </summary>
+    [DataField]
+    public string? DeployedFixture = "turret";
+
+    /// <summary>
+    /// When retracted, the following damage modifier set will be applied to the turret.
+    /// </summary>
+    [DataField]
+    public ProtoId<DamageModifierSetPrototype>? RetractedDamageModifierSetId;
+
+    /// <summary>
+    /// When deployed, the following damage modifier set will be applied to the turret.
+    /// </summary>
+    [DataField]
+    public ProtoId<DamageModifierSetPrototype>? DeployedDamageModifierSetId;
+
+    #region: Sound data
+
+    /// <summary>
+    /// Sound to play when denied access to the turret.
+    /// </summary>
+    [DataField]
+    public SoundSpecifier AccessDeniedSound = new SoundPathSpecifier("/Audio/Machines/custom_deny.ogg");
+
+    /// <summary>
+    /// Sound to play when the turret deploys.
+    /// </summary>
+    [DataField]
+    public SoundSpecifier DeploymentSound = new SoundPathSpecifier("/Audio/Machines/blastdoor.ogg");
+
+    /// <summary>
+    /// Sound to play when the turret retracts.
+    /// </summary>
+    [DataField]
+    public SoundSpecifier RetractionSound = new SoundPathSpecifier("/Audio/Machines/blastdoor.ogg");
+
+    #endregion
+
+    #region: Animation data
+
+    /// <summary>
+    /// The length of the deployment animation (in seconds)
+    /// </summary>
+    [DataField]
+    public float DeploymentLength = 1.19f;
+
+    /// <summary>
+    /// The length of the retraction animation (in seconds)
+    /// </summary>
+    [DataField]
+    public float RetractionLength = 1.19f;
+
+    /// <summary>
+    /// The time that the current animation should complete (in seconds)
+    /// </summary>
+    [DataField, AutoPausedField]
+    public TimeSpan AnimationCompletionTime = TimeSpan.Zero;
+
+    /// <summary>
+    /// The animation used when turret activates
+    /// </summary>
+    [ViewVariables(VVAccess.ReadWrite)]
+    public object DeploymentAnimation = default!;
+
+    /// <summary>
+    /// The animation used when turret deactivates
+    /// </summary>
+    [ViewVariables(VVAccess.ReadWrite)]
+    public object RetractionAnimation = default!;
+
+    /// <summary>
+    /// The key used to index the animation played when turning the turret on/off.
+    /// </summary>
+    [ViewVariables(VVAccess.ReadOnly)]
+    public const string AnimationKey = "deployable_turret_animation";
+
+    #endregion
+
+    #region: Visual state data
+
+    /// <summary>
+    /// The visual state to use when the turret is deployed.
+    /// </summary>
+    [DataField]
+    public string DeployedState = "cover_open";
+
+    /// <summary>
+    /// The visual state to use when the turret is not deployed.
+    /// </summary>
+    [DataField]
+    public string RetractedState = "cover_closed";
+
+    /// <summary>
+    /// Used to build the deployment animation when the component is initialized.
+    /// </summary>
+    [DataField]
+    public string DeployingState = "cover_opening";
+
+    /// <summary>
+    /// Used to build the retraction animation when the component is initialized.
+    /// </summary>
+    [DataField]
+    public string RetractingState = "cover_closing";
+
+    #endregion
+}
+
+[Serializable, NetSerializable]
+public enum DeployableTurretVisuals : byte
+{
+    Turret,
+    Weapon,
+    Broken,
+}
+
+[Serializable, NetSerializable]
+public enum DeployableTurretState : byte
+{
+    Retracted = 0,
+    Deployed = (1 << 0),
+    Retracting = (1 << 1),
+    Deploying = (1 << 1) | Deployed,
+    Firing = (1 << 2) | Deployed,
+    Disabled = (1 << 3),
+    Broken = (1 << 4),
+}
diff --git a/Content.Shared/Turrets/SharedDeployableTurretSystem.cs b/Content.Shared/Turrets/SharedDeployableTurretSystem.cs
new file mode 100644 (file)
index 0000000..8209a49
--- /dev/null
@@ -0,0 +1,167 @@
+using Content.Shared.Access.Components;
+using Content.Shared.Access.Systems;
+using Content.Shared.Damage;
+using Content.Shared.Database;
+using Content.Shared.Interaction;
+using Content.Shared.Popups;
+using Content.Shared.Timing;
+using Content.Shared.Verbs;
+using Content.Shared.Weapons.Ranged.Events;
+using Content.Shared.Wires;
+using Robust.Shared.Audio.Systems;
+using Robust.Shared.Physics;
+using Robust.Shared.Physics.Systems;
+using Robust.Shared.Timing;
+using Robust.Shared.Utility;
+
+namespace Content.Shared.Turrets;
+
+public abstract partial class SharedDeployableTurretSystem : EntitySystem
+{
+    [Dependency] private readonly SharedPopupSystem _popup = default!;
+    [Dependency] private readonly SharedAudioSystem _audio = default!;
+    [Dependency] private readonly UseDelaySystem _useDelay = default!;
+    [Dependency] private readonly AccessReaderSystem _accessReader = default!;
+    [Dependency] private readonly DamageableSystem _damageable = default!;
+    [Dependency] private readonly SharedPhysicsSystem _physics = default!;
+    [Dependency] private readonly SharedWiresSystem _wires = default!;
+    [Dependency] private readonly IGameTiming _timing = default!;
+
+    public override void Initialize()
+    {
+        base.Initialize();
+
+        SubscribeLocalEvent<DeployableTurretComponent, ActivateInWorldEvent>(OnActivate);
+        SubscribeLocalEvent<DeployableTurretComponent, AttemptChangePanelEvent>(OnAttemptChangeWirePanelWire);
+        SubscribeLocalEvent<DeployableTurretComponent, GetVerbsEvent<Verb>>(OnGetVerb);
+    }
+
+    private void OnGetVerb(Entity<DeployableTurretComponent> ent, ref GetVerbsEvent<Verb> args)
+    {
+        if (!args.CanAccess || !args.CanInteract || !args.CanComplexInteract)
+            return;
+
+        if (!_accessReader.IsAllowed(args.User, ent))
+            return;
+
+        var user = args.User;
+
+        var verb = new Verb
+        {
+            Priority = 1,
+            Text = ent.Comp.Enabled ? Loc.GetString("deployable-turret-component-deactivate") : Loc.GetString("deployable-turret-component-activate"),
+            Icon = new SpriteSpecifier.Texture(new("/Textures/Interface/VerbIcons/Spare/poweronoff.svg.192dpi.png")),
+            Disabled = !HasAmmo(ent),
+            Impact = LogImpact.Low,
+            Act = () => { TryToggleState(ent, user); }
+        };
+
+        args.Verbs.Add(verb);
+    }
+
+    private void OnActivate(Entity<DeployableTurretComponent> ent, ref ActivateInWorldEvent args)
+    {
+        if (TryComp(ent, out UseDelayComponent? useDelay) && !_useDelay.TryResetDelay((ent, useDelay), true))
+            return;
+
+        if (!_accessReader.IsAllowed(args.User, ent))
+        {
+            _popup.PopupClient(Loc.GetString("deployable-turret-component-access-denied"), ent, args.User);
+            _audio.PlayPredicted(ent.Comp.AccessDeniedSound, ent, args.User);
+
+            return;
+        }
+
+        TryToggleState(ent, args.User);
+    }
+
+    private void OnAttemptChangeWirePanelWire(Entity<DeployableTurretComponent> ent, ref AttemptChangePanelEvent args)
+    {
+        if (!ent.Comp.Enabled || args.Cancelled)
+            return;
+
+        _popup.PopupClient(Loc.GetString("deployable-turret-component-cannot-access-wires"), ent, args.User);
+
+        args.Cancelled = true;
+    }
+
+    public bool TryToggleState(Entity<DeployableTurretComponent> ent, EntityUid? user = null)
+    {
+        return TrySetState(ent, !ent.Comp.Enabled, user);
+    }
+
+    public bool TrySetState(Entity<DeployableTurretComponent> ent, bool enabled, EntityUid? user = null)
+    {
+        if (enabled && ent.Comp.CurrentState == DeployableTurretState.Broken)
+        {
+            if (user != null)
+                _popup.PopupClient(Loc.GetString("deployable-turret-component-is-broken"), ent, user.Value);
+
+            return false;
+        }
+
+        if (enabled && !HasAmmo(ent))
+        {
+            if (user != null)
+                _popup.PopupClient(Loc.GetString("deployable-turret-component-no-ammo"), ent, user.Value);
+
+            return false;
+        }
+
+        SetState(ent, enabled, user);
+
+        return true;
+    }
+
+    protected virtual void SetState(Entity<DeployableTurretComponent> ent, bool enabled, EntityUid? user = null)
+    {
+        if (ent.Comp.Enabled == enabled)
+            return;
+
+        // Hide the wires panel UI on activation
+        if (enabled && TryComp<WiresPanelComponent>(ent, out var wires) && wires.Open)
+        {
+            _wires.TogglePanel(ent, wires, false);
+            _audio.PlayPredicted(wires.ScrewdriverCloseSound, ent, user);
+        }
+
+        // Determine how much time is remaining in the current animation and the one next in queue
+        // We track this so that when a turret is toggled on/off, we can wait for all queued animations
+        // to end before the turret's HTN is reactivated
+        var animTimeRemaining = MathF.Max((float)(ent.Comp.AnimationCompletionTime - _timing.CurTime).TotalSeconds, 0f);
+        var animTimeNext = enabled ? ent.Comp.DeploymentLength : ent.Comp.RetractionLength;
+
+        ent.Comp.AnimationCompletionTime = _timing.CurTime + TimeSpan.FromSeconds(animTimeNext + animTimeRemaining);
+
+        // Change the turret's damage modifiers
+        if (TryComp<DamageableComponent>(ent, out var damageable))
+        {
+            var damageSetID = enabled ? ent.Comp.DeployedDamageModifierSetId : ent.Comp.RetractedDamageModifierSetId;
+            _damageable.SetDamageModifierSetId(ent, damageSetID, damageable);
+        }
+
+        // Change the turret's fixtures
+        if (ent.Comp.DeployedFixture != null &&
+            TryComp(ent, out FixturesComponent? fixtures) &&
+            fixtures.Fixtures.TryGetValue(ent.Comp.DeployedFixture, out var fixture))
+        {
+            _physics.SetHard(ent, fixture, enabled);
+        }
+
+        // Play pop up message
+        var msg = enabled ? "deployable-turret-component-activating" : "deployable-turret-component-deactivating";
+        _popup.PopupClient(Loc.GetString(msg), ent, user);
+
+        // Update enabled state
+        ent.Comp.Enabled = enabled;
+        DirtyField(ent, ent.Comp, "Enabled");
+    }
+
+    public bool HasAmmo(Entity<DeployableTurretComponent> ent)
+    {
+        var ammoCountEv = new GetAmmoCountEvent();
+        RaiseLocalEvent(ent, ref ammoCountEv);
+
+        return ammoCountEv.Count > 0;
+    }
+}
index b0ca1f215cc1a5645dffb6386b3c325364b53291..77b9f53b7b0a56267d8f586ab119df474bb86b38 100644 (file)
@@ -43,3 +43,9 @@ public sealed partial class BatteryWeaponFireMode
     [DataField]
     public float FireCost = 100;
 }
+
+[Serializable, NetSerializable]
+public enum BatteryWeaponFireModeVisuals : byte
+{
+    State
+}
index bae5b95a193db13c0c376018d279055c5ada56e6..0c90ae1637837f519ae51cced6f15a4d708c0f4d 100644 (file)
@@ -1,7 +1,8 @@
-using System.Linq;
+using Content.Shared.Access.Components;
+using Content.Shared.Access.Systems;
 using Content.Shared.Database;
 using Content.Shared.Examine;
-using Content.Shared.Interaction;
+using Content.Shared.Interaction.Events;
 using Content.Shared.Popups;
 using Content.Shared.Verbs;
 using Content.Shared.Weapons.Ranged.Components;
@@ -14,12 +15,14 @@ public sealed class BatteryWeaponFireModesSystem : EntitySystem
 {
     [Dependency] private readonly IPrototypeManager _prototypeManager = default!;
     [Dependency] private readonly SharedPopupSystem _popupSystem = default!;
+    [Dependency] private readonly AccessReaderSystem _accessReaderSystem = default!;
+    [Dependency] private readonly SharedAppearanceSystem _appearanceSystem = default!;
 
     public override void Initialize()
     {
         base.Initialize();
 
-        SubscribeLocalEvent<BatteryWeaponFireModesComponent, ActivateInWorldEvent>(OnInteractHandEvent);
+        SubscribeLocalEvent<BatteryWeaponFireModesComponent, UseInHandEvent>(OnUseInHandEvent);
         SubscribeLocalEvent<BatteryWeaponFireModesComponent, GetVerbsEvent<Verb>>(OnGetVerb);
         SubscribeLocalEvent<BatteryWeaponFireModesComponent, ExaminedEvent>(OnExamined);
     }
@@ -44,12 +47,15 @@ public sealed class BatteryWeaponFireModesSystem : EntitySystem
 
     private void OnGetVerb(EntityUid uid, BatteryWeaponFireModesComponent component, GetVerbsEvent<Verb> args)
     {
-        if (!args.CanAccess || !args.CanInteract || args.Hands == null)
+        if (!args.CanAccess || !args.CanInteract || !args.CanComplexInteract)
             return;
 
         if (component.FireModes.Count < 2)
             return;
 
+        if (!_accessReaderSystem.IsAllowed(args.User, uid))
+            return;
+
         for (var i = 0; i < component.FireModes.Count; i++)
         {
             var fireMode = component.FireModes[i];
@@ -62,11 +68,11 @@ public sealed class BatteryWeaponFireModesSystem : EntitySystem
                 Category = VerbCategory.SelectType,
                 Text = entProto.Name,
                 Disabled = i == component.CurrentFireMode,
-                Impact = LogImpact.Low,
+                Impact = LogImpact.Medium,
                 DoContactInteraction = true,
                 Act = () =>
                 {
-                    SetFireMode(uid, component, index, args.User);
+                    TrySetFireMode(uid, component, index, args.User);
                 }
             };
 
@@ -74,24 +80,31 @@ public sealed class BatteryWeaponFireModesSystem : EntitySystem
         }
     }
 
-    private void OnInteractHandEvent(EntityUid uid, BatteryWeaponFireModesComponent component, ActivateInWorldEvent args)
+    private void OnUseInHandEvent(EntityUid uid, BatteryWeaponFireModesComponent component, UseInHandEvent args)
     {
-        if (!args.Complex)
-            return;
+        TryCycleFireMode(uid, component, args.User);
+    }
 
+    public void TryCycleFireMode(EntityUid uid, BatteryWeaponFireModesComponent component, EntityUid? user = null)
+    {
         if (component.FireModes.Count < 2)
             return;
 
-        CycleFireMode(uid, component, args.User);
+        var index = (component.CurrentFireMode + 1) % component.FireModes.Count;
+        TrySetFireMode(uid, component, index, user);
     }
 
-    private void CycleFireMode(EntityUid uid, BatteryWeaponFireModesComponent component, EntityUid user)
+    public bool TrySetFireMode(EntityUid uid, BatteryWeaponFireModesComponent component, int index, EntityUid? user = null)
     {
-        if (component.FireModes.Count < 2)
-            return;
+        if (index < 0 || index >= component.FireModes.Count)
+            return false;
+
+        if (user != null && !_accessReaderSystem.IsAllowed(user.Value, uid))
+            return false;
 
-        var index = (component.CurrentFireMode + 1) % component.FireModes.Count;
         SetFireMode(uid, component, index, user);
+
+        return true;
     }
 
     private void SetFireMode(EntityUid uid, BatteryWeaponFireModesComponent component, int index, EntityUid? user = null)
@@ -100,26 +113,30 @@ public sealed class BatteryWeaponFireModesSystem : EntitySystem
         component.CurrentFireMode = index;
         Dirty(uid, component);
 
-        if (TryComp(uid, out ProjectileBatteryAmmoProviderComponent? projectileBatteryAmmoProviderComponent))
+        if (_prototypeManager.TryIndex<EntityPrototype>(fireMode.Prototype, out var prototype))
         {
-            if (!_prototypeManager.TryIndex<EntityPrototype>(fireMode.Prototype, out var prototype))
-                return;
+            if (TryComp<AppearanceComponent>(uid, out var appearance))
+                _appearanceSystem.SetData(uid, BatteryWeaponFireModeVisuals.State, prototype.ID, appearance);
 
+            if (user != null)
+                _popupSystem.PopupClient(Loc.GetString("gun-set-fire-mode", ("mode", prototype.Name)), uid, user.Value);
+        }
+
+        if (TryComp(uid, out ProjectileBatteryAmmoProviderComponent? projectileBatteryAmmoProviderComponent))
+        {
             // TODO: Have this get the info directly from the batteryComponent when power is moved to shared.
             var OldFireCost = projectileBatteryAmmoProviderComponent.FireCost;
             projectileBatteryAmmoProviderComponent.Prototype = fireMode.Prototype;
             projectileBatteryAmmoProviderComponent.FireCost = fireMode.FireCost;
+
             float FireCostDiff = (float)fireMode.FireCost / (float)OldFireCost;
-            projectileBatteryAmmoProviderComponent.Shots = (int)Math.Round(projectileBatteryAmmoProviderComponent.Shots/FireCostDiff);
-            projectileBatteryAmmoProviderComponent.Capacity = (int)Math.Round(projectileBatteryAmmoProviderComponent.Capacity/FireCostDiff);
+            projectileBatteryAmmoProviderComponent.Shots = (int)Math.Round(projectileBatteryAmmoProviderComponent.Shots / FireCostDiff);
+            projectileBatteryAmmoProviderComponent.Capacity = (int)Math.Round(projectileBatteryAmmoProviderComponent.Capacity / FireCostDiff);
+
             Dirty(uid, projectileBatteryAmmoProviderComponent);
+
             var updateClientAmmoEvent = new UpdateClientAmmoEvent();
             RaiseLocalEvent(uid, ref updateClientAmmoEvent);
-
-            if (user != null)
-            {
-                _popupSystem.PopupClient(Loc.GetString("gun-set-fire-mode", ("mode", prototype.Name)), uid, user.Value);
-            }
         }
     }
 }
index 430888ed36530bdc326f56194c81fabbb5e844ce..b2c86dd6a690d7ba3e5296ae2178afbd678792ca 100644 (file)
@@ -8,3 +8,5 @@ construction-insert-info-examine-name-instrument-string = string intrument
 construction-insert-info-examine-name-instrument-woodwind = woodwind instrument
 construction-insert-info-examine-name-knife = knife
 construction-insert-info-examine-name-utensil = utensil
+construction-insert-info-examine-name-laser-cannon = high power laser weapon
+construction-insert-info-examine-name-power-cell = power cell
index dd473866dcab13836c36697b1a6f4194d809592c..c19903c3137850bb88a74499e990d04da2d5111e 100644 (file)
@@ -9,6 +9,8 @@ device-frequency-prototype-name-fax = Fax
 device-frequency-prototype-name-basic-device = Basic Devices
 device-frequency-prototype-name-cyborg-control = Cyborg Control
 device-frequency-prototype-name-robotics-console = Robotics Console
+device-frequency-prototype-name-turret = Sentry Turret
+device-frequency-prototype-name-turret-control = Sentry Turret Control
 
 ## camera frequencies
 device-frequency-prototype-name-surveillance-camera-test = Subnet Test
@@ -32,6 +34,7 @@ device-address-prefix-heater = HTR-
 device-address-prefix-freezer = FZR-
 device-address-prefix-volume-pump = VPP-
 device-address-prefix-smes = SMS-
+device-address-prefix-turret = TRT-
 
 # PDAs and terminals
 device-address-prefix-console = CLS-
diff --git a/Resources/Locale/en-US/weapons/ranged/turrets.ftl b/Resources/Locale/en-US/weapons/ranged/turrets.ftl
new file mode 100644 (file)
index 0000000..213599d
--- /dev/null
@@ -0,0 +1,12 @@
+# Deployable turret component
+deployable-turret-component-activating = Deploying...
+deployable-turret-component-deactivating = Deactivating...
+deployable-turret-component-activate = Activate
+deployable-turret-component-deactivate = Deactivate
+deployable-turret-component-access-denied = Access denied
+deployable-turret-component-no-ammo = Weapon systems depleted
+deployable-turret-component-is-broken = The turret is heavily damaged and must be repaired
+deployable-turret-component-cannot-access-wires = You can't reach the maintenance panel while the turret is active 
+
+# Turret notification for station AI
+station-ai-turret-is-attacking-warning = {CAPITALIZE($source)} has engaged a hostile target.
\ No newline at end of file
index 08e5af4000b06df998923eb11c137c19af515b87..1c35bdeb8cc6507f8640b60a0f85903960048072 100644 (file)
@@ -43,6 +43,8 @@ wires-board-name-jukebox = Jukebox
 wires-board-name-computer = Computer
 wires-board-name-holopad = Holopad
 wires-board-name-barsign = Bar Sign
+wires-board-name-weapon-energy-turret = Sentry turret
+wires-board-name-turret-controls = Sentry turret control panel
 
 # names that get displayed in the wire hacking hud & admin logs.
 
index ecdbb3bb4c249952009e2d298a98a4dc7eb22cb2..64b8c8e68787a68c63c1e0895b59328615781bec 100644 (file)
   name: device-frequency-prototype-name-cyborg-control
   frequency: 1292
 
+# Turret controllers send data to their turrets on this frequency
+- type: deviceFrequency
+  id: TurretControl
+  name: device-frequency-prototype-name-turret-control
+  frequency: 2151
+
+# Turrets send data to their controllers on this frequency
+- type: deviceFrequency
+  id: Turret
+  name: device-frequency-prototype-name-turret
+  frequency: 2152
+  
+# AI turret controllers send data to their turrets on this frequency
+- type: deviceFrequency
+  id: TurretControlAI
+  name: device-frequency-prototype-name-turret-control
+  frequency: 2153
+
+# AI turrets send data to their controllers on this frequency
+- type: deviceFrequency
+  id: TurretAI
+  name: device-frequency-prototype-name-turret
+  frequency: 2154
+
 # This frequency will likely have a LARGE number of listening entities. Please don't broadcast on this frequency.
 - type: deviceFrequency
   id: SmartLight #used by powered lights.
diff --git a/Resources/Prototypes/Entities/Objects/Devices/Circuitboards/Machine/turrets.yml b/Resources/Prototypes/Entities/Objects/Devices/Circuitboards/Machine/turrets.yml
new file mode 100644 (file)
index 0000000..5bbf2bb
--- /dev/null
@@ -0,0 +1,36 @@
+- type: entity
+  id: WeaponEnergyTurretStationMachineCircuitboard
+  parent: BaseMachineCircuitboard
+  name: sentry turret machine board
+  description: A machine printed circuit board for a sentry turret.
+  components:
+  - type: Sprite
+    sprite: Objects/Misc/module.rsi
+    state: security
+  - type: MachineBoard
+    prototype: WeaponEnergyTurretStation
+    tagRequirements:
+      TurretCompatibleWeapon:
+        amount: 1
+        defaultPrototype: WeaponLaserCannon
+        examineName: construction-insert-info-examine-name-laser-cannon 
+      ProximitySensor:
+        amount: 1
+        defaultPrototype: ProximitySensor
+    componentRequirements:
+      PowerCell:
+        amount: 1
+        defaultPrototype: PowerCellMedium
+        examineName: construction-insert-info-examine-name-power-cell 
+        
+- type: entity
+  id: WeaponEnergyTurretAIMachineCircuitboard
+  parent: WeaponEnergyTurretStationMachineCircuitboard
+  name: AI sentry turret machine board
+  description: A machine printed circuit board for an AI sentry turret.
+  components:
+  - type: Sprite
+    sprite: Objects/Misc/module.rsi
+    state: command
+  - type: MachineBoard
+    prototype: WeaponEnergyTurretAI
\ No newline at end of file
index 73d06a200c9bd49c55a375ca332d58d00ac74f2b..7b1c77b8d03cae0dd7e91ce79222989f5340cc2b 100644 (file)
   - type: HitscanBatteryAmmoProvider
     proto: RedHeavyLaser
     fireCost: 100
+  - type: Tag
+    tags:
+    - TurretCompatibleWeapon
 
 - type: entity
   name: portable particle decelerator
index aaa45a2136b75cd4e52f3b737af980e5f0ae121d..ed157365da3a65e9f7836833f96bedfa6877d35d 100644 (file)
     maxCharge: 2000
     startingCharge: 0
   - type: ApcPowerReceiverBattery
-    idlePowerUse: 5
+    idleLoad: 5
     batteryRechargeRate: 200
     batteryRechargeEfficiency: 1.225
   - type: ApcPowerReceiver
     powerLoad: 5
-  - type: ExtensionCableReceiver
\ No newline at end of file
+  - type: ExtensionCableReceiver
+  - type: HTN
+    rootTask:
+      task: EnergyTurretCompound
\ No newline at end of file
diff --git a/Resources/Prototypes/Entities/Objects/Weapons/Guns/Turrets/turrets_energy.yml b/Resources/Prototypes/Entities/Objects/Weapons/Guns/Turrets/turrets_energy.yml
new file mode 100644 (file)
index 0000000..2a37ba0
--- /dev/null
@@ -0,0 +1,182 @@
+- type: entity
+  parent: [BaseWeaponEnergyTurret, ConstructibleMachine]
+  id: WeaponEnergyTurretStation
+  name: sentry turret
+  description: A high-tech autonomous weapons system designed to keep unauthorized personnel out of sensitive areas.
+  components:
+  
+  # Physics
+  - type: Fixtures
+    fixtures:
+      body:
+        shape:
+          !type:PhysShapeCircle
+          radius: 0.45
+        density: 60
+        mask:
+          - Impassable
+      turret:
+        shape:
+          !type:PhysShapeCircle
+          radius: 0.45
+        density: 60
+        mask:
+          - MachineMask
+        layer:
+          - MachineLayer
+        hard: false
+        
+  # Sprites and appearance
+  - type: Sprite
+    sprite: Objects/Weapons/Guns/Turrets/sentry_turret.rsi
+    drawdepth: FloorObjects
+    granularLayersRendering: true
+    layers:
+    - state: support
+      renderingStrategy: NoRotation
+    - state: base_shadow
+      map: [ "shadow" ]
+    - state: base
+      map: [ "base" ]
+    - state: stun
+      map: [ "enum.DeployableTurretVisuals.Weapon" ]
+      shader: "unshaded"
+      visible: false
+    - state: cover_closed
+      map: [ "enum.DeployableTurretVisuals.Turret" ]
+      renderingStrategy: NoRotation
+    - state: cover_light_on
+      map: [ "enum.PowerDeviceVisualLayers.Powered" ]
+      shader: "unshaded"
+      renderingStrategy: NoRotation
+      visible: false
+    - state: panel
+      map: [ "enum.WiresVisualLayers.MaintenancePanel" ]
+      renderingStrategy: NoRotation
+      visible: false
+  - type: AnimationPlayer
+  - type: Appearance
+  - type: GenericVisualizer
+    visuals:
+      enum.BatteryWeaponFireModeVisuals.State:
+        enum.DeployableTurretVisuals.Weapon:
+          BulletEnergyTurretDisabler: { state: stun }
+          BulletEnergyTurretLaser: { state: lethal }
+      enum.DeployableTurretVisuals.Broken:
+        base:
+          True: { state: destroyed }
+          False: { state: base }
+      enum.WiresVisuals.MaintenancePanelState:
+        enum.WiresVisualLayers.MaintenancePanel:
+          True: { visible: false }
+          False: { visible: true }
+  
+  # HTN
+  - type: HTN
+    enabled: false
+  
+  # Faction / control
+  - type: StationAiWhitelist
+  - type: NpcFactionMember
+    factions:
+    - AllHostile
+  - type: AccessReader
+    access: [["Security"]]
+    
+  # Weapon systems
+  - type: ProjectileBatteryAmmoProvider
+    proto: BulletEnergyTurretDisabler
+    fireCost: 100
+  - type: BatteryWeaponFireModes
+    fireModes:
+    - proto: BulletEnergyTurretDisabler
+      fireCost: 100
+    - proto: BulletEnergyTurretLaser
+      fireCost: 100
+  - type: TurretTargetSettings
+    exemptAccessLevels:
+    - Security
+    - Borg
+    - BasicSilicon
+    
+  # Defenses / destruction
+  - type: DeployableTurret
+    retractedDamageModifierSetId: Metallic
+    deployedDamageModifierSetId: FlimsyMetallic
+  - type: Damageable
+    damageModifierSet: Metallic
+  - type: Repairable
+    doAfterDelay: 10
+    allowSelfRepair: false
+  - type: Destructible
+    thresholds:
+    - trigger:
+        !type:DamageTrigger
+        damage: 300
+      behaviors:
+      - !type:PlaySoundBehavior
+        sound:
+          collection: MetalBreak
+      - !type:DoActsBehavior
+        acts: [ "Breakage" ]
+    - trigger:
+        !type:DamageTrigger
+        damage: 600
+      behaviors:
+      - !type:PlaySoundBehavior
+        sound:
+          collection: MetalBreak
+      - !type:ChangeConstructionNodeBehavior
+        node: machineFrame
+      - !type:DoActsBehavior
+        acts: ["Destruction"]
+      
+  # Device network
+  - type: DeviceNetwork
+    deviceNetId: Wired
+    receiveFrequencyId: TurretControl
+    transmitFrequencyId: Turret
+    sendBroadcastAttemptEvent: true
+    prefix: device-address-prefix-turret
+    examinableAddress: true
+  - type: DeviceNetworkRequiresPower
+  - type: WiredNetworkConnection
+  
+  # Wires
+  - type: UserInterface
+    interfaces:
+      enum.WiresUiKey.Key:
+        type: WiresBoundUserInterface
+  - type: WiresPanel
+  - type: WiresVisuals
+  - type: Wires
+    boardName: wires-board-name-weapon-energy-turret
+    layoutId: WeaponEnergyTurret
+  - type: Lock
+    locked: true
+    unlockOnClick: false
+  - type: LockedWiresPanel
+  
+  # General properties  
+  - type: Machine
+    board: WeaponEnergyTurretStationMachineCircuitboard
+  - type: UseDelay
+    delay: 1.2
+
+- type: entity
+  parent: WeaponEnergyTurretStation
+  id: WeaponEnergyTurretAI
+  name: AI sentry turret
+  description: A high-tech autonomous weapons system under the direct control of a local artifical intelligence.
+  components:
+  - type: AccessReader
+    access: [["StationAi"]]
+  - type: TurretTargetSettings
+    exemptAccessLevels:
+    - Borg
+    - BasicSilicon
+  - type: Machine
+    board: WeaponEnergyTurretAIMachineCircuitboard
+  - type: DeviceNetwork
+    receiveFrequencyId: TurretControlAI
+    transmitFrequencyId: TurretAI
index 03764e2b1f1518887b91d350526decd31a248e42..69ae4a337ded09edf9374ca4f8a97f828d79e4f3 100644 (file)
       curve: !type:BoolCurve
     - !type:TargetIsCritCon
       curve: !type:InverseBoolCurve
+    - !type:TargetIsStunnedCon
+      curve: !type:InverseBoolCurve  
     - !type:TurretTargetingCon
       curve: !type:BoolCurve
     - !type:TargetDistanceCon
index 32c14886835a4749573d83ce143fc4951148261b..d94355361f91dac214888f8ab5f29e4ad5f29162 100644 (file)
   wires:
   - !type:PowerWireAction
   - !type:AiInteractWireAction
-  - !type:AccessWireAction
\ No newline at end of file
+  - !type:AccessWireAction
+  
+- type: wireLayout
+  id: WeaponEnergyTurret
+  dummyWires: 4
+  wires:
+  - !type:PowerWireAction
+  - !type:PowerWireAction
+    pulseTimeout: 15
+  - !type:AiInteractWireAction
+  - !type:AccessWireAction
+  
+- type: wireLayout
+  id: TurretControls
+  dummyWires: 2
+  wires:
+  - !type:PowerWireAction
+  - !type:PowerWireAction
+    pulseTimeout: 15
+  - !type:AiInteractWireAction
+  - !type:AccessWireAction 
+  
\ No newline at end of file
index 68b69fc3b4d9051ad728658260e828a3abdbc490..6577b7eb89883c2c476b91442409f2e4a283a121 100644 (file)
 
 - type: Tag
   id: Truncheon
+  
+- type: Tag
+  id: TurretCompatibleWeapon # Used in the construction of sentry turrets
+  
+- type: Tag
+  id: TurretControlElectronics # Used in the construction of sentry turret control panels
 
 - type: Tag
   id: Unimplantable