using System;
using Robust.Client.GameObjects;
using Robust.Client.Graphics;
using Robust.Client.Utility;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Maths;
using Robust.Shared.Serialization.Manager.Attributes;
using Robust.Shared.ViewVariables;
using static Robust.Client.GameObjects.SpriteComponent;
namespace Content.Client.Clickable
{
[RegisterComponent]
public sealed class ClickableComponent : Component
{
[Dependency] private readonly IClickMapManager _clickMapManager = default!;
[Dependency] private readonly IEyeManager _eyeManager = default!;
[Dependency] private readonly IEntityManager _entMan = default!;
[ViewVariables] [DataField("bounds")] private DirBoundData? Bounds;
///
/// Used to check whether a click worked. Will first check if the click falls inside of some explicit bounding
/// boxes (see ). If that fails, attempts to use automatically generated click maps.
///
/// The world position that was clicked.
///
/// The draw depth for the sprite that captured the click.
///
/// True if the click worked, false otherwise.
public bool CheckClick(Vector2 worldPos, out int drawDepth, out uint renderOrder)
{
if (!_entMan.TryGetComponent(Owner, out ISpriteComponent? sprite) || !sprite.Visible)
{
drawDepth = default;
renderOrder = default;
return false;
}
drawDepth = sprite.DrawDepth;
renderOrder = sprite.RenderOrder;
var transform = _entMan.GetComponent(Owner);
var worldRot = transform.WorldRotation;
// We need to convert world angle to a positive value. between 0 and 2pi. This is important for
// CalcRectWorldAngle to get the right angle. Otherwise can get incorrect results for sprites at angles like
// -135 degrees (seems highly specific, but AI actors & other entities can snap to those angles while
// moving). As to why we treat world-angle like this, but not eye angle or world+eye, it is just because
// thats what sprite-rendering does.
worldRot = worldRot.Reduced();
if (worldRot.Theta < 0)
worldRot = new Angle(worldRot.Theta + Math.Tau);
var invSpriteMatrix = Matrix3.CreateTransform(Vector2.Zero, -sprite.Rotation, (1,1)/sprite.Scale);
var relativeRotation = worldRot + _eyeManager.CurrentEye.Rotation;
// localPos is the clicked location in the entity's coordinate frame, but with the sprite offset removed.
var localPos = transform.InvWorldMatrix.Transform(worldPos) - sprite.Offset;
// Check explicitly defined click-able bounds
if (CheckDirBound(sprite, relativeRotation, localPos, invSpriteMatrix))
return true;
// Next check each individual sprite layer using automatically computed click maps.
foreach (var spriteLayer in sprite.AllLayers)
{
if (!spriteLayer.Visible || spriteLayer is not Layer layer)
continue;
// How many orientations does this rsi have?
var dirCount = sprite.GetLayerDirectionCount(layer);
// If the sprite does not actually rotate we need to fix the rotation that was added to localPos via invSpriteMatrix
var modAngle = Angle.Zero;
if (sprite.NoRotation)
modAngle += CalcRectWorldAngle(relativeRotation, dirCount);
// Check the layer's texture, if it has one
if (layer.Texture != null)
{
// Convert to sprite-coordinates. This includes sprite matrix (scale, rotation), and noRot corrections (modAngle)
// Recall that the sprite offset was already removed
var spritePos = invSpriteMatrix.Transform(modAngle.RotateVec(localPos));
// Convert to image coordinates
var imagePos = (Vector2i) (spritePos * EyeManager.PixelsPerMeter * (1, -1) + layer.Texture.Size / 2f);
if (_clickMapManager.IsOccluding(layer.Texture, imagePos))
return true;
}
// As the texture failed, we check the RSI next
if (layer.State == null || layer.ActualRsi is not RSI rsi || !rsi.TryGetState(layer.State, out var state))
continue;
// Get the sprite direction, in the absence of any direction offset or override. this is just for
// determining the angle-snapping correction for directional sprites.
var dir = relativeRotation.ToRsiDirection(state.Directions);
// Add the correction to the sprite angle due to the fact that it is a directional sprite. This is some
// multiple of 90 or 45 degrees for 4/8 directional sprites, or 0 for one-directional sprites.
modAngle += dir.Convert().ToAngle();
// TODO SPRITE LAYER ROTATION
// Currently this doesn't support layers with scale & rotation. Whenever/if-ever that should be added,
// this needs fixing. See also Layer.CalculateBoundingBox and other engine code with similar warnings.
// Convert to sprite-coordinates. This is the same as for the texture check, but now also includes
// directional angle-snapping corrections in modAngle (+ the previous NoRot stuff)
var layerPos = invSpriteMatrix.Transform(modAngle.RotateVec(localPos));
// Convert to image coordinates
var layerImagePos = (Vector2i) (layerPos * EyeManager.PixelsPerMeter * (1, -1) + layer.ActualRsi.Size / 2f);
// Next, to get the right click map we need the "direction" of this layer that is actually being used to draw the sprite on the screen.
// This **can** differ from the dir defined before, but can also just be the same.
if (sprite.EnableDirectionOverride)
dir = sprite.DirectionOverride.Convert(state.Directions);;
dir = dir.OffsetRsiDir(layer.DirOffset);
if (_clickMapManager.IsOccluding(layer.ActualRsi, layer.State, dir, layer.AnimationFrame, layerImagePos))
return true;
}
drawDepth = default;
renderOrder = default;
return false;
}
public bool CheckDirBound(ISpriteComponent sprite, Angle relativeRotation, Vector2 localPos, Matrix3 spriteMatrix)
{
if (Bounds == null)
return false;
// Here we get sprite orientation, either from an explicit override or just from the relative rotation
var direction = relativeRotation.GetCardinalDir();
// Assuming the sprite snaps to 4 orientations we need to adjust our localPos relative to the entity by 90
// degree steps. Effectively, this accounts for the fact that the entity's world rotation + sprite rotation
// does not match the **actual** drawn rotation.
var modAngle = direction.ToAngle();
// If our sprite does not rotate at all, we shouldn't have been bothering with all that rotation logic,
// but it sorta came for free with the matrix transform, but we gotta undo that now.
if (sprite.NoRotation)
modAngle += CalcRectWorldAngle(relativeRotation, 4);
var spritePos = spriteMatrix.Transform(modAngle.RotateVec(localPos));
// First, check the bounding box that is valid for all orientations
if (Bounds.All.Contains(spritePos))
return true;
// Next, get and check the appropriate bounding box for the current sprite orientation
var boundsForDir = (sprite.EnableDirectionOverride ? sprite.DirectionOverride : direction) switch
{
Direction.East => Bounds.East,
Direction.North => Bounds.North,
Direction.South => Bounds.South,
Direction.West => Bounds.West,
_ => throw new InvalidOperationException()
};
return boundsForDir.Contains(spritePos);
}
[DataDefinition]
public sealed class DirBoundData
{
[ViewVariables] [DataField("all")] public Box2 All;
[ViewVariables] [DataField("north")] public Box2 North;
[ViewVariables] [DataField("south")] public Box2 South;
[ViewVariables] [DataField("east")] public Box2 East;
[ViewVariables] [DataField("west")] public Box2 West;
}
}
}