diff --git a/Content.Server/_Forge/Projectiles/TargetSeeking/TargetSeekingSystem.Forge.cs b/Content.Server/_Forge/Projectiles/TargetSeeking/TargetSeekingSystem.Forge.cs new file mode 100644 index 00000000000..3920aaf4e44 --- /dev/null +++ b/Content.Server/_Forge/Projectiles/TargetSeeking/TargetSeekingSystem.Forge.cs @@ -0,0 +1,15 @@ +using System.Numerics; +using Robust.Shared.Map.Components; + +namespace Content.Server._Mono.Projectiles.TargetSeeking; + +public sealed partial class TargetSeekingSystem +{ + private Vector2 GetTargetWorldPosition(EntityUid target, TransformComponent xform) + { + if (TryComp(target, out var grid)) + return Vector2.Transform(grid.LocalAABB.Center, _transform.GetWorldMatrix(xform)); + + return _transform.GetWorldPosition(xform); + } +} diff --git a/Content.Server/_Forge/ShipWeapons/ForgeTorpedoLauncherComponent.cs b/Content.Server/_Forge/ShipWeapons/ForgeTorpedoLauncherComponent.cs new file mode 100644 index 00000000000..ae716f561a7 --- /dev/null +++ b/Content.Server/_Forge/ShipWeapons/ForgeTorpedoLauncherComponent.cs @@ -0,0 +1,48 @@ +using Content.Shared.Physics; + +namespace Content.Server._Forge.ShipWeapons; + +/// +/// Marks a static ship launcher that uses its own facing for launch direction and +/// the gunnery cursor only for target-grid selection. +/// +[RegisterComponent] +public sealed partial class ForgeTorpedoLauncherComponent : Component +{ + /// + /// Distance from the launcher center used to sample the tile in front of the muzzle. + /// + [DataField, ViewVariables(VVAccess.ReadWrite)] + public float TileProbeDistance = 1f; + + [DataField, ViewVariables(VVAccess.ReadWrite)] + public float ClearanceRayDistance = 1.25f; + + [DataField, ViewVariables(VVAccess.ReadWrite)] + public int ClearanceCollisionMask = (int) (CollisionGroup.Opaque | CollisionGroup.Impassable); + + /// + /// Distance used to build a gun shoot coordinate straight out of the launcher. + /// The actual locked target is stored separately, because the cursor is not the muzzle direction. + /// + [DataField, ViewVariables(VVAccess.ReadWrite)] + public float LaunchAimDistance = 32f; + + /// + /// Minimum random delay before the prepared torpedo launch is actually fired. + /// + [DataField, ViewVariables(VVAccess.ReadWrite)] + public float LaunchDelayMin = 0f; + + [DataField, ViewVariables(VVAccess.ReadWrite)] + public float LaunchDelayMax = 0.1f; + + [DataField, ViewVariables(VVAccess.ReadWrite)] + public float TargetSearchRadius = 48f; + + /// + /// Minimum tile count for a grid to be considered a valid torpedo target. + /// + [DataField, ViewVariables(VVAccess.ReadWrite)] + public int MinTargetTiles = 25; +} diff --git a/Content.Server/_Forge/ShipWeapons/ForgeTorpedoLauncherSystem.cs b/Content.Server/_Forge/ShipWeapons/ForgeTorpedoLauncherSystem.cs new file mode 100644 index 00000000000..8f0125da1d1 --- /dev/null +++ b/Content.Server/_Forge/ShipWeapons/ForgeTorpedoLauncherSystem.cs @@ -0,0 +1,396 @@ +using System.Numerics; +using Content.Server._Mono.FireControl; +using Content.Server._Mono.Projectiles.TargetSeeking; +using Content.Server.Power.EntitySystems; +using Content.Server.Weapons.Ranged.Systems; +using Content.Shared.Examine; +using Content.Shared.Maps; +using Content.Shared.Weapons.Ranged.Components; +using Content.Shared.Weapons.Ranged.Events; +using Robust.Shared.Map; +using Robust.Shared.Map.Components; +using Robust.Shared.Physics; +using Robust.Shared.Physics.Systems; +using Robust.Shared.Random; +using Robust.Shared.Timing; + +namespace Content.Server._Forge.ShipWeapons; + +public sealed class ForgeTorpedoLauncherSystem : EntitySystem +{ + private static readonly TimeSpan ShotResultTimeout = TimeSpan.FromSeconds(0.5); + + [Dependency] private readonly IMapManager _mapManager = default!; + [Dependency] private readonly IGameTiming _timing = default!; + [Dependency] private readonly GunSystem _gun = default!; + [Dependency] private readonly PowerReceiverSystem _power = default!; + [Dependency] private readonly SharedMapSystem _map = default!; + [Dependency] private readonly SharedPhysicsSystem _physics = default!; + [Dependency] private readonly IRobustRandom _random = default!; + [Dependency] private readonly SharedTransformSystem _transform = default!; + [Dependency] private readonly TargetSeekingSystem _targetSeeking = default!; + [Dependency] private readonly TurfSystem _turf = default!; + + private readonly List> _targetGridScratch = new(); + private readonly Dictionary _pendingLaunches = new(); + private readonly List _pendingLaunchScratch = new(); + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnExamined); + SubscribeLocalEvent(OnAmmoShot); + SubscribeLocalEvent(OnEmptyShot); + SubscribeLocalEvent(OnShutdown); + } + + private void OnExamined(EntityUid uid, ForgeTorpedoLauncherComponent component, ExaminedEvent args) + { + if (!args.IsInDetailsRange) + return; + + args.PushMarkup(Loc.GetString("forge-torpedo-launcher-examine")); + } + + private void OnShutdown(EntityUid uid, ForgeTorpedoLauncherComponent component, ComponentShutdown args) + { + CancelPreparedLaunch(uid); + } + + public override void Update(float frameTime) + { + base.Update(frameTime); + + _pendingLaunchScratch.Clear(); + _pendingLaunchScratch.AddRange(_pendingLaunches.Keys); + + foreach (var weapon in _pendingLaunchScratch) + { + if (!_pendingLaunches.TryGetValue(weapon, out var pending)) + continue; + + if (pending.Fired) + { + if (pending.ExpireTime <= _timing.CurTime) + _pendingLaunches.Remove(weapon); + + continue; + } + + if (pending.FireTime > _timing.CurTime) + continue; + + if (!TryGetLaunchState(pending, out var component, out var xform, out var gun) || + TerminatingOrDeleted(pending.TargetGrid) || + !CanFireNow(pending.Weapon, gun, pending.NoServer) || + !CanLaunch(pending.Weapon, GetLaunchDirection(xform), component, xform)) + { + _pendingLaunches.Remove(weapon); + continue; + } + + _pendingLaunches[weapon] = pending with + { + Fired = true, + ExpireTime = _timing.CurTime + ShotResultTimeout, + }; + + _gun.AttemptShots(pending.User, pending.Weapon, gun, pending.FireCoordinates, TimeSpan.FromSeconds(0.2)); + } + + _pendingLaunchScratch.Clear(); + } + + private void OnAmmoShot(EntityUid uid, ForgeTorpedoLauncherComponent component, AmmoShotEvent args) + { + if (!_pendingLaunches.Remove(uid, out var pending) || + !pending.Fired || + args.FiredProjectiles.Count == 0 || + TerminatingOrDeleted(pending.TargetGrid)) + { + return; + } + + foreach (var projectile in args.FiredProjectiles) + { + if (!TryComp(projectile, out var seeking)) + continue; + + _targetSeeking.SetSeekerTarget((projectile, seeking), pending.TargetGrid, Transform(projectile)); + } + } + + private void OnEmptyShot(EntityUid uid, ForgeTorpedoLauncherComponent component, ref OnEmptyGunShotEvent args) + { + CancelPreparedLaunch(uid); + } + + public bool HasPendingLaunch(EntityUid uid) + { + return _pendingLaunches.ContainsKey(uid); + } + + public void CancelPreparedLaunch(EntityUid uid) + { + _pendingLaunches.Remove(uid); + } + + public Vector2 GetLaunchDirection(TransformComponent xform) + { + return _transform.GetWorldRotation(xform).ToWorldVec(); + } + + public bool TryQueueLaunch( + EntityUid uid, + EntityUid user, + EntityCoordinates aimCoordinates, + Vector2 launchDirection, + ForgeTorpedoLauncherComponent component, + TransformComponent xform, + bool noServer) + { + if (_pendingLaunches.ContainsKey(uid) || + !TryComp(uid, out var gun) || + !CanFireNow(uid, gun, noServer) || + !TryBuildPendingLaunch(uid, user, aimCoordinates, launchDirection, component, xform, noServer, out var pending)) + { + return false; + } + + _pendingLaunches[uid] = pending; + return true; + } + + private bool TryBuildPendingLaunch( + EntityUid uid, + EntityUid user, + EntityCoordinates aimCoordinates, + Vector2 launchDirection, + ForgeTorpedoLauncherComponent component, + TransformComponent xform, + bool noServer, + out PendingLaunch pending) + { + pending = default; + + if (launchDirection.LengthSquared() <= 0.0001f) + return false; + + launchDirection = Vector2.Normalize(launchDirection); + + if (!CanLaunch(uid, launchDirection, component, xform) || + !TryFindTargetGrid(uid, aimCoordinates, component, xform, out var targetGrid)) + { + return false; + } + + var launcherCoords = _transform.GetMapCoordinates(xform); + var launchMapCoordinates = new MapCoordinates( + launcherCoords.Position + launchDirection * component.LaunchAimDistance, + launcherCoords.MapId); + + var minDelay = MathF.Max(0f, component.LaunchDelayMin); + var maxDelay = MathF.Max(0f, component.LaunchDelayMax); + if (maxDelay < minDelay) + (minDelay, maxDelay) = (maxDelay, minDelay); + + var delay = maxDelay > minDelay + ? _random.NextFloat(minDelay, maxDelay) + : minDelay; + + pending = new PendingLaunch( + uid, + user, + _transform.ToCoordinates((uid, xform), launchMapCoordinates), + targetGrid, + _timing.CurTime + TimeSpan.FromSeconds(delay), + noServer); + + return true; + } + + public bool CanLaunch( + EntityUid uid, + Vector2 worldDirection, + ForgeTorpedoLauncherComponent? component = null, + TransformComponent? xform = null) + { + if (!Resolve(uid, ref component, ref xform)) + return false; + + if (worldDirection.LengthSquared() <= 0.0001f) + return false; + + if (xform.GridUid is not { } gridUid || !TryComp(gridUid, out var grid)) + return false; + + worldDirection = Vector2.Normalize(worldDirection); + + return HasSpaceAhead(gridUid, grid, xform, worldDirection, component) && + HasClearMuzzleRay(uid, xform, worldDirection, component); + } + + public bool TryFindTargetGrid( + EntityUid uid, + EntityCoordinates aimCoordinates, + ForgeTorpedoLauncherComponent? component, + TransformComponent? xform, + out EntityUid targetGrid) + { + targetGrid = default; + + if (!Resolve(uid, ref component, ref xform)) + return false; + + var aimMapCoordinates = _transform.ToMapCoordinates(aimCoordinates); + if (aimMapCoordinates.MapId != xform.MapID) + return false; + + var searchRadius = MathF.Max(0f, component.TargetSearchRadius); + var searchExtents = new Vector2(searchRadius, searchRadius); + var searchBox = new Box2( + aimMapCoordinates.Position - searchExtents, + aimMapCoordinates.Position + searchExtents); + + _targetGridScratch.Clear(); + _mapManager.FindGridsIntersecting(aimMapCoordinates.MapId, searchBox, ref _targetGridScratch, approx: true, includeMap: false); + + var sourceGrid = xform.GridUid; + var bestDistance = float.MaxValue; + + for (var i = 0; i < _targetGridScratch.Count; i++) + { + var candidate = _targetGridScratch[i]; + var candidateUid = candidate.Owner; + + if (candidateUid == sourceGrid || + TerminatingOrDeleted(candidateUid) || + !HasEnoughTiles(candidateUid, candidate.Comp, component.MinTargetTiles)) + { + continue; + } + + var candidateXform = Transform(candidateUid); + if (candidateXform.MapID != aimMapCoordinates.MapId) + continue; + + var bounds = _transform.GetWorldMatrix(candidateXform).TransformBox(candidate.Comp.LocalAABB); + var distance = DistanceSquaredToBox(aimMapCoordinates.Position, bounds); + if (distance >= bestDistance) + continue; + + bestDistance = distance; + targetGrid = candidateUid; + } + + _targetGridScratch.Clear(); + return targetGrid != default; + } + + private bool TryGetLaunchState( + PendingLaunch pending, + out ForgeTorpedoLauncherComponent component, + out TransformComponent xform, + out GunComponent gun) + { + component = default!; + xform = default!; + gun = default!; + + return !TerminatingOrDeleted(pending.Weapon) && + !TerminatingOrDeleted(pending.User) && + TryComp(pending.Weapon, out component) && + TryComp(pending.Weapon, out xform) && + TryComp(pending.Weapon, out gun); + } + + private bool CanFireNow(EntityUid weapon, GunComponent gun, bool noServer) + { + if (!_power.IsPowered(weapon) || !_gun.CanShoot(gun)) + return false; + + return noServer || + TryComp(weapon, out var fireControllable) && + fireControllable.ControllingServer != null; + } + + private bool HasSpaceAhead( + EntityUid gridUid, + MapGridComponent grid, + TransformComponent launcherXform, + Vector2 worldDirection, + ForgeTorpedoLauncherComponent component) + { + var launcherCoords = _transform.GetMapCoordinates(launcherXform); + var probeCoords = launcherCoords.Offset(worldDirection * component.TileProbeDistance); + var probeTile = _map.TileIndicesFor(gridUid, grid, probeCoords); + + if (!_map.TryGetTileRef(gridUid, grid, probeTile, out var tileRef)) + return true; + + return tileRef.Tile.IsEmpty || + _turf.GetContentTileDefinition(tileRef).ID == ContentTileDefinition.SpaceID; + } + + private bool HasClearMuzzleRay( + EntityUid uid, + TransformComponent launcherXform, + Vector2 worldDirection, + ForgeTorpedoLauncherComponent component) + { + var launcherCoords = _transform.GetMapCoordinates(launcherXform); + var ray = new CollisionRay( + launcherCoords.Position, + worldDirection, + collisionMask: component.ClearanceCollisionMask); + + foreach (var _ in _physics.IntersectRayWithPredicate( + launcherXform.MapID, + ray, + uid, + (entity, source) => entity == source, + component.ClearanceRayDistance, + returnOnFirstHit: true)) + { + return false; + } + + return true; + } + + private bool HasEnoughTiles(EntityUid gridUid, MapGridComponent grid, int minimumTiles) + { + if (minimumTiles <= 0) + return true; + + var count = 0; + var enumerator = _map.GetAllTilesEnumerator(gridUid, grid); + while (enumerator.MoveNext(out _)) + { + count++; + if (count >= minimumTiles) + return true; + } + + return false; + } + + private static float DistanceSquaredToBox(Vector2 point, Box2 box) + { + var dx = MathF.Max(MathF.Max(box.Left - point.X, 0f), point.X - box.Right); + var dy = MathF.Max(MathF.Max(box.Bottom - point.Y, 0f), point.Y - box.Top); + + return dx * dx + dy * dy; + } + + private readonly record struct PendingLaunch( + EntityUid Weapon, + EntityUid User, + EntityCoordinates FireCoordinates, + EntityUid TargetGrid, + TimeSpan FireTime, + bool NoServer, + bool Fired = false, + TimeSpan ExpireTime = default); +} diff --git a/Content.Server/_Mono/FireControl/FireControlSystem.cs b/Content.Server/_Mono/FireControl/FireControlSystem.cs index cc22058ef00..369e279040c 100644 --- a/Content.Server/_Mono/FireControl/FireControlSystem.cs +++ b/Content.Server/_Mono/FireControl/FireControlSystem.cs @@ -35,6 +35,7 @@ public sealed partial class FireControlSystem : EntitySystem [Dependency] private PowerReceiverSystem _power = default!; [Dependency] private RotateToFaceSystem _rotateToFace = default!; [Dependency] private ShipWeaponHomeGridSystem _shipWeaponHomeGrid = default!; // Forge-Change + [Dependency] private ForgeTorpedoLauncherSystem _forgeTorpedoLauncher = default!; // Forge-Change /// /// Dictionary of entities that have visualization enabled /// @@ -43,6 +44,7 @@ public sealed partial class FireControlSystem : EntitySystem private EntityQuery _artilleryQuery; private EntityQuery _fireRotateQuery; private EntityQuery _gunQuery; + private EntityQuery _forgeTorpedoLauncherQuery; // Forge-Change public override void Initialize() { @@ -66,6 +68,7 @@ public override void Initialize() _artilleryQuery = GetEntityQuery(); _fireRotateQuery = GetEntityQuery(); _gunQuery = GetEntityQuery(); + _forgeTorpedoLauncherQuery = GetEntityQuery(); // Forge-Change } private void OnPowerChanged(EntityUid uid, FireControlServerComponent component, PowerChangedEvent args) @@ -533,6 +536,24 @@ public bool AttemptFire(EntityUid weapon, EntityUid user, EntityCoordinates coor // Calculate direction var direction = targetPos - weaponPos; + + // Forge-Change-Start: static torpedo tubes use the cursor only to pick a target grid. + var isTorpedoLauncher = _forgeTorpedoLauncherQuery.TryComp(weapon, out var torpedoLauncher); + if (isTorpedoLauncher) + { + direction = _forgeTorpedoLauncher.GetLaunchDirection(weaponXform); + + return _forgeTorpedoLauncher.TryQueueLaunch( + weapon, + user, + coords, + direction, + torpedoLauncher, + weaponXform, + noServer); + } + // Forge-Change-End + var distance = direction.Length(); if (distance <= float.Epsilon) return false; // Can't fire at the same position @@ -553,13 +574,12 @@ public bool AttemptFire(EntityUid weapon, EntityUid user, EntityCoordinates coor } // Try to get a gun component and fire the weapon - if (_gunQuery.TryComp(weapon, out var gun)) - { - _gun.AttemptShots(user, weapon, gun, coords, TimeSpan.FromSeconds(0.2)); - return true; - } + if (!_gunQuery.TryComp(weapon, out var gun)) + return false; - return false; + _gun.AttemptShots(user, weapon, gun, coords, TimeSpan.FromSeconds(0.2)); + + return true; } /// diff --git a/Content.Server/_Mono/Projectiles/TargetSeeking/TargetSeekingSystem.cs b/Content.Server/_Mono/Projectiles/TargetSeeking/TargetSeekingSystem.cs index 91828d4e7ce..cf0611fca68 100644 --- a/Content.Server/_Mono/Projectiles/TargetSeeking/TargetSeekingSystem.cs +++ b/Content.Server/_Mono/Projectiles/TargetSeeking/TargetSeekingSystem.cs @@ -297,7 +297,7 @@ public void AcquireTarget(EntityUid uid, TargetSeekingComponent component, Trans public Angle ApplyPredictiveTracking(Entity ent, Entity target, float frameTime) { // Get current positions - var currentTargetPosition = _transform.GetWorldPosition(target.Comp2); + var currentTargetPosition = GetTargetWorldPosition(target.Owner, target.Comp2); // Forge-Change var sourcePosition = _transform.GetWorldPosition(ent.Comp3); // Calculate current distance @@ -336,7 +336,7 @@ public Angle ApplyAdvancedTracking(Entity ent, Entity target, float frameTime) { // Get the angle directly toward the target - var angleToTarget = (_transform.GetWorldPosition(target.Comp) - _transform.GetWorldPosition(ent.Comp)).ToWorldAngle(); + var angleToTarget = (GetTargetWorldPosition(target.Owner, target.Comp) - _transform.GetWorldPosition(ent.Comp)).ToWorldAngle(); // Forge-Change return angleToTarget; } diff --git a/Resources/Locale/en-US/_Forge/research/ship_weapons.ftl b/Resources/Locale/en-US/_Forge/research/ship_weapons.ftl index 9a2b31e7a6b..7d88f8a1faf 100644 --- a/Resources/Locale/en-US/_Forge/research/ship_weapons.ftl +++ b/Resources/Locale/en-US/_Forge/research/ship_weapons.ftl @@ -14,6 +14,7 @@ forge-research-technology-shipweapon-prometheus = Prometheus Laser Cannon forge-research-technology-shipweapon-serpent = Serpent Torpedo Launcher forge-research-technology-shipweapon-tovek = TOVEK Missile Pod forge-research-technology-shipweapon-trident = Trident Torpedo Rack +forge-research-technology-shipweapon-heavy-torpedo = Gungnir Heavy Torpedo Launcher forge-research-technology-shipweapon-light-munitions-bay = Light Munitions Bay forge-research-technology-shipweapon-longbow = Longbow Autocannon forge-research-technology-shipweapon-cyrexa = CYREXA Cannon diff --git a/Resources/Locale/en-US/_Forge/ship_weapons.ftl b/Resources/Locale/en-US/_Forge/ship_weapons.ftl new file mode 100644 index 00000000000..3e153ec9d33 --- /dev/null +++ b/Resources/Locale/en-US/_Forge/ship_weapons.ftl @@ -0,0 +1 @@ +forge-torpedo-launcher-examine = It can launch torpedoes without direct line of sight, but the muzzle must point into empty space. diff --git a/Resources/Locale/ru-RU/_Forge/research/ship_weapons.ftl b/Resources/Locale/ru-RU/_Forge/research/ship_weapons.ftl index 608fcb171de..54b46c0f6c2 100644 --- a/Resources/Locale/ru-RU/_Forge/research/ship_weapons.ftl +++ b/Resources/Locale/ru-RU/_Forge/research/ship_weapons.ftl @@ -14,6 +14,7 @@ forge-research-technology-shipweapon-prometheus = Лазер «Прометей forge-research-technology-shipweapon-serpent = Торпедный аппарат «Змея» forge-research-technology-shipweapon-tovek = Ракетная установка TOVEK forge-research-technology-shipweapon-trident = Торпедная установка «Трезубец» +forge-research-technology-shipweapon-heavy-torpedo = Тяжёлая торпедная установка «Гунгнир» forge-research-technology-shipweapon-light-munitions-bay = Лёгкий бомболюк forge-research-technology-shipweapon-longbow = Автопушка Longbow forge-research-technology-shipweapon-cyrexa = Пушка CYREXA diff --git a/Resources/Locale/ru-RU/_Forge/ship_weapons.ftl b/Resources/Locale/ru-RU/_Forge/ship_weapons.ftl new file mode 100644 index 00000000000..11c1ba35ebc --- /dev/null +++ b/Resources/Locale/ru-RU/_Forge/ship_weapons.ftl @@ -0,0 +1 @@ +forge-torpedo-launcher-examine = Может запускать торпеды без прямой видимости цели, но дуло должно смотреть в пустой космос. diff --git a/Resources/Prototypes/_Forge/Entities/Objects/Devices/Circuitboards/Machine/ship_weapons.yml b/Resources/Prototypes/_Forge/Entities/Objects/Devices/Circuitboards/Machine/ship_weapons.yml index 52538c19cf8..999fd079ece 100644 --- a/Resources/Prototypes/_Forge/Entities/Objects/Devices/Circuitboards/Machine/ship_weapons.yml +++ b/Resources/Prototypes/_Forge/Entities/Objects/Devices/Circuitboards/Machine/ship_weapons.yml @@ -396,6 +396,18 @@ tags: - MachineBoard2x2 +- type: entity + id: WeaponTurretForgeHeavyTorpedoCircuitboard + parent: ForgeShipWeaponCircuitboardT4 + name: FHT-300 Gungnir heavy torpedo launcher machine board + description: A machine printed circuit board for an FHT-300 Gungnir heavy torpedo launcher. + components: + - type: MachineBoard + prototype: WeaponTurretForgeHeavyTorpedoPortable + - type: Tag + tags: + - MachineBoard2x2 + - type: entity id: WeaponTurretDrakeApolloMk2Circuitboard parent: ForgeShipWeaponCircuitboardT4 diff --git a/Resources/Prototypes/_Forge/Entities/SpaceArtillery/heavy_torpedo.yml b/Resources/Prototypes/_Forge/Entities/SpaceArtillery/heavy_torpedo.yml new file mode 100644 index 00000000000..02494c66924 --- /dev/null +++ b/Resources/Prototypes/_Forge/Entities/SpaceArtillery/heavy_torpedo.yml @@ -0,0 +1,158 @@ +- type: entity + id: WeaponTurretForgeHeavyTorpedoBase + parent: BallisticArtilleryTier4HP + abstract: true + name: FHT-300 Gungnir heavy torpedo launcher + description: A heavy torpedo launcher for heavy guided torpedoes. It can fire without direct line of sight, but needs a clear muzzle lane. + components: + - type: StaticPrice + price: 18000 + - type: Sprite + sprite: _Forge/Objects/ShuttleWeapons/gungnir.rsi + layers: + - state: space_artillery + - type: Appearance + - type: ForgeTorpedoLauncher + tileProbeDistance: 1.5 + clearanceRayDistance: 1.75 + - type: AmmoCounter + - type: Battery + maxCharge: 90000 + startingCharge: 90000 + - type: ExaminableBattery + - type: WirelessNetworkConnection + range: 500 + - type: Gun + fireRate: 0.08 + recoil: 20 + minAngle: 0 + maxAngle: 0 + shootThermalSignature: 48000000 + soundGunshot: + path: /Audio/Weapons/Guns/Gunshots/rocket_launcher.ogg + soundEmpty: + path: /Audio/Weapons/Guns/EmptyAlarm/smg_empty_alarm.ogg + - type: SpaceArtillery + powerChargeRate: 600 + powerUsePassive: 1800 + powerUseActive: 0 + - type: Fixtures + fixtures: + fix1: + shape: !type:PhysShapeAabb + bounds: "-0.5,-1.5,0.5,1.5" + density: 800 + mask: + - MachineMask + layer: + - MachineLayer + - type: BatterySelfRecharger + autoRecharge: true + autoRechargeRate: 250 + - type: Actions + - type: ProjectileBatteryAmmoProvider + proto: ShipTorpedoForgeHeavy + fireCost: 30000 + - type: RadarBlip + radarColor: "#ff9d00" + scale: 2.5 + visibleFromOtherGrids: true + gridConfig: + color: "#ff9d00" + shape: Square + respectZoom: true + rotate: true + bounds: "-0.5,-1.5,0.5,1.5" + - type: ShipGunType + shipType: Missile + - type: ShipGunClass + shipClass: Superheavy + - type: ShipRepairable + repairTime: 45 + repairCost: 160 + +- type: entity + id: WeaponTurretForgeHeavyTorpedo + parent: [WeaponTurretForgeHeavyTorpedoBase, BallisticArtilleryMapped] + suffix: For mapping + +- type: entity + id: WeaponTurretForgeHeavyTorpedoPortable + parent: [WeaponTurretForgeHeavyTorpedoBase, BallisticArtillery] + suffix: DO NOT MAP + +- type: entity + id: ShipTorpedoForgeHeavy + name: FHT-300 heavy torpedo + parent: BaseTorpedo + categories: [ HideSpawnMenu ] + components: + - type: Fixtures + fixtures: + projectile: + shape: !type:PhysShapeCircle + radius: 0.3 + density: 1500 + layer: + - WallLayer + mask: + - MachineMask + - BulletImpassable + - type: Projectile + damage: + types: + Structural: 4500 + Blunt: 900 + Heat: 450 + - type: Sprite + sprite: _Mono/Objects/SpaceArtillery/mini_rocket.rsi + layers: + - state: mini-rocket + scale: 1, 2 + - type: Ammo + muzzleFlash: null + - type: ShipWeaponProjectile + - type: RadarBlip + radarColor: "#ff9d00" + scale: 4 + shape: triangle + - type: TimedDespawn + lifetime: 22 + - type: PointLight + radius: 5 + color: orange + energy: 2 + - type: ExplodeOnTrigger + - type: Explosive + explosionType: Default + totalIntensity: 2500 + intensitySlope: 35 + maxIntensity: 220 + - type: GatheringProjectile + - type: MiningGatheringSoft + - type: MiningGatheringHard + - type: TargetSeeking + acceleration: 45 + detectionRange: 850 + scanArc: 55 + launchSpeed: 35 + maxSpeed: 90 + trackDelay: 2 + - type: Damageable + damageContainer: Inorganic + damageModifierSet: Metallic + - type: Meteor + damageTypes: + types: + Blunt: 1 + - type: Destructible + thresholds: + - trigger: !type:DamageTrigger + damage: 1200 + behaviors: + - !type:DoActsBehavior + acts: [ "Destruction" ] + - !type:ExplodeBehavior "" + - !type:PlaySoundBehavior + sound: + collection: MetalBreak diff --git a/Resources/Prototypes/_Forge/Recipes/Lathes/Packs/ship_weapons.yml b/Resources/Prototypes/_Forge/Recipes/Lathes/Packs/ship_weapons.yml index 72de0e30f98..704579f7d0b 100644 --- a/Resources/Prototypes/_Forge/Recipes/Lathes/Packs/ship_weapons.yml +++ b/Resources/Prototypes/_Forge/Recipes/Lathes/Packs/ship_weapons.yml @@ -24,3 +24,4 @@ - WeaponTurretCyrexaCircuitboard - WeaponTurretCharonReloadCircuitboard - WeaponLaserTurretApolloCircuitboard + - WeaponTurretForgeHeavyTorpedoCircuitboard diff --git a/Resources/Prototypes/_Forge/Recipes/Lathes/ship_weapons.yml b/Resources/Prototypes/_Forge/Recipes/Lathes/ship_weapons.yml index 8003c62a486..9e30a3477f6 100644 --- a/Resources/Prototypes/_Forge/Recipes/Lathes/ship_weapons.yml +++ b/Resources/Prototypes/_Forge/Recipes/Lathes/ship_weapons.yml @@ -182,3 +182,8 @@ parent: ForgeShipWeaponBoardRecipeT4 id: WeaponLaserTurretApolloCircuitboard result: WeaponLaserTurretApolloCircuitboard + +- type: latheRecipe + parent: ForgeShipWeaponBoardRecipeT4 + id: WeaponTurretForgeHeavyTorpedoCircuitboard + result: WeaponTurretForgeHeavyTorpedoCircuitboard diff --git a/Resources/Prototypes/_Forge/Research/ship_weapons.yml b/Resources/Prototypes/_Forge/Research/ship_weapons.yml index db45848f8dd..374871b4b5d 100644 --- a/Resources/Prototypes/_Forge/Research/ship_weapons.yml +++ b/Resources/Prototypes/_Forge/Research/ship_weapons.yml @@ -252,6 +252,19 @@ - ForgeShipWeaponResearchSerpent position: -11, 8 +- type: technology + id: ForgeShipWeaponResearchHeavyTorpedo + name: forge-research-technology-shipweapon-heavy-torpedo + entityIcon: WeaponTurretForgeHeavyTorpedo + discipline: ShipArmory + tier: 3 + cost: 32000 + recipeUnlocks: + - WeaponTurretForgeHeavyTorpedoCircuitboard + technologyPrerequisites: + - ForgeShipWeaponResearchTrident + position: -11, 9 + # --- Column -18: mining --- - type: technology diff --git a/Resources/Textures/_Forge/Objects/ShuttleWeapons/gungnir.rsi/meta.json b/Resources/Textures/_Forge/Objects/ShuttleWeapons/gungnir.rsi/meta.json new file mode 100644 index 00000000000..d5d633571dd --- /dev/null +++ b/Resources/Textures/_Forge/Objects/ShuttleWeapons/gungnir.rsi/meta.json @@ -0,0 +1,15 @@ +{ + "version": 1, + "license": "CC-BY-SA-3.0", + "copyright": "theg33ka for Forge-station", + "size": { + "x": 32, + "y": 86 + }, + "states": [ + { + "name": "space_artillery", + "directions": 1 + } + ] +} diff --git a/Resources/Textures/_Forge/Objects/ShuttleWeapons/gungnir.rsi/space_artillery.png b/Resources/Textures/_Forge/Objects/ShuttleWeapons/gungnir.rsi/space_artillery.png new file mode 100644 index 00000000000..b7cc45e2eed Binary files /dev/null and b/Resources/Textures/_Forge/Objects/ShuttleWeapons/gungnir.rsi/space_artillery.png differ