From f4f334de22e5d2782f35115a9b1461326e1c4a8c Mon Sep 17 00:00:00 2001 From: Doubleumc Date: Wed, 6 Dec 2023 05:38:20 -0500 Subject: [PATCH] Vehicle autofire (#4959) # About the pull request Convert vehicle hardpoints from using their bespoke firing system to one structured closely on handheld guns and deployables such as the M2C. Now using the `autofire` component. Much like handheld weapons it is capable of different firemodes (semi/burst/auto) and changing targets during fire. Hardpoints were converted to match their old effectiveness as closely as possible; this is intended as a quality of life improvement, not a rebalance. Damage, AP, range, ammo, etc were not touched. Fire rates were copied over directly. Single-fire weapons with long delays were made semi-auto (e.g. LTB), and those with short delays were made full-auto (e.g. autocannon). Burst-fire weapons with significant extra delays after the burst remained burst-fire (cupola, smokescreen), and the rest were converted to full-auto (e.g. dual cannon). While changing firemodes is easily implemented, no weapon seemed a good candidate for more than one firemode and so that is omitted for now. Scatter was approximated. The existing `accuracy` functioned as a percent chance the shot would stray one tile from the target. Gun-style `scatter` is instead a cone of fire in degrees. No direct conversion is possible, so scatter values are roughly set such that firing at a tile at the edge of the screen should "feel" about as accurate. Closer ranges would experience less spread than before, longer ranges more. The buffing weapon sensor module was adjusted to work with the new firing system, and effects hardpoint scatter angle and firing rate. Vehicle buffs still use multipliers instead of adding/subtracting as handheld guns do, as a flat +/- adjustment to fire delay would have a significantly different effect on slow firing weapons (e.g. LTB) vs fast firing (e.g. autocannon). One major difference is that burstfire delays are effected and buffs increases the burst density. Before, there was a single cooldown initiated at the start of the burst, and only that cooldown was modified by the buff. Now, since the inter-burst delay is needed by the `autofire` component both the inter-burst delay and the after-burst delay are modified by buffs. Activating non-selected hardpoints was removed as not compatible. The issue is that tracking a single click's modifiers is no longer sufficient, it has to track through the whole mousedown-to-mouseup period and the user can change multiple click modifiers in that time. I could not find a method that was satisfactory without a much bigger overhaul of vehicle controls than I'd like to take on in a PR not meant for it. I'm sure it can be done, but that brings up the question of if that's even the control scheme we'd want, in a PR that was never meant to ask that question let alone answer it. # Explain why it's good for the game Vehicle weapons using `gun`-like code makes them easier and more familiar to use, and more code commonality makes maintenance just a little bit easier. # Testing Photographs and Procedure
Screenshots & Videos Put screenshots and videos here with an empty line between the screenshots and the `
` tags.
# Changelog :cl: refactor: vehicle weapons can fire full-auto del: no more controls for firing vehicle non-selected weapons /:cl: --- code/modules/vehicles/hardpoints/hardpoint.dm | 441 ++++++++++++------ .../vehicles/hardpoints/holder/holder.dm | 24 +- .../vehicles/hardpoints/holder/tank_turret.dm | 46 +- .../vehicles/hardpoints/primary/autocannon.dm | 9 +- .../hardpoints/primary/dual_cannon.dm | 29 +- .../vehicles/hardpoints/primary/flamer.dm | 37 +- .../vehicles/hardpoints/primary/ltb.dm | 5 +- .../vehicles/hardpoints/primary/minigun.dm | 86 ++-- .../vehicles/hardpoints/secondary/cupola.dm | 31 +- .../vehicles/hardpoints/secondary/flamer.dm | 47 +- .../hardpoints/secondary/frontal_cannon.dm | 29 +- .../hardpoints/secondary/grenade_launcher.dm | 41 +- .../vehicles/hardpoints/secondary/tow.dm | 6 +- .../hardpoints/special/firing_port_weapon.dm | 93 ++-- .../vehicles/hardpoints/support/artillery.dm | 9 +- .../vehicles/hardpoints/support/flare.dm | 5 +- .../vehicles/interior/interactable/seats.dm | 6 +- code/modules/vehicles/multitile/multitile.dm | 9 + .../multitile/multitile_hardpoints.dm | 44 -- .../multitile/multitile_interaction.dm | 111 ++--- .../vehicles/multitile/multitile_verbs.dm | 13 +- 21 files changed, 550 insertions(+), 571 deletions(-) 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 ea961bc72593..253b4a066b4f 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")