MODsuits (Port From Goob #1242) (#1640)

# Description

Ports MODsuits from Goobstation PR
https://github.com/Goob-Station/Goob-Station/pull/1242. The PR author
has confirmed that he is okay with me doing this.

---

# TODO

- [X] Port in sprites
- [x] Port in YMLs
- [X] Port code
- [x] Port code PATCHES
- [x] Update EE with required fixes

---

<details><summary><h1>Media</h1></summary>
<p>

## Modsuit crafting

https://github.com/user-attachments/assets/8ff03d3a-0fc1-4818-b710-bfc43f0e2a68

## Modsuit sealing

https://github.com/user-attachments/assets/6671459a-7767-499b-8678-062fc1db7134

</p>
</details>

---

# Changelog

🆑
- add: Modsuits have been ported from Goobstation!

---------

Signed-off-by: Eris <erisfiregamer1@gmail.com>
Co-authored-by: DEATHB4DEFEAT <77995199+DEATHB4DEFEAT@users.noreply.github.com>
Co-authored-by: VMSolidus <evilexecutive@gmail.com>

(cherry picked from commit cb06c41fc07275e1f15af916babb44368c0c26c2)
This commit is contained in:
Eris
2025-01-25 20:17:44 +03:00
committed by Spatison
parent fce733b7b8
commit d6f3265e83
121 changed files with 2759 additions and 239 deletions

View File

@@ -112,7 +112,8 @@ public sealed class DoAfterOverlay : Overlay
var alpha = 1f;
if (doAfter.Args.Hidden)
{
if (uid != localEnt)
// Goobstation - Show doAfter progress bar to another entity
if (uid != localEnt && localEnt != doAfter.Args.ShowTo)
continue;
// Hints to the local player that this do-after is not visible to other players.

View File

@@ -0,0 +1,11 @@
namespace Content.Client._Goobstation.Clothing.Components;
[RegisterComponent]
public sealed partial class SealableClothingVisualsComponent : Component
{
[DataField]
public string SpriteLayer = "sealed";
[DataField]
public Dictionary<string, List<PrototypeLayerData>> VisualLayers = new();
}

View File

@@ -0,0 +1,7 @@
using Content.Shared._Goobstation.Clothing.Systems;
namespace Content.Client._Goobstation.Clothing.EntitySystems;
public sealed class PoweredSealableClothingSystem : SharedPoweredSealableClothingSystem
{
}

View File

@@ -0,0 +1,7 @@
using Content.Shared._Goobstation.Clothing.Systems;
namespace Content.Client._Goobstation.Clothing.EntitySystems;
public sealed partial class SealableClothingSystem : SharedSealableClothingSystem
{
}

View File

@@ -0,0 +1,57 @@
using Content.Client._Goobstation.Clothing.Components;
using Content.Client.Clothing;
using Content.Shared._Goobstation.Clothing;
using Content.Shared.Clothing;
using Content.Shared.Item;
using Robust.Client.GameObjects;
using System.Linq;
namespace Content.Client._Goobstation.Clothing.EntitySystems;
public sealed class SealableClothingVisualizerSystem : VisualizerSystem<SealableClothingVisualsComponent>
{
[Dependency] private readonly SharedItemSystem _itemSystem = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<SealableClothingVisualsComponent, GetEquipmentVisualsEvent>(OnGetEquipmentVisuals, after: new[] { typeof(ClientClothingSystem) });
}
protected override void OnAppearanceChange(EntityUid uid, SealableClothingVisualsComponent component, ref AppearanceChangeEvent args)
{
if (!AppearanceSystem.TryGetData<bool>(uid, SealableClothingVisuals.Sealed, out var isSealed, args.Component))
return;
if (args.Sprite != null && component.SpriteLayer != null && args.Sprite.LayerMapTryGet(component.SpriteLayer, out var layer))
args.Sprite.LayerSetVisible(layer, isSealed);
_itemSystem.VisualsChanged(uid);
}
private void OnGetEquipmentVisuals(Entity<SealableClothingVisualsComponent> sealable, ref GetEquipmentVisualsEvent args)
{
var (uid, comp) = sealable;
if (!TryComp(uid, out AppearanceComponent? appearance)
|| !AppearanceSystem.TryGetData<bool>(uid, SealableClothingVisuals.Sealed, out var isSealed, appearance)
|| !isSealed)
return;
if (!comp.VisualLayers.TryGetValue(args.Slot, out var layers))
return;
var i = 0;
foreach (var layer in layers)
{
var key = layer.MapKeys?.FirstOrDefault();
if (key == null)
{
key = i == 0 ? $"{args.Slot}-sealed" : $"{args.Slot}-sealed-{i}";
i++;
}
args.Layers.Add((key, layer));
}
}
}

View File

@@ -0,0 +1,39 @@
using Content.Shared.Clothing.Components;
using Robust.Client.Graphics;
using Robust.Client.Input;
using Robust.Client.UserInterface;
namespace Content.Client._Goobstation.Clothing;
public sealed class ToggleableClothingBoundUserInterface : BoundUserInterface
{
[Dependency] private readonly IClyde _displayManager = default!;
[Dependency] private readonly IInputManager _inputManager = default!;
private IEntityManager _entityManager;
private ToggleableClothingRadialMenu? _menu;
public ToggleableClothingBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey)
{
IoCManager.InjectDependencies(this);
_entityManager = IoCManager.Resolve<IEntityManager>();
}
protected override void Open()
{
base.Open();
_menu = this.CreateWindow<ToggleableClothingRadialMenu>();
_menu.SetEntity(Owner);
_menu.SendToggleClothingMessageAction += SendToggleableClothingMessage;
var vpSize = _displayManager.ScreenSize;
_menu.OpenCenteredAt(_inputManager.MouseScreenPosition.Position / vpSize);
}
private void SendToggleableClothingMessage(EntityUid uid)
{
var message = new ToggleableClothingUiMessage(_entityManager.GetNetEntity(uid));
SendPredictedMessage(message);
}
}

View File

@@ -0,0 +1,6 @@
<ui:RadialMenu xmlns="https://spacestation14.io"
xmlns:ui="clr-namespace:Content.Client.UserInterface.Controls"
BackButtonStyleClass="RadialMenuBackButton" CloseButtonStyleClass="RadialMenuCloseButton"
VerticalExpand="True" HorizontalExpand="True" MinSize="450 450">
<ui:RadialContainer Name="Main" VerticalExpand="True" HorizontalExpand="True" Radius="64" ReserveSpaceForHiddenChildren="False" />
</ui:RadialMenu>

View File

@@ -0,0 +1,96 @@
using Content.Client.UserInterface.Controls;
using Content.Shared.Clothing.Components;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Prototypes;
using System.Numerics;
namespace Content.Client._Goobstation.Clothing;
public sealed partial class ToggleableClothingRadialMenu : RadialMenu
{
[Dependency] private readonly EntityManager _entityManager = default!;
public event Action<EntityUid>? SendToggleClothingMessageAction;
public EntityUid Entity { get; set; }
public ToggleableClothingRadialMenu()
{
IoCManager.InjectDependencies(this);
RobustXamlLoader.Load(this);
}
public void SetEntity(EntityUid uid)
{
Entity = uid;
RefreshUI();
}
public void RefreshUI()
{
// Even EmotesMenu has to call this, I'm assuming it's essential.
var main = FindControl<RadialContainer>("Main");
if (!_entityManager.TryGetComponent<ToggleableClothingComponent>(Entity, out var clothing)
|| clothing.Container is not { } clothingContainer)
return;
foreach (var attached in clothing.ClothingUids)
{
// Change tooltip text if attached clothing is toggle/untoggled
var tooltipText = Loc.GetString(clothing.UnattachTooltip);
if (clothingContainer.Contains(attached.Key))
tooltipText = Loc.GetString(clothing.AttachTooltip);
var button = new ToggleableClothingRadialMenuButton()
{
StyleClasses = { "RadialMenuButton" },
SetSize = new Vector2(64, 64),
ToolTip = tooltipText,
AttachedClothingId = attached.Key
};
var spriteView = new SpriteView()
{
SetSize = new Vector2(48, 48),
VerticalAlignment = VAlignment.Center,
HorizontalAlignment = HAlignment.Center,
Stretch = SpriteView.StretchMode.Fill
};
spriteView.SetEntity(attached.Key);
button.AddChild(spriteView);
main.AddChild(button);
}
AddToggleableClothingMenuButtonOnClickAction(main);
}
private void AddToggleableClothingMenuButtonOnClickAction(Control control)
{
if (control is not RadialContainer mainControl)
return;
foreach (var child in mainControl.Children)
{
if (child is not ToggleableClothingRadialMenuButton castChild)
continue;
castChild.OnButtonDown += _ =>
{
SendToggleClothingMessageAction?.Invoke(castChild.AttachedClothingId);
mainControl.DisposeAllChildren();
RefreshUI();
};
}
}
}
public sealed class ToggleableClothingRadialMenuButton : RadialMenuTextureButton
{
public EntityUid AttachedClothingId { get; set; }
}

View File

@@ -18,21 +18,27 @@ namespace Content.Server.Atmos.EntitySystems
[Dependency] private readonly AtmosphereSystem _atmosphereSystem = default!;
[Dependency] private readonly DamageableSystem _damageableSystem = default!;
[Dependency] private readonly AlertsSystem _alertsSystem = default!;
[Dependency] private readonly IAdminLogManager _adminLogger= default!;
[Dependency] private readonly IAdminLogManager _adminLogger = default!;
[Dependency] private readonly InventorySystem _inventorySystem = default!;
[Dependency] private readonly ILogManager _logManager = default!;
private const float UpdateTimer = 1f;
private ISawmill _sawmill = default!;
private float _timer;
public override void Initialize()
{
SubscribeLocalEvent<PressureProtectionComponent, GotEquippedEvent>(OnPressureProtectionEquipped);
SubscribeLocalEvent<PressureProtectionComponent, GotUnequippedEvent>(OnPressureProtectionUnequipped);
SubscribeLocalEvent<PressureProtectionComponent, ComponentInit>(OnUpdateResistance);
SubscribeLocalEvent<PressureProtectionComponent, ComponentRemove>(OnUpdateResistance);
SubscribeLocalEvent<PressureProtectionComponent, ComponentInit>(OnPressureProtectionChanged); // Goobstation - Update component state on toggle
SubscribeLocalEvent<PressureProtectionComponent, ComponentRemove>(OnPressureProtectionChanged); // Goobstation - Update component state on toggle
SubscribeLocalEvent<PressureImmunityComponent, ComponentInit>(OnPressureImmuneInit);
SubscribeLocalEvent<PressureImmunityComponent, ComponentRemove>(OnPressureImmuneRemove);
// _sawmill = _logManager.GetSawmill("barotrauma");
}
private void OnPressureImmuneInit(EntityUid uid, PressureImmunityComponent pressureImmunity, ComponentInit args)
@@ -51,6 +57,27 @@ namespace Content.Server.Atmos.EntitySystems
}
}
// Goobstation - Modsuits - Update component state on toggle
private void OnPressureProtectionChanged(EntityUid uid, PressureProtectionComponent pressureProtection, EntityEventArgs args)
{
var protectionTarget = uid;
string? slotTarget = null;
if (_inventorySystem.TryGetContainingEntity(uid, out var entity) && _inventorySystem.TryGetContainingSlot(uid, out var slot))
{
protectionTarget = entity.Value;
slotTarget = slot.Name;
}
if (!TryComp<BarotraumaComponent>(protectionTarget, out var barotrauma))
return;
if (slotTarget != null && !barotrauma.ProtectionSlots.Contains(slotTarget))
return;
UpdateCachedResistances(protectionTarget, barotrauma);
}
/// <summary>
/// Generic method for updating resistance on component Lifestage events
/// </summary>

View File

@@ -34,4 +34,10 @@ public sealed partial class LungComponent : Component
/// </summary>
[DataField]
public ProtoId<AlertPrototype> Alert = "LowOxygen";
[DataField]
public float MaxVolume = 100f;
[DataField]
public bool CanReact = false; // No Dexalin lungs... right?
}

View File

@@ -1,20 +1,25 @@
using Content.Server.Atmos.Components;
using Content.Server.Atmos.EntitySystems;
using Content.Server.Body.Components;
using Content.Server.Chemistry.Containers.EntitySystems;
using Content.Shared.Chemistry.EntitySystems;
using Content.Shared.Atmos;
using Content.Shared.Chemistry.Components;
using Content.Shared.Clothing;
using Content.Shared.Inventory.Events;
using Content.Shared.Inventory;
using Content.Server.Power.EntitySystems;
using Robust.Server.Containers;
namespace Content.Server.Body.Systems;
public sealed class LungSystem : EntitySystem
{
[Dependency] private readonly AtmosphereSystem _atmos = default!;
[Dependency] private readonly InternalsSystem _internals = default!;
[Dependency] private readonly SolutionContainerSystem _solutionContainerSystem = default!;
[Dependency] private readonly AtmosphereSystem _atmosphereSystem = default!;
[Dependency] private readonly InternalsSystem _internals = default!;
[Dependency] private readonly SharedSolutionContainerSystem _solutionContainerSystem = default!;
[Dependency] private readonly InventorySystem _inventory = default!; // Goobstation
public static string LungSolutionName = "Lung";
@@ -22,6 +27,7 @@ public sealed class LungSystem : EntitySystem
{
base.Initialize();
SubscribeLocalEvent<LungComponent, ComponentInit>(OnComponentInit);
SubscribeLocalEvent<BreathToolComponent, ComponentInit>(OnBreathToolInit); // Goobstation - Modsuits - Update on component toggle
SubscribeLocalEvent<BreathToolComponent, GotEquippedEvent>(OnGotEquipped);
SubscribeLocalEvent<BreathToolComponent, GotUnequippedEvent>(OnGotUnequipped);
SubscribeLocalEvent<BreathToolComponent, ItemMaskToggledEvent>(OnMaskToggled);
@@ -50,16 +56,36 @@ public sealed class LungSystem : EntitySystem
private void OnComponentInit(Entity<LungComponent> entity, ref ComponentInit args)
{
var solution = _solutionContainerSystem.EnsureSolution(entity.Owner, entity.Comp.SolutionName);
solution.MaxVolume = 100.0f;
solution.CanReact = false; // No dexalin lungs
if (_solutionContainerSystem.EnsureSolution(entity.Owner, entity.Comp.SolutionName, out var solution))
{
solution.MaxVolume = entity.Comp.MaxVolume;
solution.CanReact = entity.Comp.CanReact;
}
}
// Goobstation - Update component state on component toggle
private void OnBreathToolInit(Entity<BreathToolComponent> ent, ref ComponentInit args)
{
var comp = ent.Comp;
comp.IsFunctional = true;
if (!_inventory.TryGetContainingEntity(ent.Owner, out var parent)
|| !_inventory.TryGetContainingSlot(ent.Owner, out var slot)
|| (slot.SlotFlags & comp.AllowedSlots) == 0
|| !TryComp(parent, out InternalsComponent? internals))
return;
ent.Comp.ConnectedInternalsEntity = parent;
_internals.ConnectBreathTool((parent.Value, internals), ent);
}
private void OnMaskToggled(Entity<BreathToolComponent> ent, ref ItemMaskToggledEvent args)
{
if (args.IsToggled || args.IsEquip)
{
_atmos.DisconnectInternals(ent.Comp);
_atmosphereSystem.DisconnectInternals(ent);
}
else
{

View File

@@ -29,6 +29,7 @@ public sealed class IdentitySystem : SharedIdentitySystem
[Dependency] private readonly HumanoidAppearanceSystem _humanoid = default!;
[Dependency] private readonly CriminalRecordsConsoleSystem _criminalRecordsConsole = default!;
[Dependency] private readonly PsionicsRecordsConsoleSystem _psionicsRecordsConsole = default!;
[Dependency] private readonly InventorySystem _inventorySystem = default!; // Goobstation - Update component state on component toggle
private HashSet<EntityUid> _queuedIdentityUpdates = new();
@@ -41,7 +42,11 @@ public sealed class IdentitySystem : SharedIdentitySystem
SubscribeLocalEvent<IdentityComponent, DidUnequipEvent>((uid, _, _) => QueueIdentityUpdate(uid));
SubscribeLocalEvent<IdentityComponent, DidUnequipHandEvent>((uid, _, _) => QueueIdentityUpdate(uid));
SubscribeLocalEvent<IdentityComponent, WearerMaskToggledEvent>((uid, _, _) => QueueIdentityUpdate(uid));
SubscribeLocalEvent<IdentityComponent, EntityRenamedEvent>((uid, _, _) => QueueIdentityUpdate(uid));
SubscribeLocalEvent<IdentityComponent, MapInitEvent>(OnMapInit);
SubscribeLocalEvent<IdentityBlockerComponent, ComponentInit>(BlockerUpdateIdentity); // Goobstation - Update component state on component toggle
SubscribeLocalEvent<IdentityBlockerComponent, ComponentRemove>(BlockerUpdateIdentity); // Goobstation - Update component state on component toggle
}
public override void Update(float frameTime)
@@ -175,5 +180,16 @@ public sealed class IdentitySystem : SharedIdentitySystem
return new(trueName, gender, ageString, presumedName, presumedJob);
}
// Goobstation - Update component state on component toggle
private void BlockerUpdateIdentity(EntityUid uid, IdentityBlockerComponent component, EntityEventArgs args)
{
var target = uid;
if (_inventorySystem.TryGetContainingEntity(uid, out var containing))
target = containing.Value;
QueueIdentityUpdate(target);
}
#endregion
}

View File

@@ -11,6 +11,8 @@ using System.Diagnostics.CodeAnalysis;
using Content.Shared.Storage.Components;
using Robust.Server.Containers;
using Content.Shared.Whitelist;
using Content.Shared.Inventory;
using Content.Shared._Goobstation.Clothing.Systems;
namespace Content.Server.Power.EntitySystems;
@@ -22,6 +24,7 @@ internal sealed class ChargerSystem : EntitySystem
[Dependency] private readonly BatterySystem _battery = default!;
[Dependency] private readonly SharedAppearanceSystem _appearance = default!;
[Dependency] private readonly EntityWhitelistSystem _whitelistSystem = default!;
[Dependency] private readonly InventorySystem _inventorySystem = default!;
public override void Initialize()
{
@@ -256,15 +259,45 @@ internal sealed class ChargerSystem : EntitySystem
UpdateStatus(uid, component);
}
// Goobstation - Modsuits - Changed charger logic to work with suits in cyborg charger
private bool SearchForBattery(EntityUid uid, [NotNullWhen(true)] out EntityUid? batteryUid, [NotNullWhen(true)] out BatteryComponent? component)
{
batteryUid = null;
component = null;
// try get a battery directly on the inserted entity
if (!TryComp(uid, out component))
if (TryComp(uid, out component))
{
// or by checking for a power cell slot on the inserted entity
return _powerCell.TryGetBatteryFromSlot(uid, out batteryUid, out component);
batteryUid = uid;
return true;
}
batteryUid = uid;
return true;
// Try to get the battery by checking for a power cell slot on the inserted entity
if (_powerCell.TryGetBatteryFromSlot(uid, out batteryUid, out component))
return true;
if (TryComp<InventoryComponent>(uid, out var inventory))
{
var relayEv = new FindInventoryBatteryEvent();
_inventorySystem.RelayEvent((uid, inventory), ref relayEv);
if (relayEv.FoundBattery != null)
{
batteryUid = relayEv.FoundBattery.Value.Owner;
component = relayEv.FoundBattery.Value.Comp;
return true;
}
}
return false;
}
}
// Goobstation - Modsuits stuff
[ByRefEvent]
public record struct FindInventoryBatteryEvent() : IInventoryRelayEvent
{
public SlotFlags TargetSlots { get; } = SlotFlags.WITHOUT_POCKET;
public Entity<BatteryComponent>? FoundBattery { get; set; }
}

View File

@@ -0,0 +1,112 @@
using Content.Server.Power.EntitySystems;
using Content.Server.PowerCell;
using Content.Shared._Goobstation.Clothing.Components;
using Content.Shared._Goobstation.Clothing.Systems;
using Content.Shared.Alert;
using Content.Shared.Inventory;
using Content.Shared.Movement.Systems;
using Content.Shared.PowerCell;
using Content.Shared.PowerCell.Components;
using Content.Shared.Rounding;
namespace Content.Server._Goobstation.Clothing.Systems;
public sealed partial class PoweredSealableClothingSystem : SharedPoweredSealableClothingSystem
{
[Dependency] private readonly AlertsSystem _alertsSystem = default!;
[Dependency] private readonly PowerCellSystem _powerCellSystem = default!;
[Dependency] private readonly MovementSpeedModifierSystem _movementSpeed = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<SealableClothingRequiresPowerComponent, InventoryRelayedEvent<RefreshMovementSpeedModifiersEvent>>(OnMovementSpeedChange);
SubscribeLocalEvent<SealableClothingRequiresPowerComponent, PowerCellChangedEvent>(OnPowerCellChanged);
SubscribeLocalEvent<SealableClothingRequiresPowerComponent, PowerCellSlotEmptyEvent>(OnPowerCellEmpty);
SubscribeLocalEvent<SealableClothingRequiresPowerComponent, ClothingControlSealCompleteEvent>(OnRequiresPowerSealCompleteEvent);
SubscribeLocalEvent<SealableClothingRequiresPowerComponent, InventoryRelayedEvent<FindInventoryBatteryEvent>>(OnFindInventoryBatteryEvent);
}
private void OnPowerCellChanged(Entity<SealableClothingRequiresPowerComponent> entity, ref PowerCellChangedEvent args)
{
if (!entity.Comp.IsPowered && _powerCellSystem.HasDrawCharge(entity))
{
entity.Comp.IsPowered = true;
Dirty(entity);
ModifySpeed(entity);
}
UpdateClothingPowerAlert(entity);
}
private void OnPowerCellEmpty(Entity<SealableClothingRequiresPowerComponent> entity, ref PowerCellSlotEmptyEvent args)
{
entity.Comp.IsPowered = false;
Dirty(entity);
ModifySpeed(entity);
}
/// Enables or disables power cell draw on seal/unseal complete
private void OnRequiresPowerSealCompleteEvent(Entity<SealableClothingRequiresPowerComponent> entity, ref ClothingControlSealCompleteEvent args)
{
if (!TryComp(entity, out PowerCellDrawComponent? drawComp))
return;
_powerCellSystem.SetDrawEnabled((entity.Owner, drawComp), args.IsSealed);
UpdateClothingPowerAlert(entity);
ModifySpeed(entity);
}
private void OnMovementSpeedChange(Entity<SealableClothingRequiresPowerComponent> entity, ref InventoryRelayedEvent<RefreshMovementSpeedModifiersEvent> args)
{
if (!TryComp(entity, out SealableClothingControlComponent? controlComp))
return;
// If suit is unsealed - don't care about penalty
if (!controlComp.IsCurrentlySealed)
return;
if (!entity.Comp.IsPowered)
args.Args.ModifySpeed(entity.Comp.MovementSpeedPenalty);
}
private void ModifySpeed(EntityUid uid)
{
if (!TryComp(uid, out SealableClothingControlComponent? controlComp) || controlComp.WearerEntity == null)
return;
_movementSpeed.RefreshMovementSpeedModifiers(controlComp.WearerEntity.Value);
}
/// Sets power alert to wearer when clothing is sealed
private void UpdateClothingPowerAlert(Entity<SealableClothingRequiresPowerComponent> entity)
{
var (uid, comp) = entity;
if (!TryComp<SealableClothingControlComponent>(uid, out var controlComp) || controlComp.WearerEntity == null)
return;
if (!_powerCellSystem.TryGetBatteryFromSlot(entity, out var battery) || !controlComp.IsCurrentlySealed)
{
_alertsSystem.ClearAlert(controlComp.WearerEntity.Value, comp.SuitPowerAlert);
return;
}
var severity = ContentHelpers.RoundToLevels(MathF.Max(0f, battery.CurrentCharge), battery.MaxCharge, 6);
_alertsSystem.ShowAlert(controlComp.WearerEntity.Value, comp.SuitPowerAlert, (short) severity);
}
/// Tries to find battery for charger
private void OnFindInventoryBatteryEvent(Entity<SealableClothingRequiresPowerComponent> entity, ref InventoryRelayedEvent<FindInventoryBatteryEvent> args)
{
if (args.Args.FoundBattery != null)
return;
if (_powerCellSystem.TryGetBatteryFromSlot(entity, out var batteryEnt, out var battery))
args.Args.FoundBattery = (batteryEnt.Value, battery);
}
}

View File

@@ -0,0 +1,5 @@
using Content.Shared._Goobstation.Clothing.Systems;
namespace Content.Server._Goobstation.Clothing.Systems;
public sealed partial class SealableClothingSystem : SharedSealableClothingSystem { }

View File

@@ -1,3 +1,4 @@
using Content.Shared.Popups;
using Robust.Shared.GameStates;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
@@ -15,8 +16,15 @@ public sealed partial class ConfirmableActionComponent : Component
/// <summary>
/// Warning popup shown when priming the action.
/// </summary>
[DataField(required: true)]
public LocId Popup = string.Empty;
// Goobstation - Modsuits - Removed required string
[DataField]
public LocId? Popup = null;
/// <summary>
/// Type of warning popup - Goobstaiton - Modsuits
/// </summary>
[DataField("popupType")]
public PopupType PopupFontType = PopupType.LargeCaution;
/// <summary>
/// If not null, this is when the action can be confirmed at.

View File

@@ -10,6 +10,7 @@ namespace Content.Shared.Actions;
public sealed class ConfirmableActionSystem : EntitySystem
{
[Dependency] private readonly IGameTiming _timing = default!;
[Dependency] private readonly SharedActionsSystem _actions = default!; // Goobstation
[Dependency] private readonly SharedPopupSystem _popup = default!;
public override void Initialize()
@@ -67,7 +68,12 @@ public sealed class ConfirmableActionSystem : EntitySystem
comp.NextUnprime = comp.NextConfirm + comp.PrimeTime;
Dirty(uid, comp);
_popup.PopupClient(Loc.GetString(comp.Popup), user, user, PopupType.LargeCaution);
// Goobstation - Confirmable action with changed icon - Start
if (!string.IsNullOrEmpty(comp.Popup))
_popup.PopupClient(Loc.GetString(comp.Popup), user, user, comp.PopupFontType);
_actions.SetToggled(ent, true);
// Goobstation - Confirmable action with changed icon - End
}
private void Unprime(Entity<ConfirmableActionComponent> ent)
@@ -75,6 +81,9 @@ public sealed class ConfirmableActionSystem : EntitySystem
var (uid, comp) = ent;
comp.NextConfirm = null;
comp.NextUnprime = null;
_actions.SetToggled(ent, false); // Goobstation - Confirmable action with changed icon
Dirty(uid, comp);
}
}

View File

@@ -1,5 +1,6 @@
using Content.Shared.Damage;
using Robust.Shared.GameStates;
using Robust.Shared.Serialization;
using Robust.Shared.Utility;
namespace Content.Shared.Armor;
@@ -7,20 +8,20 @@ namespace Content.Shared.Armor;
/// <summary>
/// Used for clothing that reduces damage when worn.
/// </summary>
[RegisterComponent, NetworkedComponent, Access(typeof(SharedArmorSystem))]
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState] // Goobstation - remove access restrictions
public sealed partial class ArmorComponent : Component
{
/// <summary>
/// The damage reduction
/// </summary>
[DataField(required: true)]
[DataField(required: true), AutoNetworkedField]
public DamageModifierSet Modifiers = default!;
/// <summary>
/// A multiplier applied to the calculated point value
/// to determine the monetary value of the armor
/// </summary>
[DataField]
[DataField, AutoNetworkedField]
public float PriceMultiplier = 1;
}

View File

@@ -1,4 +1,5 @@
using Content.Shared.Clothing.EntitySystems;
using Robust.Shared.Containers;
using Robust.Shared.GameStates;
namespace Content.Shared.Clothing.Components;
@@ -13,9 +14,21 @@ namespace Content.Shared.Clothing.Components;
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
public sealed partial class AttachedClothingComponent : Component
{
// Goobstation - Modsuits changes this system entirely
public const string DefaultClothingContainerId = "replaced-clothing";
/// <summary>
/// The Id of the piece of clothing that this entity belongs to.
/// </summary>
[DataField, AutoNetworkedField]
public EntityUid AttachedUid;
/// <summary>
/// Container ID for clothing that will be replaced with this one
/// </summary>
[DataField, AutoNetworkedField]
public string ClothingContainerId = DefaultClothingContainerId;
[ViewVariables, NonSerialized]
public ContainerSlot? ClothingContainer;
}

View File

@@ -3,7 +3,7 @@ using Content.Shared.Inventory;
using Robust.Shared.Containers;
using Robust.Shared.GameStates;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
using Robust.Shared.Serialization;
namespace Content.Shared.Clothing.Components;
@@ -25,18 +25,31 @@ public sealed partial class ToggleableClothingComponent : Component
[DataField, AutoNetworkedField]
public EntityUid? ActionEntity;
// Goobstation - ClothingPrototype and Slot Fields saved for compatibility with old prototype
/// <summary>
/// Default clothing entity prototype to spawn into the clothing container.
/// </summary>
[DataField(required: true), AutoNetworkedField]
public EntProtoId ClothingPrototype = default!;
[DataField, AutoNetworkedField]
public EntProtoId? ClothingPrototype;
/// <summary>
/// The inventory slot that the clothing is equipped to.
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
[DataField, AutoNetworkedField]
public string Slot = "head";
public string Slot = string.Empty;
/// <summary>
/// Dictionary of inventory slots and entity prototypes to spawn into the clothing container.
/// </summary>
[DataField, AutoNetworkedField]
public Dictionary<string, EntProtoId> ClothingPrototypes = new();
/// <summary>
/// Dictionary of clothing uids and slots
/// </summary>
[DataField, AutoNetworkedField]
public Dictionary<EntityUid, string> ClothingUids = new();
/// <summary>
/// The inventory slot flags required for this component to function.
@@ -51,14 +64,7 @@ public sealed partial class ToggleableClothingComponent : Component
public string ContainerId = DefaultClothingContainerId;
[ViewVariables]
public ContainerSlot? Container;
/// <summary>
/// The Id of the piece of clothing that belongs to this component. Required for map-saving if the clothing is
/// currently not inside of the container.
/// </summary>
[DataField, AutoNetworkedField]
public EntityUid? ClothingUid;
public Container? Container;
/// <summary>
/// Time it takes for this clothing to be toggled via the stripping menu verbs. Null prevents the verb from even showing up.
@@ -71,4 +77,39 @@ public sealed partial class ToggleableClothingComponent : Component
/// </summary>
[DataField, AutoNetworkedField]
public string? VerbText;
/// <summary>
/// If true it will block unequip of this entity until all attached clothing are removed
/// </summary>
[DataField, AutoNetworkedField]
public bool BlockUnequipWhenAttached = false;
/// <summary>
/// If true all attached will replace already equipped clothing on equip attempt
/// </summary>
[DataField, AutoNetworkedField]
public bool ReplaceCurrentClothing = false;
[DataField, AutoNetworkedField]
public string AttachTooltip = "toggleable-clothing-attach-tooltip";
[DataField, AutoNetworkedField]
public string UnattachTooltip = "toggleable-clothing-unattach-tooltip";
}
[Serializable, NetSerializable]
public enum ToggleClothingUiKey : byte
{
Key
}
[Serializable, NetSerializable]
public sealed class ToggleableClothingUiMessage : BoundUserInterfaceMessage
{
public NetEntity AttachedClothingUid;
public ToggleableClothingUiMessage(NetEntity attachedClothingUid)
{
AttachedClothingUid = attachedClothingUid;
}
}

View File

@@ -13,9 +13,11 @@ using Robust.Shared.Network;
using Robust.Shared.Serialization;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
using System.Linq;
namespace Content.Shared.Clothing.EntitySystems;
// GOOBSTATION - MODSUITS - THIS SYSTEM FULLY CHANGED
public sealed class ToggleableClothingSystem : EntitySystem
{
[Dependency] private readonly IGameTiming _timing = default!;
@@ -27,19 +29,22 @@ public sealed class ToggleableClothingSystem : EntitySystem
[Dependency] private readonly SharedPopupSystem _popupSystem = default!;
[Dependency] private readonly SharedDoAfterSystem _doAfter = default!;
[Dependency] private readonly SharedStrippableSystem _strippable = default!;
[Dependency] private readonly ThievingSystem _thieving = default!;
[Dependency] private readonly SharedUserInterfaceSystem _uiSystem = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<ToggleableClothingComponent, ComponentInit>(OnInit);
SubscribeLocalEvent<ToggleableClothingComponent, ComponentInit>(OnToggleableInit);
SubscribeLocalEvent<ToggleableClothingComponent, MapInitEvent>(OnMapInit);
SubscribeLocalEvent<ToggleableClothingComponent, ToggleClothingEvent>(OnToggleClothing);
SubscribeLocalEvent<ToggleableClothingComponent, ToggleClothingEvent>(OnToggleClothingAction);
SubscribeLocalEvent<ToggleableClothingComponent, GetItemActionsEvent>(OnGetActions);
SubscribeLocalEvent<ToggleableClothingComponent, ComponentRemove>(OnRemoveToggleable);
SubscribeLocalEvent<ToggleableClothingComponent, GotUnequippedEvent>(OnToggleableUnequip);
SubscribeLocalEvent<ToggleableClothingComponent, ToggleableClothingUiMessage>(OnToggleClothingMessage);
SubscribeLocalEvent<ToggleableClothingComponent, BeingUnequippedAttemptEvent>(OnToggleableUnequipAttempt);
SubscribeLocalEvent<AttachedClothingComponent, ComponentInit>(OnAttachedInit);
SubscribeLocalEvent<AttachedClothingComponent, InteractHandEvent>(OnInteractHand);
SubscribeLocalEvent<AttachedClothingComponent, GotUnequippedEvent>(OnAttachedUnequip);
SubscribeLocalEvent<AttachedClothingComponent, ComponentRemove>(OnRemoveAttached);
@@ -51,62 +56,64 @@ public sealed class ToggleableClothingSystem : EntitySystem
SubscribeLocalEvent<ToggleableClothingComponent, ToggleClothingDoAfterEvent>(OnDoAfterComplete);
}
private void GetRelayedVerbs(EntityUid uid, ToggleableClothingComponent component, InventoryRelayedEvent<GetVerbsEvent<EquipmentVerb>> args)
private void GetRelayedVerbs(Entity<ToggleableClothingComponent> toggleable, ref InventoryRelayedEvent<GetVerbsEvent<EquipmentVerb>> args)
{
OnGetVerbs(uid, component, args.Args);
OnGetVerbs(toggleable, ref args.Args);
}
private void OnGetVerbs(EntityUid uid, ToggleableClothingComponent component, GetVerbsEvent<EquipmentVerb> args)
private void OnGetVerbs(Entity<ToggleableClothingComponent> toggleable, ref GetVerbsEvent<EquipmentVerb> args)
{
if (!args.CanAccess || !args.CanInteract || component.ClothingUid == null || component.Container == null)
var comp = toggleable.Comp;
if (!args.CanAccess || !args.CanInteract || args.Hands == null || comp.ClothingUids.Count == 0 || comp.Container == null)
return;
var text = component.VerbText ?? (component.ActionEntity == null ? null : Name(component.ActionEntity.Value));
var text = comp.VerbText ?? (comp.ActionEntity == null ? null : Name(comp.ActionEntity.Value));
if (text == null)
return;
if (!_inventorySystem.InSlotWithFlags(uid, component.RequiredFlags))
if (!_inventorySystem.InSlotWithFlags(toggleable.Owner, comp.RequiredFlags))
return;
var wearer = Transform(uid).ParentUid;
if (args.User != wearer && component.StripDelay == null)
var wearer = Transform(toggleable).ParentUid;
if (args.User != wearer && comp.StripDelay == null)
return;
var user = args.User;
var verb = new EquipmentVerb()
{
Icon = new SpriteSpecifier.Texture(new("/Textures/Interface/VerbIcons/outfit.svg.192dpi.png")),
Text = Loc.GetString(text),
};
if (args.User == wearer)
if (user == wearer)
{
verb.EventTarget = uid;
verb.ExecutionEventArgs = new ToggleClothingEvent() { Performer = args.User };
verb.Act = () => ToggleClothing(user, toggleable);
}
else
{
verb.Act = () => StartDoAfter(args.User, uid, Transform(uid).ParentUid, component);
verb.Act = () => StartDoAfter(user, toggleable, wearer);
}
args.Verbs.Add(verb);
}
private void StartDoAfter(EntityUid user, EntityUid item, EntityUid wearer, ToggleableClothingComponent component)
private void StartDoAfter(EntityUid user, Entity<ToggleableClothingComponent> toggleable, EntityUid wearer)
{
if (component.StripDelay == null)
var comp = toggleable.Comp;
if (comp.StripDelay == null)
return;
var (time, stealth) = _strippable.GetStripTimeModifiers(user, wearer, component.StripDelay.Value);
var (time, stealth) = _strippable.GetStripTimeModifiers(user, wearer, comp.StripDelay.Value);
bool hidden = (stealth == ThievingStealth.Hidden);
bool hidden = stealth == ThievingStealth.Hidden;
var args = new DoAfterArgs(EntityManager, user, time, new ToggleClothingDoAfterEvent(), item, wearer, item)
var args = new DoAfterArgs(EntityManager, user, time, new ToggleClothingDoAfterEvent(), toggleable, wearer, toggleable)
{
BreakOnDamage = true,
BreakOnMove = true,
// This should just re-use the BUI range checks & cancel the do after if the BUI closes. But that is all
// server-side at the moment.
// TODO BUI REFACTOR.
DistanceThreshold = 2,
};
@@ -114,189 +121,404 @@ public sealed class ToggleableClothingSystem : EntitySystem
return;
if (!hidden)
_strippable.StripPopup("strippable-component-alert-owner-interact", stealth, wearer, user: Identity.Entity(user, EntityManager), item: item);
{
var popup = Loc.GetString("strippable-component-alert-owner-interact", ("user", Identity.Entity(user, EntityManager)), ("item", toggleable));
_popupSystem.PopupEntity(popup, wearer, wearer, PopupType.Large);
}
}
private void OnGetAttachedStripVerbsEvent(EntityUid uid, AttachedClothingComponent component, GetVerbsEvent<EquipmentVerb> args)
private void OnGetAttachedStripVerbsEvent(Entity<AttachedClothingComponent> attached, ref GetVerbsEvent<EquipmentVerb> args)
{
var comp = attached.Comp;
if (!TryComp<ToggleableClothingComponent>(comp.AttachedUid, out var toggleableComp))
return;
// redirect to the attached entity.
OnGetVerbs(component.AttachedUid, Comp<ToggleableClothingComponent>(component.AttachedUid), args);
OnGetVerbs((comp.AttachedUid, toggleableComp), ref args);
}
private void OnDoAfterComplete(EntityUid uid, ToggleableClothingComponent component, ToggleClothingDoAfterEvent args)
private void OnDoAfterComplete(Entity<ToggleableClothingComponent> toggleable, ref ToggleClothingDoAfterEvent args)
{
if (args.Cancelled)
return;
ToggleClothing(args.User, uid, component);
ToggleClothing(args.User, toggleable);
}
private void OnInteractHand(EntityUid uid, AttachedClothingComponent component, InteractHandEvent args)
private void OnInteractHand(Entity<AttachedClothingComponent> attached, ref InteractHandEvent args)
{
var comp = attached.Comp;
if (args.Handled)
return;
if (!TryComp(component.AttachedUid, out ToggleableClothingComponent? toggleCom)
|| toggleCom.Container == null)
if (!TryComp(comp.AttachedUid, out ToggleableClothingComponent? toggleableComp)
|| toggleableComp.Container == null)
return;
if (!_inventorySystem.TryUnequip(Transform(uid).ParentUid, toggleCom.Slot, force: true))
// Get slot from dictionary of uid-slot
if (!toggleableComp.ClothingUids.TryGetValue(attached.Owner, out var attachedSlot))
return;
_containerSystem.Insert(uid, toggleCom.Container);
if (!_inventorySystem.TryUnequip(Transform(attached.Owner).ParentUid, attachedSlot, force: true))
return;
_containerSystem.Insert(attached.Owner, toggleableComp.Container);
args.Handled = true;
}
/// <summary>
/// Prevents from unequipping entity if all attached not unequipped
/// </summary>
private void OnToggleableUnequipAttempt(Entity<ToggleableClothingComponent> toggleable, ref BeingUnequippedAttemptEvent args)
{
var comp = toggleable.Comp;
if (!comp.BlockUnequipWhenAttached)
return;
if (GetAttachedToggleStatus(toggleable) == ToggleableClothingAttachedStatus.NoneToggled)
return;
_popupSystem.PopupClient(Loc.GetString("toggleable-clothing-remove-all-attached-first"), args.Unequipee, args.Unequipee);
args.Cancel();
}
/// <summary>
/// Called when the suit is unequipped, to ensure that the helmet also gets unequipped.
/// </summary>
private void OnToggleableUnequip(EntityUid uid, ToggleableClothingComponent component, GotUnequippedEvent args)
private void OnToggleableUnequip(Entity<ToggleableClothingComponent> toggleable, ref GotUnequippedEvent args)
{
var comp = toggleable.Comp;
// If it's a part of PVS departure then don't handle it.
if (_timing.ApplyingState)
return;
// If the attached clothing is not currently in the container, this just assumes that it is currently equipped.
// This should maybe double check that the entity currently in the slot is actually the attached clothing, but
// if its not, then something else has gone wrong already...
if (component.Container != null && component.Container.ContainedEntity == null && component.ClothingUid != null)
_inventorySystem.TryUnequip(args.Equipee, component.Slot, force: true);
// Check if container exists and we have linked clothings
if (comp.Container == null || comp.ClothingUids.Count == 0)
return;
var parts = comp.ClothingUids;
foreach (var part in parts)
{
// Check if entity in container what means it already unequipped
if (comp.Container.Contains(part.Key) || part.Value == null)
continue;
_inventorySystem.TryUnequip(args.Equipee, part.Value, force: true);
}
}
private void OnRemoveToggleable(EntityUid uid, ToggleableClothingComponent component, ComponentRemove args)
private void OnRemoveToggleable(Entity<ToggleableClothingComponent> toggleable, ref ComponentRemove args)
{
// If the parent/owner component of the attached clothing is being removed (entity getting deleted?) we will
// delete the attached entity. We do this regardless of whether or not the attached entity is currently
// "outside" of the container or not. This means that if a hardsuit takes too much damage, the helmet will also
// automatically be deleted.
_actionsSystem.RemoveAction(component.ActionEntity);
var comp = toggleable.Comp;
if (component.ClothingUid != null && !_netMan.IsClient)
QueueDel(component.ClothingUid.Value);
_actionsSystem.RemoveAction(comp.ActionEntity);
if (comp.ClothingUids == null || _netMan.IsClient)
return;
foreach (var clothing in comp.ClothingUids.Keys)
QueueDel(clothing);
}
private void OnAttachedUnequipAttempt(EntityUid uid, AttachedClothingComponent component, BeingUnequippedAttemptEvent args)
private void OnAttachedUnequipAttempt(Entity<AttachedClothingComponent> attached, ref BeingUnequippedAttemptEvent args)
{
args.Cancel();
}
private void OnRemoveAttached(EntityUid uid, AttachedClothingComponent component, ComponentRemove args)
private void OnRemoveAttached(Entity<AttachedClothingComponent> attached, ref ComponentRemove args)
{
// if the attached component is being removed (maybe entity is being deleted?) we will just remove the
// toggleable clothing component. This means if you had a hard-suit helmet that took too much damage, you would
// still be left with a suit that was simply missing a helmet. There is currently no way to fix a partially
// broken suit like this.
if (!TryComp(component.AttachedUid, out ToggleableClothingComponent? toggleComp))
var comp = attached.Comp;
if (!TryComp(comp.AttachedUid, out ToggleableClothingComponent? toggleableComp)
|| toggleableComp.LifeStage > ComponentLifeStage.Running)
return;
if (toggleComp.LifeStage > ComponentLifeStage.Running)
var clothingUids = toggleableComp.ClothingUids;
if (!clothingUids.Remove(attached.Owner) || clothingUids.Count > 0)
return;
_actionsSystem.RemoveAction(toggleComp.ActionEntity);
RemComp(component.AttachedUid, toggleComp);
// If no attached clothing left - remove component and action
if (clothingUids.Count > 0)
return;
_actionsSystem.RemoveAction(toggleableComp.ActionEntity);
RemComp(comp.AttachedUid, toggleableComp);
}
/// <summary>
/// Called if the helmet was unequipped, to ensure that it gets moved into the suit's container.
/// Called if the clothing was unequipped, to ensure that it gets moved into the suit's container.
/// </summary>
private void OnAttachedUnequip(EntityUid uid, AttachedClothingComponent component, GotUnequippedEvent args)
private void OnAttachedUnequip(Entity<AttachedClothingComponent> attached, ref GotUnequippedEvent args)
{
// Let containers worry about it.
if (_timing.ApplyingState)
var comp = attached.Comp;
// Death told me to do this- if you need to figure out why each of these are here, idk, figure it out.
if (_timing.ApplyingState
|| comp.LifeStage > ComponentLifeStage.Running
|| !TryComp(comp.AttachedUid, out ToggleableClothingComponent? toggleableComp)
|| toggleableComp.LifeStage > ComponentLifeStage.Running
|| !toggleableComp.ClothingUids.ContainsKey(attached.Owner))
return;
if (component.LifeStage > ComponentLifeStage.Running)
return;
if (toggleableComp.Container != null)
_containerSystem.Insert(attached.Owner, toggleableComp.Container);
}
if (!TryComp(component.AttachedUid, out ToggleableClothingComponent? toggleComp))
return;
/// <summary>
/// Equip or unequip toggle clothing with ui message
/// </summary>
private void OnToggleClothingMessage(Entity<ToggleableClothingComponent> toggleable, ref ToggleableClothingUiMessage args)
{
var attachedUid = GetEntity(args.AttachedClothingUid);
if (toggleComp.LifeStage > ComponentLifeStage.Running)
return;
// As unequipped gets called in the middle of container removal, we cannot call a container-insert without causing issues.
// So we delay it and process it during a system update:
if (toggleComp.ClothingUid != null && toggleComp.Container != null)
_containerSystem.Insert(toggleComp.ClothingUid.Value, toggleComp.Container);
ToggleClothing(args.Actor, toggleable, attachedUid);
}
/// <summary>
/// Equip or unequip the toggleable clothing.
/// </summary>
private void OnToggleClothing(EntityUid uid, ToggleableClothingComponent component, ToggleClothingEvent args)
private void OnToggleClothingAction(Entity<ToggleableClothingComponent> toggleable, ref ToggleClothingEvent args)
{
var comp = toggleable.Comp;
if (args.Handled)
return;
args.Handled = true;
ToggleClothing(args.Performer, uid, component);
}
private void ToggleClothing(EntityUid user, EntityUid target, ToggleableClothingComponent component)
{
if (component.Container == null || component.ClothingUid == null)
if (comp.Container == null || comp.ClothingUids.Count == 0)
return;
var parent = Transform(target).ParentUid;
if (component.Container.ContainedEntity == null)
_inventorySystem.TryUnequip(user, parent, component.Slot, force: true);
else if (_inventorySystem.TryGetSlotEntity(parent, component.Slot, out var existing))
{
_popupSystem.PopupClient(Loc.GetString("toggleable-clothing-remove-first", ("entity", existing)),
user, user);
}
args.Handled = true;
// If clothing have only one attached clothing (like helmets) action will just toggle it
// If it have more attached clothings, it'll open radial menu
if (comp.ClothingUids.Count == 1)
ToggleClothing(args.Performer, toggleable, comp.ClothingUids.First().Key);
else
_inventorySystem.TryEquip(user, parent, component.ClothingUid.Value, component.Slot);
_uiSystem.OpenUi(toggleable.Owner, ToggleClothingUiKey.Key, args.Performer);
}
private void OnGetActions(EntityUid uid, ToggleableClothingComponent component, GetItemActionsEvent args)
/// <summary>
/// Toggle function for single clothing
/// </summary>
private void ToggleClothing(EntityUid user, Entity<ToggleableClothingComponent> toggleable, EntityUid attachedUid)
{
if (component.ClothingUid != null
&& component.ActionEntity != null
&& (args.SlotFlags & component.RequiredFlags) == component.RequiredFlags)
var comp = toggleable.Comp;
var attachedClothings = comp.ClothingUids;
var container = comp.Container;
if (!CanToggleClothing(user, toggleable))
return;
if (!attachedClothings.TryGetValue(attachedUid, out var slot) || string.IsNullOrEmpty(slot))
return;
if (!container!.Contains(attachedUid))
UnequipClothing(user, toggleable, attachedUid, slot!);
else
EquipClothing(user, toggleable, attachedUid, slot!);
}
/// <summary>
/// Toggle function for toggling multiple clothings at once
/// </summary>
private void ToggleClothing(EntityUid user, Entity<ToggleableClothingComponent> toggleable)
{
var comp = toggleable.Comp;
var attachedClothings = comp.ClothingUids;
var container = comp.Container;
if (!CanToggleClothing(user, toggleable))
return;
if (GetAttachedToggleStatus(toggleable, comp) == ToggleableClothingAttachedStatus.NoneToggled)
foreach (var clothing in attachedClothings)
EquipClothing(user, toggleable, clothing.Key, clothing.Value);
else
foreach (var clothing in attachedClothings)
if (!container!.Contains(clothing.Key))
UnequipClothing(user, toggleable, clothing.Key, clothing.Value);
}
private bool CanToggleClothing(EntityUid user, Entity<ToggleableClothingComponent> toggleable)
{
var comp = toggleable.Comp;
var attachedClothings = comp.ClothingUids;
var container = comp.Container;
if (container == null || attachedClothings.Count == 0)
return false;
var ev = new ToggleClothingAttemptEvent(user, toggleable);
RaiseLocalEvent(toggleable, ev);
return !ev.Cancelled;
}
private void UnequipClothing(EntityUid user, Entity<ToggleableClothingComponent> toggleable, EntityUid clothing, string slot)
{
var parent = Transform(toggleable.Owner).ParentUid;
_inventorySystem.TryUnequip(user, parent, slot, force: true);
// If attached have clothing in container - equip it
if (!TryComp<AttachedClothingComponent>(clothing, out var attachedComp) || attachedComp.ClothingContainer == null)
return;
var storedClothing = attachedComp.ClothingContainer.ContainedEntity;
if (storedClothing != null)
_inventorySystem.TryEquip(parent, storedClothing.Value, slot, force: true);
}
private void EquipClothing(EntityUid user, Entity<ToggleableClothingComponent> toggleable, EntityUid clothing, string slot)
{
var parent = Transform(toggleable.Owner).ParentUid;
var comp = toggleable.Comp;
if (_inventorySystem.TryGetSlotEntity(parent, slot, out var currentClothing))
{
args.AddAction(component.ActionEntity.Value);
// Check if we need to replace current clothing
if (!TryComp<AttachedClothingComponent>(clothing, out var attachedComp) || !comp.ReplaceCurrentClothing)
{
_popupSystem.PopupClient(Loc.GetString("toggleable-clothing-remove-first", ("entity", currentClothing)), user, user);
return;
}
// Check if attached clothing have container or this container not empty
if (attachedComp.ClothingContainer == null || attachedComp.ClothingContainer.ContainedEntity != null)
return;
if (_inventorySystem.TryUnequip(user, parent, slot))
_containerSystem.Insert(currentClothing.Value, attachedComp.ClothingContainer);
}
_inventorySystem.TryEquip(user, parent, clothing, slot);
}
private void OnInit(EntityUid uid, ToggleableClothingComponent component, ComponentInit args)
private void OnGetActions(Entity<ToggleableClothingComponent> toggleable, ref GetItemActionsEvent args)
{
component.Container = _containerSystem.EnsureContainer<ContainerSlot>(uid, component.ContainerId);
var comp = toggleable.Comp;
if (comp.ClothingUids.Count == 0 || comp.ActionEntity == null || args.SlotFlags != comp.RequiredFlags)
return;
args.AddAction(comp.ActionEntity.Value);
}
private void OnToggleableInit(Entity<ToggleableClothingComponent> toggleable, ref ComponentInit args)
{
var comp = toggleable.Comp;
comp.Container = _containerSystem.EnsureContainer<Container>(toggleable, comp.ContainerId);
}
private void OnAttachedInit(Entity<AttachedClothingComponent> attached, ref ComponentInit args)
{
var comp = attached.Comp;
comp.ClothingContainer = _containerSystem.EnsureContainer<ContainerSlot>(attached, comp.ClothingContainerId);
}
/// <summary>
/// On map init, either spawn the appropriate entity into the suit slot, or if it already exists, perform some
/// sanity checks. Also updates the action icon to show the toggled-entity.
/// </summary>
private void OnMapInit(EntityUid uid, ToggleableClothingComponent component, MapInitEvent args)
private void OnMapInit(Entity<ToggleableClothingComponent> toggleable, ref MapInitEvent args)
{
if (component.Container!.ContainedEntity is {} ent)
var comp = toggleable.Comp;
if (comp.Container!.Count != 0)
{
DebugTools.Assert(component.ClothingUid == ent, "Unexpected entity present inside of a toggleable clothing container.");
DebugTools.Assert(comp.ClothingUids.Count != 0, "Unexpected entity present inside of a toggleable clothing container.");
return;
}
if (component.ClothingUid != null && component.ActionEntity != null)
if (comp.ClothingUids.Count != 0 && comp.ActionEntity != null)
return;
// Add prototype from ClothingPrototype and Slot field to ClothingPrototypes dictionary
if (comp.ClothingPrototype != null && !string.IsNullOrEmpty(comp.Slot) && !comp.ClothingPrototypes.ContainsKey(comp.Slot))
{
DebugTools.Assert(Exists(component.ClothingUid), "Toggleable clothing is missing expected entity.");
DebugTools.Assert(TryComp(component.ClothingUid, out AttachedClothingComponent? comp), "Toggleable clothing is missing an attached component");
DebugTools.Assert(comp?.AttachedUid == uid, "Toggleable clothing uid mismatch");
}
else
{
var xform = Transform(uid);
component.ClothingUid = Spawn(component.ClothingPrototype, xform.Coordinates);
var attachedClothing = EnsureComp<AttachedClothingComponent>(component.ClothingUid.Value);
attachedClothing.AttachedUid = uid;
Dirty(component.ClothingUid.Value, attachedClothing);
_containerSystem.Insert(component.ClothingUid.Value, component.Container, containerXform: xform);
Dirty(uid, component);
comp.ClothingPrototypes.Add(comp.Slot, comp.ClothingPrototype.Value);
}
if (_actionContainer.EnsureAction(uid, ref component.ActionEntity, out var action, component.Action))
_actionsSystem.SetEntityIcon(component.ActionEntity.Value, component.ClothingUid, action);
var xform = Transform(toggleable.Owner);
if (comp.ClothingPrototypes == null)
return;
var prototypes = comp.ClothingPrototypes;
foreach (var prototype in prototypes)
{
var spawned = Spawn(prototype.Value, xform.Coordinates);
var attachedClothing = EnsureComp<AttachedClothingComponent>(spawned);
attachedClothing.AttachedUid = toggleable;
EnsureComp<ContainerManagerComponent>(spawned);
comp.ClothingUids.Add(spawned, prototype.Key);
_containerSystem.Insert(spawned, comp.Container, containerXform: xform);
Dirty(spawned, attachedClothing);
}
Dirty(toggleable, comp);
if (_actionContainer.EnsureAction(toggleable, ref comp.ActionEntity, out var action, comp.Action))
_actionsSystem.SetEntityIcon(comp.ActionEntity.Value, toggleable, action);
}
// Checks status of all attached clothings toggle status
public ToggleableClothingAttachedStatus GetAttachedToggleStatus(EntityUid toggleable, ToggleableClothingComponent? component = null)
{
if (!Resolve(toggleable, ref component))
return ToggleableClothingAttachedStatus.NoneToggled;
var container = component.Container;
var attachedClothings = component.ClothingUids;
// If entity don't have any attached clothings it means none toggled
if (container == null || attachedClothings.Count == 0)
return ToggleableClothingAttachedStatus.NoneToggled;
var toggledCount = attachedClothings.Count(c => !container.Contains(c.Key));
if (toggledCount == 0)
return ToggleableClothingAttachedStatus.NoneToggled;
if (toggledCount < attachedClothings.Count)
return ToggleableClothingAttachedStatus.PartlyToggled;
return ToggleableClothingAttachedStatus.AllToggled;
}
public List<EntityUid>? GetAttachedClothingsList(EntityUid toggleable, ToggleableClothingComponent? component = null)
{
if (!Resolve(toggleable, ref component) || component.ClothingUids.Count == 0)
return null;
var newList = new List<EntityUid>();
foreach (var attachee in component.ClothingUids)
newList.Add(attachee.Key);
return newList;
}
}
@@ -308,3 +530,29 @@ public sealed partial class ToggleClothingEvent : InstantActionEvent
public sealed partial class ToggleClothingDoAfterEvent : SimpleDoAfterEvent
{
}
/// <summary>
/// Event raises on toggleable clothing when someone trying to toggle it
/// </summary>
public sealed class ToggleClothingAttemptEvent : CancellableEntityEventArgs
{
public EntityUid User { get; }
public EntityUid Target { get; }
public ToggleClothingAttemptEvent(EntityUid user, EntityUid target)
{
User = user;
Target = target;
}
}
/// <summary>
/// Status of toggleable clothing attachee
/// </summary>
[Serializable, NetSerializable]
public enum ToggleableClothingAttachedStatus : byte
{
NoneToggled,
PartlyToggled,
AllToggled
}

View File

@@ -19,14 +19,14 @@ public sealed partial class DoAfterArgs
/// <summary>
/// How long does the do_after require to complete
/// </summary>
[DataField("delay", required: true)]
[DataField(required: true)]
public TimeSpan Delay;
/// <summary>
/// Applicable target (if relevant)
/// </summary>
[NonSerialized]
[DataField("target")]
[DataField]
public EntityUid? Target;
public NetEntity? NetTarget;
@@ -40,17 +40,28 @@ public sealed partial class DoAfterArgs
public NetEntity? NetUsed;
// Goobstation - Show doAfter progress bar to another entity
[NonSerialized]
[DataField]
public EntityUid? ShowTo;
public NetEntity? NetShowTo;
/// <summary>
/// Whether the progress bar for this DoAfter should be hidden from other players.
/// </summary>
[DataField]
public bool Hidden;
/// Whether the delay multiplier event should be raised
[DataField]
public bool MultiplyDelay = true;
#region Event options
/// <summary>
/// The event that will get raised when the DoAfter has finished. If null, this will simply raise a <see cref="SimpleDoAfterEvent"/>
/// </summary>
[DataField("event", required: true)]
[DataField(required: true)]
public DoAfterEvent Event = default!;
/// <summary>
@@ -64,7 +75,7 @@ public sealed partial class DoAfterArgs
/// Entity which will receive the directed event. If null, no directed event will be raised.
/// </summary>
[NonSerialized]
[DataField("eventTarget")]
[DataField]
public EntityUid? EventTarget;
public NetEntity? NetEventTarget;
@@ -72,7 +83,7 @@ public sealed partial class DoAfterArgs
/// <summary>
/// Should the DoAfter event broadcast? If this is false, then <see cref="EventTarget"/> should be a valid entity.
/// </summary>
[DataField("broadcast")]
[DataField]
public bool Broadcast;
#endregion
@@ -81,16 +92,24 @@ public sealed partial class DoAfterArgs
/// <summary>
/// Whether or not this do after requires the user to have hands.
/// </summary>
[DataField("needHand")]
[DataField]
public bool NeedHand;
/// <summary>
/// Whether we need to keep our active hand as is (i.e. can't change hand or change item). This also covers
/// requiring the hand to be free (if applicable). This does nothing if <see cref="NeedHand"/> is false.
/// </summary>
[DataField("breakOnHandChange")]
[DataField]
public bool BreakOnHandChange = true;
/// <summary>
/// Whether the do-after should get interrupted if we drop the
/// active item we started the do-after with
/// This does nothing if <see cref="NeedHand"/> is false.
/// </summary>
[DataField]
public bool BreakOnDropItem = true;
/// <summary>
/// If do_after stops when the user or target moves
/// </summary>
@@ -107,31 +126,31 @@ public sealed partial class DoAfterArgs
/// <summary>
/// Threshold for user and target movement
/// </summary>
[DataField("movementThreshold")]
[DataField]
public float MovementThreshold = 0.3f;
/// <summary>
/// Threshold for distance user from the used OR target entities.
/// </summary>
[DataField("distanceThreshold")]
[DataField]
public float? DistanceThreshold;
/// <summary>
/// Whether damage will cancel the DoAfter. See also <see cref="DamageThreshold"/>.
/// </summary>
[DataField("breakOnDamage")]
[DataField]
public bool BreakOnDamage;
/// <summary>
/// Threshold for user damage. This damage has to be dealt in a single event, not over time.
/// </summary>
[DataField("damageThreshold")]
[DataField]
public FixedPoint2 DamageThreshold = 1;
/// <summary>
/// If true, this DoAfter will be canceled if the user can no longer interact with the target.
/// </summary>
[DataField("requireCanInteract")]
[DataField]
public bool RequireCanInteract = true;
#endregion
@@ -143,7 +162,7 @@ public sealed partial class DoAfterArgs
/// Note that this will block even if the duplicate is cancelled because either DoAfter had
/// <see cref="CancelDuplicate"/> enabled.
/// </remarks>
[DataField("blockDuplicate")]
[DataField]
public bool BlockDuplicate = true;
//TODO: User pref to not cancel on second use on specific doafters
@@ -151,7 +170,7 @@ public sealed partial class DoAfterArgs
/// If true, this will cancel any duplicate DoAfters when attempting to add a new DoAfter. See also
/// <see cref="DuplicateConditions"/>.
/// </summary>
[DataField("cancelDuplicate")]
[DataField]
public bool CancelDuplicate = true;
/// <summary>
@@ -162,7 +181,7 @@ public sealed partial class DoAfterArgs
/// Note that both DoAfters may have their own conditions, and they will be considered duplicated if either set
/// of conditions is satisfied.
/// </remarks>
[DataField("duplicateCondition")]
[DataField]
public DuplicateConditions DuplicateCondition = DuplicateConditions.All;
#endregion
@@ -184,6 +203,7 @@ public sealed partial class DoAfterArgs
/// <param name="eventTarget">The entity at which the event will be directed. If null, the event will not be directed.</param>
/// <param name="target">The entity being targeted by the DoAFter. Not the same as <see cref="EventTarget"/></param>.
/// <param name="used">The entity being used during the DoAfter. E.g., a tool</param>
/// <param name="showTo">Goobstation - The entity that should see doafter progress bar except doAfter entity</param>
public DoAfterArgs(
IEntityManager entManager,
EntityUid user,
@@ -191,7 +211,8 @@ public sealed partial class DoAfterArgs
DoAfterEvent @event,
EntityUid? eventTarget,
EntityUid? target = null,
EntityUid? used = null)
EntityUid? used = null,
EntityUid? showTo = null) // Goobstation - Show doAfter popup to another entity
{
User = user;
Delay = delay;
@@ -199,18 +220,12 @@ public sealed partial class DoAfterArgs
Used = used;
EventTarget = eventTarget;
Event = @event;
ShowTo = showTo; // Goobstation
NetUser = entManager.GetNetEntity(User);
NetTarget = entManager.GetNetEntity(Target);
NetUsed = entManager.GetNetEntity(Used);
}
/// <summary>
/// An empty do-after constructor. This WILL cause runtime errors if used to create a do-after. Only use this if you really know what you're doing!
/// </summary>
[Obsolete("Use the other constructors if possible.")]
public DoAfterArgs()
{
NetShowTo = entManager.GetNetEntity(ShowTo); // Goobstation - Show doAfter popup to another entity
}
/// <summary>
@@ -248,6 +263,7 @@ public sealed partial class DoAfterArgs
Broadcast = other.Broadcast;
NeedHand = other.NeedHand;
BreakOnHandChange = other.BreakOnHandChange;
BreakOnDropItem = other.BreakOnDropItem;
BreakOnMove = other.BreakOnMove;
BreakOnWeightlessMove = other.BreakOnWeightlessMove;
MovementThreshold = other.MovementThreshold;
@@ -259,12 +275,16 @@ public sealed partial class DoAfterArgs
BlockDuplicate = other.BlockDuplicate;
CancelDuplicate = other.CancelDuplicate;
DuplicateCondition = other.DuplicateCondition;
ShowTo = other.ShowTo; // Goobstation - Show doAfter popup to another entity
MultiplyDelay = other.MultiplyDelay; // Goobstation
// Networked
NetUser = other.NetUser;
NetTarget = other.NetTarget;
NetUsed = other.NetUsed;
NetEventTarget = other.NetEventTarget;
NetShowTo = other.NetShowTo; // Goobstation - Show doAfter popup to another entity
Event = other.Event.Clone();
}

View File

@@ -130,6 +130,7 @@ public abstract partial class SharedDoAfterSystem : EntitySystem
doAfterArgs.Used = EnsureEntity<DoAfterComponent>(doAfterArgs.NetUsed, uid);
doAfterArgs.User = EnsureEntity<DoAfterComponent>(doAfterArgs.NetUser, uid);
doAfterArgs.EventTarget = EnsureEntity<DoAfterComponent>(doAfterArgs.NetEventTarget, uid);
doAfterArgs.ShowTo = EnsureEntity<DoAfterComponent>(doAfterArgs.NetShowTo, uid); // Goobstation - Show doAfter popup to another entity
}
comp.NextId = state.NextId;

View File

@@ -139,4 +139,17 @@ public partial class InventorySystem
//Try insert into hands, or drop on the floor
_handsSystem.PickupOrDrop(entity, itemToSpawn, false);
}
// Goobstation
public bool TryGetContainingEntity(Entity<TransformComponent?, MetaDataComponent?> entity, [NotNullWhen(true)] out EntityUid? containingEntity)
{
if (!_containerSystem.TryGetContainingContainer(entity, out var container) || !HasComp<InventoryComponent>(container.Owner))
{
containingEntity = null;
return false;
}
containingEntity = container.Owner;
return true;
}
}

View File

@@ -16,11 +16,20 @@ public sealed class ComponentTogglerSystem : EntitySystem
private void OnToggled(Entity<ComponentTogglerComponent> ent, ref ItemToggledEvent args)
{
var target = ent.Comp.Parent ? Transform(ent).ParentUid : ent.Owner;
ToggleComponent(ent, args.Activated);
}
if (args.Activated)
EntityManager.AddComponents(target, ent.Comp.Components);
// Goobstation - Make this system more flexible
public void ToggleComponent(EntityUid uid, bool activate)
{
if (!TryComp<ComponentTogglerComponent>(uid, out var component))
return;
var target = component.Parent ? Transform(uid).ParentUid : uid;
if (activate)
EntityManager.AddComponents(target, component.Components);
else
EntityManager.RemoveComponents(target, ent.Comp.RemoveComponents ?? ent.Comp.Components);
EntityManager.RemoveComponents(target, component.RemoveComponents ?? component.Components);
}
}

View File

@@ -0,0 +1,30 @@
using Content.Shared._Goobstation.Clothing.Systems;
using Robust.Shared.Audio;
using Robust.Shared.GameStates;
namespace Content.Shared._Goobstation.Clothing.Components;
/// Defines the clothing entity that can be sealed by <see cref="SealableClothingControlComponent"/>
[RegisterComponent]
[NetworkedComponent, AutoGenerateComponentState]
[Access(typeof(SharedSealableClothingSystem))]
public sealed partial class SealableClothingComponent : Component
{
[DataField, AutoNetworkedField]
public bool IsSealed = false;
[DataField, AutoNetworkedField]
public TimeSpan SealingTime = TimeSpan.FromSeconds(1.75);
[DataField]
public LocId SealUpPopup = "sealable-clothing-seal-up";
[DataField]
public LocId SealDownPopup = "sealable-clothing-seal-down";
[DataField]
public SoundSpecifier SealUpSound = new SoundPathSpecifier("/Audio/Mecha/mechmove03.ogg");
[DataField]
public SoundSpecifier SealDownSound = new SoundPathSpecifier("/Audio/Mecha/mechmove03.ogg");
}

View File

@@ -0,0 +1,75 @@
using Content.Shared._Goobstation.Clothing.Systems;
using Content.Shared.Inventory;
using Robust.Shared.Audio;
using Robust.Shared.GameStates;
using Robust.Shared.Prototypes;
namespace Content.Shared._Goobstation.Clothing.Components;
/// Component used to designate contol of sealable clothing. It'll contain action to seal clothing
[RegisterComponent]
[NetworkedComponent, AutoGenerateComponentState]
[Access(typeof(SharedSealableClothingSystem))]
public sealed partial class SealableClothingControlComponent : Component
{
/// Action that used to start sealing
[DataField, AutoNetworkedField]
public EntProtoId SealAction = "ActionClothingSeal";
[DataField, AutoNetworkedField]
public EntityUid? SealActionEntity;
/// Slot required for control to show action
[DataField("requiredSlot"), AutoNetworkedField]
public SlotFlags RequiredControlSlot = SlotFlags.BACK;
/// True if clothing in sealing/unsealing process, false if not
[DataField, AutoNetworkedField]
public bool IsInProcess = false;
/// True if clothing is currently sealed and need to start unsealing process. False if opposite
[DataField, AutoNetworkedField]
public bool IsCurrentlySealed = false;
/// Queue of attached parts that should be sealed/unsealed
[DataField, AutoNetworkedField]
public Queue<NetEntity> ProcessQueue = new();
/// Uid of entity that currently wear seal control
[DataField, AutoNetworkedField]
public EntityUid? WearerEntity;
/// Doafter time for other players to start sealing via stripping menu
[DataField, AutoNetworkedField]
public TimeSpan NonWearerSealingTime = TimeSpan.FromSeconds(4);
#region Popups & Sounds
[DataField]
public LocId ToggleFailedPopup = "sealable-clothing-equipment-not-toggled";
[DataField]
public LocId SealFailedPopup = "sealable-clothing-equipment-seal-failed";
[DataField]
public LocId SealedInProcessToggleFailPopup = "sealable-clothing-sealed-process-toggle-fail";
[DataField]
public LocId UnsealedInProcessToggleFailPopup = "sealable-clothing-unsealed-process-toggle-fail";
[DataField]
public LocId CurrentlySealedToggleFailPopup = "sealable-clothing-sealed-toggle-fail";
[DataField]
public LocId VerbText = "sealable-clothing-seal-verb";
[DataField]
public SoundSpecifier FailSound = new SoundPathSpecifier("/Audio/Machines/scanbuzz.ogg");
[DataField]
public SoundSpecifier SealCompleteSound = new SoundPathSpecifier("/Audio/_Goobstation/Mecha/nominal.ogg");
[DataField]
public SoundSpecifier UnsealCompleteSound = new SoundPathSpecifier("/Audio/_Goobstation/Machines/computer_end.ogg");
#endregion
}

View File

@@ -0,0 +1,30 @@
using Content.Shared.Alert;
using Robust.Shared.GameStates;
using Robust.Shared.Prototypes;
namespace Content.Shared._Goobstation.Clothing.Components;
[RegisterComponent]
[NetworkedComponent, AutoGenerateComponentState]
public sealed partial class SealableClothingRequiresPowerComponent : Component
{
[DataField]
public LocId NotPoweredPopup = "sealable-clothing-not-powered";
[DataField]
public LocId OpenSealedPanelFailPopup = "sealable-clothing-open-sealed-panel-fail";
[DataField]
public LocId ClosePanelFirstPopup = "sealable-clothing-close-panel-first";
/// Movement speed when without power
[DataField]
public float MovementSpeedPenalty = 0.3f;
[DataField, AutoNetworkedField]
public bool IsPowered = false;
/// Alert to show for the suit's power
[DataField]
public ProtoId<AlertPrototype> SuitPowerAlert = "ModsuitPower";
}

View File

@@ -0,0 +1,9 @@
using Robust.Shared.Serialization;
namespace Content.Shared._Goobstation.Clothing;
[Serializable, NetSerializable]
public enum SealableClothingVisuals : byte
{
Sealed
}

View File

@@ -0,0 +1,73 @@
using Content.Shared._Goobstation.Clothing.Components;
using Content.Shared.Inventory;
using Content.Shared.Popups;
using Content.Shared.PowerCell;
using Content.Shared.Wires;
namespace Content.Shared._Goobstation.Clothing.Systems;
/// Used for sealable clothing that requires power to work
public abstract class SharedPoweredSealableClothingSystem : EntitySystem
{
[Dependency] private readonly SharedPopupSystem _popupSystem = default!;
[Dependency] private readonly SharedPowerCellSystem _powerCellSystem = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<SealableClothingRequiresPowerComponent, MapInitEvent>(OnRequiresPowerMapInit);
SubscribeLocalEvent<SealableClothingRequiresPowerComponent, ClothingSealAttemptEvent>(OnRequiresPowerSealAttempt);
SubscribeLocalEvent<SealableClothingRequiresPowerComponent, AttemptChangePanelEvent>(OnRequiresPowerChangePanelAttempt);
}
private void OnRequiresPowerMapInit(Entity<SealableClothingRequiresPowerComponent> entity, ref MapInitEvent args)
{
if (!TryComp(entity, out SealableClothingControlComponent? control) || !TryComp(entity, out PowerCellDrawComponent? draw))
return;
draw.Enabled = control.IsCurrentlySealed;
}
/// Checks if control have enough power to seal
private void OnRequiresPowerSealAttempt(Entity<SealableClothingRequiresPowerComponent> entity, ref ClothingSealAttemptEvent args)
{
if (!TryComp(entity, out SealableClothingControlComponent? controlComp)
|| !TryComp(entity, out PowerCellDrawComponent? cellDrawComp)
|| args.Cancelled)
return;
// Prevents sealing if wires panel is opened
if (TryComp(entity, out WiresPanelComponent? panel) && panel.Open)
{
_popupSystem.PopupClient(Loc.GetString(entity.Comp.ClosePanelFirstPopup), entity, args.User);
args.Cancel();
return;
}
// Control shouldn't use charge on unsealing
if (controlComp.IsCurrentlySealed)
return;
if (!_powerCellSystem.HasDrawCharge(entity, cellDrawComp) || !_powerCellSystem.HasActivatableCharge(entity, cellDrawComp))
{
_popupSystem.PopupClient(Loc.GetString(entity.Comp.NotPoweredPopup), entity, args.User);
args.Cancel();
}
}
/// Prevents wires panel from opening if clothing is sealed
private void OnRequiresPowerChangePanelAttempt(Entity<SealableClothingRequiresPowerComponent> entity, ref AttemptChangePanelEvent args)
{
if (args.Cancelled || !TryComp(entity, out SealableClothingControlComponent? controlComp))
return;
if (controlComp.IsCurrentlySealed || controlComp.IsInProcess)
{
_popupSystem.PopupClient(Loc.GetString(entity.Comp.OpenSealedPanelFailPopup), entity, args.User);
args.Cancelled = true;
}
}
}

View File

@@ -0,0 +1,392 @@
using Content.Shared._Goobstation.Clothing.Components;
using Content.Shared.ActionBlocker;
using Content.Shared.Actions;
using Content.Shared.Clothing;
using Content.Shared.Clothing.EntitySystems;
using Content.Shared.DoAfter;
using Content.Shared.IdentityManagement;
using Content.Shared.Interaction;
using Content.Shared.Item.ItemToggle;
using Content.Shared.Popups;
using Content.Shared.PowerCell;
using Content.Shared.Verbs;
using Content.Shared.Wires;
using Robust.Shared.Audio.Systems;
using Robust.Shared.Network;
using Robust.Shared.Serialization;
using Robust.Shared.Utility;
namespace Content.Shared._Goobstation.Clothing.Systems;
/// System used for sealable clothing
public abstract class SharedSealableClothingSystem : EntitySystem
{
[Dependency] private readonly INetManager _netManager = default!;
[Dependency] private readonly ActionBlockerSystem _actionBlockerSystem = default!;
[Dependency] private readonly ActionContainerSystem _actionContainerSystem = default!;
[Dependency] private readonly ComponentTogglerSystem _componentTogglerSystem = default!;
[Dependency] private readonly SharedActionsSystem _actionsSystem = default!;
[Dependency] private readonly SharedAppearanceSystem _appearanceSystem = default!;
[Dependency] private readonly SharedAudioSystem _audioSystem = default!;
[Dependency] private readonly SharedDoAfterSystem _doAfterSystem = default!;
[Dependency] private readonly SharedInteractionSystem _interactionSystem = default!;
[Dependency] private readonly SharedPopupSystem _popupSystem = default!;
[Dependency] private readonly SharedPowerCellSystem _powerCellSystem = default!;
[Dependency] private readonly ToggleableClothingSystem _toggleableSystem = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<SealableClothingComponent, ClothingPartSealCompleteEvent>(OnPartSealingComplete);
SubscribeLocalEvent<SealableClothingControlComponent, ClothingControlSealCompleteEvent>(OnControlSealingComplete);
SubscribeLocalEvent<SealableClothingControlComponent, ClothingGotEquippedEvent>(OnControlEquip);
SubscribeLocalEvent<SealableClothingControlComponent, ClothingGotUnequippedEvent>(OnControlUnequip);
SubscribeLocalEvent<SealableClothingControlComponent, ComponentRemove>(OnControlRemove);
SubscribeLocalEvent<SealableClothingControlComponent, GetItemActionsEvent>(OnControlGetItemActions);
SubscribeLocalEvent<SealableClothingControlComponent, GetVerbsEvent<Verb>>(OnEquipmentVerb);
SubscribeLocalEvent<SealableClothingControlComponent, MapInitEvent>(OnControlMapInit);
SubscribeLocalEvent<SealableClothingControlComponent, SealClothingDoAfterEvent>(OnSealClothingDoAfter);
SubscribeLocalEvent<SealableClothingControlComponent, SealClothingEvent>(OnControlSealEvent);
//SubscribeLocalEvent<SealableClothingControlComponent, StartSealingProcessDoAfterEvent>(OnStartSealingDoAfter);
SubscribeLocalEvent<SealableClothingControlComponent, ToggleClothingAttemptEvent>(OnToggleClothingAttempt);
}
#region Events
/// Toggles components on part when suit complete sealing process
private void OnPartSealingComplete(Entity<SealableClothingComponent> part, ref ClothingPartSealCompleteEvent args)
=> _componentTogglerSystem.ToggleComponent(part, args.IsSealed);
/// Toggles components on control when suit complete sealing process
private void OnControlSealingComplete(Entity<SealableClothingControlComponent> control, ref ClothingControlSealCompleteEvent args)
=> _componentTogglerSystem.ToggleComponent(control, args.IsSealed);
/// Add/Remove wearer on clothing equip/unequip
private void OnControlEquip(Entity<SealableClothingControlComponent> control, ref ClothingGotEquippedEvent args)
{
control.Comp.WearerEntity = args.Wearer;
Dirty(control);
}
private void OnControlUnequip(Entity<SealableClothingControlComponent> control, ref ClothingGotUnequippedEvent args)
{
control.Comp.WearerEntity = null;
Dirty(control);
}
/// Removes seal action on component remove
private void OnControlRemove(Entity<SealableClothingControlComponent> control, ref ComponentRemove args)
{
var comp = control.Comp;
_actionsSystem.RemoveAction(comp.SealActionEntity);
}
/// Ensures seal action to wearer when it equip the seal control
private void OnControlGetItemActions(Entity<SealableClothingControlComponent> control, ref GetItemActionsEvent args)
{
var (uid, comp) = control;
if (comp.SealActionEntity == null || args.SlotFlags != comp.RequiredControlSlot)
return;
args.AddAction(comp.SealActionEntity.Value);
}
/// Adds unsealing verbs to sealing control allowing other users to unseal/seal clothing via stripping
private void OnEquipmentVerb(Entity<SealableClothingControlComponent> control, ref GetVerbsEvent<Verb> args)
{
var (uid, comp) = control;
var user = args.User;
if (!args.CanComplexInteract
// Since sealing control in wearer's container system just won't show verb on args.CanAccess
|| !_interactionSystem.InRangeUnobstructed(user, uid)
|| comp.WearerEntity == null
|| comp.WearerEntity != user
&& _actionBlockerSystem.CanInteract(comp.WearerEntity.Value, null))
return;
var verbIcon = comp.IsCurrentlySealed ?
new SpriteSpecifier.Texture(new("/Textures/Interface/VerbIcons/unlock.svg.192dpi.png")) :
new SpriteSpecifier.Texture(new("/Textures/Interface/VerbIcons/lock.svg.192dpi.png"));
var verb = new Verb()
{
Icon = verbIcon,
Priority = 5,
Text = Loc.GetString(comp.VerbText),
Act = () => TryStartSealToggleProcess(control, user)
};
/* This should make as do after to start unsealing of suit with verb, but, for some reason i couldn't figure out, it ends with doAfter enumerator change exception
* Would be nice if some can fix this, yet unsealing will be possible only on incapacitated wearers
if (args.User == comp.WearerEntity)
{
verb.Act = () => TryStartSealToggleProcess(control);
}
else
{
var doAfterArgs = new DoAfterArgs(EntityManager, args.User, comp.NonWearerSealingTime, new StartSealingProcessDoAfterEvent(), uid)
{
RequireCanInteract = true,
BreakOnMove = true,
BlockDuplicate = true
};
verb.Act = () => _doAfterSystem.TryStartDoAfter(doAfterArgs);
}*/
args.Verbs.Add(verb);
}
/// Ensure actionEntity on map init
private void OnControlMapInit(Entity<SealableClothingControlComponent> control, ref MapInitEvent args)
{
var (uid, comp) = control;
_actionContainerSystem.EnsureAction(uid, ref comp.SealActionEntity, comp.SealAction);
}
/* This should make as do after to start unsealing of suit with verb, but, for some reason i couldn't figure out, it ends with doAfter enumerator change exception
* Would be nice if some can fix this, yet unsealing will be possible only on incapacitated wearers
private void OnStartSealingDoAfter(Entity<SealableClothingControlComponent> control, ref StartSealingProcessDoAfterEvent args)
{
if (args.Cancelled)
return;
TryStartSealToggleProcess(control);
}*/
/// Trying to start sealing on action. It'll notify wearer if process already started
private void OnControlSealEvent(Entity<SealableClothingControlComponent> control, ref SealClothingEvent args)
{
var (uid, comp) = control;
if (!_actionBlockerSystem.CanInteract(args.Performer, null))
return;
if (comp.IsInProcess)
{
if (comp.IsCurrentlySealed)
{
_popupSystem.PopupClient(Loc.GetString(comp.SealedInProcessToggleFailPopup), uid, args.Performer);
_audioSystem.PlayPredicted(comp.FailSound, uid, args.Performer);
}
else
{
_popupSystem.PopupClient(Loc.GetString(comp.UnsealedInProcessToggleFailPopup), uid, args.Performer);
_audioSystem.PlayPredicted(comp.FailSound, uid, args.Performer);
}
return;
}
TryStartSealToggleProcess(control, args.Performer);
}
/// Toggle seal on one part and starts same process on next part
private void OnSealClothingDoAfter(Entity<SealableClothingControlComponent> control, ref SealClothingDoAfterEvent args)
{
var (uid, comp) = control;
if (args.Cancelled || args.Handled || args.Target == null)
return;
var part = args.Target;
if (!TryComp<SealableClothingComponent>(part, out var sealableComponent))
return;
sealableComponent.IsSealed = !comp.IsCurrentlySealed;
Dirty(part.Value, sealableComponent);
_audioSystem.PlayPvs(sealableComponent.SealUpSound, uid);
_appearanceSystem.SetData(part.Value, SealableClothingVisuals.Sealed, sealableComponent.IsSealed);
var ev = new ClothingPartSealCompleteEvent(sealableComponent.IsSealed);
RaiseLocalEvent(part.Value, ref ev);
NextSealProcess(control);
}
/// Prevents clothing from toggling if it's sealed or in sealing process
private void OnToggleClothingAttempt(Entity<SealableClothingControlComponent> control, ref ToggleClothingAttemptEvent args)
{
var (uid, comp) = control;
// Popup if currently sealing
if (comp.IsInProcess)
{
_popupSystem.PopupClient(Loc.GetString(comp.UnsealedInProcessToggleFailPopup), uid, args.User);
_audioSystem.PlayPredicted(comp.FailSound, uid, args.User);
args.Cancel();
return;
}
// Popup if sealed, but not in process
if (comp.IsCurrentlySealed)
{
_popupSystem.PopupClient(Loc.GetString(comp.CurrentlySealedToggleFailPopup), uid, args.User);
_audioSystem.PlayPredicted(comp.FailSound, uid, args.User);
args.Cancel();
return;
}
return;
}
#endregion
/// Tries to start sealing process
public bool TryStartSealToggleProcess(Entity<SealableClothingControlComponent> control, EntityUid? user = null)
{
var (uid, comp) = control;
// Prevent sealing/unsealing if modsuit don't have wearer or already started process
if (comp.WearerEntity == null || comp.IsInProcess)
return false;
if (user == null)
user = comp.WearerEntity;
var ev = new ClothingSealAttemptEvent(user.Value);
RaiseLocalEvent(control, ev);
if (ev.Cancelled)
return false;
// All parts required to be toggled to perform sealing
if (_toggleableSystem.GetAttachedToggleStatus(uid) != ToggleableClothingAttachedStatus.AllToggled)
{
_popupSystem.PopupClient(Loc.GetString(comp.ToggleFailedPopup), uid, user);
_audioSystem.PlayPredicted(comp.FailSound, uid, user);
return false;
}
// Trying to get all clothing to seal
var sealeableList = _toggleableSystem.GetAttachedClothingsList(uid);
if (sealeableList == null)
return false;
foreach (var sealeable in sealeableList)
{
if (!HasComp<SealableClothingComponent>(sealeable))
{
_popupSystem.PopupEntity(Loc.GetString(comp.ToggleFailedPopup), uid);
_audioSystem.PlayPredicted(comp.FailSound, uid, user);
comp.ProcessQueue.Clear();
Dirty(control);
return false;
}
comp.ProcessQueue.Enqueue(EntityManager.GetNetEntity(sealeable));
}
comp.IsInProcess = true;
Dirty(control);
NextSealProcess(control);
return true;
}
/// Recursively seals/unseals all parts of sealable clothing
private void NextSealProcess(Entity<SealableClothingControlComponent> control)
{
var (uid, comp) = control;
// Finish sealing process
if (comp.ProcessQueue.Count == 0)
{
comp.IsInProcess = false;
comp.IsCurrentlySealed = !comp.IsCurrentlySealed;
_audioSystem.PlayEntity(comp.IsCurrentlySealed ? comp.SealCompleteSound : comp.UnsealCompleteSound, comp.WearerEntity!.Value, uid);
var ev = new ClothingControlSealCompleteEvent(comp.IsCurrentlySealed);
RaiseLocalEvent(control, ref ev);
_appearanceSystem.SetData(uid, SealableClothingVisuals.Sealed, comp.IsCurrentlySealed);
Dirty(control);
return;
}
var processingPart = EntityManager.GetEntity(comp.ProcessQueue.Dequeue());
Dirty(control);
if (!TryComp<SealableClothingComponent>(processingPart, out var sealableComponent) || !comp.IsInProcess)
{
_popupSystem.PopupClient(Loc.GetString(comp.ToggleFailedPopup), uid, comp.WearerEntity);
_audioSystem.PlayPredicted(comp.FailSound, uid, comp.WearerEntity);
NextSealProcess(control);
return;
}
// If part is sealed when control trying to seal - it should just skip this part
if (sealableComponent.IsSealed != comp.IsCurrentlySealed)
{
NextSealProcess(control);
return;
}
var doAfterArgs = new DoAfterArgs(EntityManager, uid, sealableComponent.SealingTime, new SealClothingDoAfterEvent(), uid, target: processingPart, showTo: comp.WearerEntity)
{
NeedHand = false,
RequireCanInteract = false,
};
// Checking for client here to skip first process popup spam that happens. Predicted popups don't work here because doafter starts on sealable control, not on player.
if (!_doAfterSystem.TryStartDoAfter(doAfterArgs) || _netManager.IsClient)
return;
if (comp.IsCurrentlySealed)
_popupSystem.PopupEntity(Loc.GetString(sealableComponent.SealDownPopup,
("partName", Identity.Name(processingPart, EntityManager))),
uid, comp.WearerEntity!.Value);
else
_popupSystem.PopupEntity(Loc.GetString(sealableComponent.SealUpPopup,
("partName", Identity.Name(processingPart, EntityManager))),
uid, comp.WearerEntity!.Value);
}
}
[Serializable, NetSerializable]
public sealed partial class SealClothingDoAfterEvent : SimpleDoAfterEvent { }
[Serializable, NetSerializable]
public sealed partial class StartSealingProcessDoAfterEvent : SimpleDoAfterEvent { }
public sealed partial class SealClothingEvent : InstantActionEvent { }
/// Raises on control when clothing finishes it's sealing or unsealing process
[ByRefEvent]
public readonly record struct ClothingControlSealCompleteEvent(bool IsSealed)
{
public readonly bool IsSealed = IsSealed;
}
/// Raises on part when clothing finishes it's sealing or unsealing process
[ByRefEvent]
public readonly record struct ClothingPartSealCompleteEvent(bool IsSealed)
{
public readonly bool IsSealed = IsSealed;
}
public sealed partial class ClothingSealAttemptEvent : CancellableEntityEventArgs
{
public EntityUid User;
public ClothingSealAttemptEvent(EntityUid user)
{
User = user;
}
}

View File

@@ -0,0 +1,14 @@
using Content.Shared.Containers.ItemSlots;
using Robust.Shared.GameStates;
namespace Content.Shared._Goobstation.Wires.Components;
/// This is used for items slots that require entity to have wire panel for interactions
[RegisterComponent]
[NetworkedComponent]
public sealed partial class ItemSlotsRequirePanelComponent : Component
{
/// For each slot: true - slot require opened panel for interaction, false - slot require closed panel for interaction
[DataField]
public Dictionary<string, bool> Slots = new();
}

View File

@@ -0,0 +1,37 @@
using Content.Shared._Goobstation.Wires.Components;
using Content.Shared.Containers.ItemSlots;
using Content.Shared.Wires;
namespace Content.Shared._Goobstation.Wires.Systems;
public sealed partial class RequirePanelSystem : EntitySystem
{
[Dependency] private readonly ItemSlotsSystem _itemSlots = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<ItemSlotsRequirePanelComponent, ItemSlotInsertAttemptEvent>(ItemSlotInsertAttempt);
SubscribeLocalEvent<ItemSlotsRequirePanelComponent, ItemSlotEjectAttemptEvent>(ItemSlotEjectAttempt);
}
private void ItemSlotInsertAttempt(Entity<ItemSlotsRequirePanelComponent> entity, ref ItemSlotInsertAttemptEvent args)
=> args.Cancelled = !CheckPanelStateForItemSlot(entity, args.Slot.ID);
private void ItemSlotEjectAttempt(Entity<ItemSlotsRequirePanelComponent> entity, ref ItemSlotEjectAttemptEvent args)
=> args.Cancelled = !CheckPanelStateForItemSlot(entity, args.Slot.ID);
public bool CheckPanelStateForItemSlot(Entity<ItemSlotsRequirePanelComponent> entity, string? slot)
{
var (uid, comp) = entity;
if (slot == null
// If slot doesn't require a wire panel - don't cancel interaction
|| !comp.Slots.TryGetValue(slot, out var isRequireOpen)
|| !TryComp<WiresPanelComponent>(uid, out var wiresPanel))
return false;
return wiresPanel.Open == isRequireOpen;
}
}

View File

@@ -166,7 +166,7 @@
license: "CC0-1.0"
copyright: "by Ko4erga"
source: "https://github.com/space-wizards/space-station-14/pull/30431"
- files: ["double_ring.ogg"]
license: "CC0-1.0"
copyright: "Created by fspera, converted to OGG and modified by chromiumboy."
@@ -180,3 +180,8 @@
license: "CC0-1.0"
copyright: "by ScarKy0"
source: "https://github.com/space-wizards/space-station-14/pull/32012"
- files: ["scanbuzz.ogg"]
license: "CC-BY-SA-3.0"
copyright: "Taken from TG station"
source: "https://github.com/tgstation/tgstation/pull/39986"

Binary file not shown.

View File

@@ -0,0 +1,4 @@
- files: ["computer_end.ogg"]
license: "CC-BY-NC-SA-3.0"
copyright: "Taken from TG station."
source: "https://github.com/tgstation/tgstation/pull/32336"

Binary file not shown.

View File

@@ -0,0 +1,4 @@
- files: ["nominal.ogg"]
license: "CC-BY-NC-SA-3.0"
copyright: "Taken from TG station."
source: "https://github.com/tgstation/tgstation/commit/3517810d119fcb5eeb9d477c87cda3eb3cd2048c"

Binary file not shown.

View File

@@ -0,0 +1,2 @@
alerts-modsuit-power-name = [color=yellow]Modsuit Power[/color]
alerts-modsuit-power-desc = Displays the current power level of your modsuit. Low power may affect suit functionality.

View File

@@ -0,0 +1,23 @@
sealable-clothing-equipment-not-toggled = Deploy all parts first!
sealable-clothing-equipment-seal-failed = Sealing failed!
sealable-clothing-seal-verb = Toggle Seals
sealable-clothing-seal-up = The {$partName} is sealing
sealable-clothing-seal-up-helmet = The {$partName} hisses as it closes.
sealable-clothing-seal-up-gauntlets = The {$partName} tightens around your fingers and wrists.
sealable-clothing-seal-up-chestplate = The {$partName} clenches tightly around your chest.
sealable-clothing-seal-up-boots = The {$partName} seals around your feet.
sealable-clothing-seal-down = The {$partName} is unsealing
sealable-clothing-seal-down-helmet = The {$partName} hisses open.
sealable-clothing-seal-down-gauntlets = The {$partName} become loose around your fingers.
sealable-clothing-seal-down-chestplate = The {$partName} releases your chest.
sealable-clothing-seal-down-boots= The {$partName} relaxes its grip on your legs.
sealable-clothing-sealed-process-toggle-fail = Suit is already shutting down!
sealable-clothing-unsealed-process-toggle-fail = Suit is already starting up!
sealable-clothing-sealed-toggle-fail = Deactivate the suit first!
sealable-clothing-not-powered = Suit is not powered!
sealable-clothing-open-sealed-panel-fail = Wiring panel is too tightly sealed!
sealable-clothing-close-panel-first = Close the wiring panel first!

View File

@@ -0,0 +1 @@
fibers-modular = modular

View File

@@ -8,3 +8,4 @@ lathe-category-mechs-gygax = Gygax
lathe-category-mechs-durand = Durand
lathe-category-mechs-equipment = Mech equipment
lathe-category-mechs-weapons = Mech weapons
lathe-category-modsuit = MOD Suits

View File

@@ -6,3 +6,4 @@ research-technology-gygax = Gygax
research-technology-durand = Durand
research-technology-explosive-mech-ammunition = Explosive Mech Ammunition
research-technology-honk-weapons = Bananium Weapons
research-technology-modsuits = Modular Technologies

View File

@@ -1,3 +1,6 @@
toggle-clothing-verb-text = Toggle {CAPITALIZE($entity)}
toggleable-clothing-remove-first = You have to unequip {$entity} first.
toggleable-clothing-remove-all-attached-first = You have to unequip all toggled clothing first.
toggleable-clothing-attach-tooltip = Equip
toggleable-clothing-unattach-tooltip = Unequip

View File

@@ -46151,7 +46151,7 @@ entities:
occludes: True
ents:
- 6158
toggleable-clothing: !type:ContainerSlot
toggleable-clothing: !type:Container
showEnts: False
occludes: True
ent: null

View File

@@ -4,31 +4,32 @@
# If item is not in list it will go at the bottom (ties broken by alert type enum value)
id: BaseAlertOrder
order:
- category: Health
- category: Mood
- category: Stamina
- alertType: SuitPower
- category: Internals
- alertType: Fire
- alertType: Handcuffed
- alertType: Ensnared
- category: Buckled
- alertType: Pulling
- category: Piloting
- alertType: Corporeal
- alertType: Stun
- alertType: KnockedDown # WD EDIT
- category: Breathing # Vox gang not calling this oxygen
- category: Pressure
- alertType: Bleed
- category: Temperature
- category: Hunger
- category: Thirst
- alertType: Magboots
- alertType: Pacified
- alertType: Offer
- alertType: RecentlyBlocked # WD EDIT
- alertType: Deflecting
- category: Health
- category: Mood
- category: Stamina
- alertType: SuitPower
- alertType: ModsuitPower # Goobstation - Modsuits
- category: Internals
- alertType: Fire
- alertType: Handcuffed
- alertType: Ensnared
- category: Buckled
- alertType: Pulling
- category: Piloting
- alertType: Corporeal
- alertType: Stun
- alertType: KnockedDown # WD EDIT
- category: Breathing # Vox gang not calling this oxygen
- category: Pressure
- alertType: Bleed
- category: Temperature
- category: Hunger
- category: Thirst
- alertType: Magboots
- alertType: Pacified
- alertType: Offer
- alertType: Deflecting
- alertType: RecentlyBlocked # WD EDIT
- type: entity
id: AlertSpriteView
@@ -51,8 +52,8 @@
id: LowNitrogen
category: Breathing
icons:
- sprite: /Textures/Interface/Alerts/breathing.rsi
state: not_enough_nitro
- sprite: /Textures/Interface/Alerts/breathing.rsi
state: not_enough_nitro
name: alerts-low-nitrogen-name
description: alerts-low-nitrogen-desc
@@ -508,8 +509,8 @@
category: Mood
onClick: !type:ShowMoodEffects { }
icons:
- sprite: /Textures/Interface/Alerts/mood.rsi
state: mood_insane
- sprite: /Textures/Interface/Alerts/mood.rsi
state: mood_insane
name: alerts-mood-insane-name
description: alerts-mood-insane-desc
@@ -518,8 +519,8 @@
category: Mood
onClick: !type:ShowMoodEffects { }
icons:
- sprite: /Textures/Interface/Alerts/mood.rsi
state: mood1
- sprite: /Textures/Interface/Alerts/mood.rsi
state: mood1
name: alerts-mood-horrible-name
description: alerts-mood-horrible-desc
@@ -528,8 +529,8 @@
category: Mood
onClick: !type:ShowMoodEffects { }
icons:
- sprite: /Textures/Interface/Alerts/mood.rsi
state: mood2
- sprite: /Textures/Interface/Alerts/mood.rsi
state: mood2
name: alerts-mood-terrible-name
description: alerts-mood-terrible-desc
@@ -538,8 +539,8 @@
category: Mood
onClick: !type:ShowMoodEffects { }
icons:
- sprite: /Textures/Interface/Alerts/mood.rsi
state: mood3
- sprite: /Textures/Interface/Alerts/mood.rsi
state: mood3
name: alerts-mood-bad-name
description: alerts-mood-bad-desc
@@ -548,8 +549,8 @@
category: Mood
onClick: !type:ShowMoodEffects { }
icons:
- sprite: /Textures/Interface/Alerts/mood.rsi
state: mood4
- sprite: /Textures/Interface/Alerts/mood.rsi
state: mood4
name: alerts-mood-meh-name
description: alerts-mood-meh-desc
@@ -558,8 +559,8 @@
category: Mood
onClick: !type:ShowMoodEffects { }
icons:
- sprite: /Textures/Interface/Alerts/mood.rsi
state: mood5
- sprite: /Textures/Interface/Alerts/mood.rsi
state: mood5
name: alerts-mood-neutral-name
description: alerts-mood-neutral-desc
@@ -568,8 +569,8 @@
category: Mood
onClick: !type:ShowMoodEffects { }
icons:
- sprite: /Textures/Interface/Alerts/mood.rsi
state: mood6
- sprite: /Textures/Interface/Alerts/mood.rsi
state: mood6
name: alerts-mood-good-name
description: alerts-mood-good-desc
@@ -578,8 +579,8 @@
category: Mood
onClick: !type:ShowMoodEffects { }
icons:
- sprite: /Textures/Interface/Alerts/mood.rsi
state: mood7
- sprite: /Textures/Interface/Alerts/mood.rsi
state: mood7
name: alerts-mood-great-name
description: alerts-mood-great-desc
@@ -588,8 +589,8 @@
category: Mood
onClick: !type:ShowMoodEffects { }
icons:
- sprite: /Textures/Interface/Alerts/mood.rsi
state: mood8
- sprite: /Textures/Interface/Alerts/mood.rsi
state: mood8
name: alerts-mood-exceptional-name
description: alerts-mood-exceptional-desc
@@ -598,8 +599,8 @@
category: Mood
onClick: !type:ShowMoodEffects { }
icons:
- sprite: /Textures/Interface/Alerts/mood.rsi
state: mood9
- sprite: /Textures/Interface/Alerts/mood.rsi
state: mood9
name: alerts-mood-perfect-name
description: alerts-mood-perfect-desc
@@ -608,8 +609,8 @@
category: Mood
onClick: !type:ShowMoodEffects { }
icons:
- sprite: /Textures/Interface/Alerts/mood.rsi
state: mood_happiness_bad
- sprite: /Textures/Interface/Alerts/mood.rsi
state: mood_happiness_bad
name: alerts-mood-dead-name
description: alerts-mood-dead-desc

View File

@@ -8,7 +8,7 @@
sprite: Clothing/Neck/Cloaks/centcomcloakformal.rsi
- type: StealTarget
stealGroup: HeadCloak # leaving this here because I suppose it might be interesting?
- type: entity
parent: ClothingNeckBase
id: ClothingNeckCloakCap
@@ -159,7 +159,7 @@
slot: head
- type: ContainerContainer
containers:
toggleable-clothing: !type:ContainerSlot {}
toggleable-clothing: !type:Container {}
- type: entity
parent: ClothingNeckBase
@@ -185,7 +185,7 @@
slot: head
- type: ContainerContainer
containers:
toggleable-clothing: !type:ContainerSlot {}
toggleable-clothing: !type:Container {}
- type: TypingIndicatorClothing
proto: moth
@@ -287,4 +287,4 @@
slot: head
- type: ContainerContainer
containers:
toggleable-clothing: !type:ContainerSlot {}
toggleable-clothing: !type:Container {}

View File

@@ -120,7 +120,7 @@
slot: head
- type: ContainerContainer
containers:
toggleable-clothing: !type:ContainerSlot {}
toggleable-clothing: !type:Container {}
storagebase: !type:Container
ents: []
@@ -155,7 +155,7 @@
slot: head
- type: ContainerContainer
containers:
toggleable-clothing: !type:ContainerSlot {}
toggleable-clothing: !type:Container {}
- type: GroupExamine
- type: Tag
tags:
@@ -242,7 +242,7 @@
slot: head
- type: ContainerContainer
containers:
toggleable-clothing: !type:ContainerSlot {}
toggleable-clothing: !type:Container {}
storagebase: !type:Container
ents: []

View File

@@ -58,7 +58,7 @@
slot: head
- type: ContainerContainer
containers:
toggleable-clothing: !type:ContainerSlot {}
toggleable-clothing: !type:Container {}
#Prisoner EVA
- type: entity

View File

@@ -144,7 +144,7 @@
slot: head
- type: ContainerContainer
containers:
toggleable-clothing: !type:ContainerSlot {}
toggleable-clothing: !type:Container {}
- type: ClothingRequiredStepTriggerImmune
slots: WITHOUT_POCKET
- type: Tag
@@ -251,7 +251,7 @@
sprite: Clothing/OuterClothing/Suits/monkey.rsi
- type: ContainerContainer
containers:
toggleable-clothing: !type:ContainerSlot {}
toggleable-clothing: !type:Container {}
- type: ClothingRequiredStepTriggerImmune
slots: WITHOUT_POCKET
- type: Tag
@@ -274,7 +274,7 @@
clothingPrototype: ClothingHeadHatHoodIan
- type: ContainerContainer
containers:
toggleable-clothing: !type:ContainerSlot {}
toggleable-clothing: !type:Container {}
- type: Construction
graph: ClothingOuterSuitIan
node: suit
@@ -297,7 +297,7 @@
clothingPrototype: ClothingHeadHatHoodCarp
- type: ContainerContainer
containers:
toggleable-clothing: !type:ContainerSlot {}
toggleable-clothing: !type:Container {}
- type: entity
parent: ClothingOuterSuitCarp
@@ -321,4 +321,4 @@
- type: Sprite
sprite: Clothing/OuterClothing/Suits/witchrobe.rsi
- type: Clothing
sprite: Clothing/OuterClothing/Suits/witchrobe.rsi
sprite: Clothing/OuterClothing/Suits/witchrobe.rsi

View File

@@ -45,7 +45,7 @@
slot: head
- type: ContainerContainer
containers:
toggleable-clothing: !type:ContainerSlot {}
toggleable-clothing: !type:Container {}
storagebase: !type:Container
ents: []

View File

@@ -687,6 +687,8 @@
- TorsoBorgService
- MechAirTank # Goobstation
- MechThruster # Goobstation
- PowerCageMedium # Goobstation - Powercell to exosuit fab
- PowerCageSmall # Goobstation - Powercell to exosuit fab
dynamicRecipes:
- ProximitySensor
- BorgModuleLightReplacer
@@ -762,6 +764,14 @@
- MechEquipmentKineticAccelerator
- MechEquipmentHonkerBananaMortar
- MechEquipmentHonkerMousetrapMortar
# Goobstation - Modsuits
- ModsuitChestplate
- ModsuitBoots
- ModsuitHelmet
- ModsuitGauntlets
- ModsuitShell
- ModsuitPlatingExternal
- PowerCageHigh # Goobstation - Powercell to exosuit fab
- type: EmagLatheRecipes
emagDynamicRecipes:
- WeaponMechCombatImmolationGun

View File

@@ -246,6 +246,7 @@
components:
- BorgChassis
- Silicon # Parkstation IPCs
- Inventory # Goobstation - Modsuits
- type: Construction
containers:
- machine_parts

View File

@@ -74,7 +74,7 @@
clothingPrototype: ClothingHeadHatHoodCultHoodTrue
- type: ContainerContainer
containers:
toggleable-clothing: !type:ContainerSlot { }
toggleable-clothing: !type:Container { }
- type: entity
parent: ClothingHeadHatHoodCulthood

View File

@@ -0,0 +1,20 @@
- type: entity
id: ActionClothingSeal
name: Seal/Unseal Clothing
description: Seals or unseals your current clothing.
categories: [ HideSpawnMenu ]
components:
- type: ConfirmableAction
confirmDelay: 0
primeTime: 2
- type: InstantAction
checkCanInteract: true
checkConsciousness: true
itemIconStyle: NoItem
icon:
sprite: _Goobstation/Actions/modsuit.rsi
state: activate
iconOn:
sprite: _Goobstation/Actions/modsuit.rsi
state: activate-ready
event: !type:SealClothingEvent {}

View File

@@ -0,0 +1,19 @@
- type: alert
id: ModsuitPower
icons:
- sprite: /Textures/_Goobstation/Interface/Alerts/modpower.rsi
state: modpower0
- sprite: /Textures/_Goobstation/Interface/Alerts/modpower.rsi
state: modpower1
- sprite: /Textures/_Goobstation/Interface/Alerts/modpower.rsi
state: modpower2
- sprite: /Textures/_Goobstation/Interface/Alerts/modpower.rsi
state: modpower3
- sprite: /Textures/_Goobstation/Interface/Alerts/modpower.rsi
state: modpower4
- sprite: /Textures/_Goobstation/Interface/Alerts/modpower.rsi
state: modpower5
name: alerts-modsuit-power-name
description: alerts-modsuit-power-desc
minSeverity: 0
maxSeverity: 5

View File

@@ -0,0 +1,9 @@
- type: cargoProduct
id: ScienceModsuitCores
icon:
sprite: _Goobstation/Objects/Specific/Robotics/modsuit_parts.rsi
state: mod-core-standard
product: CrateScienceModsuitCoresFilled
cost: 3000
category: cargoproduct-category-name-science
group: market

View File

@@ -0,0 +1,10 @@
- type: entity
id: CrateScienceModsuitCoresFilled
parent: CrateScienceSecure
name: MOD cores crate
description: Contains three MOD cores inside.
components:
- type: StorageFill
contents:
- id: ModsuitCoreStandard
amount: 3

View File

@@ -0,0 +1,92 @@
- type: entity
parent: [Clothing, ContentsExplosionResistanceBase]
id: ClothingModsuitStandard
name: standard modsuit control
description: A special modular suit contol containing all modular suit parts.
components:
- type: Appearance
- type: Sprite
sprite: _Goobstation/Clothing/Back/Modsuits/standard.rsi
layers:
- state: control
- state: control-sealed
visible: false
shader: unshaded
map: [ "sealed" ]
- type: Item
size: Huge
- type: Clothing
quickEquip: false
slots:
- back
- type: Storage
grid:
- 0,0,6,3
maxItemSize: Huge
- type: ContainerContainer
containers:
storagebase: !type:Container
ents: []
toggleable-clothing: !type:Container
cell_slot: !type:ContainerSlot
- type: UserInterface
interfaces:
enum.StorageUiKey.Key:
type: StorageBoundUserInterface
enum.ToggleClothingUiKey.Key:
type: ToggleableClothingBoundUserInterface
- type: UseDelay
delay: 0.5
- type: ExplosionResistance
damageCoefficient: 0.9
- type: ToggleableClothing
requiredSlot: back
blockUnequipWhenAttached: true
replaceCurrentClothing: true
clothingPrototypes:
head: ClothingModsuitHelmetStandard
gloves: ClothingModsuitGauntletsStandard
outerClothing: ClothingModsuitChestplateStandard
shoes: ClothingModsuitBootsStandard
- type: WiresPanel
- type: ItemSlots
slots:
cell_slot:
name: power-cell-slot-component-slot-name-default
whitelist:
components:
- PowerCell
- type: ItemSlotsRequirePanel
slots:
cell_slot: true
- type: PowerCellDraw
drawRate: 1 # Sealed draw rate
useRate: 5 # Draw rate for sealing
- type: PowerCellSlot
cellSlotId: cell_slot
fitsInCharger: false
- type: DoAfter
- type: SealableClothingControl
- type: SealableClothingRequiresPower
- type: SealableClothingVisuals
visualLayers:
back:
- state: equipped-BACKPACK-sealed
shader: unshaded
- type: Construction
graph: Modsuit
node: standard
- type: entity
parent: ClothingModsuitStandard
id: ClothingModsuitStandardPowerCell
suffix: High-Capacity Battery
components:
- type: ItemSlots
slots:
cell_slot:
name: power-cell-slot-component-slot-name-default
startingItem: PowerCellHigh
whitelist:
components:
- PowerCell

View File

@@ -0,0 +1,32 @@
- type: entity
parent: ClothingHandsBase
id: ClothingModsuitGauntletsStandard
name: standard modsuit gauntlets
description: A special modular suit gloves that protect wearer from electric shock.
categories: [ HideSpawnMenu ]
components:
- type: Appearance
- type: Sprite
sprite: _Goobstation/Clothing/Hands/Modsuits/standard.rsi
layers:
- state: gauntlets
- state: gauntlets-sealed
visible: false
map: [ "sealed" ]
- type: Clothing
equipSound: /Audio/Mecha/mechmove03.ogg
unequipSound: /Audio/Mecha/mechmove03.ogg
slots: [ gloves ]
- type: Insulated
- type: Fiber
fiberMaterial: fibers-modular
fiberColor: fibers-black
- type: FingerprintMask
- type: SealableClothing
sealUpPopup: sealable-clothing-seal-up-gauntlets
sealDownPopup: sealable-clothing-seal-down-gauntlets
- type: SealableClothingVisuals
visualLayers:
gloves:
- state: equipped-HAND-sealed
shader: unshaded

View File

@@ -0,0 +1,128 @@
- type: entity
abstract: true
# Used to put pressureProtection, layers blocker and etc components into ComponentToggler
id: BaseClothingModsuitHelmet
name: base modsuit helmet
categories: [ HideSpawnMenu ]
components:
- type: Sprite
state: icon
- type: Clickable
- type: InteractionOutline
- type: GroupExamine
- type: Clothing
equippedPrefix: off
equipSound: /Audio/Mecha/mechmove03.ogg
unequipSound: /Audio/Mecha/mechmove03.ogg
quickEquip: false
slots: [ HEAD ]
clothingVisuals:
head:
- state: equipped-HEAD
- type: Tag
tags:
- WhitelistChameleon
- type: entity
abstract: true
parent: BaseClothingModsuitHelmet
# Used for helmets that hide your identity even if it's not sealed
id: BaseClothingModsuitHelmetHideIdentity
name: base modsuit helmet
categories: [ HideSpawnMenu ]
components:
- type: IdentityBlocker
- type: IngestionBlocker
- type: HideLayerClothing
slots:
- Hair
- Snout
- HeadTop
- HeadSide
- type: entity
parent: BaseClothingModsuitHelmet
id: ClothingModsuitHelmetStandard
name: standard modsuit helmet
description: A special modular suit spaceproof helmet designed for compact folding inside modular suit control.
categories: [ HideSpawnMenu ]
components:
- type: Appearance
- type: Sprite
sprite: _Goobstation/Clothing/Head/Modsuits/standard.rsi
layers:
- state: helmet
- state: helmet-sealed
visible: false
map: [ "sealed" ]
- type: HideLayerClothing # This helmet don't have sprite on unsealed state
slots:
- Snout
- type: SealableClothing
sealUpPopup: sealable-clothing-seal-up-helmet
sealDownPopup: sealable-clothing-seal-down-helmet
- type: SealableClothingVisuals
visualLayers:
head:
- state: equipped-HEAD-sealed
- type: Armor
modifiers:
coefficients:
Blunt: 0.90
Slash: 0.90
Piercing: 0.95
Heat: 0.90
Radiation: 0.75
- type: ComponentToggler
components:
- type: BreathMask
- type: PressureProtection
highPressureMultiplier: 0.3
lowPressureMultiplier: 1000
- type: TemperatureProtection
coefficient: 0.1
- type: IdentityBlocker
- type: IngestionBlocker
- type: HideLayerClothing
slots:
- Hair
- Snout
- HeadTop
- HeadSide
# This will all be replaced by modules later
- type: ToggleableLightVisuals
- type: PointLight
enabled: false
radius: 3
energy: 2
mask: /Textures/Effects/LightMasks/cone.png
autoRot: true
netsync: false
- type: HandheldLight
addPrefix: true
blinkingBehaviourId: blinking
radiatingBehaviourId: radiating
- type: LightBehaviour
behaviours:
- !type:FadeBehaviour
id: radiating
interpolate: Linear
maxDuration: 2.0
startValue: 3.0
endValue: 2.0
isLooped: true
reverseWhenFinished: true
- !type:PulseBehaviour
id: blinking
interpolate: Nearest
maxDuration: 1.0
minValue: 0.1
maxValue: 2.0
isLooped: true
- type: Battery
maxCharge: 600 # Lights drain 3/s but recharge of 2 makes this 1/s, therefore 600 is 10 minutes of light
startingCharge: 600
- type: BatterySelfRecharger
autoRecharge: true
autoRechargeRate: 2

View File

@@ -0,0 +1,47 @@
- type: entity
parent: ClothingOuterBase
id: ClothingModsuitChestplateStandard
name: standard modsuit chestplate
description: A special modular suit spaceproof cover designed for compact folding inside modular suit control.
categories: [ HideSpawnMenu ]
components:
- type: Appearance
- type: AllowSuitStorage
- type: Sprite
sprite: _Goobstation/Clothing/OuterClothing/Modsuits/standard.rsi
layers:
- state: chestplate
- state: chestplate-sealed
visible: false
map: [ "sealed" ]
- type: Clothing
equipSound: /Audio/Mecha/mechmove03.ogg
unequipSound: /Audio/Mecha/mechmove03.ogg
slots: [ outerClothing ]
- type: ClothingSpeedModifier
walkModifier: 0.9
sprintModifier: 0.9
- type: SealableClothing
sealUpPopup: sealable-clothing-seal-up-chestplate
sealDownPopup: sealable-clothing-seal-down-chestplate
- type: SealableClothingVisuals
visualLayers:
outerClothing:
- state: equipped-OUTERCLOTHING-sealed
shader: unshaded
- type: Armor
modifiers:
coefficients:
Blunt: 0.85
Slash: 0.85
Piercing: 0.85
Heat: 0.85
Radiation: 0.6
- type: ComponentToggler
components:
- type: PressureProtection
highPressureMultiplier: 0.3
lowPressureMultiplier: 1000
- type: TemperatureProtection
coefficient: 0.1

View File

@@ -0,0 +1,33 @@
- type: entity
parent: ClothingShoesBase
id: ClothingModsuitBootsStandard
name: standard modsuit boots
description: A special modular suit boots designed for compact folding inside modular suit control.
categories: [ HideSpawnMenu ]
components:
- type: Appearance
- type: Sprite
sprite: _Goobstation/Clothing/Shoes/Modsuits/standard.rsi
layers:
- state: boots
- state: boots-sealed
visible: false
map: [ "sealed" ]
- type: Clothing
equipSound: /Audio/Mecha/mechmove03.ogg
unequipSound: /Audio/Mecha/mechmove03.ogg
slots: [ feet ]
- type: Tag
tags:
- WhitelistChameleon
- type: SealableClothing
sealUpPopup: sealable-clothing-seal-up-boots
sealDownPopup: sealable-clothing-seal-down-boots
- type: ClothingSpeedModifier
walkModifier: 0.9
sprintModifier: 0.9
- type: SealableClothingVisuals
visualLayers:
shoes:
- state: equipped-FEET-sealed
shader: unshaded

View File

@@ -0,0 +1,162 @@
- type: entity
id: PartModsuit
parent: BaseItem
name: MOD part
description: A part used in MOD construction.
abstract: true
components:
- type: Sprite
sprite: _Goobstation/Objects/Specific/Robotics/modsuit_parts.rsi
- type: Icon
sprite: _Goobstation/Objects/Specific/Robotics/modsuit_parts.rsi
- type: Item
size: Normal
- type: ContainerContainer
containers:
bodypart: !type:Container
ents: []
#- type: GuideHelp TODO: Guide
- type: entity
id: ModsuitChestplate
parent: PartModsuit
name: MOD chestplate
components:
- type: Sprite
state: chestplate
- type: Icon
state: chestplate
- type: PhysicalComposition
materialComposition:
Steel: 62
- type: Tag
tags:
- ModsuitPart
- ModsuitChestplate
- type: entity
id: ModsuitBoots
parent: PartModsuit
name: MOD boots
components:
- type: Sprite
state: boots
- type: Icon
state: boots
- type: PhysicalComposition
materialComposition:
Steel: 62
- type: Tag
tags:
- ModsuitPart
- ModsuitBoots
- type: entity
id: ModsuitHelmet
parent: PartModsuit
name: MOD helmet
components:
- type: Sprite
state: helmet
- type: Icon
state: helmet
- type: PhysicalComposition
materialComposition:
Steel: 62
- type: Tag
tags:
- ModsuitPart
- ModsuitHelmet
- type: entity
id: ModsuitGauntlets
parent: PartModsuit
name: MOD gauntlets
components:
- type: Sprite
state: gauntlets
- type: Icon
state: gauntlets
- type: PhysicalComposition
materialComposition:
Steel: 62
- type: Tag
tags:
- ModsuitPart
- ModsuitGauntlets
- type: entity
id: ModsuitShell
parent: PartModsuit
name: MOD shell
components:
- type: Appearance
- type: Sprite
state: shell
- type: Icon
state: shell
- type: PhysicalComposition
materialComposition:
Steel: 125
Plasma: 62
- type: ContainerContainer
containers:
cell_slot: !type:Container
core-container: !type:Container
- type: Construction
graph: Modsuit
node: start
defaultTarget: standard
containers:
- cell_slot
- core-container
- type: GenericVisualizer
visuals:
enum.ConstructionVisuals.Key:
enum.ConstructionVisuals.Layer:
shell-core: { state: shell-core }
shell-core-secured: { state: shell-core-secured }
shell-helmet: { state: shell-helmet }
shell-chestplate: { state: shell-chestplate }
shell-gauntlets: { state: shell-gauntlets }
shell-boots: { state: shell-boots }
shell-secured: { state: shell-secured }
- type: entity
id: ModsuitPlatingExternal
parent: PartModsuit
name: MOD standard external plating
description: A part used in MOD construction.
components:
- type: Sprite
state: standard-plating
- type: Icon
state: standard-plating
- type: PhysicalComposition
materialComposition:
Steel: 75
Glass: 37
Plasma: 12
- type: Tag
tags:
- ModsuitPart
- ModsuitPlatingExternal
- type: entity
id: ModsuitCoreStandard
parent: PartModsuit
name: MOD standard core
description: Growing in the most lush, fertile areas of the planet Sprout, there is a crystal known as the Heartbloom. These rare, organic piezoelectric crystals are of incredible cultural significance to the artist castes of the Ethereals, owing to their appearance; which is exactly similar to that of an Ethereal's heart. Which one you have in your suit is unclear, but either way, it's been repurposed to be an internal power source for a Modular Outerwear Device.
components:
- type: Sprite
state: mod-core-standard
- type: Icon
state: mod-core-standard
- type: Tag
tags:
- ModsuitPart
- ModsuitCore
- type: PhysicalComposition
materialComposition:
Plasma: 50
Glass: 25

View File

@@ -0,0 +1,113 @@
- type: constructionGraph
id: Modsuit
start: start
graph:
- node: start
entity: ModsuitShell
edges:
- to: shell-core
steps:
- tag: ModsuitCore
name: MOD core
store: core-container
completed:
- !type:PlaySound
sound: /Audio/Items/screwdriver2.ogg
- node: shell-core
actions:
- !type:AppearanceChange
edges:
- to: start
steps:
- tool: Prying
completed:
- !type:EmptyContainer
container: core-container
- to: shell-core-secured
steps:
- tool: Screwing
doAfter: 1
- node: shell-core-secured
actions:
- !type:AppearanceChange
edges:
- to: shell-helmet
steps:
- tag: ModsuitHelmet
name: MOD helmet
doAfter: 1
completed:
- !type:PlaySound
sound: /Audio/Items/screwdriver2.ogg
- node: shell-helmet
actions:
- !type:AppearanceChange
edges:
- to: shell-chestplate
steps:
- tag: ModsuitChestplate
name: MOD chestplate
doAfter: 1
completed:
- !type:PlaySound
sound: /Audio/Items/screwdriver2.ogg
- node: shell-chestplate
actions:
- !type:AppearanceChange
edges:
- to: shell-gauntlets
steps:
- tag: ModsuitGauntlets
name: MOD gauntlets
doAfter: 1
completed:
- !type:PlaySound
sound: /Audio/Items/screwdriver2.ogg
- node: shell-gauntlets
actions:
- !type:AppearanceChange
edges:
- to: shell-boots
steps:
- tag: ModsuitBoots
name: MOD boots
doAfter: 1
completed:
- !type:PlaySound
sound: /Audio/Items/screwdriver2.ogg
- node: shell-boots
actions:
- !type:AppearanceChange
edges:
- to: shell-secured
steps:
- tool: Anchoring
doAfter: 1
- tool: Screwing
doAfter: 1
- node: shell-secured
actions:
- !type:AppearanceChange
edges:
- to: standard
steps:
- tag: ModsuitPlatingExternal
name: any MOD plating
doAfter: 1
completed:
- !type:PlaySound
sound: /Audio/Items/screwdriver2.ogg
- tool: Anchoring
doAfter: 1
- node: standard
entity: ClothingModsuitStandard

View File

@@ -38,3 +38,7 @@
- type: latheCategory
id: MechWeapons
name: lathe-category-mechs-weapons
- type: latheCategory
id: Modsuit
name: lathe-category-modsuit

View File

@@ -0,0 +1,50 @@
- type: latheRecipe
id: ModsuitChestplate
result: ModsuitChestplate
category: Modsuit
completetime: 5
materials:
Steel: 250
- type: latheRecipe
id: ModsuitBoots
result: ModsuitBoots
category: Modsuit
completetime: 5
materials:
Steel: 250
- type: latheRecipe
id: ModsuitHelmet
result: ModsuitHelmet
category: Modsuit
completetime: 5
materials:
Steel: 250
- type: latheRecipe
id: ModsuitGauntlets
result: ModsuitGauntlets
category: Modsuit
completetime: 5
materials:
Steel: 250
- type: latheRecipe
id: ModsuitShell
result: ModsuitShell
category: Modsuit
completetime: 5
materials:
Steel: 500
Plasma: 250
- type: latheRecipe
id: ModsuitPlatingExternal
result: ModsuitPlatingExternal
category: Modsuit
completetime: 5
materials:
Steel: 300
Glass: 150
Plasma: 50

View File

@@ -0,0 +1,19 @@
- type: technology
id: ModsuitsTech
name: research-technology-modsuits
icon:
sprite: _Goobstation/Objects/Specific/Robotics/modsuit_parts.rsi
state: mod-core-standard
discipline: Experimental
tier: 3
cost: 5000
technologyPrerequisites:
- AdvancedTacsuits
softCapContribution: 1.5
recipeUnlocks:
- ModsuitChestplate
- ModsuitBoots
- ModsuitHelmet
- ModsuitGauntlets
- ModsuitShell
- ModsuitPlatingExternal

View File

@@ -96,7 +96,7 @@
- type: Tag
id: RipleyMkII
- type: Tag
id: Clarke
@@ -104,4 +104,30 @@
id: Durand
- type: Tag
id: Gygax
id: Gygax
# MODsuits
- type: Tag
id: ModsuitPart
- type: Tag
id: ModsuitShell
- type: Tag
id: ModsuitGauntlets
- type: Tag
id: ModsuitHelmet
- type: Tag
id: ModsuitBoots
- type: Tag
id: ModsuitChestplate
- type: Tag
id: ModsuitCore
- type: Tag
id: ModsuitPlatingExternal

Binary file not shown.

After

Width:  |  Height:  |  Size: 811 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 252 B

View File

@@ -0,0 +1,17 @@
{
"version": 1,
"license": "CC-BY-SA-3.0",
"copyright": "From tgstation - https://github.com/tgstation/tgstation/pull/59109",
"size": {
"x": 32,
"y": 32
},
"states": [
{
"name": "activate"
},
{
"name": "activate-ready"
}
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 390 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 238 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 392 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 673 B

View File

@@ -0,0 +1,26 @@
{
"version": 1,
"license": "CC-BY-SA-3.0",
"copyright": "From tgstation - https://github.com/tgstation/tgstation/pull/59109",
"size": {
"x": 32,
"y": 32
},
"states": [
{
"name": "control"
},
{
"name": "control-sealed",
"delays": [[ 0.1, 0.1, 0.1, 0.1 ]]
},
{
"name": "equipped-BACKPACK",
"directions": 4
},
{
"name": "equipped-BACKPACK-sealed",
"directions": 4
}
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 145 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 496 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 277 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 270 B

View File

@@ -0,0 +1,25 @@
{
"version": 1,
"license": "CC-BY-SA-3.0",
"copyright": "From tgstation - https://github.com/tgstation/tgstation/pull/59109",
"size": {
"x": 32,
"y": 32
},
"states": [
{
"name": "gauntlets"
},
{
"name": "gauntlets-sealed"
},
{
"name": "equipped-HAND",
"directions": 4
},
{
"name": "equipped-HAND-sealed",
"directions": 4
}
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 763 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 474 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 315 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 365 B

View File

@@ -0,0 +1,25 @@
{
"version": 1,
"license": "CC-BY-SA-3.0",
"copyright": "From tgstation - https://github.com/tgstation/tgstation/pull/59109",
"size": {
"x": 32,
"y": 32
},
"states": [
{
"name": "helmet"
},
{
"name": "helmet-sealed"
},
{
"name": "equipped-HEAD",
"directions": 4
},
{
"name": "equipped-HEAD-sealed",
"directions": 4
}
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 110 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 428 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 130 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -0,0 +1,25 @@
{
"version": 1,
"license": "CC-BY-SA-3.0",
"copyright": "From tgstation - https://github.com/tgstation/tgstation/pull/59109",
"size": {
"x": 32,
"y": 32
},
"states": [
{
"name": "chestplate"
},
{
"name": "chestplate-sealed"
},
{
"name": "equipped-OUTERCLOTHING",
"directions": 4
},
{
"name": "equipped-OUTERCLOTHING-sealed",
"directions": 4
}
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 109 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 307 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 131 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 501 B

View File

@@ -0,0 +1,25 @@
{
"version": 1,
"license": "CC-BY-SA-3.0",
"copyright": "From tgstation - https://github.com/tgstation/tgstation/pull/59109",
"size": {
"x": 32,
"y": 32
},
"states": [
{
"name": "boots"
},
{
"name": "boots-sealed"
},
{
"name": "equipped-FEET",
"directions": 4
},
{
"name": "equipped-FEET-sealed",
"directions": 4
}
]
}

View File

@@ -0,0 +1,29 @@
{
"version": 1,
"license": "CC0-1.0",
"copyright": "Taken from TG",
"size": {
"x": 32,
"y": 32
},
"states": [
{
"name": "modpower0"
},
{
"name": "modpower1"
},
{
"name": "modpower2"
},
{
"name": "modpower3"
},
{
"name": "modpower4"
},
{
"name": "modpower5"
}
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 324 B

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