diff --git a/Content.Server/_CorvaxNext/ExecutionChair/ExecutionChairComponent.cs b/Content.Server/_CorvaxNext/ExecutionChair/ExecutionChairComponent.cs new file mode 100644 index 00000000000..f62810f04fc --- /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(customTypeSerializer: typeof(PrototypeIdSerializer))] + 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(customTypeSerializer: typeof(PrototypeIdSerializer))] + 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(customTypeSerializer: typeof(PrototypeIdSerializer))] + 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..a1c2f786bde --- /dev/null +++ b/Content.Server/_CorvaxNext/ExecutionChair/ExecutionChairSystem.cs @@ -0,0 +1,205 @@ +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; +using Content.Server._CorvaxNext.ExecutionChair; + +namespace Content.Server._CorvaxNext.ExecutionChair +{ + /// + /// This system manages the logic and state of the Execution Chair entity, including responding to + /// incoming signals, applying electrocution damage to entities strapped into it, and handling sound + /// and popups when it activates or deactivates. + /// + public sealed partial class ExecutionChairSystem : EntitySystem + { + // Dependencies automatically resolved by the IoC container. + [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!; + + // Volume variation range for the shock sound effects to add some randomness. + private const float VolumeVariationMin = 0.8f; + private const float VolumeVariationMax = 1.2f; + + /// + /// Initializes the system and sets up event subscriptions for when the chair is spawned + /// and when signals are received (e.g., toggle, on, off) from a device network. + /// + public override void Initialize() + { + base.Initialize(); + SetupEventSubscriptions(); + } + + /// + /// Subscribes the system to relevant local events: + /// - MapInitEvent: when the chair is placed on the map, ensuring the correct device ports. + /// - SignalReceivedEvent: when the chair receives device link signals to turn on/off or toggle. + /// + private void SetupEventSubscriptions() + { + SubscribeLocalEvent(OnChairSpawned); + SubscribeLocalEvent(OnSignalReceived); + } + + /// + /// Called when the Execution Chair is initialized on the map. Ensures that the chair's + /// device link ports (Toggle, On, Off) are correctly created so it can receive signals. + /// + private void OnChairSpawned(EntityUid uid, ExecutionChairComponent component, ref MapInitEvent args) + { + // Ensure that all required device ports are available for linking. + _deviceSystem.EnsureSinkPorts(uid, component.TogglePort, component.OnPort, component.OffPort); + } + + /// + /// Called when the Execution Chair receives a signal from linked devices. + /// Depending on the port signaled, the chair will toggle, turn on, or turn off. + /// Any unexpected port signals are logged. + /// + private void OnSignalReceived(EntityUid uid, ExecutionChairComponent component, ref SignalReceivedEvent args) + { + var portSignal = args.Port; + + // Determine new state based on received signal. + var newState = portSignal switch + { + var p when p == component.TogglePort => !component.Enabled, + var p when p == component.OnPort => true, + var p when p == component.OffPort => false, + _ => component.Enabled // If port does not match expected, state remains unchanged. + }; + + // Log a debug message if the port signal is unexpected. + if (portSignal != component.TogglePort && portSignal != component.OnPort && portSignal != component.OffPort) + { + Logger.DebugS("execution_chair", $"Received unexpected port signal: {portSignal} on chair {ToPrettyString(uid)}"); + } + + // Update the chair state based on the new determined state. + UpdateChairState(uid, newState, component); + } + + /// + /// Updates the Execution Chair's active state (enabled or disabled), synchronizes that state, + /// and shows a popup message indicating the new state to nearby players. + /// + private void UpdateChairState(EntityUid uid, bool activated, ExecutionChairComponent? component = null) + { + // Resolve the component if not provided, ensuring we have a valid reference. + if (!Resolve(uid, ref component)) + return; + + component.Enabled = activated; + + // Mark the component as "Dirty" so that any networked clients update their state. + Dirty(uid, component); + + // Display a popup message to indicate the chair has been turned on or off. + var message = activated + ? Loc.GetString("execution-chair-turn-on") + : Loc.GetString("execution-chair-chair-turn-off"); + + _popup.PopupEntity(message, uid, PopupType.Medium); + } + + /// + /// Called each frame (or tick). If a chair is active, powered, anchored, and has entities strapped in, + /// it attempts to electrocute those entities at regular intervals. + /// + public override void Update(float deltaTime) + { + base.Update(deltaTime); + ProcessActiveChairs(); + } + + /// + /// Iterates over all Execution Chairs currently in the game. + /// For each chair, if it is enabled, anchored, and powered, and if the time has come for the next damage tick, + /// applies an electrocution effect to all buckled entities. + /// + private void ProcessActiveChairs() + { + var query = EntityQueryEnumerator(); + + // Process each chair found in the world. + while (query.MoveNext(out var uid, out var chair)) + { + // Validate that the chair can operate (is anchored, powered, enabled, and ready for next damage tick). + if (!ValidateChairOperation(uid, chair)) + continue; + + // Check if the chair has a StrapComponent and actually has entities buckled to it. + if (!TryComp(uid, out var restraint) || restraint.BuckledEntities.Count == 0) + continue; + + // Apply shock damage and effects to all entities buckled into the chair. + 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; + } + + /// + /// Attempts to electrocute all entities currently strapped to the chair, causing them damage. + /// If successful, plays shock sound effects (if configured). + /// After applying the shocks, sets the next damage tick to one second later. + /// + private void ApplyShockEffect(EntityUid uid, ExecutionChairComponent chair, StrapComponent restraint) + { + // Calculate the duration for which each shock is applied. + var shockDuration = TimeSpan.FromSeconds(chair.DamageTime); + + // For each buckled entity, try to perform an electrocution action. + foreach (var target in restraint.BuckledEntities) + { + // Randomize volume a bit to make each shock sound slightly different. + var volumeModifier = _randomGen.NextFloat(VolumeVariationMin, VolumeVariationMax); + + // Attempt to electrocute the target. Ignore insulation to ensure damage. + var shockSuccess = _shockSystem.TryDoElectrocution( + target, + uid, + chair.DamagePerTick, + shockDuration, + true, + volumeModifier, + ignoreInsulation: true); + + // If the shock was applied and chair is configured to play sounds, play shock sound. + if (shockSuccess && chair.PlaySoundOnShock && chair.ShockNoises != null) + { + var audioParams = AudioParams.Default.WithVolume(chair.ShockVolume); + _soundSystem.PlayPvs(chair.ShockNoises, target, audioParams); + } + } + + // Schedule the next damage tick one second in the future. + 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..e999d37f8e3 --- /dev/null +++ b/Resources/Textures/_CorvaxNext/Structures/Furniture/execution_chair.rsi/meta.json @@ -0,0 +1,15 @@ +{ + "version": 1, + "license": "CC-BY-SA-3.0", + "copyright": "Taken from https://github.com/tgstation/tgstation/blob/HEAD/icons/obj/chairs.dmi", + "size": { + "x": 32, + "y": 32 + }, + "states": [ + { + "name": "execution-chair", + "directions": 4 + } + ] +}