Sprite-based click detection.
This commit is contained in:
196
Content.Client/ClickMapManager.cs
Normal file
196
Content.Client/ClickMapManager.cs
Normal file
@@ -0,0 +1,196 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.Interfaces.ResourceManagement;
|
||||
using Robust.Client.ResourceManagement;
|
||||
using Robust.Client.Utility;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.ViewVariables;
|
||||
using SixLabors.ImageSharp;
|
||||
using SixLabors.ImageSharp.PixelFormats;
|
||||
|
||||
#nullable enable
|
||||
|
||||
namespace Content.Client
|
||||
{
|
||||
internal class ClickMapManager : IClickMapManager, IPostInjectInit
|
||||
{
|
||||
private const float Threshold = 0.25f;
|
||||
private const int ClickRadius = 0;
|
||||
|
||||
[Dependency] private readonly IResourceCache _resourceCache = default!;
|
||||
|
||||
[ViewVariables]
|
||||
private readonly Dictionary<Texture, ClickMap> _textureMaps = new Dictionary<Texture, ClickMap>();
|
||||
|
||||
[ViewVariables] private readonly Dictionary<RSI, RsiClickMapData> _rsiMaps =
|
||||
new Dictionary<RSI, RsiClickMapData>();
|
||||
|
||||
public void PostInject()
|
||||
{
|
||||
_resourceCache.OnRawTextureLoaded += OnRawTextureLoaded;
|
||||
_resourceCache.OnRsiLoaded += OnOnRsiLoaded;
|
||||
}
|
||||
|
||||
private void OnOnRsiLoaded(RsiLoadedEventArgs obj)
|
||||
{
|
||||
if (obj.Atlas is Image<Rgba32> rgba)
|
||||
{
|
||||
var clickMap = ClickMap.FromImage(rgba, Threshold);
|
||||
|
||||
var rsiData = new RsiClickMapData(clickMap, obj.AtlasOffsets);
|
||||
_rsiMaps[obj.Resource.RSI] = rsiData;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnRawTextureLoaded(TextureLoadedEventArgs obj)
|
||||
{
|
||||
if (obj.Image is Image<Rgba32> rgba)
|
||||
{
|
||||
_textureMaps[obj.Resource] = ClickMap.FromImage(rgba, Threshold);
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsOccluding(Texture texture, Vector2i pos)
|
||||
{
|
||||
if (!_textureMaps.TryGetValue(texture, out var clickMap))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return SampleClickMap(clickMap, pos, clickMap.Size, Vector2i.Zero);
|
||||
}
|
||||
|
||||
public bool IsOccluding(RSI rsi, RSI.StateId state, RSI.State.Direction dir, int frame, Vector2i pos)
|
||||
{
|
||||
if (!_rsiMaps.TryGetValue(rsi, out var rsiData))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var offset = rsiData.Offsets[state][(int) dir][frame];
|
||||
return SampleClickMap(rsiData.ClickMap, pos, rsi.Size, offset);
|
||||
}
|
||||
|
||||
private static bool SampleClickMap(ClickMap map, Vector2i pos, Vector2i bounds, Vector2i offset)
|
||||
{
|
||||
var (width, height) = bounds;
|
||||
var (px, py) = pos;
|
||||
|
||||
for (var x = -ClickRadius; x <= ClickRadius; x++)
|
||||
{
|
||||
var ox = px + x;
|
||||
if (ox < 0 || ox >= width)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
for (var y = -ClickRadius; y <= ClickRadius; y++)
|
||||
{
|
||||
var oy = py + y;
|
||||
|
||||
if (oy < 0 || oy >= height)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (map.IsOccluded((ox, oy) + offset))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private sealed class RsiClickMapData
|
||||
{
|
||||
public readonly ClickMap ClickMap;
|
||||
public readonly Dictionary<RSI.StateId, Vector2i[][]> Offsets;
|
||||
|
||||
public RsiClickMapData(ClickMap clickMap, Dictionary<RSI.StateId, Vector2i[][]> offsets)
|
||||
{
|
||||
ClickMap = clickMap;
|
||||
Offsets = offsets;
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class ClickMap
|
||||
{
|
||||
[ViewVariables] private readonly byte[] _data;
|
||||
|
||||
public int Width { get; }
|
||||
public int Height { get; }
|
||||
[ViewVariables] public Vector2i Size => (Width, Height);
|
||||
|
||||
public bool IsOccluded(int x, int y)
|
||||
{
|
||||
var i = y * Width + x;
|
||||
return (_data[i / 8] & (1 << (i % 8))) != 0;
|
||||
}
|
||||
|
||||
public bool IsOccluded(Vector2i vector)
|
||||
{
|
||||
var (x, y) = vector;
|
||||
return IsOccluded(x, y);
|
||||
}
|
||||
|
||||
private ClickMap(byte[] data, int width, int height)
|
||||
{
|
||||
Width = width;
|
||||
Height = height;
|
||||
_data = data;
|
||||
}
|
||||
|
||||
public static ClickMap FromImage<T>(Image<T> image, float threshold) where T : unmanaged, IPixel<T>
|
||||
{
|
||||
var threshByte = (byte) (threshold * 255);
|
||||
var width = image.Width;
|
||||
var height = image.Height;
|
||||
|
||||
var dataSize = (int) Math.Ceiling(width * height / 8f);
|
||||
var data = new byte[dataSize];
|
||||
|
||||
var pixelSpan = image.GetPixelSpan();
|
||||
|
||||
for (var i = 0; i < pixelSpan.Length; i++)
|
||||
{
|
||||
Rgba32 rgba = default;
|
||||
pixelSpan[i].ToRgba32(ref rgba);
|
||||
if (rgba.A >= threshByte)
|
||||
{
|
||||
data[i / 8] |= (byte) (1 << (i % 8));
|
||||
}
|
||||
}
|
||||
|
||||
return new ClickMap(data, width, height);
|
||||
}
|
||||
|
||||
public string DumpText()
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
for (var y = 0; y < Height; y++)
|
||||
{
|
||||
for (var x = 0; x < Width; x++)
|
||||
{
|
||||
sb.Append(IsOccluded(x, y) ? "1" : "0");
|
||||
}
|
||||
|
||||
sb.AppendLine();
|
||||
}
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public interface IClickMapManager
|
||||
{
|
||||
public bool IsOccluding(Texture texture, Vector2i pos);
|
||||
|
||||
public bool IsOccluding(RSI rsi, RSI.StateId state, RSI.State.Direction dir, int frame, Vector2i pos);
|
||||
}
|
||||
}
|
||||
@@ -32,6 +32,7 @@ namespace Content.Client
|
||||
IoCManager.Register<IItemSlotManager, ItemSlotManager>();
|
||||
IoCManager.Register<IStylesheetManager, StylesheetManager>();
|
||||
IoCManager.Register<IScreenshotHook, ScreenshotHook>();
|
||||
IoCManager.Register<IClickMapManager, ClickMapManager>();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
146
Content.Client/GameObjects/Components/ClickableComponent.cs
Normal file
146
Content.Client/GameObjects/Components/ClickableComponent.cs
Normal file
@@ -0,0 +1,146 @@
|
||||
using System;
|
||||
using Robust.Client.Graphics.ClientEye;
|
||||
using Robust.Client.Interfaces.GameObjects.Components;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Interfaces.Serialization;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.Serialization;
|
||||
using Robust.Shared.ViewVariables;
|
||||
|
||||
#nullable enable
|
||||
|
||||
namespace Content.Client.GameObjects.Components
|
||||
{
|
||||
[RegisterComponent]
|
||||
public sealed class ClickableComponent : Component
|
||||
{
|
||||
public override string Name => "Clickable";
|
||||
|
||||
[Dependency] private readonly IClickMapManager _clickMapManager = default!;
|
||||
|
||||
[ViewVariables] private DirBoundData _data = default!;
|
||||
|
||||
public override void ExposeData(ObjectSerializer serializer)
|
||||
{
|
||||
base.ExposeData(serializer);
|
||||
|
||||
serializer.DataField(ref _data, "bounds", DirBoundData.Default);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Used to check whether a click worked.
|
||||
/// </summary>
|
||||
/// <param name="worldPos">The world position that was clicked.</param>
|
||||
/// <param name="drawDepth">
|
||||
/// The draw depth for the sprite that captured the click.
|
||||
/// </param>
|
||||
/// <returns>True if the click worked, false otherwise.</returns>
|
||||
public bool CheckClick(Vector2 worldPos, out int drawDepth, out uint renderOrder)
|
||||
{
|
||||
if (!Owner.TryGetComponent(out ISpriteComponent sprite) || !sprite.Visible)
|
||||
{
|
||||
drawDepth = default;
|
||||
renderOrder = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
var localPos = Owner.Transform.InvWorldMatrix.Transform(worldPos);
|
||||
|
||||
var worldRotation = Owner.Transform.WorldRotation;
|
||||
if (sprite.Directional)
|
||||
{
|
||||
localPos = new Angle(worldRotation).RotateVec(localPos);
|
||||
}
|
||||
else
|
||||
{
|
||||
localPos = new Angle(MathHelper.PiOver2).RotateVec(localPos);
|
||||
}
|
||||
|
||||
var localOffset = localPos * EyeManager.PixelsPerMeter * (1, -1);
|
||||
|
||||
var found = false;
|
||||
|
||||
if (_data.All.Contains(localPos))
|
||||
{
|
||||
found = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
// TODO: diagonal support?
|
||||
var dir = sprite.Directional ? worldRotation.GetCardinalDir() : Direction.South;
|
||||
var boundsForDir = dir switch
|
||||
{
|
||||
Direction.East => _data.East,
|
||||
Direction.North => _data.North,
|
||||
Direction.South => _data.South,
|
||||
Direction.West => _data.West,
|
||||
_ => throw new InvalidOperationException()
|
||||
};
|
||||
|
||||
if (boundsForDir.Contains(localPos))
|
||||
{
|
||||
found = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!found)
|
||||
{
|
||||
foreach (var layer in sprite.AllLayers)
|
||||
{
|
||||
if (layer.Texture != null)
|
||||
{
|
||||
if (_clickMapManager.IsOccluding(layer.Texture,
|
||||
(Vector2i) (localOffset + layer.Texture.Size / 2f)))
|
||||
{
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
else if (layer.RsiState != default)
|
||||
{
|
||||
var rsi = layer.ActualRsi;
|
||||
if (rsi == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var (mX, mY) = localOffset + rsi.Size / 2;
|
||||
|
||||
var dir = layer.EffectiveDirection(worldRotation);
|
||||
if (_clickMapManager.IsOccluding(rsi, layer.RsiState, dir,
|
||||
layer.AnimationFrame, ((int) mX, (int) mY)))
|
||||
{
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
drawDepth = sprite.DrawDepth;
|
||||
renderOrder = sprite.RenderOrder;
|
||||
return found;
|
||||
}
|
||||
|
||||
private sealed class DirBoundData : IExposeData
|
||||
{
|
||||
[ViewVariables] public Box2 All;
|
||||
[ViewVariables] public Box2 North;
|
||||
[ViewVariables] public Box2 South;
|
||||
[ViewVariables] public Box2 East;
|
||||
[ViewVariables] public Box2 West;
|
||||
|
||||
public static DirBoundData Default { get; } = new DirBoundData();
|
||||
|
||||
public void ExposeData(ObjectSerializer serializer)
|
||||
{
|
||||
serializer.DataField(ref All, "all", default);
|
||||
serializer.DataField(ref North, "north", default);
|
||||
serializer.DataField(ref South, "south", default);
|
||||
serializer.DataField(ref East, "east", default);
|
||||
serializer.DataField(ref West, "west", default);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Threading;
|
||||
using Content.Client.State;
|
||||
@@ -21,11 +22,13 @@ using Robust.Client.Player;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.Utility;
|
||||
using Robust.Shared.Containers;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.GameObjects.Systems;
|
||||
using Robust.Shared.Input;
|
||||
using Robust.Shared.Input.Binding;
|
||||
using Robust.Shared.Interfaces.GameObjects;
|
||||
using Robust.Shared.Interfaces.Map;
|
||||
using Robust.Shared.Interfaces.Timing;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Log;
|
||||
@@ -47,6 +50,7 @@ namespace Content.Client.GameObjects.EntitySystems
|
||||
[Dependency] private readonly IItemSlotManager _itemSlotManager;
|
||||
[Dependency] private readonly IGameTiming _gameTiming;
|
||||
[Dependency] private readonly IUserInterfaceManager _userInterfaceManager;
|
||||
[Dependency] private readonly IMapManager _mapManager;
|
||||
#pragma warning restore 649
|
||||
|
||||
private EntityList _currentEntityList;
|
||||
@@ -110,7 +114,9 @@ namespace Content.Client.GameObjects.EntitySystems
|
||||
return false;
|
||||
}
|
||||
|
||||
var entities = gameScreen.GetEntitiesUnderPosition(args.Coordinates);
|
||||
var mapCoordinates = args.Coordinates.ToMap(_mapManager);
|
||||
var entities = _entityManager.GetEntitiesIntersecting(mapCoordinates.MapId,
|
||||
Box2.CenteredAround(mapCoordinates.Position, (0.5f, 0.5f))).ToList();
|
||||
|
||||
if (entities.Count == 0)
|
||||
{
|
||||
@@ -119,9 +125,20 @@ namespace Content.Client.GameObjects.EntitySystems
|
||||
|
||||
_currentEntityList = new EntityList();
|
||||
_currentEntityList.OnPopupHide += CloseAllMenus;
|
||||
for (var i = 0; i < entities.Count; i++)
|
||||
var first = true;
|
||||
foreach (var entity in entities)
|
||||
{
|
||||
if (i != 0)
|
||||
if (!entity.TryGetComponent(out ISpriteComponent sprite) || !sprite.Visible)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (ContainerHelpers.TryGetContainer(entity, out var container) && !container.ShowContents)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!first)
|
||||
{
|
||||
_currentEntityList.List.AddChild(new PanelContainer
|
||||
{
|
||||
@@ -130,9 +147,8 @@ namespace Content.Client.GameObjects.EntitySystems
|
||||
});
|
||||
}
|
||||
|
||||
var entity = entities[i];
|
||||
|
||||
_currentEntityList.List.AddChild(new EntityButton(this, entity));
|
||||
first = false;
|
||||
}
|
||||
|
||||
_userInterfaceManager.ModalRoot.AddChild(_currentEntityList);
|
||||
|
||||
@@ -17,6 +17,7 @@ using Robust.Shared.Interfaces.Map;
|
||||
using Robust.Shared.Interfaces.Timing;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.Timing;
|
||||
|
||||
namespace Content.Client.State
|
||||
@@ -54,7 +55,9 @@ namespace Content.Client.State
|
||||
base.FrameUpdate(e);
|
||||
|
||||
var mousePosWorld = _eyeManager.ScreenToMap(_inputManager.MouseScreenPosition);
|
||||
var entityToClick = _userInterfaceManager.CurrentlyHovered != null ? null : GetEntityUnderPosition(mousePosWorld);
|
||||
var entityToClick = _userInterfaceManager.CurrentlyHovered != null
|
||||
? null
|
||||
: GetEntityUnderPosition(mousePosWorld);
|
||||
|
||||
var inRange = false;
|
||||
if (_playerManager.LocalPlayer.ControlledEntity != null && entityToClick != null)
|
||||
@@ -62,7 +65,10 @@ namespace Content.Client.State
|
||||
var playerPos = _playerManager.LocalPlayer.ControlledEntity.Transform.MapPosition;
|
||||
var entityPos = entityToClick.Transform.MapPosition;
|
||||
inRange = _entitySystemManager.GetEntitySystem<SharedInteractionSystem>()
|
||||
.InRangeUnobstructed(playerPos, entityPos, predicate:entity => entity == _playerManager.LocalPlayer.ControlledEntity || entity == entityToClick, insideBlockerValid:true);
|
||||
.InRangeUnobstructed(playerPos, entityPos,
|
||||
predicate: entity =>
|
||||
entity == _playerManager.LocalPlayer.ControlledEntity || entity == entityToClick,
|
||||
insideBlockerValid: true);
|
||||
}
|
||||
|
||||
InteractionOutlineComponent outline;
|
||||
@@ -72,6 +78,7 @@ namespace Content.Client.State
|
||||
{
|
||||
outline.UpdateInRange(inRange);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -103,17 +110,18 @@ namespace Content.Client.State
|
||||
public IList<IEntity> GetEntitiesUnderPosition(MapCoordinates coordinates)
|
||||
{
|
||||
// Find all the entities intersecting our click
|
||||
var entities = _entityManager.GetEntitiesIntersecting(coordinates.MapId, coordinates.Position);
|
||||
var entities = _entityManager.GetEntitiesIntersecting(coordinates.MapId,
|
||||
Box2.CenteredAround(coordinates.Position, (1, 1)));
|
||||
|
||||
// Check the entities against whether or not we can click them
|
||||
var foundEntities = new List<(IEntity clicked, int drawDepth)>();
|
||||
var foundEntities = new List<(IEntity clicked, int drawDepth, uint renderOrder)>();
|
||||
foreach (var entity in entities)
|
||||
{
|
||||
if (entity.TryGetComponent<IClientClickableComponent>(out var component)
|
||||
if (entity.TryGetComponent<ClickableComponent>(out var component)
|
||||
&& entity.Transform.IsMapTransform
|
||||
&& component.CheckClick(coordinates.Position, out var drawDepthClicked))
|
||||
&& component.CheckClick(coordinates.Position, out var drawDepthClicked, out var renderOrder))
|
||||
{
|
||||
foundEntities.Add((entity, drawDepthClicked));
|
||||
foundEntities.Add((entity, drawDepthClicked, renderOrder));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -126,9 +134,10 @@ namespace Content.Client.State
|
||||
return foundEntities.Select(a => a.clicked).ToList();
|
||||
}
|
||||
|
||||
internal class ClickableEntityComparer : IComparer<(IEntity clicked, int depth)>
|
||||
internal class ClickableEntityComparer : IComparer<(IEntity clicked, int depth, uint renderOrder)>
|
||||
{
|
||||
public int Compare((IEntity clicked, int depth) x, (IEntity clicked, int depth) y)
|
||||
public int Compare((IEntity clicked, int depth, uint renderOrder) x,
|
||||
(IEntity clicked, int depth, uint renderOrder) y)
|
||||
{
|
||||
var val = x.depth.CompareTo(y.depth);
|
||||
if (val != 0)
|
||||
@@ -136,9 +145,24 @@ namespace Content.Client.State
|
||||
return val;
|
||||
}
|
||||
|
||||
// Turning this off it can make picking stuff out of lockers and such up a bit annoying.
|
||||
/*
|
||||
val = x.renderOrder.CompareTo(y.renderOrder);
|
||||
if (val != 0)
|
||||
{
|
||||
return val;
|
||||
}
|
||||
*/
|
||||
|
||||
var transx = x.clicked.Transform;
|
||||
var transy = y.clicked.Transform;
|
||||
return transx.GridPosition.Y.CompareTo(transy.GridPosition.Y);
|
||||
val = transx.GridPosition.Y.CompareTo(transy.GridPosition.Y);
|
||||
if (val != 0)
|
||||
{
|
||||
return val;
|
||||
}
|
||||
|
||||
return x.clicked.Uid.CompareTo(y.clicked.Uid);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -43,6 +43,7 @@ namespace Content.Server
|
||||
"ItemStatus",
|
||||
"Marker",
|
||||
"EmergencyLight",
|
||||
"Clickable",
|
||||
};
|
||||
|
||||
foreach (var ignoreName in registerIgnore)
|
||||
|
||||
49
Content.Tests/Client/ClickMapTest.cs
Normal file
49
Content.Tests/Client/ClickMapTest.cs
Normal file
@@ -0,0 +1,49 @@
|
||||
using Content.Client;
|
||||
using NUnit.Framework;
|
||||
using SixLabors.ImageSharp;
|
||||
using SixLabors.ImageSharp.PixelFormats;
|
||||
|
||||
namespace Content.Tests.Client
|
||||
{
|
||||
[TestFixture]
|
||||
public class ClickMapTest
|
||||
{
|
||||
[Test]
|
||||
public void TestBasic()
|
||||
{
|
||||
var img = new Image<Rgba32>(2, 2)
|
||||
{
|
||||
[0, 0] = new Rgba32(0, 0, 0, 0f),
|
||||
[1, 0] = new Rgba32(0, 0, 0, 1f),
|
||||
[0, 1] = new Rgba32(0, 0, 0, 1f),
|
||||
[1, 1] = new Rgba32(0, 0, 0, 0f)
|
||||
};
|
||||
|
||||
var clickMap = ClickMapManager.ClickMap.FromImage(img, 0.5f);
|
||||
|
||||
Assert.That(clickMap.IsOccluded(0, 0), Is.False);
|
||||
Assert.That(clickMap.IsOccluded(1, 0), Is.True);
|
||||
Assert.That(clickMap.IsOccluded(0, 1), Is.True);
|
||||
Assert.That(clickMap.IsOccluded(1, 1), Is.False);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestThreshold()
|
||||
{
|
||||
var img = new Image<Rgba32>(2, 2)
|
||||
{
|
||||
[0, 0] = new Rgba32(0, 0, 0, 0f),
|
||||
[1, 0] = new Rgba32(0, 0, 0, 0.25f),
|
||||
[0, 1] = new Rgba32(0, 0, 0, 0.75f),
|
||||
[1, 1] = new Rgba32(0, 0, 0, 1f)
|
||||
};
|
||||
|
||||
var clickMap = ClickMapManager.ClickMap.FromImage(img, 0.5f);
|
||||
|
||||
Assert.That(clickMap.IsOccluded(0, 0), Is.False);
|
||||
Assert.That(clickMap.IsOccluded(1, 0), Is.False);
|
||||
Assert.That(clickMap.IsOccluded(0, 1), Is.True);
|
||||
Assert.That(clickMap.IsOccluded(1, 1), Is.True);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,11 @@
|
||||
name: "unpowered light"
|
||||
components:
|
||||
- type: Clickable
|
||||
bounds:
|
||||
south: 0.25,-0.5,0.75,0.5
|
||||
north: -0.25,-0.5,0.25,0.5
|
||||
east: -0.5,-0.5,0.5,0
|
||||
west: -0.5,0,0.5,0.5
|
||||
- type: InteractionOutline
|
||||
- type: Collidable
|
||||
- type: LoopingSound
|
||||
|
||||
@@ -69,6 +69,9 @@
|
||||
- type: Icon
|
||||
texture: Objects/Tools/cable_coil.png
|
||||
- type: WirePlacer
|
||||
- type: Clickable
|
||||
bounds:
|
||||
all: -0.15,-0.15,0.15,0.15
|
||||
|
||||
- type: entity
|
||||
id: CableStack1
|
||||
|
||||
Reference in New Issue
Block a user