Sentry turrets - Part 4: The sentry turret and its primary systems (#35123)

* 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
This commit is contained in:
chromiumboy
2025-03-29 12:55:58 -05:00
committed by GitHub
parent 587afe7598
commit dfd3e36a0a
24 changed files with 1005 additions and 30 deletions

View File

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

View File

@@ -9,8 +9,17 @@ namespace Content.Server.Destructible
[RegisterComponent] [RegisterComponent]
public sealed partial class DestructibleComponent : Component 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(); 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;
} }
} }

View File

@@ -57,6 +57,8 @@ namespace Content.Server.Destructible
/// </summary> /// </summary>
public void Execute(EntityUid uid, DestructibleComponent component, DamageChangedEvent args) public void Execute(EntityUid uid, DestructibleComponent component, DamageChangedEvent args)
{ {
component.IsBroken = false;
foreach (var threshold in component.Thresholds) foreach (var threshold in component.Thresholds)
{ {
if (threshold.Reached(args.Damageable, this)) if (threshold.Reached(args.Damageable, this))
@@ -96,6 +98,12 @@ namespace Content.Server.Destructible
threshold.Execute(uid, this, EntityManager, args.Origin); 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 destruction behavior (or some other deletion effect) occurred, don't run other triggers.
if (EntityManager.IsQueuedForDeletion(uid) || Deleted(uid)) if (EntityManager.IsQueuedForDeletion(uid) || Deleted(uid))
return; return;

View File

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

View File

@@ -19,6 +19,7 @@ using Content.Shared.Mobs.Systems;
using Content.Shared.NPC.Systems; using Content.Shared.NPC.Systems;
using Content.Shared.Nutrition.Components; using Content.Shared.Nutrition.Components;
using Content.Shared.Nutrition.EntitySystems; using Content.Shared.Nutrition.EntitySystems;
using Content.Shared.Stunnable;
using Content.Shared.Tools.Systems; using Content.Shared.Tools.Systems;
using Content.Shared.Turrets; using Content.Shared.Turrets;
using Content.Shared.Weapons.Melee; using Content.Shared.Weapons.Melee;
@@ -360,6 +361,10 @@ public sealed class NPCUtilitySystem : EntitySystem
return 1f; return 1f;
return 0f; return 0f;
} }
case TargetIsStunnedCon:
{
return HasComp<StunnedComponent>(targetUid) ? 1f : 0f;
}
case TurretTargetingCon: case TurretTargetingCon:
{ {
if (!TryComp<TurretTargetSettingsComponent>(owner, out var turretTargetSettings) || if (!TryComp<TurretTargetSettingsComponent>(owner, out var turretTargetSettings) ||

View File

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

View File

@@ -296,7 +296,7 @@ namespace Content.Shared.Damage
DamageChanged(uid, component, new DamageSpecifier()); 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)) if (!_damageableQuery.Resolve(uid, ref comp))
return; return;

View File

@@ -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),
}

View File

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

View File

@@ -43,3 +43,9 @@ public sealed partial class BatteryWeaponFireMode
[DataField] [DataField]
public float FireCost = 100; public float FireCost = 100;
} }
[Serializable, NetSerializable]
public enum BatteryWeaponFireModeVisuals : byte
{
State
}

View 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.Database;
using Content.Shared.Examine; using Content.Shared.Examine;
using Content.Shared.Interaction; using Content.Shared.Interaction.Events;
using Content.Shared.Popups; using Content.Shared.Popups;
using Content.Shared.Verbs; using Content.Shared.Verbs;
using Content.Shared.Weapons.Ranged.Components; using Content.Shared.Weapons.Ranged.Components;
@@ -14,12 +15,14 @@ public sealed class BatteryWeaponFireModesSystem : EntitySystem
{ {
[Dependency] private readonly IPrototypeManager _prototypeManager = default!; [Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly SharedPopupSystem _popupSystem = default!; [Dependency] private readonly SharedPopupSystem _popupSystem = default!;
[Dependency] private readonly AccessReaderSystem _accessReaderSystem = default!;
[Dependency] private readonly SharedAppearanceSystem _appearanceSystem = default!;
public override void Initialize() public override void Initialize()
{ {
base.Initialize(); base.Initialize();
SubscribeLocalEvent<BatteryWeaponFireModesComponent, ActivateInWorldEvent>(OnInteractHandEvent); SubscribeLocalEvent<BatteryWeaponFireModesComponent, UseInHandEvent>(OnUseInHandEvent);
SubscribeLocalEvent<BatteryWeaponFireModesComponent, GetVerbsEvent<Verb>>(OnGetVerb); SubscribeLocalEvent<BatteryWeaponFireModesComponent, GetVerbsEvent<Verb>>(OnGetVerb);
SubscribeLocalEvent<BatteryWeaponFireModesComponent, ExaminedEvent>(OnExamined); SubscribeLocalEvent<BatteryWeaponFireModesComponent, ExaminedEvent>(OnExamined);
} }
@@ -44,12 +47,15 @@ public sealed class BatteryWeaponFireModesSystem : EntitySystem
private void OnGetVerb(EntityUid uid, BatteryWeaponFireModesComponent component, GetVerbsEvent<Verb> args) 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; return;
if (component.FireModes.Count < 2) if (component.FireModes.Count < 2)
return; return;
if (!_accessReaderSystem.IsAllowed(args.User, uid))
return;
for (var i = 0; i < component.FireModes.Count; i++) for (var i = 0; i < component.FireModes.Count; i++)
{ {
var fireMode = component.FireModes[i]; var fireMode = component.FireModes[i];
@@ -62,11 +68,11 @@ public sealed class BatteryWeaponFireModesSystem : EntitySystem
Category = VerbCategory.SelectType, Category = VerbCategory.SelectType,
Text = entProto.Name, Text = entProto.Name,
Disabled = i == component.CurrentFireMode, Disabled = i == component.CurrentFireMode,
Impact = LogImpact.Low, Impact = LogImpact.Medium,
DoContactInteraction = true, DoContactInteraction = true,
Act = () => 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) TryCycleFireMode(uid, component, args.User);
return;
if (component.FireModes.Count < 2)
return;
CycleFireMode(uid, component, args.User);
} }
private void CycleFireMode(EntityUid uid, BatteryWeaponFireModesComponent component, EntityUid user) public void TryCycleFireMode(EntityUid uid, BatteryWeaponFireModesComponent component, EntityUid? user = null)
{ {
if (component.FireModes.Count < 2) if (component.FireModes.Count < 2)
return; return;
var index = (component.CurrentFireMode + 1) % component.FireModes.Count; var index = (component.CurrentFireMode + 1) % component.FireModes.Count;
TrySetFireMode(uid, component, index, user);
}
public bool TrySetFireMode(EntityUid uid, BatteryWeaponFireModesComponent component, int index, EntityUid? user = null)
{
if (index < 0 || index >= component.FireModes.Count)
return false;
if (user != null && !_accessReaderSystem.IsAllowed(user.Value, uid))
return false;
SetFireMode(uid, component, index, user); SetFireMode(uid, component, index, user);
return true;
} }
private void SetFireMode(EntityUid uid, BatteryWeaponFireModesComponent component, int index, EntityUid? user = null) private void SetFireMode(EntityUid uid, BatteryWeaponFireModesComponent component, int index, EntityUid? user = null)
@@ -100,26 +113,30 @@ public sealed class BatteryWeaponFireModesSystem : EntitySystem
component.CurrentFireMode = index; component.CurrentFireMode = index;
Dirty(uid, component); Dirty(uid, component);
if (_prototypeManager.TryIndex<EntityPrototype>(fireMode.Prototype, out var prototype))
{
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)) if (TryComp(uid, out ProjectileBatteryAmmoProviderComponent? projectileBatteryAmmoProviderComponent))
{ {
if (!_prototypeManager.TryIndex<EntityPrototype>(fireMode.Prototype, out var prototype))
return;
// TODO: Have this get the info directly from the batteryComponent when power is moved to shared. // TODO: Have this get the info directly from the batteryComponent when power is moved to shared.
var OldFireCost = projectileBatteryAmmoProviderComponent.FireCost; var OldFireCost = projectileBatteryAmmoProviderComponent.FireCost;
projectileBatteryAmmoProviderComponent.Prototype = fireMode.Prototype; projectileBatteryAmmoProviderComponent.Prototype = fireMode.Prototype;
projectileBatteryAmmoProviderComponent.FireCost = fireMode.FireCost; projectileBatteryAmmoProviderComponent.FireCost = fireMode.FireCost;
float FireCostDiff = (float)fireMode.FireCost / (float)OldFireCost; float FireCostDiff = (float)fireMode.FireCost / (float)OldFireCost;
projectileBatteryAmmoProviderComponent.Shots = (int)Math.Round(projectileBatteryAmmoProviderComponent.Shots/FireCostDiff); projectileBatteryAmmoProviderComponent.Shots = (int)Math.Round(projectileBatteryAmmoProviderComponent.Shots / FireCostDiff);
projectileBatteryAmmoProviderComponent.Capacity = (int)Math.Round(projectileBatteryAmmoProviderComponent.Capacity/FireCostDiff); projectileBatteryAmmoProviderComponent.Capacity = (int)Math.Round(projectileBatteryAmmoProviderComponent.Capacity / FireCostDiff);
Dirty(uid, projectileBatteryAmmoProviderComponent); Dirty(uid, projectileBatteryAmmoProviderComponent);
var updateClientAmmoEvent = new UpdateClientAmmoEvent(); var updateClientAmmoEvent = new UpdateClientAmmoEvent();
RaiseLocalEvent(uid, ref updateClientAmmoEvent); RaiseLocalEvent(uid, ref updateClientAmmoEvent);
if (user != null)
{
_popupSystem.PopupClient(Loc.GetString("gun-set-fire-mode", ("mode", prototype.Name)), uid, user.Value);
}
} }
} }
} }

View 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-instrument-woodwind = woodwind instrument
construction-insert-info-examine-name-knife = knife construction-insert-info-examine-name-knife = knife
construction-insert-info-examine-name-utensil = utensil 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

View File

@@ -9,6 +9,8 @@ device-frequency-prototype-name-fax = Fax
device-frequency-prototype-name-basic-device = Basic Devices device-frequency-prototype-name-basic-device = Basic Devices
device-frequency-prototype-name-cyborg-control = Cyborg Control device-frequency-prototype-name-cyborg-control = Cyborg Control
device-frequency-prototype-name-robotics-console = Robotics Console 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 ## camera frequencies
device-frequency-prototype-name-surveillance-camera-test = Subnet Test 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-freezer = FZR-
device-address-prefix-volume-pump = VPP- device-address-prefix-volume-pump = VPP-
device-address-prefix-smes = SMS- device-address-prefix-smes = SMS-
device-address-prefix-turret = TRT-
# PDAs and terminals # PDAs and terminals
device-address-prefix-console = CLS- device-address-prefix-console = CLS-

View File

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

View File

@@ -43,6 +43,8 @@ wires-board-name-jukebox = Jukebox
wires-board-name-computer = Computer wires-board-name-computer = Computer
wires-board-name-holopad = Holopad wires-board-name-holopad = Holopad
wires-board-name-barsign = Bar Sign 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. # names that get displayed in the wire hacking hud & admin logs.

View File

@@ -87,6 +87,30 @@
name: device-frequency-prototype-name-cyborg-control name: device-frequency-prototype-name-cyborg-control
frequency: 1292 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. # This frequency will likely have a LARGE number of listening entities. Please don't broadcast on this frequency.
- type: deviceFrequency - type: deviceFrequency
id: SmartLight #used by powered lights. id: SmartLight #used by powered lights.

View File

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

View File

@@ -371,6 +371,9 @@
- type: HitscanBatteryAmmoProvider - type: HitscanBatteryAmmoProvider
proto: RedHeavyLaser proto: RedHeavyLaser
fireCost: 100 fireCost: 100
- type: Tag
tags:
- TurretCompatibleWeapon
- type: entity - type: entity
name: portable particle decelerator name: portable particle decelerator

View File

@@ -127,9 +127,12 @@
maxCharge: 2000 maxCharge: 2000
startingCharge: 0 startingCharge: 0
- type: ApcPowerReceiverBattery - type: ApcPowerReceiverBattery
idlePowerUse: 5 idleLoad: 5
batteryRechargeRate: 200 batteryRechargeRate: 200
batteryRechargeEfficiency: 1.225 batteryRechargeEfficiency: 1.225
- type: ApcPowerReceiver - type: ApcPowerReceiver
powerLoad: 5 powerLoad: 5
- type: ExtensionCableReceiver - type: ExtensionCableReceiver
- type: HTN
rootTask:
task: EnergyTurretCompound

View File

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

View File

@@ -190,6 +190,8 @@
curve: !type:BoolCurve curve: !type:BoolCurve
- !type:TargetIsCritCon - !type:TargetIsCritCon
curve: !type:InverseBoolCurve curve: !type:InverseBoolCurve
- !type:TargetIsStunnedCon
curve: !type:InverseBoolCurve
- !type:TurretTargetingCon - !type:TurretTargetingCon
curve: !type:BoolCurve curve: !type:BoolCurve
- !type:TargetDistanceCon - !type:TargetDistanceCon

View File

@@ -205,3 +205,24 @@
- !type:PowerWireAction - !type:PowerWireAction
- !type:AiInteractWireAction - !type:AiInteractWireAction
- !type:AccessWireAction - !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

View File

@@ -1318,6 +1318,12 @@
- type: Tag - type: Tag
id: Truncheon 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 - type: Tag
id: Unimplantable id: Unimplantable