]> git.smokeofanarchy.ru Git - space-station-14.git/commitdiff
Improved RCDs (#22799)
authorchromiumboy <50505512+chromiumboy@users.noreply.github.com>
Sun, 31 Mar 2024 04:29:47 +0000 (23:29 -0500)
committerGitHub <noreply@github.com>
Sun, 31 Mar 2024 04:29:47 +0000 (23:29 -0500)
* Initial radial menu prototyping for the RCD

* Radial UI buttons can send messages to the server

* Beginning to update RCDSystem

* RCD building system in progress

* Further updates

* Added extra effects, RCDSystem now reads RCD prototype data

* Replacing tiles is instant, multiple constructions are allowed, deconstruction is broken

* Added extra functionality to RadialContainers plus documentation

* Fixed localization of RCD UI strings

* Menu opens near cursor, added basic RCD

* Avoiding merge conflict

* Implemented atomized construction / deconstruction rules

* Increased RCD ammo base charges

* Moved input context definition to content

* Removed obsoleted code

* Updates to system

* Switch machine and computer frames for electrical cabling

* Added construction ghosts

* Fixed issue with keybind detection code

* Fixed RCD construction ghost mispredications

* Code clean up

* Updated deconstruction effects

* RCDs effects don't rotate

* Code clean up

* Balancing for ammo counts

* Code clean up

* Added missing localized strings

* More clean up

* Made directional window handling more robust

* Added documentation to radial menus and made them no longer dependent on Content

* Made radial containers more robust

* Further robustness to the radial menu

* The RCD submenu buttons are only shown when the destination layer has at least one children

* Expanded upon deconstructing plus construction balance

* Fixed line endings

* Updated list of RCD deconstructable entities. Now needs a component to deconstruct instead of a tag

* Bug fixes

* Revert unnecessary change

* Updated RCD strings

* Fixed bug

* More fixes

* Deconstructed tiles/subflooring convert to lattice instead

* Fixed failed tests (Linux doesn't like invalid spritespecifer paths)

* Fixing merge conflict

* Updated airlock assembly

* Fixing merge conflict

* Fixing merge conflict

* More fixing...

* Removed erroneous project file change

* Fixed string handling issue

* Trying to fix merge conflict

* Still fixing merge conflicts

* Balancing

* Hidden RCD construction ghosts when in 'build' mode

* Fixing merge conflict

* Implemented requested changes (Part 1)

* Added more requested changes

* Fix for failed test. Removed sussy null suppression

* Made requested changes - custom construction ghost system was replaced

* Fixing merge conflict

* Fixed merge conflict

* Fixed bug in RCD construction ghost validation

* Fixing merge conflict

* Merge conflict fixed

* Made required update

* Removed lingering RCD deconstruct tag

* Fixing merge conflict

* Merge conflict fixed

* Made requested changes

* Bug fixes and balancing

* Made string names more consistent

* Can no longer stack catwalks

100 files changed:
Content.Client/Input/ContentContexts.cs
Content.Client/RCD/AlignRCDConstruction.cs [new file with mode: 0644]
Content.Client/RCD/RCDConstructionGhostSystem.cs [new file with mode: 0644]
Content.Client/RCD/RCDMenu.xaml [new file with mode: 0644]
Content.Client/RCD/RCDMenu.xaml.cs [new file with mode: 0644]
Content.Client/RCD/RCDMenuBoundUserInterface.cs [new file with mode: 0644]
Content.Client/Stylesheets/StyleNano.cs
Content.Client/UserInterface/Controls/RadialContainer.cs [new file with mode: 0644]
Content.Client/UserInterface/Controls/RadialMenu.cs [new file with mode: 0644]
Content.Shared/Charges/Systems/SharedChargesSystem.cs
Content.Shared/Construction/Components/ComputerBoardComponent.cs
Content.Shared/RCD/Components/RCDAmmoComponent.cs
Content.Shared/RCD/Components/RCDComponent.cs
Content.Shared/RCD/Components/RCDDeconstructibleComponent.cs [new file with mode: 0644]
Content.Shared/RCD/RCDEvents.cs [new file with mode: 0644]
Content.Shared/RCD/RCDPrototype.cs [new file with mode: 0644]
Content.Shared/RCD/Systems/RCDSystem.cs
Resources/Locale/en-US/rcd/components/rcd-component.ftl
Resources/Locale/en-US/ui/general.ftl [new file with mode: 0644]
Resources/Prototypes/Catalog/Fills/Crates/engineering.yml
Resources/Prototypes/Entities/Effects/chemistry_effects.yml
Resources/Prototypes/Entities/Effects/rcd.yml
Resources/Prototypes/Entities/Markers/construction_ghost.yml
Resources/Prototypes/Entities/Objects/Tools/tools.yml
Resources/Prototypes/Entities/Structures/Doors/Airlocks/airlocks.yml
Resources/Prototypes/Entities/Structures/Doors/Airlocks/base_assembly.yml
Resources/Prototypes/Entities/Structures/Doors/Airlocks/base_structureairlocks.yml
Resources/Prototypes/Entities/Structures/Doors/Airlocks/external.yml
Resources/Prototypes/Entities/Structures/Doors/Airlocks/shuttle.yml
Resources/Prototypes/Entities/Structures/Doors/Firelocks/firelock.yml
Resources/Prototypes/Entities/Structures/Doors/Firelocks/frame.yml
Resources/Prototypes/Entities/Structures/Doors/MaterialDoors/material_doors.yml
Resources/Prototypes/Entities/Structures/Doors/SecretDoor/secret_door.yml
Resources/Prototypes/Entities/Structures/Doors/Windoors/assembly.yml
Resources/Prototypes/Entities/Structures/Doors/Windoors/base_structurewindoors.yml
Resources/Prototypes/Entities/Structures/Lighting/base_lighting.yml
Resources/Prototypes/Entities/Structures/Power/cable_terminal.yml
Resources/Prototypes/Entities/Structures/Power/cables.yml
Resources/Prototypes/Entities/Structures/Walls/fence_metal.yml
Resources/Prototypes/Entities/Structures/Walls/fence_wood.yml
Resources/Prototypes/Entities/Structures/Walls/grille.yml
Resources/Prototypes/Entities/Structures/Walls/railing.yml
Resources/Prototypes/Entities/Structures/Walls/walls.yml
Resources/Prototypes/Entities/Structures/Windows/mining.yml
Resources/Prototypes/Entities/Structures/Windows/plasma.yml
Resources/Prototypes/Entities/Structures/Windows/reinforced.yml
Resources/Prototypes/Entities/Structures/Windows/rplasma.yml
Resources/Prototypes/Entities/Structures/Windows/ruranium.yml
Resources/Prototypes/Entities/Structures/Windows/shuttle.yml
Resources/Prototypes/Entities/Structures/Windows/uranium.yml
Resources/Prototypes/Entities/Structures/Windows/window.yml
Resources/Prototypes/Entities/Structures/catwalk.yml
Resources/Prototypes/RCD/rcd.yml [new file with mode: 0644]
Resources/Prototypes/tags.yml
Resources/Textures/Effects/rcd.rsi/construct.png [deleted file]
Resources/Textures/Effects/rcd.rsi/construct0.png [new file with mode: 0644]
Resources/Textures/Effects/rcd.rsi/construct1.png [new file with mode: 0644]
Resources/Textures/Effects/rcd.rsi/construct2.png [new file with mode: 0644]
Resources/Textures/Effects/rcd.rsi/construct3.png [new file with mode: 0644]
Resources/Textures/Effects/rcd.rsi/construct4.png [new file with mode: 0644]
Resources/Textures/Effects/rcd.rsi/deconstruct2.png [new file with mode: 0644]
Resources/Textures/Effects/rcd.rsi/deconstruct4.png [new file with mode: 0644]
Resources/Textures/Effects/rcd.rsi/deconstruct6.png [new file with mode: 0644]
Resources/Textures/Effects/rcd.rsi/deconstruct8.png [new file with mode: 0644]
Resources/Textures/Effects/rcd.rsi/deconstructPreview.png [new file with mode: 0644]
Resources/Textures/Effects/rcd.rsi/meta.json
Resources/Textures/Interface/Radial/RCD/airlock.png [new file with mode: 0644]
Resources/Textures/Interface/Radial/RCD/airlocks.png [new file with mode: 0644]
Resources/Textures/Interface/Radial/RCD/bulb_light.png [new file with mode: 0644]
Resources/Textures/Interface/Radial/RCD/cable_terminal.png [new file with mode: 0644]
Resources/Textures/Interface/Radial/RCD/catwalk.png [new file with mode: 0644]
Resources/Textures/Interface/Radial/RCD/computer_frame.png [new file with mode: 0644]
Resources/Textures/Interface/Radial/RCD/computers_and_frames.png [new file with mode: 0644]
Resources/Textures/Interface/Radial/RCD/deconstruct.png [new file with mode: 0644]
Resources/Textures/Interface/Radial/RCD/directional.png [new file with mode: 0644]
Resources/Textures/Interface/Radial/RCD/directional_reinforced.png [new file with mode: 0644]
Resources/Textures/Interface/Radial/RCD/firelock.png [new file with mode: 0644]
Resources/Textures/Interface/Radial/RCD/glass_airlock.png [new file with mode: 0644]
Resources/Textures/Interface/Radial/RCD/grille.png [new file with mode: 0644]
Resources/Textures/Interface/Radial/RCD/hv_coil.png [new file with mode: 0644]
Resources/Textures/Interface/Radial/RCD/lighting.png [new file with mode: 0644]
Resources/Textures/Interface/Radial/RCD/lv_coil.png [new file with mode: 0644]
Resources/Textures/Interface/Radial/RCD/machine_frame.png [new file with mode: 0644]
Resources/Textures/Interface/Radial/RCD/metal_tile.png [new file with mode: 0644]
Resources/Textures/Interface/Radial/RCD/multicoil.png [new file with mode: 0644]
Resources/Textures/Interface/Radial/RCD/mv_coil.png [new file with mode: 0644]
Resources/Textures/Interface/Radial/RCD/plating.png [new file with mode: 0644]
Resources/Textures/Interface/Radial/RCD/reinforced_wall.png [new file with mode: 0644]
Resources/Textures/Interface/Radial/RCD/solid_wall.png [new file with mode: 0644]
Resources/Textures/Interface/Radial/RCD/tube_light.png [new file with mode: 0644]
Resources/Textures/Interface/Radial/RCD/walls_and_flooring.png [new file with mode: 0644]
Resources/Textures/Interface/Radial/RCD/window.png [new file with mode: 0644]
Resources/Textures/Interface/Radial/RCD/window_reinforced.png [new file with mode: 0644]
Resources/Textures/Interface/Radial/RCD/windows_and_grilles.png [new file with mode: 0644]
Resources/Textures/Interface/Radial/back_hover.png [new file with mode: 0644]
Resources/Textures/Interface/Radial/back_normal.png [new file with mode: 0644]
Resources/Textures/Interface/Radial/button_hover.png [new file with mode: 0644]
Resources/Textures/Interface/Radial/button_normal.png [new file with mode: 0644]
Resources/Textures/Interface/Radial/close_hover.png [new file with mode: 0644]
Resources/Textures/Interface/Radial/close_normal.png [new file with mode: 0644]

index 589de6d6a7804e9965675b893cf1832221a6126e..2e888b3df98f01d8bd5ee4d08eecde1218b1a85c 100644 (file)
@@ -45,6 +45,9 @@ namespace Content.Client.Input
             // Not in engine because the engine doesn't understand what a flipped object is
             common.AddFunction(ContentKeyFunctions.EditorFlipObject);
 
+            // Not in engine so that the RCD can rotate objects
+            common.AddFunction(EngineKeyFunctions.EditorRotateObject);
+
             var human = contexts.GetContext("human");
             human.AddFunction(EngineKeyFunctions.MoveUp);
             human.AddFunction(EngineKeyFunctions.MoveDown);
diff --git a/Content.Client/RCD/AlignRCDConstruction.cs b/Content.Client/RCD/AlignRCDConstruction.cs
new file mode 100644 (file)
index 0000000..da7b22c
--- /dev/null
@@ -0,0 +1,122 @@
+using System.Numerics;
+using Content.Client.Gameplay;
+using Content.Shared.Hands.Components;
+using Content.Shared.Interaction;
+using Content.Shared.RCD.Components;
+using Content.Shared.RCD.Systems;
+using Robust.Client.Placement;
+using Robust.Client.Player;
+using Robust.Client.State;
+using Robust.Shared.Map;
+using Robust.Shared.Map.Components;
+
+namespace Content.Client.RCD;
+
+public sealed class AlignRCDConstruction : PlacementMode
+{
+    [Dependency] private readonly IEntityManager _entityManager = default!;
+    [Dependency] private readonly IMapManager _mapManager = default!;
+    [Dependency] private readonly SharedMapSystem _mapSystem = default!;
+    [Dependency] private readonly RCDSystem _rcdSystem = default!;
+    [Dependency] private readonly SharedTransformSystem _transformSystem = default!;
+    [Dependency] private readonly IPlayerManager _playerManager = default!;
+    [Dependency] private readonly IStateManager _stateManager = default!;
+
+    private const float SearchBoxSize = 2f;
+    private const float PlaceColorBaseAlpha = 0.5f;
+
+    private EntityCoordinates _unalignedMouseCoords = default;
+
+    /// <summary>
+    /// This placement mode is not on the engine because it is content specific (i.e., for the RCD)
+    /// </summary>
+    public AlignRCDConstruction(PlacementManager pMan) : base(pMan)
+    {
+        var dependencies = IoCManager.Instance!;
+        _entityManager = dependencies.Resolve<IEntityManager>();
+        _mapManager = dependencies.Resolve<IMapManager>();
+        _playerManager = dependencies.Resolve<IPlayerManager>();
+        _stateManager = dependencies.Resolve<IStateManager>();
+
+        _mapSystem = _entityManager.System<SharedMapSystem>();
+        _rcdSystem = _entityManager.System<RCDSystem>();
+        _transformSystem = _entityManager.System<SharedTransformSystem>();
+
+        ValidPlaceColor = ValidPlaceColor.WithAlpha(PlaceColorBaseAlpha);
+    }
+
+    public override void AlignPlacementMode(ScreenCoordinates mouseScreen)
+    {
+        _unalignedMouseCoords = ScreenToCursorGrid(mouseScreen);
+        MouseCoords = _unalignedMouseCoords.AlignWithClosestGridTile(SearchBoxSize, _entityManager, _mapManager);
+
+        var gridId = MouseCoords.GetGridUid(_entityManager);
+
+        if (!_entityManager.TryGetComponent<MapGridComponent>(gridId, out var mapGrid))
+            return;
+
+        CurrentTile = _mapSystem.GetTileRef(gridId.Value, mapGrid, MouseCoords);
+
+        float tileSize = mapGrid.TileSize;
+        GridDistancing = tileSize;
+
+        if (pManager.CurrentPermission!.IsTile)
+        {
+            MouseCoords = new EntityCoordinates(MouseCoords.EntityId, new Vector2(CurrentTile.X + tileSize / 2,
+                CurrentTile.Y + tileSize / 2));
+        }
+        else
+        {
+            MouseCoords = new EntityCoordinates(MouseCoords.EntityId, new Vector2(CurrentTile.X + tileSize / 2 + pManager.PlacementOffset.X,
+                CurrentTile.Y + tileSize / 2 + pManager.PlacementOffset.Y));
+        }
+    }
+
+    public override bool IsValidPosition(EntityCoordinates position)
+    {
+        var player = _playerManager.LocalSession?.AttachedEntity;
+
+        // If the destination is out of interaction range, set the placer alpha to zero
+        if (!_entityManager.TryGetComponent<TransformComponent>(player, out var xform))
+            return false;
+
+        if (!xform.Coordinates.InRange(_entityManager, _transformSystem, position, SharedInteractionSystem.InteractionRange))
+        {
+            InvalidPlaceColor = InvalidPlaceColor.WithAlpha(0);
+            return false;
+        }
+
+        // Otherwise restore the alpha value
+        else
+        {
+            InvalidPlaceColor = InvalidPlaceColor.WithAlpha(PlaceColorBaseAlpha);
+        }
+
+        // Determine if player is carrying an RCD in their active hand
+        if (!_entityManager.TryGetComponent<HandsComponent>(player, out var hands))
+            return false;
+
+        var heldEntity = hands.ActiveHand?.HeldEntity;
+
+        if (!_entityManager.TryGetComponent<RCDComponent>(heldEntity, out var rcd))
+            return false;
+
+        // Retrieve the map grid data for the position
+        if (!_rcdSystem.TryGetMapGridData(position, out var mapGridData))
+            return false;
+
+        // Determine if the user is hovering over a target
+        var currentState = _stateManager.CurrentState;
+
+        if (currentState is not GameplayStateBase screen)
+            return false;
+
+        var target = screen.GetClickedEntity(_unalignedMouseCoords.ToMap(_entityManager, _transformSystem));
+
+        // Determine if the RCD operation is valid or not
+        if (!_rcdSystem.IsRCDOperationStillValid(heldEntity.Value, rcd, mapGridData.Value, target, player.Value, false))
+            return false;
+
+        return true;
+    }
+}
diff --git a/Content.Client/RCD/RCDConstructionGhostSystem.cs b/Content.Client/RCD/RCDConstructionGhostSystem.cs
new file mode 100644 (file)
index 0000000..792916b
--- /dev/null
@@ -0,0 +1,78 @@
+using Content.Shared.Hands.Components;
+using Content.Shared.Interaction;
+using Content.Shared.RCD;
+using Content.Shared.RCD.Components;
+using Content.Shared.RCD.Systems;
+using Robust.Client.Placement;
+using Robust.Client.Player;
+using Robust.Shared.Enums;
+
+namespace Content.Client.RCD;
+
+public sealed class RCDConstructionGhostSystem : EntitySystem
+{
+    [Dependency] private readonly IPlayerManager _playerManager = default!;
+    [Dependency] private readonly RCDSystem _rcdSystem = default!;
+    [Dependency] private readonly IPlacementManager _placementManager = default!;
+
+    private string _placementMode = typeof(AlignRCDConstruction).Name;
+    private Direction _placementDirection = default;
+
+    public override void Update(float frameTime)
+    {
+        base.Update(frameTime);
+
+        // Get current placer data
+        var placerEntity = _placementManager.CurrentPermission?.MobUid;
+        var placerProto = _placementManager.CurrentPermission?.EntityType;
+        var placerIsRCD = HasComp<RCDComponent>(placerEntity);
+
+        // Exit if erasing or the current placer is not an RCD (build mode is active)
+        if (_placementManager.Eraser || (placerEntity != null && !placerIsRCD))
+            return;
+
+        // Determine if player is carrying an RCD in their active hand
+        var player = _playerManager.LocalSession?.AttachedEntity;
+
+        if (!TryComp<HandsComponent>(player, out var hands))
+            return;
+
+        var heldEntity = hands.ActiveHand?.HeldEntity;
+
+        if (!TryComp<RCDComponent>(heldEntity, out var rcd))
+        {
+            // If the player was holding an RCD, but is no longer, cancel placement
+            if (placerIsRCD)
+                _placementManager.Clear();
+
+            return;
+        }
+
+        // Update the direction the RCD prototype based on the placer direction
+        if (_placementDirection != _placementManager.Direction)
+        {
+            _placementDirection = _placementManager.Direction;
+            RaiseNetworkEvent(new RCDConstructionGhostRotationEvent(GetNetEntity(heldEntity.Value), _placementDirection));
+        }
+
+        // If the placer has not changed, exit
+        _rcdSystem.UpdateCachedPrototype(heldEntity.Value, rcd);
+
+        if (heldEntity == placerEntity && rcd.CachedPrototype.Prototype == placerProto)
+            return;
+
+        // Create a new placer
+        var newObjInfo = new PlacementInformation
+        {
+            MobUid = heldEntity.Value,
+            PlacementOption = _placementMode,
+            EntityType = rcd.CachedPrototype.Prototype,
+            Range = (int) Math.Ceiling(SharedInteractionSystem.InteractionRange),
+            IsTile = (rcd.CachedPrototype.Mode == RcdMode.ConstructTile),
+            UseEditorContext = false,
+        };
+
+        _placementManager.Clear();
+        _placementManager.BeginPlacing(newObjInfo);
+    }
+}
diff --git a/Content.Client/RCD/RCDMenu.xaml b/Content.Client/RCD/RCDMenu.xaml
new file mode 100644 (file)
index 0000000..b3d5367
--- /dev/null
@@ -0,0 +1,47 @@
+<ui:RadialMenu xmlns="https://spacestation14.io"
+                xmlns:ui="clr-namespace:Content.Client.UserInterface.Controls"
+                xmlns:rcd="clr-namespace:Content.Client.RCD"
+                BackButtonStyleClass="RadialMenuBackButton"
+                CloseButtonStyleClass="RadialMenuCloseButton"
+                VerticalExpand="True"
+                HorizontalExpand="True"
+                MinSize="450 450">
+    
+    <!-- Note: The min size of the window just determine how close to the edge of the screen the center of the radial menu can be placed -->
+    <!-- The radial menu will try to open so that its center is located where the player's cursor is currently -->
+
+    <!-- Entry layer (shows main categories) -->
+    <ui:RadialContainer Name="Main" VerticalExpand="True" HorizontalExpand="True" Radius="64" ReserveSpaceForHiddenChildren="False">
+        <ui:RadialMenuTextureButton StyleClasses="RadialMenuButton" SetSize="64 64" ToolTip="{Loc 'rcd-component-walls-and-flooring'}" TargetLayer="WallsAndFlooring" Visible="False">
+            <TextureRect VerticalAlignment="Center" HorizontalAlignment="Center" TextureScale="2 2" TexturePath="/Textures/Interface/Radial/RCD/walls_and_flooring.png"/>
+        </ui:RadialMenuTextureButton>
+        <ui:RadialMenuTextureButton StyleClasses="RadialMenuButton" SetSize="64 64" ToolTip="{Loc 'rcd-component-windows-and-grilles'}" TargetLayer="WindowsAndGrilles" Visible="False">
+            <TextureRect VerticalAlignment="Center" HorizontalAlignment="Center" TextureScale="2 2" TexturePath="/Textures/Interface/Radial/RCD/windows_and_grilles.png"/>
+        </ui:RadialMenuTextureButton>
+        <ui:RadialMenuTextureButton StyleClasses="RadialMenuButton" SetSize="64 64" ToolTip="{Loc 'rcd-component-airlocks'}" TargetLayer="Airlocks" Visible="False">
+            <TextureRect VerticalAlignment="Center" HorizontalAlignment="Center" TextureScale="2 2" TexturePath="/Textures/Interface/Radial/RCD/airlocks.png"/>
+        </ui:RadialMenuTextureButton>
+        <ui:RadialMenuTextureButton StyleClasses="RadialMenuButton" SetSize="64 64" ToolTip="{Loc 'rcd-component-electrical'}" TargetLayer="Electrical" Visible="False">
+            <TextureRect VerticalAlignment="Center" HorizontalAlignment="Center" TextureScale="2 2" TexturePath="/Textures/Interface/Radial/RCD/multicoil.png"/>
+        </ui:RadialMenuTextureButton>
+        <ui:RadialMenuTextureButton StyleClasses="RadialMenuButton" SetSize="64 64" ToolTip="{Loc 'rcd-component-lighting'}" TargetLayer="Lighting" Visible="False">
+            <TextureRect VerticalAlignment="Center" HorizontalAlignment="Center" TextureScale="2 2" TexturePath="/Textures/Interface/Radial/RCD/lighting.png"/>
+        </ui:RadialMenuTextureButton>
+    </ui:RadialContainer>
+
+    <!-- Walls and flooring -->
+    <ui:RadialContainer Name="WallsAndFlooring"  VerticalExpand="True" HorizontalExpand="True" Radius="64"/>
+
+    <!-- Windows and grilles -->
+    <ui:RadialContainer Name="WindowsAndGrilles"  VerticalExpand="True" HorizontalExpand="True" Radius="64"/>
+
+    <!-- Airlocks -->
+    <ui:RadialContainer Name="Airlocks"  VerticalExpand="True" HorizontalExpand="True" Radius="64"/>
+
+    <!-- Computer and machine frames -->
+    <ui:RadialContainer Name="Electrical"  VerticalExpand="True" HorizontalExpand="True" Radius="64"/>
+       
+    <!-- Lighting -->
+    <ui:RadialContainer Name="Lighting"  VerticalExpand="True" HorizontalExpand="True" Radius="64"/>
+
+</ui:RadialMenu>
diff --git a/Content.Client/RCD/RCDMenu.xaml.cs b/Content.Client/RCD/RCDMenu.xaml.cs
new file mode 100644 (file)
index 0000000..8679e78
--- /dev/null
@@ -0,0 +1,137 @@
+using Content.Client.UserInterface.Controls;
+using Content.Shared.RCD;
+using Content.Shared.RCD.Components;
+using Robust.Client.AutoGenerated;
+using Robust.Client.GameObjects;
+using Robust.Client.UserInterface;
+using Robust.Client.UserInterface.Controls;
+using Robust.Client.UserInterface.XAML;
+using Robust.Shared.Prototypes;
+using System.Numerics;
+
+namespace Content.Client.RCD;
+
+[GenerateTypedNameReferences]
+public sealed partial class RCDMenu : RadialMenu
+{
+    [Dependency] private readonly EntityManager _entManager = default!;
+    [Dependency] private readonly IPrototypeManager _protoManager = default!;
+
+    private readonly SpriteSystem _spriteSystem;
+
+    public event Action<ProtoId<RCDPrototype>>? SendRCDSystemMessageAction;
+
+    public RCDMenu(EntityUid owner, RCDMenuBoundUserInterface bui)
+    {
+        IoCManager.InjectDependencies(this);
+        RobustXamlLoader.Load(this);
+
+        _spriteSystem = _entManager.System<SpriteSystem>();
+
+        // Find the main radial container
+        var main = FindControl<RadialContainer>("Main");
+
+        if (main == null)
+            return;
+
+        // Populate secondary radial containers
+        if (!_entManager.TryGetComponent<RCDComponent>(owner, out var rcd))
+            return;
+
+        foreach (var protoId in rcd.AvailablePrototypes)
+        {
+            if (!_protoManager.TryIndex(protoId, out var proto))
+                continue;
+
+            if (proto.Mode == RcdMode.Invalid)
+                continue;
+
+            var parent = FindControl<RadialContainer>(proto.Category);
+
+            if (parent == null)
+                continue;
+
+            var name = Loc.GetString(proto.SetName);
+            name = char.ToUpper(name[0]) + name.Remove(0, 1);
+
+            var button = new RCDMenuButton()
+            {
+                StyleClasses = { "RadialMenuButton" },
+                SetSize = new Vector2(64f, 64f),
+                ToolTip = name,
+                ProtoId = protoId,
+            };
+
+            if (proto.Sprite != null)
+            {
+                var tex = new TextureRect()
+                {
+                    VerticalAlignment = VAlignment.Center,
+                    HorizontalAlignment = HAlignment.Center,
+                    Texture = _spriteSystem.Frame0(proto.Sprite),
+                    TextureScale = new Vector2(2f, 2f),
+                };
+
+                button.AddChild(tex);
+            }
+
+            parent.AddChild(button);
+
+            // Ensure that the button that transitions the menu to the associated category layer
+            // is visible in the main radial container (as these all start with Visible = false)
+            foreach (var child in main.Children)
+            {
+                var castChild = child as RadialMenuTextureButton;
+
+                if (castChild is not RadialMenuTextureButton)
+                    continue;
+
+                if (castChild.TargetLayer == proto.Category)
+                {
+                    castChild.Visible = true;
+                    break;
+                }
+            }
+        }
+
+        // Set up menu actions
+        foreach (var child in Children)
+            AddRCDMenuButtonOnClickActions(child);
+
+        OnChildAdded += AddRCDMenuButtonOnClickActions;
+
+        SendRCDSystemMessageAction += bui.SendRCDSystemMessage;
+    }
+
+    private void AddRCDMenuButtonOnClickActions(Control control)
+    {
+        var radialContainer = control as RadialContainer;
+
+        if (radialContainer == null)
+            return;
+
+        foreach (var child in radialContainer.Children)
+        {
+            var castChild = child as RCDMenuButton;
+
+            if (castChild == null)
+                continue;
+
+            castChild.OnButtonUp += _ =>
+            {
+                SendRCDSystemMessageAction?.Invoke(castChild.ProtoId);
+                Close();
+            };
+        }
+    }
+}
+
+public sealed class RCDMenuButton : RadialMenuTextureButton
+{
+    public ProtoId<RCDPrototype> ProtoId { get; set; }
+
+    public RCDMenuButton()
+    {
+
+    }
+}
diff --git a/Content.Client/RCD/RCDMenuBoundUserInterface.cs b/Content.Client/RCD/RCDMenuBoundUserInterface.cs
new file mode 100644 (file)
index 0000000..a37dbce
--- /dev/null
@@ -0,0 +1,49 @@
+using Content.Shared.RCD;
+using Content.Shared.RCD.Components;
+using JetBrains.Annotations;
+using Robust.Client.Graphics;
+using Robust.Client.Input;
+using Robust.Shared.Prototypes;
+
+namespace Content.Client.RCD;
+
+[UsedImplicitly]
+public sealed class RCDMenuBoundUserInterface : BoundUserInterface
+{
+    [Dependency] private readonly IClyde _displayManager = default!;
+    [Dependency] private readonly IInputManager _inputManager = default!;
+
+    private RCDMenu? _menu;
+
+    public RCDMenuBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey)
+    {
+        IoCManager.InjectDependencies(this);
+    }
+
+    protected override void Open()
+    {
+        base.Open();
+
+        _menu = new(Owner, this);
+        _menu.OnClose += Close;
+
+        // Open the menu, centered on the mouse
+        var vpSize = _displayManager.ScreenSize;
+        _menu.OpenCenteredAt(_inputManager.MouseScreenPosition.Position / vpSize);
+    }
+
+    public void SendRCDSystemMessage(ProtoId<RCDPrototype> protoId)
+    {
+        // A predicted message cannot be used here as the RCD UI is closed immediately 
+        // after this message is sent, which will stop the server from receiving it
+        SendMessage(new RCDSystemMessage(protoId));
+    }
+
+    protected override void Dispose(bool disposing)
+    {
+        base.Dispose(disposing);
+        if (!disposing) return;
+
+        _menu?.Dispose();
+    }
+}
index 426af1616eced8d91b339ea6fdd692afb375c296..2c7a1873a36827f57b85bdd75dc5e33f8a409fc9 100644 (file)
@@ -290,7 +290,7 @@ namespace Content.Client.Stylesheets
             var buttonTex = resCache.GetTexture("/Textures/Interface/Nano/button.svg.96dpi.png");
             var topButtonBase = new StyleBoxTexture
             {
-             Texture = buttonTex,
+                Texture = buttonTex,
             };
             topButtonBase.SetPatchMargin(StyleBox.Margin.All, 10);
             topButtonBase.SetPadding(StyleBox.Margin.All, 0);
@@ -298,19 +298,19 @@ namespace Content.Client.Stylesheets
 
             var topButtonOpenRight = new StyleBoxTexture(topButtonBase)
             {
-             Texture = new AtlasTexture(buttonTex, UIBox2.FromDimensions(new Vector2(0, 0), new Vector2(14, 24))),
+                Texture = new AtlasTexture(buttonTex, UIBox2.FromDimensions(new Vector2(0, 0), new Vector2(14, 24))),
             };
             topButtonOpenRight.SetPatchMargin(StyleBox.Margin.Right, 0);
 
             var topButtonOpenLeft = new StyleBoxTexture(topButtonBase)
             {
-             Texture = new AtlasTexture(buttonTex, UIBox2.FromDimensions(new Vector2(10, 0), new Vector2(14, 24))),
+                Texture = new AtlasTexture(buttonTex, UIBox2.FromDimensions(new Vector2(10, 0), new Vector2(14, 24))),
             };
             topButtonOpenLeft.SetPatchMargin(StyleBox.Margin.Left, 0);
 
             var topButtonSquare = new StyleBoxTexture(topButtonBase)
             {
-             Texture = new AtlasTexture(buttonTex, UIBox2.FromDimensions(new Vector2(10, 0), new Vector2(3, 24))),
+                Texture = new AtlasTexture(buttonTex, UIBox2.FromDimensions(new Vector2(10, 0), new Vector2(3, 24))),
             };
             topButtonSquare.SetPatchMargin(StyleBox.Margin.Horizontal, 0);
 
@@ -368,9 +368,9 @@ namespace Content.Client.Stylesheets
             };
             tabContainerPanel.SetPatchMargin(StyleBox.Margin.All, 2);
 
-            var tabContainerBoxActive = new StyleBoxFlat {BackgroundColor = new Color(64, 64, 64)};
+            var tabContainerBoxActive = new StyleBoxFlat { BackgroundColor = new Color(64, 64, 64) };
             tabContainerBoxActive.SetContentMarginOverride(StyleBox.Margin.Horizontal, 5);
-            var tabContainerBoxInactive = new StyleBoxFlat {BackgroundColor = new Color(32, 32, 32)};
+            var tabContainerBoxInactive = new StyleBoxFlat { BackgroundColor = new Color(32, 32, 32) };
             tabContainerBoxInactive.SetContentMarginOverride(StyleBox.Margin.Horizontal, 5);
 
             var progressBarBackground = new StyleBoxFlat
@@ -409,21 +409,21 @@ namespace Content.Client.Stylesheets
 
             // Placeholder
             var placeholderTexture = resCache.GetTexture("/Textures/Interface/Nano/placeholder.png");
-            var placeholder = new StyleBoxTexture {Texture = placeholderTexture};
+            var placeholder = new StyleBoxTexture { Texture = placeholderTexture };
             placeholder.SetPatchMargin(StyleBox.Margin.All, 19);
             placeholder.SetExpandMargin(StyleBox.Margin.All, -5);
             placeholder.Mode = StyleBoxTexture.StretchMode.Tile;
 
-            var itemListBackgroundSelected = new StyleBoxFlat {BackgroundColor = new Color(75, 75, 86)};
+            var itemListBackgroundSelected = new StyleBoxFlat { BackgroundColor = new Color(75, 75, 86) };
             itemListBackgroundSelected.SetContentMarginOverride(StyleBox.Margin.Vertical, 2);
             itemListBackgroundSelected.SetContentMarginOverride(StyleBox.Margin.Horizontal, 4);
-            var itemListItemBackgroundDisabled = new StyleBoxFlat {BackgroundColor = new Color(10, 10, 12)};
+            var itemListItemBackgroundDisabled = new StyleBoxFlat { BackgroundColor = new Color(10, 10, 12) };
             itemListItemBackgroundDisabled.SetContentMarginOverride(StyleBox.Margin.Vertical, 2);
             itemListItemBackgroundDisabled.SetContentMarginOverride(StyleBox.Margin.Horizontal, 4);
-            var itemListItemBackground = new StyleBoxFlat {BackgroundColor = new Color(55, 55, 68)};
+            var itemListItemBackground = new StyleBoxFlat { BackgroundColor = new Color(55, 55, 68) };
             itemListItemBackground.SetContentMarginOverride(StyleBox.Margin.Vertical, 2);
             itemListItemBackground.SetContentMarginOverride(StyleBox.Margin.Horizontal, 4);
-            var itemListItemBackgroundTransparent = new StyleBoxFlat {BackgroundColor = Color.Transparent};
+            var itemListItemBackgroundTransparent = new StyleBoxFlat { BackgroundColor = Color.Transparent };
             itemListItemBackgroundTransparent.SetContentMarginOverride(StyleBox.Margin.Vertical, 2);
             itemListItemBackgroundTransparent.SetContentMarginOverride(StyleBox.Margin.Horizontal, 4);
 
@@ -489,9 +489,9 @@ namespace Content.Client.Stylesheets
             sliderForeBox.SetPatchMargin(StyleBox.Margin.All, 12);
             sliderGrabBox.SetPatchMargin(StyleBox.Margin.All, 12);
 
-            var sliderFillGreen = new StyleBoxTexture(sliderFillBox) {Modulate = Color.LimeGreen};
-            var sliderFillRed = new StyleBoxTexture(sliderFillBox) {Modulate = Color.Red};
-            var sliderFillBlue = new StyleBoxTexture(sliderFillBox) {Modulate = Color.Blue};
+            var sliderFillGreen = new StyleBoxTexture(sliderFillBox) { Modulate = Color.LimeGreen };
+            var sliderFillRed = new StyleBoxTexture(sliderFillBox) { Modulate = Color.Red };
+            var sliderFillBlue = new StyleBoxTexture(sliderFillBox) { Modulate = Color.Blue };
             var sliderFillWhite = new StyleBoxTexture(sliderFillBox) { Modulate = Color.White };
 
             var boxFont13 = resCache.GetFont("/Fonts/Boxfont-round/Boxfont Round.ttf", 13);
@@ -1468,6 +1468,25 @@ namespace Content.Client.Stylesheets
                 Element<Label>().Class("Disabled")
                     .Prop("font-color", DisabledFore),
 
+                // Radial menu buttons
+                Element<TextureButton>().Class("RadialMenuButton")
+                    .Prop(TextureButton.StylePropertyTexture, resCache.GetTexture("/Textures/Interface/Radial/button_normal.png")),
+                Element<TextureButton>().Class("RadialMenuButton")
+                    .Pseudo(TextureButton.StylePseudoClassHover)
+                    .Prop(TextureButton.StylePropertyTexture, resCache.GetTexture("/Textures/Interface/Radial/button_hover.png")),
+
+                Element<TextureButton>().Class("RadialMenuCloseButton")
+                    .Prop(TextureButton.StylePropertyTexture, resCache.GetTexture("/Textures/Interface/Radial/close_normal.png")),
+                Element<TextureButton>().Class("RadialMenuCloseButton")
+                    .Pseudo(TextureButton.StylePseudoClassHover)
+                    .Prop(TextureButton.StylePropertyTexture, resCache.GetTexture("/Textures/Interface/Radial/close_hover.png")),
+
+                Element<TextureButton>().Class("RadialMenuBackButton")
+                    .Prop(TextureButton.StylePropertyTexture, resCache.GetTexture("/Textures/Interface/Radial/back_normal.png")),
+                Element<TextureButton>().Class("RadialMenuBackButton")
+                    .Pseudo(TextureButton.StylePseudoClassHover)
+                    .Prop(TextureButton.StylePropertyTexture, resCache.GetTexture("/Textures/Interface/Radial/back_hover.png")),
+
                 //PDA - Backgrounds
                 Element<PanelContainer>().Class("PdaContentBackground")
                     .Prop(PanelContainer.StylePropertyPanel, BaseButtonOpenBoth)
diff --git a/Content.Client/UserInterface/Controls/RadialContainer.cs b/Content.Client/UserInterface/Controls/RadialContainer.cs
new file mode 100644 (file)
index 0000000..be263d1
--- /dev/null
@@ -0,0 +1,105 @@
+using Robust.Client.Graphics;
+using Robust.Client.UserInterface.Controls;
+using System.Linq;
+using System.Numerics;
+
+namespace Content.Client.UserInterface.Controls;
+
+[Virtual]
+public class RadialContainer : LayoutContainer
+{
+    /// <summary>
+    /// Specifies the anglular range, in radians, in which child elements will be placed.
+    /// The first value denotes the angle at which the first element is to be placed, and
+    /// the second value denotes the angle at which the last element is to be placed.
+    /// Both values must be between 0 and 2 PI radians
+    /// </summary>
+    /// <remarks>
+    /// The top of the screen is at 0 radians, and the bottom of the screen is at PI radians
+    /// </remarks>
+    [ViewVariables(VVAccess.ReadWrite)]
+    public Vector2 AngularRange
+    {
+        get
+        {
+            return _angularRange;
+        }
+
+        set
+        {
+            var x = value.X;
+            var y = value.Y;
+
+            x = x > MathF.Tau ? x % MathF.Tau : x;
+            y = y > MathF.Tau ? y % MathF.Tau : y;
+
+            x = x < 0 ? MathF.Tau + x : x;
+            y = y < 0 ? MathF.Tau + y : y;
+
+            _angularRange = new Vector2(x, y);
+        }
+    }
+
+    private Vector2 _angularRange = new Vector2(0f, MathF.Tau - float.Epsilon);
+
+    /// <summary>
+    /// Determines the direction in which child elements will be arranged
+    /// </summary>
+    [ViewVariables(VVAccess.ReadWrite)]
+    public RAlignment RadialAlignment { get; set; } = RAlignment.Clockwise;
+
+    /// <summary>
+    /// Determines how far from the radial container's center that its child elements will be placed
+    /// </summary>
+    [ViewVariables(VVAccess.ReadWrite)]
+    public float Radius { get; set; } = 100f;
+
+    /// <summary>
+    /// Sets whether the container should reserve a space on the layout for child which are not currently visible
+    /// </summary>
+    [ViewVariables(VVAccess.ReadWrite)]
+    public bool ReserveSpaceForHiddenChildren { get; set; } = true;
+
+    /// <summary>
+    /// This container arranges its children, evenly separated, in a radial pattern
+    /// </summary>
+    public RadialContainer()
+    {
+
+    }
+
+    protected override void Draw(DrawingHandleScreen handle)
+    {
+        var children = ReserveSpaceForHiddenChildren ? Children : Children.Where(x => x.Visible);
+        var childCount = children.Count();
+
+        // Determine the size of the arc, accounting for clockwise and anti-clockwise arrangements
+        var arc = AngularRange.Y - AngularRange.X;
+        arc = (arc < 0) ? MathF.Tau + arc : arc;
+        arc = (RadialAlignment == RAlignment.AntiClockwise) ? MathF.Tau - arc : arc;
+
+        // Account for both circular arrangements and arc-based arrangements
+        var childMod = MathHelper.CloseTo(arc, MathF.Tau, 0.01f) ? 0 : 1;
+
+        // Determine the separation between child elements
+        var sepAngle = arc / (childCount - childMod);
+        sepAngle *= (RadialAlignment == RAlignment.AntiClockwise) ? -1f : 1f;
+
+        // Adjust the positions of all the child elements
+        foreach (var (i, child) in children.Select((x, i) => (i, x)))
+        {
+            var position = new Vector2(Radius * MathF.Sin(AngularRange.X + sepAngle * i) + Width / 2f - child.Width / 2f, -Radius * MathF.Cos(AngularRange.X + sepAngle * i) + Height / 2f - child.Height / 2f);
+            SetPosition(child, position);
+        }
+    }
+
+    /// <summary>
+    /// Specifies the different radial alignment modes
+    /// </summary>
+    /// <seealso cref="RadialAlignment"/>
+    public enum RAlignment : byte
+    {
+        Clockwise,
+        AntiClockwise,
+    }
+}
diff --git a/Content.Client/UserInterface/Controls/RadialMenu.cs b/Content.Client/UserInterface/Controls/RadialMenu.cs
new file mode 100644 (file)
index 0000000..5f56ad7
--- /dev/null
@@ -0,0 +1,255 @@
+using Robust.Client.UserInterface;
+using Robust.Client.UserInterface.Controls;
+using Robust.Client.UserInterface.CustomControls;
+using System.Linq;
+using System.Numerics;
+
+namespace Content.Client.UserInterface.Controls;
+
+[Virtual]
+public class RadialMenu : BaseWindow
+{
+    /// <summary>
+    /// Contextual button used to traverse through previous layers of the radial menu
+    /// </summary>
+    public TextureButton? ContextualButton { get; set; }
+
+    /// <summary>
+    /// Set a style class to be applied to the contextual button when it is set to move the user back through previous layers of the radial menu
+    /// </summary>  
+    public string? BackButtonStyleClass
+    {
+        get
+        {
+            return _backButtonStyleClass;
+        }
+
+        set
+        {
+            _backButtonStyleClass = value;
+
+            if (_path.Count > 0 && ContextualButton != null && _backButtonStyleClass != null)
+                ContextualButton.SetOnlyStyleClass(_backButtonStyleClass);
+        }
+    }
+
+    /// <summary>
+    /// Set a style class to be applied to the contextual button when it will close the radial menu
+    /// </summary>
+    public string? CloseButtonStyleClass
+    {
+        get
+        {
+            return _closeButtonStyleClass;
+        }
+
+        set
+        {
+            _closeButtonStyleClass = value;
+
+            if (_path.Count == 0 && ContextualButton != null && _closeButtonStyleClass != null)
+                ContextualButton.SetOnlyStyleClass(_closeButtonStyleClass);
+        }
+    }
+
+    private List<Control> _path = new();
+    private string? _backButtonStyleClass;
+    private string? _closeButtonStyleClass;
+
+    /// <summary>
+    /// A free floating menu which enables the quick display of one or more radial containers
+    /// </summary>
+    /// <remarks>
+    /// Only one radial container is visible at a time (each container forming a separate 'layer' within 
+    /// the menu), along with a contextual button at the menu center, which will either return the user  
+    /// to the previous layer or close the menu if there are no previous layers left to traverse.
+    /// To create a functional radial menu, simply parent one or more named radial containers to it,
+    /// and populate the radial containers with RadialMenuButtons. Setting the TargetLayer field of these
+    /// buttons to the name of a radial conatiner will display the container in question to the user
+    /// whenever it is clicked in additon to any other actions assigned to the button
+    /// </remarks>
+    public RadialMenu()
+    {
+        // Hide all starting children (if any) except the first (this is the active layer)
+        if (ChildCount > 1)
+        {
+            for (int i = 1; i < ChildCount; i++)
+                GetChild(i).Visible = false;
+        }
+
+        // Auto generate a contextual button for moving back through visited layers
+        ContextualButton = new TextureButton()
+        {
+            HorizontalAlignment = HAlignment.Center,
+            VerticalAlignment = VAlignment.Center,
+            SetSize = new Vector2(64f, 64f),
+        };
+
+        ContextualButton.OnButtonUp += _ => ReturnToPreviousLayer();
+        AddChild(ContextualButton);
+
+        // Hide any further add children, unless its promoted to the active layer
+        OnChildAdded += child => child.Visible = (GetCurrentActiveLayer() == child);
+    }
+
+    private Control? GetCurrentActiveLayer()
+    {
+        var children = Children.Where(x => x != ContextualButton);
+
+        if (!children.Any())
+            return null;
+
+        return children.First(x => x.Visible);
+    }
+
+    public bool TryToMoveToNewLayer(string newLayer)
+    {
+        if (newLayer == string.Empty)
+            return false;
+
+        var currentLayer = GetCurrentActiveLayer();
+
+        if (currentLayer == null)
+            return false;
+
+        var result = false;
+
+        foreach (var child in Children)
+        {
+            if (child == ContextualButton)
+                continue;
+
+            // Hide layers which are not of interest
+            if (result == true || child.Name != newLayer)
+            {
+                child.Visible = false;
+            }
+
+            // Show the layer of interest
+            else
+            {
+                child.Visible = true;
+                result = true;
+            }
+        }
+
+        // Update the traversal path
+        if (result)
+            _path.Add(currentLayer);
+
+        // Set the style class of the button
+        if (_path.Count > 0 && ContextualButton != null && BackButtonStyleClass != null)
+            ContextualButton.SetOnlyStyleClass(BackButtonStyleClass);
+
+        return result;
+    }
+
+    public void ReturnToPreviousLayer()
+    {
+        // Close the menu if the traversal path is empty
+        if (_path.Count == 0)
+        {
+            Close();
+            return;
+        }
+
+        var lastChild = _path[^1];
+
+        // Hide all children except the contextual button
+        foreach (var child in Children)
+        {
+            if (child != ContextualButton)
+                child.Visible = false;
+        }
+
+        // Make the last visited layer visible, update the path list
+        lastChild.Visible = true;
+        _path.RemoveAt(_path.Count - 1);
+
+        // Set the style class of the button
+        if (_path.Count == 0 && ContextualButton != null && CloseButtonStyleClass != null)
+            ContextualButton.SetOnlyStyleClass(CloseButtonStyleClass);
+    }
+}
+
+[Virtual]
+public class RadialMenuButton : Button
+{
+    /// <summary>
+    /// Upon clicking this button the radial menu will transition to the named layer
+    /// </summary>
+    public string? TargetLayer { get; set; }
+
+    /// <summary>
+    /// A simple button that can move the user to a different layer within a radial menu
+    /// </summary>
+    public RadialMenuButton()
+    {
+        OnButtonUp += OnClicked;
+    }
+
+    private void OnClicked(ButtonEventArgs args)
+    {
+        if (TargetLayer == null || TargetLayer == string.Empty)
+            return;
+
+        var parent = FindParentMultiLayerContainer(this);
+
+        if (parent == null)
+            return;
+
+        parent.TryToMoveToNewLayer(TargetLayer);
+    }
+
+    private RadialMenu? FindParentMultiLayerContainer(Control control)
+    {
+        foreach (var ancestor in control.GetSelfAndLogicalAncestors())
+        {
+            if (ancestor is RadialMenu)
+                return ancestor as RadialMenu;
+        }
+
+        return null;
+    }
+}
+
+[Virtual]
+public class RadialMenuTextureButton : TextureButton
+{
+    /// <summary>
+    /// Upon clicking this button the radial menu will be moved to the named layer
+    /// </summary>
+    public string TargetLayer { get; set; } = string.Empty;
+
+    /// <summary>
+    /// A simple texture button that can move the user to a different layer within a radial menu
+    /// </summary>
+    public RadialMenuTextureButton()
+    {
+        OnButtonUp += OnClicked;
+    }
+
+    private void OnClicked(ButtonEventArgs args)
+    {
+        if (TargetLayer == string.Empty)
+            return;
+
+        var parent = FindParentMultiLayerContainer(this);
+
+        if (parent == null)
+            return;
+
+        parent.TryToMoveToNewLayer(TargetLayer);
+    }
+
+    private RadialMenu? FindParentMultiLayerContainer(Control control)
+    {
+        foreach (var ancestor in control.GetSelfAndLogicalAncestors())
+        {
+            if (ancestor is RadialMenu)
+                return ancestor as RadialMenu;
+        }
+
+        return null;
+    }
+}
index 021191ac2634d1b9ad9c04b2b43c15eaca843e99..5de1383cde0244528277e5e83b70f7b49b73bfc6 100644 (file)
@@ -61,4 +61,26 @@ public abstract class SharedChargesSystem : EntitySystem
         if (Resolve(uid, ref comp, false))
             AddCharges(uid, -1, comp);
     }
+
+    /// <summary>
+    /// Gets the limited charges component and returns true if the number of charges remaining is less than the specified value.
+    /// Will return false if there is no limited charges component.
+    /// </summary>
+    public bool HasInsufficientCharges(EntityUid uid, int requiredCharges, LimitedChargesComponent? comp = null)
+    {
+        // can't be empty if there are no limited charges
+        if (!Resolve(uid, ref comp, false))
+            return false;
+
+        return comp.Charges < requiredCharges;
+    }
+
+    /// <summary>
+    /// Uses up a specified number of charges. Must check HasInsufficentCharges beforehand to prevent using with insufficient remaining charges.
+    /// </summary>
+    public virtual void UseCharges(EntityUid uid, int chargesUsed, LimitedChargesComponent? comp = null)
+    {
+        if (Resolve(uid, ref comp, false))
+            AddCharges(uid, -chargesUsed, comp);
+    }
 }
index f80a6687781bd97ef09bdcea89cea8ca78823c44..539e09245d3c1ca802d6f520d1e12df6e54839e8 100644 (file)
@@ -1,4 +1,4 @@
-using Robust.Shared.Prototypes;
+using Robust.Shared.Prototypes;
 using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
 
 namespace Content.Shared.Construction.Components
@@ -9,7 +9,7 @@ namespace Content.Shared.Construction.Components
     [RegisterComponent]
     public sealed partial class ComputerBoardComponent : Component
     {
-        [DataField("prototype", customTypeSerializer:typeof(PrototypeIdSerializer<EntityPrototype>))]
+        [DataField("prototype", customTypeSerializer: typeof(PrototypeIdSerializer<EntityPrototype>))]
         public string? Prototype { get; private set; }
     }
 }
index 7b1fc001d4d1da53bfb9641060ee4597e9026551..4135b606e22aeac978a94dbbff8831d17ec135ea 100644 (file)
@@ -12,7 +12,5 @@ public sealed partial class RCDAmmoComponent : Component
     /// Can be partially transferred into an RCD, until it is empty then it gets deleted.
     /// </summary>
     [DataField("charges"), ViewVariables(VVAccess.ReadWrite), AutoNetworkedField]
-    public int Charges = 5;
+    public int Charges = 30;
 }
-
-// TODO: state??? check if it desyncs
index 8e1032884aa04e336fb681420961b188c451b505..39bb6fd3e9fee4d17fea2d2dc405ce0f653aef14 100644 (file)
@@ -1,20 +1,11 @@
-using Content.Shared.Maps;
 using Content.Shared.RCD.Systems;
 using Robust.Shared.Audio;
 using Robust.Shared.GameStates;
-using Robust.Shared.Serialization;
-using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
+using Robust.Shared.Physics;
+using Robust.Shared.Prototypes;
 
 namespace Content.Shared.RCD.Components;
 
-public enum RcdMode : byte
-{
-    Floors,
-    Walls,
-    Airlock,
-    Deconstruct
-}
-
 /// <summary>
 /// Main component for the RCD
 /// Optionally uses LimitedChargesComponent.
@@ -25,27 +16,57 @@ public enum RcdMode : byte
 public sealed partial class RCDComponent : Component
 {
     /// <summary>
-    /// Time taken to do an action like placing a wall
+    /// List of RCD prototypes that the device comes loaded with
     /// </summary>
-    [DataField("delay"), ViewVariables(VVAccess.ReadWrite), AutoNetworkedField]
-    public float Delay = 2f;
+    [DataField, AutoNetworkedField]
+    public HashSet<ProtoId<RCDPrototype>> AvailablePrototypes { get; set; } = new();
 
-    [DataField("swapModeSound")]
-    public SoundSpecifier SwapModeSound = new SoundPathSpecifier("/Audio/Items/genhit.ogg");
+    /// <summary>
+    /// Sound that plays when a RCD operation successfully completes
+    /// </summary>
+    [DataField]
+    public SoundSpecifier SuccessSound { get; set; } = new SoundPathSpecifier("/Audio/Items/deconstruct.ogg");
 
-    [DataField("successSound")]
-    public SoundSpecifier SuccessSound = new SoundPathSpecifier("/Audio/Items/deconstruct.ogg");
+    /// <summary>
+    /// The ProtoId of the currently selected RCD prototype
+    /// </summary>
+    [DataField, AutoNetworkedField]
+    public ProtoId<RCDPrototype> ProtoId { get; set; } = "Invalid";
 
     /// <summary>
-    /// What mode are we on? Can be floors, walls, airlock, deconstruct.
+    /// A cached copy of currently selected RCD prototype
     /// </summary>
-    [DataField("mode"), AutoNetworkedField]
-    public RcdMode Mode = RcdMode.Floors;
+    /// <remarks>
+    /// If the ProtoId is changed, make sure to update the CachedPrototype as well
+    /// </remarks>
+    [ViewVariables(VVAccess.ReadOnly)]
+    public RCDPrototype CachedPrototype { get; set; } = default!;
+
+    /// <summary>
+    /// The direction constructed entities will face upon spawning
+    /// </summary>
+    [DataField, AutoNetworkedField]
+    public Direction ConstructionDirection
+    {
+        get
+        {
+            return _constructionDirection;
+        }
+        set
+        {
+            _constructionDirection = value;
+            ConstructionTransform = new Transform(new(), _constructionDirection.ToAngle());
+        }
+    }
+
+    private Direction _constructionDirection = Direction.South;
 
     /// <summary>
-    /// ID of the floor to create when using the floor mode.
+    /// Returns a rotated transform based on the specified ConstructionDirection
     /// </summary>
-    [DataField("floor", customTypeSerializer: typeof(PrototypeIdSerializer<ContentTileDefinition>))]
-    [ViewVariables(VVAccess.ReadWrite), AutoNetworkedField]
-    public string Floor = "FloorSteel";
+    /// <remarks>
+    /// Contains no position data
+    /// </remarks>
+    [ViewVariables(VVAccess.ReadOnly)]
+    public Transform ConstructionTransform { get; private set; } = default!;
 }
diff --git a/Content.Shared/RCD/Components/RCDDeconstructibleComponent.cs b/Content.Shared/RCD/Components/RCDDeconstructibleComponent.cs
new file mode 100644 (file)
index 0000000..0ddc689
--- /dev/null
@@ -0,0 +1,34 @@
+using Content.Shared.RCD.Systems;
+using Robust.Shared.GameStates;
+using Robust.Shared.Prototypes;
+
+namespace Content.Shared.RCD.Components;
+
+[RegisterComponent, NetworkedComponent]
+[Access(typeof(RCDSystem))]
+public sealed partial class RCDDeconstructableComponent : Component
+{
+    /// <summary>
+    /// Number of charges consumed when the deconstruction is completed
+    /// </summary>
+    [DataField, ViewVariables(VVAccess.ReadWrite)]
+    public int Cost = 1;
+
+    /// <summary>
+    /// The length of the deconstruction 
+    /// </summary>
+    [DataField, ViewVariables(VVAccess.ReadWrite)]
+    public float Delay = 1f;
+
+    /// <summary>
+    /// The visual effect that plays during deconstruction
+    /// </summary>
+    [DataField("fx"), ViewVariables(VVAccess.ReadWrite)]
+    public EntProtoId? Effect = null;
+
+    /// <summary>
+    /// Toggles whether this entity is deconstructable or not
+    /// </summary>
+    [DataField, ViewVariables(VVAccess.ReadWrite)]
+    public bool Deconstructable = true;
+}
diff --git a/Content.Shared/RCD/RCDEvents.cs b/Content.Shared/RCD/RCDEvents.cs
new file mode 100644 (file)
index 0000000..a15a010
--- /dev/null
@@ -0,0 +1,34 @@
+using Robust.Shared.Prototypes;
+using Robust.Shared.Serialization;
+
+namespace Content.Shared.RCD;
+
+[Serializable, NetSerializable]
+public sealed class RCDSystemMessage : BoundUserInterfaceMessage
+{
+    public ProtoId<RCDPrototype> ProtoId;
+
+    public RCDSystemMessage(ProtoId<RCDPrototype> protoId)
+    {
+        ProtoId = protoId;
+    }
+}
+
+[Serializable, NetSerializable]
+public sealed class RCDConstructionGhostRotationEvent : EntityEventArgs
+{
+    public readonly NetEntity NetEntity;
+    public readonly Direction Direction;
+
+    public RCDConstructionGhostRotationEvent(NetEntity netEntity, Direction direction)
+    {
+        NetEntity = netEntity;
+        Direction = direction;
+    }
+}
+
+[Serializable, NetSerializable]
+public enum RcdUiKey : byte
+{
+    Key
+}
diff --git a/Content.Shared/RCD/RCDPrototype.cs b/Content.Shared/RCD/RCDPrototype.cs
new file mode 100644 (file)
index 0000000..1e80abf
--- /dev/null
@@ -0,0 +1,144 @@
+using Content.Shared.Physics;
+using Robust.Shared.Physics.Collision.Shapes;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Utility;
+
+namespace Content.Shared.RCD;
+
+/// <summary>
+/// Contains the parameters for a RCD construction / operation
+/// </summary>
+[Prototype("rcd")]
+public sealed class RCDPrototype : IPrototype
+{
+    [IdDataField]
+    public string ID { get; private set; } = default!;
+
+    /// <summary>
+    /// The RCD mode associated with the operation
+    /// </summary>
+    [DataField(required: true), ViewVariables(VVAccess.ReadOnly)]
+    public RcdMode Mode { get; private set; } = RcdMode.Invalid;
+
+    /// <summary>
+    /// The name associated with the prototype
+    /// </summary>
+    [DataField("name"), ViewVariables(VVAccess.ReadOnly)]
+    public string SetName { get; private set; } = "Unknown";
+
+    /// <summary>
+    /// The name of the radial container that this prototype will be listed under on the RCD menu
+    /// </summary>
+    [DataField, ViewVariables(VVAccess.ReadOnly)]
+    public string Category { get; private set; } = "Undefined";
+
+    /// <summary>
+    /// Texture path for this prototypes menu icon
+    /// </summary>
+    [DataField, ViewVariables(VVAccess.ReadOnly)]
+    public SpriteSpecifier? Sprite { get; private set; } = null;
+
+    /// <summary>
+    /// The entity prototype that will be constructed (mode dependent)
+    /// </summary>
+    [DataField, ViewVariables(VVAccess.ReadOnly)]
+    public string? Prototype { get; private set; } = string.Empty;
+
+    /// <summary>
+    /// Number of charges consumed when the operation is completed
+    /// </summary>
+    [DataField, ViewVariables(VVAccess.ReadOnly)]
+    public int Cost { get; private set; } = 1;
+
+    /// <summary>
+    /// The length of the operation 
+    /// </summary>
+    [DataField, ViewVariables(VVAccess.ReadOnly)]
+    public float Delay { get; private set; } = 1f;
+
+    /// <summary>
+    /// The visual effect that plays during this operation
+    /// </summary>
+    [DataField("fx"), ViewVariables(VVAccess.ReadOnly)]
+    public EntProtoId? Effect { get; private set; } = null;
+
+    /// <summary>
+    /// A list of rules that govern where the entity prototype can be contructed
+    /// </summary>
+    [DataField("rules"), ViewVariables(VVAccess.ReadOnly)]
+    public HashSet<RcdConstructionRule> ConstructionRules { get; private set; } = new();
+
+    /// <summary>
+    /// The collision mask used for determining whether the entity prototype will fit into a target tile
+    /// </summary>
+    [DataField, ViewVariables(VVAccess.ReadOnly)]
+    public CollisionGroup CollisionMask { get; private set; } = CollisionGroup.None;
+
+    /// <summary>
+    /// Specifies a set of custom collision bounds for determining whether the entity prototype will fit into a target tile 
+    /// </summary>
+    /// <remarks>
+    /// Should be set assuming that the entity faces south.
+    /// Make sure that Rotation is set to RcdRotation.User if the entity is to be rotated by the user
+    /// </remarks>
+    [DataField, ViewVariables(VVAccess.ReadOnly)]
+    public Box2? CollisionBounds
+    {
+        get
+        {
+            return _collisionBounds;
+        }
+
+        private set
+        {
+            _collisionBounds = value;
+
+            if (_collisionBounds != null)
+            {
+                var poly = new PolygonShape();
+                poly.SetAsBox(_collisionBounds.Value);
+
+                CollisionPolygon = poly;
+            }
+        }
+    }
+
+    private Box2? _collisionBounds = null;
+
+    /// <summary>
+    /// The polygon shape associated with the prototype CollisionBounds (if set) 
+    /// </summary>
+    [ViewVariables(VVAccess.ReadOnly)]
+    public PolygonShape? CollisionPolygon { get; private set; } = null;
+
+    /// <summary>
+    /// Governs how the local rotation of the constructed entity will be set
+    /// </summary>
+    [DataField, ViewVariables(VVAccess.ReadOnly)]
+    public RcdRotation Rotation { get; private set; } = RcdRotation.User;
+}
+
+public enum RcdMode : byte
+{
+    Invalid,
+    Deconstruct,
+    ConstructTile,
+    ConstructObject,
+}
+
+// These are to be replaced with more flexible 'RulesRule' at a later time
+public enum RcdConstructionRule : byte
+{
+    MustBuildOnEmptyTile,       // Can only be built on empty space (e.g. lattice)
+    CanBuildOnEmptyTile,        // Can be built on empty space or replace an existing tile (e.g. hull plating)
+    MustBuildOnSubfloor,        // Can only be built on exposed subfloor (e.g. catwalks on lattice or hull plating)
+    IsWindow,                   // The entity is a window and can be built on grilles
+    IsCatwalk,                  // The entity is a catwalk
+}
+
+public enum RcdRotation : byte
+{
+    Fixed,      // The entity has a local rotation of zero
+    Camera,     // The rotation of the entity matches the local player camera
+    User,       // The entity can be rotated by the local player prior to placement
+}
index 6282a117bb36273720d2923580300afb5cd3bb08..cd1e90dc1ff655d1166b8b7d163ef82e6fc28b65 100644 (file)
@@ -1,28 +1,35 @@
 using Content.Shared.Administration.Logs;
 using Content.Shared.Charges.Components;
 using Content.Shared.Charges.Systems;
+using Content.Shared.Construction;
 using Content.Shared.Database;
 using Content.Shared.DoAfter;
 using Content.Shared.Examine;
+using Content.Shared.Hands.Components;
 using Content.Shared.Interaction;
-using Content.Shared.Interaction.Events;
 using Content.Shared.Maps;
 using Content.Shared.Physics;
 using Content.Shared.Popups;
 using Content.Shared.RCD.Components;
 using Content.Shared.Tag;
 using Content.Shared.Tiles;
-using Robust.Shared.Audio;
 using Robust.Shared.Audio.Systems;
 using Robust.Shared.Map;
 using Robust.Shared.Map.Components;
 using Robust.Shared.Network;
+using Robust.Shared.Physics;
+using Robust.Shared.Physics.Collision.Shapes;
+using Robust.Shared.Physics.Dynamics;
+using Robust.Shared.Prototypes;
 using Robust.Shared.Serialization;
 using Robust.Shared.Timing;
+using System.Diagnostics.CodeAnalysis;
+using System.Linq;
 
 namespace Content.Shared.RCD.Systems;
 
-public sealed class RCDSystem : EntitySystem
+[Virtual]
+public class RCDSystem : EntitySystem
 {
     [Dependency] private readonly IGameTiming _timing = default!;
     [Dependency] private readonly INetManager _net = default!;
@@ -34,312 +41,599 @@ public sealed class RCDSystem : EntitySystem
     [Dependency] private readonly SharedDoAfterSystem _doAfter = default!;
     [Dependency] private readonly SharedInteractionSystem _interaction = default!;
     [Dependency] private readonly SharedPopupSystem _popup = default!;
-    [Dependency] private readonly TagSystem _tag = default!;
     [Dependency] private readonly TurfSystem _turf = default!;
-    [Dependency] private readonly IGameTiming _gameTiming = default!;
+    [Dependency] private readonly EntityLookupSystem _lookup = default!;
+    [Dependency] private readonly IPrototypeManager _protoManager = default!;
+    [Dependency] private readonly SharedMapSystem _mapSystem = default!;
+    [Dependency] private readonly TagSystem _tags = default!;
 
-    private readonly int _rcdModeCount = Enum.GetValues(typeof(RcdMode)).Length;
+    private readonly int _instantConstructionDelay = 0;
+    private readonly EntProtoId _instantConstructionFx = "EffectRCDConstruct0";
+    private readonly ProtoId<RCDPrototype> _deconstructTileProto = "DeconstructTile";
+    private readonly ProtoId<RCDPrototype> _deconstructLatticeProto = "DeconstructLattice";
+
+    private HashSet<EntityUid> _intersectingEntities = new();
 
     public override void Initialize()
     {
         base.Initialize();
 
+        SubscribeLocalEvent<RCDComponent, MapInitEvent>(OnMapInit);
         SubscribeLocalEvent<RCDComponent, ExaminedEvent>(OnExamine);
-        SubscribeLocalEvent<RCDComponent, UseInHandEvent>(OnUseInHand);
         SubscribeLocalEvent<RCDComponent, AfterInteractEvent>(OnAfterInteract);
         SubscribeLocalEvent<RCDComponent, RCDDoAfterEvent>(OnDoAfter);
         SubscribeLocalEvent<RCDComponent, DoAfterAttemptEvent<RCDDoAfterEvent>>(OnDoAfterAttempt);
+        SubscribeLocalEvent<RCDComponent, RCDSystemMessage>(OnRCDSystemMessage);
+        SubscribeNetworkEvent<RCDConstructionGhostRotationEvent>(OnRCDconstructionGhostRotationEvent);
     }
 
-    private void OnExamine(EntityUid uid, RCDComponent comp, ExaminedEvent args)
+    #region Event handling
+
+    private void OnMapInit(EntityUid uid, RCDComponent component, MapInitEvent args)
     {
-        if (!args.IsInDetailsRange)
+        // On init, set the RCD to its first available recipe
+        if (component.AvailablePrototypes.Any())
+        {
+            component.ProtoId = component.AvailablePrototypes.First();
+            UpdateCachedPrototype(uid, component);
+            Dirty(uid, component);
+
             return;
+        }
 
-        var msg = Loc.GetString("rcd-component-examine-detail", ("mode", comp.Mode));
-        args.PushMarkup(msg);
+        // The RCD has no valid recipes somehow? Get rid of it
+        QueueDel(uid);
     }
 
-    private void OnUseInHand(EntityUid uid, RCDComponent comp, UseInHandEvent args)
+    private void OnRCDSystemMessage(EntityUid uid, RCDComponent component, RCDSystemMessage args)
     {
-        if (args.Handled)
+        // Exit if the RCD doesn't actually know the supplied prototype
+        if (!component.AvailablePrototypes.Contains(args.ProtoId))
             return;
 
-        NextMode(uid, comp, args.User);
-        args.Handled = true;
+        if (!_protoManager.HasIndex(args.ProtoId))
+            return;
+
+        // Set the current RCD prototype to the one supplied
+        component.ProtoId = args.ProtoId;
+        UpdateCachedPrototype(uid, component);
+        Dirty(uid, component);
+
+        if (args.Session.AttachedEntity != null)
+        {
+            // Popup message
+            var msg = (component.CachedPrototype.Prototype != null) ?
+                Loc.GetString("rcd-component-change-build-mode", ("name", Loc.GetString(component.CachedPrototype.SetName))) :
+                Loc.GetString("rcd-component-change-mode", ("mode", Loc.GetString(component.CachedPrototype.SetName)));
+
+            _popup.PopupClient(msg, uid, args.Session.AttachedEntity.Value);
+        }
     }
 
-    private void OnAfterInteract(EntityUid uid, RCDComponent comp, AfterInteractEvent args)
+    private void OnExamine(EntityUid uid, RCDComponent component, ExaminedEvent args)
+    {
+        if (!args.IsInDetailsRange)
+            return;
+
+        // Update cached prototype if required
+        UpdateCachedPrototype(uid, component);
+
+        var msg = (component.CachedPrototype.Prototype != null) ?
+            Loc.GetString("rcd-component-examine-build-details", ("name", Loc.GetString(component.CachedPrototype.SetName))) :
+            Loc.GetString("rcd-component-examine-mode-details", ("mode", Loc.GetString(component.CachedPrototype.SetName)));
+
+        args.PushMarkup(msg);
+    }
+
+    private void OnAfterInteract(EntityUid uid, RCDComponent component, AfterInteractEvent args)
     {
         if (args.Handled || !args.CanReach)
             return;
 
         var user = args.User;
+        var location = args.ClickLocation;
 
-        TryComp<LimitedChargesComponent>(uid, out var charges);
-        if (_charges.IsEmpty(uid, charges))
+        // Initial validity checks
+        if (!location.IsValid(EntityManager))
+            return;
+
+        if (!TryGetMapGridData(location, out var mapGridData))
         {
-            _popup.PopupClient(Loc.GetString("rcd-component-no-ammo-message"), uid, user);
+            _popup.PopupClient(Loc.GetString("rcd-component-no-valid-grid"), uid, user);
             return;
         }
 
-        var location = args.ClickLocation;
-        // Initial validity check
-        if (!location.IsValid(EntityManager))
+        if (!IsRCDOperationStillValid(uid, component, mapGridData.Value, args.Target, args.User))
             return;
 
-        var gridId = location.GetGridUid(EntityManager);
-        if (!HasComp<MapGridComponent>(gridId))
+        if (!_net.IsServer)
+            return;
+
+        // Get the starting cost, delay, and effect from the prototype
+        var cost = component.CachedPrototype.Cost;
+        var delay = component.CachedPrototype.Delay;
+        var effectPrototype = component.CachedPrototype.Effect;
+
+        #region: Operation modifiers
+
+        // Deconstruction modifiers
+        switch (component.CachedPrototype.Mode)
         {
-            location = location.AlignWithClosestGridTile();
-            gridId = location.GetGridUid(EntityManager);
-            // Check if fixing it failed / get final grid ID
-            if (!HasComp<MapGridComponent>(gridId))
-                return;
+            case RcdMode.Deconstruct:
+
+                // Deconstructing an object
+                if (args.Target != null)
+                {
+                    if (TryComp<RCDDeconstructableComponent>(args.Target, out var destructible))
+                    {
+                        cost = destructible.Cost;
+                        delay = destructible.Delay;
+                        effectPrototype = destructible.Effect;
+                    }
+                }
+
+                // Deconstructing a tile
+                else
+                {
+                    var deconstructedTile = _mapSystem.GetTileRef(mapGridData.Value.GridUid, mapGridData.Value.Component, mapGridData.Value.Location);
+                    var protoName = deconstructedTile.IsSpace() ? _deconstructTileProto : _deconstructLatticeProto;
+
+                    if (_protoManager.TryIndex(protoName, out var deconProto))
+                    {
+                        cost = deconProto.Cost;
+                        delay = deconProto.Delay;
+                        effectPrototype = deconProto.Effect;
+                    }
+                }
+
+                break;
+
+            case RcdMode.ConstructTile:
+
+                // If replacing a tile, make the construction instant
+                var contructedTile = _mapSystem.GetTileRef(mapGridData.Value.GridUid, mapGridData.Value.Component, mapGridData.Value.Location);
+
+                if (!contructedTile.Tile.IsEmpty)
+                {
+                    delay = _instantConstructionDelay;
+                    effectPrototype = _instantConstructionFx;
+                }
+
+                break;
         }
 
-        var doAfterArgs = new DoAfterArgs(EntityManager, user, comp.Delay, new RCDDoAfterEvent(GetNetCoordinates(location), comp.Mode), uid, target: args.Target, used: uid)
+        #endregion
+
+        // Try to start the do after
+        var effect = Spawn(effectPrototype, mapGridData.Value.Location);
+        var ev = new RCDDoAfterEvent(GetNetCoordinates(mapGridData.Value.Location), component.ProtoId, cost, EntityManager.GetNetEntity(effect));
+
+        var doAfterArgs = new DoAfterArgs(EntityManager, user, delay, ev, uid, target: args.Target, used: uid)
         {
             BreakOnDamage = true,
-            NeedHand = true,
             BreakOnHandChange = true,
             BreakOnMove = true,
-            AttemptFrequency = AttemptFrequency.EveryTick
+            AttemptFrequency = AttemptFrequency.EveryTick,
+            CancelDuplicate = false,
+            BlockDuplicate = false
         };
 
         args.Handled = true;
 
-        if (_doAfter.TryStartDoAfter(doAfterArgs) && _gameTiming.IsFirstTimePredicted)
-            Spawn("EffectRCDConstruction", location);
+        if (!_doAfter.TryStartDoAfter(doAfterArgs))
+            QueueDel(effect);
     }
 
-    private void OnDoAfterAttempt(EntityUid uid, RCDComponent comp, DoAfterAttemptEvent<RCDDoAfterEvent> args)
+    private void OnDoAfterAttempt(EntityUid uid, RCDComponent component, DoAfterAttemptEvent<RCDDoAfterEvent> args)
     {
-        // sus client crash why
         if (args.Event?.DoAfter?.Args == null)
             return;
 
-        var location = GetCoordinates(args.Event.Location);
+        // Exit if the RCD prototype has changed
+        if (component.ProtoId != args.Event.StartingProtoId)
+            return;
 
-        var gridId = location.GetGridUid(EntityManager);
-        if (!HasComp<MapGridComponent>(gridId))
-        {
-            location = location.AlignWithClosestGridTile();
-            gridId = location.GetGridUid(EntityManager);
-            // Check if fixing it failed / get final grid ID
-            if (!HasComp<MapGridComponent>(gridId))
-                return;
-        }
+        // Ensure the RCD operation is still valid
+        var location = GetCoordinates(args.Event.Location);
 
-        var mapGrid = Comp<MapGridComponent>(gridId.Value);
-        var tile = mapGrid.GetTileRef(location);
+        if (!TryGetMapGridData(location, out var mapGridData))
+            return;
 
-        if (!IsRCDStillValid(uid, comp, args.Event.User, args.Event.Target, mapGrid, tile, args.Event.StartingMode))
+        if (!IsRCDOperationStillValid(uid, component, mapGridData.Value, args.Event.Target, args.Event.User))
             args.Cancel();
     }
 
-    private void OnDoAfter(EntityUid uid, RCDComponent comp, RCDDoAfterEvent args)
+    private void OnDoAfter(EntityUid uid, RCDComponent component, RCDDoAfterEvent args)
     {
+        if (args.Cancelled && _net.IsServer)
+            QueueDel(EntityManager.GetEntity(args.Effect));
+
         if (args.Handled || args.Cancelled || !_timing.IsFirstTimePredicted)
             return;
 
-        var user = args.User;
+        args.Handled = true;
+
         var location = GetCoordinates(args.Location);
 
-        var gridId = location.GetGridUid(EntityManager);
-        if (!HasComp<MapGridComponent>(gridId))
+        if (!TryGetMapGridData(location, out var mapGridData))
+            return;
+
+        // Ensure the RCD operation is still valid
+        if (!IsRCDOperationStillValid(uid, component, mapGridData.Value, args.Target, args.User))
+            return;
+
+        // Finalize the operation
+        FinalizeRCDOperation(uid, component, mapGridData.Value, args.Target, args.User);
+
+        // Play audio and consume charges
+        _audio.PlayPredicted(component.SuccessSound, uid, args.User);
+        _charges.UseCharges(uid, args.Cost);
+    }
+
+    private void OnRCDconstructionGhostRotationEvent(RCDConstructionGhostRotationEvent ev, EntitySessionEventArgs session)
+    {
+        var uid = GetEntity(ev.NetEntity);
+
+        // Determine if player that send the message is carrying the specified RCD in their active hand
+        if (session.SenderSession.AttachedEntity == null)
+            return;
+
+        if (!TryComp<HandsComponent>(session.SenderSession.AttachedEntity, out var hands) ||
+            uid != hands.ActiveHand?.HeldEntity)
+            return;
+
+        if (!TryComp<RCDComponent>(uid, out var rcd))
+            return;
+
+        // Update the construction direction
+        rcd.ConstructionDirection = ev.Direction;
+        Dirty(uid, rcd);
+    }
+
+    #endregion
+
+    #region Entity construction/deconstruction rule checks
+
+    public bool IsRCDOperationStillValid(EntityUid uid, RCDComponent component, MapGridData mapGridData, EntityUid? target, EntityUid user, bool popMsgs = true)
+    {
+        // Update cached prototype if required
+        UpdateCachedPrototype(uid, component);
+
+        // Check that the RCD has enough ammo to get the job done
+        TryComp<LimitedChargesComponent>(uid, out var charges);
+
+        // Both of these were messages were suppose to be predicted, but HasInsufficientCharges wasn't being checked on the client for some reason?
+        if (_charges.IsEmpty(uid, charges))
         {
-            location = location.AlignWithClosestGridTile();
-            gridId = location.GetGridUid(EntityManager);
-            // Check if fixing it failed / get final grid ID
-            if (!HasComp<MapGridComponent>(gridId))
-                return;
+            if (popMsgs)
+                _popup.PopupClient(Loc.GetString("rcd-component-no-ammo-message"), uid, user);
+
+            return false;
         }
 
-        var mapGrid = Comp<MapGridComponent>(gridId.Value);
-        var tile = mapGrid.GetTileRef(location);
-        var snapPos = mapGrid.TileIndicesFor(location);
+        if (_charges.HasInsufficientCharges(uid, component.CachedPrototype.Cost, charges))
+        {
+            if (popMsgs)
+                _popup.PopupClient(Loc.GetString("rcd-component-insufficient-ammo-message"), uid, user);
 
-        // I love that this uses entirely separate code to construction and tile placement!!!
+            return false;
+        }
 
-        switch (comp.Mode)
-        {
-            //Floor mode just needs the tile to be a space tile (subFloor)
-            case RcdMode.Floors:
-                if (!_floors.CanPlaceTile(gridId.Value, mapGrid, out var reason))
-                {
-                    _popup.PopupClient(reason, user, user);
-                    return;
-                }
+        // Exit if the target / target location is obstructed
+        var unobstructed = (target == null)
+            ? _interaction.InRangeUnobstructed(user, _mapSystem.GridTileToWorld(mapGridData.GridUid, mapGridData.Component, mapGridData.Position), popup: popMsgs)
+            : _interaction.InRangeUnobstructed(user, target.Value, popup: popMsgs);
 
-                mapGrid.SetTile(snapPos, new Tile(_tileDefMan[comp.Floor].TileId));
-                _adminLogger.Add(LogType.RCD, LogImpact.High, $"{ToPrettyString(args.User):user} used RCD to set grid: {tile.GridUid} {snapPos} to {comp.Floor}");
-                break;
-            //We don't want to place a space tile on something that's already a space tile. Let's do the inverse of the last check.
-            case RcdMode.Deconstruct:
-                if (!IsTileBlocked(tile)) // Delete the turf
-                {
-                    mapGrid.SetTile(snapPos, Tile.Empty);
-                    _adminLogger.Add(LogType.RCD, LogImpact.High, $"{ToPrettyString(args.User):user} used RCD to set grid: {tile.GridUid} tile: {snapPos} to space");
-                }
-                else // Delete the targeted thing
-                {
-                    var target = args.Target!.Value;
-                    _adminLogger.Add(LogType.RCD, LogImpact.High, $"{ToPrettyString(args.User):user} used RCD to delete {ToPrettyString(target):target}");
-                    QueueDel(target);
-                }
-                break;
-            //Walls are a special behaviour, and require us to build a new object with a transform rather than setting a grid tile,
-            // thus we early return to avoid the tile set code.
-            case RcdMode.Walls:
-                // only spawn on the server
-                if (_net.IsServer)
-                {
-                    var ent = Spawn("WallSolid", mapGrid.GridTileToLocal(snapPos));
-                    Transform(ent).LocalRotation = Angle.Zero; // Walls always need to point south.
-                    _adminLogger.Add(LogType.RCD, LogImpact.High, $"{ToPrettyString(args.User):user} used RCD to spawn {ToPrettyString(ent)} at {snapPos} on grid {tile.GridUid}");
-                }
-                break;
-            case RcdMode.Airlock:
-                // only spawn on the server
-                if (_net.IsServer)
-                {
-                    var airlock = Spawn("Airlock", mapGrid.GridTileToLocal(snapPos));
-                    Transform(airlock).LocalRotation = Transform(uid).LocalRotation; //Now apply icon smoothing.
-                    _adminLogger.Add(LogType.RCD, LogImpact.High, $"{ToPrettyString(args.User):user} used RCD to spawn {ToPrettyString(airlock)} at {snapPos} on grid {tile.GridUid}");
-                }
-                break;
-            default:
-                args.Handled = true;
-                return; //I don't know why this would happen, but sure I guess. Get out of here invalid state!
+        if (!unobstructed)
+            return false;
+
+        // Return whether the operation location is valid
+        switch (component.CachedPrototype.Mode)
+        {
+            case RcdMode.ConstructTile: return IsConstructionLocationValid(uid, component, mapGridData, user, popMsgs);
+            case RcdMode.ConstructObject: return IsConstructionLocationValid(uid, component, mapGridData, user, popMsgs);
+            case RcdMode.Deconstruct: return IsDeconstructionStillValid(uid, component, mapGridData, target, user, popMsgs);
         }
 
-        _audio.PlayPredicted(comp.SuccessSound, uid, user);
-        _charges.UseCharge(uid);
-        args.Handled = true;
+        return false;
     }
 
-    private bool IsRCDStillValid(EntityUid uid, RCDComponent comp, EntityUid user, EntityUid? target, MapGridComponent mapGrid, TileRef tile, RcdMode startingMode)
+    private bool IsConstructionLocationValid(EntityUid uid, RCDComponent component, MapGridData mapGridData, EntityUid user, bool popMsgs = true)
     {
-        //Less expensive checks first. Failing those ones, we need to check that the tile isn't obstructed.
-        if (comp.Mode != startingMode)
+        // Check rule: Must build on empty tile
+        if (component.CachedPrototype.ConstructionRules.Contains(RcdConstructionRule.MustBuildOnEmptyTile) && !mapGridData.Tile.Tile.IsEmpty)
+        {
+            if (popMsgs)
+                _popup.PopupClient(Loc.GetString("rcd-component-must-build-on-empty-tile-message"), uid, user);
+
             return false;
+        }
 
-        var unobstructed = target == null
-            ? _interaction.InRangeUnobstructed(user, mapGrid.GridTileToWorld(tile.GridIndices), popup: true)
-            : _interaction.InRangeUnobstructed(user, target.Value, popup: true);
+        // Check rule: Must build on non-empty tile
+        if (!component.CachedPrototype.ConstructionRules.Contains(RcdConstructionRule.CanBuildOnEmptyTile) && mapGridData.Tile.Tile.IsEmpty)
+        {
+            if (popMsgs)
+                _popup.PopupClient(Loc.GetString("rcd-component-cannot-build-on-empty-tile-message"), uid, user);
 
-        if (!unobstructed)
             return false;
+        }
+
+        // Check rule: Must place on subfloor
+        if (component.CachedPrototype.ConstructionRules.Contains(RcdConstructionRule.MustBuildOnSubfloor) && !mapGridData.Tile.Tile.GetContentTileDefinition().IsSubFloor)
+        {
+            if (popMsgs)
+                _popup.PopupClient(Loc.GetString("rcd-component-must-build-on-subfloor-message"), uid, user);
+
+            return false;
+        }
+
+        // Tile specific rules
+        if (component.CachedPrototype.Mode == RcdMode.ConstructTile)
+        {
+            // Check rule: Tile placement is valid
+            if (!_floors.CanPlaceTile(mapGridData.GridUid, mapGridData.Component, out var reason))
+            {
+                if (popMsgs)
+                    _popup.PopupClient(reason, uid, user);
+
+                return false;
+            }
+
+            // Check rule: Tiles can't be identical
+            if (mapGridData.Tile.Tile.GetContentTileDefinition().ID == component.CachedPrototype.Prototype)
+            {
+                if (popMsgs)
+                    _popup.PopupClient(Loc.GetString("rcd-component-cannot-build-identical-tile"), uid, user);
+
+                return false;
+            }
+
+            // Ensure that all construction rules shared between tiles and object are checked before exiting here
+            return true;
+        }
+
+        // Entity specific rules
+
+        // Check rule: The tile is unoccupied
+        var isWindow = component.CachedPrototype.ConstructionRules.Contains(RcdConstructionRule.IsWindow);
+        var isCatwalk = component.CachedPrototype.ConstructionRules.Contains(RcdConstructionRule.IsCatwalk);
 
-        switch (comp.Mode)
+        _intersectingEntities.Clear();
+        _lookup.GetLocalEntitiesIntersecting(mapGridData.GridUid, mapGridData.Position, _intersectingEntities, -0.05f, LookupFlags.Uncontained);
+
+        foreach (var ent in _intersectingEntities)
         {
-            //Floor mode just needs the tile to be a space tile (subFloor)
-            case RcdMode.Floors:
-                if (!tile.Tile.IsEmpty)
+            if (isWindow && HasComp<SharedCanBuildWindowOnTopComponent>(ent))
+                continue;
+
+            if (isCatwalk && _tags.HasTag(ent, "Catwalk"))
+            {
+                if (popMsgs)
+                    _popup.PopupClient(Loc.GetString("rcd-component-cannot-build-on-occupied-tile-message"), uid, user);
+
+                return false;
+            }
+
+            if (component.CachedPrototype.CollisionMask != CollisionGroup.None && TryComp<FixturesComponent>(ent, out var fixtures))
+            {
+                foreach (var fixture in fixtures.Fixtures.Values)
                 {
-                    _popup.PopupClient(Loc.GetString("rcd-component-cannot-build-floor-tile-not-empty-message"), uid, user);
+                    // Continue if no collision is possible
+                    if (fixture.CollisionLayer <= 0 || (fixture.CollisionLayer & (int) component.CachedPrototype.CollisionMask) == 0)
+                        continue;
+
+                    // Continue if our custom collision bounds are not intersected
+                    if (component.CachedPrototype.CollisionPolygon != null &&
+                        !DoesCustomBoundsIntersectWithFixture(component.CachedPrototype.CollisionPolygon, component.ConstructionTransform, ent, fixture))
+                        continue;
+
+                    // Collision was detected
+                    if (popMsgs)
+                        _popup.PopupClient(Loc.GetString("rcd-component-cannot-build-on-occupied-tile-message"), uid, user);
+
                     return false;
                 }
+            }
+        }
 
-                return true;
-            //We don't want to place a space tile on something that's already a space tile. Let's do the inverse of the last check.
-            case RcdMode.Deconstruct:
-                if (tile.Tile.IsEmpty)
-                    return false;
+        return true;
+    }
 
-                //They tried to decon a turf but...
-                if (target == null)
-                {
-                    // the turf is blocked
-                    if (IsTileBlocked(tile))
-                    {
-                        _popup.PopupClient(Loc.GetString("rcd-component-tile-obstructed-message"), uid, user);
-                        return false;
-                    }
-                    // the turf can't be destroyed (planet probably)
-                    var tileDef = (ContentTileDefinition) _tileDefMan[tile.Tile.TypeId];
-                    if (tileDef.Indestructible)
-                    {
-                        _popup.PopupClient(Loc.GetString("rcd-component-tile-indestructible-message"), uid, user);
-                        return false;
-                    }
-                }
-                //They tried to decon a non-turf but it's not in the whitelist
-                else if (!_tag.HasTag(target.Value, "RCDDeconstructWhitelist"))
-                {
+    private bool IsDeconstructionStillValid(EntityUid uid, RCDComponent component, MapGridData mapGridData, EntityUid? target, EntityUid user, bool popMsgs = true)
+    {
+        // Attempt to deconstruct a floor tile
+        if (target == null)
+        {
+            // The tile is empty
+            if (mapGridData.Tile.Tile.IsEmpty)
+            {
+                if (popMsgs)
+                    _popup.PopupClient(Loc.GetString("rcd-component-nothing-to-deconstruct-message"), uid, user);
+
+                return false;
+            }
+
+            // The tile has a structure sitting on it
+            if (_turf.IsTileBlocked(mapGridData.Tile, CollisionGroup.MobMask))
+            {
+                if (popMsgs)
+                    _popup.PopupClient(Loc.GetString("rcd-component-tile-obstructed-message"), uid, user);
+
+                return false;
+            }
+
+            // The tile cannot be destroyed
+            var tileDef = (ContentTileDefinition) _tileDefMan[mapGridData.Tile.Tile.TypeId];
+
+            if (tileDef.Indestructible)
+            {
+                if (popMsgs)
+                    _popup.PopupClient(Loc.GetString("rcd-component-tile-indestructible-message"), uid, user);
+
+                return false;
+            }
+        }
+
+        // Attempt to deconstruct an object
+        else
+        {
+            // The object is not in the whitelist
+            if (!TryComp<RCDDeconstructableComponent>(target, out var deconstructible) || !deconstructible.Deconstructable)
+            {
+                if (popMsgs)
                     _popup.PopupClient(Loc.GetString("rcd-component-deconstruct-target-not-on-whitelist-message"), uid, user);
-                    return false;
-                }
 
-                return true;
-            //Walls are a special behaviour, and require us to build a new object with a transform rather than setting a grid tile, thus we early return to avoid the tile set code.
-            case RcdMode.Walls:
-                if (tile.Tile.IsEmpty)
-                {
-                    _popup.PopupClient(Loc.GetString("rcd-component-cannot-build-wall-tile-not-empty-message"), uid, user);
-                    return false;
-                }
+                return false;
+            }
+        }
+
+        return true;
+    }
+
+    #endregion
+
+    #region Entity construction/deconstruction
+
+    private void FinalizeRCDOperation(EntityUid uid, RCDComponent component, MapGridData mapGridData, EntityUid? target, EntityUid user)
+    {
+        if (!_net.IsServer)
+            return;
+
+        if (component.CachedPrototype.Prototype == null)
+            return;
 
-                if (IsTileBlocked(tile))
+        switch (component.CachedPrototype.Mode)
+        {
+            case RcdMode.ConstructTile:
+                _mapSystem.SetTile(mapGridData.GridUid, mapGridData.Component, mapGridData.Position, new Tile(_tileDefMan[component.CachedPrototype.Prototype].TileId));
+                _adminLogger.Add(LogType.RCD, LogImpact.High, $"{ToPrettyString(user):user} used RCD to set grid: {mapGridData.GridUid} {mapGridData.Position} to {component.CachedPrototype.Prototype}");
+                break;
+
+            case RcdMode.ConstructObject:
+                var ent = Spawn(component.CachedPrototype.Prototype, _mapSystem.GridTileToLocal(mapGridData.GridUid, mapGridData.Component, mapGridData.Position));
+
+                switch (component.CachedPrototype.Rotation)
                 {
-                    _popup.PopupClient(Loc.GetString("rcd-component-tile-obstructed-message"), uid, user);
-                    return false;
+                    case RcdRotation.Fixed:
+                        Transform(ent).LocalRotation = Angle.Zero;
+                        break;
+                    case RcdRotation.Camera:
+                        Transform(ent).LocalRotation = Transform(uid).LocalRotation;
+                        break;
+                    case RcdRotation.User:
+                        Transform(ent).LocalRotation = component.ConstructionDirection.ToAngle();
+                        break;
                 }
-                return true;
-            case RcdMode.Airlock:
-                if (tile.Tile.IsEmpty)
+
+                _adminLogger.Add(LogType.RCD, LogImpact.High, $"{ToPrettyString(user):user} used RCD to spawn {ToPrettyString(ent)} at {mapGridData.Position} on grid {mapGridData.GridUid}");
+                break;
+
+            case RcdMode.Deconstruct:
+
+                if (target == null)
                 {
-                    _popup.PopupClient(Loc.GetString("rcd-component-cannot-build-airlock-tile-not-empty-message"), uid, user);
-                    return false;
+                    // Deconstruct tile (either converts the tile to lattice, or removes lattice)
+                    var tile = (mapGridData.Tile.Tile.GetContentTileDefinition().ID != "Lattice") ? new Tile(_tileDefMan["Lattice"].TileId) : Tile.Empty;
+                    _mapSystem.SetTile(mapGridData.GridUid, mapGridData.Component, mapGridData.Position, tile);
+                    _adminLogger.Add(LogType.RCD, LogImpact.High, $"{ToPrettyString(user):user} used RCD to set grid: {mapGridData.GridUid} tile: {mapGridData.Position} open to space");
                 }
-                if (IsTileBlocked(tile))
+                else
                 {
-                    _popup.PopupClient(Loc.GetString("rcd-component-tile-obstructed-message"), uid, user);
-                    return false;
+                    // Deconstruct object
+                    _adminLogger.Add(LogType.RCD, LogImpact.High, $"{ToPrettyString(user):user} used RCD to delete {ToPrettyString(target):target}");
+                    QueueDel(target);
                 }
-                return true;
-            default:
-                return false; //I don't know why this would happen, but sure I guess. Get out of here invalid state!
+
+                break;
         }
     }
 
-    private void NextMode(EntityUid uid, RCDComponent comp, EntityUid user)
+    #endregion
+
+    #region Utility functions
+
+    public bool TryGetMapGridData(EntityCoordinates location, [NotNullWhen(true)] out MapGridData? mapGridData)
     {
-        _audio.PlayPredicted(comp.SwapModeSound, uid, user);
+        mapGridData = null;
+        var gridUid = location.GetGridUid(EntityManager);
+
+        if (!TryComp<MapGridComponent>(gridUid, out var mapGrid))
+        {
+            location = location.AlignWithClosestGridTile(1.75f, EntityManager);
+            gridUid = location.GetGridUid(EntityManager);
+
+            // Check if we got a grid ID the second time round
+            if (!TryComp(gridUid, out mapGrid))
+                return false;
+        }
+
+        gridUid = mapGrid.Owner;
 
-        var mode = (int) comp.Mode;
-        mode = ++mode % _rcdModeCount;
-        comp.Mode = (RcdMode) mode;
-        Dirty(uid, comp);
+        var tile = _mapSystem.GetTileRef(gridUid.Value, mapGrid, location);
+        var position = _mapSystem.TileIndicesFor(gridUid.Value, mapGrid, location);
+        mapGridData = new MapGridData(gridUid.Value, mapGrid, location, tile, position);
 
-        var msg = Loc.GetString("rcd-component-change-mode", ("mode", comp.Mode.ToString()));
-        _popup.PopupClient(msg, uid, user);
+        return true;
     }
 
-    private bool IsTileBlocked(TileRef tile)
+    private bool DoesCustomBoundsIntersectWithFixture(PolygonShape boundingPolygon, Transform boundingTransform, EntityUid fixtureOwner, Fixture fixture)
     {
-        return _turf.IsTileBlocked(tile, CollisionGroup.MobMask);
+        var entXformComp = Transform(fixtureOwner);
+        var entXform = new Transform(new(), entXformComp.LocalRotation);
+
+        return boundingPolygon.ComputeAABB(boundingTransform, 0).Intersects(fixture.Shape.ComputeAABB(entXform, 0));
+    }
+
+    public void UpdateCachedPrototype(EntityUid uid, RCDComponent component)
+    {
+        if (component.ProtoId.Id != component.CachedPrototype?.Prototype)
+            component.CachedPrototype = _protoManager.Index(component.ProtoId);
+    }
+
+    #endregion
+}
+
+public struct MapGridData
+{
+    public EntityUid GridUid;
+    public MapGridComponent Component;
+    public EntityCoordinates Location;
+    public TileRef Tile;
+    public Vector2i Position;
+
+    public MapGridData(EntityUid gridUid, MapGridComponent component, EntityCoordinates location, TileRef tile, Vector2i position)
+    {
+        GridUid = gridUid;
+        Component = component;
+        Location = location;
+        Tile = tile;
+        Position = position;
     }
 }
 
 [Serializable, NetSerializable]
 public sealed partial class RCDDoAfterEvent : DoAfterEvent
 {
-    [DataField("location", required: true)]
-    public NetCoordinates Location = default!;
+    [DataField(required: true)]
+    public NetCoordinates Location { get; private set; } = default!;
 
-    [DataField("startingMode", required: true)]
-    public RcdMode StartingMode = default!;
+    [DataField]
+    public ProtoId<RCDPrototype> StartingProtoId { get; private set; } = default!;
 
-    private RCDDoAfterEvent()
-    {
-    }
+    [DataField]
+    public int Cost { get; private set; } = 1;
+
+    [DataField("fx")]
+    public NetEntity? Effect { get; private set; } = null;
+
+    private RCDDoAfterEvent() { }
 
-    public RCDDoAfterEvent(NetCoordinates location, RcdMode startingMode)
+    public RCDDoAfterEvent(NetCoordinates location, ProtoId<RCDPrototype> startingProtoId, int cost, NetEntity? effect = null)
     {
         Location = location;
-        StartingMode = startingMode;
+        StartingProtoId = startingProtoId;
+        Cost = cost;
+        Effect = effect;
     }
 
     public override DoAfterEvent Clone() => this;
index b7920c9edeaacad52ea4039b30303b7c35998762..bb65e76f3f7da411385d5a0510634eac61707006 100644 (file)
@@ -1,18 +1,66 @@
 
 ### UI
 
-# Shown when an RCD is examined in details range
-rcd-component-examine-detail = It's currently on {$mode} mode.
+rcd-component-examine-mode-details = It's currently set to '{$mode}' mode.
+rcd-component-examine-build-details = It's currently set to build {MAKEPLURAL($name)}.
+
 
 ### Interaction Messages
 
-# Shown when changing RCD Mode
-rcd-component-change-mode = The RCD is now set to {$mode} mode.
+# Mode change
+rcd-component-change-mode = The RCD is now set to '{$mode}' mode.
+rcd-component-change-build-mode = The RCD is now set to build {MAKEPLURAL($name)}.
+
+# Ammo count
+rcd-component-no-ammo-message = The RCD has run out of charges!
+rcd-component-insufficient-ammo-message = The RCD doesn't have enough charges left!
 
-rcd-component-no-ammo-message = The RCD is out of ammo!
-rcd-component-tile-obstructed-message = That tile is obstructed!
-rcd-component-tile-indestructible-message = That tile can't be destroyed!
+# Deconstruction
+rcd-component-tile-indestructible-message = That tile can't be destructed!
 rcd-component-deconstruct-target-not-on-whitelist-message = You can't deconstruct that!
-rcd-component-cannot-build-floor-tile-not-empty-message = You can only build a floor on space!
-rcd-component-cannot-build-wall-tile-not-empty-message = You cannot build a wall on space!
-rcd-component-cannot-build-airlock-tile-not-empty-message = Cannot build an airlock on space!
+rcd-component-nothing-to-deconstruct-message = There's nothing to deconstruct!
+rcd-component-tile-obstructed-message = You can't deconstruct tiles when there's something on top of them!
+
+# Construction
+rcd-component-no-valid-grid = You're too far into open space to build here!
+rcd-component-must-build-on-empty-tile-message = A foundation already exists here!
+rcd-component-cannot-build-on-empty-tile-message = You can't build that without a foundation!
+rcd-component-must-build-on-subfloor-message = You can only build that on exposed subfloor!
+rcd-component-cannot-build-on-subfloor-message = You can't build that on exposed subfloor!
+rcd-component-cannot-build-on-occupied-tile-message = You can't build here, the space is already occupied!
+rcd-component-cannot-build-identical-tile = That tile already exists there!
+
+
+### Category names
+
+rcd-component-walls-and-flooring = Walls and flooring
+rcd-component-windows-and-grilles = Windows and grilles
+rcd-component-airlocks = Airlocks
+rcd-component-electrical = Electrical
+rcd-component-lighting = Lighting
+
+
+### Prototype names (note: constructable items will be puralized)
+
+rcd-component-deconstruct = deconstruct
+rcd-component-wall-solid = solid wall
+rcd-component-floor-steel = steel tile
+rcd-component-plating = hull plate
+rcd-component-catwalk = catwalk
+rcd-component-wall-reinforced = reinforced wall
+rcd-component-grille = grille
+rcd-component-window = window
+rcd-component-window-directional = directional window
+rcd-component-window-reinforced-directional = directional reinforced window
+rcd-component-reinforced-window = reinforced window
+rcd-component-airlock = standard airlock
+rcd-component-airlock-glass = glass airlock
+rcd-component-firelock = firelock
+rcd-component-computer-frame = computer frame
+rcd-component-machine-frame = machine frame
+rcd-component-tube-light = light
+rcd-component-window-bulb-light = small light
+rcd-component-window-lv-cable = LV cable
+rcd-component-window-mv-cable = MV cable
+rcd-component-window-hv-cable = HV cable
+rcd-component-window-cable-terminal = cable terminal
diff --git a/Resources/Locale/en-US/ui/general.ftl b/Resources/Locale/en-US/ui/general.ftl
new file mode 100644 (file)
index 0000000..1471261
--- /dev/null
@@ -0,0 +1,3 @@
+### Loc for the various UI-related verbs
+ui-verb-toggle-open = Toggle UI
+verb-instrument-openui = Play Music
index 668f3776dd1752c7b728a666425cc9b970ccdf60..03c870fa58087a398287a8e838900101ca1e889b 100644 (file)
 - type: entity
   id: CrateRCDAmmo
   parent: CrateEngineering
-  name: RCD ammo crate
-  description: 3 RCD ammo, each restoring 5 charges.
+  name: compressed matter crate
+  description: Contains three compressed matter cartridges.
   components:
   - type: StorageFill
     contents:
   id: CrateRCD
   parent: CrateEngineeringSecure
   name: RCD crate
-  description: A crate containing a single Rapid Construction Device.
+  description: A crate containing a single rapid construction device.
   components:
   - type: StorageFill
     contents:
index a8e28a1ef731bd931275dfd7cdaa0ccde53d683e..739464e9611f90e1da5e446be4fa11c49afc61e7 100644 (file)
     snap:
     - Wall
   components:
-  - type: Tag
-    tags:
-    - RCDDeconstructWhitelist
+  - type: RCDDeconstructable
+    cost: 2
+    delay: 2
+    fx: EffectRCDDeconstruct2  
   - type: Clickable
   - type: InteractionOutline
   - type: Sprite
index adc6aa593c14f12031fb701e99a962cde4d71cea..902429818e511e893ca96011d650396a7529c295 100644 (file)
 - type: entity
-  id: EffectRCDConstruction
+  id: EffectRCDBase
+  abstract: true
   noSpawn: true
   components:
   - type: Transform
     anchored: True
   - type: Sprite
+    snapCardinals: true
+    noRot: true
     drawdepth: Effects
     sprite: /Textures/Effects/rcd.rsi
-    state: construct
-  - type: TimedDespawn
-    lifetime: 3.2
+    state: construct0
   - type: Tag
     tags:
     - HideContextMenu
   - type: AnimationPlayer
+  
+- type: entity
+  parent: EffectRCDBase
+  id: EffectRCDDeconstructPreview
+  noSpawn: true
+  components:
+  - type: Sprite
+    state: deconstructPreview
+  
+- type: entity
+  parent: EffectRCDBase
+  id: EffectRCDConstruct0
+  noSpawn: true
+  components:
+  - type: Sprite
+    state: construct0
+  - type: TimedDespawn
+    lifetime: 1.2
+    
+- type: entity
+  parent: EffectRCDBase
+  id: EffectRCDConstruct1
+  noSpawn: true
+  components:
+  - type: Sprite
+    state: construct1
+  - type: TimedDespawn
+    lifetime: 2.2
+    
+- type: entity
+  parent: EffectRCDBase
+  id: EffectRCDConstruct2
+  noSpawn: true
+  components:
+  - type: Sprite
+    state: construct2
+  - type: TimedDespawn
+    lifetime: 3.2
+    
+- type: entity
+  parent: EffectRCDBase
+  id: EffectRCDConstruct3
+  noSpawn: true
+  components:
+  - type: Sprite
+    state: construct3
+  - type: TimedDespawn
+    lifetime: 4.2
+    
+- type: entity
+  parent: EffectRCDBase
+  id: EffectRCDConstruct4
+  noSpawn: true
+  components:
+  - type: Sprite
+    state: construct4
+  - type: TimedDespawn
+    lifetime: 5.2
+
+- type: entity
+  parent: EffectRCDBase
+  id: EffectRCDDeconstruct2
+  noSpawn: true
+  components:
+  - type: Sprite
+    state: deconstruct2
+  - type: TimedDespawn
+    lifetime: 3.2
+    
+- type: entity
+  parent: EffectRCDBase
+  id: EffectRCDDeconstruct4
+  noSpawn: true
+  components:
+  - type: Sprite
+    state: deconstruct4
+  - type: TimedDespawn
+    lifetime: 5.2
+
+- type: entity
+  parent: EffectRCDBase
+  id: EffectRCDDeconstruct6
+  noSpawn: true
+  components:
+  - type: Sprite
+    state: deconstruct6
+  - type: TimedDespawn
+    lifetime: 7.2
+
+- type: entity
+  parent: EffectRCDBase
+  id: EffectRCDDeconstruct8
+  noSpawn: true
+  components:
+  - type: Sprite
+    state: deconstruct8
+  - type: TimedDespawn
+    lifetime: 9.2
\ No newline at end of file
index d198ebdd51f7f4f53da81462597c1829822de902..be9cc915d91d904548304c07c928b92081bc1f7e 100644 (file)
@@ -7,4 +7,4 @@
     color: '#3F38'
   - type: ConstructionGhost
   - type: Clickable
-  - type: InteractionOutline
+  - type: InteractionOutline
\ No newline at end of file
index b46eded7d84fe620e13b605212a26e9b594ea8de..2b11c211e8ebc585ee44f5abe5a8b26acb340d59 100644 (file)
       path: "/Audio/Items/drill_hit.ogg"
 
 - type: entity
-  name: RCD
-  parent: BaseItem
   id: RCD
-  description: An advanced construction device which can place/remove walls, floors, and airlocks quickly.
+  parent: BaseItem
+  name: RCD
+  description: The rapid construction device can be used to quickly place and remove various station structures and fixtures. Requires compressed matter to function.
   components:
   - type: RCD
+    availablePrototypes:   
+    - WallSolid
+    - FloorSteel
+    - Plating
+    - Catwalk
+    - Grille
+    - Window    
+    - WindowDirectional
+    - WindowReinforcedDirectional
+    - ReinforcedWindow
+    - Airlock
+    - AirlockGlass
+    - Firelock
+    - TubeLight
+    - BulbLight
+    - LVCable
+    - MVCable
+    - HVCable
+    - CableTerminal
+    - Deconstruct
   - type: LimitedCharges
-    maxCharges: 5
-    charges: 5
+    maxCharges: 30
+    charges: 30
   - type: UseDelay
   - type: Sprite
     sprite: Objects/Tools/rcd.rsi
       Plastic: 100
   - type: StaticPrice
     price: 100
+  - type: UserInterface
+    interfaces:
+    - key: enum.RcdUiKey.Key
+      type: RCDMenuBoundUserInterface
+  - type: ActivatableUI
+    key: enum.RcdUiKey.Key
 
 - type: entity
   id: RCDEmpty
   suffix: Empty
   components:
   - type: LimitedCharges
-    maxCharges: 5
     charges: 0
+  - type: RCD
+    availablePrototypes:   
+    - WallSolid
+    - FloorSteel
+    - Plating
+    - Catwalk
+    - Grille
+    - Window
+    - WindowDirectional
+    - WindowReinforcedDirectional
+    - ReinforcedWindow
+    - Airlock
+    - AirlockGlass
+    - Firelock
 
 - type: entity
   id: RCDRecharging
   parent: RCD
-  name: experimental rcd
-  description: A bluespace-enhanced RCD that regenerates charges passively.
+  name: experimental RCD
+  description: A bluespace-enhanced rapid construction device that passively generates its own compressed matter.
   suffix: AutoRecharge
   components:
   - type: LimitedCharges
-    maxCharges: 3
-    charges: 3
+    maxCharges: 20
+    charges: 20
   - type: AutoRecharge
-    rechargeDuration: 30
+    rechargeDuration: 10
 
 - type: entity
   id: RCDExperimental
   parent: RCD
   suffix: Admeme
-  name: experimental rcd
-  description: A bluespace-enhanced RCD that regenerates charges passively.
+  name: experimental RCD
+  description: A bluespace-enhanced rapid construction device that passively generates its own compressed matter.
   components:
   - type: AutoRecharge
-    rechargeDuration: 5
+    rechargeDuration: 1
 
 - type: entity
-  name: RCD Ammo
+  name: compressed matter
   parent: BaseItem
   id: RCDAmmo
-  description: Ammo cartridge for an RCD.
+  description: A cartridge of raw matter compacted by bluespace technology. Used in rapid construction devices.
   components:
   - type: RCDAmmo
   - type: Sprite
index ce2b32612945a068e89e72fc4cf2a484bb9e58b1..ff02e315cb21797f46274b625073b2b52562d3bd 100644 (file)
     sprite: Structures/Doors/Airlocks/Standard/hatch_maint.rsi
 
 # Glass
-
-- type: entity
-  id: AirlockGlass
-  parent: Airlock
-  name: glass airlock
-  components:
-  - type: MeleeSound
-    soundGroups:
-      Brute:
-        collection: GlassSmack
-  - type: Door
-    occludes: false
-  - type: Occluder
-    enabled: false
-  - type: Sprite
-    sprite: Structures/Doors/Airlocks/Glass/glass.rsi
-  - type: AnimationPlayer
-  - type: Fixtures
-    fixtures:
-      fix1:
-        shape:
-          !type:PhysShapeAabb
-          bounds: "-0.49,-0.49,0.49,0.49" # don't want this colliding with walls or they won't close
-        density: 100
-        mask:
-        - FullTileMask
-        layer:     #removed opaque from the layer, allowing lasers to pass through glass airlocks
-        - GlassAirlockLayer
-  - type: LayerChangeOnWeld
-    unWeldedLayer: GlassAirlockLayer
-    weldedLayer: GlassLayer
-  - type: Construction
-    graph: Airlock
-    node: glassAirlock
-  - type: PaintableAirlock
-    group: Glass
-  - type: RadiationBlocker
-    resistance: 2
-  - type: Tag
-    tags:
-      - GlassAirlock
-      # This tag is used to nagivate the Airlock construction graph. It's needed because the construction graph is shared between Airlock, AirlockGlass, and HighSecDoor
 - type: entity
   parent: AirlockGlass
   id: AirlockEngineeringGlass
   - type: Sprite
     sprite: Structures/Doors/Airlocks/Glass/centcomm.rsi
   - type: WiresPanelSecurity
-    securityLevel: medSecurity
+    securityLevel: medSecurity
\ No newline at end of file
index fcdb0d2deafbe5527d37452c73e5e5fd3f0ec0df..283c9f22ae5ad6c82563431d52e36caa655ec6e7 100644 (file)
   - type: Damageable
     damageContainer: Inorganic
     damageModifierSet: Metallic
+  - type: RCDDeconstructable
+    cost: 6
+    delay: 8
+    fx: EffectRCDDeconstruct8
   - type: Destructible
     thresholds:
     - trigger:
index 53a32e0f6fa5d477b0d76798840904194b5d8412..abc86b2be8e25c35443f1983539d7102cb46ed31 100644 (file)
   - type: Damageable
     damageContainer: StructuralInorganic
     damageModifierSet: StrongMetallic
+  - type: RCDDeconstructable
+    cost: 6
+    delay: 8
+    fx: EffectRCDDeconstruct8    
   - type: Destructible
     thresholds:
     - trigger:
   - type: BlockWeather
   placement:
     mode: SnapgridCenter
+- type: entity
+  id: AirlockRCDResistant
+  parent: Airlock
+  abstract: true
+  components:
+  - type: RCDDeconstructable
+    deconstructable: false
 
-
+- type: entity
+  id: AirlockGlass
+  parent: Airlock
+  name: glass airlock
+  components:
+  - type: MeleeSound
+    soundGroups:
+      Brute:
+        collection: GlassSmack
+  - type: Door
+    occludes: false
+  - type: Occluder
+    enabled: false
+  - type: Sprite
+    sprite: Structures/Doors/Airlocks/Glass/glass.rsi
+  - type: AnimationPlayer
+  - type: Fixtures
+    fixtures:
+      fix1:
+        shape:
+          !type:PhysShapeAabb
+          bounds: "-0.49,-0.49,0.49,0.49" # don't want this colliding with walls or they won't close
+        density: 100
+        mask:
+        - FullTileMask
+        layer:     #removed opaque from the layer, allowing lasers to pass through glass airlocks
+        - GlassAirlockLayer
+  - type: LayerChangeOnWeld
+    unWeldedLayer: GlassAirlockLayer
+    weldedLayer: GlassLayer
+  - type: Construction
+    graph: Airlock
+    node: glassAirlock
+  - type: PaintableAirlock
+    group: Glass
+  - type: RadiationBlocker
+    resistance: 2
+  - type: Tag
+    tags:
+      - GlassAirlock
+      # This tag is used to nagivate the Airlock construction graph. It's needed because the construction graph is shared between Airlock, AirlockGlass, and HighSecDoor
\ No newline at end of file
index 75b23f7071902b91009b6367d7f79cd8a0043eeb..293aaac273d8dfc89e6424db75c258e344dce8eb 100644 (file)
@@ -1,5 +1,5 @@
 - type: entity
-  parent: Airlock
+  parent: AirlockRCDResistant
   id: AirlockExternal
   suffix: External
   description: It opens, it closes, it might crush you, and there might be only space behind it.
index 9771f633888b6fdc4b1119c59fdc2e9c958b1e19..5d6b1088f12465793f59120fcb58b0185b133bde 100644 (file)
@@ -1,5 +1,5 @@
 - type: entity
-  parent: Airlock
+  parent: AirlockRCDResistant
   id: AirlockShuttle
   suffix: Docking
   name: external airlock
index e677ef185be6b0957e389269136925810baefe2d..0dd65ab4d07039cb788355baa64e08c6086c7b69 100644 (file)
     - type: Damageable
       damageContainer: Inorganic
       damageModifierSet: Metallic
+    - type: RCDDeconstructable
+      cost: 4
+      delay: 6
+      fx: EffectRCDDeconstruct6  
     - type: Destructible
       thresholds:
       - trigger:
index 8cf75e89e1ab6214662a9638013a2913e4cbce58..3f4306e4aa171fff58260dab906bde92022aae55 100644 (file)
   - type: Damageable
     damageContainer: Inorganic
     damageModifierSet: Metallic
+  - type: RCDDeconstructable
+    cost: 4
+    delay: 6
+    fx: EffectRCDDeconstruct6    
   - type: Destructible
     thresholds:
     - trigger:
index 8dfe2f62a51a2d8ee87601729e6c75fe97b63b7f..b8fb203b517a64e8e996d2e7ad3b854898ca986c 100644 (file)
   - type: Damageable
     damageContainer: Inorganic
     damageModifierSet: Metallic
+  - type: RCDDeconstructable
+    cost: 6
+    delay: 6
+    fx: EffectRCDDeconstruct6
   - type: Destructible
     thresholds:
     - trigger:
index d6c087af0a5e3ac42f5770f40b4fc5ff654ad712..06e9d2219a5e4e3a7aac9f698d89061cd5ec4b17 100644 (file)
   - type: Damageable
     damageContainer: Inorganic
     damageModifierSet: Metallic
+  - type: RCDDeconstructable
+    cost: 6
+    delay: 8
+    fx: EffectRCDDeconstruct8
   - type: Destructible
     thresholds:
     - trigger:
   - type: Damageable
     damageContainer: Inorganic
     damageModifierSet: Metallic
+  - type: RCDDeconstructable
+    cost: 6
+    delay: 8
+    fx: EffectRCDDeconstruct8
   - type: Destructible
     thresholds:
     - trigger:
index 8d9cedac03ad656bd3c06804a0b82f4a4e25292a..5d47d9c5c4a3de549a9b6b064bae8ac71541bf41 100644 (file)
   - type: Damageable
     damageContainer: Inorganic
     damageModifierSet: Metallic
+  - type: RCDDeconstructable
+    cost: 6
+    delay: 8
+    fx: EffectRCDDeconstruct8
   - type: Destructible
     thresholds:
     - trigger:
index d03765d4fc9e52dc7c823d9518003cb24406c3ad..d58273edcc99f258c0e1c579e59b06ea84e3ffe0 100644 (file)
     damageContainer: Inorganic
     damageModifierSet: Glass
   - type: ExaminableDamage
-    messages: WindowMessages
+    messages: WindowMessages   
+  - type: RCDDeconstructable
+    cost: 8
+    delay: 8
+    fx: EffectRCDDeconstruct8
   - type: Destructible
     thresholds:
     - trigger:
index b4b198eb774ea2c9dec6e02c65b8a33695a90c89..ef89088d1abf36672dd6dd4a10d5a85883b3b05e 100644 (file)
   - type: Damageable
     damageContainer: Inorganic
     damageModifierSet: Metallic
+  - type: RCDDeconstructable
+    cost: 4
+    delay: 2
+    fx: EffectRCDDeconstruct2  
   - type: Destructible
     thresholds:
     - trigger:
@@ -70,7 +74,7 @@
     mode: SnapgridCenter
     snap:
     - Wallmount
-
+    
 - type: entity
   name: light
   description: "A light fixture. Draws power and produces light when equipped with a light tube."
index da724014dc420cc3ceda78d122ef1ed604062cb8..2e8f047c214b97432455f8d4c73fc821c6d09575 100644 (file)
     - type: Damageable
       damageContainer: Inorganic
       damageModifierSet: Metallic
+    - type: RCDDeconstructable
+      cost: 2
+      delay: 2
+      fx: EffectRCDDeconstruct2  
     - type: Destructible
       thresholds:
         - trigger:
index de66dfc66d60b8d4dec2869fa63a838b9cf2fa3c..a81c89de0fb3ef3bb40de07244c23cf5fbfe3bbb 100644 (file)
     lowVoltageNode: power
   - type: CableVis
     node: power
+  - type: RCDDeconstructable
+    cost: 2
+    delay: 2
+    fx: EffectRCDDeconstruct2  
 
 - type: entity
   parent: CableBase
index 88d2f272c08bcf980cc9845c76d8d8941e595dd8..1dca59225cbc413f1dc7ddcb7d9e7408ff4c78a3 100644 (file)
@@ -10,9 +10,6 @@
       Brute:
         path:
           "/Audio/Weapons/grille_hit.ogg"
-  - type: Tag
-    tags:
-    - RCDDeconstructWhitelist
   - type: Sprite
     sprite: Structures/Walls/fence.rsi
     drawdepth: WallTops
           True: { visible: True }
           False: { visible: False }
   - type: AnimationPlayer
+  - type: RCDDeconstructable
+    cost: 2
+    delay: 2
+    fx: EffectRCDDeconstruct2
 
 - type: entity
   parent: BaseFenceMetal
index f2b03aaeb8f68b5faca43c81dab0ec8976832fa4..55b7e40803b5c2283d0da856e0e181e5abbd805d 100644 (file)
@@ -12,7 +12,6 @@
           "/Audio/Weapons/boxingpunch1.ogg"
   - type: Tag
     tags:
-    - RCDDeconstructWhitelist
     - Wooden
   - type: Sprite
     sprite: Structures/Walls/wooden_fence.rsi
   - type: Damageable
     damageContainer: Inorganic
     damageModifierSet: Wood
+  - type: RCDDeconstructable
+    cost: 2
+    delay: 2
+    fx: EffectRCDDeconstruct2
   - type: Destructible
     thresholds:
     - trigger:
index b532db221dab29a2d74f3b121506c7be196f91e2..11ada142fa57dd2ac34065bec4e54195a888300b 100644 (file)
@@ -9,9 +9,10 @@
         Brute:
           path:
             "/Audio/Weapons/grille_hit.ogg"
-    - type: Tag
-      tags:
-        - RCDDeconstructWhitelist
+    - type: RCDDeconstructable
+      cost: 6
+      delay: 4
+      fx: EffectRCDDeconstruct4  
     - type: CanBuildWindowOnTop
     - type: Sprite
       drawdepth: Walls
     - type: Icon
       sprite: Structures/Walls/grille.rsi
       state: grille_broken
-    - type: Tag
-      tags:
-        - RCDDeconstructWhitelist
+    - type: RCDDeconstructable
+      cost: 6
+      delay: 4
+      fx: EffectRCDDeconstruct4
     - type: Construction
       graph: Grille
       node: grilleBroken
index 95d16742d5832bd69061fbdb32ac3306d35b9141..a23c559abab8c74433772729179e6989cbb10d2d 100644 (file)
   - type: Construction
     graph: Railing
     node: railing
+  - type: RCDDeconstructable
+    cost: 2
+    delay: 2
+    fx: EffectRCDDeconstruct2  
 
 - type: entity
   parent: BaseStructure
   - type: Construction
     graph: Railing
     node: railingCorner
+  - type: RCDDeconstructable
+    cost: 2
+    delay: 2
+    fx: EffectRCDDeconstruct2  
 
 - type: entity
   parent: BaseStructure
   - type: Construction
     graph: Railing
     node: railingCornerSmall
-
+  - type: RCDDeconstructable
+    cost: 2
+    delay: 2
+    fx: EffectRCDDeconstruct2  
+    
 - type: entity
   parent: BaseStructure
   id: RailingRound
   - type: Construction
     graph: Railing
     node: railingRound
+  - type: RCDDeconstructable
+    cost: 2
+    delay: 2
+    fx: EffectRCDDeconstruct2  
index f06c0fc42445b7fe9f15fc41bee4f195b7b2bb77..8eca58b1242824393f0d35cb02db06e79573dd13 100644 (file)
   - type: Tag
     tags:
       - Wall
-      - RCDDeconstructWhitelist
   - type: Sprite
     sprite: Structures/Walls/brick.rsi
   - type: Icon
     sprite: Structures/Walls/brick.rsi
+  - type: RCDDeconstructable
+    cost: 6
+    delay: 8
+    fx: EffectRCDDeconstruct8
   - type: Destructible
     thresholds:
     - trigger:
@@ -90,7 +93,6 @@
   - type: Tag
     tags:
       - Wall
-      - RCDDeconstructWhitelist
   - type: Sprite
     sprite: Structures/Walls/clock.rsi
   - type: Icon
   - type: Tag
     tags:
       - Wall
-      - RCDDeconstructWhitelist
   - type: Sprite
     sprite: Structures/Walls/clown.rsi
   - type: Icon
   - type: Construction
     graph: Girder
     node: bananiumWall
+  - type: RCDDeconstructable
+    cost: 6
+    delay: 8
+    fx: EffectRCDDeconstruct8    
   - type: Destructible
     thresholds:
     - trigger:
   components:
   - type: Tag
     tags:
-      - RCDDeconstructWhitelist
       - Wall
       - Structure
   - type: Sprite
   components:
   - type: Tag
     tags:
-      - RCDDeconstructWhitelist
       - Wall
   - type: Sprite
     sprite: Structures/Walls/cult.rsi
     tags:
       - Wall
       - Debug
-      - RCDDeconstructWhitelist
   - type: Sprite
     sprite: Structures/Walls/debug.rsi
   - type: Icon
   - type: Tag
     tags:
       - Wall
-      - RCDDeconstructWhitelist
   - type: Sprite
     sprite: Structures/Walls/diamond.rsi
   - type: Icon
     sprite: Structures/Walls/diamond.rsi
+  - type: RCDDeconstructable
+    cost: 6
+    delay: 8
+    fx: EffectRCDDeconstruct8   
   - type: Destructible
     thresholds:
     - trigger:
   - type: Tag
     tags:
       - Wall
-      - RCDDeconstructWhitelist
   - type: Sprite
     sprite: Structures/Walls/gold.rsi
   - type: Icon
   - type: Construction
     graph: Girder
     node: goldWall
+  - type: RCDDeconstructable
+    cost: 6
+    delay: 8
+    fx: EffectRCDDeconstruct8  
   - type: Destructible
     thresholds:
     - trigger:
   - type: Tag
     tags:
       - Wall
-      - RCDDeconstructWhitelist
   - type: Sprite
     sprite: Structures/Walls/ice.rsi
   - type: Icon
   - type: Tag
     tags:
       - Wall
-      - RCDDeconstructWhitelist
   - type: Sprite
     sprite: Structures/Walls/plasma.rsi
   - type: Icon
   - type: Construction
     graph: Girder
     node: plasmaWall
+  - type: RCDDeconstructable
+    cost: 6
+    delay: 8
+    fx: EffectRCDDeconstruct8
   - type: Destructible
     thresholds:
     - trigger:
   - type: Tag
     tags:
       - Wall
-      - RCDDeconstructWhitelist
   - type: Sprite
     sprite: Structures/Walls/plastic.rsi
   - type: Icon
   - type: Construction
     graph: Girder
     node: plasticWall
+  - type: RCDDeconstructable
+    cost: 6
+    delay: 8
+    fx: EffectRCDDeconstruct8    
   - type: Destructible
     thresholds:
     - trigger:
     - type: Tag
       tags:
         - Wall
-        - RCDDeconstructWhitelist
     - type: Destructible
       thresholds:
         - trigger:
   - type: Tag
     tags:
       - Wall
-      - RCDDeconstructWhitelist
   - type: Sprite
     sprite: Structures/Walls/riveted.rsi
   - type: Icon
   - type: Tag
     tags:
       - Wall
-      - RCDDeconstructWhitelist
   - type: Sprite
     sprite: Structures/Walls/sandstone.rsi
   - type: Icon
     sprite: Structures/Walls/sandstone.rsi
+  - type: RCDDeconstructable
+    cost: 6
+    delay: 8
+    fx: EffectRCDDeconstruct8    
   - type: Destructible
     thresholds:
     - trigger:
   - type: Tag
     tags:
       - Wall
-      - RCDDeconstructWhitelist
   - type: Sprite
     sprite: Structures/Walls/silver.rsi
   - type: Icon
   - type: Construction
     graph: Girder
     node: silverWall
+  - type: RCDDeconstructable
+    cost: 6
+    delay: 8
+    fx: EffectRCDDeconstruct8    
   - type: Destructible
     thresholds:
     - trigger:
   - type: Tag
     tags:
       - Wall
-      - RCDDeconstructWhitelist
   - type: Sprite
     sprite: Structures/Walls/solid.rsi
   - type: WallReplacementMarker
     node: wall
   - type: Icon
     sprite: Structures/Walls/solid.rsi
+  - type: RCDDeconstructable
+    cost: 6
+    delay: 8
+    fx: EffectRCDDeconstruct8    
   - type: Destructible
     thresholds:
     - trigger:
   - type: Tag
     tags:
       - Wall
-      - RCDDeconstructWhitelist
   - type: Sprite
     sprite: Structures/Walls/web.rsi
   - type: Icon
   - type: Tag
     tags:
       - Wall
-      - RCDDeconstructWhitelist
   - type: Sprite
     sprite: Structures/Walls/cobblebrick.rsi
   - type: Icon
     sprite: Structures/Walls/cobblebrick.rsi
+  - type: RCDDeconstructable
+    cost: 6
+    delay: 8
+    fx: EffectRCDDeconstruct8    
   - type: Destructible
     thresholds:
     - trigger:
index 910c3daae2a38393e7a43d72f777c4bf03020836..82d11b732b6516314146a392de456f9a471c7eae 100644 (file)
@@ -1,7 +1,7 @@
 - type: entity
   id: MiningWindow
   name: mining window
-  parent: Window
+  parent: WindowRCDResistant
   components:
   - type: Sprite
     drawdepth: WallTops
index 0dd2a1b06ccbefa0aa13af5a05dfabbab62c13e4..36a12f2d84407fece2f6d2185832f8cd5b71d2bc 100644 (file)
@@ -1,7 +1,7 @@
 - type: entity
   id: PlasmaWindow
   name: plasma window
-  parent: Window
+  parent: WindowRCDResistant
   components:
   - type: Sprite
     drawdepth: WallTops
@@ -55,7 +55,7 @@
 
 - type: entity
   id: PlasmaWindowDirectional
-  parent: WindowDirectional
+  parent: WindowDirectionalRCDResistant
   name: directional plasma window
   description: Don't smudge up the glass down there.
   placement:
index 1c79644ce4d86d99b8458f54c7f410fdec7cea8e..d8b6c7d11d8ea64c2ce2ce64191fe8a9ee0a4cb6 100644 (file)
   - type: Damageable
     damageContainer: StructuralInorganic
     damageModifierSet: RGlass
+  - type: RCDDeconstructable
+    cost: 6
+    delay: 6
+    fx: EffectRCDDeconstruct6
   - type: Destructible
     thresholds:
     - trigger:
       sprite: Structures/Windows/cracks_directional.rsi
   - type: Damageable
     damageModifierSet: RGlass
+  - type: RCDDeconstructable
+    cost: 4
+    delay: 4
+    fx: EffectRCDDeconstruct4  
   - type: Destructible
     thresholds:
     - trigger:
index d81204be071e82cf9f0808bcd8f32569948b9605..93859b1db2c72f8cc2256cbd721129de2653c1ea 100644 (file)
@@ -1,7 +1,7 @@
 - type: entity
   id: ReinforcedPlasmaWindow
   name: reinforced plasma window
-  parent: Window
+  parent: WindowRCDResistant
   components:
   - type: Sprite
     drawdepth: WallTops
@@ -58,7 +58,7 @@
 
 - type: entity
   id: PlasmaReinforcedWindowDirectional
-  parent: WindowDirectional
+  parent: WindowDirectionalRCDResistant
   name: directional reinforced plasma window
   description: Don't smudge up the glass down there.
   placement:
index 6ed2cc592673c2ba8f7cc4867f5d8ff7e62690c5..e26fec65b77249d29295f2380415986663b81a6f 100644 (file)
@@ -1,7 +1,7 @@
 - type: entity
   id: ReinforcedUraniumWindow
   name: reinforced uranium window
-  parent: Window
+  parent: WindowRCDResistant
   components:
   - type: Sprite
     drawdepth: WallTops
index d953cc588ac977d3b85d3ae27ba4274c66e4284d..f1b840c14354dab1390915fe9c01915261d49592 100644 (file)
@@ -1,7 +1,7 @@
 - type: entity
   id: ShuttleWindow
   name: shuttle window
-  parent: Window
+  parent: WindowRCDResistant
   components:
   - type: Sprite
     drawdepth: WallTops
index b956d369fa38af89e6b043ee973899d3a404ca7f..e5228bc593e3e266b64545e025b0c285f9f1ca2a 100644 (file)
@@ -1,7 +1,7 @@
 - type: entity
   id: UraniumWindow
   name: uranium window
-  parent: Window
+  parent: WindowRCDResistant
   components:
   - type: Sprite
     drawdepth: WallTops
index 375d0c16aedda8e24fc6d79f81160fc290d8134f..606c54e35b8b9de2874c58ee8298fa3355b8753b 100644 (file)
@@ -16,7 +16,6 @@
     arc: 360 # interact despite grilles
   - type: Tag
     tags:
-      - RCDDeconstructWhitelist
       - ForceFixRotations
       - Window
   - type: Sprite
   - type: ExaminableDamage
     messages: WindowMessages
   - type: Repairable
+  - type: RCDDeconstructable
+    cost: 6
+    delay: 4
+    fx: EffectRCDDeconstruct4
   - type: Destructible
     thresholds:
     - trigger:
   - type: StaticPrice
     price: 100
   - type: BlockWeather
+  
+- type: entity
+  id: WindowRCDResistant
+  parent: Window
+  abstract: true
+  components:
+  - type: RCDDeconstructable
+    deconstructable: false
 
 - type: entity
   id: WindowDirectional
     damageModifierSet: Glass
   - type: ExaminableDamage
     messages: WindowMessages
+  - type: RCDDeconstructable
+    cost: 4
+    delay: 2
+    fx: EffectRCDDeconstruct2
   - type: Destructible
     thresholds:
     - trigger:
   - type: StaticPrice
     price: 10
 
+- type: entity
+  id: WindowDirectionalRCDResistant  
+  parent: WindowDirectional
+  abstract: true
+  components:
+  - type: RCDDeconstructable
+    deconstructable: false
+
 - type: entity
   id: WindowFrostedDirectional
   parent: WindowDirectional
index c727c249522f6936536fdf392def085091faa8ac..6dee91365ef0bde138a7f943c69f587d1f000fe6 100644 (file)
@@ -51,3 +51,7 @@
             max: 1
       - !type:DoActsBehavior
         acts: [ "Destruction" ]
+  - type: RCDDeconstructable
+    cost: 2
+    delay: 2
+    fx: EffectRCDDeconstruct2  
\ No newline at end of file
diff --git a/Resources/Prototypes/RCD/rcd.yml b/Resources/Prototypes/RCD/rcd.yml
new file mode 100644 (file)
index 0000000..cb2c9ed
--- /dev/null
@@ -0,0 +1,294 @@
+# Operations
+- type: rcd
+  id: Invalid   # Hidden prototype - do not add to RCDs
+  mode: Invalid
+  
+- type: rcd
+  id: Deconstruct
+  name: rcd-component-deconstruct
+  category: Main
+  sprite: /Textures/Interface/Radial/RCD/deconstruct.png
+  mode: Deconstruct
+  prototype: EffectRCDDeconstructPreview
+  rotation: Camera
+
+- type: rcd
+  id: DeconstructLattice   # Hidden prototype - do not add to RCDs  
+  mode: Deconstruct
+  cost: 2
+  delay: 1
+  rotation: Camera
+  fx: EffectRCDDeconstruct2
+    
+- type: rcd
+  id: DeconstructTile      # Hidden prototype - do not add to RCDs
+  mode: Deconstruct
+  cost: 4
+  delay: 4
+  rotation: Camera
+  fx: EffectRCDDeconstruct4
+
+# Flooring 
+- type: rcd
+  id: Plating
+  name: rcd-component-plating
+  category: WallsAndFlooring
+  sprite: /Textures/Interface/Radial/RCD/plating.png
+  mode: ConstructTile
+  prototype: Plating
+  cost: 1
+  delay: 1
+  collisionMask: InteractImpassable
+  rules:
+    - CanBuildOnEmptyTile
+  fx: EffectRCDConstruct1
+  
+- type: rcd
+  id: FloorSteel
+  name: rcd-component-floor-steel
+  category: WallsAndFlooring
+  sprite: /Textures/Interface/Radial/RCD/metal_tile.png
+  mode: ConstructTile
+  prototype: FloorSteel
+  cost: 1
+  delay: 1
+  collisionMask: InteractImpassable
+  rules:
+    - CanBuildOnEmptyTile
+  fx: EffectRCDConstruct1
+
+- type: rcd
+  id: Catwalk
+  name: rcd-component-catwalk
+  category: WallsAndFlooring
+  sprite: /Textures/Interface/Radial/RCD/catwalk.png
+  mode: ConstructObject
+  prototype: Catwalk
+  cost: 1
+  delay: 1
+  collisionMask: InteractImpassable
+  rules:
+    - MustBuildOnSubfloor
+    - IsCatwalk
+  rotation: Fixed
+  fx: EffectRCDConstruct1
+
+# Walls
+- type: rcd
+  id: WallSolid
+  name: rcd-component-wall-solid
+  category: WallsAndFlooring
+  sprite: /Textures/Interface/Radial/RCD/solid_wall.png
+  mode: ConstructObject
+  prototype: WallSolid 
+  cost: 4
+  delay: 2
+  collisionMask: FullTileMask
+  rotation: Fixed
+  fx: EffectRCDConstruct2
+
+- type: rcd
+  id: Grille
+  name: rcd-component-grille
+  category: WindowsAndGrilles
+  sprite: /Textures/Interface/Radial/RCD/grille.png
+  mode: ConstructObject
+  prototype: Grille
+  cost: 4
+  delay: 2
+  collisionMask: FullTileMask
+  rotation: Fixed
+  fx: EffectRCDConstruct2
+
+# Windows
+- type: rcd
+  id: Window
+  name: rcd-component-window
+  category: WindowsAndGrilles
+  sprite: /Textures/Interface/Radial/RCD/window.png
+  mode: ConstructObject
+  prototype: Window
+  cost: 3
+  delay: 2
+  collisionMask: FullTileMask
+  rules:
+    - IsWindow
+  rotation: Fixed
+  fx: EffectRCDConstruct2
+  
+- type: rcd
+  id: WindowDirectional
+  name: rcd-component-window-directional
+  category: WindowsAndGrilles
+  sprite: /Textures/Interface/Radial/RCD/directional.png
+  mode: ConstructObject
+  prototype: WindowDirectional
+  cost: 2
+  delay: 1
+  collisionMask: FullTileMask
+  collisionBounds: "-0.23,-0.49,0.23,-0.36"
+  rules:
+    - IsWindow
+  rotation: User
+  fx: EffectRCDConstruct1
+  
+- type: rcd
+  id: ReinforcedWindow
+  name: rcd-component-reinforced-window
+  category: WindowsAndGrilles
+  sprite: /Textures/Interface/Radial/RCD/window_reinforced.png
+  mode: ConstructObject
+  prototype: ReinforcedWindow
+  cost: 4
+  delay: 3
+  collisionMask: FullTileMask
+  rules:
+    - IsWindow
+  rotation: User
+  fx: EffectRCDConstruct3
+    
+- type: rcd
+  id: WindowReinforcedDirectional
+  name: rcd-component-window-reinforced-directional
+  category: WindowsAndGrilles
+  sprite: /Textures/Interface/Radial/RCD/directional_reinforced.png
+  mode: ConstructObject
+  prototype: WindowReinforcedDirectional
+  cost: 3
+  delay: 2
+  collisionMask: FullTileMask
+  collisionBounds: "-0.23,-0.49,0.23,-0.36"
+  rules:
+    - IsWindow
+  rotation: User
+  fx: EffectRCDConstruct2
+
+# Airlocks
+- type: rcd
+  id: Airlock
+  name: rcd-component-airlock
+  category: Airlocks
+  sprite: /Textures/Interface/Radial/RCD/airlock.png
+  mode: ConstructObject
+  prototype: Airlock
+  cost: 4
+  delay: 4
+  collisionMask: FullTileMask
+  rotation: Camera
+  fx: EffectRCDConstruct4
+  
+- type: rcd
+  id: AirlockGlass
+  name: rcd-component-airlock-glass
+  category: Airlocks
+  sprite: /Textures/Interface/Radial/RCD/glass_airlock.png
+  mode: ConstructObject
+  prototype: AirlockGlass
+  cost: 4
+  delay: 4
+  collisionMask: FullTileMask
+  rotation: Camera
+  fx: EffectRCDConstruct4
+  
+- type: rcd
+  id: Firelock
+  name: rcd-component-firelock
+  category: Airlocks
+  sprite: /Textures/Interface/Radial/RCD/firelock.png
+  mode: ConstructObject
+  prototype: Firelock
+  cost: 4
+  delay: 3
+  collisionMask: FullTileMask
+  rotation: Camera
+  fx: EffectRCDConstruct3
+
+# Lighting
+- type: rcd
+  id: TubeLight
+  name: rcd-component-tube-light
+  category: Lighting
+  sprite: /Textures/Interface/Radial/RCD/tube_light.png
+  mode: ConstructObject
+  prototype: Poweredlight
+  cost: 2
+  delay: 1
+  collisionMask: TabletopMachineMask
+  collisionBounds: "-0.23,-0.49,0.23,-0.36"
+  rotation: User
+  fx: EffectRCDConstruct1
+  
+- type: rcd
+  id: BulbLight
+  name: rcd-component-window-bulb-light
+  category: Lighting
+  sprite: /Textures/Interface/Radial/RCD/bulb_light.png
+  mode: ConstructObject
+  prototype: PoweredSmallLight
+  cost: 2
+  delay: 1
+  collisionMask: TabletopMachineMask
+  collisionBounds: "-0.23,-0.49,0.23,-0.36"
+  rotation: User
+  fx: EffectRCDConstruct1
+
+# Electrical
+- type: rcd
+  id: LVCable
+  name: rcd-component-window-lv-cable
+  category: Electrical
+  sprite: /Textures/Interface/Radial/RCD/lv_coil.png
+  mode: ConstructObject
+  prototype: CableApcExtension
+  cost: 1
+  delay: 0
+  collisionMask: InteractImpassable
+  rules:
+  - MustBuildOnSubfloor
+  rotation: Fixed
+  fx: EffectRCDConstruct0
+    
+- type: rcd
+  id: MVCable
+  name: rcd-component-window-mv-cable
+  category: Electrical
+  sprite: /Textures/Interface/Radial/RCD/mv_coil.png
+  mode: ConstructObject
+  prototype: CableMV
+  cost: 1
+  delay: 0
+  collisionMask: InteractImpassable
+  rules:
+  - MustBuildOnSubfloor
+  rotation: Fixed
+  fx: EffectRCDConstruct0
+    
+- type: rcd
+  id: HVCable
+  name: rcd-component-window-hv-cable
+  category: Electrical
+  sprite: /Textures/Interface/Radial/RCD/hv_coil.png
+  mode: ConstructObject
+  prototype: CableHV
+  cost: 1
+  delay: 0
+  collisionMask: InteractImpassable
+  rules:
+  - MustBuildOnSubfloor
+  rotation: Fixed
+  fx: EffectRCDConstruct0
+    
+- type: rcd
+  id: CableTerminal
+  name: rcd-component-window-cable-terminal
+  category: Electrical
+  sprite: /Textures/Interface/Radial/RCD/cable_terminal.png
+  mode: ConstructObject
+  prototype: CableTerminal
+  cost: 1
+  delay: 0
+  collisionMask: InteractImpassable
+  rules:
+  - MustBuildOnSubfloor
+  rotation: User
+  fx: EffectRCDConstruct0
\ No newline at end of file
index fe8c5a3cc17d09951fb37cccbf0360ea45f506c1..8f0038915deae95fd2a4430521d6df65ec1fa4d9 100644 (file)
 - type: Tag
   id: RawMaterial
 
-- type: Tag
-  id: RCDDeconstructWhitelist
-
 # Give this to something that doesn't need any special recycler behavior and just needs deleting.
 - type: Tag
   id: Recyclable
diff --git a/Resources/Textures/Effects/rcd.rsi/construct.png b/Resources/Textures/Effects/rcd.rsi/construct.png
deleted file mode 100644 (file)
index f4be36c..0000000
Binary files a/Resources/Textures/Effects/rcd.rsi/construct.png and /dev/null differ
diff --git a/Resources/Textures/Effects/rcd.rsi/construct0.png b/Resources/Textures/Effects/rcd.rsi/construct0.png
new file mode 100644 (file)
index 0000000..d83fa05
Binary files /dev/null and b/Resources/Textures/Effects/rcd.rsi/construct0.png differ
diff --git a/Resources/Textures/Effects/rcd.rsi/construct1.png b/Resources/Textures/Effects/rcd.rsi/construct1.png
new file mode 100644 (file)
index 0000000..0b597fe
Binary files /dev/null and b/Resources/Textures/Effects/rcd.rsi/construct1.png differ
diff --git a/Resources/Textures/Effects/rcd.rsi/construct2.png b/Resources/Textures/Effects/rcd.rsi/construct2.png
new file mode 100644 (file)
index 0000000..0b597fe
Binary files /dev/null and b/Resources/Textures/Effects/rcd.rsi/construct2.png differ
diff --git a/Resources/Textures/Effects/rcd.rsi/construct3.png b/Resources/Textures/Effects/rcd.rsi/construct3.png
new file mode 100644 (file)
index 0000000..0b597fe
Binary files /dev/null and b/Resources/Textures/Effects/rcd.rsi/construct3.png differ
diff --git a/Resources/Textures/Effects/rcd.rsi/construct4.png b/Resources/Textures/Effects/rcd.rsi/construct4.png
new file mode 100644 (file)
index 0000000..1f47a0f
Binary files /dev/null and b/Resources/Textures/Effects/rcd.rsi/construct4.png differ
diff --git a/Resources/Textures/Effects/rcd.rsi/deconstruct2.png b/Resources/Textures/Effects/rcd.rsi/deconstruct2.png
new file mode 100644 (file)
index 0000000..d2144af
Binary files /dev/null and b/Resources/Textures/Effects/rcd.rsi/deconstruct2.png differ
diff --git a/Resources/Textures/Effects/rcd.rsi/deconstruct4.png b/Resources/Textures/Effects/rcd.rsi/deconstruct4.png
new file mode 100644 (file)
index 0000000..7077115
Binary files /dev/null and b/Resources/Textures/Effects/rcd.rsi/deconstruct4.png differ
diff --git a/Resources/Textures/Effects/rcd.rsi/deconstruct6.png b/Resources/Textures/Effects/rcd.rsi/deconstruct6.png
new file mode 100644 (file)
index 0000000..7077115
Binary files /dev/null and b/Resources/Textures/Effects/rcd.rsi/deconstruct6.png differ
diff --git a/Resources/Textures/Effects/rcd.rsi/deconstruct8.png b/Resources/Textures/Effects/rcd.rsi/deconstruct8.png
new file mode 100644 (file)
index 0000000..79d724e
Binary files /dev/null and b/Resources/Textures/Effects/rcd.rsi/deconstruct8.png differ
diff --git a/Resources/Textures/Effects/rcd.rsi/deconstructPreview.png b/Resources/Textures/Effects/rcd.rsi/deconstructPreview.png
new file mode 100644 (file)
index 0000000..2d11400
Binary files /dev/null and b/Resources/Textures/Effects/rcd.rsi/deconstructPreview.png differ
index 5004a9c4fcf86e24d910134c4e0a7f28f4ce53e5..c9e8320c60b88eb99e350832ed616beaf664fb11 100644 (file)
@@ -8,7 +8,202 @@
     "copyright": "Taken from tgStation at commit https://github.com/tgstation/tgstation/commit/d75cbd0a2900fdec4c12cd5ba986b52ccff03713/icons/effects/effects_rcd.dmi",
     "states": [
         {
-            "name": "construct",
+            "name": "construct0",
+            "delays": [
+                [
+                    0.1,
+                    0.1,
+                    0.1,
+                    0.1,
+                    0.1,
+                    0.1,
+                    0.1,
+                    0.1,
+                    0.1,
+                    0.1,
+                    0.1,
+                    0.1
+                ]
+            ]
+        },             
+               {
+            "name": "construct1",
+            "delays": [
+                [
+                    0.05,
+                    0.05,
+                    0.05,
+                    0.05,
+                    0.05,
+                    0.05,
+                    0.05,
+                    0.05,
+                    0.05,
+                    0.05,
+                    0.05,
+                    0.05,
+                    0.05,
+                    0.05,
+                    0.05,
+                    0.05,
+                    0.05,
+                    0.05,
+                    0.05,
+                    0.05,
+                    0.1,
+                    0.1,
+                    0.1,
+                    0.1,
+                    0.1,
+                    0.1,
+                    0.1,
+                    0.1,
+                    0.1,
+                    0.1,
+                    0.1,
+                    0.1
+                ]
+            ]
+        },             
+               {
+            "name": "construct2",
+            "delays": [
+                [
+                    0.1,
+                    0.1,
+                    0.1,
+                    0.1,
+                    0.1,
+                    0.1,
+                    0.1,
+                    0.1,
+                    0.1,
+                    0.1,
+                    0.1,
+                    0.1,
+                    0.1,
+                    0.1,
+                    0.1,
+                    0.1,
+                    0.1,
+                    0.1,
+                    0.1,
+                    0.1,
+                    0.1,
+                    0.1,
+                    0.1,
+                    0.1,
+                    0.1,
+                    0.1,
+                    0.1,
+                    0.1,
+                    0.1,
+                    0.1,
+                    0.1,
+                    0.1
+                ]
+            ]
+        },
+               {
+            "name": "construct3",
+            "delays": [
+                [
+                    0.15,
+                    0.15,
+                    0.15,
+                    0.15,
+                    0.15,
+                    0.15,
+                    0.15,
+                    0.15,
+                    0.15,
+                    0.15,
+                    0.15,
+                    0.15,
+                    0.15,
+                    0.15,
+                    0.15,
+                    0.15,
+                    0.15,
+                    0.15,
+                    0.15,
+                    0.15,
+                    0.1,
+                    0.1,
+                    0.1,
+                    0.1,
+                    0.1,
+                    0.1,
+                    0.1,
+                    0.1,
+                    0.1,
+                    0.1,
+                    0.1,
+                    0.1
+                ]
+            ]
+        },
+               {
+            "name": "construct4",
+            "delays": [
+                [
+                    0.1,
+                    0.1,
+                    0.1,
+                    0.1,
+                    0.1,
+                    0.1,
+                    0.1,
+                    0.1,
+                    0.1,
+                    0.1,
+                    0.1,
+                    0.1,
+                    0.1,
+                    0.1,
+                    0.1,
+                    0.1,
+                    0.1,
+                    0.1,
+                    0.1,
+                    0.1,
+                                       0.1,
+                    0.1,
+                    0.1,
+                    0.1,
+                    0.1,
+                    0.1,
+                    0.1,
+                    0.1,
+                    0.1,
+                    0.1,
+                    0.1,
+                    0.1,
+                    0.1,
+                    0.1,
+                    0.1,
+                    0.1,
+                    0.1,
+                    0.1,
+                    0.1,
+                    0.1,
+                    0.1,
+                    0.1,
+                    0.1,
+                    0.1,
+                    0.1,
+                    0.1,
+                    0.1,
+                    0.1,
+                    0.1,
+                    0.1,
+                    0.1,
+                    0.1
+                ]
+            ]
+        },             
+               {
+            "name": "deconstruct2",
             "delays": [
                 [
                     0.1,
                     0.1
                 ]
             ]
+        },
+               {
+            "name": "deconstruct4",
+            "delays": [
+                [
+                    0.1,
+                    0.1,
+                    0.1,
+                    0.1,
+                    0.1,
+                    0.1,
+                    0.1,
+                    0.1,
+                    0.1,
+                    0.1,
+                    0.1,
+                    0.1,
+                    0.1,
+                    0.1,
+                    0.1,
+                    0.1,
+                    0.1,
+                    0.1,
+                    0.1,
+                    0.1,
+                                       0.1,
+                    0.1,
+                    0.1,
+                    0.1,
+                    0.1,
+                    0.1,
+                    0.1,
+                    0.1,
+                    0.1,
+                    0.1,
+                    0.1,
+                    0.1,
+                    0.1,
+                    0.1,
+                    0.1,
+                    0.1,
+                    0.1,
+                    0.1,
+                    0.1,
+                    0.1,
+                    0.1,
+                    0.1,
+                    0.1,
+                    0.1,
+                    0.1,
+                    0.1,
+                    0.1,
+                    0.1,
+                    0.1,
+                    0.1,
+                    0.1,
+                    0.1
+                ]
+            ]
+        },
+               {
+            "name": "deconstruct6",
+            "delays": [
+                [
+                    0.15,
+                    0.15,
+                    0.15,
+                    0.15,
+                    0.15,
+                    0.15,
+                    0.15,
+                    0.15,
+                    0.15,
+                    0.15,
+                    0.15,
+                    0.15,
+                    0.15,
+                    0.15,
+                    0.15,
+                    0.15,
+                    0.15,
+                    0.15,
+                    0.15,
+                    0.15,
+                                       0.15,
+                    0.15,
+                    0.15,
+                    0.15,
+                    0.15,
+                    0.15,
+                    0.15,
+                    0.15,
+                    0.15,
+                    0.15,
+                    0.15,
+                    0.15,
+                    0.15,
+                    0.15,
+                    0.15,
+                    0.15,
+                    0.15,
+                    0.15,
+                    0.15,
+                    0.15,
+                    0.1,
+                    0.1,
+                    0.1,
+                    0.1,
+                    0.1,
+                    0.1,
+                    0.1,
+                    0.1,
+                    0.1,
+                    0.1,
+                    0.1,
+                    0.1
+                ]
+            ]
+        },             
+               {
+            "name": "deconstruct8",
+            "delays": [
+                [
+                    0.2,
+                    0.2,
+                    0.2,
+                    0.2,
+                    0.2,
+                    0.2,
+                    0.2,
+                    0.2,
+                    0.2,
+                    0.2,
+                    0.2,
+                    0.2,
+                    0.2,
+                    0.2,
+                    0.2,
+                    0.2,
+                    0.2,
+                    0.2,
+                    0.2,
+                    0.2,
+                                       0.2,
+                    0.2,
+                    0.2,
+                    0.2,
+                    0.2,
+                    0.2,
+                    0.2,
+                    0.2,
+                    0.2,
+                    0.2,
+                    0.2,
+                    0.2,
+                    0.2,
+                    0.2,
+                    0.2,
+                    0.2,
+                    0.2,
+                    0.2,
+                    0.2,
+                    0.2,
+                    0.1,
+                    0.1,
+                    0.1,
+                    0.1,
+                    0.1,
+                    0.1,
+                    0.1,
+                    0.1,
+                    0.1,
+                    0.1,
+                    0.1,
+                    0.1
+                ]
+            ]
+        },
+               {
+            "name": "deconstructPreview",
+            "delays": [
+                [
+                    0.05,
+                    0.05,
+                    0.05,
+                    0.05,
+                    0.05,
+                    0.05,
+                    0.05,
+                    0.05,
+                    0.05,
+                    0.05,
+                    0.05,
+                    0.05,
+                    0.05,
+                    0.05,
+                    0.05,
+                    0.05,
+                    0.05,
+                    0.05,
+                    0.05,
+                    0.05,
+                                       0.05,
+                    0.05,
+                    0.05,
+                    0.05,
+                    0.05,
+                    0.05,
+                    0.05,
+                    0.05,
+                    0.05,
+                    0.05                    
+                ]
+            ]
         }
     ]
 }
diff --git a/Resources/Textures/Interface/Radial/RCD/airlock.png b/Resources/Textures/Interface/Radial/RCD/airlock.png
new file mode 100644 (file)
index 0000000..7b1e0e9
Binary files /dev/null and b/Resources/Textures/Interface/Radial/RCD/airlock.png differ
diff --git a/Resources/Textures/Interface/Radial/RCD/airlocks.png b/Resources/Textures/Interface/Radial/RCD/airlocks.png
new file mode 100644 (file)
index 0000000..1195d79
Binary files /dev/null and b/Resources/Textures/Interface/Radial/RCD/airlocks.png differ
diff --git a/Resources/Textures/Interface/Radial/RCD/bulb_light.png b/Resources/Textures/Interface/Radial/RCD/bulb_light.png
new file mode 100644 (file)
index 0000000..d607beb
Binary files /dev/null and b/Resources/Textures/Interface/Radial/RCD/bulb_light.png differ
diff --git a/Resources/Textures/Interface/Radial/RCD/cable_terminal.png b/Resources/Textures/Interface/Radial/RCD/cable_terminal.png
new file mode 100644 (file)
index 0000000..7634566
Binary files /dev/null and b/Resources/Textures/Interface/Radial/RCD/cable_terminal.png differ
diff --git a/Resources/Textures/Interface/Radial/RCD/catwalk.png b/Resources/Textures/Interface/Radial/RCD/catwalk.png
new file mode 100644 (file)
index 0000000..c550f12
Binary files /dev/null and b/Resources/Textures/Interface/Radial/RCD/catwalk.png differ
diff --git a/Resources/Textures/Interface/Radial/RCD/computer_frame.png b/Resources/Textures/Interface/Radial/RCD/computer_frame.png
new file mode 100644 (file)
index 0000000..39661a4
Binary files /dev/null and b/Resources/Textures/Interface/Radial/RCD/computer_frame.png differ
diff --git a/Resources/Textures/Interface/Radial/RCD/computers_and_frames.png b/Resources/Textures/Interface/Radial/RCD/computers_and_frames.png
new file mode 100644 (file)
index 0000000..0f17ab5
Binary files /dev/null and b/Resources/Textures/Interface/Radial/RCD/computers_and_frames.png differ
diff --git a/Resources/Textures/Interface/Radial/RCD/deconstruct.png b/Resources/Textures/Interface/Radial/RCD/deconstruct.png
new file mode 100644 (file)
index 0000000..ca9548e
Binary files /dev/null and b/Resources/Textures/Interface/Radial/RCD/deconstruct.png differ
diff --git a/Resources/Textures/Interface/Radial/RCD/directional.png b/Resources/Textures/Interface/Radial/RCD/directional.png
new file mode 100644 (file)
index 0000000..473ad25
Binary files /dev/null and b/Resources/Textures/Interface/Radial/RCD/directional.png differ
diff --git a/Resources/Textures/Interface/Radial/RCD/directional_reinforced.png b/Resources/Textures/Interface/Radial/RCD/directional_reinforced.png
new file mode 100644 (file)
index 0000000..3cb416d
Binary files /dev/null and b/Resources/Textures/Interface/Radial/RCD/directional_reinforced.png differ
diff --git a/Resources/Textures/Interface/Radial/RCD/firelock.png b/Resources/Textures/Interface/Radial/RCD/firelock.png
new file mode 100644 (file)
index 0000000..5b581a2
Binary files /dev/null and b/Resources/Textures/Interface/Radial/RCD/firelock.png differ
diff --git a/Resources/Textures/Interface/Radial/RCD/glass_airlock.png b/Resources/Textures/Interface/Radial/RCD/glass_airlock.png
new file mode 100644 (file)
index 0000000..bc0dbb3
Binary files /dev/null and b/Resources/Textures/Interface/Radial/RCD/glass_airlock.png differ
diff --git a/Resources/Textures/Interface/Radial/RCD/grille.png b/Resources/Textures/Interface/Radial/RCD/grille.png
new file mode 100644 (file)
index 0000000..5a52346
Binary files /dev/null and b/Resources/Textures/Interface/Radial/RCD/grille.png differ
diff --git a/Resources/Textures/Interface/Radial/RCD/hv_coil.png b/Resources/Textures/Interface/Radial/RCD/hv_coil.png
new file mode 100644 (file)
index 0000000..c2b5cab
Binary files /dev/null and b/Resources/Textures/Interface/Radial/RCD/hv_coil.png differ
diff --git a/Resources/Textures/Interface/Radial/RCD/lighting.png b/Resources/Textures/Interface/Radial/RCD/lighting.png
new file mode 100644 (file)
index 0000000..df167f3
Binary files /dev/null and b/Resources/Textures/Interface/Radial/RCD/lighting.png differ
diff --git a/Resources/Textures/Interface/Radial/RCD/lv_coil.png b/Resources/Textures/Interface/Radial/RCD/lv_coil.png
new file mode 100644 (file)
index 0000000..f83ed75
Binary files /dev/null and b/Resources/Textures/Interface/Radial/RCD/lv_coil.png differ
diff --git a/Resources/Textures/Interface/Radial/RCD/machine_frame.png b/Resources/Textures/Interface/Radial/RCD/machine_frame.png
new file mode 100644 (file)
index 0000000..5dd9025
Binary files /dev/null and b/Resources/Textures/Interface/Radial/RCD/machine_frame.png differ
diff --git a/Resources/Textures/Interface/Radial/RCD/metal_tile.png b/Resources/Textures/Interface/Radial/RCD/metal_tile.png
new file mode 100644 (file)
index 0000000..14f1ce2
Binary files /dev/null and b/Resources/Textures/Interface/Radial/RCD/metal_tile.png differ
diff --git a/Resources/Textures/Interface/Radial/RCD/multicoil.png b/Resources/Textures/Interface/Radial/RCD/multicoil.png
new file mode 100644 (file)
index 0000000..9e32919
Binary files /dev/null and b/Resources/Textures/Interface/Radial/RCD/multicoil.png differ
diff --git a/Resources/Textures/Interface/Radial/RCD/mv_coil.png b/Resources/Textures/Interface/Radial/RCD/mv_coil.png
new file mode 100644 (file)
index 0000000..c58e9a2
Binary files /dev/null and b/Resources/Textures/Interface/Radial/RCD/mv_coil.png differ
diff --git a/Resources/Textures/Interface/Radial/RCD/plating.png b/Resources/Textures/Interface/Radial/RCD/plating.png
new file mode 100644 (file)
index 0000000..d3a63ac
Binary files /dev/null and b/Resources/Textures/Interface/Radial/RCD/plating.png differ
diff --git a/Resources/Textures/Interface/Radial/RCD/reinforced_wall.png b/Resources/Textures/Interface/Radial/RCD/reinforced_wall.png
new file mode 100644 (file)
index 0000000..0748683
Binary files /dev/null and b/Resources/Textures/Interface/Radial/RCD/reinforced_wall.png differ
diff --git a/Resources/Textures/Interface/Radial/RCD/solid_wall.png b/Resources/Textures/Interface/Radial/RCD/solid_wall.png
new file mode 100644 (file)
index 0000000..b3efd02
Binary files /dev/null and b/Resources/Textures/Interface/Radial/RCD/solid_wall.png differ
diff --git a/Resources/Textures/Interface/Radial/RCD/tube_light.png b/Resources/Textures/Interface/Radial/RCD/tube_light.png
new file mode 100644 (file)
index 0000000..9c94ccb
Binary files /dev/null and b/Resources/Textures/Interface/Radial/RCD/tube_light.png differ
diff --git a/Resources/Textures/Interface/Radial/RCD/walls_and_flooring.png b/Resources/Textures/Interface/Radial/RCD/walls_and_flooring.png
new file mode 100644 (file)
index 0000000..88d3c36
Binary files /dev/null and b/Resources/Textures/Interface/Radial/RCD/walls_and_flooring.png differ
diff --git a/Resources/Textures/Interface/Radial/RCD/window.png b/Resources/Textures/Interface/Radial/RCD/window.png
new file mode 100644 (file)
index 0000000..ed486c2
Binary files /dev/null and b/Resources/Textures/Interface/Radial/RCD/window.png differ
diff --git a/Resources/Textures/Interface/Radial/RCD/window_reinforced.png b/Resources/Textures/Interface/Radial/RCD/window_reinforced.png
new file mode 100644 (file)
index 0000000..80efff0
Binary files /dev/null and b/Resources/Textures/Interface/Radial/RCD/window_reinforced.png differ
diff --git a/Resources/Textures/Interface/Radial/RCD/windows_and_grilles.png b/Resources/Textures/Interface/Radial/RCD/windows_and_grilles.png
new file mode 100644 (file)
index 0000000..e51ab09
Binary files /dev/null and b/Resources/Textures/Interface/Radial/RCD/windows_and_grilles.png differ
diff --git a/Resources/Textures/Interface/Radial/back_hover.png b/Resources/Textures/Interface/Radial/back_hover.png
new file mode 100644 (file)
index 0000000..7378a60
Binary files /dev/null and b/Resources/Textures/Interface/Radial/back_hover.png differ
diff --git a/Resources/Textures/Interface/Radial/back_normal.png b/Resources/Textures/Interface/Radial/back_normal.png
new file mode 100644 (file)
index 0000000..2c4a200
Binary files /dev/null and b/Resources/Textures/Interface/Radial/back_normal.png differ
diff --git a/Resources/Textures/Interface/Radial/button_hover.png b/Resources/Textures/Interface/Radial/button_hover.png
new file mode 100644 (file)
index 0000000..49885a2
Binary files /dev/null and b/Resources/Textures/Interface/Radial/button_hover.png differ
diff --git a/Resources/Textures/Interface/Radial/button_normal.png b/Resources/Textures/Interface/Radial/button_normal.png
new file mode 100644 (file)
index 0000000..66dc277
Binary files /dev/null and b/Resources/Textures/Interface/Radial/button_normal.png differ
diff --git a/Resources/Textures/Interface/Radial/close_hover.png b/Resources/Textures/Interface/Radial/close_hover.png
new file mode 100644 (file)
index 0000000..873d209
Binary files /dev/null and b/Resources/Textures/Interface/Radial/close_hover.png differ
diff --git a/Resources/Textures/Interface/Radial/close_normal.png b/Resources/Textures/Interface/Radial/close_normal.png
new file mode 100644 (file)
index 0000000..99417f2
Binary files /dev/null and b/Resources/Textures/Interface/Radial/close_normal.png differ