Ver código fonte

Adds mortars (#223)

* Ports RMC mortar files and gets them into a half functional state

At the moment the game can build. The mortar and its ammo exist ingame and the mortar can be deployed. Trying to open the UI will call the open function but the UI does not actually open. Explosions are also entirely commented out and the code to support it have not been ported. Locale files are also not ported. All mortar related sprites have been ported.

* Cleans the UI and unnecessary RMC dependencies

The blue horizontal line was removed as it would require editing base ss14 UI functions which I dont want to fuck with. The camera button on the mortar has been removed and the max dial section was moved above the dial buttons. RMC pop out window related code was removed.

* Comments out a debug assert which should not exist

Goes off because nomad is the name of the fallback job instead of passenger for some reason. Honestly no idea why there is an assert for this.

* Ports the locale file

A few things were removed too such as the ones about the mortar camera and the planet.

* Ports mortar sfx, fixes mortar animation and cleans some code

* Setup mortar sprites and fixed some other issues

New inhands, undeployed and firing sprites my me via editing civ13 mortar sprites. All the sfx are the same as the RMC ones besides the melee hit sfx.

* "old mortar" added

Uses some terrible programmer sprites for the inhands which should honestly be redone.

* Adds navigation tools for aiming the mortars

They all just function like a GPS for now and tell your current location x and y.

* Tweaks some values and disables warning messages

* Takes out the unused RMC sprites

I forgot to take these out after i replaced them
dallaszzz 6 meses atrás
pai
commit
0ced16fa6a
48 arquivos alterados com 1359 adições e 2 exclusões
  1. 70 0
      Content.Client/_RMC14/Mortar/MortarBui.cs
  2. 70 0
      Content.Client/_RMC14/Mortar/MortarSystem.cs
  3. 33 0
      Content.Client/_RMC14/Mortar/MortarWindow.xaml
  4. 71 0
      Content.Client/_RMC14/Mortar/MortarWindow.xaml.cs
  5. 26 0
      Content.Client/_RMC14/UserInterface/UIExtensions.cs
  6. 2 2
      Content.Server/GameTicking/GameTicker.cs
  7. 84 0
      Content.Server/_RMC14/Mortar/MortarSystem.cs
  8. 13 0
      Content.Shared/_RMC14/Extensions/IntExtensions.cs
  9. 37 0
      Content.Shared/_RMC14/Mortar/ActiveMortarShellComponent.cs
  10. 7 0
      Content.Shared/_RMC14/Mortar/DeployMortarDoAfterEvent.cs
  11. 15 0
      Content.Shared/_RMC14/Mortar/DialMortarDoAfterEvent.cs
  12. 7 0
      Content.Shared/_RMC14/Mortar/LoadMortarShellDoAfterEvent.cs
  13. 89 0
      Content.Shared/_RMC14/Mortar/MortarComponent.cs
  14. 9 0
      Content.Shared/_RMC14/Mortar/MortarFiredEvent.cs
  15. 20 0
      Content.Shared/_RMC14/Mortar/MortarShellComponent.cs
  16. 6 0
      Content.Shared/_RMC14/Mortar/MortarShellLandEvent.cs
  17. 24 0
      Content.Shared/_RMC14/Mortar/MortarUI.cs
  18. 16 0
      Content.Shared/_RMC14/Mortar/MortarVisuals.cs
  19. 447 0
      Content.Shared/_RMC14/Mortar/SharedMortarSystem.cs
  20. 15 0
      Content.Shared/_RMC14/Mortar/TargetMortarDoAfterEvent.cs
  21. BIN
      Resources/Audio/_RMC/Weapons/gun_mortar_fire.ogg
  22. BIN
      Resources/Audio/_RMC/Weapons/gun_mortar_reload.ogg
  23. BIN
      Resources/Audio/_RMC/Weapons/gun_mortar_travel.ogg
  24. BIN
      Resources/Audio/_RMC/Weapons/gun_mortar_unpack.ogg
  25. BIN
      Resources/Audio/_RMC/Weapons/gun_orbital_travel.ogg
  26. 45 0
      Resources/Locale/en-US/_RMC/mortar.ftl
  27. 31 0
      Resources/Prototypes/Civ14/Entities/Objects/Explosives/Mortars/mortar_shells.yml
  28. 69 0
      Resources/Prototypes/Civ14/Entities/Objects/Explosives/Mortars/mortars.yml
  29. 43 0
      Resources/Prototypes/Civ14/Entities/Objects/Tools/navigation.yml
  30. BIN
      Resources/Textures/Civ14/Objects/Mortars/mortar.rsi/inhand-left.png
  31. BIN
      Resources/Textures/Civ14/Objects/Mortars/mortar.rsi/inhand-right.png
  32. 48 0
      Resources/Textures/Civ14/Objects/Mortars/mortar.rsi/meta.json
  33. BIN
      Resources/Textures/Civ14/Objects/Mortars/mortar.rsi/mortar_carry.png
  34. BIN
      Resources/Textures/Civ14/Objects/Mortars/mortar.rsi/mortar_deploy.png
  35. BIN
      Resources/Textures/Civ14/Objects/Mortars/mortar.rsi/mortar_fire.png
  36. BIN
      Resources/Textures/Civ14/Objects/Mortars/mortar_old.rsi/inhand-left.png
  37. BIN
      Resources/Textures/Civ14/Objects/Mortars/mortar_old.rsi/inhand-right.png
  38. 48 0
      Resources/Textures/Civ14/Objects/Mortars/mortar_old.rsi/meta.json
  39. BIN
      Resources/Textures/Civ14/Objects/Mortars/mortar_old.rsi/mortar_carry.png
  40. BIN
      Resources/Textures/Civ14/Objects/Mortars/mortar_old.rsi/mortar_deploy.png
  41. BIN
      Resources/Textures/Civ14/Objects/Mortars/mortar_old.rsi/mortar_fire.png
  42. 14 0
      Resources/Textures/Civ14/Objects/Mortars/shells.rsi/meta.json
  43. BIN
      Resources/Textures/Civ14/Objects/Mortars/shells.rsi/mortar_ammo (1).png
  44. BIN
      Resources/Textures/Civ14/Objects/Mortars/shells.rsi/mortar_ammo (2).png
  45. BIN
      Resources/Textures/Civ14/Objects/Mortars/shells.rsi/mortar_ammo (4).png
  46. BIN
      Resources/Textures/Civ14/Objects/Mortars/shells.rsi/mortar_ammo (5).png
  47. BIN
      Resources/Textures/Civ14/Objects/Mortars/shells.rsi/mortar_ammo_he.png
  48. BIN
      Resources/Textures/Civ14/Objects/Mortars/type89.rsi/type89.png

+ 70 - 0
Content.Client/_RMC14/Mortar/MortarBui.cs

@@ -0,0 +1,70 @@
+using Content.Shared._RMC14.Mortar;
+using JetBrains.Annotations;
+using Robust.Client.UserInterface;
+using Robust.Client.UserInterface.Controls;
+
+namespace Content.Client._RMC14.Mortar;
+
+[UsedImplicitly]
+public sealed class MortarBui(EntityUid owner, Enum uiKey) : BoundUserInterface(owner, uiKey)
+{
+    private MortarWindow? _window;
+
+    protected override void Open()
+    {
+        base.Open();
+        _window = this.CreateWindow<MortarWindow>();
+
+        Refresh();
+
+        static int Parse(FloatSpinBox spinBox)
+        {
+            return (int) spinBox.Value;
+        }
+
+        static void SetSpinBox(FloatSpinBox spinBox, int limit, int value)
+        {
+            spinBox.Value = value;
+            spinBox.OnValueChanged += args =>
+            {
+                var value = Math.Clamp(args.Value, -limit, limit);
+                spinBox.Value = value;
+            };
+        }
+
+        if (EntMan.TryGetComponent(Owner, out MortarComponent? mortar))
+        {
+            SetSpinBox(_window.TargetX, mortar.MaxTarget, mortar.Target.X);
+            SetSpinBox(_window.TargetY, mortar.MaxTarget, mortar.Target.Y);
+            SetSpinBox(_window.DialX, mortar.MaxDial, mortar.Dial.X);
+            SetSpinBox(_window.DialY, mortar.MaxDial, mortar.Dial.Y);
+            _window.SetTargetButton.OnPressed += _ =>
+                SendPredictedMessage(new MortarTargetBuiMsg((Parse(_window.TargetX), Parse(_window.TargetY))));
+
+            _window.SetOffsetButton.OnPressed += _ =>
+                SendPredictedMessage(new MortarDialBuiMsg((Parse(_window.DialX), Parse(_window.DialY))));
+        }
+
+    }
+
+    public void Refresh()
+    {
+        if (_window is not { IsOpen: true })
+            return;
+
+        if (!EntMan.TryGetComponent(Owner, out MortarComponent? mortar))
+            return;
+
+        static void SetValue(FloatSpinBox? spinBox, int value)
+        {
+            if (spinBox != null)
+                spinBox.Value = value;
+        }
+
+        SetValue(_window.TargetX, mortar.Target.X);
+        SetValue(_window.TargetY, mortar.Target.Y);
+        SetValue(_window.DialX, mortar.Dial.X);
+        SetValue(_window.DialY, mortar.Dial.Y);
+        _window.MaxDialLabel.Text = Loc.GetString("rmc-mortar-offset-max", ("max", mortar.MaxDial));
+    }
+}

+ 70 - 0
Content.Client/_RMC14/Mortar/MortarSystem.cs

@@ -0,0 +1,70 @@
+using Content.Shared._RMC14.Mortar;
+using Robust.Client.Animations;
+using Robust.Client.GameObjects;
+
+namespace Content.Client._RMC14.Mortar;
+
+public sealed class MortarSystem : SharedMortarSystem
+{
+    [Dependency] private readonly AnimationPlayerSystem _animation = default!;
+
+    private const string AnimationKey = "rmc_mortar_fire";
+
+    public override void Initialize()
+    {
+        base.Initialize();
+
+        SubscribeAllEvent<MortarFiredEvent>(OnMortarFiredEvent);
+        SubscribeLocalEvent<MortarComponent, AfterAutoHandleStateEvent>(OnMortarHandleState);
+    }
+
+    private void OnMortarFiredEvent(MortarFiredEvent ev)
+    {
+        if (!TryGetEntity(ev.Mortar, out var mortarId) ||
+            !TryComp(mortarId, out MortarComponent? mortar))
+        {
+            return;
+        }
+
+        if (_animation.HasRunningAnimation(mortarId.Value, AnimationKey))
+            return;
+
+        _animation.Play(mortarId.Value,
+            new Animation
+            {
+                Length = mortar.AnimationTime,
+                AnimationTracks =
+                {
+                    new AnimationTrackSpriteFlick
+                    {
+                        LayerKey = mortar.AnimationLayer,
+                        KeyFrames =
+                        {
+                            new AnimationTrackSpriteFlick.KeyFrame(mortar.AnimationState, 0f),
+                            new AnimationTrackSpriteFlick.KeyFrame(mortar.DeployedState, 0.3f),
+                        },
+                    },
+                },
+            },
+            AnimationKey);
+    }
+
+    private void OnMortarHandleState(Entity<MortarComponent> mortar, ref AfterAutoHandleStateEvent args)
+    {
+        try
+        {
+            if (!TryComp(mortar, out UserInterfaceComponent? ui))
+                return;
+
+            foreach (var open in ui.ClientOpenInterfaces.Values)
+            {
+                if (open is MortarBui bui)
+                    bui.Refresh();
+            }
+        }
+        catch (Exception e)
+        {
+            Log.Error($"Error refreshing {nameof(MortarBui)}:\n{e}");
+        }
+    }
+}

+ 33 - 0
Content.Client/_RMC14/Mortar/MortarWindow.xaml

@@ -0,0 +1,33 @@
+<controls:MortarWindow
+    xmlns="https://spacestation14.io"
+    xmlns:controls="clr-namespace:Content.Client._RMC14.Mortar"
+    xmlns:ui="clr-namespace:Content.Client._RMC14.UserInterface"
+    Title="{Loc 'rmc-mortar-interface'}">
+    <BoxContainer Orientation="Vertical">
+        <BoxContainer Orientation="Horizontal">
+            <BoxContainer Orientation="Vertical">
+                <BoxContainer Name="TargetXContainer" Access="Public" Orientation="Horizontal">
+                    <Label Text="{Loc 'rmc-mortar-target-x'}" Margin="0 0 5 0" />
+                </BoxContainer>
+                <BoxContainer Name="TargetYContainer" Access="Public" Orientation="Horizontal" Margin="0 5 0 0">
+                    <Label Text="{Loc 'rmc-mortar-target-y'}" Margin="0 0 5 0" />
+                </BoxContainer>
+                <Button Name="SetTargetButton" Access="Public" StyleClasses="OpenBoth"
+                        Text="{Loc 'rmc-mortar-target-set'}" Margin="0 5 0 0" />
+            </BoxContainer>
+        </BoxContainer>
+        <BoxContainer Orientation="Horizontal">
+            <BoxContainer Orientation="Vertical">
+                <Label Name="MaxDialLabel" Access="Public" VerticalAlignment="Top" Margin="0 5" />
+                <BoxContainer Name="DialXContainer" Access="Public" Orientation="Horizontal">
+                    <Label Text="{Loc 'rmc-mortar-offset-x'}" Margin="0 0 5 0" />
+                </BoxContainer>
+                <BoxContainer Name="DialYContainer" Access="Public" Orientation="Horizontal" Margin="0 5 0 0">
+                    <Label Text="{Loc 'rmc-mortar-offset-y'}" Margin="0 0 5 0" />
+                </BoxContainer>
+                <Button Name="SetOffsetButton" Access="Public" StyleClasses="OpenBoth"
+                        Text="{Loc 'rmc-mortar-offset-set'}" Margin="0 5 0 0" />
+            </BoxContainer>
+        </BoxContainer>
+    </BoxContainer>
+</controls:MortarWindow>

+ 71 - 0
Content.Client/_RMC14/Mortar/MortarWindow.xaml.cs

@@ -0,0 +1,71 @@
+using Content.Client._RMC14.UserInterface;
+using Content.Client.UserInterface.ControlExtensions;
+using Robust.Client.AutoGenerated;
+using Robust.Client.UserInterface.Controls;
+using Robust.Client.UserInterface.CustomControls;
+using Robust.Client.UserInterface.XAML;
+using Robust.Shared.Input;
+
+namespace Content.Client._RMC14.Mortar;
+
+[GenerateTypedNameReferences]
+public sealed partial class MortarWindow : DefaultWindow
+{
+    public readonly FloatSpinBox TargetX;
+    public readonly FloatSpinBox TargetY;
+    public readonly FloatSpinBox DialX;
+    public readonly FloatSpinBox DialY;
+
+    public MortarWindow()
+    {
+        RobustXamlLoader.Load(this);
+
+        static LineEdit? SpinBoxGetLineEdit(FloatSpinBox box)
+        {
+            foreach (var edit in box.GetControlOfType<LineEdit>())
+            {
+                return edit;
+            }
+
+            return null;
+        }
+
+        void SpinBoxTab(FloatSpinBox from, FloatSpinBox to)
+        {
+            if (SpinBoxGetLineEdit(from) is not { } fromEdit ||
+                SpinBoxGetLineEdit(to) is not { } toEdit)
+            {
+                return;
+            }
+
+            fromEdit.OnKeyBindDown += args =>
+            {
+                if (args.Function == EngineKeyFunctions.GuiTabNavigateNext)
+                    toEdit.GrabKeyboardFocus();
+            };
+
+            toEdit.OnKeyBindDown += args =>
+            {
+                if (args.Function == EngineKeyFunctions.GuiTabNavigatePrev)
+                    fromEdit.GrabKeyboardFocus();
+            };
+        }
+
+        TargetX = UIExtensions.CreateDialSpinBox();
+        TargetXContainer.AddChild(TargetX);
+
+        TargetY = UIExtensions.CreateDialSpinBox();
+        TargetYContainer.AddChild(TargetY);
+
+        DialX = UIExtensions.CreateDialSpinBox();
+        DialXContainer.AddChild(DialX);
+
+        DialY = UIExtensions.CreateDialSpinBox();
+        DialYContainer.AddChild(DialY);
+
+        SpinBoxTab(TargetX, TargetY);
+        SpinBoxTab(TargetY, DialX);
+        SpinBoxTab(DialX, DialY);
+        SpinBoxTab(DialY, TargetX);
+    }
+}

+ 26 - 0
Content.Client/_RMC14/UserInterface/UIExtensions.cs

@@ -0,0 +1,26 @@
+using Content.Client.UserInterface.ControlExtensions;
+using Robust.Client.GameObjects;
+using Robust.Client.UserInterface;
+using Robust.Client.UserInterface.Controls;
+using static Robust.Client.UserInterface.Controls.FloatSpinBox;
+
+namespace Content.Client._RMC14.UserInterface;
+
+public static class UIExtensions
+{
+    public static FloatSpinBox CreateDialSpinBox(float value = default, Action<FloatSpinBoxEventArgs>? onValueChanged = null, bool buttons = true, int minWidth = 130)
+    {
+        var spinBox = new FloatSpinBox(1, 0) { MinWidth = minWidth };
+        spinBox.Value = value;
+        spinBox.OnValueChanged += onValueChanged;
+        if (!buttons)
+        {
+            foreach (var button in spinBox.GetControlOfType<Button>())
+            {
+                button.Visible = false;
+            }
+        }
+
+        return spinBox;
+    }
+}

+ 2 - 2
Content.Server/GameTicking/GameTicker.cs

@@ -88,8 +88,8 @@ public override void Initialize()
             InitializePlayer();
             InitializeLobbyBackground();
             InitializeGamePreset();
-            DebugTools.Assert(_prototypeManager.Index<JobPrototype>(FallbackOverflowJob).Name == FallbackOverflowJobName,
-                "Overflow role does not have the correct name!");
+            //DebugTools.Assert(_prototypeManager.Index<JobPrototype>(FallbackOverflowJob).Name == FallbackOverflowJobName,
+            //    "Overflow role does not have the correct name!");
             InitializeGameRules();
             InitializeReplays();
             _initialized = true;

+ 84 - 0
Content.Server/_RMC14/Mortar/MortarSystem.cs

@@ -0,0 +1,84 @@
+using System.Numerics;
+using Content.Server.Popups;
+using Content.Shared._RMC14.Mortar;
+using Robust.Server.Containers;
+using Robust.Shared.Map;
+using Robust.Shared.Random;
+using Robust.Shared.Timing;
+using static Content.Shared.Popups.PopupType;
+
+namespace Content.Server._RMC14.Mortar;
+
+public sealed class MortarSystem : SharedMortarSystem
+{
+    [Dependency] private readonly ContainerSystem _container = default!;
+    [Dependency] private readonly PopupSystem _popup = default!;
+    [Dependency] private readonly IRobustRandom _random = default!;
+    [Dependency] private readonly IGameTiming _timing = default!;
+    [Dependency] private readonly SharedTransformSystem _transform = default!;
+
+    protected override bool CanLoadPopup(
+        Entity<MortarComponent> mortar,
+        Entity<MortarShellComponent> shell,
+        EntityUid user,
+        out TimeSpan travelTime,
+        out MapCoordinates coordinates)
+    {
+        travelTime = default;
+        coordinates = default;
+
+        if (!mortar.Comp.Deployed)
+        {
+            _popup.PopupEntity(Loc.GetString("rmc-mortar-not-deployed", ("mortar", mortar)), user, user, SmallCaution);
+            return false;
+        }
+
+        var time = _timing.CurTime;
+        if (time < mortar.Comp.LastFiredAt + mortar.Comp.FireDelay)
+        {
+            _popup.PopupEntity(Loc.GetString("rmc-mortar-fire-cooldown", ("mortar", mortar)), user, user, SmallCaution);
+            return false;
+        }
+
+        var target = mortar.Comp.Target + mortar.Comp.Offset + mortar.Comp.Dial;
+        if (target == Vector2i.Zero)
+        {
+            _popup.PopupEntity(Loc.GetString("rmc-mortar-not-aimed", ("mortar", mortar)), user, user, SmallCaution);
+            return false;
+        }
+
+        var mortarCoordinates = _transform.GetMapCoordinates(mortar);
+        coordinates = new MapCoordinates(Vector2.Zero, mortarCoordinates.MapId);
+
+        coordinates = coordinates.Offset(target);
+        travelTime = shell.Comp.TravelDelay;
+
+        if ((mortarCoordinates.Position - coordinates.Position).Length() < mortar.Comp.MinimumRange)
+        {
+            _popup.PopupEntity(Loc.GetString("rmc-mortar-target-too-close"), user, user, SmallCaution);
+            return false;
+        }
+
+        if ((mortarCoordinates.Position - coordinates.Position).Length() > mortar.Comp.MaximumRange)
+        {
+            _popup.PopupEntity(Loc.GetString("rmc-mortar-target-too-far"), user, user, SmallCaution);
+            return false;
+        }
+
+        if (mortar.Comp.FireRandomOffset is { Length: > 0 } fireRandomOffset)
+        {
+            var xDeviation = _random.Pick(fireRandomOffset);
+            var yDeviation = _random.Pick(fireRandomOffset);
+            coordinates = coordinates.Offset(new Vector2(xDeviation, yDeviation));
+        }
+
+        if (_container.TryGetContainer(mortar, mortar.Comp.ContainerId, out var container) &&
+            !_container.CanInsert(shell, container))
+        {
+            _popup.PopupClient(Loc.GetString("rmc-mortar-cant-insert", ("shell", shell), ("mortar", mortar)), user, user, SmallCaution);
+            return false;
+        }
+
+        return true;
+    }
+}

+ 13 - 0
Content.Shared/_RMC14/Extensions/IntExtensions.cs

@@ -0,0 +1,13 @@
+namespace Content.Shared._RMC14.Extensions;
+
+public static class IntExtensions
+{
+    public static void Cap(this ref int value, int at)
+    {
+        at = Math.Abs(at);
+        if (value > at)
+            value = at;
+        else if (value < -at)
+            value = -at;
+    }
+}

+ 37 - 0
Content.Shared/_RMC14/Mortar/ActiveMortarShellComponent.cs

@@ -0,0 +1,37 @@
+using Robust.Shared.Audio;
+using Robust.Shared.GameStates;
+using Robust.Shared.Map;
+
+namespace Content.Shared._RMC14.Mortar;
+
+[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
+[Access(typeof(SharedMortarSystem))]
+public sealed partial class ActiveMortarShellComponent : Component
+{
+    [DataField, AutoNetworkedField]
+    public EntityCoordinates Coordinates;
+
+    [DataField, AutoNetworkedField]
+    public TimeSpan WarnAt;
+
+    [DataField, AutoNetworkedField]
+    public bool Warned;
+
+    [DataField, AutoNetworkedField]
+    public float WarnRange = 15;
+
+    [DataField, AutoNetworkedField]
+    public SoundSpecifier? WarnSound = new SoundPathSpecifier("/Audio/_RMC/Weapons/gun_mortar_travel.ogg");
+
+    [DataField, AutoNetworkedField]
+    public TimeSpan ImpactWarnAt;
+
+    [DataField, AutoNetworkedField]
+    public bool ImpactWarned;
+
+    [DataField, AutoNetworkedField]
+    public float ImpactWarnRange = 10;
+
+    [DataField, AutoNetworkedField]
+    public TimeSpan LandAt;
+}

+ 7 - 0
Content.Shared/_RMC14/Mortar/DeployMortarDoAfterEvent.cs

@@ -0,0 +1,7 @@
+using Content.Shared.DoAfter;
+using Robust.Shared.Serialization;
+
+namespace Content.Shared._RMC14.Mortar;
+
+[Serializable, NetSerializable]
+public sealed partial class DeployMortarDoAfterEvent : SimpleDoAfterEvent;

+ 15 - 0
Content.Shared/_RMC14/Mortar/DialMortarDoAfterEvent.cs

@@ -0,0 +1,15 @@
+using Content.Shared.DoAfter;
+using Robust.Shared.Serialization;
+
+namespace Content.Shared._RMC14.Mortar;
+
+[Serializable, NetSerializable]
+public sealed partial class DialMortarDoAfterEvent : SimpleDoAfterEvent
+{
+    public readonly Vector2i Vector;
+
+    public DialMortarDoAfterEvent(Vector2i vector)
+    {
+        Vector = vector;
+    }
+}

+ 7 - 0
Content.Shared/_RMC14/Mortar/LoadMortarShellDoAfterEvent.cs

@@ -0,0 +1,7 @@
+using Content.Shared.DoAfter;
+using Robust.Shared.Serialization;
+
+namespace Content.Shared._RMC14.Mortar;
+
+[Serializable, NetSerializable]
+public sealed partial class LoadMortarShellDoAfterEvent : SimpleDoAfterEvent;

+ 89 - 0
Content.Shared/_RMC14/Mortar/MortarComponent.cs

@@ -0,0 +1,89 @@
+using Robust.Shared.Audio;
+using Robust.Shared.GameStates;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
+
+namespace Content.Shared._RMC14.Mortar;
+
+[RegisterComponent, NetworkedComponent, AutoGenerateComponentState, AutoGenerateComponentPause]
+[Access(typeof(SharedMortarSystem))]
+public sealed partial class MortarComponent : Component
+{
+    [DataField, AutoNetworkedField]
+    public string ContainerId = "rmc_mortar_container";
+
+    [DataField, AutoNetworkedField]
+    public TimeSpan DeployDelay = TimeSpan.FromSeconds(4);
+
+    [DataField, AutoNetworkedField]
+    public TimeSpan TargetDelay = TimeSpan.FromSeconds(3);
+
+    [DataField, AutoNetworkedField]
+    public TimeSpan DialDelay = TimeSpan.FromSeconds(1);
+
+    [DataField, AutoNetworkedField]
+    public bool Deployed;
+
+    [DataField, AutoNetworkedField]
+    public Vector2i Target;
+
+    [DataField, AutoNetworkedField]
+    public Vector2i Offset;
+
+    [DataField, AutoNetworkedField]
+    public Vector2i Dial;
+
+    [DataField, AutoNetworkedField]
+    public TimeSpan FireDelay = TimeSpan.FromSeconds(4); // original was 9
+
+    [DataField, AutoNetworkedField]
+    public int TilesPerOffset = 20;
+
+    [DataField, AutoNetworkedField]
+    public int MaxTarget = 1000;
+
+    [DataField, AutoNetworkedField]
+    public int MaxDial = 10;
+
+    [DataField, AutoNetworkedField]
+    public int MinimumRange = 25;
+
+    [DataField, AutoNetworkedField]
+    public int MaximumRange = 75;
+
+    [DataField, AutoNetworkedField]
+    public string FixtureId = "mortar";
+
+    [DataField, AutoNetworkedField]
+    public TimeSpan AnimationTime = TimeSpan.FromSeconds(0.3);
+
+    [DataField, AutoNetworkedField]
+    public string AnimationLayer = "mortar";
+
+    [DataField, AutoNetworkedField]
+    public string AnimationState = "mortar_fire";
+
+    [DataField, AutoNetworkedField]
+    public string DeployedState = "mortar_deploy";
+
+    [DataField, AutoNetworkedField]
+    public SoundSpecifier? DeploySound = new SoundPathSpecifier("/Audio/_RMC/Weapons/gun_mortar_unpack.ogg");
+
+    [DataField, AutoNetworkedField]
+    public SoundSpecifier? ReloadSound = new SoundPathSpecifier("/Audio/_RMC/Weapons/gun_mortar_reload.ogg");
+
+    [DataField, AutoNetworkedField]
+    public SoundSpecifier? FireSound = new SoundPathSpecifier("/Audio/_RMC/Weapons/gun_mortar_fire.ogg", AudioParams.Default.AddVolume(4));
+
+    [DataField, AutoNetworkedField]
+    public TimeSpan? Cooldown;
+
+    [DataField(customTypeSerializer: typeof(TimeOffsetSerializer)), AutoNetworkedField, AutoPausedField]
+    public TimeSpan LastFiredAt;
+
+    [DataField, AutoNetworkedField]
+    public EntProtoId Drop = "MortarKit";
+
+    [DataField, AutoNetworkedField]
+    public int[] FireRandomOffset = new[] { -2, 0, 0, 2 }; // used to be -1 0 0 1
+}

+ 9 - 0
Content.Shared/_RMC14/Mortar/MortarFiredEvent.cs

@@ -0,0 +1,9 @@
+using Robust.Shared.Serialization;
+
+namespace Content.Shared._RMC14.Mortar;
+
+[Serializable, NetSerializable]
+public sealed class MortarFiredEvent(NetEntity mortar) : EntityEventArgs
+{
+    public readonly NetEntity Mortar = mortar;
+}

+ 20 - 0
Content.Shared/_RMC14/Mortar/MortarShellComponent.cs

@@ -0,0 +1,20 @@
+using Robust.Shared.GameStates;
+
+namespace Content.Shared._RMC14.Mortar;
+
+[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
+[Access(typeof(SharedMortarSystem))]
+public sealed partial class MortarShellComponent : Component
+{
+    [DataField, AutoNetworkedField]
+    public TimeSpan LoadDelay = TimeSpan.FromSeconds(1.5);
+
+    [DataField, AutoNetworkedField]
+    public TimeSpan TravelDelay = TimeSpan.FromSeconds(8); //used to be 4.5
+
+    [DataField, AutoNetworkedField]
+    public TimeSpan ImpactWarningDelay = TimeSpan.FromSeconds(2.5);
+
+    [DataField, AutoNetworkedField]
+    public TimeSpan ImpactDelay = TimeSpan.FromSeconds(4.5);
+}

+ 6 - 0
Content.Shared/_RMC14/Mortar/MortarShellLandEvent.cs

@@ -0,0 +1,6 @@
+using Robust.Shared.Map;
+
+namespace Content.Shared._RMC14.Mortar;
+
+[ByRefEvent]
+public readonly record struct MortarShellLandEvent(EntityCoordinates Coordinates);

+ 24 - 0
Content.Shared/_RMC14/Mortar/MortarUI.cs

@@ -0,0 +1,24 @@
+using Robust.Shared.Serialization;
+
+namespace Content.Shared._RMC14.Mortar;
+
+[Serializable, NetSerializable]
+public enum MortarUiKey
+{
+    Key,
+}
+
+[Serializable, NetSerializable]
+public sealed class MortarTargetBuiMsg(Vector2i target) : BoundUserInterfaceMessage
+{
+    public Vector2i Target = target;
+}
+
+[Serializable, NetSerializable]
+public sealed class MortarDialBuiMsg(Vector2i target) : BoundUserInterfaceMessage
+{
+    public Vector2i Target = target;
+}
+
+[Serializable, NetSerializable]
+public sealed class MortarViewCamerasMsg : BoundUserInterfaceMessage;

+ 16 - 0
Content.Shared/_RMC14/Mortar/MortarVisuals.cs

@@ -0,0 +1,16 @@
+using Robust.Shared.Serialization;
+
+namespace Content.Shared._RMC14.Mortar;
+
+[Serializable, NetSerializable]
+public enum MortarVisuals
+{
+    Item,
+    Deployed,
+}
+
+[Serializable, NetSerializable]
+public enum MortarVisualLayers
+{
+    State,
+}

+ 447 - 0
Content.Shared/_RMC14/Mortar/SharedMortarSystem.cs

@@ -0,0 +1,447 @@
+using Content.Shared._RMC14.Extensions;
+using Content.Shared.Administration.Logs;
+using Content.Shared.Construction.Components;
+using Content.Shared.Coordinates;
+using Content.Shared.Damage;
+using Content.Shared.Destructible;
+using Content.Shared.DoAfter;
+using Content.Shared.Examine;
+using Content.Shared.Interaction;
+using Content.Shared.Interaction.Events;
+using Content.Shared.Popups;
+using Content.Shared.UserInterface;
+using Content.Shared.Explosion.EntitySystems;
+using Robust.Shared.Audio.Systems;
+using Robust.Shared.Containers;
+using Robust.Shared.Map;
+using Robust.Shared.Network;
+using Robust.Shared.Physics.Systems;
+using Robust.Shared.Player;
+using Robust.Shared.Random;
+using Robust.Shared.Timing;
+
+namespace Content.Shared._RMC14.Mortar;
+
+public abstract class SharedMortarSystem : EntitySystem
+{
+    [Dependency] private readonly ISharedAdminLogManager _adminLogs = default!;
+    [Dependency] private readonly SharedAppearanceSystem _appearance = default!;
+    [Dependency] private readonly SharedAudioSystem _audio = default!;
+    [Dependency] private readonly SharedContainerSystem _container = default!;
+    [Dependency] private readonly SharedDoAfterSystem _doAfter = default!;
+    [Dependency] private readonly FixtureSystem _fixture = default!;
+    [Dependency] private readonly MetaDataSystem _metaData = default!;
+    [Dependency] private readonly INetManager _net = default!;
+    [Dependency] private readonly SharedPhysicsSystem _physics = default!;
+    [Dependency] private readonly SharedPopupSystem _popup = default!;
+    [Dependency] private readonly ISharedPlayerManager _player = default!;
+    [Dependency] private readonly IRobustRandom _random = default!;
+    [Dependency] private readonly IGameTiming _timing = default!;
+    [Dependency] private readonly SharedTransformSystem _transform = default!;
+    [Dependency] private readonly SharedUserInterfaceSystem _ui = default!;
+    [Dependency] private readonly SharedExplosionSystem _explosion = default!;
+
+    private EntityQuery<TransformComponent> _transformQuery;
+
+    public override void Initialize()
+    {
+        _transformQuery = GetEntityQuery<TransformComponent>();
+
+        SubscribeLocalEvent<MortarComponent, UseInHandEvent>(OnMortarUseInHand, before: [typeof(ActivatableUISystem)]);
+        SubscribeLocalEvent<MortarComponent, DeployMortarDoAfterEvent>(OnMortarDeployDoAfter);
+        SubscribeLocalEvent<MortarComponent, TargetMortarDoAfterEvent>(OnMortarTargetDoAfter);
+        SubscribeLocalEvent<MortarComponent, DialMortarDoAfterEvent>(OnMortarDialDoAfter);
+        SubscribeLocalEvent<MortarComponent, InteractUsingEvent>(OnMortarInteractUsing);
+        SubscribeLocalEvent<MortarComponent, LoadMortarShellDoAfterEvent>(OnMortarLoadDoAfter);
+        SubscribeLocalEvent<MortarComponent, UnanchorAttemptEvent>(OnMortarUnanchorAttempt);
+        SubscribeLocalEvent<MortarComponent, AnchorStateChangedEvent>(OnMortarAnchorStateChanged);
+        SubscribeLocalEvent<MortarComponent, ExaminedEvent>(OnMortarExamined);
+        SubscribeLocalEvent<MortarComponent, ActivatableUIOpenAttemptEvent>(OnMortarActivatableUIOpenAttempt);
+        SubscribeLocalEvent<MortarComponent, CombatModeShouldHandInteractEvent>(OnMortarShouldInteract);
+        SubscribeLocalEvent<MortarComponent, DestructionEventArgs>(OnMortarDestruction);
+        SubscribeLocalEvent<MortarComponent, BeforeDamageChangedEvent>(OnMortarBeforeDamageChanged);
+
+        Subs.BuiEvents<MortarComponent>(MortarUiKey.Key,
+            subs =>
+            {
+                subs.Event<MortarTargetBuiMsg>(OnMortarTargetBui);
+                subs.Event<MortarDialBuiMsg>(OnMortarDialBui);
+            });
+    }
+
+    private void OnMortarBeforeDamageChanged(Entity<MortarComponent> ent, ref BeforeDamageChangedEvent args)
+    {
+        if (!ent.Comp.Deployed) // cannot destroy in item form
+            args.Cancelled = true;
+    }
+
+    private void OnMortarDestruction(Entity<MortarComponent> mortar, ref DestructionEventArgs args)
+    {
+        if (!mortar.Comp.Deployed || _net.IsClient)
+            return;
+
+        SpawnAtPosition(mortar.Comp.Drop, mortar.Owner.ToCoordinates());
+    }
+
+    private void OnMortarUseInHand(Entity<MortarComponent> mortar, ref UseInHandEvent args)
+    {
+        args.Handled = true;
+        DeployMortar(mortar, args.User);
+    }
+
+    private void OnMortarDeployDoAfter(Entity<MortarComponent> mortar, ref DeployMortarDoAfterEvent args)
+    {
+        var user = args.User;
+        if (args.Cancelled || args.Handled)
+            return;
+
+        args.Handled = true;
+        if (mortar.Comp.Deployed)
+            return;
+
+        if (!CanDeployPopup(mortar, user))
+            return;
+
+        mortar.Comp.Deployed = true;
+        Dirty(mortar);
+
+        if (_fixture.GetFixtureOrNull(mortar, mortar.Comp.FixtureId) is { } fixture)
+            _physics.SetHard(mortar, fixture, true);
+
+        _appearance.SetData(mortar, MortarVisualLayers.State, MortarVisuals.Deployed);
+
+        var xform = Transform(mortar);
+        var coordinates = _transform.GetMoverCoordinates(mortar, xform);
+        var rotation = Transform(user).LocalRotation.GetCardinalDir().ToAngle();
+        _transform.SetCoordinates(mortar, xform, coordinates, rotation);
+        _transform.AnchorEntity((mortar, xform));
+
+        _audio.PlayPredicted(mortar.Comp.DeploySound, mortar, user);
+    }
+
+    private void OnMortarTargetDoAfter(Entity<MortarComponent> mortar, ref TargetMortarDoAfterEvent args)
+    {
+        if (args.Cancelled || args.Handled)
+            return;
+
+        args.Handled = true;
+
+        var user = args.User;
+        var selfMsg = Loc.GetString("rmc-mortar-target-finish-self", ("mortar", mortar));
+        var othersMsg = Loc.GetString("rmc-mortar-target-finish-others", ("user", user), ("mortar", mortar));
+        _popup.PopupPredicted(selfMsg, othersMsg, user, user);
+        if (_net.IsClient)
+            return;
+
+        var target = args.Vector;
+        var position = _transform.GetMapCoordinates(mortar).Position;
+        var offset = target;
+        //if (_rmcPlanet.TryGetOffset(_transform.GetMapCoordinates(mortar.Owner), out var planetOffset))
+        //    offset -= planetOffset;
+        // RMC uses a system to offset based on set values per map
+
+        mortar.Comp.Target = target;
+
+        var tilesPer = mortar.Comp.TilesPerOffset;
+        var xOffset = (int) Math.Floor(Math.Abs(offset.X - position.X) / tilesPer);
+        var yOffset = (int) Math.Floor(Math.Abs(offset.Y - position.Y) / tilesPer);
+        mortar.Comp.Offset = (_random.Next(-xOffset, xOffset + 1), _random.Next(-yOffset, yOffset + 1));
+
+        Dirty(mortar);
+    }
+
+    private void OnMortarDialDoAfter(Entity<MortarComponent> mortar, ref DialMortarDoAfterEvent args)
+    {
+        if (args.Cancelled || args.Handled)
+            return;
+
+        args.Handled = true;
+
+        mortar.Comp.Dial = args.Vector;
+        Dirty(mortar);
+
+        var user = args.User;
+        var selfMsg = Loc.GetString("rmc-mortar-dial-finish-self", ("mortar", mortar));
+        var othersMsg = Loc.GetString("rmc-mortar-dial-finish-others", ("user", user), ("mortar", mortar));
+        _popup.PopupPredicted(selfMsg, othersMsg, user, user);
+    }
+
+    private void OnMortarInteractUsing(Entity<MortarComponent> mortar, ref InteractUsingEvent args)
+    {
+        var shellId = args.Used;
+        if (!TryComp(shellId, out MortarShellComponent? shell))
+            return;
+
+        args.Handled = true;
+        var user = args.User;
+
+        if (!CanLoadPopup(mortar, (shellId, shell), user, out _, out _))
+            return;
+
+        var ev = new LoadMortarShellDoAfterEvent();
+        var doAfter = new DoAfterArgs(EntityManager, user, shell.LoadDelay, ev, mortar, mortar, shellId)
+        {
+            BreakOnMove = true,
+            BreakOnHandChange = true,
+            //ForceVisible = true,
+        };
+
+        if (_doAfter.TryStartDoAfter(doAfter))
+        {
+            var selfMsg = Loc.GetString("rmc-mortar-shell-load-start-self", ("mortar", mortar), ("shell", shellId));
+            var othersMsg = Loc.GetString("rmc-mortar-shell-load-start-others",
+                ("user", user),
+                ("mortar", mortar),
+                ("shell", shellId));
+            _popup.PopupPredicted(selfMsg, othersMsg, mortar, user);
+
+            _audio.PlayPredicted(mortar.Comp.ReloadSound, mortar, user);
+        }
+    }
+
+    private void OnMortarLoadDoAfter(Entity<MortarComponent> mortar, ref LoadMortarShellDoAfterEvent args)
+    {
+        var user = args.User;
+        if (args.Cancelled || args.Handled || args.Used is not { } shellId)
+            return;
+
+        args.Handled = true;
+        if (_net.IsClient)
+            return;
+
+        if (!TryComp(shellId, out MortarShellComponent? shell))
+            return;
+
+        if (!mortar.Comp.Deployed)
+            return;
+
+        if (HasComp<ActiveMortarShellComponent>(shellId))
+            return;
+
+        if (!CanLoadPopup(mortar, (shellId, shell), user, out var travelTime, out var coordinates))
+            return;
+
+        var container = _container.EnsureContainer<Container>(mortar, mortar.Comp.ContainerId);
+        if (!_container.Insert(shellId, container))
+            return;
+
+        var time = _timing.CurTime;
+        mortar.Comp.LastFiredAt = time;
+
+        var active = new ActiveMortarShellComponent
+        {
+            Coordinates = _transform.ToCoordinates(coordinates),
+            WarnAt = time + travelTime,
+            ImpactWarnAt = time + travelTime + shell.ImpactWarningDelay,
+            LandAt = time + travelTime + shell.ImpactDelay,
+        };
+
+        AddComp(shellId, active, true);
+
+        var selfMsg = Loc.GetString("rmc-mortar-shell-load-finish-self", ("mortar", mortar), ("shell", shellId));
+        var othersMsg = Loc.GetString("rmc-mortar-shell-load-finish-others", ("user", user), ("mortar", mortar), ("shell", shellId));
+        _popup.PopupPredicted(selfMsg, othersMsg, user, user);
+
+        othersMsg = Loc.GetString("rmc-mortar-shell-fire", ("mortar", mortar));
+        _popup.PopupEntity(othersMsg, mortar, PopupType.MediumCaution);
+
+        var filter = Filter.Pvs(mortar);
+        _audio.PlayPvs(mortar.Comp.FireSound, mortar);
+
+        var ev = new MortarFiredEvent(GetNetEntity(mortar));
+        if (_net.IsServer)
+            RaiseNetworkEvent(ev, filter);
+    }
+
+    private void OnMortarUnanchorAttempt(Entity<MortarComponent> mortar, ref UnanchorAttemptEvent args)
+    {
+        if (args.Cancelled)
+            return;
+    }
+
+    private void OnMortarAnchorStateChanged(Entity<MortarComponent> mortar, ref AnchorStateChangedEvent args)
+    {
+        if (args.Anchored)
+            return;
+
+        mortar.Comp.Deployed = false;
+        Dirty(mortar);
+
+        if (_fixture.GetFixtureOrNull(mortar, mortar.Comp.FixtureId) is { } fixture)
+            _physics.SetHard(mortar, fixture, false);
+
+        _appearance.SetData(mortar, MortarVisualLayers.State, MortarVisuals.Item);
+    }
+
+    private void OnMortarExamined(Entity<MortarComponent> ent, ref ExaminedEvent args)
+    {
+        using (args.PushGroup(nameof(MortarComponent)))
+        {
+            args.PushMarkup(Loc.GetString("rmc-mortar-less-accurate-with-range"));
+        }
+    }
+
+    private void OnMortarActivatableUIOpenAttempt(Entity<MortarComponent> ent, ref ActivatableUIOpenAttemptEvent args)
+    {
+        if (args.Cancelled)
+            return;
+
+        if (!ent.Comp.Deployed)
+            args.Cancel();
+    }
+
+    private void OnMortarShouldInteract(Entity<MortarComponent> ent, ref CombatModeShouldHandInteractEvent args)
+    {
+        args.Cancelled = true;
+    }
+
+    private void OnMortarTargetBui(Entity<MortarComponent> mortar, ref MortarTargetBuiMsg args)
+    {
+        args.Target.X.Cap(mortar.Comp.MaxTarget);
+        args.Target.Y.Cap(mortar.Comp.MaxTarget);
+
+        var user = args.Actor;
+        var ev = new TargetMortarDoAfterEvent(args.Target);
+        var doAfter = new DoAfterArgs(EntityManager, user, mortar.Comp.TargetDelay, ev, mortar)
+        {
+            BreakOnMove = true,
+        };
+
+        if (_doAfter.TryStartDoAfter(doAfter))
+        {
+            var selfMsg = Loc.GetString("rmc-mortar-target-start-self", ("mortar", mortar));
+            var othersMsg = Loc.GetString("rmc-mortar-target-start-others", ("user", user), ("mortar", mortar));
+            _popup.PopupPredicted(selfMsg, othersMsg, user, user);
+        }
+    }
+
+    private void OnMortarDialBui(Entity<MortarComponent> mortar, ref MortarDialBuiMsg args)
+    {
+        args.Target.X.Cap(mortar.Comp.MaxDial);
+        args.Target.Y.Cap(mortar.Comp.MaxDial);
+
+        var user = args.Actor;
+        var ev = new DialMortarDoAfterEvent(args.Target);
+        var doAfter = new DoAfterArgs(EntityManager, user, mortar.Comp.TargetDelay, ev, mortar)
+        {
+            BreakOnMove = true,
+        };
+
+        if (_doAfter.TryStartDoAfter(doAfter))
+        {
+            var selfMsg = Loc.GetString("rmc-mortar-dial-start-self", ("mortar", mortar));
+            var othersMsg = Loc.GetString("rmc-mortar-dial-start-others", ("user", user), ("mortar", mortar));
+            _popup.PopupPredicted(selfMsg, othersMsg, user, user);
+        }
+    }
+
+    private void DeployMortar(Entity<MortarComponent> mortar, EntityUid user)
+    {
+        if (mortar.Comp.Deployed)
+            return;
+
+        if (!CanDeployPopup(mortar, user))
+            return;
+
+        var ev = new DeployMortarDoAfterEvent();
+        var args = new DoAfterArgs(EntityManager, user, mortar.Comp.DeployDelay, ev, mortar)
+        {
+            BreakOnMove = true,
+            BreakOnHandChange = true,
+        };
+
+        if (_doAfter.TryStartDoAfter(args))
+            _popup.PopupClient(Loc.GetString("rmc-mortar-deploy-start", ("mortar", mortar)), user, user);
+    }
+
+    private bool CanDeployPopup(Entity<MortarComponent> mortar, EntityUid user)
+    {
+        //if (!_area.CanMortarPlacement(user.ToCoordinates()))
+        //{
+        //    _popup.PopupClient(Loc.GetString("rmc-mortar-covered", ("mortar", mortar)), user, user, PopupType.SmallCaution);
+        //    return false;
+        //}
+        // Need a system to check for a roof
+
+        return true;
+    }
+
+    protected virtual bool CanLoadPopup(
+        Entity<MortarComponent> mortar,
+        Entity<MortarShellComponent> shell,
+        EntityUid user,
+        out TimeSpan travelTime,
+        out MapCoordinates coordinates)
+    {
+        travelTime = default;
+        coordinates = default;
+        return false;
+    }
+
+    public void PopupWarning(MapCoordinates coordinates, float range, LocId warning, LocId warningAbove)
+    {
+        foreach (var session in _player.NetworkedSessions)
+        {
+            if (session.AttachedEntity is not { } recipient ||
+                !_transformQuery.TryComp(recipient, out var xform) ||
+                xform.MapID != coordinates.MapId)
+            {
+                continue;
+            }
+
+            var sessionCoordinates = _transform.GetMapCoordinates(xform);
+            var distanceVec = (coordinates.Position - sessionCoordinates.Position);
+            var distance = distanceVec.Length();
+            if (distance > range)
+                continue;
+
+            var direction = distanceVec.GetDir().ToString().ToUpperInvariant();
+            var msg = distance < 1
+                ? Loc.GetString(warningAbove)
+                : Loc.GetString(warning, ("direction", direction));
+            _popup.PopupEntity(msg, recipient, recipient, PopupType.LargeCaution);
+        }
+    }
+
+    public override void Update(float frameTime)
+    {
+        if (_net.IsClient)
+            return;
+
+        var time = _timing.CurTime;
+        var shells = EntityQueryEnumerator<ActiveMortarShellComponent>();
+        while (shells.MoveNext(out var uid, out var active))
+        {
+            if (!active.Warned && time >= active.WarnAt)
+            {
+                active.Warned = true;
+                var coordinates = _transform.ToMapCoordinates(active.Coordinates);
+                //PopupWarning(coordinates,
+                //    active.WarnRange,
+                //    "rmc-mortar-shell-warning",
+                //    "rmc-mortar-shell-warning-above");
+                _audio.PlayPvs(active.WarnSound, active.Coordinates);
+            }
+
+            if (!active.ImpactWarned && time >= active.ImpactWarnAt)
+            {
+                active.ImpactWarned = true;
+                var coordinates = _transform.ToMapCoordinates(active.Coordinates);
+                //PopupWarning(coordinates,
+                //    active.WarnRange,
+                //    "rmc-mortar-shell-impact-warning",
+                //    "rmc-mortar-shell-impact-warning-above");
+            }
+
+            if (time >= active.LandAt)
+            {
+                _transform.SetCoordinates(uid, active.Coordinates);
+
+                var ev = new MortarShellLandEvent(active.Coordinates);
+                RaiseLocalEvent(uid, ref ev);
+
+                _explosion.TriggerExplosive(uid);
+            }
+        }
+    }
+}

+ 15 - 0
Content.Shared/_RMC14/Mortar/TargetMortarDoAfterEvent.cs

@@ -0,0 +1,15 @@
+using Content.Shared.DoAfter;
+using Robust.Shared.Serialization;
+
+namespace Content.Shared._RMC14.Mortar;
+
+[Serializable, NetSerializable]
+public sealed partial class TargetMortarDoAfterEvent : SimpleDoAfterEvent
+{
+    public readonly Vector2i Vector;
+
+    public TargetMortarDoAfterEvent(Vector2i vector)
+    {
+        Vector = vector;
+    }
+}

BIN
Resources/Audio/_RMC/Weapons/gun_mortar_fire.ogg


BIN
Resources/Audio/_RMC/Weapons/gun_mortar_reload.ogg


BIN
Resources/Audio/_RMC/Weapons/gun_mortar_travel.ogg


BIN
Resources/Audio/_RMC/Weapons/gun_mortar_unpack.ogg


BIN
Resources/Audio/_RMC/Weapons/gun_orbital_travel.ogg


+ 45 - 0
Resources/Locale/en-US/_RMC/mortar.ftl

@@ -0,0 +1,45 @@
+rmc-mortar-deploy-start = You start deploying {THE($mortar)}.
+rmc-mortar-shell-busy = Someone else is currently using {THE($mortar)}
+rmc-mortar-not-aimed = {CAPITALIZE(THE($mortar))} needs to be aimed first.
+rmc-mortar-covered = You probably shouldn't deploy {THE($mortar)} indoors.
+rmc-mortar-target-invalid = You cannot fire {THE($mortar)} to this target.
+rmc-mortar-target-not-area = This area is out of bounds!
+rmc-mortar-target-covered = You cannot hit the target. It is probably underground.
+rmc-mortar-bad-idea = You realize how bad of an idea this is and quickly stop.
+rmc-mortar-cant-insert = You can't put {THE($shell)} in {THE($mortar)}!
+rmc-mortar-not-deployed = You need to deploy {THE($mortar)} first!
+rmc-mortar-fire-cooldown = {CAPITALIZE(THE($mortar))} barrel is still steaming hot. Wait a few seconds and stop firing it.
+rmc-mortar-less-accurate-with-range = [color=red]It will be less accurate the further away the target is![/color]
+rmc-mortar-target-start-self = You start adjusting {THE($mortar)}'s firing angle and distance to match the new coordinates.
+rmc-mortar-target-start-others = {$user} starts adjusting {THE($mortar)}'s firing angle and distance.
+rmc-mortar-target-finish-self = You finish adjusting {THE($mortar)}'s firing angle and distance to match the new coordinates.
+rmc-mortar-target-finish-others = {$user} finishes adjusting {THE($mortar)}'s firing angle and distance.
+rmc-mortar-dial-start-self = You start dialing {THE($mortar)}'s firing angle and distance to match the new coordinates.
+rmc-mortar-dial-start-others = {$user} starts dialing {THE($mortar)}'s firing angle and distance.
+rmc-mortar-dial-finish-self = You finish dialing {THE($mortar)}'s firing angle and distance to match the new coordinates.
+rmc-mortar-dial-finish-others = {$user} finishes dialing {THE($mortar)}'s firing angle and distance.
+
+rmc-mortar-shell-load-start-self = You start loading {THE($shell)} into {THE($mortar)}.
+rmc-mortar-shell-load-start-others = {$user} starts loading {THE($shell)} into {THE($mortar)}.
+rmc-mortar-shell-load-finish-self = You load {THE($shell)} into {THE($mortar)}.
+rmc-mortar-shell-fire = {CAPITALIZE(THE($mortar))} fires!
+rmc-mortar-shell-warning = A SHELL IS COMING DOWN TO YOUR {$direction}
+rmc-mortar-shell-warning-above = A SHELL IS COMING DOWN RIGHT ABOVE YOU
+rmc-mortar-shell-impact-warning = A SHELL IS ABOUT TO IMPACT TO YOUR {$direction}
+rmc-mortar-shell-impact-warning-above = A SHELL IS ABOUT TO IMPACT RIGHT ABOVE YOU
+
+rmc-mortar-interface = Mortar Interface
+
+rmc-mortar-target-x = Target X:
+rmc-mortar-target-y = Target Y:
+rmc-mortar-target-set = Set Target
+rmc-mortar-target-too-close = You cannot aim at this coordinate, it is too close to your mortar.
+rmc-mortar-target-too-far = You cannot aim at this coordinate, it is too far from your mortar.
+
+rmc-mortar-offset-x = X Offset:
+rmc-mortar-offset-y = Y Offset:
+rmc-mortar-offset-set = Dial Offset
+rmc-mortar-offset-too-far = You cannot dial to this coordinate, it is too far away from the original target.
+rmc-mortar-offset-too-close = You cannot dial to this coordinate, it is too close to your mortar.
+rmc-mortar-offset-max = Max dial
+  offset: {$max}

+ 31 - 0
Resources/Prototypes/Civ14/Entities/Objects/Explosives/Mortars/mortar_shells.yml

@@ -0,0 +1,31 @@
+- type: entity
+  abstract: true
+  parent: BaseItem
+  id: MortarAmmoBase
+  name: base mortar shell
+  components:
+  - type: Sprite
+    sprite: Civ14/Objects/Mortars/shells.rsi
+    state: mortar_ammo_he
+  - type: Item
+    size: Huge
+  - type: MortarShell
+
+- type: entity
+  parent: MortarAmmoBase
+  id: MortarShellHE
+  name: high explosive mortar shell
+  description: A mortar shell packed with high explosive. Drop into a mortar tube to activate.
+  components:
+  - type: Item
+  - type: Sprite
+    sprite: Civ14/Objects/Mortars/shells.rsi
+    state: mortar_ammo_he
+  - type: MortarShell
+  - type: Explosive
+    explosionType: Default
+    maxIntensity: 10 # these values need tuning to im sure
+    intensitySlope: 3
+    totalIntensity: 120
+    canCreateVacuum: false
+    deleteAfterExplosion: true

+ 69 - 0
Resources/Prototypes/Civ14/Entities/Objects/Explosives/Mortars/mortars.yml

@@ -0,0 +1,69 @@
+- type: entity
+  parent: BaseItem
+  id: MortarKit
+  name: portable mortar
+  description: A support weapon capable of droping bombs on heads from a distance. This one seems rather modern.
+  components:
+  - type: Sprite
+    sprite: Civ14/Objects/Mortars/mortar.rsi
+    layers:
+    - map: [ "mortar" ]
+      state: mortar_carry
+  - type: Fixtures
+    fixtures:
+      fix1:
+        shape:
+          !type:PhysShapeAabb
+          bounds: "-0.25,-0.25,0.25,0.25"
+        density: 20
+        mask:
+        - ItemMask
+        restitution: 0.3
+        friction: 0.2
+      mortar:
+        shape:
+          !type:PhysShapeAabb
+          bounds: "-0.49,-0.49,0.49,0.49"
+        density: 20
+        layer:
+        - Impassable
+        mask:
+        - Impassable
+        hard: false
+  - type: CollisionWake
+    enabled: false
+  - type: Anchorable
+    flags:
+    - Unanchorable
+  - type: Item
+    size: Huge
+  - type: Mortar
+  - type: Damageable
+    damageContainer: Inorganic
+  - type: UserInterface
+    interfaces:
+      enum.MortarUiKey.Key:
+        type: MortarBui
+  - type: ActivatableUI
+    key: enum.MortarUiKey.Key
+  - type: Appearance
+  - type: GenericVisualizer
+    visuals:
+      enum.MortarVisualLayers.State:
+        mortar:
+          Item: { state: mortar_carry }
+          Deployed: { state: mortar_deploy }
+  - type: MeleeSound
+    soundGroups:
+      Brute:
+        path:
+          "/Audio/Effects/metal_break1.ogg"
+
+- type: entity
+  parent: MortarKit
+  id: MortarOld
+  name: old portable mortar
+  description: A support weapon capable of droping bombs on heads from a distance.
+  components:
+  - type: Sprite
+    sprite: Civ14/Objects/Mortars/mortar_old.rsi

+ 43 - 0
Resources/Prototypes/Civ14/Entities/Objects/Tools/navigation.yml

@@ -0,0 +1,43 @@
+- type: entity
+  name: compass
+  parent: BaseItem
+  id: Compass
+  description: A device to help navigate.
+  components:
+  - type: Sprite
+    sprite: Civ14/Objects/items.rsi
+    state: compass
+  - type: Item
+  - type: HandheldGPS
+  - type: Tag
+    tags:
+    - GPS
+
+- type: entity
+  name: map and compass
+  parent: Compass
+  id: CompassMap
+  description: A set of navigation tools.
+  components:
+  - type: Sprite
+    sprite: Civ14/Objects/items.rsi
+    state: compass_map
+
+- type: entity
+  name : modern map and compass
+  parent: CompassMap
+  id: ModernCompassMap
+  components:
+  - type: Sprite
+    sprite: Civ14/Objects/items.rsi
+    state: compass_map_modern
+
+- type: entity
+  name : digital map
+  parent: CompassMap
+  id: DigitalMap
+  description: A tablet with a map and navigation tools on it.
+  components:
+  - type: Sprite
+    sprite: Civ14/Objects/items.rsi
+    state: compass_modern

BIN
Resources/Textures/Civ14/Objects/Mortars/mortar.rsi/inhand-left.png


BIN
Resources/Textures/Civ14/Objects/Mortars/mortar.rsi/inhand-right.png


+ 48 - 0
Resources/Textures/Civ14/Objects/Mortars/mortar.rsi/meta.json

@@ -0,0 +1,48 @@
+{
+  "version": 1,
+  "license": "CC-BY-SA-3.0",
+  "copyright": "Taken from civ13 and edited by dallaszzz on github",
+  "size": {
+    "x": 32,
+    "y": 32
+  },
+  "states": [
+    {
+      "name": "mortar_deploy",
+      "directions": 4
+    },
+    {
+      "name": "mortar_fire",
+      "directions": 4,
+      "delays": [
+        [
+          0.1,
+          0.2
+        ],
+        [
+          0.1,
+          0.2
+        ],
+        [
+          0.1,
+          0.2
+        ],
+        [
+          0.1,
+          0.2
+        ]
+      ]
+    },
+    {
+      "name": "mortar_carry"
+    },
+    {
+      "name": "inhand-left",
+      "directions": 4
+    },
+    {
+      "name": "inhand-right",
+      "directions": 4
+    }
+  ]
+}

BIN
Resources/Textures/Civ14/Objects/Mortars/mortar.rsi/mortar_carry.png


BIN
Resources/Textures/Civ14/Objects/Mortars/mortar.rsi/mortar_deploy.png


BIN
Resources/Textures/Civ14/Objects/Mortars/mortar.rsi/mortar_fire.png


BIN
Resources/Textures/Civ14/Objects/Mortars/mortar_old.rsi/inhand-left.png


BIN
Resources/Textures/Civ14/Objects/Mortars/mortar_old.rsi/inhand-right.png


+ 48 - 0
Resources/Textures/Civ14/Objects/Mortars/mortar_old.rsi/meta.json

@@ -0,0 +1,48 @@
+{
+  "version": 1,
+  "license": "CC-BY-SA-3.0",
+  "copyright": "Taken from civ13 and edited by dallaszzz on github",
+  "size": {
+    "x": 32,
+    "y": 32
+  },
+  "states": [
+    {
+      "name": "mortar_deploy",
+      "directions": 4
+    },
+    {
+      "name": "mortar_fire",
+      "directions": 4,
+      "delays": [
+        [
+          0.1,
+          0.2
+        ],
+        [
+          0.1,
+          0.2
+        ],
+        [
+          0.1,
+          0.2
+        ],
+        [
+          0.1,
+          0.2
+        ]
+      ]
+    },
+    {
+      "name": "mortar_carry"
+    },
+    {
+      "name": "inhand-left",
+      "directions": 4
+    },
+    {
+      "name": "inhand-right",
+      "directions": 4
+    }
+  ]
+}

BIN
Resources/Textures/Civ14/Objects/Mortars/mortar_old.rsi/mortar_carry.png


BIN
Resources/Textures/Civ14/Objects/Mortars/mortar_old.rsi/mortar_deploy.png


BIN
Resources/Textures/Civ14/Objects/Mortars/mortar_old.rsi/mortar_fire.png


+ 14 - 0
Resources/Textures/Civ14/Objects/Mortars/shells.rsi/meta.json

@@ -0,0 +1,14 @@
+{
+  "version": 1,
+  "license": "CC-BY-SA-3.0",
+  "copyright": "Sprites taken from civ13",
+  "size": {
+    "x": 32,
+    "y": 32
+  },
+  "states": [
+    {
+      "name": "mortar_ammo_he"
+    }
+  ]
+}

BIN
Resources/Textures/Civ14/Objects/Mortars/shells.rsi/mortar_ammo (1).png


BIN
Resources/Textures/Civ14/Objects/Mortars/shells.rsi/mortar_ammo (2).png


BIN
Resources/Textures/Civ14/Objects/Mortars/shells.rsi/mortar_ammo (4).png


BIN
Resources/Textures/Civ14/Objects/Mortars/shells.rsi/mortar_ammo (5).png


BIN
Resources/Textures/Civ14/Objects/Mortars/shells.rsi/mortar_ammo_he.png


BIN
Resources/Textures/Civ14/Objects/Mortars/type89.rsi/type89.png