mirror of
https://github.com/Bubberstation/Bubberstation.git
synced 2026-08-26 22:50:26 +01:00
[MIRROR] Changelings can now speak through their decoy brain if placed in an MMI [MDB IGNORE] (#23879)
* Changelings can now speak through their decoy brain if placed in an MMI (#78342) ## About The Pull Request - If a changeling's decoy brain is placed in an MMI, they will now be prompted to speak through it. - They can speak through the decoy even if incapacitated or dead (or fake-dead). https://github.com/tgstation/tgstation/assets/51863163/804bd48a-c4b8-4feb-b021-019ea70e4b8e ## Why It's Good For The Game The oft-controversial ling MMI test has been brought up time and time again so I figure I throw my cards in for a solution. We want as few ways as possible for people to hard and fast discover whether someone is an antag, especially changling which is supposed to revel in paranoia. This soft-patches out a big way, the MMI test, in which you place a ling's brain in an MMI to determine if it's vestigial and therefore, a ling. Now the ling player can provide some benefit of the doubt by speaking through the brain as normal, appearing active while actually in their body still. ## Changelog 🆑 Melbert add: Changelings can now speak through their decoy brain if it is placed in an MMI, to maintain the illusion they are actually dead and have been debrained. /🆑 * Changelings can now speak through their decoy brain if placed in an MMI --------- Co-authored-by: MrMelbert <51863163+MrMelbert@users.noreply.github.com>
This commit is contained in:
@@ -178,11 +178,16 @@
|
||||
else
|
||||
RegisterSignal(living_mob, COMSIG_MOB_HUD_CREATED, PROC_REF(on_hud_created))
|
||||
|
||||
make_brain_decoy(living_mob)
|
||||
|
||||
/datum/antagonist/changeling/proc/make_brain_decoy(mob/living/ling)
|
||||
var/obj/item/organ/internal/brain/our_ling_brain = ling.get_organ_slot(ORGAN_SLOT_BRAIN)
|
||||
if(isnull(our_ling_brain) || our_ling_brain.decoy_override)
|
||||
return
|
||||
|
||||
// Brains are optional for lings.
|
||||
var/obj/item/organ/internal/brain/our_ling_brain = living_mob.get_organ_slot(ORGAN_SLOT_BRAIN)
|
||||
if(our_ling_brain)
|
||||
our_ling_brain.organ_flags &= ~ORGAN_VITAL
|
||||
our_ling_brain.decoy_override = TRUE
|
||||
// This is automatically cleared if the ling is.
|
||||
our_ling_brain.AddComponent(/datum/component/ling_decoy_brain, src)
|
||||
|
||||
/datum/antagonist/changeling/proc/generate_name()
|
||||
var/honorific
|
||||
@@ -226,15 +231,10 @@
|
||||
QDEL_NULL(lingchemdisplay)
|
||||
QDEL_NULL(lingstingdisplay)
|
||||
|
||||
// The old body's brain still remains a decoy, I guess?
|
||||
|
||||
/datum/antagonist/changeling/on_removal()
|
||||
remove_changeling_powers(include_innate = TRUE)
|
||||
if(!iscarbon(owner.current))
|
||||
return
|
||||
var/mob/living/carbon/carbon_owner = owner.current
|
||||
var/obj/item/organ/internal/brain/not_ling_brain = carbon_owner.get_organ_slot(ORGAN_SLOT_BRAIN)
|
||||
if(not_ling_brain && (not_ling_brain.decoy_override != initial(not_ling_brain.decoy_override)))
|
||||
not_ling_brain.organ_flags |= ORGAN_VITAL
|
||||
not_ling_brain.decoy_override = FALSE
|
||||
return ..()
|
||||
|
||||
/datum/antagonist/changeling/farewell()
|
||||
@@ -294,14 +294,18 @@
|
||||
adjust_chemicals((chem_recharge_rate - chem_recharge_slowdown) * delta_time)
|
||||
|
||||
/**
|
||||
* Signal proc for [COMSIG_LIVING_POST_FULLY_HEAL], getting admin-healed restores our chemicals.
|
||||
* Signal proc for [COMSIG_LIVING_POST_FULLY_HEAL]
|
||||
*/
|
||||
/datum/antagonist/changeling/proc/on_fullhealed(datum/source, heal_flags)
|
||||
/datum/antagonist/changeling/proc/on_fullhealed(mob/living/source, heal_flags)
|
||||
SIGNAL_HANDLER
|
||||
|
||||
// Aheal restores all chemicals
|
||||
if(heal_flags & HEAL_ADMIN)
|
||||
adjust_chemicals(INFINITY)
|
||||
|
||||
// Makes sure the brain, if recreated, is a decoy as expected
|
||||
make_brain_decoy(source)
|
||||
|
||||
/**
|
||||
* Signal proc for [COMSIG_MOB_MIDDLECLICKON] and [COMSIG_MOB_ALTCLICKON].
|
||||
* Allows the changeling to sting people with a click.
|
||||
|
||||
@@ -0,0 +1,140 @@
|
||||
/datum/action/changeling/mmi_talk
|
||||
name = "MMI Talk"
|
||||
desc = "Our decoy brain has been implanted into a Man-Machine Interface. \
|
||||
In order to maintain our secrecy, we can speak through the decoy as if a normal brain. \
|
||||
The decoy brain will relay speech it hears to you in purple."
|
||||
button_icon = 'icons/obj/assemblies/assemblies.dmi'
|
||||
button_icon_state = "mmi_off"
|
||||
dna_cost = CHANGELING_POWER_UNOBTAINABLE
|
||||
ignores_fakedeath = TRUE // Can be used while fake dead
|
||||
req_stat = DEAD // Can be used while real dead too
|
||||
|
||||
/**
|
||||
* Reference to the brain we're talking through.
|
||||
*
|
||||
* Set when created via the ling decoy component.
|
||||
* If the brain ends up being qdelled, this action will also be qdelled, and thus this ref is cleared.
|
||||
*/
|
||||
VAR_FINAL/obj/item/organ/internal/brain/brain_ref
|
||||
|
||||
/// A map view of the area around the MMI.
|
||||
VAR_FINAL/atom/movable/screen/map_view/mmi_view
|
||||
/// The background for the MMI map view.
|
||||
VAR_FINAL/atom/movable/screen/background/mmi_view_background
|
||||
/// The key that the map view uses.
|
||||
VAR_FINAL/mmi_view_key
|
||||
/// A movement detector that updates the map view when the MMI moves around.
|
||||
VAR_FINAL/datum/movement_detector/update_view_tracker
|
||||
|
||||
/datum/action/changeling/mmi_talk/Destroy()
|
||||
brain_ref = null
|
||||
QDEL_NULL(mmi_view)
|
||||
QDEL_NULL(mmi_view_background)
|
||||
QDEL_NULL(update_view_tracker)
|
||||
return ..()
|
||||
|
||||
/datum/action/changeling/mmi_talk/Remove(mob/remove_from)
|
||||
. = ..()
|
||||
SStgui.close_uis(src)
|
||||
|
||||
/datum/action/changeling/mmi_talk/can_sting(mob/living/user, mob/living/target)
|
||||
. = ..()
|
||||
if(!.)
|
||||
return FALSE
|
||||
// This generally shouldn't happen, but just in case
|
||||
if(isnull(brain_ref))
|
||||
stack_trace("[type] can_sting was called with a null brain!")
|
||||
return FALSE
|
||||
if(!istype(brain_ref.loc, /obj/item/mmi))
|
||||
stack_trace("[type] can_sting was called with a brain not located in an MMI!")
|
||||
return FALSE
|
||||
return TRUE
|
||||
|
||||
/datum/action/changeling/mmi_talk/sting_action(mob/living/user, mob/living/target)
|
||||
..()
|
||||
ui_interact(user)
|
||||
return TRUE
|
||||
|
||||
/datum/action/changeling/mmi_talk/ui_state(mob/user)
|
||||
return GLOB.always_state
|
||||
|
||||
/datum/action/changeling/mmi_talk/ui_status(mob/user, datum/ui_state/state)
|
||||
if(user != owner)
|
||||
return UI_CLOSE
|
||||
return ..()
|
||||
|
||||
/datum/action/changeling/mmi_talk/ui_static_data(mob/user)
|
||||
var/list/data = list()
|
||||
data["mmi_view"] = mmi_view_key
|
||||
return data
|
||||
|
||||
/datum/action/changeling/mmi_talk/ui_interact(mob/user, datum/tgui/ui)
|
||||
if(isnull(mmi_view_key))
|
||||
// it's worth noting a ling could have multiple of these actions.
|
||||
mmi_view_key = "ling_mmi_[REF(src)]_view"
|
||||
// Generate background
|
||||
mmi_view_background = new()
|
||||
mmi_view_background.assigned_map = mmi_view_key
|
||||
mmi_view_background.del_on_map_removal = FALSE
|
||||
mmi_view_background.fill_rect(1, 1, 5, 5)
|
||||
// Generate map view
|
||||
mmi_view = new()
|
||||
mmi_view.generate_view(mmi_view_key)
|
||||
// Generate movement detector (to update the view on MMI movement)
|
||||
update_view_tracker = new(brain_ref, CALLBACK(src, PROC_REF(update_mmi_view)))
|
||||
|
||||
// Shows the view to the user foremost
|
||||
mmi_view.display_to(user)
|
||||
user.client.register_map_obj(mmi_view_background)
|
||||
update_mmi_view()
|
||||
// Makes the MMI relay heard messages
|
||||
if(!HAS_TRAIT_FROM(brain_ref.loc, TRAIT_HEARING_SENSITIVE, REF(src)))
|
||||
var/obj/item/mmi/mmi = brain_ref.loc
|
||||
mmi.become_hearing_sensitive(REF(src))
|
||||
RegisterSignal(mmi, COMSIG_MOVABLE_HEAR, PROC_REF(relay_hearing))
|
||||
// Actually open the UI
|
||||
ui = SStgui.try_update_ui(user, src, ui)
|
||||
if(!ui)
|
||||
ui = new(user, src, "LingMMITalk")
|
||||
ui.open()
|
||||
|
||||
/datum/action/changeling/mmi_talk/ui_close(mob/user)
|
||||
var/obj/item/mmi/mmi = brain_ref.loc
|
||||
UnregisterSignal(mmi, COMSIG_MOVABLE_HEAR)
|
||||
mmi.lose_hearing_sensitivity(REF(src))
|
||||
|
||||
/datum/action/changeling/mmi_talk/ui_act(action, list/params, datum/tgui/ui, datum/ui_state/state)
|
||||
. = ..()
|
||||
if(.)
|
||||
return TRUE
|
||||
|
||||
if(action != "send_mmi_message")
|
||||
return FALSE
|
||||
|
||||
var/obj/item/mmi/mmi = brain_ref.loc
|
||||
if(mmi.brainmob.stat != CONSCIOUS)
|
||||
to_chat(usr, span_warning("Our decoy brain is too damaged to speak."))
|
||||
else
|
||||
// Say will perform input sanitization and such for us
|
||||
mmi.brainmob.say(params["message"], sanitize = TRUE)
|
||||
return TRUE
|
||||
|
||||
/// Used in callbacks to update the map view when the MMI moves.
|
||||
/datum/action/changeling/mmi_talk/proc/update_mmi_view()
|
||||
mmi_view.vis_contents.Cut()
|
||||
for(var/turf/visible_turf in view(2, get_turf(brain_ref)))
|
||||
mmi_view.vis_contents += visible_turf
|
||||
|
||||
/// Signal proc for [COMSIG_MOVABLE_HEAR] to relay stuff the MMI hears to the ling.
|
||||
/// Not super good, but it works.
|
||||
/datum/action/changeling/mmi_talk/proc/relay_hearing(obj/item/mmi/source, list/hear_args)
|
||||
SIGNAL_HANDLER
|
||||
|
||||
// We can likely already hear them, so do not bother
|
||||
if(can_see(owner, hear_args[HEARING_SPEAKER], 7))
|
||||
return
|
||||
|
||||
var/list/new_args = hear_args.Copy()
|
||||
new_args[HEARING_SPANS] |= "purple"
|
||||
new_args[HEARING_RANGE] = INFINITY // so we can hear it from any distance away
|
||||
owner.Hear(arglist(new_args))
|
||||
@@ -15,22 +15,16 @@
|
||||
..()
|
||||
to_chat(user, span_notice("You feel an itching, both inside and outside as your tissues knit and reknit."))
|
||||
var/mob/living/carbon/carbon_user = user
|
||||
if(length(carbon_user.get_missing_limbs()))
|
||||
var/got_limbs_back = length(carbon_user.get_missing_limbs()) >= 1
|
||||
carbon_user.fully_heal(HEAL_BODY)
|
||||
// Occurs after fully heal so the ling themselves can hear the sound effects (if deaf prior)
|
||||
if(got_limbs_back)
|
||||
playsound(user, 'sound/magic/demon_consume.ogg', 50, TRUE)
|
||||
carbon_user.visible_message(
|
||||
span_warning("[user]'s missing limbs reform, making a loud, grotesque sound!"),
|
||||
span_userdanger("Your limbs regrow, making a loud, crunchy sound and giving you great pain!"),
|
||||
span_hear("You hear organic matter ripping and tearing!"),
|
||||
)
|
||||
|
||||
carbon_user.emote("scream")
|
||||
|
||||
carbon_user.fully_heal(HEAL_BODY)
|
||||
|
||||
// Make sure the brain's nonvital
|
||||
// Shouldn't be necessary but you can never be certain with lingcode
|
||||
var/obj/item/organ/internal/brain/replacement_brain = user.get_organ_slot(ORGAN_SLOT_BRAIN)
|
||||
replacement_brain.organ_flags &= ~ORGAN_VITAL
|
||||
replacement_brain.decoy_override = TRUE
|
||||
|
||||
return TRUE
|
||||
|
||||
@@ -57,7 +57,7 @@
|
||||
if(newbrain.suicided)
|
||||
to_chat(user, span_warning("[newbrain] is completely useless."))
|
||||
return
|
||||
if(!newbrain.brainmob?.mind || !newbrain.brainmob)
|
||||
if(!newbrain.brainmob)
|
||||
var/install = tgui_alert(user, "[newbrain] is inactive, slot it in anyway?", "Installing Brain", list("Yes", "No"))
|
||||
if(install != "Yes")
|
||||
return
|
||||
@@ -73,7 +73,7 @@
|
||||
if(!user.transferItemToLoc(O, src))
|
||||
return
|
||||
var/mob/living/brain/B = newbrain.brainmob
|
||||
if(!B.key)
|
||||
if(!B.key && !newbrain.decoy_override)
|
||||
B.notify_ghost_cloning("Someone has put your brain in a MMI!", source = src)
|
||||
user.visible_message(span_notice("[user] sticks \a [newbrain] into [src]."), span_notice("[src]'s indicator light turn on as you insert [newbrain]."))
|
||||
|
||||
@@ -241,14 +241,23 @@
|
||||
. = ..()
|
||||
if(radio)
|
||||
. += span_notice("There is a switch to toggle the radio system [radio.is_on() ? "off" : "on"].[brain ? " It is currently being covered by [brain]." : null]")
|
||||
if(brainmob)
|
||||
var/mob/living/brain/B = brainmob
|
||||
if(!B.key || !B.mind || B.stat == DEAD)
|
||||
. += span_warning("\The [src] indicates that the brain is completely unresponsive.")
|
||||
else if(!B.client)
|
||||
. += span_warning("\The [src] indicates that the brain is currently inactive; it might change.")
|
||||
|
||||
if(!isnull(brain))
|
||||
// It's dead, show it as much
|
||||
if((brain.organ_flags & ORGAN_FAILING) || brainmob?.stat == DEAD)
|
||||
if(brain.suicided || (brainmob && HAS_TRAIT(brainmob, TRAIT_SUICIDED)))
|
||||
. += span_warning("[src] indicator light is red.")
|
||||
else
|
||||
. += span_warning("[src] indicator light is yellow - perhaps you should check the brain for damage.")
|
||||
// If we have a client, OR it's a decoy brain, show as active
|
||||
else if(brain.decoy_override || brainmob?.client)
|
||||
. += span_notice("[src] indicates that the brain is active.")
|
||||
// If we have a brainmob and it has a mind, it may just be DC'd
|
||||
else if(brainmob?.mind)
|
||||
. += span_warning("[src] indicates that the brain is currently inactive; it might change.")
|
||||
// No brainmob, no mind, and not a decoy, it's a dead brain
|
||||
else
|
||||
. += span_notice("\The [src] indicates that the brain is active.")
|
||||
. += span_warning("[src] indicates that the brain is completely unresponsive.")
|
||||
|
||||
/obj/item/mmi/relaymove(mob/living/user, direction)
|
||||
return //so that the MMI won't get a warning about not being able to move if it tries to move
|
||||
@@ -259,6 +268,10 @@
|
||||
if(user)
|
||||
to_chat(user, span_warning("\The [src] indicates that there is no mind present!"))
|
||||
return FALSE
|
||||
if(brain.decoy_override)
|
||||
if(user)
|
||||
to_chat(user, span_warning("This [name] does not seem to fit!"))
|
||||
return FALSE
|
||||
if(!B.key || !B.mind)
|
||||
if(user)
|
||||
to_chat(user, span_warning("\The [src] indicates that their mind is completely unresponsive!"))
|
||||
|
||||
@@ -54,17 +54,23 @@
|
||||
brain_owner.update_body_parts()
|
||||
return
|
||||
|
||||
// Not a ling? Now you get to assume direct control.
|
||||
if(brainmob)
|
||||
if(brain_owner.key)
|
||||
brain_owner.ghostize()
|
||||
// If it's a ling decoy brain, nothing to transfer, just throw it out
|
||||
if(decoy_override)
|
||||
if(brainmob?.key)
|
||||
stack_trace("Decoy override brain with a key assigned - This should never happen.")
|
||||
|
||||
if(brainmob.mind)
|
||||
brainmob.mind.transfer_to(brain_owner)
|
||||
// Not a ling - assume direct control
|
||||
else
|
||||
brain_owner.key = brainmob.key
|
||||
if(brain_owner.key)
|
||||
brain_owner.ghostize()
|
||||
|
||||
brain_owner.set_suicide(HAS_TRAIT(brainmob, TRAIT_SUICIDED))
|
||||
if(brainmob.mind)
|
||||
brainmob.mind.transfer_to(brain_owner)
|
||||
else
|
||||
brain_owner.key = brainmob.key
|
||||
|
||||
brain_owner.set_suicide(HAS_TRAIT(brainmob, TRAIT_SUICIDED))
|
||||
|
||||
QDEL_NULL(brainmob)
|
||||
else
|
||||
@@ -126,8 +132,13 @@
|
||||
|
||||
/obj/item/organ/internal/brain/proc/transfer_identity(mob/living/L)
|
||||
name = "[L.name]'s [initial(name)]"
|
||||
if(brainmob || decoy_override)
|
||||
return
|
||||
if(brainmob)
|
||||
if(!decoy_override)
|
||||
return
|
||||
|
||||
// it's just a dummy, throw it out
|
||||
QDEL_NULL(brainmob)
|
||||
|
||||
if(!L.mind)
|
||||
return
|
||||
brainmob = new(src)
|
||||
@@ -146,9 +157,10 @@
|
||||
// Hack, fucked dna needs to follow the brain to prevent memes, so we need to copy over the trait sources and shit
|
||||
for(var/source in GET_TRAIT_SOURCES(L, TRAIT_BADDNA))
|
||||
ADD_TRAIT(brainmob, TRAIT_BADDNA, source)
|
||||
if(L.mind && L.mind.current)
|
||||
|
||||
if(L.mind && L.mind.current && !decoy_override)
|
||||
L.mind.transfer_to(brainmob)
|
||||
to_chat(brainmob, span_notice("You feel slightly disoriented. That's normal when you're just a brain."))
|
||||
to_chat(brainmob, span_notice("You feel slightly disoriented. That's normal when you're just a brain."))
|
||||
|
||||
/obj/item/organ/internal/brain/attackby(obj/item/O, mob/user, params)
|
||||
user.changeNext_move(CLICK_CD_MELEE)
|
||||
@@ -214,7 +226,7 @@
|
||||
if(suicided)
|
||||
. += span_info("It's started turning slightly grey. They must not have been able to handle the stress of it all.")
|
||||
return
|
||||
if((brainmob && (brainmob.client || brainmob.get_ghost())) || decoy_override)
|
||||
if(brainmob && (decoy_override || brainmob.client || brainmob.get_ghost()))
|
||||
if(organ_flags & ORGAN_FAILING)
|
||||
. += span_info("It seems to still have a bit of energy within it, but it's rather damaged... You may be able to restore it with some <b>mannitol</b>.")
|
||||
else if(damage >= BRAIN_DAMAGE_DEATH*0.5)
|
||||
@@ -262,13 +274,11 @@
|
||||
..()
|
||||
|
||||
/obj/item/organ/internal/brain/Destroy() //copypasted from MMIs.
|
||||
if(brainmob)
|
||||
QDEL_NULL(brainmob)
|
||||
QDEL_NULL(brainmob)
|
||||
QDEL_LIST(traumas)
|
||||
|
||||
destroy_all_skillchips()
|
||||
if(owner?.mind) //You aren't allowed to return to brains that don't exist
|
||||
owner.mind.set_current(null)
|
||||
owner?.mind?.set_current(null) //You aren't allowed to return to brains that don't exist
|
||||
return ..()
|
||||
|
||||
/obj/item/organ/internal/brain/on_life(seconds_per_tick, times_fired)
|
||||
|
||||
@@ -161,6 +161,7 @@
|
||||
#include "leash.dm"
|
||||
#include "lesserform.dm"
|
||||
#include "limbsanity.dm"
|
||||
#include "ling_decap.dm"
|
||||
#include "liver.dm"
|
||||
#include "load_map_security.dm"
|
||||
#include "lungs.dm"
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
/// Test lings don't die when decapitated.
|
||||
/datum/unit_test/ling_decap
|
||||
|
||||
/datum/unit_test/ling_decap/Run()
|
||||
var/mob/living/carbon/human/ling = allocate(/mob/living/carbon/human/consistent)
|
||||
ling.mind_initialize()
|
||||
ling.mind.add_antag_datum(/datum/antagonist/changeling)
|
||||
|
||||
var/obj/item/bodypart/head/noggin = ling.get_bodypart(BODY_ZONE_HEAD)
|
||||
noggin.dismember()
|
||||
TEST_ASSERT_NULL(ling.get_bodypart(BODY_ZONE_HEAD), "Changeling failed to be decapitated.")
|
||||
TEST_ASSERT_NULL(noggin.brainmob.mind, "Changeling's mind was moved to their head after decapitation, but it should have remained in their body.")
|
||||
|
||||
var/obj/item/organ/internal/brain/oldbrain = noggin.brain
|
||||
noggin.drop_organs()
|
||||
TEST_ASSERT_NULL(noggin.brain, "Changeling's head failed to drop its brain.")
|
||||
TEST_ASSERT_NULL(oldbrain.brainmob.mind, "Changeling's mind was moved to their brain after decapitation and organ dropping, but it should have remained in their body.")
|
||||
|
||||
TEST_ASSERT_EQUAL(ling.stat, CONSCIOUS, "Changeling was not conscious after losing their head.")
|
||||
|
||||
// Cleanup
|
||||
qdel(noggin)
|
||||
for(var/obj/item/organ/leftover in ling.loc)
|
||||
qdel(leftover)
|
||||
|
||||
/// Tests people get decapitated properly.
|
||||
/datum/unit_test/normal_decap
|
||||
|
||||
/datum/unit_test/normal_decap/Run()
|
||||
var/mob/living/carbon/human/normal_guy = allocate(/mob/living/carbon/human/consistent)
|
||||
normal_guy.mind_initialize()
|
||||
var/my_guys_mind = normal_guy.mind
|
||||
|
||||
var/obj/item/bodypart/head/noggin = normal_guy.get_bodypart(BODY_ZONE_HEAD)
|
||||
noggin.dismember()
|
||||
TEST_ASSERT_EQUAL(noggin.brainmob.mind, my_guys_mind, "Dummy's mind was not moved to their head after decapitation.")
|
||||
|
||||
var/obj/item/organ/internal/brain/oldbrain = noggin.brain
|
||||
noggin.drop_organs()
|
||||
TEST_ASSERT_EQUAL(oldbrain.brainmob.mind, my_guys_mind, "Dummy's mind was not moved to their brain after being removed from their head.")
|
||||
|
||||
// Cleanup
|
||||
qdel(noggin)
|
||||
for(var/obj/item/organ/leftover in normal_guy.loc)
|
||||
qdel(leftover)
|
||||
Reference in New Issue
Block a user