using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Numerics; using Robust.Client.GameObjects; using Robust.Client.Graphics; using Robust.Client.Input; using Robust.Client.ResourceManagement; using Robust.Shared.GameObjects; using Robust.Shared.Graphics; using Robust.Shared.IoC; using Robust.Shared.Log; using Robust.Shared.Map; using Robust.Shared.Map.Components; using Robust.Shared.Maths; using Robust.Shared.Physics.Systems; using Robust.Shared.Utility; namespace Robust.Client.Placement { public abstract class PlacementMode { public readonly PlacementManager pManager; /// /// Holds the current tile we are hovering our mouse over /// public TileRef CurrentTile { get; set; } /// /// Local coordinates of our cursor on the map /// public EntityCoordinates MouseCoords { get; set; } /// /// Texture resources to draw to represent the entity we are trying to spawn /// public List? TexturesToDraw { get; set; } /// /// Color set to the ghost entity when it has a valid spawn position /// public Color ValidPlaceColor { get; set; } = new(20, 180, 20); //Default valid color is green /// /// Color set to the ghost entity when it has an invalid spawn position /// public Color InvalidPlaceColor { get; set; } = new(180, 20, 20); //Default invalid placement is red /// /// Used for line and grid placement to determine how spaced apart the entities should be /// protected float GridDistancing = 1f; /// /// Whether this mode requires us to verify the player is spawning within a certain range of themselves /// public virtual bool RangeRequired => false; /// /// Whether this mode can use the line placement mode /// public virtual bool HasLineMode => false; /// /// Whether this mode can use the grid placement mode /// public virtual bool HasGridMode => false; protected PlacementMode(PlacementManager pMan) { pManager = pMan; } public virtual string ModeName => GetType().Name; /// /// Aligns the location of placement based on cursor location /// /// /// Returns whether the current position is a valid placement position public abstract void AlignPlacementMode(ScreenCoordinates mouseScreen); /// /// Verifies the location of placement is a valid position to place at /// /// /// public abstract bool IsValidPosition(EntityCoordinates position); public virtual void Render(in OverlayDrawArgs args) { var uid = pManager.CurrentPlacementOverlayEntity; if (!pManager.EntityManager.TryGetComponent(uid, out SpriteComponent? sprite) || !sprite.Visible) { // TODO draw something for placement of invisible & sprite-less entities. return; } IEnumerable locationcollection; switch (pManager.PlacementType) { case PlacementManager.PlacementTypes.None: locationcollection = SingleCoordinate(); break; case PlacementManager.PlacementTypes.Line: locationcollection = LineCoordinates(); break; case PlacementManager.PlacementTypes.Grid: locationcollection = GridCoordinates(); break; default: locationcollection = SingleCoordinate(); break; } var dirAng = pManager.Direction.ToAngle(); var spriteSys = pManager.EntityManager.System(); var transformSys = pManager.EntityManager.System(); foreach (var coordinate in locationcollection) { if (!coordinate.IsValid(pManager.EntityManager)) return; // Just some paranoia just in case var worldPos = coordinate.ToMapPos(pManager.EntityManager, transformSys); var worldRot = pManager.EntityManager.GetComponent(coordinate.EntityId).WorldRotation + dirAng; sprite.Color = IsValidPosition(coordinate) ? ValidPlaceColor : InvalidPlaceColor; var rot = args.Viewport.Eye?.Rotation ?? default; spriteSys.Render(uid.Value, sprite, args.WorldHandle, rot, worldRot, worldPos); } } public IEnumerable SingleCoordinate() { yield return MouseCoords; } public IEnumerable LineCoordinates() { var mouseScreen = pManager.InputManager.MouseScreenPosition; var mousePos = pManager.EyeManager.PixelToMap(mouseScreen); var transformSys = pManager.EntityManager.System(); if (mousePos.MapId == MapId.Nullspace) yield break; var (_, (x, y)) = EntityCoordinates.FromMap(pManager.StartPoint.EntityId, mousePos, transformSys, pManager.EntityManager) - pManager.StartPoint; float iterations; Vector2 distance; if (Math.Abs(x) > Math.Abs(y)) { var xSign = Math.Sign(x); iterations = MathF.Ceiling(Math.Abs((x + GridDistancing / 2f * xSign) / GridDistancing)); distance = new Vector2(x > 0 ? 1 : -1, 0); } else { var ySign = Math.Sign(y); iterations = MathF.Ceiling(Math.Abs((y + GridDistancing / 2f * ySign) / GridDistancing)); distance = new Vector2(0, y > 0 ? 1 : -1); } for (var i = 0; i < iterations; i++) { yield return new EntityCoordinates(pManager.StartPoint.EntityId, pManager.StartPoint.Position + distance * i); } } // This name is a nice reminder of our origins. Never forget. public IEnumerable GridCoordinates() { var mouseScreen = pManager.InputManager.MouseScreenPosition; var mousePos = pManager.EyeManager.PixelToMap(mouseScreen); var transformSys = pManager.EntityManager.System(); if (mousePos.MapId == MapId.Nullspace) yield break; var placementdiff = EntityCoordinates.FromMap(pManager.StartPoint.EntityId, mousePos, transformSys, pManager.EntityManager) - pManager.StartPoint; var xSign = Math.Sign(placementdiff.X); var ySign = Math.Sign(placementdiff.Y); var iterationsX = Math.Ceiling(Math.Abs((placementdiff.X + GridDistancing / 2f * xSign) / GridDistancing)); var iterationsY = Math.Ceiling(Math.Abs((placementdiff.Y + GridDistancing / 2f * ySign) / GridDistancing)); for (var x = 0; x < iterationsX; x++) { for (var y = 0; y < iterationsY; y++) { yield return new EntityCoordinates(pManager.StartPoint.EntityId, pManager.StartPoint.Position + new Vector2(x * xSign, y * ySign) * GridDistancing); } } } /// /// Returns the tile ref for a grid, or a map. /// public TileRef GetTileRef(EntityCoordinates coordinates) { var gridUidOpt = coordinates.GetGridUid(pManager.EntityManager); return gridUidOpt is EntityUid gridUid && gridUid.IsValid() ? pManager.EntityManager.GetComponent(gridUid).GetTileRef(MouseCoords) : new TileRef(gridUidOpt ?? EntityUid.Invalid, MouseCoords.ToVector2i(pManager.EntityManager, pManager.MapManager, pManager.EntityManager.System()), Tile.Empty); } public TextureResource GetSprite(string key) { return pManager.ResourceCache.GetResource(new ResPath("/Textures/") / key); } public bool TryGetSprite(string key, [NotNullWhen(true)] out TextureResource? sprite) { return pManager.ResourceCache.TryGetResource(new ResPath(@"/Textures/") / key, out sprite); } /// /// Checks if the player is spawning within a certain range of his character if range is required on this mode /// /// public bool RangeCheck(EntityCoordinates coordinates) { if (!RangeRequired) return true; var controlled = pManager.PlayerManager.LocalEntity ?? EntityUid.Invalid; if (controlled == EntityUid.Invalid) { return false; } var range = pManager.CurrentPermission!.Range; var transformSys = pManager.EntityManager.System(); if (range > 0 && !pManager.EntityManager.GetComponent(controlled).Coordinates.InRange(pManager.EntityManager, transformSys, coordinates, range)) return false; return true; } public bool IsColliding(EntityCoordinates coordinates) { var bounds = pManager.ColliderAABB; var transformSys = pManager.EntityManager.System(); var mapCoords = coordinates.ToMap(pManager.EntityManager, transformSys); var (x, y) = mapCoords.Position; var collisionBox = Box2.FromDimensions( bounds.Left + x, bounds.Bottom + y, bounds.Width, bounds.Height); return EntitySystem.Get().TryCollideRect(collisionBox, mapCoords.MapId); } protected Vector2 ScreenToWorld(Vector2 point) { return pManager.EyeManager.ScreenToMap(point).Position; } protected Vector2 WorldToScreen(Vector2 point) { return pManager.EyeManager.WorldToScreen(point); } protected EntityCoordinates ScreenToCursorGrid(ScreenCoordinates coords) { var mapCoords = pManager.EyeManager.PixelToMap(coords.Position); if (!pManager.MapManager.TryFindGridAt(mapCoords, out var gridUid, out var grid)) { return EntityCoordinates.FromMap(pManager.MapManager, mapCoords); } var transformSys = pManager.EntityManager.System(); return EntityCoordinates.FromMap(gridUid, mapCoords, transformSys, pManager.EntityManager); } } }