diff --git a/Content.Client/_EE/Flight/Components/FlightVisualsComponent.cs b/Content.Client/_EE/Flight/Components/FlightVisualsComponent.cs
new file mode 100644
index 0000000000..f1ea3f2564
--- /dev/null
+++ b/Content.Client/_EE/Flight/Components/FlightVisualsComponent.cs
@@ -0,0 +1,36 @@
+using Robust.Client.Graphics;
+
+namespace Content.Client._EE.Flight.Components;
+
+[RegisterComponent]
+public sealed partial class FlightVisualsComponent : Component
+{
+ ///
+ /// The speed of the shader animation.
+ ///
+ [DataField]
+ public float Speed;
+ ///
+ /// How far it goes in any direction.
+ ///
+ [DataField]
+ public float Multiplier;
+
+ ///
+ /// How much the limbs (if there are any) rotate.
+ ///
+ [DataField]
+ public float Offset;
+
+ ///
+ /// Are we animating layers or the entire sprite?
+ ///
+ public bool AnimateLayer = false;
+ public int? TargetLayer;
+
+ [DataField]
+ public string AnimationKey = "default";
+
+ [ViewVariables(VVAccess.ReadWrite)]
+ public ShaderInstance Shader = default!;
+}
\ No newline at end of file
diff --git a/Content.Client/_EE/Flight/FlightSystem.cs b/Content.Client/_EE/Flight/FlightSystem.cs
new file mode 100644
index 0000000000..446210a464
--- /dev/null
+++ b/Content.Client/_EE/Flight/FlightSystem.cs
@@ -0,0 +1,65 @@
+using Robust.Client.GameObjects;
+using Content.Shared._EE.Flight;
+using Content.Shared._EE.Flight.Events;
+using Content.Client._EE.Flight.Components;
+
+namespace Content.Client._EE.Flight;
+
+public sealed class FlightSystem : SharedFlightSystem
+{
+ [Dependency] private readonly IEntityManager _entityManager = default!;
+ public override void Initialize()
+ {
+ base.Initialize();
+ SubscribeNetworkEvent(OnFlight);
+ }
+
+ private void OnFlight(FlightEvent args)
+ {
+ var uid = GetEntity(args.Uid);
+ if (!_entityManager.TryGetComponent(uid, out SpriteComponent? sprite)
+ || !args.IsAnimated
+ || !_entityManager.TryGetComponent(uid, out FlightComponent? flight))
+ return;
+
+ int? targetLayer = null;
+ if (flight.IsLayerAnimated && flight.Layer is not null)
+ {
+ targetLayer = GetAnimatedLayer(uid, flight.Layer, sprite);
+ if (targetLayer == null)
+ return;
+ }
+
+ if (args.IsFlying && args.IsAnimated && flight.AnimationKey != "default")
+ {
+ var comp = new FlightVisualsComponent
+ {
+ AnimateLayer = flight.IsLayerAnimated,
+ AnimationKey = flight.AnimationKey,
+ Multiplier = flight.ShaderMultiplier,
+ Offset = flight.ShaderOffset,
+ Speed = flight.ShaderSpeed,
+ TargetLayer = targetLayer,
+ };
+ AddComp(uid, comp);
+ }
+ if (!args.IsFlying)
+ RemComp(uid);
+ }
+
+ public int? GetAnimatedLayer(EntityUid uid, string targetLayer, SpriteComponent? sprite = null)
+ {
+ if (!Resolve(uid, ref sprite))
+ return null;
+
+ var index = 0;
+ foreach (var layer in sprite.AllLayers)
+ {
+ // This feels like absolute shitcode, isn't there a better way to check for it?
+ if (layer.Rsi?.Path.ToString() == targetLayer)
+ return index;
+ index++;
+ }
+ return null;
+ }
+}
\ No newline at end of file
diff --git a/Content.Client/_EE/Flight/FlyingVisualizerSystem.cs b/Content.Client/_EE/Flight/FlyingVisualizerSystem.cs
new file mode 100644
index 0000000000..c5a01e22e8
--- /dev/null
+++ b/Content.Client/_EE/Flight/FlyingVisualizerSystem.cs
@@ -0,0 +1,62 @@
+using Content.Client._EE.Flight.Components;
+using Robust.Client.GameObjects;
+using Robust.Client.Graphics;
+using Robust.Shared.Prototypes;
+
+namespace Content.Client._EE.Flight;
+
+///
+/// Handles offsetting an entity while flying
+///
+public sealed class FlyingVisualizerSystem : EntitySystem
+{
+ [Dependency] private readonly IPrototypeManager _protoMan = default!;
+ public override void Initialize()
+ {
+ base.Initialize();
+ SubscribeLocalEvent(OnStartup);
+ SubscribeLocalEvent(OnShutdown);
+ SubscribeLocalEvent(OnBeforeShaderPost);
+ }
+
+ private void OnStartup(EntityUid uid, FlightVisualsComponent comp, ComponentStartup args)
+ {
+ comp.Shader = _protoMan.Index(comp.AnimationKey).InstanceUnique();
+ AddShader(uid, comp.Shader, comp.AnimateLayer, comp.TargetLayer);
+ SetValues(comp, comp.Speed, comp.Offset, comp.Multiplier);
+ }
+
+ private void OnShutdown(EntityUid uid, FlightVisualsComponent comp, ComponentShutdown args)
+ {
+ AddShader(uid, null, comp.AnimateLayer, comp.TargetLayer);
+ }
+
+ private void AddShader(Entity entity, ShaderInstance? shader, bool animateLayer, int? layer)
+ {
+ if (!Resolve(entity, ref entity.Comp, false))
+ return;
+
+ entity.Comp.PostShader = shader;
+
+ if (animateLayer && layer is not null)
+ entity.Comp.LayerSetShader(layer.Value, shader);
+
+ entity.Comp.GetScreenTexture = shader is not null;
+ entity.Comp.RaiseShaderEvent = shader is not null;
+ }
+
+ ///
+ /// This function can be used to modify the shader's values while its running.
+ ///
+ private void OnBeforeShaderPost(EntityUid uid, FlightVisualsComponent comp, ref BeforePostShaderRenderEvent args)
+ {
+ SetValues(comp, comp.Speed, comp.Offset, comp.Multiplier);
+ }
+
+ private void SetValues(FlightVisualsComponent comp, float speed, float offset, float multiplier)
+ {
+ comp.Shader.SetParameter("Speed", speed);
+ comp.Shader.SetParameter("Offset", offset);
+ comp.Shader.SetParameter("Multiplier", multiplier);
+ }
+}
\ No newline at end of file
diff --git a/Content.Server/_EE/Flight/FlightSystem.cs b/Content.Server/_EE/Flight/FlightSystem.cs
new file mode 100644
index 0000000000..0dae42588c
--- /dev/null
+++ b/Content.Server/_EE/Flight/FlightSystem.cs
@@ -0,0 +1,5 @@
+using Content.Shared._EE.Flight;
+
+namespace Content.Server._EE.Flight;
+
+public sealed class FlightSystem : SharedFlightSystem;
\ No newline at end of file
diff --git a/Content.Server/_EE/FootPrint/FootPrintsSystem.cs b/Content.Server/_EE/FootPrint/FootPrintsSystem.cs
index 7531cd8ef3..b8fff2a5aa 100644
--- a/Content.Server/_EE/FootPrint/FootPrintsSystem.cs
+++ b/Content.Server/_EE/FootPrint/FootPrintsSystem.cs
@@ -1,9 +1,9 @@
using Content.Server.Atmos.Components;
+using Content.Shared._EE.Flight; // DeltaV
using Content.Shared._EE.FootPrint;
using Content.Shared.Inventory;
using Content.Shared.Mobs;
using Content.Shared.Mobs.Components;
-using Content.Shared._EE.FootPrint;
// using Content.Shared.Standing;
using Content.Shared.Chemistry.Components.SolutionManager;
using Content.Shared.Chemistry.EntitySystems;
@@ -21,10 +21,12 @@ public sealed class FootPrintsSystem : EntitySystem
[Dependency] private readonly SharedSolutionContainerSystem _solution = default!;
[Dependency] private readonly SharedAppearanceSystem _appearance = default!;
[Dependency] private readonly SharedTransformSystem _transform = default!;
+ [Dependency] private readonly SharedFlightSystem _flight = default!; // DeltaV
private EntityQuery _transformQuery;
private EntityQuery _mobThresholdQuery;
private EntityQuery _appearanceQuery;
+
// private EntityQuery _layingQuery;
public override void Initialize()
@@ -47,6 +49,9 @@ public sealed class FootPrintsSystem : EntitySystem
private void OnMove(EntityUid uid, FootPrintsComponent component, ref MoveEvent args)
{
+ if (_flight.IsFlying(uid)) // DeltaV - Flying players won't make footprints
+ return;
+
if (component.PrintsColor.A <= 0f
|| !_transformQuery.TryComp(uid, out var transform)
|| !_mobThresholdQuery.TryComp(uid, out var mobThreshHolds)
diff --git a/Content.Server/_EE/FootPrint/PuddleFootPrintsSystem.cs b/Content.Server/_EE/FootPrint/PuddleFootPrintsSystem.cs
index 810bada5e2..765daa8e3d 100644
--- a/Content.Server/_EE/FootPrint/PuddleFootPrintsSystem.cs
+++ b/Content.Server/_EE/FootPrint/PuddleFootPrintsSystem.cs
@@ -1,4 +1,5 @@
using System.Linq;
+using Content.Shared._EE.Flight; // DeltaV
using Content.Shared._EE.FootPrint;
using Content.Shared.Chemistry.Components.SolutionManager;
using Content.Shared.Chemistry.EntitySystems;
@@ -12,6 +13,7 @@ public sealed class PuddleFootPrintsSystem : EntitySystem
{
[Dependency] private readonly SharedAppearanceSystem _appearance = default!;
[Dependency] private readonly SharedSolutionContainerSystem _solutionContainer = default!;
+ [Dependency] private readonly SharedFlightSystem _flight = default!; // DeltaV
public override void Initialize()
{
@@ -21,6 +23,9 @@ public sealed class PuddleFootPrintsSystem : EntitySystem
private void OnStepTrigger(EntityUid uid, PuddleFootPrintsComponent component, ref EndCollideEvent args)
{
+ if (_flight.IsFlying(uid)) // DeltaV - Flying players won't make footprints
+ return;
+
if (!TryComp(uid, out var appearance)
|| !TryComp(uid, out var puddle)
|| !TryComp(args.OtherEntity, out var tripper)
diff --git a/Content.Shared/Cuffs/SharedCuffableSystem.cs b/Content.Shared/Cuffs/SharedCuffableSystem.cs
index 3b0f6c8a30..267d753482 100644
--- a/Content.Shared/Cuffs/SharedCuffableSystem.cs
+++ b/Content.Shared/Cuffs/SharedCuffableSystem.cs
@@ -1,4 +1,5 @@
using System.Linq;
+using Content.Shared._EE.Flight; // DeltaV - Harpy Flight
using Content.Shared.ActionBlocker;
using Content.Shared.Administration.Components;
using Content.Shared.Administration.Logs;
@@ -55,6 +56,7 @@ namespace Content.Shared.Cuffs
[Dependency] private readonly SharedTransformSystem _transform = default!;
[Dependency] private readonly UseDelaySystem _delay = default!;
[Dependency] private readonly SharedCombatModeSystem _combatMode = default!;
+ [Dependency] private readonly SharedFlightSystem _flight = default!; // DeltaV - Harpy flight
public override void Initialize()
{
@@ -514,6 +516,15 @@ namespace Content.Shared.Cuffs
return false;
}
+ // EE - Harpy Flight
+ if (_flight.IsFlying(target))
+ {
+ _popup.PopupClient(Loc.GetString("handcuff-component-target-flying-error",
+ ("targetName", Identity.Name(target, EntityManager, user))), user, user);
+ return false;
+ }
+ // END EE
+
var cuffTime = handcuffComponent.CuffTime;
if (HasComp(target))
diff --git a/Content.Shared/Damage/Systems/SharedStaminaSystem.cs b/Content.Shared/Damage/Systems/SharedStaminaSystem.cs
index e5650266b7..2e73247a1a 100644
--- a/Content.Shared/Damage/Systems/SharedStaminaSystem.cs
+++ b/Content.Shared/Damage/Systems/SharedStaminaSystem.cs
@@ -45,6 +45,7 @@ public abstract partial class SharedStaminaSystem : EntitySystem
[Dependency] private readonly SharedColorFlashEffectSystem _color = default!;
[Dependency] private readonly StatusEffectsSystem _status = default!;
[Dependency] protected readonly SharedStunSystem StunSystem = default!;
+ [Dependency] private readonly MovementSpeedModifierSystem _movement = default!; // EE - Harpy Flight
///
/// How much of a buffer is there between the stun duration and when stuns can be re-applied.
@@ -265,7 +266,8 @@ public abstract partial class SharedStaminaSystem : EntitySystem
}
public void TakeStaminaDamage(EntityUid uid, float value, StaminaComponent? component = null,
- EntityUid? source = null, EntityUid? with = null, bool visual = true, SoundSpecifier? sound = null, bool ignoreResist = false)
+ EntityUid? source = null, EntityUid? with = null, bool visual = true, SoundSpecifier? sound = null, bool ignoreResist = false,
+ bool? allowsSlowdown = true) // EE - Harpy Flight
{
if (!Resolve(uid, ref component, false))
return;
@@ -283,6 +285,9 @@ public abstract partial class SharedStaminaSystem : EntitySystem
value = UniversalStaminaDamageModifier * value;
+ if (allowsSlowdown == true) // EE - Harpy Flight
+ _movement.RefreshMovementSpeedModifiers(uid);
+
// Have we already reached the point of max stamina damage?
if (component.Critical)
return;
@@ -362,12 +367,23 @@ public abstract partial class SharedStaminaSystem : EntitySystem
{
// Just in case we have active but not stamina we'll check and account for it.
if (!stamQuery.TryGetComponent(uid, out var comp) ||
- comp.StaminaDamage <= 0f && !comp.Critical)
+ comp.StaminaDamage <= 0f && !comp.Critical && comp.ActiveDrains.Count == 0) // EE - Harpy Flight
{
RemComp(uid);
continue;
}
+ // EE - Harpy Flight
+ if (comp.ActiveDrains.Count > 0)
+ foreach (var (source, (drainRate, modifiesSpeed)) in comp.ActiveDrains)
+ TakeStaminaDamage(uid,
+ drainRate * frameTime,
+ comp,
+ source: source,
+ visual: false,
+ allowsSlowdown: modifiesSpeed);
+ // End EE
+
// Shouldn't need to consider paused time as we're only iterating non-paused stamina components.
var nextUpdate = comp.NextUpdate;
@@ -380,10 +396,11 @@ public abstract partial class SharedStaminaSystem : EntitySystem
comp.NextUpdate += TimeSpan.FromSeconds(1f);
- TakeStaminaDamage(
- uid,
- comp.AfterCritical ? -comp.Decay * comp.AfterCritDecayMultiplier : -comp.Decay, // Recover faster after crit
- comp);
+ if (comp.ActiveDrains.Count == 0) // EE - Harpy Flight
+ TakeStaminaDamage(
+ uid,
+ comp.AfterCritical ? -comp.Decay * comp.AfterCritDecayMultiplier : -comp.Decay, // Recover faster after crit
+ comp);
Dirty(uid, comp);
}
diff --git a/Content.Shared/Gravity/SharedFloatingVisualizerSystem.cs b/Content.Shared/Gravity/SharedFloatingVisualizerSystem.cs
index ae5c73b498..f190467da3 100644
--- a/Content.Shared/Gravity/SharedFloatingVisualizerSystem.cs
+++ b/Content.Shared/Gravity/SharedFloatingVisualizerSystem.cs
@@ -1,12 +1,13 @@
using System.Numerics;
using Robust.Shared.Map;
+using Content.Shared._EE.Flight.Events;
namespace Content.Shared.Gravity;
///
/// Handles offsetting a sprite when there is no gravity
///
-public abstract class SharedFloatingVisualizerSystem : EntitySystem
+public abstract partial class SharedFloatingVisualizerSystem : EntitySystem // DeltaV - Made Partial for Harpy Flying
{
[Dependency] private readonly SharedGravitySystem _gravity = default!;
@@ -16,6 +17,8 @@ public abstract class SharedFloatingVisualizerSystem : EntitySystem
SubscribeLocalEvent(OnComponentStartup);
SubscribeLocalEvent(OnWeightlessnessChanged);
+
+ SubscribeNetworkEvent(OnFlight);
}
///
diff --git a/Content.Shared/Gravity/SharedGravitySystem.cs b/Content.Shared/Gravity/SharedGravitySystem.cs
index 4ba312f4e0..06fe37cf17 100644
--- a/Content.Shared/Gravity/SharedGravitySystem.cs
+++ b/Content.Shared/Gravity/SharedGravitySystem.cs
@@ -1,3 +1,4 @@
+using Content.Shared._EE.Flight; // DeltaV - Harpy Flight
using Content.Shared.Alert;
using Content.Shared.Inventory;
using Content.Shared.Throwing;
@@ -16,6 +17,7 @@ public abstract partial class SharedGravitySystem : EntitySystem
{
[Dependency] protected readonly IGameTiming Timing = default!;
[Dependency] private readonly AlertsSystem _alerts = default!;
+ [Dependency] private readonly SharedFlightSystem _flight = default!; // DeltaV - Harpy Flight
public static readonly ProtoId WeightlessAlert = "Weightless";
@@ -69,6 +71,9 @@ public abstract partial class SharedGravitySystem : EntitySystem
if (entity.Comp2.BodyType is BodyType.Static or BodyType.Kinematic)
return false;
+ if (_flight.IsFlying(entity.Owner)) // DeltaV - Harpy Flight
+ return true;
+
// Check if something other than the grid or map is overriding our gravity
var ev = new IsWeightlessEvent();
RaiseLocalEvent(entity, ref ev);
diff --git a/Content.Shared/_EE/Damage/Components/StaminaComponent.Flying.cs b/Content.Shared/_EE/Damage/Components/StaminaComponent.Flying.cs
new file mode 100644
index 0000000000..f7a900b9ee
--- /dev/null
+++ b/Content.Shared/_EE/Damage/Components/StaminaComponent.Flying.cs
@@ -0,0 +1,13 @@
+namespace Content.Shared.Damage.Components;
+
+public sealed partial class StaminaComponent : Component
+{
+ ///
+ /// A dictionary of active stamina drains, with the key being the source of the drain,
+ /// DrainRate how much it changes per tick, and ModifiesSpeed if it should slow down the user.
+ ///
+ /// Used primarily for harpy flying.
+ ///
+ [DataField, AutoNetworkedField]
+ public Dictionary ActiveDrains = new();
+}
\ No newline at end of file
diff --git a/Content.Shared/_EE/Damage/Systems/SharedStaminaSystem.Flying.cs b/Content.Shared/_EE/Damage/Systems/SharedStaminaSystem.Flying.cs
new file mode 100644
index 0000000000..a9bc4da632
--- /dev/null
+++ b/Content.Shared/_EE/Damage/Systems/SharedStaminaSystem.Flying.cs
@@ -0,0 +1,25 @@
+using Content.Shared.Damage.Components;
+
+namespace Content.Shared.Damage.Systems;
+
+public abstract partial class SharedStaminaSystem : EntitySystem
+{
+ public void ToggleStaminaDrain(EntityUid target, float drainRate, bool enabled, bool modifiesSpeed, EntityUid? source = null)
+ {
+ if (!TryComp(target, out var stamina))
+ return;
+
+ // If theres no source, we assume its the target that caused the drain.
+ var actualSource = source ?? target;
+
+ if (enabled)
+ {
+ stamina.ActiveDrains[actualSource] = (drainRate, modifiesSpeed);
+ EnsureComp(target);
+ }
+ else
+ stamina.ActiveDrains.Remove(actualSource);
+
+ Dirty(target, stamina);
+ }
+}
\ No newline at end of file
diff --git a/Content.Shared/_EE/Flight/Components/FlightComponent.cs b/Content.Shared/_EE/Flight/Components/FlightComponent.cs
new file mode 100644
index 0000000000..3c761e567e
--- /dev/null
+++ b/Content.Shared/_EE/Flight/Components/FlightComponent.cs
@@ -0,0 +1,120 @@
+using Robust.Shared.Audio;
+using Robust.Shared.GameStates;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
+
+namespace Content.Shared._EE.Flight;
+
+///
+/// Adds an action that allows the user to become temporarily
+/// weightless at the cost of stamina and hand usage.
+///
+[RegisterComponent, NetworkedComponent(), AutoGenerateComponentState]
+public sealed partial class FlightComponent : Component
+{
+ [DataField(customTypeSerializer: typeof(PrototypeIdSerializer))]
+ public string? ToggleAction = "ActionToggleFlight";
+
+ [DataField, AutoNetworkedField]
+ public EntityUid? ToggleActionEntity;
+
+ ///
+ /// Is the user flying right now?
+ ///
+ [DataField, AutoNetworkedField]
+ public bool IsCurrentlyFlying;
+
+ ///
+ /// Stamina drain per second when flying
+ ///
+ [DataField, AutoNetworkedField]
+ public float StaminaDrainRate = 10.0f;
+
+ ///
+ /// DeltaV - Stamina cost when taking off.
+ ///
+ [DataField, AutoNetworkedField]
+ public float InitialStaminaCost = 0f;
+
+ ///
+ /// DoAfter delay until the user becomes weightless.
+ ///
+ [DataField, AutoNetworkedField]
+ public float ActivationDelay = 0.5f;
+
+ ///
+ /// Speed modifier while in flight
+ ///
+ [DataField, AutoNetworkedField]
+ public float SpeedModifier = 2.0f;
+
+ ///
+ /// DeltaV - Friction modifier while in flight. Should be less than one so
+ /// they have less control while flying. Also applies to friction with no inputs.
+ ///
+ [DataField, AutoNetworkedField]
+ public float FrictionModifier = 1f;
+
+ ///
+ /// DeltaV - Acceleration modifer while in flight.
+ ///
+ [DataField, AutoNetworkedField]
+ public float AccelerationModifer = 1.5f;
+
+ ///
+ /// Path to a sound specifier or collection for the noises made during flight
+ ///
+ [DataField]
+ public SoundSpecifier FlapSound = new SoundCollectionSpecifier("WingFlaps");
+
+ ///
+ /// Is the flight animated?
+ ///
+ [DataField]
+ public bool IsAnimated = true;
+
+ ///
+ /// Does the animation animate a layer?.
+ ///
+ [DataField]
+ public bool IsLayerAnimated;
+
+ ///
+ /// Which RSI layer path does this animate?
+ ///
+ [DataField]
+ public string? Layer;
+
+ ///
+ /// Whats the speed of the shader?
+ ///
+ [DataField]
+ public float ShaderSpeed = 6.0f;
+
+ ///
+ /// How much are the values in the shader's calculations multiplied by?
+ ///
+ [DataField]
+ public float ShaderMultiplier = 0.02f;
+
+ ///
+ /// What is the offset on the shader?
+ ///
+ [DataField]
+ public float ShaderOffset = 0.25f;
+
+ ///
+ /// What animation does the flight use?
+ ///
+
+ [DataField]
+ public string AnimationKey = "default";
+
+ ///
+ /// Time between sounds being played
+ ///
+ [DataField]
+ public float FlapInterval = 1.0f;
+
+ public float TimeUntilFlap;
+}
\ No newline at end of file
diff --git a/Content.Shared/_EE/Flight/Events.cs b/Content.Shared/_EE/Flight/Events.cs
new file mode 100644
index 0000000000..4848fb4de2
--- /dev/null
+++ b/Content.Shared/_EE/Flight/Events.cs
@@ -0,0 +1,15 @@
+using Robust.Shared.Serialization;
+using Content.Shared.DoAfter;
+
+namespace Content.Shared._EE.Flight.Events;
+
+[Serializable, NetSerializable]
+public sealed partial class FlightDoAfterEvent : SimpleDoAfterEvent;
+
+[Serializable, NetSerializable]
+public sealed class FlightEvent(NetEntity uid, bool isFlying, bool isAnimated) : EntityEventArgs
+{
+ public NetEntity Uid { get; } = uid;
+ public bool IsFlying { get; } = isFlying;
+ public bool IsAnimated { get; } = isAnimated;
+}
\ No newline at end of file
diff --git a/Content.Shared/_EE/Flight/SharedFlightSystem.cs b/Content.Shared/_EE/Flight/SharedFlightSystem.cs
new file mode 100644
index 0000000000..77ca700779
--- /dev/null
+++ b/Content.Shared/_EE/Flight/SharedFlightSystem.cs
@@ -0,0 +1,321 @@
+using Content.Shared.Actions;
+using Content.Shared.Movement.Systems;
+using Content.Shared.Damage.Systems;
+using Content.Shared.Gravity;
+using Content.Shared.Hands.Components;
+using Content.Shared.Hands.EntitySystems;
+using Content.Shared.Interaction.Components;
+using Content.Shared.Inventory.VirtualItem;
+using Content.Shared._EE.Flight.Events;
+using Content.Shared.Standing;
+using Content.Shared.Bed.Sleep;
+using Content.Shared.Cuffs.Components;
+using Content.Shared.Damage.Components;
+using Content.Shared.DoAfter;
+using Content.Shared.Mobs;
+using Content.Shared.Popups;
+using Content.Shared.Stunnable;
+using Content.Shared.StepTrigger.Systems;
+using Content.Shared.Zombies;
+using Robust.Shared.Audio.Systems;
+
+namespace Content.Shared._EE.Flight;
+
+public abstract class SharedFlightSystem : EntitySystem
+{
+ [Dependency] private readonly SharedActionsSystem _actionsSystem = default!;
+ [Dependency] private readonly SharedVirtualItemSystem _virtualItem = default!;
+ [Dependency] private readonly SharedStaminaSystem _stamina = default!;
+ [Dependency] private readonly SharedHandsSystem _hands = default!;
+ [Dependency] private readonly MovementSpeedModifierSystem _movementSpeed = default!;
+ [Dependency] private readonly SharedAudioSystem _audio = default!;
+ [Dependency] private readonly SharedDoAfterSystem _doAfter = default!;
+ [Dependency] private readonly SharedPopupSystem _popupSystem = default!;
+ [Dependency] private readonly StandingStateSystem _standing = default!;
+ [Dependency] private readonly SharedGravitySystem _gravity = default!;
+
+
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ SubscribeLocalEvent(OnStartup);
+ SubscribeLocalEvent(OnShutdown);
+ SubscribeLocalEvent(OnRefreshMoveSpeed);
+ SubscribeLocalEvent(OnRefreshFrictionModifiers);
+ SubscribeLocalEvent(OnRefreshWeightlessModifiers);
+
+ SubscribeLocalEvent(OnToggleFlight);
+ SubscribeLocalEvent(OnFlightDoAfter);
+ SubscribeLocalEvent(OnMobStateChangedEvent);
+ SubscribeLocalEvent(OnZombified);
+ SubscribeLocalEvent(OnKnockedDown);
+ SubscribeLocalEvent(OnStunned);
+ SubscribeLocalEvent(OnSleep);
+ SubscribeLocalEvent(OnStepTriggerAttempt);
+ }
+
+ public override void Update(float frameTime)
+ {
+ base.Update(frameTime);
+
+ var query = EntityQueryEnumerator();
+ while (query.MoveNext(out var uid, out var component))
+ {
+ if (!component.IsCurrentlyFlying)
+ continue;
+
+ component.TimeUntilFlap -= frameTime;
+
+ if (component.TimeUntilFlap > 0f)
+ continue;
+
+ _audio.PlayPredicted(component.FlapSound, uid, uid);
+ component.TimeUntilFlap = component.FlapInterval;
+
+ }
+ }
+ #region Query Functions
+
+ public bool IsFlying(Entity entity)
+ {
+ if (!Resolve(entity, ref entity.Comp, false))
+ return false;
+
+ return entity.Comp.IsCurrentlyFlying;
+ }
+
+ #endregion
+
+
+ #region Core Functions
+
+ public void ToggleActive(Entity ent, bool active)
+ {
+ ent.Comp.IsCurrentlyFlying = active;
+ ent.Comp.TimeUntilFlap = 0f;
+ _actionsSystem.SetToggled(ent.Comp.ToggleActionEntity, ent.Comp.IsCurrentlyFlying);
+ RaiseNetworkEvent(new FlightEvent(GetNetEntity(ent), ent.Comp.IsCurrentlyFlying, ent.Comp.IsAnimated));
+ UpdateHands(ent, active);
+ _stamina.TryTakeStamina(ent.Owner, ent.Comp.InitialStaminaCost, visual: false);
+ _stamina.ToggleStaminaDrain(ent, ent.Comp.StaminaDrainRate, active, false);
+
+ _gravity.RefreshWeightless(ent.Owner, active);
+ _movementSpeed.RefreshMovementSpeedModifiers(ent);
+ _movementSpeed.RefreshFrictionModifiers(ent);
+ _movementSpeed.RefreshWeightlessModifiers(ent);
+
+ Dirty(ent, ent.Comp);
+ }
+
+ private bool CanFly(EntityUid uid, FlightComponent component)
+ {
+ if (TryComp(uid, out var standing) && _standing.IsDown((uid, standing)))
+ {
+ _popupSystem.PopupClient(Loc.GetString("no-flight-while-down"), uid, uid, PopupType.Small);
+ return false;
+ }
+
+ if (TryComp(uid, out var cuffableComp) && !cuffableComp.CanStillInteract)
+ {
+ _popupSystem.PopupClient(Loc.GetString("no-flight-while-restrained"), uid, uid, PopupType.Small);
+ return false;
+ }
+
+ if (HasComp(uid))
+ {
+ _popupSystem.PopupClient(Loc.GetString("no-flight-while-zombified"), uid, uid, PopupType.Small);
+ return false;
+ }
+
+ // Got to have stamina to fly
+ if (!TryComp(uid, out var stam))
+ return false;
+
+ var hasEnoughStamina = stam.StaminaDamage + component.InitialStaminaCost < stam.CritThreshold || stam.Critical;
+ if (!hasEnoughStamina)
+ {
+ _popupSystem.PopupClient(Loc.GetString("no-flight-exhausted"), uid, uid, PopupType.MediumCaution);
+ return false;
+ }
+
+ // All preflight checks complete, ready for take-off!
+ return true;
+ }
+
+ private void UpdateHands(EntityUid uid, bool flying)
+ {
+ if (!TryComp(uid, out var handsComponent))
+ return;
+
+ if (flying)
+ BlockHands(uid, handsComponent);
+ else
+ FreeHands(uid);
+ }
+
+ private void BlockHands(EntityUid uid, HandsComponent handsComponent)
+ {
+ var freeHands = 0;
+ foreach (var hand in _hands.EnumerateHands((uid, handsComponent)))
+ {
+ if (!_hands.TryGetHeldItem((uid, handsComponent), hand, out var heldItem))
+ {
+ freeHands++;
+ continue;
+ }
+
+ // Is this entity removable? (they might have handcuffs on)
+ if (HasComp(heldItem) && heldItem != uid)
+ continue;
+
+ if (_hands.TryDrop((uid, handsComponent), hand))
+ {
+ freeHands++;
+ }
+
+ if (freeHands == 2)
+ break;
+ }
+ if (_virtualItem.TrySpawnVirtualItemInHand(uid, uid, out var virtItem1))
+ EnsureComp(virtItem1.Value);
+
+ if (_virtualItem.TrySpawnVirtualItemInHand(uid, uid, out var virtItem2))
+ EnsureComp(virtItem2.Value);
+ }
+
+ private void FreeHands(EntityUid uid)
+ {
+ _virtualItem.DeleteInHandsMatching(uid, uid);
+ }
+
+ #endregion
+
+ #region Events
+ private void OnStartup(EntityUid uid, FlightComponent component, ComponentStartup args)
+ {
+ _actionsSystem.AddAction(uid, ref component.ToggleActionEntity, component.ToggleAction);
+ }
+
+ private void OnShutdown(EntityUid uid, FlightComponent component, ComponentShutdown args)
+ {
+ _actionsSystem.RemoveAction(uid, component.ToggleActionEntity);
+ }
+ private void OnRefreshMoveSpeed(EntityUid uid, FlightComponent component, RefreshMovementSpeedModifiersEvent args)
+ {
+ if (!component.IsCurrentlyFlying) // If we're not flying, don't apply flying's modifier
+ return;
+
+ args.ModifySpeed(component.SpeedModifier, component.SpeedModifier);
+ }
+
+ // DeltaV - Since we use the new movement system and EE doesn't, we got to also apply friction modifiers.
+ private void OnRefreshFrictionModifiers(Entity ent, ref RefreshFrictionModifiersEvent args)
+ {
+ if (!ent.Comp.IsCurrentlyFlying) // If we're not flying, don't apply flying's modifier
+ return;
+
+ args.ModifyFriction(ent.Comp.FrictionModifier, ent.Comp.FrictionModifier);
+ args.ModifyAcceleration(ent.Comp.AccelerationModifer);
+ }
+
+ private void OnRefreshWeightlessModifiers(Entity ent, ref RefreshWeightlessModifiersEvent args)
+ {
+ if (!ent.Comp.IsCurrentlyFlying) // If we're not flying, don't apply flying's modifier
+ return;
+
+ //args.ModifyFriction(ent.Comp.FrictionModifier, ent.Comp.FrictionModifier);
+ args.ModifyAcceleration(ent.Comp.AccelerationModifer);
+ }
+
+ private void OnToggleFlight(EntityUid uid, FlightComponent component, ToggleFlightEvent args)
+ {
+ // If the user isnt flying, we check for conditionals and initiate a doafter.
+ if (!component.IsCurrentlyFlying)
+ {
+ if (!CanFly(uid, component))
+ return;
+
+ var doAfterArgs = new DoAfterArgs(EntityManager,
+ uid, component.ActivationDelay,
+ new FlightDoAfterEvent(), uid, target: uid)
+ {
+ BlockDuplicate = true,
+ BreakOnMove = true,
+ BreakOnDamage = true,
+ NeedHand = true
+ };
+
+ if (!_doAfter.TryStartDoAfter(doAfterArgs))
+ return;
+ }
+ else
+ ToggleActive((uid, component), false);
+ }
+
+ private void OnFlightDoAfter(EntityUid uid, FlightComponent component, FlightDoAfterEvent args)
+ {
+ if (args.Handled || args.Cancelled)
+ return;
+
+ ToggleActive((uid, component), true);
+ args.Handled = true;
+ }
+
+ private void OnMobStateChangedEvent(EntityUid uid, FlightComponent component, MobStateChangedEvent args)
+ {
+ if (!component.IsCurrentlyFlying || args.NewMobState is MobState.Critical or MobState.Dead)
+ return;
+
+ ToggleActive((args.Target, component), false);
+ }
+
+ private void OnZombified(EntityUid uid, FlightComponent component, ref EntityZombifiedEvent args)
+ {
+ if (!component.IsCurrentlyFlying)
+ return;
+
+ ToggleActive((args.Target, component), false);
+
+ if (!TryComp(uid, out var stamina))
+ return;
+
+ Dirty(uid, stamina);
+ }
+
+ private void OnKnockedDown(EntityUid uid, FlightComponent component, ref KnockedDownEvent args)
+ {
+ if (!component.IsCurrentlyFlying)
+ return;
+
+ ToggleActive((uid, component), false);
+ }
+
+ private void OnStunned(EntityUid uid, FlightComponent component, ref StunnedEvent args)
+ {
+ if (!component.IsCurrentlyFlying)
+ return;
+
+ ToggleActive((uid, component), false);
+ }
+
+ private void OnSleep(EntityUid uid, FlightComponent component, ref SleepStateChangedEvent args)
+ {
+ if (!component.IsCurrentlyFlying || !args.FellAsleep)
+ return;
+
+ ToggleActive((uid, component), false);
+ if (!TryComp(uid, out var stamina))
+ return;
+
+ Dirty(uid, stamina);
+ }
+ private void OnStepTriggerAttempt(Entity ent, ref StepTriggerAttemptEvent args)
+ {
+ if (ent.Comp.IsCurrentlyFlying)
+ args.Cancelled = true;
+ }
+
+ #endregion
+}
+public sealed partial class ToggleFlightEvent : InstantActionEvent { }
\ No newline at end of file
diff --git a/Content.Shared/_EE/Gravity/SharedFloatingVisualizerSystem.Flying.cs b/Content.Shared/_EE/Gravity/SharedFloatingVisualizerSystem.Flying.cs
new file mode 100644
index 0000000000..ba23954247
--- /dev/null
+++ b/Content.Shared/_EE/Gravity/SharedFloatingVisualizerSystem.Flying.cs
@@ -0,0 +1,23 @@
+using Content.Shared._EE.Flight.Events;
+
+namespace Content.Shared.Gravity;
+
+///
+/// Handles flying event handlers.
+///
+public abstract partial class SharedFloatingVisualizerSystem : EntitySystem
+{
+ private void OnFlight(FlightEvent args)
+ {
+ var uid = GetEntity(args.Uid);
+ if (!TryComp(uid, out var floating))
+ return;
+
+ floating.CanFloat = args.IsFlying;
+
+ if (!args.IsFlying || !args.IsAnimated)
+ return;
+
+ FloatAnimation(uid, floating.Offset, floating.AnimationKey, floating.AnimationTime);
+ }
+}
\ No newline at end of file
diff --git a/Resources/Audio/_DV/Effects/Flight/attributions.yml b/Resources/Audio/_DV/Effects/Flight/attributions.yml
new file mode 100644
index 0000000000..c965360cdf
--- /dev/null
+++ b/Resources/Audio/_DV/Effects/Flight/attributions.yml
@@ -0,0 +1,12 @@
+- files:
+ - "wingflap1.ogg"
+ - "wingflap2.ogg"
+ - "wingflap3.ogg"
+ - "wingflap4.ogg"
+ - "wingflap5.ogg"
+ - "wingflap6.ogg"
+ - "wingflap7.ogg"
+ - "wingflap8.ogg"
+ license: "CC0-1.0"
+ copyright: "Wing Flap originally made by DRAGONSTUDIO. Split into 1s segments by ShepardToTheStars."
+ source: "https://ko-fi.com/s/964e22818d/"
diff --git a/Resources/Audio/_DV/Effects/Flight/wingflap1.ogg b/Resources/Audio/_DV/Effects/Flight/wingflap1.ogg
new file mode 100644
index 0000000000..c72ea2fa18
Binary files /dev/null and b/Resources/Audio/_DV/Effects/Flight/wingflap1.ogg differ
diff --git a/Resources/Audio/_DV/Effects/Flight/wingflap2.ogg b/Resources/Audio/_DV/Effects/Flight/wingflap2.ogg
new file mode 100644
index 0000000000..dc44ca72ea
Binary files /dev/null and b/Resources/Audio/_DV/Effects/Flight/wingflap2.ogg differ
diff --git a/Resources/Audio/_DV/Effects/Flight/wingflap3.ogg b/Resources/Audio/_DV/Effects/Flight/wingflap3.ogg
new file mode 100644
index 0000000000..34ed2b6e53
Binary files /dev/null and b/Resources/Audio/_DV/Effects/Flight/wingflap3.ogg differ
diff --git a/Resources/Audio/_DV/Effects/Flight/wingflap4.ogg b/Resources/Audio/_DV/Effects/Flight/wingflap4.ogg
new file mode 100644
index 0000000000..45fdfe0afb
Binary files /dev/null and b/Resources/Audio/_DV/Effects/Flight/wingflap4.ogg differ
diff --git a/Resources/Audio/_DV/Effects/Flight/wingflap5.ogg b/Resources/Audio/_DV/Effects/Flight/wingflap5.ogg
new file mode 100644
index 0000000000..0fd5e1b9fc
Binary files /dev/null and b/Resources/Audio/_DV/Effects/Flight/wingflap5.ogg differ
diff --git a/Resources/Audio/_DV/Effects/Flight/wingflap6.ogg b/Resources/Audio/_DV/Effects/Flight/wingflap6.ogg
new file mode 100644
index 0000000000..25ea109ad7
Binary files /dev/null and b/Resources/Audio/_DV/Effects/Flight/wingflap6.ogg differ
diff --git a/Resources/Audio/_DV/Effects/Flight/wingflap7.ogg b/Resources/Audio/_DV/Effects/Flight/wingflap7.ogg
new file mode 100644
index 0000000000..d02a8cf22a
Binary files /dev/null and b/Resources/Audio/_DV/Effects/Flight/wingflap7.ogg differ
diff --git a/Resources/Audio/_DV/Effects/Flight/wingflap8.ogg b/Resources/Audio/_DV/Effects/Flight/wingflap8.ogg
new file mode 100644
index 0000000000..b178236c9b
Binary files /dev/null and b/Resources/Audio/_DV/Effects/Flight/wingflap8.ogg differ
diff --git a/Resources/Locale/en-US/_EE/flight/flight_system.ftl b/Resources/Locale/en-US/_EE/flight/flight_system.ftl
new file mode 100644
index 0000000000..5529c02051
--- /dev/null
+++ b/Resources/Locale/en-US/_EE/flight/flight_system.ftl
@@ -0,0 +1,7 @@
+handcuff-component-target-flying-error = You cannot reach {$targetName}'s hands!
+
+no-flight-while-restrained = You can't fly right now.
+no-flight-while-zombified = You can't use your wings right now.
+
+no-flight-while-down = You can't fly while crawling on the ground.
+no-flight-exhausted = You are too exhausted to take flight.
\ No newline at end of file
diff --git a/Resources/Prototypes/_DV/Entities/Mobs/Species/harpy.yml b/Resources/Prototypes/_DV/Entities/Mobs/Species/harpy.yml
index d16414869c..89a6486b39 100644
--- a/Resources/Prototypes/_DV/Entities/Mobs/Species/harpy.yml
+++ b/Resources/Prototypes/_DV/Entities/Mobs/Species/harpy.yml
@@ -6,6 +6,10 @@
abstract: true
components:
- type: HarpySinger
+ - type: Flight # EE - Flight
+ isLayerAnimated: true
+ layer: "/Textures/_EE/Mobs/Customization/Harpy/harpy_wings.rsi"
+ animationKey: "Flap"
- type: Instrument
allowPercussion: false
program: 52
@@ -236,5 +240,16 @@
- type: Action
icon: _DV/Interface/Actions/harpy_syrinx.png
itemIconStyle: BigAction
+
+- type: entity
+ id: ActionToggleFlight
+ name: Fly
+ description: Make use of your wings to fly. Beat the flightless bird allegations.
+ components:
+ - type: Action
+ itemIconStyle: BigAction
+ icon: { sprite: _EE/Interface/Actions/flight.rsi, state: flight_off }
+ iconOn: { sprite : _EE/Interface/Actions/flight.rsi, state: flight_on }
+ checkCanInteract: false
- type: InstantAction
- event: !type:VoiceMaskSetNameEvent
+ event: !type:ToggleFlightEvent
diff --git a/Resources/Prototypes/_DV/SoundCollections/flight.yml b/Resources/Prototypes/_DV/SoundCollections/flight.yml
new file mode 100644
index 0000000000..dcb26f9c8f
--- /dev/null
+++ b/Resources/Prototypes/_DV/SoundCollections/flight.yml
@@ -0,0 +1,11 @@
+- type: soundCollection
+ id: WingFlaps
+ files:
+ - /Audio/_DV/Effects/Flight/wingflap1.ogg
+ - /Audio/_DV/Effects/Flight/wingflap2.ogg
+ - /Audio/_DV/Effects/Flight/wingflap3.ogg
+ - /Audio/_DV/Effects/Flight/wingflap4.ogg
+ - /Audio/_DV/Effects/Flight/wingflap5.ogg
+ - /Audio/_DV/Effects/Flight/wingflap6.ogg
+ - /Audio/_DV/Effects/Flight/wingflap7.ogg
+ - /Audio/_DV/Effects/Flight/wingflap8.ogg
diff --git a/Resources/Prototypes/_EE/Shaders/shaders.yml b/Resources/Prototypes/_EE/Shaders/shaders.yml
new file mode 100644
index 0000000000..8a2d7808b5
--- /dev/null
+++ b/Resources/Prototypes/_EE/Shaders/shaders.yml
@@ -0,0 +1,5 @@
+ # Flight shaders
+- type: shader
+ id: Flap
+ kind: source
+ path: "/Textures/_EE/Shaders/flap.swsl"
\ No newline at end of file
diff --git a/Resources/ServerInfo/Guidebook/Mobs/_DV/Harpy.xml b/Resources/ServerInfo/Guidebook/Mobs/_DV/Harpy.xml
index 5ad4e5c249..cd5b10d054 100644
--- a/Resources/ServerInfo/Guidebook/Mobs/_DV/Harpy.xml
+++ b/Resources/ServerInfo/Guidebook/Mobs/_DV/Harpy.xml
@@ -19,6 +19,7 @@
- Somewhat smaller than Humans, although not as small as Felinids.
- Their talons deal Piercing damage.
+ - They have the ability to fly for short amounts of time.
- Can imitate around 70% of the game's sound library through a huge list of voice emotes.
- They can "Sing" midis by imitating instruments. Harpies can select their imitated instrument by right-clicking on themselves.
- Moves 10% faster than other species.
diff --git a/Resources/Textures/_EE/Interface/Actions/flight.rsi/flight_off.png b/Resources/Textures/_EE/Interface/Actions/flight.rsi/flight_off.png
new file mode 100644
index 0000000000..852dd300e9
Binary files /dev/null and b/Resources/Textures/_EE/Interface/Actions/flight.rsi/flight_off.png differ
diff --git a/Resources/Textures/_EE/Interface/Actions/flight.rsi/flight_on.png b/Resources/Textures/_EE/Interface/Actions/flight.rsi/flight_on.png
new file mode 100644
index 0000000000..c47b923a68
Binary files /dev/null and b/Resources/Textures/_EE/Interface/Actions/flight.rsi/flight_on.png differ
diff --git a/Resources/Textures/_EE/Interface/Actions/flight.rsi/meta.json b/Resources/Textures/_EE/Interface/Actions/flight.rsi/meta.json
new file mode 100644
index 0000000000..d9644400b1
--- /dev/null
+++ b/Resources/Textures/_EE/Interface/Actions/flight.rsi/meta.json
@@ -0,0 +1,17 @@
+{
+ "copyright" : "Made by dootythefrooty (273243513800622090)",
+ "license" : "CC-BY-SA-3.0",
+ "version": 1,
+ "size": {
+ "x": 32,
+ "y": 32
+ },
+ "states": [
+ {
+ "name": "flight_off"
+ },
+ {
+ "name": "flight_on"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/Resources/Textures/_EE/Shaders/flap.swsl b/Resources/Textures/_EE/Shaders/flap.swsl
new file mode 100644
index 0000000000..3082e19b49
--- /dev/null
+++ b/Resources/Textures/_EE/Shaders/flap.swsl
@@ -0,0 +1,35 @@
+preset raw;
+
+varying highp vec4 VtxModulate;
+varying highp vec2 Pos;
+
+uniform highp float Speed;
+uniform highp float Multiplier;
+uniform highp float Offset;
+
+void fragment() {
+ highp vec4 texColor = zTexture(UV);
+ lowp vec3 lightSample = texture2D(lightMap, Pos).rgb;
+ COLOR = texColor * VtxModulate * vec4(lightSample, 1.0);
+}
+
+void vertex() {
+ vec2 pos = aPos;
+
+ // Apply MVP transformation first
+ vec2 transformedPos = apply_mvp(pos);
+
+ // Calculate vertical movement in screen space
+ float verticalOffset = (sin(TIME * Speed) + Offset) * Multiplier;
+
+ // Apply vertical movement after MVP transformation
+ transformedPos.y += verticalOffset;
+
+ // Assign the final position
+ VERTEX = transformedPos;
+
+ // Keep the original UV coordinates
+ UV = mix(modifyUV.xy, modifyUV.zw, tCoord);
+ Pos = (VERTEX + 1.0) / 2.0;
+ VtxModulate = zFromSrgb(modulate);
+}
\ No newline at end of file