diff --git a/code/__DEFINES/atmospherics/atmos_mob_interaction.dm b/code/__DEFINES/atmospherics/atmos_mob_interaction.dm index a283a78a70c4..e3419ebc0b88 100644 --- a/code/__DEFINES/atmospherics/atmos_mob_interaction.dm +++ b/code/__DEFINES/atmospherics/atmos_mob_interaction.dm @@ -11,6 +11,7 @@ /// Amount of air to take a from a tile #define BREATH_PERCENTAGE (BREATH_VOLUME/CELL_VOLUME) + #define QUANTIZE(variable) (round((variable), (MOLAR_ACCURACY))) /// Return this from a while_present proc to call its on_loss version, if one exists @@ -148,7 +149,7 @@ #define LOW_PRESSURE_DAMAGE 2 /// Humans are slowed by the difference between bodytemp and BODYTEMP_COLD_DAMAGE_LIMIT divided by this -#define COLD_SLOWDOWN_FACTOR 20 +#define COLD_SLOWDOWN_FACTOR 100 //CLOTHES @@ -195,5 +196,3 @@ ///Minimum temperature for items on fire #define BURNING_ITEM_MINIMUM_TEMPERATURE (150+T0C) - -#define SKIP_INTERNALS "skip_internals" diff --git a/code/__DEFINES/bodyparts.dm b/code/__DEFINES/bodyparts.dm index 6ed6d2135c6f..5c6cfa28c73b 100644 --- a/code/__DEFINES/bodyparts.dm +++ b/code/__DEFINES/bodyparts.dm @@ -5,13 +5,13 @@ /// The max damage a limb can take before it stops taking damage. /// Used by the max_damage var. -#define LIMB_MAX_HP_DEFAULT 150 // -#define LIMB_MAX_HP_CORE 600 // +#define LIMB_MAX_HP_DEFAULT 75 +#define LIMB_MAX_HP_CORE LIMB_MAX_HP_DEFAULT * 3 /// Xenomorph Limbs -#define LIMB_MAX_HP_ALIEN_LARVA 100 // 50 //Used by the weird larva chest and head. Did you know they have those? -#define LIMB_MAX_HP_ALIEN_LIMBS 300 // 100 //Used by xenomorph limbs. -#define LIMB_MAX_HP_ALIEN_CORE 1500 // 500 //Used by xenomorph chests and heads +#define LIMB_MAX_HP_ALIEN_LARVA 50 //Used by the weird larva chest and head. Did you know they have those? +#define LIMB_MAX_HP_ALIEN_LIMBS 100 //Used by xenomorph limbs. +#define LIMB_MAX_HP_ALIEN_CORE 500 //Used by xenomorph chests and heads // EMP // Note most of these values are doubled on heavy EMP diff --git a/code/__DEFINES/living.dm b/code/__DEFINES/living.dm index 9922b4233736..53027749cdc6 100644 --- a/code/__DEFINES/living.dm +++ b/code/__DEFINES/living.dm @@ -23,6 +23,7 @@ #define HANDLE_BLOOD_NO_EFFECTS (1<<2) /// Various lists of body zones affected by pain. + #define BODY_ZONES_ALL list(BODY_ZONE_HEAD, BODY_ZONE_CHEST, BODY_ZONE_L_ARM, BODY_ZONE_R_ARM, BODY_ZONE_L_LEG, BODY_ZONE_R_LEG) #define BODY_ZONES_MINUS_HEAD list(BODY_ZONE_CHEST, BODY_ZONE_L_ARM, BODY_ZONE_R_ARM, BODY_ZONE_L_LEG, BODY_ZONE_R_LEG) #define BODY_ZONES_LIMBS list(BODY_ZONE_L_ARM, BODY_ZONE_R_ARM, BODY_ZONE_L_LEG, BODY_ZONE_R_LEG) @@ -146,6 +147,11 @@ /// Updates associated self-huds on the mob #define UPDATE_SELF (UPDATE_SELF_DAMAGE | UPDATE_SELF_HEALTH) +/// Threshold that heart beat becomes "slow" +#define SLOW_HEARTBEAT_THRESHOLD 6 +/// Threshold that heart beat becomes "fast" +#define FAST_HEARTBEAT_THRESHOLD 11 + // Used in living mob offset list for determining pixel offsets #define PIXEL_W_OFFSET "w" #define PIXEL_X_OFFSET "x" diff --git a/code/__DEFINES/sound.dm b/code/__DEFINES/sound.dm index df79800b0bc6..2cdaf64b11a3 100644 --- a/code/__DEFINES/sound.dm +++ b/code/__DEFINES/sound.dm @@ -187,5 +187,6 @@ GLOBAL_LIST_INIT(announcer_keys, list( #define SFX_MALE_SIGH "male_sigh" #define SFX_FEMALE_SIGH "female_sigh" #define SFX_WRITING_PEN "writing_pen" +#define SFX_CLOTH_RIP "cloth_rip" #define SFX_SEATBELT_BUCKLE "buckle" #define SFX_SEATBELT_UNBUCKLE "unbuckle" diff --git a/code/__DEFINES/status_effects.dm b/code/__DEFINES/status_effects.dm index bee51c529055..4e901c4ba2ce 100644 --- a/code/__DEFINES/status_effects.dm +++ b/code/__DEFINES/status_effects.dm @@ -6,8 +6,6 @@ #define STATUS_EFFECT_REPLACE 2 /// if it only allows one, and new instances just instead refresh the timer #define STATUS_EFFECT_REFRESH 3 -/// Like [STATUS_EFFECT_REPLACE] but only for differing types - Only replace if the new effect is of a different typepath -#define STATUS_EFFECT_LOOSE_REPLACE 4 ///Processing flags - used to define the speed at which the status will work ///This is fast - 0.2s between ticks (I believe!) diff --git a/code/__DEFINES/traits/declarations.dm b/code/__DEFINES/traits/declarations.dm index 70251221d714..a32f6a41c1af 100644 --- a/code/__DEFINES/traits/declarations.dm +++ b/code/__DEFINES/traits/declarations.dm @@ -526,10 +526,6 @@ Remember to update _globalvars/traits.dm if you're adding/removing/renaming trai #define TRAIT_MAINTENANCE_METABOLISM "maintenance_metabolism" #define TRAIT_CORONER_METABOLISM "coroner_metabolism" -//LUNG TRAITS -/// Lungs always breathe normally when in vacuum/space. -#define TRAIT_SPACEBREATHING "spacebreathing" - /// This mob can strip other mobs. #define TRAIT_CAN_STRIP "can_strip" /// Can use the nuclear device's UI, regardless of a lack of hands diff --git a/code/__DEFINES/vv.dm b/code/__DEFINES/vv.dm index dfa930f7a76b..64522d2ba8c3 100644 --- a/code/__DEFINES/vv.dm +++ b/code/__DEFINES/vv.dm @@ -157,6 +157,7 @@ #define VV_HK_PURRBATION "purrbation" #define VV_HK_APPLY_DNA_INFUSION "apply_dna_infusion" #define VV_HK_TURN_INTO_MMI "turn_into_mmi" +#define VV_HK_MAKE_ME_TANKY "make_me_tanky" // misc #define VV_HK_SPACEVINE_PURGE "spacevine_purge" diff --git a/code/__DEFINES/wounds.dm b/code/__DEFINES/wounds.dm index 6836483f193f..8054f2c95b1b 100644 --- a/code/__DEFINES/wounds.dm +++ b/code/__DEFINES/wounds.dm @@ -291,8 +291,8 @@ GLOBAL_LIST_INIT(biotypes_to_scar_file, list( // ~random wound balance defines /// how quickly sanitization removes infestation and decays per second #define WOUND_BURN_SANITIZATION_RATE 0.075 -/// how much blood you can lose per tick per slash max. -#define WOUND_SLASH_MAX_BLOODFLOW 4.5 +/// how much blood you can lose per tick per wound max. +#define WOUND_MAX_BLOODFLOW 4.5 /// further slash attacks on a bodypart with a slash wound have their blood_flow further increased by damage * this (10 damage slash adds .25 flow) #define WOUND_SLASH_DAMAGE_FLOW_COEFF 0.025 /// if we suffer a bone wound to the head that creates brain traumas, the timer for the trauma cycle is +/- by this percent (0-100) diff --git a/code/_compile_options.dm b/code/_compile_options.dm index f3fe5618ac15..922387ae8e3f 100644 --- a/code/_compile_options.dm +++ b/code/_compile_options.dm @@ -152,6 +152,11 @@ #warn Compiling in MAP_TEST mode. Certain game mechanics will be disabled. #endif +// For balancing health +// #define HEALTH_DEBUG +// For debugging pain +// #define PAIN_DEBUG + /// Uncomment to compile content related to the story or characters of Maplestation. // #define MAPLESTATION_STORY_CONTENT diff --git a/code/_globalvars/traits/_traits.dm b/code/_globalvars/traits/_traits.dm index a297027807d2..e5d3edf8fa2b 100644 --- a/code/_globalvars/traits/_traits.dm +++ b/code/_globalvars/traits/_traits.dm @@ -562,9 +562,6 @@ GLOBAL_LIST_INIT(traits_by_type, list( "TRAIT_PRETENDER_ROYAL_METABOLISM" = TRAIT_PRETENDER_ROYAL_METABOLISM, "TRAIT_ROYAL_METABOLISM" = TRAIT_ROYAL_METABOLISM, ), - /obj/item/organ/internal/lungs = list( - "TRAIT_SPACEBREATHING" = TRAIT_SPACEBREATHING, - ), /obj/item/reagent_containers = list( "TRAIT_MAY_CONTAIN_BLENDED_DUST" = TRAIT_MAY_CONTAIN_BLENDED_DUST, ), diff --git a/code/_globalvars/traits/admin_tooling.dm b/code/_globalvars/traits/admin_tooling.dm index 44c2ab704973..d2e486eb806f 100644 --- a/code/_globalvars/traits/admin_tooling.dm +++ b/code/_globalvars/traits/admin_tooling.dm @@ -273,9 +273,6 @@ GLOBAL_LIST_INIT(admin_visible_traits, list( "TRAIT_PRETENDER_ROYAL_METABOLISM" = TRAIT_PRETENDER_ROYAL_METABOLISM, "TRAIT_ROYAL_METABOLISM" = TRAIT_ROYAL_METABOLISM, ), - /obj/item/organ/internal/lungs = list( - "TRAIT_SPACEBREATHING" = TRAIT_SPACEBREATHING, - ), )) /// value -> trait name, generated as needed for adminning. diff --git a/code/datums/brain_damage/mild.dm b/code/datums/brain_damage/mild.dm index 953f9323fe48..50287486825d 100644 --- a/code/datums/brain_damage/mild.dm +++ b/code/datums/brain_damage/mild.dm @@ -136,7 +136,7 @@ var/fall_chance = 1 if(owner.move_intent == MOVE_INTENT_RUN) fall_chance += 2 - if(SPT_PROB(0.5 * fall_chance, seconds_per_tick) && owner.body_position == STANDING_UP) + if(SPT_PROB(0.5 * fall_chance, seconds_per_tick) && owner.body_position == STANDING_UP && !owner.buckled) to_chat(owner, span_warning("Your leg gives out!")) owner.Paralyze(35) @@ -147,7 +147,7 @@ if(SPT_PROB(0.5 * drop_chance, seconds_per_tick) && owner.dropItemToGround(I)) to_chat(owner, span_warning("You drop [I]!")) - else if(SPT_PROB(1.5, seconds_per_tick)) + else if(SPT_PROB(1.5, seconds_per_tick) && owner.getStaminaLoss() < 100) to_chat(owner, span_warning("You feel a sudden weakness in your muscles!")) owner.apply_damage(25, STAMINA, BODY_ZONE_L_LEG) owner.apply_damage(25, STAMINA, BODY_ZONE_R_LEG) diff --git a/code/datums/elements/digitalcamo.dm b/code/datums/elements/digitalcamo.dm index 3f4db60de2d5..d590fe5817e2 100644 --- a/code/datums/elements/digitalcamo.dm +++ b/code/datums/elements/digitalcamo.dm @@ -39,10 +39,10 @@ var/datum/atom_hud/S = GLOB.huds[AI.sec_hud] S.unhide_single_atomhud_from(AI,target) -/datum/element/digitalcamo/proc/on_examine(datum/source, mob/M) +/datum/element/digitalcamo/proc/on_examine(datum/source, mob/M, list/examine_list) SIGNAL_HANDLER - to_chat(M, span_warning("[source.p_their()] skin seems to be shifting like something is moving below it.")) + examine_list += span_danger("[source.p_Their()] skin seems to be shifting like something is moving below it.") /datum/element/digitalcamo/proc/can_track(datum/source, mob/user) SIGNAL_HANDLER diff --git a/code/datums/mood.dm b/code/datums/mood.dm index b4c4ba8b7c6e..24b6e2330073 100644 --- a/code/datums/mood.dm +++ b/code/datums/mood.dm @@ -340,11 +340,11 @@ if(0 to NUTRITION_LEVEL_STARVING) msg += span_boldwarning("I'm starving!\n") - var/drunkness = mob_parent.get_timed_status_effect_duration(/datum/status_effect/inebriated) + var/drunkness = mob_parent.get_drunk_amount() if(drunkness >= 1) msg += span_notice("My current drunkenness: ") switch(drunkness) - if(1 to 10) + if(0 to 10) msg += span_info("I'm feeling a little tipsy.\n") if(11 to 21) msg += span_info("I'm feeling a bit drunk.\n") diff --git a/code/datums/status_effects/_status_effect_helpers.dm b/code/datums/status_effects/_status_effect_helpers.dm index 77d6f965df35..144a972da621 100644 --- a/code/datums/status_effects/_status_effect_helpers.dm +++ b/code/datums/status_effects/_status_effect_helpers.dm @@ -37,11 +37,6 @@ if(STATUS_EFFECT_REFRESH) existing_effect.refresh(arglist(arguments)) return - // Replace but only if the type differs - if(STATUS_EFFECT_LOOSE_REPLACE) - if(existing_effect.type == new_effect) - return - existing_effect.be_replaced() // Create the status effect with our mob + our arguments var/datum/status_effect/new_instance = new new_effect(arguments) diff --git a/code/datums/status_effects/buffs.dm b/code/datums/status_effects/buffs.dm index 8208c1975ad5..96b78e41e3be 100644 --- a/code/datums/status_effects/buffs.dm +++ b/code/datums/status_effects/buffs.dm @@ -601,3 +601,27 @@ /datum/status_effect/jump_jet/on_remove() owner.RemoveElement(/datum/element/forced_gravity, 0) + +/datum/status_effect/basic_health_regen + id = "basic_health_regen" + status_type = STATUS_EFFECT_MULTIPLE + tick_interval = 1 SECONDS + duration = INFINITY + alert_type = null + /// How much to heal per second + var/modifier = 1 + +/datum/status_effect/basic_health_regen/on_creation(mob/living/new_owner, modifier = 1) + src.modifier = modifier + return ..() + +/datum/status_effect/basic_health_regen/tick(seconds_between_ticks) + if(owner.stat == DEAD) + return + owner.adjustBruteLoss(-1 * modifier * seconds_between_ticks) + owner.adjustFireLoss(-1 * modifier * seconds_between_ticks) + owner.adjustToxLoss(-1 * modifier * seconds_between_ticks) + owner.adjustOxyLoss(-1 * modifier * seconds_between_ticks) + if(owner.blood_volume < BLOOD_VOLUME_NORMAL) + owner.blood_volume += modifier * seconds_between_ticks + owner.cause_pain(BODY_ZONES_ALL, -0.5 * modifier * seconds_between_ticks) diff --git a/code/datums/status_effects/debuffs/cursed.dm b/code/datums/status_effects/debuffs/cursed.dm index 8d331bbe90ad..40cc9e187794 100644 --- a/code/datums/status_effects/debuffs/cursed.dm +++ b/code/datums/status_effects/debuffs/cursed.dm @@ -66,7 +66,7 @@ playsound(human_owner, SFX_SEAR, 50, TRUE) var/obj/item/bodypart/affecting = human_owner.get_active_hand() branded_hand = affecting - affecting.force_wound_upwards(/datum/wound/burn/flesh/severe/cursed_brand, wound_source = "curse of the slot machine") + affecting.force_wound_upwards(/datum/wound/flesh/burn/severe/cursed_brand, wound_source = "curse of the slot machine") messages += span_boldwarning("Your hand burns, and you quickly let go of the lever! You feel a little sick as the nerves deaden in your hand...") messages += span_boldwarning("Some smoke appears to be coming out of your hand now, but it's not too bad...") @@ -116,7 +116,7 @@ SIGNAL_HANDLER if(!isnull(branded_hand)) - var/datum/wound/brand = branded_hand.get_wound_type(/datum/wound/burn/flesh/severe/cursed_brand) + var/datum/wound/brand = branded_hand.get_wound_type(/datum/wound/flesh/burn/severe/cursed_brand) brand?.remove_wound() owner.visible_message( diff --git a/code/datums/status_effects/debuffs/thermia.dm b/code/datums/status_effects/debuffs/thermia.dm index 3a8342f443a3..44f5b17b76c8 100644 --- a/code/datums/status_effects/debuffs/thermia.dm +++ b/code/datums/status_effects/debuffs/thermia.dm @@ -1,84 +1,262 @@ /// Hypo and Hyperthermia status effects. /datum/status_effect/thermia - id = "thermia" alert_type = null - status_type = STATUS_EFFECT_LOOSE_REPLACE - tick_interval = 3 SECONDS + tick_interval = 2 SECONDS processing_speed = STATUS_EFFECT_NORMAL_PROCESS - /// Flat penalty of consciousness applied over time - var/consciousness_mod = 0 - var/max_consciousness_mod = 0 - /// Cooldown for updating the consciousness mod, since we don't want to do it constantly - COOLDOWN_DECLARE(update_cd) + remove_on_fullheal = TRUE + heal_flag_necessary = HEAL_TEMP + /// What trait resists this status effect? + var/resist_trait + /// tracks how long the effect has been active + VAR_PRIVATE/seconds_active = 0 + /// tracks the intensity of the effect. goes up to 100 (which is "uh oh" territory) + /// intensity increases over time, scaling with how far the body temperature is from the threshold and how long the effect has been active + VAR_PRIVATE/intensity = 0 + /// Used to prevent spamming the player with the same warning + VAR_PRIVATE/last_warning = 0 + /// Message displayed early on to warn the player it's starting + var/early_warning = "" + /// Message displayed mid-way to warn about worsening condition + var/mid_warning = "" + /// Message displayed when the condition is severe and nearing critical + var/late_warning = "" /datum/status_effect/thermia/on_apply() - if(consciousness_mod) - owner.add_consciousness_modifier(id, 0) - if(max_consciousness_mod) - owner.add_max_consciousness_value(id, UPPER_CONSCIOUSNESS_MAX) + if(HAS_TRAIT(owner, resist_trait)) + return FALSE - COOLDOWN_START(src, update_cd, 6 SECONDS) + RegisterSignal(owner, SIGNAL_ADDTRAIT(resist_trait), PROC_REF(clear_effect)) + RegisterSignal(owner, COMSIG_LIVING_HEALTHSCAN, PROC_REF(on_scan)) return TRUE /datum/status_effect/thermia/on_remove() owner.remove_consciousness_modifier(id) owner.remove_max_consciousness_value(id) + UnregisterSignal(owner, SIGNAL_ADDTRAIT(resist_trait)) + UnregisterSignal(owner, COMSIG_LIVING_HEALTHSCAN) -/datum/status_effect/thermia/proc/check_remove() - return FALSE +/datum/status_effect/thermia/proc/clear_effect(datum/source) + SIGNAL_HANDLER + qdel(src) -/datum/status_effect/thermia/tick(seconds_between_ticks) - if(!COOLDOWN_FINISHED(src, update_cd)) - return +/datum/status_effect/thermia/proc/on_scan(datum/source, list/report, advanced, ...) + SIGNAL_HANDLER - if(check_remove()) + report += "[id] detected.[advanced ? " ([round(intensity)]%)" : ""]
" + +/datum/status_effect/thermia/proc/adjust_intensity(change, min = 0, max = 100) + intensity = clamp(intensity + change, min, max) + if(change <= 0 && intensity <= 0) qdel(src) return - if(!iscarbon(owner)) - return + // updating consciousness / max consciousness + owner.add_consciousness_modifier(id, intensity * -0.5) + if(intensity >= 75) + owner.add_max_consciousness_value(id, 50) + else if(intensity >= 90) + owner.add_max_consciousness_value(id, 25) + else + owner.remove_max_consciousness_value(id) - var/mob/living/carbon/carbowner = owner - // Counts up from 0 to [consciousness_mod] - if(consciousness_mod) - var/current_mod = LAZYACCESS(carbowner.consciousness_modifiers, id) * -1 - if(current_mod < consciousness_mod) - owner.add_consciousness_modifier(id, min(consciousness_mod, current_mod + 5) * -1) + // resetting warnings + if(change <= 0) + if(intensity <= 5) + last_warning = 0 + else if(intensity <= 40) + last_warning = 1 + else if(intensity <= 75) + last_warning = 2 + +/datum/status_effect/thermia/tick(seconds_between_ticks) + if(HAS_TRAIT(owner, TRAIT_STASIS) || owner.stat == DEAD) + return + seconds_active += seconds_between_ticks + update_intenisty_per_temp(seconds_between_ticks, clamp(seconds_active / 30, 1, 6)) + if(QDELETED(src)) + return + on_tick(seconds_between_ticks) + switch(intensity) + if(5 to 10) + if(last_warning < 1) + to_chat(owner, span_warning(early_warning)) + last_warning = 1 + if(40 to 50) + if(last_warning < 2) + to_chat(owner, span_warning(mid_warning)) + last_warning = 2 + if(75 to 85) + if(last_warning < 3) + to_chat(owner, span_warning(late_warning)) + last_warning = 3 - // Counts down from 150 to [max_consciousness_mod] - if(max_consciousness_mod) - var/current_mod = LAZYACCESS(carbowner.max_consciousness_values, id) - if(current_mod > max_consciousness_mod) - owner.add_max_consciousness_value(id, max(max_consciousness_mod, current_mod - 15)) +/datum/status_effect/thermia/proc/update_intenisty_per_temp(seconds_between_ticks = 2, time_modifier = 1) + return - COOLDOWN_START(src, update_cd, 9 SECONDS) +/datum/status_effect/thermia/proc/on_tick(seconds_between_ticks) + return /datum/status_effect/thermia/hypo + id = "Hypothermia" + resist_trait = TRAIT_RESISTCOLD + early_warning = "You start to shiver." + mid_warning = "You can barely feel your fingers." + late_warning = "You feel like you're about to pass out from the cold!" + + VAR_PRIVATE/cold_stacks = 0 + VAR_PRIVATE/cold_threshold = 0 + + // Thresholds for when we should increase the intensity faster + VAR_PRIVATE/cold_threshold_low + VAR_PRIVATE/cold_threshold_medium + VAR_PRIVATE/cold_threshold_high + +/datum/status_effect/thermia/hypo/on_apply() + if(owner.body_temperature > owner.bodytemp_cold_damage_limit) + return FALSE + + cold_threshold = 10 + rand(0, 15) + + var/cold_diff = owner.bodytemp_cold_damage_limit - owner.standard_body_temperature + cold_threshold_low = owner.bodytemp_cold_damage_limit + cold_diff * 1.2 + cold_threshold_medium = owner.bodytemp_cold_damage_limit + cold_diff * 1.75 + cold_threshold_high = owner.bodytemp_cold_damage_limit + cold_diff * 2 + return ..() + +/datum/status_effect/thermia/hypo/update_intenisty_per_temp(seconds_between_ticks = 2, time_modifier = 1) + if(owner.body_temperature > owner.bodytemp_heat_damage_limit) + adjust_intensity( -3 * seconds_between_ticks) + else if(owner.body_temperature > owner.bodytemp_cold_damage_limit) + adjust_intensity( -1 * seconds_between_ticks) + else if(owner.has_reagent(/datum/reagent/medicine/cryoxadone, needs_metabolizing = TRUE)) + adjust_intensity( -1 * seconds_between_ticks, min = 25) -/datum/status_effect/thermia/hypo/check_remove() - return owner.body_temperature > owner.bodytemp_cold_damage_limit || HAS_TRAIT(owner, TRAIT_RESISTCOLD) + else if(owner.body_temperature > cold_threshold_low) + adjust_intensity(0.2 * time_modifier * seconds_between_ticks, max = 75) + else if(owner.body_temperature > cold_threshold_medium) + adjust_intensity(0.5 * time_modifier * seconds_between_ticks, max = 90) + else if(owner.body_temperature > cold_threshold_high) + adjust_intensity( 1 * time_modifier * seconds_between_ticks) + else + adjust_intensity(1.5 * time_modifier * seconds_between_ticks) -/datum/status_effect/thermia/hypo/one - consciousness_mod = 5 +/datum/status_effect/thermia/hypo/on_tick(seconds_between_ticks) + if(!ishuman(owner)) + return + + var/area_temp = owner.get_temperature(owner.loc?.return_air()) + if(area_temp < owner.bodytemp_cold_damage_limit) + cold_stacks += (0.5 * seconds_between_ticks) + else + cold_stacks = max(cold_stacks - (1 * seconds_between_ticks), 0) -/datum/status_effect/thermia/hypo/two - consciousness_mod = 10 + if(cold_stacks >= cold_threshold) + apply_frostbite() + cold_stacks = 0 + // threshold re-randomizes + cold_threshold = 10 + rand(0, 25) -/datum/status_effect/thermia/hypo/three - consciousness_mod = 20 - max_consciousness_mod = HARD_CRIT_THRESHOLD +/datum/status_effect/thermia/hypo/proc/apply_frostbite() + var/mob/living/carbon/human/human_owner = owner + var/list/pick_from = BODY_ZONES_LIMBS + while(length(pick_from)) + var/obj/item/bodypart/bodypart = human_owner.get_bodypart(pick_n_take(pick_from)) + if(isnull(bodypart)) + continue + if(locate(/datum/wound/flesh/frostbite) in bodypart.wounds) + continue + bodypart.force_wound_upwards(/datum/wound/flesh/frostbite, wound_source = "cold temperatures") + return /datum/status_effect/thermia/hyper + id = "Hyperthermia" + resist_trait = TRAIT_RESISTHEAT + early_warning = "You start to sweat profusely." + mid_warning = "You feel dizzy and disoriented." + late_warning = "You feel like you're about to pass out from the heat!" + + /// Tracks the number of burn stacks before we apply a burn wound + VAR_PRIVATE/burn_stacks = 0 + /// The threshold of stacks needed to be passed to apply a wound + VAR_PRIVATE/burn_threshold = 0 + /// Tracks number of wounds applied, so the threshold increases per wound + VAR_PRIVATE/wounds_applied = 0 + + // Thresholds for when we should increase the intensity faster + VAR_PRIVATE/heat_threshold_low + VAR_PRIVATE/heat_threshold_medium + VAR_PRIVATE/heat_threshold_high + +/datum/status_effect/thermia/hyper/on_apply() + if(owner.body_temperature < owner.bodytemp_heat_damage_limit) + return FALSE + + var/heat_diff = owner.bodytemp_heat_damage_limit - owner.standard_body_temperature + heat_threshold_low = owner.bodytemp_heat_damage_limit + heat_diff * 0.75 + heat_threshold_medium = owner.bodytemp_heat_damage_limit + heat_diff * 1.25 + heat_threshold_high = owner.bodytemp_heat_damage_limit + heat_diff * 2 + + burn_threshold = 10 + rand(0, 20) + return ..() + +/datum/status_effect/thermia/hyper/update_intenisty_per_temp(seconds_between_ticks = 2, time_modifier = 1) + + if(owner.body_temperature < owner.bodytemp_cold_damage_limit) + adjust_intensity( -3 * seconds_between_ticks) + else if(owner.body_temperature < owner.bodytemp_heat_damage_limit) + adjust_intensity( -1 * seconds_between_ticks) + else if(owner.has_reagent(/datum/reagent/medicine/pyroxadone, needs_metabolizing = TRUE)) + adjust_intensity( -1 * seconds_between_ticks, min = 25) + else if(owner.body_temperature < heat_threshold_low) + adjust_intensity(0.2 * time_modifier * seconds_between_ticks, max = 75) + else if(owner.body_temperature < heat_threshold_medium) + adjust_intensity(0.5 * time_modifier * seconds_between_ticks, max = 90) + else if(owner.body_temperature < heat_threshold_high) + adjust_intensity( 1 * time_modifier * seconds_between_ticks) + else + adjust_intensity( 1.5 * time_modifier * seconds_between_ticks) + +// if we're experiencing hyperthermia and we are in a hot area, we will start to get burn wounds! +/datum/status_effect/thermia/hyper/on_tick(seconds_between_ticks) + if(!ishuman(owner)) + return + + var/area_temp = owner.get_temperature(owner.loc?.return_air()) + if(area_temp > owner.bodytemp_heat_damage_limit) + burn_stacks += (0.5 * seconds_between_ticks) + else + burn_stacks = max(burn_stacks - (1 * seconds_between_ticks), 0) + + if(burn_stacks >= burn_threshold) + apply_burn() + wounds_applied += 1 + burn_stacks = 0 + // threshold re-randomizes and goes up, slightly + burn_threshold = 10 + rand(0, 20) + (wounds_applied * 2) -/datum/status_effect/thermia/hyper/check_remove() - return owner.body_temperature < owner.bodytemp_heat_damage_limit || HAS_TRAIT(owner, TRAIT_RESISTHEAT) +/datum/status_effect/thermia/hyper/proc/apply_burn() + var/mob/living/carbon/human/human_owner = owner + // Lets pick a random body part and check for an existing burn + var/obj/item/bodypart/bodypart = pick(human_owner.bodyparts) + var/datum/wound/existing_burn + for (var/datum/wound/iterated_wound as anything in bodypart.wounds) + var/datum/wound_pregen_data/pregen_data = iterated_wound.get_pregen_data() + if (pregen_data.wound_series in GLOB.wounding_types_to_series[WOUND_BURN]) + existing_burn = iterated_wound + break -/datum/status_effect/thermia/hyper/one - consciousness_mod = 5 + // If we have an existing burn try to upgrade it + var/severity = WOUND_SEVERITY_MODERATE + var/heat_damage = 2 * HEAT_DAMAGE * human_owner.physiology.heat_mod + if(human_owner.body_temperature > heat_threshold_medium * 8) + if(existing_burn?.severity < WOUND_SEVERITY_CRITICAL) + severity = WOUND_SEVERITY_CRITICAL + heat_damage *= 8 -/datum/status_effect/thermia/hyper/two - consciousness_mod = 10 + else if(human_owner.body_temperature > heat_threshold_medium * 2) + if(existing_burn?.severity < WOUND_SEVERITY_SEVERE) + severity = WOUND_SEVERITY_SEVERE + heat_damage *= 3 -/datum/status_effect/thermia/hyper/three - consciousness_mod = 20 - max_consciousness_mod = HARD_CRIT_THRESHOLD + human_owner.cause_wound_of_type_and_severity(WOUND_BURN, bodypart, severity, wound_source = "hot temperatures") + human_owner.apply_damage(HEAT_DAMAGE, BURN, bodypart, wound_bonus = CANT_WOUND) diff --git a/code/datums/status_effects/drug_effects.dm b/code/datums/status_effects/drug_effects.dm index efba760833ba..fa2376714c93 100644 --- a/code/datums/status_effects/drug_effects.dm +++ b/code/datums/status_effects/drug_effects.dm @@ -38,6 +38,8 @@ /datum/status_effect/high_blood_pressure/proc/on_healthscan(datum/source, list/render_list, advanced, mob/user, mode, tochat) SIGNAL_HANDLER + if(owner.has_status_effect(/datum/status_effect/low_blood_pressure)) + return render_list += "Hypertension detected.
" /datum/status_effect/seizure diff --git a/code/datums/wounds/burns.dm b/code/datums/wounds/burns.dm index e57dab593e22..50d80f76b541 100644 --- a/code/datums/wounds/burns.dm +++ b/code/datums/wounds/burns.dm @@ -4,91 +4,82 @@ */ // TODO: well, a lot really, but specifically I want to add potential fusing of clothing/equipment on the affected area, and limb infections, though those may go in body part code -/datum/wound/burn - name = "Burn Wound" - undiagnosed_name = "Burns" - a_or_from = "from" - sound_effect = 'sound/effects/wounds/sizzle1.ogg' - -/datum/wound/burn/wound_injury(datum/wound/old_wound, attack_direction) - if(!old_wound && limb.current_gauze && (wound_flags & ACCEPTS_GAUZE)) - qdel(limb.remove_gauze()) - // oops your existing gauze got burned, need a new one now - var/obj/effect/decal/cleanable/ash/ash = new(limb.drop_location()) - ash.desc += " It looks like it used to be some kind of bandage." - - return ..() - -/datum/wound/burn/flesh - name = "Burn (Flesh) Wound" - a_or_from = "from" - processes = TRUE +/datum/wound/flesh + name = "Flesh Wound" default_scar_file = FLESH_SCAR_FILE - + processes = TRUE treatable_by = list(/obj/item/stack/medical/ointment, /obj/item/stack/medical/mesh) // sterilizer and alcohol will require reagent treatments, coming soon + /// Shorthand for the name of the wound for the analyzer + var/scanner_name = "" // Flesh damage vars /// How much damage to our flesh we currently have. Once both this and infestation reach 0, the wound is considered healed var/flesh_damage = 5 /// Our current counter for how much flesh regeneration we have stacked from regenerative mesh/synthflesh/whatever, decrements each tick and lowers flesh_damage - var/flesh_healing = 0 + VAR_FINAL/flesh_healing = 0 // Infestation vars (only for severe and critical) /// How quickly infection breeds on this burn if we don't have disinfectant var/infestation_rate = 0 /// Our current level of infection - var/infestation = 0 + VAR_FINAL/infestation = 0 /// Our current level of sanitization/anti-infection, from disinfectants/alcohol/UV lights. While positive, totally pauses and slowly reverses infestation effects each tick - var/sanitization = 0 + VAR_FINAL/sanitization = 0 /// Once we reach infestation beyond WOUND_INFESTATION_SEPSIS, we get this many warnings before the limb is completely paralyzed (you'd have to ignore a really bad burn for a really long time for this to happen) - var/strikes_to_lose_limb = 3 - -/datum/wound/burn/flesh/handle_process(seconds_per_tick, times_fired) + VAR_PRIVATE/strikes_to_lose_limb = 3 - if (!victim || HAS_TRAIT(victim, TRAIT_STASIS)) +/datum/wound/flesh/handle_process(seconds_per_tick, times_fired) + if(isnull(victim) || HAS_TRAIT(victim, TRAIT_STASIS) || QDELETED(src)) return - . = ..() if(strikes_to_lose_limb <= 0) // we've already hit sepsis, nothing more to do victim.adjustToxLoss(0.25 * seconds_per_tick) if(SPT_PROB(0.5, seconds_per_tick)) - victim.visible_message(span_danger("The infection on the remnants of [victim]'s [limb.plaintext_zone] shift and bubble nauseatingly!"), span_warning("You can feel the infection on the remnants of your [limb.plaintext_zone] coursing through your veins!"), vision_distance = COMBAT_MESSAGE_RANGE) + victim.visible_message( + span_danger("The infection on the remnants of [victim]'s [limb.plaintext_zone] shift and bubble nauseatingly!"), + span_warning("You can feel the infection on the remnants of your [limb.plaintext_zone] coursing through your veins!"), + vision_distance = COMBAT_MESSAGE_RANGE, + visible_message_flags = ALWAYS_SHOW_SELF_MESSAGE, + ) return for(var/datum/reagent/reagent as anything in victim.reagents.reagent_list) if(reagent.chemical_flags & REAGENT_AFFECTS_WOUNDS) reagent.on_burn_wound_processing(src) - if(HAS_TRAIT(victim, TRAIT_VIRUS_RESISTANCE)) - sanitization += 0.9 - if(limb.current_gauze) limb.seep_gauze(WOUND_BURN_SANITIZATION_RATE * seconds_per_tick) - if(flesh_healing > 0) // good bandages multiply the length of flesh healing + handle_healing(seconds_per_tick) + if(QDELETED(src)) + return + + handle_sanitization(seconds_per_tick) + handle_infection(seconds_per_tick) + +/datum/wound/flesh/proc/handle_healing(seconds_per_tick, damage_decay_mod = 0.5, heal_decay_mod = 0.5) + // good bandages multiply the length of flesh healing + if(flesh_healing > 0) var/bandage_factor = limb.current_gauze?.burn_cleanliness_bonus || 1 - flesh_damage = max(flesh_damage - (0.5 * seconds_per_tick), 0) - flesh_healing = max(flesh_healing - (0.5 * bandage_factor * seconds_per_tick), 0) // good bandages multiply the length of flesh healing + flesh_damage = max(flesh_damage - (damage_decay_mod * seconds_per_tick), 0) + flesh_healing = max(flesh_healing - (heal_decay_mod * bandage_factor * seconds_per_tick), 0) // good bandages multiply the length of flesh healing // if we have little/no infection, the limb doesn't have much burn damage, and our nutrition is good, heal some flesh if(infestation <= WOUND_INFECTION_MODERATE && (limb.burn_dam < 5) && (victim.nutrition >= NUTRITION_LEVEL_FED)) flesh_healing += 0.2 - // here's the check to see if we're cleared up - if((flesh_damage <= 0) && (infestation <= WOUND_INFECTION_MODERATE)) - to_chat(victim, span_green("The burns on your [limb.plaintext_zone] have cleared up!")) - qdel(src) - return +/datum/wound/flesh/proc/handle_sanitization(seconds_per_tick, infestation_decay_mod = 1, sanitization_decay_mod = 1) + if(HAS_TRAIT(victim, TRAIT_VIRUS_RESISTANCE)) + sanitization += 0.9 - // sanitization is checked after the clearing check but before the actual ill-effects, because we freeze the effects of infection while we have sanitization if(sanitization > 0) var/bandage_factor = limb.current_gauze?.burn_cleanliness_bonus || 1 infestation = max(infestation - (WOUND_BURN_SANITIZATION_RATE * seconds_per_tick), 0) sanitization = max(sanitization - (WOUND_BURN_SANITIZATION_RATE * bandage_factor * seconds_per_tick), 0) - return +/datum/wound/flesh/proc/handle_infection(seconds_per_tick) infestation += infestation_rate * seconds_per_tick switch(infestation) if(0 to WOUND_INFECTION_MODERATE) @@ -142,14 +133,14 @@ threshold_penalty = 120 // piss easy to destroy set_disabling(TRUE) -/datum/wound/burn/flesh/set_disabling(new_value) +/datum/wound/flesh/set_disabling(new_value) . = ..() if(new_value && strikes_to_lose_limb <= 0) treat_text_short = "Amputate or augment limb immediately, or place the patient into cryogenics." else treat_text_short = initial(treat_text_short) -/datum/wound/burn/flesh/get_wound_description(mob/user) +/datum/wound/flesh/get_wound_description(mob/user) if(strikes_to_lose_limb <= 0) return span_deadsay("[victim.p_Their()] [limb.plaintext_zone] has locked up completely and is non-functional.") @@ -182,9 +173,9 @@ return "[condition.Join()]" -/datum/wound/burn/flesh/severity_text(simple = FALSE) +/datum/wound/flesh/severity_text() . = ..() - . += " Burn / " + . += " [scanner_name || name] / " switch(infestation) if(-INFINITY to WOUND_INFECTION_MODERATE) . += "No" @@ -198,7 +189,7 @@ . += "Total" . += " Infection" -/datum/wound/burn/flesh/get_scanner_description(mob/user) +/datum/wound/flesh/get_scanner_description(mob/user) if(strikes_to_lose_limb <= 0) // Unclear if it can go below 0, best to not take the chance var/oopsie = "Type: [name]
Severity: [severity_text()]" oopsie += "
Infection Level: [span_deadsay("The body part has suffered complete sepsis and must be removed. Amputate or augment limb immediately, or place the patient in a cryotube.")]
" @@ -230,15 +221,16 @@ new burn common procs */ -/datum/wound/burn/flesh/proc/can_be_ointmented_or_meshed() - if(infestation > 0 || sanitization < infestation) +/// Checks if the wound is in a state that ointment or flesh will help +/datum/wound/flesh/proc/can_be_ointmented_or_meshed() + if(infestation > 0 && sanitization < infestation) return TRUE - if(flesh_damage > 0 || flesh_healing <= flesh_damage) + if(flesh_damage > 0 && flesh_healing <= flesh_damage) return TRUE return FALSE /// Paramedic UV penlights -/datum/wound/burn/flesh/proc/uv(obj/item/flashlight/pen/paramedic/I, mob/user) +/datum/wound/flesh/proc/uv(obj/item/flashlight/pen/paramedic/I, mob/user) if(!COOLDOWN_FINISHED(I, uv_cooldown)) to_chat(user, span_notice("[I] is still recharging!")) return TRUE @@ -246,33 +238,67 @@ to_chat(user, span_notice("There's no infection to treat on [victim]'s [limb.plaintext_zone]!")) return TRUE - user.visible_message(span_notice("[user] flashes the burns on [victim]'s [limb] with [I]."), span_notice("You flash the burns on [user == victim ? "your" : "[victim]'s"] [limb.plaintext_zone] with [I]."), vision_distance=COMBAT_MESSAGE_RANGE) + user.visible_message( + span_notice("[user] flashes the burns on [victim]'s [limb] with [I]."), + span_notice("You flash the burns on [user == victim ? "your" : "[victim]'s"] [limb.plaintext_zone] with [I]."), + vision_distance = COMBAT_MESSAGE_RANGE, + visible_message_flags = ALWAYS_SHOW_SELF_MESSAGE, + ) sanitization += I.uv_power COOLDOWN_START(I, uv_cooldown, I.uv_cooldown_length) return TRUE -/datum/wound/burn/flesh/treat(obj/item/I, mob/user) +/datum/wound/flesh/treat(obj/item/I, mob/user) if(istype(I, /obj/item/flashlight/pen/paramedic)) return uv(I, user) // people complained about burns not healing on stasis beds, so in addition to checking if it's cured, they also get the special ability to very slowly heal on stasis beds if they have the healing effects stored -/datum/wound/burn/flesh/on_stasis(seconds_per_tick, times_fired) +/datum/wound/flesh/on_stasis(seconds_per_tick, times_fired) . = ..() if(strikes_to_lose_limb <= 0) // we've already hit sepsis, nothing more to do if(SPT_PROB(0.5, seconds_per_tick)) - victim.visible_message(span_danger("The infection on the remnants of [victim]'s [limb.plaintext_zone] shift and bubble nauseatingly!"), span_warning("You can feel the infection on the remnants of your [limb.plaintext_zone] coursing through your veins!"), vision_distance = COMBAT_MESSAGE_RANGE) + victim.visible_message( + span_danger("The infection on the remnants of [victim]'s [limb.plaintext_zone] shift and bubble nauseatingly!"), + span_warning("You can feel the infection on the remnants of your [limb.plaintext_zone] coursing through your veins!"), + vision_distance = COMBAT_MESSAGE_RANGE, + visible_message_flags = ALWAYS_SHOW_SELF_MESSAGE, + ) return - if(flesh_healing > 0) - flesh_damage = max(flesh_damage - (0.1 * seconds_per_tick), 0) + + handle_healing(seconds_per_tick, damage_decay_mod = 0.1, heal_decay_mod = 0) + handle_sanitization(seconds_per_tick, infestation_decay_mod = 0.1, sanitization_decay_mod = 0) + +/datum/wound/flesh/on_synthflesh(reac_volume) + flesh_healing += reac_volume * 0.5 // 20u patch will heal 10 flesh standard + +/datum/wound/flesh/burn + name = "Burn (Flesh) Wound" + a_or_from = "from" + undiagnosed_name = "Burns" + sound_effect = 'sound/effects/wounds/sizzle1.ogg' + scanner_name = "Burn" + +/datum/wound/flesh/burn/handle_healing(seconds_per_tick, damage_decay_mod = 0.5, heal_decay_mod = 0.5) + . = ..() + // here's the check to see if we're cleared up + if((flesh_damage <= 0) && (infestation <= WOUND_INFECTION_MODERATE)) + to_chat(victim, span_green("The burns on your [limb.plaintext_zone] have cleared up!")) + qdel(src) + +/datum/wound/flesh/burn/on_stasis(seconds_per_tick, times_fired) + . = ..() if((flesh_damage <= 0) && (infestation <= 1)) to_chat(victim, span_green("The burns on your [limb.plaintext_zone] have cleared up!")) qdel(src) - return - if(sanitization > 0) - infestation = max(infestation - (0.1 * WOUND_BURN_SANITIZATION_RATE * seconds_per_tick), 0) -/datum/wound/burn/flesh/on_synthflesh(reac_volume) - flesh_healing += reac_volume * 0.5 // 20u patch will heal 10 flesh standard +/datum/wound/flesh/burn/wound_injury(datum/wound/old_wound, attack_direction) + if(!old_wound && limb.current_gauze && (wound_flags & ACCEPTS_GAUZE)) + qdel(limb.remove_gauze()) + // oops your existing gauze got burned, need a new one now + var/obj/effect/decal/cleanable/ash/ash = new(limb.drop_location()) + ash.desc += " It looks like it used to be some kind of bandage." + + return ..() /datum/wound_pregen_data/flesh_burn abstract = TRUE @@ -282,11 +308,11 @@ wound_series = WOUND_SERIES_FLESH_BURN_BASIC -/datum/wound/burn/flesh/get_limb_examine_description() +/datum/wound/flesh/burn/get_limb_examine_description() return span_warning("The flesh on this limb appears badly cooked.") // we don't even care about first degree burns, straight to second -/datum/wound/burn/flesh/moderate +/datum/wound/flesh/burn/moderate name = "Second Degree Burns" desc = "Patient is suffering considerable burns with mild skin penetration, weakening limb integrity and increased burning sensations." treat_text = "Apply topical ointment or regenerative mesh to the wound." @@ -307,11 +333,11 @@ /datum/wound_pregen_data/flesh_burn/second_degree abstract = FALSE - wound_path_to_generate = /datum/wound/burn/flesh/moderate + wound_path_to_generate = /datum/wound/flesh/burn/moderate threshold_minimum = 40 -/datum/wound/burn/flesh/severe +/datum/wound/flesh/burn/severe name = "Third Degree Burns" desc = "Patient is suffering extreme burns with full skin penetration, creating serious risk of infection and greatly reduced limb integrity." treat_text = "Swiftly apply healing aids such as Synthflesh or regenerative mesh to the wound. \ @@ -337,11 +363,11 @@ /datum/wound_pregen_data/flesh_burn/third_degree abstract = FALSE - wound_path_to_generate = /datum/wound/burn/flesh/severe + wound_path_to_generate = /datum/wound/flesh/burn/severe threshold_minimum = 80 -/datum/wound/burn/flesh/critical +/datum/wound/flesh/burn/critical name = "Catastrophic Burns" desc = "Patient is suffering near complete loss of tissue and significantly charred muscle and bone, creating life-threatening risk of infection and negligible limb integrity." treat_text = "Immediately apply healing aids such as Synthflesh or regenerative mesh to the wound. \ @@ -368,12 +394,12 @@ /datum/wound_pregen_data/flesh_burn/fourth_degree abstract = FALSE - wound_path_to_generate = /datum/wound/burn/flesh/critical + wound_path_to_generate = /datum/wound/flesh/burn/critical threshold_minimum = 140 ///special severe wound caused by sparring interference or other god related punishments. -/datum/wound/burn/flesh/severe/brand +/datum/wound/flesh/burn/severe/brand name = "Holy Brand" desc = "Patient is suffering extreme burns from a strange brand marking, creating serious risk of infection and greatly reduced limb integrity." examine_desc = "appears to have holy symbols painfully branded into their flesh, leaving severe burns." @@ -385,20 +411,110 @@ abstract = FALSE can_be_randomly_generated = FALSE - wound_path_to_generate = /datum/wound/burn/flesh/severe/brand + wound_path_to_generate = /datum/wound/flesh/burn/severe/brand /// special severe wound caused by the cursed slot machine. -/datum/wound/burn/flesh/severe/cursed_brand +/datum/wound/flesh/burn/severe/cursed_brand name = "Ancient Brand" desc = "Patient is suffering extreme burns with oddly ornate brand markings, creating serious risk of infection and greatly reduced limb integrity." examine_desc = "appears to have ornate symbols painfully branded into their flesh, leaving severe burns" occur_text = "chars rapidly into a pattern that can only be described as an agglomeration of several financial symbols, burned into the flesh" -/datum/wound/burn/flesh/severe/cursed_brand/get_limb_examine_description() +/datum/wound/flesh/burn/severe/cursed_brand/get_limb_examine_description() return span_warning("The flesh on this limb has several ornate symbols burned into it, with pitting throughout.") /datum/wound_pregen_data/flesh_burn/third_degree/cursed_brand abstract = FALSE can_be_randomly_generated = FALSE - wound_path_to_generate = /datum/wound/burn/flesh/severe/cursed_brand + wound_path_to_generate = /datum/wound/flesh/burn/severe/cursed_brand + +/datum/wound/flesh/frostbite + name = "Frostbite" + a_or_from = "from" + undiagnosed_name = "Frostbite" + desc = "Patient is suffering from frostbite, with skin and tissue freezing and dying, creating a risk of infection and reduced limb integrity." + treat_text = "Regenerative mesh and ointment will not directly help, but will sanitize the wound. \ + Keep the wound wrapped in clean gauze and monitor for worsening condition while rewarming the limb." + treat_text_short = "Bandage and monitor for worsening condition while rewarming the limb. \ + Regenerative mesh will not directly help, but will sanitize the wound." + examine_desc = "is red and swollen" + occur_text = "" + severity = WOUND_SEVERITY_MODERATE + damage_multiplier_penalty = 1.1 + interaction_efficiency_penalty = 0.9 + threshold_penalty = 25 + infestation_rate = 0.05 + flesh_damage = 10 + treatable_by = list(/obj/item/flashlight/pen/paramedic, /obj/item/stack/medical/ointment, /obj/item/stack/medical/mesh) + + simple_desc = "Patient's skin is frozen and dying, with a risk of infection and reduced limb integrity." + simple_treat_text = "Bandage and monitor for worsening condition while rewarming the limb. Regenerative mesh will not directly help, but will sanitize the wound." + homemade_treat_text = "Healthy tea will help with recovery. Salt, or preferably a salt-water mixture, will sanitize the wound, but the former especially will cause skin irritation and dehydration, speeding up infection. Space Cleaner can be used as disinfectant in a pinch." + +/datum/wound/flesh/frostbite/second_wind() + return + +/datum/wound/flesh/frostbite/apply_wound(obj/item/bodypart/L, silent, datum/wound/old_wound, smited, attack_direction, wound_source, replacing, injury_roll) + . = ..() + if(!silent) + to_chat(victim, span_warning("Your [L.plaintext_zone] turns red and feels extremely cold to the touch!")) + +/datum/wound/flesh/frostbite/proc/upgrade_severity(new_severity) + var/old_severity = severity + severity = max(new_severity, severity) + if(severity == old_severity) + return + switch(severity) + if(WOUND_SEVERITY_SEVERE) + infestation_rate = 0.075 + damage_multiplier_penalty = 1.2 + interaction_efficiency_penalty = 0.6 + threshold_penalty = 50 + examine_desc = "is turning white" + if(WOUND_SEVERITY_CRITICAL) + infestation_rate = 0.1 + damage_multiplier_penalty = 1.25 + interaction_efficiency_penalty = 0.3 + threshold_penalty = 75 + examine_desc = "is turning black" + +/datum/wound/flesh/frostbite/handle_healing(seconds_per_tick, damage_decay_mod = 0.5, heal_decay_mod = 0.5) + if(victim.get_bodypart_pain(limb.body_zone) < 10 * severity && SPT_PROB(6, seconds_per_tick)) + victim.cause_pain(limb.body_zone, 5 * severity, BURN) + + var/skin_temp = victim.get_skin_temperature() + if(skin_temp > victim.bodytemp_cold_damage_limit) + flesh_damage = max(flesh_damage - (damage_decay_mod * seconds_per_tick), 0) + if(flesh_damage <= 0) + to_chat(victim, span_green("The skin on your [limb.plaintext_zone] regains color and feeling!")) + qdel(src) + return + + var/cold_diff = victim.bodytemp_cold_damage_limit - victim.standard_body_temperature + var/cold_threshold_medium = victim.bodytemp_cold_damage_limit + cold_diff * 1.25 + + // flesh damage goes UP if we're cold (scaling based on coldness) + flesh_damage += (0.2 * damage_decay_mod * seconds_per_tick) * (cold_threshold_medium / skin_temp) + if(flesh_damage > 30) + if(severity < WOUND_SEVERITY_CRITICAL) + upgrade_severity(WOUND_SEVERITY_CRITICAL) + victim.sharp_pain(limb.body_zone, 20, BURN, 10 SECONDS) + to_chat(victim, span_boldwarning("Your [limb.plaintext_zone] goes completely numb, turning black!")) + else if(flesh_damage > 20) + if(severity < WOUND_SEVERITY_SEVERE) + upgrade_severity(WOUND_SEVERITY_SEVERE) + victim.sharp_pain(limb.body_zone, 12, BURN, 10 SECONDS) + to_chat(victim, span_boldwarning("Your [limb.plaintext_zone] starts to tingle and harden, turning a pale while!")) + +/datum/wound/flesh/frostbite/can_be_ointmented_or_meshed() + if(infestation > 0 && sanitization < infestation) + return TRUE + return FALSE + +/datum/wound_pregen_data/frostbite + abstract = FALSE + can_be_randomly_generated = FALSE + required_limb_biostate = BIO_FLESH + required_wounding_types = list(WOUND_BURN) + wound_path_to_generate = /datum/wound/flesh/frostbite diff --git a/code/datums/wounds/internal_bleeding.dm b/code/datums/wounds/internal_bleeding.dm index 7aec8889cd32..ea8bec76c78e 100644 --- a/code/datums/wounds/internal_bleeding.dm +++ b/code/datums/wounds/internal_bleeding.dm @@ -9,9 +9,9 @@ /datum/wound/bleed_internal name = "Internal Bleeding" desc = "The patient is bleeding internally, causing severe pain and difficulty breathing." - treat_text = "May heal over time through rest, but can be repaired surgically. \ - Blood clotting medication may slow the bleeding, but only very rare and advanced medication can repair it on its own." - treat_text_short = "Rest or repair surgically." + treat_text = "May heal over time through rest or use of blood clotting medication, but can be repaired surgically. \ + If left untreated, it may worsen and require surgery." + treat_text_short = "Supply blood clotting medication, repair surgically, or wait and rest." examine_desc = "" scar_keyword = "" severity = WOUND_SEVERITY_TRIVIAL @@ -38,6 +38,13 @@ /datum/wound/bleed_internal/get_self_check_description(mob/user) return span_warning("It feels tense to the touch.") // same as rib fracture! +/datum/wound/bleed_internal/severity_text() + // "minor" = trivial but won't heal passively + if(severity == WOUND_SEVERITY_TRIVIAL && highest_severity != WOUND_SEVERITY_TRIVIAL) + return "Minor" + // "trivial" = trivial, and it'll heal passively + return ..() + /datum/wound/bleed_internal/proc/upgrade_severity(new_severity) if(new_severity == severity) return @@ -46,7 +53,7 @@ severity = new_severity bleed_amount = initial(bleed_amount) - if(severity == WOUND_SEVERITY_TRIVIAL) + if(severity == WOUND_SEVERITY_TRIVIAL && highest_severity == WOUND_SEVERITY_TRIVIAL) treat_text_short = initial(treat_text_short) treat_text = initial(treat_text) else @@ -60,7 +67,13 @@ var/severity_mod = (severity + 1) victim.bleed(bleed_amount * severity_mod * seconds_per_tick, drip = FALSE) if(severity == WOUND_SEVERITY_TRIVIAL) - heal_percent(0.01 * seconds_per_tick) + if(highest_severity == WOUND_SEVERITY_TRIVIAL) + var/percent = 0.01 + if(victim.body_position == LYING_DOWN) + percent *= 2 + if(HAS_TRAIT(victim, TRAIT_KNOCKEDOUT)) + percent *= 2 + heal_percent(percent * seconds_per_tick) if(QDELETED(src)) return diff --git a/code/datums/wounds/pierce.dm b/code/datums/wounds/pierce.dm index fd3ce3d4e32a..2fd93d75e0b9 100644 --- a/code/datums/wounds/pierce.dm +++ b/code/datums/wounds/pierce.dm @@ -89,7 +89,6 @@ if (!victim || HAS_TRAIT(victim, TRAIT_STASIS)) return - set_blood_flow(min(blood_flow, WOUND_SLASH_MAX_BLOODFLOW)) if(limb.can_bleed()) if(!HAS_TRAIT(victim, TRAIT_RESISTCOLD) && victim.get_skin_temperature() < victim.bodytemp_cold_damage_limit) @@ -105,12 +104,14 @@ return if(limb.current_gauze) - var/amt_blocking = limb.current_gauze.absorption_rate * seconds_per_tick - limb.seep_gauze(amt_blocking) - adjust_blood_flow(-1 * amt_blocking * gauzed_clot_rate) + var/gauze_power = limb.current_gauze.absorption_rate + limb.seep_gauze(gauze_power * seconds_per_tick) + adjust_blood_flow(-gauze_power * gauzed_clot_rate * seconds_per_tick) /datum/wound/pierce/bleed/adjust_blood_flow(adjust_by, minimum) . = ..() + if(blood_flow > WOUND_MAX_BLOODFLOW) + blood_flow = WOUND_MAX_BLOODFLOW if(blood_flow <= 0 && !QDELETED(src)) to_chat(victim, span_green("The holes on your [limb.plaintext_zone] have [!limb.can_bleed() ? "healed up" : "stopped bleeding"]!")) qdel(src) diff --git a/code/datums/wounds/slash.dm b/code/datums/wounds/slash.dm index cfe2c05a1f33..959a4c4d89fa 100644 --- a/code/datums/wounds/slash.dm +++ b/code/datums/wounds/slash.dm @@ -43,9 +43,6 @@ /// Once the blood flow drops below minimum_flow, we demote it to this type of wound. If there's none, we're all better var/demotes_to - /// The maximum flow we've had so far - var/highest_flow - /// A bad system I'm using to track the worst scar we earned (since we can demote, we want the biggest our wound has been, not what it was when it was cured (probably moderate)) var/datum/scar/highest_scar @@ -140,29 +137,23 @@ return BLOOD_FLOW_INCREASING /datum/wound/slash/flesh/handle_process(seconds_per_tick, times_fired) - if (!victim || HAS_TRAIT(victim, TRAIT_STASIS)) return // in case the victim has the NOBLOOD trait, the wound will simply not clot on it's own if(limb.can_bleed()) - set_blood_flow(min(blood_flow, WOUND_SLASH_MAX_BLOODFLOW)) + if(clot_rate > 0) + adjust_blood_flow(-clot_rate * seconds_per_tick) + if(QDELETED(src)) + return if(HAS_TRAIT(victim, TRAIT_BLOODY_MESS)) adjust_blood_flow(0.25) // old heparin used to just add +2 bleed stacks per tick, this adds 0.5 bleed flow to all open cuts which is probably even stronger as long as you can cut them first - //gauze always reduces blood flow, even for non bleeders if(limb.current_gauze) - if(clot_rate > 0) - adjust_blood_flow(-clot_rate * seconds_per_tick) - limb.seep_gauze(limb.current_gauze.absorption_rate * seconds_per_tick) - adjust_blood_flow(-limb.current_gauze.absorption_rate * seconds_per_tick) - //otherwise, only clot if it's a bleeder - else if(limb.can_bleed()) - adjust_blood_flow(-clot_rate * seconds_per_tick) - - if(blood_flow > highest_flow) - highest_flow = blood_flow + var/gauze_power = limb.current_gauze.absorption_rate + limb.seep_gauze(gauze_power * seconds_per_tick) + adjust_blood_flow(-gauze_power * seconds_per_tick) /* BEWARE, THE BELOW NONSENSE IS MADNESS. bones.dm looks more like what I have in mind and is sufficiently clean, don't pay attention to this messiness */ @@ -197,8 +188,7 @@ /// if a felinid is licking this cut to reduce bleeding /datum/wound/slash/flesh/proc/lick_wounds(mob/living/carbon/human/user) // transmission is one way patient -> felinid since google said cat saliva is antiseptic or whatever, and also because felinids are already risking getting beaten for this even without people suspecting they're spreading a deathvirus - for(var/i in victim.diseases) - var/datum/disease/iter_disease = i + for(var/datum/disease/iter_disease as anything in victim.diseases) if(iter_disease.spread_flags & (DISEASE_SPREAD_SPECIAL | DISEASE_SPREAD_NON_CONTAGIOUS)) continue user.ForceContractDisease(iter_disease) @@ -210,7 +200,6 @@ user.visible_message(span_notice("[user] licks the wounds on [victim]'s [limb.plaintext_zone]."), span_notice("You lick some of the wounds on [victim]'s [limb.plaintext_zone]"), ignored_mobs=victim) to_chat(victim, span_green("[user] licks the wounds on your [limb.plaintext_zone]!")) - var/mob/victim_stored = victim adjust_blood_flow(-0.5) @@ -221,6 +210,8 @@ /datum/wound/slash/flesh/adjust_blood_flow(adjust_by, minimum) . = ..() + if(blood_flow > WOUND_MAX_BLOODFLOW) + blood_flow = WOUND_MAX_BLOODFLOW if(blood_flow < minimum_flow && !QDELETED(src)) if(demotes_to) replace_wound(new demotes_to) diff --git a/code/game/machinery/dna_infuser/organ_sets/carp_organs.dm b/code/game/machinery/dna_infuser/organ_sets/carp_organs.dm index afbb8404060f..608b12241a3d 100644 --- a/code/game/machinery/dna_infuser/organ_sets/carp_organs.dm +++ b/code/game/machinery/dna_infuser/organ_sets/carp_organs.dm @@ -25,11 +25,18 @@ greyscale_config = /datum/greyscale_config/mutant_organ greyscale_colors = CARP_COLORS + low_pressure_threshold = 0 + high_pressure_threshold = ONE_ATMOSPHERE * 1.2 + /obj/item/organ/internal/lungs/carp/Initialize(mapload) . = ..() AddElement(/datum/element/noticable_organ, "neck has odd gills.", BODY_ZONE_HEAD) AddElement(/datum/element/organ_set_bonus, /datum/status_effect/organ_set_bonus/carp) - ADD_TRAIT(src, TRAIT_SPACEBREATHING, REF(src)) + +/obj/item/organ/internal/lungs/carp/pre_breath_gas_handling(mob/living/carbon/human/breather, datum/gas_mixture/breath) + // 0 mol breaths function as "successful breaths" and thus heal us + if(breath.total_moles() <= 0) + heal_oxyloss_on_breath(breather, breath) ///occasionally sheds carp teeth, stronger melee (bite) attacks, but you can't cover your mouth anymore. /obj/item/organ/internal/tongue/carp diff --git a/code/game/machinery/recycler.dm b/code/game/machinery/recycler.dm index 03c5c1d3966a..6be0435d4725 100644 --- a/code/game/machinery/recycler.dm +++ b/code/game/machinery/recycler.dm @@ -240,7 +240,7 @@ // Instantly lie down, also go unconscious from the pain, before you die. if(!issilicon(L)) L.Unconscious(10 SECONDS) - L.apply_damage(crush_damage) + L.apply_damage(crush_damage, BRUTE, spread_damage = TRUE) /obj/machinery/recycler/on_deconstruction() safety_mode = TRUE diff --git a/code/game/objects/effects/decals/cleanable/humans.dm b/code/game/objects/effects/decals/cleanable/humans.dm index 1375c8400e60..41ee6ab916d1 100644 --- a/code/game/objects/effects/decals/cleanable/humans.dm +++ b/code/game/objects/effects/decals/cleanable/humans.dm @@ -470,6 +470,10 @@ var/splatter_strength = 3 /// Insurance so that we don't keep moving once we hit a stoppoint var/hit_endpoint = FALSE + /// How fast the splatter moves + var/splatter_speed = 0.1 SECONDS + /// Tracks what direction we're flying + var/flight_dir = NONE /obj/effect/decal/cleanable/blood/hitsplatter/Initialize(mapload, splatter_strength) . = ..() @@ -480,14 +484,13 @@ /obj/effect/decal/cleanable/blood/hitsplatter/proc/expire() if(isturf(loc) && !skip) playsound(src, 'sound/effects/wounds/splatter.ogg', 60, TRUE, -1) - if(blood_dna_info) - loc.add_blood_DNA(blood_dna_info) + loc.add_blood_DNA(blood_dna_info) qdel(src) /// Set the splatter up to fly through the air until it rounds out of steam or hits something /obj/effect/decal/cleanable/blood/hitsplatter/proc/fly_towards(turf/target_turf, range) - var/delay = 2 - var/datum/move_loop/loop = SSmove_manager.move_towards(src, target_turf, delay, timeout = delay * range, priority = MOVEMENT_ABOVE_SPACE_PRIORITY, flags = MOVEMENT_LOOP_START_FAST) + flight_dir = get_dir(src, target_turf) + var/datum/move_loop/loop = SSmove_manager.move_towards(src, target_turf, splatter_speed, timeout = splatter_speed * range, priority = MOVEMENT_ABOVE_SPACE_PRIORITY, flags = MOVEMENT_LOOP_START_FAST) RegisterSignal(loop, COMSIG_MOVELOOP_PREPROCESS_CHECK, PROC_REF(pre_move)) RegisterSignal(loop, COMSIG_MOVELOOP_POSTPROCESS, PROC_REF(post_move)) RegisterSignal(loop, COMSIG_QDELETING, PROC_REF(loop_done)) @@ -498,6 +501,9 @@ /obj/effect/decal/cleanable/blood/hitsplatter/proc/post_move(datum/move_loop/source) SIGNAL_HANDLER + if(loc == prev_loc) + return + for(var/atom/movable/iter_atom in loc) if(hit_endpoint) return @@ -505,12 +511,21 @@ continue if(splatter_strength <= 0) break - iter_atom.add_blood_DNA(blood_dna_info) - splatter_strength-- - if(splatter_strength <= 0) // we used all the puff so we delete it. + splatter_strength-- + // we used all our blood so go away + if(splatter_strength <= 0) expire() + return + // make a trail + var/obj/effect/decal/cleanable/blood/fly_trail = new(loc) + fly_trail.dir = dir + if(ISDIAGONALDIR(flight_dir)) + fly_trail.transform = fly_trail.transform.Turn((flight_dir == NORTHEAST || flight_dir == SOUTHWEST) ? 135 : 45) + fly_trail.icon_state = pick("trails_1", "trails2") + fly_trail.adjust_bloodiness(fly_trail.bloodiness * -0.66) + fly_trail.add_blood_DNA(blood_dna_info) /obj/effect/decal/cleanable/blood/hitsplatter/proc/loop_done(datum/source) SIGNAL_HANDLER diff --git a/code/game/objects/items/bodybag.dm b/code/game/objects/items/bodybag.dm index e6cc241119a6..845e19ba707a 100644 --- a/code/game/objects/items/bodybag.dm +++ b/code/game/objects/items/bodybag.dm @@ -61,23 +61,51 @@ unfoldedbag_path = /obj/structure/closet/body_bag/bluespace w_class = WEIGHT_CLASS_SMALL item_flags = NO_MAT_REDEMPTION + /// Tracks the air from the bodybag + var/datum/gas_mixture/internal_air + +/obj/item/bodybag/bluespace/return_air() + return internal_air || ..() + +/obj/item/bodybag/bluespace/return_analyzable_air() + return internal_air || ..() + +/obj/item/bodybag/bluespace/assume_air(datum/gas_mixture/giver) + return internal_air ? internal_air.merge(giver) : ..() + +/obj/item/bodybag/bluespace/remove_air(amount) + return internal_air ? internal_air.remove(amount) : ..() /obj/item/bodybag/bluespace/examine(mob/user) . = ..() - if(contents.len) - var/s = contents.len == 1 ? "" : "s" - . += span_notice("You can make out the shape[s] of [contents.len] object[s] through the fabric.") + if(length(contents)) + . += span_notice("You can make out the shape of [length(contents)] object\s through the fabric.") + +/obj/item/bodybag/bluespace/deconstruct(disassembled) + for(var/atom/movable/inside in src) + inside.forceMove(get_turf(src)) + if(isliving(inside)) + to_chat(inside, span_notice("You suddenly feel the space around you torn apart! You're free!")) + return ..() /obj/item/bodybag/bluespace/Destroy() - for(var/atom/movable/A in contents) - A.forceMove(get_turf(src)) - if(isliving(A)) - to_chat(A, span_notice("You suddenly feel the space around you torn apart! You're free!")) + for(var/mob/living/leftover in src) + stack_trace("Bluespace Bodybag qdeleted before dumping mobs!") + leftover.forceMove(get_turf(src)) + QDEL_NULL(internal_air) return ..() /obj/item/bodybag/bluespace/deploy_bodybag(mob/user, atom/location) var/obj/structure/closet/body_bag/item_bag = new unfoldedbag_path(location) - for(var/atom/movable/inside in contents) + if(internal_air) + // get rid of any air it might have collected in init + if(item_bag.internal_air) + location.assume_air(item_bag.internal_air) + // replace it with our air + item_bag.internal_air = internal_air + internal_air = null + + for(var/atom/movable/inside in src) inside.forceMove(item_bag) if(isliving(inside)) to_chat(inside, span_notice("You suddenly feel air around you! You're free!")) diff --git a/code/game/objects/items/cosmetics.dm b/code/game/objects/items/cosmetics.dm index 0bc98cd39fdf..5f1dff21bcf6 100644 --- a/code/game/objects/items/cosmetics.dm +++ b/code/game/objects/items/cosmetics.dm @@ -187,9 +187,11 @@ icon_state = "razor" inhand_icon_state = "razor" obj_flags = CONDUCTS_ELECTRICITY - w_class = WEIGHT_CLASS_TINY + w_class = WEIGHT_CLASS_SMALL drop_sound = 'maplestation_modules/sound/items/pickup/metalweapon.ogg' pickup_sound = 'maplestation_modules/sound/items/pickup/surgery_metal.ogg' + /// sfx played on shave + var/shave_sound = 'sound/items/welder2.ogg' /obj/item/razor/suicide_act(mob/living/carbon/user) user.visible_message(span_suicide("[user] begins shaving [user.p_them()]self without the razor guard! It looks like [user.p_theyre()] trying to commit suicide!")) @@ -202,7 +204,7 @@ skinhead.set_facial_hairstyle("Shaved", update = TRUE) else skinhead.set_hairstyle("Skinhead", update = TRUE) - playsound(loc, 'sound/items/welder2.ogg', 20, TRUE) + playsound(loc, shave_sound, 33, TRUE) /obj/item/razor/attack(mob/target_mob, mob/living/user, params) if(!ishuman(target_mob)) diff --git a/code/game/objects/items/devices/flashlight.dm b/code/game/objects/items/devices/flashlight.dm index ad93cfc6a60f..4d3951821bb0 100644 --- a/code/game/objects/items/devices/flashlight.dm +++ b/code/game/objects/items/devices/flashlight.dm @@ -319,7 +319,7 @@ new /obj/effect/temp_visual/medical_holosign(target_turf, user) //produce a holographic glow COOLDOWN_START(src, holosign_cooldown, 10 SECONDS) -// see: [/datum/wound/burn/flesh/proc/uv()] +// see: [/datum/wound/flesh/proc/uv()] /obj/item/flashlight/pen/paramedic name = "paramedic penlight" desc = "A high-powered UV penlight intended to help stave off infection in the field on serious burned patients. Probably really bad to look into." diff --git a/code/game/objects/items/devices/scanners/health_analyzer.dm b/code/game/objects/items/devices/scanners/health_analyzer.dm index d209a647367a..3657b8397866 100644 --- a/code/game/objects/items/devices/scanners/health_analyzer.dm +++ b/code/game/objects/items/devices/scanners/health_analyzer.dm @@ -367,12 +367,16 @@ var/datum/blood_type/target_blood_type = target.get_blood_type() if(target_blood_type) var/bpm = target.get_bpm() + var/needs_heart = TRUE + if(ishuman(target)) + var/mob/living/carbon/human/humantarget = target + needs_heart = humantarget.needs_heart() - var/bpm_format = "[bpm] bpm" + var/bpm_format = "[needs_heart ? bpm : "n/a"] bpm" var/level_format = "[round_and_format_decimal(target.blood_volume, 0.1)] cl" // round to 0.1 but also print "100.0" and not "100" var/blood_type_format = "[target_blood_type.name]" - if(bpm < 60 || bpm > 100) + if(needs_heart && (bpm < 60 || bpm > 100)) bpm_format = span_alert(bpm_format) switch(target.blood_volume) diff --git a/code/game/objects/items/stacks/medical.dm b/code/game/objects/items/stacks/medical.dm index af4f194fec2b..29c8f0a9d2e2 100644 --- a/code/game/objects/items/stacks/medical.dm +++ b/code/game/objects/items/stacks/medical.dm @@ -35,16 +35,41 @@ var/sanitization /// How much we add to flesh_healing for burn wounds on application var/flesh_regeneration - /// Time it takes to assess injuries when looping healing - var/assessing_injury_delay = 1 SECONDS + /// Verb used when applying this object to someone + var/apply_verb = "treating" + /// Whether this item can be used on dead bodies + var/works_on_dead = FALSE + /// Optional flags to supply to can_inject + var/can_inject_flags = NONE /obj/item/stack/medical/interact_with_atom(atom/interacting_with, mob/living/user) if(!isliving(interacting_with)) return NONE - if(!begin_heal_loop(interacting_with, user)) + if(!begin_heal_loop(interacting_with, user, auto_change_zone = TRUE)) return NONE // [ITEM_INTERACT_BLOCKING] would be redundant as we are nobludgeon return ITEM_INTERACT_SUCCESS +/obj/item/stack/medical/interact_with_atom_secondary(atom/interacting_with, mob/living/user, list/modifiers) + if(!isliving(interacting_with)) + return NONE + if(!begin_heal_loop(interacting_with, user, auto_change_zone = FALSE)) + return NONE // see above + return ITEM_INTERACT_SUCCESS + +/obj/item/stack/medical/Initialize(mapload, new_amount, merge, list/mat_override, mat_amt) + . = ..() + register_item_context() + +/obj/item/stack/medical/add_item_context(obj/item/source, list/context, atom/target, mob/living/user) + if(!isliving(target)) + return NONE + if(iscarbon(target)) + context[SCREENTIP_CONTEXT_LMB] = "Auto Heal" + context[SCREENTIP_CONTEXT_RMB] = "Manual Heal" + else + context[SCREENTIP_CONTEXT_LMB] = "Heal" + return CONTEXTUAL_SCREENTIP_SET + /obj/item/stack/medical/apply_fantasy_bonuses(bonus) . = ..() if(heal_brute) @@ -68,125 +93,226 @@ /// Used to begin the recursive healing loop. /// Returns TRUE if we entered the loop, FALSE if we didn't -/obj/item/stack/medical/proc/begin_heal_loop(mob/living/patient, mob/user) +/obj/item/stack/medical/proc/begin_heal_loop(mob/living/patient, mob/living/user, auto_change_zone = TRUE) if(DOING_INTERACTION_WITH_TARGET(user, patient)) return FALSE - if(!can_heal(patient, user)) + var/heal_zone = check_zone(user.zone_selected) + if(!try_heal_checks(patient, user, heal_zone)) return FALSE - - INVOKE_ASYNC(src, PROC_REF(try_heal), patient, user) + if(heal_sound) + playsound(src, heal_sound, 33, FALSE) + SSblackbox.record_feedback("nested tally", "medical_item_used", 1, list("[auto_change_zone ? "auto" : "manual"]", "[type]")) + patient.balloon_alert(user, "[apply_verb] [parse_zone(heal_zone)]...") + INVOKE_ASYNC(src, PROC_REF(try_heal), patient, user, heal_zone, FALSE, iscarbon(patient) && auto_change_zone) // auto change is useless for non-carbons return TRUE -/// Checks if the passed patient can be healed by the passed user -/obj/item/stack/medical/proc/can_heal(mob/living/patient, mob/user) - return patient.try_inject(user, injection_flags = INJECT_TRY_SHOW_ERROR_MESSAGE) - -/// In which we print the message that we're starting to heal someone, then we try healing them. Does the do_after whether or not it can actually succeed on a targeted mob -/obj/item/stack/medical/proc/try_heal(mob/living/patient, mob/user, silent = FALSE) - if(!try_heal_checks(patient, user, heal_brute, heal_burn)) - return - if(heal_sound) - playsound(patient, heal_sound, 33, FALSE) +/** + * What actually handles printing the message that we're starting to heal someone, and trying to heal them + * + * This proc is recursively called until we run out of charges OR until the patient is fully healed + * OR until the target zone is fully healed (if auto_change_zone is FALSE) + * + * * patient - The mob we're trying to heal + * * user - The mob that's trying to heal the patient + * * healed_zone - The zone we're trying to heal on the patient + * Disregarded if auto_change_zone is TRUE + * * silent - If we should not print the message that we're starting to heal the patient + * Used so looping the proc doesn't spam messages + * * auto_change_zone - Handles the behavior when we finish healing a zone + * If auto_change_zone is set to TRUE, it picks the next most damaged zone to heal + * If auto_change_zone is set to FALSE, it'll give the user a chance to pick a new zone to heal + */ +/obj/item/stack/medical/proc/try_heal(mob/living/patient, mob/living/user, healed_zone, silent = FALSE, auto_change_zone = TRUE) if(patient == user) if(!silent) user.visible_message( span_notice("[user] starts to apply [src] on [user.p_them()]self..."), span_notice("You begin applying [src] on yourself..."), + visible_message_flags = ALWAYS_SHOW_SELF_MESSAGE, ) if(!do_after( user, - self_delay, + self_delay * (auto_change_zone ? 1 : 0.9), patient, - extra_checks = CALLBACK(src, PROC_REF(can_heal), patient, user), + extra_checks = CALLBACK(src, PROC_REF(can_heal), patient, user, healed_zone), )) return + if(!auto_change_zone) + healed_zone = check_zone(user.zone_selected) + if(!try_heal_checks(patient, user, healed_zone)) + return else if(other_delay) if(!silent) user.visible_message( span_notice("[user] starts to apply [src] on [patient]."), span_notice("You begin applying [src] on [patient]..."), + visible_message_flags = ALWAYS_SHOW_SELF_MESSAGE, ) if(!do_after( user, - other_delay, + other_delay * (auto_change_zone ? 1 : 0.9), patient, - extra_checks = CALLBACK(src, PROC_REF(can_heal), patient, user), + extra_checks = CALLBACK(src, PROC_REF(can_heal), patient, user, healed_zone), )) return + if(!auto_change_zone) + healed_zone = check_zone(user.zone_selected) + if(!try_heal_checks(patient, user, healed_zone)) + return + + else + if(!silent) + user.visible_message( + span_notice("[user] applies [src] on [patient]."), + span_notice("You apply [src] on [patient]."), + visible_message_flags = ALWAYS_SHOW_SELF_MESSAGE, + ) - if(!try_heal_checks(patient, user, heal_brute, heal_burn)) - return if(iscarbon(patient)) - if(!heal_carbon(patient, user, heal_brute, heal_burn)) + if(!heal_carbon(patient, user, healed_zone)) return else if(isanimal_or_basicmob(patient)) - if(!heal_simplemob(patient, user, heal_brute)) + if(!heal_simplemob(patient, user)) return else CRASH("Stack medical item healing a non-carbon, non-animal mob [patient] ([patient.type])") - log_combat(user, patient, "healed", name) + log_combat(user, patient, "healed", src) if(!use(1) || !repeating || amount <= 0) + var/atom/alert_loc = QDELETED(src) ? user : src + alert_loc.balloon_alert(user, repeating ? "all used up!" : "treated [parse_zone(healed_zone)]") + return + + // first, just try looping + // 1. we can keep healing the current target + // 2. the user's changed their target (and thus we should heal that limb instead) + var/preferred_target = check_zone(user.zone_selected) + if(try_heal_checks(patient, user, preferred_target, silent = TRUE)) + if(preferred_target != healed_zone) + patient.balloon_alert(user, "[apply_verb] [parse_zone(preferred_target)]...") + try_heal(patient, user, preferred_target, TRUE, auto_change_zone) + return + + // second, handle what happens otherwise + if(!iscarbon(patient)) + // behavior 0: non-carbons have no limbs so we can assume they are fully healed + patient.balloon_alert(user, "fully treated") + else if(auto_change_zone) + // behavior 1: automatically pick another zone to heal + try_heal_auto_change_zone(patient, user, preferred_target, healed_zone) + else + // behavior 2: assess injury, giving the user time to manually pick another zone + try_heal_manual_target(patient, user) + +/obj/item/stack/medical/proc/try_heal_auto_change_zone(mob/living/carbon/patient, mob/living/user, preferred_target, last_zone) + PRIVATE_PROC(TRUE) + + var/list/other_affected_limbs = list() + for(var/obj/item/bodypart/limb as anything in patient.bodyparts) + if(!try_heal_checks(patient, user, limb.body_zone, silent = TRUE)) + continue + other_affected_limbs += limb.body_zone + + if(!length(other_affected_limbs)) + patient.balloon_alert(user, "fully treated") + return + + var/next_picked = (preferred_target in other_affected_limbs) ? preferred_target : other_affected_limbs[1] + if(next_picked != last_zone) + patient.balloon_alert(user, "[apply_verb] [parse_zone(next_picked)]...") + try_heal(patient, user, next_picked, silent = TRUE, auto_change_zone = TRUE) + +/obj/item/stack/medical/proc/try_heal_manual_target(mob/living/carbon/patient, mob/living/user) + PRIVATE_PROC(TRUE) + + patient.balloon_alert(user, "assessing injury...") + if(!do_after(user, 1 SECONDS, patient)) return - if(!can_heal(patient, user)) + var/new_zone = check_zone(user.zone_selected) + if(!try_heal_checks(patient, user, new_zone)) return - try_heal(patient, user, silent = TRUE) + patient.balloon_alert(user, "[apply_verb] [parse_zone(new_zone)]...") + try_heal(patient, user, new_zone, silent = TRUE, auto_change_zone = FALSE) -/obj/item/stack/medical/proc/try_heal_checks(mob/living/patient, mob/user, brute, burn) - if(patient.stat == DEAD) - patient.balloon_alert(user, "they're dead!") +/// Checks if the passed patient can be healed by the passed user +/obj/item/stack/medical/proc/can_heal(mob/living/patient, mob/living/user, healed_zone, silent = FALSE) + return patient.try_inject(user, healed_zone, injection_flags = can_inject_flags | (silent ? NONE : INJECT_TRY_SHOW_ERROR_MESSAGE)) + +/// Checks a bunch of stuff to see if we can heal the patient, including can_heal +/// Gives a feedback if we can't ultimatly heal the patient (unless silent is TRUE) +/obj/item/stack/medical/proc/try_heal_checks(mob/living/patient, mob/living/user, healed_zone, silent = FALSE) + if(!(healed_zone in BODY_ZONES_ALL)) + stack_trace("Invalid zone ([healed_zone || "null"]) passed to try_heal_checks.") + healed_zone = BODY_ZONE_CHEST + + if(!can_heal(patient, user, healed_zone, silent)) + // has its own feedback + return FALSE + if(!works_on_dead && patient.stat == DEAD) + if(!silent) + patient.balloon_alert(user, "[patient.p_theyre()] dead!") return FALSE if(iscarbon(patient)) var/mob/living/carbon/carbon_patient = patient - var/obj/item/bodypart/affecting = carbon_patient.get_bodypart(check_zone(user.zone_selected)) + var/obj/item/bodypart/affecting = carbon_patient.get_bodypart(healed_zone) if(!affecting) //Missing limb? - carbon_patient.balloon_alert(user, "no [parse_zone(user.zone_selected)]!") + if(!silent) + carbon_patient.balloon_alert(user, "no [parse_zone(healed_zone)]!") return FALSE if(!IS_ORGANIC_LIMB(affecting)) //Limb must be organic to be healed - RR - carbon_patient.balloon_alert(user, "[affecting.plaintext_zone] is not organic!") + if(!silent) + carbon_patient.balloon_alert(user, "[affecting.plaintext_zone] is not organic!") return FALSE - var/datum/wound/burn/flesh/any_burn_wound = locate() in affecting.wounds + var/datum/wound/flesh/any_burn_wound = locate() in affecting.wounds var/can_heal_burn_wounds = (flesh_regeneration || sanitization) && any_burn_wound?.can_be_ointmented_or_meshed() var/can_suture_bleeding = stop_bleeding && affecting.get_modified_bleed_rate() > 0 - var/brute_to_heal = brute && affecting.brute_dam > 0 - var/burn_to_heal = burn && affecting.burn_dam > 0 + var/brute_to_heal = heal_brute && affecting.brute_dam > 0 + var/burn_to_heal = heal_burn && affecting.burn_dam > 0 if(!brute_to_heal && !burn_to_heal && !can_heal_burn_wounds && !can_suture_bleeding) - if(!brute_to_heal && stop_bleeding) // no brute, no bleeding - carbon_patient.balloon_alert(user, "[affecting.plaintext_zone] is not bleeding!") - else if(!burn_to_heal && (flesh_regeneration || sanitization) && any_burn_wound) // no burns, existing burn wounds are treated - carbon_patient.balloon_alert(user, "[affecting.plaintext_zone] has been fully treated!") - else if(!affecting.brute_dam && !affecting.burn_dam) // not hurt at all - carbon_patient.balloon_alert(user, "[affecting.plaintext_zone] is not hurt!") - else // probably hurt in some way but we are not the right item for this - carbon_patient.balloon_alert(user, "can't heal [affecting.plaintext_zone] with [name]!") + if(!silent) + if(!brute_to_heal && stop_bleeding) // no brute, no bleeding + carbon_patient.balloon_alert(user, "[affecting.plaintext_zone] is not bleeding or bruised!") + else if(!burn_to_heal && (flesh_regeneration || sanitization) && any_burn_wound) // no burns, existing burn wounds are treated + carbon_patient.balloon_alert(user, "[affecting.plaintext_zone] is fully treated, give it time!") + else if(!affecting.brute_dam && !affecting.burn_dam) // not hurt at all + carbon_patient.balloon_alert(user, "[affecting.plaintext_zone] is not hurt!") + else // probably hurt in some way but we are not the right item for this + carbon_patient.balloon_alert(user, "can't heal [affecting.plaintext_zone] with [name]!") return FALSE return TRUE if(isanimal_or_basicmob(patient)) if(!heal_brute) // only brute can heal - patient.balloon_alert(user, "can't heal with [name]!") + if(!silent) + patient.balloon_alert(user, "can't heal with [name]!") return FALSE if(!(patient.mob_biotypes & MOB_ORGANIC)) - patient.balloon_alert(user, "no organic tissue!") + if(!silent) + patient.balloon_alert(user, "no organic tissue!") return FALSE if(patient.health == patient.maxHealth) - patient.balloon_alert(user, "not hurt!") + if(!silent) + patient.balloon_alert(user, "not hurt!") return FALSE return TRUE + return FALSE -/// The healing effects on a carbon patient. Since we have extra details for dealing with bodyparts, we get our own fancy proc. Still returns TRUE on success and FALSE on fail -/obj/item/stack/medical/proc/heal_carbon(mob/living/carbon/patient, mob/user, brute, burn) - var/obj/item/bodypart/affecting = patient.get_bodypart(check_zone(user.zone_selected)) +/// The healing effects on a carbon patient. +/// Since we have extra details for dealing with bodyparts, we get our own fancy proc. +/// Still returns TRUE on success and FALSE on fail +/obj/item/stack/medical/proc/heal_carbon(mob/living/carbon/patient, mob/living/user, healed_zone) + var/obj/item/bodypart/affecting = patient.get_bodypart(healed_zone) user.visible_message( - span_infoplain(span_green("[user] applies [src] on [patient]'s [parse_zone(affecting.body_zone)].")), - span_infoplain(span_green("You apply [src] on [patient]'s [parse_zone(affecting.body_zone)].")) + span_green("[user] applies [src] on [patient]'s [affecting.plaintext_zone]."), + span_green("You apply [src] on [patient]'s [affecting.plaintext_zone]."), + visible_message_flags = ALWAYS_SHOW_SELF_MESSAGE, ) var/previous_damage = affecting.get_damage() - if(affecting.heal_damage(brute, burn)) + if(affecting.heal_damage(heal_brute, heal_burn)) patient.update_damage_overlays() if(stop_bleeding) for(var/datum/wound/wound as anything in affecting.wounds) @@ -195,25 +321,26 @@ break // one at a time affecting.adjustBleedStacks(-1 * stop_bleeding) if(flesh_regeneration || sanitization) - for(var/datum/wound/burn/flesh/wound as anything in affecting.wounds) + for(var/datum/wound/flesh/burn/wound as anything in affecting.wounds) if(wound.can_be_ointmented_or_meshed()) wound.flesh_healing += flesh_regeneration wound.sanitization += sanitization - break // one at a time (though i don't think you can get more than one burn wound on a limb) - + break // one at a time post_heal_effects(max(previous_damage - affecting.get_damage(), 0), patient, user) return TRUE -/obj/item/stack/medical/proc/heal_simplemob(mob/living/patient, mob/user, brute) +/// Healing a simple mob, just an adjustbruteloss call +/obj/item/stack/medical/proc/heal_simplemob(mob/living/patient, mob/living/user) patient.adjustBruteLoss(-1 * (heal_brute * patient.maxHealth / 100)) user.visible_message( - span_infoplain(span_green("[user] applies [src] on [patient].")), - span_infoplain(span_green("You apply [src] on [patient].")), + span_green("[user] applies [src] on [patient]."), + span_green("You apply [src] on [patient]."), + visible_message_flags = ALWAYS_SHOW_SELF_MESSAGE, ) return TRUE -///Override this proc for special post heal effects. -/obj/item/stack/medical/proc/post_heal_effects(amount_healed, mob/living/carbon/healed_mob, mob/user) +///Override this proc for special post heal effects. Only called for carbon patients. +/obj/item/stack/medical/proc/post_heal_effects(amount_healed, mob/living/carbon/healed_mob, mob/living/user) return /obj/item/stack/medical/bruise_pack @@ -228,6 +355,7 @@ other_delay = 2 SECONDS grind_results = list(/datum/reagent/medicine/c2/libital = 10) merge_type = /obj/item/stack/medical/bruise_pack + can_inject_flags = INJECT_CHECK_IGNORE_SPECIES /obj/item/stack/medical/bruise_pack/suicide_act(mob/living/user) user.visible_message(span_suicide("[user] is bludgeoning [user.p_them()]self with [src]! It looks like [user.p_theyre()] trying to commit suicide!")) @@ -251,6 +379,10 @@ burn_cleanliness_bonus = 0.35 merge_type = /obj/item/stack/medical/gauze drop_sound = 'sound/items/handling/cloth_drop.ogg' + apply_verb = "wrapping" + works_on_dead = TRUE + can_inject_flags = INJECT_CHECK_IGNORE_SPECIES + heal_sound = SFX_CLOTH_RIP /// tracks how many times we've been scrubbed thoroughly var/times_cleaned = 0 @@ -283,7 +415,7 @@ . = ..() if(.) return . - if(!(clean_types & CLEAN_TYPE_HARD_DECAL)) // gotta scrub realllly hard to clean gauze + if(!(clean_types & CLEAN_TYPE_BLOOD)) return . times_cleaned += 1 var/clean_to = initial(absorption_capacity) * (3 / (times_cleaned + 3)) @@ -294,95 +426,105 @@ return . -/obj/item/stack/medical/gauze/try_heal_checks(mob/living/patient, mob/user, brute, burn) - var/obj/item/bodypart/limb = patient.get_bodypart(check_zone(user.zone_selected)) - if(!limb) - patient.balloon_alert(user, "no [parse_zone(user.zone_selected)]!") +/obj/item/stack/medical/gauze/add_item_context(obj/item/source, list/context, atom/target, mob/living/user) + if(iscarbon(target)) + context[SCREENTIP_CONTEXT_LMB] = "Apply Gauze" + return CONTEXTUAL_SCREENTIP_SET + return NONE + +/obj/item/stack/medical/gauze/try_heal_checks(mob/living/patient, mob/living/user, healed_zone, silent = FALSE) + var/obj/item/bodypart/limb = patient.get_bodypart(healed_zone) + if(isnull(limb)) + if(!silent) + patient.balloon_alert(user, "no [parse_zone(healed_zone)]!") return FALSE if(limb.current_gauze && (limb.current_gauze.absorption_capacity * 1.2 > absorption_capacity)) // ignore if our new wrap is < 20% better than the current one, so someone doesn't bandage it 5 times in a row - patient.balloon_alert(user, pick("already bandaged!", "bandage is clean!")) // good enough + if(!silent) + patient.balloon_alert(user, pick("already bandaged!", "bandage is clean!")) // good enough return FALSE return TRUE // gauze is only relevant for wounds, which are handled in the wounds themselves -/obj/item/stack/medical/gauze/try_heal(mob/living/patient, mob/user, silent) - if(!try_heal_checks(patient, user, 0, 0)) - return - +/obj/item/stack/medical/gauze/try_heal(mob/living/patient, mob/living/user, healed_zone, silent, auto_change_zone) + var/obj/item/bodypart/limb = patient.get_bodypart(healed_zone) var/treatment_delay = (user == patient ? self_delay : other_delay) - var/obj/item/bodypart/limb = patient.get_bodypart(check_zone(user.zone_selected)) - - var/boosted = FALSE - if(LAZYLEN(limb.wounds)) - for(var/datum/wound/wound as anything in limb.wounds) - if(HAS_TRAIT(wound, TRAIT_WOUND_SCANNED)) - boosted = TRUE - break - else - // gives you extra time so you realize you're not treating a wound - treatment_delay *= 2 - - var/whose = user == patient ? "your" : "[patient]'s" - var/theirs = user == patient ? patient.p_their() : "[patient]'s" - var/wrap_or_replace = limb.current_gauze ? "replacing [limb.current_gauze] on" : "wrapping" - var/with_what = limb.current_gauze?.type == type ? "more of [src]" : src - if(boosted) + var/any_scanned = FALSE + for(var/datum/wound/woundies as anything in limb.wounds) + if(HAS_TRAIT(woundies, TRAIT_WOUND_SCANNED)) + any_scanned = TRUE + break + + if(any_scanned) treatment_delay *= 0.5 - user.visible_message( - span_notice("[user] begins expertly [wrap_or_replace] [theirs] [limb.plaintext_zone] with [with_what]."), - span_notice("You begin quickly [wrap_or_replace] [whose] [limb.plaintext_zone] with [with_what], keeping the holo-image indications in mind..."), - ) + if(user == patient) + if(!silent) + user.visible_message( + span_notice("[user] begins expertly wrapping the wounds on [p_their()]'s [limb.plaintext_zone] with [src]..."), + span_notice("You begin quickly wrapping the wounds on your [limb.plaintext_zone] with [src], keeping the holo-image indications in mind..."), + visible_message_flags = ALWAYS_SHOW_SELF_MESSAGE, + ) + else + if(!silent) + user.visible_message( + span_notice("[user] begins expertly wrapping the wounds on [patient]'s [limb.plaintext_zone] with [src]..."), + span_notice("You begin quickly wrapping the wounds on [patient]'s [limb.plaintext_zone] with [src], keeping the holo-image indications in mind..."), + visible_message_flags = ALWAYS_SHOW_SELF_MESSAGE, + ) else - user.visible_message( - span_notice("[user] begins [wrap_or_replace] [theirs] [limb.plaintext_zone] with [with_what]."), - span_notice("You begin [wrap_or_replace] [whose] [limb.plaintext_zone] with [with_what]..."), - ) - user.balloon_alert(user, "applying gauze...") - if(user != patient) - user.balloon_alert(patient, "applying gauze...") - - playsound(patient, pick( - 'maplestation_modules/sound/items/rip1.ogg', - 'maplestation_modules/sound/items/rip2.ogg', - 'maplestation_modules/sound/items/rip3.ogg', - 'maplestation_modules/sound/items/rip4.ogg', - ), 33) + if(!silent) + user.visible_message( + span_notice("[user] begins wrapping [patient]'s [limb.plaintext_zone] with [src]..."), + span_notice("You begin wrapping [user == patient ? "your" : "[patient]'s"] [limb.plaintext_zone] with [src]..."), + visible_message_flags = ALWAYS_SHOW_SELF_MESSAGE, + ) if(!do_after(user, treatment_delay, target = patient)) user.balloon_alert(user, "interrupted!") return - if(!try_heal_checks(patient, user, 0, 0)) - return - user.balloon_alert(user, "gauze applied") - if(user != patient) - user.balloon_alert(patient, "gauze applied") - - user.visible_message( - span_infoplain(span_green("[user] applies [src] to [theirs] [limb.plaintext_zone].")), - span_infoplain(span_green("You [limb.current_gauze?.type == type ? "replace" : "bandage"] the wounds on [whose] [limb.plaintext_zone].")), - ) + if(!silent) + patient.balloon_alert(user, "wrapped [parse_zone(healed_zone)]") + user.visible_message( + span_green("[user] applies [src] to [patient]'s [limb.plaintext_zone]."), + span_green("You bandage the wounds on [user == patient ? "your" : "[patient]'s"] [limb.plaintext_zone]."), + visible_message_flags = ALWAYS_SHOW_SELF_MESSAGE, + ) limb.apply_gauze(src) /obj/item/stack/medical/gauze/twelve amount = 12 -/obj/item/stack/medical/gauze/attackby(obj/item/I, mob/user, params) - if(I.tool_behaviour == TOOL_WIRECUTTER || I.get_sharpness()) - if(get_amount() < 2) - balloon_alert(user, "not enough gauze!") - return - var/obj/item/stack/sheet/cloth/cloth = new(I.drop_location()) - if(user.CanReach(src)) +/obj/item/stack/medical/gauze/item_interaction(mob/living/user, obj/item/tool, list/modifiers, is_right_clicking) + . = ..() + if(. & ITEM_INTERACT_BLOCKING) + return + if(tool.tool_behaviour != TOOL_WIRECUTTER && !tool.get_sharpness()) + return + if(get_amount() < 2) + balloon_alert(user, "not enough gauze!") + return ITEM_INTERACT_BLOCKING + + playsound(tool, 'maplestation_modules/sound/items/snip.ogg', 33, FALSE) + playsound(src, SFX_CLOTH_RIP, 33, FALSE) + if(!(flags_1 & HOLOGRAM_1)) + var/drop_loc = drop_location() + var/obj/item/stack/sheet/cloth/cloth = new(drop_loc) + if(user.CanReach(drop_loc)) + if(!QDELETED(cloth)) + transfer_fingerprints_to(cloth) + cloth.add_fingerprint(user) user.put_in_hands(cloth) - user.visible_message(span_notice("[user] cuts [src] into pieces of cloth with [I]."), \ - span_notice("You cut [src] into pieces of cloth with [I]."), \ - span_hear("You hear cutting.")) + user.visible_message( + span_notice("[user] cuts [src] into pieces of cloth with [tool]."), + span_notice("You cut [src] into pieces of cloth with [tool]."), + span_hear("You hear cutting."), + ) else //telekinesis - visible_message(span_notice("[I] cuts [src] into pieces of cloth."), \ - blind_message = span_hear("You hear cutting.")) - use(2) - else - return ..() + tool.visible_message( + span_notice("[tool] cuts [src] into pieces of cloth."), + blind_message = span_hear("You hear cutting."), + ) + use(2) + return ITEM_INTERACT_SUCCESS /obj/item/stack/medical/gauze/suicide_act(mob/living/user) user.visible_message(span_suicide("[user] begins tightening [src] around [user.p_their()] neck! It looks like [user.p_they()] forgot how to use medical supplies!")) @@ -460,6 +602,7 @@ sanitization = 1 grind_results = list(/datum/reagent/medicine/c2/lenturi = 10) merge_type = /obj/item/stack/medical/ointment + can_inject_flags = INJECT_CHECK_IGNORE_SPECIES /obj/item/stack/medical/ointment/suicide_act(mob/living/user) user.visible_message(span_suicide("[user] is squeezing [src] into [user.p_their()] mouth! [user.p_do(TRUE)]n't [user.p_they()] know that stuff is toxic?")) @@ -483,6 +626,7 @@ var/is_open = TRUE ///This var determines if the sterile packaging of the mesh has been opened. grind_results = list(/datum/reagent/medicine/spaceacillin = 2) merge_type = /obj/item/stack/medical/mesh + can_inject_flags = INJECT_CHECK_IGNORE_SPECIES /obj/item/stack/medical/mesh/Initialize(mapload, new_amount, merge = TRUE, list/mat_override=null, mat_amt=1) . = ..() @@ -495,14 +639,12 @@ return ..() icon_state = "regen_mesh_closed" -/obj/item/stack/medical/mesh/can_heal(mob/living/patient, mob/user) - . = ..() - if(!.) - return . +/obj/item/stack/medical/mesh/try_heal_checks(mob/living/patient, mob/living/user, healed_zone, silent = FALSE) if(!is_open) - balloon_alert(user, "open it first!") + if(!silent) + balloon_alert(user, "open it first!") return FALSE - return . + return ..() /obj/item/stack/medical/mesh/AltClick(mob/living/user) if(!is_open) @@ -559,6 +701,7 @@ heal_burn = 3 grind_results = list(/datum/reagent/consumable/aloejuice = 1) merge_type = /obj/item/stack/medical/aloe + can_inject_flags = INJECT_CHECK_IGNORE_SPECIES /obj/item/stack/medical/aloe/fresh amount = 2 @@ -579,6 +722,7 @@ grind_results = list(/datum/reagent/bone_dust = 10, /datum/reagent/carbon = 10) novariants = TRUE merge_type = /obj/item/stack/medical/bone_gel + can_inject_flags = INJECT_CHECK_IGNORE_SPECIES /obj/item/stack/medical/bone_gel/get_surgery_tool_overlay(tray_extended) return "gel" + (tray_extended ? "" : "_out") @@ -616,7 +760,11 @@ /obj/item/stack/medical/poultice name = "mourning poultices" singular_name = "mourning poultice" - desc = "A type of primitive herbal poultice.\nWhile traditionally used to prepare corpses for the mourning feast, it can also treat scrapes and burns on the living, however, it is liable to cause shortness of breath when employed in this manner.\nIt is imbued with ancient wisdom." + desc = "A type of primitive herbal poultice.\n\ + While traditionally used to prepare corpses for the mourning feast, \ + it can also treat scrapes and burns on the living, however, \ + it is liable to cause shortness of breath when employed in this manner.\n\ + It is imbued with ancient wisdom." icon_state = "poultice" amount = 15 max_amount = 15 @@ -629,9 +777,13 @@ mob_throw_hit_sound = 'sound/misc/moist_impact.ogg' hitsound = 'sound/misc/moist_impact.ogg' merge_type = /obj/item/stack/medical/poultice + apply_verb = "applying to" + works_on_dead = TRUE + can_inject_flags = INJECT_CHECK_IGNORE_SPECIES -/obj/item/stack/medical/poultice/post_heal_effects(amount_healed, mob/living/carbon/healed_mob, mob/user) +/obj/item/stack/medical/poultice/post_heal_effects(amount_healed, mob/living/carbon/healed_mob, mob/living/user) . = ..() + playsound(src, 'sound/misc/soggy.ogg', 30, TRUE) healed_mob.adjustOxyLoss(amount_healed) playsound(src, 'sound/misc/soggy.ogg', 30, TRUE) @@ -650,6 +802,7 @@ self_delay = 3 SECONDS other_delay = 1 SECONDS grind_results = list(/datum/reagent/medicine/c2/libital = 2) + can_inject_flags = INJECT_CHECK_IGNORE_SPECIES /obj/item/stack/medical/bandage/makeshift name = "makeshift bandage" diff --git a/code/game/objects/items/storage/backpack.dm b/code/game/objects/items/storage/backpack.dm index 0c09fa98d417..00875324e183 100644 --- a/code/game/objects/items/storage/backpack.dm +++ b/code/game/objects/items/storage/backpack.dm @@ -794,6 +794,7 @@ new /obj/item/gun/energy/recharge/kinetic_accelerator(src) new /obj/item/knife/combat/survival(src) new /obj/item/flashlight/seclite(src) + new /obj/item/skillchip/job/off_z_pain_resistance(src) /* * Messenger Bag Types diff --git a/code/game/objects/items/storage/belt.dm b/code/game/objects/items/storage/belt.dm index 03b5f7267452..2142464de914 100644 --- a/code/game/objects/items/storage/belt.dm +++ b/code/game/objects/items/storage/belt.dm @@ -288,6 +288,7 @@ /obj/item/surgicaldrill, /obj/item/tank/internals/emergency_oxygen, /obj/item/wrench/medical, + /obj/item/razor/scissors, )) /obj/item/storage/belt/medical/paramedic @@ -300,7 +301,7 @@ /obj/item/storage/belt/medical/paramedic/PopulateContents() SSwardrobe.provide_type(/obj/item/sensor_device, src) SSwardrobe.provide_type(/obj/item/stack/medical/gauze/twelve, src) - SSwardrobe.provide_type(/obj/item/stack/medical/bone_gel, src) + SSwardrobe.provide_type(/obj/item/razor/scissors/medical/trauma, src) SSwardrobe.provide_type(/obj/item/stack/sticky_tape/surgical, src) SSwardrobe.provide_type(/obj/item/reagent_containers/syringe, src) SSwardrobe.provide_type(/obj/item/reagent_containers/cup/bottle/ammoniated_mercury, src) diff --git a/code/game/objects/items/storage/medkit.dm b/code/game/objects/items/storage/medkit.dm index 4282cc376837..05642f528e68 100644 --- a/code/game/objects/items/storage/medkit.dm +++ b/code/game/objects/items/storage/medkit.dm @@ -388,6 +388,7 @@ /obj/item/hemostat, /obj/item/cautery, /obj/item/autopsy_scanner, + /obj/item/clothing/mask/surgical, )) /obj/item/storage/medkit/coroner/PopulateContents() diff --git a/code/game/objects/items/tanks/tanks.dm b/code/game/objects/items/tanks/tanks.dm index f742e85265f6..a6ed58a7782e 100644 --- a/code/game/objects/items/tanks/tanks.dm +++ b/code/game/objects/items/tanks/tanks.dm @@ -273,7 +273,7 @@ // (kpa * L * K * mol) / (kpa * L * K) // If we cancel it all out, we get moles, which is the expected unit // This sort of thing comes up often in atmos, keep the tool in mind for other bits of code - var/moles_needed = actual_distribute_pressure*volume_to_return/(R_IDEAL_GAS_EQUATION*air_contents.temperature) + var/moles_needed = (actual_distribute_pressure * volume_to_return) / (R_IDEAL_GAS_EQUATION * air_contents.return_temperature()) return remove_air(moles_needed) diff --git a/code/game/objects/objs.dm b/code/game/objects/objs.dm index 66139594433c..91489853edd8 100644 --- a/code/game/objects/objs.dm +++ b/code/game/objects/objs.dm @@ -113,13 +113,17 @@ GLOBAL_LIST_EMPTY(objects_by_id_tag) // datum/air_group to tell lifeform to process using that breath return //DEFAULT: Take air from turf to give to have mob process - if(breath_request>0) - var/datum/gas_mixture/environment = return_air() - var/breath_percentage = BREATH_VOLUME / environment.return_volume() - return remove_air(environment.total_moles() * breath_percentage) - else + if(breath_request <= 0) + return null + + var/datum/gas_mixture/environment = return_air() + if(isnull(environment)) return null + // a little bit of handwaving is done here, exposing the mob to only a fraction (1 atm) of the total air + var/mols_requested = (ONE_ATMOSPHERE * breath_request) / (R_IDEAL_GAS_EQUATION * environment.return_temperature()) + return remove_air(min(mols_requested, environment.total_moles())) + /obj/proc/updateUsrDialog() if(!(obj_flags & IN_USE)) return diff --git a/code/game/objects/structures/bedsheet_bin.dm b/code/game/objects/structures/bedsheet_bin.dm index 75a2c364ad63..f9562e7a3877 100644 --- a/code/game/objects/structures/bedsheet_bin.dm +++ b/code/game/objects/structures/bedsheet_bin.dm @@ -124,17 +124,35 @@ LINEN BINS UnregisterSignal(sleeper, COMSIG_QDELETING) signal_sleeper = null -/obj/item/bedsheet/attackby(obj/item/I, mob/user, params) - if(I.tool_behaviour == TOOL_WIRECUTTER || I.get_sharpness()) - if (!(flags_1 & HOLOGRAM_1)) - var/obj/item/stack/sheet/cloth/shreds = new (get_turf(src), stack_amount) +/obj/item/bedsheet/item_interaction(mob/living/user, obj/item/tool, list/modifiers, is_right_clicking) + . = ..() + if(. & ITEM_INTERACT_ANY_BLOCKER) + return + if(tool.tool_behaviour != TOOL_WIRECUTTER && !tool.get_sharpness()) + return + + playsound(tool, 'maplestation_modules/sound/items/snip.ogg', 33, FALSE) + playsound(src, SFX_CLOTH_RIP, 33, FALSE) + if (!(flags_1 & HOLOGRAM_1)) + var/drop_loc = drop_location() + var/obj/item/stack/sheet/cloth/shreds = new (drop_loc, stack_amount) + if(user.CanReach(drop_loc)) if(!QDELETED(shreds)) //stacks merged transfer_fingerprints_to(shreds) shreds.add_fingerprint(user) - qdel(src) - to_chat(user, span_notice("You tear [src] up.")) - else - return ..() + user.put_in_hands(shreds) + user.visible_message( + span_notice("[user] cuts [src] into pieces of cloth with [tool]."), + span_notice("You cut [src] into pieces of cloth with [tool]."), + span_hear("You hear cutting."), + ) + else //telekinesis + tool.visible_message( + span_notice("[tool] cuts [src] into pieces of cloth."), + blind_message = span_hear("You hear cutting."), + ) + qdel(src) + return ITEM_INTERACT_SUCCESS /obj/item/bedsheet/AltClick(mob/living/user) // double check the canUseTopic args to make sure it's correct diff --git a/code/game/objects/structures/crates_lockers/closets/bodybag.dm b/code/game/objects/structures/crates_lockers/closets/bodybag.dm index ccfa0c4e8cae..2630186a6ded 100644 --- a/code/game/objects/structures/crates_lockers/closets/bodybag.dm +++ b/code/game/objects/structures/crates_lockers/closets/bodybag.dm @@ -178,25 +178,23 @@ /obj/structure/closet/body_bag/bluespace/perform_fold(mob/living/carbon/human/the_folder) visible_message(span_notice("[the_folder] folds up [src].")) - var/obj/item/bodybag/folding_bodybag = undeploy_bodybag(the_folder.loc) + var/obj/item/bodybag/bluespace/folding_bodybag = undeploy_bodybag(the_folder.loc) + folding_bodybag.internal_air = internal_air + internal_air = null var/max_weight_of_contents = initial(folding_bodybag.w_class) - for(var/am in contents) - var/atom/movable/content = am + for(var/atom/movable/content in src) content.forceMove(folding_bodybag) if(isliving(content)) to_chat(content, span_userdanger("You're suddenly forced into a tiny, compressed space!")) - if(iscarbon(content)) - var/mob/living/carbon/mob = content - if (mob.dna?.get_mutation(/datum/mutation/human/dwarfism)) - max_weight_of_contents = max(WEIGHT_CLASS_NORMAL, max_weight_of_contents) - continue + if(HAS_TRAIT(content, TRAIT_DWARF)) + max_weight_of_contents = max(WEIGHT_CLASS_NORMAL, max_weight_of_contents) + continue if(!isitem(content)) max_weight_of_contents = max(WEIGHT_CLASS_BULKY, max_weight_of_contents) continue - var/obj/item/A_is_item = content - if(A_is_item.w_class < max_weight_of_contents) - continue - max_weight_of_contents = A_is_item.w_class + var/obj/item/content_item = content + max_weight_of_contents = max(max_weight_of_contents, content_item.w_class) + folding_bodybag.update_weight_class(max_weight_of_contents) the_folder.put_in_hands(folding_bodybag) diff --git a/code/game/objects/structures/crates_lockers/crates/critter.dm b/code/game/objects/structures/crates_lockers/crates/critter.dm index 5e1873d166ab..d1859f77a179 100644 --- a/code/game/objects/structures/crates_lockers/crates/critter.dm +++ b/code/game/objects/structures/crates_lockers/crates/critter.dm @@ -17,20 +17,7 @@ can_install_electronics = FALSE elevation = 21 elevation_open = 0 - - var/obj/item/tank/internals/emergency_oxygen/tank - -/obj/structure/closet/crate/critter/Initialize(mapload) - . = ..() - tank = new - -/obj/structure/closet/crate/critter/Destroy() - var/turf/T = get_turf(src) - if(tank) - tank.forceMove(T) - tank = null - - return ..() + sealed = TRUE /obj/structure/closet/crate/critter/update_icon_state() SHOULD_CALL_PARENT(FALSE) @@ -45,15 +32,3 @@ . += "crittercrate_door" if(manifest) . += "manifest" - -/obj/structure/closet/crate/critter/return_air() - if(tank) - return tank.return_air() - else - return loc.return_air() - -/obj/structure/closet/crate/critter/return_analyzable_air() - if(tank) - return tank.return_analyzable_air() - else - return null diff --git a/code/game/objects/structures/signs/_signs.dm b/code/game/objects/structures/signs/_signs.dm index e2afb08bcaf9..8f1650117ee7 100644 --- a/code/game/objects/structures/signs/_signs.dm +++ b/code/game/objects/structures/signs/_signs.dm @@ -275,6 +275,10 @@ var/obj/structure/sign/potential_sign = s if(!initial(potential_sign.is_editable)) continue - output[initial(potential_sign.sign_change_name)] = potential_sign + var/shown_name = initial(potential_sign.sign_change_name) || capitalize(format_text(initial(potential_sign.name))) + if(output[shown_name]) + stack_trace("Two signs share the same sign_change_name: [output[shown_name]] and [potential_sign]") + continue + output[shown_name] = potential_sign output = sort_list(output) //Alphabetizes the results. return output diff --git a/code/game/objects/structures/watercloset.dm b/code/game/objects/structures/watercloset.dm index 229cf4343dd4..30e2b9982901 100644 --- a/code/game/objects/structures/watercloset.dm +++ b/code/game/objects/structures/watercloset.dm @@ -655,20 +655,6 @@ MAPPING_DIRECTIONAL_HELPERS(/obj/structure/sink/kitchen, (-16)) playsound(loc, 'sound/effects/slosh.ogg', 25, TRUE) return - if(istype(O, /obj/item/stack/medical/gauze)) - var/obj/item/stack/medical/gauze/G = O - new /obj/item/reagent_containers/cup/rag(loc) - to_chat(user, span_notice("You tear off a strip of gauze and make a rag.")) - G.use(1) - return - - if(istype(O, /obj/item/stack/sheet/cloth)) - var/obj/item/stack/sheet/cloth/cloth = O - new /obj/item/reagent_containers/cup/rag(loc) - to_chat(user, span_notice("You tear off a strip of cloth and make a rag.")) - cloth.use(1) - return - if(istype(O, /obj/item/stack/ore/glass)) new /obj/item/stack/sheet/sandblock(loc) to_chat(user, span_notice("You wet the sand and form it into a block.")) diff --git a/code/game/sound.dm b/code/game/sound.dm index c5c330bbb75f..118f90f548f0 100644 --- a/code/game/sound.dm +++ b/code/game/sound.dm @@ -536,6 +536,13 @@ 'sound/effects/writing_pen/writing_pen6.ogg', 'sound/effects/writing_pen/writing_pen7.ogg', ) + if(SFX_CLOTH_RIP) + soundin = pick( + 'maplestation_modules/sound/items/rip1.ogg', + 'maplestation_modules/sound/items/rip2.ogg', + 'maplestation_modules/sound/items/rip3.ogg', + 'maplestation_modules/sound/items/rip4.ogg', + ) if(SFX_SEATBELT_BUCKLE) soundin = pick( 'sound/machines/buckle/buckle1.ogg', diff --git a/code/modules/atmospherics/machinery/components/unary_devices/cryo.dm b/code/modules/atmospherics/machinery/components/unary_devices/cryo.dm index 53b2dfa1818c..8f5079ac0f3e 100644 --- a/code/modules/atmospherics/machinery/components/unary_devices/cryo.dm +++ b/code/modules/atmospherics/machinery/components/unary_devices/cryo.dm @@ -63,6 +63,11 @@ else animate(src) +#define TREATING_BASIC_DAMAGE (1<<0) +#define TREATING_WOUNDS (1<<1) +#define TREATING_PAIN (1<<2) +#define TREATING_SHOCK (1<<3) + /// Cryo cell /obj/machinery/cryo_cell name = "cryo cell" @@ -95,8 +100,8 @@ var/obj/item/radio/radio /// Visual content - Occupant var/atom/movable/visual/cryo_occupant/occupant_vis - ///Cryo will continue to treat people with 0 damage but existing wounds, but will sound off when damage healing is done in case doctors want to directly treat the wounds instead - var/treating_wounds = FALSE + /// tracks everything the machine is currently treating + var/ongoing_treatment = NONE /// Reference to the datum connector we're using to interface with the pipe network var/datum/gas_machine_connector/internal_connector /// Check if the machine has been turned on @@ -118,7 +123,7 @@ occupant_vis = new(mapload, src) vis_contents += occupant_vis - internal_connector = new(loc, src, dir, CELL_VOLUME * 0.5) + internal_connector = new(loc, src, dir, TANK_STANDARD_VOLUME * 4) register_context() @@ -320,26 +325,37 @@ return PROCESS_KILL // Don't bother with fully healed people. - if(mob_occupant.get_organic_health() >= mob_occupant.getMaxHealth()) - if(iscarbon(mob_occupant)) - var/mob/living/carbon/C = mob_occupant - if(C.all_wounds) - if(!treating_wounds) // if we have wounds and haven't already alerted the doctors we're only dealing with the wounds, let them know - treating_wounds = TRUE - playsound(src, 'sound/machines/cryo_warning.ogg', 100) // Bug the doctors. - radio.talk_into(src, "Patient vitals fully recovered, continuing automated wound treatment.", RADIO_CHANNEL_MEDICAL) - else // otherwise if we were only treating wounds and now we don't have any, turn off treating_wounds so we can boot 'em out - treating_wounds = FALSE - - if(!treating_wounds) - set_on(FALSE) - playsound(src, 'sound/machines/cryo_warning.ogg', 100) // Bug the doctors. - var/msg = "Patient fully restored." - if(autoeject) // Eject if configured. - msg += " Auto ejecting patient now." - open_machine() - radio.talk_into(src, msg, RADIO_CHANNEL_MEDICAL) - return PROCESS_KILL + if(mob_occupant.get_organic_health() < mob_occupant.getMaxHealth()) + ongoing_treatment |= TREATING_BASIC_DAMAGE + else + ongoing_treatment &= ~TREATING_BASIC_DAMAGE + + if(iscarbon(mob_occupant)) + var/mob/living/carbon/carbon_occupant = mob_occupant + if(LAZYLEN(carbon_occupant.all_wounds)) + ongoing_treatment |= TREATING_WOUNDS + else + ongoing_treatment &= ~TREATING_WOUNDS + + if(mob_occupant.pain_controller?.get_total_pain() > 30) + ongoing_treatment |= TREATING_PAIN + else + ongoing_treatment &= ~TREATING_PAIN + + if(mob_occupant.pain_controller?.traumatic_shock > 30) + ongoing_treatment |= TREATING_SHOCK + else + ongoing_treatment &= ~TREATING_SHOCK + + if(!ongoing_treatment) + set_on(FALSE) + playsound(src, 'sound/machines/cryo_warning.ogg', 100) // Bug the doctors. + var/msg = "Patient fully restored." + if(autoeject) // Eject if configured. + msg += " Auto ejecting patient now." + open_machine() + radio.talk_into(src, msg, RADIO_CHANNEL_MEDICAL) + return PROCESS_KILL var/datum/gas_mixture/air1 = internal_connector.gas_connector.airs[1] if(!QDELETED(beaker) && air1.total_moles() > CRYO_MIN_GAS_MOLES) @@ -350,6 +366,11 @@ methods = VAPOR ) +#undef TREATING_BASIC_DAMAGE +#undef TREATING_WOUNDS +#undef TREATING_PAIN +#undef TREATING_SHOCK + /obj/machinery/cryo_cell/process_atmos() if(!on) return PROCESS_KILL @@ -393,15 +414,18 @@ /obj/machinery/cryo_cell/handle_internal_lifeform(mob/lifeform_inside_me, breath_request) if(breath_request <= 0) return null - //return breathable air - var/datum/gas_mixture/air1 = internal_connector.gas_connector.airs[1] - var/breath_percentage = breath_request / air1.volume - . = air1.remove(air1.total_moles() * breath_percentage) + var/datum/gas_mixture/internal_air = internal_connector.gas_connector.airs[1] + // a little bit of handwaving is done here, exposing the mob to only a fraction (1 atm) of the total air in the cell + var/mols_requested = (ONE_ATMOSPHERE * breath_request) / (R_IDEAL_GAS_EQUATION * internal_air.return_temperature()) + . = internal_air.remove(min(mols_requested, internal_air.total_moles())) //update molar changes throughout the pipenet internal_connector.gas_connector.update_parents() +/obj/machinery/cryo_cell/return_analyzable_air() + return internal_connector.gas_connector.airs[1] + /obj/machinery/cryo_cell/assume_air(datum/gas_mixture/giver) return internal_connector.gas_connector.airs[1].merge(giver) @@ -414,10 +438,10 @@ if(!state_open && !panel_open) set_on(FALSE) flick("pod-open-anim", src) + ongoing_treatment = NONE return ..() /obj/machinery/cryo_cell/close_machine(mob/living/carbon/user, density_to_set = TRUE) - treating_wounds = FALSE if(state_open && !panel_open) flick("pod-close-anim", src) . = ..() diff --git a/code/modules/clothing/neck/_neck.dm b/code/modules/clothing/neck/_neck.dm index efdce1904e67..11fde2da0025 100644 --- a/code/modules/clothing/neck/_neck.dm +++ b/code/modules/clothing/neck/_neck.dm @@ -273,12 +273,16 @@ //assess heart if(body_part == BODY_ZONE_CHEST)//if we're listening to the chest - if(isnull(heart) || !heart.is_beating() || carbon_patient.stat == DEAD) - render_list += "You don't hear a heartbeat!\n"//they're dead or their heart isn't beating - else if(heart.damage > 10 || carbon_patient.blood_volume <= BLOOD_VOLUME_OKAY) - render_list += "You hear a weak heartbeat.\n"//their heart is damaged, or they have critical blood - else - render_list += "You hear a healthy heartbeat.\n"//they're okay :D + var/heart_rate = carbon_patient.get_heart_rate() + switch(heart_rate) + if(0) + render_list += "You don't hear a heartbeat!\n" + if(1 to SLOW_HEARTBEAT_THRESHOLD) + render_list += "You hear a weak heartbeat.\n" + if(SLOW_HEARTBEAT_THRESHOLD to FAST_HEARTBEAT_THRESHOLD) + render_list += "You hear a healthy heartbeat.\n"//they're okay :D + if(FAST_HEARTBEAT_THRESHOLD to INFINITY) + render_list += "You hear a fast heartbeat.\n" if(BODY_ZONE_PRECISE_GROIN)//If we're targeting the groin render_list += span_info("You carefully press down on [carbon_patient]'s abdomen:\n") diff --git a/code/modules/mob/living/basic/icemoon/ice_demon/ice_demon.dm b/code/modules/mob/living/basic/icemoon/ice_demon/ice_demon.dm index abbaff291344..d8f071adb20e 100644 --- a/code/modules/mob/living/basic/icemoon/ice_demon/ice_demon.dm +++ b/code/modules/mob/living/basic/icemoon/ice_demon/ice_demon.dm @@ -51,6 +51,9 @@ new /obj/item/raw_anomaly_core/bluespace(loc) return ..() +/mob/living/basic/mining/ice_demon/get_blood_type() + return null + /mob/living/basic/mining/demon_afterimage name = "afterimage demonic watcher" desc = "Is this some sort of illusion?" @@ -87,3 +90,6 @@ melee_damage_lower = 10 melee_damage_upper = 10 existence_period = 7 SECONDS + +/mob/living/basic/mining/demon_afterimage/get_blood_type() + return null diff --git a/code/modules/mob/living/carbon/carbon.dm b/code/modules/mob/living/carbon/carbon.dm index d2f076bf5075..982ad61ccc5f 100644 --- a/code/modules/mob/living/carbon/carbon.dm +++ b/code/modules/mob/living/carbon/carbon.dm @@ -603,6 +603,22 @@ if(num_seared_parts >= 3) become_husk(BURN) +#ifdef HEALTH_DEBUG + if(client && hud_used) + hud_used.healthdoll.maptext_x = -50 + hud_used.healthdoll.maptext_height = 200 + hud_used.healthdoll.maptext_width = 75 + hud_used.healthdoll.maptext = MAPTEXT("Th: [health]\ +
Br: [total_brute]\ +
Bu: [total_burn]\ +
O: [total_oxy]\ +
T: [total_tox]\ +
Con: [consciousness]\ +
Pain: [pain_controller.get_total_pain()]\ +
Shock: [pain_controller.traumatic_shock]\ + ") +#endif + SEND_SIGNAL(src, COMSIG_LIVING_HEALTH_UPDATE) /datum/movespeed_modifier/carbon_consciousness diff --git a/code/modules/mob/living/carbon/carbon_defense.dm b/code/modules/mob/living/carbon/carbon_defense.dm index 5e0d6701a9a2..4c71c435d4da 100644 --- a/code/modules/mob/living/carbon/carbon_defense.dm +++ b/code/modules/mob/living/carbon/carbon_defense.dm @@ -294,13 +294,13 @@ var/wound_type var/wound_name = "" if(electrical_burn_roll > 150) - wound_type = /datum/wound/burn/flesh/critical + wound_type = /datum/wound/flesh/burn/critical wound_name = "Arc Flash Electrical Burn" else if(electrical_burn_roll > 80) - wound_type = /datum/wound/burn/flesh/severe + wound_type = /datum/wound/flesh/burn/severe wound_name = "HV Electrical Burn" else if(electrical_burn_roll > 20) - wound_type = /datum/wound/burn/flesh/moderate + wound_type = /datum/wound/flesh/burn/moderate wound_name = "LV Electrical Burn" if(wound_type) var/datum/wound/the_wound = zapped.force_wound_upwards(wound_type, wound_source = source) diff --git a/code/modules/mob/living/carbon/human/_species.dm b/code/modules/mob/living/carbon/human/_species.dm index adefe612ede4..ff8c441f007f 100644 --- a/code/modules/mob/living/carbon/human/_species.dm +++ b/code/modules/mob/living/carbon/human/_species.dm @@ -1051,16 +1051,11 @@ GLOBAL_LIST_EMPTY(features_by_species) user.do_pat_fire(target) return TRUE - if(!target.appears_alive()) - to_chat(user, span_warning("[target] has no pulse!")) + if(target.body_position != LYING_DOWN) + target.help_shake_act(user) return TRUE - if(target.stat == HARD_CRIT && target.body_position == LYING_DOWN) // having a heart attack coincidentally also puts you in hard crit - user.do_cpr(target) - return TRUE - - target.help_shake_act(user) - log_combat(user, target, "shaken") + user.open_help_radial(target) return TRUE /datum/species/proc/grab(mob/living/carbon/human/user, mob/living/carbon/human/target, datum/martial_art/attacker_style) diff --git a/code/modules/mob/living/carbon/human/human.dm b/code/modules/mob/living/carbon/human/human.dm index be6062c7a7a3..3b52d51a5826 100644 --- a/code/modules/mob/living/carbon/human/human.dm +++ b/code/modules/mob/living/carbon/human/human.dm @@ -349,10 +349,7 @@ /mob/living/carbon/human/try_inject(mob/user, target_zone, injection_flags) . = ..() if(!. && (injection_flags & INJECT_TRY_SHOW_ERROR_MESSAGE) && user) - if(!target_zone) - target_zone = get_bodypart(check_zone(user.zone_selected)) - var/obj/item/bodypart/the_part = isbodypart(target_zone) ? target_zone : get_bodypart(check_zone(target_zone)) //keep these synced - to_chat(user, span_alert("There is no exposed flesh or thin material on [p_their()] [the_part.plaintext_zone].")) + balloon_alert(user, "no exposed skin on [target_zone || check_zone(user.zone_selected)]!") #define CHECK_PERMIT(item) (item && item.item_flags & NEEDS_PERMIT) @@ -450,6 +447,8 @@ /mob/living/carbon/human/proc/do_cpr(mob/living/carbon/target) if(DOING_INTERACTION_WITH_TARGET(src, target)) return + if(target.body_position != LYING_DOWN || target.on_fire || target == src) + return cpr_process(target, beat = 1) // begin at beat 1, skip the first breath @@ -739,6 +738,7 @@ VV_DROPDOWN_OPTION(VV_HK_PURRBATION, "Toggle Purrbation") VV_DROPDOWN_OPTION(VV_HK_APPLY_DNA_INFUSION, "Apply DNA Infusion") VV_DROPDOWN_OPTION(VV_HK_TURN_INTO_MMI, "Turn into MMI") + VV_DROPDOWN_OPTION(VV_HK_MAKE_ME_TANKY, "Make This Mob Tankier") /mob/living/carbon/human/vv_do_topic(list/href_list) . = ..() @@ -859,7 +859,66 @@ qdel(src) + if(href_list[VV_HK_MAKE_ME_TANKY]) + var/list/options = list( + "More effective HP", + "Less brute", + "Less burn", + "Less toxin / oxygen / brain", + "Less pain", + "Less environmental", + "Tougher organs", + "Stun resistance", + "Health regen", + ) + var/list/picked = list() + while(length(options)) + var/result = tgui_input_list(usr, "Pick something. Select \"Done\" to finish your selection.", "Tanky", options + list("Done", "Cancel")) + if(QDELETED(src) || !result || result == "Cancel") + return + if(result == "Done") + break + picked += result + options -= result + if(!length(picked)) + return + var/major = tgui_input_list(usr, "REALLY tanky or just a little tanky?", "Tanky", list("Little", "Lot")) + if(QDELETED(src) || !major) + return + major = (major == "Lot") + message_admins("[key_name(usr)] has made [key_name(src)] tanky with the following: [english_list(picked)]") + log_admin("[key_name(usr)] has made [key_name(src)] tanky with the following: [english_list(picked)]") + + for(var/i in picked) + switch(i) + if("More effective HP") + add_consciousness_modifier("badmin", major ? 30 : 10) + if("Less brute") + physiology.brute_mod *= (major ? 0.5 : 0.75) + physiology.bleed_mod *= (major ? 0.6 : 0.8) + if("Less burn") + physiology.burn_mod *= (major ? 0.5 : 0.75) + if("Less toxin / oxygen / brain") + physiology.tox_mod *= (major ? 0.5 : 0.75) + physiology.oxy_mod *= (major ? 0.5 : 0.75) + physiology.brain_mod *= (major ? 0.5 : 0.75) + if("Less pain") + set_pain_mod("badmin", major ? 0.6 : 0.8) + if("Tougher organs") + for(var/obj/item/organ/internal/organ as anything in bodyparts) + organ.maxHealth *= (major ? 1.5 : 1.25) + if("Less environmental") + physiology.pressure_mod *= (major ? 0.6 : 0.8) + physiology.heat_mod *= (major ? 0.6 : 0.8) + physiology.cold_mod *= (major ? 0.6 : 0.8) + if("Stun resistance") + ADD_TRAIT(src, TRAIT_BATON_RESISTANCE, "Badmin") + physiology.stamina_mod *= (major ? 0.5 : 0.75) + physiology.stun_mod *= (major ? 0.5 : 0.75) + physiology.knockdown_mod *= (major ? 0.5 : 0.75) + if("Health regen") + apply_status_effect(/datum/status_effect/basic_health_regen, major ? 1.5 : 0.5) /mob/living/carbon/human/limb_attack_self() var/obj/item/bodypart/arm = hand_bodyparts[active_hand_index] diff --git a/code/modules/mob/living/carbon/human/species_types/lizardpeople.dm b/code/modules/mob/living/carbon/human/species_types/lizardpeople.dm index 85c03ceac322..a0dd1ade8b0d 100644 --- a/code/modules/mob/living/carbon/human/species_types/lizardpeople.dm +++ b/code/modules/mob/living/carbon/human/species_types/lizardpeople.dm @@ -206,9 +206,7 @@ Lizard subspecies: SILVER SCALED id = SPECIES_LIZARD_SILVER inherent_biotypes = MOB_ORGANIC|MOB_HUMANOID|MOB_REPTILE inherent_traits = list( - TRAIT_HOLY, - TRAIT_NOBREATH, - TRAIT_PIERCEIMMUNE, + TRAIT_PIERCEIMMUNE, // future todo : someone should make pierce immunity per-bodypart rather than blanket TRAIT_RESISTHIGHPRESSURE, TRAIT_RESISTLOWPRESSURE, TRAIT_TACKLING_TAILED_DEFENDER, diff --git a/code/modules/mob/living/carbon/life.dm b/code/modules/mob/living/carbon/life.dm index e37e5a696e5c..0c0d5442262b 100644 --- a/code/modules/mob/living/carbon/life.dm +++ b/code/modules/mob/living/carbon/life.dm @@ -58,9 +58,6 @@ add_mood_event("suffocation", /datum/mood_event/suffocation) else clear_mood_event("suffocation") - else if(isobj(loc)) - var/obj/location_as_object = loc - location_as_object.handle_internal_lifeform(src, 0) // Second link in a breath chain, calls [carbon/proc/check_breath()] /mob/living/carbon/proc/breathe(seconds_per_tick, times_fired, next_breath = 4) @@ -87,21 +84,11 @@ losebreath -= 1 if(prob(10) && consciousness > 10) pain_emote("gasp", 1 SECONDS) - if(isobj(loc)) - var/obj/loc_as_obj = loc - loc_as_obj.handle_internal_lifeform(src, 0) skip_breath = TRUE // Breathe from internals or externals (name is misleading) else if(internal || external) - breath = get_breath_from_internal(BREATH_VOLUME) - - if(breath == SKIP_INTERNALS) //in case of 0 pressure internals - breath = get_breath_from_surroundings(environment, BREATH_VOLUME) - - else if(isobj(loc)) //Breathe from loc as obj again - var/obj/loc_as_obj = loc - loc_as_obj.handle_internal_lifeform(src, 0) + breath = get_breath_from_internal(BREATH_VOLUME) || get_breath_from_surroundings(environment, BREATH_VOLUME) // Breathe from air else @@ -151,18 +138,22 @@ if(invalid_internals()) // Unexpectely lost breathing apparatus and ability to breathe from the internal air tank. cutoff_internals() - return SKIP_INTERNALS + return null + var/datum/gas_mixture/breath = new(volume_needed) + var/datum/gas_mixture/obtained if (external) - . = external.remove_air_volume(volume_needed) + obtained = external.remove_air_volume(volume_needed) else if (internal) - . = internal.remove_air_volume(volume_needed) + obtained = internal.remove_air_volume(volume_needed) else // Return without taking a breath if there is no air tank. stack_trace("get_breath_from_internal called on a mob without internals or externals") - return SKIP_INTERNALS + return null - return . + if(obtained) + breath.merge(obtained) + return breath /** * Attempts to take a breath from the surroundings. @@ -171,14 +162,18 @@ * Returns null if there was no gas in the surroundings or no gas was distributed. */ /mob/living/carbon/proc/get_breath_from_surroundings(datum/gas_mixture/environment, volume_needed) + var/datum/gas_mixture/breath = new(volume_needed) + var/datum/gas_mixture/obtained if(isobj(loc)) //Breathe from loc as object var/obj/loc_as_obj = loc - . = loc_as_obj.handle_internal_lifeform(src, volume_needed) + obtained = loc_as_obj.handle_internal_lifeform(src, volume_needed) else if(isturf(loc)) //Breathe from loc as turf - . = loc.remove_air((environment?.total_moles() * BREATH_PERCENTAGE) || 0) + obtained = loc.remove_air((environment?.total_moles() * BREATH_PERCENTAGE) || 0) - return . + if(obtained) + breath.merge(obtained) + return breath /mob/living/carbon/proc/handle_blood(seconds_per_tick, times_fired) return diff --git a/code/modules/mob/living/life.dm b/code/modules/mob/living/life.dm index 93f15d3b04ba..e741b077b322 100644 --- a/code/modules/mob/living/life.dm +++ b/code/modules/mob/living/life.dm @@ -97,20 +97,16 @@ var/burn_damage = HEAT_DAMAGE if(effective_temp > heat_threshold_high) burn_damage *= 8 - // melbert todo : status effects should handle the damage, not the proc - apply_status_effect(/datum/status_effect/thermia/hyper/three) else if(effective_temp > heat_threshold_medium) burn_damage *= 4 - apply_status_effect(/datum/status_effect/thermia/hyper/two) else if(effective_temp > heat_threshold_low) burn_damage *= 2 - apply_status_effect(/datum/status_effect/thermia/hyper/one) if(temperature_burns(burn_damage * seconds_per_tick) > 0 && SPT_PROB(10, seconds_per_tick) && !temperature_insulation && !on_fire) var/expected_temp = get_temperature(loc?.return_air()) var/insulation = get_insulation(expected_temp) if(insulation > 0.5 && expected_temp < body_temperature) - to_chat(src, span_danger("Your clothing is insulating you, keeping you warmer!")) + to_chat(src, span_danger("Your insulation is keeping you warmer!")) if(effective_temp > heat_threshold_medium) apply_status_effect(/datum/status_effect/stacking/heat_exposure, 1, heat_threshold_medium) @@ -128,20 +124,23 @@ var/cold_damage = COLD_DAMAGE if(body_temperature < cold_threshold_high) cold_damage *= 8 - // melbert todo : status effects should handle the damage, not the proc - apply_status_effect(/datum/status_effect/thermia/hypo/three) else if(body_temperature < cold_threshold_medium) cold_damage *= 4 - apply_status_effect(/datum/status_effect/thermia/hypo/two) else if(body_temperature < cold_threshold_low) cold_damage *= 2 - apply_status_effect(/datum/status_effect/thermia/hypo/one) if(temperature_cold_damage(cold_damage * seconds_per_tick) > 0 && SPT_PROB(10, seconds_per_tick) && !temperature_insulation) var/expected_temp = get_temperature(loc?.return_air()) var/insulation = get_insulation(expected_temp) if(insulation > 0.5 && expected_temp > body_temperature) - to_chat(src, span_danger("Your clothing is insulating you, keeping you colder!")) + to_chat(src, span_danger("You insulation is keeping you cooler!")) + +/mob/living/carbon/body_temperature_damage(datum/gas_mixture/environment, seconds_per_tick, times_fired) + if(body_temperature > bodytemp_heat_damage_limit && !HAS_TRAIT(src, TRAIT_RESISTHEAT)) + apply_status_effect(/datum/status_effect/thermia/hyper) + + if(body_temperature < bodytemp_cold_damage_limit && !HAS_TRAIT(src, TRAIT_RESISTCOLD)) + apply_status_effect(/datum/status_effect/thermia/hypo) /// Applies damage to the mob due to being too cold /mob/living/proc/temperature_cold_damage(damage) diff --git a/code/modules/mob/living/simple_animal/hostile/megafauna/hierophant.dm b/code/modules/mob/living/simple_animal/hostile/megafauna/hierophant.dm index 7883a668a4db..995ac3055a10 100644 --- a/code/modules/mob/living/simple_animal/hostile/megafauna/hierophant.dm +++ b/code/modules/mob/living/simple_animal/hostile/megafauna/hierophant.dm @@ -501,6 +501,9 @@ Difficulty: Hard burst_range = initial(burst_range) + round(anger_modifier * 0.08) beam_range = initial(beam_range) + round(anger_modifier * 0.12) +/mob/living/simple_animal/hostile/megafauna/hierophant/get_blood_type() + return null + //Hierophant overlays /obj/effect/temp_visual/hierophant name = "vortex energy" diff --git a/code/modules/mob/mob.dm b/code/modules/mob/mob.dm index 2dc8a6040ca9..183b09d8f4a9 100644 --- a/code/modules/mob/mob.dm +++ b/code/modules/mob/mob.dm @@ -1544,7 +1544,7 @@ /mob/living/carbon/set_nutrition(set_to, forced) . = ..() var/hungermod = (HAS_TRAIT(src, TRAIT_NOHUNGER) || nutrition > NUTRITION_LEVEL_HUNGRY) ? 0 : (-20 * (1 - (nutrition / NUTRITION_LEVEL_HUNGRY))) - add_consciousness_modifier(HUNGER, hungermod) + add_consciousness_modifier(HUNGER, round(hungermod, 0.01)) /mob/proc/update_equipment_speed_mods() var/speedies = equipped_speed_mods() diff --git a/code/modules/reagents/chemistry/reagents.dm b/code/modules/reagents/chemistry/reagents.dm index 60c3f57f8392..a32b3c479691 100644 --- a/code/modules/reagents/chemistry/reagents.dm +++ b/code/modules/reagents/chemistry/reagents.dm @@ -176,7 +176,7 @@ /// Called in burns.dm *if* the reagent has the REAGENT_AFFECTS_WOUNDS process flag -/datum/reagent/proc/on_burn_wound_processing(datum/wound/burn/flesh/burn_wound) +/datum/reagent/proc/on_burn_wound_processing(datum/wound/flesh/burn_wound) return /* diff --git a/code/modules/reagents/chemistry/reagents/drinks/alcohol_reagents.dm b/code/modules/reagents/chemistry/reagents/drinks/alcohol_reagents.dm index 67bb809a54b5..181556759053 100644 --- a/code/modules/reagents/chemistry/reagents/drinks/alcohol_reagents.dm +++ b/code/modules/reagents/chemistry/reagents/drinks/alcohol_reagents.dm @@ -105,7 +105,7 @@ return exposed_mob.adjust_fire_stacks(reac_volume / 15) - exposed_mob.add_timed_surgery_speed_mod(type, clamp(round(20 / (15 + sqrt(max(1, boozepwr))), 0.01), 0.25, 1.25), reac_volume * 1 MINUTES) + exposed_mob.add_timed_surgery_speed_mod(type, clamp(round(20 / (15 + sqrt(max(1, boozepwr))), 0.01), 0.25, 1.25), min(reac_volume * 1 MINUTES, 5 MINUTES)) /datum/reagent/consumable/ethanol/beer name = "Beer" diff --git a/code/modules/reagents/chemistry/reagents/drinks/drink_reagents.dm b/code/modules/reagents/chemistry/reagents/drinks/drink_reagents.dm index 62b313e6f028..70049c5cfc29 100644 --- a/code/modules/reagents/chemistry/reagents/drinks/drink_reagents.dm +++ b/code/modules/reagents/chemistry/reagents/drinks/drink_reagents.dm @@ -335,7 +335,7 @@ return TRUE // There's a designated burn process, but I felt this would be better for consistency with the rest of the reagent's procs -/datum/wound/burn/flesh/tea_life_process() +/datum/wound/flesh/burn/tea_life_process() // Sanitizes and heals, but with a limit flesh_healing = (flesh_healing > 0.1) ? flesh_healing : flesh_healing + 0.02 infestation_rate = max(infestation_rate - 0.005, 0) diff --git a/code/modules/reagents/chemistry/reagents/food_reagents.dm b/code/modules/reagents/chemistry/reagents/food_reagents.dm index b364f819ec52..cf9ae1279e98 100644 --- a/code/modules/reagents/chemistry/reagents/food_reagents.dm +++ b/code/modules/reagents/chemistry/reagents/food_reagents.dm @@ -492,7 +492,7 @@ clot_rate *= 0.75 to_chat(carbies, span_notice("The salt bits seep in and stick to [lowertext(src)], painfully irritating the skin but soaking up most of the blood.")) -/datum/wound/burn/flesh/on_salt(reac_volume) +/datum/wound/flesh/burn/on_salt(reac_volume) // Slightly sanitizes and disinfects, but also increases infestation rate (some bacteria are aided by salt), and decreases flesh healing (can damage the skin from moisture absorption) sanitization += VALUE_PER(0.4, 30) * reac_volume infestation -= max(VALUE_PER(0.3, 30) * reac_volume, 0) @@ -666,7 +666,7 @@ // Don't pour flour onto burn wounds, it increases infection risk! Very unwise. Backed up by REAL info from REAL professionals. // https://www.reuters.com/article/uk-factcheck-flour-burn-idUSKCN26F2N3 -/datum/wound/burn/flesh/on_flour(reac_volume) +/datum/wound/flesh/burn/on_flour(reac_volume) to_chat(victim, span_notice("The flour seeps into [lowertext(src)], spiking you with intense pain! That probably wasn't a good idea...")) sanitization -= min(0, 1) infestation += 0.2 @@ -772,7 +772,7 @@ // When some nerd adds infection for wounds, make this increase the infection return -/datum/wound/burn/flesh/on_starch(reac_volume, mob/living/carbon/carbies) +/datum/wound/flesh/burn/on_starch(reac_volume, mob/living/carbon/carbies) to_chat(carbies, span_notice("The slimey starch seeps into [lowertext(src)], spiking you with intense pain! That probably wasn't a good idea...")) sanitization -= min(0, 0.5) infestation += 0.1 @@ -826,7 +826,7 @@ if(!(methods & (TOUCH|VAPOR|PATCH))) return - exposed_mob.add_timed_surgery_speed_mod(type, 0.4, reac_volume * 1 MINUTES) + exposed_mob.add_timed_surgery_speed_mod(type, 0.4, min(reac_volume * 1 MINUTES, 5 MINUTES)) /datum/reagent/consumable/mayonnaise name = "Mayonnaise" diff --git a/code/modules/reagents/chemistry/reagents/medicine_reagents.dm b/code/modules/reagents/chemistry/reagents/medicine_reagents.dm index 61cadbc8d956..38157202c7ba 100644 --- a/code/modules/reagents/chemistry/reagents/medicine_reagents.dm +++ b/code/modules/reagents/chemistry/reagents/medicine_reagents.dm @@ -388,7 +388,7 @@ to_chat(exposed_mob, span_warning("Your stomach feels empty and cramps!")) if(methods & (PATCH|TOUCH)) - exposed_mob.add_timed_surgery_speed_mod(type, 0.9, reac_volume * 1 MINUTES) + exposed_mob.add_timed_surgery_speed_mod(type, 0.9, min(reac_volume * 1 MINUTES, 5 MINUTES)) if(show_message) to_chat(exposed_mob, span_danger("You feel your injuries fade away to nothing!") ) @@ -400,7 +400,7 @@ . = ..() affected_mob.remove_status_effect(/datum/status_effect/grouped/screwy_hud/fake_healthy, type) -/datum/reagent/medicine/mine_salve/on_burn_wound_processing(datum/wound/burn/flesh/burn_wound) +/datum/reagent/medicine/mine_salve/on_burn_wound_processing(datum/wound/flesh/burn_wound) burn_wound.sanitization += 0.3 burn_wound.flesh_healing += 0.5 @@ -1203,7 +1203,7 @@ . = ..() if(!(methods & (TOUCH|VAPOR|PATCH))) return - exposed_mob.add_timed_surgery_speed_mod(type, 1.1, reac_volume * 1 MINUTES) + exposed_mob.add_timed_surgery_speed_mod(type, 1.1, min(reac_volume * 1 MINUTES, 5 MINUTES)) /datum/reagent/medicine/stimulants name = "Stimulants" diff --git a/code/modules/reagents/chemistry/reagents/other_reagents.dm b/code/modules/reagents/chemistry/reagents/other_reagents.dm index 5c244c1a2b5b..a41f59c89deb 100644 --- a/code/modules/reagents/chemistry/reagents/other_reagents.dm +++ b/code/modules/reagents/chemistry/reagents/other_reagents.dm @@ -348,7 +348,7 @@ adjust_blood_flow(-0.1 * reac_volume, initial_flow * 0.5) to_chat(carbies, span_notice("The salt water splashes over [lowertext(src)], soaking up the blood.")) -/datum/wound/burn/flesh/on_saltwater(reac_volume) +/datum/wound/flesh/burn/on_saltwater(reac_volume) // Similar but better stats from normal salt. sanitization += VALUE_PER(0.6, 30) * reac_volume infestation -= max(VALUE_PER(0.5, 30) * reac_volume, 0) @@ -1177,9 +1177,9 @@ . = ..() if(!(methods & (TOUCH|VAPOR|PATCH))) return - exposed_mob.add_timed_surgery_speed_mod(type, 0.8, reac_volume * 1 MINUTES) + exposed_mob.add_timed_surgery_speed_mod(type, 0.8, min(reac_volume * 1 MINUTES, 5 MINUTES)) -/datum/reagent/space_cleaner/sterilizine/on_burn_wound_processing(datum/wound/burn/flesh/burn_wound) +/datum/reagent/space_cleaner/sterilizine/on_burn_wound_processing(datum/wound/flesh/burn_wound) burn_wound.sanitization += 0.9 /datum/reagent/iron @@ -1379,7 +1379,7 @@ if(methods & (TOUCH|VAPOR)) exposed_mob.wash(clean_types) -/datum/reagent/space_cleaner/on_burn_wound_processing(datum/wound/burn/flesh/burn_wound) +/datum/reagent/space_cleaner/on_burn_wound_processing(datum/wound/flesh/burn_wound) burn_wound.sanitization += 0.3 if(prob(5)) to_chat(burn_wound.victim, span_notice("Your [burn_wound] stings and burns from the [src] covering it! It does look pretty clean though.")) diff --git a/code/modules/reagents/chemistry/reagents/toxin_reagents.dm b/code/modules/reagents/chemistry/reagents/toxin_reagents.dm index a5cea421f3e5..e7e50c4b765d 100644 --- a/code/modules/reagents/chemistry/reagents/toxin_reagents.dm +++ b/code/modules/reagents/chemistry/reagents/toxin_reagents.dm @@ -1396,7 +1396,7 @@ if(current_cycle > 38 && !length(traits_not_applied) && SPT_PROB(5, seconds_per_tick) && !affected_mob.undergoing_cardiac_arrest()) affected_mob.set_heartattack(TRUE) - to_chat(affected_mob, span_danger("you feel a burning pain spread throughout your chest, oh no...")) + to_chat(affected_mob, span_danger("You feel a burning pain spread throughout your chest, oh no...")) if(need_mob_update) return UPDATE_MOB_HEALTH diff --git a/code/modules/reagents/reagent_containers/condiment.dm b/code/modules/reagents/reagent_containers/condiment.dm index bbd0a84504fa..7cc842275ea2 100644 --- a/code/modules/reagents/reagent_containers/condiment.dm +++ b/code/modules/reagents/reagent_containers/condiment.dm @@ -286,6 +286,8 @@ icon_state = "oliveoil" list_reagents = list(/datum/reagent/consumable/nutriment/fat/oil/olive = 50) fill_icon_thresholds = null + drop_sound = 'maplestation_modules/sound/items/drop/bottle.ogg' + pickup_sound = 'maplestation_modules/sound/items/pickup/bottle.ogg' /obj/item/reagent_containers/condiment/yoghurt name = "yoghurt carton" diff --git a/code/modules/religion/sparring/sparring_datum.dm b/code/modules/religion/sparring/sparring_datum.dm index bfaa94d65a8a..2d9e37878567 100644 --- a/code/modules/religion/sparring/sparring_datum.dm +++ b/code/modules/religion/sparring/sparring_datum.dm @@ -216,7 +216,7 @@ var/mob/living/carbon/human/branded = interfering to_chat(interfering, span_warning("[GLOB.deity] brands your flesh for interfering with [chaplain]'s sparring match!!")) var/obj/item/bodypart/branded_limb = pick(branded.bodyparts) - branded_limb.force_wound_upwards(/datum/wound/burn/flesh/severe/brand, wound_source = "divine intervention") + branded_limb.force_wound_upwards(/datum/wound/flesh/burn/severe/brand, wound_source = "divine intervention") branded.emote("scream") flubs-- diff --git a/code/modules/research/designs/autolathe/medsci_designs.dm b/code/modules/research/designs/autolathe/medsci_designs.dm index e0b03384044b..3c72b53fd569 100644 --- a/code/modules/research/designs/autolathe/medsci_designs.dm +++ b/code/modules/research/designs/autolathe/medsci_designs.dm @@ -215,10 +215,28 @@ name = "Hemostat" id = "hemostat" build_type = AUTOLATHE | PROTOLATHE | AWAY_LATHE - materials = list(/datum/material/iron =HALF_SHEET_MATERIAL_AMOUNT * 5, /datum/material/glass = SHEET_MATERIAL_AMOUNT * 1.25) + materials = list(/datum/material/iron = SHEET_MATERIAL_AMOUNT * 2.5, /datum/material/glass = SHEET_MATERIAL_AMOUNT * 1.25) build_path = /obj/item/hemostat category = list( RND_CATEGORY_INITIAL, RND_CATEGORY_TOOLS + RND_SUBCATEGORY_TOOLS_MEDICAL, ) departmental_flags = DEPARTMENT_BITFLAG_MEDICAL | DEPARTMENT_BITFLAG_SCIENCE + +/datum/design/medical_scissors + name = "Medical Scissors" + id = "medical_scissors" + build_type = AUTOLATHE | PROTOLATHE | AWAY_LATHE + materials = list(/datum/material/iron = SHEET_MATERIAL_AMOUNT * 2.5, /datum/material/glass = HALF_SHEET_MATERIAL_AMOUNT) + build_path = /obj/item/razor/scissors/medical + category = list( + RND_CATEGORY_INITIAL, + RND_CATEGORY_TOOLS + RND_SUBCATEGORY_TOOLS_MEDICAL, + ) + departmental_flags = DEPARTMENT_BITFLAG_MEDICAL | DEPARTMENT_BITFLAG_SCIENCE + +/datum/design/medical_scissors/trauma + name = "Trauma Scissors" + id = "medical_scissors_trauma" + materials = list(/datum/material/iron = SHEET_MATERIAL_AMOUNT * 5, /datum/material/glass = SHEET_MATERIAL_AMOUNT) + build_path = /obj/item/razor/scissors/medical/trauma diff --git a/code/modules/research/designs/autolathe/service_designs.dm b/code/modules/research/designs/autolathe/service_designs.dm index 807b3db750ca..a37ce646cab3 100644 --- a/code/modules/research/designs/autolathe/service_designs.dm +++ b/code/modules/research/designs/autolathe/service_designs.dm @@ -606,3 +606,15 @@ RND_CATEGORY_TOOLS + RND_SUBCATEGORY_TOOLS_SERVICE, ) departmental_flags = DEPARTMENT_BITFLAG_SERVICE + +/datum/design/barber_scissors + name = "Barber's Scissors" + id = "barber_scissors" + build_type = AUTOLATHE | PROTOLATHE + materials = list(/datum/material/iron = SHEET_MATERIAL_AMOUNT * 2.5, /datum/material/glass = HALF_SHEET_MATERIAL_AMOUNT) + build_path = /obj/item/razor/scissors/barber + category = list( + RND_CATEGORY_INITIAL, + RND_CATEGORY_TOOLS + RND_SUBCATEGORY_TOOLS_SERVICE, + ) + departmental_flags = DEPARTMENT_BITFLAG_SERVICE diff --git a/code/modules/research/techweb/nodes/medbay_nodes.dm b/code/modules/research/techweb/nodes/medbay_nodes.dm index 9c2138c11374..c3085549ca07 100644 --- a/code/modules/research/techweb/nodes/medbay_nodes.dm +++ b/code/modules/research/techweb/nodes/medbay_nodes.dm @@ -29,6 +29,7 @@ "dropper", "pillbottle", "xlarge_beaker", + "medical_scissors", ) experiments_to_unlock = list( /datum/experiment/autopsy/human, @@ -77,6 +78,7 @@ "defibmount", "medicalbed_emergency", "piercesyringe", + "medical_scissors_trauma", ) research_costs = list(TECHWEB_POINT_TYPE_GENERIC = TECHWEB_TIER_3_POINTS) discount_experiments = list(/datum/experiment/scanning/reagent/haloperidol = TECHWEB_TIER_3_POINTS) diff --git a/code/modules/research/techweb/nodes/service_nodes.dm b/code/modules/research/techweb/nodes/service_nodes.dm index 898815aae763..89d086e6bec0 100644 --- a/code/modules/research/techweb/nodes/service_nodes.dm +++ b/code/modules/research/techweb/nodes/service_nodes.dm @@ -33,6 +33,7 @@ "normtrash", "wirebrush", "flashlight", + "barber_scissors", ) /datum/techweb_node/sanitation diff --git a/code/modules/surgery/bodyparts/_bodyparts.dm b/code/modules/surgery/bodyparts/_bodyparts.dm index b49be1edb794..87bb7fe5993a 100644 --- a/code/modules/surgery/bodyparts/_bodyparts.dm +++ b/code/modules/surgery/bodyparts/_bodyparts.dm @@ -458,23 +458,24 @@ * called when a bodypart is taking damage * Damage will not exceed max_damage using this proc, and negative damage cannot be used to heal * Returns TRUE if damage icon states changes + * * Args: - * brute - The amount of brute damage dealt. - * burn - The amount of burn damage dealt. - * blocked - The amount of damage blocked by armor. - * update_health - Whether to update the owner's health from receiving the hit. - * required_bodytype - A bodytype flag requirement to get this damage (ex: BODYTYPE_ORGANIC) - * wound_bonus - Additional bonus chance to get a wound. - * bare_wound_bonus - Additional bonus chance to get a wound if the bodypart is naked. - * sharpness - Flag on whether the attack is edged or pointy - * attack_direction - The direction the bodypart is attacked from, used to send blood flying in the opposite direction. - * damage_source - The source of damage, typically a weapon. + * * brute - The amount of brute damage dealt. + * * burn - The amount of burn damage dealt. + * * blocked - The amount of damage blocked by armor. + * * update_health - Whether to update the owner's health from receiving the hit. + * * required_bodytype - A bodytype flag requirement to get this damage (ex: BODYTYPE_ORGANIC) + * * wound_bonus - Additional bonus chance to get a wound. + * * bare_wound_bonus - Additional bonus chance to get a wound if the bodypart is naked. + * * sharpness - Flag on whether the attack is edged or pointy + * * attack_direction - The direction the bodypart is attacked from, used to send blood flying in the opposite direction. + * * damage_source - The source of damage, typically a weapon. */ /obj/item/bodypart/proc/receive_damage(brute = 0, burn = 0, blocked = 0, updating_health = TRUE, forced = FALSE, required_bodytype = null, wound_bonus = 0, bare_wound_bonus = 0, sharpness = NONE, attack_direction = null, damage_source) SHOULD_CALL_PARENT(TRUE) var/hit_percent = forced ? 1 : (100-blocked)/100 - if((!brute && !burn) || hit_percent <= 0) + if((brute <= 0 && burn <= 0) || hit_percent <= 0) return FALSE if (!forced) if(!isnull(owner)) @@ -488,7 +489,7 @@ brute = round(max(brute * hit_percent * brute_modifier, 0), DAMAGE_PRECISION) burn = round(max(burn * hit_percent * burn_modifier, 0), DAMAGE_PRECISION) - if(!brute && !burn) + if(brute <= 0 && burn <= 0) return FALSE brute *= wound_damage_multiplier @@ -511,22 +512,27 @@ var/can_inflict = max_damage - get_damage() var/total_damage = brute + burn if(total_damage > can_inflict && total_damage > 0) // TODO: the second part of this check should be removed once disabling is all done - brute = round(brute * (can_inflict / total_damage),DAMAGE_PRECISION) - burn = round(burn * (can_inflict / total_damage),DAMAGE_PRECISION) - - if(can_inflict <= 0) + var/new_brute = round(brute * (can_inflict / total_damage), DAMAGE_PRECISION) + var/new_burn = round(burn * (can_inflict / total_damage), DAMAGE_PRECISION) + // going over the limb limit causes direct shock + owner?.adjust_traumatic_shock(max(0.5, (brute + burn - new_brute - new_burn) * 0.5)) + // and now update these values to reflect the new damage + brute = new_brute + burn = new_burn + total_damage = brute + burn + + // if we're not doing any damage (now), we don't need to do anything else + if(total_damage <= 0) return FALSE if(brute) set_brute_dam(brute_dam + brute) if(burn) set_burn_dam(burn_dam + burn) - - if(owner) - if(can_be_disabled) - update_disabled() - if(updating_health) - owner.updatehealth() - return update_bodypart_damage_state() || . + if(can_be_disabled) + update_disabled() + if(updating_health) + owner?.updatehealth() + return update_bodypart_damage_state() /// Returns a bitflag using ANATOMY_EXTERIOR or ANATOMY_INTERIOR. Used to determine if we as a whole have a interior or exterior biostate, or both. /obj/item/bodypart/proc/get_bio_state_status() diff --git a/code/modules/surgery/burn_dressing.dm b/code/modules/surgery/burn_dressing.dm index ca1cf0012ba8..ccba2ee1cc1e 100644 --- a/code/modules/surgery/burn_dressing.dm +++ b/code/modules/surgery/burn_dressing.dm @@ -3,9 +3,9 @@ ///// Debride burnt flesh /datum/surgery/debride - name = "Debride burnt flesh" + name = "Debride infected flesh" surgery_flags = SURGERY_REQUIRE_RESTING | SURGERY_REQUIRE_LIMB | SURGERY_REQUIRES_REAL_LIMB - targetable_wound = /datum/wound/burn/flesh + targetable_wound = /datum/wound/flesh possible_locs = list( BODY_ZONE_R_ARM, BODY_ZONE_L_ARM, @@ -19,7 +19,7 @@ /datum/surgery_step/dress, ) -/datum/surgery/debride/is_valid_wound(datum/wound/burn/flesh/wound) +/datum/surgery/debride/is_valid_wound(datum/wound/flesh/wound) return ..() && wound.infestation > 0 //SURGERY STEPS @@ -43,7 +43,7 @@ var/infestation_removed = 4 /// To give the surgeon a heads up how much work they have ahead of them -/datum/surgery_step/debride/proc/get_progress(mob/user, mob/living/carbon/target, datum/wound/burn/flesh/burn_wound) +/datum/surgery_step/debride/proc/get_progress(mob/user, mob/living/carbon/target, datum/wound/flesh/burn_wound) if(!burn_wound?.infestation || !infestation_removed) return var/estimated_remaining_steps = burn_wound.infestation / infestation_removed @@ -63,7 +63,7 @@ /datum/surgery_step/debride/preop(mob/user, mob/living/carbon/target, target_zone, obj/item/tool, datum/surgery/surgery) if(surgery.operated_wound) - var/datum/wound/burn/flesh/burn_wound = surgery.operated_wound + var/datum/wound/flesh/burn_wound = surgery.operated_wound if(burn_wound.infestation <= 0) to_chat(user, span_notice("[target]'s [parse_zone(target_zone)] has no infected flesh to remove!")) surgery.status++ @@ -87,7 +87,7 @@ user.visible_message(span_notice("[user] looks for [target]'s [parse_zone(target_zone)]."), span_notice("You look for [target]'s [parse_zone(target_zone)]...")) /datum/surgery_step/debride/success(mob/living/user, mob/living/carbon/target, target_zone, obj/item/tool, datum/surgery/surgery, default_display_results = FALSE) - var/datum/wound/burn/flesh/burn_wound = surgery.operated_wound + var/datum/wound/flesh/burn_wound = surgery.operated_wound if(burn_wound) var/progress_text = get_progress(user, target, burn_wound) display_results( @@ -121,14 +121,14 @@ /datum/surgery_step/debride/initiate(mob/user, mob/living/carbon/target, target_zone, obj/item/tool, datum/surgery/surgery, try_to_fail = FALSE) if(!..()) return - var/datum/wound/burn/flesh/burn_wound = surgery.operated_wound + var/datum/wound/flesh/burn/burn_wound = surgery.operated_wound while(burn_wound && burn_wound.infestation > 0.25) if(!..()) break ///// Dressing burns /datum/surgery_step/dress - name = "bandage burns (gauze/tape)" + name = "bandage flesh (gauze/tape)" implements = list( /obj/item/stack/medical/gauze = 100, /obj/item/stack/sticky_tape/surgical = 100) @@ -140,14 +140,14 @@ /datum/surgery_step/dress/preop(mob/user, mob/living/carbon/target, target_zone, obj/item/tool, datum/surgery/surgery) - var/datum/wound/burn/flesh/burn_wound = surgery.operated_wound + var/datum/wound/flesh/burn_wound = surgery.operated_wound if(burn_wound) display_results( user, target, - span_notice("You begin to dress the burns on [target]'s [parse_zone(target_zone)]..."), - span_notice("[user] begins to dress the burns on [target]'s [parse_zone(target_zone)] with [tool]."), - span_notice("[user] begins to dress the burns on [target]'s [parse_zone(target_zone)]."), + span_notice("You begin to dress the flesh on [target]'s [parse_zone(target_zone)]..."), + span_notice("[user] begins to dress the flesh on [target]'s [parse_zone(target_zone)] with [tool]."), + span_notice("[user] begins to dress the flesh on [target]'s [parse_zone(target_zone)]."), ) display_pain( target = target, @@ -158,7 +158,7 @@ user.visible_message(span_notice("[user] looks for [target]'s [parse_zone(target_zone)]."), span_notice("You look for [target]'s [parse_zone(target_zone)]...")) /datum/surgery_step/dress/success(mob/living/user, mob/living/carbon/target, target_zone, obj/item/tool, datum/surgery/surgery, default_display_results = FALSE) - var/datum/wound/burn/flesh/burn_wound = surgery.operated_wound + var/datum/wound/flesh/burn_wound = surgery.operated_wound if(burn_wound) display_results( user, @@ -167,13 +167,13 @@ span_notice("[user] successfully wraps [target]'s [parse_zone(target_zone)] with [tool]!"), span_notice("[user] successfully wraps [target]'s [parse_zone(target_zone)]!"), ) - log_combat(user, target, "dressed burns in", addition="COMBAT MODE: [uppertext(user.combat_mode)]") + log_combat(user, target, "dressed flesh in", addition="COMBAT MODE: [uppertext(user.combat_mode)]") burn_wound.sanitization += sanitization_added burn_wound.flesh_healing += flesh_healing_added var/obj/item/bodypart/the_part = target.get_bodypart(target_zone) the_part.apply_gauze(tool) else - to_chat(user, span_warning("[target] has no burns there!")) + to_chat(user, span_warning("[target] has no flesh wounds there!")) return ..() /datum/surgery_step/dress/failure(mob/user, mob/living/target, target_zone, obj/item/tool, datum/surgery/surgery, fail_prob = 0) diff --git a/code/modules/surgery/organs/_organ.dm b/code/modules/surgery/organs/_organ.dm index 3f39bdbb4492..3c016e6d1898 100644 --- a/code/modules/surgery/organs/_organ.dm +++ b/code/modules/surgery/organs/_organ.dm @@ -317,26 +317,36 @@ INITIALIZE_IMMEDIATE(/obj/item/organ) /// Called by medical scanners to get a simple summary of how healthy the organ is. Returns an empty string if things are fine. /obj/item/organ/proc/get_status_text(advanced, add_tooltips) if(advanced && (organ_flags & ORGAN_HAZARDOUS)) - return conditional_tooltip("Harmful Foreign Body", "Remove surgically.", add_tooltips) + return conditional_tooltip("Harmful Foreign Body", \ + "Remove surgically.", add_tooltips) if(organ_flags & ORGAN_IRRADIATED) - return conditional_tooltip("Irradiated", "Replace or use specialty medication, such as [/datum/reagent/medicine/potass_iodide::name] or [/datum/reagent/medicine/pen_acid::name].", add_tooltips) + return conditional_tooltip("Irradiated", \ + "Replace or use specialty medication, such as [/datum/reagent/medicine/potass_iodide::name] or [/datum/reagent/medicine/pen_acid::name].", add_tooltips) if(organ_flags & ORGAN_EMP) - return conditional_tooltip("EMP-Derived Failure", "Repair or replace surgically.", add_tooltips) + return conditional_tooltip("EMP-Derived Failure", \ + "Repair or replace surgically.", add_tooltips) var/tech_text = "" if(owner.has_reagent(/datum/reagent/inverse/technetium)) tech_text = "[round((damage / maxHealth) * 100, 1)]% damaged" if(organ_flags & ORGAN_FAILING) - return conditional_tooltip("[tech_text || "Non-Functional"]", "Repair or replace surgically.", add_tooltips) + return conditional_tooltip("[tech_text || "Non-Functional"]", \ + "Repair or replace surgically.", add_tooltips) if(damage > high_threshold) - return conditional_tooltip("[tech_text || "Severely Damaged"]", "[healing_factor ? "Treat with rest or use specialty medication." : "Repair surgically or use specialty medication."]", add_tooltips && owner.stat != DEAD) + var/dead_tooltip = "Ignore until revived, but be careful not to cause further damage." + var/alive_tooltip = healing_factor ? "Treat with rest or use specialty medication." : "Repair surgically or use specialty medication." + return conditional_tooltip("[tech_text || "Severely Damaged"]", \ + "[owner.stat == DEAD ? dead_tooltip : alive_tooltip]", add_tooltips) if(damage > low_threshold) - return conditional_tooltip("[tech_text || "Mildly Damaged"] ", "[healing_factor ? "Treat with rest." : "Use specialty medication."]", add_tooltips && owner.stat != DEAD) + var/dead_tooltip = "Ignore until revived." + var/alive_tooltip = healing_factor ? "Treat with rest or use specialty medication." : "Repair surgically or use specialty medication." + return conditional_tooltip("[tech_text || "Mildly Damaged"] ", \ + "[owner.stat == DEAD ? dead_tooltip : alive_tooltip]", add_tooltips) if(tech_text) return "[tech_text]" diff --git a/code/modules/surgery/organs/internal/heart/_heart.dm b/code/modules/surgery/organs/internal/heart/_heart.dm index a92012bb46dd..862b9d9459e2 100644 --- a/code/modules/surgery/organs/internal/heart/_heart.dm +++ b/code/modules/surgery/organs/internal/heart/_heart.dm @@ -104,7 +104,7 @@ /obj/item/organ/internal/heart/get_status_text(advanced, add_tooltips) if(!beating && !(organ_flags & ORGAN_FAILING) && owner.needs_heart() && owner.stat != DEAD) - return conditional_tooltip("Cardiac Arrest", "Apply defibrillation immediately. Similar electric shocks may work in emergencies.", add_tooltips) + return conditional_tooltip("Cardiac Arrest", "Defibrillate immediately. Similar electric shocks may work in emergencies.", add_tooltips) return ..() /obj/item/organ/internal/heart/show_on_condensed_scans() @@ -131,15 +131,15 @@ var/heartrate = get_heart_rate() switch(heartrate) - if(1 to 6) + if(1 to SLOW_HEARTBEAT_THRESHOLD) if(playing_heartbeat_sfx != BEAT_SLOW) playing_heartbeat_sfx = BEAT_SLOW SEND_SOUND(owner, sound('sound/health/slowbeat.ogg', repeat = TRUE, channel = CHANNEL_HEARTBEAT, volume = 40)) - if(0, 6 to 11) + if(0, SLOW_HEARTBEAT_THRESHOLD to FAST_HEARTBEAT_THRESHOLD) if(playing_heartbeat_sfx != BEAT_NONE) playing_heartbeat_sfx = BEAT_NONE owner.stop_sound_channel(CHANNEL_HEARTBEAT) - if(11 to INFINITY) + if(FAST_HEARTBEAT_THRESHOLD to INFINITY) if(playing_heartbeat_sfx != BEAT_FAST) playing_heartbeat_sfx = BEAT_FAST SEND_SOUND(owner, sound('sound/health/fastbeat.ogg', repeat = TRUE, channel = CHANNEL_HEARTBEAT, volume = 40)) @@ -178,8 +178,11 @@ base_amount += round(owner.pain_controller?.get_total_pain() / 50, 0.5) base_amount += round(owner.pain_controller?.traumatic_shock / 25, 0.5) base_amount += round((BLOOD_VOLUME_NORMAL - owner.blood_volume) / 250, 0.5) - base_amount -= round((CONSCIOUSNESS_MAX - owner.consciousness) / 25, 0.5) - var/damage_multiplier = clamp(1.5 * ((maxHealth - damage) / maxHealth), 0.5, 1) + base_amount -= round((CONSCIOUSNESS_MAX - owner.consciousness) / 33, 0.5) + if(ishuman(owner)) + var/mob/living/carbon/human/human_owner = owner + base_amount += round((human_owner.sprint_length_max - human_owner.sprint_length) / human_owner.sprint_length_max, 0.1) + var/damage_multiplier = clamp(2.5 * ((maxHealth - damage) / maxHealth), 0.5, 1) return clamp(round(base_amount * damage_multiplier, 0.5), 1, 100) diff --git a/code/modules/surgery/organs/internal/lungs/_lungs.dm b/code/modules/surgery/organs/internal/lungs/_lungs.dm index a85f727d5e39..3add69de08dc 100644 --- a/code/modules/surgery/organs/internal/lungs/_lungs.dm +++ b/code/modules/surgery/organs/internal/lungs/_lungs.dm @@ -44,7 +44,7 @@ //Breath damage //These thresholds are checked against what amounts to total_mix_pressure * (gas_type_mols/total_mols) var/safe_oxygen_min = 16 // Minimum safe partial pressure of O2, in kPa - var/safe_oxygen_max = 0 + var/safe_oxygen_max = ONE_ATMOSPHERE * 1.25 var/safe_nitro_min = 0 var/safe_co2_max = 10 // Yes it's an arbitrary value who cares? var/safe_plasma_min = 0 @@ -109,6 +109,12 @@ /// Type of damage dealt by a hot breath var/heat_damage_type = BURN + /// Below this threshold, the mob lungs take damage due to the pressure difference + /// This is guaranteed to be lower than the lowest breathable gas minimum + var/low_pressure_threshold = ONE_ATMOSPHERE * 0.12 + /// Above this threshold, the mob lungs take damage due to the pressure difference + var/high_pressure_threshold = ONE_ATMOSPHERE * 2.4 + // assign the respiration_type /obj/item/organ/internal/lungs/Initialize(mapload) . = ..() @@ -121,6 +127,10 @@ if(safe_plasma_min) respiration_type |= RESPIRATION_PLASMA + // Always ensures our low_pressure_threshold is less than our smallest (non-zero) breathable gas minimum + var/lowest_nonzero_min = min(safe_oxygen_min || INFINITY, safe_nitro_min || INFINITY, safe_plasma_min || INFINITY) + low_pressure_threshold = (lowest_nonzero_min == INFINITY) ? 0 : clamp(lowest_nonzero_min - 1, 0, low_pressure_threshold) + // Sets up what gases we want to react to, and in what way // always is always processed, while_present is called when the gas is in the breath, and on_loss is called right after a gas is lost // The naming convention goes like this @@ -248,7 +258,7 @@ /// Handles oxygen breathing. Always called by things that need o2, no matter what /obj/item/organ/internal/lungs/proc/breathe_oxygen(mob/living/carbon/breather, datum/gas_mixture/breath, o2_pp, old_o2_pp) - if(o2_pp < safe_oxygen_min && !HAS_TRAIT(src, TRAIT_SPACEBREATHING)) + if(o2_pp < safe_oxygen_min) // Not safe to check the old pp because of can_breath_vacuum breather.throw_alert(ALERT_NOT_ENOUGH_OXYGEN, /atom/movable/screen/alert/not_enough_oxy) @@ -264,11 +274,7 @@ breather.clear_alert(ALERT_NOT_ENOUGH_OXYGEN) breathe_gas_volume(breath, /datum/gas/oxygen, /datum/gas/carbon_dioxide) - if(HAS_TRAIT(breather, TRAIT_NOBLOOD)) - breather.adjustOxyLoss(-4) - else - // Less blood so breaths give you less oxygen - breather.adjustOxyLoss(-1 * min(5, BLOOD_VOLUME_NORMAL / breather.blood_volume)) + heal_oxyloss_on_breath(breather, breath) /// Maximum Oxygen effects. "Too much O2!" /obj/item/organ/internal/lungs/proc/too_much_oxygen(mob/living/carbon/breather, datum/gas_mixture/breath, o2_pp, old_o2_pp) @@ -296,7 +302,7 @@ /// If the lungs need Nitrogen to breathe properly, N2 is exchanged with CO2. /obj/item/organ/internal/lungs/proc/breathe_nitro(mob/living/carbon/breather, datum/gas_mixture/breath, nitro_pp, old_nitro_pp) - if(nitro_pp < safe_nitro_min && !HAS_TRAIT(src, TRAIT_SPACEBREATHING)) + if(nitro_pp < safe_nitro_min) // Suffocation side-effects. // Not safe to check the old pp because of can_breath_vacuum breather.throw_alert(ALERT_NOT_ENOUGH_NITRO, /atom/movable/screen/alert/not_enough_nitro) @@ -311,11 +317,7 @@ // Inhale N2, exhale equivalent amount of CO2. Look ma, sideways breathing! breathe_gas_volume(breath, /datum/gas/nitrogen, /datum/gas/carbon_dioxide) - if(HAS_TRAIT(breather, TRAIT_NOBLOOD)) - breather.adjustOxyLoss(-4) - else - // Less blood so breaths give you less oxygen - breather.adjustOxyLoss(-1 * min(5, BLOOD_VOLUME_NORMAL / breather.blood_volume)) + heal_oxyloss_on_breath(breather, breath) /// Maximum CO2 effects. "Too much CO2!" /obj/item/organ/internal/lungs/proc/too_much_co2(mob/living/carbon/breather, datum/gas_mixture/breath, co2_pp, old_co2_pp) @@ -351,7 +353,7 @@ /// If the lungs need Plasma to breathe properly, Plasma is exchanged with CO2. /obj/item/organ/internal/lungs/proc/breathe_plasma(mob/living/carbon/breather, datum/gas_mixture/breath, plasma_pp, old_plasma_pp) // Suffocation side-effects. - if(plasma_pp < safe_plasma_min && !HAS_TRAIT(src, TRAIT_SPACEBREATHING)) + if(plasma_pp < safe_plasma_min) // Could check old_plasma_pp but vacuum breathing hates me breather.throw_alert(ALERT_NOT_ENOUGH_PLASMA, /atom/movable/screen/alert/not_enough_plas) // Breathe insufficient amount of Plasma, exhale CO2. @@ -365,11 +367,7 @@ breather.clear_alert(ALERT_NOT_ENOUGH_PLASMA) // Inhale Plasma, exhale equivalent amount of CO2. breathe_gas_volume(breath, /datum/gas/plasma, /datum/gas/carbon_dioxide) - if(HAS_TRAIT(breather, TRAIT_NOBLOOD)) - breather.adjustOxyLoss(-4) - else - // Less blood so breaths give you less oxygen - breather.adjustOxyLoss(-1 * min(5, BLOOD_VOLUME_NORMAL / breather.blood_volume)) + heal_oxyloss_on_breath(breather, breath) /// Maximum Plasma effects. "Too much Plasma!" /obj/item/organ/internal/lungs/proc/too_much_plasma(mob/living/carbon/breather, datum/gas_mixture/breath, plasma_pp, old_plasma_pp) @@ -616,51 +614,46 @@ return FALSE // If the breath is null, it's actually a failed breath - var/no_breath = isnull(breath) || skip_breath - if(no_breath) + if(isnull(breath) || skip_breath) var/static/datum/gas_mixture/immutable/empty_breath = new(BREATH_VOLUME) breath = empty_breath - // Indicates if there are moles of gas in the breath. - var/num_moles = breath.total_moles() - var/not_low_pressure = num_moles > 0.01 || HAS_TRAIT(breather, TRAIT_RESISTLOWPRESSURE) - var/not_high_pressure = num_moles < 0.1 || HAS_TRAIT(breather, TRAIT_RESISTHIGHPRESSURE) - // Check for moles of gas and handle partial pressures / special conditions. - if(num_moles > 0 && not_low_pressure && not_high_pressure) - // Breath has more than 0 moles of gas. - // Route gases through mask filter if breather is wearing one. - if(istype(breather.wear_mask) && (breather.wear_mask.clothing_flags & GAS_FILTERING) && breather.wear_mask.has_filter) - breath = breather.wear_mask.consume_filter(breath) - // Breath has 0 moles of gas, and we can breathe space - else if(HAS_TRAIT(src, TRAIT_SPACEBREATHING)) - // The lungs can breathe anyways. What are you? Some bottom-feeding, scum-sucking algae eater? - breather.failed_last_breath = FALSE - // Vacuum-adapted lungs regenerate oxyloss even when breathing nothing. - if(HAS_TRAIT(breather, TRAIT_NOBLOOD)) - breather.adjustOxyLoss(-4) - else - // Less blood so breaths give you less oxygen - breather.adjustOxyLoss(-1 * min(5, BLOOD_VOLUME_NORMAL / breather.blood_volume)) - - // We're in a low / high pressure environment, can't breathe, but trying to, so this hurts the lungs - // Unless it's cybernetic then it just doesn't care. Handwave magic whatever - else if(!skip_breath && !IS_ROBOTIC_ORGAN(src)) - if(!failed) - // Lungs are poppin + // Breath has more than 0 moles of gas. + // Route gases through mask filter if breather is wearing one. + if(breath.total_moles() > 0 && istype(breather.wear_mask) && (breather.wear_mask.clothing_flags & GAS_FILTERING) && breather.wear_mask.has_filter) + breath = breather.wear_mask.consume_filter(breath) + + var/breath_pressure = breath.return_pressure() + var/too_low_pressure = breath_pressure < low_pressure_threshold && !HAS_TRAIT(breather, TRAIT_RESISTLOWPRESSURE) + var/too_high_pressure = breath_pressure > high_pressure_threshold && !HAS_TRAIT(breather, TRAIT_RESISTHIGHPRESSURE) + // Everything is fine + if(!too_low_pressure && !too_high_pressure) + breath = pre_breath_gas_handling(breather, breath) || breath + + // We are in a low pressure environment or we aren't breathing correctly + // We straight up failed to breathe as a consequence + else if(too_low_pressure || skip_breath) + if(!skip_breath && !IS_ROBOTIC_ORGAN(src) && !failed) if(damage >= 40 && damage <= 50 && CAN_FEEL_PAIN(breather)) to_chat(breather, span_userdanger("You feel a stabbing pain in your chest!")) - else if(num_moles < 0.02) + else to_chat(breather, span_boldwarning("You feel air rapidly exiting your lungs!")) - else if(num_moles > 0.1) - to_chat(breather, span_boldwarning("You feel air force itself into your lungs!")) - breather.cause_pain(BODY_ZONE_CHEST, 10, BRUTE) - apply_organ_damage(15) - breather.failed_last_breath = TRUE - // Robot, don't care lol - else + apply_organ_damage(maxHealth * 0.075) + breather.failed_last_breath = TRUE + // We are in a high pressure environment + // We technically didn't fail the breath, but it will still be very uncomfortable + else if(too_high_pressure) + if(!skip_breath && !IS_ROBOTIC_ORGAN(src) && !failed) + if(damage >= 40 && damage <= 50 && CAN_FEEL_PAIN(breather)) + to_chat(breather, span_userdanger("You feel a stabbing pain in your chest!")) + else + to_chat(breather, span_boldwarning("You feel air force itself into your lungs!")) + breather.cause_pain(BODY_ZONE_CHEST, 10, BRUTE) + apply_organ_damage(maxHealth * 0.075) + // The list of gases in the breath. var/list/breath_gases = breath.gases // Copy the breath's temperature into breath_out to avoid cooling the output breath down unfairly @@ -722,7 +715,7 @@ src.last_partial_pressures = partial_pressures - if(num_moles > 0) + if(breath.total_moles() + breath_out.total_moles() > 0) handle_breath_temperature(breath, breather) // Merge breath_out into breath. They're kept seprerate before now to ensure stupid like, order of operations shit doesn't happen // But that time has passed @@ -731,9 +724,7 @@ breath_out.garbage_collect() breath.garbage_collect() - // Returning FALSE indicates the breath failed. - if(!breather.failed_last_breath) - return TRUE + return !breather.failed_last_breath /// Remove gas from breath. If output_gas is given, transfers the removed gas to the lung's gas_mixture. /// Removes 100% of the given gas type unless given a volume argument. @@ -747,6 +738,19 @@ breath_out.gases[exchange_id][MOLES] += volume return volume +/// Handles what happens when we breathe in successfully, before we parse through the gases to determine specific side effects +/// Returns a gas mixture, or null (to use the original breath) +/obj/item/organ/internal/lungs/proc/pre_breath_gas_handling(mob/living/carbon/human/breather, datum/gas_mixture/breath) + return null + +/// Handles what happens when we breathe in something we need to live +/obj/item/organ/internal/lungs/proc/heal_oxyloss_on_breath(mob/living/carbon/human/breather, datum/gas_mixture/breath) + if(HAS_TRAIT(breather, TRAIT_NOBLOOD)) + breather.adjustOxyLoss(-4) + else + // Less blood so breaths give you less oxygen + breather.adjustOxyLoss(-1 * min(5, BLOOD_VOLUME_NORMAL / breather.blood_volume)) + /// Applies suffocation side-effects to a given Human, scaling based on ratio of required pressure VS "true" pressure. /// If pressure is greater than 0, the return value will represent the amount of gas successfully breathed. /obj/item/organ/internal/lungs/proc/handle_suffocation(mob/living/carbon/human/suffocator = null, breath_pp = 0, safe_breath_min = 0, mole_count = 0) diff --git a/code/modules/surgery/tools.dm b/code/modules/surgery/tools.dm index c596d042c225..0ee965824694 100644 --- a/code/modules/surgery/tools.dm +++ b/code/modules/surgery/tools.dm @@ -34,7 +34,7 @@ inhand_icon_state = "hemostat" lefthand_file = 'icons/mob/inhands/equipment/medical_lefthand.dmi' righthand_file = 'icons/mob/inhands/equipment/medical_righthand.dmi' - custom_materials = list(/datum/material/iron =SHEET_MATERIAL_AMOUNT * 2.5, /datum/material/glass = SHEET_MATERIAL_AMOUNT*1.25) + custom_materials = list(/datum/material/iron = SHEET_MATERIAL_AMOUNT * 2.5, /datum/material/glass = SHEET_MATERIAL_AMOUNT * 1.25) obj_flags = CONDUCTS_ELECTRICITY item_flags = SURGICAL_TOOL w_class = WEIGHT_CLASS_TINY @@ -699,3 +699,158 @@ /obj/item/scalpel/cruel/Initialize(mapload) . = ..() AddElement(/datum/element/bane, mob_biotypes = MOB_UNDEAD, damage_multiplier = 1) //Just in case one of the tennants get uppity + +// subtypes razors so we can cut hair. melbert todo : componentize haircutting behavior +/obj/item/razor/scissors + name = "scissors" + desc = "A pair of scissors. Used to cut paper, hair, or people." + icon_state = "scissors" + icon = 'maplestation_modules/icons/obj/surgery_tools.dmi' + inhand_icon_state = "hemostat" + lefthand_file = 'icons/mob/inhands/equipment/medical_lefthand.dmi' + righthand_file = 'icons/mob/inhands/equipment/medical_righthand.dmi' + custom_materials = list(/datum/material/iron = SHEET_MATERIAL_AMOUNT * 2.5, /datum/material/glass = HALF_SHEET_MATERIAL_AMOUNT) + attack_verb_continuous = list("attacks", "slashes", "stabs", "slices", "tears", "lacerates", "rips", "dices", "cuts") + attack_verb_simple = list("attack", "slash", "stab", "slice", "tear", "lacerate", "rip", "dice", "cut") + throw_range = 4 + throwforce = 6 + throw_speed = 1 + tool_behaviour = TOOL_WIRECUTTER // TOOL_SCISSORS // melbert todo : add this later when it's actually relevant + item_flags = SURGICAL_TOOL + obj_flags = CONDUCTS_ELECTRICITY + w_class = WEIGHT_CLASS_SMALL + toolspeed = 1.5 + force = 12 + sharpness = SHARP_EDGED + drop_sound = 'maplestation_modules/sound/items/drop/knife3.ogg' + pickup_sound = 'maplestation_modules/sound/items/pickup/surgery_metal.ogg' + hitsound = 'sound/weapons/bladeslice.ogg' // 'maplestation_modules/sound/items/snip.ogg' // melbert todo : maybe too funny. add a custom one? + shave_sound = 'maplestation_modules/sound/items/snip.ogg' + mob_throw_hit_sound = 'sound/weapons/pierce.ogg' + article = "a pair of" + +/obj/item/razor/scissors/Initialize(mapload) + . = ..() + register_item_context() + AddComponent(/datum/component/butchering, \ + speed = 10 SECONDS * toolspeed, \ + effectiveness = 100 * (1 / toolspeed), \ + bonus_modifier = 0, \ + ) + +/obj/item/razor/scissors/add_item_context(obj/item/source, list/context, atom/target, mob/living/user) + if(iscarbon(target)) + context[SCREENTIP_CONTEXT_LMB] = "Remove Bandages" + return CONTEXTUAL_SCREENTIP_SET + return NONE + +/obj/item/razor/scissors/suicide_act(mob/living/carbon/user) + if(!user.get_bodypart(BODY_ZONE_HEAD)) + return NONE + user.visible_message( + span_suicide("[user] starts to cut too close to [user.p_their()] neck! It looks like [user.p_theyre()] trying to commit suicide!"), + ) + if(!do_after(user, 3 SECONDS)) + return SHAME + var/obj/item/bodypart/head/head = user.get_bodypart(BODY_ZONE_HEAD) + shave(user, BODY_ZONE_PRECISE_MOUTH) + shave(user, BODY_ZONE_HEAD) + if(head.dismember(BRUTE)) + return BRUTELOSS + user.visible_message( + span_suicide("[user] avoids cutting [user.p_their()] neck, somehow?"), + ) + return SHAME + +/obj/item/razor/scissors/get_surgery_tool_overlay(tray_extended) + return "hemostat" + +/obj/item/razor/scissors/proc/cut_gauze_on_mob(mob/living/carbon/cutting, mob/living/user) + var/obj/item/bodypart/target_limb = cutting.get_bodypart(user.zone_selected) + if(isnull(target_limb?.current_gauze)) + return NONE + + cutting.balloon_alert(user, "cutting bandages...") + user.visible_message( + span_notice("[user] begins cutting through [target_limb.current_gauze] on [cutting == user ? "[user.p_their()]" : "[cutting]'s"] [target_limb.plaintext_zone]..."), + span_notice("You begin cutting through [target_limb.current_gauze] on [cutting == user ? "your" : "[cutting]'s"] [target_limb.plaintext_zone]..."), + ) + playsound(src, shave_sound, 33, TRUE, SHORT_RANGE_SOUND_EXTRARANGE) + if(!do_after(user, 3 SECONDS, cutting)) + return ITEM_INTERACT_BLOCKING + + target_limb = cutting.get_bodypart(user.zone_selected) + if(isnull(target_limb?.current_gauze)) + return ITEM_INTERACT_BLOCKING + + playsound(src, shave_sound, 33, TRUE, SHORT_RANGE_SOUND_EXTRARANGE) + cutting.balloon_alert(user, "bandages cut") + user.visible_message( + span_notice("[user] cut through [target_limb.current_gauze] on [cutting == user ? "[user.p_their()]" : "[cutting]'s"] [target_limb.plaintext_zone]."), + span_notice("You cut through [target_limb.current_gauze] on [cutting == user ? "your" : "[cutting]'s"] [target_limb.plaintext_zone]."), + ) + new /obj/effect/decal/cleanable/shreds(cutting.drop_location(), target_limb.current_gauze.name) + qdel(target_limb.remove_gauze()) + return ITEM_INTERACT_SUCCESS + +/obj/item/razor/scissors/interact_with_atom(atom/interacting_with, mob/living/user, list/modifiers) + if(user.combat_mode) + return ITEM_INTERACT_SKIP_TO_ATTACK + if(iscarbon(interacting_with)) + return cut_gauze_on_mob(interacting_with, user) + + return NONE + +/obj/item/razor/scissors/equipped(mob/user, slot, initial) + . = ..() + if(slot & ITEM_SLOT_HANDS) + RegisterSignal(user, COMSIG_MOVABLE_MOVED, PROC_REF(running_with_scissors), override = TRUE) + +/obj/item/razor/scissors/dropped(mob/user, silent) + . = ..() + UnregisterSignal(user, COMSIG_MOVABLE_MOVED) + +// melbert todo : component this (just for fun) +/obj/item/razor/scissors/proc/running_with_scissors(mob/living/source) + SIGNAL_HANDLER + + if(!istype(source) || !source.get_bodypart(BODY_ZONE_HEAD)) + return + if(source.move_intent != MOVE_INTENT_RUN) + return + if(source.body_position != STANDING_UP || source.pulledby || source.buckled) + return + // clowns are more likely to trip and fall + var/fall_prob = (HAS_TRAIT(source, TRAIT_CLUMSY) || HAS_TRAIT(source, TRAIT_DUMB)) ? 33 : 1 + if(prob(100 - fall_prob)) + return + SpinAnimation(0.4 SECONDS, 1) + playsound(src, hitsound, get_clamped_volume(), TRUE, falloff_distance = 0) + source.visible_message( + span_danger("[source] trips and falls, cutting [source.p_them()]self with [src]!"), + span_userdanger("You trip and fall, cutting yourself with [src]!"), + ) + source.apply_damage(force, damtype, BODY_ZONE_HEAD, bare_wound_bonus = 20, sharpness = get_sharpness(), attacking_item = src) + source.Knockdown(5 SECONDS) + source.pain_emote("scream") + add_fingerprint(source) + var/bloodness = source.get_blood_dna_list() + if(ishuman(source)) + var/mob/living/carbon/human/husource = source + husource.add_blood_DNA_to_items(bloodness, ITEM_SLOT_OCLOTHING|ITEM_SLOT_HEAD|ITEM_SLOT_MASK) + add_blood_DNA(bloodness) + +/obj/item/razor/scissors/medical + name = "medical scissors" + desc = "A pair of scissors intended for use in medical procedures, such as cutting bandages or sutures." + +/obj/item/razor/scissors/medical/trauma + name = "trauma shears" + desc = "A pair of scissors designed for cutting through tough materials, such as clothing or casts." + custom_materials = list(/datum/material/iron = SHEET_MATERIAL_AMOUNT * 5, /datum/material/glass = SHEET_MATERIAL_AMOUNT) + force = 15 + toolspeed = 1 + +/obj/item/razor/scissors/barber + name = "barber scissors" + desc = "A pair of scissors intended for use in hair cutting and styling." diff --git a/code/modules/unit_tests/medical_wounds.dm b/code/modules/unit_tests/medical_wounds.dm index 161492a726a9..4eb415123303 100644 --- a/code/modules/unit_tests/medical_wounds.dm +++ b/code/modules/unit_tests/medical_wounds.dm @@ -15,7 +15,7 @@ for(iter_test_wound_list in list(list(/datum/wound/blunt/bone/moderate, /datum/wound/blunt/bone/severe, /datum/wound/blunt/bone/critical),\ list(/datum/wound/slash/flesh/moderate, /datum/wound/slash/flesh/severe, /datum/wound/slash/flesh/critical),\ list(/datum/wound/pierce/bleed/moderate, /datum/wound/pierce/bleed/severe, /datum/wound/pierce/bleed/critical),\ - list(/datum/wound/burn/flesh/moderate, /datum/wound/burn/flesh/severe, /datum/wound/burn/flesh/critical))) + list(/datum/wound/flesh/burn/moderate, /datum/wound/flesh/burn/severe, /datum/wound/flesh/burn/critical))) TEST_ASSERT_EQUAL(length(victim.all_wounds), 0, "Patient is somehow wounded before test") var/datum/wound/iter_test_wound @@ -56,7 +56,7 @@ for(iter_test_wound_list in list(list(/datum/wound/blunt/bone/moderate, /datum/wound/blunt/bone/severe, /datum/wound/blunt/bone/critical),\ list(/datum/wound/slash/flesh/moderate, /datum/wound/slash/flesh/severe, /datum/wound/slash/flesh/critical),\ list(/datum/wound/pierce/bleed/moderate, /datum/wound/pierce/bleed/severe, /datum/wound/pierce/bleed/critical),\ - list(/datum/wound/burn/flesh/moderate, /datum/wound/burn/flesh/severe, /datum/wound/burn/flesh/critical))) + list(/datum/wound/flesh/burn/moderate, /datum/wound/flesh/burn/severe, /datum/wound/flesh/burn/critical))) TEST_ASSERT_EQUAL(length(victim.all_wounds), 0, "Patient is somehow wounded before test") var/datum/wound/iter_test_wound diff --git a/maplestation_modules/code/controllers/subsystem/pain_subsystem.dm b/maplestation_modules/code/controllers/subsystem/pain_subsystem.dm index 690da8aa97f1..727db355cfdd 100644 --- a/maplestation_modules/code/controllers/subsystem/pain_subsystem.dm +++ b/maplestation_modules/code/controllers/subsystem/pain_subsystem.dm @@ -1,5 +1,5 @@ PROCESSING_SUBSYSTEM_DEF(pain) name = "Pain" flags = SS_NO_INIT | SS_BACKGROUND - priority = 20 + priority = 90 wait = 2 SECONDS diff --git a/maplestation_modules/code/datums/pain/pain.dm b/maplestation_modules/code/datums/pain/pain.dm index 11a992b7ffc7..ab00960cbc27 100644 --- a/maplestation_modules/code/datums/pain/pain.dm +++ b/maplestation_modules/code/datums/pain/pain.dm @@ -1,6 +1,3 @@ -// For debugging pain -// #define PAIN_DEBUG - /** * # Pain controller * @@ -157,6 +154,7 @@ var/limb_removed_pain = (dismembered ? PAIN_LIMB_DISMEMBERED : PAIN_LIMB_REMOVED) adjust_bodypart_pain(BODY_ZONE_CHEST, limb_removed_pain) adjust_bodypart_pain(BODY_ZONES_MINUS_CHEST, limb_removed_pain / 3) + adjust_traumatic_shock(limb_removed_pain / 4) if(!QDELETED(lost_limb)) lost_limb.pain = initial(lost_limb.pain) @@ -277,6 +275,10 @@ if(print_debug_messages && (print_debug_decay || abs(adjusted_amount) > 1)) testing("PAIN DEBUG: [parent] recived [adjusted_amount] pain to [adjusted_bodypart]. Part pain: [adjusted_bodypart.pain]") #endif +#ifdef HEALTH_DEBUG + // solely for updating the debug view + parent.updatehealth() +#endif return TRUE @@ -286,7 +288,7 @@ * * amount - the number of ticks of progress to remove. Note that one tick = two seconds for pain. * * down_to - the minimum amount of pain shock the mob can have. */ -/datum/pain/proc/adjust_traumatic_shock(amount, down_to = -20) +/datum/pain/proc/adjust_traumatic_shock(amount, down_to = 0) if(amount > 0) amount *= max(pain_modifier, 0.33) @@ -295,7 +297,7 @@ if(traumatic_shock <= 0) parent.remove_consciousness_modifier(PAINSHOCK) else - parent.add_consciousness_modifier(PAINSHOCK, -0.33 * traumatic_shock) + parent.add_consciousness_modifier(PAINSHOCK, -0.15 * traumatic_shock) // Soft crit if(traumatic_shock >= SHOCK_DANGER_THRESHOLD) if(!HAS_TRAIT_FROM(parent, TRAIT_SOFT_CRIT, PAINSHOCK)) @@ -310,6 +312,12 @@ parent.remove_max_consciousness_value(PAINSHOCK) parent.remove_status_effect(/datum/status_effect/low_blood_pressure) parent.remove_traits(list(TRAIT_SOFT_CRIT, TRAIT_LABOURED_BREATHING), PAINSHOCK) + +#ifdef HEALTH_DEBUG + // solely for updating the debug view + parent.updatehealth() +#endif + /** * Set the minimum amount of pain in all [def_zones] by [amount]. * @@ -461,14 +469,15 @@ #endif adjust_bodypart_min_pain(wounded_limb.body_zone, initial(applied_wound.severity) * 5) - adjust_bodypart_pain(wounded_limb.body_zone, (applied_wound.severity) * 7.5) + adjust_bodypart_pain(wounded_limb.body_zone, initial(applied_wound.severity) * 7.5) + adjust_traumatic_shock(initial(applied_wound.severity) * 4) /// Removes pain when a wound is healed. /datum/pain/proc/remove_wound_pain(mob/living/carbon/source, datum/wound/removed_wound, obj/item/bodypart/wounded_limb) SIGNAL_HANDLER - adjust_bodypart_min_pain(wounded_limb.body_zone, initial(removed_wound.severity) * - 5) - adjust_bodypart_pain(wounded_limb.body_zone, initial(removed_wound.severity) * - 5) + adjust_bodypart_min_pain(wounded_limb.body_zone, initial(removed_wound.severity) * -5) + adjust_bodypart_pain(wounded_limb.body_zone, initial(removed_wound.severity) * -5) /datum/pain/process(seconds_per_tick) if(parent.stat == DEAD) @@ -512,9 +521,9 @@ shock_mod *= 0.5 if(parent.health > 0) shock_mod *= 0.25 - if(parent.health <= parent.maxHealth * -2 || (!HAS_TRAIT(parent, TRAIT_NOBLOOD) && parent.blood_volume < BLOOD_VOLUME_BAD)) + if(parent.health <= parent.maxHealth * -2) shock_mod *= 2 - if(parent.health <= parent.maxHealth * -4 || (!HAS_TRAIT(parent, TRAIT_NOBLOOD) && parent.blood_volume < BLOOD_VOLUME_SURVIVE)) + if(parent.health <= parent.maxHealth * -4) shock_mod *= 3 // stacks with above var/curr_pain = get_total_pain() if(curr_pain < 25) @@ -755,10 +764,7 @@ if(!length(phrase)) return - var/num_repeats = floor(((get_total_pain() / 75) + (traumatic_shock / 75)) * pain_modifier) - if(traumatic_shock < 90) - num_repeats *= 0.5 - + var/num_repeats = floor(((get_total_pain() / 50) + (traumatic_shock / 50)) * pain_modifier) num_repeats = clamp(num_repeats, 1, 6) if(num_repeats <= 1) return diff --git a/maplestation_modules/code/datums/pain/pain_effects.dm b/maplestation_modules/code/datums/pain/pain_effects.dm index 4a94a359eae1..7fa7acff45ec 100644 --- a/maplestation_modules/code/datums/pain/pain_effects.dm +++ b/maplestation_modules/code/datums/pain/pain_effects.dm @@ -101,6 +101,7 @@ id = "recent_defib" status_type = STATUS_EFFECT_REFRESH alert_type = null + remove_on_fullheal = TRUE /// Base amount of consciousness / max consciousness to give the patient var/base_con = 15 diff --git a/maplestation_modules/code/datums/pain/pain_helpers.dm b/maplestation_modules/code/datums/pain/pain_helpers.dm index 943cf1538262..7ce582d0f905 100644 --- a/maplestation_modules/code/datums/pain/pain_helpers.dm +++ b/maplestation_modules/code/datums/pain/pain_helpers.dm @@ -84,9 +84,9 @@ * Adjusts the progress of pain shock on the current mob. * * * amount - the number of ticks of progress to remove. Note that one tick = two seconds for pain. - * * down_to - the minimum amount of pain shock the mob can have. Defaults to -30, giving the mob a buffer against shock. + * * down_to - the minimum amount of pain shock the mob can have. */ -/mob/living/proc/adjust_traumatic_shock(amount, down_to = -20) +/mob/living/proc/adjust_traumatic_shock(amount, down_to = 0) pain_controller?.adjust_traumatic_shock(amount, down_to) /** diff --git a/maplestation_modules/code/datums/pain/pain_implements.dm b/maplestation_modules/code/datums/pain/pain_implements.dm index 67786277cf62..98150949669a 100644 --- a/maplestation_modules/code/datums/pain/pain_implements.dm +++ b/maplestation_modules/code/datums/pain/pain_implements.dm @@ -210,6 +210,12 @@ desc = "A mix of Aspirin, Paracetamol and Coffee to produce an effective, but short lasting painkiller with little to no side effects. Do not take multiple at once." list_reagents = list(/datum/reagent/medicine/painkiller/aspirin_para_coffee = 10) +/obj/item/reagent_containers/pill/nitroglycerin + name = "nitroglycerin pill" + desc = "Used to treat heart conditions." + icon_state = "pill10" + list_reagents = list(/datum/reagent/nitroglycerin = 5) + /obj/item/storage/pill_bottle/prescription name = "prescription pill bottle" desc = "Contains prescription pills." diff --git a/maplestation_modules/code/datums/pain/pain_reagents/painkillers.dm b/maplestation_modules/code/datums/pain/pain_reagents/painkillers.dm index 181248a76f87..5b408ff35c9b 100644 --- a/maplestation_modules/code/datums/pain/pain_reagents/painkillers.dm +++ b/maplestation_modules/code/datums/pain/pain_reagents/painkillers.dm @@ -395,7 +395,7 @@ chemical_flags = REAGENT_CAN_BE_SYNTHESIZED pain_modifier = 0.75 pain_type_to_look_for = BURN - wound_type_to_look_for = /datum/wound/burn + wound_type_to_look_for = /datum/wound/flesh /datum/reagent/medicine/painkiller/specialized/anurifen/on_mob_life(mob/living/carbon/M, seconds_per_tick, times_fired) // a bit of aiuri influence diff --git a/maplestation_modules/code/datums/pain/pain_status_effects/low_blood_pressure.dm b/maplestation_modules/code/datums/pain/pain_status_effects/low_blood_pressure.dm index a9ab6b8ba060..99bcdd0827b7 100644 --- a/maplestation_modules/code/datums/pain/pain_status_effects/low_blood_pressure.dm +++ b/maplestation_modules/code/datums/pain/pain_status_effects/low_blood_pressure.dm @@ -33,4 +33,6 @@ /datum/status_effect/low_blood_pressure/proc/on_healthscan(datum/source, list/render_list, advanced, mob/user, mode, tochat) SIGNAL_HANDLER + if(owner.has_status_effect(/datum/status_effect/high_blood_pressure)) + return render_list += "Hypotension detected.
" diff --git a/maplestation_modules/code/game/objects/structures/vital_floor_scanner.dm b/maplestation_modules/code/game/objects/structures/vital_floor_scanner.dm index d976cc555d23..b538e050f04f 100644 --- a/maplestation_modules/code/game/objects/structures/vital_floor_scanner.dm +++ b/maplestation_modules/code/game/objects/structures/vital_floor_scanner.dm @@ -1,5 +1,4 @@ #define LINK_RANGE 2 -#define SCAN_EFFECT_DURATION 1 SECONDS /datum/design/board/vital_floor_scanner name = "Vitals Scanning Pad" @@ -53,6 +52,7 @@ COMSIG_ATOM_EXITED = PROC_REF(on_exited), ) AddElement(/datum/element/connect_loc, loc_connections) + AddComponent(/datum/component/simple_rotation, ROTATION_REQUIRE_WRENCH|ROTATION_IGNORE_ANCHORED) update_appearance(UPDATE_OVERLAYS) register_context() @@ -87,7 +87,7 @@ return COOLDOWN_START(src, scan_cooldown, 5 SECONDS) - playsound(src, 'maplestation_modules/sound/healthscanner_used.ogg', 25, FALSE, MEDIUM_RANGE_SOUND_EXTRARANGE) + playsound(src, 'maplestation_modules/sound/healthscanner_used.ogg', 15, FALSE, MEDIUM_RANGE_SOUND_EXTRARANGE) set_occupant(arrived) use_power(active_power_usage) @@ -114,6 +114,7 @@ if(isnull(reader.patient)) // It failed I guess continue + reader.beeps = FALSE var/scansound = 'maplestation_modules/sound/healthscanner_stable.ogg' switch(reader.patient.stat) if(DEAD) @@ -128,7 +129,7 @@ else reader.beep_message("lets out a beep.") - playsound(reader, scansound, 25, FALSE, MEDIUM_RANGE_SOUND_EXTRARANGE) + playsound(reader, scansound, 15, FALSE, MEDIUM_RANGE_SOUND_EXTRARANGE) return /obj/machinery/vital_floor_scanner/proc/disable_vitals_nearby(mob/leaving = occupant) @@ -165,24 +166,18 @@ if(QDELING(src)) return if(!isnull(occupant)) - addtimer(CALLBACK(src, PROC_REF(enable_vitals_nearby)), SCAN_EFFECT_DURATION, TIMER_UNIQUE) + addtimer(CALLBACK(src, PROC_REF(enable_vitals_nearby)), /obj/effect/temp_visual/vitals_scan_effect::duration, TIMER_UNIQUE) scan_effect() else if(!isnull(old_occupant)) disable_vitals_nearby(old_occupant) + for(var/obj/effect/temp_visual/vitals_scan_effect/effect in old_occupant) + animate(effect, alpha = 0, time = 0.2 SECONDS, flags = ANIMATION_PARALLEL) /obj/machinery/vital_floor_scanner/proc/scan_effect() - var/image/scan_effect = image( - icon = 'maplestation_modules/icons/obj/floor_scan.dmi', - loc = occupant, - icon_state = "scan_effect", - layer = ABOVE_ALL_MOB_LAYER, - ) - scan_effect.alpha = 200 - animate(scan_effect, alpha = 100, time = SCAN_EFFECT_DURATION * 0.25) - animate(scan_effect, alpha = 200, time = SCAN_EFFECT_DURATION * 0.25, loop = -1) - SET_PLANE_EXPLICIT(scan_effect, occupant.plane, occupant) - occupant.flick_overlay_view(scan_effect, SCAN_EFFECT_DURATION) + var/obj/effect/temp_visual/vitals_scan_effect/effect = new(occupant) + SET_PLANE_IMPLICIT(effect, plane) + occupant.vis_contents += effect /obj/machinery/vital_floor_scanner/screwdriver_act(mob/living/user, obj/item/tool) balloon_alert(user, "deconstructing...") @@ -192,5 +187,24 @@ deconstruct(TRUE) return ITEM_INTERACT_SUCCESS +/obj/effect/temp_visual/vitals_scan_effect + duration = 1.1 SECONDS + alpha = 200 + icon = 'maplestation_modules/icons/obj/floor_scan.dmi' + icon_state = "scan_effect.1" + base_icon_state = "scan_effect" + layer = ABOVE_ALL_MOB_LAYER + mouse_opacity = MOUSE_OPACITY_TRANSPARENT + +/obj/effect/temp_visual/vitals_scan_effect/Initialize(mapload) + . = ..() + // we manually animate this, rather than just using an animated icon state or flick, to work around byond animated state memes + // (normally, all animated icon states are synced to the same time, which would bad here) + for(var/i in 2 to duration) + if(PERFORM_ALL_TESTS(focus_only/runtime_icon_states) && !icon_exists(icon, "[base_icon_state].[i]")) + stack_trace("Missing scan effect icon state: [base_icon_state].[i]") + animate(src, time = 0.1 SECONDS, icon_state = "[base_icon_state].[i]", flags = ANIMATION_CONTINUE) + if(PERFORM_ALL_TESTS(focus_only/runtime_icon_states) && icon_exists(icon, "[base_icon_state].[duration + 1]")) + stack_trace("Extra scan effect icon state: [base_icon_state].[duration + 1]") + #undef LINK_RANGE -#undef SCAN_EFFECT_DURATION diff --git a/maplestation_modules/code/game/objects/structures/vital_reader.dm b/maplestation_modules/code/game/objects/structures/vital_reader.dm index 13dbb1df0a63..11f5255019ce 100644 --- a/maplestation_modules/code/game/objects/structures/vital_reader.dm +++ b/maplestation_modules/code/game/objects/structures/vital_reader.dm @@ -37,6 +37,11 @@ ) result_path = /obj/machinery/computer/vitals_reader +/obj/item/wallframe/status_display/vitals/after_attach(obj/machinery/computer/vitals_reader/attached_to) + . = ..() + if(istype(attached_to)) + attached_to.beeps = FALSE + /obj/item/wallframe/status_display/vitals/advanced name = "advanced vitals display frame" desc = "Used to build advanced vitals displays. Performs a more detailed scan of the patient than the basic display." @@ -418,7 +423,10 @@ MAPPING_DIRECTIONAL_HELPERS(/obj/machinery/computer/vitals_reader/no_hand, 32) var/obj/machinery/computer/operating/op = nearby_thing patient = op.table?.patient - if(!istype(patient) || (patient.mob_biotypes & MOB_ROBOTIC)) + if(!istype(patient)) + continue + // skips bots, borgs, and drones - not synths and androids + if((patient.mob_biotypes & MOB_ROBOTIC) && !(patient.mob_biotypes & MOB_HUMANOID)) continue set_patient(patient) @@ -467,7 +475,7 @@ MAPPING_DIRECTIONAL_HELPERS(/obj/machinery/computer/vitals_reader/no_hand, 32) beep_message("lets out a beep.") last_reported_stat = CONSCIOUS - playsound(src, beepsound, 25, FALSE, SHORT_RANGE_SOUND_EXTRARANGE) + playsound(src, beepsound, 15, FALSE, SHORT_RANGE_SOUND_EXTRARANGE) /obj/machinery/computer/vitals_reader/proc/beep_message(message) for(var/mob/viewer as anything in viewers(src)) diff --git a/maplestation_modules/code/modules/cargo/packs.dm b/maplestation_modules/code/modules/cargo/packs.dm index 89aa57dd8444..536579fa10c1 100644 --- a/maplestation_modules/code/modules/cargo/packs.dm +++ b/maplestation_modules/code/modules/cargo/packs.dm @@ -208,6 +208,19 @@ /obj/item/storage/pill_bottle/prescription/buproprion, ) +/obj/item/storage/pill_bottle/prescription/nitroglycerin + pill_type = /obj/item/reagent_containers/pill/nitroglycerin + num_pills = 5 + +/datum/supply_pack/goody/nitroglycerin + name = "Nitroglycerin Prescription" + desc = "Contains a pill bottle of Nitroglycerin, which helps with heart conditions." + group = GROUP_DRUGS + cost = PAYCHECK_CREW * 10 + contains = list( + /obj/item/storage/pill_bottle/prescription/nitroglycerin, + ) + /datum/supply_pack/medical/painkiller_syringes name = "Painkiller Syringe Shipment" desc = "Contains six syringes of general medicinal painkillers - Ibuprofen, Paracetamol, and Aspirin." diff --git a/maplestation_modules/code/modules/library/skill_learning/job_skillchips/mining.dm b/maplestation_modules/code/modules/library/skill_learning/job_skillchips/mining.dm index baf85702c901..5343f4ca0646 100644 --- a/maplestation_modules/code/modules/library/skill_learning/job_skillchips/mining.dm +++ b/maplestation_modules/code/modules/library/skill_learning/job_skillchips/mining.dm @@ -3,20 +3,43 @@ /obj/item/skillchip/job/off_z_pain_resistance name = "D1GGY skillchip" skill_name = "Off Station Pain Resistance" - skill_description = "For the adventurous in life, this skillchip provides a reduction in pain received when off the station." + skill_description = "For the adventurous in life, this skillchip provides \ + a reduction in pain (and boost to endurance) when off the station. \ + Additionally, includes an emergency injector which provides a temporary \ + boost of movement speed and pain resistance." skill_icon = "fist-raised" activate_message = span_notice("You feel like you can safely take on the unknown.") deactivate_message = span_notice("You feel more vulnerable to the unknown.") + var/datum/action/cooldown/miner_skillchip/action + var/refill_cd = 10 MINUTES + +/obj/item/skillchip/job/off_z_pain_resistance/Initialize(mapload, is_removable) + . = ..() + create_reagents(25) + action = new(src) + /obj/item/skillchip/job/off_z_pain_resistance/on_activate(mob/living/carbon/user, silent = FALSE) . = ..() RegisterSignal(user, COMSIG_MOVABLE_Z_CHANGED, PROC_REF(check_z)) + check_z(user, null, user.loc) /obj/item/skillchip/job/off_z_pain_resistance/on_deactivate(mob/living/carbon/user, silent = FALSE) UnregisterSignal(user, COMSIG_MOVABLE_Z_CHANGED) user.unset_pain_mod(PAIN_MOD_OFF_STATION) return ..() +/obj/item/skillchip/job/off_z_pain_resistance/proc/inject() + var/mob/living/injectee = holding_brain?.owner + if(isnull(injectee)) + return + reagents.trans_to(injectee, amount = reagents.maximum_volume, transferred_by = injectee, methods = INJECT) + addtimer(CALLBACK(src, PROC_REF(refill)), refill_cd) + +/obj/item/skillchip/job/off_z_pain_resistance/proc/refill() + reagents.add_reagent(/datum/reagent/medicine/painkiller/morphine, amount = 10, added_purity = 1) + reagents.add_reagent(/datum/reagent/medicine/ephedrine, amount = 10, added_purity = 1) + reagents.add_reagent(/datum/reagent/medicine/modafinil, amount = 5, added_purity = 1) /** * Signal proc for [COMSIG_MOVABLE_Z_CHANGED]. @@ -26,16 +49,40 @@ /obj/item/skillchip/job/off_z_pain_resistance/proc/check_z(datum/source, turf/old_turf, turf/new_turf) SIGNAL_HANDLER - if(!iscarbon(source) || !isturf(new_turf)) + if(!ishuman(source) || !isturf(new_turf)) return - var/mob/living/carbon/carbon_source = source - if(is_station_level(new_turf.z)) - if(carbon_source.unset_pain_mod(PAIN_MOD_OFF_STATION)) - to_chat(carbon_source, span_green("Returning to the station, you feel much more vulnerable to incoming pain.")) + var/station_check = SSmapping.is_planetary() ? !SSmapping.level_trait(new_turf.z, ZTRAIT_UP) : is_station_level(new_turf.z) + var/mob/living/carbon/human/human_source = source + if(station_check) + if(human_source.unset_pain_mod(PAIN_MOD_OFF_STATION)) + human_source.sprint_length_max /= 1.5 + human_source.sprint_regen_per_second /= 1.5 + action.Remove(human_source) + to_chat(human_source, span_green("Returning to the station, you feel much more vulnerable to incoming pain.")) else - if(carbon_source.set_pain_mod(PAIN_MOD_OFF_STATION, 0.5)) - to_chat(carbon_source, span_green("As you depart from the station, you feel more resilient to incoming pain.")) + if(human_source.set_pain_mod(PAIN_MOD_OFF_STATION, 0.5)) + human_source.sprint_length_max *= 1.5 + human_source.sprint_regen_per_second *= 1.5 + action.Grant(human_source) + to_chat(human_source, span_green("As you depart from the station, you feel more resilient to incoming pain.")) + +/datum/action/cooldown/miner_skillchip + name = "Emergency Injection" + desc = "Activates your skillchip's emergency injector, giving you a temporary boost of movement speed and pain resistance." + button_icon = 'icons/hud/implants.dmi' + button_icon_state = "adrenal" + +/datum/action/cooldown/miner_skillchip/Activate() + var/obj/item/skillchip/job/off_z_pain_resistance/chip = target + chip.inject() + StartCooldown(chip.refill_cd) + owner.balloon_alert(owner, "you feel a surge of energy") + +/datum/action/cooldown/miner_skillchip/update_button_name(atom/movable/screen/movable/action_button/button, force) + . = ..() + var/obj/item/skillchip/job/off_z_pain_resistance/chip = target + button.desc += " Recharges in [DisplayTimeText(chip.refill_cd)]." /datum/outfit/job/miner skillchips = list(/obj/item/skillchip/job/off_z_pain_resistance) diff --git a/maplestation_modules/code/modules/mob/living/blood.dm b/maplestation_modules/code/modules/mob/living/blood.dm index 7b790ac75a9a..0c2a92afe62f 100644 --- a/maplestation_modules/code/modules/mob/living/blood.dm +++ b/maplestation_modules/code/modules/mob/living/blood.dm @@ -149,6 +149,7 @@ PROCESSING_SUBSYSTEM_DEF(blood_drying) // Handle adding a single drip to the base atom // Makes use of viscontents so every drip can dry at an individual rate (with an individual color) var/obj/effect/decal/cleanable/blood/drip/new_drop = new(drop) + new_drop.bloodiness = 0 new_drop.icon_state = pick_n_take(drop.random_icon_states) new_drop.color = color new_drop.vis_flags |= (VIS_INHERIT_LAYER|VIS_INHERIT_PLANE|VIS_INHERIT_ID) diff --git a/maplestation_modules/code/modules/mob/living/carbon/human/heart_rework/cpr.dm b/maplestation_modules/code/modules/mob/living/carbon/human/heart_rework/cpr.dm index e683778ce22c..187fdbe46d19 100644 --- a/maplestation_modules/code/modules/mob/living/carbon/human/heart_rework/cpr.dm +++ b/maplestation_modules/code/modules/mob/living/carbon/human/heart_rework/cpr.dm @@ -1,9 +1,126 @@ +/mob/living/carbon/human/proc/open_help_radial(mob/living/carbon/human/target) + set waitfor = FALSE + + if(client?.prefs.read_preference(/datum/preference/toggle/default_shake_help)) + target.help_shake_act(src) + return TRUE + + var/static/list/help_options + if(!help_options) + help_options = list( + "CPR" = image(icon = 'maplestation_modules/icons/hud/radial.dmi', icon_state = "cpr"), + "Check Pulse" = image(icon = 'maplestation_modules/icons/hud/radial.dmi', icon_state = "check_pulse"), + "Help Up" = image(icon = 'maplestation_modules/icons/hud/radial.dmi', icon_state = "help_up"), + ) + + var/picked = show_radial_menu(src, target, help_options, require_near = TRUE, custom_check = CALLBACK(src, PROC_REF(can_help_radial), target)) + if(!picked || !can_help_radial(target)) + return FALSE + + switch(picked) + if("CPR") + do_cpr(target) + if("Check Pulse") + check_pulse(target) + if("Help Up") + target.help_shake_act(src) + + return TRUE + +/mob/living/carbon/human/proc/can_help_radial(mob/living/carbon/human/target) + var/target_checks = !QDELETED(target) && target.body_position == LYING_DOWN && !target.on_fire + var/user_checks = !QDELETED(src) && body_position != LYING_DOWN && !incapacitated() + return target_checks && user_checks + +/// Allows people to skip the radial if they don't want to use it +/datum/preference/toggle/default_shake_help + category = PREFERENCE_CATEGORY_GAME_PREFERENCES + savefile_key = "default_shake_when_helping" + savefile_identifier = PREFERENCE_PLAYER + default_value = FALSE + +/// Emote to skip the radial and just perform CPR +/datum/emote/living/carbon/human/cpr + key = "cpr" + hands_use_check = TRUE + +/datum/emote/living/carbon/human/cpr/run_emote(mob/user, params, type_override, intentional) + . = ..() + if(!.) + return + + var/obj/item/hand_item/cpr/hand = new(user) + user.put_in_hands(hand) + +/// Emote to skip the radial and just check pulse +/datum/emote/living/carbon/human/check_pulse + key = "check" + hands_use_check = TRUE + +/datum/emote/living/carbon/human/check_pulse/run_emote(mob/user, params, type_override, intentional) + . = ..() + if(!.) + return + + var/obj/item/hand_item/check_pulse/hand = new(user) + user.put_in_hands(hand) + +/// Hand item that serves as a placeholder for CPR +/obj/item/hand_item/cpr + name = "cpr" + desc = "Stay with me!" + inhand_icon_state = "nothing" + +/obj/item/hand_item/cpr/interact_with_atom(atom/interacting_with, mob/living/user, list/modifiers) + if(!ishuman(interacting_with) || interacting_with == user) + return NONE + var/mob/living/carbon/human/human_user = user + human_user.do_cpr(interacting_with) + return ITEM_INTERACT_SUCCESS + +/// Hand item that serves as a placeholder for checking pulse +/obj/item/hand_item/check_pulse + name = "check pulse" + desc = "Let's see if you're still kicking..." + inhand_icon_state = "nothing" + +/obj/item/hand_item/check_pulse/interact_with_atom(atom/interacting_with, mob/living/user, list/modifiers) + if(!ishuman(interacting_with)) + return NONE + var/mob/living/carbon/human/human_user = user + human_user.check_pulse(interacting_with) + return ITEM_INTERACT_SUCCESS + +/// Check the pulse of a target +/mob/living/carbon/human/proc/check_pulse(mob/living/carbon/human/target) + if(target.on_fire) // you have better things to worry about than a pulse + return + + target.balloon_alert(src, "checking pulse...") + visible_message( + span_notice("[src] checks [target.name]'s pulse..."), + span_notice("You check [target.name]'s pulse..."), + visible_message_flags = ALWAYS_SHOW_SELF_MESSAGE, + ) + target.share_blood_on_touch(src, ITEM_SLOT_NECK) + if(!do_after(src, 6 SECONDS, target)) + return + var/own_bpm_penalty = get_heart_rate() > 11 ? 1.2 : 1 + var/bpm_a = round(target.get_bpm() * own_bpm_penalty, 1) + if(bpm_a <= 0) + to_chat(src, span_warning("[target.name] has no pulse!")) + return + + var/bpm_b = round(target.get_bpm() * 1.2 * own_bpm_penalty, 1) + to_chat(src, span_notice("[target.name]'s pulse is around [min(bpm_a, bpm_b)] to [max(bpm_a, bpm_b)] bpm.")) + /// Number of "beats" per CPR cycle /// This corresponds to N - 1 compressions and 1 breath #define BEATS_PER_CPR_CYCLE 16 // Also I'm kinda cheating here because we do 15 compressions to 1 breath rather than 30 compressions to 2 breaths // But it's close enough to the real thing (ratio wise) that I'm OK with it +/// The actual process loop of CPR /mob/living/carbon/human/proc/cpr_process(mob/living/carbon/human/target, beat = 0, panicking = FALSE) set waitfor = FALSE @@ -11,11 +128,24 @@ to_chat(src, span_warning("Your hands are full, you can't perform CPR!")) return + // -- cpr begins -- + + target.share_blood_on_touch(src) + + // check panic state var/cpr_certified = HAS_TRAIT(src, TRAIT_CPR_CERTIFIED) - if(!panicking && target.stat != CONSCIOUS && beat >= BEATS_PER_CPR_CYCLE + 1) - to_chat(src, span_warning("[target] still has no pulse[cpr_certified ? " - you pick up the pace." : "! You try harder!"]")) - panicking = TRUE + var/should_panic = beat >= BEATS_PER_CPR_CYCLE + 1 && target.get_bpm() <= 0 + if(panicking) + if(!should_panic) + to_chat(src, span_warning("[target] seems to be stabilising... You feel a pulse!")) + panicking = FALSE + else + if(should_panic) + to_chat(src, span_warning("[target] still has no pulse[cpr_certified ? " - you pick up the pace." : "! You try harder!"]")) + panicking = TRUE + + // check breaths var/doafter_mod = panicking ? 0.5 : 1 var/doing_a_breath = FALSE if(beat % BEATS_PER_CPR_CYCLE == 0 || beat == 1) @@ -34,15 +164,13 @@ else doing_a_breath = TRUE + // actually do the thing if(doing_a_breath) visible_message( span_notice("[src] attempts to give [target.name] a rescue breath!"), span_notice("You attempt to give [target.name] a rescue breath as a part of CPR... Hold still!"), + visible_message_flags = ALWAYS_SHOW_SELF_MESSAGE, ) - if(target.stat == DEAD) - to_chat(src, span_warning("[target.p_Their()] mouth feels cold to the touch...")) - else if(!target.undergoing_cardiac_arrest()) - to_chat(src, span_notice("You feel a pulse!")) if(!do_after(user = src, delay = doafter_mod * 6 SECONDS, target = target)) return @@ -51,6 +179,7 @@ visible_message( span_notice("[src] delivers a rescue breath to [target.name]!"), span_notice("You deliver a rescue breath to [target.name]."), + visible_message_flags = ALWAYS_SHOW_SELF_MESSAGE, ) else @@ -59,6 +188,7 @@ visible_message( span_notice("[src] attempts to give [target.name] chest compressions!"), span_notice("You try to perform chest compressions as a part of CPR on [target.name]... Hold still!"), + visible_message_flags = ALWAYS_SHOW_SELF_MESSAGE, ) if(!do_after(user = src, delay = doafter_mod * 1 SECONDS, target = target)) @@ -68,12 +198,15 @@ visible_message( span_notice("[src] delivers chest compressions on [target.name]!"), span_notice("You deliver chest compressions on [target.name]."), + visible_message_flags = ALWAYS_SHOW_SELF_MESSAGE, ) target.apply_status_effect(/datum/status_effect/cpr_applied) // compressions help a little bit with oxyloss, to simulate blood flow returning to your body target.adjustOxyLoss(-0.5 * (cpr_certified ? 2 : 1)) + // -- cpr ends, effects begin -- + if(doing_a_breath) if(HAS_TRAIT(target, TRAIT_NOBREATH)) to_chat(target, span_unconscious("You feel a breath of fresh air... which is a sensation you don't recognise...")) @@ -120,9 +253,12 @@ log_combat(src, target, "CPRed", addition = "(compression)") - if(target.body_position != LYING_DOWN || !target.undergoing_cardiac_arrest()) + // -- effects end, check loop -- + + if(target.body_position != LYING_DOWN || target.on_fire) return + // loop cpr_process(target, beat + 1, panicking) #undef BEATS_PER_CPR_CYCLE diff --git a/maplestation_modules/code/modules/mob/living/carbon/human/species_types/silverscale.dm b/maplestation_modules/code/modules/mob/living/carbon/human/species_types/silverscale.dm index 8a870d808a36..af4bf36e26cc 100644 --- a/maplestation_modules/code/modules/mob/living/carbon/human/species_types/silverscale.dm +++ b/maplestation_modules/code/modules/mob/living/carbon/human/species_types/silverscale.dm @@ -6,7 +6,7 @@ . = ..() techweb_point_items += list( - /obj/item/organ/internal/tongue/lizard/silver = list(TECHWEB_POINT_TYPE_GENERIC = 65000) + /obj/item/organ/internal/tongue/lizard/silver = list(TECHWEB_POINT_TYPE_GENERIC = TECHWEB_TIER_5_POINTS) ) /datum/export/organ/tongue/lizard/silver @@ -35,23 +35,18 @@ /// Tracks what color our glint is, to prevent unnecessary updates VAR_PRIVATE/glint_color + organ_traits = list( + TRAIT_HOLY, + ) + /obj/item/organ/internal/tongue/lizard/silver/Initialize(mapload) . = ..() - desc += " Whoever this tongue is attached to will inherit the abilities of the silverscale." - desc += span_blue(" These tongues are highly sought after by scientists galaxy-wide (though they never make open inquries). This is sure to fetch a high \ - price in the cargo shuttle, or supply a hefty amount of research information if destructively analyzed.") - //Migrating silverscale traits to the tongue - LAZYOR(organ_traits, list( - TRAIT_HOLY, - TRAIT_NOBREATH, - TRAIT_PIERCEIMMUNE, - TRAIT_RESISTHIGHPRESSURE, - TRAIT_RESISTLOWPRESSURE, - TRAIT_VIRUSIMMUNE, - TRAIT_WINE_TASTER, - )) +/obj/item/organ/internal/tongue/lizard/silver/examine(mob/user) + . = ..() + . += span_blue("These tongues are highly sought after by scientists galaxy-wide (though they never make open inquries). This is sure to fetch a high \ + price in the cargo shuttle, or supply a hefty amount of research information if destructively analyzed.") /obj/item/organ/internal/tongue/lizard/silver/on_mob_insert(mob/living/carbon/organ_owner, special, movement_flags) . = ..() @@ -225,7 +220,7 @@ /datum/species/lizard/silverscale plural_form = "Silverscales" damage_modifier = 0 //It belongs on the tongue now - mutantlungs = /obj/item/organ/internal/lungs + mutantlungs = /obj/item/organ/internal/lungs/silverscale /datum/species/lizard/silverscale/on_species_loss(mob/living/carbon/human/C, datum/species/new_species, pref_load) . = ..() @@ -343,3 +338,32 @@ return to_add #undef SILVERSCALE_LOST_TONGUE_MOOD_ID + +/obj/item/organ/internal/lungs/silverscale + name = "silverscale lungs" + icon = 'icons/obj/medical/organs/infuser_organs.dmi' + icon_state = "lungs" + greyscale_config = /datum/greyscale_config/mutant_organ + greyscale_colors = "#eeeeee#eeeeee#eeeeee" + + // evolved to smoke better + maxHealth = 1.5 * STANDARD_ORGAN_THRESHOLD + // breathing in a vacuum is "fine" + safe_oxygen_min = 0 + safe_oxygen_max = 0 + // more resistant to gas hazards + safe_plasma_max = 10 + safe_co2_max = 20 + n2o_para_min = 5 + n2o_sleep_min = 10 + BZ_trip_balls_min = 10 + BZ_brain_damage_min = 20 + tritium_irradiation_moles_min = 10 + tritium_irradiation_moles_max = 30 + // wider range of safe pressure / immune to low pressure + low_pressure_threshold = 0 + high_pressure_threshold = ONE_ATMOSPHERE * 3 + +/obj/item/organ/internal/lungs/silverscale/pre_breath_gas_handling(mob/living/carbon/human/breather, datum/gas_mixture/breath) + // Any successful breath gives us healing + heal_oxyloss_on_breath(breather, breath) diff --git a/maplestation_modules/code/modules/recycling/holder.dm b/maplestation_modules/code/modules/recycling/holder.dm index 0035e900e1b4..c318704c3b93 100644 --- a/maplestation_modules/code/modules/recycling/holder.dm +++ b/maplestation_modules/code/modules/recycling/holder.dm @@ -26,8 +26,23 @@ playsound(loc, 'sound/effects/clang.ogg', 30, TRUE, FALSE) else if(prob(CONFIG_GET(number/disposals_damage_chance))) trashed_individual.apply_damage(2 * CONFIG_GET(number/disposals_damage_multiplier), BRUTE) - playsound(loc, 'sound/effects/wounds/crack2.ogg', 50, TRUE, FALSE) + if(trashed_individual.has_bones()) + playsound(loc, 'sound/effects/wounds/crack2.ogg', 50, TRUE, FALSE) else if(prob(CONFIG_GET(number/disposals_pain_chance))) var/mob/living/carbon/carbon_trashed = trashed_individual - playsound(loc, 'sound/effects/wounds/crack1.ogg', 50, TRUE, FALSE) + if(trashed_individual.has_bones()) + playsound(loc, 'sound/effects/wounds/crack1.ogg', 50, TRUE, FALSE) carbon_trashed.sharp_pain(pick(BODY_ZONES_ALL), 4 * CONFIG_GET(number/disposals_damage_multiplier), BRUTE) + +/// Check if the mob has any bones +/mob/living/proc/has_bones() + return (mob_biotypes & (MOB_ORGANIC|MOB_UNDEAD|MOB_BEAST)) + +/mob/living/silicon/has_bones() + return FALSE + +/mob/living/carbon/has_bones() + for(var/obj/item/bodypart/part as anything in bodyparts) + if(part.biological_state & BIO_BONE) + return TRUE + return FALSE diff --git a/maplestation_modules/icons/hud/radial.dmi b/maplestation_modules/icons/hud/radial.dmi new file mode 100644 index 000000000000..ac95c038118a Binary files /dev/null and b/maplestation_modules/icons/hud/radial.dmi differ diff --git a/maplestation_modules/icons/obj/floor_scan.dmi b/maplestation_modules/icons/obj/floor_scan.dmi index 14b41c71071b..3b10d3698f26 100644 Binary files a/maplestation_modules/icons/obj/floor_scan.dmi and b/maplestation_modules/icons/obj/floor_scan.dmi differ diff --git a/maplestation_modules/icons/obj/surgery_tools.dmi b/maplestation_modules/icons/obj/surgery_tools.dmi new file mode 100644 index 000000000000..18a955c6af10 Binary files /dev/null and b/maplestation_modules/icons/obj/surgery_tools.dmi differ diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/game_preferences/_help_shake.tsx b/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/game_preferences/_help_shake.tsx new file mode 100644 index 000000000000..6d2a01fe2dbd --- /dev/null +++ b/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/game_preferences/_help_shake.tsx @@ -0,0 +1,11 @@ +import { CheckboxInput, FeatureToggle } from '../base'; + +export const default_shake_when_helping: FeatureToggle = { + name: 'Default Shake When Helping', + category: 'GAMEPLAY', + description: + 'When clicking on a downed humanoid, you will always shake them, \ + rather than opening the help wheel. To do CPR or check their pulse, \ + you will need to use the *cpr or *check commands.', + component: CheckboxInput, +}; diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/game_preferences/init_stats.tsx b/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/game_preferences/_init_stats.tsx similarity index 100% rename from tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/game_preferences/init_stats.tsx rename to tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/game_preferences/_init_stats.tsx