Ship Weapon Fix (#23038)

* Fixed ship munitions occasionally failing to enter their overmap
target when their original physical projectile no longer existed.
* Fixed several ship munitions reconstructing as the wrong projectile
after overmap travel, ensuring explosive rounds correctly detonate on
impact.

Also had the AI write a unit test that simulates ship combat using every
munition type, so we don't have more regressions here.

AI usage disclosure: The code for this was created using GPT 5.6 Sol.
This commit is contained in:
Geeves
2026-08-10 19:47:19 +00:00
committed by GitHub
parent 8dc9711f6d
commit 448932f01c
13 changed files with 203 additions and 8 deletions
@@ -22,7 +22,8 @@
var/obj/effect/overmap/origin
var/atom/overmap_target
var/obj/entry_point
var/obj/projectile/original_projectile
/// The exact physical projectile type to reconstruct when this ammunition leaves the overmap.
var/fired_projectile_type
var/heading = SOUTH
var/range = OVERMAP_PROJECTILE_RANGE_MEDIUM
var/mob_carry_size = 12 //How large a mob has to be to carry the shell
@@ -61,7 +62,8 @@
origin = null
overmap_target = null
entry_point = null
original_projectile = null
// The typepath is runtime metadata for one fired round and must not outlive it.
fired_projectile_type = null
return ..()
/obj/item/ship_ammunition/attackby(obj/item/attacking_item, mob/user)
@@ -322,9 +324,11 @@
H.playsound_local(null, 'sound/effects/explosionfar.ogg', 25)
shake_camera(H, 2, 2)
..()
// Preserve only the concrete type needed at the destination. The ammunition
// has already moved into the overmap carrier, so this physical shell is spent.
ammo.fired_projectile_type = type
if(ammo.touch_map_edge(z))
ammo.original_projectile = src
forceMove(ammo)
qdel(src)
/obj/projectile/ship_ammo/on_hit(atom/target, blocked, def_zone, var/is_landmark_hit = FALSE) //is_landmark_hit is TRUE when we hit a landmark on a visitable non-ship overmap object.
if(target && !hit_target)
@@ -101,12 +101,16 @@
/obj/effect/overmap/projectile/proc/prepare_for_entry()
moving = FALSE
entering = FALSE
// Entry is a one-shot ownership handoff; stale Move() callbacks must not repeat it.
entering = TRUE
walk(src, 0)
///Checks if we can hit the thing we just bumped into. If we can, do the initial setup for the projectile entering the submab and continue.
/obj/effect/overmap/projectile/proc/check_entry()
. = FALSE
// A translated carrier can remain referenced until qdel completes.
if(entering)
return TRUE
if(!ammunition)
return
var/turf/T = get_turf(src)
@@ -116,12 +120,16 @@
if(istype(A, /obj/effect/overmap/visitable))
var/obj/effect/overmap/visitable/V = A
if((V.check_ownership(submap_target)) || (V == target)) //If the visitable is owned by the target landmark, or is the target itself, we can hit it.
if(!ammunition.fired_projectile_type)
// This carrier was not produced by a valid ship projectile transition.
qdel(src)
return TRUE
var/turf/target_turf = get_turf(submap_target)
var/obj/projectile/ship_ammo/widowmaker = new ammunition.original_projectile.type
var/obj/projectile/ship_ammo/widowmaker = new ammunition.fired_projectile_type
prepare_for_entry()
widowmaker.ammo = ammunition
qdel(ammunition.original_projectile) //No longer needed.
ammunition.original_projectile = widowmaker
// Transfer the payload before deleting the now-empty overmap carrier.
ammunition.forceMove(widowmaker)
widowmaker.primed = TRUE
if(istype(V, /obj/effect/overmap/visitable/sector/exoplanet) && (ammunition.overmap_behaviour & SHIP_AMMO_CAN_HIT_PLANETS))
@@ -20,6 +20,7 @@
caliber = SHIP_CALIBER_BLASTER
overmap_icon_state = "med_laser"
impact_type = SHIP_AMMO_IMPACT_BLASTER
projectile_type_override = /obj/projectile/ship_ammo/blaster
/obj/projectile/ship_ammo/blaster
name = "blaster charge"
@@ -29,6 +29,7 @@
burst = 11
cookoff_heavy = 0
overmap_icon_state = "cannon_salvo"
projectile_type_override = /obj/projectile/ship_ammo/bruiser
/obj/item/ship_ammunition/bruiser/get_speed()
return 35
@@ -20,6 +20,7 @@
caliber = SHIP_CALIBER_COILGUN
overmap_icon_state = "cannon"
impact_type = SHIP_AMMO_IMPACT_BUNKERBUSTER
projectile_type_override = /obj/projectile/ship_ammo/coilgun
/obj/projectile/ship_ammo/coilgun
name = "high-power tungsten rod"
@@ -28,6 +28,7 @@
caliber = SHIP_CALIBER_40MM
burst = 8
cookoff_heavy = 0
projectile_type_override = /obj/projectile/ship_ammo/francisca
/obj/item/ship_ammunition/francisca/ap
name = "40mm AP ammunition box"
@@ -19,6 +19,7 @@
caliber = SHIP_CALIBER_200MM
overmap_icon_state = "cannon"
impact_type = SHIP_AMMO_IMPACT_HE
projectile_type_override = /obj/projectile/ship_ammo/lammergeier
/obj/projectile/ship_ammo/lammergeier
name = "typhoon shell"
@@ -128,6 +128,7 @@
caliber = SHIP_CALIBER_ZAT
impact_type = SHIP_AMMO_IMPACT_ZAT
overmap_icon_state = "heavy_pulse"
projectile_type_override = /obj/projectile/ship_ammo/leviathan
/obj/item/ship_ammunition/leviathan/Initialize()
. = ..()
@@ -21,6 +21,7 @@
caliber = SHIP_CALIBER_COILGUN
overmap_icon_state = "light_pulse"
impact_type = SHIP_AMMO_IMPACT_AP
projectile_type_override = /obj/projectile/ship_ammo/coilgun/light
/obj/projectile/ship_ammo/coilgun/light
name = "low-power tungsten rod"
@@ -6,6 +6,7 @@
item_state = "generic_casing_obj"
caliber = SHIP_CALIBER_406MM
ammunition_behaviour = SHIP_AMMO_BEHAVIOUR_DUMBFIRE
projectile_type_override = /obj/projectile/ship_ammo/longbow
var/obj/item/primer/primer
var/obj/item/warhead/longbow/warhead
@@ -34,6 +34,7 @@
caliber = SHIP_CALIBER_BLASTER
burst = 15
cookoff_heavy = 0
projectile_type_override = /obj/projectile/ship_ammo/nephilim/he
/obj/item/ship_ammunition/nephilim/ap
name = "Blaster AP hydrogen cell"
@@ -42,6 +43,7 @@
icon = 'icons/obj/guns/ship/ship_ammo_nephilim.dmi'
icon_state = "repeater_ap"
impact_type = SHIP_AMMO_IMPACT_AP
projectile_type_override = /obj/projectile/ship_ammo/nephilim/ap
/obj/projectile/ship_ammo/nephilim
name = "hydrogen cell he"
@@ -58,6 +60,7 @@
damage = 20
armor_penetration = 100
penetrating = 4
explosion_strength = list(0, 0, 0)
/obj/projectile/ship_ammo/nephilim/he/on_hit(atom/target, blocked, def_zone, is_landmark_hit)
. = ..()