diff --git a/Content.Client/Overlays/StencilOverlay.cs b/Content.Client/Overlays/StencilOverlay.cs index 78b1c4d2b1..22064ae609 100644 --- a/Content.Client/Overlays/StencilOverlay.cs +++ b/Content.Client/Overlays/StencilOverlay.cs @@ -1,6 +1,7 @@ using System.Numerics; using Content.Client.Parallax; using Content.Client.Weather; +using Content.Shared._CP14.WorldEdge; using Content.Shared.Salvage; using Content.Shared.Weather; using Robust.Client.GameObjects; @@ -72,6 +73,13 @@ public sealed partial class StencilOverlay : Overlay DrawRestrictedRange(args, restrictedRangeComponent, invMatrix); } + //CP14 World Edge overlay + if (_entManager.TryGetComponent(mapUid, out var worldEdge)) + { + DrawWorldEdge(args, worldEdge, invMatrix); + } + //CP14 World Edge overlay end + args.WorldHandle.UseShader(null); args.WorldHandle.SetTransform(Matrix3x2.Identity); } diff --git a/Content.Client/_CP14/Overlays/StencilOverlay.WorldEdge.cs b/Content.Client/_CP14/Overlays/StencilOverlay.WorldEdge.cs new file mode 100644 index 0000000000..a8de49386e --- /dev/null +++ b/Content.Client/_CP14/Overlays/StencilOverlay.WorldEdge.cs @@ -0,0 +1,57 @@ +using System.Numerics; +using Content.Shared._CP14.WorldEdge; +using Robust.Client.Graphics; +using Robust.Shared.Utility; + +namespace Content.Client.Overlays; + +public sealed partial class StencilOverlay +{ + private void DrawWorldEdge(in OverlayDrawArgs args, CP14WorldEdgeComponent rangeComp, Matrix3x2 invMatrix) + { + var worldHandle = args.WorldHandle; + var renderScale = args.Viewport.RenderScale.X; + // TODO: This won't handle non-standard zooms so uhh yeah, not sure how to structure it on the shader side. + var zoom = args.Viewport.Eye?.Zoom ?? Vector2.One; + var length = zoom.X; + var bufferRange = MathF.Min(10f, rangeComp.Range); + + var pixelCenter = Vector2.Transform(rangeComp.Origin, invMatrix); + // Something something offset? + var vertical = args.Viewport.Size.Y; + + var pixelMaxRange = rangeComp.Range * renderScale / length * EyeManager.PixelsPerMeter; + var pixelBufferRange = bufferRange * renderScale / length * EyeManager.PixelsPerMeter; + var pixelMinRange = pixelMaxRange - pixelBufferRange; + + _shader.SetParameter("position", new Vector2(pixelCenter.X, vertical - pixelCenter.Y)); + _shader.SetParameter("maxRange", pixelMaxRange); + _shader.SetParameter("minRange", pixelMinRange); + _shader.SetParameter("bufferRange", pixelBufferRange); + _shader.SetParameter("gradient", 0.80f); + + var worldAABB = args.WorldAABB; + var worldBounds = args.WorldBounds; + var position = args.Viewport.Eye?.Position.Position ?? Vector2.Zero; + var localAABB = invMatrix.TransformBox(worldAABB); + + // 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!, () => + { + worldHandle.UseShader(_shader); + worldHandle.DrawRect(localAABB, Color.White); + }, Color.Transparent); + + worldHandle.SetTransform(Matrix3x2.Identity); + worldHandle.UseShader(_protoManager.Index("StencilMask").Instance()); + worldHandle.DrawTextureRect(_blep!.Texture, worldBounds); + var curTime = _timing.RealTime; + var sprite = _sprite.GetFrame(new SpriteSpecifier.Texture(new ResPath("/Textures/Parallaxes/noise.png")), curTime); + + // Draw the rain + worldHandle.UseShader(_protoManager.Index("StencilDraw").Instance()); + _parallax.DrawParallax(worldHandle, worldAABB, sprite, curTime, position, new Vector2(0.2f, 0.1f)); + } +} diff --git a/Content.Server/_CP14/WorldEdge/CP14WorldEdgeSystem.cs b/Content.Server/_CP14/WorldEdge/CP14WorldEdgeSystem.cs new file mode 100644 index 0000000000..f85d93f8dc --- /dev/null +++ b/Content.Server/_CP14/WorldEdge/CP14WorldEdgeSystem.cs @@ -0,0 +1,143 @@ +using System.Numerics; +using Content.Server.Chat.Managers; +using Content.Server.Database; +using Content.Shared._CP14.WorldEdge; +using Content.Shared.Administration.Logs; +using Content.Shared.Chat; +using Content.Shared.Database; +using Content.Shared.Mind.Components; +using Content.Shared.Physics; +using Robust.Server.GameObjects; +using Robust.Shared.Map; +using Robust.Shared.Physics.Collision.Shapes; +using Robust.Shared.Physics.Components; +using Robust.Shared.Physics.Events; +using Robust.Shared.Physics.Systems; +using Robust.Shared.Player; +using Robust.Shared.Timing; + +namespace Content.Server._CP14.WorldEdge; + +public sealed class CP14WorldEdgeSystem : CP14SharedWorldEdgeSystem +{ + [Dependency] private readonly IChatManager _chatManager = default!; + [Dependency] protected readonly ISharedAdminLogManager AdminLog = default!; + [Dependency] private readonly FixtureSystem _fixtures = default!; + [Dependency] private readonly SharedPhysicsSystem _physics = default!; + [Dependency] private readonly IGameTiming _timing = default!; + [Dependency] private readonly TransformSystem _transform = default!; + + public override void Initialize() + { + base.Initialize(); + SubscribeLocalEvent(OnMapInit); + + SubscribeLocalEvent(OnWorldEdgeCollide); + SubscribeLocalEvent(OnPendingUnpaused); + } + + private void OnPendingUnpaused(Entity ent, ref EntityUnpausedEvent args) + { + ent.Comp.RemoveTime += args.PausedTime; + } + + public override void Update(float frameTime) + { + base.Update(frameTime); + + var query = EntityQueryEnumerator(); + while (query.MoveNext(out var uid, out var pending)) + { + if (pending.RemoveTime >= _timing.CurTime) + continue; + + if (Paused(uid)) + continue; + + if (pending.Bounding == null) + { + CancelRemoving((uid, pending)); + continue; + } + + var entPos = _transform.GetWorldPosition(uid); + var originPos = _transform.GetWorldPosition(pending.Bounding.Value); + var distance = Vector2.Distance(entPos, originPos); + + if (distance > pending.Bounding.Value.Comp.Range) + { + RoundRemoveMind((uid, pending)); + } + else + { + CancelRemoving((uid, pending)); + } + } + } + + private void RoundRemoveMind(Entity ent) + { + AdminLog.Add( + LogType.Action, + LogImpact.High, + $"{ToPrettyString(ent):player} has left the playing area, and is out of the round."); + + QueueDel(ent); + } + + private void CancelRemoving(Entity ent) + { + RemComp(ent); + + if (TryComp(ent, out var actor)) + { + var msg = Loc.GetString("cp14-world-edge-cancel-removing-message"); + _chatManager.ChatMessageToOne(ChatChannel.Server, msg, msg, ent, false, actor.PlayerSession.Channel); + } + } + + private void OnWorldEdgeCollide(Entity bounding, ref StartCollideEvent args) + { + if (!TryComp(args.OtherEntity, out var mindContainer)) + return; + + if (TryComp(args.OtherEntity, out var actor) && + !HasComp(args.OtherEntity)) + { + var msg = Loc.GetString("cp14-world-edge-pre-remove-message", + ("second", bounding.Comp.ReturnTime.TotalSeconds)); + _chatManager.ChatMessageToOne(ChatChannel.Server, msg, msg, args.OtherEntity, false, actor.PlayerSession.Channel); + } + + var removePending = EnsureComp(args.OtherEntity); + removePending.RemoveTime = _timing.CurTime + bounding.Comp.ReturnTime; + removePending.Bounding = bounding; + + } + + private void OnMapInit(Entity ent, ref MapInitEvent args) + { + ent.Comp.BoundaryEntity = CreateBoundary(new EntityCoordinates(ent, ent.Comp.Origin), ent.Comp.Range); + } + + public EntityUid CreateBoundary(EntityCoordinates coordinates, float range) + { + var boundaryUid = Spawn(null, coordinates); + var boundaryPhysics = AddComp(boundaryUid); + var cShape = new ChainShape(); + // Don't need it to be a perfect circle, just need it to be loosely accurate. + cShape.CreateLoop(Vector2.Zero, range + 0.25f, false, 4); + _fixtures.TryCreateFixture( + boundaryUid, + cShape, + "boundary", + collisionLayer: (int) (CollisionGroup.HighImpassable | CollisionGroup.Impassable | CollisionGroup.LowImpassable), + body: boundaryPhysics, + hard: false); + + _physics.WakeBody(boundaryUid, body: boundaryPhysics); + var bounding = AddComp(boundaryUid); + bounding.Range = range + 0.25f; + return boundaryUid; + } +} diff --git a/Content.Shared/_CP14/WorldEdge/CP14SharedWorldEdgeSystem.cs b/Content.Shared/_CP14/WorldEdge/CP14SharedWorldEdgeSystem.cs new file mode 100644 index 0000000000..1cab7fe2cf --- /dev/null +++ b/Content.Shared/_CP14/WorldEdge/CP14SharedWorldEdgeSystem.cs @@ -0,0 +1,5 @@ +namespace Content.Shared._CP14.WorldEdge; + +public abstract class CP14SharedWorldEdgeSystem : EntitySystem +{ +} diff --git a/Content.Shared/_CP14/WorldEdge/CP14WorldBoundingComponent.cs b/Content.Shared/_CP14/WorldEdge/CP14WorldBoundingComponent.cs new file mode 100644 index 0000000000..a60aef7ad6 --- /dev/null +++ b/Content.Shared/_CP14/WorldEdge/CP14WorldBoundingComponent.cs @@ -0,0 +1,14 @@ +namespace Content.Shared._CP14.WorldEdge; + +/// +/// when colliding with a player, starts a timer to remove him from the round. +/// +[RegisterComponent, Access(typeof(CP14SharedWorldEdgeSystem))] +public sealed partial class CP14WorldBoundingComponent : Component +{ + [DataField] + public TimeSpan ReturnTime = TimeSpan.FromSeconds(15f); + + [DataField] + public float Range = 0f; +} diff --git a/Content.Shared/_CP14/WorldEdge/CP14WorldEdgeComponent.cs b/Content.Shared/_CP14/WorldEdge/CP14WorldEdgeComponent.cs new file mode 100644 index 0000000000..dc79048937 --- /dev/null +++ b/Content.Shared/_CP14/WorldEdge/CP14WorldEdgeComponent.cs @@ -0,0 +1,20 @@ +using System.Numerics; +using Robust.Shared.GameStates; + +namespace Content.Shared._CP14.WorldEdge; + +/// +/// creates a world boundary that removes players who pass through it +/// +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState, Access(typeof(CP14SharedWorldEdgeSystem))] +public sealed partial class CP14WorldEdgeComponent : Component +{ + [DataField(required: true), AutoNetworkedField, ViewVariables(VVAccess.ReadWrite)] + public float Range = 20f; + + [DataField, AutoNetworkedField, ViewVariables(VVAccess.ReadWrite)] + public Vector2 Origin; + + [DataField] + public EntityUid BoundaryEntity; +} diff --git a/Content.Shared/_CP14/WorldEdge/CP14WorldRemovePendingComponent.cs b/Content.Shared/_CP14/WorldEdge/CP14WorldRemovePendingComponent.cs new file mode 100644 index 0000000000..957dbbb756 --- /dev/null +++ b/Content.Shared/_CP14/WorldEdge/CP14WorldRemovePendingComponent.cs @@ -0,0 +1,14 @@ +namespace Content.Shared._CP14.WorldEdge; + +/// +/// when colliding with a player, starts a timer to remove him from the round. +/// +[RegisterComponent, Access(typeof(CP14SharedWorldEdgeSystem))] +public sealed partial class CP14WorldRemovePendingComponent : Component +{ + [DataField] + public TimeSpan RemoveTime; + + [DataField] + public Entity? Bounding; +} diff --git a/Resources/Locale/en-US/_CP14/worldEdge/world-edge.ftl b/Resources/Locale/en-US/_CP14/worldEdge/world-edge.ftl new file mode 100644 index 0000000000..1f5165df6a --- /dev/null +++ b/Resources/Locale/en-US/_CP14/worldEdge/world-edge.ftl @@ -0,0 +1,2 @@ +cp14-world-edge-pre-remove-message = [color=red]CAUTION![/color] You are leaving the game zone! If you do not return within [color=red]{$second}[/color] seconds, you will be permanently removed from the round! +cp14-world-edge-cancel-removing-message = The exit round has been canceled. \ No newline at end of file diff --git a/Resources/Locale/ru-RU/_CP14/worldEdge/world-edge.ftl b/Resources/Locale/ru-RU/_CP14/worldEdge/world-edge.ftl new file mode 100644 index 0000000000..9d8d0676a7 --- /dev/null +++ b/Resources/Locale/ru-RU/_CP14/worldEdge/world-edge.ftl @@ -0,0 +1,2 @@ +cp14-world-edge-pre-remove-message = [color=red]ВНИМАНИЕ![/color] Вы покидаете игровую зону! Если вы не вернетесь назад в течении [color=red]{$second}[/color] секунд, вы будете окончательно удалены из раунда! +cp14-world-edge-cancel-removing-message = Выход из раунда отменен. \ No newline at end of file diff --git a/Resources/Maps/_CP14/alchemy_test.yml b/Resources/Maps/_CP14/alchemy_test.yml index 8131a60454..7e5296d4a5 100644 --- a/Resources/Maps/_CP14/alchemy_test.yml +++ b/Resources/Maps/_CP14/alchemy_test.yml @@ -36,6 +36,8 @@ entities: - type: OccluderTree - type: LoadedMap - type: MapLight + - type: CP14WorldEdge + range: 30 - type: CP14DayCycle timeEntries: - duration: 80