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-15 22:51:13 -06:00
committed by StrangeWeirdKitten
parent 9e4220fad0
commit f7d6aec68d
7 changed files with 403 additions and 53 deletions

View File

@@ -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