Exosuit: Ripley (#12668)

* mechs

* interaction relay

* atmos handling

* fuck around with interaction events

SPAGHETTI CODE OH MY GOD

* more sprites and whatever the hell

* more mech shit

* more shit for equipment

* starting equipment (for nukie mechs and such)

* equipment cycling

* starting with some of the ui

* a fat chunk of ui prototyping

* done tinkering with ui

* a bunch of ui stuff and what have yous

* cleaning up grabber and state handling

* make the ui actually functional + watch me port a million icons

I swear i'll prune the sprites later blease

* start on construction

* construction yo mamma

* remove some unused files

* fix a silly

* make the graph sane

* make it actually constructible.

* print the boards as well, bozo

* rebalance part prices

* eject action

also i appease the russians by remembering to localize

* Punch Shit

* make mech integrity and repairs work

* Make the UI more based

STOMP STOMP STOMP STOMP

* make equipment even more based

* batteries and other such delights

* make the ui look pimpin af

* make the construction mega based

* UI but so epic

* equipment

* some sweat tweaks

* damage rebalancing

* restructure tech

* fix some shit

* mechs inherit access

* make icons actually use sprite specifiers

* TRAILING COMMAA!!!!!

* fix a mild indentation sin

* undo this change because it isn't needed

* actually fix this

* secret webeditting shhhh

* place this tech here

* comments

* foo
This commit is contained in:
Nemanja 2022-12-10 12:05:39 -05:00 committed by GitHub
parent 11d81aa155
commit 913e1ee676
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
218 changed files with 3956 additions and 34 deletions

View File

@ -131,7 +131,7 @@ public abstract class CartridgeLoaderBoundUserInterface : BoundUserInterface
private UIFragment? RetrieveCartridgeUI(EntityUid? cartridgeUid)
{
var component = _entityManager?.GetComponentOrNull<UIFragmentComponent>(cartridgeUid);
component?.Ui?.Setup(this);
component?.Ui?.Setup(this, cartridgeUid);
return component?.Ui;
}
}

View File

@ -14,7 +14,7 @@ public sealed class NetProbeUi : UIFragment
return _fragment!;
}
public override void Setup(BoundUserInterface userInterface)
public override void Setup(BoundUserInterface userInterface, EntityUid? fragmentOwner)
{
_fragment = new NetProbeUiFragment();
}

View File

@ -15,7 +15,7 @@ public sealed class NotekeeperUi : UIFragment
return _fragment!;
}
public override void Setup(BoundUserInterface userInterface)
public override void Setup(BoundUserInterface userInterface, EntityUid? fragmentOwner)
{
_fragment = new NotekeeperUiFragment();
_fragment.OnNoteRemoved += note => SendNotekeeperMessage(NotekeeperUiAction.Remove, note, userInterface);

View File

@ -0,0 +1,24 @@
using Content.Shared.Mech;
using Robust.Client.GameObjects;
namespace Content.Client.Mech;
/// <summary>
/// Handles the sprite state changes while
/// constructing mech assemblies.
/// </summary>
public sealed class MechAssemblyVisualizerSystem : VisualizerSystem<MechAssemblyVisualsComponent>
{
protected override void OnAppearanceChange(EntityUid uid, MechAssemblyVisualsComponent component,
ref AppearanceChangeEvent args)
{
base.OnAppearanceChange(uid, component, ref args);
if (!args.Component.TryGetData(MechAssemblyVisuals.State, out int stage))
return;
var state = component.StatePrefix + stage;
args.Sprite?.LayerSetState(0, state);
}
}

View File

@ -0,0 +1,15 @@
namespace Content.Client.Mech;
/// <summary>
/// This is used for visualizing mech constructions
/// </summary>
[RegisterComponent]
public sealed class MechAssemblyVisualsComponent : Component
{
/// <summary>
/// The prefix that is followed by the number which
/// denotes the current state to use.
/// </summary>
[DataField("statePrefix", required: true)]
public string StatePrefix = string.Empty;
}

View File

@ -0,0 +1,12 @@
using Content.Shared.Mech.Components;
using Robust.Shared.GameStates;
namespace Content.Client.Mech;
/// <inheritdoc/>
[RegisterComponent, NetworkedComponent]
[ComponentReference(typeof(SharedMechComponent))]
public sealed class MechComponent : SharedMechComponent
{
}

View File

@ -0,0 +1,43 @@
using Content.Shared.Mech;
using Content.Shared.Mech.EntitySystems;
using Robust.Client.GameObjects;
using DrawDepth = Content.Shared.DrawDepth.DrawDepth;
namespace Content.Client.Mech;
/// <inheritdoc/>
public sealed class MechSystem : SharedMechSystem
{
/// <inheritdoc/>
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<MechComponent, AppearanceChangeEvent>(OnAppearanceChanged);
}
private void OnAppearanceChanged(EntityUid uid, MechComponent component, ref AppearanceChangeEvent args)
{
if (args.Sprite == null)
return;
if (!args.Sprite.TryGetLayer((int) MechVisualLayers.Base, out var layer))
return;
var state = component.BaseState;
var drawDepth = DrawDepth.Mobs;
if (component.BrokenState != null && args.Component.TryGetData(MechVisuals.Broken, out bool broken) && broken)
{
state = component.BrokenState;
drawDepth = DrawDepth.SmallMobs;
}
else if (component.OpenState != null && args.Component.TryGetData(MechVisuals.Open, out bool open) && open)
{
state = component.OpenState;
drawDepth = DrawDepth.SmallMobs;
}
layer.SetState(state);
args.Sprite.DrawDepth = (int) drawDepth;
}
}

View File

@ -0,0 +1,36 @@
using Content.Client.UserInterface.Fragments;
using Content.Shared.Mech;
using Robust.Client.GameObjects;
using Robust.Client.UserInterface;
namespace Content.Client.Mech.Ui.Equipment;
public sealed class MechGrabberUi : UIFragment
{
private MechGrabberUiFragment? _fragment;
public override Control GetUIFragmentRoot()
{
return _fragment!;
}
public override void Setup(BoundUserInterface userInterface, EntityUid? fragmentOwner)
{
if (fragmentOwner == null)
return;
_fragment = new MechGrabberUiFragment();
_fragment.OnEjectAction += e =>
{
userInterface.SendMessage(new MechGrabberEjectMessage(fragmentOwner.Value, e));
};
}
public override void UpdateState(BoundUserInterfaceState state)
{
if (state is not MechGrabberUiState grabberState)
return;
_fragment?.UpdateContents(grabberState);
}
}

View File

@ -0,0 +1,15 @@
<equipment:MechGrabberUiFragment
xmlns:equipment="clr-namespace:Content.Client.Mech.Ui.Equipment"
xmlns="https://spacestation14.io" Margin="1 0 2 0" HorizontalExpand="True" VerticalExpand="True">
<BoxContainer Orientation="Vertical"
HorizontalExpand="True"
VerticalExpand="True">
<ItemList Name="ItemList"
VerticalExpand="True"
MinHeight="120"
HorizontalExpand="True"
SelectMode="Button">
</ItemList>
<Label Name="SpaceLabel" HorizontalAlignment="Right" StyleClasses="LabelSubText"></Label>
</BoxContainer>
</equipment:MechGrabberUiFragment>

View File

@ -0,0 +1,35 @@
using Content.Shared.Mech;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML;
namespace Content.Client.Mech.Ui.Equipment;
[GenerateTypedNameReferences]
public sealed partial class MechGrabberUiFragment : BoxContainer
{
[Dependency] private readonly IEntityManager _entity = default!;
public event Action<EntityUid>? OnEjectAction;
public MechGrabberUiFragment()
{
RobustXamlLoader.Load(this);
IoCManager.InjectDependencies(this);
}
public void UpdateContents(MechGrabberUiState state)
{
SpaceLabel.Text = $"{state.Contents.Count}/{state.MaxContents}";
for (var i = 0; i < state.Contents.Count; i++)
{
var ent = state.Contents[i];
if (!_entity.TryGetComponent<MetaDataComponent>(ent, out var meta))
continue;
ItemList.AddItem(meta.EntityName);
ItemList[i].OnSelected += _ => OnEjectAction?.Invoke(ent);
}
}
}

View File

@ -0,0 +1,84 @@
using Content.Client.UserInterface.Fragments;
using Content.Shared.Mech;
using JetBrains.Annotations;
using Robust.Client.GameObjects;
namespace Content.Client.Mech.Ui;
[UsedImplicitly]
public sealed class MechBoundUserInterface : BoundUserInterface
{
[Dependency] private readonly IEntityManager _ent = default!;
private readonly EntityUid _mech;
private MechMenu? _menu;
public MechBoundUserInterface(ClientUserInterfaceComponent owner, Enum uiKey) : base(owner, uiKey)
{
IoCManager.InjectDependencies(this);
_mech = owner.Owner;
}
protected override void Open()
{
base.Open();
_menu = new(_mech);
_menu.OnClose += Close;
_menu.OpenCenteredLeft();
_menu.OnRemoveButtonPressed += uid =>
{
SendMessage(new MechEquipmentRemoveMessage(uid));
};
}
protected override void UpdateState(BoundUserInterfaceState state)
{
base.UpdateState(state);
if (state is not MechBoundUiState msg)
return;
UpdateEquipmentControls(msg);
_menu?.UpdateMechStats();
_menu?.UpdateEquipmentView();
}
public void UpdateEquipmentControls(MechBoundUiState state)
{
if (!_ent.TryGetComponent<MechComponent>(_mech, out var mechComp))
return;
foreach (var ent in mechComp.EquipmentContainer.ContainedEntities)
{
var ui = GetEquipmentUi(ent);
if (ui == null)
continue;
foreach (var (attached, estate) in state.EquipmentStates)
{
if (ent == attached)
ui.UpdateState(estate);
}
}
}
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
if (!disposing)
return;
_menu?.Close();
}
public UIFragment? GetEquipmentUi(EntityUid? uid)
{
var component = _ent.GetComponentOrNull<UIFragmentComponent>(uid);
component?.Ui?.Setup(this, uid);
return component?.Ui;
}
}

View File

@ -0,0 +1,26 @@
<Control xmlns="https://spacestation14.io"
xmlns:customControls="clr-namespace:Content.Client.Administration.UI.CustomControls">
<Button Disabled="True"
Margin="0 0 0 0"
HorizontalExpand="True"
VerticalExpand="True"
StyleClasses="ButtonSquare">
<BoxContainer Orientation="Vertical" HorizontalExpand="True" VerticalExpand="True" VerticalAlignment="Top">
<BoxContainer HorizontalExpand="True" VerticalExpand="True" Orientation="Horizontal">
<SpriteView
Name="EquipmentView"
OverrideDirection="South"
MinSize="32 32"
SetSize="32 32"
Scale="1 1"
RectClipContent="True"/>
<RichTextLabel Name="EquipmentName" VerticalAlignment="Center"/>
<BoxContainer Orientation="Vertical" HorizontalExpand="True" HorizontalAlignment="Right" VerticalAlignment="Center">
<TextureButton Name="RemoveButton" Scale="0.5 0.5"></TextureButton>
</BoxContainer>
</BoxContainer>
<customControls:HSeparator Name="Separator" StyleClasses="LowDivider" Visible="False"/>
<BoxContainer Name="CustomControlContainer" Margin="0 10 0 0" HorizontalExpand="True" VerticalExpand="True"></BoxContainer>
</BoxContainer>
</Button>
</Control>

View File

@ -0,0 +1,28 @@
using Robust.Client.AutoGenerated;
using Robust.Client.GameObjects;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.XAML;
namespace Content.Client.Mech.Ui;
[GenerateTypedNameReferences]
public sealed partial class MechEquipmentControl : Control
{
public event Action? OnRemoveButtonPressed;
public MechEquipmentControl(string itemName, SpriteComponent? sprite, Control? fragment)
{
RobustXamlLoader.Load(this);
EquipmentName.SetMessage(itemName);
EquipmentView.Sprite = sprite;
RemoveButton.TexturePath = "/Textures/Interface/Nano/cross.svg.png";
if (fragment != null)
{
Separator.Visible = true;
CustomControlContainer.AddChild(fragment);
}
RemoveButton.OnPressed += _ => OnRemoveButtonPressed?.Invoke();
}
}

View File

@ -0,0 +1,71 @@
<controls:FancyWindow xmlns="https://spacestation14.io"
xmlns:gfx="clr-namespace:Robust.Client.Graphics;assembly=Robust.Client"
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
Title="{Loc 'mech-menu-title'}"
MinSize="350 440"
SetSize="350 440">
<BoxContainer Orientation="Vertical" HorizontalExpand="True" VerticalExpand="True">
<BoxContainer Margin="10 10 10 10" Orientation="Horizontal" HorizontalExpand="True">
<BoxContainer Orientation="Vertical" HorizontalExpand="True">
<BoxContainer Orientation="Horizontal" HorizontalExpand="True">
<ProgressBar Name="IntegrityDisplayBar"
HorizontalExpand="True"
SetHeight="25"
MaxValue="1"
Value="0">
<Label Name="IntegrityDisplay"
HorizontalAlignment="Left"
Margin="5 0 0 0"
VerticalAlignment="Center" />
</ProgressBar>
</BoxContainer>
<BoxContainer Orientation="Horizontal" HorizontalExpand="True" Margin="0 5 0 0">
<ProgressBar Name="EnergyDisplayBar"
HorizontalExpand="True"
SetHeight="25"
MaxValue="1"
Value="0">
<Label Name="EnergyDisplay"
Margin="5 0 0 0"
HorizontalAlignment="Left"
VerticalAlignment="Center"></Label>
</ProgressBar>
</BoxContainer>
<Label Name="SlotDisplay"
HorizontalAlignment="Left"
Access="Public"
HorizontalExpand="True" />
</BoxContainer>
<SpriteView Name="MechView"
Margin="10 0 0 0"
VerticalAlignment="Center"
HorizontalAlignment="Right"
OverrideDirection="South"
SetSize="64 64"
MaxSize="64 64"
Scale="2 2">
</SpriteView>
</BoxContainer>
<BoxContainer VerticalExpand="True" Margin="10 0 10 10" Orientation="Vertical">
<PanelContainer VerticalExpand="True" MinSize="0 200">
<PanelContainer.PanelOverride>
<gfx:StyleBoxFlat BackgroundColor="#1B1B1E" />
</PanelContainer.PanelOverride>
<ScrollContainer
HScrollEnabled="False"
HorizontalExpand="True"
MinSize="100 256"
SizeFlagsStretchRatio="2"
VerticalExpand="True">
<BoxContainer
Name="EquipmentControlContainer"
MinSize="100 256"
Orientation="Vertical"
SizeFlagsStretchRatio="2"
VerticalExpand="True">
</BoxContainer>
</ScrollContainer>
</PanelContainer>
</BoxContainer>
</BoxContainer>
</controls:FancyWindow>

View File

@ -0,0 +1,72 @@
using Content.Client.UserInterface.Controls;
using Content.Client.UserInterface.Fragments;
using Content.Shared.Mech.Components;
using Robust.Client.AutoGenerated;
using Robust.Client.GameObjects;
using Robust.Client.UserInterface.XAML;
namespace Content.Client.Mech.Ui;
[GenerateTypedNameReferences]
public sealed partial class MechMenu : FancyWindow
{
[Dependency] private readonly IEntityManager _ent = default!;
private readonly EntityUid _mech;
public event Action<EntityUid>? OnRemoveButtonPressed;
public MechMenu(EntityUid mech)
{
RobustXamlLoader.Load(this);
IoCManager.InjectDependencies(this);
_mech = mech;
if (!_ent.TryGetComponent<SpriteComponent>(mech, out var sprite))
return;
MechView.Sprite = sprite;
}
public void UpdateMechStats()
{
if (!_ent.TryGetComponent<SharedMechComponent>(_mech, out var mechComp))
return;
var integrityPercent = mechComp.Integrity / mechComp.MaxIntegrity;
IntegrityDisplayBar.Value = integrityPercent.Float();
IntegrityDisplay.Text = Loc.GetString("mech-integrity-display", ("amount", (integrityPercent*100).Int()));
var energyPercent = mechComp.Energy / mechComp.MaxEnergy;
EnergyDisplayBar.Value = energyPercent.Float();
EnergyDisplay.Text = Loc.GetString("mech-energy-display", ("amount", (energyPercent*100).Int()));
SlotDisplay.Text = Loc.GetString("mech-slot-display",
("amount", mechComp.MaxEquipmentAmount - mechComp.EquipmentContainer.ContainedEntities.Count));
}
public void UpdateEquipmentView()
{
if (!_ent.TryGetComponent<MechComponent>(_mech, out var mechComp))
return;
EquipmentControlContainer.Children.Clear();
foreach (var ent in mechComp.EquipmentContainer.ContainedEntities)
{
if (!_ent.TryGetComponent<SpriteComponent>(ent, out var sprite) ||
!_ent.TryGetComponent<MetaDataComponent>(ent, out var metaData))
continue;
var uicomp = _ent.GetComponentOrNull<UIFragmentComponent>(ent);
var ui = uicomp?.Ui?.GetUIFragmentRoot();
var control = new MechEquipmentControl(metaData.EntityName, sprite, ui);
control.OnRemoveButtonPressed += () => OnRemoveButtonPressed?.Invoke(ent);
EquipmentControlContainer.AddChild(control);
}
}
}

View File

@ -18,7 +18,7 @@ public abstract class UIFragment
{
public abstract Control GetUIFragmentRoot();
public abstract void Setup(BoundUserInterface userInterface);
public abstract void Setup(BoundUserInterface userInterface, EntityUid? fragmentOwner);
public abstract void UpdateState(BoundUserInterfaceState state);

View File

@ -0,0 +1,71 @@
using Content.Server.Mech.Components;
using Content.Server.Mech.Systems;
using Content.Server.Power.Components;
using Content.Shared.Construction;
using JetBrains.Annotations;
using Robust.Server.Containers;
using Robust.Shared.Containers;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
namespace Content.Server.Construction.Completions;
/// <summary>
/// Creates the mech entity while transferring all relevant parts inside of it,
/// for right now, the cell that was used in construction.
/// </summary>
[UsedImplicitly, DataDefinition]
public sealed class BuildMech : IGraphAction
{
[DataField("mechPrototype", required: true, customTypeSerializer: typeof(PrototypeIdSerializer<EntityPrototype>))]
public string MechPrototype = string.Empty;
[DataField("container")]
public string Container = "battery-container";
public void PerformAction(EntityUid uid, EntityUid? userUid, IEntityManager entityManager)
{
if (!entityManager.TryGetComponent(uid, out ContainerManagerComponent? containerManager))
{
Logger.Warning($"Mech construct entity {uid} did not have a container manager! Aborting build mech action.");
return;
}
var containerSystem = entityManager.EntitySysManager.GetEntitySystem<ContainerSystem>();
var mechSys = entityManager.System<MechSystem>();
if (!containerSystem.TryGetContainer(uid, Container, out var container, containerManager))
{
Logger.Warning($"Mech construct entity {uid} did not have the specified '{Container}' container! Aborting build mech action.");
return;
}
if (container.ContainedEntities.Count != 1)
{
Logger.Warning($"Mech construct entity {uid} did not have exactly one item in the specified '{Container}' container! Aborting build mech action.");
}
var cell = container.ContainedEntities[0];
if (!entityManager.TryGetComponent<BatteryComponent>(cell, out var batteryComponent))
{
Logger.Warning($"Mech construct entity {uid} had an invalid entity in container \"{Container}\"! Aborting build mech action.");
return;
}
container.Remove(cell);
var transform = entityManager.GetComponent<TransformComponent>(uid);
var mech = entityManager.SpawnEntity(MechPrototype, transform.Coordinates);
if (entityManager.TryGetComponent<MechComponent>(mech, out var mechComp) && mechComp.BatterySlot.ContainedEntity == null)
{
mechSys.InsertBattery(mech, cell, mechComp, batteryComponent);
mechComp.BatterySlot.Insert(cell);
}
// Delete the original entity.
entityManager.DeleteEntity(uid);
}
}

View File

@ -0,0 +1,50 @@
using Content.Shared.Storage.Components;
using Content.Shared.Tag;
using Content.Shared.Tools;
using Robust.Shared.Containers;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.Dictionary;
namespace Content.Server.Mech.Components;
/// <summary>
/// A component used to create a mech chassis
/// after the correct parts have been placed inside
/// of it.
/// </summary>
/// <remarks>
/// The actual visualization of the parts being inserted is
/// done via <see cref="ItemMapperComponent"/>
/// </remarks>
[RegisterComponent]
public sealed class MechAssemblyComponent : Component
{
/// <summary>
/// The parts needed to be placed within the assembly,
/// stored as a tag and a bool tracking whether or not
/// they're present.
/// </summary>
[DataField("requiredParts", required: true, customTypeSerializer: typeof(PrototypeIdDictionarySerializer<bool, TagPrototype>))]
public Dictionary<string, bool> RequiredParts = new();
/// <summary>
/// The prototype spawned when the assembly is finished
/// </summary>
[DataField("finishedPrototype", required: true, customTypeSerializer: typeof(PrototypeIdSerializer<EntityPrototype>))]
public string FinishedPrototype = default!;
/// <summary>
/// The container that stores all of the parts when
/// they're being assembled.
/// </summary>
[ViewVariables]
public Container PartsContainer = default!;
/// <summary>
/// The quality of tool needed to remove all the parts
/// from the parts container.
/// </summary>
[DataField("qualityNeeded", customTypeSerializer: typeof(PrototypeIdSerializer<ToolQualityPrototype>))]
public string QualityNeeded = "Prying";
}

View File

@ -0,0 +1,118 @@
using System.Threading;
using Content.Server.Atmos;
using Content.Shared.Mech.Components;
using Robust.Shared.GameStates;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.List;
namespace Content.Server.Mech.Components;
/// <inheritdoc/>
[RegisterComponent, NetworkedComponent]
[ComponentReference(typeof(SharedMechComponent))]
public sealed class MechComponent : SharedMechComponent
{
/// <summary>
/// How long it takes to enter the mech.
/// </summary>
[DataField("entryDelay")]
public float EntryDelay = 3;
/// <summary>
/// How long it takes to pull *another person*
/// outside of the mech. You can exit instantly yourself.
/// </summary>
[DataField("exitDelay")]
public float ExitDelay = 3;
/// <summary>
/// How long it takes to pull out the battery.
/// </summary>
[DataField("batteryRemovalDelay")]
public float BatteryRemovalDelay = 2;
public CancellationTokenSource? EntryTokenSource;
/// <summary>
/// Whether or not the mech is airtight.
/// </summary>
/// <remarks>
/// This needs to be redone
/// when mech internals are added
/// </remarks>
[DataField("airtight"), ViewVariables(VVAccess.ReadWrite)]
public bool Airtight;
/// <summary>
/// The equipment that the mech initially has when it spawns.
/// Good for things like nukie mechs that start with guns.
/// </summary>
[DataField("startingEquipment", customTypeSerializer: typeof(PrototypeIdListSerializer<EntityPrototype>))]
public List<string> StartingEquipment = new();
/// <summary>
/// The battery the mech initially has when it spawns
/// Good for admemes and nukie mechs.
/// </summary>
[DataField("startingBattery", customTypeSerializer: typeof(PrototypeIdSerializer<EntityPrototype>))]
public string? StartingBattery;
//TODO: this doesn't support a tank implant for mechs or anything like that
[ViewVariables(VVAccess.ReadWrite)]
public GasMixture Air = new (GasMixVolume);
public const float GasMixVolume = 70f;
}
/// <summary>
/// Event raised when a person successfully enters a mech
/// </summary>
public sealed class MechEntryFinishedEvent : EntityEventArgs
{
public EntityUid User;
public MechEntryFinishedEvent(EntityUid user)
{
User = user;
}
}
/// <summary>
/// Event raised when a person fails to enter a mech
/// </summary>
public sealed class MechEntryCanclledEvent : EntityEventArgs
{
}
/// <summary>
/// Event raised when a person successfully removes someone from a mech
/// </summary>
public sealed class MechExitFinishedEvent : EntityEventArgs
{
}
/// <summary>
/// Event raised when a person fails to remove someone from a mech
/// </summary>
public sealed class MechExitCanclledEvent : EntityEventArgs
{
}
/// <summary>
/// Event raised when the battery is successfully removed from the mech
/// </summary>
public sealed class MechRemoveBatteryFinishedEvent : EntityEventArgs
{
}
/// <summary>
/// Event raised when the battery fails to be removed from the mech
/// </summary>
public sealed class MechRemoveBatteryCancelledEvent : EntityEventArgs
{
}

View File

@ -0,0 +1,74 @@
using System.Threading;
using Robust.Shared.Audio;
using Robust.Shared.Containers;
namespace Content.Server.Mech.Equipment.Components;
/// <summary>
/// A piece of mech equipment that grabs entities and stores them
/// inside of a container so large objects can be moved.
/// </summary>
[RegisterComponent]
public sealed class MechGrabberComponent : Component
{
/// <summary>
/// The change in energy after each grab.
/// </summary>
[DataField("grabEnergyDelta")]
public float GrabEnergyDelta = -30;
/// <summary>
/// How long does it take to grab something?
/// </summary>
[DataField("grabDelay")]
public float GrabDelay = 2.5f;
/// <summary>
/// The offset from the mech when an item is dropped.
/// This is here for things like lockers and vendors
/// </summary>
[DataField("depositOffset")]
public Vector2 DepositOffset = new(0, -1);
/// <summary>
/// The maximum amount of items that can be fit in this grabber
/// </summary>
[DataField("maxContents")]
public int MaxContents = 10;
/// <summary>
/// The sound played when a mech is grabbing something
/// </summary>
[DataField("grabSound")]
public SoundSpecifier GrabSound = new SoundPathSpecifier("/Audio/Mecha/sound_mecha_hydraulic.ogg");
public IPlayingAudioStream? AudioStream;
[ViewVariables(VVAccess.ReadWrite)]
public Container ItemContainer = default!;
public CancellationTokenSource? Token;
}
/// <summary>
/// Event raised on the user when the grab is complete.
/// </summary>
public sealed class MechGrabberGrabFinishedEvent : EntityEventArgs
{
/// <summary>
/// The thing that was grabbed.
/// </summary>
public EntityUid Grabbed;
public MechGrabberGrabFinishedEvent(EntityUid grabbed)
{
Grabbed = grabbed;
}
}
/// <summary>
/// Event raised on the user when the grab fails
/// </summary>
public sealed class MechGrabberGrabCancelledEvent : EntityEventArgs
{
}

View File

@ -0,0 +1,167 @@
using System.Linq;
using Content.Server.DoAfter;
using Content.Server.Interaction;
using Content.Server.Mech.Components;
using Content.Server.Mech.Equipment.Components;
using Content.Server.Mech.Systems;
using Content.Shared.Interaction;
using Content.Shared.Mech;
using Content.Shared.Mech.Equipment.Components;
using Content.Shared.Wall;
using Robust.Shared.Containers;
using Robust.Shared.Map;
namespace Content.Server.Mech.Equipment.EntitySystems;
/// <summary>
/// Handles <see cref="MechGrabberComponent"/> and all related UI logic
/// </summary>
public sealed class MechGrabberSystem : EntitySystem
{
[Dependency] private readonly SharedContainerSystem _container = default!;
[Dependency] private readonly MechSystem _mech = default!;
[Dependency] private readonly DoAfterSystem _doAfter = default!;
[Dependency] private readonly InteractionSystem _interaction = default!;
[Dependency] private readonly SharedAudioSystem _audio = default!;
/// <inheritdoc/>
public override void Initialize()
{
SubscribeLocalEvent<MechGrabberComponent, MechEquipmentUiMessageRelayEvent>(OnGrabberMessage);
SubscribeLocalEvent<MechGrabberComponent, ComponentStartup>(OnStartup);
SubscribeLocalEvent<MechGrabberComponent, MechEquipmentUiStateReadyEvent>(OnUiStateReady);
SubscribeLocalEvent<MechGrabberComponent, MechEquipmentRemovedEvent>(OnEquipmentRemoved);
SubscribeLocalEvent<MechGrabberComponent, AttemptRemoveMechEquipmentEvent>(OnAttemptRemove);
SubscribeLocalEvent<MechGrabberComponent, InteractNoHandEvent>(OnInteract);
SubscribeLocalEvent<MechGrabberComponent, MechGrabberGrabFinishedEvent>(OnGrabFinished);
SubscribeLocalEvent<MechGrabberComponent, MechGrabberGrabCancelledEvent>(OnGrabCancelled);
}
private void OnGrabberMessage(EntityUid uid, MechGrabberComponent component, MechEquipmentUiMessageRelayEvent args)
{
if (args.Message is not MechGrabberEjectMessage msg)
return;
if (!TryComp<MechEquipmentComponent>(uid, out var equipmentComponent) ||
equipmentComponent.EquipmentOwner == null)
return;
var mech = equipmentComponent.EquipmentOwner.Value;
var targetCoords = new EntityCoordinates(mech, component.DepositOffset);
if (!_interaction.InRangeUnobstructed(mech, targetCoords))
return;
if (!component.ItemContainer.Contains(msg.Item))
return;
RemoveItem(uid, mech, msg.Item, component);
}
/// <summary>
/// Removes an item from the grabber's container
/// </summary>
/// <param name="uid">The mech grabber</param>
/// <param name="mech">The mech it belongs to</param>
/// <param name="toRemove">The item being removed</param>
/// <param name="component"></param>
public void RemoveItem(EntityUid uid, EntityUid mech, EntityUid toRemove, MechGrabberComponent? component = null)
{
if (!Resolve(uid, ref component))
return;
component.ItemContainer.Remove(toRemove);
var mechxform = Transform(mech);
var xform = Transform(toRemove);
xform.AttachToGridOrMap();
xform.WorldPosition = mechxform.WorldPosition + mechxform.WorldRotation.RotateVec(component.DepositOffset);
xform.WorldRotation = Angle.Zero;
_mech.UpdateUserInterface(mech);
}
private void OnEquipmentRemoved(EntityUid uid, MechGrabberComponent component, ref MechEquipmentRemovedEvent args)
{
if (!TryComp<MechEquipmentComponent>(uid, out var equipmentComponent) ||
equipmentComponent.EquipmentOwner == null)
return;
var mech = equipmentComponent.EquipmentOwner.Value;
var allItems = new List<EntityUid>(component.ItemContainer.ContainedEntities);
foreach (var item in allItems)
{
RemoveItem(uid, mech, item, component);
}
}
private void OnAttemptRemove(EntityUid uid, MechGrabberComponent component, ref AttemptRemoveMechEquipmentEvent args)
{
args.Cancelled = component.ItemContainer.ContainedEntities.Any();
}
private void OnStartup(EntityUid uid, MechGrabberComponent component, ComponentStartup args)
{
component.ItemContainer = _container.EnsureContainer<Container>(uid, "item-container");
}
private void OnUiStateReady(EntityUid uid, MechGrabberComponent component, MechEquipmentUiStateReadyEvent args)
{
var state = new MechGrabberUiState
{
Contents = component.ItemContainer.ContainedEntities.ToList(),
MaxContents = component.MaxContents
};
args.States.Add(uid, state);
}
private void OnInteract(EntityUid uid, MechGrabberComponent component, InteractNoHandEvent args)
{
if (args.Handled || args.Target == null)
return;
var xform = Transform(args.Target.Value);
if (xform.Anchored || HasComp<WallMountComponent>(args.Target.Value))
return;
if (component.ItemContainer.ContainedEntities.Count >= component.MaxContents)
return;
if (!TryComp<MechComponent>(args.User, out var mech))
return;
if (mech.Energy + component.GrabEnergyDelta < 0)
return;
if (component.Token != null)
return;
args.Handled = true;
component.Token = new();
component.AudioStream = _audio.PlayPvs(component.GrabSound, uid);
_doAfter.DoAfter(new DoAfterEventArgs(args.User, component.GrabDelay, component.Token.Token, args.Target, uid)
{
BreakOnTargetMove = true,
BreakOnUserMove = true,
UsedFinishedEvent = new MechGrabberGrabFinishedEvent(args.Target.Value),
UserCancelledEvent = new MechGrabberGrabCancelledEvent()
});
}
private void OnGrabFinished(EntityUid uid, MechGrabberComponent component, MechGrabberGrabFinishedEvent args)
{
component.Token = null;
if (!TryComp<MechEquipmentComponent>(uid, out var equipmentComponent) || equipmentComponent.EquipmentOwner == null)
return;
if (!_mech.TryChangeEnergy(equipmentComponent.EquipmentOwner.Value, component.GrabEnergyDelta))
return;
component.ItemContainer.Insert(args.Grabbed);
_mech.UpdateUserInterface(equipmentComponent.EquipmentOwner.Value);
}
private void OnGrabCancelled(EntityUid uid, MechGrabberComponent component, MechGrabberGrabCancelledEvent args)
{
component.AudioStream?.Stop();
component.Token = null;
}
}

View File

@ -0,0 +1,64 @@
using Content.Server.Mech.Components;
using Content.Shared.Interaction;
using Content.Shared.Tag;
using Content.Shared.Tools.Components;
using Robust.Server.Containers;
using Robust.Shared.Containers;
namespace Content.Server.Mech.Systems;
/// <summary>
/// Handles <see cref="MechAssemblyComponent"/> and the insertion
/// and removal of parts from the assembly.
/// </summary>
public sealed class MechAssemblySystem : EntitySystem
{
[Dependency] private readonly ContainerSystem _container = default!;
/// <inheritdoc/>
public override void Initialize()
{
SubscribeLocalEvent<MechAssemblyComponent, ComponentInit>(OnInit);
SubscribeLocalEvent<MechAssemblyComponent, InteractUsingEvent>(OnInteractUsing);
}
private void OnInit(EntityUid uid, MechAssemblyComponent component, ComponentInit args)
{
component.PartsContainer = _container.EnsureContainer<Container>(uid, "mech-assembly-container");
}
private void OnInteractUsing(EntityUid uid, MechAssemblyComponent component, InteractUsingEvent args)
{
if (TryComp<ToolComponent>(args.Used, out var toolComp) && toolComp.Qualities.Contains(component.QualityNeeded))
{
foreach (var tag in component.RequiredParts.Keys)
{
component.RequiredParts[tag] = false;
}
_container.EmptyContainer(component.PartsContainer);
return;
}
if (!TryComp<TagComponent>(args.Used, out var tagComp))
return;
foreach (var (tag, val) in component.RequiredParts)
{
if (!val && tagComp.Tags.Contains(tag))
{
component.RequiredParts[tag] = true;
component.PartsContainer.Insert(args.Used);
break;
}
}
//check to see if we have all the parts
foreach (var val in component.RequiredParts.Values)
{
if (!val)
return;
}
Spawn(component.FinishedPrototype, Transform(uid).Coordinates);
EntityManager.DeleteEntity(uid);
}
}

View File

@ -0,0 +1,73 @@
using Content.Server.DoAfter;
using Content.Server.Mech.Components;
using Content.Server.Popups;
using Content.Shared.Interaction;
using Content.Shared.Mech.Equipment.Components;
using Robust.Shared.Player;
namespace Content.Server.Mech.Systems;
/// <summary>
/// Handles the insertion of mech equipment into mechs.
/// </summary>
public sealed class MechEquipmentSystem : EntitySystem
{
[Dependency] private readonly MechSystem _mech = default!;
[Dependency] private readonly DoAfterSystem _doAfter = default!;
[Dependency] private readonly PopupSystem _popup = default!;
/// <inheritdoc/>
public override void Initialize()
{
SubscribeLocalEvent<MechEquipmentComponent, AfterInteractEvent>(OnUsed);
SubscribeLocalEvent<MechEquipmentComponent, MechEquipmentInstallFinished>(OnFinished);
SubscribeLocalEvent<MechEquipmentComponent, MechEquipmentInstallCancelled>(OnCancelled);
}
private void OnUsed(EntityUid uid, MechEquipmentComponent component, AfterInteractEvent args)
{
if (component.TokenSource != null)
return;
if (args.Handled || !args.CanReach || args.Target == null)
return;
var mech = args.Target.Value;
if (!TryComp<MechComponent>(mech, out var mechComp))
return;
if (args.User == mechComp.PilotSlot.ContainedEntity)
return;
if (mechComp.EquipmentContainer.ContainedEntities.Count >= mechComp.MaxEquipmentAmount)
return;
if (mechComp.EquipmentWhitelist != null && !mechComp.EquipmentWhitelist.IsValid(uid))
return;
_popup.PopupEntity(Loc.GetString("mech-equipment-begin-install", ("item", uid)), mech, Filter.Pvs(mech));
component.TokenSource = new();
_doAfter.DoAfter(new DoAfterEventArgs(args.User, component.InstallDuration, component.TokenSource.Token, mech, uid)
{
BreakOnStun = true,
BreakOnTargetMove = true,
BreakOnUserMove = true,
UsedFinishedEvent = new MechEquipmentInstallFinished(mech),
UsedCancelledEvent = new MechEquipmentInstallCancelled()
});
}
private void OnFinished(EntityUid uid, MechEquipmentComponent component, MechEquipmentInstallFinished args)
{
component.TokenSource = null;
_popup.PopupEntity(Loc.GetString("mech-equipment-finish-install", ("item", uid)), args.Mech, Filter.Pvs(args.Mech));
_mech.InsertEquipment(args.Mech, uid);
}
private void OnCancelled(EntityUid uid, MechEquipmentComponent component, MechEquipmentInstallCancelled args)
{
component.TokenSource = null;
}
}

View File

@ -0,0 +1,428 @@
using System.Linq;
using System.Threading;
using Content.Server.Atmos.EntitySystems;
using Content.Server.DoAfter;
using Content.Server.Mech.Components;
using Content.Server.Power.Components;
using Content.Server.Wires;
using Content.Shared.Damage;
using Content.Shared.FixedPoint;
using Content.Shared.Interaction;
using Content.Shared.Mech;
using Content.Shared.Mech.Components;
using Content.Shared.Mech.EntitySystems;
using Content.Shared.Tools.Components;
using Content.Shared.Verbs;
using Robust.Server.Containers;
using Robust.Server.GameObjects;
using Robust.Shared.Map;
namespace Content.Server.Mech.Systems;
/// <inheritdoc/>
public sealed class MechSystem : SharedMechSystem
{
[Dependency] private readonly AtmosphereSystem _atmosphere = default!;
[Dependency] private readonly ContainerSystem _container = default!;
[Dependency] private readonly DamageableSystem _damageable = default!;
[Dependency] private readonly DoAfterSystem _doAfter = default!;
[Dependency] private readonly IMapManager _map = default!;
[Dependency] private readonly UserInterfaceSystem _ui = default!;
private ISawmill _sawmill = default!;
/// <inheritdoc/>
public override void Initialize()
{
base.Initialize();
_sawmill = Logger.GetSawmill("mech");
SubscribeLocalEvent<MechComponent, InteractUsingEvent>(OnInteractUsing);
SubscribeLocalEvent<MechComponent, MapInitEvent>(OnMapInit);
SubscribeLocalEvent<MechComponent, GetVerbsEvent<AlternativeVerb>>(OnAlternativeVerb);
SubscribeLocalEvent<MechComponent, MechOpenUiEvent>(OnOpenUi);
SubscribeLocalEvent<MechComponent, MechEntryFinishedEvent>(OnEntryFinished);
SubscribeLocalEvent<MechComponent, MechEntryCanclledEvent>(OnEntryExitCancelled);
SubscribeLocalEvent<MechComponent, MechExitFinishedEvent>(OnExitFinished);
SubscribeLocalEvent<MechComponent, MechExitCanclledEvent>(OnEntryExitCancelled);
SubscribeLocalEvent<MechComponent, MechRemoveBatteryFinishedEvent>(OnRemoveBatteryFinished);
SubscribeLocalEvent<MechComponent, MechRemoveBatteryCancelledEvent>(OnRemoveBatteryCancelled);
SubscribeLocalEvent<MechComponent, DamageChangedEvent>(OnDamageChanged);
SubscribeLocalEvent<MechComponent, MechEquipmentRemoveMessage>(OnRemoveEquipmentMessage);
SubscribeLocalEvent<MechPilotComponent, InhaleLocationEvent>(OnInhale);
SubscribeLocalEvent<MechPilotComponent, ExhaleLocationEvent>(OnExhale);
SubscribeLocalEvent<MechPilotComponent, AtmosExposedGetAirEvent>(OnExpose);
#region Equipment UI message relays
SubscribeLocalEvent<MechComponent, MechGrabberEjectMessage>(RecieveEquipmentUiMesssages);
#endregion
}
private void OnInteractUsing(EntityUid uid, MechComponent component, InteractUsingEvent args)
{
if (TryComp<WiresComponent>(uid, out var wires) && !wires.IsPanelOpen)
return;
if (component.BatterySlot.ContainedEntity == null && TryComp<BatteryComponent>(args.Used, out var battery))
{
InsertBattery(uid, args.Used, component, battery);
return;
}
if (component.EntryTokenSource == null &&
TryComp<ToolComponent>(args.Used, out var tool) &&
tool.Qualities.Contains("Prying") &&
component.BatterySlot.ContainedEntity != null)
{
component.EntryTokenSource = new();
_doAfter.DoAfter(new DoAfterEventArgs(args.User, component.BatteryRemovalDelay, component.EntryTokenSource.Token, uid, args.Target)
{
BreakOnTargetMove = true,
BreakOnUserMove = true,
TargetFinishedEvent = new MechRemoveBatteryFinishedEvent(),
TargetCancelledEvent = new MechRemoveBatteryCancelledEvent()
});
}
}
private void OnRemoveBatteryFinished(EntityUid uid, MechComponent component, MechRemoveBatteryFinishedEvent args)
{
component.EntryTokenSource = null;
RemoveBattery(uid, component);
}
private void OnRemoveBatteryCancelled(EntityUid uid, MechComponent component, MechRemoveBatteryCancelledEvent args)
{
component.EntryTokenSource = null;
}
private void OnMapInit(EntityUid uid, MechComponent component, MapInitEvent args)
{
var xform = Transform(uid);
foreach (var ent in component.StartingEquipment.Select(equipment => Spawn(equipment, xform.Coordinates)))
{
InsertEquipment(uid, ent, component);
}
component.Integrity = component.MaxIntegrity;
component.Energy = component.MaxEnergy;
if (component.StartingBattery != null)
{
var battery = Spawn(component.StartingBattery, Transform(uid).Coordinates);
InsertBattery(uid, battery, component);
}
Dirty(component);
}
private void OnRemoveEquipmentMessage(EntityUid uid, SharedMechComponent component, MechEquipmentRemoveMessage args)
{
if (!Exists(args.Equipment) || Deleted(args.Equipment))
return;
if (!component.EquipmentContainer.ContainedEntities.Contains(args.Equipment))
return;
RemoveEquipment(uid, args.Equipment, component);
}
private void OnOpenUi(EntityUid uid, MechComponent component, MechOpenUiEvent args)
{
args.Handled = true;
ToggleMechUi(uid, component);
}
private void OnAlternativeVerb(EntityUid uid, MechComponent component, GetVerbsEvent<AlternativeVerb> args)
{
if (!args.CanAccess || !args.CanInteract || component.Broken)
return;
if (CanInsert(uid, args.User, component))
{
var enterVerb = new AlternativeVerb
{
Text = Loc.GetString("mech-verb-enter"),
Act = () =>
{
if (component.EntryTokenSource != null)
return;
component.EntryTokenSource = new CancellationTokenSource();
_doAfter.DoAfter(new DoAfterEventArgs(args.User, component.EntryDelay, component.EntryTokenSource.Token, uid)
{
BreakOnUserMove = true,
BreakOnStun = true,
TargetFinishedEvent = new MechEntryFinishedEvent(args.User),
TargetCancelledEvent = new MechEntryCanclledEvent()
});
}
};
var openUiVerb = new AlternativeVerb //can't hijack someone else's mech
{
Act = () => ToggleMechUi(uid, component, args.User),
Text = Loc.GetString("mech-ui-open-verb")
};
args.Verbs.Add(enterVerb);
args.Verbs.Add(openUiVerb);
}
else if (!IsEmpty(component))
{
var ejectVerb = new AlternativeVerb
{
Text = Loc.GetString("mech-verb-exit"),
Priority = 1, // Promote to top to make ejecting the ALT-click action
Act = () =>
{
if (component.EntryTokenSource != null)
return;
if (args.User == component.PilotSlot.ContainedEntity)
{
TryEject(uid, component);
return;
}
component.EntryTokenSource = new CancellationTokenSource();
_doAfter.DoAfter(new DoAfterEventArgs(args.User, component.ExitDelay, component.EntryTokenSource.Token, uid)
{
BreakOnUserMove = true,
BreakOnTargetMove = true,
BreakOnStun = true,
TargetFinishedEvent = new MechExitFinishedEvent(),
TargetCancelledEvent = new MechExitCanclledEvent()
});
}
};
args.Verbs.Add(ejectVerb);
}
}
private void OnEntryFinished(EntityUid uid, MechComponent component, MechEntryFinishedEvent args)
{
component.EntryTokenSource = null;
TryInsert(uid, args.User, component);
}
private void OnExitFinished(EntityUid uid, MechComponent component, MechExitFinishedEvent args)
{
component.EntryTokenSource = null;
TryEject(uid, component);
}
private void OnEntryExitCancelled(EntityUid uid, MechComponent component, EntityEventArgs args)
{
component.EntryTokenSource = null;
}
private void OnDamageChanged(EntityUid uid, SharedMechComponent component, DamageChangedEvent args)
{
var integrity = component.MaxIntegrity - args.Damageable.TotalDamage;
SetIntegrity(uid, integrity, component);
if (args.DamageIncreased &&
args.DamageDelta != null &&
component.PilotSlot.ContainedEntity != null)
{
var damage = args.DamageDelta * component.MechToPilotDamageMultiplier;
_damageable.TryChangeDamage(component.PilotSlot.ContainedEntity, damage);
}
}
private void ToggleMechUi(EntityUid uid, MechComponent? component = null, EntityUid? user = null)
{
if (!Resolve(uid, ref component))
return;
user ??= component.PilotSlot.ContainedEntity;
if (user == null)
return;
if (!TryComp<ActorComponent>(user, out var actor))
return;
_ui.TryToggleUi(uid, MechUiKey.Key, actor.PlayerSession);
UpdateUserInterface(uid, component);
}
private void RecieveEquipmentUiMesssages<T>(EntityUid uid, MechComponent component, T args) where T : MechEquipmentUiMessage
{
var ev = new MechEquipmentUiMessageRelayEvent(args);
var allEquipment = new List<EntityUid>(component.EquipmentContainer.ContainedEntities);
foreach (var equipment in allEquipment)
{
if (args.Equipment == equipment)
RaiseLocalEvent(equipment, ev);
}
}
public override void UpdateUserInterface(EntityUid uid, SharedMechComponent? component = null)
{
if (!Resolve(uid, ref component))
return;
base.UpdateUserInterface(uid, component);
var ev = new MechEquipmentUiStateReadyEvent();
foreach (var ent in component.EquipmentContainer.ContainedEntities)
{
RaiseLocalEvent(ent, ev);
}
var state = new MechBoundUiState
{
EquipmentStates = ev.States
};
var ui = _ui.GetUi(uid, MechUiKey.Key);
_ui.SetUiState(ui, state);
}
public override bool TryInsert(EntityUid uid, EntityUid? toInsert, SharedMechComponent? component = null)
{
if (!Resolve(uid, ref component))
return false;
if (!base.TryInsert(uid, toInsert, component))
return false;
var mech = (MechComponent) component;
if (mech.Airtight)
{
var coordinates = Transform(uid).MapPosition;
if (_map.TryFindGridAt(coordinates, out var grid))
{
var tile = grid.GetTileRef(coordinates);
if (_atmosphere.GetTileMixture(tile.GridUid, null, tile.GridIndices, true) is {} environment)
{
_atmosphere.Merge(mech.Air, environment.RemoveVolume(MechComponent.GasMixVolume));
}
}
}
return true;
}
public override bool TryEject(EntityUid uid, SharedMechComponent? component = null)
{
if (!Resolve(uid, ref component))
return false;
if (!base.TryEject(uid, component))
return false;
var mech = (MechComponent) component;
if (mech.Airtight)
{
var coordinates = Transform(uid).MapPosition;
if (_map.TryFindGridAt(coordinates, out var grid))
{
var tile = grid.GetTileRef(coordinates);
if (_atmosphere.GetTileMixture(tile.GridUid, null, tile.GridIndices, true) is {} environment)
{
_atmosphere.Merge(environment, mech.Air);
mech.Air.Clear();
}
}
}
return true;
}
public override void BreakMech(EntityUid uid, SharedMechComponent? component = null)
{
base.BreakMech(uid, component);
_ui.TryCloseAll(uid, MechUiKey.Key);
}
public override bool TryChangeEnergy(EntityUid uid, FixedPoint2 delta, SharedMechComponent? component = null)
{
if (!Resolve(uid, ref component))
return false;
if (!base.TryChangeEnergy(uid, delta, component))
return false;
var battery = component.BatterySlot.ContainedEntity;
if (battery == null)
return false;
if (!TryComp<BatteryComponent>(battery, out var batteryComp))
return false;
batteryComp.CurrentCharge = batteryComp.CurrentCharge + delta.Float();
if (batteryComp.CurrentCharge != component.Energy) //if there's a discrepency, we have to resync them
{
_sawmill.Debug($"Battery charge was not equal to mech charge. Battery {batteryComp.CurrentCharge}. Mech {component.Energy}");
component.Energy = batteryComp.CurrentCharge;
Dirty(component);
}
return true;
}
public void InsertBattery(EntityUid uid, EntityUid toInsert, MechComponent? component = null, BatteryComponent? battery = null)
{
if (!Resolve(uid, ref component, false))
return;
if (!Resolve(toInsert, ref battery, false))
return;
component.BatterySlot.Insert(toInsert);
component.Energy = battery.CurrentCharge;
component.MaxEnergy = battery.MaxCharge;
Dirty(component);
UpdateUserInterface(uid, component);
}
public void RemoveBattery(EntityUid uid, MechComponent? component = null)
{
if (!Resolve(uid, ref component))
return;
_container.EmptyContainer(component.BatterySlot);
component.Energy = 0;
component.MaxEnergy = 0;
Dirty(component);
UpdateUserInterface(uid, component);
}
#region Atmos Handling
private void OnInhale(EntityUid uid, MechPilotComponent component, InhaleLocationEvent args)
{
if (!TryComp<MechComponent>(component.Mech, out var mech))
return;
if (mech.Airtight)
args.Gas = mech.Air;
}
private void OnExhale(EntityUid uid, MechPilotComponent component, ExhaleLocationEvent args)
{
if (!TryComp<MechComponent>(component.Mech, out var mech))
return;
if (mech.Airtight)
args.Gas = mech.Air;
}
private void OnExpose(EntityUid uid, MechPilotComponent component, ref AtmosExposedGetAirEvent args)
{
if (args.Handled)
return;
if (!TryComp<MechComponent>(component.Mech, out var mech))
return;
args.Gas = mech.Airtight ? mech.Air : _atmosphere.GetContainingMixture(component.Mech);
args.Handled = true;
}
#endregion
}

View File

@ -59,9 +59,9 @@ public sealed partial class RevenantSystem
private void OnInteract(EntityUid uid, RevenantComponent component, InteractNoHandEvent args)
{
var target = args.Target;
if (target == args.User)
if (args.Target == args.User || args.Target == null)
return;
var target = args.Target.Value;
if (HasComp<PoweredLightComponent>(target))
{
@ -72,9 +72,6 @@ public sealed partial class RevenantSystem
if (!HasComp<MobStateComponent>(target) || !HasComp<HumanoidComponent>(target) || HasComp<RevenantComponent>(target))
return;
if (!_interact.InRangeUnobstructed(uid, target))
return;
args.Handled = true;
if (!TryComp<EssenceComponent>(target, out var essence) || !essence.SearchComplete)
{

View File

@ -4,7 +4,6 @@ using Content.Shared.Emoting;
using Content.Shared.Interaction;
using Content.Shared.Interaction.Events;
using Content.Shared.Item;
using Content.Shared.Movement;
using Content.Shared.Movement.Components;
using Content.Shared.Movement.Events;
using Content.Shared.Speech;
@ -148,7 +147,11 @@ namespace Content.Shared.ActionBlocker
public bool CanAttack(EntityUid uid, EntityUid? target = null)
{
if (_container.IsEntityInContainer(uid))
return false;
{
var containerEv = new CanAttackFromContainerEvent(uid, target);
RaiseLocalEvent(uid, containerEv);
return containerEv.CanAttack;
}
var ev = new AttackAttemptEvent(uid, target);
RaiseLocalEvent(uid, ev);

View File

@ -0,0 +1,34 @@
using Robust.Shared.GameStates;
using Robust.Shared.Serialization;
namespace Content.Shared.Interaction.Components;
/// <summary>
/// Relays an entities interactions to another entity.
/// This doesn't raise the same events, but just relays
/// the clicks of the mouse.
/// </summary>
[RegisterComponent, NetworkedComponent]
[Access(typeof(SharedInteractionSystem))]
public sealed class InteractionRelayComponent : Component
{
/// <summary>
/// The entity the interactions are being relayed to.
/// </summary>
[ViewVariables]
public EntityUid? RelayEntity;
}
/// <summary>
/// Contains network state for <see cref="InteractionRelayComponent"/>
/// </summary>
[Serializable, NetSerializable]
public sealed class InteractionRelayComponentState : ComponentState
{
public EntityUid? RelayEntity;
public InteractionRelayComponentState(EntityUid? relayEntity)
{
RelayEntity = relayEntity;
}
}

View File

@ -18,4 +18,20 @@ namespace Content.Shared.Interaction.Events
Target = target;
}
}
/// <summary>
/// Raised directed at an entity to check if they can attack while inside of a container.
/// </summary>
public sealed class CanAttackFromContainerEvent : EntityEventArgs
{
public EntityUid Uid;
public EntityUid? Target;
public bool CanAttack = false;
public CanAttackFromContainerEvent(EntityUid uid, EntityUid? target = null)
{
Uid = uid;
Target = target;
}
}
}

View File

@ -1,4 +1,5 @@
using JetBrains.Annotations;
using Robust.Shared.Map;
namespace Content.Shared.Interaction
{
@ -37,44 +38,57 @@ namespace Content.Shared.Interaction
}
}
public sealed class InteractNoHandEvent : HandledEntityEventArgs, ITargetedInteractEventArgs
/// <summary>
/// Low-level interaction event used for entities without hands.
/// </summary>
/// <remarks>
/// SHIT IS CURSED.
/// </remarks>
//TODO: KILLLLLLL
public sealed class InteractNoHandEvent : HandledEntityEventArgs
{
/// <summary>
/// Entity that triggered the interaction.
/// </summary>
public EntityUid User { get; }
public EntityUid User;
/// <summary>
/// Entity that was interacted on.
/// </summary>
public EntityUid Target { get; }
public EntityUid? Target;
public InteractNoHandEvent(EntityUid user, EntityUid target)
public EntityCoordinates ClickLocation;
public InteractNoHandEvent(EntityUid user, EntityUid? target, EntityCoordinates clickLocation)
{
User = user;
Target = target;
ClickLocation = clickLocation;
}
}
/// <summary>
/// Reverse of the InteractNoHandEvent - raised on what was interacted on, rather than the other way around.
/// </summary>
public sealed class InteractedNoHandEvent : HandledEntityEventArgs, ITargetedInteractEventArgs
public sealed class InteractedNoHandEvent : HandledEntityEventArgs
{
/// <summary>
/// Entity that was interacted on
/// </summary>
public EntityUid Target { get; }
public EntityUid Target;
/// <summary>
/// Entity that triggered this interaction
/// </summary>
public EntityUid User { get; }
public EntityUid User;
public InteractedNoHandEvent(EntityUid target, EntityUid user)
public EntityCoordinates ClickLocation;
public InteractedNoHandEvent(EntityUid target, EntityUid user, EntityCoordinates clickLocation)
{
Target = target;
User = user;
ClickLocation = clickLocation;
}
}
}

View File

@ -0,0 +1,35 @@
using Content.Shared.Interaction.Components;
using Robust.Shared.GameStates;
namespace Content.Shared.Interaction;
public abstract partial class SharedInteractionSystem
{
public void InitializeRelay()
{
SubscribeLocalEvent<InteractionRelayComponent, ComponentGetState>(OnGetState);
SubscribeLocalEvent<InteractionRelayComponent, ComponentHandleState>(OnHandleState);
}
private void OnGetState(EntityUid uid, InteractionRelayComponent component, ref ComponentGetState args)
{
args.State = new InteractionRelayComponentState(component.RelayEntity);
}
private void OnHandleState(EntityUid uid, InteractionRelayComponent component, ref ComponentHandleState args)
{
if (args.Current is not InteractionRelayComponentState state)
return;
component.RelayEntity = state.RelayEntity;
}
public void SetRelay(EntityUid uid, EntityUid? relayEntity, InteractionRelayComponent? component = null)
{
if (!Resolve(uid, ref component))
return;
component.RelayEntity = relayEntity;
Dirty(component);
}
}

View File

@ -40,7 +40,7 @@ namespace Content.Shared.Interaction
/// Governs interactions during clicking on entities
/// </summary>
[UsedImplicitly]
public abstract class SharedInteractionSystem : EntitySystem
public abstract partial class SharedInteractionSystem : EntitySystem
{
[Dependency] private readonly IGameTiming _gameTiming = default!;
[Dependency] private readonly IMapManager _mapManager = default!;
@ -81,6 +81,8 @@ namespace Content.Shared.Interaction
.Bind(ContentKeyFunctions.TryPullObject,
new PointerInputCmdHandler(HandleTryPullObject))
.Register<SharedInteractionSystem>();
InitializeRelay();
}
public override void Shutdown()
@ -224,6 +226,11 @@ namespace Content.Shared.Interaction
bool checkAccess = true,
bool checkCanUse = true)
{
if (TryComp<InteractionRelayComponent>(user, out var relay) && relay.RelayEntity is not null)
{
UserInteraction(relay.RelayEntity.Value, coordinates, target, altInteract, checkCanInteract, checkAccess, checkCanUse);
}
if (target != null && Deleted(target.Value))
return;
@ -255,25 +262,25 @@ namespace Content.Shared.Interaction
&& !CanAccessViaStorage(user, target.Value))
return;
var inRangeUnobstructed = target == null
? !checkAccess || InRangeUnobstructed(user, coordinates)
: !checkAccess || InRangeUnobstructed(user, target.Value); // permits interactions with wall mounted entities
// Does the user have hands?
if (!TryComp(user, out SharedHandsComponent? hands) || hands.ActiveHand == null)
{
var ev = new InteractNoHandEvent(user, target, coordinates);
RaiseLocalEvent(user, ev);
if (target != null)
{
var ev = new InteractNoHandEvent(user, target.Value);
RaiseLocalEvent(user, ev);
var interactedEv = new InteractedNoHandEvent(target.Value, user);
var interactedEv = new InteractedNoHandEvent(target.Value, user, coordinates);
RaiseLocalEvent(target.Value, interactedEv);
DoContactInteraction(user, target.Value, ev);
}
return;
}
var inRangeUnobstructed = target == null
? !checkAccess || InRangeUnobstructed(user, coordinates)
: !checkAccess || InRangeUnobstructed(user, target.Value); // permits interactions with wall mounted entities
// empty-hand interactions
if (hands.ActiveHandEntity is not { } held)
{

View File

@ -0,0 +1,26 @@
using Robust.Shared.GameStates;
using Robust.Shared.Serialization;
namespace Content.Shared.Mech.Components;
/// <summary>
/// Attached to entities piloting a <see cref="SharedMechComponent"/>
/// </summary>
/// <remarks>
/// Get in the robot, Shinji
/// </remarks>
[RegisterComponent, NetworkedComponent]
public sealed class MechPilotComponent : Component
{
/// <summary>
/// The mech being piloted
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
public EntityUid Mech;
}
[Serializable, NetSerializable]
public sealed class MechPilotComponentState : ComponentState
{
public EntityUid Mech;
}

View File

@ -0,0 +1,132 @@
using Content.Shared.Actions.ActionTypes;
using Content.Shared.FixedPoint;
using Content.Shared.Whitelist;
using Robust.Shared.Containers;
using Robust.Shared.Serialization;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
namespace Content.Shared.Mech.Components;
/// <summary>
/// A large, pilotable machine that has equipment that is
/// powered via an internal battery.
/// </summary>
public abstract class SharedMechComponent : Component
{
/// <summary>
/// How much "health" the mech has left.
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
public FixedPoint2 Integrity;
/// <summary>
/// The maximum amount of damage the mech can take.
/// </summary>
[DataField("maxIntegrity")]
public FixedPoint2 MaxIntegrity = 250;
/// <summary>
/// How much energy the mech has.
/// Derived from the currently inserted battery.
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
public FixedPoint2 Energy = 0;
/// <summary>
/// The maximum amount of energy the mech can have.
/// Derived from the currently inserted battery.
/// </summary>
[DataField("maxEnergy")]
public FixedPoint2 MaxEnergy = 0;
/// <summary>
/// The slot the battery is stored in.
/// </summary>
[ViewVariables]
public ContainerSlot BatterySlot = default!;
[ViewVariables]
public readonly string BatterySlotId = "mech-battery-slot";
/// <summary>
/// A multiplier used to calculate how much of the damage done to a mech
/// is transfered to the pilot
/// </summary>
[DataField("mechToPilotDamageMultiplier")]
public float MechToPilotDamageMultiplier;
/// <summary>
/// Whether the mech has been destroyed and is no longer pilotable.
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
public bool Broken = false;
/// <summary>
/// The slot the pilot is stored in.
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
public ContainerSlot PilotSlot = default!;
[ViewVariables]
public readonly string PilotSlotId = "mech-pilot-slot";
/// <summary>
/// The current selected equipment of the mech.
/// If null, the mech is using just its fists.
/// </summary>
[ViewVariables]
public EntityUid? CurrentSelectedEquipment;
/// <summary>
/// The maximum amount of equipment items that can be installed in the mech
/// </summary>
[DataField("maxEquipmentAmount")]
public int MaxEquipmentAmount = 3;
/// <summary>
/// A whitelist for inserting equipment items.
/// </summary>
[DataField("equipmentWhitelist")]
public EntityWhitelist? EquipmentWhitelist;
/// <summary>
/// A container for storing the equipment entities.
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
public Container EquipmentContainer = default!;
[ViewVariables]
public readonly string EquipmentContainerId = "mech-equipment-container";
#region Action Prototypes
[DataField("mechCycleAction", customTypeSerializer: typeof(PrototypeIdSerializer<InstantActionPrototype>))]
public string MechCycleAction = "MechCycleEquipment";
[DataField("mechUiAction", customTypeSerializer: typeof(PrototypeIdSerializer<InstantActionPrototype>))]
public string MechUiAction = "MechOpenUI";
[DataField("mechEjectAction", customTypeSerializer: typeof(PrototypeIdSerializer<InstantActionPrototype>))]
public string MechEjectAction = "MechEject";
#endregion
#region Visualizer States
[DataField("baseState")]
public string? BaseState;
[DataField("openState")]
public string? OpenState;
[DataField("brokenState")]
public string? BrokenState;
#endregion
}
/// <summary>
/// Contains network state for <see cref="SharedMechComponent"/>.
/// </summary>
[Serializable, NetSerializable]
public sealed class MechComponentState : ComponentState
{
public FixedPoint2 Integrity;
public FixedPoint2 MaxIntegrity;
public FixedPoint2 Energy;
public FixedPoint2 MaxEnergy;
public EntityUid? CurrentSelectedEquipment;
public bool Broken;
}

View File

@ -0,0 +1,463 @@
using System.Linq;
using Content.Shared.Access.Components;
using Content.Shared.Access.Systems;
using Content.Shared.ActionBlocker;
using Content.Shared.Actions;
using Content.Shared.Actions.ActionTypes;
using Content.Shared.Body.Components;
using Content.Shared.Destructible;
using Content.Shared.FixedPoint;
using Content.Shared.Interaction;
using Content.Shared.Interaction.Components;
using Content.Shared.Interaction.Events;
using Content.Shared.Mech.Components;
using Content.Shared.Mech.Equipment.Components;
using Content.Shared.Movement.Components;
using Content.Shared.Movement.Systems;
using Content.Shared.Popups;
using Content.Shared.Weapons.Melee;
using Robust.Shared.Containers;
using Robust.Shared.GameStates;
using Robust.Shared.Player;
using Robust.Shared.Prototypes;
using Robust.Shared.Timing;
namespace Content.Shared.Mech.EntitySystems;
/// <summary>
/// Handles all of the interactions, UI handling, and items shennanigans for <see cref="SharedMechComponent"/>
/// </summary>
public abstract class SharedMechSystem : EntitySystem
{
[Dependency] private readonly IGameTiming _timing = default!;
[Dependency] private readonly IPrototypeManager _prototype = default!;
[Dependency] private readonly ActionBlockerSystem _actionBlocker = default!;
[Dependency] private readonly AccessReaderSystem _access = default!;
[Dependency] private readonly SharedActionsSystem _actions = default!;
[Dependency] private readonly SharedAppearanceSystem _appearance = default!;
[Dependency] private readonly SharedContainerSystem _container = default!;
[Dependency] private readonly SharedInteractionSystem _interaction = default!;
[Dependency] private readonly SharedMoverController _mover = default!;
[Dependency] private readonly SharedPopupSystem _popup = default!;
/// <inheritdoc/>
public override void Initialize()
{
SubscribeLocalEvent<SharedMechComponent, ComponentGetState>(OnGetState);
SubscribeLocalEvent<SharedMechComponent, ComponentHandleState>(OnHandleState);
SubscribeLocalEvent<MechPilotComponent, ComponentGetState>(OnPilotGetState);
SubscribeLocalEvent<MechPilotComponent, ComponentHandleState>(OnPilotHandleState);
SubscribeLocalEvent<SharedMechComponent, MechToggleEquipmentEvent>(OnToggleEquipmentAction);
SubscribeLocalEvent<SharedMechComponent, MechEjectPilotEvent>(OnEjectPilotEvent);
SubscribeLocalEvent<SharedMechComponent, InteractNoHandEvent>(RelayInteractionEvent);
SubscribeLocalEvent<SharedMechComponent, ComponentStartup>(OnStartup);
SubscribeLocalEvent<SharedMechComponent, DestructionEventArgs>(OnDestruction);
SubscribeLocalEvent<SharedMechComponent, GetAdditionalAccessEvent>(OnGetAdditionalAccess);
SubscribeLocalEvent<MechPilotComponent, GetMeleeWeaponEvent>(OnGetMeleeWeapon);
SubscribeLocalEvent<MechPilotComponent, CanAttackFromContainerEvent>(OnCanAttackFromContainer);
}
#region State Handling
private void OnGetState(EntityUid uid, SharedMechComponent component, ref ComponentGetState args)
{
args.State = new MechComponentState
{
Integrity = component.Integrity,
MaxIntegrity = component.MaxIntegrity,
Energy = component.Energy,
MaxEnergy = component.MaxEnergy,
CurrentSelectedEquipment = component.CurrentSelectedEquipment,
Broken = component.Broken
};
}
private void OnHandleState(EntityUid uid, SharedMechComponent component, ref ComponentHandleState args)
{
if (args.Current is not MechComponentState state)
return;
component.Integrity = state.Integrity;
component.MaxIntegrity = state.MaxIntegrity;
component.Energy = state.Energy;
component.MaxEnergy = state.MaxEnergy;
component.CurrentSelectedEquipment = state.CurrentSelectedEquipment;
component.Broken = state.Broken;
}
private void OnPilotGetState(EntityUid uid, MechPilotComponent component, ref ComponentGetState args)
{
args.State = new MechPilotComponentState
{
Mech = component.Mech
};
}
private void OnPilotHandleState(EntityUid uid, MechPilotComponent component, ref ComponentHandleState args)
{
if (args.Current is not MechPilotComponentState state)
return;
component.Mech = state.Mech;
}
#endregion
private void OnToggleEquipmentAction(EntityUid uid, SharedMechComponent component, MechToggleEquipmentEvent args)
{
if (args.Handled)
return;
args.Handled = true;
CycleEquipment(uid);
}
private void OnEjectPilotEvent(EntityUid uid, SharedMechComponent component, MechEjectPilotEvent args)
{
if (args.Handled)
return;
args.Handled = true;
TryEject(uid, component);
}
private void RelayInteractionEvent(EntityUid uid, SharedMechComponent component, InteractNoHandEvent args)
{
var pilot = component.PilotSlot.ContainedEntity;
if (pilot == null)
return;
if (!_timing.IsFirstTimePredicted)
return;
if (component.CurrentSelectedEquipment != null)
{
RaiseLocalEvent(component.CurrentSelectedEquipment.Value, args);
}
}
private void OnStartup(EntityUid uid, SharedMechComponent component, ComponentStartup args)
{
component.PilotSlot = _container.EnsureContainer<ContainerSlot>(uid, component.PilotSlotId);
component.EquipmentContainer = _container.EnsureContainer<Container>(uid, component.EquipmentContainerId);
component.BatterySlot = _container.EnsureContainer<ContainerSlot>(uid, component.BatterySlotId);
UpdateAppearance(uid, component);
}
private void OnDestruction(EntityUid uid, SharedMechComponent component, DestructionEventArgs args)
{
BreakMech(uid, component);
}
private void OnGetAdditionalAccess(EntityUid uid, SharedMechComponent component, ref GetAdditionalAccessEvent args)
{
var pilot = component.PilotSlot.ContainedEntity;
if (pilot == null)
return;
args.Entities.Add(pilot.Value);
_access.FindAccessItemsInventory(pilot.Value, out var items);
args.Entities = args.Entities.Union(items).ToHashSet();
}
private void SetupUser(EntityUid mech, EntityUid pilot, SharedMechComponent? component = null)
{
if (!Resolve(mech, ref component))
return;
var rider = EnsureComp<MechPilotComponent>(pilot);
var relay = EnsureComp<RelayInputMoverComponent>(pilot);
var irelay = EnsureComp<InteractionRelayComponent>(pilot);
_mover.SetRelay(pilot, mech, relay);
_interaction.SetRelay(pilot, mech, irelay);
rider.Mech = mech;
Dirty(rider);
_actions.AddAction(pilot, new InstantAction(_prototype.Index<InstantActionPrototype>(component.MechCycleAction)), mech);
_actions.AddAction(pilot, new InstantAction(_prototype.Index<InstantActionPrototype>(component.MechUiAction)), mech);
_actions.AddAction(pilot, new InstantAction(_prototype.Index<InstantActionPrototype>(component.MechEjectAction)), mech);
}
private void RemoveUser(EntityUid mech, EntityUid pilot)
{
if (!RemComp<MechPilotComponent>(pilot))
return;
RemComp<RelayInputMoverComponent>(pilot);
RemComp<InteractionRelayComponent>(pilot);
_actions.RemoveProvidedActions(pilot, mech);
}
/// <summary>
/// Destroys the mech, removing the user and ejecting all installed equipment.
/// </summary>
/// <param name="uid"></param>
/// <param name="component"></param>
public virtual void BreakMech(EntityUid uid, SharedMechComponent? component = null)
{
if (!Resolve(uid, ref component))
return;
TryEject(uid, component);
var equipment = new List<EntityUid>(component.EquipmentContainer.ContainedEntities);
foreach (var ent in equipment)
{
RemoveEquipment(uid, ent, component, forced: true);
}
component.Broken = true;
UpdateAppearance(uid, component);
}
/// <summary>
/// Cycles through the currently selected equipment.
/// </summary>
/// <param name="uid"></param>
/// <param name="component"></param>
public void CycleEquipment(EntityUid uid, SharedMechComponent? component = null)
{
if (!Resolve(uid, ref component))
return;
var allEquipment = component.EquipmentContainer.ContainedEntities.ToList();
var equipmentIndex = -1;
if (component.CurrentSelectedEquipment != null)
{
bool StartIndex(EntityUid u) => u == component.CurrentSelectedEquipment;
equipmentIndex = allEquipment.FindIndex(StartIndex);
}
equipmentIndex++;
component.CurrentSelectedEquipment = equipmentIndex >= allEquipment.Count
? null
: allEquipment[equipmentIndex];
var popupString = component.CurrentSelectedEquipment != null
? Loc.GetString("mech-equipment-select-popup", ("item", component.CurrentSelectedEquipment))
: Loc.GetString("mech-equipment-select-none-popup");
if (_timing.IsFirstTimePredicted)
_popup.PopupEntity(popupString, uid, Filter.Pvs(uid));
Dirty(component);
}
/// <summary>
/// Inserts an equipment item into the mech.
/// </summary>
/// <param name="uid"></param>
/// <param name="toInsert"></param>
/// <param name="component"></param>
/// <param name="equipmentComponent"></param>
public void InsertEquipment(EntityUid uid, EntityUid toInsert, SharedMechComponent? component = null, MechEquipmentComponent? equipmentComponent = null)
{
if (!Resolve(uid, ref component))
return;
if (!Resolve(toInsert, ref equipmentComponent))
return;
if (component.EquipmentContainer.ContainedEntities.Count >= component.MaxEquipmentAmount)
return;
if (component.EquipmentWhitelist != null && !component.EquipmentWhitelist.IsValid(uid))
return;
equipmentComponent.EquipmentOwner = uid;
component.EquipmentContainer.Insert(toInsert, EntityManager);
var ev = new MechEquipmentInsertedEvent(uid);
RaiseLocalEvent(toInsert, ref ev);
UpdateUserInterface(uid, component);
}
/// <summary>
/// Removes an equipment item from a mech.
/// </summary>
/// <param name="uid"></param>
/// <param name="toRemove"></param>
/// <param name="component"></param>
/// <param name="equipmentComponent"></param>
/// <param name="forced">Whether or not the removal can be cancelled</param>
public void RemoveEquipment(EntityUid uid, EntityUid toRemove, SharedMechComponent? component = null, MechEquipmentComponent? equipmentComponent = null, bool forced = false)
{
if (!Resolve(uid, ref component))
return;
if (!Resolve(toRemove, ref equipmentComponent))
return;
if (!forced)
{
var attemptev = new AttemptRemoveMechEquipmentEvent();
RaiseLocalEvent(toRemove, ref attemptev);
if (attemptev.Cancelled)
return;
}
equipmentComponent.EquipmentOwner = null;
component.EquipmentContainer.Remove(toRemove, EntityManager);
var ev = new MechEquipmentRemovedEvent(uid);
RaiseLocalEvent(toRemove, ref ev);
if (component.CurrentSelectedEquipment == toRemove)
CycleEquipment(uid, component);
UpdateUserInterface(uid, component);
}
/// <summary>
/// Attempts to change the amount of energy in the mech.
/// </summary>
/// <param name="uid">The mech itself</param>
/// <param name="delta">The change in energy</param>
/// <param name="component"></param>
/// <returns>If the energy was successfully changed.</returns>
public virtual bool TryChangeEnergy(EntityUid uid, FixedPoint2 delta, SharedMechComponent? component = null)
{
if (!Resolve(uid, ref component))
return false;
if (component.Energy + delta < 0)
return false;
component.Energy = FixedPoint2.Clamp(component.Energy + delta, 0, component.MaxEnergy);
Dirty(component);
UpdateUserInterface(uid, component);
return true;
}
/// <summary>
/// Sets the integrity of the mech.
/// </summary>
/// <param name="uid">The mech itself</param>
/// <param name="value">The value the integrity will be set at</param>
/// <param name="component"></param>
public void SetIntegrity(EntityUid uid, FixedPoint2 value, SharedMechComponent? component = null)
{
if (!Resolve(uid, ref component))
return;
component.Integrity = FixedPoint2.Clamp(value, 0, component.MaxIntegrity);
if (component.Integrity <= 0)
{
BreakMech(uid, component);
}
else if (component.Broken)
{
component.Broken = false;
UpdateAppearance(uid, component);
}
Dirty(component);
UpdateUserInterface(uid, component);
}
/// <summary>
/// Checks if the pilot is present
/// </summary>
/// <param name="component"></param>
/// <returns>Whether or not the pilot is present</returns>
public bool IsEmpty(SharedMechComponent component)
{
return component.PilotSlot.ContainedEntity == null;
}
/// <summary>
/// Checks if an entity can be inserted into the mech.
/// </summary>
/// <param name="uid"></param>
/// <param name="toInsert"></param>
/// <param name="component"></param>
/// <returns></returns>
public bool CanInsert(EntityUid uid, EntityUid toInsert, SharedMechComponent? component = null)
{
if (!Resolve(uid, ref component))
return false;
return IsEmpty(component) && _actionBlocker.CanMove(toInsert) && HasComp<BodyComponent>(toInsert);
}
/// <summary>
/// Updates the user interface
/// </summary>
/// <remarks>
/// This is defined here so that UI updates can be accessed from shared.
/// </remarks>
public virtual void UpdateUserInterface(EntityUid uid, SharedMechComponent? component = null)
{
}
/// <summary>
/// Attempts to insert a pilot into the mech.
/// </summary>
/// <param name="uid"></param>
/// <param name="toInsert"></param>
/// <param name="component"></param>
/// <returns>Whether or not the entity was inserted</returns>
public virtual bool TryInsert(EntityUid uid, EntityUid? toInsert, SharedMechComponent? component = null)
{
if (!Resolve(uid, ref component))
return false;
if (toInsert == null || component.PilotSlot.ContainedEntity == toInsert)
return false;
if (!CanInsert(uid, toInsert.Value, component))
return false;
SetupUser(uid, toInsert.Value);
component.PilotSlot.Insert(toInsert.Value, EntityManager);
UpdateAppearance(uid, component);
return true;
}
/// <summary>
/// Attempts to eject the current pilot from the mech
/// </summary>
/// <param name="uid"></param>
/// <param name="component"></param>
/// <returns>Whether or not the pilot was ejected.</returns>
public virtual bool TryEject(EntityUid uid, SharedMechComponent? component = null)
{
if (!Resolve(uid, ref component))
return false;
if (component.PilotSlot.ContainedEntity == null)
return false;
var pilot = component.PilotSlot.ContainedEntity.Value;
RemoveUser(uid, pilot);
_container.RemoveEntity(uid, pilot);
UpdateAppearance(uid, component);
return true;
}
private void OnGetMeleeWeapon(EntityUid uid, MechPilotComponent component, GetMeleeWeaponEvent args)
{
if (args.Handled)
return;
if (!TryComp<SharedMechComponent>(component.Mech, out var mech))
return;
var weapon = mech.CurrentSelectedEquipment ?? component.Mech;
args.Weapon = weapon;
args.Handled = true;
}
private void OnCanAttackFromContainer(EntityUid uid, MechPilotComponent component, CanAttackFromContainerEvent args)
{
args.CanAttack = true;
}
private void UpdateAppearance(EntityUid uid, SharedMechComponent ? component = null, AppearanceComponent? appearance = null)
{
if (!Resolve(uid, ref component, ref appearance, false))
return;
_appearance.SetData(uid, MechVisuals.Open, IsEmpty(component), appearance);
_appearance.SetData(uid, MechVisuals.Broken, component.Broken, appearance);
}
}

View File

@ -0,0 +1,45 @@
using System.Threading;
using Content.Shared.Mech.Components;
namespace Content.Shared.Mech.Equipment.Components;
/// <summary>
/// A piece of equipment that can be installed into <see cref="SharedMechComponent"/>
/// </summary>
[RegisterComponent]
public sealed class MechEquipmentComponent : Component
{
/// <summary>
/// How long does it take to install this piece of equipment
/// </summary>
[DataField("installDuration")]
public float InstallDuration = 5;
/// <summary>
/// The mech that the equipment is inside of.
/// </summary>
[ViewVariables]
public EntityUid? EquipmentOwner;
public CancellationTokenSource? TokenSource = null;
}
/// <summary>
/// Raised on the equipment when the installation is finished successfully
/// </summary>
public sealed class MechEquipmentInstallFinished : EntityEventArgs
{
public EntityUid Mech;
public MechEquipmentInstallFinished(EntityUid mech)
{
Mech = mech;
}
}
/// <summary>
/// Raised on the equipment when the installation fails.
/// </summary>
public sealed class MechEquipmentInstallCancelled : EntityEventArgs
{
}

View File

@ -0,0 +1,102 @@
using Robust.Shared.Serialization;
namespace Content.Shared.Mech;
[Serializable, NetSerializable]
public enum MechUiKey : byte
{
Key
}
/// <summary>
/// Event raised to collect BUI states for each of the mech's equipment items
/// </summary>
public sealed class MechEquipmentUiStateReadyEvent : EntityEventArgs
{
public Dictionary<EntityUid, BoundUserInterfaceState> States = new();
}
/// <summary>
/// Event raised to relay an equipment ui message
/// </summary>
public sealed class MechEquipmentUiMessageRelayEvent : EntityEventArgs
{
public MechEquipmentUiMessage Message;
public MechEquipmentUiMessageRelayEvent(MechEquipmentUiMessage message)
{
Message = message;
}
}
/// <summary>
/// UI event raised to remove a piece of equipment from a mech
/// </summary>
[Serializable, NetSerializable]
public sealed class MechEquipmentRemoveMessage : BoundUserInterfaceMessage
{
public EntityUid Equipment;
public MechEquipmentRemoveMessage(EntityUid equipment)
{
Equipment = equipment;
}
}
/// <summary>
/// base for all mech ui messages
/// </summary>
[Serializable, NetSerializable]
public abstract class MechEquipmentUiMessage : BoundUserInterfaceMessage
{
public EntityUid Equipment;
}
/// <summary>
/// event raised for the grabber equipment to eject an item from it's storage
/// </summary>
[Serializable, NetSerializable]
public sealed class MechGrabberEjectMessage : MechEquipmentUiMessage
{
public EntityUid Item;
public MechGrabberEjectMessage(EntityUid equipment, EntityUid uid)
{
Equipment = equipment;
Item = uid;
}
}
/// <summary>
/// BUI state for mechs that also contains all equipment ui states.
/// </summary>
/// <remarks>
/// ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡠⢐⠤⢃⢰⠐⡄⣀⠀⠀
/// ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⠔⣨⠀⢁⠁⠐⡐⠠⠜⠐⠀
/// ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠔⠐⢀⡁⣀⠔⡌⠡⢀⢐⠁⠀
/// ⠀⠀⠀⠀⢀⠔⠀⡂⡄⠠⢀⡀⠀⣄⡀⠠⠤⠴⡋⠑⡠⠀⠔⠐⢂⠕⢀⡂⠀⠀
/// ⠀⠀⠀⡔⠁⠠⡐⠁⠀⠀⠀⢘⠀⠀⠀⠀⠠⠀⠈⠪⠀⠑⠡⣃⠈⠤⡈⠀⠀⠀
/// ⠀⠀⠨⠀⠄⡒⠀⡂⢈⠀⣀⢌⠀⠀⠁⡈⠀⢆⢀⠀⡀⠉⠒⢆⠑⠀⠀⠀⠀⠀
/// ⠀⠀⠀⡁⠐⠠⠐⡀⠀⢀⣀⠣⡀⠢⡀⠀⢀⡃⠰⠀⠈⠠⢁⠎⠀⠀⠀⠀⠀⠀
/// ⠀⠀⠀⠅⠒⣈⢣⠠⠈⠕⠁⠱⠄⢤⠈⠪⠡⠎⢘⠈⡁⢙⠈⠀⠀⠀⠀⠀⠀⠀
/// ⠀⠀⠀⠃⠀⢡⠀⠧⠀⡇⠀⠀⠀⠀⠀⠀⠀⠀⠀⢕⡈⠌⠀⠀⠀⠀⠀⠀⠀⠀
/// ⠀⠀⠀⠀⠀⠀⠈⡀⡀⡆⠀⠀⠀⠀⠀⠀⠀⠀⠀⡰⠀⡐⠀⠀⠀⠀⠀⠀⠀⠀
/// ⠀⠀⠀⠀⠀⠀⠀⢈⢂⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠸⠀⡃⠀⠀⠀⠀⠀⠀⠀⠀
/// ⠀⠀⠀⠀⠀⠀⠀⠎⠐⢅⠀⠀⠀⠀⠀⠀⠀⠀⠀⢐⠅⠚⠄⠀⠀⠀⠀⠀⠀⠀
/// ⠀⠀⢈⠩⠈⠀⠐⠁⠀⢀⠀⠄⡂⠒⠐⠀⠆⠁⠰⠠⠀⢅⠈⠐⠄⢁⢡⠀⠀⠀
/// ⠀⠀⢈⡀⠰⡁⠀⠁⠴⠁⠔⠀⠀⠄⠄⡁⠀⠂⠀⠢⠠⠁⠀⠠⠈⠂⠬⠀⠀⠀
/// ⠀⠀⠠⡂⢄⠤⠒⣁⠐⢕⢀⡈⡐⡠⠄⢐⠀⠈⠠⠈⡀⠂⢀⣀⠰⠁⠠⠀⠀
/// trojan horse bui state
/// </remarks>
[Serializable, NetSerializable]
public sealed class MechBoundUiState : BoundUserInterfaceState
{
public Dictionary<EntityUid, BoundUserInterfaceState> EquipmentStates = new();
}
[Serializable, NetSerializable]
public sealed class MechGrabberUiState : BoundUserInterfaceState
{
public List<EntityUid> Contents = new();
public int MaxContents;
}

View File

@ -0,0 +1,62 @@
using Content.Shared.Actions;
using Robust.Shared.Serialization;
namespace Content.Shared.Mech;
[Serializable, NetSerializable]
public enum MechVisuals : byte
{
Open, //whether or not it's open and has a rider
Broken //if it broke and no longer works.
}
[Serializable, NetSerializable]
public enum MechAssemblyVisuals : byte
{
State
}
[Serializable, NetSerializable]
public enum MechVisualLayers : byte
{
Base
}
/// <summary>
/// Event raised on equipment when it is inserted into a mech
/// </summary>
[ByRefEvent]
public readonly record struct MechEquipmentInsertedEvent(EntityUid Mech)
{
public readonly EntityUid Mech = Mech;
}
/// <summary>
/// Event raised on equipment when it is removed from a mech
/// </summary>
[ByRefEvent]
public readonly record struct MechEquipmentRemovedEvent(EntityUid Mech)
{
public readonly EntityUid Mech = Mech;
}
/// <summary>
/// Raised on the mech equipment before it is going to be removed.
/// </summary>
[ByRefEvent]
public record struct AttemptRemoveMechEquipmentEvent()
{
public bool Cancelled = false;
}
public sealed class MechToggleEquipmentEvent : InstantActionEvent
{
}
public sealed class MechOpenUiEvent : InstantActionEvent
{
}
public sealed class MechEjectPilotEvent : InstantActionEvent
{
}

View File

@ -17,6 +17,7 @@ using Robust.Shared.Physics.Controllers;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
using System.Diagnostics.CodeAnalysis;
using Content.Shared.Mech.Components;
using Robust.Shared.Physics.Components;
using Robust.Shared.Physics.Systems;
@ -414,6 +415,12 @@ namespace Content.Shared.Movement.Systems
mobMover.StepSoundDistance -= distanceNeeded;
if (TryComp<FootstepModifierComponent>(mover.Owner, out var moverModifier))
{
sound = moverModifier.Sound;
return true;
}
if (_inventory.TryGetSlotEntity(mover.Owner, "shoes", out var shoes) &&
EntityManager.TryGetComponent<FootstepModifierComponent>(shoes, out var modifier))
{

View File

@ -130,6 +130,15 @@ public sealed class MeleeWeaponComponent : Component
public SoundSpecifier NoDamageSound { get; set; } = new SoundPathSpecifier("/Audio/Weapons/tap.ogg");
}
/// <summary>
/// Event raised on entity in GetWeapon function to allow systems to manually
/// specify what the weapon should be.
/// </summary>
public sealed class GetMeleeWeaponEvent : HandledEntityEventArgs
{
public EntityUid? Weapon;
}
[Serializable, NetSerializable]
public sealed class MeleeWeaponComponentState : ComponentState
{

View File

@ -19,7 +19,6 @@ using Robust.Shared.GameStates;
using Robust.Shared.Map;
using Robust.Shared.Physics;
using Robust.Shared.Physics.Systems;
using Robust.Shared.Player;
using Robust.Shared.Players;
using Robust.Shared.Prototypes;
using Robust.Shared.Timing;
@ -227,6 +226,13 @@ public abstract class SharedMeleeWeaponSystem : EntitySystem
{
MeleeWeaponComponent? melee;
var ev = new GetMeleeWeaponEvent();
RaiseLocalEvent(entity, ev);
if (ev.Handled)
{
return EntityManager.GetComponentOrNull<MeleeWeaponComponent>(ev.Weapon);
}
// Use inhands entity if we got one.
if (EntityManager.TryGetComponent(entity, out SharedHandsComponent? hands) &&
hands.ActiveHandEntity is { } held)

View File

@ -0,0 +1,12 @@
- files: ["mechmove03.ogg"]
license: "CC-BY-NC-SA-3.0"
copyright: "Taken from TG station."
source: "https://github.com/tgstation/tgstation/commit/d4f678a1772007ff8d7eddd21cf7218c8e07bfc0"
- files: ["sound_mecha_hydraulic.ogg"]
license: "CC-BY-NC-SA-3.0"
copyright: "Taken from TG station."
source: "https://github.com/tgstation/tgstation/commit/45123dd06cb6dc7c56e8004c528230682ea559b2"
- files: ["sound_mecha_powerloader_step.ogg"]
license: "CC-BY-NC-SA-3.0"
copyright: "Taken from TG station."
source: "https://github.com/tgstation/tgstation/commit/45123dd06cb6dc7c56e8004c528230682ea559b2"

View File

@ -1 +0,0 @@
mechmove03.ogg taken from TG station at commit https://github.com/tgstation/tgstation/commit/d4f678a1772007ff8d7eddd21cf7218c8e07bfc0

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,8 @@
action-name-mech-cycle = Cycle
action-description-mech-cycle = Cycles currently selected equipment
action-name-mech-control-panel = Control Panel
action-description-mech-control-panel = Opens the control panel for the mech
action-name-mech-eject = Eject
action-description-mech-eject = Ejects the pilot from the mech

View File

@ -0,0 +1,16 @@
mech-verb-enter = Enter
mech-verb-exit = Exit
mech-equipment-begin-install = Installing the {THE($item)}...
mech-equipment-finish-install = Finished installing the {THE($item)}
mech-equipment-select-popup = {$item} selected
mech-equipment-select-none-popup = Nothing selected
mech-ui-open-verb = Open control panel
mech-menu-title = mech control panel
mech-integrity-display = Integrity: {$amount}%
mech-energy-display = Energy: {$amount}%
mech-slot-display = Open Slots: {$amount}

View File

@ -76,5 +76,8 @@ technologies-robotics-technology-description = Parts needed for constructing mec
technologies-archaeology = Archeological equipment
technologies-archaeology-description = Advanced equipment for uncovering the secrets of artifacts.
technologies-adv-parts-technology = Advanced parts technology
technologies-adv-parts-technology-description = Like the previous ones, but better!
technologies-ripley-technology = Exosuit: Ripley
technologies-ripley-technology-description = The latest and greatest in mechanized cargo construction.
technologies-adv-parts-technology-description = Like the previous ones, but better!
technologies-adv-parts-technology = Advanced parts technology

View File

@ -0,0 +1,31 @@
- type: instantAction
id: MechCycleEquipment
name: action-name-mech-cycle
description: action-description-mech-cycle
itemIconStyle: NoItem
icon:
sprite: Interface/Actions/actions_mecha.rsi
state: mech_cycle_equip_on
event: !type:MechToggleEquipmentEvent
useDelay: 0.5
- type: instantAction
id: MechOpenUI
name: action-name-mech-control-panel
description: action-description-mech-control-panel
itemIconStyle: NoItem
icon:
sprite: Interface/Actions/actions_mecha.rsi
state: mech_view_stats
event: !type:MechOpenUiEvent
useDelay: 1
- type: instantAction
id: MechEject
name: action-name-mech-eject
description: action-description-mech-eject
itemIconStyle: NoItem
icon:
sprite: Interface/Actions/actions_mecha.rsi
state: mech_eject
event: !type:MechEjectPilotEvent

View File

@ -125,4 +125,4 @@
description: action-desc-wake
icon: { sprite: Objects/Consumable/Food/egg.rsi, state: icon }
checkCanInteract: false
event: !type:WakeActionEvent
event: !type:WakeActionEvent

View File

@ -259,6 +259,26 @@
#TODO- Gyroscope
#TODO- Thruster
- type: technology
name: technologies-ripley-technology
id: RipleyTechnology
description: technologies-ripley-technology-description
icon:
sprite: Objects/Specific/Mech/mecha.rsi
state: ripley
requiredPoints: 30000
requiredTechnologies:
- SalvageEquipment
unlockedRecipes:
- RipleyCentralElectronics
- RipleyPeripheralsElectronics
- MechEquipmentGrabber
- RipleyHarness
- RipleyLArm
- RipleyRArm
- RipleyLLeg
- RipleyRLeg
# Industrial Engineering Technology Tree
- type: technology

View File

@ -0,0 +1,25 @@
- type: entity
id: RipleyCentralElectronics
parent: BaseElectronics
name: ripley central control module
description: The electrical control center for the ripley mech.
components:
- type: Sprite
sprite: Objects/Misc/module.rsi
state: mainboard
- type: Tag
tags:
- RipleyCentralControlModule
- type: entity
id: RipleyPeripheralsElectronics
parent: BaseElectronics
name: ripley peripherals control module
description: The electrical peripherals control for the ripley mech.
components:
- type: Sprite
sprite: Objects/Misc/module.rsi
state: id_mod
- type: Tag
tags:
- RipleyPeripheralsControlModule

View File

@ -0,0 +1,150 @@
- type: entity
id: BaseMechPart
abstract: true
components:
- type: Clickable
- type: InteractionOutline
- type: CollisionWake
- type: TileFrictionModifier
modifier: 0.5
- type: Physics
bodyType: Dynamic
fixedRotation: false
- type: Fixtures
fixtures:
- shape:
!type:PhysShapeAabb
bounds: "-0.25,-0.25,0.25,0.25"
density: 100
mask:
- ItemMask
restitution: 0.3 # fite me
friction: 0.2
- type: Pullable
- type: Sprite
drawdepth: Items
noRot: false
netsync: false
sprite: Objects/Specific/Mech/ripley_construction.rsi
- type: entity
id: BaseMechPartItem
parent: BaseMechPart
abstract: true
components:
- type: Item
size: 50
- type: entity
id: BaseMechConstruct
parent: BaseMechPart
abstract: true
components:
- type: Appearance
- type: ContainerContainer
containers:
battery-container: !type:Container
- type: MechAssemblyVisuals
statePrefix: ripley
- type: entity
parent: BaseMechPart
id: RipleyHarness
name: ripley harness
description: The core of the ripley mech
components:
- type: Appearance
- type: ItemMapper
mapLayers:
ripley_l_arm+o:
whitelist:
tags:
- RipleyLArm
ripley_r_arm+o:
whitelist:
tags:
- RipleyRArm
ripley_l_leg+o:
whitelist:
tags:
- RipleyLLeg
ripley_r_leg+o:
whitelist:
tags:
- RipleyRLeg
sprite: Objects/Specific/Mech/ripley_construction.rsi
- type: ContainerContainer
containers:
mech-assembly-container: !type:Container
- type: MechAssembly
finishedPrototype: RipleyChassis
requiredParts:
RipleyLArm: false
RipleyRArm: false
RipleyLLeg: false
RipleyRLeg: false
- type: Sprite
state: ripley_harness+o
noRot: true
- type: entity
parent: BaseMechPartItem
id: RipleyLArm
name: ripley left arm
description: Ripley mech left arm
components:
- type: Sprite
state: ripley_l_arm
- type: Tag
tags:
- RipleyLArm
- type: entity
parent: BaseMechPartItem
id: RipleyLLeg
name: ripley left leg
description: Ripley mech left leg
components:
- type: Sprite
state: ripley_l_leg
- type: Tag
tags:
- RipleyLLeg
- type: entity
parent: BaseMechPartItem
id: RipleyRLeg
name: ripley right leg
description: Ripley mech right leg
components:
- type: Sprite
state: ripley_r_leg
- type: Tag
tags:
- RipleyRLeg
- type: entity
parent: BaseMechPartItem
id: RipleyRArm
name: ripley right arm
description: Ripley mech right arm
components:
- type: Sprite
state: ripley_r_arm
- type: Tag
tags:
- RipleyRArm
- type: entity
id: RipleyChassis
parent: BaseMechConstruct
name: ripley chassis
description: An in-progress construction of a ripley mech.
components:
- type: Sprite
noRot: true
state: ripley0
- type: Construction
graph: Ripley
node: start
defaultTarget: ripley

View File

@ -0,0 +1,26 @@
- type: entity
parent: BaseItem
id: BaseMechEquipment
abstract: true
components:
- type: Sprite
sprite: Objects/Specific/Mech/mecha_equipment.rsi
- type: Item
sprite: Objects/Specific/Mech/mecha_equipment.rsi
size: 50
- type: MechEquipment
- type: entity
id: MechEquipmentGrabber
parent: BaseMechEquipment
name: hydraulic clamp
description: Gives the mech the ability to grab things and drag them around.
components:
- type: Sprite
state: mecha_clamp
- type: MechGrabber
- type: UIFragment
ui: !type:MechGrabberUi
- type: ContainerContainer
containers:
item-container: !type:Container

View File

@ -0,0 +1,100 @@
- type: entity
id: BaseMech
save: false
abstract: true
components:
- type: Sprite
netsync: false
drawdepth: Mobs
noRot: true
sprite: Objects/Specific/Mech/mecha.rsi
layers:
- map: [ "enum.MechVisualLayers.Base" ]
state: ripley
- type: MobMover
- type: Mech
baseState: ripley
openState: ripley-open
brokenState: ripley-broken
- type: DoAfter
- type: Repairable
fuelCost: 25
doAfterDelay: 10
- type: UserInterface
interfaces:
- key: enum.MechUiKey.Key
type: MechBoundUserInterface
- type: MeleeWeapon
hidden: true
attackRate: 0.75
damage:
types:
Blunt: 25 #thwack
Structural: 20
- type: Puller
needsHands: false
- type: InputMover
- type: InteractionOutline
- type: MovementSpeedModifier
baseWalkSpeed: 1
baseSprintSpeed: 2
- type: Tag
tags:
- DoorBumpOpener
- FootstepSound
- type: Pullable
- type: Physics
bodyType: KinematicController
- type: Clickable
- type: Wires #we just want the panel
BoardName: Mech
LayoutId: Mech
- type: Fixtures
fixtures:
- shape:
!type:PhysShapeCircle
radius: 0.45
density: 1000
mask:
- MobMask
layer:
- MobLayer
- type: Appearance
- type: ContainerContainer
containers:
mech-pilot-slot: !type:ContainerSlot
mech-equipment-container: !type:Container
mech-battery-slot: !type:ContainerSlot
- type: Damageable
damageContainer: Inorganic
damageModifierSet: Metallic
- type: FootstepModifier
footstepSoundCollection:
path: /Audio/Mecha/mechmove03.ogg
- type: entity
id: MechRipley
parent: BaseMech
name: ripley
description: Cargo's favorite robotic box hauling friend.
components:
- type: FootstepModifier
footstepSoundCollection:
path: /Audio/Mecha/sound_mecha_powerloader_step.ogg
- type: Mech
baseState: ripley
openState: ripley-open
brokenState: ripley-broken
mechToPilotDamageMultiplier: 0.5
- type: entity
id: MechRipleyBattery
parent: MechRipley
suffix: Battery
components:
- type: Mech
baseState: ripley
openState: ripley-open
brokenState: ripley-broken
startingBattery: PowerCellHigh
mechToPilotDamageMultiplier: 0.5

View File

@ -290,6 +290,8 @@
- CloningConsoleComputerCircuitboard
- StasisBedMachineCircuitboard
- OreProcessorMachineCircuitboard
- RipleyCentralElectronics
- RipleyPeripheralsElectronics
- GeneratorPlasmaMachineCircuitboard
- GeneratorUraniumMachineCircuitboard
- WallmountGeneratorElectronics
@ -334,6 +336,12 @@
- ProximitySensor
- LeftArmBorg
- RightArmBorg
- RipleyHarness
- RipleyLArm
- RipleyRArm
- RipleyLLeg
- RipleyRLeg
- MechEquipmentGrabber
- type: MaterialStorage
whitelist:
tags:

View File

@ -0,0 +1,125 @@
- type: constructionGraph
id: Ripley
start: start
graph:
- node: start
edges:
- to: ripley
steps:
- tool: Anchoring
doAfter: 1
completed:
- !type:VisualizerDataInt
key: "enum.MechAssemblyVisuals.State"
data: 1
- tool: Screwing
doAfter: 1
completed:
- !type:VisualizerDataInt
key: "enum.MechAssemblyVisuals.State"
data: 2
- material: Cable
completed:
- !type:VisualizerDataInt
key: "enum.MechAssemblyVisuals.State"
data: 3
- tool: Cutting
doAfter: 1
completed:
- !type:VisualizerDataInt
key: "enum.MechAssemblyVisuals.State"
data: 4
- tag: RipleyCentralControlModule
name: ripley central control module
icon:
sprite: "Objects/Misc/module.rsi"
state: "mainboard"
completed:
- !type:VisualizerDataInt
key: "enum.MechAssemblyVisuals.State"
data: 5
- tool: Screwing
doAfter: 1
completed:
- !type:VisualizerDataInt
key: "enum.MechAssemblyVisuals.State"
data: 6
- tag: RipleyPeripheralsControlModule
name: ripley peripherals control module
icon:
sprite: "Objects/Misc/module.rsi"
state: id_mod
completed:
- !type:VisualizerDataInt
key: "enum.MechAssemblyVisuals.State"
data: 7
- tool: Screwing
doAfter: 1
completed:
- !type:VisualizerDataInt
key: "enum.MechAssemblyVisuals.State"
data: 12
#i omitted the steps involving inserting machine parts because
#currently mechs don't support upgrading. add them back in once that's squared away.
- component: PowerCell
name: power cell
store: battery-container
icon:
sprite: Objects/Power/power_cells.rsi
state: small
completed:
- !type:VisualizerDataInt
key: "enum.MechAssemblyVisuals.State"
data: 13
- tool: Screwing
doAfter: 1
completed:
- !type:VisualizerDataInt
key: "enum.MechAssemblyVisuals.State"
data: 14
- material: Steel
amount: 5
completed:
- !type:VisualizerDataInt
key: "enum.MechAssemblyVisuals.State"
data: 15
- tool: Anchoring
doAfter: 1
completed:
- !type:VisualizerDataInt
key: "enum.MechAssemblyVisuals.State"
data: 16
- tool: Welding
doAfter: 1
completed:
- !type:VisualizerDataInt
key: "enum.MechAssemblyVisuals.State"
data: 17
- material: MetalRod
amount: 10
completed:
- !type:VisualizerDataInt
key: "enum.MechAssemblyVisuals.State"
data: 18
- tool: Welding
doAfter: 1
- node: ripley
actions:
- !type:BuildMech
mechPrototype: MechRipley

View File

@ -280,6 +280,27 @@
Steel: 100
Glass: 900
Gold: 100
- type: latheRecipe
id: RipleyCentralElectronics
icon: Objects/Misc/module.rsi/mainboard.png
result: RipleyCentralElectronics
completetime: 4
materials:
Steel: 100
Glass: 900
Gold: 100
- type: latheRecipe
id: RipleyPeripheralsElectronics
icon: Objects/Misc/module.rsi/id_mod.png
result: RipleyPeripheralsElectronics
completetime: 4
materials:
Steel: 100
Glass: 900
Gold: 100
# Power
- type: latheRecipe
id: APCElectronics

View File

@ -0,0 +1,65 @@
- type: latheRecipe
id: RipleyHarness
icon:
sprite: Objects/Specific/Mech/ripley_construction.rsi
state: ripley_harness
result: RipleyHarness
completetime: 10
materials:
Steel: 3000
Glass: 1200
- type: latheRecipe
id: RipleyLArm
icon:
sprite: Objects/Specific/Mech/ripley_construction.rsi
state: ripley_l_arm
result: RipleyLArm
completetime: 10
materials:
Steel: 3000
Glass: 1200
- type: latheRecipe
id: RipleyLLeg
icon:
sprite: Objects/Specific/Mech/ripley_construction.rsi
state: ripley_l_leg
result: RipleyLLeg
completetime: 10
materials:
Steel: 3000
Glass: 1200
- type: latheRecipe
id: RipleyRLeg
icon:
sprite: Objects/Specific/Mech/ripley_construction.rsi
state: ripley_r_leg
result: RipleyRLeg
completetime: 10
materials:
Steel: 3000
Glass: 1200
- type: latheRecipe
id: RipleyRArm
icon:
sprite: Objects/Specific/Mech/ripley_construction.rsi
state: ripley_r_arm
result: RipleyRArm
completetime: 10
materials:
Steel: 3000
Glass: 1200
- type: latheRecipe
id: MechEquipmentGrabber
icon:
sprite: Objects/Specific/Mech/mecha_equipment.rsi
state: mecha_clamp
result: MechEquipmentGrabber
completetime: 10
materials:
Steel: 500
Plastic: 200

View File

@ -458,12 +458,30 @@
- type: Tag
id: RawMaterial
- type: Tag
id: RCDDeconstructWhitelist
# Give this to something that doesn't need any special recycler behavior and just needs deleting.
- type: Tag
id: Recyclable
- type: Tag
id: RCDDeconstructWhitelist
id: RipleyCentralControlModule
- type: Tag
id: RipleyPeripheralsControlModule
- type: Tag
id: RipleyLArm
- type: Tag
id: RipleyLLeg
- type: Tag
id: RipleyRLeg
- type: Tag
id: RipleyRArm
- type: Tag
id: RodMetal1

Binary file not shown.

After

Width:  |  Height:  |  Size: 474 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 572 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 546 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 591 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 543 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 549 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 547 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 477 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 635 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 642 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 676 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 964 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 553 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 558 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 503 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 513 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 541 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 550 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 640 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 926 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 636 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 591 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 605 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 636 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 509 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 611 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 617 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 650 B

View File

@ -0,0 +1,112 @@
{
"copyright" : "Taken from https://github.com/tgstation/tgstation at at https://github.com/tgstation/tgstation/commit/40d89d11ea4a5cb81d61dc1018b46f4e7d32c62a",
"license" : "CC-BY-SA-3.0",
"version": 1,
"size": {
"x": 32,
"y": 32
},
"states": [
{
"name": "mech_lights_off"
},
{
"name": "mech_lights_on"
},
{
"name": "mech_view_stats"
},
{
"name": "mech_eject"
},
{
"name": "mech_seat_swap"
},
{
"name": "mech_internals_off"
},
{
"name": "mech_internals_on"
},
{
"name": "mech_cycle_equip_off"
},
{
"name": "mech_cycle_equip_on"
},
{
"name": "mech_defense_mode_off"
},
{
"name": "mech_defense_mode_on"
},
{
"name": "mech_thrusters_off"
},
{
"name": "mech_thrusters_on"
},
{
"name": "mech_smoke"
},
{
"name": "mech_zoom_off"
},
{
"name": "mech_zoom_on"
},
{
"name": "mech_phasing_off"
},
{
"name": "mech_phasing_on"
},
{
"name": "mech_damtype_brute"
},
{
"name": "mech_damtype_burn"
},
{
"name": "mech_damtype_toxin"
},
{
"name": "mech_overload_off"
},
{
"name": "mech_overload_on"
},
{
"name": "strafe"
},
{
"name": "meson"
},
{
"name": "mech_ivanov"
},
{
"name": "mech_ivanov_cooldown",
"delays": [
[
0.2,
0.2,
0.2
]
]
},
{
"name": "mech_savannah"
},
{
"name": "mech_savannah_cooldown",
"delays": [
[
0.2,
0.2,
0.2
]
]
}
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 488 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Some files were not shown because too many files have changed in this diff Show More