port better borgs from frontier (#3110)

* BetterBorgs: droppable, swappable cyborg item interactions (#2766)

* WIP: droppable, swappable, insertable cyborg items

* Half-baked borg HandPlaceholderComponent

* cyborg: sprite representation for empty slots

* nullable prototype

---------

Co-authored-by: Dvir <39403717+dvir001@users.noreply.github.com>

* BorgSystem: check droppable items for duped mods (#2887)

* BorgSystem: check droppable items for duped mods

* Cache item comparer

* BorgSystem: Unremoveable after equip (#2854)

* raise interaction events to add fibers to things

---------

Co-authored-by: Whatstone <166147148+whatston3@users.noreply.github.com>
Co-authored-by: Dvir <39403717+dvir001@users.noreply.github.com>
Co-authored-by: deltanedas <@deltanedas:kde.org>
This commit is contained in:
deltanedas 2025-03-04 12:10:32 +00:00 committed by GitHub
parent 32384762bb
commit 0f3edc0b39
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
26 changed files with 507 additions and 18 deletions

View File

@ -13,6 +13,7 @@ using Robust.Client.UserInterface.Controllers;
using Robust.Shared.Input;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
using Content.Shared._NF.Interaction.Components;
namespace Content.Client.UserInterface.Systems.Hands;
@ -138,6 +139,13 @@ public sealed class HandsUIController : UIController, IOnStateEntered<GameplaySt
handButton.SetEntity(virt.BlockingEntity);
handButton.Blocked = true;
}
// Frontier - borg hand placeholder
else if (_entities.TryGetComponent(hand.HeldEntity, out HandPlaceholderVisualsComponent? placeholder))
{
handButton.SetEntity(placeholder.Dummy);
handButton.Blocked = true;
}
// End Frontier - borg hand placeholder
else
{
handButton.SetEntity(hand.HeldEntity);
@ -189,6 +197,13 @@ public sealed class HandsUIController : UIController, IOnStateEntered<GameplaySt
hand.SetEntity(virt.BlockingEntity);
hand.Blocked = true;
}
// Frontier: borg hand placeholders
else if (_entities.TryGetComponent(entity, out HandPlaceholderVisualsComponent? placeholder))
{
hand.SetEntity(placeholder.Dummy);
hand.Blocked = true;
}
// End Frontier: borg hand placeholders
else
{
hand.SetEntity(entity);

View File

@ -0,0 +1,13 @@
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.XAML;
namespace Content.Client._NF.Hands.UI
{
public sealed class HandPlaceholderStatus : Control
{
public HandPlaceholderStatus()
{
RobustXamlLoader.Load(this);
}
}
}

View File

@ -0,0 +1,3 @@
<Control xmlns="https://spacestation14.io">
<Label StyleClasses="ItemStatus" Text="{Loc 'hand-placeholder-name'}" />
</Control>

View File

@ -0,0 +1,10 @@
namespace Content.Shared._NF.Interaction.Components;
[RegisterComponent]
// Client-side component of the HandPlaceholder. Creates and tracks a client-side entity for hand blocking visuals
public sealed partial class HandPlaceholderVisualsComponent : Component
{
[DataField]
public EntityUid Dummy;
}

View File

@ -0,0 +1,47 @@
using Content.Client._NF.Hands.UI;
using Content.Client.Items;
using Content.Client.Items.Systems;
using Content.Shared._NF.Interaction.Components;
using JetBrains.Annotations;
using Robust.Client.GameObjects;
namespace Content.Client._NF.Interaction.Systems;
/// <summary>
/// Handles interactions with items that spawn HandPlaceholder items.
/// </summary>
[UsedImplicitly]
public sealed partial class HandPlaceholderVisualsSystem : EntitySystem
{
[Dependency] ContainerSystem _container = default!;
[Dependency] ItemSystem _item = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<HandPlaceholderComponent, AfterAutoHandleStateEvent>(OnAfterAutoHandleState);
SubscribeLocalEvent<HandPlaceholderVisualsComponent, ComponentRemove>(PlaceholderRemove);
Subs.ItemStatus<HandPlaceholderVisualsComponent>(_ => new HandPlaceholderStatus());
}
private void OnAfterAutoHandleState(Entity<HandPlaceholderComponent> ent, ref AfterAutoHandleStateEvent args)
{
if (!TryComp(ent, out HandPlaceholderVisualsComponent? placeholder))
return;
if (placeholder.Dummy != EntityUid.Invalid)
QueueDel(placeholder.Dummy);
placeholder.Dummy = Spawn(ent.Comp.Prototype);
if (_container.IsEntityInContainer(ent))
_item.VisualsChanged(ent);
}
private void PlaceholderRemove(Entity<HandPlaceholderVisualsComponent> ent, ref ComponentRemove args)
{
if (ent.Comp.Dummy != EntityUid.Invalid)
QueueDel(ent.Comp.Dummy);
}
}

View File

@ -28,7 +28,8 @@ public sealed class PrototypeSaveTest
{
// The only prototypes that should get ignored are those that REQUIRE setup to get a sprite. At that point it is
// the responsibility of the spawner to ensure that a valid sprite is set.
"VirtualItem"
"VirtualItem",
"HandPlaceholder" // Frontier
};
[Test]

View File

@ -4,6 +4,7 @@ using Content.Shared.Interaction.Components;
using Content.Shared.Silicons.Borgs.Components;
using Content.Shared.Whitelist;
using Robust.Shared.Containers;
using Content.Shared._NF.Interaction.Components; // Frontier
namespace Content.Server.Silicons.Borgs;
@ -197,6 +198,7 @@ public sealed partial class BorgSystem
if (!component.ItemsCreated)
{
item = Spawn(itemProto, xform.Coordinates);
_interaction.DoContactInteraction(chassis, item); // DeltaV - give items fibers before they might be dropped
}
else
{
@ -225,6 +227,63 @@ public sealed partial class BorgSystem
component.ProvidedItems.Add(handId, item);
}
// Frontier: droppable cyborg items
foreach (var itemProto in component.DroppableItems)
{
EntityUid item;
if (!component.ItemsCreated)
{
item = Spawn(itemProto.ID, xform.Coordinates);
var placeComp = EnsureComp<HandPlaceholderRemoveableComponent>(item);
placeComp.Whitelist = itemProto.Whitelist;
placeComp.Prototype = itemProto.ID;
Dirty(item, placeComp);
}
else
{
item = component.ProvidedContainer.ContainedEntities
.FirstOrDefault(ent => _whitelistSystem.IsWhitelistPassOrNull(itemProto.Whitelist, ent) || TryComp<HandPlaceholderComponent>(ent, out var placeholder));
if (!item.IsValid())
{
Log.Debug($"no items found: {component.ProvidedContainer.ContainedEntities.Count}");
continue;
}
// Just in case, make sure the borg can't drop the placeholder.
if (!HasComp<HandPlaceholderComponent>(item))
{
var placeComp = EnsureComp<HandPlaceholderRemoveableComponent>(item);
placeComp.Whitelist = itemProto.Whitelist;
placeComp.Prototype = itemProto.ID;
Dirty(item, placeComp);
}
}
if (!item.IsValid())
{
Log.Debug("no valid item");
continue;
}
var handId = $"{uid}-item{component.HandCounter}";
component.HandCounter++;
_hands.AddHand(chassis, handId, HandLocation.Middle, hands);
_hands.DoPickup(chassis, hands.Hands[handId], item, hands);
if (hands.Hands[handId].HeldEntity != item)
{
// If we didn't pick up our expected item, delete the hand. No free hands!
_hands.RemoveHand(chassis, handId);
}
else if (HasComp<HandPlaceholderComponent>(item))
{
// Placeholders can't be put down, must be changed after picked up (otherwise it'll fail to pick up)
EnsureComp<UnremoveableComponent>(item);
}
component.DroppableProvidedItems.Add(handId, (item, itemProto));
}
// End Frontier: droppable cyborg items
component.ItemsCreated = true;
}
@ -244,6 +303,14 @@ public sealed partial class BorgSystem
_hands.RemoveHand(chassis, hand, hands);
}
component.ProvidedItems.Clear();
// Frontier: droppable items
foreach (var (hand, item) in component.DroppableProvidedItems)
{
QueueDel(item.Item1);
_hands.RemoveHand(chassis, hand, hands);
}
component.DroppableProvidedItems.Clear();
// End Frontier: droppable items
return;
}
@ -257,6 +324,20 @@ public sealed partial class BorgSystem
_hands.RemoveHand(chassis, handId, hands);
}
component.ProvidedItems.Clear();
// Frontier: remove all items from borg hands directly, not from the provided items set
foreach (var (handId, _) in component.DroppableProvidedItems)
{
_hands.TryGetHand(chassis, handId, out var hand, hands);
if (hand?.HeldEntity != null)
{
RemComp<UnremoveableComponent>(hand.HeldEntity.Value);
_container.Insert(hand.HeldEntity.Value, component.ProvidedContainer);
}
_hands.RemoveHand(chassis, handId, hands);
}
component.DroppableProvidedItems.Clear();
// End Frontier
}
/// <summary>
@ -283,13 +364,16 @@ public sealed partial class BorgSystem
if (TryComp<ItemBorgModuleComponent>(module, out var itemModuleComp))
{
var droppableComparer = new DroppableBorgItemComparer(); // Frontier: cached comparer
foreach (var containedModuleUid in component.ModuleContainer.ContainedEntities)
{
if (!TryComp<ItemBorgModuleComponent>(containedModuleUid, out var containedItemModuleComp))
continue;
if (containedItemModuleComp.Items.Count == itemModuleComp.Items.Count &&
containedItemModuleComp.Items.All(itemModuleComp.Items.Contains))
containedItemModuleComp.DroppableItems.Count == itemModuleComp.DroppableItems.Count && // Frontier
containedItemModuleComp.Items.All(itemModuleComp.Items.Contains) &&
containedItemModuleComp.DroppableItems.All(x => itemModuleComp.DroppableItems.Contains(x, droppableComparer))) // Frontier
{
if (user != null)
Popup.PopupEntity(Loc.GetString("borg-module-duplicate"), uid, user.Value);
@ -301,6 +385,30 @@ public sealed partial class BorgSystem
return true;
}
// Frontier: droppable borg item comparator
private sealed class DroppableBorgItemComparer : IEqualityComparer<DroppableBorgItem>
{
public bool Equals(DroppableBorgItem? x, DroppableBorgItem? y)
{
// Same object (or both null)
if (ReferenceEquals(x, y))
return true;
// One-side null
if (x == null || y == null)
return false;
// Otherwise, use EntProtoId of item
return x.ID == y.ID;
}
public int GetHashCode(DroppableBorgItem obj)
{
if (obj is null)
return 0;
return obj.ID.GetHashCode();
}
}
// End Frontier
/// <summary>
/// Check if a module can be removed from a borg.
/// </summary>

View File

@ -53,6 +53,7 @@ public sealed partial class BorgSystem : SharedBorgSystem
[Dependency] private readonly ThrowingSystem _throwing = default!;
[Dependency] private readonly UserInterfaceSystem _ui = default!;
[Dependency] private readonly SharedContainerSystem _container = default!;
[Dependency] private readonly SharedInteractionSystem _interaction = default!; // DeltaV
[Dependency] private readonly EntityWhitelistSystem _whitelistSystem = default!;

View File

@ -0,0 +1,7 @@
namespace Content.Server._NF.Whitelist.Components;
/// <summary>
/// Whitelist component for book bags to avoid tag redefinition and collisions
/// </summary>
[RegisterComponent]
public sealed partial class NFBookBagComponent : Component;

View File

@ -0,0 +1,7 @@
namespace Content.Server._NF.Whitelist.Components;
/// <summary>
/// Whitelist component for lighters to avoid tag redefinition and collisions
/// </summary>
[RegisterComponent]
public sealed partial class NFLighterComponent : Component;

View File

@ -0,0 +1,7 @@
namespace Content.Server._NF.Whitelist.Components;
/// <summary>
/// Whitelist component for ore bags to avoid tag redefinition and collisions
/// </summary>
[RegisterComponent]
public sealed partial class NFOreBagComponent : Component;

View File

@ -0,0 +1,7 @@
namespace Content.Server._NF.Whitelist.Components;
/// <summary>
/// Whitelist component for plant bags to avoid tag redefinition and collisions
/// </summary>
[RegisterComponent]
public sealed partial class NFPlantBagComponent : Component;

View File

@ -0,0 +1,7 @@
namespace Content.Server._NF.Whitelist.Components;
/// <summary>
/// Whitelist component for shakers to avoid tag redefinition and collisions
/// </summary>
[RegisterComponent]
public sealed partial class NFShakerComponent : Component;

View File

@ -1,4 +1,3 @@
using Content.Shared.Whitelist;
using Robust.Shared.GameStates;
namespace Content.Shared.Interaction.Components

View File

@ -1,4 +1,5 @@
using Robust.Shared.Containers;
using Content.Shared.Whitelist;
using Robust.Shared.Containers;
using Robust.Shared.GameStates;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.List;
@ -14,15 +15,27 @@ public sealed partial class ItemBorgModuleComponent : Component
/// <summary>
/// The items that are provided.
/// </summary>
[DataField("items", customTypeSerializer: typeof(PrototypeIdListSerializer<EntityPrototype>), required: true)]
[DataField("items", customTypeSerializer: typeof(PrototypeIdListSerializer<EntityPrototype>))] // Frontier: removed
public List<string> Items = new();
/// <summary>
/// Frontier: The droppable items that are provided.
/// </summary>
[DataField]
public List<DroppableBorgItem> DroppableItems = new();
/// <summary>
/// The entities from <see cref="Items"/> that were spawned.
/// </summary>
[DataField("providedItems")]
public SortedDictionary<string, EntityUid> ProvidedItems = new();
/// <summary>
/// The entities from <see cref="Items"/> that were spawned.
/// </summary>
[DataField("droppableProvidedItems")]
public SortedDictionary<string, (EntityUid, DroppableBorgItem)> DroppableProvidedItems = new();
/// <summary>
/// A counter that ensures a unique
/// </summary>
@ -49,3 +62,13 @@ public sealed partial class ItemBorgModuleComponent : Component
public string ProvidedContainerId = "provided_container";
}
// Frontier: droppable borg item data definitions
[DataDefinition]
public sealed partial class DroppableBorgItem
{
[IdDataField]
public EntProtoId ID;
[DataField]
public EntityWhitelist Whitelist;
}

View File

@ -0,0 +1,22 @@
using Content.Shared.Whitelist;
using Robust.Shared.GameStates;
using Robust.Shared.Prototypes;
namespace Content.Shared._NF.Interaction.Components;
[RegisterComponent]
[NetworkedComponent]
[AutoGenerateComponentState(true)]
// When an entity with this is removed from a hand, it is replaced with a placeholder entity that blocks the hand's use until re-equipped with the same prototype.
public sealed partial class HandPlaceholderComponent : Component
{
/// <summary>
/// A whitelist to match entities that this should accept.
/// </summary>
[ViewVariables, AutoNetworkedField]
public EntityWhitelist? Whitelist;
[ViewVariables, AutoNetworkedField]
public EntProtoId? Prototype;
}

View File

@ -0,0 +1,17 @@
using Content.Shared.Whitelist;
using Robust.Shared.GameStates;
using Robust.Shared.Prototypes;
namespace Content.Shared._NF.Interaction.Components;
[RegisterComponent]
[NetworkedComponent]
// When an entity with this is removed from a hand, it is replaced with a placeholder entity that blocks the hand's use until re-equipped with the same prototype.
public sealed partial class HandPlaceholderRemoveableComponent : Component
{
[DataField]
public EntityWhitelist? Whitelist;
[DataField]
public EntProtoId? Prototype;
}

View File

@ -0,0 +1,120 @@
using Content.Shared._NF.Interaction.Components;
using Content.Shared.Hands;
using Content.Shared.Hands.EntitySystems;
using Content.Shared.Interaction;
using Content.Shared.Interaction.Events;
using Content.Shared.Item;
using Content.Shared.Whitelist;
using JetBrains.Annotations;
using Robust.Shared.Containers;
using Robust.Shared.Network;
using Robust.Shared.Prototypes;
namespace Content.Shared._NF.Interaction.Systems;
/// <summary>
/// Handles interactions with items that spawn HandPlaceholder items.
/// </summary>
[UsedImplicitly]
public sealed partial class HandPlaceholderSystem : EntitySystem
{
[Dependency] private readonly INetManager _net = default!;
[Dependency] private readonly SharedHandsSystem _hands = default!;
[Dependency] private readonly SharedContainerSystem _container = default!;
[Dependency] private readonly SharedInteractionSystem _interaction = default!; // DeltaV
[Dependency] private readonly SharedItemSystem _item = default!;
[Dependency] private readonly EntityWhitelistSystem _whitelist = default!;
[Dependency] private readonly MetaDataSystem _metadata = default!;
[Dependency] private readonly IPrototypeManager _proto = default!;
public override void Initialize()
{
SubscribeLocalEvent<HandPlaceholderRemoveableComponent, GotUnequippedHandEvent>(OnUnequipHand);
SubscribeLocalEvent<HandPlaceholderRemoveableComponent, DroppedEvent>(OnDropped);
SubscribeLocalEvent<HandPlaceholderComponent, AfterInteractEvent>(AfterInteract);
SubscribeLocalEvent<HandPlaceholderComponent, BeforeRangedInteractEvent>(BeforeRangedInteract);
}
private void OnUnequipHand(Entity<HandPlaceholderRemoveableComponent> ent, ref GotUnequippedHandEvent args)
{
if (args.Handled)
return; // If this is happening in practice, this is a bug.
SpawnAndPickUpPlaceholder(ent, args.User);
RemCompDeferred<HandPlaceholderRemoveableComponent>(ent);
args.Handled = true;
}
private void OnDropped(Entity<HandPlaceholderRemoveableComponent> ent, ref DroppedEvent args)
{
if (args.Handled)
return; // If this is happening in practice, this is a bug.
SpawnAndPickUpPlaceholder(ent, args.User);
RemCompDeferred<HandPlaceholderRemoveableComponent>(ent);
args.Handled = true;
}
private void SpawnAndPickUpPlaceholder(Entity<HandPlaceholderRemoveableComponent> ent, EntityUid user)
{
if (_net.IsServer)
{
var placeholder = Spawn("HandPlaceholder");
if (TryComp<HandPlaceholderComponent>(placeholder, out var placeComp))
{
placeComp.Whitelist = ent.Comp.Whitelist;
placeComp.Prototype = ent.Comp.Prototype;
Dirty(placeholder, placeComp);
}
if (_proto.TryIndex(ent.Comp.Prototype, out var itemProto))
_metadata.SetEntityName(placeholder, itemProto.Name);
if (!_hands.TryPickup(user, placeholder)) // Can we get the hand this came from?
QueueDel(placeholder);
}
}
private void BeforeRangedInteract(Entity<HandPlaceholderComponent> ent, ref BeforeRangedInteractEvent args)
{
if (args.Target == null || args.Handled)
return;
args.Handled = true;
TryToPickUpTarget(ent, args.Target.Value, args.User);
}
private void AfterInteract(Entity<HandPlaceholderComponent> ent, ref AfterInteractEvent args)
{
if (args.Target == null || args.Handled)
return;
args.Handled = true;
TryToPickUpTarget(ent, args.Target.Value, args.User);
}
private void TryToPickUpTarget(Entity<HandPlaceholderComponent> ent, EntityUid target, EntityUid user)
{
if (_whitelist.IsWhitelistFail(ent.Comp.Whitelist, target))
return;
// Can't get the hand we're holding this with? Something's wrong, abort. No empty hands.
if (!_hands.IsHolding(user, ent, out var hand))
return;
// Cache the whitelist/prototype, entity might be deleted.
var whitelist = ent.Comp.Whitelist;
var prototype = ent.Comp.Prototype;
if (_net.IsServer)
Del(ent);
_hands.DoPickup(user, hand, target); // Force pickup - empty hands are not okay
var placeComp = EnsureComp<HandPlaceholderRemoveableComponent>(target);
placeComp.Whitelist = whitelist;
placeComp.Prototype = prototype;
Dirty(target, placeComp);
_interaction.DoContactInteraction(user, target); // DeltaV - borgs picking up items leaves fibers
}
}

View File

@ -0,0 +1 @@
hand-placeholder-name = Module slot for

View File

@ -39,6 +39,7 @@
mixOnInteract: false
reactionTypes:
- Shake
- type: NFShaker # Frontier
- type: entity
parent: DrinkGlassBase

View File

@ -157,3 +157,4 @@
difficulty: 2
recipes:
- PlantBagOfHolding
- type: NFPlantBag # Frontier

View File

@ -28,3 +28,4 @@
- TabletopBoard
- Write
- type: Dumpable
- type: NFBookBag # Frontier

View File

@ -134,8 +134,13 @@
- state: generic
- state: icon-fire-extinguisher
- type: ItemBorgModule
items:
- FireExtinguisher
# Frontier: droppable borg items
droppableItems:
- id: FireExtinguisher
whitelist:
tags:
- FireExtinguisher
# End Frontier
- type: BorgModuleIcon
icon: { sprite: Interface/Actions/actions_borg.rsi, state: extinguisher-module }
@ -221,9 +226,16 @@
items:
- MiningDrill
- MineralScannerUnpowered
- OreBag
# - OreBag # Frontier
- Crowbar
- RadioHandheld
# Frontier: droppable borg items
droppableItems:
- id: OreBag
whitelist:
components:
- NFOreBag
# End Frontier: droppable borg items
- type: BorgModuleIcon
icon: { sprite: Interface/Actions/actions_borg.rsi, state: mining-module }
@ -345,8 +357,15 @@
- type: ItemBorgModule
items:
- MopItem
- Bucket
# - Bucket # Frontier
- TrashBag
# Frontier: droppable items
droppableItems:
- id: Bucket
whitelist:
tags:
- Bucket
# End Frontier
- type: BorgModuleIcon
icon: { sprite: Interface/Actions/actions_borg.rsi, state: cleaning-module }
@ -433,10 +452,21 @@
- type: ItemBorgModule
items:
- HandheldHealthAnalyzerUnpowered
- Beaker
- Beaker
# - Beaker # Frontier
# - Beaker # Frontier
- BorgDropper
- BorgHypo
# Frontier: droppable borg items
droppableItems:
- id: Beaker
whitelist:
tags:
- GlassBeaker
- id: Beaker
whitelist:
tags:
- GlassBeaker
# End Frontier: droppable borg items
- type: BorgModuleIcon
icon: { sprite: Interface/Actions/actions_borg.rsi, state: adv-diagnosis-module }
@ -489,11 +519,26 @@
- type: ItemBorgModule
items:
- Pen
- BooksBag
# - BooksBag # Frontier
- HandLabeler
- Lighter
- DrinkShaker
# - Lighter # Frontier
# - DrinkShaker # Frontier
- BorgDropper
# Frontier: droppable
droppableItems:
- id: BooksBag
whitelist:
components:
- NFBookBag
- id: Lighter
whitelist:
components:
- NFLighter
- id: DrinkShaker
whitelist:
components:
- NFShaker
# End Frontier
- type: BorgModuleIcon
icon: { sprite: Interface/Actions/actions_borg.rsi, state: service-module }
@ -528,7 +573,14 @@
- HydroponicsToolMiniHoe
- HydroponicsToolSpade
- HydroponicsToolClippers
- Bucket
# - Bucket # Frontier
# Frontier: droppable borg items
droppableItems:
- id: Bucket
whitelist:
tags:
- Bucket
# End Frontier
- type: BorgModuleIcon
icon: { sprite: Interface/Actions/actions_borg.rsi, state: gardening-module }
@ -545,7 +597,14 @@
items:
- HydroponicsToolScythe
- HydroponicsToolHatchet
- PlantBag
# - PlantBag # Frontier
# Frontier: droppable borg items
droppableItems:
- id: PlantBag
whitelist:
components:
- NFPlantBag
# End Frontier
- type: BorgModuleIcon
icon: { sprite: Interface/Actions/actions_borg.rsi, state: harvesting-module }

View File

@ -46,8 +46,9 @@
enum.ToggleVisuals.Layer:
True: { state: icon_on }
False: { state: icon }
# End DeltaV Additions
- type: ReverseEngineering # DeltaV
- type: ReverseEngineering
difficulty: 2
recipes:
- OreBagOfHolding
# End DeltaV Additions
- type: NFOreBag # Frontier

View File

@ -95,6 +95,7 @@
collection: lighterOnSounds
endSound:
collection: lighterOffSounds
- type: NFLighter # Frontier
- type: entity
name: cheap lighter

View File

@ -0,0 +1,10 @@
- type: entity
id: HandPlaceholder
name: unknown tool
categories: [ HideSpawnMenu ]
components:
- type: Item
size: Ginormous # no storage insertion visuals
- type: Unremoveable
- type: HandPlaceholder
- type: HandPlaceholderVisuals