diff --git a/code/__DEFINES/dcs/signals/atom/mob/signals_mob.dm b/code/__DEFINES/dcs/signals/atom/mob/signals_mob.dm
index 04afcd3beb90..1b272624d18f 100644
--- a/code/__DEFINES/dcs/signals/atom/mob/signals_mob.dm
+++ b/code/__DEFINES/dcs/signals/atom/mob/signals_mob.dm
@@ -143,6 +143,7 @@
#define COMSIG_MOB_MOUSEDOWN "mob_mousedown" //from /client/MouseDown(): (atom/object, turf/location, control, params)
#define COMSIG_MOB_MOUSEUP "mob_mouseup" //from /client/MouseUp(): (atom/object, turf/location, control, params)
#define COMSIG_MOB_MOUSEDRAG "mob_mousedrag" //from /client/MouseDrag(): (atom/src_object, atom/over_object, turf/src_location, turf/over_location, src_control, over_control, params)
+#define COMSIG_MOB_MOUSEMOVE "mob_mousemove" //from /client/MouseMove(): (atom/object, turf/location, control, params)
#define COMSIG_MOB_CLICK_CANCELED (1<<0)
#define COMSIG_MOB_CLICK_HANDLED (1<<1)
diff --git a/code/__DEFINES/dcs/signals/atom/signals_projectile.dm b/code/__DEFINES/dcs/signals/atom/signals_projectile.dm
index 46014d5351d5..f9030051cb89 100644
--- a/code/__DEFINES/dcs/signals/atom/signals_projectile.dm
+++ b/code/__DEFINES/dcs/signals/atom/signals_projectile.dm
@@ -29,6 +29,8 @@
#define COMSIG_BULLET_PRE_HANDLE_TURF "bullet_pre_handle_turf"
#define COMPONENT_BULLET_PASS_THROUGH (1<<0)
#define COMSIG_BULLET_TERMINAL "bullet_terminal"
+/// From /obj/projectile/fly(), every tick of flight (unlike COMSIG_BULLET_TERMINAL, which only fires once, right before impact): ()
+#define COMSIG_BULLET_STEP "bullet_step"
/// Called when a bullet hits a living mob on a sprite click (original target is final target)
#define COMSIG_BULLET_DIRECT_HIT "bullet_direct_hit"
diff --git a/code/__DEFINES/keybinding.dm b/code/__DEFINES/keybinding.dm
index bcf1b06b6fe4..cffa10c309ee 100644
--- a/code/__DEFINES/keybinding.dm
+++ b/code/__DEFINES/keybinding.dm
@@ -232,6 +232,7 @@
#define COMSIG_KB_VEHICLE_CHANGE_SELECTED_WEAPON "keybinding_change_selected_weapon"
#define COMSIG_KB_VEHICLE_ACTIVATE_HORN "keybinding_activate_horn"
#define COMSIG_KB_VEHICLE_RELOAD_WEAPON "keybinding_reload_weapon"
+#define COMSIG_KB_VEHICLE_TOGGLE_HARDPOINT_FIRE_MODE "keybinding_vehicle_toggle_hardpoint_fire_mode"
#define CATEGORY_CLIENT "CLIENT"
#define CATEGORY_EMOTE "EMOTE"
diff --git a/code/__DEFINES/layers.dm b/code/__DEFINES/layers.dm
index 8f1afc5130b6..8613755e2d89 100644
--- a/code/__DEFINES/layers.dm
+++ b/code/__DEFINES/layers.dm
@@ -132,8 +132,21 @@
#define FACEHUGGER_LAYER 4.13
/// For Signs above everything but below weather
#define BILLBOARD_LAYER 4.13
+
+/// For objs atop a tank
+#define TANK_RIDER_OBJ_LAYER 4.505
+/// Mostly for bodybags atop a tank
+/// Not used yet. This is a reminder for me to add a specific tank_layer var to OBJs to keep track of
+/// ...which specific layer they need to go when brought atop.
+#define TANK_RIDER_ABOVE_OBJ_LAYER 4.506
+/// For mobs riding atop a tank
+#define TANK_BELOW_RIDER_LAYER 4.508
+#define TANK_LYING_RIDER_LAYER 4.509
+#define TANK_RIDER_LAYER 4.51
+#define TANK_ABOVE_RIDER_LAYER 4.515
+
/// For WEATHER
-#define WEATHER_LAYER 4.14
+#define WEATHER_LAYER 5
//#define FLY_LAYER 5
diff --git a/code/__DEFINES/vehicle.dm b/code/__DEFINES/vehicle.dm
index d40e89826eff..4ab95812473c 100644
--- a/code/__DEFINES/vehicle.dm
+++ b/code/__DEFINES/vehicle.dm
@@ -73,3 +73,29 @@
#define BUCKLE_PREVENTS_PULL (1<<3)
#define BUCKLE_NEEDS_HAND (1<<4)
#define BUCKLE_NEEDS_TWO_HANDS (1<<5)
+
+//Tank turret mouse-aim rotation
+
+/// How close current_angle needs to be to desired_angle before the turret rotation loop considers itself settled and stops ticking.
+#define ROTATION_SETTLE_TOLERANCE 0.5
+/// How close current_angle needs to be to the angle-to-target before a turret-mounted weapon is allowed to fire.
+#define FIRING_GATE_TOLERANCE 5
+/// Baseline turn speed (degrees per decisecond) for a turret whose median mounted traverse_arc equals TURRET_ARC_NORMALIZATION.
+#define TURRET_BASE_ANGULAR_VELOCITY 6
+/// The traverse_arc value that maps 1:1 onto TURRET_BASE_ANGULAR_VELOCITY.
+#define TURRET_ARC_NORMALIZATION 90
+/// Turn speed floor, so a turret can never be fully immobilized by a degenerate (e.g. 0) traverse_arc value.
+#define TURRET_MIN_ANGULAR_VELOCITY 2
+/// Turn speed used when the turret has no weapons mounted at all.
+#define TURRET_DEFAULT_ANGULAR_VELOCITY 4
+/// Flat angular acceleration (degrees per decisecond^2) used to ramp angular_velocity up/down.
+#define TURRET_ANGULAR_ACCEL 1
+/// Half-width, in degrees, of the arc a self_gimballed weapon's driver-controlled aim is clamped to around the turret's own current facing (see /obj/item/hardpoint/proc/update_desired_angle()) - keeps the visual swivel (see /obj/item/hardpoint/holder/tank_turret/get_hardpoint_image()) to a modest look-around instead of a full swing to any absolute angle.
+#define SLAVED_GIMBAL_ARC_HALF_WIDTH 30
+
+// Flamer hardpoint fire modes - see /obj/item/hardpoint/proc/toggle_fire_mode()
+/// Directional line stream (walks toward the target, self-propagating tile by tile).
+#define FLAME_MODE_STREAM "stream"
+/// Single traveling projectile that plants one AOE ignition point on impact.
+#define FLAME_MODE_GLOB "glob"
+/// should these go into a _DEFINES for hardpoints instead???
diff --git a/code/__HELPERS/unsorted.dm b/code/__HELPERS/unsorted.dm
index f562f6b913c4..46c73c019fba 100644
--- a/code/__HELPERS/unsorted.dm
+++ b/code/__HELPERS/unsorted.dm
@@ -63,11 +63,14 @@
// GLOBAL PROCS //
/// Gives X position on pixel grid of an object, accounting for offsets
-/proc/get_pixel_position_x(atom/subject, relative = FALSE)
+/proc/get_pixel_position_x(atom/subject, relative = FALSE, include_size_adjustment = TRUE)
. = subject.pixel_x + subject.base_pixel_x
if(!relative)
. += world.icon_size * subject.x
+ if(!include_size_adjustment)
+ return
+
if(ismob(subject)) // Mobs use baked in icon_size due to eg. Xenos only using a visual size
var/mob/mob_subject = subject
. += (mob_subject.icon_size - world.icon_size) / 2
@@ -77,11 +80,14 @@
. += (big_subject.bound_width - world.icon_size) / 2
/// Gives Y position on pixel grid of an object, accounting for offsets
-/proc/get_pixel_position_y(atom/subject, relative = FALSE)
+/proc/get_pixel_position_y(atom/subject, relative = FALSE, include_size_adjustment = TRUE)
. = subject.pixel_y + subject.base_pixel_y
if(!relative)
. += world.icon_size * subject.y
+ if(!include_size_adjustment)
+ return
+
if(ismob(subject)) // Mobs use baked in icon_size due to eg. Xenos only using a visual size
var/mob/mob_subject = subject
. += (mob_subject.icon_size - world.icon_size) / 2
@@ -106,12 +112,42 @@
var/dx = get_pixel_position_x(end) - get_pixel_position_x(start)
return delta_to_angle(dx, dy)
+/**
+ * Same as Get_Angle(), but skips the automatic "sprite is bigger than a tile, so nudge the position
+ * by half the extra size" compensation baked into get_pixel_position_x/y()
+ *
+ * only used for actual tturret aiming/hit-resolution math (update_desired_angle(), track_and_charge()), not for purely
+ * cosmetic uses (LTB reticle placement, muzzle flashes, beams).
+ *
+ * That automatic guess assumes a sprite bigger than world.icon_size is centered on its tile, which
+ * is wrong for a mob like the Queen (icon_size 64) whose sprite is anchored at the feet and simply
+ * extends upward...
+*/
+/proc/Get_Angle_Grounded(atom/start, atom/end)
+ if(!start || !end)
+ return 0
+ if(!start.z)
+ start = get_turf(start)
+ if(!start)
+ return 0
+ if(!end.z)
+ end = get_turf(end)
+ if(!end)
+ return 0
+ var/dy = get_pixel_position_y(end, include_size_adjustment = FALSE) - get_pixel_position_y(start, include_size_adjustment = FALSE)
+ var/dx = get_pixel_position_x(end, include_size_adjustment = FALSE) - get_pixel_position_x(start, include_size_adjustment = FALSE)
+ return delta_to_angle(dx, dy)
+
/// Calculate the angle produced by a pair of x and y deltas. Uses north-clockwise convention: NORTH = 0, EAST = 90, etc.
/proc/delta_to_angle(dx, dy)
. = arctan(dy, dx) //y-then-x results in north-clockwise convention: https://en.wikipedia.org/wiki/Atan2#East-counterclockwise,_north-clockwise_and_south-clockwise_conventions,_etc.
if(. < 0)
. += 360
+/// Shortest signed angle difference from b to a, in the range -180..180. Positive means a is clockwise of b.
+/proc/angle_delta(a, b)
+ return ((a - b + 540) % 360) - 180
+
/proc/angle_to_dir(angle)
angle = ((angle % 360) + 382.5) % 360
switch(angle) //diagonal directions get priority over straight directions in edge cases
diff --git a/code/_onclick/click_hold.dm b/code/_onclick/click_hold.dm
index 2963dddfd9a3..da8b6edd5422 100644
--- a/code/_onclick/click_hold.dm
+++ b/code/_onclick/click_hold.dm
@@ -103,6 +103,19 @@
// Add the hovered atom to the trace
LAZYADD(mouse_trace_history, over_obj)
+// Fires at near-pixel granularity while the cursor moves over the map, even with no button held.
+// if this ever gets used anywhere else, be careful: Listeners are expected to rate-limit themselves - BWSB
+/client/MouseMove(atom/object, turf/location, control, params)
+ if(!object)
+ return
+
+ var/click_catcher_click = FALSE
+ CONVERT_CLICK_CATCHER(object, location, click_catcher_click)
+ if(click_catcher_click)
+ params += CLICK_CATCHER_ADD_PARAM
+
+ SEND_SIGNAL(mob, COMSIG_MOB_MOUSEMOVE, object, location, control, params)
+
/client/MouseDrop(datum/src_object, datum/over_object, src_location, over_location, src_control, over_control, params)
. = ..()
if(HAS_TRAIT(usr, TRAIT_HAULED))
diff --git a/code/_onclick/human.dm b/code/_onclick/human.dm
index 6fdc1f80578e..1b7ae362098e 100644
--- a/code/_onclick/human.dm
+++ b/code/_onclick/human.dm
@@ -156,6 +156,23 @@
grab_level = GRAB_CARRY
+ var/obj/vehicle/multitile/tank/tank = get_tank_on_top_of()
+ if(tank && !target.get_tank_on_top_of())
+
+ if(istype(tank))
+ var/turf/target_turf = get_turf(target)
+ var/adjacent_to_tank = FALSE
+ for(var/turf/tank_turf in tank.locs)
+ if(get_dist(target_turf, tank_turf) <= 1 && target_turf != tank_turf)
+ adjacent_to_tank = TRUE
+ break
+
+ if(adjacent_to_tank)
+ target.forceMove(user.loc)
+ tank.mark_on_top(target)
+ target.update_transform(TRUE)
+ return
+
target.Move(user.loc, get_dir(target.loc, user.loc))
target.update_transform(TRUE)
diff --git a/code/datums/ammo/bullet/tank.dm b/code/datums/ammo/bullet/tank.dm
index e9da5d110d97..268ca21d44bb 100644
--- a/code/datums/ammo/bullet/tank.dm
+++ b/code/datums/ammo/bullet/tank.dm
@@ -12,11 +12,11 @@
sound_hit = 'sound/weapons/sting_boom_small1.ogg'
damage_falloff = 0
flags_ammo_behavior = AMMO_BALLISTIC
- accurate_range_min = 4
+ accurate_range_min = 3
accuracy = HIT_ACCURACY_TIER_8
scatter = 0
- damage = 60
+ damage = 140
damage_var_high = PROJECTILE_VARIANCE_TIER_8
penetration = ARMOR_PENETRATION_TIER_6
accurate_range = 32
@@ -24,21 +24,36 @@
shell_speed = AMMO_SPEED_TIER_6
/datum/ammo/bullet/tank/flak/on_hit_mob(mob/M,obj/projectile/P)
- burst(get_turf(M),P,damage_type, 2 , 3)
- burst(get_turf(M),P,damage_type, 1 , 3 , 0)
+ create_shrapnel(get_turf(M), 24, rand(0, 359), 180, /datum/ammo/bullet/shrapnel/flak, P.weapon_cause_data, FALSE, 0.15, TRUE)
+ apply_micro_stun(M)
/datum/ammo/bullet/tank/flak/on_near_target(turf/T, obj/projectile/P)
- burst(get_turf(T),P,damage_type, 2 , 3)
- burst(get_turf(T),P,damage_type, 1 , 3, 0)
+ create_shrapnel(get_turf(T), 24, rand(0, 359), 180, /datum/ammo/bullet/shrapnel/flak, P.weapon_cause_data, FALSE, 0.15, TRUE)
return 1
/datum/ammo/bullet/tank/flak/on_hit_obj(obj/O,obj/projectile/P)
- burst(get_turf(P),P,damage_type, 2 , 3)
- burst(get_turf(P),P,damage_type, 1 , 3 , 0)
+ create_shrapnel(get_turf(P), 24, rand(0, 359), 180, /datum/ammo/bullet/shrapnel/flak, P.weapon_cause_data, FALSE, 0.15, TRUE)
/datum/ammo/bullet/tank/flak/on_hit_turf(turf/T,obj/projectile/P)
- burst(get_turf(T),P,damage_type, 2 , 3)
- burst(get_turf(T),P,damage_type, 1 , 3 , 0)
+ create_shrapnel(get_turf(T), 24, rand(0, 359), 180, /datum/ammo/bullet/shrapnel/flak, P.weapon_cause_data, FALSE, 0.15, TRUE)
+
+// applies a micro-stun modelled after the AMR SPEC's second shot.
+// unlike the AMR spec, this only works on xenos that aren't big sized.
+/datum/ammo/bullet/tank/flak/proc/apply_micro_stun(mob/living/living_mob)
+ if(!isxeno(living_mob) || living_mob.mob_size >= MOB_SIZE_BIG)
+ return
+ var/mob/living/carbon/xenomorph/target = living_mob
+ target.KnockDown(0.15)
+
+/datum/ammo/bullet/tank/flak/set_bullet_traits()
+ . = ..()
+ LAZYADD(traits_to_give, list(
+ BULLET_TRAIT_ENTRY(/datum/element/bullet_trait_iff)
+ ))
+
+/// same shrapnel as any other fragmentation source, just capped to a 4 tile radius
+/datum/ammo/bullet/shrapnel/flak
+ max_range = 4
/datum/ammo/bullet/tank/dualcannon
name = "dualcannon bullet"
@@ -90,6 +105,9 @@
damage = 40
penetration = ARMOR_PENETRATION_TIER_6
damage_armor_punch = 1
+ // Close-range weapon by design: full damage out to 4 tiles, roughly halved by 7 tiles, tapers to nothing by 10 tiles.
+ effective_range_max = 4
+ damage_falloff = 6.7
/datum/ammo/bullet/tank/setup_faction_clash_values()
. = ..()
diff --git a/code/datums/ammo/misc.dm b/code/datums/ammo/misc.dm
index 8f7659dd9871..c69aa18bc4b6 100644
--- a/code/datums/ammo/misc.dm
+++ b/code/datums/ammo/misc.dm
@@ -48,16 +48,41 @@
/datum/ammo/flamethrower/do_at_max_range(obj/projectile/P)
drop_flame(get_turf(P), P.weapon_cause_data)
+// "Glob shot" mode ammo for the tank's primary/secondary flamer hardpoints
/datum/ammo/flamethrower/tank_flamer
flamer_reagent_id = "highdamagenapalm"
max_range = 8
shell_speed = 1.5
+ var/loaded_chem_id
/datum/ammo/flamethrower/tank_flamer/drop_flame(turf/turf, datum/cause_data/cause_data)
if(!istype(turf))
return
- var/datum/reagent/napalm/high_damage/reagent = new()
+ var/datum/reagent/reagent = loaded_chem_id ? GLOB.chemical_reagents_list[loaded_chem_id] : null
+ if(!reagent)
+ reagent = new /datum/reagent/napalm/high_damage()
+ new /obj/flamer_fire(turf, cause_data, reagent, 1)
+
+ var/datum/effect_system/smoke_spread/landingsmoke = new /datum/effect_system/smoke_spread
+ landingsmoke.set_up(1, 0, turf, null, 4, cause_data)
+ landingsmoke.start()
+ landingsmoke = null
+
+/// Same as tank_flamer above, but for the secondary flamer hardpoint's smaller glob.
+/datum/ammo/flamethrower/tank_flamer_secondary
+ flamer_reagent_id = "highdamagenapalm"
+ max_range = 8
+ shell_speed = 1.5
+ var/loaded_chem_id
+
+/datum/ammo/flamethrower/tank_flamer_secondary/drop_flame(turf/turf, datum/cause_data/cause_data)
+ if(!istype(turf))
+ return
+
+ var/datum/reagent/reagent = loaded_chem_id ? GLOB.chemical_reagents_list[loaded_chem_id] : null
+ if(!reagent)
+ reagent = new /datum/reagent/napalm/high_damage()
new /obj/flamer_fire(turf, cause_data, reagent, 1)
var/datum/effect_system/smoke_spread/landingsmoke = new /datum/effect_system/smoke_spread
@@ -336,6 +361,29 @@
damage = 2.5
flare_type = /obj/item/device/flashlight/flare/on/starshell_ash
+// Fired from the tank turret's flare launcher
+/datum/ammo/flare/starshell/burst
+ name = "star shell burst"
+ max_range = 7
+
+/datum/ammo/flare/starshell/burst/on_hit_mob(mob/M, obj/projectile/P)
+ detonate(get_turf(M), P)
+
+/datum/ammo/flare/starshell/burst/on_hit_obj(obj/O, obj/projectile/P)
+ detonate(get_turf(O), P)
+
+/datum/ammo/flare/starshell/burst/on_hit_turf(turf/T, obj/projectile/P)
+ if(T.density && isturf(P.loc))
+ detonate(P.loc, P)
+ else
+ detonate(T, P)
+
+/datum/ammo/flare/starshell/burst/do_at_max_range(obj/projectile/P, mob/firer)
+ detonate(get_turf(P), P)
+
+/datum/ammo/flare/starshell/burst/proc/detonate(turf/hit_turf, obj/projectile/fired_projectile)
+ create_shrapnel(hit_turf, 8, fired_projectile.dir, 360, /datum/ammo/flare/starshell, fired_projectile.weapon_cause_data, FALSE, 0)
+
/datum/ammo/flare/starshell/set_bullet_traits()
LAZYADD(traits_to_give, list(
BULLET_TRAIT_ENTRY(/datum/element/bullet_trait_iff),
@@ -440,11 +488,6 @@
/datum/ammo/grenade_container/rifle
flags_ammo_behavior = NO_FLAGS
-/datum/ammo/grenade_container/smoke
- name = "smoke grenade shell"
- nade_type = /obj/item/explosive/grenade/smokebomb
- icon_state = "smoke_shell"
-
/datum/ammo/grenade_container/tank_glauncher
max_range = 8
diff --git a/code/datums/ammo/rocket.dm b/code/datums/ammo/rocket.dm
index ec74c8622a7a..2c5aeef2a097 100644
--- a/code/datums/ammo/rocket.dm
+++ b/code/datums/ammo/rocket.dm
@@ -159,20 +159,26 @@
shell_speed = AMMO_SPEED_TIER_3
/datum/ammo/rocket/ltb/on_hit_mob(mob/mob, obj/projectile/projectile)
- cell_explosion(get_turf(mob), 220, 50, EXPLOSION_FALLOFF_SHAPE_LINEAR, null, projectile.weapon_cause_data)
- cell_explosion(get_turf(mob), 200, 100, EXPLOSION_FALLOFF_SHAPE_LINEAR, null, projectile.weapon_cause_data)
+ cell_explosion(get_turf(mob), 200, 200, EXPLOSION_FALLOFF_SHAPE_LINEAR, null, projectile.weapon_cause_data)
+ cell_explosion(get_turf(mob), 100, 25, EXPLOSION_FALLOFF_SHAPE_LINEAR, null, projectile.weapon_cause_data)
/datum/ammo/rocket/ltb/on_hit_obj(obj/object, obj/projectile/projectile)
- cell_explosion(get_turf(object), 220, 50, EXPLOSION_FALLOFF_SHAPE_LINEAR, null, projectile.weapon_cause_data)
- cell_explosion(get_turf(object), 200, 100, EXPLOSION_FALLOFF_SHAPE_LINEAR, null, projectile.weapon_cause_data)
+ cell_explosion(get_turf(object), 200, 200, EXPLOSION_FALLOFF_SHAPE_LINEAR, null, projectile.weapon_cause_data)
+ cell_explosion(get_turf(object), 100, 25, EXPLOSION_FALLOFF_SHAPE_LINEAR, null, projectile.weapon_cause_data)
/datum/ammo/rocket/ltb/on_hit_turf(turf/turf, obj/projectile/projectile)
- cell_explosion(get_turf(turf), 220, 50, EXPLOSION_FALLOFF_SHAPE_LINEAR, null, projectile.weapon_cause_data)
- cell_explosion(get_turf(turf), 200, 100, EXPLOSION_FALLOFF_SHAPE_LINEAR, null, projectile.weapon_cause_data)
+ cell_explosion(get_turf(turf), 200, 200, EXPLOSION_FALLOFF_SHAPE_LINEAR, null, projectile.weapon_cause_data)
+ cell_explosion(get_turf(turf), 100, 25, EXPLOSION_FALLOFF_SHAPE_LINEAR, null, projectile.weapon_cause_data)
/datum/ammo/rocket/ltb/do_at_max_range(obj/projectile/projectile)
- cell_explosion(get_turf(projectile), 220, 50, EXPLOSION_FALLOFF_SHAPE_LINEAR, null, projectile.weapon_cause_data)
- cell_explosion(get_turf(projectile), 200, 100, EXPLOSION_FALLOFF_SHAPE_LINEAR, null, projectile.weapon_cause_data)
+ cell_explosion(get_turf(projectile), 200, 200, EXPLOSION_FALLOFF_SHAPE_LINEAR, null, projectile.weapon_cause_data)
+ cell_explosion(get_turf(projectile), 100, 25, EXPLOSION_FALLOFF_SHAPE_LINEAR, null, projectile.weapon_cause_data)
+
+/datum/ammo/rocket/ltb/set_bullet_traits()
+ . = ..()
+ LAZYADD(traits_to_give, list(
+ BULLET_TRAIT_ENTRY(/datum/element/bullet_trait_iff)
+ ))
/datum/ammo/rocket/wp
name = "white phosphorous rocket"
diff --git a/code/datums/components/tank_rider.dm b/code/datums/components/tank_rider.dm
new file mode 100644
index 000000000000..8d9ae3d1c1e0
--- /dev/null
+++ b/code/datums/components/tank_rider.dm
@@ -0,0 +1,41 @@
+/**
+ * tank_rider tracks whether an atom/movable (a mob or a droppable obj) is currently riding atop a tank's hull.
+ *
+ * Added by /obj/vehicle/multitile/tank/proc/mark_on_top and obj_mark_on_top when something climbs onto,
+ * or is otherwise placed atop, a tank. Removed by clear_on_top/obj_clear_on_top whenit leaves
+ *
+ * holds a weakref to the tank rather than a direct reference so a rider never keeps a destroyed tank
+ * referenced, and so the component can detect a qdeld tank and clean ittself up on the next lookup
+ */
+/datum/component/tank_rider
+ dupe_mode = COMPONENT_DUPE_UNIQUE_PASSARGS
+ var/datum/weakref/tank_ref
+
+/datum/component/tank_rider/Initialize(obj/vehicle/multitile/tank/tank)
+ if(!ismovable(parent) || !istype(tank))
+ return COMPONENT_INCOMPATIBLE
+ tank_ref = WEAKREF(tank)
+
+/datum/component/tank_rider/InheritComponent(datum/component/tank_rider/new_comp, i_am_original, obj/vehicle/multitile/tank/tank)
+ if(istype(tank))
+ tank_ref = WEAKREF(tank)
+
+/datum/component/tank_rider/Destroy(force, silent)
+ tank_ref = null
+ return ..()
+
+/datum/component/tank_rider/RegisterWithParent()
+ RegisterSignal(parent, COMSIG_PARENT_QDELETING, PROC_REF(on_parent_qdel))
+
+/datum/component/tank_rider/UnregisterFromParent()
+ UnregisterSignal(parent, COMSIG_PARENT_QDELETING)
+
+/datum/component/tank_rider/proc/on_parent_qdel()
+ SIGNAL_HANDLER
+ qdel(src)
+
+/datum/component/tank_rider/proc/get_tank()
+ var/obj/vehicle/multitile/tank/tank = tank_ref?.resolve()
+ if(!tank)
+ qdel(src)
+ return tank
diff --git a/code/datums/elements/bloody_feet.dm b/code/datums/elements/bloody_feet.dm
index 8964eac5c882..2990e360ad99 100644
--- a/code/datums/elements/bloody_feet.dm
+++ b/code/datums/elements/bloody_feet.dm
@@ -11,6 +11,7 @@
/// Whether the human has moved into the turf giving them bloody feet
/// Necessary because of how Crossed is called before Moved
var/list/entered_bloody_turf
+ var/dry_timer_id
/datum/element/bloody_feet/Attach(datum/target, dry_time, obj/item/clothing/shoes, steps, bcolor)
. = ..()
@@ -32,9 +33,11 @@
RegisterSignal(shoes, COMSIG_ITEM_DROPPED, PROC_REF(on_shoes_removed), override = TRUE)
if(dry_time)
- addtimer(CALLBACK(src, PROC_REF(clear_blood), target), dry_time)
+ dry_timer_id = addtimer(CALLBACK(src, PROC_REF(clear_blood), target), dry_time, TIMER_STOPPABLE)
/datum/element/bloody_feet/Detach(datum/target, force)
+ deltimer(dry_timer_id)
+ dry_timer_id = null
UnregisterSignal(target, list(
COMSIG_MOVABLE_MOVED,
COMSIG_HUMAN_BLOOD_CROSSED,
diff --git a/code/datums/keybinding/vehicles.dm b/code/datums/keybinding/vehicles.dm
index 22c20e678c4b..dcc4b95e6c59 100644
--- a/code/datums/keybinding/vehicles.dm
+++ b/code/datums/keybinding/vehicles.dm
@@ -84,3 +84,25 @@
var/obj/vehicle/multitile/vehicle_user = user.mob.interactee
vehicle_user.reload_firing_port_weapon()
return TRUE
+
+/**
+ * Toggles the fire mode of hardpoints with more than one fire mode. currently only used for the primary and secondary
+ * flamers
+ */
+/datum/keybinding/vehicles/toggle_hardpoint_fire_mode
+ hotkey_keys = list("Unbound")
+ classic_keys = list("Unbound")
+ name = "Toggle hardpoint fire mode"
+ full_name = "Toggle Hardpoint Fire Mode"
+ keybind_signal = COMSIG_KB_VEHICLE_TOGGLE_HARDPOINT_FIRE_MODE
+
+/datum/keybinding/vehicles/toggle_hardpoint_fire_mode/down(client/user)
+ . = ..()
+ if(.)
+ return
+ var/obj/vehicle/multitile/vehicle_user = user.mob.interactee
+ var/obj/item/hardpoint/HP = vehicle_user.get_mob_hp(user.mob)
+ if(!HP)
+ return
+ HP.toggle_fire_mode(user.mob)
+ return TRUE
diff --git a/code/datums/supply_packs/explosives.dm b/code/datums/supply_packs/explosives.dm
index bf17a60a7880..b987103b2bfb 100644
--- a/code/datums/supply_packs/explosives.dm
+++ b/code/datums/supply_packs/explosives.dm
@@ -238,8 +238,14 @@
contains = list(
/obj/item/storage/box/packet/flare,
/obj/item/storage/box/packet/flare,
+ /obj/item/storage/box/packet/flare,
+ /obj/item/storage/box/packet/flare,
+ /obj/item/storage/box/packet/flare,
+ /obj/item/storage/box/packet/flare,
+ /obj/item/storage/box/packet/flare,
+ /obj/item/storage/box/packet/flare,
)
- cost = 40
+ cost = 10
containertype = /obj/structure/closet/crate/explosives
containername = "M74 AGM-Star Shell Grenade Crate"
group = "Explosives"
diff --git a/code/datums/supply_packs/vehicle_ammo.dm b/code/datums/supply_packs/vehicle_ammo.dm
index 43ce36ec2b64..9e783d424ec7 100644
--- a/code/datums/supply_packs/vehicle_ammo.dm
+++ b/code/datums/supply_packs/vehicle_ammo.dm
@@ -83,16 +83,22 @@
containername = "M92T Grenade Launcher ammo crate"
group = "Vehicle Ammo"
+// Mimicks the star shell pack in the explosive sections. Also, does it really make sense to have those cost 4k ??
/datum/supply_packs/ammo_slauncher
- name = "M34A2-A Multipurpose Turret smoke screen magazines (x4)"
+ name = "M74 AGM-S star shell packets (x8)"
contains = list(
- /obj/item/ammo_magazine/hardpoint/turret_smoke,
- /obj/item/ammo_magazine/hardpoint/turret_smoke,
- /obj/item/ammo_magazine/hardpoint/turret_smoke,
+ /obj/item/storage/box/packet/flare,
+ /obj/item/storage/box/packet/flare,
+ /obj/item/storage/box/packet/flare,
+ /obj/item/storage/box/packet/flare,
+ /obj/item/storage/box/packet/flare,
+ /obj/item/storage/box/packet/flare,
+ /obj/item/storage/box/packet/flare,
+ /obj/item/storage/box/packet/flare,
)
- cost = 20
- containertype = /obj/structure/closet/crate/ammo
- containername = "M34A2-A Multipurpose Turret smoke screen ammo crate"
+ cost = 10
+ containertype = /obj/structure/closet/crate/explosives
+ containername = "M34A2-A Multipurpose Turret flare ammo crate"
group = "Vehicle Ammo"
/datum/supply_packs/ammo_towlauncher
@@ -106,8 +112,8 @@
/datum/supply_packs/ammo_m56_cupola
name = "M56 Cupola magazines (x2)"
contains = list(
- /obj/item/ammo_magazine/hardpoint/m56_cupola,
- /obj/item/ammo_magazine/hardpoint/m56_cupola,
+ /obj/item/ammo_magazine/m56d,
+ /obj/item/ammo_magazine/m56d,
)
cost = 20
containertype = /obj/structure/closet/crate/ammo
diff --git a/code/game/atoms_movable.dm b/code/game/atoms_movable.dm
index 68ea2b137818..200a76f3cc22 100644
--- a/code/game/atoms_movable.dm
+++ b/code/game/atoms_movable.dm
@@ -39,6 +39,11 @@
/// Holds a reference to the emissive blocker overlay
var/emissive_overlay
+ /// Is this item allowed atop a climbable vehicle?
+ /// Not at all intended for structures: This is meant for grabbable items. Weapons, ammo, pizza crates, etc...
+ /// ...However, defining it as part of obj instead of obj/item helps us handle exceptions.
+ /// E.G: Exceptionally, as QoL for corpsmen: bodybags, stasis bags, and roller beds are allowed.
+ var/is_allowed_atop_vehicle = FALSE
/// A weakref to the mob currently interacting with us.
var/datum/weakref/interactor
@@ -500,8 +505,18 @@
SEND_SIGNAL(src, COMSIG_OBJ_AFTER_BUCKLE, buckled_mob)
if(!buckled_mob)
UnregisterSignal(buckle_target, COMSIG_PARENT_QDELETING)
+ if(isliving(buckle_target))
+ var/mob/living/living_M = buckle_target
+ var/obj/vehicle/multitile/tank/tank = living_M.get_tank_on_top_of()
+ if(tank)
+ tank.clear_on_top(living_M)
else
RegisterSignal(buckled_mob, COMSIG_PARENT_QDELETING, PROC_REF(unbuckle))
+ if(isliving(buckled_mob))
+ var/mob/living/living_M = buckled_mob
+ var/obj/vehicle/multitile/tank/tank = get_tank_on_top_of()
+ if(tank)
+ tank.mark_on_top(living_M)
return buckled_mob
/atom/movable/proc/handle_rotation()
@@ -548,3 +563,12 @@
return NO_BLOCKED_MOVEMENT
return ..()
+
+/atom/movable/proc/get_tank_on_top_of()
+ var/datum/component/tank_rider/rider = GetComponent(/datum/component/tank_rider)
+ if(!rider)
+ return null
+ return rider.get_tank()
+
+/atom/movable/proc/is_atop_vehicle()
+ return GetComponent(/datum/component/tank_rider) ? TRUE : FALSE
diff --git a/code/game/machinery/vending/vendor_types/crew/vehicle_crew.dm b/code/game/machinery/vending/vendor_types/crew/vehicle_crew.dm
index 3ced90bfe322..836f1eb586c4 100644
--- a/code/game/machinery/vending/vendor_types/crew/vehicle_crew.dm
+++ b/code/game/machinery/vending/vendor_types/crew/vehicle_crew.dm
@@ -141,11 +141,14 @@ GLOBAL_LIST_INIT(cm_vending_vehicle_crew_tank, list(
list("AC3-E Autocannon", 0, /obj/effect/essentials_set/tank/autocannon, VEHICLE_PRIMARY_AVAILABLE, VENDOR_ITEM_RECOMMENDED),
list("DRG-N Offensive Flamer Unit", 0, /obj/effect/essentials_set/tank/dragonflamer, VEHICLE_PRIMARY_AVAILABLE, VENDOR_ITEM_REGULAR),
list("LTAA-AP Minigun", 0, /obj/effect/essentials_set/tank/gatling, VEHICLE_PRIMARY_AVAILABLE, VENDOR_ITEM_REGULAR),
+ list("LTB Cannon", 0, /obj/effect/essentials_set/tank/ltb, VEHICLE_PRIMARY_AVAILABLE, VENDOR_ITEM_RECOMMENDED),
list("SECONDARY WEAPON", 0, null, null, null),
list("M92T Grenade Launcher", 0, /obj/effect/essentials_set/tank/tankgl, VEHICLE_SECONDARY_AVAILABLE, VENDOR_ITEM_REGULAR),
- list("M56 Cupola", 0, /obj/effect/essentials_set/tank/m56cupola, VEHICLE_SECONDARY_AVAILABLE, VENDOR_ITEM_REGULAR),
- list("LZR-N Flamer Unit", 0, /obj/effect/essentials_set/tank/tankflamer, VEHICLE_SECONDARY_AVAILABLE, VENDOR_ITEM_RECOMMENDED),
+ list("M56 Cupola", 0, /obj/effect/essentials_set/tank/m56cupola, VEHICLE_SECONDARY_AVAILABLE, VENDOR_ITEM_RECOMMENDED),
+ list("M6H-BRUTE Launcher", 0, /obj/effect/essentials_set/tank/brute_launcher, VEHICLE_SECONDARY_AVAILABLE, VENDOR_ITEM_REGULAR),
+ list("LZR-N Flamer Unit", 0, /obj/effect/essentials_set/tank/tankflamer, VEHICLE_SECONDARY_AVAILABLE, VENDOR_ITEM_REGULAR),
+ list("Mounted UA Flag", 0, /obj/effect/essentials_set/tank/united_americas_flag, VEHICLE_SECONDARY_AVAILABLE, VENDOR_ITEM_RECOMMENDED),
list("SUPPORT MODULE", 0, null, null, null),
list("Artillery Module", 0, /obj/item/hardpoint/support/artillery_module, VEHICLE_SUPPORT_AVAILABLE, VENDOR_ITEM_REGULAR),
@@ -375,11 +378,18 @@ GLOBAL_LIST_INIT(cm_vending_clothing_vehicle_crew, list(
desc = "A giant cannon firing explosive 86mm shells. You'd be lucky if this even leaves the dust of whatever you hit with it."
spawned_gear_list = list(
/obj/item/hardpoint/primary/cannon,
- /obj/item/ammo_magazine/hardpoint/ltb_cannon,
- /obj/item/ammo_magazine/hardpoint/ltb_cannon,
- /obj/item/ammo_magazine/hardpoint/ltb_cannon,
- /obj/item/ammo_magazine/hardpoint/ltb_cannon,
- /obj/item/ammo_magazine/hardpoint/ltb_cannon,
+ /obj/item/ammo_magazine/hardpoint/ltb_cannon/he_shell,
+ /obj/item/ammo_magazine/hardpoint/ltb_cannon/he_shell,
+ /obj/item/ammo_magazine/hardpoint/ltb_cannon/he_shell,
+ /obj/item/ammo_magazine/hardpoint/ltb_cannon/he_shell,
+ /obj/item/ammo_magazine/hardpoint/ltb_cannon/he_shell,
+ /obj/item/ammo_magazine/hardpoint/ltb_cannon/he_shell,
+ /obj/item/ammo_magazine/hardpoint/ltb_cannon/he_shell,
+ /obj/item/ammo_magazine/hardpoint/ltb_cannon/he_shell,
+ /obj/item/ammo_magazine/hardpoint/ltb_cannon/he_shell,
+ /obj/item/ammo_magazine/hardpoint/ltb_cannon/he_shell,
+ /obj/item/ammo_magazine/hardpoint/ltb_cannon/he_shell,
+ /obj/item/ammo_magazine/hardpoint/ltb_cannon/he_shell,
)
/obj/effect/essentials_set/tank/gatling
@@ -427,7 +437,7 @@ GLOBAL_LIST_INIT(cm_vending_clothing_vehicle_crew, list(
desc = "A permanently fixed M56D, firing standard issue 10x28mm rounds."
spawned_gear_list = list(
/obj/item/hardpoint/secondary/m56cupola,
- /obj/item/ammo_magazine/hardpoint/m56_cupola,
+ /obj/item/ammo_magazine/m56d,
)
/obj/effect/essentials_set/tank/tankgl
@@ -440,11 +450,29 @@ GLOBAL_LIST_INIT(cm_vending_clothing_vehicle_crew, list(
/obj/item/ammo_magazine/hardpoint/tank_glauncher,
)
+/obj/effect/essentials_set/tank/brute_launcher
+ desc = "A tank-mounted BRUTE breaching rocket launcher. Laser-guides a single 90mm shaped-charge rocket onto structures and walls, given a few seconds to lock on."
+ spawned_gear_list = list(
+ /obj/item/hardpoint/secondary/brute_launcher,
+ /obj/item/ammo_magazine/rocket/brute,
+ /obj/item/ammo_magazine/rocket/brute,
+ /obj/item/ammo_magazine/rocket/brute,
+ /obj/item/ammo_magazine/rocket/brute,
+ /obj/item/ammo_magazine/rocket/brute,
+ /obj/item/ammo_magazine/rocket/brute,
+ )
+
+/obj/effect/essentials_set/tank/united_americas_flag
+ desc = "An United Americas Flag for mounting atop the tank."
+ spawned_gear_list = list(
+ /obj/item/hardpoint/secondary/united_americas_flag,
+ )
+
/obj/effect/essentials_set/tank/turret
spawned_gear_list = list(
/obj/item/hardpoint/holder/tank_turret,
- /obj/item/ammo_magazine/hardpoint/turret_smoke,
- /obj/item/ammo_magazine/hardpoint/turret_smoke,
+ /obj/item/storage/box/packet/flare,
+ /obj/item/storage/box/packet/flare,
)
/obj/effect/essentials_set/apc/dualcannon
diff --git a/code/game/objects/effects/aliens.dm b/code/game/objects/effects/aliens.dm
index e7b39d45da5c..1e1feb3b2da0 100644
--- a/code/game/objects/effects/aliens.dm
+++ b/code/game/objects/effects/aliens.dm
@@ -43,7 +43,7 @@
density = FALSE
opacity = FALSE
anchored = TRUE
- layer = ABOVE_OBJ_LAYER
+ layer = TANK_RIDER_LAYER
mouse_opacity = MOUSE_OPACITY_TRANSPARENT
var/datum/cause_data/cause_data
diff --git a/code/game/objects/items.dm b/code/game/objects/items.dm
index fe0e01e50186..8ef8ba3507f4 100644
--- a/code/game/objects/items.dm
+++ b/code/game/objects/items.dm
@@ -167,6 +167,9 @@
/// Special storages this item prioritizes
var/list/preferred_storage
+ /// Rifles, shells, grenades, metal sheets, et-cetera should all be able to 'fit' atop the tank.
+ is_allowed_atop_vehicle = TRUE
+
/obj/item/Initialize(mapload, ...)
. = ..()
@@ -410,6 +413,18 @@
playsound(src, drop_sound, dropvol, drop_vary)
src.do_drop_animation(user)
+ if(isliving(user))
+ var/mob/living/living_mob = user
+ var/obj/vehicle/multitile/tank/tank = living_mob.get_tank_on_top_of()
+ var/obj/vehicle/multitile/tank/current_tank = src.get_tank_on_top_of()
+ if(tank && !current_tank) // tank exists, our user is atop the tank, we are not marked as atop the tank yet.
+ tank.obj_mark_on_top(src)
+ else if (!tank && current_tank) // only remove from vehicle if it is atop a vehicle to begin with
+ current_tank.obj_clear_on_top(src)
+ else if (tank && current_tank) // User on tank AND already marked
+ src.pixel_y = initial(src.pixel_y) + 12 // Just refresh the pixel offset and layer to avoid a visual bug with animations
+ src.layer = TANK_RIDER_OBJ_LAYER
+
appearance_flags &= ~NO_CLIENT_COLOR //So saturation/desaturation etc. effects affect it.
/// Called just as an item is picked up (loc is not yet changed) and will return TRUE if the pickup wasn't canceled.
diff --git a/code/game/objects/items/bodybag.dm b/code/game/objects/items/bodybag.dm
index 937867a7f443..4f70bd0150b8 100644
--- a/code/game/objects/items/bodybag.dm
+++ b/code/game/objects/items/bodybag.dm
@@ -26,9 +26,17 @@
deploy_bodybag(user, T)
/obj/item/bodybag/proc/deploy_bodybag(mob/user, atom/location)
- var/obj/structure/closet/bodybag/R = new unfolded_path(location, src)
- R.add_fingerprint(user)
- R.open(user)
+ var/obj/structure/closet/bodybag/bodybag = new unfolded_path(location, src)
+ bodybag.add_fingerprint(user)
+ bodybag.open(user)
+ if(isliving(user))
+ var/mob/living/living_mob = user
+ var/obj/vehicle/multitile/tank/tank = living_mob.get_tank_on_top_of()
+ var/obj/vehicle/multitile/tank/current_tank = bodybag.get_tank_on_top_of()
+ if(tank && !current_tank) // tank exists, our user is atop the tank, we are not marked as atop the tank yet.
+ tank.obj_mark_on_top(bodybag)
+ else if (!tank && current_tank) // only remove from vehicle if it is atop a vehicle to begin with
+ current_tank.obj_clear_on_top(bodybag)
user.temp_drop_inv_item(src)
qdel(src)
@@ -83,6 +91,7 @@
/// How many extra pixels to offset the bag by when buckled, since rollerbeds are set up to offset a centered horizontal human sprite.
var/buckle_offset = 5
store_items = FALSE
+ is_allowed_atop_vehicle = TRUE // Allows bodybags, exceptionally, as structures, to be used atop vehicles such as the tank.
/obj/structure/closet/bodybag/Initialize()
. = ..()
@@ -208,7 +217,8 @@
/obj/structure/closet/bodybag/forceMove(atom/destination)
if(roller_buckled)
- roller_buckled.unbuckle()
+ if(!is_atop_vehicle() && !roller_buckled.is_atop_vehicle())
+ roller_buckled.unbuckle()
. = ..()
@@ -272,10 +282,16 @@
overlays.Cut() // makes sure any previous triage cards are removed
if(!stasis_mob)
- layer = initial(layer)
+ if(is_atop_vehicle())
+ layer = TANK_RIDER_OBJ_LAYER
+ else
+ layer = initial(layer)
return
- layer = LYING_BETWEEN_MOB_LAYER
+ if(is_atop_vehicle())
+ layer = TANK_RIDER_OBJ_LAYER
+ else
+ layer = LYING_BETWEEN_MOB_LAYER
if(stasis_mob.holo_card_color && !opened)
var/image/holo_card_icon = image('icons/obj/bodybag.dmi', src, "cryocard_[stasis_mob.holo_card_color]")
@@ -297,6 +313,12 @@
if(stasis_mob)
stasis_mob.in_stasis = FALSE
UnregisterSignal(stasis_mob, COMSIG_HUMAN_TRIAGE_CARD_UPDATED)
+ var/obj/vehicle/multitile/tank/tank = src.get_tank_on_top_of()
+ var/obj/vehicle/multitile/tank/mob_tank = stasis_mob.get_tank_on_top_of()
+ if(tank)
+ tank.mark_on_top(stasis_mob)
+ else if (mob_tank)
+ mob_tank.clear_on_top(stasis_mob)
stasis_mob = null
STOP_PROCESSING(SSobj, src)
if(used > max_uses)
diff --git a/code/game/objects/items/explosives/mine.dm b/code/game/objects/items/explosives/mine.dm
index 61d72151355e..b904d78d9157 100644
--- a/code/game/objects/items/explosives/mine.dm
+++ b/code/game/objects/items/explosives/mine.dm
@@ -235,6 +235,8 @@
return
if(HAS_TRAIT(enemy, TRAIT_ABILITY_BURROWED))
return
+ if(enemy.is_on_tank_hull())
+ return
enemy.visible_message(SPAN_DANGER("[icon2html(src, viewers(src))] The [name] clicks as [enemy] moves in front of it."),
SPAN_DANGER("[icon2html(src, enemy)] The [name] clicks as you move in front of it."),
diff --git a/code/game/objects/items/storage/belt.dm b/code/game/objects/items/storage/belt.dm
index 4e6511195a5a..a389f5774128 100644
--- a/code/game/objects/items/storage/belt.dm
+++ b/code/game/objects/items/storage/belt.dm
@@ -2830,9 +2830,9 @@
/obj/item/ammo_magazine/hardpoint/secondary_flamer,
/obj/item/ammo_magazine/hardpoint/ace_autocannon,
/obj/item/ammo_magazine/hardpoint/towlauncher,
- /obj/item/ammo_magazine/hardpoint/m56_cupola,
+ /obj/item/ammo_magazine/m56d,
/obj/item/ammo_magazine/hardpoint/tank_glauncher,
- /obj/item/ammo_magazine/hardpoint/turret_smoke,
+ /obj/item/storage/box/packet/flare,
/obj/item/ammo_magazine/hardpoint/boyars_dualcannon,
/obj/item/ammo_magazine/hardpoint/flare_launcher,
)
diff --git a/code/game/objects/objs.dm b/code/game/objects/objs.dm
index b635123a1b0c..966cd010cf02 100644
--- a/code/game/objects/objs.dm
+++ b/code/game/objects/objs.dm
@@ -36,7 +36,6 @@
vis_flags = VIS_INHERIT_PLANE
-
/obj/Initialize(mapload, ...)
. = ..()
if(garbage)
@@ -235,6 +234,55 @@
buckle_mob(M, user)
else . = ..()
+/obj/Moved(atom/oldloc, direction, Forced = FALSE)
+ . = ..()
+ var/obj/vehicle/multitile/tank/tank = src.get_tank_on_top_of()
+ if(tank && isturf(loc))
+ var/still_on_tank = FALSE
+ for(var/obj/vehicle/multitile/tank/candidate in loc)
+ if(loc in candidate.locs)
+ still_on_tank = TRUE
+ break
+ if(!still_on_tank)
+ tank.obj_clear_on_top(src)
+
+/obj/Move(NewLoc, direct)
+ . = ..()
+ handle_rotation()
+ if(src.is_atop_vehicle())
+ var/still_on_tank = FALSE
+ if(isturf(NewLoc))
+ for(var/obj/vehicle/multitile/tank/T in NewLoc)
+ if(NewLoc in T.locs)
+ still_on_tank = TRUE
+ break
+
+ if(!still_on_tank && buckled_mob && ismob(buckled_mob))
+ var/mob/living/buckled_living = buckled_mob
+ var/obj/vehicle/multitile/tank/temp_tank = buckled_living.get_tank_on_top_of()
+ if(temp_tank)
+ temp_tank.clear_on_top(buckled_living)
+
+ src.forceMove(NewLoc)
+ if(buckled_mob)
+ buckled_mob.forceMove(NewLoc)
+ return
+ if(. && buckled_mob && !handle_buckled_mob_movement(loc,direct))
+ . = FALSE
+
+/obj/forceMove(atom/dest)
+ . = ..()
+
+ // Bring the buckled_mob with us. No Move(), on_move callbacks, or any of this bullshit, we just got teleported
+ if(buckled_mob && loc == dest)
+ buckled_mob.forceMove(dest)
+
+/obj/BlockedPassDirs(atom/movable/mover, target_dir)
+ if(mover == buckled_mob) //can't collide with the thing you're buckled to
+ return NO_BLOCKED_MOVEMENT
+
+ return ..()
+
/obj/item/proc/get_mob_overlay(mob/user_mob, slot, default_bodytype = "Default")
var/bodytype = default_bodytype
var/mob/living/carbon/human/user_human
@@ -332,6 +380,10 @@
return
/obj/handle_flamer_fire(obj/flamer_fire/fire, damage, delta_time)
+ if(isitem(src))
+ var/obj/item/dropped_item = src
+ if(dropped_item.is_atop_vehicle()) // don't process flamer fire if the item is atop a climbable vehicle.
+ return
. = ..()
flamer_fire_act(damage, fire.weapon_cause_data)
diff --git a/code/game/objects/structures/barricade/deployable.dm b/code/game/objects/structures/barricade/deployable.dm
index 727629f763b9..28f62aecc2c4 100644
--- a/code/game/objects/structures/barricade/deployable.dm
+++ b/code/game/objects/structures/barricade/deployable.dm
@@ -147,6 +147,12 @@
to_chat(usr, SPAN_WARNING("[singular_name] cannot be built here!"))
return
+ if(isliving(user))
+ var/mob/living/L = user
+ if(L.is_on_tank_hull())
+ to_chat(user, SPAN_WARNING("[singular_name] cannot be built here!"))
+ return
+
user.visible_message(SPAN_NOTICE("[user] begins deploying [singular_name]."),
SPAN_NOTICE("You begin deploying [singular_name]."))
diff --git a/code/game/objects/structures/crates_lockers/closets.dm b/code/game/objects/structures/crates_lockers/closets.dm
index f817ba2e196e..7edefcf837b3 100644
--- a/code/game/objects/structures/crates_lockers/closets.dm
+++ b/code/game/objects/structures/crates_lockers/closets.dm
@@ -76,8 +76,17 @@
/obj/structure/closet/proc/dump_contents()
- for(var/obj/I in src)
- I.forceMove(loc)
+ var/obj/vehicle/multitile/tank/tank = src.get_tank_on_top_of()
+
+ for(var/obj/object in src)
+ object.forceMove(loc)
+ // closets can't go atop vehicles, but this sets behavior for bodybags, which can.
+ if(tank)
+ tank.obj_mark_on_top(object)
+ else
+ var/obj/vehicle/multitile/tank/obj_tank = object.get_tank_on_top_of()
+ if(obj_tank)
+ obj_tank.obj_clear_on_top(object)
for(var/mob/M in src)
M.forceMove(loc)
@@ -88,6 +97,13 @@
if(living_M.mobility_flags & MOBILITY_MOVE)
M.visible_message(SPAN_WARNING("[M] suddenly gets out of [src]!"),
SPAN_WARNING("You get out of [src] and get your bearings!"))
+ // closets can't go atop vehicles, but this sets behavior for bodybags, which can.
+ if(tank)
+ tank.mark_on_top(living_M)
+ else
+ var/obj/vehicle/multitile/tank/mob_tank = living_M.get_tank_on_top_of()
+ if(mob_tank)
+ mob_tank.clear_on_top(living_M)
/// Attempts to open this closet by user, skipping checks that prevent opening if forced
/obj/structure/closet/proc/open(mob/user, force)
diff --git a/code/game/objects/structures/stool_bed_chair_nest/bed.dm b/code/game/objects/structures/stool_bed_chair_nest/bed.dm
index ce3f9c6993a6..e3d68055e101 100644
--- a/code/game/objects/structures/stool_bed_chair_nest/bed.dm
+++ b/code/game/objects/structures/stool_bed_chair_nest/bed.dm
@@ -60,14 +60,20 @@
/obj/structure/bed/afterbuckle(mob/M)
. = ..()
if(. && buckled_mob == M)
- M.pixel_y = buckling_y
+ if(is_atop_vehicle())
+ M.pixel_y = buckling_y + 12 // Magic number bad. TODO: Make tank pixel offset its own variable.
+ else
+ M.pixel_y = buckling_y
M.old_y = buckling_y
M.pixel_x = buckling_x
M.old_x = buckling_x
if(base_bed_icon)
density = TRUE
else
- M.pixel_y = initial(buckled_mob.pixel_y)
+ if(is_atop_vehicle())
+ M.pixel_y = initial(buckled_mob.pixel_y) + 12 // Magic number bad. TODO: Make tank pixel offset its own variable.
+ else
+ M.pixel_y = initial(buckled_mob.pixel_y)
M.old_y = initial(buckled_mob.pixel_y)
M.pixel_x = initial(buckled_mob.pixel_x)
M.old_x = initial(buckled_mob.pixel_x)
@@ -85,7 +91,11 @@
buckled_bodybag = B
density = TRUE
update_icon()
- if(buckling_y)
+ var/obj/vehicle/multitile/tank/tank = get_tank_on_top_of()
+ if(tank)
+ tank.obj_mark_on_top(buckled_bodybag)
+ buckled_bodybag.pixel_y = buckled_bodybag.buckle_offset + 12 // Magic number bad. TODO: Make tank pixel offset its own variable.
+ else if(buckling_y)
buckled_bodybag.pixel_y = buckled_bodybag.buckle_offset + buckling_y
add_fingerprint(user)
var/mob/living/carbon/human/contained_mob = locate() in B.contents
@@ -94,7 +104,10 @@
/obj/structure/bed/unbuckle()
if(buckled_bodybag)
- buckled_bodybag.pixel_y = initial(buckled_bodybag.pixel_y)
+ if(is_atop_vehicle())
+ buckled_bodybag.pixel_y = initial(buckled_bodybag.pixel_y) + 12 // Magic number bad. TODO: Make tank pixel offset its own variable.
+ else
+ buckled_bodybag.pixel_y = initial(buckled_bodybag.pixel_y)
buckled_bodybag.roller_buckled = null
buckled_bodybag = null
density = FALSE
@@ -214,6 +227,7 @@
foldabletype = /obj/item/roller
accepts_bodybag = TRUE
base_bed_icon = "roller"
+ is_allowed_atop_vehicle = TRUE // Allows roller beds, exceptionally, as structures, to be used atop vehicles such as the tank.
/obj/structure/bed/roller/Initialize(mapload, ...)
. = ..()
@@ -311,6 +325,14 @@
roller.add_fingerprint(user)
user.temp_drop_inv_item(src)
forceMove(roller)
+ if(isliving(user))
+ var/mob/living/living_mob = user
+ var/obj/vehicle/multitile/tank/tank = living_mob.get_tank_on_top_of()
+ var/obj/vehicle/multitile/tank/roller_tank = roller.get_tank_on_top_of()
+ if(tank && !roller_tank) // tank exists, our user is atop the tank, we are not marked as atop the tank yet.
+ tank.obj_mark_on_top(roller)
+ else if (!tank && roller_tank) // only remove from vehicle if it is atop a vehicle to begin with
+ roller_tank.obj_clear_on_top(roller)
SEND_SIGNAL(user, COMSIG_MOB_ITEM_ROLLER_DEPLOYED, roller)
if(target_mob)
roller.buckle_mob(target_mob, user)
@@ -467,6 +489,11 @@ GLOBAL_LIST_EMPTY(activated_medevac_stretchers)
matter = list("plastic" = 5000, "metal" = 5000)
/obj/item/roller/medevac/deploy_roller(mob/user, atom/location)
+ if(isliving(user))
+ var/mob/living/L = user
+ if(L.is_on_tank_hull())
+ to_chat(user, SPAN_WARNING("You can't set up \the [src] here."))
+ return
var/obj/structure/bed/medevac_stretcher/medevac_stretcher = new rollertype(location)
medevac_stretcher.add_fingerprint(user)
user.temp_drop_inv_item(src)
diff --git a/code/game/turfs/open_space.dm b/code/game/turfs/open_space.dm
index dfbffbc77c2d..490682e7d316 100644
--- a/code/game/turfs/open_space.dm
+++ b/code/game/turfs/open_space.dm
@@ -105,7 +105,18 @@ GLOBAL_DATUM_INIT(openspace_backdrop_one_for_all, /atom/movable/openspace_backdr
while(istype(below, /turf/open_space))
below = SSmapping.get_turf_below(below)
+ var/obj/vehicle/multitile/tank/tank_at_destination = null
+ for(var/obj/vehicle/multitile/tank/T in below.contents)
+ if(below in T.locs)
+ tank_at_destination = T
+ break
+
user.forceMove(below)
+
+ if(tank_at_destination && isliving(user))
+ var/mob/living/L = user
+ tank_at_destination.mark_on_top(L)
+
for(var/atom/movable/thing as anything in grabbed_things) // grabbed things aren't moved to the tile immediately to: make the animation better, preserve the grab
thing.forceMove(below)
below.on_climb_down(user)
@@ -128,6 +139,16 @@ GLOBAL_DATUM_INIT(openspace_backdrop_one_for_all, /atom/movable/openspace_backdr
var/mob/living/living_mob = movable
living_mob.death(create_cause_data("falling from a high place"))
+ var/obj/vehicle/multitile/tank/tank_at_destination = null
+ for(var/obj/vehicle/multitile/tank/T in below.contents)
+ if(below in T.locs)
+ tank_at_destination = T
+ break
+
+ if(tank_at_destination && isliving(movable))
+ var/mob/living/L = movable
+ tank_at_destination.mark_on_top(L)
+
/// Returns a boolean whether the turf is closed and has no opening in any cardinal direction
/turf/proc/check_blocked()
return FALSE
diff --git a/code/modules/animations/animation_library.dm b/code/modules/animations/animation_library.dm
index 5a176eaf8b64..db3ddd8b3720 100644
--- a/code/modules/animations/animation_library.dm
+++ b/code/modules/animations/animation_library.dm
@@ -207,7 +207,10 @@ Can look good elsewhere as well.*/
pixel_x_diff = -pixel_offset
pixel_y_diff = -pixel_offset
animate(src, pixel_x = pixel_x + pixel_x_diff, pixel_y = pixel_y + pixel_y_diff, time = 2, flags = ANIMATION_PARALLEL)
- animate(pixel_x = initial(pixel_x), pixel_y = initial(pixel_y), time = 2)
+ if(isliving(src) && src:is_on_tank_hull())
+ animate(pixel_x = initial(pixel_x), pixel_y = initial(pixel_y) + 12, time = 2)
+ else
+ animate(pixel_x = initial(pixel_x), pixel_y = initial(pixel_y), time = 2)
/atom/proc/animation_spin(speed = 5, loop_amount = -1, clockwise = TRUE, sections = 3, angular_offset = 0, pixel_fuzz = 0)
if(!sections)
diff --git a/code/modules/cm_aliens/XenoStructures.dm b/code/modules/cm_aliens/XenoStructures.dm
index cf1ce914e46b..0bccaa437f55 100644
--- a/code/modules/cm_aliens/XenoStructures.dm
+++ b/code/modules/cm_aliens/XenoStructures.dm
@@ -227,6 +227,10 @@
if(HAS_TRAIT(H, TRAIT_HAULED))
return
+ // mobs atop the tank shouldn't get foot-stabbed
+ if(H.is_on_tank_hull())
+ return
+
H.apply_armoured_damage(damage, penetration = penetration, def_zone = pick(target_limbs))
H.last_damage_data = construction_data
@@ -408,7 +412,7 @@
return 1
/obj/structure/mineral_door/resin/proc/take_damage(dam, mob/mob)
- health -= dam
+ update_health(dam)
healthcheck()
/obj/structure/mineral_door/resin/attackby(obj/item/W, mob/living/user)
diff --git a/code/modules/cm_aliens/structures/trap.dm b/code/modules/cm_aliens/structures/trap.dm
index 5caf6f0ff713..067e54dd5390 100644
--- a/code/modules/cm_aliens/structures/trap.dm
+++ b/code/modules/cm_aliens/structures/trap.dm
@@ -94,6 +94,10 @@
if(RESIN_TRAP_HUGGER)
if(can_hug(victim, hivenumber) && !isyautja(victim) && !issynth(victim) && !isthrall(victim))
var/mob/living/victim_mob = victim
+ // tank treads block the hugger's hole. This behavior can be also added to normal acid traps.
+ var/obj/vehicle/multitile/tank/victim_tank = victim_mob.get_tank_on_top_of()
+ if(victim_tank && (get_turf(victim_mob) in victim_tank.locs))
+ return
victim_mob.visible_message(SPAN_WARNING("[victim_mob] trips on [src]!"))
to_chat(victim_mob, SPAN_DANGER("You trip on [src]!"))
victim_mob.apply_effect(1, WEAKEN)
@@ -109,6 +113,10 @@
if(isxeno(victim))
var/mob/living/carbon/xenomorph/victim_xeno = victim
if(victim_xeno.hivenumber != hivenumber)
+ // just in case a Greeno decides to ride on the tank.
+ var/obj/vehicle/multitile/tank/victim_tank = victim_xeno.get_tank_on_top_of()
+ if(victim_tank && (get_turf(victim_xeno) in victim_tank.locs))
+ return
trigger_trap()
if(isVehicleMultitile(victim) && trap_type != RESIN_TRAP_GAS)
trigger_trap()
diff --git a/code/modules/cm_aliens/structures/xeno_structures_boilertrap.dm b/code/modules/cm_aliens/structures/xeno_structures_boilertrap.dm
index 0194ea74d3a6..ef2368f57133 100644
--- a/code/modules/cm_aliens/structures/xeno_structures_boilertrap.dm
+++ b/code/modules/cm_aliens/structures/xeno_structures_boilertrap.dm
@@ -68,6 +68,10 @@
if (X.hivenumber != hivenumber)
trigger_trap(A)
else if(ishuman(A))
+ // boiler's foul resin 'spikes' do not stab you through the tank
+ var/mob/living/M = A
+ if(M && M.is_on_tank_hull())
+ return
trigger_trap(A)
return ..()
@@ -91,4 +95,8 @@
return
if(ishuman(A))
+ // mobs atop the tank shouldn't trigger tripwires
+ var/mob/living/M = A
+ if(M && M.is_on_tank_hull())
+ return
linked_trap.trigger_trap(A)
diff --git a/code/modules/cm_marines/equipment/mortar/mortars.dm b/code/modules/cm_marines/equipment/mortar/mortars.dm
index badb9cb5df49..567c180120b9 100644
--- a/code/modules/cm_marines/equipment/mortar/mortars.dm
+++ b/code/modules/cm_marines/equipment/mortar/mortars.dm
@@ -624,6 +624,12 @@
if(CEILING_IS_PROTECTED(area.ceiling, CEILING_PROTECTION_TIER_1) && is_ground_level(deploy_turf.z))
to_chat(user, SPAN_WARNING("You probably shouldn't deploy [src] indoors."))
return
+ if(isliving(user))
+ var/mob/living/L = user
+ if(L.is_on_tank_hull())
+ to_chat(user, SPAN_WARNING("You can't deploy [src] here!"))
+ return
+
user.visible_message(SPAN_NOTICE("[user] starts deploying [src]."),
SPAN_NOTICE("You start deploying [src]."))
playsound(deploy_turf, 'sound/items/Deconstruct.ogg', 25, 1)
diff --git a/code/modules/cm_marines/m2c.dm b/code/modules/cm_marines/m2c.dm
index 5d51c42d83e8..2ce367860d13 100644
--- a/code/modules/cm_marines/m2c.dm
+++ b/code/modules/cm_marines/m2c.dm
@@ -111,6 +111,11 @@
if(OT.density || !isturf(OT) || !OT.allow_construction)
to_chat(user, SPAN_WARNING("You can't set up \the [src] here."))
return FALSE
+ if(isliving(user))
+ var/mob/living/L = user
+ if(L.is_on_tank_hull())
+ to_chat(user, SPAN_WARNING("You can't set up \the [src] here."))
+ return FALSE
if(rotate_check.density)
to_chat(user, SPAN_WARNING("You can't set up \the [src] that way, there's a wall behind you!"))
return FALSE
diff --git a/code/modules/cm_tech/techs/marine/tier1/arc.dm b/code/modules/cm_tech/techs/marine/tier1/arc.dm
index 5a7b84a9aa0a..20497b7d431a 100644
--- a/code/modules/cm_tech/techs/marine/tier1/arc.dm
+++ b/code/modules/cm_tech/techs/marine/tier1/arc.dm
@@ -16,11 +16,14 @@
/datum/tech/arc/can_unlock(mob/unlocking_mob)
. = ..()
- var/obj/structure/machinery/cm_vending/gear/vehicle_crew/gearcomp = GLOB.VehicleGearConsole
-
- if(gearcomp.selected_vehicle == "TANK")
- to_chat(unlocking_mob, SPAN_WARNING ("A vehicle has already been selected for this operation."))
- return FALSE
+ // ARC can be bought even after the Tank has been selected.
+ // With all of the TANK's gear being taken at roundstart (hopefully), this means that the ARC ...
+ // ... can overwrite the vehicle ASRS' contents without much of an issue.
+
+ // var/obj/structure/machinery/cm_vending/gear/vehicle_crew/gearcomp = GLOB.VehicleGearConsole
+ // if(gearcomp.selected_vehicle == "TANK")
+ // to_chat(unlocking_mob, SPAN_WARNING ("A vehicle has already been selected for this operation."))
+ // return FALSE
return TRUE
diff --git a/code/modules/mob/living/carbon/carbon.dm b/code/modules/mob/living/carbon/carbon.dm
index 7ff77d825c5c..eecb9ee648cb 100644
--- a/code/modules/mob/living/carbon/carbon.dm
+++ b/code/modules/mob/living/carbon/carbon.dm
@@ -29,6 +29,15 @@
halimage = null
halbody = null
+// Same as the parent proc, but adds 1 additional check for climb_down
+/mob/living/carbon/_handle_tank_edge_move(NewLoc, direct)
+ var/obj/vehicle/multitile/tank/tank = get_tank_on_top_of()
+ var/result = ..()
+ if(result)
+ var/turf/edge = get_step(get_turf(src), direct)
+ if(tank.climb_down(src, edge))
+ return TRUE
+ return result
/mob/living/carbon/Move(NewLoc, direct)
. = ..()
@@ -539,6 +548,9 @@
var/location = get_turf(loc)
remove_traits(list(TRAIT_HAULED, TRAIT_NO_STRAY, TRAIT_FLOORED, TRAIT_IMMOBILIZED), TRAIT_SOURCE_XENO_HAUL)
pixel_y = 0
+ var/obj/vehicle/multitile/tank/TANK = null
+ if(hauling_xeno.is_on_tank_hull())
+ TANK = hauling_xeno.get_tank_on_top_of()
UnregisterSignal(src, list(COMSIG_ATTEMPT_MOB_PULL, COMSIG_LIVING_PREIGNITION, COMSIG_LIVING_FLAMER_CROSSED, COMSIG_LIVING_FLAMER_FLAMED))
UnregisterSignal(hauling_xeno, COMSIG_MOB_DEATH)
hauling_xeno = null
@@ -552,7 +564,8 @@
object.Crossed(src)
next_haul_resist = 0
SEND_SIGNAL(src, COMSIG_MOB_UNHAULED)
-
+ if(TANK)
+ TANK.mark_on_top(src)
/mob/living/carbon/proc/extinguish_mob(mob/living/carbon/C)
adjust_fire_stacks(-5, min_stacks = 0)
diff --git a/code/modules/mob/living/carbon/carbon_debug_verbs.dm b/code/modules/mob/living/carbon/carbon_debug_verbs.dm
new file mode 100644
index 000000000000..84d26ce7c87d
--- /dev/null
+++ b/code/modules/mob/living/carbon/carbon_debug_verbs.dm
@@ -0,0 +1,195 @@
+/// Shared lookup for the debug verbs below: finds the tank_turret on the tank this mob is currently seated in, if any. Reports why to src and returns null on failure.
+/mob/living/carbon/proc/get_seated_tank_turret()
+ if(!istype(buckled, /obj/structure/bed/chair/comfy/vehicle))
+ to_chat(src, SPAN_WARNING("You're not seated in a vehicle."))
+ return null
+
+ var/obj/structure/bed/chair/comfy/vehicle/seat_chair = buckled
+ var/obj/vehicle/multitile/tank/tank_vehicle = seat_chair.vehicle
+ if(!istype(tank_vehicle))
+ to_chat(src, SPAN_WARNING("You're not seated in a tank."))
+ return null
+
+ var/obj/item/hardpoint/holder/tank_turret/turret
+ for(var/obj/item/hardpoint/holder/tank_turret/TT in tank_vehicle.hardpoints)
+ turret = TT
+ break
+ if(!turret)
+ to_chat(src, SPAN_WARNING("This tank has no turret installed."))
+ return null
+
+ return turret
+
+// Debug tool for live-tuning tank turret weapon sprite alignment
+/mob/living/carbon/verb/set_tank_weapon_rotation_pivot()
+ set name = "Set Tank Weapon Rotation Pivot"
+ set category = "Debug"
+ set desc = "Live-tunes the rotation_pivot of a weapon mounted on the turret of the tank you're seated in."
+
+ if(!check_rights(R_DEBUG))
+ return
+
+ var/obj/item/hardpoint/holder/tank_turret/turret = get_seated_tank_turret()
+ if(!turret)
+ return
+
+ if(!LAZYLEN(turret.hardpoints))
+ to_chat(src, SPAN_WARNING("This turret has no weapons mounted."))
+ return
+
+ var/datum/tank_pivot_tuner/tuner = new
+ tuner.turret = turret
+ for(var/obj/item/hardpoint/H in turret.hardpoints)
+ tuner.weapon = H
+ break
+ tuner.tgui_interact(src)
+
+/datum/tank_pivot_tuner
+ var/obj/item/hardpoint/holder/tank_turret/turret
+ var/obj/item/hardpoint/weapon
+
+/datum/tank_pivot_tuner/ui_state(mob/user)
+ return GLOB.always_state
+
+/datum/tank_pivot_tuner/ui_close(mob/user)
+ . = ..()
+ qdel(src)
+
+/datum/tank_pivot_tuner/tgui_interact(mob/user, datum/tgui/ui)
+ ui = SStgui.try_update_ui(user, src, ui)
+ if(!ui)
+ ui = new(user, src, "TankPivotTuner")
+ ui.open()
+ ui.set_autoupdate(TRUE)
+
+/// The nearest of the 8 cardinal/diagonal dir constants to the turret's current continuous facing
+/datum/tank_pivot_tuner/proc/get_turret_octant()
+ return angle_to_dir(turret.current_angle)
+
+/datum/tank_pivot_tuner/ui_data(mob/user)
+ . = list()
+ if(QDELETED(turret))
+ return
+
+ var/turret_octant = get_turret_octant()
+ .["dir_name"] = dir2text(turret_octant)
+
+ var/dir_key = "[turret_octant]"
+ var/list/weapon_list = list()
+ for(var/obj/item/hardpoint/H in turret.hardpoints)
+ var/list/pivot = LAZYACCESS(H.rotation_pivot, dir_key) || list(0, 0)
+ weapon_list += list(list(
+ "ref" = "\ref[H]",
+ "name" = H.name,
+ "pivot_x" = pivot[1],
+ "pivot_y" = pivot[2],
+ "selected" = (H == weapon),
+ ))
+ .["weapons"] = weapon_list
+
+ if(QDELETED(weapon))
+ return
+ var/list/selected_pivot = LAZYACCESS(weapon.rotation_pivot, dir_key) || list(0, 0)
+ .["tuning_label"] = "turret facing: [dir2text(turret_octant)]"
+ .["selected_name"] = weapon.name
+ .["pivot_x"] = selected_pivot[1]
+ .["pivot_y"] = selected_pivot[2]
+
+ var/list/selected_offset = LAZYACCESS(weapon.px_offsets, dir_key) || list(0, 0)
+ .["offset_x"] = selected_offset[1]
+ .["offset_y"] = selected_offset[2]
+
+ .["is_gimballed"] = weapon.self_gimballed
+ if(weapon.self_gimballed)
+ var/list/selected_gimbal_pivot = LAZYACCESS(weapon.gimbal_pivot, dir_key) || list(0, 0)
+ .["gimbal_pivot_x"] = selected_gimbal_pivot[1]
+ .["gimbal_pivot_y"] = selected_gimbal_pivot[2]
+
+/datum/tank_pivot_tuner/ui_act(action, list/params, datum/tgui/ui)
+ . = ..()
+ if(.)
+ return
+ if(QDELETED(turret))
+ return
+
+ switch(action)
+ if("select_weapon")
+ var/obj/item/hardpoint/H = locate(params["ref"]) in turret.hardpoints
+ if(!H)
+ return
+ weapon = H
+ . = TRUE
+ if("adjust")
+ if(QDELETED(weapon))
+ return
+ var/axis = params["axis"]
+ var/delta = params["delta"]
+ var/target = params["target"]
+ if(!delta || !(axis == "x" || axis == "y") || !(target == "rotation" || target == "gimbal" || target == "offset"))
+ return
+ if(target == "gimbal" && !weapon.self_gimballed)
+ return
+
+ var/list/pivot_list
+ switch(target)
+ if("gimbal")
+ pivot_list = weapon.gimbal_pivot
+ if("offset")
+ pivot_list = weapon.px_offsets
+ else
+ pivot_list = weapon.rotation_pivot
+ var/dir_key = "[get_turret_octant()]"
+ var/list/pivot = LAZYACCESS(pivot_list, dir_key) || list(0, 0)
+ var/list/new_pivot = list(pivot[1], pivot[2])
+ if(axis == "x")
+ new_pivot[1] = clamp(pivot[1] + delta, -200, 200)
+ else
+ new_pivot[2] = clamp(pivot[2] + delta, -200, 200)
+ LAZYSET(pivot_list, dir_key, new_pivot)
+ switch(target)
+ if("gimbal")
+ weapon.gimbal_pivot = pivot_list
+ if("offset")
+ weapon.px_offsets = pivot_list
+ else
+ weapon.rotation_pivot = pivot_list
+
+ turret.owner.update_icon()
+ . = TRUE
+
+// instantly swaps whichever primary/secondary weapon is mounted on the tank turret
+
+/mob/living/carbon/verb/cycle_tank_weapon_hardpoint()
+ set name = "Cycle Tank Weapon Hardpoint"
+ set category = "Debug"
+ set desc = "Swaps the turret's primary/secondary weapon (whichever slot the next type in the cycle belongs to) to the next available type."
+
+ if(!check_rights(R_DEBUG))
+ return
+
+ var/obj/item/hardpoint/holder/tank_turret/turret = get_seated_tank_turret()
+ if(!turret)
+ return
+
+ if(!LAZYLEN(turret.accepted_hardpoints))
+ to_chat(src, SPAN_WARNING("This turret doesn't accept any hardpoints."))
+ return
+
+ turret.debug_cycle_index = (turret.debug_cycle_index % length(turret.accepted_hardpoints)) + 1
+ var/next_type = turret.accepted_hardpoints[turret.debug_cycle_index]
+
+ var/obj/item/hardpoint/next_hardpoint = new next_type()
+
+ var/obj/item/hardpoint/existing
+ for(var/obj/item/hardpoint/H in turret.hardpoints)
+ if(H.slot == next_hardpoint.slot)
+ existing = H
+ break
+ if(existing)
+ turret.remove_hardpoint(existing, get_turf(turret))
+ qdel(existing)
+
+ turret.add_hardpoint(next_hardpoint)
+ turret.owner.update_icon()
+
+ to_chat(src, SPAN_NOTICE("Installed [next_hardpoint.name] on the turret ([turret.debug_cycle_index]/[length(turret.accepted_hardpoints)])."))
diff --git a/code/modules/mob/living/carbon/xenomorph/XenoProcs.dm b/code/modules/mob/living/carbon/xenomorph/XenoProcs.dm
index a8d19062ff71..a1a374ea5d12 100644
--- a/code/modules/mob/living/carbon/xenomorph/XenoProcs.dm
+++ b/code/modules/mob/living/carbon/xenomorph/XenoProcs.dm
@@ -233,11 +233,24 @@
return
. = ..()
+// this proc only serves to fix a visual bug where hauled mobs get layered below the tank.
+// this is a rare edge case, because xenos won't usually be hauling mobs atop the tank, but just for completeness...
+/mob/living/carbon/xenomorph/proc/check_on_tank_hauled_mob(mob/victim)
+ var/obj/vehicle/multitile/tank/tank = null
+ if(src.is_on_tank_hull())
+ tank = src.get_tank_on_top_of()
+ tank._apply_rider_visuals(victim)
+ else
+ victim.layer = initial(victim.layer)
+ victim.plane = initial(victim.plane)
+ victim.pixel_y = initial(victim.pixel_y)
+
/mob/living/carbon/xenomorph/Move(NewLoc, direct)
. = ..()
var/mob/user = hauled_mob?.resolve()
if(user)
user.forceMove(loc)
+ check_on_tank_hauled_mob(user)
/mob/living/carbon/xenomorph/forceMove(atom/destination)
. = ..()
@@ -245,8 +258,10 @@
if(user)
if(!isturf(destination))
user.forceMove(src)
+ check_on_tank_hauled_mob(user)
else
user.forceMove(loc)
+ check_on_tank_hauled_mob(user)
/mob/living/carbon/xenomorph/relaymove(mob/user, direction)
. = ..()
@@ -354,6 +369,14 @@
ADD_TRAIT(src, TRAIT_IMMOBILIZED, TRAIT_SOURCE_ABILITY("Pounce"))
pounceAction.freeze_timer_id = addtimer(CALLBACK(src, PROC_REF(unfreeze_pounce)), pounceAction.freeze_time, TIMER_STOPPABLE)
pounceAction.additional_effects(carbon_mob)
+ var/obj/vehicle/multitile/tank/pounced_tank = pounced_mob.get_tank_on_top_of()
+ if(pounced_tank)
+ src.forceMove(get_turf(pounced_mob))
+ pounced_tank.mark_on_top(src)
+ else
+ var/obj/vehicle/multitile/tank/tank = src.get_tank_on_top_of()
+ if(tank)
+ tank.clear_on_top(src)
if(pounceAction.slash)
carbon_mob.attack_alien(src, pounceAction.slash_bonus_damage)
@@ -374,6 +397,21 @@
obj_launch_collision(O)
return
+ // Castes that can pounce can use this ability to instantly climb ontop or down a tank with no delay.
+ var/obj/vehicle/multitile/tank/T = null
+ if(istype(O, /obj/vehicle/multitile/tank))
+ T = O
+ if(T)
+ var/turf/current = get_turf(src)
+ var/turf/facing = get_step(current, dir)
+ if(facing && (facing in T.locs))
+ forceMove(facing)
+ T.mark_on_top(src)
+ else
+ var/obj/vehicle/multitile/tank/current_tank = src.get_tank_on_top_of()
+ if(current_tank && !(locate(/obj/vehicle/multitile/tank) in get_turf(src)))
+ current_tank.clear_on_top(src) // if we're not atop the tank still, clear us from it.
+
if (pounceAction.should_destroy_objects)
if(istype(O, /obj/structure/surface/table) || istype(O, /obj/structure/surface/rack) || istype(O, /obj/structure/window_frame))
var/obj/structure/S = O
diff --git a/code/modules/mob/living/carbon/xenomorph/abilities/ability_helper_procs.dm b/code/modules/mob/living/carbon/xenomorph/abilities/ability_helper_procs.dm
index 4d008f6dc2b1..d49e9f17e7b6 100644
--- a/code/modules/mob/living/carbon/xenomorph/abilities/ability_helper_procs.dm
+++ b/code/modules/mob/living/carbon/xenomorph/abilities/ability_helper_procs.dm
@@ -301,7 +301,8 @@
var/atom/movable/temp = new spray_path()
var/atom/movable/blocker = LinkBlocked(temp, prev_turf, turf)
qdel(temp)
- if(blocker)
+ // despite the tank being a blocker, acid sprays should still go through it to hit marines ontop.
+ if(blocker && !locate(/obj/vehicle/multitile/tank) in turf.contents)
blocker.acid_spray_act(src)
break
diff --git a/code/modules/mob/living/carbon/xenomorph/abilities/general_abilities.dm b/code/modules/mob/living/carbon/xenomorph/abilities/general_abilities.dm
index 04fa1f7fe9b9..c561e161de0d 100644
--- a/code/modules/mob/living/carbon/xenomorph/abilities/general_abilities.dm
+++ b/code/modules/mob/living/carbon/xenomorph/abilities/general_abilities.dm
@@ -408,12 +408,18 @@
/datum/action/xeno_action/onclick/xenohide/can_use_action()
var/mob/living/carbon/xenomorph/xeno = owner
- if(xeno && !xeno.buckled && !xeno.is_mob_incapacitated() && !LAZYLEN(xeno.buckled_mobs))
- if(!(SEND_SIGNAL(xeno, COMSIG_LIVING_SHIMMY_LAYER) & COMSIG_LIVING_SHIMMY_LAYER_CANCEL))
- return TRUE
-
+ if(xeno)
+ if(xeno.get_tank_on_top_of()) // Prevents cheesy layering underneath the tank.
+ return FALSE
+ if(!xeno.buckled && !xeno.is_mob_incapacitated() && !LAZYLEN(xeno.buckled_mobs))
+ if(!(SEND_SIGNAL(xeno, COMSIG_LIVING_SHIMMY_LAYER) & COMSIG_LIVING_SHIMMY_LAYER_CANCEL))
+ return TRUE
/// remove hide and apply modified attack cooldown
/datum/action/xeno_action/onclick/xenohide/proc/post_attack()
+ remove_hide_status()
+ apply_cooldown(4) //2 second cooldown after attacking
+
+/datum/action/xeno_action/onclick/xenohide/proc/remove_hide_status()
var/mob/living/carbon/xenomorph/xeno = owner
UnregisterSignal(xeno, COMSIG_MOB_STATCHANGE)
if(xeno.layer == XENO_HIDING_LAYER)
@@ -421,7 +427,6 @@
button.icon_state = "template_xeno"
xeno.update_wounds()
xeno.update_layer()
- apply_cooldown(4) //2 second cooldown after attacking
/datum/action/xeno_action/onclick/xenohide/give_to(mob/living/living_mob)
. = ..()
diff --git a/code/modules/mob/living/carbon/xenomorph/abilities/general_powers.dm b/code/modules/mob/living/carbon/xenomorph/abilities/general_powers.dm
index 08a0dcebc2bf..e431482ae7fc 100644
--- a/code/modules/mob/living/carbon/xenomorph/abilities/general_powers.dm
+++ b/code/modules/mob/living/carbon/xenomorph/abilities/general_powers.dm
@@ -1126,19 +1126,31 @@
if(distance > stab_range)
return FALSE
+ // allows tailstabs into mobs riding atop the tank
+ var/obj/vehicle/multitile/tank/skip_tank = null
+ if(ismob(targetted_atom))
+ var/mob/living/target_mob = targetted_atom
+ if(istype(target_mob))
+ skip_tank = target_mob.get_tank_on_top_of()
+
var/list/turf/path = get_line(stabbing_xeno, targetted_atom, include_start_atom = FALSE)
for(var/turf/path_turf as anything in path)
if(path_turf.density)
to_chat(stabbing_xeno, SPAN_WARNING("There's something blocking our strike!"))
return FALSE
for(var/obj/path_contents in path_turf.contents)
+ if(skip_tank && path_contents == skip_tank)
+ continue
if(path_contents != targetted_atom && path_contents.density && !path_contents.throwpass)
to_chat(stabbing_xeno, SPAN_WARNING("There's something blocking our strike!"))
return FALSE
var/atom/barrier = path_turf.handle_barriers(stabbing_xeno, null, (PASS_MOB_THRU_XENO|PASS_OVER_THROW_MOB|PASS_TYPE_CRAWLER))
if(barrier != path_turf)
+ if(skip_tank && barrier == skip_tank)
+ continue
var/tail_stab_cooldown_multiplier = barrier.handle_tail_stab(stabbing_xeno, blunt_stab)
+
if(!tail_stab_cooldown_multiplier)
to_chat(stabbing_xeno, SPAN_WARNING("There's something blocking our strike!"))
else
diff --git a/code/modules/mob/living/living.dm b/code/modules/mob/living/living.dm
index 9ef0a24c2eab..558e7f9e8739 100644
--- a/code/modules/mob/living/living.dm
+++ b/code/modules/mob/living/living.dm
@@ -156,6 +156,76 @@
return
+/**
+ * _handle_tank_edge_move blocks consensual movement outside of tank tiles, unless you invoke climb_down()
+ *
+ * This proc makes it so that you cannot instantly dismount the tank, nor accidentally walk out of it.
+ * Marines and xenos alike become unable to dismount unless:
+ * - They are dismounted forcefully (e.g, by an explosion, grab, et-cetera)
+ * > Marines eat a 3 second stun if this happens. Xenos currently do not get punished.
+ * - They use some kind of skill to dismount (e.g pouncing outside)
+ * - They wait 1 second until climb_down finishes.
+ *
+ * OOPS! As a fix for LINTERS, climb_down() is called on the proc in carbon.dm
+ *
+ * Arguments:
+ * * NewLoc - Wherever we're trying to step down towards.
+ * * Direct - Direction we're facing
+ *
+ * Returns:
+ * * TRUE - When dismounting is impossible (blocked turf) or consensual (climb_down)
+ * * FALSE - When the mob dismounts improperly, and must be punished.
+ */
+/mob/living/proc/_handle_tank_edge_move(NewLoc, direct)
+ var/obj/vehicle/multitile/tank/tank = get_tank_on_top_of()
+
+ if(!tank)
+ return FALSE
+
+ var/turf/here = get_turf(src)
+ if(!(here in tank.locs))
+ tank.clear_on_top(src)
+ return FALSE
+
+ // only care about steps that would leave the hull.
+ if(isturf(NewLoc) && !(NewLoc in tank.locs))
+ if(!(flags_atom & DIRLOCK))
+ setDir(direct)
+ SEND_SIGNAL(src, COMSIG_MOB_MOVE_OR_LOOK, TRUE, direct, direct)
+ if(pulledby || throwing)
+ return FALSE
+ var/turf/edge = get_step(here, direct)
+ if(!edge || edge.z != z || (edge in tank.locs))
+ return FALSE
+ if(tank._blocked_except_mobs(edge))
+ return TRUE
+ // special interaction for simple_animals and (edge case) silicons...
+ // ...to prevent SpacemanDMM from sleeping and still allow dismounts.
+ if(!iscarbon(src))
+ tank.clear_on_top(src)
+ src.forceMove(NewLoc)
+ return TRUE
+ return TRUE
+
+ return FALSE
+
+/**
+ * is_on_tank_hull checks if a mob has a valid tank rider component.
+ *
+ * This proc checks the tank this mob's tank_rider component (if any) points to.
+ * If there is a valid tank, it means this mob is atop a tank tile.
+ *
+ * Returns:
+ * * TRUE - If there is a valid tile under the tank, and if mob and tank are on the same Z-level.
+ * * FALSE - If the mob has no tank_rider component, if the Z-levels of the mob and Tank mistmach, if the tile Tu is invalid,
+ * * ...Or if the tile Tu is not, in fact, underneath a tank.
+ */
+/mob/living/proc/is_on_tank_hull()
+ var/obj/vehicle/multitile/tank/tank = get_tank_on_top_of()
+ if(!tank || z != tank.z) return FALSE
+ var/turf/Tu = get_turf(src)
+ return Tu && (Tu in tank.locs)
+
/mob/living/Move(NewLoc, direct)
if(lying_angle != 0)
lying_angle_on_movement(direct)
@@ -165,6 +235,20 @@
else
return FALSE
+ if(is_on_tank_hull())
+ if(_handle_tank_edge_move(NewLoc, direct))
+ return FALSE
+ if(isturf(NewLoc))
+ var/obj/vehicle/multitile/tank/tank = get_tank_on_top_of()
+ if(tank && !(NewLoc in tank.locs))
+ if(ishuman(src))
+ var/mob/living/carbon/human/H = src
+ H.apply_effect(1.5, WEAKEN)
+ H.apply_effect(3, SUPERSLOW)
+ H.apply_effect(5, SLOW)
+ playsound(src, "punch", 25, TRUE)
+ tank.clear_on_top(src)
+
var/atom/movable/pullee = pulling
if(pullee && get_dist(src, pullee) > 1) //Is the pullee adjacent?
if(!pullee.clone || (pullee.clone && get_dist(src, pullee.clone) > 2)) //Be lenient with the close
@@ -389,6 +473,23 @@
now_pushing = FALSE
return
+ // tank-related collision rules
+ var/src_on_tank = is_on_tank_hull()
+ var/target_on_tank = living_mob.is_on_tank_hull()
+
+ if(src_on_tank != target_on_tank)
+ now_pushing = FALSE
+ return
+
+ if(src_on_tank && target_on_tank)
+ var/obj/vehicle/multitile/tank/tank = get_tank_on_top_of()
+ if(tank == living_mob.get_tank_on_top_of())
+ var/turf/destination = get_step(living_mob.loc, dir)
+ if(destination && !(destination in tank.locs))
+ if(src.a_intent != INTENT_HELP || living_mob.a_intent != INTENT_HELP)
+ now_pushing = FALSE
+ return
+
if(!living_mob.buckled && !living_mob.anchored)
var/mob_swap
//the puller can always swap with its victim if on grab intent
@@ -632,11 +733,20 @@
/mob/living/proc/update_layer()
//so mob lying always appear behind standing mobs, but dead ones appear behind living ones
if(pulledby && pulledby.grab_level == GRAB_CARRY)
- layer = ABOVE_MOB_LAYER
+ if(is_on_tank_hull())
+ layer = TANK_ABOVE_RIDER_LAYER
+ else
+ layer = ABOVE_MOB_LAYER
else if (body_position == LYING_DOWN && stat == DEAD)
- layer = LYING_DEAD_MOB_LAYER // Dead mobs should layer under living ones
+ if(is_on_tank_hull())
+ layer = TANK_BELOW_RIDER_LAYER
+ else
+ layer = LYING_DEAD_MOB_LAYER // Dead mobs should layer under living ones
else if(body_position == LYING_DOWN && layer == initial(layer)) //to avoid things like hiding larvas. //i have no idea what this means
- layer = LYING_LIVING_MOB_LAYER
+ if(is_on_tank_hull())
+ layer = TANK_LYING_RIDER_LAYER
+ else
+ layer = LYING_LIVING_MOB_LAYER
/// Called when mob changes from a standing position into a prone while lacking the ability to stand up at the moment.
/mob/living/proc/on_fall()
diff --git a/code/modules/mob/living/living_defense.dm b/code/modules/mob/living/living_defense.dm
index 4f8c1eccbbf9..9786b3433ad9 100644
--- a/code/modules/mob/living/living_defense.dm
+++ b/code/modules/mob/living/living_defense.dm
@@ -231,9 +231,14 @@
clear_fullscreen("weather")
/mob/living/handle_flamer_fire(obj/flamer_fire/fire, damage, delta_time)
+ if(is_on_tank_hull())
+ return
. = ..()
fire.set_on_fire(src)
/mob/living/handle_flamer_fire_crossed(obj/flamer_fire/fire)
+ if(is_on_tank_hull())
+ return
. = ..()
fire.set_on_fire(src)
+
diff --git a/code/modules/mob/mob_grab.dm b/code/modules/mob/mob_grab.dm
index 097fba18a8a7..872d8aa12c22 100644
--- a/code/modules/mob/mob_grab.dm
+++ b/code/modules/mob/mob_grab.dm
@@ -116,6 +116,25 @@
playsound(loc, 'sound/weapons/thudswoosh.ogg', 25, 1, 7)
user.visible_message(SPAN_WARNING("[user] holds [victim] by the neck and starts choking them!"), null, null, 5)
msg_admin_attack("[key_name(user)] started to choke [key_name(victim)] at [get_area_name(victim)]", victim.loc.x, victim.loc.y, victim.loc.z)
+
+ // allows pulling ppl onto the tank if you are already ontop of it..
+ // the idea is to allow the tank to charge into an incend OB and for corpsmen ontop to recover people.
+ var/obj/vehicle/multitile/tank/tank = user.get_tank_on_top_of()
+ if(tank && !victim.get_tank_on_top_of())
+ if(istype(tank))
+ var/turf/victim_turf = get_turf(victim)
+ var/adjacent_to_tank = FALSE
+ for(var/turf/tank_turf in tank.locs)
+ if(get_dist(victim_turf, tank_turf) <= 1 && victim_turf != tank_turf)
+ adjacent_to_tank = TRUE
+ break
+
+ if(adjacent_to_tank)
+ victim.forceMove(user.loc)
+ tank.mark_on_top(victim)
+ victim.update_transform(TRUE)
+ return
+
victim.Move(user.loc, get_dir(victim.loc, user.loc))
victim.update_transform(TRUE)
diff --git a/code/modules/mob/mob_verbs.dm b/code/modules/mob/mob_verbs.dm
index c42b79562c6a..e3e112971512 100644
--- a/code/modules/mob/mob_verbs.dm
+++ b/code/modules/mob/mob_verbs.dm
@@ -253,6 +253,12 @@
if(observed_atom)
QDEL_NULL(observed_atom)
+ if(buckled && istype(buckled, /obj/structure/bed/chair/comfy/vehicle))
+ var/obj/structure/bed/chair/comfy/vehicle/vehicle_seat = buckled
+ if(vehicle_seat.vehicle && (vehicle_seat.seat == VEHICLE_DRIVER || vehicle_seat.seat == VEHICLE_GUNNER))
+ if(client)
+ client.change_view(8, vehicle_seat.vehicle)
+ vehicle_seat.vehicle.set_seated_mob(vehicle_seat.seat, src)
return
if(!client)
@@ -271,6 +277,15 @@
to_chat(src, SPAN_WARNING("We cannot look up here, we are burrowed!"))
return
+ var/turf/vehicle_turf = null
+ if(buckled && istype(buckled, /obj/structure/bed/chair/comfy/vehicle))
+ var/obj/structure/bed/chair/comfy/vehicle/vehicle_seat = buckled
+ if(vehicle_seat.vehicle && (vehicle_seat.seat == VEHICLE_DRIVER || vehicle_seat.seat == VEHICLE_GUNNER))
+ vehicle_turf = get_turf(vehicle_seat.vehicle)
+ else
+ to_chat(src, SPAN_WARNING("You cannot look up from this position."))
+ return
+
if(!isturf(loc))
to_chat(src, SPAN_WARNING("You cannot look up here."))
return
@@ -280,6 +295,10 @@
to_chat(src, SPAN_WARNING("You cannot look up here."))
return
+ // this is probably a bit hacky.
+ if(!(vehicle_turf == null))
+ above = locate(vehicle_turf.x, vehicle_turf.y, vehicle_turf.z+1)
+
if(!istransparentturf(above))
to_chat(src, SPAN_WARNING("You cannot look up here."))
return
diff --git a/code/modules/movement/launching/launching.dm b/code/modules/movement/launching/launching.dm
index b5c177371b9e..a1ceb2a6b80f 100644
--- a/code/modules/movement/launching/launching.dm
+++ b/code/modules/movement/launching/launching.dm
@@ -211,6 +211,8 @@
break
if (LM.dist >= LM.range)
break
+ if(locate(/obj/vehicle/multitile/tank) in T)
+ layer = TANK_RIDER_OBJ_LAYER // prevents thrown items from being layered under a vehicle during throw.
if (!Move(T)) // If this returns FALSE, then a collision happened
break
last_loc = loc
@@ -219,10 +221,23 @@
sleep(delay)
//done throwing, either because it hit something or it finished moving
+ // prevents xeno pounces from layering under the tank, as well as other species with pounces.
+ if (isliving(src))
+ var/mob/living/living_mob = src
+ living_mob.update_layer()
+ var/obj/vehicle/multitile/tank/rider_tank = living_mob.get_tank_on_top_of()
+ if(rider_tank)
+ if(!(locate(/obj/vehicle/multitile/tank) in get_turf(living_mob)))
+ rider_tank.clear_on_top(living_mob) // if we're not atop the tank still, clear us from it.
+ else
+ src.layer = initial(src.layer)
if ((isobj(src) || ismob(src)) && throwing && !early_exit)
var/turf/T = get_turf(src)
if(!istype(T))
return
+ var/obj/vehicle/multitile/tank/tank = locate(/obj/vehicle/multitile/tank)
+ if(tank && (tank in T))
+ tank.obj_mark_on_top(src)
var/atom/hit_atom = ismob(LM.target) ? null : T // TODO, just check for LM.target, the ismob is to prevent funky behavior with grenades 'n crates
if(!hit_atom)
for(var/atom/A in T)
diff --git a/code/modules/projectiles/gun.dm b/code/modules/projectiles/gun.dm
index d3c2a715f3bb..89f31cd8a0bd 100644
--- a/code/modules/projectiles/gun.dm
+++ b/code/modules/projectiles/gun.dm
@@ -1278,6 +1278,8 @@ and you're good to go.
var/atom/original_target = target //This is for burst mode, in case the target changes per scatter chance in between fired bullets.
+ var/obj/vehicle/multitile/tank/rider_tank = user.get_tank_on_top_of() // Used to calculate inaccuracy when firing atop a moving tank.
+
if(loc != user || (flags_gun_features & GUN_WIELDED_FIRING_ONLY && !(flags_item & WIELDED)))
return TRUE
@@ -1290,7 +1292,7 @@ and you're good to go.
var/original_scatter = projectile_to_fire.scatter
var/original_accuracy = projectile_to_fire.accuracy
- apply_bullet_scatter(projectile_to_fire, user, reflex, dual_wield) //User can be passed as null.
+ apply_bullet_scatter(projectile_to_fire, user, reflex, dual_wield, rider_tank) //User can be passed as null.
curloc = get_turf(user)
if(QDELETED(original_target)) //If the target's destroyed, shoot at where it was last.
@@ -1914,13 +1916,23 @@ not all weapons use normal magazines etc. load_into_chamber() itself is designed
return TRUE
//This proc calculates scatter and accuracy
-/obj/item/weapon/gun/proc/apply_bullet_scatter(obj/projectile/projectile_to_fire, mob/user, reflex = 0, dual_wield = 0)
+/obj/item/weapon/gun/proc/apply_bullet_scatter(obj/projectile/projectile_to_fire, mob/user, reflex = 0, dual_wield = 0, obj/vehicle/multitile/tank/rider_tank = null)
var/gun_accuracy_mult = accuracy_mult_unwielded
var/gun_scatter = scatter_unwielded
if(flags_item & WIELDED || flags_gun_features & GUN_ONE_HAND_WIELDED)
gun_accuracy_mult = accuracy_mult
gun_scatter = scatter
+ // increases scatter to penalize marines firing atop a moving tank instead of outright restricting it
+ // only for wielded firearms.
+ if(rider_tank)
+ var/obj/vehicle/multitile/tank/TANK = rider_tank
+ if(world.time < TANK.on_top_mobs_shooting_inaccuracy_time)
+ if(world.time % 3)
+ to_chat(gun_user, SPAN_DANGER("You struggle to keep your aim centered as the [TANK] moves!"))
+ gun_accuracy_mult = max(0.1, gun_accuracy_mult * 0.4) // 60% accuracy loss
+ gun_scatter += SCATTER_AMOUNT_TIER_4
+
else if(user && world.time - user.l_move_time < 5) //moved during the last half second
//accuracy and scatter penalty if the user fires unwielded right after moving
gun_accuracy_mult = max(0.1, gun_accuracy_mult - max(0,movement_onehanded_acc_penalty_mult * HIT_ACCURACY_MULT_TIER_3))
diff --git a/code/modules/projectiles/homing_projectile_component.dm b/code/modules/projectiles/homing_projectile_component.dm
index 8fd36fd67f31..903bff54deab 100644
--- a/code/modules/projectiles/homing_projectile_component.dm
+++ b/code/modules/projectiles/homing_projectile_component.dm
@@ -22,17 +22,30 @@
src.homing_target = homing_target
/datum/component/homing_projectile/RegisterWithParent()
+ RegisterSignal(parent, COMSIG_BULLET_STEP, PROC_REF(step_retarget))
RegisterSignal(parent, COMSIG_BULLET_TERMINAL, PROC_REF(terminal_retarget))
/datum/component/homing_projectile/UnregisterFromParent()
- UnregisterSignal(parent, COMSIG_BULLET_TERMINAL)
+ UnregisterSignal(parent, list(COMSIG_BULLET_STEP, COMSIG_BULLET_TERMINAL))
homing_target = null
/datum/component/homing_projectile/Destroy()
homing_target = null
return ..()
+// Continuously re-aims toward the target's live position every tick of flight
+/datum/component/homing_projectile/proc/step_retarget()
+ SIGNAL_HANDLER
+ if(QDELETED(homing_target))
+ return
+ var/obj/projectile/projectile = parent
+ var/turf/homing_turf = get_turf(homing_target)
+ if(!homing_turf || homing_turf == get_turf(projectile))
+ return
+ projectile.retarget(homing_turf, keep_angle = FALSE)
+
/datum/component/homing_projectile/proc/terminal_retarget()
+ SIGNAL_HANDLER
var/obj/projectile/projectile = parent
var/turf/homing_turf = get_turf(homing_target)
projectile.speed *= 2 // Double speed to ensure hitting next tick despite eventual movement
diff --git a/code/modules/projectiles/projectile.dm b/code/modules/projectiles/projectile.dm
index 83ecb9b43ce1..c41576dd938c 100644
--- a/code/modules/projectiles/projectile.dm
+++ b/code/modules/projectiles/projectile.dm
@@ -91,6 +91,9 @@
var/damage_boosted = 0
var/last_damage_mult = 1
+ /// Tracks if the projectile is inside obj/vehicle/multitile/tank
+ var/obj/vehicle/multitile/tank/inside_tank = null
+
/obj/projectile/Initialize(mapload, datum/cause_data/cause_data)
. = ..()
path = list()
@@ -358,6 +361,11 @@
SHOULD_NOT_SLEEP(TRUE)
PRIVATE_PROC(TRUE)
var/turf/current_turf = get_turf(src)
+
+ // Lets homing-style components continuously re-aim toward their live ttarget
+ // keeps the shot actually curving to follow a moving target over the whole flight.
+ SEND_SIGNAL(src, COMSIG_BULLET_STEP)
+
var/turf/next_turf = popleft(path)
// Terminal projectiles (about to hit) are handled firer for retarget logic
@@ -408,6 +416,7 @@
path.Cut(1, 2) // remove the turf we're already on
var/atom/source = keep_angle ? original : current_turf
update_angle(source, new_target)
+ target_turf = get_turf(new_target)
/obj/projectile/proc/scan_a_turf(turf/turf, proj_dir)
. = TRUE // Sleep safeguard: stop the bullet
@@ -419,6 +428,26 @@
if(!istype(turf))
return FALSE
+ // Checkc if we're inside a tank and trying to exit
+ if(inside_tank)
+ var/tank_found = FALSE
+ for(var/obj/obj in turf)
+ var/obj/vehicle/multitile/tank/M = _owning_tank_of(obj)
+ if(M == inside_tank || obj == inside_tank)
+ tank_found = TRUE
+ break
+
+ // hits the tank if there are no tank parts inside the turf
+ if(!tank_found)
+ // unless it was shot by a mob atop the tank.
+ if(isliving(firer))
+ var/mob/living/M = firer
+ if(M.is_on_tank_hull())
+ return FALSE
+ ammo.on_hit_obj(inside_tank, src)
+ inside_tank.bullet_act(src)
+ return TRUE
+
if(turf.density) // Handle wall hit
if(turf in permutated)
return FALSE
@@ -478,6 +507,21 @@
return FALSE
permutated |= obj
+ var/obj/vehicle/multitile/tank/M = _owning_tank_of(obj)
+ if(!M)
+ if(istype(obj, /obj/vehicle/multitile/tank))
+ M = obj
+
+ // this block allows projectiles fired from outside the tank to travel inside it.
+ if(M)
+ if(!inside_tank)
+ inside_tank = M
+ return FALSE
+ else if(inside_tank == M)
+ return FALSE
+ else // if inside_tank exists but is not M, it means we're firing on another tank, so, we return true.
+ return TRUE
+
var/hit_chance = obj.get_projectile_hit_boolean(src)
if(hit_chance) // Calculated from combination of both ammo accuracy and gun accuracy
SEND_SIGNAL(src, COMSIG_BULLET_PRE_HANDLE_OBJ, obj)
@@ -1349,6 +1393,12 @@
if(dx == -1 || dx == 1)
return TRUE
+// helper proc to see if we are about to hit a tank
+/obj/projectile/proc/_owning_tank_of(atom/A)
+ if(istype(A, /obj/vehicle/multitile/tank))
+ return A
+ return null
+
/obj/projectile/vulture
accuracy_range_falloff = 10
/// The odds of hitting a xeno in less than your gun's range. Doesn't apply to humans.
diff --git a/code/modules/vehicles/hardpoints/hardpoint.dm b/code/modules/vehicles/hardpoints/hardpoint.dm
index a75fb9353b43..05e411ac7685 100644
--- a/code/modules/vehicles/hardpoints/hardpoint.dm
+++ b/code/modules/vehicles/hardpoints/hardpoint.dm
@@ -33,6 +33,11 @@
/// List of pixel offsets for each direction.
var/list/px_offsets
+ // pivots used to calculate the correctt rotation both for secondary hardpoints and for primary hardpoints.
+ // because the secondary hardpoints are rotating WITH the turret, but also AROUND themselves, they need two pivots.
+ var/list/rotation_pivot
+ var/list/gimbal_pivot
+
/// Visual layer of hardpoint when on vehicle.
var/hdpt_layer = HDPT_LAYER_WHEELS
@@ -68,8 +73,35 @@
/// Used to prevent welder click spam.
var/being_repaired = FALSE
- /// The firing arc of this hardpoint.
- var/firing_arc = 0 //in degrees. 0 skips whole arc of fire check
+ /// How fast this weapon can traverse. On a turret-mounted weapon, contributes to the shared turret's turn speed (see recalculate_turn_rate()). On a fixed-mount weapon, this is a static firing cone instead (see in_firing_arc()).
+ var/traverse_arc = 0 //in degrees. 0 skips whole arc of fire check on fixed mounts
+
+ /// If TRUE, this hardpoint runs its own independent mouse-aim rotation (current_angle/rotation_loop below) instead of following its holder's rotate() cascade - see /obj/item/hardpoint/secondary/proc/toggle_slaved_to_driver().
+ var/self_gimballed = FALSE
+ /// The hardpoint's live, continuously-simulated facing (0-360, north-clockwise). Distinct from dir, which stays snapped to a cardinal for sprite selection. Only meaningful for the turret itself, or a self_gimballed hardpoint.
+ var/current_angle = 0
+ /// Where the mouse currently wants this hardpoint to face (0-360, north-clockwise). current_angle chases this.
+ var/desired_angle = 0
+ /// Current turn speed, in degrees per tick, ramped toward max_angular_velocity.
+ var/angular_velocity = 0
+ /// Turn speed cap. For the turret, derived from the median traverse_arc of mounted weapons (see recalculate_turn_rate()). For a self_gimballed weapon, derived from its own traverse_arc.
+ var/max_angular_velocity = TURRET_DEFAULT_ANGULAR_VELOCITY
+ /// How fast angular_velocity ramps up/down, in degrees per tick^2.
+ var/angular_accel = TURRET_ANGULAR_ACCEL
+ /// Guards against spawning more than one concurrent rotation_loop().
+ var/rotation_active = FALSE
+ /// world.time of the last processed mouse-move, used to rate-limit update_desired_angle().
+ var/last_desired_update_time = 0
+ /// While TRUE, update_desired_angle() no-ops - set on whichever object get_rotation_owner() resolves
+ /// to while a track_and_charge() lock-on is in progress, so live mouse input can't fight it.
+ var/aim_locked = FALSE
+ /// Guards against spawning more than one concurrent track_and_charge() on this hardpoint.
+ var/charging = FALSE
+ /// Set by cancel_charge() (COMSIG_GUN_STOP_FIRE/COMSIG_GUN_INTERRUPT_FIRE) to abort an in-progress
+ /// track_and_charge() loop. Distinct from fire_wait_cancelled, which guards a different wait
+ /// (arc-entry, not an active charge) and could otherwise interfere if both were active at once.
+ var/charge_cancelled = FALSE
+ var/trigger_held = FALSE
// Muzzleflash
var/use_muzzle_flash = FALSE
@@ -81,6 +113,11 @@
/// Currently loaded ammo that we shoot from.
var/obj/item/ammo_magazine/hardpoint/ammo
+ /// The type of magazine this hardpoint accepts, cached from ammo's class-default in Initialize()
+ /// unlike ammo itself, this stays set even while ammo is temporary null, so
+ /// get_hardpoints_with_ammo()/try_add_clip()/the weapons loader can still recognize this
+ /// hardpoint as reloadable and know whatt magazine type to accept
+ var/ammo_type
/// Spare magazines that we can reload from.
var/list/backup_clips
/// Maximum amount of spare mags.
@@ -96,6 +133,8 @@
var/shots_fired = 0
/// Delay before a new firing sequence can start.
COOLDOWN_DECLARE(fire_cooldown)
+ /// Set by cancel_firing_arc_wait() to break out of wait_for_firing_arc() early.
+ var/fire_wait_cancelled = FALSE
// Firemodes.
/// Current selected firemode of the gun.
@@ -127,6 +166,8 @@
var/atom/target
/// The type of projectile to fire
var/projectile_type = /obj/projectile
+ /// How many ammo.current_rounds a single base handle_fire() shot costs. The flamer hardpoints (primary/flamer.dm, secondary/flamer.dm) set this to 0 and do their own reagent-based fuel deduction instead, since their ammo tracks real chemical volume rather than plain integer rounds.
+ var/ammo_cost_per_shot = 1
//-----------------------------
//------GENERAL PROCS----------
@@ -141,6 +182,8 @@
/obj/item/hardpoint/Initialize()
. = ..()
set_bullet_traits()
+ if(ammo)
+ ammo_type = ammo.type
AddComponent(/datum/component/automatedfire/autofire, fire_delay, burst_delay, burst_amount, gun_firemode, autofire_slow_mult, CALLBACK(src, PROC_REF(set_burst_firing)), CALLBACK(src, PROC_REF(reset_fire)), CALLBACK(src, PROC_REF(fire_wrapper)), callback_set_firing = CALLBACK(src, PROC_REF(set_auto_firing)))
/obj/item/hardpoint/Destroy()
@@ -169,6 +212,8 @@
/obj/item/hardpoint/proc/generate_bullet(mob/user, turf/origin_turf)
var/obj/projectile/P = new projectile_type(origin_turf, create_cause_data(initial(name), user))
P.generate_bullet(new ammo.default_ammo)
+ P.effective_range_max = P.ammo.effective_range_max
+ P.effective_range_min = P.ammo.effective_range_min
// Apply bullet traits from gun
for(var/entry in traits_to_give)
var/list/L
@@ -354,17 +399,23 @@
sleep(20)
- forceMove(ammo, get_turf(src))
- ammo.update_icon()
+ if(ammo)
+ forceMove(ammo, get_turf(src))
+ ammo.update_icon()
ammo = A
LAZYREMOVE(backup_clips, A)
+ owner?.update_icon()
to_chat(user, SPAN_NOTICE("You reload \the [name]."))
+/// Whether this hardpoint will accept a given magazine for reloading. Eg, m56d cupola accepts m56d drums.
+/obj/item/hardpoint/proc/accepts_magazine(obj/item/ammo_magazine/magazine)
+ return istype(magazine, ammo_type)
+
//try adding magazine to hardpoint's backup clips. Called via weapons loader
/obj/item/hardpoint/proc/try_add_clip(obj/item/ammo_magazine/A, mob/user)
- if(!ammo)
- to_chat(user, SPAN_WARNING("\The [name] doesn't use ammunition."))
+ if(!ammo_type)
+ to_chat(user, SPAN_WARNING("\The [name] doesn't use ammunition.")) // UA flag
return FALSE
if(max_clips == 0)
to_chat(user, SPAN_WARNING("\The [name] does not have room for additional ammo."))
@@ -387,7 +438,7 @@
playsound(loc, 'sound/machines/hydraulics_2.ogg', 50)
LAZYADD(backup_clips, A)
- to_chat(user, SPAN_NOTICE("You load \the [A] into \the [name]. Ammo: [SPAN_HELPFUL(ammo.current_rounds)]/[SPAN_HELPFUL(ammo.max_rounds)] | Mags: [SPAN_HELPFUL(LAZYLEN(backup_clips))]/[SPAN_HELPFUL(max_clips)]"))
+ to_chat(user, SPAN_NOTICE("You load \the [A] into \the [name]. Ammo: [SPAN_HELPFUL(ammo ? ammo.current_rounds : 0)]/[SPAN_HELPFUL(ammo ? ammo.max_rounds : 0)] | Mags: [SPAN_HELPFUL(LAZYLEN(backup_clips))]/[SPAN_HELPFUL(max_clips)]"))
return TRUE
/obj/item/hardpoint/attackby(obj/item/O, mob/user)
@@ -523,6 +574,7 @@
/// Reset variables used in firing and remove the gun from the autofire system.
/obj/item/hardpoint/proc/stop_fire(datum/source, atom/object, turf/location, control, params)
+ trigger_held = FALSE
SEND_SIGNAL(src, COMSIG_GUN_STOP_FIRE)
if(auto_firing)
reset_fire() //automatic fire doesn't reset itself from COMSIG_GUN_STOP_FIRE
@@ -545,16 +597,20 @@
return
set_target(get_turf_on_clickcatcher(object, source, params))
+ trigger_held = TRUE
if(gun_firemode == GUN_FIREMODE_SEMIAUTO)
- var/fire_return = try_fire(object, source, params)
- //end-of-fire, show ammo (if changed)
- if(fire_return == AUTOFIRE_CONTINUE)
- reset_fire()
- display_ammo(source)
+ INVOKE_ASYNC(src, PROC_REF(fire_semiauto), object, source, params)
else
SEND_SIGNAL(src, COMSIG_GUN_FIRE)
+// Fires a single semi-auto shot and handles its result so it can be invoked asynchronous for linters
+/obj/item/hardpoint/proc/fire_semiauto(atom/target, mob/living/user, params)
+ var/fire_return = try_fire(target, user, params)
+ if(fire_return == AUTOFIRE_CONTINUE)
+ reset_fire()
+ display_ammo(user)
+
/// Wrapper proc for the autofire system to ensure the important args aren't null.
/obj/item/hardpoint/proc/fire_wrapper(atom/target, mob/living/user, params)
if(!target)
@@ -572,23 +628,198 @@
to_chat(user, SPAN_WARNING("\The [name] is broken!"))
return NONE
- if(ammo && ammo.current_rounds <= 0)
+ // A hardpoint can legitimately have no magazine installed at all. This is needed especifically so the
+ // DRGN flamer and the secondary version can 'eject' their magazines to be refueled from a fuel source..
+ // so, other guns can now eject their full magazines and have nothing loaded.
+ if(!ammo)
+ click_empty(user)
+ return NONE
+
+ if(ammo.current_rounds <= 0)
click_empty(user)
return NONE
- if(!in_firing_arc(target))
- to_chat(user, SPAN_WARNING("The target is not within your firing arc!"))
+ // stops you from firing at your own hull. prevents a bug found in testing where guns shoot straight up.
+ if(owner && (get_turf(target) in owner.locs))
+ to_chat(user, SPAN_WARNING("Invalid target!"))
return NONE
- return handle_fire(target, user, params)
+ var/atom/original_target = target
+ var/obj/item/hardpoint/holder/tank_turret/turret = loc
+ var/turret_mounted = istype(turret)
+ if((self_gimballed || turret_mounted) && gun_firemode != GUN_FIREMODE_SEMIAUTO)
+ // Held-trigger fire modes (burst/automatic) don't wait for the turret to finish swinging onto target
+ var/obj/item/hardpoint/facing_source = self_gimballed ? src : turret
+ target = facing_source.redirect_to_current_facing(get_origin_turf(), target)
+ else if(!in_firing_arc(target))
+ // Turret-mounted weapons wait for the turret (or their own gimbal) to finish swinging onto the target before firing
+ if(!turret_mounted || !wait_for_firing_arc(target, user))
+ to_chat(user, SPAN_WARNING("The target is not within your firing arc!"))
+ return NONE
+
+ return handle_fire(target, user, params, original_target)
+
+/**
+ * Waits for in_firing_arc(target) to become true, instead of instantly rejecting the shot.
+ *
+ * Returns TRUE once in arc and ready to fire, FALSE if the wait was cancelled or the shot became
+ * impossible for some other reason while waiting (weapon/target gone, broke, ran dry, trigger let go).
+ */
+/obj/item/hardpoint/proc/wait_for_firing_arc(atom/target, mob/user)
+ fire_wait_cancelled = FALSE
+ RegisterSignal(src, list(COMSIG_GUN_STOP_FIRE, COMSIG_GUN_INTERRUPT_FIRE), PROC_REF(cancel_firing_arc_wait))
+
+ . = TRUE
+ while(!in_firing_arc(target))
+ sleep(1)
+ if(fire_wait_cancelled || !trigger_held || QDELETED(src) || QDELETED(target) || health <= 0 || (ammo && ammo.current_rounds <= 0))
+ . = FALSE
+ break
+
+ UnregisterSignal(src, list(COMSIG_GUN_STOP_FIRE, COMSIG_GUN_INTERRUPT_FIRE))
+
+/obj/item/hardpoint/proc/cancel_firing_arc_wait()
+ SIGNAL_HANDLER
+ fire_wait_cancelled = TRUE
+
+/**
+ * Resolves which object actually owns continuous rotation (current_angle/desired_angle/rotation_loop)
+ * for this hardpoint - itself if self_gimballed, or the shared turret holder if turret-mounted.
+ * Mirrors the same dispatch already used implicitly by in_firing_arc()/check_gimbal_firing_arc()/
+ * in_turret_firing_arc() above.
+ */
+/obj/item/hardpoint/proc/get_rotation_owner()
+ RETURN_TYPE(/obj/item/hardpoint)
+ if(self_gimballed)
+ return src
+ var/obj/item/hardpoint/holder/tank_turret/turret = loc
+ if(istype(turret))
+ return turret
+ return src
+
+/// Single choke point for locking/unlocking mouse-driven aim on whichever object get_rotation_owner()
+/// resolves to, so ttrack_and_charge() below can lock/unlock symmetrically.
+/obj/item/hardpoint/proc/lock_rotation(lock)
+ get_rotation_owner().aim_locked = lock
+
+/**
+ * Locks onto target and keeps re-aiming at its live position for aim_time, cancellable if the target
+ * drifts further behind the current facing than this hardpoint's own traverse_arc allows, giving the
+ * mount a litttle bit of allowance to catch up before the shot is considered lost
+ *
+ * Recomputes the true world-space angle to the target fresh every tick from this hardpoint's current
+ * position, so it needs no special-casing for the vehicle itself moving/turning mid-charge
+ *
+ *
+ * Returns TRUE if the charge completed with the target still valid and in arc, FALSE if cancelled.
+ */
+/obj/item/hardpoint/proc/track_and_charge(atom/target, mob/living/user, aim_time)
+ if(charging)
+ return FALSE
+ charging = TRUE
+
+ set_target(target)
+ lock_rotation(TRUE)
+ charge_cancelled = FALSE
+ RegisterSignal(src, list(COMSIG_GUN_STOP_FIRE, COMSIG_GUN_INTERRUPT_FIRE), PROC_REF(cancel_charge))
+ start_aim_visuals(target, user)
+
+ . = TRUE
+ var/elapsed = 0
+ while(elapsed < aim_time)
+ sleep(1)
+ elapsed += 1
+
+ if(charge_cancelled || !trigger_held || QDELETED(src) || QDELETED(target) || health <= 0 || (ammo && ammo.current_rounds <= 0))
+ . = FALSE
+ break
+
+ if(isliving(target))
+ var/mob/living/living_target = target
+ if(living_target.is_dead())
+ . = FALSE
+ break
+
+ var/turf/origin_turf = get_origin_turf()
+ var/turf/target_turf = get_turf(target)
+ if(origin_turf == target_turf)
+ . = FALSE
+ break
+
+ try
+ // Angle uses the target atom itself, not target_turf
+ var/angle_to_target = Get_Angle_Grounded(origin_turf, target)
+ var/obj/item/hardpoint/rotation_owner = get_rotation_owner()
+ var/obj/item/hardpoint/holder/tank_turret/mount = self_gimballed ? loc : null
+
+ if(istype(mount)) // self-gimballed, mounted on a turret
+ var/turret_facing = mount.current_angle
+ var/raw_delta = angle_delta(angle_to_target, turret_facing)
+ if(abs(raw_delta) > SLAVED_GIMBAL_ARC_HALF_WIDTH)
+ to_chat(user, SPAN_WARNING("Target moved out of the firing arc!"))
+ . = FALSE
+ else
+ var/swing = clamp(raw_delta, -SLAVED_GIMBAL_ARC_HALF_WIDTH, SLAVED_GIMBAL_ARC_HALF_WIDTH)
+ rotation_owner.desired_angle = ((turret_facing + swing) % 360 + 360) % 360
+ else
+ rotation_owner.desired_angle = angle_to_target
+ if(traverse_arc && abs(angle_delta(angle_to_target, rotation_owner.current_angle)) > traverse_arc * 0.5)
+ to_chat(user, SPAN_WARNING("Target moved out of the firing arc!"))
+ . = FALSE
+ if(. != FALSE)
+ rotation_owner.start_rotation_if_needed()
+ on_track_tick(target)
+ catch(var/exception/tick_error)
+ log_runtime(tick_error)
+ . = FALSE
+
+ if(. == FALSE)
+ break
+
+ UnregisterSignal(src, list(COMSIG_GUN_STOP_FIRE, COMSIG_GUN_INTERRUPT_FIRE))
+ end_aim_visuals(target, user, .)
+ lock_rotation(FALSE)
+ charging = FALSE
+
+/obj/item/hardpoint/proc/cancel_charge()
+ SIGNAL_HANDLER
+ charge_cancelled = TRUE
+
+/// No-op hook - override to spawn lock-on visuals (overlays/beams) when a track_and_charge() begins.
+// currently overriden by LTB and mounted BRUTE launcher only. Same goes for the other hooks below. - BWSB
+/obj/item/hardpoint/proc/start_aim_visuals(atom/target, mob/living/user)
+ return
+
+/// No-op hook - override to tear down whatever start_aim_visuals() spawned. Always called, success or not.
+/obj/item/hardpoint/proc/end_aim_visuals(atom/target, mob/living/user, success)
+ return
+
+/// No-op hook - called once per tick during track_and_charge(), after desired_angle has been updated.
+/// Override to keep any tick-dependent visuals (e.g. a beam anchor) following the live muzzle position.
+/obj/item/hardpoint/proc/on_track_tick(atom/target)
+ return
/// Actually fires the gun, sets up the projectile and fires it.
-/obj/item/hardpoint/proc/handle_fire(atom/target, mob/living/user, params)
+/obj/item/hardpoint/proc/handle_fire(atom/target, mob/living/user, params, atom/original_target)
+ if(isnull(original_target))
+ original_target = target
var/turf/origin_turf = get_origin_turf()
- var/obj/projectile/projectile_to_fire = generate_bullet(user, origin_turf)
- ammo.current_rounds--
+ // Spawn projectile outside the vehicle hull so riders aren't hit.
+ var/turf/spawn_turf = origin_turf
+ if(owner && length(owner.locs) > 1)
+ var/turf/target_turf = get_turf(target)
+ var/list/path = get_line(origin_turf, target_turf)
+ for(var/turf/T as anything in path)
+ // preventts a bug where the projectile disappears if it occupies the same tile the projectile spawns in
+ if(!(T in owner.locs) && T != target_turf)
+ spawn_turf = T
+ break
+
+ var/obj/projectile/projectile_to_fire = generate_bullet(user, spawn_turf)
+ ammo.current_rounds -= ammo_cost_per_shot
SEND_SIGNAL(projectile_to_fire, COMSIG_BULLET_USER_EFFECTS, user)
+ projectile_to_fire.original = original_target
// turf-targeted projectiles are fired without scatter, because proc would raytrace them further away
var/ammo_flags = projectile_to_fire.ammo.flags_ammo_behavior | projectile_to_fire.projectile_override_flags
@@ -608,6 +839,51 @@
return AUTOFIRE_CONTINUE
+/**
+ * No-op by default - only the flamer hardpoints (primary/flamer.dm, secondary/flamer.dm) override
+ * this to flip between FLAME_MODE_STTREAM and FLAME_MODE_GLOB
+ */
+/obj/item/hardpoint/proc/toggle_fire_mode(mob/user)
+ return
+
+/**
+ * Proc that makes the flamer stream work more closely to how the M240 Incinerator is implemented in game
+ * instead of drawing from an arbitrary ammo pool, it now draws from the chem reagents inside a tank, just like the m240.
+ */
+/obj/item/hardpoint/proc/fire_flame_stream(atom/target, mob/living/user, max_range, flameshape = FLAMESHAPE_LINE)
+ //step forward along path so flame starts outside hull
+ var/list/turfs = get_line(get_origin_turf(), get_turf(target))
+ var/turf/origin_turf
+ for(var/turf/turf as anything in turfs)
+ if(turf in owner.locs)
+ continue
+ origin_turf = turf
+ break
+
+ var/distance = get_dist(origin_turf, get_turf(target))
+ var/fire_amount = min(distance+1, max_range)
+
+ var/datum/reagent/chem = LAZYACCESS(ammo.reagents?.reagent_list, 1)
+ new /obj/flamer_fire(origin_turf, create_cause_data(initial(name), user), chem, fire_amount, ammo.reagents, flameshape, target, CALLBACK(src, PROC_REF(sync_ammo_from_reagents)))
+ sync_ammo_from_reagents()
+
+ play_firing_sounds()
+
+ COOLDOWN_START(src, fire_cooldown, fire_delay)
+
+ return AUTOFIRE_CONTINUE
+
+/**
+ * Re-syncs ammo.current_rounds to match ammo.reagents.total_volume.sync_ammo_from_reagents()
+ * Called both right after firing and again once a since reagent consumption happens asynchronously tile by tile
+ * as the flame propagates, not all at once at the moment of firing. Also refreshes the mounted sprite
+ * immediately.
+ */
+/obj/item/hardpoint/proc/sync_ammo_from_reagents()
+ if(ammo && ammo.reagents)
+ ammo.current_rounds = round(ammo.reagents.total_volume)
+ owner?.update_icon()
+
/// Start cooldown to respect delay of firemode.
/obj/item/hardpoint/proc/set_fire_cooldown(firemode)
var/cooldown_time = 0
@@ -622,6 +898,10 @@
/// Adjust target based on random scatter angle.
/obj/item/hardpoint/proc/simulate_scatter(obj/projectile/projectile_to_fire, atom/target, turf/curloc, turf/targloc)
+ // without this, clicking a point-blank target that resolves to the same turf as the muzzle
+ if(curloc == targloc)
+ return target
+
var/fire_angle = Get_Angle(curloc, targloc)
var/total_scatter_angle = projectile_to_fire.scatter
@@ -630,6 +910,9 @@
fire_angle += rand(-total_scatter_angle, total_scatter_angle)
target = get_angle_target_turf(curloc, fire_angle, 30)
+ if(curloc.z != targloc.z)
+ target = locate(target.x, target.y, targloc.z)
+
return target
/// Get turf at hardpoint origin offset, used as the muzzle.
@@ -649,7 +932,14 @@
/// Determines whether something is in firing arc of a hardpoint.
/obj/item/hardpoint/proc/in_firing_arc(atom/target)
- if(!firing_arc || !ISINRANGE_EX(firing_arc, 0, 360))
+ if(self_gimballed)
+ return check_gimbal_firing_arc(target)
+
+ var/obj/item/hardpoint/holder/tank_turret/turret = loc
+ if(istype(turret))
+ return turret.in_turret_firing_arc(src, target)
+
+ if(!traverse_arc || !ISINRANGE_EX(traverse_arc, 0, 360))
return TRUE
var/turf/muzzle_turf = get_origin_turf()
@@ -665,7 +955,247 @@
else if(angle_diff > 180)
angle_diff -= 360
- return abs(angle_diff) <= (firing_arc * 0.5)
+ return abs(angle_diff) <= (traverse_arc * 0.5)
+
+/**
+ * Firing-arc gate for a self-gimballed hardpoint (see toggle_slaved_to_driver()): current_angle
+ * must have caught up to within FIRING_GATE_TOLERANCE of the angle from the weapon's own muzzle to
+ * target. Same shape as /obj/item/hardpoint/holder/tank_turret/proc/in_turret_firing_arc(), but
+ * measured against this hardpoint's own current_angle instead of a parent turret's.
+ */
+/obj/item/hardpoint/proc/check_gimbal_firing_arc(atom/target)
+ var/turf/muzzle_turf = get_origin_turf()
+ var/turf/target_turf = get_turf(target)
+
+ //same tile angle is undefined for Gett_Angle, returning FALSE to match the legacy static-arc check
+ if(muzzle_turf == target_turf)
+ return FALSE
+
+ var/target_angle = Get_Angle(muzzle_turf, target_turf)
+ return abs(angle_delta(target_angle, current_angle)) <= FIRING_GATE_TOLERANCE
+
+/**
+ * records where the mouse currently wants this hardpoint to face. Called on every processed
+ * mouse-move while a relevant crew member is seated.
+ *
+ * A self_gimballed weapon's desired_angle gets clamped to within SLAVED_GIMBAL_ARC_HALF_WIDTH
+ * degrees of the turret's own current continuous facing. the turret can sit anywhere within its own lean, e.g. 168
+ * degrees, and the allowed swing needs to follow that actual angle, not snap to whichever cardinal it's nearest to.
+ */
+/obj/item/hardpoint/proc/update_desired_angle(atom/object, mob/user, params)
+ if(health <= 0)
+ return
+ if(aim_locked) // a track_and_charge() lock-on is in progress - mouse input can't fight it
+ return
+ if(world.time == last_desired_update_time) // collapses same-tick MouseMove spam into one update
+ return
+ last_desired_update_time = world.time
+
+ // Aim at the hovered mob/object itself, not its bare turf
+ var/atom/aim_target
+ if(ismob(object) || isobj(object))
+ aim_target = object
+ else
+ aim_target = get_turf_on_clickcatcher(object, user, params)
+ if(!aim_target)
+ return
+ var/turf/target_turf = get_turf(aim_target)
+ var/turf/origin_turf = get_origin_turf()
+ if(!origin_turf || target_turf == origin_turf)
+ return
+
+ // Using the raw pixel-precise angle here aims at a tall sprite's visual midpoint instead of the tile it's
+ // actually standing on, which is what actually needs to be hit. very shittty bug to fix during testing
+ // which made the autocannon and minigun DPS plummet.
+ desired_angle = Get_Angle_Grounded(origin_turf, aim_target)
+ desired_angle = Get_Angle_Grounded(origin_turf, target_turf)
+
+ if(self_gimballed)
+ var/obj/item/hardpoint/holder/tank_turret/turret = loc
+ if(istype(turret))
+ var/turret_facing = turret.current_angle
+ var/swing = clamp(angle_delta(desired_angle, turret_facing), -SLAVED_GIMBAL_ARC_HALF_WIDTH, SLAVED_GIMBAL_ARC_HALF_WIDTH)
+ desired_angle = ((turret_facing + swing) % 360 + 360) % 360
+
+ start_rotation_if_needed()
+
+/obj/item/hardpoint/proc/start_rotation_if_needed()
+ if(rotation_active)
+ return
+ rotation_active = TRUE
+ spawn(0)
+ rotation_loop()
+
+/**
+ * Ticks current_angle toward desired_angle with inertia (accelerating up to max_angular_velocity,
+ * decelerating on approach). Self-terminates once current_angle settles within ROTATION_SETTLE_TOLERANCE of
+ * desired_angle, and restarts on demand from update_desired_angle().
+ */
+/obj/item/hardpoint/proc/rotation_loop()
+ while(TRUE)
+ sleep(1)
+
+ if(QDELETED(src) || health <= 0)
+ angular_velocity = 0
+ rotation_active = FALSE
+ return
+
+ var/delta = angle_delta(desired_angle, current_angle)
+ if(abs(delta) < ROTATION_SETTLE_TOLERANCE)
+ current_angle = desired_angle
+ angular_velocity = 0
+ drag_self_gimballed_weapons(delta)
+ apply_current_angle()
+ rotation_active = FALSE
+ return
+
+ // Accelerate toward max_angular_velocity, decelerate once we're close enough to stop in time.
+ var/stopping_distance = (angular_velocity ** 2) / (2 * angular_accel)
+ if(abs(delta) <= stopping_distance)
+ angular_velocity = max(angular_velocity - angular_accel, 0)
+ else
+ angular_velocity = min(angular_velocity + angular_accel, max_angular_velocity)
+
+ var/turn_sign = (delta > 0) - (delta < 0)
+ var/step = min(angular_velocity, abs(delta)) * turn_sign
+ current_angle = ((current_angle + step) % 360 + 360) % 360
+ drag_self_gimballed_weapons(step)
+ apply_current_angle()
+
+/**
+ * No-op by default. /obj/item/hardpoint/holder/tank_turret overrides this to drag every
+ * self_gimballed mounted weapon's own current_angle/desired_angle along by the same delta this
+ * hardpoint's current_angle just moved by.
+ *
+ * Without this, a slaved weapon's current_angle stays a fixed absolute world angle.
+ * This would be fine, but, since we can slave the secondaries to the driver, then they also need to drag by themselves.
+ */
+/obj/item/hardpoint/proc/drag_self_gimballed_weapons(delta)
+ return
+
+/// Snaps a continuous angle down to the nearest 90-degree cardinal quadrant
+/obj/item/hardpoint/proc/angle_to_cardinal(angle)
+ angle = ((angle % 360) + 360) % 360
+ if(angle >= 315 || angle < 45)
+ return NORTH
+ if(angle < 135)
+ return EAST
+ if(angle < 225)
+ return SOUTH
+ return WEST
+
+/**
+ * Keeps dir/origins in sync with current_angle's quadrant, then refreshes the vehicle's sprite.
+ */
+/obj/item/hardpoint/proc/apply_current_angle()
+ if(owner)
+ owner.update_icon()
+
+/**
+ * Builds a matrix that rotates by tilt degrees around pivot (a local x,y point relative to the
+ * icon's own center) instead of the icon's default center. pivot defaults to (0,0) if null/unset.
+ *
+ * (Translate(pivot), called last, applied last) - Translate(-pivot) -> Turn(tilt) -> Translate(pivot).
+ */
+/obj/item/hardpoint/proc/build_tilt_matrix(tilt, list/pivot)
+ var/matrix/tilt_matrix = matrix()
+ if(pivot && (pivot[1] || pivot[2]))
+ tilt_matrix.Translate(-pivot[1], -pivot[2])
+ tilt_matrix.Turn(tilt)
+ tilt_matrix.Translate(pivot[1], pivot[2])
+ else
+ tilt_matrix.Turn(tilt)
+ return tilt_matrix
+
+/**
+ * Like build_tilt_matrix(), but composes two nested rotations onto one matrix instead of one:
+ * inner_tilt around inner_pivot is applied first (closest to the raw icon), then outter_tilt around
+ * outer_pivot is applied on top of that, carrying the already-tilted icon along with it. Used to
+ * render a self_gimballed weapon, its own idependent swivel (inner, pivoting around its own local
+ * mount joint, gimbal_pivot) rides along with the turret's own lean (outer, pivoting around the
+ * turret's own rotation center, rotation_pivot) instead of replacing it - two separate pivots of
+ * rotation, one nested inside the other, same as a real two-stage gimbal mountted on a turret.
+ */
+/obj/item/hardpoint/proc/build_nested_tilt_matrix(inner_tilt, list/inner_pivot, outer_tilt, list/outer_pivot)
+ var/matrix/tilt_matrix = build_tilt_matrix(inner_tilt, inner_pivot)
+ if(outer_tilt)
+ if(outer_pivot && (outer_pivot[1] || outer_pivot[2]))
+ tilt_matrix.Translate(-outer_pivot[1], -outer_pivot[2])
+ tilt_matrix.Turn(outer_tilt)
+ tilt_matrix.Translate(outer_pivot[1], outer_pivot[2])
+ else
+ tilt_matrix.Turn(outer_tilt)
+ return tilt_matrix
+
+/**
+ * Looks up the pivot for a given angle out of up to 8 keyed keyframes (dir constants, same
+ * convention as dir2angle()), blending between a cardinal and an adjacent diagonal if that
+ * diagonal has a tuned entry - otherwise hard-snaps to the cardinal (old angle_to_cardinal()
+ * behavior), so untuned weapons render unchanged. Returns null if pivot_data is empty.
+ *
+ * Uses a direct equality switch rather than angle_to_dir() that proc rounds to the NEARESTT dir
+ * and misresolves exact cardinal angles (0/90/180/270) to an adjacent diagonal, which a multiple of 45 avoids
+ */
+/obj/item/hardpoint/proc/interpolate_pivot(list/pivot_data, angle)
+ if(!pivot_data)
+ return null
+
+ angle = ((angle % 360) + 360) % 360
+ var/lower_anchor = FLOOR(angle, 45)
+ var/upper_anchor = (lower_anchor + 45) % 360
+
+ var/list/lower_pivot = LAZYACCESS(pivot_data, "[anchor_angle_to_dir(lower_anchor)]")
+ var/list/upper_pivot = LAZYACCESS(pivot_data, "[anchor_angle_to_dir(upper_anchor)]")
+
+ if(!lower_pivot)
+ return upper_pivot
+ if(!upper_pivot)
+ return lower_pivot
+
+ var/frac = (angle - lower_anchor) / 45
+ return list(
+ lower_pivot[1] + (upper_pivot[1] - lower_pivot[1]) * frac,
+ lower_pivot[2] + (upper_pivot[2] - lower_pivot[2]) * frac,
+ )
+
+/// Exact reverse of dir2angle() for one of the 8 cardinal/diagonal 45-degree-multiple angles - see interpolate_pivot()'s doc comment for why this can't just reuse the global angle_to_dir() nearest-neighbor helper.
+/obj/item/hardpoint/proc/anchor_angle_to_dir(angle)
+ switch(angle)
+ if(0)
+ return NORTH
+ if(45)
+ return NORTHEAST
+ if(90)
+ return EAST
+ if(135)
+ return SOUTHEAST
+ if(180)
+ return SOUTH
+ if(225)
+ return SOUTHWEST
+ if(270)
+ return WEST
+ if(315)
+ return NORTHWEST
+ return NORTH
+
+/**
+ * Redirects a target to preserve its range from origin_turf but along this hardpoint's actual
+ * current facing (current_angle) instead of the originally-aimed direction. Used by held-ttrigger
+ * fire modes (see try_fire()), which don't wait for the turret (or a self-gimballed weapon's own gimbal) to finish swinging onto target.
+ */
+/obj/item/hardpoint/proc/redirect_to_current_facing(turf/origin_turf, atom/target)
+ var/turf/target_turf = get_turf(target)
+ if(!origin_turf || !target_turf || origin_turf == target_turf)
+ return target
+
+ // Once the barrel has fully caught up to its own tracked aim point, there's no
+ // rotational lag left to simulate. fire directly at the target.
+ if(abs(angle_delta(desired_angle, current_angle)) <= FIRING_GATE_TOLERANCE)
+ return target
+
+ var/range = get_dist_euclidian(origin_turf, target_turf)
+ return get_angle_target_turf(origin_turf, current_angle, range)
//-----------------------------
//------ICON PROCS----------
diff --git a/code/modules/vehicles/hardpoints/hardpoint_ammo/flare_ammo.dm b/code/modules/vehicles/hardpoints/hardpoint_ammo/flare_ammo.dm
new file mode 100644
index 000000000000..099fef47c49e
--- /dev/null
+++ b/code/modules/vehicles/hardpoints/hardpoint_ammo/flare_ammo.dm
@@ -0,0 +1,13 @@
+/obj/item/ammo_magazine/hardpoint/turret_flare
+ name = "Turret Flare Screen Magazine"
+ desc = "The turret's internal star shell holders. Loaded directly with individual M74 AGM-S star shells or star shell packets, not swapped as a whole magazine."
+ caliber = "flare"
+ icon = 'icons/obj/items/weapons/guns/ammo_by_faction/USCM/vehicles.dmi'
+ icon_state = "slauncher_1"
+ w_class = SIZE_LARGE
+ default_ammo = /datum/ammo/flare/starshell/burst
+ max_rounds = 6
+ gun_type = /obj/item/hardpoint/holder/tank_turret
+
+/obj/item/ammo_magazine/hardpoint/turret_flare/update_icon()
+ icon_state = "slauncher_[current_rounds <= 0 ? "0" : "1"]"
diff --git a/code/modules/vehicles/hardpoints/hardpoint_ammo/ltb_ammo.dm b/code/modules/vehicles/hardpoints/hardpoint_ammo/ltb_ammo.dm
index ee6de433eb36..aa7b0803dad9 100644
--- a/code/modules/vehicles/hardpoints/hardpoint_ammo/ltb_ammo.dm
+++ b/code/modules/vehicles/hardpoints/hardpoint_ammo/ltb_ammo.dm
@@ -11,3 +11,13 @@
/obj/item/ammo_magazine/hardpoint/ltb_cannon/update_icon()
icon_state = "ltbcannon_[current_rounds]"
+
+// Single-round alternative magazine, still istype()-matched against the hardpoint's cached ammo_type
+/obj/item/ammo_magazine/hardpoint/ltb_cannon/he_shell
+ name = "86x455mm HE Shell"
+ desc = "A single high-explosive 86x455mm shell for the LTB Cannon."
+ icon_state = "lbtshell_1"
+ max_rounds = 1
+
+/obj/item/ammo_magazine/hardpoint/ltb_cannon/he_shell/update_icon()
+ icon_state = "lbtshell_[current_rounds]"
diff --git a/code/modules/vehicles/hardpoints/hardpoint_ammo/primary_flamer_ammo.dm b/code/modules/vehicles/hardpoints/hardpoint_ammo/primary_flamer_ammo.dm
index 8384b3f6d94e..1c596f54c207 100644
--- a/code/modules/vehicles/hardpoints/hardpoint_ammo/primary_flamer_ammo.dm
+++ b/code/modules/vehicles/hardpoints/hardpoint_ammo/primary_flamer_ammo.dm
@@ -5,12 +5,103 @@
icon = 'icons/obj/items/weapons/guns/ammo_by_faction/USCM/vehicles.dmi'
icon_state = "drgn_flametank"
w_class = SIZE_LARGE
- max_rounds = 60
+ max_rounds = 300
gun_type = /obj/item/hardpoint/primary/flamer
default_ammo = /datum/ammo/flamethrower/tank_flamer
+ // Reagent id this tank starts filled with
+ var/flamer_chem = "highdamagenapalm"
+/obj/item/ammo_magazine/hardpoint/primary_flamer/Initialize(mapload, ...)
+ . = ..()
+ create_reagents(max_rounds)
+ if(flamer_chem)
+ reagents.add_reagent(flamer_chem, max_rounds)
+ current_rounds = round(reagents.total_volume)
+ update_icon()
+
+/**
+ * Refuels this tank from a fuel source, exactly mirroring /obj/item/ammo_magazine/flamer_tank/afterattack()
+ *
+ * also follows the same rules regarding what fuel types are or aren't allowed.
+ */
+/obj/item/ammo_magazine/hardpoint/primary_flamer/afterattack(obj/target, mob/user, flag)
+ if(get_dist(user, target) > 1)
+ return ..()
+ if(!istype(target, /obj/structure/reagent_dispensers/tank/fuel) && !istype(target, /obj/item/tool/weldpack) && !istype(target, /obj/item/storage/backpack/marine/engineerpack) && !istype(target, /obj/item/ammo_magazine/hardpoint/primary_flamer))
+ return ..()
+
+ if(!target.reagents || length(target.reagents.reagent_list) < 1)
+ to_chat(user, SPAN_WARNING("[target] is empty!"))
+ return
+
+ if(!reagents)
+ create_reagents(max_rounds)
+
+ var/datum/reagent/to_add = target.reagents.reagent_list[1]
+
+ if(!istype(to_add) || (length(reagents.reagent_list) && flamer_chem != to_add.id) || length(target.reagents.reagent_list) > 1)
+ to_chat(user, SPAN_WARNING("You can't mix fuel mixtures!"))
+ return
+
+ if(!to_add.intensityfire && to_add.id != "stablefoam")
+ to_chat(user, SPAN_WARNING("This chemical is not potent enough to be used in a flamethrower!"))
+ return
+
+ var/fuel_amt_to_remove = clamp(to_add.volume, 0, max_rounds - reagents.get_reagent_amount(to_add.id))
+ if(!fuel_amt_to_remove)
+ if(!max_rounds)
+ to_chat(user, SPAN_WARNING("[target] is empty!"))
+ return
+
+ target.reagents.remove_reagent(to_add.id, fuel_amt_to_remove)
+ reagents.add_reagent(to_add.id, fuel_amt_to_remove)
+ playsound(loc, 'sound/effects/refill.ogg', 25, 1, 3)
+ caliber = to_add.name
+ flamer_chem = to_add.id
+ current_rounds = round(reagents.total_volume)
+
+ to_chat(user, SPAN_NOTICE("You refill [src] with [caliber]."))
+ update_icon()
+
+
+// Colored fuel strip overlay
/obj/item/ammo_magazine/hardpoint/primary_flamer/update_icon()
+ overlays.Cut()
if(current_rounds > 0)
icon_state = "drgn_flametank"
+ var/image/strip = image(icon, icon_state = "drgn_flametank_strip")
+ if(reagents)
+ strip.color = mix_color_from_reagents(reagents.reagent_list)
+ overlays += strip
else
icon_state = "drgn_flametank_empty"
+
+// Lists loaded fuel by name/volume on examine
+/obj/item/ammo_magazine/hardpoint/primary_flamer/get_examine_text(mob/user)
+ . = ..()
+ . += SPAN_NOTICE("It contains:")
+ if(reagents && length(reagents.reagent_list))
+ for(var/datum/reagent/reagent in reagents.reagent_list)
+ . += SPAN_NOTICE(" [reagent.volume] units of [reagent.name].")
+ else
+ . += SPAN_NOTICE(" Nothing.")
+
+// "Empty canister" verb, also found in the m240 flamer.
+/obj/item/ammo_magazine/hardpoint/primary_flamer/verb/remove_reagents()
+ set name = "Empty canister"
+ set category = "Object"
+
+ set src in usr
+
+ if(usr.get_active_hand() != src)
+ return
+
+ if(alert(usr, "Do you really want to empty out [src]?", "Empty canister", "Yes", "No") != "Yes")
+ return
+
+ reagents.clear_reagents()
+ current_rounds = round(reagents.total_volume)
+
+ playsound(loc, 'sound/effects/refill.ogg', 25, 1, 3)
+ to_chat(usr, SPAN_NOTICE("You empty out [src]"))
+ update_icon()
diff --git a/code/modules/vehicles/hardpoints/hardpoint_ammo/secondary_flamer_ammo.dm b/code/modules/vehicles/hardpoints/hardpoint_ammo/secondary_flamer_ammo.dm
index 4a09046c9678..d2d3cb34a872 100644
--- a/code/modules/vehicles/hardpoints/hardpoint_ammo/secondary_flamer_ammo.dm
+++ b/code/modules/vehicles/hardpoints/hardpoint_ammo/secondary_flamer_ammo.dm
@@ -7,3 +7,97 @@
w_class = SIZE_LARGE
max_rounds = 150
gun_type = /obj/item/hardpoint/secondary/small_flamer
+ // Only used when the hardpoint is toggled to FLAME_MODE_GLOB
+ default_ammo = /datum/ammo/flamethrower/tank_flamer_secondary
+ // Reagent id this tank starts filled with
+ var/flamer_chem = "napalm"
+
+/obj/item/ammo_magazine/hardpoint/secondary_flamer/Initialize(mapload, ...)
+ . = ..()
+ create_reagents(max_rounds)
+ if(flamer_chem)
+ reagents.add_reagent(flamer_chem, max_rounds)
+ current_rounds = round(reagents.total_volume)
+ update_icon()
+
+// Colored fuel strip overlay
+/obj/item/ammo_magazine/hardpoint/secondary_flamer/update_icon()
+ overlays.Cut()
+ var/image/strip = image(icon, icon_state = "flametank_large_custom_strip")
+ if(reagents && length(reagents.reagent_list))
+ strip.color = mix_color_from_reagents(reagents.reagent_list)
+ overlays += strip
+
+/**
+ * Refuels this tank from a fuel source, exactly mirroring /obj/item/ammo_magazine/flamer_tank/afterattack()
+ *
+ * also follows the same rules regarding what fuel types are or aren't allowed.
+ */
+/obj/item/ammo_magazine/hardpoint/secondary_flamer/afterattack(obj/target, mob/user, flag)
+ if(get_dist(user, target) > 1)
+ return ..()
+ if(!istype(target, /obj/structure/reagent_dispensers/tank/fuel) && !istype(target, /obj/item/tool/weldpack) && !istype(target, /obj/item/storage/backpack/marine/engineerpack) && !istype(target, /obj/item/ammo_magazine/hardpoint/secondary_flamer))
+ return ..()
+
+ if(!target.reagents || length(target.reagents.reagent_list) < 1)
+ to_chat(user, SPAN_WARNING("[target] is empty!"))
+ return
+
+ if(!reagents)
+ create_reagents(max_rounds)
+
+ var/datum/reagent/to_add = target.reagents.reagent_list[1]
+
+ if(!istype(to_add) || (length(reagents.reagent_list) && flamer_chem != to_add.id) || length(target.reagents.reagent_list) > 1)
+ to_chat(user, SPAN_WARNING("You can't mix fuel mixtures!"))
+ return
+
+ if(!to_add.intensityfire && to_add.id != "stablefoam")
+ to_chat(user, SPAN_WARNING("This chemical is not potent enough to be used in a flamethrower!"))
+ return
+
+ var/fuel_amt_to_remove = clamp(to_add.volume, 0, max_rounds - reagents.get_reagent_amount(to_add.id))
+ if(!fuel_amt_to_remove)
+ if(!max_rounds)
+ to_chat(user, SPAN_WARNING("[target] is empty!"))
+ return
+
+ target.reagents.remove_reagent(to_add.id, fuel_amt_to_remove)
+ reagents.add_reagent(to_add.id, fuel_amt_to_remove)
+ playsound(loc, 'sound/effects/refill.ogg', 25, 1, 3)
+ caliber = to_add.name
+ flamer_chem = to_add.id
+ current_rounds = round(reagents.total_volume)
+
+ to_chat(user, SPAN_NOTICE("You refill [src] with [caliber]."))
+ update_icon()
+
+// Lists loaded fuel by name/volume when examining
+/obj/item/ammo_magazine/hardpoint/secondary_flamer/get_examine_text(mob/user)
+ . = ..()
+ . += SPAN_NOTICE("It contains:")
+ if(reagents && length(reagents.reagent_list))
+ for(var/datum/reagent/reagent in reagents.reagent_list)
+ . += SPAN_NOTICE(" [reagent.volume] units of [reagent.name].")
+ else
+ . += SPAN_NOTICE(" Nothing.")
+
+// "Empty canister" verb, also found in the m240 flamer.
+/obj/item/ammo_magazine/hardpoint/secondary_flamer/verb/remove_reagents()
+ set name = "Empty canister"
+ set category = "Object"
+
+ set src in usr
+
+ if(usr.get_active_hand() != src)
+ return
+
+ if(alert(usr, "Do you really want to empty out [src]?", "Empty canister", "Yes", "No") != "Yes")
+ return
+
+ reagents.clear_reagents()
+ current_rounds = round(reagents.total_volume)
+
+ playsound(loc, 'sound/effects/refill.ogg', 25, 1, 3)
+ to_chat(usr, SPAN_NOTICE("You empty out [src]"))
+ update_icon()
diff --git a/code/modules/vehicles/hardpoints/hardpoint_ammo/smoke_ammo.dm b/code/modules/vehicles/hardpoints/hardpoint_ammo/smoke_ammo.dm
deleted file mode 100644
index 04846d74c423..000000000000
--- a/code/modules/vehicles/hardpoints/hardpoint_ammo/smoke_ammo.dm
+++ /dev/null
@@ -1,13 +0,0 @@
-/obj/item/ammo_magazine/hardpoint/turret_smoke
- name = "Turret Smoke Screen Magazine"
- desc = "A smoke grenades magazine used by tank turret."
- caliber = "grenade"
- icon = 'icons/obj/items/weapons/guns/ammo_by_faction/USCM/vehicles.dmi'
- icon_state = "slauncher_1"
- w_class = SIZE_LARGE
- default_ammo = /datum/ammo/grenade_container/smoke
- max_rounds = 10
- gun_type = /obj/item/hardpoint/holder/tank_turret
-
-/obj/item/ammo_magazine/hardpoint/turret_smoke/update_icon()
- icon_state = "slauncher_[current_rounds <= 0 ? "0" : "1"]"
diff --git a/code/modules/vehicles/hardpoints/holder/holder.dm b/code/modules/vehicles/hardpoints/holder/holder.dm
index a1797cb68b8b..9fa4cd3a5775 100644
--- a/code/modules/vehicles/hardpoints/holder/holder.dm
+++ b/code/modules/vehicles/hardpoints/holder/holder.dm
@@ -142,6 +142,11 @@
H.on_uninstall(owner)
H.reset_rotation()
+
+ if(H.self_gimballed)
+ H.self_gimballed = FALSE
+ H.allowed_seat = initial(H.allowed_seat)
+
hardpoints -= H
H.owner = null
@@ -161,7 +166,7 @@
/obj/item/hardpoint/holder/proc/get_hardpoints_with_ammo(seat)
var/list/hps = list()
for(var/obj/item/hardpoint/H in hardpoints)
- if(!H.ammo || seat && seat != H.allowed_seat)
+ if(!H.ammo_type || seat && seat != H.allowed_seat)
continue
hps += H
return hps
diff --git a/code/modules/vehicles/hardpoints/holder/tank_turret.dm b/code/modules/vehicles/hardpoints/holder/tank_turret.dm
index edb119cc1327..bc949ec6a396 100644
--- a/code/modules/vehicles/hardpoints/holder/tank_turret.dm
+++ b/code/modules/vehicles/hardpoints/holder/tank_turret.dm
@@ -1,6 +1,6 @@
/obj/item/hardpoint/holder/tank_turret
name = "\improper M34A2-A Multipurpose Turret"
- desc = "The centerpiece of the tank. Designed to support quick installation and deinstallation of various tank weapon modules. Has inbuilt smoke screen deployment system."
+ desc = "The centerpiece of the tank. Designed to support quick installation and deinstallation of various tank weapon modules. Has inbuilt flare launcher."
icon = 'icons/obj/vehicles/tank.dmi'
icon_state = "tank_turret_0"
@@ -14,8 +14,8 @@
activatable = TRUE
- ammo = new /obj/item/ammo_magazine/hardpoint/turret_smoke
- max_clips = 2
+ ammo = new /obj/item/ammo_magazine/hardpoint/turret_flare
+ max_clips = 0
use_muzzle_flash = FALSE
w_class = SIZE_MASSIVE
@@ -40,7 +40,9 @@
/obj/item/hardpoint/secondary/small_flamer,
/obj/item/hardpoint/secondary/towlauncher,
/obj/item/hardpoint/secondary/m56cupola,
- /obj/item/hardpoint/secondary/grenade_launcher
+ /obj/item/hardpoint/secondary/grenade_launcher,
+ /obj/item/hardpoint/secondary/brute_launcher,
+ /obj/item/hardpoint/secondary/united_americas_flag
)
hdpt_layer = HDPT_LAYER_TURRET
@@ -53,19 +55,24 @@
var/gyro = FALSE
- // How long the windup is before the turret rotates
- var/rotation_windup = 15
- // Used during the windup
- var/rotating = FALSE
+ /// Index into accepted_hardpoints, used by /mob/living/carbon/verb/cycle_tank_weapon_hardpoint() to track cycling progress.
+ var/debug_cycle_index = 0
scatter = 4
- gun_firemode = GUN_FIREMODE_BURSTFIRE
- gun_firemode_list = list(
- GUN_FIREMODE_BURSTFIRE,
- )
- burst_amount = 2
- burst_delay = 1.0 SECONDS
- extra_delay = 13.0 SECONDS
+ fire_delay = 7.0 SECONDS
+
+ // Total width, in degrees, of the arc in front of the hull that each star shell can land within.
+ var/flare_spread = 45
+ // Minimum/maximum tiles in front of the hull (not the turret's own facing) each star shell can land.
+ var/flare_range_min = 5
+ var/flare_range_max = 7
+
+/obj/item/hardpoint/holder/tank_turret/Initialize()
+ . = ..()
+ current_angle = dir2angle(dir)
+ desired_angle = current_angle
+ // Loaded shell-by-shell rather than swapped as a whole magazine.
+ ammo_type = null
/obj/item/hardpoint/holder/tank_turret/update_icon()
var/broken = (health <= 0)
@@ -92,11 +99,53 @@
return I
+/**
+ * Layers a continuous visual tilt on top of the turret's (and every mounted weapon's) cardinal
+ * sprite. The base dir still snaps every 90 degrees for sprite selection.
+ */
+/obj/item/hardpoint/holder/tank_turret/get_hardpoint_image()
+ var/list/images = ..()
+
+ var/tilt = angle_delta(current_angle, dir2angle(dir))
+
+ var/image/turret_image = images[1]
+ if(turret_image && tilt)
+ turret_image.transform = build_tilt_matrix(tilt, interpolate_pivot(rotation_pivot, current_angle))
+
+ var/image_index = 2
+ for(var/obj/item/hardpoint/weapon in hardpoints)
+ var/image/weapon_image = images[image_index]
+ image_index++
+ if(!weapon_image)
+ continue
+
+ // A self_gimballed weapon needs TWO nested rotations around TWO genuinely different pivots.
+ // Collapsing these into one combined rotation only works if both
+ // pivots happen to coincide, which isn't the case.
+ if(weapon.self_gimballed)
+ var/own_tilt = angle_delta(weapon.current_angle, current_angle)
+ if(own_tilt || tilt)
+ var/list/inner_pivot = weapon.interpolate_pivot(weapon.gimbal_pivot, current_angle) || weapon.interpolate_pivot(weapon.rotation_pivot, current_angle)
+ var/list/outer_pivot = weapon.interpolate_pivot(weapon.rotation_pivot, current_angle)
+ weapon_image.transform = weapon.build_nested_tilt_matrix(own_tilt, inner_pivot, tilt, outer_pivot)
+ else if(tilt)
+ weapon_image.transform = build_tilt_matrix(tilt, weapon.interpolate_pivot(weapon.rotation_pivot, current_angle))
+
+ return images
+
// no picking this big beast up
/obj/item/hardpoint/holder/tank_turret/attack_hand(mob/user)
return
/obj/item/hardpoint/holder/tank_turret/attackby(obj/item/I, mob/user)
+ if(istype(I, /obj/item/explosive/grenade/high_explosive/airburst/starshell))
+ load_star_shell(I, user)
+ return TRUE
+
+ if(istype(I, /obj/item/storage/box/packet/flare))
+ load_star_shell_packet(I, user)
+ return TRUE
+
if(istype(I, /obj/item/powerloader_clamp))
var/obj/item/powerloader_clamp/PC = I
if(!PC.linked_powerloader)
@@ -113,16 +162,53 @@
return TRUE
..()
+// Loads a single loose star shell directly into the turret's internal holders.
+/obj/item/hardpoint/holder/tank_turret/proc/load_star_shell(obj/item/explosive/grenade/high_explosive/airburst/starshell/shell, mob/user)
+ if(shell.active)
+ to_chat(user, SPAN_WARNING("You can't load a primed star shell into \the [src]!"))
+ return
+ if(ammo.current_rounds >= ammo.max_rounds)
+ to_chat(user, SPAN_WARNING("\The [src]'s shell holders are already full."))
+ return
+ user.visible_message(SPAN_NOTICE("[user] loads \a [shell] into \the [src]."), SPAN_NOTICE("You load \a [shell] into \the [src]'s shell holders."))
+ playsound(loc, 'sound/weapons/gun_shotgun_shell_insert.ogg', 25, TRUE)
+ ammo.current_rounds++
+ qdel(shell)
+
+// Loads as many star shells as fit out of a star shell packet into the turret's internal holders.
+/obj/item/hardpoint/holder/tank_turret/proc/load_star_shell_packet(obj/item/storage/box/packet/flare/packet, mob/user)
+ var/space_left = ammo.max_rounds - ammo.current_rounds
+ if(space_left <= 0)
+ to_chat(user, SPAN_WARNING("\The [src]'s shell holders are already full."))
+ return
+
+ var/list/shells_to_load = list()
+ for(var/obj/item/explosive/grenade/high_explosive/airburst/starshell/shell in packet.contents)
+ shells_to_load += shell
+ if(length(shells_to_load) >= space_left)
+ break
+
+ if(!length(shells_to_load))
+ to_chat(user, SPAN_WARNING("\The [packet] has no star shells left to load."))
+ return
+
+ for(var/obj/item/explosive/grenade/high_explosive/airburst/starshell/shell as anything in shells_to_load)
+ qdel(shell)
+
+ ammo.current_rounds += length(shells_to_load)
+ packet.update_icon()
+ user.visible_message(SPAN_NOTICE("[user] loads [length(shells_to_load)] star shell\s from \the [packet] into \the [src]."), SPAN_NOTICE("You load [length(shells_to_load)] star shell\s from \the [packet] into \the [src]'s shell holders."))
+ playsound(loc, 'sound/weapons/gun_shotgun_shell_insert.ogg', 25, TRUE)
/obj/item/hardpoint/holder/tank_turret/get_tgui_info()
var/list/data = list()
- data += list(list( // turret smokescreen data
- "name" = "M34A2-A Turret Smoke Screen",
+ data += list(list( // turret flare launcher data
+ "name" = "M34A2-A Turret Flare Launcher",
"health" = health <= 0 ? null : floor(get_integrity_percent()),
"uses_ammo" = TRUE,
- "current_rounds" = ammo.current_rounds / 2,
- "max_rounds"= ammo.max_rounds / 2,
+ "current_rounds" = ammo.current_rounds,
+ "max_rounds"= ammo.max_rounds,
"mags" = LAZYLEN(backup_clips),
"max_mags" = max_clips,
))
@@ -141,74 +227,94 @@
gyro = !gyro
to_chat(user, SPAN_NOTICE("You toggle \the [src]'s gyroscopic stabilizer [gyro ? "ON" :"OFF"]."))
-/obj/item/hardpoint/holder/tank_turret/proc/user_rotation(mob/user, deg)
- // no rotating a broken turret
- if(health <= 0)
- return
+/// Keeps dir/origins in sync with current_angle's quadrant, then refreshes the vehicle's sprite.
+/obj/item/hardpoint/holder/tank_turret/apply_current_angle()
+ var/target_cardinal = angle_to_cardinal(current_angle)
+ if(target_cardinal != dir)
+ rotate(turning_angle(dir, target_cardinal), override_gyro = TRUE, sync_angle = FALSE)
+ owner.update_icon()
- if(rotating)
+// See base proc's doc comment (hardpoint.dm) - drags any self_gimballed mounted weapon's angle along with this turret's own, so it keeps riding the turret's motion instead of holding a fixed absolute world angle.
+/obj/item/hardpoint/holder/tank_turret/drag_self_gimballed_weapons(delta)
+ if(!delta)
+ return
+ for(var/obj/item/hardpoint/mounted_weapon in hardpoints)
+ if(!mounted_weapon.self_gimballed)
+ continue
+ mounted_weapon.current_angle = ((mounted_weapon.current_angle + delta) % 360 + 360) % 360
+ mounted_weapon.desired_angle = ((mounted_weapon.desired_angle + delta) % 360 + 360) % 360
+
+/**
+ * Gates firing for a weapon mounted on this turret. current_angle must have caught up to within
+ * FIRING_GATE_TOLERANCE of the angle from tthe weapon's own muzzle to target.
+ */
+/obj/item/hardpoint/holder/tank_turret/proc/in_turret_firing_arc(obj/item/hardpoint/weapon, atom/target)
+ var/turf/muzzle_turf = weapon.get_origin_turf()
+ var/turf/target_turf = get_turf(target)
+
+ //same tile angle is undefined for Get_Angle, returning FALSE to match the legacy static-arc check
+ if(muzzle_turf == target_turf)
+ return FALSE
+
+ var/target_angle = Get_Angle(muzzle_turf, target_turf)
+ return abs(angle_delta(target_angle, current_angle)) <= FIRING_GATE_TOLERANCE
+
+// Recomputes max_angular_velocity from the median traverse_arc of currently-mounted weapons.
+/obj/item/hardpoint/holder/tank_turret/proc/recalculate_turn_rate()
+ var/list/arcs = list()
+ for(var/obj/item/hardpoint/mounted_weapon in hardpoints)
+ if(!mounted_weapon.traverse_arc)
+ continue
+ arcs += mounted_weapon.traverse_arc
+
+ if(!length(arcs))
+ max_angular_velocity = TURRET_DEFAULT_ANGULAR_VELOCITY
return
- rotating = TRUE
- to_chat(user, SPAN_NOTICE("You begin rotating the turret towards the [dir2text(turn(dir,deg))]."))
+ sortTim(arcs)
+ var/weapon_count = length(arcs)
+ var/median_arc = (weapon_count % 2) ? arcs[(weapon_count + 1) / 2] : (arcs[weapon_count / 2] + arcs[weapon_count / 2 + 1]) / 2
- if(!do_after(user, rotation_windup, INTERRUPT_ALL, BUSY_ICON_GENERIC))
- rotating = FALSE
- return
- rotating = FALSE
+ max_angular_velocity = max(TURRET_BASE_ANGULAR_VELOCITY * (median_arc / TURRET_ARC_NORMALIZATION), TURRET_MIN_ANGULAR_VELOCITY)
+
+/obj/item/hardpoint/holder/tank_turret/add_hardpoint(obj/item/hardpoint/new_hardpoint)
+ . = ..()
+ recalculate_turn_rate()
- rotate(deg, TRUE)
+/obj/item/hardpoint/holder/tank_turret/remove_hardpoint(obj/item/hardpoint/removed_hardpoint, turf/uninstall_to)
+ . = ..()
+ recalculate_turn_rate()
-/obj/item/hardpoint/holder/tank_turret/rotate(deg, override_gyro = FALSE)
+/**
+ * override_gyro - bypasses the gyro lock (used by mouse-driven aiming, which should always work).
+ * sync_angle - whether to drag current_angle along by deg. TRUE for hull-turn cascades (rotate_hardpoints),
+ * FALSE when apply_current_angle() calls this to snap dir to a quadrant it already accounted for.
+ */
+/obj/item/hardpoint/holder/tank_turret/rotate(deg, override_gyro = FALSE, sync_angle = TRUE)
if(gyro && !override_gyro)
return
..(deg)
+ if(sync_angle)
+ // deg is in turn()'s rotational convention, which is the opposite
+ // sign of dir2angle()'s north-clockwise convention that current_angle is tracked in
+ // subtract, don't add, or current_angle drifts away from the direction dir actually turned.
+ current_angle = ((current_angle - deg) % 360 + 360) % 360
+ drag_self_gimballed_weapons(-deg)
+
var/obj/vehicle/multitile/tank/C = owner
var/obj/item/hardpoint/support/artillery_module/AM
for(var/obj/item/hardpoint/support/artillery_module/A in C.hardpoints)
AM = A
- if(AM && AM.is_active)
- var/mob/user = C.seats[VEHICLE_GUNNER]
- if(user && user.client)
- user = C.seats[VEHICLE_GUNNER]
- user.client.change_view(AM.view_buff, src)
-
- switch(dir)
- if(NORTH)
- user.client.set_pixel_x(0)
- user.client.set_pixel_y(AM.view_tile_offset * 32)
- if(SOUTH)
- user.client.set_pixel_x(0)
- user.client.set_pixel_y(-1 * AM.view_tile_offset * 32)
- if(EAST)
- user.client.set_pixel_x(AM.view_tile_offset * 32)
- user.client.set_pixel_y(0)
- if(WEST)
- user.client.set_pixel_x(-1 * AM.view_tile_offset * 32)
- user.client.set_pixel_y(0)
+ if(AM)
+ var/mob/living/user = C.seats[VEHICLE_GUNNER]
+ if(user && (user in AM.optics_users))
+ AM.apply_gunner_view(user)
+// lobs a single starshell forwards
/obj/item/hardpoint/holder/tank_turret/try_fire(atom/target, mob/living/user, params)
- var/turf/L
- var/turf/R
- switch(owner.dir)
- if(NORTH)
- L = locate(owner.x - 2, owner.y + 4, owner.z)
- R = locate(owner.x + 2, owner.y + 4, owner.z)
- if(SOUTH)
- L = locate(owner.x + 2, owner.y - 4, owner.z)
- R = locate(owner.x - 2, owner.y - 4, owner.z)
- if(EAST)
- L = locate(owner.x + 4, owner.y + 2, owner.z)
- R = locate(owner.x + 4, owner.y - 2, owner.z)
- else
- L = locate(owner.x - 4, owner.y + 2, owner.z)
- R = locate(owner.x - 4, owner.y - 2, owner.z)
-
- if(shots_fired)
- target = R
- else
- target = L
-
+ var/fire_angle = dir2angle(owner.dir) + rand(-flare_spread * 0.5, flare_spread * 0.5)
+ var/fire_range = rand(flare_range_min, flare_range_max)
+ target = get_angle_target_turf(owner, fire_angle, fire_range)
return ..()
diff --git a/code/modules/vehicles/hardpoints/primary/autocannon.dm b/code/modules/vehicles/hardpoints/primary/autocannon.dm
index 8148e5357dd8..863c7eef5c17 100644
--- a/code/modules/vehicles/hardpoints/primary/autocannon.dm
+++ b/code/modules/vehicles/hardpoints/primary/autocannon.dm
@@ -8,7 +8,7 @@
activation_sounds = list('sound/weapons/vehicles/autocannon_fire.ogg')
health = 500
- firing_arc = 60
+ traverse_arc = 60
ammo = new /obj/item/ammo_magazine/hardpoint/ace_autocannon
max_clips = 2
@@ -19,10 +19,16 @@
"4" = list(32, 0),
"8" = list(-32, 0)
)
+ rotation_pivot = list(
+ "1" = list(0, -22),
+ "2" = list(0, 32),
+ "4" = list(-32, 0),
+ "8" = list(32, 0)
+ )
scatter = 1
gun_firemode = GUN_FIREMODE_AUTOMATIC
gun_firemode_list = list(
GUN_FIREMODE_AUTOMATIC,
)
- fire_delay = 0.7 SECONDS
+ fire_delay = 1.4 SECONDS
diff --git a/code/modules/vehicles/hardpoints/primary/dual_cannon.dm b/code/modules/vehicles/hardpoints/primary/dual_cannon.dm
index 9f4c7286afeb..33e483721644 100644
--- a/code/modules/vehicles/hardpoints/primary/dual_cannon.dm
+++ b/code/modules/vehicles/hardpoints/primary/dual_cannon.dm
@@ -12,7 +12,7 @@
damage_multiplier = 0.2
health = 500
- firing_arc = 60
+ traverse_arc = 60
origins = list(0, 1)
diff --git a/code/modules/vehicles/hardpoints/primary/flamer.dm b/code/modules/vehicles/hardpoints/primary/flamer.dm
index 60a7979cd5fb..e541d6dc643c 100644
--- a/code/modules/vehicles/hardpoints/primary/flamer.dm
+++ b/code/modules/vehicles/hardpoints/primary/flamer.dm
@@ -8,23 +8,37 @@
activation_sounds = list('sound/weapons/vehicles/flamethrower.ogg')
health = 400
- firing_arc = 90
+ traverse_arc = 90
ammo = new /obj/item/ammo_magazine/hardpoint/primary_flamer
max_clips = 1
px_offsets = list(
- "1" = list(0, 21),
- "2" = list(0, -32),
+ "1" = list(0, 27),
+ "2" = list(0, -26),
"4" = list(32, 1),
"8" = list(-32, 1)
)
+ rotation_pivot = list(
+ "1" = list(0, -27),
+ "2" = list(0, 26),
+ "4" = list(-32, -1),
+ "8" = list(32, -1)
+ )
use_muzzle_flash = FALSE
scatter = 5
fire_delay = 2.0 SECONDS
+ // Range for FLAME_MODE_STREAM
+ var/max_range = 8
+ var/flame_mode = FLAME_MODE_GLOB
+ // Flat fuel cost for a single FLAME_MODE_GLOB shot
+ var/glob_fuel_cost = 5
+ // The reagent-based ammo/fuel deduction is done manually in handle_fire() below
+ ammo_cost_per_shot = 0
+
/obj/item/hardpoint/primary/flamer/set_bullet_traits()
..()
LAZYADD(traits_to_give, list(
@@ -37,3 +51,39 @@
return NONE
return ..()
+
+// Toggles between FLAME_MODE_GLOB and FLAME_MODE_STREAM
+/obj/item/hardpoint/primary/flamer/toggle_fire_mode(mob/user)
+ flame_mode = (flame_mode == FLAME_MODE_STREAM) ? FLAME_MODE_GLOB : FLAME_MODE_STREAM
+ to_chat(user, SPAN_NOTICE("You switch \the [src] to [flame_mode == FLAME_MODE_STREAM ? "stream" : "glob shot"] mode."))
+
+/obj/item/hardpoint/primary/flamer/handle_fire(atom/target, mob/living/user, params)
+ if(flame_mode == FLAME_MODE_STREAM)
+ return fire_flame_stream(target, user, max_range, FLAMESHAPE_TRIANGLE)
+
+ var/datum/reagent/chem = LAZYACCESS(ammo.reagents?.reagent_list, 1)
+ if(!chem || ammo.reagents.get_reagent_amount(chem.id) < glob_fuel_cost)
+ click_empty(user)
+ return NONE
+ ammo.reagents.remove_reagent(chem.id, glob_fuel_cost)
+ sync_ammo_from_reagents()
+ return ..()
+
+// Tags the freshly-generated glob projectile's ammo datum with the id of whichever fuel is actually loaded
+
+/obj/item/hardpoint/primary/flamer/generate_bullet(mob/user, turf/origin_turf)
+ . = ..()
+ var/obj/projectile/fired = .
+ if(istype(fired?.ammo, /datum/ammo/flamethrower/tank_flamer))
+ var/datum/ammo/flamethrower/tank_flamer/glob_ammo = fired.ammo
+ var/datum/reagent/chem = LAZYACCESS(ammo.reagents?.reagent_list, 1)
+ glob_ammo.loaded_chem_id = chem?.id
+
+// Layers an overlay on the mounted/turret sprite, colored via mix_color_from_reagents() to match whatever fuel is currently loaded
+/obj/item/hardpoint/primary/flamer/get_icon_image(x_offset, y_offset, new_dir)
+ var/image/base_image = ..()
+ var/image/strip = image(icon = disp_icon, icon_state = "drgn_flamer_strip", dir = new_dir)
+ if(ammo?.reagents && length(ammo.reagents.reagent_list))
+ strip.color = mix_color_from_reagents(ammo.reagents.reagent_list)
+ base_image.overlays += strip
+ return base_image
diff --git a/code/modules/vehicles/hardpoints/primary/ltb.dm b/code/modules/vehicles/hardpoints/primary/ltb.dm
index 6c818f30b8d2..02af0955eff8 100644
--- a/code/modules/vehicles/hardpoints/primary/ltb.dm
+++ b/code/modules/vehicles/hardpoints/primary/ltb.dm
@@ -1,3 +1,6 @@
+/// Low end of the LTB's aiming laser opacity ramp
+#define LTB_LASER_MIN_ALPHA 50
+
/obj/item/hardpoint/primary/cannon
name = "\improper LTB Cannon"
desc = "A primary cannon for tanks that shoots explosive rounds."
@@ -8,10 +11,10 @@
activation_sounds = list('sound/weapons/vehicles/cannon_fire1.ogg', 'sound/weapons/vehicles/cannon_fire2.ogg')
health = 500
- firing_arc = 60
+ traverse_arc = 60
- ammo = new /obj/item/ammo_magazine/hardpoint/ltb_cannon
- max_clips = 3
+ ammo = new /obj/item/ammo_magazine/hardpoint/ltb_cannon/he_shell
+ max_clips = 1
px_offsets = list(
"1" = list(0, 21),
@@ -19,6 +22,12 @@
"4" = list(32, 0),
"8" = list(-32, 0)
)
+ rotation_pivot = list(
+ "1" = list(0, -21),
+ "2" = list(0, 32),
+ "4" = list(-32, 0),
+ "8" = list(32, 0)
+ )
muzzle_flash_pos = list(
"1" = list(0, 59),
@@ -28,4 +37,161 @@
)
scatter = 2
- fire_delay = 20.0 SECONDS
+ fire_delay = 15.0 SECONDS
+
+ /// How long the lock-on charge takes before firing is allowed.
+ var/f_aiming_time = 4 SECONDS
+ /// Stashed for generate_bullet() below, which doesn't receive target directly.
+ var/atom/homing_target
+
+ var/image/lockon_icon
+ var/image/lockon_direction_icon
+ var/lockon_pixel_x = 0
+ var/lockon_pixel_y = 0
+ var/datum/beam/laser_beam
+ var/obj/effect/beam_anchor/anchor
+ /// Incrementted every start_aim_visuals(), lets a stale cycle_laser_color() invocation from a
+ /// previous charge recognize it's outdated and stop, even if a new charge starts before it wakes
+ /// up from its own sleep() and notices laser beam has already been replaced. fixes a very annoying bug where more than one reticle can appear.
+ var/aim_generation = 0
+ /// world.time the current charge started - used to ramp the beam's opacity up gradually across the whole charge
+ var/charge_start_time = 0
+
+/**
+ * Same lock-on/tracking charge as the secondary BRUTE launcher. Targets any living mob, blocked by the same things Aimed Shot is
+ * (dense+opaque turfs, opaque objects, obscuring smoke...
+ * The fired round homes in on the target same as Aimed Shot's own /datum/component/homing_projectile.
+ */
+/obj/item/hardpoint/primary/cannon/handle_fire(atom/target, mob/living/user, params)
+ if(charging)
+ SEND_SIGNAL(src, COMSIG_GUN_INTERRUPT_FIRE)
+ return NONE
+
+ if(!isliving(target))
+ to_chat(user, SPAN_WARNING("Invalid target!"))
+ return NONE
+
+ var/mob/living/living_target = target
+ if(living_target.is_dead())
+ to_chat(user, SPAN_WARNING("Invalid target!"))
+ return NONE
+
+ if(check_shot_is_blocked(target))
+ to_chat(user, SPAN_WARNING("Target obscured!"))
+ return NONE
+
+ if(!track_and_charge(target, user, f_aiming_time))
+ return NONE
+
+ // Re-validate once more after the charge
+ if(QDELETED(target) || living_target.is_dead() || check_shot_is_blocked(target))
+ to_chat(user, SPAN_WARNING("You lose the shot!"))
+ return NONE
+
+ homing_target = target
+ . = ..()
+ homing_target = null
+ return .
+
+/obj/item/hardpoint/primary/cannon/generate_bullet(mob/user, turf/origin_turf)
+ var/obj/projectile/fired_projectile = ..()
+ if(homing_target)
+ fired_projectile.AddComponent(/datum/component/homing_projectile, homing_target, user)
+ return fired_projectile
+
+/obj/item/hardpoint/primary/cannon/proc/check_shot_is_blocked(atom/target)
+ var/turf/origin_turf = get_origin_turf()
+ var/turf/target_turf = get_turf(target)
+ var/list/turf/path = get_line(origin_turf, target_turf, include_start_atom = FALSE)
+ if(!length(path))
+ return TRUE
+
+ for(var/turf/path_turf in path)
+ if(path_turf.density && path_turf.opacity)
+ return TRUE
+
+ for(var/obj/path_obj in path_turf)
+ if(path_obj.get_projectile_hit_boolean(null) && path_obj.opacity)
+ return TRUE
+
+ for(var/obj/effect/particle_effect/smoke/smoke in path_turf)
+ if(smoke.obscuring)
+ return TRUE
+
+ return FALSE
+
+/obj/item/hardpoint/primary/cannon/start_aim_visuals(atom/target, mob/living/user)
+ anchor = new(get_origin_turf())
+ charge_start_time = world.time
+
+ var/mob/living_target = target
+ lockon_pixel_x = -living_target.pixel_x + living_target.base_pixel_x
+ lockon_pixel_y = (living_target.icon_size - world.icon_size) * 0.5 - living_target.pixel_y + living_target.base_pixel_y
+
+ apply_beam_visuals(target, TRUE)
+ aim_generation++
+ INVOKE_ASYNC(src, PROC_REF(cycle_laser_color), target, aim_generation)
+
+/**
+ * Recolors the (already-built) reticle icons and rebuilds the beam using the AMR's focused-fire beam,
+ * the flicker alternates between this sprite's native blue and a darker shade of the same blue.
+ *
+ * Also refreshes the direction arrow's facing (the target/tank can have moved since the last cycle)
+ * and ramps the beam's opacity based on total elapsed charge time (charge_start_time).
+ *
+ * The intent, with the flickering, and the blue color of the AMR focused shot, is to make it clear for xenos that
+ * they are being targeted by something dangerous.
+ */
+/obj/item/hardpoint/primary/cannon/proc/apply_beam_visuals(atom/target, use_intense)
+ var/turf/origin_turf = get_origin_turf()
+
+ if(lockon_icon)
+ target.overlays -= lockon_icon
+ if(lockon_direction_icon)
+ target.overlays -= lockon_direction_icon
+
+ lockon_icon = image(icon = 'icons/effects/Targeted.dmi', icon_state = use_intense ? "sniper_lockon_intense" : "sniper_lockon")
+ lockon_icon.pixel_x = lockon_pixel_x
+ lockon_icon.pixel_y = lockon_pixel_y
+ target.overlays += lockon_icon
+
+ lockon_direction_icon = image(icon = 'icons/effects/Targeted.dmi', icon_state = use_intense ? "sniper_lockon_intense_direction" : "sniper_lockon_direction", dir = get_cardinal_dir(target, origin_turf))
+ lockon_direction_icon.pixel_x = lockon_pixel_x
+ lockon_direction_icon.pixel_y = lockon_pixel_y
+ target.overlays += lockon_direction_icon
+
+ QDEL_NULL(laser_beam)
+ laser_beam = anchor.beam(target, use_intense ? "laser_beam_intense" : "laser_beam", 'icons/effects/beam.dmi', (f_aiming_time + 1 SECONDS), beam_type = /obj/effect/ebeam/laser/intense/tank_mounted)
+
+ var/elapsed = world.time - charge_start_time
+ var/current_progress = clamp(elapsed / f_aiming_time, 0, 1)
+ var/next_progress = clamp((elapsed + 5) / f_aiming_time, 0, 1)
+ laser_beam.visuals.alpha = LTB_LASER_MIN_ALPHA + (255 - LTB_LASER_MIN_ALPHA) * current_progress
+ animate(laser_beam.visuals, alpha = LTB_LASER_MIN_ALPHA + (255 - LTB_LASER_MIN_ALPHA) * next_progress, 0.5 SECONDS, easing = SINE_EASING|EASE_OUT)
+
+/// Flickers the reticle/beam between red and blue about twice a second while charging
+/obj/item/hardpoint/primary/cannon/proc/cycle_laser_color(atom/locked_target, my_generation)
+ var/use_intense = TRUE
+ while(laser_beam && !QDELETED(laser_beam) && !QDELETED(locked_target) && aim_generation == my_generation)
+ sleep(5)
+ if(!laser_beam || QDELETED(laser_beam) || QDELETED(locked_target) || aim_generation != my_generation)
+ break
+ use_intense = !use_intense
+ apply_beam_visuals(locked_target, use_intense)
+
+/obj/item/hardpoint/primary/cannon/on_track_tick(atom/target)
+ if(!anchor)
+ return
+ var/turf/origin_turf = get_origin_turf()
+ if(anchor.loc != origin_turf)
+ anchor.forceMove(origin_turf)
+
+/obj/item/hardpoint/primary/cannon/end_aim_visuals(atom/target, mob/living/user, success)
+ if(lockon_icon)
+ target.overlays -= lockon_icon
+ lockon_icon = null
+ if(lockon_direction_icon)
+ target.overlays -= lockon_direction_icon
+ lockon_direction_icon = null
+ QDEL_NULL(laser_beam)
+ QDEL_NULL(anchor)
diff --git a/code/modules/vehicles/hardpoints/primary/minigun.dm b/code/modules/vehicles/hardpoints/primary/minigun.dm
index fd1f81990445..b7a9d923e95f 100644
--- a/code/modules/vehicles/hardpoints/primary/minigun.dm
+++ b/code/modules/vehicles/hardpoints/primary/minigun.dm
@@ -7,7 +7,7 @@
disp_icon_state = "ltaaap_minigun"
health = 350
- firing_arc = 90
+ traverse_arc = 90
ammo = new /obj/item/ammo_magazine/hardpoint/ltaaap_minigun
max_clips = 1
@@ -18,6 +18,12 @@
"4" = list(32, 0),
"8" = list(-32, 0)
)
+ rotation_pivot = list(
+ "1" = list(0, -21),
+ "2" = list(0, 32),
+ "4" = list(-32, 0),
+ "8" = list(32, 0)
+ )
muzzle_flash_pos = list(
"1" = list(0, 57),
@@ -26,7 +32,7 @@
"8" = list(-77, 0)
)
- scatter = 18 //base scatter, modified by stake_delay_mult
+ scatter = 2
gun_firemode = GUN_FIREMODE_AUTOMATIC
gun_firemode_list = list(
GUN_FIREMODE_AUTOMATIC,
@@ -35,7 +41,7 @@
activation_sounds = list('sound/weapons/gun_minigun.ogg')
/// Active firing time to reach max spin_stage.
- var/spinup_time = 10 SECONDS
+ var/spinup_time = 3 SECONDS
/// Grace period before losing spin_stage.
var/spindown_grace_time = 2 SECONDS
COOLDOWN_DECLARE(spindown_grace_cooldown)
@@ -43,8 +49,9 @@
var/spindown_time = 3 SECONDS
/// Index of stage_rate.
var/spin_stage = 1
- /// Shots fired per fire_delay at a particular spin_stage.
- var/list/stage_rate = list(1, 1, 2, 2, 3, 3, 3, 4, 4, 4, 5)
+ /// this is meant to be an already-fearsome close range weapon even before it spins
+ /// all the way up, not a weapon that needs several seconds of commitment before it does anything.
+ var/list/stage_rate = list(3, 3, 4, 4, 4, 5, 5, 5, 5, 5, 5)
/// Fire delay multiplier for current spin_stage.
var/stage_delay_mult = 1
/// When it was last fired, related to world.time.
@@ -52,7 +59,7 @@
/obj/item/hardpoint/primary/minigun/set_fire_delay(value)
fire_delay = value
- SEND_SIGNAL(src, COMSIG_GUN_AUTOFIREDELAY_MODIFIED, fire_delay * stage_delay_mult, scatter * stage_delay_mult)
+ SEND_SIGNAL(src, COMSIG_GUN_AUTOFIREDELAY_MODIFIED, fire_delay * stage_delay_mult)
/obj/item/hardpoint/primary/minigun/set_fire_cooldown()
calculate_stage_delay_mult() //needs to check grace_cooldown before refreshed
@@ -79,7 +86,7 @@
var/new_stage_rate = stage_rate[floor(spin_stage)]
if(old_stage_rate != new_stage_rate)
- scatter = initial(scatter) * (1/new_stage_rate)
+ // deliberate low scatter at every stage instead of the old huge-scatter-while-spinning-up behavior.
stage_delay_mult = 1 / new_stage_rate
SEND_SIGNAL(src, COMSIG_GUN_AUTOFIREDELAY_MODIFIED, fire_delay * stage_delay_mult)
diff --git a/code/modules/vehicles/hardpoints/secondary/brute_launcher.dm b/code/modules/vehicles/hardpoints/secondary/brute_launcher.dm
new file mode 100644
index 000000000000..164d7fd2a484
--- /dev/null
+++ b/code/modules/vehicles/hardpoints/secondary/brute_launcher.dm
@@ -0,0 +1,118 @@
+/obj/item/hardpoint/secondary/brute_launcher
+ name = "\improper M6H-BRUTE Launcher"
+ desc = "A tank-mounted variant of the M6H-BRUTE breaching rocket system. Laser-guides a 90mm shaped-charge rocket onto a fortified position, given a few seconds to lock on."
+
+ icon_state = "brute_launcher"
+ disp_icon = "tank"
+ disp_icon_state = "brutelauncher"
+
+ health = 500
+ traverse_arc = 120
+ activation_sounds = list('sound/weapons/gun_rocketlauncher.ogg')
+
+ // reuses the handheld BRUTE launcher's own rocket item directly
+ // It's very silly that the M56D also needed its own 'vehicle magazines' instead of just using the default drum. This has properly been changed.
+ ammo = new /obj/item/ammo_magazine/rocket/brute
+ max_clips = 4
+
+ px_offsets = list(
+ "1" = list(1, 10),
+ "2" = list(-1, 5),
+ "4" = list(0, 0),
+ "8" = list(0, 18)
+ )
+ rotation_pivot = list(
+ "1" = list(-1, -10),
+ "2" = list(1, -5),
+ "4" = list(0, 0),
+ "8" = list(0, -18)
+ )
+ gimbal_pivot = list(
+ "1" = list(6, -6),
+ "2" = list(-6, 6),
+ "4" = list(-6, 6),
+ "8" = list(6, 6)
+ )
+ muzzle_flash_pos = list(
+ "1" = list(8, -1),
+ "2" = list(-8, -16),
+ "4" = list(5, -8),
+ "8" = list(-5, 10)
+ )
+
+ scatter = 4 // irrelevant - /datum/ammo/rocket/brute sets AMMO_HITS_TARGET_TURF, which skips scatter entirely
+ fire_delay = 15.0 SECONDS
+
+ // How long the lock-on charge takes before firing is allowed.
+ var/f_aiming_time = 5 SECONDS
+
+ var/image/lockon_icon
+ var/image/lockon_direction_icon
+ var/datum/beam/laser_beam
+ var/obj/effect/beam_anchor/anchor
+
+/obj/item/hardpoint/secondary/brute_launcher/handle_fire(atom/target, mob/living/user, params)
+ if(charging)
+ SEND_SIGNAL(src, COMSIG_GUN_INTERRUPT_FIRE)
+ return NONE
+
+ if(!(istype(target, /obj/structure) || istype(target, /turf/closed/wall)))
+ to_chat(user, SPAN_WARNING("Invalid target!"))
+ return NONE
+
+ var/turf/origin_turf = get_origin_turf()
+ var/list/turf/path = get_line(origin_turf, get_turf(target), include_start_atom = FALSE)
+ for(var/turf/turf_path in path)
+ if(turf_path.opacity && turf_path != target)
+ to_chat(user, SPAN_WARNING("Target obscured!"))
+ return NONE
+
+ if(!track_and_charge(target, user, f_aiming_time))
+ return NONE
+
+ return ..()
+
+/obj/item/hardpoint/secondary/brute_launcher/start_aim_visuals(atom/target, mob/living/user)
+ var/turf/origin_turf = get_origin_turf()
+
+ lockon_icon = image(icon = 'icons/effects/Targeted.dmi', icon_state = "sniper_lockon_guided")
+ target.overlays += lockon_icon
+
+ lockon_direction_icon = image(icon = 'icons/effects/Targeted.dmi', icon_state = "sniper_lockon_guided_direction", dir = get_cardinal_dir(target, origin_turf))
+ target.overlays += lockon_direction_icon
+
+ anchor = new(origin_turf)
+ laser_beam = anchor.beam(target, "laser_beam_guided", 'icons/effects/beam.dmi', (f_aiming_time + 1 SECONDS), beam_type = /obj/effect/ebeam/laser/intense/tank_mounted)
+ laser_beam.visuals.alpha = 0
+ animate(laser_beam.visuals, alpha = initial(laser_beam.visuals.alpha), f_aiming_time, easing = SINE_EASING|EASE_OUT)
+
+/obj/item/hardpoint/secondary/brute_launcher/on_track_tick(atom/target)
+ if(!anchor)
+ return
+ var/turf/origin_turf = get_origin_turf()
+ if(anchor.loc != origin_turf)
+ anchor.forceMove(origin_turf)
+
+/obj/item/hardpoint/secondary/brute_launcher/end_aim_visuals(atom/target, mob/living/user, success)
+ if(lockon_icon)
+ target.overlays -= lockon_icon
+ lockon_icon = null
+ if(lockon_direction_icon)
+ target.overlays -= lockon_direction_icon
+ lockon_direction_icon = null
+ QDEL_NULL(laser_beam)
+ QDEL_NULL(anchor)
+
+// Plain /obj/effect/ebeam/laser/intense defaults to the base /obj layer, which renders underneath the
+// tank's own ABOVE_MOB_LAYER (4.1) sprite, so, we bump it above it.
+/obj/effect/ebeam/laser/intense/tank_mounted
+ layer = 4.501
+
+// Small invisible movable used solely as a beam origin that actually moves with the muzzle
+/obj/effect/beam_anchor
+ name = ""
+ icon = null
+ anchored = FALSE
+ density = FALSE
+ invisibility = INVISIBILITY_MAXIMUM
+ mouse_opacity = MOUSE_OPACITY_TRANSPARENT
diff --git a/code/modules/vehicles/hardpoints/secondary/cupola.dm b/code/modules/vehicles/hardpoints/secondary/cupola.dm
index cf32340070b0..c4e185fdba9f 100644
--- a/code/modules/vehicles/hardpoints/secondary/cupola.dm
+++ b/code/modules/vehicles/hardpoints/secondary/cupola.dm
@@ -8,11 +8,19 @@
activation_sounds = list('sound/weapons/gun_smartgun1.ogg', 'sound/weapons/gun_smartgun2.ogg', 'sound/weapons/gun_smartgun3.ogg', 'sound/weapons/gun_smartgun4.ogg')
health = 350
- firing_arc = 120
+ traverse_arc = 120
- ammo = new /obj/item/ammo_magazine/hardpoint/m56_cupola
+ // Uses the real M56D drum magazine.
+ ammo = new /obj/item/ammo_magazine/m56d
max_clips = 1
+ gimbal_pivot = list(
+ "1" = list(8, 4),
+ "2" = list(-7, 11),
+ "4" = list(-6, 6),
+ "8" = list(5, 24)
+ )
+
muzzle_flash_pos = list(
"1" = list(8, -1),
"2" = list(-7, -15),
diff --git a/code/modules/vehicles/hardpoints/secondary/flag.dm b/code/modules/vehicles/hardpoints/secondary/flag.dm
new file mode 100644
index 000000000000..6f3c03b46178
--- /dev/null
+++ b/code/modules/vehicles/hardpoints/secondary/flag.dm
@@ -0,0 +1,151 @@
+#define PLANTED_FLAG_BUFF 4
+#define PLANTED_FLAG_RANGE 11
+
+/obj/item/hardpoint/secondary/united_americas_flag
+ name = "\improper Mounted UA Flag"
+ desc = "An United Americas flag on a mounting support for the M34A2-A Multipurpose Turret. You feel a burst of energy by its mere sight."
+
+ icon_state = "mounted_flag"
+ disp_icon = "tank"
+ disp_icon_state = "mountedflag"
+ activatable = FALSE // Cannot be toggled - always active when mounted
+ health = 300
+ use_muzzle_flash = FALSE
+ var/buff_range = PLANTED_FLAG_RANGE
+ var/buff_strength = PLANTED_FLAG_BUFF
+ var/faction = FACTION_MARINE
+ var/flag_active = FALSE
+ var/datum/shape/range_bounds
+ var/luminosity_strength = 3
+
+ /// lets a fitted tank be selected as a laser/airstrike aiming point, same system as a planted flag turret
+ var/datum/cas_signal/signal
+ /// Dedicated per-tank-flag counter for the display name below
+ var/static/tank_flag_count = 0
+ var/flag_number
+
+ px_offsets = list(
+ "1" = list(0, 0),
+ "2" = list(0, 1),
+ "4" = list(0, 0),
+ "8" = list(0, 1)
+ )
+
+
+/obj/item/hardpoint/secondary/united_americas_flag/Destroy()
+ if(flag_active)
+ deactivate_flag()
+ range_bounds = null
+ return ..()
+
+/obj/item/hardpoint/secondary/united_americas_flag/on_install(obj/vehicle/multitile/vehicle)
+ ..()
+ activate_flag()
+
+/obj/item/hardpoint/secondary/united_americas_flag/on_uninstall(obj/vehicle/multitile/vehicle)
+ if(flag_active)
+ deactivate_flag()
+ ..()
+
+/obj/item/hardpoint/secondary/united_americas_flag/proc/activate_flag()
+ if(flag_active)
+ return
+ if(!owner)
+ return
+ flag_active = TRUE
+ set_light(luminosity_strength)
+ start_processing()
+ activate_signal()
+
+/obj/item/hardpoint/secondary/united_americas_flag/proc/deactivate_flag()
+ if(!flag_active)
+ return
+
+ flag_active = FALSE
+ set_light(0)
+ stop_processing()
+ range_bounds = null
+ deactivate_signal()
+
+/obj/item/hardpoint/secondary/united_americas_flag/proc/activate_signal()
+ if(signal || !owner)
+ return
+ if(!faction || !GLOB.cas_groups[faction])
+ return
+
+ if(!flag_number)
+ flag_number = ++tank_flag_count
+
+ // signal_loc = owner (the tank itself, a movable atom) rather than a snapshotted turf
+ signal = new(owner)
+ signal.target_id = ++GLOB.cas_tracking_id_increment
+ signal.name = "TANK-[flag_number < 10 ? "0[flag_number]" : "[flag_number]"]"
+ signal.linked_cam = new(get_turf(owner), signal.name)
+ GLOB.cas_groups[faction].add_signal(signal)
+
+/obj/item/hardpoint/secondary/united_americas_flag/proc/deactivate_signal()
+ if(!signal)
+ return
+ if(faction && GLOB.cas_groups[faction])
+ GLOB.cas_groups[faction].remove_signal(signal)
+ QDEL_NULL(signal)
+
+/obj/item/hardpoint/secondary/united_americas_flag/proc/start_processing()
+ START_PROCESSING(SSobj, src)
+
+/obj/item/hardpoint/secondary/united_americas_flag/proc/stop_processing()
+ STOP_PROCESSING(SSobj, src)
+
+/obj/item/hardpoint/secondary/united_americas_flag/process()
+ if(!flag_active || !owner)
+ stop_processing()
+ return
+
+ if(health <= 0)
+ deactivate_flag()
+ return
+
+ apply_area_effect()
+
+/obj/item/hardpoint/secondary/united_americas_flag/proc/apply_area_effect()
+ if(!owner || !flag_active)
+ return
+
+ // Update range bounds to follow the vehicle
+ var/turf/owner_turf = get_turf(owner)
+ if(!owner_turf)
+ return
+
+ // Keep the CAS signal's z-lock in sync with the tank's real position
+ if(signal && signal.z_initial != owner_turf.z)
+ signal.z_initial = owner_turf.z
+
+ // Keep the CAS camera view following the tank.
+ if(signal?.linked_cam && signal.linked_cam.loc != owner_turf)
+ signal.linked_cam.forceMove(owner_turf)
+
+ range_bounds = SQUARE(owner_turf.x, owner_turf.y, buff_range)
+
+ var/list/targets = SSquadtree.players_in_range(range_bounds, owner_turf.z, QTREE_SCAN_MOBS | QTREE_FILTER_LIVING)
+ if(!targets)
+ return
+
+ for(var/mob/living/carbon/human/H in targets)
+ // Check if the human is of the correct faction
+ if(!H.faction || !(faction in H.faction_group))
+ continue
+
+ apply_buff_to_player(H)
+
+/obj/item/hardpoint/secondary/united_americas_flag/proc/apply_buff_to_player(mob/living/carbon/human/H)
+ H.activate_order_buff(COMMAND_ORDER_HOLD, buff_strength, 5 SECONDS)
+ H.activate_order_buff(COMMAND_ORDER_FOCUS, buff_strength, 5 SECONDS)
+ H.activate_order_buff(COMMAND_ORDER_MOVE, buff_strength, 5 SECONDS)
+
+/obj/item/hardpoint/secondary/united_americas_flag/on_destroy()
+ if(flag_active)
+ deactivate_flag()
+ ..()
+
+#undef PLANTED_FLAG_BUFF
+#undef PLANTED_FLAG_RANGE
diff --git a/code/modules/vehicles/hardpoints/secondary/flamer.dm b/code/modules/vehicles/hardpoints/secondary/flamer.dm
index 56a9995b60c5..661c9724667a 100644
--- a/code/modules/vehicles/hardpoints/secondary/flamer.dm
+++ b/code/modules/vehicles/hardpoints/secondary/flamer.dm
@@ -8,7 +8,7 @@
activation_sounds = list('sound/weapons/vehicles/flamethrower.ogg')
health = 300
- firing_arc = 120
+ traverse_arc = 120
ammo = new /obj/item/ammo_magazine/hardpoint/secondary_flamer
max_clips = 1
@@ -23,10 +23,28 @@
"4" = list(3, 0),
"8" = list(-3, 18)
)
+ rotation_pivot = list(
+ "1" = list(-2, -14),
+ "2" = list(2, -3),
+ "4" = list(-3, 0),
+ "8" = list(3, -18)
+ )
+
+ gimbal_pivot = list(
+ "1" = list(6, -6),
+ "2" = list(-6, 6),
+ "4" = list(-6, 6),
+ "8" = list(6, 6)
+ )
scatter = 6
fire_delay = 3.0 SECONDS
+ var/flame_mode = FLAME_MODE_STREAM
+ var/glob_fuel_cost = 3
+ /// The reagent-based ammo/fuel deduction is done manually in handle_fire()
+ ammo_cost_per_shot = 0
+
/obj/item/hardpoint/secondary/small_flamer/try_fire(atom/target, mob/living/user, params)
if(get_turf(target) in owner.locs)
to_chat(user, SPAN_WARNING("The target is too close."))
@@ -34,24 +52,29 @@
return ..()
-/obj/item/hardpoint/secondary/small_flamer/handle_fire(atom/target, mob/living/user, params)
- //step forward along path so flame starts outside hull
- var/list/turfs = get_line(get_origin_turf(), get_turf(target))
- var/turf/origin_turf
- for(var/turf/turf as anything in turfs)
- if(turf in owner.locs)
- continue
- origin_turf = turf
- break
- var/distance = get_dist(origin_turf, get_turf(target))
- var/fire_amount = min(ammo.current_rounds, distance+1, max_range)
- ammo.current_rounds -= fire_amount
+// Toggles between FLAME_MODE_STREAM and FLAME_MODE_GLOB
+/obj/item/hardpoint/secondary/small_flamer/toggle_fire_mode(mob/user)
+ flame_mode = (flame_mode == FLAME_MODE_STREAM) ? FLAME_MODE_GLOB : FLAME_MODE_STREAM
+ to_chat(user, SPAN_NOTICE("You switch \the [src] to [flame_mode == FLAME_MODE_STREAM ? "stream" : "glob shot"] mode."))
- new /obj/flamer_fire(origin_turf, create_cause_data(initial(name), user), null, fire_amount, null, FLAMESHAPE_LINE, target)
-
- play_firing_sounds()
+/obj/item/hardpoint/secondary/small_flamer/handle_fire(atom/target, mob/living/user, params)
+ if(flame_mode == FLAME_MODE_STREAM)
+ return fire_flame_stream(target, user, max_range)
- COOLDOWN_START(src, fire_cooldown, fire_delay)
+ var/datum/reagent/chem = LAZYACCESS(ammo.reagents?.reagent_list, 1)
+ if(!chem || ammo.reagents.get_reagent_amount(chem.id) < glob_fuel_cost)
+ click_empty(user)
+ return NONE
+ ammo.reagents.remove_reagent(chem.id, glob_fuel_cost)
+ sync_ammo_from_reagents()
+ return ..()
- return AUTOFIRE_CONTINUE
+// Tags the freshly-generated glob projectile's ammo datum with the id of whichever fuel is actually loaded
+/obj/item/hardpoint/secondary/small_flamer/generate_bullet(mob/user, turf/origin_turf)
+ . = ..()
+ var/obj/projectile/fired = .
+ if(istype(fired?.ammo, /datum/ammo/flamethrower/tank_flamer_secondary))
+ var/datum/ammo/flamethrower/tank_flamer_secondary/glob_ammo = fired.ammo
+ var/datum/reagent/chem = LAZYACCESS(ammo.reagents?.reagent_list, 1)
+ glob_ammo.loaded_chem_id = chem?.id
diff --git a/code/modules/vehicles/hardpoints/secondary/frontal_cannon.dm b/code/modules/vehicles/hardpoints/secondary/frontal_cannon.dm
index 57ce6e2e5ff8..9ca5ec2a3237 100644
--- a/code/modules/vehicles/hardpoints/secondary/frontal_cannon.dm
+++ b/code/modules/vehicles/hardpoints/secondary/frontal_cannon.dm
@@ -11,7 +11,7 @@
damage_multiplier = 0.11
health = 350
- firing_arc = 120
+ traverse_arc = 120
origins = list(0, -1)
diff --git a/code/modules/vehicles/hardpoints/secondary/grenade_launcher.dm b/code/modules/vehicles/hardpoints/secondary/grenade_launcher.dm
index ee06e7405e07..486350bee25b 100644
--- a/code/modules/vehicles/hardpoints/secondary/grenade_launcher.dm
+++ b/code/modules/vehicles/hardpoints/secondary/grenade_launcher.dm
@@ -8,7 +8,7 @@
activation_sounds = list('sound/weapons/gun_m92_attachable.ogg')
health = 500
- firing_arc = 90
+ traverse_arc = 120
ammo = new /obj/item/ammo_magazine/hardpoint/tank_glauncher
max_clips = 3
@@ -22,6 +22,20 @@
"8" = list(-6, 17)
)
+ rotation_pivot = list(
+ "1" = list(0, -17),
+ "2" = list(0, 0),
+ "4" = list(-6, 0),
+ "8" = list(6, -17)
+ )
+
+ gimbal_pivot = list(
+ "1" = list(6, -6),
+ "2" = list(-6, 6),
+ "4" = list(-6, 6),
+ "8" = list(6, 6)
+ )
+
scatter = 10
fire_delay = 3.0 SECONDS
diff --git a/code/modules/vehicles/hardpoints/secondary/secondary.dm b/code/modules/vehicles/hardpoints/secondary/secondary.dm
index 366c7fcfa9fb..61114ca14c04 100644
--- a/code/modules/vehicles/hardpoints/secondary/secondary.dm
+++ b/code/modules/vehicles/hardpoints/secondary/secondary.dm
@@ -7,3 +7,48 @@
damage_multiplier = 0.125
activatable = TRUE
+
+/**
+ * Toggles control of this secondary between the gunner (default,, follows the turret's shared
+ * rotation like any other mounted weapon) and the driver (self-gimballed, its own independent unrestricted 360-degree mouse-aim
+ */
+/obj/item/hardpoint/secondary/proc/toggle_slaved_to_driver(mob/user)
+ if(health <= 0)
+ to_chat(user, SPAN_WARNING("\The [src]'s controls are busted!"))
+ return
+
+ var/obj/item/hardpoint/holder/tank_turret/turret = loc
+ if(!istype(turret))
+ return
+
+ self_gimballed = !self_gimballed
+ SEND_SIGNAL(src, COMSIG_GUN_INTERRUPT_FIRE)
+
+ if(self_gimballed)
+ allowed_seat = VEHICLE_DRIVER
+ max_angular_velocity = max(TURRET_BASE_ANGULAR_VELOCITY * (traverse_arc / TURRET_ARC_NORMALIZATION), TURRET_MIN_ANGULAR_VELOCITY)
+ current_angle = dir2angle(dir)
+ desired_angle = current_angle
+ angular_velocity = 0
+ rotation_active = FALSE
+
+ if(owner.active_hp[VEHICLE_GUNNER] == src)
+ owner.active_hp[VEHICLE_GUNNER] = null
+
+ to_chat(user, SPAN_NOTICE("You slave \the [src]'s controls to the driver's seat."))
+ var/mob/driver = owner.seats[VEHICLE_DRIVER]
+ if(driver)
+ to_chat(driver, SPAN_NOTICE("\The [src] has been slaved to your controls. Use \"Cycle Active Hardpoint\" to select it."))
+ else
+ allowed_seat = initial(allowed_seat)
+
+ if(owner.active_hp[VEHICLE_DRIVER] == src)
+ owner.active_hp[VEHICLE_DRIVER] = null
+
+ // dir already always mirrors the turret's own (unaffected by self_gimballed) so just settle current_angle back to zero swivel.
+ current_angle = dir2angle(dir)
+ desired_angle = current_angle
+ angular_velocity = 0
+ rotation_active = FALSE
+
+ to_chat(user, SPAN_NOTICE("You return \the [src]'s controls to the gunner's seat."))
diff --git a/code/modules/vehicles/hardpoints/secondary/tow.dm b/code/modules/vehicles/hardpoints/secondary/tow.dm
index c15f8aa1ef63..2247a6f80d96 100644
--- a/code/modules/vehicles/hardpoints/secondary/tow.dm
+++ b/code/modules/vehicles/hardpoints/secondary/tow.dm
@@ -7,7 +7,7 @@
disp_icon_state = "towlauncher"
health = 500
- firing_arc = 60
+ traverse_arc = 120
ammo = new /obj/item/ammo_magazine/hardpoint/towlauncher
max_clips = 1
@@ -19,6 +19,20 @@
"8" = list(0, 18)
)
+ rotation_pivot = list(
+ "1" = list(-1, -10),
+ "2" = list(1, -5),
+ "4" = list(0, 0),
+ "8" = list(0, -18)
+ )
+
+ gimbal_pivot = list(
+ "1" = list(6, -6),
+ "2" = list(-6, 6),
+ "4" = list(-6, 6),
+ "8" = list(6, 6)
+ )
+
muzzle_flash_pos = list(
"1" = list(8, -1),
"2" = list(-8, -16),
diff --git a/code/modules/vehicles/hardpoints/special/firing_port_weapon.dm b/code/modules/vehicles/hardpoints/special/firing_port_weapon.dm
index d9d68e0af237..9e3688d972a8 100644
--- a/code/modules/vehicles/hardpoints/special/firing_port_weapon.dm
+++ b/code/modules/vehicles/hardpoints/special/firing_port_weapon.dm
@@ -10,7 +10,7 @@
activation_sounds = list('sound/weapons/gun_smartgun1.ogg', 'sound/weapons/gun_smartgun2.ogg', 'sound/weapons/gun_smartgun3.ogg', 'sound/weapons/gun_smartgun4.ogg')
health = 100
- firing_arc = 120
+ traverse_arc = 120
//FPWs reload automatically
var/reloading = FALSE
var/reload_time = 10 SECONDS
diff --git a/code/modules/vehicles/hardpoints/support/artillery.dm b/code/modules/vehicles/hardpoints/support/artillery.dm
index 758f6b828cb3..cbdb9804f2b2 100644
--- a/code/modules/vehicles/hardpoints/support/artillery.dm
+++ b/code/modules/vehicles/hardpoints/support/artillery.dm
@@ -1,6 +1,6 @@
/obj/item/hardpoint/support/artillery_module
name = "\improper Artillery Module"
- desc = "Allows the user to look far into the distance."
+ desc = "Allows the user to look far into the distance, and offers a night vision overlay."
icon_state = "artillery"
disp_icon = "tank"
@@ -8,30 +8,62 @@
health = 250
- activatable = TRUE
+ // Not fired/toggled through the normal active-hardpoint cycle anymore. Uses itts own verbs.
+ activatable = FALSE
- var/is_active = 0
var/view_buff = 10 //This way you can VV for more or less fun
- var/view_tile_offset = 7
+ var/view_tile_offset = 2
-/obj/item/hardpoint/support/artillery_module/handle_fire(atom/target, mob/living/user, params)
- if(!user.client)
+ /// Mobs (driver and/or gunner) that currently have the magnified optics view toggled on for themselves.
+ var/list/mob/living/optics_users = list()
+ /// Mobs (driver and/or gunner) that currently have the night vision overlay toggled on for themselves.
+ var/list/mob/living/nvg_users = list()
+
+ /// Color used for the night vision overlay's tint.
+ var/nvg_color = NV_COLOR_GREEN
+ /// The alpha of darkness the user is set to see through while night vision is active.
+ var/nvg_lighting_alpha = 100
+
+/obj/item/hardpoint/support/artillery_module/proc/toggle_optics(mob/living/user)
+ if(health <= 0)
+ to_chat(user, SPAN_WARNING("\The [src] is broken!"))
+ return
+ if(!user?.client)
return
- if(is_active)
- user.client.change_view(8, owner)
- user.client.set_pixel_x(0)
- user.client.set_pixel_y(0)
- is_active = FALSE
+ if(user in optics_users)
+ disable_optics(user)
+ else
+ enable_optics(user)
+
+/obj/item/hardpoint/support/artillery_module/proc/enable_optics(mob/living/user)
+ if(!user?.client || (user in optics_users))
return
+ optics_users += user
- var/atom/holder = owner
- for(var/obj/item/hardpoint/holder/tank_turret/T in owner.hardpoints)
- holder = T
- break
+ var/obj/vehicle/multitile/vehicle = owner
+ if(user == vehicle.seats[VEHICLE_GUNNER])
+ apply_gunner_view(user)
+ else
+ apply_driver_view(user)
+
+/obj/item/hardpoint/support/artillery_module/proc/disable_optics(mob/living/user)
+ if(!(user in optics_users))
+ return
+ optics_users -= user
+ reset_view(user)
+
+/// Zooms the gunner's view and shifts it forward along the turret's current facing.
+/obj/item/hardpoint/support/artillery_module/proc/apply_gunner_view(mob/living/user)
+ if(!user || !user.client)
+ return
user.client.change_view(view_buff, owner)
- is_active = TRUE
+
+ var/atom/holder = owner
+ for(var/obj/item/hardpoint/holder/tank_turret/turret in owner.hardpoints)
+ holder = turret
+ break
switch(holder.dir)
if(NORTH)
@@ -47,25 +79,76 @@
user.client.set_pixel_x(-1 * view_tile_offset * 32)
user.client.set_pixel_y(0)
-/obj/item/hardpoint/support/artillery_module/deactivate()
- if(!is_active)
+/// Zooms the driver's view, centered on the tank instead of shifted forwards
+/obj/item/hardpoint/support/artillery_module/proc/apply_driver_view(mob/living/user)
+ if(!user || !user.client)
return
+ user.client.change_view(view_buff, owner)
+ user.client.set_pixel_x(0)
+ user.client.set_pixel_y(0)
- var/obj/vehicle/multitile/C = owner
- for(var/seat in C.seats)
- if(!ismob(C.seats[seat]))
- continue
- var/mob/user = C.seats[seat]
- if(!user.client)
- continue
- user.client.change_view(GLOB.world_view_size, owner)
- user.client.set_pixel_x(0)
- user.client.set_pixel_y(0)
- is_active = FALSE
-
-/obj/item/hardpoint/support/artillery_module/try_fire(target, user, params)
+/obj/item/hardpoint/support/artillery_module/proc/reset_view(mob/living/user)
+ if(!user || !user.client)
+ return
+ user.client.change_view(8, owner)
+ user.client.set_pixel_x(0)
+ user.client.set_pixel_y(0)
+
+/obj/item/hardpoint/support/artillery_module/proc/toggle_nvg(mob/living/user)
if(health <= 0)
- to_chat(usr, SPAN_WARNING("\The [src] is broken!"))
- return NONE
+ to_chat(user, SPAN_WARNING("\The [src] is broken!"))
+ return
+ if(!ishuman(user))
+ return
+
+ if(user in nvg_users)
+ disable_nvg(user)
+ else
+ enable_nvg(user)
+
+/obj/item/hardpoint/support/artillery_module/proc/enable_nvg(mob/living/carbon/human/user)
+ if(!ishuman(user) || (user in nvg_users))
+ return
+ nvg_users += user
+
+ RegisterSignal(user, COMSIG_HUMAN_POST_UPDATE_SIGHT, PROC_REF(on_update_sight))
+ user.add_client_color_matrix("tank_artillery_nvg", 99, color_matrix_multiply(color_matrix_saturation(0), color_matrix_from_string(nvg_color)))
+ user.overlay_fullscreen("tank_artillery_nvg", /atom/movable/screen/fullscreen/flash/noise/nvg)
+ user.overlay_fullscreen("tank_artillery_nvg_blur", /atom/movable/screen/fullscreen/brute/nvg, 3)
+ user.update_sight()
+ to_chat(user, SPAN_NOTICE("You activate \the [src]'s night vision overlay."))
+
+/obj/item/hardpoint/support/artillery_module/proc/disable_nvg(mob/living/carbon/human/user)
+ if(!(user in nvg_users))
+ return
+ nvg_users -= user
+
+ UnregisterSignal(user, COMSIG_HUMAN_POST_UPDATE_SIGHT)
+ user.remove_client_color_matrix("tank_artillery_nvg", 1 SECONDS)
+ user.clear_fullscreen("tank_artillery_nvg", 0.5 SECONDS)
+ user.clear_fullscreen("tank_artillery_nvg_blur", 0.5 SECONDS)
+ user.update_sight()
+ to_chat(user, SPAN_NOTICE("You deactivate \the [src]'s night vision overlay."))
+
+/obj/item/hardpoint/support/artillery_module/proc/on_update_sight(mob/living/carbon/human/user)
+ SIGNAL_HANDLER
+ if(nvg_lighting_alpha < 255)
+ user.see_in_dark = 12
+ user.lighting_alpha = nvg_lighting_alpha
+ user.sync_lighting_plane_alpha()
+
+/// Clears both effects off of one specific user
+/obj/item/hardpoint/support/artillery_module/proc/clear_user_effects(mob/living/user)
+ disable_optics(user)
+ disable_nvg(user)
+
+/// Clears both effects off of everyone currently using them if the module gets broken or uninstalled when used
+/obj/item/hardpoint/support/artillery_module/deactivate()
+ for(var/mob/living/user as anything in optics_users.Copy())
+ disable_optics(user)
+ for(var/mob/living/user as anything in nvg_users.Copy())
+ disable_nvg(user)
- return handle_fire(target, user, params)
+/obj/item/hardpoint/support/artillery_module/on_uninstall(obj/vehicle/multitile/vehicle)
+ deactivate()
+ ..()
diff --git a/code/modules/vehicles/hardpoints/support/flare.dm b/code/modules/vehicles/hardpoints/support/flare.dm
index 0fa37de34d2f..bfc36e9d54b3 100644
--- a/code/modules/vehicles/hardpoints/support/flare.dm
+++ b/code/modules/vehicles/hardpoints/support/flare.dm
@@ -13,7 +13,7 @@
activatable = TRUE
health = 500
- firing_arc = 120
+ traverse_arc = 120
ammo = new /obj/item/ammo_magazine/hardpoint/flare_launcher
max_clips = 3
diff --git a/code/modules/vehicles/interior/interactable/intercom.dm b/code/modules/vehicles/interior/interactable/intercom.dm
new file mode 100644
index 000000000000..ef5064a00289
--- /dev/null
+++ b/code/modules/vehicles/interior/interactable/intercom.dm
@@ -0,0 +1,82 @@
+/obj/structure/vehicle_intercom
+ name = "vehicle intercom"
+ desc = "A wall-mounted intercom wired into the vehicle's hull, relaying speech between the crew compartment and the outside world."
+ icon = 'icons/obj/structures/phone.dmi'
+ icon_state = "wall_phone"
+ anchored = TRUE
+ density = FALSE
+ layer = 3.2
+
+ unacidable = TRUE
+ unslashable = TRUE
+ explo_proof = TRUE
+
+ flags_atom = USES_HEARING
+
+ var/obj/vehicle/multitile/vehicle = null
+ var/obj/item/phone/handset
+
+ // Mic = picks up interior speech to broadcast outside. Speaker = plays exterior speech inside.
+ var/mic_muted = FALSE
+ var/speaker_muted = FALSE
+
+/obj/structure/vehicle_intercom/Initialize(mapload, ...)
+ . = ..()
+ handset = new /obj/item/phone(src)
+
+/obj/structure/vehicle_intercom/Destroy()
+ QDEL_NULL(handset)
+ vehicle = null
+ return ..()
+
+/obj/structure/vehicle_intercom/attack_hand(mob/user)
+ . = ..()
+ if(.)
+ return
+
+ var/choice = tgui_input_list(user, "Adjust the intercom's microphone and speaker.", "Vehicle Intercom",
+ list("Toggle Microphone ([mic_muted ? "Muted" : "Live"])", "Toggle Speaker ([speaker_muted ? "Muted" : "Live"])", "Cancel"))
+
+ if(!choice || choice == "Cancel" || !Adjacent(user))
+ return
+
+ if(findtext(choice, "Microphone"))
+ mic_muted = !mic_muted
+ to_chat(user, SPAN_NOTICE("You [mic_muted ? "mute" : "unmute"] the intercom's microphone. [mic_muted ? "Nobody outside will hear the crew compartment." : "The crew compartment can be heard outside again."]"))
+ else if(findtext(choice, "Speaker"))
+ speaker_muted = !speaker_muted
+ to_chat(user, SPAN_NOTICE("You [speaker_muted ? "mute" : "unmute"] the intercom's speaker. [speaker_muted ? "The crew compartment will no longer hear the outside." : "The crew compartment can hear the outside again."]"))
+
+// anyone speaking near the inttercom inside the interior gets relayed outside.
+/obj/structure/vehicle_intercom/hear_talk(mob/living/M, msg, verb = "says", datum/language/speaking, italics = 0)
+ if(!mic_muted)
+ relay_to_exterior(M, msg, verb, speaking, italics)
+ ..()
+
+/obj/structure/vehicle_intercom/proc/relay_to_exterior(mob/living/M, msg, verb, datum/language/speaking, italics)
+ if(!vehicle)
+ return
+ var/turf/exterior_turf = get_turf(vehicle)
+ if(!exterior_turf)
+ return
+ var/list/listeners = get_mobs_in_view(7, exterior_turf)
+ if(!length(listeners))
+ return
+ for(var/mob/L in listeners)
+ L.hear_radio(msg, verb, speaking, part_a = "",
+ part_b = " ", vname = "[vehicle] Intercom",
+ speaker = M, no_paygrade = TRUE)
+ vehicle.langchat_speech(msg, listeners, speaking, M.langchat_color, FALSE, LANGCHAT_FAST_POP, list(M.langchat_styles))
+
+// Called by /obj/vehicle/multitile/hear_talk() when someone speaks near the vehicle's exterior.
+/obj/structure/vehicle_intercom/proc/relay_exterior_speech(mob/living/M, msg, verb, datum/language/speaking, italics)
+ if(speaker_muted || !vehicle || !vehicle.interior)
+ return
+ var/list/listeners = vehicle.interior.get_passengers()
+ if(!length(listeners))
+ return
+ for(var/mob/L in listeners)
+ L.hear_radio(msg, verb, speaking, part_a = "",
+ part_b = " ", vname = "Intercom",
+ speaker = M, no_paygrade = TRUE)
+ langchat_speech(msg, listeners, speaking, M.langchat_color, FALSE, LANGCHAT_FAST_POP, list(M.langchat_styles))
diff --git a/code/modules/vehicles/interior/interactable/seats.dm b/code/modules/vehicles/interior/interactable/seats.dm
index 7048aa44ccc3..74b9a07cb7ef 100644
--- a/code/modules/vehicles/interior/interactable/seats.dm
+++ b/code/modules/vehicles/interior/interactable/seats.dm
@@ -47,6 +47,10 @@
M.client.set_pixel_x(0)
M.client.set_pixel_y(0)
M.reset_view()
+ if(isliving(M))
+ var/mob/living/living_mob = M
+ if(living_mob.observed_atom)
+ QDEL_NULL(living_mob.observed_atom)
else
if(M.stat == DEAD)
unbuckle()
diff --git a/code/modules/vehicles/interior/interactable/weapons_loader.dm b/code/modules/vehicles/interior/interactable/weapons_loader.dm
index 18b3f894e2a2..7b77865c6820 100644
--- a/code/modules/vehicles/interior/interactable/weapons_loader.dm
+++ b/code/modules/vehicles/interior/interactable/weapons_loader.dm
@@ -14,7 +14,19 @@
// Loading new magazines
/obj/structure/weapons_loader/attackby(obj/item/I, mob/user)
- if(!istype(I, /obj/item/ammo_magazine/hardpoint))
+ // The tank turret's flare launcher isn't loaded via a swappable magazine so it
+ // shouldn't be picked up by the generic magazine-matching loop below. Delegate loose star shells and
+ // star shell packets straight to the turret's own attackby().
+ if(istype(I, /obj/item/explosive/grenade/high_explosive/airburst/starshell) || istype(I, /obj/item/storage/box/packet/flare))
+ if(!skillcheck(user, SKILL_VEHICLE, SKILL_VEHICLE_LARGE))
+ to_chat(user, SPAN_NOTICE("You have no idea how to operate this thing!"))
+ return
+ var/obj/item/hardpoint/holder/tank_turret/turret = locate() in vehicle?.hardpoints
+ if(!turret)
+ return ..()
+ return turret.attackby(I, user)
+
+ if(!istype(I, /obj/item/ammo_magazine))
return ..()
if(!skillcheck(user, SKILL_VEHICLE, SKILL_VEHICLE_LARGE))
@@ -24,10 +36,10 @@
// Check if any of the hardpoints accept the magazine
var/obj/item/hardpoint/reloading_hardpoint = null
for(var/obj/item/hardpoint/H in vehicle.get_hardpoints_with_ammo())
- if(QDELETED(H) || QDELETED(H.ammo))
+ if(QDELETED(H))
continue
- if(istype(I, H.ammo.type))
+ if(H.accepts_magazine(I))
reloading_hardpoint = H
break
@@ -88,7 +100,16 @@
return
if(!LAZYLEN(HP.backup_clips))
- to_chat(user, SPAN_WARNING("\The [HP] has no remaining backup magazines!"))
+ if(!HP.ammo)
+ to_chat(user, SPAN_WARNING("\The [HP] has no remaining backup magazines, and no magazine currently loaded!"))
+ return
+
+ to_chat(user, SPAN_WARNING("\The [HP] has no remaining backup magazines, so you remove the current magazine instead."))
+ HP.ammo.forceMove(get_turf(user))
+ HP.ammo.update_icon()
+ HP.ammo = null
+ HP.owner?.update_icon()
+ playsound(loc, 'sound/machines/hydraulics_3.ogg', 50)
return
var/obj/item/ammo_magazine/M = LAZYACCESS(HP.backup_clips, 1)
@@ -102,10 +123,12 @@
to_chat(user, SPAN_WARNING("Something interrupted you while reloading \the [HP]."))
return
- HP.ammo.forceMove(get_turf(src))
- HP.ammo.update_icon()
+ if(HP.ammo)
+ HP.ammo.forceMove(get_turf(src))
+ HP.ammo.update_icon()
HP.ammo = M
LAZYREMOVE(HP.backup_clips, M)
+ HP.owner?.update_icon()
playsound(loc, 'sound/machines/hydraulics_3.ogg', 50)
to_chat(user, SPAN_NOTICE("You reload \the [HP]. Ammo: [SPAN_HELPFUL(HP.ammo.current_rounds)]/[SPAN_HELPFUL(HP.ammo.max_rounds)] | Mags: [SPAN_HELPFUL(LAZYLEN(HP.backup_clips))]/[SPAN_HELPFUL(HP.max_clips)]"))
diff --git a/code/modules/vehicles/interior/interior_landmarks.dm b/code/modules/vehicles/interior/interior_landmarks.dm
index b57defea5f4b..02cf7a68cb55 100644
--- a/code/modules/vehicles/interior/interior_landmarks.dm
+++ b/code/modules/vehicles/interior/interior_landmarks.dm
@@ -247,6 +247,32 @@
/obj/effect/landmark/interior/spawn/telephone/wy
icon = 'icons/obj/vehicles/interiors/general_wy.dmi'
+// Landmark for spawning the ambient vehicle intercom
+/obj/effect/landmark/interior/spawn/intercom
+ name = "vehicle intercom spawner"
+ icon = 'icons/obj/vehicles/interiors/general.dmi'
+ icon_state = "wall_phone"
+ color = "cyan"
+
+/obj/effect/landmark/interior/spawn/intercom/on_load(datum/interior/interior_data)
+ var/obj/structure/vehicle_intercom/Intercom = new(loc)
+ var/obj/vehicle/multitile/vehicle = interior_data.exterior
+
+ Intercom.icon = icon
+ Intercom.icon_state = icon_state
+ Intercom.layer = layer
+ Intercom.setDir(dir)
+ Intercom.alpha = alpha
+ Intercom.update_icon()
+ Intercom.pixel_x = pixel_x
+ Intercom.pixel_y = pixel_y
+ Intercom.vehicle = vehicle
+
+ vehicle.intercom = Intercom
+ vehicle.flags_atom |= USES_HEARING
+
+ qdel(src)
+
// Landmark for spawning the reloader
/obj/effect/landmark/interior/spawn/weapons_loader
name = "vehicle weapons reloader spawner"
diff --git a/code/modules/vehicles/multitile/multitile.dm b/code/modules/vehicles/multitile/multitile.dm
index d04e1e0e8f3e..d33230478483 100644
--- a/code/modules/vehicles/multitile/multitile.dm
+++ b/code/modules/vehicles/multitile/multitile.dm
@@ -53,7 +53,13 @@
// Determines how much slower the vehicle is when it lacks its full momentum
// When the vehicle has 0 momentum, it's movement delay will be move_delay * momentum_build_factor
// The movement delay gradually reduces up to move_delay when momentum increases
+ //// This is mostly vestigial and it looks like it cancels itself out when calculating movement delay??? -bwsb
var/move_momentum_build_factor = 1.3
+ // How fast momentum decays when you stop moving.
+ var/move_momentum_loss_factor = 1
+ // How long do you have to go without moving for momentum decay to start
+ // Slower moving vehicles like the tank require more time, or else they won't get past minimum speed.
+ var/idle_time_required = 10 // 10 seconds. Tested to be OK on APC and Van.
//Sound to play when moving
var/movement_sound
@@ -89,6 +95,7 @@
// Map file name of the vehicle interior
var/interior_map = null
var/datum/interior/interior = null
+ var/obj/structure/vehicle_intercom/intercom = null
//common passenger slots
var/passengers_slots = 2
@@ -174,12 +181,23 @@
///Minimap iconstate to use for this vehicle
var/minimap_icon_state
+ // Structures that we should collide with, but that aren't being collided with when we call T.Enter in multitile_movement
+ // associative list should guarantee an O(1) lookup in case this needs to be expanded.
+ var/static/list/blocking_structures = list(
+ /obj/structure/shuttle/part = TRUE,
+ /obj/structure/mineral_door/resin = TRUE,
+ )
+
+ var/momentum_decay_active = FALSE // Track if momentum decay loop is running
+ var/last_input_time = 0 // Track when last movement input was received
+
/obj/vehicle/multitile/Initialize()
. = ..()
var/angle_to_turn = turning_angle(SOUTH, dir)
rotate_entrances(angle_to_turn)
rotate_bounds(angle_to_turn)
+ update_langchat_height()
if(bound_width > world.icon_size || bound_height > world.icon_size)
lighting_holder = new(src)
@@ -225,6 +243,24 @@
return ..()
+// No-op unless an intercom landmark spawned one into this vehicle's interior.
+/obj/vehicle/multitile/hear_talk(mob/living/speaker, msg, verb = "says", datum/language/speaking, italics = 0)
+ if(intercom)
+ intercom.relay_exterior_speech(speaker, msg, verb, speaking, italics)
+ if(!intercom.speaker_muted)
+ for(var/seat_key in seats)
+ var/mob/seated = seats[seat_key]
+ if(seated?.client?.prefs && !seated.client.prefs.lang_chat_disabled && !seated.ear_deaf && seated.say_understands(speaker, speaking))
+ speaker.langchat_display_image(seated)
+ ..()
+
+// offsets langchat height to show, properly, above and around the middle of the vehicle.
+/obj/vehicle/multitile/proc/update_langchat_height()
+ langchat_height = bound_height - bound_y - 8
+
+/obj/vehicle/multitile/get_maxptext_x_offset(image/maptext_image)
+ return ..() - bound_x - 16
+
/obj/vehicle/multitile/proc/initialize_cameras()
return
@@ -468,6 +504,12 @@
forceMove(get_turf(mover))
+/obj/vehicle/multitile/proc/is_blocking_structure(atom/A)
+ for(var/blocked_type in blocking_structures)
+ if(ispath(A.type, blocked_type))
+ return TRUE
+ return FALSE
+
///Updates the vehicles minimap icon
/obj/vehicle/multitile/proc/update_minimap_icon(modules_broken)
if(!minimap_icon_state)
diff --git a/code/modules/vehicles/multitile/multitile_bump.dm b/code/modules/vehicles/multitile/multitile_bump.dm
index fd1b6ec870b9..38fed407120f 100644
--- a/code/modules/vehicles/multitile/multitile_bump.dm
+++ b/code/modules/vehicles/multitile/multitile_bump.dm
@@ -46,6 +46,12 @@
return TRUE
return FALSE
+/obj/structure/mineral_door/resin/handle_vehicle_bump(obj/vehicle/multitile/V)
+ take_damage(V.wall_ram_damage)
+ visible_message(SPAN_DANGER("\The [V] rams [src]!"))
+ playsound(V, 'sound/effects/metal_crash.ogg', 20)
+ return FALSE
+
/obj/structure/barricade/handle_vehicle_bump(obj/vehicle/multitile/V)
if(!(V.vehicle_flags & VEHICLE_CLASS_WEAK))
take_damage(maxhealth)
@@ -56,12 +62,12 @@
return FALSE
/obj/structure/barricade/plasteel/handle_vehicle_bump(obj/vehicle/multitile/V)
+ if(V.vehicle_flags & VEHICLE_CLASS_MEDIUM || V.vehicle_flags & VEHICLE_CLASS_HEAVY)
+ close(src)
+ return TRUE
if(V.seats[VEHICLE_DRIVER])
close(src)
- return FALSE
- else
- . = ..()
- return FALSE
+ return ..()
/obj/structure/barricade/deployable/handle_vehicle_bump(obj/vehicle/multitile/V)
visible_message(SPAN_DANGER("\The [V] crushes [src]!"))
@@ -524,6 +530,17 @@
playsound(V, 'sound/effects/metal_crash.ogg', 35)
return FALSE
+// legacy cargo train gets ran over and crushed without a fuss by medium and heavy vehicles
+/obj/vehicle/train/cargo/handle_vehicle_bump(obj/vehicle/multitile/V)
+ if(V.vehicle_flags & VEHICLE_CLASS_MEDIUM || V.vehicle_flags & VEHICLE_CLASS_HEAVY)
+ health = 0
+ healthcheck()
+
+ visible_message(SPAN_DANGER("\The [V] crushes into \the [src]!"))
+ playsound(V, 'sound/effects/metal_crash.ogg', 35)
+ return TRUE
+ . = ..()
+
//-----------------------------------------------------------
//-------------------------MOBS------------------------------
//-----------------------------------------------------------
@@ -615,8 +632,9 @@
apply_damage(10 + rand(0, 10), BRUTE)
else if(V.vehicle_flags & VEHICLE_CLASS_MEDIUM)
- apply_effect(3, WEAKEN)
- apply_damage(10 + rand(0, 10), BRUTE)
+ apply_effect((1.5 * (V.move_momentum / V.move_max_momentum)), WEAKEN) // Even with the nerfs, still by far the most frustrating thing about the tank.
+ apply_effect((3 * (V.move_momentum / V.move_max_momentum)), SLOW)
+ apply_damage(5 + rand(0, 5), BRUTE)
dmg = TRUE
else if(V.vehicle_flags & VEHICLE_CLASS_HEAVY)
diff --git a/code/modules/vehicles/multitile/multitile_hardpoints.dm b/code/modules/vehicles/multitile/multitile_hardpoints.dm
index d40749aedcf5..f15eb383da03 100644
--- a/code/modules/vehicles/multitile/multitile_hardpoints.dm
+++ b/code/modules/vehicles/multitile/multitile_hardpoints.dm
@@ -29,7 +29,7 @@
var/obj/item/hardpoint/holder/HP = H
if(HP.hardpoints)
hps += HP.get_hardpoints_with_ammo(seat)
- if(!H.ammo || seat && seat != H.allowed_seat)
+ if(!H.ammo_type || seat && seat != H.allowed_seat)
continue
hps += H
return hps
diff --git a/code/modules/vehicles/multitile/multitile_interaction.dm b/code/modules/vehicles/multitile/multitile_interaction.dm
index d3106ea8183c..d069dca5861f 100644
--- a/code/modules/vehicles/multitile/multitile_interaction.dm
+++ b/code/modules/vehicles/multitile/multitile_interaction.dm
@@ -373,9 +373,10 @@
RegisterSignal(user, COMSIG_MOB_MOUSEDOWN, PROC_REF(crew_mousedown))
RegisterSignal(user, COMSIG_MOB_MOUSEDRAG, PROC_REF(crew_mousedrag))
RegisterSignal(user, COMSIG_MOB_MOUSEUP, PROC_REF(crew_mouseup))
+ RegisterSignal(user, COMSIG_MOB_MOUSEMOVE, PROC_REF(crew_mousemove))
/obj/vehicle/multitile/on_unset_interaction(mob/user)
- UnregisterSignal(user, list(COMSIG_MOB_MOUSEUP, COMSIG_MOB_MOUSEDOWN, COMSIG_MOB_MOUSEDRAG))
+ UnregisterSignal(user, list(COMSIG_MOB_MOUSEUP, COMSIG_MOB_MOUSEDOWN, COMSIG_MOB_MOUSEDRAG, COMSIG_MOB_MOUSEMOVE))
var/obj/item/hardpoint/hardpoint = get_mob_hp(user)
if(hardpoint)
@@ -399,6 +400,43 @@
hardpoint.change_target(source, src_object, over_object, src_location, over_location, src_control, over_control, params)
+ // BYOND doesn't send MouseMove while a button is held - MouseDrag takes over. Feed it into
+ // the same tracking patths so aiming doesn't freeze while the crew is firing/holding M1
+ track_gunner_mouse(source, over_object, params)
+ track_driver_mouse(source, over_object, params)
+
+/// Relays live cursor position to the gunner's turret / driver's slaved secondary, if any, so they can track the mouse.
+/obj/vehicle/multitile/proc/crew_mousemove(datum/source, atom/object, turf/location, control, params)
+ SIGNAL_HANDLER
+ track_gunner_mouse(source, object, params)
+ track_driver_mouse(source, object, params)
+
+/// Updates the gunner's turret (if any) to aim toward the given screen position.
+/obj/vehicle/multitile/proc/track_gunner_mouse(mob/source, atom/object, params)
+ if(get_mob_seat(source) != VEHICLE_GUNNER)
+ return
+
+ var/obj/item/hardpoint/holder/tank_turret/turret = locate() in hardpoints
+ if(!turret)
+ return
+
+ turret.update_desired_angle(object, source, params)
+
+/// Updates the driver's slaved secondary (if any) to aim toward the given screen position.
+/obj/vehicle/multitile/proc/track_driver_mouse(mob/source, atom/object, params)
+ if(get_mob_seat(source) != VEHICLE_DRIVER)
+ return
+
+ var/obj/item/hardpoint/holder/tank_turret/turret = locate() in hardpoints
+ if(!turret)
+ return
+
+ for(var/obj/item/hardpoint/secondary/secondary_weapon in turret.hardpoints)
+ if(!secondary_weapon.self_gimballed)
+ continue
+ secondary_weapon.update_desired_angle(object, source, params)
+ return
+
/// Checks for special control keybinds, else relays crew mouse press to active hardpoint.
/obj/vehicle/multitile/proc/crew_mousedown(datum/source, atom/object, turf/location, control, params)
SIGNAL_HANDLER
diff --git a/code/modules/vehicles/multitile/multitile_movement.dm b/code/modules/vehicles/multitile/multitile_movement.dm
index b4faf7f5ba9a..1e943e2b20f4 100644
--- a/code/modules/vehicles/multitile/multitile_movement.dm
+++ b/code/modules/vehicles/multitile/multitile_movement.dm
@@ -34,6 +34,8 @@
if(health <= 0)
return FALSE
+ last_input_time = world.time
+
return pre_movement(direction)
// This determines what type of movement to execute
@@ -54,6 +56,9 @@
if(move_on_turn)
try_move(direction)
+ if(success)
+ start_momentum_decay_if_needed()
+
return success
// Attempts to execute the given movement input
@@ -102,6 +107,7 @@
rotate_hardpoints(deg)
rotate_entrances(deg)
rotate_bounds(deg)
+ update_langchat_height()
setDir(turn(dir, deg), TRUE)
last_move_dir = dir
@@ -121,10 +127,6 @@
// Increases/decreases the vehicle's momentum according to whether or not the user is steppin' on the gas or not
/obj/vehicle/multitile/proc/update_momentum(direction)
- // If we've stood still for long enough we go back to 0 momentum
- if(world.time > next_move + move_delay*move_momentum_build_factor)
- move_momentum = 0
-
if(direction == dir)
move_momentum = min(move_momentum + 1, move_max_momentum)
else
@@ -140,12 +142,12 @@
/obj/vehicle/multitile/proc/update_next_move()
// 1/((m/M)*b) where m is momentum, M is max momentum and b is the build factor
+ //// move_momentum_build_factor seems to cancel itself out here. It's worth to revisit this section and maybe refactor it.
var/anti_build_factor = 1/((max(abs(move_momentum), 1)/move_max_momentum) * move_momentum_build_factor)
next_move = world.time + move_delay * move_momentum_build_factor * anti_build_factor * misc_multipliers["move"]
l_move_time = world.time
-
// This just checks if the vehicle can physically move in the given direction
/obj/vehicle/multitile/proc/can_move(direction)
var/can_move = TRUE
@@ -158,6 +160,11 @@
var/bound_height_tiles = bound_height / world.icon_size
var/list/old_turfs = CORNER_BLOCK(min_turf, bound_width_tiles, bound_height_tiles)
+ for(var/turf/T as anything in old_turfs)
+ for(var/obj/structure/barricade/B in T)
+ if(!QDELETED(B) && B.last_bumped != world.time && (B.dir & direction))
+ Collide(B)
+
var/turf/new_loc = get_step(src, direction)
min_turf = locate(new_loc.x + bound_x_tiles, new_loc.y + bound_y_tiles, z)
@@ -166,11 +173,31 @@
if(T in old_turfs)
continue
+ // This first istype() check is probably a bad practice but it'll allow V-TOL to still enter...
+ // ... open_space turfs in case Tank Desant and VTOL get TM'd together.
+ if(istype(src, /obj/vehicle/multitile/tank) && istype(T, /turf/open_space))
+ // early return so we skip crash behavior.
+ move_momentum = floor(move_momentum/2)
+ update_next_move()
+ return FALSE
+
if(!T.Enter(src))
can_move = FALSE
+ for(var/obj/structure/barricade/B in T)
+ if(!QDELETED(B) && B.last_bumped != world.time)
+ if(REVERSE_DIR(B.dir) & direction)
+ Collide(B)
+
+ // any other tile-blocking items that weren't caught by !T.Enter
+ for(var/atom/A in T)
+ if(is_blocking_structure(A))
+ can_move = FALSE
+ break
+
// Crashed with something that stopped us
if(!can_move)
+ on_crash()
move_momentum = floor(move_momentum/2)
update_next_move()
interior_crash_effect()
@@ -307,3 +334,59 @@
return
else
cell_explosion(target, explosion_strength, explosion_falloff, EXPLOSION_FALLOFF_SHAPE_LINEAR, null, cause_data)
+
+/**
+ * Base proc for defining special behavior when crashing
+ *
+ * on_crash is a proc meant to be overridable by child classes
+ * That way, all of the effects that happen when crashing are in the same spot.
+ */
+/obj/vehicle/multitile/proc/on_crash()
+ return
+
+
+/**
+ * Starts the momentum decay loop if not already running and momentum exists.
+ * This should be called after any movement to ensure decay starts when player stops.
+ */
+/obj/vehicle/multitile/proc/start_momentum_decay_if_needed()
+ if(abs(move_momentum) > 0 && !momentum_decay_active)
+ momentum_decay_active = TRUE
+ spawn(0)
+ momentum_decay_loop()
+
+/**
+ * The main momentum decay loop - runs continuously while vehicle has momentum.
+ * Checks every second if the player has stopped giving input, and decays momentum accordingly.
+ *
+ * decay_interval controls how fast mometum will decay. By default, it decays every second.
+ *
+ * idle_time_required is how long you have to go without pressing a movement key.
+ *
+ * So, for example, if idle time is 20 and decay interval is 5, you will start losing momentum twice per second after you spend 2 seconds without moving.
+ *
+ */
+/obj/vehicle/multitile/proc/momentum_decay_loop()
+ var/decay_interval = 10 // Decay every 1 second
+
+ while(abs(move_momentum) > 0)
+ sleep(decay_interval)
+
+ var/time_since_input = world.time - last_input_time
+ if(time_since_input < idle_time_required)
+ continue
+
+ var/momentum_abs = abs(move_momentum)
+ momentum_abs -= move_momentum_loss_factor
+ if(momentum_abs <= 0)
+ move_momentum = 0
+ momentum_decay_active = FALSE
+ return
+
+ if(move_momentum > 0)
+ move_momentum = momentum_abs
+
+ else
+ move_momentum = -momentum_abs
+
+ momentum_decay_active = FALSE
diff --git a/code/modules/vehicles/multitile/multitile_verbs.dm b/code/modules/vehicles/multitile/multitile_verbs.dm
index df53803f9f78..7df1f0897210 100644
--- a/code/modules/vehicles/multitile/multitile_verbs.dm
+++ b/code/modules/vehicles/multitile/multitile_verbs.dm
@@ -4,7 +4,7 @@
//Used to swap which module a position is using
-//e.g. swapping primary gunner from the minigun to the smoke launcher
+//e.g. swapping primary gunner from the minigun to the flare launcher
/obj/vehicle/multitile/proc/switch_hardpoint()
set name = "Change Active Hardpoint"
set category = "Vehicle"
@@ -242,6 +242,99 @@
return
T.toggle_gyro(usr)
+//toggles the artillery module's magnified optics for your own view - available to both driver and gunner
+/obj/vehicle/multitile/proc/toggle_artillery_optics()
+ set name = "Toggle Artillery Optics"
+ set desc = "Toggles the artillery module's magnified optics for your own view."
+ set category = "Vehicle"
+
+ var/mob/living/user = usr
+ if(!user || !istype(user))
+ return
+
+ var/obj/vehicle/multitile/vehicle = user.interactee
+ if(!istype(vehicle))
+ return
+
+ var/obj/item/hardpoint/support/artillery_module/AM = locate() in vehicle.hardpoints
+ if(!AM)
+ to_chat(user, SPAN_WARNING("No artillery module installed."))
+ return
+
+ AM.toggle_optics(user)
+
+//toggles the artillery module's night vision overlay for your own view - available to both driver and gunner
+/obj/vehicle/multitile/proc/toggle_artillery_nvg()
+ set name = "Toggle Artillery Night Vision"
+ set desc = "Toggles the artillery module's night vision overlay for your own view."
+ set category = "Vehicle"
+
+ var/mob/living/user = usr
+ if(!user || !istype(user))
+ return
+
+ var/obj/vehicle/multitile/vehicle = user.interactee
+ if(!istype(vehicle))
+ return
+
+ var/obj/item/hardpoint/support/artillery_module/AM = locate() in vehicle.hardpoints
+ if(!AM)
+ to_chat(user, SPAN_WARNING("No artillery module installed."))
+ return
+
+ AM.toggle_nvg(user)
+
+//hands control of the secondary hardpoint (aim + fire) between the gunner and the driver
+/obj/vehicle/multitile/proc/toggle_slave_secondary_to_driver()
+ set name = "Slave Secondary to Driver"
+ set desc = "Toggles handing control of the secondary weapon's aim and fire between yourself and the driver."
+ set category = "Vehicle"
+
+ var/mob/user = usr
+ if(!user || !istype(user))
+ return
+
+ var/obj/vehicle/multitile/vehicle = user.interactee
+ if(!istype(vehicle))
+ return
+
+ var/obj/item/hardpoint/holder/tank_turret/turret = locate() in vehicle.hardpoints
+ if(!turret)
+ return
+
+ var/obj/item/hardpoint/secondary/secondary_weapon = null
+ for(var/obj/item/hardpoint/secondary/candidate in turret.hardpoints)
+ if(!candidate.is_activatable()) // passive hardpoints (e.g. the mounted flag) can't be slaved/selected
+ continue
+ secondary_weapon = candidate
+ break
+ if(!secondary_weapon)
+ to_chat(user, SPAN_WARNING("No secondary weapon installed."))
+ return
+
+ secondary_weapon.toggle_slaved_to_driver(usr)
+
+//toggles the fire mode for hardpoints. currently only used for the primary and secondary flamers to switch between glob and stream
+/obj/vehicle/multitile/proc/toggle_hardpoint_fire_mode()
+ set name = "Toggle Hardpoint Fire Mode"
+ set desc = "Toggles the fire mode of your currently selected hardpoint, if it supports one."
+ set category = "Vehicle"
+
+ var/mob/user = usr
+ if(!user || !istype(user))
+ return
+
+ var/obj/vehicle/multitile/vehicle = user.interactee
+ if(!istype(vehicle))
+ return
+
+ var/obj/item/hardpoint/selected_hardpoint = vehicle.get_mob_hp(user)
+ if(!selected_hardpoint)
+ to_chat(user, SPAN_WARNING("You have no hardpoint selected."))
+ return
+
+ selected_hardpoint.toggle_fire_mode(usr)
+
//single use verb that allows VCs to add a nickname in "" at the end of their vehicle name
/obj/vehicle/multitile/proc/name_vehicle()
set name = "Name Vehicle"
diff --git a/code/modules/vehicles/tank/tank.dm b/code/modules/vehicles/tank/tank.dm
index b462e31658ae..fc5a3818b769 100644
--- a/code/modules/vehicles/tank/tank.dm
+++ b/code/modules/vehicles/tank/tank.dm
@@ -35,6 +35,7 @@
move_max_momentum = 3
move_momentum_build_factor = 1.8
move_turn_momentum_loss_factor = 0.6
+ idle_time_required = 20
vehicle_light_range = 7
@@ -76,6 +77,15 @@
explosive_resistance = 400
minimap_icon_state = "tank"
+ var/list/on_top_mobs = list() /// keeps track of all mobs currently atop the tank
+ var/list/on_top_obj = list() /// keeps track of all objs currently atop the tank
+ var/on_top_mobs_shooting_inaccuracy_time = 0 /// world_time must be bigger than this so mobs don't get penalized for shooting while atop the tank.
+
+/obj/vehicle/multitile/tank/update_next_move()
+ var/anti_build_factor = 1/((max(abs(move_momentum), 1)/move_max_momentum) * move_momentum_build_factor)
+ on_top_mobs_shooting_inaccuracy_time = world.time + move_delay * move_momentum_build_factor * anti_build_factor * misc_multipliers["move"] * 5
+ . = ..()
+
/obj/vehicle/multitile/tank/initialize_cameras(change_tag = FALSE)
if(!camera)
camera = new /obj/structure/machinery/camera/vehicle(src)
@@ -111,11 +121,20 @@
add_verb(M.client, list(
/obj/vehicle/multitile/proc/toggle_door_lock,
/obj/vehicle/multitile/proc/activate_horn,
+ /obj/vehicle/multitile/proc/cycle_hardpoint,
+ /obj/vehicle/multitile/proc/toggle_hardpoint_fire_mode,
))
else if(seat == VEHICLE_GUNNER)
add_verb(M.client, list(
/obj/vehicle/multitile/proc/cycle_hardpoint,
/obj/vehicle/multitile/proc/toggle_gyrostabilizer,
+ /obj/vehicle/multitile/proc/toggle_slave_secondary_to_driver,
+ /obj/vehicle/multitile/proc/toggle_hardpoint_fire_mode,
+ ))
+ if(seat == VEHICLE_DRIVER || seat == VEHICLE_GUNNER)
+ add_verb(M.client, list(
+ /obj/vehicle/multitile/proc/toggle_artillery_optics,
+ /obj/vehicle/multitile/proc/toggle_artillery_nvg,
))
@@ -133,12 +152,28 @@
remove_verb(M.client, list(
/obj/vehicle/multitile/proc/toggle_door_lock,
/obj/vehicle/multitile/proc/activate_horn,
+ /obj/vehicle/multitile/proc/cycle_hardpoint,
+ /obj/vehicle/multitile/proc/toggle_hardpoint_fire_mode,
))
else if(seat == VEHICLE_GUNNER)
remove_verb(M.client, list(
/obj/vehicle/multitile/proc/cycle_hardpoint,
/obj/vehicle/multitile/proc/toggle_gyrostabilizer,
+ /obj/vehicle/multitile/proc/toggle_slave_secondary_to_driver,
+ /obj/vehicle/multitile/proc/toggle_hardpoint_fire_mode,
))
+ if(seat == VEHICLE_DRIVER || seat == VEHICLE_GUNNER)
+ remove_verb(M.client, list(
+ /obj/vehicle/multitile/proc/toggle_artillery_optics,
+ /obj/vehicle/multitile/proc/toggle_artillery_nvg,
+ ))
+ var/obj/item/hardpoint/support/artillery_module/AM = get_artillery_module()
+ AM?.clear_user_effects(M)
+
+/obj/vehicle/multitile/tank/proc/get_artillery_module()
+ for(var/obj/item/hardpoint/support/artillery_module/AM in hardpoints)
+ return AM
+ return null
//Called when players try to move vehicle
//Another wrapper for try_move()
@@ -148,59 +183,463 @@
if(!(locate(/obj/item/hardpoint/locomotion/treads) in hardpoints))
return FALSE
+ // this if statement seems redundant with var/success = ..() below
+ // but it prevents a massive performance overhead:
+ // we only run transform math when we are ready to move...
+ // ...instead of every instant while a movement key is held
+ if(!(world.time < next_move))
+ // PRE-STATE snapshot
+ var/list/oldc = _current_center()
+ var/old_cx = oldc[1]
+ var/old_cy = oldc[2]
+ var/old_dir = dir
+
+ var/success = ..()
+ if(success)
+ // POST-STATE snapshot
+ var/list/newc = _current_center()
+ var/new_cx = newc[1]
+ var/new_cy = newc[2]
+ var/new_dir = dir
+ if(new_cx != old_cx || new_cy != old_cy || new_dir != old_dir)
+ _update_riders_after_motion(old_cx, old_cy, old_dir, new_cx, new_cy, new_dir)
+ revalidate_on_top()
+ return TRUE
+
+ // Gunner turret aiming is mouse-driven now (see crew_mousemove() in multitile_interaction.dm).
+ return FALSE
+
+// !!!! No point in keeping this now that you can freely climb onto the tank. !!!!
+// Unless, maybe, you end up stuck in the center (turret) tile.
+
+// /obj/vehicle/multitile/tank/MouseDrop_T(mob/dropped, mob/user)
+// . = ..()
+// if((dropped != user) || !isxeno(user))
+// return
+
+// if(health > 0)
+// to_chat(user, SPAN_XENO("We can't jump over [src] until it is destroyed!"))
+// return
+
+// var/turf/current_turf = get_turf(user)
+// var/dir_to_go = get_dir(current_turf, src)
+// for(var/i in 1 to 3)
+// current_turf = get_step(current_turf, dir_to_go)
+// if(!(current_turf in locs))
+// break
+
+// if(current_turf.density)
+// to_chat(user, SPAN_XENO("The path over [src] is obstructed!"))
+// return
+
+// // Now we check to make sure the turf on the other side of the tank isn't dense too
+// current_turf = get_step(current_turf, dir_to_go)
+// if(current_turf.density)
+// to_chat(user, SPAN_XENO("The path over [src] is obstructed!"))
+// return
+
+// to_chat(user, SPAN_XENO("We begin to jump over [src]..."))
+// if(!do_after(user, 3 SECONDS, INTERRUPT_ALL, BUSY_ICON_HOSTILE))
+// to_chat(user, SPAN_XENO("We stop jumping over [src]."))
+// return
+
+// user.forceMove(current_turf)
+// to_chat(user, SPAN_XENO("We jump to the other side of [src]."))
+
+/**
+ * Applies a jousting effect when crashing above a certain speed.
+ *
+ * Because momentum defines how close a vehicle is to its top speed, 0.5 should mean that the tank will joust its riders...
+ * ...After it crashes at anything while above half of its max speed.
+ *
+ * Keep in mind that momentum decays while standing still, and it can be negative in case we are moving backwards.
+ */
+/obj/vehicle/multitile/tank/on_crash()
+ if(abs(move_momentum) < move_max_momentum * 0.5)
+ return
+ _scatter_riders_on_crash()
+ revalidate_on_top()
+
+/**
+ * Collided handles what happens when something bumps into the tank.
+ *
+ * If we are being bumped by our riders, we'll do nothing.
+ * Otherwise, we will attempt to climb if we pass through the edge cases on each if statement.
+ *
+ * Arguments:
+ * * atom/movable/AM - The atom bumping into the tank.
+ */
+/obj/vehicle/multitile/tank/Collided(atom/movable/AM)
+ if(_is_our_rider(AM))
+ return
+
+ if(!ismob(AM))
+ if(istype(AM, /obj))
+ var/obj/O = AM
+ if(O.buckled_mob)
+ Collided(O.buckled_mob)
return ..()
- if(user != seats[VEHICLE_GUNNER])
- return FALSE
+ var/mob/living/M = AM
+ if(!istype(M))
+ return ..()
+ if(M.action_busy)
+ return
+ if(M.pulledby || M.throwing)
+ return
+ var/turf/facing_turf = get_step(get_turf(M), M.dir)
+ if(!facing_turf)
+ return ..()
+ if(M.resting)
+ M.forceMove(facing_turf)
+ return ..()
+ if(get_turf(M) in locs)
+ return
- var/obj/item/hardpoint/holder/tank_turret/T = null
- for(var/obj/item/hardpoint/holder/tank_turret/TT in hardpoints)
- T = TT
- break
- if(!T)
- return FALSE
+ climb_onto(M, facing_turf)
+ return
- if(direction == GLOB.reverse_dir[T.dir] || direction == T.dir)
- return FALSE
+/**
+ * climb_onto is called by Collided to allow a mob to climb onto the tank.
+ *
+ * We run a number of edge case checks first, then we begin the process of climbing properly.
+ * It takes a few seconds or less, and, after that, if the spot is still reachable (AKA, if the tank hasn't driven away)
+ * then we'll climb up!
+ *
+ * We also call _carry_move_with_grabs to pull grabbed mobs in with us.
+ * ^^ Kind of how you can grab people when stepping into a window frame to pull them in.
+ *
+ * Arguments:
+ * * mob/living/user - The mob trying to get atop the tank
+ * * turf/preferred - The exact 'turf' of the tank we want to climb upon.
+ * * As in: Bumping into the middle tile of a side shouldn't make you climb up onto another tile.
+ */
+/obj/vehicle/multitile/tank/proc/climb_onto(mob/living/user, turf/preferred)
+ if(!istype(user))
+ return
+
+ if(user.action_busy)
+ return
- T.user_rotation(user, turning_angle(T.dir, direction))
- update_icon()
+ if(!_validate_climb_target(user, preferred, TRUE))
+ to_chat(user, SPAN_WARNING("You can't climb there."))
+ return
- return TRUE
+ user.visible_message(
+ SPAN_WARNING("[user] starts climbing onto [src]."),
+ SPAN_WARNING("You start climbing onto [src]."))
-/obj/vehicle/multitile/tank/MouseDrop_T(mob/dropped, mob/user)
- . = ..()
- if((dropped != user) || !isxeno(user))
+ var/climb_speed_factor = (move_momentum / move_max_momentum) // % of our max speed attained.
+
+ if(health == 0) // When broken, everyone takes 0.20 seconds to climb up. Keep in mind that the real perceived value is around 2-3x higher when you take in account lag.
+ if(!do_after (user, 0.20 SECONDS, INTERRUPT_MOVED, BUSY_ICON_CLIMBING, numticks = 1))
+ to_chat(user, SPAN_WARNING("You stop climbing onto [src]."))
+ return
+
+ else if(isxeno(user)) // Xenos take longer to climb to avoid accidentally getting on top while slashing inside boiler gas.
+ // takes between 0.8 seconds when still and 2 seconds when at full speed. (You'd have to climb through one of the sides) OR pounce on it.
+ if(!do_after(user, max((2 * climb_speed_factor), 0.80) SECONDS, INTERRUPT_MOVED, BUSY_ICON_CLIMBING, numticks = 4))
+ to_chat(user, SPAN_WARNING("You stop climbing onto [src]."))
+ return
+
+ // Humans and preds climb up faster. Humans can't climb down instantly.
+ // takes between 0.25 seconds when still and 1.20 second when at full speed (You'd have to climb through one of the sides)
+ else if(!do_after(user, max((1.20 * climb_speed_factor), 0.30) SECONDS, INTERRUPT_MOVED, BUSY_ICON_CLIMBING, numticks = 3))
+ to_chat(user, SPAN_WARNING("You stop climbing onto [src]."))
+ return
+
+ if(!_validate_climb_target(user, preferred, TRUE))
+ to_chat(user, SPAN_WARNING("The spot on [src] is no longer reachable."))
+ return
+
+ _carry_move_with_grabs(user, preferred, TRUE)
+ user.visible_message(
+ SPAN_WARNING("[user] climbs onto [src]."),
+ SPAN_WARNING("You climb onto [src]."))
+ return
+
+/**
+ * climb_down is called inside living.dm by _handle_tank_edge_move(), which is called by Move()
+ *
+ * This proc handles climbing down normally, and, if allowed to run, will result in a successful disembark.
+ *
+ * Climbing down was a bit more tricky to implement than climbing up, since we need to block movement into-
+ * what would otherwise be a 'free', open tile.
+ *
+ * The code is pretty self-explanatory. Of note is the fact that we only interrupt on an incapacitation. This is to allow
+ * easier disembarking even when the tank is moving.
+ * Arguments:
+ * * mob/living/user - The mob trying to get down the tank
+ * * turf/target - The exact 'turf' of the tank we want to climb down onto.
+ */
+/obj/vehicle/multitile/tank/proc/climb_down(mob/living/user, turf/target)
+ if(user.action_busy)
return
- if(health > 0)
- to_chat(user, SPAN_XENO("We can't jump over [src] until it is destroyed!"))
+ if(!_validate_climb_target(user, target, FALSE))
return
- var/turf/current_turf = get_turf(user)
- var/dir_to_go = get_dir(current_turf, src)
- for(var/i in 1 to 3)
- current_turf = get_step(current_turf, dir_to_go)
- if(!(current_turf in locs))
- break
+ var/climb_speed_factor = (move_momentum / move_max_momentum) // % of our max speed attained.
+
+ user.visible_message(
+ SPAN_WARNING("[user] starts climbing down from [src]."),
+ SPAN_WARNING("You start climbing down from [src]."))
- if(current_turf.density)
- to_chat(user, SPAN_XENO("The path over [src] is obstructed!"))
+ if(!ishuman(user)) // Xenos and preds can always climb down with almost no delay to avoid situations where they get stuck atop the tank.
+ // The tiny delay here is meant to prevent fast castes from accidentally disembarking from a misinput
+ if(!do_after(user, 0.20 SECONDS, INTERRUPT_INCAPACITATED, BUSY_ICON_GENERIC, numticks = 1))
+ to_chat(user, SPAN_WARNING("You stop climbing down from [src]."))
return
- // Now we check to make sure the turf on the other side of the tank isn't dense too
- current_turf = get_step(current_turf, dir_to_go)
- if(current_turf.density)
- to_chat(user, SPAN_XENO("The path over [src] is obstructed!"))
+ // Humans take at most 0.9 seconds to climb down from a tank at full speed, and 0.30 when standing still.
+ else if(!do_after(user, max((0.9 * climb_speed_factor), 0.30) SECONDS, INTERRUPT_INCAPACITATED, BUSY_ICON_GENERIC, numticks = 3))
+ to_chat(user, SPAN_WARNING("You stop climbing down from [src]."))
return
- to_chat(user, SPAN_XENO("We begin to jump over [src]..."))
- if(!do_after(user, 3 SECONDS, INTERRUPT_ALL, BUSY_ICON_HOSTILE))
- to_chat(user, SPAN_XENO("We stop jumping over [src]."))
+
+ // edge case where tank moves while we're climbing down
+ if(!_validate_climb_target(user, target, FALSE))
+ to_chat(user, SPAN_WARNING("The spot is no longer reachable."))
return
+ _carry_remove_with_grabs(user, target)
+ user.visible_message(
+ SPAN_WARNING("[user] climbs down from [src]."),
+ SPAN_WARNING("You climb down from [src]."))
+ return
+
+/**
+ * mark_on_top marks mobs as riders of a specific tank.
+ *
+ * This proc does three things:
+ *
+ * It adds a mob to the tank's list of passengers (on_top_mobs)
+ * It attaches a /datum/component/tank_rider to the mob, marking us as the tank it's atop of
+ * It calls _apply_rider_visuals() to set the layer atop the tank's
+ *
+ * If a xeno is hidden, it gets un-hidden.
+ *
+ * Arguments:
+ * * mob/living/rider_mob - The mob being marked ontop.
+ */
+/obj/vehicle/multitile/tank/proc/mark_on_top(mob/living/rider_mob)
+ if(!istype(rider_mob))
+ return
+ if(rider_mob.z != z)
+ return
+ if(!(get_turf(rider_mob) in locs))
+ return
+ if(rider_mob.get_tank_on_top_of() == src)
+ return
+ on_top_mobs |= rider_mob
+ rider_mob.AddComponent(/datum/component/tank_rider, src)
+ _apply_rider_visuals(rider_mob)
+
+ if(isxeno(rider_mob))
+ var/mob/living/carbon/xenomorph/X = rider_mob
+ if(X.layer == XENO_HIDING_LAYER)
+ var/datum/action/xeno_action/onclick/xenohide/hide = get_action(X, /datum/action/xeno_action/onclick/xenohide)
+ if(hide)
+ hide.remove_hide_status() // prevents cheesing the layer system
+
+/**
+ * obj_mark_on_top WOULD be an ad-hoc polymorph of mark_on_top. It does the same thing, but with an obj/ type.
+ *
+ * This proc does three things:
+ *
+ * Checks if the obj can be brought atop a tank.
+ * It adds the obj to the tank's list of objs (on_top_objs)
+ * It attaches a /datum/component/tank_rider to the obj, marking us as the tank it's atop of
+ * It calls _obj_apply_rider_visuals() to set the layer atop the tank's
+ *
+ * Arguments:
+ * * obj/rider_obj - The obj being marked ontop.
+ */
+/obj/vehicle/multitile/tank/proc/obj_mark_on_top(obj/rider_obj)
+ if(!istype(rider_obj))
+ return
+ if(!rider_obj.is_allowed_atop_vehicle)
+ return
+ if(rider_obj.z != z)
+ return
+ if(!(get_turf(rider_obj) in locs))
+ return
+ if(rider_obj.get_tank_on_top_of() == src)
+ rider_obj.layer = TANK_RIDER_OBJ_LAYER // prevents a visual bug with layering
+ return
+ on_top_obj |= rider_obj
+ rider_obj.AddComponent(/datum/component/tank_rider, src)
+ rider_obj.layer = TANK_RIDER_OBJ_LAYER
+ if(istype(rider_obj, /obj/structure/closet/bodybag))
+ var/obj/structure/closet/bodybag/BB = rider_obj
+ if(BB.roller_buckled)
+ BB.pixel_y = BB.buckle_offset + 12
+ else
+ BB.pixel_y = initial(rider_obj.pixel_y) + 12
+ else
+ rider_obj.pixel_y = initial(rider_obj.pixel_y) + 12
+
+/**
+ * clear_on_top removes rider effects from a mob who was previously atop the tank.
+ *
+ * This proc resets the layer and plane of a rider.
+ * This proc removes a rider from the on_top_mobs list of the tank.
+ * This proc removes the mob's /datum/component/tank_rider.
+ *
+ * Arguments:
+ * * mob/living/rider_mob - The mob who has disembarked.
+ */
+/obj/vehicle/multitile/tank/proc/clear_on_top(mob/living/rider_mob)
+ if(!istype(rider_mob))
+ return
+ on_top_mobs -= rider_mob
+ qdel(rider_mob.GetComponent(/datum/component/tank_rider))
+ rider_mob.layer = initial(rider_mob.layer)
+ rider_mob.plane = initial(rider_mob.plane)
+ rider_mob.pixel_y = initial(rider_mob.pixel_y)
+
+/**
+ * obj_clear_on_top removes rider effects from an obj atop the tank.
+ *
+ * This behaves as an ad-hoc polymorphic version of clear_on_top.
+ */
+/obj/vehicle/multitile/tank/proc/obj_clear_on_top(obj/rider_obj)
+ if(!istype(rider_obj))
+ return
+ on_top_obj -= rider_obj
+ qdel(rider_obj.GetComponent(/datum/component/tank_rider))
+ rider_obj.layer = initial(rider_obj.layer)
+ if(istype(rider_obj, /obj/structure/closet/bodybag))
+ var/obj/structure/closet/bodybag/BB = rider_obj
+ if(BB.roller_buckled)
+ BB.pixel_y = BB.buckle_offset
+ else
+ BB.pixel_y = initial(rider_obj.pixel_y)
+ else
+ rider_obj.pixel_y = initial(rider_obj.pixel_y)
+
+/**
+ * Destroy proc. This shouldn't normally be called, but just in case.
+ *
+ * This proc ensures all mobs atop the tank are cleared.
+ * we also cut the on_top_mobs and on_top_obj list, just in case DM doesn't have garbage collection.
+ *
+ */
+/obj/vehicle/multitile/tank/Destroy()
+ for(var/mob/living/M in on_top_mobs.Copy())
+ if(M)
+ clear_on_top(M)
+ for(var/obj/O in on_top_obj.Copy())
+ if(O)
+ obj_clear_on_top(O)
+ on_top_mobs.Cut()
+ on_top_obj.Cut()
+ return ..()
+
+/**
+ * revalidate_on_top checks if a mob is still atop the tank.
+ *
+ * This proc goes through all of the mobs in the tank's on_top_mobs list and either:
+ * - removes them from the list if they're not on the same tile anymore
+ * - refreshes rider visuals
+ *
+ * This proc probably doesn't need to be called, and you might be able to get away with removing it if it's a performance issue.
+ * It's just there for safety.
+ */
+/obj/vehicle/multitile/tank/proc/revalidate_on_top()
+ for(var/mob/living/M in on_top_mobs.Copy())
+ if(!M || M.z != z || !(get_turf(M) in locs))
+ clear_on_top(M)
+ else
+ _apply_rider_visuals(M)
+
+/**
+ * BlockedPassDirs allows us to move atop the tank if we're on top of it.
+ *
+ * You might've noticed that this method is in UpperCamelCase instead of snake_case.
+ * This is because we're overriding a method from /atom.
+ *
+ * This proc checks if the atom mover is a mob, and, if it is, allows it to move freely atop the tank.
+ *
+ * * Arguments:
+ * * atom/movable/mover - The atom attempting to move in a tank turf.
+ * * target_dir - The direction we're moving towards.
+ */
+/obj/vehicle/multitile/tank/BlockedPassDirs(atom/movable/mover, target_dir)
+ if(ismob(mover))
+ var/mob/living/mover_mob = mover
+ if(istype(mover_mob) && mover_mob.get_tank_on_top_of() == src)
+ var/turf/start = get_turf(mover_mob)
+ var/turf/target = get_step(start, target_dir)
+ if(target && (target in src.locs))
+ return NO_BLOCKED_MOVEMENT
+ else if(isobj(mover))
+ var/obj/mover_obj = mover
+ if(mover_obj.is_atop_vehicle())
+ var/turf/start = get_turf(mover_obj)
+ var/turf/target = get_step(start, target_dir)
+ if(target && (target in src.locs))
+ return NO_BLOCKED_MOVEMENT
+ var/ret = ..()
+ if(ret != null)
+ return ret
+ return null
+
+/**
+ * take_damage_type handles taking damage when the tank gets attacked.
+ *
+ * Because xenos can now hop atop the tank to attack it in a fairly one-sided display of force, I've introduced
+ * this function to make destroying the tank a bit slower. This might be removed if/once someone (i) get(s) around to
+ * making tank hardpoints repairable.
+ *
+ * Xenos atop the tank are flat-out unable to hit the Treads module. This is to allow the tank to run back
+ * towards marines. Also, it's impossible to break the treads from ontop. How are you going to reach them?
+ *
+ * Xenos atop the tank also suffer a 30% damage penalty, again, to give time for tank crewmembers to retreat to
+ * safety.
+ *
+ * * Arguments:
+ * * damage - Numeric value of damage.
+ * * type - Damage type. Brute, Slash, Acid...
+ * * atom/attacker - Whoever is damaging the tank
+ */
+/obj/vehicle/multitile/tank/take_damage_type(damage, type, atom/attacker)
+ if(type == "slash" && istype(attacker, /mob/living/carbon/xenomorph))
+ var/mob/living/carbon/xenomorph/attacker_xeno = attacker
+
+ // must actually be on top of THIS tank (exterior), same z, and stading on one of our tiles
+ if(attacker_xeno.get_tank_on_top_of() == src && attacker_xeno.z == z)
+ var/turf/xturf = get_turf(attacker_xeno)
+ if(xturf && (xturf in src.locs))
+ var/adj_damage = damage * 0.7
+ var/all_broken = TRUE
+ var/dmg_multi = get_dmg_multi(type)
+
+ for(var/obj/item/hardpoint/H in hardpoints)
+ if(istype(H, /obj/item/hardpoint/locomotion/treads))
+ continue
+ // turret doesn't count as a 'hardpoint' for all_broken, otherwise the tank is... too tanky.
+ if(istype(H, /obj/item/hardpoint/holder/tank_turret))
+ H.take_damage(floor(adj_damage * dmg_multi))
+ continue
+ if(H.can_take_damage())
+ H.take_damage(floor(adj_damage * dmg_multi))
+ all_broken = FALSE
+
+ // If all *eligible* hardpoints are broken, the frame begins taking full damage...
+ // ...otherwise, the frame takes one tenth, just like the base behavior.
+ if(all_broken)
+ health = max(0, health - adj_damage * dmg_multi)
+ else
+ health = max(0, health - floor(adj_damage * dmg_multi / 10))
+ if(ismob(attacker))
+ var/mob/M = attacker
+ log_attack("[src] took [adj_damage] [type] damage (top-hit penalty) from [M] ([M.client ? M.client.ckey : "disconnected"]).")
+ else
+ log_attack("[src] took [adj_damage] [type] damage (top-hit penalty) from [attacker].")
+ update_icon()
+ return
+ return ..()
- user.forceMove(current_turf)
- to_chat(user, SPAN_XENO("We jump to the other side of [src]."))
/*
** PRESETS SPAWNERS
*/
diff --git a/code/modules/vehicles/tank/tank_helpers.dm b/code/modules/vehicles/tank/tank_helpers.dm
new file mode 100644
index 000000000000..3bee83afa067
--- /dev/null
+++ b/code/modules/vehicles/tank/tank_helpers.dm
@@ -0,0 +1,595 @@
+// --- Helper for crash response
+
+/**
+ * _scatter_riders_on_crash joustles mobs riding atop the tank in a random direction.
+ *
+ * This proc applies a 1.5s WEAKEN effect to all mobs atop it when the tank crashes into something with sufficient speed.
+ * This includes large mobs such as ravagers, queens, kings, et-cetera.
+ *
+ * If a mob happens to be thrown OFF of a tank tile, they will instead get a 3 second stun.
+ * ^^ See Move() function inside living.dm
+ *
+ * The intent of this proc is double: Allowing tank drivers to defend themselves and have a chance against xenos
+ * atop the tank and also punish tank drivers for bad driving by fucking up the marines on top.
+ *
+ * Also scatters objs on top!
+ *
+ */
+/obj/vehicle/multitile/tank/proc/_scatter_riders_on_crash()
+ if(abs(move_momentum) < 1.6)
+ return
+
+ var/sweep_range = 3
+ var/src_z = src.z
+ var/list/src_locs = src.locs
+
+ for(var/obj/O in on_top_obj.Copy())
+ if(!O || O.z != src_z)
+ obj_clear_on_top(O)
+ continue
+
+ var/turf/start = get_turf(O)
+ var/throw_dir = get_dir(src, start)
+ if(!throw_dir)
+ throw_dir = pick(GLOB.cardinals)
+
+ var/turf/target = get_step(start, throw_dir)
+ if(!target || (target in src_locs) || target.density)
+ step_away(O, src, sweep_range, 3)
+ var/turf/cur = get_turf(O)
+ target = get_step(cur, throw_dir)
+
+ if(target && !(target in src_locs) && !target.density)
+ obj_clear_on_top(O)
+ O.throw_atom(target, sweep_range, SPEED_FAST)
+ else if(!(start in src_locs))
+ obj_clear_on_top(O)
+
+ for(var/mob/living/M in on_top_mobs.Copy())
+ if(!M || M.z != src_z)
+ clear_on_top(M)
+ continue
+
+ var/turf/start = get_turf(M)
+ var/throw_dir = get_dir(src, start)
+ if(!throw_dir)
+ throw_dir = pick(GLOB.cardinals)
+
+ var/turf/next_out = get_step(start, throw_dir)
+
+ if(next_out && !(next_out in src_locs) && !next_out.density)
+ M.forceMove(next_out)
+ clear_on_top(M)
+ else
+ step_away(M, src, sweep_range, 3)
+ var/turf/cur = get_turf(M)
+ var/turf/next2 = get_step(cur, throw_dir)
+ if(next2 && !(next2 in src_locs) && !next2.density)
+ M.forceMove(next2)
+ clear_on_top(M)
+
+ to_chat(M, SPAN_WARNING("You're thrown from [src]!"))
+ playsound(M, "punch", 25, TRUE)
+ shake_camera(M, 2, 1)
+
+ var/turf/final_pos = get_turf(M)
+
+ if(!(final_pos in src_locs))
+ M.apply_effect(3, WEAKEN)
+ clear_on_top(M)
+ continue
+
+ var/list/hull_neighbors = list()
+ for(var/d in GLOB.cardinals)
+ var/turf/H = get_step(final_pos, d)
+ if(H && (H in src_locs) && !H.density)
+ hull_neighbors += H
+
+ if(hull_neighbors.len)
+ var/turf/spot = pick(hull_neighbors)
+ M.forceMove(spot)
+ M.apply_effect(1.5, WEAKEN)
+ mark_on_top(M)
+
+// --- motion geometry and transform math
+
+/**
+ * _current_center() builds a matrix of the tank's tiles and returns the center pair.
+ *
+ * The tank occupies a 3x3 set of tiles. We can imagine it as a matrix:
+ *
+ *(10,20) (11,20) (12,20)
+ *(10,21) (11,21) (12,21)
+ *(10,22) (11,22) (12,22)
+ *
+ * This proc gets the middle (1,1) element of the matrix and returns it, in this case, (11,21)
+ *
+ * Returns:
+ * * A LIST of two integer elements. [11,21]
+ */
+/obj/vehicle/multitile/tank/proc/_current_center()
+ var/first = TRUE
+ var/minx
+ var/miny
+ var/maxx
+ var/maxy
+ for(var/turf/T in locs)
+ if(first)
+ minx = maxx = T.x
+ miny = maxy = T.y
+ first = FALSE
+ else
+ if(T.x < minx)
+ minx = T.x
+ if(T.x > maxx)
+ maxx = T.x
+ if(T.y < miny)
+ miny = T.y
+ if(T.y > maxy)
+ maxy = T.y
+
+ return list(minx + 1, miny + 1)
+
+/**
+ * calculates how many 90-degree rotations are needed to go from one cardinal direction to another.
+ *
+ * NORTH → EAST = +1 (90° CW)
+ * NORTH → WEST = -1 (90° CCW)
+ * NORTH → SOUTH = +2 (180°)
+ * EAST → SOUTH = +1 (90° CW)
+ * SOUTH → WEST = +1 (90° CW)
+ *
+ * This proc is a key part of the positioning system when the vehicle rotates.
+ *
+ * Arguments:
+ * * old_dir = The direction we are initially at
+ * * new_dir = The direction we want to be at
+ *
+ * Returns:
+ * * 0 : No rotation needed
+ * * 1 : 90° clockwise rotation
+ * * -1: 90° counter-clockwise rotation
+ * * 2 : 180° rotation. Will never happen because the tank can't turn over like that in one move.
+ */
+/obj/vehicle/multitile/tank/proc/_quarter_turns(old_dir, new_dir)
+ if(old_dir == new_dir)
+ return 0
+ switch(old_dir)
+ if(NORTH)
+ if(new_dir == EAST)
+ return 1
+ if(new_dir == WEST)
+ return -1
+ if(new_dir == SOUTH)
+ return 2
+ if(SOUTH)
+ if(new_dir == WEST)
+ return 1
+ if(new_dir == EAST)
+ return -1
+ if(new_dir == NORTH)
+ return 2
+ if(EAST)
+ if(new_dir == SOUTH)
+ return 1
+ if(new_dir == NORTH)
+ return -1
+ if(new_dir == WEST)
+ return 2
+ if(WEST)
+ if(new_dir == NORTH)
+ return 1
+ if(new_dir == SOUTH)
+ return -1
+ if(new_dir == EAST)
+ return 2
+ return 0
+
+/**
+ * _rotated_offset takes an offset (dx, dy) and rotates it by 90% degree increments
+ *
+ * this proc applies 2D coordinate rotation mathematics to transform a position offset
+ * based on the rotation amount calculated by _quarter_turns.
+ *
+ * Arguments:
+ * * dx = x axis position, relative to the tank
+ * * dy = y axis position, relative to the tank
+ * * k = a numeric value acquired from _quarter_turns to know how many turns we have to do
+ *
+ * Returns:
+ * * a LIST, containing a 2-slot permutation of {{dx, -dx} {dy, -dy}} depending on K.
+ */
+/obj/vehicle/multitile/tank/proc/_rotated_offset(dx, dy, k)
+ if(!k)
+ return list(dx, dy)
+ if(k == 2)
+ return list(-dx, -dy) // 180°
+ if(k == 1)
+ return list(dy, -dx) // CW 90°
+ // k == -1 (CCW 90°)
+ return list(-dy, dx)
+
+/**
+ * _update_riders_after_motion coordinates moving all mobs atop the tank whenever it moves or rotates.
+ *
+ * This proc is the main coordination function that handles moving all riders atop the tank.
+ * It combines translation, rotation, and collision detection to keep riders properly positioned on the hull.
+ *
+ * First it calls _quarter_turns to understand what kind of rotations it'll need to turn from one orientation to the other.
+ * Then it calls _rotated_offset to know where to position the riders.
+ * Lastly, it applies those changes.
+ *
+ * Arguments:
+ * * old_cx = the X axis position currently occupied by the tank
+ * * old_cy = the y axis position currently occupied by the tank
+ * * old_dir = the direction the tank is currently facing. N/E/S/W
+ * * new_cx = the X axis position that we are moving to
+ * * new_cy = the y axis position that we are moving to
+ * * new_dir = the direction we want to face. N/E/S/W
+ *
+ */
+/obj/vehicle/multitile/tank/proc/_update_riders_after_motion(old_cx, old_cy, old_dir, new_cx, new_cy, new_dir)
+ var/k = _quarter_turns(old_dir, new_dir) // -1,0,1,2
+ var/src_z = src.z
+ var/list/src_locs = src.locs
+
+ // simpler version for objs
+ for(var/obj/O in on_top_obj.Copy())
+ if(!O || O.z != src_z)
+ obj_clear_on_top(O)
+ continue
+
+ // Don't move body bags atop roller beds. We will handle those when we move the roller beds themselves.
+ if(istype(O, /obj/structure/closet/bodybag))
+ var/obj/structure/closet/bodybag/BB = O
+ if(BB.roller_buckled)
+ continue
+
+ var/turf/from = get_turf(O)
+ if(!from)
+ obj_clear_on_top(O)
+ continue
+
+ var/list/rd = _rotated_offset(from.x - old_cx, from.y - old_cy, k)
+ var/turf/target = locate(new_cx + rd[1], new_cy + rd[2], src_z)
+
+ if(target && (target in src_locs) && target != from)
+ O.forceMove(target)
+ obj_mark_on_top(O)
+ if(istype(O, /obj/structure/bed/roller))
+ var/obj/structure/bed/roller/R = O
+ if(R.buckled_bodybag)
+ R.buckled_bodybag.forceMove(target)
+ obj_mark_on_top(R.buckled_bodybag)
+ else if(from in src_locs)
+ obj_mark_on_top(O)
+ else
+ obj_clear_on_top(O)
+
+ for(var/mob/living/M in on_top_mobs.Copy())
+ if(!M || M.z != src_z)
+ clear_on_top(M)
+ continue
+
+ // If the mob is buckled to something, we'll skip it, because the buckled obj has already moved with it.
+ if(M.buckled)
+ continue
+ var/turf/from = get_turf(M)
+ if(!from)
+ clear_on_top(M)
+ continue
+
+ // Offset from OLD center -> rotate by k -> translate to NEW center
+ var/list/rd = _rotated_offset(from.x - old_cx, from.y - old_cy, k)
+ var/turf/target = locate(new_cx + rd[1], new_cy + rd[2], src_z)
+
+ // this shouldn't happen, but just to be safe.
+ if(!target || !(target in src_locs))
+ if(from in src_locs)
+ mark_on_top(M)
+ else
+ clear_on_top(M)
+ continue
+
+ // If target is blocked by a non-rider dense atom, (shouldn't happen cuz tank runs over shit) keep if possible
+ var/blocked = target.density // Start with turf density check
+ if(!blocked)
+ for(var/atom/A in target)
+ if(A != src && A.density && (!ismob(A) || !(A in on_top_mobs)))
+ blocked = TRUE
+ break
+
+ if(blocked)
+ if(from in src_locs)
+ mark_on_top(M)
+ else
+ clear_on_top(M)
+ continue
+
+ // Move and keep elevation
+ if(target != from)
+ M.forceMove(target)
+ mark_on_top(M)
+
+// --- Helper for layering effects
+
+/**
+ * simply brings a rider's layer atop the tanks.
+ *
+ * TANK_RIDER_LAYER being at 4.51 is not arbitrary.
+ * The tank chassis is set at 4.1, but the turret is at 4.5.
+ * Therefore, 4.51 is needed to avoid clipping.
+ *
+ * Arguments:
+ * * mob/living/M = Mob whose layer priority is being increased.
+ */
+/obj/vehicle/multitile/tank/proc/_apply_rider_visuals(mob/living/M)
+ M.layer = TANK_RIDER_LAYER
+ M.pixel_y = M.old_y + 12
+
+// --- helpers for handlign interactions with grabbed mobs
+
+/**
+ * This proc allows a marine to pull another one up the tank once he finishes climbing.
+ *
+ * _carry_move_with_grabs does something similar to grabbing someone as you climb a window frame:
+ * You'll pull the marine you're grabbing up with you as soon as YOUR climb_onto finishes.
+ * The marine pulled up will get a 2 second weaken, but they can be shaked out of it.
+ *
+ * !!!! The order of the operations of clearing, marking and moving is not arbitrary !!!!
+ *
+ * Arguments:
+ * * mob/living/user = Mob doing the pulling.
+ * * turf/dest = Which turf on the tank we're moving to
+ */
+/obj/vehicle/multitile/tank/proc/_carry_move_with_grabs(mob/living/user, turf/dest)
+ var/list/grabbed_things = list()
+ for(var/obj/item/grab/G in list(user.l_hand, user.r_hand))
+ if(G?.grabbed_thing)
+ grabbed_things += G.grabbed_thing
+
+ for(var/atom/movable/thing as anything in grabbed_things)
+ if(isliving(thing))
+ var/mob/living/L = thing
+ L.apply_effect(2, WEAKEN)
+ _add_to_top_buckled_to(L, dest)
+ L.forceMove(dest)
+ mark_on_top(L)
+
+ else if(isobj(thing))
+ var/obj/O = thing
+ if(O.is_allowed_atop_vehicle)
+ _add_to_top_buckled_entity(O, dest)
+
+ user.forceMove(dest)
+ mark_on_top(user)
+
+/**
+ * Handles the addition of a roller bed whom a Mob is buckled to onto the top of the tank
+ *
+ * Mobs can be buckled to roller beds and pulled atop the tank. If this happens, we need to take the roller bed up with us
+ *
+ * Arguments:
+ * * mob/living/L = The mob that may be buckled to a roller bed.
+ * * turf/dest = Which turf onto the tank we're moving to
+ */
+
+/obj/vehicle/multitile/tank/proc/_add_to_top_buckled_to(mob/living/L, turf/dest)
+ if(L.buckled && istype(L.buckled, /obj/structure/bed/roller))
+ var/obj/structure/bed/roller/R = L.buckled
+ R.forceMove(dest)
+ obj_mark_on_top(R)
+
+/**
+ * Handles the addition of entities buckled to an obj to atop the tank
+ *
+ * Roller beds can have mobs and body or stasis bags attached to it. If we are pulling a roller bed onhto the tank with an attached...
+ * bag or mob, we should ensure those go up with it correctly.
+ *
+ * Arguments:
+ * * obj/object = The object that may have living beings buckled to it
+ * * turf/dest = Which turf onto the tank we're moving to
+ */
+
+/obj/vehicle/multitile/tank/proc/_add_to_top_buckled_entity(obj/object, turf/dest)
+ if(istype(object, /obj/structure/bed/roller))
+ var/obj/structure/bed/roller/R = object
+
+ R.forceMove(dest)
+ obj_mark_on_top(R)
+ if(R.buckled_mob)
+ var/mob/living/L = R.buckled_mob
+ L.forceMove(dest)
+ mark_on_top(L)
+ else if (R.buckled_bodybag)
+ var/obj/structure/closet/bodybag/BB = R.buckled_bodybag
+ BB.forceMove(dest)
+ obj_mark_on_top(BB)
+ else if (istype(object, /obj/structure/closet/bodybag)) // Grab is on a bodybag, possibly atop a roller.
+ var/obj/structure/closet/bodybag/BB = object
+
+ if (BB.roller_buckled)
+ var/obj/structure/bed/roller/R = BB.roller_buckled
+ R.forceMove(dest)
+ obj_mark_on_top(R) // mark ASAP to prevent bodybag from unbuckling
+ BB.forceMove(dest)
+ obj_mark_on_top(BB)
+
+ else // lone bodybag
+ BB.forceMove(dest)
+ obj_mark_on_top(BB)
+
+ else // any other obj
+ object.forceMove(dest)
+ obj_mark_on_top(object)
+
+/**
+ * This proc allows a marine to pull another one DOWN the tank once he finishes climbing down.
+ *
+ * Opposite of carry_move_with_grabs. This one does the same thing but for climbing DOWN.
+ *
+ * !!!! The order of the operations of clearing, marking and moving is not arbitrary !!!!
+ *
+ * Arguments:
+ * * mob/living/user = Mob doing the pulling.
+ * * turf/dest = Which turf on the tank we're moving to
+ */
+/obj/vehicle/multitile/tank/proc/_carry_remove_with_grabs(mob/living/user, turf/dest)
+ var/list/grabbed_things = list()
+ for(var/obj/item/grab/G in list(user.l_hand, user.r_hand))
+ if(G?.grabbed_thing)
+ grabbed_things += G.grabbed_thing
+
+ for(var/atom/movable/thing as anything in grabbed_things)
+ if(isliving(thing)) // GRAB is on MOB
+ var/mob/living/L = thing
+ _remove_from_top_buckled_to(L, dest)
+ clear_on_top(L)
+ L.forceMove(dest)
+
+ else if(isobj(thing)) // GRAB is on OBJ
+ var/obj/O = thing
+ if(O.is_allowed_atop_vehicle)
+ _remove_from_top_buckled_entity(O, dest)
+
+ clear_on_top(user)
+ user.forceMove(dest)
+
+/**
+ * Handles the removal of a roller bed whom a Mob is buckled to off the top of the tank
+ *
+ * Mobs can be buckled to roller beds and pulled off of the tank. If this happens, we need to take the roller bed down with us
+ *
+ * Arguments:
+ * * mob/living/L = The mob that may be buckled to a roller bed.
+ * * turf/dest = Which turf off the tank we're moving to
+ */
+
+/obj/vehicle/multitile/tank/proc/_remove_from_top_buckled_to(mob/living/L, turf/dest)
+ if(L.buckled && istype(L.buckled, /obj/structure/bed/roller))
+ var/obj/structure/bed/roller/R = L.buckled
+ R.forceMove(dest)
+ obj_clear_on_top(R)
+
+/**
+ * Handles the removal of entities buckled to an obj from atop the tank
+ *
+ * Roller beds can have mobs and body or stasis bags attached to it. If we are pulling a roller bed down the tank with an attached...
+ * bag or mob, we should ensure those go down with it correctly.
+ *
+ * Arguments:
+ * * obj/object = The object that may have living beings buckled to it
+ * * turf/dest = Which turf off the tank we're moving to
+ */
+
+/obj/vehicle/multitile/tank/proc/_remove_from_top_buckled_entity(obj/object, turf/dest)
+ if(istype(object, /obj/structure/bed/roller))
+ var/obj/structure/bed/roller/R = object
+
+ if(R.buckled_mob)
+ var/mob/living/L = R.buckled_mob
+ obj_clear_on_top(object) // clear first then move to prevent buckled mobs from getting debuffed for improperly dismounting.
+ object.forceMove(dest)
+ clear_on_top(L)
+
+ else if (R.buckled_bodybag)
+ var/obj/structure/closet/bodybag/BB = R.buckled_bodybag
+ object.forceMove(dest)
+ obj_clear_on_top(object)
+ BB.forceMove(dest) // must be moved last to avoid unbuckling
+ obj_clear_on_top(BB)
+
+ else // empty roller bed
+ object.forceMove(dest)
+ obj_clear_on_top(object)
+
+ else if (istype(object, /obj/structure/closet/bodybag)) // Grab is on a bodybag, possibly atop a roller.
+ var/obj/structure/closet/bodybag/BB = object
+
+ if (BB.roller_buckled)
+ var/obj/structure/bed/roller/R = BB.roller_buckled
+ R.forceMove(dest)
+ obj_clear_on_top(R)
+ BB.forceMove(dest) // must be moved last to avoid unbuckling
+ obj_clear_on_top(BB)
+
+ else // lone bodybag
+ object.forceMove(dest)
+ obj_clear_on_top(object)
+
+ else // any other item
+ object.forceMove(dest)
+ obj_clear_on_top(object)
+
+// --- blocking helpers. Checks if turf is blocked, etc.
+
+/**
+ * This proc checks a specific turf and returns false if anything dense is on it, except for a mob.
+ *
+ * _blocked_except_mobs is a helper that check if a turf/T has anything dense in it.
+ * If there is something dense in it and it is NOT a mob, it returns TRUE, otherwise, it returns false.
+ *
+ * Arguments:
+ * * turf/T = Which turf we're checking for dense atoms in
+ *
+ * Returns:
+ * * TRUE = If something dense is present, and it is not a mob.
+ * * FALSE = If the turf is clear of dense objects, or if the only dense objects in it are mobs.
+ */
+/obj/vehicle/multitile/tank/proc/_blocked_except_mobs(turf/T)
+ if(!istype(T))
+ return TRUE
+ if(T.density)
+ return TRUE
+ for(var/atom/A in T)
+ if(A == src)
+ continue
+ if(ismob(A))
+ continue
+ if(A.density)
+ return TRUE
+ return FALSE
+
+/**
+ * This proc checks if a specific turf/spot is a valid climbing target
+ *
+ * We are mostly interested in ensuring that the tank hasn't moved too far away, and this function helps with that.
+ * It also has a couple of other edge cases such as being in a different Z, or having a climb-spot blocked by
+ * anything other than a mob, juuuuust to be safe.
+ *
+ * Arguments:
+ * * turf/spot = Which turf we want to climb onto
+ * * mob/living/user = Whoever wants to climb onto spot
+ *
+ * Returns:
+ * * TRUE = If nothing is blocking the turf
+ * * FALSE = If something is blocking the turf.
+ */
+/obj/vehicle/multitile/tank/proc/_validate_climb_target(mob/living/user, turf/spot)
+ if(!istype(user) || !istype(spot))
+ return FALSE
+ if(user.z != z)
+ return FALSE
+ if(get_dist(spot, get_turf(user)) != 1)
+ return FALSE
+ if(_blocked_except_mobs(spot))
+ return FALSE
+ return TRUE
+
+/**
+ * Helper function to know if a mob is a rider of this specific tank
+ *
+ * Checks if this tank is the tank the given mob's /datum/component/tank_rider is tracking.
+ *
+ * Arguments:
+ * * atom/movable/AM = whatever mob/atom we want to check if its a rider.
+ *
+ * Returns:
+ * * TRUE = If AM is a mob/living and this tank is what its tank_rider component points to.
+ * * FALSE = If AM is not a mob/living, or if this tank isn't what its tank_rider component points to.
+ */
+/obj/vehicle/multitile/tank/proc/_is_our_rider(atom/movable/AM)
+ if(!ismob(AM))
+ return FALSE
+ var/mob/living/rider_mob = AM
+ return istype(rider_mob) && rider_mob.get_tank_on_top_of() == src
diff --git a/colonialmarines.dme b/colonialmarines.dme
index c92d4825079d..706807b60cae 100644
--- a/colonialmarines.dme
+++ b/colonialmarines.dme
@@ -460,6 +460,7 @@
#include "code\datums\components\speed_modifier.dm"
#include "code\datums\components\status_effect_component.dm"
#include "code\datums\components\tacmap.dm"
+#include "code\datums\components\tank_rider.dm"
#include "code\datums\components\temporary_mute.dm"
#include "code\datums\components\toxin_buildup.dm"
#include "code\datums\components\tracker_bullets.dm"
@@ -2112,6 +2113,7 @@
#include "code\modules\mob\living\brain\MMI.dm"
#include "code\modules\mob\living\brain\say.dm"
#include "code\modules\mob\living\carbon\carbon.dm"
+#include "code\modules\mob\living\carbon\carbon_debug_verbs.dm"
#include "code\modules\mob\living\carbon\carbon_defines.dm"
#include "code\modules\mob\living\carbon\carbon_helpers.dm"
#include "code\modules\mob\living\carbon\give.dm"
@@ -2651,6 +2653,7 @@
#include "code\modules\vehicles\hardpoints\hardpoint_ammo\cupola_ammo.dm"
#include "code\modules\vehicles\hardpoints\hardpoint_ammo\dualcannon_ammo.dm"
#include "code\modules\vehicles\hardpoints\hardpoint_ammo\firing_port_weapon_ammo.dm"
+#include "code\modules\vehicles\hardpoints\hardpoint_ammo\flare_ammo.dm"
#include "code\modules\vehicles\hardpoints\hardpoint_ammo\flare_launcher_ammo.dm"
#include "code\modules\vehicles\hardpoints\hardpoint_ammo\gl_ammo.dm"
#include "code\modules\vehicles\hardpoints\hardpoint_ammo\hardpoint_ammo.dm"
@@ -2658,7 +2661,6 @@
#include "code\modules\vehicles\hardpoints\hardpoint_ammo\minigun_ammo.dm"
#include "code\modules\vehicles\hardpoints\hardpoint_ammo\primary_flamer_ammo.dm"
#include "code\modules\vehicles\hardpoints\hardpoint_ammo\secondary_flamer_ammo.dm"
-#include "code\modules\vehicles\hardpoints\hardpoint_ammo\smoke_ammo.dm"
#include "code\modules\vehicles\hardpoints\hardpoint_ammo\tow_ammo.dm"
#include "code\modules\vehicles\hardpoints\holder\holder.dm"
#include "code\modules\vehicles\hardpoints\holder\tank_turret.dm"
@@ -2669,7 +2671,9 @@
#include "code\modules\vehicles\hardpoints\primary\ltb.dm"
#include "code\modules\vehicles\hardpoints\primary\minigun.dm"
#include "code\modules\vehicles\hardpoints\primary\primary.dm"
+#include "code\modules\vehicles\hardpoints\secondary\brute_launcher.dm"
#include "code\modules\vehicles\hardpoints\secondary\cupola.dm"
+#include "code\modules\vehicles\hardpoints\secondary\flag.dm"
#include "code\modules\vehicles\hardpoints\secondary\flamer.dm"
#include "code\modules\vehicles\hardpoints\secondary\frontal_cannon.dm"
#include "code\modules\vehicles\hardpoints\secondary\grenade_launcher.dm"
@@ -2693,6 +2697,7 @@
#include "code\modules\vehicles\interior\interior_hull.dm"
#include "code\modules\vehicles\interior\interior_landmarks.dm"
#include "code\modules\vehicles\interior\interactable\doors.dm"
+#include "code\modules\vehicles\interior\interactable\intercom.dm"
#include "code\modules\vehicles\interior\interactable\seats.dm"
#include "code\modules\vehicles\interior\interactable\vehicle_locker.dm"
#include "code\modules\vehicles\interior\interactable\vendors.dm"
@@ -2706,6 +2711,7 @@
#include "code\modules\vehicles\multitile\multitile_verbs.dm"
#include "code\modules\vehicles\tank\interior.dm"
#include "code\modules\vehicles\tank\tank.dm"
+#include "code\modules\vehicles\tank\tank_helpers.dm"
#include "code\modules\vehicles\van\box_van.dm"
#include "code\modules\vehicles\van\clf_van.dm"
#include "code\modules\vehicles\van\interior.dm"
diff --git a/icons/obj/items/weapons/guns/ammo_by_faction/USCM/vehicles.dmi b/icons/obj/items/weapons/guns/ammo_by_faction/USCM/vehicles.dmi
index 612d4cc9f43a..ed1df2927236 100644
Binary files a/icons/obj/items/weapons/guns/ammo_by_faction/USCM/vehicles.dmi and b/icons/obj/items/weapons/guns/ammo_by_faction/USCM/vehicles.dmi differ
diff --git a/icons/obj/vehicles/hardpoints/tank.dmi b/icons/obj/vehicles/hardpoints/tank.dmi
index c9af30849eb8..eb6512e61629 100644
Binary files a/icons/obj/vehicles/hardpoints/tank.dmi and b/icons/obj/vehicles/hardpoints/tank.dmi differ
diff --git a/icons/obj/vehicles/tank.dmi b/icons/obj/vehicles/tank.dmi
index 1991bc9caa70..9ec706743234 100644
Binary files a/icons/obj/vehicles/tank.dmi and b/icons/obj/vehicles/tank.dmi differ
diff --git a/maps/interiors/tank.dmm b/maps/interiors/tank.dmm
index e5c019e01993..e36eead70aab 100644
--- a/maps/interiors/tank.dmm
+++ b/maps/interiors/tank.dmm
@@ -13,7 +13,13 @@
phone_category = "Vehicles";
phone_id = "M34A2 Longstreet Light Tank";
pixel_x = 22;
- pixel_y = -14
+ pixel_y = -6
+ },
+/obj/effect/landmark/interior/spawn/intercom{
+ pixel_x = 22;
+ pixel_y = -15;
+ dir = 8;
+ drag_delay = 3.05
},
/turf/open/shuttle/vehicle/floor_1_12,
/area/interior/vehicle/tank)
@@ -67,7 +73,8 @@
alpha = 50;
icon_state = "exterior_2";
layer = 5.2;
- pixel_y = 32
+ pixel_y = 32;
+ pixel_x = 0
},
/turf/open/void/vehicle,
/area/space)
diff --git a/maps/map_files/Tyrargo_Rift/Tyrargo_Rift.dmm b/maps/map_files/Tyrargo_Rift/Tyrargo_Rift.dmm
index cf030992a157..f80ac950d5e5 100644
--- a/maps/map_files/Tyrargo_Rift/Tyrargo_Rift.dmm
+++ b/maps/map_files/Tyrargo_Rift/Tyrargo_Rift.dmm
@@ -56373,8 +56373,8 @@
/area/tyrargo/oob/outdoors)
"cKo" = (
/obj/structure/surface/rack,
-/obj/item/ammo_magazine/hardpoint/turret_smoke,
-/obj/item/ammo_magazine/hardpoint/turret_smoke,
+/obj/item/storage/box/packet/flare,
+/obj/item/storage/box/packet/flare,
/obj/item/ammo_magazine/hardpoint/m56_cupola,
/obj/item/ammo_magazine/hardpoint/m56_cupola,
/turf/open/auto_turf/tyrargo_grass/layer0_mud,
diff --git a/tgui/packages/tgui/interfaces/TankPivotTuner.tsx b/tgui/packages/tgui/interfaces/TankPivotTuner.tsx
new file mode 100644
index 000000000000..329cd3258533
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/TankPivotTuner.tsx
@@ -0,0 +1,115 @@
+import type { BooleanLike } from 'common/react';
+import { useBackend } from 'tgui/backend';
+import { Button, LabeledList, Section, Stack } from 'tgui/components';
+import { Window } from 'tgui/layouts';
+
+type Weapon = {
+ ref: string;
+ name: string;
+ pivot_x: number;
+ pivot_y: number;
+ selected: BooleanLike;
+};
+
+type Data = {
+ dir_name: string;
+ weapons: Weapon[];
+ selected_name: string;
+ tuning_label: string;
+ pivot_x: number;
+ pivot_y: number;
+ offset_x: number;
+ offset_y: number;
+ is_gimballed: BooleanLike;
+ gimbal_pivot_x: number;
+ gimbal_pivot_y: number;
+};
+
+const AXIS_STEPS = [-10, -1, 1, 10];
+
+const AxisRow = (props: {
+ readonly axis: 'x' | 'y';
+ readonly value: number;
+ readonly target: 'rotation' | 'gimbal' | 'offset';
+}) => {
+ const { act } = useBackend();
+ const { axis, value, target } = props;
+ return (
+
+
+ {AXIS_STEPS.map((step) => (
+
+
+
+ ))}
+
+
+ );
+};
+
+export const TankPivotTuner = (props) => {
+ const { act, data } = useBackend();
+ const {
+ dir_name,
+ weapons = [],
+ selected_name,
+ tuning_label,
+ pivot_x,
+ pivot_y,
+ offset_x,
+ offset_y,
+ is_gimballed,
+ gimbal_pivot_x,
+ gimbal_pivot_y,
+ } = data;
+
+ return (
+
+
+
+
+ {weapons.map((weaponEntry) => (
+
+
+
+ ))}
+
+
+ {selected_name && (
+
+ )}
+ {selected_name && (
+
+ )}
+ {selected_name && is_gimballed && (
+
+ )}
+
+
+ );
+};