diff --git a/Content.IntegrationTests/Tests/GameRules/RuleMaxTimeRestartTest.cs b/Content.IntegrationTests/Tests/GameRules/RuleMaxTimeRestartTest.cs index 4708bd10c08a..c805d04a7124 100644 --- a/Content.IntegrationTests/Tests/GameRules/RuleMaxTimeRestartTest.cs +++ b/Content.IntegrationTests/Tests/GameRules/RuleMaxTimeRestartTest.cs @@ -1,10 +1,7 @@ -using Content.Server.GameTicking; -using Content.Server.GameTicking.Commands; +using Content.Server.GameTicking; using Content.Server.GameTicking.Rules; using Content.Server.GameTicking.Rules.Components; -using Content.Shared.CCVar; using Content.Shared.GameTicking.Components; -using Robust.Shared.Configuration; using Robust.Shared.GameObjects; using Robust.Shared.Timing; @@ -27,8 +24,12 @@ public async Task RestartTest() var sGameTicker = server.ResolveDependency().GetEntitySystem(); var sGameTiming = server.ResolveDependency(); - sGameTicker.StartGameRule("MaxTimeRestart", out var ruleEntity); - Assert.That(entityManager.TryGetComponent(ruleEntity, out var maxTime)); + MaxTimeRestartRuleComponent maxTime = null; + await server.WaitPost(() => + { + sGameTicker.StartGameRule("MaxTimeRestart", out var ruleEntity); + Assert.That(entityManager.TryGetComponent(ruleEntity, out maxTime)); + }); Assert.That(server.EntMan.Count(), Is.EqualTo(1)); Assert.That(server.EntMan.Count(), Is.EqualTo(1)); diff --git a/Content.IntegrationTests/Tests/GameRules/TraitorRuleTest.cs b/Content.IntegrationTests/Tests/GameRules/TraitorRuleTest.cs index 31d33ba61746..d2717521b235 100644 --- a/Content.IntegrationTests/Tests/GameRules/TraitorRuleTest.cs +++ b/Content.IntegrationTests/Tests/GameRules/TraitorRuleTest.cs @@ -77,16 +77,17 @@ await server.WaitAssertion(() => await pair.SetAntagPreference(TraitorAntagRoleName, true); // Add the game rule - var gameRuleEnt = ticker.AddGameRule(TraitorGameRuleProtoId); - Assert.That(entMan.TryGetComponent(gameRuleEnt, out var traitorRule)); - - // Ready up - ticker.ToggleReadyAll(true); - Assert.That(ticker.PlayerGameStatuses.Values.All(x => x == PlayerGameStatus.ReadyToPlay)); - - // Start the round + TraitorRuleComponent traitorRule = null; await server.WaitPost(() => { + var gameRuleEnt = ticker.AddGameRule(TraitorGameRuleProtoId); + Assert.That(entMan.TryGetComponent(gameRuleEnt, out traitorRule)); + + // Ready up + ticker.ToggleReadyAll(true); + Assert.That(ticker.PlayerGameStatuses.Values.All(x => x == PlayerGameStatus.ReadyToPlay)); + + // Start the round ticker.StartRound(); // Force traitor mode to start (skip the delay) ticker.StartGameRule(gameRuleEnt); diff --git a/Content.Server/GameTicking/GameTicker.GameRule.cs b/Content.Server/GameTicking/GameTicker.GameRule.cs index c10f3023471e..cf0b0eceb11a 100644 --- a/Content.Server/GameTicking/GameTicker.GameRule.cs +++ b/Content.Server/GameTicking/GameTicker.GameRule.cs @@ -9,6 +9,7 @@ using Robust.Shared.Console; using Robust.Shared.Map; using Robust.Shared.Prototypes; +using Robust.Shared.Localization; namespace Content.Server.GameTicking; @@ -71,6 +72,16 @@ public EntityUid AddGameRule(string ruleId) var ruleEntity = Spawn(ruleId, MapCoordinates.Nullspace); _sawmill.Info($"Added game rule {ToPrettyString(ruleEntity)}"); _adminLogger.Add(LogType.EventStarted, $"Added game rule {ToPrettyString(ruleEntity)}"); + var str = Loc.GetString("station-event-system-run-event", ("eventName", ToPrettyString(ruleEntity))); +#if DEBUG + _chatManager.SendAdminAlert(str); +#else + if (RunLevel == GameRunLevel.InRound) // avoids telling admins the round type before it starts so that can be handled elsewhere. + { + _chatManager.SendAdminAlert(str); + } +#endif + Log.Info(str); var ev = new GameRuleAddedEvent(ruleEntity, ruleId); RaiseLocalEvent(ruleEntity, ref ev, true); diff --git a/Content.Server/StationEvents/BasicStationEventSchedulerSystem.cs b/Content.Server/StationEvents/BasicStationEventSchedulerSystem.cs index 501d9033b9c0..bdc9a47e1867 100644 --- a/Content.Server/StationEvents/BasicStationEventSchedulerSystem.cs +++ b/Content.Server/StationEvents/BasicStationEventSchedulerSystem.cs @@ -2,11 +2,12 @@ using Content.Server.Administration; using Content.Server.GameTicking; using Content.Server.GameTicking.Rules; -using Content.Server.GameTicking.Rules.Components; using Content.Server.StationEvents.Components; using Content.Shared.Administration; +using Content.Shared.EntityTable; using Content.Shared.GameTicking.Components; using JetBrains.Annotations; +using Robust.Shared.Prototypes; using Robust.Shared.Random; using Robust.Shared.Toolshed; using Robust.Shared.Utility; @@ -23,13 +24,17 @@ public sealed class BasicStationEventSchedulerSystem : GameRuleSystem 0) { eventScheduler.TimeUntilNextEvent -= frameTime; - return; + continue; } - _event.RunRandomEvent(); + _event.RunRandomEvent(eventScheduler.ScheduledGameRules); ResetTimer(eventScheduler); } } @@ -62,7 +67,7 @@ public override void Update(float frameTime) /// private void ResetTimer(BasicStationEventSchedulerComponent component) { - component.TimeUntilNextEvent = _random.NextFloat(MinEventTime, MaxEventTime); + component.TimeUntilNextEvent = component.MinMaxEventTiming.Next(_random); } } @@ -70,7 +75,8 @@ private void ResetTimer(BasicStationEventSchedulerComponent component) public sealed class StationEventCommand : ToolshedCommand { private EventManagerSystem? _stationEvent; - private BasicStationEventSchedulerSystem? _basicScheduler; + private EntityTableSystem? _entityTable; + private IComponentFactory? _compFac; private IRobustRandom? _random; /// @@ -88,10 +94,11 @@ public sealed class StationEventCommand : ToolshedCommand /// to even exist) so I think it's fine. /// [CommandImplementation("simulate")] - public IEnumerable<(string, float)> Simulate([CommandArgument] int rounds, [CommandArgument] int playerCount, [CommandArgument] float roundEndMean, [CommandArgument] float roundEndStdDev) + public IEnumerable<(string, float)> Simulate([CommandArgument] EntityPrototype eventScheduler, [CommandArgument] int rounds, [CommandArgument] int playerCount, [CommandArgument] float roundEndMean, [CommandArgument] float roundEndStdDev) { _stationEvent ??= GetSys(); - _basicScheduler ??= GetSys(); + _entityTable ??= GetSys(); + _compFac ??= IoCManager.Resolve(); _random ??= IoCManager.Resolve(); var occurrences = new Dictionary(); @@ -101,6 +108,13 @@ public sealed class StationEventCommand : ToolshedCommand occurrences.Add(ev.Key.ID, 0); } + if (!eventScheduler.TryGetComponent(out var basicScheduler, _compFac)) + { + return occurrences.Select(p => (p.Key, (float)p.Value)).OrderByDescending(p => p.Item2); + } + + var compMinMax = basicScheduler.MinMaxEventTiming; // we gotta do this since we cant execute on comp w/o an ent. + for (var i = 0; i < rounds; i++) { var curTime = TimeSpan.Zero; @@ -111,9 +125,16 @@ public sealed class StationEventCommand : ToolshedCommand while (curTime.TotalSeconds < randomEndTime) { // sim an event - curTime += TimeSpan.FromSeconds(_random.NextFloat(BasicStationEventSchedulerSystem.MinEventTime, BasicStationEventSchedulerSystem.MaxEventTime)); + curTime += TimeSpan.FromSeconds(compMinMax.Next(_random)); + + if (!_stationEvent.TryBuildLimitedEvents(basicScheduler.ScheduledGameRules, out var selectedEvents)) + { + continue; // doesnt break because maybe the time is preventing events being available. + } var available = _stationEvent.AvailableEvents(false, playerCount, curTime); - var ev = _stationEvent.FindEvent(available); + var plausibleEvents = new Dictionary(available.Intersect(selectedEvents)); // C# makes me sad + + var ev = _stationEvent.FindEvent(plausibleEvents); if (ev == null) continue; @@ -125,26 +146,40 @@ public sealed class StationEventCommand : ToolshedCommand } [CommandImplementation("lsprob")] - public IEnumerable<(string, float)> LsProb() + public IEnumerable<(string, float)> LsProb([CommandArgument] EntityPrototype eventScheduler) { + _compFac ??= IoCManager.Resolve(); _stationEvent ??= GetSys(); - var events = _stationEvent.AllEvents(); - var totalWeight = events.Sum(x => x.Value.Weight); + if (!eventScheduler.TryGetComponent(out var basicScheduler, _compFac)) + yield break; - foreach (var (proto, comp) in events) + if (!_stationEvent.TryBuildLimitedEvents(basicScheduler.ScheduledGameRules, out var events)) + yield break; + + var totalWeight = events.Sum(x => x.Value.Weight); // Well this shit definitely isnt correct now, and I see no way to make it correct. + // Its probably *fine* but it wont be accurate if the EntityTableSelector does any subsetting. + foreach (var (proto, comp) in events) // The only solution I see is to do a simulation, and we already have that, so...! { yield return (proto.ID, comp.Weight / totalWeight); } } [CommandImplementation("lsprobtime")] - public IEnumerable<(string, float)> LsProbTime([CommandArgument] float time) + public IEnumerable<(string, float)> LsProbTime([CommandArgument] EntityPrototype eventScheduler, [CommandArgument] float time) { + _compFac ??= IoCManager.Resolve(); _stationEvent ??= GetSys(); - var events = _stationEvent.AllEvents().Where(pair => pair.Value.EarliestStart <= time).ToList(); - var totalWeight = events.Sum(x => x.Value.Weight); + if (!eventScheduler.TryGetComponent(out var basicScheduler, _compFac)) + yield break; + + if (!_stationEvent.TryBuildLimitedEvents(basicScheduler.ScheduledGameRules, out var untimedEvents)) + yield break; + + var events = untimedEvents.Where(pair => pair.Value.EarliestStart <= time).ToList(); + + var totalWeight = events.Sum(x => x.Value.Weight); // same subsetting issue as lsprob. foreach (var (proto, comp) in events) { @@ -153,12 +188,18 @@ public sealed class StationEventCommand : ToolshedCommand } [CommandImplementation("prob")] - public float Prob([CommandArgument] string eventId) + public float Prob([CommandArgument] EntityPrototype eventScheduler, [CommandArgument] string eventId) { + _compFac ??= IoCManager.Resolve(); _stationEvent ??= GetSys(); - var events = _stationEvent.AllEvents(); - var totalWeight = events.Sum(x => x.Value.Weight); + if (!eventScheduler.TryGetComponent(out var basicScheduler, _compFac)) + return 0f; + + if (!_stationEvent.TryBuildLimitedEvents(basicScheduler.ScheduledGameRules, out var events)) + return 0f; + + var totalWeight = events.Sum(x => x.Value.Weight); // same subsetting issue as lsprob. var weight = 0f; if (events.TryFirstOrNull(p => p.Key.ID == eventId, out var pair)) { diff --git a/Content.Server/StationEvents/Components/BasicStationEventSchedulerComponent.cs b/Content.Server/StationEvents/Components/BasicStationEventSchedulerComponent.cs index a6ea139f04e7..b777831856b2 100644 --- a/Content.Server/StationEvents/Components/BasicStationEventSchedulerComponent.cs +++ b/Content.Server/StationEvents/Components/BasicStationEventSchedulerComponent.cs @@ -1,14 +1,35 @@ -namespace Content.Server.StationEvents.Components; +using Content.Shared.Destructible.Thresholds; +using Content.Shared.EntityTable.EntitySelectors; + + +namespace Content.Server.StationEvents.Components; [RegisterComponent, Access(typeof(BasicStationEventSchedulerSystem))] public sealed partial class BasicStationEventSchedulerComponent : Component { - public const float MinimumTimeUntilFirstEvent = 300; + /// + /// How long the the scheduler waits to begin starting rules. + /// + [DataField] + public float MinimumTimeUntilFirstEvent = 200; + + /// + /// The minimum and maximum time between rule starts in seconds. + /// + [DataField] + public MinMax MinMaxEventTiming = new(3 * 60, 10 * 60); + + /// + /// How long until the next check for an event runs, is initially set based on MinimumTimeUntilFirstEvent & MinMaxEventTiming. + /// + [DataField] + public float TimeUntilNextEvent; /// - /// How long until the next check for an event runs + /// The gamerules that the scheduler can choose from /// - /// Default value is how long until first event is allowed - [ViewVariables(VVAccess.ReadWrite)] - public float TimeUntilNextEvent = MinimumTimeUntilFirstEvent; + /// Reminder that though we could do all selection via the EntityTableSelector, we also need to consider various restrictions. + /// As such, we want to pass a list of acceptable game rules, which are then parsed for restrictions by the . + [DataField(required: true)] + public EntityTableSelector ScheduledGameRules = default!; } diff --git a/Content.Server/StationEvents/Components/MeteorSchedulerComponent.cs b/Content.Server/StationEvents/Components/MeteorSchedulerComponent.cs deleted file mode 100644 index 23337f9261ed..000000000000 --- a/Content.Server/StationEvents/Components/MeteorSchedulerComponent.cs +++ /dev/null @@ -1,24 +0,0 @@ -using Content.Shared.Random; -using Robust.Shared.Prototypes; - -namespace Content.Server.StationEvents.Components; - -/// -/// This is used for running meteor swarm events at regular intervals. -/// -[RegisterComponent, Access(typeof(MeteorSchedulerSystem)), AutoGenerateComponentPause] -public sealed partial class MeteorSchedulerComponent : Component -{ - /// - /// The weights for which swarms will be selected. - /// - [DataField] - public ProtoId Config = "DefaultConfig"; - - /// - /// The time at which the next swarm occurs. - /// - [DataField, AutoPausedField] - public TimeSpan NextSwarmTime = TimeSpan.Zero; - -} diff --git a/Content.Server/StationEvents/Components/RampingStationEventSchedulerComponent.cs b/Content.Server/StationEvents/Components/RampingStationEventSchedulerComponent.cs index 53bc8b62a438..8bf433144c7f 100644 --- a/Content.Server/StationEvents/Components/RampingStationEventSchedulerComponent.cs +++ b/Content.Server/StationEvents/Components/RampingStationEventSchedulerComponent.cs @@ -1,17 +1,41 @@ -namespace Content.Server.StationEvents.Components; +using Content.Shared.EntityTable.EntitySelectors; + +namespace Content.Server.StationEvents.Components; [RegisterComponent, Access(typeof(RampingStationEventSchedulerSystem))] public sealed partial class RampingStationEventSchedulerComponent : Component { - [DataField("endTime"), ViewVariables(VVAccess.ReadWrite)] + /// + /// Average ending chaos modifier for the ramping event scheduler. Higher means faster. + /// Max chaos chosen for a round will deviate from this + /// + [DataField] + public float AverageChaos = 6f; + + /// + /// 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. + /// + [DataField] + public float AverageEndTime = 40f; + + [DataField] public float EndTime; - [DataField("maxChaos"), ViewVariables(VVAccess.ReadWrite)] + [DataField] public float MaxChaos; - [DataField("startingChaos"), ViewVariables(VVAccess.ReadWrite)] + [DataField] public float StartingChaos; - [DataField("timeUntilNextEvent"), ViewVariables(VVAccess.ReadWrite)] + [DataField] public float TimeUntilNextEvent; + + /// + /// The gamerules that the scheduler can choose from + /// + /// Reminder that though we could do all selection via the EntityTableSelector, we also need to consider various restrictions. + /// As such, we want to pass a list of acceptable game rules, which are then parsed for restrictions by the . + [DataField(required: true)] + public EntityTableSelector ScheduledGameRules = default!; } diff --git a/Content.Server/StationEvents/EventManagerSystem.cs b/Content.Server/StationEvents/EventManagerSystem.cs index 2e44040fcc8b..bc6b1834002d 100644 --- a/Content.Server/StationEvents/EventManagerSystem.cs +++ b/Content.Server/StationEvents/EventManagerSystem.cs @@ -1,5 +1,4 @@ using System.Linq; -using Content.Server.Chat.Managers; using Content.Server.GameTicking; using Content.Server.RoundEnd; using Content.Server.StationEvents.Components; @@ -8,6 +7,8 @@ using Robust.Shared.Configuration; using Robust.Shared.Prototypes; using Robust.Shared.Random; +using Content.Shared.EntityTable.EntitySelectors; +using Content.Shared.EntityTable; namespace Content.Server.StationEvents; @@ -17,7 +18,7 @@ public sealed class EventManagerSystem : EntitySystem [Dependency] private readonly IPlayerManager _playerManager = default!; [Dependency] private readonly IRobustRandom _random = default!; [Dependency] private readonly IPrototypeManager _prototype = default!; - [Dependency] private readonly IChatManager _chat = default!; + [Dependency] private readonly EntityTableSystem _entityTable = default!; [Dependency] public readonly GameTicker GameTicker = default!; [Dependency] private readonly RoundEndSystem _roundEnd = default!; @@ -34,7 +35,8 @@ public override void Initialize() /// /// Randomly runs a valid event. /// - public string RunRandomEvent() + [Obsolete("use overload taking EnityTableSelector instead or risk unexpected results")] + public void RunRandomEvent() { var randomEvent = PickRandomEvent(); @@ -42,14 +44,86 @@ public string RunRandomEvent() { var errStr = Loc.GetString("station-event-system-run-random-event-no-valid-events"); Log.Error(errStr); - return errStr; + return; } - var ent = GameTicker.AddGameRule(randomEvent); - var str = Loc.GetString("station-event-system-run-event",("eventName", ToPrettyString(ent))); - _chat.SendAdminAlert(str); - Log.Info(str); - return str; + GameTicker.AddGameRule(randomEvent); + } + + /// + /// Randomly runs an event from provided EntityTableSelector. + /// + public void RunRandomEvent(EntityTableSelector limitedEventsTable) + { + if (!TryBuildLimitedEvents(limitedEventsTable, out var limitedEvents)) + { + Log.Warning("Provided event table could not build dict!"); + return; + } + + var randomLimitedEvent = FindEvent(limitedEvents); // this picks the event, It might be better to use the GetSpawns to do it, but that will be a major rebalancing fuck. + if (randomLimitedEvent == null) + { + Log.Warning("The selected random event is null!"); + return; + } + + if (!_prototype.TryIndex(randomLimitedEvent, out _)) + { + Log.Warning("A requested event is not available!"); + return; + } + + GameTicker.AddGameRule(randomLimitedEvent); + } + + /// + /// Returns true if the provided EntityTableSelector gives at least one prototype with a StationEvent comp. + /// + public bool TryBuildLimitedEvents(EntityTableSelector limitedEventsTable, out Dictionary limitedEvents) + { + limitedEvents = new Dictionary(); + + var availableEvents = AvailableEvents(); // handles the player counts and individual event restrictions + + if (availableEvents.Count == 0) + { + Log.Warning("No events were available to run!"); + return false; + } + + var selectedEvents = _entityTable.GetSpawns(limitedEventsTable); + + if (selectedEvents.Any() != true) // This is here so if you fuck up the table it wont die. + return false; + + foreach (var eventid in selectedEvents) + { + if (!_prototype.TryIndex(eventid, out var eventproto)) + { + Log.Warning("An event ID has no prototype index!"); + continue; + } + + if (limitedEvents.ContainsKey(eventproto)) // This stops it from dying if you add duplicate entries in a fucked table + continue; + + if (eventproto.Abstract) + continue; + + if (!eventproto.TryGetComponent(out var stationEvent, EntityManager.ComponentFactory)) + continue; + + if (!availableEvents.ContainsKey(eventproto)) + continue; + + limitedEvents.Add(eventproto, stationEvent); + } + + if (!limitedEvents.Any()) + return false; + + return true; } /// diff --git a/Content.Server/StationEvents/MeteorSchedulerSystem.cs b/Content.Server/StationEvents/MeteorSchedulerSystem.cs deleted file mode 100644 index c5162293603a..000000000000 --- a/Content.Server/StationEvents/MeteorSchedulerSystem.cs +++ /dev/null @@ -1,54 +0,0 @@ -using Content.Server.GameTicking.Rules; -using Content.Server.StationEvents.Components; -using Content.Shared.CCVar; -using Content.Shared.GameTicking.Components; -using Content.Shared.Random.Helpers; -using Robust.Shared.Configuration; -using Robust.Shared.Prototypes; - -namespace Content.Server.StationEvents; - -/// -/// This handles scheduling and launching meteors at a station at regular intervals. -/// TODO: there is 100% a world in which this is genericized and can be used for lots of basic event scheduling -/// -public sealed class MeteorSchedulerSystem : GameRuleSystem -{ - [Dependency] private readonly IPrototypeManager _prototypeManager = default!; - [Dependency] private readonly IConfigurationManager _cfg = default!; - - private TimeSpan _meteorMinDelay; - private TimeSpan _meteorMaxDelay; - - public override void Initialize() - { - base.Initialize(); - - _cfg.OnValueChanged(CCVars.MeteorSwarmMinTime, f => { _meteorMinDelay = TimeSpan.FromMinutes(f); }, true); - _cfg.OnValueChanged(CCVars.MeteorSwarmMaxTime, f => { _meteorMaxDelay = TimeSpan.FromMinutes(f); }, true); - } - - protected override void Started(EntityUid uid, MeteorSchedulerComponent component, GameRuleComponent gameRule, GameRuleStartedEvent args) - { - base.Started(uid, component, gameRule, args); - - component.NextSwarmTime = Timing.CurTime + RobustRandom.Next(_meteorMinDelay, _meteorMaxDelay); - } - - protected override void ActiveTick(EntityUid uid, MeteorSchedulerComponent component, GameRuleComponent gameRule, float frameTime) - { - base.ActiveTick(uid, component, gameRule, frameTime); - - if (Timing.CurTime < component.NextSwarmTime) - return; - RunSwarm((uid, component)); - - component.NextSwarmTime += RobustRandom.Next(_meteorMinDelay, _meteorMaxDelay); - } - - private void RunSwarm(Entity ent) - { - var swarmWeights = _prototypeManager.Index(ent.Comp.Config); - GameTicker.StartGameRule(swarmWeights.Pick(RobustRandom)); - } -} diff --git a/Content.Server/StationEvents/RampingStationEventSchedulerSystem.cs b/Content.Server/StationEvents/RampingStationEventSchedulerSystem.cs index f65105de0669..a5dbe102ca13 100644 --- a/Content.Server/StationEvents/RampingStationEventSchedulerSystem.cs +++ b/Content.Server/StationEvents/RampingStationEventSchedulerSystem.cs @@ -1,22 +1,20 @@ -using Content.Server.GameTicking; +using Content.Server.GameTicking; using Content.Server.GameTicking.Rules; -using Content.Server.GameTicking.Rules.Components; using Content.Server.StationEvents.Components; -using Content.Server.StationEvents.Events; -using Content.Shared.CCVar; using Content.Shared.GameTicking.Components; -using Robust.Shared.Configuration; using Robust.Shared.Random; namespace Content.Server.StationEvents; public sealed class RampingStationEventSchedulerSystem : GameRuleSystem { - [Dependency] private readonly IConfigurationManager _cfg = default!; [Dependency] private readonly IRobustRandom _random = default!; [Dependency] private readonly EventManagerSystem _event = default!; [Dependency] private readonly GameTicker _gameTicker = default!; + /// + /// Returns the ChaosModifier which increases as round time increases to a point. + /// public float GetChaosModifier(EntityUid uid, RampingStationEventSchedulerComponent component) { var roundTime = (float) _gameTicker.RoundDuration().TotalSeconds; @@ -30,14 +28,11 @@ protected override void Started(EntityUid uid, RampingStationEventSchedulerCompo { base.Started(uid, component, gameRule, args); - var avgChaos = _cfg.GetCVar(CCVars.EventsRampingAverageChaos); - var avgTime = _cfg.GetCVar(CCVars.EventsRampingAverageEndTime); - // Worlds shittiest probability distribution // Got a complaint? Send them to - component.MaxChaos = _random.NextFloat(avgChaos - avgChaos / 4, avgChaos + avgChaos / 4); + component.MaxChaos = _random.NextFloat(component.AverageChaos - component.AverageChaos / 4, component.AverageChaos + component.AverageChaos / 4); // This is in minutes, so *60 for seconds (for the chaos calc) - component.EndTime = _random.NextFloat(avgTime - avgTime / 4, avgTime + avgTime / 4) * 60f; + component.EndTime = _random.NextFloat(component.AverageEndTime - component.AverageEndTime / 4, component.AverageEndTime + component.AverageEndTime / 4) * 60f; component.StartingChaos = component.MaxChaos / 10; PickNextEventTime(uid, component); @@ -54,19 +49,22 @@ public override void Update(float frameTime) while (query.MoveNext(out var uid, out var scheduler, out var gameRule)) { if (!GameTicker.IsGameRuleActive(uid, gameRule)) - return; + continue; if (scheduler.TimeUntilNextEvent > 0f) { scheduler.TimeUntilNextEvent -= frameTime; - return; + continue; } PickNextEventTime(uid, scheduler); - _event.RunRandomEvent(); + _event.RunRandomEvent(scheduler.ScheduledGameRules); } } + /// + /// Sets the timing of the next event addition. + /// private void PickNextEventTime(EntityUid uid, RampingStationEventSchedulerComponent component) { var mod = GetChaosModifier(uid, component); diff --git a/Content.Shared/CCVar/CCVars.cs b/Content.Shared/CCVar/CCVars.cs index 3049df28aeb2..adad4e6b00c3 100644 --- a/Content.Shared/CCVar/CCVars.cs +++ b/Content.Shared/CCVar/CCVars.cs @@ -110,32 +110,6 @@ public sealed class CCVars : CVars 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); - - /// - /// Minimum time between meteor swarms in minutes. - /// - public static readonly CVarDef - MeteorSwarmMinTime = CVarDef.Create("events.meteor_swarm_min_time", 12.5f, CVar.ARCHIVE | CVar.SERVERONLY); - - /// - /// Maximum time between meteor swarms in minutes. - /// - public static readonly CVarDef - MeteorSwarmMaxTime = CVarDef.Create("events.meteor_swarm_max_time", 17.5f, CVar.ARCHIVE | CVar.SERVERONLY); - /* * Game */ diff --git a/Resources/Locale/en-US/game-ticking/game-presets/preset-allatonce.ftl b/Resources/Locale/en-US/game-ticking/game-presets/preset-allatonce.ftl index 3452b2faa969..ba4f62c4ca0a 100644 --- a/Resources/Locale/en-US/game-ticking/game-presets/preset-allatonce.ftl +++ b/Resources/Locale/en-US/game-ticking/game-presets/preset-allatonce.ftl @@ -1,2 +1,5 @@ all-at-once-title = All at once all-at-once-description = It's just not your day... + +aller-at-once-title = Aller at once +aller-at-once-description = You have fucked up now. You *have* fucked up now. \ No newline at end of file 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 index 231733eabfb4..492bb9e34105 100644 --- a/Resources/Locale/en-US/game-ticking/game-presets/preset-survival.ftl +++ b/Resources/Locale/en-US/game-ticking/game-presets/preset-survival.ftl @@ -1,2 +1,5 @@ survival-title = Survival survival-description = No internal threats, but how long can the station survive increasingly chaotic and frequent events? + +kessler-syndrome-title = Kessler Syndrome +kessler-syndrome-description = No internal threats, but the station is quickly falling into a belt of meteors! \ No newline at end of file diff --git a/Resources/Locale/en-US/game-ticking/game-presets/preset-zombies.ftl b/Resources/Locale/en-US/game-ticking/game-presets/preset-zombies.ftl index efb5c6ef1e19..4d127d9fa535 100644 --- a/Resources/Locale/en-US/game-ticking/game-presets/preset-zombies.ftl +++ b/Resources/Locale/en-US/game-ticking/game-presets/preset-zombies.ftl @@ -1,6 +1,9 @@ zombie-title = Zombies zombie-description = The undead have been unleashed on the station! Work with the crew to survive the outbreak and secure the station. +zombieteors-title = Zombieteors +zombieteors-description = The undead have been unleashed on the station amid a cataclysmic meteor shower! Work with your fellow crew and do your best to survive! + zombie-not-enough-ready-players = Not enough players readied up for the game! There were {$readyPlayersCount} players readied up out of {$minimumPlayers} needed. Can't start Zombies. zombie-no-one-ready = No players readied up! Can't start Zombies. diff --git a/Resources/Prototypes/GameRules/cargo_gifts.yml b/Resources/Prototypes/GameRules/cargo_gifts.yml index ffc2ed404b70..f4e4a5bf8fc6 100644 --- a/Resources/Prototypes/GameRules/cargo_gifts.yml +++ b/Resources/Prototypes/GameRules/cargo_gifts.yml @@ -1,3 +1,22 @@ +# Tables -- Add your new event to this Table if you want it to happen via the basic/ramping schedulers. + +- type: entityTable + id: CargoGiftsTable + table: !type:GroupSelector # ~~we need to pass a list of rules, since rules have further restrictions to consider via StationEventComp~~ But we arent doing that shit yet, it picks a random one StationEventComp be damned. + children: + - id: GiftsEngineering + - id: GiftsFireProtection + - id: GiftsJanitor + - id: GiftsMedical + - id: GiftsPizzaPartyLarge + - id: GiftsPizzaPartySmall + - id: GiftsSecurityGuns + - id: GiftsSecurityRiot + - id: GiftsSpacingSupplies + - id: GiftsVendingRestock + +# Game Rules + - type: entity id: CargoGiftsBase parent: BaseGameRule diff --git a/Resources/Prototypes/GameRules/events.yml b/Resources/Prototypes/GameRules/events.yml index fe9d6410d042..56ffeda7f035 100644 --- a/Resources/Prototypes/GameRules/events.yml +++ b/Resources/Prototypes/GameRules/events.yml @@ -1,3 +1,40 @@ +- type: entityTable + id: BasicCalmEventsTable + table: !type:AllSelector # we need to pass a list of rules, since rules have further restrictions to consider via StationEventComp + children: + - id: AnomalySpawn + - id: BluespaceArtifact + - id: BluespaceLocker + - id: BreakerFlip + - id: BureaucraticError + - id: ClericalError + - id: CockroachMigration + - id: GasLeak + - id: IonStorm # its calm like 90% of the time smh + - id: KudzuGrowth + - id: MassHallucinations + - id: MimicVendorRule + - id: MouseMigration + - id: PowerGridCheck + - id: SlimesSpawn + - id: SolarFlare + - id: SpiderClownSpawn + - id: SpiderSpawn + - id: VentClog + +- type: entityTable + id: BasicAntagEventsTable + table: !type:AllSelector # we need to pass a list of rules, since rules have further restrictions to consider via StationEventComp + children: + - id: ClosetSkeleton + - id: DragonSpawn + - id: KingRatMigration + - id: NinjaSpawn + - id: RevenantSpawn + - id: SleeperAgents + - id: ZombieOutbreak + + - type: entity id: BaseStationEvent parent: BaseGameRule @@ -526,45 +563,6 @@ sounds: collection: Paracusia -- type: entity - id: ImmovableRodSpawn - parent: BaseGameRule - components: - - type: StationEvent - startAnnouncement: station-event-immovable-rod-start-announcement - startAudio: - path: /Audio/Announcements/attention.ogg - weight: 3.5 - duration: 1 - earliestStart: 30 - minimumPlayers: 20 - - type: ImmovableRodRule - rodPrototypes: - - id: ImmovableRodKeepTilesStill - prob: 0.95 - orGroup: rodProto - - id: ImmovableRodMop - prob: 0.0072 - orGroup: rodProto - - id: ImmovableRodShark - prob: 0.0072 - orGroup: rodProto - - id: ImmovableRodClown - prob: 0.0072 - orGroup: rodProto - - id: ImmovableRodBanana - prob: 0.0072 - orGroup: rodProto - - id: ImmovableRodHammer - prob: 0.0072 - orGroup: rodProto - - id: ImmovableRodThrongler - prob: 0.0072 - orGroup: rodProto - - id: ImmovableRodGibstick - prob: 0.0072 - orGroup: rodProto - - type: entity parent: BaseGameRule id: IonStorm diff --git a/Resources/Prototypes/GameRules/meteorswarms.yml b/Resources/Prototypes/GameRules/meteorswarms.yml index 2f1cb4eba5ed..d69fd9700b93 100644 --- a/Resources/Prototypes/GameRules/meteorswarms.yml +++ b/Resources/Prototypes/GameRules/meteorswarms.yml @@ -1,21 +1,39 @@ -- type: entity - parent: BaseGameRule - id: GameRuleMeteorScheduler - components: - - type: GameRule - minPlayers: 25 - cancelPresetOnTooFewPlayers: false - - type: MeteorScheduler +# Event Tables -- type: weightedRandomEntity - id: DefaultConfig - weights: - GameRuleSpaceDustMinor: 44 - GameRuleSpaceDustMajor: 22 - GameRuleMeteorSwarmSmall: 18 - GameRuleMeteorSwarmMedium: 10 - GameRuleMeteorSwarmLarge: 5 - GameRuleUristSwarm: 0.05 +- type: entityTable + id: MeteorSwarmDustEventsTable + table: !type:AllSelector # we need to pass a list of rules, since rules have further restrictions to consider via StationEventComp + children: + - id: GameRuleSpaceDustMinor + - id: GameRuleSpaceDustMajor + +- type: entityTable + id: MeteorSwarmSmallChanceEventsTable + table: !type:AllSelector # we need to pass a list of rules, since rules have further restrictions to consider via StationEventComp + children: + - id: GameRuleMeteorSwarmSmall + prob: 0.15 + +- type: entityTable + id: MeteorSwarmMildTable + table: !type:AllSelector # we need to pass a list of rules, since rules have further restrictions to consider via StationEventComp + children: + - !type:NestedSelector + tableId: MeteorSwarmDustEventsTable + - !type:NestedSelector + tableId: MeteorSwarmSmallChanceEventsTable + +- type: entityTable + id: BasicMeteorSwarmEventsTable + table: !type:AllSelector # we need to pass a list of rules, since rules have further restrictions to consider via StationEventComp + children: + - !type:NestedSelector + tableId: MeteorSwarmDustEventsTable + - id: GameRuleMeteorSwarmSmall + - id: GameRuleMeteorSwarmMedium + - id: GameRuleMeteorSwarmLarge + - id: GameRuleUristSwarm + - id: ImmovableRodSpawn - type: weightedRandomEntity id: MeteorSpawnAsteroidWallTable @@ -29,7 +47,46 @@ AsteroidRockPlasma: 2 AsteroidRockDiamond: 2 AsteroidRockUranium: 0.5 - AsteroidRockBananium: 0.5 + AsteroidRockBananium: 0.5 + +# Event Schedulers + +- type: entity + parent: BaseGameRule + id: MeteorSwarmScheduler + components: + - type: GameRule + - type: BasicStationEventScheduler + minimumTimeUntilFirstEvent: 300 # 5 min + minMaxEventTiming: + min: 750 # 12.5 min + max: 930 # 17.5 min + scheduledGameRules: !type:NestedSelector + tableId: BasicMeteorSwarmEventsTable + +- type: entity + parent: BaseGameRule + id: MeteorSwarmMildScheduler + components: + - type: GameRule + - type: BasicStationEventScheduler + minimumTimeUntilFirstEvent: 300 # 5 min + minMaxEventTiming: + min: 750 # 12.5 min + max: 930 # 17.5 min + scheduledGameRules: !type:NestedSelector + tableId: MeteorSwarmMildTable + +- type: entity + parent: BaseGameRule + id: KesslerSyndromeScheduler + components: + - type: GameRule + - type: RampingStationEventScheduler + scheduledGameRules: !type:NestedSelector + tableId: BasicMeteorSwarmEventsTable + +# Game Rules - type: entity parent: BaseGameRule @@ -37,12 +94,19 @@ abstract: true components: - type: GameRule + - type: StationEvent + earliestStart: 12 + minimumPlayers: 25 - type: MeteorSwarm - type: entity parent: GameRuleMeteorSwarm id: GameRuleSpaceDustMinor components: + - type: StationEvent + weight: 44 + earliestStart: 2 + minimumPlayers: 0 - type: MeteorSwarm announcement: null announcementSound: null @@ -60,6 +124,9 @@ parent: GameRuleMeteorSwarm id: GameRuleSpaceDustMajor components: + - type: StationEvent + weight: 22 + minimumPlayers: 0 - type: MeteorSwarm announcement: station-event-space-dust-start-announcement announcementSound: /Audio/Announcements/attention.ogg @@ -77,6 +144,9 @@ parent: GameRuleMeteorSwarm id: GameRuleMeteorSwarmSmall components: + - type: StationEvent + weight: 18 + minimumPlayers: 15 - type: MeteorSwarm meteors: MeteorSmall: 7 @@ -86,6 +156,8 @@ parent: GameRuleMeteorSwarm id: GameRuleMeteorSwarmMedium components: + - type: StationEvent + weight: 10 - type: MeteorSwarm meteors: MeteorSmall: 3 @@ -96,6 +168,8 @@ parent: GameRuleMeteorSwarm id: GameRuleMeteorSwarmLarge components: + - type: StationEvent + weight: 5 - type: MeteorSwarm meteors: MeteorSmall: 2 @@ -106,6 +180,8 @@ parent: GameRuleMeteorSwarm id: GameRuleUristSwarm components: + - type: StationEvent + weight: 0.05 - type: MeteorSwarm announcement: station-event-meteor-urist-start-announcement announcementSound: /Audio/Announcements/attention.ogg @@ -117,3 +193,42 @@ meteorsPerWave: min: 10 max: 10 + +- type: entity + id: ImmovableRodSpawn + parent: BaseGameRule + components: + - type: StationEvent + startAnnouncement: station-event-immovable-rod-start-announcement + startAudio: + path: /Audio/Announcements/attention.ogg + weight: 3.5 + duration: 1 + earliestStart: 30 + minimumPlayers: 25 + - type: ImmovableRodRule + rodPrototypes: + - id: ImmovableRodKeepTilesStill + prob: 0.95 + orGroup: rodProto + - id: ImmovableRodMop + prob: 0.0072 + orGroup: rodProto + - id: ImmovableRodShark + prob: 0.0072 + orGroup: rodProto + - id: ImmovableRodClown + prob: 0.0072 + orGroup: rodProto + - id: ImmovableRodBanana + prob: 0.0072 + orGroup: rodProto + - id: ImmovableRodHammer + prob: 0.0072 + orGroup: rodProto + - id: ImmovableRodThrongler + prob: 0.0072 + orGroup: rodProto + - id: ImmovableRodGibstick + prob: 0.0072 + orGroup: rodProto diff --git a/Resources/Prototypes/GameRules/roundstart.yml b/Resources/Prototypes/GameRules/roundstart.yml index a10a6c8d2a6f..8ef0db965827 100644 --- a/Resources/Prototypes/GameRules/roundstart.yml +++ b/Resources/Prototypes/GameRules/roundstart.yml @@ -262,17 +262,68 @@ prototype: InitialInfected # event schedulers + +- type: entityTable + id: BasicGameRulesTable + table: !type:AllSelector # we need to pass a list of rules, since rules have further restrictions to consider via StationEventComp + children: + - !type:NestedSelector + tableId: BasicCalmEventsTable + - !type:NestedSelector + tableId: BasicAntagEventsTable + - !type:NestedSelector + tableId: CargoGiftsTable + +- type: entityTable + id: SpaceTrafficControlTable + table: !type:AllSelector # we need to pass a list of rules, since rules have further restrictions to consider via StationEventComp + children: + - !type:NestedSelector + tableId: UnknownShuttlesFriendlyTable + - !type:NestedSelector + tableId: UnknownShuttlesFreelanceTable + - !type:NestedSelector + tableId: UnknownShuttlesHostileTable + - type: entity id: BasicStationEventScheduler parent: BaseGameRule components: - type: BasicStationEventScheduler + scheduledGameRules: !type:NestedSelector + tableId: BasicGameRulesTable - type: entity id: RampingStationEventScheduler parent: BaseGameRule components: - type: RampingStationEventScheduler + scheduledGameRules: !type:NestedSelector + tableId: BasicGameRulesTable + +- type: entity + id: SpaceTrafficControlEventScheduler # iff we make a selector for EntityTables that can respect StationEventComp restrictions, or somehow impliment them otherwise in said tables, + parent: BaseGameRule # we can remerge this with the other schedulers, but it will silently fail due to that limitation without a separate scheduler to balance atm. + components: + - type: BasicStationEventScheduler + minimumTimeUntilFirstEvent: 1200 # 20 mins + minMaxEventTiming: + min: 600 # 10 mins + max: 1800 # 30 mins + scheduledGameRules: !type:NestedSelector + tableId: SpaceTrafficControlTable + +- type: entity + id: SpaceTrafficControlFriendlyEventScheduler + parent: BaseGameRule + components: + - type: BasicStationEventScheduler + minimumTimeUntilFirstEvent: 1200 # 20 mins + minMaxEventTiming: + min: 600 # 10 mins + max: 1800 # 30 mins + scheduledGameRules: !type:NestedSelector + tableId: UnknownShuttlesFriendlyTable # variation passes - type: entity diff --git a/Resources/Prototypes/GameRules/unknown_shuttles.yml b/Resources/Prototypes/GameRules/unknown_shuttles.yml index 1ce365cd816b..3f707fd57e1f 100644 --- a/Resources/Prototypes/GameRules/unknown_shuttles.yml +++ b/Resources/Prototypes/GameRules/unknown_shuttles.yml @@ -1,3 +1,28 @@ +# Shuttle Game Rule Tables -- If you dont add your rules to these they wont be used by the games schedulers. + +- type: entityTable + id: UnknownShuttlesFriendlyTable + table: !type:AllSelector # we need to pass a list of rules, since rules have further restrictions to consider via StationEventComp + children: + - id: UnknownShuttleCargoLost + - id: UnknownShuttleTravelingCuisine + - id: UnknownShuttleDisasterEvacPod + - id: UnknownShuttleHonki + +- type: entityTable + id: UnknownShuttlesFreelanceTable + table: !type:AllSelector # we need to pass a list of rules, since rules have further restrictions to consider via StationEventComp + children: + - id: UnknownShuttleSyndieEvacPod + +- type: entityTable + id: UnknownShuttlesHostileTable + table: !type:AllSelector # we need to pass a list of rules, since rules have further restrictions to consider via StationEventComp + children: + - id: LoneOpsSpawn + +# Shuttle Game Rules + - type: entity abstract: true parent: BaseGameRule diff --git a/Resources/Prototypes/game_presets.yml b/Resources/Prototypes/game_presets.yml index 3a3f07760b9a..efce3c4e24cc 100644 --- a/Resources/Prototypes/game_presets.yml +++ b/Resources/Prototypes/game_presets.yml @@ -6,12 +6,31 @@ showInVote: false # secret description: survival-description rules: + - MeteorSwarmScheduler - RampingStationEventScheduler - - GameRuleMeteorScheduler + - SpaceTrafficControlEventScheduler + - SpaceTrafficControlFriendlyEventScheduler + - BasicRoundstartVariation + +- type: gamePreset + id: KesslerSyndrome + alias: + - kessler + - junk + - meteorhell + name: kessler-syndrome-title + showInVote: false # secret + description: kessler-syndrome-description + rules: + - KesslerSyndromeScheduler + - RampingStationEventScheduler + - SpaceTrafficControlEventScheduler - BasicRoundstartVariation - type: gamePreset id: AllAtOnce + alias: + - all name: all-at-once-title description: all-at-once-description showInVote: false @@ -20,8 +39,34 @@ - Traitor - Revolutionary - Zombie + - KesslerSyndromeScheduler + - RampingStationEventScheduler + - SpaceTrafficControlEventScheduler + - BasicRoundstartVariation + +- type: gamePreset + id: AllerAtOnce + alias: + - allall + - aller + - badidea + - punishment + name: aller-at-once-title + description: all-at-once-description + showInVote: false #Please god dont do this + rules: + - Nukeops + - Traitor + - Revolutionary + - Zombie + - BasicStationEventScheduler + - KesslerSyndromeScheduler + - MeteorSwarmMildScheduler + - MeteorSwarmScheduler - RampingStationEventScheduler - - GameRuleMeteorScheduler + - SpaceTrafficControlEventScheduler + - SpaceTrafficControlFriendlyEventScheduler + - BasicRoundstartVariation - type: gamePreset id: Extended @@ -33,7 +78,8 @@ description: extended-description rules: - BasicStationEventScheduler - - GameRuleMeteorScheduler + - MeteorSwarmScheduler + - SpaceTrafficControlEventScheduler - BasicRoundstartVariation - type: gamePreset @@ -45,6 +91,7 @@ showInVote: false #4boring4vote description: greenshift-description rules: + - SpaceTrafficControlFriendlyEventScheduler - BasicRoundstartVariation - type: gamePreset @@ -67,7 +114,9 @@ description: secret-description rules: - BasicStationEventScheduler - - GameRuleMeteorScheduler + - MeteorSwarmScheduler + - SpaceTrafficControlEventScheduler + - BasicRoundstartVariation - type: gamePreset id: SecretGreenshift #For Admin Use: Runs Greenshift but shows "Secret" in lobby. @@ -76,6 +125,9 @@ name: secret-title showInVote: false #Admin Use description: secret-description + rules: + - SpaceTrafficControlFriendlyEventScheduler + - BasicRoundstartVariation - type: gamePreset id: Sandbox @@ -92,6 +144,7 @@ id: Traitor alias: - traitor + - tator name: traitor-title description: traitor-description showInVote: false @@ -99,7 +152,8 @@ - Traitor - SubGamemodesRule - BasicStationEventScheduler - - GameRuleMeteorScheduler + - MeteorSwarmScheduler + - SpaceTrafficControlEventScheduler - BasicRoundstartVariation - type: gamePreset @@ -126,7 +180,8 @@ - Nukeops - SubGamemodesRule - BasicStationEventScheduler - - GameRuleMeteorScheduler + - MeteorSwarmScheduler + - SpaceTrafficControlEventScheduler - BasicRoundstartVariation - type: gamePreset @@ -142,7 +197,8 @@ - Revolutionary - SubGamemodesRule - BasicStationEventScheduler - - GameRuleMeteorScheduler + - MeteorSwarmScheduler + - SpaceTrafficControlEventScheduler - BasicRoundstartVariation - type: gamePreset @@ -159,5 +215,22 @@ rules: - Zombie - BasicStationEventScheduler - - GameRuleMeteorScheduler + - MeteorSwarmScheduler + - SpaceTrafficControlEventScheduler + - BasicRoundstartVariation + +- type: gamePreset + id: Zombieteors + alias: + - zombieteors + - zombombies + - meteombies + name: zombieteors-title + description: zombieteors-description + showInVote: false + rules: + - Zombie + - BasicStationEventScheduler + - KesslerSyndromeScheduler + - SpaceTrafficControlEventScheduler - BasicRoundstartVariation diff --git a/Resources/Prototypes/secret_weights.yml b/Resources/Prototypes/secret_weights.yml index 7af610af6c7d..0a272e24c7c0 100644 --- a/Resources/Prototypes/secret_weights.yml +++ b/Resources/Prototypes/secret_weights.yml @@ -3,6 +3,8 @@ weights: Nukeops: 0.20 Traitor: 0.60 - Zombie: 0.05 - Survival: 0.10 + Zombie: 0.04 + Zombieteors: 0.01 + Survival: 0.09 + KesslerSyndrome: 0.01 Revolutionary: 0.05