219 lines
8.0 KiB
C#
219 lines
8.0 KiB
C#
using System.Linq;
|
|
using System.Numerics;
|
|
using Content.Client.Parallax;
|
|
using Content.Shared.Weather;
|
|
using Robust.Client.GameObjects;
|
|
using Robust.Client.Graphics;
|
|
using Robust.Client.ResourceManagement;
|
|
using Robust.Client.Utility;
|
|
using Robust.Shared.Enums;
|
|
using Robust.Shared.Graphics.RSI;
|
|
using Robust.Shared.Map;
|
|
using Robust.Shared.Map.Components;
|
|
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<WeatherComponent>(_mapManager.GetMapEntityId(args.MapId), out var weather) ||
|
|
weather.Weather.Count == 0)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
return base.BeforeDraw(in args);
|
|
}
|
|
|
|
protected override void Draw(in OverlayDrawArgs args)
|
|
{
|
|
var mapUid = _mapManager.GetMapEntityId(args.MapId);
|
|
|
|
if (!_entManager.TryGetComponent<WeatherComponent>(mapUid, out var comp))
|
|
{
|
|
return;
|
|
}
|
|
|
|
foreach (var (proto, weather) in comp.Weather)
|
|
{
|
|
if (!_protoManager.TryIndex<WeatherPrototype>(proto, out var weatherProto))
|
|
continue;
|
|
|
|
var alpha = _weather.GetPercent(weather, mapUid);
|
|
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<PhysicsComponent>();
|
|
var xformQuery = _entManager.GetEntityQuery<TransformComponent>();
|
|
var weatherIgnoreQuery = _entManager.GetEntityQuery<IgnoreWeatherComponent>();
|
|
|
|
// idk if this is safe to cache in a field and clear sloth help
|
|
var grids = new List<Entity<MapGridComponent>>();
|
|
_mapManager.FindGridsIntersecting(mapId, worldAABB, ref grids);
|
|
|
|
foreach (var grid in grids)
|
|
{
|
|
var matrix = _transform.GetWorldMatrix(grid, xformQuery);
|
|
Matrix3.Multiply(in matrix, in invMatrix, out var matty);
|
|
worldHandle.SetTransform(matty);
|
|
|
|
foreach (var tile in grid.Comp.GetTilesIntersecting(worldAABB))
|
|
{
|
|
// Ignored tiles for stencil
|
|
if (_weather.CanWeatherAffect(grid, tile, weatherIgnoreQuery, bodyQuery))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
var gridTile = new Box2(tile.GridIndices * grid.Comp.TileSize,
|
|
(tile.GridIndices + Vector2i.One) * grid.Comp.TileSize);
|
|
|
|
worldHandle.DrawRect(gridTile, Color.White);
|
|
}
|
|
}
|
|
|
|
}, Color.Transparent);
|
|
|
|
worldHandle.SetTransform(Matrix3.Identity);
|
|
worldHandle.UseShader(_protoManager.Index<ShaderPrototype>("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<RSIResource>(rsi.RsiPath).RSI;
|
|
rsiActual.TryGetState(rsi.RsiState, out var state);
|
|
var frames = state!.GetFrames(RsiDirection.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<ShaderPrototype>("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(new Vector2(x, y), size);
|
|
worldHandle.DrawTextureRect(sprite, box, (weatherProto.Color ?? Color.White).WithAlpha(alpha));
|
|
}
|
|
}
|
|
|
|
worldHandle.SetTransform(Matrix3.Identity);
|
|
worldHandle.UseShader(null);
|
|
}
|
|
}
|