Weather entities (#41427)

* weather status effect entities

* proto port + fixes

* Update weather.yml

* fix visuals

* Update SharedWeatherSystem.cs

* Update SharedWeatherSystem.cs

* Update WeatherSystem.cs

* Thanks Slarti, thats a much better

* Update WeatherSystem.cs

* Update StencilOverlay.Weather.cs

* Update Content.Client/Overlays/StencilOverlay.Weather.cs

Co-authored-by: Pok <113675512+Pok27@users.noreply.github.com>

* Update Content.Shared/StatusEffectNew/Components/StatusEffectComponent.cs

Co-authored-by: Pok <113675512+Pok27@users.noreply.github.com>

* Update Content.Client/Overlays/StencilOverlay.Weather.cs

Co-authored-by: Pok <113675512+Pok27@users.noreply.github.com>

* Revise weather command help descriptions

Updated weather command help texts for clarity.

* Merge branch 'master' into ed-14-11-2025-weather-entities

* Tayrtahn review apply

* fixes and cleanup

---------

Co-authored-by: Pok <113675512+Pok27@users.noreply.github.com>
Co-authored-by: slarticodefast <161409025+slarticodefast@users.noreply.github.com>
This commit is contained in:
Red 2026-03-09 02:28:48 +03:00 committed by Coryler
parent e2a2f0c969
commit 4186c8caa2
21 changed files with 825 additions and 705 deletions

View File

@ -1,9 +1,9 @@
using System.Numerics;
using Content.Shared.Light.Components;
using Content.Shared.StatusEffectNew.Components;
using Content.Shared.Weather;
using Robust.Client.Graphics;
using Robust.Shared.Map.Components;
using Robust.Shared.Physics.Components;
namespace Content.Client.Overlays;
@ -14,8 +14,7 @@ public sealed partial class StencilOverlay
private void DrawWeather(
in OverlayDrawArgs args,
CachedResources res,
WeatherPrototype weatherProto,
float alpha,
HashSet<Entity<WeatherStatusEffectComponent, StatusEffectComponent>> weathers,
Matrix3x2 invMatrix)
{
var worldHandle = args.WorldHandle;
@ -27,47 +26,58 @@ public sealed partial class StencilOverlay
// Cut out the irrelevant bits via stencil
// This is why we don't just use parallax; we might want specific tiles to get drawn over
// particularly for planet maps or stations.
worldHandle.RenderInRenderTarget(res.Blep!, () =>
{
var xformQuery = _entManager.GetEntityQuery<TransformComponent>();
_grids.Clear();
// idk if this is safe to cache in a field and clear sloth help
_mapManager.FindGridsIntersecting(mapId, worldAABB, ref _grids);
foreach (var grid in _grids)
worldHandle.RenderInRenderTarget(res.Blep!,
() =>
{
var matrix = _transform.GetWorldMatrix(grid, xformQuery);
var matty = Matrix3x2.Multiply(matrix, invMatrix);
worldHandle.SetTransform(matty);
_entManager.TryGetComponent(grid.Owner, out RoofComponent? roofComp);
var xformQuery = _entManager.GetEntityQuery<TransformComponent>();
_grids.Clear();
foreach (var tile in _map.GetTilesIntersecting(grid.Owner, grid, worldAABB))
// idk if this is safe to cache in a field and clear sloth help
_mapManager.FindGridsIntersecting(mapId, worldAABB, ref _grids);
foreach (var grid in _grids)
{
// Ignored tiles for stencil
if (_weather.CanWeatherAffect(grid.Owner, grid, tile, roofComp))
var matrix = _transform.GetWorldMatrix(grid, xformQuery);
var matty = Matrix3x2.Multiply(matrix, invMatrix);
worldHandle.SetTransform(matty);
_entManager.TryGetComponent(grid.Owner, out RoofComponent? roofComp);
foreach (var tile in _map.GetTilesIntersecting(grid.Owner, grid, worldAABB))
{
continue;
// Ignored tiles for stencil
if (_weather.CanWeatherAffect((grid.Owner, grid, roofComp), tile))
continue;
var gridTile = new Box2(tile.GridIndices * grid.Comp.TileSize,
(tile.GridIndices + Vector2i.One) * grid.Comp.TileSize);
worldHandle.DrawRect(gridTile, Color.White);
}
var gridTile = new Box2(tile.GridIndices * grid.Comp.TileSize,
(tile.GridIndices + Vector2i.One) * grid.Comp.TileSize);
worldHandle.DrawRect(gridTile, Color.White);
}
}
}, Color.Transparent);
},
Color.Transparent);
worldHandle.SetTransform(Matrix3x2.Identity);
worldHandle.UseShader(_protoManager.Index(StencilMask).Instance());
worldHandle.DrawTextureRect(res.Blep!.Texture, worldBounds);
var curTime = _timing.RealTime;
var sprite = _sprite.GetFrame(weatherProto.Sprite, curTime);
// Draw the rain
worldHandle.UseShader(_protoManager.Index(StencilDraw).Instance());
_parallax.DrawParallax(worldHandle, worldAABB, sprite, curTime, position, Vector2.Zero, modulate: (weatherProto.Color ?? Color.White).WithAlpha(alpha));
foreach (var (uid, weather, status) in weathers)
{
var alpha = _weather.GetWeatherPercent((uid, status));
var sprite = _sprite.GetFrame(weather.Sprite, curTime);
// Draw the rain
worldHandle.UseShader(_protoManager.Index(StencilDraw).Instance());
_parallax.DrawParallax(worldHandle,
worldAABB,
sprite,
curTime,
position,
weather.Scrolling ?? Vector2.Zero,
modulate: (weather.Color ?? Color.White).WithAlpha(alpha));
}
worldHandle.SetTransform(Matrix3x2.Identity);
worldHandle.UseShader(null);

View File

@ -3,6 +3,8 @@ using Content.Client.Graphics;
using Content.Client.Parallax;
using Content.Client.Weather;
using Content.Shared.Salvage;
using Content.Shared.StatusEffectNew;
using Content.Shared.StatusEffectNew.Components;
using Content.Shared.Weather;
using Robust.Client.GameObjects;
using Robust.Client.Graphics;
@ -32,6 +34,8 @@ public sealed partial class StencilOverlay : Overlay
private readonly SharedMapSystem _map;
private readonly SpriteSystem _sprite;
private readonly WeatherSystem _weather;
private readonly StatusEffectsSystem _statusEffects;
private HashSet<Entity<WeatherStatusEffectComponent, StatusEffectComponent>>? _weatherSet = new();
public override OverlaySpace Space => OverlaySpace.WorldSpaceBelowFOV;
@ -39,7 +43,7 @@ public sealed partial class StencilOverlay : Overlay
private readonly ShaderInstance _shader;
public StencilOverlay(ParallaxSystem parallax, SharedTransformSystem transform, SharedMapSystem map, SpriteSystem sprite, WeatherSystem weather)
public StencilOverlay(ParallaxSystem parallax, SharedTransformSystem transform, SharedMapSystem map, SpriteSystem sprite, WeatherSystem weather, StatusEffectsSystem statusEffects)
{
ZIndex = ParallaxSystem.ParallaxZIndex + 1;
_parallax = parallax;
@ -47,6 +51,7 @@ public sealed partial class StencilOverlay : Overlay
_map = map;
_sprite = sprite;
_weather = weather;
_statusEffects = statusEffects;
IoCManager.InjectDependencies(this);
_shader = _protoManager.Index(CircleShader).InstanceUnique();
}
@ -64,22 +69,11 @@ public sealed partial class StencilOverlay : Overlay
res.Blep = _clyde.CreateRenderTarget(args.Viewport.Size, new RenderTargetFormatParameters(RenderTargetColorFormat.Rgba8Srgb), name: "weather-stencil");
}
if (_entManager.TryGetComponent<WeatherComponent>(mapUid, out var comp))
{
foreach (var (proto, weather) in comp.Weather)
{
if (!_protoManager.Resolve<WeatherPrototype>(proto, out var weatherProto))
continue;
var alpha = _weather.GetPercent(weather, mapUid);
DrawWeather(args, res, weatherProto, alpha, invMatrix);
}
}
if (_statusEffects.TryEffectsWithComp(mapUid, out _weatherSet))
DrawWeather(args, res, _weatherSet, invMatrix);
if (_entManager.TryGetComponent<RestrictedRangeComponent>(mapUid, out var restrictedRangeComponent))
{
DrawRestrictedRange(args, res, restrictedRangeComponent, invMatrix);
}
args.WorldHandle.UseShader(null);
args.WorldHandle.SetTransform(Matrix3x2.Identity);

View File

@ -1,5 +1,6 @@
using Content.Client.Parallax;
using Content.Client.Weather;
using Content.Shared.StatusEffectNew;
using Robust.Client.GameObjects;
using Robust.Client.Graphics;
@ -13,11 +14,12 @@ public sealed class StencilOverlaySystem : EntitySystem
[Dependency] private readonly SharedMapSystem _map = default!;
[Dependency] private readonly SpriteSystem _sprite = default!;
[Dependency] private readonly WeatherSystem _weather = default!;
[Dependency] private readonly StatusEffectsSystem _status = default!;
public override void Initialize()
{
base.Initialize();
_overlay.AddOverlay(new StencilOverlay(_parallax, _transform, _map, _sprite, _weather));
_overlay.AddOverlay(new StencilOverlay(_parallax, _transform, _map, _sprite, _weather, _status));
}
public override void Shutdown()

View File

@ -1,15 +1,15 @@
using System.Numerics;
using Content.Shared.Light.Components;
using Content.Shared.StatusEffectNew.Components;
using Content.Shared.Weather;
using Robust.Client.Audio;
using Robust.Client.GameObjects;
using Robust.Client.Player;
using Robust.Shared.Audio.Components;
using Robust.Shared.Audio.Systems;
using Robust.Shared.GameStates;
using Robust.Shared.Map;
using Robust.Shared.Map.Components;
using Robust.Shared.Player;
using AudioComponent = Robust.Shared.Audio.Components.AudioComponent;
namespace Content.Client.Weather;
@ -20,163 +20,121 @@ public sealed class WeatherSystem : SharedWeatherSystem
[Dependency] private readonly MapSystem _mapSystem = default!;
[Dependency] private readonly SharedTransformSystem _transform = default!;
private EntityQuery<AudioComponent> _audioQuery;
private EntityQuery<MapGridComponent> _gridQuery;
private EntityQuery<RoofComponent> _roofQuery;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<WeatherComponent, ComponentHandleState>(OnWeatherHandleState);
SubscribeLocalEvent<WeatherStatusEffectComponent, ComponentShutdown>(OnComponentShutdown);
_audioQuery = GetEntityQuery<AudioComponent>();
_gridQuery = GetEntityQuery<MapGridComponent>();
_roofQuery = GetEntityQuery<RoofComponent>();
}
protected override void Run(EntityUid uid, WeatherData weather, WeatherPrototype weatherProto, float frameTime)
private void OnComponentShutdown(Entity<WeatherStatusEffectComponent> ent, ref ComponentShutdown args)
{
base.Run(uid, weather, weatherProto, frameTime);
var ent = _playerManager.LocalEntity;
if (ent == null)
return;
var mapUid = Transform(uid).MapUid;
var entXform = Transform(ent.Value);
// Maybe have the viewports manage this?
if (mapUid == null || entXform.MapUid != mapUid)
{
weather.Stream = _audio.Stop(weather.Stream);
return;
}
if (!Timing.IsFirstTimePredicted || weatherProto.Sound == null)
return;
weather.Stream ??= _audio.PlayGlobal(weatherProto.Sound, Filter.Local(), true)?.Entity;
if (!TryComp(weather.Stream, out AudioComponent? comp))
return;
var occlusion = 0f;
// Work out tiles nearby to determine volume.
if (TryComp<MapGridComponent>(entXform.GridUid, out var grid))
{
TryComp(entXform.GridUid, out RoofComponent? roofComp);
var gridId = entXform.GridUid.Value;
// FloodFill to the nearest tile and use that for audio.
var seed = _mapSystem.GetTileRef(gridId, grid, entXform.Coordinates);
var frontier = new Queue<TileRef>();
frontier.Enqueue(seed);
// If we don't have a nearest node don't play any sound.
EntityCoordinates? nearestNode = null;
var visited = new HashSet<Vector2i>();
while (frontier.TryDequeue(out var node))
{
if (!visited.Add(node.GridIndices))
continue;
if (!CanWeatherAffect(entXform.GridUid.Value, grid, node, roofComp))
{
// Add neighbors
// TODO: Ideally we pick some deterministically random direction and use that
// We can't just do that naively here because it will flicker between nearby tiles.
for (var x = -1; x <= 1; x++)
{
for (var y = -1; y <= 1; y++)
{
if (Math.Abs(x) == 1 && Math.Abs(y) == 1 ||
x == 0 && y == 0 ||
(new Vector2(x, y) + node.GridIndices - seed.GridIndices).Length() > 3)
{
continue;
}
frontier.Enqueue(_mapSystem.GetTileRef(gridId, grid, new Vector2i(x, y) + node.GridIndices));
}
}
continue;
}
nearestNode = new EntityCoordinates(entXform.GridUid.Value,
node.GridIndices + grid.TileSizeHalfVector);
break;
}
// Get occlusion to the targeted node if it exists, otherwise set a default occlusion.
if (nearestNode != null)
{
var entPos = _transform.GetMapCoordinates(entXform);
var nodePosition = _transform.ToMapCoordinates(nearestNode.Value).Position;
var delta = nodePosition - entPos.Position;
var distance = delta.Length();
occlusion = _audio.GetOcclusion(entPos, delta, distance);
}
else
{
occlusion = 3f;
}
}
var alpha = GetPercent(weather, uid);
alpha *= SharedAudioSystem.VolumeToGain(weatherProto.Sound.Params.Volume);
_audio.SetGain(weather.Stream, alpha, comp);
comp.Occlusion = occlusion;
ent.Comp.Stream = _audio.Stop(ent.Comp.Stream);
}
protected override bool SetState(EntityUid uid, WeatherState state, WeatherComponent comp, WeatherData weather, WeatherPrototype weatherProto)
public override void Update(float frameTime)
{
if (!base.SetState(uid, state, comp, weather, weatherProto))
return false;
base.Update(frameTime);
if (!Timing.IsFirstTimePredicted)
return true;
// Begin DeltaV Additions: Prevent hearing weather in the lobby
if (_playerManager.LocalEntity is not {} ent)
return false;
var map = Transform(uid).MapUid;
var entMap = Transform(ent).MapUid;
if (map == null || entMap != map)
{
weather.Stream = _audio.Stop(weather.Stream);
return false;
}
// End DeltaV Additions
// TODO: Fades (properly)
weather.Stream = _audio.Stop(weather.Stream);
weather.Stream = _audio.PlayGlobal(weatherProto.Sound, Filter.Local(), true)?.Entity;
return true;
}
private void OnWeatherHandleState(EntityUid uid, WeatherComponent component, ref ComponentHandleState args)
{
if (args.Current is not WeatherComponentState state)
return;
foreach (var (proto, weather) in component.Weather)
var player = _playerManager.LocalEntity;
if (player == null)
return;
var playerXform = Transform(player.Value);
var query = EntityQueryEnumerator<WeatherStatusEffectComponent, StatusEffectComponent>();
while (query.MoveNext(out var uid, out var weather, out var status))
{
// End existing one
if (!state.Weather.TryGetValue(proto, out var stateData))
if (weather.Sound == null || status.AppliedTo != playerXform.MapUid)
{
EndWeather(uid, component, proto);
continue;
weather.Stream = _audio.Stop(weather.Stream);
return;
}
// Data update?
weather.StartTime = stateData.StartTime;
weather.EndTime = stateData.EndTime;
weather.State = stateData.State;
}
weather.Stream ??= _audio.PlayGlobal(weather.Sound, Filter.Local(), true)?.Entity;
foreach (var (proto, weather) in state.Weather)
{
if (component.Weather.ContainsKey(proto))
continue;
if (!_audioQuery.TryComp(weather.Stream, out var audio))
return;
// New weather
StartWeather(uid, component, ProtoMan.Index<WeatherPrototype>(proto), weather.EndTime);
var occlusion = 0f;
// Work out tiles nearby to determine volume.
if (_gridQuery.TryComp(playerXform.GridUid, out var grid))
{
_roofQuery.TryComp(playerXform.GridUid, out var roofComp);
var gridId = playerXform.GridUid.Value;
// FloodFill to the nearest tile and use that for audio.
var seed = _mapSystem.GetTileRef(gridId, grid, playerXform.Coordinates);
var frontier = new Queue<TileRef>();
frontier.Enqueue(seed);
// If we don't have a nearest node don't play any sound.
EntityCoordinates? nearestNode = null;
var visited = new HashSet<Vector2i>();
while (frontier.TryDequeue(out var node))
{
if (!visited.Add(node.GridIndices))
continue;
if (!CanWeatherAffect((playerXform.GridUid.Value, grid, roofComp), node))
{
// Add neighbors
// TODO: Ideally we pick some deterministically random direction and use that
// We can't just do that naively here because it will flicker between nearby tiles.
for (var x = -1; x <= 1; x++)
{
for (var y = -1; y <= 1; y++)
{
if (Math.Abs(x) == 1 && Math.Abs(y) == 1 ||
x == 0 && y == 0 ||
(new Vector2(x, y) + node.GridIndices - seed.GridIndices).Length() > 3)
{
continue;
}
frontier.Enqueue(_mapSystem.GetTileRef(gridId, grid, new Vector2i(x, y) + node.GridIndices));
}
}
continue;
}
nearestNode = new EntityCoordinates(playerXform.GridUid.Value,
node.GridIndices + grid.TileSizeHalfVector);
break;
}
// Get occlusion to the targeted node if it exists, otherwise set a default occlusion.
if (nearestNode != null)
{
var entPos = _transform.GetMapCoordinates(playerXform);
var nodePosition = _transform.ToMapCoordinates(nearestNode.Value).Position;
var delta = nodePosition - entPos.Position;
var distance = delta.Length();
occlusion = _audio.GetOcclusion(entPos, delta, distance);
}
else
{
occlusion = 3f;
}
}
var alpha = GetWeatherPercent((uid, status));
alpha *= SharedAudioSystem.VolumeToGain(weather.Sound.Params.Volume);
_audio.SetGain(weather.Stream, alpha, audio);
audio.Occlusion = occlusion;
}
}
}

View File

@ -0,0 +1,89 @@
using Content.Server.Administration;
using Content.Shared.Administration;
using Content.Shared.Prototypes;
using Content.Shared.Weather;
using Robust.Shared.Console;
using Robust.Shared.Map;
using Robust.Shared.Prototypes;
namespace Content.Server.Weather.Commands;
/// <summary>
/// Add specific weather to map.
/// </summary>
[AdminCommand(AdminFlags.Fun)]
public sealed class WeatherAddCommand : LocalizedEntityCommands
{
[Dependency] private readonly IPrototypeManager _proto = default!;
[Dependency] private readonly SharedMapSystem _map = default!;
[Dependency] private readonly WeatherSystem _weather = default!;
[Dependency] private readonly IComponentFactory _compFactory = default!;
public override string Command => "weatheradd";
public override void Execute(IConsoleShell shell, string argStr, string[] args)
{
if (args.Length < 2)
{
shell.WriteError(Loc.GetString("cmd-weather-error-no-arguments"));
return;
}
//MapId parse
if (!int.TryParse(args[0], out var mapInt))
return;
var mapId = new MapId(mapInt);
if (!_map.MapExists(mapId))
{
shell.WriteError(Loc.GetString("cmd-weather-error-wrong-map", ("id", mapId.ToString())));
return;
}
//Weather proto parse
EntProtoId weatherProto = args[1];
if (!_proto.TryIndex(weatherProto, out _))
{
shell.WriteError(Loc.GetString("cmd-weather-error-unknown-proto"));
return;
}
//Time parsing
TimeSpan? duration = null;
if (args.Length == 3)
{
if (int.TryParse(args[2], out var durationInt))
duration = TimeSpan.FromSeconds(durationInt);
else
shell.WriteError(Loc.GetString("cmd-weather-error-wrong-time"));
}
_weather.TryAddWeather(mapId, weatherProto, out _, duration);
}
public override CompletionResult GetCompletion(IConsoleShell shell, string[] args)
{
if (args.Length == 1)
return CompletionResult.FromHintOptions(CompletionHelper.MapIds(EntityManager), Loc.GetString("cmd-weather-hint-map-id"));
if (args.Length == 2)
{
var opts = new List<CompletionOption>();
foreach (var proto in _proto.EnumeratePrototypes<EntityPrototype>())
{
if (!proto.HasComponent<WeatherStatusEffectComponent>(_compFactory))
continue;
opts.Add(new CompletionOption(proto.ID, proto.Name));
}
return CompletionResult.FromHintOptions(opts, Loc.GetString("cmd-weather-hint-prototype"));
}
if (args.Length == 3)
return CompletionResult.FromHint(Loc.GetString("cmd-weather-hint-time"));
return CompletionResult.Empty;
}
}

View File

@ -0,0 +1,82 @@
using Content.Server.Administration;
using Content.Shared.Administration;
using Content.Shared.Prototypes;
using Content.Shared.Weather;
using Robust.Shared.Console;
using Robust.Shared.Map;
using Robust.Shared.Prototypes;
namespace Content.Server.Weather.Commands;
/// <summary>
/// Remove specific weather from map.
/// </summary>
[AdminCommand(AdminFlags.Fun)]
public sealed class WeatherRemoveCommand : LocalizedEntityCommands
{
[Dependency] private readonly IPrototypeManager _proto = default!;
[Dependency] private readonly SharedMapSystem _map = default!;
[Dependency] private readonly WeatherSystem _weather = default!;
[Dependency] private readonly IComponentFactory _compFactory = default!;
public override string Command => "weatherremove";
public override void Execute(IConsoleShell shell, string argStr, string[] args)
{
if (args.Length < 2)
{
shell.WriteError(Loc.GetString("cmd-weather-error-no-arguments"));
return;
}
//MapId parse
if (!int.TryParse(args[0], out var mapInt))
return;
var mapId = new MapId(mapInt);
if (!_map.MapExists(mapId))
{
shell.WriteError(Loc.GetString("cmd-weather-error-wrong-map", ("id", mapId.ToString())));
return;
}
//Weather proto parse
EntProtoId weatherProto = args[1];
if (!_proto.TryIndex(weatherProto, out _))
{
shell.WriteError(Loc.GetString("cmd-weather-error-unknown-proto"));
return;
}
if (!_weather.HasWeather(mapId, weatherProto))
{
shell.WriteError(Loc.GetString("cmd-weather-error-no-weather"));
return;
}
_weather.TryRemoveWeather(mapId, weatherProto);
}
public override CompletionResult GetCompletion(IConsoleShell shell, string[] args)
{
if (args.Length == 1)
return CompletionResult.FromHintOptions(CompletionHelper.MapIds(EntityManager), Loc.GetString("cmd-weather-hint-map-id"));
if (args.Length == 2) //TODO: dont show ALL weathers here, only weathers applied to selected map
{
var opts = new List<CompletionOption>();
foreach (var proto in _proto.EnumeratePrototypes<EntityPrototype>())
{
if (!proto.HasComponent<WeatherStatusEffectComponent>(_compFactory))
continue;
opts.Add(new CompletionOption(proto.ID, proto.Name));
}
return CompletionResult.FromHintOptions(opts, Loc.GetString("cmd-weather-hint-prototype"));
}
return CompletionResult.Empty;
}
}

View File

@ -0,0 +1,91 @@
using Content.Server.Administration;
using Content.Shared.Administration;
using Content.Shared.Prototypes;
using Content.Shared.Weather;
using Robust.Shared.Console;
using Robust.Shared.Map;
using Robust.Shared.Prototypes;
namespace Content.Server.Weather.Commands;
/// <summary>
/// Removes all weather except the specified one. If the specified weather does not exist on the map, it adds it.
/// </summary>
[AdminCommand(AdminFlags.Fun)]
public sealed class WeatherSetCommand : LocalizedEntityCommands
{
[Dependency] private readonly IPrototypeManager _proto = default!;
[Dependency] private readonly SharedMapSystem _map = default!;
[Dependency] private readonly WeatherSystem _weather = default!;
[Dependency] private readonly IComponentFactory _compFactory = default!;
public override string Command => "weatherset";
public override void Execute(IConsoleShell shell, string argStr, string[] args)
{
if (args.Length < 2)
{
shell.WriteError(Loc.GetString("cmd-weather-error-no-arguments"));
return;
}
//MapId parse
if (!int.TryParse(args[0], out var mapInt))
return;
var mapId = new MapId(mapInt);
if (!_map.MapExists(mapId))
{
shell.WriteError(Loc.GetString("cmd-weather-error-wrong-map", ("id", mapId.ToString())));
return;
}
//Weather proto parse
EntProtoId? weatherProto = args[1];
if (args[1] == "null")
weatherProto = null;
else if (!_proto.TryIndex(weatherProto, out _))
{
shell.WriteError(Loc.GetString("cmd-weather-error-unknown-proto"));
return;
}
//Time parsing
TimeSpan? duration = null;
if (args.Length == 3)
{
if (int.TryParse(args[2], out var durationInt))
duration = TimeSpan.FromSeconds(durationInt);
else
shell.WriteError(Loc.GetString("cmd-weather-error-wrong-time"));
}
_weather.TrySetWeather(mapId, weatherProto, out _, duration);
}
public override CompletionResult GetCompletion(IConsoleShell shell, string[] args)
{
if (args.Length == 1)
return CompletionResult.FromHintOptions(CompletionHelper.MapIds(EntityManager), Loc.GetString("cmd-weather-hint-map-id"));
if (args.Length == 2)
{
var opts = new List<CompletionOption>();
foreach (var proto in _proto.EnumeratePrototypes<EntityPrototype>())
{
if (!proto.HasComponent<WeatherStatusEffectComponent>(_compFactory))
continue;
opts.Add(new CompletionOption(proto.ID, proto.Name));
}
return CompletionResult.FromHintOptions(opts, Loc.GetString("cmd-weather-hint-prototype"));
}
if (args.Length == 3)
return CompletionResult.FromHint(Loc.GetString("cmd-weather-hint-time"));
return CompletionResult.Empty;
}
}

View File

@ -1,92 +1,30 @@
using Content.Server.Administration;
using Content.Shared.Administration;
using Content.Shared.Weather;
using Robust.Shared.Console;
using Robust.Shared.GameStates;
using Robust.Shared.Map;
using System.Linq;
using Robust.Server.GameStates;
namespace Content.Server.Weather;
public sealed class WeatherSystem : SharedWeatherSystem
{
[Dependency] private readonly IConsoleHost _console = default!;
[Dependency] private readonly SharedMapSystem _mapSystem = default!;
//I dont really like to PVS override weather entities, but map status effect containers dont PVS-ing out of the box
[Dependency] private readonly PvsOverrideSystem _pvs = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<WeatherComponent, ComponentGetState>(OnWeatherGetState);
_console.RegisterCommand("weather",
Loc.GetString("cmd-weather-desc"),
Loc.GetString("cmd-weather-help"),
WeatherTwo,
WeatherCompletion);
SubscribeLocalEvent<WeatherStatusEffectComponent, ComponentInit>(OnCompInit);
SubscribeLocalEvent<WeatherStatusEffectComponent, ComponentShutdown>(OnCompShutdown);
}
private void OnWeatherGetState(EntityUid uid, WeatherComponent component, ref ComponentGetState args)
private void OnCompInit(Entity<WeatherStatusEffectComponent> ent, ref ComponentInit args)
{
args.State = new WeatherComponentState(component.Weather);
// The map entitiy itself is networked by PVS if the player is on that map but not anything inside a container,
// So we need to add an overridce to make sure the client sees it.
_pvs.AddGlobalOverride(ent);
}
[AdminCommand(AdminFlags.Fun)]
private void WeatherTwo(IConsoleShell shell, string argStr, string[] args)
private void OnCompShutdown(Entity<WeatherStatusEffectComponent> ent, ref ComponentShutdown args)
{
if (args.Length < 2)
{
shell.WriteError(Loc.GetString("cmd-weather-error-no-arguments"));
return;
}
if (!int.TryParse(args[0], out var mapInt))
return;
var mapId = new MapId(mapInt);
if (!_mapSystem.MapExists(mapId))
return;
if (!_mapSystem.TryGetMap(mapId, out var mapUid))
return;
var weatherComp = EnsureComp<WeatherComponent>(mapUid.Value);
//Weather Proto parsing
WeatherPrototype? weather = null;
if (!args[1].Equals("null"))
{
if (!ProtoMan.TryIndex(args[1], out weather))
{
shell.WriteError(Loc.GetString("cmd-weather-error-unknown-proto"));
return;
}
}
//Time parsing
TimeSpan? endTime = null;
if (args.Length == 3)
{
var curTime = Timing.CurTime;
if (int.TryParse(args[2], out var durationInt))
{
endTime = curTime + TimeSpan.FromSeconds(durationInt);
}
else
{
shell.WriteError(Loc.GetString("cmd-weather-error-wrong-time"));
}
}
SetWeather(mapId, weather, endTime);
}
private CompletionResult WeatherCompletion(IConsoleShell shell, string[] args)
{
if (args.Length == 1)
return CompletionResult.FromHintOptions(CompletionHelper.MapIds(EntityManager), "Map Id");
var a = CompletionHelper.PrototypeIDs<WeatherPrototype>(true, ProtoMan);
var b = a.Concat(new[] { new CompletionOption("null", Loc.GetString("cmd-weather-null")) });
return CompletionResult.FromHintOptions(b, Loc.GetString("cmd-weather-hint"));
_pvs.RemoveGlobalOverride(ent);
}
}

View File

@ -20,8 +20,8 @@ public sealed partial class SalvageWeatherMod : IPrototype, IBiomeSpecificMod
public List<ProtoId<SalvageBiomeModPrototype>>? Biomes { get; private set; } = null;
/// <summary>
/// Weather prototype to use on the planet.
/// Weather status effect prototype to use on the planet.
/// </summary>
[DataField("weather", required: true, customTypeSerializer:typeof(PrototypeIdSerializer<WeatherPrototype>))]
public string WeatherPrototype = string.Empty;
[DataField("weather", required: true)]
public EntProtoId WeatherPrototype = string.Empty;
}

View File

@ -50,4 +50,10 @@ public sealed partial class StatusEffectComponent : Component
/// </summary>
[DataField]
public EntityWhitelist? Blacklist;
/// <summary>
/// QoL function, returns total duration of this status effect.
/// </summary>
[ViewVariables]
public TimeSpan Duration => EndEffectTime == null ? TimeSpan.MaxValue : EndEffectTime.Value - StartEffectTime;
}

View File

@ -226,7 +226,7 @@ public sealed partial class StatusEffectsSystem : EntitySystem
var endTime = delay == null ? _timing.CurTime + duration : _timing.CurTime + delay + duration;
SetStatusEffectEndTime((effect.Value, effectComp), endTime);
var startTime = delay == null ? TimeSpan.Zero : _timing.CurTime + delay.Value;
var startTime = delay == null ? _timing.CurTime : _timing.CurTime + delay.Value;
SetStatusEffectStartTime(effect.Value, startTime);
TryApplyStatusEffect((statusEffect.Value, effectComp));

View File

@ -12,10 +12,10 @@ namespace Content.Shared.Trigger.Components.Effects;
public sealed partial class WeatherOnTriggerComponent : BaseXOnTriggerComponent
{
/// <summary>
/// Weather type. Null to clear the weather.
/// Weather status effect proto. Null to clear the weather.
/// </summary>
[DataField, AutoNetworkedField]
public ProtoId<WeatherPrototype>? Weather;
public EntProtoId? Weather;
/// <summary>
/// How long the weather should last. Null for forever.

View File

@ -17,13 +17,13 @@ public sealed class WeatherTriggerSystem : XOnTriggerSystem<WeatherOnTriggerComp
if (ent.Comp.Weather == null) //Clear weather if nothing is set
{
_weather.SetWeather(xform.MapID, null, null);
_weather.TrySetWeather(xform.MapID, null, out _);
return;
}
var endTime = ent.Comp.Duration == null ? null : ent.Comp.Duration + _timing.CurTime;
if (_prototypeManager.Resolve(ent.Comp.Weather, out var weatherPrototype))
_weather.SetWeather(xform.MapID, weatherPrototype, endTime);
_weather.TrySetWeather(xform.MapID, weatherPrototype, out _, endTime);
}
}

View File

@ -1,11 +1,13 @@
using System.Diagnostics.CodeAnalysis;
using Content.Shared.Light.Components;
using Content.Shared.Light.EntitySystems;
using Content.Shared.Maps;
using Content.Shared.StatusEffectNew;
using Content.Shared.StatusEffectNew.Components;
using Robust.Shared.Audio.Systems;
using Robust.Shared.Map;
using Robust.Shared.Map.Components;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization;
using Robust.Shared.Timing;
namespace Content.Shared.Weather;
@ -14,236 +16,193 @@ public abstract class SharedWeatherSystem : EntitySystem
{
[Dependency] protected readonly IGameTiming Timing = default!;
[Dependency] protected readonly IPrototypeManager ProtoMan = default!;
[Dependency] protected readonly SharedAudioSystem Audio = default!;
[Dependency] private readonly ITileDefinitionManager _tileDefManager = default!;
[Dependency] private readonly MetaDataSystem _metadata = default!;
[Dependency] private readonly SharedAudioSystem _audio = default!;
[Dependency] private readonly SharedMapSystem _mapSystem = default!;
[Dependency] private readonly SharedRoofSystem _roof = default!;
[Dependency] private readonly StatusEffectsSystem _statusEffects = default!;
private EntityQuery<BlockWeatherComponent> _blockQuery;
private EntityQuery<WeatherStatusEffectComponent> _weatherQuery;
public static readonly TimeSpan StartupTime = TimeSpan.FromSeconds(15);
public static readonly TimeSpan ShutdownTime = TimeSpan.FromSeconds(15);
public override void Initialize()
{
base.Initialize();
_blockQuery = GetEntityQuery<BlockWeatherComponent>();
SubscribeLocalEvent<WeatherComponent, EntityUnpausedEvent>(OnWeatherUnpaused);
_weatherQuery = GetEntityQuery<WeatherStatusEffectComponent>();
}
private void OnWeatherUnpaused(EntityUid uid, WeatherComponent component, ref EntityUnpausedEvent args)
{
foreach (var weather in component.Weather.Values)
{
weather.StartTime += args.PausedTime;
if (weather.EndTime != null)
weather.EndTime = weather.EndTime.Value + args.PausedTime;
}
component.NextUpdate += args.PausedTime; // DeltaV
}
public bool CanWeatherAffect(EntityUid uid, MapGridComponent grid, TileRef tileRef, RoofComponent? roofComp = null)
public bool CanWeatherAffect(Entity<MapGridComponent?, RoofComponent?> ent, TileRef tileRef)
{
if (tileRef.Tile.IsEmpty)
return true;
if (Resolve(uid, ref roofComp, false) && _roof.IsRooved((uid, grid, roofComp), tileRef.GridIndices))
if (!Resolve(ent, ref ent.Comp1))
return false;
var tileDef = (ContentTileDefinition) _tileDefManager[tileRef.Tile.TypeId];
if (Resolve(ent, ref ent.Comp2, false) && _roof.IsRooved((ent, ent.Comp1, ent.Comp2), tileRef.GridIndices))
return false;
var tileDef = (ContentTileDefinition)_tileDefManager[tileRef.Tile.TypeId];
if (!tileDef.Weather)
return false;
var anchoredEntities = _mapSystem.GetAnchoredEntitiesEnumerator(uid, grid, tileRef.GridIndices);
var anchoredEntities = _mapSystem.GetAnchoredEntitiesEnumerator(ent, ent.Comp1, tileRef.GridIndices);
while (anchoredEntities.MoveNext(out var ent))
while (anchoredEntities.MoveNext(out var anchored))
{
if (_blockQuery.HasComponent(ent.Value))
if (_blockQuery.HasComponent(anchored.Value))
return false;
}
return true;
}
public float GetPercent(WeatherData component, EntityUid mapUid)
/// <summary>
/// Calculates the current “strength” of the specified weather based on the duration of the status effect.
/// Between 0 and 1.
/// </summary>
public float GetWeatherPercent(Entity<StatusEffectComponent> ent)
{
var pauseTime = _metadata.GetPauseTime(mapUid);
var elapsed = Timing.CurTime - (component.StartTime + pauseTime);
var duration = component.Duration;
var elapsed = Timing.CurTime - ent.Comp.StartEffectTime;
var duration = ent.Comp.Duration;
var remaining = duration - elapsed;
float alpha;
if (remaining < WeatherComponent.ShutdownTime)
{
alpha = (float) (remaining / WeatherComponent.ShutdownTime);
}
else if (elapsed < WeatherComponent.StartupTime)
{
alpha = (float) (elapsed / WeatherComponent.StartupTime);
}
if (remaining < ShutdownTime)
return (float)(remaining / ShutdownTime);
else if (elapsed < StartupTime)
return (float)(elapsed / StartupTime);
else
{
alpha = 1f;
}
return alpha;
}
public override void Update(float frameTime)
{
base.Update(frameTime);
if (!Timing.IsFirstTimePredicted)
return;
var curTime = Timing.CurTime;
var query = EntityQueryEnumerator<WeatherComponent>();
while (query.MoveNext(out var uid, out var comp))
{
if (comp.Weather.Count == 0)
continue;
foreach (var (proto, weather) in comp.Weather)
{
var endTime = weather.EndTime;
// Ended
if (endTime != null && endTime < curTime)
{
EndWeather(uid, comp, proto);
continue;
}
var remainingTime = endTime - curTime;
// Admin messed up or the likes.
if (!ProtoMan.TryIndex<WeatherPrototype>(proto, out var weatherProto))
{
Log.Error($"Unable to find weather prototype for {comp.Weather}, ending!");
EndWeather(uid, comp, proto);
continue;
}
// Shutting down
if (endTime != null && remainingTime < WeatherComponent.ShutdownTime)
{
SetState(uid, WeatherState.Ending, comp, weather, weatherProto);
}
// Starting up
else
{
var startTime = weather.StartTime;
var elapsed = Timing.CurTime - startTime;
if (elapsed < WeatherComponent.StartupTime)
{
SetState(uid, WeatherState.Starting, comp, weather, weatherProto);
}
else // DeltaV: Set state to Running when it finishes the starting time
{
SetState(uid, WeatherState.Running, comp, weather, weatherProto);
}
}
// Run whatever code we need.
Run(uid, weather, weatherProto, frameTime);
}
}
return 1f;
}
/// <summary>
/// Shuts down all existing weather and starts the new one if applicable.
/// Attempts to add a new weather status effect to the specified map.
/// Does not remove or replace any other existing weather effects on the map.
/// If the specified weather effect already exists, its duration will be overridden.
/// </summary>
public void SetWeather(MapId mapId, WeatherPrototype? proto, TimeSpan? endTime)
/// <param name="mapId">The <see cref="MapId"/> of the target map to apply the weather effect to.</param>
/// <param name="weatherProto">The prototype ID (<see cref="EntProtoId"/>) of the weather status effect to add.</param>
/// <param name="weatherEnt">When this method returns, contains the <see cref="EntityUid"/> of the weather entity if the operation succeeded; otherwise, <c>null</c>.</param>
/// <param name="duration">Optional. The duration for which the weather should exist on the map. If <c>null</c>, the weather will persist indefinitely.</param>
/// <returns><c>true</c> if the weather was successfully added or updated; otherwise, <c>false</c>.</returns>
public bool TryAddWeather(MapId mapId, EntProtoId weatherProto, [NotNullWhen(true)] out EntityUid? weatherEnt, TimeSpan? duration = null)
{
weatherEnt = null;
if (!_mapSystem.TryGetMap(mapId, out var mapUid))
return;
var weatherComp = EnsureComp<WeatherComponent>(mapUid.Value);
foreach (var (eProto, weather) in weatherComp.Weather)
{
// if we turn off the weather, we don't want endTime = null
if (proto == null)
endTime ??= Timing.CurTime + WeatherComponent.ShutdownTime;
// Reset cooldown if it's an existing one.
if (proto is not null && eProto == proto.ID)
{
weather.EndTime = endTime;
if (weather.State == WeatherState.Ending)
weather.State = WeatherState.Running;
Dirty(mapUid.Value, weatherComp);
continue;
}
// Speedrun
var end = Timing.CurTime + WeatherComponent.ShutdownTime;
if (weather.EndTime == null || weather.EndTime > end)
{
weather.EndTime = end;
Dirty(mapUid.Value, weatherComp);
}
}
if (proto != null)
StartWeather(mapUid.Value, weatherComp, proto, endTime);
}
/// <summary>
/// Run every tick when the weather is running.
/// </summary>
protected virtual void Run(EntityUid uid, WeatherData weather, WeatherPrototype weatherProto, float frameTime) { }
protected void StartWeather(EntityUid uid, WeatherComponent component, WeatherPrototype weather, TimeSpan? endTime)
{
if (component.Weather.ContainsKey(weather.ID))
return;
var data = new WeatherData()
{
StartTime = Timing.CurTime,
EndTime = endTime,
};
component.Weather.Add(weather.ID, data);
Dirty(uid, component);
}
protected virtual void EndWeather(EntityUid uid, WeatherComponent component, string proto)
{
if (!component.Weather.TryGetValue(proto, out var data))
return;
_audio.Stop(data.Stream);
data.Stream = null;
component.Weather.Remove(proto);
Dirty(uid, component);
}
protected virtual bool SetState(EntityUid uid, WeatherState state, WeatherComponent component, WeatherData weather, WeatherPrototype weatherProto)
{
if (weather.State.Equals(state))
return false;
weather.State = state;
Dirty(uid, component);
return true;
return TryAddWeather(mapUid.Value, weatherProto, out weatherEnt, duration);
}
[Serializable, NetSerializable]
protected sealed class WeatherComponentState : ComponentState
/// <summary>
/// Adds a new weather to a map. Does not remove other existing weathers. If this type of weather already exists, it simply overrides its duration.
/// </summary>
/// <param name="mapUid">Target map entity</param>
/// <param name="weatherProto">EntProtoId of weather status effect</param>
/// <param name="weatherEnt">When this method returns, contains the <see cref="EntityUid"/> of the weather entity if the operation succeeded; otherwise, <c>null</c>.</param>
/// <param name="duration">How long this weather should exist on the map? If null - infinite duration</param>
public bool TryAddWeather(EntityUid mapUid, EntProtoId weatherProto, [NotNullWhen(true)] out EntityUid? weatherEnt, TimeSpan? duration = null)
{
public Dictionary<ProtoId<WeatherPrototype>, WeatherData> Weather;
return _statusEffects.TrySetStatusEffectDuration(mapUid, weatherProto, out weatherEnt, duration);
}
public WeatherComponentState(Dictionary<ProtoId<WeatherPrototype>, WeatherData> weather)
/// <summary>
/// Checks if a specific weather exists on the given map.
/// </summary>
/// <param name="mapId">Target mapId</param>
/// <param name="weatherProto">EntProtoId of weather status effect</param>
/// <returns>True if the weather exists, otherwise false</returns>
public bool HasWeather(MapId mapId, EntProtoId weatherProto)
{
if (!_mapSystem.TryGetMap(mapId, out var mapUid))
return false;
return _statusEffects.TryGetStatusEffect(mapUid.Value, weatherProto, out _);
}
/// <summary>
/// Slowly remove weather from a map. It should be gone after <see cref="ShutdownTime"/> seconds.
/// </summary>
/// <param name="mapId">Target mapId</param>
/// <param name="weatherProto">EntProtoId of weather status effect</param>
public bool TryRemoveWeather(MapId mapId, EntProtoId weatherProto)
{
if (!_mapSystem.TryGetMap(mapId, out var mapUid))
return false;
return TryRemoveWeather(mapUid.Value, weatherProto);
}
/// <summary>
/// Slowly remove weather from map. It should be gone after <see cref="ShutdownTime"/> seconds.
/// </summary>
/// <param name="mapUid">Target entity map</param>
/// <param name="weatherProto">EntProtoId of weather status effect</param>
public bool TryRemoveWeather(EntityUid mapUid, EntProtoId weatherProto)
{
if (!_statusEffects.TryGetStatusEffect(mapUid, weatherProto, out var weatherEnt))
return false;
if (!_weatherQuery.HasComp(weatherEnt))
return false;
return _statusEffects.TrySetStatusEffectDuration(mapUid, weatherProto, ShutdownTime);
}
/// <summary>
/// Removes all weather conditions except the specified one. If the specified weather does not exist on the map, it adds it.
/// Returns true if the specified weather is present or was added, false otherwise.
/// </summary>
/// <param name="mapId">Target mapId</param>
/// <param name="weatherProto">EntProtoId of weather status effect</param>
/// <param name="weatherEnt">When this method returns, contains the <see cref="EntityUid"/> of the weather entity if the operation succeeded; otherwise, <c>null</c>.</param>
/// <param name="duration">How long this weather should exist on map? If null - infinite duration</param>
/// <returns><c>true</c> if the specified weather is present or was added; otherwise, <c>false</c>.</returns>
public bool TrySetWeather(MapId mapId, EntProtoId? weatherProto, out EntityUid? weatherEnt, TimeSpan? duration = null)
{
weatherEnt = null;
if (!_mapSystem.TryGetMap(mapId, out var mapUid))
return false;
// Remove all other weather effects except the specified one
if (_statusEffects.TryEffectsWithComp<WeatherStatusEffectComponent>(mapUid, out var effects))
{
Weather = weather;
foreach (var effect in effects)
{
var effectProto = Prototype(effect);
if (effectProto is null)
continue;
if (effectProto != weatherProto)
{
TryRemoveWeather(mapUid.Value, effectProto);
}
else
{
weatherEnt = effect;
}
}
}
// If weatherProto is null, we just removed all weather and return true
if (weatherProto is null)
return true;
// If the specified weather already exists, just update its duration
if (weatherEnt != null)
{
TryAddWeather(mapUid.Value, weatherProto.Value, out weatherEnt, duration);
return true;
}
// Otherwise, add the specified weather
return TryAddWeather(mapUid.Value, weatherProto.Value, out weatherEnt, duration);
}
}

View File

@ -1,65 +0,0 @@
using Robust.Shared.GameStates;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
namespace Content.Shared.Weather;
[RegisterComponent, NetworkedComponent]
public sealed partial class WeatherComponent : Component
{
/// <summary>
/// Currently running weathers
/// </summary>
[DataField]
public Dictionary<ProtoId<WeatherPrototype>, WeatherData> Weather = new();
/// <summary>
/// DeltaV: How long to wait between updating weather effects.
/// </summary>
[DataField]
public TimeSpan UpdateDelay = TimeSpan.FromSeconds(1);
/// <summary>
/// DeltaV: When to next update weather effects (damage).
/// </summary>
[DataField(customTypeSerializer: typeof(TimeOffsetSerializer))]
public TimeSpan NextUpdate = TimeSpan.Zero;
public static readonly TimeSpan StartupTime = TimeSpan.FromSeconds(15);
public static readonly TimeSpan ShutdownTime = TimeSpan.FromSeconds(15);
}
[DataDefinition, Serializable, NetSerializable]
public sealed partial class WeatherData
{
// Client audio stream.
[NonSerialized]
public EntityUid? Stream;
/// <summary>
/// When the weather started if relevant.
/// </summary>
[DataField(customTypeSerializer: typeof(TimeOffsetSerializer))] //TODO: Remove Custom serializer
public TimeSpan StartTime = TimeSpan.Zero;
/// <summary>
/// When the applied weather will end.
/// </summary>
[DataField(customTypeSerializer: typeof(TimeOffsetSerializer))] //TODO: Remove Custom serializer
public TimeSpan? EndTime;
[ViewVariables]
public TimeSpan Duration => EndTime == null ? TimeSpan.MaxValue : EndTime.Value - StartTime;
[DataField]
public WeatherState State = WeatherState.Invalid;
}
public enum WeatherState : byte
{
Invalid = 0,
Starting,
Running,
Ending,
}

View File

@ -1,38 +0,0 @@
using Content.Shared.Damage;
using Content.Shared.Whitelist;
using Robust.Shared.Audio;
using Robust.Shared.Prototypes;
using Robust.Shared.Utility;
namespace Content.Shared.Weather;
[Prototype]
public sealed partial class WeatherPrototype : IPrototype
{
[IdDataField] public string ID { get; private set; } = default!;
[ViewVariables(VVAccess.ReadWrite), DataField("sprite", required: true)]
public SpriteSpecifier Sprite = default!;
[ViewVariables(VVAccess.ReadWrite), DataField("color")]
public Color? Color;
/// <summary>
/// Sound to play on the affected areas.
/// </summary>
[ViewVariables(VVAccess.ReadWrite), DataField("sound")]
public SoundSpecifier? Sound;
/// <summary>
/// DeltaV: Damage you can take from being in this weather.
/// Only applies when weather has fully set in.
/// </summary>
[DataField]
public DamageSpecifier? Damage;
/// <summary>
/// DeltaV: Don't damage entities that match this blacklist.
/// </summary>
[DataField]
public EntityWhitelist? DamageBlacklist;
}

View File

@ -0,0 +1,46 @@
using System.Numerics;
using Content.Shared.StatusEffectNew.Components;
using Robust.Shared.Audio;
using Robust.Shared.GameStates;
using Robust.Shared.Utility;
namespace Content.Shared.Weather;
/// <summary>
/// Used only in conjure with <see cref="StatusEffectComponent"/> for status effects applied to map entities.
/// Contains basic information about all types of weather effects.
/// </summary>
[RegisterComponent, NetworkedComponent, Access(typeof(SharedWeatherSystem))]
public sealed partial class WeatherStatusEffectComponent : Component
{
/// <summary>
/// A texture that will tile and render as a weather effect across the entire map.
/// </summary>
[DataField(required: true)]
public SpriteSpecifier Sprite = default!;
/// <summary>
/// Tint that will be applied to the weather texture.
/// </summary>
[DataField]
public Color? Color;
/// <summary>
/// Weather scrolling speed.
/// </summary>
[DataField]
public Vector2? Scrolling;
/// <summary>
/// Sound to play on the affected areas.
/// </summary>
[DataField]
public SoundSpecifier? Sound;
/// <summary>
/// Client audio stream.
/// Not used on the server.
/// </summary>
[ViewVariables]
public EntityUid? Stream;
}

View File

@ -1,8 +1,17 @@
cmd-weather-desc = Sets the weather for the current map.
cmd-weather-help = weather <mapId> <prototype / null>
cmd-weather-hint = Weather prototype
cmd-weather-null = Clears the weather
cmd-weatherremove-desc = Remove specific weather from map.
cmd-weatherset-desc = Removes all weather except the specified one. If the specified weather does not exist on the map, it adds it.
cmd-weatheradd-desc = Add specific weather to map.
cmd-weatherremove-help = weatherremove <mapId> <prototype>
cmd-weatherset-help = weatherset <mapId> <prototype / null>
cmd-weatheradd-help = weatheradd <mapId> <prototype / null>
cmd-weather-error-no-arguments = Not enough arguments!
cmd-weather-error-unknown-proto = Unknown Weather prototype!
cmd-weather-error-wrong-time = Time is in the wrong format!
cmd-weather-error-wrong-time = Time is in the wrong format!
cmd-weather-error-wrong-map = Map with MapId {$id} doesn't exist!
cmd-weather-error-no-weather = This weather does not exist on the selected map!
cmd-weather-hint-map-id = Map Id
cmd-weather-hint-prototype = Weather entity prototype
cmd-weather-hint-time = Duration in seconds (leave empty for infinite duration)

View File

@ -0,0 +1,178 @@
- type: entity
parent: StatusEffectBase
id: WeatherBase
abstract: true
components:
- type: WeatherStatusEffect
- type: StatusEffect
whitelist:
components:
- Map
- type: entity
parent: WeatherBase
id: WeatherRain
components:
- type: WeatherStatusEffect
sprite:
sprite: /Textures/Effects/weather.rsi
state: rain
sound:
collection: Rain
params:
loop: true
volume: -6
- type: entity
parent: WeatherBase
id: WeatherAshfall
components:
- type: WeatherStatusEffect
sprite:
sprite: /Textures/Effects/weather.rsi
state: ashfall
sound:
path: /Audio/Effects/Weather/snowstorm_weak.ogg
params:
loop: true
volume: -6
- type: entity
parent: WeatherBase
id: WeatherAshfallLight
components:
- type: WeatherStatusEffect
sprite:
sprite: /Textures/Effects/weather.rsi
state: ashfall_light
sound:
path: /Audio/Effects/Weather/snowstorm_weak.ogg
params:
loop: true
volume: -6
- type: entity
parent: WeatherBase
id: WeatherAshfallHeavy
components:
- type: WeatherStatusEffect
sprite:
sprite: /Textures/Effects/weather.rsi
state: ashfall_heavy
sound:
path: /Audio/Effects/Weather/snowstorm.ogg
params:
loop: true
volume: -6
- type: entity
parent: WeatherBase
id: WeatherFallout
components:
- type: WeatherStatusEffect
sprite:
sprite: /Textures/Effects/weather.rsi
state: fallout
sound:
path: /Audio/Effects/Weather/snowstorm_weak.ogg
params:
loop: true
volume: -6
- type: entity
parent: WeatherBase
id: WeatherHail
components:
- type: WeatherStatusEffect
sprite:
sprite: /Textures/Effects/weather.rsi
state: hail
sound:
path: /Audio/Effects/Weather/rain.ogg
params:
loop: true
volume: -6
- type: entity
parent: WeatherBase
id: WeatherSandstorm
components:
- type: WeatherStatusEffect
sprite:
sprite: /Textures/Effects/weather.rsi
state: sandstorm
sound:
path: /Audio/Effects/Weather/snowstorm_weak.ogg
params:
loop: true
volume: -6
- type: entity
parent: WeatherBase
id: WeatherSandstormHeavy
components:
- type: WeatherStatusEffect
sprite:
sprite: /Textures/Effects/weather.rsi
state: sandstorm_heavy
sound:
path: /Audio/Effects/Weather/snowstorm.ogg
params:
loop: true
volume: -6
- type: entity
parent: WeatherBase
id: WeatherSnowfallLight
components:
- type: WeatherStatusEffect
sprite:
sprite: /Textures/Effects/weather.rsi
state: snowfall_light
sound:
path: /Audio/Effects/Weather/snowstorm_weak.ogg
params:
loop: true
volume: -6
- type: entity
parent: WeatherBase
id: WeatherSnowfallMedium
components:
- type: WeatherStatusEffect
sprite:
sprite: /Textures/Effects/weather.rsi
state: snowfall_med
sound:
path: /Audio/Effects/Weather/snowstorm_weak.ogg
params:
loop: true
volume: -6
- type: entity
parent: WeatherBase
id: WeatherSnowfallHeavy
components:
- type: WeatherStatusEffect
sprite:
sprite: /Textures/Effects/weather.rsi
state: snowfall_heavy
sound:
path: /Audio/Effects/Weather/snowstorm.ogg
params:
loop: true
volume: -6
- type: entity
parent: WeatherBase
id: WeatherStorm
components:
- type: WeatherStatusEffect
sprite:
sprite: /Textures/Effects/weather.rsi
state: storm
sound:
path: /Audio/Effects/Weather/rain_heavy.ogg
params:
loop: true
volume: -6

View File

@ -0,0 +1,5 @@
- type: soundCollection
id: Rain
files:
- /Audio/Effects/Weather/rain.ogg
- /Audio/Effects/Weather/rain2.ogg

View File

@ -1,144 +0,0 @@
- type: weather
id: Ashfall
sprite:
sprite: /Textures/Effects/weather.rsi
state: ashfall
sound:
path: /Audio/Effects/Weather/snowstorm_weak.ogg
params:
loop: true
volume: -6
damage: # DeltaV
types:
Heat: 4
damageBlacklist: # DeltaV
components:
- AshStormImmune
- type: weather
id: AshfallLight
sprite:
sprite: /Textures/Effects/weather.rsi
state: ashfall_light
sound:
path: /Audio/Effects/Weather/snowstorm_weak.ogg
params:
loop: true
volume: -6
- type: weather
id: AshfallHeavy
sprite:
sprite: /Textures/Effects/weather.rsi
state: ashfall_heavy
sound:
path: /Audio/Effects/Weather/snowstorm.ogg
params:
loop: true
volume: -6
- type: weather
id: Fallout
sprite:
sprite: /Textures/Effects/weather.rsi
state: fallout
sound:
path: /Audio/Effects/Weather/snowstorm_weak.ogg
params:
loop: true
volume: -6
- type: weather
id: Hail
sprite:
sprite: /Textures/Effects/weather.rsi
state: hail
sound:
path:
/Audio/Effects/Weather/rain.ogg
params:
loop: true
volume: -6
- type: weather
id: Rain
sprite:
sprite: /Textures/Effects/weather.rsi
state: rain
sound:
collection: Rain
params:
loop: true
volume: -6
- type: soundCollection
id: Rain
files:
- /Audio/Effects/Weather/rain.ogg
- /Audio/Effects/Weather/rain2.ogg
- type: weather
id: Sandstorm
sprite:
sprite: /Textures/Effects/weather.rsi
state: sandstorm
sound:
path: /Audio/Effects/Weather/snowstorm_weak.ogg
params:
loop: true
volume: -6
- type: weather
id: SandstormHeavy
sprite:
sprite: /Textures/Effects/weather.rsi
state: sandstorm_heavy
sound:
path: /Audio/Effects/Weather/snowstorm.ogg
params:
loop: true
volume: -6
- type: weather
id: SnowfallLight
sprite:
sprite: /Textures/Effects/weather.rsi
state: snowfall_light
sound:
path: /Audio/Effects/Weather/snowstorm_weak.ogg
params:
loop: true
volume: -6
- type: weather
id: SnowfallMedium
sprite:
sprite: /Textures/Effects/weather.rsi
state: snowfall_med
sound:
path: /Audio/Effects/Weather/snowstorm_weak.ogg
params:
loop: true
volume: -6
- type: weather
id: SnowfallHeavy
sprite:
sprite: /Textures/Effects/weather.rsi
state: snowfall_heavy
sound:
path: /Audio/Effects/Weather/snowstorm.ogg
params:
loop: true
volume: -6
- type: weather
id: Storm
sprite:
sprite: /Textures/Effects/weather.rsi
state: storm
sound:
path: /Audio/Effects/Weather/rain_heavy.ogg
params:
loop: true
volume: -6