From 3a2bd5516d3ea61b75bc1f7f596a3200208d3619 Mon Sep 17 00:00:00 2001 From: MrMelbert <51863163+MrMelbert@users.noreply.github.com> Date: Fri, 4 Mar 2022 05:15:12 -0600 Subject: [PATCH] Overhauls heretic rituals slightly for better user feedback (and to help locate a bug) (#65223) --- code/__DEFINES/antagonists.dm | 8 + code/__HELPERS/cmp.dm | 4 + .../antagonists/heretic/heretic_antag.dm | 63 ++++++- .../antagonists/heretic/heretic_knowledge.dm | 58 +++++- .../heretic/heretic_living_heart.dm | 2 +- .../antagonists/heretic/knowledge/ash_lore.dm | 1 + .../heretic/knowledge/flesh_lore.dm | 3 + .../heretic/knowledge/general_side.dm | 16 +- .../heretic/knowledge/rust_lore.dm | 18 +- .../sacrifice_knowledge.dm | 48 ++--- .../heretic/knowledge/starting_lore.dm | 60 ++++-- .../heretic/knowledge/void_lore.dm | 5 + .../heretic/structures/mawed_crucible.dm | 2 +- .../antagonists/heretic/transmutation_rune.dm | 177 +++++++++--------- code/modules/unit_tests/heretic_rituals.dm | 6 +- 15 files changed, 312 insertions(+), 159 deletions(-) diff --git a/code/__DEFINES/antagonists.dm b/code/__DEFINES/antagonists.dm index e5f6ec2c8e6..1d5a4926ef8 100644 --- a/code/__DEFINES/antagonists.dm +++ b/code/__DEFINES/antagonists.dm @@ -70,6 +70,14 @@ #define PATH_FLESH "Flesh Path" #define PATH_VOID "Void Path" +/// Defines are used in /proc/has_living_heart() to report if the heretic has no heart period, no living heart, or has a living heart. +#define HERETIC_NO_HEART_ORGAN -1 +#define HERETIC_NO_LIVING_HEART 0 +#define HERETIC_HAS_LIVING_HEART 1 + +/// A define used in ritual priority for heretics. +#define MAX_KNOWLEDGE_PRIORITY 100 + /// Forces the blob to place the core where they currently are, ignoring any checks. #define BLOB_FORCE_PLACEMENT -1 /// Normal blob placement, does the regular checks to make sure the blob isn't placing itself in an invalid location diff --git a/code/__HELPERS/cmp.dm b/code/__HELPERS/cmp.dm index cc21e8535f9..b53694fc60e 100644 --- a/code/__HELPERS/cmp.dm +++ b/code/__HELPERS/cmp.dm @@ -157,3 +157,7 @@ GLOBAL_VAR_INIT(cmp_field, "name") rhs = ispath(B, /datum/reagent) ? 0 : 1 return lhs - rhs + +/// Orders heretic knowledge by priority +/proc/cmp_heretic_knowledge(datum/heretic_knowledge/knowledge_a, datum/heretic_knowledge/knowledge_b) + return initial(knowledge_b.priority) - initial(knowledge_a.priority) diff --git a/code/modules/antagonists/heretic/heretic_antag.dm b/code/modules/antagonists/heretic/heretic_antag.dm index 9746aa033ff..0b4adc0f2cd 100644 --- a/code/modules/antagonists/heretic/heretic_antag.dm +++ b/code/modules/antagonists/heretic/heretic_antag.dm @@ -75,9 +75,7 @@ // Final knowledge can't be learned until all objectives are complete. if(ispath(knowledge, /datum/heretic_knowledge/final)) - for(var/datum/objective/must_be_done as anything in objectives) - if(!must_be_done.check_completion()) - knowledge_data["disabled"] = TRUE + knowledge_data["disabled"] = !can_ascend() knowledge_data["hereticPath"] = initial(knowledge.route) knowledge_data["color"] = path_to_color[initial(knowledge.route)] || "grey" @@ -412,13 +410,12 @@ /datum/antagonist/heretic/get_admin_commands() . = ..() - var/obj/item/organ/our_living_heart = owner.current?.getorganslot(living_heart_organ_slot) - if(our_living_heart) - if(HAS_TRAIT(our_living_heart, TRAIT_LIVING_HEART)) + switch(has_living_heart()) + if(HERETIC_NO_LIVING_HEART) + .["Give Living Heart"] = CALLBACK(src, .proc/give_living_heart) + if(HERETIC_HAS_LIVING_HEART) .["Add Heart Target (Marked Mob)"] = CALLBACK(src, .proc/add_marked_as_target) .["Remove Heart Target"] = CALLBACK(src, .proc/remove_target) - else - .["Give Living Heart"] = CALLBACK(src, .proc/give_living_heart) .["Adjust Knowledge Points"] = CALLBACK(src, .proc/admin_change_points) @@ -562,6 +559,54 @@ /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. + * + * Returns an associated list of [knowledge name] to [knowledge datum] sorted by knowledge priority. + */ +/datum/antagonist/heretic/proc/get_rituals() + var/list/rituals = list() + + for(var/knowledge_index in researched_knowledge) + var/datum/heretic_knowledge/knowledge = researched_knowledge[knowledge_index] + if(!knowledge.can_be_invoked(src)) + continue + rituals[knowledge.name] = knowledge + + return sortTim(rituals, /proc/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. + */ +/datum/antagonist/heretic/proc/can_ascend() + for(var/datum/objective/must_be_done as anything in objectives) + if(!must_be_done.check_completion()) + 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 + * - Is missing the organ they need in the slot to make a living heart + * + * Returns HERETIC_NO_HEART_ORGAN if they have no heart (organ) at all, + * Returns HERETIC_NO_LIVING_HEART if they have a heart (organ) but it's not a living one, + * and returns HERETIC_HAS_LIVING_HEART if they have a living heart + */ +/datum/antagonist/heretic/proc/has_living_heart() + var/obj/item/organ/our_living_heart = owner.current?.getorganslot(living_heart_organ_slot) + if(!our_living_heart) + return HERETIC_NO_HEART_ORGAN + + if(!HAS_TRAIT(our_living_heart, TRAIT_LIVING_HEART)) + return HERETIC_NO_LIVING_HEART + + return HERETIC_HAS_LIVING_HEART + /// Heretic's minor sacrifice objective. "Minor sacrifices" includes anyone. /datum/objective/minor_sacrifice name = "minor sacrifice" @@ -648,7 +693,7 @@ num_we_have++ - return num_we_have >= target_amount + return completed || (num_we_have >= target_amount) /datum/outfit/heretic name = "Heretic (Preview only)" diff --git a/code/modules/antagonists/heretic/heretic_knowledge.dm b/code/modules/antagonists/heretic/heretic_knowledge.dm index 28fb4b1a13d..7caf7df613f 100644 --- a/code/modules/antagonists/heretic/heretic_knowledge.dm +++ b/code/modules/antagonists/heretic/heretic_knowledge.dm @@ -26,6 +26,9 @@ var/list/result_atoms = list() /// Cost of knowledge in knowlege points var/cost = 0 + /// The priority of the knowledge. Higher priority knowledge appear higher in the ritual list. + /// Number itself is completely arbitrary. Does not need to be set for non-ritual knowledge. + var/priority = 0 /// What path is this on. If set to "null", assumed to be unreachable (or abstract). var/route @@ -62,6 +65,15 @@ */ /datum/heretic_knowledge/proc/on_lose(mob/user) +/** + * Determines if a heretic can actually attempt to invoke the knowledge as a ritual. + * By default, we can only invoke knowledge with rituals associated. + * + * Return TRUE to have the ritual show up in the rituals list, FALSE otherwise. + */ +/datum/heretic_knowledge/proc/can_be_invoked(datum/antagonist/heretic/invoker) + return !!LAZYLEN(required_atoms) + /** * Special check for rituals. * Called before any of the required atoms are checked. @@ -181,7 +193,11 @@ if(QDELETED(real_thing)) LAZYREMOVE(created_items, ref) - return LAZYLEN(created_items) < limit + if(LAZYLEN(created_items) >= limit) + loc.balloon_alert(user, "ritual failed, at limit!") + return FALSE + + return TRUE /datum/heretic_knowledge/limited_amount/on_finished_recipe(mob/living/user, list/selected_atoms, turf/loc) for(var/result in result_atoms) @@ -204,7 +220,12 @@ fingerprints[requirements.return_fingerprints()] = 1 list_clear_nulls(fingerprints) - return length(fingerprints) // No fingerprints? No ritual + // No fingerprints? No ritual + if(!length(fingerprints)) + loc.balloon_alert(user, "ritual failed, no fingerprints!") + return FALSE + + return TRUE /datum/heretic_knowledge/curse/on_finished_recipe(mob/living/user, list/selected_atoms, turf/loc) @@ -237,11 +258,15 @@ * Calls a curse onto [chosen_mob]. */ /datum/heretic_knowledge/curse/proc/curse(mob/living/carbon/human/chosen_mob) + SHOULD_CALL_PARENT(FALSE) + CRASH("[type] did not implement curse()!") /** * Removes a curse from [chosen_mob]. Used in timers / callbacks. */ /datum/heretic_knowledge/curse/proc/uncurse(mob/living/carbon/human/chosen_mob) + SHOULD_CALL_PARENT(FALSE) + CRASH("[type] did not implement uncurse()!") /* * A knowledge subtype lets the heretic summon a monster with the ritual. @@ -295,6 +320,7 @@ desc = "A randomly generated transmutation ritual that rewards knowledge points and can only be completed once." gain_text = "Everything can be a key to unlocking the secrets behind the Gates. I must be wary and wise." cost = 1 + priority = MAX_KNOWLEDGE_PRIORITY - 10 // A pretty important midgame ritual. /// Whether we've done the ritual. Only doable once. var/was_completed = FALSE @@ -351,13 +377,16 @@ to_chat(user, span_hierophant("The [name] requires the following:")) for(var/obj/item/path as anything in required_atoms) var/amount_needed = required_atoms[path] - to_chat(user, span_hypnophrase("[amount_needed] [initial(path.name)][amount_needed == 1 ? "":"s"]...")) - requirements_string += "[amount_needed == 1 ? "":"[amount_needed] "][initial(path.name)][amount_needed == 1 ? "":"s"]" + to_chat(user, span_hypnophrase("[amount_needed] [initial(path.name)]\s...")) + requirements_string += "[amount_needed == 1 ? "":"[amount_needed] "][initial(path.name)]\s" to_chat(user, span_hierophant("Completing it will reward you [KNOWLEDGE_RITUAL_POINTS] knowledge points. You can check the knowledge in your Researched Knowledge to be reminded.")) desc = "Allows you to transmute [english_list(requirements_string)] for [KNOWLEDGE_RITUAL_POINTS] bonus knowledge points. This can only be completed once." +/datum/heretic_knowledge/knowledge_ritual/can_be_invoked(datum/antagonist/heretic/invoker) + return !was_completed + /datum/heretic_knowledge/knowledge_ritual/recipe_snowflake_check(mob/living/user, list/atoms, list/selected_atoms, turf/loc) return !was_completed @@ -370,6 +399,7 @@ to_chat(user, span_boldnotice("[name] completed!")) to_chat(user, span_hypnophrase(span_big("[drain_message]"))) desc += " (Completed!)" + log_heretic_knowledge("[key_name(user)] completed a [name] at [worldtime2text()].") return TRUE #undef KNOWLEDGE_RITUAL_POINTS @@ -379,6 +409,7 @@ */ /datum/heretic_knowledge/final cost = 2 + 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/final/on_research(mob/user) @@ -392,14 +423,19 @@ 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)") -/datum/heretic_knowledge/final/recipe_snowflake_check(mob/living/user, list/atoms, list/selected_atoms, turf/loc) - var/datum/antagonist/heretic/heretic_datum = IS_HERETIC(user) - if(heretic_datum.ascended) +/datum/heretic_knowledge/final/can_be_invoked(datum/antagonist/heretic/invoker) + if(invoker.ascended) return FALSE - for(var/datum/objective/must_be_done as anything in heretic_datum.objectives) - if(!must_be_done.check_completion()) - return FALSE + if(!invoker.can_ascend()) + return FALSE + + return TRUE + +/datum/heretic_knowledge/final/recipe_snowflake_check(mob/living/user, list/atoms, list/selected_atoms, turf/loc) + var/datum/antagonist/heretic/heretic_datum = IS_HERETIC(user) + if(!can_be_invoked(heretic_datum)) + return FALSE // Remove all non-dead humans from the atoms list. // (We only want to sacrifice dead folk.) @@ -426,6 +462,8 @@ human_user.physiology.brute_mod *= 0.5 human_user.physiology.burn_mod *= 0.5 + + log_heretic_knowledge("[key_name(user)] completed their final ritual at [worldtime2text()].") return TRUE /datum/heretic_knowledge/final/cleanup_atoms(list/selected_atoms) diff --git a/code/modules/antagonists/heretic/heretic_living_heart.dm b/code/modules/antagonists/heretic/heretic_living_heart.dm index e9e61e99128..0019e276506 100644 --- a/code/modules/antagonists/heretic/heretic_living_heart.dm +++ b/code/modules/antagonists/heretic/heretic_living_heart.dm @@ -22,7 +22,7 @@ return COMPONENT_INCOMPATIBLE var/obj/item/organ/organ_parent = parent - if(organ_parent.status != ORGAN_ORGANIC) + if(organ_parent.status != ORGAN_ORGANIC || (organ_parent.organ_flags & ORGAN_SYNTHETIC)) return COMPONENT_INCOMPATIBLE if(!IS_HERETIC(organ_parent.owner)) diff --git a/code/modules/antagonists/heretic/knowledge/ash_lore.dm b/code/modules/antagonists/heretic/knowledge/ash_lore.dm index 5c66a0e7d32..695897db13f 100644 --- a/code/modules/antagonists/heretic/knowledge/ash_lore.dm +++ b/code/modules/antagonists/heretic/knowledge/ash_lore.dm @@ -46,6 +46,7 @@ result_atoms = list(/obj/item/melee/sickly_blade/ash) limit = 2 cost = 1 + priority = MAX_KNOWLEDGE_PRIORITY - 5 route = PATH_ASH /datum/heretic_knowledge/limited_amount/base_ash/on_research(mob/user) diff --git a/code/modules/antagonists/heretic/knowledge/flesh_lore.dm b/code/modules/antagonists/heretic/knowledge/flesh_lore.dm index e5af4b4630a..6516065556c 100644 --- a/code/modules/antagonists/heretic/knowledge/flesh_lore.dm +++ b/code/modules/antagonists/heretic/knowledge/flesh_lore.dm @@ -51,6 +51,7 @@ result_atoms = list(/obj/item/melee/sickly_blade/flesh) limit = 3 // Bumped up so they can arm up their ghouls too. cost = 1 + priority = MAX_KNOWLEDGE_PRIORITY - 5 route = PATH_FLESH /datum/heretic_knowledge/limited_amount/base_flesh/on_research(mob/user) @@ -157,6 +158,7 @@ atoms -= body if(!(locate(/mob/living/carbon/human) in atoms)) + loc.balloon_alert(user, "ritual failed, no valid body!") return FALSE return ..() @@ -165,6 +167,7 @@ var/mob/living/carbon/human/soon_to_be_ghoul = locate() in selected_atoms if(QDELETED(soon_to_be_ghoul)) // No body? No ritual stack_trace("[type] reached on_finished_recipe without a human in selected_atoms to make a ghoul out of.") + loc.balloon_alert(user, "ritual failed, no valid body!") return FALSE soon_to_be_ghoul.grab_ghost() diff --git a/code/modules/antagonists/heretic/knowledge/general_side.dm b/code/modules/antagonists/heretic/knowledge/general_side.dm index e60e6d9de37..e56af034337 100644 --- a/code/modules/antagonists/heretic/knowledge/general_side.dm +++ b/code/modules/antagonists/heretic/knowledge/general_side.dm @@ -9,7 +9,6 @@ /obj/item/food/grown/harebell = 1, /obj/item/book = 1, /obj/item/clothing/under = 1, - /mob/living/carbon/human = 1, ) cost = 1 route = PATH_SIDE @@ -17,15 +16,14 @@ /datum/heretic_knowledge/reroll_targets/recipe_snowflake_check(mob/living/user, list/atoms, list/selected_atoms, turf/loc) var/datum/antagonist/heretic/heretic_datum = IS_HERETIC(user) - var/obj/item/organ/our_living_heart = user.getorganslot(heretic_datum.living_heart_organ_slot) - if(!our_living_heart || !HAS_TRAIT(our_living_heart, TRAIT_LIVING_HEART)) + // Check first if they have a Living Heart. If it's missing, we should + // throw a fail to show the heretic that there's no point in rerolling + // if you don't have a heart to track the targets in the first place. + if(heretic_datum.has_living_heart() != HERETIC_HAS_LIVING_HEART) + loc.balloon_alert(user, "ritual failed, no living heart!") return FALSE - if(!LAZYLEN(heretic_datum.sac_targets)) - return FALSE - - atoms += user - return (user in range(1, loc)) + return TRUE /datum/heretic_knowledge/reroll_targets/on_finished_recipe(mob/living/user, list/selected_atoms, turf/loc) var/datum/antagonist/heretic/heretic_datum = IS_HERETIC(user) @@ -36,7 +34,7 @@ CRASH("Heretic datum didn't have a hunt_and_sacrifice knowledge learned, what?") if(!target_finder.obtain_targets(user)) - loc.balloon_alert(user, "ritual failed, no targets!") + loc.balloon_alert(user, "ritual failed, no targets found!") return FALSE return TRUE diff --git a/code/modules/antagonists/heretic/knowledge/rust_lore.dm b/code/modules/antagonists/heretic/knowledge/rust_lore.dm index 86fa5a82e87..a80368bc568 100644 --- a/code/modules/antagonists/heretic/knowledge/rust_lore.dm +++ b/code/modules/antagonists/heretic/knowledge/rust_lore.dm @@ -46,6 +46,7 @@ result_atoms = list(/obj/item/melee/sickly_blade/rust) limit = 2 cost = 1 + priority = MAX_KNOWLEDGE_PRIORITY - 5 route = PATH_RUST /datum/heretic_knowledge/limited_amount/base_rust/on_research(mob/user) @@ -252,7 +253,7 @@ /// If TRUE, then immunities are currently active. var/immunities_active = FALSE /// A typepath to an area that we must finish the ritual in. - var/ritual_location = /area/command/bridge + var/area/ritual_location = /area/command/bridge /// A static list of traits we give to the heretic when on rust. var/static/list/conditional_immunities = list( TRAIT_STUNIMMUNE, @@ -270,10 +271,19 @@ TRAIT_NOBREATH, ) +/datum/heretic_knowledge/final/rust_final/on_research(mob/user) + . = ..() + // This map doesn't have a Bridge, for some reason?? + // Let them complete the ritual anywhere + if(!GLOB.areas_by_type[ritual_location]) + ritual_location = null + /datum/heretic_knowledge/final/rust_final/recipe_snowflake_check(mob/living/user, list/atoms, list/selected_atoms, turf/loc) - var/area/our_area = get_area(loc) - if(!istype(our_area, ritual_location)) - return FALSE + if(ritual_location) + var/area/our_area = get_area(loc) + if(!istype(our_area, ritual_location)) + loc.balloon_alert(user, "ritual failed, must be in [initial(ritual_location.name)]!") // "must be in bridge" + return FALSE return ..() 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 f5414b7e0e6..53db0c5a14f 100644 --- a/code/modules/antagonists/heretic/knowledge/sacrifice_knowledge/sacrifice_knowledge.dm +++ b/code/modules/antagonists/heretic/knowledge/sacrifice_knowledge/sacrifice_knowledge.dm @@ -14,11 +14,10 @@ If you have no targets, stand on a transmutation rune and invoke it to aquire some." required_atoms = list(/mob/living/carbon/human = 1) cost = 0 + priority = MAX_KNOWLEDGE_PRIORITY // Should be at the top route = PATH_START /// Whether we've generated a heretic sacrifice z-level yet, from any heretic. var/static/heretic_level_generated = FALSE - /// If TRUE, we skip the ritual when our target list is empty. Done to avoid locking up the heretic. - var/skip_this_ritual = FALSE /// A weakref to the mind of our heretic. var/datum/mind/heretic_mind /// Lazylist of minds that we won't pick as targets. @@ -48,38 +47,45 @@ 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) - // We've got no targets set, let's try to set some. Adds the user to the list of atoms, - // then returns TRUE if skip_this_ritual is FALSE and the user's on top of the rune. - // If skip_this_ritual is TRUE, returns FALSE to fail the check and move onto the next ritual. var/datum/antagonist/heretic/heretic_datum = IS_HERETIC(user) - var/obj/item/organ/our_living_heart = user.getorganslot(heretic_datum.living_heart_organ_slot) - if(!our_living_heart || !HAS_TRAIT(our_living_heart, TRAIT_LIVING_HEART)) + // First we have to check if the heretic has a Living Heart. + // You may wonder why we don't straight up prevent them from invoking the ritual if they don't have one - + // Hunt and sacrifice should always be invokable for clarity's sake, even if it'll fail immediately. + if(heretic_datum.has_living_heart() != HERETIC_HAS_LIVING_HEART) + loc.balloon_alert(user, "ritual failed, no living heart!") return FALSE + // We've got no targets set, let's try to set some. + // If we recently failed to aquire targets, we will be unable to aquire any. if(!LAZYLEN(heretic_datum.sac_targets)) - if(skip_this_ritual) - return FALSE - atoms += user - return (user in range(1, loc)) + return TRUE - // Determine if livings in our atoms are valid + // If we have targets, we can check to see if we can do a sacrifice + // Let's remove any humans in our atoms list that aren't a sac target for(var/mob/living/carbon/human/sacrifice in atoms) // If the mob's not in soft crit or worse, or isn't one of the sacrifices, remove it from the list if(sacrifice.stat < SOFT_CRIT || !(WEAKREF(sacrifice) in heretic_datum.sac_targets)) atoms -= sacrifice - // Finally, return TRUE if we have a mob remaining in our list - // Otherwise, return FALSE and stop the ritual - return !!(locate(/mob/living/carbon/human) in atoms) + // Finally, return TRUE if we have a target in the list + if(locate(/mob/living/carbon/human) in atoms) + return TRUE + + // or FALSE if we don't + loc.balloon_alert(user, "ritual failed, no sacrifice found!") + return FALSE /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)) - sacrifice_process(user, selected_atoms, loc) - else - obtain_targets(user) + if(!LAZYLEN(heretic_datum.sac_targets)) + if(obtain_targets(user)) + return TRUE + else + loc.balloon_alert(user, "ritual failed, no targets found!") + return FALSE + sacrifice_process(user, selected_atoms, loc) return TRUE /** @@ -106,9 +112,7 @@ if(!length(valid_targets)) if(!silent) - to_chat(user, span_danger("No sacrifice targets could be found! Attempt the ritual later.")) - skip_this_ritual = TRUE - addtimer(VARSET_CALLBACK(src, skip_this_ritual, FALSE), 5 MINUTES) + to_chat(user, span_hierophant_warning("No sacrifice targets could be found!")) return FALSE // Now, let's try to get four targets. diff --git a/code/modules/antagonists/heretic/knowledge/starting_lore.dm b/code/modules/antagonists/heretic/knowledge/starting_lore.dm index 58cd30a60ae..8c1a8e80494 100644 --- a/code/modules/antagonists/heretic/knowledge/starting_lore.dm +++ b/code/modules/antagonists/heretic/knowledge/starting_lore.dm @@ -48,6 +48,7 @@ GLOBAL_LIST_INIT(heretic_start_knowledge, initialize_starting_knowledge()) /obj/item/food/grown/poppy = 1, ) cost = 0 + priority = MAX_KNOWLEDGE_PRIORITY - 1 // Knowing how to remake your heart is important route = PATH_START /// The typepath of the organ type required for our heart. var/required_organ_type = /obj/item/organ/heart @@ -93,45 +94,75 @@ GLOBAL_LIST_INIT(heretic_start_knowledge, initialize_starting_knowledge()) if(our_living_heart) qdel(our_living_heart.GetComponent(/datum/component/living_heart)) +// Don't bother letting them invoke this ritual if they have a Living Heart already in their chest +/datum/heretic_knowledge/living_heart/can_be_invoked(datum/antagonist/heretic/invoker) + if(invoker.has_living_heart() == HERETIC_HAS_LIVING_HEART) + return FALSE + return TRUE + /datum/heretic_knowledge/living_heart/recipe_snowflake_check(mob/living/user, list/atoms, list/selected_atoms, turf/loc) var/datum/antagonist/heretic/our_heretic = IS_HERETIC(user) var/obj/item/organ/our_living_heart = user.getorganslot(our_heretic.living_heart_organ_slot) - if(!our_living_heart || HAS_TRAIT(our_living_heart, TRAIT_LIVING_HEART)) + // Obviously you need a heart in your chest to do a ritual on your... heart + if(!our_living_heart) + loc.balloon_alert(user, "ritual failed, you have no [our_heretic.living_heart_organ_slot]!") // "you have no heart!" + return FALSE + // For sanity's sake, check if they've got a heart - + // even though it's not invokable if you already have one, + // they may have gained one unexpectantly in between now and then + if(HAS_TRAIT(our_living_heart, TRAIT_LIVING_HEART)) + loc.balloon_alert(user, "ritual failed, already have a living heart!") return FALSE - if(our_living_heart.status == ORGAN_ORGANIC) + // By this point they are making a new heart + // If their current heart is organic / not synthetic, we can continue the ritual as normal + if(our_living_heart.status == ORGAN_ORGANIC && !(our_living_heart.organ_flags & ORGAN_SYNTHETIC)) return TRUE - else - for(var/obj/item/organ/nearby_organ in atoms) - if(!istype(nearby_organ, required_organ_type)) - continue - - if(nearby_organ.status == ORGAN_ORGANIC && nearby_organ.useable) - selected_atoms += nearby_organ - return TRUE - + // If their current heart is not organic / is synthetic, they need an organic replacement + // ...But if our organ-to-be-replaced is unremovable, we're screwed + if(our_living_heart.organ_flags & ORGAN_UNREMOVABLE) + loc.balloon_alert(user, "ritual failed, [our_heretic.living_heart_organ_slot] unremovable!") // "heart unremovable!" return FALSE + // Otherwise, seek out a replacement in our atoms + for(var/obj/item/organ/nearby_organ in atoms) + if(!istype(nearby_organ, required_organ_type)) + continue + if(!nearby_organ.useable) + continue + if(nearby_organ.status != ORGAN_ORGANIC || (nearby_organ.organ_flags & (ORGAN_SYNTHETIC|ORGAN_FAILING))) + continue + + selected_atoms += nearby_organ + return TRUE + + loc.balloon_alert(user, "ritual failed, need a replacement [our_heretic.living_heart_organ_slot]!") // "need a replacement heart!" + return FALSE /datum/heretic_knowledge/living_heart/on_finished_recipe(mob/living/user, list/selected_atoms, turf/loc) var/datum/antagonist/heretic/our_heretic = IS_HERETIC(user) var/obj/item/organ/our_new_heart = user.getorganslot(our_heretic.living_heart_organ_slot) - if(our_new_heart.status != ORGAN_ORGANIC) + // Our heart is robotic or synthetic - we need to replace it, and we fortunately should have one by here + if(our_new_heart.status != ORGAN_ORGANIC || (our_new_heart.organ_flags & ORGAN_SYNTHETIC)) var/obj/item/organ/our_replacement_heart = locate(required_organ_type) in selected_atoms if(our_replacement_heart) - user.visible_message("[user]'s [our_replacement_heart.name] bursts suddenly out of [user.p_their()] chest!") + // Throw our current heart out of our chest, violently + user.visible_message(span_boldwarning("[user]'s [our_new_heart.name] bursts suddenly out of [user.p_their()] chest!")) INVOKE_ASYNC(user, /mob/proc/emote, "scream") user.apply_damage(20, BRUTE, BODY_ZONE_CHEST) - + // And put our organic heart in its place our_replacement_heart.Insert(user, TRUE, TRUE) our_new_heart.throw_at(get_edge_target_turf(user, pick(GLOB.alldirs)), 2, 2) our_new_heart = our_replacement_heart + else + CRASH("[type] required a replacement organic heart in on_finished_recipe, but did not find one.") if(!our_new_heart) CRASH("[type] somehow made it to on_finished_recipe without a heart. What?") + // Don't delete our shiny new heart if(our_new_heart in selected_atoms) selected_atoms -= our_new_heart @@ -154,4 +185,5 @@ GLOBAL_LIST_INIT(heretic_start_knowledge, initialize_starting_knowledge()) ) result_atoms = list(/obj/item/clothing/neck/heretic_focus) cost = 0 + priority = MAX_KNOWLEDGE_PRIORITY - 2 // Not as important as making a heart or sacrificing, but important enough. route = PATH_START diff --git a/code/modules/antagonists/heretic/knowledge/void_lore.dm b/code/modules/antagonists/heretic/knowledge/void_lore.dm index bafe83bdfa6..3d6ddb15477 100644 --- a/code/modules/antagonists/heretic/knowledge/void_lore.dm +++ b/code/modules/antagonists/heretic/knowledge/void_lore.dm @@ -44,6 +44,7 @@ result_atoms = list(/obj/item/melee/sickly_blade/void) limit = 2 cost = 1 + priority = MAX_KNOWLEDGE_PRIORITY - 5 route = PATH_VOID /datum/heretic_knowledge/limited_amount/base_void/on_research(mob/user) @@ -53,10 +54,12 @@ /datum/heretic_knowledge/limited_amount/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!") return FALSE var/turf/open/our_turf = loc if(our_turf.GetTemperature() > T0C) + loc.balloon_alert(user, "ritual failed, not cold enough!") return FALSE return ..() @@ -239,10 +242,12 @@ /datum/heretic_knowledge/final/void_final/recipe_snowflake_check(mob/living/user, list/atoms, list/selected_atoms, turf/loc) if(!isopenturf(loc)) + loc.balloon_alert(user, "ritual failed, invalid location!") return FALSE var/turf/open/our_turf = loc if(our_turf.GetTemperature() > T0C) + loc.balloon_alert(user, "ritual failed, not cold enough!") return FALSE return ..() diff --git a/code/modules/antagonists/heretic/structures/mawed_crucible.dm b/code/modules/antagonists/heretic/structures/mawed_crucible.dm index bb500b3b7e8..32311eefada 100644 --- a/code/modules/antagonists/heretic/structures/mawed_crucible.dm +++ b/code/modules/antagonists/heretic/structures/mawed_crucible.dm @@ -84,7 +84,7 @@ if(istype(weapon, /obj/item/organ)) var/obj/item/organ/consumed = weapon - if(consumed.status != ORGAN_ORGANIC) + if(consumed.status != ORGAN_ORGANIC || (consumed.organ_flags & ORGAN_SYNTHETIC)) balloon_alert(user, "not organic!") return if(consumed.organ_flags & ORGAN_VITAL) // Basically, don't eat organs like brains diff --git a/code/modules/antagonists/heretic/transmutation_rune.dm b/code/modules/antagonists/heretic/transmutation_rune.dm index 4029296ffb7..a99df7f0d32 100644 --- a/code/modules/antagonists/heretic/transmutation_rune.dm +++ b/code/modules/antagonists/heretic/transmutation_rune.dm @@ -40,15 +40,25 @@ return TRUE /** - * Wrapper for do_rituals to ensure is_in_use - * is enabled and disabled before and after. + * Attempt to begin a ritual, giving them an input list to chose from. + * Also ensures is_in_use is enabled and disabled before and after. */ /obj/effect/heretic_rune/proc/try_rituals(mob/living/user) is_in_use = TRUE var/datum/antagonist/heretic/heretic_datum = IS_HERETIC(user) - do_rituals(user, flatten_list(heretic_datum.researched_knowledge)) + var/list/rituals = heretic_datum.get_rituals() + if(!length(rituals)) + loc.balloon_alert(user, "no rituals available!") + is_in_use = FALSE + return + var/chosen = tgui_input_list(user, "Chose a ritual to attempt.", "Chose a Ritual", rituals) + if(!chosen || !istype(rituals[chosen], /datum/heretic_knowledge) || QDELETED(src) || QDELETED(user) || QDELETED(heretic_datum)) + is_in_use = FALSE + return + + do_ritual(user, rituals[chosen]) is_in_use = FALSE /** @@ -60,9 +70,7 @@ * * returns TRUE if any rituals passed succeeded, FALSE if they all failed. */ -/obj/effect/heretic_rune/proc/do_rituals(mob/living/user, list/knowledge_list) - if(!length(knowledge_list)) - CRASH("[type] do_rituals called without any passed knowledge!") +/obj/effect/heretic_rune/proc/do_ritual(mob/living/user, datum/heretic_knowledge/ritual) // Collect all nearby valid atoms over the rune for processing in rituals. var/list/atom/movable/atoms_in_range = list() @@ -76,101 +84,98 @@ atoms_in_range += close_atom - // Now let's go through all of our knowledge and try to do some magic. - for(var/datum/heretic_knowledge/knowledge as anything in knowledge_list) + // A copy of our requirements list. + // We decrement the values of to determine if enough of each key is present. + var/list/requirements_list = ritual.required_atoms.Copy() + // A list of all atoms we've selected to use in this recipe. + var/list/selected_atoms = list() - // It's not a ritual, we don't care. - if(!LAZYLEN(knowledge.required_atoms)) - continue + // Do the snowflake check to see if we can continue or not. + // selected_atoms is passed and can be modified by this proc. + if(!ritual.recipe_snowflake_check(user, atoms_in_range, selected_atoms, loc)) + return FALSE - // A copy of our requirements list. - // We decrement the values of to determine if enough of each key is present. - var/list/requirements_list = knowledge.required_atoms.Copy() - // A list of all atoms we've selected to use in this recipe. - var/list/selected_atoms = list() - - // Do the snowflake check to see if we can continue or not. - // selected_atoms is passed and can be modified by this proc. - if(!knowledge.recipe_snowflake_check(user, atoms_in_range, selected_atoms, loc)) - continue - - // Now go through all our nearby atoms and see which are good for our ritual. - for(var/atom/nearby_atom as anything in atoms_in_range) - // Go through all of our required atoms - for(var/req_type in requirements_list) - // We already have enough of this type, skip - if(requirements_list[req_type] <= 0) - continue - if(!istype(nearby_atom, req_type)) - continue - - // This item is a valid type. Add it to our selected atoms list. - selected_atoms |= nearby_atom - // If it's a stack, we gotta see if it has more than one inside, - // as our requirements may want more than one item of a stack - if(isstack(nearby_atom)) - var/obj/item/stack/picked_stack = nearby_atom - requirements_list[req_type] -= picked_stack.amount // Can go negative, but doesn't matter. Negative = fulfilled - - // Otherwise, just add the mark down the item as fulfilled x1 - else - requirements_list[req_type]-- - - // All of the atoms have been checked, let's see if the ritual was successful - var/requirements_fulfilled = TRUE + // Now go through all our nearby atoms and see which are good for our ritual. + for(var/atom/nearby_atom as anything in atoms_in_range) + // Go through all of our required atoms for(var/req_type in requirements_list) + // We already have enough of this type, skip if(requirements_list[req_type] <= 0) continue + if(!istype(nearby_atom, req_type)) + continue - // One if our requirements wasn't entirely filled - // This ritual failed, move on to the next one - requirements_fulfilled = FALSE - break + // This item is a valid type. Add it to our selected atoms list. + selected_atoms |= nearby_atom + // If it's a stack, we gotta see if it has more than one inside, + // as our requirements may want more than one item of a stack + if(isstack(nearby_atom)) + var/obj/item/stack/picked_stack = nearby_atom + requirements_list[req_type] -= picked_stack.amount // Can go negative, but doesn't matter. Negative = fulfilled - // We didn't find enough for this ritual. Move onto the next one - if(!requirements_fulfilled) + // Otherwise, just add the mark down the item as fulfilled x1 + else + requirements_list[req_type]-- + + // All of the atoms have been checked, let's see if the ritual was successful + var/list/what_are_we_missing = list() + for(var/atom/req_type as anything in requirements_list) + var/number_of_things = requirements_list[req_type] + // <= 0 means it's fulfilled, skip + if(number_of_things <= 0) continue - // If we made it here, the ritual had all necessary components, and we can try to cast it. - // This doesn't necessarily mean the ritual will succeed, but it's valid! - // Do the animations and associated feedback. - flick("[icon_state]_active", src) - playsound(user, 'sound/magic/castsummon.ogg', 75, TRUE, extrarange = SILENCED_SOUND_EXTRARANGE, falloff_exponent = 10) + // > 0 means it's unfilfilled - the ritual has failed, we should tell them why + // Lets format the thing they're missing and put it into our list + var/formatted_thing = "[number_of_things] [initial(req_type.name)]\s" + if(ispath(req_type, /mob/living/carbon/human)) + // If we need a human, there is a high likelihood we actually need a (dead) body + formatted_thing = "[number_of_things] [number_of_things > 1 ? "bodies":"body"]" - // - We temporarily make all of our chosen atoms invisible, as some rituals may sleep, - // and we don't want people to be able to run off with ritual items. - // - We make a duplicate list here to ensure that all atoms are correctly un-invisibled by the end. - // Some rituals may remove atoms from the selected_atoms list, and not consume them. - var/list/initial_selected_atoms = selected_atoms.Copy() - for(var/atom/to_disappear as anything in selected_atoms) - to_disappear.invisibility = INVISIBILITY_ABSTRACT + what_are_we_missing += formatted_thing - // All the components have been invisibled, time to actually do the ritual. Call on_finished_recipe - // (Note: on_finished_recipe may sleep in the case of some rituals like summons, which expect ghost candidates.) - // - If the ritual was success (Returned TRUE), proceede to clean up the atoms involved in the ritual. The result has already been spawned by this point. - // - If the ritual failed for some reason (Returned FALSE), likely due to no ghosts taking a role or an error, we shouldn't clean up anything, and reset. - var/ritual_result = knowledge.on_finished_recipe(user, selected_atoms, loc) - if(ritual_result) - knowledge.cleanup_atoms(selected_atoms) + if(length(what_are_we_missing)) + // Let them know it screwed up + loc.balloon_alert(user, "ritual failed, missing components!") + // Then let them know what they're missing + to_chat(user, span_hierophant_warning("You are missing [english_list(what_are_we_missing)] in order to complete the ritual \"[ritual.name]\".")) + return FALSE - // Clean up done, re-appear anything that hasn't been deleted. - for(var/atom/to_appear as anything in initial_selected_atoms) - if(QDELETED(to_appear)) - continue - to_appear.invisibility = initial(to_appear.invisibility) + // If we made it here, the ritual had all necessary components, and we can try to cast it. + // This doesn't necessarily mean the ritual will succeed, but it's valid! + // Do the animations and associated feedback. + flick("[icon_state]_active", src) + playsound(user, 'sound/magic/castsummon.ogg', 75, TRUE, extrarange = SILENCED_SOUND_EXTRARANGE, falloff_exponent = 10) - // And finally, give some user feedback - // No feedback is given on failure here - - // the ritual itself should handle it (providing specifics as to why it failed) - if(ritual_result) - loc.balloon_alert(user, "ritual complete") + // - We temporarily make all of our chosen atoms invisible, as some rituals may sleep, + // and we don't want people to be able to run off with ritual items. + // - We make a duplicate list here to ensure that all atoms are correctly un-invisibled by the end. + // Some rituals may remove atoms from the selected_atoms list, and not consume them. + var/list/initial_selected_atoms = selected_atoms.Copy() + for(var/atom/to_disappear as anything in selected_atoms) + to_disappear.invisibility = INVISIBILITY_ABSTRACT - return ritual_result + // All the components have been invisibled, time to actually do the ritual. Call on_finished_recipe + // (Note: on_finished_recipe may sleep in the case of some rituals like summons, which expect ghost candidates.) + // - If the ritual was success (Returned TRUE), proceede to clean up the atoms involved in the ritual. The result has already been spawned by this point. + // - If the ritual failed for some reason (Returned FALSE), likely due to no ghosts taking a role or an error, we shouldn't clean up anything, and reset. + var/ritual_result = ritual.on_finished_recipe(user, selected_atoms, loc) + if(ritual_result) + ritual.cleanup_atoms(selected_atoms) - // General "fail" case. Only reached if we looped through all knowledges researched and not a single one had all components necessary. - // (Note: if a ritual fails despite having all components necessary, it will not be handled here - instead, it will above, see "ritual_result".) - loc.balloon_alert(user, "ritual failed!") - return FALSE + // Clean up done, re-appear anything that hasn't been deleted. + for(var/atom/to_appear as anything in initial_selected_atoms) + if(QDELETED(to_appear)) + continue + to_appear.invisibility = initial(to_appear.invisibility) + + // And finally, give some user feedback + // No feedback is given on failure here - + // the ritual itself should handle it (providing specifics as to why it failed) + if(ritual_result) + loc.balloon_alert(user, "ritual complete") + + return ritual_result /// A 3x3 heretic rune. The kind heretics actually draw in game. /obj/effect/heretic_rune/big diff --git a/code/modules/unit_tests/heretic_rituals.dm b/code/modules/unit_tests/heretic_rituals.dm index 761c36beeec..2440e4f4aaf 100644 --- a/code/modules/unit_tests/heretic_rituals.dm +++ b/code/modules/unit_tests/heretic_rituals.dm @@ -68,9 +68,9 @@ // Now, we can ACTUALLY run the ritual. Let's do it. // Attempt to run the knowledge via the sacrifice rune. - // If do_rituals() returns FALSE with our knowledge, it messed up. - // If do_rituals() returns TRUE, then it was successful. - if(!our_rune.do_rituals(our_heretic, list(knowledge))) + // If do_ritual() returns FALSE with our knowledge, it messed up. + // If do_ritual() returns TRUE, then it was successful. + if(!our_rune.do_ritual(our_heretic, knowledge)) // We failed. The knowledge should have everything to succeed, yet it returned FALSE! // Clean up the atoms it was meant to consume, so we can keep testing. for(var/atom/leftover as anything in created_atoms)