From b4f08811eb6ba05fe2162143df5564920b12203a Mon Sep 17 00:00:00 2001 From: Exp Date: Sat, 8 Aug 2020 14:33:36 +0200 Subject: [PATCH] Adds Spray Bottles (#1522) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Spray Bottle * -"Proper" wall detection -Removed some using -Fixed sound * Just don't add it * -Removed spill sound -Added sound parameter to SpillHelper.SpillAt * Now spawns Vapor instead of Puddles instantly * -Review -Nullable * Reworkkkk * AABB shittery Co-authored-by: Víctor Aguilera Puerto <6766154+Zumorica@users.noreply.github.com> --- Content.Client/IgnoredComponents.cs | 2 + .../Components/Chemistry/VaporComponent.cs | 129 ++++++++++++++++++ .../Components/Fluids/SpillHelper.cs | 12 +- .../Components/Fluids/SprayComponent.cs | 101 ++++++++++++++ .../GameObjects/EntitySystems/VaporSystem.cs | 18 +++ Content.Shared/Physics/VaporController.cs | 16 +++ Resources/Audio/Effects/spray.ogg | Bin 0 -> 19037 bytes .../Entities/Objects/Specific/janitor.yml | 21 +++ Resources/Prototypes/Entities/chemistry.yml | 18 +++ .../Objects/Specific/Janitorial/cleaner.png | Bin 0 -> 411 bytes 10 files changed, 312 insertions(+), 5 deletions(-) create mode 100644 Content.Server/GameObjects/Components/Chemistry/VaporComponent.cs create mode 100644 Content.Server/GameObjects/Components/Fluids/SprayComponent.cs create mode 100644 Content.Server/GameObjects/EntitySystems/VaporSystem.cs create mode 100644 Content.Shared/Physics/VaporController.cs create mode 100644 Resources/Audio/Effects/spray.ogg create mode 100644 Resources/Textures/Objects/Specific/Janitorial/cleaner.png diff --git a/Content.Client/IgnoredComponents.cs b/Content.Client/IgnoredComponents.cs index a765115910..10784bf2e2 100644 --- a/Content.Client/IgnoredComponents.cs +++ b/Content.Client/IgnoredComponents.cs @@ -151,6 +151,8 @@ "Flippable", "Airtight", "MovedByPressure", + "Spray", + "Vapor", "DamageOnHighSpeedImpact", }; } diff --git a/Content.Server/GameObjects/Components/Chemistry/VaporComponent.cs b/Content.Server/GameObjects/Components/Chemistry/VaporComponent.cs new file mode 100644 index 0000000000..dd5fea942b --- /dev/null +++ b/Content.Server/GameObjects/Components/Chemistry/VaporComponent.cs @@ -0,0 +1,129 @@ +using Content.Server.GameObjects.Components.Fluids; +using Content.Shared.Chemistry; +using Content.Shared.Physics; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.EntityFrameworkCore.Update.Internal; +using Robust.Server.GameObjects; +using Robust.Shared.GameObjects; +using Robust.Shared.GameObjects.Components; +using Robust.Shared.Interfaces.GameObjects; +using Robust.Shared.Interfaces.Map; +using Robust.Shared.IoC; +using Robust.Shared.Map; +using Robust.Shared.Maths; +using Robust.Shared.Physics; +using Robust.Shared.Serialization; +using Robust.Shared.Timers; +using Robust.Shared.ViewVariables; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Text; +using System.Threading; +using Timer = Robust.Shared.Timers.Timer; + +namespace Content.Server.GameObjects.Components.Chemistry +{ + [RegisterComponent] + class VaporComponent : Component, ICollideBehavior + { +#pragma warning disable 649 + [Dependency] private readonly IMapManager _mapManager = default!; +#pragma warning enable 649 + public override string Name => "Vapor"; + + [ViewVariables] + private SolutionComponent _contents; + [ViewVariables] + private ReagentUnit _transferAmount; + + private bool _running; + private Vector2 _direction; + private float _velocity; + + + public override void Initialize() + { + base.Initialize(); + _contents = Owner.GetComponent(); + } + + public void Start(Vector2 dir, float velocity) + { + _running = true; + _direction = dir; + _velocity = velocity; + // Set Move + if (Owner.TryGetComponent(out ICollidableComponent collidable)) + { + var controller = collidable.EnsureController(); + controller.Move(_direction, _velocity); + } + } + + public override void ExposeData(ObjectSerializer serializer) + { + base.ExposeData(serializer); + serializer.DataField(ref _transferAmount, "transferAmount", ReagentUnit.New(0.5)); + } + + public void Update() + { + if (!_running) + return; + + // Get all intersecting tiles with the vapor and spray the divided solution on there + if (Owner.TryGetComponent(out ICollidableComponent collidable)) + { + var worldBounds = collidable.WorldAABB; + var mapGrid = _mapManager.GetGrid(Owner.Transform.GridID); + + var tiles = mapGrid.GetTilesIntersecting(worldBounds); + var amount = _transferAmount / ReagentUnit.New(tiles.Count()); + foreach (var tile in tiles) + { + var pos = tile.GridIndices.ToGridCoordinates(_mapManager, tile.GridIndex); + SpillHelper.SpillAt(pos, _contents.SplitSolution(amount), "PuddleSmear", false); //make non PuddleSmear? + } + } + + if (_contents.CurrentVolume == 0) + { + // Delete this + Owner.Delete(); + } + } + + internal bool TryAddSolution(Solution solution) + { + if (solution.TotalVolume == 0) + { + return false; + } + var result = _contents.TryAddSolution(solution); + if (!result) + { + return false; + } + + return true; + } + + void ICollideBehavior.CollideWith(IEntity collidedWith) + { + // Check for collision with a impassable object (e.g. wall) and stop + if (collidedWith.TryGetComponent(out ICollidableComponent collidable)) + { + if ((collidable.CollisionLayer & (int) CollisionGroup.Impassable) != 0 && collidable.Hard) + { + if (Owner.TryGetComponent(out ICollidableComponent coll)) + { + var controller = coll.EnsureController(); + controller.Stop(); + } + } + } + } + } +} diff --git a/Content.Server/GameObjects/Components/Fluids/SpillHelper.cs b/Content.Server/GameObjects/Components/Fluids/SpillHelper.cs index d7898c0ef9..493da11ffa 100644 --- a/Content.Server/GameObjects/Components/Fluids/SpillHelper.cs +++ b/Content.Server/GameObjects/Components/Fluids/SpillHelper.cs @@ -18,10 +18,11 @@ namespace Content.Server.GameObjects.Components.Fluids /// Entity location to spill at /// Initial solution for the prototype /// Prototype to use - internal static void SpillAt(IEntity entity, Solution solution, string prototype) + /// Play the spill sound + internal static void SpillAt(IEntity entity, Solution solution, string prototype, bool sound = true) { var entityLocation = entity.Transform.GridPosition; - SpillAt(entityLocation, solution, prototype); + SpillAt(entityLocation, solution, prototype, sound); } // Other functions will be calling this one @@ -32,7 +33,8 @@ namespace Content.Server.GameObjects.Components.Fluids /// /// Initial solution for the prototype /// Prototype to use - internal static PuddleComponent? SpillAt(GridCoordinates gridCoordinates, Solution solution, string prototype) + /// Play the spill sound + internal static PuddleComponent? SpillAt(GridCoordinates gridCoordinates, Solution solution, string prototype, bool sound = true) { if (solution.TotalVolume == 0) { @@ -67,7 +69,7 @@ namespace Content.Server.GameObjects.Components.Fluids continue; } - if (!puddleComponent.TryAddSolution(solution)) + if (!puddleComponent.TryAddSolution(solution, sound)) { continue; } @@ -84,7 +86,7 @@ namespace Content.Server.GameObjects.Components.Fluids var puddle = serverEntityManager.SpawnEntity(prototype, spillGridCoords); var newPuddleComponent = puddle.GetComponent(); - newPuddleComponent.TryAddSolution(solution); + newPuddleComponent.TryAddSolution(solution, sound); return newPuddleComponent; } diff --git a/Content.Server/GameObjects/Components/Fluids/SprayComponent.cs b/Content.Server/GameObjects/Components/Fluids/SprayComponent.cs new file mode 100644 index 0000000000..11c450f391 --- /dev/null +++ b/Content.Server/GameObjects/Components/Fluids/SprayComponent.cs @@ -0,0 +1,101 @@ +using System; +using System.Collections.Generic; +using Content.Server.GameObjects.Components.Chemistry; +using Content.Server.Interfaces; +using Content.Shared.Chemistry; +using Content.Shared.Interfaces.GameObjects.Components; +using Microsoft.EntityFrameworkCore.Update.Internal; +using Robust.Server.GameObjects.EntitySystems; +using Robust.Server.Interfaces.GameObjects; +using Robust.Shared.GameObjects; +using Robust.Shared.GameObjects.Components; +using Robust.Shared.GameObjects.Systems; +using Robust.Shared.Interfaces.GameObjects; +using Robust.Shared.Interfaces.Map; +using Robust.Shared.IoC; +using Robust.Shared.Localization; +using Robust.Shared.Map; +using Robust.Shared.Maths; +using Robust.Shared.Serialization; +using Robust.Shared.ViewVariables; + +namespace Content.Server.GameObjects.Components.Fluids +{ + [RegisterComponent] + class SprayComponent : Component, IAfterInteract + { +#pragma warning disable 649 + [Dependency] private readonly IServerNotifyManager _notifyManager = default!; + [Dependency] private readonly IServerEntityManager _serverEntityManager = default!; +#pragma warning restore 649 + public override string Name => "Spray"; + + private ReagentUnit _transferAmount; + private string _spraySound; + private float _sprayVelocity; + + /// + /// The amount of solution to be sprayer from this solution when using it + /// + [ViewVariables] + public ReagentUnit TransferAmount + { + get => _transferAmount; + set => _transferAmount = value; + } + + /// + /// The speed at which the vapor starts when sprayed + /// + [ViewVariables] + public float Velocity + { + get => _sprayVelocity; + set => _sprayVelocity = value; + } + + private SolutionComponent _contents; + public ReagentUnit CurrentVolume => _contents.CurrentVolume; + + public override void Initialize() + { + base.Initialize(); + _contents = Owner.GetComponent(); + } + + public override void ExposeData(ObjectSerializer serializer) + { + base.ExposeData(serializer); + serializer.DataField(ref _transferAmount, "transferAmount", ReagentUnit.New(10)); + serializer.DataField(ref _sprayVelocity, "sprayVelocity", 5.0f); + serializer.DataField(ref _spraySound, "spraySound", string.Empty); + } + + void IAfterInteract.AfterInteract(AfterInteractEventArgs eventArgs) + { + if (CurrentVolume <= 0) + { + _notifyManager.PopupMessage(Owner, eventArgs.User, Loc.GetString("It's empty!")); + return; + } + + var playerPos = eventArgs.User.Transform.GridPosition; + if (eventArgs.ClickLocation.GridID != playerPos.GridID) + return; + + var direction = (eventArgs.ClickLocation.Position - playerPos.Position).Normalized; + var solution = _contents.SplitSolution(_transferAmount); + + playerPos = playerPos.Offset(direction); // Move a bit so we don't hit the player + //TODO: check for wall? + var vapor = _serverEntityManager.SpawnEntity("Vapor", playerPos); + // Add the solution to the vapor and actually send the thing + var vaporComponent = vapor.GetComponent(); + vaporComponent.TryAddSolution(solution); + vaporComponent.Start(direction, _sprayVelocity); //TODO: maybe make the velocity depending on the distance to the click + + //Play sound + EntitySystem.Get().PlayFromEntity(_spraySound, Owner); + } + } +} diff --git a/Content.Server/GameObjects/EntitySystems/VaporSystem.cs b/Content.Server/GameObjects/EntitySystems/VaporSystem.cs new file mode 100644 index 0000000000..65823a8bb0 --- /dev/null +++ b/Content.Server/GameObjects/EntitySystems/VaporSystem.cs @@ -0,0 +1,18 @@ +using Content.Server.GameObjects.Components.Chemistry; +using Robust.Shared.GameObjects; +using Robust.Shared.GameObjects.Systems; + +namespace Content.Server.GameObjects.EntitySystems +{ + public class VaporSystem : EntitySystem + { + /// + public override void Update(float frameTime) + { + foreach (var vaporComp in ComponentManager.EntityQuery()) + { + vaporComp.Update(); + } + } + } +} diff --git a/Content.Shared/Physics/VaporController.cs b/Content.Shared/Physics/VaporController.cs new file mode 100644 index 0000000000..190dd62b54 --- /dev/null +++ b/Content.Shared/Physics/VaporController.cs @@ -0,0 +1,16 @@ +using Robust.Shared.Maths; +using Robust.Shared.Physics; +using System; +using System.Collections.Generic; +using System.Text; + +namespace Content.Shared.Physics +{ + public class VaporController : VirtualController + { + public void Move(Vector2 velocityDirection, float speed) + { + LinearVelocity = velocityDirection * speed; + } + } +} diff --git a/Resources/Audio/Effects/spray.ogg b/Resources/Audio/Effects/spray.ogg new file mode 100644 index 0000000000000000000000000000000000000000..d7796264c68d231129155b623fe41d299769f107 GIT binary patch literal 19037 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 literal 0 HcmV?d00001 diff --git a/Resources/Prototypes/Entities/Objects/Specific/janitor.yml b/Resources/Prototypes/Entities/Objects/Specific/janitor.yml index a7d5b15d51..7386b835b5 100644 --- a/Resources/Prototypes/Entities/Objects/Specific/janitor.yml +++ b/Resources/Prototypes/Entities/Objects/Specific/janitor.yml @@ -174,3 +174,24 @@ state: soapomega - type: Slippery paralyzeTime: 7 + +- type: entity + name: spray bottle + id: SprayBottle + parent: BaseItem + description: A spray bottle with an unscrewable top. + components: + - type: Sprite + texture: Objects/Specific/Janitorial/cleaner.png + - type: Icon + texture: Objects/Specific/Janitorial/cleaner.png + - type: Solution + maxVol: 100 + caps: 1 + - type: Pourable + transferAmount: 5.0 + - type: CanSpill + - type: Spray + transferAmount: 10 + sprayVelocity: 5 + spraySound: /Audio/Effects/spray.ogg diff --git a/Resources/Prototypes/Entities/chemistry.yml b/Resources/Prototypes/Entities/chemistry.yml index f10a3525e6..4dc90f70a0 100644 --- a/Resources/Prototypes/Entities/chemistry.yml +++ b/Resources/Prototypes/Entities/chemistry.yml @@ -6,3 +6,21 @@ components: - type: Solution maxVol: 5 + +- type: entity + id: Vapor + name: "vapor" + abstract: true + components: + - type: SnapGrid + offset: Center + - type: Solution + maxVol: 50 + - type: Vapor + - type: Physics + - type: Collidable + shapes: + - !type:PhysShapeAabb + bounds: "-0.25,-0.25,0.25,0.25" + mask: + - Impassable \ No newline at end of file diff --git a/Resources/Textures/Objects/Specific/Janitorial/cleaner.png b/Resources/Textures/Objects/Specific/Janitorial/cleaner.png new file mode 100644 index 0000000000000000000000000000000000000000..f25c12a24e5f108c5823143509ea7f563118acaa GIT binary patch literal 411 zcmV;M0c8G(P)dqa8y1DcJHzg;M95O?z9Do6k@1udf2Wb|UlVkYLD$1Z1!2%40c_fDd*&%S} zEfV|GafbN;&J1?3@g$o=l_fa(VjI-)0%;b24^1H;pM40dFg zLs1BTEMJj1lY!yd8M4hF+X42)4h$=z4lpp>0CH{t@oOL^qj)0A0Yx^}jAiG}|2Nwi z3g-X%_6