Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Электрический стул. #148

Merged
merged 7 commits into from
Dec 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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]
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]
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]
public string OffPort = "Off";
}
138 changes: 138 additions & 0 deletions Content.Server/_CorvaxNext/ExecutionChair/ExecutionChairSystem.cs
Original file line number Diff line number Diff line change
@@ -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<ExecutionChairComponent, MapInitEvent>(OnChairSpawned);
SubscribeLocalEvent<ExecutionChairComponent, SignalReceivedEvent>(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<ExecutionChairComponent>();

while (query.MoveNext(out var uid, out var chair))
{
if (!ValidateChairOperation(uid, chair))
continue;

if (!TryComp<StrapComponent>(uid, out var restraint) || restraint.BuckledEntities.Count == 0)
continue;

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;
}

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);
}
}
}
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-4.0",
"copyright": "Made by ko4erga(discord 266899933632659456) for Corvax",
"size": {
"x": 32,
"y": 32
},
"states": [
{
"name": "execution-chair",
"directions": 4
}
]
}
Loading