Skip to content

Commit

Permalink
Guncode Agony 4: The Great Projectile Purge (tgstation#87740)
Browse files Browse the repository at this point in the history
## About The Pull Request
~~Kept you waitin huh!~~
The projectile refactor is finally here, 4 years later. This PR (almost)
completely rewrites projectile logic to be more maintainable and
performant.

### Key changes:
* Instead of moving by a fixed amount of pixels, potentially skipping
tile corners and being performance-heavy, projectiles now use
raymarching in order to teleport through tiles and only visually animate
themselves. This allows us to do custom per-projectile animations and
makes the code much more reliable, sane and maintainable. You (did not)
serve us well, pixel_move.
* Speed variable now measures how many tiles (if SSprojectiles has
default values) a projectile passes in a tick instead of being a magical
Kevinz Unit:tm: coefficient. pixel_speed_multiplier has been retired
because it never had a right to exist in the first place. __This means
that downstreams will need to set all of their custom projectiles' speed
values to ``pixel_speed_multiplier / speed``__ in order to prevent
projectiles from inverting their speed.
* Hitscans no longer operate with spartial vectors and instead only
store key points in which the projectile impacted something or changed
its angle. This should similarly make the code much easier to work with,
as well as fixing some visual jank due to incorrect calculations.
* Projectiles only delete themselves the ***next*** tick after impacting
something or reaching their maximum range. Doing so allows them to
finish their impact animation and hide themselves between ticks via
animation chains. This means that projectiles no longer disappear ~a
tile before hitting their target, and that we can finally make impact
markers be consistent with where the projectile actually landed instead
of being entirely random.

<details>

<summary>Here is an example of how this affects our slowest-moving
projectile: Magic Missiles.</summary>


Before:


https://github.com/user-attachments/assets/06b3a980-4701-4aeb-aa3e-e21cd056020e

After:


https://github.com/user-attachments/assets/abe8ed5c-4b81-4120-8d2f-cf16ff5be915

</details>


<details>

<summary>And here is a much faster, and currently jankier, disabler
SMG.</summary>


Before:


https://github.com/user-attachments/assets/2d84aef1-0c83-44ef-a698-8ec716587348

After:


https://github.com/user-attachments/assets/2e7c1336-f611-404f-b3ff-87433398d238

</details>

### But how will this affect the ~~trout population~~ gameplay?

Beyond improved visuals, smoother movement and a few minor bugfixes,
this should not have a major gameplay impact. If something changed its
behavior in an unexpected way or started looking odd, please make an
issue report.
Projectile impacts should now be consistent with their visual position,
so hitting and dodging shots should be slightly easier and more
intuitive.

This PR should be testmerged extensively due to the amount of changes it
brings and considerable difficulty in reviewing them. Please contact me
to ensure its good to merge.

Closes tgstation#71822
Closes tgstation#78547
Closes tgstation#78871
Closes tgstation#83901
Closes tgstation#87802
Closes tgstation#88073

## Why It's Good For The Game

Our core projectile code is an ungodly abomination that nobody except
me, Kapu and Potato dared to poke in the past months (potentially
longer). It is laggy, overcomplicated and absolutely unmaintaineable -
while a lot of decisions made sense 4 years ago when we were attempting
to introduce pixel movement, nowadays they are only acting as major
roadblocks for any contributor who is attempting to make projectile
behavior that differs from normal in any way.

Huge thanks to Kapu and Potato (Lemon) on the discord for providing
insights, ideas and advice throughout the past months regarding
potential improvements to projectile code, almost all of which made it
in.

## Changelog
:cl:
qol: Projectiles now visually impact their targets instead of
disappearing about a tile short of it.
fix: Fixed multiple minor issues with projectile behavior
refactor: Completely rewrote almost all of our projectile code - if
anything broke or started looking/behaving oddly, make an issue report!
/:cl:
  • Loading branch information
SmArtKar authored Nov 23, 2024
1 parent 2e9932a commit bbb7a41
Show file tree
Hide file tree
Showing 113 changed files with 1,016 additions and 908 deletions.
4 changes: 4 additions & 0 deletions .github/CODEOWNERS
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,10 @@
/code/game/area/ @san7890
/icons/area/ @san7890

# SmArtKar

/code/modules/projectiles/projectile.dm @SmArtKar

# stylemistake

/code/__DEFINES/chat.dm @stylemistake
Expand Down
4 changes: 0 additions & 4 deletions code/__DEFINES/combat.dm
Original file line number Diff line number Diff line change
Expand Up @@ -224,10 +224,6 @@ GLOBAL_LIST_INIT(shove_disarming_types, typecacheof(list(
#define SUPPRESSED_QUIET 1 ///standard suppressed
#define SUPPRESSED_VERY 2 /// no message

//Projectile Reflect
#define REFLECT_NORMAL (1<<0)
#define REFLECT_FAKEPROJECTILE (1<<1)

//His Grace.
#define HIS_GRACE_SATIATED 0 //He hungers not. If bloodthirst is set to this, His Grace is asleep.
#define HIS_GRACE_PECKISH 20 //Slightly hungry.
Expand Down
10 changes: 5 additions & 5 deletions code/__DEFINES/dcs/signals/signals_object.dm
Original file line number Diff line number Diff line change
Expand Up @@ -380,9 +380,9 @@

// /obj/projectile signals (sent to the firer)

///from base of /obj/projectile/proc/on_hit(), like COMSIG_PROJECTILE_ON_HIT but on the projectile itself and with the hit limb (if any): (atom/movable/firer, atom/target, angle, hit_limb, blocked)
///from base of /obj/projectile/proc/on_hit(), like COMSIG_PROJECTILE_ON_HIT but on the projectile itself and with the hit limb (if any): (atom/movable/firer, atom/target, angle, hit_limb, blocked, pierce_hit)
#define COMSIG_PROJECTILE_SELF_ON_HIT "projectile_self_on_hit"
///from base of /obj/projectile/proc/on_hit(): (atom/movable/firer, atom/target, angle, hit_limb, blocked)
///from base of /obj/projectile/proc/on_hit(): (atom/movable/firer, atom/target, angle, hit_limb, blocked, pierce_hit)
#define COMSIG_PROJECTILE_ON_HIT "projectile_on_hit"
///from base of /obj/projectile/proc/fire(): (obj/projectile, atom/original_target)
#define COMSIG_PROJECTILE_BEFORE_FIRE "projectile_before_fire"
Expand All @@ -393,11 +393,11 @@
///sent to targets during the process_hit proc of projectiles
#define COMSIG_PROJECTILE_PREHIT "com_proj_prehit"
#define PROJECTILE_INTERRUPT_HIT (1<<0)
///from /obj/projectile/pixel_move(): ()
#define COMSIG_PROJECTILE_PIXEL_STEP "projectile_pixel_step"
///from /obj/projectile/process_movement(): ()
#define COMSIG_PROJECTILE_MOVE_PROCESS_STEP "projectile_move_process_step"
///sent to self during the process_hit proc of projectiles
#define COMSIG_PROJECTILE_SELF_PREHIT "com_proj_prehit"
///from the base of /obj/projectile/Range(): ()
///from the base of /obj/projectile/reduce_range(): ()
#define COMSIG_PROJECTILE_RANGE "projectile_range"
///from the base of /obj/projectile/on_range(): ()
#define COMSIG_PROJECTILE_RANGE_OUT "projectile_range_out"
Expand Down
3 changes: 3 additions & 0 deletions code/__DEFINES/maths.dm
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@
/// Gets the sign of x, returns -1 if negative, 0 if 0, 1 if positive
#define SIGN(x) ( ((x) > 0) - ((x) < 0) )

/// Returns the integer closest to 0 from a division
#define SIGNED_FLOOR_DIVISION(x, y) (SIGN(x) * FLOOR(abs(x) / y, 1))

#define CEILING(x, y) ( -round(-(x) / (y)) * (y) )

#define ROUND_UP(x) ( -round(-(x)))
Expand Down
21 changes: 18 additions & 3 deletions code/__DEFINES/projectiles.dm
Original file line number Diff line number Diff line change
Expand Up @@ -77,12 +77,27 @@
#define RETURN_PRECISE_POSITION(A) new /datum/position(A)
#define RETURN_PRECISE_POINT(A) new /datum/point(A)

#define RETURN_POINT_VECTOR(ATOM, ANGLE, SPEED) (new /datum/point/vector(ATOM, null, null, null, null, ANGLE, SPEED))
#define RETURN_POINT_VECTOR_INCREMENT(ATOM, ANGLE, SPEED, AMT) (new /datum/point/vector(ATOM, null, null, null, null, ANGLE, SPEED, AMT))

///The self charging rate of energy guns that magically recharge themselves, in watts.
#define STANDARD_ENERGY_GUN_SELF_CHARGE_RATE (0.05 * STANDARD_CELL_CHARGE)

/// Macro to turn a number of laser shots into an energy cost, based on the above define
/// e.g. LASER_SHOTS(12, STANDARD_CELL_CHARGE) means 12 shots
#define LASER_SHOTS(X, MAX_CHARGE) (((100 * MAX_CHARGE) - ((100 * MAX_CHARGE) % X)) / (100 * X)) // I wish I could just use round, but it can't be used in datum members

/// How far do the projectile hits the prone mob
#define MAX_RANGE_HIT_PRONE_TARGETS 10

/// Queued for impact deletion (simple qdel)
#define PROJECTILE_IMPACT_DELETE "impact_delete"
/// Queued for range deletion (on_range call)
#define PROJECTILE_RANGE_DELETE "range_delete"

/// Projectile either hasn't impacted anything, or pierced through the target
#define PROJECTILE_IMPACT_PASSED "impact_passed"
/// Projectile has been "deleted" before bullet_act call has occured
#define PROJECTILE_IMPACT_INTERRUPTED "impact_interrupted"
/// Projectile has successfully impacted something and is scheduled for deletion
#define PROJECTILE_IMPACT_SUCCESSFUL "impact_successful"

/// For how long projectile tracers linger
#define PROJECTILE_TRACER_DURATION 0.3 SECONDS
2 changes: 1 addition & 1 deletion code/__HELPERS/maths.dm
Original file line number Diff line number Diff line change
Expand Up @@ -238,7 +238,7 @@
return min(new_value, threshold * -1)

/// Takes two values x and y, and returns 1/((1/x) + y)
/// Useful for providing an additive modifier to a value that is used as a divisor, such as `/obj/projectile/var/speed`
/// Useful for providing an additive modifier to a value that is used as a divisor
/proc/reciprocal_add(x, y)
return 1/((1/x)+y)

Expand Down
33 changes: 15 additions & 18 deletions code/controllers/subsystem/processing/projectiles.dm
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,18 @@ PROCESSING_SUBSYSTEM_DEF(projectiles)
wait = 1
stat_tag = "PP"
flags = SS_NO_INIT|SS_TICKER
var/global_max_tick_moves = 10
var/global_pixel_speed = 2
var/global_iterations_per_move = 16

/datum/controller/subsystem/processing/projectiles/proc/set_pixel_speed(new_speed)
global_pixel_speed = new_speed
for(var/i in processing)
var/obj/projectile/P = i
if(istype(P)) //there's non projectiles on this too.
P.set_pixel_speed(new_speed)

/datum/controller/subsystem/processing/projectiles/vv_edit_var(var_name, var_value)
switch(var_name)
if(NAMEOF(src, global_pixel_speed))
set_pixel_speed(var_value)
return TRUE
else
return ..()
/*
* Maximum amount of pixels a projectile can pass per tick *unless* its a hitscan projectile.
* This prevents projectiles from turning into essentially hitscans if SSprojectiles starts chugging
* and projectiles accumulate a bunch of overtime they try to process next tick to fly through half the map.
* Shouldn't really be increased past 5 tiles per tick because this maxes out at 100 FPS (recommended as of now)
* and making a projectile faster than that will make it look jumpy because it'll be passing inconsistent
* amounts of pixels per tick.
*/
var/max_pixels_per_tick = ICON_SIZE_ALL * 5
/*
* How many pixels a projectile with a speed value of 1 passes in a tick. Currently all speed values
* assume that 1 speed = 1 tile per decisecond, but this is a variable so that admins/debuggers can edit
* in order to debug projectile behavior by evenly slowing or speeding all of them up.
*/
var/pixels_per_decisecond = ICON_SIZE_ALL
8 changes: 1 addition & 7 deletions code/datums/actions/mobs/create_legion_turrets.dm
Original file line number Diff line number Diff line change
Expand Up @@ -78,8 +78,7 @@
return
//Now we generate the tracer.
var/angle = get_angle(our_turf, target_turf)
var/datum/point/vector/V = new(our_turf.x, our_turf.y, our_turf.z, 0, 0, angle)
generate_tracer_between_points(V, V.return_vector_after_increments(6), /obj/effect/projectile/tracer/legion/tracer, 0, shot_delay, 0, 0, 0, null)
our_turf.Beam(target_turf, 'icons/effects/beam.dmi', "blood_light", time = shot_delay)
playsound(src, 'sound/machines/airlock/airlockopen.ogg', 100, TRUE)
addtimer(CALLBACK(src, PROC_REF(fire_beam), angle), shot_delay)

Expand All @@ -105,11 +104,6 @@
hitscan = TRUE
projectile_piercing = ALL

/// Used for the legion turret tracer.
/obj/effect/projectile/tracer/legion/tracer
icon = 'icons/effects/beam.dmi'
icon_state = "blood_light"

/// Used for the legion turret beam.
/obj/effect/projectile/tracer/legion
icon = 'icons/effects/beam.dmi'
Expand Down
6 changes: 3 additions & 3 deletions code/datums/actions/mobs/projectileattack.dm
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@
if(!isnum(speed_multiplier))
speed_multiplier = projectile_speed_multiplier
our_projectile.speed *= speed_multiplier
our_projectile.preparePixelProjectile(endloc, startloc, null, projectile_spread)
our_projectile.aim_projectile(endloc, startloc, null, projectile_spread)
our_projectile.firer = firer
if(target)
our_projectile.original = target
Expand Down Expand Up @@ -224,7 +224,7 @@
cooldown_time = 10 SECONDS
projectile_type = /obj/projectile/colossus/wendigo_shockwave
shot_angles = list(-20, -10, 0, 10, 20)
projectile_speed_multiplier = 4
projectile_speed_multiplier = 0.25


/datum/action/cooldown/mob_cooldown/projectile_attack/shotgun_blast/colossus
Expand Down Expand Up @@ -378,7 +378,7 @@
if(enraged)
projectile_speed_multiplier = 1
else
projectile_speed_multiplier = 1.5
projectile_speed_multiplier = 0.66
var/shots_per = 24
for(var/shoot_times in 1 to 8)
var/offset = shoot_times % 2
Expand Down
2 changes: 0 additions & 2 deletions code/datums/components/dart_insert.dm
Original file line number Diff line number Diff line change
Expand Up @@ -135,8 +135,6 @@
/datum/component/dart_insert/proc/apply_var_modifiers(obj/projectile/projectile)
var_modifiers = istype(modifier_getter) ? modifier_getter.Invoke() : list()
projectile.damage += var_modifiers["damage"]
if(var_modifiers["speed"])
var_modifiers["speed"] = reciprocal_add(projectile.speed, var_modifiers["speed"]) - projectile.speed
projectile.speed += var_modifiers["speed"]
projectile.armour_penetration += var_modifiers["armour_penetration"]
projectile.wound_bonus += var_modifiers["wound_bonus"]
Expand Down
14 changes: 7 additions & 7 deletions code/datums/components/mirv.dm
Original file line number Diff line number Diff line change
Expand Up @@ -32,12 +32,12 @@
var/turf/target_turf = get_turf(target)
for(var/turf/shootat_turf in RANGE_TURFS(radius, target) - RANGE_TURFS(radius-1, target))

var/obj/projectile/P = new projectile_type(target_turf)
var/obj/projectile/proj = new projectile_type(target_turf)
//Shooting Code:
P.range = radius+1
proj.range = radius+1
if(override_projectile_range)
P.range = override_projectile_range
P.preparePixelProjectile(shootat_turf, target)
P.firer = firer // don't hit ourself that would be really annoying
P.impacted = list(WEAKREF(target) = TRUE) // don't hit the target we hit already with the flak
P.fire()
proj.range = override_projectile_range
proj.aim_projectile(shootat_turf, target)
proj.firer = firer // don't hit ourself that would be really annoying
proj.impacted = list(WEAKREF(target) = TRUE) // don't hit the target we hit already with the flak
proj.fire()
11 changes: 7 additions & 4 deletions code/datums/components/parry.dm
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
/// Callback for special effects upon parrying
var/datum/callback/parry_callback

/datum/component/parriable_projectile/Initialize(parry_speed_mult = 0.8, parry_damage_mult = 1.15, boost_speed_mult = 0.6, boost_damage_mult = 1.5, parry_trait = TRAIT_MINING_PARRYING, grace_period = 0.25 SECONDS, datum/callback/parry_callback = null)
/datum/component/parriable_projectile/Initialize(parry_speed_mult = 1.25, parry_damage_mult = 1.15, boost_speed_mult = 1.6, boost_damage_mult = 1.5, parry_trait = TRAIT_MINING_PARRYING, grace_period = 0.25 SECONDS, datum/callback/parry_callback = null)
if(!isprojectile(parent))
return COMPONENT_INCOMPATIBLE
src.parry_speed_mult = parry_speed_mult
Expand All @@ -41,13 +41,13 @@
. = ..()

/datum/component/parriable_projectile/RegisterWithParent()
RegisterSignal(parent, COMSIG_PROJECTILE_PIXEL_STEP, PROC_REF(on_moved))
RegisterSignal(parent, COMSIG_PROJECTILE_MOVE_PROCESS_STEP, PROC_REF(on_moved))
RegisterSignal(parent, COMSIG_MOVABLE_MOVED, PROC_REF(before_move))
RegisterSignal(parent, COMSIG_PROJECTILE_BEFORE_MOVE, PROC_REF(before_move))
RegisterSignal(parent, COMSIG_PROJECTILE_SELF_PREHIT, PROC_REF(before_hit))

/datum/component/parriable_projectile/UnregisterFromParent()
UnregisterSignal(parent, list(COMSIG_PROJECTILE_PIXEL_STEP, COMSIG_MOVABLE_MOVED, COMSIG_PROJECTILE_BEFORE_MOVE, COMSIG_PROJECTILE_SELF_PREHIT))
UnregisterSignal(parent, list(COMSIG_PROJECTILE_MOVE_PROCESS_STEP, COMSIG_MOVABLE_MOVED, COMSIG_PROJECTILE_BEFORE_MOVE, COMSIG_PROJECTILE_SELF_PREHIT))

/datum/component/parriable_projectile/proc/before_move(obj/projectile/source)
SIGNAL_HANDLER
Expand All @@ -71,7 +71,7 @@

/datum/component/parriable_projectile/proc/on_moved(obj/projectile/source)
SIGNAL_HANDLER
if (!isturf(source.loc))
if (!isturf(source.loc) || parry_turfs[source.loc])
return
parry_turfs[source.loc] = world.time + grace_period
RegisterSignal(source.loc, COMSIG_CLICK, PROC_REF(on_turf_click))
Expand All @@ -95,6 +95,9 @@
attempt_parry(source, user)

/datum/component/parriable_projectile/proc/attempt_parry(obj/projectile/source, mob/user)
if (QDELETED(source) || source.deletion_queued)
return

if (SEND_SIGNAL(user, COMSIG_LIVING_PROJECTILE_PARRIED, source) & INTERCEPT_PARRY_EFFECTS)
return

Expand Down
44 changes: 22 additions & 22 deletions code/datums/components/pellet_cloud.dm
Original file line number Diff line number Diff line change
Expand Up @@ -224,10 +224,10 @@
break

///One of our pellets hit something, record what it was and check if we're done (terminated == num_pellets)
/datum/component/pellet_cloud/proc/pellet_hit(obj/projectile/P, atom/movable/firer, atom/target, Angle, hit_zone)
/datum/component/pellet_cloud/proc/pellet_hit(obj/projectile/proj, atom/movable/firer, atom/target, Angle, hit_zone)
SIGNAL_HANDLER

pellets -= P
pellets -= proj
terminated++
hits++
var/obj/item/bodypart/hit_part
Expand All @@ -237,34 +237,34 @@
hit_part = hit_carbon.get_bodypart(hit_zone)
if(hit_part)
target = hit_part
if(P.wound_bonus != CANT_WOUND) // handle wounding
if(proj.wound_bonus != CANT_WOUND) // handle wounding
// unfortunately, due to how pellet clouds handle finalizing only after every pellet is accounted for, that also means there might be a short delay in dealing wounds if one pellet goes wide
// while buckshot may reach a target or miss it all in one tick, we also have to account for possible ricochets that may take a bit longer to hit the target
if(isnull(wound_info_by_part[hit_part]))
wound_info_by_part[hit_part] = list(0, 0, 0)
wound_info_by_part[hit_part][CLOUD_POSITION_DAMAGE] += P.damage // these account for decay
wound_info_by_part[hit_part][CLOUD_POSITION_W_BONUS] += P.wound_bonus
wound_info_by_part[hit_part][CLOUD_POSITION_BW_BONUS] += P.bare_wound_bonus
P.wound_bonus = CANT_WOUND // actual wounding will be handled aggregate
wound_info_by_part[hit_part][CLOUD_POSITION_DAMAGE] += proj.damage // these account for decay
wound_info_by_part[hit_part][CLOUD_POSITION_W_BONUS] += proj.wound_bonus
wound_info_by_part[hit_part][CLOUD_POSITION_BW_BONUS] += proj.bare_wound_bonus
proj.wound_bonus = CANT_WOUND // actual wounding will be handled aggregate
else if(isobj(target))
var/obj/hit_object = target
if(hit_object.damage_deflection > P.damage || !P.damage)
if(hit_object.damage_deflection > proj.damage || !proj.damage)
damage = FALSE

LAZYADDASSOC(targets_hit[target], "hits", 1)
LAZYSET(targets_hit[target], "damage", damage)
if(targets_hit[target]["hits"] == 1)
RegisterSignal(target, COMSIG_QDELETING, PROC_REF(on_target_qdel), override=TRUE)
UnregisterSignal(P, list(COMSIG_QDELETING, COMSIG_PROJECTILE_RANGE_OUT, COMSIG_PROJECTILE_SELF_ON_HIT))
RegisterSignal(target, COMSIG_QDELETING, PROC_REF(on_target_qdel), override = TRUE)
UnregisterSignal(proj, list(COMSIG_QDELETING, COMSIG_PROJECTILE_RANGE_OUT, COMSIG_PROJECTILE_SELF_ON_HIT))
if(terminated == num_pellets)
finalize()

///One of our pellets disappeared due to hitting their max range (or just somehow got qdel'd), remove it from our list and check if we're done (terminated == num_pellets)
/datum/component/pellet_cloud/proc/pellet_range(obj/projectile/P)
/datum/component/pellet_cloud/proc/pellet_range(obj/projectile/proj)
SIGNAL_HANDLER
pellets -= P
pellets -= proj
terminated++
UnregisterSignal(P, list(COMSIG_QDELETING, COMSIG_PROJECTILE_RANGE_OUT, COMSIG_PROJECTILE_SELF_ON_HIT))
UnregisterSignal(proj, list(COMSIG_QDELETING, COMSIG_PROJECTILE_RANGE_OUT, COMSIG_PROJECTILE_SELF_ON_HIT))
if(terminated == num_pellets)
finalize()

Expand All @@ -279,18 +279,18 @@
pellet.firer = parent // don't hit ourself that would be really annoying
pellet.impacted = list(WEAKREF(parent) = TRUE) // don't hit the target we hit already with the flak
pellet.suppressed = SUPPRESSED_VERY // set the projectiles to make no message so we can do our own aggregate message
pellet.preparePixelProjectile(target, parent)
pellet.aim_projectile(target, parent)
RegisterSignal(pellet, COMSIG_PROJECTILE_SELF_ON_HIT, PROC_REF(pellet_hit))
RegisterSignals(pellet, list(COMSIG_PROJECTILE_RANGE_OUT, COMSIG_QDELETING), PROC_REF(pellet_range))
pellets += pellet
pellet.fire()
if(landmine_victim)
pellet.process_hit_loop(target)
pellet.impact(target)

///All of our pellets are accounted for, time to go target by target and tell them how many things they got hit by.
/datum/component/pellet_cloud/proc/finalize()
var/obj/projectile/P = projectile_type
var/proj_name = initial(P.name)
var/obj/projectile/proj_type = projectile_type
var/proj_name = initial(proj_type.name)

for(var/atom/target in targets_hit)
var/num_hits = targets_hit[target]["hits"]
Expand All @@ -303,24 +303,24 @@
hit_part = null //so the visible_message later on doesn't generate extra text.
else
target = hit_part.owner
if(wound_info_by_part[hit_part] && (initial(P.damage_type) == BRUTE || initial(P.damage_type) == BURN)) // so a cloud of disablers that deal stamina don't inadvertently end up causing burn wounds)
if(wound_info_by_part[hit_part] && (initial(proj_type.damage_type) == BRUTE || initial(proj_type.damage_type) == BURN)) // so a cloud of disablers that deal stamina don't inadvertently end up causing burn wounds)
var/damage_dealt = wound_info_by_part[hit_part][CLOUD_POSITION_DAMAGE]
var/w_bonus = wound_info_by_part[hit_part][CLOUD_POSITION_W_BONUS]
var/bw_bonus = wound_info_by_part[hit_part][CLOUD_POSITION_BW_BONUS]
var/wounding_type = (initial(P.damage_type) == BRUTE) ? WOUND_BLUNT : WOUND_BURN // sharpness is handled in the wound rolling
var/wounding_type = (initial(proj_type.damage_type) == BRUTE) ? WOUND_BLUNT : WOUND_BURN // sharpness is handled in the wound rolling
wound_info_by_part -= hit_part

// technically this only checks armor worn the moment that all the pellets resolve rather than as each one hits you,
// but this isn't important enough to warrant all the extra loops of mostly redundant armor checks
var/mob/living/carbon/hit_carbon = target
var/armor_factor = hit_carbon.getarmor(hit_part, initial(P.armor_flag))
var/armor_factor = hit_carbon.getarmor(hit_part, initial(proj_type.armor_flag))
armor_factor = min(ARMOR_MAX_BLOCK, armor_factor) //cap damage reduction at 90%
if(armor_factor > 0)
if(initial(P.weak_against_armour) && armor_factor >= 0)
if(initial(proj_type.weak_against_armour) && armor_factor >= 0)
armor_factor *= ARMOR_WEAKENED_MULTIPLIER
damage_dealt *= max(0, 1 - armor_factor*0.01)

hit_part.painless_wound_roll(wounding_type, damage_dealt, w_bonus, bw_bonus, initial(P.sharpness))
hit_part.painless_wound_roll(wounding_type, damage_dealt, w_bonus, bw_bonus, initial(proj_type.sharpness))

var/limb_hit_text = ""
if(hit_part)
Expand Down
4 changes: 2 additions & 2 deletions code/datums/components/splat.dm
Original file line number Diff line number Diff line change
Expand Up @@ -48,9 +48,9 @@
/datum/component/splat/UnregisterFromParent()
UnregisterSignal(parent, list(COMSIG_MOVABLE_IMPACT, COMSIG_PROJECTILE_SELF_ON_HIT))

/datum/component/splat/proc/projectile_splat(obj/projectile/source, atom/firer, atom/target, angle, hit_limb_zone, blocked)
/datum/component/splat/proc/projectile_splat(obj/projectile/source, atom/firer, atom/target, angle, hit_limb_zone, blocked, pierce_hit)
SIGNAL_HANDLER
if(blocked != 100)
if(blocked != 100 && !pierce_hit)
splat(source, target)

/datum/component/splat/proc/throw_splat(atom/movable/source, atom/hit_atom, datum/thrownthing/throwing_datum, caught)
Expand Down
6 changes: 5 additions & 1 deletion code/datums/elements/embed.dm
Original file line number Diff line number Diff line change
Expand Up @@ -98,12 +98,16 @@
* That's awful, and it'll limit us to drop-deletable shrapnels in the worry of stuff like
* arrows and harpoons being embeddable even when not let loose by their weapons.
*/
/datum/element/embed/proc/check_embed_projectile(obj/projectile/source, atom/movable/firer, atom/hit, angle, hit_zone, blocked)
/datum/element/embed/proc/check_embed_projectile(obj/projectile/source, atom/movable/firer, atom/hit, angle, hit_zone, blocked, pierce_hit)
SIGNAL_HANDLER

if (pierce_hit)
return

if(!source.can_embed_into(hit) || blocked)
Detach(source)
return // we don't care

var/payload_type = source.shrapnel_type
var/obj/item/payload = new payload_type(get_turf(hit))
payload.set_embed(source.get_embed())
Expand Down
Loading

0 comments on commit bbb7a41

Please sign in to comment.