From d81e82cef7180af5337a26df1f43815853866fe2 Mon Sep 17 00:00:00 2001 From: SlamBamActionman <83650252+SlamBamActionman@users.noreply.github.com> Date: Wed, 4 Jun 2025 12:52:59 +0200 Subject: [PATCH] Add Diona rooting (#32782) * Initial commit * Add sound * Review commets * addressing review * I think this is what Slart meant? * Review fixes * More fixes * tiny formatting * Review fixes * Review fixes * Fix small timing error * Follow new action system * review --------- Co-authored-by: ScarKy0 <106310278+ScarKy0@users.noreply.github.com> Co-authored-by: ScarKy0 --- Content.Client/Rootable/RootableSystem.cs | 5 + .../Systems/DamageUserOnTriggerSystem.cs | 4 +- Content.Server/Rootable/RootableSystem.cs | 77 ++++++++ .../DamageUserOnTriggerComponent.cs | 4 +- Content.Shared/Rootable/RootableComponent.cs | 76 ++++++++ .../Rootable/SharedRootableSystem.cs | 177 ++++++++++++++++++ Content.Shared/Slippery/SlipperySystem.cs | 9 +- .../Locale/en-US/actions/actions/rootable.ftl | 2 + Resources/Locale/en-US/alerts/alerts.ftl | 3 + Resources/Prototypes/Actions/types.yml | 12 ++ Resources/Prototypes/Alerts/alerts.yml | 1 + Resources/Prototypes/Alerts/rooted.yml | 5 + .../Entities/Mobs/Species/diona.yml | 1 + .../Textures/Interface/Actions/rooting.png | Bin 0 -> 15369 bytes .../Interface/Alerts/Rooted/rooted.png | Bin 0 -> 15637 bytes 15 files changed, 369 insertions(+), 7 deletions(-) create mode 100644 Content.Client/Rootable/RootableSystem.cs create mode 100644 Content.Server/Rootable/RootableSystem.cs rename {Content.Server => Content.Shared}/Damage/Components/DamageUserOnTriggerComponent.cs (77%) create mode 100644 Content.Shared/Rootable/RootableComponent.cs create mode 100644 Content.Shared/Rootable/SharedRootableSystem.cs create mode 100644 Resources/Locale/en-US/actions/actions/rootable.ftl create mode 100644 Resources/Prototypes/Alerts/rooted.yml create mode 100644 Resources/Textures/Interface/Actions/rooting.png create mode 100644 Resources/Textures/Interface/Alerts/Rooted/rooted.png 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/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 bedf05536b..40d12d9ebe 100644 --- a/Content.Shared/Slippery/SlipperySystem.cs +++ b/Content.Shared/Slippery/SlipperySystem.cs @@ -95,7 +95,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); @@ -148,7 +148,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/Locale/en-US/actions/actions/rootable.ftl b/Resources/Locale/en-US/actions/actions/rootable.ftl new file mode 100644 index 0000000000..ac853a06af --- /dev/null +++ b/Resources/Locale/en-US/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/alerts/alerts.ftl b/Resources/Locale/en-US/alerts/alerts.ftl index 800e8950a5..eb6d179027 100644 --- a/Resources/Locale/en-US/alerts/alerts.ftl +++ b/Resources/Locale/en-US/alerts/alerts.ftl @@ -113,3 +113,6 @@ 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. diff --git a/Resources/Prototypes/Actions/types.yml b/Resources/Prototypes/Actions/types.yml index f276c295d9..279608293b 100644 --- a/Resources/Prototypes/Actions/types.yml +++ b/Resources/Prototypes/Actions/types.yml @@ -399,6 +399,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 471ece63ee..60a23294d3 100644 --- a/Resources/Prototypes/Alerts/alerts.yml +++ b/Resources/Prototypes/Alerts/alerts.yml @@ -24,6 +24,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 1e39eaec76..c2939347a8 100644 --- a/Resources/Prototypes/Entities/Mobs/Species/diona.yml +++ b/Resources/Prototypes/Entities/Mobs/Species/diona.yml @@ -112,6 +112,7 @@ 32: sprite: Mobs/Species/Human/displacement.rsi state: jumpsuit-female + - type: Rootable - type: entity parent: BaseSpeciesDummy diff --git a/Resources/Textures/Interface/Actions/rooting.png b/Resources/Textures/Interface/Actions/rooting.png new file mode 100644 index 0000000000000000000000000000000000000000..4fc05fb3f32eb4bdfa53e4c475324c41b6f5c21c GIT binary patch literal 15369 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 literal 0 HcmV?d00001