Improved RCDs (#22799)

* Initial radial menu prototyping for the RCD

* Radial UI buttons can send messages to the server

* Beginning to update RCDSystem

* RCD building system in progress

* Further updates

* Added extra effects, RCDSystem now reads RCD prototype data

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

* Added extra functionality to RadialContainers plus documentation

* Fixed localization of RCD UI strings

* Menu opens near cursor, added basic RCD

* Avoiding merge conflict

* Implemented atomized construction / deconstruction rules

* Increased RCD ammo base charges

* Moved input context definition to content

* Removed obsoleted code

* Updates to system

* Switch machine and computer frames for electrical cabling

* Added construction ghosts

* Fixed issue with keybind detection code

* Fixed RCD construction ghost mispredications

* Code clean up

* Updated deconstruction effects

* RCDs effects don't rotate

* Code clean up

* Balancing for ammo counts

* Code clean up

* Added missing localized strings

* More clean up

* Made directional window handling more robust

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

* Made radial containers more robust

* Further robustness to the radial menu

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

* Expanded upon deconstructing plus construction balance

* Fixed line endings

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

* Bug fixes

* Revert unnecessary change

* Updated RCD strings

* Fixed bug

* More fixes

* Deconstructed tiles/subflooring convert to lattice instead

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

* Fixing merge conflict

* Updated airlock assembly

* Fixing merge conflict

* Fixing merge conflict

* More fixing...

* Removed erroneous project file change

* Fixed string handling issue

* Trying to fix merge conflict

* Still fixing merge conflicts

* Balancing

* Hidden RCD construction ghosts when in 'build' mode

* Fixing merge conflict

* Implemented requested changes (Part 1)

* Added more requested changes

* Fix for failed test. Removed sussy null suppression

* Made requested changes - custom construction ghost system was replaced

* Fixing merge conflict

* Fixed merge conflict

* Fixed bug in RCD construction ghost validation

* Fixing merge conflict

* Merge conflict fixed

* Made required update

* Removed lingering RCD deconstruct tag

* Fixing merge conflict

* Merge conflict fixed

* Made requested changes

* Bug fixes and balancing

* Made string names more consistent

* Can no longer stack catwalks
This commit is contained in:
chromiumboy
2024-03-30 23:29:47 -05:00
committed by GitHub
parent 4d2aa1a70a
commit 02273ca0e7
100 changed files with 2765 additions and 380 deletions

View File

@@ -45,6 +45,9 @@ namespace Content.Client.Input
// Not in engine because the engine doesn't understand what a flipped object is // Not in engine because the engine doesn't understand what a flipped object is
common.AddFunction(ContentKeyFunctions.EditorFlipObject); common.AddFunction(ContentKeyFunctions.EditorFlipObject);
// Not in engine so that the RCD can rotate objects
common.AddFunction(EngineKeyFunctions.EditorRotateObject);
var human = contexts.GetContext("human"); var human = contexts.GetContext("human");
human.AddFunction(EngineKeyFunctions.MoveUp); human.AddFunction(EngineKeyFunctions.MoveUp);
human.AddFunction(EngineKeyFunctions.MoveDown); human.AddFunction(EngineKeyFunctions.MoveDown);

View File

@@ -0,0 +1,122 @@
using System.Numerics;
using Content.Client.Gameplay;
using Content.Shared.Hands.Components;
using Content.Shared.Interaction;
using Content.Shared.RCD.Components;
using Content.Shared.RCD.Systems;
using Robust.Client.Placement;
using Robust.Client.Player;
using Robust.Client.State;
using Robust.Shared.Map;
using Robust.Shared.Map.Components;
namespace Content.Client.RCD;
public sealed class AlignRCDConstruction : PlacementMode
{
[Dependency] private readonly IEntityManager _entityManager = default!;
[Dependency] private readonly IMapManager _mapManager = default!;
[Dependency] private readonly SharedMapSystem _mapSystem = default!;
[Dependency] private readonly RCDSystem _rcdSystem = default!;
[Dependency] private readonly SharedTransformSystem _transformSystem = default!;
[Dependency] private readonly IPlayerManager _playerManager = default!;
[Dependency] private readonly IStateManager _stateManager = default!;
private const float SearchBoxSize = 2f;
private const float PlaceColorBaseAlpha = 0.5f;
private EntityCoordinates _unalignedMouseCoords = default;
/// <summary>
/// This placement mode is not on the engine because it is content specific (i.e., for the RCD)
/// </summary>
public AlignRCDConstruction(PlacementManager pMan) : base(pMan)
{
var dependencies = IoCManager.Instance!;
_entityManager = dependencies.Resolve<IEntityManager>();
_mapManager = dependencies.Resolve<IMapManager>();
_playerManager = dependencies.Resolve<IPlayerManager>();
_stateManager = dependencies.Resolve<IStateManager>();
_mapSystem = _entityManager.System<SharedMapSystem>();
_rcdSystem = _entityManager.System<RCDSystem>();
_transformSystem = _entityManager.System<SharedTransformSystem>();
ValidPlaceColor = ValidPlaceColor.WithAlpha(PlaceColorBaseAlpha);
}
public override void AlignPlacementMode(ScreenCoordinates mouseScreen)
{
_unalignedMouseCoords = ScreenToCursorGrid(mouseScreen);
MouseCoords = _unalignedMouseCoords.AlignWithClosestGridTile(SearchBoxSize, _entityManager, _mapManager);
var gridId = MouseCoords.GetGridUid(_entityManager);
if (!_entityManager.TryGetComponent<MapGridComponent>(gridId, out var mapGrid))
return;
CurrentTile = _mapSystem.GetTileRef(gridId.Value, mapGrid, MouseCoords);
float tileSize = mapGrid.TileSize;
GridDistancing = tileSize;
if (pManager.CurrentPermission!.IsTile)
{
MouseCoords = new EntityCoordinates(MouseCoords.EntityId, new Vector2(CurrentTile.X + tileSize / 2,
CurrentTile.Y + tileSize / 2));
}
else
{
MouseCoords = new EntityCoordinates(MouseCoords.EntityId, new Vector2(CurrentTile.X + tileSize / 2 + pManager.PlacementOffset.X,
CurrentTile.Y + tileSize / 2 + pManager.PlacementOffset.Y));
}
}
public override bool IsValidPosition(EntityCoordinates position)
{
var player = _playerManager.LocalSession?.AttachedEntity;
// If the destination is out of interaction range, set the placer alpha to zero
if (!_entityManager.TryGetComponent<TransformComponent>(player, out var xform))
return false;
if (!xform.Coordinates.InRange(_entityManager, _transformSystem, position, SharedInteractionSystem.InteractionRange))
{
InvalidPlaceColor = InvalidPlaceColor.WithAlpha(0);
return false;
}
// Otherwise restore the alpha value
else
{
InvalidPlaceColor = InvalidPlaceColor.WithAlpha(PlaceColorBaseAlpha);
}
// Determine if player is carrying an RCD in their active hand
if (!_entityManager.TryGetComponent<HandsComponent>(player, out var hands))
return false;
var heldEntity = hands.ActiveHand?.HeldEntity;
if (!_entityManager.TryGetComponent<RCDComponent>(heldEntity, out var rcd))
return false;
// Retrieve the map grid data for the position
if (!_rcdSystem.TryGetMapGridData(position, out var mapGridData))
return false;
// Determine if the user is hovering over a target
var currentState = _stateManager.CurrentState;
if (currentState is not GameplayStateBase screen)
return false;
var target = screen.GetClickedEntity(_unalignedMouseCoords.ToMap(_entityManager, _transformSystem));
// Determine if the RCD operation is valid or not
if (!_rcdSystem.IsRCDOperationStillValid(heldEntity.Value, rcd, mapGridData.Value, target, player.Value, false))
return false;
return true;
}
}

View File

@@ -0,0 +1,78 @@
using Content.Shared.Hands.Components;
using Content.Shared.Interaction;
using Content.Shared.RCD;
using Content.Shared.RCD.Components;
using Content.Shared.RCD.Systems;
using Robust.Client.Placement;
using Robust.Client.Player;
using Robust.Shared.Enums;
namespace Content.Client.RCD;
public sealed class RCDConstructionGhostSystem : EntitySystem
{
[Dependency] private readonly IPlayerManager _playerManager = default!;
[Dependency] private readonly RCDSystem _rcdSystem = default!;
[Dependency] private readonly IPlacementManager _placementManager = default!;
private string _placementMode = typeof(AlignRCDConstruction).Name;
private Direction _placementDirection = default;
public override void Update(float frameTime)
{
base.Update(frameTime);
// Get current placer data
var placerEntity = _placementManager.CurrentPermission?.MobUid;
var placerProto = _placementManager.CurrentPermission?.EntityType;
var placerIsRCD = HasComp<RCDComponent>(placerEntity);
// Exit if erasing or the current placer is not an RCD (build mode is active)
if (_placementManager.Eraser || (placerEntity != null && !placerIsRCD))
return;
// Determine if player is carrying an RCD in their active hand
var player = _playerManager.LocalSession?.AttachedEntity;
if (!TryComp<HandsComponent>(player, out var hands))
return;
var heldEntity = hands.ActiveHand?.HeldEntity;
if (!TryComp<RCDComponent>(heldEntity, out var rcd))
{
// If the player was holding an RCD, but is no longer, cancel placement
if (placerIsRCD)
_placementManager.Clear();
return;
}
// Update the direction the RCD prototype based on the placer direction
if (_placementDirection != _placementManager.Direction)
{
_placementDirection = _placementManager.Direction;
RaiseNetworkEvent(new RCDConstructionGhostRotationEvent(GetNetEntity(heldEntity.Value), _placementDirection));
}
// If the placer has not changed, exit
_rcdSystem.UpdateCachedPrototype(heldEntity.Value, rcd);
if (heldEntity == placerEntity && rcd.CachedPrototype.Prototype == placerProto)
return;
// Create a new placer
var newObjInfo = new PlacementInformation
{
MobUid = heldEntity.Value,
PlacementOption = _placementMode,
EntityType = rcd.CachedPrototype.Prototype,
Range = (int) Math.Ceiling(SharedInteractionSystem.InteractionRange),
IsTile = (rcd.CachedPrototype.Mode == RcdMode.ConstructTile),
UseEditorContext = false,
};
_placementManager.Clear();
_placementManager.BeginPlacing(newObjInfo);
}
}

View File

@@ -0,0 +1,47 @@
<ui:RadialMenu xmlns="https://spacestation14.io"
xmlns:ui="clr-namespace:Content.Client.UserInterface.Controls"
xmlns:rcd="clr-namespace:Content.Client.RCD"
BackButtonStyleClass="RadialMenuBackButton"
CloseButtonStyleClass="RadialMenuCloseButton"
VerticalExpand="True"
HorizontalExpand="True"
MinSize="450 450">
<!-- Note: The min size of the window just determine how close to the edge of the screen the center of the radial menu can be placed -->
<!-- The radial menu will try to open so that its center is located where the player's cursor is currently -->
<!-- Entry layer (shows main categories) -->
<ui:RadialContainer Name="Main" VerticalExpand="True" HorizontalExpand="True" Radius="64" ReserveSpaceForHiddenChildren="False">
<ui:RadialMenuTextureButton StyleClasses="RadialMenuButton" SetSize="64 64" ToolTip="{Loc 'rcd-component-walls-and-flooring'}" TargetLayer="WallsAndFlooring" Visible="False">
<TextureRect VerticalAlignment="Center" HorizontalAlignment="Center" TextureScale="2 2" TexturePath="/Textures/Interface/Radial/RCD/walls_and_flooring.png"/>
</ui:RadialMenuTextureButton>
<ui:RadialMenuTextureButton StyleClasses="RadialMenuButton" SetSize="64 64" ToolTip="{Loc 'rcd-component-windows-and-grilles'}" TargetLayer="WindowsAndGrilles" Visible="False">
<TextureRect VerticalAlignment="Center" HorizontalAlignment="Center" TextureScale="2 2" TexturePath="/Textures/Interface/Radial/RCD/windows_and_grilles.png"/>
</ui:RadialMenuTextureButton>
<ui:RadialMenuTextureButton StyleClasses="RadialMenuButton" SetSize="64 64" ToolTip="{Loc 'rcd-component-airlocks'}" TargetLayer="Airlocks" Visible="False">
<TextureRect VerticalAlignment="Center" HorizontalAlignment="Center" TextureScale="2 2" TexturePath="/Textures/Interface/Radial/RCD/airlocks.png"/>
</ui:RadialMenuTextureButton>
<ui:RadialMenuTextureButton StyleClasses="RadialMenuButton" SetSize="64 64" ToolTip="{Loc 'rcd-component-electrical'}" TargetLayer="Electrical" Visible="False">
<TextureRect VerticalAlignment="Center" HorizontalAlignment="Center" TextureScale="2 2" TexturePath="/Textures/Interface/Radial/RCD/multicoil.png"/>
</ui:RadialMenuTextureButton>
<ui:RadialMenuTextureButton StyleClasses="RadialMenuButton" SetSize="64 64" ToolTip="{Loc 'rcd-component-lighting'}" TargetLayer="Lighting" Visible="False">
<TextureRect VerticalAlignment="Center" HorizontalAlignment="Center" TextureScale="2 2" TexturePath="/Textures/Interface/Radial/RCD/lighting.png"/>
</ui:RadialMenuTextureButton>
</ui:RadialContainer>
<!-- Walls and flooring -->
<ui:RadialContainer Name="WallsAndFlooring" VerticalExpand="True" HorizontalExpand="True" Radius="64"/>
<!-- Windows and grilles -->
<ui:RadialContainer Name="WindowsAndGrilles" VerticalExpand="True" HorizontalExpand="True" Radius="64"/>
<!-- Airlocks -->
<ui:RadialContainer Name="Airlocks" VerticalExpand="True" HorizontalExpand="True" Radius="64"/>
<!-- Computer and machine frames -->
<ui:RadialContainer Name="Electrical" VerticalExpand="True" HorizontalExpand="True" Radius="64"/>
<!-- Lighting -->
<ui:RadialContainer Name="Lighting" VerticalExpand="True" HorizontalExpand="True" Radius="64"/>
</ui:RadialMenu>

View File

@@ -0,0 +1,137 @@
using Content.Client.UserInterface.Controls;
using Content.Shared.RCD;
using Content.Shared.RCD.Components;
using Robust.Client.AutoGenerated;
using Robust.Client.GameObjects;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Prototypes;
using System.Numerics;
namespace Content.Client.RCD;
[GenerateTypedNameReferences]
public sealed partial class RCDMenu : RadialMenu
{
[Dependency] private readonly EntityManager _entManager = default!;
[Dependency] private readonly IPrototypeManager _protoManager = default!;
private readonly SpriteSystem _spriteSystem;
public event Action<ProtoId<RCDPrototype>>? SendRCDSystemMessageAction;
public RCDMenu(EntityUid owner, RCDMenuBoundUserInterface bui)
{
IoCManager.InjectDependencies(this);
RobustXamlLoader.Load(this);
_spriteSystem = _entManager.System<SpriteSystem>();
// Find the main radial container
var main = FindControl<RadialContainer>("Main");
if (main == null)
return;
// Populate secondary radial containers
if (!_entManager.TryGetComponent<RCDComponent>(owner, out var rcd))
return;
foreach (var protoId in rcd.AvailablePrototypes)
{
if (!_protoManager.TryIndex(protoId, out var proto))
continue;
if (proto.Mode == RcdMode.Invalid)
continue;
var parent = FindControl<RadialContainer>(proto.Category);
if (parent == null)
continue;
var name = Loc.GetString(proto.SetName);
name = char.ToUpper(name[0]) + name.Remove(0, 1);
var button = new RCDMenuButton()
{
StyleClasses = { "RadialMenuButton" },
SetSize = new Vector2(64f, 64f),
ToolTip = name,
ProtoId = protoId,
};
if (proto.Sprite != null)
{
var tex = new TextureRect()
{
VerticalAlignment = VAlignment.Center,
HorizontalAlignment = HAlignment.Center,
Texture = _spriteSystem.Frame0(proto.Sprite),
TextureScale = new Vector2(2f, 2f),
};
button.AddChild(tex);
}
parent.AddChild(button);
// Ensure that the button that transitions the menu to the associated category layer
// is visible in the main radial container (as these all start with Visible = false)
foreach (var child in main.Children)
{
var castChild = child as RadialMenuTextureButton;
if (castChild is not RadialMenuTextureButton)
continue;
if (castChild.TargetLayer == proto.Category)
{
castChild.Visible = true;
break;
}
}
}
// Set up menu actions
foreach (var child in Children)
AddRCDMenuButtonOnClickActions(child);
OnChildAdded += AddRCDMenuButtonOnClickActions;
SendRCDSystemMessageAction += bui.SendRCDSystemMessage;
}
private void AddRCDMenuButtonOnClickActions(Control control)
{
var radialContainer = control as RadialContainer;
if (radialContainer == null)
return;
foreach (var child in radialContainer.Children)
{
var castChild = child as RCDMenuButton;
if (castChild == null)
continue;
castChild.OnButtonUp += _ =>
{
SendRCDSystemMessageAction?.Invoke(castChild.ProtoId);
Close();
};
}
}
}
public sealed class RCDMenuButton : RadialMenuTextureButton
{
public ProtoId<RCDPrototype> ProtoId { get; set; }
public RCDMenuButton()
{
}
}

View File

@@ -0,0 +1,49 @@
using Content.Shared.RCD;
using Content.Shared.RCD.Components;
using JetBrains.Annotations;
using Robust.Client.Graphics;
using Robust.Client.Input;
using Robust.Shared.Prototypes;
namespace Content.Client.RCD;
[UsedImplicitly]
public sealed class RCDMenuBoundUserInterface : BoundUserInterface
{
[Dependency] private readonly IClyde _displayManager = default!;
[Dependency] private readonly IInputManager _inputManager = default!;
private RCDMenu? _menu;
public RCDMenuBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey)
{
IoCManager.InjectDependencies(this);
}
protected override void Open()
{
base.Open();
_menu = new(Owner, this);
_menu.OnClose += Close;
// Open the menu, centered on the mouse
var vpSize = _displayManager.ScreenSize;
_menu.OpenCenteredAt(_inputManager.MouseScreenPosition.Position / vpSize);
}
public void SendRCDSystemMessage(ProtoId<RCDPrototype> protoId)
{
// A predicted message cannot be used here as the RCD UI is closed immediately
// after this message is sent, which will stop the server from receiving it
SendMessage(new RCDSystemMessage(protoId));
}
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
if (!disposing) return;
_menu?.Dispose();
}
}

View File

@@ -368,9 +368,9 @@ namespace Content.Client.Stylesheets
}; };
tabContainerPanel.SetPatchMargin(StyleBox.Margin.All, 2); 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); 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); tabContainerBoxInactive.SetContentMarginOverride(StyleBox.Margin.Horizontal, 5);
var progressBarBackground = new StyleBoxFlat var progressBarBackground = new StyleBoxFlat
@@ -409,21 +409,21 @@ namespace Content.Client.Stylesheets
// Placeholder // Placeholder
var placeholderTexture = resCache.GetTexture("/Textures/Interface/Nano/placeholder.png"); 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.SetPatchMargin(StyleBox.Margin.All, 19);
placeholder.SetExpandMargin(StyleBox.Margin.All, -5); placeholder.SetExpandMargin(StyleBox.Margin.All, -5);
placeholder.Mode = StyleBoxTexture.StretchMode.Tile; 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.Vertical, 2);
itemListBackgroundSelected.SetContentMarginOverride(StyleBox.Margin.Horizontal, 4); 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.Vertical, 2);
itemListItemBackgroundDisabled.SetContentMarginOverride(StyleBox.Margin.Horizontal, 4); 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.Vertical, 2);
itemListItemBackground.SetContentMarginOverride(StyleBox.Margin.Horizontal, 4); 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.Vertical, 2);
itemListItemBackgroundTransparent.SetContentMarginOverride(StyleBox.Margin.Horizontal, 4); itemListItemBackgroundTransparent.SetContentMarginOverride(StyleBox.Margin.Horizontal, 4);
@@ -489,9 +489,9 @@ namespace Content.Client.Stylesheets
sliderForeBox.SetPatchMargin(StyleBox.Margin.All, 12); sliderForeBox.SetPatchMargin(StyleBox.Margin.All, 12);
sliderGrabBox.SetPatchMargin(StyleBox.Margin.All, 12); sliderGrabBox.SetPatchMargin(StyleBox.Margin.All, 12);
var sliderFillGreen = new StyleBoxTexture(sliderFillBox) {Modulate = Color.LimeGreen}; var sliderFillGreen = new StyleBoxTexture(sliderFillBox) { Modulate = Color.LimeGreen };
var sliderFillRed = new StyleBoxTexture(sliderFillBox) {Modulate = Color.Red}; var sliderFillRed = new StyleBoxTexture(sliderFillBox) { Modulate = Color.Red };
var sliderFillBlue = new StyleBoxTexture(sliderFillBox) {Modulate = Color.Blue}; var sliderFillBlue = new StyleBoxTexture(sliderFillBox) { Modulate = Color.Blue };
var sliderFillWhite = new StyleBoxTexture(sliderFillBox) { Modulate = Color.White }; var sliderFillWhite = new StyleBoxTexture(sliderFillBox) { Modulate = Color.White };
var boxFont13 = resCache.GetFont("/Fonts/Boxfont-round/Boxfont Round.ttf", 13); var boxFont13 = resCache.GetFont("/Fonts/Boxfont-round/Boxfont Round.ttf", 13);
@@ -1468,6 +1468,25 @@ namespace Content.Client.Stylesheets
Element<Label>().Class("Disabled") Element<Label>().Class("Disabled")
.Prop("font-color", DisabledFore), .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 //PDA - Backgrounds
Element<PanelContainer>().Class("PdaContentBackground") Element<PanelContainer>().Class("PdaContentBackground")
.Prop(PanelContainer.StylePropertyPanel, BaseButtonOpenBoth) .Prop(PanelContainer.StylePropertyPanel, BaseButtonOpenBoth)

View File

@@ -0,0 +1,105 @@
using Robust.Client.Graphics;
using Robust.Client.UserInterface.Controls;
using System.Linq;
using System.Numerics;
namespace Content.Client.UserInterface.Controls;
[Virtual]
public class RadialContainer : LayoutContainer
{
/// <summary>
/// Specifies the anglular range, in radians, in which child elements will be placed.
/// The first value denotes the angle at which the first element is to be placed, and
/// the second value denotes the angle at which the last element is to be placed.
/// Both values must be between 0 and 2 PI radians
/// </summary>
/// <remarks>
/// The top of the screen is at 0 radians, and the bottom of the screen is at PI radians
/// </remarks>
[ViewVariables(VVAccess.ReadWrite)]
public Vector2 AngularRange
{
get
{
return _angularRange;
}
set
{
var x = value.X;
var y = value.Y;
x = x > MathF.Tau ? x % MathF.Tau : x;
y = y > MathF.Tau ? y % MathF.Tau : y;
x = x < 0 ? MathF.Tau + x : x;
y = y < 0 ? MathF.Tau + y : y;
_angularRange = new Vector2(x, y);
}
}
private Vector2 _angularRange = new Vector2(0f, MathF.Tau - float.Epsilon);
/// <summary>
/// Determines the direction in which child elements will be arranged
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
public RAlignment RadialAlignment { get; set; } = RAlignment.Clockwise;
/// <summary>
/// Determines how far from the radial container's center that its child elements will be placed
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
public float Radius { get; set; } = 100f;
/// <summary>
/// Sets whether the container should reserve a space on the layout for child which are not currently visible
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
public bool ReserveSpaceForHiddenChildren { get; set; } = true;
/// <summary>
/// This container arranges its children, evenly separated, in a radial pattern
/// </summary>
public RadialContainer()
{
}
protected override void Draw(DrawingHandleScreen handle)
{
var children = ReserveSpaceForHiddenChildren ? Children : Children.Where(x => x.Visible);
var childCount = children.Count();
// Determine the size of the arc, accounting for clockwise and anti-clockwise arrangements
var arc = AngularRange.Y - AngularRange.X;
arc = (arc < 0) ? MathF.Tau + arc : arc;
arc = (RadialAlignment == RAlignment.AntiClockwise) ? MathF.Tau - arc : arc;
// Account for both circular arrangements and arc-based arrangements
var childMod = MathHelper.CloseTo(arc, MathF.Tau, 0.01f) ? 0 : 1;
// Determine the separation between child elements
var sepAngle = arc / (childCount - childMod);
sepAngle *= (RadialAlignment == RAlignment.AntiClockwise) ? -1f : 1f;
// Adjust the positions of all the child elements
foreach (var (i, child) in children.Select((x, i) => (i, x)))
{
var position = new Vector2(Radius * MathF.Sin(AngularRange.X + sepAngle * i) + Width / 2f - child.Width / 2f, -Radius * MathF.Cos(AngularRange.X + sepAngle * i) + Height / 2f - child.Height / 2f);
SetPosition(child, position);
}
}
/// <summary>
/// Specifies the different radial alignment modes
/// </summary>
/// <seealso cref="RadialAlignment"/>
public enum RAlignment : byte
{
Clockwise,
AntiClockwise,
}
}

View File

@@ -0,0 +1,255 @@
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.CustomControls;
using System.Linq;
using System.Numerics;
namespace Content.Client.UserInterface.Controls;
[Virtual]
public class RadialMenu : BaseWindow
{
/// <summary>
/// Contextual button used to traverse through previous layers of the radial menu
/// </summary>
public TextureButton? ContextualButton { get; set; }
/// <summary>
/// Set a style class to be applied to the contextual button when it is set to move the user back through previous layers of the radial menu
/// </summary>
public string? BackButtonStyleClass
{
get
{
return _backButtonStyleClass;
}
set
{
_backButtonStyleClass = value;
if (_path.Count > 0 && ContextualButton != null && _backButtonStyleClass != null)
ContextualButton.SetOnlyStyleClass(_backButtonStyleClass);
}
}
/// <summary>
/// Set a style class to be applied to the contextual button when it will close the radial menu
/// </summary>
public string? CloseButtonStyleClass
{
get
{
return _closeButtonStyleClass;
}
set
{
_closeButtonStyleClass = value;
if (_path.Count == 0 && ContextualButton != null && _closeButtonStyleClass != null)
ContextualButton.SetOnlyStyleClass(_closeButtonStyleClass);
}
}
private List<Control> _path = new();
private string? _backButtonStyleClass;
private string? _closeButtonStyleClass;
/// <summary>
/// A free floating menu which enables the quick display of one or more radial containers
/// </summary>
/// <remarks>
/// Only one radial container is visible at a time (each container forming a separate 'layer' within
/// the menu), along with a contextual button at the menu center, which will either return the user
/// to the previous layer or close the menu if there are no previous layers left to traverse.
/// To create a functional radial menu, simply parent one or more named radial containers to it,
/// and populate the radial containers with RadialMenuButtons. Setting the TargetLayer field of these
/// buttons to the name of a radial conatiner will display the container in question to the user
/// whenever it is clicked in additon to any other actions assigned to the button
/// </remarks>
public RadialMenu()
{
// Hide all starting children (if any) except the first (this is the active layer)
if (ChildCount > 1)
{
for (int i = 1; i < ChildCount; i++)
GetChild(i).Visible = false;
}
// Auto generate a contextual button for moving back through visited layers
ContextualButton = new TextureButton()
{
HorizontalAlignment = HAlignment.Center,
VerticalAlignment = VAlignment.Center,
SetSize = new Vector2(64f, 64f),
};
ContextualButton.OnButtonUp += _ => ReturnToPreviousLayer();
AddChild(ContextualButton);
// Hide any further add children, unless its promoted to the active layer
OnChildAdded += child => child.Visible = (GetCurrentActiveLayer() == child);
}
private Control? GetCurrentActiveLayer()
{
var children = Children.Where(x => x != ContextualButton);
if (!children.Any())
return null;
return children.First(x => x.Visible);
}
public bool TryToMoveToNewLayer(string newLayer)
{
if (newLayer == string.Empty)
return false;
var currentLayer = GetCurrentActiveLayer();
if (currentLayer == null)
return false;
var result = false;
foreach (var child in Children)
{
if (child == ContextualButton)
continue;
// Hide layers which are not of interest
if (result == true || child.Name != newLayer)
{
child.Visible = false;
}
// Show the layer of interest
else
{
child.Visible = true;
result = true;
}
}
// Update the traversal path
if (result)
_path.Add(currentLayer);
// Set the style class of the button
if (_path.Count > 0 && ContextualButton != null && BackButtonStyleClass != null)
ContextualButton.SetOnlyStyleClass(BackButtonStyleClass);
return result;
}
public void ReturnToPreviousLayer()
{
// Close the menu if the traversal path is empty
if (_path.Count == 0)
{
Close();
return;
}
var lastChild = _path[^1];
// Hide all children except the contextual button
foreach (var child in Children)
{
if (child != ContextualButton)
child.Visible = false;
}
// Make the last visited layer visible, update the path list
lastChild.Visible = true;
_path.RemoveAt(_path.Count - 1);
// Set the style class of the button
if (_path.Count == 0 && ContextualButton != null && CloseButtonStyleClass != null)
ContextualButton.SetOnlyStyleClass(CloseButtonStyleClass);
}
}
[Virtual]
public class RadialMenuButton : Button
{
/// <summary>
/// Upon clicking this button the radial menu will transition to the named layer
/// </summary>
public string? TargetLayer { get; set; }
/// <summary>
/// A simple button that can move the user to a different layer within a radial menu
/// </summary>
public RadialMenuButton()
{
OnButtonUp += OnClicked;
}
private void OnClicked(ButtonEventArgs args)
{
if (TargetLayer == null || TargetLayer == string.Empty)
return;
var parent = FindParentMultiLayerContainer(this);
if (parent == null)
return;
parent.TryToMoveToNewLayer(TargetLayer);
}
private RadialMenu? FindParentMultiLayerContainer(Control control)
{
foreach (var ancestor in control.GetSelfAndLogicalAncestors())
{
if (ancestor is RadialMenu)
return ancestor as RadialMenu;
}
return null;
}
}
[Virtual]
public class RadialMenuTextureButton : TextureButton
{
/// <summary>
/// Upon clicking this button the radial menu will be moved to the named layer
/// </summary>
public string TargetLayer { get; set; } = string.Empty;
/// <summary>
/// A simple texture button that can move the user to a different layer within a radial menu
/// </summary>
public RadialMenuTextureButton()
{
OnButtonUp += OnClicked;
}
private void OnClicked(ButtonEventArgs args)
{
if (TargetLayer == string.Empty)
return;
var parent = FindParentMultiLayerContainer(this);
if (parent == null)
return;
parent.TryToMoveToNewLayer(TargetLayer);
}
private RadialMenu? FindParentMultiLayerContainer(Control control)
{
foreach (var ancestor in control.GetSelfAndLogicalAncestors())
{
if (ancestor is RadialMenu)
return ancestor as RadialMenu;
}
return null;
}
}

View File

@@ -61,4 +61,26 @@ public abstract class SharedChargesSystem : EntitySystem
if (Resolve(uid, ref comp, false)) if (Resolve(uid, ref comp, false))
AddCharges(uid, -1, comp); 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);
}
} }

View File

@@ -1,4 +1,4 @@
using Robust.Shared.Prototypes; using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
namespace Content.Shared.Construction.Components namespace Content.Shared.Construction.Components
@@ -9,7 +9,7 @@ namespace Content.Shared.Construction.Components
[RegisterComponent] [RegisterComponent]
public sealed partial class ComputerBoardComponent : Component public sealed partial class ComputerBoardComponent : Component
{ {
[DataField("prototype", customTypeSerializer:typeof(PrototypeIdSerializer<EntityPrototype>))] [DataField("prototype", customTypeSerializer: typeof(PrototypeIdSerializer<EntityPrototype>))]
public string? Prototype { get; private set; } public string? Prototype { get; private set; }
} }
} }

View File

@@ -12,7 +12,5 @@ public sealed partial class RCDAmmoComponent : Component
/// Can be partially transferred into an RCD, until it is empty then it gets deleted. /// Can be partially transferred into an RCD, until it is empty then it gets deleted.
/// </summary> /// </summary>
[DataField("charges"), ViewVariables(VVAccess.ReadWrite), AutoNetworkedField] [DataField("charges"), ViewVariables(VVAccess.ReadWrite), AutoNetworkedField]
public int Charges = 5; public int Charges = 30;
} }
// TODO: state??? check if it desyncs

View File

@@ -1,20 +1,11 @@
using Content.Shared.Maps;
using Content.Shared.RCD.Systems; using Content.Shared.RCD.Systems;
using Robust.Shared.Audio; using Robust.Shared.Audio;
using Robust.Shared.GameStates; using Robust.Shared.GameStates;
using Robust.Shared.Serialization; using Robust.Shared.Physics;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; using Robust.Shared.Prototypes;
namespace Content.Shared.RCD.Components; namespace Content.Shared.RCD.Components;
public enum RcdMode : byte
{
Floors,
Walls,
Airlock,
Deconstruct
}
/// <summary> /// <summary>
/// Main component for the RCD /// Main component for the RCD
/// Optionally uses LimitedChargesComponent. /// Optionally uses LimitedChargesComponent.
@@ -25,27 +16,57 @@ public enum RcdMode : byte
public sealed partial class RCDComponent : Component public sealed partial class RCDComponent : Component
{ {
/// <summary> /// <summary>
/// Time taken to do an action like placing a wall /// List of RCD prototypes that the device comes loaded with
/// </summary> /// </summary>
[DataField("delay"), ViewVariables(VVAccess.ReadWrite), AutoNetworkedField] [DataField, AutoNetworkedField]
public float Delay = 2f; public HashSet<ProtoId<RCDPrototype>> AvailablePrototypes { get; set; } = new();
[DataField("swapModeSound")]
public SoundSpecifier SwapModeSound = new SoundPathSpecifier("/Audio/Items/genhit.ogg");
[DataField("successSound")]
public SoundSpecifier SuccessSound = new SoundPathSpecifier("/Audio/Items/deconstruct.ogg");
/// <summary> /// <summary>
/// What mode are we on? Can be floors, walls, airlock, deconstruct. /// Sound that plays when a RCD operation successfully completes
/// </summary> /// </summary>
[DataField("mode"), AutoNetworkedField] [DataField]
public RcdMode Mode = RcdMode.Floors; public SoundSpecifier SuccessSound { get; set; } = new SoundPathSpecifier("/Audio/Items/deconstruct.ogg");
/// <summary> /// <summary>
/// ID of the floor to create when using the floor mode. /// The ProtoId of the currently selected RCD prototype
/// </summary> /// </summary>
[DataField("floor", customTypeSerializer: typeof(PrototypeIdSerializer<ContentTileDefinition>))] [DataField, AutoNetworkedField]
[ViewVariables(VVAccess.ReadWrite), AutoNetworkedField] public ProtoId<RCDPrototype> ProtoId { get; set; } = "Invalid";
public string Floor = "FloorSteel";
/// <summary>
/// A cached copy of currently selected RCD prototype
/// </summary>
/// <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>
/// Returns a rotated transform based on the specified ConstructionDirection
/// </summary>
/// <remarks>
/// Contains no position data
/// </remarks>
[ViewVariables(VVAccess.ReadOnly)]
public Transform ConstructionTransform { get; private set; } = default!;
} }

View File

@@ -0,0 +1,34 @@
using Content.Shared.RCD.Systems;
using Robust.Shared.GameStates;
using Robust.Shared.Prototypes;
namespace Content.Shared.RCD.Components;
[RegisterComponent, NetworkedComponent]
[Access(typeof(RCDSystem))]
public sealed partial class RCDDeconstructableComponent : Component
{
/// <summary>
/// Number of charges consumed when the deconstruction is completed
/// </summary>
[DataField, ViewVariables(VVAccess.ReadWrite)]
public int Cost = 1;
/// <summary>
/// The length of the deconstruction
/// </summary>
[DataField, ViewVariables(VVAccess.ReadWrite)]
public float Delay = 1f;
/// <summary>
/// The visual effect that plays during deconstruction
/// </summary>
[DataField("fx"), ViewVariables(VVAccess.ReadWrite)]
public EntProtoId? Effect = null;
/// <summary>
/// Toggles whether this entity is deconstructable or not
/// </summary>
[DataField, ViewVariables(VVAccess.ReadWrite)]
public bool Deconstructable = true;
}

View File

@@ -0,0 +1,34 @@
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization;
namespace Content.Shared.RCD;
[Serializable, NetSerializable]
public sealed class RCDSystemMessage : BoundUserInterfaceMessage
{
public ProtoId<RCDPrototype> ProtoId;
public RCDSystemMessage(ProtoId<RCDPrototype> protoId)
{
ProtoId = protoId;
}
}
[Serializable, NetSerializable]
public sealed class RCDConstructionGhostRotationEvent : EntityEventArgs
{
public readonly NetEntity NetEntity;
public readonly Direction Direction;
public RCDConstructionGhostRotationEvent(NetEntity netEntity, Direction direction)
{
NetEntity = netEntity;
Direction = direction;
}
}
[Serializable, NetSerializable]
public enum RcdUiKey : byte
{
Key
}

View File

@@ -0,0 +1,144 @@
using Content.Shared.Physics;
using Robust.Shared.Physics.Collision.Shapes;
using Robust.Shared.Prototypes;
using Robust.Shared.Utility;
namespace Content.Shared.RCD;
/// <summary>
/// Contains the parameters for a RCD construction / operation
/// </summary>
[Prototype("rcd")]
public sealed class RCDPrototype : IPrototype
{
[IdDataField]
public string ID { get; private set; } = default!;
/// <summary>
/// The RCD mode associated with the operation
/// </summary>
[DataField(required: true), ViewVariables(VVAccess.ReadOnly)]
public RcdMode Mode { get; private set; } = RcdMode.Invalid;
/// <summary>
/// The name associated with the prototype
/// </summary>
[DataField("name"), ViewVariables(VVAccess.ReadOnly)]
public string SetName { get; private set; } = "Unknown";
/// <summary>
/// The name of the radial container that this prototype will be listed under on the RCD menu
/// </summary>
[DataField, ViewVariables(VVAccess.ReadOnly)]
public string Category { get; private set; } = "Undefined";
/// <summary>
/// Texture path for this prototypes menu icon
/// </summary>
[DataField, ViewVariables(VVAccess.ReadOnly)]
public SpriteSpecifier? Sprite { get; private set; } = null;
/// <summary>
/// The entity prototype that will be constructed (mode dependent)
/// </summary>
[DataField, ViewVariables(VVAccess.ReadOnly)]
public string? Prototype { get; private set; } = string.Empty;
/// <summary>
/// Number of charges consumed when the operation is completed
/// </summary>
[DataField, ViewVariables(VVAccess.ReadOnly)]
public int Cost { get; private set; } = 1;
/// <summary>
/// The length of the operation
/// </summary>
[DataField, ViewVariables(VVAccess.ReadOnly)]
public float Delay { get; private set; } = 1f;
/// <summary>
/// The visual effect that plays during this operation
/// </summary>
[DataField("fx"), ViewVariables(VVAccess.ReadOnly)]
public EntProtoId? Effect { get; private set; } = null;
/// <summary>
/// A list of rules that govern where the entity prototype can be contructed
/// </summary>
[DataField("rules"), ViewVariables(VVAccess.ReadOnly)]
public HashSet<RcdConstructionRule> ConstructionRules { get; private set; } = new();
/// <summary>
/// The collision mask used for determining whether the entity prototype will fit into a target tile
/// </summary>
[DataField, ViewVariables(VVAccess.ReadOnly)]
public CollisionGroup CollisionMask { get; private set; } = CollisionGroup.None;
/// <summary>
/// Specifies a set of custom collision bounds for determining whether the entity prototype will fit into a target tile
/// </summary>
/// <remarks>
/// Should be set assuming that the entity faces south.
/// Make sure that Rotation is set to RcdRotation.User if the entity is to be rotated by the user
/// </remarks>
[DataField, ViewVariables(VVAccess.ReadOnly)]
public Box2? CollisionBounds
{
get
{
return _collisionBounds;
}
private set
{
_collisionBounds = value;
if (_collisionBounds != null)
{
var poly = new PolygonShape();
poly.SetAsBox(_collisionBounds.Value);
CollisionPolygon = poly;
}
}
}
private Box2? _collisionBounds = null;
/// <summary>
/// The polygon shape associated with the prototype CollisionBounds (if set)
/// </summary>
[ViewVariables(VVAccess.ReadOnly)]
public PolygonShape? CollisionPolygon { get; private set; } = null;
/// <summary>
/// Governs how the local rotation of the constructed entity will be set
/// </summary>
[DataField, ViewVariables(VVAccess.ReadOnly)]
public RcdRotation Rotation { get; private set; } = RcdRotation.User;
}
public enum RcdMode : byte
{
Invalid,
Deconstruct,
ConstructTile,
ConstructObject,
}
// These are to be replaced with more flexible 'RulesRule' at a later time
public enum RcdConstructionRule : byte
{
MustBuildOnEmptyTile, // Can only be built on empty space (e.g. lattice)
CanBuildOnEmptyTile, // Can be built on empty space or replace an existing tile (e.g. hull plating)
MustBuildOnSubfloor, // Can only be built on exposed subfloor (e.g. catwalks on lattice or hull plating)
IsWindow, // The entity is a window and can be built on grilles
IsCatwalk, // The entity is a catwalk
}
public enum RcdRotation : byte
{
Fixed, // The entity has a local rotation of zero
Camera, // The rotation of the entity matches the local player camera
User, // The entity can be rotated by the local player prior to placement
}

View File

@@ -1,28 +1,35 @@
using Content.Shared.Administration.Logs; using Content.Shared.Administration.Logs;
using Content.Shared.Charges.Components; using Content.Shared.Charges.Components;
using Content.Shared.Charges.Systems; using Content.Shared.Charges.Systems;
using Content.Shared.Construction;
using Content.Shared.Database; using Content.Shared.Database;
using Content.Shared.DoAfter; using Content.Shared.DoAfter;
using Content.Shared.Examine; using Content.Shared.Examine;
using Content.Shared.Hands.Components;
using Content.Shared.Interaction; using Content.Shared.Interaction;
using Content.Shared.Interaction.Events;
using Content.Shared.Maps; using Content.Shared.Maps;
using Content.Shared.Physics; using Content.Shared.Physics;
using Content.Shared.Popups; using Content.Shared.Popups;
using Content.Shared.RCD.Components; using Content.Shared.RCD.Components;
using Content.Shared.Tag; using Content.Shared.Tag;
using Content.Shared.Tiles; using Content.Shared.Tiles;
using Robust.Shared.Audio;
using Robust.Shared.Audio.Systems; using Robust.Shared.Audio.Systems;
using Robust.Shared.Map; using Robust.Shared.Map;
using Robust.Shared.Map.Components; using Robust.Shared.Map.Components;
using Robust.Shared.Network; 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.Serialization;
using Robust.Shared.Timing; using Robust.Shared.Timing;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
namespace Content.Shared.RCD.Systems; namespace Content.Shared.RCD.Systems;
public sealed class RCDSystem : EntitySystem [Virtual]
public class RCDSystem : EntitySystem
{ {
[Dependency] private readonly IGameTiming _timing = default!; [Dependency] private readonly IGameTiming _timing = default!;
[Dependency] private readonly INetManager _net = default!; [Dependency] private readonly INetManager _net = default!;
@@ -34,312 +41,599 @@ public sealed class RCDSystem : EntitySystem
[Dependency] private readonly SharedDoAfterSystem _doAfter = default!; [Dependency] private readonly SharedDoAfterSystem _doAfter = default!;
[Dependency] private readonly SharedInteractionSystem _interaction = default!; [Dependency] private readonly SharedInteractionSystem _interaction = default!;
[Dependency] private readonly SharedPopupSystem _popup = default!; [Dependency] private readonly SharedPopupSystem _popup = default!;
[Dependency] private readonly TagSystem _tag = default!;
[Dependency] private readonly TurfSystem _turf = 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() public override void Initialize()
{ {
base.Initialize(); base.Initialize();
SubscribeLocalEvent<RCDComponent, MapInitEvent>(OnMapInit);
SubscribeLocalEvent<RCDComponent, ExaminedEvent>(OnExamine); SubscribeLocalEvent<RCDComponent, ExaminedEvent>(OnExamine);
SubscribeLocalEvent<RCDComponent, UseInHandEvent>(OnUseInHand);
SubscribeLocalEvent<RCDComponent, AfterInteractEvent>(OnAfterInteract); SubscribeLocalEvent<RCDComponent, AfterInteractEvent>(OnAfterInteract);
SubscribeLocalEvent<RCDComponent, RCDDoAfterEvent>(OnDoAfter); SubscribeLocalEvent<RCDComponent, RCDDoAfterEvent>(OnDoAfter);
SubscribeLocalEvent<RCDComponent, DoAfterAttemptEvent<RCDDoAfterEvent>>(OnDoAfterAttempt); 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)
{
// 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;
}
// The RCD has no valid recipes somehow? Get rid of it
QueueDel(uid);
}
private void OnRCDSystemMessage(EntityUid uid, RCDComponent component, RCDSystemMessage args)
{
// Exit if the RCD doesn't actually know the supplied prototype
if (!component.AvailablePrototypes.Contains(args.ProtoId))
return;
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 OnExamine(EntityUid uid, RCDComponent component, ExaminedEvent args)
{ {
if (!args.IsInDetailsRange) if (!args.IsInDetailsRange)
return; return;
var msg = Loc.GetString("rcd-component-examine-detail", ("mode", comp.Mode)); // 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); args.PushMarkup(msg);
} }
private void OnUseInHand(EntityUid uid, RCDComponent comp, UseInHandEvent args) private void OnAfterInteract(EntityUid uid, RCDComponent component, AfterInteractEvent args)
{
if (args.Handled)
return;
NextMode(uid, comp, args.User);
args.Handled = true;
}
private void OnAfterInteract(EntityUid uid, RCDComponent comp, AfterInteractEvent args)
{ {
if (args.Handled || !args.CanReach) if (args.Handled || !args.CanReach)
return; return;
var user = args.User; var user = args.User;
TryComp<LimitedChargesComponent>(uid, out var charges);
if (_charges.IsEmpty(uid, charges))
{
_popup.PopupClient(Loc.GetString("rcd-component-no-ammo-message"), uid, user);
return;
}
var location = args.ClickLocation; var location = args.ClickLocation;
// Initial validity check
// Initial validity checks
if (!location.IsValid(EntityManager)) if (!location.IsValid(EntityManager))
return; return;
var gridId = location.GetGridUid(EntityManager); if (!TryGetMapGridData(location, out var mapGridData))
if (!HasComp<MapGridComponent>(gridId))
{ {
location = location.AlignWithClosestGridTile(); _popup.PopupClient(Loc.GetString("rcd-component-no-valid-grid"), uid, user);
gridId = location.GetGridUid(EntityManager);
// Check if fixing it failed / get final grid ID
if (!HasComp<MapGridComponent>(gridId))
return; return;
} }
var doAfterArgs = new DoAfterArgs(EntityManager, user, comp.Delay, new RCDDoAfterEvent(GetNetCoordinates(location), comp.Mode), uid, target: args.Target, used: uid) if (!IsRCDOperationStillValid(uid, component, mapGridData.Value, args.Target, args.User))
return;
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)
{
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;
}
#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, BreakOnDamage = true,
NeedHand = true,
BreakOnHandChange = true, BreakOnHandChange = true,
BreakOnMove = true, BreakOnMove = true,
AttemptFrequency = AttemptFrequency.EveryTick AttemptFrequency = AttemptFrequency.EveryTick,
CancelDuplicate = false,
BlockDuplicate = false
}; };
args.Handled = true; args.Handled = true;
if (_doAfter.TryStartDoAfter(doAfterArgs) && _gameTiming.IsFirstTimePredicted) if (!_doAfter.TryStartDoAfter(doAfterArgs))
Spawn("EffectRCDConstruction", location); 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) if (args.Event?.DoAfter?.Args == null)
return; return;
// Exit if the RCD prototype has changed
if (component.ProtoId != args.Event.StartingProtoId)
return;
// Ensure the RCD operation is still valid
var location = GetCoordinates(args.Event.Location); var location = GetCoordinates(args.Event.Location);
var gridId = location.GetGridUid(EntityManager); if (!TryGetMapGridData(location, out var mapGridData))
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; return;
}
var mapGrid = Comp<MapGridComponent>(gridId.Value); if (!IsRCDOperationStillValid(uid, component, mapGridData.Value, args.Event.Target, args.Event.User))
var tile = mapGrid.GetTileRef(location);
if (!IsRCDStillValid(uid, comp, args.Event.User, args.Event.Target, mapGrid, tile, args.Event.StartingMode))
args.Cancel(); 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) if (args.Handled || args.Cancelled || !_timing.IsFirstTimePredicted)
return; return;
var user = args.User; args.Handled = true;
var location = GetCoordinates(args.Location); var location = GetCoordinates(args.Location);
var gridId = location.GetGridUid(EntityManager); if (!TryGetMapGridData(location, out var mapGridData))
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; return;
}
var mapGrid = Comp<MapGridComponent>(gridId.Value); // Ensure the RCD operation is still valid
var tile = mapGrid.GetTileRef(location); if (!IsRCDOperationStillValid(uid, component, mapGridData.Value, args.Target, args.User))
var snapPos = mapGrid.TileIndicesFor(location);
// I love that this uses entirely separate code to construction and tile placement!!!
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; 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);
} }
mapGrid.SetTile(snapPos, new Tile(_tileDefMan[comp.Floor].TileId)); private void OnRCDconstructionGhostRotationEvent(RCDConstructionGhostRotationEvent ev, EntitySessionEventArgs session)
_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); var uid = GetEntity(ev.NetEntity);
_adminLogger.Add(LogType.RCD, LogImpact.High, $"{ToPrettyString(args.User):user} used RCD to set grid: {tile.GridUid} tile: {snapPos} to space");
} // Determine if player that send the message is carrying the specified RCD in their active hand
else // Delete the targeted thing if (session.SenderSession.AttachedEntity == null)
{ return;
var target = args.Target!.Value;
_adminLogger.Add(LogType.RCD, LogImpact.High, $"{ToPrettyString(args.User):user} used RCD to delete {ToPrettyString(target):target}"); if (!TryComp<HandsComponent>(session.SenderSession.AttachedEntity, out var hands) ||
QueueDel(target); uid != hands.ActiveHand?.HeldEntity)
} return;
break;
//Walls are a special behaviour, and require us to build a new object with a transform rather than setting a grid tile, if (!TryComp<RCDComponent>(uid, out var rcd))
// thus we early return to avoid the tile set code. return;
case RcdMode.Walls:
// only spawn on the server // Update the construction direction
if (_net.IsServer) rcd.ConstructionDirection = ev.Direction;
{ Dirty(uid, rcd);
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!
} }
_audio.PlayPredicted(comp.SuccessSound, uid, user); #endregion
_charges.UseCharge(uid);
args.Handled = true;
}
private bool IsRCDStillValid(EntityUid uid, RCDComponent comp, EntityUid user, EntityUid? target, MapGridComponent mapGrid, TileRef tile, RcdMode startingMode) #region Entity construction/deconstruction rule checks
public bool IsRCDOperationStillValid(EntityUid uid, RCDComponent component, MapGridData mapGridData, EntityUid? target, EntityUid user, bool popMsgs = true)
{ {
//Less expensive checks first. Failing those ones, we need to check that the tile isn't obstructed. // Update cached prototype if required
if (comp.Mode != startingMode) 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))
{
if (popMsgs)
_popup.PopupClient(Loc.GetString("rcd-component-no-ammo-message"), uid, user);
return false; return false;
}
var unobstructed = target == null if (_charges.HasInsufficientCharges(uid, component.CachedPrototype.Cost, charges))
? _interaction.InRangeUnobstructed(user, mapGrid.GridTileToWorld(tile.GridIndices), popup: true) {
: _interaction.InRangeUnobstructed(user, target.Value, popup: true); if (popMsgs)
_popup.PopupClient(Loc.GetString("rcd-component-insufficient-ammo-message"), uid, user);
return false;
}
// 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);
if (!unobstructed) if (!unobstructed)
return false; return false;
switch (comp.Mode) // Return whether the operation location is valid
switch (component.CachedPrototype.Mode)
{ {
//Floor mode just needs the tile to be a space tile (subFloor) case RcdMode.ConstructTile: return IsConstructionLocationValid(uid, component, mapGridData, user, popMsgs);
case RcdMode.Floors: case RcdMode.ConstructObject: return IsConstructionLocationValid(uid, component, mapGridData, user, popMsgs);
if (!tile.Tile.IsEmpty) case RcdMode.Deconstruct: return IsDeconstructionStillValid(uid, component, mapGridData, target, user, popMsgs);
{ }
_popup.PopupClient(Loc.GetString("rcd-component-cannot-build-floor-tile-not-empty-message"), uid, user);
return false; return false;
} }
return true; private bool IsConstructionLocationValid(EntityUid uid, RCDComponent component, MapGridData mapGridData, EntityUid user, bool popMsgs = 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: // Check rule: Must build on empty tile
if (tile.Tile.IsEmpty) if (component.CachedPrototype.ConstructionRules.Contains(RcdConstructionRule.MustBuildOnEmptyTile) && !mapGridData.Tile.Tile.IsEmpty)
return false; {
if (popMsgs)
_popup.PopupClient(Loc.GetString("rcd-component-must-build-on-empty-tile-message"), uid, user);
//They tried to decon a turf but... return false;
}
// 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);
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);
_intersectingEntities.Clear();
_lookup.GetLocalEntitiesIntersecting(mapGridData.GridUid, mapGridData.Position, _intersectingEntities, -0.05f, LookupFlags.Uncontained);
foreach (var ent in _intersectingEntities)
{
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)
{
// 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;
}
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) if (target == null)
{ {
// the turf is blocked // The tile is empty
if (IsTileBlocked(tile)) if (mapGridData.Tile.Tile.IsEmpty)
{ {
_popup.PopupClient(Loc.GetString("rcd-component-tile-obstructed-message"), uid, user); if (popMsgs)
_popup.PopupClient(Loc.GetString("rcd-component-nothing-to-deconstruct-message"), uid, user);
return false; return false;
} }
// the turf can't be destroyed (planet probably)
var tileDef = (ContentTileDefinition) _tileDefMan[tile.Tile.TypeId]; // 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 (tileDef.Indestructible)
{ {
if (popMsgs)
_popup.PopupClient(Loc.GetString("rcd-component-tile-indestructible-message"), uid, user); _popup.PopupClient(Loc.GetString("rcd-component-tile-indestructible-message"), uid, user);
return false; return false;
} }
} }
//They tried to decon a non-turf but it's not in the whitelist
else if (!_tag.HasTag(target.Value, "RCDDeconstructWhitelist")) // 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); _popup.PopupClient(Loc.GetString("rcd-component-deconstruct-target-not-on-whitelist-message"), uid, user);
return false; return false;
} }
}
return true; 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) #endregion
#region Entity construction/deconstruction
private void FinalizeRCDOperation(EntityUid uid, RCDComponent component, MapGridData mapGridData, EntityUid? target, EntityUid user)
{ {
_popup.PopupClient(Loc.GetString("rcd-component-cannot-build-wall-tile-not-empty-message"), uid, user); if (!_net.IsServer)
return;
if (component.CachedPrototype.Prototype == null)
return;
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)
{
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;
}
_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)
{
// 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");
}
else
{
// Deconstruct object
_adminLogger.Add(LogType.RCD, LogImpact.High, $"{ToPrettyString(user):user} used RCD to delete {ToPrettyString(target):target}");
QueueDel(target);
}
break;
}
}
#endregion
#region Utility functions
public bool TryGetMapGridData(EntityCoordinates location, [NotNullWhen(true)] out MapGridData? mapGridData)
{
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; return false;
} }
if (IsTileBlocked(tile)) gridUid = mapGrid.Owner;
{
_popup.PopupClient(Loc.GetString("rcd-component-tile-obstructed-message"), uid, user); var tile = _mapSystem.GetTileRef(gridUid.Value, mapGrid, location);
return false; var position = _mapSystem.TileIndicesFor(gridUid.Value, mapGrid, location);
} mapGridData = new MapGridData(gridUid.Value, mapGrid, location, tile, position);
return true; return true;
case RcdMode.Airlock:
if (tile.Tile.IsEmpty)
{
_popup.PopupClient(Loc.GetString("rcd-component-cannot-build-airlock-tile-not-empty-message"), uid, user);
return false;
}
if (IsTileBlocked(tile))
{
_popup.PopupClient(Loc.GetString("rcd-component-tile-obstructed-message"), uid, user);
return false;
}
return true;
default:
return false; //I don't know why this would happen, but sure I guess. Get out of here invalid state!
}
} }
private void NextMode(EntityUid uid, RCDComponent comp, EntityUid user) private bool DoesCustomBoundsIntersectWithFixture(PolygonShape boundingPolygon, Transform boundingTransform, EntityUid fixtureOwner, Fixture fixture)
{ {
_audio.PlayPredicted(comp.SwapModeSound, uid, user); var entXformComp = Transform(fixtureOwner);
var entXform = new Transform(new(), entXformComp.LocalRotation);
var mode = (int) comp.Mode; return boundingPolygon.ComputeAABB(boundingTransform, 0).Intersects(fixture.Shape.ComputeAABB(entXform, 0));
mode = ++mode % _rcdModeCount;
comp.Mode = (RcdMode) mode;
Dirty(uid, comp);
var msg = Loc.GetString("rcd-component-change-mode", ("mode", comp.Mode.ToString()));
_popup.PopupClient(msg, uid, user);
} }
private bool IsTileBlocked(TileRef tile) public void UpdateCachedPrototype(EntityUid uid, RCDComponent component)
{ {
return _turf.IsTileBlocked(tile, CollisionGroup.MobMask); 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] [Serializable, NetSerializable]
public sealed partial class RCDDoAfterEvent : DoAfterEvent public sealed partial class RCDDoAfterEvent : DoAfterEvent
{ {
[DataField("location", required: true)] [DataField(required: true)]
public NetCoordinates Location = default!; public NetCoordinates Location { get; private set; } = default!;
[DataField("startingMode", required: true)] [DataField]
public RcdMode StartingMode = default!; public ProtoId<RCDPrototype> StartingProtoId { get; private set; } = default!;
private RCDDoAfterEvent() [DataField]
{ public int Cost { get; private set; } = 1;
}
public RCDDoAfterEvent(NetCoordinates location, RcdMode startingMode) [DataField("fx")]
public NetEntity? Effect { get; private set; } = null;
private RCDDoAfterEvent() { }
public RCDDoAfterEvent(NetCoordinates location, ProtoId<RCDPrototype> startingProtoId, int cost, NetEntity? effect = null)
{ {
Location = location; Location = location;
StartingMode = startingMode; StartingProtoId = startingProtoId;
Cost = cost;
Effect = effect;
} }
public override DoAfterEvent Clone() => this; public override DoAfterEvent Clone() => this;

View File

@@ -1,18 +1,66 @@
### UI ### UI
# Shown when an RCD is examined in details range rcd-component-examine-mode-details = It's currently set to '{$mode}' mode.
rcd-component-examine-detail = It's currently on {$mode} mode. rcd-component-examine-build-details = It's currently set to build {MAKEPLURAL($name)}.
### Interaction Messages ### Interaction Messages
# Shown when changing RCD Mode # Mode change
rcd-component-change-mode = The RCD is now set to {$mode} mode. 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)}.
rcd-component-no-ammo-message = The RCD is out of ammo! # Ammo count
rcd-component-tile-obstructed-message = That tile is obstructed! rcd-component-no-ammo-message = The RCD has run out of charges!
rcd-component-tile-indestructible-message = That tile can't be destroyed! rcd-component-insufficient-ammo-message = The RCD doesn't have enough charges left!
# 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-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-nothing-to-deconstruct-message = There's nothing to deconstruct!
rcd-component-cannot-build-wall-tile-not-empty-message = You cannot build a wall on space! rcd-component-tile-obstructed-message = You can't deconstruct tiles when there's something on top of them!
rcd-component-cannot-build-airlock-tile-not-empty-message = Cannot build an airlock on space!
# 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

View File

@@ -0,0 +1,3 @@
### Loc for the various UI-related verbs
ui-verb-toggle-open = Toggle UI
verb-instrument-openui = Play Music

View File

@@ -166,8 +166,8 @@
- type: entity - type: entity
id: CrateRCDAmmo id: CrateRCDAmmo
parent: CrateEngineering parent: CrateEngineering
name: RCD ammo crate name: compressed matter crate
description: 3 RCD ammo, each restoring 5 charges. description: Contains three compressed matter cartridges.
components: components:
- type: StorageFill - type: StorageFill
contents: contents:
@@ -178,7 +178,7 @@
id: CrateRCD id: CrateRCD
parent: CrateEngineeringSecure parent: CrateEngineeringSecure
name: RCD crate name: RCD crate
description: A crate containing a single Rapid Construction Device. description: A crate containing a single rapid construction device.
components: components:
- type: StorageFill - type: StorageFill
contents: contents:

View File

@@ -128,9 +128,10 @@
snap: snap:
- Wall - Wall
components: components:
- type: Tag - type: RCDDeconstructable
tags: cost: 2
- RCDDeconstructWhitelist delay: 2
fx: EffectRCDDeconstruct2
- type: Clickable - type: Clickable
- type: InteractionOutline - type: InteractionOutline
- type: Sprite - type: Sprite

View File

@@ -1,16 +1,115 @@
- type: entity - type: entity
id: EffectRCDConstruction id: EffectRCDBase
abstract: true
noSpawn: true noSpawn: true
components: components:
- type: Transform - type: Transform
anchored: True anchored: True
- type: Sprite - type: Sprite
snapCardinals: true
noRot: true
drawdepth: Effects drawdepth: Effects
sprite: /Textures/Effects/rcd.rsi sprite: /Textures/Effects/rcd.rsi
state: construct state: construct0
- type: TimedDespawn
lifetime: 3.2
- type: Tag - type: Tag
tags: tags:
- HideContextMenu - HideContextMenu
- type: AnimationPlayer - 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

View File

@@ -337,15 +337,35 @@
path: "/Audio/Items/drill_hit.ogg" path: "/Audio/Items/drill_hit.ogg"
- type: entity - type: entity
name: RCD
parent: BaseItem
id: RCD 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: components:
- type: RCD - type: RCD
availablePrototypes:
- WallSolid
- FloorSteel
- Plating
- Catwalk
- Grille
- Window
- WindowDirectional
- WindowReinforcedDirectional
- ReinforcedWindow
- Airlock
- AirlockGlass
- Firelock
- TubeLight
- BulbLight
- LVCable
- MVCable
- HVCable
- CableTerminal
- Deconstruct
- type: LimitedCharges - type: LimitedCharges
maxCharges: 5 maxCharges: 30
charges: 5 charges: 30
- type: UseDelay - type: UseDelay
- type: Sprite - type: Sprite
sprite: Objects/Tools/rcd.rsi sprite: Objects/Tools/rcd.rsi
@@ -363,6 +383,12 @@
Plastic: 100 Plastic: 100
- type: StaticPrice - type: StaticPrice
price: 100 price: 100
- type: UserInterface
interfaces:
- key: enum.RcdUiKey.Key
type: RCDMenuBoundUserInterface
- type: ActivatableUI
key: enum.RcdUiKey.Key
- type: entity - type: entity
id: RCDEmpty id: RCDEmpty
@@ -370,37 +396,50 @@
suffix: Empty suffix: Empty
components: components:
- type: LimitedCharges - type: LimitedCharges
maxCharges: 5
charges: 0 charges: 0
- type: RCD
availablePrototypes:
- WallSolid
- FloorSteel
- Plating
- Catwalk
- Grille
- Window
- WindowDirectional
- WindowReinforcedDirectional
- ReinforcedWindow
- Airlock
- AirlockGlass
- Firelock
- type: entity - type: entity
id: RCDRecharging id: RCDRecharging
parent: RCD parent: RCD
name: experimental rcd name: experimental RCD
description: A bluespace-enhanced RCD that regenerates charges passively. description: A bluespace-enhanced rapid construction device that passively generates its own compressed matter.
suffix: AutoRecharge suffix: AutoRecharge
components: components:
- type: LimitedCharges - type: LimitedCharges
maxCharges: 3 maxCharges: 20
charges: 3 charges: 20
- type: AutoRecharge - type: AutoRecharge
rechargeDuration: 30 rechargeDuration: 10
- type: entity - type: entity
id: RCDExperimental id: RCDExperimental
parent: RCD parent: RCD
suffix: Admeme suffix: Admeme
name: experimental rcd name: experimental RCD
description: A bluespace-enhanced RCD that regenerates charges passively. description: A bluespace-enhanced rapid construction device that passively generates its own compressed matter.
components: components:
- type: AutoRecharge - type: AutoRecharge
rechargeDuration: 5 rechargeDuration: 1
- type: entity - type: entity
name: RCD Ammo name: compressed matter
parent: BaseItem parent: BaseItem
id: RCDAmmo id: RCDAmmo
description: Ammo cartridge for an RCD. description: A cartridge of raw matter compacted by bluespace technology. Used in rapid construction devices.
components: components:
- type: RCDAmmo - type: RCDAmmo
- type: Sprite - type: Sprite

View File

@@ -138,48 +138,6 @@
sprite: Structures/Doors/Airlocks/Standard/hatch_maint.rsi sprite: Structures/Doors/Airlocks/Standard/hatch_maint.rsi
# Glass # 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 - type: entity
parent: AirlockGlass parent: AirlockGlass
id: AirlockEngineeringGlass id: AirlockEngineeringGlass

View File

@@ -30,6 +30,10 @@
- type: Damageable - type: Damageable
damageContainer: Inorganic damageContainer: Inorganic
damageModifierSet: Metallic damageModifierSet: Metallic
- type: RCDDeconstructable
cost: 6
delay: 8
fx: EffectRCDDeconstruct8
- type: Destructible - type: Destructible
thresholds: thresholds:
- trigger: - trigger:

View File

@@ -111,6 +111,10 @@
- type: Damageable - type: Damageable
damageContainer: StructuralInorganic damageContainer: StructuralInorganic
damageModifierSet: StrongMetallic damageModifierSet: StrongMetallic
- type: RCDDeconstructable
cost: 6
delay: 8
fx: EffectRCDDeconstruct8
- type: Destructible - type: Destructible
thresholds: thresholds:
- trigger: - trigger:
@@ -150,4 +154,52 @@
placement: placement:
mode: SnapgridCenter 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

View File

@@ -1,5 +1,5 @@
- type: entity - type: entity
parent: Airlock parent: AirlockRCDResistant
id: AirlockExternal id: AirlockExternal
suffix: External suffix: External
description: It opens, it closes, it might crush you, and there might be only space behind it. description: It opens, it closes, it might crush you, and there might be only space behind it.

View File

@@ -1,5 +1,5 @@
- type: entity - type: entity
parent: Airlock parent: AirlockRCDResistant
id: AirlockShuttle id: AirlockShuttle
suffix: Docking suffix: Docking
name: external airlock name: external airlock

View File

@@ -19,6 +19,10 @@
- type: Damageable - type: Damageable
damageContainer: Inorganic damageContainer: Inorganic
damageModifierSet: Metallic damageModifierSet: Metallic
- type: RCDDeconstructable
cost: 4
delay: 6
fx: EffectRCDDeconstruct6
- type: Destructible - type: Destructible
thresholds: thresholds:
- trigger: - trigger:

View File

@@ -25,6 +25,10 @@
- type: Damageable - type: Damageable
damageContainer: Inorganic damageContainer: Inorganic
damageModifierSet: Metallic damageModifierSet: Metallic
- type: RCDDeconstructable
cost: 4
delay: 6
fx: EffectRCDDeconstruct6
- type: Destructible - type: Destructible
thresholds: thresholds:
- trigger: - trigger:

View File

@@ -43,6 +43,10 @@
- type: Damageable - type: Damageable
damageContainer: Inorganic damageContainer: Inorganic
damageModifierSet: Metallic damageModifierSet: Metallic
- type: RCDDeconstructable
cost: 6
delay: 6
fx: EffectRCDDeconstruct6
- type: Destructible - type: Destructible
thresholds: thresholds:
- trigger: - trigger:

View File

@@ -41,6 +41,10 @@
- type: Damageable - type: Damageable
damageContainer: Inorganic damageContainer: Inorganic
damageModifierSet: Metallic damageModifierSet: Metallic
- type: RCDDeconstructable
cost: 6
delay: 8
fx: EffectRCDDeconstruct8
- type: Destructible - type: Destructible
thresholds: thresholds:
- trigger: - trigger:
@@ -97,6 +101,10 @@
- type: Damageable - type: Damageable
damageContainer: Inorganic damageContainer: Inorganic
damageModifierSet: Metallic damageModifierSet: Metallic
- type: RCDDeconstructable
cost: 6
delay: 8
fx: EffectRCDDeconstruct8
- type: Destructible - type: Destructible
thresholds: thresholds:
- trigger: - trigger:

View File

@@ -26,6 +26,10 @@
- type: Damageable - type: Damageable
damageContainer: Inorganic damageContainer: Inorganic
damageModifierSet: Metallic damageModifierSet: Metallic
- type: RCDDeconstructable
cost: 6
delay: 8
fx: EffectRCDDeconstruct8
- type: Destructible - type: Destructible
thresholds: thresholds:
- trigger: - trigger:

View File

@@ -67,6 +67,10 @@
damageModifierSet: Glass damageModifierSet: Glass
- type: ExaminableDamage - type: ExaminableDamage
messages: WindowMessages messages: WindowMessages
- type: RCDDeconstructable
cost: 8
delay: 8
fx: EffectRCDDeconstruct8
- type: Destructible - type: Destructible
thresholds: thresholds:
- trigger: - trigger:

View File

@@ -40,6 +40,10 @@
- type: Damageable - type: Damageable
damageContainer: Inorganic damageContainer: Inorganic
damageModifierSet: Metallic damageModifierSet: Metallic
- type: RCDDeconstructable
cost: 4
delay: 2
fx: EffectRCDDeconstruct2
- type: Destructible - type: Destructible
thresholds: thresholds:
- trigger: - trigger:

View File

@@ -17,6 +17,10 @@
- type: Damageable - type: Damageable
damageContainer: Inorganic damageContainer: Inorganic
damageModifierSet: Metallic damageModifierSet: Metallic
- type: RCDDeconstructable
cost: 2
delay: 2
fx: EffectRCDDeconstruct2
- type: Destructible - type: Destructible
thresholds: thresholds:
- trigger: - trigger:

View File

@@ -43,6 +43,10 @@
lowVoltageNode: power lowVoltageNode: power
- type: CableVis - type: CableVis
node: power node: power
- type: RCDDeconstructable
cost: 2
delay: 2
fx: EffectRCDDeconstruct2
- type: entity - type: entity
parent: CableBase parent: CableBase

View File

@@ -10,9 +10,6 @@
Brute: Brute:
path: path:
"/Audio/Weapons/grille_hit.ogg" "/Audio/Weapons/grille_hit.ogg"
- type: Tag
tags:
- RCDDeconstructWhitelist
- type: Sprite - type: Sprite
sprite: Structures/Walls/fence.rsi sprite: Structures/Walls/fence.rsi
drawdepth: WallTops drawdepth: WallTops
@@ -78,6 +75,10 @@
True: { visible: True } True: { visible: True }
False: { visible: False } False: { visible: False }
- type: AnimationPlayer - type: AnimationPlayer
- type: RCDDeconstructable
cost: 2
delay: 2
fx: EffectRCDDeconstruct2
- type: entity - type: entity
parent: BaseFenceMetal parent: BaseFenceMetal

View File

@@ -12,7 +12,6 @@
"/Audio/Weapons/boxingpunch1.ogg" "/Audio/Weapons/boxingpunch1.ogg"
- type: Tag - type: Tag
tags: tags:
- RCDDeconstructWhitelist
- Wooden - Wooden
- type: Sprite - type: Sprite
sprite: Structures/Walls/wooden_fence.rsi sprite: Structures/Walls/wooden_fence.rsi
@@ -24,6 +23,10 @@
- type: Damageable - type: Damageable
damageContainer: Inorganic damageContainer: Inorganic
damageModifierSet: Wood damageModifierSet: Wood
- type: RCDDeconstructable
cost: 2
delay: 2
fx: EffectRCDDeconstruct2
- type: Destructible - type: Destructible
thresholds: thresholds:
- trigger: - trigger:

View File

@@ -9,9 +9,10 @@
Brute: Brute:
path: path:
"/Audio/Weapons/grille_hit.ogg" "/Audio/Weapons/grille_hit.ogg"
- type: Tag - type: RCDDeconstructable
tags: cost: 6
- RCDDeconstructWhitelist delay: 4
fx: EffectRCDDeconstruct4
- type: CanBuildWindowOnTop - type: CanBuildWindowOnTop
- type: Sprite - type: Sprite
drawdepth: Walls drawdepth: Walls
@@ -120,9 +121,10 @@
- type: Icon - type: Icon
sprite: Structures/Walls/grille.rsi sprite: Structures/Walls/grille.rsi
state: grille_broken state: grille_broken
- type: Tag - type: RCDDeconstructable
tags: cost: 6
- RCDDeconstructWhitelist delay: 4
fx: EffectRCDDeconstruct4
- type: Construction - type: Construction
graph: Grille graph: Grille
node: grilleBroken node: grilleBroken

View File

@@ -57,6 +57,10 @@
- type: Construction - type: Construction
graph: Railing graph: Railing
node: railing node: railing
- type: RCDDeconstructable
cost: 2
delay: 2
fx: EffectRCDDeconstruct2
- type: entity - type: entity
parent: BaseStructure parent: BaseStructure
@@ -126,6 +130,10 @@
- type: Construction - type: Construction
graph: Railing graph: Railing
node: railingCorner node: railingCorner
- type: RCDDeconstructable
cost: 2
delay: 2
fx: EffectRCDDeconstruct2
- type: entity - type: entity
parent: BaseStructure parent: BaseStructure
@@ -186,6 +194,10 @@
- type: Construction - type: Construction
graph: Railing graph: Railing
node: railingCornerSmall node: railingCornerSmall
- type: RCDDeconstructable
cost: 2
delay: 2
fx: EffectRCDDeconstruct2
- type: entity - type: entity
parent: BaseStructure parent: BaseStructure
@@ -261,3 +273,7 @@
- type: Construction - type: Construction
graph: Railing graph: Railing
node: railingRound node: railingRound
- type: RCDDeconstructable
cost: 2
delay: 2
fx: EffectRCDDeconstruct2

View File

@@ -59,11 +59,14 @@
- type: Tag - type: Tag
tags: tags:
- Wall - Wall
- RCDDeconstructWhitelist
- type: Sprite - type: Sprite
sprite: Structures/Walls/brick.rsi sprite: Structures/Walls/brick.rsi
- type: Icon - type: Icon
sprite: Structures/Walls/brick.rsi sprite: Structures/Walls/brick.rsi
- type: RCDDeconstructable
cost: 6
delay: 8
fx: EffectRCDDeconstruct8
- type: Destructible - type: Destructible
thresholds: thresholds:
- trigger: - trigger:
@@ -90,7 +93,6 @@
- type: Tag - type: Tag
tags: tags:
- Wall - Wall
- RCDDeconstructWhitelist
- type: Sprite - type: Sprite
sprite: Structures/Walls/clock.rsi sprite: Structures/Walls/clock.rsi
- type: Icon - type: Icon
@@ -123,7 +125,6 @@
- type: Tag - type: Tag
tags: tags:
- Wall - Wall
- RCDDeconstructWhitelist
- type: Sprite - type: Sprite
sprite: Structures/Walls/clown.rsi sprite: Structures/Walls/clown.rsi
- type: Icon - type: Icon
@@ -131,6 +132,10 @@
- type: Construction - type: Construction
graph: Girder graph: Girder
node: bananiumWall node: bananiumWall
- type: RCDDeconstructable
cost: 6
delay: 8
fx: EffectRCDDeconstruct8
- type: Destructible - type: Destructible
thresholds: thresholds:
- trigger: - trigger:
@@ -156,7 +161,6 @@
components: components:
- type: Tag - type: Tag
tags: tags:
- RCDDeconstructWhitelist
- Wall - Wall
- Structure - Structure
- type: Sprite - type: Sprite
@@ -190,7 +194,6 @@
components: components:
- type: Tag - type: Tag
tags: tags:
- RCDDeconstructWhitelist
- Wall - Wall
- type: Sprite - type: Sprite
sprite: Structures/Walls/cult.rsi sprite: Structures/Walls/cult.rsi
@@ -223,7 +226,6 @@
tags: tags:
- Wall - Wall
- Debug - Debug
- RCDDeconstructWhitelist
- type: Sprite - type: Sprite
sprite: Structures/Walls/debug.rsi sprite: Structures/Walls/debug.rsi
- type: Icon - type: Icon
@@ -253,11 +255,14 @@
- type: Tag - type: Tag
tags: tags:
- Wall - Wall
- RCDDeconstructWhitelist
- type: Sprite - type: Sprite
sprite: Structures/Walls/diamond.rsi sprite: Structures/Walls/diamond.rsi
- type: Icon - type: Icon
sprite: Structures/Walls/diamond.rsi sprite: Structures/Walls/diamond.rsi
- type: RCDDeconstructable
cost: 6
delay: 8
fx: EffectRCDDeconstruct8
- type: Destructible - type: Destructible
thresholds: thresholds:
- trigger: - trigger:
@@ -283,7 +288,6 @@
- type: Tag - type: Tag
tags: tags:
- Wall - Wall
- RCDDeconstructWhitelist
- type: Sprite - type: Sprite
sprite: Structures/Walls/gold.rsi sprite: Structures/Walls/gold.rsi
- type: Icon - type: Icon
@@ -291,6 +295,10 @@
- type: Construction - type: Construction
graph: Girder graph: Girder
node: goldWall node: goldWall
- type: RCDDeconstructable
cost: 6
delay: 8
fx: EffectRCDDeconstruct8
- type: Destructible - type: Destructible
thresholds: thresholds:
- trigger: - trigger:
@@ -325,7 +333,6 @@
- type: Tag - type: Tag
tags: tags:
- Wall - Wall
- RCDDeconstructWhitelist
- type: Sprite - type: Sprite
sprite: Structures/Walls/ice.rsi sprite: Structures/Walls/ice.rsi
- type: Icon - type: Icon
@@ -355,7 +362,6 @@
- type: Tag - type: Tag
tags: tags:
- Wall - Wall
- RCDDeconstructWhitelist
- type: Sprite - type: Sprite
sprite: Structures/Walls/plasma.rsi sprite: Structures/Walls/plasma.rsi
- type: Icon - type: Icon
@@ -363,6 +369,10 @@
- type: Construction - type: Construction
graph: Girder graph: Girder
node: plasmaWall node: plasmaWall
- type: RCDDeconstructable
cost: 6
delay: 8
fx: EffectRCDDeconstruct8
- type: Destructible - type: Destructible
thresholds: thresholds:
- trigger: - trigger:
@@ -399,7 +409,6 @@
- type: Tag - type: Tag
tags: tags:
- Wall - Wall
- RCDDeconstructWhitelist
- type: Sprite - type: Sprite
sprite: Structures/Walls/plastic.rsi sprite: Structures/Walls/plastic.rsi
- type: Icon - type: Icon
@@ -407,6 +416,10 @@
- type: Construction - type: Construction
graph: Girder graph: Girder
node: plasticWall node: plasticWall
- type: RCDDeconstructable
cost: 6
delay: 8
fx: EffectRCDDeconstruct8
- type: Destructible - type: Destructible
thresholds: thresholds:
- trigger: - trigger:
@@ -459,7 +472,6 @@
- type: Tag - type: Tag
tags: tags:
- Wall - Wall
- RCDDeconstructWhitelist
- type: Destructible - type: Destructible
thresholds: thresholds:
- trigger: - trigger:
@@ -604,7 +616,6 @@
- type: Tag - type: Tag
tags: tags:
- Wall - Wall
- RCDDeconstructWhitelist
- type: Sprite - type: Sprite
sprite: Structures/Walls/riveted.rsi sprite: Structures/Walls/riveted.rsi
- type: Icon - type: Icon
@@ -639,11 +650,14 @@
- type: Tag - type: Tag
tags: tags:
- Wall - Wall
- RCDDeconstructWhitelist
- type: Sprite - type: Sprite
sprite: Structures/Walls/sandstone.rsi sprite: Structures/Walls/sandstone.rsi
- type: Icon - type: Icon
sprite: Structures/Walls/sandstone.rsi sprite: Structures/Walls/sandstone.rsi
- type: RCDDeconstructable
cost: 6
delay: 8
fx: EffectRCDDeconstruct8
- type: Destructible - type: Destructible
thresholds: thresholds:
- trigger: - trigger:
@@ -669,7 +683,6 @@
- type: Tag - type: Tag
tags: tags:
- Wall - Wall
- RCDDeconstructWhitelist
- type: Sprite - type: Sprite
sprite: Structures/Walls/silver.rsi sprite: Structures/Walls/silver.rsi
- type: Icon - type: Icon
@@ -677,6 +690,10 @@
- type: Construction - type: Construction
graph: Girder graph: Girder
node: silverWall node: silverWall
- type: RCDDeconstructable
cost: 6
delay: 8
fx: EffectRCDDeconstruct8
- type: Destructible - type: Destructible
thresholds: thresholds:
- trigger: - trigger:
@@ -842,7 +859,6 @@
- type: Tag - type: Tag
tags: tags:
- Wall - Wall
- RCDDeconstructWhitelist
- type: Sprite - type: Sprite
sprite: Structures/Walls/solid.rsi sprite: Structures/Walls/solid.rsi
- type: WallReplacementMarker - type: WallReplacementMarker
@@ -851,6 +867,10 @@
node: wall node: wall
- type: Icon - type: Icon
sprite: Structures/Walls/solid.rsi sprite: Structures/Walls/solid.rsi
- type: RCDDeconstructable
cost: 6
delay: 8
fx: EffectRCDDeconstruct8
- type: Destructible - type: Destructible
thresholds: thresholds:
- trigger: - trigger:
@@ -1012,7 +1032,6 @@
- type: Tag - type: Tag
tags: tags:
- Wall - Wall
- RCDDeconstructWhitelist
- type: Sprite - type: Sprite
sprite: Structures/Walls/web.rsi sprite: Structures/Walls/web.rsi
- type: Icon - type: Icon
@@ -1224,11 +1243,14 @@
- type: Tag - type: Tag
tags: tags:
- Wall - Wall
- RCDDeconstructWhitelist
- type: Sprite - type: Sprite
sprite: Structures/Walls/cobblebrick.rsi sprite: Structures/Walls/cobblebrick.rsi
- type: Icon - type: Icon
sprite: Structures/Walls/cobblebrick.rsi sprite: Structures/Walls/cobblebrick.rsi
- type: RCDDeconstructable
cost: 6
delay: 8
fx: EffectRCDDeconstruct8
- type: Destructible - type: Destructible
thresholds: thresholds:
- trigger: - trigger:

View File

@@ -1,7 +1,7 @@
- type: entity - type: entity
id: MiningWindow id: MiningWindow
name: mining window name: mining window
parent: Window parent: WindowRCDResistant
components: components:
- type: Sprite - type: Sprite
drawdepth: WallTops drawdepth: WallTops

View File

@@ -1,7 +1,7 @@
- type: entity - type: entity
id: PlasmaWindow id: PlasmaWindow
name: plasma window name: plasma window
parent: Window parent: WindowRCDResistant
components: components:
- type: Sprite - type: Sprite
drawdepth: WallTops drawdepth: WallTops
@@ -55,7 +55,7 @@
- type: entity - type: entity
id: PlasmaWindowDirectional id: PlasmaWindowDirectional
parent: WindowDirectional parent: WindowDirectionalRCDResistant
name: directional plasma window name: directional plasma window
description: Don't smudge up the glass down there. description: Don't smudge up the glass down there.
placement: placement:

View File

@@ -14,6 +14,10 @@
- type: Damageable - type: Damageable
damageContainer: StructuralInorganic damageContainer: StructuralInorganic
damageModifierSet: RGlass damageModifierSet: RGlass
- type: RCDDeconstructable
cost: 6
delay: 6
fx: EffectRCDDeconstruct6
- type: Destructible - type: Destructible
thresholds: thresholds:
- trigger: - trigger:
@@ -99,6 +103,10 @@
sprite: Structures/Windows/cracks_directional.rsi sprite: Structures/Windows/cracks_directional.rsi
- type: Damageable - type: Damageable
damageModifierSet: RGlass damageModifierSet: RGlass
- type: RCDDeconstructable
cost: 4
delay: 4
fx: EffectRCDDeconstruct4
- type: Destructible - type: Destructible
thresholds: thresholds:
- trigger: - trigger:

View File

@@ -1,7 +1,7 @@
- type: entity - type: entity
id: ReinforcedPlasmaWindow id: ReinforcedPlasmaWindow
name: reinforced plasma window name: reinforced plasma window
parent: Window parent: WindowRCDResistant
components: components:
- type: Sprite - type: Sprite
drawdepth: WallTops drawdepth: WallTops
@@ -58,7 +58,7 @@
- type: entity - type: entity
id: PlasmaReinforcedWindowDirectional id: PlasmaReinforcedWindowDirectional
parent: WindowDirectional parent: WindowDirectionalRCDResistant
name: directional reinforced plasma window name: directional reinforced plasma window
description: Don't smudge up the glass down there. description: Don't smudge up the glass down there.
placement: placement:

View File

@@ -1,7 +1,7 @@
- type: entity - type: entity
id: ReinforcedUraniumWindow id: ReinforcedUraniumWindow
name: reinforced uranium window name: reinforced uranium window
parent: Window parent: WindowRCDResistant
components: components:
- type: Sprite - type: Sprite
drawdepth: WallTops drawdepth: WallTops

View File

@@ -1,7 +1,7 @@
- type: entity - type: entity
id: ShuttleWindow id: ShuttleWindow
name: shuttle window name: shuttle window
parent: Window parent: WindowRCDResistant
components: components:
- type: Sprite - type: Sprite
drawdepth: WallTops drawdepth: WallTops

View File

@@ -1,7 +1,7 @@
- type: entity - type: entity
id: UraniumWindow id: UraniumWindow
name: uranium window name: uranium window
parent: Window parent: WindowRCDResistant
components: components:
- type: Sprite - type: Sprite
drawdepth: WallTops drawdepth: WallTops

View File

@@ -16,7 +16,6 @@
arc: 360 # interact despite grilles arc: 360 # interact despite grilles
- type: Tag - type: Tag
tags: tags:
- RCDDeconstructWhitelist
- ForceFixRotations - ForceFixRotations
- Window - Window
- type: Sprite - type: Sprite
@@ -42,6 +41,10 @@
- type: ExaminableDamage - type: ExaminableDamage
messages: WindowMessages messages: WindowMessages
- type: Repairable - type: Repairable
- type: RCDDeconstructable
cost: 6
delay: 4
fx: EffectRCDDeconstruct4
- type: Destructible - type: Destructible
thresholds: thresholds:
- trigger: - trigger:
@@ -90,6 +93,14 @@
price: 100 price: 100
- type: BlockWeather - type: BlockWeather
- type: entity
id: WindowRCDResistant
parent: Window
abstract: true
components:
- type: RCDDeconstructable
deconstructable: false
- type: entity - type: entity
id: WindowDirectional id: WindowDirectional
parent: BaseStructure parent: BaseStructure
@@ -139,6 +150,10 @@
damageModifierSet: Glass damageModifierSet: Glass
- type: ExaminableDamage - type: ExaminableDamage
messages: WindowMessages messages: WindowMessages
- type: RCDDeconstructable
cost: 4
delay: 2
fx: EffectRCDDeconstruct2
- type: Destructible - type: Destructible
thresholds: thresholds:
- trigger: - trigger:
@@ -181,6 +196,14 @@
- type: StaticPrice - type: StaticPrice
price: 10 price: 10
- type: entity
id: WindowDirectionalRCDResistant
parent: WindowDirectional
abstract: true
components:
- type: RCDDeconstructable
deconstructable: false
- type: entity - type: entity
id: WindowFrostedDirectional id: WindowFrostedDirectional
parent: WindowDirectional parent: WindowDirectional

View File

@@ -51,3 +51,7 @@
max: 1 max: 1
- !type:DoActsBehavior - !type:DoActsBehavior
acts: [ "Destruction" ] acts: [ "Destruction" ]
- type: RCDDeconstructable
cost: 2
delay: 2
fx: EffectRCDDeconstruct2

View File

@@ -0,0 +1,294 @@
# Operations
- type: rcd
id: Invalid # Hidden prototype - do not add to RCDs
mode: Invalid
- type: rcd
id: Deconstruct
name: rcd-component-deconstruct
category: Main
sprite: /Textures/Interface/Radial/RCD/deconstruct.png
mode: Deconstruct
prototype: EffectRCDDeconstructPreview
rotation: Camera
- type: rcd
id: DeconstructLattice # Hidden prototype - do not add to RCDs
mode: Deconstruct
cost: 2
delay: 1
rotation: Camera
fx: EffectRCDDeconstruct2
- type: rcd
id: DeconstructTile # Hidden prototype - do not add to RCDs
mode: Deconstruct
cost: 4
delay: 4
rotation: Camera
fx: EffectRCDDeconstruct4
# Flooring
- type: rcd
id: Plating
name: rcd-component-plating
category: WallsAndFlooring
sprite: /Textures/Interface/Radial/RCD/plating.png
mode: ConstructTile
prototype: Plating
cost: 1
delay: 1
collisionMask: InteractImpassable
rules:
- CanBuildOnEmptyTile
fx: EffectRCDConstruct1
- type: rcd
id: FloorSteel
name: rcd-component-floor-steel
category: WallsAndFlooring
sprite: /Textures/Interface/Radial/RCD/metal_tile.png
mode: ConstructTile
prototype: FloorSteel
cost: 1
delay: 1
collisionMask: InteractImpassable
rules:
- CanBuildOnEmptyTile
fx: EffectRCDConstruct1
- type: rcd
id: Catwalk
name: rcd-component-catwalk
category: WallsAndFlooring
sprite: /Textures/Interface/Radial/RCD/catwalk.png
mode: ConstructObject
prototype: Catwalk
cost: 1
delay: 1
collisionMask: InteractImpassable
rules:
- MustBuildOnSubfloor
- IsCatwalk
rotation: Fixed
fx: EffectRCDConstruct1
# Walls
- type: rcd
id: WallSolid
name: rcd-component-wall-solid
category: WallsAndFlooring
sprite: /Textures/Interface/Radial/RCD/solid_wall.png
mode: ConstructObject
prototype: WallSolid
cost: 4
delay: 2
collisionMask: FullTileMask
rotation: Fixed
fx: EffectRCDConstruct2
- type: rcd
id: Grille
name: rcd-component-grille
category: WindowsAndGrilles
sprite: /Textures/Interface/Radial/RCD/grille.png
mode: ConstructObject
prototype: Grille
cost: 4
delay: 2
collisionMask: FullTileMask
rotation: Fixed
fx: EffectRCDConstruct2
# Windows
- type: rcd
id: Window
name: rcd-component-window
category: WindowsAndGrilles
sprite: /Textures/Interface/Radial/RCD/window.png
mode: ConstructObject
prototype: Window
cost: 3
delay: 2
collisionMask: FullTileMask
rules:
- IsWindow
rotation: Fixed
fx: EffectRCDConstruct2
- type: rcd
id: WindowDirectional
name: rcd-component-window-directional
category: WindowsAndGrilles
sprite: /Textures/Interface/Radial/RCD/directional.png
mode: ConstructObject
prototype: WindowDirectional
cost: 2
delay: 1
collisionMask: FullTileMask
collisionBounds: "-0.23,-0.49,0.23,-0.36"
rules:
- IsWindow
rotation: User
fx: EffectRCDConstruct1
- type: rcd
id: ReinforcedWindow
name: rcd-component-reinforced-window
category: WindowsAndGrilles
sprite: /Textures/Interface/Radial/RCD/window_reinforced.png
mode: ConstructObject
prototype: ReinforcedWindow
cost: 4
delay: 3
collisionMask: FullTileMask
rules:
- IsWindow
rotation: User
fx: EffectRCDConstruct3
- type: rcd
id: WindowReinforcedDirectional
name: rcd-component-window-reinforced-directional
category: WindowsAndGrilles
sprite: /Textures/Interface/Radial/RCD/directional_reinforced.png
mode: ConstructObject
prototype: WindowReinforcedDirectional
cost: 3
delay: 2
collisionMask: FullTileMask
collisionBounds: "-0.23,-0.49,0.23,-0.36"
rules:
- IsWindow
rotation: User
fx: EffectRCDConstruct2
# Airlocks
- type: rcd
id: Airlock
name: rcd-component-airlock
category: Airlocks
sprite: /Textures/Interface/Radial/RCD/airlock.png
mode: ConstructObject
prototype: Airlock
cost: 4
delay: 4
collisionMask: FullTileMask
rotation: Camera
fx: EffectRCDConstruct4
- type: rcd
id: AirlockGlass
name: rcd-component-airlock-glass
category: Airlocks
sprite: /Textures/Interface/Radial/RCD/glass_airlock.png
mode: ConstructObject
prototype: AirlockGlass
cost: 4
delay: 4
collisionMask: FullTileMask
rotation: Camera
fx: EffectRCDConstruct4
- type: rcd
id: Firelock
name: rcd-component-firelock
category: Airlocks
sprite: /Textures/Interface/Radial/RCD/firelock.png
mode: ConstructObject
prototype: Firelock
cost: 4
delay: 3
collisionMask: FullTileMask
rotation: Camera
fx: EffectRCDConstruct3
# Lighting
- type: rcd
id: TubeLight
name: rcd-component-tube-light
category: Lighting
sprite: /Textures/Interface/Radial/RCD/tube_light.png
mode: ConstructObject
prototype: Poweredlight
cost: 2
delay: 1
collisionMask: TabletopMachineMask
collisionBounds: "-0.23,-0.49,0.23,-0.36"
rotation: User
fx: EffectRCDConstruct1
- type: rcd
id: BulbLight
name: rcd-component-window-bulb-light
category: Lighting
sprite: /Textures/Interface/Radial/RCD/bulb_light.png
mode: ConstructObject
prototype: PoweredSmallLight
cost: 2
delay: 1
collisionMask: TabletopMachineMask
collisionBounds: "-0.23,-0.49,0.23,-0.36"
rotation: User
fx: EffectRCDConstruct1
# Electrical
- type: rcd
id: LVCable
name: rcd-component-window-lv-cable
category: Electrical
sprite: /Textures/Interface/Radial/RCD/lv_coil.png
mode: ConstructObject
prototype: CableApcExtension
cost: 1
delay: 0
collisionMask: InteractImpassable
rules:
- MustBuildOnSubfloor
rotation: Fixed
fx: EffectRCDConstruct0
- type: rcd
id: MVCable
name: rcd-component-window-mv-cable
category: Electrical
sprite: /Textures/Interface/Radial/RCD/mv_coil.png
mode: ConstructObject
prototype: CableMV
cost: 1
delay: 0
collisionMask: InteractImpassable
rules:
- MustBuildOnSubfloor
rotation: Fixed
fx: EffectRCDConstruct0
- type: rcd
id: HVCable
name: rcd-component-window-hv-cable
category: Electrical
sprite: /Textures/Interface/Radial/RCD/hv_coil.png
mode: ConstructObject
prototype: CableHV
cost: 1
delay: 0
collisionMask: InteractImpassable
rules:
- MustBuildOnSubfloor
rotation: Fixed
fx: EffectRCDConstruct0
- type: rcd
id: CableTerminal
name: rcd-component-window-cable-terminal
category: Electrical
sprite: /Textures/Interface/Radial/RCD/cable_terminal.png
mode: ConstructObject
prototype: CableTerminal
cost: 1
delay: 0
collisionMask: InteractImpassable
rules:
- MustBuildOnSubfloor
rotation: User
fx: EffectRCDConstruct0

View File

@@ -1034,9 +1034,6 @@
- type: Tag - type: Tag
id: RawMaterial id: RawMaterial
- type: Tag
id: RCDDeconstructWhitelist
# Give this to something that doesn't need any special recycler behavior and just needs deleting. # Give this to something that doesn't need any special recycler behavior and just needs deleting.
- type: Tag - type: Tag
id: Recyclable id: Recyclable

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

View File

@@ -8,7 +8,65 @@
"copyright": "Taken from tgStation at commit https://github.com/tgstation/tgstation/commit/d75cbd0a2900fdec4c12cd5ba986b52ccff03713/icons/effects/effects_rcd.dmi", "copyright": "Taken from tgStation at commit https://github.com/tgstation/tgstation/commit/d75cbd0a2900fdec4c12cd5ba986b52ccff03713/icons/effects/effects_rcd.dmi",
"states": [ "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": [ "delays": [
[ [
0.1, 0.1,
@@ -45,6 +103,357 @@
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,
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": "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
]
]
} }
] ]
} }

Binary file not shown.

After

Width:  |  Height:  |  Size: 710 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 776 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 283 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 646 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 404 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 467 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 775 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 312 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 296 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 637 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 698 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 223 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 856 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 901 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 850 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 655 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 297 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 976 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 833 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 436 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 448 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 266 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 431 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 739 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 495 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 525 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 211 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 186 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 389 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 391 B