diff --git a/Content.Client/Movement/Systems/JetpackSystem.cs b/Content.Client/Movement/Systems/JetpackSystem.cs index 6810bb24cc..bf80ed4252 100644 --- a/Content.Client/Movement/Systems/JetpackSystem.cs +++ b/Content.Client/Movement/Systems/JetpackSystem.cs @@ -34,10 +34,6 @@ public sealed class JetpackSystem : SharedJetpackSystem { Appearance.TryGetData(uid, JetpackVisuals.Enabled, out var enabled, args.Component); - var state = "icon" + (enabled ? "-on" : ""); - if (args.Sprite != null) - _sprite.LayerSetRsiState((uid, args.Sprite), 0, state); - if (TryComp(uid, out var clothing)) _clothing.SetEquippedPrefix(uid, enabled ? "on" : null, clothing); } diff --git a/Content.Client/Rootable/RootableSystem.cs b/Content.Client/Rootable/RootableSystem.cs new file mode 100644 index 0000000000..33e68ae594 --- /dev/null +++ b/Content.Client/Rootable/RootableSystem.cs @@ -0,0 +1,5 @@ +using Content.Shared.Rootable; + +namespace Content.Client.Rootable; + +public sealed class RootableSystem : SharedRootableSystem; diff --git a/Content.Server/Damage/Systems/DamageUserOnTriggerSystem.cs b/Content.Server/Damage/Systems/DamageUserOnTriggerSystem.cs index 5051751be9..8a0ee51076 100644 --- a/Content.Server/Damage/Systems/DamageUserOnTriggerSystem.cs +++ b/Content.Server/Damage/Systems/DamageUserOnTriggerSystem.cs @@ -1,8 +1,6 @@ -using Content.Server.Damage.Components; using Content.Server.Explosion.EntitySystems; using Content.Shared.Damage; -using Content.Shared.StepTrigger; -using Content.Shared.StepTrigger.Systems; +using Content.Shared.Damage.Components; namespace Content.Server.Damage.Systems; diff --git a/Content.Server/Rootable/RootableSystem.cs b/Content.Server/Rootable/RootableSystem.cs new file mode 100644 index 0000000000..ce88f18dc3 --- /dev/null +++ b/Content.Server/Rootable/RootableSystem.cs @@ -0,0 +1,77 @@ +using Content.Server.Body.Components; +using Content.Server.Body.Systems; +using Content.Shared.Administration.Logs; +using Content.Shared.Chemistry; +using Content.Shared.Chemistry.Components; +using Content.Shared.Chemistry.EntitySystems; +using Content.Shared.Database; +using Content.Shared.FixedPoint; +using Content.Shared.Fluids.Components; +using Content.Shared.Rootable; +using Robust.Shared.Timing; + +namespace Content.Server.Rootable; + +/// +/// Adds an action to toggle rooting to the ground, primarily for the Diona species. +/// +public sealed class RootableSystem : SharedRootableSystem +{ + [Dependency] private readonly ISharedAdminLogManager _logger = default!; + [Dependency] private readonly IGameTiming _timing = default!; + [Dependency] private readonly SharedSolutionContainerSystem _solutionContainer = default!; + [Dependency] private readonly ReactiveSystem _reactive = default!; + [Dependency] private readonly BloodstreamSystem _blood = default!; + + public override void Update(float frameTime) + { + base.Update(frameTime); + + var query = EntityQueryEnumerator(); + var curTime = _timing.CurTime; + while (query.MoveNext(out var uid, out var rooted, out var bloodstream)) + { + if (!rooted.Rooted || rooted.PuddleEntity == null || curTime < rooted.NextUpdate || !PuddleQuery.TryComp(rooted.PuddleEntity, out var puddleComp)) + continue; + + rooted.NextUpdate += rooted.TransferFrequency; + + PuddleReact((uid, rooted, bloodstream), (rooted.PuddleEntity.Value, puddleComp!)); + } + } + + /// + /// Determines if the puddle is set up properly and if so, moves on to reacting. + /// + private void PuddleReact(Entity entity, Entity puddleEntity) + { + if (!_solutionContainer.ResolveSolution(puddleEntity.Owner, puddleEntity.Comp.SolutionName, ref puddleEntity.Comp.Solution, out var solution) || + solution.Contents.Count == 0) + { + return; + } + + ReactWithEntity(entity, puddleEntity, solution); + } + + /// + /// Attempt to transfer an amount of the solution to the entity's bloodstream. + /// + private void ReactWithEntity(Entity entity, Entity puddleEntity, Solution solution) + { + if (!_solutionContainer.ResolveSolution(entity.Owner, entity.Comp2.ChemicalSolutionName, ref entity.Comp2.ChemicalSolution, out var chemSolution) || chemSolution.AvailableVolume <= 0) + return; + + var availableTransfer = FixedPoint2.Min(solution.Volume, entity.Comp1.TransferRate); + var transferAmount = FixedPoint2.Min(availableTransfer, chemSolution.AvailableVolume); + var transferSolution = _solutionContainer.SplitSolution(puddleEntity.Comp.Solution!.Value, transferAmount); + + _reactive.DoEntityReaction(entity, transferSolution, ReactionMethod.Ingestion); + + if (_blood.TryAddToChemicals(entity, transferSolution, entity.Comp2)) + { + // Log solution addition by puddle + _logger.Add(LogType.ForceFeed, LogImpact.Medium, $"{ToPrettyString(entity):target} absorbed puddle {SharedSolutionContainerSystem.ToPrettyString(transferSolution)}"); + } + } +} diff --git a/Content.Server/Damage/Components/DamageUserOnTriggerComponent.cs b/Content.Shared/Damage/Components/DamageUserOnTriggerComponent.cs similarity index 77% rename from Content.Server/Damage/Components/DamageUserOnTriggerComponent.cs rename to Content.Shared/Damage/Components/DamageUserOnTriggerComponent.cs index 2a30374709..87adc0cc90 100644 --- a/Content.Server/Damage/Components/DamageUserOnTriggerComponent.cs +++ b/Content.Shared/Damage/Components/DamageUserOnTriggerComponent.cs @@ -1,6 +1,4 @@ -using Content.Shared.Damage; - -namespace Content.Server.Damage.Components; +namespace Content.Shared.Damage.Components; [RegisterComponent] public sealed partial class DamageUserOnTriggerComponent : Component diff --git a/Content.Shared/Movement/Systems/SharedJetpackSystem.cs b/Content.Shared/Movement/Systems/SharedJetpackSystem.cs index 6da1a9c2b3..709f0a4acd 100644 --- a/Content.Shared/Movement/Systems/SharedJetpackSystem.cs +++ b/Content.Shared/Movement/Systems/SharedJetpackSystem.cs @@ -203,4 +203,5 @@ public abstract class SharedJetpackSystem : EntitySystem public enum JetpackVisuals : byte { Enabled, + Layer } diff --git a/Content.Shared/Rootable/RootableComponent.cs b/Content.Shared/Rootable/RootableComponent.cs new file mode 100644 index 0000000000..94f8dbcea9 --- /dev/null +++ b/Content.Shared/Rootable/RootableComponent.cs @@ -0,0 +1,76 @@ +using Content.Shared.Alert; +using Content.Shared.FixedPoint; +using Robust.Shared.Audio; +using Robust.Shared.GameStates; +using Robust.Shared.Prototypes; +using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom; + +namespace Content.Shared.Rootable; + +/// +/// A rooting action, for Diona. +/// +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState, AutoGenerateComponentPause] +public sealed partial class RootableComponent : Component +{ + /// + /// The action prototype that toggles the rootable state. + /// + [DataField] + public EntProtoId Action = "ActionToggleRootable"; + + /// + /// Entity to hold the action prototype. + /// + [DataField, AutoNetworkedField] + public EntityUid? ActionEntity; + + /// + /// The prototype for the "rooted" alert, indicating the user that they are rooted. + /// + [DataField] + public ProtoId RootedAlert = "Rooted"; + + /// + /// Is the entity currently rooted? + /// + [DataField, AutoNetworkedField] + public bool Rooted; + + /// + /// The puddle that is currently affecting this entity. + /// + [DataField, AutoNetworkedField] + public EntityUid? PuddleEntity; + + /// + /// The time at which the next absorption metabolism will occur. + /// + [DataField(customTypeSerializer: typeof(TimeOffsetSerializer)), AutoNetworkedField] + [AutoPausedField] + public TimeSpan NextUpdate; + + /// + /// The max rate (in reagent units per transfer) at which chemicals are transferred from the puddle to the rooted entity. + /// + [DataField] + public FixedPoint2 TransferRate = 0.75; + + /// + /// The frequency of which chemicals are transferred from the puddle to the rooted entity. + /// + [DataField] + public TimeSpan TransferFrequency = TimeSpan.FromSeconds(1); + + /// + /// The movement speed modifier for when rooting is active. + /// + [DataField] + public float SpeedModifier = 0.8f; + + /// + /// Sound that plays when rooting is toggled. + /// + [DataField] + public SoundSpecifier RootSound = new SoundPathSpecifier("/Audio/Voice/Diona/diona_salute.ogg"); +} diff --git a/Content.Shared/Rootable/SharedRootableSystem.cs b/Content.Shared/Rootable/SharedRootableSystem.cs new file mode 100644 index 0000000000..9a6697cf97 --- /dev/null +++ b/Content.Shared/Rootable/SharedRootableSystem.cs @@ -0,0 +1,177 @@ +using Content.Shared.Damage.Components; +using Content.Shared.Actions; +using Content.Shared.Actions.Components; +using Content.Shared.Alert; +using Content.Shared.Coordinates; +using Content.Shared.Fluids.Components; +using Content.Shared.Gravity; +using Content.Shared.Mobs; +using Content.Shared.Movement.Systems; +using Content.Shared.Slippery; +using Content.Shared.Toggleable; +using Robust.Shared.Audio.Systems; +using Robust.Shared.Physics.Components; +using Robust.Shared.Physics.Events; +using Robust.Shared.Physics.Systems; +using Robust.Shared.Timing; + +namespace Content.Shared.Rootable; + +/// +/// Adds an action to toggle rooting to the ground, primarily for the Diona species. +/// Being rooted prevents weighlessness and slipping, but causes any floor contents to transfer its reagents to the bloodstream. +/// +public abstract class SharedRootableSystem : EntitySystem +{ + [Dependency] private readonly IGameTiming _timing = default!; + [Dependency] private readonly SharedActionsSystem _actions = default!; + [Dependency] private readonly SharedGravitySystem _gravity = default!; + [Dependency] private readonly SharedPhysicsSystem _physics = default!; + [Dependency] private readonly MovementSpeedModifierSystem _movementSpeedModifier = default!; + [Dependency] private readonly AlertsSystem _alerts = default!; + [Dependency] private readonly SharedAudioSystem _audio = default!; + + protected EntityQuery PuddleQuery; + protected EntityQuery PhysicsQuery; + + public override void Initialize() + { + base.Initialize(); + + PuddleQuery = GetEntityQuery(); + PhysicsQuery = GetEntityQuery(); + + SubscribeLocalEvent(OnRootableMapInit); + SubscribeLocalEvent(OnRootableShutdown); + SubscribeLocalEvent(OnStartCollide); + SubscribeLocalEvent(OnEndCollide); + SubscribeLocalEvent(OnRootableToggle); + SubscribeLocalEvent(OnMobStateChanged); + SubscribeLocalEvent(OnIsWeightless); + SubscribeLocalEvent(OnSlipAttempt); + SubscribeLocalEvent(OnRefreshMovementSpeed); + } + + private void OnRootableMapInit(Entity entity, ref MapInitEvent args) + { + if (!TryComp(entity, out ActionsComponent? comp)) + return; + + entity.Comp.NextUpdate = _timing.CurTime; + _actions.AddAction(entity, ref entity.Comp.ActionEntity, entity.Comp.Action, component: comp); + } + + private void OnRootableShutdown(Entity entity, ref ComponentShutdown args) + { + if (!TryComp(entity, out ActionsComponent? comp)) + return; + + var actions = new Entity(entity, comp); + _actions.RemoveAction(actions, entity.Comp.ActionEntity); + } + + private void OnRootableToggle(Entity entity, ref ToggleActionEvent args) + { + args.Handled = TryToggleRooting((entity, entity)); + } + + private void OnMobStateChanged(Entity entity, ref MobStateChangedEvent args) + { + if (entity.Comp.Rooted) + TryToggleRooting((entity, entity)); + } + + public bool TryToggleRooting(Entity entity) + { + if (!Resolve(entity, ref entity.Comp)) + return false; + + entity.Comp.Rooted = !entity.Comp.Rooted; + _movementSpeedModifier.RefreshMovementSpeedModifiers(entity); + Dirty(entity); + + if (entity.Comp.Rooted) + { + _alerts.ShowAlert(entity, entity.Comp.RootedAlert); + var curTime = _timing.CurTime; + if (curTime > entity.Comp.NextUpdate) + { + entity.Comp.NextUpdate = curTime; + } + } + else + { + _alerts.ClearAlert(entity, entity.Comp.RootedAlert); + } + + _audio.PlayPredicted(entity.Comp.RootSound, entity.Owner.ToCoordinates(), entity); + + return true; + } + + private void OnIsWeightless(Entity ent, ref IsWeightlessEvent args) + { + if (args.Handled || !ent.Comp.Rooted) + return; + + // do not cancel weightlessness if the person is in off-grid. + if (!_gravity.EntityOnGravitySupportingGridOrMap(ent.Owner)) + return; + + args.IsWeightless = false; + args.Handled = true; + } + + private void OnSlipAttempt(Entity ent, ref SlipAttemptEvent args) + { + if (!ent.Comp.Rooted) + return; + + if (args.SlipCausingEntity != null && HasComp(args.SlipCausingEntity)) + return; + + args.NoSlip = true; + } + + private void OnStartCollide(Entity entity, ref StartCollideEvent args) + { + if (!PuddleQuery.HasComp(args.OtherEntity)) + return; + + entity.Comp.PuddleEntity = args.OtherEntity; + + if (entity.Comp.NextUpdate < _timing.CurTime) // To prevent constantly moving to new puddles resetting the timer + entity.Comp.NextUpdate = _timing.CurTime; + } + + private void OnEndCollide(Entity entity, ref EndCollideEvent args) + { + if (entity.Comp.PuddleEntity != args.OtherEntity) + return; + + var exists = Exists(args.OtherEntity); + + if (!PhysicsQuery.TryComp(entity, out var body)) + return; + + foreach (var ent in _physics.GetContactingEntities(entity, body)) + { + if (exists && ent == args.OtherEntity) + continue; + + if (!PuddleQuery.HasComponent(ent)) + continue; + + entity.Comp.PuddleEntity = ent; + return; // New puddle found, no need to continue + } + + entity.Comp.PuddleEntity = null; + } + + private void OnRefreshMovementSpeed(Entity entity, ref RefreshMovementSpeedModifiersEvent args) + { + if (entity.Comp.Rooted) + args.ModifySpeed(entity.Comp.SpeedModifier); + } +} diff --git a/Content.Shared/Slippery/SlipperySystem.cs b/Content.Shared/Slippery/SlipperySystem.cs index 54078bec72..fdfc871ceb 100644 --- a/Content.Shared/Slippery/SlipperySystem.cs +++ b/Content.Shared/Slippery/SlipperySystem.cs @@ -119,7 +119,7 @@ public sealed class SlipperySystem : EntitySystem if (HasComp(other) && !component.SlipData.SuperSlippery) return; - var attemptEv = new SlipAttemptEvent(); + var attemptEv = new SlipAttemptEvent(uid); RaiseLocalEvent(other, attemptEv); if (attemptEv.SlowOverSlippery) _speedModifier.AddModifiedEntity(other); @@ -184,7 +184,14 @@ public sealed class SlipAttemptEvent : EntityEventArgs, IInventoryRelayEvent public bool SlowOverSlippery; + public EntityUid? SlipCausingEntity; + public SlotFlags TargetSlots { get; } = SlotFlags.FEET; + + public SlipAttemptEvent(EntityUid? slipCausingEntity) + { + SlipCausingEntity = slipCausingEntity; + } } /// diff --git a/Resources/Changelog/Changelog.yml b/Resources/Changelog/Changelog.yml index 860749eb07..372c5b602f 100644 --- a/Resources/Changelog/Changelog.yml +++ b/Resources/Changelog/Changelog.yml @@ -1,11 +1,4 @@ Entries: -- author: metalgearsloth - changes: - - message: Fix pulling alert being goofy. - type: Fix - id: 8102 - time: '2025-03-22T11:41:16.0000000+00:00' - url: https://github.com/space-wizards/space-station-14/pull/36004 - author: Boaz1111 changes: - message: Unrevivable now lets you be cloned again @@ -3927,3 +3920,11 @@ id: 8613 time: '2025-06-04T10:12:37.0000000+00:00' url: https://github.com/space-wizards/space-station-14/pull/31442 +- author: SlamBamActionman + changes: + - message: Diona can now root, keeping them from slipping but absorbing fluids from + the ground. + type: Add + id: 8614 + time: '2025-06-04T10:53:00.0000000+00:00' + url: https://github.com/space-wizards/space-station-14/pull/32782 diff --git a/Resources/Locale/en-US/_strings/actions/actions/rootable.ftl b/Resources/Locale/en-US/_strings/actions/actions/rootable.ftl new file mode 100644 index 0000000000..ac853a06af --- /dev/null +++ b/Resources/Locale/en-US/_strings/actions/actions/rootable.ftl @@ -0,0 +1,2 @@ +action-name-toggle-rootable = Rootable +action-description-toggle-rootable = Begin or stop being rooted to the floor. diff --git a/Resources/Locale/en-US/_strings/alerts/alerts.ftl b/Resources/Locale/en-US/_strings/alerts/alerts.ftl index 554b0f6ad8..4d7ab924f0 100644 --- a/Resources/Locale/en-US/_strings/alerts/alerts.ftl +++ b/Resources/Locale/en-US/_strings/alerts/alerts.ftl @@ -114,9 +114,12 @@ alerts-revenant-essence-desc = The power of souls. It sustains you and is used f alerts-revenant-corporeal-name = Corporeal alerts-revenant-corporeal-desc = You have manifested physically. People around you can see and hurt you. +alerts-rooted-name = Rooted +alerts-rooted-desc = You are attached to the ground. You can't slip, but you absorb fluids under you. + alerts-changeling-chemicals-name = Chemicals alerts-changeling-chemicals-desc = Spend chemicals to use your abilities. Slowly regenerates. alerts-changeling-biomass-name = Biomass alerts-changeling-biomass-desc = - This is your health. If it reaches 0 - it's [color=red]game over[/color]. Absorb humanoids to recover some of it. \ No newline at end of file + This is your health. If it reaches 0 - it's [color=red]game over[/color]. Absorb humanoids to recover some of it. diff --git a/Resources/Prototypes/Actions/types.yml b/Resources/Prototypes/Actions/types.yml index 70b106a16a..ecc562c487 100644 --- a/Resources/Prototypes/Actions/types.yml +++ b/Resources/Prototypes/Actions/types.yml @@ -449,6 +449,18 @@ useDelay: 1 itemIconStyle: BigAction +- type: entity + parent: BaseToggleAction + id: ActionToggleRootable + name: action-name-toggle-rootable + description: action-description-toggle-rootable + components: + - type: Action + icon: Interface/Actions/rooting.png + iconOn: Interface/Actions/rooting.png + itemIconStyle: NoItem + useDelay: 1 + - type: entity id: ActionChameleonController name: Control clothing diff --git a/Resources/Prototypes/Alerts/alerts.yml b/Resources/Prototypes/Alerts/alerts.yml index a48ac69a33..a358ebc694 100644 --- a/Resources/Prototypes/Alerts/alerts.yml +++ b/Resources/Prototypes/Alerts/alerts.yml @@ -28,6 +28,7 @@ - category: Hunger - category: Thirst - alertType: Magboots + - alertType: Rooted - alertType: Pacified - type: entity diff --git a/Resources/Prototypes/Alerts/rooted.yml b/Resources/Prototypes/Alerts/rooted.yml new file mode 100644 index 0000000000..088e4be2b6 --- /dev/null +++ b/Resources/Prototypes/Alerts/rooted.yml @@ -0,0 +1,5 @@ +- type: alert + id: Rooted + icons: [ /Textures/Interface/Alerts/Rooted/rooted.png ] + name: alerts-rooted-name + description: alerts-rooted-desc diff --git a/Resources/Prototypes/Entities/Mobs/Species/diona.yml b/Resources/Prototypes/Entities/Mobs/Species/diona.yml index ca5e4d7f48..a43f71a545 100644 --- a/Resources/Prototypes/Entities/Mobs/Species/diona.yml +++ b/Resources/Prototypes/Entities/Mobs/Species/diona.yml @@ -124,6 +124,7 @@ 32: sprite: Mobs/Species/Human/displacement.rsi state: jumpsuit-female + - type: Rootable # Sunrise-Start - type: CollectiveMind minds: diff --git a/Resources/Prototypes/Entities/Objects/Tools/jetpacks.yml b/Resources/Prototypes/Entities/Objects/Tools/jetpacks.yml index 10d79040d8..5376b62bce 100644 --- a/Resources/Prototypes/Entities/Objects/Tools/jetpacks.yml +++ b/Resources/Prototypes/Entities/Objects/Tools/jetpacks.yml @@ -25,35 +25,43 @@ name: jetpack description: It's a jetpack. It can hold 5 L of gas. components: - - type: Sprite - sprite: Objects/Tanks/Jetpacks/blue.rsi - state: icon - - type: Item - sprite: Objects/Tanks/Jetpacks/blue.rsi - size: Huge - - type: UserInterface - interfaces: - enum.SharedGasTankUiKey.Key: - type: GasTankBoundUserInterface - - type: UseDelay - delays: - gasTank: - length: 1.0 - - type: Clothing - sprite: Objects/Tanks/Jetpacks/blue.rsi - quickEquip: false - slots: - - Back - - type: GasTank - outputPressure: 21.3 - air: - volume: 5 - temperature: 293.15 - - type: Jetpack - moleUsage: 0.00085 - - type: Appearance - - type: StaticPrice - price: 100 + - type: Sprite + sprite: Objects/Tanks/Jetpacks/blue.rsi + layers: + - state: icon + map: ["enum.JetpackVisuals.Layer"] + - type: Item + sprite: Objects/Tanks/Jetpacks/blue.rsi + size: Huge + - type: UserInterface + interfaces: + enum.SharedGasTankUiKey.Key: + type: GasTankBoundUserInterface + - type: UseDelay + delays: + gasTank: + length: 1.0 + - type: Clothing + sprite: Objects/Tanks/Jetpacks/blue.rsi + quickEquip: false + slots: + - Back + - type: GasTank + outputPressure: 21.3 + air: + volume: 5 + temperature: 293.15 + - type: Jetpack + moleUsage: 0.00085 + - type: Appearance + - type: GenericVisualizer + visuals: + enum.JetpackVisuals.Enabled: + enum.JetpackVisuals.Layer: + True: {state: icon-on} + False: {state: icon} + - type: StaticPrice + price: 100 - type: entity parent: BaseAction diff --git a/Resources/Prototypes/GameRules/variation.yml b/Resources/Prototypes/GameRules/variation.yml index de7a0d7fab..718e5111a7 100644 --- a/Resources/Prototypes/GameRules/variation.yml +++ b/Resources/Prototypes/GameRules/variation.yml @@ -106,7 +106,6 @@ - Carbon - Hydrogen - Fat - - SpaceLube - SpaceGlue - Sulfur - Acetone diff --git a/Resources/Textures/Interface/Actions/rooting.png b/Resources/Textures/Interface/Actions/rooting.png new file mode 100644 index 0000000000..4fc05fb3f3 Binary files /dev/null and b/Resources/Textures/Interface/Actions/rooting.png differ diff --git a/Resources/Textures/Interface/Alerts/Rooted/rooted.png b/Resources/Textures/Interface/Alerts/Rooted/rooted.png new file mode 100644 index 0000000000..a68a1009c0 Binary files /dev/null and b/Resources/Textures/Interface/Alerts/Rooted/rooted.png differ