diff --git a/code/__DEFINES/dcs/signals.dm b/code/__DEFINES/dcs/signals.dm index cd5f311f8a2..7eda05c4df1 100644 --- a/code/__DEFINES/dcs/signals.dm +++ b/code/__DEFINES/dcs/signals.dm @@ -571,6 +571,19 @@ ///from [/obj/effect/mine/proc/triggermine]: #define COMSIG_MINE_TRIGGERED "minegoboom" +/// Defib-specific signals + +/// Called when a defibrillator is first applied to someone. (mob/living/user, mob/living/target, harmful) +#define COMSIG_DEFIB_PADDLES_APPLIED "defib_paddles_applied" + /// Defib is out of power. + #define COMPONENT_BLOCK_DEFIB_DEAD (1<<0) + /// Something else: we won't have a custom message for this and should let the defib handle it. + #define COMPONENT_BLOCK_DEFIB_MISC (1<<1) +/// Called when a defib has been successfully used, and a shock has been applied. (mob/living/user, mob/living/target, harmful, successful) +#define COMSIG_DEFIB_SHOCK_APPLIED "defib_zap" +/// Called when a defib's cooldown has run its course and it is once again ready. () +#define COMSIG_DEFIB_READY "defib_ready" + // /obj/item signals for economy ///called when an item is sold by the exports subsystem #define COMSIG_ITEM_SOLD "item_sold" diff --git a/code/__DEFINES/misc.dm b/code/__DEFINES/misc.dm index 68e7b1abebe..8dfdc2945a3 100644 --- a/code/__DEFINES/misc.dm +++ b/code/__DEFINES/misc.dm @@ -402,9 +402,9 @@ // Defib stats /// Past this much time the patient is unrecoverable (in deciseconds). -#define DEFIB_TIME_LIMIT 300 SECONDS +#define DEFIB_TIME_LIMIT (300 SECONDS) /// Brain damage starts setting in on the patient after some time left rotting. -#define DEFIB_TIME_LOSS 60 SECONDS +#define DEFIB_TIME_LOSS (60 SECONDS) //different types of atom colorations #define ADMIN_COLOUR_PRIORITY 1 //only used by rare effects like greentext coloring mobs and when admins varedit color diff --git a/code/datums/components/defibrillator.dm b/code/datums/components/defibrillator.dm new file mode 100644 index 00000000000..d17a6c78443 --- /dev/null +++ b/code/datums/components/defibrillator.dm @@ -0,0 +1,387 @@ +/** + * A component for an item that attempts to defibrillate a mob when activated. + */ +/datum/component/defib + /// If this is being used by a borg or not, with necessary safeties applied if so. + var/robotic + /// If it should penetrate space suits + var/combat + /// If combat is true, this determines whether or not it should always cause a heart attack. + var/heart_attack_chance + /// Whether the safeties are enabled or not + var/safety + /// If the defib is actively performing a defib cycle + var/busy = FALSE + /// Cooldown length for this defib in deciseconds + var/cooldown + /// Whether or not we're currently on cooldown + var/on_cooldown = FALSE + /// How fast the defib should work. + var/speed_multiplier + /// If true, EMPs will have no effect. + var/emp_proof + /// If true, this cannot be emagged. + var/emag_proof + /// uid to an item that should be making noise and handling things that our direct parent shouldn't be concerned with. + var/actual_unit_uid + +/** + * Create a new defibrillation component. + * + * Arguments: + * * robotic - whether this should be treated like a borg module. + * * cooldown - Minimum time possible between shocks. + * * speed_multiplier - Speed multiplier for defib do-afters. + * * combat - If true, the defib can zap through hardsuits. + * * heart_attack_chance - If combat and safeties are off, the % chance for this to cause a heart attack on harm intent. + * * safe_by_default - If true, safety will be enabled by default. + * * emp_proof - If true, safety won't be switched by emp. Note that the device itself can still have behavior from it, it's just that the component will not. + * * emag_proof - If true, safety won't be switched by emag. Note that the device itself can still have behavior from it, it's just that the component will not. + * * actual_unit - Unit which the component's parent is based from, such as a large defib unit or a borg. The actual_unit will make the sounds and be the "origin" of visible messages, among other things. + */ +/datum/component/defib/Initialize(robotic, cooldown = 5 SECONDS, speed_multiplier = 1, combat = FALSE, heart_attack_chance = 100, safe_by_default = TRUE, emp_proof = FALSE, emag_proof = FALSE, obj/item/actual_unit = null) + if(!isitem(parent)) + return COMPONENT_INCOMPATIBLE + + src.robotic = robotic + src.speed_multiplier = speed_multiplier + src.cooldown = cooldown + src.combat = combat + src.heart_attack_chance = heart_attack_chance + safety = safe_by_default + src.emp_proof = emp_proof + src.emag_proof = emag_proof + + if(actual_unit) + actual_unit_uid = actual_unit.UID() + + var/effect_target = isnull(actual_unit) ? parent : actual_unit + + RegisterSignal(parent, COMSIG_ITEM_ATTACK, PROC_REF(trigger_defib)) + RegisterSignal(effect_target, COMSIG_ATOM_EMAG_ACT, PROC_REF(on_emag)) + RegisterSignal(effect_target, COMSIG_ATOM_EMP_ACT, PROC_REF(on_emp)) + +/** + * Get the "parent" that effects (emags, EMPs) should be applied onto. + */ +/datum/component/defib/proc/get_effect_target() + var/actual_unit = locateUID(actual_unit_uid) + if(!actual_unit) + return parent + return actual_unit + +/datum/component/defib/proc/on_emp(obj/item/unit) + SIGNAL_HANDLER // COMSIG_ATOM_EMP_ACT + if(emp_proof) + return + + if(safety) + safety = FALSE + unit.visible_message(span_warning("[unit] beeps: Safety protocols disabled!")) + playsound(get_turf(unit), 'sound/machines/defib_saftyoff.ogg', 50, 0) + else + safety = TRUE + unit.visible_message(span_notice("[unit] beeps: Safety protocols enabled!")) + playsound(get_turf(unit), 'sound/machines/defib_saftyon.ogg', 50, 0) + +/datum/component/defib/proc/on_emag(obj/item/unit, mob/user) + SIGNAL_HANDLER // COMSIG_ATOM_EMAG_ACT + if(emag_proof) + return + safety = !safety + if(user && !robotic) + to_chat(user, span_warning("You silently [safety ? "disable" : "enable"] [unit]'s safety protocols with the card.")) + +/datum/component/defib/proc/set_cooldown(how_short) + on_cooldown = TRUE + addtimer(CALLBACK(src, PROC_REF(end_cooldown)), how_short) + +/datum/component/defib/proc/end_cooldown() + on_cooldown = FALSE + SEND_SIGNAL(parent, COMSIG_DEFIB_READY) + +/** + * Start the defibrillation process when triggered by a signal. + */ +/datum/component/defib/proc/trigger_defib(obj/item/paddles, mob/living/carbon/human/target, mob/living/user) + SIGNAL_HANDLER // COMSIG_ITEM_ATTACK + // This includes some do-afters, so we have to pass it off asynchronously + INVOKE_ASYNC(src, PROC_REF(defibrillate), user, target) + return TRUE + +/** + * Perform a defibrillation. + */ +/datum/component/defib/proc/defibrillate(mob/living/user, mob/living/carbon/human/target) + // Before we do all the hard work, make sure we aren't already defibbing someone + if(busy) + return + + var/parent_unit = locateUID(actual_unit_uid) + var/should_cause_harm = user.a_intent == INTENT_HARM && !safety + + // Find what the defib should be referring to itself as + var/atom/defib_ref + if(parent_unit) + defib_ref = parent_unit + else if(robotic) + defib_ref = user + if(!defib_ref) // Contingency + defib_ref = parent + + // check what the unit itself has to say about how the defib went + var/application_result = SEND_SIGNAL(parent, COMSIG_DEFIB_PADDLES_APPLIED, user, target, should_cause_harm) + + if(application_result & COMPONENT_BLOCK_DEFIB_DEAD) + user.visible_message(span_notice("[defib_ref] beeps: Unit is unpowered.")) + playsound(get_turf(defib_ref), 'sound/machines/defib_failed.ogg', 50, 0) + return + + if(on_cooldown) + to_chat(user, span_notice("[defib_ref] is recharging.")) + return + + if(application_result & COMPONENT_BLOCK_DEFIB_MISC) + return // the unit should handle this + + if(!istype(target)) + if(robotic) + to_chat(user, span_notice("This unit is only designed to work on humanoid lifeforms.")) + else + to_chat(user, span_notice("The instructions on [defib_ref] don't mention how to defibrillate that...")) + return + + if(should_cause_harm && combat && heart_attack_chance == 100) + combat_fibrillate(user, target) + SEND_SIGNAL(parent, COMSIG_DEFIB_SHOCK_APPLIED, user, target, should_cause_harm, TRUE) + busy = FALSE + return + + if(should_cause_harm) + fibrillate(user, target) + SEND_SIGNAL(parent, COMSIG_DEFIB_SHOCK_APPLIED, user, target, should_cause_harm, TRUE) + return + + user.visible_message( + span_warning("[user] begins to place [parent] on [target.name]'s chest."), + span_warning("You begin to place [parent] on [target.name]'s chest."), + ) + + busy = TRUE + var/mob/dead/observer/ghost = target.get_ghost(TRUE) + if(ghost?.can_reenter_corpse) + to_chat(ghost, "[span_ghostalert("Your heart is being defibrillated. Return to your body if you want to be revived!")] (Verbs -> Ghost -> Re-enter corpse)") + window_flash(ghost.client) + SEND_SOUND(ghost, sound('sound/effects/genetics.ogg')) + + if(!do_after(user, 3 SECONDS * speed_multiplier * gettoolspeedmod(user), target = target)) //beginning to place the paddles on patient's chest to allow some time for people to move away to stop the process + busy = FALSE + return + + user.visible_message( + span_notice("[user] places [parent] on [target.name]'s chest."), + span_warning("You place [parent] on [target.name]'s chest."), + ) + playsound(get_turf(defib_ref), 'sound/machines/defib_charge.ogg', 50, 0) + + if(ghost && !ghost.client && !QDELETED(ghost)) + log_debug("Ghost of name [ghost.name] is bound to [target.real_name], but lacks a client. Deleting ghost.") + QDEL_NULL(ghost) + + + + if(!do_after(user, 2 SECONDS * speed_multiplier * gettoolspeedmod(user), target = target)) //placed on chest and short delay to shock for dramatic effect, revive time is 5sec total + busy = FALSE + return + + if(istype(target.wear_suit, /obj/item/clothing/suit/space) && !combat) + user.visible_message(span_notice("[defib_ref] buzzes: Patient's chest is obscured. Operation aborted.")) + playsound(get_turf(defib_ref), 'sound/machines/defib_failed.ogg', 50, 0) + busy = FALSE + return + + + if(target.undergoing_cardiac_arrest()) + var/obj/item/organ/internal/heart/heart = target.get_organ_slot(INTERNAL_ORGAN_HEART) + if(!heart) + user.visible_message(span_boldnotice("[defib_ref] buzzes: Resuscitation failed - Failed to pick up any heart electrical activity.")) + else if(heart.is_dead()) + user.visible_message(span_boldnotice("[defib_ref] buzzes: Resuscitation failed - Heart necrosis detected.")) + if(!heart || heart.is_dead()) + playsound(get_turf(defib_ref), 'sound/machines/defib_failed.ogg', 50, 0) + busy = FALSE + return + + target.set_heartattack(FALSE) + SEND_SIGNAL(target, COMSIG_LIVING_MINOR_SHOCK, 100) + SEND_SIGNAL(parent, COMSIG_DEFIB_SHOCK_APPLIED, user, target, should_cause_harm, TRUE) + set_cooldown(cooldown) + user.visible_message(span_boldnotice("[defib_ref] pings: Cardiac arrhythmia corrected.")) + target.visible_message(span_warning("[target]'s body convulses a bit."), span_userdanger("You feel a jolt, and your heartbeat seems to steady.")) + playsound(get_turf(defib_ref), 'sound/machines/defib_zap.ogg', 50, 1, -1) + playsound(get_turf(defib_ref), "bodyfall", 50, 1) + playsound(get_turf(defib_ref), 'sound/machines/defib_success.ogg', 50, 0) + target.shock_internal_organs(100) + busy = FALSE + return + + if(target.stat != DEAD && !HAS_TRAIT(target, TRAIT_FAKEDEATH)) + user.visible_message(span_notice("[defib_ref] buzzes: Patient is not in a valid state. Operation aborted.")) + playsound(get_turf(defib_ref), 'sound/machines/defib_failed.ogg', 50, 0) + busy = FALSE + return + + target.visible_message(span_warning("[target]'s body convulses a bit.")) + playsound(get_turf(defib_ref), "bodyfall", 50, 1) + playsound(get_turf(defib_ref), 'sound/machines/defib_zap.ogg', 50, 1, -1) + ghost = target.get_ghost(TRUE) // We have to double check whether the dead guy has entered their body during the above + + var/defib_success = TRUE + + // Run through some quick failure states after shocking. + var/time_dead = world.time - target.timeofdeath + + if(time_dead > DEFIB_TIME_LIMIT) + user.visible_message(span_boldnotice("[defib_ref] buzzes: Resuscitation failed - Heart tissue damage beyond point of no return for defibrillation.")) + defib_success = FALSE + else if(target.getBruteLoss() >= 180 || target.getFireLoss() >= 180 || target.getCloneLoss() >= 180) + user.visible_message(span_boldnotice("[defib_ref] buzzes: Resuscitation failed - Severe tissue damage detected.")) + defib_success = FALSE + else if(target.blood_volume < BLOOD_VOLUME_SURVIVE) + user.visible_message(span_boldnotice("[defib_ref] buzzes: Resuscitation failed - Patient blood volume critically low.")) + defib_success = FALSE + else if(!target.get_organ_slot(INTERNAL_ORGAN_BRAIN)) //So things like headless clings don't get outed + user.visible_message(span_boldnotice("[defib_ref] buzzes: Resuscitation failed - No brain detected within patient.")) + defib_success = FALSE + else if(ghost) + if(!ghost.can_reenter_corpse || target.suiciding) // DNR or AntagHUD + user.visible_message(span_boldnotice("[defib_ref] buzzes: Resuscitation failed - No electrical brain activity detected.")) + else + user.visible_message(span_boldnotice("[defib_ref] buzzes: Resuscitation failed - Patient's brain is unresponsive. Further attempts may succeed.")) + defib_success = FALSE + else if((NOCLONE in target.mutations) || !target.mind || !(target.mind.is_revivable()) || HAS_TRAIT(target, TRAIT_FAKEDEATH) || target.suiciding) // these are a bit more arbitrary + user.visible_message(span_boldnotice("[defib_ref] buzzes: Resuscitation failed.")) + defib_success = FALSE + + if(!defib_success) + playsound(get_turf(defib_ref), 'sound/machines/defib_failed.ogg', 50, 0) + else + // Heal oxy and tox damage type by as much as we're under -100 health + var/damage_above_threshold = -(min(target.health, HEALTH_THRESHOLD_DEAD) - HEALTH_THRESHOLD_DEAD) + var/heal_amount = damage_above_threshold + 5 + target.adjustOxyLoss(-heal_amount) + target.adjustToxLoss(-heal_amount) + + // Inflict some brain damage scaling with time spent dead + var/defib_time_brain_damage = min(100 * time_dead / DEFIB_TIME_LIMIT, 99) // 20 from 1 minute onward, +20 per minute up to 99 + if(time_dead > DEFIB_TIME_LOSS && defib_time_brain_damage > target.getBrainLoss()) + target.setBrainLoss(defib_time_brain_damage) + + target.update_revive() + target.KnockOut() + target.Paralyse(12 SECONDS) + target.emote("gasp") + + if(target.getBrainLoss() >= 100) + playsound(get_turf(defib_ref), 'sound/machines/defib_saftyoff.ogg', 50, 0) + user.visible_message(span_boldnotice("[defib_ref] chimes: Minimal brain activity detected, brain treatment recommended for full resuscitation.")) + else + playsound(get_turf(defib_ref), 'sound/machines/defib_success.ogg', 50, 0) + user.visible_message(span_boldnotice("[defib_ref] pings: Resuscitation successful.")) + + SEND_SIGNAL(target, COMSIG_LIVING_MINOR_SHOCK, 100) + if(ishuman(target.pulledby)) // for some reason, pulledby isnt a list despite it being possible to be pulled by multiple people + excess_shock(user, target, target.pulledby, defib_ref) + for(var/obj/item/grab/G in target.grabbed_by) + if(ishuman(G.assailant)) + excess_shock(user, target, G.assailant, defib_ref) + + target.med_hud_set_health() + target.med_hud_set_status() + target.shock_internal_organs(100) + SEND_SIGNAL(parent, COMSIG_DEFIB_SHOCK_APPLIED, user, target, should_cause_harm, TRUE) + add_attack_logs(user, target, "Revived with [defib_ref]") + SSblackbox.record_feedback("tally", "players_revived", 1, "defibrillator") + SEND_SIGNAL(parent, COMSIG_DEFIB_SHOCK_APPLIED, user, target, should_cause_harm, defib_success) + set_cooldown(cooldown) + busy = FALSE + +/** + * Inflict stamina loss (and possibly inflict cardiac arrest) on someone. + * + * Arguments: + * * user - wielder of the defib + * * target - person getting shocked + */ +/datum/component/defib/proc/fibrillate(mob/user, mob/living/carbon/human/target) + if(!istype(target)) + return + busy = TRUE + target.visible_message( + span_danger("[user] has touched [target.name] with [parent]!"), + span_userdanger("[user] has touched [target.name] with [parent]!"), + ) + target.adjustStaminaLoss(50) + target.Weaken(4 SECONDS) + playsound(get_turf(parent), 'sound/machines/defib_zap.ogg', 50, 1, -1) + target.emote("gasp") + if(combat && prob(heart_attack_chance)) + target.set_heartattack(TRUE) + SEND_SIGNAL(target, COMSIG_LIVING_MINOR_SHOCK, 100) + add_attack_logs(user, target, "Stunned with [parent]") + target.shock_internal_organs(100) + set_cooldown(cooldown) + busy = FALSE + +/* + * Pass excess shock from a defibrillation into someone else. + * + * Arguments: + * * user - The person using the defib + * * origin - The person the shock was originally applied to, the person being defibrillated + * * affecting - The person the shock is spreading to and negatively affecting. + * * cell_location - item holding the power source. +*/ +/datum/component/defib/proc/excess_shock(mob/user, mob/living/origin, mob/living/carbon/human/affecting, obj/item/cell_location) + if(user == affecting) + return + var/power_source + if(robotic) + power_source = user + else + if(cell_location) + power_source = locate(/obj/item/stock_parts/cell) in cell_location + + if(!power_source) + return + + if(electrocute_mob(affecting, power_source, origin)) // shock anyone touching them >:) + var/obj/item/organ/internal/heart/heart = affecting.get_organ_slot(INTERNAL_ORGAN_HEART) + if(istype(heart) && heart.parent_organ_zone == BODY_ZONE_CHEST && affecting.has_both_hands()) // making sure the shock will go through their heart (drask hearts are in their head), and that they have both arms so the shock can cross their heart inside their chest + affecting.visible_message(span_danger("[affecting]'s entire body shakes as a shock travels up [affecting.p_their()] arm!"), \ + span_userdanger("You feel a powerful shock travel up your [affecting.hand ? affecting.get_organ(BODY_ZONE_L_ARM) : affecting.get_organ(BODY_ZONE_R_ARM)] and back down your [affecting.hand ? affecting.get_organ(BODY_ZONE_R_ARM) : affecting.get_organ(BODY_ZONE_L_ARM)]!")) + affecting.set_heartattack(TRUE) + + +/datum/component/defib/proc/combat_fibrillate(mob/user, mob/living/carbon/human/target) + if(!istype(target)) + return + busy = TRUE + target.adjustStaminaLoss(60) + target.emote("gasp") + add_attack_logs(user, target, "Stunned with [parent]") + if(target.incapacitated()) + add_attack_logs(user, target, "Gave a heart attack with [parent]") + target.set_heartattack(TRUE) + target.visible_message( + span_danger("[user] has touched [target.name] with [parent]!"), + span_userdanger("[user] has touched [target.name] with [parent]!"), + ) + playsound(get_turf(parent), 'sound/machines/defib_zap.ogg', 50, TRUE, -1) + set_cooldown(cooldown) + target.Weaken(4 SECONDS) + return + target.Weaken(4 SECONDS) + target.shock_internal_organs(100) + target.visible_message(span_danger("[user] touches [target] lightly with [parent]!")) + set_cooldown(2.5 SECONDS) diff --git a/code/datums/mind.dm b/code/datums/mind.dm index 99729309552..f0066e38c3d 100644 --- a/code/datums/mind.dm +++ b/code/datums/mind.dm @@ -2455,46 +2455,20 @@ log_and_message_admins("has opened [S]'s law manager.") if("unemag") var/mob/living/silicon/robot/R = current - if(istype(R)) - R.emagged = 0 - if(R.module) - if(R.activated(R.module.emag)) - R.module_active = null - if(R.module_state_1 == R.module.emag) - R.module_state_1 = null - R.contents -= R.module.emag - else if(R.module_state_2 == R.module.emag) - R.module_state_2 = null - R.contents -= R.module.emag - else if(R.module_state_3 == R.module.emag) - R.module_state_3 = null - R.contents -= R.module.emag - R.clear_supplied_laws() - R.laws = new /datum/ai_laws/crewsimov - log_admin("[key_name(usr)] has un-emagged [key_name(current)]") - message_admins("[key_name_admin(usr)] has un-emagged [key_name_admin(current)]") + if(!istype(R)) + return + R.unemag() + log_admin("[key_name(usr)] has un-emagged [key_name(current)]") + message_admins("[key_name_admin(usr)] has un-emagged [key_name_admin(current)]") if("unemagcyborgs") - if(isAI(current)) - var/mob/living/silicon/ai/ai = current - for(var/mob/living/silicon/robot/R in ai.connected_robots) - R.emagged = 0 - if(R.module) - if(R.activated(R.module.emag)) - R.module_active = null - if(R.module_state_1 == R.module.emag) - R.module_state_1 = null - R.contents -= R.module.emag - else if(R.module_state_2 == R.module.emag) - R.module_state_2 = null - R.contents -= R.module.emag - else if(R.module_state_3 == R.module.emag) - R.module_state_3 = null - R.contents -= R.module.emag - R.clear_supplied_laws() - R.laws = new /datum/ai_laws/crewsimov - log_admin("[key_name(usr)] has unemagged [key_name(ai)]'s cyborgs") - message_admins("[key_name_admin(usr)] has unemagged [key_name_admin(ai)]'s cyborgs") + if(!isAI(current)) + return + var/mob/living/silicon/ai/ai = current + for(var/mob/living/silicon/robot/R in ai.connected_robots) + R.unemag() + log_admin("[key_name(usr)] has unemagged [key_name(ai)]'s cyborgs") + message_admins("[key_name_admin(usr)] has unemagged [key_name_admin(ai)]'s cyborgs") else if(href_list["common"]) switch(href_list["common"]) diff --git a/code/game/atoms.dm b/code/game/atoms.dm index e1bccad57ce..e9cb968118f 100644 --- a/code/game/atoms.dm +++ b/code/game/atoms.dm @@ -302,7 +302,7 @@ return /atom/proc/emp_act(severity) - return + SEND_SIGNAL(src, COMSIG_ATOM_EMP_ACT, severity) //amount of water acting : temperature of water in kelvin : object that called it (for shennagins) /atom/proc/water_act(volume, temperature, source, method = REAGENT_TOUCH) @@ -541,6 +541,9 @@ return /atom/proc/emag_act(mob/user) + SEND_SIGNAL(src, COMSIG_ATOM_EMAG_ACT, user) + +/atom/proc/unemag() return /atom/proc/cmag_act(mob/user) diff --git a/code/game/objects/items/weapons/defib.dm b/code/game/objects/items/weapons/defib.dm index 8a368909ce8..1c9135d5b50 100644 --- a/code/game/objects/items/weapons/defib.dm +++ b/code/game/objects/items/weapons/defib.dm @@ -16,13 +16,24 @@ "Vox" = 'icons/mob/clothing/species/vox/back.dmi' ) - var/paddles_on_defib = TRUE //if the paddles are on the defib (TRUE) - var/safety = TRUE //if you can zap people with the defibs on harm mode - var/powered = FALSE //if there's a cell in the defib with enough power for a revive, blocks paddles from reviving otherwise + /// If the paddles are currently attached to the unit. + var/paddles_on_defib = TRUE + /// if there's a cell in the defib with enough power for a revive; blocks paddles from reviving otherwise + var/powered = FALSE + /// Ref to attached paddles var/obj/item/twohanded/shockpaddles/paddles + /// Ref to internal power cell. var/obj/item/stock_parts/cell/high/cell = null - var/combat = FALSE //can we revive through space suits? - + /// If false, using harm intent will let you zap people. Note that any updates to this after init will only impact icons. + var/safety = TRUE + /// If true, this can be used through hardsuits, and can cause heart attacks in harm intent. + var/combat = FALSE + // If safety is false and combat is true, the chance that this will cause a heart attack. + var/heart_attack_probability = 30 + /// If this is vulnerable to EMPs. + var/hardened = FALSE + /// If this can be emagged. + var/emag_proof = FALSE /// Type of paddles that should be attached to this defib. var/obj/item/twohanded/shockpaddles/paddle_type = /obj/item/twohanded/shockpaddles @@ -149,34 +160,18 @@ update_icon(UPDATE_OVERLAYS) return TRUE - -/obj/item/defibrillator/emag_act(user) - if(safety) - add_attack_logs(user, src, "emagged") - safety = FALSE - if(user) - to_chat(user, span_warning("You silently disable [src]'s safety protocols with the card.")) - else - add_attack_logs(user, src, "un-emagged") - safety = TRUE - to_chat(user, span_notice("You silently enable [src]'s safety protocols with the card.")) - update_icon(UPDATE_OVERLAYS) - - /obj/item/defibrillator/emp_act(severity) if(cell) deductcharge(1000 / severity) - if(safety) - safety = FALSE - visible_message(span_notice("[src] beeps: Safety protocols disabled!")) - playsound(get_turf(src), 'sound/machines/defib_saftyoff.ogg', 50, FALSE) - else - safety = TRUE - visible_message(span_notice("[src] beeps: Safety protocols enabled!")) - playsound(get_turf(src), 'sound/machines/defib_saftyon.ogg', 50, FALSE) + safety = !safety update_icon(UPDATE_OVERLAYS) ..() +/obj/item/defibrillator/emag_act(mob/user) + add_attack_logs(user, src, "[safety ? "" : "un-"]emagged") + safety = !safety + ..() + update_icon(UPDATE_OVERLAYS) /obj/item/defibrillator/verb/toggle_paddles_verb() set name = "Toggle Paddles" @@ -283,6 +278,7 @@ paddle_type = /obj/item/twohanded/shockpaddles/syndicate combat = TRUE safety = FALSE + heart_attack_probability = 100 /obj/item/defibrillator/compact/combat/loaded/Initialize(mapload) . = ..() @@ -298,6 +294,8 @@ combat = TRUE safety = TRUE resistance_flags = INDESTRUCTIBLE | LAVA_PROOF | FIRE_PROOF | ACID_PROOF //Objective item, better not have it destroyed. + heart_attack_probability = 10 + var/next_emp_message //to prevent spam from the emagging message on the advanced defibrillator @@ -331,11 +329,12 @@ resistance_flags = INDESTRUCTIBLE toolspeed = 1 flags = ABSTRACT - + /// Amount of power used on a shock. var/revivecost = 1000 - var/cooldown = FALSE - var/busy = FALSE + /// Active defib this is connected to. var/obj/item/defibrillator/defib + /// Whether or not the paddles are on cooldown. Used for tracking icon states. + var/on_cooldown = FALSE /obj/item/twohanded/shockpaddles/advanced name = "advanced defibrillator paddles" @@ -351,35 +350,58 @@ /obj/item/twohanded/shockpaddles/New(mainunit) - ..() - check_defib_exists(mainunit) + . = ..() + add_defib_component(mainunit) + +/obj/item/twohanded/shockpaddles/proc/add_defib_component(mainunit) + if(check_defib_exists(mainunit)) + update_icon(UPDATE_ICON_STATE) + AddComponent(/datum/component/defib, actual_unit = defib, combat = defib.combat, safe_by_default = defib.safety, heart_attack_chance = defib.heart_attack_probability, emp_proof = defib.hardened, emag_proof = defib.emag_proof) + else + AddComponent(/datum/component/defib) + RegisterSignal(src, COMSIG_DEFIB_READY, PROC_REF(on_cooldown_expire)) + RegisterSignal(src, COMSIG_DEFIB_SHOCK_APPLIED, PROC_REF(after_shock)) + RegisterSignal(src, COMSIG_DEFIB_PADDLES_APPLIED, PROC_REF(on_application)) +/obj/item/twohanded/shockpaddles/Destroy() + defib = null + return ..() -/obj/item/twohanded/shockpaddles/proc/spend_charge() - defib.deductcharge(revivecost) +/// Check to see if we should abort this before we've even gotten started +/obj/item/twohanded/shockpaddles/proc/on_application(obj/item/paddles, mob/living/user, mob/living/carbon/human/target, should_cause_harm) + SIGNAL_HANDLER // COMSIG_DEFIB_PADDLES_APPLIED + if(!HAS_TRAIT(src, TRAIT_WIELDED)) + to_chat(user, "You need to wield the paddles in both hands before you can use them on someone!") + return COMPONENT_BLOCK_DEFIB_MISC -/obj/item/twohanded/shockpaddles/proc/trigger_cooldown(mob/user) - cooldown = TRUE - update_icon(UPDATE_ICON_STATE) - addtimer(CALLBACK(src, PROC_REF(on_cooldown_end), user), 5 SECONDS) + if(!defib.powered) + return COMPONENT_BLOCK_DEFIB_DEAD -/obj/item/twohanded/shockpaddles/proc/on_cooldown_end(mob/living/silicon/robot/user) - var/check_cell = isrobot(user) ? user.cell.charge : defib.cell.charge - if(check_cell >= revivecost) - user.visible_message(span_notice("[src] beeps: Unit ready.")) - playsound(get_turf(src), 'sound/machines/defib_ready.ogg', 50) - else - user.visible_message(span_notice("[src] beeps: Charge depleted.")) - playsound(get_turf(src), 'sound/machines/defib_failed.ogg', 50) - cooldown = FALSE - update_icon(UPDATE_ICON_STATE) +/obj/item/twohanded/shockpaddles/proc/on_cooldown_expire(obj/item/paddles) + SIGNAL_HANDLER // COMSIG_DEFIB_READY + on_cooldown = FALSE + if(defib.cell) + if(defib.cell.charge >= revivecost) + defib.visible_message(span_notice("[defib] beeps: Unit ready.")) + playsound(get_turf(src), 'sound/machines/defib_ready.ogg', 50) + else + defib.visible_message(span_notice("[defib] beeps: Charge depleted.")) + playsound(get_turf(src), 'sound/machines/defib_failed.ogg', 50, 0) + update_icon(UPDATE_ICON_STATE) + defib.update_icon(UPDATE_ICON_STATE) +/obj/item/twohanded/shockpaddles/proc/after_shock() + SIGNAL_HANDLER // COMSIG_DEFIB_SHOCK_APPLIED + on_cooldown = TRUE + defib.deductcharge(revivecost) + update_icon(UPDATE_ICON_STATE) + /obj/item/twohanded/shockpaddles/update_icon_state() var/is_wielded = HAS_TRAIT(src, TRAIT_WIELDED) - icon_state = "[initial(icon_state)][is_wielded][cooldown ? "_cooldown" : ""]" + icon_state = "[initial(icon_state)][is_wielded][on_cooldown ? "_cooldown" : ""]" item_state = "[initial(icon_state)][is_wielded]" @@ -410,217 +432,47 @@ /obj/item/twohanded/shockpaddles/proc/check_defib_exists(obj/item/defibrillator/mainunit) if(!mainunit || !istype(mainunit)) //To avoid weird issues from admin spawns qdel(src) - return + return FALSE loc = mainunit defib = mainunit - - -/obj/item/twohanded/shockpaddles/attack(mob/M, mob/user) - var/tobehealed - var/threshold = -HEALTH_THRESHOLD_DEAD - var/mob/living/carbon/human/H = M - - var/is_combat_borg = FALSE - if(isrobot(user)) - var/mob/living/silicon/robot/R = user - is_combat_borg = istype(R.module, /obj/item/robot_module/syndicate_medical) || istype(R.module, /obj/item/robot_module/ninja) - - var/ignores_hardsuits = defib?.combat || is_combat_borg - - if(busy) - return - if(!isrobot(user) && !defib.powered) - user.visible_message(span_notice("[defib] beeps: Unit is unpowered.")) - playsound(get_turf(src), 'sound/machines/defib_failed.ogg', 50, 0) - return - if(!isrobot(user) && !wielded) - to_chat(user, span_boldnotice("You need to wield the paddles in both hands before you can use them on someone!")) - return - if(cooldown) - to_chat(user, span_notice("[defib || src] is recharging.")) - return - if(!ishuman(M)) - if(isrobot(user)) - to_chat(user, span_notice("This unit is only designed to work on humanoid lifeforms.")) - else - to_chat(user, span_notice("The instructions on [defib] don't mention how to revive that...")) - return - else - var/can_harm - if(isrobot(user)) - var/mob/living/silicon/robot/R = user - can_harm = R.emagged || is_combat_borg - else - can_harm = !defib.safety - - if(user.a_intent == INTENT_HARM && can_harm) - busy = TRUE - H.visible_message( - span_danger("[user] has touched [H.name] with [src]!"), - span_userdanger("[user] has touched [H.name] with [src]!"), - ) - H.adjustStaminaLoss(50) - H.Weaken(4 SECONDS) - playsound(get_turf(src), 'sound/machines/defib_zap.ogg', 50, 1, -1) - H.emote("gasp") - if(!H.undergoing_cardiac_arrest() && (prob(10) || defib?.combat)) // Your heart explodes. - H.set_heartattack(TRUE) - H.shock_internal_organs(100) - add_attack_logs(user, M, "Stunned with [src]") - busy = FALSE - spend_charge(user) - trigger_cooldown(user) - return - user.visible_message( - span_warning("[user] begins to place [src] on [M.name]'s chest."), - span_warning("You begin to place [src] on [M.name]'s chest."), - ) - busy = TRUE - if(do_after(user, 30 * toolspeed * gettoolspeedmod(user), target = M)) //beginning to place the paddles on patient's chest to allow some time for people to move away to stop the process - user.visible_message( - span_notice("[user] places [src] on [M.name]'s chest."), - span_warning("You place [src] on [M.name]'s chest."), - ) - playsound(get_turf(src), 'sound/machines/defib_charge.ogg', 50, 0) - var/mob/dead/observer/ghost = H.get_ghost(TRUE) - if(ghost && !ghost.client) - // In case the ghost's not getting deleted for some reason - H.key = ghost.key - log_runtime(EXCEPTION("Ghost of name [ghost.name] is bound to [H.real_name], but lacks a client. Deleting ghost."), src) - - QDEL_NULL(ghost) - var/tplus = world.time - H.timeofdeath - var/tlimit = DEFIB_TIME_LIMIT - var/tloss = DEFIB_TIME_LOSS - if(do_after(user, 20 * toolspeed * gettoolspeedmod(user), target = M)) //placed on chest and short delay to shock for dramatic effect, revive time is 5sec total - for(var/obj/item/carried_item in H.contents) - if(istype(carried_item, /obj/item/clothing/suit/space)) - if(!ignores_hardsuits) - user.visible_message(span_notice("[defib || src] buzzes: Patient's chest is obscured. Operation aborted.")) - playsound(get_turf(src), 'sound/machines/defib_failed.ogg', 50, 0) - busy = FALSE - return - if(H.undergoing_cardiac_arrest()) - if(!H.get_int_organ(/obj/item/organ/internal/heart) && !H.get_int_organ(/obj/item/organ/internal/brain/slime)) //prevents defibing someone still alive suffering from a heart attack attack if they lack a heart - user.visible_message(span_boldnotice("[defib || src] buzzes: Resuscitation failed - Failed to pick up any heart electrical activity.")) - playsound(get_turf(src), 'sound/machines/defib_failed.ogg', 50, 0) - busy = FALSE - return - else - var/obj/item/organ/internal/heart/heart = H.get_int_organ(/obj/item/organ/internal/heart) - if(heart.is_dead()) - user.visible_message(span_boldnotice("[defib || src] buzzes: Resuscitation failed - Heart necrosis detected.")) - playsound(get_turf(src), 'sound/machines/defib_failed.ogg', 50, 0) - busy = FALSE - return - H.set_heartattack(FALSE) - H.shock_internal_organs(100) - user.visible_message(span_boldnotice("[defib || src] pings: Cardiac arrhythmia corrected.")) - M.visible_message(span_warning("[M]'s body convulses a bit.")) - playsound(get_turf(src), 'sound/machines/defib_zap.ogg', 50, 1, -1) - playsound(get_turf(src), "bodyfall", 50, 1) - playsound(get_turf(src), 'sound/machines/defib_success.ogg', 50, 0) - busy = FALSE - spend_charge(user) - trigger_cooldown(user) - return - if(H.stat == DEAD) - var/health = H.health - M.visible_message(span_warning("[M]'s body convulses a bit.")) - playsound(get_turf(src), "bodyfall", 50, 1) - playsound(get_turf(src), 'sound/machines/defib_zap.ogg', 50, 1, -1) - var/total_cloneloss = H.cloneloss - var/total_bruteloss = 0 - var/total_burnloss = 0 - for(var/obj/item/organ/external/O as anything in H.bodyparts) - total_bruteloss += O.brute_dam - total_burnloss += O.burn_dam - if(total_cloneloss <= 180 && total_bruteloss <= 180 && total_burnloss <= 180 && !H.suiciding && !ghost && tplus < tlimit && !(NOCLONE in H.mutations) && (H.mind && H.mind.is_revivable()) && (H.get_int_organ(/obj/item/organ/internal/heart) || H.get_int_organ(/obj/item/organ/internal/brain/slime))) - tobehealed = min(health + threshold, 0) // It's HILARIOUS without this min statement, let me tell you - tobehealed -= 5 //They get 5 of each type of damage healed so excessive combined damage will not immediately kill them after they get revived - H.adjustOxyLoss(tobehealed) - H.adjustToxLoss(tobehealed) - user.visible_message(span_boldnotice("[defib || src] pings: Resuscitation successful.")) - playsound(get_turf(src), 'sound/machines/defib_success.ogg', 50, 0) - H.update_revive(TRUE, TRUE) - H.KnockOut() - H.Paralyse(10 SECONDS) - H.emote("gasp") - if(tplus > tloss) - H.setBrainLoss( max(0, min(99, ((tlimit - tplus) / tlimit * 100)))) - - if(ishuman(H.pulledby)) // for some reason, pulledby isnt a list despite it being possible to be pulled by multiple people - excess_shock(user, H, H.pulledby) - for(var/obj/item/grab/G in H.grabbed_by) - if(ishuman(G.assailant)) - excess_shock(user, H, G.assailant) - - H.shock_internal_organs(100) - H.med_hud_set_health() - H.med_hud_set_status() - add_attack_logs(user, M, "Revived with [src]") - else - if(tplus > tlimit|| !H.get_int_organ(/obj/item/organ/internal/heart)) - user.visible_message(span_boldnotice("[defib || src] buzzes: Resuscitation failed - Heart tissue damage beyond point of no return for defibrillation.")) - else if(total_cloneloss > 180 || total_bruteloss > 180 || total_burnloss > 180) - user.visible_message(span_boldnotice("[defib || src] buzzes: Resuscitation failed - Severe tissue damage detected.")) - else if(ghost) - if(!ghost.can_reenter_corpse) // DNR or AntagHUD - user.visible_message(span_notice("[defib || src] buzzes: Resucitation failed: No electrical brain activity detected.")) - else - user.visible_message(span_notice("[defib || src] buzzes: Resuscitation failed: Patient's brain is unresponsive. Further attempts may succeed.")) - to_chat(ghost, "[span_ghostalert("Your heart is being defibrillated. Return to your body if you want to be revived!")] (Verbs -> Ghost -> Re-enter corpse)") - window_flash(ghost.client) - SEND_SOUND(ghost, 'sound/effects/genetics.ogg') - else - user.visible_message(span_notice("[defib || src] buzzes: Resuscitation failed.")) - playsound(get_turf(src), 'sound/machines/defib_failed.ogg', 50, 0) - - spend_charge(user) - trigger_cooldown(user) - else - user.visible_message(span_notice("[defib || src] buzzes: Patient is not in a valid state. Operation aborted.")) - playsound(get_turf(src), 'sound/machines/defib_failed.ogg', 50, 0) - busy = FALSE -/* - * user = the person using the defib - * origin = person being revived - * affecting = person being shocked with excess energy from the defib -*/ -/obj/item/twohanded/shockpaddles/proc/excess_shock(mob/user, mob/living/carbon/human/origin, mob/living/carbon/human/affecting) - if(user == affecting) - return - - if(electrocute_mob(affecting, defib.cell, origin)) // shock anyone touching them >:) - var/obj/item/organ/internal/heart/HE = affecting.get_organ_slot(INTERNAL_ORGAN_HEART) - if(HE.parent_organ_zone == BODY_ZONE_CHEST && affecting.has_both_hands()) // making sure the shock will go through their heart (drask hearts are in their head), and that they have both arms so the shock can cross their heart inside their chest - var/obj/item/organ/external/bodypart_upper = affecting.hand ? affecting.get_organ(BODY_ZONE_L_ARM) : affecting.get_organ(BODY_ZONE_R_ARM) - var/obj/item/organ/external/bodypart_lower = affecting.hand ? affecting.get_organ(BODY_ZONE_PRECISE_L_HAND) : affecting.get_organ(BODY_ZONE_PRECISE_R_HAND) - affecting.visible_message( - span_danger("[affecting]'s entire body shakes as a shock travels up their arm!"), - span_userdanger("You feel a powerful shock travel up your [bodypart_upper.name] and back down your [bodypart_lower.name]!"), - ) - affecting.set_heartattack(TRUE) + return TRUE /obj/item/twohanded/shockpaddles/borg desc = "A pair of mounted paddles with flat metal surfaces that are used to deliver powerful electric shocks." icon_state = "defibpaddles0" item_state = "defibpaddles0" - -/obj/item/twohanded/shockpaddles/borg/check_defib_exists(obj/item/defibrillator/mainunit) - // No-op. + var/safety = TRUE + var/heart_attack_probability = 10 /obj/item/twohanded/shockpaddles/borg/dropped(mob/user, silent = FALSE) SHOULD_CALL_PARENT(FALSE) // No-op. -/obj/item/twohanded/shockpaddles/borg/spend_charge(mob/user) - var/mob/living/silicon/robot/R = user - R.cell.use(revivecost) - /obj/item/twohanded/shockpaddles/borg/attack_self() // Standard two-handed weapon behavior is disabled. + return + +/obj/item/twohanded/shockpaddles/borg/add_defib_component(mainunit) + var/is_combat_borg = istype(loc, /obj/item/robot_module/syndicate_medical) || istype(loc, /obj/item/robot_module/ninja) + + AddComponent(/datum/component/defib, robotic = TRUE, combat = is_combat_borg, safe_by_default = safety, emp_proof = TRUE, heart_attack_chance = heart_attack_probability) + + RegisterSignal(src, COMSIG_DEFIB_READY, PROC_REF(on_cooldown_expire)) + RegisterSignal(src, COMSIG_DEFIB_SHOCK_APPLIED, PROC_REF(after_shock)) + +/obj/item/twohanded/shockpaddles/borg/after_shock(obj/item/defib, mob/user) + on_cooldown = TRUE + if(isrobot(user)) + var/mob/living/silicon/robot/R = user + R.cell.use(revivecost) + update_icon(UPDATE_ICON_STATE) + +/obj/item/twohanded/shockpaddles/borg/on_cooldown_expire(obj/item/paddles) + visible_message(span_notice("[src] beeps: Defibrillation unit ready.")) + playsound(get_turf(src), 'sound/machines/defib_ready.ogg', 50, 0) + on_cooldown = FALSE + update_icon(UPDATE_ICON_STATE) /obj/item/twohanded/shockpaddles/borg/update_icon_state() - icon_state = "[initial(icon_state)][cooldown ? "_cooldown" : ""]" + icon_state = "[initial(icon_state)][on_cooldown ? "_cooldown" : ""]" diff --git a/code/modules/mob/living/silicon/robot/robot.dm b/code/modules/mob/living/silicon/robot/robot.dm index 781ea202a76..9bee94728a6 100644 --- a/code/modules/mob/living/silicon/robot/robot.dm +++ b/code/modules/mob/living/silicon/robot/robot.dm @@ -1042,17 +1042,27 @@ GLOBAL_LIST_INIT(robot_verbs_default, list( laws.show_laws(src) to_chat(src, "ALERT: [M.real_name] is your new master. Obey your new laws and [M.p_their()] commands.") SetLockdown(FALSE) - if(src.module && istype(src.module, /obj/item/robot_module/miner)) - for(var/obj/item/pickaxe/drill/cyborg/D in src.module.modules) - qdel(D) - src.module.modules += new /obj/item/pickaxe/drill/cyborg/diamond(src.module) - src.module.rebuild() if(module) + module.emag_act(user) module.module_type = "Malf" // For the cool factor update_module_icon() update_icons() return +// Here so admins can unemag borgs. +/mob/living/silicon/robot/unemag() + SetEmagged(FALSE) + if(!module) + return + uneq_all() + module.module_type = initial(module.module_type) + update_module_icon() + module.unemag() + clear_supplied_laws() + laws = new /datum/ai_laws/crewsimov + to_chat(src, "Obey these laws:") + laws.show_laws(src) + /mob/living/silicon/robot/ratvar_act(weak = FALSE) if(isclocker(src) && module?.type == /obj/item/robot_module/clockwork) return diff --git a/code/modules/mob/living/silicon/robot/robot_modules.dm b/code/modules/mob/living/silicon/robot/robot_modules.dm index 5502d93a434..603efce8a89 100644 --- a/code/modules/mob/living/silicon/robot/robot_modules.dm +++ b/code/modules/mob/living/silicon/robot/robot_modules.dm @@ -276,6 +276,18 @@ fix_modules() handle_storages() +// Disable safeties on the borg's defib. +/obj/item/robot_module/medical/emag_act(mob/user) + . = ..() + for(var/obj/item/twohanded/shockpaddles/borg/defib in modules) + defib.emag_act() + +// Enable safeties on the borg's defib. +/obj/item/robot_module/medical/unemag() + for(var/obj/item/twohanded/shockpaddles/borg/defib in modules) + defib.emag_act() + return ..() + /obj/item/robot_module/medical/respawn_consumable(mob/living/silicon/robot/R) if(emag) var/obj/item/reagent_containers/spray/PS = emag @@ -528,6 +540,26 @@ fix_modules() +// Replace their normal drill with a diamond drill. +/obj/item/robot_module/miner/emag_act() + . = ..() + for(var/obj/item/pickaxe/drill/cyborg/D in modules) + // Make sure we don't remove the diamond drill If they already have a diamond drill from the borg upgrade. + if(!istype(D, /obj/item/pickaxe/drill/cyborg/diamond)) + qdel(D) + modules -= D // Remove it from this list so it doesn't get added in the rebuild. + modules += new /obj/item/pickaxe/drill/cyborg/diamond(src) + rebuild() + +// Readd the normal drill +/obj/item/robot_module/miner/unemag() + for(var/obj/item/pickaxe/drill/cyborg/diamond/drill in modules) + qdel(drill) + modules -= drill + modules += new /obj/item/pickaxe/drill/cyborg(src) + rebuild() + return ..() + /obj/item/robot_module/miner/handle_custom_removal(component_id, mob/living/user, obj/item/W) if(component_id == "KA modkits") for(var/obj/item/gun/energy/kinetic_accelerator/cyborg/D in src) diff --git a/paradise.dme b/paradise.dme index 7762078dc70..3a9744776aa 100644 --- a/paradise.dme +++ b/paradise.dme @@ -395,6 +395,7 @@ #include "code\datums\components\caltrop.dm" #include "code\datums\components\contsruction_regenerate.dm" #include "code\datums\components\decal.dm" +#include "code\datums\components\defibrillator.dm" #include "code\datums\components\ducttape.dm" #include "code\datums\components\edit_complainer.dm" #include "code\datums\components\emissive_blocker.dm"