Skip to content

Commit

Permalink
iff fire prevention component (#120)
Browse files Browse the repository at this point in the history
  • Loading branch information
AndroBetel authored Mar 4, 2024
1 parent dcb7032 commit b41ec8d
Show file tree
Hide file tree
Showing 7 changed files with 227 additions and 150 deletions.
3 changes: 3 additions & 0 deletions code/__DEFINES/dcs/signals/atom/signals_gun.dm
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,6 @@
#define COMSIG_GUN_BEFORE_FIRE "gun_before_fire"
#define COMPONENT_CANCEL_GUN_BEFORE_FIRE (1<<0) //continue full-auto/burst attempts
#define COMPONENT_HARD_CANCEL_GUN_BEFORE_FIRE (1<<1) //hard stop firing

/// Called when IFF is toggled on or off
#define COMSIG_GUN_IFF_TOGGLED "gun_iff_toggled"
6 changes: 6 additions & 0 deletions code/_onclick/hud/fullscreen.dm
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,12 @@
else
client.remove_from_screen(screen)

/mob/proc/get_maximum_view_range()
if(!client)
return world.view

var/offset = max(abs(client.pixel_x), abs(client.pixel_y))
return client.view + offset / 32

/atom/movable/screen/fullscreen
icon = 'icons/mob/hud/screen1_full.dmi'
Expand Down
105 changes: 105 additions & 0 deletions code/datums/components/iff_fire_prevention.dm
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
#define IFF_HALT_COOLDOWN 0.5 SECONDS

/// A component that prevents gun (although you can attach it to anything else that shoot projectiles) from shooting when mob from the same faction stands in the way.
/// You can also pass number of ticks, to make gun have an additional delay if firing prevention comes into play, but it is not neccesary.
/datum/component/iff_fire_prevention
var/iff_additional_fire_delay
COOLDOWN_DECLARE(iff_halt_cooldown)

/datum/component/iff_fire_prevention/Initialize(additional_fire_delay = 0)
. = ..()
iff_additional_fire_delay = additional_fire_delay


/datum/component/iff_fire_prevention/RegisterWithParent()
. = ..()
RegisterSignal(parent, COMSIG_GUN_BEFORE_FIRE, PROC_REF(check_firing_lane))
RegisterSignal(parent, COMSIG_GUN_IFF_TOGGLED, PROC_REF(handle_iff_toggle))

/datum/component/iff_fire_prevention/UnregisterFromParent()
. = ..()
UnregisterSignal(parent, list(
COMSIG_GUN_BEFORE_FIRE,
COMSIG_GUN_IFF_TOGGLED
))

/datum/component/iff_fire_prevention/proc/check_firing_lane(obj/firing_weapon, obj/projectile/projectile_to_fire, atom/target, mob/living/user)
SIGNAL_HANDLER

var/angle = get_angle(user, target)

var/range_to_check = user.get_maximum_view_range()

var/extended_target_turf = get_angle_target_turf(user, angle, range_to_check)

var/turf/starting_turf = get_turf(user)

if(!starting_turf || !extended_target_turf)
return COMPONENT_CANCEL_GUN_BEFORE_FIRE

var/list/checked_turfs = getline2(starting_turf, extended_target_turf)

//Don't shoot yourself, thanks
if(target == user)
if(COOLDOWN_FINISHED(src, iff_halt_cooldown) && user.client)
playsound_client(user.client, 'sound/weapons/smartgun_fail.ogg', src, 25)
to_chat(user, SPAN_WARNING("[firing_weapon] halts firing as an IFF marked target crosses your field of fire!"))
COOLDOWN_START(src, iff_halt_cooldown, IFF_HALT_COOLDOWN)
if(iff_additional_fire_delay)
var/obj/item/weapon/gun/gun = firing_weapon
if(istype(gun))
LAZYSET(user.fire_delay_next_fire, gun, world.time + iff_additional_fire_delay)
return COMPONENT_CANCEL_GUN_BEFORE_FIRE

//At some angles (scatter or otherwise) the original target is not in checked_turfs so we put it in there in order based on distance from user
//If we are literally clicking on someone with IFF then we don't want to fire, feels funny as a user otherwise
if(projectile_to_fire.original)
var/turf/original_target_turf = get_turf(projectile_to_fire.original)

if(original_target_turf && !(original_target_turf in checked_turfs))
var/user_to_target_dist = get_dist(starting_turf, original_target_turf)
var/list/temp_checked_turfs = checked_turfs.Copy()
checked_turfs = list()

for(var/turf/checked_turf as anything in temp_checked_turfs)
if(!(original_target_turf in checked_turfs) && user_to_target_dist < get_dist(starting_turf, checked_turf))
checked_turfs += original_target_turf

checked_turfs += checked_turf

for(var/turf/checked_turf as anything in checked_turfs)

//Wall, should block the bullet so we're good to stop checking.
if(istype(checked_turf, /turf/closed))
return

for(var/mob/living/checked_living in checked_turf)
if(checked_living == user) // sometimes it still happens
continue
if(checked_living.body_position == LYING_DOWN && projectile_to_fire.original != checked_living)
continue

if(checked_living.get_target_lock(user.faction_group))
if(HAS_TRAIT(checked_living, TRAIT_CLOAKED))
continue
if(COOLDOWN_FINISHED(src, iff_halt_cooldown) && user.client)
playsound_client(user.client, 'sound/weapons/smartgun_fail.ogg', src, 25)
to_chat(user, SPAN_WARNING("[firing_weapon] halts firing as an IFF marked target crosses your field of fire!"))
COOLDOWN_START(src, iff_halt_cooldown, IFF_HALT_COOLDOWN)
if(iff_additional_fire_delay)
var/obj/item/weapon/gun/gun = firing_weapon
if(istype(gun))
LAZYSET(user.fire_delay_next_fire, gun, world.time + iff_additional_fire_delay)
return COMPONENT_CANCEL_GUN_BEFORE_FIRE

return //if we have a target we *can* hit and find it before any IFF targets we want to fire

/// Disable fire prevention when IFF is toggled off and other way around
/datum/component/iff_fire_prevention/proc/handle_iff_toggle(obj/gun, iff_enabled)
SIGNAL_HANDLER
if(iff_enabled)
RegisterSignal(parent, COMSIG_GUN_BEFORE_FIRE, PROC_REF(check_firing_lane))
else
UnregisterSignal(parent, COMSIG_GUN_BEFORE_FIRE)

#undef IFF_HALT_COOLDOWN
12 changes: 12 additions & 0 deletions code/modules/cm_marines/smartgun_mount.dm
Original file line number Diff line number Diff line change
Expand Up @@ -512,6 +512,8 @@
burst_scatter_mult = SCATTER_AMOUNT_TIER_7
update_icon()
AddComponent(/datum/component/automatedfire/autofire, fire_delay, burst_fire_delay, burst_amount, gun_firemode, autofire_slow_mult, CALLBACK(src, PROC_REF(set_burst_firing)), CALLBACK(src, PROC_REF(reset_fire)), CALLBACK(src, PROC_REF(try_fire)), CALLBACK(src, PROC_REF(display_ammo)))
AddComponent(/datum/component/iff_fire_prevention)


/obj/structure/machinery/m56d_hmg/Destroy(force) //Make sure we pick up our trash.
operator?.unset_interaction()
Expand Down Expand Up @@ -723,6 +725,16 @@
final_angle += rand(-total_scatter_angle, total_scatter_angle)
target = get_angle_target_turf(T, final_angle, 30)

var/before_fire_cancel = SEND_SIGNAL(src, COMSIG_GUN_BEFORE_FIRE, in_chamber, target, operator)

if(before_fire_cancel)
if(before_fire_cancel & COMPONENT_CANCEL_GUN_BEFORE_FIRE)
return AUTOFIRE_CONTINUE

if(before_fire_cancel & COMPONENT_HARD_CANCEL_GUN_BEFORE_FIRE)
return


in_chamber.weapon_cause_data = create_cause_data(initial(name), operator)
in_chamber.setDir(dir)
in_chamber.def_zone = pick("chest","chest","chest","head")
Expand Down
96 changes: 58 additions & 38 deletions code/modules/projectiles/gun.dm
Original file line number Diff line number Diff line change
Expand Up @@ -234,7 +234,6 @@
/// The multiplier for how much slower this should fire in automatic mode. 1 is normal, 1.2 is 20% slower, 2 is 100% slower, etc. Protected due to it never needing to be edited.
VAR_PROTECTED/autofire_slow_mult = 1


/**
* An assoc list where the keys are fire delay group string defines
* and the keys are when the guns of the group can be fired again
Expand Down Expand Up @@ -1142,8 +1141,9 @@ and you're good to go.
flags_gun_features &= ~GUN_BURST_FIRING
return NONE

apply_bullet_effects(projectile_to_fire, user, reflex, dual_wield) //User can be passed as null.
SEND_SIGNAL(projectile_to_fire, COMSIG_BULLET_USER_EFFECTS, user)
var/original_scatter = projectile_to_fire.scatter
var/original_accuracy = projectile_to_fire.accuracy
apply_bullet_scatter(projectile_to_fire, user, reflex, dual_wield) //User can be passed as null.

curloc = get_turf(user)
if(QDELETED(original_target)) //If the target's destroyed, shoot at where it was last.
Expand Down Expand Up @@ -1192,14 +1192,25 @@ and you're good to go.
return NONE

var/before_fire_cancel = SEND_SIGNAL(src, COMSIG_GUN_BEFORE_FIRE, projectile_to_fire, target, user)

if(before_fire_cancel)

//yeah we revert these since we are not going to shoot anyway
projectile_to_fire.scatter = original_scatter
projectile_to_fire.accuracy = original_accuracy

if(before_fire_cancel & COMPONENT_CANCEL_GUN_BEFORE_FIRE)
return TRUE

if(before_fire_cancel & COMPONENT_HARD_CANCEL_GUN_BEFORE_FIRE)
return NONE

apply_bullet_effects(projectile_to_fire, user, reflex, dual_wield) //User can be passed as null.
SEND_SIGNAL(projectile_to_fire, COMSIG_BULLET_USER_EFFECTS, user)

projectile_to_fire.firer = user
if(isliving(user))
projectile_to_fire.def_zone = user.zone_selected

play_firing_sounds(projectile_to_fire, user)

if(targloc != curloc)
Expand Down Expand Up @@ -1244,7 +1255,7 @@ and you're good to go.
return TRUE //Nothing else to do here, time to cancel out.
return TRUE

#define EXECUTION_CHECK (attacked_mob.stat == UNCONSCIOUS || attacked_mob.is_mob_restrained()) && ((user.a_intent == INTENT_GRAB) || (user.a_intent == INTENT_DISARM)) && !(length(user.faction_group & attacked_mob.faction_group)) && ishuman(attacked_mob)
#define EXECUTION_CHECK (attacked_mob.stat == UNCONSCIOUS || attacked_mob.is_mob_restrained()) && ((user.a_intent == INTENT_GRAB)||(user.a_intent == INTENT_DISARM))

/obj/item/weapon/gun/afterattack(atom/target, mob/user, proximity_flag, click_parameters)
if(!proximity_flag)
Expand Down Expand Up @@ -1400,6 +1411,15 @@ and you're good to go.
click_empty(user)
break

//Checking if we even can PB the mob, only for the first projectile because why check the rest
if(bullets_fired == 1)
var/before_fire_cancel = SEND_SIGNAL(src, COMSIG_GUN_BEFORE_FIRE, projectile_to_fire, attacked_mob, user)
if(before_fire_cancel)
if(before_fire_cancel & COMPONENT_CANCEL_GUN_BEFORE_FIRE)
return TRUE
if(before_fire_cancel & COMPONENT_HARD_CANCEL_GUN_BEFORE_FIRE)
return NONE

if(SEND_SIGNAL(projectile_to_fire.ammo, COMSIG_AMMO_POINT_BLANK, attacked_mob, projectile_to_fire, user, src) & COMPONENT_CANCEL_AMMO_POINT_BLANK)
flags_gun_features &= ~GUN_BURST_FIRING
return TRUE
Expand All @@ -1411,7 +1431,7 @@ and you're good to go.

var/damage_buff = BASE_BULLET_DAMAGE_MULT
//if target is lying or unconscious - add damage bonus
if(!(attacked_mob.mobility_flags & MOBILITY_STAND) || attacked_mob.stat == UNCONSCIOUS)
if(attacked_mob.body_position == LYING_DOWN || attacked_mob.stat == UNCONSCIOUS)
damage_buff += BULLET_DAMAGE_MULT_TIER_4
projectile_to_fire.damage *= damage_buff //Multiply the damage for point blank.
if(bullets_fired == 1) //First shot gives the PB message.
Expand All @@ -1421,6 +1441,10 @@ and you're good to go.
user.track_shot(initial(name))
apply_bullet_effects(projectile_to_fire, user, bullets_fired, dual_wield) //We add any damage effects that we need.

projectile_to_fire.firer = user
if(isliving(user))
projectile_to_fire.def_zone = user.zone_selected

play_firing_sounds(projectile_to_fire, user)

SEND_SIGNAL(projectile_to_fire, COMSIG_BULLET_USER_EFFECTS, user)
Expand Down Expand Up @@ -1623,6 +1647,32 @@ not all weapons use normal magazines etc. load_into_chamber() itself is designed

//This proc applies some bonus effects to the shot/makes the message when a bullet is actually fired.
/obj/item/weapon/gun/proc/apply_bullet_effects(obj/projectile/projectile_to_fire, mob/user, reflex = 0, dual_wield = 0)
if(wield_delay > 0 && (world.time < wield_time || world.time < pull_time))
var/old_time = max(wield_time, pull_time) - wield_delay
var/new_time = world.time
var/pct_settled = 1 - (new_time-old_time + 1)/wield_delay
if(delay_style & WEAPON_DELAY_ACCURACY)
var/accuracy_debuff = 1 + (SETTLE_ACCURACY_MULTIPLIER - 1) * pct_settled
projectile_to_fire.accuracy /=accuracy_debuff
if(delay_style & WEAPON_DELAY_SCATTER)
var/scatter_debuff = 1 + (SETTLE_SCATTER_MULTIPLIER - 1) * pct_settled
projectile_to_fire.scatter *= scatter_debuff

projectile_to_fire.damage = round(projectile_to_fire.damage * damage_mult) // Apply gun damage multiplier to projectile damage

// Apply effective range and falloffs/buildups
projectile_to_fire.damage_falloff = damage_falloff_mult * projectile_to_fire.ammo.damage_falloff
projectile_to_fire.damage_buildup = damage_buildup_mult * projectile_to_fire.ammo.damage_buildup

projectile_to_fire.effective_range_min = effective_range_min + projectile_to_fire.ammo.effective_range_min //Add on ammo-level value, if specified.
projectile_to_fire.effective_range_max = effective_range_max + projectile_to_fire.ammo.effective_range_max //Add on ammo-level value, if specified.

projectile_to_fire.shot_from = src

return 1

//This proc calculates scatter and accuracy
/obj/item/weapon/gun/proc/apply_bullet_scatter(obj/projectile/projectile_to_fire, mob/user, reflex = 0, dual_wield = 0)
var/gun_accuracy_mult = accuracy_mult_unwielded
var/gun_scatter = scatter_unwielded

Expand Down Expand Up @@ -1651,35 +1701,7 @@ not all weapons use normal magazines etc. load_into_chamber() itself is designed
projectile_to_fire.accuracy = round(projectile_to_fire.accuracy * gun_accuracy_mult) // Apply gun accuracy multiplier to projectile accuracy
projectile_to_fire.scatter += gun_scatter

if(wield_delay > 0 && (world.time < wield_time || world.time < pull_time))
var/old_time = max(wield_time, pull_time) - wield_delay
var/new_time = world.time
var/pct_settled = 1 - (new_time-old_time + 1)/wield_delay
if(delay_style & WEAPON_DELAY_ACCURACY)
var/accuracy_debuff = 1 + (SETTLE_ACCURACY_MULTIPLIER - 1) * pct_settled
projectile_to_fire.accuracy /=accuracy_debuff
if(delay_style & WEAPON_DELAY_SCATTER)
var/scatter_debuff = 1 + (SETTLE_SCATTER_MULTIPLIER - 1) * pct_settled
projectile_to_fire.scatter *= scatter_debuff

projectile_to_fire.damage = round(projectile_to_fire.damage * damage_mult) // Apply gun damage multiplier to projectile damage

// Apply effective range and falloffs/buildups
projectile_to_fire.damage_falloff = damage_falloff_mult * projectile_to_fire.ammo.damage_falloff
projectile_to_fire.damage_buildup = damage_buildup_mult * projectile_to_fire.ammo.damage_buildup

projectile_to_fire.effective_range_min = effective_range_min + projectile_to_fire.ammo.effective_range_min //Add on ammo-level value, if specified.
projectile_to_fire.effective_range_max = effective_range_max + projectile_to_fire.ammo.effective_range_max //Add on ammo-level value, if specified.

projectile_to_fire.shot_from = src

if(user)
projectile_to_fire.firer = user
if(isliving(user))
projectile_to_fire.def_zone = user.zone_selected

return 1

/// When the gun is about to shoot this is called to play the specific gun's firing sound. Requires the firing projectile and the gun's user as the first and second argument
/obj/item/weapon/gun/proc/play_firing_sounds(obj/projectile/projectile_to_fire, mob/user)
if(!user) //The gun only messages when fired by a user.
return
Expand All @@ -1694,16 +1716,14 @@ not all weapons use normal magazines etc. load_into_chamber() itself is designed
//Guns with low ammo have their firing sound
var/firing_sndfreq = (current_mag && (current_mag.current_rounds / current_mag.max_rounds) > GUN_LOW_AMMO_PERCENTAGE) ? FALSE : SOUND_FREQ_HIGH

firing_sndfreq = in_chamber.ammo.firing_freq_offset ? in_chamber.ammo.firing_freq_offset : firing_sndfreq

//firing from an attachment
if(active_attachable && active_attachable.flags_attach_features & ATTACH_PROJECTILE)
if(active_attachable.fire_sound) //If we're firing from an attachment, use that noise instead.
playsound(user, active_attachable.fire_sound, 50)
else
if(!(flags_gun_features & GUN_SILENCED))
if (firing_sndfreq && fire_rattle)
playsound(user, fire_rattle, firesound_volume, FALSE) //if the gun has a unique 'mag rattle' SFX play that instead of pitch shifting.
playsound(user, fire_rattle, firesound_volume, FALSE)//if the gun has a unique 'mag rattle' SFX play that instead of pitch shifting.
else
playsound(user, actual_sound, firesound_volume, firing_sndfreq)
else
Expand Down
Loading

0 comments on commit b41ec8d

Please sign in to comment.