Roundstart variation game rules (#24397)

* Raise `StationPostInitEvent` broadcast

* Basic variation pass handling

* standardize names + rule entities

* why does it work like that?

* add to defaults

* light break variation pass

* ent spawn entry

* move some stationevent utility functions to gamerule + add one for finding random tile on specified station

* forgot how statistics works

* powered light variation pass is good now

* station tile count function

* public method to ensure all solutions (for procedural use before mapinit)

* move gamerulesystem utility funcs to partial

* ensure all solutions before spilling in puddlesystem. for use when spilling before mapinit

* trash & puddle variation passes!

* oh yeah

* ehh lets live a little

* std

* utility for game rule check based on comp

* entprotoid the trash spawner oops

* generalize trash variation

* use added instead of started for secret rule

* random cleanup

* generic replacement variation system

* Wall rusting variation rule

* account for modifying while enumerating

* use localaabb

* fix test

* minor tweaks

* reinforced wall replacer + puddletweaker

(cherry picked from commit cc24ba6a317c4bee84ffa1eda8397c255ca92be9)
This commit is contained in:
Kara
2024-01-31 06:52:35 +01:00
committed by Debug
parent aa4c6f9c6a
commit d35e2f39a7
37 changed files with 978 additions and 149 deletions

View File

@@ -188,7 +188,7 @@ namespace Content.Server.GameTicking
return true;
}
private void StartGamePresetRules()
public void StartGamePresetRules()
{
// May be touched by the preset during init.
var rules = new List<EntityUid>(GetAddedGameRules());

View File

@@ -141,6 +141,24 @@ public sealed partial class GameTicker
return true;
}
/// <summary>
/// Returns true if a game rule with the given component has been added.
/// </summary>
public bool IsGameRuleAdded<T>()
where T : IComponent
{
var query = EntityQueryEnumerator<T, GameRuleComponent>();
while (query.MoveNext(out var uid, out _, out _))
{
if (HasComp<EndedGameRuleComponent>(uid))
continue;
return true;
}
return false;
}
public bool IsGameRuleAdded(EntityUid ruleEntity, GameRuleComponent? component = null)
{
return Resolve(ruleEntity, ref component) && !HasComp<EndedGameRuleComponent>(ruleEntity);
@@ -157,6 +175,22 @@ public sealed partial class GameTicker
return false;
}
/// <summary>
/// Returns true if a game rule with the given component is active..
/// </summary>
public bool IsGameRuleActive<T>()
where T : IComponent
{
var query = EntityQueryEnumerator<T, ActiveGameRuleComponent, GameRuleComponent>();
// out, damned underscore!!!
while (query.MoveNext(out _, out _, out _, out _))
{
return true;
}
return false;
}
public bool IsGameRuleActive(EntityUid ruleEntity, GameRuleComponent? component = null)
{
return Resolve(ruleEntity, ref component) && HasComp<ActiveGameRuleComponent>(ruleEntity);

View File

@@ -0,0 +1,19 @@
using Content.Shared.Storage;
using Robust.Shared.Prototypes;
namespace Content.Server.GameTicking.Rules.Components;
/// <summary>
/// This handles starting various roundstart variation rules after a station has been loaded.
/// </summary>
[RegisterComponent]
public sealed partial class RoundstartStationVariationRuleComponent : Component
{
/// <summary>
/// The list of rules that will be started once the map is spawned.
/// Uses <see cref="EntitySpawnEntry"/> to support probabilities for various rules
/// without having to hardcode the probability directly in the rule's logic.
/// </summary>
[DataField(required: true)]
public List<EntitySpawnEntry> Rules = new();
}

View File

@@ -0,0 +1,9 @@
namespace Content.Server.GameTicking.Rules.Components;
/// <summary>
/// This is a marker component placed on rule entities which are a single "pass" of station variation.
/// </summary>
[RegisterComponent]
public sealed partial class StationVariationPassRuleComponent : Component
{
}

View File

@@ -0,0 +1,137 @@
using System.Diagnostics.CodeAnalysis;
using Content.Server.GameTicking.Rules.Components;
using Content.Server.Station.Components;
using Robust.Shared.Collections;
using Robust.Shared.Map;
using Robust.Shared.Map.Components;
using Robust.Shared.Random;
namespace Content.Server.GameTicking.Rules;
public abstract partial class GameRuleSystem<T> where T: IComponent
{
protected EntityQueryEnumerator<ActiveGameRuleComponent, T, GameRuleComponent> QueryActiveRules()
{
return EntityQueryEnumerator<ActiveGameRuleComponent, T, GameRuleComponent>();
}
protected bool TryRoundStartAttempt(RoundStartAttemptEvent ev, string localizedPresetName)
{
var query = EntityQueryEnumerator<ActiveGameRuleComponent, T, GameRuleComponent>();
while (query.MoveNext(out _, out _, out _, out var gameRule))
{
var minPlayers = gameRule.MinPlayers;
if (!ev.Forced && ev.Players.Length < minPlayers)
{
ChatManager.SendAdminAnnouncement(Loc.GetString("preset-not-enough-ready-players",
("readyPlayersCount", ev.Players.Length), ("minimumPlayers", minPlayers),
("presetName", localizedPresetName)));
ev.Cancel();
continue;
}
if (ev.Players.Length == 0)
{
ChatManager.DispatchServerAnnouncement(Loc.GetString("preset-no-one-ready"));
ev.Cancel();
}
}
return !ev.Cancelled;
}
/// <summary>
/// Utility function for finding a random event-eligible station entity
/// </summary>
protected bool TryGetRandomStation([NotNullWhen(true)] out EntityUid? station, Func<EntityUid, bool>? filter = null)
{
var stations = new ValueList<EntityUid>(Count<StationEventEligibleComponent>());
filter ??= _ => true;
var query = AllEntityQuery<StationEventEligibleComponent>();
while (query.MoveNext(out var uid, out _))
{
if (!filter(uid))
continue;
stations.Add(uid);
}
if (stations.Count == 0)
{
station = null;
return false;
}
// TODO: Engine PR.
station = stations[RobustRandom.Next(stations.Count)];
return true;
}
protected bool TryFindRandomTile(out Vector2i tile,
[NotNullWhen(true)] out EntityUid? targetStation,
out EntityUid targetGrid,
out EntityCoordinates targetCoords)
{
tile = default;
targetStation = EntityUid.Invalid;
targetGrid = EntityUid.Invalid;
targetCoords = EntityCoordinates.Invalid;
if (TryGetRandomStation(out targetStation))
{
return TryFindRandomTileOnStation((targetStation.Value, Comp<StationDataComponent>(targetStation.Value)),
out tile,
out targetGrid,
out targetCoords);
}
return false;
}
protected bool TryFindRandomTileOnStation(Entity<StationDataComponent> station,
out Vector2i tile,
out EntityUid targetGrid,
out EntityCoordinates targetCoords)
{
tile = default;
targetCoords = EntityCoordinates.Invalid;
targetGrid = EntityUid.Invalid;
var possibleTargets = station.Comp.Grids;
if (possibleTargets.Count == 0)
{
targetGrid = EntityUid.Invalid;
return false;
}
targetGrid = RobustRandom.Pick(possibleTargets);
if (!TryComp<MapGridComponent>(targetGrid, out var gridComp))
return false;
var found = false;
var aabb = gridComp.LocalAABB;
for (var i = 0; i < 10; i++)
{
var randomX = RobustRandom.Next((int) aabb.Left, (int) aabb.Right);
var randomY = RobustRandom.Next((int) aabb.Bottom, (int) aabb.Top);
tile = new Vector2i(randomX, randomY);
if (_atmosphere.IsTileSpace(targetGrid, Transform(targetGrid).MapUid, tile,
mapGridComp: gridComp)
|| _atmosphere.IsTileAirBlocked(targetGrid, tile, mapGridComp: gridComp))
{
continue;
}
found = true;
targetCoords = _map.GridTileToLocal(targetGrid, gridComp, tile);
break;
}
return found;
}
}

View File

@@ -1,13 +1,27 @@
using System.Diagnostics.CodeAnalysis;
using Content.Server.Atmos.EntitySystems;
using Content.Server.Chat.Managers;
using Content.Server.GameTicking.Rules.Components;
using Content.Server.Station.Components;
using Robust.Server.GameObjects;
using Robust.Shared.Collections;
using Robust.Shared.Map;
using Robust.Shared.Map.Components;
using Robust.Shared.Random;
namespace Content.Server.GameTicking.Rules;
public abstract partial class GameRuleSystem<T> : EntitySystem where T : IComponent
{
[Dependency] protected readonly IRobustRandom RobustRandom = default!;
[Dependency] protected readonly IChatManager ChatManager = default!;
[Dependency] protected readonly GameTicker GameTicker = default!;
// Not protected, just to be used in utility methods
[Dependency] private readonly SharedTransformSystem _transform = default!;
[Dependency] private readonly AtmosphereSystem _atmosphere = default!;
[Dependency] private readonly MapSystem _map = default!;
public override void Initialize()
{
base.Initialize();
@@ -71,36 +85,6 @@ public abstract partial class GameRuleSystem<T> : EntitySystem where T : ICompon
}
protected EntityQueryEnumerator<ActiveGameRuleComponent, T, GameRuleComponent> QueryActiveRules()
{
return EntityQueryEnumerator<ActiveGameRuleComponent, T, GameRuleComponent>();
}
protected bool TryRoundStartAttempt(RoundStartAttemptEvent ev, string localizedPresetName)
{
var query = EntityQueryEnumerator<ActiveGameRuleComponent, T, GameRuleComponent>();
while (query.MoveNext(out _, out _, out _, out var gameRule))
{
var minPlayers = gameRule.MinPlayers;
if (!ev.Forced && ev.Players.Length < minPlayers)
{
ChatManager.SendAdminAnnouncement(Loc.GetString("preset-not-enough-ready-players",
("readyPlayersCount", ev.Players.Length), ("minimumPlayers", minPlayers),
("presetName", localizedPresetName)));
ev.Cancel();
continue;
}
if (ev.Players.Length == 0)
{
ChatManager.DispatchServerAnnouncement(Loc.GetString("preset-no-one-ready"));
ev.Cancel();
}
}
return !ev.Cancelled;
}
public override void Update(float frameTime)
{
base.Update(frameTime);

View File

@@ -0,0 +1,70 @@
using System.Linq;
using Content.Server.GameTicking.Rules.Components;
using Content.Server.Shuttles.Systems;
using Content.Server.Station.Components;
using Content.Server.Station.Events;
using Content.Shared.Storage;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;
namespace Content.Server.GameTicking.Rules;
/// <inheritdoc cref="RoundstartStationVariationRuleComponent"/>
public sealed class RoundstartStationVariationRuleSystem : GameRuleSystem<RoundstartStationVariationRuleComponent>
{
[Dependency] private readonly IRobustRandom _random = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<StationPostInitEvent>(OnStationPostInit, after: new []{typeof(ShuttleSystem)});
}
protected override void Added(EntityUid uid, RoundstartStationVariationRuleComponent component, GameRuleComponent gameRule, GameRuleAddedEvent args)
{
var spawns = EntitySpawnCollection.GetSpawns(component.Rules, _random);
foreach (var rule in spawns)
{
GameTicker.AddGameRule(rule);
}
}
private void OnStationPostInit(ref StationPostInitEvent ev)
{
// as long as one is running
if (!GameTicker.IsGameRuleAdded<RoundstartStationVariationRuleComponent>())
return;
// this is unlikely, but could theoretically happen if it was saved and reloaded, so check anyway
if (HasComp<StationVariationHasRunComponent>(ev.Station))
return;
Log.Info($"Running variation rules for station {ToPrettyString(ev.Station)}");
// raise the event on any passes that have been added
var passEv = new StationVariationPassEvent(ev.Station);
var passQuery = EntityQueryEnumerator<StationVariationPassRuleComponent, GameRuleComponent>();
while (passQuery.MoveNext(out var uid, out _, out _))
{
// TODO: for some reason, ending a game rule just gives it a marker comp,
// and doesnt delete it
// so we have to check here that it isnt an ended game rule (which could happen if a preset failed to start
// or it was ended before station maps spawned etc etc etc)
if (HasComp<EndedGameRuleComponent>(uid))
continue;
RaiseLocalEvent(uid, ref passEv);
}
EnsureComp<StationVariationHasRunComponent>(ev.Station);
}
}
/// <summary>
/// Raised directed on game rule entities which are added and marked as <see cref="StationVariationPassRuleComponent"/>
/// when a new station is initialized that should be varied.
/// </summary>
/// <param name="Station">The new station that was added, and its config & grids.</param>
[ByRefEvent]
public readonly record struct StationVariationPassEvent(Entity<StationDataComponent> Station);

View File

@@ -18,9 +18,9 @@ public sealed class SecretRuleSystem : GameRuleSystem<SecretRuleComponent>
[Dependency] private readonly IConfigurationManager _configurationManager = default!;
[Dependency] private readonly IAdminLogManager _adminLogger = default!;
protected override void Started(EntityUid uid, SecretRuleComponent component, GameRuleComponent gameRule, GameRuleStartedEvent args)
protected override void Added(EntityUid uid, SecretRuleComponent component, GameRuleComponent gameRule, GameRuleAddedEvent args)
{
base.Started(uid, component, gameRule, args);
base.Added(uid, component, gameRule, args);
PickRule(component);
}
@@ -40,13 +40,24 @@ public sealed class SecretRuleSystem : GameRuleSystem<SecretRuleComponent>
// but currently there's no way to know anyway as they use cvars.
var presetString = _configurationManager.GetCVar(CCVars.SecretWeightPrototype);
var preset = _prototypeManager.Index<WeightedRandomPrototype>(presetString).Pick(_random);
Logger.InfoS("gamepreset", $"Selected {preset} for secret.");
Log.Info($"Selected {preset} for secret.");
_adminLogger.Add(LogType.EventStarted, $"Selected {preset} for secret.");
var rules = _prototypeManager.Index<GamePresetPrototype>(preset).Rules;
foreach (var rule in rules)
{
GameTicker.StartGameRule(rule, out var ruleEnt);
EntityUid ruleEnt;
// if we're pre-round (i.e. will only be added)
// then just add rules. if we're added in the middle of the round (or at any other point really)
// then we want to start them as well
if (GameTicker.RunLevel <= GameRunLevel.InRound)
ruleEnt = GameTicker.AddGameRule(rule);
else
{
GameTicker.StartGameRule(rule, out ruleEnt);
}
component.AdditionalGameRules.Add(ruleEnt);
}
}

View File

@@ -0,0 +1,75 @@
using Content.Server.GameTicking.Rules.VariationPass.Components;
using Content.Shared.Storage;
using Robust.Shared.Map;
using Robust.Shared.Random;
using Robust.Shared.Timing;
namespace Content.Server.GameTicking.Rules.VariationPass;
/// <inheritdoc cref="EntityReplaceVariationPassComponent"/>
/// <summary>
/// A base system for fast replacement of entities utilizing a query, rather than having to iterate every entity
/// To use, you must have a marker component to use for <see cref="TEntComp"/>--each replaceable entity must have it
/// Then you need an inheriting system as well as a unique game rule component for <see cref="TGameRuleComp"/>
///
/// This means a bit more boilerplate for each one, but significantly faster to actually execute.
/// See <see cref="WallReplaceVariationPassSystem"/>
/// </summary>
public abstract class BaseEntityReplaceVariationPassSystem<TEntComp, TGameRuleComp> : VariationPassSystem<TGameRuleComp>
where TEntComp: IComponent
where TGameRuleComp: IComponent
{
/// <summary>
/// Used so we don't modify while enumerating
/// if the replaced entity also has <see cref="TEntComp"/>.
///
/// Filled and cleared within the same tick so no persistence issues.
/// </summary>
private readonly Queue<(string, EntityCoordinates, Angle)> _queuedSpawns = new();
protected override void ApplyVariation(Entity<TGameRuleComp> ent, ref StationVariationPassEvent args)
{
if (!TryComp<EntityReplaceVariationPassComponent>(ent, out var pass))
return;
var stopwatch = new Stopwatch();
stopwatch.Start();
var replacementMod = Random.NextGaussian(pass.EntitiesPerReplacementAverage, pass.EntitiesPerReplacementStdDev);
var prob = (float) Math.Clamp(1 / replacementMod, 0f, 1f);
if (prob == 0)
return;
var enumerator = AllEntityQuery<TEntComp, TransformComponent>();
while (enumerator.MoveNext(out var uid, out _, out var xform))
{
if (!IsMemberOfStation((uid, xform), ref args))
continue;
if (RobustRandom.Prob(prob))
QueueReplace((uid, xform), pass.Replacements);
}
while (_queuedSpawns.TryDequeue(out var tup))
{
var (spawn, coords, rot) = tup;
var newEnt = Spawn(spawn, coords);
Transform(newEnt).LocalRotation = rot;
}
Log.Debug($"Entity replacement took {stopwatch.Elapsed} with {Stations.GetTileCount(args.Station)} tiles");
}
private void QueueReplace(Entity<TransformComponent> ent, List<EntitySpawnEntry> replacements)
{
var coords = ent.Comp.Coordinates;
var rot = ent.Comp.LocalRotation;
QueueDel(ent);
foreach (var spawn in EntitySpawnCollection.GetSpawns(replacements, RobustRandom))
{
_queuedSpawns.Enqueue((spawn, coords, rot));
}
}
}

View File

@@ -0,0 +1,33 @@
using Content.Shared.Storage;
using Content.Shared.Whitelist;
using Robust.Shared.Prototypes;
namespace Content.Server.GameTicking.Rules.VariationPass.Components;
/// <summary>
/// This is used for replacing a certain amount of entities with other entities in a variation pass.
///
/// </summary>
/// <remarks>
/// POTENTIALLY REPLACEABLE ENTITIES MUST BE MARKED WITH A REPLACEMENT MARKER
/// AND HAVE A SYSTEM INHERITING FROM <see cref="BaseEntityReplaceVariationPassSystem{TEntComp,TGameRuleComp}"/>
/// SEE <see cref="WallReplaceVariationPassSystem"/>
/// </remarks>
[RegisterComponent]
public sealed partial class EntityReplaceVariationPassComponent : Component
{
/// <summary>
/// Number of matching entities before one will be replaced on average.
/// </summary>
[DataField(required: true)]
public float EntitiesPerReplacementAverage;
[DataField(required: true)]
public float EntitiesPerReplacementStdDev;
/// <summary>
/// Prototype(s) to replace matched entities with.
/// </summary>
[DataField(required: true)]
public List<EntitySpawnEntry> Replacements = default!;
}

View File

@@ -0,0 +1,27 @@
using Content.Shared.Random;
using Content.Shared.Storage;
using Robust.Shared.Prototypes;
namespace Content.Server.GameTicking.Rules.VariationPass.Components;
/// <summary>
/// This is used for spawning entities randomly dotted around the station in a variation pass.
/// </summary>
[RegisterComponent]
public sealed partial class EntitySpawnVariationPassComponent : Component
{
/// <summary>
/// Number of tiles before we spawn one entity on average.
/// </summary>
[DataField]
public float TilesPerEntityAverage = 50f;
[DataField]
public float TilesPerEntityStdDev = 7f;
/// <summary>
/// Spawn entries for each chosen location.
/// </summary>
[DataField(required: true)]
public List<EntitySpawnEntry> Entities = default!;
}

View File

@@ -0,0 +1,38 @@
using Content.Shared.Light.Components;
using Robust.Shared.Prototypes;
namespace Content.Server.GameTicking.Rules.VariationPass.Components;
/// <summary>
/// This handle randomly destroying lights, causing them to flicker endlessly, or replacing their tube/bulb with different variants.
/// </summary>
[RegisterComponent]
public sealed partial class PoweredLightVariationPassComponent : Component
{
/// <summary>
/// Chance that a light will be replaced with a broken variant.
/// </summary>
[DataField]
public float LightBreakChance = 0.15f;
/// <summary>
/// Chance that a light will be replaced with an aged variant.
/// </summary>
[DataField]
public float LightAgingChance = 0.05f;
[DataField]
public float AgedLightTubeFlickerChance = 0.03f;
[DataField]
public EntProtoId BrokenLightBulbPrototype = "LightBulbBroken";
[DataField]
public EntProtoId BrokenLightTubePrototype = "LightTubeBroken";
[DataField]
public EntProtoId AgedLightBulbPrototype = "LightBulbOld";
[DataField]
public EntProtoId AgedLightTubePrototype = "LightTubeOld";
}

View File

@@ -0,0 +1,26 @@
using Content.Shared.Random;
using Robust.Shared.Prototypes;
namespace Content.Server.GameTicking.Rules.VariationPass.Components;
/// <summary>
/// Handles spilling puddles with various reagents randomly around the station.
/// </summary>
[RegisterComponent]
public sealed partial class PuddleMessVariationPassComponent : Component
{
/// <summary>
/// Tiles before one spill on average.
/// </summary>
[DataField]
public float TilesPerSpillAverage = 600f;
[DataField]
public float TilesPerSpillStdDev = 50f;
/// <summary>
/// Weighted random prototype to use for random messes.
/// </summary>
[DataField(required: true)]
public ProtoId<WeightedRandomFillSolutionPrototype> RandomPuddleSolutionFill = default!;
}

View File

@@ -0,0 +1,7 @@
namespace Content.Server.GameTicking.Rules.VariationPass.Components;
[RegisterComponent]
public sealed partial class ReinforcedWallReplaceVariationPassComponent : Component
{
}

View File

@@ -0,0 +1,9 @@
namespace Content.Server.GameTicking.Rules.VariationPass.Components.ReplacementMarkers;
/// <summary>
/// This component marks replaceable reinforced walls for use with fast queries in variation passes.
/// </summary>
[RegisterComponent]
public sealed partial class ReinforcedWallReplacementMarkerComponent : Component
{
}

View File

@@ -0,0 +1,9 @@
namespace Content.Server.GameTicking.Rules.VariationPass.Components.ReplacementMarkers;
/// <summary>
/// This component marks replaceable walls for use with fast queries in variation passes.
/// </summary>
[RegisterComponent]
public sealed partial class WallReplacementMarkerComponent : Component
{
}

View File

@@ -0,0 +1,7 @@
namespace Content.Server.GameTicking.Rules.VariationPass.Components;
[RegisterComponent]
public sealed partial class WallReplaceVariationPassComponent : Component
{
}

View File

@@ -0,0 +1,29 @@
using Content.Server.GameTicking.Rules.VariationPass.Components;
using Content.Shared.Storage;
using Robust.Shared.Random;
namespace Content.Server.GameTicking.Rules.VariationPass;
/// <inheritdoc cref="EntitySpawnVariationPassComponent"/>
public sealed class EntitySpawnVariationPassSystem : VariationPassSystem<EntitySpawnVariationPassComponent>
{
protected override void ApplyVariation(Entity<EntitySpawnVariationPassComponent> ent, ref StationVariationPassEvent args)
{
var totalTiles = Stations.GetTileCount(args.Station);
var dirtyMod = Random.NextGaussian(ent.Comp.TilesPerEntityAverage, ent.Comp.TilesPerEntityStdDev);
var trashTiles = Math.Max((int) (totalTiles * (1 / dirtyMod)), 0);
for (var i = 0; i < trashTiles; i++)
{
if (!TryFindRandomTileOnStation(args.Station, out _, out _, out var coords))
continue;
var ents = EntitySpawnCollection.GetSpawns(ent.Comp.Entities, Random);
foreach (var spawn in ents)
{
SpawnAtPosition(spawn, coords);
}
}
}
}

View File

@@ -0,0 +1,51 @@
using Content.Server.GameTicking.Rules.VariationPass.Components;
using Content.Server.Light.Components;
using Content.Server.Light.EntitySystems;
using Content.Shared.Light.Components;
using Robust.Shared.Random;
namespace Content.Server.GameTicking.Rules.VariationPass;
/// <inheritdoc cref="PoweredLightVariationPassComponent"/>
public sealed class PoweredLightVariationPassSystem : VariationPassSystem<PoweredLightVariationPassComponent>
{
[Dependency] private readonly PoweredLightSystem _poweredLight = default!;
protected override void ApplyVariation(Entity<PoweredLightVariationPassComponent> ent, ref StationVariationPassEvent args)
{
var query = AllEntityQuery<PoweredLightComponent, TransformComponent>();
while (query.MoveNext(out var uid, out var comp, out var xform))
{
if (!IsMemberOfStation((uid, xform), ref args))
continue;
if (Random.Prob(ent.Comp.LightBreakChance))
{
var proto = comp.BulbType switch
{
LightBulbType.Tube => ent.Comp.BrokenLightTubePrototype,
_ => ent.Comp.BrokenLightBulbPrototype,
};
_poweredLight.ReplaceSpawnedPrototype((uid, comp), proto);
continue;
}
if (!Random.Prob(ent.Comp.LightAgingChance))
continue;
if (comp.BulbType == LightBulbType.Tube)
{
// some aging fluorescents (tubes) start to flicker
// its also way too annoying right now so we wrap it in another prob lol
if (Random.Prob(ent.Comp.AgedLightTubeFlickerChance))
_poweredLight.ToggleBlinkingLight(uid, comp, true);
_poweredLight.ReplaceSpawnedPrototype((uid, comp), ent.Comp.AgedLightTubePrototype);
}
else
{
_poweredLight.ReplaceSpawnedPrototype((uid, comp), ent.Comp.AgedLightBulbPrototype);
}
}
}
}

View File

@@ -0,0 +1,35 @@
using Content.Server.Fluids.EntitySystems;
using Content.Server.GameTicking.Rules.VariationPass.Components;
using Content.Shared.Chemistry.Components;
using Content.Shared.Random.Helpers;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;
namespace Content.Server.GameTicking.Rules.VariationPass;
/// <inheritdoc cref="PuddleMessVariationPassComponent"/>
public sealed class PuddleMessVariationPassSystem : VariationPassSystem<PuddleMessVariationPassComponent>
{
[Dependency] private readonly PuddleSystem _puddle = default!;
[Dependency] private readonly IPrototypeManager _proto = default!;
protected override void ApplyVariation(Entity<PuddleMessVariationPassComponent> ent, ref StationVariationPassEvent args)
{
var totalTiles = Stations.GetTileCount(args.Station);
if (!_proto.TryIndex(ent.Comp.RandomPuddleSolutionFill, out var proto))
return;
var puddleMod = Random.NextGaussian(ent.Comp.TilesPerSpillAverage, ent.Comp.TilesPerSpillStdDev);
var puddleTiles = Math.Max((int) (totalTiles * (1 / puddleMod)), 0);
for (var i = 0; i < puddleTiles; i++)
{
if (!TryFindRandomTileOnStation(args.Station, out _, out _, out var coords))
continue;
var sol = proto.Pick(Random);
_puddle.TrySpillAt(coords, new Solution(sol.reagent, sol.quantity), out _, sound: false);
}
}
}

View File

@@ -0,0 +1,11 @@
using Content.Server.GameTicking.Rules.VariationPass.Components;
using Content.Server.GameTicking.Rules.VariationPass.Components.ReplacementMarkers;
namespace Content.Server.GameTicking.Rules.VariationPass;
/// <summary>
/// This handles the ability to replace entities marked with <see cref="ReinforcedWallReplacementMarkerComponent"/> in a variation pass
/// </summary>
public sealed class ReinforcedWallReplaceVariationPassSystem : BaseEntityReplaceVariationPassSystem<ReinforcedWallReplacementMarkerComponent, ReinforcedWallReplaceVariationPassComponent>
{
}

View File

@@ -0,0 +1,29 @@
using Content.Server.Station.Systems;
using Robust.Shared.Random;
namespace Content.Server.GameTicking.Rules.VariationPass;
/// <summary>
/// Base class for procedural variation rule passes, which apply some kind of variation to a station,
/// so we simply reduce the boilerplate for the event handling a bit with this.
/// </summary>
public abstract class VariationPassSystem<T> : GameRuleSystem<T>
where T: IComponent
{
[Dependency] protected readonly StationSystem Stations = default!;
[Dependency] protected readonly IRobustRandom Random = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<T, StationVariationPassEvent>(ApplyVariation);
}
protected bool IsMemberOfStation(Entity<TransformComponent> ent, ref StationVariationPassEvent args)
{
return Stations.GetOwningStation(ent, ent.Comp) == args.Station.Owner;
}
protected abstract void ApplyVariation(Entity<T> ent, ref StationVariationPassEvent args);
}

View File

@@ -0,0 +1,11 @@
using Content.Server.GameTicking.Rules.VariationPass.Components;
using Content.Server.GameTicking.Rules.VariationPass.Components.ReplacementMarkers;
namespace Content.Server.GameTicking.Rules.VariationPass;
/// <summary>
/// This handles the ability to replace entities marked with <see cref="WallReplacementMarkerComponent"/> in a variation pass
/// </summary>
public sealed class WallReplaceVariationPassSystem : BaseEntityReplaceVariationPassSystem<WallReplacementMarkerComponent, WallReplaceVariationPassComponent>
{
}