Turnstiles (#36313)
* construction rotation fix * Turnstiles * renaming * review-slarticodefast-1 * mild attempts to fix (sorry sloth) * move some more shit * Remove engine dependency * grid agnostic * remove debug string * fix json * Update Content.Shared/Movement/Pulling/Systems/PullingSystem.cs Co-authored-by: ArtisticRoomba <145879011+ArtisticRoomba@users.noreply.github.com> * Update Content.Shared/Movement/Pulling/Systems/PullingSystem.cs Co-authored-by: ArtisticRoomba <145879011+ArtisticRoomba@users.noreply.github.com> * remove pass delay for mispredict reasons. * most minor of changes * Give directional indicator on examine --------- Co-authored-by: ArtisticRoomba <145879011+ArtisticRoomba@users.noreply.github.com>
This commit is contained in:
75
Content.Client/Doors/TurnstileSystem.cs
Normal file
75
Content.Client/Doors/TurnstileSystem.cs
Normal file
@@ -0,0 +1,75 @@
|
||||
using Content.Shared.Doors.Components;
|
||||
using Content.Shared.Doors.Systems;
|
||||
using Content.Shared.Examine;
|
||||
using Robust.Client.Animations;
|
||||
using Robust.Client.GameObjects;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Client.Doors;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public sealed class TurnstileSystem : SharedTurnstileSystem
|
||||
{
|
||||
[Dependency] private readonly AnimationPlayerSystem _animationPlayer = default!;
|
||||
|
||||
private static EntProtoId _examineArrow = "TurnstileArrow";
|
||||
|
||||
private const string AnimationKey = "Turnstile";
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
SubscribeLocalEvent<TurnstileComponent, AnimationCompletedEvent>(OnAnimationCompleted);
|
||||
SubscribeLocalEvent<TurnstileComponent, ExaminedEvent>(OnExamined);
|
||||
}
|
||||
|
||||
private void OnAnimationCompleted(Entity<TurnstileComponent> ent, ref AnimationCompletedEvent args)
|
||||
{
|
||||
if (args.Key != AnimationKey)
|
||||
return;
|
||||
|
||||
if (!TryComp<SpriteComponent>(ent, out var sprite))
|
||||
return;
|
||||
sprite.LayerSetState(TurnstileVisualLayers.Base, new RSI.StateId(ent.Comp.DefaultState));
|
||||
}
|
||||
|
||||
private void OnExamined(Entity<TurnstileComponent> ent, ref ExaminedEvent args)
|
||||
{
|
||||
Spawn(_examineArrow, new EntityCoordinates(ent, 0, 0));
|
||||
}
|
||||
|
||||
protected override void PlayAnimation(EntityUid uid, string stateId)
|
||||
{
|
||||
if (!TryComp<AnimationPlayerComponent>(uid, out var animation) || !TryComp<SpriteComponent>(uid, out var sprite))
|
||||
return;
|
||||
var ent = (uid, animation);
|
||||
|
||||
if (_animationPlayer.HasRunningAnimation(animation, AnimationKey))
|
||||
_animationPlayer.Stop(ent, AnimationKey);
|
||||
|
||||
if (sprite.BaseRSI == null || !sprite.BaseRSI.TryGetState(stateId, out var state))
|
||||
return;
|
||||
var animLength = state.AnimationLength;
|
||||
|
||||
var anim = new Animation
|
||||
{
|
||||
AnimationTracks =
|
||||
{
|
||||
new AnimationTrackSpriteFlick
|
||||
{
|
||||
LayerKey = TurnstileVisualLayers.Base,
|
||||
KeyFrames =
|
||||
{
|
||||
new AnimationTrackSpriteFlick.KeyFrame(state.StateId, 0f),
|
||||
},
|
||||
},
|
||||
},
|
||||
Length = TimeSpan.FromSeconds(animLength),
|
||||
};
|
||||
|
||||
_animationPlayer.Play(ent, anim, AnimationKey);
|
||||
}
|
||||
}
|
||||
6
Content.Server/Doors/Systems/TurnstileSystem.cs
Normal file
6
Content.Server/Doors/Systems/TurnstileSystem.cs
Normal file
@@ -0,0 +1,6 @@
|
||||
using Content.Shared.Doors.Systems;
|
||||
|
||||
namespace Content.Server.Doors.Systems;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public sealed class TurnstileSystem : SharedTurnstileSystem;
|
||||
71
Content.Shared/Doors/Components/TurnstileComponent.cs
Normal file
71
Content.Shared/Doors/Components/TurnstileComponent.cs
Normal file
@@ -0,0 +1,71 @@
|
||||
using Content.Shared.Doors.Systems;
|
||||
using Content.Shared.Whitelist;
|
||||
using Robust.Shared.Audio;
|
||||
using Robust.Shared.GameStates;
|
||||
using Robust.Shared.Serialization;
|
||||
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
|
||||
|
||||
namespace Content.Shared.Doors.Components;
|
||||
|
||||
/// <summary>
|
||||
/// This is used for a condition door that allows entry only through a single side.
|
||||
/// </summary>
|
||||
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState, AutoGenerateComponentPause]
|
||||
[Access(typeof(SharedTurnstileSystem))]
|
||||
public sealed partial class TurnstileComponent : Component
|
||||
{
|
||||
/// <summary>
|
||||
/// A whitelist of the things this turnstile can choose to block or let through.
|
||||
/// Things not in this whitelist will be ignored by default.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public EntityWhitelist? ProcessWhitelist;
|
||||
|
||||
/// <summary>
|
||||
/// The next time at which the resist message can show.
|
||||
/// </summary>
|
||||
[DataField(customTypeSerializer: typeof(TimeOffsetSerializer)), AutoNetworkedField, AutoPausedField]
|
||||
public TimeSpan NextResistTime;
|
||||
|
||||
/// <summary>
|
||||
/// Maintained hashset of entities currently passing through the turnstile.
|
||||
/// </summary>
|
||||
[DataField, AutoNetworkedField]
|
||||
public HashSet<EntityUid> CollideExceptions = new();
|
||||
|
||||
/// <summary>
|
||||
/// default state of the turnstile sprite.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public string DefaultState = "turnstile";
|
||||
|
||||
/// <summary>
|
||||
/// animation state of the turnstile spinning.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public string SpinState = "operate";
|
||||
|
||||
/// <summary>
|
||||
/// animation state of the turnstile denying entry.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public string DenyState = "deny";
|
||||
|
||||
/// <summary>
|
||||
/// Sound to play when the turnstile admits a mob through.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public SoundSpecifier? TurnSound = new SoundPathSpecifier("/Audio/Items/ratchet.ogg");
|
||||
|
||||
/// <summary>
|
||||
/// Sound to play when the turnstile denies entry
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public SoundSpecifier? DenySound = new SoundPathSpecifier("/Audio/Machines/airlock_deny.ogg");
|
||||
}
|
||||
|
||||
[Serializable, NetSerializable]
|
||||
public enum TurnstileVisualLayers : byte
|
||||
{
|
||||
Base
|
||||
}
|
||||
131
Content.Shared/Doors/Systems/SharedTurnstileSystem.cs
Normal file
131
Content.Shared/Doors/Systems/SharedTurnstileSystem.cs
Normal file
@@ -0,0 +1,131 @@
|
||||
using Content.Shared.Access.Systems;
|
||||
using Content.Shared.Doors.Components;
|
||||
using Content.Shared.Movement.Pulling.Systems;
|
||||
using Content.Shared.Popups;
|
||||
using Content.Shared.Whitelist;
|
||||
using Robust.Shared.Audio.Systems;
|
||||
using Robust.Shared.Physics.Events;
|
||||
using Robust.Shared.Timing;
|
||||
|
||||
namespace Content.Shared.Doors.Systems;
|
||||
|
||||
/// <summary>
|
||||
/// This handles logic and interactions related to <see cref="TurnstileComponent"/>
|
||||
/// </summary>
|
||||
public abstract partial class SharedTurnstileSystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly IGameTiming _timing = default!;
|
||||
[Dependency] private readonly AccessReaderSystem _accessReader = default!;
|
||||
[Dependency] private readonly SharedAudioSystem _audio = default!;
|
||||
[Dependency] private readonly EntityWhitelistSystem _entityWhitelist = default!;
|
||||
[Dependency] private readonly PullingSystem _pulling = default!;
|
||||
[Dependency] private readonly SharedTransformSystem _transform = default!;
|
||||
[Dependency] private readonly SharedPopupSystem _popup = default!;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void Initialize()
|
||||
{
|
||||
SubscribeLocalEvent<TurnstileComponent, PreventCollideEvent>(OnPreventCollide);
|
||||
SubscribeLocalEvent<TurnstileComponent, StartCollideEvent>(OnStartCollide);
|
||||
SubscribeLocalEvent<TurnstileComponent, EndCollideEvent>(OnEndCollide);
|
||||
}
|
||||
|
||||
private void OnPreventCollide(Entity<TurnstileComponent> ent, ref PreventCollideEvent args)
|
||||
{
|
||||
if (args.Cancelled || !args.OurFixture.Hard || !args.OtherFixture.Hard)
|
||||
return;
|
||||
|
||||
if (ent.Comp.CollideExceptions.Contains(args.OtherEntity))
|
||||
{
|
||||
args.Cancelled = true;
|
||||
return;
|
||||
}
|
||||
|
||||
// We need to add this in here too for chain pulls
|
||||
if (_pulling.GetPuller(args.OtherEntity) is { } puller && ent.Comp.CollideExceptions.Contains(puller))
|
||||
{
|
||||
ent.Comp.CollideExceptions.Add(args.OtherEntity);
|
||||
Dirty(ent);
|
||||
args.Cancelled = true;
|
||||
return;
|
||||
}
|
||||
|
||||
// unblockables go through for free.
|
||||
if (_entityWhitelist.IsWhitelistFail(ent.Comp.ProcessWhitelist, args.OtherEntity))
|
||||
{
|
||||
args.Cancelled = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (CanPassDirection(ent, args.OtherEntity))
|
||||
{
|
||||
if (!_accessReader.IsAllowed(args.OtherEntity, ent))
|
||||
return;
|
||||
|
||||
ent.Comp.CollideExceptions.Add(args.OtherEntity);
|
||||
if (_pulling.GetPulling(args.OtherEntity) is { } uid)
|
||||
ent.Comp.CollideExceptions.Add(uid);
|
||||
|
||||
args.Cancelled = true;
|
||||
Dirty(ent);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (_timing.CurTime >= ent.Comp.NextResistTime)
|
||||
{
|
||||
_popup.PopupClient(Loc.GetString("turnstile-component-popup-resist", ("turnstile", ent.Owner)), ent, args.OtherEntity);
|
||||
ent.Comp.NextResistTime = _timing.CurTime + TimeSpan.FromSeconds(0.1);
|
||||
Dirty(ent);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void OnStartCollide(Entity<TurnstileComponent> ent, ref StartCollideEvent args)
|
||||
{
|
||||
if (!ent.Comp.CollideExceptions.Contains(args.OtherEntity))
|
||||
{
|
||||
if (CanPassDirection(ent, args.OtherEntity))
|
||||
{
|
||||
if (!_accessReader.IsAllowed(args.OtherEntity, ent))
|
||||
{
|
||||
_audio.PlayPredicted(ent.Comp.DenySound, ent, args.OtherEntity);
|
||||
PlayAnimation(ent, ent.Comp.DenyState);
|
||||
}
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
// if they passed through:
|
||||
PlayAnimation(ent, ent.Comp.SpinState);
|
||||
_audio.PlayPredicted(ent.Comp.TurnSound, ent, args.OtherEntity);
|
||||
}
|
||||
|
||||
private void OnEndCollide(Entity<TurnstileComponent> ent, ref EndCollideEvent args)
|
||||
{
|
||||
if (!args.OurFixture.Hard)
|
||||
{
|
||||
ent.Comp.CollideExceptions.Remove(args.OtherEntity);
|
||||
Dirty(ent);
|
||||
}
|
||||
}
|
||||
|
||||
protected bool CanPassDirection(Entity<TurnstileComponent> ent, EntityUid other)
|
||||
{
|
||||
var xform = Transform(ent);
|
||||
var otherXform = Transform(other);
|
||||
|
||||
var (pos, rot) = _transform.GetWorldPositionRotation(xform);
|
||||
var otherPos = _transform.GetWorldPosition(otherXform);
|
||||
|
||||
var approachAngle = (pos - otherPos).ToAngle();
|
||||
var rotateAngle = rot.ToWorldVec().ToAngle();
|
||||
|
||||
var dif = Math.Min(Math.Abs(approachAngle.Theta - rotateAngle.Theta), Math.Abs(rotateAngle.Theta - approachAngle.Theta));
|
||||
return dif < Math.PI / 4;
|
||||
}
|
||||
|
||||
protected virtual void PlayAnimation(EntityUid uid, string stateId)
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
@@ -341,6 +341,16 @@ public sealed class PullingSystem : EntitySystem
|
||||
return Resolve(puller, ref component, false) && component.Pulling != null;
|
||||
}
|
||||
|
||||
public EntityUid? GetPuller(EntityUid puller, PullableComponent? component = null)
|
||||
{
|
||||
return !Resolve(puller, ref component, false) ? null : component.Puller;
|
||||
}
|
||||
|
||||
public EntityUid? GetPulling(EntityUid puller, PullerComponent? component = null)
|
||||
{
|
||||
return !Resolve(puller, ref component, false) ? null : component.Pulling;
|
||||
}
|
||||
|
||||
private void OnReleasePulledObject(ICommonSession? session)
|
||||
{
|
||||
if (session?.AttachedEntity is not { Valid: true } player)
|
||||
|
||||
1
Resources/Locale/en-US/doors/components/turnstile.ftl
Normal file
1
Resources/Locale/en-US/doors/components/turnstile.ftl
Normal file
@@ -0,0 +1 @@
|
||||
turnstile-component-popup-resist = {CAPITALIZE(THE($turnstile))} resists your efforts!
|
||||
77
Resources/Prototypes/Entities/Structures/Doors/turnstile.yml
Normal file
77
Resources/Prototypes/Entities/Structures/Doors/turnstile.yml
Normal file
@@ -0,0 +1,77 @@
|
||||
- type: entity
|
||||
id: Turnstile
|
||||
parent: BaseStructure
|
||||
name: turnstile
|
||||
description: A mechanical door that permits one-way access and prevents tailgating.
|
||||
components:
|
||||
- type: Sprite
|
||||
sprite: Structures/Doors/turnstile.rsi
|
||||
snapCardinals: true
|
||||
drawdepth: Doors
|
||||
layers:
|
||||
- state: turnstile
|
||||
map: [ "enum.TurnstileVisualLayers.Base" ]
|
||||
- type: AnimationPlayer
|
||||
- type: Fixtures
|
||||
fixtures:
|
||||
fix1:
|
||||
shape:
|
||||
!type:PhysShapeAabb
|
||||
bounds: "-0.49,-0.49,0.49,0.49" # same dimensions as a door for tall turnstile, prevents objects being thrown through
|
||||
density: 100
|
||||
mask:
|
||||
- FullTileMask
|
||||
layer:
|
||||
- AirlockLayer
|
||||
fix2:
|
||||
shape:
|
||||
!type:PhysShapeAabb
|
||||
bounds: "-0.50,-0.50,0.50,0.50" # same dimensions as a door for tall turnstile, prevents objects being thrown through
|
||||
hard: false
|
||||
mask:
|
||||
- FullTileMask
|
||||
layer:
|
||||
- AirlockLayer
|
||||
- type: MeleeSound
|
||||
soundGroups:
|
||||
Brute:
|
||||
path:
|
||||
"/Audio/Weapons/smash.ogg"
|
||||
- type: InteractionOutline
|
||||
- type: Turnstile
|
||||
processWhitelist:
|
||||
components:
|
||||
- MobState # no mobs
|
||||
- Pullable # no dragging things in
|
||||
- type: Appearance
|
||||
- type: Damageable
|
||||
damageContainer: Inorganic
|
||||
damageModifierSet: StrongMetallic
|
||||
- type: Destructible
|
||||
thresholds:
|
||||
- trigger:
|
||||
!type:DamageTrigger
|
||||
damage: 500
|
||||
behaviors:
|
||||
- !type:DoActsBehavior
|
||||
acts: ["Destruction"]
|
||||
- type: Construction
|
||||
graph: Turnstile
|
||||
node: turnstile
|
||||
|
||||
- # Spawned by the client-side turnstile examine code to indicate the direction to pass through.
|
||||
type: entity
|
||||
id: TurnstileArrow
|
||||
categories: [ HideSpawnMenu ]
|
||||
components:
|
||||
- type: Sprite
|
||||
sprite: Structures/Doors/turnstile.rsi
|
||||
color: "#FFFFFFBB"
|
||||
layers:
|
||||
- state: arrow
|
||||
offset: 0, 0.78125
|
||||
- type: TimedDespawn
|
||||
lifetime: 2
|
||||
- type: Tag
|
||||
tags:
|
||||
- HideContextMenu
|
||||
@@ -0,0 +1,33 @@
|
||||
- type: constructionGraph
|
||||
id: Turnstile
|
||||
start: start
|
||||
graph:
|
||||
- node: start
|
||||
actions:
|
||||
- !type:DeleteEntity { }
|
||||
edges:
|
||||
- to: turnstile
|
||||
completed:
|
||||
- !type:SnapToGrid
|
||||
steps:
|
||||
- material: MetalRod
|
||||
amount: 4
|
||||
doAfter: 6
|
||||
- material: Steel
|
||||
amount: 1
|
||||
doAfter: 2
|
||||
|
||||
- node: turnstile
|
||||
entity: Turnstile
|
||||
edges:
|
||||
- to: start
|
||||
completed:
|
||||
- !type:SpawnPrototype
|
||||
prototype: PartRodMetal1
|
||||
amount: 4
|
||||
- !type:DeleteEntity
|
||||
steps:
|
||||
- tool: Welding
|
||||
doAfter: 4.0
|
||||
- tool: Cutting
|
||||
doAfter: 2.0
|
||||
@@ -796,6 +796,23 @@
|
||||
conditions:
|
||||
- !type:TileNotBlocked
|
||||
|
||||
- type: construction
|
||||
name: turnstile
|
||||
id: Turnstile
|
||||
graph: Turnstile
|
||||
startNode: start
|
||||
targetNode: turnstile
|
||||
category: construction-category-structures
|
||||
description: A mechanical door that permits one-way access and prevents tailgating.
|
||||
icon:
|
||||
sprite: Structures/Doors/turnstile.rsi
|
||||
state: turnstile
|
||||
objectType: Structure
|
||||
placementMode: SnapgridCenter
|
||||
canBuildInImpassable: false
|
||||
conditions:
|
||||
- !type:TileNotBlocked
|
||||
|
||||
- type: construction
|
||||
name: shutter
|
||||
id: Shutters
|
||||
|
||||
BIN
Resources/Textures/Structures/Doors/turnstile.rsi/arrow.png
Normal file
BIN
Resources/Textures/Structures/Doors/turnstile.rsi/arrow.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 543 B |
BIN
Resources/Textures/Structures/Doors/turnstile.rsi/deny.png
Normal file
BIN
Resources/Textures/Structures/Doors/turnstile.rsi/deny.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.9 KiB |
65
Resources/Textures/Structures/Doors/turnstile.rsi/meta.json
Normal file
65
Resources/Textures/Structures/Doors/turnstile.rsi/meta.json
Normal file
@@ -0,0 +1,65 @@
|
||||
{
|
||||
"version": 1,
|
||||
"license": "CC-BY-SA-3.0",
|
||||
"copyright": "Taken from OracleStation at https://github.com/OracleStation/OracleStation/blob/c1046bdd14674dc5a8631074aaa6650770b2937e/icons/obj/turnstile.dmi. Arrow by EmoGarbage404 (github).",
|
||||
"size": {
|
||||
"x": 32,
|
||||
"y": 32
|
||||
},
|
||||
"states": [
|
||||
{
|
||||
"name": "arrow",
|
||||
"delays": [
|
||||
[
|
||||
0.5,
|
||||
0.1,
|
||||
0.1,
|
||||
0.1,
|
||||
0.1,
|
||||
0.1,
|
||||
0.1,
|
||||
0.1,
|
||||
0.1,
|
||||
0.1,
|
||||
0.1
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "turnstile"
|
||||
},
|
||||
{
|
||||
"name": "turnstile_map",
|
||||
"directions": 4
|
||||
},
|
||||
{
|
||||
"name": "operate",
|
||||
"delays": [
|
||||
[
|
||||
0.1,
|
||||
0.1,
|
||||
0.1,
|
||||
0.1,
|
||||
0.1,
|
||||
0.1,
|
||||
0.1,
|
||||
0.1,
|
||||
0.1
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "deny",
|
||||
"delays": [
|
||||
[
|
||||
0.1,
|
||||
0.1,
|
||||
0.1,
|
||||
0.1,
|
||||
0.1,
|
||||
0.1
|
||||
]
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
BIN
Resources/Textures/Structures/Doors/turnstile.rsi/operate.png
Normal file
BIN
Resources/Textures/Structures/Doors/turnstile.rsi/operate.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.4 KiB |
BIN
Resources/Textures/Structures/Doors/turnstile.rsi/turnstile.png
Normal file
BIN
Resources/Textures/Structures/Doors/turnstile.rsi/turnstile.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.4 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 1.8 KiB |
Reference in New Issue
Block a user