[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:
SkyratBot
2023-09-24 07:00:24 -04:00
committed by GitHub
co-authored by MrMelbert
parent bfa7a1201c
commit 2fa835185e
10 changed files with 382 additions and 48 deletions
@@ -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
+22 -9
View File
@@ -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!"))
+26 -16
View File
@@ -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)
+1
View File
@@ -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"
+45
View File
@@ -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)