Reworks Tasers (doesn't give them back to Security though) Because Funny (#88347)

## About The Pull Request

https://github.com/user-attachments/assets/ef727913-6568-4705-91ca-330e7a1bb177

- Electrodes now have a physical representation in the form of a beam

- When you are hit with an electrode you will rapidly lose stamina while
the beam is attached

- The electrode, being physical, can be interacted with
   - If you are stun immune, you can resist to remove them
   - If you get out of range of them, they'll remove themselves
   - You can help intent on someone to remove them
- If someone runs into them, they will become tangled in them (being
partially stunned themselves, but removing them)

- Yes, this affects AI turrets. AI turret electrodes are stronger (they
do double the amount of stamina damage)
- Turret tasers will avoid tasing the same target for infinite time, and
will attempt to avoid tasing the same target

- Handheld tasers can be cancelled early by clicking on the target again

- Tasers will drain power as they continue to tase a target

## Why It's Good For The Game

Tasers are pretty one note so I thought it'd be cool to add some depth
to them while making them function more like... well... a taser.

<Details>

<Summary> Original idea behind this </Summary>

I had an idea, in that we could give security more specialized gear
setups (a la Goon) - putting stuff like tasers in a support weapon
position while batons sit in a "front line" position, then allowing
people to pick and choose at shift start

This way we could balance certain aspects of the security toolkit
(*cough cough batons*) while still allowing newer players to be
effective in a security position

As right now, if you want to contribute as a security officer, you
**have** to know your way around melee combat - lest you get disarmed
and owned, which is why it's so strong.

Buuuuut if newer players had the option to pick a primarily ranged
option (with its own strengths and weaknesses), or an option that is
most effective when paired with a friend, we could bring the baton
strength down accordingly.

</Details>

## Changelog

🆑 Melbert
balance: Tasers are now more realistic
del: Electrodes are no longer in the hallucination projectile pool
/🆑
# Conflicts:
#	code/modules/projectiles/projectile/energy/stun.dm
#	code/modules/vehicles/mecha/equipment/weapons/weapons.dm
#	icons/effects/beam.dmi
This commit is contained in:
MrMelbert
2024-12-21 15:01:37 -07:00
committed by StrangeWeirdKitten
parent 9e4220fad0
commit f7d6aec68d
7 changed files with 403 additions and 53 deletions
@@ -483,13 +483,17 @@ DEFINE_BITFIELD(turret_flags, list(
else if(iscarbon(A))
var/mob/living/carbon/C = A
//If not emagged, only target carbons that can use items
if(mode != TURRET_LETHAL && (C.stat || C.handcuffed || !(C.mobility_flags & MOBILITY_USE)))
continue
//If emagged, target all but dead carbons
if(mode == TURRET_LETHAL && C.stat == DEAD)
continue
switch(mode)
//If not emagged, only target carbons that can use items
if(TURRET_STUN)
if(!(C.mobility_flags & MOBILITY_USE))
continue
if(HAS_TRAIT(C, TRAIT_INCAPACITATED))
continue
//If emagged, target all but dead carbons
if(TURRET_LETHAL)
if(C.stat == DEAD)
continue
//if the target is a human and not in our faction, analyze threat level
if(ishuman(C) && !in_faction(C))
@@ -840,6 +844,7 @@ DEFINE_BITFIELD(turret_flags, list(
return TRUE
/obj/machinery/porta_turret/ai
scan_range = /obj/projectile/energy/electrode/ai_turrets::range + 1
turret_flags = TURRET_FLAG_SHOOT_CRIMINALS | TURRET_FLAG_SHOOT_ANOMALOUS | TURRET_FLAG_SHOOT_HEADS
/obj/machinery/porta_turret/ai/assess_perp(mob/living/carbon/human/perp)
@@ -220,32 +220,6 @@
afflicted.adjustStaminaLoss(20)
afflicted.adjust_eye_blur(4 SECONDS)
/obj/projectile/hallucination/taser
name = "electrode"
damage_type = BURN
hal_icon_state = "spark"
color = COLOR_YELLOW
hal_fire_sound = 'sound/items/weapons/taser.ogg'
hal_hitsound = 'sound/items/weapons/taserhit.ogg'
hal_hitsound_wall = null
hal_impact_effect = null
hal_impact_effect_wall = null
/obj/projectile/hallucination/taser/apply_effect_to_hallucinator(mob/living/afflicted)
afflicted.Paralyze(10 SECONDS)
afflicted.adjust_stutter(40 SECONDS)
if(HAS_TRAIT(afflicted, TRAIT_HULK))
afflicted.say(pick(
";RAAAAAAAARGH!",
";HNNNNNNNNNGGGGGGH!",
";GWAAAAAAAARRRHHH!",
"NNNNNNNNGGGGGGGGHH!",
";AAAAAAARRRGH!"),
forced = "hulk (hallucinating)",
)
else if(!afflicted.check_stun_immunity(CANKNOCKDOWN))
addtimer(CALLBACK(afflicted, TYPE_PROC_REF(/mob/living/carbon, do_jitter_animation), 20), 0.5 SECONDS)
/obj/projectile/hallucination/disabler
name = "disabler beam"
damage_type = STAMINA
@@ -16,6 +16,9 @@
/obj/item/ammo_casing/energy/electrode/old
e_cost = LASER_SHOTS(1, STANDARD_CELL_CHARGE)
/obj/item/ammo_casing/energy/electrode/ai_turrets
projectile_type = /obj/projectile/energy/electrode/ai_turrets
/obj/item/ammo_casing/energy/disabler
projectile_type = /obj/projectile/beam/disabler
select_name = "disable"
@@ -136,7 +136,7 @@
inhand_icon_state = "turretlaser"
slot_flags = null
w_class = WEIGHT_CLASS_HUGE
ammo_type = list(/obj/item/ammo_casing/energy/electrode, /obj/item/ammo_casing/energy/laser)
ammo_type = list(/obj/item/ammo_casing/energy/electrode/ai_turrets, /obj/item/ammo_casing/energy/laser)
weapon_weight = WEAPON_HEAVY
trigger_guard = TRIGGER_GUARD_NONE
ammo_x_offset = 2
@@ -2,30 +2,397 @@
name = "electrode"
icon_state = "spark"
color = COLOR_YELLOW
stamina = 70 // SKYRAT EDIT CHANGE
paralyze = 10 SECONDS
stutter = 10 SECONDS
jitter = 40 SECONDS
hitsound = 'sound/items/weapons/taserhit.ogg'
range = 7
range = 5
reflectable = FALSE
tracer_type = /obj/effect/projectile/tracer/stun
muzzle_type = /obj/effect/projectile/muzzle/stun
impact_type = /obj/effect/projectile/impact/stun
/// How much stamina damage will the tase deal in 1 second
VAR_PROTECTED/tase_stamina = 60
/// Electrodes that follow the projectile
VAR_PRIVATE/datum/weakref/beam_weakref
/// We need to track who was the ORIGINAL firer of the projectile specifically to ensure deflects work correctly
VAR_PRIVATE/datum/weakref/initial_firer_weakref
/obj/projectile/energy/electrode/on_hit(atom/target, blocked = 0, pierce_hit)
/obj/projectile/energy/electrode/is_hostile_projectile()
return TRUE
/obj/projectile/energy/electrode/Destroy()
QDEL_NULL(beam_weakref)
return ..()
/obj/projectile/energy/electrode/fire(fire_angle, atom/direct_target)
if(firer)
beam_weakref = WEAKREF(firer.Beam(
BeamTarget = src,
icon = 'icons/effects/beam.dmi',
icon_state = "electrodes_nozap",
maxdistance = maximum_range + 1,
beam_type = /obj/effect/ebeam/electrodes_nozap,
))
initial_firer_weakref = WEAKREF(firer)
return ..()
/obj/projectile/energy/electrode/on_hit(mob/living/target, blocked = 0, pierce_hit)
. = ..()
if(!ismob(target) || blocked >= 100) //Fully blocked by mob or collided with dense object - burst into sparks!
do_sparks(1, TRUE, src)
else if(iscarbon(target))
var/mob/living/carbon/C = target
C.adjust_confusion_up_to(15 SECONDS, 30 SECONDS) // SKYRAT EDIT ADDITION - Electrode jitteriness
C.add_mood_event("tased", /datum/mood_event/tased)
SEND_SIGNAL(C, COMSIG_LIVING_MINOR_SHOCK)
if(HAS_TRAIT(C, TRAIT_HULK))
C.say(pick(";RAAAAAAAARGH!", ";HNNNNNNNNNGGGGGGH!", ";GWAAAAAAAARRRHHH!", "NNNNNNNNGGGGGGGGHH!", ";AAAAAAARRRGH!" ), forced = "hulk")
else if(!C.check_stun_immunity(CANKNOCKDOWN))
addtimer(CALLBACK(C, TYPE_PROC_REF(/mob/living/carbon, do_jitter_animation), 20), 0.5 SECONDS)
if(pierce_hit)
return
if(. == BULLET_ACT_BLOCK || blocked >= 100 || !isliving(target))
return
// we need a "from", otherwise, where does the electricity come from?
if(isnull(fired_from))
target.visible_message(
span_warning("[src]\s collide with [target] harmlessly[isfloorturf(target.loc) ? ", before falling to [target.loc]" : ""]."),
span_notice("[src] collide with you harmlessly[isfloorturf(target.loc) ? ", before falling to [target.loc]" : ""]."),
)
return
do_sparks(1, TRUE, src)
do_sparks(1, TRUE, fired_from)
target.apply_status_effect(
/*type = *//datum/status_effect/tased,
/*taser = */fired_from,
/*firer = */initial_firer_weakref?.resolve() || firer,
/*tase_stamina = */tase_stamina,
/*energy_drain = */STANDARD_CELL_CHARGE * 0.05,
/*electrode_name = */"\the [src]\s",
/*tase_range = */maximum_range + 1,
)
/obj/projectile/energy/electrode/on_range() //to ensure the bolt sparks when it reaches the end of its range if it didn't hit a target yet
do_sparks(1, TRUE, src)
..()
return ..()
/obj/projectile/energy/electrode/ai_turrets
tase_stamina = 120
/// Status effect tracking being tased by someone!
/datum/status_effect/tased
id = "being_tased"
status_type = STATUS_EFFECT_MULTIPLE
alert_type = null
tick_interval = 0.25 SECONDS
on_remove_on_mob_delete = TRUE
/// What atom is tasing us?
VAR_PRIVATE/datum/taser
/// What atom is using the atom tasing us? Sometimes the same as the taser, such as with turrets.
VAR_PRIVATE/atom/movable/firer
/// The beam datum representing the taser electrodes
VAR_PRIVATE/datum/beam/tase_line
/// How much stamina damage does it aim to cause in a second?
VAR_FINAL/stamina_per_second = 80
/// How much energy does the taser use per tick?
VAR_FINAL/energy_drain = STANDARD_CELL_CHARGE * 0.05
/// What do we name the electrodes?
VAR_FINAL/electrode_name
/// How far can the taser reach?
VAR_FINAL/tase_range = 6
/datum/status_effect/tased/on_creation(
mob/living/new_owner,
datum/fired_from,
atom/movable/firer,
tase_stamina = 80,
energy_drain = STANDARD_CELL_CHARGE * 0.05,
electrode_name = "the electrodes",
tase_range = 6,
)
if(isnull(fired_from) || isnull(firer) || !can_tase_with(fired_from))
qdel(src)
return
src.stamina_per_second = tase_stamina
src.energy_drain = energy_drain
src.electrode_name = electrode_name
src.tase_range = tase_range
. = ..()
if(!.)
return
set_taser(fired_from)
set_firer(firer)
/// Checks if the passed atom is captable of being used to tase someone
/datum/status_effect/tased/proc/can_tase_with(datum/with_what)
if(istype(with_what, /obj/item/gun/energy))
var/obj/item/gun/energy/taser_gun = with_what
if(isnull(taser_gun.cell))
return FALSE
else if(istype(with_what, /obj/machinery))
var/obj/machinery/taser_machine = with_what
if(!taser_machine.is_operational)
return FALSE
return TRUE
/// Actually does the tasing with the passed atom
/// Returns TRUE if the tasing was successful, FALSE if it failed
/datum/status_effect/tased/proc/do_tase_with(atom/with_what, seconds_between_ticks)
if(!can_see(taser, owner, 5))
return FALSE
if(istype(with_what, /obj/item/gun/energy))
var/obj/item/gun/energy/taser_gun = with_what
if(!taser_gun.cell?.use(energy_drain * seconds_between_ticks))
return FALSE
taser_gun.update_appearance()
return TRUE
if(istype(taser, /obj/machinery))
var/obj/machinery/taser_machine = taser
if(!taser_machine.is_operational)
return FALSE
if(!taser_machine.use_energy(energy_drain * seconds_between_ticks, force = FALSE))
return FALSE
return TRUE
if(istype(taser, /obj/item/mecha_parts/mecha_equipment))
var/obj/item/mecha_parts/mecha_equipment/taser_equipment = taser
if(!taser_equipment.chassis \
|| !taser_equipment.active \
|| taser_equipment.get_integrity() <= 1 \
|| taser_equipment.chassis.is_currently_ejecting \
|| taser_equipment.chassis.equipment_disabled \
|| !taser_equipment.chassis.use_energy(energy_drain * seconds_between_ticks))
return FALSE
return TRUE
return TRUE
/datum/status_effect/tased/on_apply()
if(issilicon(owner) || isbot(owner) || isdrone(owner) || HAS_TRAIT(owner, TRAIT_PIERCEIMMUNE))
owner.visible_message(span_warning("[capitalize(electrode_name)] fail to catch [owner][isfloorturf(owner.loc) ? ", falling to [owner.loc]" : ""]!"))
return FALSE
RegisterSignal(owner, COMSIG_LIVING_RESIST, PROC_REF(try_remove_taser))
RegisterSignal(owner, COMSIG_CARBON_PRE_MISC_HELP, PROC_REF(someome_removing_taser))
SEND_SIGNAL(owner, COMSIG_LIVING_MINOR_SHOCK)
if(!owner.has_status_effect(type))
// does not use the status effect api because we snowflake it a bit
owner.throw_alert(type, /atom/movable/screen/alert/tazed)
owner.add_mood_event("tased", /datum/mood_event/tased)
owner.add_movespeed_modifier(/datum/movespeed_modifier/being_tased)
if(!HAS_TRAIT(owner, TRAIT_ANALGESIA))
owner.emote("scream")
if(HAS_TRAIT(owner, TRAIT_HULK))
owner.say(pick(
";RAAAAAAAARGH!",
";HNNNNNNNNNGGGGGGH!",
";GWAAAAAAAARRRHHH!",
"NNNNNNNNGGGGGGGGHH!",
";AAAAAAARRRGH!",
), forced = "hulk")
if(ishuman(owner))
var/mob/living/carbon/human/human_owner = owner
human_owner.force_say()
return TRUE
/datum/status_effect/tased/on_remove()
if(istype(taser, /obj/machinery/porta_turret))
var/obj/machinery/porta_turret/taser_turret = taser
taser_turret.manual_control = initial(taser_turret.manual_control)
taser_turret.always_up = initial(taser_turret.always_up)
taser_turret.check_should_process()
else if(istype(taser, /obj/machinery/power/emitter))
var/obj/machinery/power/emitter/taser_emitter = taser
taser_emitter.manual = initial(taser_emitter.manual)
var/mob/living/mob_firer = firer
if(istype(mob_firer))
mob_firer.remove_movespeed_modifier(/datum/movespeed_modifier/tasing_someone)
if(!QDELING(owner) && !owner.has_status_effect(type))
owner.adjust_jitter_up_to(10 SECONDS, 1 MINUTES)
owner.remove_movespeed_modifier(/datum/movespeed_modifier/being_tased)
owner.clear_alert(type)
taser = null
firer = null
QDEL_NULL(tase_line)
/datum/status_effect/tased/tick(seconds_between_ticks)
if(!do_tase_with(taser, seconds_between_ticks))
end_tase()
return
owner.adjust_stutter_up_to(10 SECONDS, 20 SECONDS)
owner.adjust_jitter_up_to(20 SECONDS, 30 SECONDS)
if(owner.stat <= SOFT_CRIT)
owner.do_jitter_animation(INFINITY) // maximum POWER
// You are damp, that's bad when you're being tased
if(owner.fire_stacks < 0)
owner.apply_damage(max(1, owner.fire_stacks * -0.5 * seconds_between_ticks), FIRE, spread_damage = TRUE)
if(SPT_PROB(25, seconds_between_ticks))
do_sparks(1, FALSE, owner)
// clumsy people might hit their head while being tased
if(HAS_TRAIT(owner, TRAIT_CLUMSY) && owner.body_position == LYING_DOWN && SPT_PROB(20, seconds_between_ticks))
owner.apply_damage(10, BRUTE, BODY_ZONE_HEAD)
playsound(owner, 'sound/effects/tableheadsmash.ogg', 75, TRUE)
// the actual stunning is here
if(!owner.check_stun_immunity(CANSTUN|CANKNOCKDOWN))
owner.apply_damage(stamina_per_second * seconds_between_ticks, STAMINA)
/// Sets the passed atom as the "taser"
/datum/status_effect/tased/proc/set_taser(datum/new_taser)
taser = new_taser
RegisterSignals(taser, list(COMSIG_QDELETING, COMSIG_ITEM_DROPPED, COMSIG_ITEM_EQUIPPED), PROC_REF(end_tase))
RegisterSignal(taser, COMSIG_GUN_TRY_FIRE, PROC_REF(block_firing))
// snowflake cases! yay!
if(istype(taser, /obj/machinery/porta_turret))
var/obj/machinery/porta_turret/taser_turret = taser
taser_turret.manual_control = TRUE
taser_turret.always_up = TRUE
else if(istype(taser, /obj/machinery/power/emitter))
var/obj/machinery/power/emitter/taser_emitter = taser
taser_emitter.manual = TRUE
/// Sets the passed atom as the person operating the taser, the "firer"
/datum/status_effect/tased/proc/set_firer(atom/new_firer)
firer = new_firer
if(taser != firer) // Turrets, notably, are both
RegisterSignal(firer, COMSIG_QDELETING, PROC_REF(end_tase))
RegisterSignal(firer, COMSIG_MOB_CLICKON, PROC_REF(user_cancel_tase))
// Ensures AI mobs or turrets don't tase players until they run out of power
var/mob/living/mob_firer = new_firer
if(!istype(mob_firer) || isnull(mob_firer.client))
// If multiple things are tasing the same mob, give up sooner, so they can select a new target potentially
addtimer(CALLBACK(src, PROC_REF(end_tase)), (owner.has_status_effect(type) != src) ? 2 SECONDS : 8 SECONDS)
if(istype(mob_firer))
mob_firer.add_movespeed_modifier(/datum/movespeed_modifier/tasing_someone)
if(firer == owner)
return
tase_line = firer.Beam(
BeamTarget = owner,
icon = 'icons/effects/beam.dmi',
icon_state = "electrodes",
maxdistance = tase_range,
beam_type = /obj/effect/ebeam/reacting/electrodes,
)
RegisterSignal(tase_line, COMSIG_BEAM_ENTERED, PROC_REF(disrupt_tase))
RegisterSignal(tase_line, COMSIG_QDELETING, PROC_REF(end_tase))
// moves the tase beam up or down if the target moves up or down
tase_line.RegisterSignal(owner, COMSIG_LIVING_SET_BODY_POSITION, TYPE_PROC_REF(/datum/beam, redrawing))
/datum/status_effect/tased/proc/block_firing(...)
SIGNAL_HANDLER
return COMPONENT_CANCEL_GUN_FIRE
/datum/status_effect/tased/proc/user_cancel_tase(mob/living/source, atom/clicked_on, modifiers)
SIGNAL_HANDLER
if(clicked_on != owner)
return NONE
if(LAZYACCESS(modifiers, SHIFT_CLICK))
return NONE
end_tase()
source.changeNext_move(CLICK_CD_GRABBING)
return COMSIG_MOB_CANCEL_CLICKON
/datum/status_effect/tased/proc/end_tase(...)
SIGNAL_HANDLER
if(QDELING(src))
return
owner.visible_message(
span_warning("[capitalize(electrode_name)] stop shocking [owner][isfloorturf(owner.loc) ? ", falling to [owner.loc]" : ""]."),
span_notice("[capitalize(electrode_name)] stop shocking you[isfloorturf(owner.loc) ? ", falling to [owner.loc]" : ""]."),
)
qdel(src)
/datum/status_effect/tased/proc/try_remove_taser(datum/source)
SIGNAL_HANDLER
INVOKE_ASYNC(src, PROC_REF(try_remove_taser_async), owner)
/datum/status_effect/tased/proc/someome_removing_taser(datum/source, mob/living/helper)
SIGNAL_HANDLER
INVOKE_ASYNC(src, PROC_REF(try_remove_taser_async), helper)
return COMPONENT_BLOCK_MISC_HELP
/datum/status_effect/tased/proc/try_remove_taser_async(mob/living/remover)
if(DOING_INTERACTION(remover, id))
return
owner.shake_up_animation()
playsound(owner, 'sound/items/weapons/thudswoosh.ogg', 50, TRUE, -1)
remover.visible_message(
span_warning("[owner] tries to remove [electrode_name][remover == owner ? "" : " from [owner]"]!"),
span_notice("You try to remove [electrode_name][remover == owner ? "" : " from [owner]"]!"),
)
// If embedding was less... difficult to work with, I would make tasers rely on an embedded object to handle this
if(!do_after(remover, 5 SECONDS, owner, extra_checks = CALLBACK(src, PROC_REF(try_remove_taser_checks)), interaction_key = id))
return
remover.visible_message(
span_warning("[owner] removes [electrode_name] from [remover == owner ? "[owner.p_their()]" : "[owner]'s"] body!"),
span_notice("You remove [electrode_name][remover == owner ? "" : " from [owner]'s body"]!"),
)
end_tase()
/datum/status_effect/tased/proc/try_remove_taser_checks()
return !QDELETED(src)
/datum/status_effect/tased/proc/disrupt_tase(datum/beam/source, obj/effect/ebeam/beam_effect, atom/movable/entering)
SIGNAL_HANDLER
if(!isliving(entering) || entering == taser || entering == firer || entering == owner)
return
if(entering.pass_flags & (PASSMOB|PASSGRILLE|PASSTABLE))
return
var/mob/living/disruptor = entering
if(!HAS_TRAIT(entering, TRAIT_CLUMSY) || prob(50))
if(isliving(firer))
// taser firer can lie down so people can cross over it!
var/mob/living/firer_living = firer
if(firer_living.body_position != disruptor.body_position)
return
else
// otherwise you can limbo under it
if(disruptor.body_position == LYING_DOWN)
return
disruptor.visible_message(
span_warning("[disruptor] gets tangled in [electrode_name]!"),
span_warning("You get tangled in [electrode_name]!"),
)
if(!disruptor.check_stun_immunity(CANSTUN|CANKNOCKDOWN))
disruptor.apply_damage(90, STAMINA)
disruptor.Knockdown(5 SECONDS)
disruptor.adjust_jitter_up_to(10 SECONDS, 30 SECONDS)
qdel(src)
/// Screen alert for being tased, clicking does a resist
/atom/movable/screen/alert/tazed
name = "Tased!"
desc = "You're being tased! You can click this or resist to attempt to stop it, assuming you've not already collapsed."
icon_state = "stun"
clickable_glow = TRUE
/atom/movable/screen/alert/tazed/Click(location, control, params)
. = ..()
if(!.)
return
var/mob/living/clicker = usr
clicker.resist()
/obj/effect/ebeam/electrodes_nozap
name = "electrodes"
alpha = 192
/obj/effect/ebeam/reacting/electrodes
name = "electrodes"
light_system = OVERLAY_LIGHT
light_on = TRUE
light_color = COLOR_YELLOW
light_power = 1
light_range = 1.5
// movespeed mods
/datum/movespeed_modifier/tasing_someone
multiplicative_slowdown = 2
/datum/movespeed_modifier/being_tased
multiplicative_slowdown = 4
@@ -68,7 +68,8 @@
var/obj/projectile/projectile_obj = new projectile(get_turf(src))
projectile_obj.log_override = TRUE //we log being fired ourselves a little further down.
projectile_obj.firer = chassis
projectile_obj.preparePixelProjectile(target, source, modifiers, spread)
projectile_obj.fired_from = src // mech = firer, equipment = fired from
projectile_obj.aim_projectile(target, source, modifiers, spread)
if(isliving(source) && source.client) //dont want it to happen from syndie mecha npc mobs, they do direct fire anyways
var/mob/living/shooter = source
projectile_obj.hit_prone_targets = shooter.combat_mode
Binary file not shown.

Before

Width:  |  Height:  |  Size: 125 KiB

After

Width:  |  Height:  |  Size: 129 KiB