From 57777bf9df7ad721c7065343acf9a87c2fb7a013 Mon Sep 17 00:00:00 2001
From: Mothblocks <35135081+Mothblocks@users.noreply.github.com>
Date: Sun, 26 Nov 2023 18:22:42 -0800
Subject: [PATCH] Blood brothers is now a single person conversion antagonist
---
.../signals/signals_mob/signals_mob_main.dm | 1 +
.../components/can_flash_from_behind.dm | 22 +++
code/datums/memory/general_memories.dm | 12 ++
.../dynamic/dynamic_rulesets_roundstart.dm | 47 +++----
code/modules/antagonists/brother/brother.dm | 132 +++++++++++-------
code/modules/antagonists/cult/cult.dm | 15 +-
.../antagonists/revolution/revolution.dm | 41 ++----
code/modules/mob/living/carbon/inventory.dm | 18 +++
tgstation.dme | 1 +
.../tgui/interfaces/AntagInfoBrother.tsx | 4 +-
10 files changed, 173 insertions(+), 120 deletions(-)
create mode 100644 code/datums/components/can_flash_from_behind.dm
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 b564a114b02cca..0ef458cece9580 100644
--- a/code/__DEFINES/dcs/signals/signals_mob/signals_mob_main.dm
+++ b/code/__DEFINES/dcs/signals/signals_mob/signals_mob_main.dm
@@ -212,6 +212,7 @@
/// Return to stop the flash entirely
#define STOP_FLASH (1<<3)
/// from /obj/item/assembly/flash/flash_carbon, to the mob flashing another carbon
+/// (mob/living/carbon/flashed, obj/item/assembly/flash/flash, deviation (from code/__DEFINES/mobs.dm))
#define COMSIG_MOB_SUCCESSFUL_FLASHED_CARBON "mob_success_flashed_carbon"
/// from mob/get_status_tab_items(): (list/items)
diff --git a/code/datums/components/can_flash_from_behind.dm b/code/datums/components/can_flash_from_behind.dm
new file mode 100644
index 00000000000000..c443d160dfbd30
--- /dev/null
+++ b/code/datums/components/can_flash_from_behind.dm
@@ -0,0 +1,22 @@
+/// This mob can flash others from behind and still get at least a partial
+// Component and not element because elements can't stack.
+// I don't want to have a bunch of helpers for that. We need to do this generally
+// because this keeps coming up.
+/datum/component/can_flash_from_behind
+ dupe_mode = COMPONENT_DUPE_SOURCES
+
+/datum/component/can_flash_from_behind/Initialize()
+ if (!ismob(parent))
+ return COMPONENT_INCOMPATIBLE
+
+/datum/component/can_flash_from_behind/RegisterWithParent()
+ RegisterSignal(parent, COMSIG_MOB_PRE_FLASHED_CARBON, PROC_REF(on_pre_flashed_carbon))
+
+/datum/component/can_flash_from_behind/UnregisterFromParent()
+ UnregisterSignal(parent, COMSIG_MOB_PRE_FLASHED_CARBON)
+
+/datum/component/can_flash_from_behind/proc/on_pre_flashed_carbon(source, flashed, flash, deviation)
+ SIGNAL_HANDLER
+
+ // Always partial flash at the very least
+ return (deviation == DEVIATION_FULL) ? DEVIATION_OVERRIDE_PARTIAL : NONE
diff --git a/code/datums/memory/general_memories.dm b/code/datums/memory/general_memories.dm
index 3e5eb05cf40269..c71014b63b46dc 100644
--- a/code/datums/memory/general_memories.dm
+++ b/code/datums/memory/general_memories.dm
@@ -915,6 +915,18 @@
"[antagonist_name] lifts an odd device to [protagonist_name]'s eyes and flashes him, imprinting murderous instructions.",
)
+/// Who converted into a blood brother
+/datum/memory/recruited_by_blood_brother
+
+/datum/memory/recruited_by_blood_brother/get_names()
+ return list("[protagonist_name] is converted into a blood brother by [antagonist_name]")
+
+/datum/memory/recruited_by_blood_brother/get_starts()
+ return list(
+ "[antagonist_name] acts just a bit too friendly with [protagonist_name], moments away from converting them into a blood brother.",
+ "[protagonist_name] is brought into [antagonist_name]'s life of crime and espionage.",
+ )
+
/// Saw someone play Russian Roulette.
/datum/memory/witnessed_gods_wrath
memory_flags = MEMORY_CHECK_BLINDNESS|MEMORY_SKIP_UNCONSCIOUS
diff --git a/code/game/gamemodes/dynamic/dynamic_rulesets_roundstart.dm b/code/game/gamemodes/dynamic/dynamic_rulesets_roundstart.dm
index a90eba738c146c..767087467233c8 100644
--- a/code/game/gamemodes/dynamic/dynamic_rulesets_roundstart.dm
+++ b/code/game/gamemodes/dynamic/dynamic_rulesets_roundstart.dm
@@ -112,45 +112,34 @@ GLOBAL_VAR_INIT(revolutionary_win, FALSE)
JOB_AI,
JOB_CYBORG,
)
- required_candidates = 2
- weight = 2
- cost = 12
+ weight = 5
+ cost = 8
scaling_cost = 15
requirements = list(40,30,30,20,20,15,15,15,10,10)
- antag_cap = 2 // Can pick 3 per team, but rare enough it doesn't matter.
- var/list/datum/team/brother_team/pre_brother_teams = list()
- var/const/min_team_size = 2
-
-/datum/dynamic_ruleset/roundstart/traitorbro/forget_startup()
- pre_brother_teams = list()
- return ..()
+ antag_cap = 1
/datum/dynamic_ruleset/roundstart/traitorbro/pre_execute(population)
. = ..()
- var/num_teams = (get_antag_cap(population)/min_team_size) * (scaled_times + 1) // 1 team per scaling
- for(var/j = 1 to num_teams)
- if(candidates.len < min_team_size || candidates.len < required_candidates)
+
+ for (var/_ in 1 to get_antag_cap(population) * (scaled_times + 1))
+ var/mob/candidate = pick_n_take(candidates)
+ if (isnull(candidate))
break
- var/datum/team/brother_team/team = new
- var/team_size = prob(10) ? min(3, candidates.len) : 2
- for(var/k = 1 to team_size)
- var/mob/bro = pick_n_take(candidates)
- assigned += bro.mind
- team.add_member(bro.mind)
- bro.mind.special_role = "brother"
- bro.mind.restricted_roles = restricted_roles
- GLOB.pre_setup_antags += bro.mind
- pre_brother_teams += team
+
+ assigned += candidate.mind
+ candidate.mind.restricted_roles = restricted_roles
+ GLOB.pre_setup_antags += candidate.mind
+
return TRUE
/datum/dynamic_ruleset/roundstart/traitorbro/execute()
- for(var/datum/team/brother_team/team in pre_brother_teams)
- team.pick_meeting_area()
+ for (var/datum/mind/mind in assigned)
+ var/datum/team/brother_team/team = new
+ team.add_member(mind)
team.forge_brother_objectives()
- for(var/datum/mind/M in team.members)
- M.add_antag_datum(/datum/antagonist/brother, team)
- GLOB.pre_setup_antags -= M
- team.update_name()
+ mind.add_antag_datum(/datum/antagonist/brother, team)
+ GLOB.pre_setup_antags -= mind
+
return TRUE
//////////////////////////////////////////////
diff --git a/code/modules/antagonists/brother/brother.dm b/code/modules/antagonists/brother/brother.dm
index 67fffb4fa19fa2..baf6b30f6b178f 100644
--- a/code/modules/antagonists/brother/brother.dm
+++ b/code/modules/antagonists/brother/brother.dm
@@ -7,9 +7,10 @@
hijack_speed = 0.5
ui_name = "AntagInfoBrother"
suicide_cry = "FOR MY BROTHER!!"
- var/datum/team/brother_team/team
antag_moodlet = /datum/mood_event/focused
hardcore_random_bonus = TRUE
+ VAR_PRIVATE
+ datum/team/brother_team/team
/datum/antagonist/brother/create_team(datum/team/brother_team/new_team)
if(!new_team)
@@ -25,12 +26,64 @@
objectives += team.objectives
owner.special_role = special_role
finalize_brother()
+
+ var/is_first_brother = team.members.len == 1
+ team.brothers_left -= 1
+
+ if (is_first_brother || team.brothers_left > 0)
+ var/mob/living/carbon/carbon_owner = owner.current
+ if (istype(carbon_owner))
+ carbon_owner.equip_conspicuous_item(new /obj/item/assembly/flash)
+ carbon_owner.AddComponentFrom(REF(src), /datum/component/can_flash_from_behind)
+ RegisterSignal(carbon_owner, COMSIG_MOB_SUCCESSFUL_FLASHED_CARBON, PROC_REF(on_mob_successful_flashed_carbon))
+
+ if (!is_first_brother)
+ to_chat(carbon_owner, span_boldwarning("The Syndicate have higher expectations from you than others. They have granted you an extra flash to convert one other person."))
+
return ..()
/datum/antagonist/brother/on_removal()
owner.special_role = null
+ owner.RemoveComponentSource(REF(src), /datum/component/can_flash_from_behind)
+ UnregisterSignal(owner, COMSIG_MOB_SUCCESSFUL_FLASHED_CARBON)
+
return ..()
+/datum/antagonist/brother/proc/on_mob_successful_flashed_carbon(mob/living/source, mob/living/carbon/flashed, obj/item/assembly/flash/flash)
+ SIGNAL_HANDLER
+
+ if (flashed.stat == DEAD)
+ return
+
+ if (flashed.stat != CONSCIOUS)
+ flashed.balloon_alert(source, "unconscious!")
+ return
+
+ if (isnull(flashed.mind) || !GET_CLIENT(flashed))
+ flashed.balloon_alert(source, "[flashed.p_their()] mind is vacant!")
+ return
+
+ if (flashed.mind.has_antag_datum(/datum/antagonist/brother))
+ flashed.balloon_alert(source, "[flashed.p_theyre()] loyal to someone else!")
+ return
+
+ if (HAS_TRAIT(flashed, TRAIT_MINDSHIELD) || flashed.mind.assigned_role?.departments_bitflags & DEPARTMENT_BITFLAG_SECURITY)
+ flashed.balloon_alert(source, "[flashed.p_they()] resist!")
+ return
+
+ flashed.mind.add_antag_datum(/datum/antagonist/brother, team)
+
+ flashed.balloon_alert(source, "converted")
+ flash.burn_out()
+ flashed.mind.add_memory( \
+ /datum/memory/recruited_by_blood_brother, \
+ protagonist = flashed, \
+ antagonist = owner.current, \
+ )
+
+ UnregisterSignal(source, COMSIG_MOB_SUCCESSFUL_FLASHED_CARBON)
+ source.RemoveComponentSource(REF(src), /datum/component/can_flash_from_behind)
+
/datum/antagonist/brother/antag_panel_data()
return "Conspirators : [get_brother_names()]"
@@ -74,74 +127,36 @@
brother_text += ", "
return brother_text
-/datum/antagonist/brother/proc/give_meeting_area()
- if(!owner.current || !team || !team.meeting_area)
- return
- to_chat(owner.current, "Your designated meeting area: [team.meeting_area]")
- antag_memory += "Meeting Area: [team.meeting_area]
"
-
/datum/antagonist/brother/greet()
- var/brother_text = get_brother_names()
- to_chat(owner.current, span_alertsyndie("You are the [owner.special_role] of [brother_text]."))
- to_chat(owner.current, "The Syndicate only accepts those that have proven themselves. Prove yourself and prove your [team.member_name]s by completing your objectives together!")
+ to_chat(owner.current, span_alertsyndie("You are the [owner.special_role]."))
owner.announce_objectives()
- give_meeting_area()
/datum/antagonist/brother/proc/finalize_brother()
owner.current.playsound_local(get_turf(owner.current), 'sound/ambience/antag/tatoralert.ogg', 100, FALSE, pressure_affected = FALSE, use_reverb = FALSE)
team.update_name()
/datum/antagonist/brother/admin_add(datum/mind/new_owner,mob/admin)
- //show list of possible brothers
- var/list/candidates = list()
- for(var/mob/living/L in GLOB.alive_mob_list)
- if(!L.mind || L.mind == new_owner || !can_be_owned(L.mind))
- continue
- candidates[L.mind.name] = L.mind
-
- sortTim(candidates, GLOBAL_PROC_REF(cmp_text_asc))
- var/choice = tgui_input_list(admin, "Choose the blood brother.", "Brother", candidates)
- if(!choice)
- return
- var/datum/mind/bro = candidates[choice]
- var/datum/team/brother_team/T = new
- T.add_member(new_owner)
- T.add_member(bro)
- T.pick_meeting_area()
- T.forge_brother_objectives()
- new_owner.add_antag_datum(/datum/antagonist/brother,T)
- bro.add_antag_datum(/datum/antagonist/brother, T)
- message_admins("[key_name_admin(admin)] made [key_name_admin(new_owner)] and [key_name_admin(bro)] into blood brothers.")
- log_admin("[key_name(admin)] made [key_name(new_owner)] and [key_name(bro)] into blood brothers.")
+ var/datum/team/brother_team/team = new
+ team.add_member(new_owner)
+ new_owner.add_antag_datum(/datum/antagonist/brother, team)
+ message_admins("[key_name_admin(admin)] made [key_name_admin(new_owner)] into a blood brother.")
+ log_admin("[key_name(admin)] made [key_name(new_owner)] into a blood brother.")
/datum/antagonist/brother/ui_static_data(mob/user)
var/list/data = list()
data["antag_name"] = name
data["objectives"] = get_objectives()
- data["brothers"] = get_brother_names()
return data
/datum/team/brother_team
name = "\improper Blood Brothers"
member_name = "blood brother"
- ///Selected meeting area given to the team members
- var/meeting_area
- ///List of meeting areas that are randomly selected.
- var/static/meeting_areas = list(
- "The Bar",
- "Dorms",
- "Escape Dock",
- "Arrivals",
- "Holodeck",
- "Primary Tool Storage",
- "Recreation Area",
- "Chapel",
- "Library",
- )
+ var/brothers_left = 2
-/datum/team/brother_team/proc/pick_meeting_area()
- meeting_area = pick(meeting_areas)
- meeting_areas -= meeting_area
+/datum/team/brother_team/New()
+ . = ..()
+ if (prob(10))
+ brothers_left += 1
/datum/team/brother_team/proc/update_name()
var/list/last_names = list()
@@ -149,10 +164,16 @@
var/list/split_name = splittext(team_minds.name," ")
last_names += split_name[split_name.len]
- name = "[initial(name)] of " + last_names.Join(" & ")
+ if (last_names.len == 1)
+ name = "[last_names[1]]'s Isolated Intifada"
+ else
+ name = "[initial(name)] of " + last_names.Join(" & ")
/datum/team/brother_team/proc/forge_brother_objectives()
objectives = list()
+
+ add_objective(new /datum/objective/convert_brother)
+
var/is_hijacker = prob(10)
for(var/i = 1 to max(1, CONFIG_GET(number/brother_objectives_amount) + (members.len > 2) - is_hijacker))
forge_single_objective()
@@ -172,3 +193,12 @@
add_objective(new /datum/objective/assassinate, needs_target = TRUE)
else
add_objective(new /datum/objective/steal, needs_target = TRUE)
+
+/datum/objective/convert_brother
+ name = "convert brother"
+ explanation_text = "Convert someone else using your flash."
+ admin_grantable = FALSE
+ martyr_compatible = TRUE
+
+/datum/objective/convert_brother/check_completion()
+ return length(team?.members) > 1
diff --git a/code/modules/antagonists/cult/cult.dm b/code/modules/antagonists/cult/cult.dm
index 0f43b5ae29ef4d..9cd24f8e707c73 100644
--- a/code/modules/antagonists/cult/cult.dm
+++ b/code/modules/antagonists/cult/cult.dm
@@ -151,20 +151,13 @@
///Attempts to make a new item and put it in a potential inventory slot in the provided mob.
/datum/antagonist/cult/proc/cult_give_item(obj/item/item_path, mob/living/carbon/human/mob)
- var/list/slots = list(
- "backpack" = ITEM_SLOT_BACKPACK,
- "left pocket" = ITEM_SLOT_LPOCKET,
- "right pocket" = ITEM_SLOT_RPOCKET,
- )
-
- var/T = new item_path(mob)
- var/item_name = initial(item_path.name)
- var/where = mob.equip_in_one_of_slots(T, slots)
+ var/item = new item_path(mob)
+ var/where = mob.equip_conspicuous_item(item)
if(!where)
- to_chat(mob, span_userdanger("Unfortunately, you weren't able to get a [item_name]. This is very bad and you should adminhelp immediately (press F1)."))
+ to_chat(mob, span_userdanger("Unfortunately, you weren't able to get [item]. This is very bad and you should adminhelp immediately (press F1)."))
return FALSE
else
- to_chat(mob, span_danger("You have a [item_name] in your [where]."))
+ to_chat(mob, span_danger("You have [item] in your [where]."))
if(where == "backpack")
mob.back.atom_storage?.show_contents(mob)
return TRUE
diff --git a/code/modules/antagonists/revolution/revolution.dm b/code/modules/antagonists/revolution/revolution.dm
index 68491617aaa0ec..6ee5ba5dcc7e36 100644
--- a/code/modules/antagonists/revolution/revolution.dm
+++ b/code/modules/antagonists/revolution/revolution.dm
@@ -196,21 +196,14 @@
/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_PRE_FLASHED_CARBON, PROC_REF(on_flash))
+ real_mob.AddComponentFrom(REF(src), /datum/component/can_flash_from_behind)
RegisterSignal(real_mob, COMSIG_MOB_SUCCESSFUL_FLASHED_CARBON, PROC_REF(on_flash_success))
/datum/antagonist/rev/head/remove_innate_effects(mob/living/mob_override)
. = ..()
var/mob/living/real_mob = mob_override || owner.current
- UnregisterSignal(real_mob, list(COMSIG_MOB_PRE_FLASHED_CARBON, COMSIG_MOB_SUCCESSFUL_FLASHED_CARBON))
-
-/// Signal proc for [COMSIG_MOB_PRE_FLASHED_CARBON].
-/// Flashes will always result in partial success even if it's from behind someone
-/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
- return (deviation == DEVIATION_FULL) ? DEVIATION_OVERRIDE_PARTIAL : NONE
+ real_mob.RemoveComponentSource(REF(src), /datum/component/can_flash_from_behind)
+ UnregisterSignal(real_mob, COMSIG_MOB_SUCCESSFUL_FLASHED_CARBON)
/// Signal proc for [COMSIG_MOB_SUCCESSFUL_FLASHED_CARBON].
/// Bread and butter of revolution conversion, successfully flashing a carbon will make them a revolutionary
@@ -359,31 +352,25 @@
return ..()
/datum/antagonist/rev/head/equip_rev()
- var/mob/living/carbon/C = owner.current
- if(!ishuman(C))
+ var/mob/living/carbon/carbon_owner = owner.current
+ if(!ishuman(carbon_owner))
return
if(give_flash)
- var/obj/item/assembly/flash/handheld/T = new(C)
- var/list/slots = list (
- "backpack" = ITEM_SLOT_BACKPACK,
- "left pocket" = ITEM_SLOT_LPOCKET,
- "right pocket" = ITEM_SLOT_RPOCKET
- )
- var/where = C.equip_in_one_of_slots(T, slots, indirect_action = TRUE)
- if (!where)
- to_chat(C, "The Syndicate were unfortunately unable to get you a flash.")
+ var/where = carbon_owner.equip_conspicuous_item(new /obj/item/assembly/flash/handheld)
+ if (where)
+ to_chat(carbon_owner, "The flash in your [where] will help you to persuade the crew to join your cause.")
else
- to_chat(C, "The flash in your [where] will help you to persuade the crew to join your cause.")
+ to_chat(carbon_owner, "The Syndicate were unfortunately unable to get you a flash.")
if(give_hud)
- var/obj/item/organ/internal/cyberimp/eyes/hud/security/syndicate/S = new()
- S.Insert(C)
- if(C.get_quirk(/datum/quirk/body_purist))
- to_chat(C, "Being a body purist, you would never accept cybernetic implants. Upon hearing this, your employers signed you up for a special program, which... for \
+ var/obj/item/organ/internal/cyberimp/eyes/hud/security/syndicate/hud = new()
+ hud.Insert(carbon_owner)
+ if(carbon_owner.get_quirk(/datum/quirk/body_purist))
+ to_chat(carbon_owner, "Being a body purist, you would never accept cybernetic implants. Upon hearing this, your employers signed you up for a special program, which... for \
some odd reason, you just can't remember... either way, the program must have worked, because you have gained the ability to keep track of who is mindshield-implanted, and therefore unable to be recruited.")
else
- to_chat(C, "Your eyes have been implanted with a cybernetic security HUD which will help you keep track of who is mindshield-implanted, and therefore unable to be recruited.")
+ to_chat(carbon_owner, "Your eyes have been implanted with a cybernetic security HUD which will help you keep track of who is mindshield-implanted, and therefore unable to be recruited.")
/datum/team/revolution
name = "\improper Revolution"
diff --git a/code/modules/mob/living/carbon/inventory.dm b/code/modules/mob/living/carbon/inventory.dm
index fc96815045d92e..a375dbe878dfcc 100644
--- a/code/modules/mob/living/carbon/inventory.dm
+++ b/code/modules/mob/living/carbon/inventory.dm
@@ -455,3 +455,21 @@
covered_flags |= worn_item.body_parts_covered
return covered_flags
+
+/// Attempts to equip the given item in a conspicious place.
+/// This is used when, for instance, a character spawning with an item
+/// in their hands would be a dead giveaway that they are an antagonist.
+/// Returns the human readable name of where it placed the item, or null otherwise.
+/mob/living/carbon/proc/equip_conspicuous_item(obj/item/item, delete_item_if_failed = TRUE)
+ var/list/slots = list (
+ "backpack" = ITEM_SLOT_BACKPACK,
+ "left pocket" = ITEM_SLOT_LPOCKET,
+ "right pocket" = ITEM_SLOT_RPOCKET
+ )
+
+ var/placed_in = equip_in_one_of_slots(item, slots, indirect_action = TRUE)
+
+ if (isnull(placed_in) && delete_item_if_failed)
+ qdel(item)
+
+ return placed_in
diff --git a/tgstation.dme b/tgstation.dme
index 9adb663bc48197..3f1d417e836e3a 100644
--- a/tgstation.dme
+++ b/tgstation.dme
@@ -995,6 +995,7 @@
#include "code\datums\components\burning.dm"
#include "code\datums\components\butchering.dm"
#include "code\datums\components\caltrop.dm"
+#include "code\datums\components\can_flash_from_behind.dm"
#include "code\datums\components\chasm.dm"
#include "code\datums\components\chuunibyou.dm"
#include "code\datums\components\cleaner.dm"
diff --git a/tgui/packages/tgui/interfaces/AntagInfoBrother.tsx b/tgui/packages/tgui/interfaces/AntagInfoBrother.tsx
index 327c46c8df91ea..63094344eb6abc 100644
--- a/tgui/packages/tgui/interfaces/AntagInfoBrother.tsx
+++ b/tgui/packages/tgui/interfaces/AntagInfoBrother.tsx
@@ -11,14 +11,14 @@ type Info = {
export const AntagInfoBrother = (props, context) => {
const { data } = useBackend(context);
- const { antag_name, brothers, objectives } = data;
+ const { antag_name, objectives } = data;
return (
- You are the {antag_name} of {brothers}!
+ You are the {antag_name}!