Implement inflatable wall (#3703)
* Implement inflatable wall * Actually block atmos * Fix naming and add description * Add requested changes * Change prototype to field * Refactor checks to use existing methods * Fix PrototypeIdSerializer imports * Fix mass in yaml
This commit is contained in:
@@ -250,6 +250,8 @@ namespace Content.Client
|
|||||||
"ToySpawner",
|
"ToySpawner",
|
||||||
"FigureSpawner",
|
"FigureSpawner",
|
||||||
"RandomSpawner",
|
"RandomSpawner",
|
||||||
|
"SpawnAfterInteract",
|
||||||
|
"DisassembleOnActivate",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
#nullable enable
|
||||||
|
using Content.Shared.GameObjects;
|
||||||
|
using Robust.Shared.GameObjects;
|
||||||
|
using Robust.Shared.Prototypes;
|
||||||
|
using Robust.Shared.Serialization.Manager.Attributes;
|
||||||
|
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
|
||||||
|
using Robust.Shared.ViewVariables;
|
||||||
|
using System.Threading;
|
||||||
|
|
||||||
|
namespace Content.Server.GameObjects.Components.Engineering
|
||||||
|
{
|
||||||
|
[RegisterComponent]
|
||||||
|
public class DisassembleOnActivateComponent : Component
|
||||||
|
{
|
||||||
|
public override string Name => "DisassembleOnActivate";
|
||||||
|
public override uint? NetID => ContentNetIDs.DISASSEMBLE_ON_ACTIVATE;
|
||||||
|
|
||||||
|
[ViewVariables]
|
||||||
|
[field: DataField("prototype", customTypeSerializer: typeof(PrototypeIdSerializer<EntityPrototype>))]
|
||||||
|
public string? Prototype { get; }
|
||||||
|
|
||||||
|
[ViewVariables]
|
||||||
|
[DataField("doAfter")]
|
||||||
|
public float DoAfterTime = 0;
|
||||||
|
|
||||||
|
public CancellationTokenSource TokenSource { get; } = new();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
#nullable enable
|
||||||
|
using Content.Shared.GameObjects;
|
||||||
|
using Robust.Shared.GameObjects;
|
||||||
|
using Robust.Shared.Prototypes;
|
||||||
|
using Robust.Shared.Serialization.Manager.Attributes;
|
||||||
|
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
|
||||||
|
using Robust.Shared.ViewVariables;
|
||||||
|
|
||||||
|
namespace Content.Server.GameObjects.Components.Engineering
|
||||||
|
{
|
||||||
|
[RegisterComponent]
|
||||||
|
public class SpawnAfterInteractComponent : Component
|
||||||
|
{
|
||||||
|
public override string Name => "SpawnAfterInteract";
|
||||||
|
public override uint? NetID => ContentNetIDs.SPAWN_AFTER_INTERACT;
|
||||||
|
|
||||||
|
[ViewVariables]
|
||||||
|
[field: DataField("prototype", customTypeSerializer: typeof(PrototypeIdSerializer<EntityPrototype>))]
|
||||||
|
public string? Prototype { get; }
|
||||||
|
|
||||||
|
[ViewVariables]
|
||||||
|
[DataField("doAfter")]
|
||||||
|
public float DoAfterTime = 0;
|
||||||
|
|
||||||
|
[ViewVariables]
|
||||||
|
[DataField("removeOnInteract")]
|
||||||
|
public bool RemoveOnInteract = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Content.Server.GameObjects.Components.Items.Storage;
|
using Content.Server.GameObjects.Components.Items.Storage;
|
||||||
@@ -131,7 +131,7 @@ namespace Content.Server.GameObjects.EntitySystems.Click
|
|||||||
private void InteractionActivate(IEntity user, IEntity used)
|
private void InteractionActivate(IEntity user, IEntity used)
|
||||||
{
|
{
|
||||||
var activateMsg = new ActivateInWorldMessage(user, used);
|
var activateMsg = new ActivateInWorldMessage(user, used);
|
||||||
RaiseLocalEvent(activateMsg);
|
RaiseLocalEvent(used.Uid, activateMsg);
|
||||||
if (activateMsg.Handled)
|
if (activateMsg.Handled)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -0,0 +1,68 @@
|
|||||||
|
using Content.Server.GameObjects.Components.Engineering;
|
||||||
|
using Content.Server.GameObjects.Components.GUI;
|
||||||
|
using Content.Server.GameObjects.Components.Items.Storage;
|
||||||
|
using Content.Server.GameObjects.Components.Stack;
|
||||||
|
using Content.Server.GameObjects.EntitySystems.DoAfter;
|
||||||
|
using Content.Shared.Interfaces.GameObjects.Components;
|
||||||
|
using Content.Shared.Utility;
|
||||||
|
using JetBrains.Annotations;
|
||||||
|
using Robust.Shared.GameObjects;
|
||||||
|
using System.Threading;
|
||||||
|
|
||||||
|
namespace Content.Server.GameObjects.EntitySystems
|
||||||
|
{
|
||||||
|
[UsedImplicitly]
|
||||||
|
public class DisassembleOnActivateSystem : EntitySystem
|
||||||
|
{
|
||||||
|
public override void Initialize()
|
||||||
|
{
|
||||||
|
base.Initialize();
|
||||||
|
|
||||||
|
SubscribeLocalEvent<DisassembleOnActivateComponent, ActivateInWorldMessage>(HandleActivateInWorld);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Shutdown()
|
||||||
|
{
|
||||||
|
base.Shutdown();
|
||||||
|
|
||||||
|
UnsubscribeLocalEvent<DisassembleOnActivateComponent, ActivateInWorldMessage>(HandleActivateInWorld);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async void HandleActivateInWorld(EntityUid uid, DisassembleOnActivateComponent component, ActivateInWorldMessage args)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(component.Prototype))
|
||||||
|
return;
|
||||||
|
if (!args.User.InRangeUnobstructed(args.Activated))
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (component.DoAfterTime > 0 && TryGet<DoAfterSystem>(out var doAfterSystem))
|
||||||
|
{
|
||||||
|
var doAfterArgs = new DoAfterEventArgs(args.User, component.DoAfterTime, component.TokenSource.Token)
|
||||||
|
{
|
||||||
|
BreakOnUserMove = true,
|
||||||
|
BreakOnStun = true,
|
||||||
|
};
|
||||||
|
var result = await doAfterSystem.DoAfter(doAfterArgs);
|
||||||
|
|
||||||
|
if (result != DoAfterStatus.Finished)
|
||||||
|
return;
|
||||||
|
component.TokenSource.Cancel();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (component.Deleted || component.Owner.Deleted)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var entity = EntityManager.SpawnEntity(component.Prototype, component.Owner.Transform.Coordinates);
|
||||||
|
|
||||||
|
if (args.User.TryGetComponent<HandsComponent>(out var hands)
|
||||||
|
&& entity.TryGetComponent<ItemComponent>(out var item))
|
||||||
|
{
|
||||||
|
hands.PutInHandOrDrop(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
component.Owner.Delete();
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
#nullable enable
|
||||||
|
using Content.Server.GameObjects.Components.Engineering;
|
||||||
|
using Content.Server.GameObjects.Components.Stack;
|
||||||
|
using Content.Server.GameObjects.EntitySystems.DoAfter;
|
||||||
|
using Content.Server.Utility;
|
||||||
|
using Content.Shared.Interfaces.GameObjects.Components;
|
||||||
|
using Content.Shared.Maps;
|
||||||
|
using Content.Shared.Utility;
|
||||||
|
using JetBrains.Annotations;
|
||||||
|
using Robust.Shared.GameObjects;
|
||||||
|
using Robust.Shared.IoC;
|
||||||
|
using Robust.Shared.Map;
|
||||||
|
|
||||||
|
namespace Content.Server.GameObjects.EntitySystems
|
||||||
|
{
|
||||||
|
[UsedImplicitly]
|
||||||
|
public class SpawnAfterInteractSystem : EntitySystem
|
||||||
|
{
|
||||||
|
[Dependency] private readonly IMapManager _mapManager = default!;
|
||||||
|
|
||||||
|
public override void Initialize()
|
||||||
|
{
|
||||||
|
base.Initialize();
|
||||||
|
|
||||||
|
SubscribeLocalEvent<SpawnAfterInteractComponent, AfterInteractMessage>(HandleAfterInteract);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Shutdown()
|
||||||
|
{
|
||||||
|
base.Shutdown();
|
||||||
|
|
||||||
|
UnsubscribeLocalEvent<SpawnAfterInteractComponent, AfterInteractMessage>(HandleAfterInteract);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async void HandleAfterInteract(EntityUid uid, SpawnAfterInteractComponent component, AfterInteractMessage args)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(component.Prototype))
|
||||||
|
return;
|
||||||
|
if (!args.ClickLocation.TryGetTileRef(out var tileRef, EntityManager, _mapManager))
|
||||||
|
return;
|
||||||
|
|
||||||
|
bool IsTileClear()
|
||||||
|
{
|
||||||
|
return tileRef.Value.Tile.IsEmpty == false && args.User.InRangeUnobstructed(args.ClickLocation, popup: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!IsTileClear())
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (component.DoAfterTime > 0 && TryGet<DoAfterSystem>(out var doAfterSystem))
|
||||||
|
{
|
||||||
|
var doAfterArgs = new DoAfterEventArgs(args.User, component.DoAfterTime)
|
||||||
|
{
|
||||||
|
BreakOnUserMove = true,
|
||||||
|
BreakOnStun = true,
|
||||||
|
PostCheck = IsTileClear,
|
||||||
|
};
|
||||||
|
var result = await doAfterSystem.DoAfter(doAfterArgs);
|
||||||
|
|
||||||
|
if (result != DoAfterStatus.Finished)
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (component.Deleted || component.Owner.Deleted)
|
||||||
|
return;
|
||||||
|
|
||||||
|
StackComponent? stack = null;
|
||||||
|
if (component.RemoveOnInteract && component.Owner.TryGetComponent(out stack) && !stack.Use(1))
|
||||||
|
return;
|
||||||
|
|
||||||
|
EntityManager.SpawnEntity(component.Prototype, tileRef.Value.GridPosition(_mapManager));
|
||||||
|
|
||||||
|
if (component.RemoveOnInteract && stack == null && !component.Owner.Deleted)
|
||||||
|
component.Owner.Delete();
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -95,6 +95,8 @@ namespace Content.Shared.GameObjects
|
|||||||
public const uint TAG = 1086;
|
public const uint TAG = 1086;
|
||||||
// Used for clientside fake prediction of doors.
|
// Used for clientside fake prediction of doors.
|
||||||
public const uint DOOR = 1087;
|
public const uint DOOR = 1087;
|
||||||
|
public const uint SPAWN_AFTER_INTERACT = 1088;
|
||||||
|
public const uint DISASSEMBLE_ON_ACTIVATE = 1089;
|
||||||
|
|
||||||
// Net IDs for integration tests.
|
// Net IDs for integration tests.
|
||||||
public const uint PREDICTION_TEST = 10001;
|
public const uint PREDICTION_TEST = 10001;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using Content.Shared.Interfaces;
|
using Content.Shared.Interfaces;
|
||||||
using Content.Shared.Interfaces.GameObjects.Components;
|
using Content.Shared.Interfaces.GameObjects.Components;
|
||||||
using Content.Shared.Physics;
|
using Content.Shared.Physics;
|
||||||
@@ -541,5 +541,63 @@ namespace Content.Shared.GameObjects.EntitySystems
|
|||||||
|
|
||||||
return InRangeUnobstructed(user, otherPosition, range, collisionMask, predicate, ignoreInsideBlocker, popup);
|
return InRangeUnobstructed(user, otherPosition, range, collisionMask, predicate, ignoreInsideBlocker, popup);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Checks that the user and target of a
|
||||||
|
/// <see cref="AfterInteractMessage"/> are within a certain distance
|
||||||
|
/// without any entity that matches the collision mask obstructing them.
|
||||||
|
/// If the <paramref name="range"/> is zero or negative,
|
||||||
|
/// this method will only check if nothing obstructs the entity and component.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="args">The event args to use.</param>
|
||||||
|
/// <param name="range">
|
||||||
|
/// Maximum distance between the two entity and set of map coordinates.
|
||||||
|
/// </param>
|
||||||
|
/// <param name="collisionMask">The mask to check for collisions.</param>
|
||||||
|
/// <param name="predicate">
|
||||||
|
/// A predicate to check whether to ignore an entity or not.
|
||||||
|
/// If it returns true, it will be ignored.
|
||||||
|
/// </param>
|
||||||
|
/// <param name="ignoreInsideBlocker">
|
||||||
|
/// If true and both the user and target are inside
|
||||||
|
/// the obstruction, ignores the obstruction and considers the interaction
|
||||||
|
/// unobstructed.
|
||||||
|
/// Therefore, setting this to true makes this check more permissive,
|
||||||
|
/// such as allowing an interaction to occur inside something impassable
|
||||||
|
/// (like a wall). The default, false, makes the check more restrictive.
|
||||||
|
/// </param>
|
||||||
|
/// <param name="popup">
|
||||||
|
/// Whether or not to popup a feedback message on the user entity for
|
||||||
|
/// it to see.
|
||||||
|
/// </param>
|
||||||
|
/// <returns>
|
||||||
|
/// True if the two points are within a given range without being obstructed.
|
||||||
|
/// </returns>
|
||||||
|
public bool InRangeUnobstructed(
|
||||||
|
AfterInteractMessage args,
|
||||||
|
float range = InteractionRange,
|
||||||
|
CollisionGroup collisionMask = CollisionGroup.Impassable,
|
||||||
|
Ignored? predicate = null,
|
||||||
|
bool ignoreInsideBlocker = false,
|
||||||
|
bool popup = false)
|
||||||
|
{
|
||||||
|
var user = args.User;
|
||||||
|
var target = args.Attacked;
|
||||||
|
predicate ??= e => e == user;
|
||||||
|
|
||||||
|
MapCoordinates otherPosition;
|
||||||
|
|
||||||
|
if (target == null)
|
||||||
|
{
|
||||||
|
otherPosition = args.ClickLocation.ToMap(EntityManager);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
otherPosition = target.Transform.MapPosition;
|
||||||
|
predicate += e => e == target;
|
||||||
|
}
|
||||||
|
|
||||||
|
return InRangeUnobstructed(user, otherPosition, range, collisionMask, predicate, ignoreInsideBlocker, popup);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -58,9 +58,9 @@ namespace Content.Shared.Maps
|
|||||||
return tile;
|
return tile;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static bool TryGetTileRef(this EntityCoordinates coordinates, [NotNullWhen(true)] out TileRef? turf)
|
public static bool TryGetTileRef(this EntityCoordinates coordinates, [NotNullWhen(true)] out TileRef? turf, IEntityManager? entityManager = null, IMapManager? mapManager = null)
|
||||||
{
|
{
|
||||||
return (turf = coordinates.GetTileRef()) != null;
|
return (turf = coordinates.GetTileRef(entityManager, mapManager)) != null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
#nullable enable
|
#nullable enable
|
||||||
using Content.Shared.GameObjects.EntitySystems;
|
using Content.Shared.GameObjects.EntitySystems;
|
||||||
using Content.Shared.Interfaces.GameObjects.Components;
|
using Content.Shared.Interfaces.GameObjects.Components;
|
||||||
using Content.Shared.Physics;
|
using Content.Shared.Physics;
|
||||||
@@ -430,5 +430,19 @@ namespace Content.Shared.Utility
|
|||||||
ignoreInsideBlocker, popup);
|
ignoreInsideBlocker, popup);
|
||||||
}
|
}
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
|
#region EntityEventArgs
|
||||||
|
public static bool InRangeUnobstructed(
|
||||||
|
this AfterInteractMessage args,
|
||||||
|
float range = InteractionRange,
|
||||||
|
CollisionGroup collisionMask = CollisionGroup.Impassable,
|
||||||
|
Ignored? predicate = null,
|
||||||
|
bool ignoreInsideBlocker = false,
|
||||||
|
bool popup = false)
|
||||||
|
{
|
||||||
|
return SharedInteractionSystem.InRangeUnobstructed(args, range, collisionMask, predicate,
|
||||||
|
ignoreInsideBlocker, popup);
|
||||||
|
}
|
||||||
|
#endregion
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -78,3 +78,30 @@
|
|||||||
Asphyxiation: 0
|
Asphyxiation: 0
|
||||||
Bloodloss: 0
|
Bloodloss: 0
|
||||||
Cellular: 0
|
Cellular: 0
|
||||||
|
|
||||||
|
- type: resistanceSet
|
||||||
|
id: inflatableResistances
|
||||||
|
coefficients:
|
||||||
|
Blunt: 0.5
|
||||||
|
Slash: 1.0
|
||||||
|
Piercing: 2.0
|
||||||
|
Heat: 0.5
|
||||||
|
Shock: 0
|
||||||
|
Cold: 0
|
||||||
|
Poison: 0
|
||||||
|
Radiation: 0
|
||||||
|
Asphyxiation: 0
|
||||||
|
Bloodloss: 0
|
||||||
|
Cellular: 0
|
||||||
|
flatReductions:
|
||||||
|
Blunt: 5
|
||||||
|
Slash: 0
|
||||||
|
Piercing: 0
|
||||||
|
Heat: 0
|
||||||
|
Shock: 0
|
||||||
|
Cold: 0
|
||||||
|
Poison: 0
|
||||||
|
Radiation: 0
|
||||||
|
Asphyxiation: 0
|
||||||
|
Bloodloss: 0
|
||||||
|
Cellular: 0
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
- type: entity
|
||||||
|
id: InflatableWall
|
||||||
|
name: inflatable barricade
|
||||||
|
description: An inflated membrane. Activate to deflate. Do not puncture.
|
||||||
|
components:
|
||||||
|
- type: Clickable
|
||||||
|
- type: InteractionOutline
|
||||||
|
- type: Sprite
|
||||||
|
sprite: Objects/Misc/inflatable_wall.rsi
|
||||||
|
state: inflatable_wall
|
||||||
|
- type: Physics
|
||||||
|
bodyType: Static
|
||||||
|
fixtures:
|
||||||
|
- shape:
|
||||||
|
!type:PhysShapeAabb
|
||||||
|
bounds: "-0.5, -0.5, 0.5, 0.5"
|
||||||
|
mass: 15
|
||||||
|
layer:
|
||||||
|
- Impassable
|
||||||
|
- MobImpassable
|
||||||
|
- VaultImpassable
|
||||||
|
- SmallImpassable
|
||||||
|
- type: Damageable
|
||||||
|
resistances: inflatableResistances
|
||||||
|
- type: Destructible
|
||||||
|
thresholds:
|
||||||
|
- trigger:
|
||||||
|
!type:DamageTrigger
|
||||||
|
damage: 20
|
||||||
|
behaviors:
|
||||||
|
- !type:DoActsBehavior
|
||||||
|
acts: [ "Destruction" ]
|
||||||
|
- type: DisassembleOnActivate
|
||||||
|
prototype: InflatableWallStack1
|
||||||
|
doAfter: 3
|
||||||
|
- type: Airtight
|
||||||
|
- type: SnapGrid
|
||||||
|
offset: Center
|
||||||
|
placement:
|
||||||
|
mode: SnapgridCenter
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
- type: entity
|
||||||
|
id: InflatableWallStack
|
||||||
|
parent: BaseItem
|
||||||
|
name: inflatable barricade
|
||||||
|
description: A folded membrane which rapidly expands into a large cubical shape on activation.
|
||||||
|
suffix: Full
|
||||||
|
components:
|
||||||
|
- type: Stack
|
||||||
|
stackType: InflatableWall
|
||||||
|
- type: Sprite
|
||||||
|
sprite: Objects/Misc/inflatable_wall.rsi
|
||||||
|
state: item_wall
|
||||||
|
netsync: false
|
||||||
|
- type: Item
|
||||||
|
sprite: Objects/Misc/inflatable_wall.rsi
|
||||||
|
size: 5
|
||||||
|
- type: SpawnAfterInteract
|
||||||
|
prototype: InflatableWall
|
||||||
|
doAfter: 1
|
||||||
|
removeOnInteract: true
|
||||||
|
- type: Clickable
|
||||||
|
# - type: Appearance # TODO: Add stack sprites
|
||||||
|
# visuals:
|
||||||
|
# - type: StackVisualizer
|
||||||
|
# stackLayers:
|
||||||
|
# - coillv-10
|
||||||
|
# - coillv-20
|
||||||
|
# - coillv-30
|
||||||
|
|
||||||
|
- type: entity
|
||||||
|
parent: InflatableWallStack
|
||||||
|
id: InflatableWallStack1
|
||||||
|
suffix: 1
|
||||||
|
components:
|
||||||
|
- type: Sprite
|
||||||
|
state: item_wall
|
||||||
|
- type: Item
|
||||||
|
size: 5
|
||||||
|
- type: Stack
|
||||||
|
count: 1
|
||||||
4
Resources/Prototypes/Stacks/engineering_stacks.yml
Normal file
4
Resources/Prototypes/Stacks/engineering_stacks.yml
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
- type: stack
|
||||||
|
id: InflatableWall
|
||||||
|
name: inflatable wall
|
||||||
|
spawn: InflatableWallStack1
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 2.1 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 906 B |
@@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"version": 1,
|
||||||
|
"license": "CC-BY-SA-3.0",
|
||||||
|
"copyright": "inflatable_wall taken from https://github.com/discordia-space/CEV-Eris/blob/c34c1b30abf18aa552e19294523924c39e5ea127/icons/obj/inflatable.dmi and modified. item_wall by ShadowCommander.",
|
||||||
|
"size": {
|
||||||
|
"x": 32,
|
||||||
|
"y": 32
|
||||||
|
},
|
||||||
|
"states": [
|
||||||
|
{
|
||||||
|
"name": "inflatable_wall"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "item_wall"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user