diff --git a/code/__DEFINES/dcs/signals/signals_mob/signals_mob_main.dm b/code/__DEFINES/dcs/signals/signals_mob/signals_mob_main.dm
index 750b69fb5a6..064f5303bf2 100644
--- a/code/__DEFINES/dcs/signals/signals_mob/signals_mob_main.dm
+++ b/code/__DEFINES/dcs/signals/signals_mob/signals_mob_main.dm
@@ -172,6 +172,16 @@
///from living/flash_act(), when a mob is successfully flashed.
#define COMSIG_MOB_FLASHED "mob_flashed"
+/// from /obj/item/assembly/flash/flash_carbon, to the mob flashing another carbon
+#define COMSIG_MOB_FLASHED_CARBON "mob_flashed_carbon"
+ /// Return to override deviation to be full deviation (fail the flash, usually)
+ #define DEVIATION_OVERRIDE_FULL (1<<0)
+ /// Return to override deviation to be partial deviation
+ #define DEVIATION_OVERRIDE_PARTIAL (1<<1)
+ /// Return to override deviation to be no deviation
+ #define DEVIATION_OVERRIDE_NONE (1<<2)
+ /// Return to stop the flash entirely
+ #define STOP_FLASH (1<<3)
/// from mob/get_status_tab_items(): (list/items)
#define COMSIG_MOB_GET_STATUS_TAB_ITEMS "mob_get_status_tab_items"
diff --git a/code/__DEFINES/mobs.dm b/code/__DEFINES/mobs.dm
index a6ffeb0ac91..00c5e037a77 100644
--- a/code/__DEFINES/mobs.dm
+++ b/code/__DEFINES/mobs.dm
@@ -880,6 +880,14 @@ GLOBAL_LIST_INIT(layers_to_offset, list(
#define READING_CHECK_LITERACY (1<<0)
#define READING_CHECK_LIGHT (1<<1)
+// Flash deviation defines
+/// No deviation at all. Flashed from the front or front-left/front-right. Alternatively, flashed in direct view.
+#define DEVIATION_NONE 0
+/// Partial deviation. Flashed from the side. Alternatively, flashed out the corner of your eyes.
+#define DEVIATION_PARTIAL 1
+/// Full deviation. Flashed from directly behind or behind-left/behind-rack. Not flashed at all.
+#define DEVIATION_FULL 2
+
/// In dynamic human icon gen we don't replace the held item.
#define NO_REPLACE 0
diff --git a/code/datums/mind/antag.dm b/code/datums/mind/antag.dm
index 2061f934d5d..b7ed150784c 100644
--- a/code/datums/mind/antag.dm
+++ b/code/datums/mind/antag.dm
@@ -201,7 +201,7 @@
else if(IS_REVOLUTIONARY(creator))
var/datum/antagonist/rev/converter = creator.mind.has_antag_datum(/datum/antagonist/rev,TRUE)
- converter.add_revolutionary(src,FALSE)
+ converter.add_revolutionary(src, stun = FALSE, mute = FALSE)
else if(IS_NUKE_OP(creator))
var/datum/antagonist/nukeop/converter = creator.mind.has_antag_datum(/datum/antagonist/nukeop,TRUE)
diff --git a/code/modules/antagonists/brainwashing/brainwashing.dm b/code/modules/antagonists/brainwashing/brainwashing.dm
index 6f87930b818..51372af4933 100644
--- a/code/modules/antagonists/brainwashing/brainwashing.dm
+++ b/code/modules/antagonists/brainwashing/brainwashing.dm
@@ -22,8 +22,9 @@
var/end_message = "."
var/rendered = begin_message + obj_message + end_message
deadchat_broadcast(rendered, "[L]", follow_target = L, turf_target = get_turf(L), message_type=DEADCHAT_ANNOUNCEMENT)
- if(prob(1) || check_holidays(APRIL_FOOLS))
- L.say("You son of a bitch! I'm in.", forced = "That son of a bitch! They're in.")
+ if(check_holidays(APRIL_FOOLS))
+ // Note: most of the time you're getting brainwashed you're unconscious
+ L.say("You son of a bitch! I'm in.", forced = "That son of a bitch! They're in. (April Fools)")
/datum/antagonist/brainwashed
name = "\improper Brainwashed Victim"
diff --git a/code/modules/antagonists/cult/runes.dm b/code/modules/antagonists/cult/runes.dm
index 007328b3c9a..18583ba45b8 100644
--- a/code/modules/antagonists/cult/runes.dm
+++ b/code/modules/antagonists/cult/runes.dm
@@ -262,30 +262,45 @@ structure_check() searches for nearby cultist structures required for the invoca
to_chat(M, span_warning("Something is shielding [convertee]'s mind!"))
log_game("Offer rune with [convertee] on it failed - convertee had anti-magic.")
return FALSE
+
var/brutedamage = convertee.getBruteLoss()
var/burndamage = convertee.getFireLoss()
if(brutedamage || burndamage)
convertee.adjustBruteLoss(-(brutedamage * 0.75))
convertee.adjustFireLoss(-(burndamage * 0.75))
- convertee.visible_message("[convertee] writhes in pain \
- [brutedamage || burndamage ? "even as [convertee.p_their()] wounds heal and close" : "as the markings below [convertee.p_them()] glow a bloody red"]!", \
- span_cultlarge("AAAAAAAAAAAAAA-"))
- convertee.mind?.add_antag_datum(/datum/antagonist/cult)
- convertee.Unconscious(100)
+
+ convertee.visible_message(
+ span_warning("[convertee] writhes in pain [(brutedamage || burndamage) \
+ ? "even as [convertee.p_their()] wounds heal and close" \
+ : "as the markings below [convertee.p_them()] glow a bloody red"]!"),
+ span_cultlarge("AAAAAAAAAAAAAA-"),
+ )
+
+ // We're not guaranteed to be a human but we'll cast here since we use it in a few branches
+ var/mob/living/carbon/human/human_convertee = convertee
+
+ if(check_holidays(APRIL_FOOLS) && prob(10))
+ convertee.Paralyze(10 SECONDS)
+ if(istype(human_convertee))
+ human_convertee.force_say()
+ convertee.say("You son of a bitch! I'm in.", forced = "That son of a bitch! They're in. (April Fools)")
+
+ else
+ convertee.Unconscious(10 SECONDS)
+
new /obj/item/melee/cultblade/dagger(get_turf(src))
convertee.mind.special_role = ROLE_CULTIST
- to_chat(convertee, "Your blood pulses. Your head throbs. The world goes red. All at once you are aware of a horrible, horrible, truth. The veil of reality has been ripped away \
- and something evil takes root.")
- to_chat(convertee, "Assist your new compatriots in their dark dealings. Your goal is theirs, and theirs is yours. You serve the Geometer above all else. Bring it back.\
- ")
- if(ishuman(convertee))
- var/mob/living/carbon/human/H = convertee
- H.uncuff()
- H.remove_status_effect(/datum/status_effect/speech/slurring/cult)
- H.remove_status_effect(/datum/status_effect/speech/stutter)
- if(prob(1) || check_holidays(APRIL_FOOLS))
- H.say("You son of a bitch! I'm in.", forced = "That son of a bitch! They're in.")
+ to_chat(convertee, span_cultitalic("Your blood pulses. Your head throbs. The world goes red. \
+ All at once you are aware of a horrible, horrible, truth. The veil of reality has been ripped away \
+ and something evil takes root."))
+ to_chat(convertee, span_cultitalic("Assist your new compatriots in their dark dealings. \
+ Your goal is theirs, and theirs is yours. You serve the Geometer above all else. Bring it back."))
+
+ if(istype(human_convertee))
+ human_convertee.uncuff()
+ human_convertee.remove_status_effect(/datum/status_effect/speech/slurring/cult)
+ human_convertee.remove_status_effect(/datum/status_effect/speech/stutter)
if(isshade(convertee))
convertee.icon_state = "shade_cult"
convertee.name = convertee.real_name
diff --git a/code/modules/antagonists/revolution/revolution.dm b/code/modules/antagonists/revolution/revolution.dm
index d8093b220fb..a4646e373f6 100644
--- a/code/modules/antagonists/revolution/revolution.dm
+++ b/code/modules/antagonists/revolution/revolution.dm
@@ -184,6 +184,51 @@
S.Remove(C)
return ..()
+/datum/antagonist/rev/head/apply_innate_effects(mob/living/mob_override)
+ . = ..()
+ var/mob/living/real_mob = mob_override || owner.current
+ RegisterSignal(real_mob, COMSIG_MOB_FLASHED_CARBON, PROC_REF(on_flash))
+
+/datum/antagonist/rev/head/remove_innate_effects(mob/living/mob_override)
+ . = ..()
+ var/mob/living/real_mob = mob_override || owner.current
+ UnregisterSignal(real_mob, COMSIG_MOB_FLASHED_CARBON)
+
+/// Signal proc for [COMSIG_MOB_FLASHED_CARBON].
+/// The bread and butter of head revolutionary conversions.
+/datum/antagonist/rev/head/proc/on_flash(mob/living/source, mob/living/carbon/flashed, obj/item/assembly/flash/flash, deviation)
+ SIGNAL_HANDLER
+
+ // Always partial flash at the very least
+ . = (deviation == DEVIATION_FULL) ? DEVIATION_OVERRIDE_PARTIAL : NONE
+
+ if(flashed.stat == DEAD)
+ return .
+ if(flashed.stat != CONSCIOUS)
+ to_chat(source, span_warning("[flashed.p_they(capitalized = TRUE)] must be conscious before you can convert [flashed.p_them()]!"))
+ return .
+
+ if(isnull(flashed.mind) || !GET_CLIENT(flashed))
+ to_chat(source, span_warning("[flashed]'s mind is so vacant that it is not susceptible to influence!"))
+ return .
+
+ var/holiday_meme_chance = check_holidays(APRIL_FOOLS) && prob(10)
+ if(add_revolutionary(flashed.mind, mute = !holiday_meme_chance)) // don't mute if we roll the meme holiday chance
+ if(holiday_meme_chance)
+ INVOKE_ASYNC(src, PROC_REF(_async_holiday_meme_say), flashed)
+ flash.times_used-- // Flashes are less likely to burn out for headrevs, when used for conversion
+ else
+ to_chat(source, span_warning("[flashed] seems resistant to [flash]!"))
+
+ return .
+
+/// Used / called async from [proc/on_flash] to deliver a funny meme line
+/datum/antagonist/rev/head/proc/_async_holiday_meme_say(mob/living/carbon/flashed)
+ if(ishuman(flashed))
+ var/mob/living/carbon/human/human_flashed = flashed
+ human_flashed.force_say()
+ flashed.say("You son of a bitch! I'm in.", forced = "That son of a bitch! They're in. (April Fools)")
+
/datum/antagonist/rev/head/antag_listing_name()
return ..() + "(Leader)"
@@ -228,11 +273,20 @@
return FALSE
return TRUE
-/datum/antagonist/rev/proc/add_revolutionary(datum/mind/rev_mind,stun = TRUE)
+/**
+ * Adds a new mind to our revoltuion
+ *
+ * * rev_mind - the mind we're adding
+ * * stun - If TRUE, we will flash act and apply a long stun when we're applied
+ * * mute - If TRUE, we will apply a mute when we're applied
+ */
+/datum/antagonist/rev/proc/add_revolutionary(datum/mind/rev_mind, stun = TRUE, mute = TRUE)
if(!can_be_converted(rev_mind.current))
return FALSE
- if(stun)
+
+ if(mute)
rev_mind.current.set_silence_if_lower(10 SECONDS)
+ if(stun)
rev_mind.current.flash_act(1, 1)
rev_mind.current.Stun(10 SECONDS)
diff --git a/code/modules/assembly/flash.dm b/code/modules/assembly/flash.dm
index f6428fb177a..693415176fb 100644
--- a/code/modules/assembly/flash.dm
+++ b/code/modules/assembly/flash.dm
@@ -1,13 +1,5 @@
#define CONFUSION_STACK_MAX_MULTIPLIER 2
-
-/// No deviation at all. Flashed from the front or front-left/front-right. Alternatively, flashed in direct view.
-#define DEVIATION_NONE 0
-/// Partial deviation. Flashed from the side. Alternatively, flashed out the corner of your eyes.
-#define DEVIATION_PARTIAL 1
-/// Full deviation. Flashed from directly behind or behind-left/behind-rack. Not flashed at all.
-#define DEVIATION_FULL 2
-
/obj/item/assembly/flash
name = "flash"
desc = "A powerful and versatile flashbulb device, with applications ranging from disorienting attackers to acting as visual receptors in robot production."
@@ -156,25 +148,26 @@
var/deviation = calculate_deviation(flashed, user || src)
- var/datum/antagonist/rev/head/converter = user?.mind?.has_antag_datum(/datum/antagonist/rev/head)
+ if(user)
+ var/sigreturn = SEND_SIGNAL(user, COMSIG_MOB_FLASHED_CARBON, flashed, src, deviation)
+ if(sigreturn & STOP_FLASH)
+ return
+
+ if(sigreturn & DEVIATION_OVERRIDE_FULL)
+ deviation = DEVIATION_FULL
+ else if(sigreturn & DEVIATION_OVERRIDE_PARTIAL)
+ deviation = DEVIATION_PARTIAL
+ else if(sigreturn & DEVIATION_OVERRIDE_NONE)
+ deviation = DEVIATION_NONE
//If you face away from someone they shouldnt notice any effects.
- if(deviation == DEVIATION_FULL && !converter)
+ if(deviation == DEVIATION_FULL)
return
if(targeted)
if(flashed.flash_act(1, 1))
flashed.set_confusion_if_lower(confusion_duration * CONFUSION_STACK_MAX_MULTIPLIER)
- // Special check for if we're a revhead. Special cases to attempt conversion.
- if(converter)
- // Did we try to flash them from behind?
- if(deviation == DEVIATION_FULL)
- // Headrevs can use a tacticool leaning technique so that they don't have to worry about facing for their conversions.
- to_chat(user, span_notice("You use the tacticool tier, lean over the shoulder technique to blind [flashed] with a flash!"))
- deviation = DEVIATION_PARTIAL
- // Convert them. Terribly.
- terrible_conversion_proc(flashed, user)
- visible_message(span_danger("[user] blinds [flashed] with the flash!"), span_userdanger("[user] blinds you with the flash!"))
+ visible_message(span_danger("[user] blinds [flashed] with the flash!"), span_userdanger("[user] blinds you with the flash!"))
//easy way to make sure that you can only long stun someone who is facing in your direction
flashed.adjustStaminaLoss(rand(80, 120) * (1 - (deviation * 0.5)))
flashed.Paralyze(rand(25, 50) * (1 - (deviation * 0.5)))
@@ -277,34 +270,6 @@
return
AOE_flash()
-/**
- * Converts the victim to revs
- *
- * Arguments:
- * * victim - Victim
- * * aggressor - Attacker
- */
-/obj/item/assembly/flash/proc/terrible_conversion_proc(mob/living/carbon/victim, mob/aggressor)
- if(!istype(victim) || victim.stat == DEAD)
- return
- if(!aggressor.mind)
- return
- if(!victim.client)
- to_chat(aggressor, span_warning("This mind is so vacant that it is not susceptible to influence!"))
- return
- if(victim.stat != CONSCIOUS)
- to_chat(aggressor, span_warning("They must be conscious before you can convert [victim.p_them()]!"))
- return
- //If this proc fires the mob must be a revhead
- var/datum/antagonist/rev/head/converter = aggressor.mind.has_antag_datum(/datum/antagonist/rev/head)
- if(converter.add_revolutionary(victim.mind))
- if(prob(1) || check_holidays(APRIL_FOOLS))
- victim.say("You son of a bitch! I'm in.", forced = "That son of a bitch! They're in.")
- times_used -- //Flashes less likely to burn out for headrevs when used for conversion
- else
- to_chat(aggressor, span_warning("This mind seems resistant to the flash!"))
-
-
/obj/item/assembly/flash/cyborg
/obj/item/assembly/flash/cyborg/attack(mob/living/M, mob/user)
@@ -415,6 +380,3 @@
M.adjust_pacifism(4 SECONDS)
#undef CONFUSION_STACK_MAX_MULTIPLIER
-#undef DEVIATION_NONE
-#undef DEVIATION_PARTIAL
-#undef DEVIATION_FULL
diff --git a/code/modules/unit_tests/_unit_tests.dm b/code/modules/unit_tests/_unit_tests.dm
index e4da75f5748..b4b7e976de0 100644
--- a/code/modules/unit_tests/_unit_tests.dm
+++ b/code/modules/unit_tests/_unit_tests.dm
@@ -181,6 +181,7 @@
#include "reagent_recipe_collisions.dm"
#include "reagent_transfer.dm"
#include "resist.dm"
+#include "rev_conversion.dm"
#include "say.dm"
#include "screenshot_antag_icons.dm"
#include "screenshot_basic.dm"
diff --git a/code/modules/unit_tests/rev_conversion.dm b/code/modules/unit_tests/rev_conversion.dm
new file mode 100644
index 00000000000..4f35193d172
--- /dev/null
+++ b/code/modules/unit_tests/rev_conversion.dm
@@ -0,0 +1,22 @@
+/// Tests that headrevs can convert people by clicking on them with flashes
+/datum/unit_test/revolution_conversion
+
+/datum/unit_test/revolution_conversion/Run()
+ var/mob/living/carbon/human/leader = allocate(/mob/living/carbon/human/consistent, run_loc_floor_bottom_left)
+ var/mob/living/carbon/human/peasant = allocate(/mob/living/carbon/human/consistent, run_loc_floor_bottom_left)
+
+ leader.mind_initialize()
+ leader.mock_client = new()
+ peasant.mind_initialize()
+ peasant.mock_client = new()
+
+ var/datum/antagonist/rev/head/lead_datum = leader.mind.add_antag_datum(/datum/antagonist/rev/head)
+ var/datum/team/revolution/revolution = lead_datum.get_team()
+
+ var/obj/item/assembly/flash/handheld/converter = allocate(/obj/item/assembly/flash/handheld)
+ leader.put_in_active_hand(converter, forced = TRUE)
+ leader.ClickOn(peasant)
+
+ TEST_ASSERT(peasant.IsParalyzed(), "Peasant was not paralyzed after being flashed by the leader") // Flash paralyze
+ TEST_ASSERT(peasant.IsStun(), "Peasant was not stunned after being converted by the leader") // Conversion stun
+ TEST_ASSERT_EQUAL(length(revolution.members), 2, "Expected revolution to have 2 members after the leader flashes the peasant.")