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

@@ -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();
}
}