160175ee8b
Yeah uhh this'll probably need testmerging even after it's done because yeah it's a bit big. If y'all want me to atomize this into two PRs (pass flags vs projectiles) tell me please. Pass flags would have to go in first though, in that case, as new projectile hit handling will rely on pass_flags_self. Pass flags: Pass flags handling now uses an atom variable named pass_flags_self. If any of these match a pass_flag on a thing trying to pass through, it's allowed through by default. This makes overriding CanAllowThrough unnecessary for the majority of things. I've however not removed overrides for very.. weird cases, like plastic flaps which uses a prob(60) for letting PASSGLASS things through for god knows why. LETPASSTHROW is now on pass_flags_self Projectiles: Not finalized yet, need to do something to make the system I have in mind have less unneeded overhead + snowflake Basically, for piercing/phasing/otherwise projectiles that go through things instead of hitting the first dense object, I have them use pass_flags flags for two new variables, projectile_phasing and projectile_piercing. Anything with pass_flags_self in the former gets phased through entirely. Anything in the latter gets hit, and the projectile then goes through. on_hit will also register a piercing hit vs a normal hit (so things like missiles can only explode on a normal hit or otherwise, instead of exploding multiple times. Not needed as missiles qdel(src) right now but it's nice to have for the future). I still need to decide what to do for hit handling proper, as Bump() is still preferred due to it not being as high-overhead as something like scanning on Moved(). I'm thinking I'll make Moved() only scan for cases where it needs to hit a non-dense object - a prone human the user clicked on, anything special like that. Don't know the exact specifics yet, which is why this is still WIP. Projectiles now use check_pierce() to determine if it goes through something and hits it, doesn't hit it, or doesn't go through something at all (should delete self after hitting). Will likely make an on_pierce proc to be called post-piercing something so you can have !fun! things like projectiles that go down in damage after piercing something. This will likely deprecate the process_hit proc, or at least make it less awful. scan_for_hit() is now used to attempt to hit something and will return whether the projectile got deleted or not. It will delete the projectile if the projectile does hit something and fails to pierce through it. scan_moved_turf() (WIP) will be used for handling moving onto a turf. permutated has been renamed to impacted. Ricocheting projectiles get it reset, allowing projectiles to pierce and potentially hit something again if it goes back around. A new unit test has been added checking for projectiles with movement type of PHASING. This is because PHASING completely causes projectiles to break down as projectiles mainly sense collisions through Bump. The small boost in performance from using PHASING instead of having all pass flags active/overriding check_pierce is in my opinion not worth the extra snowflake in scan_moved_turf() I'd have to do to deal with having to check for hits manually rather than Bump()ing things. Movement types UNSTOPPABLE renamed to PHASING to better describe what it is, going through and crossing everything but not actually bumping. Why It's Good For The Game Better pass flags handling allows for less proc overrides, bitflag checks are far less expensive in general. Fixes penetrating projectiles like sniper penetrators This system also allows for better handling of piercing projectiles (see above) without too much snowflake code, as you'd only need to modify on_pierce() if you needed to do special handling like dampening damage per target pierced, and otherwise you could just use the standardized system and just set pass flags to what's needed. If you really need a projectile that pierces almost everything, override check_pierce(), which is still going to be easier than what was done before (even with snowflake handling of UNSTOPPABLE flag process_hit() was extremely ugly, now we don't rely on movement types at all.)
943 lines
37 KiB
Plaintext
943 lines
37 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
|
|
mouse_opacity = MOUSE_OPACITY_TRANSPARENT
|
|
movement_type = FLYING
|
|
wound_bonus = CANT_WOUND // can't wound by default
|
|
generic_canpass = FALSE
|
|
//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/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!
|
|
/// We already impacted these things, do not impact them again. Used to make sure we can pierce things we want to pierce. Lazylist, typecache style (object = TRUE) for performance.
|
|
var/list/impacted
|
|
/// If TRUE, we can hit our firer.
|
|
var/ignore_source_check = FALSE
|
|
/// We are flagged PHASING temporarily to not stop moving when we Bump something but want to keep going anyways.
|
|
var/temporary_unstoppable_movement = FALSE
|
|
|
|
/** PROJECTILE PIERCING
|
|
* WARNING:
|
|
* Projectile piercing MUST be done using these variables.
|
|
* Ordinary passflags will be **IGNORED**.
|
|
* The two flag variables below both use pass flags.
|
|
* In the context of LETPASStHROW, it means the projectile will ignore things that are currently "in the air" from a throw.
|
|
*
|
|
* Also, projectiles sense hits using Bump(), and then pierce them if necessary.
|
|
* They simply do not follow conventional movement rules.
|
|
* NEVER flag a projectile as PHASING movement type.
|
|
* If you so badly need to make one go through *everything*, override check_pierce() for your projectile to always return PROJECTILE_PIERCE_PHASE/HIT.
|
|
*/
|
|
/// The "usual" flags of pass_flags is used in that can_hit_target ignores these unless they're specifically targeted/clicked on. This behavior entirely bypasses process_hit if triggered, rather than phasing which uses prehit_pierce() to check.
|
|
pass_flags = PASSTABLE
|
|
/// If FALSE, allow us to hit something directly targeted/clicked/whatnot even if we're able to phase through it
|
|
var/phasing_ignore_direct_target = FALSE
|
|
/// Bitflag for things the projectile should just phase through entirely - No hitting unless direct target and [phasing_ignore_direct_target] is FALSE. Uses pass_flags flags.
|
|
var/projectile_phasing = NONE
|
|
/// Bitflag for things the projectile should hit, but pierce through without deleting itself. Defers to projectile_phasing. Uses pass_flags flags.
|
|
var/projectile_piercing = NONE
|
|
/// number of times we've pierced something. Incremented BEFORE bullet_act and on_hit proc!
|
|
var/pierces = 0
|
|
|
|
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/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)
|
|
|
|
///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()
|
|
. = ..()
|
|
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
|
|
|
|
/**
|
|
* Called when the projectile hits something
|
|
*
|
|
* @params
|
|
* target - thing hit
|
|
* blocked - percentage of hit blocked
|
|
* pierce_hit - are we piercing through or regular hitting
|
|
*/
|
|
/obj/projectile/proc/on_hit(atom/target, blocked = FALSE, pierce_hit)
|
|
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)
|
|
SEND_SIGNAL(src, COMSIG_PROJECTILE_SELF_ON_HIT, firer, target, Angle, hit_limb)
|
|
|
|
if(QDELETED(src)) // in case one of the above signals deleted the projectile for whatever reason
|
|
return
|
|
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)
|
|
if(isturf(target) && hitsound_wall)
|
|
var/volume = clamp(vol_by_damage() + 20, 0, 100)
|
|
if(suppressed)
|
|
volume = 5
|
|
playsound(loc, hitsound_wall, volume, TRUE, -1)
|
|
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(src, 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?.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)
|
|
SEND_SIGNAL(src, COMSIG_MOVABLE_BUMP, A)
|
|
if(!can_hit_target(A, A == original, TRUE))
|
|
return
|
|
Impact(A)
|
|
|
|
/**
|
|
* Called when the projectile hits something
|
|
* This can either be from it bumping something,
|
|
* or it passing over a turf/being crossed and scanning that there is infact
|
|
* a valid target it needs to hit.
|
|
* This target isn't however necessarily WHAT it hits
|
|
* that is determined by process_hit and select_target.
|
|
*
|
|
* Furthermore, this proc shouldn't check can_hit_target - this should only be called if can hit target is already checked.
|
|
* Also, we select_target to find what to process_hit first.
|
|
*/
|
|
/obj/projectile/proc/Impact(atom/A)
|
|
if(!trajectory)
|
|
qdel(src)
|
|
return FALSE
|
|
if(impacted[A]) // NEVER doublehit
|
|
return FALSE
|
|
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)
|
|
impacted = list() // Shoot a x-ray laser at a pair of mirrors I dare you
|
|
ignore_source_check = TRUE // Firer is no longer immune
|
|
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.
|
|
|
|
return process_hit(T, select_target(T, A)) // SELECT TARGET FIRST!
|
|
|
|
/**
|
|
* The primary workhorse proc of projectile impacts.
|
|
* This is a RECURSIVE call - process_hit is called on the first selected target, and then repeatedly called if the projectile still hasn't been deleted.
|
|
*
|
|
* Order of operations:
|
|
* 1. Checks if we are deleted, or if we're somehow trying to hit a null, in which case, bail out
|
|
* 2. Adds the thing we're hitting to impacted so we can make sure we don't doublehit
|
|
* 3. Checks piercing - stores this.
|
|
* Afterwards:
|
|
* Hit and delete, hit without deleting and pass through, pass through without hitting, or delete without hitting depending on result
|
|
* If we're going through without hitting, find something else to hit if possible and recurse, set unstoppable movement to true
|
|
* If we're deleting without hitting, delete and return
|
|
* Otherwise, send signal of COMSIG_PROJECTILE_PREHIT to target
|
|
* Then, hit, deleting ourselves if necessary.
|
|
* @params
|
|
* T - Turf we're on/supposedly hitting
|
|
* target - target we're hitting
|
|
* hit_something - only should be set by recursive calling by this proc - tracks if we hit something already
|
|
*
|
|
* Returns if we hit something.
|
|
*/
|
|
/obj/projectile/proc/process_hit(turf/T, atom/target, hit_something = FALSE)
|
|
// 1.
|
|
if(QDELETED(src) || !T || !target)
|
|
return
|
|
// 2.
|
|
impacted[target] = TRUE //hash lookup > in for performance in hit-checking
|
|
// 3.
|
|
var/mode = prehit_pierce(target)
|
|
if(mode == PROJECTILE_DELETE_WITHOUT_HITTING)
|
|
qdel(src)
|
|
return hit_something
|
|
else if(mode == PROJECTILE_PIERCE_PHASE)
|
|
if(!(movement_type & PHASING))
|
|
temporary_unstoppable_movement = TRUE
|
|
movement_type |= PHASING
|
|
return process_hit(T, select_target(T, target), hit_something) // try to hit something else
|
|
// at this point we are going to hit the thing
|
|
// in which case send signal to it
|
|
SEND_SIGNAL(target, COMSIG_PROJECTILE_PREHIT, args)
|
|
if(mode == PROJECTILE_PIERCE_HIT)
|
|
++pierces
|
|
hit_something = TRUE
|
|
var/result = target.bullet_act(src, def_zone, mode == PROJECTILE_PIERCE_HIT)
|
|
if((result == BULLET_ACT_FORCE_PIERCE) || (mode == PROJECTILE_PIERCE_HIT))
|
|
if(!(movement_type & PHASING))
|
|
temporary_unstoppable_movement = TRUE
|
|
movement_type |= PHASING
|
|
return process_hit(T, select_target(T, target), TRUE)
|
|
qdel(src)
|
|
return hit_something
|
|
|
|
/**
|
|
* Selects a target to hit from a turf
|
|
*
|
|
* @params
|
|
* T - The turf
|
|
* target - The "preferred" atom to hit, usually what we Bumped() first.
|
|
*
|
|
* Priority:
|
|
* 0. Anything that is already in impacted is ignored no matter what. Furthermore, in any bracket, if the target atom parameter is in it, that's hit first.
|
|
* Furthermore, can_hit_target is always checked. This (entire proc) is PERFORMANCE OVERHEAD!! But, it shouldn't be ""too"" bad and I frankly don't have a better *generic non snowflakey* way that I can think of right now at 3 AM.
|
|
* FURTHERMORE, mobs/objs have a density check from can_hit_target - to hit non dense objects over a turf, you must click on them, same for mobs that usually wouldn't get hit.
|
|
* 1. The thing originally aimed at/clicked on
|
|
* 2. Mobs - picks lowest buckled mob to prevent scarp piggybacking memes
|
|
* 3. Objs
|
|
* 4. Turf
|
|
* 5. Nothing
|
|
*/
|
|
/obj/projectile/proc/select_target(turf/T, atom/target)
|
|
// 1. original
|
|
if(can_hit_target(original, TRUE, FALSE))
|
|
return original
|
|
var/list/atom/possible = list() // let's define these ONCE
|
|
var/list/atom/considering = list()
|
|
// 2. mobs
|
|
possible = typecache_filter_list(T, GLOB.typecache_living) // living only
|
|
for(var/i in possible)
|
|
if(!can_hit_target(i, i == original, TRUE))
|
|
continue
|
|
considering += i
|
|
if(considering.len)
|
|
var/mob/living/M = pick(considering)
|
|
return M.lowest_buckled_mob()
|
|
considering.len = 0
|
|
// 3. objs
|
|
possible = typecache_filter_list(T, GLOB.typecache_machine_or_structure) // because why are items ever dense?
|
|
for(var/i in possible)
|
|
if(!can_hit_target(i, i == original, TRUE))
|
|
continue
|
|
considering += i
|
|
if(considering.len)
|
|
return pick(considering)
|
|
// 4. turf
|
|
if(can_hit_target(T, T == original, TRUE))
|
|
return T
|
|
// 5. nothing
|
|
// (returns null)
|
|
|
|
//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, direct_target = FALSE, ignore_loc = FALSE)
|
|
if(QDELETED(target) || impacted[target])
|
|
return FALSE
|
|
if(!ignore_loc && (loc != target.loc))
|
|
return FALSE
|
|
// if pass_flags match, pass through entirely
|
|
if(target.pass_flags_self & pass_flags) // phasing
|
|
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(target.density) //This thing blocks projectiles, hit it regardless of layer/mob stuns/etc.
|
|
return TRUE
|
|
if(!isliving(target))
|
|
if(isturf(target)) // non dense turfs
|
|
return FALSE
|
|
if(target.layer < PROJECTILE_HIT_THRESHHOLD_LAYER)
|
|
return FALSE
|
|
else if(!direct_target) // non dense objects do not get hit unless specifically clicked
|
|
return FALSE
|
|
else
|
|
var/mob/living/L = target
|
|
if(direct_target)
|
|
return TRUE
|
|
// If target not able to use items, move and stand - or if they're just dead, pass over.
|
|
if(L.stat == DEAD || (!hit_stunned_targets && HAS_TRAIT(L, TRAIT_IMMOBILIZED) && HAS_TRAIT(L, TRAIT_FLOORED) && HAS_TRAIT(L, TRAIT_HANDS_BLOCKED)))
|
|
return FALSE
|
|
return TRUE
|
|
|
|
/**
|
|
* Scan if we should hit something and hit it if we need to
|
|
* The difference between this and handling in Impact is
|
|
* In this we strictly check if we need to Impact() something in specific
|
|
* If we do, we do
|
|
* We don't even check if it got hit already - Impact() does that
|
|
* In impact there's more code for selecting WHAT to hit
|
|
* So this proc is more of checking if we should hit something at all BY having an atom cross us.
|
|
*/
|
|
/obj/projectile/proc/scan_crossed_hit(atom/movable/A)
|
|
if(can_hit_target(A, direct_target = (A == original)))
|
|
Impact(A)
|
|
|
|
/**
|
|
* Scans if we should hit something on the turf we just moved to if we haven't already
|
|
*
|
|
* This proc is a little high in overhead but allows us to not snowflake CanPass in living and other things.
|
|
*/
|
|
/obj/projectile/proc/scan_moved_turf()
|
|
// Optimally, we scan: mobs --> objs --> turf for impact
|
|
// but, overhead is a thing and 2 for loops every time it moves is a no-go.
|
|
// realistically, since we already do select_target in impact, we can not do that
|
|
// and hope projectiles get refactored again in the future to have a less stupid impact detection system
|
|
// that hopefully won't also involve a ton of overhead
|
|
if(can_hit_target(original, TRUE, FALSE))
|
|
Impact(original) // try to hit thing clicked on
|
|
// else, try to hit mobs
|
|
else // because if we impacted original and pierced we'll already have select target'd and hit everything else we should be hitting
|
|
for(var/mob/M in loc) // so I guess we're STILL doing a for loop of mobs because living movement would otherwise have snowflake code for projectile CanPass
|
|
// so the snowflake vs performance is pretty arguable here
|
|
if(can_hit_target(M, M == original, TRUE))
|
|
Impact(M)
|
|
break
|
|
|
|
/**
|
|
* Projectile crossed: When something enters a projectile's tile, make sure the projectile hits it if it should be hitting it.
|
|
*/
|
|
/obj/projectile/Crossed(atom/movable/AM)
|
|
. = ..()
|
|
scan_crossed_hit(AM)
|
|
|
|
/**
|
|
* Projectile can pass through
|
|
* Used to not even attempt to Bump() or fail to Cross() anything we already hit.
|
|
*/
|
|
/obj/projectile/CanPassThrough(atom/blocker, turf/target, blocker_opinion)
|
|
return impacted[blocker]? TRUE : ..()
|
|
|
|
/**
|
|
* Projectile moved:
|
|
*
|
|
* If not fired yet, do not do anything. Else,
|
|
*
|
|
* If temporary unstoppable movement used for piercing through things we already hit (impacted list) is set, unset it.
|
|
* Scan turf we're now in for anything we can/should hit. This is useful for hitting non dense objects the user
|
|
* directly clicks on, as well as for PHASING projectiles to be able to hit things at all as they don't ever Bump().
|
|
*/
|
|
/obj/projectile/Moved(atom/OldLoc, Dir)
|
|
. = ..()
|
|
if(!fired)
|
|
return
|
|
if(temporary_unstoppable_movement)
|
|
temporary_unstoppable_movement = FALSE
|
|
movement_type &= ~PHASING
|
|
scan_moved_turf() //mostly used for making sure we can hit a non-dense object the user directly clicked on, and for penetrating projectiles that don't bump
|
|
|
|
/**
|
|
* Checks if we should pierce something.
|
|
*
|
|
* NOT meant to be a pure proc, since this replaces prehit() which was used to do things.
|
|
* Return PROJECTILE_DELETE_WITHOUT_HITTING to delete projectile without hitting at all!
|
|
*/
|
|
/obj/projectile/proc/prehit_pierce(atom/A)
|
|
if(projectile_phasing & A.pass_flags_self)
|
|
return PROJECTILE_PIERCE_PHASE
|
|
if(projectile_piercing & A.pass_flags_self)
|
|
return PROJECTILE_PIERCE_HIT
|
|
if(ismovable(A))
|
|
var/atom/movable/AM = A
|
|
if(AM.throwing)
|
|
return (projectile_phasing & LETPASSTHROW)? PROJECTILE_PIERCE_PHASE : ((projectile_piercing & LETPASSTHROW)? PROJECTILE_PIERCE_HIT : PROJECTILE_PIERCE_NONE)
|
|
return PROJECTILE_PIERCE_NONE
|
|
|
|
/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 && LAZYLEN(embedding))
|
|
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 && (get_dist(direct_target, get_turf(src)) <= 1)) // point blank shots
|
|
process_hit(get_turf(direct_target), direct_target)
|
|
if(QDELETED(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
|
|
LAZYINITLIST(impacted)
|
|
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(QDELETED(src)) // we coulda bumped something
|
|
return
|
|
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
|
|
|
|
//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/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
|