diff --git a/Content.Client/Power/APC/ApcVisualizer.cs b/Content.Client/Power/APC/ApcVisualizer.cs index f731ff567e..7db4388037 100644 --- a/Content.Client/Power/APC/ApcVisualizer.cs +++ b/Content.Client/Power/APC/ApcVisualizer.cs @@ -1,6 +1,7 @@ using Content.Shared.APC; using JetBrains.Annotations; using Robust.Client.GameObjects; +using Robust.Client.State; using Robust.Shared.GameObjects; using Robust.Shared.IoC; using Robust.Shared.Maths; @@ -22,6 +23,9 @@ namespace Content.Client.Power.APC var sprite = IoCManager.Resolve().GetComponent(entity); + sprite.LayerMapSet(Layers.Panel, sprite.AddLayerState("apc0")); + sprite.LayerSetShader(Layers.Panel, "unshaded"); + sprite.LayerMapSet(Layers.ChargeState, sprite.AddLayerState("apco3-0")); sprite.LayerSetShader(Layers.ChargeState, "unshaded"); @@ -45,9 +49,21 @@ namespace Content.Client.Power.APC var ent = IoCManager.Resolve(); var sprite = ent.GetComponent(component.Owner); - if (component.TryGetData(ApcVisuals.ChargeState, out var state)) + if (component.TryGetData(ApcVisuals.PanelState, out var panelState)) { - switch (state) + switch (panelState) + { + case ApcPanelState.Closed: + sprite.LayerSetState(Layers.Panel, "apc0"); + break; + case ApcPanelState.Open: + sprite.LayerSetState(Layers.Panel, "apcframe"); + break; + } + } + if (component.TryGetData(ApcVisuals.ChargeState, out var chargeState)) + { + switch (chargeState) { case ApcChargeState.Lack: sprite.LayerSetState(Layers.ChargeState, "apco3-0"); @@ -65,7 +81,7 @@ namespace Content.Client.Power.APC if (ent.TryGetComponent(component.Owner, out SharedPointLightComponent? light)) { - light.Color = state switch + light.Color = chargeState switch { ApcChargeState.Lack => LackColor, ApcChargeState.Charging => ChargingColor, @@ -88,6 +104,7 @@ namespace Content.Client.Power.APC Equipment, Lighting, Environment, + Panel, } } } diff --git a/Content.Server/Construction/Completions/GivePrototype.cs b/Content.Server/Construction/Completions/GivePrototype.cs new file mode 100644 index 0000000000..086715aac9 --- /dev/null +++ b/Content.Server/Construction/Completions/GivePrototype.cs @@ -0,0 +1,44 @@ +using Content.Server.Stack; +using Content.Shared.Construction; +using Content.Shared.Hands.EntitySystems; +using Content.Shared.Prototypes; +using JetBrains.Annotations; +using Robust.Shared.Prototypes; +using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; + +namespace Content.Server.Construction.Completions +{ + [UsedImplicitly] + [DataDefinition] + public sealed class GivePrototype : IGraphAction + { + [DataField("prototype", customTypeSerializer:typeof(PrototypeIdSerializer))] + public string Prototype { get; private set; } = string.Empty; + [DataField("amount")] + public int Amount { get; private set; } = 1; + + public void PerformAction(EntityUid uid, EntityUid? userUid, IEntityManager entityManager) + { + if (string.IsNullOrEmpty(Prototype)) + return; + + var coordinates = entityManager.GetComponent(userUid ?? uid).Coordinates; + + if (EntityPrototypeHelpers.HasComponent(Prototype)) + { + var stackEnt = entityManager.SpawnEntity(Prototype, coordinates); + var stack = entityManager.GetComponent(stackEnt); + entityManager.EntitySysManager.GetEntitySystem().SetCount(stackEnt, Amount, stack); + entityManager.EntitySysManager.GetEntitySystem().PickupOrDrop(userUid, stackEnt); + } + else + { + for (var i = 0; i < Amount; i++) + { + var item = entityManager.SpawnEntity(Prototype, coordinates); + entityManager.EntitySysManager.GetEntitySystem().PickupOrDrop(userUid, item); + } + } + } + } +} diff --git a/Content.Server/Construction/Conditions/ApcPanel.cs b/Content.Server/Construction/Conditions/ApcPanel.cs new file mode 100644 index 0000000000..35d73aaba8 --- /dev/null +++ b/Content.Server/Construction/Conditions/ApcPanel.cs @@ -0,0 +1,51 @@ +using Content.Server.Power.Components; +using Content.Shared.Construction; +using Content.Shared.Examine; +using JetBrains.Annotations; + +namespace Content.Server.Construction.Conditions +{ + [UsedImplicitly] + [DataDefinition] + public sealed class ApcPanel : IGraphCondition + { + [DataField("open")] public bool Open { get; private set; } = true; + + public bool Condition(EntityUid uid, IEntityManager entityManager) + { + if (!entityManager.TryGetComponent(uid, out ApcComponent? apc)) + return true; + + return apc.IsApcOpen == Open; + } + + public bool DoExamine(ExaminedEvent args) + { + var entity = args.Examined; + + if (!IoCManager.Resolve().TryGetComponent(entity, out ApcComponent? apc)) return false; + + switch (Open) + { + case true when !apc.IsApcOpen: + args.PushMarkup(Loc.GetString("construction-examine-condition-apc-open")); + return true; + case false when apc.IsApcOpen: + args.PushMarkup(Loc.GetString("construction-examine-condition-apc-close")); + return true; + } + + return false; + } + + public IEnumerable GenerateGuideEntry() + { + yield return new ConstructionGuideEntry() + { + Localization = Open + ? "construction-step-condition-apc-open" + : "construction-step-condition-apc-close" + }; + } + } +} diff --git a/Content.Server/Power/Components/ApcComponent.cs b/Content.Server/Power/Components/ApcComponent.cs index df17b16703..d9ed4a4e9a 100644 --- a/Content.Server/Power/Components/ApcComponent.cs +++ b/Content.Server/Power/Components/ApcComponent.cs @@ -6,7 +6,6 @@ using Robust.Shared.Audio; namespace Content.Server.Power.Components; [RegisterComponent] -[Access(typeof(ApcSystem))] public sealed class ApcComponent : BaseApcNetComponent { [DataField("onReceiveMessageSound")] @@ -16,6 +15,13 @@ public sealed class ApcComponent : BaseApcNetComponent public ApcChargeState LastChargeState; public TimeSpan LastChargeStateTime; + /// + /// Is the panel open for this entity's APC? + /// + [ViewVariables] + [DataField("open")] + public bool IsApcOpen { get; set; } + [ViewVariables] public ApcExternalPowerState LastExternalState; public TimeSpan LastUiUpdate; @@ -38,4 +44,11 @@ public sealed class ApcComponent : BaseApcNetComponent { apcNet.RemoveApc(this); } + + [DataField("screwdriverOpenSound")] + public SoundSpecifier ScrewdriverOpenSound = new SoundPathSpecifier("/Audio/Machines/screwdriveropen.ogg"); + + [DataField("screwdriverCloseSound")] + public SoundSpecifier ScrewdriverCloseSound = new SoundPathSpecifier("/Audio/Machines/screwdriverclose.ogg"); + } diff --git a/Content.Server/Power/Components/ApcElectronicsComponent.cs b/Content.Server/Power/Components/ApcElectronicsComponent.cs new file mode 100644 index 0000000000..e403a53fee --- /dev/null +++ b/Content.Server/Power/Components/ApcElectronicsComponent.cs @@ -0,0 +1,11 @@ +using Robust.Shared.GameStates; + +namespace Content.Server.Power.Components +{ + [RegisterComponent] + /// + /// This object is an APC electronics, used for constructing APCs + /// + public sealed class ApcElectronicsComponent : Component + { } +} diff --git a/Content.Server/Power/EntitySystems/ApcSystem.cs b/Content.Server/Power/EntitySystems/ApcSystem.cs index 72341c3270..bcac0e4132 100644 --- a/Content.Server/Power/EntitySystems/ApcSystem.cs +++ b/Content.Server/Power/EntitySystems/ApcSystem.cs @@ -1,10 +1,16 @@ using Content.Server.Popups; using Content.Server.Power.Components; +using Content.Server.Tools; +using Content.Server.Wires; using Content.Shared.Access.Components; using Content.Shared.Access.Systems; using Content.Shared.APC; using Content.Shared.Emag.Systems; +using Content.Shared.Examine; +using Content.Shared.Interaction; using Content.Shared.Popups; +using Content.Shared.Tools.Components; +using Content.Shared.Wires; using JetBrains.Annotations; using Robust.Server.GameObjects; using Robust.Shared.Audio; @@ -20,6 +26,9 @@ namespace Content.Server.Power.EntitySystems [Dependency] private readonly UserInterfaceSystem _userInterfaceSystem = default!; [Dependency] private readonly PopupSystem _popupSystem = default!; [Dependency] private readonly IGameTiming _gameTiming = default!; + [Dependency] private readonly ToolSystem _toolSystem = default!; + + private const float ScrewTime = 2f; public override void Initialize() { @@ -31,6 +40,10 @@ namespace Content.Server.Power.EntitySystems SubscribeLocalEvent(OnBatteryChargeChanged); SubscribeLocalEvent(OnToggleMainBreaker); SubscribeLocalEvent(OnEmagged); + + SubscribeLocalEvent(OnToolFinished); + SubscribeLocalEvent(OnInteractUsing); + SubscribeLocalEvent(OnExamine); } // Change the APC's state only when the battery state changes, or when it's first created. @@ -88,13 +101,18 @@ namespace Content.Server.Power.EntitySystems if (!Resolve(uid, ref apc, ref battery)) return; + if (TryComp(uid, out AppearanceComponent? appearance)) + { + UpdatePanelAppearance(uid, appearance, apc); + } + var newState = CalcChargeState(uid, apc, battery); if (newState != apc.LastChargeState && apc.LastChargeStateTime + ApcComponent.VisualsChangeDelay < _gameTiming.CurTime) { apc.LastChargeState = newState; apc.LastChargeStateTime = _gameTiming.CurTime; - if (TryComp(uid, out AppearanceComponent? appearance)) + if (appearance != null) { appearance.SetData(ApcVisuals.ChargeState, newState); } @@ -168,5 +186,69 @@ namespace Content.Server.Power.EntitySystems return ApcExternalPowerState.Good; } + + public static ApcPanelState GetPanelState(ApcComponent apc) + { + if (apc.IsApcOpen) + return ApcPanelState.Open; + else + return ApcPanelState.Closed; + } + + private void OnInteractUsing(EntityUid uid, ApcComponent component, InteractUsingEvent args) + { + if (!EntityManager.TryGetComponent(args.Used, out ToolComponent? tool)) + return; + if (_toolSystem.UseTool(args.Used, args.User, uid, 0f, ScrewTime, new string[] { "Screwing" }, doAfterCompleteEvent: new ApcToolFinishedEvent(uid), toolComponent: tool)) + { + args.Handled = true; + } + } + + private void OnToolFinished(ApcToolFinishedEvent args) + { + if (!EntityManager.TryGetComponent(args.Target, out ApcComponent? component)) + return; + component.IsApcOpen = !component.IsApcOpen; + + if (TryComp(args.Target, out AppearanceComponent? appearance)) + { + UpdatePanelAppearance(args.Target, appearance); + } + + if (component.IsApcOpen) + { + SoundSystem.Play(component.ScrewdriverOpenSound.GetSound(), Filter.Pvs(args.Target), args.Target); + } + else + { + SoundSystem.Play(component.ScrewdriverCloseSound.GetSound(), Filter.Pvs(args.Target), args.Target); + } + } + + private void UpdatePanelAppearance(EntityUid uid, AppearanceComponent? appearance = null, ApcComponent? apc = null) + { + if (!Resolve(uid, ref appearance, ref apc, false)) + return; + + appearance.SetData(ApcVisuals.PanelState, GetPanelState(apc)); + } + + private sealed class ApcToolFinishedEvent : EntityEventArgs + { + public EntityUid Target { get; } + + public ApcToolFinishedEvent(EntityUid target) + { + Target = target; + } + } + + private void OnExamine(EntityUid uid, ApcComponent component, ExaminedEvent args) + { + args.PushMarkup(Loc.GetString(component.IsApcOpen + ? "apc-component-on-examine-panel-open" + : "apc-component-on-examine-panel-closed")); + } } } diff --git a/Content.Shared/APC/SharedApc.cs b/Content.Shared/APC/SharedApc.cs index c565adba1a..7669e32aa2 100644 --- a/Content.Shared/APC/SharedApc.cs +++ b/Content.Shared/APC/SharedApc.cs @@ -1,11 +1,32 @@ -using Robust.Shared.Serialization; +using Robust.Shared.Serialization; namespace Content.Shared.APC { [Serializable, NetSerializable] public enum ApcVisuals { - ChargeState + /// + /// APC lights/HUD. + /// + ChargeState, + + /// + /// APC frame. + /// + PanelState + } + + [Serializable, NetSerializable] + public enum ApcPanelState + { + /// + /// APC is closed. + /// + Closed, + /// + /// APC opened. + /// + Open } [Serializable, NetSerializable] diff --git a/Resources/Locale/en-US/apc/components/apc-component.ftl b/Resources/Locale/en-US/apc/components/apc-component.ftl index 36e132cb15..d180bcbe06 100644 --- a/Resources/Locale/en-US/apc/components/apc-component.ftl +++ b/Resources/Locale/en-US/apc/components/apc-component.ftl @@ -1 +1,3 @@ -apc-component-insufficient-access = Insufficient access! \ No newline at end of file +apc-component-insufficient-access = Insufficient access! +apc-component-on-examine-panel-open = The [color=lightgray]APC electronics panel[/color] is [color=red]open[/color]. +apc-component-on-examine-panel-closed = The [color=lightgray]APC electronics panel[/color] is [color=darkgreen]closed[/color]. \ No newline at end of file diff --git a/Resources/Locale/en-US/construction/conditions/apc-open-condition.ftl b/Resources/Locale/en-US/construction/conditions/apc-open-condition.ftl new file mode 100644 index 0000000000..19f2769212 --- /dev/null +++ b/Resources/Locale/en-US/construction/conditions/apc-open-condition.ftl @@ -0,0 +1,5 @@ +# APC +construction-examine-condition-apc-open = First, screw open the APC. +construction-examine-condition-apc-close = First, screw shut the APC. +construction-step-condition-apc-open = The APC electronics panel must be screwed open. +construction-step-condition-apc-close = The APC electronics panel must be screwed shut. diff --git a/Resources/Prototypes/Entities/Objects/Devices/Electronics/power_electronics.yml b/Resources/Prototypes/Entities/Objects/Devices/Electronics/power_electronics.yml index 398b5dc05b..b009247232 100644 --- a/Resources/Prototypes/Entities/Objects/Devices/Electronics/power_electronics.yml +++ b/Resources/Prototypes/Entities/Objects/Devices/Electronics/power_electronics.yml @@ -5,6 +5,7 @@ name: APC electronics description: Circuit used in APC construction. components: + - type: ApcElectronics - type: Sprite sprite: Objects/Misc/module.rsi state: charger_APC diff --git a/Resources/Prototypes/Entities/Structures/Power/apc.yml b/Resources/Prototypes/Entities/Structures/Power/apc.yml index 730baf8e24..29c82cfc9d 100644 --- a/Resources/Prototypes/Entities/Structures/Power/apc.yml +++ b/Resources/Prototypes/Entities/Structures/Power/apc.yml @@ -93,6 +93,61 @@ acts: [ "Destruction" ] - type: StationInfiniteBatteryTarget +# APC under construction +- type: entity + noSpawn: true + id: APCFrame + name: APC frame + description: A control terminal for the area's electrical systems, lacking the electronics. + placement: + mode: SnapgridCenter + components: + - type: Clickable + - type: InteractionOutline + - type: Transform + anchored: true + - type: Sprite + drawdepth: WallMountedItems + netsync: false + sprite: Structures/Power/apc.rsi + state: apcframe + - type: Construction + graph: APC + node: apcFrame + - type: WallMount + - type: Damageable + damageContainer: Inorganic + damageModifierSet: Metallic + - type: Destructible + thresholds: + - trigger: + !type:DamageTrigger + damage: 200 + behaviors: #excess damage, don't spawn entities. + - !type:DoActsBehavior + acts: [ "Destruction" ] + - trigger: + !type:DamageTrigger + damage: 50 + behaviors: + - !type:SpawnEntitiesBehavior + spawn: + SheetSteel1: + min: 1 + max: 1 + - !type:DoActsBehavior + acts: [ "Destruction" ] + +# Constructed APC +- type: entity + parent: BaseAPC + id: APCConstructed + suffix: Open + components: + - type: Apc + voltage: Apc + open: true + # APCs in use - type: entity parent: BaseAPC diff --git a/Resources/Prototypes/Recipes/Construction/Graphs/utilities/APC.yml b/Resources/Prototypes/Recipes/Construction/Graphs/utilities/APC.yml index 2f38cd1848..6808f62d7a 100644 --- a/Resources/Prototypes/Recipes/Construction/Graphs/utilities/APC.yml +++ b/Resources/Prototypes/Recipes/Construction/Graphs/utilities/APC.yml @@ -1,13 +1,43 @@ -- type: constructionGraph +- type: constructionGraph id: APC start: start graph: - node: start edges: - - to: apc + - to: apcFrame steps: - material: Steel amount: 3 + - node: apcFrame + entity: APCFrame + edges: + - to: apc + steps: + - component: ApcElectronics + name: "APC electronics" + doAfter: 2 + - to: start + completed: + - !type:GivePrototype + prototype: SheetSteel1 + amount: 3 + - !type:DeleteEntity {} + steps: + - tool: Screwing + doAfter: 2 + - node: apc - entity: BaseAPC + entity: APCConstructed + edges: + - to: apcFrame + completed: + - !type:GivePrototype + prototype: APCElectronics + amount: 1 + conditions: + - !type:ApcPanel + open: true + steps: + - tool: Prying + doAfter: 4 diff --git a/Resources/Textures/Structures/Power/apc.rsi/meta.json b/Resources/Textures/Structures/Power/apc.rsi/meta.json index 1ca38e53c7..865a256e11 100644 --- a/Resources/Textures/Structures/Power/apc.rsi/meta.json +++ b/Resources/Textures/Structures/Power/apc.rsi/meta.json @@ -10,6 +10,9 @@ { "name": "broken" }, + { + "name": "apcframe" + }, { "name": "sparks-unlit", "delays": [