diff --git a/_maps/templates/lazy_templates/wizard_den.dmm b/_maps/templates/lazy_templates/wizard_den.dmm index 025f9e8cdd00..75ee8f52454b 100644 --- a/_maps/templates/lazy_templates/wizard_den.dmm +++ b/_maps/templates/lazy_templates/wizard_den.dmm @@ -427,7 +427,7 @@ /turf/open/floor/wood, /area/centcom/wizard_station) "SD" = ( -/obj/structure/bookcase/random/reference, +/obj/structure/bookcase/random/reference/wizard, /turf/open/floor/engine/cult, /area/centcom/wizard_station) "Tl" = ( diff --git a/code/__DEFINES/combat.dm b/code/__DEFINES/combat.dm index 040930e52056..03b8514e0cf1 100644 --- a/code/__DEFINES/combat.dm +++ b/code/__DEFINES/combat.dm @@ -67,12 +67,27 @@ #define EFFECT_PARALYZE "paralyze" #define EFFECT_IMMOBILIZE "immobilize" //Bitflags defining which status effects could be or are inflicted on a mob +/// If set, this mob can be stunned. #define CANSTUN (1<<0) +/// If set, this mob can be knocked down (or stamcrit) #define CANKNOCKDOWN (1<<1) +/// If set, this mob can be knocked unconscious via status effect. +/// NOTE, does not mean immune to sleep. Unconscious and sleep are two different things. +/// NOTE, does not relate to the unconscious stat either. Only the status effect. #define CANUNCONSCIOUS (1<<2) +/// If set, this mob can be grabbed or pushed when bumped into #define CANPUSH (1<<3) +/// Mob godmode. Prevents most statuses and damage from being taken, but is more often than not a crapshoot. Use with caution. #define GODMODE (1<<4) +DEFINE_BITFIELD(status_flags, list( + "CAN STUN" = CANSTUN, + "CAN KNOCKDOWN" = CANKNOCKDOWN, + "CAN UNCONSCIOUS" = CANUNCONSCIOUS, + "CAN PUSH" = CANPUSH, + "GOD MODE" = GODMODE, +)) + //Health Defines #define HEALTH_THRESHOLD_CRIT 0 #define HEALTH_THRESHOLD_FULLCRIT -30 diff --git a/code/__DEFINES/dcs/signals/signals_mob/signals_mob_living.dm b/code/__DEFINES/dcs/signals/signals_mob/signals_mob_living.dm index d737e448ecb3..63fd640d77f9 100644 --- a/code/__DEFINES/dcs/signals/signals_mob/signals_mob_living.dm +++ b/code/__DEFINES/dcs/signals/signals_mob/signals_mob_living.dm @@ -73,10 +73,14 @@ #define COMSIG_LIVING_STATUS_PARALYZE "living_paralyze" ///from base of mob/living/Immobilize() (amount, ignore_canstun) #define COMSIG_LIVING_STATUS_IMMOBILIZE "living_immobilize" +///from base of mob/living/incapacitate() (amount, ignore_canstun) +#define COMSIG_LIVING_STATUS_INCAPACITATE "living_incapacitate" ///from base of mob/living/Unconscious() (amount, ignore_canstun) #define COMSIG_LIVING_STATUS_UNCONSCIOUS "living_unconscious" ///from base of mob/living/Sleeping() (amount, ignore_canstun) #define COMSIG_LIVING_STATUS_SLEEP "living_sleeping" +/// from mob/living/check_stun_immunity(): (check_flags) +#define COMSIG_LIVING_GENERIC_STUN_CHECK "living_check_stun" #define COMPONENT_NO_STUN (1<<0) //For all of them ///from base of /mob/living/can_track(): (mob/user) #define COMSIG_LIVING_CAN_TRACK "mob_cantrack" diff --git a/code/__DEFINES/dcs/signals/signals_wizard.dm b/code/__DEFINES/dcs/signals/signals_wizard.dm index a4b8f0755c42..5cdebf4f0622 100644 --- a/code/__DEFINES/dcs/signals/signals_wizard.dm +++ b/code/__DEFINES/dcs/signals/signals_wizard.dm @@ -1,4 +1,4 @@ -/// Signal sent when we finish invoking a rune +/// Signal sent when we finish invoking a rune. Will also send the amount of cheese sacrificed on the rune : (cheese_sacrificed) #define COMSIG_GRAND_RUNE_COMPLETE "grand rune complete" /// Signal sent when we finish 7 grand rituals #define COMSIG_GRAND_RITUAL_FINAL_COMPLETE "grand ritual finale complete" diff --git a/code/__DEFINES/traits.dm b/code/__DEFINES/traits.dm index d18c8785b40d..91b5b6c2e323 100644 --- a/code/__DEFINES/traits.dm +++ b/code/__DEFINES/traits.dm @@ -191,6 +191,8 @@ Remember to update _globalvars/traits.dm if you're adding/removing/renaming trai #define TRAIT_DISFIGURED "disfigured" /// Tracks whether we're gonna be a baby alien's mummy. #define TRAIT_XENO_HOST "xeno_host" +/// This mob is immune to stun causing status effects and stamcrit. +/// Prefer to use [/mob/living/proc/check_stun_immunity] over checking for this trait exactly. #define TRAIT_STUNIMMUNE "stun_immunity" #define TRAIT_BATON_RESISTANCE "baton_resistance" /// Anti Dual-baton cooldown bypass exploit. @@ -671,6 +673,8 @@ Remember to update _globalvars/traits.dm if you're adding/removing/renaming trai #define TRAIT_CUSTOM_TAP_SOUND "no_tap_sound" /// Makes the feedback message when someone else is putting this item on you more noticeable #define TRAIT_DANGEROUS_OBJECT "dangerous_object" +/// determines whether or not objects are haunted and teleport/attack randomly +#define TRAIT_HAUNTED "haunted" //quirk traits #define TRAIT_ALCOHOL_TOLERANCE "alcohol_tolerance" @@ -911,7 +915,6 @@ Remember to update _globalvars/traits.dm if you're adding/removing/renaming trai #define CHANGELING_DRAIN "drain" /// changelings with this trait can no longer talk over the hivemind #define CHANGELING_HIVEMIND_MUTE "ling_mute" -#define HIGHLANDER "highlander" #define TRAIT_HULK "hulk" #define STASIS_MUTE "stasis" #define GENETICS_SPELL "genetics_spell" @@ -921,6 +924,7 @@ Remember to update _globalvars/traits.dm if you're adding/removing/renaming trai #define NANITES_TRAIT "nanites" #define FLASHLIGHT_EYES "flashlight_eyes" #define IMPURE_OCULINE "impure_oculine" +#define HAUNTIUM_REAGENT_TRAIT "hauntium_reagent_trait" #define TRAIT_SANTA "santa" #define SCRYING_ORB "scrying-orb" #define ABDUCTOR_ANTAGONIST "abductor-antagonist" @@ -1019,6 +1023,8 @@ Remember to update _globalvars/traits.dm if you're adding/removing/renaming trai #define SPEED_TRAIT "speed_trait" /// Trait given to mobs that have been autopsied #define AUTOPSY_TRAIT "autopsy_trait" +/// Trait given by [/datum/status_effect/blessing_of_insanity] +#define MAD_WIZARD_TRAIT "mad_wizard_trait" /** diff --git a/code/_globalvars/traits.dm b/code/_globalvars/traits.dm index ee663ac6c6fe..55326d5633ef 100644 --- a/code/_globalvars/traits.dm +++ b/code/_globalvars/traits.dm @@ -219,6 +219,7 @@ GLOBAL_LIST_INIT(traits_by_type, list( "TRAIT_APC_SHOCKING" = TRAIT_APC_SHOCKING, "TRAIT_UNCATCHABLE" = TRAIT_UNCATCHABLE, "TRAIT_DANGEROUS_OBJECT" = TRAIT_DANGEROUS_OBJECT, + "TRAIT_HAUNTED" = TRAIT_HAUNTED, ), /atom = list( "TRAIT_KEEP_TOGETHER" = TRAIT_KEEP_TOGETHER, diff --git a/code/datums/ai/monkey/monkey_behaviors.dm b/code/datums/ai/monkey/monkey_behaviors.dm index bce880ced7e9..e2a2a4ae1f75 100644 --- a/code/datums/ai/monkey/monkey_behaviors.dm +++ b/code/datums/ai/monkey/monkey_behaviors.dm @@ -318,7 +318,7 @@ if(possible_enemy == controller.pawn) continue // don't target ourselves if(!enemies[possible_enemy]) //We don't hate this creature! But we might still attack it! - if(!controller.blackboard[BB_MONKEY_AGGRESSIVE]) //We are not aggressive either, so we won't attack! + if(faction_check(possible_enemy.faction, list(FACTION_MONKEY, FACTION_JUNGLE), exact_match = FALSE) && !controller.blackboard[BB_MONKEY_TARGET_MONKEYS]) // do not target your team. includes monkys gorillas etc. continue if(HAS_AI_CONTROLLER_TYPE(possible_enemy, /datum/ai_controller/monkey) && !controller.blackboard[BB_MONKEY_TARGET_MONKEYS]) //Do not target poor monkes continue diff --git a/code/datums/ai/monkey/monkey_controller.dm b/code/datums/ai/monkey/monkey_controller.dm index e365a2f3bcb1..fdd82c0d3bd7 100644 --- a/code/datums/ai/monkey/monkey_controller.dm +++ b/code/datums/ai/monkey/monkey_controller.dm @@ -52,7 +52,9 @@ have ways of interacting with a specific mob and control it. . = ..() if(. & AI_CONTROLLER_INCOMPATIBLE) return + pawn = new_pawn set_blackboard_key(BB_MONKEY_AGGRESSIVE, TRUE) //Angry + set_trip_mode(mode = FALSE) /datum/ai_controller/monkey/TryPossessPawn(atom/new_pawn) if(!isliving(new_pawn)) diff --git a/code/datums/components/twohanded.dm b/code/datums/components/twohanded.dm index a0c491a27a50..756f30bd644d 100644 --- a/code/datums/components/twohanded.dm +++ b/code/datums/components/twohanded.dm @@ -148,13 +148,6 @@ /datum/component/two_handed/proc/wield(mob/living/carbon/user) if(wielded) return - if(ismonkey(user)) - if(require_twohands) - to_chat(user, span_notice("[parent] is too heavy and cumbersome for you to carry!")) - user.dropItemToGround(parent, force=TRUE) - else - to_chat(user, span_notice("It's too heavy for you to wield fully.")) - return if(user.get_inactive_held_item()) if(require_twohands) to_chat(user, span_notice("[parent] is too cumbersome to carry in one hand!")) diff --git a/code/datums/elements/haunted.dm b/code/datums/elements/haunted.dm index 1ed7f325294b..d678083dd199 100644 --- a/code/datums/elements/haunted.dm +++ b/code/datums/elements/haunted.dm @@ -24,3 +24,13 @@ REMOVE_TRAIT(master, TRAIT_MOVE_FLYING, ELEMENT_TRAIT(type)) master.RemoveElement(/datum/element/movetype_handler) return ..() + +/atom/movable/proc/make_haunted(source, color) //if not haunted, make haunted + if(!HAS_TRAIT(src, TRAIT_HAUNTED)) + AddElement(/datum/element/haunted, color) + ADD_TRAIT(src, TRAIT_HAUNTED, source) + +/atom/movable/proc/remove_haunted(source) //if haunted, make not haunted + REMOVE_TRAIT(src, TRAIT_HAUNTED, source) + if(!HAS_TRAIT(src, TRAIT_HAUNTED)) + RemoveElement(/datum/element/haunted) diff --git a/code/datums/materials/hauntium.dm b/code/datums/materials/hauntium.dm index 9a4e09f90818..79e254417208 100644 --- a/code/datums/materials/hauntium.dm +++ b/code/datums/materials/hauntium.dm @@ -15,10 +15,8 @@ /datum/material/hauntium/on_applied_obj(obj/o, amount, material_flags) . = ..() - if(isitem(o)) - o.AddElement(/datum/element/haunted) + o.make_haunted(INNATE_TRAIT, "#f8f8ff") /datum/material/hauntium/on_removed_obj(obj/o, amount, material_flags) . = ..() - if(isitem(o)) - o.RemoveElement(/datum/element/haunted) + o.remove_haunted(INNATE_TRAIT) diff --git a/code/datums/mood_events/drug_events.dm b/code/datums/mood_events/drug_events.dm index 66518199122f..1ca2f5abbe3c 100644 --- a/code/datums/mood_events/drug_events.dm +++ b/code/datums/mood_events/drug_events.dm @@ -107,3 +107,8 @@ /datum/mood_event/nicotine_withdrawal_severe description = "Head pounding. Cold sweating. Feeling anxious. Need a smoke to calm down!" mood_change = -8 + +/datum/mood_event/hauntium_spirits + description = "I feel my soul degrading!" + mood_change = -8 + timeout = 8 MINUTES diff --git a/code/datums/mood_events/generic_negative_events.dm b/code/datums/mood_events/generic_negative_events.dm index d4d50d5a29a6..ef4cec4ea467 100644 --- a/code/datums/mood_events/generic_negative_events.dm +++ b/code/datums/mood_events/generic_negative_events.dm @@ -409,3 +409,9 @@ /datum/mood_event/unsatisfied_nomad description = "I've been here too long! I want to go out and explore space!" mood_change = -3 + +///Wizard cheesy grand finale - what everyone but the wizard gets +/datum/mood_event/madness_despair + description = "UNWORTHY, UNWORTHY, UNWORTHY!!!" + mood_change = -200 + special_screen_obj = "mood_despair" diff --git a/code/datums/mood_events/generic_positive_events.dm b/code/datums/mood_events/generic_positive_events.dm index ebac71a6d4cb..18432d12ef4d 100644 --- a/code/datums/mood_events/generic_positive_events.dm +++ b/code/datums/mood_events/generic_positive_events.dm @@ -359,3 +359,17 @@ mood_change = 2 special_screen_obj = "birthday" special_screen_replace = FALSE + + + + + + + + + + +///Wizard cheesy grand finale - what the wizard gets +/datum/mood_event/madness_elation + description = "Madness truly is the greatest of blessings..." + mood_change = 200 diff --git a/code/datums/mood_events/morbid_events.dm b/code/datums/mood_events/morbid_events.dm new file mode 100644 index 000000000000..cfb8771bff1a --- /dev/null +++ b/code/datums/mood_events/morbid_events.dm @@ -0,0 +1,4 @@ +/datum/mood_event/morbid_hauntium + description = "I feel a better connection with the spirits, I love this!" + mood_change = 3 + timeout = 6 MINUTES diff --git a/code/datums/mutations/hulk.dm b/code/datums/mutations/hulk.dm index 5344cdeaba11..e1784f009c8f 100644 --- a/code/datums/mutations/hulk.dm +++ b/code/datums/mutations/hulk.dm @@ -12,7 +12,7 @@ var/scream_delay = 50 var/last_scream = 0 /// List of traits to add/remove when someone gets this mutation. - var/static/list/mutation_traits = list( + var/list/mutation_traits = list( TRAIT_CHUNKYFINGERS, TRAIT_HULK, TRAIT_IGNOREDAMAGESLOWDOWN, @@ -256,4 +256,17 @@ yeeted_person.throw_at(T, 10, 6, the_hulk, TRUE, TRUE) log_combat(the_hulk, yeeted_person, "has thrown by tail") +/datum/mutation/human/hulk/wizardly + species_allowed = null //yes skeleton/lizard hulk - note that species that dont have skintone changing (like skellies) get custom handling + health_req = 0 + instability = 0 + scream_delay = 2.5 SECONDS // halved to be more annoying (spell doesn't last long anyways) + /// List of traits to add/remove when someone gets this mutation. + mutation_traits = list( + TRAIT_HULK, + TRAIT_IGNOREDAMAGESLOWDOWN, + TRAIT_PUSHIMMUNE, + TRAIT_STUNIMMUNE, + ) // no chunk + #undef HULK_TAILTHROW_STEPS diff --git a/code/datums/mutations/sight.dm b/code/datums/mutations/sight.dm index 233e9ca338d2..b71c6cdd6a14 100644 --- a/code/datums/mutations/sight.dm +++ b/code/datums/mutations/sight.dm @@ -166,7 +166,7 @@ to_chat(source, span_warning("You shoot with your laser eyes!")) source.changeNext_move(CLICK_CD_RANGE) source.newtonian_move(get_dir(target, source)) - var/obj/projectile/beam/laser_eyes/LE = new(source.loc) + var/obj/projectile/beam/laser/laser_eyes/LE = new(source.loc) LE.firer = source LE.def_zone = ran_zone(source.zone_selected) LE.preparePixelProjectile(target, source, modifiers) @@ -174,7 +174,7 @@ playsound(source, 'sound/weapons/taser2.ogg', 75, TRUE) ///Projectile type used by laser eyes -/obj/projectile/beam/laser_eyes +/obj/projectile/beam/laser/laser_eyes name = "beam" icon = 'icons/effects/genetics.dmi' icon_state = "eyelasers" diff --git a/code/datums/status_effects/buffs.dm b/code/datums/status_effects/buffs.dm index f0046785a94c..72dece18b8bd 100644 --- a/code/datums/status_effects/buffs.dm +++ b/code/datums/status_effects/buffs.dm @@ -22,10 +22,16 @@ return ..() /datum/status_effect/his_grace/on_apply() - owner.log_message("gained His Grace's stun immunity", LOG_ATTACK) - owner.add_stun_absorption("hisgrace", INFINITY, 3, null, "His Grace protects you from the stun!") + owner.add_stun_absorption( + source = id, + priority = 3, + self_message = span_boldwarning("His Grace protects you from the stun!"), + ) return ..() +/datum/status_effect/his_grace/on_remove() + owner.remove_stun_absorption(id) + /datum/status_effect/his_grace/tick() bloodlust = 0 var/graces = 0 @@ -45,11 +51,6 @@ owner.adjustOxyLoss(-(grace_heal * 2)) owner.adjustCloneLoss(-grace_heal) -/datum/status_effect/his_grace/on_remove() - owner.log_message("lost His Grace's stun immunity", LOG_ATTACK) - if(islist(owner.stun_absorption) && owner.stun_absorption["hisgrace"]) - owner.stun_absorption -= "hisgrace" - /datum/status_effect/wish_granters_gift //Fully revives after ten seconds. id = "wish_granters_gift" @@ -112,34 +113,30 @@ icon_state = "blooddrunk" /datum/status_effect/blooddrunk/on_apply() - . = ..() - if(.) - ADD_TRAIT(owner, TRAIT_IGNOREDAMAGESLOWDOWN, BLOODDRUNK_TRAIT) - if(ishuman(owner)) - var/mob/living/carbon/human/H = owner - H.physiology.brute_mod *= 0.1 - H.physiology.burn_mod *= 0.1 - H.physiology.tox_mod *= 0.1 - H.physiology.oxy_mod *= 0.1 - H.physiology.clone_mod *= 0.1 - H.physiology.stamina_mod *= 0.1 - owner.log_message("gained blood-drunk stun immunity", LOG_ATTACK) - owner.add_stun_absorption("blooddrunk", INFINITY, 4) - owner.playsound_local(get_turf(owner), 'sound/effects/singlebeat.ogg', 40, 1, use_reverb = FALSE) + ADD_TRAIT(owner, TRAIT_IGNOREDAMAGESLOWDOWN, BLOODDRUNK_TRAIT) + if(ishuman(owner)) + var/mob/living/carbon/human/human_owner = owner + human_owner.physiology.brute_mod *= 0.1 + human_owner.physiology.burn_mod *= 0.1 + human_owner.physiology.tox_mod *= 0.1 + human_owner.physiology.oxy_mod *= 0.1 + human_owner.physiology.clone_mod *= 0.1 + human_owner.physiology.stamina_mod *= 0.1 + owner.add_stun_absorption(source = id, priority = 4) + owner.playsound_local(get_turf(owner), 'sound/effects/singlebeat.ogg', 40, 1, use_reverb = FALSE) + return TRUE /datum/status_effect/blooddrunk/on_remove() if(ishuman(owner)) - var/mob/living/carbon/human/H = owner - H.physiology.brute_mod *= 10 - H.physiology.burn_mod *= 10 - H.physiology.tox_mod *= 10 - H.physiology.oxy_mod *= 10 - H.physiology.clone_mod *= 10 - H.physiology.stamina_mod *= 10 - owner.log_message("lost blood-drunk stun immunity", LOG_ATTACK) - REMOVE_TRAIT(owner, TRAIT_IGNOREDAMAGESLOWDOWN, BLOODDRUNK_TRAIT); - if(islist(owner.stun_absorption) && owner.stun_absorption["blooddrunk"]) - owner.stun_absorption -= "blooddrunk" + var/mob/living/carbon/human/human_owner = owner + human_owner.physiology.brute_mod *= 10 + human_owner.physiology.burn_mod *= 10 + human_owner.physiology.tox_mod *= 10 + human_owner.physiology.oxy_mod *= 10 + human_owner.physiology.clone_mod *= 10 + human_owner.physiology.stamina_mod *= 10 + REMOVE_TRAIT(owner, TRAIT_IGNOREDAMAGESLOWDOWN, BLOODDRUNK_TRAIT) + owner.remove_stun_absorption(id) //Used by changelings to rapidly heal //Heals 10 brute and oxygen damage every second, and 5 fire @@ -486,3 +483,52 @@ name = "Nest Vitalization" desc = "The resin seems to pulsate around you. It seems to be sustaining your vital functions. You feel ill..." icon_state = "nest_life" + +/** + * Granted to wizards upon satisfying the cheese sacrifice during grand rituals. + * Halves incoming damage and makes the owner stun immune, damage slow immune, levitating(even in space and hyperspace!) and glowing. + */ +/datum/status_effect/blessing_of_insanity + id = "blessing_of_insanity" + duration = -1 + tick_interval = -1 + alert_type = /atom/movable/screen/alert/status_effect/blessing_of_insanity + +/atom/movable/screen/alert/status_effect/blessing_of_insanity + name = "Blessing of Insanity" + desc = "Your devotion to madness has improved your resilience to all damage and you gain the power to levitate!" + //no screen alert - the gravity already throws one + +/datum/status_effect/blessing_of_insanity/on_apply() + if(ishuman(owner)) + var/mob/living/carbon/human/human_owner = owner + var/datum/physiology/owner_physiology = human_owner.physiology + owner_physiology.brute_mod *= 0.5 + owner_physiology.burn_mod *= 0.5 + owner_physiology.tox_mod *= 0.5 + owner_physiology.oxy_mod *= 0.5 + owner_physiology.clone_mod *= 0.5 + owner_physiology.stamina_mod *= 0.5 + owner.add_filter("mad_glow", 2, list("type" = "outline", "color" = "#eed811c9", "size" = 2)) + owner.AddElement(/datum/element/forced_gravity, 0) + owner.AddElement(/datum/element/simple_flying) + owner.add_stun_absorption(source = id, priority = 4) + add_traits(list(TRAIT_IGNOREDAMAGESLOWDOWN, TRAIT_FREE_HYPERSPACE_MOVEMENT), MAD_WIZARD_TRAIT) + owner.playsound_local(get_turf(owner), 'sound/chemistry/ahaha.ogg', vol = 100, vary = TRUE, use_reverb = TRUE) + return TRUE + +/datum/status_effect/blessing_of_insanity/on_remove() + if(ishuman(owner)) + var/mob/living/carbon/human/human_owner = owner + var/datum/physiology/owner_physiology = human_owner.physiology + owner_physiology.brute_mod *= 2 + owner_physiology.burn_mod *= 2 + owner_physiology.tox_mod *= 2 + owner_physiology.oxy_mod *= 2 + owner_physiology.clone_mod *= 2 + owner_physiology.stamina_mod *= 2 + owner.remove_filter("mad_glow") + owner.RemoveElement(/datum/element/forced_gravity, 0) + owner.RemoveElement(/datum/element/simple_flying) + owner.remove_stun_absorption(id) + remove_traits(list(TRAIT_IGNOREDAMAGESLOWDOWN, TRAIT_FREE_HYPERSPACE_MOVEMENT), MAD_WIZARD_TRAIT) diff --git a/code/datums/status_effects/buffs/stun_asorption.dm b/code/datums/status_effects/buffs/stun_asorption.dm new file mode 100644 index 000000000000..d68f2f7408cc --- /dev/null +++ b/code/datums/status_effects/buffs/stun_asorption.dm @@ -0,0 +1,235 @@ +/** + * # Stun absorption + * + * A status effect effectively functions as [TRAIT_STUNIMMUNE], but with additional effects tied to it, + * such as showing a message on trigger / examine, or only blocking a limited amount of stuns. + * + * Apply this via [/mob/living/proc/add_stun_absorption]. If you do not supply a duration, + * remove this via [/mob/living/proc/remove_stun_absorption]. + */ +/datum/status_effect/stun_absorption + id = "absorb_stun" + tick_interval = -1 + alert_type = null + status_type = STATUS_EFFECT_MULTIPLE + + /// The string key sourcer of the stun absorption, used for logging + var/source + /// The priority of the stun absorption. Used so that multiple sources will not trigger at once. + /// This number is arbitrary but try to keep in sane / in line with other sources that exist. + var/priority = -1 + /// How many total seconds of stuns that have been blocked. + var/seconds_of_stuns_absorbed = 0 SECONDS + /// The max number of seconds we can block before self-deleting. + var/max_seconds_of_stuns_blocked = INFINITY + /// The message shown via visible message to all nearby mobs when the effect triggers. + var/shown_message + /// The message shown to the owner when the effect triggers. + var/self_message + /// Message shown on anyone examining the owner. + var/examine_message + + /// Static list of all generic "stun received " signals that we will react to and block. + /// These all have the same arguments sent, so we can handle them all via the same signal handler. + /// Note though that we can register other signals to block effects outside of these if we want. + var/static/list/incapacitation_effect_signals = list( + COMSIG_LIVING_STATUS_IMMOBILIZE, + COMSIG_LIVING_STATUS_INCAPACITATE, + COMSIG_LIVING_STATUS_KNOCKDOWN, + COMSIG_LIVING_STATUS_PARALYZE, + COMSIG_LIVING_STATUS_STUN, + ) + +/datum/status_effect/stun_absorption/on_creation( + mob/living/new_owner, + source, + duration, + priority = -1, + shown_message, + self_message, + examine_message, + max_seconds_of_stuns_blocked = INFINITY, +) + + if(isnum(duration)) + src.duration = duration + + src.source = source + src.priority = priority + src.shown_message = shown_message + src.self_message = self_message + src.examine_message = examine_message + src.max_seconds_of_stuns_blocked = max_seconds_of_stuns_blocked + + return ..() + +/datum/status_effect/stun_absorption/on_apply() + if(owner.mind || owner.client) + owner.log_message("gained stun absorption (from: [source || "Unknown"])", LOG_ATTACK) + + RegisterSignals(owner, incapacitation_effect_signals, PROC_REF(try_absorb_incapacitating_effect)) + RegisterSignal(owner, COMSIG_LIVING_GENERIC_STUN_CHECK, PROC_REF(try_absorb_generic_effect)) + return TRUE + +/datum/status_effect/stun_absorption/on_remove() + if(owner.mind || owner.client) + owner.log_message("lost stun absorption (from: [source || "Unknown"])", LOG_ATTACK) + + UnregisterSignal(owner, incapacitation_effect_signals) + UnregisterSignal(owner, COMSIG_LIVING_GENERIC_STUN_CHECK) + +/datum/status_effect/stun_absorption/get_examine_text() + return replacetext(examine_message, "%EFFECT_OWNER_THEYRE", owner.p_theyre(TRUE)) + +/** + * Signal proc for generic stun signals being sent, such as [COMSIG_LIVING_STATUS_STUN] or [COMSIG_LIVING_STATUS_KNOCKDOWN]. + * + * When we get stunned, we will try to absorb a number of seconds from the stun, and return [COMPONENT_NO_STUN] if we succeed. + */ +/datum/status_effect/stun_absorption/proc/try_absorb_incapacitating_effect(mob/living/source, amount = 0, ignore_canstun = FALSE) + SIGNAL_HANDLER + + // we blocked a stun this tick that resulting is us qdeling, so stop + if(QDELING(src)) + return NONE + + // Amount less than (or equal to) zero is removing stuns, so we don't want to block that + if(amount <= 0 || ignore_canstun) + return NONE + + if(!absorb_stun(amount)) + return NONE + + return COMPONENT_NO_STUN + +/** + * Signal proc for [COMSIG_LIVING_GENERIC_STUN_CHECK]. (Note, this includes being stamcrit) + * + * Whenever a generic stun check is done against us, we'll just try to block it with "0 second" stun. + * This prevents spam us from showing feedback messages, and is for the generic "can be stunned" check. + */ +/datum/status_effect/stun_absorption/proc/try_absorb_generic_effect(mob/living/source, check_flags) + SIGNAL_HANDLER + + if(QDELING(src)) + return NONE + + // "0 amount" / "0 seconds of stun" is used so no feedback is sent on success + if(!absorb_stun(0)) + return NONE + + return COMPONENT_NO_STUN + +/** + * Absorb a number of seconds of stuns. + * If we hit the max amount of absorption, we will qdel ourself in this proc. + * + * * amount - this is the number of deciseconds being absorbed at once. + * + * Returns TRUE on successful absorption, or FALSE otherwise. + */ +/datum/status_effect/stun_absorption/proc/absorb_stun(amount) + if(owner.stat != CONSCIOUS) + return FALSE + + // Now we gotta check that no other stun absorption we have is blocking us + for(var/datum/status_effect/stun_absorption/similar_effect in owner.status_effects) + if(similar_effect == src) + continue + // they blocked a stun this tick that resulted in them qdeling, so disregard + if(QDELING(similar_effect)) + continue + // if we have another stun absorption with higher priority, + // don't do anything, let them handle it instead + if(similar_effect.priority > priority) + return FALSE + + // At this point, a stun was successfully absorbed + + // Only do effects if the amount was > 0 seconds + if(amount > 0 SECONDS) + // Show the message + if(shown_message) + // We do this replacement meme, instead of just setting it up in creation, + // so that we respect indentity changes done while active + var/really_shown_message = replacetext(shown_message, "%EFFECT_OWNER", "[owner]") + owner.visible_message(really_shown_message, ignored_mobs = owner) + + // Send the self message + if(self_message) + to_chat(owner, self_message) + + // Count seconds absorbed + seconds_of_stuns_absorbed += amount + if(seconds_of_stuns_absorbed >= max_seconds_of_stuns_blocked) + qdel(src) + + return TRUE + +/** + * [proc/apply_status_effect] wrapper specifically for [/datum/status_effect/stun_absorption], + * specifically so that it's easier to apply stun absorptions with named arguments. + * + * If the mob already has a stun absorption from the same source, will not re-apply the effect, + * unless the new effect's priority is higher than the old effect's priority. + * + * Arguments + * * source - the source of the stun absorption. + * * duration - how long does the stun absorption last before it ends? -1 or null = infinite duration + * * priority - what is this effect's priority to other stun absorptions? higher = more priority + * * message - optional, "other message" arg of visible message, shown on trigger. Use %EFFECT_OWNER if you want the owner's name to be inserted. + * * self_message - optional, "self message" arg of visible message, shown on trigger + * * examine_message - optional, what is shown on examine of the mob. + * * max_seconds_of_stuns_blocked - optional, how many seconds of stuns can it block before deleting? the stun that breaks over this number is still blocked, even if it is much higher. + * + * Returns an instance of a stun absorption effect, or NULL if failure + */ +/mob/living/proc/add_stun_absorption( + source, + duration, + priority = -1, + message, + self_message, + examine_message, + max_seconds_of_stuns_blocked = INFINITY, +) + + // Handle duplicate sources + for(var/datum/status_effect/stun_absorption/existing_effect in status_effects) + if(existing_effect.source != source) + continue + + // If an existing effect's priority is greater or equal to our passed priority... + if(existing_effect.priority >= priority) + // don't bother re-applying the effect, and return + return + + // otherwise, delete existing and replace with new + qdel(existing_effect) + + return apply_status_effect( + /datum/status_effect/stun_absorption, + source, + duration, + priority, + message, + self_message, + examine_message, + max_seconds_of_stuns_blocked, + ) + +/** + * Removes all stub absorptions with the passed source. + * + * Returns TRUE if an effect was deleted, FALSE otherwise + */ +/mob/living/proc/remove_stun_absorption(source) + . = FALSE + for(var/datum/status_effect/stun_absorption/effect in status_effects) + if(effect.source != source) + continue + + qdel(effect) + . = TRUE + + return . diff --git a/code/game/objects/items/food/cheese.dm b/code/game/objects/items/food/cheese.dm index 4652017c65b8..ed980bd2a57e 100644 --- a/code/game/objects/items/food/cheese.dm +++ b/code/game/objects/items/food/cheese.dm @@ -56,6 +56,15 @@ /obj/item/food/cheese/wheel/make_bakeable() AddComponent(/datum/component/bakeable, /obj/item/food/baked_cheese, rand(20 SECONDS, 25 SECONDS), TRUE, TRUE) +/** + * Whiffs away cheese that was touched by the chaos entity byond the realm. In layman's terms, deletes the cheese and throws sparks. + * Used in wizard grand rituals' optional cheesy alternative. + */ +/obj/item/food/cheese/wheel/proc/consume_cheese() + visible_message(span_revenwarning("...and is consumed in a vortex of chaos!")) + do_sparks(number = 1, cardinal_only = TRUE, source = get_turf(src)) + qdel(src) + /obj/item/food/cheese/royal name = "royal cheese" desc = "Ascend the throne. Consume the wheel. Feel the POWER." diff --git a/code/game/objects/items/granters/magic/summon_cheese.dm b/code/game/objects/items/granters/magic/summon_cheese.dm new file mode 100644 index 000000000000..668d3be8f9ac --- /dev/null +++ b/code/game/objects/items/granters/magic/summon_cheese.dm @@ -0,0 +1,28 @@ +/obj/item/book/granter/action/spell/summon_cheese + name = "Lusty Xenomorph Maid vol. III - Cheese Bakery" + desc = "Wonderful! Time for a celebration... Cheese for everyone!" + icon_state = "bookcheese" + action_name = "summon cheese" + granted_action = /datum/action/cooldown/spell/conjure/cheese + remarks = list( + "Always forward, never back...", + "Are these pages... cheese slices?..", + "Healthy snacks for unsuspecting victims...", + "I never knew so many types of cheese existed...", + "Madness reeks of goat cheese...", + "Madness tastes of gouda...", + "Madness tastes of parmesan...", + "Time is an artificial construct...", + "Was it order or biscuits?..", + "What's this about sacrificing cheese?!..", + "Who wouldn't like that?..", + "Why cheese, of all things?..", + "Why do I need a reason for everything?..", + ) + +/obj/item/book/granter/action/spell/summon_cheese/recoil(mob/living/user) + to_chat(user, span_warning("\The [src] turns into a wedge of cheese!")) + var/obj/item/food/cheese/wedge/book_cheese = new + user.temporarilyRemoveItemFromInventory(src, force = TRUE) + user.put_in_hands(book_cheese) + qdel(src) diff --git a/code/game/objects/items/stacks/sheets/sheet_types.dm b/code/game/objects/items/stacks/sheets/sheet_types.dm index b983a4ab5746..5d7a3715850f 100644 --- a/code/game/objects/items/stacks/sheets/sheet_types.dm +++ b/code/game/objects/items/stacks/sheets/sheet_types.dm @@ -861,6 +861,7 @@ new /datum/stack_recipe("paper frame door", /obj/structure/mineral_door/paperfra merge_type = /obj/item/stack/sheet/hauntium material_type = /datum/material/hauntium material_modifier = 1 //None of that wussy stuff + grind_results = list(/datum/reagent/hauntium = 20) /obj/item/stack/sheet/hauntium/fifty amount = 50 diff --git a/code/game/objects/items/weaponry.dm b/code/game/objects/items/weaponry.dm index b2fdb93407c6..a5c22653266a 100644 --- a/code/game/objects/items/weaponry.dm +++ b/code/game/objects/items/weaponry.dm @@ -150,12 +150,18 @@ for further reading, please see: https://github.com/tgstation/tgstation/pull/301 /obj/item/claymore/highlander/pickup(mob/living/user) . = ..() to_chat(user, span_notice("The power of Scotland protects you! You are shielded from all stuns and knockdowns.")) - user.add_stun_absorption("highlander", INFINITY, 1, " is protected by the power of Scotland!", "The power of Scotland absorbs the stun!", " is protected by the power of Scotland!") - user.ignore_slowdown(HIGHLANDER) + user.ignore_slowdown(HIGHLANDER_TRAIT) + user.add_stun_absorption( + source = HIGHLANDER_TRAIT, + message = span_warning("%EFFECT_OWNER is protected by the power of Scotland!"), + self_message = span_boldwarning("The power of Scotland absorbs the stun!"), + examine_message = span_warning("%EFFECT_OWNER_THEYRE protected by the power of Scotland!"), + ) /obj/item/claymore/highlander/dropped(mob/living/user) . = ..() - user.unignore_slowdown(HIGHLANDER) + user.unignore_slowdown(HIGHLANDER_TRAIT) + user.remove_stun_absorption(HIGHLANDER_TRAIT) /obj/item/claymore/highlander/examine(mob/user) . = ..() @@ -573,6 +579,7 @@ for further reading, please see: https://github.com/tgstation/tgstation/pull/301 gender = PLURAL icon = 'icons/obj/wizard.dmi' icon_state = "ectoplasm" + grind_results = list(/datum/reagent/hauntium = 25) //can be ground into hauntium /obj/item/ectoplasm/suicide_act(mob/living/user) user.visible_message(span_suicide("[user] is inhaling [src]! It looks like [user.p_theyre()] trying to visit the astral plane!")) diff --git a/code/modules/antagonists/cult/cult_bastard_sword.dm b/code/modules/antagonists/cult/cult_bastard_sword.dm index 30fe655f69ba..5ae594207db7 100644 --- a/code/modules/antagonists/cult/cult_bastard_sword.dm +++ b/code/modules/antagonists/cult/cult_bastard_sword.dm @@ -45,7 +45,14 @@ /obj/item/cult_bastard/proc/on_spin(mob/living/user, duration) var/oldcolor = user.color user.color = "#ff0000" - user.add_stun_absorption("bloody bastard sword", duration, 2, "doesn't even flinch as the sword's power courses through them!", "You shrug off the stun!", " glowing with a blazing red aura!") + user.add_stun_absorption( + source = name, + duration = duration, + priority = 2, + message = span_warning("%EFFECT_OWNER doesn't even flinch as the sword's power courses through [user.p_them()]!"), + self_message = span_boldwarning("You shrug off the stun!"), + examine_message = span_warning("%EFFECT_OWNER_THEYRE glowing with a blazing red aura!"), + ) user.spin(duration, 1) animate(user, color = oldcolor, time = duration, easing = EASE_IN) addtimer(CALLBACK(user, TYPE_PROC_REF(/atom, update_atom_colour)), duration) diff --git a/code/modules/antagonists/highlander/highlander.dm b/code/modules/antagonists/highlander/highlander.dm index c740de9279fd..98659ef19419 100644 --- a/code/modules/antagonists/highlander/highlander.dm +++ b/code/modules/antagonists/highlander/highlander.dm @@ -66,7 +66,7 @@ P.attack_self(H) var/obj/item/card/id/advanced/highlander/W = new(H) W.registered_name = H.real_name - ADD_TRAIT(W, TRAIT_NODROP, HIGHLANDER) + ADD_TRAIT(W, TRAIT_NODROP, HIGHLANDER_TRAIT) W.update_label() W.update_icon() H.equip_to_slot_or_del(W, ITEM_SLOT_ID) diff --git a/code/modules/antagonists/revenant/haunted_item.dm b/code/modules/antagonists/revenant/haunted_item.dm index a4772912a61f..4bee2f6e8f12 100644 --- a/code/modules/antagonists/revenant/haunted_item.dm +++ b/code/modules/antagonists/revenant/haunted_item.dm @@ -34,7 +34,7 @@ if(istype(haunted_item.ai_controller, /datum/ai_controller/haunted)) // already spooky return COMPONENT_INCOMPATIBLE - haunted_item.AddElement(/datum/element/haunted, haunt_color) + haunted_item.make_haunted(MAGIC_TRAIT, haunt_color) if(isnull(haunted_item.ai_controller)) // failed to make spooky! don't go on return COMPONENT_INCOMPATIBLE @@ -71,7 +71,7 @@ // because we want to make sure they always get dealt with no matter how the component is removed if(!isnull(pre_haunt_throwforce)) haunted_item.throwforce = pre_haunt_throwforce - haunted_item.RemoveElement(/datum/element/haunted) + haunted_item.remove_haunted(MAGIC_TRAIT) return ..() /datum/component/haunted_item/RegisterWithParent() diff --git a/code/modules/antagonists/wizard/equipment/soulstone.dm b/code/modules/antagonists/wizard/equipment/soulstone.dm index f93ed8abcff2..0a0746e52992 100644 --- a/code/modules/antagonists/wizard/equipment/soulstone.dm +++ b/code/modules/antagonists/wizard/equipment/soulstone.dm @@ -24,6 +24,7 @@ var/theme = THEME_CULT /// Role check, if any needed var/required_role = /datum/antagonist/cult + grind_results = list(/datum/reagent/hauntium = 25, /datum/reagent/silicon = 10) //can be ground into hauntium /obj/item/soulstone/Initialize(mapload) . = ..() diff --git a/code/modules/antagonists/wizard/equipment/spellbook_entries/offensive.dm b/code/modules/antagonists/wizard/equipment/spellbook_entries/offensive.dm index b23a1461c49a..57aa2ad7af18 100644 --- a/code/modules/antagonists/wizard/equipment/spellbook_entries/offensive.dm +++ b/code/modules/antagonists/wizard/equipment/spellbook_entries/offensive.dm @@ -23,6 +23,14 @@ spell_type = /datum/action/cooldown/spell/touch/smite category = "Offensive"*/ +/datum/spellbook_entry/summon_simians + name = "Summon Simians" + desc = "This spell reaches deep into the elemental plane of bananas (the monkey one, not the clown one), and \ + summons primal monkeys and lesser gorillas that will promptly flip out and attack everything in sight. Fun! \ + Their lesser, easily manipulable minds will be convinced you are one of their allies, but only for a minute. Unless you also are a monkey." + spell_type = /datum/action/cooldown/spell/conjure/simian + category = "Offensive" + /datum/spellbook_entry/blind name = "Blind" desc = "Temporarily blinds a single target." diff --git a/code/modules/antagonists/wizard/grand_ritual/fluff.dm b/code/modules/antagonists/wizard/grand_ritual/fluff.dm new file mode 100644 index 000000000000..8a1e340a84bf --- /dev/null +++ b/code/modules/antagonists/wizard/grand_ritual/fluff.dm @@ -0,0 +1,25 @@ +/** + * Fluff book to hint at the cheesy grand ritual. + */ +/obj/item/book/manual/ancient_parchment + name = "ancient parchment" + icon = 'icons/obj/wizard.dmi' + icon_state = "scroll-ancient" + unique = TRUE + w_class = WEIGHT_CLASS_SMALL + starting_author = "Pelagius the Mad" + starting_title = "Worship and Reverence of the Divine Insanity" + starting_content = {" + + + + + Most of the scroll's contents are unintelligible, plagued with mold, milk stains and a stench of spolied goat cheese so potent,
+ you can barely resist turning your head to retch. What's left of the writings is vague and abstract, as if the author
+ was in a mad dash to pass on their findings.

+ However, the runes they have managed to scribe onto the parchment are oddly untouched by time, and remain distinct.
+ You also discover a schema for a more widely-used Grand Ritual rune, however it is dotted with yellow circles, which in turn are
+ filled with black dots. Are these supposed to be... cheese wheels?..

+ As you finish skimming through the wreck that is this scroll, you hear a faint snicker somewhere beyond your mind's eye...

+ + "} diff --git a/code/modules/antagonists/wizard/grand_ritual/grand_ritual.dm b/code/modules/antagonists/wizard/grand_ritual/grand_ritual.dm index ce1d89edcc61..f3ac9b010710 100644 --- a/code/modules/antagonists/wizard/grand_ritual/grand_ritual.dm +++ b/code/modules/antagonists/wizard/grand_ritual/grand_ritual.dm @@ -1,3 +1,10 @@ +/** + * Total cheese goal to sacrifice to [REDACTED] during wizard grand rituals. + * The easiest way for a wizard to procure cheese is with Summon Cheese spell, which summons 9 per cast. + * The wizard needs to complete 7 rituals, so let's give them some breathing room with cheese offerings. + * 7 * 9 = 63, so the wizard can potentially miss two casts worth of cheese if they summon cheese on each rune. +*/ +#define CHEESE_SACRIFICE_GOAL 50 /** * The Grand Ritual is the Wizard's alternate victory condition * and also a tool to make funny distractions and progress the round state. @@ -30,6 +37,10 @@ var/drew_finale = FALSE /// True while you are drawing a rune, prevents action spamming var/drawing_rune = FALSE + /// Number of cheese sacrificed on previously drawn runes + var/total_cheese_sacrificed = 0 + /// Whether we have sacrificed enough cheese or not + var/total_cheese_goal_met = FALSE /// Weakref to a rune drawn in the current area, if there is one var/datum/weakref/rune @@ -150,13 +161,20 @@ /// Draws the ritual rune /datum/action/cooldown/grand_ritual/proc/draw_rune(turf/target_turf) drawing_rune = TRUE + var/next_rune_typepath = get_appropriate_rune_typepath() target_turf.balloon_alert(owner, "conjuring rune...") - var/obj/effect/temp_visual/wizard_rune/drawing/draw_effect = new(target_turf) + var/draw_effect_typepath = /obj/effect/temp_visual/wizard_rune/drawing + if(next_rune_typepath == /obj/effect/grand_rune/finale/cheesy) + draw_effect_typepath = /obj/effect/temp_visual/wizard_rune/drawing/cheese + var/obj/effect/temp_visual/wizard_rune/drawing/draw_effect = new draw_effect_typepath(target_turf) if(!do_after(owner, 4 SECONDS, target_turf)) target_turf.balloon_alert(owner, "interrupted!") drawing_rune = FALSE qdel(draw_effect) - new /obj/effect/temp_visual/wizard_rune/failed(target_turf) + var/fail_effect_typepath = /obj/effect/temp_visual/wizard_rune/failed + if(next_rune_typepath == /obj/effect/grand_rune/finale/cheesy) + fail_effect_typepath = /obj/effect/temp_visual/wizard_rune/failed/cheese + new fail_effect_typepath(target_turf) return var/evaporated_obstacles = FALSE @@ -176,25 +194,37 @@ playsound(target_turf, 'sound/magic/blind.ogg', 100, TRUE) target_turf.balloon_alert(owner, "rune created") - var/obj/effect/grand_rune/new_rune = create_appropriate_rune(target_turf) + var/obj/effect/grand_rune/new_rune = new next_rune_typepath(target_turf, times_completed) + if(istype(new_rune, /obj/effect/grand_rune/finale)) + drew_finale = TRUE rune = WEAKREF(new_rune) RegisterSignal(new_rune, COMSIG_GRAND_RUNE_COMPLETE, PROC_REF(on_rune_complete)) drawing_rune = FALSE StartCooldown(2 MINUTES) // To put a damper on wizards who have 5 ranks of Teleport /// The seventh rune we spawn is special -/datum/action/cooldown/grand_ritual/proc/create_appropriate_rune(turf/target_turf) +/datum/action/cooldown/grand_ritual/proc/get_appropriate_rune_typepath() if (times_completed < GRAND_RITUAL_FINALE_COUNT - 1) - return new /obj/effect/grand_rune(target_turf, times_completed) + return /obj/effect/grand_rune if (drew_finale) - return new /obj/effect/grand_rune(target_turf, times_completed) - drew_finale = TRUE - return new /obj/effect/grand_rune/finale(target_turf, times_completed) + return /obj/effect/grand_rune + if (total_cheese_sacrificed >= CHEESE_SACRIFICE_GOAL) + return /obj/effect/grand_rune/finale/cheesy + return /obj/effect/grand_rune/finale /// Called when you finish invoking a rune you drew, get ready for another one. -/datum/action/cooldown/grand_ritual/proc/on_rune_complete(atom/source) +/datum/action/cooldown/grand_ritual/proc/on_rune_complete(atom/source, cheese_sacrificed) SIGNAL_HANDLER UnregisterSignal(source, COMSIG_GRAND_RUNE_COMPLETE) + total_cheese_sacrificed += cheese_sacrificed + if(total_cheese_sacrificed >= CHEESE_SACRIFICE_GOAL) + if(!total_cheese_goal_met) + total_cheese_goal_met = TRUE + to_chat(owner, span_revenbignotice("YES! CHEESE! CHEESE FOR EVERYONE! SUCH A GRAND FEAST! YOU SHALL HAVE YOUR PRIZE, MY CHAMPION!!")) + else + to_chat(owner, span_revennotice("You hear maddening laughter as you are hit with an overwhelming odor of fine cheddar...")) + else if (total_cheese_sacrificed) + to_chat(owner, span_revendanger("You please me, mortal. Do continue to send cheese, my feast still needs [CHEESE_SACRIFICE_GOAL - total_cheese_sacrificed] more to be magnificent...")) rune = null times_completed++ set_new_area() @@ -203,8 +233,12 @@ to_chat(owner, span_warning("Your collected power is growing, \ but further rituals will alert your enemies to your position.")) if (GRAND_RITUAL_IMMINENT_FINALE_POTENCY) - to_chat(owner, span_warning("You are overflowing with power! \ - Your next Grand Ritual will allow you to choose a powerful effect, and grant you victory.")) + var/message = "You are overflowing with power! \ + Your next Grand Ritual will allow you to choose a powerful effect, and grant you victory." + if(total_cheese_sacrificed >= CHEESE_SACRIFICE_GOAL) + message = "You are overflowing with chaotic energies! \ + Your next Grand Ritual will conjure a powerful artefact for your use, and grant you victory." + to_chat(owner, span_warning(message)) if (GRAND_RITUAL_FINALE_COUNT) SEND_SIGNAL(src, COMSIG_GRAND_RITUAL_FINAL_COMPLETE) @@ -283,3 +317,13 @@ /obj/effect/temp_visual/wizard_rune/failed icon_state = "wizard_rune_fail" duration = 0.5 SECONDS + +/// Cheese drawing +/obj/effect/temp_visual/wizard_rune/drawing/cheese + icon_state = "wizard_rune_cheese_draw" + +/// Cheese fail +/obj/effect/temp_visual/wizard_rune/failed/cheese + icon_state = "wizard_rune_cheese_fail" + +#undef CHEESE_SACRIFICE_GOAL diff --git a/code/modules/antagonists/wizard/grand_ritual/grand_ritual_finale.dm b/code/modules/antagonists/wizard/grand_ritual/grand_ritual_finale.dm index 82ed11910f98..d0cc58ff0f59 100644 --- a/code/modules/antagonists/wizard/grand_ritual/grand_ritual_finale.dm +++ b/code/modules/antagonists/wizard/grand_ritual/grand_ritual_finale.dm @@ -33,12 +33,13 @@ /datum/grand_finale/proc/get_radial_choice() if (!name || !desc || !icon || !icon_state) return + var/time_remaining_desc = "" if (minimum_time >= world.time - SSticker.round_start_time) - return + time_remaining_desc = "This ritual will be available to begin invoking in [DisplayTimeText(minimum_time - world.time - SSticker.round_start_time)]" var/datum/radial_menu_choice/choice = new() choice.name = name choice.image = image(icon = icon, icon_state = icon_state) - choice.info = desc + choice.info = desc + time_remaining_desc return choice /** @@ -321,7 +322,8 @@ /datum/dimension_theme/natural, /datum/dimension_theme/clown, //monkestation edit: HONK! /datum/dimension_theme/fancy, //monkestation edit - /datum/dimension_theme/disco,, //monkestation edit + /datum/dimension_theme/disco, //monkestation edit + /datum/dimension_theme/clockwork, //monkestation edit: clock cult W ) var/datum/dimension_theme/chosen_theme @@ -392,24 +394,28 @@ var/static/list/doom_options = list() if (!length(doom_options)) - doom_options = list(DOOM_SINGULARITY, DOOM_TESLA, DOOM_EVENTS, DOOM_ANTAGS, DOOM_ROD) //monkestation edit: added DOOM_EVENTS, DOOM_ANTAGS and DOOM_ROD +// doom_options = list(DOOM_SINGULARITY, DOOM_TESLA) //monkestation removal + doom_options = list(DOOM_EVENTS, DOOM_ANTAGS, DOOM_ROD) //monkestation edit if (!SSmapping.config.planetary) doom_options += DOOM_METEORS switch(pick(doom_options)) - if (DOOM_SINGULARITY) +//monkestation removal start + /*if (DOOM_SINGULARITY) var/obj/singularity/singulo = new(current_location) singulo.energy = 300 if (DOOM_TESLA) var/obj/energy_ball/tesla = new (current_location) - tesla.energy = 200 + tesla.energy = 200*/ +//monkestation removal end if (DOOM_METEORS) var/datum/dynamic_ruleset/roundstart/meteor/meteors = new() meteors.meteordelay = 0 var/datum/game_mode/dynamic/mode = SSticker.mode mode.execute_roundstart_rule(meteors) // Meteors will continue until morale is crushed. priority_announce("Meteors have been detected on collision course with the station.", "Meteor Alert", ANNOUNCER_METEORS) - if (DOOM_EVENTS) //monkestation edit start: triggers a MASSIVE amount of events pretty quickly +//monkestation edit start + if (DOOM_EVENTS) //triggers a MASSIVE amount of events pretty quickly summon_events() //wont effect the events created directly from this, but it will effect any events that happen after var/list/possible_events = list() for(var/datum/round_event_control/possible_event as anything in SSevents.control) @@ -438,7 +444,58 @@ var/obj/effect/immovablerod/rod = new(current_location) rod.loopy_rod = TRUE rod.can_suplex = FALSE - rod.deadchat_plays(ANARCHY_MODE, 4 SECONDS)//monkestation edit end + rod.deadchat_plays(ANARCHY_MODE, 4 SECONDS) +//monkestation edit end + +/** + * Gives the wizard a defensive/mood buff and a Wabbajack, a juiced up chaos staff that will surely break something. + * Everyone but the wizard goes crazy, suffers major brain damage, and is given a vendetta against the wizard. + * Already insane people are instead cured of their madness, ignoring any other effects as the station around them loses its marbles. + */ +/datum/grand_finale/cheese + // we don't set name, desc and others, thus we won't appear in the radial choice of a normal finale rune + dire_warning = TRUE + minimum_time = 45 MINUTES //i'd imagine speedrunning this would be crummy, but the wizard's average lifespan is barely reaching this point + +/datum/grand_finale/cheese/trigger(mob/living/invoker) + message_admins("[key_name(invoker)] has summoned forth The Wabbajack and cursed the crew with madness!") + priority_announce("Danger: Extremely potent reality altering object has been summoned on station. Immediate evacuation advised. Brace for impact.", "Central Command Higher Dimensional Affairs", 'sound/effects/glassbr1.ogg') + + for (var/mob/living/carbon/human/crewmate as anything in GLOB.human_list) + if (isnull(crewmate.mind)) + continue + if (crewmate == invoker) //everyone but the wizard is royally fucked, no matter who they are + continue + if (crewmate.has_trauma_type(/datum/brain_trauma/mild/hallucinations)) //for an already insane person, this is retribution + to_chat(crewmate, span_boldwarning("Your surroundings suddenly fill with a cacophony of manic laughter and psychobabble...")) + to_chat(crewmate, span_nicegreen("...but as the moment passes, you realise that whatever eldritch power behind the event happened to affect you \ + has resonated within the ruins of your already shattered mind, creating a singularity of mental instability! \ + As it collapses unto itself, you feel... at peace, finally.")) + if(crewmate.has_quirk(/datum/quirk/insanity)) + crewmate.remove_quirk(/datum/quirk/insanity) + else + crewmate.cure_trauma_type(/datum/brain_trauma/mild/hallucinations, TRAUMA_RESILIENCE_ABSOLUTE) + else + //everyone else gets to relish in madness + //yes killing their mood will also trigger mood hallucinations + create_vendetta(crewmate.mind, invoker.mind) + to_chat(crewmate, span_boldwarning("Your surroundings suddenly fill with a cacophony of manic laughter and psychobabble. \n\ + You feel your inner psyche shatter into a myriad pieces of jagged glass of colors unknown to the universe, \ + infinitely reflecting a blinding, maddening light coming from the innermost sanctums of your destroyed mind. \n\ + After a brief pause which felt like a millenia, one phrase rebounds ceaselessly in your head, imbued with the false hope of absolution... \n\ + [invoker] must die.")) + var/datum/brain_trauma/mild/hallucinations/added_trauma = new() + added_trauma.resilience = TRAUMA_RESILIENCE_ABSOLUTE + crewmate.adjustOrganLoss(ORGAN_SLOT_BRAIN, BRAIN_DAMAGE_DEATH - 25, BRAIN_DAMAGE_DEATH - 25) //you'd better hope chap didn't pick a hypertool + crewmate.gain_trauma(added_trauma) + crewmate.add_mood_event("wizard_ritual_finale", /datum/mood_event/madness_despair) + + //drip our wizard out + invoker.apply_status_effect(/datum/status_effect/blessing_of_insanity) + invoker.add_mood_event("wizard_ritual_finale", /datum/mood_event/madness_elation) + var/obj/item/gun/magic/staff/chaos/true_wabbajack/the_wabbajack = new + invoker.put_in_active_hand(the_wabbajack) + to_chat(invoker, span_mind_control("Your every single instinct and rational thought is screaming at you as [the_wabbajack] appears in your firm grip...")) #undef DOOM_SINGULARITY #undef DOOM_TESLA diff --git a/code/modules/antagonists/wizard/grand_ritual/grand_rune.dm b/code/modules/antagonists/wizard/grand_ritual/grand_rune.dm index 80eb6cccd1de..88ec1686693f 100644 --- a/code/modules/antagonists/wizard/grand_ritual/grand_rune.dm +++ b/code/modules/antagonists/wizard/grand_ritual/grand_rune.dm @@ -31,6 +31,10 @@ var/times_invoked = 0 /// What colour you glow while channeling var/spell_colour = "#de3aff48" + /// How much cheese was sacrificed to the other realm, if any + var/cheese_sacrificed = 0 + /// What kind of remains this rune leaves behind after completing invokation + var/remains_typepath = /obj/effect/decal/cleanable/grand_remains /// Magic words you say to invoke the ritual var/list/magic_words = list() /// Things you might yell when invoking a rune @@ -67,7 +71,8 @@ . = ..() src.potency = potency invoke_time = get_invoke_time() - magic_words = pick(possible_magic_words) + if(!length(magic_words)) + magic_words = pick(possible_magic_words) var/image/silicon_image = image(icon = 'icons/effects/eldritch.dmi', icon_state = null, loc = src) silicon_image.override = TRUE add_alt_appearance(/datum/atom_hud/alternate_appearance/basic/silicons, "wizard_rune", silicon_image) @@ -114,6 +119,7 @@ is_in_use = TRUE add_channel_effect(user) user.balloon_alert(user, "invoking rune...") + if(!do_after(user, invoke_time, src)) remove_channel_effect(user) user.balloon_alert(user, "interrupted!") @@ -121,6 +127,30 @@ return times_invoked++ + + //fetch cheese on the rune + var/list/obj/item/food/cheese/wheel/cheese_list = list() + for(var/obj/item/food/cheese/wheel/nearby_cheese in range(1, src)) + if(HAS_TRAIT(nearby_cheese, TRAIT_HAUNTED)) //already haunted + continue + cheese_list += nearby_cheese + //handle cheese sacrifice - haunt a part of all cheese on the rune with each invocation, then delete it + var/list/obj/item/food/cheese/wheel/cheese_to_haunt = list() + cheese_list = shuffle(cheese_list) + //the intent here is to sacrifice cheese in parts, roughly in thirds since we invoke the rune three times + //so hopefully this will properly do that, and on the third invocation it will just eat all remaining cheese + cheese_to_haunt = cheese_list.Copy(1, min(round(length(cheese_list) * times_invoked * 0.4), max(length(cheese_list), 3))) + for(var/obj/item/food/cheese/wheel/sacrifice as anything in cheese_to_haunt) + sacrifice.AddComponent(\ + /datum/component/haunted_item, \ + haunt_color = spell_colour, \ + haunt_duration = 10 SECONDS, \ + aggro_radius = 0, \ + spawn_message = span_revenwarning("[sacrifice] begins to float and twirl into the air as it becomes enveloped in otherworldy energies..."), \ + ) + addtimer(CALLBACK(sacrifice, TYPE_PROC_REF(/obj/item/food/cheese/wheel, consume_cheese)), 10 SECONDS) + cheese_sacrificed += length(cheese_to_haunt) + user.say(magic_words[times_invoked], forced = "grand ritual invocation") remove_channel_effect(user) @@ -130,7 +160,7 @@ if(times_invoked >= GRAND_RUNE_INVOKES_TO_COMPLETE) on_invocation_complete(user) return - flick("wizard_rune_flash", src) + flick("[icon_state]_flash", src) playsound(src,'sound/magic/staff_animation.ogg', 75, TRUE) INVOKE_ASYNC(src, PROC_REF(invoke_rune), user) @@ -159,7 +189,7 @@ addtimer(CALLBACK(src, PROC_REF(remove_rune)), 6) /obj/effect/grand_rune/proc/remove_rune() - new /obj/effect/decal/cleanable/grand_remains(get_turf(src)) + new remains_typepath(get_turf(src)) qdel(src) /// Triggers some form of event somewhere on the station @@ -272,9 +302,14 @@ return ..() /obj/effect/grand_rune/finale/interact(mob/living/user) - if (chosen_effect) - return ..() - select_finale(user) + if (!chosen_effect) + select_finale(user) + return + var/round_time_passed = world.time - SSticker.round_start_time + if (chosen_effect && finale_effect.minimum_time >= round_time_passed) + to_chat(user, span_warning("The chosen grand finale will only be available in [DisplayTimeText(finale_effect.minimum_time - round_time_passed)]!")) + return + return ..() #define PICK_NOTHING "Continuation" @@ -307,6 +342,7 @@ invoke_time = get_invoke_time() if (finale_effect.glow_colour) spell_colour = finale_effect.glow_colour + add_filter("finale_picked_glow", 2, list("type" = "outline", "color" = spell_colour, "size" = 2)) /obj/effect/grand_rune/finale/summon_round_event(mob/living/user) if (!finale_effect) @@ -318,6 +354,25 @@ return ..() return finale_effect.ritual_invoke_time +/** + * Spawned when 50 or more cheese was sacrificed during previous grand rituals. + * Will spawn instead of the usual grand ritual rune, and its effect is already set and can't be changed. + * Sorry, no narwal fighting on the open ocean this time. + */ +/obj/effect/grand_rune/finale/cheesy + name = "especially grand rune" + desc = "A ritual circle of maddening shapes and outlines, its mere presence an insult to reason." + icon_state = "wizard_rune_cheese" + magic_words = list("Greetings! Salutations!", "Welcome! Now go away.", "Leave. Run. Or die.") + remains_typepath = /obj/effect/decal/cleanable/grand_remains/cheese + +/obj/effect/grand_rune/finale/cheesy/Initialize(mapload, potency) + . = ..() + finale_effect = new /datum/grand_finale/cheese() + chosen_effect = TRUE + add_filter("finale_picked_glow", 2, list("type" = "outline", "color" = spell_colour, "size" = 2)) + + /** * Spawned when we are done with the rune */ @@ -334,4 +389,9 @@ clean_type = CLEAN_TYPE_HARD_DECAL layer = SIGIL_LAYER +/obj/effect/decal/cleanable/grand_remains/cheese + name = "cheese soot marks" + desc = "The bizarre shapes on the ground turn out to be a cheese crust burned to black tar." + icon_state = "wizard_rune_cheese_burned" + #undef PICK_NOTHING diff --git a/code/modules/clothing/head/jobs.dm b/code/modules/clothing/head/jobs.dm index 40dbc8f4ffea..fc018e15a0dc 100644 --- a/code/modules/clothing/head/jobs.dm +++ b/code/modules/clothing/head/jobs.dm @@ -458,7 +458,7 @@ /obj/item/clothing/head/beret/highlander/Initialize(mapload) . = ..() - ADD_TRAIT(src, TRAIT_NODROP, HIGHLANDER) + ADD_TRAIT(src, TRAIT_NODROP, HIGHLANDER_TRAIT) //CentCom /obj/item/clothing/head/beret/centcom_formal diff --git a/code/modules/clothing/head/mind_monkey_helmet.dm b/code/modules/clothing/head/mind_monkey_helmet.dm index 83fa277d2258..4df509e951ba 100644 --- a/code/modules/clothing/head/mind_monkey_helmet.dm +++ b/code/modules/clothing/head/mind_monkey_helmet.dm @@ -81,7 +81,9 @@ if(prob(10)) switch(rand(1,4)) if(1) //blood rage - magnification.ai_controller.set_blackboard_key(BB_MONKEY_AGGRESSIVE, TRUE) + var/datum/ai_controller/monkey/monky_controller = magnification.ai_controller + monky_controller.set_trip_mode(mode = FALSE) + monky_controller.set_blackboard_key(BB_MONKEY_AGGRESSIVE, TRUE) if(2) //brain death magnification.apply_damage(500,BRAIN,BODY_ZONE_HEAD,FALSE,FALSE,FALSE) if(3) //primal gene (gorilla) diff --git a/code/modules/clothing/under/costume.dm b/code/modules/clothing/under/costume.dm index fd7db5abb120..ef1f0002c1b8 100644 --- a/code/modules/clothing/under/costume.dm +++ b/code/modules/clothing/under/costume.dm @@ -90,7 +90,7 @@ /obj/item/clothing/under/costume/kilt/highlander/Initialize(mapload) . = ..() - ADD_TRAIT(src, TRAIT_NODROP, HIGHLANDER) + ADD_TRAIT(src, TRAIT_NODROP, HIGHLANDER_TRAIT) /obj/item/clothing/under/costume/gladiator name = "gladiator uniform" diff --git a/code/modules/hallucination/stray_bullet.dm b/code/modules/hallucination/stray_bullet.dm index f04c718f90b8..f3c5253b53cb 100644 --- a/code/modules/hallucination/stray_bullet.dm +++ b/code/modules/hallucination/stray_bullet.dm @@ -242,7 +242,7 @@ ";AAAAAAARRRGH!"), forced = "hulk (hallucinating)", ) - else if((afflicted.status_flags & CANKNOCKDOWN) && !HAS_TRAIT(afflicted, TRAIT_STUNIMMUNE)) + else if(!afflicted.check_stun_immunity(CANKNOCKDOWN)) addtimer(CALLBACK(afflicted, TYPE_PROC_REF(/mob/living/carbon, do_jitter_animation), 20), 0.5 SECONDS) /obj/projectile/hallucination/disabler diff --git a/code/modules/library/random_books.dm b/code/modules/library/random_books.dm index 1b15eeb5e975..3dcdff50a580 100644 --- a/code/modules/library/random_books.dm +++ b/code/modules/library/random_books.dm @@ -94,6 +94,7 @@ /obj/structure/bookcase/random/reference name = "bookcase (Reference)" random_category = "Reference" + ///Chance to spawn a random manual book var/ref_book_prob = 20 /obj/structure/bookcase/random/reference/Initialize(mapload) @@ -101,3 +102,15 @@ while(books_to_load > 0 && prob(ref_book_prob)) books_to_load-- new /obj/item/book/manual/random(src) + +/obj/structure/bookcase/random/reference/wizard + desc = "It reeks of cheese..." + ///Whether this shelf has spawned a cheese granter + var/static/cheese_granter_spawned = FALSE + +/obj/structure/bookcase/random/reference/wizard/after_random_load() + if(cheese_granter_spawned) + return + cheese_granter_spawned = TRUE + new /obj/item/book/granter/action/spell/summon_cheese(src) + new /obj/item/book/manual/ancient_parchment(src) diff --git a/code/modules/mob/living/carbon/human/examine.dm b/code/modules/mob/living/carbon/human/examine.dm index 6054b5dc865f..5f884023135b 100644 --- a/code/modules/mob/living/carbon/human/examine.dm +++ b/code/modules/mob/living/carbon/human/examine.dm @@ -291,11 +291,6 @@ if(reagents.has_reagent(/datum/reagent/teslium, needs_metabolizing = TRUE)) msg += "[t_He] [t_is] emitting a gentle blue glow!\n" - if(islist(stun_absorption)) - for(var/i in stun_absorption) - if(stun_absorption[i]["end_time"] > world.time && stun_absorption[i]["examine_message"]) - msg += "[t_He] [t_is][stun_absorption[i]["examine_message"]]\n" - if(just_sleeping) msg += "[t_He] [t_is]n't responding to anything around [t_him] and seem[p_s()] to be asleep.\n" diff --git a/code/modules/mob/living/carbon/status_procs.dm b/code/modules/mob/living/carbon/status_procs.dm index af2ef1cb1eb6..91230207fccf 100644 --- a/code/modules/mob/living/carbon/status_procs.dm +++ b/code/modules/mob/living/carbon/status_procs.dm @@ -13,11 +13,9 @@ return /mob/living/carbon/stamina_stun() - if(!(status_flags & CANKNOCKDOWN) || HAS_TRAIT(src, TRAIT_STUNIMMUNE)) - return if(HAS_TRAIT_FROM(src, TRAIT_INCAPACITATED, STAMINA)) //Already in stamcrit return - if(absorb_stun(0)) //continuous effect, so we don't want it to increment the stuns absorbed. + if(check_stun_immunity(CANKNOCKDOWN)) return var/chance = STAMINA_SCALING_STUN_BASE + (STAMINA_SCALING_STUN_SCALER * stamina.current * STAMINA_STUN_THRESHOLD_MODIFIER) if(!prob(chance)) diff --git a/code/modules/mob/living/damage_procs.dm b/code/modules/mob/living/damage_procs.dm index 399a52bb5d09..b686d92e840c 100644 --- a/code/modules/mob/living/damage_procs.dm +++ b/code/modules/mob/living/damage_procs.dm @@ -150,7 +150,7 @@ adjust_drowsiness(drowsy) if(eyeblur) adjust_eye_blur(eyeblur) - if(jitter && (status_flags & CANSTUN) && !HAS_TRAIT(src, TRAIT_STUNIMMUNE)) + if(jitter && !check_stun_immunity(CANSTUN)) adjust_jitter(jitter) if(slur) adjust_slurring(slur) diff --git a/code/modules/mob/living/silicon/robot/robot_model.dm b/code/modules/mob/living/silicon/robot/robot_model.dm index b3e403248498..bbc4890dd69c 100644 --- a/code/modules/mob/living/silicon/robot/robot_model.dm +++ b/code/modules/mob/living/silicon/robot/robot_model.dm @@ -952,7 +952,7 @@ robot.equip_module_to_slot(locate(/obj/item/claymore/highlander/robot) in basic_modules, 1) robot.equip_module_to_slot(locate(/obj/item/pinpointer/nuke) in basic_modules, 2) robot.place_on_head(new /obj/item/clothing/head/beret/highlander(robot)) //THE ONLY PART MORE IMPORTANT THAN THE SWORD IS THE HAT - ADD_TRAIT(robot.hat, TRAIT_NODROP, HIGHLANDER) + ADD_TRAIT(robot.hat, TRAIT_NODROP, HIGHLANDER_TRAIT) // ------------------------------------------ Storages diff --git a/code/modules/mob/living/simple_animal/hostile/gorilla/gorilla.dm b/code/modules/mob/living/simple_animal/hostile/gorilla/gorilla.dm index fa86cc950e51..55007a1121ce 100644 --- a/code/modules/mob/living/simple_animal/hostile/gorilla/gorilla.dm +++ b/code/modules/mob/living/simple_animal/hostile/gorilla/gorilla.dm @@ -114,6 +114,23 @@ oogas = rand(2,6) playsound(src, 'sound/creatures/gorilla.ogg', 50) +/mob/living/simple_animal/hostile/gorilla/lesser + name = "lesser Gorilla" + desc = "An adolescent Gorilla. It may not be fully grown but, much like a banana, that just means it's sturdier and harder to chew!" + speak_chance = 100 // compensating for something + maxHealth = 120 + health = 120 + butcher_results = list(/obj/item/food/meat/slab/gorilla = 2) + speed = 0.35 + melee_damage_lower = 10 + melee_damage_upper = 15 + obj_damage = 15 + stat_attack = SOFT_CRIT + unique_name = TRUE + +/mob/living/simple_animal/hostile/gorilla/lesser/Initialize(mapload) + . = ..() + transform *= 0.75 // smolrilla /mob/living/simple_animal/hostile/gorilla/cargo_domestic name = "Cargorilla" // Overriden, normally diff --git a/code/modules/mob/living/simple_animal/hostile/megafauna/blood_drunk_miner.dm b/code/modules/mob/living/simple_animal/hostile/megafauna/blood_drunk_miner.dm index 35c892c76a08..e62c1eac7c5b 100644 --- a/code/modules/mob/living/simple_animal/hostile/megafauna/blood_drunk_miner.dm +++ b/code/modules/mob/living/simple_animal/hostile/megafauna/blood_drunk_miner.dm @@ -97,9 +97,8 @@ Difficulty: Medium open_force = 10 /obj/item/melee/cleaving_saw/miner/attack(mob/living/target, mob/living/carbon/human/user) - target.add_stun_absorption("miner", 10, INFINITY) - . = ..() - target.stun_absorption -= "miner" + target.add_stun_absorption(source = "miner", duration = 1 SECONDS, priority = INFINITY) + return ..() /obj/projectile/kinetic/miner damage = 20 diff --git a/code/modules/mob/living/status_procs.dm b/code/modules/mob/living/status_procs.dm index 4e54d46b3da2..691003fc4f12 100644 --- a/code/modules/mob/living/status_procs.dm +++ b/code/modules/mob/living/status_procs.dm @@ -1,7 +1,34 @@ -//Here are the procs used to modify status effects of a mob. -//The effects include: stun, knockdown, unconscious, sleeping, resting -#define IS_STUN_IMMUNE(source, ignore_canstun) ((source.status_flags & GODMODE) || (!ignore_canstun && (!(source.status_flags & CANKNOCKDOWN) || HAS_TRAIT(source, TRAIT_STUNIMMUNE)))) + +/** + * Checks if we have stun immunity. Godmode always passes this check. + * + * * check_flags - bitflag of status flags that must be set in order for the stun to succeed. Passing NONE will always return false. + * * force_stun - whether we ignore stun immunity with the exception of godmode + * + * returns TRUE if stun immune, FALSE otherwise + */ +/mob/living/proc/check_stun_immunity(check_flags = CANSTUN, force_stun = FALSE) + SHOULD_CALL_PARENT(TRUE) + + if(status_flags & GODMODE) + return TRUE + + if(force_stun) // Does not take priority over god mode? I guess + return FALSE + + if(SEND_SIGNAL(src, COMSIG_LIVING_GENERIC_STUN_CHECK, check_flags, force_stun) & COMPONENT_NO_STUN) + return TRUE + + if(HAS_TRAIT(src, TRAIT_STUNIMMUNE)) + return TRUE + + // Do we have the correct flag set to allow this status? + // This checks that ALL flags are set, not just one of them. + if((status_flags & check_flags) == check_flags) + return FALSE + + return TRUE /* STUN */ /mob/living/proc/IsStun() //If we're stunned @@ -17,9 +44,7 @@ amount *= stun_diminish if(SEND_SIGNAL(src, COMSIG_LIVING_STATUS_STUN, amount, ignore_canstun) & COMPONENT_NO_STUN) return - if(IS_STUN_IMMUNE(src, ignore_canstun)) - return - if(absorb_stun(amount, ignore_canstun)) + if(check_stun_immunity(CANSTUN, ignore_canstun)) return var/datum/status_effect/incapacitating/stun/S = IsStun() if(S) @@ -33,15 +58,13 @@ /mob/living/proc/SetStun(amount, ignore_canstun = FALSE) //Sets remaining duration if(SEND_SIGNAL(src, COMSIG_LIVING_STATUS_STUN, amount, ignore_canstun) & COMPONENT_NO_STUN) return - if(IS_STUN_IMMUNE(src, ignore_canstun)) + if(check_stun_immunity(CANSTUN, ignore_canstun)) return var/datum/status_effect/incapacitating/stun/S = IsStun() if(amount <= 0) if(S) qdel(S) else - if(absorb_stun(amount, ignore_canstun)) - return if(S) S.duration = world.time + amount else @@ -52,9 +75,7 @@ amount *= stun_diminish if(SEND_SIGNAL(src, COMSIG_LIVING_STATUS_STUN, amount, ignore_canstun) & COMPONENT_NO_STUN) return - if(IS_STUN_IMMUNE(src, ignore_canstun)) - return - if(absorb_stun(amount, ignore_canstun)) + if(check_stun_immunity(CANSTUN, ignore_canstun)) return var/datum/status_effect/incapacitating/stun/S = IsStun() if(S) @@ -79,9 +100,7 @@ amount *= knockdown_diminish if(SEND_SIGNAL(src, COMSIG_LIVING_STATUS_KNOCKDOWN, amount, ignore_canstun) & COMPONENT_NO_STUN) return - if(IS_STUN_IMMUNE(src, ignore_canstun)) - return - if(absorb_stun(amount, ignore_canstun)) + if(check_stun_immunity(CANKNOCKDOWN, ignore_canstun)) return var/datum/status_effect/incapacitating/knockdown/K = IsKnockdown() if(K) @@ -95,15 +114,13 @@ /mob/living/proc/SetKnockdown(amount, ignore_canstun = FALSE) //Sets remaining duration if(SEND_SIGNAL(src, COMSIG_LIVING_STATUS_KNOCKDOWN, amount, ignore_canstun) & COMPONENT_NO_STUN) return - if(IS_STUN_IMMUNE(src, ignore_canstun)) + if(check_stun_immunity(CANKNOCKDOWN, ignore_canstun)) return var/datum/status_effect/incapacitating/knockdown/K = IsKnockdown() if(amount <= 0) if(K) qdel(K) else - if(absorb_stun(amount, ignore_canstun)) - return if(K) K.duration = world.time + amount else @@ -114,9 +131,7 @@ amount *= knockdown_diminish if(SEND_SIGNAL(src, COMSIG_LIVING_STATUS_KNOCKDOWN, amount, ignore_canstun) & COMPONENT_NO_STUN) return - if(IS_STUN_IMMUNE(src, ignore_canstun)) - return - if(absorb_stun(amount, ignore_canstun)) + if(check_stun_immunity(CANKNOCKDOWN, ignore_canstun)) return var/datum/status_effect/incapacitating/knockdown/K = IsKnockdown() if(K) @@ -140,9 +155,7 @@ /mob/living/proc/Immobilize(amount, ignore_canstun = FALSE) //Can't go below remaining duration if(SEND_SIGNAL(src, COMSIG_LIVING_STATUS_IMMOBILIZE, amount, ignore_canstun) & COMPONENT_NO_STUN) return - if(IS_STUN_IMMUNE(src, ignore_canstun)) - return - if(absorb_stun(amount, ignore_canstun)) + if(check_stun_immunity(CANSTUN, ignore_canstun)) return var/datum/status_effect/incapacitating/immobilized/I = IsImmobilized() if(I) @@ -154,15 +167,13 @@ /mob/living/proc/SetImmobilized(amount, ignore_canstun = FALSE) //Sets remaining duration if(SEND_SIGNAL(src, COMSIG_LIVING_STATUS_IMMOBILIZE, amount, ignore_canstun) & COMPONENT_NO_STUN) return - if(IS_STUN_IMMUNE(src, ignore_canstun)) + if(check_stun_immunity(CANSTUN, ignore_canstun)) return var/datum/status_effect/incapacitating/immobilized/I = IsImmobilized() if(amount <= 0) if(I) qdel(I) else - if(absorb_stun(amount, ignore_canstun)) - return if(I) I.duration = world.time + amount else @@ -172,9 +183,7 @@ /mob/living/proc/AdjustImmobilized(amount, ignore_canstun = FALSE) //Adds to remaining duration if(SEND_SIGNAL(src, COMSIG_LIVING_STATUS_IMMOBILIZE, amount, ignore_canstun) & COMPONENT_NO_STUN) return - if(IS_STUN_IMMUNE(src, ignore_canstun)) - return - if(absorb_stun(amount, ignore_canstun)) + if(check_stun_immunity(CANSTUN, ignore_canstun)) return var/datum/status_effect/incapacitating/immobilized/I = IsImmobilized() if(I) @@ -197,9 +206,7 @@ amount *= paralyze_diminish if(SEND_SIGNAL(src, COMSIG_LIVING_STATUS_PARALYZE, amount, ignore_canstun) & COMPONENT_NO_STUN) return - if(IS_STUN_IMMUNE(src, ignore_canstun)) - return - if(absorb_stun(amount, ignore_canstun)) + if(check_stun_immunity(CANSTUN|CANKNOCKDOWN, ignore_canstun)) // this requires both can stun and can knockdown return var/datum/status_effect/incapacitating/paralyzed/P = IsParalyzed(FALSE) if(P) @@ -213,15 +220,13 @@ /mob/living/proc/SetParalyzed(amount, ignore_canstun = FALSE) //Sets remaining duration if(SEND_SIGNAL(src, COMSIG_LIVING_STATUS_PARALYZE, amount, ignore_canstun) & COMPONENT_NO_STUN) return - if(IS_STUN_IMMUNE(src, ignore_canstun)) + if(check_stun_immunity(CANSTUN|CANKNOCKDOWN, ignore_canstun)) return var/datum/status_effect/incapacitating/paralyzed/P = IsParalyzed(FALSE) if(amount <= 0) if(P) qdel(P) else - if(absorb_stun(amount, ignore_canstun)) - return if(P) P.duration = world.time + amount else @@ -232,9 +237,7 @@ amount *= paralyze_diminish if(SEND_SIGNAL(src, COMSIG_LIVING_STATUS_PARALYZE, amount, ignore_canstun) & COMPONENT_NO_STUN) return - if(IS_STUN_IMMUNE(src, ignore_canstun)) - return - if(absorb_stun(amount, ignore_canstun)) + if(check_stun_immunity(CANSTUN|CANKNOCKDOWN, ignore_canstun)) return var/datum/status_effect/incapacitating/paralyzed/P = IsParalyzed(FALSE) if(P) @@ -262,9 +265,9 @@ * * ignore_canstun - If TRUE, the mob's resistance to stuns is ignored. */ /mob/living/proc/incapacitate(amount, ignore_canstun = FALSE) - if(IS_STUN_IMMUNE(src, ignore_canstun)) + if(SEND_SIGNAL(src, COMSIG_LIVING_STATUS_INCAPACITATE, amount, ignore_canstun) & COMPONENT_NO_STUN) return - if(absorb_stun(amount, ignore_canstun)) + if(check_stun_immunity(CANSTUN, ignore_canstun)) return var/datum/status_effect/incapacitating/incapacitated/incapacitated_status_effect = has_status_effect(/datum/status_effect/incapacitating/incapacitated) if(incapacitated_status_effect) @@ -280,15 +283,15 @@ * * ignore_canstun - If TRUE, the mob's resistance to stuns is ignored. */ /mob/living/proc/set_incapacitated(amount, ignore_canstun = FALSE) - if(IS_STUN_IMMUNE(src, ignore_canstun)) + if(SEND_SIGNAL(src, COMSIG_LIVING_STATUS_INCAPACITATE, amount, ignore_canstun) & COMPONENT_NO_STUN) + return + if(check_stun_immunity(CANSTUN, ignore_canstun)) return var/datum/status_effect/incapacitating/incapacitated/incapacitated_status_effect = has_status_effect(/datum/status_effect/incapacitating/incapacitated) if(amount <= 0) if(incapacitated_status_effect) qdel(incapacitated_status_effect) else - if(absorb_stun(amount, ignore_canstun)) - return if(incapacitated_status_effect) incapacitated_status_effect.duration = world.time + amount else @@ -302,9 +305,9 @@ * * ignore_canstun - If TRUE, the mob's resistance to stuns is ignored. */ /mob/living/proc/adjust_incapacitated(amount, ignore_canstun = FALSE) //Adds to remaining duration - if(IS_STUN_IMMUNE(src, ignore_canstun)) + if(SEND_SIGNAL(src, COMSIG_LIVING_STATUS_INCAPACITATE, amount, ignore_canstun) & COMPONENT_NO_STUN) return - if(absorb_stun(amount, ignore_canstun)) + if(check_stun_immunity(CANSTUN, ignore_canstun)) return var/datum/status_effect/incapacitating/incapacitated/incapacitated_status_effect = has_status_effect(/datum/status_effect/incapacitating/incapacitated) if(incapacitated_status_effect) @@ -348,7 +351,7 @@ /mob/living/proc/Unconscious(amount, ignore_canstun = FALSE) //Can't go below remaining duration if(SEND_SIGNAL(src, COMSIG_LIVING_STATUS_UNCONSCIOUS, amount, ignore_canstun) & COMPONENT_NO_STUN) return - if(IS_STUN_IMMUNE(src, ignore_canstun)) + if(check_stun_immunity(CANUNCONSCIOUS, ignore_canstun)) return var/datum/status_effect/incapacitating/unconscious/U = IsUnconscious() if(U) @@ -360,7 +363,7 @@ /mob/living/proc/SetUnconscious(amount, ignore_canstun = FALSE) //Sets remaining duration if(SEND_SIGNAL(src, COMSIG_LIVING_STATUS_UNCONSCIOUS, amount, ignore_canstun) & COMPONENT_NO_STUN) return - if(IS_STUN_IMMUNE(src, ignore_canstun)) + if(check_stun_immunity(CANUNCONSCIOUS, ignore_canstun)) return var/datum/status_effect/incapacitating/unconscious/U = IsUnconscious() if(amount <= 0) @@ -375,7 +378,7 @@ /mob/living/proc/AdjustUnconscious(amount, ignore_canstun = FALSE) //Adds to remaining duration if(SEND_SIGNAL(src, COMSIG_LIVING_STATUS_UNCONSCIOUS, amount, ignore_canstun) & COMPONENT_NO_STUN) return - if(IS_STUN_IMMUNE(src, ignore_canstun)) + if(check_stun_immunity(CANUNCONSCIOUS, ignore_canstun)) return var/datum/status_effect/incapacitating/unconscious/U = IsUnconscious() if(U) @@ -463,43 +466,6 @@ /mob/living/proc/IsFrozen() return has_status_effect(/datum/status_effect/freon) - -/* STUN ABSORPTION*/ -/mob/living/proc/add_stun_absorption(key, duration, priority, message, self_message, examine_message) -//adds a stun absorption with a key, a duration in deciseconds, its priority, and the messages it makes when you're stun/examined, if any - if(!islist(stun_absorption)) - stun_absorption = list() - if(stun_absorption[key]) - stun_absorption[key]["end_time"] = world.time + duration - stun_absorption[key]["priority"] = priority - stun_absorption[key]["stuns_absorbed"] = 0 - else - stun_absorption[key] = list("end_time" = world.time + duration, "priority" = priority, "stuns_absorbed" = 0, \ - "visible_message" = message, "self_message" = self_message, "examine_message" = examine_message) - -/mob/living/proc/absorb_stun(amount, ignoring_flag_presence) - if(amount < 0 || stat || ignoring_flag_presence || !islist(stun_absorption)) - return FALSE - if(!amount) - amount = 0 - var/priority_absorb_key - var/highest_priority - for(var/i in stun_absorption) - if(stun_absorption[i]["end_time"] > world.time && (!priority_absorb_key || stun_absorption[i]["priority"] > highest_priority)) - priority_absorb_key = stun_absorption[i] - highest_priority = priority_absorb_key["priority"] - if(priority_absorb_key) - if(amount) //don't spam up the chat for continuous stuns - if(priority_absorb_key["visible_message"] || priority_absorb_key["self_message"]) - if(priority_absorb_key["visible_message"] && priority_absorb_key["self_message"]) - visible_message(span_warning("[src][priority_absorb_key["visible_message"]]"), span_boldwarning("[priority_absorb_key["self_message"]]")) - else if(priority_absorb_key["visible_message"]) - visible_message(span_warning("[src][priority_absorb_key["visible_message"]]")) - else if(priority_absorb_key["self_message"]) - to_chat(src, span_boldwarning("[priority_absorb_key["self_message"]]")) - priority_absorb_key["stuns_absorbed"] += amount - return TRUE - /** * Adds the passed quirk to the mob * @@ -777,5 +743,3 @@ /// Helper to check if we seem to be alive or not /mob/living/proc/appears_alive() return health >= 0 && !HAS_TRAIT(src, TRAIT_FAKEDEATH) - -#undef IS_STUN_IMMUNE diff --git a/code/modules/photography/photos/photo.dm b/code/modules/photography/photos/photo.dm index e83553d35ab5..7bc248d59190 100644 --- a/code/modules/photography/photos/photo.dm +++ b/code/modules/photography/photos/photo.dm @@ -43,6 +43,7 @@ if(!isobserver(seen)) continue set_custom_materials(list(/datum/material/hauntium = 2000)) + grind_results = list(/datum/reagent/hauntium = 20) break /obj/item/photo/update_icon_state() diff --git a/code/modules/projectiles/guns/magic/staff.dm b/code/modules/projectiles/guns/magic/staff.dm index bfcd18baa220..4f83561ef645 100644 --- a/code/modules/projectiles/guns/magic/staff.dm +++ b/code/modules/projectiles/guns/magic/staff.dm @@ -127,9 +127,9 @@ no_den_usage = 1 school = SCHOOL_FORBIDDEN //this staff is evil. okay? it just is. look at this projectile type list. this is wrong. - /// Static list of all projectiles we can fire from our staff. + /// List of all projectiles we can fire from our staff. /// Doesn't contain all subtypes of magic projectiles, unlike what it looks like - var/static/list/allowed_projectile_types = list( + var/list/allowed_projectile_types = list( /obj/projectile/magic/animate, /obj/projectile/magic/antimagic, /obj/projectile/magic/arcane_barrage, @@ -169,6 +169,49 @@ process_fire(user, user, FALSE) return FALSE +/** + * Staff of chaos given to the wizard upon completing a cheesy grand ritual. Is completely evil and if something + * breaks, it's completely intended. Fuck off. + * Also can be used by everyone, because why not. + */ +/obj/item/gun/magic/staff/chaos/true_wabbajack + name = "\proper Wabbajack" + desc = "If there is some deity out there, they've definitely skipped their psych appointment before creating this." + icon_state = "the_wabbajack" + inhand_icon_state = "the_wabbajack" + resistance_flags = INDESTRUCTIBLE | LAVA_PROOF | FIRE_PROOF | UNACIDABLE | ACID_PROOF //fuck you + max_charges = 999999 //fuck you + recharge_rate = 1 + allow_intruder_use = TRUE + +/obj/item/gun/magic/staff/chaos/true_wabbajack/Initialize(mapload) + . = ..() + allowed_projectile_types |= subtypesof(/obj/projectile/bullet/cannonball) + allowed_projectile_types |= subtypesof(/obj/projectile/bullet/rocket) + allowed_projectile_types |= subtypesof(/obj/projectile/energy/tesla) + allowed_projectile_types |= subtypesof(/obj/projectile/magic) + allowed_projectile_types |= subtypesof(/obj/projectile/temp) + allowed_projectile_types |= list( + /obj/projectile/beam/mindflayer, + /obj/projectile/bullet/gyro, + /obj/projectile/bullet/honker, + /obj/projectile/bullet/mime, + /obj/projectile/curse_hand, + /obj/projectile/energy/declone, + /obj/projectile/energy/electrode, + /obj/projectile/energy/net, + /obj/projectile/energy/nuclear_particle, + /obj/projectile/gravityattract, + /obj/projectile/gravitychaos, + /obj/projectile/gravityrepulse, + /obj/projectile/ion, + /obj/projectile/meteor, + /obj/projectile/neurotoxin, + /obj/projectile/plasma, + ) //if you ever try to expand this list, avoid adding bullets/energy projectiles, this ain't supposed to be a gun... unless it's funny + + + /obj/item/gun/magic/staff/door name = "staff of door creation" desc = "An artefact that spits bolts of transformative magic that can create doors in walls." diff --git a/code/modules/projectiles/projectile/energy/stun.dm b/code/modules/projectiles/projectile/energy/stun.dm index 261dab29b27c..03cf5f85d84d 100644 --- a/code/modules/projectiles/projectile/energy/stun.dm +++ b/code/modules/projectiles/projectile/energy/stun.dm @@ -21,7 +21,7 @@ SEND_SIGNAL(C, COMSIG_LIVING_MINOR_SHOCK) if(C.dna && C.dna.check_mutation(/datum/mutation/human/hulk)) C.say(pick(";RAAAAAAAARGH!", ";HNNNNNNNNNGGGGGGH!", ";GWAAAAAAAARRRHHH!", "NNNNNNNNGGGGGGGGHH!", ";AAAAAAARRRGH!" ), forced = "hulk") - else if((C.status_flags & CANKNOCKDOWN) && !HAS_TRAIT(C, TRAIT_STUNIMMUNE)) + else if(!C.check_stun_immunity(CANKNOCKDOWN)) addtimer(CALLBACK(C, TYPE_PROC_REF(/mob/living/carbon, do_jitter_animation), 20), 5) /obj/projectile/energy/electrode/on_range() //to ensure the bolt sparks when it reaches the end of its range if it didn't hit a target yet diff --git a/code/modules/reagents/chemistry/reagents/other_reagents.dm b/code/modules/reagents/chemistry/reagents/other_reagents.dm index cd1840b1ab0c..c1243cfbdc68 100644 --- a/code/modules/reagents/chemistry/reagents/other_reagents.dm +++ b/code/modules/reagents/chemistry/reagents/other_reagents.dm @@ -3037,3 +3037,42 @@ if(SPT_PROB(10, seconds_per_tick)) carbon_metabolizer.set_heartattack(TRUE) + +/datum/reagent/hauntium + name = "Hauntium" + color = "#3B3B3BA3" + description = "An eerie liquid created by purifying the prescence of ghosts. If it happens to get in your body, it starts hurting your soul." //soul as in mood and heart + taste_description = "evil spirits" + metabolization_rate = 0.75 * REAGENTS_METABOLISM + material = /datum/material/hauntium + ph = 10 + chemical_flags = REAGENT_CAN_BE_SYNTHESIZED + +/datum/reagent/hauntium/expose_obj(obj/exposed_obj, volume) //gives 15 seconds of haunting effect for every unit of it that touches an object + . = ..() + if(HAS_TRAIT_FROM(exposed_obj, TRAIT_HAUNTED, HAUNTIUM_REAGENT_TRAIT)) + return + exposed_obj.make_haunted(HAUNTIUM_REAGENT_TRAIT, "#f8f8ff") + addtimer(CALLBACK(exposed_obj, TYPE_PROC_REF(/atom/movable/, remove_haunted), HAUNTIUM_REAGENT_TRAIT), volume * 20 SECONDS) + +/datum/reagent/hauntium/on_mob_metabolize(mob/living/carbon/affected_mob, seconds_per_tick, times_fired) + to_chat(affected_mob, span_userdanger("You feel an evil presence inside you!")) + if(affected_mob.mob_biotypes & MOB_UNDEAD) + affected_mob.add_mood_event("morbid_hauntium", /datum/mood_event/morbid_hauntium, name) //8 minutes of slight mood buff if undead or morbid + else + affected_mob.add_mood_event("hauntium_spirits", /datum/mood_event/hauntium_spirits, name) //8 minutes of mood debuff + +/datum/reagent/hauntium/on_mob_life(mob/living/carbon/affected_mob, seconds_per_tick, times_fired) + if(affected_mob.mob_biotypes & MOB_UNDEAD) //if morbid or undead, acts like an addiction-less drug + affected_mob.remove_status_effect(/datum/status_effect/jitter) + affected_mob.AdjustStun(-50 * REM * seconds_per_tick) + affected_mob.AdjustKnockdown(-50 * REM * seconds_per_tick) + affected_mob.AdjustUnconscious(-50 * REM * seconds_per_tick) + affected_mob.AdjustParalyzed(-50 * REM * seconds_per_tick) + affected_mob.AdjustImmobilized(-50 * REM * seconds_per_tick) + ..() + else + affected_mob.adjustOrganLoss(ORGAN_SLOT_HEART, REM * seconds_per_tick) //1 heart damage per tick + if(SPT_PROB(10, seconds_per_tick)) + affected_mob.emote(pick("twitch","choke","shiver","gag")) + ..() diff --git a/code/modules/reagents/chemistry/recipes/others.dm b/code/modules/reagents/chemistry/recipes/others.dm index 0da89eb08131..ef30d43ce8aa 100644 --- a/code/modules/reagents/chemistry/recipes/others.dm +++ b/code/modules/reagents/chemistry/recipes/others.dm @@ -951,3 +951,14 @@ for(var/i in rand(1, created_volume) to created_volume) new /mob/living/basic/ant(location) ..() + +/datum/chemical_reaction/hauntium_solidification + required_reagents = list(/datum/reagent/water/holywater = 10, /datum/reagent/hauntium = 20, /datum/reagent/iron = 1) + mob_react = FALSE +// reaction_flags = REACTION_INSTANT monkestation removal + reaction_tags = REACTION_TAG_EASY | REACTION_TAG_UNIQUE | REACTION_TAG_OTHER + +/datum/chemical_reaction/hauntium_solidification/on_reaction(datum/reagents/holder, datum/equilibrium/reaction, created_volume) + var/location = get_turf(holder.my_atom) + for(var/i in 1 to created_volume) + new /obj/item/stack/sheet/hauntium(location) diff --git a/code/modules/reagents/reagent_containers/syringes.dm b/code/modules/reagents/reagent_containers/syringes.dm index 5a1da3222245..046c1320a0d3 100644 --- a/code/modules/reagents/reagent_containers/syringes.dm +++ b/code/modules/reagents/reagent_containers/syringes.dm @@ -245,6 +245,16 @@ possible_transfer_amounts = list(1,5) volume = 5 +// Used by monkeys from the elemental plane of bananas. Reagents come from bungo pit, death berries, destroying angel, jupiter cups, and jumping beans. +/obj/item/reagent_containers/syringe/crude/tribal + name = "tribal syringe" + desc = "A crudely made syringe. Smells like bananas." + +/obj/item/reagent_containers/syringe/crude/tribal/Initialize(mapload) + var/toxin_to_get = pick(/datum/reagent/toxin/bungotoxin, /datum/reagent/toxin/coniine, /datum/reagent/toxin/amanitin, /datum/reagent/consumable/liquidelectricity, /datum/reagent/ants) + list_reagents = list((toxin_to_get) = 5) + return ..() + /obj/item/reagent_containers/syringe/spider_extract name = "spider extract syringe" desc = "Contains crikey juice - makes any gold core create the most deadly companions in the world." diff --git a/code/modules/spells/spell.dm b/code/modules/spells/spell.dm index 5bed28d1bb06..da38fb49fcb2 100644 --- a/code/modules/spells/spell.dm +++ b/code/modules/spells/spell.dm @@ -188,7 +188,7 @@ if(ishuman(owner)) if(spell_requirements & SPELL_REQUIRES_WIZARD_GARB) var/mob/living/carbon/human/human_owner = owner - if(!(human_owner.wear_suit?.clothing_flags & CASTING_CLOTHES)) + if(!(human_owner.wear_suit?.clothing_flags & CASTING_CLOTHES) && !ismonkey(human_owner)) // Monkeys don't need robes to cast as they are inherently imbued with power from the banana dimension if(feedback) to_chat(owner, span_warning("You don't feel strong enough without your robe!")) return FALSE @@ -198,12 +198,21 @@ return FALSE else - // If the spell requires wizard equipment and we're not a human (can't wear robes or hats), that's just a given - if(spell_requirements & (SPELL_REQUIRES_WIZARD_GARB|SPELL_REQUIRES_HUMAN)) + // If you strictly need to be a human, well, goodbye. + if(spell_requirements & SPELL_REQUIRES_HUMAN) if(feedback) to_chat(owner, span_warning("[src] can only be cast by humans!")) return FALSE + // Otherwise, we can check for contents if they have wizardly apparel. This isn't *quite* perfect, but it'll do, especially since many of the edge cases (gorilla holding a wizard hat) still more or less make sense. + if(spell_requirements & SPELL_REQUIRES_WIZARD_GARB) + for(var/atom/movable/item in owner.contents) + var/obj/item/clothing/clothem = item + if(istype(clothem) && clothem.clothing_flags & CASTING_CLOTHES) + return TRUE + to_chat(owner, span_warning("You don't feel strong enough without your hat!")) + return FALSE + if(!(spell_requirements & SPELL_CASTABLE_AS_BRAIN) && isbrain(owner)) if(feedback) to_chat(owner, span_warning("[src] can't be cast in this state!")) diff --git a/code/modules/spells/spell_types/conjure/cheese.dm b/code/modules/spells/spell_types/conjure/cheese.dm new file mode 100644 index 000000000000..d9c90d1dbac3 --- /dev/null +++ b/code/modules/spells/spell_types/conjure/cheese.dm @@ -0,0 +1,17 @@ +/datum/action/cooldown/spell/conjure/cheese + name = "Summon Cheese" + desc = "This spell conjures a bunch of cheese wheels. What the hell?" + sound = 'sound/magic/summonitems_generic.ogg' + button_icon_state = "cheese" + + school = SCHOOL_CONJURATION + cooldown_time = 1 MINUTES + spell_requirements = null + + invocation = "PL`YR DOT PL`CTM` OOO`BEE G" //player.placeatme 00064B33 9 + invocation_type = INVOCATION_SHOUT + garbled_invocation_prob = 0 //i'd rather it remain like this + + summon_radius = 1 + summon_amount = 9 + summon_type = list(/obj/item/food/cheese/wheel) diff --git a/code/modules/spells/spell_types/conjure/simian.dm b/code/modules/spells/spell_types/conjure/simian.dm new file mode 100644 index 000000000000..556a78e50127 --- /dev/null +++ b/code/modules/spells/spell_types/conjure/simian.dm @@ -0,0 +1,98 @@ +/datum/action/cooldown/spell/conjure/simian + name = "Summon Simians" + desc = "This spell reaches deep into the elemental plane of bananas (the monkey one, not the clown one), and \ + summons monkeys and gorillas that will promptly flip out and attack everything in sight. Fun! \ + Their lesser, easily manipulable minds will be convinced you are one of their allies, but only for a minute. Unless you also are a monkey." + button_icon_state = "simian" + sound = 'sound/ambience/antag/monkey.ogg' + + school = SCHOOL_CONJURATION + cooldown_time = 1.5 MINUTES + cooldown_reduction_per_rank = 15 SECONDS + + invocation = "OOGA OOGA OOGA!!!!" + invocation_type = INVOCATION_SHOUT + + summon_radius = 2 + summon_type = list(/mob/living/carbon/human/species/monkey/angry, /mob/living/carbon/human/species/monkey/angry, /mob/living/simple_animal/hostile/gorilla/lesser) + summon_amount = 4 + +/datum/action/cooldown/spell/conjure/simian/level_spell(bypass_cap) + . = ..() + summon_amount++ // MORE, MOOOOORE + if(spell_level == spell_max_level) // We reward the faithful. + summon_type = list(/mob/living/carbon/human/species/monkey/angry, /mob/living/simple_animal/hostile/gorilla) + spell_requirements = SPELL_REQUIRES_NO_ANTIMAGIC // Max level lets you cast it naked, for monkey larp. + to_chat(owner, span_notice("Your simian power has reached maximum capacity! You can now cast this spell naked, and you will create adult Gorillas with each cast.")) + +/datum/action/cooldown/spell/conjure/simian/cast(atom/cast_on) + . = ..() + var/mob/living/cast_mob = cast_on + if(!istype(cast_mob)) + return + if(FACTION_MONKEY in cast_mob.faction) + return + cast_mob.faction |= FACTION_MONKEY + addtimer(CALLBACK(src, PROC_REF(remove_monky_faction), cast_mob), 1 MINUTES) + +/datum/action/cooldown/spell/conjure/simian/proc/remove_monky_faction(mob/cast_mob) + cast_mob.faction -= FACTION_MONKEY + +/datum/action/cooldown/spell/conjure/simian/post_summon(atom/summoned_object, atom/cast_on) + var/mob/living/alive_dude = summoned_object + alive_dude.faction |= list(FACTION_MONKEY) + if(ismonkey(alive_dude)) + equip_monky(alive_dude) + return + +/** Equips summoned monky with gear depending on how the roll plays out, affected by spell lvl. + * Can give them bananas and garland or gatfruit and axes. Monkeys are comically inept, which balances out what might otherwise be a little crazy. + */ +/datum/action/cooldown/spell/conjure/simian/proc/equip_monky(mob/living/carbon/human/species/monkey/summoned_monkey) + + // These are advanced monkeys we're talking about + var/datum/ai_controller/monkey/monky_controller = summoned_monkey.ai_controller + monky_controller.set_trip_mode(mode = FALSE) + summoned_monkey.fully_replace_character_name(summoned_monkey.real_name, "primal " + summoned_monkey.name) + + // Monkeys get a random gear tier, but it's more likely to be good the more leveled the spell is! + var/monkey_gear_tier = rand(0, 5) + (spell_level - 1) + monkey_gear_tier = min(monkey_gear_tier, 5) + + // Monkey weapons, ordered by tier + var/static/list/monky_weapon = list( + list(/obj/item/food/grown/banana, /obj/item/grown/bananapeel), + list(/obj/item/tailclub, /obj/item/knife/combat/bone), + list(/obj/item/shovel/serrated, /obj/item/spear/bamboospear), + list(/obj/item/spear/bonespear, /obj/item/fireaxe/boneaxe), + list(/obj/item/gun/syringe/blowgun, /obj/item/gun/ballistic/revolver), + ) + + var/list/options = monky_weapon[min(monkey_gear_tier, length(monky_weapon))] + + var/obj/item/weapon + if(monkey_gear_tier != 0) + var/weapon_type = pick(options) + weapon = new weapon_type(summoned_monkey) + summoned_monkey.equip_to_slot_or_del(weapon, ITEM_SLOT_HANDS) + + // Load the ammo + if(istype(weapon, /obj/item/gun/syringe/blowgun)) + var/obj/item/reagent_containers/syringe/crude/tribal/syring = new(summoned_monkey) + weapon.attackby(syring, summoned_monkey) + + // Wield the weapon! + if(is_type_in_list(weapon, list(/obj/item/spear, /obj/item/fireaxe))) + weapon.attack_self(summoned_monkey) + + // Fashionable ape wear, organised by tier + var/list/static/monky_hats = list( + null, // nothin here + /obj/item/clothing/head/costume/garland, + /obj/item/clothing/head/helmet/durathread, + /obj/item/clothing/head/helmet/skull, + ) + + var/stylish_monkey_hat = monky_hats[min(monkey_gear_tier, length(monky_hats))] + if(!isnull(stylish_monkey_hat)) + summoned_monkey.equip_to_slot_or_del(new stylish_monkey_hat(summoned_monkey), ITEM_SLOT_HEAD) diff --git a/code/modules/spells/spell_types/self/mutate.dm b/code/modules/spells/spell_types/self/mutate.dm index 5368ce0b7ab6..612a95164e5d 100644 --- a/code/modules/spells/spell_types/self/mutate.dm +++ b/code/modules/spells/spell_types/self/mutate.dm @@ -38,12 +38,25 @@ /datum/action/cooldown/spell/apply_mutations/mutate name = "Mutate" - desc = "This spell causes you to turn into a hulk and gain laser vision for a short while." + desc = "This spell causes you to turn into a gigantic hulk and gain laser vision for a short while. Unlike the lesser nonmagical version, it works on non-humans and mantains hand dexterity as well!" cooldown_time = 60 SECONDS //monkestation edit: from 40 to 60 seconds cooldown_reduction_per_rank = 5 SECONDS //monkestation edit: from 2.5 to 5 seconds invocation = "BIRUZ BENNAR" invocation_type = INVOCATION_SHOUT - mutations_to_add = list(/datum/mutation/human/laser_eyes, /datum/mutation/human/hulk) + mutations_to_add = list(/datum/mutation/human/laser_eyes, /datum/mutation/human/hulk/wizardly, /datum/mutation/human/gigantism) mutation_duration = 30 SECONDS + +/datum/action/cooldown/spell/apply_mutations/mutate/cast(mob/living/carbon/human/cast_on) + ..() + /*if(HAS_TRAIT(cast_on, TRAIT_USES_SKINTONES) || HAS_TRAIT(cast_on, TRAIT_MUTANT_COLORS)) + return*/ //monkestation removal, we dont have this refactor yet + // Our caster has a species that doesn't greenify when hulked, so we will do it manually. + cast_on.add_atom_colour("#00FF00", TEMPORARY_COLOUR_PRIORITY) + +/datum/action/cooldown/spell/apply_mutations/mutate/remove_mutations(mob/living/carbon/human/cast_on) + if(QDELETED(cast_on) || !is_valid_target(cast_on)) + return + + cast_on.remove_atom_colour(TEMPORARY_COLOUR_PRIORITY) diff --git a/code/modules/unit_tests/_unit_tests.dm b/code/modules/unit_tests/_unit_tests.dm index 4ad8b3dc449e..4143f1b8e5bc 100644 --- a/code/modules/unit_tests/_unit_tests.dm +++ b/code/modules/unit_tests/_unit_tests.dm @@ -208,6 +208,7 @@ #include "stomach.dm" #include "strange_reagent.dm" #include "strippable.dm" +#include "stuns.dm" #include "subsystem_init.dm" #include "suit_storage_icons.dm" #include "surgeries.dm" diff --git a/code/modules/unit_tests/stuns.dm b/code/modules/unit_tests/stuns.dm new file mode 100644 index 000000000000..68110e72e559 --- /dev/null +++ b/code/modules/unit_tests/stuns.dm @@ -0,0 +1,72 @@ +/// Tests stun and the canstun flag +/datum/unit_test/stun + +/datum/unit_test/stun/Run() + var/mob/living/carbon/human/gets_stunned = allocate(/mob/living/carbon/human/consistent) + + gets_stunned.Stun(1 SECONDS) + TEST_ASSERT(gets_stunned.IsStun(), "Stun() failed to apply stun") + + gets_stunned.SetStun(0 SECONDS) + TEST_ASSERT(!gets_stunned.IsStun(), "SetStun(0) failed to clear stun") + + gets_stunned.status_flags &= ~CANSTUN + gets_stunned.Stun(1 SECONDS) + TEST_ASSERT(!gets_stunned.IsStun(), "Stun() stunned despite not having CANSTUN flag") + +/// Tests knockdown and the canknockdown flag +/datum/unit_test/knockdown + +/datum/unit_test/knockdown/Run() + var/mob/living/carbon/human/gets_knockdown = allocate(/mob/living/carbon/human/consistent) + + gets_knockdown.Knockdown(1 SECONDS) + TEST_ASSERT(gets_knockdown.IsKnockdown(), "Knockdown() failed to apply knockdown") + + gets_knockdown.SetKnockdown(0 SECONDS) + TEST_ASSERT(!gets_knockdown.IsKnockdown(), "SetKnockdown(0) failed to clear knockdown") + + gets_knockdown.status_flags &= ~CANKNOCKDOWN + gets_knockdown.Knockdown(1 SECONDS) + TEST_ASSERT(!gets_knockdown.IsKnockdown(), "Knockdown() knocked over despite not having CANKNOCKDOWN flag") + +/// Tests paralyze and stuns that have two flags checked (in this case, canstun and canknockdown) +/datum/unit_test/paralyze + +/datum/unit_test/paralyze/Run() + var/mob/living/carbon/human/gets_paralyzed = allocate(/mob/living/carbon/human/consistent) + + gets_paralyzed.Paralyze(1 SECONDS) + TEST_ASSERT(gets_paralyzed.IsParalyzed(), "Paralyze() failed to apply paralyze") + + gets_paralyzed.SetParalyzed(0 SECONDS) + TEST_ASSERT(!gets_paralyzed.IsParalyzed(), "SetParalyzed(0) failed to clear paralyze") + + gets_paralyzed.status_flags &= ~CANSTUN // paralyze needs both CANSTUN and CANKNOCKDOWN to succeed + gets_paralyzed.Paralyze(1 SECONDS) + TEST_ASSERT(!gets_paralyzed.IsParalyzed(), "Paralyze() paralyzed a mob despite not having CANSTUN flag (but still having CANKNOCKDOWN)") + +/// Tests unconsciousness and the canunconscious flag +/datum/unit_test/unconsciousness + +/datum/unit_test/unconsciousness/Run() + var/mob/living/carbon/human/gets_unconscious = allocate(/mob/living/carbon/human/consistent) + + gets_unconscious.Unconscious(1 SECONDS) + TEST_ASSERT(gets_unconscious.IsUnconscious(), "Unconscious() failed to apply unconsciousness") + + gets_unconscious.SetUnconscious(0 SECONDS) + TEST_ASSERT(!gets_unconscious.IsUnconscious(), "SetUnconscious(0) failed to clear unconsciousness") + + gets_unconscious.status_flags &= ~CANUNCONSCIOUS + gets_unconscious.Unconscious(1 SECONDS) + TEST_ASSERT(!gets_unconscious.IsUnconscious(), "Unconscious() knocked unconscious despite not having CANUNCONSCIOUS flag") + +/// Tests for stun absorption +/datum/unit_test/stun_absorb + +/datum/unit_test/stun_absorb/Run() + var/mob/living/carbon/human/doesnt_get_stunned = allocate(/mob/living/carbon/human/consistent) + doesnt_get_stunned.add_stun_absorption(source = TRAIT_SOURCE_UNIT_TESTS) + doesnt_get_stunned.Stun(1 SECONDS) + TEST_ASSERT(!doesnt_get_stunned.IsStun(), "Stun() stunned despite having stun absorption") diff --git a/icons/effects/96x96.dmi b/icons/effects/96x96.dmi index 1c59abec3b28..a0d7ccfc35c3 100644 Binary files a/icons/effects/96x96.dmi and b/icons/effects/96x96.dmi differ diff --git a/icons/hud/screen_gen.dmi b/icons/hud/screen_gen.dmi index d80a2dbe762a..1484e3f4042a 100644 Binary files a/icons/hud/screen_gen.dmi and b/icons/hud/screen_gen.dmi differ diff --git a/icons/mob/actions/actions_spells.dmi b/icons/mob/actions/actions_spells.dmi index 9d657c6b88cf..39ea58104173 100644 Binary files a/icons/mob/actions/actions_spells.dmi and b/icons/mob/actions/actions_spells.dmi differ diff --git a/icons/mob/clothing/back.dmi b/icons/mob/clothing/back.dmi index 96e97899c66e..3421f1d485fd 100644 Binary files a/icons/mob/clothing/back.dmi and b/icons/mob/clothing/back.dmi differ diff --git a/icons/mob/inhands/weapons/staves_lefthand.dmi b/icons/mob/inhands/weapons/staves_lefthand.dmi index 82c552b50382..d75ee393d1a5 100644 Binary files a/icons/mob/inhands/weapons/staves_lefthand.dmi and b/icons/mob/inhands/weapons/staves_lefthand.dmi differ diff --git a/icons/mob/inhands/weapons/staves_righthand.dmi b/icons/mob/inhands/weapons/staves_righthand.dmi index 769709f53da1..8143819f807a 100644 Binary files a/icons/mob/inhands/weapons/staves_righthand.dmi and b/icons/mob/inhands/weapons/staves_righthand.dmi differ diff --git a/icons/obj/library.dmi b/icons/obj/library.dmi index b079fd32949b..3b79434df294 100644 Binary files a/icons/obj/library.dmi and b/icons/obj/library.dmi differ diff --git a/icons/obj/weapons/guns/magic.dmi b/icons/obj/weapons/guns/magic.dmi index 3a97682ef661..92210a4e0542 100644 Binary files a/icons/obj/weapons/guns/magic.dmi and b/icons/obj/weapons/guns/magic.dmi differ diff --git a/icons/obj/wizard.dmi b/icons/obj/wizard.dmi index 6b8309b90382..7fe2af730582 100644 Binary files a/icons/obj/wizard.dmi and b/icons/obj/wizard.dmi differ diff --git a/monkestation/code/modules/antagonists/wizard/equipment/artefact.dm b/monkestation/code/modules/antagonists/wizard/equipment/artefact.dm index b4876facf8e0..c36be4bbb47c 100644 --- a/monkestation/code/modules/antagonists/wizard/equipment/artefact.dm +++ b/monkestation/code/modules/antagonists/wizard/equipment/artefact.dm @@ -43,7 +43,7 @@ var/static/list/spell_list = list(/datum/action/cooldown/spell/rod_form, /datum/action/cooldown/spell/aoe/magic_missile, /datum/action/cooldown/spell/emp/disable_tech, /datum/action/cooldown/spell/aoe/repulse/wizard, /datum/action/cooldown/spell/timestop, /datum/action/cooldown/spell/forcewall, /datum/action/cooldown/spell/conjure/the_traps, - /datum/action/cooldown/spell/conjure/bee, /datum/action/cooldown/spell/conjure/lesser_summonapes, + /datum/action/cooldown/spell/conjure/bee, /datum/action/cooldown/spell/conjure/simian, /datum/action/cooldown/spell/teleport/radius_turf/blink) COOLDOWN_DECLARE(armor_cooldown) //unsure if I should use a world.time instead of this diff --git a/monkestation/code/modules/antagonists/wizard/equipment/spellbook_entries/defensive.dm b/monkestation/code/modules/antagonists/wizard/equipment/spellbook_entries/defensive.dm index 5f7c02ee5503..27e95736d103 100644 --- a/monkestation/code/modules/antagonists/wizard/equipment/spellbook_entries/defensive.dm +++ b/monkestation/code/modules/antagonists/wizard/equipment/spellbook_entries/defensive.dm @@ -5,13 +5,6 @@ category = "Defensive" cost = 1 -/datum/spellbook_entry/lesser_summon_apes - name = "Lesser Summon Apes" - desc = "This spell conjures a group of hostile apes, they WILL be hostile to you." - spell_type = /datum/action/cooldown/spell/conjure/lesser_summonapes - category = "Defensive" - cost = 2 - /datum/spellbook_entry/item/reactive_talisman name = "Reactive Talisman" desc = "An enchanted talisman that has a chance to cast a spell if it's wearer is hit." diff --git a/monkestation/code/modules/spells/spell_types/conjure/lesser_summon_apes.dm b/monkestation/code/modules/spells/spell_types/conjure/lesser_summon_apes.dm deleted file mode 100644 index 3afc05e5a875..000000000000 --- a/monkestation/code/modules/spells/spell_types/conjure/lesser_summon_apes.dm +++ /dev/null @@ -1,16 +0,0 @@ -/datum/action/cooldown/spell/conjure/lesser_summonapes - name = "Lesser Summon Apes" - desc = "This spell conjures a group of hostile apes, they WILL be hostile to you." - invocation = "MON'KE" - button_icon = 'icons/mob/actions/actions_silicon.dmi' - button_icon_state = "monkey_down" - invocation_type = INVOCATION_SHOUT - summon_radius = 2 - sound = 'sound/creatures/monkey/monkey_screech_1.ogg' - cooldown_time = 2 MINUTES - cooldown_reduction_per_rank = 15 SECONDS - - summon_type = list(/mob/living/carbon/human/species/monkey/angry, /mob/living/simple_animal/hostile/gorilla) - - summon_lifespan = 90 SECONDS - summon_amount = 4 diff --git a/tgstation.dme b/tgstation.dme index 663fb1bbe32a..3f4c30ea187d 100644 --- a/tgstation.dme +++ b/tgstation.dme @@ -1439,6 +1439,7 @@ #include "code\datums\mood_events\drug_events.dm" #include "code\datums\mood_events\generic_negative_events.dm" #include "code\datums\mood_events\generic_positive_events.dm" +#include "code\datums\mood_events\morbid_events.dm" #include "code\datums\mood_events\needs_events.dm" #include "code\datums\mutations\_combined.dm" #include "code\datums\mutations\_mutations.dm" @@ -1502,6 +1503,7 @@ #include "code\datums\status_effects\song_effects.dm" #include "code\datums\status_effects\stacking_effect.dm" #include "code\datums\status_effects\wound_effects.dm" +#include "code\datums\status_effects\buffs\stun_asorption.dm" #include "code\datums\status_effects\debuffs\blindness.dm" #include "code\datums\status_effects\debuffs\choke.dm" #include "code\datums\status_effects\debuffs\confusion.dm" @@ -2087,6 +2089,7 @@ #include "code\game\objects\items\granters\magic\mindswap.dm" #include "code\game\objects\items\granters\magic\sacred_flame.dm" #include "code\game\objects\items\granters\magic\smoke.dm" +#include "code\game\objects\items\granters\magic\summon_cheese.dm" #include "code\game\objects\items\granters\magic\summon_item.dm" #include "code\game\objects\items\granters\martial_arts\_martial_arts.dm" #include "code\game\objects\items\granters\martial_arts\cqc.dm" @@ -2821,6 +2824,7 @@ #include "code\modules\antagonists\wizard\equipment\spellbook_entries\mobility.dm" #include "code\modules\antagonists\wizard\equipment\spellbook_entries\offensive.dm" #include "code\modules\antagonists\wizard\equipment\spellbook_entries\summons.dm" +#include "code\modules\antagonists\wizard\grand_ritual\fluff.dm" #include "code\modules\antagonists\wizard\grand_ritual\grand_ritual.dm" #include "code\modules\antagonists\wizard\grand_ritual\grand_ritual_finale.dm" #include "code\modules\antagonists\wizard\grand_ritual\grand_rune.dm" @@ -4956,6 +4960,7 @@ #include "code\modules\spells\spell_types\conjure\_conjure.dm" #include "code\modules\spells\spell_types\conjure\bees.dm" #include "code\modules\spells\spell_types\conjure\carp.dm" +#include "code\modules\spells\spell_types\conjure\cheese.dm" #include "code\modules\spells\spell_types\conjure\constructs.dm" #include "code\modules\spells\spell_types\conjure\creatures.dm" #include "code\modules\spells\spell_types\conjure\cult_turfs.dm" @@ -4964,6 +4969,7 @@ #include "code\modules\spells\spell_types\conjure\invisible_wall.dm" #include "code\modules\spells\spell_types\conjure\link_worlds.dm" #include "code\modules\spells\spell_types\conjure\presents.dm" +#include "code\modules\spells\spell_types\conjure\simian.dm" #include "code\modules\spells\spell_types\conjure\soulstone.dm" #include "code\modules\spells\spell_types\conjure\the_traps.dm" #include "code\modules\spells\spell_types\conjure_item\_conjure_item.dm" @@ -6108,7 +6114,6 @@ #include "monkestation\code\modules\skyrat_snipes\vending_machines\vendors.dm" #include "monkestation\code\modules\slash_commands\world_topics.dm" #include "monkestation\code\modules\spells\spell_types\aoe_spell\mind_swap.dm" -#include "monkestation\code\modules\spells\spell_types\conjure\lesser_summon_apes.dm" #include "monkestation\code\modules\spells\spell_types\conjure_item\summon_mjollnir.dm" #include "monkestation\code\modules\spells\spell_types\pointed\smite.dm" #include "monkestation\code\modules\store\admin\admin_coin_modification.dm"