diff --git a/code/__DEFINES/dcs/signals/signals_mob/signals_mob_carbon.dm b/code/__DEFINES/dcs/signals/signals_mob/signals_mob_carbon.dm index c98168560b748..96b949c5999f5 100644 --- a/code/__DEFINES/dcs/signals/signals_mob/signals_mob_carbon.dm +++ b/code/__DEFINES/dcs/signals/signals_mob/signals_mob_carbon.dm @@ -1,7 +1,12 @@ +///Called from /datum/species/proc/help : (mob/living/carbon/human/helper, datum/martial_art/helper_style) +#define COMSIG_CARBON_PRE_HELP "carbon_pre_help" + /// Stops the rest of the help + #define COMPONENT_BLOCK_HELP_ACT (1<<0) + ///Called from /mob/living/carbon/help_shake_act, before any hugs have ocurred. (mob/living/helper) -#define COMSIG_CARBON_PRE_HELP_ACT "carbon_pre_help" +#define COMSIG_CARBON_PRE_MISC_HELP "carbon_pre_misc_help" /// Stops the rest of help act (hugging, etc) from occuring - #define COMPONENT_BLOCK_HELP_ACT (1<<0) + #define COMPONENT_BLOCK_MISC_HELP (1<<0) ///Called from /mob/living/carbon/help_shake_act on the person being helped, after any hugs have ocurred. (mob/living/helper) #define COMSIG_CARBON_HELP_ACT "carbon_help" @@ -84,10 +89,20 @@ #define COMPONENT_OVERRIDE_HEALTH_HUD (1<<0) ///Called when a carbon updates their sanity (source = carbon) #define COMSIG_CARBON_SANITY_UPDATE "carbon_sanity_update" +///Called when a carbon attempts to breath, before the breath has actually occured +#define COMSIG_CARBON_ATTEMPT_BREATHE "carbon_attempt_breathe" + // Prevents the breath + #define COMSIG_CARBON_BLOCK_BREATH (1 << 0) ///Called when a carbon breathes, before the breath has actually occured #define COMSIG_CARBON_PRE_BREATHE "carbon_pre_breathe" ///Called when a carbon updates their mood #define COMSIG_CARBON_MOOD_UPDATE "carbon_mood_update" +///Called when a carbon attempts to eat (eating) +#define COMSIG_CARBON_ATTEMPT_EAT "carbon_attempt_eat" + // Prevents the breath + #define COMSIG_CARBON_BLOCK_EAT (1 << 0) +///Called when a carbon vomits : (distance, force) +#define COMSIG_CARBON_VOMITED "carbon_vomited" // /mob/living/carbon/human signals @@ -103,9 +118,11 @@ #define COMSIG_HUMAN_EARLY_UNARMED_ATTACK "human_early_unarmed_attack" ///from mob/living/carbon/human/UnarmedAttack(): (atom/target, proximity, modifiers) #define COMSIG_HUMAN_MELEE_UNARMED_ATTACK "human_melee_unarmed_attack" -//from /mob/living/carbon/human/proc/check_shields(): (atom/hit_by, damage, attack_text, attack_type, armour_penetration) +///from /mob/living/carbon/human/proc/check_shields(): (atom/hit_by, damage, attack_text, attack_type, armour_penetration) #define COMSIG_HUMAN_CHECK_SHIELDS "human_check_shields" #define SHIELD_BLOCK (1<<0) +///from /mob/living/carbon/human/proc/force_say(): () +#define COMSIG_HUMAN_FORCESAY "human_forcesay" // Mob transformation signals ///Called when a human turns into a monkey, from /mob/living/carbon/proc/finish_monkeyize() diff --git a/code/__DEFINES/traits.dm b/code/__DEFINES/traits.dm index 2d6ef8cb537f5..363b8233f4f05 100644 --- a/code/__DEFINES/traits.dm +++ b/code/__DEFINES/traits.dm @@ -855,6 +855,8 @@ Remember to update _globalvars/traits.dm if you're adding/removing/renaming trai #define ITEM_SCALING_TRAIT "item_scaling" /// Trait given by Objects that provide blindsight #define ITEM_BLIND_TRAIT "blind_item_trait" +/// Trait given by choking +#define CHOKING_TRAIT "choking_trait" /// Trait given by hallucinations #define HALLUCINATION_TRAIT "hallucination_trait" diff --git a/code/controllers/subsystem/timer.dm b/code/controllers/subsystem/timer.dm index ffc232c49e800..451793f5b9e82 100644 --- a/code/controllers/subsystem/timer.dm +++ b/code/controllers/subsystem/timer.dm @@ -662,6 +662,32 @@ SUBSYSTEM_DEF(timer) return null return timer.timeToRun - (timer.flags & TIMER_CLIENT_TIME ? REALTIMEOFDAY : world.time) +/** + * Update the delay on an existing LOOPING timer + * Will come into effect on the next process + * + * Arguments: + * * id a timerid or a /datum/timedevent + * * new_wait the new wait to give this looping timer + */ +/proc/updatetimedelay(id, new_wait, datum/controller/subsystem/timer/timer_subsystem) + if (!id) + return + if (id == TIMER_ID_NULL) + CRASH("Tried to update the wait of null timerid. Use TIMER_STOPPABLE flag") + if (istype(id, /datum/timedevent)) + var/datum/timedevent/timer = id + timer.wait = new_wait + return + timer_subsystem = timer_subsystem || SStimer + //id is string + var/datum/timedevent/timer = timer_subsystem.timer_id_dict[id] + if(!timer || timer.spent) + return + if(!(timer.flags & TIMER_LOOP)) + CRASH("Tried to update the wait of a non looping timer. This is not supported") + timer.wait = new_wait + #undef BUCKET_LEN #undef BUCKET_POS #undef TIMER_MAX diff --git a/code/datums/components/food/edible.dm b/code/datums/components/food/edible.dm index 97583ef0baf08..9106c6e73ebe0 100644 --- a/code/datums/components/food/edible.dm +++ b/code/datums/components/food/edible.dm @@ -451,6 +451,8 @@ Behavior that's still missing from this component that original food items had t var/who = (isnull(feeder) || eater == feeder) ? "your" : "[eater.p_their()]" to_chat(feeder, span_warning("You have to remove [who] [covered] first!")) return FALSE + if(SEND_SIGNAL(eater, COMSIG_CARBON_ATTEMPT_EAT, parent) & COMSIG_CARBON_BLOCK_EAT) + return return TRUE ///Check foodtypes to see if we should send a moodlet diff --git a/code/datums/looping_sounds/_looping_sound.dm b/code/datums/looping_sounds/_looping_sound.dm index c13ee7bba8ff5..d8b5abc5560c6 100644 --- a/code/datums/looping_sounds/_looping_sound.dm +++ b/code/datums/looping_sounds/_looping_sound.dm @@ -2,12 +2,14 @@ * A datum for sounds that need to loop, with a high amount of configurability. */ /datum/looping_sound - /// The source of the sound, or the recipient of the sound. - var/atom/parent /// (list or soundfile) Since this can be either a list or a single soundfile you can have random sounds. May contain further lists but must contain a soundfile at the end. var/mid_sounds /// The length of time to wait between playing mid_sounds. var/mid_length + /// Amount of time to add/take away from the mid length, randomly + var/mid_length_vary = 0 + /// If we should always play each sound once per loop of all sounds. Weights here only really effect order, and could be disgarded + var/each_once = FALSE /// Override for volume of start sound. var/start_volume /// (soundfile) Played before starting the mid_sounds loop. @@ -26,26 +28,34 @@ var/vary = FALSE /// The max amount of loops to run for. var/max_loops - /// If true, plays directly to provided atoms instead of from them. - var/direct /// The extra range of the sound in tiles, defaults to 0. var/extra_range = 0 - /// The ID of the timer that's used to loop the sounds. - var/timer_id /// How much the sound will be affected by falloff per tile. var/falloff_exponent /// The falloff distance of the sound, var/falloff_distance - /// Do we skip the starting sounds? - var/skip_starting_sounds = FALSE /// Are the sounds affected by pressure? Defaults to TRUE. var/pressure_affected = TRUE /// Are the sounds subject to reverb? Defaults to TRUE. var/use_reverb = TRUE /// Are we ignoring walls? Defaults to TRUE. var/ignore_walls = TRUE + + // State stuff + /// The source of the sound, or the recipient of the sound. + var/atom/parent + /// The ID of the timer that's used to loop the sounds. + var/timer_id /// Has the looping started yet? var/loop_started = FALSE + /// If we're using cut_mid, this is the list we cut from + var/list/cut_list + + // Args + /// Do we skip the starting sounds? + var/skip_starting_sounds = FALSE + /// If true, plays directly to provided atoms instead of from them. + var/direct /datum/looping_sound/New(_parent, start_immediately = FALSE, _direct = FALSE, _skip_starting_sounds = FALSE) if(!mid_sounds) @@ -108,6 +118,9 @@ if(max_loops && world.time >= start_time + mid_length * max_loops) stop() return + // If we have a timer, we're varying mid length, and this is happening while we're runnin mid_sounds + if(timer_id && mid_length_vary && start_time) + updatetimedelay(timer_id, mid_length + rand(-mid_length_vary, mid_length_vary)) if(!chance || prob(chance)) play(get_sound()) @@ -140,10 +153,41 @@ /// Returns the sound we should now be playing. /datum/looping_sound/proc/get_sound(_mid_sounds) - . = _mid_sounds || mid_sounds + var/list/play_from = _mid_sounds || mid_sounds + if(!each_once) + . = play_from + while(!isfile(.) && !isnull(.)) + . = pick_weight(.) + return . + + + if(!length(cut_list)) + cut_list = shuffle(play_from.Copy()) + var/list/tree = list() + . = cut_list while(!isfile(.) && !isnull(.)) + // Tree is a list of lists containign files + // If an entry in the tree goes to 0 length, we cut it from the list + tree += list(.) . = pick_weight(.) + if(!isfile(.)) + return + + // Remove the sound file + tree[length(tree)] -= . + + // Walk the tree bottom up, remove any lists that are empty + // Don't do anything for the topmost list, cause we do not care + for(var/i in length(tree) to 2 step -1) + var/list/branch = tree[i] + if(length(branch)) + break + tree[i - 1] -= list(branch) // Remove the empty list + return . + + + /// A proc that's there to handle delaying the main sounds if there's a start_sound, and simply starting the sound loop in general. /datum/looping_sound/proc/on_start() var/start_wait = 0 diff --git a/code/datums/looping_sounds/choking.dm b/code/datums/looping_sounds/choking.dm new file mode 100644 index 0000000000000..444204efa1fa6 --- /dev/null +++ b/code/datums/looping_sounds/choking.dm @@ -0,0 +1,12 @@ +/datum/looping_sound/choking + mid_sounds = list('sound/creatures/gag1.ogg' = 1, 'sound/creatures/gag2.ogg' = 1, 'sound/creatures/gag3.ogg' = 1, 'sound/creatures/gag4.ogg' = 1, 'sound/creatures/gag5.ogg' = 1) + mid_length = 1.6 SECONDS + mid_length_vary = 0.3 SECONDS + each_once = TRUE + volume = 90 + // We want you to be hard to hear far away + falloff_exponent = 12 + pressure_affected = TRUE + vary = TRUE + // Same as above + ignore_walls = FALSE diff --git a/code/datums/mood_events/generic_negative_events.dm b/code/datums/mood_events/generic_negative_events.dm index cd615a8996852..1bcc3719281d0 100644 --- a/code/datums/mood_events/generic_negative_events.dm +++ b/code/datums/mood_events/generic_negative_events.dm @@ -154,6 +154,10 @@ description = "I'm nervous and on edge and I can't stand still!!" mood_change = -2 +/datum/mood_event/choke + description = "I CAN'T BREATHE!!!" + mood_change = -10 + /datum/mood_event/vomit description = "I just threw up. Gross." mood_change = -2 diff --git a/code/datums/status_effects/debuffs/choke.dm b/code/datums/status_effects/debuffs/choke.dm new file mode 100644 index 0000000000000..27ed856d8bff7 --- /dev/null +++ b/code/datums/status_effects/debuffs/choke.dm @@ -0,0 +1,300 @@ +// Status effect given to you if you're choking on something +/datum/status_effect/choke + id = "choke" + tick_interval = 0.2 SECONDS + alert_type = null + /// Weakref to the thing we're choking on + var/datum/weakref/choking_on_ref + /// If the thing we're choking on is on fire + var/flaming + + /// The particle holder we're using to run our ash effects + var/obj/effect/abstract/particle_holder/ash + /// Our choking audio loop + var/datum/looping_sound/choke_loop + /// The delta client.pixel_y we've got + var/delta_y = 0 + /// The delta client.pixel_x we've got + var/delta_x = 0 + +/datum/status_effect/choke/on_creation(mob/living/new_owner, atom/movable/choke_on, flaming = FALSE, vomit_delay = -1) + choking_on_ref = WEAKREF(choke_on) + src.flaming = flaming + src.duration = vomit_delay + return ..() + +/datum/status_effect/choke/on_apply() + if(!iscarbon(owner)) // Ghosts do not have stomachs, and non carbons can't vomit + return FALSE + var/atom/movable/choking_on = choking_on_ref?.resolve() + if(!choking_on) + return FALSE + // If we've got an item, remove it from storage + if(isitem(choking_on)) + var/obj/item/chokin_item = choking_on + if(chokin_item.item_flags & IN_INVENTORY) + var/mob/inventory_in = chokin_item.loc + inventory_in.transferItemToLoc(chokin_item, owner, force = TRUE, silent = TRUE) + else if(!(chokin_item.item_flags & IN_STORAGE) || !chokin_item.remove_item_from_storage(owner)) + choking_on.forceMove(owner) // backup + else + choking_on.forceMove(owner) + RegisterSignal(owner, SIGNAL_ADDTRAIT(TRAIT_NOBREATH), .proc/no_breathing) + RegisterSignal(owner, COMSIG_MOB_LOGOUT, .proc/on_logout) + RegisterSignal(owner, COMSIG_LIVING_POST_FULLY_HEAL, .proc/remove_choke) + RegisterSignal(owner, COMSIG_LIVING_DEATH, .proc/on_death) + // Of note, this means plasma lovers lose some methods of vomiting up + RegisterSignal(owner, COMSIG_CARBON_VOMITED, .proc/on_vomit) + RegisterSignal(owner, COMSIG_CARBON_ATTEMPT_EAT, .proc/attempt_eat) + RegisterSignal(owner, COMSIG_CARBON_PRE_HELP, .proc/helped) + RegisterSignal(owner, COMSIG_CARBON_PRE_MISC_HELP, .proc/shook) + + RegisterSignal(choking_on, COMSIG_PARENT_QDELETING, .proc/remove_choke) + RegisterSignal(choking_on, COMSIG_MOVABLE_MOVED, .proc/hazard_moved) + ADD_TRAIT(owner, TRAIT_MUTE, CHOKING_TRAIT) + + owner.add_mood_event(id, /datum/mood_event/choke) + //stop on death + choke_loop = new /datum/looping_sound/choking(owner) + check_audio_state() + + owner.visible_message(span_bolddanger("[owner] tries to speak, but can't! They're choking!"), \ + span_userdanger("You try to breath, but there's a block! You're choking!"), \ + ) + + //barticles + if(flaming) + ash = new(owner, /particles/smoke/ash) + var/clear_in = rand(15 SECONDS, 25 SECONDS) + if(duration != -1) + clear_in = min(duration, clear_in) + addtimer(CALLBACK(src, .proc/clear_flame), clear_in) + return TRUE + +/datum/status_effect/choke/proc/should_do_effects() + return owner.stat != DEAD && !HAS_TRAIT(owner, TRAIT_NOBREATH) + +/datum/status_effect/choke/proc/check_audio_state() + if(!should_do_effects()) + choke_loop.stop() + return + choke_loop.start() + +/datum/status_effect/choke/on_remove() + owner.clear_mood_event(id) + REMOVE_TRAIT(owner, TRAIT_MUTE, CHOKING_TRAIT) + clean_client() + clear_flame() + if(choke_loop) + QDEL_NULL(choke_loop) + vomit_up() + return ..() + +/datum/status_effect/choke/proc/clean_client() + // juuust in case, reset our x and y's + var/client/client_owner = owner.canon_client + if(client_owner) + // Ok listen I want to use non relative animates here but BYOND WILL NOT LET ME + // REEEEEEEEE + animate(client_owner, pixel_x = client_owner.pixel_x - delta_x, pixel_y = client_owner.pixel_y - delta_y, 0.05 SECONDS, ANIMATION_PARALLEL) + delta_x = 0 + delta_y = 0 + +/datum/status_effect/choke/proc/clear_flame() + flaming = FALSE + if(ash) + QDEL_NULL(ash) + +/datum/status_effect/choke/proc/vomit_up() + var/atom/movable/choking_on = choking_on_ref?.resolve() + if(choking_on && iscarbon(owner)) + var/mob/living/carbon/carbon_owner = owner + // This will yeet the thing we're choking on out of us + carbon_owner.vomit(lost_nutrition = 20, force = TRUE, distance = 2) + +/datum/status_effect/choke/proc/on_vomit(mob/source, distance, force) + SIGNAL_HANDLER + var/atom/movable/choking_on = choking_on_ref?.resolve() + if(choking_on) + choking_on_ref = null + choking_on.forceMove(get_turf(source)) // Gotta be on a tile to throw yourself bestie + var/atom/target = get_edge_target_turf(source, source.dir) + choking_on.throw_at(target, distance, 1, source) + +/datum/status_effect/choke/get_examine_text() + return span_boldwarning("[owner.p_they(TRUE)] [owner.p_are()] choking!") + +/datum/status_effect/choke/proc/remove_choke(datum/source) + SIGNAL_HANDLER + if(!QDELETED(src)) + qdel(src) + +/datum/status_effect/choke/proc/hazard_moved(atom/movable/source) + SIGNAL_HANDLER + if(QDELETED(src)) + return + if(source.loc != owner) + qdel(src) + +/datum/status_effect/choke/proc/no_breathing(mob/living/source) + SIGNAL_HANDLER + RegisterSignal(source, SIGNAL_REMOVETRAIT(TRAIT_NOBREATH), .proc/on_breathable) + check_audio_state() + +/datum/status_effect/choke/proc/on_breathable(mob/living/source) + SIGNAL_HANDLER + UnregisterSignal(source, SIGNAL_REMOVETRAIT(TRAIT_NOBREATH)) + check_audio_state() + +/datum/status_effect/choke/proc/on_logout(datum/source) + SIGNAL_HANDLER + clean_client() + +/datum/status_effect/choke/proc/on_death(mob/living/source) + SIGNAL_HANDLER + RegisterSignal(source, COMSIG_LIVING_REVIVE, .proc/on_revive) + check_audio_state() + +/datum/status_effect/choke/proc/on_revive(mob/living/source) + SIGNAL_HANDLER + UnregisterSignal(source, COMSIG_LIVING_REVIVE) + check_audio_state() + +/datum/status_effect/choke/proc/attempt_eat(mob/source, atom/eating) + SIGNAL_HANDLER + source.balloon_alert(source, "can't get it down!") + return COMSIG_CARBON_BLOCK_EAT + +/datum/status_effect/choke/proc/helped(mob/source, mob/helping) + SIGNAL_HANDLER + INVOKE_ASYNC(src, .proc/heimlich, source, helping) + return COMPONENT_BLOCK_HELP_ACT + +/datum/status_effect/choke/proc/shook(mob/source, mob/helping) + SIGNAL_HANDLER + INVOKE_ASYNC(src, .proc/heimlich, source, helping) + return COMPONENT_BLOCK_MISC_HELP + +/datum/status_effect/choke/proc/heimlich(mob/victim, mob/aggressor) + if(victim == aggressor) + return + if(DOING_INTERACTION_WITH_TARGET(aggressor, victim)) + victim.balloon_alert(aggressor, "already helping!") + return + if(DOING_INTERACTION(aggressor, "heimlich")) + victim.balloon_alert(aggressor, "already helping someone!") + return + + if(!thrusting_continues(victim, aggressor, before_work = TRUE)) + return + aggressor.start_pulling(victim) + // I want the thing to look vaugely like the heimlich. Sue me + victim.setDir(aggressor.dir) + + var/hand_name = "hand" + if(!iscarbon(aggressor)) + hand_name = "paw" // Fuck you + + var/mob/living/livin_victim = victim + if(iscarbon(aggressor) && livin_victim.body_position == STANDING_UP) + owner.visible_message(span_warning("[aggressor] wraps [aggressor.p_their()] arms around [victim]'s stomach, and begins thrusting [aggressor.p_their()] fists towards themselves!"), \ + span_boldwarning("[aggressor] wraps [aggressor.p_their()] arms around you, and begins thrusting their hands into your chest. [capitalize(GLOB.deity)] that hurts!"), \ + ) + else + owner.visible_message(span_warning("[aggressor] places [aggressor.p_their()] [hand_name]s on [victim]'s back, and begins forcefully striking it!"), \ + span_boldwarning("You feel [aggressor]\s [hand_name]s on your back, and then repeated striking!")) + + if(!do_after_mob(aggressor, victim, 7 SECONDS, extra_checks = CALLBACK(src, .proc/thrusting_continues, victim, aggressor), interaction_key = "heimlich")) + aggressor.stop_pulling() + return + aggressor.stop_pulling() + + var/atom/movable/choking_on = choking_on_ref?.resolve() + owner.visible_message(span_green("[victim] vomits up \the[choking_on]. [victim.p_theyre()] gonna make it!"), \ + span_green("You vomit up that accursed blockage. YOU CAN BREATHE! The broken chest is a hell of a price to pay.")) + if(iscarbon(victim)) + var/mob/living/carbon/carbon_victim = victim + var/obj/item/bodypart/chest = carbon_victim.get_bodypart(BODY_ZONE_CHEST) + if(chest) + chest.force_wound_upwards(/datum/wound/blunt/severe) + playsound(owner, 'sound/creatures/crack_vomit.ogg', 120, extrarange = 5, falloff_exponent = 4) + vomit_up() + +/datum/status_effect/choke/proc/mirror_dir(atom/source, old_dir, new_dir) + SIGNAL_HANDLER + owner.dir = new_dir + +/datum/status_effect/choke/proc/thrusting_continues(mob/living/victim, mob/aggressor, before_work = FALSE) + if(iscarbon(aggressor)) + var/free_hands = 0 + // Listen bud, you need at least 2 free hands for this + for(var/hand_i in 1 to length(aggressor.held_items)) + if(!aggressor.has_hand_for_held_index(hand_i) || aggressor.held_items[hand_i]) + continue + free_hands += 1 + if(free_hands < 2) + victim.balloon_alert(aggressor, "need 2 free hands!") + return FALSE + + if(iscarbon(victim)) + var/mob/living/carbon/carbon_victim = victim + if(!carbon_victim.appears_alive()) + victim.balloon_alert(aggressor, "too late...") + return FALSE + + if(!choking_on_ref) + return FALSE + + if(!before_work) + // This check isn't valid at first because it looks dumb if other things fail + if(victim.pulledby != aggressor) + victim.balloon_alert(aggressor, "must be able to move them!") + return FALSE + + // Similarly, but also this is a burden of knowhow that's cringe + if(aggressor.dir != get_dir(aggressor, victim)) + victim.balloon_alert(aggressor, "must be facing them!") + return FALSE + + // See above + if(victim.dir != aggressor.dir) + victim.balloon_alert(aggressor, "must be facing the same way!") + return FALSE + + // If we ain't starting, deal a tad bit of brute, as a treat + // Note, we attempt to process 10 times a second, so over 7 seconds this'll deal 14 brute + if(!before_work) + victim.adjustBruteLoss(0.2) + return TRUE + +/datum/status_effect/choke/tick(delta_time) + if(!should_do_effects()) + return + + deal_damage(delta_time) + + var/client/client_owner = owner.client + if(client_owner) + do_vfx(client_owner) + +/datum/status_effect/choke/proc/deal_damage(delta_time) + owner.losebreath += 1 * delta_time // 1 breath loss a second. This will deal additional breath damage, and prevent breathing + if(flaming) + var/obj/item/bodypart/head = owner.get_bodypart(BODY_ZONE_HEAD) + if(head) + head.receive_damage(0, 2 * delta_time, 2 * delta_time) + +/datum/status_effect/choke/proc/do_vfx(client/vfx_on) + var/old_x = delta_x + delta_x = WRAP(delta_x + rand(1, 6), -13, 13) + var/old_y = delta_y + delta_y = WRAP(delta_y + rand(1, 6), -13, 13) + // We run on fast process, which procs us once every 0.2 seconds. We want to move every 0.05, so we do 4 animates + // In other news, ANIMATION_RELATIVE does not work on client.pixel_x/y. Thanks byond + animate(vfx_on, pixel_x = vfx_on.pixel_x + (delta_x - old_x), pixel_y = vfx_on.pixel_y + (delta_y - old_y), time = 0.05 SECONDS, flags = ANIMATION_PARALLEL) + for(var/i in 1 to 3) + old_x = delta_x + delta_x = WRAP(delta_x + rand(1, 6), -13, 13) + old_y = delta_y + delta_y = WRAP(delta_y + rand(1, 6), -13, 13) + animate(pixel_x = vfx_on.pixel_x + (delta_x - old_x), pixel_y = vfx_on.pixel_y + (delta_y - old_y), time = 0.05 SECONDS) diff --git a/code/datums/status_effects/debuffs/strandling.dm b/code/datums/status_effects/debuffs/strandling.dm index 9dc9ede90d663..0b63c17401f9c 100644 --- a/code/datums/status_effects/debuffs/strandling.dm +++ b/code/datums/status_effects/debuffs/strandling.dm @@ -12,11 +12,11 @@ /datum/status_effect/strandling/on_apply() RegisterSignal(owner, COMSIG_CARBON_PRE_BREATHE, .proc/on_breathe) RegisterSignal(owner, COMSIG_ATOM_TOOL_ACT(TOOL_WIRECUTTER), .proc/on_cut) - RegisterSignal(owner, COMSIG_CARBON_PRE_HELP_ACT, .proc/on_self_check) + RegisterSignal(owner, COMSIG_CARBON_PRE_MISC_HELP, .proc/on_self_check) return TRUE /datum/status_effect/strandling/on_remove() - UnregisterSignal(owner, list(COMSIG_CARBON_PRE_BREATHE, COMSIG_ATOM_TOOL_ACT(TOOL_WIRECUTTER), COMSIG_CARBON_PRE_HELP_ACT)) + UnregisterSignal(owner, list(COMSIG_CARBON_PRE_BREATHE, COMSIG_ATOM_TOOL_ACT(TOOL_WIRECUTTER), COMSIG_CARBON_PRE_MISC_HELP)) /datum/status_effect/strandling/get_examine_text() return span_warning("[owner.p_they(TRUE)] seem[owner.p_s()] to be being choked by some durathread strands. You may be able to cut them off.") @@ -40,7 +40,7 @@ INVOKE_ASYNC(src, .proc/try_remove_effect, user, tool) return COMPONENT_BLOCK_TOOL_ATTACK -/// Signal proc for [COMSIG_CARBON_PRE_HELP_ACT], allowing someone to remove the effect by hand +/// Signal proc for [COMSIG_CARBON_PRE_MISC_HELP], allowing someone to remove the effect by hand /datum/status_effect/strandling/proc/on_self_check(mob/living/carbon/source, mob/living/helper) SIGNAL_HANDLER @@ -48,7 +48,7 @@ return INVOKE_ASYNC(src, .proc/try_remove_effect, helper) - return COMPONENT_BLOCK_HELP_ACT + return COMPONENT_BLOCK_MISC_HELP /** * Attempts a do_after to remove the effect and stop the strangling. diff --git a/code/game/objects/effects/particles/smoke.dm b/code/game/objects/effects/particles/smoke.dm index 3355dffa6da65..85ad0e30ba11c 100644 --- a/code/game/objects/effects/particles/smoke.dm +++ b/code/game/objects/effects/particles/smoke.dm @@ -22,3 +22,13 @@ spawning = 1 velocity = list(0, 0.3, 0) friction = 0.25 + +/particles/smoke/ash + icon_state = list("ash_1" = 2, "ash_2" = 2, "ash_3" = 1, "smoke_1" = 3, "smoke_2" = 2) + count = 500 + spawning = 1 + lifespan = 1 SECONDS + fade = 0.2 SECONDS + fadein = 0.7 SECONDS + position = generator("vector", list(-3, 5, 0), list(3, 6.5, 0), NORMAL_RAND) + velocity = generator("vector", list(-0.1, 0.4, 0), list(0.1, 0.5, 0), NORMAL_RAND) diff --git a/code/game/objects/items/cigs_lighters.dm b/code/game/objects/items/cigs_lighters.dm index 0012e1719dfd3..f71d9a95f0190 100644 --- a/code/game/objects/items/cigs_lighters.dm +++ b/code/game/objects/items/cigs_lighters.dm @@ -163,7 +163,10 @@ CIGARETTE PACKETS ARE IN FANCY.DM var/smoke_all = FALSE /// How much damage this deals to the lungs per drag. var/lung_harm = 1 - + /// If, when glorf'd, we will choke on this cig forever + var/choke_forever = FALSE + /// When choking, what is the maximum amount of time we COULD choke for + var/choke_time_max = 30 SECONDS // I am mean /obj/item/clothing/mask/cigarette/Initialize(mapload) . = ..() @@ -181,6 +184,21 @@ CIGARETTE PACKETS ARE IN FANCY.DM STOP_PROCESSING(SSobj, src) return ..() +/obj/item/clothing/mask/cigarette/equipped(mob/equipee, slot) + . = ..() + if(!(slot & ITEM_SLOT_MASK)) + UnregisterSignal(equipee, COMSIG_HUMAN_FORCESAY) + return + RegisterSignal(equipee, COMSIG_HUMAN_FORCESAY, .proc/on_forcesay) + +/obj/item/clothing/mask/cigarette/dropped(mob/dropee) + . = ..() + UnregisterSignal(dropee, COMSIG_HUMAN_FORCESAY) + +/obj/item/clothing/mask/cigarette/proc/on_forcesay(mob/living/source) + SIGNAL_HANDLER + source.apply_status_effect(/datum/status_effect/choke, src, lit, choke_forever ? -1 : rand(25 SECONDS, choke_time_max)) + /obj/item/clothing/mask/cigarette/suicide_act(mob/user) user.visible_message(span_suicide("[user] is huffing [src] as quickly as [user.p_they()] can! It looks like [user.p_theyre()] trying to give [user.p_them()]self cancer.")) return (TOXLOSS|OXYLOSS) @@ -431,6 +449,7 @@ CIGARETTE PACKETS ARE IN FANCY.DM smoketime = 4 MINUTES chem_volume = 50 list_reagents = null + choke_time_max = 40 SECONDS /obj/item/clothing/mask/cigarette/rollie/Initialize(mapload) name = pick(list( @@ -503,6 +522,7 @@ CIGARETTE PACKETS ARE IN FANCY.DM heat = 473.15 // Lowered so that the sugar can be carmalized, but not burnt. lung_harm = 0.5 list_reagents = list(/datum/reagent/consumable/sugar = 20) + choke_time_max = 70 SECONDS // This shit really is deadly /obj/item/clothing/mask/cigarette/candy/nicotine desc = "For all ages*! Doesn't contain any* amount of nicotine. Health and safety risks can be read on the tip of the cigarette." @@ -538,6 +558,7 @@ CIGARETTE PACKETS ARE IN FANCY.DM smoketime = 11 MINUTES chem_volume = 40 list_reagents = list(/datum/reagent/drug/nicotine = 25) + choke_time_max = 40 SECONDS /obj/item/clothing/mask/cigarette/cigar/cohiba name = "\improper Cohiba Robusto cigar" @@ -589,6 +610,7 @@ CIGARETTE PACKETS ARE IN FANCY.DM chem_volume = 200 // So we can fit densified chemicals plants list_reagents = null w_class = WEIGHT_CLASS_SMALL + choke_forever = TRUE ///name of the stuff packed inside this pipe var/packeditem diff --git a/code/modules/mob/living/carbon/carbon.dm b/code/modules/mob/living/carbon/carbon.dm index ee20c9f02deec..40a8d12e828a2 100644 --- a/code/modules/mob/living/carbon/carbon.dm +++ b/code/modules/mob/living/carbon/carbon.dm @@ -417,6 +417,8 @@ if((HAS_TRAIT(src, TRAIT_NOHUNGER) || HAS_TRAIT(src, TRAIT_TOXINLOVER)) && !force) return TRUE + SEND_SIGNAL(src, COMSIG_CARBON_VOMITED, distance, force) + var/starting_dir = dir if(nutrition < 100 && !blood && !force) if(message) visible_message(span_warning("[src] dry heaves!"), \ @@ -455,7 +457,7 @@ else if(T) T.add_vomit_floor(src, vomit_type, purge_ratio) //toxic barf looks different || call purge when doing detoxicfication to pump more chems out of the stomach. - T = get_step(T, dir) + T = get_step(T, starting_dir) if (T?.is_blocked_turf()) break return TRUE diff --git a/code/modules/mob/living/carbon/carbon_defense.dm b/code/modules/mob/living/carbon/carbon_defense.dm index c051260aa0d5b..dc853c72d19e1 100644 --- a/code/modules/mob/living/carbon/carbon_defense.dm +++ b/code/modules/mob/living/carbon/carbon_defense.dm @@ -403,7 +403,7 @@ to_chat(helper, span_warning("You can't put [p_them()] out with just your bare hands!")) return - if(SEND_SIGNAL(src, COMSIG_CARBON_PRE_HELP_ACT, helper) & COMPONENT_BLOCK_HELP_ACT) + if(SEND_SIGNAL(src, COMSIG_CARBON_PRE_MISC_HELP, helper) & COMPONENT_BLOCK_MISC_HELP) return if(helper == src) diff --git a/code/modules/mob/living/carbon/human/species.dm b/code/modules/mob/living/carbon/human/species.dm index 5647ea8e68b2d..85e60d1507dfa 100644 --- a/code/modules/mob/living/carbon/human/species.dm +++ b/code/modules/mob/living/carbon/human/species.dm @@ -1068,9 +1068,11 @@ GLOBAL_LIST_EMPTY(features_by_species) /datum/species/proc/spec_fully_heal(mob/living/carbon/human/H) return - /datum/species/proc/help(mob/living/carbon/human/user, mob/living/carbon/human/target, datum/martial_art/attacker_style) - if(target.body_position == STANDING_UP || (target.health >= 0 && !HAS_TRAIT(target, TRAIT_FAKEDEATH))) + if(SEND_SIGNAL(target, COMSIG_CARBON_PRE_HELP, user, attacker_style) & COMPONENT_BLOCK_HELP_ACT) + return TRUE + + if(target.body_position == STANDING_UP || target.appears_alive()) target.help_shake_act(user) if(target != user) log_combat(user, target, "shaken") diff --git a/code/modules/mob/living/carbon/life.dm b/code/modules/mob/living/carbon/life.dm index b13fbea94377c..da21621091ea7 100644 --- a/code/modules/mob/living/carbon/life.dm +++ b/code/modules/mob/living/carbon/life.dm @@ -71,7 +71,7 @@ //Second link in a breath chain, calls check_breath() /mob/living/carbon/proc/breathe(delta_time, times_fired) var/obj/item/organ/internal/lungs = getorganslot(ORGAN_SLOT_LUNGS) - if(reagents.has_reagent(/datum/reagent/toxin/lexorin, needs_metabolizing = TRUE)) + if(SEND_SIGNAL(src, COMSIG_CARBON_ATTEMPT_BREATHE) & COMSIG_CARBON_BLOCK_BREATH) return SEND_SIGNAL(src, COMSIG_CARBON_PRE_BREATHE) @@ -151,7 +151,7 @@ failed_last_breath = TRUE throw_alert(ALERT_NOT_ENOUGH_OXYGEN, /atom/movable/screen/alert/not_enough_oxy) return FALSE - + var/safe_oxy_min = 16 var/safe_co2_max = 10 var/safe_plas_max = 0.05 diff --git a/code/modules/mob/living/status_procs.dm b/code/modules/mob/living/status_procs.dm index c14a3e274d5f7..c5af051f4cee5 100644 --- a/code/modules/mob/living/status_procs.dm +++ b/code/modules/mob/living/status_procs.dm @@ -761,3 +761,7 @@ /mob/living/proc/get_drunk_amount() var/datum/status_effect/inebriated/inebriation = has_status_effect(/datum/status_effect/inebriated) return inebriation?.drunk_value || 0 + +/// Helper to check if we seem to be alive or not +/mob/living/proc/appears_alive() + return health >= 0 && !HAS_TRAIT(src, TRAIT_FAKEDEATH) diff --git a/code/modules/reagents/chemistry/reagents/toxin_reagents.dm b/code/modules/reagents/chemistry/reagents/toxin_reagents.dm index 308c0ace3c633..a2286ddc247a9 100644 --- a/code/modules/reagents/chemistry/reagents/toxin_reagents.dm +++ b/code/modules/reagents/chemistry/reagents/toxin_reagents.dm @@ -176,6 +176,16 @@ C.emote("gasp") ..() +/datum/reagent/toxin/lexorin/on_mob_metabolize(mob/living/L) + RegisterSignal(L, COMSIG_CARBON_ATTEMPT_BREATHE, .proc/block_breath) + +/datum/reagent/toxin/lexorin/on_mob_end_metabolize(mob/living/L) + UnregisterSignal(L, COMSIG_CARBON_ATTEMPT_BREATHE, .proc/block_breath) + +/datum/reagent/toxin/lexorin/proc/block_breath(mob/living/source) + SIGNAL_HANDLER + return COMSIG_CARBON_BLOCK_BREATH + /datum/reagent/toxin/slimejelly name = "Slime Jelly" description = "A gooey semi-liquid produced from one of the deadliest lifeforms in existence. SO REAL." diff --git a/code/modules/tgui_input/say_modal/speech.dm b/code/modules/tgui_input/say_modal/speech.dm index bf357133a7d55..eff380b5d5913 100644 --- a/code/modules/tgui_input/say_modal/speech.dm +++ b/code/modules/tgui_input/say_modal/speech.dm @@ -65,6 +65,7 @@ log_speech_indicators("[key_name(client)] FORCED to stop typing, indicators enabled.") else log_speech_indicators("[key_name(client)] FORCED to stop typing, indicators DISABLED.") + SEND_SIGNAL(src, COMSIG_HUMAN_FORCESAY) /** * Handles text entry and forced speech. diff --git a/dependencies.sh b/dependencies.sh index d41a85879ab08..ca17f1f9e1ef3 100644 --- a/dependencies.sh +++ b/dependencies.sh @@ -15,7 +15,7 @@ export NODE_VERSION=14 export NODE_VERSION_PRECISE=14.16.1 # SpacemanDMM git tag -export SPACEMAN_DMM_VERSION=suite-1.7.1 +export SPACEMAN_DMM_VERSION=suite-1.7.2 # Python version for mapmerge and other tools export PYTHON_VERSION=3.7.9 diff --git a/icons/effects/particles/smoke.dmi b/icons/effects/particles/smoke.dmi index ba52a5d80be3f..4a3239499b965 100644 Binary files a/icons/effects/particles/smoke.dmi and b/icons/effects/particles/smoke.dmi differ diff --git a/sound/creatures/crack_vomit.ogg b/sound/creatures/crack_vomit.ogg new file mode 100644 index 0000000000000..93c3606c3b864 Binary files /dev/null and b/sound/creatures/crack_vomit.ogg differ diff --git a/sound/creatures/gag1.ogg b/sound/creatures/gag1.ogg new file mode 100644 index 0000000000000..6b2781a9b13e7 Binary files /dev/null and b/sound/creatures/gag1.ogg differ diff --git a/sound/creatures/gag2.ogg b/sound/creatures/gag2.ogg new file mode 100644 index 0000000000000..ae11cd25b41b6 Binary files /dev/null and b/sound/creatures/gag2.ogg differ diff --git a/sound/creatures/gag3.ogg b/sound/creatures/gag3.ogg new file mode 100644 index 0000000000000..4424fe5da392a Binary files /dev/null and b/sound/creatures/gag3.ogg differ diff --git a/sound/creatures/gag4.ogg b/sound/creatures/gag4.ogg new file mode 100644 index 0000000000000..6b2c1eaef6ffc Binary files /dev/null and b/sound/creatures/gag4.ogg differ diff --git a/sound/creatures/gag5.ogg b/sound/creatures/gag5.ogg new file mode 100644 index 0000000000000..89ffa21dc084b Binary files /dev/null and b/sound/creatures/gag5.ogg differ diff --git a/tgstation.dme b/tgstation.dme index 52605c6fa0446..767f3af63fd47 100644 --- a/tgstation.dme +++ b/tgstation.dme @@ -1128,6 +1128,7 @@ #include "code\datums\keybinding\robot.dm" #include "code\datums\looping_sounds\_looping_sound.dm" #include "code\datums\looping_sounds\acid.dm" +#include "code\datums\looping_sounds\choking.dm" #include "code\datums\looping_sounds\cyborg.dm" #include "code\datums\looping_sounds\item_sounds.dm" #include "code\datums\looping_sounds\machinery_sounds.dm" @@ -1227,6 +1228,7 @@ #include "code\datums\status_effects\song_effects.dm" #include "code\datums\status_effects\stacking_effect.dm" #include "code\datums\status_effects\wound_effects.dm" +#include "code\datums\status_effects\debuffs\choke.dm" #include "code\datums\status_effects\debuffs\confusion.dm" #include "code\datums\status_effects\debuffs\debuffs.dm" #include "code\datums\status_effects\debuffs\dizziness.dm"