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