mirror of
https://github.com/Bubberstation/Bubberstation.git
synced 2025-12-11 02:01:22 +00:00
## About The Pull Request  Resprites: Tesla Cannon Tesla Cannon crafting kit ### New SFX / VFX The tesla cannon now uses a new type of beam effect that randomly picks sprite variants for each segment instead of a tracer. This makes the arc look more dynamic and less distorted. Autofire guns can now choose to use a looping sound datum when firing.  #### Balance changes The tesla cannon must now have its stock unfolded before firing, this takes 1.5 seconds and makes the gun bulky. It is still normal sized when folded, and folding it is instant. ### Bug fixes Fixed a bug where looping_sound.stop() would fail to stop sounds. The tesla cannon is an incredibly powerfu ## Why It's Good For The Game ### My reasons for respriting The old sprite was not bad, by all means but I had a few gripes with it. * The old sprite does not incorporate the flux anomaly yellow colour. * The old sprite looks a bit much like a real, professionally produced sci-fi weapon, * The old sprite looks pretty small for such a ultra high dps full auto weapon. * The old inhand is quite indistinct for something that can game end you in like one second. ### My design I think that anomaly items should be very mad science coded and, since anomaly science is by definition a poorly studied field, they should look more like prototypes created by a scientist rather than something professionally made in a factory. ## Changelog 🆑 image: The tesla cannon has new sprites. image: The tesla parts kit has new sprites. image: The tesla cannon has a new shocking beam effect when firing. sound: The tesla cannon has new sounds. balance: The tesla cannon must now be unfolded to fire. fix: looping sounds now stop playing sounds when commaned to do so. /🆑
332 lines
13 KiB
Plaintext
332 lines
13 KiB
Plaintext
#define AUTOFIRE_MOUSEUP 0
|
|
#define AUTOFIRE_MOUSEDOWN 1
|
|
|
|
/datum/component/automatic_fire
|
|
var/client/clicker
|
|
var/mob/living/shooter
|
|
var/atom/target
|
|
var/turf/target_loc //For dealing with locking on targets due to BYOND engine limitations (the mouse input only happening when mouse moves).
|
|
var/autofire_stat = AUTOFIRE_STAT_IDLE
|
|
var/mouse_parameters
|
|
/// Time between individual shots.
|
|
var/autofire_shot_delay = 0.3 SECONDS
|
|
/// This seems hacky but there can be two MouseDown() without a MouseUp() in between if the user holds click and uses alt+tab, printscreen or similar.
|
|
var/mouse_status = AUTOFIRE_MOUSEUP
|
|
/// Should dual wielding be allowed?
|
|
var/allow_akimbo
|
|
|
|
///windup autofire vars
|
|
///Whether the delay between shots increases over time, simulating a spooling weapon
|
|
var/windup_autofire = FALSE
|
|
///the reduction to shot delay for windup
|
|
var/current_windup_reduction = 0
|
|
///the percentage of autfire_shot_delay that is added to current_windup_reduction
|
|
var/windup_autofire_reduction_multiplier = 0.3
|
|
///How high of a reduction that current_windup_reduction can reach
|
|
var/windup_autofire_cap = 0.3
|
|
///How long it takes for weapons that have spooled-up to reset back to the original firing speed
|
|
var/windup_spindown = 3 SECONDS
|
|
///Timer for tracking the spindown reset timings
|
|
var/timerid
|
|
///Looping sound while firing.
|
|
var/datum/looping_sound/autofire_sound_loop
|
|
COOLDOWN_DECLARE(next_shot_cd)
|
|
|
|
|
|
/datum/component/automatic_fire/Initialize(autofire_shot_delay, windup_autofire, windup_autofire_reduction_multiplier, windup_autofire_cap, windup_spindown, allow_akimbo = TRUE, firing_sound_loop)
|
|
. = ..()
|
|
if(!isgun(parent))
|
|
return COMPONENT_INCOMPATIBLE
|
|
var/obj/item/gun = parent
|
|
RegisterSignal(parent, COMSIG_ITEM_EQUIPPED, PROC_REF(wake_up))
|
|
if(autofire_shot_delay)
|
|
src.autofire_shot_delay = autofire_shot_delay
|
|
src.allow_akimbo = allow_akimbo
|
|
if(windup_autofire)
|
|
src.windup_autofire = windup_autofire
|
|
src.windup_autofire_reduction_multiplier = windup_autofire_reduction_multiplier
|
|
src.windup_autofire_cap = windup_autofire_cap
|
|
src.windup_spindown = windup_spindown
|
|
if(autofire_stat == AUTOFIRE_STAT_IDLE && ismob(gun.loc))
|
|
var/mob/user = gun.loc
|
|
wake_up(src, user)
|
|
|
|
if(firing_sound_loop)
|
|
autofire_sound_loop = new firing_sound_loop(parent)
|
|
|
|
|
|
/datum/component/automatic_fire/Destroy()
|
|
QDEL_NULL(autofire_sound_loop)
|
|
autofire_off()
|
|
return ..()
|
|
|
|
/datum/component/automatic_fire/process(seconds_per_tick)
|
|
if(autofire_stat != AUTOFIRE_STAT_FIRING)
|
|
STOP_PROCESSING(SSprojectiles, src)
|
|
return
|
|
process_shot()
|
|
|
|
/datum/component/automatic_fire/proc/wake_up(datum/source, mob/user, slot)
|
|
SIGNAL_HANDLER
|
|
|
|
if(autofire_stat == AUTOFIRE_STAT_ALERT)
|
|
return //We've updated the firemode. No need for more.
|
|
if(autofire_stat == AUTOFIRE_STAT_FIRING)
|
|
stop_autofiring() //Let's stop shooting to avoid issues.
|
|
return
|
|
if(user.is_holding(parent))
|
|
autofire_on(user.client)
|
|
|
|
// There is a gun and there is a user wielding it. The component now waits for the mouse click.
|
|
/datum/component/automatic_fire/proc/autofire_on(client/usercli)
|
|
SIGNAL_HANDLER
|
|
|
|
if(autofire_stat != AUTOFIRE_STAT_IDLE)
|
|
return
|
|
autofire_stat = AUTOFIRE_STAT_ALERT
|
|
if(!QDELETED(usercli))
|
|
clicker = usercli
|
|
shooter = clicker.mob
|
|
RegisterSignal(clicker, COMSIG_CLIENT_MOUSEDOWN, PROC_REF(on_mouse_down))
|
|
if(!QDELETED(shooter))
|
|
RegisterSignal(shooter, COMSIG_MOB_LOGOUT, PROC_REF(autofire_off))
|
|
UnregisterSignal(shooter, COMSIG_MOB_LOGIN)
|
|
RegisterSignals(parent, list(COMSIG_QDELETING, COMSIG_ITEM_DROPPED), PROC_REF(autofire_off))
|
|
parent.RegisterSignal(src, COMSIG_AUTOFIRE_ONMOUSEDOWN, TYPE_PROC_REF(/obj/item/gun/, autofire_bypass_check))
|
|
parent.RegisterSignal(parent, COMSIG_AUTOFIRE_SHOT, TYPE_PROC_REF(/obj/item/gun/, do_autofire))
|
|
|
|
|
|
/datum/component/automatic_fire/proc/autofire_off(datum/source)
|
|
SIGNAL_HANDLER
|
|
if(autofire_stat == AUTOFIRE_STAT_IDLE)
|
|
return
|
|
if(autofire_stat == AUTOFIRE_STAT_FIRING)
|
|
stop_autofiring()
|
|
|
|
autofire_stat = AUTOFIRE_STAT_IDLE
|
|
|
|
if(!QDELETED(clicker))
|
|
UnregisterSignal(clicker, list(COMSIG_CLIENT_MOUSEDOWN, COMSIG_CLIENT_MOUSEUP, COMSIG_CLIENT_MOUSEDRAG))
|
|
mouse_status = AUTOFIRE_MOUSEUP //In regards to the component there's no click anymore to care about.
|
|
clicker = null
|
|
if(!QDELETED(shooter))
|
|
RegisterSignal(shooter, COMSIG_MOB_LOGIN, PROC_REF(on_client_login))
|
|
UnregisterSignal(shooter, COMSIG_MOB_LOGOUT)
|
|
UnregisterSignal(parent, list(COMSIG_QDELETING, COMSIG_ITEM_DROPPED))
|
|
shooter = null
|
|
parent.UnregisterSignal(parent, COMSIG_AUTOFIRE_SHOT)
|
|
parent.UnregisterSignal(src, COMSIG_AUTOFIRE_ONMOUSEDOWN)
|
|
|
|
/datum/component/automatic_fire/proc/on_client_login(mob/source)
|
|
SIGNAL_HANDLER
|
|
if(!source.client)
|
|
return
|
|
if(source.is_holding(parent))
|
|
autofire_on(source.client)
|
|
|
|
/datum/component/automatic_fire/proc/on_mouse_down(client/source, atom/_target, turf/location, control, params)
|
|
SIGNAL_HANDLER
|
|
var/list/modifiers = params2list(params) //If they're shift+clicking, for example, let's not have them accidentally shoot.
|
|
|
|
if(LAZYACCESS(modifiers, SHIFT_CLICK))
|
|
return
|
|
if(LAZYACCESS(modifiers, CTRL_CLICK))
|
|
return
|
|
if(LAZYACCESS(modifiers, MIDDLE_CLICK))
|
|
return
|
|
if(LAZYACCESS(modifiers, RIGHT_CLICK))
|
|
return
|
|
if(LAZYACCESS(modifiers, ALT_CLICK))
|
|
return
|
|
if(source.mob.throw_mode)
|
|
return
|
|
if(!isturf(source.mob.loc)) //No firing inside lockers and stuff.
|
|
return
|
|
if(get_dist(source.mob, _target) < 2) //Adjacent clicking.
|
|
return
|
|
|
|
if(isnull(location) || istype(_target, /atom/movable/screen)) //Clicking on a screen object.
|
|
if(_target.plane != CLICKCATCHER_PLANE) //The clickcatcher is a special case. We want the click to trigger then, under it.
|
|
return //If we click and drag on our worn backpack, for example, we want it to open instead.
|
|
_target = parse_caught_click_modifiers(modifiers, get_turf(source.eye), source)
|
|
params = list2params(modifiers)
|
|
if(!_target)
|
|
CRASH("Failed to get the turf under clickcatcher")
|
|
|
|
if(SEND_SIGNAL(src, COMSIG_AUTOFIRE_ONMOUSEDOWN, source, _target, location, control, params) & COMPONENT_AUTOFIRE_ONMOUSEDOWN_BYPASS)
|
|
return
|
|
|
|
source.click_intercept_time = world.time //From this point onwards Click() will no longer be triggered.
|
|
|
|
if(autofire_stat == (AUTOFIRE_STAT_IDLE))
|
|
CRASH("on_mouse_down() called with [autofire_stat] autofire_stat")
|
|
if(autofire_stat == AUTOFIRE_STAT_FIRING)
|
|
stop_autofiring() //This can happen if we click and hold and then alt+tab, printscreen or other such action. MouseUp won't be called then and it will keep autofiring.
|
|
|
|
target = _target
|
|
target_loc = get_turf(target)
|
|
mouse_parameters = params
|
|
INVOKE_ASYNC(src, PROC_REF(start_autofiring))
|
|
|
|
|
|
//Dakka-dakka
|
|
/datum/component/automatic_fire/proc/start_autofiring()
|
|
if(autofire_stat == AUTOFIRE_STAT_FIRING)
|
|
return
|
|
autofire_stat = AUTOFIRE_STAT_FIRING
|
|
|
|
clicker.mouse_override_icon = 'icons/effects/mouse_pointers/weapon_pointer.dmi'
|
|
clicker.mouse_pointer_icon = clicker.mouse_override_icon
|
|
|
|
if(mouse_status == AUTOFIRE_MOUSEUP) //See mouse_status definition for the reason for this.
|
|
RegisterSignal(clicker, COMSIG_CLIENT_MOUSEUP, PROC_REF(on_mouse_up))
|
|
mouse_status = AUTOFIRE_MOUSEDOWN
|
|
|
|
RegisterSignal(shooter, COMSIG_MOB_SWAP_HANDS, PROC_REF(stop_autofiring))
|
|
|
|
if(isgun(parent))
|
|
var/obj/item/gun/shoota = parent
|
|
if(!shoota.on_autofire_start(shooter)) //This is needed because the minigun has a do_after before firing and signals are async.
|
|
stop_autofiring()
|
|
return
|
|
if(autofire_stat != AUTOFIRE_STAT_FIRING)
|
|
return //Things may have changed while on_autofire_start() was being processed, due to do_after's sleep.
|
|
|
|
if(!process_shot()) //First shot is processed instantly.
|
|
return //If it fails, such as when the gun is empty, then there's no need to schedule a second shot.
|
|
|
|
START_PROCESSING(SSprojectiles, src)
|
|
RegisterSignal(clicker, COMSIG_CLIENT_MOUSEDRAG, PROC_REF(on_mouse_drag))
|
|
|
|
if(autofire_sound_loop)
|
|
autofire_sound_loop.start(shooter)
|
|
|
|
/datum/component/automatic_fire/proc/on_mouse_up(datum/source, atom/object, turf/location, control, params)
|
|
SIGNAL_HANDLER
|
|
UnregisterSignal(clicker, COMSIG_CLIENT_MOUSEUP)
|
|
mouse_status = AUTOFIRE_MOUSEUP
|
|
if(autofire_stat == AUTOFIRE_STAT_FIRING)
|
|
stop_autofiring()
|
|
return COMPONENT_CLIENT_MOUSEUP_INTERCEPT
|
|
|
|
|
|
/datum/component/automatic_fire/proc/stop_autofiring(datum/source, atom/object, turf/location, control, params)
|
|
SIGNAL_HANDLER
|
|
if(autofire_stat != AUTOFIRE_STAT_FIRING)
|
|
return
|
|
STOP_PROCESSING(SSprojectiles, src)
|
|
autofire_stat = AUTOFIRE_STAT_ALERT
|
|
if(clicker)
|
|
clicker.mouse_override_icon = null
|
|
clicker.mouse_pointer_icon = clicker.mouse_override_icon
|
|
UnregisterSignal(clicker, COMSIG_CLIENT_MOUSEDRAG)
|
|
if(!QDELETED(shooter))
|
|
UnregisterSignal(shooter, COMSIG_MOB_SWAP_HANDS)
|
|
target = null
|
|
target_loc = null
|
|
mouse_parameters = null
|
|
|
|
if(autofire_sound_loop)
|
|
autofire_sound_loop.stop()
|
|
|
|
/datum/component/automatic_fire/proc/on_mouse_drag(client/source, atom/src_object, atom/over_object, turf/src_location, turf/over_location, src_control, over_control, params)
|
|
SIGNAL_HANDLER
|
|
if(isnull(over_location)) //This happens when the mouse is over an inventory or screen object, or on entering deep darkness, for example.
|
|
var/list/modifiers = params2list(params)
|
|
var/new_target = parse_caught_click_modifiers(modifiers, get_turf(source.eye), source)
|
|
params = list2params(modifiers)
|
|
mouse_parameters = params
|
|
if(!new_target)
|
|
if(QDELETED(target)) //No new target acquired, and old one was deleted, get us out of here.
|
|
stop_autofiring()
|
|
CRASH("on_mouse_drag failed to get the turf under screen object [over_object.type]. Old target was incidentally QDELETED.")
|
|
target = get_turf(target) //If previous target wasn't a turf, let's turn it into one to avoid locking onto a potentially moving target.
|
|
target_loc = target
|
|
CRASH("on_mouse_drag failed to get the turf under screen object [over_object.type]")
|
|
target = new_target
|
|
target_loc = new_target
|
|
return
|
|
target = over_object
|
|
target_loc = get_turf(over_object)
|
|
mouse_parameters = params
|
|
|
|
/datum/component/automatic_fire/proc/process_shot()
|
|
if(autofire_stat != AUTOFIRE_STAT_FIRING)
|
|
return FALSE
|
|
if(!COOLDOWN_FINISHED(src, next_shot_cd))
|
|
return TRUE
|
|
if(QDELETED(target) || get_turf(target) != target_loc) //Target moved or got destroyed since we last aimed.
|
|
target = target_loc //So we keep firing on the emptied tile until we move our mouse and find a new target.
|
|
if(get_dist(shooter, target) <= 0)
|
|
target = get_step(shooter, shooter.dir) //Shoot in the direction faced if the mouse is on the same tile as we are.
|
|
target_loc = target
|
|
else if(!CAN_THEY_SEE(target, shooter))
|
|
stop_autofiring() //Elvis has left the building.
|
|
return FALSE
|
|
shooter.face_atom(target)
|
|
var/next_delay = autofire_shot_delay
|
|
if(windup_autofire)
|
|
next_delay = clamp(next_delay - current_windup_reduction, round(autofire_shot_delay * windup_autofire_cap), autofire_shot_delay)
|
|
current_windup_reduction = (current_windup_reduction + round(autofire_shot_delay * windup_autofire_reduction_multiplier))
|
|
timerid = addtimer(CALLBACK(src, PROC_REF(windup_reset), FALSE), windup_spindown, TIMER_UNIQUE|TIMER_OVERRIDE|TIMER_STOPPABLE)
|
|
if(HAS_TRAIT(shooter, TRAIT_DOUBLE_TAP))
|
|
next_delay = round(next_delay * 0.5, SSprojectiles.wait)
|
|
COOLDOWN_START(src, next_shot_cd, next_delay)
|
|
if(SEND_SIGNAL(parent, COMSIG_AUTOFIRE_SHOT, target, shooter, allow_akimbo, mouse_parameters) & COMPONENT_AUTOFIRE_SHOT_SUCCESS)
|
|
return TRUE
|
|
stop_autofiring()
|
|
return FALSE
|
|
|
|
/// Reset for our windup, resetting everything back to initial values after a variable set amount of time (determined by var/windup_spindown).
|
|
/datum/component/automatic_fire/proc/windup_reset(deltimer)
|
|
current_windup_reduction = initial(current_windup_reduction)
|
|
if(deltimer && timerid)
|
|
deltimer(timerid)
|
|
|
|
// Gun procs.
|
|
|
|
/obj/item/gun/proc/on_autofire_start(mob/living/shooter)
|
|
if(fire_cd || shooter.incapacitated || !can_trigger_gun(shooter))
|
|
return FALSE
|
|
if(!can_shoot())
|
|
shoot_with_empty_chamber(shooter)
|
|
return FALSE
|
|
var/obj/item/bodypart/other_hand = shooter.has_hand_for_held_index(shooter.get_inactive_hand_index())
|
|
if(weapon_weight == WEAPON_HEAVY && (shooter.get_inactive_held_item() || !other_hand))
|
|
balloon_alert(shooter, "use both hands!")
|
|
return FALSE
|
|
return TRUE
|
|
|
|
|
|
/obj/item/gun/proc/autofire_bypass_check(datum/source, client/clicker, atom/target, turf/location, control, params)
|
|
SIGNAL_HANDLER
|
|
if(clicker.mob.get_active_held_item() != src)
|
|
return COMPONENT_AUTOFIRE_ONMOUSEDOWN_BYPASS
|
|
|
|
|
|
/obj/item/gun/proc/do_autofire(datum/source, atom/target, mob/living/shooter, allow_akimbo, params)
|
|
SIGNAL_HANDLER
|
|
if(fire_cd || shooter.incapacitated)
|
|
return NONE
|
|
if(!can_shoot())
|
|
shoot_with_empty_chamber(shooter)
|
|
return NONE
|
|
INVOKE_ASYNC(src, PROC_REF(do_autofire_shot), source, target, shooter, allow_akimbo, params)
|
|
return COMPONENT_AUTOFIRE_SHOT_SUCCESS //All is well, we can continue shooting.
|
|
|
|
|
|
/obj/item/gun/proc/do_autofire_shot(datum/source, atom/target, mob/living/shooter, allow_akimbo, params)
|
|
var/obj/item/gun/akimbo_gun = shooter.get_inactive_held_item()
|
|
var/bonus_spread = 0
|
|
if(istype(akimbo_gun) && weapon_weight < WEAPON_MEDIUM && allow_akimbo)
|
|
if(akimbo_gun.weapon_weight < WEAPON_MEDIUM && akimbo_gun.can_trigger_gun(shooter))
|
|
if(!akimbo_gun.can_shoot())
|
|
addtimer(CALLBACK(akimbo_gun, TYPE_PROC_REF(/obj/item/gun, shoot_with_empty_chamber), shooter), 0.1 SECONDS)
|
|
else
|
|
bonus_spread = dual_wield_spread
|
|
addtimer(CALLBACK(akimbo_gun, TYPE_PROC_REF(/obj/item/gun, process_fire), target, shooter, TRUE, params, null, bonus_spread), 0.1 SECONDS)
|
|
process_fire(target, shooter, TRUE, params, null, bonus_spread)
|
|
|
|
#undef AUTOFIRE_MOUSEUP
|
|
#undef AUTOFIRE_MOUSEDOWN
|