diff --git a/Content.Client/Buckle/BuckleComponent.cs b/Content.Client/Buckle/BuckleComponent.cs index a840927388..f791a6f082 100644 --- a/Content.Client/Buckle/BuckleComponent.cs +++ b/Content.Client/Buckle/BuckleComponent.cs @@ -1,8 +1,7 @@ using Content.Shared.ActionBlocker; using Content.Shared.Buckle.Components; +using Content.Shared.Vehicle.Components; using Robust.Client.GameObjects; -using Robust.Shared.GameObjects; -using Robust.Shared.IoC; namespace Content.Client.Buckle { @@ -42,6 +41,11 @@ namespace Content.Client.Buckle return; } + if (!_entMan.TryGetComponent(Owner, out RiderComponent? rider)) + { + return; + } + if (_buckled && buckle.DrawDepth.HasValue) { _originalDrawDepth ??= ownerSprite.DrawDepth; diff --git a/Content.Client/Vehicle/VehicleSystem.cs b/Content.Client/Vehicle/VehicleSystem.cs new file mode 100644 index 0000000000..61784fb8eb --- /dev/null +++ b/Content.Client/Vehicle/VehicleSystem.cs @@ -0,0 +1,32 @@ +using Robust.Client.GameObjects; +using Content.Shared.Vehicle; + +namespace Content.Client.Vehicle +{ + /// + /// Controls client-side visuals for + /// vehicles + /// + public sealed class VehicleSystem : VisualizerSystem + { + protected override void OnAppearanceChange(EntityUid uid, VehicleVisualsComponent component, ref AppearanceChangeEvent args) + { + /// First check is for the sprite itself + if (TryComp(uid, out SpriteComponent? sprite) + && args.Component.TryGetData(VehicleVisuals.DrawDepth, out int drawDepth) && sprite != null) + { + sprite.DrawDepth = drawDepth; + } + /// Set vehicle layer to animated or not (i.e. are the wheels turning or not) + if (args.Component.TryGetData(VehicleVisuals.AutoAnimate, out bool autoAnimate)) + { + sprite?.LayerSetAutoAnimated(VehicleVisualLayers.AutoAnimate, autoAnimate); + } + } + } +} +public enum VehicleVisualLayers : byte +{ + /// Layer for the vehicle's wheels + AutoAnimate, +} diff --git a/Content.Client/Vehicle/VehicleVisualsComponent.cs b/Content.Client/Vehicle/VehicleVisualsComponent.cs new file mode 100644 index 0000000000..459cf3f6fd --- /dev/null +++ b/Content.Client/Vehicle/VehicleVisualsComponent.cs @@ -0,0 +1,10 @@ +namespace Content.Client.Vehicle; + +/// +/// Controls visuals for vehicles +/// +[RegisterComponent] +public sealed class VehicleVisualsComponent : Component +{ + public int DrawDepth = 0; +} diff --git a/Content.Server/Buckle/Components/BuckleComponent.cs b/Content.Server/Buckle/Components/BuckleComponent.cs index 83593a037c..dd92894d5f 100644 --- a/Content.Server/Buckle/Components/BuckleComponent.cs +++ b/Content.Server/Buckle/Components/BuckleComponent.cs @@ -2,6 +2,7 @@ using System.Diagnostics.CodeAnalysis; using Content.Server.Hands.Components; using Content.Server.Pulling; using Content.Shared.ActionBlocker; +using Content.Shared.Vehicle.Components; using Content.Shared.Alert; using Content.Shared.Buckle.Components; using Content.Shared.Interaction; @@ -288,6 +289,10 @@ namespace Content.Server.Buckle.Components { return false; } + // If the strap is a vehicle and the rider is not the person unbuckling, return. + if (_entMan.TryGetComponent(oldBuckledTo.Owner, out var vehicle) && + vehicle.Rider != user) + return false; } BuckledTo = null; diff --git a/Content.Server/Buckle/Components/StrapComponent.cs b/Content.Server/Buckle/Components/StrapComponent.cs index 2995436eaa..8f94992a65 100644 --- a/Content.Server/Buckle/Components/StrapComponent.cs +++ b/Content.Server/Buckle/Components/StrapComponent.cs @@ -34,7 +34,7 @@ namespace Content.Server.Buckle.Components /// If this offset it too big, it will be clamped to /// [DataField("buckleOffset", required: false)] - private Vector2 _buckleOffset = Vector2.Zero; + public Vector2 BuckleOffsetUnclamped = Vector2.Zero; private bool _enabled = true; @@ -57,7 +57,7 @@ namespace Content.Server.Buckle.Components /// Don't change it unless you really have to /// [DataField("maxBuckleDistance", required: false)] - public float MaxBuckleDistance = 0.5f; + public float MaxBuckleDistance = 0.1f; /// /// You can specify the offset the entity will have after unbuckling. @@ -69,7 +69,7 @@ namespace Content.Server.Buckle.Components /// Gets and clamps the buckle offset to MaxBuckleDistance /// public Vector2 BuckleOffset => Vector2.Clamp( - _buckleOffset, + BuckleOffsetUnclamped, Vector2.One * -MaxBuckleDistance, Vector2.One * MaxBuckleDistance); diff --git a/Content.Server/Entry/IgnoredComponents.cs b/Content.Server/Entry/IgnoredComponents.cs index 44997f3d96..f48d64b299 100644 --- a/Content.Server/Entry/IgnoredComponents.cs +++ b/Content.Server/Entry/IgnoredComponents.cs @@ -12,6 +12,7 @@ namespace Content.Server.Entry "MeleeWeaponArcAnimation", "AnimationsTest", "ItemStatus", + "VehicleVisuals", "Marker", "Clickable", "Icon", diff --git a/Content.Server/Physics/Controllers/MoverController.cs b/Content.Server/Physics/Controllers/MoverController.cs index 48a2949ced..87b883412d 100644 --- a/Content.Server/Physics/Controllers/MoverController.cs +++ b/Content.Server/Physics/Controllers/MoverController.cs @@ -1,5 +1,6 @@ using Content.Server.Shuttles.Components; using Content.Server.Shuttles.EntitySystems; +using Content.Shared.Vehicle.Components; using Content.Shared.Movement; using Content.Shared.Movement.Components; using Content.Shared.Shuttles.Components; @@ -14,7 +15,12 @@ namespace Content.Server.Physics.Controllers [Dependency] private readonly IMapManager _mapManager = default!; [Dependency] private readonly ShuttleSystem _shuttle = default!; [Dependency] private readonly ThrusterSystem _thruster = default!; - + /// + /// These mobs will get skipped over when checking which mobs + /// should be moved. Prediction is handled elsewhere by + /// cancelling the movement attempt in the shared and + /// client namespace. + /// private HashSet _excludedMobs = new(); private Dictionary> _shuttlePilots = new(); @@ -40,6 +46,7 @@ namespace Content.Server.Physics.Controllers } HandleShuttleMovement(frameTime); + HandleVehicleMovement(frameTime); foreach (var (mover, physics) in EntityManager.EntityQuery(true)) { @@ -60,7 +67,7 @@ namespace Content.Server.Physics.Controllers _excludedMobs.Add(mover.Owner); var gridId = xform.GridID; - + // This tries to see if the grid is a shuttle if (!_mapManager.TryGetGrid(gridId, out var grid) || !EntityManager.TryGetComponent(grid.GridEntityId, out ShuttleComponent? shuttleComponent)) continue; @@ -252,5 +259,17 @@ namespace Content.Server.Physics.Controllers } } } + /// + /// Add mobs riding vehicles to the list of mobs whose input + /// should be ignored. + /// + private void HandleVehicleMovement(float frameTime) + { + foreach (var (rider, mover, xform) in EntityManager.EntityQuery()) + { + if (rider.Vehicle == null) continue; + _excludedMobs.Add(mover.Owner); + } + } } } diff --git a/Content.Server/Standing/StandingStateSystem.cs b/Content.Server/Standing/StandingStateSystem.cs index 91c04de1d5..4d4d215bf8 100644 --- a/Content.Server/Standing/StandingStateSystem.cs +++ b/Content.Server/Standing/StandingStateSystem.cs @@ -17,6 +17,9 @@ public sealed class StandingStateSystem : EntitySystem var direction = EntityManager.TryGetComponent(uid, out PhysicsComponent? comp) ? comp.LinearVelocity / 50 : Vector2.Zero; var dropAngle = _random.NextFloat(0.8f, 1.2f); + var fellEvent = new FellDownEvent(uid); + RaiseLocalEvent(uid, fellEvent, false); + if (!TryComp(uid, out SharedHandsComponent? handsComp)) return; @@ -43,3 +46,15 @@ public sealed class StandingStateSystem : EntitySystem } } + + /// + /// Raised after an entity falls down. + /// + public sealed class FellDownEvent : EntityEventArgs + { + public EntityUid Uid { get; } + public FellDownEvent(EntityUid uid) + { + Uid = uid; + } + } diff --git a/Content.Server/Vehicle/HonkSystem.cs b/Content.Server/Vehicle/HonkSystem.cs new file mode 100644 index 0000000000..ec9fb8afbe --- /dev/null +++ b/Content.Server/Vehicle/HonkSystem.cs @@ -0,0 +1,57 @@ +using Content.Shared.Vehicle.Components; +using Content.Shared.Vehicle; +using Content.Shared.Toggleable; +using Content.Shared.Audio; +using Robust.Shared.Player; +using Robust.Shared.Audio; + +namespace Content.Server.Vehicle +{ + /// + /// Controls all the vehicle horns. + /// + public sealed class HonkSystem : EntitySystem + { + public override void Initialize() + { + base.Initialize(); + SubscribeLocalEvent(OnHonk); + SubscribeLocalEvent(OnSirenToggle); + } + + /// + /// This fires when the rider presses the honk action + /// + private void OnHonk(EntityUid uid, VehicleComponent vehicle, HonkActionEvent args) + { + if (args.Handled) + return; + if (vehicle.HornSound != null) + { + SoundSystem.Play(Filter.Pvs(uid), vehicle.HornSound.GetSound(), uid, AudioHelpers.WithVariation(0.1f).WithVolume(8f)); + args.Handled = true; + } + } + + /// + /// For vehicles with horn sirens (like the secway) this uses different logic that makes the siren + /// loop instead of using a normal honk. + /// + private void OnSirenToggle(EntityUid uid, VehicleComponent vehicle, ToggleActionEvent args) + { + if (args.Handled || !vehicle.HornIsLooping) + return; + + if (!vehicle.LoopingHornIsPlaying) + { + vehicle.SirenPlayingStream?.Stop(); + vehicle.LoopingHornIsPlaying = true; + if (vehicle.HornSound != null) + vehicle.SirenPlayingStream = SoundSystem.Play(Filter.Pvs(uid), vehicle.HornSound.GetSound(), uid, AudioParams.Default.WithLoop(true).WithVolume(1.8f)); + return; + } + vehicle.SirenPlayingStream?.Stop(); + vehicle.LoopingHornIsPlaying = false; + } + } +} diff --git a/Content.Server/Vehicle/RiderSystem.cs b/Content.Server/Vehicle/RiderSystem.cs new file mode 100644 index 0000000000..961dfe61d7 --- /dev/null +++ b/Content.Server/Vehicle/RiderSystem.cs @@ -0,0 +1,57 @@ +using Content.Server.Buckle.Components; +using Content.Shared.Vehicle.Components; +using Content.Shared.MobState; +using Content.Server.Standing; +using Content.Shared.Hands; + +namespace Content.Server.Vehicle +{ + public sealed class RiderSystem : EntitySystem + { + public override void Initialize() + { + base.Initialize(); + SubscribeLocalEvent(OnVirtualItemDeleted); + SubscribeLocalEvent(OnFallDown); + SubscribeLocalEvent(OnMobStateChanged); + } + + /// + /// Kick the rider off the vehicle if they press q / drop the virtual item + /// + private void OnVirtualItemDeleted(EntityUid uid, RiderComponent component, VirtualItemDeletedEvent args) + { + if (args.BlockingEntity == component.Vehicle?.Owner) + { + UnbuckleFromVehicle(uid); + } + } + + /// + /// Kick the rider off the vehicle if they get stunned + /// + private void OnFallDown(EntityUid uid, RiderComponent rider, FellDownEvent args) + { + UnbuckleFromVehicle(uid); + } + + /// + /// Kick the rider off the vehicle if they go into crit or die. + /// + private void OnMobStateChanged(EntityUid uid, RiderComponent rider, MobStateChangedEvent args) + { + if (args.Component.IsCritical() || args.Component.IsDead()) + { + UnbuckleFromVehicle(uid); + } + } + + private void UnbuckleFromVehicle(EntityUid uid) + { + if (!TryComp(uid, out var buckle)) + return; + + buckle.TryUnbuckle(uid, true); + } + } +} diff --git a/Content.Server/Vehicle/VehicleSystem.cs b/Content.Server/Vehicle/VehicleSystem.cs new file mode 100644 index 0000000000..a073bea802 --- /dev/null +++ b/Content.Server/Vehicle/VehicleSystem.cs @@ -0,0 +1,256 @@ +using Content.Shared.Vehicle.Components; +using Content.Shared.Vehicle; +using Content.Shared.Buckle.Components; +using Content.Shared.Movement.Components; +using Content.Shared.Containers.ItemSlots; +using Content.Shared.Actions; +using Content.Shared.Audio; +using Content.Shared.Pulling.Components; +using Content.Server.Light.Components; +using Content.Server.Buckle.Components; +using Content.Server.Hands.Systems; +using Content.Shared.Tag; +using Robust.Shared.Random; +using Robust.Shared.Containers; + +namespace Content.Server.Vehicle +{ + public sealed class VehicleSystem : EntitySystem + { + [Dependency] private readonly HandVirtualItemSystem _virtualItemSystem = default!; + [Dependency] private readonly IRobustRandom _random = default!; + [Dependency] private readonly SharedActionsSystem _actionsSystem = default!; + [Dependency] private readonly SharedAmbientSoundSystem _ambientSound = default!; + [Dependency] private readonly TagSystem _tagSystem = default!; + [Dependency] private readonly ItemSlotsSystem _itemSlotsSystem = default!; + + public override void Initialize() + { + base.Initialize(); + SubscribeLocalEvent(OnBuckleChange); + SubscribeLocalEvent(OnComponentInit); + SubscribeLocalEvent(OnMove); + SubscribeLocalEvent(OnEntInserted); + SubscribeLocalEvent(OnEntRemoved); + } + /// + /// This just controls whether the wheels are turning. + /// + public override void Update(float frameTime) + { + foreach (var (vehicle, mover) in EntityQuery()) + { + if (mover.VelocityDir.sprinting == Vector2.Zero) + { + UpdateAutoAnimate(vehicle.Owner, false); + continue; + } + UpdateAutoAnimate(vehicle.Owner, true); + } + } + /// + /// Sets the initial appearance / sound, then stores the initial buckle offset and resets it. + /// + private void OnComponentInit(EntityUid uid, VehicleComponent component, ComponentInit args) + { + UpdateDrawDepth(uid, 2); + _ambientSound.SetAmbience(uid, false); + if (!TryComp(uid, out var strap)) + return; + + component.BaseBuckleOffset = strap.BuckleOffset; + strap.BuckleOffsetUnclamped = Vector2.Zero; //You're going to align these facing east, so... + } + /// + /// Give the user the rider component if they're buckling to the vehicle, + /// otherwise remove it. + /// + private void OnBuckleChange(EntityUid uid, VehicleComponent component, BuckleChangeEvent args) + { + if (args.Buckling) + { + /// Set up the rider and vehicle with each other + EnsureComp(uid); + var rider = EnsureComp(args.BuckledEntity); + component.Rider = args.BuckledEntity; + rider.Vehicle = component; + component.HasRider = true; + + /// Handle pulling + RemComp(args.BuckledEntity); + RemComp(uid); + /// Add a virtual item to rider's hand + _virtualItemSystem.TrySpawnVirtualItemInHand(uid, args.BuckledEntity); + /// Let this open doors if it has the key in it + if (component.HasKey) + { + _tagSystem.AddTag(uid, "DoorBumpOpener"); + } + /// Update appearance stuff, add actions + UpdateBuckleOffset(Transform(uid), component); + UpdateDrawDepth(uid, GetDrawDepth(Transform(uid), component.NorthOnly)); + if (TryComp(args.BuckledEntity, out var actions) && TryComp(uid, out var flashlight)) + { + _actionsSystem.AddAction(args.BuckledEntity, flashlight.ToggleAction, uid, actions); + } + if (component.HornSound != null) + { + _actionsSystem.AddAction(args.BuckledEntity, component.HornAction, uid, actions); + } + _itemSlotsSystem.SetLock(uid, component.Name, true); + return; + } + // Clean up actions and virtual items + _actionsSystem.RemoveProvidedActions(args.BuckledEntity, uid); + _virtualItemSystem.DeleteInHandsMatching(args.BuckledEntity, uid); + // Go back to old pullable behavior + _tagSystem.RemoveTag(uid, "DoorBumpOpener"); + EnsureComp(args.BuckledEntity); + EnsureComp(uid); + /// Entity is no longer riding + RemComp(args.BuckledEntity); + /// Reset component + component.HasRider = false; + component.Rider = null; + _itemSlotsSystem.SetLock(uid, component.Name, false); + + } + + /// + /// Every time the vehicle moves we update its visual and buckle positions. + /// Not the most beautiful thing but it works. + /// + private void OnMove(EntityUid uid, VehicleComponent component, ref MoveEvent args) + { + /// This first check is just for safety + if (!HasComp(uid)) + { + UpdateAutoAnimate(uid, false); + return; + } + /// The random check means the vehicle will stop after a few tiles without a key or without a rider + if ((!component.HasRider || !component.HasKey) && _random.Prob(0.015f)) + { + RemComp(uid); + UpdateAutoAnimate(uid, false); + } + UpdateBuckleOffset(args.Component, component); + UpdateDrawDepth(uid, GetDrawDepth(args.Component, component.NorthOnly)); + } + + /// + /// Handle adding keys to the ignition, give stuff the InVehicleComponent so it can't be picked + /// up by people not in the vehicle. + /// + private void OnEntInserted(EntityUid uid, VehicleComponent component, EntInsertedIntoContainerMessage args) + { + var inVehicle = AddComp(args.Entity); + inVehicle.Vehicle = component; + + if (_tagSystem.HasTag(args.Entity, "VehicleKey")) + { + /// Return if the slot is not the key slot + /// That slot ID should be inherited from basevehicle in the .yml + if (args.Container.ID != "key_slot") + { + return; + } + + /// This lets the vehicle move + EnsureComp(uid); + /// This lets the vehicle open doors + if (component.HasRider) + _tagSystem.AddTag(uid, "DoorBumpOpener"); + + component.HasKey = true; + + // Audiovisual feedback + _ambientSound.SetAmbience(uid, true); + } + } + + /// + /// Turn off the engine when key is removed. + /// + private void OnEntRemoved(EntityUid uid, VehicleComponent component, EntRemovedFromContainerMessage args) + { + RemComp(args.Entity); + + if (_tagSystem.HasTag(args.Entity, "VehicleKey")) + { + component.HasKey = false; + _ambientSound.SetAmbience(uid, false); + } + } + + /// + /// Depending on which direction the vehicle is facing, + /// change its draw depth. Vehicles can choose between special drawdetph + /// when facing north or south. East and west are easy. + /// + private int GetDrawDepth(TransformComponent xform, bool northOnly) + { + if (northOnly) + { + return xform.LocalRotation.Degrees switch + { + < 135f => 10, + <= 225f => 2, + _ => 10 + }; + } + return xform.LocalRotation.Degrees switch + { + < 45f => 10, + <= 315f => 2, + _ => 10 + }; + } + + /// + /// Change the buckle offset based on what direction the vehicle is facing and + /// teleport any buckled entities to it. This is the most crucial part of making + /// buckled vehicles work. + /// + private void UpdateBuckleOffset(TransformComponent xform, VehicleComponent component) + { + if (!TryComp(component.Owner, out var strap)) + return; + strap.BuckleOffsetUnclamped = xform.LocalRotation.Degrees switch + { + < 45f => (0, component.SouthOverride), + <= 135f => component.BaseBuckleOffset, + < 225f => (0, component.NorthOverride), + <= 315f => (component.BaseBuckleOffset.X * -1, component.BaseBuckleOffset.Y), + _ => (0, component.SouthOverride) + }; + + foreach (var buckledEntity in strap.BuckledEntities) + { + var buckleXform = Transform(buckledEntity); + buckleXform.LocalPosition = strap.BuckleOffset; + } + + } + /// + /// Set the draw depth for the sprite. + /// + private void UpdateDrawDepth(EntityUid uid, int drawDepth) + { + if (!TryComp(uid, out var appearance)) + return; + + appearance.SetData(VehicleVisuals.DrawDepth, drawDepth); + } + + /// + /// Set whether the vehicle's base layer is animating or not. + /// + private void UpdateAutoAnimate(EntityUid uid, bool autoAnimate) + { + if (!TryComp(uid, out var appearance)) + return; + appearance.SetData(VehicleVisuals.AutoAnimate, autoAnimate); + } + } +} diff --git a/Content.Shared/Containers/ItemSlot/ItemSlotsSystem.cs b/Content.Shared/Containers/ItemSlot/ItemSlotsSystem.cs index bf461be347..d0d5cfef93 100644 --- a/Content.Shared/Containers/ItemSlot/ItemSlotsSystem.cs +++ b/Content.Shared/Containers/ItemSlot/ItemSlotsSystem.cs @@ -336,10 +336,16 @@ namespace Content.Shared.Containers.ItemSlots { item = null; + /// This handles logic with the slot itself if (!CanEject(slot)) return false; item = slot.Item; + + /// This handles user logic + if (user != null && item != null && !_actionBlockerSystem.CanPickup(user.Value, item.Value)) + return false; + Eject(uid, slot, item!.Value, user, excludeUserAudio); return true; } diff --git a/Content.Shared/Movement/EntitySystems/SharedMoverSystem.cs b/Content.Shared/Movement/EntitySystems/SharedMoverSystem.cs index 9012abdc7e..99fa7e987d 100644 --- a/Content.Shared/Movement/EntitySystems/SharedMoverSystem.cs +++ b/Content.Shared/Movement/EntitySystems/SharedMoverSystem.cs @@ -1,12 +1,10 @@ using System.Diagnostics.CodeAnalysis; using Content.Shared.MobState.Components; using Content.Shared.Movement.Components; +using Content.Shared.Vehicle.Components; using Robust.Shared.Containers; -using Robust.Shared.GameObjects; using Robust.Shared.Input; using Robust.Shared.Input.Binding; -using Robust.Shared.IoC; -using Robust.Shared.Maths; using Robust.Shared.Players; namespace Content.Shared.Movement.EntitySystems @@ -60,6 +58,14 @@ namespace Content.Shared.Movement.EntitySystems var relayMoveEvent = new RelayMovementEntityEvent(owner.Value); EntityManager.EventBus.RaiseLocalEvent(EntityManager.GetComponent(owner.Value).ParentUid, relayMoveEvent); } + // Pass the rider's inputs to the vehicle (the rider itself is on the ignored list in C.S/MoverController.cs) + if (TryComp(owner.Value, out var rider) && rider.Vehicle != null && rider.Vehicle.HasKey) + { + if (TryComp(rider.Vehicle.Owner, out var vehicleMover)) + { + vehicleMover.SetVelocityDirection(dir, subTick, state); + } + } } moverComp.SetVelocityDirection(dir, subTick, state); diff --git a/Content.Shared/Vehicle/Components/InVehicleComponent.cs b/Content.Shared/Vehicle/Components/InVehicleComponent.cs new file mode 100644 index 0000000000..7e30024917 --- /dev/null +++ b/Content.Shared/Vehicle/Components/InVehicleComponent.cs @@ -0,0 +1,16 @@ +namespace Content.Shared.Vehicle.Components +{ + /// + /// Added to objects inside a vehicle to stop people besides the rider from + /// removing them. + /// + [RegisterComponent] + public sealed class InVehicleComponent : Component + { + /// + /// The vehicle this rider is currently riding. + /// + [ViewVariables] + public VehicleComponent Vehicle = default!; + } +} diff --git a/Content.Shared/Vehicle/Components/RiderComponent.cs b/Content.Shared/Vehicle/Components/RiderComponent.cs new file mode 100644 index 0000000000..846b566063 --- /dev/null +++ b/Content.Shared/Vehicle/Components/RiderComponent.cs @@ -0,0 +1,16 @@ +namespace Content.Shared.Vehicle.Components +{ + /// + /// Added to people when they are riding in a vehicle + /// used mostly to keep track of them for entityquery. + /// + [RegisterComponent] + public sealed class RiderComponent : Component + { + /// + /// The vehicle this rider is currently riding. + /// + [ViewVariables] + public VehicleComponent Vehicle = default!; + } +} diff --git a/Content.Shared/Vehicle/Components/VehicleComponent.cs b/Content.Shared/Vehicle/Components/VehicleComponent.cs new file mode 100644 index 0000000000..bcb7204f8a --- /dev/null +++ b/Content.Shared/Vehicle/Components/VehicleComponent.cs @@ -0,0 +1,98 @@ +using Content.Shared.Actions.ActionTypes; +using Content.Shared.Sound; +using Content.Shared.Containers.ItemSlots; +using Robust.Shared.Audio; +using Robust.Shared.Utility; +using Content.Shared.Whitelist; + +namespace Content.Shared.Vehicle.Components +{ + /// + /// This is particularly for vehicles that use + /// buckle. Stuff like clown cars may need a different + /// component at some point. + /// All vehicles should have Physics, Strap, and SharedPlayerInputMover components. + /// + [RegisterComponent] + public sealed class VehicleComponent : Component + { + /// + /// Whether someone is currently riding the vehicle + /// + /// The entity currently riding the vehicle. + /// + [ViewVariables] + public EntityUid? Rider; + + /// + /// Whether the vehicle should treat north as it's unique direction in its visualizer + /// + [DataField("northOnly")] + public bool NorthOnly = false; + + /// + /// What the y buckle offset should be in north / south + /// + [DataField("northOverride")] + public float NorthOverride = 0f; + + /// + /// What the y buckle offset should be in north / south + /// + [DataField("southOverride")] + public float SouthOverride = 0f; + + /// + /// The base offset for the vehicle (when facing east) + /// + public Vector2 BaseBuckleOffset = Vector2.Zero; + + /// + /// The sound that the horn makes + /// + [DataField("hornSound")] + public SoundSpecifier? HornSound = new SoundPathSpecifier("/Audio/Effects/Vehicle/carhorn.ogg"); + + /// + /// Whether the horn is a siren or not. + /// + [DataField("hornIsSiren")] + public bool HornIsLooping = false; + + /// + /// If this vehicle has a siren currently playing. + /// + public bool LoopingHornIsPlaying = false; + + public IPlayingAudioStream? SirenPlayingStream; + + /// Use ambient sound component for the idle sound. + + /// + /// The action for the horn (if any) + /// + [DataField("hornAction")] + public InstantAction HornAction = new() + { + UseDelay = TimeSpan.FromSeconds(3.4), + Icon = new SpriteSpecifier.Texture(new ResourcePath("Objects/Fun/bikehorn.rsi/icon.png")), + Name = "action-name-honk", + Description = "action-desc-honk", + Event = new HonkActionEvent(), + }; + + /// + /// The prototype ID of the key that was inserted so it can be + /// spawned when the key is removed. + /// + public ItemSlot KeySlot = new(); + + /// + /// Whether the vehicle has a key currently inside it or not. + /// + public bool HasKey = false; + } +} diff --git a/Content.Shared/Vehicle/SharedVehicleSystem.cs b/Content.Shared/Vehicle/SharedVehicleSystem.cs new file mode 100644 index 0000000000..d223f819f9 --- /dev/null +++ b/Content.Shared/Vehicle/SharedVehicleSystem.cs @@ -0,0 +1,51 @@ +using Content.Shared.Vehicle.Components; +using Content.Shared.Actions; +using Content.Shared.Item; +using Robust.Shared.Serialization; + +/// +/// Stores the VehicleVisuals and shared event +/// Nothing for a system but these need to be put somewhere in +/// Content.Shared +/// +namespace Content.Shared.Vehicle +{ + public sealed class SharedVehicleSystem : EntitySystem + { + public override void Initialize() + { + base.Initialize(); + SubscribeLocalEvent(OnPickupAttempt); + } + + private void OnPickupAttempt(EntityUid uid, InVehicleComponent component, GettingPickedUpAttemptEvent args) + { + if (component.Vehicle == null || !component.Vehicle.HasRider) + return; + + if (component.Vehicle.Rider != args.User) + args.Cancel(); + } + } + + + /// + /// Stores the vehicle's draw depth mostly + /// + [Serializable, NetSerializable] + public enum VehicleVisuals : byte + { + /// + /// What layer the vehicle should draw on (assumed integer) + /// + DrawDepth, + /// + /// Whether the wheels should be turning + /// + AutoAnimate + } + /// + /// Raised when someone honks a vehicle horn + /// + public sealed class HonkActionEvent : InstantActionEvent { } +} diff --git a/Resources/Audio/Effects/Vehicle/carhorn.ogg b/Resources/Audio/Effects/Vehicle/carhorn.ogg new file mode 100644 index 0000000000..53b6801103 Binary files /dev/null and b/Resources/Audio/Effects/Vehicle/carhorn.ogg differ diff --git a/Resources/Audio/Effects/Vehicle/license.txt b/Resources/Audio/Effects/Vehicle/license.txt new file mode 100644 index 0000000000..9db54a21f6 --- /dev/null +++ b/Resources/Audio/Effects/Vehicle/license.txt @@ -0,0 +1,5 @@ +vehiclestartup.ogg and vehicleengineidle.ogg taken from user InspectorJ at https://freesound.org/people/InspectorJ/sounds/338954/ + +carhorn.ogg taken from /tg/station at commit https://github.com/tgstation/tgstation/commit/40d89d11ea4a5cb81d61dc1018b46f4e7d32c62a + +policesiren.ogg taken from freesound user blukotek at https://freesound.org/people/blukotek/sounds/431651/ diff --git a/Resources/Audio/Effects/Vehicle/policesiren.ogg b/Resources/Audio/Effects/Vehicle/policesiren.ogg new file mode 100644 index 0000000000..65d71ee5d5 Binary files /dev/null and b/Resources/Audio/Effects/Vehicle/policesiren.ogg differ diff --git a/Resources/Audio/Effects/Vehicle/vehicleengineidle.ogg b/Resources/Audio/Effects/Vehicle/vehicleengineidle.ogg new file mode 100644 index 0000000000..2d7911ef7b Binary files /dev/null and b/Resources/Audio/Effects/Vehicle/vehicleengineidle.ogg differ diff --git a/Resources/Audio/Effects/Vehicle/vehiclestartup.ogg b/Resources/Audio/Effects/Vehicle/vehiclestartup.ogg new file mode 100644 index 0000000000..195d00fe52 Binary files /dev/null and b/Resources/Audio/Effects/Vehicle/vehiclestartup.ogg differ diff --git a/Resources/Audio/Items/license.txt b/Resources/Audio/Items/license.txt index 0bccb3cac2..e68584bde4 100644 --- a/Resources/Audio/Items/license.txt +++ b/Resources/Audio/Items/license.txt @@ -1 +1,2 @@ pill.ogg contains audio from '437480__ruanza__swallowing.wav' by user RuanZA, used under CC BY-NC 3.0 (https://freesound.org/people/RuanZA/sounds/437480/), as well as part of '330657__diniunicorn__popping-pills.wav' by user diniunicorn, used under CC0 1.0 (https://freesound.org/people/diniunicorn/sounds/330657/). ring.ogg used udner CC-BY-SA-3.0, taken from /tg/station commit https://github.com/tgstation/tgstation/commit/c61c452d78425d89920b41ed5f95fd190e733a3c. + diff --git a/Resources/Locale/en-US/actions/actions/vehicle.ftl b/Resources/Locale/en-US/actions/actions/vehicle.ftl new file mode 100644 index 0000000000..4b9014249f --- /dev/null +++ b/Resources/Locale/en-US/actions/actions/vehicle.ftl @@ -0,0 +1,4 @@ +action-name-honk = Honk +action-desc-honk = Honk! +action-name-siren = Toggle Siren +action-desc-siren = Wee-woo. diff --git a/Resources/Locale/en-US/vehicle/vehicle.ftl b/Resources/Locale/en-US/vehicle/vehicle.ftl new file mode 100644 index 0000000000..3aa5def1e7 --- /dev/null +++ b/Resources/Locale/en-US/vehicle/vehicle.ftl @@ -0,0 +1 @@ +vehicle-use-key = You use {THE($keys)} to start {THE($vehicle)}. diff --git a/Resources/Prototypes/Catalog/Cargo/cargo_fun.yml b/Resources/Prototypes/Catalog/Cargo/cargo_fun.yml index 56b213cde3..ddefa9cef3 100644 --- a/Resources/Prototypes/Catalog/Cargo/cargo_fun.yml +++ b/Resources/Prototypes/Catalog/Cargo/cargo_fun.yml @@ -57,3 +57,15 @@ cost: 1500 category: Fun group: market + +- type: cargoProduct + name: "ATV crate" + id: FunATV + description: "An Absolutely Taxable Vehicle to help cargo with hauling." + icon: + sprite: Objects/Vehicles/atv.rsi + state: vehicle + product: CrateFunATV + cost: 2500 + category: Fun + group: market diff --git a/Resources/Prototypes/Catalog/Fills/Crates/fun.yml b/Resources/Prototypes/Catalog/Fills/Crates/fun.yml index 2e70209f55..3128bfbbb8 100644 --- a/Resources/Prototypes/Catalog/Fills/Crates/fun.yml +++ b/Resources/Prototypes/Catalog/Fills/Crates/fun.yml @@ -111,3 +111,15 @@ amount: 1 - id: d6Dice amount: 4 + +- type: entity + id: CrateFunATV + name: ATV crate + parent: CrateLivestock + components: + - type: StorageFill + contents: + - id: VehicleATV + amount: 1 + - id: VehicleKeyATV + amount: 1 diff --git a/Resources/Prototypes/Entities/Mobs/Player/admin_ghost.yml b/Resources/Prototypes/Entities/Mobs/Player/admin_ghost.yml index e177170e64..8f8bfdf7b5 100644 --- a/Resources/Prototypes/Entities/Mobs/Player/admin_ghost.yml +++ b/Resources/Prototypes/Entities/Mobs/Player/admin_ghost.yml @@ -145,6 +145,7 @@ - FunArtSupplies - FunInstruments - FunBoardGames + - FunATV - MaterialSteel - MaterialGlass - MaterialPlastic diff --git a/Resources/Prototypes/Entities/Mobs/Player/silicon.yml b/Resources/Prototypes/Entities/Mobs/Player/silicon.yml index d71b70da50..104f7345c3 100644 --- a/Resources/Prototypes/Entities/Mobs/Player/silicon.yml +++ b/Resources/Prototypes/Entities/Mobs/Player/silicon.yml @@ -162,8 +162,8 @@ event: !type:ToggleActionEvent - type: PointLight enabled: false - radius: 2.5 - softness: 5 + radius: 3.5 + softness: 1 mask: /Textures/Effects/LightMasks/cone.png autoRot: true - type: Tag diff --git a/Resources/Prototypes/Entities/Objects/Vehicles/buckleable.yml b/Resources/Prototypes/Entities/Objects/Vehicles/buckleable.yml new file mode 100644 index 0000000000..52195984cc --- /dev/null +++ b/Resources/Prototypes/Entities/Objects/Vehicles/buckleable.yml @@ -0,0 +1,257 @@ +- type: entity + id: VehicleBase + save: false + abstract: true + name: Vehicle + components: + - type: Strap + buckleOffset: "0.10, 0.36" + maxBuckleDistance: 1 + - type: AmbientSound + sound: "/Audio/Effects/Vehicle/vehicleengineidle.ogg" + range: 10 + volume: -1 + - type: PlayerInputMover + - type: InteractionOutline + - type: Vehicle + - type: MovementSpeedModifier + baseWalkSpeed : 7 + baseSprintSpeed : 7 + - type: Tag + - type: Pullable + - type: Physics + bodyType: KinematicController + - type: Clickable + - type: Fixtures + fixtures: + - shape: + !type:PhysShapeCircle + radius: 0.4 + mass: 180 + restitution: 0.0 + mask: + - Impassable + - VaultImpassable + layer: + - VaultImpassable + - type: VehicleVisuals + - type: Appearance + - type: Repairable + fuelcost: 20 + doAfterDelay: 20 + - type: Damageable + damageContainer: Inorganic + damageModifierSet: Metallic + - type: Destructible + thresholds: + - trigger: + !type:DamageTrigger + damage: 300 + behaviors: + - !type:DoActsBehavior + acts: ["Destruction"] + - !type:PlaySoundBehavior + sound: + path: /Audio/Effects/metalbreak.ogg + - !type:ExplodeBehavior + - type: ItemSlots + slots: + key_slot: #this slot name is important + name: Keys + whitelist: + requireAll: true + tags: + - VehicleKey + insertSound: /Audio/Effects/Vehicle/vehiclestartup.ogg + +- type: entity + id: VehiclePussyWagon + parent: VehicleBase + name: janicart + description: The janitor's trusty steed. + components: + - type: Vehicle + northOverride: -0.15 + southOverride: 0.22 + - type: Sprite + sprite: Objects/Vehicles/pussywagon.rsi + layers: + - state: vehicle + map: ["enum.VehicleVisualLayers.AutoAnimate"] + netsync: false + noRot: true + - type: Access + tags: + - Maintenance + - Janitor + - type: UnpoweredFlashlight + toggleAction: + name: action-name-toggle-light + description: action-description-toggle-light + icon: Objects/Tools/flashlight.rsi/flashlight.png + iconOn: Objects/Tools/flashlight.rsi/flashlight-on.png + event: !type:ToggleActionEvent + - type: PointLight + enabled: false + radius: 3.5 + softness: 2 + mask: /Textures/Effects/LightMasks/cone.png + autoRot: true + - type: Destructible + thresholds: + - trigger: + !type:DamageTrigger + damage: 250 + behaviors: + - !type:DoActsBehavior + acts: ["Destruction"] + - !type:PlaySoundBehavior + sound: + path: /Audio/Effects/metalbreak.ogg + - !type:ExplodeBehavior + - !type:SpawnEntitiesBehavior # in future should also emit a cloud of hot gas + spawn: + VehiclePussyWagonDestroyed: + min: 1 + max: 1 + - type: ItemSlots + slots: + key_slot: + name: Keys + whitelist: + requireAll: true + tags: + - VehicleKey + - PussyWagonKeys + insertSound: /Audio/Effects/Vehicle/vehiclestartup.ogg + trashbag_slot: + name: Bag + whitelist: + tags: + - TrashBag + - type: ItemMapper + mapLayers: + storage: + whitelist: + tags: + - TrashBag + - type: Appearance + visuals: + - type: MappedItemVisualizer + sprite: Objects/Vehicles/pussywagon.rsi + +- type: entity + id: VehiclePussyWagonDestroyed + parent: MachineFrameDestroyed + name: destroyed janicart + description: R.I.P. (Rest In Pussy) + components: + - type: Sprite + sprite: Objects/Vehicles/pussywagon.rsi + state: destroyed + netsync: false + +- type: entity + id: VehicleSecway + parent: VehicleBase + name: secway + description: The future of transportation. Popularized by St. James, the patron saint of security officers and internet forum moderators. + components: + - type: Vehicle + northOnly: true + northOverride: -0.1 + southOverride: 0.1 + hornSound: + path: /Audio/Effects/Vehicle/policesiren.ogg + hornAction: + name: action-name-siren + description: action-desc-siren + icon: Objects/Fun/bikehorn.rsi/icon.png + iconOn: Objects/Fun/bikehorn.rsi/icon.png + event: !type:ToggleActionEvent + hornIsSiren: true + - type: Sprite + sprite: Objects/Vehicles/secway.rsi + layers: + - state: vehicle + map: ["enum.VehicleVisualLayers.AutoAnimate"] + netsync: false + noRot: true + - type: Access + tags: + - Security + - Brig + - Maintenance + - Service + - type: Strap + buckleOffset: "0.15, -0.05" + maxBuckleDistance: 1 + - type: MovementSpeedModifier + baseWalkSpeed : 8 + baseSprintSpeed : 8 + - type: Armor + modifiers: + coeffecients: + Blunt: 0.8 + Slash: 0.6 + Piercing: 0.85 + - type: ItemSlots + slots: + key_slot: + name: Keys + whitelist: + requireAll: true + tags: + - VehicleKey + - SecwayKeys + insertSound: /Audio/Effects/Vehicle/vehiclestartup.ogg + +- type: entity + parent: VehicleBase + id: VehicleATV + name: ATV + description: All-Tile Vehicle. + components: + - type: Vehicle + northOverride: -0.1 + southOverride: 0.1 + - type: Sprite + sprite: Objects/Vehicles/atv.rsi + layers: + - state: vehicle + map: ["enum.VehicleVisualLayers.AutoAnimate"] + netsync: false + noRot: true + - type: Access + tags: + - Cargo + - Maintenance + - type: MovementSpeedModifier + baseWalkSpeed : 8 + baseSprintSpeed : 8 + - type: Strap + buckleOffset: "0.1, -0.05" + maxBuckleDistance: 1 + - type: UnpoweredFlashlight + toggleAction: + name: action-name-toggle-light + description: action-description-toggle-light + icon: Objects/Tools/flashlight.rsi/flashlight.png + iconOn: Objects/Tools/flashlight.rsi/flashlight-on.png + event: !type:ToggleActionEvent + - type: PointLight + enabled: false + radius: 3.5 + softness: 2 + mask: /Textures/Effects/LightMasks/cone.png + autoRot: true + - type: ItemSlots + slots: + key_slot: + name: Keys + whitelist: + requireAll: true + tags: + - VehicleKey + - ATVKeys + insertSound: /Audio/Effects/Vehicle/vehiclestartup.ogg diff --git a/Resources/Prototypes/Entities/Objects/Vehicles/keys.yml b/Resources/Prototypes/Entities/Objects/Vehicles/keys.yml new file mode 100644 index 0000000000..518bb142e8 --- /dev/null +++ b/Resources/Prototypes/Entities/Objects/Vehicles/keys.yml @@ -0,0 +1,59 @@ +- type: entity + parent: BaseItem + id: VehicleKeyPussyWagon + name: janicart keys + description: Interesting design. + components: + - type: Item + size: 2 + - type: Tag + tags: + - VehicleKey + - PussyWagonKeys + - type: Sprite + sprite: Objects/Vehicles/pussywagon.rsi + state: keys + netsync: false + +- type: entity + parent: VehicleKeyPussyWagon + id: VehicleKeySecway + name: secway keys + description: The keys to the future. + components: + - type: Tag + tags: + - VehicleKey + - SecwayKeys + - type: Sprite + sprite: Objects/Vehicles/secway.rsi + state: keys + netsync: false + +- type: entity + parent: VehicleKeyPussyWagon + id: VehicleKeyATV + name: ATV keys + description: Think this looks like just one key? ATV keys means "actually two vehicle keys." + components: + - type: Tag + tags: + - VehicleKey + - ATVKeys + - type: Sprite + sprite: Objects/Vehicles/atv.rsi + state: keys + netsync: false + +- type: entity + parent: VehicleKeyATV + id: VehicleKeySkeleton + name: vehicle skeleton keys + description: Unlock any vehicle. + components: + - type: Tag + tags: + - VehicleKey + - PussyWagonKeys + - SecwayKeys + - ATVKeys diff --git a/Resources/Prototypes/Entities/Structures/Machines/Computers/computers.yml b/Resources/Prototypes/Entities/Structures/Machines/Computers/computers.yml index f7f5da7de9..f265df3597 100644 --- a/Resources/Prototypes/Entities/Structures/Machines/Computers/computers.yml +++ b/Resources/Prototypes/Entities/Structures/Machines/Computers/computers.yml @@ -395,6 +395,7 @@ - FunInstruments - FunBrass - FunBoardGames + - FunATV - MaterialSteel - MaterialGlass - MaterialPlastic diff --git a/Resources/Prototypes/tags.yml b/Resources/Prototypes/tags.yml index b3163bd049..17d19762fe 100644 --- a/Resources/Prototypes/tags.yml +++ b/Resources/Prototypes/tags.yml @@ -3,6 +3,9 @@ - type: Tag id: AirAlarmElectronics +- type: Tag + id: ATVKeys + - type: Tag id: Baguette @@ -123,6 +126,9 @@ - type: Tag id: Donut +- type: Tag + id: Drone + - type: Tag id: DroneUsable @@ -264,6 +270,9 @@ - type: Tag id: Powerdrill +- type: Tag + id: PussyWagonKeys + # Give this to something that doesn't need any special recycler behavior and just needs deleting. - type: Tag id: Recyclable @@ -280,6 +289,9 @@ - type: Tag id: Screwdriver +- type: Tag + id: SecwayKeys + - type: Tag id: Sheet @@ -310,6 +322,9 @@ - type: Tag id: TrashBag +- type: Tag + id: VehicleKey + - type: Tag id: WetFloorSign @@ -336,6 +351,3 @@ - type: Tag id: Write - -- type: Tag - id: Drone diff --git a/Resources/Textures/Objects/Vehicles/atv.rsi/keys.png b/Resources/Textures/Objects/Vehicles/atv.rsi/keys.png new file mode 100644 index 0000000000..d1b314b8a2 Binary files /dev/null and b/Resources/Textures/Objects/Vehicles/atv.rsi/keys.png differ diff --git a/Resources/Textures/Objects/Vehicles/atv.rsi/meta.json b/Resources/Textures/Objects/Vehicles/atv.rsi/meta.json new file mode 100644 index 0000000000..402589e84d --- /dev/null +++ b/Resources/Textures/Objects/Vehicles/atv.rsi/meta.json @@ -0,0 +1,44 @@ +{ + "version": 1, + "size": { + "x": 32, + "y": 32 + }, + "license": "CC-BY-SA-3.0", + "copyright": "Taken from tgstation at https://github.com/tgstation/tgstation/commit/40d89d11ea4a5cb81d61dc1018b46f4e7d32c62a", + "states": [ + { + "name": "vehicle", + "directions": 4, + "delays": [ + [ + 0.2, + 0.2, + 0.2, + 0.2 + ], + [ + 0.2, + 0.2, + 0.2, + 0.2 + ], + [ + 0.2, + 0.2, + 0.2, + 0.2 + ], + [ + 0.2, + 0.2, + 0.2, + 0.2 + ] + ] + }, + { + "name": "keys" + } + ] +} diff --git a/Resources/Textures/Objects/Vehicles/atv.rsi/vehicle.png b/Resources/Textures/Objects/Vehicles/atv.rsi/vehicle.png new file mode 100644 index 0000000000..ddff0b2f1c Binary files /dev/null and b/Resources/Textures/Objects/Vehicles/atv.rsi/vehicle.png differ diff --git a/Resources/Textures/Objects/Vehicles/pussywagon.rsi/destroyed.png b/Resources/Textures/Objects/Vehicles/pussywagon.rsi/destroyed.png new file mode 100644 index 0000000000..3cf275c769 Binary files /dev/null and b/Resources/Textures/Objects/Vehicles/pussywagon.rsi/destroyed.png differ diff --git a/Resources/Textures/Objects/Vehicles/pussywagon.rsi/keys.png b/Resources/Textures/Objects/Vehicles/pussywagon.rsi/keys.png new file mode 100644 index 0000000000..7a362ca331 Binary files /dev/null and b/Resources/Textures/Objects/Vehicles/pussywagon.rsi/keys.png differ diff --git a/Resources/Textures/Objects/Vehicles/pussywagon.rsi/meta.json b/Resources/Textures/Objects/Vehicles/pussywagon.rsi/meta.json new file mode 100644 index 0000000000..ef5d1c28e9 --- /dev/null +++ b/Resources/Textures/Objects/Vehicles/pussywagon.rsi/meta.json @@ -0,0 +1,47 @@ +{ + "version": 1, + "size": { + "x": 32, + "y": 32 + }, + "license": "CC-BY-SA-3.0", + "copyright": "Taken from tgstation at https://github.com/tgstation/tgstation/commit/40d89d11ea4a5cb81d61dc1018b46f4e7d32c62a", + "states": [ + { + "name": "vehicle", + "directions": 4, + "delays": [ + [ + 0.2, + 0.2, + 0.2 + ], + [ + 0.2, + 0.2, + 0.2 + ], + [ + 0.2, + 0.2, + 0.2 + ], + [ + 0.2, + 0.2, + 0.2 + ] + ] + }, + { + "name": "storage", + "directions": 4 + }, + { + "name": "destroyed" + }, + { + "name": "keys" + } + ] +} diff --git a/Resources/Textures/Objects/Vehicles/pussywagon.rsi/storage.png b/Resources/Textures/Objects/Vehicles/pussywagon.rsi/storage.png new file mode 100644 index 0000000000..25cb598fce Binary files /dev/null and b/Resources/Textures/Objects/Vehicles/pussywagon.rsi/storage.png differ diff --git a/Resources/Textures/Objects/Vehicles/pussywagon.rsi/vehicle.png b/Resources/Textures/Objects/Vehicles/pussywagon.rsi/vehicle.png new file mode 100644 index 0000000000..0eb087a925 Binary files /dev/null and b/Resources/Textures/Objects/Vehicles/pussywagon.rsi/vehicle.png differ diff --git a/Resources/Textures/Objects/Vehicles/secway.rsi/keys.png b/Resources/Textures/Objects/Vehicles/secway.rsi/keys.png new file mode 100644 index 0000000000..30281fdf96 Binary files /dev/null and b/Resources/Textures/Objects/Vehicles/secway.rsi/keys.png differ diff --git a/Resources/Textures/Objects/Vehicles/secway.rsi/meta.json b/Resources/Textures/Objects/Vehicles/secway.rsi/meta.json new file mode 100644 index 0000000000..853fbde3d9 --- /dev/null +++ b/Resources/Textures/Objects/Vehicles/secway.rsi/meta.json @@ -0,0 +1,40 @@ +{ + "version": 1, + "size": { + "x": 32, + "y": 32 + }, + "license": "CC-BY-SA-3.0", + "copyright": "Taken from tgstation at https://github.com/tgstation/tgstation/commit/40d89d11ea4a5cb81d61dc1018b46f4e7d32c62a", + "states": [ + { + "name": "vehicle", + "directions": 4, + "delays": [ + [ + 0.2, + 0.2, + 0.2 + ], + [ + 0.2, + 0.2, + 0.2 + ], + [ + 0.2, + 0.2, + 0.2 + ], + [ + 0.2, + 0.2, + 0.2 + ] + ] + }, + { + "name": "keys" + } + ] +} diff --git a/Resources/Textures/Objects/Vehicles/secway.rsi/vehicle.png b/Resources/Textures/Objects/Vehicles/secway.rsi/vehicle.png new file mode 100644 index 0000000000..ca75805957 Binary files /dev/null and b/Resources/Textures/Objects/Vehicles/secway.rsi/vehicle.png differ