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:
121
Content.Client/Turrets/DeployableTurretSystem.cs
Normal file
121
Content.Client/Turrets/DeployableTurretSystem.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
@@ -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) ||
|
||||||
|
|||||||
175
Content.Server/Turrets/DeployableTurretSystem.cs
Normal file
175
Content.Server/Turrets/DeployableTurretSystem.cs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
|||||||
161
Content.Shared/Turrets/DeployableTurretComponent.cs
Normal file
161
Content.Shared/Turrets/DeployableTurretComponent.cs
Normal 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),
|
||||||
|
}
|
||||||
167
Content.Shared/Turrets/SharedDeployableTurretSystem.cs
Normal file
167
Content.Shared/Turrets/SharedDeployableTurretSystem.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
|||||||
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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-
|
||||||
|
|||||||
12
Resources/Locale/en-US/weapons/ranged/turrets.ftl
Normal file
12
Resources/Locale/en-US/weapons/ranged/turrets.ftl
Normal 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.
|
||||||
@@ -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.
|
||||||
|
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -204,4 +204,25 @@
|
|||||||
wires:
|
wires:
|
||||||
- !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
|
||||||
|
|
||||||
@@ -1317,6 +1317,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
|
||||||
|
|||||||
Reference in New Issue
Block a user