diff --git a/Content.Client/Weapons/Ranged/Systems/GunSystem.Ballistic.cs b/Content.Client/Weapons/Ranged/Systems/GunSystem.Ballistic.cs index d09661b770b..a1d9bc7abd9 100644 --- a/Content.Client/Weapons/Ranged/Systems/GunSystem.Ballistic.cs +++ b/Content.Client/Weapons/Ranged/Systems/GunSystem.Ballistic.cs @@ -36,7 +36,7 @@ protected override void Cycle(EntityUid uid, BallisticAmmoProviderComponent comp Containers.Remove(existing, component.Container); EnsureShootable(existing); } - else if (component.UnspawnedCount > 0) + else if (component.UnspawnedCount > 0 && !component.InfiniteUnspawned) { component.UnspawnedCount--; ent = Spawn(component.Proto, coordinates); diff --git a/Content.Client/_Mono/FireControl/UI/FireControlWindow.xaml.cs b/Content.Client/_Mono/FireControl/UI/FireControlWindow.xaml.cs index 11cd15a8ab6..8b5978efe91 100644 --- a/Content.Client/_Mono/FireControl/UI/FireControlWindow.xaml.cs +++ b/Content.Client/_Mono/FireControl/UI/FireControlWindow.xaml.cs @@ -1,6 +1,7 @@ // Copyright Rane (elijahrane@gmail.com) 2025 // All rights reserved. Relicensed under AGPL with permission +using System.Linq; using Content.Client.UserInterface.Controls; using Content.Shared._Mono.FireControl; using Content.Shared._Mono.ShipGuns; @@ -27,6 +28,8 @@ public sealed partial class FireControlWindow : FancyWindow // Dictionary to store weapon entity to type mapping private readonly Dictionary _weaponTypes = new(); + private FireControlConsoleBoundInterfaceState? _currentState; + public FireControlWindow() { RobustXamlLoader.Load(this); @@ -48,6 +51,8 @@ private void SelectAllWeapons(BaseButton.ButtonEventArgs args) } OnWeaponSelectionChanged?.Invoke(); + + UpdateAllWeaponButtonTexts(); } private void UnselectAllWeapons(BaseButton.ButtonEventArgs args) @@ -58,6 +63,8 @@ private void UnselectAllWeapons(BaseButton.ButtonEventArgs args) } OnWeaponSelectionChanged?.Invoke(); + + UpdateAllWeaponButtonTexts(); } private void SelectBallisticWeapons(BaseButton.ButtonEventArgs args) @@ -81,6 +88,7 @@ private void SelectBallisticWeapons(BaseButton.ButtonEventArgs args) } OnWeaponSelectionChanged?.Invoke(); + UpdateAllWeaponButtonTexts(); } private void SelectEnergyWeapons(BaseButton.ButtonEventArgs args) @@ -104,6 +112,7 @@ private void SelectEnergyWeapons(BaseButton.ButtonEventArgs args) } OnWeaponSelectionChanged?.Invoke(); + UpdateAllWeaponButtonTexts(); } private void SelectMissileWeapons(BaseButton.ButtonEventArgs args) @@ -127,6 +136,48 @@ private void SelectMissileWeapons(BaseButton.ButtonEventArgs args) } OnWeaponSelectionChanged?.Invoke(); + UpdateAllWeaponButtonTexts(); + } + + /// + /// Updates the text of a weapon button based on its selection state and manual reload status. + /// + private void UpdateWeaponButtonText(Button button, FireControllableEntry controllable) + { + if (button.Pressed && controllable.HasManualReload && controllable.AmmoCount.HasValue) + { + button.Text = Loc.GetString("gunnery-gun-select-ammo", ("name", controllable.Name), ("ammo", controllable.AmmoCount.Value)); + + if (controllable.AmmoCount.Value == 0) + { + button.ModulateSelfOverride = Color.Red; + } + else + { + button.ModulateSelfOverride = null; + } + } + else + { + button.Text = Loc.GetString("gunnery-gun-select", ("name", controllable.Name)); + button.ModulateSelfOverride = null; + } + } + + /// + /// Updates all weapon button texts based on current selection state. + /// + private void UpdateAllWeaponButtonTexts() + { + foreach (var (netEntity, button) in WeaponsList) + { + var controllable = _currentState?.FireControllables?.FirstOrDefault(c => c.NetEntity == netEntity); + + if (controllable.HasValue) + { + UpdateWeaponButtonText(button, controllable.Value); + } + } } private void SelectMiningWeapons(BaseButton.ButtonEventArgs args) @@ -151,6 +202,7 @@ private void SelectMiningWeapons(BaseButton.ButtonEventArgs args) public void UpdateStatus(FireControlConsoleBoundInterfaceState state) { + _currentState = state; NavRadar.UpdateState(state.NavState); if (state.Connected) @@ -167,6 +219,8 @@ public void UpdateStatus(FireControlConsoleBoundInterfaceState state) UpdateWeaponsList(state); + UpdateAllWeaponButtonTexts(); + // Update the category buttons state based on whether weapons of that type are available bool hasBallisticWeapons = false; bool hasEnergyWeapons = false; @@ -226,6 +280,7 @@ private void UpdateWeaponsList(FireControlConsoleBoundInterfaceState state) if (WeaponsList.TryGetValue(controllable.NetEntity, out var existingButton)) { toRemove.Remove(controllable.NetEntity); + UpdateWeaponButtonText(existingButton, controllable); } else { @@ -238,10 +293,16 @@ private void UpdateWeaponsList(FireControlConsoleBoundInterfaceState state) Margin = new Thickness(4, 1) }; - button.OnToggled += _ => OnWeaponSelectionChanged?.Invoke(); + button.OnToggled += _ => + { + OnWeaponSelectionChanged?.Invoke(); + UpdateAllWeaponButtonTexts(); + }; ControllablesBox.AddChild(button); WeaponsList.Add(controllable.NetEntity, button); + + UpdateWeaponButtonText(button, controllable); } } diff --git a/Content.Server/Lightning/LightningSystem.cs b/Content.Server/Lightning/LightningSystem.cs index 8b0a18afb35..f9fb77bd7f8 100644 --- a/Content.Server/Lightning/LightningSystem.cs +++ b/Content.Server/Lightning/LightningSystem.cs @@ -4,6 +4,7 @@ using Content.Server.Lightning.Components; using Content.Shared.Lightning; using Robust.Server.GameObjects; +using Robust.Shared.Prototypes; // Mono using Robust.Shared.Random; namespace Content.Server.Lightning; @@ -19,6 +20,7 @@ namespace Content.Server.Lightning; public sealed class LightningSystem : SharedLightningSystem { [Dependency] private readonly BeamSystem _beam = default!; + [Dependency] private readonly IPrototypeManager _proto = default!; // Mono [Dependency] private readonly IRobustRandom _random = default!; [Dependency] private readonly EntityLookupSystem _lookup = default!; [Dependency] private readonly TransformSystem _transform = default!; @@ -48,6 +50,18 @@ private void OnRemove(EntityUid uid, LightningComponent component, ComponentRemo /// The prototype for the lightning to be created /// if the lightnings being fired should trigger lightning events. public void ShootLightning(EntityUid user, EntityUid target, string lightningPrototype = "Lightning", bool triggerLightningEvents = true) + { + // Mono + EntProtoId? spawnOnHit = null; + var proto = _proto.Index(lightningPrototype); + if (proto.TryGetComponent(out var lightningComp, EntityManager.ComponentFactory)) + spawnOnHit = lightningComp.SpawnOnHit; + + ShootLightning(user, target, lightningPrototype, triggerLightningEvents); + } + + // Mono - for optimisation purposes + private void ShootLightning(EntityUid user, EntityUid target, EntProtoId? spawnOnHit, string lightningPrototype = "Lightning", bool triggerLightningEvents = true) { var spriteState = LightningRandomizer(); _beam.TryCreateBeam(user, target, lightningPrototype, spriteState); @@ -57,6 +71,9 @@ public void ShootLightning(EntityUid user, EntityUid target, string lightningPro var ev = new HitByLightningEvent(user, target); RaiseLocalEvent(target, ref ev); } + + if (spawnOnHit != null) + Spawn(spawnOnHit.Value, _transform.GetMapCoordinates(target)); } @@ -70,6 +87,18 @@ public void ShootLightning(EntityUid user, EntityUid target, string lightningPro /// how many times to recursively fire lightning bolts from the target points of the first shot. /// if the lightnings being fired should trigger lightning events. public void ShootRandomLightnings(EntityUid user, float range, int boltCount, string lightningPrototype = "Lightning", int arcDepth = 0, bool triggerLightningEvents = true) + { + // Mono + EntProtoId? spawnOnHit = null; + var proto = _proto.Index(lightningPrototype); + if (proto.TryGetComponent(out var lightningComp, EntityManager.ComponentFactory)) + spawnOnHit = lightningComp.SpawnOnHit; + + ShootRandomLightnings(user, range, boltCount, spawnOnHit, lightningPrototype, arcDepth, triggerLightningEvents); + } + + // Mono - for optimisation purposes + private void ShootRandomLightnings(EntityUid user, float range, int boltCount, EntProtoId? spawnOnHit, string lightningPrototype = "Lightning", int arcDepth = 0, bool triggerLightningEvents = true) { //TODO: add support to different priority target tablem for different lightning types //TODO: Remove Hardcode LightningTargetComponent (this should be a parameter of the SharedLightningComponent) @@ -92,10 +121,10 @@ public void ShootRandomLightnings(EntityUid user, float range, int boltCount, st if (!_random.Prob(curTarget.Comp.HitProbability)) //Chance to ignore target continue; - ShootLightning(user, targets[count].Owner, lightningPrototype, triggerLightningEvents); + ShootLightning(user, targets[count].Owner, spawnOnHit, lightningPrototype, triggerLightningEvents); if (arcDepth - targets[count].Comp.LightningResistance > 0) { - ShootRandomLightnings(targets[count].Owner, range, 1, lightningPrototype, arcDepth - targets[count].Comp.LightningResistance, triggerLightningEvents); + ShootRandomLightnings(targets[count].Owner, range, 1, spawnOnHit, lightningPrototype, arcDepth - targets[count].Comp.LightningResistance, triggerLightningEvents); } shootedCount++; } diff --git a/Content.Server/Weapons/Ranged/Systems/GunSystem.Ballistic.cs b/Content.Server/Weapons/Ranged/Systems/GunSystem.Ballistic.cs index 60680deaaaa..3fe2ab77d69 100644 --- a/Content.Server/Weapons/Ranged/Systems/GunSystem.Ballistic.cs +++ b/Content.Server/Weapons/Ranged/Systems/GunSystem.Ballistic.cs @@ -6,6 +6,16 @@ namespace Content.Server.Weapons.Ranged.Systems; public sealed partial class GunSystem { + /// + /// Adds ammo to a ballistic ammo provider by incrementing UnspawnedCount. + /// + public void AddBallisticAmmo(EntityUid uid, BallisticAmmoProviderComponent component, int amount = 1) + { + component.UnspawnedCount += amount; + + DirtyField(uid, component, nameof(BallisticAmmoProviderComponent.UnspawnedCount)); + } + protected override void Cycle(EntityUid uid, BallisticAmmoProviderComponent component, MapCoordinates coordinates) { EntityUid? ent = null; @@ -18,9 +28,10 @@ protected override void Cycle(EntityUid uid, BallisticAmmoProviderComponent comp DirtyField(uid, component, nameof(BallisticAmmoProviderComponent.Entities)); Containers.Remove(existing, component.Container); + ent = existing; //Mono: Sound bugfix EnsureShootable(existing); } - else if (component.UnspawnedCount > 0) + else if (component.UnspawnedCount > 0 && !component.InfiniteUnspawned) // Mono - no ammo generator { component.UnspawnedCount--; DirtyField(uid, component, nameof(BallisticAmmoProviderComponent.UnspawnedCount)); diff --git a/Content.Server/Weapons/Ranged/Systems/GunSystem.cs b/Content.Server/Weapons/Ranged/Systems/GunSystem.cs index 13f77b07bae..133d2bdd1d1 100644 --- a/Content.Server/Weapons/Ranged/Systems/GunSystem.cs +++ b/Content.Server/Weapons/Ranged/Systems/GunSystem.cs @@ -52,7 +52,7 @@ public override void Initialize() private void OnBallisticPrice(EntityUid uid, BallisticAmmoProviderComponent component, ref PriceCalculationEvent args) { - if (string.IsNullOrEmpty(component.Proto) || component.UnspawnedCount == 0) + if (string.IsNullOrEmpty(component.Proto) || component.UnspawnedCount == 0 || component.InfiniteUnspawned) // Mono return; if (!ProtoManager.TryIndex(component.Proto, out var proto)) diff --git a/Content.Server/_Mono/AmmoLoader/AmmoLoaderSystem.cs b/Content.Server/_Mono/AmmoLoader/AmmoLoaderSystem.cs new file mode 100644 index 00000000000..4bd723ec599 --- /dev/null +++ b/Content.Server/_Mono/AmmoLoader/AmmoLoaderSystem.cs @@ -0,0 +1,399 @@ +using System.Linq; +using Content.Server.DeviceLinking.Systems; +using Content.Server.Weapons.Ranged.Systems; +using Content.Shared._Mono.AmmoLoader; +using Content.Server._Mono.SpaceArtillery.Components; +using Content.Shared.Containers.ItemSlots; +using Content.Shared.DeviceLinking; +using Content.Shared.DeviceLinking.Events; +using Content.Shared.Interaction; +using Content.Shared.Popups; +using Content.Shared.Verbs; +using Content.Shared.Weapons.Ranged.Components; +using Content.Shared.Weapons.Ranged; +using Content.Shared.Weapons.Ranged.Events; +using Content.Shared.Whitelist; +using Robust.Shared.Containers; +using Robust.Shared.Utility; + +namespace Content.Server._Mono.AmmoLoader; + +public sealed class AmmoLoaderSystem : EntitySystem +{ + [Dependency] private readonly SharedContainerSystem _containers = default!; + [Dependency] private readonly DeviceLinkSystem _deviceLink = default!; + [Dependency] private readonly SharedPopupSystem _popup = default!; + [Dependency] private readonly GunSystem _gun = default!; + [Dependency] private readonly ItemSlotsSystem _slots = default!; + [Dependency] private readonly EntityWhitelistSystem _whitelistSystem = default!; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnComponentInit); + SubscribeLocalEvent(OnInteractHand); + SubscribeLocalEvent>(AddFlushVerb); + SubscribeLocalEvent(OnAfterInteractUsing); + SubscribeLocalEvent(OnLinkAttempt); + } + + private void OnLinkAttempt(Entity ent, ref LinkAttemptEvent args) + { + if (args.Source != ent.Owner) + return; + + if (TryComp(ent, out var sourceComponent) && + sourceComponent.LinkedPorts.Count > ent.Comp.MaxConnections) + { + args.Cancel(); + } + } + + private void OnComponentInit(Entity ent, ref ComponentInit args) + { + ent.Comp.Container = _containers.EnsureContainer(ent, AmmoLoaderComponent.ContainerId); + + _deviceLink.EnsureSourcePorts(ent, ent.Comp.LoadPort); + } + + private void OnInteractHand(Entity ent, ref InteractHandEvent args) + { + if (args.Handled) + return; + + if (ent.Comp.Container.ContainedEntities.Count > 0) + { + TryEjectContents(ent, ent.Comp); + args.Handled = true; + } + } + + private void AddFlushVerb(Entity ent, ref GetVerbsEvent args) + { + if (!args.CanAccess || !args.CanInteract) + return; + + if (ent.Comp.Container.ContainedEntities.Count == 0) + return; + + var user = args.User; + + var ejectableCount = 0; + + foreach (var contained in ent.Comp.Container.ContainedEntities) + { + if (_containers.CanRemove(contained, ent.Comp.Container)) + ejectableCount++; + } + + if (ejectableCount > 0) + { + AlternativeVerb ejectVerb = new() + { + Act = () => TryEjectContents(ent, ent.Comp), + Category = VerbCategory.Eject, + Text = Loc.GetString("ammo-loader-eject-verb"), + Icon = new SpriteSpecifier.Texture(new("/Textures/Interface/VerbIcons/eject.svg.192dpi.png")), + Priority = 0, + }; + args.Verbs.Add(ejectVerb); + } + + var linkedArtillery = GetLinkedArtillery(ent); + + foreach (var artillery in linkedArtillery) + { + var artilleryName = MetaData(artillery).EntityName; + var artilleryId = artillery.ToString(); + + var (ammoCount, ammoCapacity) = (0, 0); + if (_gun.TryGetGun(artillery, out var gunUid, out _)) + { + if (TryComp(gunUid, out _)) + { + var ev = new GetAmmoCountEvent(); + RaiseLocalEvent(gunUid, ref ev, false); + ammoCount = ev.Count; + ammoCapacity = ev.Capacity; + } + else if (TryComp(gunUid, out var ammoProvider)) + { + ammoCount = ammoProvider.Count; + ammoCapacity = ammoProvider.Capacity; + } + } + + AlternativeVerb flushVerb = new() + { + Act = () => TryFlushToArtillery(ent, ent.Comp, artillery, user), + Text = Loc.GetString("ammo-loader-flush-to-artillery-with-ammo-and-id", + ("artillery", artilleryName), + ("ammo", ammoCount), + ("capacity", ammoCapacity), + ("id", artilleryId)), + Icon = new SpriteSpecifier.Texture(new("/Textures/Interface/VerbIcons/eject.svg.192dpi.png")), + Priority = 1, + }; + args.Verbs.Add(flushVerb); + } + } + + private void OnAfterInteractUsing(Entity ent, ref AfterInteractUsingEvent args) + { + if (args.Handled || !args.CanReach) + return; + + if (ent.Comp.Container.ContainedEntities.Count >= ent.Comp.MaxCapacity) + { + _popup.PopupEntity(Loc.GetString("ammo-loader-insert-fail"), ent, args.User); + args.Handled = true; + return; + } + + if (!CanInsert(ent, ent.Comp, args.Used)) + return; + + if (_containers.Insert(args.Used, ent.Comp.Container)) + { + _popup.PopupEntity(Loc.GetString("ammo-loader-insert-success"), ent, args.User); + args.Handled = true; + } + } + + private bool CanInsert(Entity ent, AmmoLoaderComponent component, EntityUid entity) + { + if (!Transform(ent).Anchored) + return false; + + if (!_containers.CanInsert(entity, component.Container)) + return false; + + if (!HasComp(entity) && + !HasComp(entity) && + !HasComp(entity)) + return false; + + return true; + } + + private void TryEjectContents(Entity ent, AmmoLoaderComponent component) + { + foreach (var entity in component.Container.ContainedEntities.ToArray()) + { + _containers.Remove(entity, component.Container); + } + } + + private bool ValidateFlush(Entity ent, AmmoLoaderComponent component, EntityUid user) + { + if (!Transform(ent).Anchored) + { + _popup.PopupEntity(Loc.GetString("ammo-loader-not-anchored"), ent, user); + return false; + } + + if (component.Container.ContainedEntities.Count == 0) + { + _popup.PopupEntity(Loc.GetString("ammo-loader-empty"), ent, user); + return false; + } + + return true; + } + + private void TryFlushToArtillery(Entity ent, AmmoLoaderComponent component, EntityUid artillery, EntityUid user) + { + if (!ValidateFlush(ent, component, user)) + return; + + component.Engaged = true; + Dirty(ent, component); + + var artilleryName = MetaData(artillery).EntityName; + if (TryTransferAmmoTo(ent, artillery)) + { + _popup.PopupEntity(Loc.GetString("ammo-loader-flushed-to-artillery", ("artillery", artilleryName)), ent, user); + } + else + { + _popup.PopupEntity(Loc.GetString("ammo-loader-transfer-failed-to-artillery", ("artillery", artilleryName)), ent, user); + component.Engaged = false; + Dirty(ent, component); + } + } + + private List GetLinkedArtillery(Entity loader) + { + var linkedArtillery = new List(); + + if (!TryComp(loader, out var sourceComponent)) + return linkedArtillery; + + foreach (var (linkedEntity, portLinks) in sourceComponent.LinkedPorts) + { + foreach (var (sourcePort, sinkPort) in portLinks) + { + if (sourcePort == loader.Comp.LoadPort) + { + if (HasComp(linkedEntity)) + { + if (!linkedArtillery.Contains(linkedEntity)) + { + linkedArtillery.Add(linkedEntity); + } + + break; + } + } + } + } + + return linkedArtillery; + } + + private bool IsAmmoCompatible(Entity loader, EntityUid artillery, EntityUid ammoEntity) + { + if (!_gun.TryGetGun(artillery, out var gunUid, out _)) + return false; + + if (TryComp(gunUid, out _)) + { + if (TryComp(ammoEntity, out _)) + { + if (TryComp(gunUid, out var itemSlots)) + { + var magazineSlot = itemSlots.Slots.GetValueOrDefault("gun_magazine"); + if (magazineSlot != null) + { + return !_whitelistSystem.IsWhitelistFailOrNull(magazineSlot.Whitelist, ammoEntity) && + !_whitelistSystem.IsBlacklistPass(magazineSlot.Blacklist, ammoEntity); + } + } + return false; + } + } + + if (TryComp(gunUid, out var artilleryAmmo)) + { + if (TryComp(ammoEntity, out _)) + return false; + + if (HasComp(ammoEntity) || HasComp(ammoEntity)) + { + return !_whitelistSystem.IsWhitelistFailOrNull(artilleryAmmo.Whitelist, ammoEntity); + } + } + + return false; + } + + public bool TryTransferAmmoTo(Entity loader, EntityUid artillery) + { + if (loader.Comp.Container.ContainedEntities.Count == 0) + return false; + + var successCount = 0; + + foreach (var ammoEntity in loader.Comp.Container.ContainedEntities.ToArray()) + { + if (!IsAmmoCompatible(loader, artillery, ammoEntity)) + continue; + + if (TryTransferSingleAmmo(loader, artillery, ammoEntity)) + { + successCount++; + } + } + + return successCount > 0; + } + + private bool TryTransferSingleAmmo(Entity loader, EntityUid artillery, EntityUid ammoEntity) + { + if (!_gun.TryGetGun(artillery, out var gunUid, out _)) + return false; + + if (TryComp(gunUid, out _)) + { + if (TryComp(ammoEntity, out _)) + { + _containers.Remove(ammoEntity, loader.Comp.Container); + + if (TryComp(gunUid, out var itemSlots)) + { + var magazineSlot = itemSlots.Slots.GetValueOrDefault("gun_magazine"); + if (magazineSlot != null) + { + if (magazineSlot.HasItem) + { + _slots.TryEject(gunUid, "gun_magazine", null, out var ejectedMag, excludeUserAudio: true); + } + + if (_slots.TryInsert(gunUid, magazineSlot, ammoEntity, null, excludeUserAudio: true)) + { + return true; + } + } + } + + _containers.Insert(ammoEntity, loader.Comp.Container); + return false; + } + } + + if (!TryComp(gunUid, out var artilleryAmmo)) + return false; + + if (TryComp(ammoEntity, out var magazineAmmoProvider)) + { + _containers.Remove(ammoEntity, loader.Comp.Container); + + foreach (var existingAmmo in artilleryAmmo.Container.ContainedEntities.ToArray()) + { + _containers.Remove(existingAmmo, artilleryAmmo.Container); + Del(existingAmmo); + } + + foreach (var bullet in magazineAmmoProvider.Container.ContainedEntities.ToArray()) + { + if (artilleryAmmo.Count >= artilleryAmmo.Capacity) + break; + + _containers.Remove(bullet, magazineAmmoProvider.Container); + _containers.Insert(bullet, artilleryAmmo.Container); + } + + Del(ammoEntity); + + Dirty(gunUid, artilleryAmmo); + return true; + } + + if (artilleryAmmo.Count >= artilleryAmmo.Capacity) + { + _containers.Remove(ammoEntity, loader.Comp.Container); + Dirty(loader, loader.Comp); + return false; + } + + if (HasComp(ammoEntity)) + { + _containers.Remove(ammoEntity, loader.Comp.Container); + _containers.Insert(ammoEntity, artilleryAmmo.Container); + Dirty(gunUid, artilleryAmmo); + return true; + } + + if (HasComp(ammoEntity)) + { + _containers.Remove(ammoEntity, loader.Comp.Container); + _gun.AddBallisticAmmo(gunUid, artilleryAmmo, 1); + Del(ammoEntity); + return true; + } + + return false; + } +} + diff --git a/Content.Server/_Mono/FireControl/FireControlSystem.Console.cs b/Content.Server/_Mono/FireControl/FireControlSystem.Console.cs index 30ac4f40c4e..a2d7b163586 100644 --- a/Content.Server/_Mono/FireControl/FireControlSystem.Console.cs +++ b/Content.Server/_Mono/FireControl/FireControlSystem.Console.cs @@ -8,7 +8,10 @@ using Content.Shared.Power; using Content.Shared.Shuttles.BUIStates; using Content.Shared.UserInterface; +using Content.Shared.Weapons.Ranged; +using Content.Shared.Weapons.Ranged.Components; using Robust.Server.GameObjects; +using Robust.Shared.Containers; namespace Content.Server._Mono.FireControl; @@ -19,6 +22,7 @@ public sealed partial class FireControlSystem : EntitySystem [Dependency] private readonly TransformSystem _transform = default!; [Dependency] private readonly CrewedShuttleSystem _crewedShuttle = default!; [Dependency] private readonly SharedPopupSystem _popup = default!; + [Dependency] private readonly SharedContainerSystem _containers = default!; private bool _completedCheck = false; @@ -120,6 +124,8 @@ private void OnFire(EntityUid uid, FireControlConsoleComponent component, FireCo // Fire the actual weapons FireWeapons((EntityUid)component.ConnectedServer, args.Selected, args.Coordinates, server); + UpdateUi(uid, component); + // Raise an event to track the cursor position even when not firing var fireEvent = new FireControlConsoleFireEvent(args.Coordinates, args.Selected); RaiseLocalEvent(uid, fireEvent); @@ -226,6 +232,10 @@ private void UpdateUi(EntityUid uid, FireControlConsoleComponent? component = nu controlled.Coordinates = GetNetCoordinates(Transform(controllable).Coordinates); controlled.Name = MetaData(controllable).EntityName; + var (ammoCount, hasManualReload) = GetWeaponAmmunitionInfo(controllable); + controlled.AmmoCount = ammoCount; + controlled.HasManualReload = hasManualReload; + controllables.Add(controlled); } } @@ -235,4 +245,59 @@ private void UpdateUi(EntityUid uid, FireControlConsoleComponent? component = nu var state = new FireControlConsoleBoundInterfaceState(component.ConnectedServer != null, array, navState); _ui.SetUiState(uid, FireControlConsoleUiKey.Key, state); } + + /// + /// Gets ammo information for a weapon to determine if it has manual reload. + /// + private (int? ammoCount, bool hasManualReload) GetWeaponAmmunitionInfo(EntityUid weaponEntity) + { + if (TryComp(weaponEntity, out var basicAmmo)) + { + var hasRecharge = HasComp(weaponEntity); + + return (basicAmmo.Count, !hasRecharge); + } + + if (TryComp(weaponEntity, out var ballisticAmmo)) + { + // if we're InfiniteUnspawned consider us to be non-reloading when at 0 ammo + return (ballisticAmmo.Count, ballisticAmmo.Cycleable && (ballisticAmmo.Count != 0 || !ballisticAmmo.InfiniteUnspawned)); + } + + if (TryComp(weaponEntity, out var magazineAmmo)) + { + var magazineEntity = (weaponEntity); + if (magazineEntity != null) + { + if (TryComp(magazineEntity, out var magazineBallisticAmmo)) + { + var hasAmmo = magazineBallisticAmmo.Cycleable + && (magazineBallisticAmmo.Count != 0 || !magazineBallisticAmmo.InfiniteUnspawned); + return (magazineBallisticAmmo.Count, hasAmmo); + } + + if (TryComp(magazineEntity, out var magazineBasicAmmo)) + { + var hasRecharge = HasComp(magazineEntity); + return (magazineBasicAmmo.Count, !hasRecharge); + } + } + } + + return (null, false); + } + + /// + /// Gets the magazine entity from a weapon's magazine slot. + /// + private EntityUid? GetMagazineEntity(EntityUid weaponEntity) + { + if (!_containers.TryGetContainer(weaponEntity, "gun_magazine", out var container) || + container is not ContainerSlot slot) + { + return null; + } + + return slot.ContainedEntity; + } } diff --git a/Content.Server/_Mono/Gatherable/GatherableSystemHitscan.cs b/Content.Server/_Mono/Gatherable/GatherableSystemHitscan.cs new file mode 100644 index 00000000000..fbcf2aa2eaf --- /dev/null +++ b/Content.Server/_Mono/Gatherable/GatherableSystemHitscan.cs @@ -0,0 +1,22 @@ +using Content.Server.Gatherable; +using Content.Server.Gatherable.Components; +using Content.Shared.Weapons.Hitscan.Events; + +namespace Content.Server._Mono.Gatherable; + +public sealed class GatherableSystemHitscan : EntitySystem +{ + [Dependency] private readonly GatherableSystem _gather = default!; + public override void Initialize() + { + SubscribeLocalEvent(OnHitscanHit); + } + + private void OnHitscanHit(Entity ent, ref HitscanDamageDealtEvent ev) + { + if (!TryComp(ev.Target, out var gatherable)) + return; + + _gather.Gather(ev.Target, ent, gatherable); + } +} diff --git a/Content.Server/_Mono/Gatherable/HitscanGatheringComponent.cs b/Content.Server/_Mono/Gatherable/HitscanGatheringComponent.cs new file mode 100644 index 00000000000..5ff54913ff7 --- /dev/null +++ b/Content.Server/_Mono/Gatherable/HitscanGatheringComponent.cs @@ -0,0 +1,10 @@ +namespace Content.Server._Mono.Gatherable; + +/// +/// Allows to destroy entities with GatherableComponent for hitscans. +/// +[RegisterComponent] +public sealed partial class HitscanGatheringComponent : Component +{ + +} diff --git a/Content.Server/_Mono/SpaceArtillery/Components/SpaceArtilleryComponent.cs b/Content.Server/_Mono/SpaceArtillery/Components/SpaceArtilleryComponent.cs index 358d66c3118..ccda95df825 100644 --- a/Content.Server/_Mono/SpaceArtillery/Components/SpaceArtilleryComponent.cs +++ b/Content.Server/_Mono/SpaceArtillery/Components/SpaceArtilleryComponent.cs @@ -37,4 +37,10 @@ public sealed partial class SpaceArtilleryComponent : Component [DataField("spaceArtilleryFirePort", customTypeSerializer: typeof(PrototypeIdSerializer))] public string SpaceArtilleryFirePort = "SpaceArtilleryFire"; + /// + /// Signal port for receiving ammo from an ammo loader. + /// + [DataField("spaceArtilleryLoadPort", customTypeSerializer: typeof(PrototypeIdSerializer))] + public string SpaceArtilleryLoadPort = "SpaceArtilleryLoad"; + } diff --git a/Content.Server/_Mono/SpaceArtillery/SpaceArtillerySystem.cs b/Content.Server/_Mono/SpaceArtillery/SpaceArtillerySystem.cs index 5231d6029f8..8ba69aba198 100644 --- a/Content.Server/_Mono/SpaceArtillery/SpaceArtillerySystem.cs +++ b/Content.Server/_Mono/SpaceArtillery/SpaceArtillerySystem.cs @@ -1,10 +1,12 @@ using System.Numerics; +using Content.Server._Mono.AmmoLoader; using Content.Server._Mono.FireControl; using Content.Shared.DeviceLinking.Events; using Content.Server.DeviceLinking.Systems; using Content.Server.Power.Components; using Content.Server.Power.EntitySystems; using Content.Server.Weapons.Ranged.Systems; +using Content.Shared._Mono.AmmoLoader; using Content.Shared._Mono.ShipGuns; using Content.Shared._Mono.SpaceArtillery; using Content.Shared.Camera; @@ -27,6 +29,7 @@ public sealed partial class SpaceArtillerySystem : EntitySystem [Dependency] private readonly SharedTransformSystem _xform = default!; [Dependency] private readonly SharedCameraRecoilSystem _recoilSystem = default!; [Dependency] private readonly FireControlSystem _fireControl = default!; + [Dependency] private readonly AmmoLoaderSystem _ammoLoader = default!; private const float DISTANCE = 100; private const float BIG_DAMAGE = 1000; @@ -52,6 +55,15 @@ private void OnSignalReceived(EntityUid uid, SpaceArtilleryComponent component, if (!TryComp(uid, out var source)) return; + if (args.Port == component.SpaceArtilleryLoadPort) + { + if (TryComp(args.Trigger, out var loader) && args.Trigger != null) + { + _ammoLoader.TryTransferAmmoTo(new Entity(args.Trigger.Value, loader), uid); + } + return; + } + if (args.Port != component.SpaceArtilleryFirePort) OnMalfunction(uid, component); diff --git a/Content.Shared/Lightning/Components/SharedLightningComponent.cs b/Content.Shared/Lightning/Components/SharedLightningComponent.cs index 396f2710f3f..a5406fe97fc 100644 --- a/Content.Shared/Lightning/Components/SharedLightningComponent.cs +++ b/Content.Shared/Lightning/Components/SharedLightningComponent.cs @@ -54,4 +54,11 @@ public abstract partial class SharedLightningComponent : Component /// [DataField("collisionMask")] public int CollisionMask = (int) (CollisionGroup.MobMask | CollisionGroup.MachineMask); + + // Mono + /// + /// Entity to spawn on the hit target, if any. + /// + [DataField] + public EntProtoId? SpawnOnHit = null; } diff --git a/Content.Shared/Weapons/Ranged/Components/BallisticAmmoProviderComponent.cs b/Content.Shared/Weapons/Ranged/Components/BallisticAmmoProviderComponent.cs index ee806125e78..2b17b88bfa8 100644 --- a/Content.Shared/Weapons/Ranged/Components/BallisticAmmoProviderComponent.cs +++ b/Content.Shared/Weapons/Ranged/Components/BallisticAmmoProviderComponent.cs @@ -22,7 +22,7 @@ public sealed partial class BallisticAmmoProviderComponent : Component [ViewVariables(VVAccess.ReadWrite), DataField] public int Capacity = 30; - public int Count => UnspawnedCount + Container.ContainedEntities.Count; + public int Count => Container.ContainedEntities.Count + (InfiniteUnspawned ? 0 : UnspawnedCount); // Mono [ViewVariables(VVAccess.ReadWrite), DataField, AutoNetworkedField] public int UnspawnedCount; @@ -57,4 +57,17 @@ public sealed partial class BallisticAmmoProviderComponent : Component /// [DataField] public TimeSpan FillDelay = TimeSpan.FromSeconds(0.5); + + /// + /// Goobstation - is ammo automatically ejected after each shot + /// + [DataField] + public bool AutoCycle = true; + + /// + /// Monolith - whether to ignore UnspawnedCount and be able to fire infinitely + /// Will prioritise and fire/spend entity ammo if loaded + /// + [DataField] + public bool InfiniteUnspawned = false; } diff --git a/Content.Shared/Weapons/Ranged/Events/TakeAmmoEvent.cs b/Content.Shared/Weapons/Ranged/Events/TakeAmmoEvent.cs index 27b1083cf36..d5bddd96d4c 100644 --- a/Content.Shared/Weapons/Ranged/Events/TakeAmmoEvent.cs +++ b/Content.Shared/Weapons/Ranged/Events/TakeAmmoEvent.cs @@ -5,7 +5,7 @@ namespace Content.Shared.Weapons.Ranged.Events; /// /// Raised on a gun when it would like to take the specified amount of ammo. /// -public sealed class TakeAmmoEvent : EntityEventArgs +public class TakeAmmoEvent : EntityEventArgs // Mono: unseal { public readonly EntityUid? User; public readonly int Shots; diff --git a/Content.Shared/Weapons/Ranged/Systems/SharedGunSystem.Ballistic.cs b/Content.Shared/Weapons/Ranged/Systems/SharedGunSystem.Ballistic.cs index 92363b916d3..2c16986a768 100644 --- a/Content.Shared/Weapons/Ranged/Systems/SharedGunSystem.Ballistic.cs +++ b/Content.Shared/Weapons/Ranged/Systems/SharedGunSystem.Ballistic.cs @@ -136,7 +136,7 @@ private void OnBallisticAmmoFillDoAfter(EntityUid uid, BallisticAmmoProviderComp } // End Frontier - if (component.Entities.Count + component.UnspawnedCount == 0) + if (GetBallisticShots(component) == 0) // Mono { Popup( Loc.GetString("gun-ballistic-transfer-empty", @@ -196,7 +196,7 @@ void SimulateInsertAmmo(EntityUid ammo, EntityUid ammoProvider, EntityCoordinate else if (revolverTarget is not null) moreSpace = GetRevolverCount(revolverTarget) < revolverTarget.Capacity; // End Frontier - var moreAmmo = component.Entities.Count + component.UnspawnedCount > 0; + var moreAmmo = GetBallisticShots(component) > 0; // Mono args.Repeat = moreSpace && moreAmmo && validAmmoType; // Frontier: do not repeat reload attempts with invalid ammo. } @@ -222,7 +222,11 @@ private void OnBallisticExamine(EntityUid uid, BallisticAmmoProviderComponent co if (!args.IsInDetailsRange) return; - args.PushMarkup(Loc.GetString("gun-magazine-examine", ("color", AmmoExamineColor), ("count", GetBallisticShots(component)))); + // Mono + if (component.InfiniteUnspawned) + args.PushMarkup(Loc.GetString("gun-magazine-infinite-examine", ("color", AmmoExamineSpecialColor), ("count", GetBallisticShots(component)))); + else + args.PushMarkup(Loc.GetString("gun-magazine-examine", ("color", AmmoExamineColor), ("count", GetBallisticShots(component)))); } private void ManualCycle(EntityUid uid, BallisticAmmoProviderComponent component, MapCoordinates coordinates, EntityUid? user = null, GunComponent? gunComp = null) @@ -275,7 +279,7 @@ private void OnBallisticMapInit(EntityUid uid, BallisticAmmoProviderComponent co protected int GetBallisticShots(BallisticAmmoProviderComponent component) { - return component.Entities.Count + component.UnspawnedCount; + return component.Entities.Count + (component.InfiniteUnspawned ? 0 : component.UnspawnedCount); // Mono } private void OnBallisticTakeAmmo(EntityUid uid, BallisticAmmoProviderComponent component, TakeAmmoEvent args) @@ -293,10 +297,14 @@ private void OnBallisticTakeAmmo(EntityUid uid, BallisticAmmoProviderComponent c DirtyField(uid, component, nameof(BallisticAmmoProviderComponent.Entities)); Containers.Remove(entity, component.Container); } - else if (component.UnspawnedCount > 0) + else if (component.UnspawnedCount > 0 + || component.InfiniteUnspawned) // Mono { - component.UnspawnedCount--; - DirtyField(uid, component, nameof(BallisticAmmoProviderComponent.UnspawnedCount)); + if (!component.InfiniteUnspawned) // Mono + { + component.UnspawnedCount--; + DirtyField(uid, component, nameof(BallisticAmmoProviderComponent.UnspawnedCount)); + } entity = Spawn(component.Proto, args.Coordinates); args.Ammo.Add((entity, EnsureShootable(entity))); } diff --git a/Content.Shared/Weapons/Ranged/Systems/SharedGunSystem.Magazine.cs b/Content.Shared/Weapons/Ranged/Systems/SharedGunSystem.Magazine.cs index 0abcc327688..159e76d8702 100644 --- a/Content.Shared/Weapons/Ranged/Systems/SharedGunSystem.Magazine.cs +++ b/Content.Shared/Weapons/Ranged/Systems/SharedGunSystem.Magazine.cs @@ -1,6 +1,7 @@ using Content.Shared.Examine; using Content.Shared.Interaction.Events; using Content.Shared.Verbs; +using Content.Shared.Weapons.Ranged.Components; // Mono using Content.Shared.Weapons.Ranged.Events; using Robust.Shared.Containers; @@ -34,7 +35,13 @@ private void OnMagazineExamine(EntityUid uid, MagazineAmmoProviderComponent comp return; var (count, _) = GetMagazineCountCapacity(uid, component); - args.PushMarkup(Loc.GetString("gun-magazine-examine", ("color", AmmoExamineColor), ("count", count))); + + // Mono + var mag = GetMagazineEntity(uid); + if (TryComp(mag, out var ballistic) && ballistic.InfiniteUnspawned) + args.PushMarkup(Loc.GetString("gun-magazine-infinite-examine", ("color", AmmoExamineSpecialColor), ("count", count))); + else + args.PushMarkup(Loc.GetString("gun-magazine-examine", ("color", AmmoExamineColor), ("count", count))); } private void OnMagazineUse(EntityUid uid, MagazineAmmoProviderComponent component, UseInHandEvent args) diff --git a/Content.Shared/Weapons/Ranged/Systems/SharedGunSystem.cs b/Content.Shared/Weapons/Ranged/Systems/SharedGunSystem.cs index 77009c9827b..ee3a9a643f2 100644 --- a/Content.Shared/Weapons/Ranged/Systems/SharedGunSystem.cs +++ b/Content.Shared/Weapons/Ranged/Systems/SharedGunSystem.cs @@ -77,6 +77,7 @@ public abstract partial class SharedGunSystem : EntitySystem private const double SafetyNextFire = 0.5; private const float EjectOffset = 0.4f; protected const string AmmoExamineColor = "yellow"; + protected const string AmmoExamineSpecialColor = "orange"; // Mono public const string FireRateExamineColor = "yellow"; // Frontier: protected +/// Loading system that can transfer ammunition to linked ship artillery. +/// +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState] +public sealed partial class AmmoLoaderComponent : Component +{ + public const string ContainerId = "ammo_loader"; + + /// + /// Container holding the ammo to be loaded. + /// + [ViewVariables] + public Container Container = default!; + + /// + /// Whether the loader is currently engaged for flushing. + /// + [DataField, AutoNetworkedField] + public bool Engaged; + + /// + /// Source port for sending load signals to linked artillery. + /// + [DataField(customTypeSerializer: typeof(PrototypeIdSerializer))] + public string LoadPort = "AmmoLoaderLoad"; + + /// + /// Maximum number of network connections allowed on this ammo loader. Connections beyond this limit will be rejected. + /// + [DataField, AutoNetworkedField] + public int MaxConnections = 1; + + /// + /// Maximum number of items that can be stored in the loader's container. + /// + [DataField, AutoNetworkedField] + public int MaxCapacity = 1; +} + diff --git a/Content.Shared/_Mono/ArmorPiercing/ArmorPiercingComponent.cs b/Content.Shared/_Mono/ArmorPiercing/ArmorPiercingComponent.cs new file mode 100644 index 00000000000..cfac915085e --- /dev/null +++ b/Content.Shared/_Mono/ArmorPiercing/ArmorPiercingComponent.cs @@ -0,0 +1,27 @@ +// SPDX-FileCopyrightText: 2025 ark1368 +// +// SPDX-License-Identifier: MPL-2.0 + +using Robust.Shared.GameStates; + +namespace Content.Shared._Mono.ArmorPiercing; + +/// +/// Component that allows projectiles to pierce through walls based on thickness. Piercing thickness is reduced by 50% after each successful pierce. +/// +[RegisterComponent, NetworkedComponent] +[AutoGenerateComponentState] +public sealed partial class ArmorPiercingComponent : Component +{ + /// + /// The piercing thickness of this projectile. + /// + [DataField, AutoNetworkedField] + public int PiercingThickness = 5; + + /// + /// Entities that this projectile has already pierced through. + /// + [DataField, AutoNetworkedField] + public HashSet PiercedEntities = new(); +} diff --git a/Content.Shared/_Mono/ArmorPiercing/ArmorPiercingSystem.cs b/Content.Shared/_Mono/ArmorPiercing/ArmorPiercingSystem.cs new file mode 100644 index 00000000000..a9ec8cff733 --- /dev/null +++ b/Content.Shared/_Mono/ArmorPiercing/ArmorPiercingSystem.cs @@ -0,0 +1,56 @@ +// SPDX-FileCopyrightText: 2025 ark1368 +// +// SPDX-License-Identifier: MPL-2.0 + +using Content.Shared.Physics; +using Robust.Shared.Physics.Events; + +namespace Content.Shared._Mono.ArmorPiercing; + +/// +/// Handles collision logic for projectiles with ArmorPiercingComponent. +/// +public sealed class ArmorPiercingSystem : EntitySystem +{ + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnPreventCollide); + } + + private void OnPreventCollide(Entity ent, ref PreventCollideEvent args) + { + var comp = ent.Comp; + + var isWall = (args.OtherFixture.CollisionLayer & (int)CollisionGroup.Impassable) != 0; + + if (!isWall) + return; + + if (!TryComp(args.OtherEntity, out ArmorThicknessComponent? armorThickness)) + return; + + if (!armorThickness.CanBePierced) + return; + + var alreadyPierced = comp.PiercedEntities.Contains(args.OtherEntity); + + if (alreadyPierced) + { + args.Cancelled = true; + return; + } + + if (comp.PiercingThickness < armorThickness.Thickness) + return; + + args.Cancelled = true; + + comp.PiercedEntities.Add(args.OtherEntity); + + comp.PiercingThickness = Math.Max(1, comp.PiercingThickness / 2); + + Dirty(ent, comp); + } +} diff --git a/Content.Shared/_Mono/ArmorPiercing/ArmorThicknessComponent.cs b/Content.Shared/_Mono/ArmorPiercing/ArmorThicknessComponent.cs new file mode 100644 index 00000000000..b4d6763bab2 --- /dev/null +++ b/Content.Shared/_Mono/ArmorPiercing/ArmorThicknessComponent.cs @@ -0,0 +1,27 @@ +// SPDX-FileCopyrightText: 2025 ark1368 +// +// SPDX-License-Identifier: MPL-2.0 + +using Robust.Shared.GameStates; + +namespace Content.Shared._Mono.ArmorPiercing; + +/// +/// Component that defines the armor thickness of entities. +/// +[RegisterComponent, NetworkedComponent] +[AutoGenerateComponentState] +public sealed partial class ArmorThicknessComponent : Component +{ + /// + /// The thickness value of this entity. + /// + [DataField, AutoNetworkedField] + public int Thickness = 10; + + /// + /// Whether this armor can be pierced at all. + /// + [DataField, AutoNetworkedField] + public bool CanBePierced = true; +} diff --git a/Content.Shared/_Mono/FireControl/FireControlMessages.cs b/Content.Shared/_Mono/FireControl/FireControlMessages.cs index 131ce787501..b10ff028dd3 100644 --- a/Content.Shared/_Mono/FireControl/FireControlMessages.cs +++ b/Content.Shared/_Mono/FireControl/FireControlMessages.cs @@ -89,10 +89,22 @@ public struct FireControllableEntry /// public string Name; - public FireControllableEntry(NetEntity entity, NetCoordinates coordinates, string name) + /// + /// Current ammunition count. + /// + public int? AmmoCount; + + /// + /// Whether this weapon has manual reload. + /// + public bool HasManualReload; + + public FireControllableEntry(NetEntity entity, NetCoordinates coordinates, string name, int? ammoCount = null, bool hasManualReload = false) { NetEntity = entity; Coordinates = coordinates; Name = name; + AmmoCount = ammoCount; + HasManualReload = hasManualReload; } } diff --git a/Resources/Fonts/Minecraftia.ttf b/Resources/Fonts/Minecraftia.ttf new file mode 100644 index 00000000000..2cf2af47085 Binary files /dev/null and b/Resources/Fonts/Minecraftia.ttf differ diff --git a/Resources/Locale/en-US/_Mono/ammo-loader.ftl b/Resources/Locale/en-US/_Mono/ammo-loader.ftl new file mode 100644 index 00000000000..13e49c6118b --- /dev/null +++ b/Resources/Locale/en-US/_Mono/ammo-loader.ftl @@ -0,0 +1,23 @@ +# Ammo Loader +ammo-loader-flush-verb = Flush +ammo-loader-eject-verb = Eject contents +ammo-loader-insert-success = Ammunition inserted. +ammo-loader-not-anchored = The loader must be anchored to flush! +ammo-loader-empty = The loader is empty! +ammo-loader-flushed = Ammo flushed! +ammo-loader-no-artillery = No ship artillery linked to this loader. +ammo-loader-transfer-failed = Failed to transfer ammunition to any linked ship artillery. +ammo-loader-insert-fail = Loader is full. + +# Artillery Flush Verbs +ammo-loader-flush-to-artillery-with-ammo-and-id = Flush to { $artillery } ({ $ammo }/{ $capacity }) [{ $id }] +ammo-loader-flushed-to-artillery = Ammunition flushed to { $artillery }! +ammo-loader-transfer-failed-to-artillery = Failed to transfer ammunition to { $artillery }! + +# Device Link Ports +signal-port-name-ammo-loader-load = Load Ammo +signal-port-description-ammo-loader-load = Transfers loaded ammunition to linked ship artillery. + +signal-port-name-space-artillery-load = Receive Ammo +signal-port-description-space-artillery-load = Receives ammunition from a linked loader. + diff --git a/Resources/Locale/en-US/_Mono/guidebook/guides.ftl b/Resources/Locale/en-US/_Mono/guidebook/guides.ftl index a95981fdeeb..d5135afcdf9 100644 --- a/Resources/Locale/en-US/_Mono/guidebook/guides.ftl +++ b/Resources/Locale/en-US/_Mono/guidebook/guides.ftl @@ -5,15 +5,10 @@ guide-entry-shipyard-judiciary = Judiciary guide-entry-gridclaimer = Claiming Debris # Gunnery Guides -guide-entry-gunnery = Ship Warfare -guide-entry-l85 = L85 Autocannon -guide-entry-ak570 = AK570 Heavy Autocannon -guide-entry-marauder = MARAUDER-type Plasma Launcher -guide-entry-charon = M381 CHARON Mass Driver -guide-entry-asm302 = ASM-302 "Vanyk" Missile Launcher -guide-entry-rubicon = M220 RUBICON EMP Launcher -guide-entry-cyrexa = CYREXA 220mm Main Battery -guide-entry-dymere = ADEX-9 Dymere Turbolaser Battery +guide-entry-gunnery = Ship Gunnery +guide-entry-ballistics = Kinetic +guide-entry-energy = Energy +guide-entry-missiles = Missiles # Generic guides guide-entry-fentanyl = Fentanyl Production diff --git a/Resources/Locale/en-US/_Mono/gunnery-console.ftl b/Resources/Locale/en-US/_Mono/gunnery-console.ftl index 0d8f256cdaa..630ed554d55 100644 --- a/Resources/Locale/en-US/_Mono/gunnery-console.ftl +++ b/Resources/Locale/en-US/_Mono/gunnery-console.ftl @@ -5,6 +5,8 @@ gunnery-window-connected = CONNECTED gunnery-select-all = Select All gunnery-unselect-all = Unselect All gunnery-guns = Guns +gunnery-gun-select = {$name} +gunnery-gun-select-ammo = {$name}: {$ammo} # Gunnery Server Examine gunnery-server-examine-detail = The server is using [color={$valueColor}]{$usedProcessingPower}/{$processingPower}[/color] of its processing power. diff --git a/Resources/Locale/en-US/_Mono/weapons/gun.ftl b/Resources/Locale/en-US/_Mono/weapons/gun.ftl new file mode 100644 index 00000000000..404c096974b --- /dev/null +++ b/Resources/Locale/en-US/_Mono/weapons/gun.ftl @@ -0,0 +1 @@ +gun-magazine-infinite-examine = It has [color={$color}]{$count} special[/color] shots remaining. diff --git a/Resources/Prototypes/Entities/Mobs/base.yml b/Resources/Prototypes/Entities/Mobs/base.yml index 7939bb287a8..9f0524b3db3 100644 --- a/Resources/Prototypes/Entities/Mobs/base.yml +++ b/Resources/Prototypes/Entities/Mobs/base.yml @@ -124,7 +124,7 @@ path: /Audio/Effects/hit_kick.ogg - type: Pullable - type: LightningTarget - priority: 2 + priority: 1 # Mono lightningExplode: false # Used for mobs that can enter combat mode and can attack. diff --git a/Resources/Prototypes/Entities/Structures/Machines/Computers/computers.yml b/Resources/Prototypes/Entities/Structures/Machines/Computers/computers.yml index 0df84059e11..af46ef81b7e 100644 --- a/Resources/Prototypes/Entities/Structures/Machines/Computers/computers.yml +++ b/Resources/Prototypes/Entities/Structures/Machines/Computers/computers.yml @@ -179,6 +179,12 @@ - ShuttleCraft - type: NoSignalOnLink - type: NoDeconstruct + # Mono + - type: RadarBlip + visibleFromOtherGrids: true + radarColor: "#03f8fc" + scale: 1.5 + shape: diamond - type: entity parent: [BaseComputerShuttle, BaseShuttleIntercom] # Frontier - BaseShuttleIntercom diff --git a/Resources/Prototypes/Entities/Structures/Power/Generation/ame.yml b/Resources/Prototypes/Entities/Structures/Power/Generation/ame.yml index bfadaa2faf7..df7760d6eaf 100644 --- a/Resources/Prototypes/Entities/Structures/Power/Generation/ame.yml +++ b/Resources/Prototypes/Entities/Structures/Power/Generation/ame.yml @@ -115,6 +115,12 @@ requirePower: true highVoltageNode: input mediumVoltageNode: ame + # Mono + - type: RadarBlip + visibleFromOtherGrids: true + radarColor: "#fcba03" + scale: 2 + shape: diamond - type: entity categories: [ HideSpawnMenu ] diff --git a/Resources/Prototypes/Entities/Structures/Power/Generation/portable_generator.yml b/Resources/Prototypes/Entities/Structures/Power/Generation/portable_generator.yml index 43c946f3245..662493f3dd1 100644 --- a/Resources/Prototypes/Entities/Structures/Power/Generation/portable_generator.yml +++ b/Resources/Prototypes/Entities/Structures/Power/Generation/portable_generator.yml @@ -34,6 +34,11 @@ - MachineMask layer: - MachineLayer + - type: RadarBlip + visibleFromOtherGrids: true + radarColor: "#fcba03" + scale: 1 + shape: square # Visuals - type: Appearance @@ -208,6 +213,7 @@ rate: 1 # End Frontier: magnet pickup, fuel adapter + - type: entity name: S.U.P.E.R.P.A.C.M.A.N. portable generator # Frontier: no -type description: |- diff --git a/Resources/Prototypes/Entities/Structures/Shuttles/cannons.yml b/Resources/Prototypes/Entities/Structures/Shuttles/cannons.yml index ca8f079ceba..6837028324b 100644 --- a/Resources/Prototypes/Entities/Structures/Shuttles/cannons.yml +++ b/Resources/Prototypes/Entities/Structures/Shuttles/cannons.yml @@ -462,3 +462,16 @@ - type: DeviceLinkSink ports: - Trigger + # Mono Start + - type: FireControllable + - type: ShipGunClass + shipClass: Superlight + - type: RadarBlip + visibleFromOtherGrids: true + radarColor: "#FF0040" + scale: 1.25 + - type: ShipGunType + shipType: Ballistic + - type: StaticPrice + price: 400 + # Mono End diff --git a/Resources/Prototypes/Entities/Structures/Shuttles/thrusters.yml b/Resources/Prototypes/Entities/Structures/Shuttles/thrusters.yml index 42944f1f36e..dced17aff72 100644 --- a/Resources/Prototypes/Entities/Structures/Shuttles/thrusters.yml +++ b/Resources/Prototypes/Entities/Structures/Shuttles/thrusters.yml @@ -119,6 +119,7 @@ id: Thruster # End Frontier - type: RadarBlip # Mono + visibleFromOtherGrids: true radarColor: "#99FF99" scale: 1 shape: diamond diff --git a/Resources/Prototypes/_DV/Entities/Objects/Weapons/Guns/Projectiles/muzzleflashes.yml b/Resources/Prototypes/_DV/Entities/Objects/Weapons/Guns/Projectiles/muzzleflashes.yml index 51142c053d2..3b5ada58a78 100644 --- a/Resources/Prototypes/_DV/Entities/Objects/Weapons/Guns/Projectiles/muzzleflashes.yml +++ b/Resources/Prototypes/_DV/Entities/Objects/Weapons/Guns/Projectiles/muzzleflashes.yml @@ -12,6 +12,59 @@ sprite: _Impstation/Objects/Weapons/Guns/Projectiles/muzzleflashes.rsi state: kineticsmall +- type: entity + parent: MuzzleFlashEffect + id: MuzzleFlashEffectSyndLaser + categories: [ HideSpawnMenu ] + components: + - type: PointLight + enabled: true + color: "#83db2b" + radius: 1.4 + energy: 5.0 + - type: Sprite + drawdepth: BelowMobs + offset: 0.15, 0 + layers: + - shader: unshaded + map: ["enum.EffectLayers.Unshaded"] + sprite: _Impstation/Objects/Weapons/Guns/Projectiles/muzzleflashes.rsi + state: energysynd + +- type: entity + parent: MuzzleFlashEffect + id: MuzzleFlashEffectGoldLaser + categories: [ HideSpawnMenu ] + components: + - type: Sprite + drawdepth: BelowMobs + offset: 0.15, 0 + layers: + - shader: unshaded + map: ["enum.EffectLayers.Unshaded"] + sprite: _Impstation/Objects/Weapons/Guns/Projectiles/muzzleflashes.rsi + state: energygold + +- type: entity + parent: MuzzleFlashEffect + id: MuzzleFlashEffectPurpLaser + categories: [ HideSpawnMenu ] + components: + - type: PointLight + enabled: true + color: "#c800ff" + radius: 1.4 + energy: 5.0 + - type: Sprite + drawdepth: BelowMobs + offset: 0.15, 0 + layers: + - shader: unshaded + map: ["enum.EffectLayers.Unshaded"] + sprite: _Impstation/Objects/Weapons/Guns/Projectiles/muzzleflashes.rsi + state: energypurp + +# Mono Start - type: entity parent: MuzzleFlashEffect id: MuzzleFlashEffectPlasmaPulser @@ -30,6 +83,7 @@ map: ["enum.EffectLayers.Unshaded"] sprite: _Impstation/Objects/Weapons/Guns/Projectiles/muzzleflashes.rsi state: energyplasma +# Mono End - type: entity parent: MuzzleFlashEffect @@ -49,3 +103,22 @@ map: ["enum.EffectLayers.Unshaded"] sprite: _Impstation/Objects/Weapons/Guns/Projectiles/muzzleflashes.rsi state: tempcold + +- type: entity + parent: MuzzleFlashEffect + id: MuzzleFlashEffectTempHot + categories: [ HideSpawnMenu ] + components: + - type: PointLight + enabled: true + color: "#820a16" + radius: 1.4 + energy: 3.0 + - type: Sprite + drawdepth: BelowMobs + offset: 0.15, 0 + layers: + - shader: unshaded + map: ["enum.EffectLayers.Unshaded"] + sprite: _Impstation/Objects/Weapons/Guns/Projectiles/muzzleflashes.rsi + state: temphot diff --git a/Resources/Prototypes/_Lua/Catalog/Cargo/cargo_shipweapon.yml b/Resources/Prototypes/_Lua/Catalog/Cargo/cargo_shipweapon.yml index 72f4d150d4a..7f497cfe32a 100644 --- a/Resources/Prototypes/_Lua/Catalog/Cargo/cargo_shipweapon.yml +++ b/Resources/Prototypes/_Lua/Catalog/Cargo/cargo_shipweapon.yml @@ -327,15 +327,16 @@ category: Корабельное вооружение group: market -- type: cargoProduct - id: CargoCrateWeaponTurretCharonetteFlatPack - icon: - sprite: _Lua/Structures/Storage/Crates/sweapon.rsi - state: icon - product: CrateWeaponTurretCharonetteFlatPack - cost: 450000 - category: Корабельное вооружение - group: market +# Mono deleted +# - type: cargoProduct +# id: CargoCrateWeaponTurretCharonetteFlatPack +# icon: +# sprite: _Lua/Structures/Storage/Crates/sweapon.rsi +# state: icon +# product: CrateWeaponTurretCharonetteFlatPack +# cost: 450000 +# category: Корабельное вооружение +# group: market #MARK: Cargo Wall Market @@ -390,4 +391,4 @@ product: CrateComputerAdvancedRadarFlatPack cost: 1250000 category: Корабельное вооружение - group: market \ No newline at end of file + group: market diff --git a/Resources/Prototypes/_Lua/Entities/Objects/Misc/flatpacks.yml b/Resources/Prototypes/_Lua/Entities/Objects/Misc/flatpacks.yml index a7906fc0589..6ad99b183b6 100644 --- a/Resources/Prototypes/_Lua/Entities/Objects/Misc/flatpacks.yml +++ b/Resources/Prototypes/_Lua/Entities/Objects/Misc/flatpacks.yml @@ -247,20 +247,21 @@ layers: - state: ship_weapon -- type: entity - parent: BaseNFFlatpack - id: WeaponTurretCharonetteFlatPack - name: M194 CHARONETTE mass driver - description: A smaller, more compact railgun that fires a large slug at rapid velocity, capable of big damage to enemy ships. - components: - - type: Flatpack - entity: WeaponTurretCharonette - - type: StaticPrice - price: 5000 - - type: Sprite - sprite: _Lua/Flatpack/flatpack.rsi - layers: - - state: ship_weapon +# Mono deleted +# - type: entity +# parent: BaseNFFlatpack +# id: WeaponTurretCharonetteFlatPack +# name: M194 CHARONETTE mass driver +# description: A smaller, more compact railgun that fires a large slug at rapid velocity, capable of big damage to enemy ships. +# components: +# - type: Flatpack +# entity: WeaponTurretCharonette +# - type: StaticPrice +# price: 5000 +# - type: Sprite +# sprite: _Lua/Flatpack/flatpack.rsi +# layers: +# - state: ship_weapon - type: entity parent: BaseNFFlatpack @@ -277,20 +278,21 @@ layers: - state: ship_weapon -- type: entity - parent: BaseNFFlatpack - id: WeaponTurretKargilFlatPack - name: Kargil 203mm cannon - description: A single barrelled, medium-heavy artillery cannon. Can be remotely activated, or linked up to a GCS. - components: - - type: Flatpack - entity: WeaponTurretKargil - - type: StaticPrice - price: 5000 - - type: Sprite - sprite: _Lua/Flatpack/flatpack.rsi - layers: - - state: ship_weapon +# Mono deleted +# - type: entity +# parent: BaseNFFlatpack +# id: WeaponTurretKargilFlatPack +# name: Kargil 203mm cannon +# description: A single barrelled, medium-heavy artillery cannon. Can be remotely activated, or linked up to a GCS. +# components: +# - type: Flatpack +# entity: WeaponTurretKargil +# - type: StaticPrice +# price: 5000 +# - type: Sprite +# sprite: _Lua/Flatpack/flatpack.rsi +# layers: +# - state: ship_weapon #MARK:Mono - missle turrets diff --git a/Resources/Prototypes/_Lua/Entities/Objects/Weapons/Guns/SpaceArtillery/Ammunition/Crates/shipw_crates.yml b/Resources/Prototypes/_Lua/Entities/Objects/Weapons/Guns/SpaceArtillery/Ammunition/Crates/shipw_crates.yml index 75edaf582a3..98b773b82ad 100644 --- a/Resources/Prototypes/_Lua/Entities/Objects/Weapons/Guns/SpaceArtillery/Ammunition/Crates/shipw_crates.yml +++ b/Resources/Prototypes/_Lua/Entities/Objects/Weapons/Guns/SpaceArtillery/Ammunition/Crates/shipw_crates.yml @@ -255,17 +255,18 @@ - id: WeaponTurretBoforsFlatPack amount: 1 -- type: entity - id: CrateWeaponTurretKargilFlatPack - parent: CrateShipWeaponBase - name: ящик с упакованным Kargil 203mm cannon - description: содержит 1 упакованный Kargil 203mm cannon - categories: [ HideSpawnMenu ] - components: - - type: StorageFill - contents: - - id: WeaponTurretKargilFlatPack - amount: 1 +# Mono deleted +# - type: entity +# id: CrateWeaponTurretKargilFlatPack +# parent: CrateShipWeaponBase +# name: ящик с упакованным Kargil 203mm cannon +# description: содержит 1 упакованный Kargil 203mm cannon +# categories: [ HideSpawnMenu ] +# components: +# - type: StorageFill +# contents: +# - id: WeaponTurretKargilFlatPack +# amount: 1 # Monolith - Energy #added to cargo @@ -556,14 +557,15 @@ #MARK: MONO turrets -- type: entity - id: CrateWeaponTurretCharonetteFlatPack - parent: CrateShipWeaponBase - name: M194 CHARONETTE mass driver - description: A smaller, more compact railgun that fires a large slug at rapid velocity, capable of big damage to enemy ships. - categories: [ HideSpawnMenu ] - components: - - type: StorageFill - contents: - - id: WeaponTurretCharonetteFlatPack - amount: 1 +# Mono deleted +# - type: entity +# id: CrateWeaponTurretCharonetteFlatPack +# parent: CrateShipWeaponBase +# name: M194 CHARONETTE mass driver +# description: A smaller, more compact railgun that fires a large slug at rapid velocity, capable of big damage to enemy ships. +# categories: [ HideSpawnMenu ] +# components: +# - type: StorageFill +# contents: +# - id: WeaponTurretCharonetteFlatPack +# amount: 1 diff --git a/Resources/Prototypes/_Lua/tags.yml b/Resources/Prototypes/_Lua/tags.yml index 9535bf1fe03..de78c74ff37 100644 --- a/Resources/Prototypes/_Lua/tags.yml +++ b/Resources/Prototypes/_Lua/tags.yml @@ -53,9 +53,6 @@ - type: Tag id: Cartridge140mm -- type: Tag - id: Magazine20mm - - type: Tag id: Magazine53mm diff --git a/Resources/Prototypes/_Mono/Catalogs/security_uplink_catalog.yml b/Resources/Prototypes/_Mono/Catalogs/security_uplink_catalog.yml index 6f28eabb0f7..7c4d9166590 100644 --- a/Resources/Prototypes/_Mono/Catalogs/security_uplink_catalog.yml +++ b/Resources/Prototypes/_Mono/Catalogs/security_uplink_catalog.yml @@ -1,13 +1,3 @@ -# SPDX-FileCopyrightText: 2025 EctoplasmIsGood -# SPDX-FileCopyrightText: 2025 HungryCuban -# SPDX-FileCopyrightText: 2025 Redrover1760 -# SPDX-FileCopyrightText: 2025 ScyronX -# SPDX-FileCopyrightText: 2025 Your Name -# SPDX-FileCopyrightText: 2025 core-mene -# SPDX-FileCopyrightText: 2025 starch -# -# SPDX-License-Identifier: AGPL-3.0-or-later - # Lua fully ignored # All content moved by path: \Prototypes\_Lua\Catalog\uplink_catalog_security.yml diff --git a/Resources/Prototypes/_Mono/DeviceLinking/sink_ports.yml b/Resources/Prototypes/_Mono/DeviceLinking/sink_ports.yml new file mode 100644 index 00000000000..55b7f74c48b --- /dev/null +++ b/Resources/Prototypes/_Mono/DeviceLinking/sink_ports.yml @@ -0,0 +1,14 @@ +- type: sinkPort + id: Resume + name: signal-port-name-lathe-resume + description: signal-description-port-lathe-resume + +- type: sinkPort + id: Pause + name: signal-port-name-lathe-pause + description: signal-port-description-lathe-pause + +- type: sinkPort + id: SpaceArtilleryLoad + name: signal-port-name-space-artillery-load + description: signal-port-description-space-artillery-load \ No newline at end of file diff --git a/Resources/Prototypes/_Mono/DeviceLinking/source_ports.yml b/Resources/Prototypes/_Mono/DeviceLinking/source_ports.yml new file mode 100644 index 00000000000..24f3ac3cce3 --- /dev/null +++ b/Resources/Prototypes/_Mono/DeviceLinking/source_ports.yml @@ -0,0 +1,9 @@ +- type: sourcePort + id: Produced + name: signal-port-name-lathe-produced + description: signal-port-description-lathe-produced + +- type: sourcePort + id: AmmoLoaderLoad + name: signal-port-name-ammo-loader-load + description: signal-port-description-ammo-loader-load \ No newline at end of file diff --git a/Resources/Prototypes/_Mono/Entities/AmmoLoader/ammo_loader.yml b/Resources/Prototypes/_Mono/Entities/AmmoLoader/ammo_loader.yml new file mode 100644 index 00000000000..bb2e52be3c3 --- /dev/null +++ b/Resources/Prototypes/_Mono/Entities/AmmoLoader/ammo_loader.yml @@ -0,0 +1,100 @@ +- type: entity + id: AmmoLoader + parent: BaseMachinePowered + name: ammo loader + description: A pneumatic ammunition loading system manufactured by Erebus HI. Link it to ship artillery with a multitool to transfer ammunition. This model supports 8 guns. + placement: + mode: SnapgridCenter + components: + - type: Sprite + sprite: _Mono/Objects/AmmoLoader/powdergate_sealed.rsi + snapCardinals: true + layers: + - state: icon + map: [ "base" ] + - type: Physics + bodyType: Static + - type: Fixtures + fixtures: + fix1: + shape: + !type:PhysShapeAabb + bounds: "-0.45,-0.45,0.45,0.45" + density: 55 + mask: + - TableMask + layer: + - TableLayer + - type: Destructible + thresholds: + - trigger: + !type:DamageTrigger + damage: 5000 + behaviors: + - !type:DoActsBehavior + acts: [ "Destruction" ] + - trigger: + !type:DamageTrigger + damage: 2000 + behaviors: + - !type:DoActsBehavior + acts: ["Destruction"] + - !type:PlaySoundBehavior + sound: + collection: MetalBreak + - !type:SpawnEntitiesBehavior + spawn: + SheetSteel1: + min: 1 + max: 1 + - type: ContainerContainer + containers: + ammo_loader: !type:Container + - type: ThrowInsertContainer + containerId: ammo_loader + - type: AmmoLoader + maxConnections: 8 + - type: ApcPowerReceiver + powerLoad: 200 + - type: DeviceLinkSource + range: 100 + ports: + - AmmoLoaderLoad + +- type: entity + id: AmmoLoaderSmall + parent: AmmoLoader + name: small ammo loader + description: A pneumatic ammunition loading system manufactured by Erebus HI. Link it to ship artillery with a multitool to transfer ammunition. This model supports only 2 guns, but is more durable. + components: + - type: Sprite + sprite: _Mono/Objects/AmmoLoader/payloadgate_sealed.rsi + snapCardinals: true + layers: + - state: icon + map: [ "base" ] + - type: Destructible + thresholds: + - trigger: + !type:DamageTrigger + damage: 5000 + behaviors: + - !type:DoActsBehavior + acts: [ "Destruction" ] + - trigger: + !type:DamageTrigger + damage: 4000 + behaviors: + - !type:DoActsBehavior + acts: ["Destruction"] + - !type:PlaySoundBehavior + sound: + collection: MetalBreak + - !type:SpawnEntitiesBehavior + spawn: + SheetSteel1: + min: 1 + max: 1 + - type: AmmoLoader + maxConnections: 2 + maxCapacity: 8 diff --git a/Resources/Prototypes/_Mono/Entities/Effects/projectile.yml b/Resources/Prototypes/_Mono/Entities/Effects/projectile.yml new file mode 100644 index 00000000000..a3119fd58f3 --- /dev/null +++ b/Resources/Prototypes/_Mono/Entities/Effects/projectile.yml @@ -0,0 +1,29 @@ +- type: entity + id: InstantEffectEMP + name: instant EMP effect + components: + - type: TriggerOnSpawn + - type: DeleteOnTrigger + - type: EmpOnTrigger + range: 1 # low range + energyConsumption: 20000 # 20 kJ + disableDuration: 20 # as opposed to 60 + +- type: entity + name: ionised lightning + id: LightningEMP + parent: BaseLightning + categories: [ HideSpawnMenu ] + components: + - type: Lightning + canArc: true + spawnOnHit: InstantEffectEMP + - type: PointLight + color: "#80C0FF" + +#example usage: add this to projectile parenting from BaseBulletTrigger +# - type: LightningOnTrigger +# chance: 0.08 +# range: 20 # long +# lightningProto: LightningEMP +# lightningEffects: false # only EMP no explosion diff --git a/Resources/Prototypes/_Mono/Entities/SpaceArtillery/SpaceArtillery/Energy/launcher.yml b/Resources/Prototypes/_Mono/Entities/SpaceArtillery/SpaceArtillery/Energy/launcher.yml index 6bb3d5d35db..fde41c0fefa 100644 --- a/Resources/Prototypes/_Mono/Entities/SpaceArtillery/SpaceArtillery/Energy/launcher.yml +++ b/Resources/Prototypes/_Mono/Entities/SpaceArtillery/SpaceArtillery/Energy/launcher.yml @@ -1,4 +1,5 @@ # MARAUDER + - type: entity id: WeaponTurretType35 name: Type-35 MARAUDER-type plasma cannon @@ -21,7 +22,10 @@ range: 500 - type: Gun fireRate: 0.5 - projectileSpeed: 90 + recoil: 5 # 1/5x + minAngle: 0 + maxAngle: 0 + projectileSpeed: 100 shootThermalSignature: 4000000 # ~2.8km with continuous fire soundGunshot: path: /Audio/Weapons/Guns/Gunshots/laser_cannon2.ogg @@ -43,13 +47,13 @@ thresholds: - trigger: !type:DamageTrigger - damage: 15000 + damage: 6000 behaviors: - !type:DoActsBehavior acts: [ "Destruction" ] - trigger: !type:DamageTrigger - damage: 12500 + damage: 3000 behaviors: - !type:DoActsBehavior acts: [ "Destruction" ] @@ -57,6 +61,7 @@ sound: collection: MetalBreak - type: RadarBlip + visibleFromOtherGrids: true radarColor: "#C92BCC" scale: 2 - type: ShipGunType @@ -64,60 +69,62 @@ - type: ShipGunClass shipClass: Medium -# Type 54 autopulser +# Cerbrus - type: entity - id: WeaponTurretType54 - name: Type-54C plasma autopulser + id: WeaponTurretCerberus + name: TPC Cerberus Plasma scattercannon parent: BallisticArtilleryUnanchorable #Lua BallisticArtillery