diff --git a/Content.Server/Chemistry/Components/ActiveSolutionHeaterComponent.cs b/Content.Server/Chemistry/Components/ActiveSolutionHeaterComponent.cs index b94855ef39..e190c0ebd3 100644 --- a/Content.Server/Chemistry/Components/ActiveSolutionHeaterComponent.cs +++ b/Content.Server/Chemistry/Components/ActiveSolutionHeaterComponent.cs @@ -3,5 +3,4 @@ [RegisterComponent] public sealed class ActiveSolutionHeaterComponent : Component { - } diff --git a/Content.Server/Chemistry/Components/SolutionHeaterComponent.cs b/Content.Server/Chemistry/Components/SolutionHeaterComponent.cs index ddeb4ff055..2c5f5e5ab8 100644 --- a/Content.Server/Chemistry/Components/SolutionHeaterComponent.cs +++ b/Content.Server/Chemistry/Components/SolutionHeaterComponent.cs @@ -1,19 +1,50 @@ -namespace Content.Server.Chemistry.Components; +using Content.Shared.Whitelist; + +namespace Content.Server.Chemistry.Components; [RegisterComponent] public sealed class SolutionHeaterComponent : Component { - public readonly string BeakerSlotId = "beakerSlot"; - - [DataField("heatPerSecond")] - public float HeatPerSecond = 120; + /// + /// How much heat is added per second to the solution, with no upgrades. + /// + [DataField("baseHeatPerSecond")] + public float BaseHeatPerSecond = 120; + /// + /// How much heat is added per second to the solution, taking upgrades into account. + /// [ViewVariables(VVAccess.ReadWrite)] - public float HeatMultiplier = 1; + public float HeatPerSecond; - [DataField("machinePartHeatPerSecond")] - public string MachinePartHeatPerSecond = "Capacitor"; + /// + /// The machine part that affects the heat multiplier. + /// + [DataField("machinePartHeatMultiplier")] + public string MachinePartHeatMultiplier = "Capacitor"; + /// + /// How much each upgrade multiplies the heat by. + /// [DataField("partRatingHeatMultiplier")] public float PartRatingHeatMultiplier = 1.5f; + + /// + /// The entities that are placed on the heater. + /// + [DataField("placedEntities")] + public HashSet PlacedEntities = new(); + + /// + /// The max amount of entities that can be heated at the same time. + /// + [DataField("maxEntities")] + public uint MaxEntities = 1; + + /// + /// Whitelist for entities that can be placed on the heater. + /// + [DataField("whitelist")] + [ViewVariables(VVAccess.ReadWrite)] + public EntityWhitelist? Whitelist; } diff --git a/Content.Server/Chemistry/EntitySystems/SolutionHeaterSystem.cs b/Content.Server/Chemistry/EntitySystems/SolutionHeaterSystem.cs index 47f2d2d981..4e3db49e80 100644 --- a/Content.Server/Chemistry/EntitySystems/SolutionHeaterSystem.cs +++ b/Content.Server/Chemistry/EntitySystems/SolutionHeaterSystem.cs @@ -1,14 +1,22 @@ -using Content.Server.Chemistry.Components; +using System.Linq; +using Content.Server.Chemistry.Components; using Content.Server.Chemistry.Components.SolutionManager; using Content.Server.Construction; using Content.Server.Power.Components; -using Content.Shared.Containers.ItemSlots; +using Content.Server.Power.EntitySystems; +using Content.Shared.Chemistry; +using Content.Shared.Placeable; +using Robust.Shared.Physics.Events; +using Robust.Shared.Physics.Systems; namespace Content.Server.Chemistry.EntitySystems; public sealed class SolutionHeaterSystem : EntitySystem { - [Dependency] private readonly ItemSlotsSystem _itemSlots = default!; + [Dependency] private readonly SharedAppearanceSystem _appearance = default!; + [Dependency] private readonly SharedPhysicsSystem _physics = default!; + [Dependency] private readonly PlaceableSurfaceSystem _placeableSurface = default!; + [Dependency] private readonly PowerReceiverSystem _powerReceiver = default!; [Dependency] private readonly SolutionContainerSystem _solution = default!; /// @@ -17,48 +25,101 @@ public sealed class SolutionHeaterSystem : EntitySystem SubscribeLocalEvent(OnPowerChanged); SubscribeLocalEvent(OnRefreshParts); SubscribeLocalEvent(OnUpgradeExamine); + SubscribeLocalEvent(OnStartCollide); + SubscribeLocalEvent(OnEndCollide); + } + + private void TurnOn(EntityUid uid) + { + _appearance.SetData(uid, SolutionHeaterVisuals.IsOn, true); + EnsureComp(uid); + } + + public bool TryTurnOn(EntityUid uid, SolutionHeaterComponent component) + { + if (component.PlacedEntities.Count <= 0 || !_powerReceiver.IsPowered(uid)) + return false; + + TurnOn(uid); + return true; + } + + public void TurnOff(EntityUid uid) + { + _appearance.SetData(uid, SolutionHeaterVisuals.IsOn, false); + RemComp(uid); } private void OnPowerChanged(EntityUid uid, SolutionHeaterComponent component, ref PowerChangedEvent args) { - if (args.Powered) + if (args.Powered && component.PlacedEntities.Count > 0) { - EnsureComp(uid); + TurnOn(uid); } else { - RemComp(uid); + TurnOff(uid); } } private void OnRefreshParts(EntityUid uid, SolutionHeaterComponent component, RefreshPartsEvent args) { - var heatRating = args.PartRatings[component.MachinePartHeatPerSecond] - 1; + var heatRating = args.PartRatings[component.MachinePartHeatMultiplier] - 1; - component.HeatMultiplier = MathF.Pow(component.PartRatingHeatMultiplier, heatRating); + component.HeatPerSecond = component.BaseHeatPerSecond * MathF.Pow(component.PartRatingHeatMultiplier, heatRating); } private void OnUpgradeExamine(EntityUid uid, SolutionHeaterComponent component, UpgradeExamineEvent args) { - args.AddPercentageUpgrade("solution-heater-upgrade-heat", component.HeatMultiplier); + args.AddPercentageUpgrade("solution-heater-upgrade-heat", component.HeatPerSecond / component.BaseHeatPerSecond); + } + + private void OnStartCollide(EntityUid uid, SolutionHeaterComponent component, ref StartCollideEvent args) + { + if (component.Whitelist is not null && !component.Whitelist.IsValid(args.OtherEntity)) + return; + + // Disallow sleeping so we can detect when entity is removed from the heater. + _physics.SetSleepingAllowed(args.OtherEntity, args.OtherBody, false); + + component.PlacedEntities.Add(args.OtherEntity); + + TryTurnOn(uid, component); + + if (component.PlacedEntities.Count >= component.MaxEntities) + _placeableSurface.SetPlaceable(uid, false); + } + + private void OnEndCollide(EntityUid uid, SolutionHeaterComponent component, ref EndCollideEvent args) + { + // Re-allow sleeping. + _physics.SetSleepingAllowed(args.OtherEntity, args.OtherBody, true); + + component.PlacedEntities.Remove(args.OtherEntity); + + if (component.PlacedEntities.Count == 0) // Last entity was removed + TurnOff(uid); + + _placeableSurface.SetPlaceable(uid, true); } public override void Update(float frameTime) { base.Update(frameTime); - foreach (var (_, heater) in EntityQuery()) + var query = EntityQueryEnumerator(); + while (query.MoveNext(out _, out _, out var heater)) { - if (_itemSlots.GetItemOrNull(heater.Owner, heater.BeakerSlotId) is not { } item) - continue; - - if (!TryComp(item, out var solution)) - continue; - - var energy = heater.HeatPerSecond * heater.HeatMultiplier * frameTime; - foreach (var s in solution.Solutions.Values) + foreach (var heatingEntity in heater.PlacedEntities.Take((int) heater.MaxEntities)) { - _solution.AddThermalEnergy(solution.Owner, s, energy); + if (!TryComp(heatingEntity, out var solution)) + continue; + + var energy = heater.HeatPerSecond * frameTime; + foreach (var s in solution.Solutions.Values) + { + _solution.AddThermalEnergy(heatingEntity, s, energy); + } } } } diff --git a/Content.Shared/Chemistry/SharedSolutionHeater.cs b/Content.Shared/Chemistry/SharedSolutionHeater.cs new file mode 100644 index 0000000000..32032a59f0 --- /dev/null +++ b/Content.Shared/Chemistry/SharedSolutionHeater.cs @@ -0,0 +1,9 @@ +using Robust.Shared.Serialization; + +namespace Content.Shared.Chemistry; + +[Serializable, NetSerializable] +public enum SolutionHeaterVisuals +{ + IsOn, +} diff --git a/Resources/Prototypes/Entities/Structures/Machines/hotplate.yml b/Resources/Prototypes/Entities/Structures/Machines/hotplate.yml index ab7e9e65b5..0c5fa3989a 100644 --- a/Resources/Prototypes/Entities/Structures/Machines/hotplate.yml +++ b/Resources/Prototypes/Entities/Structures/Machines/hotplate.yml @@ -15,7 +15,10 @@ mask: - TabletopMachineMask layer: - - TabletopMachineLayer + - Impassable + - MidImpassable + - LowImpassable + hard: false - type: Sprite sprite: Structures/Machines/hotplate.rsi drawdepth: SmallObjects @@ -23,16 +26,10 @@ layers: - state: icon - state: on - map: ["enum.PowerDeviceVisualLayers.Powered"] + map: ["enum.SolutionHeaterVisuals.IsOn"] shader: unshaded - type: ApcPowerReceiver powerLoad: 300 - - type: ItemSlots - slots: - beakerSlot: - whitelist: - components: - - FitsInDispenser - type: ItemMapper sprite: Structures/Machines/hotplate.rsi mapLayers: @@ -41,17 +38,22 @@ components: - FitsInDispenser - type: SolutionHeater + whitelist: + components: + - FitsInDispenser + - type: PlaceableSurface + placeCentered: true + positionOffset: 0, 0.25 - type: Machine board: HotplateMachineCircuitboard - type: Appearance - type: ContainerContainer containers: - beakerSlot: !type:ContainerSlot machine_board: !type:Container machine_parts: !type:Container - type: GenericVisualizer visuals: - enum.PowerDeviceVisuals.Powered: - enum.PowerDeviceVisualLayers.Powered: + enum.SolutionHeaterVisuals.IsOn: + enum.SolutionHeaterVisuals.IsOn: True: { visible: true } False: { visible: false }