Files
Aurora.3/code/modules/projectiles/projectile.dm
Cody Brittain 9a6edb999f Cleanup and expand suppressors (#17173)
* Cleanup and expand suppressors

Suicide uses suppressed sounds too

Fix CL

Added the suppressor to the antagonist uplink

Fix CL escape characters

Fix random list

Resolve issues from code review

Split the suppressor into its own file

* Bayonets and ammo displays now add their weight to their attached weapon

* Balloon alerts for weapon mods

* Fix lint
2023-09-04 08:22:14 +00:00

701 lines
25 KiB
Plaintext

#define MUZZLE_EFFECT_PIXEL_INCREMENT 16 //How many pixels to move the muzzle flash up so your character doesn't look like they're shitting out lasers.
/obj/item/projectile
name = "projectile"
icon = 'icons/obj/projectiles.dmi'
icon_state = "bullet"
density = TRUE
unacidable = TRUE
anchored = TRUE //There's a reason this is here, Mport. God fucking damn it -Agouri. Find&Fix by Pete. The reason this is here is to stop the curving of emitter shots.
pass_flags = PASSTABLE|PASSRAILING
mouse_opacity = MOUSE_OPACITY_TRANSPARENT
animate_movement = 0 //Use SLIDE_STEPS in conjunction with legacy
var/projectile_type = /obj/item/projectile
var/ping_effect = "ping_b" //Effect displayed when a bullet hits a barricade. See atom/proc/bullet_ping.
var/def_zone = "" //Aiming at
var/hit_zone // The place that actually got hit
var/mob/firer = null//Who shot it
var/suppressed = FALSE //Attack message
var/shot_from = "" // name of the object which shot us
var/accuracy = 0
var/dispersion = 0.0
//used for shooting at blank range, you shouldn't be able to miss
var/point_blank = FALSE
//Effects
var/damage = 10
var/damage_type = DAMAGE_BRUTE //DAMAGE_BRUTE, DAMAGE_BURN, DAMAGE_TOXIN, DAMAGE_OXY, DAMAGE_CLONE, DAMAGE_PAIN are the only things that should be in here
var/damage_flags = DAMAGE_FLAG_BULLET
var/nodamage = FALSE //Determines if the projectile will skip any damage inflictions
var/check_armor = "bullet" //Defines what armor to use when it hits things. Must be set to bullet, laser, energy,or bomb //Cael - bio and rad are also valid
var/list/impact_sounds //for different categories, IMPACT_MEAT etc
var/stun = 0
var/weaken = 0
var/paralyze = 0
var/irradiate = 0
var/stutter = 0
var/eyeblur = 0
var/drowsy = 0
var/agony = 0
var/incinerate = 0
var/embed = 0 // whether or not the projectile can embed itself in the mob
var/embed_chance = 0 // a flat bonus to the % chance to embed
var/shrapnel_type //type of shrapnel the projectile leaves in its target.
var/p_x = 16
var/p_y = 16 // the pixel location of the tile that the player clicked. Default is the center
//For Maim / Maiming.
var/maiming = 0 //Enables special limb dismemberment calculation; used primarily for ranged weapons that can maim, but do not do brute damage.
var/maim_rate = 0 //Factor that the recipiant will be maimed by the projectile (NOT OUT OF 100%.)
var/clean_cut = 0 //Is the delimbning painful and unclean? Probably. Can be a function or proc, if you're doing something odd.
var/maim_type = DROPLIMB_EDGE
/*Does the projectile simply lop/tear the limb off, or does it vaporize it?
Set maim_type to DROPLIMB_EDGE to chop off the limb
set maim_type to DROPLIMB_BURN to vaporize it.
set maim_type to DROPLIMB_BLUNT to gib (Explode/Hamburger) the limb.
*/
//Movement parameters
var/speed = 0.2 //Amount of deciseconds it takes for projectile to travel
var/pixel_speed = 33 //pixels per move - DO NOT FUCK WITH THIS UNLESS YOU ABSOLUTELY KNOW WHAT YOU ARE DOING OR UNEXPECTED THINGS /WILL/ HAPPEN!
var/Angle = 0
var/original_angle = 0 //Angle at firing
var/nondirectional_sprite = FALSE //Set TRUE to prevent projectiles from having their sprites rotated based on firing angle
var/yo = null
var/xo = null
var/atom/original // the target clicked (not necessarily where the projectile is headed). Should probably be renamed to 'target' or something.
var/turf/starting // the projectile's starting turf
var/list/permutated // we've passed through these atoms, don't try to hit them again
var/penetrating = 0 //If greater than zero, the projectile will pass through dense objects as specified by on_penetrate()
var/forcedodge = FALSE //to pass through everything
var/ignore_source_check = FALSE
//Fired processing vars
var/fired = FALSE //Have we been fired yet
var/paused = FALSE //for suspending the projectile midair
var/reflected = FALSE
var/last_projectile_move = 0
var/last_process = 0
var/time_offset = 0
var/datum/point/vector/trajectory
var/trajectory_ignore_forcemove = FALSE //instructs forceMove to NOT reset our trajectory to the new location!
var/range = 50 //This will de-increment every step. When 0, it will deletze the projectile.
var/aoe = 0 //For KAs, really
//Hitscan
var/hitscan = FALSE //Whether this is hitscan. If it is, speed is basically ignored.
var/list/beam_segments //assoc list of datum/point or datum/point/vector, start = end. Used for hitscan effect generation.
var/datum/point/beam_index
var/turf/hitscan_last //last turf touched during hitscanning.
var/tracer_type
var/muzzle_type
var/impact_type
var/hit_effect
var/anti_materiel_potential = 1 //how much the damage of this bullet is increased against mechs
var/iff // identify friend or foe. will check mob's IDs to see if they match, if they do, won't hit
/obj/item/projectile/CanPass()
return TRUE
//TODO: make it so this is called more reliably, instead of sometimes by bullet_act() and sometimes not
/obj/item/projectile/proc/on_hit(var/atom/target, var/blocked = 0, var/def_zone = null)
if(blocked >= 100) //Full block
return FALSE
if(!isliving(target))
return FALSE
if(isanimal(target))
return FALSE
var/mob/living/L = target
if(damage_type == DAMAGE_BRUTE && damage > 5) //weak hits shouldn't make you gush blood
var/splatter_color = "#A10808"
var/mob/living/carbon/human/H = target
if (istype(H) && H.species && H.species.blood_color)
splatter_color = H.species.blood_color
var/splatter_dir = starting ? get_dir(starting, target.loc) : dir
new /obj/effect/temp_visual/dir_setting/bloodsplatter(target.loc, splatter_dir, splatter_color)
if(hit_effect)
new hit_effect(target.loc)
L.apply_effects(0, weaken, paralyze, 0, stutter, eyeblur, drowsy, 0, incinerate, blocked)
L.stun_effect_act(stun, agony, def_zone, src, damage_flags)
L.apply_damage(irradiate, DAMAGE_RADIATION, damage_flags = DAMAGE_FLAG_DISPERSED) //radiation protection is handled separately from other armor types.
return 1
//called when the projectile stops flying because it collided with something
/obj/item/projectile/proc/on_impact(var/atom/A, var/affected_limb)
return
//Checks if the projectile is eligible for embedding. Not that it necessarily will.
/obj/item/projectile/proc/can_embed()
//embed must be enabled and damage type must be brute
if(!embed || damage_type != DAMAGE_BRUTE)
return FALSE
return TRUE
/obj/item/projectile/proc/do_embed(var/obj/item/organ/external/organ)
var/obj/item/SP = new shrapnel_type(organ)
SP.edge = TRUE
SP.sharp = TRUE
SP.name = (name != "shrapnel") ? "[initial(name)] shrapnel" : "shrapnel"
SP.desc += " It looks like it was fired from [shot_from]."
SP.forceMove(organ)
organ.embed(SP)
return SP
/obj/item/projectile/proc/get_structure_damage()
if(damage_type == DAMAGE_BRUTE || damage_type == DAMAGE_BURN)
return damage * anti_materiel_potential
return FALSE
//return TRUE if the projectile should be allowed to pass through after all, FALSE if not.
/obj/item/projectile/proc/check_penetrate(atom/A)
return TRUE
/obj/item/projectile/proc/launch_projectile(atom/target, target_zone, mob/user, params, angle_override, forced_spread = 0)
original = target
def_zone = check_zone(target_zone)
firer = user
var/direct_target
if(get_turf(target) == get_turf(src))
direct_target = target
preparePixelProjectile(target, user? user : get_turf(src), params, forced_spread)
return fire(angle_override, direct_target)
//called to launch a projectile from a gun
/obj/item/projectile/proc/launch_from_gun(atom/target, target_zone, mob/user, params, angle_override, forced_spread, obj/item/gun/launcher)
shot_from = launcher.name
suppressed = launcher.suppressed
if(launcher.iff_capable && user)
iff = get_iff_from_user(user)
return launch_projectile(target, target_zone, user, params, angle_override, forced_spread)
/obj/item/projectile/proc/get_iff_from_user(var/mob/user)
var/obj/item/card/id/ID = user.GetIdCard()
if(ID)
return ID.iff_faction
return null
//Called when the projectile intercepts a mob. Returns 1 if the projectile hit the mob, 0 if it missed and should keep flying.
/obj/item/projectile/proc/attack_mob(var/mob/living/target_mob, var/distance, var/miss_modifier=0)
if(!istype(target_mob))
return
//roll to-hit
miss_modifier = max(15*(distance-1) - round(25*accuracy) + miss_modifier, 0)
hit_zone = get_zone_with_miss_chance(def_zone, target_mob, miss_modifier, (distance > 1 || original != target_mob), point_blank) //if the projectile hits a target we weren't originally aiming at then retain the chance to miss
var/result = PROJECTILE_FORCE_MISS
if(hit_zone)
def_zone = hit_zone //set def_zone, so if the projectile ends up hitting someone else later (to be implemented), it is more likely to hit the same part
if(!target_mob.aura_check(AURA_TYPE_BULLET, src, def_zone))
return TRUE
result = target_mob.bullet_act(src, def_zone)
switch(result)
if(PROJECTILE_FORCE_MISS)
if(!point_blank)
if(!suppressed)
target_mob.visible_message("<span class='notice'>\The [src] misses [target_mob] narrowly!</span>")
playsound(target_mob, /singleton/sound_category/bulletflyby_sound, 50, 1)
return FALSE
if(PROJECTILE_DODGED)
return FALSE
if(PROJECTILE_STOPPED)
return TRUE
var/impacted_organ = target_mob.get_organ_name_from_zone(def_zone)
//hit messages
if(suppressed)
to_chat(target_mob, "<span class='danger'>You've been hit in the [impacted_organ] by \a [src]!</span>")
else
target_mob.visible_message("<span class='danger'>\The [target_mob] is hit by \a [src] in the [impacted_organ]!</span>", "<span class='danger'><font size=2>You are hit by \a [src] in the [impacted_organ]!</font></span>")//X has fired Y is now given by the guns so you cant tell who shot you if you could not see the shooter
var/no_clients = FALSE
//admin logs
if((!ismob(firer) || !firer.client) && !target_mob.client)
no_clients = TRUE
if(istype(target_mob, /mob/living/heavy_vehicle))
var/mob/living/heavy_vehicle/HV = target_mob
for(var/pilot in HV.pilots)
var/mob/M = pilot
if(M.client)
no_clients = FALSE
break
if(no_clients)
no_attack_log = TRUE
if(!no_attack_log)
if(ismob(firer))
var/attacker_message = "shot with \a [src.type]"
var/victim_message = "shot with \a [src.type]"
var/admin_message = "shot (\a [src.type])"
admin_attack_log(firer, target_mob, attacker_message, victim_message, admin_message)
else
target_mob.attack_log += "\[[time_stamp()]\] <b>UNKNOWN SUBJECT (No longer exists)</b> shot <b>[target_mob]/[target_mob.ckey]</b> with <b>\a [src]</b>"
msg_admin_attack("UNKNOWN shot [target_mob] ([target_mob.ckey]) with \a [src] (<A HREF='?_src_=holder;adminplayerobservecoodjump=1;X=[target_mob.x];Y=[target_mob.y];Z=[target_mob.z]'>JMP</a>)",ckey=key_name(target_mob))
//sometimes bullet_act() will want the projectile to continue flying
if (result == PROJECTILE_CONTINUE)
return FALSE
return TRUE
/obj/item/projectile/Collide(atom/A)
. = ..()
if(A == src)
return FALSE //no.
if(A in permutated)
return FALSE
if(firer && !ignore_source_check)
if(A == firer || (A == firer.loc)) //cannot shoot yourself or your mech
trajectory_ignore_forcemove = TRUE
forceMove(get_turf(A))
trajectory_ignore_forcemove = FALSE
return FALSE
var/distance = get_dist(get_turf(A), starting) // Get the distance between the turf shot from and the mob we hit and use that for the calculations.
var/passthrough = FALSE //if the projectile should continue flying
if(ismob(A))
var/mob/M = A
if(isliving(A)) //so ghosts don't stop bullets
if(check_iff(M))
passthrough = TRUE
else
if(M.dir & get_dir(M, starting)) // only check neckgrab if they're facing in the direction the bullets came from
//if they have a neck grab on someone, that person gets hit instead
for(var/obj/item/grab/G in list(M.l_hand, M.r_hand))
if(!G.affecting.lying && G.state >= GRAB_NECK)
visible_message(SPAN_DANGER("\The [M] uses [G.affecting] as a shield!"))
if(Collide(G.affecting))
return //If Collide() returns 0 (keep going) then we continue on to attack M.
passthrough = !attack_mob(M, distance)
else
passthrough = TRUE
else
passthrough = (A.bullet_act(src, def_zone) == PROJECTILE_CONTINUE) //backwards compatibility
if(isturf(A))
for(var/obj/O in A)
O.bullet_act(src)
for(var/mob/living/M in A)
attack_mob(M, distance)
//penetrating projectiles can pass through things that otherwise would not let them
if(!passthrough && penetrating > 0)
if(check_penetrate(A))
passthrough = TRUE
penetrating--
//the bullet passes through a dense object!
if(passthrough || forcedodge)
//move ourselves onto A so we can continue on our way.
if(A)
trajectory_ignore_forcemove = TRUE
if(istype(A, /turf))
forceMove(A)
else
forceMove(get_turf(A))
trajectory_ignore_forcemove = FALSE
permutated.Add(A)
return FALSE
//stop flying
on_impact(A, hit_zone)
qdel(src)
return TRUE
/obj/item/projectile/proc/check_iff(var/mob/M)
if(isnull(iff))
return FALSE
var/obj/item/card/id/ID = M.GetIdCard()
if(ID && (ID.iff_faction == iff))
return TRUE
return FALSE
/obj/item/projectile/ex_act(var/severity = 2.0)
return //explosions probably shouldn't delete projectiles
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
/obj/item/projectile/proc/old_style_target(atom/target, atom/source)
if(!source)
source = get_turf(src)
starting = get_turf(source)
original = target
setAngle(get_projectile_angle(source, target))
/obj/item/projectile/proc/fire(angle, atom/direct_target)
//If no angle needs to resolve it from xo/yo!
if(direct_target)
direct_target.bullet_act(src, def_zone)
on_impact(direct_target, def_zone)
qdel(src)
return
if(isnum(angle))
setAngle(angle)
// trajectory dispersion
var/turf/starting = get_turf(src)
if(!starting)
return
if(isnull(Angle)) //Try to resolve through offsets if there's no angle set.
if(isnull(xo) || isnull(yo))
crash_with("WARNING: Projectile [type] deleted due to being unable to resolve a target after angle was null!")
qdel(src)
return
var/turf/target = locate(Clamp(starting + xo, 1, world.maxx), Clamp(starting + yo, 1, world.maxy), starting.z)
setAngle(get_projectile_angle(src, target))
if(dispersion)
setAngle(Angle + rand(-dispersion, dispersion))
original_angle = Angle
if(!nondirectional_sprite)
var/matrix/M = new
M.Turn(Angle)
transform = M
forceMove(starting)
trajectory = new(starting.x, starting.y, starting.z, 0, 0, Angle, pixel_speed)
last_projectile_move = world.time
fired = TRUE
if(hitscan)
return process_hitscan()
else
generate_muzzle_flash()
if(!isprocessing)
START_PROCESSING(SSprojectiles, src)
pixel_move(1) //move it now!
/obj/item/projectile/proc/preparePixelProjectile(atom/target, atom/source, params, angle_offset = 0)
var/turf/curloc = get_turf(source)
var/turf/targloc = get_turf(target)
forceMove(get_turf(source))
starting = get_turf(source)
original = target
var/list/calculated = list(null,null,null)
if(isliving(source) && params)
calculated = calculate_projectile_angle_and_pixel_offsets(source, params)
p_x = calculated[2]
p_y = calculated[3]
setAngle(calculated[1])
else if(targloc && curloc)
yo = targloc.y - curloc.y
xo = targloc.x - curloc.x
setAngle(get_projectile_angle(src, targloc))
else
crash_with("WARNING: Projectile [type] fired without either mouse parameters, or a target atom to aim at!")
qdel(src)
if(angle_offset)
setAngle(Angle + angle_offset)
/obj/item/projectile/proc/before_move()
return
/obj/item/projectile/proc/after_move()
return
/obj/item/projectile/Crossed(atom/movable/AM) //A mob moving on a tile with a projectile is hit by it.
..()
if(isliving(AM) && (AM.density || AM == original) && !(pass_flags & PASSMOB))
Collide(AM)
/obj/item/projectile/Initialize()
. = ..()
permutated = list()
/obj/item/projectile/damage_flags()
return damage_flags
/obj/item/projectile/proc/pixel_move(moves, trajectory_multiplier = 1, hitscanning = FALSE)
if(!loc || !trajectory)
if(!QDELETED(src))
if(loc)
on_impact(loc)
qdel(src)
return
if (QDELETED(src))
return
last_projectile_move = world.time
if(!nondirectional_sprite && !hitscanning)
var/matrix/M = new
M.Turn(Angle)
transform = M
trajectory.increment(trajectory_multiplier)
var/turf/T = trajectory.return_turf()
if (!T) // Nowhere to go. Just die.
qdel(src)
return
if(T.z != loc.z)
before_move()
before_z_change(loc, T)
trajectory_ignore_forcemove = TRUE
forceMove(T)
trajectory_ignore_forcemove = FALSE
after_move()
if(!hitscanning)
pixel_x = trajectory.return_px()
pixel_y = trajectory.return_py()
else
if(T != loc)
before_move()
Move(T)
after_move()
if(!hitscanning)
pixel_x = trajectory.return_px() - trajectory.mpx * trajectory_multiplier
pixel_y = trajectory.return_py() - trajectory.mpy * trajectory_multiplier
if(!hitscanning)
animate(src, pixel_x = trajectory.return_px(), pixel_y = trajectory.return_py(), time = 1, flags = ANIMATION_END_NOW)
if(isturf(loc))
hitscan_last = loc
if(can_hit_target(original, permutated))
Collide(original, TRUE)
Range()
//Returns true if the target atom is on our current turf and above the right layer
/obj/item/projectile/proc/can_hit_target(atom/target, var/list/passthrough)
return (target && ((target.layer >= TURF_LAYER + 0.3) || ismob(target)) && (loc == get_turf(target)) && (!(target in passthrough)))
/proc/calculate_projectile_angle_and_pixel_offsets(mob/user, params)
var/list/mouse_control = params2list(params)
var/p_x = 0
var/p_y = 0
var/angle = 0
if(mouse_control["icon-x"])
p_x = text2num(mouse_control["icon-x"])
if(mouse_control["icon-y"])
p_y = text2num(mouse_control["icon-y"])
if(mouse_control["screen-loc"])
//Split screen-loc up into X+Pixel_X and Y+Pixel_Y
var/list/screen_loc_params = splittext(mouse_control["screen-loc"], ",")
//Split X+Pixel_X up into list(X, Pixel_X)
var/list/screen_loc_X = splittext(screen_loc_params[1],":")
//Split Y+Pixel_Y up into list(Y, Pixel_Y)
var/list/screen_loc_Y = splittext(screen_loc_params[2],":")
var/x = text2num(screen_loc_X[1]) * 32 + text2num(screen_loc_X[2]) - 32
var/y = text2num(screen_loc_Y[1]) * 32 + text2num(screen_loc_Y[2]) - 32
//Calculate the "resolution" of screen based on client's view and world's icon size. This will work if the user can view more tiles than average.
if(istype(user, /mob/living/heavy_vehicle))
var/mob/living/heavy_vehicle/H = user
user = pick(H.pilots) //since i assume this is a list, we want only 1 person
var/list/screenview = getviewsize(user.client.view)
var/screenviewX = screenview[1] * world.icon_size
var/screenviewY = screenview[2] * world.icon_size
var/ox = round(screenviewX/2) - user.client.pixel_x //"origin" x
var/oy = round(screenviewY/2) - user.client.pixel_y //"origin" y
angle = Atan2(y - oy, x - ox)
return list(angle, p_x, p_y)
/obj/item/projectile/proc/Range()
range--
if(range <= 0 && loc)
on_range()
/obj/item/projectile/proc/on_range() //if we want there to be effects when they reach the end of their range
on_impact(loc)
qdel(src)
/obj/item/projectile/proc/store_hitscan_collision(datum/point/pcache)
beam_segments[beam_index] = pcache
beam_index = pcache
beam_segments[beam_index] = null
/obj/item/projectile/proc/return_predicted_turf_after_moves(moves, forced_angle) //I say predicted because there's no telling that the projectile won't change direction/location in flight.
if(!trajectory && isnull(forced_angle) && isnull(Angle))
return FALSE
var/datum/point/vector/current = trajectory
if(!current)
var/turf/T = get_turf(src)
current = new(T.x, T.y, T.z, pixel_x, pixel_y, isnull(forced_angle)? Angle : forced_angle, pixel_speed)
var/datum/point/vector/v = current.return_vector_after_increments(moves)
return v.return_turf()
/obj/item/projectile/proc/return_pathing_turfs_in_moves(moves, forced_angle)
var/turf/current = get_turf(src)
var/turf/ending = return_predicted_turf_after_moves(moves, forced_angle)
return getline(current, ending)
/obj/item/projectile/proc/process_hitscan()
var/safety = range * 3
var/return_vector = RETURN_POINT_VECTOR_INCREMENT(src, Angle, MUZZLE_EFFECT_PIXEL_INCREMENT, 1)
record_hitscan_start(return_vector)
while(loc && !QDELETED(src))
if(paused)
stoplag(1)
continue
if(safety-- <= 0)
qdel(src)
crash_with("WARNING: [type] projectile encountered infinite recursion during hitscanning in [__FILE__]/[__LINE__]!")
return //Kill!
pixel_move(1, 1, TRUE)
/obj/item/projectile/proc/record_hitscan_start(datum/point/pcache)
beam_segments = list() //initialize segment list with the list for the first segment
beam_index = pcache
beam_segments[beam_index] = null //record start.
/obj/item/projectile/proc/vol_by_damage()
if(src.damage)
return Clamp((src.damage) * 0.67, 30, 100)// Multiply projectile damage by 0.67, then CLAMP the value between 30 and 100
else
return 50 //if the projectile doesn't do damage, play its hitsound at 50% volume.
/obj/item/projectile/proc/before_z_change(turf/oldloc, turf/newloc)
var/datum/point/pcache = trajectory.copy_to()
if(hitscan)
store_hitscan_collision(pcache)
/obj/item/projectile/process()
last_process = world.time
if(!loc || !fired || !trajectory)
fired = FALSE
return PROCESS_KILL
if(paused || !isturf(loc))
last_projectile_move += world.time - last_process //Compensates for pausing, so it doesn't become a hitscan projectile when unpaused from charged up ticks.
return
var/elapsed_time_deciseconds = (world.time - last_projectile_move) + time_offset
time_offset = 0
var/required_moves = 0
if(speed > 0)
required_moves = Floor(elapsed_time_deciseconds / speed, 1)
if(required_moves > SSprojectiles.global_max_tick_moves)
var/overrun = required_moves - SSprojectiles.global_max_tick_moves
required_moves = SSprojectiles.global_max_tick_moves
time_offset += overrun * speed
time_offset += Modulus(elapsed_time_deciseconds, speed)
else
required_moves = SSprojectiles.global_max_tick_moves
if(!required_moves)
return
for(var/i = 1; i <= required_moves && !QDELETED(src); i++)
pixel_move(required_moves)
/obj/item/projectile/proc/setAngle(new_angle) //wrapper for overrides.
Angle = new_angle
if(!nondirectional_sprite)
var/matrix/M = new
M.Turn(Angle)
transform = M
if(trajectory)
trajectory.set_angle(new_angle)
return TRUE
/obj/item/projectile/proc/redirect(x, y, starting, source)
old_style_target(locate(x, y, z), starting? get_turf(starting) : get_turf(source))
/obj/item/projectile/forceMove(atom/target)
. = ..()
if(trajectory && !trajectory_ignore_forcemove && isturf(target))
trajectory.initialize_location(target.x, target.y, target.z, 0, 0)
/obj/item/projectile/Destroy()
if(hitscan)
if(loc && trajectory)
var/datum/point/pcache = trajectory.copy_to()
beam_segments[beam_index] = pcache
generate_hitscan_tracers()
STOP_PROCESSING(SSprojectiles, src)
return ..()
/obj/item/projectile/proc/generate_muzzle_flash(duration = 3)
if(duration <= 0)
return
if(!muzzle_type || suppressed)
return
var/datum/point/p = trajectory
var/atom/movable/thing = new muzzle_type
p.move_atom_to_src(thing)
var/matrix/M = new
M.Turn(original_angle)
thing.transform = M
QDEL_IN(thing, duration)
/obj/item/projectile/proc/generate_hitscan_tracers(cleanup = TRUE, duration = 3)
if(!length(beam_segments))
return
if(duration <= 0)
return
if(tracer_type)
for(var/datum/point/p in beam_segments)
generate_tracer_between_points(p, beam_segments[p], tracer_type, color, duration)
if(muzzle_type && !suppressed)
var/datum/point/p = beam_segments[1]
var/atom/movable/thing = new muzzle_type
p.move_atom_to_src(thing)
var/matrix/M = new
M.Turn(original_angle)
thing.transform = M
QDEL_IN(thing, duration)
if(impact_type)
var/datum/point/p = beam_segments[beam_segments[beam_segments.len]]
var/atom/movable/thing = new impact_type
p.move_atom_to_src(thing)
var/matrix/M = new
M.Turn(Angle)
thing.transform = M
QDEL_IN(thing, duration)
if(cleanup)
for(var/i in beam_segments)
qdel(i)
beam_segments = null
QDEL_NULL(beam_index)
/obj/item/projectile/get_print_info()
. = "<br>"
. += "Damage: [initial(damage)]<br>"
. += "Damage Type: [initial(damage_type)]<br>"
. += "Blocked by Armor Type: [initial(check_armor)]<br>"
. += "Stuns: [initial(stun) ? "true" : "false"]<br>"
if(initial(shrapnel_type))
var/obj/shrapnel = new shrapnel_type
. += "Shrapnel Type: [shrapnel.name]<br>"
. += "Armor Penetration: [initial(armor_penetration)]%<br>"
//This is where the bullet bounces off.
/atom/proc/bullet_ping(obj/item/projectile/P, var/pixel_x_offset, var/pixel_y_offset)
if(!P || !P.ping_effect)
return
var/image/I = image('icons/obj/projectiles.dmi', src,P.ping_effect,10, pixel_x = pixel_x_offset, pixel_y = pixel_y_offset)
var/angle = (P.firer && prob(60)) ? round(Get_Angle(P.firer,src)) : round(rand(1,359))
I.pixel_x += rand(-6,6)
I.pixel_y += rand(-6,6)
var/matrix/rotate = matrix()
rotate.Turn(angle)
I.transform = rotate
// Need to do this in order to prevent the ping from being deleted
addtimer(CALLBACK(I, TYPE_PROC_REF(/image, flick_overlay), src, 3), 1)
/image/proc/flick_overlay(var/atom/A, var/duration)
A.overlays.Add(src)
addtimer(CALLBACK(src, PROC_REF(flick_remove_overlay), A), duration)
/image/proc/flick_remove_overlay(var/atom/A)
if(A)
A.overlays.Remove(src)
#undef MUZZLE_EFFECT_PIXEL_INCREMENT