diff --git a/code/__DEFINES/antagonists.dm b/code/__DEFINES/antagonists.dm index 428cfae8239d9..7f82faf40d0eb 100644 --- a/code/__DEFINES/antagonists.dm +++ b/code/__DEFINES/antagonists.dm @@ -102,6 +102,9 @@ /// A define used in ritual priority for heretics. #define MAX_KNOWLEDGE_PRIORITY 100 +/// The maximum (and optimal) number of sacrifice targets a heretic should roll. +#define HERETIC_MAX_SAC_TARGETS 4 + /// How much does it cost to reroll strains? #define BLOB_REROLL_COST 40 diff --git a/code/__HELPERS/mobs.dm b/code/__HELPERS/mobs.dm index fd7679d468711..893fc6f3d00c6 100644 --- a/code/__HELPERS/mobs.dm +++ b/code/__HELPERS/mobs.dm @@ -758,6 +758,22 @@ GLOBAL_DATUM_INIT(dview_mob, /mob/dview, new) . = orange(distance,center) return +/** + * Gets the mind from a variable, whether it be a mob, or a mind itself. + * If [include_last] is true, then it will also return last_mind for carbons if there isn't a current mind. + */ +/proc/get_mind(target, include_last = FALSE) + if(istype(target, /datum/mind)) + return target + if(ismob(target)) + var/mob/mob_target = target + if(!QDELETED(mob_target.mind)) + return mob_target.mind + if(include_last && iscarbon(mob_target)) + var/mob/living/carbon/carbon_target = mob_target + if(!QDELETED(carbon_target.last_mind)) + return carbon_target.last_mind + #undef FACING_SAME_DIR #undef FACING_EACHOTHER #undef FACING_INIT_FACING_TARGET_TARGET_FACING_PERPENDICULAR diff --git a/code/__HELPERS/turfs.dm b/code/__HELPERS/turfs.dm index a1875bde3727d..1b900a65ac0a1 100644 --- a/code/__HELPERS/turfs.dm +++ b/code/__HELPERS/turfs.dm @@ -369,3 +369,26 @@ Turf and target are separate in case you want to teleport some distance from a t if(rail.dir == test_dir || is_fulltile) return FALSE return TRUE + +/proc/is_turf_safe(turf/open/floor/floor) + // It's probably not safe if it's not a floor. + if(!istype(floor)) + return FALSE + var/datum/gas_mixture/air = floor.air + // Certainly unsafe if it completely lacks air. + if(QDELETED(air)) + return FALSE + // Can most things breathe? + for(var/id in air.get_gases()) + if(id in GLOB.hardcoded_gases) + continue + return FALSE + if(air.get_moles(GAS_O2) < 16 || air.get_moles(GAS_PLASMA) || air.get_moles(GAS_CO2) >= 10) + return FALSE + var/temperature = air.return_temperature() + if(temperature <= 270 || temperature >= 360) + return FALSE + var/pressure = air.return_pressure() + if(pressure <= 20 || pressure >= 550) + return FALSE + return TRUE diff --git a/code/datums/helper_datums/teleport.dm b/code/datums/helper_datums/teleport.dm index e12efe3ec4363..4a5b3d4225629 100644 --- a/code/datums/helper_datums/teleport.dm +++ b/code/datums/helper_datums/teleport.dm @@ -137,32 +137,7 @@ if(!isfloorturf(random_location)) continue var/turf/open/floor/F = random_location - if(!F.air) - continue - - var/datum/gas_mixture/A = F.air - var/trace_gases - for(var/id in A.get_gases()) - if(id in GLOB.hardcoded_gases) - continue - trace_gases = TRUE - break - - // Can most things breathe? - if(trace_gases) - continue - if(A.get_moles(GAS_O2) < 16) - continue - if(A.get_moles(GAS_PLASMA)) - continue - if(A.get_moles(GAS_CO2) >= 10) - continue - - // Aim for goldilocks temperatures and pressure - if((A.return_temperature() <= 270) || (A.return_temperature() >= 360)) - continue - var/pressure = A.return_pressure() - if((pressure <= 20) || (pressure >= 550)) + if(!is_turf_safe(F)) continue if(extended_safety_checks) diff --git a/code/modules/antagonists/heretic/heretic_antag.dm b/code/modules/antagonists/heretic/heretic_antag.dm index fddceb7f3869d..b3d49c66426a7 100644 --- a/code/modules/antagonists/heretic/heretic_antag.dm +++ b/code/modules/antagonists/heretic/heretic_antag.dm @@ -19,6 +19,7 @@ antag_moodlet = /datum/mood_event/heretics banning_key = ROLE_HERETIC required_living_playtime = 4 + ui_name = "AntagInfoHeretic" /// Whether we've ascended! (Completed one of the final rituals) var/ascended = FALSE /// The path our heretic has chosen. Mostly used for flavor. @@ -33,20 +34,25 @@ var/total_sacrifices = 0 /// A list of TOTAL how many high value sacrifices completed. var/high_value_sacrifices = 0 - /// Lazy assoc list of [weakrefs to humans] to [image previews of the human]. Humans that we have as sacrifice targets. + /// Weakrefs to the minds of monsters have been successfully summoned. Includes ghouls. + var/list/datum/weakref/monsters_summoned + /// Lazy assoc list of [weakrefs to minds] to [image previews of the human]. Humans that we have as sacrifice targets. var/list/datum/weakref/sac_targets + /// A list of minds we won't pick as targets. + var/list/datum/mind/target_blacklist /// Whether we're drawing a rune or not var/drawing_rune = FALSE /// A static typecache of all tools we can scribe with. 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)) - var/datum/action/innate/hereticmenu/menu - ui_name = "AntagInfoHeretic" + var/datum/action/antag_info/heretic/menu -/datum/antagonist/heretic/ui_data(mob/user) - var/list/data = list() +/datum/antagonist/heretic/Destroy() + . = ..() + LAZYCLEARLIST(target_blacklist) +/datum/antagonist/heretic/ui_data(mob/user) var/static/list/path_to_color = list( HERETIC_PATH_START = "grey", HERETIC_PATH_SIDE = "green", @@ -56,7 +62,8 @@ HERETIC_PATH_VOID = "blue", ) - data["charges"] = knowledge_points + var/list/available = list() + var/list/researched = list() for(var/datum/heretic_knowledge/knowledge as anything in get_researchable_knowledge()) var/list/knowledge_data = list() @@ -74,7 +81,7 @@ knowledge_data["hereticPath"] = initial(knowledge.route) knowledge_data["color"] = path_to_color[initial(knowledge.route)] || "grey" - data["learnableKnowledge"] += list(knowledge_data) + available += list(knowledge_data) for(var/path in researched_knowledge) var/list/knowledge_data = list() @@ -86,18 +93,29 @@ knowledge_data["hereticPath"] = found_knowledge.route knowledge_data["color"] = path_to_color[found_knowledge.route] || "grey" - data["learnedKnowledge"] += list(knowledge_data) - - return data + researched += list(knowledge_data) + + return list( + "sacrifices" = list( + "total" = total_sacrifices, + "command" = high_value_sacrifices + ), + "ascended" = ascended, + "points" = knowledge_points, + "knowledge" = list( + "available" = available, + "researched" = researched + ) + ) /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 list("objectives" = get_objectives()) - return data +/datum/antagonist/heretic/ui_interact(mob/user, datum/tgui/ui) + ui = SStgui.try_update_ui(user, src, ui) + if(!ui) + ui = new(user, src, ui_name, name) + ui.open() /datum/antagonist/heretic/make_info_button() return // we already handle this with our own button @@ -106,19 +124,16 @@ . = ..() if(.) return - switch(action) if("research") var/datum/heretic_knowledge/researched_path = text2path(params["path"]) if(!ispath(researched_path)) CRASH("Heretic attempted to learn non-heretic_knowledge path! (Got: [researched_path])") - if(initial(researched_path.cost) > knowledge_points) return if(!gain_knowledge(researched_path)) return - - knowledge_points -= initial(researched_path.cost) + adjust_knowledge_points(-initial(researched_path.cost)) return TRUE /datum/antagonist/heretic/ui_status(mob/user, datum/ui_state/state) @@ -214,18 +229,15 @@ */ /datum/antagonist/heretic/proc/on_spell_cast(mob/living/source, obj/effect/proc_holder/spell/spell) SIGNAL_HANDLER - // Non-Heretic spells, we don't care if(!spell.requires_heretic_focus) return - // If we've got the trait, we don't care if(HAS_TRAIT(source, TRAIT_ALLOW_HERETIC_CASTING)) return // All powerful, don't care if(ascended) return - // We shouldn't be able to cast this! Cancel it. source.balloon_alert(source, "You need a focus") return COMPONENT_CANCEL_SPELL @@ -239,16 +251,13 @@ */ /datum/antagonist/heretic/proc/on_item_afterattack(mob/living/source, atom/target, obj/item/weapon, proximity_flag, click_parameters) SIGNAL_HANDLER - if(!is_type_in_typecache(weapon, scribing_tools)) return if(!isturf(target) || !isliving(source) || !proximity_flag) return - var/obj/item/offhand = source.get_inactive_held_item() if(QDELETED(offhand) || !istype(offhand, /obj/item/melee/touch_attack/mansus_fist)) return - try_draw_rune(source, target, additional_checks = CALLBACK(src, PROC_REF(check_mansus_grasp_offhand), source)) return COMPONENT_CANCEL_ATTACK_CHAIN @@ -266,15 +275,12 @@ if(!isopenturf(nearby_turf) || is_type_in_typecache(nearby_turf, blacklisted_rune_turfs)) target_turf.balloon_alert(user, "Invalid placement for rune") return - if(locate(/obj/effect/heretic_rune) in range(3, target_turf)) target_turf.balloon_alert(user, "Too close to another rune") return - if(drawing_rune) target_turf.balloon_alert(user, "Already drawing a rune") return - INVOKE_ASYNC(src, PROC_REF(draw_rune), user, target_turf, drawing_time, additional_checks) /** @@ -288,13 +294,19 @@ */ /datum/antagonist/heretic/proc/draw_rune(mob/living/user, turf/target_turf, drawing_time = 30 SECONDS, additional_checks) drawing_rune = TRUE - target_turf.balloon_alert(user, "You start drawing a rune") + for(var/knowledge_index in researched_knowledge) + var/datum/heretic_knowledge/limited_amount/knowledge = researched_knowledge[knowledge_index] + if(!istype(knowledge)) + continue + var/length_mul = knowledge.rune_draw_time_multiplier(user, target_turf) + if(isnum_safe(length_mul) && length_mul > 0) + drawing_time *= length_mul + break if(!do_after(user, drawing_time, target = target_turf, extra_checks = additional_checks)) target_turf.balloon_alert(user, "Interrupted") drawing_rune = FALSE return - target_turf.balloon_alert(user, "Rune created") new /obj/effect/heretic_rune/big(target_turf) drawing_rune = FALSE @@ -318,55 +330,109 @@ */ /datum/antagonist/heretic/proc/fix_influence_network(mob/source) SIGNAL_HANDLER - GLOB.reality_smash_track.rework_network() /** * Create our objectives for our heretic. */ /datum/antagonist/heretic/proc/forge_objectives() + // Give research objective var/datum/objective/heretic_research/research_objective = new() research_objective.owner = owner objectives += research_objective - + // Calculate how many command members there are var/num_heads = 0 for(var/mob/player in SSticker.mode.current_players[CURRENT_LIVING_PLAYERS]) - if(player.mind.assigned_role in list("Captain", "Head of Personnel", "Chief Engineer", "Head of Security", "Research Director", "Chief Medical Officer")) + if((player.mind.assigned_role in GLOB.command_positions) && player.ckey && player.client) num_heads++ - + // Give normal sacrifice objective var/datum/objective/minor_sacrifice/sac_objective = new() sac_objective.owner = owner if(num_heads < 2) // They won't get major sacrifice, so bump up minor sacrifice a bit sac_objective.target_amount += 2 sac_objective.update_explanation_text() objectives += sac_objective - + // Give command sacrifice objective (if there's at least 2 command staff) if(num_heads >= 2) var/datum/objective/major_sacrifice/other_sac_objective = new() other_sac_objective.owner = owner objectives += other_sac_objective + update_static_data_for_all_viewers() /** * Add [target] as a sacrifice target for the heretic. - * Generates a preview image and associates it with a weakref of the mob. + * Generates a preview image and associates it with a weakref of the mob's mind. */ -/datum/antagonist/heretic/proc/add_sacrifice_target(mob/living/carbon/human/target) +/datum/antagonist/heretic/proc/add_sacrifice_target(target) + var/datum/mind/target_mind = get_mind(target, TRUE) + if(QDELETED(target_mind)) + return FALSE + var/mob/living/carbon/target_body = target_mind.current + if(!istype(target_body)) + return FALSE + RegisterSignal(target_mind, COMSIG_MIND_CRYOED, PROC_REF(on_sac_target_cryoed)) + LAZYSET(sac_targets, WEAKREF(target_mind), getFlatIcon(target_body, defdir = SOUTH)) + return TRUE - var/image/target_image = image(icon = target.icon, icon_state = target.icon_state) - target_image.overlays = target.overlays +/** + * Remove [target] as a sacrifice target for the heretic. + */ +/datum/antagonist/heretic/proc/remove_sacrifice_target(target) + var/datum/mind/target_mind = get_mind(target, TRUE) + if(!QDELETED(target_mind)) + UnregisterSignal(target_mind, COMSIG_MIND_CRYOED) + LAZYREMOVE(sac_targets, WEAKREF(target_mind)) - LAZYSET(sac_targets, WEAKREF(target), target_image) +/** + * Returns a list of valid sacrifice targets from the current living players. + */ +/datum/antagonist/heretic/proc/possible_sacrifice_targets(include_current_targets = TRUE) + . = list() + for(var/mob/living/carbon/human/player in SSticker.mode.current_players[CURRENT_LIVING_PLAYERS]) + if(!player.mind || !player.client || player.client.is_afk()) + continue + var/datum/mind/possible_target = player.mind + if(!include_current_targets && (WEAKREF(possible_target) in sac_targets)) + continue + if(possible_target == src) + continue + if(possible_target in target_blacklist) + continue + if(player.stat == DEAD || player.InFullCritical()) + continue + . += possible_target + +/datum/antagonist/heretic/proc/on_sac_target_cryoed(datum/mind/sac_mind) + SIGNAL_HANDLER + if(!istype(sac_mind) || !LAZYLEN(sac_targets)) + return + remove_sacrifice_target(sac_mind) + var/list/candidates = possible_sacrifice_targets(include_current_targets = FALSE) + if(!length(candidates)) + to_chat(owner, "You feel one of your sacrifice targets leave your reach... but the Mansus remains silent.") + return + var/datum/mind/new_target = pick(candidates) + add_sacrifice_target(new_target) + to_chat(owner, "The Mansus whispers to you a new name as one of your previous sacrifice targets exits your grasp... [new_target.name]. Go forth and sacrifice [new_target.current.p_them()]!") /** * Increments knowledge by one. * Used in callbacks for passive gain over time. */ /datum/antagonist/heretic/proc/passive_influence_gain() - knowledge_points++ + adjust_knowledge_points(1) if(owner.current.stat <= SOFT_CRIT) to_chat(owner.current, "You hear a whisper... [pick(strings(HERETIC_INFLUENCE_FILE, "drain_message"))]") addtimer(CALLBACK(src, PROC_REF(passive_influence_gain)), passive_gain_timer) +/datum/antagonist/heretic/proc/adjust_knowledge_points(amount) + knowledge_points += amount + ui_update() + +/datum/antagonist/heretic/proc/add_menu_action() + menu = new /datum/action/antag_info/heretic(src) + menu.Grant(owner.current) + /datum/antagonist/heretic/roundend_report() var/list/parts = list() @@ -378,14 +444,13 @@ if(length(objectives)) var/count = 1 for(var/datum/objective/objective as anything in objectives) - parts += "Objective #[count]: [objective.get_completion_message()]" + parts += "Objective #[count]: [objective.get_completion_message()]" if(!objective.check_completion()) succeeded = FALSE count++ if(ascended) parts += "THE HERETIC ASCENDED!" - else if(succeeded) parts += "The heretic was successful, but did not ascend!" @@ -395,87 +460,71 @@ parts += "Knowledge Researched: " var/list/string_of_knowledge = list() - for(var/knowledge_index in researched_knowledge) var/datum/heretic_knowledge/knowledge = researched_knowledge[knowledge_index] string_of_knowledge += knowledge.name - parts += english_list(string_of_knowledge) - return parts.Join("
") /datum/antagonist/heretic/get_admin_commands() . = ..() - switch(has_living_heart()) if(HERETIC_NO_LIVING_HEART) - .["Give Living Heart"] = CALLBACK(src, PROC_REF(give_living_heart)) + .["Give Living Heart"] = CALLBACK(src, PROC_REF(admin_give_living_heart)) if(HERETIC_HAS_LIVING_HEART) - .["Add Heart Target (Marked Mob)"] = CALLBACK(src, PROC_REF(add_marked_as_target)) - .["Remove Heart Target"] = CALLBACK(src, PROC_REF(remove_target)) - + .["Add Heart Target (Marked Mob)"] = CALLBACK(src, PROC_REF(admin_add_marked_target)) + .["Remove Heart Target"] = CALLBACK(src, PROC_REF(admin_remove_target)) .["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) +/datum/antagonist/heretic/proc/admin_give_living_heart(mob/admin) if(!admin.client?.holder) to_chat(admin, "You shouldn't be using this!") return - var/datum/heretic_knowledge/living_heart/heart_knowledge = get_knowledge(/datum/heretic_knowledge/living_heart) if(!heart_knowledge) to_chat(admin, "The heretic doesn't have a living heart knowledge for some reason. What?") return - heart_knowledge.on_research(owner.current) /* * Admin proc for adding a marked mob to a heretic's sac list. */ -/datum/antagonist/heretic/proc/add_marked_as_target(mob/admin) +/datum/antagonist/heretic/proc/admin_add_marked_target(mob/admin) if(!admin.client?.holder) to_chat(admin, "You shouldn't be using this!") return - var/mob/living/carbon/human/new_target = admin.client?.holder.marked_datum if(!istype(new_target)) to_chat(admin, "You need to mark a human to do this!") return - - if(alert(admin, "Let them know their targets have been updated?", "Whispers of the Mansus", "Yes", "No") == "Yes") + if(tgui_alert(admin, "Let them know their targets have been updated?", "Whispers of the Mansus", list("Yes", "No")) == "Yes") to_chat(owner.current, "The Mansus has modified your targets. Go find them!") to_chat(owner.current, "[new_target.real_name], the [new_target.mind?.assigned_role || "human"].") - 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) +/datum/antagonist/heretic/proc/admin_remove_target(mob/admin) if(!admin.client?.holder) to_chat(admin, "You shouldn't be using this!") return - var/list/removable = list() for(var/datum/weakref/ref as anything in sac_targets) - var/mob/living/carbon/human/old_target = ref.resolve() + var/datum/mind/old_target = ref.resolve() if(!QDELETED(old_target)) removable[old_target.name] = old_target - - var/name_of_removed = input(admin, "Choose a human to remove", "Who to Spare") as null|anything in removable + var/name_of_removed = tgui_input_list(admin, "Choose a human to remove", "Who to Spare", removable) if(QDELETED(src) || !admin.client?.holder || isnull(name_of_removed)) return - var/mob/living/carbon/human/chosen_target = removable[name_of_removed] - if(QDELETED(chosen_target) || !ishuman(chosen_target)) + var/datum/mind/chosen_target = removable[name_of_removed] + if(!istype(chosen_target) || !(WEAKREF(chosen_target) in sac_targets)) return - if(!(WEAKREF(chosen_target) in sac_targets)) - return - LAZYREMOVE(sac_targets, WEAKREF(chosen_target)) - - if(alert(admin, "Let them know their targets have been updated?", "Whispers of the Mansus", "Yes", "No") == "Yes") + if(tgui_alert(admin, "Let them know their targets have been updated?", "Whispers of the Mansus", list("Yes", "No")) == "Yes") to_chat(owner.current, "The Mansus has modified your targets.") /* @@ -486,10 +535,10 @@ to_chat(admin, "You shouldn't be using this!") return - var/change_num = input(admin, "Add or remove knowledge points", "Points") as null|num + var/change_num = tgui_input_number(admin, "Add or remove knowledge points", "Points", 0) if(!change_num || QDELETED(src)) return - knowledge_points += change_num + adjust_knowledge_points(change_num) message_admins("[admin] modified [src]'s knowledge points by [change_num].") /datum/antagonist/heretic/antag_panel_data() @@ -506,15 +555,14 @@ /datum/antagonist/heretic/antag_panel_objectives() . = ..() - . += "
" . += "Current Targets:
" if(LAZYLEN(sac_targets)) for(var/datum/weakref/ref as anything in sac_targets) - var/mob/living/carbon/human/actual_target = ref.resolve() - if(QDELETED(actual_target)) + var/datum/mind/actual_target = ref.resolve() + if(istype(actual_target)) continue - . += " - [actual_target.real_name], the [actual_target.mind?.assigned_role || "Unknown"].
" + . += " - [actual_target.name], the [actual_target.assigned_role || "Unknown"].
" else . += "None!
" . += "
" @@ -553,8 +601,28 @@ /* * Check if the wanted type-path is in the list of research knowledge. */ -/datum/antagonist/heretic/proc/get_knowledge(wanted) - return researched_knowledge[wanted] +/datum/antagonist/heretic/proc/get_knowledge(wanted, subtypes = FALSE) + if(subtypes) + for(var/knowledge_path in typesof(wanted)) + if(researched_knowledge[knowledge_path]) + return researched_knowledge[knowledge_path] + else + return researched_knowledge[wanted] + +/** + * Check to see if the given mob can be sacrificed + */ +/datum/antagonist/heretic/proc/can_sacrifice(target) + . = FALSE + var/datum/mind/target_mind = get_mind(target, TRUE) + if(!istype(target_mind)) + return + if(LAZYACCESS(sac_targets, WEAKREF(target_mind))) + return TRUE + // You can ALWAYS sacrifice heads of staff if you need to do so. + var/datum/objective/major_sacrifice/major_sacc_objective = locate() in objectives + if(major_sacc_objective && !major_sacc_objective.check_completion() && (target_mind.assigned_role in GLOB.command_positions)) + return TRUE /* * Get a list of all rituals this heretic can invoke on a rune. @@ -564,13 +632,11 @@ */ /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) /* @@ -598,10 +664,8 @@ var/obj/item/organ/our_living_heart = owner.current?.getorganslot(ORGAN_SLOT_HEART) 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. @@ -619,9 +683,7 @@ /datum/objective/minor_sacrifice/check_completion() var/datum/antagonist/heretic/heretic_datum = owner?.has_antag_datum(/datum/antagonist/heretic) - if(!heretic_datum) - return FALSE - return heretic_datum.total_sacrifices >= target_amount + return ..() || (heretic_datum?.total_sacrifices >= target_amount) /// Heretic's major sacrifice objective. "Major sacrifices" are heads of staff. /datum/objective/major_sacrifice @@ -631,9 +693,7 @@ /datum/objective/major_sacrifice/check_completion() var/datum/antagonist/heretic/heretic_datum = owner?.has_antag_datum(/datum/antagonist/heretic) - if(!heretic_datum) - return FALSE - return heretic_datum.high_value_sacrifices >= target_amount + return ..() || (heretic_datum?.high_value_sacrifices >= target_amount) /// Heretic's research objective. "Research" is heretic knowledge nodes (You start with some). /datum/objective/heretic_research @@ -643,7 +703,6 @@ /datum/objective/heretic_research/New(text) . = ..() - if(!main_path_length) // Let's find the length of a main path. We'll use rust because it's the coolest. // (All the main paths are (should be) the same length, so it doesn't matter.) @@ -651,9 +710,7 @@ for(var/datum/heretic_knowledge/knowledge as anything in subtypesof(/datum/heretic_knowledge)) if(initial(knowledge.route) == HERETIC_PATH_RUST) rust_paths_found++ - main_path_length = rust_paths_found - // Factor in the length of the main path first. target_amount = main_path_length // Add in the base research we spawn with, otherwise it'd be too easy. @@ -668,9 +725,7 @@ /datum/objective/heretic_research/check_completion() var/datum/antagonist/heretic/heretic_datum = owner?.has_antag_datum(/datum/antagonist/heretic) - if(!heretic_datum) - return FALSE - return length(heretic_datum.researched_knowledge) >= target_amount + return completed || (length(heretic_datum?.researched_knowledge) >= target_amount) /datum/objective/heretic_summon name = "summon monsters" @@ -678,19 +733,8 @@ explanation_text = "Summon 2 monsters from the Mansus into this realm." /datum/objective/heretic_summon/check_completion() - - var/num_we_have = 0 - for(var/datum/antagonist/heretic_monster/monster in GLOB.antagonists) - if(!monster.master) - continue - if(ishuman(monster.owner.current)) - continue - if(monster.master != owner) - continue - - num_we_have++ - - return completed || (num_we_have >= target_amount) + var/datum/antagonist/heretic/heretic_datum = owner?.has_antag_datum(/datum/antagonist/heretic) + return ..() || (LAZYLEN(heretic_datum?.monsters_summoned) >= target_amount) /datum/action/antag_info/heretic name = "Forbidden Knowledge" diff --git a/code/modules/antagonists/heretic/heretic_knowledge.dm b/code/modules/antagonists/heretic/heretic_knowledge.dm index ed9eb138decdd..c3bf08c16466c 100644 --- a/code/modules/antagonists/heretic/heretic_knowledge.dm +++ b/code/modules/antagonists/heretic/heretic_knowledge.dm @@ -183,7 +183,7 @@ /// A list of weakrefs to all items we've created. var/list/datum/weakref/created_items -/datum/heretic_knowledge/limited_amount/Destroy(force, ...) +/datum/heretic_knowledge/limited_amount/Destroy() LAZYCLEARLIST(created_items) return ..() @@ -192,11 +192,9 @@ var/atom/real_thing = ref.resolve() if(QDELETED(real_thing)) LAZYREMOVE(created_items, ref) - 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) @@ -205,49 +203,60 @@ LAZYADD(created_items, WEAKREF(created_thing)) return TRUE +/* + * Returns a multiplier for the time required to draw a rune. + */ +/datum/heretic_knowledge/limited_amount/proc/rune_draw_time_multiplier(mob/living/user, turf/target_turf) + var/datum/antagonist/heretic/our_heretic = IS_HERETIC(user) + if(our_heretic.ascended) + return 0.2 + if(our_heretic.can_sacrifice(user.pulling)) + return 0.5 + for(var/mob/living/target as anything in range(1, target_turf) + range(1, user)) + if(istype(target) && our_heretic.can_sacrifice(target)) + return 0.5 + return 1 + /* * A knowledge subtype lets the heretic curse someone with a ritual. */ /datum/heretic_knowledge/curse /// The duration of the curse var/duration = 5 MINUTES - /// Cache list of fingerprints (actual fingerprint strings) we have from our current ritual - var/list/fingerprints + /// Cache list of DNA (UI strings) we have from our current ritual + var/list/dna /datum/heretic_knowledge/curse/recipe_snowflake_check(mob/living/user, list/atoms, list/selected_atoms, turf/loc) - fingerprints = list() + dna = list() for(var/atom/requirements as anything in atoms) - fingerprints[requirements.return_fingerprints()] = 1 - list_clear_nulls(fingerprints) - - // No fingerprints? No ritual - if(!length(fingerprints)) - loc.balloon_alert(user, "ritual failed, no fingerprints!") + for(var/fingerprint in requirements.return_fingerprints()) + dna[fingerprint] = TRUE + for(var/blood in requirements.return_blood_DNA()) + dna[blood] = TRUE + list_clear_nulls(dna) + // No DNA? No ritual + if(!length(dna)) + loc.balloon_alert(user, "ritual failed, no DNA!") return FALSE - return TRUE /datum/heretic_knowledge/curse/on_finished_recipe(mob/living/user, list/selected_atoms, turf/loc) - var/list/compiled_list = list() - - for(var/mob/living/carbon/carbon_to_check as anything in GLOB.carbon_list) - if(!istype(carbon_to_check, /mob/living/carbon/human)) + for(var/mob/living/carbon/human/candidate in GLOB.carbon_list) + if(candidate == user || QDELETED(candidate.mind) || !length(candidate.dna?.uni_identity)) continue - var/mob/living/carbon/human/human_to_check = carbon_to_check - if(fingerprints[md5(human_to_check.dna.uni_identity)]) - compiled_list |= human_to_check.real_name - compiled_list[human_to_check.real_name] = human_to_check + var/dna_string = rustg_hash_string(RUSTG_HASH_MD5, candidate.dna.uni_identity) + if(dna[dna_string]) + compiled_list |= candidate.real_name + compiled_list[candidate.real_name] = candidate if(!length(compiled_list)) - loc.balloon_alert(user, "Ritual failed, no fingerprints found") + loc.balloon_alert(user, "Ritual failed, no DNA found") return FALSE - var/chosen_mob = input(user, "Select the person you wish to curse", "Eldritch Curse") as null|anything in sort_list(compiled_list, GLOBAL_PROC_REF(cmp_mob_realname_dsc)) - if(isnull(chosen_mob)) + var/mob/living/carbon/human/to_curse = tgui_input_list(user, "Select the person you wish to curse", "Eldritch Curse", sort_list(compiled_list, GLOBAL_PROC_REF(cmp_mob_realname_dsc))) + if(isnull(to_curse)) return FALSE - - var/mob/living/carbon/human/to_curse = compiled_list[chosen_mob] if(QDELETED(to_curse)) loc.balloon_alert(user, "Ritual failed, invalid choice") return FALSE @@ -307,6 +316,9 @@ var/datum/antagonist/heretic_monster/heretic_monster = summoned.mind.add_antag_datum(/datum/antagonist/heretic_monster) heretic_monster.set_owner(user.mind) + var/datum/antagonist/heretic/our_heretic = IS_HERETIC(user) + LAZYOR(our_heretic.monsters_summoned, WEAKREF(summoned.mind)) + return TRUE /// The amount of knowledge points the knowledge ritual gives on success. @@ -328,36 +340,36 @@ . = ..() var/static/list/potential_organs = list( /obj/item/organ/appendix, - /obj/item/organ/tail, - /obj/item/organ/eyes, - /obj/item/organ/tongue, /obj/item/organ/ears, + /obj/item/organ/eyes, /obj/item/organ/heart, /obj/item/organ/liver, - /obj/item/organ/stomach, /obj/item/organ/lungs, + /obj/item/organ/stomach, + /obj/item/organ/tail, + /obj/item/organ/tongue ) var/static/list/potential_easy_items = list( - /obj/item/shard, - /obj/item/candle, /obj/item/book, - /obj/item/pen, - /obj/item/paper, - /obj/item/toy/crayon, - /obj/item/flashlight, + /obj/item/candle, /obj/item/clipboard, + /obj/item/flashlight, + /obj/item/paper, + /obj/item/pen, + /obj/item/shard, + /obj/item/toy/crayon ) var/static/list/potential_uncommoner_items = list( - /obj/item/restraints/legcuffs/beartrap, - /obj/item/restraints/handcuffs/cable/zipties, - /obj/item/circular_saw, - /obj/item/scalpel, /obj/item/binoculars, + /obj/item/circular_saw, + /obj/item/clothing/glasses/sunglasses, /obj/item/clothing/gloves/color/yellow, /obj/item/melee/baton, - /obj/item/clothing/glasses/sunglasses, + /obj/item/restraints/handcuffs/cable/zipties, + /obj/item/restraints/legcuffs/beartrap, + /obj/item/scalpel ) required_atoms = list() @@ -392,7 +404,7 @@ /datum/heretic_knowledge/knowledge_ritual/on_finished_recipe(mob/living/user, list/selected_atoms, turf/loc) var/datum/antagonist/heretic/our_heretic = IS_HERETIC(user) - our_heretic.knowledge_points += KNOWLEDGE_RITUAL_POINTS + our_heretic.adjust_knowledge_points(KNOWLEDGE_RITUAL_POINTS) was_completed = TRUE var/drain_message = pick(strings(HERETIC_INFLUENCE_FILE, "drain_message")) @@ -419,25 +431,17 @@ total_points += knowledge.cost /datum/heretic_knowledge/final/can_be_invoked(datum/antagonist/heretic/invoker) - if(invoker.ascended) - return FALSE - - if(!invoker.can_ascend()) - return FALSE - - return TRUE + return !invoker.ascended && invoker.can_ascend() /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.) for(var/mob/living/carbon/human/sacrifice in atoms) if(!is_valid_sacrifice(sacrifice)) atoms -= sacrifice - // All the non-dead humans are removed in this proc. // We handle checking if we have enough humans in the ritual itself. return TRUE @@ -463,5 +467,4 @@ for(var/mob/living/carbon/human/sacrifice in selected_atoms) selected_atoms -= sacrifice sacrifice.gib() - return ..() diff --git a/code/modules/antagonists/heretic/heretic_living_heart.dm b/code/modules/antagonists/heretic/heretic_living_heart.dm index acf2182aaa7b8..6459989cb2af7 100644 --- a/code/modules/antagonists/heretic/heretic_living_heart.dm +++ b/code/modules/antagonists/heretic/heretic_living_heart.dm @@ -72,26 +72,22 @@ desc = "Track a Sacrifice Target" check_flags = AB_CHECK_CONSCIOUS background_icon_state = "bg_ecult" - /// The real name of the last mob we tracked - var/last_tracked_name /// Whether the target radial is currently opened. var/radial_open = FALSE /// How long we have to wait between tracking uses. - var/track_cooldown_lenth = 8 SECONDS + var/track_cooldown_length = 8 SECONDS /// The cooldown between button uses. COOLDOWN_DECLARE(track_cooldown) /datum/action/item_action/organ_action/track_target/Grant(mob/granted) if(!IS_HERETIC(granted)) return - return ..() /datum/action/item_action/organ_action/track_target/IsAvailable() . = ..() if(!.) return - if(!IS_HERETIC(owner)) return FALSE if(!HAS_TRAIT(target, TRAIT_LIVING_HEART)) @@ -107,22 +103,21 @@ return var/datum/antagonist/heretic/heretic_datum = IS_HERETIC(owner) - if(!LAZYLEN(heretic_datum.sac_targets)) + if(!LAZYLEN(heretic_datum.sac_targets) && !length(GLOB.reality_smash_track.smashes)) owner.balloon_alert(owner, "No targets, visit a rune") return TRUE var/list/targets_to_choose = list() - var/list/mob/living/carbon/human/human_targets = list() + var/list/mob/living/carbon/tracked_targets = list() for(var/datum/weakref/target_ref as anything in heretic_datum.sac_targets) - var/mob/living/carbon/human/real_target = target_ref.resolve() - if(QDELETED(real_target)) + var/datum/mind/target_mind = target_ref.resolve() + if(!istype(target_mind) || !iscarbon(target_mind.current)) continue - - human_targets[real_target.real_name] = real_target - targets_to_choose[real_target.real_name] = heretic_datum.sac_targets[target_ref] + tracked_targets[target_mind.name] = target_mind.current + targets_to_choose[target_mind.name] = heretic_datum.sac_targets[target_ref] radial_open = TRUE - last_tracked_name = show_radial_menu( + var/tracked = show_radial_menu( owner, owner, targets_to_choose, @@ -132,42 +127,50 @@ tooltips = TRUE, ) radial_open = FALSE - // If our last tracked name is still null, skip the trigger - if(isnull(last_tracked_name)) + if(isnull(tracked)) return FALSE - var/mob/living/carbon/human/tracked_mob = human_targets[last_tracked_name] + var/mob/living/carbon/tracked_mob = tracked_targets[tracked] if(QDELETED(tracked_mob)) - last_tracked_name = null return FALSE + . = track_sacrifice_target(tracked_mob) - COOLDOWN_START(src, track_cooldown, track_cooldown_lenth) - var/balloon_message = "Your target is " + if(.) + COOLDOWN_START(src, track_cooldown, track_cooldown_length) + playsound(owner, 'sound/effects/singlebeat.ogg', vol = 50, vary = TRUE, extrarange = SILENCED_SOUND_EXTRARANGE) - playsound(owner, 'sound/effects/singlebeat.ogg', 50, TRUE, SILENCED_SOUND_EXTRARANGE) - if(isturf(tracked_mob.loc) && owner.z != tracked_mob.z) - balloon_message += "on another plane" +/datum/action/item_action/organ_action/track_target/proc/track_sacrifice_target(mob/living/carbon/tracked) + var/turf/owner_turf = get_turf(owner) + var/turf/tracked_turf = get_turf(tracked) + var/balloon_message = "Your target is " + if(tracked.stat == DEAD) + balloon_message += "dead and " + else if(!tracked.ckey) + balloon_message += "catatonic and " + if(owner_turf.get_virtual_z_level() != tracked_turf.get_virtual_z_level()) + if(is_reserved_level(tracked_turf.z)) + balloon_message += "traveling through space" + else + balloon_message += "on another plane" else - var/dist = get_dist(get_turf(owner), get_turf(tracked_mob)) - var/dir = get_dir(get_turf(owner), get_turf(tracked_mob)) - - switch(dist) - if(0 to 15) - balloon_message += "very near, [dir2text(dir)]" - if(16 to 31) - balloon_message += "near, [dir2text(dir)]" - if(32 to 127) - balloon_message += "far, [dir2text(dir)]" - else - balloon_message += "very far" - - if(tracked_mob.stat == DEAD) - balloon_message += "dead, they're " + balloon_message - + balloon_message += distance_hint(owner_turf, tracked_turf) owner.balloon_alert(owner, balloon_message) return TRUE +/datum/action/item_action/organ_action/track_target/proc/distance_hint(turf/source, turf/target) + var/dist = get_dist(source, target) + var/dir = get_dir(source, target) + switch(dist) + if(0 to 15) + return "very near, [dir2text(dir)]" + if(16 to 31) + return "near, [dir2text(dir)]" + if(32 to 127) + return "far away, [dir2text(dir)]" + else + return "very far away" + /// Callback for the radial to ensure it's closed when not allowed. /datum/action/item_action/organ_action/track_target/proc/check_menu() if(QDELETED(src)) diff --git a/code/modules/antagonists/heretic/influences.dm b/code/modules/antagonists/heretic/influences.dm index 8b394c5d71477..edfb744df3112 100644 --- a/code/modules/antagonists/heretic/influences.dm +++ b/code/modules/antagonists/heretic/influences.dm @@ -1,6 +1,11 @@ - /// The number of influences spawned per heretic #define NUM_INFLUENCES_PER_HERETIC 4 +/// The minimum amount of time until midround influences begin to spawn +#define MIDROUND_INFLUENCE_MIN_TIME 25 MINUTES +/// The maximum amount of time until midround influences begin to spawn +#define MIDROUND_INFLUENCE_MAX_TIME 40 MINUTES +/// How many tiles to "fuzz" the influence spawn location by. +#define HERETIC_INFLUENCE_SPAWN_FUZZ 5 /** * #Reality smash tracker @@ -18,10 +23,13 @@ var/num_drained = 0 /// List of tracked influences (reality smashes) var/list/obj/effect/heretic_influence/smashes = list() + /// List of visible influences (harvested reality smashes) + var/list/obj/effect/visible_heretic_influence/visible_smashes = list() /// List of minds with the ability to see influences var/list/datum/mind/tracked_heretics = list() -/datum/reality_smash_tracker/Destroy(force, ...) + +/datum/reality_smash_tracker/Destroy() if(GLOB.reality_smash_track == src) stack_trace("[type] was deleted. Heretics may no longer access any influences. Fix it, or call coder support.") message_admins("The [type] was deleted. Heretics may no longer access any influences. Fix it, or call coder support.") @@ -29,6 +37,39 @@ tracked_heretics.Cut() return ..() +/datum/reality_smash_tracker/proc/start_midround_influence_timer() + var/time = FLOOR(rand(MIDROUND_INFLUENCE_MIN_TIME, MIDROUND_INFLUENCE_MAX_TIME), 15 SECONDS) + addtimer(CALLBACK(src, PROC_REF(spawn_midround_influences)), time, TIMER_UNIQUE | TIMER_OVERRIDE | TIMER_DELETE_ME) + +/** + * Generates some random new influences in the middle of the round for each heretic, + * regardless of pre-existing heretics. + */ +/datum/reality_smash_tracker/proc/spawn_midround_influences() + start_midround_influence_timer() + var/living_heretics = 0 + for(var/datum/mind/heretic_mind as anything in tracked_heretics) + var/mob/living/carbon/heretic_body = heretic_mind.current + var/datum/antagonist/heretic/heretic_datum = heretic_mind.has_antag_datum(/datum/antagonist/heretic) + if(QDELETED(heretic_datum)) + continue + if(heretic_datum.ascended) + continue + if(heretic_datum.can_ascend() && heretic_datum.get_knowledge(/datum/heretic_knowledge/final, subtypes = TRUE)) + continue + if(QDELETED(heretic_body) || !istype(heretic_body)) + continue + if(!heretic_body.ckey || heretic_body.stat == DEAD || heretic_body.InFullCritical()) + continue + living_heretics++ + if(!living_heretics) + return + var/influences_to_spawn = rand(1, 3) + for(var/heretic_number = 1 to living_heretics) + influences_to_spawn += max(NUM_INFLUENCES_PER_HERETIC - heretic_number, 1) + log_game("Spawning [influences_to_spawn] new midround influences for heretics") + INVOKE_ASYNC(src, PROC_REF(generate_new_influences), influences_to_spawn) + /** * Automatically fixes the target and smash network * @@ -64,28 +105,149 @@ * based on the number of already existing smashes * 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) +/datum/reality_smash_tracker/proc/generate_new_influences(amount) + /// Static list of station areas which would otherwise be valid, but we don't want to spawn influences in. + var/static/list/station_area_blacklist = typecacheof(list( + /area/chapel/office, + /area/commons/dorms/barracks, + /area/crew_quarters/theatre/abandoned, + /area/crew_quarters/theatre/backstage, + /area/hallway/secondary/asteroid, + /area/hallway/secondary/command, + /area/hallway/secondary/construction, + /area/hallway/secondary/service, + /area/hallway/upper/secondary/command, + /area/hallway/upper/secondary/construction, + /area/hallway/upper/secondary/service, + /area/holodeck/rec_center/offstation_one, + /area/hydroponics/garden/abandoned, + /area/library/abandoned + )) + /// Static list of station areas we will attempt to spawn influences in the general vicinity of. + var/static/list/public_station_areas = typecacheof(list( + /area/cargo/lobby, + /area/chapel, + /area/commons/dorms, + /area/crew_quarters/cafeteria, + /area/crew_quarters/fitness, + /area/crew_quarters/locker, + /area/crew_quarters/lounge, + /area/crew_quarters/park, + /area/crew_quarters/theatre, + /area/crew_quarters/toilet, + /area/hallway, + /area/holodeck/rec_center, + /area/hydroponics/garden, + /area/library, + /area/medical/medbay/central, + /area/medical/medbay/lobby, + /area/medical/sleeper, + /area/science/lobby, + /area/storage/art, + /area/storage/primary, + /area/storage/tools + )) - station_area_blacklist + /// Static typecache of 'secondary' areas that should be weighted less. + var/static/list/low_weight_areas = typecacheof(/area/security/checkpoint) + /// Static typecache of the areas we will NEVER spawn influences in. + var/static/list/forbidden_influence_areas = (typecacheof(list( + /area/ai_monitored, + /area/asteroid, + /area/bridge, + /area/comms, + /area/crew_quarters/heads, + /area/docking, + /area/drydock, + /area/engine/atmospherics_engine, + /area/engine/engine_room, + /area/engine/gravity_generator, + /area/engine/transit_tube, + /area/gateway, + /area/maintenance, + /area/medical/abandoned, + /area/quartermaster/qm, + /area/quartermaster/qm_bedroom, + /area/science/mixing/chamber, + /area/security, + /area/server, + /area/shuttle, + /area/solar, + /area/space, + /area/tcommsat, + /area/teleporter + )) + station_area_blacklist) - low_weight_areas + + if(!isnum_safe(amount) || amount <= 0) + // 1 heretic = 4 influences + // 2 heretics = 7 influences + // 3 heretics = 9 influences + // 4 heretics = 10 influences, +1 for each onwards. + var/max_amount = 0 + for(var/heretic_number in 1 to length(tracked_heretics)) + max_amount += max(NUM_INFLUENCES_PER_HERETIC - heretic_number + 1, 1) + amount = max_amount - (length(smashes) + num_drained) + // Don't bother doing all this stuff if we've made enough influences already. + if(amount <= 0) + rework_network() + return var/location_sanity = 0 - while((length(smashes) + num_drained) < how_many_can_we_make && location_sanity < 100) - var/turf/chosen_location = get_safe_random_station_turfs() - - // We don't want them close to each other - at least 1 tile of seperation - var/list/nearby_things = range(1, chosen_location) - var/obj/effect/heretic_influence/what_if_i_have_one = locate() in nearby_things - var/obj/effect/visible_heretic_influence/what_if_i_had_one_but_its_used = locate() in nearby_things - if(what_if_i_have_one || what_if_i_had_one_but_its_used) + var/list/primary_turfs = list() + var/list/banned_turfs = list() + // Ensure at least 1 tile of seperation between other influences. + for(var/obj/smash as anything in smashes + visible_smashes) + var/turf/smash_loc = get_turf(smash) + for(var/near_smash in RANGE_TURFS(1, smash_loc)) + banned_turfs[near_smash] = TRUE + for(var/area/area_to_check as anything in GLOB.areas) + if(!is_type_in_typecache(area_to_check, public_station_areas)) + continue + for(var/turf/open/floor/floor in area_to_check.get_contained_turfs()) + if(banned_turfs[floor]) + continue + primary_turfs |= floor + if(!length(primary_turfs)) + CRASH("Could not find any valid turfs to spawn heretic influences on!") + var/spawned = 0 + while(length(primary_turfs) && spawned < amount && location_sanity < 100) + var/turf/chosen_location = pick_n_take(primary_turfs) + var/list/eligible_locations = list() + var/list/tested_turfs = list() + view_loop: + for(var/turf/open/floor/possibility in view(HERETIC_INFLUENCE_SPAWN_FUZZ, chosen_location)) + // Ensure we don't waste any time re-checking the same turf (or a banned turf) + if(tested_turfs[possibility] || banned_turfs[possibility]) + continue + tested_turfs[possibility] = TRUE + var/area/area = get_area(possibility) + // Ensure the turf isn't in an outright forbidden area + if(is_type_in_typecache(area, forbidden_influence_areas)) + continue + // Ensure all adjacent turfs are open (i.e no tiny rooms) + for(var/turf/nearby_turf as anything in RANGE_TURFS(1, possibility)) + if(!isfloorturf(nearby_turf)) + continue view_loop + // Ensure there's no dense objects on this turf + for(var/obj/thingymajig as anything in possibility) // Minor optimization: don't filter the list for objs beforehand, as we're going to break on the first dense obj anyways. + if(istype(thingymajig) && (thingymajig.density || initial(thingymajig.density))) + continue view_loop + // Ensure the turf is safe + if(!is_turf_safe(possibility)) + continue + // Lower weight for certain areas, and add it to our list of eligible locations + eligible_locations[possibility] = is_type_in_typecache(area, low_weight_areas) ? 1 : 5 + if(!length(eligible_locations)) location_sanity++ continue - - new /obj/effect/heretic_influence(chosen_location) + var/turf/chosen_turf = pick_weight(eligible_locations) + if(QDELETED(chosen_turf)) + location_sanity++ + continue + for(var/near_chosen in RANGE_TURFS(1, chosen_turf)) + banned_turfs[near_chosen] = TRUE + primary_turfs -= near_chosen + new /obj/effect/heretic_influence(chosen_turf) + spawned++ rework_network() @@ -95,6 +257,9 @@ * Use this whenever you want to add someone to the list */ /datum/reality_smash_tracker/proc/add_tracked_mind(datum/mind/heretic) + // First heretic? Set up our midround timer! + if(!length(tracked_heretics)) + start_midround_influence_timer() tracked_heretics |= heretic // If our heretic's on station, generate some new influences @@ -103,7 +268,6 @@ add_to_smashes(heretic) - /** * Removes a mind from the list of people that can see the reality smashes * @@ -123,14 +287,20 @@ resistance_flags = FIRE_PROOF | UNACIDABLE | ACID_PROOF alpha = 0 -/obj/effect/visible_heretic_influence/Initialize(mapload) +/obj/effect/visible_heretic_influence/Initialize() . = ..() + GLOB.reality_smash_track.visible_smashes += src addtimer(CALLBACK(src, PROC_REF(show_presence)), 15 SECONDS) var/image/silicon_image = image('icons/effects/heretic.dmi', src, null, OBJ_LAYER) silicon_image.override = TRUE add_alt_appearance(/datum/atom_hud/alternate_appearance/basic/silicons, "pierced_reality", silicon_image) - addtimer(CALLBACK(src,PROC_REF(dissipate)), 60 SECONDS) + addtimer(CALLBACK(src, PROC_REF(dissipate)), 1 MINUTES) + +/obj/effect/visible_heretic_influence/Destroy() + GLOB.reality_smash_track.visible_smashes -= src + return ..() + /* * Makes the influence fade in after 15 seconds. */ @@ -138,22 +308,19 @@ animate(src, alpha = 255, time = 15 SECONDS) /obj/effect/visible_heretic_influence/proc/dissipate() - animate(src,alpha = 0,time = 15 SECONDS) + animate(src,alpha = 0, time = 15 SECONDS) QDEL_IN(src, 15 SECONDS) /obj/effect/visible_heretic_influence/attack_hand(mob/living/user, list/modifiers) . = ..() - if(.) - return - if(!ishuman(user)) + if(. || !ishuman(user)) return - - if(IS_HERETIC(user)) + if(IS_HERETIC_OR_MONSTER(user)) to_chat(user, "You know better than to tempt forces out of your control!") return TRUE - var/mob/living/carbon/human/human_user = user var/obj/item/bodypart/their_poor_arm = human_user.get_active_hand() + // om nom nom if(prob(25)) to_chat(human_user, "An otherwordly presence tears and atomizes your [their_poor_arm.name] as you try to touch the hole in the very fabric of reality!") their_poor_arm.dismember() @@ -165,38 +332,32 @@ /obj/effect/visible_heretic_influence/attack_tk(mob/user) if(!ishuman(user)) return - . = COMPONENT_CANCEL_ATTACK_CHAIN - - if(IS_HERETIC(user)) + if(IS_HERETIC_OR_MONSTER(user)) to_chat(user, "You know better than to tempt forces out of your control!") return - var/mob/living/carbon/human/human_user = user - // A very elaborate way to suicide to_chat(human_user, "Eldritch energy lashes out, piercing your fragile mind, tearing it to pieces!") human_user.ghostize() + // Your head asplode! var/obj/item/bodypart/head/head = locate() in human_user.get_bodypart(BODY_ZONE_HEAD) if(head) head.dismember() qdel(head) else human_user.gib() - var/datum/effect_system/reagents_explosion/explosion = new() explosion.set_up(1, get_turf(human_user), TRUE, 0) explosion.start(src) -/obj/effect/visible_heretic_influence/examine(mob/user) +/obj/effect/visible_heretic_influence/examine(mob/living/carbon/human/user) . = ..() - if(IS_HERETIC(user) || !ishuman(user)) + if(!istype(user) || IS_HERETIC_OR_MONSTER(user)) return - - var/mob/living/carbon/human/human_user = user - to_chat(human_user, "Your mind burns as you stare at the tear!") - human_user.adjustOrganLoss(ORGAN_SLOT_BRAIN, 10, 190) - SEND_SIGNAL(human_user, COMSIG_ADD_MOOD_EVENT, "gates_of_mansus", /datum/mood_event/gates_of_mansus) + to_chat(user, "Your mind burns as you stare at the tear!") + user.adjustOrganLoss(ORGAN_SLOT_BRAIN, 10, 190) + SEND_SIGNAL(user, COMSIG_ADD_MOOD_EVENT, "gates_of_mansus", /datum/mood_event/gates_of_mansus) /obj/effect/heretic_influence name = "reality smash" @@ -224,26 +385,21 @@ GLOB.reality_smash_track.smashes -= src for(var/datum/mind/heretic in minds) remove_mind(heretic) - heretic_image = null return ..() /obj/effect/heretic_influence/attack_hand(mob/user, list/modifiers) if(!IS_HERETIC(user)) // Shouldn't be able to do this, but just in case return - if(being_drained) balloon_alert(user, "Already being drained") else INVOKE_ASYNC(src, PROC_REF(drain_influence), user, 1) - return - /obj/effect/heretic_influence/attackby(obj/item/weapon, mob/user, params) . = ..() if(.) return - // Using a codex will give you two knowledge points for draining. if(!being_drained && istype(weapon, /obj/item/codex_cicatrix)) var/obj/item/codex_cicatrix/codex = weapon @@ -259,24 +415,20 @@ * If successful, the influence is drained and deleted. */ /obj/effect/heretic_influence/proc/drain_influence(mob/living/user, knowledge_to_gain) - being_drained = TRUE balloon_alert(user, "You begin draining the influence") RegisterSignal(user, COMSIG_PARENT_EXAMINE, PROC_REF(on_examine)) - if(!do_after(user, 10 SECONDS, target = src)) being_drained = FALSE balloon_alert(user, "Interrupted") UnregisterSignal(user, COMSIG_PARENT_EXAMINE) return - // We don't need to set being_drained back since we delete after anyways UnregisterSignal(user, COMSIG_PARENT_EXAMINE) balloon_alert(user, "Influence drained") - + // Actually grant the heretic their well-earned knowledge var/datum/antagonist/heretic/heretic_datum = IS_HERETIC(user) - heretic_datum.knowledge_points += knowledge_to_gain - + heretic_datum.adjust_knowledge_points(knowledge_to_gain) // Aaand now we delete it after_drain(user) @@ -287,10 +439,8 @@ if(user) to_chat(user, "[pick(strings(HERETIC_INFLUENCE_FILE, "drain_message"))]") to_chat(user, "[src] begins to fade into reality!") - var/obj/effect/visible_heretic_influence/illusion = new /obj/effect/visible_heretic_influence(drop_location()) illusion.name = "\improper" + pick(strings(HERETIC_INFLUENCE_FILE, "drained")) + " " + format_text(name) - GLOB.reality_smash_track.num_drained++ qdel(src) @@ -299,13 +449,11 @@ * * Gives a chance for examiners to see that the heretic is interacting with an infuence. */ -/obj/effect/heretic_influence/proc/on_examine(atom/source, mob/user, list/examine_list) +/obj/effect/heretic_influence/proc/on_examine(mob/living/source, mob/user, list/examine_list) SIGNAL_HANDLER - - if(prob(50)) + if(!IS_HERETIC_OR_MONSTER(user) && prob(50)) return - - examine_list += "[source]'s hand seems to be glowing a ["strange purple"]..." + examine_list += "[source.p_their(TRUE)] hand seems to be glowing a ominous, cosmic purple..." /* * Add a mind to the list of tracked minds, @@ -322,7 +470,6 @@ /obj/effect/heretic_influence/proc/remove_mind(datum/mind/heretic) if(!(heretic in minds)) CRASH("[type] - remove_mind called with a mind not present in the minds list!") - minds -= heretic heretic.current?.client?.images -= heretic_image @@ -332,4 +479,7 @@ /obj/effect/heretic_influence/proc/generate_name() name = "\improper" + pick(strings(HERETIC_INFLUENCE_FILE, "prefix")) + " " + pick(strings(HERETIC_INFLUENCE_FILE, "postfix")) +#undef HERETIC_INFLUENCE_SPAWN_FUZZ +#undef MIDROUND_INFLUENCE_MAX_TIME +#undef MIDROUND_INFLUENCE_MIN_TIME #undef NUM_INFLUENCES_PER_HERETIC diff --git a/code/modules/antagonists/heretic/knowledge/flesh_lore.dm b/code/modules/antagonists/heretic/knowledge/flesh_lore.dm index 2a3c0d73eeb41..e7e9167832abe 100644 --- a/code/modules/antagonists/heretic/knowledge/flesh_lore.dm +++ b/code/modules/antagonists/heretic/knowledge/flesh_lore.dm @@ -62,6 +62,7 @@ var/datum/objective/heretic_summon/summon_objective = new() summon_objective.owner = our_heretic.owner our_heretic.objectives += summon_objective + our_heretic.update_static_data_for_all_viewers() to_chat(user, "Undertaking the Path of Flesh, you are given another objective.") our_heretic.owner.announce_objectives() @@ -119,6 +120,9 @@ var/datum/mind/human_target_mind = human_target.mind INVOKE_ASYNC(human_target_mind, TYPE_PROC_REF(/datum/mind, add_antag_datum), /datum/antagonist/heretic_monster) + var/datum/antagonist/heretic/our_heretic = IS_HERETIC(source) + LAZYOR(our_heretic.monsters_summoned, WEAKREF(human_target_mind)) + /datum/heretic_knowledge/limited_amount/flesh_grasp/proc/remove_ghoul(mob/living/carbon/human/source) SIGNAL_HANDLER @@ -196,6 +200,9 @@ var/datum/antagonist/heretic_monster/heretic_monster = soon_to_be_ghoul.mind.add_antag_datum(/datum/antagonist/heretic_monster) heretic_monster.set_owner(user.mind) + var/datum/antagonist/heretic/our_heretic = IS_HERETIC(user) + LAZYOR(our_heretic.monsters_summoned, WEAKREF(soon_to_be_ghoul.mind)) + selected_atoms -= soon_to_be_ghoul LAZYADD(created_items, WEAKREF(soon_to_be_ghoul)) diff --git a/code/modules/antagonists/heretic/knowledge/rust_lore.dm b/code/modules/antagonists/heretic/knowledge/rust_lore.dm index 0b4d1a6452156..b497789255aa8 100644 --- a/code/modules/antagonists/heretic/knowledge/rust_lore.dm +++ b/code/modules/antagonists/heretic/knowledge/rust_lore.dm @@ -54,6 +54,15 @@ var/datum/antagonist/heretic/our_heretic = IS_HERETIC(user) our_heretic.heretic_path = route +/datum/heretic_knowledge/limited_amount/base_rust/rune_draw_time_multiplier(mob/living/user, turf/target_turf) + . = ..() + var/rust = 0 + for(var/turf/open/floor in RANGE_TURFS(1, target_turf)) + if(HAS_TRAIT(floor, TRAIT_RUSTY)) + rust++ + if(rust >= 4) + . = min(., 0.65) + /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. \ 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 9d696dd4f9f4b..cb95207dc4521 100644 --- a/code/modules/antagonists/heretic/knowledge/sacrifice_knowledge/sacrifice_knowledge.dm +++ b/code/modules/antagonists/heretic/knowledge/sacrifice_knowledge/sacrifice_knowledge.dm @@ -22,14 +22,11 @@ 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. - 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 /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) @@ -67,7 +64,7 @@ // 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)) + if(sacrifice.stat < SOFT_CRIT || !heretic_datum.can_sacrifice(sacrifice)) atoms -= sacrifice // Finally, return TRUE if we have a target in the list @@ -97,24 +94,10 @@ * 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) + var/datum/antagonist/heretic/heretic_datum = IS_HERETIC(user) // First construct a list of minds that are valid objective targets. - var/list/datum/mind/valid_targets = list() - for(var/mob/player in SSticker.mode.current_players[CURRENT_LIVING_PLAYERS]) - if(player.mind) - var/datum/mind/possible_target = player.mind - if(possible_target == user.mind) - continue - if(possible_target in target_blacklist) - continue - if(!ishuman(player)) - continue - if(player.stat == DEAD) - continue - valid_targets += possible_target - else - continue - + var/list/datum/mind/valid_targets = heretic_datum.possible_sacrifice_targets() if(!length(valid_targets)) if(!silent) to_chat(user,"No sacrifice targets could be found!") @@ -129,14 +112,14 @@ // First target, any command. for(var/datum/mind/head_mind as anything in shuffle_inplace(valid_targets)) - if(head_mind.assigned_role in list("Captain", "Head of Personnel", "Chief Engineer", "Head of Security", "Research Director", "Chief Medical Officer")) + if(head_mind.assigned_role in GLOB.command_positions) final_targets += head_mind valid_targets -= head_mind break // Second target, any security for(var/datum/mind/sec_mind as anything in shuffle_inplace(valid_targets)) - if(sec_mind.assigned_role in list("Security Officer", "Warden", "Detective", "Head of Security", "Brig Physician", "Deputy")) + if(sec_mind.assigned_role in GLOB.security_positions) final_targets += sec_mind valid_targets -= sec_mind break @@ -154,12 +137,10 @@ // If any of our targets failed to aquire, // Let's run a loop until we get four total, grabbing random targets. var/target_sanity = 0 - while(length(final_targets) < 4 && length(valid_targets) > 4 && target_sanity < 25) + while(length(final_targets) < HERETIC_MAX_SAC_TARGETS && length(valid_targets) > HERETIC_MAX_SAC_TARGETS && 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, "Your targets have been determined. Your Living Heart will allow you to track their position. Go forth, and sacrifice them!") @@ -184,21 +165,21 @@ var/mob/living/carbon/human/sacrifice = locate() in selected_atoms if(!sacrifice) CRASH("[type] sacrifice_process didn't have a human in the atoms list. How'd it make it so far?") - if(!(WEAKREF(sacrifice) in heretic_datum.sac_targets)) + var/datum/mind/sacrifice_mind = get_mind(sacrifice, TRUE) + if(!heretic_datum.can_sacrifice(sacrifice_mind)) CRASH("[type] sacrifice_process managed to get a non-target human. This is incorrect.") - if(sacrifice.mind) - LAZYADD(target_blacklist, sacrifice.mind) - LAZYREMOVE(heretic_datum.sac_targets, WEAKREF(sacrifice)) + LAZYADD(heretic_datum.target_blacklist, sacrifice_mind) + heretic_datum.remove_sacrifice_target(sacrifice_mind) to_chat(user, "Your patron accepts your offer.") - if(sacrifice.mind?.assigned_role in list("Captain", "Head of Personnel", "Chief Engineer", "Head of Security", "Research Director", "Chief Medical Officer")) - heretic_datum.knowledge_points++ + if(sacrifice_mind.assigned_role in GLOB.command_positions) + heretic_datum.adjust_knowledge_points(1) heretic_datum.high_value_sacrifices++ heretic_datum.total_sacrifices++ - heretic_datum.knowledge_points += 2 + heretic_datum.adjust_knowledge_points(2) if(!begin_sacrifice(sacrifice)) disembowel_target(sacrifice) @@ -243,6 +224,8 @@ addtimer(CALLBACK(sac_target, TYPE_PROC_REF(/mob/living/carbon, do_jitter_animation), 100), SACRIFICE_SLEEP_DURATION * (1/3)) addtimer(CALLBACK(sac_target, TYPE_PROC_REF(/mob/living/carbon, do_jitter_animation), 100), SACRIFICE_SLEEP_DURATION * (2/3)) + // Grab their ghost, just in case they're dead or something. + sac_target.grab_ghost() // If our target is dead, try to revive them // and if we fail to revive them, don't proceede the chain if(!sac_target.heal_and_revive(50, "[sac_target]'s heart begins to beat with an unholy force as they return from death!")) @@ -274,6 +257,8 @@ if(QDELETED(sac_target)) return + // Grab ghost again, just to be safe. + sac_target.grab_ghost() // The target disconnected or something, we shouldn't bother sending them along. if(!sac_target.client || !sac_target.mind) disembowel_target(sac_target) diff --git a/code/modules/antagonists/heretic/knowledge/void_lore.dm b/code/modules/antagonists/heretic/knowledge/void_lore.dm index 1a77d2a8409a1..5f53a18aeb823 100644 --- a/code/modules/antagonists/heretic/knowledge/void_lore.dm +++ b/code/modules/antagonists/heretic/knowledge/void_lore.dm @@ -52,16 +52,19 @@ var/datum/antagonist/heretic/our_heretic = IS_HERETIC(user) our_heretic.heretic_path = route +/datum/heretic_knowledge/limited_amount/base_void/rune_draw_time_multiplier(mob/living/user, turf/open/target_turf) + . = ..() + if(istype(target_turf) && target_turf.GetTemperature() <= T0C) + . = min(., 0.65) + /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 ..() /datum/heretic_knowledge/void_grasp diff --git a/tgui/packages/tgui/interfaces/AntagInfoHeretic.tsx b/tgui/packages/tgui/interfaces/AntagInfoHeretic.tsx index 06987f3b8e436..84a15094c5d2e 100644 --- a/tgui/packages/tgui/interfaces/AntagInfoHeretic.tsx +++ b/tgui/packages/tgui/interfaces/AntagInfoHeretic.tsx @@ -27,6 +27,11 @@ const hereticYellow = { color: 'yellow', }; +enum CurrentTab { + Info, + Research, +} + type Knowledge = { path: string; name: string; @@ -39,8 +44,8 @@ type Knowledge = { }; type KnowledgeInfo = { - learnableKnowledge: Knowledge[]; - learnedKnowledge: Knowledge[]; + available: Knowledge[]; + researched: Knowledge[]; }; type Objective = { @@ -49,11 +54,17 @@ type Objective = { explanation: string; }; +type SacrificeInfo = { + total: number; + command: number; +}; + type Info = { - charges: number; - total_sacrifices: number; + points: number; + sacrifices: SacrificeInfo; ascended: BooleanLike; objectives: Objective[]; + knowledge: KnowledgeInfo; }; const IntroductionSection = () => { @@ -153,8 +164,13 @@ const GuideSection = () => { }; const InformationSection = (props, context) => { - const { data } = useBackend(context); - const { charges, total_sacrifices, ascended } = data; + const { + data: { + points, + ascended, + sacrifices: { total }, + }, + } = useBackend(context); return ( @@ -172,12 +188,12 @@ const InformationSection = (props, context) => { )} - You have {charges || 0}  - knowledge point{charges !== 1 ? 's' : ''}. + You have {points || 0}  + knowledge point{points !== 1 ? 's' : ''}. You have made a total of  - {total_sacrifices || 0}  + {total || 0}  sacrifices. @@ -186,8 +202,9 @@ const InformationSection = (props, context) => { }; const ObjectivePrintout = (props, context) => { - const { data } = useBackend(context); - const { objectives } = data; + const { + data: { objectives }, + } = useBackend(context); return ( @@ -206,15 +223,18 @@ const ObjectivePrintout = (props, context) => { }; const ResearchedKnowledge = (props, context) => { - const { data } = useBackend(context); - const { learnedKnowledge } = data; + const { + data: { + knowledge: { researched }, + }, + } = useBackend(context); return (
- {(!learnedKnowledge.length && 'None!') || - learnedKnowledge.map((learned) => ( + {(!researched.length && 'None!') || + researched.map((learned) => (