From: Nemanja <98561806+EmoGarbage404@users.noreply.github.com> Date: Thu, 24 Apr 2025 11:39:40 +0000 (-0400) Subject: Turnstiles (#36313) X-Git-Url: https://git.smokeofanarchy.ru/gitweb.cgi?a=commitdiff_plain;h=712954f1c4ab6e54d3d04b166546e1234dd48fd2;p=space-station-14.git 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> --- diff --git a/Content.Client/Doors/TurnstileSystem.cs b/Content.Client/Doors/TurnstileSystem.cs new file mode 100644 index 0000000000..6e76ce6aa0 --- /dev/null +++ b/Content.Client/Doors/TurnstileSystem.cs @@ -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; + +/// +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(OnAnimationCompleted); + SubscribeLocalEvent(OnExamined); + } + + private void OnAnimationCompleted(Entity ent, ref AnimationCompletedEvent args) + { + if (args.Key != AnimationKey) + return; + + if (!TryComp(ent, out var sprite)) + return; + sprite.LayerSetState(TurnstileVisualLayers.Base, new RSI.StateId(ent.Comp.DefaultState)); + } + + private void OnExamined(Entity ent, ref ExaminedEvent args) + { + Spawn(_examineArrow, new EntityCoordinates(ent, 0, 0)); + } + + protected override void PlayAnimation(EntityUid uid, string stateId) + { + if (!TryComp(uid, out var animation) || !TryComp(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); + } +} diff --git a/Content.Server/Doors/Systems/TurnstileSystem.cs b/Content.Server/Doors/Systems/TurnstileSystem.cs new file mode 100644 index 0000000000..687a97095f --- /dev/null +++ b/Content.Server/Doors/Systems/TurnstileSystem.cs @@ -0,0 +1,6 @@ +using Content.Shared.Doors.Systems; + +namespace Content.Server.Doors.Systems; + +/// +public sealed class TurnstileSystem : SharedTurnstileSystem; diff --git a/Content.Shared/Doors/Components/TurnstileComponent.cs b/Content.Shared/Doors/Components/TurnstileComponent.cs new file mode 100644 index 0000000000..5f19dd3e0b --- /dev/null +++ b/Content.Shared/Doors/Components/TurnstileComponent.cs @@ -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; + +/// +/// This is used for a condition door that allows entry only through a single side. +/// +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState, AutoGenerateComponentPause] +[Access(typeof(SharedTurnstileSystem))] +public sealed partial class TurnstileComponent : Component +{ + /// + /// A whitelist of the things this turnstile can choose to block or let through. + /// Things not in this whitelist will be ignored by default. + /// + [DataField] + public EntityWhitelist? ProcessWhitelist; + + /// + /// The next time at which the resist message can show. + /// + [DataField(customTypeSerializer: typeof(TimeOffsetSerializer)), AutoNetworkedField, AutoPausedField] + public TimeSpan NextResistTime; + + /// + /// Maintained hashset of entities currently passing through the turnstile. + /// + [DataField, AutoNetworkedField] + public HashSet CollideExceptions = new(); + + /// + /// default state of the turnstile sprite. + /// + [DataField] + public string DefaultState = "turnstile"; + + /// + /// animation state of the turnstile spinning. + /// + [DataField] + public string SpinState = "operate"; + + /// + /// animation state of the turnstile denying entry. + /// + [DataField] + public string DenyState = "deny"; + + /// + /// Sound to play when the turnstile admits a mob through. + /// + [DataField] + public SoundSpecifier? TurnSound = new SoundPathSpecifier("/Audio/Items/ratchet.ogg"); + + /// + /// Sound to play when the turnstile denies entry + /// + [DataField] + public SoundSpecifier? DenySound = new SoundPathSpecifier("/Audio/Machines/airlock_deny.ogg"); +} + +[Serializable, NetSerializable] +public enum TurnstileVisualLayers : byte +{ + Base +} diff --git a/Content.Shared/Doors/Systems/SharedTurnstileSystem.cs b/Content.Shared/Doors/Systems/SharedTurnstileSystem.cs new file mode 100644 index 0000000000..0af6b91378 --- /dev/null +++ b/Content.Shared/Doors/Systems/SharedTurnstileSystem.cs @@ -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; + +/// +/// This handles logic and interactions related to +/// +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!; + + /// + public override void Initialize() + { + SubscribeLocalEvent(OnPreventCollide); + SubscribeLocalEvent(OnStartCollide); + SubscribeLocalEvent(OnEndCollide); + } + + private void OnPreventCollide(Entity 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 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 ent, ref EndCollideEvent args) + { + if (!args.OurFixture.Hard) + { + ent.Comp.CollideExceptions.Remove(args.OtherEntity); + Dirty(ent); + } + } + + protected bool CanPassDirection(Entity 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) + { + + } +} diff --git a/Content.Shared/Movement/Pulling/Systems/PullingSystem.cs b/Content.Shared/Movement/Pulling/Systems/PullingSystem.cs index 6ac0460f8e..369225df2d 100644 --- a/Content.Shared/Movement/Pulling/Systems/PullingSystem.cs +++ b/Content.Shared/Movement/Pulling/Systems/PullingSystem.cs @@ -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) diff --git a/Resources/Locale/en-US/doors/components/turnstile.ftl b/Resources/Locale/en-US/doors/components/turnstile.ftl new file mode 100644 index 0000000000..16dca94244 --- /dev/null +++ b/Resources/Locale/en-US/doors/components/turnstile.ftl @@ -0,0 +1 @@ +turnstile-component-popup-resist = {CAPITALIZE(THE($turnstile))} resists your efforts! diff --git a/Resources/Prototypes/Entities/Structures/Doors/turnstile.yml b/Resources/Prototypes/Entities/Structures/Doors/turnstile.yml new file mode 100644 index 0000000000..6d675c5928 --- /dev/null +++ b/Resources/Prototypes/Entities/Structures/Doors/turnstile.yml @@ -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 diff --git a/Resources/Prototypes/Recipes/Construction/Graphs/structures/turnstile.yml b/Resources/Prototypes/Recipes/Construction/Graphs/structures/turnstile.yml new file mode 100644 index 0000000000..cc1b8efb6a --- /dev/null +++ b/Resources/Prototypes/Recipes/Construction/Graphs/structures/turnstile.yml @@ -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 diff --git a/Resources/Prototypes/Recipes/Construction/structures.yml b/Resources/Prototypes/Recipes/Construction/structures.yml index 7602a6cb11..11579ac9ba 100644 --- a/Resources/Prototypes/Recipes/Construction/structures.yml +++ b/Resources/Prototypes/Recipes/Construction/structures.yml @@ -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 diff --git a/Resources/Textures/Structures/Doors/turnstile.rsi/arrow.png b/Resources/Textures/Structures/Doors/turnstile.rsi/arrow.png new file mode 100644 index 0000000000..6332f6b326 Binary files /dev/null and b/Resources/Textures/Structures/Doors/turnstile.rsi/arrow.png differ diff --git a/Resources/Textures/Structures/Doors/turnstile.rsi/deny.png b/Resources/Textures/Structures/Doors/turnstile.rsi/deny.png new file mode 100644 index 0000000000..4afe444d7c Binary files /dev/null and b/Resources/Textures/Structures/Doors/turnstile.rsi/deny.png differ diff --git a/Resources/Textures/Structures/Doors/turnstile.rsi/meta.json b/Resources/Textures/Structures/Doors/turnstile.rsi/meta.json new file mode 100644 index 0000000000..dea5fb8946 --- /dev/null +++ b/Resources/Textures/Structures/Doors/turnstile.rsi/meta.json @@ -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 + ] + ] + } + ] +} diff --git a/Resources/Textures/Structures/Doors/turnstile.rsi/operate.png b/Resources/Textures/Structures/Doors/turnstile.rsi/operate.png new file mode 100644 index 0000000000..3178c298dd Binary files /dev/null and b/Resources/Textures/Structures/Doors/turnstile.rsi/operate.png differ diff --git a/Resources/Textures/Structures/Doors/turnstile.rsi/turnstile.png b/Resources/Textures/Structures/Doors/turnstile.rsi/turnstile.png new file mode 100644 index 0000000000..48c85577e7 Binary files /dev/null and b/Resources/Textures/Structures/Doors/turnstile.rsi/turnstile.png differ diff --git a/Resources/Textures/Structures/Doors/turnstile.rsi/turnstile_map.png b/Resources/Textures/Structures/Doors/turnstile.rsi/turnstile_map.png new file mode 100644 index 0000000000..fb3fcc5c31 Binary files /dev/null and b/Resources/Textures/Structures/Doors/turnstile.rsi/turnstile_map.png differ