diff --git a/Content.Client/IgnoredComponents.cs b/Content.Client/IgnoredComponents.cs index 5db13ba33a..1dfd6423f4 100644 --- a/Content.Client/IgnoredComponents.cs +++ b/Content.Client/IgnoredComponents.cs @@ -233,6 +233,7 @@ namespace Content.Client "FoamSolutionAreaEffect", "GasFilter", "Recyclable", + "LightReplacer", "SecretStash", "Toilet", "ClusterFlash", diff --git a/Content.Server/GameObjects/Components/Janitorial/LightReplacerComponent.cs b/Content.Server/GameObjects/Components/Janitorial/LightReplacerComponent.cs new file mode 100644 index 0000000000..7ec4df5dd6 --- /dev/null +++ b/Content.Server/GameObjects/Components/Janitorial/LightReplacerComponent.cs @@ -0,0 +1,167 @@ +#nullable enable +using Content.Server.GameObjects.Components.Items.Storage; +using Content.Server.GameObjects.Components.Power.ApcNetComponents.PowerReceiverUsers; +using Content.Shared.GameObjects; +using Content.Shared.GameObjects.EntitySystems.ActionBlocker; +using Content.Shared.Interfaces; +using Content.Shared.Interfaces.GameObjects.Components; +using Robust.Server.GameObjects; +using Robust.Shared.Audio; +using Robust.Shared.Containers; +using Robust.Shared.GameObjects; +using Robust.Shared.Localization; +using Robust.Shared.Player; +using Robust.Shared.Prototypes; +using Robust.Shared.Serialization.Manager.Attributes; +using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; +using Robust.Shared.ViewVariables; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace Content.Server.GameObjects.Components.Janitorial +{ + /// + /// Device that allows user to quikly change bulbs in + /// Can be reloaded by new light tubes or light bulbs + /// + [RegisterComponent] + public class LightReplacerComponent : Component + { + public override string Name => "LightReplacer"; + public override uint? NetID => ContentNetIDs.LIGHT_REPLACER; + + [DataField("sound")] private string _sound = "/Audio/Weapons/click.ogg"; + + // bulbs that were inside light replacer when it spawned + [DataField("contents")] private List _contents = new(); + // bulbs that were inserted inside light replacer + [ViewVariables] private IContainer _insertedBulbs = default!; + + public override void Initialize() + { + base.Initialize(); + _insertedBulbs = ContainerHelpers.EnsureContainer(Owner, "light_replacer_storage"); + } + + public bool TryReplaceBulb(PoweredLightComponent fixture, IEntity? user = null) + { + // check if light bulb is broken or missing + if (fixture.LightBulb != null && fixture.LightBulb.State == LightBulbState.Normal) return false; + + // try get first inserted bulb of the same type as targeted light fixtutre + var bulb = _insertedBulbs.ContainedEntities.FirstOrDefault( + (e) => e.GetComponentOrNull()?.Type == fixture.BulbType); + + // found bulb in inserted storage + if (bulb != null) + { + // try to remove it + var hasRemoved = _insertedBulbs.Remove(bulb); + if (!hasRemoved) + return false; + } + // try to create new instance of bulb from LightReplacerEntity + else + { + var bulbEnt = _contents.FirstOrDefault((e) => e.Type == fixture.BulbType && e.Amount > 0); + + // found right bulb, let's spawn it + if (bulbEnt != null) + { + bulb = Owner.EntityManager.SpawnEntity(bulbEnt.PrototypeName, Owner.Transform.Coordinates); + bulbEnt.Amount--; + } + // not found any light bulbs + else + { + if (user != null) + { + var msg = Loc.GetString("comp-light-replacer-missing-light", ("light-replacer", Owner)); + user.PopupMessage(msg); + } + return false; + } + } + + // insert it into fixture + var wasReplaced = fixture.ReplaceBulb(bulb); + if (wasReplaced) + { + EntitySystem.Get().Play(Filter.Broadcast(), _sound, + Owner, AudioParams.Default.WithVolume(-4f)); + } + + + return wasReplaced; + } + + public bool TryInsertBulb(LightBulbComponent bulb, IEntity? user = null, bool showTooltip = false) + { + // only normal lights can be inserted inside light replacer + if (bulb.State != LightBulbState.Normal) + { + if (showTooltip && user != null) + { + var msg = Loc.GetString("comp-light-replacer-insert-broken-light"); + user.PopupMessage(msg); + } + + return false; + } + + // try insert light and show message + var hasInsert = _insertedBulbs.Insert(bulb.Owner); + if (hasInsert && showTooltip && user != null) + { + var msg = Loc.GetString("comp-light-replacer-insert-light", + ("light-replacer", Owner), ("bulb", bulb.Owner)); + user.PopupMessage(msg); + } + + + return hasInsert; + } + + public bool TryInsertBulb(ServerStorageComponent storage, IEntity? user = null) + { + if (storage.StoredEntities == null) + return false; + + var insertedBulbs = 0; + var storagedEnts = storage.StoredEntities.ToArray(); + foreach (var ent in storagedEnts) + { + if (ent.TryGetComponent(out LightBulbComponent? bulb)) + { + if (TryInsertBulb(bulb)) + insertedBulbs++; + } + } + + // show some message if success + if (insertedBulbs > 0 && user != null) + { + var msg = Loc.GetString("comp-light-replacer-refill-from-storage", ("light-replacer", Owner)); + user.PopupMessage(msg); + } + + return insertedBulbs > 0; + } + + [Serializable] + [DataDefinition] + public class LightReplacerEntity + { + [DataField("name", customTypeSerializer: typeof(PrototypeIdSerializer))] + public string? PrototypeName; + + [DataField("amount")] + public int Amount; + + [DataField("type")] + public LightBulbType Type; + } + } +} diff --git a/Content.Server/GameObjects/Components/Power/ApcNetComponents/PowerReceiverUsers/PoweredLightComponent.cs b/Content.Server/GameObjects/Components/Power/ApcNetComponents/PowerReceiverUsers/PoweredLightComponent.cs index 6b75a9333e..ad78398cf9 100644 --- a/Content.Server/GameObjects/Components/Power/ApcNetComponents/PowerReceiverUsers/PoweredLightComponent.cs +++ b/Content.Server/GameObjects/Components/Power/ApcNetComponents/PowerReceiverUsers/PoweredLightComponent.cs @@ -62,12 +62,13 @@ namespace Content.Server.GameObjects.Components.Power.ApcNetComponents.PowerRece [ViewVariables] [DataField("ignoreGhostsBoo")] private bool _ignoreGhostsBoo; - [DataField("bulb")] - private LightBulbType BulbType = LightBulbType.Tube; + [DataField("bulb")] private LightBulbType _bulbType = LightBulbType.Tube; + public LightBulbType BulbType => _bulbType; + [ViewVariables] private ContainerSlot _lightBulbContainer = default!; [ViewVariables] - private LightBulbComponent? LightBulb + public LightBulbComponent? LightBulb { get { @@ -126,6 +127,15 @@ namespace Content.Server.GameObjects.Components.Power.ApcNetComponents.PowerRece } } + /// + /// Try to replace current bulb with a new one + /// + public bool ReplaceBulb(IEntity bulb) + { + EjectBulb(); + return InsertBulb(bulb); + } + /// /// Inserts the bulb if possible. /// @@ -134,7 +144,7 @@ namespace Content.Server.GameObjects.Components.Power.ApcNetComponents.PowerRece { if (LightBulb != null) return false; if (!bulb.TryGetComponent(out LightBulbComponent? lightBulb)) return false; - if (lightBulb.Type != BulbType) return false; + if (lightBulb.Type != _bulbType) return false; var inserted = _lightBulbContainer.Insert(bulb); @@ -149,7 +159,7 @@ namespace Content.Server.GameObjects.Components.Power.ApcNetComponents.PowerRece /// /// Ejects the bulb to a mob's hand if possible. /// - private void EjectBulb(IEntity user) + private void EjectBulb(IEntity? user = null) { if (LightBulb == null) return; @@ -160,9 +170,17 @@ namespace Content.Server.GameObjects.Components.Power.ApcNetComponents.PowerRece if (!_lightBulbContainer.Remove(bulb.Owner)) return; - if (!user.TryGetComponent(out HandsComponent? hands) - || !hands.PutInHand(bulb.Owner.GetComponent())) - bulb.Owner.Transform.Coordinates = user.Transform.Coordinates; + if (user != null) + { + if (!user.TryGetComponent(out HandsComponent? hands) + || !hands.PutInHand(bulb.Owner.GetComponent())) + bulb.Owner.Transform.Coordinates = user.Transform.Coordinates; + } + else + { + bulb.Owner.Transform.Coordinates = Owner.Transform.Coordinates; + } + } /// @@ -259,7 +277,7 @@ namespace Content.Server.GameObjects.Components.Power.ApcNetComponents.PowerRece { if (_hasLampOnSpawn) { - var prototype = BulbType switch + var prototype = _bulbType switch { LightBulbType.Bulb => "LightBulb", LightBulbType.Tube => "LightTube", diff --git a/Content.Server/GameObjects/EntitySystems/Janitorial/LightReplacerSystem.cs b/Content.Server/GameObjects/EntitySystems/Janitorial/LightReplacerSystem.cs new file mode 100644 index 0000000000..4bc32cb12d --- /dev/null +++ b/Content.Server/GameObjects/EntitySystems/Janitorial/LightReplacerSystem.cs @@ -0,0 +1,66 @@ +#nullable enable +using System; +using Content.Server.GameObjects.Components.Items.Storage; +using Content.Server.GameObjects.Components.Janitorial; +using Content.Server.GameObjects.Components.Power.ApcNetComponents.PowerReceiverUsers; +using Content.Shared.GameObjects.EntitySystems.ActionBlocker; +using Content.Shared.Interfaces.GameObjects.Components; +using JetBrains.Annotations; +using Robust.Shared.GameObjects; + +namespace Content.Server.GameObjects.EntitySystems.Janitorial +{ + [UsedImplicitly] + public class LightReplacerSystem : EntitySystem + { + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(HandleInteract); + SubscribeLocalEvent(HandleAfterInteract); + } + + public override void Shutdown() + { + base.Shutdown(); + + UnsubscribeLocalEvent(HandleInteract); + UnsubscribeLocalEvent(HandleAfterInteract); + } + + private void HandleAfterInteract(EntityUid uid, LightReplacerComponent component, AfterInteractMessage eventArgs) + { + // standard interaction checks + if (!ActionBlockerSystem.CanUse(eventArgs.User)) return; + if (!eventArgs.CanReach) return; + + // behaviour will depends on target type + if (eventArgs.Attacked != null) + { + // replace broken light in fixture? + if (eventArgs.Attacked.TryGetComponent(out PoweredLightComponent? fixture)) + component.TryReplaceBulb(fixture, eventArgs.User); + // add new bulb to light replacer container? + else if (eventArgs.Attacked.TryGetComponent(out LightBulbComponent? bulb)) + component.TryInsertBulb(bulb, eventArgs.User, true); + } + } + + private void HandleInteract(EntityUid uid, LightReplacerComponent component, InteractUsingMessage eventArgs) + { + // standard interaction checks + if (!ActionBlockerSystem.CanInteract(eventArgs.User)) return; + + if (eventArgs.ItemInHand != null) + { + // want to insert a new light bulb? + if (eventArgs.ItemInHand.TryGetComponent(out LightBulbComponent? bulb)) + component.TryInsertBulb(bulb, eventArgs.User, true); + // add bulbs from storage? + else if (eventArgs.ItemInHand.TryGetComponent(out ServerStorageComponent? storage)) + component.TryInsertBulb(storage, eventArgs.User); + } + } + } +} diff --git a/Content.Shared/GameObjects/ContentNetIDs.cs b/Content.Shared/GameObjects/ContentNetIDs.cs index d7f12ed5f2..6b4168f17a 100644 --- a/Content.Shared/GameObjects/ContentNetIDs.cs +++ b/Content.Shared/GameObjects/ContentNetIDs.cs @@ -97,6 +97,7 @@ namespace Content.Shared.GameObjects public const uint DOOR = 1087; public const uint SPAWN_AFTER_INTERACT = 1088; public const uint DISASSEMBLE_ON_ACTIVATE = 1089; + public const uint LIGHT_REPLACER = 1090; // Net IDs for integration tests. public const uint PREDICTION_TEST = 10001; diff --git a/Resources/Locale/en-US/components/light-replacer-component.ftl b/Resources/Locale/en-US/components/light-replacer-component.ftl new file mode 100644 index 0000000000..f183ac8f7e --- /dev/null +++ b/Resources/Locale/en-US/components/light-replacer-component.ftl @@ -0,0 +1,14 @@ + +### Interaction Messages + +# Shown when player tries to replace light, but there is no lighs left +comp-light-replacer-missing-light = No lights left in {$light-replacer}. + +# Shown when player inserts light bulb inside light replacer +comp-light-replacer-insert-light = You insert {$bulb} into {$light-replacer}. + +# Shown when player tries to insert in light replacer brolen light bulb +comp-light-replacer-insert-broken-light = You can't insert broken lights! + +# Shown when player refill light from light box +comp-light-replacer-refill-from-storage = You refill {$light-replacer}. \ No newline at end of file diff --git a/Resources/Prototypes/Catalog/Fills/Lockers/service.yml b/Resources/Prototypes/Catalog/Fills/Lockers/service.yml index 00790d63d3..c65688972b 100644 --- a/Resources/Prototypes/Catalog/Fills/Lockers/service.yml +++ b/Resources/Prototypes/Catalog/Fills/Lockers/service.yml @@ -47,6 +47,10 @@ amount: 3 - name: TrashBag amount: 2 + - name: LightReplacer + amount: 1 + - name: BoxLightMixed + amount: 1 #- type: entity # id: LockerLegalFilled diff --git a/Resources/Prototypes/Entities/Objects/Specific/janitor.yml b/Resources/Prototypes/Entities/Objects/Specific/janitor.yml index 5afe3a58fc..3870deaadf 100644 --- a/Resources/Prototypes/Entities/Objects/Specific/janitor.yml +++ b/Resources/Prototypes/Entities/Objects/Specific/janitor.yml @@ -266,3 +266,24 @@ quickInsert: true areaInsert: true storageSoundCollection: trashBagRustle + +- type: entity + parent: BaseItem + name: light replacer + id: LightReplacer + description: An item which uses magnets to easily replace broken lights. + components: + - type: Sprite + sprite: Objects/Specific/Janitorial/light_replacer.rsi + state: icon + - type: Item + sprite: Objects/Specific/Janitorial/light_replacer.rsi + - type: LightReplacer + contents: + - name: LightTube + amount: 8 + type: Tube + - name: LightBulb + amount: 5 + type: Bulb + diff --git a/Resources/Prototypes/Recipes/Lathes/janitorial.yml b/Resources/Prototypes/Recipes/Lathes/janitorial.yml index dd15452c58..ec40593cc0 100644 --- a/Resources/Prototypes/Recipes/Lathes/janitorial.yml +++ b/Resources/Prototypes/Recipes/Lathes/janitorial.yml @@ -46,3 +46,12 @@ completetime: 300 materials: plastic: 100 + +- type: latheRecipe + id: LightReplacer + icon: Objects/Specific/Janitorial/light_replacer.rsi + result: LightReplacer + completetime: 600 + materials: + steel: 100 + glass: 1000 diff --git a/Resources/Textures/Objects/Specific/Janitorial/light_replacer.rsi/emagged.png b/Resources/Textures/Objects/Specific/Janitorial/light_replacer.rsi/emagged.png new file mode 100644 index 0000000000..5b0d4b79b7 Binary files /dev/null and b/Resources/Textures/Objects/Specific/Janitorial/light_replacer.rsi/emagged.png differ diff --git a/Resources/Textures/Objects/Specific/Janitorial/light_replacer.rsi/icon.png b/Resources/Textures/Objects/Specific/Janitorial/light_replacer.rsi/icon.png new file mode 100644 index 0000000000..6524ae80e5 Binary files /dev/null and b/Resources/Textures/Objects/Specific/Janitorial/light_replacer.rsi/icon.png differ diff --git a/Resources/Textures/Objects/Specific/Janitorial/light_replacer.rsi/inhand-left.png b/Resources/Textures/Objects/Specific/Janitorial/light_replacer.rsi/inhand-left.png new file mode 100644 index 0000000000..301ecdaefb Binary files /dev/null and b/Resources/Textures/Objects/Specific/Janitorial/light_replacer.rsi/inhand-left.png differ diff --git a/Resources/Textures/Objects/Specific/Janitorial/light_replacer.rsi/inhand-right.png b/Resources/Textures/Objects/Specific/Janitorial/light_replacer.rsi/inhand-right.png new file mode 100644 index 0000000000..d5e242d336 Binary files /dev/null and b/Resources/Textures/Objects/Specific/Janitorial/light_replacer.rsi/inhand-right.png differ diff --git a/Resources/Textures/Objects/Specific/Janitorial/light_replacer.rsi/meta.json b/Resources/Textures/Objects/Specific/Janitorial/light_replacer.rsi/meta.json new file mode 100644 index 0000000000..ea81c49910 --- /dev/null +++ b/Resources/Textures/Objects/Specific/Janitorial/light_replacer.rsi/meta.json @@ -0,0 +1,25 @@ +{ + "version": 1, + "size": { + "x": 32, + "y": 32 + }, + "license": "CC-BY-SA-3.0", + "copyright": "Taken from https://github.com/discordia-space/CEV-Eris/commit/d76180f48949870dd57c7274d494175b3b3515ba", + "states": [ + { + "name": "icon" + }, + { + "name": "emagged" + }, + { + "name": "inhand-right", + "directions": 4 + }, + { + "name": "inhand-left", + "directions": 4 + } + ] +} \ No newline at end of file