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