Skip to content
Closed
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
@@ -1,17 +1,60 @@
using Content.Server.StationEvents.Events;
// SPDX-FileCopyrightText: 2023 DrSmugleaf
// SPDX-FileCopyrightText: 2023 Nemanja
// SPDX-FileCopyrightText: 2023 Nim
// SPDX-FileCopyrightText: 2023 Slava0135
// SPDX-FileCopyrightText: 2023 metalgearsloth
// SPDX-FileCopyrightText: 2025 Jakumba
// SPDX-FileCopyrightText: 2025 empty0set
// SPDX-FileCopyrightText: 2025 sleepyyapril
//
// SPDX-License-Identifier: MIT

using Content.Server.StationEvents.Events;
using Content.Shared.EntityTable.EntitySelectors;
using Content.Shared.Storage;
using Robust.Shared.Audio;
using Robust.Shared.Map; // DeltaV

namespace Content.Server.StationEvents.Components;

[RegisterComponent, Access(typeof(VentCrittersRule))]
public sealed partial class VentCrittersRuleComponent : Component
{
[DataField("entries")]
public List<EntitySpawnEntry> Entries = new();
// DeltaV: Replaced by Table
//[DataField("entries")]
//public List<EntitySpawnEntry> Entries = new();

/// <summary>
/// DeltaV: Table of possible entities to spawn.
/// </summary>
[DataField(required: true)]
public EntityTableSelector Table = default!;

/// <summary>
/// At least one special entry is guaranteed to spawn
/// </summary>
[DataField("specialEntries")]
public List<EntitySpawnEntry> SpecialEntries = new();


/// <summary>
/// DeltaV: Base minimum number of critters to spawn.
/// </summary>
[DataField]
public int Min = 3;

/// <summary>
/// DeltaV: Base maximum number of critters to spawn.
/// </summary>
[DataField]
public int Max = 4;

/// <summary>
/// DeltaV: Min and max get multiplied by the player count then divided by this.
/// </summary>
[DataField]
public int PlayerRatio = 20;

[DataField("sound")]
public SoundSpecifier? Sound = new SoundPathSpecifier("/Audio/_DEN/VentCritters/vent_harmless_critter.ogg");
}
158 changes: 134 additions & 24 deletions Content.Server/StationEvents/Events/VentCrittersRule.cs
Original file line number Diff line number Diff line change
@@ -1,61 +1,171 @@
// SPDX-FileCopyrightText: 2023 Nemanja
// SPDX-FileCopyrightText: 2023 Nim
// SPDX-FileCopyrightText: 2023 Slava0135
// SPDX-FileCopyrightText: 2023 metalgearsloth
// SPDX-FileCopyrightText: 2024 VMSolidus
// SPDX-FileCopyrightText: 2024 deltanedas
// SPDX-FileCopyrightText: 2025 Eightballll
// SPDX-FileCopyrightText: 2025 Jakumba
// SPDX-FileCopyrightText: 2025 empty0set
// SPDX-FileCopyrightText: 2025 portfiend
// SPDX-FileCopyrightText: 2025 sleepyyapril
//
// SPDX-License-Identifier: AGPL-3.0-or-later

using Content.Server.StationEvents.Components;
using Content.Server.Antag;
using Content.Shared.EntityTable;
using Content.Shared.GameTicking.Components;
using Content.Shared.Station.Components;
using Content.Shared.Storage;
using Robust.Shared.Map;
using Robust.Shared.Player;
using Robust.Shared.Random;
using Robust.Shared.Audio;
using Content.Server.Chat.Systems;
using Content.Shared.Pinpointer;
using System.Linq;

namespace Content.Server.StationEvents.Events;

/// <summary>
/// DeltaV: Reworked vent critters to spawn a number of mobs at a single telegraphed location.
/// This gives players time to run away and let sec do their job.
/// </summary>
/// <remarks>
/// This entire file is rewritten, ignore upstream changes.
/// </remarks>
///
public sealed class VentCrittersRule : StationEventSystem<VentCrittersRuleComponent>
{
/*
* DO NOT COPY PASTE THIS TO MAKE YOUR MOB EVENT.
* USE THE PROTOTYPE.
*/

protected override void Started(EntityUid uid, VentCrittersRuleComponent component, GameRuleComponent gameRule, GameRuleStartedEvent args)
[Dependency] private readonly AntagSelectionSystem _antag = default!;
[Dependency] private readonly EntityTableSystem _entityTable = default!;
[Dependency] private readonly ISharedPlayerManager _player = default!;
[Dependency] private readonly ChatSystem _chatSystem = default!;
[Dependency] private readonly EntityLookupSystem _lookup = default!; // DEN

private List<VentCritterLocationData> _locations = new();
private Entity<TransformComponent> _selectedBeacon = new();

protected override void Added(EntityUid uid, VentCrittersRuleComponent comp, GameRuleComponent gameRule, GameRuleAddedEvent args)
{
base.Started(uid, component, gameRule, args);
PickSpawnLocations();

if (!TryGetRandomStation(out var station))
if (_locations.Count == 0)
{
ForceEndSelf(uid, gameRule);
return;
}

var locations = EntityQueryEnumerator<VentCritterSpawnLocationComponent, TransformComponent>();
var validLocations = new List<EntityCoordinates>();
while (locations.MoveNext(out _, out _, out var transform))
foreach (var location in _locations)
{
_chatSystem.TrySendInGameICMessage(location.LocationUid, "emits an ominous rumbling sound...", Shared.Chat.InGameICChatType.Emote, Shared.Chat.ChatTransmitRange.Normal, false, null, null, "nearby vent", false, true);
}

var audio = AudioParams.Default;
audio.Volume = 200;
audio.RolloffFactor = 0.01f;
audio.ReferenceDistance = 400;

Audio.PlayPvs(comp.Sound, _selectedBeacon.Comp.Coordinates, audio);

base.Added(uid, comp, gameRule, args);
}

protected override void Ended(EntityUid uid, VentCrittersRuleComponent comp, GameRuleComponent gameRule, GameRuleEndedEvent args)
{
if (_locations.Count == 0)
return;

var players = _antag.GetTotalPlayerCount(_player.Sessions);
var min = comp.Min * players / comp.PlayerRatio;
var max = comp.Max * players / comp.PlayerRatio;
var maxSpawns = Math.Max(RobustRandom.Next(min, max), 1);

var spawnsPerVent = Math.Max((maxSpawns / _locations.Count), 1);

foreach (var location in _locations)
{
if (!transform.Anchored)
continue;
_chatSystem.TrySendInGameICMessage(location.LocationUid, "screeches as something bursts free in a cloud of dust!", Shared.Chat.InGameICChatType.Emote, Shared.Chat.ChatTransmitRange.Normal, false, null, null, "nearby vent", false, true);

Spawn("AdminInstantEffectSmoke10", location.Coordinates); // Dust effect

if (CompOrNull<StationMemberComponent>(transform.GridUid)?.Station == station)
SpawnCritters(comp, location, spawnsPerVent);
}

base.Ended(uid, comp, gameRule, args);
}

private void SpawnCritters(VentCrittersRuleComponent comp, VentCritterLocationData vent, int spawnCount)
{
for (int i = 0; i < spawnCount; i++)
{
foreach (var spawn in _entityTable.GetSpawns(comp.Table))
{
validLocations.Add(transform.Coordinates);
foreach (var spawn in EntitySpawnCollection.GetSpawns(component.Entries, RobustRandom))
{
Spawn(spawn, transform.Coordinates);
}
Spawn(spawn, vent.Coordinates);
}
}

if (component.SpecialEntries.Count == 0 || validLocations.Count == 0)
// guaranteed spawn
if (comp.SpecialEntries.Count > 0)
{
return;
var specialEntry = RobustRandom.Pick(comp.SpecialEntries);
Spawn(specialEntry.PrototypeId, vent.Coordinates);
}
}

// guaranteed spawn
var specialEntry = RobustRandom.Pick(component.SpecialEntries);
var specialSpawn = RobustRandom.Pick(validLocations);
Spawn(specialEntry.PrototypeId, specialSpawn);
private void PickSpawnLocations()
{
if (!TryGetRandomStation(out var station))
return;

_locations.Clear();

foreach (var location in validLocations)
// Get all beacons on station
var beacons = EntityQueryEnumerator<NavMapBeaconComponent, TransformComponent>();
var beaconList = new List<Entity<TransformComponent>>();

while (beacons.MoveNext(out var beaconUid, out var navMapBeacon, out var beaconPosition))
{
foreach (var spawn in EntitySpawnCollection.GetSpawns(component.SpecialEntries, RobustRandom))
// Check that the beacon is actually on the station, if so add to the list
if (CompOrNull<StationMemberComponent>(beaconPosition.GridUid)?.Station == station)
{
Spawn(spawn, location);
beaconList.Add((beaconUid, beaconPosition));
}
}
// Grab a random beacon from our list
if (!beaconList.Any())
return;

_selectedBeacon = RobustRandom.Pick(beaconList);

// 10 tile range is purely arbitrary, it would be better to pick vents up to a maximum value instead but
var ventsInRange = _lookup.GetEntitiesInRange<VentCritterSpawnLocationComponent>(_selectedBeacon.Comp.Coordinates, 10).Where(x => x.Comp.CanSpawn);

foreach (var vent in ventsInRange)
{
_locations.Add(new VentCritterLocationData(vent.Owner, vent.Comp, Transform(vent.Owner).Coordinates));
}
}
}

/// <summary>
/// Contains location data for the vents that have been selected to spawn critters
/// </summary>
public sealed class VentCritterLocationData
{
public EntityUid LocationUid;
public VentCritterSpawnLocationComponent SpawnLocationComponent;
public EntityCoordinates Coordinates;

public VentCritterLocationData(EntityUid uid, VentCritterSpawnLocationComponent spawnComp, EntityCoordinates coords)
{
LocationUid = uid;
SpawnLocationComponent = spawnComp;
Coordinates = coords;
}
}
25 changes: 25 additions & 0 deletions Resources/Audio/_DEN/VentCritters/attributions.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# SPDX-FileCopyrightText: 2025 Jakumba
#
# SPDX-License-Identifier: AGPL-3.0-or-later

- files: ["vent_xeno.ogg"]
license: "CC-BY-SA-3.0"
copyright: "Edited and composited by MajorMoth, samples taken from freesound.org at ID's 800277, 803044, 795432, 806647, 806649"
source: "https://freesound.org"

- files: ["vent_carp.ogg"]
license: "CC-BY-SA-3.0"
copyright: "Edited and composited by MajorMoth, modified bite.ogg and metal_break4.ogg"
source: "https://github.com/TheDenSS14/TheDen"

- files: ["vent_harmless_critter.ogg"]
license: "CC-BY-SA-3.0"
copyright: "Edited and composited by MajorMoth, samples taken from freesound.org at ID's 244015, 528191"
source: "https://freesound.org"

- files: ["vent_slime.ogg"]
license: "CC-BY-SA-3.0"
copyright: "Edited and composited by MajorMoth, samples taken from https://www.youtube.com/watch?v=hX5S4KC0hx0, https://www.youtube.com/watch?v=REOeff9WJ2o,
https://www.youtube.com/watch?v=9XIDm_sZhx4,
https://www.youtube.com/watch?v=jiTV8MwgL3c"
source: "https://www.youtube.com"
Binary file added Resources/Audio/_DEN/VentCritters/vent_carp.ogg
Binary file not shown.
Binary file not shown.
Binary file added Resources/Audio/_DEN/VentCritters/vent_slime.ogg
Binary file not shown.
Binary file added Resources/Audio/_DEN/VentCritters/vent_xeno.ogg
Binary file not shown.
Loading