diff --git a/code/__DEFINES/bitfields.dm b/code/__DEFINES/_bitfields.dm similarity index 100% rename from code/__DEFINES/bitfields.dm rename to code/__DEFINES/_bitfields.dm diff --git a/code/__DEFINES/actions.dm b/code/__DEFINES/actions.dm index ca206810699..eb1edc8d623 100644 --- a/code/__DEFINES/actions.dm +++ b/code/__DEFINES/actions.dm @@ -7,6 +7,13 @@ ///Action button checks if user is conscious #define AB_CHECK_CONSCIOUS (1<<3) +DEFINE_BITFIELD(check_flags, list( + "CHECK IF HANDS BLOCKED" = AB_CHECK_HANDS_BLOCKED, + "CHECK IF IMMOBILIZED" = AB_CHECK_IMMOBILE, + "CHECK IF LYING DOWN" = AB_CHECK_LYING, + "CHECK IF CONSCIOUS" = AB_CHECK_CONSCIOUS, +)) + ///Action button triggered with right click #define TRIGGER_SECONDARY_ACTION (1<<0) diff --git a/code/__DEFINES/magic.dm b/code/__DEFINES/magic.dm index e92e17ea23c..d6a099562d1 100644 --- a/code/__DEFINES/magic.dm +++ b/code/__DEFINES/magic.dm @@ -95,3 +95,13 @@ DEFINE_BITFIELD(antimagic_flags, list( "MAGIC_RESISTANCE_HOLY" = MAGIC_RESISTANCE_HOLY, "MAGIC_RESISTANCE_MIND" = MAGIC_RESISTANCE_MIND, )) + +/** + * Checks if our mob is jaunting actively (within a phased mob object) + * Used in jaunting spells specifically to determine whether they should be entering or exiting jaunt + * + * If you want to use this in non-jaunt related code, it is preferable + * to instead check for trait [TRAIT_MAGICALLY_PHASED] instead of using this + * as it encompasses more states in which a mob may be "incorporeal from magic" + */ +#define is_jaunting(atom) (istype(atom.loc, /obj/effect/dummy/phased_mob)) diff --git a/code/__DEFINES/sound.dm b/code/__DEFINES/sound.dm index d9a830d9460..88bf2fb8dc6 100644 --- a/code/__DEFINES/sound.dm +++ b/code/__DEFINES/sound.dm @@ -7,6 +7,7 @@ #define CHANNEL_AMBIENCE 1019 #define CHANNEL_BUZZ 1018 #define CHANNEL_TRAITOR 1017 +#define CHANNEL_CHARGED_SPELL 1016 ///Default range of a sound. #define SOUND_RANGE 17 diff --git a/code/__DEFINES/traits.dm b/code/__DEFINES/traits.dm index 25776c1c6b6..e0ff56dd136 100644 --- a/code/__DEFINES/traits.dm +++ b/code/__DEFINES/traits.dm @@ -965,6 +965,9 @@ Remember to update _globalvars/traits.dm if you're adding/removing/renaming trai /// Makes a species be better/worse at defending against tackling depending on their tail's status #define TRAIT_TACKLING_TAILED_DEFENDER "tackling_tailed_defender" +/// Is runechat for this atom/movable currently disabled, regardless of prefs or anything? +#define TRAIT_RUNECHAT_HIDDEN "runechat_hudden" + /// some trait sorces dirived from bodyparts BODYPART_TRAIT is generic. #define BODYPART_TRAIT "bodypart" #define HEAD_TRAIT "head" diff --git a/code/__HELPERS/mobs.dm b/code/__HELPERS/mobs.dm index b2d60995ce7..03906dd656f 100644 --- a/code/__HELPERS/mobs.dm +++ b/code/__HELPERS/mobs.dm @@ -802,6 +802,8 @@ GLOBAL_LIST_EMPTY(species_list) return BODY_ZONE_CHEST if(BODY_ZONE_PRECISE_EYES) return BODY_ZONE_HEAD + if(BODY_ZONE_PRECISE_MOUTH) + return BODY_ZONE_HEAD if(BODY_ZONE_PRECISE_R_HAND) return BODY_ZONE_R_ARM if(BODY_ZONE_PRECISE_L_HAND) diff --git a/code/_globalvars/phobias.dm b/code/_globalvars/phobias.dm index f803088ea46..7547a8a17c5 100644 --- a/code/_globalvars/phobias.dm +++ b/code/_globalvars/phobias.dm @@ -69,6 +69,7 @@ GLOBAL_LIST_INIT(phobia_mobs, list( /mob/living/simple_animal/hostile/construct, /mob/living/simple_animal/hostile/dark_wizard, /mob/living/simple_animal/hostile/faithless, + /mob/living/simple_animal/hostile/heretic_summon, /mob/living/simple_animal/hostile/imp, /mob/living/simple_animal/hostile/retaliate/bat, /mob/living/simple_animal/hostile/retaliate/ghost, @@ -335,16 +336,25 @@ GLOBAL_LIST_INIT(phobia_objs, list( )), "the supernatural" = typecacheof(list( + /obj/effect/floating_blade, + /obj/effect/heretic_influence, + /obj/effect/heretic_rune, /obj/effect/rune, + /obj/effect/visible_heretic_influence, /obj/item/clothing/head/hooded/cult_hoodie, /obj/item/clothing/head/wizard, + /obj/item/clothing/mask/madness_mask, + /obj/item/clothing/neck/heretic_focus, + /obj/item/clothing/neck/eldritch_amulet, /obj/item/clothing/suit/hooded/cultrobes, /obj/item/clothing/suit/wizrobe, /obj/item/clothing/under/rank/civilian/chaplain, + /obj/item/codex_cicatrix, /obj/item/cult_bastard, - /obj/item/gun/magic/staff, - /obj/item/gun/magic/wand, + /obj/item/gun/magic, /obj/item/melee/cultblade, + /obj/item/melee/rune_carver, + /obj/item/melee/sickly_blade, /obj/item/necromantic_stone, /obj/item/nullrod, /obj/item/restraints/legcuffs/bola/cult, @@ -356,13 +366,16 @@ GLOBAL_LIST_INIT(phobia_objs, list( /obj/item/staff, /obj/item/storage/toolbox/haunted, /obj/item/tome, + /obj/item/toy/cards/deck/tarot, /obj/item/toy/eightball/haunted, + /obj/item/toy/eldritch_book, + /obj/item/toy/reality_pierce, /obj/item/warp_whistle, /obj/machinery/door/airlock/cult, /obj/narsie, /obj/structure/destructible/cult, + /obj/structure/destructible/eldritch_crucible, /obj/structure/spirit_board, - /obj/item/toy/cards/deck/tarot, )), "aliens" = typecacheof(list( diff --git a/code/_globalvars/traits.dm b/code/_globalvars/traits.dm index 56279ee7f6d..ee5a741866e 100644 --- a/code/_globalvars/traits.dm +++ b/code/_globalvars/traits.dm @@ -204,6 +204,7 @@ GLOBAL_LIST_INIT(traits_by_type, list( "TRAIT_SNOWSTORM_IMMUNE" = TRAIT_SNOWSTORM_IMMUNE, "TRAIT_VOIDSTORM_IMMUNE" = TRAIT_VOIDSTORM_IMMUNE, "TRAIT_WEATHER_IMMUNE" = TRAIT_WEATHER_IMMUNE, + "TRAIT_RUNECHAT_HIDDEN" = TRAIT_RUNECHAT_HIDDEN, ), /obj/item/card/id = list( "TRAIT_MAGNETIC_ID_CARD" = TRAIT_MAGNETIC_ID_CARD, diff --git a/code/datums/chatmessage.dm b/code/datums/chatmessage.dm index 55f8510fc57..3d22279ad2b 100644 --- a/code/datums/chatmessage.dm +++ b/code/datums/chatmessage.dm @@ -260,6 +260,8 @@ /mob/proc/create_chat_message(atom/movable/speaker, datum/language/message_language, raw_message, list/spans, runechat_flags = NONE) if(SSlag_switch.measures[DISABLE_RUNECHAT] && !HAS_TRAIT(speaker, TRAIT_BYPASS_MEASURES)) return + if(HAS_TRAIT(speaker, TRAIT_RUNECHAT_HIDDEN)) + return // Ensure the list we are using, if present, is a copy so we don't modify the list provided to us spans = spans ? spans.Copy() : list() diff --git a/code/datums/mind/antag.dm b/code/datums/mind/antag.dm index 54ec61bdfa0..1e2b87f2a1d 100644 --- a/code/datums/mind/antag.dm +++ b/code/datums/mind/antag.dm @@ -218,6 +218,9 @@ for(var/datum/objective/objective as anything in get_all_objectives()) to_chat(current, "[objective.objective_name] #[obj_count]: [objective.explanation_text]") obj_count++ + // Objectives are often stored in the static data of antag uis, so we should update those as well + for(var/datum/antagonist/antag as anything in antag_datums) + antag.update_static_data(current) /datum/mind/proc/find_syndicate_uplink(check_unlocked) var/list/L = current.get_all_contents() diff --git a/code/datums/mutations/fire_breath.dm b/code/datums/mutations/fire_breath.dm index 5b32c16b3c7..97e5459f82c 100644 --- a/code/datums/mutations/fire_breath.dm +++ b/code/datums/mutations/fire_breath.dm @@ -76,10 +76,8 @@ living_cast_on.setDir(original_dir) /datum/action/cooldown/spell/cone/staggered/fire_breath/calculate_cone_shape(current_level) - // This makes the cone shoot out into a 3 wide column of flames. - // You may be wondering, "that equation doesn't seem like it'd make a 3 wide column" - // well it does, and that's all that matters. - return (2 * current_level) - 1 + // This makes the cone shoot out into a 3 wide column of flames no matter the distance + return 3 /datum/action/cooldown/spell/cone/staggered/fire_breath/do_turf_cone_effect(turf/target_turf, atom/caster, level) // Further turfs experience less exposed_temperature and exposed_volume diff --git a/code/datums/mutations/touch.dm b/code/datums/mutations/touch.dm index 806dfe20d83..0964492d1ca 100644 --- a/code/datums/mutations/touch.dm +++ b/code/datums/mutations/touch.dm @@ -32,6 +32,7 @@ cooldown_time = 12 SECONDS invocation_type = INVOCATION_NONE spell_requirements = NONE + antimagic_flags = NONE //Vars for zaps made when power chromosome is applied, ripped and toned down from reactive tesla armor code. ///This var decides if the spell should chain, dictated by presence of power chromosome diff --git a/code/datums/status_effects/gas.dm b/code/datums/status_effects/gas.dm index 00353be157f..29b2cccf821 100644 --- a/code/datums/status_effects/gas.dm +++ b/code/datums/status_effects/gas.dm @@ -52,6 +52,10 @@ duration = 8 can_melt = FALSE +/datum/status_effect/freon/lasting + id = "lasting_frozen" + duration = -1 + /datum/status_effect/hypernob_protection id = "hypernob_protection" duration = 10 SECONDS @@ -80,4 +84,3 @@ var/mob/living/carbon/human/human_owner = owner human_owner.remove_movespeed_modifier(/datum/movespeed_modifier/reagent/hypernoblium) REMOVE_TRAIT(human_owner, TRAIT_NOFIRE, type) - diff --git a/code/modules/antagonists/heretic/heretic_antag.dm b/code/modules/antagonists/heretic/heretic_antag.dm index 1c13a7a257e..8aa5711bded 100644 --- a/code/modules/antagonists/heretic/heretic_antag.dm +++ b/code/modules/antagonists/heretic/heretic_antag.dm @@ -49,15 +49,8 @@ var/static/list/scribing_tools = typecacheof(list(/obj/item/pen, /obj/item/toy/crayon)) /// A blacklist of turfs we cannot scribe on. var/static/list/blacklisted_rune_turfs = typecacheof(list(/turf/open/space, /turf/open/openspace, /turf/open/lava, /turf/open/chasm)) - -/datum/antagonist/heretic/Destroy() - LAZYNULL(sac_targets) - return ..() - -/datum/antagonist/heretic/ui_data(mob/user) - var/list/data = list() - - var/static/list/path_to_color = list( + /// Static list of what each path converts to in the UI (colors are TGUI colors) + var/static/list/path_to_ui_color = list( PATH_START = "grey", PATH_SIDE = "green", PATH_RUST = "brown", @@ -67,8 +60,20 @@ PATH_BLADE = "label", // my favorite color is label ) - data["charges"] = knowledge_points +/datum/antagonist/heretic/Destroy() + LAZYNULL(sac_targets) + return ..() +/datum/antagonist/heretic/ui_data(mob/user) + var/list/data = list() + + data["charges"] = knowledge_points + data["total_sacrifices"] = total_sacrifices + data["ascended"] = ascended + + // This should be cached in some way, but the fact that final knowledge + // has to update its disabled state based on whether all objectives are complete, + // makes this very difficult. I'll figure it out one day maybe for(var/datum/heretic_knowledge/knowledge as anything in get_researchable_knowledge()) var/list/knowledge_data = list() knowledge_data["path"] = knowledge @@ -83,10 +88,17 @@ knowledge_data["disabled"] = !can_ascend() knowledge_data["hereticPath"] = initial(knowledge.route) - knowledge_data["color"] = path_to_color[initial(knowledge.route)] || "grey" + knowledge_data["color"] = path_to_ui_color[initial(knowledge.route)] || "grey" data["learnableKnowledge"] += list(knowledge_data) + return data + +/datum/antagonist/heretic/ui_static_data(mob/user) + var/list/data = list() + + data["objectives"] = get_objectives() + for(var/path in researched_knowledge) var/list/knowledge_data = list() var/datum/heretic_knowledge/found_knowledge = researched_knowledge[path] @@ -95,21 +107,12 @@ knowledge_data["gainFlavor"] = found_knowledge.gain_text knowledge_data["cost"] = found_knowledge.cost knowledge_data["hereticPath"] = found_knowledge.route - knowledge_data["color"] = path_to_color[found_knowledge.route] || "grey" + knowledge_data["color"] = path_to_ui_color[found_knowledge.route] || "grey" data["learnedKnowledge"] += list(knowledge_data) return data -/datum/antagonist/heretic/ui_static_data(mob/user) - var/list/data = list() - - data["total_sacrifices"] = total_sacrifices - data["ascended"] = ascended - data["objectives"] = get_objectives() - - return data - /datum/antagonist/heretic/ui_act(action, params) . = ..() if(.) @@ -122,9 +125,9 @@ CRASH("Heretic attempted to learn non-heretic_knowledge path! (Got: [researched_path])") if(initial(researched_path.cost) > knowledge_points) - return + return TRUE if(!gain_knowledge(researched_path)) - return + return TRUE log_heretic_knowledge("[key_name(owner)] gained knowledge: [initial(researched_path.name)]") knowledge_points -= initial(researched_path.cost) @@ -183,7 +186,7 @@ /datum/antagonist/heretic/on_removal() for(var/knowledge_index in researched_knowledge) var/datum/heretic_knowledge/knowledge = researched_knowledge[knowledge_index] - knowledge.on_lose(owner.current) + knowledge.on_lose(owner.current, src) GLOB.reality_smash_track.remove_tracked_mind(owner) QDEL_LIST_ASSOC_VAL(researched_knowledge) @@ -218,8 +221,8 @@ . = ..() for(var/knowledge_index in researched_knowledge) var/datum/heretic_knowledge/knowledge = researched_knowledge[knowledge_index] - knowledge.on_lose(old_body) - knowledge.on_gain(new_body) + knowledge.on_lose(old_body, src) + knowledge.on_gain(new_body, src) /* * Signal proc for [COMSIG_MOB_BEFORE_SPELL_CAST] and [COMSIG_MOB_SPELL_ACTIVATED]. @@ -476,7 +479,7 @@ .["Adjust Knowledge Points"] = CALLBACK(src, PROC_REF(admin_change_points)) -/* +/** * Admin proc for giving a heretic a Living Heart easily. */ /datum/antagonist/heretic/proc/give_living_heart(mob/admin) @@ -489,9 +492,9 @@ to_chat(admin, span_warning("The heretic doesn't have a living heart knowledge for some reason. What?")) return - heart_knowledge.on_research(owner.current) + heart_knowledge.on_research(owner.current, src) -/* +/** * Admin proc for adding a marked mob to a heretic's sac list. */ /datum/antagonist/heretic/proc/add_marked_as_target(mob/admin) @@ -510,7 +513,7 @@ add_sacrifice_target(new_target) -/* +/** * Admin proc for removing a mob from a heretic's sac list. */ /datum/antagonist/heretic/proc/remove_target(mob/admin) @@ -536,7 +539,7 @@ if(tgui_alert(admin, "Let them know their targets have been updated?", "Whispers of the Mansus", list("Yes", "No")) == "Yes") to_chat(owner.current, span_danger("The Mansus has modified your targets.")) -/* +/** * Admin proc for easily adding / removing knowledge points. */ /datum/antagonist/heretic/proc/admin_change_points(mob/admin) @@ -575,7 +578,7 @@ . += "None!
" . += "
" -/* +/** * Learns the passed [typepath] of knowledge, creating a knowledge datum * and adding it to our researched knowledge list. * @@ -589,10 +592,11 @@ return FALSE var/datum/heretic_knowledge/initialized_knowledge = new knowledge_type() researched_knowledge[knowledge_type] = initialized_knowledge - initialized_knowledge.on_research(owner.current) + initialized_knowledge.on_research(owner.current, src) + update_static_data(owner.current) return TRUE -/* +/** * Get a list of all knowledge TYPEPATHS that we can currently research. */ /datum/antagonist/heretic/proc/get_researchable_knowledge() @@ -606,13 +610,13 @@ researchable_knowledge -= banned_knowledge return researchable_knowledge -/* +/** * Check if the wanted type-path is in the list of research knowledge. */ /datum/antagonist/heretic/proc/get_knowledge(wanted) return researched_knowledge[wanted] -/* +/** * Get a list of all rituals this heretic can invoke on a rune. * Iterates over all of our knowledge and, if we can invoke it, adds it to our list. * @@ -629,7 +633,7 @@ return sortTim(rituals, GLOBAL_PROC_REF(cmp_heretic_knowledge), associative = TRUE) -/* +/** * Checks to see if our heretic can ccurrently ascend. * * Returns FALSE if not all of our objectives are complete, or TRUE otherwise. @@ -640,7 +644,7 @@ return FALSE return TRUE -/* +/** * Helper to determine if a Heretic * - Has a Living Heart * - Has a an organ in the correct slot that isn't a living heart @@ -666,7 +670,7 @@ /datum/objective/minor_sacrifice/New(text) . = ..() - target_amount = rand(2, 3) + target_amount = rand(3, 4) update_explanation_text() /datum/objective/minor_sacrifice/update_explanation_text() diff --git a/code/modules/antagonists/heretic/heretic_knowledge.dm b/code/modules/antagonists/heretic/heretic_knowledge.dm index 3296fc74e93..89bc9b978d8 100644 --- a/code/modules/antagonists/heretic/heretic_knowledge.dm +++ b/code/modules/antagonists/heretic/heretic_knowledge.dm @@ -50,14 +50,15 @@ * This is only ever called once per heretic. * * Arguments - * * user - the heretic who researched something + * * user - The heretic who researched something + * * our_heretic - The antag datum of who researched us. This should never be null. */ -/datum/heretic_knowledge/proc/on_research(mob/user) +/datum/heretic_knowledge/proc/on_research(mob/user, datum/antagonist/heretic/our_heretic) SHOULD_CALL_PARENT(TRUE) if(gain_text) to_chat(user, span_warning("[gain_text]")) - on_gain(user) + on_gain(user, our_heretic) /** * Called when the knowledge is applied to a mob. @@ -66,8 +67,9 @@ * * Arguments * * user - the heretic which we're applying things to + * * our_heretic - The antag datum of who gained us. This should never be null. */ -/datum/heretic_knowledge/proc/on_gain(mob/user) +/datum/heretic_knowledge/proc/on_gain(mob/user, datum/antagonist/heretic/our_heretic) return /** @@ -76,8 +78,9 @@ * * Arguments * * user - the heretic which we're removing things from + * * our_heretic - The antag datum of who is losing us. This should never be null. */ -/datum/heretic_knowledge/proc/on_lose(mob/user) +/datum/heretic_knowledge/proc/on_lose(mob/user, datum/antagonist/heretic/our_heretic) return /** @@ -165,7 +168,7 @@ selected_atoms -= sacrificed qdel(sacrificed) -/* +/** * A knowledge subtype that grants the heretic a certain spell. */ /datum/heretic_knowledge/spell @@ -179,7 +182,7 @@ QDEL_NULL(created_spell_ref) return ..() -/datum/heretic_knowledge/spell/on_gain(mob/user) +/datum/heretic_knowledge/spell/on_gain(mob/user, datum/antagonist/heretic/our_heretic) // Added spells are tracked on the body, and not the mind, // because we handle heretic mind transfers // via the antag datum (on_gain and on_lose). @@ -187,11 +190,11 @@ created_spell.Grant(user) created_spell_ref = WEAKREF(created_spell) -/datum/heretic_knowledge/spell/on_lose(mob/user) +/datum/heretic_knowledge/spell/on_lose(mob/user, datum/antagonist/heretic/our_heretic) var/datum/action/cooldown/spell/created_spell = created_spell_ref?.resolve() created_spell?.Remove(user) -/* +/** * A knowledge subtype for knowledge that can only * have a limited amount of it's resulting atoms * created at once. @@ -225,7 +228,7 @@ LAZYADD(created_items, WEAKREF(created_thing)) return TRUE -/* +/** * A knowledge subtype for limited_amount knowledge * used for base knowledge (the ones that make blades) * @@ -247,11 +250,12 @@ continue banned_knowledge += final_knowledge_type -/datum/heretic_knowledge/limited_amount/starting/on_research(mob/user) +/datum/heretic_knowledge/limited_amount/starting/on_research(mob/user, datum/antagonist/heretic/our_heretic) . = ..() + our_heretic.heretic_path = route SSblackbox.record_feedback("tally", "heretic_path_taken", 1, route) -/* +/** * A knowledge subtype for heretic knowledge * that applies a mark on use. * @@ -264,11 +268,11 @@ /// The status effect typepath we apply on people on mansus grasp. var/datum/status_effect/eldritch/mark_type -/datum/heretic_knowledge/mark/on_gain(mob/user) +/datum/heretic_knowledge/mark/on_gain(mob/user, datum/antagonist/heretic/our_heretic) RegisterSignal(user, COMSIG_HERETIC_MANSUS_GRASP_ATTACK, PROC_REF(on_mansus_grasp)) RegisterSignal(user, COMSIG_HERETIC_BLADE_ATTACK, PROC_REF(on_eldritch_blade)) -/datum/heretic_knowledge/mark/on_lose(mob/user) +/datum/heretic_knowledge/mark/on_lose(mob/user, datum/antagonist/heretic/our_heretic) UnregisterSignal(user, list(COMSIG_HERETIC_MANSUS_GRASP_ATTACK, COMSIG_HERETIC_BLADE_ATTACK)) /** @@ -316,7 +320,7 @@ mark.on_effect() return TRUE -/* +/** * A knowledge subtype for heretic knowledge that * upgrades their sickly blade, either on melee or range. * @@ -327,11 +331,11 @@ mutually_exclusive = TRUE cost = 2 -/datum/heretic_knowledge/blade_upgrade/on_gain(mob/user) +/datum/heretic_knowledge/blade_upgrade/on_gain(mob/user, datum/antagonist/heretic/our_heretic) RegisterSignal(user, COMSIG_HERETIC_BLADE_ATTACK, PROC_REF(on_eldritch_blade)) RegisterSignal(user, COMSIG_HERETIC_RANGED_BLADE_ATTACK, PROC_REF(on_ranged_eldritch_blade)) -/datum/heretic_knowledge/blade_upgrade/on_lose(mob/user) +/datum/heretic_knowledge/blade_upgrade/on_lose(mob/user, datum/antagonist/heretic/our_heretic) UnregisterSignal(user, list(COMSIG_HERETIC_BLADE_ATTACK, COMSIG_HERETIC_RANGED_BLADE_ATTACK)) @@ -369,7 +373,7 @@ /datum/heretic_knowledge/blade_upgrade/proc/do_ranged_effects(mob/living/source, mob/living/target, obj/item/melee/sickly_blade/blade) return -/* +/** * A knowledge subtype lets the heretic curse someone with a ritual. */ /datum/heretic_knowledge/curse @@ -492,7 +496,7 @@ chosen_mob.remove_filter(name) -/* +/** * A knowledge subtype lets the heretic summon a monster with the ritual. */ /datum/heretic_knowledge/summon @@ -540,7 +544,7 @@ /// The amount of knowledge points the knowledge ritual gives on success. #define KNOWLEDGE_RITUAL_POINTS 4 -/* +/** * A subtype of knowledge that generates random ritual components. */ /datum/heretic_knowledge/knowledge_ritual @@ -599,7 +603,7 @@ // 1 uncommon item. required_atoms[pick(potential_uncommoner_items)] += 1 -/datum/heretic_knowledge/knowledge_ritual/on_research(mob/user) +/datum/heretic_knowledge/knowledge_ritual/on_research(mob/user, datum/antagonist/heretic/our_heretic) . = ..() var/list/requirements_string = list() @@ -634,7 +638,7 @@ #undef KNOWLEDGE_RITUAL_POINTS -/* +/** * The special final tier of knowledges that unlocks ASCENSION. */ /datum/heretic_knowledge/ultimate @@ -644,16 +648,15 @@ priority = MAX_KNOWLEDGE_PRIORITY + 1 // Yes, the final ritual should be ABOVE the max priority. required_atoms = list(/mob/living/carbon/human = 3) -/datum/heretic_knowledge/ultimate/on_research(mob/user) +/datum/heretic_knowledge/ultimate/on_research(mob/user, datum/antagonist/heretic/our_heretic) . = ..() - var/datum/antagonist/heretic/heretic_datum = IS_HERETIC(user) var/total_points = 0 - for(var/datum/heretic_knowledge/knowledge as anything in flatten_list(heretic_datum.researched_knowledge)) + for(var/datum/heretic_knowledge/knowledge as anything in flatten_list(our_heretic.researched_knowledge)) total_points += knowledge.cost log_heretic_knowledge("[key_name(user)] gained knowledge of their final ritual at [worldtime2text()]. \ - They have [length(heretic_datum.researched_knowledge)] knowledge nodes researched, totalling [total_points] points \ - and have sacrificed [heretic_datum.total_sacrifices] people ([heretic_datum.high_value_sacrifices] of which were high value)") + They have [length(our_heretic.researched_knowledge)] knowledge nodes researched, totalling [total_points] points \ + and have sacrificed [our_heretic.total_sacrifices] people ([our_heretic.high_value_sacrifices] of which were high value)") /datum/heretic_knowledge/ultimate/can_be_invoked(datum/antagonist/heretic/invoker) if(invoker.ascended) @@ -689,6 +692,9 @@ var/datum/antagonist/heretic/heretic_datum = IS_HERETIC(user) heretic_datum.ascended = TRUE + // Show the cool red gradiant in our UI + heretic_datum.update_static_data(user) + if(ishuman(user)) var/mob/living/carbon/human/human_user = user human_user.physiology.brute_mod *= 0.5 diff --git a/code/modules/antagonists/heretic/influences.dm b/code/modules/antagonists/heretic/influences.dm index 6b53a08963f..d24a1b6e637 100644 --- a/code/modules/antagonists/heretic/influences.dm +++ b/code/modules/antagonists/heretic/influences.dm @@ -1,6 +1,6 @@ /// The number of influences spawned per heretic -#define NUM_INFLUENCES_PER_HERETIC 4 +#define NUM_INFLUENCES_PER_HERETIC 5 /** * #Reality smash tracker @@ -65,10 +65,6 @@ * and the number of minds we're tracking. */ /datum/reality_smash_tracker/proc/generate_new_influences() - // 1 heretic = 4 influences - // 2 heretics = 7 influences - // 3 heretics = 9 influences - // 4 heretics = 10 influences, +1 for each onwards. var/how_many_can_we_make = 0 for(var/heretic_number in 1 to length(tracked_heretics)) how_many_can_we_make += max(NUM_INFLUENCES_PER_HERETIC - heretic_number + 1, 1) diff --git a/code/modules/antagonists/heretic/knowledge/ash_lore.dm b/code/modules/antagonists/heretic/knowledge/ash_lore.dm index 551d17d341b..b83c7d43b58 100644 --- a/code/modules/antagonists/heretic/knowledge/ash_lore.dm +++ b/code/modules/antagonists/heretic/knowledge/ash_lore.dm @@ -12,6 +12,7 @@ * * Mark of Ash * Ritual of Knowledge + * Fire Blast * Mask of Madness * > Sidepaths: * Curse of Corrosion @@ -39,11 +40,6 @@ result_atoms = list(/obj/item/melee/sickly_blade/ash) route = PATH_ASH -/datum/heretic_knowledge/limited_amount/starting/base_ash/on_research(mob/user) - . = ..() - var/datum/antagonist/heretic/our_heretic = IS_HERETIC(user) - our_heretic.heretic_path = route - /datum/heretic_knowledge/ashen_grasp name = "Grasp of Ash" desc = "Your Mansus Grasp will burn the eyes of the victim, causing damage and blindness." @@ -53,10 +49,10 @@ cost = 1 route = PATH_ASH -/datum/heretic_knowledge/ashen_grasp/on_gain(mob/user) +/datum/heretic_knowledge/ashen_grasp/on_gain(mob/user, datum/antagonist/heretic/our_heretic) RegisterSignal(user, COMSIG_HERETIC_MANSUS_GRASP_ATTACK, PROC_REF(on_mansus_grasp)) -/datum/heretic_knowledge/ashen_grasp/on_lose(mob/user) +/datum/heretic_knowledge/ashen_grasp/on_lose(mob/user, datum/antagonist/heretic/our_heretic) UnregisterSignal(user, COMSIG_HERETIC_MANSUS_GRASP_ATTACK) /datum/heretic_knowledge/ashen_grasp/proc/on_mansus_grasp(mob/living/source, mob/living/target) @@ -110,9 +106,21 @@ grasp.UpdateButtons() /datum/heretic_knowledge/knowledge_ritual/ash - next_knowledge = list(/datum/heretic_knowledge/mad_mask) + next_knowledge = list(/datum/heretic_knowledge/spell/fire_blast) route = PATH_ASH +/datum/heretic_knowledge/spell/fire_blast + name = "Volcano Blast" + desc = "Grants you Volcano Blast, a spell that - after a short charge - fires off a beam of energy \ + at a nearby enemy, setting them on fire and burning them. If they do not extinguish themselves, \ + the beam will continue to another target." + gain_text = "No fire was hot enough to rekindle them. No fire was bright enough to save them. No fire is eternal." + next_knowledge = list(/datum/heretic_knowledge/mad_mask) + spell_to_add = /datum/action/cooldown/spell/charged/beam/fire_blast + cost = 1 + route = PATH_ASH + + /datum/heretic_knowledge/mad_mask name = "Mask of Madness" desc = "Allows you to transmute any mask, four candles, a stun baton, and a liver to create a Mask of Madness. \ @@ -209,6 +217,12 @@ var/datum/action/cooldown/spell/fire_cascade/big/screen_wide_fire_spell = new(user.mind) screen_wide_fire_spell.Grant(user) + var/datum/action/cooldown/spell/charged/beam/fire_blast/existing_beam_spell = locate() in user.actions + if(existing_beam_spell) + existing_beam_spell.max_beam_bounces *= 2 // Double beams + existing_beam_spell.beam_duration *= 0.66 // Faster beams + existing_beam_spell.cooldown_time *= 0.66 // Lower cooldown + user.client?.give_award(/datum/award/achievement/misc/ash_ascension, user) for(var/trait in traits_to_apply) ADD_TRAIT(user, trait, MAGIC_TRAIT) diff --git a/code/modules/antagonists/heretic/knowledge/blade_lore.dm b/code/modules/antagonists/heretic/knowledge/blade_lore.dm index 26aeeec86e1..c354690d12a 100644 --- a/code/modules/antagonists/heretic/knowledge/blade_lore.dm +++ b/code/modules/antagonists/heretic/knowledge/blade_lore.dm @@ -12,6 +12,7 @@ * * Mark of the Blade * Ritual of Knowledge + * Realignment * Stance of the Scarred Duelist * > Sidepaths: * Carving Knife @@ -49,10 +50,10 @@ cost = 1 route = PATH_BLADE -/datum/heretic_knowledge/blade_grasp/on_gain(mob/user) +/datum/heretic_knowledge/blade_grasp/on_gain(mob/user, datum/antagonist/heretic/our_heretic) RegisterSignal(user, COMSIG_HERETIC_MANSUS_GRASP_ATTACK, PROC_REF(on_mansus_grasp)) -/datum/heretic_knowledge/blade_grasp/on_lose(mob/user) +/datum/heretic_knowledge/blade_grasp/on_lose(mob/user, datum/antagonist/heretic/our_heretic) UnregisterSignal(user, COMSIG_HERETIC_MANSUS_GRASP_ATTACK) /datum/heretic_knowledge/blade_grasp/proc/on_mansus_grasp(mob/living/source, mob/living/target) @@ -112,10 +113,10 @@ /// Used instead of cooldowns, so we can give feedback when it's ready again var/riposte_ready = TRUE -/datum/heretic_knowledge/blade_dance/on_gain(mob/user) +/datum/heretic_knowledge/blade_dance/on_gain(mob/user, datum/antagonist/heretic/our_heretic) RegisterSignal(user, COMSIG_HUMAN_CHECK_SHIELDS, PROC_REF(on_shield_reaction)) -/datum/heretic_knowledge/blade_dance/on_lose(mob/user) +/datum/heretic_knowledge/blade_dance/on_lose(mob/user, datum/antagonist/heretic/our_heretic) UnregisterSignal(user, COMSIG_HUMAN_CHECK_SHIELDS) /datum/heretic_knowledge/blade_dance/proc/on_shield_reaction( @@ -201,12 +202,11 @@ /datum/heretic_knowledge/mark/blade_mark/create_mark(mob/living/source, mob/living/target) var/datum/status_effect/eldritch/blade/blade_mark = ..() - if(!istype(blade_mark)) - return - - var/area/to_lock_to = get_area(target) - blade_mark.locked_to = to_lock_to - to_chat(target, span_hypnophrase("An otherworldly force is compelling you to stay in [get_area_name(to_lock_to)]!")) + if(istype(blade_mark)) + var/area/to_lock_to = get_area(target) + blade_mark.locked_to = to_lock_to + to_chat(target, span_hypnophrase("An otherworldly force is compelling you to stay in [get_area_name(to_lock_to)]!")) + return blade_mark /datum/heretic_knowledge/mark/blade_mark/trigger_mark(mob/living/source, mob/living/target) . = ..() @@ -215,7 +215,18 @@ source.apply_status_effect(/datum/status_effect/protective_blades, 60 SECONDS, 1, 20, 0 SECONDS) /datum/heretic_knowledge/knowledge_ritual/blade + next_knowledge = list(/datum/heretic_knowledge/spell/realignment) + route = PATH_BLADE + +/datum/heretic_knowledge/spell/realignment + name = "Realignment" + desc = "Grants you Realignment a spell that wil realign your body rapidly for a short period. \ + During this process, you will rapidly regenerate stamina and stuns, however, you will be unable to attack. \ + This spell can be casted in rapid succession, but doing so will increase the cooldown." + gain_text = "In the flurry of death, he found peace within himself. Despite insurmountable odds, he forged on." next_knowledge = list(/datum/heretic_knowledge/duel_stance) + spell_to_add = /datum/action/cooldown/spell/realignment + cost = 1 route = PATH_BLADE /// The amount of blood flow reduced per level of severity of gained bleeding wounds for Stance of the Torn Champion. @@ -239,7 +250,7 @@ /// Whether we're currently in duelist stance, gaining certain buffs (low health) var/in_duelist_stance = FALSE -/datum/heretic_knowledge/duel_stance/on_gain(mob/user) +/datum/heretic_knowledge/duel_stance/on_gain(mob/user, datum/antagonist/heretic/our_heretic) ADD_TRAIT(user, TRAIT_NODISMEMBER, type) RegisterSignal(user, COMSIG_PARENT_EXAMINE, PROC_REF(on_examine)) RegisterSignal(user, COMSIG_CARBON_GAIN_WOUND, PROC_REF(on_wound_gain)) @@ -247,7 +258,7 @@ on_health_update(user) // Run this once, so if the knowledge is learned while hurt it activates properly -/datum/heretic_knowledge/duel_stance/on_lose(mob/user) +/datum/heretic_knowledge/duel_stance/on_lose(mob/user, datum/antagonist/heretic/our_heretic) REMOVE_TRAIT(user, TRAIT_NODISMEMBER, type) if(in_duelist_stance) REMOVE_TRAIT(user, TRAIT_HARDLY_WOUNDED, type) @@ -298,6 +309,10 @@ a flurry of blades, neither hitting their mark, for the Champion was indomitable." next_knowledge = list(/datum/heretic_knowledge/spell/furious_steel) route = PATH_BLADE + /// How much force do we apply to the offhand? + var/offand_force_decrement = 0 + /// How much force was the last weapon we offhanded with? If it's different, we need to re-calculate the decrement + var/last_weapon_force = -1 /datum/heretic_knowledge/blade_upgrade/blade/do_melee_effects(mob/living/source, mob/living/target, obj/item/melee/sickly_blade/blade) if(target == source) @@ -317,18 +332,36 @@ /datum/heretic_knowledge/blade_upgrade/blade/proc/follow_up_attack(mob/living/source, mob/living/target, obj/item/melee/sickly_blade/blade) if(QDELETED(source) || QDELETED(target) || QDELETED(blade)) return - // Sanity to ensure that the blade we're delivering - // an offhand attack with is actually our offhand + // Sanity to ensure that the blade we're delivering an offhand attack with is ACTUALLY our offhand if(blade != source.get_inactive_held_item()) return + // And we easily could've moved away if(!source.Adjacent(target)) return - // Blade are 17 force: 17 + 17 = 34 (3 hits to crit, unarmored) - // So, -5 force is put on the offhand blade: 17 + 12 = 29 (4 hits to crit, unarmored) - blade.force -= 5 + // Check if we need to recaclulate our offhand force + // This is just so we don't run this block every attack, that's wasteful + if(last_weapon_force != blade.force) + offand_force_decrement = 0 + // We want to make sure that the offhand blade increases their hits to crit by one, just about + // So, let's do some quick math. Yes this'll be inaccurate if their mainhand blade is modified (whetstone), no I don't care + // Find how much force we need to detract from the second blade + var/hits_to_crit_on_average = ROUND_UP(100 / (blade.force * 2)) + while(hits_to_crit_on_average <= 3) // 3 hits and beyond is a bit too absurd + if(offand_force_decrement + 2 > blade.force * 0.5) // But also cutting the force beyond half is absurd + break + + offand_force_decrement += 2 + hits_to_crit_on_average = ROUND_UP(100 / (blade.force * 2 - offand_force_decrement)) + + // Save the force as our last weapon force + last_weapon_force = blade.force + // Subtract the decrement + blade.force -= offand_force_decrement + // Perform the offhand attack blade.melee_attack_chain(source, target) - blade.force += 5 + // Restore the force. + blade.force = last_weapon_force /datum/heretic_knowledge/spell/furious_steel name = "Furious Steel" diff --git a/code/modules/antagonists/heretic/knowledge/flesh_lore.dm b/code/modules/antagonists/heretic/knowledge/flesh_lore.dm index 67684c00773..d315a566ea9 100644 --- a/code/modules/antagonists/heretic/knowledge/flesh_lore.dm +++ b/code/modules/antagonists/heretic/knowledge/flesh_lore.dm @@ -17,6 +17,7 @@ * * Mark of Flesh * Ritual of Knowledge + * Flesh Surgery * Raw Ritual * > Sidepaths: * Blood Siphon @@ -45,11 +46,8 @@ limit = 3 // Bumped up so they can arm up their ghouls too. route = PATH_FLESH -/datum/heretic_knowledge/limited_amount/starting/base_flesh/on_research(mob/user) +/datum/heretic_knowledge/limited_amount/starting/base_flesh/on_research(mob/user, datum/antagonist/heretic/our_heretic) . = ..() - var/datum/antagonist/heretic/our_heretic = IS_HERETIC(user) - our_heretic.heretic_path = route - var/datum/objective/heretic_summon/summon_objective = new() summon_objective.owner = our_heretic.owner our_heretic.objectives += summon_objective @@ -68,10 +66,10 @@ cost = 1 route = PATH_FLESH -/datum/heretic_knowledge/limited_amount/flesh_grasp/on_gain(mob/user) +/datum/heretic_knowledge/limited_amount/flesh_grasp/on_gain(mob/user, datum/antagonist/heretic/our_heretic) RegisterSignal(user, COMSIG_HERETIC_MANSUS_GRASP_ATTACK, PROC_REF(on_mansus_grasp)) -/datum/heretic_knowledge/limited_amount/flesh_grasp/on_lose(mob/user) +/datum/heretic_knowledge/limited_amount/flesh_grasp/on_lose(mob/user, datum/antagonist/heretic/our_heretic) UnregisterSignal(user, COMSIG_HERETIC_MANSUS_GRASP_ATTACK) /datum/heretic_knowledge/limited_amount/flesh_grasp/proc/on_mansus_grasp(mob/living/source, mob/living/target) @@ -219,7 +217,19 @@ mark_type = /datum/status_effect/eldritch/flesh /datum/heretic_knowledge/knowledge_ritual/flesh + next_knowledge = list(/datum/heretic_knowledge/spell/flesh_surgery) + route = PATH_FLESH + +/datum/heretic_knowledge/spell/flesh_surgery + name = "Knitting of Flesh" + desc = "Grants you the spell Knit Flesh. This spell allows you to remove organs from victims \ + without requiring a lengthy surgery. This process is much longer if the target is not dead. \ + This spell also allows you to heal your minions and summons, or restore failing organs to acceptable status." + gain_text = "But they were not out of my reach for long. With every step, the screams grew, until at last \ + I learned that they could be silenced." next_knowledge = list(/datum/heretic_knowledge/summon/raw_prophet) + spell_to_add = /datum/action/cooldown/spell/touch/flesh_surgery + cost = 1 route = PATH_FLESH /datum/heretic_knowledge/summon/raw_prophet diff --git a/code/modules/antagonists/heretic/knowledge/general_side.dm b/code/modules/antagonists/heretic/knowledge/general_side.dm index fafaa3f77ba..b771108e272 100644 --- a/code/modules/antagonists/heretic/knowledge/general_side.dm +++ b/code/modules/antagonists/heretic/knowledge/general_side.dm @@ -34,7 +34,7 @@ if(!target_finder) CRASH("Heretic datum didn't have a hunt_and_sacrifice knowledge learned, what?") - if(!target_finder.obtain_targets(user)) + if(!target_finder.obtain_targets(user, heretic_datum = heretic_datum)) loc.balloon_alert(user, "ritual failed, no targets found!") return FALSE diff --git a/code/modules/antagonists/heretic/knowledge/rust_lore.dm b/code/modules/antagonists/heretic/knowledge/rust_lore.dm index ac33a74387c..f18eefa9a8f 100644 --- a/code/modules/antagonists/heretic/knowledge/rust_lore.dm +++ b/code/modules/antagonists/heretic/knowledge/rust_lore.dm @@ -12,6 +12,7 @@ * * Mark of Rust * Ritual of Knowledge + * Rust Construction * Aggressive Spread * > Sidepaths: * Curse of Corrosion @@ -39,11 +40,6 @@ result_atoms = list(/obj/item/melee/sickly_blade/rust) route = PATH_RUST -/datum/heretic_knowledge/limited_amount/starting/base_rust/on_research(mob/user) - . = ..() - var/datum/antagonist/heretic/our_heretic = IS_HERETIC(user) - our_heretic.heretic_path = route - /datum/heretic_knowledge/rust_fist name = "Grasp of Rust" desc = "Your Mansus Grasp will deal 500 damage to non-living matter and rust any surface it touches. \ @@ -53,11 +49,11 @@ cost = 1 route = PATH_RUST -/datum/heretic_knowledge/rust_fist/on_gain(mob/user) +/datum/heretic_knowledge/rust_fist/on_gain(mob/user, datum/antagonist/heretic/our_heretic) RegisterSignal(user, COMSIG_HERETIC_MANSUS_GRASP_ATTACK, PROC_REF(on_mansus_grasp)) RegisterSignal(user, COMSIG_HERETIC_MANSUS_GRASP_ATTACK_SECONDARY, PROC_REF(on_secondary_mansus_grasp)) -/datum/heretic_knowledge/rust_fist/on_lose(mob/user) +/datum/heretic_knowledge/rust_fist/on_lose(mob/user, datum/antagonist/heretic/our_heretic) UnregisterSignal(user, list(COMSIG_HERETIC_MANSUS_GRASP_ATTACK, COMSIG_HERETIC_MANSUS_GRASP_ATTACK_SECONDARY)) /datum/heretic_knowledge/rust_fist/proc/on_mansus_grasp(mob/living/source, mob/living/target) @@ -87,11 +83,11 @@ cost = 1 route = PATH_RUST -/datum/heretic_knowledge/rust_regen/on_gain(mob/user) +/datum/heretic_knowledge/rust_regen/on_gain(mob/user, datum/antagonist/heretic/our_heretic) RegisterSignal(user, COMSIG_MOVABLE_MOVED, PROC_REF(on_move)) RegisterSignal(user, COMSIG_LIVING_LIFE, PROC_REF(on_life)) -/datum/heretic_knowledge/rust_regen/on_lose(mob/user) +/datum/heretic_knowledge/rust_regen/on_lose(mob/user, datum/antagonist/heretic/our_heretic) UnregisterSignal(user, list(COMSIG_MOVABLE_MOVED, COMSIG_LIVING_LIFE)) /* @@ -144,7 +140,18 @@ mark_type = /datum/status_effect/eldritch/rust /datum/heretic_knowledge/knowledge_ritual/rust + next_knowledge = list(/datum/heretic_knowledge/spell/rust_construction) + route = PATH_RUST + +/datum/heretic_knowledge/spell/rust_construction + name = "Rust Construction" + desc = "Grants you Rust Construction, a spell that allows you to raise a wall out of a rusted floor. \ + Anyone overtop the wall will be throw aside (or upwards) and sustain damage." + gain_text = "Images of foreign and ominous structures began to dance in my mind. Covered head to toe in thick rust, \ + they no longer looked man made. Or perhaps they never were in the first place." next_knowledge = list(/datum/heretic_knowledge/spell/area_conversion) + spell_to_add = /datum/action/cooldown/spell/pointed/rust_construction + cost = 1 route = PATH_RUST /datum/heretic_knowledge/spell/area_conversion @@ -221,7 +228,7 @@ TRAIT_NOBREATH, ) -/datum/heretic_knowledge/ultimate/rust_final/on_research(mob/user) +/datum/heretic_knowledge/ultimate/rust_final/on_research(mob/user, datum/antagonist/heretic/our_heretic) . = ..() // This map doesn't have a Bridge, for some reason?? // Let them complete the ritual anywhere diff --git a/code/modules/antagonists/heretic/knowledge/sacrifice_knowledge/sacrifice_knowledge.dm b/code/modules/antagonists/heretic/knowledge/sacrifice_knowledge/sacrifice_knowledge.dm index 3415b9f5603..0e629388134 100644 --- a/code/modules/antagonists/heretic/knowledge/sacrifice_knowledge/sacrifice_knowledge.dm +++ b/code/modules/antagonists/heretic/knowledge/sacrifice_knowledge/sacrifice_knowledge.dm @@ -16,6 +16,8 @@ cost = 0 priority = MAX_KNOWLEDGE_PRIORITY // Should be at the top route = PATH_START + /// How many targets do we generate? + var/num_targets_to_generate = 5 /// Whether we've generated a heretic sacrifice z-level yet, from any heretic. var/static/heretic_level_generated = FALSE /// A weakref to the mind of our heretic. @@ -23,27 +25,29 @@ /// Lazylist of minds that we won't pick as targets. var/list/datum/mind/target_blacklist /// An assoc list of [ref] to [timers] - a list of all the timers of people in the shadow realm currently - var/return_timers + var/list/return_timers /datum/heretic_knowledge/hunt_and_sacrifice/Destroy(force, ...) heretic_mind = null LAZYCLEARLIST(target_blacklist) return ..() -/datum/heretic_knowledge/hunt_and_sacrifice/on_research(mob/user, regained = FALSE) +/datum/heretic_knowledge/hunt_and_sacrifice/on_research(mob/user, datum/antagonist/heretic/our_heretic) . = ..() - obtain_targets(user, silent = TRUE) - heretic_mind = user.mind + obtain_targets(user, silent = TRUE, heretic_datum = our_heretic) + heretic_mind = our_heretic.owner if(!heretic_level_generated) heretic_level_generated = TRUE - message_admins("Generating z-level for heretic sacrifices...") + log_game("Generating z-level for heretic sacrifices...") INVOKE_ASYNC(src, PROC_REF(generate_heretic_z_level)) /// Generate the sacrifice z-level. /datum/heretic_knowledge/hunt_and_sacrifice/proc/generate_heretic_z_level() var/datum/map_template/heretic_sacrifice_level/new_level = new() if(!new_level.load_new_z()) - message_admins("The heretic sacrifice z-level failed to load. Any heretics are gonna have a field day disemboweling people, probably. Up to you if you're fine with it.") + log_game("The heretic sacrifice z-level failed to load.") + message_admins("The heretic sacrifice z-level failed to load. Heretic sacrifices won't be teleported to the shadow realm. \ + If you want, you can spawn an /obj/effect/landmark/heretic somewhere to stop that from happening.") CRASH("Failed to initialize heretic sacrifice z-level!") /datum/heretic_knowledge/hunt_and_sacrifice/recipe_snowflake_check(mob/living/user, list/atoms, list/selected_atoms, turf/loc) @@ -79,7 +83,7 @@ /datum/heretic_knowledge/hunt_and_sacrifice/on_finished_recipe(mob/living/user, list/selected_atoms, turf/loc) var/datum/antagonist/heretic/heretic_datum = IS_HERETIC(user) if(!LAZYLEN(heretic_datum.sac_targets)) - if(obtain_targets(user)) + if(obtain_targets(user, heretic_datum = heretic_datum)) return TRUE else loc.balloon_alert(user, "ritual failed, no targets found!") @@ -94,7 +98,7 @@ * * Returns FALSE if no targets are found, TRUE if the targets list was populated. */ -/datum/heretic_knowledge/hunt_and_sacrifice/proc/obtain_targets(mob/living/user, silent = FALSE) +/datum/heretic_knowledge/hunt_and_sacrifice/proc/obtain_targets(mob/living/user, silent = FALSE, datum/antagonist/heretic/heretic_datum) // First construct a list of minds that are valid objective targets. var/list/datum/mind/valid_targets = list() @@ -143,18 +147,12 @@ valid_targets -= department_mind break - // Final target, just get someone random. - final_targets += pick_n_take(valid_targets) - - // If any of our targets failed to aquire, - // Let's run a loop until we get four total, grabbing random targets. + // Now grab completely random targets until we'll full var/target_sanity = 0 - while(length(final_targets) < 4 && length(valid_targets) > 4 && target_sanity < 25) + while(length(final_targets) < num_targets_to_generate && length(valid_targets) > num_targets_to_generate && target_sanity < 25) final_targets += pick_n_take(valid_targets) target_sanity++ - var/datum/antagonist/heretic/heretic_datum = IS_HERETIC(user) - if(!silent) to_chat(user, span_danger("Your targets have been determined. Your Living Heart will allow you to track their position. Go and sacrifice them!")) @@ -216,9 +214,9 @@ if(!LAZYLEN(GLOB.heretic_sacrifice_landmarks)) CRASH("[type] - begin_sacrifice was called, but no heretic sacrifice landmarks were found!") - var/obj/effect/landmark/heretic/destination_landmark = GLOB.heretic_sacrifice_landmarks[our_heretic.heretic_path] + var/obj/effect/landmark/heretic/destination_landmark = GLOB.heretic_sacrifice_landmarks[our_heretic.heretic_path] || GLOB.heretic_sacrifice_landmarks[PATH_START] if(!destination_landmark) - CRASH("[type] - begin_sacrifice could not find a destination landmark to send the sacrifice! (heretic's path: [our_heretic.heretic_path])") + CRASH("[type] - begin_sacrifice could not find a destination landmark OR default landmark to send the sacrifice! (Heretic's path: [our_heretic.heretic_path])") var/turf/destination = get_turf(destination_landmark) @@ -241,6 +239,7 @@ // If our target is dead, try to revive them // and if we fail to revive them, don't proceede the chain + sac_target.adjustOxyLoss(-100, FALSE) if(!sac_target.heal_and_revive(50, span_danger("[sac_target]'s heart begins to beat with an unholy force as they return from death!"))) return @@ -283,7 +282,8 @@ // If our target died during the (short) wait timer, // and we fail to revive them (using a lower number than before), // just disembowel them and stop the chain - if(!sac_target.heal_and_revive(75, span_danger("[sac_target]'s heart begins to beat with an unholy force as they return from death!"))) + sac_target.adjustOxyLoss(-100, FALSE) + if(!sac_target.heal_and_revive(60, span_danger("[sac_target]'s heart begins to beat with an unholy force as they return from death!"))) disembowel_target(sac_target) return @@ -368,6 +368,7 @@ sac_target.remove_status_effect(/datum/status_effect/unholy_determination) sac_target.reagents?.del_reagent(/datum/reagent/inverse/helgrasp/heretic) sac_target.clear_mood_event("shadow_realm") + sac_target.gain_trauma(/datum/brain_trauma/mild/phobia/supernatural, TRAUMA_RESILIENCE_MAGIC) // Wherever we end up, we sure as hell won't be able to explain sac_target.adjust_timed_status_effect(40 SECONDS, /datum/status_effect/speech/slurring/heretic) diff --git a/code/modules/antagonists/heretic/knowledge/starting_lore.dm b/code/modules/antagonists/heretic/knowledge/starting_lore.dm index ad05e3f55e3..756d00f2eee 100644 --- a/code/modules/antagonists/heretic/knowledge/starting_lore.dm +++ b/code/modules/antagonists/heretic/knowledge/starting_lore.dm @@ -51,10 +51,9 @@ GLOBAL_LIST_INIT(heretic_start_knowledge, initialize_starting_knowledge()) /// The typepath of the organ type required for our heart. var/required_organ_type = /obj/item/organ/internal/heart -/datum/heretic_knowledge/living_heart/on_research(mob/user) +/datum/heretic_knowledge/living_heart/on_research(mob/user, datum/antagonist/heretic/our_heretic) . = ..() - var/datum/antagonist/heretic/our_heretic = IS_HERETIC(user) var/obj/item/organ/where_to_put_our_heart = user.getorganslot(our_heretic.living_heart_organ_slot) // Our heart slot is not valid to put a heart if(!is_valid_heart(where_to_put_our_heart)) @@ -93,8 +92,7 @@ GLOBAL_LIST_INIT(heretic_start_knowledge, initialize_starting_knowledge()) else to_chat(user, span_boldnotice("You don't have a heart, or any chest organs for that matter. You didn't get a Living Heart because of it.")) -/datum/heretic_knowledge/living_heart/on_lose(mob/user) - var/datum/antagonist/heretic/our_heretic = IS_HERETIC(user) +/datum/heretic_knowledge/living_heart/on_lose(mob/user, datum/antagonist/heretic/our_heretic) var/obj/item/organ/our_living_heart = user.getorganslot(our_heretic.living_heart_organ_slot) if(our_living_heart) qdel(our_living_heart.GetComponent(/datum/component/living_heart)) diff --git a/code/modules/antagonists/heretic/knowledge/void_lore.dm b/code/modules/antagonists/heretic/knowledge/void_lore.dm index f7c5489fcca..df7e921273d 100644 --- a/code/modules/antagonists/heretic/knowledge/void_lore.dm +++ b/code/modules/antagonists/heretic/knowledge/void_lore.dm @@ -12,6 +12,7 @@ * * Mark of Void * Ritual of Knowledge + * Cone of Cold * Void Phase * > Sidepaths: * Carving Knife @@ -37,11 +38,6 @@ result_atoms = list(/obj/item/melee/sickly_blade/void) route = PATH_VOID -/datum/heretic_knowledge/limited_amount/starting/base_void/on_research(mob/user) - . = ..() - var/datum/antagonist/heretic/our_heretic = IS_HERETIC(user) - our_heretic.heretic_path = route - /datum/heretic_knowledge/limited_amount/starting/base_void/recipe_snowflake_check(mob/living/user, list/atoms, list/selected_atoms, turf/loc) if(!isopenturf(loc)) loc.balloon_alert(user, "ritual failed, invalid location!") @@ -63,10 +59,10 @@ cost = 1 route = PATH_VOID -/datum/heretic_knowledge/void_grasp/on_gain(mob/user) +/datum/heretic_knowledge/void_grasp/on_gain(mob/user, datum/antagonist/heretic/our_heretic) RegisterSignal(user, COMSIG_HERETIC_MANSUS_GRASP_ATTACK, PROC_REF(on_mansus_grasp)) -/datum/heretic_knowledge/void_grasp/on_lose(mob/user) +/datum/heretic_knowledge/void_grasp/on_lose(mob/user, datum/antagonist/heretic/our_heretic) UnregisterSignal(user, COMSIG_HERETIC_MANSUS_GRASP_ATTACK) /datum/heretic_knowledge/void_grasp/proc/on_mansus_grasp(mob/living/source, mob/living/target) @@ -94,11 +90,11 @@ cost = 1 route = PATH_VOID -/datum/heretic_knowledge/cold_snap/on_gain(mob/user) +/datum/heretic_knowledge/cold_snap/on_gain(mob/user, datum/antagonist/heretic/our_heretic) ADD_TRAIT(user, TRAIT_RESISTCOLD, type) ADD_TRAIT(user, TRAIT_NOBREATH, type) -/datum/heretic_knowledge/cold_snap/on_lose(mob/user) +/datum/heretic_knowledge/cold_snap/on_lose(mob/user, datum/antagonist/heretic/our_heretic) REMOVE_TRAIT(user, TRAIT_RESISTCOLD, type) REMOVE_TRAIT(user, TRAIT_NOBREATH, type) @@ -113,15 +109,26 @@ mark_type = /datum/status_effect/eldritch/void /datum/heretic_knowledge/knowledge_ritual/void + next_knowledge = list(/datum/heretic_knowledge/spell/void_cone) + route = PATH_VOID + +/datum/heretic_knowledge/spell/void_cone + name = "Void Blast" + desc = "Grants you Void Blast, a spell that shoots out a freezing blast in a code ahead of you, \ + freezing the ground and any victims within." + gain_text = "Every door I open racks my body. I am afraid of what is behind them. Someone is expecting me, \ + and my legs start to drag. Is that... snow?" next_knowledge = list(/datum/heretic_knowledge/spell/void_phase) + spell_to_add = /datum/action/cooldown/spell/cone/staggered/cone_of_cold/void + cost = 1 route = PATH_VOID /datum/heretic_knowledge/spell/void_phase name = "Void Phase" desc = "Grants you Void Phase, a long range targeted teleport spell. \ Additionally causes damage to heathens around your original and target destination." - gain_text = "The entity calls themself the Aristocrat. They effortlessly walk through air like\ - nothing leaving a harsh, cold breeze in their wake. They disappear, and I am left in the snow." + gain_text = "The entity calls themself the Aristocrat. They effortlessly walk through air like \ + nothing - leaving a harsh, cold breeze in their wake. They disappear, and I am left in the blizzard." next_knowledge = list( /datum/heretic_knowledge/blade_upgrade/void, /datum/heretic_knowledge/reroll_targets, @@ -204,7 +211,7 @@ RegisterSignal(user, COMSIG_LIVING_LIFE, PROC_REF(on_life)) RegisterSignal(user, COMSIG_LIVING_DEATH, PROC_REF(on_death)) -/datum/heretic_knowledge/ultimate/void_final/on_lose(mob/user) +/datum/heretic_knowledge/ultimate/void_final/on_lose(mob/user, datum/antagonist/heretic/our_heretic) on_death() // Losing is pretty much dying. I think RegisterSignal(user, list(COMSIG_LIVING_LIFE, COMSIG_LIVING_DEATH)) @@ -243,7 +250,7 @@ * * Stop the storm when the heretic passes away. */ -/datum/heretic_knowledge/ultimate/void_final/proc/on_death() +/datum/heretic_knowledge/ultimate/void_final/proc/on_death(datum/source) SIGNAL_HANDLER if(sound_loop) diff --git a/code/modules/antagonists/heretic/magic/blood_cleave.dm b/code/modules/antagonists/heretic/magic/blood_cleave.dm index 8bcd35c1107..c62312f3dd4 100644 --- a/code/modules/antagonists/heretic/magic/blood_cleave.dm +++ b/code/modules/antagonists/heretic/magic/blood_cleave.dm @@ -7,15 +7,18 @@ ranged_mousepointer = 'icons/effects/mouse_pointers/throw_target.dmi' school = SCHOOL_FORBIDDEN - cooldown_time = 35 SECONDS + cooldown_time = 40 SECONDS - invocation = "CL'VE" + invocation = "CL'VE!" invocation_type = INVOCATION_WHISPER spell_requirements = NONE cast_range = 9 + /// The radius of the cleave effect var/cleave_radius = 1 + /// What type of wound we apply + var/wound_type = /datum/wound/slash/critical /datum/action/cooldown/spell/pointed/cleave/is_valid_target(atom/cast_on) return ..() && ishuman(cast_on) @@ -27,7 +30,7 @@ nearby += nearby_human for(var/mob/living/carbon/human/victim as anything in nearby) - if(victim == owner) + if(victim == owner || IS_HERETIC_OR_MONSTER(victim)) continue if(victim.can_block_magic()) victim.visible_message( @@ -45,17 +48,18 @@ ) var/obj/item/bodypart/bodypart = pick(victim.bodyparts) - var/datum/wound/slash/critical/crit_wound = new() + var/datum/wound/slash/crit_wound = new wound_type() crit_wound.apply_wound(bodypart) victim.apply_damage(20, BURN, wound_bonus = CANT_WOUND) - new /obj/effect/temp_visual/cleave(victim.drop_location()) + new /obj/effect/temp_visual/cleave(get_turf(victim)) return TRUE /datum/action/cooldown/spell/pointed/cleave/long name = "Lesser Cleave" - cooldown_time = 65 SECONDS + cooldown_time = 60 SECONDS + wound_type = /datum/wound/slash/severe /obj/effect/temp_visual/cleave icon = 'icons/effects/eldritch.dmi' diff --git a/code/modules/antagonists/heretic/magic/blood_siphon.dm b/code/modules/antagonists/heretic/magic/blood_siphon.dm index 406d3123d78..1609991a6eb 100644 --- a/code/modules/antagonists/heretic/magic/blood_siphon.dm +++ b/code/modules/antagonists/heretic/magic/blood_siphon.dm @@ -10,7 +10,7 @@ school = SCHOOL_FORBIDDEN cooldown_time = 15 SECONDS - invocation = "FL'MS O'ET'RN'ITY" + invocation = "FL'MS O'ET'RN'ITY." invocation_type = INVOCATION_WHISPER spell_requirements = NONE diff --git a/code/modules/antagonists/heretic/magic/fire_blast.dm b/code/modules/antagonists/heretic/magic/fire_blast.dm new file mode 100644 index 00000000000..e6c70203d87 --- /dev/null +++ b/code/modules/antagonists/heretic/magic/fire_blast.dm @@ -0,0 +1,178 @@ +/datum/action/cooldown/spell/charged/beam/fire_blast + name = "Volcano Blast" + desc = "Charge up a blast of fire that chains between nearby targets, setting them ablaze. \ + Targets already on fire will take priority. If the target fails to catch ablaze, or \ + extinguishes themselves before it bounces, the chain will stop." + background_icon_state = "bg_ecult" + icon_icon = 'icons/mob/actions/actions_ecult.dmi' + button_icon_state = "flames" + sound = 'sound/magic/fireball.ogg' + + school = SCHOOL_FORBIDDEN + cooldown_time = 45 SECONDS + + invocation = "V'LC'N!" + invocation_type = INVOCATION_SHOUT + spell_requirements = NONE + channel_time = 5 SECONDS + target_radius = 5 + max_beam_bounces = 4 + + /// How long the beam visual lasts, also used to determine time between jumps + var/beam_duration = 2 SECONDS + +/datum/action/cooldown/spell/charged/beam/fire_blast/cast(atom/cast_on) + if(isliving(cast_on)) + var/mob/living/caster = cast_on + // Caster becomes fireblasted, but in a good way - heals damage over time + caster.apply_status_effect(/datum/status_effect/fire_blasted, beam_duration, -2) + return ..() + +/datum/action/cooldown/spell/charged/beam/fire_blast/send_beam(atom/origin, mob/living/carbon/to_beam, bounces = 4) + // Send a beam from the origin to the hit mob + origin.Beam(to_beam, icon_state = "solar_beam", time = beam_duration, beam_type = /obj/effect/ebeam/fire) + + // If they block the magic, the chain wont necessarily stop, + // but likely will (due to them not catching on fire) + if(to_beam.can_block_magic(antimagic_flags)) + to_beam.visible_message( + span_warning("[to_beam] absorbs the spell, remaining unharmed!"), + span_userdanger("You absorb the spell, remaining unharmed!"), + ) + // Apply status effect but with no overlay + to_beam.apply_status_effect(/datum/status_effect/fire_blasted) + + // Otherwise, if unblocked apply the damage and set them up + else + to_beam.apply_damage(20, BURN, wound_bonus = 5) + to_beam.adjust_fire_stacks(3) + to_beam.ignite_mob() + // Apply the fire blast status effect to show they got blasted + to_beam.apply_status_effect(/datum/status_effect/fire_blasted, beam_duration * 0.5) + + // We can keep bouncing, try to continue the chain + if(bounces >= 1) + playsound(to_beam, sound, 50, vary = TRUE, extrarange = -1) + // Chain continues shortly after. If they extinguish themselves in this time, the chain will stop anyways. + addtimer(CALLBACK(src, PROC_REF(continue_beam), to_beam, bounces), beam_duration * 0.5) + + else + playsound(to_beam, sound, 50, vary = TRUE, frequency = 12000) + // We hit the maximum chain length, apply a bonus for managing it + new /obj/effect/temp_visual/fire_blast_bonus(to_beam.loc) + for(var/mob/living/nearby_living in range(1, to_beam)) + if(IS_HERETIC_OR_MONSTER(nearby_living) || nearby_living == owner) + continue + nearby_living.Knockdown(0.8 SECONDS) + nearby_living.apply_damage(15, BURN, wound_bonus = 5) + nearby_living.adjust_fire_stacks(2) + nearby_living.ignite_mob() + +/// Timer callback to continue the chain, calling send_fire_bream recursively. +/datum/action/cooldown/spell/charged/beam/fire_blast/proc/continue_beam(mob/living/carbon/beamed, bounces) + // We will only continue the chain if we exist, are still on fire, and still have the status effect + if(QDELETED(beamed) || !beamed.on_fire || !beamed.has_status_effect(/datum/status_effect/fire_blasted)) + return + // We fulfilled the conditions, get the next target + var/mob/living/carbon/to_beam_next = get_target(beamed) + if(isnull(to_beam_next)) // No target = no chain + return + + // Chain again! Recursively + send_beam(beamed, to_beam_next, bounces - 1) + +/// Pick a carbon mob in a radius around us that we can reach. +/// Mobs on fire will have priority and be targeted over others. +/// Returns null or a carbon mob. +/datum/action/cooldown/spell/charged/beam/fire_blast/get_target(atom/center) + var/list/possibles = list() + var/list/priority_possibles = list() + for(var/mob/living/carbon/to_check in view(target_radius, center)) + if(to_check == center || to_check == owner) + continue + if(to_check.has_status_effect(/datum/status_effect/fire_blasted)) // Already blasted + continue + if(IS_HERETIC_OR_MONSTER(to_check)) + continue + if(!length(get_path_to(center, to_check, max_distance = target_radius, simulated_only = FALSE))) + continue + + possibles += to_check + if(to_check.on_fire && to_check.stat != DEAD) + priority_possibles += to_check + + if(!length(possibles)) + return null + + return length(priority_possibles) ? pick(priority_possibles) : pick(possibles) + +/** + * Status effect applied when someone's hit by the fire blast. + * + * Applies an overlay, then causes a damage over time (or heal over time) + */ +/datum/status_effect/fire_blasted + id = "fire_blasted" + alert_type = null + duration = 5 SECONDS + tick_interval = 0.5 SECONDS + /// How much fire / stam to do per tick (stamina damage is doubled this) + var/tick_damage = 1 + /// How long does the animation of the appearance last? If 0 or negative, we make no overlay + var/animate_duration = 0.75 SECONDS + +/datum/status_effect/fire_blasted/on_creation(mob/living/new_owner, animate_duration = -1, tick_damage = 1) + src.animate_duration = animate_duration + src.tick_damage = tick_damage + return ..() + +/datum/status_effect/fire_blasted/on_apply() + if(owner.on_fire && animate_duration > 0 SECONDS) + var/image/warning_sign = image(icon = 'icons/effects/effects.dmi', icon_state = "blessed", layer = BELOW_MOB_LAYER, loc = owner) + owner.flick_overlay_view(warning_sign, initial(duration)) + warning_sign.alpha = 50 + animate(warning_sign, alpha = 255, time = animate_duration) + + return TRUE + +/datum/status_effect/fire_blasted/tick(delta_time, times_fired) + owner.adjustFireLoss(tick_damage) + owner.adjustStaminaLoss(2 * tick_damage) + +// The beam fireblast spits out, causes people to walk through it to be on fire +/obj/effect/ebeam/fire + name = "fire beam" + +/obj/effect/ebeam/fire/Initialize(mapload) + . = ..() + var/static/list/loc_connections = list( + COMSIG_ATOM_ENTERED = PROC_REF(on_entered), + ) + AddElement(/datum/element/connect_loc, loc_connections) + + if(!isturf(loc) || mapload) // idk if this would ever be maploaded but you never know + return + + for(var/mob/living/living_mob in loc) + on_entered(entered = living_mob) + +/obj/effect/ebeam/fire/proc/on_entered(datum/source, atom/movable/entered) + SIGNAL_HANDLER + + if(!isliving(entered)) + return + var/mob/living/living_entered = entered + if(IS_HERETIC_OR_MONSTER(living_entered) || living_entered.has_status_effect(/datum/status_effect/fire_blasted)) + return + living_entered.apply_damage(10, BURN, wound_bonus = 5) + living_entered.adjust_fire_stacks(2) + living_entered.ignite_mob() + // Apply the fireblasted effect - no overlay + living_entered.apply_status_effect(/datum/status_effect/fire_blasted) + +// Visual effect played when we hit the max bounces +/obj/effect/temp_visual/fire_blast_bonus + name = "fire blast" + icon = 'icons/effects/effects.dmi' + icon_state = "explosion" + duration = 1 SECONDS diff --git a/code/modules/antagonists/heretic/magic/flesh_surgery.dm b/code/modules/antagonists/heretic/magic/flesh_surgery.dm new file mode 100644 index 00000000000..44f97365bc1 --- /dev/null +++ b/code/modules/antagonists/heretic/magic/flesh_surgery.dm @@ -0,0 +1,232 @@ +/datum/action/cooldown/spell/touch/flesh_surgery + name = "Knit Flesh" + desc = "A touch spell that allows you to either harvest or restore flesh of target. \ + Left-clicking will extract the organs of a victim without needing to complete surgery or disembowel. \ + Right-clicking, if done on summons or minions, will restore health. Can also be used to heal damaged organs." + background_icon_state = "bg_ecult" + icon_icon = 'icons/mob/actions/actions_ecult.dmi' + button_icon_state = "mad_touch" + sound = null + + school = SCHOOL_FORBIDDEN + cooldown_time = 20 SECONDS + invocation = "CL'M M'N!" // "CLAIM MINE", but also almost "KALI MA" + invocation_type = INVOCATION_SHOUT + spell_requirements = NONE + + hand_path = /obj/item/melee/touch_attack/flesh_surgery + can_cast_on_self = TRUE + + /// If used on an organ, how much percent of the organ's HP do we restore + var/organ_percent_healing = 0.5 + /// If used on a heretic mob, how much brute do we heal + var/monster_brute_healing = 10 + /// If used on a heretic mob, how much burn do we heal + var/monster_burn_healing = 5 + +/datum/action/cooldown/spell/touch/flesh_surgery/is_valid_target(atom/cast_on) + return isliving(cast_on) || isorgan(cast_on) + +/datum/action/cooldown/spell/touch/flesh_surgery/cast_on_hand_hit(obj/item/melee/touch_attack/hand, atom/victim, mob/living/carbon/caster) + if(isorgan(victim)) + return heal_organ(hand, victim, caster) + + if(isliving(victim)) + return steal_organ_from_mob(hand, victim, caster) + + return FALSE + +/datum/action/cooldown/spell/touch/flesh_surgery/cast_on_secondary_hand_hit(obj/item/melee/touch_attack/hand, atom/victim, mob/living/carbon/caster) + if(isorgan(victim)) + return SECONDARY_ATTACK_CALL_NORMAL + + if(isliving(victim)) + var/mob/living/mob_victim = victim + if(mob_victim.stat == DEAD || !IS_HERETIC_MONSTER(mob_victim)) + return SECONDARY_ATTACK_CALL_NORMAL + + if(heal_heretic_monster(hand, mob_victim, caster)) + return SECONDARY_ATTACK_CONTINUE_CHAIN + + return SECONDARY_ATTACK_CANCEL_ATTACK_CHAIN + +/datum/action/cooldown/spell/touch/flesh_surgery/register_hand_signals() + . = ..() + RegisterSignal(attached_hand, COMSIG_ITEM_REQUESTING_CONTEXT_FOR_TARGET, PROC_REF(add_item_context)) + attached_hand.item_flags |= ITEM_HAS_CONTEXTUAL_SCREENTIPS + +/datum/action/cooldown/spell/touch/flesh_surgery/unregister_hand_signals() + . = ..() + UnregisterSignal(attached_hand, COMSIG_ITEM_REQUESTING_CONTEXT_FOR_TARGET) + +/// Signal proc for [COMSIG_ITEM_REQUESTING_CONTEXT_FOR_TARGET] to add some context to the hand. +/datum/action/cooldown/spell/touch/flesh_surgery/proc/add_item_context(obj/item/melee/touch_attack/source, list/context, atom/victim, mob/living/user) + SIGNAL_HANDLER + + . = NONE + + if(isliving(victim)) + var/mob/living/mob_victim = victim + + if(iscarbon(mob_victim)) + context[SCREENTIP_CONTEXT_LMB] = "Extract organ" + . = CONTEXTUAL_SCREENTIP_SET + + if(IS_HERETIC_MONSTER(mob_victim)) + context[SCREENTIP_CONTEXT_RMB] = "Heal [ishuman(mob_victim) ? "minion" : "summon"]" + . = CONTEXTUAL_SCREENTIP_SET + + else if(isorgan(victim)) + context[SCREENTIP_CONTEXT_LMB] = "Heal organ" + . = CONTEXTUAL_SCREENTIP_SET + + return . + +/// If cast on an organ, we'll restore it's health and even un-fail it. +/datum/action/cooldown/spell/touch/flesh_surgery/proc/heal_organ(obj/item/melee/touch_attack/hand, obj/item/organ/to_heal, mob/living/carbon/caster) + to_heal.balloon_alert(caster, "healing organ...") + if(!do_after(caster, 1 SECONDS, to_heal, extra_checks = CALLBACK(src, PROC_REF(heal_checks), hand, to_heal, caster))) + to_heal.balloon_alert(caster, "interrupted!") + return FALSE + + var/organ_hp_to_heal = to_heal.maxHealth * organ_percent_healing + if(to_heal.damage < organ_hp_to_heal) + to_heal.setOrganDamage(organ_hp_to_heal) + to_heal.balloon_alert(caster, "organ healed") + playsound(to_heal, 'sound/magic/staff_healing.ogg', 30) + new /obj/effect/temp_visual/cult/sparks(get_turf(to_heal)) + caster.visible_message( + span_warning("[caster]'s hand glows a brilliant red as [caster.p_they()] restore \the [to_heal] to good condition!"), + span_notice("Your hand glows a brilliant red as you restore \the [to_heal] to good condition!"), + ) + else + to_heal.balloon_alert(caster, "already in good condition!") + + return TRUE + +/// If cast on a heretic monster who's not dead we'll heal it a bit. +/datum/action/cooldown/spell/touch/flesh_surgery/proc/heal_heretic_monster(obj/item/melee/touch_attack/hand, mob/living/to_heal, mob/living/carbon/caster) + var/what_are_we = ishuman(to_heal) ? "minion" : "summon" + to_heal.balloon_alert(caster, "healing [what_are_we]...") + if(!do_after(caster, 1 SECONDS, to_heal, extra_checks = CALLBACK(src, PROC_REF(heal_checks), hand, to_heal, caster))) + to_heal.balloon_alert(caster, "interrupted!") + return FALSE + + // Keep in mind that, for simplemobs(summons), this will just flat heal the combined value of both brute and burn healing, + // while for human minions(ghouls), this will heal brute and burn like normal. So be careful adjusting to bigger numbers + to_heal.balloon_alert(caster, "[what_are_we] healed") + to_heal.heal_overall_damage(monster_brute_healing, monster_burn_healing) + playsound(to_heal, 'sound/magic/staff_healing.ogg', 30) + new /obj/effect/temp_visual/cult/sparks(get_turf(to_heal)) + caster.visible_message( + span_warning("[caster]'s hand glows a brilliant red as [caster.p_they()] restore [to_heal] to good condition!"), + span_notice("Your hand glows a brilliant red as you restore [to_heal] to good condition!"), + ) + return TRUE + +/// If cast on a carbon, we'll try to steal one of their organs directly from their person. +/datum/action/cooldown/spell/touch/flesh_surgery/proc/steal_organ_from_mob(obj/item/melee/touch_attack/hand, mob/living/victim, mob/living/carbon/caster) + var/mob/living/carbon/carbon_victim = victim + if(!istype(carbon_victim) || !length(carbon_victim.internal_organs)) + victim.balloon_alert(caster, "no organs!") + return FALSE + + // Round u pto the nearest generic zone (body, chest, arm) + var/zone_to_check = check_zone(caster.zone_selected) + var/parsed_zone = parse_zone(zone_to_check) + + var/list/organs_we_can_remove = list() + for(var/obj/item/organ/organ as anything in carbon_victim.internal_organs) + // Only show organs which are in our generic zone + if(deprecise_zone(organ.zone) != zone_to_check) + continue + // Also, some organs to exclude. Don't remove vital (brains), don't remove synthetics, and don't remove unremovable + if(organ.organ_flags & (ORGAN_SYNTHETIC|ORGAN_VITAL|ORGAN_UNREMOVABLE)) + continue + + organs_we_can_remove[organ.name] = organ + + if(!length(organs_we_can_remove)) + victim.balloon_alert(caster, "no organs there!") + return FALSE + + var/chosen_organ = tgui_input_list(caster, "Which organ do you want to extract?", name, sort_list(organs_we_can_remove)) + if(isnull(chosen_organ)) + return FALSE + var/obj/item/organ/picked_organ = organs_we_can_remove[chosen_organ] + if(!istype(picked_organ) || !extraction_checks(picked_organ, hand, victim, caster)) + return FALSE + + // Don't let people stam crit into steal heart true combo + var/time_it_takes = carbon_victim.stat == DEAD ? 3 SECONDS : 15 SECONDS + + // Sure you can remove your own organs, fun party trick + if(carbon_victim == caster) + var/are_you_sure = tgui_alert(caster, "Are you sure you want to remove your own [chosen_organ]?", "Are you sure?", list("Yes", "No")) + if(are_you_sure != "Yes" || !extraction_checks(picked_organ, hand, victim, caster)) + return FALSE + + time_it_takes = 6 SECONDS + caster.visible_message( + span_danger("[caster]'s hand glows a brilliant red as [caster.p_they()] reach directly into [caster.p_their()] own [parsed_zone]!"), + span_userdanger("Your hand glows a brilliant red as you reach directly into your own [parsed_zone]!"), + ) + + else + carbon_victim.visible_message( + span_danger("[caster]'s hand glows a brilliant red as [caster.p_they()] reach directly into [carbon_victim]'s [parsed_zone]!"), + span_userdanger("[caster]'s hand glows a brilliant red as [caster.p_they()] reach directly into your [parsed_zone]!"), + ) + + carbon_victim.balloon_alert(caster, "extracting [chosen_organ]...") + playsound(victim, 'sound/weapons/slice.ogg', 50, TRUE) + carbon_victim.add_atom_colour(COLOR_DARK_RED, TEMPORARY_COLOUR_PRIORITY) + if(!do_after(caster, time_it_takes, carbon_victim, extra_checks = CALLBACK(src, PROC_REF(extraction_checks), picked_organ, hand, victim, caster))) + carbon_victim.balloon_alert(caster, "interrupted!") + return FALSE + + // Visible message done before Remove() + // Mainly so it gets across if you're taking the eyes of someone who's conscious + if(carbon_victim == caster) + caster.visible_message( + span_bolddanger("[caster] pulls [caster.p_their()] own [chosen_organ] out of [caster.p_their()] [parsed_zone]!!"), + span_userdanger("You pull your own [chosen_organ] out of your [parsed_zone]!!"), + ) + + else + carbon_victim.visible_message( + span_bolddanger("[caster]'s pulls [carbon_victim]'s [chosen_organ] out of [carbon_victim.p_their()] [parsed_zone]!!"), + span_userdanger("[caster]'s pulls your [chosen_organ] out of [carbon_victim.p_their()] [parsed_zone]!!"), + ) + + picked_organ.Remove(carbon_victim) + carbon_victim.balloon_alert(caster, "[chosen_organ] removed") + carbon_victim.remove_atom_colour(TEMPORARY_COLOUR_PRIORITY, COLOR_DARK_RED) + playsound(victim, 'sound/effects/dismember.ogg', 50, TRUE) + if(carbon_victim.stat == CONSCIOUS) + carbon_victim.adjust_timed_status_effect(15 SECONDS, /datum/status_effect/speech/slurring/heretic) + carbon_victim.emote("scream") + + // We need to wait for the spell to actually finish casting to put the organ in their hands, hence, 1 ms timer. + addtimer(CALLBACK(caster, TYPE_PROC_REF(/mob, put_in_hands), picked_organ), 1) + return TRUE + +/// Extra checks ran while we're extracting an organ to make sure we can continue to do. +/datum/action/cooldown/spell/touch/flesh_surgery/proc/extraction_checks(obj/item/organ/picked_organ, obj/item/melee/touch_attack/hand, mob/living/carbon/victim, mob/living/carbon/caster) + if(QDELETED(src) || QDELETED(hand) || QDELETED(picked_organ) || QDELETED(victim) || !IsAvailable()) + return FALSE + + return TRUE + +/// Extra checks ran while we're healing something (organ, mob). +/datum/action/cooldown/spell/touch/flesh_surgery/proc/heal_checks(obj/item/melee/touch_attack/hand, atom/healing, mob/living/carbon/caster) + if(QDELETED(src) || QDELETED(hand) ||QDELETED(healing) || !IsAvailable()) + return FALSE + + return TRUE + +/obj/item/melee/touch_attack/flesh_surgery + name = "\improper knit flesh" + desc = "Let's go practice medicine." + icon_state = "disintegrate" + inhand_icon_state = "disintegrate" diff --git a/code/modules/antagonists/heretic/magic/madness_touch.dm b/code/modules/antagonists/heretic/magic/madness_touch.dm index a3e20477e9b..632643becd7 100644 --- a/code/modules/antagonists/heretic/magic/madness_touch.dm +++ b/code/modules/antagonists/heretic/magic/madness_touch.dm @@ -12,23 +12,21 @@ spell_requirements = NONE antimagic_flags = MAGIC_RESISTANCE|MAGIC_RESISTANCE_MIND -/datum/action/cooldown/spell/touch/mad_touch/cast_on_hand_hit(obj/item/melee/touch_attack/hand, atom/victim, mob/living/carbon/caster) - if(!ishuman(victim)) +/datum/action/cooldown/spell/touch/mad_touch/is_valid_target(atom/cast_on) + if(!ishuman(cast_on)) return FALSE - - var/mob/living/carbon/human/human_victim = victim - if(!human_victim.mind || IS_HERETIC(human_victim)) + var/mob/living/carbon/human/human_cast_on = cast_on + if(!human_cast_on.mind || !human_cast_on.mob_mood || IS_HERETIC_OR_MONSTER(human_cast_on)) return FALSE - - if(human_victim.can_block_magic(antimagic_flags)) - victim.visible_message( - span_danger("The spell bounces off of [victim]!"), - span_danger("The spell bounces off of you!"), - ) - return FALSE - - to_chat(caster, span_warning("[human_victim.name] has been cursed!")) - if (isliving(target)) - var/mob/living/target_mob = target - target_mob.add_mood_event("gates_of_mansus", /datum/mood_event/gates_of_mansus) + return TRUE + +/datum/action/cooldown/spell/touch/mad_touch/on_antimagic_triggered(obj/item/melee/touch_attack/hand, atom/victim, mob/living/carbon/caster) + victim.visible_message( + span_danger("The spell bounces off of [victim]!"), + span_danger("The spell bounces off of you!"), + ) + +/datum/action/cooldown/spell/touch/mad_touch/cast_on_hand_hit(obj/item/melee/touch_attack/hand, mob/living/carbon/human/victim, mob/living/carbon/caster) + to_chat(caster, span_warning("[victim.name] has been cursed!")) + victim.add_mood_event("gates_of_mansus", /datum/mood_event/gates_of_mansus) return TRUE diff --git a/code/modules/antagonists/heretic/magic/manse_link.dm b/code/modules/antagonists/heretic/magic/manse_link.dm index d3e62bc9b1d..b8775d334dd 100644 --- a/code/modules/antagonists/heretic/magic/manse_link.dm +++ b/code/modules/antagonists/heretic/magic/manse_link.dm @@ -13,6 +13,7 @@ invocation = "PI'RC' TH' M'ND." invocation_type = INVOCATION_SHOUT spell_requirements = NONE + antimagic_flags = MAGIC_RESISTANCE|MAGIC_RESISTANCE_MIND cast_range = 7 diff --git a/code/modules/antagonists/heretic/magic/mansus_grasp.dm b/code/modules/antagonists/heretic/magic/mansus_grasp.dm index 46e3dfa512d..37743b6945c 100644 --- a/code/modules/antagonists/heretic/magic/mansus_grasp.dm +++ b/code/modules/antagonists/heretic/magic/mansus_grasp.dm @@ -16,24 +16,26 @@ hand_path = /obj/item/melee/touch_attack/mansus_fist +/datum/action/cooldown/spell/touch/mansus_grasp/is_valid_target(atom/cast_on) + return TRUE // This baby can hit anything + /datum/action/cooldown/spell/touch/mansus_grasp/can_cast_spell(feedback = TRUE) return ..() && !!IS_HERETIC(owner) +/datum/action/cooldown/spell/touch/mansus_grasp/on_antimagic_triggered(obj/item/melee/touch_attack/hand, atom/victim, mob/living/carbon/caster) + victim.visible_message( + span_danger("The spell bounces off of [victim]!"), + span_danger("The spell bounces off of you!"), + ) + /datum/action/cooldown/spell/touch/mansus_grasp/cast_on_hand_hit(obj/item/melee/touch_attack/hand, atom/victim, mob/living/carbon/caster) if(!isliving(victim)) return FALSE - var/mob/living/living_hit = victim - if(living_hit.can_block_magic(antimagic_flags)) - victim.visible_message( - span_danger("The spell bounces off of [victim]!"), - span_danger("The spell bounces off of you!"), - ) - return FALSE - if(SEND_SIGNAL(caster, COMSIG_HERETIC_MANSUS_GRASP_ATTACK, victim) & COMPONENT_BLOCK_HAND_USE) return FALSE + var/mob/living/living_hit = victim living_hit.apply_damage(10, BRUTE, wound_bonus = CANT_WOUND) if(iscarbon(victim)) var/mob/living/carbon/carbon_hit = victim @@ -84,9 +86,12 @@ /obj/item/melee/touch_attack/mansus_fist/suicide_act(mob/living/user) user.visible_message(span_suicide("[user] covers [user.p_their()] face with [user.p_their()] sickly-looking hand! It looks like [user.p_theyre()] trying to commit suicide!")) var/mob/living/carbon/carbon_user = user //iscarbon already used in spell's parent - var/datum/action/cooldown/spell/touch/mansus_grasp/source = locate() in user.actions - if(!IS_HERETIC(user)) - return + var/datum/action/cooldown/spell/touch/mansus_grasp/source = spell_which_made_us?.resolve() + if(QDELETED(source) || !IS_HERETIC(user)) + return SHAME + + if(user.can_block_magic(source.antimagic_flags)) + return SHAME var/escape_our_torment = 0 while(carbon_user.stat == CONSCIOUS) @@ -102,7 +107,7 @@ carbon_user.emote("scream") carbon_user.adjust_stutter(26 SECONDS) - source?.cast_on_hand_hit(src, user, user) + source.cast_on_hand_hit(src, user, user) escape_our_torment++ stoplag(0.4 SECONDS) diff --git a/code/modules/antagonists/heretic/magic/nightwatcher_rebirth.dm b/code/modules/antagonists/heretic/magic/nightwatcher_rebirth.dm index 589862ead7d..9d0a93f6bc5 100644 --- a/code/modules/antagonists/heretic/magic/nightwatcher_rebirth.dm +++ b/code/modules/antagonists/heretic/magic/nightwatcher_rebirth.dm @@ -35,11 +35,12 @@ return things /datum/action/cooldown/spell/aoe/fiery_rebirth/cast_on_thing_in_aoe(mob/living/carbon/victim, mob/living/carbon/human/caster) - new /obj/effect/temp_visual/eldritch_smoke(victim.drop_location()) + new /obj/effect/temp_visual/eldritch_smoke(get_turf(victim)) + victim.Beam(caster, icon_state = "r_beam", time = 2 SECONDS) //This is essentially a death mark, use this to finish your opponent quicker. - if(HAS_TRAIT(victim, TRAIT_CRITICAL_CONDITION) && !HAS_TRAIT(victim, TRAIT_NODEATH)) - victim.investigate_log("has been killed by fiery rebirth.", INVESTIGATE_DEATHS) + if(CAN_SUCCUMB(victim)) + victim.investigate_log("has been executed by fiery rebirth.", INVESTIGATE_DEATHS) victim.death() victim.apply_damage(20, BURN) diff --git a/code/modules/antagonists/heretic/magic/realignment.dm b/code/modules/antagonists/heretic/magic/realignment.dm new file mode 100644 index 00000000000..7ba184ed4d8 --- /dev/null +++ b/code/modules/antagonists/heretic/magic/realignment.dm @@ -0,0 +1,80 @@ +// Realignment. It's like Fleshmend but solely for stamina damage and stuns. Sec meta +/datum/action/cooldown/spell/realignment + name = "Realignment" + desc = "Realign yourself, rapidly regenerating stamina and reducing any stuns or knockdowns. \ + You cannot attack while realigning. Can be casted multiple times in short succession, but each cast lengthens the cooldown." + background_icon_state = "bg_ecult" + icon_icon = 'icons/obj/implants.dmi' + button_icon_state = "adrenal" + // sound = 'sound/magic/whistlereset.ogg' + + school = SCHOOL_FORBIDDEN + cooldown_time = 6 SECONDS + cooldown_reduction_per_rank = -6 SECONDS // we're not a wizard spell but we use the levelling mechanic + spell_max_level = 10 // we can get up to / over a minute duration cd time + + invocation = "R'S'T." + invocation_type = INVOCATION_SHOUT + spell_requirements = NONE + +/datum/action/cooldown/spell/realignment/is_valid_target(atom/cast_on) + return isliving(cast_on) + +/datum/action/cooldown/spell/realignment/cast(mob/living/cast_on) + . = ..() + cast_on.apply_status_effect(/datum/status_effect/realignment) + to_chat(cast_on, span_notice("We begin to realign ourselves.")) + +/datum/action/cooldown/spell/realignment/after_cast(atom/cast_on) + . = ..() + // With every cast, our spell level increases for a short time, which goes back down after a period + // and with every spell level, the cooldown duration of the spell goes up + if(level_spell()) + var/reduction_timer = max(cooldown_time * spell_max_level * 0.5, 1.5 MINUTES) + addtimer(CALLBACK(src, PROC_REF(delevel_spell)), reduction_timer) + +/datum/action/cooldown/spell/realignment/update_spell_name() + var/spell_title = "" + switch(spell_level) + if(1, 2) + spell_title = "Hasty " // Hasty Realignment + if(3, 4) + spell_title = "" // Realignment + if(5, 6, 7) + spell_title = "Slowed " // Slowed Realignment + if(8, 9, 10) + spell_title = "Laborious " // Laborious Realignment (don't reach here) + + name = "[spell_title][initial(name)]" + UpdateButtons() + +/datum/status_effect/realignment + id = "realigment" + status_type = STATUS_EFFECT_REFRESH + duration = 8 SECONDS + alert_type = /atom/movable/screen/alert/status_effect/realignment + tick_interval = 0.2 SECONDS + +/datum/status_effect/realignment/get_examine_text() + return span_notice("[owner.p_theyre(TRUE)] glowing a soft white.") + +/datum/status_effect/realignment/on_apply() + ADD_TRAIT(owner, TRAIT_PACIFISM, id) + owner.add_filter(id, 2, list("type" = "outline", "color" = "#d6e3e7", "size" = 2)) + var/filter = owner.get_filter(id) + animate(filter, alpha = 127, time = 1 SECONDS, loop = -1) + animate(alpha = 63, time = 2 SECONDS) + return TRUE + +/datum/status_effect/realignment/on_remove() + REMOVE_TRAIT(owner, TRAIT_PACIFISM, id) + owner.remove_filter(id) + +/datum/status_effect/realignment/tick(delta_time, times_fired) + owner.adjustStaminaLoss(-5) + owner.AdjustAllImmobility(-0.5 SECONDS) + +/atom/movable/screen/alert/status_effect/realignment + name = "Realignment" + desc = "You're realignment yourself. You cannot attack, but are rapidly regenerating stamina." + icon_state = "realignment" diff --git a/code/modules/antagonists/heretic/magic/rust_construction.dm b/code/modules/antagonists/heretic/magic/rust_construction.dm new file mode 100644 index 00000000000..b3daea21ba5 --- /dev/null +++ b/code/modules/antagonists/heretic/magic/rust_construction.dm @@ -0,0 +1,117 @@ +/datum/action/cooldown/spell/pointed/rust_construction + name = "Rust Formation" + desc = "Transforms a rusted floor into a full wall of rust. Creating a wall underneath a mob will harm it." + background_icon_state = "bg_ecult" + icon_icon = 'icons/mob/actions/actions_ecult.dmi' + button_icon_state = "cleave" + ranged_mousepointer = 'icons/effects/mouse_pointers/throw_target.dmi' + check_flags = AB_CHECK_CONSCIOUS|AB_CHECK_HANDS_BLOCKED + + school = SCHOOL_FORBIDDEN + cooldown_time = 5 SECONDS + + invocation = "Someone raises a wall of rust." + invocation_self_message = "You raise a wall of rust." + invocation_type = INVOCATION_EMOTE + spell_requirements = NONE + + cast_range = 4 + aim_assist = FALSE + + /// How long does the filter last on walls we make? + var/filter_duration = 2 MINUTES + +/datum/action/cooldown/spell/pointed/rust_construction/is_valid_target(atom/cast_on) + if(!isfloorturf(cast_on)) + if(isturf(cast_on) && owner) + cast_on.balloon_alert(owner, "not a floor!") + return FALSE + + if(!HAS_TRAIT(cast_on, TRAIT_RUSTY)) + if(owner) + cast_on.balloon_alert(owner, "not rusted!") + return FALSE + + return TRUE + +/datum/action/cooldown/spell/pointed/rust_construction/before_cast(turf/open/cast_on) + . = ..() + if(!isliving(owner)) + return + + var/mob/living/living_owner = owner + invocation = span_danger("[owner] drags [owner.p_their()] hand[living_owner.usable_hands == 1 ? "":"s"] upwards as a wall of rust rises out of [cast_on]!") + invocation_self_message = span_notice("You drag [living_owner.usable_hands == 1 ? "a hand":"your hands"] upwards as a wall of rust rises out of [cast_on].") + +/datum/action/cooldown/spell/pointed/rust_construction/cast(turf/open/cast_on) + . = ..() + var/rises_message = "rises out of [cast_on]" + var/turf/closed/wall/new_wall = cast_on.PlaceOnTop(/turf/closed/wall) + if(!istype(new_wall)) + return + + playsound(new_wall, 'sound/effects/constructform.ogg', 50, TRUE) + new_wall.rust_heretic_act() + new_wall.name = "\improper enchanted [new_wall.name]" + new_wall.hardness = 10 + new_wall.sheet_amount = 0 + new_wall.girder_type = null + + // I wanted to do a cool animation of a wall raising from the ground + // but I guess a fading filter will have to do for now as walls have 0 depth (currently) + // damn though with 3/4ths walls this'll look sick just imagine it + new_wall.add_filter("rust_wall", 2, list("type" = "outline", "color" = "#85be299c", "size" = 2)) + addtimer(CALLBACK(src, PROC_REF(fade_wall_filter), new_wall), filter_duration * (1/20)) + addtimer(CALLBACK(src,PROC_REF(remove_wall_filter), new_wall), filter_duration) + + var/message_shown = FALSE + for(var/mob/living/living_mob in cast_on) + message_shown = TRUE + if(IS_HERETIC_OR_MONSTER(living_mob) || living_mob == owner) + living_mob.visible_message( + span_warning("\A [new_wall] [rises_message] and pushes along [living_mob]!"), + span_notice("\A [new_wall] [rises_message] beneath your feet and pushes you along!"), + ) + else + living_mob.visible_message( + span_warning("\A [new_wall] [rises_message] and slams into [living_mob]!"), + span_userdanger("\A [new_wall] [rises_message] beneath your feet and slams into you!"), + ) + living_mob.apply_damage(10, BRUTE, wound_bonus = 10) + living_mob.Knockdown(5 SECONDS) + living_mob.SpinAnimation(5, 1) + + // If we're a multiz map send them to the next floor + var/turf/above_us = get_step_multiz(cast_on, UP) + if(above_us) + living_mob.forceMove(above_us) + continue + + // If we're not throw them to a nearby (open) turf + var/list/turfs_by_us = get_adjacent_open_turfs(cast_on) + // If there is no side by us, hardstun them + if(!length(turfs_by_us)) + living_mob.Paralyze(5 SECONDS) + continue + + // If there's an open turf throw them to the side + living_mob.throw_at(pick(turfs_by_us), 1, 3, thrower = owner, spin = FALSE) + + if(!message_shown) + new_wall.visible_message(span_warning("\A [new_wall] [rises_message]!")) + +/datum/action/cooldown/spell/pointed/rust_construction/proc/fade_wall_filter(turf/closed/wall) + if(QDELETED(wall)) + return + + var/rust_filter = wall.get_filter("rust_wall") + if(!rust_filter) + return + + animate(rust_filter, alpha = 0, time = filter_duration * (19/20)) + +/datum/action/cooldown/spell/pointed/rust_construction/proc/remove_wall_filter(turf/closed/wall) + if(QDELETED(wall)) + return + + wall.remove_filter("rust_wall") diff --git a/code/modules/antagonists/heretic/magic/rust_wave.dm b/code/modules/antagonists/heretic/magic/rust_wave.dm index e7f4d267d7e..baf7fce7d89 100644 --- a/code/modules/antagonists/heretic/magic/rust_wave.dm +++ b/code/modules/antagonists/heretic/magic/rust_wave.dm @@ -1,7 +1,9 @@ // Shoots out in a wave-like, what rust heretics themselves get /datum/action/cooldown/spell/cone/staggered/entropic_plume name = "Entropic Plume" - desc = "Spews forth a disorienting plume that causes enemies to strike each other, briefly blinds them(increasing with range) and poisons them(decreasing with range). Also spreads rust in the path of the plume." + desc = "Spews forth a disorienting plume that causes enemies to strike each other, \ + briefly blinds them (increasing with range) and poisons them (decreasing with range). \ + Also spreads rust in the path of the plume." background_icon_state = "bg_ecult" icon_icon = 'icons/mob/actions/actions_ecult.dmi' button_icon_state = "entropic_plume" @@ -25,19 +27,21 @@ target_turf.rust_heretic_act() /datum/action/cooldown/spell/cone/staggered/entropic_plume/do_mob_cone_effect(mob/living/victim, atom/caster, level) - if(victim.can_block_magic(antimagic_flags) || IS_HERETIC_OR_MONSTER(victim)) + if(victim.can_block_magic(antimagic_flags) || IS_HERETIC_OR_MONSTER(victim) || victim == caster) return victim.apply_status_effect(/datum/status_effect/amok) victim.apply_status_effect(/datum/status_effect/cloudstruck, level * 1 SECONDS) victim.reagents?.add_reagent(/datum/reagent/eldritch, max(1, 6 - level)) /datum/action/cooldown/spell/cone/staggered/entropic_plume/calculate_cone_shape(current_level) - if(current_level == cone_levels) - return 5 - else if(current_level == cone_levels - 1) + // At the first level (that isn't level 1) we will be small + if(current_level == 2) return 3 - else - return 2 + // At the max level, we turn small again + if(current_level == cone_levels) + return 3 + // Otherwise, all levels in between will be wider + return 5 /obj/effect/temp_visual/dir_setting/entropic icon = 'icons/effects/160x160.dmi' diff --git a/code/modules/antagonists/heretic/magic/void_cold_cone.dm b/code/modules/antagonists/heretic/magic/void_cold_cone.dm new file mode 100644 index 00000000000..05e62eb34cf --- /dev/null +++ b/code/modules/antagonists/heretic/magic/void_cold_cone.dm @@ -0,0 +1,36 @@ +/datum/action/cooldown/spell/cone/staggered/cone_of_cold/void + name = "Void Blast" + desc = "Fires a cone of chilling void in front of you, freezing everything in its path. \ + Enemies in the cone of the blast will be damaged slightly, slowed, and chilled overtime. \ + Additionally, objects hit will be frozen and can shatter, and ground hit will be iced over and slippery - \ + though they may thaw shortly if used in room temperature." + background_icon_state = "bg_ecult" + icon_icon = 'icons/mob/actions/actions_ecult.dmi' + button_icon_state = "entropic_plume" + + school = SCHOOL_FORBIDDEN + cooldown_time = 1 MINUTES + + invocation = "FR'ZE!" + invocation_type = INVOCATION_SHOUT + spell_requirements = NONE + + // In room temperature, the ice won't last very long + // ...but in space / freezing rooms, it will stick around + turf_freeze_type = TURF_WET_ICE + unfreeze_turf_duration = 1 MINUTES + // Applies an "infinite" version of basic void chill + // (This stacks with mansus grasp's void chill) + frozen_status_effect_path = /datum/status_effect/void_chill/lasting + unfreeze_mob_duration = 30 SECONDS + // Does a smidge of damage + on_freeze_brute_damage = 12 + on_freeze_burn_damage = 10 + // Also freezes stuff (Which will likely be unfrozen similarly to turfs) + unfreeze_object_duration = 30 SECONDS + +/datum/action/cooldown/spell/cone/staggered/cone_of_cold/void/do_mob_cone_effect(mob/living/target_mob, atom/caster, level) + if(IS_HERETIC_OR_MONSTER(target_mob)) + return + + return ..() diff --git a/code/modules/antagonists/heretic/magic/void_phase.dm b/code/modules/antagonists/heretic/magic/void_phase.dm index 10588e35f37..166a3b5ad19 100644 --- a/code/modules/antagonists/heretic/magic/void_phase.dm +++ b/code/modules/antagonists/heretic/magic/void_phase.dm @@ -11,7 +11,7 @@ school = SCHOOL_FORBIDDEN cooldown_time = 30 SECONDS - invocation = "RE'L'TY PH'S'E" + invocation = "RE'L'TY PH'S'E." invocation_type = INVOCATION_WHISPER spell_requirements = NONE @@ -21,13 +21,14 @@ /// The radius of damage around the void bubble var/damage_radius = 1 -/datum/action/cooldown/spell/pointed/void_phase/is_valid_target(atom/cast_on) - // We do the close range check first - if(get_dist(get_turf(owner), get_turf(cast_on)) < min_cast_range) - owner.balloon_alert(owner, "too close!") - return FALSE +/datum/action/cooldown/spell/pointed/void_phase/before_cast(atom/cast_on) + . = ..() + if(. & SPELL_CANCEL_CAST) + return - return ..() + if(owner && get_dist(get_turf(owner), get_turf(cast_on)) < min_cast_range) + cast_on.balloon_alert(owner, "too close!") + return . | SPELL_CANCEL_CAST /datum/action/cooldown/spell/pointed/void_phase/cast(atom/cast_on) . = ..() diff --git a/code/modules/antagonists/heretic/magic/void_pull.dm b/code/modules/antagonists/heretic/magic/void_pull.dm index f8063f8635f..36be3e12792 100644 --- a/code/modules/antagonists/heretic/magic/void_pull.dm +++ b/code/modules/antagonists/heretic/magic/void_pull.dm @@ -10,7 +10,7 @@ school = SCHOOL_FORBIDDEN cooldown_time = 40 SECONDS - invocation = "BR'NG F'RTH TH'M T' M'" + invocation = "BR'NG F'RTH TH'M T' M'." invocation_type = INVOCATION_WHISPER spell_requirements = NONE @@ -29,8 +29,7 @@ new /obj/effect/temp_visual/voidin(get_turf(cast_on)) // Before we cast the actual effects, deal AOE damage to anyone adjacent to us - var/list/mob/living/people_near_us = get_things_to_cast_on(cast_on, damage_radius) - for(var/mob/living/nearby_living as anything in people_near_us) + for(var/mob/living/nearby_living as anything in get_things_to_cast_on(cast_on, damage_radius)) nearby_living.apply_damage(30, BRUTE, wound_bonus = CANT_WOUND) /datum/action/cooldown/spell/aoe/void_pull/get_things_to_cast_on(atom/center, radius_override = 0) diff --git a/code/modules/antagonists/heretic/status_effects/debuffs.dm b/code/modules/antagonists/heretic/status_effects/debuffs.dm index 30ed6f32772..9763aabae78 100644 --- a/code/modules/antagonists/heretic/status_effects/debuffs.dm +++ b/code/modules/antagonists/heretic/status_effects/debuffs.dm @@ -14,10 +14,12 @@ icon_state = "void_chill" /datum/status_effect/void_chill/on_apply() + owner.add_atom_colour(COLOR_BLUE_LIGHT, TEMPORARY_COLOUR_PRIORITY) owner.add_movespeed_modifier(/datum/movespeed_modifier/void_chill, update = TRUE) return TRUE /datum/status_effect/void_chill/on_remove() + owner.remove_atom_colour(TEMPORARY_COLOUR_PRIORITY, COLOR_BLUE_LIGHT) owner.remove_movespeed_modifier(/datum/movespeed_modifier/void_chill, update = TRUE) /datum/status_effect/void_chill/tick() @@ -27,6 +29,10 @@ duration = 10 SECONDS cooling_per_tick = -20 +/datum/status_effect/void_chill/lasting + id = "lasting_void_chill" + duration = -1 + /datum/movespeed_modifier/void_chill multiplicative_slowdown = 0.3 diff --git a/code/modules/antagonists/heretic/status_effects/ghoul.dm b/code/modules/antagonists/heretic/status_effects/ghoul.dm index 5857dc28bd1..079acda6596 100644 --- a/code/modules/antagonists/heretic/status_effects/ghoul.dm +++ b/code/modules/antagonists/heretic/status_effects/ghoul.dm @@ -5,6 +5,8 @@ alert_type = /atom/movable/screen/alert/status_effect/ghoul /// The new max health value set for the ghoul, if supplied var/new_max_health + /// What, if any, stamina modifier we applied to the ghoul mob + var/stamina_mod_applied /// Reference to the master of the ghoul's mind var/datum/mind/master_mind /// An optional callback invoked when a ghoul is made (on_apply) @@ -51,6 +53,9 @@ human_target.revive(ADMIN_HEAL_ALL) // Have to do an admin heal here, otherwise they'll likely just die due to missing organs or limbs if(new_max_health) + if(new_max_health < human_target.maxHealth) + stamina_mod_applied = (new_max_health / human_target.maxHealth) + human_target.physiology.stamina_mod *= stamina_mod_applied human_target.setMaxHealth(new_max_health) human_target.health = new_max_health @@ -78,6 +83,8 @@ var/mob/living/carbon/human/human_target = owner if(new_max_health) + if(isnum(stamina_mod_applied)) + human_target.physiology.stamina_mod /= stamina_mod_applied human_target.setMaxHealth(initial(human_target.maxHealth)) on_lost_callback?.Invoke(human_target) diff --git a/code/modules/antagonists/wizard/equipment/spellbook_entries/offensive.dm b/code/modules/antagonists/wizard/equipment/spellbook_entries/offensive.dm index 8992bffd731..f75f4bd8b61 100644 --- a/code/modules/antagonists/wizard/equipment/spellbook_entries/offensive.dm +++ b/code/modules/antagonists/wizard/equipment/spellbook_entries/offensive.dm @@ -45,7 +45,7 @@ /datum/spellbook_entry/teslablast name = "Tesla Blast" desc = "Charge up a tesla arc and release it at a random nearby target! You can move freely while it charges. The arc jumps between targets and can knock them down." - spell_type = /datum/action/cooldown/spell/tesla + spell_type = /datum/action/cooldown/spell/charged/beam/tesla category = "Offensive" /datum/spellbook_entry/lightningbolt diff --git a/code/modules/spells/spell.dm b/code/modules/spells/spell.dm index 39dfd210c19..7a0272ae19f 100644 --- a/code/modules/spells/spell.dm +++ b/code/modules/spells/spell.dm @@ -181,10 +181,12 @@ if(spell_requirements & SPELL_REQUIRES_WIZARD_GARB) var/mob/living/carbon/human/human_owner = owner if(!(human_owner.wear_suit?.clothing_flags & CASTING_CLOTHES)) - to_chat(owner, span_warning("You don't feel strong enough without your robe!")) + if(feedback) + to_chat(owner, span_warning("You don't feel strong enough without your robe!")) return FALSE if(!(human_owner.head?.clothing_flags & CASTING_CLOTHES)) - to_chat(owner, span_warning("You don't feel strong enough without your hat!")) + if(feedback) + to_chat(owner, span_warning("You don't feel strong enough without your hat!")) return FALSE else @@ -208,6 +210,7 @@ /** * Check if the target we're casting on is a valid target. * For self-casted spells, the target being checked (cast_on) is the caster. + * For click_to_activate spells, the target being checked is the clicked atom. * * Return TRUE if cast_on is valid, FALSE otherwise */ @@ -384,7 +387,7 @@ return FALSE spell_level++ - cooldown_time = max(cooldown_time - cooldown_reduction_per_rank, 0) + cooldown_time = max(cooldown_time - cooldown_reduction_per_rank, 0.25 SECONDS) // 0 second CD starts to break things. update_spell_name() return TRUE @@ -400,7 +403,11 @@ return FALSE spell_level-- - cooldown_time = min(cooldown_time + cooldown_reduction_per_rank, initial(cooldown_time)) + if(cooldown_reduction_per_rank > 0 SECONDS) + cooldown_time = min(cooldown_time + cooldown_reduction_per_rank, initial(cooldown_time)) + else + cooldown_time = max(cooldown_time + cooldown_reduction_per_rank, initial(cooldown_time)) + update_spell_name() return TRUE diff --git a/code/modules/spells/spell_types/charged/_charged.dm b/code/modules/spells/spell_types/charged/_charged.dm new file mode 100644 index 00000000000..7646f763956 --- /dev/null +++ b/code/modules/spells/spell_types/charged/_charged.dm @@ -0,0 +1,158 @@ +/** + * ## Channelled spells + * + * These spells do something after a channel time. + * To use this template, all that's needed is for cast() to be implemented. + */ +/datum/action/cooldown/spell/charged + /// What message do we display when we start chanelling? + var/channel_message + /// Whether we're currently channelling / charging the spell + var/currently_channeling = FALSE + /// How long it takes to channel the spell. + var/channel_time = 10 SECONDS + + // Overlay optional, applied when we start channelling + /// What icon should we use for our overlay + var/charge_overlay_icon + /// What icon state should we use for our overlay + var/charge_overlay_state + /// The actual appearance / our overlay. Don't mess with this + var/mutable_appearance/charge_overlay_instance + + // Sound optional, played when we start chanelling + /// What soundpath should we play when we start chanelling + var/charge_sound + /// The actual sound we generate, don't mess with this + var/sound/charge_sound_instance + +/datum/action/cooldown/spell/charged/New(Target, original) + . = ..() + if(!channel_message) + channel_message = span_notice("You start chanelling [src]...") + + if(charge_sound) + charge_sound_instance = sound(charge_sound, channel = CHANNEL_CHARGED_SPELL) + + if(charge_overlay_icon && charge_overlay_state) + charge_overlay_instance = mutable_appearance(charge_overlay_icon, charge_overlay_state, EFFECTS_LAYER) + +/datum/action/cooldown/spell/charged/Destroy() + if(owner) + stop_channel_effect(owner) + + charge_overlay_instance = null + charge_sound_instance = null + return ..() + +/datum/action/cooldown/spell/charged/Remove(mob/living/remove_from) + stop_channel_effect(remove_from) + return ..() + +/datum/action/cooldown/spell/charged/can_cast_spell(feedback = TRUE) + . = ..() + if(!.) + return FALSE + if(currently_channeling) + if(feedback) + to_chat(owner, span_warning("You're already channeling [src]!")) + return FALSE + + return TRUE + +/datum/action/cooldown/spell/charged/before_cast(atom/cast_on) + . = ..() + if(. & SPELL_CANCEL_CAST) + return + + to_chat(cast_on, channel_message) + + if(charge_sound_instance) + playsound(cast_on, charge_sound_instance, 50, FALSE) + + if(charge_overlay_instance) + cast_on.add_overlay(charge_overlay_instance) + + currently_channeling = TRUE + UpdateButtons(status_only = TRUE) + if(!do_after(cast_on, channel_time, timed_action_flags = (IGNORE_USER_LOC_CHANGE|IGNORE_HELD_ITEM))) + stop_channel_effect(cast_on) + return . | SPELL_CANCEL_CAST + +/datum/action/cooldown/spell/charged/cast(atom/cast_on) + . = ..() + stop_channel_effect(cast_on) + +/datum/action/cooldown/spell/charged/set_statpanel_format() + . = ..() + if(!islist(.)) + return + + if(currently_channeling) + .[PANEL_DISPLAY_STATUS] = "CHANNELING" + +/// Interrupts the chanelling effect, removing any overlay or sound playing (for the passed mob) +/datum/action/cooldown/spell/charged/proc/stop_channel_effect(mob/for_who) + if(charge_overlay_instance) + for_who.cut_overlay(charge_overlay_instance) + + if(charge_sound_instance) + for_who.stop_sound_channel(CHANNEL_CHARGED_SPELL) + // Play a null sound in to cancel the sound playing, because byond + playsound(for_who, sound(null, repeat = 0, channel = CHANNEL_CHARGED_SPELL), 50, FALSE) + + currently_channeling = FALSE + UpdateButtons(status_only = TRUE) + +/** + * ### Channelled "Beam" spells + * + * Channelled spells that pick a random target from nearby atoms to cast a spell on. + * Commonly used for beams, hence the name, but nothing's stopping projectiles or whatever from working. + * + * If no targets are nearby, cancels the spell and refunds the cooldown. + */ +/datum/action/cooldown/spell/charged/beam + /// The radius around the caster to find a target. + var/target_radius = 5 + /// The maximum number of bounces the beam will go before stopping. + var/max_beam_bounces = 1 + /// Who's our initial beam target? Set in before cast, used in cast. + var/atom/initial_target + +/datum/action/cooldown/spell/charged/beam/Destroy() + initial_target = null // This like shouuld never hang references but I've seen some cursed things so let's be safe + return ..() + +/datum/action/cooldown/spell/charged/beam/before_cast(atom/cast_on) + . = ..() + if(. & SPELL_CANCEL_CAST) + return + + initial_target = get_target(cast_on) + if(isnull(initial_target)) + cast_on.balloon_alert(cast_on, "no targets nearby!") + stop_channel_effect(cast_on) + return . | SPELL_CANCEL_CAST + +/datum/action/cooldown/spell/charged/beam/cast(atom/cast_on) + . = ..() + send_beam(cast_on, initial_target, max_beam_bounces) + initial_target = null + +/datum/action/cooldown/spell/charged/beam/proc/send_beam(atom/origin, atom/to_beam, bounces) + SHOULD_CALL_PARENT(FALSE) + CRASH("[type] did not implement send_beam and either has no effects or implemented the spell incorrectly.") + +/datum/action/cooldown/spell/charged/beam/proc/get_target(atom/center) + var/list/things = list() + for(var/atom/nearby_thing in range(target_radius, center)) + if(nearby_thing == owner || nearby_thing == center) + continue + + things += nearby_thing + + if(!length(things)) + return null + + return pick(things) diff --git a/code/modules/spells/spell_types/charged/tesla_blast.dm b/code/modules/spells/spell_types/charged/tesla_blast.dm new file mode 100644 index 00000000000..efad0e4a692 --- /dev/null +++ b/code/modules/spells/spell_types/charged/tesla_blast.dm @@ -0,0 +1,62 @@ +/datum/action/cooldown/spell/charged/beam/tesla + name = "Tesla Blast" + desc = "Charge up a tesla arc and release it at random nearby targets! \ + You can move freely while it charges. The arc jumps between targets and can knock them down." + button_icon_state = "lightning" + sound = 'sound/magic/lightningbolt.ogg' + + cooldown_time = 30 SECONDS + cooldown_reduction_per_rank = 6.75 SECONDS + + invocation = "UN'LTD P'WAH!" + invocation_type = INVOCATION_SHOUT + school = SCHOOL_EVOCATION + + channel_message = span_notice("You start gathering power...") + charge_overlay_icon = 'icons/effects/effects.dmi' + charge_overlay_state = "electricity" + charge_sound = 'sound/magic/lightning_chargeup.ogg' + target_radius = 7 + max_beam_bounces = 5 + + /// How much energy / damage does the initial bounce + var/initial_energy = 30 + /// How much energy / damage is lost per bounce + var/energy_lost_per_bounce = 5 + +/// Zaps a target, the bolt originating from origin. +/datum/action/cooldown/spell/charged/beam/tesla/send_beam(atom/origin, mob/living/carbon/to_beam, bolt_energy = 30, bounces = 5) + origin.Beam(to_beam, icon_state = "lightning[rand(1,12)]", time = 0.5 SECONDS) + playsound(get_turf(to_beam), 'sound/magic/lightningshock.ogg', 50, TRUE, -1) + + if(to_beam.can_block_magic(antimagic_flags)) + to_beam.visible_message( + span_warning("[to_beam] absorbs the spell, remaining unharmed!"), + span_userdanger("You absorb the spell, remaining unharmed!"), + ) + + else + to_beam.electrocute_act(bolt_energy, "Lightning Bolt", flags = SHOCK_NOGLOVES) + + // Bounce again! Call our proc recursively to keep the chain going (even if our mob blocked it with antimagic) + if(bounces < 1) + return + var/mob/living/carbon/to_beam_next = get_target(to_beam) + if(isnull(to_beam_next)) + return + send_beam(to_beam, to_beam_next, max(bolt_energy - energy_lost_per_bounce, energy_lost_per_bounce), bounces - 1) + +/datum/action/cooldown/spell/charged/beam/tesla/get_target(atom/center) + var/list/possibles = list() + for(var/mob/living/carbon/to_check in view(target_radius, center)) + if(to_check == center || to_check == owner) + continue + if(!length(get_path_to(center, to_check, max_distance = target_radius, simulated_only = FALSE))) + continue + + possibles += to_check + + if(!length(possibles)) + return null + + return pick(possibles) diff --git a/code/modules/spells/spell_types/cone/_cone.dm b/code/modules/spells/spell_types/cone/_cone.dm index e7a03426cd3..e1364a1d0f3 100644 --- a/code/modules/spells/spell_types/cone/_cone.dm +++ b/code/modules/spells/spell_types/cone/_cone.dm @@ -70,39 +70,50 @@ left_dir = SOUTH right_dir = NORTH - for(var/i in 1 to cone_levels) + // Go though every level of the cone levels and generate the cone. + for(var/level in 1 to cone_levels) var/list/level_turfs = list() + // Our center turf always exists, it's straight ahead of the caster. turf_to_use = get_step(turf_to_use, dir_to_use) level_turfs += turf_to_use - if(i != 1) - left_turf = get_step(turf_to_use, left_dir) - level_turfs += left_turf - right_turf = get_step(turf_to_use, right_dir) - level_turfs += right_turf - for(var/left_i in 1 to i -calculate_cone_shape(i)) - if(left_turf.density && respect_density) + // Level 1 only ever has 1 turf, it's a cone. + if(level != 1) + var/level_width_in_each_direction = round((calculate_cone_shape(level) - 1) / 2) + left_turf = turf_to_use + right_turf = turf_to_use + // Check turfs to the left... + for(var/left_of_center in 1 to level_width_in_each_direction) + if(respect_density && left_turf.density) break left_turf = get_step(left_turf, left_dir) level_turfs += left_turf - for(var/right_i in 1 to i -calculate_cone_shape(i)) - if(right_turf.density && respect_density) + // And turfs to the right. + for(var/right_of_enter in 1 to level_width_in_each_direction) + if(respect_density && right_turf.density) break right_turf = get_step(right_turf, right_dir) level_turfs += right_turf + // Add the list of all turfs on this level to the turfs to return turfs_to_return += list(level_turfs) - if(i == cone_levels) - continue - if(turf_to_use.density && respect_density) + + // If we're at the last level, we're done + if(level == cone_levels) break + // But if we're not at the last level, we should check that we can keep going + if(respect_density && turf_to_use.density) + break + return turfs_to_return -///This proc adjusts the cones width depending on the level. +/** + * Adjusts the width of the cone at the passed level. + * This is never called on the first level of the cone (level 1 is always 1 width) + * + * Return a number - the TOTAL width of the cone at the passed level. + */ /datum/action/cooldown/spell/cone/proc/calculate_cone_shape(current_level) - var/end_taper_start = round(cone_levels * 0.8) - if(current_level > end_taper_start) - return (current_level % end_taper_start) * 2 //someone more talented and probably come up with a better formula. - else - return 2 + // Default formula: (1 (innate) -> 3 -> 5 -> 5 -> 7 -> 7 -> 9 -> 9 -> ...) + return current_level + (current_level % 2) + 1 /** * ### Staggered Cone diff --git a/code/modules/spells/spell_types/cone/cone_of_cold.dm b/code/modules/spells/spell_types/cone/cone_of_cold.dm new file mode 100644 index 00000000000..c60c8613e4e --- /dev/null +++ b/code/modules/spells/spell_types/cone/cone_of_cold.dm @@ -0,0 +1,67 @@ +/datum/action/cooldown/spell/cone/staggered/cone_of_cold + name = "Cone of Cold" + desc = "Shoots out a freezing cone in front of you." + + school = SCHOOL_EVOCATION + cooldown_time = 30 SECONDS + cooldown_reduction_per_rank = 4 SECONDS + + invocation = "ISAGE!" // What killed the dinosaurs? THE ICE AGE + invocation_type = INVOCATION_SHOUT + + cone_levels = 4 + respect_density = TRUE + delay_between_level = 0.05 SECONDS + + /// What flags do we pass to MakeSlippery when affecting turfs? + /// null / NONE / TURF_DRY means the turf is unaffected + var/turf_freeze_type = TURF_WET_PERMAFROST + /// How long do turfs remain slippery / frozen for? + /// 0 seconds means the turf is unaffected, INFINITY means it's made perma-wet + var/unfreeze_turf_duration = 45 SECONDS + + /// What status effect do we apply when affecting mobs? + /// null means no status effect is applied + var/datum/status_effect/frozen_status_effect_path = /datum/status_effect/freon/lasting + /// How long do mobs remain frozen for? + /// 0 seconds means no status effect is applied, INFINITY means infinite duration (or default duration of the status effect) + var/unfreeze_mob_duration = 20 SECONDS + /// How much brute do we apply on freeze? + var/on_freeze_brute_damage = 10 + /// How much burn do we apply on freeze? + var/on_freeze_burn_damage = 20 + + /// How long do objects remain frozen for? + /// 0 seconds mean no objects are frozen, INFINITY means infinite duration freeze + var/unfreeze_object_duration = 20 SECONDS + +/datum/action/cooldown/spell/cone/staggered/cone_of_cold/do_turf_cone_effect(turf/target_turf, atom/caster, level) + if(!turf_freeze_type || unfreeze_turf_duration <= 0 SECONDS) // 0 duration = don't apply the slip + return + if(!isopenturf(target_turf)) + return + var/turf/open/frozen_floor = target_turf + frozen_floor.MakeSlippery(turf_freeze_type, unfreeze_turf_duration, permanent = (unfreeze_turf_duration == INFINITY)) + +/datum/action/cooldown/spell/cone/staggered/cone_of_cold/do_mob_cone_effect(mob/living/target_mob, atom/caster, level) + if(target_mob.can_block_magic(antimagic_flags) || target_mob == caster) + return + + if(ispath(frozen_status_effect_path) && unfreeze_mob_duration > 0 SECONDS) // 0 duration = don't apply the status effect + var/datum/status_effect/freeze = target_mob.apply_status_effect(frozen_status_effect_path) + if(unfreeze_mob_duration != INFINITY) + freeze.duration = world.time + unfreeze_mob_duration + + if(on_freeze_brute_damage || on_freeze_burn_damage) + target_mob.take_overall_damage(on_freeze_brute_damage, on_freeze_burn_damage) + + to_chat(target_mob, span_userdanger("You feel a bitter cold!")) + +/datum/action/cooldown/spell/cone/staggered/cone_of_cold/do_obj_cone_effect(obj/target_obj, atom/caster, level) + if(unfreeze_object_duration <= 0 SECONDS) // 0 duration = don't apply a freeze + return + if(!target_obj.freeze()) + return + if(unfreeze_object_duration == INFINITY) // Infinity duration = don't set an unfreeze timer + return + addtimer(CALLBACK(target_obj, TYPE_PROC_REF(/obj/, unfreeze)), unfreeze_object_duration) diff --git a/code/modules/spells/spell_types/conjure/invisible_chair.dm b/code/modules/spells/spell_types/conjure/invisible_chair.dm index e0694898c09..1fa727a68a5 100644 --- a/code/modules/spells/spell_types/conjure/invisible_chair.dm +++ b/code/modules/spells/spell_types/conjure/invisible_chair.dm @@ -4,6 +4,7 @@ background_icon_state = "bg_mime" icon_icon = 'icons/mob/actions/actions_mime.dmi' button_icon_state = "invisible_chair" + check_flags = AB_CHECK_CONSCIOUS|AB_CHECK_HANDS_BLOCKED panel = "Mime" sound = null diff --git a/code/modules/spells/spell_types/conjure/invisible_wall.dm b/code/modules/spells/spell_types/conjure/invisible_wall.dm index 9433fbd7df0..3124277453f 100644 --- a/code/modules/spells/spell_types/conjure/invisible_wall.dm +++ b/code/modules/spells/spell_types/conjure/invisible_wall.dm @@ -4,6 +4,7 @@ background_icon_state = "bg_mime" icon_icon = 'icons/mob/actions/actions_mime.dmi' button_icon_state = "invisible_wall" + check_flags = AB_CHECK_CONSCIOUS|AB_CHECK_HANDS_BLOCKED panel = "Mime" sound = null diff --git a/code/modules/spells/spell_types/conjure_item/invisible_box.dm b/code/modules/spells/spell_types/conjure_item/invisible_box.dm index 64e14179474..adc66994bd8 100644 --- a/code/modules/spells/spell_types/conjure_item/invisible_box.dm +++ b/code/modules/spells/spell_types/conjure_item/invisible_box.dm @@ -6,6 +6,7 @@ background_icon_state = "bg_mime" icon_icon = 'icons/mob/actions/actions_mime.dmi' button_icon_state = "invisible_box" + check_flags = AB_CHECK_CONSCIOUS|AB_CHECK_HANDS_BLOCKED panel = "Mime" sound = null diff --git a/code/modules/spells/spell_types/jaunt/_jaunt.dm b/code/modules/spells/spell_types/jaunt/_jaunt.dm index cd1c713b186..9b6b417d2fd 100644 --- a/code/modules/spells/spell_types/jaunt/_jaunt.dm +++ b/code/modules/spells/spell_types/jaunt/_jaunt.dm @@ -19,6 +19,9 @@ /// What dummy mob type do we put jaunters in on jaunt? var/jaunt_type = /obj/effect/dummy/phased_mob +/datum/action/cooldown/spell/jaunt/before_cast(atom/cast_on) + return ..() | SPELL_NO_FEEDBACK // Don't do the feedback until after we're jaunting + /datum/action/cooldown/spell/jaunt/can_cast_spell(feedback = TRUE) . = ..() if(!.) @@ -49,10 +52,16 @@ * Returns the holder mob that was created */ /datum/action/cooldown/spell/jaunt/proc/enter_jaunt(mob/living/jaunter, turf/loc_override) + SHOULD_CALL_PARENT(TRUE) + var/obj/effect/dummy/phased_mob/jaunt = new jaunt_type(loc_override || get_turf(jaunter), jaunter) RegisterSignal(jaunt, COMSIG_MOB_EJECTED_FROM_JAUNT, PROC_REF(on_jaunt_exited)) spell_requirements |= SPELL_CASTABLE_WHILE_PHASED ADD_TRAIT(jaunter, TRAIT_MAGICALLY_PHASED, REF(src)) + ADD_TRAIT(jaunter, TRAIT_RUNECHAT_HIDDEN, REF(src)) + // Don't do the feedback until we have runechat hidden. + // Otherwise the text will follow the jaunt holder, which reveals where our caster is travelling. + spell_feedback() // This needs to happen at the end, after all the traits and stuff is handled SEND_SIGNAL(jaunter, COMSIG_MOB_ENTER_JAUNT, src, jaunt) @@ -68,6 +77,8 @@ * Returns TRUE on successful exit, FALSE otherwise */ /datum/action/cooldown/spell/jaunt/proc/exit_jaunt(mob/living/unjaunter, turf/loc_override) + SHOULD_CALL_PARENT(TRUE) + var/obj/effect/dummy/phased_mob/jaunt = unjaunter.loc if(!istype(jaunt)) return FALSE @@ -92,13 +103,10 @@ SHOULD_CALL_PARENT(TRUE) spell_requirements &= ~SPELL_CASTABLE_WHILE_PHASED REMOVE_TRAIT(unjaunter, TRAIT_MAGICALLY_PHASED, REF(src)) + REMOVE_TRAIT(unjaunter, TRAIT_RUNECHAT_HIDDEN, REF(src)) // This needs to happen at the end, after all the traits and stuff is handled SEND_SIGNAL(unjaunter, COMSIG_MOB_AFTER_EXIT_JAUNT, src) -/// Simple helper to check if the passed mob is currently jaunting or not -/datum/action/cooldown/spell/jaunt/proc/is_jaunting(mob/living/user) - return istype(user.loc, /obj/effect/dummy/phased_mob) - /datum/action/cooldown/spell/jaunt/Remove(mob/living/remove_from) exit_jaunt(remove_from) if (!is_jaunting(remove_from)) // In case you have made exit_jaunt conditional, as in mirror walk diff --git a/code/modules/spells/spell_types/jaunt/ethereal_jaunt.dm b/code/modules/spells/spell_types/jaunt/ethereal_jaunt.dm index 2aafab3b1b7..bbcc6300717 100644 --- a/code/modules/spells/spell_types/jaunt/ethereal_jaunt.dm +++ b/code/modules/spells/spell_types/jaunt/ethereal_jaunt.dm @@ -23,7 +23,7 @@ /// List of valid exit points var/list/exit_point_list -/datum/action/cooldown/spell/jaunt/ethereal_jaunt/enter_jaunt(mob/living/jaunter) +/datum/action/cooldown/spell/jaunt/ethereal_jaunt/enter_jaunt(mob/living/jaunter, turf/loc_override) . = ..() if(!.) return diff --git a/code/modules/spells/spell_types/pointed/_pointed.dm b/code/modules/spells/spell_types/pointed/_pointed.dm index 591d99e187f..8ddbd8f4fc9 100644 --- a/code/modules/spells/spell_types/pointed/_pointed.dm +++ b/code/modules/spells/spell_types/pointed/_pointed.dm @@ -87,12 +87,17 @@ to_chat(owner, span_warning("You cannot cast [src] on yourself!")) return FALSE - if(get_dist(owner, cast_on) > cast_range) - to_chat(owner, span_warning("[cast_on.p_theyre(TRUE)] too far away!")) - return FALSE - return TRUE +/datum/action/cooldown/spell/pointed/before_cast(atom/cast_on) + . = ..() + if(. & SPELL_CANCEL_CAST) + return + + if(owner && get_dist(get_turf(owner), get_turf(cast_on)) > cast_range) + cast_on.balloon_alert(owner, "too far away!") + return . | SPELL_CANCEL_CAST + /** * ### Pointed projectile spells * diff --git a/code/modules/spells/spell_types/pointed/finger_guns.dm b/code/modules/spells/spell_types/pointed/finger_guns.dm index 3ecd66ec7d2..3f53e7d6242 100644 --- a/code/modules/spells/spell_types/pointed/finger_guns.dm +++ b/code/modules/spells/spell_types/pointed/finger_guns.dm @@ -5,6 +5,7 @@ background_icon_state = "bg_mime" icon_icon = 'icons/mob/actions/actions_mime.dmi' button_icon_state = "finger_guns0" + check_flags = AB_CHECK_CONSCIOUS|AB_CHECK_HANDS_BLOCKED panel = "Mime" sound = null diff --git a/code/modules/spells/spell_types/self/forcewall.dm b/code/modules/spells/spell_types/self/forcewall.dm index e037c1ae689..150338a2db0 100644 --- a/code/modules/spells/spell_types/self/forcewall.dm +++ b/code/modules/spells/spell_types/self/forcewall.dm @@ -45,6 +45,7 @@ background_icon_state = "bg_mime" icon_icon = 'icons/mob/actions/actions_mime.dmi' button_icon_state = "invisible_blockade" + check_flags = AB_CHECK_CONSCIOUS|AB_CHECK_HANDS_BLOCKED panel = "Mime" sound = null diff --git a/code/modules/spells/spell_types/self/lightning.dm b/code/modules/spells/spell_types/self/lightning.dm deleted file mode 100644 index ecc2ad448e1..00000000000 --- a/code/modules/spells/spell_types/self/lightning.dm +++ /dev/null @@ -1,125 +0,0 @@ -/datum/action/cooldown/spell/tesla - name = "Tesla Blast" - desc = "Charge up a tesla arc and release it at random nearby targets! \ - You can move freely while it charges. The arc jumps between targets and can knock them down." - button_icon_state = "lightning" - - cooldown_time = 30 SECONDS - cooldown_reduction_per_rank = 6.75 SECONDS - - invocation = "UN'LTD P'WAH!" - invocation_type = INVOCATION_SHOUT - school = SCHOOL_EVOCATION - - /// Whether we're currently channelling a tesla blast or not - var/currently_channeling = FALSE - /// How long it takes to channel the zap. - var/channel_time = 10 SECONDS - /// The radius around (either the caster or people shocked) to which the tesla blast can reach - var/shock_radius = 7 - /// The halo that appears around the caster while charging the spell - var/static/mutable_appearance/halo - /// The sound played while charging the spell - /// Quote: "the only way i can think of to stop a sound, thank MSO for the idea." - var/sound/charge_sound - -/datum/action/cooldown/spell/tesla/Remove(mob/living/remove_from) - reset_tesla(remove_from) - return ..() - -/datum/action/cooldown/spell/tesla/set_statpanel_format() - . = ..() - if(!islist(.)) - return - - if(currently_channeling) - .[PANEL_DISPLAY_STATUS] = "CHANNELING" - -/datum/action/cooldown/spell/tesla/can_cast_spell(feedback = TRUE) - . = ..() - if(!.) - return FALSE - if(currently_channeling) - if(feedback) - to_chat(owner, span_warning("You're already channeling [src]!")) - return FALSE - - return TRUE - -/datum/action/cooldown/spell/tesla/before_cast(atom/cast_on) - . = ..() - if(. & SPELL_CANCEL_CAST) - return - - to_chat(cast_on, span_notice("You start gathering power...")) - charge_sound = new /sound('sound/magic/lightning_chargeup.ogg', channel = 7) - halo ||= mutable_appearance('icons/effects/effects.dmi', "electricity", EFFECTS_LAYER) - cast_on.add_overlay(halo) - playsound(get_turf(cast_on), charge_sound, 50, FALSE) - - currently_channeling = TRUE - if(!do_after(cast_on, channel_time, timed_action_flags = (IGNORE_USER_LOC_CHANGE|IGNORE_HELD_ITEM))) - reset_tesla(cast_on) - return . | SPELL_CANCEL_CAST - -/datum/action/cooldown/spell/tesla/reset_spell_cooldown() - reset_tesla(owner) - return ..() - -/// Resets the tesla effect. -/datum/action/cooldown/spell/tesla/proc/reset_tesla(atom/to_reset) - to_reset.cut_overlay(halo) - currently_channeling = FALSE - -/datum/action/cooldown/spell/tesla/cast(atom/cast_on) - . = ..() - - // byond, why you suck? - charge_sound = sound(null, repeat = 0, wait = 1, channel = charge_sound.channel) - // Sorry MrPerson, but the other ways just didn't do it the way i needed to work, this is the only way. - playsound(get_turf(cast_on), charge_sound, 50, FALSE) - - var/mob/living/carbon/to_zap_first = get_target(cast_on) - if(QDELETED(to_zap_first)) - cast_on.balloon_alert(cast_on, "no targets nearby!") - reset_spell_cooldown() - return FALSE - - playsound(get_turf(cast_on), 'sound/magic/lightningbolt.ogg', 50, TRUE) - zap_target(cast_on, to_zap_first) - reset_tesla(cast_on) - -/// Zaps a target, the bolt originating from origin. -/datum/action/cooldown/spell/tesla/proc/zap_target(atom/origin, mob/living/carbon/to_zap, bolt_energy = 30, bounces = 5) - origin.Beam(to_zap, icon_state = "lightning[rand(1,12)]", time = 0.5 SECONDS) - playsound(get_turf(to_zap), 'sound/magic/lightningshock.ogg', 50, TRUE, -1) - - if(to_zap.can_block_magic(antimagic_flags)) - to_zap.visible_message( - span_warning("[to_zap] absorbs the spell, remaining unharmed!"), - span_userdanger("You absorb the spell, remaining unharmed!"), - ) - - else - to_zap.electrocute_act(bolt_energy, "Lightning Bolt", flags = SHOCK_NOGLOVES) - - if(bounces >= 1) - var/mob/living/carbon/to_zap_next = get_target(to_zap) - if(!QDELETED(to_zap_next)) - zap_target(to_zap, to_zap_next, max((bolt_energy - 5), 5), bounces - 1) - -/// Get a target in view of us to zap next. Returns a carbon, or null if none were found. -/datum/action/cooldown/spell/tesla/proc/get_target(atom/center) - var/list/possibles = list() - for(var/mob/living/carbon/to_check in view(shock_radius, center)) - if(to_check == center || to_check == owner) - continue - if(!length(get_path_to(center, to_check, max_distance = shock_radius, simulated_only = FALSE))) - continue - - possibles += to_check - - if(!length(possibles)) - return null - - return pick(possibles) diff --git a/code/modules/spells/spell_types/touch/_touch.dm b/code/modules/spells/spell_types/touch/_touch.dm index f5dff19fd52..384d722bf2c 100644 --- a/code/modules/spells/spell_types/touch/_touch.dm +++ b/code/modules/spells/spell_types/touch/_touch.dm @@ -1,3 +1,23 @@ +/** + * ## Touch Spell + * + * Touch spells are spells which function through the power of an item attack. + * + * Instead of the spell triggering when the caster presses the button, + * pressing the button will give them a hand object. + * The spell's effects are cast when the hand object makes contact with something. + * + * To implement a touch spell, all you need is to implement: + * * is_valid_target - to check whether the slapped target is valid + * * cast_on_hand_hit - to implement effects on cast + * + * However, for added complexity, you can optionally implement: + * * on_antimagic_triggered - to cause effects when antimagic is triggered + * * cast_on_secondary_hand_hit - to implement different effects if the caster r-clicked + + * It is not necessarily to touch any of the core functions, and is + * (generally) inadvisable unless you know what you're doing + */ /datum/action/cooldown/spell/touch check_flags = AB_CHECK_CONSCIOUS|AB_CHECK_HANDS_BLOCKED sound = 'sound/items/welder.ogg' @@ -12,6 +32,8 @@ var/draw_message = span_notice("You channel the power of the spell to your hand.") /// The message displayed upon willingly dropping / deleting / cancelling the touch hand before using it var/drop_message = span_notice("You draw the power out of your hand.") + /// If TRUE, the caster can willingly hit themselves with the hand + var/can_cast_on_self = FALSE /datum/action/cooldown/spell/touch/Destroy() // If we have an owner, the hand is cleaned up in Remove(), which Destroy() calls. @@ -23,6 +45,10 @@ remove_hand(remove_from) return ..() +// PreActivate is overridden to not check is_valid_target on the caster, as it makes less sense. +/datum/action/cooldown/spell/touch/PreActivate(atom/target) + return Activate(target) + /datum/action/cooldown/spell/touch/UpdateButton(atom/movable/screen/movable/action_button/button, status_only = FALSE, force = FALSE) . = ..() if(!button) @@ -42,6 +68,9 @@ . = ..() if(!.) return FALSE + // Currently, hard checks for carbons. + // If someone wants to add support for simplemobs + // adminbussed to have hands, go for it if(!iscarbon(owner)) return FALSE var/mob/living/carbon/carbon_owner = owner @@ -49,8 +78,9 @@ return FALSE return TRUE +// Checks if the mob slapped with the hand is a valid target. /datum/action/cooldown/spell/touch/is_valid_target(atom/cast_on) - return iscarbon(cast_on) + return isliving(cast_on) /** * Creates a new hand_path hand and equips it to the caster. @@ -59,6 +89,8 @@ * Otherwise, registers signals and returns TRUE. */ /datum/action/cooldown/spell/touch/proc/create_hand(mob/living/carbon/cast_on) + SHOULD_CALL_PARENT(TRUE) + var/obj/item/melee/touch_attack/new_hand = new hand_path(cast_on, src) if(!cast_on.put_in_hands(new_hand, del_on_fail = TRUE)) reset_spell_cooldown() @@ -69,10 +101,7 @@ return FALSE attached_hand = new_hand - RegisterSignal(attached_hand, COMSIG_ITEM_AFTERATTACK, PROC_REF(on_hand_hit)) - RegisterSignal(attached_hand, COMSIG_ITEM_AFTERATTACK_SECONDARY, PROC_REF(on_secondary_hand_hit)) - RegisterSignal(attached_hand, COMSIG_PARENT_QDELETING, PROC_REF(on_hand_deleted)) - RegisterSignal(attached_hand, COMSIG_ITEM_DROPPED, PROC_REF(on_hand_dropped)) + register_hand_signals() to_chat(cast_on, draw_message) return TRUE @@ -83,8 +112,10 @@ * If reset_cooldown_after is FALSE, we will instead just start the spell's cooldown */ /datum/action/cooldown/spell/touch/proc/remove_hand(mob/living/hand_owner, reset_cooldown_after = FALSE) + SHOULD_CALL_PARENT(TRUE) + if(!QDELETED(attached_hand)) - UnregisterSignal(attached_hand, list(COMSIG_ITEM_AFTERATTACK, COMSIG_ITEM_AFTERATTACK_SECONDARY, COMSIG_PARENT_QDELETING, COMSIG_ITEM_DROPPED)) + unregister_hand_signals() hand_owner?.temporarilyRemoveItemFromInventory(attached_hand) QDEL_NULL(attached_hand) @@ -95,6 +126,26 @@ else StartCooldown() +/// Registers all signal procs for the hand. +/datum/action/cooldown/spell/touch/proc/register_hand_signals() + SHOULD_CALL_PARENT(TRUE) + + RegisterSignal(attached_hand, COMSIG_ITEM_AFTERATTACK, PROC_REF(on_hand_hit)) + RegisterSignal(attached_hand, COMSIG_ITEM_AFTERATTACK_SECONDARY, PROC_REF(on_secondary_hand_hit)) + RegisterSignal(attached_hand, COMSIG_ITEM_DROPPED, PROC_REF(on_hand_dropped)) + RegisterSignal(attached_hand, COMSIG_PARENT_QDELETING, PROC_REF(on_hand_deleted)) + +/// Unregisters all signal procs for the hand. +/datum/action/cooldown/spell/touch/proc/unregister_hand_signals() + SHOULD_CALL_PARENT(TRUE) + + UnregisterSignal(attached_hand, list( + COMSIG_ITEM_AFTERATTACK, + COMSIG_ITEM_AFTERATTACK_SECONDARY, + COMSIG_ITEM_DROPPED, + COMSIG_PARENT_QDELETING, + )) + // Touch spells don't go on cooldown OR give off an invocation until the hand is used itself. /datum/action/cooldown/spell/touch/before_cast(atom/cast_on) return ..() | SPELL_NO_FEEDBACK | SPELL_NO_IMMEDIATE_COOLDOWN @@ -114,12 +165,11 @@ */ /datum/action/cooldown/spell/touch/proc/on_hand_hit(datum/source, atom/victim, mob/caster, proximity_flag, click_parameters) SIGNAL_HANDLER + SHOULD_NOT_OVERRIDE(TRUE) // DEFINITELY don't put effects here, put them in cast_on_hand_hit if(!proximity_flag) return - if(victim == caster) - return - if(!can_cast_spell(feedback = FALSE)) + if(!can_hit_with_hand(victim, caster)) return INVOKE_ASYNC(src, PROC_REF(do_hand_hit), source, victim, caster) @@ -131,22 +181,43 @@ */ /datum/action/cooldown/spell/touch/proc/on_secondary_hand_hit(datum/source, atom/victim, mob/caster, proximity_flag, click_parameters) SIGNAL_HANDLER + SHOULD_NOT_OVERRIDE(TRUE) // DEFINITELY don't put effects here, put them in cast_on_secondary_hand_hit if(!proximity_flag) return - if(victim == caster) - return - if(!can_cast_spell(feedback = FALSE)) + if(!can_hit_with_hand(victim, caster)) return INVOKE_ASYNC(src, PROC_REF(do_secondary_hand_hit), source, victim, caster) + return COMPONENT_SECONDARY_CANCEL_ATTACK_CHAIN + +/// Checks if the passed victim can be cast on by the caster. +/datum/action/cooldown/spell/touch/proc/can_hit_with_hand(atom/victim, mob/caster) + if(!can_cast_on_self && victim == caster) + return FALSE + if(!is_valid_target(victim)) + return FALSE + if(!can_cast_spell(feedback = TRUE)) + return FALSE + + return TRUE /** * Calls cast_on_hand_hit() from the caster onto the victim. + * It's worth noting that victim will be guaranteed to be whatever checks are implemented in is_valid_target by this point. + * + * Implements checks for antimagic. */ /datum/action/cooldown/spell/touch/proc/do_hand_hit(obj/item/melee/touch_attack/hand, atom/victim, mob/living/carbon/caster) + SHOULD_NOT_OVERRIDE(TRUE) // Don't put effects here, put them in cast_on_hand_hit + SEND_SIGNAL(src, COMSIG_SPELL_TOUCH_HAND_HIT, victim, caster, hand) - if(!cast_on_hand_hit(hand, victim, caster)) + + var/mob/mob_victim = victim + if(istype(mob_victim) && mob_victim.can_block_magic(antimagic_flags)) + on_antimagic_triggered(hand, victim, caster) + + else if(!cast_on_hand_hit(hand, victim, caster)) return log_combat(caster, victim, "cast the touch spell [name] on", hand) @@ -155,8 +226,13 @@ /** * Calls do_secondary_hand_hit() from the caster onto the victim. + * It's worth noting that victim will be guaranteed to be whatever checks are implemented in is_valid_target by this point. + + * Does NOT check for antimagic on its own. Implement your own checks if you want the r-click to abide by it. */ /datum/action/cooldown/spell/touch/proc/do_secondary_hand_hit(obj/item/melee/touch_attack/hand, atom/victim, mob/living/carbon/caster) + SHOULD_NOT_OVERRIDE(TRUE) // Don't put effects here, put them in cast_on_secondary_hand_hit + var/secondary_result = cast_on_secondary_hand_hit(hand, victim, caster) switch(secondary_result) // Continue will remove the hand here and stop @@ -216,6 +292,22 @@ remove_hand(dropper, reset_cooldown_after = TRUE) +/** + * Called whenever our spell is cast, but blocked by antimagic. + */ +/datum/action/cooldown/spell/touch/proc/on_antimagic_triggered(obj/item/melee/touch_attack/hand, atom/victim, mob/living/carbon/caster) + return + +/** + * ## Touch attack item + * + * Used for touch spells to have something physical to slap people with. + * + * Try to avoid adding behavior onto these for your touch spells! + * The spells themselves should handle most, if not all, of the casted effects. + * + * These should generally just be dummy objects - holds name and icon stuff. + */ /obj/item/melee/touch_attack name = "\improper outstretched hand" desc = "High Five?" @@ -224,7 +316,7 @@ righthand_file = 'icons/mob/inhands/items/touchspell_righthand.dmi' icon_state = "latexballon" inhand_icon_state = null - item_flags = NEEDS_PERMIT | ABSTRACT + item_flags = NEEDS_PERMIT | ABSTRACT | HAND_ITEM w_class = WEIGHT_CLASS_HUGE force = 0 throwforce = 0 @@ -243,7 +335,7 @@ if(!iscarbon(user)) //Look ma, no hands return TRUE if(!(user.mobility_flags & MOBILITY_USE)) - to_chat(user, span_warning("You can't reach out!")) + user.balloon_alert(user, "can't reach out!") return TRUE return ..() @@ -254,7 +346,7 @@ * However, if you want to consume the hand and not give a cooldown, * such as adding a unique behavior to the hand specifically, this function will do that. */ -/obj/item/melee/touch_attack/mansus_fist/proc/remove_hand_with_no_refund(mob/holder) +/obj/item/melee/touch_attack/proc/remove_hand_with_no_refund(mob/holder) var/datum/action/cooldown/spell/touch/hand_spell = spell_which_made_us?.resolve() if(!QDELETED(hand_spell)) hand_spell.remove_hand(holder, reset_cooldown_after = FALSE) diff --git a/code/modules/spells/spell_types/touch/duffelbag_curse.dm b/code/modules/spells/spell_types/touch/duffelbag_curse.dm index 0416350ca69..1ca5d0e1ffc 100644 --- a/code/modules/spells/spell_types/touch/duffelbag_curse.dm +++ b/code/modules/spells/spell_types/touch/duffelbag_curse.dm @@ -14,11 +14,7 @@ hand_path = /obj/item/melee/touch_attack/duffelbag -/datum/action/cooldown/spell/touch/duffelbag/cast_on_hand_hit(obj/item/melee/touch_attack/hand, atom/victim, mob/living/carbon/caster) - if(!iscarbon(victim)) - return FALSE - - var/mob/living/carbon/duffel_victim = victim + /// Some meme "elaborate backstories" to use. var/static/list/elaborate_backstory = list( "spacewar origin story", "military background", @@ -28,52 +24,57 @@ "upbringing on the space farm", "fond memories with your buddy Keith", ) - if(duffel_victim.can_block_magic(antimagic_flags)) - to_chat(caster, span_warning("The spell can't seem to affect [duffel_victim]!")) - to_chat(duffel_victim, span_warning("You really don't feel like talking about your [pick(elaborate_backstory)] with complete strangers today.")) - return TRUE + +/datum/action/cooldown/spell/touch/duffelbag/is_valid_target(atom/cast_on) + return iscarbon(cast_on) + +/datum/action/cooldown/spell/touch/duffelbag/on_antimagic_triggered(obj/item/melee/touch_attack/hand, mob/living/carbon/victim, mob/living/carbon/caster) + to_chat(caster, span_warning("The spell can't seem to affect [victim]!")) + to_chat(victim, span_warning("You really don't feel like talking about your [pick(elaborate_backstory)] with complete strangers today.")) + +/datum/action/cooldown/spell/touch/duffelbag/cast_on_hand_hit(obj/item/melee/touch_attack/hand, mob/living/carbon/victim, mob/living/carbon/caster) // To get it started, stun and knockdown the person being hit - duffel_victim.flash_act() - duffel_victim.Immobilize(5 SECONDS) - duffel_victim.apply_damage(80, STAMINA) - duffel_victim.Knockdown(5 SECONDS) + victim.flash_act() + victim.Immobilize(5 SECONDS) + victim.apply_damage(80, STAMINA) + victim.Knockdown(5 SECONDS) // If someone's already cursed, don't try to give them another - if(HAS_TRAIT(duffel_victim, TRAIT_DUFFEL_CURSE_PROOF)) - to_chat(caster, span_warning("The burden of [duffel_victim]'s duffel bag becomes too much, shoving them to the floor!")) - to_chat(duffel_victim, span_warning("The weight of this bag becomes overburdening!")) + if(HAS_TRAIT(victim, TRAIT_DUFFEL_CURSE_PROOF)) + to_chat(caster, span_warning("The burden of [victim]'s duffel bag becomes too much, shoving them to the floor!")) + to_chat(victim, span_warning("The weight of this bag becomes overburdening!")) return TRUE // However if they're uncursed, they're fresh for getting a cursed bag var/obj/item/storage/backpack/duffelbag/cursed/conjured_duffel = new get_turf(victim) - duffel_victim.visible_message( - span_danger("A growling duffel bag appears on [duffel_victim]!"), + victim.visible_message( + span_danger("A growling duffel bag appears on [victim]!"), span_danger("You feel something attaching itself to you, and a strong desire to discuss your [pick(elaborate_backstory)] at length!"), ) // This duffelbag is now cuuuurrrsseed! Equip it on them ADD_TRAIT(conjured_duffel, TRAIT_DUFFEL_CURSE_PROOF, CURSED_ITEM_TRAIT(conjured_duffel.name)) - conjured_duffel.pickup(duffel_victim) - conjured_duffel.forceMove(duffel_victim) + conjured_duffel.pickup(victim) + conjured_duffel.forceMove(victim) // Put it on their back first - if(duffel_victim.dropItemToGround(duffel_victim.back)) - duffel_victim.equip_to_slot_if_possible(conjured_duffel, ITEM_SLOT_BACK, TRUE, TRUE) + if(victim.dropItemToGround(victim.back)) + victim.equip_to_slot_if_possible(conjured_duffel, ITEM_SLOT_BACK, TRUE, TRUE) return TRUE // If the back equip failed, put it in their hands first - if(duffel_victim.put_in_hands(conjured_duffel)) + if(victim.put_in_hands(conjured_duffel)) return TRUE // If they had no empty hands, try to put it in their inactive hand first - duffel_victim.dropItemToGround(duffel_victim.get_inactive_held_item()) - if(duffel_victim.put_in_hands(conjured_duffel)) + victim.dropItemToGround(victim.get_inactive_held_item()) + if(victim.put_in_hands(conjured_duffel)) return TRUE // If their inactive hand couldn't be emptied or found, put it in their active hand - duffel_victim.dropItemToGround(duffel_victim.get_active_held_item()) - if(duffel_victim.put_in_hands(conjured_duffel)) + victim.dropItemToGround(victim.get_active_held_item()) + if(victim.put_in_hands(conjured_duffel)) return TRUE // Well, we failed to give them the duffel bag, diff --git a/code/modules/spells/spell_types/touch/flesh_to_stone.dm b/code/modules/spells/spell_types/touch/flesh_to_stone.dm index c6693c7c904..a239c09adff 100644 --- a/code/modules/spells/spell_types/touch/flesh_to_stone.dm +++ b/code/modules/spells/spell_types/touch/flesh_to_stone.dm @@ -12,14 +12,13 @@ hand_path = /obj/item/melee/touch_attack/flesh_to_stone -/datum/action/cooldown/spell/touch/flesh_to_stone/cast_on_hand_hit(obj/item/melee/touch_attack/hand, atom/victim, mob/living/carbon/caster) - if(!isliving(victim)) - return FALSE +/datum/action/cooldown/spell/touch/flesh_to_stone/on_antimagic_triggered(obj/item/melee/touch_attack/hand, mob/living/victim, mob/living/carbon/caster) + to_chat(caster, span_warning("The spell can't seem to affect [victim]!")) + to_chat(victim, span_warning("You feel your flesh turn to stone for a moment, then revert back!")) +/datum/action/cooldown/spell/touch/flesh_to_stone/cast_on_hand_hit(obj/item/melee/touch_attack/hand, mob/living/victim, mob/living/carbon/caster) var/mob/living/living_victim = victim if(living_victim.can_block_magic(antimagic_flags)) - to_chat(caster, span_warning("The spell can't seem to affect [victim]!")) - to_chat(victim, span_warning("You feel your flesh turn to stone for a moment, then revert back!")) return TRUE living_victim.Stun(4 SECONDS) diff --git a/code/modules/spells/spell_types/touch/smite.dm b/code/modules/spells/spell_types/touch/smite.dm index 6a7c17aae5a..f31e844bfb9 100644 --- a/code/modules/spells/spell_types/touch/smite.dm +++ b/code/modules/spells/spell_types/touch/smite.dm @@ -14,26 +14,28 @@ hand_path = /obj/item/melee/touch_attack/smite -/datum/action/cooldown/spell/touch/smite/cast_on_hand_hit(obj/item/melee/touch_attack/hand, atom/victim, mob/living/carbon/caster) - if(!isliving(victim)) - return FALSE - +/// Smite is pretty extravagant, so whenever we get casted, we blind everyone nearby. +/datum/action/cooldown/spell/touch/smite/proc/blind_everyone_nearby(mob/living/victim, atom/center) do_sparks(sparks_amt, FALSE, get_turf(victim)) - for(var/mob/living/nearby_spectator in view(caster, 7)) - if(nearby_spectator == caster) + for(var/mob/living/nearby_spectator in view(center, 7)) + if(nearby_spectator == center) continue nearby_spectator.flash_act(affect_silicon = FALSE) - var/mob/living/living_victim = victim - if(living_victim.can_block_magic(antimagic_flags)) - caster.visible_message( - span_warning("The feedback blows [caster]'s arm off!"), - span_userdanger("The spell bounces from [living_victim]'s skin back into your arm!"), - ) - caster.flash_act() - var/obj/item/bodypart/to_dismember = caster.get_holding_bodypart_of_item(hand) - to_dismember?.dismember() - return TRUE +/datum/action/cooldown/spell/touch/smite/on_antimagic_triggered(obj/item/melee/touch_attack/hand, mob/living/victim, mob/living/carbon/caster) + caster.visible_message( + span_warning("The feedback blows [caster]'s arm off!"), + span_userdanger("The spell bounces from [victim]'s skin back into your arm!"), + ) + // Off goes the arm we were casting with! + var/obj/item/bodypart/to_dismember = caster.get_holding_bodypart_of_item(hand) + to_dismember?.dismember() + // And do the blind (us included) + caster.flash_act() + blind_everyone_nearby(caster, caster) + +/datum/action/cooldown/spell/touch/smite/cast_on_hand_hit(obj/item/melee/touch_attack/hand, mob/living/victim, mob/living/carbon/caster) + blind_everyone_nearby(victim, caster) if(ishuman(victim)) var/mob/living/carbon/human/human_victim = victim @@ -45,8 +47,8 @@ new /obj/effect/gibspawner(get_turf(victim)) return TRUE - living_victim.investigate_log("has been gibbed by the smite spell.", INVESTIGATE_DEATHS) - living_victim.gib() + victim.investigate_log("has been gibbed by the smite spell.", INVESTIGATE_DEATHS) + victim.gib() return TRUE /obj/item/melee/touch_attack/smite diff --git a/icons/hud/screen_alert.dmi b/icons/hud/screen_alert.dmi index 6292810e99e..051cc1bd686 100755 Binary files a/icons/hud/screen_alert.dmi and b/icons/hud/screen_alert.dmi differ diff --git a/strings/phobia.json b/strings/phobia.json index 7f176004621..841ef9bd9d3 100644 --- a/strings/phobia.json +++ b/strings/phobia.json @@ -191,12 +191,14 @@ "ghost", "god", "goddess", + "heretic", "lich", "magic", + "manusus", "nar nar", + "nar'sie", "nar-nar", "nar-sie", - "nar'sie", "nightmare", "ratvar", "religion", diff --git a/tgstation.dme b/tgstation.dme index e9c3b39ab8a..a2a1202bc42 100644 --- a/tgstation.dme +++ b/tgstation.dme @@ -18,6 +18,7 @@ #include "code\_compile_options.dm" #include "code\_debugger.dm" #include "code\world.dm" +#include "code\__DEFINES\_bitfields.dm" #include "code\__DEFINES\_click.dm" #include "code\__DEFINES\_globals.dm" #include "code\__DEFINES\_helpers.dm" @@ -40,7 +41,6 @@ #include "code\__DEFINES\art.dm" #include "code\__DEFINES\atom_hud.dm" #include "code\__DEFINES\basic_mobs.dm" -#include "code\__DEFINES\bitfields.dm" #include "code\__DEFINES\blackmarket.dm" #include "code\__DEFINES\blend_modes.dm" #include "code\__DEFINES\blob_defines.dm" @@ -2353,15 +2353,20 @@ #include "code\modules\antagonists\heretic\magic\eldritch_shapeshift.dm" #include "code\modules\antagonists\heretic\magic\eldritch_telepathy.dm" #include "code\modules\antagonists\heretic\magic\expand_sight.dm" +#include "code\modules\antagonists\heretic\magic\fire_blast.dm" #include "code\modules\antagonists\heretic\magic\flesh_ascension.dm" +#include "code\modules\antagonists\heretic\magic\flesh_surgery.dm" #include "code\modules\antagonists\heretic\magic\furious_steel.dm" #include "code\modules\antagonists\heretic\magic\madness_touch.dm" #include "code\modules\antagonists\heretic\magic\manse_link.dm" #include "code\modules\antagonists\heretic\magic\mansus_grasp.dm" #include "code\modules\antagonists\heretic\magic\mirror_walk.dm" #include "code\modules\antagonists\heretic\magic\nightwatcher_rebirth.dm" +#include "code\modules\antagonists\heretic\magic\realignment.dm" +#include "code\modules\antagonists\heretic\magic\rust_construction.dm" #include "code\modules\antagonists\heretic\magic\rust_wave.dm" #include "code\modules\antagonists\heretic\magic\shadow_cloak.dm" +#include "code\modules\antagonists\heretic\magic\void_cold_cone.dm" #include "code\modules\antagonists\heretic\magic\void_phase.dm" #include "code\modules\antagonists\heretic\magic\void_pull.dm" #include "code\modules\antagonists\heretic\mobs\maid_in_mirror.dm" @@ -4405,7 +4410,10 @@ #include "code\modules\spells\spell_types\aoe_spell\magic_missile.dm" #include "code\modules\spells\spell_types\aoe_spell\repulse.dm" #include "code\modules\spells\spell_types\aoe_spell\sacred_flame.dm" +#include "code\modules\spells\spell_types\charged\_charged.dm" +#include "code\modules\spells\spell_types\charged\tesla_blast.dm" #include "code\modules\spells\spell_types\cone\_cone.dm" +#include "code\modules\spells\spell_types\cone\cone_of_cold.dm" #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" @@ -4448,7 +4456,6 @@ #include "code\modules\spells\spell_types\self\disable_tech.dm" #include "code\modules\spells\spell_types\self\forcewall.dm" #include "code\modules\spells\spell_types\self\lichdom.dm" -#include "code\modules\spells\spell_types\self\lightning.dm" #include "code\modules\spells\spell_types\self\mime_vow.dm" #include "code\modules\spells\spell_types\self\mutate.dm" #include "code\modules\spells\spell_types\self\night_vision.dm" diff --git a/tgui/packages/tgui/interfaces/AntagInfoHeretic.tsx b/tgui/packages/tgui/interfaces/AntagInfoHeretic.tsx index fcc2e80ddec..56e0ecf00c5 100644 --- a/tgui/packages/tgui/interfaces/AntagInfoHeretic.tsx +++ b/tgui/packages/tgui/interfaces/AntagInfoHeretic.tsx @@ -136,12 +136,18 @@ const GuideSection = () => { - Follow your Living Heart to find your targets. Bring them back to a  - transmutation rune to  + transmutation rune in critical  + or worse condition to  sacrifice them for  knowledge points. The Mansus{' '} ONLY accepts targets pointed to by the  Living Heart. + + - Make yourself a focus to be  + able to cast various advanced spells to assist you in acquire  + harder and harder sacrifices. + - Accomplish all of your objectives to be able to learn the{' '} final ritual. Complete the ritual @@ -304,7 +310,7 @@ export const AntagInfoHeretic = (props, context) => { const [currentTab, setTab] = useLocalState(context, 'currentTab', 0); return ( - +