From: SlamBamActionman <83650252+SlamBamActionman@users.noreply.github.com> Date: Wed, 4 Jun 2025 10:52:59 +0000 (+0200) Subject: Add Diona rooting (#32782) X-Git-Url: https://git.smokeofanarchy.ru/gitweb.cgi?a=commitdiff_plain;h=d81e82cef7180af5337a26df1f43815853866fe2;p=space-station-14.git 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 --- 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 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