diff --git a/Content.Client/Parallax/ParallaxOverlay.cs b/Content.Client/Parallax/ParallaxOverlay.cs index 1f401cde91..5e63bfca0a 100644 --- a/Content.Client/Parallax/ParallaxOverlay.cs +++ b/Content.Client/Parallax/ParallaxOverlay.cs @@ -21,6 +21,7 @@ public sealed class ParallaxOverlay : Overlay public ParallaxOverlay() { + ZIndex = ParallaxSystem.ParallaxZIndex; IoCManager.InjectDependencies(this); _parallax = IoCManager.Resolve().GetEntitySystem(); diff --git a/Content.Client/Parallax/ParallaxSystem.cs b/Content.Client/Parallax/ParallaxSystem.cs index d3320ce1f9..96d756aa7f 100644 --- a/Content.Client/Parallax/ParallaxSystem.cs +++ b/Content.Client/Parallax/ParallaxSystem.cs @@ -15,6 +15,7 @@ public sealed class ParallaxSystem : SharedParallaxSystem [Dependency] private readonly IPrototypeManager _protoManager = default!; private const string Fallback = "Default"; + public const int ParallaxZIndex = 0; public override void Initialize() { diff --git a/Content.Client/Weather/WeatherOverlay.cs b/Content.Client/Weather/WeatherOverlay.cs new file mode 100644 index 0000000000..c85fe822d2 --- /dev/null +++ b/Content.Client/Weather/WeatherOverlay.cs @@ -0,0 +1,207 @@ +using System.Linq; +using Content.Client.Parallax; +using Content.Shared.Weather; +using OpenToolkit.Graphics.ES11; +using Robust.Client.GameObjects; +using Robust.Client.Graphics; +using Robust.Client.ResourceManagement; +using Robust.Client.Utility; +using Robust.Shared.Enums; +using Robust.Shared.Map; +using Robust.Shared.Physics.Components; +using Robust.Shared.Prototypes; +using Robust.Shared.Timing; +using Robust.Shared.Utility; + +namespace Content.Client.Weather; + +public sealed class WeatherOverlay : Overlay +{ + [Dependency] private readonly IClyde _clyde = default!; + [Dependency] private readonly IEntityManager _entManager = default!; + [Dependency] private readonly IGameTiming _timing = default!; + [Dependency] private readonly IMapManager _mapManager = default!; + [Dependency] private readonly IPrototypeManager _protoManager = default!; + [Dependency] private readonly IResourceCache _cache = default!; + private readonly SharedTransformSystem _transform; + private readonly SpriteSystem _sprite; + private readonly WeatherSystem _weather; + + public override OverlaySpace Space => OverlaySpace.WorldSpaceBelowFOV; + + private IRenderTexture? _blep; + + public WeatherOverlay(SharedTransformSystem transform, SpriteSystem sprite, WeatherSystem weather) + { + ZIndex = ParallaxSystem.ParallaxZIndex + 1; + _transform = transform; + _weather = weather; + _sprite = sprite; + IoCManager.InjectDependencies(this); + } + + protected override bool BeforeDraw(in OverlayDrawArgs args) + { + if (args.MapId == MapId.Nullspace) + return false; + + if (!_entManager.TryGetComponent(_mapManager.GetMapEntityId(args.MapId), out var weather) || + weather.Weather == null) + { + return false; + } + + return base.BeforeDraw(in args); + } + + protected override void Draw(in OverlayDrawArgs args) + { + var mapUid = _mapManager.GetMapEntityId(args.MapId); + + if (!_entManager.TryGetComponent(mapUid, out var weather) || + weather.Weather == null || + !_protoManager.TryIndex(weather.Weather, out var weatherProto)) + { + return; + } + + var alpha = _weather.GetPercent(weather, mapUid, weatherProto); + DrawWorld(args, weatherProto, alpha); + } + + private void DrawWorld(in OverlayDrawArgs args, WeatherPrototype weatherProto, float alpha) + { + var worldHandle = args.WorldHandle; + var mapId = args.MapId; + var worldAABB = args.WorldAABB; + var worldBounds = args.WorldBounds; + var invMatrix = args.Viewport.GetWorldToLocalMatrix(); + var position = args.Viewport.Eye?.Position.Position ?? Vector2.Zero; + + if (_blep?.Texture.Size != args.Viewport.Size) + { + _blep?.Dispose(); + _blep = _clyde.CreateRenderTarget(args.Viewport.Size, new RenderTargetFormatParameters(RenderTargetColorFormat.Rgba8Srgb), name: "weather-stencil"); + } + + // 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(_blep, () => + { + var bodyQuery = _entManager.GetEntityQuery(); + var xformQuery = _entManager.GetEntityQuery(); + + foreach (var grid in _mapManager.FindGridsIntersecting(mapId, worldAABB)) + { + var matrix = _transform.GetWorldMatrix(grid.Owner, xformQuery); + Matrix3.Multiply(in matrix, in invMatrix, out var matty); + worldHandle.SetTransform(matty); + + foreach (var tile in grid.GetTilesIntersecting(worldAABB)) + { + // Ignored tiles for stencil + if (_weather.CanWeatherAffect(grid, tile, bodyQuery)) + { + continue; + } + + var gridTile = new Box2(tile.GridIndices * grid.TileSize, + (tile.GridIndices + Vector2i.One) * grid.TileSize); + + worldHandle.DrawRect(gridTile, Color.White); + } + } + + }, Color.Transparent); + + worldHandle.SetTransform(Matrix3.Identity); + worldHandle.UseShader(_protoManager.Index("StencilMask").Instance()); + worldHandle.DrawTextureRect(_blep.Texture, worldBounds); + Texture? sprite = null; + var curTime = _timing.RealTime; + + switch (weatherProto.Sprite) + { + case SpriteSpecifier.Rsi rsi: + var rsiActual = _cache.GetResource(rsi.RsiPath).RSI; + rsiActual.TryGetState(rsi.RsiState, out var state); + var frames = state!.GetFrames(RSI.State.Direction.South); + var delays = state.GetDelays(); + var totalDelay = delays.Sum(); + var time = curTime.TotalSeconds % totalDelay; + var delaySum = 0f; + + for (var i = 0; i < delays.Length; i++) + { + var delay = delays[i]; + delaySum += delay; + + if (time > delaySum) + continue; + + sprite = frames[i]; + break; + } + + sprite ??= _sprite.Frame0(weatherProto.Sprite); + break; + case SpriteSpecifier.Texture texture: + sprite = texture.GetTexture(_cache); + break; + default: + throw new NotImplementedException(); + } + + // Draw the rain + worldHandle.UseShader(_protoManager.Index("StencilDraw").Instance()); + + // TODO: This is very similar to parallax but we need stencil support but we can probably combine these somehow + // and not make it spaghetti, while getting the advantages of not-duped code? + + + // Okay I have spent like 5 hours on this at this point and afaict you have one of the following comprises: + // - No scrolling so the weather is always centered on the player + // - Crappy looking rotation but strafing looks okay and scrolls + // - Crappy looking strafing but rotation looks okay. + // - No rotation + // - Storing state across frames to do scrolling and just having it always do topdown. + + // I have chosen no rotation. + + const float scale = 1f; + const float slowness = 0f; + var scrolling = Vector2.Zero; + + // Size of the texture in world units. + var size = (sprite.Size / (float) EyeManager.PixelsPerMeter) * scale; + var scrolled = scrolling * (float) curTime.TotalSeconds; + + // Origin - start with the parallax shift itself. + var originBL = position * slowness + scrolled; + + // Centre the image. + originBL -= size / 2; + + // Remove offset so we can floor. + var flooredBL = args.WorldAABB.BottomLeft - originBL; + + // Floor to background size. + flooredBL = (flooredBL / size).Floored() * size; + + // Re-offset. + flooredBL += originBL; + + for (var x = flooredBL.X; x < args.WorldAABB.Right; x += size.X) + { + for (var y = flooredBL.Y; y < args.WorldAABB.Top; y += size.Y) + { + var box = Box2.FromDimensions((x, y), size); + worldHandle.DrawTextureRect(sprite, box, (weatherProto.Color ?? Color.White).WithAlpha(alpha)); + } + } + + worldHandle.SetTransform(Matrix3.Identity); + worldHandle.UseShader(null); + } +} diff --git a/Content.Client/Weather/WeatherSystem.cs b/Content.Client/Weather/WeatherSystem.cs new file mode 100644 index 0000000000..258131b376 --- /dev/null +++ b/Content.Client/Weather/WeatherSystem.cs @@ -0,0 +1,219 @@ +using Content.Shared.Weather; +using Robust.Client.GameObjects; +using Robust.Client.Graphics; +using Robust.Client.Player; +using Robust.Shared.GameStates; +using Robust.Shared.Map; +using Robust.Shared.Map.Components; +using Robust.Shared.Physics; +using Robust.Shared.Physics.Components; +using Robust.Shared.Physics.Systems; +using Robust.Shared.Player; + +namespace Content.Client.Weather; + +public sealed class WeatherSystem : SharedWeatherSystem +{ + [Dependency] private readonly IOverlayManager _overlayManager = default!; + [Dependency] private readonly IPlayerManager _playerManager = default!; + [Dependency] private readonly AudioSystem _audio = default!; + [Dependency] private readonly MetaDataSystem _metadata = default!; + [Dependency] private readonly SharedPhysicsSystem _physics = default!; + [Dependency] private readonly SharedTransformSystem _transform = default!; + + // Consistency isn't really important, just want to avoid sharp changes and there's no way to lerp on engine nicely atm. + private float _lastAlpha; + private float _lastOcclusion; + + private const float OcclusionLerpRate = 4f; + private const float AlphaLerpRate = 4f; + + public override void Initialize() + { + base.Initialize(); + _overlayManager.AddOverlay(new WeatherOverlay(_transform, EntityManager.System(), this)); + SubscribeLocalEvent(OnWeatherHandleState); + } + + public override void Shutdown() + { + base.Shutdown(); + _overlayManager.RemoveOverlay(); + } + + protected override void Run(EntityUid uid, WeatherComponent component, WeatherPrototype weather, WeatherState state, float frameTime) + { + base.Run(uid, component, weather, state, frameTime); + + var ent = _playerManager.LocalPlayer?.ControlledEntity; + + 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) + { + _lastOcclusion = 0f; + _lastAlpha = 0f; + component.Stream?.Stop(); + component.Stream = null; + return; + } + + if (!Timing.IsFirstTimePredicted || weather.Sound == null) + return; + + component.Stream ??= _audio.PlayGlobal(weather.Sound, Filter.Local(), true); + var volumeMod = MathF.Pow(10, weather.Sound.Params.Volume / 10f); + + var stream = (AudioSystem.PlayingStream) component.Stream!; + var alpha = GetPercent(component, mapUid.Value, weather); + alpha = MathF.Pow(alpha, 2f) * volumeMod; + // TODO: Lerp this occlusion. + var occlusion = 0f; + // TODO: Fade-out needs to be slower + // TODO: HELPER PLZ + + // Work out tiles nearby to determine volume. + if (TryComp(entXform.GridUid, out var grid)) + { + // Floodfill to the nearest tile and use that for audio. + var seed = grid.GetTileRef(entXform.Coordinates); + var frontier = new Queue(); + frontier.Enqueue(seed); + // If we don't have a nearest node don't play any sound. + EntityCoordinates? nearestNode = null; + var bodyQuery = GetEntityQuery(); + var visited = new HashSet(); + + while (frontier.TryDequeue(out var node)) + { + if (!visited.Add(node.GridIndices)) + continue; + + if (!CanWeatherAffect(grid, node, bodyQuery)) + { + // 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(grid.GetTileRef(new Vector2i(x, y) + node.GridIndices)); + } + } + + continue; + } + + nearestNode = new EntityCoordinates(entXform.GridUid.Value, + (Vector2) node.GridIndices + (grid.TileSize / 2f)); + break; + } + + if (nearestNode == null) + alpha = 0f; + else + { + var entPos = _transform.GetWorldPosition(entXform); + var sourceRelative = nearestNode.Value.ToMap(EntityManager).Position - entPos; + + if (sourceRelative.LengthSquared > 1f) + { + occlusion = _physics.IntersectRayPenetration(entXform.MapID, + new CollisionRay(entPos, sourceRelative.Normalized, _audio.OcclusionCollisionMask), + sourceRelative.Length, stream.TrackingEntity); + } + } + } + + if (MathHelper.CloseTo(_lastOcclusion, occlusion, 0.01f)) + _lastOcclusion = occlusion; + else + _lastOcclusion += (occlusion - _lastOcclusion) * OcclusionLerpRate * frameTime; + + if (MathHelper.CloseTo(_lastAlpha, alpha, 0.01f)) + _lastAlpha = alpha; + else + _lastAlpha += (alpha - _lastAlpha) * AlphaLerpRate * frameTime; + + // Full volume if not on grid + Sawmill.Debug($"Setting alpha to {alpha:0.000}"); + stream.Source.SetVolumeDirect(_lastAlpha); + stream.Source.SetOcclusion(_lastOcclusion); + } + + public float GetPercent(WeatherComponent component, EntityUid mapUid, WeatherPrototype weatherProto) + { + var pauseTime = _metadata.GetPauseTime(mapUid); + var elapsed = Timing.CurTime - (component.StartTime + pauseTime); + var duration = component.Duration; + var remaining = duration - elapsed; + float alpha; + + if (elapsed < weatherProto.StartupTime) + { + alpha = (float) (elapsed / weatherProto.StartupTime); + } + else if (remaining < weatherProto.ShutdownTime) + { + alpha = (float) (remaining / weatherProto.ShutdownTime); + } + else + { + alpha = 1f; + } + + return alpha; + } + + protected override bool SetState(EntityUid uid, WeatherComponent component, WeatherState state, WeatherPrototype prototype) + { + if (!base.SetState(uid, component, state, prototype)) + return false; + + if (!Timing.IsFirstTimePredicted) + return true; + + // TODO: Fades + component.Stream?.Stop(); + component.Stream = null; + component.Stream = _audio.PlayGlobal(prototype.Sound, Filter.Local(), true); + return true; + } + + protected override void EndWeather(WeatherComponent component) + { + _lastOcclusion = 0f; + _lastAlpha = 0f; + base.EndWeather(component); + } + + private void OnWeatherHandleState(EntityUid uid, WeatherComponent component, ref ComponentHandleState args) + { + if (args.Current is not WeatherComponentState state) + return; + + if (component.Weather != state.Weather || !component.EndTime.Equals(state.EndTime) || !component.StartTime.Equals(state.StartTime)) + { + EndWeather(component); + + if (state.Weather != null) + StartWeather(component, ProtoMan.Index(state.Weather)); + } + + component.EndTime = state.EndTime; + component.StartTime = state.StartTime; + } +} diff --git a/Content.Server/Weather/WeatherSystem.cs b/Content.Server/Weather/WeatherSystem.cs new file mode 100644 index 0000000000..8dd3146abf --- /dev/null +++ b/Content.Server/Weather/WeatherSystem.cs @@ -0,0 +1,84 @@ +using System.Linq; +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 Robust.Shared.Map.Components; + +namespace Content.Server.Weather; + +public sealed class WeatherSystem : SharedWeatherSystem +{ + [Dependency] private readonly IConsoleHost _console = default!; + + public override void Initialize() + { + base.Initialize(); + SubscribeLocalEvent(OnWeatherGetState); + _console.RegisterCommand("weather", + Loc.GetString("cmd-weather-desc"), + Loc.GetString("cmd-weather-help"), + WeatherTwo, + WeatherCompletion); + } + + private void OnWeatherGetState(EntityUid uid, WeatherComponent component, ref ComponentGetState args) + { + args.State = new WeatherComponentState() + { + Weather = component.Weather, + EndTime = component.EndTime, + StartTime = component.StartTime, + }; + } + + [AdminCommand(AdminFlags.Fun)] + private void WeatherTwo(IConsoleShell shell, string argstr, string[] args) + { + if (args.Length != 2) + { + return; + } + + if (!int.TryParse(args[0], out var mapInt)) + { + return; + } + + var mapId = new MapId(mapInt); + + if (!MapManager.MapExists(mapId)) + { + return; + } + + if (args[1].Equals("null")) + { + SetWeather(mapId, null); + } + else if (ProtoMan.TryIndex(args[1], out var weatherProto)) + { + SetWeather(mapId, weatherProto); + } + else + { + shell.WriteError($"Unable to parse weather prototype"); + } + } + + private CompletionResult WeatherCompletion(IConsoleShell shell, string[] args) + { + var options = new List(); + + if (args.Length == 1) + { + options.AddRange(EntityQuery(true).Select(o => new CompletionOption(o.WorldMap.ToString()))); + return CompletionResult.FromHintOptions(options, "Map Id"); + } + + var a = CompletionHelper.PrototypeIDs(true, ProtoMan); + return CompletionResult.FromHintOptions(a, Loc.GetString("cmd-weather-hint")); + } +} diff --git a/Content.Shared/Maps/ContentTileDefinition.cs b/Content.Shared/Maps/ContentTileDefinition.cs index bdf58bbda0..23fbde10e4 100644 --- a/Content.Shared/Maps/ContentTileDefinition.cs +++ b/Content.Shared/Maps/ContentTileDefinition.cs @@ -69,6 +69,11 @@ namespace Content.Shared.Maps [DataField("isSpace")] public bool IsSpace { get; private set; } [DataField("sturdy")] public bool Sturdy { get; private set; } = true; + /// + /// Can weather affect this tile. + /// + [DataField("weather")] public bool Weather = false; + public void AssignTileId(ushort id) { TileId = id; diff --git a/Content.Shared/Weather/SharedWeatherSystem.cs b/Content.Shared/Weather/SharedWeatherSystem.cs new file mode 100644 index 0000000000..165a5a0ffb --- /dev/null +++ b/Content.Shared/Weather/SharedWeatherSystem.cs @@ -0,0 +1,167 @@ +using Content.Shared.Maps; +using Robust.Shared.Map; +using Robust.Shared.Map.Components; +using Robust.Shared.Physics.Components; +using Robust.Shared.Prototypes; +using Robust.Shared.Random; +using Robust.Shared.Serialization; +using Robust.Shared.Timing; +using Robust.Shared.Utility; + +namespace Content.Shared.Weather; + +public abstract class SharedWeatherSystem : EntitySystem +{ + [Dependency] protected readonly IGameTiming Timing = default!; + [Dependency] protected readonly IMapManager MapManager = default!; + [Dependency] protected readonly IPrototypeManager ProtoMan = default!; + [Dependency] private readonly IRobustRandom _random = default!; + [Dependency] private readonly ITileDefinitionManager _tileDefManager = default!; + + protected ISawmill Sawmill = default!; + + public override void Initialize() + { + base.Initialize(); + Sawmill = Logger.GetSawmill("weather"); + SubscribeLocalEvent(OnWeatherUnpaused); + } + + private void OnWeatherUnpaused(EntityUid uid, WeatherComponent component, ref EntityUnpausedEvent args) + { + component.EndTime += args.PausedTime; + } + + public bool CanWeatherAffect(MapGridComponent grid, TileRef tileRef, EntityQuery bodyQuery) + { + if (tileRef.Tile.IsEmpty) + return true; + + var tileDef = (ContentTileDefinition) _tileDefManager[tileRef.Tile.TypeId]; + + if (!tileDef.Weather) + return false; + + var anchoredEnts = grid.GetAnchoredEntitiesEnumerator(tileRef.GridIndices); + + while (anchoredEnts.MoveNext(out var ent)) + { + if (bodyQuery.TryGetComponent(ent, out var body) && body.CanCollide) + { + return false; + } + } + + return true; + + } + + public override void Update(float frameTime) + { + base.Update(frameTime); + + if (!Timing.IsFirstTimePredicted) + return; + + var curTime = Timing.CurTime; + + foreach (var (comp, metadata) in EntityQuery()) + { + if (comp.Weather == null) + continue; + + var uid = comp.Owner; + var endTime = comp.EndTime; + + // Ended + if (endTime < curTime) + { + EndWeather(comp); + continue; + } + + // Admin messed up or the likes. + if (!ProtoMan.TryIndex(comp.Weather, out var weatherProto)) + { + Sawmill.Error($"Unable to find weather prototype for {comp.Weather}, ending!"); + EndWeather(comp); + continue; + } + + var remainingTime = endTime - curTime; + + // Shutting down + if (remainingTime < weatherProto.ShutdownTime) + { + SetState(uid, comp, WeatherState.Ending, weatherProto); + } + // Starting up + else + { + var startTime = comp.StartTime; + var elapsed = Timing.CurTime - startTime; + + if (elapsed < weatherProto.StartupTime) + { + SetState(uid, comp, WeatherState.Starting, weatherProto); + } + } + + // Run whatever code we need. + Run(uid, comp, weatherProto, comp.State, frameTime); + } + } + + public void SetWeather(MapId mapId, WeatherPrototype? weather) + { + var weatherComp = EnsureComp(MapManager.GetMapEntityId(mapId)); + EndWeather(weatherComp); + + if (weather != null) + StartWeather(weatherComp, weather); + } + + /// + /// Run every tick when the weather is running. + /// + protected virtual void Run(EntityUid uid, WeatherComponent component, WeatherPrototype weather, WeatherState state, float frameTime) {} + + protected void StartWeather(WeatherComponent component, WeatherPrototype weather) + { + component.Weather = weather.ID; + // TODO: ENGINE PR + var duration = _random.NextDouble(weather.DurationMinimum.TotalSeconds, weather.DurationMaximum.TotalSeconds); + component.EndTime = Timing.CurTime + TimeSpan.FromSeconds(duration); + component.StartTime = Timing.CurTime; + DebugTools.Assert(component.State == WeatherState.Invalid); + Dirty(component); + } + + protected virtual void EndWeather(WeatherComponent component) + { + component.Stream?.Stop(); + component.Stream = null; + component.Weather = null; + component.StartTime = TimeSpan.Zero; + component.EndTime = TimeSpan.Zero; + component.State = WeatherState.Invalid; + Dirty(component); + } + + protected virtual bool SetState(EntityUid uid, WeatherComponent component, WeatherState state, WeatherPrototype prototype) + { + if (component.State.Equals(state)) + return false; + + component.State = state; + return true; + } + + [Serializable, NetSerializable] + protected sealed class WeatherComponentState : ComponentState + { + public string? Weather; + public TimeSpan StartTime; + public TimeSpan EndTime; + } +} diff --git a/Content.Shared/Weather/WeatherComponent.cs b/Content.Shared/Weather/WeatherComponent.cs new file mode 100644 index 0000000000..6cbdd5bdfa --- /dev/null +++ b/Content.Shared/Weather/WeatherComponent.cs @@ -0,0 +1,43 @@ +using Robust.Shared.Audio; +using Robust.Shared.GameStates; + +namespace Content.Shared.Weather; + +[RegisterComponent, NetworkedComponent] +public sealed class WeatherComponent : Component +{ + /// + /// Currently running weather. + /// + [ViewVariables, DataField("weather")] + public string? Weather; + + // now + public IPlayingAudioStream? Stream; + + /// + /// When the weather started. + /// + [ViewVariables, DataField("startTime")] + public TimeSpan StartTime = TimeSpan.Zero; + + /// + /// When the applied weather will end. + /// + [ViewVariables(VVAccess.ReadWrite), DataField("endTime")] + public TimeSpan EndTime = TimeSpan.Zero; + + [ViewVariables] + public TimeSpan Duration => EndTime - StartTime; + + [ViewVariables] + public WeatherState State = WeatherState.Invalid; +} + +public enum WeatherState : byte +{ + Invalid = 0, + Starting, + Running, + Ending, +} diff --git a/Content.Shared/Weather/WeatherPrototype.cs b/Content.Shared/Weather/WeatherPrototype.cs new file mode 100644 index 0000000000..77ce1611ba --- /dev/null +++ b/Content.Shared/Weather/WeatherPrototype.cs @@ -0,0 +1,43 @@ +using Content.Shared.Maps; +using Robust.Shared.Audio; +using Robust.Shared.Prototypes; +using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.List; +using Robust.Shared.Utility; + +namespace Content.Shared.Weather; + +[Prototype("weather")] +public sealed class WeatherPrototype : IPrototype +{ + [IdDataFieldAttribute] public string ID { get; } = default!; + + /// + /// Minimum duration for the weather. + /// + [ViewVariables(VVAccess.ReadWrite)] + public TimeSpan DurationMinimum = TimeSpan.FromSeconds(120); + + /// + /// Maximum duration for the weather. + /// + [ViewVariables(VVAccess.ReadWrite)] + public TimeSpan DurationMaximum = TimeSpan.FromSeconds(300); + + [ViewVariables(VVAccess.ReadWrite), DataField("startupTime")] + public TimeSpan StartupTime = TimeSpan.FromSeconds(30); + + [ViewVariables(VVAccess.ReadWrite), DataField("endTime")] + public TimeSpan ShutdownTime = TimeSpan.FromSeconds(30); + + [ViewVariables(VVAccess.ReadWrite), DataField("sprite", required: true)] + public SpriteSpecifier Sprite = default!; + + [ViewVariables(VVAccess.ReadWrite), DataField("color")] + public Color? Color; + + /// + /// Sound to play on the affected areas. + /// + [ViewVariables(VVAccess.ReadWrite), DataField("sound")] + public SoundSpecifier? Sound; +} diff --git a/Resources/Audio/Effects/Weather/licenses.txt b/Resources/Audio/Effects/Weather/licenses.txt new file mode 100644 index 0000000000..7588e276b8 --- /dev/null +++ b/Resources/Audio/Effects/Weather/licenses.txt @@ -0,0 +1,23 @@ +- files: + - rain.ogg + - snowstorm.ogg + - snowstorm_weak.ogg + - wind_2_1.ogg + - wind_2_2.ogg + - wind_3_1.ogg + - wind_4_1.ogg + - wind_4_2.ogg + - wind_5_1.ogg + license: "CC-BY-SA-3.0" + copyright: "Taken from https://github.com/Citadel-Station-13/Citadel-Station-13-RP/tree/28e11dee540e61b6c42fa293c1e1da087c1c2b0a and looped" + source: "https://github.com/Citadel-Station-13/Citadel-Station-13-RP/tree/28e11dee540e61b6c42fa293c1e1da087c1c2b0a" + +- files: ["rain2.wav"] + license: "Royalty free" + copyright: "Varazuvi - Natural Environments" + source: "Taken from Soniss.com - GDC" + +- files: ["rain_heavy.ogg"] + license: "Royalty free" + copyright: "RATH - Rain Hard Loop 08" + source: "Taken from Soniss.com - GDC" diff --git a/Resources/Audio/Effects/Weather/rain.ogg b/Resources/Audio/Effects/Weather/rain.ogg new file mode 100644 index 0000000000..2c18d51185 Binary files /dev/null and b/Resources/Audio/Effects/Weather/rain.ogg differ diff --git a/Resources/Audio/Effects/Weather/rain2.ogg b/Resources/Audio/Effects/Weather/rain2.ogg new file mode 100644 index 0000000000..82e836953e Binary files /dev/null and b/Resources/Audio/Effects/Weather/rain2.ogg differ diff --git a/Resources/Audio/Effects/Weather/rain_heavy.ogg b/Resources/Audio/Effects/Weather/rain_heavy.ogg new file mode 100644 index 0000000000..4dce6b2a57 Binary files /dev/null and b/Resources/Audio/Effects/Weather/rain_heavy.ogg differ diff --git a/Resources/Audio/Effects/Weather/snowstorm.ogg b/Resources/Audio/Effects/Weather/snowstorm.ogg new file mode 100644 index 0000000000..940102b949 Binary files /dev/null and b/Resources/Audio/Effects/Weather/snowstorm.ogg differ diff --git a/Resources/Audio/Effects/Weather/snowstorm_weak.ogg b/Resources/Audio/Effects/Weather/snowstorm_weak.ogg new file mode 100644 index 0000000000..56faa9ad26 Binary files /dev/null and b/Resources/Audio/Effects/Weather/snowstorm_weak.ogg differ diff --git a/Resources/Audio/Effects/Weather/wind_2_1.ogg b/Resources/Audio/Effects/Weather/wind_2_1.ogg new file mode 100644 index 0000000000..a4dc2cc9f0 Binary files /dev/null and b/Resources/Audio/Effects/Weather/wind_2_1.ogg differ diff --git a/Resources/Audio/Effects/Weather/wind_2_2.ogg b/Resources/Audio/Effects/Weather/wind_2_2.ogg new file mode 100644 index 0000000000..cba75effe8 Binary files /dev/null and b/Resources/Audio/Effects/Weather/wind_2_2.ogg differ diff --git a/Resources/Audio/Effects/Weather/wind_3_1.ogg b/Resources/Audio/Effects/Weather/wind_3_1.ogg new file mode 100644 index 0000000000..77270ede4c Binary files /dev/null and b/Resources/Audio/Effects/Weather/wind_3_1.ogg differ diff --git a/Resources/Audio/Effects/Weather/wind_4_1.ogg b/Resources/Audio/Effects/Weather/wind_4_1.ogg new file mode 100644 index 0000000000..ea95e1ac14 Binary files /dev/null and b/Resources/Audio/Effects/Weather/wind_4_1.ogg differ diff --git a/Resources/Audio/Effects/Weather/wind_4_2.ogg b/Resources/Audio/Effects/Weather/wind_4_2.ogg new file mode 100644 index 0000000000..fcdb30bc47 Binary files /dev/null and b/Resources/Audio/Effects/Weather/wind_4_2.ogg differ diff --git a/Resources/Audio/Effects/Weather/wind_5_1.ogg b/Resources/Audio/Effects/Weather/wind_5_1.ogg new file mode 100644 index 0000000000..dcba8d0870 Binary files /dev/null and b/Resources/Audio/Effects/Weather/wind_5_1.ogg differ diff --git a/Resources/Locale/en-US/weather/weather.ftl b/Resources/Locale/en-US/weather/weather.ftl new file mode 100644 index 0000000000..de5dbd8890 --- /dev/null +++ b/Resources/Locale/en-US/weather/weather.ftl @@ -0,0 +1,3 @@ +cmd-weather-desc = Sets the weather for the current map. +cmd-weather-help = weather +cmd-weather-hint = Weather prototype diff --git a/Resources/Prototypes/Tiles/floors.yml b/Resources/Prototypes/Tiles/floors.yml index 7f994edc0e..c392d0fa2e 100644 --- a/Resources/Prototypes/Tiles/floors.yml +++ b/Resources/Prototypes/Tiles/floors.yml @@ -1106,6 +1106,7 @@ itemDrop: FloorTileItemSnow thermalConductivity: 0.04 heatCapacity: 10000 + weather: true - type: tile id: FloorGrass @@ -1121,6 +1122,7 @@ itemDrop: FloorTileItemGrass thermalConductivity: 0.04 heatCapacity: 10000 + weather: true - type: tile id: FloorGrassJungle @@ -1136,6 +1138,7 @@ itemDrop: FloorTileItemGrassJungle thermalConductivity: 0.04 heatCapacity: 10000 + weather: true - type: tile id: FloorGrassDark @@ -1152,6 +1155,7 @@ friction: 0.30 thermalConductivity: 0.04 heatCapacity: 10000 + weather: true - type: tile id: FloorGrassLight @@ -1168,6 +1172,7 @@ friction: 0.30 thermalConductivity: 0.04 heatCapacity: 10000 + weather: true - type: tile id: FloorDirt @@ -1184,6 +1189,7 @@ friction: 0.30 thermalConductivity: 0.04 heatCapacity: 10000 + weather: true # Asteroid - type: tile @@ -1199,6 +1205,7 @@ friction: 0.30 thermalConductivity: 0.04 heatCapacity: 10000 + weather: true - type: tile id: FloorAsteroidTile @@ -1213,6 +1220,7 @@ friction: 0.30 thermalConductivity: 0.04 heatCapacity: 10000 + weather: true - type: tile id: FloorAsteroidCoarseSand0 @@ -1229,6 +1237,7 @@ friction: 0.30 thermalConductivity: 0.04 heatCapacity: 10000 + weather: true - type: tileAlias id: FloorAsteroidCoarseSand1 @@ -1251,6 +1260,7 @@ friction: 0.30 thermalConductivity: 0.04 heatCapacity: 10000 + weather: true - type: tile id: FloorAsteroidIronsand1 @@ -1265,6 +1275,7 @@ friction: 0.30 thermalConductivity: 0.04 heatCapacity: 10000 + weather: true - type: tile id: FloorAsteroidIronsand2 @@ -1279,6 +1290,7 @@ friction: 0.30 thermalConductivity: 0.04 heatCapacity: 10000 + weather: true - type: tile id: FloorAsteroidIronsand3 @@ -1293,6 +1305,7 @@ friction: 0.30 thermalConductivity: 0.04 heatCapacity: 10000 + weather: true - type: tile id: FloorAsteroidIronsand4 @@ -1307,6 +1320,7 @@ friction: 0.30 thermalConductivity: 0.04 heatCapacity: 10000 + weather: true # Caves - type: tile diff --git a/Resources/Prototypes/weather.yml b/Resources/Prototypes/weather.yml new file mode 100644 index 0000000000..a71e59354a --- /dev/null +++ b/Resources/Prototypes/weather.yml @@ -0,0 +1,138 @@ +- 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 + +- 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 diff --git a/Resources/Textures/Effects/weather.rsi/ashfall.png b/Resources/Textures/Effects/weather.rsi/ashfall.png new file mode 100644 index 0000000000..ba17ec8c65 Binary files /dev/null and b/Resources/Textures/Effects/weather.rsi/ashfall.png differ diff --git a/Resources/Textures/Effects/weather.rsi/ashfall_heavy.png b/Resources/Textures/Effects/weather.rsi/ashfall_heavy.png new file mode 100644 index 0000000000..1d385a00b8 Binary files /dev/null and b/Resources/Textures/Effects/weather.rsi/ashfall_heavy.png differ diff --git a/Resources/Textures/Effects/weather.rsi/ashfall_light.png b/Resources/Textures/Effects/weather.rsi/ashfall_light.png new file mode 100644 index 0000000000..a5eae7572c Binary files /dev/null and b/Resources/Textures/Effects/weather.rsi/ashfall_light.png differ diff --git a/Resources/Textures/Effects/weather.rsi/fallout.png b/Resources/Textures/Effects/weather.rsi/fallout.png new file mode 100644 index 0000000000..6862497479 Binary files /dev/null and b/Resources/Textures/Effects/weather.rsi/fallout.png differ diff --git a/Resources/Textures/Effects/weather.rsi/hail.png b/Resources/Textures/Effects/weather.rsi/hail.png new file mode 100644 index 0000000000..791b379ad9 Binary files /dev/null and b/Resources/Textures/Effects/weather.rsi/hail.png differ diff --git a/Resources/Textures/Effects/weather.rsi/meta.json b/Resources/Textures/Effects/weather.rsi/meta.json new file mode 100644 index 0000000000..896786c11b --- /dev/null +++ b/Resources/Textures/Effects/weather.rsi/meta.json @@ -0,0 +1,319 @@ +{ + "version": 1, + "copyright": "https://github.com/Citadel-Station-13/Citadel-Station-13-RP/tree/5781addfa1193c2811408f64d15176139395d670", + "license": "CC-BY-SA-3.0", + "size": { + "x": 32, + "y": 32 + }, + "states": [ + { + "name": "snowfall_light", + "delays": [ + [ + 0.2, + 0.2, + 0.2, + 0.2, + 0.2, + 0.2, + 0.2, + 0.2 + ] + ] + }, + { + "name": "snowfall_med", + "delays": [ + [ + 0.2, + 0.2, + 0.2, + 0.2, + 0.2, + 0.2, + 0.2, + 0.2 + ] + ] + }, + { + "name": "snowfall_heavy", + "delays": [ + [ + 0.02, + 0.02, + 0.02, + 0.02, + 0.02, + 0.02, + 0.02, + 0.02, + 0.02, + 0.02, + 0.02, + 0.02, + 0.02, + 0.02, + 0.02, + 0.02, + 0.02, + 0.02, + 0.02, + 0.02, + 0.02, + 0.02, + 0.02, + 0.02, + 0.02, + 0.02, + 0.02, + 0.02, + 0.02, + 0.02, + 0.02, + 0.02 + ] + ] + }, + { + "name": "rain", + "delays": [ + [ + 0.02, + 0.02, + 0.02, + 0.02, + 0.02, + 0.02, + 0.02, + 0.02, + 0.02, + 0.02, + 0.02, + 0.02, + 0.02, + 0.02, + 0.02, + 0.02 + ] + ] + }, + { + "name": "storm", + "delays": [ + [ + 0.030000001, + 0.030000001, + 0.030000001, + 0.030000001, + 0.030000001, + 0.030000001, + 0.030000001, + 0.030000001, + 0.030000001, + 0.030000001, + 0.030000001, + 0.030000001, + 0.030000001, + 0.030000001, + 0.030000001, + 0.030000001, + 0.030000001, + 0.030000001, + 0.030000001, + 0.030000001, + 0.030000001, + 0.030000001, + 0.030000001, + 0.030000001, + 0.030000001, + 0.030000001, + 0.030000001, + 0.030000001, + 0.030000001, + 0.030000001, + 0.030000001, + 0.030000001 + ] + ] + }, + { + "name": "hail", + "delays": [ + [ + 0.030000001, + 0.030000001, + 0.030000001, + 0.030000001, + 0.030000001, + 0.030000001, + 0.030000001, + 0.030000001, + 0.030000001, + 0.030000001, + 0.030000001, + 0.030000001, + 0.030000001, + 0.030000001, + 0.030000001, + 0.030000001, + 0.030000001, + 0.030000001, + 0.030000001, + 0.030000001, + 0.030000001, + 0.030000001, + 0.030000001, + 0.030000001, + 0.030000001, + 0.030000001, + 0.030000001, + 0.030000001, + 0.030000001, + 0.030000001, + 0.030000001, + 0.030000001 + ] + ] + }, + { + "name": "ashfall_heavy", + "delays": [ + [ + 0.1, + 0.1, + 0.1, + 0.1, + 0.1, + 0.1, + 0.1, + 0.1 + ] + ] + }, + { + "name": "ashfall_light", + "delays": [ + [ + 0.1, + 0.1, + 0.1, + 0.1, + 0.1, + 0.1, + 0.1, + 0.1, + 0.1, + 0.1, + 0.1, + 0.1, + 0.1, + 0.1, + 0.1, + 0.1, + 0.1, + 0.1, + 0.1, + 0.1, + 0.1, + 0.1, + 0.1, + 0.1, + 0.1, + 0.1, + 0.1, + 0.1, + 0.1, + 0.1, + 0.1, + 0.1 + ] + ] + }, + { + "name": "fallout", + "delays": [ + [ + 0.1, + 0.1, + 0.1, + 0.1, + 0.1, + 0.1, + 0.1, + 0.1, + 0.1, + 0.1, + 0.1, + 0.1, + 0.1, + 0.1, + 0.1, + 0.1, + 0.1, + 0.1, + 0.1, + 0.1, + 0.1, + 0.1, + 0.1, + 0.1, + 0.1, + 0.1, + 0.1, + 0.1, + 0.1, + 0.1, + 0.1, + 0.1 + ] + ] + }, + { + "name": "ashfall", + "delays": [ + [ + 0.1, + 0.1, + 0.1, + 0.1, + 0.1, + 0.1, + 0.1, + 0.1 + ] + ] + }, + { + "name": "sandstorm_heavy", + "delays": [ + [ + 0.1, + 0.1, + 0.1, + 0.1, + 0.1, + 0.1, + 0.1, + 0.1 + ] + ] + }, + { + "name": "sandstorm", + "delays": [ + [ + 0.1, + 0.1, + 0.1, + 0.1, + 0.1, + 0.1, + 0.1, + 0.1 + ] + ] + } + ] +} \ No newline at end of file diff --git a/Resources/Textures/Effects/weather.rsi/rain.png b/Resources/Textures/Effects/weather.rsi/rain.png new file mode 100644 index 0000000000..e4d340da29 Binary files /dev/null and b/Resources/Textures/Effects/weather.rsi/rain.png differ diff --git a/Resources/Textures/Effects/weather.rsi/sandstorm.png b/Resources/Textures/Effects/weather.rsi/sandstorm.png new file mode 100644 index 0000000000..5404b804e1 Binary files /dev/null and b/Resources/Textures/Effects/weather.rsi/sandstorm.png differ diff --git a/Resources/Textures/Effects/weather.rsi/sandstorm_heavy.png b/Resources/Textures/Effects/weather.rsi/sandstorm_heavy.png new file mode 100644 index 0000000000..aa6826e09b Binary files /dev/null and b/Resources/Textures/Effects/weather.rsi/sandstorm_heavy.png differ diff --git a/Resources/Textures/Effects/weather.rsi/snowfall_heavy.png b/Resources/Textures/Effects/weather.rsi/snowfall_heavy.png new file mode 100644 index 0000000000..193eca7149 Binary files /dev/null and b/Resources/Textures/Effects/weather.rsi/snowfall_heavy.png differ diff --git a/Resources/Textures/Effects/weather.rsi/snowfall_light.png b/Resources/Textures/Effects/weather.rsi/snowfall_light.png new file mode 100644 index 0000000000..a7aefbe9a3 Binary files /dev/null and b/Resources/Textures/Effects/weather.rsi/snowfall_light.png differ diff --git a/Resources/Textures/Effects/weather.rsi/snowfall_med.png b/Resources/Textures/Effects/weather.rsi/snowfall_med.png new file mode 100644 index 0000000000..25ef53032e Binary files /dev/null and b/Resources/Textures/Effects/weather.rsi/snowfall_med.png differ diff --git a/Resources/Textures/Effects/weather.rsi/storm.png b/Resources/Textures/Effects/weather.rsi/storm.png new file mode 100644 index 0000000000..d0d8f2e58c Binary files /dev/null and b/Resources/Textures/Effects/weather.rsi/storm.png differ