From ac06702c199bc3b66e2eae3487ce44d84e7feaff Mon Sep 17 00:00:00 2001 From: Vonsant Date: Tue, 10 Dec 2024 22:22:25 +0300 Subject: [PATCH] ExeCute --- .../ExecutionChair/ExecutionChairComponent.cs | 85 ++++++++ .../ExecutionChair/ExecutionChairSystem.cs | 205 ++++++++++++++++++ .../executionchair/executionchair.ftl | 2 + .../structures/machines/execution_chair.ftl | 2 + .../Structures/Machines/execution_chair.yml | 48 ++++ .../execution_chair.rsi/execution-chair.png | Bin 0 -> 2980 bytes .../Furniture/execution_chair.rsi/meta.json | 15 ++ 7 files changed, 357 insertions(+) create mode 100644 Content.Server/_CorvaxNext/ExecutionChair/ExecutionChairComponent.cs create mode 100644 Content.Server/_CorvaxNext/ExecutionChair/ExecutionChairSystem.cs create mode 100644 Resources/Locale/ru-RU/_CorvaxNext/executionchair/executionchair.ftl create mode 100644 Resources/Locale/ru-RU/ss14-ru/prototypes/_corvaxnext/entities/structures/machines/execution_chair.ftl create mode 100644 Resources/Prototypes/_CorvaxNext/Entities/Structures/Machines/execution_chair.yml create mode 100644 Resources/Textures/_CorvaxNext/Structures/Furniture/execution_chair.rsi/execution-chair.png create mode 100644 Resources/Textures/_CorvaxNext/Structures/Furniture/execution_chair.rsi/meta.json 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 0000000000000000000000000000000000000000..52d5e3c785e56ce7d96a0b79b3ee00ef4a2fdbe4 GIT binary patch literal 2980 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 aP5*xq;8G1FgDo!r0000