Files
SmArtKar d7ed479374 Improves hitscan projectile chunking (#89616)
## About The Pull Request

Hitscan projectiles that run out of dedicated tick time before they hit
anything abort their movement, ensuring that firing an emitter beam into
space won't cause horrible lag. However, most hitscans also have icons
and have visible (albeit unanimated) movement in such a case, making it
look like projectile code is exploding as tracers appear only after a
rather visible and tangible projectile hits its target.

This PR resolves the issue by making hitscans "chunk" their trails in
such cases, ensuring that they always look like actual hitscans. Video
below has an artificial speed cap on hitscans, to showcase how it'd look
during extreme lag.


https://github.com/user-attachments/assets/eeac034d-d08e-45b0-b7d2-8589376f1c7d

Also some minor hitscan code improvements because I can.

## Changelog
🆑
fix: Hitscan projectiles like emitter beams should look less weird
during extreme lag spikes
/🆑
2025-03-12 16:46:40 -04:00

213 lines
7.6 KiB
Plaintext

// Honker
/obj/projectile/bullet/honker
name = "banana"
damage = 0
movement_type = FLYING
projectile_piercing = ALL
hitsound = 'sound/items/bikehorn.ogg'
icon = 'icons/obj/service/hydroponics/harvest.dmi'
icon_state = "banana"
range = 200
embed_type = null
shrapnel_type = null
/obj/projectile/bullet/honker/Initialize(mapload)
. = ..()
SpinAnimation()
/obj/projectile/bullet/honker/on_hit(atom/target, blocked = 0, pierce_hit)
. = ..()
var/mob/M = target
if(istype(M))
if(M.can_block_magic())
return BULLET_ACT_BLOCK
else
M.slip(100, M.loc, GALOSHES_DONT_HELP|SLIDE)
// Mime
/obj/projectile/bullet/mime
damage = 40
/obj/projectile/bullet/mime/on_hit(atom/target, blocked = 0, pierce_hit)
. = ..()
if(!isliving(target))
return
var/mob/living/living_target = target
living_target.set_silence_if_lower(20 SECONDS)
// Marksman Revolver + Ricochet Coin
/// Marksman Shot
/obj/projectile/bullet/marksman
name = "marksman nanoshot"
icon_state = null
hitscan = TRUE
damage = 30
tracer_type = /obj/effect/projectile/tracer/solar
muzzle_type = /obj/effect/projectile/muzzle/bullet
impact_type = /obj/effect/projectile/impact/sniper
/// How many ricochets deep this is, for tracer size
var/ricoshot_level = 0
/obj/projectile/bullet/marksman/Initialize(mapload, obj/item/ammo_casing/casing, incoming_ricoshot_level)
. = ..()
if(isnum(incoming_ricoshot_level))
ricoshot_level = incoming_ricoshot_level
switch(ricoshot_level)
if(0)
tracer_type = /obj/effect/projectile/tracer/solar/thinnest
if(1)
tracer_type = /obj/effect/projectile/tracer/solar/thin
if(2 to INFINITY)
tracer_type = /obj/effect/projectile/tracer/solar
/obj/projectile/bullet/marksman/scan_moved_turf()
var/turf/cur_turf = get_turf(src) // check to see if we're passing over a turf with a coin on it
var/obj/projectile/bullet/coin/coin_check = cur_turf ? locate(/obj/projectile/bullet/coin) in cur_turf.contents : null
if(!coin_check || (ricoshot_level == 0 && get_dist(coin_check.target_turf, coin_check) >= 1) || coin_check.used) // no coin, keep trucking
return ..()
coin_check.check_splitshot(firer, src)
impact(coin_check)
/// Marksman Coin
/obj/projectile/bullet/coin
name = "marksman coin"
icon_state = "coinshot"
speed = 0.33
damage = 5
color = "#dbdd4c"
/// Save the turf we're aiming for for future use
var/turf/target_turf
/// Coins are valid while within a tile of their target tile, and can only be directly ricoshot during this time.
var/valid = FALSE
/// When a coin has been activated, is is marked as used, so that it is taken out of consideration for any further ricochets
var/used = FALSE
/// When this coin is targeted with a valid splitshot, it creates this many child splitshots
var/num_of_splitshots = 2
/// The crosshair icon put on the targeted turf for the user- so we can remove it from their images when done
var/image/crosshair_indicator
/// The mob who originally flipped this coin, as a matter of convenience, may be able tto be removed
var/mob/original_firer
/obj/projectile/bullet/coin/Initialize(mapload, turf/the_target, mob/original_firer)
src.original_firer = original_firer
target_turf = the_target
range = (get_dist(original_firer, target_turf) + 3) * 3 // 3 tiles past the origin (the *3 is because reduce_range() ticks 3 times a tile because of the slower speed)
. = ..()
if(!istype(original_firer) || !original_firer.client)
return
var/client/firing_client = original_firer.client
crosshair_indicator = image('icons/obj/supplypods_32x32.dmi', target_turf, "LZ")
firing_client.images += crosshair_indicator
/obj/projectile/bullet/coin/Destroy()
remove_crosshair_indicator()
return ..()
// the coin must be on the target turf to be directly targetable
/obj/projectile/bullet/coin/Moved(atom/old_loc, movement_dir, forced, list/old_locs, momentum_change)
. = ..()
if(!valid && get_dist(loc, target_turf) < 1)
original_firer?.playsound_local(src, 'sound/machines/ping.ogg', 30)
valid = TRUE
else if(valid && get_dist(loc, target_turf) > 1)
valid = FALSE
remove_crosshair_indicator()
/// Remove the crosshair indicator from the original firer if it exists
/obj/projectile/bullet/coin/proc/remove_crosshair_indicator()
if(original_firer?.client && crosshair_indicator)
original_firer.client.images -= crosshair_indicator
QDEL_NULL(crosshair_indicator)
/// We've been shot by a marksman revolver shot, or the ricochet off another coin, check if we can actually ricochet. The forced var being TRUE means it's a ricochet from another coin
/obj/projectile/bullet/coin/proc/check_splitshot(mob/living/shooter, obj/projectile/bullet/marksman/incoming_shot, forced = FALSE)
if(!forced && get_dist(src, target_turf) > 1)
return FALSE
used = TRUE
var/turf/cur_tur = get_turf(src)
cur_tur.visible_message(span_nicegreen("[incoming_shot] impacts [src] and splits!"))
iterate_splitshots(shooter, incoming_shot)
QDEL_IN(src, 0.25 SECONDS) // may not be needed
/// Now we actually create all the splitshots, loop through however many splits we'll create and fire them
/obj/projectile/bullet/coin/proc/iterate_splitshots(mob/living/shooter, obj/projectile/incoming_shot)
for(var/i in 1 to num_of_splitshots)
fire_splitshot(incoming_shot)
/// Shoot an individual splitshot at a new target
/obj/projectile/bullet/coin/proc/fire_splitshot(obj/projectile/bullet/marksman/incoming_shot)
var/atom/next_target = find_next_target()
ADD_TRAIT(next_target, TRAIT_RECENTLY_COINED, "[type]")
addtimer(TRAIT_CALLBACK_REMOVE(next_target, TRAIT_RECENTLY_COINED, "[type]"), 0.5 SECONDS)
var/outgoing_ricoshot_level = incoming_shot.ricoshot_level + 1
var/obj/projectile/bullet/marksman/new_splitshot = new /obj/projectile/bullet/marksman(get_turf(src), null, outgoing_ricoshot_level)
//Shooting Code:
new_splitshot.original = next_target
new_splitshot.fired_from = incoming_shot.fired_from
new_splitshot.firer = incoming_shot.firer
new_splitshot.damage = 2 * incoming_shot.damage
var/current_turf = get_turf(src)
var/target_turf = get_turf(next_target)
if(Adjacent(current_turf, target_turf))
new_splitshot.fire(get_angle(current_turf, target_turf), direct_target = next_target)
else
new_splitshot.aim_projectile(next_target, get_turf(src))
new_splitshot.fire()
if(istype(next_target, /obj/projectile/bullet/coin)) // handle further splitshot checks
var/obj/projectile/bullet/coin/our_coin = next_target
our_coin.check_splitshot(incoming_shot.firer, new_splitshot, forced = TRUE)
/// Find what the splitshots will want to target next, with the order roughly based off the UK coin
/obj/projectile/bullet/coin/proc/find_next_target()
var/list/valid_targets = shuffle(oview(4, loc))
valid_targets -= firer
for(var/obj/projectile/bullet/coin/iter_coin in valid_targets)
if(!iter_coin.used) // this will prevent shooting itself as well
return iter_coin
var/list/possible_victims = list()
for(var/mob/living/iter_living in valid_targets)
if(HAS_TRAIT(iter_living, TRAIT_RECENTLY_COINED) || iter_living.stat != CONSCIOUS)
continue
if(get_dist(src, iter_living) <= 2) // prioritize close mobs
return iter_living
possible_victims += iter_living
if(possible_victims.len)
return pick(possible_victims)
var/static/list/prioritized_targets = list(/obj/structure/reagent_dispensers, /obj/item/grenade, /obj/structure/window)
for(var/iter_type in prioritized_targets)
for(var/already_coined_tries in 1 to 3)
var/atom/iter_type_check = locate(iter_type) in valid_targets
if(iter_type_check)
if(HAS_TRAIT(iter_type_check, TRAIT_RECENTLY_COINED))
valid_targets -= iter_type_check
continue
else
return iter_type_check
for(var/atom/last_ditch in valid_targets)
if(!HAS_TRAIT(last_ditch, TRAIT_RECENTLY_COINED))
return last_ditch