mirror of
https://github.com/WWhiteDreamProject/wwdpublic.git
synced 2026-04-17 13:37:47 +03:00
## Mirror of PR #22962: [Landmine stepoff](https://github.com/space-wizards/space-station-14/pull/22962) from <img src="https://avatars.githubusercontent.com/u/10567778?v=4" alt="space-wizards" width="22"/> [space-wizards](https://github.com/space-wizards)/[space-station-14](https://github.com/space-wizards/space-station-14) ###### `54dd273f660d6d8d523d0771bb8d8437373b082e` PR opened by <img src="https://avatars.githubusercontent.com/u/59531932?v=4" width="16"/><a href="https://github.com/YuriyKiss"> YuriyKiss</a> at 2023-12-25 12:38:55 UTC --- PR changed 13 files with 95 additions and 47 deletions. The PR had the following labels: - Status: Awaiting Changes --- <details open="true"><summary><h1>Original Body</h1></summary> > ## About the PR > Landmine will explode when player steps off it. Landmine will make a sound effect when player steps on it > Close #21658 (Issue has some other suggestions, turn them into separate issues if relevant) > > Requires https://github.com/space-wizards/RobustToolbox/pull/4883 > > ## Why / Balance > IRL some landmines will only trigger after you release the pressure put on them. > > ## Technical details > There are two landmine modes now, one will make landmine trigger immediately after player steps on them another one will only trigger after player steps off the landmine (this is the default now). > > ## Media > Landmine stepoff in action: > > https://github.com/space-wizards/space-station-14/assets/59531932/6e884619-7217-4301-bd78-6e843e0d78f1 > > - [X] I have added screenshots/videos to this PR showcasing its changes ingame, **or** this PR does not require an ingame showcase > > ## Breaking changes > > > **Changelog** > - tweak: landmines will trigger after you step off them > - add: landmine trigger sound > </details> Signed-off-by: VMSolidus <evilexecutive@gmail.com> Co-authored-by: SimpleStation14 <Unknown> Co-authored-by: VMSolidus <evilexecutive@gmail.com>
364 lines
15 KiB
C#
364 lines
15 KiB
C#
using Content.Server.Administration.Logs;
|
|
using Content.Server.Body.Systems;
|
|
using Content.Server.Chemistry.Containers.EntitySystems;
|
|
using Content.Server.Explosion.Components;
|
|
using Content.Server.Flash;
|
|
using Content.Shared.Flash.Components;
|
|
using Content.Server.Radio.EntitySystems;
|
|
using Content.Shared.Chemistry.Components;
|
|
using Content.Shared.Chemistry.Components.SolutionManager;
|
|
using Content.Shared.Database;
|
|
using Content.Shared.Explosion.Components;
|
|
using Content.Shared.Explosion.Components.OnTrigger;
|
|
using Content.Shared.Implants.Components;
|
|
using Content.Shared.Interaction;
|
|
using Content.Shared.Inventory;
|
|
using Content.Shared.Mobs;
|
|
using Content.Shared.Mobs.Components;
|
|
using Content.Shared.Payload.Components;
|
|
using Content.Shared.Radio;
|
|
using Content.Shared.Slippery;
|
|
using Content.Shared.StepTrigger.Systems;
|
|
using Content.Shared.Trigger;
|
|
using Content.Shared.Weapons.Ranged.Events;
|
|
using JetBrains.Annotations;
|
|
using Robust.Shared.Audio;
|
|
using Robust.Shared.Audio.Systems;
|
|
using Robust.Shared.Containers;
|
|
using Robust.Shared.Physics.Events;
|
|
using Robust.Shared.Physics.Systems;
|
|
using Robust.Shared.Prototypes;
|
|
using Robust.Shared.Random;
|
|
using Robust.Shared.Player;
|
|
using Content.Shared.Coordinates;
|
|
|
|
namespace Content.Server.Explosion.EntitySystems
|
|
{
|
|
/// <summary>
|
|
/// Raised whenever something is Triggered on the entity.
|
|
/// </summary>
|
|
public sealed class TriggerEvent : HandledEntityEventArgs
|
|
{
|
|
public EntityUid Triggered { get; }
|
|
public EntityUid? User { get; }
|
|
|
|
public TriggerEvent(EntityUid triggered, EntityUid? user = null)
|
|
{
|
|
Triggered = triggered;
|
|
User = user;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Raised when timer trigger becomes active.
|
|
/// </summary>
|
|
[ByRefEvent]
|
|
public readonly record struct ActiveTimerTriggerEvent(EntityUid Triggered, EntityUid? User);
|
|
|
|
[UsedImplicitly]
|
|
public sealed partial class TriggerSystem : EntitySystem
|
|
{
|
|
[Dependency] private readonly ExplosionSystem _explosions = default!;
|
|
[Dependency] private readonly FixtureSystem _fixtures = default!;
|
|
[Dependency] private readonly FlashSystem _flashSystem = default!;
|
|
[Dependency] private readonly SharedBroadphaseSystem _broadphase = default!;
|
|
[Dependency] private readonly IAdminLogManager _adminLogger = default!;
|
|
[Dependency] private readonly SharedContainerSystem _container = default!;
|
|
[Dependency] private readonly BodySystem _body = default!;
|
|
[Dependency] private readonly SharedAudioSystem _audio = default!;
|
|
[Dependency] private readonly SharedTransformSystem _transformSystem = default!;
|
|
[Dependency] private readonly RadioSystem _radioSystem = default!;
|
|
[Dependency] private readonly IRobustRandom _random = default!;
|
|
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
|
|
[Dependency] private readonly SolutionContainerSystem _solutionContainerSystem = default!;
|
|
[Dependency] private readonly InventorySystem _inventory = default!;
|
|
|
|
public override void Initialize()
|
|
{
|
|
base.Initialize();
|
|
|
|
InitializeProximity();
|
|
InitializeOnUse();
|
|
InitializeSignal();
|
|
InitializeTimedCollide();
|
|
InitializeVoice();
|
|
InitializeMobstate();
|
|
|
|
SubscribeLocalEvent<TriggerOnSpawnComponent, MapInitEvent>(OnSpawnTriggered);
|
|
SubscribeLocalEvent<TriggerOnCollideComponent, StartCollideEvent>(OnTriggerCollide);
|
|
SubscribeLocalEvent<TriggerOnActivateComponent, ActivateInWorldEvent>(OnActivate);
|
|
SubscribeLocalEvent<TriggerImplantActionComponent, ActivateImplantEvent>(OnImplantTrigger);
|
|
SubscribeLocalEvent<TriggerOnStepTriggerComponent, StepTriggeredOffEvent>(OnStepTriggered);
|
|
SubscribeLocalEvent<TriggerOnSlipComponent, SlipEvent>(OnSlipTriggered);
|
|
SubscribeLocalEvent<TriggerWhenEmptyComponent, OnEmptyGunShotEvent>(OnEmptyTriggered);
|
|
|
|
SubscribeLocalEvent<SpawnOnTriggerComponent, TriggerEvent>(OnSpawnTrigger);
|
|
SubscribeLocalEvent<DeleteOnTriggerComponent, TriggerEvent>(HandleDeleteTrigger);
|
|
SubscribeLocalEvent<ExplodeOnTriggerComponent, TriggerEvent>(HandleExplodeTrigger);
|
|
SubscribeLocalEvent<FlashOnTriggerComponent, TriggerEvent>(HandleFlashTrigger);
|
|
SubscribeLocalEvent<GibOnTriggerComponent, TriggerEvent>(HandleGibTrigger);
|
|
|
|
SubscribeLocalEvent<AnchorOnTriggerComponent, TriggerEvent>(OnAnchorTrigger);
|
|
SubscribeLocalEvent<SoundOnTriggerComponent, TriggerEvent>(OnSoundTrigger);
|
|
SubscribeLocalEvent<RattleComponent, TriggerEvent>(HandleRattleTrigger);
|
|
}
|
|
|
|
private void OnSoundTrigger(EntityUid uid, SoundOnTriggerComponent component, TriggerEvent args)
|
|
{
|
|
if (component.RemoveOnTrigger) // if the component gets removed when it's triggered
|
|
{
|
|
var xform = Transform(uid);
|
|
_audio.PlayPvs(component.Sound, xform.Coordinates); // play the sound at its last known coordinates
|
|
}
|
|
else // if the component doesn't get removed when triggered
|
|
{
|
|
_audio.PlayPvs(component.Sound, uid); // have the sound follow the entity itself
|
|
}
|
|
}
|
|
|
|
private void OnAnchorTrigger(EntityUid uid, AnchorOnTriggerComponent component, TriggerEvent args)
|
|
{
|
|
var xform = Transform(uid);
|
|
|
|
if (xform.Anchored)
|
|
return;
|
|
|
|
_transformSystem.AnchorEntity(uid, xform);
|
|
|
|
if (component.RemoveOnTrigger)
|
|
RemCompDeferred<AnchorOnTriggerComponent>(uid);
|
|
}
|
|
|
|
private void OnSpawnTrigger(EntityUid uid, SpawnOnTriggerComponent component, TriggerEvent args)
|
|
{
|
|
var xform = Transform(uid);
|
|
|
|
var coords = xform.Coordinates;
|
|
|
|
if (!coords.IsValid(EntityManager))
|
|
return;
|
|
|
|
Spawn(component.Proto, coords);
|
|
}
|
|
|
|
private void HandleExplodeTrigger(EntityUid uid, ExplodeOnTriggerComponent component, TriggerEvent args)
|
|
{
|
|
_explosions.TriggerExplosive(uid, user: args.User);
|
|
args.Handled = true;
|
|
}
|
|
|
|
private void HandleFlashTrigger(EntityUid uid, FlashOnTriggerComponent component, TriggerEvent args)
|
|
{
|
|
// TODO Make flash durations sane ffs.
|
|
_flashSystem.FlashArea(uid, args.User, component.Range, component.Duration * 1000f);
|
|
args.Handled = true;
|
|
}
|
|
|
|
private void HandleDeleteTrigger(EntityUid uid, DeleteOnTriggerComponent component, TriggerEvent args)
|
|
{
|
|
EntityManager.QueueDeleteEntity(uid);
|
|
args.Handled = true;
|
|
}
|
|
|
|
private void HandleGibTrigger(EntityUid uid, GibOnTriggerComponent component, TriggerEvent args)
|
|
{
|
|
if (!TryComp<TransformComponent>(uid, out var xform))
|
|
return;
|
|
if (component.DeleteItems)
|
|
{
|
|
var items = _inventory.GetHandOrInventoryEntities(xform.ParentUid);
|
|
foreach (var item in items)
|
|
{
|
|
Del(item);
|
|
}
|
|
}
|
|
_body.GibBody(xform.ParentUid, true);
|
|
args.Handled = true;
|
|
}
|
|
|
|
private void HandleRattleTrigger(EntityUid uid, RattleComponent component, TriggerEvent args)
|
|
{
|
|
if (!TryComp<SubdermalImplantComponent>(uid, out var implanted))
|
|
return;
|
|
|
|
if (implanted.ImplantedEntity == null)
|
|
return;
|
|
|
|
// Gets location of the implant
|
|
var ownerXform = Transform(uid);
|
|
var pos = ownerXform.MapPosition;
|
|
var x = (int) pos.X;
|
|
var y = (int) pos.Y;
|
|
var posText = $"({x}, {y})";
|
|
|
|
var critMessage = Loc.GetString(component.CritMessage, ("user", implanted.ImplantedEntity.Value), ("position", posText));
|
|
var deathMessage = Loc.GetString(component.DeathMessage, ("user", implanted.ImplantedEntity.Value), ("position", posText));
|
|
|
|
if (!TryComp<MobStateComponent>(implanted.ImplantedEntity, out var mobstate))
|
|
return;
|
|
|
|
// Sends a message to the radio channel specified by the implant
|
|
if (mobstate.CurrentState == MobState.Critical)
|
|
_radioSystem.SendRadioMessage(uid, critMessage, _prototypeManager.Index<RadioChannelPrototype>(component.RadioChannel), uid);
|
|
if (mobstate.CurrentState == MobState.Dead)
|
|
_radioSystem.SendRadioMessage(uid, deathMessage, _prototypeManager.Index<RadioChannelPrototype>(component.RadioChannel), uid);
|
|
|
|
args.Handled = true;
|
|
}
|
|
|
|
private void OnTriggerCollide(EntityUid uid, TriggerOnCollideComponent component, ref StartCollideEvent args)
|
|
{
|
|
if (args.OurFixtureId == component.FixtureID && (!component.IgnoreOtherNonHard || args.OtherFixture.Hard))
|
|
Trigger(uid);
|
|
}
|
|
|
|
private void OnSpawnTriggered(EntityUid uid, TriggerOnSpawnComponent component, MapInitEvent args)
|
|
{
|
|
Trigger(uid);
|
|
}
|
|
|
|
private void OnActivate(EntityUid uid, TriggerOnActivateComponent component, ActivateInWorldEvent args)
|
|
{
|
|
Trigger(uid, args.User);
|
|
args.Handled = true;
|
|
}
|
|
|
|
private void OnImplantTrigger(EntityUid uid, TriggerImplantActionComponent component, ActivateImplantEvent args)
|
|
{
|
|
args.Handled = Trigger(uid);
|
|
}
|
|
|
|
private void OnStepTriggered(EntityUid uid, TriggerOnStepTriggerComponent component, ref StepTriggeredOffEvent args)
|
|
{
|
|
Trigger(uid, args.Tripper);
|
|
}
|
|
|
|
private void OnSlipTriggered(EntityUid uid, TriggerOnSlipComponent component, ref SlipEvent args)
|
|
{
|
|
Trigger(uid, args.Slipped);
|
|
}
|
|
|
|
private void OnEmptyTriggered(EntityUid uid, TriggerWhenEmptyComponent component, ref OnEmptyGunShotEvent args)
|
|
{
|
|
Trigger(uid, args.EmptyGun);
|
|
}
|
|
|
|
public bool Trigger(EntityUid trigger, EntityUid? user = null)
|
|
{
|
|
var triggerEvent = new TriggerEvent(trigger, user);
|
|
EntityManager.EventBus.RaiseLocalEvent(trigger, triggerEvent, true);
|
|
return triggerEvent.Handled;
|
|
}
|
|
|
|
public void TryDelay(EntityUid uid, float amount, ActiveTimerTriggerComponent? comp = null)
|
|
{
|
|
if (!Resolve(uid, ref comp, false))
|
|
return;
|
|
|
|
comp.TimeRemaining += amount;
|
|
}
|
|
|
|
public void HandleTimerTrigger(EntityUid uid, EntityUid? user, float delay, float beepInterval, float? initialBeepDelay, SoundSpecifier? beepSound)
|
|
{
|
|
if (delay <= 0)
|
|
{
|
|
RemComp<ActiveTimerTriggerComponent>(uid);
|
|
Trigger(uid, user);
|
|
return;
|
|
}
|
|
|
|
if (HasComp<ActiveTimerTriggerComponent>(uid))
|
|
return;
|
|
|
|
if (user != null)
|
|
{
|
|
// Check if entity is bomb/mod. grenade/etc
|
|
if (_container.TryGetContainer(uid, "payload", out BaseContainer? container) &&
|
|
container.ContainedEntities.Count > 0 &&
|
|
TryComp(container.ContainedEntities[0], out ChemicalPayloadComponent? chemicalPayloadComponent))
|
|
{
|
|
// If a beaker is missing, the entity won't explode, so no reason to log it
|
|
if (chemicalPayloadComponent?.BeakerSlotA.Item is not { } beakerA ||
|
|
chemicalPayloadComponent?.BeakerSlotB.Item is not { } beakerB ||
|
|
!TryComp(beakerA, out SolutionContainerManagerComponent? containerA) ||
|
|
!TryComp(beakerB, out SolutionContainerManagerComponent? containerB) ||
|
|
!TryComp(beakerA, out FitsInDispenserComponent? fitsA) ||
|
|
!TryComp(beakerB, out FitsInDispenserComponent? fitsB) ||
|
|
!_solutionContainerSystem.TryGetSolution((beakerA, containerA), fitsA.Solution, out _, out var solutionA) ||
|
|
!_solutionContainerSystem.TryGetSolution((beakerB, containerB), fitsB.Solution, out _, out var solutionB))
|
|
return;
|
|
|
|
_adminLogger.Add(LogType.Trigger,
|
|
$"{ToPrettyString(user.Value):user} started a {delay} second timer trigger on entity {ToPrettyString(uid):timer}, which contains {SolutionContainerSystem.ToPrettyString(solutionA)} in one beaker and {SolutionContainerSystem.ToPrettyString(solutionB)} in the other.");
|
|
}
|
|
else
|
|
{
|
|
_adminLogger.Add(LogType.Trigger,
|
|
$"{ToPrettyString(user.Value):user} started a {delay} second timer trigger on entity {ToPrettyString(uid):timer}");
|
|
}
|
|
|
|
}
|
|
else
|
|
{
|
|
_adminLogger.Add(LogType.Trigger,
|
|
$"{delay} second timer trigger started on entity {ToPrettyString(uid):timer}");
|
|
}
|
|
|
|
var active = AddComp<ActiveTimerTriggerComponent>(uid);
|
|
active.TimeRemaining = delay;
|
|
active.User = user;
|
|
active.BeepSound = beepSound;
|
|
active.BeepInterval = beepInterval;
|
|
active.TimeUntilBeep = initialBeepDelay == null ? active.BeepInterval : initialBeepDelay.Value;
|
|
|
|
var ev = new ActiveTimerTriggerEvent(uid, user);
|
|
RaiseLocalEvent(uid, ref ev);
|
|
|
|
if (TryComp<AppearanceComponent>(uid, out var appearance))
|
|
_appearance.SetData(uid, TriggerVisuals.VisualState, TriggerVisualState.Primed, appearance);
|
|
}
|
|
|
|
public override void Update(float frameTime)
|
|
{
|
|
base.Update(frameTime);
|
|
|
|
UpdateProximity();
|
|
UpdateTimer(frameTime);
|
|
UpdateTimedCollide(frameTime);
|
|
}
|
|
|
|
private void UpdateTimer(float frameTime)
|
|
{
|
|
HashSet<EntityUid> toRemove = new();
|
|
var query = EntityQueryEnumerator<ActiveTimerTriggerComponent>();
|
|
while (query.MoveNext(out var uid, out var timer))
|
|
{
|
|
timer.TimeRemaining -= frameTime;
|
|
timer.TimeUntilBeep -= frameTime;
|
|
|
|
if (timer.TimeRemaining <= 0)
|
|
{
|
|
Trigger(uid, timer.User);
|
|
toRemove.Add(uid);
|
|
continue;
|
|
}
|
|
|
|
if (timer.BeepSound == null || timer.TimeUntilBeep > 0)
|
|
continue;
|
|
|
|
timer.TimeUntilBeep += timer.BeepInterval;
|
|
_audio.PlayPvs(timer.BeepSound, uid, timer.BeepSound.Params);
|
|
}
|
|
|
|
foreach (var uid in toRemove)
|
|
{
|
|
RemComp<ActiveTimerTriggerComponent>(uid);
|
|
|
|
// In case this is a re-usable grenade, un-prime it.
|
|
if (TryComp<AppearanceComponent>(uid, out var appearance))
|
|
_appearance.SetData(uid, TriggerVisuals.VisualState, TriggerVisualState.Unprimed, appearance);
|
|
}
|
|
}
|
|
}
|
|
}
|