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.")