diff --git a/.github/workflows/conflict-labeler.yml b/.github/workflows/conflict-labeler.yml deleted file mode 100644 index 1bba6770222..00000000000 --- a/.github/workflows/conflict-labeler.yml +++ /dev/null @@ -1,21 +0,0 @@ -name: Check Merge Conflicts - -on: - pull_request_target: - types: - - opened - - synchronize - - reopened - - ready_for_review - -jobs: - Label: - if: ( github.event.pull_request.draft == false ) && ( github.actor != 'PJBot' && github.actor != 'DeltaV-Bot' && github.actor != 'SimpleStation14' ) - runs-on: ubuntu-latest - steps: - - name: Check for Merge Conflicts - uses: eps1lon/actions-label-merge-conflict@v3.0.0 - with: - dirtyLabel: "Status: Merge Conflict" - repoToken: "${{ secrets.GITHUB_TOKEN }}" - commentOnDirty: "This pull request has conflicts, please resolve those before we can evaluate the pull request." diff --git a/Content.Client/Buckle/BuckleSystem.cs b/Content.Client/Buckle/BuckleSystem.cs index 6fe6e308de2..5e62a605e10 100644 --- a/Content.Client/Buckle/BuckleSystem.cs +++ b/Content.Client/Buckle/BuckleSystem.cs @@ -1,6 +1,7 @@ using Content.Client.Rotation; using Content.Shared.Buckle; using Content.Shared.Buckle.Components; +using Content.Shared.Movement.Systems; using Content.Shared.Rotation; using Robust.Client.GameObjects; using Robust.Client.Graphics; @@ -12,6 +13,7 @@ internal sealed class BuckleSystem : SharedBuckleSystem [Dependency] private readonly RotationVisualizerSystem _rotationVisualizerSystem = default!; [Dependency] private readonly IEyeManager _eye = default!; [Dependency] private readonly SharedTransformSystem _xformSystem = default!; + [Dependency] private readonly SpriteSystem _sprite = default!; public override void Initialize() { @@ -21,6 +23,15 @@ public override void Initialize() SubscribeLocalEvent(OnStrapMoveEvent); SubscribeLocalEvent(OnBuckledEvent); SubscribeLocalEvent(OnUnbuckledEvent); + SubscribeLocalEvent(OnMobCollide); + } + + private void OnMobCollide(Entity ent, ref AttemptMobCollideEvent args) + { + if (ent.Comp.Buckled) + { + args.Cancelled = true; + } } private void OnStrapMoveEvent(EntityUid uid, StrapComponent component, ref MoveEvent args) @@ -38,6 +49,9 @@ private void OnStrapMoveEvent(EntityUid uid, StrapComponent component, ref MoveE // Give some of the sprite rotations their own drawdepth, maybe as an offset within the rsi, or something like this // And we won't ever need to set the draw depth manually + if (!component.ModifyBuckleDrawDepth) + return; + if (args.NewRotation == args.OldRotation) return; @@ -59,11 +73,11 @@ private void OnStrapMoveEvent(EntityUid uid, StrapComponent component, ref MoveE { // This will only assign if empty, it won't get overwritten by new depth on multiple calls, which do happen easily buckle.OriginalDrawDepth ??= buckledSprite.DrawDepth; - buckledSprite.DrawDepth = strapSprite.DrawDepth - 1; + _sprite.SetDrawDepth((buckledEntity, buckledSprite), strapSprite.DrawDepth - 1); } else if (buckle.OriginalDrawDepth.HasValue) { - buckledSprite.DrawDepth = buckle.OriginalDrawDepth.Value; + _sprite.SetDrawDepth((buckledEntity, buckledSprite), buckle.OriginalDrawDepth.Value); buckle.OriginalDrawDepth = null; } } @@ -75,6 +89,9 @@ private void OnStrapMoveEvent(EntityUid uid, StrapComponent component, ref MoveE /// private void OnBuckledEvent(Entity ent, ref BuckledEvent args) { + if (!args.Strap.Comp.ModifyBuckleDrawDepth) + return; + if (!TryComp(args.Strap, out var strapSprite)) return; @@ -87,7 +104,7 @@ private void OnBuckledEvent(Entity ent, ref BuckledEvent args) return; ent.Comp.OriginalDrawDepth ??= buckledSprite.DrawDepth; - buckledSprite.DrawDepth = strapSprite.DrawDepth - 1; + _sprite.SetDrawDepth((ent.Owner, buckledSprite), strapSprite.DrawDepth - 1); } /// @@ -95,23 +112,32 @@ private void OnBuckledEvent(Entity ent, ref BuckledEvent args) /// private void OnUnbuckledEvent(Entity ent, ref UnbuckledEvent args) { + if (!args.Strap.Comp.ModifyBuckleDrawDepth) + return; + if (!TryComp(ent.Owner, out var buckledSprite)) return; if (!ent.Comp.OriginalDrawDepth.HasValue) return; - buckledSprite.DrawDepth = ent.Comp.OriginalDrawDepth.Value; + _sprite.SetDrawDepth((ent.Owner, buckledSprite), ent.Comp.OriginalDrawDepth.Value); ent.Comp.OriginalDrawDepth = null; } private void OnAppearanceChange(EntityUid uid, BuckleComponent component, ref AppearanceChangeEvent args) { - if (!TryComp(uid, out var rotVisuals) - || !Appearance.TryGetData(uid, BuckleVisuals.Buckled, out var buckled, args.Component) - || !buckled || args.Sprite == null) + if (!TryComp(uid, out var rotVisuals)) return; + if (!Appearance.TryGetData(uid, BuckleVisuals.Buckled, out var buckled, args.Component) || + !buckled || + args.Sprite == null) + { + _rotationVisualizerSystem.SetHorizontalAngle((uid, rotVisuals), rotVisuals.DefaultRotation); + return; + } + // Animate strapping yourself to something at a given angle // TODO: Dump this when buckle is better _rotationVisualizerSystem.AnimateSpriteRotation(uid, args.Sprite, rotVisuals.HorizontalRotation, 0.125f); diff --git a/Content.Client/Eye/EyeLerpingSystem.cs b/Content.Client/Eye/EyeLerpingSystem.cs index ac32299dca7..c0a7c016966 100644 --- a/Content.Client/Eye/EyeLerpingSystem.cs +++ b/Content.Client/Eye/EyeLerpingSystem.cs @@ -34,7 +34,7 @@ public override void Initialize() SubscribeLocalEvent(OnDetached); UpdatesAfter.Add(typeof(TransformSystem)); - UpdatesAfter.Add(typeof(PhysicsSystem)); + UpdatesAfter.Add(typeof(Robust.Client.Physics.PhysicsSystem)); UpdatesBefore.Add(typeof(SharedEyeSystem)); UpdatesOutsidePrediction = true; } diff --git a/Content.Client/NPC/NPCSteeringSystem.cs b/Content.Client/NPC/NPCSteeringSystem.cs index eda3d74cf71..9ca3ec1a7ea 100644 --- a/Content.Client/NPC/NPCSteeringSystem.cs +++ b/Content.Client/NPC/NPCSteeringSystem.cs @@ -1,5 +1,6 @@ using System.Numerics; using Content.Client.Physics.Controllers; +using Content.Client.PhysicsSystem.Controllers; using Content.Shared.Movement.Components; using Content.Shared.NPC; using Content.Shared.NPC.Events; diff --git a/Content.Client/Physics/Controllers/MoverController.cs b/Content.Client/Physics/Controllers/MoverController.cs index ba31c5f6ffd..0f95a817c99 100644 --- a/Content.Client/Physics/Controllers/MoverController.cs +++ b/Content.Client/Physics/Controllers/MoverController.cs @@ -1,23 +1,23 @@ +using Content.Shared.Alert; using Content.Shared.CCVar; +using Content.Shared.Friction; using Content.Shared.Movement.Components; -using Content.Shared.Movement.Events; using Content.Shared.Movement.Pulling.Components; using Content.Shared.Movement.Systems; -using Robust.Client.GameObjects; using Robust.Client.Physics; using Robust.Client.Player; using Robust.Shared.Configuration; -using Robust.Shared.Physics.Components; using Robust.Shared.Player; using Robust.Shared.Timing; -namespace Content.Client.Physics.Controllers; +namespace Content.Client.PhysicsSystem.Controllers; public sealed class MoverController : SharedMoverController { - [Dependency] private readonly IConfigurationManager _config = default!; [Dependency] private readonly IGameTiming _timing = default!; [Dependency] private readonly IPlayerManager _playerManager = default!; + [Dependency] private readonly AlertsSystem _alerts = default!; + [Dependency] private readonly IConfigurationManager _cfg = default!; public override void Initialize() { @@ -30,8 +30,6 @@ public override void Initialize() SubscribeLocalEvent(OnUpdatePredicted); SubscribeLocalEvent(OnUpdateRelayTargetPredicted); SubscribeLocalEvent(OnUpdatePullablePredicted); - - Subs.CVar(_config, CCVars.DefaultWalk, _ => RaiseNetworkEvent(new UpdateInputCVarsMessage())); } private void OnUpdatePredicted(Entity entity, ref UpdateIsPredictedEvent args) @@ -63,16 +61,16 @@ private void OnUpdatePullablePredicted(Entity entity, ref Upd private void OnRelayPlayerAttached(Entity entity, ref LocalPlayerAttachedEvent args) { - Physics.UpdateIsPredicted(entity.Owner); - Physics.UpdateIsPredicted(entity.Comp.RelayEntity); + PhysicsSystem.UpdateIsPredicted(entity.Owner); + PhysicsSystem.UpdateIsPredicted(entity.Comp.RelayEntity); if (MoverQuery.TryGetComponent(entity.Comp.RelayEntity, out var inputMover)) SetMoveInput((entity.Comp.RelayEntity, inputMover), MoveButtons.None); } private void OnRelayPlayerDetached(Entity entity, ref LocalPlayerDetachedEvent args) { - Physics.UpdateIsPredicted(entity.Owner); - Physics.UpdateIsPredicted(entity.Comp.RelayEntity); + PhysicsSystem.UpdateIsPredicted(entity.Owner); + PhysicsSystem.UpdateIsPredicted(entity.Comp.RelayEntity); if (MoverQuery.TryGetComponent(entity.Comp.RelayEntity, out var inputMover)) SetMoveInput((entity.Comp.RelayEntity, inputMover), MoveButtons.None); } @@ -102,43 +100,28 @@ public override void UpdateBeforeSolve(bool prediction, float frameTime) private void HandleClientsideMovement(EntityUid player, float frameTime) { - if (!MoverQuery.TryGetComponent(player, out var mover) || - !XformQuery.TryGetComponent(player, out var xform)) - { - return; - } - - var physicsUid = player; - PhysicsComponent? body; - var xformMover = xform; - - if (mover.ToParent && RelayQuery.HasComponent(xform.ParentUid)) - { - if (!PhysicsQuery.TryGetComponent(xform.ParentUid, out body) || - !XformQuery.TryGetComponent(xform.ParentUid, out xformMover)) - { - return; - } - - physicsUid = xform.ParentUid; - } - else if (!PhysicsQuery.TryGetComponent(player, out body)) + if (!MoverQuery.TryGetComponent(player, out var mover)) { return; } // Server-side should just be handled on its own so we'll just do this shizznit - HandleMobMovement( - player, - mover, - physicsUid, - body, - xformMover, - frameTime); + HandleMobMovement((player, mover), frameTime); } protected override bool CanSound() { return _timing is { IsFirstTimePredicted: true, InSimulation: true }; } + + public override void SetSprinting(Entity entity, ushort subTick, bool walking) + { + // Logger.Info($"[{_gameTiming.CurTick}/{subTick}] Sprint: {enabled}"); + base.SetSprinting(entity, subTick, walking); + + if (walking && _cfg.GetCVar(CCVars.ToggleWalk)) + _alerts.ShowAlert(entity.Owner, WalkingAlert, showCooldown: false, autoRemove: false); + else + _alerts.ClearAlert(entity.Owner, WalkingAlert); + } } diff --git a/Content.Client/_Goobstation/Vehicles/VehicleSystem.cs b/Content.Client/_Goobstation/Vehicles/VehicleSystem.cs deleted file mode 100644 index 937531fd2d1..00000000000 --- a/Content.Client/_Goobstation/Vehicles/VehicleSystem.cs +++ /dev/null @@ -1,60 +0,0 @@ -using Content.Shared.Vehicles; -using Robust.Client.GameObjects; -using Robust.Client.Graphics; - -namespace Content.Client.Vehicles; - -public sealed class VehicleSystem : SharedVehicleSystem -{ - [Dependency] private readonly SharedAppearanceSystem _appearance = default!; - [Dependency] private readonly IEyeManager _eye = default!; - - public override void Initialize() - { - base.Initialize(); - SubscribeLocalEvent(OnAppearanceChange); - SubscribeLocalEvent(OnMove); - } - - private void OnAppearanceChange(EntityUid uid, VehicleComponent comp, ref AppearanceChangeEvent args) - { - if (args.Sprite == null || !_appearance.TryGetData(uid, VehicleState.Animated, out bool animated) || !TryComp(uid, out var spriteComp)) - return; - - SpritePos(uid, comp); - spriteComp.LayerSetAutoAnimated(0, animated); - } - - private void OnMove(EntityUid uid, VehicleComponent component, ref MoveEvent args) - { - SpritePos(uid, component); - } - - private void SpritePos(EntityUid uid, VehicleComponent comp) - { - if (!TryComp(uid, out var spriteComp)) - return; - - if (!_appearance.TryGetData(uid, VehicleState.DrawOver, out bool depth)) - return; - - spriteComp.DrawDepth = (int) Shared.DrawDepth.DrawDepth.Objects; - - if (comp.RenderOver == VehicleRenderOver.None) - return; - - var eye = _eye.CurrentEye; - Direction vehicleDir = (Transform(uid).LocalRotation + eye.Rotation).GetCardinalDir(); - - VehicleRenderOver renderOver = (VehicleRenderOver) (1 << (int) vehicleDir); - - if ((comp.RenderOver & renderOver) == renderOver) - { - spriteComp.DrawDepth = (int) Shared.DrawDepth.DrawDepth.OverMobs; - } - else - { - spriteComp.DrawDepth = (int) Shared.DrawDepth.DrawDepth.Objects; - } - } -} diff --git a/Content.Server/CardboardBox/CardboardBoxSystem.cs b/Content.Server/CardboardBox/CardboardBoxSystem.cs index 836dc485d92..11e1dbdf59e 100644 --- a/Content.Server/CardboardBox/CardboardBoxSystem.cs +++ b/Content.Server/CardboardBox/CardboardBoxSystem.cs @@ -1,20 +1,10 @@ -using Content.Server.Storage.Components; -using Content.Server.Storage.EntitySystems; -using Content.Shared.Access.Components; using Content.Shared.CardboardBox; using Content.Shared.CardboardBox.Components; -using Content.Shared.Damage; -using Content.Shared.Interaction; -using Content.Shared.Movement.Components; -using Content.Shared.Movement.Systems; using Content.Shared.Stealth; using Content.Shared.Stealth.Components; using Content.Shared.Storage.Components; -using Robust.Server.GameObjects; -using Robust.Shared.Audio; +using Content.Shared.Vehicle; using Robust.Shared.Audio.Systems; -using Robust.Shared.Containers; -using Robust.Shared.Player; using Robust.Shared.Timing; namespace Content.Server.CardboardBox; @@ -22,11 +12,9 @@ namespace Content.Server.CardboardBox; public sealed class CardboardBoxSystem : SharedCardboardBoxSystem { [Dependency] private readonly SharedAudioSystem _audio = default!; - [Dependency] private readonly SharedMoverController _mover = default!; [Dependency] private readonly IGameTiming _timing = default!; [Dependency] private readonly SharedStealthSystem _stealth = default!; - [Dependency] private readonly DamageableSystem _damageable = default!; - [Dependency] private readonly EntityStorageSystem _storage = default!; + [Dependency] private readonly VehicleSystem _vehicle = default!; public override void Initialize() { @@ -34,43 +22,6 @@ public override void Initialize() SubscribeLocalEvent(AfterStorageOpen); SubscribeLocalEvent(BeforeStorageOpen); SubscribeLocalEvent(AfterStorageClosed); - SubscribeLocalEvent(OnGetAdditionalAccess); - SubscribeLocalEvent(OnInteracted); - SubscribeLocalEvent(OnEntInserted); - SubscribeLocalEvent(OnEntRemoved); - - SubscribeLocalEvent(OnDamage); - } - - private void OnInteracted(EntityUid uid, CardboardBoxComponent component, ActivateInWorldEvent args) - { - if (args.Handled) - return; - - if (!TryComp(uid, out var box)) - return; - - if (!args.Complex) - { - if (box.Open || !box.Contents.Contains(args.User)) - return; - } - - args.Handled = true; - _storage.ToggleOpen(args.User, uid, box); - - if (box.Contents.Contains(args.User) && !box.Open) - { - _mover.SetRelay(args.User, uid); - component.Mover = args.User; - } - } - - private void OnGetAdditionalAccess(EntityUid uid, CardboardBoxComponent component, ref GetAdditionalAccessEvent args) - { - if (component.Mover == null) - return; - args.Entities.Add(component.Mover.Value); } private void BeforeStorageOpen(EntityUid uid, CardboardBoxComponent component, ref StorageBeforeOpenEvent args) @@ -79,11 +30,11 @@ private void BeforeStorageOpen(EntityUid uid, CardboardBoxComponent component, r return; //Play effect & sound - if (component.Mover != null) + if (_vehicle.TryGetOperator(uid, out var operatorUid)) { if (_timing.CurTime > component.EffectCooldown) { - RaiseNetworkEvent(new PlayBoxEffectMessage(GetNetEntity(uid), GetNetEntity(component.Mover.Value))); + RaiseNetworkEvent(new PlayBoxEffectMessage(GetNetEntity(uid), GetNetEntity(operatorUid.Value))); _audio.PlayPvs(component.EffectSound, uid); component.EffectCooldown = _timing.CurTime + component.CooldownDuration; } @@ -105,38 +56,4 @@ private void AfterStorageClosed(EntityUid uid, CardboardBoxComponent component, _stealth.SetEnabled(uid, true, stealth); } } - - //Relay damage to the mover - private void OnDamage(EntityUid uid, CardboardBoxComponent component, DamageChangedEvent args) - { - if (args.DamageDelta != null && args.DamageIncreased) - { - _damageable.TryChangeDamage(component.Mover, args.DamageDelta, origin: args.Origin); - } - } - - private void OnEntInserted(EntityUid uid, CardboardBoxComponent component, EntInsertedIntoContainerMessage args) - { - if (!TryComp(args.Entity, out MobMoverComponent? mover)) - return; - - if (component.Mover == null) - { - _mover.SetRelay(args.Entity, uid); - component.Mover = args.Entity; - } - } - - /// - /// Through e.g. teleporting, it's possible for the mover to exit the box without opening it. - /// Handle those situations but don't play the sound. - /// - private void OnEntRemoved(EntityUid uid, CardboardBoxComponent component, EntRemovedFromContainerMessage args) - { - if (args.Entity != component.Mover) - return; - - RemComp(component.Mover.Value); - component.Mover = null; - } } diff --git a/Content.Server/Mech/Equipment/EntitySystems/MechGrabberSystem.cs b/Content.Server/Mech/Equipment/EntitySystems/MechGrabberSystem.cs index 08120f5c296..eb9d4b9e7d6 100644 --- a/Content.Server/Mech/Equipment/EntitySystems/MechGrabberSystem.cs +++ b/Content.Server/Mech/Equipment/EntitySystems/MechGrabberSystem.cs @@ -8,9 +8,9 @@ using Content.Shared.Mech.Components; using Content.Shared.Mech.Equipment.Components; using Content.Shared.Mobs.Components; +using Content.Shared.Vehicle; using Content.Shared.Wall; using Robust.Server.GameObjects; -using Robust.Shared.Audio; using Robust.Shared.Audio.Systems; using Robust.Shared.Containers; using Robust.Shared.Map; @@ -30,6 +30,7 @@ public sealed class MechGrabberSystem : EntitySystem [Dependency] private readonly InteractionSystem _interaction = default!; [Dependency] private readonly SharedAudioSystem _audio = default!; [Dependency] private readonly TransformSystem _transform = default!; + [Dependency] private readonly VehicleSystem _vehicle = default!; /// public override void Initialize() @@ -145,7 +146,10 @@ private void OnInteract(EntityUid uid, MechGrabberComponent component, UserActiv if (component.ItemContainer.ContainedEntities.Count >= component.MaxContents) return; - if (!TryComp(args.User, out var mech) || mech.PilotSlot.ContainedEntity == target) + if (_vehicle.GetOperatorOrNull(args.User) == target) + return; + + if (!TryComp(args.User, out var mech)) return; if (mech.Energy + component.GrabEnergyDelta < 0) diff --git a/Content.Server/Mech/Systems/MechEquipmentSystem.cs b/Content.Server/Mech/Systems/MechEquipmentSystem.cs index f9fe5e46413..d17b4cda076 100644 --- a/Content.Server/Mech/Systems/MechEquipmentSystem.cs +++ b/Content.Server/Mech/Systems/MechEquipmentSystem.cs @@ -3,6 +3,7 @@ using Content.Shared.Interaction; using Content.Shared.Mech.Components; using Content.Shared.Mech.Equipment.Components; +using Content.Shared.Vehicle; using Content.Shared.Whitelist; namespace Content.Server.Mech.Systems; @@ -16,6 +17,7 @@ public sealed class MechEquipmentSystem : EntitySystem [Dependency] private readonly SharedDoAfterSystem _doAfter = default!; [Dependency] private readonly PopupSystem _popup = default!; [Dependency] private readonly EntityWhitelistSystem _whitelistSystem = default!; + [Dependency] private readonly VehicleSystem _vehicle = default!; /// public override void Initialize() @@ -36,7 +38,7 @@ private void OnUsed(EntityUid uid, MechEquipmentComponent component, AfterIntera if (mechComp.Broken) return; - if (args.User == mechComp.PilotSlot.ContainedEntity) + if (args.User == _vehicle.GetOperatorOrNull(mech)) return; if (mechComp.EquipmentContainer.ContainedEntities.Count >= mechComp.MaxEquipmentAmount) diff --git a/Content.Server/Mech/Systems/MechSystem.cs b/Content.Server/Mech/Systems/MechSystem.cs index b7bbc4ad473..c12f58360eb 100644 --- a/Content.Server/Mech/Systems/MechSystem.cs +++ b/Content.Server/Mech/Systems/MechSystem.cs @@ -33,11 +33,9 @@ public sealed partial class MechSystem : SharedMechSystem [Dependency] private readonly AtmosphereSystem _atmosphere = default!; [Dependency] private readonly BatterySystem _battery = default!; [Dependency] private readonly ContainerSystem _container = default!; - [Dependency] private readonly DamageableSystem _damageable = default!; [Dependency] private readonly SharedDoAfterSystem _doAfter = default!; [Dependency] private readonly SharedPopupSystem _popup = default!; [Dependency] private readonly UserInterfaceSystem _ui = default!; - [Dependency] private readonly EntityWhitelistSystem _whitelistSystem = default!; [Dependency] private readonly SharedToolSystem _toolSystem = default!; /// @@ -197,7 +195,7 @@ private void OnAlternativeVerb(EntityUid uid, MechComponent component, GetVerbsE args.Verbs.Add(enterVerb); args.Verbs.Add(openUiVerb); } - else if (!IsEmpty(component)) + else if (Vehicle.HasOperator(uid)) { var ejectVerb = new AlternativeVerb { @@ -205,7 +203,7 @@ private void OnAlternativeVerb(EntityUid uid, MechComponent component, GetVerbsE Priority = 1, // Promote to top to make ejecting the ALT-click action Act = () => { - if (args.User == uid || args.User == component.PilotSlot.ContainedEntity) + if (args.User == uid || args.User == Vehicle.GetOperatorOrNull(uid)) { TryEject(uid, component); return; @@ -226,14 +224,13 @@ private void OnMechEntry(EntityUid uid, MechComponent component, MechEntryEvent if (args.Cancelled || args.Handled) return; - if (_whitelistSystem.IsWhitelistFail(component.PilotWhitelist, args.User)) + if (!Vehicle.CanOperate(uid, args.User)) { _popup.PopupEntity(Loc.GetString("mech-no-enter", ("item", uid)), args.User); return; } TryInsert(uid, args.Args.User, component); - _actionBlocker.UpdateCanMove(uid); args.Handled = true; } @@ -250,21 +247,13 @@ private void OnDamageChanged(EntityUid uid, MechComponent component, DamageChang { var integrity = component.MaxIntegrity - args.Damageable.TotalDamage; SetIntegrity(uid, integrity, component); - - if (args.DamageIncreased && - args.DamageDelta != null && - component.PilotSlot.ContainedEntity != null) - { - var damage = args.DamageDelta * component.MechToPilotDamageMultiplier; - _damageable.TryChangeDamage(component.PilotSlot.ContainedEntity, damage); - } } private void ToggleMechUi(EntityUid uid, MechComponent? component = null, EntityUid? user = null) { if (!Resolve(uid, ref component)) return; - user ??= component.PilotSlot.ContainedEntity; + user ??= Vehicle.GetOperatorOrNull(uid); if (user == null) return; diff --git a/Content.Server/Physics/Controllers/MoverController.cs b/Content.Server/Physics/Controllers/MoverController.cs index f927e717a9d..5c87de18638 100644 --- a/Content.Server/Physics/Controllers/MoverController.cs +++ b/Content.Server/Physics/Controllers/MoverController.cs @@ -2,10 +2,12 @@ using System.Runtime.CompilerServices; using Content.Server.Shuttles.Components; using Content.Server.Shuttles.Systems; +using Content.Shared.Friction; using Content.Shared.Movement.Components; using Content.Shared.Movement.Systems; using Content.Shared.Shuttles.Components; using Content.Shared.Shuttles.Systems; +using Prometheus; using Robust.Shared.Physics.Components; using Robust.Shared.Player; using DroneConsoleComponent = Content.Server.Shuttles.DroneConsoleComponent; @@ -16,6 +18,10 @@ namespace Content.Server.Physics.Controllers; public sealed class MoverController : SharedMoverController { + private static readonly Gauge ActiveMoverGauge = Metrics.CreateGauge( + "physics_active_mover_count", + "Active amount of InputMovers being processed by MoverController"); + [Dependency] private readonly ThrusterSystem _thruster = default!; [Dependency] private readonly SharedTransformSystem _xformSystem = default!; @@ -57,50 +63,47 @@ protected override bool CanSound() return true; } + private HashSet _moverAdded = new(); + private List> _movers = new(); + + private void InsertMover(Entity source) + { + if (TryComp(source, out MovementRelayTargetComponent? relay)) + { + if (TryComp(relay.Source, out InputMoverComponent? relayMover)) + { + InsertMover((relay.Source, relayMover)); + } + } + + // Already added + if (!_moverAdded.Add(source.Owner)) + return; + + _movers.Add(source); + } + public override void UpdateBeforeSolve(bool prediction, float frameTime) { base.UpdateBeforeSolve(prediction, frameTime); + _moverAdded.Clear(); + _movers.Clear(); var inputQueryEnumerator = AllEntityQuery(); + // Need to order mob movement so that movers don't run before their relays. while (inputQueryEnumerator.MoveNext(out var uid, out var mover)) { - var physicsUid = uid; - - if (RelayQuery.HasComponent(uid)) - continue; - - if (!XformQuery.TryGetComponent(uid, out var xform)) - { - continue; - } - - PhysicsComponent? body; - var xformMover = xform; - - if (mover.ToParent && RelayQuery.HasComponent(xform.ParentUid)) - { - if (!PhysicsQuery.TryGetComponent(xform.ParentUid, out body) || - !XformQuery.TryGetComponent(xform.ParentUid, out xformMover)) - { - continue; - } - - physicsUid = xform.ParentUid; - } - else if (!PhysicsQuery.TryGetComponent(uid, out body)) - { - continue; - } + InsertMover((uid, mover)); + } - HandleMobMovement(uid, - mover, - physicsUid, - body, - xformMover, - frameTime); + foreach (var mover in _movers) + { + HandleMobMovement(mover, frameTime); } + ActiveMoverGauge.Set(_movers.Count); + HandleShuttleMovement(frameTime); } @@ -314,6 +317,9 @@ private void HandleShuttleMovement(float frameTime) var linearInput = Vector2.Zero; var brakeInput = 0f; var angularInput = 0f; + var linearCount = 0; + var brakeCount = 0; + var angularCount = 0; foreach (var (pilotUid, pilot, _, consoleXform) in pilots) { @@ -322,24 +328,27 @@ private void HandleShuttleMovement(float frameTime) if (brakes > 0f) { brakeInput += brakes; + brakeCount++; } if (strafe.Length() > 0f) { var offsetRotation = consoleXform.LocalRotation; linearInput += offsetRotation.RotateVec(strafe); + linearCount++; } if (rotation != 0f) { angularInput += rotation; + angularCount++; } } - var count = pilots.Count; - linearInput /= count; - angularInput /= count; - brakeInput /= count; + // Don't slow down the shuttle if there's someone just looking at the console + linearInput /= Math.Max(1, linearCount); + angularInput /= Math.Max(1, angularCount); + brakeInput /= Math.Max(1, brakeCount); // Handle shuttle movement if (brakeInput > 0f) diff --git a/Content.Server/_Goobstation/Vehicles/VehicleSystem.cs b/Content.Server/_Goobstation/Vehicles/VehicleSystem.cs deleted file mode 100644 index 7eaab46432a..00000000000 --- a/Content.Server/_Goobstation/Vehicles/VehicleSystem.cs +++ /dev/null @@ -1,7 +0,0 @@ -using Content.Shared.Vehicles; - -namespace Content.Server.Vehicles; - -public sealed class VehicleSystem : SharedVehicleSystem -{ -} diff --git a/Content.Shared/ActionBlocker/ActionBlockerSystem.cs b/Content.Shared/ActionBlocker/ActionBlockerSystem.cs index dcf26223016..7055b844dec 100644 --- a/Content.Shared/ActionBlocker/ActionBlockerSystem.cs +++ b/Content.Shared/ActionBlocker/ActionBlockerSystem.cs @@ -60,7 +60,9 @@ public bool UpdateCanMove(EntityUid uid, InputMoverComponent? component = null) Dirty(uid, component); component.CanMove = !ev.Cancelled; - return !ev.Cancelled; + var updatedEv = new CanMoveUpdatedEvent(component.CanMove); + RaiseLocalEvent(uid, ref updatedEv); + return component.CanMove; } /// diff --git a/Content.Shared/Buckle/Components/StrapComponent.cs b/Content.Shared/Buckle/Components/StrapComponent.cs index afd5baa0c91..e1a009bd08e 100644 --- a/Content.Shared/Buckle/Components/StrapComponent.cs +++ b/Content.Shared/Buckle/Components/StrapComponent.cs @@ -90,6 +90,12 @@ public sealed partial class StrapComponent : Component /// [DataField] public bool BuckleOnInteractHand = true; + + /// + /// Whether being buckled to this entity should change the buckled ent's drawdepth. + /// + [DataField] + public bool ModifyBuckleDrawDepth = true; } public enum StrapPosition diff --git a/Content.Shared/Buckle/SharedBuckleSystem.Buckle.cs b/Content.Shared/Buckle/SharedBuckleSystem.Buckle.cs index 25b308e2697..ce976437ad6 100644 --- a/Content.Shared/Buckle/SharedBuckleSystem.Buckle.cs +++ b/Content.Shared/Buckle/SharedBuckleSystem.Buckle.cs @@ -7,6 +7,7 @@ using Content.Shared.DoAfter; using Content.Shared.Hands.Components; using Content.Shared.IdentityManagement; +using Content.Shared.Movement.Components; using Content.Shared.Movement.Events; using Content.Shared.Movement.Pulling.Events; using Content.Shared.Popups; @@ -177,6 +178,13 @@ private void OnBuckleThrowPushbackAttempt(EntityUid uid, BuckleComponent compone private void OnBuckleUpdateCanMove(EntityUid uid, BuckleComponent component, UpdateCanMoveEvent args) { + // If we're relaying then don't cancel. + // NOTE: I don't love this solution. It's by far the easiest but i hate having it be a consideration. + // We need to have a more logical way of distinguishing between a "physical" movement being blocked + // And simply being unable to move due to being unconscious, dead, etc. -EMO + if (HasComp(uid)) + return; + if (component.Buckled) args.Cancel(); } diff --git a/Content.Shared/CCVar/CCVars.Movement.cs b/Content.Shared/CCVar/CCVars.Movement.cs new file mode 100644 index 00000000000..96ceada0994 --- /dev/null +++ b/Content.Shared/CCVar/CCVars.Movement.cs @@ -0,0 +1,67 @@ +using Content.Shared.Administration; +using Content.Shared.CCVar.CVarAccess; +using Robust.Shared.Configuration; + +namespace Content.Shared.CCVar; + +public sealed partial class CCVars +{ + /// + /// Is mob pushing enabled. + /// + [CVarControl(AdminFlags.VarEdit)] + public static readonly CVarDef MovementMobPushing = + CVarDef.Create("movement.mob_pushing", false, CVar.SERVER | CVar.REPLICATED); + + /// + /// Can we push mobs not moving. + /// + [CVarControl(AdminFlags.VarEdit)] + public static readonly CVarDef MovementPushingStatic = + CVarDef.Create("movement.pushing_static", true, CVar.SERVER | CVar.REPLICATED); + + /// + /// Dot product for the pushed entity's velocity to a target entity's velocity before it gets moved. + /// + [CVarControl(AdminFlags.VarEdit)] + public static readonly CVarDef MovementPushingVelocityProduct = + CVarDef.Create("movement.pushing_velocity_product", -9999f, CVar.SERVER | CVar.REPLICATED); + + /// + /// Cap for how much an entity can be pushed per second. + /// + [CVarControl(AdminFlags.VarEdit)] + public static readonly CVarDef MovementPushingCap = + CVarDef.Create("movement.pushing_cap", 25f, CVar.SERVER | CVar.REPLICATED); + + /// + /// Minimum pushing impulse per tick. If the value is below this it rounds to 0. + /// This is an optimisation to avoid pushing small values that won't actually move the mobs. + /// + [CVarControl(AdminFlags.VarEdit)] + public static readonly CVarDef MovementMinimumPush = + CVarDef.Create("movement.minimum_push", 0f, CVar.SERVER | CVar.REPLICATED); + + // Really this just exists because hot reloading is cooked on rider. + /// + /// Penetration depth cap for considering mob collisions. + /// + [CVarControl(AdminFlags.VarEdit)] + public static readonly CVarDef MovementPenetrationCap = + CVarDef.Create("movement.penetration_cap", 0.5f, CVar.SERVER | CVar.REPLICATED); + + /// + /// Based on the mass difference multiplies the push amount by this proportionally. + /// + [CVarControl(AdminFlags.VarEdit)] + public static readonly CVarDef MovementPushMassCap = + CVarDef.Create("movement.push_mass_cap", 1.75f, CVar.SERVER | CVar.REPLICATED); + + /// + /// Is crawling enabled + /// + [CVarControl(AdminFlags.VarEdit)] + public static readonly CVarDef MovementCrawling = + CVarDef.Create("movement.crawling", true, CVar.SERVER | CVar.REPLICATED); + +} diff --git a/Content.Shared/CCVar/CCVars.Physics.cs b/Content.Shared/CCVar/CCVars.Physics.cs index 379676b5df9..f67a4203bca 100644 --- a/Content.Shared/CCVar/CCVars.Physics.cs +++ b/Content.Shared/CCVar/CCVars.Physics.cs @@ -10,6 +10,15 @@ public sealed partial class CCVars public static readonly CVarDef RelativeMovement = CVarDef.Create("physics.relative_movement", true, CVar.ARCHIVE | CVar.REPLICATED | CVar.SERVER); + public static readonly CVarDef MinFriction = + CVarDef.Create("physics.min_friction", 0.0f, CVar.ARCHIVE | CVar.REPLICATED | CVar.SERVER); + + public static readonly CVarDef AirFriction = + CVarDef.Create("physics.air_friction", 0.2f, CVar.ARCHIVE | CVar.REPLICATED | CVar.SERVER); + + public static readonly CVarDef OffgridFriction = + CVarDef.Create("physics.offgrid_friction", 0.05f, CVar.ARCHIVE | CVar.REPLICATED | CVar.SERVER); + public static readonly CVarDef TileFrictionModifier = CVarDef.Create("physics.tile_friction", 40.0f, CVar.ARCHIVE | CVar.REPLICATED | CVar.SERVER); diff --git a/Content.Shared/CCVar/CVarAccess/CVarControl.cs b/Content.Shared/CCVar/CVarAccess/CVarControl.cs new file mode 100644 index 00000000000..799738cf3d8 --- /dev/null +++ b/Content.Shared/CCVar/CVarAccess/CVarControl.cs @@ -0,0 +1,38 @@ +using Content.Shared.Administration; +using Robust.Shared.Reflection; + +namespace Content.Shared.CCVar.CVarAccess; + +/// +/// Manages what admin flags can change the cvar value. With optional mins and maxes. +/// +[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property)] +[Reflect(discoverable: true)] +public sealed class CVarControl : Attribute +{ + public AdminFlags AdminFlags { get; } + public object? Min { get; } + public object? Max { get; } + + public CVarControl(AdminFlags adminFlags, object? min = null, object? max = null, string? helpText = null) + { + AdminFlags = adminFlags; + Min = min; + Max = max; + + // Not actually sure if its a good idea to throw exceptions in attributes. + + if (min != null && max != null) + { + if (min.GetType() != max.GetType()) + { + throw new ArgumentException("Min and max must be of the same type."); + } + } + + if (min == null && max != null || min != null && max == null) + { + throw new ArgumentException("Min and max must both be null or both be set."); + } + } +} diff --git a/Content.Shared/CardboardBox/Components/CardboardBoxComponent.cs b/Content.Shared/CardboardBox/Components/CardboardBoxComponent.cs index 0e607f50769..e681ee65bfd 100644 --- a/Content.Shared/CardboardBox/Components/CardboardBoxComponent.cs +++ b/Content.Shared/CardboardBox/Components/CardboardBoxComponent.cs @@ -11,12 +11,6 @@ namespace Content.Shared.CardboardBox.Components; [RegisterComponent, NetworkedComponent] public sealed partial class CardboardBoxComponent : Component { - /// - /// The person in control of this box - /// - [DataField("mover")] - public EntityUid? Mover; - /// /// The entity used for the box opening effect /// diff --git a/Content.Shared/Damage/DamageSpecifier.cs b/Content.Shared/Damage/DamageSpecifier.cs index 7f505b807f7..a67e7714d5c 100644 --- a/Content.Shared/Damage/DamageSpecifier.cs +++ b/Content.Shared/Damage/DamageSpecifier.cs @@ -183,6 +183,22 @@ public static DamageSpecifier ApplyModifierSets(DamageSpecifier damageSpec, IEnu return newDamage; } + /// + /// Returns a new DamageSpecifier that only contains the entries with positive value. + /// + public static DamageSpecifier GetPositive(DamageSpecifier damageSpec) + { + DamageSpecifier newDamage = new(); + + foreach (var (key, value) in damageSpec.DamageDict) + { + if (value > 0) + newDamage.DamageDict[key] = value; + } + + return newDamage; + } + /// /// Remove any damage entries with zero damage. /// diff --git a/Content.Shared/Mech/Components/MechComponent.cs b/Content.Shared/Mech/Components/MechComponent.cs index 6ebfde5f999..a21c71f5858 100644 --- a/Content.Shared/Mech/Components/MechComponent.cs +++ b/Content.Shared/Mech/Components/MechComponent.cs @@ -55,13 +55,6 @@ public sealed partial class MechComponent : Component [ViewVariables] public readonly string BatterySlotId = "mech-battery-slot"; - /// - /// A multiplier used to calculate how much of the damage done to a mech - /// is transfered to the pilot - /// - [DataField, ViewVariables(VVAccess.ReadWrite)] - public float MechToPilotDamageMultiplier; - /// /// Whether the mech has been destroyed and is no longer pilotable. /// @@ -96,9 +89,6 @@ public sealed partial class MechComponent : Component [DataField] public EntityWhitelist? EquipmentWhitelist; - [DataField] - public EntityWhitelist? PilotWhitelist; - /// /// A container for storing the equipment entities. /// diff --git a/Content.Shared/Mech/EntitySystems/SharedMechSystem.Relay.cs b/Content.Shared/Mech/EntitySystems/SharedMechSystem.Relay.cs new file mode 100644 index 00000000000..c4b81871e02 --- /dev/null +++ b/Content.Shared/Mech/EntitySystems/SharedMechSystem.Relay.cs @@ -0,0 +1,40 @@ +using Content.Shared.Interaction.Events; +using Content.Shared.Mech.Components; + +namespace Content.Shared.Mech.EntitySystems; + +public abstract partial class SharedMechSystem +{ + private void InitializeRelay() + { + SubscribeLocalEvent(RelayRefToPilot); + } + + private void RelayToPilot(Entity uid, T args) where T : class + { + if (!Vehicle.TryGetOperator(uid.Owner, out var operatorEnt)) + return; + + var ev = new MechPilotRelayedEvent(args); + + RaiseLocalEvent(operatorEnt.Value, ref ev); + } + + private void RelayRefToPilot(Entity uid, ref T args) where T :struct + { + if (!Vehicle.TryGetOperator(uid.Owner, out var operatorEnt)) + return; + + var ev = new MechPilotRelayedEvent(args); + + RaiseLocalEvent(operatorEnt.Value, ref ev); + + args = ev.Args; + } +} + +[ByRefEvent] +public record struct MechPilotRelayedEvent(TEvent Args) +{ + public TEvent Args = Args; +} diff --git a/Content.Shared/Mech/EntitySystems/SharedMechSystem.cs b/Content.Shared/Mech/EntitySystems/SharedMechSystem.cs index f977a2eeb4e..8e0840df5a7 100644 --- a/Content.Shared/Mech/EntitySystems/SharedMechSystem.cs +++ b/Content.Shared/Mech/EntitySystems/SharedMechSystem.cs @@ -1,5 +1,4 @@ using System.Linq; -using Content.Shared.Access.Components; using Content.Shared.ActionBlocker; using Content.Shared.Actions; using Content.Shared.Destructible; @@ -11,9 +10,9 @@ using Content.Shared.Interaction.Events; using Content.Shared.Mech.Components; using Content.Shared.Mech.Equipment.Components; -using Content.Shared.Movement.Components; -using Content.Shared.Movement.Systems; using Content.Shared.Popups; +using Content.Shared.Vehicle; +using Content.Shared.Vehicle.Components; using Content.Shared.Weapons.Melee; using Content.Shared.Whitelist; using Robust.Shared.Containers; @@ -37,7 +36,7 @@ namespace Content.Shared.Mech.EntitySystems; /// /// Handles all of the interactions, UI handling, and items shennanigans for /// -public abstract class SharedMechSystem : EntitySystem +public abstract partial class SharedMechSystem : EntitySystem { [Dependency] private readonly IGameTiming _timing = default!; [Dependency] private readonly INetManager _net = default!; @@ -46,9 +45,9 @@ public abstract class SharedMechSystem : EntitySystem [Dependency] private readonly SharedAppearanceSystem _appearance = default!; [Dependency] private readonly SharedContainerSystem _container = default!; [Dependency] private readonly SharedInteractionSystem _interaction = default!; - [Dependency] private readonly SharedMoverController _mover = default!; [Dependency] private readonly SharedPopupSystem _popup = default!; [Dependency] private readonly SharedDoAfterSystem _doAfter = default!; + [Dependency] protected readonly VehicleSystem Vehicle = default!; [Dependency] private readonly EntityWhitelistSystem _whitelistSystem = default!; [Dependency] private readonly SharedHandsSystem _hands = default!; // Goobstation Change [Dependency] private readonly SharedVirtualItemSystem _virtualItem = default!; // Goobstation Change @@ -56,7 +55,7 @@ public abstract class SharedMechSystem : EntitySystem // Goobstation: Local variable for checking if mech guns can be used out of them. private bool _canUseMechGunOutside; - + /// public override void Initialize() { @@ -65,10 +64,9 @@ public override void Initialize() SubscribeLocalEvent(RelayInteractionEvent); SubscribeLocalEvent(OnStartup); SubscribeLocalEvent(OnDestruction); - SubscribeLocalEvent(OnGetAdditionalAccess); SubscribeLocalEvent(OnDragDrop); SubscribeLocalEvent(OnCanDragDrop); - SubscribeLocalEvent(OnEmagged); + SubscribeLocalEvent(OnOperatorSet); SubscribeLocalEvent(OnGetMeleeWeapon); SubscribeLocalEvent(OnCanAttackFromContainer); @@ -102,8 +100,7 @@ private void OnEjectPilotEvent(EntityUid uid, MechComponent component, MechEject private void RelayInteractionEvent(EntityUid uid, MechComponent component, UserActivateInWorldEvent args) { - var pilot = component.PilotSlot.ContainedEntity; - if (pilot == null) + if (!Vehicle.HasOperator(uid)) return; // TODO why is this being blocked? @@ -129,15 +126,6 @@ private void OnDestruction(EntityUid uid, MechComponent component, DestructionEv BreakMech(uid, component); } - private void OnGetAdditionalAccess(EntityUid uid, MechComponent component, ref GetAdditionalAccessEvent args) - { - var pilot = component.PilotSlot.ContainedEntity; - if (pilot == null) - return; - - args.Entities.Add(pilot.Value); - } - private void SetupUser(EntityUid mech, EntityUid pilot, MechComponent? component = null) { if (!Resolve(mech, ref component)) @@ -148,7 +136,6 @@ private void SetupUser(EntityUid mech, EntityUid pilot, MechComponent? component // Warning: this bypasses most normal interaction blocking components on the user, like drone laws and the like. var irelay = EnsureComp(pilot); - _mover.SetRelay(pilot, mech); _interaction.SetRelay(pilot, mech, irelay); rider.Mech = mech; Dirty(pilot, rider); @@ -166,7 +153,6 @@ private void RemoveUser(EntityUid mech, EntityUid pilot) { if (!RemComp(pilot)) return; - RemComp(pilot); RemComp(pilot); _actions.RemoveProvidedActions(pilot, mech); @@ -340,16 +326,6 @@ public void SetIntegrity(EntityUid uid, FixedPoint2 value, MechComponent? compon UpdateUserInterface(uid, component); } - /// - /// Checks if the pilot is present - /// - /// - /// Whether or not the pilot is present - public bool IsEmpty(MechComponent component) - { - return component.PilotSlot.ContainedEntity == null; - } - /// /// Checks if an entity can be inserted into the mech. /// @@ -362,7 +338,16 @@ public bool CanInsert(EntityUid uid, EntityUid toInsert, MechComponent? componen if (!Resolve(uid, ref component)) return false; - return IsEmpty(component) && _actionBlocker.CanMove(toInsert); + if (!_actionBlocker.CanMove(toInsert)) + return false; + + if (Vehicle.GetOperatorOrNull(uid) == toInsert) + return false; + + if (!_container.CanInsert(toInsert, component.PilotSlot)) + return false; + + return true; } /// @@ -382,21 +367,15 @@ public virtual void UpdateUserInterface(EntityUid uid, MechComponent? component /// /// /// Whether or not the entity was inserted - public bool TryInsert(EntityUid uid, EntityUid? toInsert, MechComponent? component = null) + public bool TryInsert(EntityUid uid, EntityUid toInsert, MechComponent? component = null) { if (!Resolve(uid, ref component)) return false; - if (toInsert == null || component.PilotSlot.ContainedEntity == toInsert) - return false; - - if (!CanInsert(uid, toInsert.Value, component)) + if (!CanInsert(uid, toInsert, component)) return false; - SetupUser(uid, toInsert.Value); - _container.Insert(toInsert.Value, component.PilotSlot); - UpdateAppearance(uid, component); - UpdateHands(toInsert.Value, uid, true); // Goobstation + _container.Insert(toInsert, component.PilotSlot); return true; } @@ -412,16 +391,10 @@ public bool TryEject(EntityUid uid, MechComponent? component = null, EntityUid? if (!Resolve(uid, ref component)) return false; - if (component.PilotSlot.ContainedEntity != null) - pilot = component.PilotSlot.ContainedEntity.Value; - - if (pilot == null) + if (!Vehicle.TryGetOperator(uid, out var operatorEnt)) return false; - RemoveUser(uid, pilot.Value); - _container.RemoveEntity(uid, pilot.Value); - UpdateAppearance(uid, component); - UpdateHands(pilot.Value, uid, false); // Goobstation + _container.RemoveEntity(uid, operatorEnt.Value); return true; } @@ -515,7 +488,7 @@ private void UpdateAppearance(EntityUid uid, MechComponent? component = null, if (!Resolve(uid, ref component, ref appearance, false)) return; - _appearance.SetData(uid, MechVisuals.Open, IsEmpty(component), appearance); + _appearance.SetData(uid, MechVisuals.Open, !Vehicle.HasOperator(uid), appearance); _appearance.SetData(uid, MechVisuals.Broken, component.Broken, appearance); } @@ -541,13 +514,19 @@ private void OnCanDragDrop(EntityUid uid, MechComponent component, ref CanDropTa args.CanDrop |= !component.Broken && CanInsert(uid, args.Dragged, component); } - private void OnEmagged(EntityUid uid, MechComponent component, ref GotEmaggedEvent args) // Goobstation + private void OnOperatorSet(Entity ent, ref VehicleOperatorSetEvent args) { - if (!component.BreakOnEmag) - return; - args.Handled = true; - component.EquipmentWhitelist = null; - Dirty(uid, component); + if (args.OldOperator is { } oldOperator) + { + RemoveUser(ent, oldOperator); + } + + if (args.NewOperator is { } newOperator) + { + SetupUser(ent, newOperator, ent); + } + + UpdateAppearance(ent); } } diff --git a/Content.Shared/Movement/Components/InputMoverComponent.cs b/Content.Shared/Movement/Components/InputMoverComponent.cs index 40cb532e60a..a5d16f899ed 100644 --- a/Content.Shared/Movement/Components/InputMoverComponent.cs +++ b/Content.Shared/Movement/Components/InputMoverComponent.cs @@ -1,13 +1,9 @@ using System.Numerics; -using Content.Shared.Alert; -using Content.Shared.CCVar; using Content.Shared.Movement.Systems; -using Robust.Shared.Configuration; using Robust.Shared.GameStates; using Robust.Shared.Serialization; using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom; using Robust.Shared.Timing; -using Robust.Shared.Prototypes; namespace Content.Shared.Movement.Components { @@ -33,20 +29,31 @@ public sealed partial class InputMoverComponent : Component // (well maybe we do but the code is designed such that MoverSystem applies movement speed) // (and I'm not changing that) - /// - /// Should our velocity be applied to our parent? - /// - [ViewVariables(VVAccess.ReadWrite), DataField("toParent")] - public bool ToParent = false; - public GameTick LastInputTick; public ushort LastInputSubTick; public Vector2 CurTickWalkMovement; public Vector2 CurTickSprintMovement; + [ViewVariables] public MoveButtons HeldMoveButtons = MoveButtons.None; + /// + /// Does our input indicate actual movement, and not just modifiers? + /// + /// + /// This can be useful to filter out input from just pressing the walk button with no directions, for example. + /// + [ViewVariables] + public bool HasDirectionalMovement => (HeldMoveButtons & MoveButtons.AnyDirection) != MoveButtons.None; + + // I don't know if we even need this networked? It's mostly so conveyors can calculate properly. + /// + /// Direction to move this tick. + /// + [ViewVariables] + public Vector2 WishDir; + /// /// Entity our movement is relative to. /// @@ -69,22 +76,15 @@ public sealed partial class InputMoverComponent : Component /// If we traverse on / off a grid then set a timer to update our relative inputs. /// [DataField(customTypeSerializer: typeof(TimeOffsetSerializer))] - [ViewVariables(VVAccess.ReadWrite)] public TimeSpan LerpTarget; public const float LerpTime = 1.0f; - public bool Sprinting => DefaultSprinting - ? (HeldMoveButtons & MoveButtons.Walk) != 0x0 - : (HeldMoveButtons & MoveButtons.Walk) == 0x0; - - public bool DefaultSprinting = true; + [ViewVariables] + public bool Sprinting => (HeldMoveButtons & MoveButtons.Walk) == 0x0; [ViewVariables(VVAccess.ReadWrite)] public bool CanMove = true; - - [DataField] - public ProtoId WalkingAlert = "Walking"; } [Serializable, NetSerializable] @@ -95,6 +95,6 @@ public sealed class InputMoverComponentState : ComponentState public Angle TargetRelativeRotation; public Angle RelativeRotation; public TimeSpan LerpTarget; - public bool CanMove, DefaultSprinting; + public bool CanMove; } } diff --git a/Content.Shared/Movement/Components/MobCollisionComponent.cs b/Content.Shared/Movement/Components/MobCollisionComponent.cs new file mode 100644 index 00000000000..437cdfd409d --- /dev/null +++ b/Content.Shared/Movement/Components/MobCollisionComponent.cs @@ -0,0 +1,60 @@ +using System.Numerics; +using Content.Shared.Movement.Systems; +using Robust.Shared.GameStates; + +namespace Content.Shared.Movement.Components; + +/// +/// Handles mobs pushing against each other. +/// +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState(fieldDeltas: true)] +public sealed partial class MobCollisionComponent : Component +{ + // If you want to tweak the feel of the pushing use SpeedModifier and Strength. + // Strength goes both ways and affects how much the other mob is pushed by so controls static pushing a lot. + // Speed mod affects your own mob primarily. + + /// + /// Is this mob currently colliding? Used for SpeedModifier. + /// + [DataField, AutoNetworkedField] + public bool Colliding; + + // TODO: I hate this but also I couldn't quite figure out a way to avoid having to dirty it every tick. + // The issue is it's a time target that changes constantly so we can't just use a timespan. + // However that doesn't mean it should be modified every tick if we're still colliding. + + /// + /// Buffer time for to keep applying after the entities are no longer colliding. + /// Without this you will get jittering unless you are very specific with your values. + /// + [DataField, AutoNetworkedField] + public float BufferAccumulator = SharedMobCollisionSystem.BufferTime; + + /// + /// The speed modifier for mobs currently pushing. + /// By setting this low you can ensure you don't have to set the push-strength too high if you can push static entities. + /// + [DataField, AutoNetworkedField] + public float SpeedModifier = 1f; + + [DataField, AutoNetworkedField] + public float MinimumSpeedModifier = 0.35f; + + /// + /// Strength of the pushback for entities. This is combined between the 2 entities being pushed. + /// + [DataField, AutoNetworkedField] + public float Strength = 50f; + + // Yes I know, I will deal with it if I ever refactor collision layers due to misuse. + // If anything it probably needs some assurance on mobcollisionsystem for it. + /// + /// Fixture to listen to for mob collisions. + /// + [DataField, AutoNetworkedField] + public string FixtureId = "flammable"; + + [DataField, AutoNetworkedField] + public Vector2 Direction; +} diff --git a/Content.Shared/Movement/Components/MovementSpeedModifierComponent.cs b/Content.Shared/Movement/Components/MovementSpeedModifierComponent.cs index 88067f54aa4..1bb1f8a841c 100644 --- a/Content.Shared/Movement/Components/MovementSpeedModifierComponent.cs +++ b/Content.Shared/Movement/Components/MovementSpeedModifierComponent.cs @@ -119,5 +119,22 @@ private float _baseSprintSpeedVV public float CurrentWalkSpeed => WalkSpeedModifier * BaseWalkSpeed; [ViewVariables] public float CurrentSprintSpeed => SprintSpeedModifier * BaseSprintSpeed; + + /// + /// The body's base friction modifier that is applied in *all* circumstances. + /// + [AutoNetworkedField, DataField] + public float BaseFriction = DefaultFriction; + + /// + /// These base values should be defined in yaml and rarely if ever modified directly. + /// + [AutoNetworkedField, DataField] + public float BaseWeightlessFriction = DefaultWeightlessFriction; + + [ViewVariables] + public float WeightlessWalkSpeed => WeightlessModifier * BaseWalkSpeed; + [ViewVariables] + public float WeightlessSprintSpeed => WeightlessModifier * BaseSprintSpeed; } } diff --git a/Content.Shared/Movement/Events/MoveInputEvent.cs b/Content.Shared/Movement/Events/MoveInputEvent.cs index 08cb6a4b9f1..9c49da722cb 100644 --- a/Content.Shared/Movement/Events/MoveInputEvent.cs +++ b/Content.Shared/Movement/Events/MoveInputEvent.cs @@ -11,17 +11,12 @@ public readonly struct MoveInputEvent { public readonly Entity Entity; public readonly MoveButtons OldMovement; - public readonly Direction Dir; // Shitmed Change - public readonly bool State; // Shitmed Change public bool HasDirectionalMovement => (Entity.Comp.HeldMoveButtons & MoveButtons.AnyDirection) != MoveButtons.None; - public MoveInputEvent(Entity entity, MoveButtons oldMovement, Direction dir, bool state) // Shitmed Change + public MoveInputEvent(Entity entity, MoveButtons oldMovement) { Entity = entity; OldMovement = oldMovement; - // Shitmed Change - Dir = dir; - State = state; } } diff --git a/Content.Shared/Movement/Events/UpdateCanMoveEvent.cs b/Content.Shared/Movement/Events/UpdateCanMoveEvent.cs index d9772c1cf4c..257bb0c3932 100644 --- a/Content.Shared/Movement/Events/UpdateCanMoveEvent.cs +++ b/Content.Shared/Movement/Events/UpdateCanMoveEvent.cs @@ -15,3 +15,9 @@ public UpdateCanMoveEvent(EntityUid uid) public EntityUid Uid { get; } } + +/// +/// Event raised directed on an entity when their value of is updated. +/// +[ByRefEvent] +public readonly record struct CanMoveUpdatedEvent(bool CanMove); diff --git a/Content.Shared/Movement/Systems/SharedMobCollisionSystem.cs b/Content.Shared/Movement/Systems/SharedMobCollisionSystem.cs new file mode 100644 index 00000000000..fab9552271b --- /dev/null +++ b/Content.Shared/Movement/Systems/SharedMobCollisionSystem.cs @@ -0,0 +1,339 @@ +using System.Numerics; +using Content.Shared.CCVar; +using Content.Shared.Movement.Components; +using Robust.Shared; +using Robust.Shared.Configuration; +using Robust.Shared.Physics; +using Robust.Shared.Physics.Components; +using Robust.Shared.Physics.Systems; +using Robust.Shared.Random; +using Robust.Shared.Serialization; +using Robust.Shared.Utility; + +namespace Content.Shared.Movement.Systems; + +public abstract class SharedMobCollisionSystem : EntitySystem +{ + [Dependency] protected readonly IConfigurationManager CfgManager = default!; + [Dependency] private readonly IRobustRandom _random = default!; + [Dependency] private readonly MovementSpeedModifierSystem _moveMod = default!; + [Dependency] protected readonly SharedPhysicsSystem Physics = default!; + [Dependency] private readonly SharedTransformSystem _xformSystem = default!; + + protected EntityQuery MobQuery; + protected EntityQuery PhysicsQuery; + + /// + /// + /// + private float _pushingCap; + + /// + /// + /// + private float _pushingDotProduct; + + /// + /// + /// + private float _minimumPushSquared = 0.01f; + + private float _penCap; + + /// + /// Time after we stop colliding with another mob before adjusting the movespeedmodifier. + /// This is required so if we stop colliding for a frame we don't fully reset and get jerky movement. + /// + public const float BufferTime = 0.2f; + + private float _massDiffCap; + + public override void Initialize() + { + base.Initialize(); + + UpdatePushCap(); + Subs.CVar(CfgManager, CVars.NetTickrate, _ => UpdatePushCap()); + Subs.CVar(CfgManager, CCVars.MovementMinimumPush, val => _minimumPushSquared = val * val, true); + Subs.CVar(CfgManager, CCVars.MovementPenetrationCap, val => _penCap = val, true); + Subs.CVar(CfgManager, CCVars.MovementPushingCap, _ => UpdatePushCap()); + Subs.CVar(CfgManager, CCVars.MovementPushingVelocityProduct, + value => + { + _pushingDotProduct = value; + }, true); + Subs.CVar(CfgManager, CCVars.MovementPushMassCap, val => _massDiffCap = val, true); + + MobQuery = GetEntityQuery(); + PhysicsQuery = GetEntityQuery(); + SubscribeAllEvent(OnCollision); + SubscribeLocalEvent(OnMoveModifier); + + UpdatesBefore.Add(typeof(SharedPhysicsSystem)); + } + + private void UpdatePushCap() + { + _pushingCap = (1f / CfgManager.GetCVar(CVars.NetTickrate)) * CfgManager.GetCVar(CCVars.MovementPushingCap); + } + + public override void Update(float frameTime) + { + base.Update(frameTime); + + var query = AllEntityQuery(); + + while (query.MoveNext(out var uid, out var comp)) + { + if (!comp.Colliding) + continue; + + comp.BufferAccumulator -= frameTime; + DirtyField(uid, comp, nameof(MobCollisionComponent.BufferAccumulator)); + var direction = comp.Direction; + + if (comp.BufferAccumulator <= 0f) + { + SetColliding((uid, comp), false, 1f); + } + // Apply the mob collision; if it's too low ignore it (e.g. if mob friction would overcome it). + // This is so we don't spam velocity changes every tick. It's not that expensive for physics but + // avoids the networking side. + else if (direction != Vector2.Zero && PhysicsQuery.TryComp(uid, out var physics)) + { + DebugTools.Assert(direction.LengthSquared() >= _minimumPushSquared); + + if (direction.Length() > _pushingCap) + { + direction = direction.Normalized() * _pushingCap; + } + + Physics.ApplyLinearImpulse(uid, direction * physics.Mass, body: physics); + comp.Direction = Vector2.Zero; + DirtyField(uid, comp, nameof(MobCollisionComponent.Direction)); + } + } + } + + private void OnMoveModifier(Entity ent, ref RefreshMovementSpeedModifiersEvent args) + { + if (!ent.Comp.Colliding) + return; + + args.ModifySpeed(ent.Comp.SpeedModifier); + } + + private void SetColliding(Entity entity, bool value, float speedMod) + { + if (value) + { + entity.Comp.BufferAccumulator = BufferTime; + DirtyField(entity.Owner, entity.Comp, nameof(MobCollisionComponent.BufferAccumulator)); + } + else + { + DebugTools.Assert(speedMod.Equals(1f)); + } + + if (entity.Comp.Colliding != value) + { + entity.Comp.Colliding = value; + DirtyField(entity.Owner, entity.Comp, nameof(MobCollisionComponent.Colliding)); + } + + if (!entity.Comp.SpeedModifier.Equals(speedMod)) + { + entity.Comp.SpeedModifier = speedMod; + _moveMod.RefreshMovementSpeedModifiers(entity.Owner); + DirtyField(entity.Owner, entity.Comp, nameof(MobCollisionComponent.SpeedModifier)); + } + } + + private void OnCollision(MobCollisionMessage msg, EntitySessionEventArgs args) + { + var player = args.SenderSession.AttachedEntity; + + if (!MobQuery.TryComp(player, out var comp)) + return; + + var xform = Transform(player.Value); + + // If not parented directly to a grid then fail it. + if (xform.ParentUid != xform.GridUid && xform.ParentUid != xform.MapUid) + return; + + var direction = msg.Direction; + + MoveMob((player.Value, comp, xform), direction, msg.SpeedModifier); + } + + protected void MoveMob(Entity entity, Vector2 direction, float speedMod) + { + // Length too short to do anything. + var pushing = true; + + if (direction.LengthSquared() < _minimumPushSquared) + { + pushing = false; + direction = Vector2.Zero; + speedMod = 1f; + } + else if (float.IsNaN(direction.X) || float.IsNaN(direction.Y)) + { + direction = Vector2.Zero; + } + + speedMod = Math.Clamp(speedMod, 0f, 1f); + + SetColliding(entity, pushing, speedMod); + + if (direction == entity.Comp1.Direction) + return; + + entity.Comp1.Direction = direction; + DirtyField(entity.Owner, entity.Comp1, nameof(MobCollisionComponent.Direction)); + } + + protected bool HandleCollisions(Entity entity, float frameTime) + { + var physics = entity.Comp2; + + if (physics.ContactCount == 0) + return false; + + var ourVelocity = entity.Comp2.LinearVelocity; + + if (ourVelocity == Vector2.Zero && !CfgManager.GetCVar(CCVars.MovementPushingStatic)) + return false; + + var xform = Transform(entity.Owner); + + if (xform.ParentUid != xform.GridUid && xform.ParentUid != xform.MapUid) + return false; + + var ev = new AttemptMobCollideEvent(); + + RaiseLocalEvent(entity.Owner, ref ev); + + if (ev.Cancelled) + return false; + + var (worldPos, worldRot) = _xformSystem.GetWorldPositionRotation(xform); + var ourTransform = new Transform(worldPos, worldRot); + var contacts = Physics.GetContacts(entity.Owner); + var direction = Vector2.Zero; + var contactCount = 0; + var ourMass = physics.FixturesMass; + var speedMod = 1f; + + while (contacts.MoveNext(out var contact)) + { + if (!contact.IsTouching) + continue; + + var ourFixture = contact.OurFixture(entity.Owner); + + if (ourFixture.Id != entity.Comp1.FixtureId) + continue; + + var other = contact.OtherEnt(entity.Owner); + + if (!MobQuery.TryComp(other, out var otherComp) || !PhysicsQuery.TryComp(other, out var otherPhysics)) + continue; + + var velocityProduct = Vector2.Dot(ourVelocity, otherPhysics.LinearVelocity); + + // If we're moving opposite directions for example then ignore (based on cvar). + if (velocityProduct < _pushingDotProduct) + { + continue; + } + + var targetEv = new AttemptMobTargetCollideEvent(); + RaiseLocalEvent(other, ref targetEv); + + if (targetEv.Cancelled) + continue; + + // TODO: More robust overlap detection. + var otherTransform = Physics.GetPhysicsTransform(other); + var diff = ourTransform.Position - otherTransform.Position; + + if (diff == Vector2.Zero) + { + diff = _random.NextVector2(0.01f); + } + + // 0.7 for 0.35 + 0.35 for mob bounds (see TODO above). + // Clamp so we don't get a heap of penetration depth and suddenly lurch other mobs. + // This is also so we don't have to trigger the speed-cap above. + // Maybe we just do speedcap and dump this? Though it's less configurable and the cap is just there for cheaters. + var penDepth = Math.Clamp(0.7f - diff.Length(), 0f, _penCap); + + // Sum the strengths so we get pushes back the same amount (impulse-wise, ignoring prediction). + var mobMovement = penDepth * diff.Normalized() * (entity.Comp1.Strength + otherComp.Strength); + + // Big mob push smaller mob, needs fine-tuning and potentially another co-efficient. + if (_massDiffCap > 0f) + { + var modifier = Math.Clamp( + otherPhysics.FixturesMass / ourMass, + 1f / _massDiffCap, + _massDiffCap); + + mobMovement *= modifier; + + var speedReduction = 1f - entity.Comp1.MinimumSpeedModifier; + speedReduction /= _penCap / penDepth; + var speedModifier = Math.Clamp( + 1f - speedReduction * modifier, + entity.Comp1.MinimumSpeedModifier, 1f); + + speedMod = MathF.Min(speedModifier, 1f); + } + + // Need the push strength proportional to penetration depth. + direction += mobMovement; + contactCount++; + } + + if (direction == Vector2.Zero) + { + return contactCount > 0; + } + + direction *= frameTime; + RaiseCollisionEvent(entity.Owner, direction, speedMod); + return true; + } + + protected abstract void RaiseCollisionEvent(EntityUid uid, Vector2 direction, float speedmodifier); + + /// + /// Raised from client -> server indicating mob push direction OR server -> server for NPC mob pushes. + /// + [Serializable, NetSerializable] + protected sealed class MobCollisionMessage : EntityEventArgs + { + public Vector2 Direction; + public float SpeedModifier; + } +} + +/// +/// Raised on the entity itself when attempting to handle mob collisions. +/// +[ByRefEvent] +public record struct AttemptMobCollideEvent +{ + public bool Cancelled; +} + +/// +/// Raised on the other entity when attempting mob collisions. +/// +[ByRefEvent] +public record struct AttemptMobTargetCollideEvent +{ + public bool Cancelled; +} diff --git a/Content.Shared/Movement/Systems/SharedMoverController.CVars.cs b/Content.Shared/Movement/Systems/SharedMoverController.CVars.cs deleted file mode 100644 index 4228b778475..00000000000 --- a/Content.Shared/Movement/Systems/SharedMoverController.CVars.cs +++ /dev/null @@ -1,43 +0,0 @@ -using Content.Shared.CCVar; -using Content.Shared.Mind.Components; -using Content.Shared.Movement.Components; -using Content.Shared.Movement.Events; -using Robust.Shared.Configuration; - -namespace Content.Shared.Movement.Systems; - -public abstract partial class SharedMoverController -{ - [Dependency] private readonly INetConfigurationManager _netConfig = default!; - - private void InitializeCVars() - { - SubscribeLocalEvent(OnMindAdded); - SubscribeLocalEvent(OnMindRemoved); - SubscribeNetworkEvent(OnUpdateCVars); - } - - private void OnMindAdded(Entity ent, ref MindAddedMessage args) - { - if (args.Mind.Comp.Session?.Channel is not { } channel) - return; - - ent.Comp.DefaultSprinting = _netConfig.GetClientCVar(channel, CCVars.DefaultWalk); - WalkingAlert(ent); - } - - private void OnMindRemoved(Entity ent, ref MindRemovedMessage args) - { - // If it's an ai-controlled mob, we probably want them sprinting by default. - ent.Comp.DefaultSprinting = true; - } - - private void OnUpdateCVars(UpdateInputCVarsMessage msg, EntitySessionEventArgs args) - { - if (args.SenderSession.AttachedEntity is not { } uid || !TryComp(uid, out var mover)) - return; - - mover.DefaultSprinting = _netConfig.GetClientCVar(args.SenderSession.Channel, CCVars.DefaultWalk); - WalkingAlert((uid, mover)); - } -} diff --git a/Content.Shared/Movement/Systems/SharedMoverController.Input.cs b/Content.Shared/Movement/Systems/SharedMoverController.Input.cs index abd7772fadd..2110e94d01d 100644 --- a/Content.Shared/Movement/Systems/SharedMoverController.Input.cs +++ b/Content.Shared/Movement/Systems/SharedMoverController.Input.cs @@ -5,16 +5,15 @@ using Content.Shared.Input; using Content.Shared.Movement.Components; using Content.Shared.Movement.Events; -using Robust.Shared.Configuration; using Robust.Shared.GameStates; using Robust.Shared.Input; using Robust.Shared.Input.Binding; using Robust.Shared.Map.Components; using Robust.Shared.Player; +using Robust.Shared.Prototypes; using Robust.Shared.Serialization; using Robust.Shared.Timing; using Robust.Shared.Utility; -using Robust.Shared.Maths; // Shitmed Change namespace Content.Shared.Movement.Systems { @@ -25,6 +24,8 @@ public abstract partial class SharedMoverController { public bool CameraRotationLocked { get; set; } + public static ProtoId WalkingAlert = "Walking"; + private void InitializeInput() { var moveUpCmdHandler = new MoverDirInputCmdHandler(this, Direction.North); @@ -92,17 +93,12 @@ protected void SetMoveInput(Entity entity, MoveButtons butt // Relay the fact we had any movement event. // TODO: Ideally we'd do these in a tick instead of out of sim. - // Shitmed Change Start - Vector2 vector2 = DirVecForButtons(buttons); - Vector2i vector2i = new Vector2i((int) vector2.X, (int) vector2.Y); - Direction dir = (vector2i == Vector2i.Zero) ? Direction.Invalid : vector2i.AsDirection(); - var moveEvent = new MoveInputEvent(entity, buttons, dir, buttons != 0); - // Shitmed Change End + var moveEvent = new MoveInputEvent(entity, entity.Comp.HeldMoveButtons); entity.Comp.HeldMoveButtons = buttons; RaiseLocalEvent(entity, ref moveEvent); Dirty(entity, entity.Comp); - var ev = new SpriteMoveEvent(entity.Comp.HeldMoveButtons != MoveButtons.None); + var ev = new SpriteMoveEvent(entity.Comp.HasDirectionalMovement); RaiseLocalEvent(entity, ref ev); } @@ -117,23 +113,18 @@ private void OnMoverHandleState(Entity entity, ref Componen entity.Comp.TargetRelativeRotation = state.TargetRelativeRotation; entity.Comp.CanMove = state.CanMove; entity.Comp.RelativeEntity = EnsureEntity(state.RelativeEntity, entity.Owner); - entity.Comp.DefaultSprinting = state.DefaultSprinting; // Reset entity.Comp.LastInputTick = GameTick.Zero; entity.Comp.LastInputSubTick = 0; - // Shitmed Change Start - Vector2 vector2 = DirVecForButtons(entity.Comp.HeldMoveButtons); - Vector2i vector2i = new Vector2i((int) vector2.X, (int) vector2.Y); - Direction dir = (vector2i == Vector2i.Zero) ? Direction.Invalid : vector2i.AsDirection(); - // Shitmed Change End + if (entity.Comp.HeldMoveButtons != state.HeldMoveButtons) { - var moveEvent = new MoveInputEvent(entity, entity.Comp.HeldMoveButtons, dir, state.HeldMoveButtons != 0); // Shitmed Change + var moveEvent = new MoveInputEvent(entity, entity.Comp.HeldMoveButtons); entity.Comp.HeldMoveButtons = state.HeldMoveButtons; RaiseLocalEvent(entity.Owner, ref moveEvent); - var ev = new SpriteMoveEvent(entity.Comp.HeldMoveButtons != MoveButtons.None); + var ev = new SpriteMoveEvent(entity.Comp.HasDirectionalMovement); RaiseLocalEvent(entity, ref ev); } } @@ -148,7 +139,6 @@ private void OnMoverGetState(Entity entity, ref ComponentGe HeldMoveButtons = entity.Comp.HeldMoveButtons, RelativeRotation = entity.Comp.RelativeRotation, TargetRelativeRotation = entity.Comp.TargetRelativeRotation, - DefaultSprinting = entity.Comp.DefaultSprinting }; } @@ -178,24 +168,16 @@ public void ResetCamera(EntityUid uid) return; } - // Shitmed Change Start - var xform = XformQuery.GetComponent(uid); - if (TryComp(uid, out RelayInputMoverComponent? relay) - && TryComp(relay.RelayEntity, out TransformComponent? relayXform) - && MoverQuery.TryGetComponent(relay.RelayEntity, out var relayMover)) - xform = relayXform; - // If we updated parent then cancel the accumulator and force it now. - if (!TryUpdateRelative(mover, xform) && mover.TargetRelativeRotation.Equals(Angle.Zero)) + if (!TryUpdateRelative(uid, mover, XformQuery.GetComponent(uid)) && mover.TargetRelativeRotation.Equals(Angle.Zero)) return; - // Shitmed Change End mover.LerpTarget = TimeSpan.Zero; mover.TargetRelativeRotation = Angle.Zero; Dirty(uid, mover); } - private bool TryUpdateRelative(InputMoverComponent mover, TransformComponent xform) + private bool TryUpdateRelative(EntityUid uid, InputMoverComponent mover, TransformComponent xform) { var relative = xform.GridUid; relative ??= xform.MapUid; @@ -210,38 +192,42 @@ private bool TryUpdateRelative(InputMoverComponent mover, TransformComponent xfo // Okay need to get our old relative rotation with respect to our new relative rotation // e.g. if we were right side up on our current grid need to get what that is on our new grid. - var currentRotation = Angle.Zero; - var targetRotation = Angle.Zero; + var oldRelativeRot = Angle.Zero; + var relativeRot = Angle.Zero; // Get our current relative rotation if (XformQuery.TryGetComponent(mover.RelativeEntity, out var oldRelativeXform)) { - currentRotation = _transform.GetWorldRotation(oldRelativeXform, XformQuery) + mover.RelativeRotation; + oldRelativeRot = _transform.GetWorldRotation(oldRelativeXform); } if (XformQuery.TryGetComponent(relative, out var relativeXform)) { // This is our current rotation relative to our new parent. - mover.RelativeRotation = (currentRotation - _transform.GetWorldRotation(relativeXform)).FlipPositive(); + relativeRot = _transform.GetWorldRotation(relativeXform); } - // If we went from grid -> map we'll preserve our worldrotation - if (relative != null && HasComp(relative.Value)) + var diff = relativeRot - oldRelativeRot; + + // If we're going from a grid -> map then preserve the relative rotation so it's seamless if they go into space and back. + if (MapQuery.HasComp(relative) && MapGridQuery.HasComp(mover.RelativeEntity)) { - targetRotation = currentRotation.FlipPositive().Reduced(); + mover.TargetRelativeRotation -= diff; } - // If we went from grid -> grid OR grid -> map then snap the target to cardinal and lerp there. - // OR just rotate to zero (depending on cvar) - else if (relative != null && _mapManager.IsGrid(relative.Value)) + // Snap to nearest cardinal if map -> grid or grid -> grid + else if (MapGridQuery.HasComp(relative) && (MapQuery.HasComp(mover.RelativeEntity) || MapGridQuery.HasComp(mover.RelativeEntity))) { - if (CameraRotationLocked) - targetRotation = Angle.Zero; - else - targetRotation = mover.RelativeRotation.GetCardinalDir().ToAngle().Reduced(); + var targetDir = mover.TargetRelativeRotation - diff; + targetDir = targetDir.GetCardinalDir().ToAngle().Reduced(); + mover.TargetRelativeRotation = targetDir; } + // Preserve target rotation in relation to the new parent. + // Regardless of what the target is don't want the eye to move at all (from the player's perspective). + mover.RelativeRotation -= diff; + mover.RelativeEntity = relative; - mover.TargetRelativeRotation = targetRotation; + Dirty(uid, mover); return true; } @@ -310,37 +296,26 @@ private void OnInputParentChange(Entity entity, ref EntPare Dirty(entity.Owner, entity.Comp); } - private void HandleDirChange(EntityUid entity, Direction dir, ushort subTick, bool state) + private void HandleDirChange(Entity entity, Direction dir, ushort subTick, bool state) { // Relayed movement just uses the same keybinds given we're moving the relayed entity // the same as us. + if (!MoverQuery.Resolve(entity, ref entity.Comp)) + return; - if (TryComp(entity, out var relayMover)) + // TODO: Should move this into HandleMobMovement itself. + if (entity.Comp.CanMove && RelayQuery.TryComp(entity, out var relayMover)) { - DebugTools.Assert(relayMover.RelayEntity != entity); + DebugTools.Assert(relayMover.RelayEntity != entity.Owner); DebugTools.AssertNotNull(relayMover.RelayEntity); if (MoverQuery.TryGetComponent(entity, out var mover)) SetMoveInput((entity, mover), MoveButtons.None); - if (_mobState.IsDead(entity) - || _mobState.IsCritical(entity) && !_configManager.GetCVar(CCVars.AllowMovementWhileCrit)) - return; - HandleDirChange(relayMover.RelayEntity, dir, subTick, state); - return; } - if (!MoverQuery.TryGetComponent(entity, out var moverComp)) - return; - - // Shitmed Change Start - var moverEntity = new Entity(entity, moverComp); - var moveEvent = new MoveInputEvent(moverEntity, moverComp.HeldMoveButtons, dir, state); - RaiseLocalEvent(entity, ref moveEvent); - // Shitmed Change End - // For stuff like "Moving out of locker" or the likes // We'll relay a movement input to the parent. if (_container.IsEntityInContainer(entity) && @@ -352,7 +327,7 @@ private void HandleDirChange(EntityUid entity, Direction dir, ushort subTick, bo RaiseLocalEvent(xform.ParentUid, ref relayMoveEvent); } - SetVelocityDirection((entity, moverComp), dir, subTick, state); + SetVelocityDirection((entity, entity.Comp), dir, subTick, state); } private void OnInputInit(Entity entity, ref ComponentInit args) @@ -364,7 +339,6 @@ private void OnInputInit(Entity entity, ref ComponentInit a entity.Comp.RelativeEntity = xform.GridUid ?? xform.MapUid; entity.Comp.TargetRelativeRotation = Angle.Zero; - WalkingAlert(entity); } private void HandleRunChange(EntityUid uid, ushort subTick, bool walking) @@ -377,7 +351,6 @@ private void HandleRunChange(EntityUid uid, ushort subTick, bool walking) if (moverComp != null) { SetMoveInput((uid, moverComp), MoveButtons.None); - WalkingAlert((uid, moverComp)); } HandleRunChange(relayMover.RelayEntity, subTick, walking); @@ -493,12 +466,11 @@ private void ResetSubtick(InputMoverComponent component) component.LastInputSubTick = 0; } - public void SetSprinting(Entity entity, ushort subTick, bool walking) + public virtual void SetSprinting(Entity entity, ushort subTick, bool walking) { // Logger.Info($"[{_gameTiming.CurTick}/{subTick}] Sprint: {enabled}"); SetMoveInput(entity, subTick, walking, MoveButtons.Walk); - WalkingAlert(entity); } /// @@ -650,7 +622,7 @@ public enum MoveButtons : byte Down = 2, Left = 4, Right = 8, - Walk = 16, // This may be either a sprint button or a walk button, depending on mover config + Walk = 16, AnyDirection = Up | Down | Left | Right, } diff --git a/Content.Shared/Movement/Systems/SharedMoverController.Relay.cs b/Content.Shared/Movement/Systems/SharedMoverController.Relay.cs index 81569553772..55a1f138cb5 100644 --- a/Content.Shared/Movement/Systems/SharedMoverController.Relay.cs +++ b/Content.Shared/Movement/Systems/SharedMoverController.Relay.cs @@ -1,4 +1,5 @@ using Content.Shared.Movement.Components; +using Content.Shared.Movement.Events; namespace Content.Shared.Movement.Systems; @@ -10,16 +11,26 @@ private void InitializeRelay() SubscribeLocalEvent(OnTargetRelayShutdown); SubscribeLocalEvent(OnAfterRelayTargetState); SubscribeLocalEvent(OnAfterRelayState); + SubscribeLocalEvent(OnRelayCanMoveUpdated); } private void OnAfterRelayTargetState(Entity entity, ref AfterAutoHandleStateEvent args) { - Physics.UpdateIsPredicted(entity.Owner); + PhysicsSystem.UpdateIsPredicted(entity.Owner); } private void OnAfterRelayState(Entity entity, ref AfterAutoHandleStateEvent args) { - Physics.UpdateIsPredicted(entity.Owner); + PhysicsSystem.UpdateIsPredicted(entity.Owner); + } + + private void OnRelayCanMoveUpdated(Entity ent, ref CanMoveUpdatedEvent args) + { + if (args.CanMove) + return; + + if (MoverQuery.TryComp(ent.Comp.RelayEntity, out var inputMoverComponent)) + SetMoveInput((ent.Comp.RelayEntity, inputMoverComponent), MoveButtons.None); } /// @@ -42,7 +53,7 @@ public void SetRelay(EntityUid uid, EntityUid relayEntity) { oldTarget.Source = EntityUid.Invalid; RemComp(component.RelayEntity, oldTarget); - Physics.UpdateIsPredicted(component.RelayEntity); + PhysicsSystem.UpdateIsPredicted(component.RelayEntity); } var targetComp = EnsureComp(relayEntity); @@ -50,11 +61,11 @@ public void SetRelay(EntityUid uid, EntityUid relayEntity) { oldRelay.RelayEntity = EntityUid.Invalid; RemComp(targetComp.Source, oldRelay); - Physics.UpdateIsPredicted(targetComp.Source); + PhysicsSystem.UpdateIsPredicted(targetComp.Source); } - Physics.UpdateIsPredicted(uid); - Physics.UpdateIsPredicted(relayEntity); + PhysicsSystem.UpdateIsPredicted(uid); + PhysicsSystem.UpdateIsPredicted(relayEntity); component.RelayEntity = relayEntity; targetComp.Source = uid; Dirty(uid, component); @@ -63,8 +74,8 @@ public void SetRelay(EntityUid uid, EntityUid relayEntity) private void OnRelayShutdown(Entity entity, ref ComponentShutdown args) { - Physics.UpdateIsPredicted(entity.Owner); - Physics.UpdateIsPredicted(entity.Comp.RelayEntity); + PhysicsSystem.UpdateIsPredicted(entity.Owner); + PhysicsSystem.UpdateIsPredicted(entity.Comp.RelayEntity); if (TryComp(entity.Comp.RelayEntity, out var inputMover)) SetMoveInput((entity.Comp.RelayEntity, inputMover), MoveButtons.None); @@ -78,8 +89,8 @@ private void OnRelayShutdown(Entity entity, ref Compon private void OnTargetRelayShutdown(Entity entity, ref ComponentShutdown args) { - Physics.UpdateIsPredicted(entity.Owner); - Physics.UpdateIsPredicted(entity.Comp.Source); + PhysicsSystem.UpdateIsPredicted(entity.Owner); + PhysicsSystem.UpdateIsPredicted(entity.Comp.Source); if (Timing.ApplyingState) return; diff --git a/Content.Shared/Movement/Systems/SharedMoverController.cs b/Content.Shared/Movement/Systems/SharedMoverController.cs index a3bb17860ef..ec6cba54a2e 100644 --- a/Content.Shared/Movement/Systems/SharedMoverController.cs +++ b/Content.Shared/Movement/Systems/SharedMoverController.cs @@ -1,7 +1,6 @@ using System.Diagnostics.CodeAnalysis; using System.Numerics; -using Content.Shared.Alert; -using Content.Shared.Bed.Sleep; +using Content.Shared.ActionBlocker; using Content.Shared.CCVar; using Content.Shared.Friction; using Content.Shared.Gravity; @@ -10,9 +9,7 @@ using Content.Shared.Mobs.Systems; using Content.Shared.Movement.Components; using Content.Shared.Movement.Events; -using Content.Shared.StepTrigger.Components; using Content.Shared.Tag; -using Content.Shared.Traits.Assorted.Components; using Robust.Shared.Audio; using Robust.Shared.Audio.Systems; using Robust.Shared.Configuration; @@ -22,7 +19,7 @@ using Robust.Shared.Physics; using Robust.Shared.Physics.Components; using Robust.Shared.Physics.Controllers; -using Robust.Shared.Physics.Systems; +using Robust.Shared.Prototypes; using Robust.Shared.Timing; using Robust.Shared.Utility; using PullableComponent = Content.Shared.Movement.Pulling.Components.PullableComponent; @@ -36,11 +33,9 @@ namespace Content.Shared.Movement.Systems; public abstract partial class SharedMoverController : VirtualController { [Dependency] private readonly IConfigurationManager _configManager = default!; - [Dependency] private readonly IEntityManager _entities = default!; [Dependency] protected readonly IGameTiming Timing = default!; - [Dependency] private readonly IMapManager _mapManager = default!; [Dependency] private readonly ITileDefinitionManager _tileDefinitionManager = default!; - [Dependency] private readonly AlertsSystem _alerts = default!; + [Dependency] private readonly ActionBlockerSystem _blocker = default!; [Dependency] private readonly EntityLookupSystem _lookup = default!; [Dependency] private readonly InventorySystem _inventory = default!; [Dependency] private readonly MobStateSystem _mobState = default!; @@ -48,37 +43,40 @@ public abstract partial class SharedMoverController : VirtualController [Dependency] private readonly SharedContainerSystem _container = default!; [Dependency] private readonly SharedMapSystem _mapSystem = default!; [Dependency] private readonly SharedGravitySystem _gravity = default!; - [Dependency] protected readonly SharedPhysicsSystem Physics = default!; [Dependency] private readonly SharedTransformSystem _transform = default!; [Dependency] private readonly TagSystem _tags = default!; + protected EntityQuery CanMoveInAirQuery; + protected EntityQuery FootstepModifierQuery; protected EntityQuery MoverQuery; + protected EntityQuery MapQuery; + protected EntityQuery MapGridQuery; protected EntityQuery MobMoverQuery; protected EntityQuery RelayTargetQuery; protected EntityQuery ModifierQuery; + protected EntityQuery NoRotateQuery; protected EntityQuery PhysicsQuery; protected EntityQuery RelayQuery; protected EntityQuery PullableQuery; protected EntityQuery XformQuery; - protected EntityQuery CanMoveInAirQuery; - protected EntityQuery NoRotateQuery; - protected EntityQuery FootstepModifierQuery; - protected EntityQuery MapGridQuery; - /// - /// - /// - private float _stopSpeed; + private static readonly ProtoId FootstepSoundTag = "FootstepSound"; private bool _relativeMovement; + private float _minDamping; + private float _airDamping; + private float _offGridDamping; /// /// Cache the mob movement calculation to re-use elsewhere. /// public Dictionary UsedMobMovement = new(); + private readonly HashSet _aroundColliderSet = []; + public override void Initialize() { + UpdatesBefore.Add(typeof(TileFrictionController)); base.Initialize(); MoverQuery = GetEntityQuery(); @@ -93,13 +91,16 @@ public override void Initialize() CanMoveInAirQuery = GetEntityQuery(); FootstepModifierQuery = GetEntityQuery(); MapGridQuery = GetEntityQuery(); + MapQuery = GetEntityQuery(); + + SubscribeLocalEvent(OnTileFriction); InitializeInput(); InitializeRelay(); - InitializeCVars(); Subs.CVar(_configManager, CCVars.RelativeMovement, value => _relativeMovement = value, true); - Subs.CVar(_configManager, CCVars.StopSpeed, value => _stopSpeed = value, true); - UpdatesBefore.Add(typeof(TileFrictionController)); + Subs.CVar(_configManager, CCVars.MinFriction, value => _minDamping = value, true); + Subs.CVar(_configManager, CCVars.AirFriction, value => _airDamping = value, true); + Subs.CVar(_configManager, CCVars.OffgridFriction, value => _offGridDamping = value, true); } public override void Shutdown() @@ -118,184 +119,223 @@ public override void UpdateAfterSolve(bool prediction, float frameTime) /// Movement while considering actionblockers, weightlessness, etc. /// protected void HandleMobMovement( - EntityUid uid, - InputMoverComponent mover, - EntityUid physicsUid, - PhysicsComponent physicsComponent, - TransformComponent xform, + Entity entity, float frameTime) { - var canMove = mover.CanMove; - if (RelayTargetQuery.TryGetComponent(uid, out var relayTarget)) + var uid = entity.Owner; + var mover = entity.Comp; + + // If we're a relay then apply all of our data to the parent instead and go next. + if (RelayQuery.TryComp(uid, out var relay)) { - if (_mobState.IsIncapacitated(relayTarget.Source) || - TryComp(relayTarget.Source, out _) || - // Shitmed Change - !PhysicsQuery.TryGetComponent(relayTarget.Source, out var relayedPhysicsComponent) || - !MoverQuery.TryGetComponent(relayTarget.Source, out var relayedMover) || - !XformQuery.TryGetComponent(relayTarget.Source, out var relayedXform)) + if (!MoverQuery.TryComp(relay.RelayEntity, out var relayTargetMover)) + return; + + // Always lerp rotation so relay entities aren't cooked. + LerpRotation(uid, mover, frameTime); + var dirtied = false; + + if (relayTargetMover.RelativeEntity != mover.RelativeEntity) { - canMove = false; + relayTargetMover.RelativeEntity = mover.RelativeEntity; + dirtied = true; } - else + + if (relayTargetMover.RelativeRotation != mover.RelativeRotation) { - mover.LerpTarget = relayedMover.LerpTarget; - mover.RelativeEntity = relayedMover.RelativeEntity; - mover.RelativeRotation = relayedMover.RelativeRotation; - mover.TargetRelativeRotation = relayedMover.TargetRelativeRotation; - HandleMobMovement(relayTarget.Source, relayedMover, relayTarget.Source, relayedPhysicsComponent, relayedXform, frameTime); + relayTargetMover.RelativeRotation = mover.RelativeRotation; + dirtied = true; } + + if (relayTargetMover.TargetRelativeRotation != mover.TargetRelativeRotation) + { + relayTargetMover.TargetRelativeRotation = mover.TargetRelativeRotation; + dirtied = true; + } + + if (dirtied) + { + Dirty(relay.RelayEntity, relayTargetMover); + } + + return; } - // Update relative movement - // Shitmed Change Start - else + if (!XformQuery.TryComp(entity.Owner, out var xform)) + return; + + RelayTargetQuery.TryComp(uid, out var relayTarget); + var relaySource = relayTarget?.Source; + + // If we're not the target of a relay then handle lerp data. + if (relaySource == null) { + // Update relative movement if (mover.LerpTarget < Timing.CurTime) { - if (TryComp(uid, out RelayInputMoverComponent? relay) - && TryComp(relay.RelayEntity, out TransformComponent? relayXform)) - { - if (TryUpdateRelative(mover, relayXform)) - Dirty(uid, mover); - } - else - { - if (TryUpdateRelative(mover, xform)) - Dirty(uid, mover); - } + TryUpdateRelative(uid, mover, xform); } LerpRotation(uid, mover, frameTime); } - // Shitmed Change End - if (!canMove - || physicsComponent.BodyStatus != BodyStatus.OnGround && !CanMoveInAirQuery.HasComponent(uid) + // If we can't move then just use tile-friction / no movement handling. + if (!mover.CanMove + || !PhysicsQuery.TryComp(uid, out var physicsComponent) || PullableQuery.TryGetComponent(uid, out var pullable) && pullable.BeingPulled) { UsedMobMovement[uid] = false; return; } + // If the body is in air but isn't weightless then it can't move + // TODO: MAKE ISWEIGHTLESS EVENT BASED + var weightless = _gravity.IsWeightless(uid, physicsComponent, xform); + var inAirHelpless = false; - UsedMobMovement[uid] = true; - // Specifically don't use mover.Owner because that may be different to the actual physics body being moved. - var weightless = _gravity.IsWeightless(physicsUid, physicsComponent, xform); - var (walkDir, sprintDir) = GetVelocityInput(mover); - var touching = false; - - // Handle wall-pushes. - if (weightless) + if (physicsComponent.BodyStatus != BodyStatus.OnGround && !CanMoveInAirQuery.HasComponent(uid)) { - if (xform.GridUid != null) - touching = true; - - if (!touching) + if (!weightless) { - var ev = new CanWeightlessMoveEvent(uid); - RaiseLocalEvent(uid, ref ev, true); - // No gravity: is our entity touching anything? - touching = ev.CanMove; - - if (!touching && TryComp(uid, out var mobMover)) - touching |= IsAroundCollider(PhysicsSystem, xform, mobMover, physicsUid, physicsComponent); + UsedMobMovement[uid] = false; + return; } + inAirHelpless = true; } + UsedMobMovement[uid] = true; + + var moveSpeedComponent = ModifierQuery.CompOrNull(uid); + + float friction; + float accel; + Vector2 wishDir; + var velocity = physicsComponent.LinearVelocity; + // Get current tile def for things like speed/friction mods ContentTileDefinition? tileDef = null; - // Don't bother getting the tiledef here if we're weightless or in-air - // since no tile-based modifiers should be applying in that situation - if (MapGridQuery.TryComp(xform.GridUid, out var gridComp) - && _mapSystem.TryGetTileRef(xform.GridUid.Value, gridComp, xform.Coordinates, out var tile) - && !(weightless || physicsComponent.BodyStatus == BodyStatus.InAir)) + var touching = false; + // Whether we use tilefriction or not + if (weightless || inAirHelpless) { - tileDef = (ContentTileDefinition) _tileDefinitionManager[tile.Tile.TypeId]; - } - - // Regular movement. - // Target velocity. - // This is relative to the map / grid we're on. - var moveSpeedComponent = ModifierQuery.CompOrNull(uid); - - var walkSpeed = moveSpeedComponent?.CurrentWalkSpeed ?? MovementSpeedModifierComponent.DefaultBaseWalkSpeed; - var sprintSpeed = moveSpeedComponent?.CurrentSprintSpeed ?? MovementSpeedModifierComponent.DefaultBaseSprintSpeed; + // Find the speed we should be moving at and make sure we're not trying to move faster than that + var walkSpeed = moveSpeedComponent?.WeightlessWalkSpeed ?? MovementSpeedModifierComponent.DefaultBaseWalkSpeed; + var sprintSpeed = moveSpeedComponent?.WeightlessSprintSpeed ?? MovementSpeedModifierComponent.DefaultBaseSprintSpeed; - var total = walkDir * walkSpeed + sprintDir * sprintSpeed; + wishDir = AssertValidWish(mover, walkSpeed, sprintSpeed); - var parentRotation = GetParentGridAngle(mover); - var worldTotal = _relativeMovement ? parentRotation.RotateVec(total) : total; + var ev = new CanWeightlessMoveEvent(uid); + RaiseLocalEvent(uid, ref ev, true); - DebugTools.Assert(MathHelper.CloseToPercent(total.Length(), worldTotal.Length())); + touching = ev.CanMove || xform.GridUid != null || MapGridQuery.HasComp(xform.GridUid); - var velocity = physicsComponent.LinearVelocity; - float friction; - float weightlessModifier; - float accel; + // If we're not on a grid, and not able to move in space check if we're close enough to a grid to touch. + if (!touching && MobMoverQuery.TryComp(uid, out var mobMover)) + touching |= IsAroundCollider(_lookup, (uid, physicsComponent, mobMover, xform)); - if (weightless) - { - if (gridComp == null && !MapGridQuery.HasComp(xform.GridUid)) - friction = moveSpeedComponent?.OffGridFriction ?? MovementSpeedModifierComponent.DefaultOffGridFriction; - else if (worldTotal != Vector2.Zero && touching) - friction = moveSpeedComponent?.WeightlessFriction ?? MovementSpeedModifierComponent.DefaultWeightlessFriction; + // If we're touching then use the weightless values + if (touching) + { + touching = true; + if (wishDir != Vector2.Zero) + friction = moveSpeedComponent?.WeightlessFriction ?? _airDamping; + else + friction = moveSpeedComponent?.WeightlessFrictionNoInput ?? _airDamping; + } + // Otherwise use the off-grid values. else - friction = moveSpeedComponent?.WeightlessFrictionNoInput ?? MovementSpeedModifierComponent.DefaultWeightlessFrictionNoInput; + { + friction = moveSpeedComponent?.OffGridFriction ?? _offGridDamping; + } - weightlessModifier = moveSpeedComponent?.WeightlessModifier ?? MovementSpeedModifierComponent.DefaultWeightlessModifier; accel = moveSpeedComponent?.WeightlessAcceleration ?? MovementSpeedModifierComponent.DefaultWeightlessAcceleration; } else { - if (worldTotal != Vector2.Zero) + if (MapGridQuery.TryComp(xform.GridUid, out var gridComp) + && _mapSystem.TryGetTileRef(xform.GridUid.Value, gridComp, xform.Coordinates, out var tile) + && physicsComponent.BodyStatus == BodyStatus.OnGround) + tileDef = (ContentTileDefinition)_tileDefinitionManager[tile.Tile.TypeId]; + + var walkSpeed = moveSpeedComponent?.CurrentWalkSpeed ?? MovementSpeedModifierComponent.DefaultBaseWalkSpeed; + var sprintSpeed = moveSpeedComponent?.CurrentSprintSpeed ?? MovementSpeedModifierComponent.DefaultBaseSprintSpeed; + + wishDir = AssertValidWish(mover, walkSpeed, sprintSpeed); + + if (wishDir != Vector2.Zero) { - friction = tileDef?.MobFriction ?? moveSpeedComponent?.Friction ?? MovementSpeedModifierComponent.DefaultFriction; + friction = moveSpeedComponent?.Friction ?? MovementSpeedModifierComponent.DefaultFriction; + friction *= tileDef?.MobFriction ?? tileDef?.Friction ?? 1f; } else { - friction = tileDef?.MobFrictionNoInput ?? moveSpeedComponent?.FrictionNoInput ?? MovementSpeedModifierComponent.DefaultFrictionNoInput; + friction = moveSpeedComponent?.FrictionNoInput ?? MovementSpeedModifierComponent.DefaultFrictionNoInput; + friction *= tileDef?.Friction ?? 1f; } - weightlessModifier = 1f; - accel = tileDef?.MobAcceleration ?? moveSpeedComponent?.Acceleration ?? MovementSpeedModifierComponent.DefaultAcceleration; + accel = moveSpeedComponent?.Acceleration ?? MovementSpeedModifierComponent.DefaultAcceleration; + accel *= tileDef?.MobAcceleration ?? 1f; } + // This way friction never exceeds acceleration when you're trying to move. + // If you want to slow down an entity with "friction" you shouldn't be using this system. + if (wishDir != Vector2.Zero) + friction = Math.Min(friction, accel); + friction = Math.Max(friction, _minDamping); var minimumFrictionSpeed = moveSpeedComponent?.MinimumFrictionSpeed ?? MovementSpeedModifierComponent.DefaultMinimumFrictionSpeed; Friction(minimumFrictionSpeed, frameTime, friction, ref velocity); - if (worldTotal != Vector2.Zero) + if (!weightless || touching) + Accelerate(ref velocity, in wishDir, accel, frameTime); + + SetWishDir((uid, mover), wishDir); + + /* + * SNAKING!!! >-( 0 ================> + * Snaking is a feature where you can move faster by strafing in a direction perpendicular to the + * direction you intend to move while still holding the movement key for the direction you're trying to move. + * Snaking only works if acceleration exceeds friction, and it's effectiveness scales as acceleration continues + * to exceed friction. + * Snaking works because friction is applied first in the direction of our current velocity, while acceleration + * is applied after in our "Wish Direction" and is capped by the dot of our wish direction and current direction. + * This means when you change direction, you're technically able to accelerate more than what the velocity cap + * allows, but friction normally eats up the extra movement you gain. + * By strafing as stated above you can increase your speed by about 1.4 (square root of 2). + * This only works if friction is low enough so be sure that anytime you are letting a mob move in a low friction + * environment you take into account the fact they can snake! Also be sure to lower acceleration as well to + * prevent jerky movement! + */ + PhysicsSystem.SetLinearVelocity(uid, velocity, body: physicsComponent); + + // Ensures that players do not spiiiiiiin + PhysicsSystem.SetAngularVelocity(uid, 0, body: physicsComponent); + + // Handle footsteps at the end + if (wishDir != Vector2.Zero) { if (!NoRotateQuery.HasComponent(uid)) { // TODO apparently this results in a duplicate move event because "This should have its event run during // island solver"??. So maybe SetRotation needs an argument to avoid raising an event? var worldRot = _transform.GetWorldRotation(xform); - _transform.SetLocalRotation(xform, xform.LocalRotation + worldTotal.ToWorldAngle() - worldRot); + + _transform.SetLocalRotation(uid, xform.LocalRotation + wishDir.ToWorldAngle() - worldRot, xform); } if (!weightless && MobMoverQuery.TryGetComponent(uid, out var mobMover) && TryGetSound(weightless, uid, mover, mobMover, xform, out var sound, tileDef: tileDef)) { var soundModifier = mover.Sprinting ? 3.5f : 1.5f; - var volume = sound.Params.Volume + soundModifier; - - if (_entities.TryGetComponent(uid, out FootstepVolumeModifierComponent? volumeModifier)) - { - volume += mover.Sprinting - ? volumeModifier.SprintVolumeModifier - : volumeModifier.WalkVolumeModifier; - } var audioParams = sound.Params - .WithVolume(volume) + .WithVolume(sound.Params.Volume + soundModifier) .WithVariation(sound.Params.Variation ?? mobMover.FootstepVariation); // If we're a relay target then predict the sound for all relays. - if (relayTarget != null) + if (relaySource != null) { - _audio.PlayPredicted(sound, uid, relayTarget.Source, audioParams); + _audio.PlayPredicted(sound, uid, relaySource.Value, audioParams); } else { @@ -303,21 +343,23 @@ protected void HandleMobMovement( } } } + } - worldTotal *= weightlessModifier; - - if (!weightless || touching) - Accelerate(ref velocity, in worldTotal, accel, frameTime); - - PhysicsSystem.SetLinearVelocity(physicsUid, velocity, body: physicsComponent); + public Vector2 GetWishDir(Entity mover) + { + if (!MoverQuery.Resolve(mover.Owner, ref mover.Comp, false)) + return Vector2.Zero; - // Ensures that players do not spiiiiiiin - PhysicsSystem.SetAngularVelocity(physicsUid, 0, body: physicsComponent); + return mover.Comp.WishDir; } - private void WalkingAlert(Entity entity) + public void SetWishDir(Entity mover, Vector2 wishDir) { - _alerts.ShowAlert(entity, entity.Comp.WalkingAlert, entity.Comp.Sprinting ? (short) 1 : (short) 0); + if (mover.Comp.WishDir.Equals(wishDir)) + return; + + mover.Comp.WishDir = wishDir; + Dirty(mover); } public void LerpRotation(EntityUid uid, InputMoverComponent mover, float frameTime) @@ -341,40 +383,44 @@ public void LerpRotation(EntityUid uid, InputMoverComponent mover, float frameTi adjustment = Math.Clamp(adjustment, -angleDiff, angleDiff); } - mover.RelativeRotation += adjustment; - mover.RelativeRotation.FlipPositive(); + mover.RelativeRotation = (mover.RelativeRotation + adjustment).FlipPositive(); Dirty(uid, mover); } else if (!angleDiff.Equals(Angle.Zero)) { - mover.TargetRelativeRotation.FlipPositive(); - mover.RelativeRotation = mover.TargetRelativeRotation; + mover.RelativeRotation = mover.TargetRelativeRotation.FlipPositive(); Dirty(uid, mover); } } - private void Friction(float minimumFrictionSpeed, float frameTime, float friction, ref Vector2 velocity) + public void Friction(float minimumFrictionSpeed, float frameTime, float friction, ref Vector2 velocity) { var speed = velocity.Length(); if (speed < minimumFrictionSpeed) return; - var drop = 0f; - - var control = MathF.Max(_stopSpeed, speed); - drop += control * friction * frameTime; + // This equation is lifted from the Physics Island solver. + // We re-use it here because Kinematic Controllers can't/shouldn't use the Physics Friction + velocity *= Math.Clamp(1.0f - frameTime * friction, 0.0f, 1.0f); - var newSpeed = MathF.Max(0f, speed - drop); + } - if (newSpeed.Equals(speed)) + public void Friction(float minimumFrictionSpeed, float frameTime, float friction, ref float velocity) + { + if (Math.Abs(velocity) < minimumFrictionSpeed) return; - newSpeed /= speed; - velocity *= newSpeed; + // This equation is lifted from the Physics Island solver. + // We re-use it here because Kinematic Controllers can't/shouldn't use the Physics Friction + velocity *= Math.Clamp(1.0f - frameTime * friction, 0.0f, 1.0f); + } - private void Accelerate(ref Vector2 currentVelocity, in Vector2 velocity, float accel, float frameTime) + /// + /// Adjusts the current velocity to the target velocity based on the specified acceleration. + /// + public static void Accelerate(ref Vector2 currentVelocity, in Vector2 velocity, float accel, float frameTime) { var wishDir = velocity != Vector2.Zero ? velocity.Normalized() : Vector2.Zero; var wishSpeed = velocity.Length(); @@ -397,23 +443,29 @@ public bool UseMobMovement(EntityUid uid) } /// - /// Used for weightlessness to determine if we are near a wall. + /// Used for weightlessness to determine if we are near a wall. /// - private bool IsAroundCollider(SharedPhysicsSystem broadPhaseSystem, TransformComponent transform, MobMoverComponent mover, EntityUid physicsUid, PhysicsComponent collider) + private bool IsAroundCollider(EntityLookupSystem lookupSystem, Entity entity) { - var enlargedAABB = _lookup.GetWorldAABB(physicsUid, transform).Enlarged(mover.GrabRangeVV); + var (uid, collider, mover, transform) = entity; + var enlargedAABB = _lookup.GetWorldAABB(entity.Owner, transform).Enlarged(mover.GrabRange); - foreach (var otherCollider in broadPhaseSystem.GetCollidingEntities(transform.MapID, enlargedAABB)) + _aroundColliderSet.Clear(); + lookupSystem.GetEntitiesIntersecting(transform.MapID, enlargedAABB, _aroundColliderSet); + foreach (var otherEntity in _aroundColliderSet) { - if (otherCollider == collider) + if (otherEntity == uid) continue; // Don't try to push off of yourself! + if (!PhysicsQuery.TryComp(otherEntity, out var otherCollider)) + continue; + // Only allow pushing off of anchored things that have collision. if (otherCollider.BodyType != BodyType.Static || !otherCollider.CanCollide || ((collider.CollisionMask & otherCollider.CollisionLayer) == 0 && - (otherCollider.CollisionMask & collider.CollisionLayer) == 0) || - (TryComp(otherCollider.Owner, out PullableComponent? pullable) && pullable.BeingPulled)) + (otherCollider.CollisionMask & collider.CollisionLayer) == 0) || + (TryComp(otherEntity, out PullableComponent? pullable) && pullable.BeingPulled)) { continue; } @@ -437,7 +489,7 @@ private bool TryGetSound( { sound = null; - if (!CanSound() || !_tags.HasTag(uid, "FootstepSound")) + if (!CanSound() || !_tags.HasTag(uid, FootstepSoundTag)) return false; var coordinates = xform.Coordinates; @@ -470,9 +522,6 @@ private bool TryGetSound( if (mobMover.StepSoundDistance < distanceNeeded) return false; - var soundEv = new MakeFootstepSoundEvent(); - RaiseLocalEvent(uid, soundEv); - mobMover.StepSoundDistance -= distanceNeeded; if (FootstepModifierQuery.TryComp(uid, out var moverModifier)) @@ -481,12 +530,6 @@ private bool TryGetSound( return sound != null; } - if (_entities.TryGetComponent(uid, out NoShoesSilentFootstepsComponent? _) & - !_inventory.TryGetSlotEntity(uid, "shoes", out var _)) - { - return false; - } - if (_inventory.TryGetSlotEntity(uid, "shoes", out var shoes) && FootstepModifierQuery.TryComp(shoes, out var modifier)) { @@ -517,12 +560,12 @@ private bool TryGetFootstepSound( return sound != null; } - var position = grid.LocalToTile(xform.Coordinates); + var position = _mapSystem.LocalToTile(xform.GridUid.Value, grid, xform.Coordinates); var soundEv = new GetFootstepSoundEvent(uid); // If the coordinates have a FootstepModifier component // i.e. component that emit sound on footsteps emit that sound - var anchored = grid.GetAnchoredEntitiesEnumerator(position); + var anchored = _mapSystem.GetAnchoredEntitiesEnumerator(xform.GridUid.Value, grid, position); while (anchored.MoveNext(out var maybeFootstep)) { @@ -544,9 +587,9 @@ private bool TryGetFootstepSound( // Walking on a tile. // Tile def might have been passed in already from previous methods, so use that // if we have it - if (tileDef == null && grid.TryGetTileRef(position, out var tileRef)) + if (tileDef == null && _mapSystem.TryGetTileRef(xform.GridUid.Value, grid, position, out var tileRef)) { - tileDef = (ContentTileDefinition) _tileDefinitionManager[tileRef.Tile.TypeId]; + tileDef = (ContentTileDefinition)_tileDefinitionManager[tileRef.Tile.TypeId]; } if (tileDef == null) @@ -555,4 +598,30 @@ private bool TryGetFootstepSound( sound = haveShoes ? tileDef.FootstepSounds : tileDef.BarestepSounds; return sound != null; } + + private Vector2 AssertValidWish(InputMoverComponent mover, float walkSpeed, float sprintSpeed) + { + var (walkDir, sprintDir) = GetVelocityInput(mover); + + var total = walkDir * walkSpeed + sprintDir * sprintSpeed; + + var parentRotation = GetParentGridAngle(mover); + var wishDir = _relativeMovement ? parentRotation.RotateVec(total) : total; + + DebugTools.Assert(MathHelper.CloseToPercent(total.Length(), wishDir.Length())); + + return wishDir; + } + + private void OnTileFriction(Entity ent, ref TileFrictionEvent args) + { + if (!TryComp(ent, out var physicsComponent) || !XformQuery.TryComp(ent, out var xform)) + return; + + // TODO: Make IsWeightless event based!!! + if (physicsComponent.BodyStatus != BodyStatus.OnGround || _gravity.IsWeightless(ent, physicsComponent, xform)) + args.Modifier *= ent.Comp.BaseWeightlessFriction; + else + args.Modifier *= ent.Comp.BaseFriction; + } } diff --git a/Content.Shared/Vehicle/Components/ContainerVehicleComponent.cs b/Content.Shared/Vehicle/Components/ContainerVehicleComponent.cs new file mode 100644 index 00000000000..e0c5dd1bc3c --- /dev/null +++ b/Content.Shared/Vehicle/Components/ContainerVehicleComponent.cs @@ -0,0 +1,18 @@ +using Robust.Shared.GameStates; + +namespace Content.Shared.Vehicle.Components; + +/// +/// A whose operator must be inside a specified container. +/// Note that the operator is the first to enter the container and won't be removed until they exit the container. +/// +[RegisterComponent, NetworkedComponent] +[Access(typeof(VehicleSystem))] +public sealed partial class ContainerVehicleComponent : Component +{ + /// + /// The ID of the container for the operator. + /// + [DataField(required: true)] + public string ContainerId; +} diff --git a/Content.Shared/Vehicle/Components/GenericKeyedVehicleComponent.cs b/Content.Shared/Vehicle/Components/GenericKeyedVehicleComponent.cs new file mode 100644 index 00000000000..72df15a8256 --- /dev/null +++ b/Content.Shared/Vehicle/Components/GenericKeyedVehicleComponent.cs @@ -0,0 +1,30 @@ +using Content.Shared.Whitelist; +using Robust.Shared.GameStates; + +namespace Content.Shared.Vehicle.Components; + +/// +/// This is used for a vehicle which can only be operated when a specific key matching a whitelist is inserted. +/// +[RegisterComponent, NetworkedComponent] +[Access(typeof(VehicleSystem))] +public sealed partial class GenericKeyedVehicleComponent : Component +{ + /// + /// The ID corresponding to the container where the "key" must be inserted. + /// + [DataField(required: true)] + public string ContainerId; + + /// + /// A whitelist determining what qualifies as a valid key for this vehicle. + /// + [DataField(required: true)] + public EntityWhitelist KeyWhitelist = new(); + + /// + /// If true, prevents keys which do not pass the from being inserted into + /// + [DataField] + public bool PreventInvalidInsertion = true; +} diff --git a/Content.Shared/Vehicle/Components/StrapVehicleComponent.cs b/Content.Shared/Vehicle/Components/StrapVehicleComponent.cs new file mode 100644 index 00000000000..9bd5ef3f12e --- /dev/null +++ b/Content.Shared/Vehicle/Components/StrapVehicleComponent.cs @@ -0,0 +1,10 @@ +using Robust.Shared.GameStates; + +namespace Content.Shared.Vehicle.Components; + +/// +/// A whose operator must be buckled to it. +/// +[RegisterComponent, NetworkedComponent] +[Access(typeof(VehicleSystem))] +public sealed partial class StrapVehicleComponent : Component; diff --git a/Content.Shared/Vehicle/Components/VehicleComponent.cs b/Content.Shared/Vehicle/Components/VehicleComponent.cs new file mode 100644 index 00000000000..98371371e30 --- /dev/null +++ b/Content.Shared/Vehicle/Components/VehicleComponent.cs @@ -0,0 +1,76 @@ +using Content.Shared.Damage; +using Content.Shared.Whitelist; +using JetBrains.Annotations; +using Robust.Shared.GameStates; +using Robust.Shared.Serialization; + +namespace Content.Shared.Vehicle.Components; + +/// +/// Vehicles are objects that have the behavior of moving when a player "operates" them. +/// The details of when the vehicle can operate and who the operator is are not defined here. +/// This simply contains the baseline behavior of the vehicle itself. +/// +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState] +[Access(typeof(VehicleSystem))] +public sealed partial class VehicleComponent : Component +{ + /// + /// The driver of this vehicle. + /// + [DataField, AutoNetworkedField] + public EntityUid? Operator; + + /// + /// Simple whitelist for determining who can operator this vehicle. + /// + [DataField, AutoNetworkedField] + public EntityWhitelist? OperatorWhitelist; + + /// + /// If true, damage to the vehicle will be transferred to the operator. + /// This damage is modified by + /// + [DataField, AutoNetworkedField] + public bool TransferDamage = true; + + /// + /// A damage modifier set that adjusts the damage passed from the vehicle to the operator. + /// + [DataField, AutoNetworkedField] + public DamageModifierSet? TransferDamageModifier; +} + +[Serializable, NetSerializable] +public enum VehicleVisuals : byte +{ + HasOperator, // The vehicle has a valid operator + CanRun, // The vehicle can be moved by the operator (turned on :flushed:) +} + +/// +/// Event raised on operator when they begin to operate a vehicle +/// Values are configured before this event is raised. +/// +[ByRefEvent, UsedImplicitly] +public readonly record struct OnVehicleEnteredEvent(Entity Vehicle, EntityUid Operator); + +/// +/// Event raised on operator when they stop operating a vehicle. +/// Values are configured after this event is raised. +/// +[ByRefEvent, UsedImplicitly] +public readonly record struct OnVehicleExitedEvent(Entity Vehicle, EntityUid Operator); + +/// +/// Event raised on vehicle after an operator is set. +/// New operator can be null. +/// +[ByRefEvent, UsedImplicitly] +public readonly record struct VehicleOperatorSetEvent(EntityUid? NewOperator, EntityUid? OldOperator); + +/// +/// Event raised on a vehicle to check if it can run/move around. +/// +[ByRefEvent, UsedImplicitly] +public record struct VehicleCanRunEvent(Entity Vehicle, bool CanRun = true); diff --git a/Content.Shared/Vehicle/Components/VehicleOperatorComponent.cs b/Content.Shared/Vehicle/Components/VehicleOperatorComponent.cs new file mode 100644 index 00000000000..74033dd4e1d --- /dev/null +++ b/Content.Shared/Vehicle/Components/VehicleOperatorComponent.cs @@ -0,0 +1,17 @@ +using Robust.Shared.GameStates; + +namespace Content.Shared.Vehicle.Components; + +/// +/// Tracking component for handling the operator of a given +/// +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState] +[Access(typeof(VehicleSystem))] +public sealed partial class VehicleOperatorComponent : Component +{ + /// + /// The vehicle we are currently operating. + /// + [DataField, AutoNetworkedField] + public EntityUid? Vehicle; +} diff --git a/Content.Shared/Vehicle/VehicleSystem.Key.cs b/Content.Shared/Vehicle/VehicleSystem.Key.cs new file mode 100644 index 00000000000..b931f7b8c01 --- /dev/null +++ b/Content.Shared/Vehicle/VehicleSystem.Key.cs @@ -0,0 +1,61 @@ +using Content.Shared.Vehicle.Components; +using Robust.Shared.Containers; + +namespace Content.Shared.Vehicle; + +public sealed partial class VehicleSystem +{ + public void InitializeKey() + { + SubscribeLocalEvent(OnGenericKeyedInsertAttempt); + SubscribeLocalEvent(OnGenericKeyedEntInserted); + SubscribeLocalEvent(OnGenericKeyedEntRemoved); + SubscribeLocalEvent(OnGenericKeyedCanRun); + } + + private void OnGenericKeyedInsertAttempt(Entity ent, ref ContainerIsInsertingAttemptEvent args) + { + if (args.Cancelled || !ent.Comp.PreventInvalidInsertion || args.Container.ID != ent.Comp.ContainerId) + return; + + if (_entityWhitelist.IsWhitelistPass(ent.Comp.KeyWhitelist, args.EntityUid)) + return; + + args.Cancel(); + } + + private void OnGenericKeyedEntInserted(Entity ent, ref EntInsertedIntoContainerMessage args) + { + if (args.Container.ID != ent.Comp.ContainerId) + return; + RefreshCanRun(ent.Owner); + } + + private void OnGenericKeyedEntRemoved(Entity ent, ref EntRemovedFromContainerMessage args) + { + if (args.Container.ID != ent.Comp.ContainerId) + return; + RefreshCanRun(ent.Owner); + } + + private void OnGenericKeyedCanRun(Entity ent, ref VehicleCanRunEvent args) + { + if (!args.CanRun) + return; + // We cannot run by default + args.CanRun = false; + + if (!_container.TryGetContainer(ent.Owner, ent.Comp.ContainerId, out var container)) + return; + + foreach (var contained in container.ContainedEntities) + { + if (_entityWhitelist.IsWhitelistFail(ent.Comp.KeyWhitelist, contained)) + continue; + + // If we find a valid key, permit running and exit early. + args.CanRun = true; + break; + } + } +} diff --git a/Content.Shared/Vehicle/VehicleSystem.Operator.cs b/Content.Shared/Vehicle/VehicleSystem.Operator.cs new file mode 100644 index 00000000000..5a79e4bcd5f --- /dev/null +++ b/Content.Shared/Vehicle/VehicleSystem.Operator.cs @@ -0,0 +1,56 @@ +using Content.Shared.Buckle.Components; +using Content.Shared.Vehicle.Components; +using Robust.Shared.Containers; + +namespace Content.Shared.Vehicle; + +public sealed partial class VehicleSystem +{ + public void InitializeOperator() + { + SubscribeLocalEvent(OnVehicleStrapped); + SubscribeLocalEvent(OnVehicleUnstrapped); + + SubscribeLocalEvent(OnContainerEntInserted); + SubscribeLocalEvent(OnContainerEntRemoved); + } + + private void OnVehicleStrapped(Entity ent, ref StrappedEvent args) + { + if (!TryComp(ent, out var vehicle)) + return; + TrySetOperator((ent, vehicle), args.Buckle); + } + + private void OnVehicleUnstrapped(Entity ent, ref UnstrappedEvent args) + { + if (!TryComp(ent, out var vehicle)) + return; + TrySetOperator((ent, vehicle), null); + } + + private void OnContainerEntInserted(Entity ent, ref EntInsertedIntoContainerMessage args) + { + if (args.Container.ID != ent.Comp.ContainerId) + return; + + if (!TryComp(ent, out var vehicle)) + return; + + TrySetOperator((ent, vehicle), args.Entity, removeExisting: false); + } + + private void OnContainerEntRemoved(Entity ent, ref EntRemovedFromContainerMessage args) + { + if (args.Container.ID != ent.Comp.ContainerId) + return; + + if (!TryComp(ent, out var vehicle)) + return; + + if (vehicle.Operator != args.Entity) + return; + + TryRemoveOperator((ent, vehicle)); + } +} diff --git a/Content.Shared/Vehicle/VehicleSystem.cs b/Content.Shared/Vehicle/VehicleSystem.cs new file mode 100644 index 00000000000..8228c5f3425 --- /dev/null +++ b/Content.Shared/Vehicle/VehicleSystem.cs @@ -0,0 +1,261 @@ +using System.Diagnostics.CodeAnalysis; +using Content.Shared.Access.Components; +using Content.Shared.ActionBlocker; +using Content.Shared.Actions; +using Content.Shared.Damage; +using Content.Shared.Movement.Components; +using Content.Shared.Movement.Events; +using Content.Shared.Movement.Systems; +using Content.Shared.Vehicle.Components; +using Content.Shared.Whitelist; +using JetBrains.Annotations; +using Robust.Shared.Containers; + +namespace Content.Shared.Vehicle; + +/// +/// Handles logic relating to vehicles. +/// +public sealed partial class VehicleSystem : EntitySystem +{ + [Dependency] private readonly ActionBlockerSystem _actionBlocker = default!; + [Dependency] private readonly SharedAppearanceSystem _appearance = default!; + [Dependency] private readonly SharedContainerSystem _container = default!; + [Dependency] private readonly DamageableSystem _damageable = default!; + [Dependency] private readonly EntityWhitelistSystem _entityWhitelist = default!; + [Dependency] private readonly SharedMoverController _mover = default!; + + /// + public override void Initialize() + { + InitializeOperator(); + InitializeKey(); + + SubscribeLocalEvent(OnBeforeDamageChanged); + SubscribeLocalEvent(OnVehicleUpdateCanMove); + SubscribeLocalEvent(OnVehicleShutdown); + SubscribeLocalEvent(OnVehicleGetAdditionalAccess); + + SubscribeLocalEvent(OnOperatorShutdown); + } + + /// + /// We subscribe to BeforeDamageChangedEvent so that we can access the damage value before the container is applied. + /// + private void OnBeforeDamageChanged(Entity ent, ref BeforeDamageChangedEvent args) + { + if (!ent.Comp.TransferDamage || !args.Damage.AnyPositive() || ent.Comp.Operator is not { } operatorUid) + return; + + var damage = DamageSpecifier.GetPositive(args.Damage); + + if (ent.Comp.TransferDamageModifier is { } modifierSet) + { + // Reduce damage to via the specified modifier, if provided. + damage = DamageSpecifier.ApplyModifierSet(damage, modifierSet); + } + + _damageable.TryChangeDamage(operatorUid, damage, origin: args.Origin); + } + + private void OnVehicleUpdateCanMove(Entity ent, ref UpdateCanMoveEvent args) + { + var ev = new VehicleCanRunEvent(ent); + RaiseLocalEvent(ent, ref ev); + if (!ev.CanRun) + args.Cancel(); + } + + private void OnVehicleShutdown(Entity ent, ref ComponentShutdown args) + { + TryRemoveOperator(ent); + } + + private void OnVehicleGetAdditionalAccess(Entity ent, ref GetAdditionalAccessEvent args) + { + // Vehicles inherit access from whoever is driving them + if (ent.Comp.Operator is { } operatorUid) + args.Entities.Add(operatorUid); + } + + private void OnOperatorShutdown(Entity ent, ref ComponentShutdown args) + { + TryRemoveOperator((ent, ent)); + } + + /// + /// Set the operator for a given vehicle + /// + /// The vehicle + /// The new operator. If null, will only remove the operator. + /// If true, will remove the current operator when setting the new one. + /// If the new operator was successfully able to be set + public bool TrySetOperator(Entity entity, EntityUid? uid, bool removeExisting = true) + { + if (entity.Comp.Operator == null && uid is null) + return false; + + // Do not run logic if the entity is already operating a vehicle. + // However, if they are operating *this* vehicle, return true (they are indeed the operator) + if (TryComp(uid, out var eOperator)) + return eOperator.Vehicle == entity.Owner; + + if (!removeExisting && entity.Comp.Operator is not null) + return false; + + if (uid != null && !CanOperate(entity.AsNullable(), uid.Value)) + return false; + + var oldOperator = entity.Comp.Operator; + + if (entity.Comp.Operator is { } currentOperator && TryComp(currentOperator, out var currentOperatorComponent)) + { + var exitEvent = new OnVehicleExitedEvent(entity, currentOperator); + RaiseLocalEvent(currentOperator, ref exitEvent); + + currentOperatorComponent.Vehicle = null; + RemCompDeferred(currentOperator); + RemCompDeferred(currentOperator); + } + + entity.Comp.Operator = uid; + + if (uid != null) + { + // AddComp used for noisy fail. This should never be an issue. + var vehicleOperator = AddComp(uid.Value); + vehicleOperator.Vehicle = entity.Owner; + Dirty(uid.Value, vehicleOperator); + + _mover.SetRelay(uid.Value, entity); + + var enterEvent = new OnVehicleEnteredEvent(entity, uid.Value); + RaiseLocalEvent(uid.Value, ref enterEvent); + } + else + { + RemCompDeferred(entity); + } + + RefreshCanRun((entity, entity.Comp)); + + var setEvent = new VehicleOperatorSetEvent(uid, oldOperator); + RaiseLocalEvent(entity, ref setEvent); + + Dirty(entity); + return true; + } + + /// + /// Attempts to remove the current operator from a vehicle + /// + /// The vehicle whose operator is being removed. + /// If the operator was removed successfully + [PublicAPI] + public bool TryRemoveOperator(Entity entity) + { + return TrySetOperator(entity, null, removeExisting: true); + } + + /// + /// From an operator, removes it from the vehicle + /// + /// The operator who is riding a vehicle + /// If the operator was removed successfully or if the entity was not operating a vehicle. + [PublicAPI] + public bool TryRemoveOperator(Entity operatorEntity) + { + if (!Resolve(operatorEntity, ref operatorEntity.Comp, false)) + return true; + + if (!TryComp(operatorEntity.Comp.Vehicle, out var vehicle)) + return true; + + return TrySetOperator((operatorEntity.Comp.Vehicle.Value, vehicle), null, removeExisting: true); + } + + /// + /// Attempts to get the current operator of a vehicle + /// + /// + /// + [PublicAPI] + public bool TryGetOperator(Entity entity, [NotNullWhen(true)] out Entity? operatorEnt) + { + operatorEnt = null; + if (!Resolve(entity, ref entity.Comp)) + return false; + + if (entity.Comp.Operator is not { } operatorUid) + return false; + + if (!TryComp(operatorUid, out var operatorComponent)) + return false; + + operatorEnt = (operatorUid, operatorComponent); + return true; + } + + /// + /// Returns the operator of the vehicle or none if there isn't one present + /// + public EntityUid? GetOperatorOrNull(Entity entity) + { + TryGetOperator(entity, out var operatorEnt); + return operatorEnt; + } + + /// + /// Checks if the current vehicle has an operator. + /// + [PublicAPI] + public bool HasOperator(Entity entity) + { + return TryGetOperator(entity, out _); + } + + /// + /// Checks if a given entity is capable of operating a vehicle. + /// Note that the general ability for a vehicle to run (keys, fuel, etc.) is not checked here. + /// This is *only* for checks on the user. + /// + public bool CanOperate(Entity entity, EntityUid uid) + { + if (!Resolve(entity, ref entity.Comp)) + return false; + + if (_entityWhitelist.IsWhitelistFail(entity.Comp.OperatorWhitelist, uid)) + return false; + + return _actionBlocker.CanConsciouslyPerformAction(uid); + } + + /// + /// Checks if the vehicle is capable of running (has keys, fuel, etc.) and caches the value. + /// Updates the appearance data. + /// + public void RefreshCanRun(Entity entity) + { + if (TerminatingOrDeleted(entity)) + return; + + if (!Resolve(entity, ref entity.Comp)) + return; + + _actionBlocker.UpdateCanMove(entity); + UpdateAppearance((entity, entity.Comp)); + } + + private void UpdateAppearance(Entity entity) + { + if (!TryComp(entity, out var appearance)) + return; + + if (TryComp(entity, out var inputMover)) + { + _appearance.SetData(entity, VehicleVisuals.CanRun, inputMover.CanMove, appearance); + } + + _appearance.SetData(entity, VehicleVisuals.HasOperator, entity.Comp.Operator is not null, appearance); + } +} diff --git a/Content.Shared/_Encore/Actions/HornActionEvent.cs b/Content.Shared/_Encore/Actions/HornActionEvent.cs new file mode 100644 index 00000000000..c46c4dbe137 --- /dev/null +++ b/Content.Shared/_Encore/Actions/HornActionEvent.cs @@ -0,0 +1,4 @@ +using Content.Shared.Actions; + +namespace Content.Shared._Encore.Actions; +public sealed partial class HornActionEvent : InstantActionEvent; diff --git a/Content.Shared/_Encore/Actions/SirenActionEvent.cs b/Content.Shared/_Encore/Actions/SirenActionEvent.cs new file mode 100644 index 00000000000..1d74c0f8181 --- /dev/null +++ b/Content.Shared/_Encore/Actions/SirenActionEvent.cs @@ -0,0 +1,5 @@ +using Content.Shared.Actions; + +namespace Content.Shared._Encore.Actions; + +public sealed partial class SirenActionEvent : InstantActionEvent; diff --git a/Content.Shared/_Encore/Vehicle/Components/VehicleComponent.cs b/Content.Shared/_Encore/Vehicle/Components/VehicleComponent.cs new file mode 100644 index 00000000000..4f4c6851f17 --- /dev/null +++ b/Content.Shared/_Encore/Vehicle/Components/VehicleComponent.cs @@ -0,0 +1,41 @@ +using Robust.Shared.Audio; + + +namespace Content.Shared.Vehicle.Components; + +public sealed partial class VehicleComponent : Component +{ + [ViewVariables] + public EntityUid? HornAction; + + [ViewVariables] + public EntityUid? SirenAction; + + public bool SirenEnabled = false; + + public EntityUid? SirenStream; + + /// + /// What sound to play when the driver presses the horn action (plays once) + /// + [DataField] + public SoundSpecifier? HornSound; + + /// + /// What sound to play when the driver presses the siren action (loops) + /// + [DataField] + public SoundSpecifier? SirenSound; + + // /// + // /// What sound to play when the driver presses the horn action (plays once) + // /// + // [DataField] + // public SoundSpecifier? HornSound = new SoundPathSpecifier("/Audio/Animals/goose_honk.ogg"); + // + // /// + // /// What sound to play when the driver presses the siren action (loops) + // /// + // [DataField] + // public SoundSpecifier? SirenSound = new SoundPathSpecifier("/Audio/Animals/goose_honk.ogg"); +} diff --git a/Content.Shared/_Encore/Vehicle/VehicleSystem.cs b/Content.Shared/_Encore/Vehicle/VehicleSystem.cs new file mode 100644 index 00000000000..a65205a2ef9 --- /dev/null +++ b/Content.Shared/_Encore/Vehicle/VehicleSystem.cs @@ -0,0 +1,91 @@ +using Content.Shared._Encore.Actions; +using Content.Shared.Actions; +using Content.Shared.Buckle.Components; +using Content.Shared.Vehicle.Components; +using Robust.Shared.Audio.Systems; +using Robust.Shared.Prototypes; + + +namespace Content.Shared._Encore.Vehicle; + + +public sealed class VehicleSystem : EntitySystem +{ + [Dependency] private readonly SharedAudioSystem _audio = default!; + [Dependency] private readonly SharedActionsSystem _actions = default!; + + public static readonly EntProtoId HornActionId = "ActionHorn"; + public static readonly EntProtoId SirenActionId = "ActionSiren"; + public override void Initialize() + { + SubscribeLocalEvent(OnHorn); + SubscribeLocalEvent(OnSiren); + SubscribeLocalEvent(OnEnteredVehicle); + SubscribeLocalEvent(OnExitVehicle); + } + + private void OnHorn(EntityUid uid, VehicleComponent component, InstantActionEvent args) + { + if (args.Handled || component.Operator != args.Performer || component.HornSound == null) + return; + + _audio.PlayPvs(component.HornSound, uid); + args.Handled = true; + } + + private void OnSiren(EntityUid uid, VehicleComponent component, InstantActionEvent args) + { + if (args.Handled || component.Operator != args.Performer || component.SirenSound == null) + return; + + if (component.SirenEnabled) + { + #pragma warning disable RA0002 + component.SirenStream = _audio.Stop(component.SirenStream); + } + else + { + component.SirenStream = _audio.PlayPvs(component.SirenSound, uid)?.Entity; + } + + component.SirenEnabled = !component.SirenEnabled; + args.Handled = true; + } + + private void OnEnteredVehicle(Entity ent, ref StrappedEvent args) + { + if (ent.Comp.Operator != null) + { + AddHorns(ent); + } + } + + private void OnExitVehicle(Entity ent, ref UnstrappedEvent args) + { + var vehicleComp = ent.Comp; + var driver = args.Buckle.Owner; + + if (vehicleComp.HornAction != null) + _actions.RemoveAction((EntityUid)driver, vehicleComp.HornAction); + + if (vehicleComp.SirenAction != null) + _actions.RemoveAction((EntityUid)driver, vehicleComp.SirenAction); + } + + private void AddHorns(Entity entity) + { + var vehicleComp = entity.Comp; + var driver = entity.Comp.Operator; + if (driver == null) + { + return; + } + + if (vehicleComp.HornSound != null) + _actions.AddAction((EntityUid)driver, ref vehicleComp.HornAction, HornActionId, entity); + + if (vehicleComp.SirenSound != null) + _actions.AddAction((EntityUid)driver, ref vehicleComp.SirenAction, SirenActionId, entity); + } + +} diff --git a/Content.Shared/_Goobstation/Vehicles/SharedVehicleComponent.cs b/Content.Shared/_Goobstation/Vehicles/SharedVehicleComponent.cs deleted file mode 100644 index 4e1984c2674..00000000000 --- a/Content.Shared/_Goobstation/Vehicles/SharedVehicleComponent.cs +++ /dev/null @@ -1,73 +0,0 @@ -using Robust.Shared.Audio; -using Robust.Shared.GameStates; -using Robust.Shared.Serialization; - -namespace Content.Shared.Vehicles; - -[RegisterComponent, NetworkedComponent] -public sealed partial class VehicleComponent : Component -{ - [ViewVariables] - public EntityUid? Driver; - - [ViewVariables] - public EntityUid? HornAction; - - [ViewVariables] - public EntityUid? SirenAction; - - public bool SirenEnabled = false; - - public EntityUid? SirenStream; - - /// - /// If non-zero how many virtual items to spawn on the driver - /// unbuckles them if they dont have enough - /// - [DataField] - public int RequiredHands = 1; - - /// - /// Will the vehicle move when a driver buckles - /// - [DataField] - public bool EngineRunning = false; - - /// - /// What sound to play when the driver presses the horn action (plays once) - /// - [DataField] - public SoundSpecifier? HornSound; - - /// - /// What sound to play when the driver presses the siren action (loops) - /// - [DataField] - public SoundSpecifier? SirenSound; - - /// - /// If they should be rendered ontop of the vehicle if true or behind - /// - [DataField] - public VehicleRenderOver RenderOver = VehicleRenderOver.None; -} -[Serializable, NetSerializable] -public enum VehicleState : byte -{ - Animated, - DrawOver -} - -[Serializable, NetSerializable, Flags] -public enum VehicleRenderOver -{ - None = 0, - North = 1, - NorthEast = 2, - East = 4, - SouthEast = 8, - South = 16, - SouthWest = 32, - West = 64, - NorthWest = 128, -} diff --git a/Content.Shared/_Goobstation/Vehicles/SharedVehicleSystem.cs b/Content.Shared/_Goobstation/Vehicles/SharedVehicleSystem.cs deleted file mode 100644 index 2d627bb06c0..00000000000 --- a/Content.Shared/_Goobstation/Vehicles/SharedVehicleSystem.cs +++ /dev/null @@ -1,236 +0,0 @@ -using Content.Shared.Access.Components; -using Content.Shared.Access.Systems; -using Content.Shared.Actions; -using Content.Shared.Audio; -using Content.Shared.Buckle; -using Content.Shared.Buckle.Components; -using Content.Shared.Hands; -using Content.Shared.Inventory.VirtualItem; -using Content.Shared.Movement.Components; -using Content.Shared.Movement.Systems; -using Robust.Shared.Audio; -using Robust.Shared.Audio.Systems; -using Robust.Shared.Containers; -using Robust.Shared.Prototypes; - -namespace Content.Shared.Vehicles; - -public abstract partial class SharedVehicleSystem : EntitySystem -{ - [Dependency] private readonly AccessReaderSystem _access = default!; - [Dependency] private readonly SharedActionsSystem _actions = default!; - [Dependency] private readonly SharedAmbientSoundSystem _ambientSound = default!; - [Dependency] private readonly SharedAppearanceSystem _appearance = default!; - [Dependency] private readonly SharedAudioSystem _audio = default!; - [Dependency] private readonly SharedBuckleSystem _buckle = default!; - [Dependency] private readonly SharedMoverController _mover = default!; - [Dependency] private readonly SharedVirtualItemSystem _virtualItem = default!; - - public static readonly EntProtoId HornActionId = "ActionHorn"; - public static readonly EntProtoId SirenActionId = "ActionSiren"; - - public override void Initialize() - { - base.Initialize(); - SubscribeLocalEvent(OnInit); - SubscribeLocalEvent(OnRemove); - SubscribeLocalEvent(OnStrapAttempt); - SubscribeLocalEvent(OnStrapped); - SubscribeLocalEvent(OnUnstrapped); - SubscribeLocalEvent(OnDropped); - - SubscribeLocalEvent(OnInsert); - SubscribeLocalEvent(OnEject); - - SubscribeLocalEvent(OnHorn); - SubscribeLocalEvent(OnSiren); - } - - private void OnInit(EntityUid uid, VehicleComponent component, ComponentInit args) - { - _appearance.SetData(uid, VehicleState.Animated, component.EngineRunning); - _appearance.SetData(uid, VehicleState.DrawOver, false); - } - - private void OnRemove(EntityUid uid, VehicleComponent component, ComponentRemove args) - { - if (component.Driver == null) - return; - - _buckle.TryUnbuckle(component.Driver.Value, component.Driver.Value); - Dismount(component.Driver.Value, uid); - _appearance.SetData(uid, VehicleState.DrawOver, false); - } - - private void OnInsert(EntityUid uid, VehicleComponent component, ref EntInsertedIntoContainerMessage args) - { - if (HasComp(args.Entity)) - return; - - component.EngineRunning = true; - _appearance.SetData(uid, VehicleState.Animated, true); - - _ambientSound.SetAmbience(uid, true); - - if (component.Driver == null) - return; - - Mount(component.Driver.Value, uid); - } - - private void OnEject(EntityUid uid, VehicleComponent component, ref EntRemovedFromContainerMessage args) - { - component.EngineRunning = false; - _appearance.SetData(uid, VehicleState.Animated, false); - - _ambientSound.SetAmbience(uid, false); - - if (component.Driver == null) - return; - - Dismount(component.Driver.Value, uid); - } - - private void OnHorn(EntityUid uid, VehicleComponent component, InstantActionEvent args) - { - if (args.Handled == true || component.Driver != args.Performer || component.HornSound == null) - return; - - _audio.PlayPvs(component.HornSound, uid); - args.Handled = true; - } - - private void OnSiren(EntityUid uid, VehicleComponent component, InstantActionEvent args) - { - if (args.Handled == true || component.Driver != args.Performer || component.SirenSound == null) - return; - - if (component.SirenEnabled) - { - component.SirenStream = _audio.Stop(component.SirenStream); - } - else - { - component.SirenStream = _audio.PlayPvs(component.SirenSound, uid)?.Entity; - } - - component.SirenEnabled = !component.SirenEnabled; - args.Handled = true; - } - - - private void OnStrapAttempt(Entity ent, ref StrapAttemptEvent args) - { - var driver = args.Buckle.Owner; // i dont want to re write this shit 100 fucking times - - if (ent.Comp.Driver != null) - { - args.Cancelled = true; - return; - } - - if (ent.Comp.RequiredHands != 0) - { - for (int hands = 0; hands < ent.Comp.RequiredHands; hands++) - { - if (!_virtualItem.TrySpawnVirtualItemInHand(ent.Owner, driver, false)) - { - args.Cancelled = true; - _virtualItem.DeleteInHandsMatching(driver, ent.Owner); - return; - } - } - } - - AddHorns(driver, ent); - } - - private void OnStrapped(Entity ent, ref StrappedEvent args) - { - var driver = args.Buckle.Owner; - - if (!TryComp(driver, out MobMoverComponent? mover) || ent.Comp.Driver != null) - return; - - ent.Comp.Driver = driver; - _appearance.SetData(ent.Owner, VehicleState.DrawOver, true); - - if (!ent.Comp.EngineRunning) - return; - - Mount(driver, ent.Owner); - } - - private void OnUnstrapped(Entity ent, ref UnstrappedEvent args) - { - if (ent.Comp.Driver != args.Buckle.Owner) - return; - - Dismount(args.Buckle.Owner, ent); - _appearance.SetData(ent.Owner, VehicleState.DrawOver, false); - } - - private void OnDropped(EntityUid uid, VehicleComponent comp, VirtualItemDeletedEvent args) - { - if (comp.Driver != args.User) - return; - - _buckle.TryUnbuckle(args.User, args.User); - - Dismount(args.User, uid); - _appearance.SetData(uid, VehicleState.DrawOver, false); - } - - private void AddHorns(EntityUid driver, EntityUid vehicle) - { - if (!TryComp(vehicle, out var vehicleComp)) - return; - - if (vehicleComp.HornSound != null) - _actions.AddAction(driver, ref vehicleComp.HornAction, HornActionId, vehicle); - - if (vehicleComp.SirenSound != null) - _actions.AddAction(driver, ref vehicleComp.SirenAction, SirenActionId, vehicle); - } - - private void Mount(EntityUid driver, EntityUid vehicle) - { - if (TryComp(vehicle, out var accessComp)) - { - var accessSources = _access.FindPotentialAccessItems(driver); - var access = _access.FindAccessTags(driver, accessSources); - - foreach (var tag in access) - { - accessComp.Tags.Add(tag); - } - } - - _mover.SetRelay(driver, vehicle); - } - - private void Dismount(EntityUid driver, EntityUid vehicle) - { - if (!TryComp(vehicle, out var vehicleComp) || vehicleComp.Driver != driver) - return; - - RemComp(driver); - - vehicleComp.Driver = null; - - if (vehicleComp.HornAction != null) - _actions.RemoveAction(driver, vehicleComp.HornAction); - - if (vehicleComp.SirenAction != null) - _actions.RemoveAction(driver, vehicleComp.SirenAction); - - _virtualItem.DeleteInHandsMatching(driver, vehicle); - - if (TryComp(vehicle, out var accessComp)) - accessComp.Tags.Clear(); - } -} - -public sealed partial class HornActionEvent : InstantActionEvent; - -public sealed partial class SirenActionEvent : InstantActionEvent; diff --git a/Resources/Locale/en-US/_Goobstation/vehicle/vehicle.ftl b/Resources/Locale/en-US/_Goobstation/vehicle/vehicle.ftl deleted file mode 100644 index 36c2fbfb699..00000000000 --- a/Resources/Locale/en-US/_Goobstation/vehicle/vehicle.ftl +++ /dev/null @@ -1 +0,0 @@ -vehicle-slot-component-slot-name-keys = Keys diff --git a/Resources/Locale/en-US/vehicle/vehicle.ftl b/Resources/Locale/en-US/vehicle/vehicle.ftl new file mode 100644 index 00000000000..44353580475 --- /dev/null +++ b/Resources/Locale/en-US/vehicle/vehicle.ftl @@ -0,0 +1 @@ +vehicle-slot-component-slot-name-keys = keys diff --git a/Resources/Prototypes/Catalog/VendingMachines/Inventories/medical.yml b/Resources/Prototypes/Catalog/VendingMachines/Inventories/medical.yml index 7d1cc98c935..fe64df89c98 100644 --- a/Resources/Prototypes/Catalog/VendingMachines/Inventories/medical.yml +++ b/Resources/Prototypes/Catalog/VendingMachines/Inventories/medical.yml @@ -14,4 +14,3 @@ ClothingEyesHudMedical: 2 ClothingEyesEyepatchHudMedical: 2 ParamedHypo: 2 #Goobstation - ParamedHypo - VehicleWheelchairFolded: 3 # Goobstation - Vehicles diff --git a/Resources/Prototypes/Entities/Clothing/Shoes/magboots.yml b/Resources/Prototypes/Entities/Clothing/Shoes/magboots.yml index 49acb2a7328..c9b4963328b 100644 --- a/Resources/Prototypes/Entities/Clothing/Shoes/magboots.yml +++ b/Resources/Prototypes/Entities/Clothing/Shoes/magboots.yml @@ -207,7 +207,6 @@ moleUsage: 0.00085 - type: CanMoveInAir - type: InputMover - toParent: true - type: MovementSpeedModifier weightlessAcceleration: 1 weightlessFriction: 0.3 diff --git a/Resources/Prototypes/Entities/Objects/Devices/vehicles.yml b/Resources/Prototypes/Entities/Objects/Devices/vehicles.yml new file mode 100644 index 00000000000..09d802a2e02 --- /dev/null +++ b/Resources/Prototypes/Entities/Objects/Devices/vehicles.yml @@ -0,0 +1,177 @@ +- type: entity + id: BaseVehicleStrap + abstract: true + components: + - type: Sprite + noRot: true + - type: InputMover + - type: Clickable + - type: InteractionOutline + - type: Pullable + - type: Physics + bodyType: KinematicController + - type: Vehicle + - type: StrapVehicle + - type: Strap + - type: MovementSpeedModifier + weightlessModifier: 0 + acceleration: 2 + friction: 2 + frictionNoInput: 6 + baseWalkSpeed: 4.5 + baseSprintSpeed: 6 + - type: Fixtures + fixtures: + fix1: + shape: + !type:PhysShapeCircle + radius: 0.4 + density: 360 + restitution: 0.0 + mask: + - MobMask + layer: + - TableLayer + - type: Tag + tags: + - DoorBumpOpener + - FootstepSound + +- type: entity + id: VehicleWheelchair + parent: [BaseVehicleStrap, BaseFoldable, BaseItem] + name: wheelchair + description: A chair with big wheels. It looks like you can move in these on your own. + components: + - type: Sprite + sprite: Objects/Vehicles/wheelchair.rsi + layers: + - state: wheelchair + map: ["unfoldedLayer"] + - state: wheelchair_folded + map: ["foldedLayer"] + visible: false + - type: Appearance + - type: Item + size: Ginormous + - type: Damageable + damageContainer: Inorganic + damageModifierSet: Metallic + - type: MovementSpeedModifier + baseWalkSpeed: 2 + baseSprintSpeed: 2 + - type: Strap + buckleOffset: "0,-0.05" + - type: Fixtures + fixtures: + fix1: + shape: + !type:PhysShapeCircle + radius: 0.2 + density: 360 + restitution: 0.0 + mask: + - MobMask + layer: + - TableLayer + - type: StaticPrice + price: 70 + +- type: entity + id: VehicleJanicart + parent: BaseVehicleStrap + name: janicart + description: The janitor's trusty steed. + components: + - type: Sprite + sprite: Objects/Vehicles/janicart.rsi + drawdepth: Mobs # fixes layering like magic. + layers: + - state: vehicle + map: ["movement"] + - type: SpriteMovement + movementLayers: + movement: + state: vehicle-moving + noMovementLayers: + movement: + state: vehicle + - type: GenericKeyedVehicle + containerId: key_slot + keyWhitelist: + tags: + - JanicartKeys + - type: UnpoweredFlashlight + - type: PointLight + enabled: false + radius: 3.5 + softness: 2 + mask: /Textures/Effects/LightMasks/cone.png + autoRot: true + - type: Destructible + thresholds: + - trigger: + !type:DamageTrigger + damage: 500 + behaviors: + - !type:DoActsBehavior + acts: [ "Destruction" ] + - trigger: + !type:DamageTrigger + damage: 250 + behaviors: + - !type:DoActsBehavior + acts: ["Destruction"] + - !type:PlaySoundBehavior + sound: + collection: MetalGlassBreak + - !type:ExplodeBehavior + #- !type:SpawnEntitiesBehavior # in future should also emit a cloud of hot gas + # spawn: + # VehicleJanicartDestroyed: + # min: 1 + # max: 1 + - type: Strap + buckleOffset: "0.01,0.15" + modifyBuckleDrawDepth: false + - type: ItemSlots + slots: + key_slot: + name: vehicle-slot-component-slot-name-keys + whitelist: + tags: + - JanicartKeys + insertSound: + path: /Audio/Effects/Vehicle/vehiclestartup.ogg + params: + volume: -3 + trashbag_slot: + name: janitorial-trolley-slot-component-slot-name-trashbag + whitelist: + tags: + - TrashBag + - type: ContainerContainer + containers: + key_slot: !type:ContainerSlot + trashbag_slot: !type:ContainerSlot + - type: ItemMapper + mapLayers: + storage: + whitelist: + tags: + - TrashBag + sprite: Objects/Vehicles/janicart.rsi + +- type: entity + parent: BaseItem + id: VehicleKeyJanicart + name: janicart keys + description: Interesting design. + components: + - type: Sprite + sprite: Objects/Vehicles/janicart.rsi + state: keys + - type: Item + size: Small + - type: Tag + tags: [ JanicartKeys ] diff --git a/Resources/Prototypes/Entities/Objects/Specific/Mech/mechs.yml b/Resources/Prototypes/Entities/Objects/Specific/Mech/mechs.yml index 90f048bafd8..bbed0199f5b 100644 --- a/Resources/Prototypes/Entities/Objects/Specific/Mech/mechs.yml +++ b/Resources/Prototypes/Entities/Objects/Specific/Mech/mechs.yml @@ -6,6 +6,9 @@ - type: MobMover - type: Mech - type: MechAir + - type: Vehicle + - type: ContainerVehicle + containerId: "mech-pilot-slot" - type: AirFilter # everything except oxygen and nitrogen gases: @@ -163,8 +166,16 @@ baseState: ripley openState: ripley-open brokenState: ripley-broken - mechToPilotDamageMultiplier: 0.75 - pilotWhitelist: + - type: Vehicle + transferDamageModifier: + coefficients: + Blunt: 0.75 + Slash: 0.75 + Piercing: 0.75 + Cold: 0.75 + Heat: 0.75 + Shock: 0.75 + operatorWhitelist: components: - HumanoidAppearance - type: MeleeWeapon @@ -209,9 +220,16 @@ baseState: honker openState: honker-open brokenState: honker-broken - mechToPilotDamageMultiplier: 0.5 - airtight: true # Goobstation - Space Honk is real. - pilotWhitelist: + - type: Vehicle + transferDamageModifier: + coefficients: + Blunt: 0.5 + Slash: 0.5 + Piercing: 0.5 + Cold: 0.5 + Heat: 0.5 + Shock: 0.5 + operatorWhitelist: components: - HumanoidAppearance @@ -245,10 +263,18 @@ baseState: hamtr openState: hamtr-open brokenState: hamtr-broken - mechToPilotDamageMultiplier: 0.2 maxEquipmentAmount: 2 airtight: true - pilotWhitelist: + - type: Vehicle + transferDamageModifier: + coefficients: + Blunt: 0.2 + Slash: 0.2 + Piercing: 0.2 + Cold: 0.2 + Heat: 0.2 + Shock: 0.2 + operatorWhitelist: tags: - Hamster - type: MeleeWeapon @@ -307,16 +333,21 @@ baseState: vim openState: vim-open brokenState: vim-broken - maxEquipmentAmount: 5 - # keep mouse safe - mechToPilotDamageMultiplier: 0.1 + maxEquipmentAmount: 0 airtight: true - pilotWhitelist: + - type: Vehicle + # keep mouse safe + transferDamageModifier: + coefficients: + Blunt: 0.1 + Slash: 0.1 + Piercing: 0.1 + Cold: 0.1 + Heat: 0.1 + Shock: 0.1 + operatorWhitelist: tags: - VimPilot - equipmentWhitelist: - tags: - - CombatMech - type: MeleeWeapon hidden: true attackRate: 1.25 diff --git a/Resources/Prototypes/Entities/Objects/Tools/jetpacks.yml b/Resources/Prototypes/Entities/Objects/Tools/jetpacks.yml index e0e68f80928..804c0cafade 100644 --- a/Resources/Prototypes/Entities/Objects/Tools/jetpacks.yml +++ b/Resources/Prototypes/Entities/Objects/Tools/jetpacks.yml @@ -26,7 +26,6 @@ description: It's a jetpack. It can hold 5 L of gas. components: - type: InputMover - toParent: true - type: MovementSpeedModifier weightlessAcceleration: 1 weightlessFriction: 0.3 diff --git a/Resources/Prototypes/Entities/Structures/Furniture/chairs.yml b/Resources/Prototypes/Entities/Structures/Furniture/chairs.yml index 0b6df61bb41..8a1accd2a0a 100644 --- a/Resources/Prototypes/Entities/Structures/Furniture/chairs.yml +++ b/Resources/Prototypes/Entities/Structures/Furniture/chairs.yml @@ -89,7 +89,7 @@ name: chair id: RollingOfficeChairBase description: Scoot from desk to desk because standing is for interns. - parent: BaseVehicle + parent: BaseVehicleStrap abstract: true components: - type: Anchorable @@ -102,9 +102,6 @@ position: Stand buckleOffset: "0,-0.05" - type: Vehicle - requiredHands: 0 - engineRunning: true - renderOver: South, SouthEast, SouthWest - type: MovementSpeedModifier acceleration: 10 friction: 3.5 diff --git a/Resources/Prototypes/Entities/Structures/Storage/Closets/big_boxes.yml b/Resources/Prototypes/Entities/Structures/Storage/Closets/big_boxes.yml index ac7f053b631..6009453c64c 100644 --- a/Resources/Prototypes/Entities/Structures/Storage/Closets/big_boxes.yml +++ b/Resources/Prototypes/Entities/Structures/Storage/Closets/big_boxes.yml @@ -22,6 +22,9 @@ - MobLayer hard: true - type: Pullable + - type: Vehicle + - type: ContainerVehicle + containerId: entity_storage - type: CardboardBox effectSound: /Audio/Effects/chime.ogg - type: InputMover @@ -107,4 +110,4 @@ - type: EntityStorage isCollidableWhenOpen: false openOnMove: false - airtight: false \ No newline at end of file + airtight: false diff --git a/Resources/Prototypes/Recipes/Lathes/Packs/medical.yml b/Resources/Prototypes/Recipes/Lathes/Packs/medical.yml index 35fddf9fd5d..d9709c637c5 100644 --- a/Resources/Prototypes/Recipes/Lathes/Packs/medical.yml +++ b/Resources/Prototypes/Recipes/Lathes/Packs/medical.yml @@ -66,9 +66,6 @@ - RollerBedSpawnFolded - CheapRollerBedSpawnFolded - EmergencyRollerBedSpawnFolded - # EE EDIT START - - VehicleWheelchairFolded - # EE EDIT END - type: latheRecipePack id: MedicalClothingStatic diff --git a/Resources/Prototypes/_Encore/Actions/types.yml b/Resources/Prototypes/_Encore/Actions/types.yml new file mode 100644 index 00000000000..93c14246540 --- /dev/null +++ b/Resources/Prototypes/_Encore/Actions/types.yml @@ -0,0 +1,19 @@ +- type: entity + id: ActionHorn + name: Honk! + description: Beep the horn at whoever you will run over. + components: + - type: InstantAction + useDelay: 1 + icon: { sprite: Objects/Fun/bikehorn.rsi, state: icon } + event: !type:HornActionEvent + +- type: entity + id: ActionSiren + name: Siren + description: Alert your victim to your presence. + components: + - type: InstantAction + useDelay: 1 + icon: { sprite: Objects/Fun/bikehorn.rsi, state: icon } + event: !type:SirenActionEvent diff --git a/Resources/Prototypes/_Goobstation/Actions/types.yml b/Resources/Prototypes/_Goobstation/Actions/types.yml index 6c9c43cff67..93ae697adb5 100644 --- a/Resources/Prototypes/_Goobstation/Actions/types.yml +++ b/Resources/Prototypes/_Goobstation/Actions/types.yml @@ -13,23 +13,3 @@ sprite: /Prototypes/_Goobstation/Textures/Effects/bluespace_lifeline.rsi state: bluespace_lifeline event: !type:ActivateImplantEvent - -- type: entity - id: ActionHorn - name: Honk! - description: Beep the horn at whoever you will run over. - components: - - type: InstantAction - useDelay: 1 - icon: { sprite: Objects/Fun/bikehorn.rsi, state: icon } - event: !type:HornActionEvent - -- type: entity - id: ActionSiren - name: Siren - description: Alert your victim to your presence. - components: - - type: InstantAction - useDelay: 1 - icon: { sprite: Objects/Fun/bikehorn.rsi, state: icon } - event: !type:SirenActionEvent diff --git a/Resources/Prototypes/_Goobstation/Catalog/Fills/Crates/fun.yml b/Resources/Prototypes/_Goobstation/Catalog/Fills/Crates/fun.yml index 860b80580ae..077879cd5f2 100644 --- a/Resources/Prototypes/_Goobstation/Catalog/Fills/Crates/fun.yml +++ b/Resources/Prototypes/_Goobstation/Catalog/Fills/Crates/fun.yml @@ -3,20 +3,10 @@ parent: CrateLivestock name: ATV crate description: An Absolutely Taxable Vehicle to help cargo with hauling. - components: - - type: StorageFill - contents: - - id: VehicleATV - - id: VehicleKeyATV - type: entity id: CrateFunSyndicateSegway parent: CrateLivestock name: Syndicate segway crate description: A crate containing a two-wheeler that will help you escape from the security officers. Or not. - components: - - type: StorageFill - contents: - - id: VehicleSyndicateSegway - - id: VehicleKeySyndicateSegway diff --git a/Resources/Prototypes/_Goobstation/Entities/Markers/Spawners/vehicles.yml b/Resources/Prototypes/_Goobstation/Entities/Markers/Spawners/vehicles.yml index e97d8efcd05..3d37db6bdbd 100644 --- a/Resources/Prototypes/_Goobstation/Entities/Markers/Spawners/vehicles.yml +++ b/Resources/Prototypes/_Goobstation/Entities/Markers/Spawners/vehicles.yml @@ -1,31 +1,3 @@ -- type: entity - name: Secway Spawner - id: SpawnVehicleSecway - parent: MarkerBase - components: - - type: Sprite - layers: - - state: green - - sprite: Objects/Vehicles/secway.rsi - state: keys - - type: ConditionalSpawner - prototypes: - - VehicleSecway - -- type: entity - name: ATV Spawner - id: SpawnVehicleATV - parent: MarkerBase - components: - - type: Sprite - layers: - - state: green - - sprite: Objects/Vehicles/atv.rsi - state: keys - - type: ConditionalSpawner - prototypes: - - VehicleATV - - type: entity name: Janicart Spawner id: SpawnVehicleJanicart @@ -49,21 +21,7 @@ layers: - state: green - sprite: Objects/Vehicles/wheelchair.rsi - state: vehicle + state: wheelchair - type: ConditionalSpawner prototypes: - VehicleWheelchair - -- type: entity - name: Wheelchair [Folded] Spawner - id: SpawnVehicleWheelchairFolded - parent: MarkerBase - components: - - type: Sprite - layers: - - state: green - - sprite: Objects/Vehicles/wheelchair.rsi - state: vehicle_folded - - type: ConditionalSpawner - prototypes: - - VehicleWheelchairFolded diff --git a/Resources/Prototypes/_Goobstation/Entities/Objects/Specific/Mech/mechs.yml b/Resources/Prototypes/_Goobstation/Entities/Objects/Specific/Mech/mechs.yml index fb4c6b5deda..5cf163226ee 100644 --- a/Resources/Prototypes/_Goobstation/Entities/Objects/Specific/Mech/mechs.yml +++ b/Resources/Prototypes/_Goobstation/Entities/Objects/Specific/Mech/mechs.yml @@ -4,9 +4,6 @@ categories: [ HideSpawnMenu ] components: - type: Mech - equipmentWhitelist: - tags: - - CombatMech - type: entity id: IndustrialMech @@ -14,9 +11,6 @@ categories: [ HideSpawnMenu ] components: - type: Mech - equipmentWhitelist: - tags: - - IndustrialMech - type: entity id: SpecialMech @@ -24,9 +18,6 @@ categories: [ HideSpawnMenu ] components: - type: Mech - equipmentWhitelist: - tags: - - SpecialMech - type: entity id: SmallMech @@ -34,9 +25,6 @@ categories: [ HideSpawnMenu ] components: - type: Mech - equipmentWhitelist: - tags: - - SmallMech # Ripley MK-II - type: entity @@ -62,11 +50,7 @@ baseState: ripleymkii openState: ripleymkii-open brokenState: ripleymkii-broken - mechToPilotDamageMultiplier: 0.4 airtight: true - pilotWhitelist: - components: - - HumanoidAppearance - type: MeleeWeapon hidden: true attackRate: 1 @@ -85,7 +69,7 @@ - DoorBumpOpener - FootstepSound - RipleyMkII - + - type: entity id: MechRipley2Battery parent: MechRipley2 @@ -120,11 +104,7 @@ baseState: clarke openState: clarke-open brokenState: clarke-broken - mechToPilotDamageMultiplier: 0.5 airtight: true - pilotWhitelist: - components: - - HumanoidAppearance - type: MeleeWeapon hidden: true attackRate: 1 @@ -191,11 +171,7 @@ baseState: gygax openState: gygax-open brokenState: gygax-broken - mechToPilotDamageMultiplier: 0.3 airtight: true - pilotWhitelist: - components: - - HumanoidAppearance - type: MeleeWeapon hidden: true attackRate: 1 @@ -204,7 +180,7 @@ Blunt: 25 Structural: 180 - type: CanMoveInAir - - type: MovementAlwaysTouching + - type: MovementAlwaysTouching - type: MovementSpeedModifier baseWalkSpeed: 2 baseSprintSpeed: 2.6 @@ -250,12 +226,8 @@ baseState: durand openState: durand-open brokenState: durand-broken - mechToPilotDamageMultiplier: 0.25 airtight: true maxIntegrity: 400 - pilotWhitelist: - components: - - HumanoidAppearance - type: MeleeWeapon hidden: true attackRate: 1 @@ -317,13 +289,9 @@ baseState: marauder openState: marauder-open brokenState: marauder-broken - mechToPilotDamageMultiplier: 0.1 airtight: true maxIntegrity: 500 maxEquipmentAmount: 4 - pilotWhitelist: - components: - - HumanoidAppearance - type: MeleeWeapon hidden: true attackRate: 1 @@ -390,13 +358,9 @@ baseState: seraph openState: seraph-open brokenState: seraph-broken - mechToPilotDamageMultiplier: 0.05 airtight: true maxIntegrity: 550 maxEquipmentAmount: 5 - pilotWhitelist: - components: - - HumanoidAppearance - type: MeleeWeapon hidden: true attackRate: 1 @@ -466,13 +430,9 @@ baseState: darkgygax openState: darkgygax-open brokenState: darkgygax-broken - mechToPilotDamageMultiplier: 0.15 airtight: true maxIntegrity: 300 maxEquipmentAmount: 4 - pilotWhitelist: - components: - - HumanoidAppearance - type: MeleeWeapon hidden: true attackRate: 1 @@ -539,13 +499,9 @@ baseState: mauler openState: mauler-open brokenState: mauler-broken - mechToPilotDamageMultiplier: 0.1 airtight: true maxIntegrity: 500 maxEquipmentAmount: 5 - pilotWhitelist: - components: - - HumanoidAppearance - type: MeleeWeapon hidden: true attackRate: 1 diff --git a/Resources/Prototypes/_Goobstation/Entities/Objects/Vehicles/keys.yml b/Resources/Prototypes/_Goobstation/Entities/Objects/Vehicles/keys.yml deleted file mode 100644 index 9af4a6c062d..00000000000 --- a/Resources/Prototypes/_Goobstation/Entities/Objects/Vehicles/keys.yml +++ /dev/null @@ -1,67 +0,0 @@ -- type: entity - parent: BaseItem - id: BaseKey - abstract: true - categories: [ HideSpawnMenu ] - components: - - type: Item - size: Tiny - - type: Tag - tags: - - VehicleKey - -- type: entity - parent: BaseKey - id: VehicleKeySecway - name: secway keys - description: The keys to the future. - components: - - type: Sprite - sprite: Objects/Vehicles/secway.rsi - state: keys - - type: Tag - tags: - - VehicleKey - - SecwayKeys - -- type: entity - parent: BaseKey - id: VehicleKeySyndicateSegway - name: syndicate segway keys - description: Patterned after the iconic EMAG design. - components: - - type: Sprite - sprite: Objects/Vehicles/syndicatesegway.rsi - state: keys - - type: Tag - tags: - - VehicleKey - - SyndicateSegwayKeys - -- type: entity - parent: BaseKey - id: VehicleKeyATV - name: ATV keys - description: Think this looks like just one key? ATV keys means "actually two vehicle keys." - components: - - type: Sprite - sprite: Objects/Vehicles/atv.rsi - state: keys - - type: Tag - tags: - - VehicleKey - - ATVKeys - -- type: entity - parent: BaseKey - id: VehicleKeyJanicart - name: janicart keys - description: Interesting design. - components: - - type: Sprite - sprite: Objects/Vehicles/janicart.rsi - state: keys - - type: Tag - tags: - - VehicleKey - - JanicartKeys diff --git a/Resources/Prototypes/_Goobstation/Entities/Objects/Vehicles/vehicles.yml b/Resources/Prototypes/_Goobstation/Entities/Objects/Vehicles/vehicles.yml deleted file mode 100644 index b0232354255..00000000000 --- a/Resources/Prototypes/_Goobstation/Entities/Objects/Vehicles/vehicles.yml +++ /dev/null @@ -1,236 +0,0 @@ -- type: entity - id: BaseVehicle - abstract: true - save: false - categories: [ HideSpawnMenu ] - components: - - type: Vehicle - renderOver: East, SouthEast, South, SouthWest, West - - type: Strap - position: Stand - - type: Appearance - - type: AmbientSound - sound: "/Audio/Effects/Vehicle/vehicleengineidle.ogg" - range: 10 - volume: -10 - enabled: false - - type: InputMover - - type: Clickable - - type: InteractionOutline - - type: Access - - type: Physics - bodyType: Dynamic - - type: Fixtures - fixtures: - fix1: - shape: - !type:PhysShapeCircle - radius: 0.45 - density: 100 - mask: - - MobMask - layer: - - MobLayer - hard: true - - type: Pullable - - type: Damageable - damageContainer: Inorganic - damageModifierSet: Metallic - - type: Destructible - thresholds: - - trigger: - !type:DamageTrigger - damage: 100 - behaviors: - - !type:DoActsBehavior - acts: [ "Destruction" ] - - trigger: - !type:DamageTrigger - damage: 50 - behaviors: - - !type:DoActsBehavior - acts: ["Destruction"] - - !type:PlaySoundBehavior - sound: - collection: MetalBreak - - type: MovementSpeedModifier - acceleration: 8 - friction: 5 # wheels dont stop instantly - baseSprintSpeed: 6 - baseWalkSpeed: 4.5 # default walking speed - - type: ItemSlots - slots: - key_slot: - name: vehicle-slot-component-slot-name-keys - whitelist: - requireAll: true - tags: - - VehicleKey - insertSound: - path: /Audio/Effects/Vehicle/vehiclestartup.ogg - params: - volume: -3 - - type: StaticPrice - price: 2500 - - type: Tag - tags: - - DoorBumpOpener - - type: RequireProjectileTarget - -- type: entity - id: VehicleSecway - parent: BaseVehicle - name: secway - description: The future of transportation. Popularized by St. James, the patron saint of security officers and internet forum moderators. - components: - - type: Sprite - sprite: Objects/Vehicles/secway.rsi - state: vehicle - noRot: true - - type: Vehicle - renderOver: North, NorthEast, NorthWest - hornSound: - collection: DeskBell - params: - variation: 0.125 - sirenSound: - collection: PoliceSiren - params: - variation: 0.125 - - type: ItemSlots - slots: - key_slot: - name: vehicle-slot-component-slot-name-keys - whitelist: - requireAll: true - tags: - - VehicleKey - - SecwayKeys - insertSound: - path: /Audio/Effects/Vehicle/vehiclestartup.ogg - params: - volume: -3 - -- type: entity - id: VehicleSyndicateSegway - parent: VehicleSecway - name: syndicate segway - description: Be an enemy of the corporation, in style. - components: - - type: Sprite - sprite: Objects/Vehicles/syndicatesegway.rsi - state: vehicle - renderOver: North, NorthEast, NorthWest - noRot: true - - type: ItemSlots - slots: - key_slot: - name: vehicle-slot-component-slot-name-keys - insertSound: - path: /Audio/Effects/Vehicle/vehiclestartup.ogg - params: - volume: -3 - whitelist: - requireAll: true - tags: - - VehicleKey - - SyndicateSegwayKeys - -- type: entity - id: VehicleATV - parent: BaseVehicle - name: ATV - description: All-Tile Vehicle. - components: - - type: Sprite - sprite: Objects/Vehicles/atv.rsi - state: vehicle - noRot: true - - type: Vehicle - hornSound: - collection: BikeHorn - params: - variation: 0.125 - renderOver: North, NorthEast, East, SouthEast, South, SouthWest, West, NorthWest - - type: ItemSlots - slots: - key_slot: - name: vehicle-slot-component-slot-name-keys - whitelist: - requireAll: true - tags: - - VehicleKey - - ATVKeys - insertSound: - path: /Audio/Effects/Vehicle/vehiclestartup.ogg - params: - volume: -3 - -- type: entity - id: VehicleJanicart - parent: BaseVehicle - name: janicart - description: The janitor's trusty steed. - components: - - type: Sprite - sprite: Objects/Vehicles/janicart.rsi - state: vehicle - noRot: true - - type: Vehicle - renderOver: North, NorthEast, East, SouthEast, South, SouthWest, West, NorthWest - - type: ItemSlots - slots: - key_slot: - name: vehicle-slot-component-slot-name-keys - whitelist: - requireAll: true - tags: - - VehicleKey - - JanicartKeys - insertSound: - path: /Audio/Effects/Vehicle/vehiclestartup.ogg - params: - volume: -3 - - type: UnpoweredFlashlight - - type: PointLight - enabled: false - mask: /Textures/Effects/LightMasks/cone.png - autoRot: true - radius: 3.5 - softness: 2 - netsync: false - -- type: entity - id: VehicleWheelchair - parent: [ BaseVehicle, BaseFoldable, BaseItem ] - name: wheelchair - description: A chair with big wheels. It looks like you can move in these on your own. - components: - - type: Sprite - sprite: Objects/Vehicles/wheelchair.rsi - layers: - - state: vehicle - map: ["unfoldedLayer"] - - state: vehicle_folded - map: ["foldedLayer"] - visible: false - noRot: true - - type: Vehicle - requiredHands: 0 - engineRunning: true - renderOver: South, SouthEast, SouthWest - - type: MovementSpeedModifier - acceleration: 10 - friction: 10 - baseSprintSpeed: 3.5 - baseWalkSpeed: 2.5 - - type: StaticPrice - price: 75 - -- type: entity - parent: VehicleWheelchair - id: VehicleWheelchairFolded - suffix: folded - components: - - type: Foldable - folded: true diff --git a/Resources/Prototypes/_Goobstation/Recipes/Lathes/medical.yml b/Resources/Prototypes/_Goobstation/Recipes/Lathes/medical.yml index 32806287eba..b76547ee7d7 100644 --- a/Resources/Prototypes/_Goobstation/Recipes/Lathes/medical.yml +++ b/Resources/Prototypes/_Goobstation/Recipes/Lathes/medical.yml @@ -1,12 +1,3 @@ -- type: latheRecipe - id: VehicleWheelchairFolded - result: VehicleWheelchairFolded - completetime: 1 - materials: - Plastic: 500 - Cloth: 500 - Steel: 1000 - - type: latheRecipe id: ParamedHypo result: ParamedHypo diff --git a/Resources/Textures/Objects/Vehicles/janicart.rsi/meta.json b/Resources/Textures/Objects/Vehicles/janicart.rsi/meta.json index ef5d1c28e93..1baca772eef 100644 --- a/Resources/Textures/Objects/Vehicles/janicart.rsi/meta.json +++ b/Resources/Textures/Objects/Vehicles/janicart.rsi/meta.json @@ -9,6 +9,10 @@ "states": [ { "name": "vehicle", + "directions": 4 + }, + { + "name": "vehicle-moving", "directions": 4, "delays": [ [ diff --git a/Resources/Textures/Objects/Vehicles/janicart.rsi/vehicle-moving.png b/Resources/Textures/Objects/Vehicles/janicart.rsi/vehicle-moving.png new file mode 100644 index 00000000000..0eb087a925f Binary files /dev/null and b/Resources/Textures/Objects/Vehicles/janicart.rsi/vehicle-moving.png differ diff --git a/Resources/Textures/Objects/Vehicles/janicart.rsi/vehicle.png b/Resources/Textures/Objects/Vehicles/janicart.rsi/vehicle.png index 0eb087a925f..f509d18fcba 100644 Binary files a/Resources/Textures/Objects/Vehicles/janicart.rsi/vehicle.png and b/Resources/Textures/Objects/Vehicles/janicart.rsi/vehicle.png differ diff --git a/Resources/Textures/Objects/Vehicles/wheelchair.rsi/meta.json b/Resources/Textures/Objects/Vehicles/wheelchair.rsi/meta.json index 8c69fc12253..8625da65c94 100644 --- a/Resources/Textures/Objects/Vehicles/wheelchair.rsi/meta.json +++ b/Resources/Textures/Objects/Vehicles/wheelchair.rsi/meta.json @@ -1,26 +1,30 @@ { - "version": 1, - "size": { - "x": 32, - "y": 32 + "version": 1, + "license": "CC-BY-SA-3.0", + "copyright": "Taken from tgstation at https://github.com/tgstation/tgstation/blob/2c63c11c802b6878c013ad60786ea134204b6736/icons/mob/rideables/vehicles.dmi, https://github.com/tgstation/tgstation/blob/b1edbc1990a98239c6d3fd6871fc365daab61eb5/icons/mob/inhands/items_righthand.dmi, https://github.com/tgstation/tgstation/blob/b1edbc1990a98239c6d3fd6871fc365daab61eb5/icons/mob/inhands/items_lefthand.dmi", + "size": { + "x": 32, + "y": 32 + }, + "states": [ + { + "name": "wheelchair", + "directions": 4 }, - "license": "CC-BY-SA-3.0", - "copyright": "Taken from tgstation at commit https://github.com/tgstation/tgstation/commit/8f6e48e30ff85cb58b5b0a91add4a1741b971f0f", - "states": [ - { - "name": "vehicle", - "directions": 4 - }, - { - "name": "vehicle_folded" - }, - { - "name": "inhand-left", - "directions": 4 - }, - { - "name": "inhand-right", - "directions": 4 - } - ] -} + { + "name": "wheelchair_overlay", + "directions": 4 + }, + { + "name": "wheelchair_folded" + }, + { + "name": "inhand-right", + "directions": 4 + }, + { + "name": "inhand-left", + "directions": 4 + } + ] +} \ No newline at end of file diff --git a/Resources/Textures/Objects/Vehicles/wheelchair.rsi/vehicle.png b/Resources/Textures/Objects/Vehicles/wheelchair.rsi/wheelchair.png similarity index 100% rename from Resources/Textures/Objects/Vehicles/wheelchair.rsi/vehicle.png rename to Resources/Textures/Objects/Vehicles/wheelchair.rsi/wheelchair.png diff --git a/Resources/Textures/Objects/Vehicles/wheelchair.rsi/vehicle_folded.png b/Resources/Textures/Objects/Vehicles/wheelchair.rsi/wheelchair_folded.png similarity index 100% rename from Resources/Textures/Objects/Vehicles/wheelchair.rsi/vehicle_folded.png rename to Resources/Textures/Objects/Vehicles/wheelchair.rsi/wheelchair_folded.png diff --git a/Resources/Textures/Objects/Vehicles/wheelchair.rsi/wheelchair_overlay.png b/Resources/Textures/Objects/Vehicles/wheelchair.rsi/wheelchair_overlay.png new file mode 100644 index 00000000000..c4a91a1f6ed Binary files /dev/null and b/Resources/Textures/Objects/Vehicles/wheelchair.rsi/wheelchair_overlay.png differ