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",
|
||||
"FigureSpawner",
|
||||
"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.Threading.Tasks;
|
||||
using Content.Server.GameObjects.Components.Items.Storage;
|
||||
@@ -131,7 +131,7 @@ namespace Content.Server.GameObjects.EntitySystems.Click
|
||||
private void InteractionActivate(IEntity user, IEntity used)
|
||||
{
|
||||
var activateMsg = new ActivateInWorldMessage(user, used);
|
||||
RaiseLocalEvent(activateMsg);
|
||||
RaiseLocalEvent(used.Uid, activateMsg);
|
||||
if (activateMsg.Handled)
|
||||
{
|
||||
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;
|
||||
// Used for clientside fake prediction of doors.
|
||||
public const uint DOOR = 1087;
|
||||
public const uint SPAWN_AFTER_INTERACT = 1088;
|
||||
public const uint DISASSEMBLE_ON_ACTIVATE = 1089;
|
||||
|
||||
// Net IDs for integration tests.
|
||||
public const uint PREDICTION_TEST = 10001;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System.Linq;
|
||||
using System.Linq;
|
||||
using Content.Shared.Interfaces;
|
||||
using Content.Shared.Interfaces.GameObjects.Components;
|
||||
using Content.Shared.Physics;
|
||||
@@ -541,5 +541,63 @@ namespace Content.Shared.GameObjects.EntitySystems
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#nullable enable
|
||||
#nullable enable
|
||||
using Content.Shared.GameObjects.EntitySystems;
|
||||
using Content.Shared.Interfaces.GameObjects.Components;
|
||||
using Content.Shared.Physics;
|
||||
@@ -430,5 +430,19 @@ namespace Content.Shared.Utility
|
||||
ignoreInsideBlocker, popup);
|
||||
}
|
||||
#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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
Asphyxiation: 0
|
||||
Bloodloss: 0
|
||||
Cellular: 0
|
||||
|
||||
|
||||
- type: resistanceSet
|
||||
id: dionaResistances
|
||||
coefficients:
|
||||
@@ -51,7 +51,7 @@
|
||||
Asphyxiation: 0
|
||||
Bloodloss: 0
|
||||
Cellular: 0
|
||||
|
||||
|
||||
- type: resistanceSet
|
||||
id: metallicResistances
|
||||
coefficients:
|
||||
@@ -77,4 +77,31 @@
|
||||
Radiation: 0
|
||||
Asphyxiation: 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