mirror of
https://github.com/Bubberstation/Bubberstation.git
synced 2026-01-30 02:52:30 +00:00
Made a few changes to gunpoints and some related mechanics while tidying up and adding docs. Here's a quick list -Execution (point blank help intent shot aimed at the mouth) and gunpoint shots now apply their bonuses to every pellet fired rather than only the first, generally making them gushier. They also buff wound power as well as damage -You can no longer punch yourself while holding someone up to trigger the charged shot -You can no longer purposely fail executions and gunpoints with pax or whatever to endlessly spike the damage in a loaded round -Attacks with extremely high wounding power can now outright dismember limbs regardless of mangled status. The threshold is high enough that it mostly applies for admin edited weapons or execution shots with shotguns (or people with the frail quirk!) -Piercing wounds make further wounds a bit easier to apply to give them a bit more power -Hellguns now cost 2250 credits instead of 2000 to make them a bit harder for random crew to get -Adds special bouncy L6 rounds for admins to use to bounce off anything and everything en-masse Also as a minor note Projectiles with no trajectory (meaning they were likely spawned in manually) now qdel themselves on bumping something, rather than runtiming helplessly
784 lines
30 KiB
Plaintext
784 lines
30 KiB
Plaintext
|
|
#define MOVES_HITSCAN -1 //Not actually hitscan but close as we get without actual hitscan.
|
|
#define MUZZLE_EFFECT_PIXEL_INCREMENT 17 //How many pixels to move the muzzle flash up so your character doesn't look like they're shitting out lasers.
|
|
|
|
/obj/projectile
|
|
name = "projectile"
|
|
icon = 'icons/obj/projectiles.dmi'
|
|
icon_state = "bullet"
|
|
density = FALSE
|
|
anchored = TRUE
|
|
pass_flags = PASSTABLE
|
|
mouse_opacity = MOUSE_OPACITY_TRANSPARENT
|
|
movement_type = FLYING
|
|
wound_bonus = CANT_WOUND // can't wound by default
|
|
//The sound this plays on impact.
|
|
var/hitsound = 'sound/weapons/pierce.ogg'
|
|
var/hitsound_wall = ""
|
|
|
|
resistance_flags = LAVA_PROOF | FIRE_PROOF | UNACIDABLE | ACID_PROOF
|
|
var/def_zone = "" //Aiming at
|
|
var/atom/movable/firer = null//Who shot it
|
|
var/atom/fired_from = null // the atom that the projectile was fired from (gun, turret)
|
|
var/suppressed = FALSE //Attack message
|
|
var/yo = null
|
|
var/xo = null
|
|
var/atom/original = null // the original target clicked
|
|
var/turf/starting = null // the projectile's starting turf
|
|
var/list/permutated = list() // we've passed through these atoms, don't try to hit them again
|
|
var/p_x = 16
|
|
var/p_y = 16 // the pixel location of the tile that the player clicked. Default is the center
|
|
|
|
//Fired processing vars
|
|
var/fired = FALSE //Have we been fired yet
|
|
var/paused = FALSE //for suspending the projectile midair
|
|
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/speed = 0.8 //Amount of deciseconds it takes for projectile to travel
|
|
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/spread = 0 //amount (in degrees) of projectile spread
|
|
animate_movement = NO_STEPS //Use SLIDE_STEPS in conjunction with legacy
|
|
/// how many times we've ricochet'd so far (instance variable, not a stat)
|
|
var/ricochets = 0
|
|
/// how many times we can ricochet max
|
|
var/ricochets_max = 0
|
|
/// 0-100 (or more, I guess), the base chance of ricocheting, before being modified by the atom we shoot and our chance decay
|
|
var/ricochet_chance = 0
|
|
/// 0-1 (or more, I guess) multiplier, the ricochet_chance is modified by multiplying this after each ricochet
|
|
var/ricochet_decay_chance = 0.7
|
|
/// 0-1 (or more, I guess) multiplier, the projectile's damage is modified by multiplying this after each ricochet
|
|
var/ricochet_decay_damage = 0.7
|
|
/// On ricochet, if nonzero, we consider all mobs within this range of our projectile at the time of ricochet to home in on like Revolver Ocelot, as governed by ricochet_auto_aim_angle
|
|
var/ricochet_auto_aim_range = 0
|
|
/// On ricochet, if ricochet_auto_aim_range is nonzero, we'll consider any mobs within this range of the normal angle of incidence to home in on, higher = more auto aim
|
|
var/ricochet_auto_aim_angle = 30
|
|
/// the angle of impact must be within this many degrees of the struck surface, set to 0 to allow any angle
|
|
var/ricochet_incidence_leeway = 40
|
|
|
|
///If the object being hit can pass ths damage on to something else, it should not do it for this bullet
|
|
var/force_hit = FALSE
|
|
|
|
//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
|
|
|
|
//Fancy hitscan lighting effects!
|
|
var/hitscan_light_intensity = 1.5
|
|
var/hitscan_light_range = 0.75
|
|
var/hitscan_light_color_override
|
|
var/muzzle_flash_intensity = 3
|
|
var/muzzle_flash_range = 1.5
|
|
var/muzzle_flash_color_override
|
|
var/impact_light_intensity = 3
|
|
var/impact_light_range = 2
|
|
var/impact_light_color_override
|
|
|
|
//Homing
|
|
var/homing = FALSE
|
|
var/atom/homing_target
|
|
var/homing_turn_speed = 10 //Angle per tick.
|
|
var/homing_inaccuracy_min = 0 //in pixels for these. offsets are set once when setting target.
|
|
var/homing_inaccuracy_max = 0
|
|
var/homing_offset_x = 0
|
|
var/homing_offset_y = 0
|
|
|
|
var/ignore_source_check = FALSE
|
|
|
|
var/damage = 10
|
|
var/damage_type = BRUTE //BRUTE, BURN, TOX, OXY, CLONE are the only things that should be in here
|
|
var/nodamage = FALSE //Determines if the projectile will skip any damage inflictions
|
|
var/flag = BULLET //Defines what armor to use when it hits things. Must be set to bullet, laser, energy,or bomb
|
|
///How much armor this projectile pierces.
|
|
var/armour_penetration = 0
|
|
var/projectile_type = /obj/projectile
|
|
var/range = 50 //This will de-increment every step. When 0, it will deletze the projectile.
|
|
var/decayedRange //stores original range
|
|
var/reflect_range_decrease = 5 //amount of original range that falls off when reflecting, so it doesn't go forever
|
|
var/reflectable = NONE // Can it be reflected or not?
|
|
//Effects
|
|
var/stun = 0
|
|
var/knockdown = 0
|
|
var/paralyze = 0
|
|
var/immobilize = 0
|
|
var/unconscious = 0
|
|
var/irradiate = 0
|
|
var/stutter = 0
|
|
var/slur = 0
|
|
var/eyeblur = 0
|
|
var/drowsy = 0
|
|
var/stamina = 0
|
|
var/jitter = 0
|
|
var/dismemberment = 0 //The higher the number, the greater the bonus to dismembering. 0 will not dismember at all.
|
|
var/impact_effect_type //what type of impact effect to show when hitting something
|
|
var/log_override = FALSE //is this type spammed enough to not log? (KAs)
|
|
|
|
var/temporary_unstoppable_movement = FALSE
|
|
|
|
///If defined, on hit we create an item of this type then call hitby() on the hit target with this, mainly used for embedding items (bullets) in targets
|
|
var/shrapnel_type
|
|
///If we have a shrapnel_type defined, these embedding stats will be passed to the spawned shrapnel type, which will roll for embedding on the target
|
|
var/list/embedding
|
|
|
|
///If TRUE, hit mobs even if they're on the floor and not our target
|
|
var/hit_stunned_targets = FALSE
|
|
|
|
///For what kind of brute wounds we're rolling for, if we're doing such a thing. Lasers obviously don't care since they do burn instead.
|
|
var/sharpness = SHARP_NONE
|
|
///How much we want to drop both wound_bonus and bare_wound_bonus (to a minimum of 0 for the latter) per tile, for falloff purposes
|
|
var/wound_falloff_tile
|
|
///How much we want to drop the embed_chance value, if we can embed, per tile, for falloff purposes
|
|
var/embed_falloff_tile
|
|
|
|
|
|
/obj/projectile/Initialize()
|
|
. = ..()
|
|
permutated = list()
|
|
decayedRange = range
|
|
if(embedding)
|
|
updateEmbedding()
|
|
|
|
/obj/projectile/proc/Range()
|
|
range--
|
|
if(wound_bonus != CANT_WOUND)
|
|
wound_bonus += wound_falloff_tile
|
|
bare_wound_bonus = max(0, bare_wound_bonus + wound_falloff_tile)
|
|
if(embedding)
|
|
embedding["embed_chance"] += embed_falloff_tile
|
|
if(range <= 0 && loc)
|
|
on_range()
|
|
|
|
/obj/projectile/proc/on_range() //if we want there to be effects when they reach the end of their range
|
|
SEND_SIGNAL(src, COMSIG_PROJECTILE_RANGE_OUT)
|
|
qdel(src)
|
|
|
|
//to get the correct limb (if any) for the projectile hit message
|
|
/mob/living/proc/check_limb_hit(hit_zone)
|
|
if(has_limbs)
|
|
return hit_zone
|
|
|
|
/mob/living/carbon/check_limb_hit(hit_zone)
|
|
if(get_bodypart(hit_zone))
|
|
return hit_zone
|
|
else //when a limb is missing the damage is actually passed to the chest
|
|
return BODY_ZONE_CHEST
|
|
|
|
/obj/projectile/proc/prehit(atom/target)
|
|
return TRUE
|
|
|
|
/// Called when the projectile hits something
|
|
/obj/projectile/proc/on_hit(atom/target, blocked = FALSE)
|
|
if(fired_from)
|
|
SEND_SIGNAL(fired_from, COMSIG_PROJECTILE_ON_HIT, firer, target, Angle)
|
|
// i know that this is probably more with wands and gun mods in mind, but it's a bit silly that the projectile on_hit signal doesn't ping the projectile itself.
|
|
// maybe we care what the projectile thinks! See about combining these via args some time when it's not 5AM
|
|
var/obj/item/bodypart/hit_limb
|
|
if(isliving(target))
|
|
var/mob/living/L = target
|
|
hit_limb = L.check_limb_hit(def_zone)
|
|
if(SEND_SIGNAL(src, COMSIG_PROJECTILE_SELF_ON_HIT, firer, target, Angle, hit_limb) & COMPONENT_PROJECTILE_SELF_ON_HIT_SELF_DELETE)
|
|
return BULLET_ACT_HIT
|
|
var/turf/target_loca = get_turf(target)
|
|
|
|
var/hitx
|
|
var/hity
|
|
if(target == original)
|
|
hitx = target.pixel_x + p_x - 16
|
|
hity = target.pixel_y + p_y - 16
|
|
else
|
|
hitx = target.pixel_x + rand(-8, 8)
|
|
hity = target.pixel_y + rand(-8, 8)
|
|
|
|
if(!nodamage && (damage_type == BRUTE || damage_type == BURN) && iswallturf(target_loca) && prob(75))
|
|
var/turf/closed/wall/W = target_loca
|
|
if(impact_effect_type && !hitscan)
|
|
new impact_effect_type(target_loca, hitx, hity)
|
|
|
|
W.add_dent(WALL_DENT_SHOT, hitx, hity)
|
|
|
|
return BULLET_ACT_HIT
|
|
|
|
if(!isliving(target))
|
|
if(impact_effect_type && !hitscan)
|
|
new impact_effect_type(target_loca, hitx, hity)
|
|
return BULLET_ACT_HIT
|
|
|
|
var/mob/living/L = target
|
|
|
|
if(blocked != 100) // not completely blocked
|
|
if(damage && L.blood_volume && damage_type == BRUTE)
|
|
var/splatter_dir = dir
|
|
if(starting)
|
|
splatter_dir = get_dir(starting, target_loca)
|
|
if(isalien(L))
|
|
new /obj/effect/temp_visual/dir_setting/bloodsplatter/xenosplatter(target_loca, splatter_dir)
|
|
else
|
|
new /obj/effect/temp_visual/dir_setting/bloodsplatter(target_loca, splatter_dir)
|
|
if(prob(33))
|
|
L.add_splatter_floor(target_loca)
|
|
else if(impact_effect_type && !hitscan)
|
|
new impact_effect_type(target_loca, hitx, hity)
|
|
|
|
var/organ_hit_text = ""
|
|
var/limb_hit = hit_limb
|
|
if(limb_hit)
|
|
organ_hit_text = " in \the [parse_zone(limb_hit)]"
|
|
if(suppressed==SUPPRESSED_VERY)
|
|
playsound(loc, hitsound, 5, TRUE, -1)
|
|
else if(suppressed)
|
|
playsound(loc, hitsound, 5, TRUE, -1)
|
|
to_chat(L, "<span class='userdanger'>You're shot by \a [src][organ_hit_text]!</span>")
|
|
else
|
|
if(hitsound)
|
|
var/volume = vol_by_damage()
|
|
playsound(loc, hitsound, volume, TRUE, -1)
|
|
L.visible_message("<span class='danger'>[L] is hit by \a [src][organ_hit_text]!</span>", \
|
|
"<span class='userdanger'>You're hit by \a [src][organ_hit_text]!</span>", null, COMBAT_MESSAGE_RANGE)
|
|
L.on_hit(src)
|
|
|
|
var/reagent_note
|
|
if(reagents && reagents.reagent_list)
|
|
reagent_note = " REAGENTS:"
|
|
for(var/datum/reagent/R in reagents.reagent_list)
|
|
reagent_note += "[R.name] ([num2text(R.volume)])"
|
|
|
|
if(ismob(firer))
|
|
log_combat(firer, L, "shot", src, reagent_note)
|
|
else
|
|
L.log_message("has been shot by [firer] with [src]", LOG_ATTACK, color="orange")
|
|
|
|
return BULLET_ACT_HIT
|
|
|
|
/obj/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/projectile/proc/on_ricochet(atom/A)
|
|
if(!ricochet_auto_aim_angle || !ricochet_auto_aim_range)
|
|
return
|
|
|
|
var/mob/living/unlucky_sob
|
|
var/best_angle = ricochet_auto_aim_angle
|
|
if(firer && HAS_TRAIT(firer, TRAIT_NICE_SHOT))
|
|
best_angle += NICE_SHOT_RICOCHET_BONUS
|
|
for(var/mob/living/L in range(ricochet_auto_aim_range, src.loc))
|
|
if(L.stat == DEAD || !isInSight(src, L))
|
|
continue
|
|
var/our_angle = abs(closer_angle_difference(Angle, Get_Angle(src.loc, L.loc)))
|
|
if(our_angle < best_angle)
|
|
best_angle = our_angle
|
|
unlucky_sob = L
|
|
|
|
if(unlucky_sob)
|
|
setAngle(Get_Angle(src, unlucky_sob.loc))
|
|
|
|
/obj/projectile/proc/store_hitscan_collision(datum/point/pcache)
|
|
beam_segments[beam_index] = pcache
|
|
beam_index = pcache
|
|
beam_segments[beam_index] = null
|
|
|
|
/obj/projectile/Bump(atom/A)
|
|
if(!trajectory)
|
|
qdel(src)
|
|
return
|
|
var/datum/point/pcache = trajectory.copy_to()
|
|
var/turf/T = get_turf(A)
|
|
if(ricochets < ricochets_max && check_ricochet_flag(A) && check_ricochet(A))
|
|
ricochets++
|
|
if(A.handle_ricochet(src))
|
|
on_ricochet(A)
|
|
ignore_source_check = TRUE
|
|
decayedRange = max(0, decayedRange - reflect_range_decrease)
|
|
ricochet_chance *= ricochet_decay_chance
|
|
damage *= ricochet_decay_damage
|
|
range = decayedRange
|
|
if(hitscan)
|
|
store_hitscan_collision(pcache)
|
|
return TRUE
|
|
|
|
var/distance = get_dist(T, starting) // Get the distance between the turf shot from and the mob we hit and use that for the calculations.
|
|
def_zone = ran_zone(def_zone, max(100-(7*distance), 5)) //Lower accurancy/longer range tradeoff. 7 is a balanced number to use.
|
|
|
|
if(isturf(A) && hitsound_wall)
|
|
var/volume = clamp(vol_by_damage() + 20, 0, 100)
|
|
if(suppressed)
|
|
volume = 5
|
|
playsound(loc, hitsound_wall, volume, TRUE, -1)
|
|
|
|
return process_hit(T, select_target(T, A))
|
|
|
|
#define QDEL_SELF 1 //Delete if we're not UNSTOPPABLE flagged non-temporarily
|
|
#define DO_NOT_QDEL 2 //Pass through.
|
|
#define FORCE_QDEL 3 //Force deletion.
|
|
|
|
/obj/projectile/proc/process_hit(turf/T, atom/target, qdel_self, hit_something = FALSE) //probably needs to be reworked entirely when pixel movement is done.
|
|
if(QDELETED(src) || !T || !target) //We're done, nothing's left.
|
|
if((qdel_self == FORCE_QDEL) || ((qdel_self == QDEL_SELF) && !temporary_unstoppable_movement && !(movement_type & UNSTOPPABLE)))
|
|
qdel(src)
|
|
return hit_something
|
|
permutated |= target //Make sure we're never hitting it again. If we ever run into weirdness with piercing projectiles needing to hit something multiple times.. well.. that's a to-do.
|
|
if(!prehit(target))
|
|
return process_hit(T, select_target(T), qdel_self, hit_something) //Hit whatever else we can since that didn't work.
|
|
SEND_SIGNAL(target, COMSIG_PROJECTILE_PREHIT, args)
|
|
var/result = target.bullet_act(src, def_zone)
|
|
if(result == BULLET_ACT_FORCE_PIERCE)
|
|
if(!(movement_type & UNSTOPPABLE))
|
|
temporary_unstoppable_movement = TRUE
|
|
movement_type |= UNSTOPPABLE
|
|
return process_hit(T, select_target(T), qdel_self, TRUE) //Hit whatever else we can since we're piercing through but we're still on the same tile.
|
|
else if(result == BULLET_ACT_TURF) //We hit the turf but instead we're going to also hit something else on it.
|
|
return process_hit(T, select_target(T), QDEL_SELF, TRUE)
|
|
else //Whether it hit or blocked, we're done!
|
|
qdel_self = QDEL_SELF
|
|
hit_something = TRUE
|
|
if((qdel_self == FORCE_QDEL) || ((qdel_self == QDEL_SELF) && !temporary_unstoppable_movement && !(movement_type & UNSTOPPABLE)))
|
|
qdel(src)
|
|
return hit_something
|
|
|
|
#undef QDEL_SELF
|
|
#undef DO_NOT_QDEL
|
|
#undef FORCE_QDEL
|
|
|
|
/obj/projectile/proc/select_target(turf/T, atom/target) //Select a target from a turf.
|
|
if((original in T) && can_hit_target(original, permutated, TRUE, TRUE))
|
|
return original
|
|
if(target && can_hit_target(target, permutated, target == original, TRUE))
|
|
return target
|
|
var/list/mob/living/possible_mobs = typecache_filter_list(T, GLOB.typecache_mob)
|
|
var/list/mob/mobs = list()
|
|
for(var/mob/living/M in possible_mobs)
|
|
if(!can_hit_target(M, permutated, M == original, TRUE))
|
|
continue
|
|
mobs += M
|
|
if (length(mobs))
|
|
var/mob/M = pick(mobs)
|
|
return M.lowest_buckled_mob()
|
|
var/list/obj/possible_objs = typecache_filter_list(T, GLOB.typecache_machine_or_structure)
|
|
var/list/obj/objs = list()
|
|
for(var/obj/O in possible_objs)
|
|
if(!can_hit_target(O, permutated, O == original, TRUE))
|
|
continue
|
|
objs += O
|
|
if (length(objs))
|
|
var/obj/O = pick(objs)
|
|
return O
|
|
//Nothing else is here that we can hit, hit the turf if we haven't.
|
|
if(!(T in permutated) && can_hit_target(T, permutated, T == original, TRUE))
|
|
return T
|
|
//Returns null if nothing at all was found.
|
|
|
|
/obj/projectile/proc/check_ricochet(atom/A)
|
|
var/chance = ricochet_chance * A.receive_ricochet_chance_mod
|
|
if(firer && HAS_TRAIT(firer, TRAIT_NICE_SHOT))
|
|
chance += NICE_SHOT_RICOCHET_BONUS
|
|
if(prob(chance))
|
|
return TRUE
|
|
return FALSE
|
|
|
|
/obj/projectile/proc/check_ricochet_flag(atom/A)
|
|
if((flag in list(ENERGY, LASER)) && (A.flags_ricochet & RICOCHET_SHINY))
|
|
return TRUE
|
|
|
|
if((flag in list(BOMB, BULLET)) && (A.flags_ricochet & RICOCHET_HARD))
|
|
return TRUE
|
|
|
|
return FALSE
|
|
|
|
/obj/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, SSprojectiles.global_pixel_speed)
|
|
var/datum/point/vector/v = current.return_vector_after_increments(moves * SSprojectiles.global_iterations_per_move)
|
|
return v.return_turf()
|
|
|
|
/obj/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/projectile/Process_Spacemove(movement_dir = 0)
|
|
return TRUE //Bullets don't drift in space
|
|
|
|
/obj/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 = speed > 0? FLOOR(elapsed_time_deciseconds / speed, 1) : MOVES_HITSCAN //Would be better if a 0 speed made hitscan but everyone hates those so I can't make it a universal system :<
|
|
if(required_moves == MOVES_HITSCAN)
|
|
required_moves = SSprojectiles.global_max_tick_moves
|
|
else
|
|
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)
|
|
|
|
for(var/i in 1 to required_moves)
|
|
pixel_move(1, FALSE)
|
|
|
|
/obj/projectile/proc/fire(angle, atom/direct_target)
|
|
if(fired_from)
|
|
SEND_SIGNAL(fired_from, COMSIG_PROJECTILE_BEFORE_FIRE, src, original)
|
|
//If no angle needs to resolve it from xo/yo!
|
|
if(shrapnel_type)
|
|
AddElement(/datum/element/embed, projectile_payload = shrapnel_type)
|
|
if(!log_override && firer && original)
|
|
log_combat(firer, original, "fired at", src, "from [get_area_name(src, TRUE)]")
|
|
if(direct_target)
|
|
if(prehit(direct_target))
|
|
direct_target.bullet_act(src, def_zone)
|
|
qdel(src)
|
|
return
|
|
if(isnum(angle))
|
|
setAngle(angle)
|
|
if(spread)
|
|
setAngle(Angle + ((rand() - 0.5) * spread))
|
|
var/turf/starting = get_turf(src)
|
|
if(isnull(Angle)) //Try to resolve through offsets if there's no angle set.
|
|
if(isnull(xo) || isnull(yo))
|
|
stack_trace("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_Angle(src, target))
|
|
original_angle = Angle
|
|
if(!nondirectional_sprite)
|
|
var/matrix/M = new
|
|
M.Turn(Angle)
|
|
transform = M
|
|
trajectory_ignore_forcemove = TRUE
|
|
forceMove(starting)
|
|
trajectory_ignore_forcemove = FALSE
|
|
trajectory = new(starting.x, starting.y, starting.z, pixel_x, pixel_y, Angle, SSprojectiles.global_pixel_speed)
|
|
last_projectile_move = world.time
|
|
fired = TRUE
|
|
SEND_SIGNAL(src, COMSIG_PROJECTILE_FIRE)
|
|
if(hitscan)
|
|
process_hitscan()
|
|
if(!(datum_flags & DF_ISPROCESSING))
|
|
START_PROCESSING(SSprojectiles, src)
|
|
pixel_move(1, FALSE) //move it now!
|
|
|
|
/obj/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/projectile/forceMove(atom/target)
|
|
if(!isloc(target) || !isloc(loc) || !z)
|
|
return ..()
|
|
var/zc = target.z != z
|
|
var/old = loc
|
|
if(zc)
|
|
before_z_change(old, target)
|
|
. = ..()
|
|
if(trajectory && !trajectory_ignore_forcemove && isturf(target))
|
|
if(hitscan)
|
|
finalize_hitscan_and_generate_tracers(FALSE)
|
|
trajectory.initialize_location(target.x, target.y, target.z, 0, 0)
|
|
if(hitscan)
|
|
record_hitscan_start(RETURN_PRECISE_POINT(src))
|
|
if(zc)
|
|
after_z_change(old, target)
|
|
|
|
/obj/projectile/proc/after_z_change(atom/olcloc, atom/newloc)
|
|
|
|
/obj/projectile/proc/before_z_change(atom/oldloc, atom/newloc)
|
|
|
|
/obj/projectile/vv_edit_var(var_name, var_value)
|
|
switch(var_name)
|
|
if(NAMEOF(src, Angle))
|
|
setAngle(var_value)
|
|
return TRUE
|
|
else
|
|
return ..()
|
|
|
|
/obj/projectile/proc/set_pixel_speed(new_speed)
|
|
if(trajectory)
|
|
trajectory.set_speed(new_speed)
|
|
return TRUE
|
|
return FALSE
|
|
|
|
/obj/projectile/proc/record_hitscan_start(datum/point/pcache)
|
|
if(pcache)
|
|
beam_segments = list()
|
|
beam_index = pcache
|
|
beam_segments[beam_index] = null //record start.
|
|
|
|
/obj/projectile/proc/process_hitscan()
|
|
var/safety = range * 3
|
|
record_hitscan_start(RETURN_POINT_VECTOR_INCREMENT(src, Angle, MUZZLE_EFFECT_PIXEL_INCREMENT, 1))
|
|
while(loc && !QDELETED(src))
|
|
if(paused)
|
|
stoplag(1)
|
|
continue
|
|
if(safety-- <= 0)
|
|
if(loc)
|
|
Bump(loc)
|
|
if(!QDELETED(src))
|
|
qdel(src)
|
|
return //Kill!
|
|
pixel_move(1, TRUE)
|
|
|
|
/obj/projectile/proc/pixel_move(trajectory_multiplier, hitscanning = FALSE)
|
|
if(!loc || !trajectory)
|
|
return
|
|
last_projectile_move = world.time
|
|
if(!nondirectional_sprite && !hitscanning)
|
|
var/matrix/M = new
|
|
M.Turn(Angle)
|
|
transform = M
|
|
if(homing)
|
|
process_homing()
|
|
var/forcemoved = FALSE
|
|
for(var/i in 1 to SSprojectiles.global_iterations_per_move)
|
|
if(QDELETED(src))
|
|
return
|
|
trajectory.increment(trajectory_multiplier)
|
|
var/turf/T = trajectory.return_turf()
|
|
if(!istype(T))
|
|
qdel(src)
|
|
return
|
|
if(T.z != loc.z)
|
|
var/old = loc
|
|
before_z_change(loc, T)
|
|
trajectory_ignore_forcemove = TRUE
|
|
forceMove(T)
|
|
trajectory_ignore_forcemove = FALSE
|
|
after_z_change(old, loc)
|
|
if(!hitscanning)
|
|
pixel_x = trajectory.return_px()
|
|
pixel_y = trajectory.return_py()
|
|
forcemoved = TRUE
|
|
hitscan_last = loc
|
|
else if(T != loc)
|
|
step_towards(src, T)
|
|
hitscan_last = loc
|
|
if(!hitscanning && !forcemoved)
|
|
pixel_x = trajectory.return_px() - trajectory.mpx * trajectory_multiplier * SSprojectiles.global_iterations_per_move
|
|
pixel_y = trajectory.return_py() - trajectory.mpy * trajectory_multiplier * SSprojectiles.global_iterations_per_move
|
|
animate(src, pixel_x = trajectory.return_px(), pixel_y = trajectory.return_py(), time = 1, flags = ANIMATION_END_NOW)
|
|
Range()
|
|
|
|
/obj/projectile/proc/process_homing() //may need speeding up in the future performance wise.
|
|
if(!homing_target)
|
|
return FALSE
|
|
var/datum/point/PT = RETURN_PRECISE_POINT(homing_target)
|
|
PT.x += clamp(homing_offset_x, 1, world.maxx)
|
|
PT.y += clamp(homing_offset_y, 1, world.maxy)
|
|
var/angle = closer_angle_difference(Angle, angle_between_points(RETURN_PRECISE_POINT(src), PT))
|
|
setAngle(Angle + clamp(angle, -homing_turn_speed, homing_turn_speed))
|
|
|
|
/obj/projectile/proc/set_homing_target(atom/A)
|
|
if(!A || (!isturf(A) && !isturf(A.loc)))
|
|
return FALSE
|
|
homing = TRUE
|
|
homing_target = A
|
|
homing_offset_x = rand(homing_inaccuracy_min, homing_inaccuracy_max)
|
|
homing_offset_y = rand(homing_inaccuracy_min, homing_inaccuracy_max)
|
|
if(prob(50))
|
|
homing_offset_x = -homing_offset_x
|
|
if(prob(50))
|
|
homing_offset_y = -homing_offset_y
|
|
|
|
//Returns true if the target atom is on our current turf and above the right layer
|
|
//If direct target is true it's the originally clicked target.
|
|
/obj/projectile/proc/can_hit_target(atom/target, list/passthrough, direct_target = FALSE, ignore_loc = FALSE)
|
|
if(QDELETED(target))
|
|
return FALSE
|
|
if(!ignore_source_check && firer)
|
|
var/mob/M = firer
|
|
if((target == firer) || ((target == firer.loc) && ismecha(firer.loc)) || (target in firer.buckled_mobs) || (istype(M) && (M.buckled == target)))
|
|
return FALSE
|
|
if(!ignore_loc && (loc != target.loc))
|
|
return FALSE
|
|
if(target in passthrough)
|
|
return FALSE
|
|
if(target.density) //This thing blocks projectiles, hit it regardless of layer/mob stuns/etc.
|
|
return TRUE
|
|
if(!isliving(target))
|
|
if(target.layer < PROJECTILE_HIT_THRESHHOLD_LAYER)
|
|
return FALSE
|
|
else
|
|
var/mob/living/L = target
|
|
if(!direct_target)
|
|
var/checking = NONE
|
|
if(!hit_stunned_targets)
|
|
checking = MOBILITY_USE | MOBILITY_STAND | MOBILITY_MOVE
|
|
if(!(L.mobility_flags & checking) || L.stat == DEAD) // If target not able to use items, move and stand - or if they're just dead, pass over.
|
|
return FALSE
|
|
return TRUE
|
|
|
|
//Spread is FORCED!
|
|
/obj/projectile/proc/preparePixelProjectile(atom/target, atom/source, params, spread = 0)
|
|
var/turf/curloc = get_turf(source)
|
|
var/turf/targloc = get_turf(target)
|
|
trajectory_ignore_forcemove = TRUE
|
|
forceMove(get_turf(source))
|
|
trajectory_ignore_forcemove = FALSE
|
|
starting = get_turf(source)
|
|
original = target
|
|
if(targloc || !params)
|
|
yo = targloc.y - curloc.y
|
|
xo = targloc.x - curloc.x
|
|
setAngle(Get_Angle(src, targloc) + spread)
|
|
|
|
if(isliving(source) && params)
|
|
var/list/calculated = calculate_projectile_angle_and_pixel_offsets(source, params)
|
|
p_x = calculated[2]
|
|
p_y = calculated[3]
|
|
|
|
setAngle(calculated[1] + spread)
|
|
else if(targloc)
|
|
yo = targloc.y - curloc.y
|
|
xo = targloc.x - curloc.x
|
|
setAngle(Get_Angle(src, targloc) + spread)
|
|
else
|
|
stack_trace("WARNING: Projectile [type] fired without either mouse parameters, or a target atom to aim at!")
|
|
qdel(src)
|
|
|
|
/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.
|
|
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/projectile/Crossed(atom/movable/AM) //A mob moving on a tile with a projectile is hit by it.
|
|
. = ..()
|
|
if(isliving(AM) && !(pass_flags & PASSMOB))
|
|
var/mob/living/L = AM
|
|
if(can_hit_target(L, permutated, (AM == original)))
|
|
Bump(AM)
|
|
|
|
/obj/projectile/Move(atom/newloc, dir = NONE)
|
|
. = ..()
|
|
if(.)
|
|
if(temporary_unstoppable_movement)
|
|
temporary_unstoppable_movement = FALSE
|
|
movement_type &= ~(UNSTOPPABLE)
|
|
if(fired && can_hit_target(original, permutated, TRUE))
|
|
Bump(original)
|
|
|
|
/obj/projectile/Destroy()
|
|
if(hitscan)
|
|
finalize_hitscan_and_generate_tracers()
|
|
STOP_PROCESSING(SSprojectiles, src)
|
|
cleanup_beam_segments()
|
|
qdel(trajectory)
|
|
return ..()
|
|
|
|
/obj/projectile/proc/cleanup_beam_segments()
|
|
QDEL_LIST_ASSOC(beam_segments)
|
|
beam_segments = list()
|
|
qdel(beam_index)
|
|
|
|
/obj/projectile/proc/finalize_hitscan_and_generate_tracers(impacting = TRUE)
|
|
if(trajectory && beam_index)
|
|
var/datum/point/pcache = trajectory.copy_to()
|
|
beam_segments[beam_index] = pcache
|
|
generate_hitscan_tracers(null, null, impacting)
|
|
|
|
/obj/projectile/proc/generate_hitscan_tracers(cleanup = TRUE, duration = 3, impacting = TRUE)
|
|
if(!length(beam_segments))
|
|
return
|
|
if(tracer_type)
|
|
var/tempref = REF(src)
|
|
for(var/datum/point/p in beam_segments)
|
|
generate_tracer_between_points(p, beam_segments[p], tracer_type, color, duration, hitscan_light_range, hitscan_light_color_override, hitscan_light_intensity, tempref)
|
|
if(muzzle_type && duration > 0)
|
|
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
|
|
thing.color = color
|
|
thing.set_light(muzzle_flash_range, muzzle_flash_intensity, muzzle_flash_color_override? muzzle_flash_color_override : color)
|
|
QDEL_IN(thing, duration)
|
|
if(impacting && impact_type && duration > 0)
|
|
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
|
|
thing.color = color
|
|
thing.set_light(impact_light_range, impact_light_intensity, impact_light_color_override? impact_light_color_override : color)
|
|
QDEL_IN(thing, duration)
|
|
if(cleanup)
|
|
cleanup_beam_segments()
|
|
|
|
/obj/projectile/experience_pressure_difference()
|
|
return
|
|
|
|
///Like [/obj/item/proc/updateEmbedding] but for projectiles instead, call this when you want to add embedding or update the stats on the embedding element
|
|
/obj/projectile/proc/updateEmbedding()
|
|
if(!shrapnel_type || !LAZYLEN(embedding))
|
|
return
|
|
|
|
AddElement(/datum/element/embed,\
|
|
embed_chance = (!isnull(embedding["embed_chance"]) ? embedding["embed_chance"] : EMBED_CHANCE),\
|
|
fall_chance = (!isnull(embedding["fall_chance"]) ? embedding["fall_chance"] : EMBEDDED_ITEM_FALLOUT),\
|
|
pain_chance = (!isnull(embedding["pain_chance"]) ? embedding["pain_chance"] : EMBEDDED_PAIN_CHANCE),\
|
|
pain_mult = (!isnull(embedding["pain_mult"]) ? embedding["pain_mult"] : EMBEDDED_PAIN_MULTIPLIER),\
|
|
remove_pain_mult = (!isnull(embedding["remove_pain_mult"]) ? embedding["remove_pain_mult"] : EMBEDDED_UNSAFE_REMOVAL_PAIN_MULTIPLIER),\
|
|
rip_time = (!isnull(embedding["rip_time"]) ? embedding["rip_time"] : EMBEDDED_UNSAFE_REMOVAL_TIME),\
|
|
ignore_throwspeed_threshold = (!isnull(embedding["ignore_throwspeed_threshold"]) ? embedding["ignore_throwspeed_threshold"] : FALSE),\
|
|
impact_pain_mult = (!isnull(embedding["impact_pain_mult"]) ? embedding["impact_pain_mult"] : EMBEDDED_IMPACT_PAIN_MULTIPLIER),\
|
|
jostle_chance = (!isnull(embedding["jostle_chance"]) ? embedding["jostle_chance"] : EMBEDDED_JOSTLE_CHANCE),\
|
|
jostle_pain_mult = (!isnull(embedding["jostle_pain_mult"]) ? embedding["jostle_pain_mult"] : EMBEDDED_JOSTLE_PAIN_MULTIPLIER),\
|
|
pain_stam_pct = (!isnull(embedding["pain_stam_pct"]) ? embedding["pain_stam_pct"] : EMBEDDED_PAIN_STAM_PCT),\
|
|
projectile_payload = shrapnel_type)
|
|
return TRUE
|