diff --git a/Content.Server/_CorvaxNext/ExecutionChair/ExecutionChairComponent.cs b/Content.Server/_CorvaxNext/ExecutionChair/ExecutionChairComponent.cs new file mode 100644 index 00000000000..7e9895ee6c6 --- /dev/null +++ b/Content.Server/_CorvaxNext/ExecutionChair/ExecutionChairComponent.cs @@ -0,0 +1,85 @@ +using Content.Shared.DeviceLinking; +using Robust.Shared.Audio; +using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; +using Content.Server._CorvaxNext.ExecutionChair; + +namespace Content.Server._CorvaxNext.ExecutionChair; + +/// +/// This component represents the state and configuration of an Execution Chair entity. +/// It holds data fields that determine how the chair behaves when it delivers electric shocks +/// to entities buckled into it. It also provides fields for connecting to and receiving signals +/// from the device linking system. +/// +[RegisterComponent, Access(typeof(ExecutionChairSystem))] +public sealed partial class ExecutionChairComponent : Component +{ + /// + /// The next scheduled time at which this chair can deliver damage to strapped entities. + /// This is used to control the rate of repeated electrocution ticks. + /// + [ViewVariables] + public TimeSpan NextDamageTick = TimeSpan.Zero; + + /// + /// Indicates whether the chair is currently enabled. If true, and all conditions (powered, anchored, etc.) + /// are met, the chair will deliver electrical damage to any buckled entities at regular intervals. + /// + [DataField, AutoNetworkedField] + public bool Enabled = false; + + /// + /// Determines whether the chair should play a sound when entities are shocked. If set to true, + /// a sound from will be played each time damage is dealt. + /// + [DataField] + public bool PlaySoundOnShock = true; + + /// + /// Specifies which sound collection is played when entities are shocked. By default, uses a collection of + /// "sparks" sounds. This allows multiple random sparks audio clips to be played. + /// + [DataField] + public SoundSpecifier ShockNoises = new SoundCollectionSpecifier("sparks"); + + /// + /// Controls how loud the shock sound is. This value is applied to the base volume of the chosen sound + /// when played. + /// + [DataField] + public float ShockVolume = 20; + + /// + /// The amount of damage delivered to a buckled entity each damage tick while the chair is active. + /// + [DataField] + public int DamagePerTick = 25; + + /// + /// The duration in seconds for which the electrocution effect is applied each time damage is dealt. + /// For example, if set to 4, it electrocutes an entity for 4 seconds. + /// + [DataField] + public int DamageTime = 4; + + /// + /// The name of the device link port used to toggle the chair's state. Receiving a signal on this port + /// switches the enabled state from on to off or from off to on. + /// + [DataField] + public string TogglePort = "Toggle"; + + /// + /// The name of the device link port used to force the chair's state to enabled (on). + /// Receiving a signal here ensures the chair is active. + /// + [DataField] + public string OnPort = "On"; + + /// + /// The name of the device link port used to force the chair's state to disabled (off). + /// Receiving a signal here ensures the chair is inactive. + /// + [DataField] + public string OffPort = "Off"; +} diff --git a/Content.Server/_CorvaxNext/ExecutionChair/ExecutionChairSystem.cs b/Content.Server/_CorvaxNext/ExecutionChair/ExecutionChairSystem.cs new file mode 100644 index 00000000000..2e54af008fc --- /dev/null +++ b/Content.Server/_CorvaxNext/ExecutionChair/ExecutionChairSystem.cs @@ -0,0 +1,138 @@ +using Content.Server.DeviceLinking.Events; +using Content.Server.DeviceLinking.Systems; +using Content.Server.Electrocution; +using Content.Server.Power.EntitySystems; +using Content.Shared.Buckle.Components; +using Content.Shared.Popups; +using Robust.Shared.Audio; +using Robust.Shared.Audio.Systems; +using Robust.Shared.Random; +using Robust.Shared.Timing; + +namespace Content.Server._CorvaxNext.ExecutionChair +{ + public sealed partial class ExecutionChairSystem : EntitySystem + { + [Dependency] private readonly IGameTiming _gameTimer = default!; + [Dependency] private readonly IRobustRandom _randomGen = default!; + [Dependency] private readonly DeviceLinkSystem _deviceSystem = default!; + [Dependency] private readonly ElectrocutionSystem _shockSystem = default!; + [Dependency] private readonly SharedAudioSystem _soundSystem = default!; + [Dependency] private readonly SharedPopupSystem _popup = default!; + + private ISawmill _sawmill = default!; + + private const float VolumeVariationMin = 0.8f; + private const float VolumeVariationMax = 1.2f; + + public override void Initialize() + { + base.Initialize(); + SubscribeLocalEvent(OnChairSpawned); + SubscribeLocalEvent(OnSignalReceived); + + _sawmill = Logger.GetSawmill("execution_chair"); + } + + private void OnChairSpawned(EntityUid uid, ExecutionChairComponent component, ref MapInitEvent args) + { + _deviceSystem.EnsureSinkPorts(uid, component.TogglePort, component.OnPort, component.OffPort); + } + + private void OnSignalReceived(EntityUid uid, ExecutionChairComponent component, ref SignalReceivedEvent args) + { + // default case for switch below + bool DefaultCase(EntityUid uid, string port, ExecutionChairComponent component) + { + _sawmill.Debug($"Receieved unexpected port signal: {port} on chair {ToPrettyString(uid)}"); + return component.Enabled; + } + + var newState = args.Port switch + { + var p when p == component.TogglePort => !component.Enabled, + var p when p == component.OnPort => true, + var p when p == component.OffPort => false, + _ => DefaultCase(uid, args.Port, component) + }; + + UpdateChairState(uid, newState, component); + } + + private void UpdateChairState(EntityUid uid, bool activated, ExecutionChairComponent? component = null) + { + if (!Resolve(uid, ref component)) + return; + + component.Enabled = activated; + Dirty(uid, component); + var message = activated + ? Loc.GetString("execution-chair-turn-on") + : Loc.GetString("execution-chair-chair-turn-off"); + + _popup.PopupEntity(message, uid, PopupType.Medium); + } + + public override void Update(float frameTime) + { + base.Update(frameTime); + + var query = EntityQueryEnumerator(); + + while (query.MoveNext(out var uid, out var chair)) + { + if (!ValidateChairOperation(uid, chair)) + continue; + + if (!TryComp(uid, out var restraint) || restraint.BuckledEntities.Count == 0) + continue; + + ApplyShockEffect(uid, chair, restraint); + } + } + + /// + /// Ensures that the chair is in a valid state to operate: + /// - The chair is anchored in the world (not picked up or moved). + /// - The chair is powered. + /// - The chair is currently enabled/turned on. + /// - The current game time has passed beyond the next scheduled damage tick. + /// + private bool ValidateChairOperation(EntityUid uid, ExecutionChairComponent chair) + { + var transformComponent = Transform(uid); + return transformComponent.Anchored && + this.IsPowered(uid, EntityManager) && + chair.Enabled && + _gameTimer.CurTime >= chair.NextDamageTick; + } + + private void ApplyShockEffect(EntityUid uid, ExecutionChairComponent chair, StrapComponent restraint) + { + var shockDuration = TimeSpan.FromSeconds(chair.DamageTime); + + foreach (var target in restraint.BuckledEntities) + { + var volumeModifier = _randomGen.NextFloat(VolumeVariationMin, VolumeVariationMax); + + var shockSuccess = _shockSystem.TryDoElectrocution( + target, + uid, + chair.DamagePerTick, + shockDuration, + true, + volumeModifier, + ignoreInsulation: true + ); + + if (shockSuccess && chair.PlaySoundOnShock && chair.ShockNoises != null) + { + var audioParams = AudioParams.Default.WithVolume(chair.ShockVolume); + _soundSystem.PlayPvs(chair.ShockNoises, target, audioParams); + } + } + + chair.NextDamageTick = _gameTimer.CurTime + TimeSpan.FromSeconds(1); + } + } +} diff --git a/Resources/Locale/ru-RU/_CorvaxNext/executionchair/executionchair.ftl b/Resources/Locale/ru-RU/_CorvaxNext/executionchair/executionchair.ftl new file mode 100644 index 00000000000..6106190885d --- /dev/null +++ b/Resources/Locale/ru-RU/_CorvaxNext/executionchair/executionchair.ftl @@ -0,0 +1,2 @@ +execution-chair-turn-on = Воздух словно искрится... +execution-chair-chair-turn-off = Атмосфера разряжается. diff --git a/Resources/Locale/ru-RU/ss14-ru/prototypes/_corvaxnext/entities/structures/machines/execution_chair.ftl b/Resources/Locale/ru-RU/ss14-ru/prototypes/_corvaxnext/entities/structures/machines/execution_chair.ftl new file mode 100644 index 00000000000..b86f49cd469 --- /dev/null +++ b/Resources/Locale/ru-RU/ss14-ru/prototypes/_corvaxnext/entities/structures/machines/execution_chair.ftl @@ -0,0 +1,2 @@ +ent-ExecutionChair = электрический стул + .desc = Выглядит комфортно. diff --git a/Resources/Prototypes/_CorvaxNext/Entities/Structures/Machines/execution_chair.yml b/Resources/Prototypes/_CorvaxNext/Entities/Structures/Machines/execution_chair.yml new file mode 100644 index 00000000000..16dcbc5cd8a --- /dev/null +++ b/Resources/Prototypes/_CorvaxNext/Entities/Structures/Machines/execution_chair.yml @@ -0,0 +1,48 @@ +- type: entity + id: ExecutionChair + parent: BaseStructureDynamic + name: execution chair + description: Looks comfy. + components: + - type: Sprite + sprite: _CorvaxNext/Structures/Furniture/execution_chair.rsi + state: execution-chair + noRot: true + - type: Rotatable + - type: InteractionOutline + - type: Strap + position: Stand + buckleOffset: "0,-0.05" + - type: Fixtures + fixtures: + fix1: + shape: + !type:PhysShapeCircle + radius: 0.2 + density: 100 + mask: + - TableMask + - type: ExecutionChair + - type: ApcPowerReceiver + powerLoad: 1500 + - type: ExtensionCableReceiver + - type: Transform + anchored: true + - type: Damageable + damageModifierSet: Metallic + - type: Destructible + thresholds: + - trigger: + !type:DamageTrigger + damage: 100 + behaviors: + - !type:DoActsBehavior + acts: ["Destruction"] + - !type:PlaySoundBehavior + sound: + collection: MetalBreak + - !type:SpawnEntitiesBehavior + spawn: + SheetSteel: + min: 5 + max: 5 diff --git a/Resources/Textures/_CorvaxNext/Structures/Furniture/execution_chair.rsi/execution-chair.png b/Resources/Textures/_CorvaxNext/Structures/Furniture/execution_chair.rsi/execution-chair.png new file mode 100644 index 00000000000..52d5e3c785e Binary files /dev/null and b/Resources/Textures/_CorvaxNext/Structures/Furniture/execution_chair.rsi/execution-chair.png differ diff --git a/Resources/Textures/_CorvaxNext/Structures/Furniture/execution_chair.rsi/meta.json b/Resources/Textures/_CorvaxNext/Structures/Furniture/execution_chair.rsi/meta.json new file mode 100644 index 00000000000..bdeca289ae4 --- /dev/null +++ b/Resources/Textures/_CorvaxNext/Structures/Furniture/execution_chair.rsi/meta.json @@ -0,0 +1,15 @@ +{ + "version": 1, + "license": "CC-BY-SA-4.0", + "copyright": "Made by ko4erga(discord 266899933632659456) for Corvax", + "size": { + "x": 32, + "y": 32 + }, + "states": [ + { + "name": "execution-chair", + "directions": 4 + } + ] +}