diff --git a/Content.Server/StationEvents/BasicStationEventSchedulerSystem.cs b/Content.Server/StationEvents/BasicStationEventSchedulerSystem.cs
index a6b64af00d..0304978ed6 100644
--- a/Content.Server/StationEvents/BasicStationEventSchedulerSystem.cs
+++ b/Content.Server/StationEvents/BasicStationEventSchedulerSystem.cs
@@ -20,77 +20,30 @@ namespace Content.Server.StationEvents
{
public override string Prototype => "BasicStationEventScheduler";
- [Dependency] private readonly IConfigurationManager _configurationManager = default!;
- [Dependency] private readonly IPlayerManager _playerManager = default!;
[Dependency] private readonly IRobustRandom _random = default!;
- [Dependency] private readonly IPrototypeManager _prototype = default!;
+ [Dependency] private readonly EventManagerSystem _event = default!;
private const float MinimumTimeUntilFirstEvent = 300;
- private ISawmill _sawmill = default!;
///
/// How long until the next check for an event runs
///
/// Default value is how long until first event is allowed
+ [ViewVariables(VVAccess.ReadWrite)]
private float _timeUntilNextEvent = MinimumTimeUntilFirstEvent;
- public override void Initialize()
- {
- base.Initialize();
-
- _sawmill = Logger.GetSawmill("basicevents");
-
- // Can't just check debug / release for a default given mappers need to use release mode
- // As such we'll always pause it by default.
- _configurationManager.OnValueChanged(CCVars.EventsEnabled, SetEnabled, true);
-
- SubscribeLocalEvent(Reset);
- }
-
- public override void Shutdown()
- {
- base.Shutdown();
- _configurationManager.UnsubValueChanged(CCVars.EventsEnabled, SetEnabled);
- }
-
- public bool EventsEnabled { get; private set; }
- private void SetEnabled(bool value) => EventsEnabled = value;
-
public override void Started() { }
- public override void Ended() { }
- ///
- /// Randomly run a valid event immediately, ignoring earlieststart or whether the event is enabled
- ///
- ///
- public string RunRandomEvent()
+ public override void Ended()
{
- var randomEvent = PickRandomEvent();
-
- if (randomEvent == null
- || !_prototype.TryIndex(randomEvent.Id, out var proto))
- {
- return Loc.GetString("station-event-system-run-random-event-no-valid-events");
- }
-
- GameTicker.AddGameRule(proto);
- return Loc.GetString("station-event-system-run-event",("eventName", randomEvent.Id));
- }
-
- ///
- /// Randomly picks a valid event.
- ///
- public StationEventRuleConfiguration? PickRandomEvent()
- {
- var availableEvents = AvailableEvents(true);
- return FindEvent(availableEvents);
+ _timeUntilNextEvent = MinimumTimeUntilFirstEvent;
}
public override void Update(float frameTime)
{
base.Update(frameTime);
- if (!RuleStarted || !EventsEnabled)
+ if (!RuleStarted || !_event.EventsEnabled)
return;
if (_timeUntilNextEvent > 0)
@@ -99,17 +52,8 @@ namespace Content.Server.StationEvents
return;
}
- // No point hammering this trying to find events if none are available
- var stationEvent = FindEvent(AvailableEvents());
- if (stationEvent == null
- || !_prototype.TryIndex(stationEvent.Id, out var proto))
- {
- return;
- }
-
- GameTicker.AddGameRule(proto);
+ _event.RunRandomEvent();
ResetTimer();
- _sawmill.Info($"Started event {proto.ID}. Next event in {_timeUntilNextEvent} seconds");
}
///
@@ -120,132 +64,5 @@ namespace Content.Server.StationEvents
// 5 - 25 minutes. TG does 3-10 but that's pretty frequent
_timeUntilNextEvent = _random.Next(300, 1500);
}
-
- ///
- /// Pick a random event from the available events at this time, also considering their weightings.
- ///
- ///
- private StationEventRuleConfiguration? FindEvent(List availableEvents)
- {
- if (availableEvents.Count == 0)
- {
- return null;
- }
-
- var sumOfWeights = 0;
-
- foreach (var stationEvent in availableEvents)
- {
- sumOfWeights += (int) stationEvent.Weight;
- }
-
- sumOfWeights = _random.Next(sumOfWeights);
-
- foreach (var stationEvent in availableEvents)
- {
- sumOfWeights -= (int) stationEvent.Weight;
-
- if (sumOfWeights <= 0)
- {
- return stationEvent;
- }
- }
-
- return null;
- }
-
- ///
- /// Gets the events that have met their player count, time-until start, etc.
- ///
- ///
- ///
- private List AvailableEvents(bool ignoreEarliestStart = false)
- {
- TimeSpan currentTime;
- var playerCount = _playerManager.PlayerCount;
-
- // playerCount does a lock so we'll just keep the variable here
- if (!ignoreEarliestStart)
- {
- currentTime = GameTicker.RoundDuration();
- }
- else
- {
- currentTime = TimeSpan.Zero;
- }
-
- var result = new List();
-
- foreach (var stationEvent in AllEvents())
- {
- if (CanRun(stationEvent, playerCount, currentTime))
- {
- result.Add(stationEvent);
- }
- }
-
- return result;
- }
-
- private IEnumerable AllEvents()
- {
- return _prototype.EnumeratePrototypes()
- .Where(p => p.Configuration is StationEventRuleConfiguration)
- .Select(p => (StationEventRuleConfiguration) p.Configuration);
- }
-
- private int GetOccurrences(StationEventRuleConfiguration stationEvent)
- {
- return GameTicker.AllPreviousGameRules.Count(p => p.Item2.ID == stationEvent.Id);
- }
-
- public TimeSpan TimeSinceLastEvent(StationEventRuleConfiguration? stationEvent)
- {
- foreach (var (time, rule) in GameTicker.AllPreviousGameRules.Reverse())
- {
- if (rule.Configuration is not StationEventRuleConfiguration)
- continue;
-
- if (stationEvent == null || rule.ID == stationEvent.Id)
- return time;
- }
-
- return TimeSpan.Zero;
- }
-
- private bool CanRun(StationEventRuleConfiguration stationEvent, int playerCount, TimeSpan currentTime)
- {
- if (GameTicker.IsGameRuleStarted(stationEvent.Id))
- return false;
-
- if (stationEvent.MaxOccurrences.HasValue && GetOccurrences(stationEvent) >= stationEvent.MaxOccurrences.Value)
- {
- return false;
- }
-
- if (playerCount < stationEvent.MinimumPlayers)
- {
- return false;
- }
-
- if (currentTime != TimeSpan.Zero && currentTime.TotalMinutes < stationEvent.EarliestStart)
- {
- return false;
- }
-
- var lastRun = TimeSinceLastEvent(stationEvent);
- if (lastRun != TimeSpan.Zero && currentTime.TotalMinutes <
- stationEvent.ReoccurrenceDelay + lastRun.TotalMinutes)
- {
- return false;
- }
-
- return true;
- }
-
- private void Reset(RoundRestartCleanupEvent ev)
- {
- _timeUntilNextEvent = MinimumTimeUntilFirstEvent;
- }
}
}
diff --git a/Content.Server/StationEvents/EventManagerSystem.cs b/Content.Server/StationEvents/EventManagerSystem.cs
new file mode 100644
index 0000000000..6a2b646602
--- /dev/null
+++ b/Content.Server/StationEvents/EventManagerSystem.cs
@@ -0,0 +1,197 @@
+using System.Linq;
+using Content.Server.GameTicking;
+using Content.Server.GameTicking.Rules;
+using Content.Server.GameTicking.Rules.Configurations;
+using Content.Shared.CCVar;
+using Content.Shared.GameTicking;
+using Robust.Server.Player;
+using Robust.Shared.Configuration;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Random;
+
+namespace Content.Server.StationEvents;
+
+public sealed class EventManagerSystem : EntitySystem
+{
+ [Dependency] private readonly IConfigurationManager _configurationManager = default!;
+ [Dependency] private readonly IPlayerManager _playerManager = default!;
+ [Dependency] private readonly IRobustRandom _random = default!;
+ [Dependency] private readonly IPrototypeManager _prototype = default!;
+ [Dependency] public readonly GameTicker GameTicker = default!;
+
+ private ISawmill _sawmill = default!;
+
+ public bool EventsEnabled { get; private set; }
+ private void SetEnabled(bool value) => EventsEnabled = value;
+
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ _sawmill = Logger.GetSawmill("events");
+
+ _configurationManager.OnValueChanged(CCVars.EventsEnabled, SetEnabled, true);
+ }
+
+ public override void Shutdown()
+ {
+ base.Shutdown();
+ _configurationManager.UnsubValueChanged(CCVars.EventsEnabled, SetEnabled);
+ }
+
+ ///
+ /// Randomly runs a valid event.
+ ///
+ public string RunRandomEvent()
+ {
+ var randomEvent = PickRandomEvent();
+
+ if (randomEvent == null
+ || !_prototype.TryIndex(randomEvent.Id, out var proto))
+ {
+ var errStr = Loc.GetString("station-event-system-run-random-event-no-valid-events");
+ _sawmill.Error(errStr);
+ return errStr;
+ }
+
+ GameTicker.AddGameRule(proto);
+ var str = Loc.GetString("station-event-system-run-event",("eventName", randomEvent.Id));
+ _sawmill.Info(str);
+ return str;
+ }
+
+ ///
+ /// Randomly picks a valid event.
+ ///
+ public StationEventRuleConfiguration? PickRandomEvent()
+ {
+ var availableEvents = AvailableEvents();
+ _sawmill.Info($"Picking from {availableEvents.Count} total available events");
+ return FindEvent(availableEvents);
+ }
+
+ ///
+ /// Pick a random event from the available events at this time, also considering their weightings.
+ ///
+ ///
+ private StationEventRuleConfiguration? FindEvent(List availableEvents)
+ {
+ if (availableEvents.Count == 0)
+ {
+ _sawmill.Warning("No events were available to run!");
+ return null;
+ }
+
+ var sumOfWeights = 0;
+
+ foreach (var stationEvent in availableEvents)
+ {
+ sumOfWeights += (int) stationEvent.Weight;
+ }
+
+ sumOfWeights = _random.Next(sumOfWeights);
+
+ foreach (var stationEvent in availableEvents)
+ {
+ sumOfWeights -= (int) stationEvent.Weight;
+
+ if (sumOfWeights <= 0)
+ {
+ return stationEvent;
+ }
+ }
+
+ _sawmill.Error("Event was not found after weighted pick process!");
+ return null;
+ }
+
+ ///
+ /// Gets the events that have met their player count, time-until start, etc.
+ ///
+ ///
+ ///
+ private List AvailableEvents(bool ignoreEarliestStart = false)
+ {
+ TimeSpan currentTime;
+ var playerCount = _playerManager.PlayerCount;
+
+ // playerCount does a lock so we'll just keep the variable here
+ if (!ignoreEarliestStart)
+ {
+ currentTime = GameTicker.RoundDuration();
+ }
+ else
+ {
+ currentTime = TimeSpan.Zero;
+ }
+
+ var result = new List();
+
+ foreach (var stationEvent in AllEvents())
+ {
+ if (CanRun(stationEvent, playerCount, currentTime))
+ {
+ _sawmill.Debug($"Adding event {stationEvent.Id} to possibilities");
+ result.Add(stationEvent);
+ }
+ }
+
+ return result;
+ }
+
+ private IEnumerable AllEvents()
+ {
+ return _prototype.EnumeratePrototypes()
+ .Where(p => p.Configuration is StationEventRuleConfiguration)
+ .Select(p => (StationEventRuleConfiguration) p.Configuration);
+ }
+
+ private int GetOccurrences(StationEventRuleConfiguration stationEvent)
+ {
+ return GameTicker.AllPreviousGameRules.Count(p => p.Item2.ID == stationEvent.Id);
+ }
+
+ public TimeSpan TimeSinceLastEvent(StationEventRuleConfiguration? stationEvent)
+ {
+ foreach (var (time, rule) in GameTicker.AllPreviousGameRules.Reverse())
+ {
+ if (rule.Configuration is not StationEventRuleConfiguration)
+ continue;
+
+ if (stationEvent == null || rule.ID == stationEvent.Id)
+ return time;
+ }
+
+ return TimeSpan.Zero;
+ }
+
+ private bool CanRun(StationEventRuleConfiguration stationEvent, int playerCount, TimeSpan currentTime)
+ {
+ if (GameTicker.IsGameRuleStarted(stationEvent.Id))
+ return false;
+
+ if (stationEvent.MaxOccurrences.HasValue && GetOccurrences(stationEvent) >= stationEvent.MaxOccurrences.Value)
+ {
+ return false;
+ }
+
+ if (playerCount < stationEvent.MinimumPlayers)
+ {
+ return false;
+ }
+
+ if (currentTime != TimeSpan.Zero && currentTime.TotalMinutes < stationEvent.EarliestStart)
+ {
+ return false;
+ }
+
+ var lastRun = TimeSinceLastEvent(stationEvent);
+ if (lastRun != TimeSpan.Zero && currentTime.TotalMinutes <
+ stationEvent.ReoccurrenceDelay + lastRun.TotalMinutes)
+ {
+ return false;
+ }
+
+ return true;
+ }
+}
diff --git a/Content.Server/StationEvents/Events/BureaucraticError.cs b/Content.Server/StationEvents/Events/BureaucraticError.cs
index d765d387b4..3343fe83e0 100644
--- a/Content.Server/StationEvents/Events/BureaucraticError.cs
+++ b/Content.Server/StationEvents/Events/BureaucraticError.cs
@@ -16,13 +16,16 @@ public sealed class BureaucraticError : StationEventSystem
{
base.Started();
- if (StationSystem.Stations.Count == 0) return; // No stations
+ if (StationSystem.Stations.Count == 0)
+ return; // No stations
var chosenStation = RobustRandom.Pick(StationSystem.Stations.ToList());
var jobList = _stationJobs.GetJobs(chosenStation).Keys.ToList();
+ var mod = GetSeverityModifier();
+
// Low chance to completely change up the late-join landscape by closing all positions except infinite slots.
// Lower chance than the /tg/ equivalent of this event.
- if (RobustRandom.Prob(0.25f))
+ if (RobustRandom.Prob(Math.Min(0.25f * MathF.Sqrt(mod), 1.0f)))
{
var chosenJob = RobustRandom.PickAndTake(jobList);
_stationJobs.MakeJobUnlimited(chosenStation, chosenJob); // INFINITE chaos.
@@ -35,8 +38,10 @@ public sealed class BureaucraticError : StationEventSystem
}
else
{
+ var lower = (int) (jobList.Count * Math.Min(1.0f, 0.20 * mod));
+ var upper = (int) (jobList.Count * Math.Min(1.0f, 0.30 * mod));
// Changing every role is maybe a bit too chaotic so instead change 20-30% of them.
- for (var i = 0; i < RobustRandom.Next((int)(jobList.Count * 0.20), (int)(jobList.Count * 0.30)); i++)
+ for (var i = 0; i < RobustRandom.Next(lower, upper); i++)
{
var chosenJob = RobustRandom.PickAndTake(jobList);
if (_stationJobs.IsJobUnlimited(chosenStation, chosenJob))
diff --git a/Content.Server/StationEvents/Events/GasLeak.cs b/Content.Server/StationEvents/Events/GasLeak.cs
index 5469407896..016e145c8b 100644
--- a/Content.Server/StationEvents/Events/GasLeak.cs
+++ b/Content.Server/StationEvents/Events/GasLeak.cs
@@ -59,6 +59,8 @@ namespace Content.Server.StationEvents.Events
{
base.Started();
+ var mod = MathF.Sqrt(GetSeverityModifier());
+
// Essentially we'll pick out a target amount of gas to leak, then a rate to leak it at, then work out the duration from there.
if (TryFindRandomTile(out _targetTile, out _targetStation, out _targetGrid, out _targetCoords))
{
@@ -66,7 +68,7 @@ namespace Content.Server.StationEvents.Events
_leakGas = RobustRandom.Pick(LeakableGases);
// Was 50-50 on using normal distribution.
- var totalGas = (float) RobustRandom.Next(MinimumGas, MaximumGas);
+ var totalGas = RobustRandom.Next(MinimumGas, MaximumGas) * mod;
var startAfter = ((StationEventRuleConfiguration) Configuration).StartAfter;
_molesPerSecond = RobustRandom.Next(MinimumMolesPerSecond, MaximumMolesPerSecond);
_endAfter = totalGas / _molesPerSecond + startAfter;
diff --git a/Content.Server/StationEvents/Events/MeteorSwarm.cs b/Content.Server/StationEvents/Events/MeteorSwarm.cs
index 525dff4dc8..e11c4ae778 100644
--- a/Content.Server/StationEvents/Events/MeteorSwarm.cs
+++ b/Content.Server/StationEvents/Events/MeteorSwarm.cs
@@ -30,7 +30,8 @@ namespace Content.Server.StationEvents.Events
public override void Started()
{
base.Started();
- _waveCounter = RobustRandom.Next(MinimumWaves, MaximumWaves);
+ var mod = Math.Sqrt(GetSeverityModifier());
+ _waveCounter = (int) (RobustRandom.Next(MinimumWaves, MaximumWaves) * mod);
}
public override void Ended()
@@ -53,13 +54,16 @@ namespace Content.Server.StationEvents.Events
return;
}
+ var mod = GetSeverityModifier();
+
_cooldown -= frameTime;
- if (_cooldown > 0f) return;
+ if (_cooldown > 0f)
+ return;
_waveCounter--;
- _cooldown += (MaximumCooldown - MinimumCooldown) * RobustRandom.NextFloat() + MinimumCooldown;
+ _cooldown += (MaximumCooldown - MinimumCooldown) * RobustRandom.NextFloat() / mod + MinimumCooldown;
Box2? playableArea = null;
var mapId = GameTicker.DefaultMap;
diff --git a/Content.Server/StationEvents/Events/MouseMigration.cs b/Content.Server/StationEvents/Events/MouseMigration.cs
index b6c4405065..e2f2a5b9b0 100644
--- a/Content.Server/StationEvents/Events/MouseMigration.cs
+++ b/Content.Server/StationEvents/Events/MouseMigration.cs
@@ -15,15 +15,18 @@ public sealed class MouseMigration : StationEventSystem
{
base.Started();
+ var modifier = GetSeverityModifier();
+
var spawnLocations = EntityManager.EntityQuery().ToList();
RobustRandom.Shuffle(spawnLocations);
- var spawnAmount = RobustRandom.Next(7, 15); // A small colony of critters.
+ // sqrt so we dont get insane values for ramping events
+ var spawnAmount = (int) (RobustRandom.Next(7, 15) * Math.Sqrt(modifier)); // A small colony of critters.
for (int i = 0; i < spawnAmount && i < spawnLocations.Count - 1; i++)
{
var spawnChoice = RobustRandom.Pick(SpawnedPrototypeChoices);
- if (RobustRandom.Prob(0.01f) || i == 0) //small chance for multiple, but always at least 1
+ if (RobustRandom.Prob(Math.Min(0.01f * modifier, 1.0f)) || i == 0) //small chance for multiple, but always at least 1
spawnChoice = "SpawnPointGhostRatKing";
var coords = spawnLocations[i].Item2.Coordinates;
diff --git a/Content.Server/StationEvents/Events/RandomSentience.cs b/Content.Server/StationEvents/Events/RandomSentience.cs
index e2c30a5933..275bf6097a 100644
--- a/Content.Server/StationEvents/Events/RandomSentience.cs
+++ b/Content.Server/StationEvents/Events/RandomSentience.cs
@@ -18,10 +18,11 @@ public sealed class RandomSentience : StationEventSystem
base.Started();
HashSet stationsToNotify = new();
+ var mod = GetSeverityModifier();
var targetList = EntityManager.EntityQuery().ToList();
RobustRandom.Shuffle(targetList);
- var toMakeSentient = RobustRandom.Next(2, 5);
+ var toMakeSentient = (int) (RobustRandom.Next(2, 5) * Math.Sqrt(mod));
var groups = new HashSet();
foreach (var target in targetList)
diff --git a/Content.Server/StationEvents/Events/StationEventSystem.cs b/Content.Server/StationEvents/Events/StationEventSystem.cs
index 7f8c5a3460..46eed70731 100644
--- a/Content.Server/StationEvents/Events/StationEventSystem.cs
+++ b/Content.Server/StationEvents/Events/StationEventSystem.cs
@@ -179,6 +179,26 @@ namespace Content.Server.StationEvents.Events
.Where(p => p.Configuration is StationEventRuleConfiguration).ToArray());
}
+ public float GetSeverityModifier()
+ {
+ var ev = new GetSeverityModifierEvent();
+ RaiseLocalEvent(ev);
+ return ev.Modifier;
+ }
+
#endregion
}
+
+ ///
+ /// Raised broadcast to determine what the severity modifier should be for an event, some positive number that can be multiplied with various things.
+ /// Handled by usually other game rules (like the ramping scheduler).
+ /// Most events should try and make use of this if possible.
+ ///
+ public sealed class GetSeverityModifierEvent : EntityEventArgs
+ {
+ ///
+ /// Should be multiplied/added to rather than set, for commutativity.
+ ///
+ public float Modifier = 1.0f;
+ }
}
diff --git a/Content.Server/StationEvents/Events/VentClog.cs b/Content.Server/StationEvents/Events/VentClog.cs
index ba1aca3fde..5a1e80ca56 100644
--- a/Content.Server/StationEvents/Events/VentClog.cs
+++ b/Content.Server/StationEvents/Events/VentClog.cs
@@ -31,15 +31,16 @@ public sealed class VentClog : StationEventSystem
// This is gross, but not much can be done until event refactor, which needs Dynamic.
var sound = new SoundPathSpecifier("/Audio/Effects/extinguish.ogg");
+ var mod = (float) Math.Sqrt(GetSeverityModifier());
foreach (var (_, transform) in EntityManager.EntityQuery())
{
var solution = new Solution();
- if (!RobustRandom.Prob(0.33f))
+ if (!RobustRandom.Prob(Math.Min(0.33f * mod, 1.0f)))
continue;
- if (RobustRandom.Prob(0.05f))
+ if (RobustRandom.Prob(Math.Min(0.05f * mod, 1.0f)))
{
solution.AddReagent(RobustRandom.Pick(allReagents), 100);
}
@@ -48,7 +49,7 @@ public sealed class VentClog : StationEventSystem
solution.AddReagent(RobustRandom.Pick(SafeishVentChemicals), 100);
}
- FoamAreaReactionEffect.SpawnFoam("Foam", transform.Coordinates, solution, RobustRandom.Next(2, 6), 20, 1,
+ FoamAreaReactionEffect.SpawnFoam("Foam", transform.Coordinates, solution, (int) (RobustRandom.Next(2, 6) * mod), 20, 1,
1, sound, EntityManager);
}
}
diff --git a/Content.Server/StationEvents/Events/VentCritters.cs b/Content.Server/StationEvents/Events/VentCritters.cs
index 4054ff4db4..d1569b07f1 100644
--- a/Content.Server/StationEvents/Events/VentCritters.cs
+++ b/Content.Server/StationEvents/Events/VentCritters.cs
@@ -18,7 +18,9 @@ public sealed class VentCritters : StationEventSystem
var spawnLocations = EntityManager.EntityQuery().ToList();
RobustRandom.Shuffle(spawnLocations);
- var spawnAmount = RobustRandom.Next(4, 12); // A small colony of critters.
+ var mod = Math.Sqrt(GetSeverityModifier());
+
+ var spawnAmount = (int) (RobustRandom.Next(4, 12) * mod); // A small colony of critters.
Sawmill.Info($"Spawning {spawnAmount} of {spawnChoice}");
foreach (var location in spawnLocations)
{
diff --git a/Content.Server/StationEvents/RampingStationEventSchedulerSystem.cs b/Content.Server/StationEvents/RampingStationEventSchedulerSystem.cs
new file mode 100644
index 0000000000..d22e2d86dc
--- /dev/null
+++ b/Content.Server/StationEvents/RampingStationEventSchedulerSystem.cs
@@ -0,0 +1,104 @@
+using Content.Server.GameTicking;
+using Content.Server.GameTicking.Rules;
+using Content.Server.StationEvents.Events;
+using Content.Shared.CCVar;
+using Robust.Shared.Configuration;
+using Robust.Shared.Random;
+
+namespace Content.Server.StationEvents;
+
+public sealed class RampingStationEventSchedulerSystem : GameRuleSystem
+{
+ public override string Prototype => "RampingStationEventScheduler";
+
+ [Dependency] private readonly IConfigurationManager _cfg = default!;
+ [Dependency] private readonly IRobustRandom _random = default!;
+ [Dependency] private readonly EventManagerSystem _event = default!;
+ [Dependency] private readonly GameTicker _gameTicker = default!;
+
+ [ViewVariables(VVAccess.ReadWrite)]
+ private float _endTime;
+ [ViewVariables(VVAccess.ReadWrite)]
+ private float _maxChaos;
+ [ViewVariables(VVAccess.ReadWrite)]
+ private float _startingChaos;
+ [ViewVariables(VVAccess.ReadWrite)]
+ private float _timeUntilNextEvent;
+
+ [ViewVariables]
+ public float ChaosModifier
+ {
+ get
+ {
+ var roundTime = (float) _gameTicker.RoundDuration().TotalSeconds;
+ if (roundTime > _endTime)
+ return _maxChaos;
+
+ return (_maxChaos / _endTime) * roundTime + _startingChaos;
+ }
+ }
+
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ SubscribeLocalEvent(OnGetSeverityModifier);
+ }
+
+ public override void Started()
+ {
+ var avgChaos = _cfg.GetCVar(CCVars.EventsRampingAverageChaos);
+ var avgTime = _cfg.GetCVar(CCVars.EventsRampingAverageEndTime);
+
+ // Worlds shittiest probability distribution
+ // Got a complaint? Send them to
+ _maxChaos = _random.NextFloat(avgChaos - avgChaos / 4, avgChaos + avgChaos / 4);
+ // This is in minutes, so *60 for seconds (for the chaos calc)
+ _endTime = _random.NextFloat(avgTime - avgTime / 4, avgTime + avgTime / 4) * 60f;
+ _startingChaos = _maxChaos / 10;
+
+ PickNextEventTime();
+ }
+
+ public override void Ended()
+ {
+ _endTime = 0f;
+ _maxChaos = 0f;
+ _startingChaos = 0f;
+ _timeUntilNextEvent = 0f;
+ }
+
+ public override void Update(float frameTime)
+ {
+ base.Update(frameTime);
+
+ if (!RuleStarted || !_event.EventsEnabled)
+ return;
+
+ if (_timeUntilNextEvent > 0f)
+ {
+ _timeUntilNextEvent -= frameTime;
+ return;
+ }
+
+ PickNextEventTime();
+ _event.RunRandomEvent();
+ }
+
+ private void OnGetSeverityModifier(GetSeverityModifierEvent ev)
+ {
+ if (!RuleStarted)
+ return;
+
+ ev.Modifier *= ChaosModifier;
+ Logger.Info($"Ramping set modifier to {ev.Modifier}");
+ }
+
+ private void PickNextEventTime()
+ {
+ var mod = ChaosModifier;
+
+ // 4-12 minutes baseline. Will get faster over time as the chaos mod increases.
+ _timeUntilNextEvent = _random.NextFloat(240f / mod, 720f / mod);
+ }
+}
diff --git a/Content.Shared/CCVar/CCVars.cs b/Content.Shared/CCVar/CCVars.cs
index 4bbaf779da..a83eb7b422 100644
--- a/Content.Shared/CCVar/CCVars.cs
+++ b/Content.Shared/CCVar/CCVars.cs
@@ -96,9 +96,8 @@ namespace Content.Shared.CCVar
public static readonly CVarDef StatusMoMMIPassword =
CVarDef.Create("status.mommipassword", "", CVar.SERVERONLY | CVar.CONFIDENTIAL);
-
/*
- * Game
+ * Events
*/
///
@@ -107,6 +106,24 @@ namespace Content.Shared.CCVar
public static readonly CVarDef
EventsEnabled = CVarDef.Create("events.enabled", true, CVar.ARCHIVE | CVar.SERVERONLY);
+ ///
+ /// Average time (in minutes) for when the ramping event scheduler should stop increasing the chaos modifier.
+ /// Close to how long you expect a round to last, so you'll probably have to tweak this on downstreams.
+ ///
+ public static readonly CVarDef
+ EventsRampingAverageEndTime = CVarDef.Create("events.ramping_average_end_time", 40f, CVar.ARCHIVE | CVar.SERVERONLY);
+
+ ///
+ /// Average ending chaos modifier for the ramping event scheduler.
+ /// Max chaos chosen for a round will deviate from this
+ ///
+ public static readonly CVarDef
+ EventsRampingAverageChaos = CVarDef.Create("events.ramping_average_chaos", 6f, CVar.ARCHIVE | CVar.SERVERONLY);
+
+ /*
+ * Game
+ */
+
///
/// Disables most functionality in the GameTicker.
///
diff --git a/Resources/Locale/en-US/game-ticking/game-presets/preset-extended.ftl b/Resources/Locale/en-US/game-ticking/game-presets/preset-extended.ftl
index 56223aef40..a8c7f332ea 100644
--- a/Resources/Locale/en-US/game-ticking/game-presets/preset-extended.ftl
+++ b/Resources/Locale/en-US/game-ticking/game-presets/preset-extended.ftl
@@ -1,2 +1,2 @@
extended-title = Extended
-extended-description = No antagonists, have fun!
+extended-description = A calm experience. Admin intervention required.
diff --git a/Resources/Locale/en-US/game-ticking/game-presets/preset-survival.ftl b/Resources/Locale/en-US/game-ticking/game-presets/preset-survival.ftl
new file mode 100644
index 0000000000..231733eabf
--- /dev/null
+++ b/Resources/Locale/en-US/game-ticking/game-presets/preset-survival.ftl
@@ -0,0 +1,2 @@
+survival-title = Survival
+survival-description = No internal threats, but how long can the station survive increasingly chaotic and frequent events?
diff --git a/Resources/Locale/en-US/station-events/station-event-system.ftl b/Resources/Locale/en-US/station-events/station-event-system.ftl
index 2e7843326e..0a117fbdd6 100644
--- a/Resources/Locale/en-US/station-events/station-event-system.ftl
+++ b/Resources/Locale/en-US/station-events/station-event-system.ftl
@@ -1,4 +1,4 @@
## BasicStationEventSchedulerSystem
station-event-system-run-event = Running event {$eventName}
-station-event-system-run-random-event-no-valid-events = No valid events available
+station-event-system-run-random-event-no-valid-events = No valid event was given
diff --git a/Resources/Prototypes/GameRules/roundstart.yml b/Resources/Prototypes/GameRules/roundstart.yml
index b7f49e5421..5a62ca2706 100644
--- a/Resources/Prototypes/GameRules/roundstart.yml
+++ b/Resources/Prototypes/GameRules/roundstart.yml
@@ -72,3 +72,9 @@
config:
!type:GenericGameRuleConfiguration
id: BasicStationEventScheduler
+
+- type: gameRule
+ id: RampingStationEventScheduler
+ config:
+ !type:GenericGameRuleConfiguration
+ id: RampingStationEventScheduler
diff --git a/Resources/Prototypes/game_presets.yml b/Resources/Prototypes/game_presets.yml
index afc0369517..f56452f20b 100644
--- a/Resources/Prototypes/game_presets.yml
+++ b/Resources/Prototypes/game_presets.yml
@@ -1,13 +1,23 @@
+- type: gamePreset
+ id: Survival
+ alias:
+ - survival
+ name: survival-title
+ showInVote: false # secret
+ description: survival-description
+ rules:
+ - RampingStationEventScheduler
+
- type: gamePreset
id: Extended
alias:
- - extended
- - shittersafari
+ - extended
+ - shittersafari
name: extended-title
showInVote: false #2boring2vote
description: extended-description
rules:
- - BasicStationEventScheduler
+ - BasicStationEventScheduler
- type: gamePreset
id: Secret
diff --git a/Resources/Prototypes/round_announcements.yml b/Resources/Prototypes/round_announcements.yml
index 6c3d5d3a27..0496bb4b22 100644
--- a/Resources/Prototypes/round_announcements.yml
+++ b/Resources/Prototypes/round_announcements.yml
@@ -2,7 +2,7 @@
id: Welcome
sound: /Audio/Announcements/welcome.ogg
presets:
- - Extended
+ - Survival
- Sandbox
- Secret
- Traitor
diff --git a/Resources/Prototypes/secret_weights.yml b/Resources/Prototypes/secret_weights.yml
index fb64d74325..9c392a1549 100644
--- a/Resources/Prototypes/secret_weights.yml
+++ b/Resources/Prototypes/secret_weights.yml
@@ -1,7 +1,7 @@
- type: weightedRandom
id: Secret
weights:
- Extended: 0.25
+ Survival: 0.25
Nukeops: 0.25
Traitor: 0.75
Zombie: 0.05