diff --git a/code/__DEFINES/traits.dm b/code/__DEFINES/traits.dm index 58e278d4b3a..13cf67dd218 100644 --- a/code/__DEFINES/traits.dm +++ b/code/__DEFINES/traits.dm @@ -147,6 +147,8 @@ Remember to update _globalvars/traits.dm if you're adding/removing/renaming trai #define TRAIT_BLIND "blind" /// Mute. Can't talk. #define TRAIT_MUTE "mute" +/// Gibs on death and slips like ice. +#define TRAIT_CURSED "cursed" /// Emotemute. Can't... emote. #define TRAIT_EMOTEMUTE "emotemute" #define TRAIT_DEAF "deaf" diff --git a/code/datums/components/omen.dm b/code/datums/components/omen.dm index b7df9006ee4..e3596b903c3 100644 --- a/code/datums/components/omen.dm +++ b/code/datums/components/omen.dm @@ -8,46 +8,44 @@ */ /datum/component/omen dupe_mode = COMPONENT_DUPE_UNIQUE - /// Whatever's causing the omen, if there is one. Destroying the vessel won't stop the omen, but we destroy the vessel (if one exists) upon the omen ending var/obj/vessel - - /// Whether this is a permanent omen that cannot be removed by any non-admin means. + /// If the omen is permanent, it will never go away var/permanent = FALSE + /// Base probability of negative events. Cursed are half as unlucky. + var/luck_mod = 1 + /// Base damage from negative events. Cursed take 25% less damage. + var/damage_mod = 1 -/datum/component/omen/Initialize(silent = FALSE, _vessel, _permanent = FALSE) +/datum/component/omen/Initialize(vessel) if(!isliving(parent)) return COMPONENT_INCOMPATIBLE - vessel = _vessel - permanent = _permanent - if(!silent) - var/warning = "You get a bad feeling..." - if(permanent) - warning += " A very bad feeling... As if you are surrounded by a twisted aura of pure malevolence..." - to_chat(parent, span_warning("[warning]")) + src.vessel = vessel + +/datum/component/omen/Destroy(force) + var/mob/living/person = parent + to_chat(person, span_nicegreen("You feel a horrible omen lifted off your shoulders!")) -/datum/component/omen/Destroy(force, silent) - if(!silent) - var/mob/living/person = parent - to_chat(person, span_nicegreen("You feel a horrible omen lifted off your shoulders!")) if(vessel) vessel.visible_message(span_warning("[vessel] burns up in a sinister flash, taking an evil energy with it...")) vessel = null + return ..() /datum/component/omen/RegisterWithParent() RegisterSignal(parent, COMSIG_MOVABLE_MOVED, PROC_REF(check_accident)) - RegisterSignal(parent, COMSIG_LIVING_STATUS_KNOCKDOWN, PROC_REF(check_slip)) + RegisterSignal(parent, COMSIG_ON_CARBON_SLIP, PROC_REF(check_slip)) RegisterSignal(parent, COMSIG_CARBON_MOOD_UPDATE, PROC_REF(check_bless)) + RegisterSignal(parent, COMSIG_LIVING_DEATH, PROC_REF(check_death)) /datum/component/omen/UnregisterFromParent() - UnregisterSignal(parent, list(COMSIG_LIVING_STATUS_KNOCKDOWN, COMSIG_MOVABLE_MOVED, COMSIG_CARBON_MOOD_UPDATE)) + UnregisterSignal(parent, list(COMSIG_ON_CARBON_SLIP, COMSIG_MOVABLE_MOVED, COMSIG_CARBON_MOOD_UPDATE, COMSIG_LIVING_DEATH)) /** * check_accident() is called each step we take * - * While we're walking around, roll to see if there's any environmental hazards (currently only vending machines) on one of the adjacent tiles we can trigger. + * While we're walking around, roll to see if there's any environmental hazards on one of the adjacent tiles we can trigger. * We do the prob() at the beginning to A. add some tension for /when/ it will strike, and B. (more importantly) ameliorate the fact that we're checking up to 5 turfs's contents each time */ /datum/component/omen/proc/check_accident(atom/movable/our_guy) @@ -55,11 +53,11 @@ if(!isliving(our_guy)) return - var/mob/living/living_guy = our_guy - if(!prob(15)) + if(!prob(15 * luck_mod)) return + var/our_guy_pos = get_turf(living_guy) for(var/turf_content in our_guy_pos) if(istype(turf_content, /obj/machinery/door/airlock)) @@ -67,7 +65,7 @@ var/obj/machinery/door/airlock/darth_airlock = turf_content living_guy.apply_status_effect(/datum/status_effect/incapacitating/paralyzed, 10) INVOKE_ASYNC(darth_airlock, TYPE_PROC_REF(/obj/machinery/door/airlock, close), TRUE) - if(!permanent) + if(!permanent && !prob(66.6)) qdel(src) return @@ -91,28 +89,103 @@ /datum/component/omen/proc/check_slip(mob/living/our_guy, amount) SIGNAL_HANDLER - if(amount <= 0 || prob(50)) // 50% chance to bonk our head - return + if(prob(30)) // AAAA + INVOKE_ASYNC(our_guy, TYPE_PROC_REF(/mob, emote), "scream") + to_chat(our_guy, span_warning("What a horrible night... To have a curse!")) - var/obj/item/bodypart/the_head = our_guy.get_bodypart(BODY_ZONE_HEAD) - if(!the_head) - return + if(prob(30 * luck_mod)) /// Bonk! + var/obj/item/bodypart/the_head = our_guy.get_bodypart(BODY_ZONE_HEAD) + if(!the_head) + return + playsound(get_turf(our_guy), 'sound/effects/tableheadsmash.ogg', 90, TRUE) + our_guy.visible_message(span_danger("[our_guy] hits [our_guy.p_their()] head really badly falling down!"), span_userdanger("You hit your head really badly falling down!")) + the_head.receive_damage(75 * damage_mod) + our_guy.adjustOrganLoss(ORGAN_SLOT_BRAIN, 100 * damage_mod) + if(!permanent) + qdel(src) - playsound(get_turf(our_guy), 'sound/effects/tableheadsmash.ogg', 90, TRUE) - our_guy.visible_message(span_danger("[our_guy] hits [our_guy.p_their()] head really badly falling down!"), span_userdanger("You hit your head really badly falling down!")) - the_head.receive_damage(75) - our_guy.adjustOrganLoss(ORGAN_SLOT_BRAIN, 100) - if(!permanent) - qdel(src) + return /// Hijack the mood system to see if we get the blessing mood event to cancel the omen /datum/component/omen/proc/check_bless(mob/living/our_guy, category) SIGNAL_HANDLER - if(permanent) - return - if (!("blessing" in our_guy.mob_mood.mood_events)) return qdel(src) + +/// Severe deaths. Normally lifts the curse. +/datum/component/omen/proc/check_death(mob/living/our_guy) + SIGNAL_HANDLER + + qdel(src) + return + +/// Creates a localized explosion that shakes the camera +/datum/component/omen/proc/death_explode(mob/living/our_guy) + explosion(our_guy, explosion_cause = src) + + for(var/mob/witness as anything in view(2, our_guy)) + shake_camera(witness, 1 SECONDS, 2) + + return + +/** + * The smite omen. Permanent. + */ +/datum/component/omen/smite + +/datum/component/omen/smite/Initialize(vessel, permanent) + . = ..() + src.permanent = permanent + +/datum/component/omen/smite/check_bless(mob/living/our_guy, category) + if(!permanent) + return ..() + + return + +/datum/component/omen/smite/check_death(mob/living/our_guy) + if(!permanent) + return ..() + + death_explode(our_guy) + our_guy.gib() + + return + +/** + * The quirk omen. Permanent. + * Has only a 50% chance of bad things happening, and takes only 25% of normal damage. + */ +/datum/component/omen/quirk + permanent = TRUE + luck_mod = 0.5 // 50% chance of bad things happening + damage_mod = 0.25 // 25% of normal damage + +/datum/component/omen/quirk/RegisterWithParent() + RegisterSignal(parent, COMSIG_MOVABLE_MOVED, PROC_REF(check_accident)) + RegisterSignal(parent, COMSIG_ON_CARBON_SLIP, PROC_REF(check_slip)) + RegisterSignal(parent, COMSIG_LIVING_DEATH, PROC_REF(check_death)) + +/datum/component/omen/quirk/UnregisterFromParent() + UnregisterSignal(parent, list(COMSIG_ON_CARBON_SLIP, COMSIG_MOVABLE_MOVED, COMSIG_LIVING_DEATH)) + +/datum/component/omen/quirk/check_death(mob/living/our_guy) + if(!iscarbon(our_guy)) + our_guy.gib() + return + + // Don't explode if buckled to a stasis bed + if(our_guy.buckled) + var/obj/machinery/stasis/stasis_bed = our_guy.buckled + if(istype(stasis_bed)) + return + + death_explode(our_guy) + var/mob/living/carbon/player = our_guy + player.spread_bodyparts(skip_head = TRUE) + player.spawn_gibs() + + return diff --git a/code/datums/quirks/negative_quirks.dm b/code/datums/quirks/negative_quirks.dm index c8fd0a31ed1..20f5da2e4c1 100644 --- a/code/datums/quirks/negative_quirks.dm +++ b/code/datums/quirks/negative_quirks.dm @@ -1131,3 +1131,16 @@ if(!IS_ORGANIC_LIMB(old_limb)) cybernetics_level-- update_mood() +/datum/quirk/cursed + name = "Cursed" + desc = "You are cursed with bad luck. You are much more likely to suffer from accidents and mishaps. When it rains, it pours." + icon = "cloud-showers-heavy" + value = -8 + mob_trait = TRAIT_CURSED + gain_text = span_danger("You feel like you're going to have a bad day.") + lose_text = span_notice("You feel like you're going to have a good day.") + medical_record_text = "Patient is cursed with bad luck." + hardcore_value = 8 + +/datum/quirk/cursed/add(client/client_source) + quirk_holder.AddComponent(/datum/component/omen/quirk) diff --git a/code/game/objects/structures/mirror.dm b/code/game/objects/structures/mirror.dm index 6bcd09a9dc1..62234452315 100644 --- a/code/game/objects/structures/mirror.dm +++ b/code/game/objects/structures/mirror.dm @@ -65,7 +65,7 @@ MAPPING_DIRECTIONAL_HELPERS(/obj/structure/mirror, 28) . = ..() if(broken) // breaking a mirror truly gets you bad luck! to_chat(user, span_warning("A chill runs down your spine as [src] shatters...")) - user.AddComponent(/datum/component/omen, silent = TRUE) // we have our own message + user.AddComponent(/datum/component/omen) /obj/structure/mirror/bullet_act(obj/projectile/P) if(broken || !isliving(P.firer) || !P.damage) @@ -75,7 +75,7 @@ MAPPING_DIRECTIONAL_HELPERS(/obj/structure/mirror, 28) if(broken) // breaking a mirror truly gets you bad luck! var/mob/living/unlucky_dude = P.firer to_chat(unlucky_dude, span_warning("A chill runs down your spine as [src] shatters...")) - unlucky_dude.AddComponent(/datum/component/omen, silent=TRUE) // we have our own message + unlucky_dude.AddComponent(/datum/component/omen) /obj/structure/mirror/atom_break(damage_flag, mapload) . = ..() diff --git a/code/game/turfs/open/_open.dm b/code/game/turfs/open/_open.dm index 87a9a824299..be71cfee659 100644 --- a/code/game/turfs/open/_open.dm +++ b/code/game/turfs/open/_open.dm @@ -213,49 +213,58 @@ movable_content.wash(CLEAN_WASH) return TRUE -/turf/open/handle_slip(mob/living/carbon/slipper, knockdown_amount, obj/O, lube, paralyze_amount, force_drop) +/turf/open/handle_slip(mob/living/carbon/slipper, knockdown_amount, obj/slippable, lube, paralyze_amount, force_drop) if(slipper.movement_type & (FLYING | FLOATING)) return FALSE - if(has_gravity(src)) - var/obj/buckled_obj - if(slipper.buckled) - buckled_obj = slipper.buckled - if(!(lube&GALOSHES_DONT_HELP)) //can't slip while buckled unless it's lube. - return FALSE - else - if(!(lube & SLIP_WHEN_CRAWLING) && (slipper.body_position == LYING_DOWN || !(slipper.status_flags & CANKNOCKDOWN))) // can't slip unbuckled mob if they're lying or can't fall. - return FALSE - if(slipper.m_intent == MOVE_INTENT_WALK && (lube&NO_SLIP_WHEN_WALKING)) - return FALSE - if(!(lube&SLIDE_ICE)) - to_chat(slipper, span_notice("You slipped[ O ? " on the [O.name]" : ""]!")) - playsound(slipper.loc, 'sound/misc/slip.ogg', 50, TRUE, -3) + if(!has_gravity(src)) + return FALSE - SEND_SIGNAL(slipper, COMSIG_ON_CARBON_SLIP) - slipper.add_mood_event("slipped", /datum/mood_event/slipped) - if(force_drop) - for(var/obj/item/I in slipper.held_items) - slipper.accident(I) + var/slide_distance = 4 + if(HAS_TRAIT(slipper, TRAIT_CURSED)) + if(!(lube & SLIDE_ICE)) // Ensures we are at least sliding + lube |= SLIDE + slide_distance = rand(5, 9) - var/olddir = slipper.dir - slipper.moving_diagonally = 0 //If this was part of diagonal move slipping will stop it. - if(!(lube & SLIDE_ICE)) - slipper.Knockdown(knockdown_amount) - slipper.Paralyze(paralyze_amount) - slipper.stop_pulling() - else - slipper.Knockdown(20) + var/obj/buckled_obj + if(slipper.buckled) + buckled_obj = slipper.buckled + if(!(lube & GALOSHES_DONT_HELP)) //can't slip while buckled unless it's lube. + return FALSE + else + if(!(lube & SLIP_WHEN_CRAWLING) && (slipper.body_position == LYING_DOWN || !(slipper.status_flags & CANKNOCKDOWN))) // can't slip unbuckled mob if they're lying or can't fall. + return FALSE + if(slipper.m_intent == MOVE_INTENT_WALK && (lube & NO_SLIP_WHEN_WALKING)) + return FALSE - if(buckled_obj) - buckled_obj.unbuckle_mob(slipper) - lube |= SLIDE_ICE + if(!(lube & SLIDE_ICE)) + to_chat(slipper, span_notice("You slipped[ slippable ? " on the [slippable.name]" : ""]!")) + playsound(slipper.loc, 'sound/misc/slip.ogg', 50, TRUE, -3) - var/turf/target = get_ranged_target_turf(slipper, olddir, 4) - if(lube & SLIDE) - slipper.AddComponent(/datum/component/force_move, target, TRUE) - else if(lube&SLIDE_ICE) - slipper.AddComponent(/datum/component/force_move, target, FALSE)//spinning would be bad for ice, fucks up the next dir - return TRUE + SEND_SIGNAL(slipper, COMSIG_ON_CARBON_SLIP) + slipper.add_mood_event("slipped", /datum/mood_event/slipped) + if(force_drop) + for(var/obj/item/item in slipper.held_items) + slipper.accident(item) + + var/olddir = slipper.dir + slipper.moving_diagonally = 0 //If this was part of diagonal move slipping will stop it. + if(!(lube & SLIDE_ICE)) + slipper.Knockdown(knockdown_amount) + slipper.Paralyze(paralyze_amount) + slipper.stop_pulling() + else + slipper.Knockdown(20) + + if(buckled_obj) + buckled_obj.unbuckle_mob(slipper) + lube |= SLIDE_ICE + + var/turf/target = get_ranged_target_turf(slipper, olddir, slide_distance) + if(lube & SLIDE) + slipper.AddComponent(/datum/component/force_move, target, TRUE) + else if(lube & SLIDE_ICE) + slipper.AddComponent(/datum/component/force_move, target, FALSE)//spinning would be bad for ice, fucks up the next dir + return TRUE /turf/open/proc/MakeSlippery(wet_setting = TURF_WET_WATER, min_wet_time = 0, wet_time_to_add = 0, max_wet_time = MAXIMUM_WET_TIME, permanent) AddComponent(/datum/component/wet_floor, wet_setting, min_wet_time, wet_time_to_add, max_wet_time, permanent) @@ -306,7 +315,7 @@ if(!used_tiles.use(1)) balloon_alert(user, "need a floor tile to build!") return - + playsound(src, 'sound/weapons/genhit.ogg', 50, TRUE) var/turf/open/floor/plating/new_plating = PlaceOnTop(/turf/open/floor/plating, flags = CHANGETURF_INHERIT_AIR) if(lattice) diff --git a/code/modules/admin/smites/bad_luck.dm b/code/modules/admin/smites/bad_luck.dm index 083b5015a95..a5c00aea43d 100644 --- a/code/modules/admin/smites/bad_luck.dm +++ b/code/modules/admin/smites/bad_luck.dm @@ -14,4 +14,10 @@ /datum/smite/bad_luck/effect(client/user, mob/living/target) . = ..() - target.AddComponent(/datum/component/omen, silent, null, permanent) + target.AddComponent(/datum/component/omen/smite, permanent = permanent) + + if(silent) + return + to_chat(target, span_warning("You get a bad feeling...")) + if(permanent) + to_chat(target, span_warning("A very bad feeling... As if malevolent forces are watching you...")) diff --git a/code/modules/cargo/coupon.dm b/code/modules/cargo/coupon.dm index dba06acb96f..fc1c3a6cc65 100644 --- a/code/modules/cargo/coupon.dm +++ b/code/modules/cargo/coupon.dm @@ -15,20 +15,43 @@ /obj/item/coupon/proc/generate(rig_omen=FALSE) discounted_pack = pick(subtypesof(/datum/supply_pack/goody)) var/list/chances = list("0.10" = 4, "0.15" = 8, "0.20" = 10, "0.25" = 8, "0.50" = 4, COUPON_OMEN = 1) + if(rig_omen) discount_pct_off = COUPON_OMEN else discount_pct_off = pick_weight(chances) - if(discount_pct_off == COUPON_OMEN) - name = "coupon - fuck you" - desc = "The small text reads, 'You will be slaughtered'... That doesn't sound right, does it?" - if(ismob(loc)) - var/mob/M = loc - to_chat(M, span_warning("The coupon reads 'fuck you' in large, bold text... is- is that a prize, or?")) - M.AddComponent(/datum/component/omen, TRUE, src) - else + + if(discount_pct_off != COUPON_OMEN) discount_pct_off = text2num(discount_pct_off) name = "coupon - [round(discount_pct_off * 100)]% off [initial(discounted_pack.name)]" + return + + name = "coupon - fuck you" + desc = "The small text reads, 'You will be slaughtered'... That doesn't sound right, does it?" + if(!ismob(loc)) + return FALSE + + var/mob/cursed = loc + to_chat(cursed, span_warning("The coupon reads 'fuck you' in large, bold text... is- is that a prize, or?")) + + if(!cursed.GetComponent(/datum/component/omen)) + cursed.AddComponent(/datum/component/omen) + return TRUE + if(HAS_TRAIT(cursed, TRAIT_CURSED)) + to_chat(cursed, span_warning("What a horrible night... To have a curse!")) + addtimer(CALLBACK(src, PROC_REF(curse_heart), cursed), 5 SECONDS, TIMER_UNIQUE | TIMER_STOPPABLE) + +/// Play stupid games, win stupid prizes +/obj/item/coupon/proc/curse_heart(mob/living/cursed) + if(!iscarbon(cursed)) + cursed.gib() + return TRUE + + var/mob/living/carbon/player = cursed + INVOKE_ASYNC(player, TYPE_PROC_REF(/mob, emote), "scream") + to_chat(player, span_mind_control("What could that coupon mean?")) + to_chat(player, span_userdanger("...The suspense is killing you!")) + player.set_heartattack(status = TRUE) /obj/item/coupon/attack_atom(obj/O, mob/living/user, params) if(!istype(O, /obj/machinery/computer/cargo)) diff --git a/code/modules/food_and_drinks/machinery/microwave.dm b/code/modules/food_and_drinks/machinery/microwave.dm index 951fe83e71a..e8afafa9673 100644 --- a/code/modules/food_and_drinks/machinery/microwave.dm +++ b/code/modules/food_and_drinks/machinery/microwave.dm @@ -382,6 +382,9 @@ playsound(src, 'sound/machines/buzz-sigh.ogg', 50, FALSE) return + if(HAS_TRAIT(cooker, TRAIT_CURSED) && prob(7)) + muck() + return if(prob(max((5 / efficiency) - 5, dirty * 5))) //a clean unupgraded microwave has no risk of failure muck() return @@ -467,12 +470,16 @@ metal_amount += (cooked_item.custom_materials?[GET_MATERIAL_REF(/datum/material/iron)] || 0) + if(HAS_TRAIT(cooker, TRAIT_CURSED) && prob(5)) + spark() + broken = REALLY_BROKEN + explosion(src, light_impact_range = 2, flame_range = 1) + if(metal_amount) spark() broken = REALLY_BROKEN - if(prob(max(metal_amount / 2, 33))) + if(HAS_TRAIT(cooker, TRAIT_CURSED) || prob(max(metal_amount / 2, 33))) // If we're unlucky and have metal, we're guaranteed to explode explosion(src, heavy_impact_range = 1, light_impact_range = 2) - else dump_inventory_contents() diff --git a/code/modules/mob/living/carbon/death.dm b/code/modules/mob/living/carbon/death.dm index 56700223ada..56661d82c8f 100644 --- a/code/modules/mob/living/carbon/death.dm +++ b/code/modules/mob/living/carbon/death.dm @@ -66,8 +66,10 @@ organs.forceMove(Tsec) organs.throw_at(get_edge_target_turf(src,pick(GLOB.alldirs)),rand(1,3),5) -/mob/living/carbon/spread_bodyparts() - for(var/X in bodyparts) - var/obj/item/bodypart/BP = X - BP.drop_limb() - BP.throw_at(get_edge_target_turf(src,pick(GLOB.alldirs)),rand(1,3),5) +/// Launches all bodyparts away from the mob. skip_head will keep the head attached. +/mob/living/carbon/spread_bodyparts(skip_head = FALSE) + for(var/obj/item/bodypart/part as anything in bodyparts) + if(skip_head && part.body_zone == BODY_ZONE_HEAD) + continue + part.drop_limb() + part.throw_at(get_edge_target_turf(src, pick(GLOB.alldirs)), rand(1,3), 5) diff --git a/code/modules/projectiles/guns/ballistic/revolver.dm b/code/modules/projectiles/guns/ballistic/revolver.dm index c66aa9dd21d..d19abfc84bb 100644 --- a/code/modules/projectiles/guns/ballistic/revolver.dm +++ b/code/modules/projectiles/guns/ballistic/revolver.dm @@ -229,6 +229,9 @@ result = (chambered ? "lost" : "won")) if(chambered) + if(HAS_TRAIT(user, TRAIT_CURSED)) // I cannot live, I cannot die, trapped in myself, body my holding cell. + to_chat(user, span_warning("What a horrible night... To have a curse!")) + return var/obj/item/ammo_casing/AC = chambered if(AC.fire_casing(user, user, params, distro = 0, quiet = 0, zone_override = null, spread = 0, fired_from = src)) playsound(user, fire_sound, fire_sound_volume, vary_fire_sound) diff --git a/code/modules/religion/sparring/sparring_datum.dm b/code/modules/religion/sparring/sparring_datum.dm index 12ff983e3d0..00aac29a355 100644 --- a/code/modules/religion/sparring/sparring_datum.dm +++ b/code/modules/religion/sparring/sparring_datum.dm @@ -208,7 +208,7 @@ switch(pick(possible_punishments)) if(PUNISHMENT_OMEN) to_chat(interfering, span_warning("You get a bad feeling... for interfering with [chaplain]'s sparring match...")) - interfering.AddComponent(/datum/component/omen, TRUE, null, FALSE) + interfering.AddComponent(/datum/component/omen) if(PUNISHMENT_LIGHTNING) to_chat(interfering, span_warning("[GLOB.deity] has punished you for interfering with [chaplain]'s sparring match!")) lightningbolt(interfering)