--- /dev/null
+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;
+ }
+ }
+}
[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;
}
}
/// </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))
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;
--- /dev/null
+namespace Content.Server.NPC.Queries.Considerations;
+
+/// <summary>
+/// Returns 1f if the target has the <see cref="StunnedComponent"/>
+/// </summary>
+public sealed partial class TargetIsStunnedCon : UtilityConsideration
+{
+
+}
+
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;
return 1f;
return 0f;
}
+ case TargetIsStunnedCon:
+ {
+ return HasComp<StunnedComponent>(targetUid) ? 1f : 0f;
+ }
case TurretTargetingCon:
{
if (!TryComp<TurretTargetSettingsComponent>(owner, out var turretTargetSettings) ||
--- /dev/null
+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);
+ }
+ }
+ }
+}
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;
--- /dev/null
+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),
+}
--- /dev/null
+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;
+ }
+}
[DataField]
public float FireCost = 100;
}
+
+[Serializable, NetSerializable]
+public enum BatteryWeaponFireModeVisuals : byte
+{
+ State
+}
-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;
{
[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);
}
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];
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);
}
};
}
}
- 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)
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);
- }
}
}
}
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
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
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-
--- /dev/null
+# 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
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.
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.
--- /dev/null
+- 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
- type: HitscanBatteryAmmoProvider
proto: RedHeavyLaser
fireCost: 100
+ - type: Tag
+ tags:
+ - TurretCompatibleWeapon
- type: entity
name: portable particle decelerator
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
--- /dev/null
+- 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
curve: !type:BoolCurve
- !type:TargetIsCritCon
curve: !type:InverseBoolCurve
+ - !type:TargetIsStunnedCon
+ curve: !type:InverseBoolCurve
- !type:TurretTargetingCon
curve: !type:BoolCurve
- !type:TargetDistanceCon
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
- 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