diff --git a/Content.Server/_NF/Fluids/EntitySystems/AdvDrainSystem.cs b/Content.Server/_NF/Fluids/EntitySystems/AdvDrainSystem.cs new file mode 100644 index 00000000000..757a740aa14 --- /dev/null +++ b/Content.Server/_NF/Fluids/EntitySystems/AdvDrainSystem.cs @@ -0,0 +1,258 @@ +using Content.Server.DoAfter; +using Content.Server.Popups; +using Content.Server.Power.EntitySystems; +using Content.Server.PowerCell; +using Content.Shared.Chemistry.EntitySystems; +using Content.Shared.Audio; +using Content.Shared.Chemistry.Components.SolutionManager; +using Content.Shared.Database; +using Content.Shared.DoAfter; +using Content.Shared.Examine; +using Content.Shared.FixedPoint; +using Content.Shared.Fluids; +using Content.Shared.Fluids.Components; +using Content.Shared._NF.Fluids.Components; +using Content.Server.Fluids.EntitySystems; +using Content.Shared.Interaction; +using Content.Shared.Tag; +using Content.Shared.Verbs; +using Robust.Shared.Audio.Systems; +using Robust.Shared.Prototypes; +using Robust.Shared.Random; +using Robust.Shared.Utility; + + +namespace Content.Server._NF.Fluids.EntitySystems; + +public sealed class AdvDrainSystem : SharedDrainSystem +{ + [Dependency] private readonly EntityLookupSystem _lookup = default!; + [Dependency] private readonly SharedSolutionContainerSystem _solutionContainerSystem = default!; + [Dependency] private readonly SharedAmbientSoundSystem _ambientSoundSystem = default!; + [Dependency] private readonly SharedAudioSystem _audioSystem = default!; + [Dependency] private readonly SharedAppearanceSystem _appearanceSystem = default!; + [Dependency] private readonly PopupSystem _popupSystem = default!; + [Dependency] private readonly TagSystem _tagSystem = default!; + [Dependency] private readonly DoAfterSystem _doAfterSystem = default!; + [Dependency] private readonly PuddleSystem _puddleSystem = default!; + [Dependency] private readonly IRobustRandom _random = default!; + [Dependency] private readonly IPrototypeManager _prototypeManager = default!; + [Dependency] private readonly PowerCellSystem _powerCell = default!; + [Dependency] private readonly BatterySystem _battery = default!; + + private readonly HashSet> _puddles = new(); + + public override void Initialize() + { + base.Initialize(); + SubscribeLocalEvent(OnDrainMapInit); + SubscribeLocalEvent>(AddEmptyVerb); + SubscribeLocalEvent(OnExamined); + //SubscribeLocalEvent(OnInteract); + //SubscribeLocalEvent(OnDoAfter); + } + + private void OnDrainMapInit(Entity ent, ref MapInitEvent args) + { + // Randomise puddle drains so roundstart ones don't all dump at the same time. + ent.Comp.Accumulator = _random.NextFloat(ent.Comp.DrainFrequency); + } + + private void AddEmptyVerb(Entity entity, ref GetVerbsEvent args) + { + if (!args.CanAccess || !args.CanInteract || args.Using == null) + return; + + if (!TryComp(args.Using, out SpillableComponent? spillable) || + !TryComp(args.Target, out AdvDrainComponent? drain)) + return; + + var used = args.Using.Value; + var target = args.Target; + Verb verb = new() + { + Text = Loc.GetString("drain-component-empty-verb-inhand", ("object", Name(used))), + Act = () => + { + Empty(used, spillable, target, drain); + }, + Impact = LogImpact.Low, + Icon = new SpriteSpecifier.Texture(new ResPath("/Textures/Interface/VerbIcons/eject.svg.192dpi.png")) + + }; + args.Verbs.Add(verb); + } + + private void Empty(EntityUid container, SpillableComponent spillable, EntityUid target, AdvDrainComponent drain) + { + // Find the solution in the container that is emptied + if (!_solutionContainerSystem.TryGetDrainableSolution(container, out var containerSoln, out var containerSolution) || containerSolution.Volume == FixedPoint2.Zero) + { + _popupSystem.PopupEntity( + Loc.GetString("drain-component-empty-verb-using-is-empty-message", ("object", container)), + container); + return; + } + + // try to find the drain's solution + if (!_solutionContainerSystem.ResolveSolution(target, AdvDrainComponent.SolutionName, ref drain.Solution, out var drainSolution)) + { + return; + } + + // Try to transfer as much solution as possible to the drain + + var amountToPutInDrain = drainSolution.AvailableVolume; + var amountToSpillOnGround = containerSolution.Volume - drainSolution.AvailableVolume; + + if (amountToPutInDrain > 0) + { + var solutionToPutInDrain = _solutionContainerSystem.SplitSolution(containerSoln.Value, amountToPutInDrain); + _solutionContainerSystem.TryAddSolution(drain.Solution.Value, solutionToPutInDrain); + + _audioSystem.PlayPvs(drain.ManualDrainSound, target); + _ambientSoundSystem.SetAmbience(target, true); + } + + + // Don't actually spill the remainder. + + if (amountToSpillOnGround > 0) + { + // var solutionToSpill = _solutionContainerSystem.SplitSolution(containerSoln.Value, amountToSpillOnGround); + // _puddleSystem.TrySpillAt(Transform(target).Coordinates, solutionToSpill, out _); + _popupSystem.PopupEntity( + Loc.GetString("drain-component-empty-verb-target-is-full-message", ("object", target)), + container); + } + } + + public override void Update(float frameTime) + { + base.Update(frameTime); + var managerQuery = GetEntityQuery(); + + var query = EntityQueryEnumerator(); + while (query.MoveNext(out var uid, out var drain)) + { + // not anchored + if (!TryComp(uid, out TransformComponent? xform) || !xform.Anchored) + { + _ambientSoundSystem.SetAmbience(uid, false); + _appearanceSystem.SetData(uid, AdvDrainVisualState.IsRunning, false); + _appearanceSystem.SetData(uid, AdvDrainVisualState.IsDraining, false); + continue; + } + + // not powered + if (!_powerCell.HasCharge(uid, drain.Wattage)) + { + _ambientSoundSystem.SetAmbience(uid, false); + _appearanceSystem.SetData(uid, AdvDrainVisualState.IsRunning, false); + _appearanceSystem.SetData(uid, AdvDrainVisualState.IsDraining, false); + continue; + } + + drain.Accumulator += frameTime; + if (drain.Accumulator < drain.DrainFrequency) + { + continue; + } + drain.Accumulator -= drain.DrainFrequency; + _appearanceSystem.SetData(uid, AdvDrainVisualState.IsRunning, true); + + // Disable ambient sound from emptying manually + if (!drain.AutoDrain) + { + _ambientSoundSystem.SetAmbience(uid, false); + continue; + } + + if (!managerQuery.TryGetComponent(uid, out var manager)) + continue; + + // Best to do this one every second rather than once every tick... + if (!_solutionContainerSystem.ResolveSolution((uid, manager), AdvDrainComponent.SolutionName, ref drain.Solution, out var drainSolution)) + continue; + + if (drainSolution.AvailableVolume <= 0) + { + _ambientSoundSystem.SetAmbience(uid, false); + continue; + } + + // Remove a bit from the buffer + if (drainSolution.Volume > drain.UnitsDestroyedThreshold) + { + _appearanceSystem.SetData(uid, AdvDrainVisualState.IsVoiding, true); + _appearanceSystem.SetData(uid, AdvDrainVisualState.IsRunning, false); //they use the same indicator light, and cause artifacts when on at the same time + _solutionContainerSystem.SplitSolution(drain.Solution.Value, Math.Min(drain.UnitsDestroyedPerSecond * drain.DrainFrequency, (float)drainSolution.Volume - drain.UnitsDestroyedThreshold)); + } + else + { + _appearanceSystem.SetData(uid, AdvDrainVisualState.IsVoiding, false); + } + + // This will ensure that UnitsPerSecond is per second... + var amount = drain.UnitsPerSecond * drain.DrainFrequency; + + _puddles.Clear(); + _lookup.GetEntitiesInRange(Transform(uid).Coordinates, drain.Range, _puddles); + + if (_puddles.Count == 0) + { + _ambientSoundSystem.SetAmbience(uid, false); + _appearanceSystem.SetData(uid, AdvDrainVisualState.IsDraining, false); + continue; + } + + _ambientSoundSystem.SetAmbience(uid, true); + + // only use power if it's actively draining puddles + _powerCell.TryUseCharge(uid, drain.Wattage * drain.DrainFrequency); + _appearanceSystem.SetData(uid, AdvDrainVisualState.IsDraining, true); + amount /= _puddles.Count; + + foreach (var puddle in _puddles) + { + // Queue the solution deletion if it's empty. EvaporationSystem might also do this + // but queuedelete should be pretty safe. + if (!_solutionContainerSystem.ResolveSolution(puddle.Owner, puddle.Comp.SolutionName, ref puddle.Comp.Solution, out var puddleSolution)) + { + EntityManager.QueueDeleteEntity(puddle); + continue; + } + + // Removes the lowest of: + // the drain component's units per second adjusted for # of puddles + // the puddle's remaining volume (making it cleanly zero) + // the drain's remaining volume in its buffer. + var transferSolution = _solutionContainerSystem.SplitSolution(puddle.Comp.Solution.Value, + FixedPoint2.Min(FixedPoint2.New(amount), puddleSolution.Volume, drainSolution.AvailableVolume)); + + drainSolution.AddSolution(transferSolution, _prototypeManager); + + if (puddleSolution.Volume <= 0) + { + QueueDel(puddle); + } + } + + _solutionContainerSystem.UpdateChemicals(drain.Solution.Value); + } + } + + private void OnExamined(Entity entity, ref ExaminedEvent args) + { + if (!args.IsInDetailsRange || + !HasComp(entity) || + !TryComp(entity, out var drain) || + !_solutionContainerSystem.ResolveSolution(entity.Owner, AdvDrainComponent.SolutionName, ref entity.Comp.Solution, out var drainSolution)) + { + return; + } + + var text = Loc.GetString("adv-drain-component-examine-volume", ("volume", drainSolution.Volume), ("maxvolume", drain.UnitsDestroyedThreshold)); + args.PushMarkup(text); + } +} diff --git a/Content.Shared/Fluids/SharedDrainSystem.cs b/Content.Shared/Fluids/SharedDrainSystem.cs index d65dddb0df9..1c4215d6459 100644 --- a/Content.Shared/Fluids/SharedDrainSystem.cs +++ b/Content.Shared/Fluids/SharedDrainSystem.cs @@ -10,3 +10,13 @@ public sealed partial class DrainDoAfterEvent : SimpleDoAfterEvent { } } + +// Start Frontier: portable pump visual state +[Serializable, NetSerializable] +public enum AdvDrainVisualState : byte +{ + IsRunning, + IsDraining, + IsVoiding +} +// End Frontier diff --git a/Content.Shared/_NF/Fluids/Components/AdvDrainComponent.cs b/Content.Shared/_NF/Fluids/Components/AdvDrainComponent.cs new file mode 100644 index 00000000000..5839fff50e5 --- /dev/null +++ b/Content.Shared/_NF/Fluids/Components/AdvDrainComponent.cs @@ -0,0 +1,79 @@ +using Content.Shared.Chemistry.Components; +using Content.Shared.Tag; +using Robust.Shared.Audio; +using Content.Shared.Fluids; + +namespace Content.Shared._NF.Fluids.Components; + +/// +/// A Drain allows an entity to absorb liquid in a disposal goal. Drains can be filled manually (with the Empty verb) +/// or they can absorb puddles of liquid around them when AutoDrain is set to true. +/// When the entity also has a SolutionContainerManager attached with a solution named drainBuffer, this solution +/// gets filled until the drain is full. +/// When the drain is full, it can be unclogged using a plunger (i.e. an entity with a Plunger tag attached). +/// Later this can be refactored into a proper Plunger component if needed. +/// +[RegisterComponent, Access(typeof(SharedDrainSystem))] +public sealed partial class AdvDrainComponent : Component +{ + public const string SolutionName = "drainBuffer"; + + [ValidatePrototypeId] + public const string PlungerTag = "Plunger"; + + [DataField] + public Entity? Solution = null; + + [DataField] + public float Accumulator = 0f; + + /// + /// Does this drain automatically absorb surrouding puddles? Or is it a drain designed to empty + /// solutions in it manually? + /// + [DataField] + public bool AutoDrain = true; + + /// + /// How many units per second the drain can absorb from the surrounding puddles. + /// Divided by puddles, so if there are 5 puddles this will take 1/5 from each puddle. + /// This will stay fixed to 1 second no matter what DrainFrequency is. + /// + [DataField] + public float UnitsPerSecond = 20f; + + /// + /// How many units are ejected from the buffer per second. + /// + [DataField] + public float UnitsDestroyedPerSecond = 15f; + + /// + /// Threshold of volume to begin destroying from the buffer. The effective capacity of the drain. + /// + [DataField] + public float UnitsDestroyedThreshold = 600f; + + /// + /// How many (unobstructed) tiles away the drain will + /// drain puddles from. + /// + [DataField] + public float Range = 2.5f; + + /// + /// How often in seconds the drain checks for puddles around it. + /// If the EntityQuery seems a bit unperformant this can be increased. + /// + [DataField] + public float DrainFrequency = 1f; + + /// + /// How many watts does the device need? + /// + [DataField] + public float Wattage = 15f; + + [DataField] + public SoundSpecifier ManualDrainSound = new SoundPathSpecifier("/Audio/Effects/Fluids/slosh.ogg"); +} diff --git a/Resources/Locale/en-US/_NF/fluids/adv-drain-component.ftl b/Resources/Locale/en-US/_NF/fluids/adv-drain-component.ftl new file mode 100644 index 00000000000..82671fe83b7 --- /dev/null +++ b/Resources/Locale/en-US/_NF/fluids/adv-drain-component.ftl @@ -0,0 +1 @@ +adv-drain-component-examine-volume = [color="blue"]Contains - {$volume}u/{$maxvolume}u.[/color] diff --git a/Resources/Prototypes/_NF/Entities/Structures/Machines/portable_pump.yml b/Resources/Prototypes/_NF/Entities/Structures/Machines/portable_pump.yml new file mode 100644 index 00000000000..b9113e8ae9e --- /dev/null +++ b/Resources/Prototypes/_NF/Entities/Structures/Machines/portable_pump.yml @@ -0,0 +1,117 @@ +- type: entity + id: PortablePump + parent: [BaseMachine, StructureWheeled] + name: portable pump + description: Drains puddles around it. Has a sticker on the side that says "Do not submerge in water" + components: + - type: Transform + anchored: false + - type: Physics + bodyType: Dynamic + - type: Fixtures + fixtures: + fix1: + shape: + !type:PhysShapeCircle + radius: 0.4 + density: 100 + mask: + - MachineMask + layer: + - MachineLayer + - type: Sprite + sprite: _NF\Structures\Machines\portable_pump.rsi + noRot: true + layers: + - state: base + - state: pumping + map: ["enum.AdvDrainVisualState.IsDraining"] + - state: powered + shader: unshaded + map: ["enum.AdvDrainVisualState.IsRunning"] + - state: voiding + shader: unshaded + map: ["enum.AdvDrainVisualState.IsVoiding"] + visible: false + - type: Appearance + - type: GenericVisualizer + visuals: + enum.PowerCellSlotVisuals.Enabled: + enum.PowerDeviceVisualLayers.Powered: + True: {visible: true} + False: {visible: false} + enum.AdvDrainVisualState.IsRunning: + enum.AdvDrainVisualState.IsRunning: + True: {visible: true} + False: {visible: false} + enum.AdvDrainVisualState.IsDraining: + enum.AdvDrainVisualState.IsDraining: + True: {visible: true} + False: {visible: false} + enum.AdvDrainVisualState.IsVoiding: + enum.AdvDrainVisualState.IsVoiding: + True: {visible: true} + False: {visible: false} + - type: PortableScrubberVisuals + idleState: icon + runningState: icon-running + readyState: unlit + fullState: unlit-full + - type: AmbientSound + enabled: false + volume: -5 + range: 5 + sound: + path: /Audio/Ambience/Objects/drain.ogg + - type: Machine + - type: Damageable + damageContainer: StructuralInorganic + damageModifierSet: Metallic + - type: Destructible + thresholds: + - trigger: + !type:DamageTrigger + damage: 600 + behaviors: + - !type:DoActsBehavior + acts: [ "Destruction" ] + - trigger: + !type:DamageTrigger + damage: 300 + behaviors: + - !type:PlaySoundBehavior + sound: + collection: MetalBreak + - !type:SpawnEntitiesBehavior + spawn: + SheetSteel1: + min: 1 + max: 3 + SheetGlass1: + min: 1 + max: 2 + - !type:DoActsBehavior + acts: [ "Destruction" ] + - type: Item + size: Ginormous + - type: MultiHandedItem + - type: SolutionContainerManager + solutions: + drainBuffer: + maxVol: 1200 + - type: DrainableSolution + solution: drainBuffer + - type: AdvDrain + unitsDestroyedThreshold: 600 + - type: DumpableSolution + solution: drainBuffer + - type: PowerCellSlot + cellSlotId: cell_slot + - type: ContainerContainer + containers: + cell_slot: !type:ContainerSlot + - type: ItemSlots + slots: + cell_slot: + name: power-cell-slot-component-slot-name-default + startingItem: PowerCellMedium diff --git a/Resources/Textures/_NF/Structures/Machines/portable_pump.rsi/base.png b/Resources/Textures/_NF/Structures/Machines/portable_pump.rsi/base.png new file mode 100644 index 00000000000..eb58f83e8bd Binary files /dev/null and b/Resources/Textures/_NF/Structures/Machines/portable_pump.rsi/base.png differ diff --git a/Resources/Textures/_NF/Structures/Machines/portable_pump.rsi/meta.json b/Resources/Textures/_NF/Structures/Machines/portable_pump.rsi/meta.json new file mode 100644 index 00000000000..8218bf970db --- /dev/null +++ b/Resources/Textures/_NF/Structures/Machines/portable_pump.rsi/meta.json @@ -0,0 +1,33 @@ +{ + "version": 1, + "license": "CC-BY-4.0", + "copyright": "https://github.com/Temoffy Discord: @telos2387, https://www.artstation.com/designerhere Discord: @designerhere", + "size": { + "x": 32, + "y": 32 + }, + "states": [ + { + "name": "base" + }, + { + "name": "powered" + }, + { + "name": "voiding" + }, + { + "name": "pumping", + "delays": [ + [ + 0.2, + 0.2, + 0.2, + 0.2, + 0.2, + 0.2 + ] + ] + } + ] +} diff --git a/Resources/Textures/_NF/Structures/Machines/portable_pump.rsi/powered.png b/Resources/Textures/_NF/Structures/Machines/portable_pump.rsi/powered.png new file mode 100644 index 00000000000..918d3fa4243 Binary files /dev/null and b/Resources/Textures/_NF/Structures/Machines/portable_pump.rsi/powered.png differ diff --git a/Resources/Textures/_NF/Structures/Machines/portable_pump.rsi/pumping.png b/Resources/Textures/_NF/Structures/Machines/portable_pump.rsi/pumping.png new file mode 100644 index 00000000000..663254643e4 Binary files /dev/null and b/Resources/Textures/_NF/Structures/Machines/portable_pump.rsi/pumping.png differ diff --git a/Resources/Textures/_NF/Structures/Machines/portable_pump.rsi/voiding.png b/Resources/Textures/_NF/Structures/Machines/portable_pump.rsi/voiding.png new file mode 100644 index 00000000000..8ca1b487635 Binary files /dev/null and b/Resources/Textures/_NF/Structures/Machines/portable_pump.rsi/voiding.png differ