mirror of
https://github.com/Bubberstation/Bubberstation.git
synced 2025-12-24 08:31:54 +00:00
About The Pull Request
This PR adds medical wounds, new forms of injuries that people can suffer that cause debilitation and complications, and often require more than what can be found in a medkit to treat. But let's be honest, big complicated walls of text about medical changes make people's eyes glaze over easily- so I created a handy infograph to explain the basics!
Also there's a full guide here!
dreamseeker_2020-04-18_20-42-19.png
The infograph may not be fully up to date with the specifics of the PR's status, but it'll be updated along with major changes so people have something to use as a crash course for familiarizing themselves with how wounds function. I also have another infograph with all 9 of the possible initial wounds coming, and will be up soon. You can also find the longform design doc here with more info on the broad details, including descriptions of treatments: hackmd whee
What this does
There's a lot to cover, but here's the bullet points of the main features and changes:
Getting lots of damage on a limb can result in wounds, with more damage causing worse wounds. These can range from dislocated joints and minor cuts to compound fractures and fourth degree burns, and can affect you in different ways depending on what bodypart they're applied to (namely with broken bones).
You can damage individual bodyparts on clothing (only jumpsuits for now) through the use of lasers and sharp weapons. Bodyparts that reach max damage are considered "shredded" and will not apply any protection for that zone until it is repaired with cloth. If all zones are disabled, the entire piece of clothing is shredded and unwearable until repaired with 3 cloth. Jumpsuits give a small amount of wound protection, and since sharp weapons and lasers generally get extra wound bonuses against bare flesh, even a plain jumpsuit provides decent protection from a few laser shots or scalpel stabs.
Lasers gain a powerful niche versus unarmored/lightly armored carbons! As noted above, lasers can shred clothing and burn away zones of jumpsuits in 2 shots each, after which the target's bare flesh is exposed (barring other clothing), and lasers excel at dealing burn wounds against uncovered skin. Think big, nasty charring!
Bleeding is now totally limb based, and gauze is as well. Bleeding is also 95% cut wound based, meaning sharp weapons make you bleed rather than just having 40+ brute on a limb.
The more wounds and damage you get on a bodypart, the easier it'll be to gain more severe wounds. Wounds are arranged from Moderate, to Severe, to Critical in increasing severity, and you'll generally have to suffer the lesser ones before getting the worse ones.
dreamseeker_2020-05-15_03-15-59.png
Above: Someone having an incredibly bad day from bloodloss
dreamseeker_2020-05-04_22-29-29.png
Above: Scars from healed wounds
ShareX_2020-05-15_06-55-20.png
Above: Actual combat involving someone's head getting cracked
Here's a quick, if non-exhaustive, list of things I have left to do before I consider it feature complete
Finish adding treatments for each wound type/severity (mostly surgeries/triage for critical wounds)
Add second winds for bad injuries to give the victim a chance to get away
Flesh out severe & critical injuries in general
Find sprites for the bonesetter, bone gel, and anything else that might be needed
Add the medical items for treating the less severe wounds to the station
Polish code and remove any redundancies I left behind
Quick balance pass to make sure nothing is horribly abuseable
Why It's Good For The Game
Adds a flexible new system for representing damage on carbons with injuries that can be treated in different ways. Moderate wounds from getting toolboxed or sliced with a scalpel can usually be treated by a buddy or even by yourself with the right tools, but getting flayed with a fireaxe or a laser gun emptied into your bare skin may require extra attention or even surgery in bad cases! Also makes laser guns cooler and more like 40k lasguns that can flash fry people (cool!)
This should also make spessmen more resilient and harder to kill outright, while still adding consequences and complications to getting hurt. Wounds aren't immediately fatal, but they can do things like slow down interactions, deal damage over time through infections, and generally make you more fragile until fixed. They can also give you a "second wind" on being applied that gives you a small adrenaline boost (or whatever) to help disengage and escape immediate danger.
Changelog
🆑 Ryll/Shaps
add: Introduces medical wounds, new injuries that can happen to fleshy carbons when they sustain lots of damage on a bodypart. There's quite a lot of change here, but you can read the guide at: https://tgstation13.org/wiki/Guide_to_wounds and an extended changelog is available here: https://hackmd.io/l_FI9b3tSqa_woDadewJXA
add: Introduces scars and temporal scarring! Healing a wound leaves a scar behind that can be seen by examining someone twice rapidly, and if Temporal Scarring is enabled in character prefs, surviving a round with scars will save them to be granted at roundstart another round! Let your body tell stories!
tweak: Bleeding is now fully bodypart-focused, and 95% of bleeding comes from cut wounds from sharp weapons. Gauze is applied on a limb-by-limb basis, and helps staunch bloodflow rather than totally stop it. Notably, you no longer bleed just from having 40+ brute damage on a limb.
del: Organic bodyparts are no longer disabled at maximum damage, but are easier to cause wounds to
add: O2 medkits in emergency lockers have been replaced with new emergency medkits with basic tools for diagnosing and treating wounds and basic damage
tweak: Herapin now rapidly increases bleeding on all open cuts, rather than causing bleeding by itself. The more cuts on the target, the more it will affect them.
tweak: Neckgrab table slams now hit the targeted limb rather than just the head, with a large chance to dislocate or break a bone
tweak: Sharp weapons and burning weapons can now shred zones on jumpsuits, disabling protection on that limb. Damaged clothes can be repaired with cloth.
tweak: Slaughter demons now deal less raw damage, but gain the ability to cause cut wounds, which becomes more powerful with each attack on a humanoid but resets when bloodcrawling.
/🆑
746 lines
28 KiB
Plaintext
746 lines
28 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
|
|
//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, 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
|
|
var/shrapnel_type
|
|
///If TRUE, hit mobs even if they're on the floor and not our target
|
|
var/hit_stunned_targets = FALSE
|
|
|
|
wound_bonus = CANT_WOUND
|
|
/// For telling whether we want to roll for bone breaking or lacerations if we're bothering with wounds
|
|
var/sharpness = FALSE
|
|
|
|
|
|
/obj/projectile/Initialize()
|
|
. = ..()
|
|
permutated = list()
|
|
decayedRange = range
|
|
|
|
/obj/projectile/proc/Range()
|
|
range--
|
|
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)
|
|
SEND_SIGNAL(src, COMSIG_PROJECTILE_SELF_ON_HIT, firer, target, Angle, hit_limb)
|
|
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)
|
|
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.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
|