]> git.smokeofanarchy.ru Git - space-station-14.git/commitdiff
Add Diona rooting (#32782)
authorSlamBamActionman <83650252+SlamBamActionman@users.noreply.github.com>
Wed, 4 Jun 2025 10:52:59 +0000 (12:52 +0200)
committerGitHub <noreply@github.com>
Wed, 4 Jun 2025 10:52:59 +0000 (12:52 +0200)
* 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 <scarky0@onet.eu>
15 files changed:
Content.Client/Rootable/RootableSystem.cs [new file with mode: 0644]
Content.Server/Damage/Systems/DamageUserOnTriggerSystem.cs
Content.Server/Rootable/RootableSystem.cs [new file with mode: 0644]
Content.Shared/Damage/Components/DamageUserOnTriggerComponent.cs [moved from Content.Server/Damage/Components/DamageUserOnTriggerComponent.cs with 77% similarity]
Content.Shared/Rootable/RootableComponent.cs [new file with mode: 0644]
Content.Shared/Rootable/SharedRootableSystem.cs [new file with mode: 0644]
Content.Shared/Slippery/SlipperySystem.cs
Resources/Locale/en-US/actions/actions/rootable.ftl [new file with mode: 0644]
Resources/Locale/en-US/alerts/alerts.ftl
Resources/Prototypes/Actions/types.yml
Resources/Prototypes/Alerts/alerts.yml
Resources/Prototypes/Alerts/rooted.yml [new file with mode: 0644]
Resources/Prototypes/Entities/Mobs/Species/diona.yml
Resources/Textures/Interface/Actions/rooting.png [new file with mode: 0644]
Resources/Textures/Interface/Alerts/Rooted/rooted.png [new file with mode: 0644]

diff --git a/Content.Client/Rootable/RootableSystem.cs b/Content.Client/Rootable/RootableSystem.cs
new file mode 100644 (file)
index 0000000..33e68ae
--- /dev/null
@@ -0,0 +1,5 @@
+using Content.Shared.Rootable;
+
+namespace Content.Client.Rootable;
+
+public sealed class RootableSystem : SharedRootableSystem;
index 5051751be9d0fe3b6aa2ef91f58816eb0ba3cef3..8a0ee510769611e191c04a43c803674ea1c29e51 100644 (file)
@@ -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 (file)
index 0000000..ce88f18
--- /dev/null
@@ -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;
+
+/// <summary>
+/// Adds an action to toggle rooting to the ground, primarily for the Diona species.
+/// </summary>
+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<RootableComponent, BloodstreamComponent>();
+        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!));
+        }
+    }
+
+    /// <summary>
+    /// Determines if the puddle is set up properly and if so, moves on to reacting.
+    /// </summary>
+    private void PuddleReact(Entity<RootableComponent, BloodstreamComponent> entity, Entity<PuddleComponent> 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);
+    }
+
+    /// <summary>
+    /// Attempt to transfer an amount of the solution to the entity's bloodstream.
+    /// </summary>
+    private void ReactWithEntity(Entity<RootableComponent, BloodstreamComponent> entity, Entity<PuddleComponent> 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)}");
+        }
+    }
+}
similarity index 77%
rename from Content.Server/Damage/Components/DamageUserOnTriggerComponent.cs
rename to Content.Shared/Damage/Components/DamageUserOnTriggerComponent.cs
index 2a30374709b6a97722aa89ad1313291a822ca972..87adc0cc905e8952f9c764e48edb62088677aaf5 100644 (file)
@@ -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 (file)
index 0000000..94f8dbc
--- /dev/null
@@ -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;
+
+/// <summary>
+/// A rooting action, for Diona.
+/// </summary>
+[RegisterComponent, NetworkedComponent, AutoGenerateComponentState, AutoGenerateComponentPause]
+public sealed partial class RootableComponent : Component
+{
+    /// <summary>
+    /// The action prototype that toggles the rootable state.
+    /// </summary>
+    [DataField]
+    public EntProtoId Action = "ActionToggleRootable";
+
+    /// <summary>
+    /// Entity to hold the action prototype.
+    /// </summary>
+    [DataField, AutoNetworkedField]
+    public EntityUid? ActionEntity;
+
+    /// <summary>
+    /// The prototype for the "rooted" alert, indicating the user that they are rooted.
+    /// </summary>
+    [DataField]
+    public ProtoId<AlertPrototype> RootedAlert = "Rooted";
+
+    /// <summary>
+    /// Is the entity currently rooted?
+    /// </summary>
+    [DataField, AutoNetworkedField]
+    public bool Rooted;
+
+    /// <summary>
+    /// The puddle that is currently affecting this entity.
+    /// </summary>
+    [DataField, AutoNetworkedField]
+    public EntityUid? PuddleEntity;
+
+    /// <summary>
+    /// The time at which the next absorption metabolism will occur.
+    /// </summary>
+    [DataField(customTypeSerializer: typeof(TimeOffsetSerializer)), AutoNetworkedField]
+    [AutoPausedField]
+    public TimeSpan NextUpdate;
+
+    /// <summary>
+    /// The max rate (in reagent units per transfer) at which chemicals are transferred from the puddle to the rooted entity.
+    /// </summary>
+    [DataField]
+    public FixedPoint2 TransferRate = 0.75;
+
+    /// <summary>
+    /// The frequency of which chemicals are transferred from the puddle to the rooted entity.
+    /// </summary>
+    [DataField]
+    public TimeSpan TransferFrequency = TimeSpan.FromSeconds(1);
+
+    /// <summary>
+    /// The movement speed modifier for when rooting is active.
+    /// </summary>
+    [DataField]
+    public float SpeedModifier = 0.8f;
+
+    /// <summary>
+    /// Sound that plays when rooting is toggled.
+    /// </summary>
+    [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 (file)
index 0000000..9a6697c
--- /dev/null
@@ -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;
+
+/// <summary>
+/// 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.
+/// </summary>
+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<PuddleComponent> PuddleQuery;
+    protected EntityQuery<PhysicsComponent> PhysicsQuery;
+
+    public override void Initialize()
+    {
+        base.Initialize();
+
+        PuddleQuery = GetEntityQuery<PuddleComponent>();
+        PhysicsQuery = GetEntityQuery<PhysicsComponent>();
+
+        SubscribeLocalEvent<RootableComponent, MapInitEvent>(OnRootableMapInit);
+        SubscribeLocalEvent<RootableComponent, ComponentShutdown>(OnRootableShutdown);
+        SubscribeLocalEvent<RootableComponent, StartCollideEvent>(OnStartCollide);
+        SubscribeLocalEvent<RootableComponent, EndCollideEvent>(OnEndCollide);
+        SubscribeLocalEvent<RootableComponent, ToggleActionEvent>(OnRootableToggle);
+        SubscribeLocalEvent<RootableComponent, MobStateChangedEvent>(OnMobStateChanged);
+        SubscribeLocalEvent<RootableComponent, IsWeightlessEvent>(OnIsWeightless);
+        SubscribeLocalEvent<RootableComponent, SlipAttemptEvent>(OnSlipAttempt);
+        SubscribeLocalEvent<RootableComponent, RefreshMovementSpeedModifiersEvent>(OnRefreshMovementSpeed);
+    }
+
+    private void OnRootableMapInit(Entity<RootableComponent> 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<RootableComponent> entity, ref ComponentShutdown args)
+    {
+        if (!TryComp(entity, out ActionsComponent? comp))
+            return;
+
+        var actions = new Entity<ActionsComponent?>(entity, comp);
+        _actions.RemoveAction(actions, entity.Comp.ActionEntity);
+    }
+
+    private void OnRootableToggle(Entity<RootableComponent> entity, ref ToggleActionEvent args)
+    {
+        args.Handled = TryToggleRooting((entity, entity));
+    }
+
+    private void OnMobStateChanged(Entity<RootableComponent> entity, ref MobStateChangedEvent args)
+    {
+        if (entity.Comp.Rooted)
+            TryToggleRooting((entity, entity));
+    }
+
+    public bool TryToggleRooting(Entity<RootableComponent?> 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<RootableComponent> 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<RootableComponent> ent, ref SlipAttemptEvent args)
+    {
+        if (!ent.Comp.Rooted)
+            return;
+
+        if (args.SlipCausingEntity != null && HasComp<DamageUserOnTriggerComponent>(args.SlipCausingEntity))
+            return;
+
+        args.NoSlip = true;
+    }
+
+    private void OnStartCollide(Entity<RootableComponent> 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<RootableComponent> 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<RootableComponent> entity, ref RefreshMovementSpeedModifiersEvent args)
+    {
+        if (entity.Comp.Rooted)
+            args.ModifySpeed(entity.Comp.SpeedModifier);
+    }
+}
index bedf05536b8bee5b130fe778870ef683f493ca18..40d12d9ebeeb01d1af6192a7f4d33e4a5a9a1813 100644 (file)
@@ -95,7 +95,7 @@ public sealed class SlipperySystem : EntitySystem
         if (HasComp<KnockedDownComponent>(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;
+    }
 }
 
 /// <summary>
diff --git a/Resources/Locale/en-US/actions/actions/rootable.ftl b/Resources/Locale/en-US/actions/actions/rootable.ftl
new file mode 100644 (file)
index 0000000..ac853a0
--- /dev/null
@@ -0,0 +1,2 @@
+action-name-toggle-rootable = Rootable
+action-description-toggle-rootable = Begin or stop being rooted to the floor.
index 800e8950a50e62779bf97987659228577641eb87..eb6d17902761d0e90ab39356a51dc78ce74f5809 100644 (file)
@@ -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.
index f276c295d99720d601c6f475ee783cf2fb95434d..279608293bf7d20ed2b11e4ec9cec99ce74429f6 100644 (file)
     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
index 471ece63ee6e0373aae5b9aeb9e23b7f8d96e006..60a23294d3ad2ac491288a50e8ec06f0e4226b8e 100644 (file)
@@ -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 (file)
index 0000000..088e4be
--- /dev/null
@@ -0,0 +1,5 @@
+- type: alert
+  id: Rooted
+  icons: [ /Textures/Interface/Alerts/Rooted/rooted.png ]
+  name: alerts-rooted-name
+  description: alerts-rooted-desc
index 1e39eaec76f8828ca1f8035a6e69ddc887e85fdd..c2939347a82c10382e850d0496eff66a11cd3c59 100644 (file)
           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 (file)
index 0000000..4fc05fb
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 (file)
index 0000000..a68a100
Binary files /dev/null and b/Resources/Textures/Interface/Alerts/Rooted/rooted.png differ