From 06fa6543dbd39c421af77a55983066d9a7bf1fd7 Mon Sep 17 00:00:00 2001 From: SkyratBot <59378654+SkyratBot@users.noreply.github.com> Date: Sat, 30 Sep 2023 00:03:57 +0200 Subject: [PATCH] [MIRROR] Heretic Ascension Tweaks [MDB IGNORE] (#24024) * Heretic Ascension Tweaks (#78472) ## About The Pull Request Some changes to Cosmic and Knock heretic ascensions, in response to feedback. - Cosmic Heretics can no longer control their summon while jaunted. - Additionally the death link element the Star Gazer used... wouldn't work if there was more than one cosmic heretic. I refactored it into a component, so that it would. Frankly there are probably a lot of _other_ abilities which shouldn't be usable while jaunted but are, but I have been burned in the past by adding defaults which were applied too widely so we'll leave it case-by-case for now. - The Knock rift can no longer summon Flesh Worms of any kind. - But it _can_ summon fire sharks (we really need to reflavour these...) and any future "basic mob" heretic mobs (or ones which are converted). - Additionally the rift can't be clicked by ghosts while it's polling ghosts automatically because that would create funky empty-minded mobs. - Finally it goes away when the heretic dies. ## Why It's Good For The Game For Cosmic Heretics, controlling the Star Gazer in conjunction with Space Jaunt essentially meant that the heretic was playing an RTS as an invincible observer with an invulnerable unit, now they have to actually be present and killable in order to sic The Beast on you. For Knock Heretics, Minor Flesh Worms are a proof of concept which was never meant to be used except by admins. They're _barely_ weaker than a normal flesh worm, extraordinarily tanky, delete walls, and generally devalue the Flesh path ascension. Vanishing upon heretic death is because every _other_ ascension is at least theoretically stoppable except this one. Now this one is too. It's still _very hard_ to kill the heretic because every _10 seconds_ they can transform into a heretic mob which acts as an extra health pool. ## Changelog :cl: fix: If two cosmic heretics ascend in the same round, their star gazer survival will be linked to each individual heretic and not shared by just one of them. fix: You can't click the Knock heretic portal to join as a mob while already signed up to become a mob. balance: Cosmic heretics can't order the Star Gazer around while jaunting. balance: The Knock Heretic portal cannot summon Flesh Worms, but can summon Fire Sharks. balance: The Knock Heretic portal will disperse if its creator is killed. /:cl: * Heretic Ascension Tweaks --------- Co-authored-by: Jacquerel --- code/__DEFINES/actions.dm | 3 + code/__DEFINES/colors.dm | 1 + code/__DEFINES/magic.dm | 4 +- code/datums/actions/action.dm | 17 ++++- code/datums/actions/mobs/open_mob_commands.dm | 1 + code/datums/components/death_linked.dm | 30 ++++++++ code/datums/elements/death_linked.dm | 29 -------- code/modules/admin/admin_verbs.dm | 2 - .../heretic/knowledge/cosmic_lore.dm | 2 +- .../antagonists/heretic/magic/star_touch.dm | 6 +- .../heretic/structures/knock_final.dm | 68 ++++++++++++++++--- code/modules/spells/spell.dm | 5 -- .../spells/spell_types/jaunt/_jaunt.dm | 4 +- tgstation.dme | 2 +- 14 files changed, 116 insertions(+), 58 deletions(-) create mode 100644 code/datums/components/death_linked.dm delete mode 100644 code/datums/elements/death_linked.dm diff --git a/code/__DEFINES/actions.dm b/code/__DEFINES/actions.dm index 066d2d60c1e..5bc2b161781 100644 --- a/code/__DEFINES/actions.dm +++ b/code/__DEFINES/actions.dm @@ -8,6 +8,8 @@ #define AB_CHECK_CONSCIOUS (1<<3) ///Action button checks if user is incapacitated #define AB_CHECK_INCAPACITATED (1<<4) +///Action button checks if user is jaunting +#define AB_CHECK_PHASED (1<<5) DEFINE_BITFIELD(check_flags, list( "CHECK IF HANDS BLOCKED" = AB_CHECK_HANDS_BLOCKED, @@ -15,6 +17,7 @@ DEFINE_BITFIELD(check_flags, list( "CHECK IF LYING DOWN" = AB_CHECK_LYING, "CHECK IF CONSCIOUS" = AB_CHECK_CONSCIOUS, "CHECK IF INCAPACITATED" = AB_CHECK_INCAPACITATED, + "CHECK IF TEMPORARILY INCORPOREAL" = AB_CHECK_PHASED, )) ///Action button triggered with right click diff --git a/code/__DEFINES/colors.dm b/code/__DEFINES/colors.dm index 25bd59bc8b5..b14fd514b85 100644 --- a/code/__DEFINES/colors.dm +++ b/code/__DEFINES/colors.dm @@ -93,6 +93,7 @@ #define COLOR_STRONG_MAGENTA "#B800B8" #define COLOR_PURPLE "#800080" #define COLOR_VIOLET "#B900F7" +#define COLOR_VOID_PURPLE "#53277E" #define COLOR_STRONG_VIOLET "#6927C5" #define COLOR_DARK_PURPLE "#551A8B" diff --git a/code/__DEFINES/magic.dm b/code/__DEFINES/magic.dm index 34ec4b6659b..ecc470c04e9 100644 --- a/code/__DEFINES/magic.dm +++ b/code/__DEFINES/magic.dm @@ -50,8 +50,7 @@ /// Whether the spell can be cast by mobs who are brains / mmis. /// When applying, bear in mind most spells will not function for brains out of the box. #define SPELL_CASTABLE_AS_BRAIN (1 << 2) -/// Whether the spell can be cast while phased, such as blood crawling, ethereal jaunting or using rod form. -#define SPELL_CASTABLE_WHILE_PHASED (1 << 3) + /// Whether the spell can be cast while the user has antimagic on them that corresponds to the spell's own antimagic flags. #define SPELL_REQUIRES_NO_ANTIMAGIC (1 << 4) /// Whether the spell requires being on the station z-level to be cast. @@ -66,7 +65,6 @@ DEFINE_BITFIELD(spell_requirements, list( "SPELL_CASTABLE_AS_BRAIN" = SPELL_CASTABLE_AS_BRAIN, - "SPELL_CASTABLE_WHILE_PHASED" = SPELL_CASTABLE_WHILE_PHASED, "SPELL_CASTABLE_WITHOUT_INVOCATION" = SPELL_CASTABLE_WITHOUT_INVOCATION, "SPELL_REQUIRES_HUMAN" = SPELL_REQUIRES_HUMAN, "SPELL_REQUIRES_MIME_VOW" = SPELL_REQUIRES_MIME_VOW, diff --git a/code/datums/actions/action.dm b/code/datums/actions/action.dm index b43220fb7d4..843998ff50e 100644 --- a/code/datums/actions/action.dm +++ b/code/datums/actions/action.dm @@ -96,13 +96,15 @@ if(check_flags & AB_CHECK_CONSCIOUS) RegisterSignal(owner, COMSIG_MOB_STATCHANGE, PROC_REF(update_status_on_signal)) if(check_flags & AB_CHECK_INCAPACITATED) - RegisterSignal(owner, SIGNAL_ADDTRAIT(TRAIT_INCAPACITATED), PROC_REF(update_status_on_signal)) + RegisterSignals(owner, list(SIGNAL_ADDTRAIT(TRAIT_INCAPACITATED), SIGNAL_REMOVETRAIT(TRAIT_INCAPACITATED)), PROC_REF(update_status_on_signal)) if(check_flags & AB_CHECK_IMMOBILE) - RegisterSignal(owner, SIGNAL_ADDTRAIT(TRAIT_IMMOBILIZED), PROC_REF(update_status_on_signal)) + RegisterSignals(owner, list(SIGNAL_ADDTRAIT(TRAIT_IMMOBILIZED), SIGNAL_REMOVETRAIT(TRAIT_IMMOBILIZED)), PROC_REF(update_status_on_signal)) if(check_flags & AB_CHECK_HANDS_BLOCKED) - RegisterSignal(owner, SIGNAL_ADDTRAIT(TRAIT_HANDS_BLOCKED), PROC_REF(update_status_on_signal)) + RegisterSignals(owner, list(SIGNAL_ADDTRAIT(TRAIT_HANDS_BLOCKED), SIGNAL_REMOVETRAIT(TRAIT_HANDS_BLOCKED)), PROC_REF(update_status_on_signal)) if(check_flags & AB_CHECK_LYING) RegisterSignal(owner, COMSIG_LIVING_SET_BODY_POSITION, PROC_REF(update_status_on_signal)) + if(check_flags & AB_CHECK_PHASED) + RegisterSignals(owner, list(SIGNAL_ADDTRAIT(TRAIT_MAGICALLY_PHASED), SIGNAL_REMOVETRAIT(TRAIT_MAGICALLY_PHASED)), PROC_REF(update_status_on_signal)) if(owner_has_control) GiveAction(grant_to) @@ -130,6 +132,11 @@ SIGNAL_ADDTRAIT(TRAIT_HANDS_BLOCKED), SIGNAL_ADDTRAIT(TRAIT_IMMOBILIZED), SIGNAL_ADDTRAIT(TRAIT_INCAPACITATED), + SIGNAL_ADDTRAIT(TRAIT_MAGICALLY_PHASED), + SIGNAL_REMOVETRAIT(TRAIT_HANDS_BLOCKED), + SIGNAL_REMOVETRAIT(TRAIT_IMMOBILIZED), + SIGNAL_REMOVETRAIT(TRAIT_INCAPACITATED), + SIGNAL_REMOVETRAIT(TRAIT_MAGICALLY_PHASED), )) if(target == owner) @@ -174,6 +181,10 @@ if (feedback) owner.balloon_alert(owner, "unconscious!") return FALSE + if((check_flags & AB_CHECK_PHASED) && HAS_TRAIT(owner, TRAIT_MAGICALLY_PHASED)) + if (feedback) + owner.balloon_alert(owner, "incorporeal!") + return FALSE return TRUE /// Builds / updates all buttons we have shared or given out diff --git a/code/datums/actions/mobs/open_mob_commands.dm b/code/datums/actions/mobs/open_mob_commands.dm index 008e7c4dc1b..e7ffd104eff 100644 --- a/code/datums/actions/mobs/open_mob_commands.dm +++ b/code/datums/actions/mobs/open_mob_commands.dm @@ -5,6 +5,7 @@ overlay_icon_state = "bg_heretic_border" button_icon = 'icons/mob/actions/actions_ecult.dmi' button_icon_state = "stargazer_menu" + check_flags = AB_CHECK_CONSCIOUS | AB_CHECK_INCAPACITATED | AB_CHECK_PHASED /// Weakref for storing our stargazer var/datum/weakref/our_mob diff --git a/code/datums/components/death_linked.dm b/code/datums/components/death_linked.dm new file mode 100644 index 00000000000..59d2ce5e855 --- /dev/null +++ b/code/datums/components/death_linked.dm @@ -0,0 +1,30 @@ +/** + * ## Death link component + * + * When the owner of this component dies it also gibs a linked mob + */ +/datum/component/death_linked + ///The mob that also dies when the user dies + var/datum/weakref/linked_mob + +/datum/component/death_linked/Initialize(mob/living/target_mob) + . = ..() + if(!isliving(parent)) + return COMPONENT_INCOMPATIBLE + if(isnull(target_mob)) + stack_trace("[type] added to [parent] with no linked mob.") + src.linked_mob = WEAKREF(target_mob) + +/datum/component/death_linked/RegisterWithParent() + . = ..() + RegisterSignal(parent, COMSIG_LIVING_DEATH, PROC_REF(on_death)) + +/datum/component/death_linked/UnregisterFromParent() + . = ..() + UnregisterSignal(parent, COMSIG_LIVING_DEATH) + +///signal called by the stat of the target changing +/datum/component/death_linked/proc/on_death(mob/living/target, gibbed) + SIGNAL_HANDLER + var/mob/living/linked_mob_resolved = linked_mob?.resolve() + linked_mob_resolved?.gib() diff --git a/code/datums/elements/death_linked.dm b/code/datums/elements/death_linked.dm deleted file mode 100644 index c5d2c6c422c..00000000000 --- a/code/datums/elements/death_linked.dm +++ /dev/null @@ -1,29 +0,0 @@ -/** - * ## death linkage element! - * - * Bespoke element that when the owner dies, the linked mob dies too. - */ -/datum/element/death_linked - element_flags = ELEMENT_BESPOKE - argument_hash_start_idx = 3 - ///The mob that also dies when the user dies - var/datum/weakref/linked_mob - -/datum/element/death_linked/Attach(datum/target, mob/living/target_mob) - . = ..() - if(!isliving(target)) - return ELEMENT_INCOMPATIBLE - if(!target_mob) - stack_trace("[type] added to [target] with NO MOB.") - src.linked_mob = WEAKREF(target_mob) - RegisterSignal(target, COMSIG_LIVING_DEATH, PROC_REF(on_death)) - -/datum/element/death_linked/Detach(datum/target) - . = ..() - UnregisterSignal(target, COMSIG_LIVING_DEATH) - -///signal called by the stat of the target changing -/datum/element/death_linked/proc/on_death(mob/living/target, gibbed) - SIGNAL_HANDLER - var/mob/living/linked_mob_resolved = linked_mob?.resolve() - linked_mob_resolved?.death(TRUE) diff --git a/code/modules/admin/admin_verbs.dm b/code/modules/admin/admin_verbs.dm index 6e030d9beb8..7f135cdbefb 100644 --- a/code/modules/admin/admin_verbs.dm +++ b/code/modules/admin/admin_verbs.dm @@ -1110,8 +1110,6 @@ GLOBAL_PROTECT(admin_verbs_poll) var/reqs = initial(spell.spell_requirements) if(reqs & SPELL_CASTABLE_AS_BRAIN) real_reqs += "Castable as brain" - if(reqs & SPELL_CASTABLE_WHILE_PHASED) - real_reqs += "Castable phased" if(reqs & SPELL_REQUIRES_HUMAN) real_reqs += "Must be human" if(reqs & SPELL_REQUIRES_MIME_VOW) diff --git a/code/modules/antagonists/heretic/knowledge/cosmic_lore.dm b/code/modules/antagonists/heretic/knowledge/cosmic_lore.dm index 9e4d77bd95c..4a4e9045185 100644 --- a/code/modules/antagonists/heretic/knowledge/cosmic_lore.dm +++ b/code/modules/antagonists/heretic/knowledge/cosmic_lore.dm @@ -253,7 +253,7 @@ var/mob/living/basic/heretic_summon/star_gazer/star_gazer_mob = new /mob/living/basic/heretic_summon/star_gazer(loc) star_gazer_mob.maxHealth = INFINITY star_gazer_mob.health = INFINITY - user.AddElement(/datum/element/death_linked, star_gazer_mob) + user.AddComponent(/datum/component/death_linked, star_gazer_mob) star_gazer_mob.AddComponent(/datum/component/obeys_commands, star_gazer_commands) star_gazer_mob.AddComponent(/datum/component/damage_aura, range = 7, burn_damage = 0.5, simple_damage = 0.5, immune_factions = list(FACTION_HERETIC), current_owner = user) star_gazer_mob.befriend(user) diff --git a/code/modules/antagonists/heretic/magic/star_touch.dm b/code/modules/antagonists/heretic/magic/star_touch.dm index de3a56128de..ba8c2a56391 100644 --- a/code/modules/antagonists/heretic/magic/star_touch.dm +++ b/code/modules/antagonists/heretic/magic/star_touch.dm @@ -73,11 +73,13 @@ /obj/item/melee/touch_attack/star_touch/Initialize(mapload) . = ..() - AddComponent(/datum/component/effect_remover, \ + AddComponent(\ + /datum/component/effect_remover, \ success_feedback = "You remove %THEEFFECT.", \ tip_text = "Clear rune", \ on_clear_callback = CALLBACK(src, PROC_REF(after_clear_rune)), \ - effects_we_clear = list(/obj/effect/cosmic_rune)) + effects_we_clear = list(/obj/effect/cosmic_rune), \ + ) /* * Callback for effect_remover component. diff --git a/code/modules/antagonists/heretic/structures/knock_final.dm b/code/modules/antagonists/heretic/structures/knock_final.dm index 85face85609..c8a2058eb9f 100644 --- a/code/modules/antagonists/heretic/structures/knock_final.dm +++ b/code/modules/antagonists/heretic/structures/knock_final.dm @@ -5,39 +5,68 @@ resistance_flags = INDESTRUCTIBLE | LAVA_PROOF | FIRE_PROOF | UNACIDABLE | ACID_PROOF icon = 'icons/obj/anomaly.dmi' icon_state = "bhole3" - color = "#53277E" - light_color = "#53277E" //cooler purple + color = COLOR_VOID_PURPLE + light_color = COLOR_VOID_PURPLE light_range = 20 anchored = TRUE density = FALSE layer = HIGH_PIPE_LAYER //0.01 above sigil layer used by heretic runes move_resist = INFINITY + /// Who is our daddy? var/datum/mind/ascendee - ///a static list of heretic summons, this shouldnt even matter enough to be static but whatever + /// True if we're currently checking for ghost opinions + var/gathering_candidates = TRUE + ///a static list of heretic summons we cam create, automatically populated from heretic monster subtypes var/static/list/monster_types + /// A static list of heretic summons which we should not create + var/static/list/monster_types_blacklist = list( + /mob/living/basic/heretic_summon/star_gazer, + /mob/living/simple_animal/hostile/heretic_summon/armsy, + /mob/living/simple_animal/hostile/heretic_summon/armsy/prime, + ) -/obj/structure/knock_tear/Initialize(mapload, ascendant) +/obj/structure/knock_tear/Initialize(mapload, datum/mind/ascendant_mind) . = ..() transform *= 3 - if(!monster_types) - monster_types = subtypesof(/mob/living/simple_animal/hostile/heretic_summon) - /mob/living/simple_animal/hostile/heretic_summon/armsy/prime - if(ascendant) - ascendee = ascendant + if(isnull(monster_types)) + monster_types = subtypesof(/mob/living/simple_animal/hostile/heretic_summon) + subtypesof(/mob/living/basic/heretic_summon) - monster_types_blacklist + if(!isnull(ascendant_mind)) + ascendee = ascendant_mind + RegisterSignals(ascendant_mind.current, list(COMSIG_LIVING_DEATH, COMSIG_QDELETING), PROC_REF(end_madness)) SSpoints_of_interest.make_point_of_interest(src) INVOKE_ASYNC(src, PROC_REF(poll_ghosts)) +/// Ask ghosts if they want to make some noise /obj/structure/knock_tear/proc/poll_ghosts() var/list/candidates = poll_ghost_candidates("Would you like to be a random eldritch monster attacking the crew?", ROLE_SENTIENCE, ROLE_SENTIENCE, 10 SECONDS, POLL_IGNORE_HERETIC_MONSTER) while(LAZYLEN(candidates)) var/mob/dead/observer/candidate = pick_n_take(candidates) ghost_to_monster(candidate, should_ask = FALSE) + gathering_candidates = FALSE + +/// Destroy the rift if you kill the heretic +/obj/structure/knock_tear/proc/end_madness(datum/former_master) + SIGNAL_HANDLER + var/turf/our_turf = get_turf(src) + playsound(our_turf, 'sound/magic/castsummon.ogg', vol = 100, vary = TRUE) + visible_message(span_boldwarning("The rip in space spasms and disappears!")) + UnregisterSignal(former_master, list(COMSIG_LIVING_DEATH, COMSIG_QDELETING)) // Just in case they die THEN delete + new /obj/effect/temp_visual/destabilising_tear(our_turf) + qdel(src) /obj/structure/knock_tear/attack_ghost(mob/user) . = ..() - if(.) + if(. || gathering_candidates) return ghost_to_monster(user) +/obj/structure/knock_tear/examine(mob/user) + . = ..() + if (!isobserver(user) || gathering_candidates) + return + . += span_notice("You can use this to enter the world as a foul monster.") + +/// Turn a ghost into an 'orrible beast /obj/structure/knock_tear/proc/ghost_to_monster(mob/dead/observer/user, should_ask = TRUE) if(should_ask) var/ask = tgui_alert(user, "Become a monster?", "Ascended Rift", list("Yes", "No")) @@ -61,7 +90,26 @@ /obj/structure/knock_tear/move_crushed(atom/movable/pusher, force = MOVE_FORCE_DEFAULT, direction) return FALSE -/obj/structure/knock_tear/Destroy(force) //this shouldnt happen but hey +/obj/structure/knock_tear/Destroy(force) if(ascendee) ascendee = null return ..() + +/obj/effect/temp_visual/destabilising_tear + name = "destabilised tear" + icon = 'icons/obj/anomaly.dmi' + icon_state = "bhole3" + color = COLOR_VOID_PURPLE + light_color = COLOR_VOID_PURPLE + light_range = 20 + layer = HIGH_PIPE_LAYER + duration = 1 SECONDS + +/obj/effect/temp_visual/destabilising_tear/Initialize(mapload) + . = ..() + transform *= 3 + animate(src, transform = matrix().Scale(3.2), time = 0.15 SECONDS) + animate(transform = matrix().Scale(0.2), time = 0.75 SECONDS) + animate(transform = matrix().Scale(3, 0), time = 0.1 SECONDS) + animate(src, color = COLOR_WHITE, time = 0.25 SECONDS, flags = ANIMATION_PARALLEL) + animate(color = COLOR_VOID_PURPLE, time = 0.3 SECONDS) diff --git a/code/modules/spells/spell.dm b/code/modules/spells/spell.dm index 966f618376d..f03cd4927f8 100644 --- a/code/modules/spells/spell.dm +++ b/code/modules/spells/spell.dm @@ -180,11 +180,6 @@ to_chat(owner, span_warning("Some form of antimagic is preventing you from casting [src]!")) return FALSE - if(!(spell_requirements & SPELL_CASTABLE_WHILE_PHASED) && HAS_TRAIT(owner, TRAIT_MAGICALLY_PHASED)) - if(feedback) - to_chat(owner, span_warning("[src] cannot be cast unless you are completely manifested in the material plane!")) - return FALSE - if(!try_invoke(owner, feedback = feedback)) return FALSE diff --git a/code/modules/spells/spell_types/jaunt/_jaunt.dm b/code/modules/spells/spell_types/jaunt/_jaunt.dm index 4a94f03c041..207a7ed8b5b 100644 --- a/code/modules/spells/spell_types/jaunt/_jaunt.dm +++ b/code/modules/spells/spell_types/jaunt/_jaunt.dm @@ -62,7 +62,7 @@ var/obj/effect/dummy/phased_mob/jaunt = new jaunt_type(loc_override || get_turf(jaunter), jaunter) RegisterSignal(jaunt, COMSIG_MOB_EJECTED_FROM_JAUNT, PROC_REF(on_jaunt_exited)) - spell_requirements |= SPELL_CASTABLE_WHILE_PHASED + check_flags &= ~AB_CHECK_PHASED jaunter.add_traits(list(TRAIT_MAGICALLY_PHASED, TRAIT_RUNECHAT_HIDDEN, TRAIT_WEATHER_IMMUNE), REF(src)) // Don't do the feedback until we have runechat hidden. // Otherwise the text will follow the jaunt holder, which reveals where our caster is travelling. @@ -106,7 +106,7 @@ */ /datum/action/cooldown/spell/jaunt/proc/on_jaunt_exited(obj/effect/dummy/phased_mob/jaunt, mob/living/unjaunter) SHOULD_CALL_PARENT(TRUE) - spell_requirements &= ~SPELL_CASTABLE_WHILE_PHASED + check_flags |= AB_CHECK_PHASED unjaunter.remove_traits(list(TRAIT_MAGICALLY_PHASED, TRAIT_RUNECHAT_HIDDEN, TRAIT_WEATHER_IMMUNE), REF(src)) // This needs to happen at the end, after all the traits and stuff is handled SEND_SIGNAL(unjaunter, COMSIG_MOB_AFTER_EXIT_JAUNT, src) diff --git a/tgstation.dme b/tgstation.dme index eb031dc1720..bd01238c904 100644 --- a/tgstation.dme +++ b/tgstation.dme @@ -1049,6 +1049,7 @@ #include "code\datums\components\customizable_reagent_holder.dm" #include "code\datums\components\damage_aura.dm" #include "code\datums\components\deadchat_control.dm" +#include "code\datums\components\death_linked.dm" #include "code\datums\components\dejavu.dm" #include "code\datums\components\deployable.dm" #include "code\datums\components\drift.dm" @@ -1360,7 +1361,6 @@ #include "code\datums\elements\death_drops.dm" #include "code\datums\elements\death_explosion.dm" #include "code\datums\elements\death_gases.dm" -#include "code\datums\elements\death_linked.dm" #include "code\datums\elements\delete_on_drop.dm" #include "code\datums\elements\deliver_first.dm" #include "code\datums\elements\diggable.dm"