Skip to content

Commit

Permalink
ExeCute
Browse files Browse the repository at this point in the history
  • Loading branch information
Vonsant committed Dec 10, 2024
1 parent 504cea1 commit ac06702
Show file tree
Hide file tree
Showing 7 changed files with 357 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>
/// 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.
/// </summary>
[RegisterComponent, Access(typeof(ExecutionChairSystem))]
public sealed partial class ExecutionChairComponent : Component
{
/// <summary>
/// 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.
/// </summary>
[ViewVariables]
public TimeSpan NextDamageTick = TimeSpan.Zero;

/// <summary>
/// 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.
/// </summary>
[DataField, AutoNetworkedField]
public bool Enabled = false;

/// <summary>
/// Determines whether the chair should play a sound when entities are shocked. If set to true,
/// a sound from <see cref="ShockNoises"/> will be played each time damage is dealt.
/// </summary>
[DataField]
public bool PlaySoundOnShock = true;

/// <summary>
/// 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.
/// </summary>
[DataField]
public SoundSpecifier ShockNoises = new SoundCollectionSpecifier("sparks");

/// <summary>
/// Controls how loud the shock sound is. This value is applied to the base volume of the chosen sound
/// when played.
/// </summary>
[DataField]
public float ShockVolume = 20;

/// <summary>
/// The amount of damage delivered to a buckled entity each damage tick while the chair is active.
/// </summary>
[DataField]
public int DamagePerTick = 25;

/// <summary>
/// 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.
/// </summary>
[DataField]
public int DamageTime = 4;

/// <summary>
/// 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.
/// </summary>
[DataField(customTypeSerializer: typeof(PrototypeIdSerializer<SourcePortPrototype>))]
public string TogglePort = "Toggle";

/// <summary>
/// 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.
/// </summary>
[DataField(customTypeSerializer: typeof(PrototypeIdSerializer<SourcePortPrototype>))]
public string OnPort = "On";

/// <summary>
/// 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.
/// </summary>
[DataField(customTypeSerializer: typeof(PrototypeIdSerializer<SourcePortPrototype>))]
public string OffPort = "Off";
}
205 changes: 205 additions & 0 deletions Content.Server/_CorvaxNext/ExecutionChair/ExecutionChairSystem.cs
Original file line number Diff line number Diff line change
@@ -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
{
/// <summary>
/// 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.
/// </summary>
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;

/// <summary>
/// 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.
/// </summary>
public override void Initialize()
{
base.Initialize();
SetupEventSubscriptions();
}

/// <summary>
/// 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.
/// </summary>
private void SetupEventSubscriptions()
{
SubscribeLocalEvent<ExecutionChairComponent, MapInitEvent>(OnChairSpawned);
SubscribeLocalEvent<ExecutionChairComponent, SignalReceivedEvent>(OnSignalReceived);
}

/// <summary>
/// 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.
/// </summary>
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);
}

/// <summary>
/// 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.
/// </summary>
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);
}

/// <summary>
/// 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.
/// </summary>
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);
}

/// <summary>
/// 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.
/// </summary>
public override void Update(float deltaTime)
{
base.Update(deltaTime);
ProcessActiveChairs();
}

/// <summary>
/// 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.
/// </summary>
private void ProcessActiveChairs()
{
var query = EntityQueryEnumerator<ExecutionChairComponent>();

// 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<StrapComponent>(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);
}
}

/// <summary>
/// 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.
/// </summary>
private bool ValidateChairOperation(EntityUid uid, ExecutionChairComponent chair)
{
var transformComponent = Transform(uid);
return transformComponent.Anchored &&
this.IsPowered(uid, EntityManager) &&
chair.Enabled &&
_gameTimer.CurTime >= chair.NextDamageTick;
}

/// <summary>
/// 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.
/// </summary>
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);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
execution-chair-turn-on = Воздух словно искрится...
execution-chair-chair-turn-off = Атмосфера разряжается.
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
ent-ExecutionChair = электрический стул
.desc = Выглядит комфортно.
Original file line number Diff line number Diff line change
@@ -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
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -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
}
]
}

0 comments on commit ac06702

Please sign in to comment.