// 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);
--- /dev/null
+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;
+ }
+}
--- /dev/null
+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);
+ }
+}
--- /dev/null
+<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>
--- /dev/null
+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()
+ {
+
+ }
+}
--- /dev/null
+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();
+ }
+}
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);
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);
};
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
// 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);
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);
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)
--- /dev/null
+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,
+ }
+}
--- /dev/null
+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;
+ }
+}
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);
+ }
}
-using Robust.Shared.Prototypes;
+using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
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; }
}
}
/// 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
-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.
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!;
}
--- /dev/null
+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;
+}
--- /dev/null
+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
+}
--- /dev/null
+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
+}
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!;
[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;
### 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
--- /dev/null
+### Loc for the various UI-related verbs
+ui-verb-toggle-open = Toggle UI
+verb-instrument-openui = Play Music
- 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:
snap:
- Wall
components:
- - type: Tag
- tags:
- - RCDDeconstructWhitelist
+ - type: RCDDeconstructable
+ cost: 2
+ delay: 2
+ fx: EffectRCDDeconstruct2
- type: Clickable
- type: InteractionOutline
- type: Sprite
- 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
color: '#3F38'
- type: ConstructionGhost
- type: Clickable
- - type: InteractionOutline
+ - type: InteractionOutline
\ No newline at end of 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
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
- type: Damageable
damageContainer: Inorganic
damageModifierSet: Metallic
+ - type: RCDDeconstructable
+ cost: 6
+ delay: 8
+ fx: EffectRCDDeconstruct8
- type: Destructible
thresholds:
- trigger:
- 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
- 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.
- type: entity
- parent: Airlock
+ parent: AirlockRCDResistant
id: AirlockShuttle
suffix: Docking
name: external airlock
- type: Damageable
damageContainer: Inorganic
damageModifierSet: Metallic
+ - type: RCDDeconstructable
+ cost: 4
+ delay: 6
+ fx: EffectRCDDeconstruct6
- type: Destructible
thresholds:
- trigger:
- type: Damageable
damageContainer: Inorganic
damageModifierSet: Metallic
+ - type: RCDDeconstructable
+ cost: 4
+ delay: 6
+ fx: EffectRCDDeconstruct6
- type: Destructible
thresholds:
- trigger:
- type: Damageable
damageContainer: Inorganic
damageModifierSet: Metallic
+ - type: RCDDeconstructable
+ cost: 6
+ delay: 6
+ fx: EffectRCDDeconstruct6
- type: Destructible
thresholds:
- trigger:
- 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:
- type: Damageable
damageContainer: Inorganic
damageModifierSet: Metallic
+ - type: RCDDeconstructable
+ cost: 6
+ delay: 8
+ fx: EffectRCDDeconstruct8
- type: Destructible
thresholds:
- trigger:
damageContainer: Inorganic
damageModifierSet: Glass
- type: ExaminableDamage
- messages: WindowMessages
+ messages: WindowMessages
+ - type: RCDDeconstructable
+ cost: 8
+ delay: 8
+ fx: EffectRCDDeconstruct8
- type: Destructible
thresholds:
- trigger:
- type: Damageable
damageContainer: Inorganic
damageModifierSet: Metallic
+ - type: RCDDeconstructable
+ cost: 4
+ delay: 2
+ fx: EffectRCDDeconstruct2
- type: Destructible
thresholds:
- trigger:
mode: SnapgridCenter
snap:
- Wallmount
-
+
- type: entity
name: light
description: "A light fixture. Draws power and produces light when equipped with a light tube."
- type: Damageable
damageContainer: Inorganic
damageModifierSet: Metallic
+ - type: RCDDeconstructable
+ cost: 2
+ delay: 2
+ fx: EffectRCDDeconstruct2
- type: Destructible
thresholds:
- trigger:
lowVoltageNode: power
- type: CableVis
node: power
+ - type: RCDDeconstructable
+ cost: 2
+ delay: 2
+ fx: EffectRCDDeconstruct2
- type: entity
parent: CableBase
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
"/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:
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
- 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
- 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:
- 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:
- type: entity
id: MiningWindow
name: mining window
- parent: Window
+ parent: WindowRCDResistant
components:
- type: Sprite
drawdepth: WallTops
- type: entity
id: PlasmaWindow
name: plasma window
- parent: Window
+ parent: WindowRCDResistant
components:
- type: Sprite
drawdepth: WallTops
- type: entity
id: PlasmaWindowDirectional
- parent: WindowDirectional
+ parent: WindowDirectionalRCDResistant
name: directional plasma window
description: Don't smudge up the glass down there.
placement:
- 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:
- type: entity
id: ReinforcedPlasmaWindow
name: reinforced plasma window
- parent: Window
+ parent: WindowRCDResistant
components:
- type: Sprite
drawdepth: WallTops
- type: entity
id: PlasmaReinforcedWindowDirectional
- parent: WindowDirectional
+ parent: WindowDirectionalRCDResistant
name: directional reinforced plasma window
description: Don't smudge up the glass down there.
placement:
- type: entity
id: ReinforcedUraniumWindow
name: reinforced uranium window
- parent: Window
+ parent: WindowRCDResistant
components:
- type: Sprite
drawdepth: WallTops
- type: entity
id: ShuttleWindow
name: shuttle window
- parent: Window
+ parent: WindowRCDResistant
components:
- type: Sprite
drawdepth: WallTops
- type: entity
id: UraniumWindow
name: uranium window
- parent: Window
+ parent: WindowRCDResistant
components:
- type: Sprite
drawdepth: WallTops
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
max: 1
- !type:DoActsBehavior
acts: [ "Destruction" ]
+ - type: RCDDeconstructable
+ cost: 2
+ delay: 2
+ fx: EffectRCDDeconstruct2
\ No newline at end of file
--- /dev/null
+# 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
- 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
"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
+ ]
+ ]
}
]
}