diff --git a/code/modules/vehicles/hardpoints/hardpoint.dm b/code/modules/vehicles/hardpoints/hardpoint.dm
index 5963b0b4e36b..acdefca18fd2 100644
--- a/code/modules/vehicles/hardpoints/hardpoint.dm
+++ b/code/modules/vehicles/hardpoints/hardpoint.dm
@@ -1,22 +1,20 @@
-/*
- Hardpoints are any items that attach to a base vehicle, such as wheels/treads, support systems and guns
-*/
-
+/**
+ * Hardpoints are any items that attach to a base vehicle, such as wheels/treads, support systems and guns
+ */
/obj/item/hardpoint
//------MAIN VARS----------
- // Which slot is this hardpoint in
- // Purely to check for conflicting hardpoints
+ /// Which slot is this hardpoint in. Purely to check for conflicting hardpoints.
var/slot
- // The vehicle this hardpoint is installed on
+ /// The vehicle this hardpoint is installed on.
var/obj/vehicle/multitile/owner
health = 100
w_class = SIZE_LARGE
- // Determines how much of any incoming damage is actually taken
+ /// Determines how much of any incoming damage is actually taken.
var/damage_multiplier = 1
- // Origin coords of the hardpoint relative to the vehicle
+ /// Origin coords of the hardpoint relative to the vehicle.
var/list/origins = list(0, 0)
var/list/buff_multipliers
@@ -32,13 +30,13 @@
var/disp_icon //This also differentiates tank vs apc vs other
var/disp_icon_state
- // List of pixel offsets for each direction
+ /// List of pixel offsets for each direction.
var/list/px_offsets
- //visual layer of hardpoint when on vehicle
+ /// Visual layer of hardpoint when on vehicle.
var/hdpt_layer = HDPT_LAYER_WHEELS
- // List of offsets for where to place the muzzle flash for each direction
+ /// List of offsets for where to place the muzzle flash for each direction.
var/list/muzzle_flash_pos = list(
"1" = list(0, 0),
"2" = list(0, 0),
@@ -54,33 +52,23 @@
var/const_mz_offset_y = 0
//------SOUNDS VARS----------
- // Sounds to play when the module activated/fired
+ /// Sounds to play when the module activated/fired.
var/list/activation_sounds
//------INTERACTION VARS----------
- //which seat can use this module
+ /// Which seat can use this module.
var/allowed_seat = VEHICLE_GUNNER
- //Cooldown on use of the hardpoint
- var/cooldown = 100
- var/next_use = 0
-
- //whether hardpoint has activatable ability like shooting or zooming
+ /// Whether hardpoint has activatable ability like shooting or zooming.
var/activatable = 0
- //used to prevent welder click spam
+ /// Used to prevent welder click spam.
var/being_repaired = FALSE
- //current user. We can have only one user at a time. Better never change that
- var/user
-
- //Accuracy of the hardpoint. (which is, in fact, a scatter. Need to change this system)
- var/accuracy = 1
-
- // The firing arc of this hardpoint
+ /// The firing arc of this hardpoint.
var/firing_arc = 0 //in degrees. 0 skips whole arc of fire check
// Muzzleflash
@@ -91,17 +79,53 @@
//------AMMUNITION VARS----------
- //Currently loaded ammo that we shoot from
+ /// Currently loaded ammo that we shoot from.
var/obj/item/ammo_magazine/hardpoint/ammo
- //spare magazines that we can reload from
+ /// Spare magazines that we can reload from.
var/list/backup_clips
- //maximum amount of spare mags
+ /// Maximum amount of spare mags.
var/max_clips = 0
/// An assoc list in the format list(/datum/element/bullet_trait_to_give = list(...args))
- /// that will be given to a projectile fired from the hardpoint
+ /// that will be given to a projectile fired from the hardpoint.
var/list/list/traits_to_give
+ /// How much the bullet scatters when fired, in degrees.
+ var/scatter = 0
+ /// How many bullets the gun fired while burst firing/auto firing.
+ var/shots_fired = 0
+ /// Delay before a new firing sequence can start.
+ COOLDOWN_DECLARE(fire_cooldown)
+
+ // Firemodes.
+ /// Current selected firemode of the gun.
+ var/gun_firemode = GUN_FIREMODE_SEMIAUTO
+ /// List of allowed firemodes.
+ var/list/gun_firemode_list = list(
+ GUN_FIREMODE_SEMIAUTO,
+ )
+
+ // Semi-auto and full-auto.
+ /// For regular shots, how long to wait before firing again. Use modify_fire_delay and set_fire_delay instead of modifying this on the fly
+ var/fire_delay = 0
+ /// The multiplier for how much slower this should fire in automatic mode. 1 is normal, 1.2 is 20% slower, 2 is 100% slower, etc. Protected due to it never needing to be edited.
+ var/autofire_slow_mult = 1
+ /// If the gun is currently auto firing.
+ var/auto_firing = FALSE
+
+ // Burst fire.
+ /// How many shots can the weapon shoot in burst? Anything less than 2 and you cannot toggle burst. Use modify_burst_amount and set_burst_amount instead of modifying this
+ var/burst_amount = 1
+ /// The delay in between shots. Lower = less delay = faster. Use modify_burst_delay and set_burst_delay instead of modifying this
+ var/burst_delay = 1
+ /// When burst-firing, this number is extra time before the weapon can fire again.
+ var/extra_delay = 0
+ /// If the gun is currently burst firing.
+ var/burst_firing = FALSE
+
+ /// Currently selected target to fire at. Set with set_target().
+ var/atom/target
+
//-----------------------------
//------GENERAL PROCS----------
//-----------------------------
@@ -109,6 +133,7 @@
/obj/item/hardpoint/Initialize()
. = ..()
set_bullet_traits()
+ 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()
if(owner)
@@ -117,7 +142,7 @@
owner = null
QDEL_NULL_LIST(backup_clips)
QDEL_NULL(ammo)
-
+ set_target(null)
return ..()
/obj/item/hardpoint/ex_act(severity)
@@ -166,37 +191,64 @@
/obj/item/hardpoint/proc/get_integrity_percent()
return 100.0*health/initial(health)
-/obj/item/hardpoint/proc/on_install(obj/vehicle/multitile/V)
- apply_buff(V)
- return
+/// Apply hardpoint effects to vehicle and self.
+/obj/item/hardpoint/proc/on_install(obj/vehicle/multitile/vehicle)
+ if(!vehicle) //in loose holder
+ return
+ RegisterSignal(vehicle, COMSIG_GUN_RECALCULATE_ATTACHMENT_BONUSES, PROC_REF(recalculate_hardpoint_bonuses))
+ apply_buff(vehicle)
-/obj/item/hardpoint/proc/on_uninstall(obj/vehicle/multitile/V)
- remove_buff(V)
- return
+/// Remove hardpoint effects from vehicle and self.
+/obj/item/hardpoint/proc/on_uninstall(obj/vehicle/multitile/vehicle)
+ if(!vehicle) //in loose holder
+ return
+ UnregisterSignal(vehicle, COMSIG_GUN_RECALCULATE_ATTACHMENT_BONUSES)
+ remove_buff(vehicle)
+ //resetting values like set_gun_config_values() would be tidy, but unnecessary as it gets recalc'd on install anyway
-//applying passive buffs like damage type resistance, speed, accuracy, cooldowns
-/obj/item/hardpoint/proc/apply_buff(obj/vehicle/multitile/V)
+/// Applying passive buffs like damage type resistance, speed, accuracy, cooldowns.
+/obj/item/hardpoint/proc/apply_buff(obj/vehicle/multitile/vehicle)
if(buff_applied)
return
if(LAZYLEN(type_multipliers))
for(var/type in type_multipliers)
- V.dmg_multipliers[type] *= LAZYACCESS(type_multipliers, type)
+ vehicle.dmg_multipliers[type] *= LAZYACCESS(type_multipliers, type)
if(LAZYLEN(buff_multipliers))
for(var/type in buff_multipliers)
- V.misc_multipliers[type] *= LAZYACCESS(buff_multipliers, type)
+ vehicle.misc_multipliers[type] *= LAZYACCESS(buff_multipliers, type)
buff_applied = TRUE
+ SEND_SIGNAL(vehicle, COMSIG_GUN_RECALCULATE_ATTACHMENT_BONUSES)
-//removing buffs
-/obj/item/hardpoint/proc/remove_buff(obj/vehicle/multitile/V)
+/// Removing passive buffs like damage type resistance, speed, accuracy, cooldowns.
+/obj/item/hardpoint/proc/remove_buff(obj/vehicle/multitile/vehicle)
if(!buff_applied)
return
if(LAZYLEN(type_multipliers))
for(var/type in type_multipliers)
- V.dmg_multipliers[type] *= 1 / LAZYACCESS(type_multipliers, type)
+ vehicle.dmg_multipliers[type] *= 1 / LAZYACCESS(type_multipliers, type)
if(LAZYLEN(buff_multipliers))
for(var/type in buff_multipliers)
- V.misc_multipliers[type] *= 1 / LAZYACCESS(buff_multipliers, type)
+ vehicle.misc_multipliers[type] *= 1 / LAZYACCESS(buff_multipliers, type)
buff_applied = FALSE
+ SEND_SIGNAL(vehicle, COMSIG_GUN_RECALCULATE_ATTACHMENT_BONUSES)
+
+/// Recalculates hardpoint values based on vehicle modifiers.
+/obj/item/hardpoint/proc/recalculate_hardpoint_bonuses()
+ scatter = initial(scatter) / owner.misc_multipliers["accuracy"]
+ var/cooldown_mult = owner.misc_multipliers["cooldown"]
+ set_fire_delay(initial(fire_delay) * cooldown_mult)
+ set_burst_delay(initial(burst_delay) * cooldown_mult)
+ extra_delay = initial(extra_delay) * cooldown_mult
+
+/// Setter for fire_delay.
+/obj/item/hardpoint/proc/set_fire_delay(value)
+ fire_delay = value
+ SEND_SIGNAL(src, COMSIG_GUN_AUTOFIREDELAY_MODIFIED, fire_delay)
+
+/// Setter for burst_delay.
+/obj/item/hardpoint/proc/set_burst_delay(value)
+ burst_delay = value
+ SEND_SIGNAL(src, COMSIG_GUN_BURST_SHOT_DELAY_MODIFIED, burst_delay)
//this proc called on each move of vehicle
/obj/item/hardpoint/proc/on_move(turf/old, turf/new_turf, move_dir)
@@ -253,13 +305,12 @@
return data
-// Traces backwards from the gun origin to the vehicle to check for obstacles between the vehicle and the muzzle
-/obj/item/hardpoint/proc/clear_los(atom/A)
-
+/// Traces backwards from the gun origin to the vehicle to check for obstacles between the vehicle and the muzzle.
+/obj/item/hardpoint/proc/clear_los()
if(origins[1] == 0 && origins[2] == 0) //skipping check for modules we don't need this
return TRUE
- var/turf/muzzle_turf = locate(owner.x + origins[1], owner.y + origins[2], owner.z)
+ var/turf/muzzle_turf = get_origin_turf()
var/turf/checking_turf = muzzle_turf
while(!(owner in checking_turf))
@@ -268,24 +319,24 @@
return FALSE
// Ensure that we can pass over all objects in the turf
- for(var/obj/O in checking_turf)
+ for(var/obj/object in checking_turf)
// Since vehicles are multitile the
- if(O == owner)
+ if(object == owner)
continue
// Non-dense objects are irrelevant
- if(!O.density)
+ if(!object.density)
continue
// Make sure we can pass object from all directions
- if(!(O.pass_flags.flags_can_pass_all & PASS_OVER_THROW_ITEM))
- if(!(O.flags_atom & ON_BORDER))
+ if(!HAS_FLAG(object.pass_flags.flags_can_pass_all, PASS_OVER_THROW_ITEM))
+ if(!HAS_FLAG(object.flags_atom, ON_BORDER))
return FALSE
//If we're behind the object, check the behind pass flags
- else if(dir == O.dir && !(O.pass_flags.flags_can_pass_behind & PASS_OVER_THROW_ITEM))
+ else if(dir == object.dir && !HAS_FLAG(object.pass_flags.flags_can_pass_behind, PASS_OVER_THROW_ITEM))
return FALSE
//If we're in front, check front pass flags
- else if(dir == turn(O.dir, 180) && !(O.pass_flags.flags_can_pass_front & PASS_OVER_THROW_ITEM))
+ else if(dir == turn(object.dir, 180) && !HAS_FLAG(object.pass_flags.flags_can_pass_front, PASS_OVER_THROW_ITEM))
return FALSE
// Trace back towards the vehicle
@@ -297,47 +348,6 @@
//------INTERACTION PROCS----------
//-----------------------------
-//If the hardpoint can be activated by current user
-/obj/item/hardpoint/proc/can_activate(mob/user, atom/A)
- if(!owner)
- return
-
- var/seat = owner.get_mob_seat(user)
- if(!seat)
- return
-
- if(seat != allowed_seat)
- to_chat(user, SPAN_WARNING("Only [allowed_seat] can use [name]."))
- return
-
- if(health <= 0)
- to_chat(user, SPAN_WARNING("\The [name] is broken!"))
- return FALSE
-
- if(world.time < next_use)
- if(cooldown >= 20) //filter out guns with high firerate to prevent message spam.
- to_chat(user, SPAN_WARNING("You need to wait [SPAN_HELPFUL((next_use - world.time) / 10)] seconds before [name] can be used again."))
- return FALSE
-
- if(ammo && ammo.current_rounds <= 0)
- to_chat(user, SPAN_WARNING("\The [name] is out of ammo! Magazines: [SPAN_HELPFUL(LAZYLEN(backup_clips))]/[SPAN_HELPFUL(max_clips)]"))
- return FALSE
-
- if(!in_firing_arc(A))
- to_chat(user, SPAN_WARNING("The target is not within your firing arc!"))
- return FALSE
-
- if(!clear_los(A))
- to_chat(user, SPAN_WARNING("You don't have a clear line of sight to the target!"))
- return FALSE
-
- return TRUE
-
-//Called when you want to activate the hardpoint, by default firing a gun
-//This can also be used for some type of temporary buff or toggling mode, up to you
-/obj/item/hardpoint/proc/activate(mob/user, atom/A)
- fire(user, A)
-
/obj/item/hardpoint/proc/deactivate()
return
@@ -490,76 +500,201 @@
user.visible_message(SPAN_NOTICE("[user] stops repairing \the [name]."), SPAN_NOTICE("You stop repairing \the [name]. The integrity of the module is at [SPAN_HELPFUL(round(get_integrity_percent()))]%."))
return
-//determines whether something is in firing arc of a hardpoint
-/obj/item/hardpoint/proc/in_firing_arc(atom/A)
- if(!owner)
- return FALSE
+/// Setter proc for the automatic firing flag.
+/obj/item/hardpoint/proc/set_auto_firing(auto = FALSE)
+ if(auto_firing != auto)
+ auto_firing = auto
+ if(!auto_firing) //end-of-fire, show changed ammo
+ display_ammo()
+
+/// Setter proc for the burst firing flag.
+/obj/item/hardpoint/proc/set_burst_firing(burst = FALSE)
+ if(burst_firing != burst)
+ burst_firing = burst
+ if(!burst_firing) //end-of-fire, show changed ammo
+ display_ammo()
+
+/// Clean all firing references.
+/obj/item/hardpoint/proc/reset_fire()
+ shots_fired = 0
+ set_target(null)
+ set_auto_firing(FALSE) //on abnormal exits automatic fire doesn't call set_auto_firing()
+
+/// Set the target and take care of hard delete.
+/obj/item/hardpoint/proc/set_target(atom/object)
+ if(object == target || object == loc)
+ return
+ if(target)
+ UnregisterSignal(target, COMSIG_PARENT_QDELETING)
+ target = object
+ if(target)
+ RegisterSignal(target, COMSIG_PARENT_QDELETING, PROC_REF(clean_target))
+
+/// Set the target to its turf, so we keep shooting even when it was qdeled.
+/obj/item/hardpoint/proc/clean_target()
+ SIGNAL_HANDLER
+ target = get_turf(target)
+
+/// Print how much ammo is left to chat.
+/obj/item/hardpoint/proc/display_ammo(mob/user)
+ if(!user)
+ user = owner.get_seat_mob(allowed_seat)
+ if(!user)
+ return
- if(!firing_arc)
- return TRUE
+ if(ammo)
+ to_chat(user, SPAN_WARNING("[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)]"))
- var/turf/T = get_turf(A)
- if(!T)
- return FALSE
+/// 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)
+ SEND_SIGNAL(src, COMSIG_GUN_STOP_FIRE)
+ if(auto_firing)
+ reset_fire() //automatic fire doesn't reset itself from COMSIG_GUN_STOP_FIRE
- var/dx = T.x - (owner.x + origins[1]/2)
- var/dy = T.y - (owner.y + origins[2]/2)
-
- var/deg = 0
- switch(dir)
- if(EAST)
- deg = 0
- if(NORTH)
- deg = -90
- if(WEST)
- deg = 180
- if(SOUTH)
- deg = 90
-
- var/nx = dx * cos(deg) - dy * sin(deg)
- var/ny = dx * sin(deg) + dy * cos(deg)
- if(nx == 0)
- return firing_arc >= 90
-
- var/angle = arctan(ny/nx)
- if(nx < 0)
- angle += 180
-
- return abs(angle) <= (firing_arc/2)
-
-//doing last preparation before actually firing gun
-/obj/item/hardpoint/proc/fire(mob/user, atom/A)
- if(!ammo) //Prevents a runtime
+/// Update the target if you dragged your mouse.
+/obj/item/hardpoint/proc/change_target(datum/source, atom/src_object, atom/over_object, turf/src_location, turf/over_location, src_control, over_control, params)
+ set_target(get_turf_on_clickcatcher(over_object, source, params))
+
+/// Check if the gun can fire and add it to bucket autofire system if needed, or just fire the gun if not.
+/obj/item/hardpoint/proc/start_fire(datum/source, atom/object, turf/location, control, params)
+ if(istype(object, /atom/movable/screen))
return
- if(ammo.current_rounds <= 0)
+
+ if(QDELETED(object))
return
- next_use = world.time + cooldown * owner.misc_multipliers["cooldown"]
- if(!prob((accuracy * 100) / owner.misc_multipliers["accuracy"]))
- A = get_step(get_turf(A), pick(GLOB.cardinals))
+ if(!auto_firing && !burst_firing && !COOLDOWN_FINISHED(src, fire_cooldown))
+ if(max(fire_delay, burst_delay + extra_delay) >= 2.0 SECONDS) //filter out guns with high firerate to prevent message spam.
+ to_chat(source, SPAN_WARNING("You need to wait [SPAN_HELPFUL(COOLDOWN_SECONDSLEFT(src, fire_cooldown))] seconds before [name] can be used again."))
+ return
- if(LAZYLEN(activation_sounds))
- playsound(get_turf(src), pick(activation_sounds), 60, 1)
+ set_target(get_turf_on_clickcatcher(object, source, params))
- fire_projectile(user, A)
+ 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)
+ else
+ SEND_SIGNAL(src, COMSIG_GUN_FIRE)
+
+/// 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)
+ SHOULD_NOT_OVERRIDE(TRUE)
+ if(!target)
+ target = src.target
+ if(!user)
+ user = owner.get_seat_mob(allowed_seat)
+ if(!target || !user)
+ return NONE
+
+ return try_fire(target, user, params)
+
+/// Tests if firing should be interrupted, otherwise fires.
+/obj/item/hardpoint/proc/try_fire(atom/target, mob/living/user, params)
+ if(health <= 0)
+ to_chat(user, SPAN_WARNING("\The [name] is broken!"))
+ return NONE
- to_chat(user, SPAN_WARNING("[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)]"))
+ if(ammo && ammo.current_rounds <= 0)
+ click_empty(user)
+ return NONE
-//finally firing the gun
-/obj/item/hardpoint/proc/fire_projectile(mob/user, atom/A)
- set waitfor = 0
+ if(!in_firing_arc(target))
+ to_chat(user, SPAN_WARNING("The target is not within your firing arc!"))
+ return NONE
- var/turf/origin_turf = get_turf(src)
- origin_turf = locate(origin_turf.x + origins[1], origin_turf.y + origins[2], origin_turf.z)
+ if(!clear_los())
+ to_chat(user, SPAN_WARNING("The muzzle is obstructed!"))
+ return NONE
- var/obj/projectile/P = generate_bullet(user, origin_turf)
- SEND_SIGNAL(P, COMSIG_BULLET_USER_EFFECTS, user)
- P.fire_at(A, user, src, P.ammo.max_range, P.ammo.shell_speed)
+ return handle_fire(target, user, params)
- if(use_muzzle_flash)
- muzzle_flash(Get_Angle(origin_turf, A))
+/// Actually fires the gun, sets up the projectile and fires it.
+/obj/item/hardpoint/proc/handle_fire(atom/target, mob/living/user, params)
+ var/turf/origin_turf = get_origin_turf()
+ var/obj/projectile/projectile_to_fire = generate_bullet(user, origin_turf)
ammo.current_rounds--
+ SEND_SIGNAL(projectile_to_fire, COMSIG_BULLET_USER_EFFECTS, user)
+
+ // 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
+ if(!HAS_FLAG(ammo_flags, AMMO_HITS_TARGET_TURF) && !HAS_FLAG(ammo_flags, AMMO_EXPLOSIVE)) //AMMO_EXPLOSIVE is also a turf-targeted projectile
+ projectile_to_fire.scatter = scatter
+ target = simulate_scatter(projectile_to_fire, target, origin_turf, get_turf(target), user)
+
+ INVOKE_ASYNC(projectile_to_fire, TYPE_PROC_REF(/obj/projectile, fire_at), target, user, src, projectile_to_fire.ammo.max_range, projectile_to_fire.ammo.shell_speed)
+ projectile_to_fire = null
+
+ shots_fired++
+ play_firing_sounds()
+ if(use_muzzle_flash)
+ muzzle_flash(Get_Angle(origin_turf, target))
+
+ set_fire_cooldown(gun_firemode)
+
+ return AUTOFIRE_CONTINUE
+
+/// Start cooldown to respect delay of firemode.
+/obj/item/hardpoint/proc/set_fire_cooldown(firemode)
+ var/cooldown_time = 0
+ switch(firemode)
+ if(GUN_FIREMODE_SEMIAUTO)
+ cooldown_time = fire_delay
+ if(GUN_FIREMODE_BURSTFIRE)
+ cooldown_time = burst_delay + extra_delay
+ if(GUN_FIREMODE_AUTOMATIC)
+ cooldown_time = fire_delay
+ COOLDOWN_START(src, fire_cooldown, cooldown_time)
+
+/// Adjust target based on random scatter angle.
+/obj/item/hardpoint/proc/simulate_scatter(obj/projectile/projectile_to_fire, atom/target, turf/curloc, turf/targloc)
+ var/fire_angle = Get_Angle(curloc, targloc)
+ var/total_scatter_angle = projectile_to_fire.scatter
+
+ //Not if the gun doesn't scatter at all, or negative scatter.
+ if(total_scatter_angle > 0)
+ fire_angle += rand(-total_scatter_angle, total_scatter_angle)
+ target = get_angle_target_turf(curloc, fire_angle, 30)
+
+ return target
+
+/// Get turf at hardpoint origin offset, used as the muzzle.
+/obj/item/hardpoint/proc/get_origin_turf()
+ return get_offset_target_turf(get_turf(src), origins[1], origins[2])
+
+/// Plays 'click' noise and announced to chat. Usually called when weapon empty.
+/obj/item/hardpoint/proc/click_empty(mob/user)
+ playsound(src, 'sound/weapons/gun_empty.ogg', 25, 1, 5)
+ if(user)
+ to_chat(user, SPAN_WARNING("*click*"))
+
+/// Selects and plays a firing sound from the list.
+/obj/item/hardpoint/proc/play_firing_sounds()
+ if(LAZYLEN(activation_sounds))
+ playsound(get_turf(src), pick(activation_sounds), 60, 1)
+
+/// 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))
+ return TRUE
+
+ var/turf/muzzle_turf = get_origin_turf()
+ var/turf/target_turf = get_turf(target)
+
+ //same tile angle returns EAST, returning FALSE to ensure consistency
+ if(muzzle_turf == target_turf)
+ return FALSE
+
+ var/angle_diff = SIMPLIFY_DEGREES(dir2angle(dir) - get_angle(muzzle_turf, target_turf))
+ if(angle_diff < -180)
+ angle_diff += 360
+ else if(angle_diff > 180)
+ angle_diff -= 360
+
+ return abs(angle_diff) <= (firing_arc * 0.5)
//-----------------------------
//------ICON PROCS----------
diff --git a/code/modules/vehicles/hardpoints/holder/holder.dm b/code/modules/vehicles/hardpoints/holder/holder.dm
index b14e078a3997..fc8e849d105c 100644
--- a/code/modules/vehicles/hardpoints/holder/holder.dm
+++ b/code/modules/vehicles/hardpoints/holder/holder.dm
@@ -43,10 +43,21 @@
for(var/obj/item/hardpoint/H in hardpoints)
H.take_damage(damage)
-/obj/item/hardpoint/holder/on_install(obj/vehicle/multitile/V)
- for(var/obj/item/hardpoint/HP in hardpoints)
- HP.owner = V
- return
+/obj/item/hardpoint/holder/on_install(obj/vehicle/multitile/vehicle)
+ ..()
+ if(!vehicle) //in loose holder
+ return
+ for(var/obj/item/hardpoint/hardpoint in hardpoints)
+ hardpoint.owner = vehicle
+ hardpoint.on_install(vehicle)
+
+/obj/item/hardpoint/holder/on_uninstall(obj/vehicle/multitile/vehicle)
+ if(!vehicle) //in loose holder
+ return
+ for(var/obj/item/hardpoint/hardpoint in hardpoints)
+ hardpoint.on_uninstall(vehicle)
+ hardpoint.owner = null
+ ..()
/obj/item/hardpoint/holder/proc/can_install(obj/item/hardpoint/H)
// Can only have 1 hardpoint of each slot type
@@ -121,16 +132,17 @@
H.forceMove(src)
LAZYADD(hardpoints, H)
+ H.on_install(owner)
H.rotate(turning_angle(H.dir, dir))
/obj/item/hardpoint/holder/proc/remove_hardpoint(obj/item/hardpoint/H, turf/uninstall_to)
if(!hardpoints)
return
- hardpoints -= H
H.forceMove(uninstall_to ? uninstall_to : get_turf(src))
+ H.on_uninstall(owner)
H.reset_rotation()
-
+ hardpoints -= H
H.owner = null
if(H.health <= 0)
diff --git a/code/modules/vehicles/hardpoints/holder/tank_turret.dm b/code/modules/vehicles/hardpoints/holder/tank_turret.dm
index 27ab6c95404c..896628e609bb 100644
--- a/code/modules/vehicles/hardpoints/holder/tank_turret.dm
+++ b/code/modules/vehicles/hardpoints/holder/tank_turret.dm
@@ -13,8 +13,6 @@
density = TRUE //come on, it's huge
activatable = TRUE
- cooldown = 150
- accuracy = 0.8
ammo = new /obj/item/ammo_magazine/hardpoint/turret_smoke
max_clips = 2
@@ -60,6 +58,15 @@
// Used during the windup
var/rotating = FALSE
+ 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
+
/obj/item/hardpoint/holder/tank_turret/update_icon()
var/broken = (health <= 0)
icon_state = "tank_turret_[broken]"
@@ -182,12 +189,7 @@
user.client.pixel_x = -1 * AM.view_tile_offset * 32
user.client.pixel_y = 0
-/obj/item/hardpoint/holder/tank_turret/fire(mob/user, atom/A)
- if(ammo.current_rounds <= 0)
- return
-
- next_use = world.time + cooldown
-
+/obj/item/hardpoint/holder/tank_turret/try_fire(atom/target, mob/living/user, params)
var/turf/L
var/turf/R
switch(owner.dir)
@@ -204,26 +206,14 @@
L = locate(owner.x - 4, owner.y + 2, owner.z)
R = locate(owner.x - 4, owner.y - 2, owner.z)
- if(LAZYLEN(activation_sounds))
- playsound(get_turf(src), pick(activation_sounds), 60, 1)
- fire_projectile(user, L)
+ if(shots_fired)
+ target = R
+ else
+ target = L
- sleep(10)
+ return ..()
- if(LAZYLEN(activation_sounds))
- playsound(get_turf(src), pick(activation_sounds), 60, 1)
- fire_projectile(user, R)
-
- to_chat(user, SPAN_WARNING("Smoke Screen uses left: [SPAN_HELPFUL(ammo ? ammo.current_rounds / 2 : 0)]/[SPAN_HELPFUL(ammo ? ammo.max_rounds / 2 : 0)] | Mags: [SPAN_HELPFUL(LAZYLEN(backup_clips))]/[SPAN_HELPFUL(max_clips)]"))
-
-/obj/item/hardpoint/holder/tank_turret/fire_projectile(mob/user, atom/A)
- set waitfor = 0
-
- var/turf/origin_turf = get_turf(src)
- origin_turf = locate(origin_turf.x + origins[1], origin_turf.y + origins[2], origin_turf.z)
+/obj/item/hardpoint/holder/tank_turret/get_origin_turf()
+ var/origin_turf = ..()
origin_turf = get_step(get_step(origin_turf, owner.dir), owner.dir) //this should get us tile in front of tank to prevent grenade being stuck under us.
-
- var/obj/projectile/P = generate_bullet(user, origin_turf)
- SEND_SIGNAL(P, COMSIG_BULLET_USER_EFFECTS, owner.seats[VEHICLE_GUNNER])
- P.fire_at(A, owner.seats[VEHICLE_GUNNER], src, get_dist(origin_turf, A) + 1, P.ammo.shell_speed)
- ammo.current_rounds--
+ return origin_turf
diff --git a/code/modules/vehicles/hardpoints/primary/autocannon.dm b/code/modules/vehicles/hardpoints/primary/autocannon.dm
index df9224011b32..b6dc2cedc674 100644
--- a/code/modules/vehicles/hardpoints/primary/autocannon.dm
+++ b/code/modules/vehicles/hardpoints/primary/autocannon.dm
@@ -8,8 +8,6 @@
activation_sounds = list('sound/weapons/vehicles/autocannon_fire.ogg')
health = 500
- cooldown = 7
- accuracy = 0.98
firing_arc = 60
origins = list(0, -3)
@@ -23,3 +21,10 @@
"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
diff --git a/code/modules/vehicles/hardpoints/primary/dual_cannon.dm b/code/modules/vehicles/hardpoints/primary/dual_cannon.dm
index ad57e20e8456..4033a4bffb2a 100644
--- a/code/modules/vehicles/hardpoints/primary/dual_cannon.dm
+++ b/code/modules/vehicles/hardpoints/primary/dual_cannon.dm
@@ -12,10 +12,7 @@
damage_multiplier = 0.2
health = 500
- cooldown = 7
- accuracy = 0.98
firing_arc = 60
- var/burst_amount = 2
origins = list(0, -2)
@@ -33,27 +30,15 @@
"8" = list(14, 9)
)
+ scatter = 1
+ gun_firemode = GUN_FIREMODE_AUTOMATIC
+ gun_firemode_list = list(
+ GUN_FIREMODE_AUTOMATIC,
+ )
+ fire_delay = 0.3 SECONDS
+
/obj/item/hardpoint/primary/dualcannon/set_bullet_traits()
..()
LAZYADD(traits_to_give, list(
BULLET_TRAIT_ENTRY(/datum/element/bullet_trait_iff)
))
-
-/obj/item/hardpoint/primary/dualcannon/fire(mob/user, atom/A)
- if(ammo.current_rounds <= 0)
- return
-
- next_use = world.time + cooldown * owner.misc_multipliers["cooldown"]
-
- for(var/bullets_fired = 1, bullets_fired <= burst_amount, bullets_fired++)
- var/atom/T = A
- if(!prob((accuracy * 100) / owner.misc_multipliers["accuracy"]))
- T = get_step(get_turf(A), pick(GLOB.cardinals))
- if(LAZYLEN(activation_sounds))
- playsound(get_turf(src), pick(activation_sounds), 60, 1)
- fire_projectile(user, T)
- if(ammo.current_rounds <= 0)
- break
- if(bullets_fired < burst_amount) //we need to sleep only if there are more bullets to shoot in the burst
- sleep(3)
- to_chat(user, SPAN_WARNING("[src] 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)]"))
diff --git a/code/modules/vehicles/hardpoints/primary/flamer.dm b/code/modules/vehicles/hardpoints/primary/flamer.dm
index 929842df2307..13beee9dd2c2 100644
--- a/code/modules/vehicles/hardpoints/primary/flamer.dm
+++ b/code/modules/vehicles/hardpoints/primary/flamer.dm
@@ -8,8 +8,6 @@
activation_sounds = list('sound/weapons/vehicles/flamethrower.ogg')
health = 400
- cooldown = 20
- accuracy = 0.75
firing_arc = 90
origins = list(0, -3)
@@ -26,36 +24,19 @@
use_muzzle_flash = FALSE
+ scatter = 5
+ fire_delay = 2.0 SECONDS
+
/obj/item/hardpoint/primary/flamer/set_bullet_traits()
..()
LAZYADD(traits_to_give, list(
BULLET_TRAIT_ENTRY(/datum/element/bullet_trait_iff)
))
-/obj/item/hardpoint/primary/flamer/can_activate(mob/user, atom/A)
- if(!..())
- return FALSE
-
- var/turf/origin_turf = get_turf(src)
- origin_turf = locate(origin_turf.x + origins[1], origin_turf.y + origins[2], origin_turf.z)
- if(origin_turf == get_turf(A))
- return FALSE
-
- return TRUE
-
-/obj/item/hardpoint/primary/flamer/fire_projectile(mob/user, atom/A)
- set waitfor = 0
-
- var/turf/origin_turf = get_turf(src)
- origin_turf = locate(origin_turf.x + origins[1], origin_turf.y + origins[2], origin_turf.z)
-
- var/range = get_dist(origin_turf, A) + 1
-
- var/obj/projectile/P = generate_bullet(user, origin_turf)
- SEND_SIGNAL(P, COMSIG_BULLET_USER_EFFECTS, owner.seats[VEHICLE_GUNNER])
- P.fire_at(A, owner.seats[VEHICLE_GUNNER], src, range < P.ammo.max_range ? range : P.ammo.max_range, P.ammo.shell_speed)
-
- if(use_muzzle_flash)
- muzzle_flash(Get_Angle(owner, A))
+/obj/item/hardpoint/primary/flamer/try_fire(target, user, params)
+ var/turf/origin_turf = get_origin_turf()
+ if(origin_turf == get_turf(target))
+ to_chat(user, SPAN_WARNING("The target is too close."))
+ return NONE
- ammo.current_rounds--
+ return ..()
diff --git a/code/modules/vehicles/hardpoints/primary/ltb.dm b/code/modules/vehicles/hardpoints/primary/ltb.dm
index 7c663dc27fbc..19b5c7e7b9b4 100644
--- a/code/modules/vehicles/hardpoints/primary/ltb.dm
+++ b/code/modules/vehicles/hardpoints/primary/ltb.dm
@@ -8,8 +8,6 @@
activation_sounds = list('sound/weapons/vehicles/cannon_fire1.ogg', 'sound/weapons/vehicles/cannon_fire2.ogg')
health = 500
- cooldown = 200
- accuracy = 0.97
firing_arc = 60
origins = list(0, -3)
@@ -30,3 +28,6 @@
"4" = list(89, -4),
"8" = list(-89, -4)
)
+
+ scatter = 2
+ fire_delay = 20.0 SECONDS
diff --git a/code/modules/vehicles/hardpoints/primary/minigun.dm b/code/modules/vehicles/hardpoints/primary/minigun.dm
index a6e44d2dbf2c..3acf37eec268 100644
--- a/code/modules/vehicles/hardpoints/primary/minigun.dm
+++ b/code/modules/vehicles/hardpoints/primary/minigun.dm
@@ -7,8 +7,6 @@
disp_icon_state = "ltaaap_minigun"
health = 350
- cooldown = 8
- accuracy = 0.6
firing_arc = 90
origins = list(0, -3)
@@ -30,46 +28,58 @@
"8" = list(-77, 0)
)
- //changed minigun mechanic so instead of having lowered cooldown with each shot it now has increased burst size.
- //While it's still spammy, user doesn't have to click as fast as possible anymore and has margin of 2 seconds before minigun will start slowing down
-
- var/chained_shots = 1 //how many quick succession shots we've fired, 1 by default
- var/last_shot_time = 0 //when was last shot fired, after 3 seconds we stop barrel
- var/list/chain_bursts = list(1, 1, 2, 2, 3, 3, 3, 4, 4, 4) //how many shots per click we do
+ scatter = 7
+ gun_firemode = GUN_FIREMODE_AUTOMATIC
+ gun_firemode_list = list(
+ GUN_FIREMODE_AUTOMATIC,
+ )
+ fire_delay = 0.8 SECONDS //base fire rate, modified by stage_delay_mult
+ activation_sounds = list('sound/weapons/gun_minigun.ogg')
+ /// Active firing time to reach max spin_stage.
+ var/spinup_time = 8 SECONDS
+ /// Grace period before losing spin_stage.
+ var/spindown_grace_time = 2 SECONDS
+ COOLDOWN_DECLARE(spindown_grace_cooldown)
+ /// Cooldown time to reach min spin_stage.
+ 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)
+ /// Fire delay multiplier for current spin_stage.
+ var/stage_delay_mult = 1
+ /// When it was last fired, related to world.time.
+ var/last_fired = 0
-/obj/item/hardpoint/primary/minigun/fire(mob/user, atom/A)
+/obj/item/hardpoint/primary/minigun/set_fire_delay(value)
+ fire_delay = value
+ SEND_SIGNAL(src, COMSIG_GUN_AUTOFIREDELAY_MODIFIED, fire_delay * stage_delay_mult)
- var/S = 'sound/weapons/vehicles/minigun_stop.ogg'
- //check how much time since last shot. 2 seconds are grace period before minigun starts to lose rotation momentum
- var/t = world.time - last_shot_time - 2 SECONDS
- t = round(t / 10)
- if(t > 0)
- chained_shots = max(chained_shots - t * 3, 1) //we lose 3 chained_shots per second
- else
- if(chained_shots < 11)
- chained_shots++
- S = 'sound/weapons/vehicles/minigun_loop.ogg'
+/obj/item/hardpoint/primary/minigun/set_fire_cooldown()
+ calculate_stage_delay_mult() //needs to check grace_cooldown before refreshed
+ last_fired = world.time
+ COOLDOWN_START(src, spindown_grace_cooldown, spindown_grace_time)
+ COOLDOWN_START(src, fire_cooldown, fire_delay * stage_delay_mult)
- if(chained_shots == 1)
- playsound(get_turf(src), 'sound/weapons/vehicles/minigun_start.ogg', 40, 1)
+/obj/item/hardpoint/primary/minigun/proc/calculate_stage_delay_mult()
+ var/stage_rate_len = stage_rate.len
+ var/delta_time = world.time - last_fired
- next_use = world.time + cooldown * owner.misc_multipliers["cooldown"]
-
- //how many rounds we will shoot in this burst
- if(chained_shots > LAZYLEN(chain_bursts)) //5 shots at maximum rotation
- t = 5
+ var/old_spin_stage = spin_stage
+ if(auto_firing || burst_firing) //spinup if continuing fire
+ var/delta_stage = delta_time * (stage_rate_len - 1)
+ spin_stage += delta_stage / spinup_time
+ else if(COOLDOWN_FINISHED(src, spindown_grace_cooldown)) //spindown if initiating fire after grace
+ var/delta_stage = (delta_time - spindown_grace_time) * (stage_rate_len - 1)
+ spin_stage -= delta_stage / spindown_time
else
- t = LAZYACCESS(chain_bursts, chained_shots)
- for(var/i = 1; i <= t; i++)
- var/atom/T = A
- if(!prob((accuracy * 100) / owner.misc_multipliers["accuracy"]))
- T = get_step(get_turf(T), pick(GLOB.cardinals))
- fire_projectile(user, T)
- if(ammo.current_rounds <= 0)
- break
- sleep(2)
- to_chat(user, SPAN_WARNING("[src] 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
+ spin_stage = Clamp(spin_stage, 1, stage_rate_len)
+
+ var/old_stage_rate = stage_rate[Floor(old_spin_stage)]
+ var/new_stage_rate = stage_rate[Floor(spin_stage)]
- playsound(get_turf(src), S, 40, 1)
- last_shot_time = world.time
+ if(old_stage_rate != new_stage_rate)
+ 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/cupola.dm b/code/modules/vehicles/hardpoints/secondary/cupola.dm
index 3c329e135855..f259d6ea2623 100644
--- a/code/modules/vehicles/hardpoints/secondary/cupola.dm
+++ b/code/modules/vehicles/hardpoints/secondary/cupola.dm
@@ -8,10 +8,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 = 350
- cooldown = 15
- accuracy = 0.9
firing_arc = 120
- var/burst_amount = 3
origins = list(0, -2)
@@ -25,27 +22,17 @@
"8" = list(-5, 7)
)
+ scatter = 3
+ gun_firemode = GUN_FIREMODE_BURSTFIRE
+ gun_firemode_list = list(
+ GUN_FIREMODE_BURSTFIRE,
+ )
+ burst_amount = 3
+ burst_delay = 0.3 SECONDS
+ extra_delay = 0.6 SECONDS
+
/obj/item/hardpoint/secondary/m56cupola/set_bullet_traits()
..()
LAZYADD(traits_to_give, list(
BULLET_TRAIT_ENTRY(/datum/element/bullet_trait_iff)
))
-
-/obj/item/hardpoint/secondary/m56cupola/fire(mob/user, atom/A)
- if(ammo.current_rounds <= 0)
- return
-
- next_use = world.time + cooldown * owner.misc_multipliers["cooldown"]
-
- for(var/bullets_fired = 1, bullets_fired <= burst_amount, bullets_fired++)
- var/atom/T = A
- if(!prob((accuracy * 100) / owner.misc_multipliers["accuracy"]))
- T = get_step(get_turf(A), pick(GLOB.cardinals))
- if(LAZYLEN(activation_sounds))
- playsound(get_turf(src), pick(activation_sounds), 60, 1)
- fire_projectile(user, T)
- if(ammo.current_rounds <= 0)
- break
- if(bullets_fired < burst_amount) //we need to sleep only if there are more bullets to shoot in the burst
- sleep(3)
- to_chat(user, SPAN_WARNING("[src] 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)]"))
diff --git a/code/modules/vehicles/hardpoints/secondary/flamer.dm b/code/modules/vehicles/hardpoints/secondary/flamer.dm
index 10f7453d8c95..5557cfb24e17 100644
--- a/code/modules/vehicles/hardpoints/secondary/flamer.dm
+++ b/code/modules/vehicles/hardpoints/secondary/flamer.dm
@@ -8,8 +8,6 @@
activation_sounds = list('sound/weapons/vehicles/flamethrower.ogg')
health = 300
- cooldown = 30
- accuracy = 0.68
firing_arc = 120
origins = list(0, -2)
@@ -28,31 +26,20 @@
"8" = list(-3, 18)
)
-/obj/item/hardpoint/secondary/small_flamer/fire_projectile(mob/user, atom/A)
- set waitfor = 0
-
- var/turf/origin_turf = get_turf(src)
- origin_turf = locate(origin_turf.x + origins[1], origin_turf.y + origins[2], origin_turf.z)
- var/list/turf/turfs = getline2(origin_turf, A)
- var/distance = 0
- var/turf/prev_T
-
- for(var/turf/T in turfs)
- if(T == loc)
- prev_T = T
- continue
- if(!ammo.current_rounds) break
- if(distance >= max_range) break
- if(prev_T && LinkBlocked(prev_T, T))
- break
- ammo.current_rounds--
- flame_turf(T, user)
- distance++
- prev_T = T
- sleep(1)
-
-/obj/item/hardpoint/secondary/small_flamer/proc/flame_turf(turf/T, mob/user)
- if(!istype(T)) return
-
- if(!locate(/obj/flamer_fire) in T) // No stacking flames!
- new/obj/flamer_fire(T, create_cause_data(initial(name), user))
+ scatter = 6
+ fire_delay = 3.0 SECONDS
+
+/obj/item/hardpoint/secondary/small_flamer/handle_fire(atom/target, mob/living/user, params)
+ var/turf/origin_turf = get_origin_turf()
+
+ 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
+
+ new /obj/flamer_fire(origin_turf, create_cause_data(initial(name), user), null, fire_amount, null, FLAMESHAPE_LINE, target, CALLBACK(src, PROC_REF(display_ammo), user))
+
+ play_firing_sounds()
+
+ COOLDOWN_START(src, fire_cooldown, fire_delay)
+
+ return AUTOFIRE_CONTINUE
diff --git a/code/modules/vehicles/hardpoints/secondary/frontal_cannon.dm b/code/modules/vehicles/hardpoints/secondary/frontal_cannon.dm
index a4d7370935fe..536b5742cfcd 100644
--- a/code/modules/vehicles/hardpoints/secondary/frontal_cannon.dm
+++ b/code/modules/vehicles/hardpoints/secondary/frontal_cannon.dm
@@ -11,10 +11,7 @@
damage_multiplier = 0.11
health = 350
- cooldown = 16
- accuracy = 0.8
firing_arc = 120
- var/burst_amount = 4
origins = list(0, -2)
@@ -32,27 +29,15 @@
"8" = list(-62, -26)
)
+ scatter = 4
+ gun_firemode = GUN_FIREMODE_AUTOMATIC
+ gun_firemode_list = list(
+ GUN_FIREMODE_AUTOMATIC,
+ )
+ fire_delay = 0.3 SECONDS
+
/obj/item/hardpoint/secondary/frontalcannon/set_bullet_traits()
..()
LAZYADD(traits_to_give, list(
BULLET_TRAIT_ENTRY(/datum/element/bullet_trait_iff)
))
-
-/obj/item/hardpoint/secondary/frontalcannon/fire(mob/user, atom/A)
- if(ammo.current_rounds <= 0)
- return
-
- next_use = world.time + cooldown * owner.misc_multipliers["cooldown"]
-
- for(var/bullets_fired = 1, bullets_fired <= burst_amount, bullets_fired++)
- var/atom/T = A
- if(!prob((accuracy * 100) / owner.misc_multipliers["accuracy"]))
- T = get_step(get_turf(A), pick(GLOB.cardinals))
- if(LAZYLEN(activation_sounds))
- playsound(get_turf(src), pick(activation_sounds), 60, 1)
- fire_projectile(user, T)
- if(ammo.current_rounds <= 0)
- break
- if(bullets_fired < burst_amount) //we need to sleep only if there are more bullets to shoot in the burst
- sleep(3)
- to_chat(user, SPAN_WARNING("[src] 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)]"))
diff --git a/code/modules/vehicles/hardpoints/secondary/grenade_launcher.dm b/code/modules/vehicles/hardpoints/secondary/grenade_launcher.dm
index 8151a1ee50c1..efd151e93cb3 100644
--- a/code/modules/vehicles/hardpoints/secondary/grenade_launcher.dm
+++ b/code/modules/vehicles/hardpoints/secondary/grenade_launcher.dm
@@ -8,8 +8,6 @@
activation_sounds = list('sound/weapons/gun_m92_attachable.ogg')
health = 500
- cooldown = 30
- accuracy = 0.4
firing_arc = 90
var/max_range = 7
@@ -27,40 +25,19 @@
"8" = list(-6, 17)
)
+ scatter = 10
+ fire_delay = 3.0 SECONDS
+
/obj/item/hardpoint/secondary/grenade_launcher/set_bullet_traits()
..()
LAZYADD(traits_to_give, list(
BULLET_TRAIT_ENTRY(/datum/element/bullet_trait_iff)
))
-/obj/item/hardpoint/secondary/grenade_launcher/can_activate(mob/user, atom/A)
- if(!..())
- return FALSE
-
- var/turf/origin_turf = get_turf(src)
- origin_turf = locate(origin_turf.x + origins[1], origin_turf.y + origins[2], origin_turf.z)
- if(get_dist(origin_turf, A) < 1)
- to_chat(usr, SPAN_WARNING("The target is too close."))
- return FALSE
-
- return TRUE
-
-/obj/item/hardpoint/secondary/grenade_launcher/fire_projectile(mob/user, atom/A)
- set waitfor = 0
-
- var/turf/origin_turf = get_turf(src)
- origin_turf = locate(origin_turf.x + origins[1], origin_turf.y + origins[2], origin_turf.z)
-
- //getting distance between supposed target and tank center.
- var/range = get_dist(origin_turf, A) + 1 //otherwise nade falls one tile shorter
- if(range > max_range)
- range = max_range
-
- var/obj/projectile/P = generate_bullet(user, origin_turf)
- SEND_SIGNAL(P, COMSIG_BULLET_USER_EFFECTS, owner.seats[VEHICLE_GUNNER])
- P.fire_at(A, owner.seats[VEHICLE_GUNNER], src, P.ammo.max_range, P.ammo.shell_speed)
-
- if(use_muzzle_flash)
- muzzle_flash(Get_Angle(owner, A))
+/obj/item/hardpoint/secondary/grenade_launcher/try_fire(mob/user, atom/A)
+ var/turf/origin_turf = get_origin_turf()
+ if(origin_turf == get_turf(A))
+ to_chat(user, SPAN_WARNING("The target is too close."))
+ return NONE
- ammo.current_rounds--
+ return ..()
diff --git a/code/modules/vehicles/hardpoints/secondary/tow.dm b/code/modules/vehicles/hardpoints/secondary/tow.dm
index 4bdbc6f417fb..7c58f7970c7b 100644
--- a/code/modules/vehicles/hardpoints/secondary/tow.dm
+++ b/code/modules/vehicles/hardpoints/secondary/tow.dm
@@ -7,8 +7,6 @@
disp_icon_state = "towlauncher"
health = 500
- cooldown = 150
- accuracy = 0.8
firing_arc = 60
origins = list(0, -2)
@@ -29,3 +27,7 @@
"4" = list(5, -8),
"8" = list(-5, 10)
)
+
+ scatter = 4
+ fire_delay = 15.0 SECONDS
+
diff --git a/code/modules/vehicles/hardpoints/special/firing_port_weapon.dm b/code/modules/vehicles/hardpoints/special/firing_port_weapon.dm
index b6f3daed9f4e..780c195f00be 100644
--- a/code/modules/vehicles/hardpoints/special/firing_port_weapon.dm
+++ b/code/modules/vehicles/hardpoints/special/firing_port_weapon.dm
@@ -10,10 +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
- cooldown = 10
- accuracy = 0.9
firing_arc = 120
- var/burst_amount = 3
//FPWs reload automatically
var/reloading = FALSE
var/reload_time = 10 SECONDS
@@ -30,6 +27,13 @@
underlayer_north_muzzleflash = TRUE
+ scatter = 3
+ gun_firemode = GUN_FIREMODE_AUTOMATIC
+ gun_firemode_list = list(
+ GUN_FIREMODE_AUTOMATIC,
+ )
+ fire_delay = 0.3 SECONDS
+
/obj/item/hardpoint/special/firing_port_weapon/set_bullet_traits()
..()
LAZYADD(traits_to_give, list(
@@ -49,46 +53,6 @@
return data
-
-/obj/item/hardpoint/special/firing_port_weapon/can_activate(mob/user, atom/A)
- if(!owner)
- return FALSE
-
- var/seat = owner.get_mob_seat(user)
- if(!seat)
- return FALSE
-
- if(seat != allowed_seat)
- to_chat(user, SPAN_WARNING("Only [allowed_seat] can use [name]."))
- return FALSE
-
- //FPW stop working at 50% hull
- if(owner.health < initial(owner.health) * 0.5)
- to_chat(user, SPAN_WARNING("\The [owner]'s hull is too damaged!"))
- return FALSE
-
- if(world.time < next_use)
- if(cooldown >= 20) //filter out guns with high firerate to prevent message spam.
- to_chat(user, SPAN_WARNING("You need to wait [SPAN_HELPFUL((next_use - world.time) / 10)] seconds before [name] can be used again."))
- return FALSE
-
- if(reloading)
- to_chat(user, SPAN_NOTICE("\The [name] is reloading. Wait [SPAN_HELPFUL("[((reload_time_started + reload_time - world.time) / 10)]")] seconds."))
- return FALSE
-
- if(ammo && ammo.current_rounds <= 0)
- if(reloading)
- to_chat(user, SPAN_WARNING("\The [name] is out of ammo! You have to wait [(reload_time_started + reload_time - world.time) / 10] seconds before it reloads!"))
- else
- start_auto_reload(user)
- return FALSE
-
- if(!in_firing_arc(A))
- to_chat(user, SPAN_WARNING("The target is not within your firing arc!"))
- return FALSE
-
- return TRUE
-
/obj/item/hardpoint/special/firing_port_weapon/reload(mob/user)
if(!ammo)
ammo = new /obj/item/ammo_magazine/hardpoint/firing_port_weapon
@@ -116,27 +80,32 @@
to_chat(user, SPAN_NOTICE("\The [name] reloads automatically."))
return FALSE
+/obj/item/hardpoint/special/firing_port_weapon/try_fire(atom/target, mob/living/user, params)
+ if(!owner)
+ return NONE
+
+ //FPW stop working at 50% hull
+ if(owner.health < initial(owner.health) * 0.5)
+ to_chat(user, SPAN_WARNING("\The [owner]'s hull is too damaged!"))
+ return NONE
-/obj/item/hardpoint/special/firing_port_weapon/fire(mob/user, atom/A)
if(user.get_active_hand())
to_chat(user, SPAN_WARNING("You need a free hand to use \the [name]."))
- return
+ return NONE
- if(ammo.current_rounds <= 0)
- start_auto_reload(user)
- return
+ if(reloading)
+ to_chat(user, SPAN_NOTICE("\The [name] is reloading. Wait [SPAN_HELPFUL("[((reload_time_started + reload_time - world.time) / 10)]")] seconds."))
+ return NONE
+
+ if(ammo && ammo.current_rounds <= 0)
+ if(reloading)
+ to_chat(user, SPAN_WARNING("\The [name] is out of ammo! You have to wait [(reload_time_started + reload_time - world.time) / 10] seconds before it reloads!"))
+ else
+ start_auto_reload(user)
+ return NONE
+
+ if(!in_firing_arc(target))
+ to_chat(user, SPAN_WARNING("The target is not within your firing arc!"))
+ return NONE
- next_use = world.time + cooldown * owner.misc_multipliers["cooldown"]
-
- for(var/bullets_fired = 1, bullets_fired <= burst_amount, bullets_fired++)
- var/atom/T = A
- if(!prob((accuracy * 100) / owner.misc_multipliers["accuracy"]))
- T = get_step(get_turf(A), pick(GLOB.cardinals))
- if(LAZYLEN(activation_sounds))
- playsound(get_turf(src), pick(activation_sounds), 60, 1)
- fire_projectile(user, T)
- if(ammo.current_rounds <= 0)
- break
- if(bullets_fired < burst_amount) //we need to sleep only if there are more bullets to shoot in the burst
- sleep(3)
- to_chat(user, SPAN_WARNING("[src] Ammo: [SPAN_HELPFUL(ammo ? ammo.current_rounds : 0)]/[SPAN_HELPFUL(ammo ? ammo.max_rounds : 0)]"))
+ return handle_fire(target, user, params)
diff --git a/code/modules/vehicles/hardpoints/support/artillery.dm b/code/modules/vehicles/hardpoints/support/artillery.dm
index aacb83fcf383..dfcdcaf73f74 100644
--- a/code/modules/vehicles/hardpoints/support/artillery.dm
+++ b/code/modules/vehicles/hardpoints/support/artillery.dm
@@ -14,7 +14,7 @@
var/view_buff = 10 //This way you can VV for more or less fun
var/view_tile_offset = 7
-/obj/item/hardpoint/support/artillery_module/activate(mob/user, atom/A)
+/obj/item/hardpoint/support/artillery_module/handle_fire(atom/target, mob/living/user, params)
if(!user.client)
return
@@ -62,8 +62,9 @@
user.client.pixel_y = 0
is_active = FALSE
-/obj/item/hardpoint/support/artillery_module/can_activate()
+/obj/item/hardpoint/support/artillery_module/try_fire(target, user, params)
if(health <= 0)
to_chat(usr, SPAN_WARNING("\The [src] is broken!"))
- return FALSE
- return TRUE
+ return NONE
+
+ return handle_fire(target, user, params)
diff --git a/code/modules/vehicles/hardpoints/support/flare.dm b/code/modules/vehicles/hardpoints/support/flare.dm
index 00dcd3ac1886..432c9636dadd 100644
--- a/code/modules/vehicles/hardpoints/support/flare.dm
+++ b/code/modules/vehicles/hardpoints/support/flare.dm
@@ -13,8 +13,6 @@
activatable = TRUE
health = 500
- cooldown = 30
- accuracy = 0.7
firing_arc = 120
origins = list(0, -2)
@@ -33,6 +31,9 @@
"8" = list(14, -6)
)
+ scatter = 6
+ fire_delay = 3.0 SECONDS
+
/obj/item/hardpoint/support/flare_launcher/set_bullet_traits()
..()
LAZYADD(traits_to_give, list(
diff --git a/code/modules/vehicles/interior/interactable/seats.dm b/code/modules/vehicles/interior/interactable/seats.dm
index 1e5df9fd1d81..7c68e4ea558e 100644
--- a/code/modules/vehicles/interior/interactable/seats.dm
+++ b/code/modules/vehicles/interior/interactable/seats.dm
@@ -40,8 +40,8 @@
return
if(QDELETED(buckled_mob))
- vehicle.set_seated_mob(seat, null)
M.unset_interaction()
+ vehicle.set_seated_mob(seat, null)
if(M.client)
M.client.change_view(GLOB.world_view_size, vehicle)
M.client.pixel_x = 0
@@ -174,8 +174,8 @@
return
if(QDELETED(buckled_mob))
- vehicle.set_seated_mob(seat, null)
M.unset_interaction()
+ vehicle.set_seated_mob(seat, null)
if(M.client)
M.client.change_view(GLOB.world_view_size, vehicle)
M.client.pixel_x = 0
@@ -252,8 +252,8 @@
return
if(QDELETED(buckled_mob))
- vehicle.set_seated_mob(seat, null)
M.unset_interaction()
+ vehicle.set_seated_mob(seat, null)
if(M.client)
M.client.change_view(GLOB.world_view_size, vehicle)
M.client.pixel_x = 0
diff --git a/code/modules/vehicles/multitile/multitile.dm b/code/modules/vehicles/multitile/multitile.dm
index c8138c5b8f86..f3b7be510b08 100644
--- a/code/modules/vehicles/multitile/multitile.dm
+++ b/code/modules/vehicles/multitile/multitile.dm
@@ -340,15 +340,24 @@
M.reset_view(src)
give_action(M, /datum/action/human_action/vehicle_unbuckle)
+/// Get crewmember of seat.
/obj/vehicle/multitile/proc/get_seat_mob(seat)
return seats[seat]
+/// Get seat of crewmember.
/obj/vehicle/multitile/proc/get_mob_seat(mob/M)
for(var/seat in seats)
if(seats[seat] == M)
return seat
return null
+/// Get active hardpoint of crewmember.
+/obj/vehicle/multitile/proc/get_mob_hp(mob/crew)
+ var/seat = get_mob_seat(crew)
+ if(seat)
+ return active_hp[seat]
+ return null
+
/obj/vehicle/multitile/proc/get_passengers()
if(interior)
return interior.get_passengers()
diff --git a/code/modules/vehicles/multitile/multitile_hardpoints.dm b/code/modules/vehicles/multitile/multitile_hardpoints.dm
index 2c5a343b802a..a6014c6cf2cd 100644
--- a/code/modules/vehicles/multitile/multitile_hardpoints.dm
+++ b/code/modules/vehicles/multitile/multitile_hardpoints.dm
@@ -230,47 +230,3 @@
qdel(old)
update_icon()
-
-//proc that fires non selected weaponry
-/obj/vehicle/multitile/proc/shoot_other_weapon(mob/living/carbon/human/M, seat, atom/A)
-
- if(!istype(M))
- return
-
- var/list/usable_hps = get_hardpoints_with_ammo(seat)
- for(var/obj/item/hardpoint/HP in usable_hps)
- if(HP == active_hp[seat] || HP.slot != HDPT_PRIMARY && HP.slot != HDPT_SECONDARY)
- usable_hps.Remove(HP)
-
- if(!LAZYLEN(usable_hps))
- to_chat(M, SPAN_WARNING("No other working weapons detected."))
- return
-
- for(var/obj/item/hardpoint/HP in usable_hps)
- if(!HP.can_activate(M, A))
- return
- HP.activate(M, A)
- break
- return
-
-//proc that activates support module if it can be activated and you meet requirements
-/obj/vehicle/multitile/proc/activate_support_module(mob/living/carbon/human/M, seat, atom/A)
-
- if(!istype(M))
- return
-
- var/list/usable_hps = get_activatable_hardpoints(seat)
- for(var/obj/item/hardpoint/HP in usable_hps)
- if(HP.slot != HDPT_SUPPORT)
- usable_hps.Remove(HP)
-
- if(!LAZYLEN(usable_hps))
- to_chat(M, SPAN_WARNING("No activatable support modules detected."))
- return
-
- for(var/obj/item/hardpoint/HP in usable_hps)
- if(!HP.can_activate(M, A))
- return
- HP.activate(M, A)
- break
- return
diff --git a/code/modules/vehicles/multitile/multitile_interaction.dm b/code/modules/vehicles/multitile/multitile_interaction.dm
index 42b141327bd8..aa2025d151b5 100644
--- a/code/modules/vehicles/multitile/multitile_interaction.dm
+++ b/code/modules/vehicles/multitile/multitile_interaction.dm
@@ -330,80 +330,61 @@
healthcheck()
-/obj/vehicle/multitile/handle_click(mob/living/user, atom/A, list/mods)
-
- var/seat
- for(var/vehicle_seat in seats)
- if(seats[vehicle_seat] == user)
- seat = vehicle_seat
- break
-
- if(istype(A, /atom/movable/screen) || !seat)
+/obj/vehicle/multitile/on_set_interaction(mob/user)
+ 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))
+
+/obj/vehicle/multitile/on_unset_interaction(mob/user)
+ UnregisterSignal(user, list(COMSIG_MOB_MOUSEUP, COMSIG_MOB_MOUSEDOWN, COMSIG_MOB_MOUSEDRAG))
+
+ var/obj/item/hardpoint/hardpoint = get_mob_hp(user)
+ if(hardpoint)
+ SEND_SIGNAL(hardpoint, COMSIG_GUN_INTERRUPT_FIRE) //abort fire when crew leaves
+
+/// Relays crew mouse release to active hardpoint.
+/obj/vehicle/multitile/proc/crew_mouseup(datum/source, atom/object, turf/location, control, params)
+ SIGNAL_HANDLER
+ var/obj/item/hardpoint/hardpoint = get_mob_hp(source)
+ if(!hardpoint)
return
- if(seat == VEHICLE_DRIVER)
- if(mods["shift"] && !mods["alt"])
- A.examine(user)
- return
-
- if(mods["ctrl"] && !mods["alt"])
- activate_horn()
- return
-
- var/obj/item/hardpoint/HP = active_hp[seat]
- if(!HP)
- to_chat(user, SPAN_WARNING("Please select an active hardpoint first."))
- return
-
- if(!HP.can_activate(user, A))
- return
-
- HP.activate(user, A)
-
- if(seat == VEHICLE_GUNNER)
- if(mods["shift"] && !mods["middle"])
- if(vehicle_flags & VEHICLE_TOGGLE_SHIFT_CLICK_GUNNER)
- shoot_other_weapon(user, seat, A)
- else
- A.examine(user)
- return
- if(mods["middle"] && !mods["shift"])
- if(!(vehicle_flags & VEHICLE_TOGGLE_SHIFT_CLICK_GUNNER))
- shoot_other_weapon(user, seat, A)
- return
- if(mods["alt"])
- toggle_gyrostabilizer()
- return
- if(mods["ctrl"])
- activate_support_module(user, seat, A)
- return
+ hardpoint.stop_fire(source, object, location, control, params)
- var/obj/item/hardpoint/HP = active_hp[seat]
- if(!HP)
- to_chat(user, SPAN_WARNING("Please select an active hardpoint first."))
- return
+/// Relays crew mouse movement to active hardpoint.
+/obj/vehicle/multitile/proc/crew_mousedrag(datum/source, atom/src_object, atom/over_object, turf/src_location, turf/over_location, src_control, over_control, params)
+ SIGNAL_HANDLER
+ var/obj/item/hardpoint/hardpoint = get_mob_hp(source)
+ if(!hardpoint)
+ return
- if(!HP.can_activate(user, A))
- return
+ hardpoint.change_target(source, src_object, over_object, src_location, over_location, src_control, over_control, params)
- HP.activate(user, A)
+/// 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
- if(seat == VEHICLE_SUPPORT_GUNNER_ONE || seat == VEHICLE_SUPPORT_GUNNER_TWO)
- if(mods["shift"])
- A.examine(user)
- return
- if(mods["middle"] || mods["alt"] || mods["ctrl"])
- return
+ var/list/modifiers = params2list(params)
+ if(modifiers[SHIFT_CLICK] || modifiers[MIDDLE_CLICK] || modifiers[RIGHT_CLICK]) //don't step on examine, point, etc
+ return
- var/obj/item/hardpoint/HP = active_hp[seat]
- if(!HP)
- to_chat(user, SPAN_WARNING("Please select an active hardpoint first."))
- return
+ var/seat = get_mob_seat(source)
+ switch(seat)
+ if(VEHICLE_DRIVER)
+ if(modifiers[LEFT_CLICK] && modifiers[CTRL_CLICK])
+ activate_horn()
+ return
+ if(VEHICLE_GUNNER)
+ if(modifiers[LEFT_CLICK] && modifiers[ALT_CLICK])
+ toggle_gyrostabilizer()
+ return
- if(!HP.can_activate(user, A))
- return
+ var/obj/item/hardpoint/hardpoint = get_mob_hp(source)
+ if(!hardpoint)
+ to_chat(source, SPAN_WARNING("Please select an active hardpoint first."))
+ return
- HP.activate(user, A)
+ hardpoint.start_fire(source, object, location, control, params)
/obj/vehicle/multitile/proc/handle_player_entrance(mob/M)
if(!M || M.client == null) return
diff --git a/code/modules/vehicles/multitile/multitile_verbs.dm b/code/modules/vehicles/multitile/multitile_verbs.dm
index c7dd29bbf0a9..3801cd2e176c 100644
--- a/code/modules/vehicles/multitile/multitile_verbs.dm
+++ b/code/modules/vehicles/multitile/multitile_verbs.dm
@@ -30,6 +30,10 @@
if(!HP)
return
+ var/obj/item/hardpoint/old_HP = V.active_hp[seat]
+ if(old_HP)
+ SEND_SIGNAL(old_HP, COMSIG_GUN_INTERRUPT_FIRE) //stop fire when switching away from HP
+
V.active_hp[seat] = HP
var/msg = "You select \the [HP]."
if(HP.ammo)
@@ -66,6 +70,10 @@
if(!HP)
return
+ var/obj/item/hardpoint/old_HP = V.active_hp[seat]
+ if(old_HP)
+ SEND_SIGNAL(old_HP, COMSIG_GUN_INTERRUPT_FIRE) //stop fire when switching away from HP
+
V.active_hp[seat] = HP
var/msg = "You select \the [HP]."
if(HP.ammo)
@@ -225,10 +233,7 @@
3. \"G: Toggle Turret Gyrostabilizer\" - toggles Turret Gyrostabilizer allowing it to keep current direction ignoring hull turning. (Exists only on vehicles with rotating turret, e.g. M34A2 Longstreet Light Tank)
\
Support Gunner verbs:
1. \"Reload Firing Port Weapon\" - initiates automated reloading process for M56 FPW. Requires a confirmation.
\
Driver shortcuts:
1. \"CTRL + Click\" - activates vehicle horn.
\
- Gunner shortcuts:
1. \"ALT + Click\" - toggles Turret Gyrostabilizer. (Exists only on vehicles with rotating turret, e.g. M34A2 Longstreet Light Tank)
\
- 2. \"CTRL + Click\" - activates not destroyed activatable support module.
\
- 3. \"Middle Mouse Button Click (MMB)\" - default shortcut to shoot currently not selected weapon if possible. Won't work if SHIFT + Click firing is toggled ON.
\
- 4. \"SHIFT + Click\" - examines target as usual, unless \"G: Toggle Middle/Shift Clicking\" verb was used to toggle SHIFT + Click firing ON. In this case, it will fire currently not selected weapon if possible.
"
+ Gunner shortcuts:
1. \"ALT + Click\" - toggles Turret Gyrostabilizer. (Exists only on vehicles with rotating turret, e.g. M34A2 Longstreet Light Tank)
"
show_browser(user, dat, "Vehicle Controls Guide", "vehicle_help", "size=900x500")
onclose(user, "vehicle_help")