diff --git a/code/datums/mind.dm b/code/datums/mind.dm
index edded43f23ad..5ed6fa83c48b 100644
--- a/code/datums/mind.dm
+++ b/code/datums/mind.dm
@@ -56,8 +56,6 @@
var/miming = 0 // Mime's vow of silence
/// A list of all the antagonist datums that the player is (does not include undatumized antags)
var/list/antag_datums
- /// A lazy list of all teams the player is part of but doesnt have an antag role for, (i.e. a custom admin team)
- var/list/teams
var/antag_hud_icon_state = null //this mind's ANTAG_HUD should have this icon_state
var/datum/atom_hud/antag/antag_hud = null //this mind's antag HUD
@@ -226,11 +224,6 @@
for(var/datum/antagonist/A as anything in antag_datums)
if(A.has_antag_objectives(include_team)) // this checks teams also
return TRUE
- // For custom non-antag role teams
- if(include_team && LAZYLEN(teams))
- for(var/datum/team/team as anything in teams)
- if(team.objective_holder.has_objectives())
- return TRUE
return FALSE
/**
@@ -248,11 +241,6 @@
if(team) // have to make asure a team exists here, team?. does not work below because it will add the null to the list
all_objectives += team.objective_holder.get_objectives() // Get all of their teams' objectives
- // For custom non-antag role teams
- if(include_team && LAZYLEN(teams))
- for(var/datum/team/team as anything in teams)
- all_objectives += team.objective_holder.get_objectives()
-
return all_objectives
/**
diff --git a/code/modules/antagonists/_common/antag_datum.dm b/code/modules/antagonists/_common/antag_datum.dm
index 9dbe8cd39146..9af71e5350a9 100644
--- a/code/modules/antagonists/_common/antag_datum.dm
+++ b/code/modules/antagonists/_common/antag_datum.dm
@@ -136,12 +136,16 @@ GLOBAL_LIST_EMPTY(antagonists)
*/
/datum/antagonist/proc/remove_innate_effects(mob/living/mob_override)
SHOULD_CALL_PARENT(TRUE)
- var/mob/living/L = mob_override || owner.current
+
+ var/mob/living/remove_effects_from = mob_override || owner?.current
+ if(!remove_effects_from)
+ return
+
if(antag_hud_type && antag_hud_name)
- remove_antag_hud(L)
+ remove_antag_hud(remove_effects_from)
// If `mob_override` exists it means we're only transferring this datum, we don't need to show the clown any text.
- handle_clown_mutation(L, mob_override ? null : clown_removal_text)
- return L
+ handle_clown_mutation(remove_effects_from, mob_override ? null : clown_removal_text)
+ return remove_effects_from
/**
* Adds this datum's antag hud to `antag_mob`.
diff --git a/code/modules/antagonists/_common/antag_team.dm b/code/modules/antagonists/_common/antag_team.dm
index fe226f89bbb6..0e8f49984079 100644
--- a/code/modules/antagonists/_common/antag_team.dm
+++ b/code/modules/antagonists/_common/antag_team.dm
@@ -19,14 +19,15 @@ GLOBAL_LIST_EMPTY(antagonist_teams)
/// The name to save objective successes under in the blackboxes. Saves nothing if blank.
var/blackbox_save_name
-/datum/team/New(list/starting_members)
+/datum/team/New(list/starting_members, add_antag_datum = TRUE)
..()
members = list()
objective_holder = new(src)
+
if(starting_members && !islist(starting_members))
starting_members = list(starting_members)
for(var/datum/mind/M as anything in starting_members)
- add_member(M)
+ add_member(M, add_antag_datum)
GLOB.antagonist_teams += src
/datum/team/Destroy(force = FALSE, ...)
@@ -42,12 +43,15 @@ GLOBAL_LIST_EMPTY(antagonist_teams)
*
* Generally this should ONLY be called by `add_antag_datum()` to ensure proper order of operations.
*/
-/datum/team/proc/add_member(datum/mind/new_member)
+/datum/team/proc/add_member(datum/mind/new_member, add_antag_datum = TRUE)
SHOULD_CALL_PARENT(TRUE)
- var/datum/antagonist/antag = get_antag_datum_from_member(new_member) // make sure they have the antag datum
members |= new_member
- if(!antag) // this team has no antag role, we'll add it directly to their mind team
- LAZYDISTINCTADD(new_member.teams, src)
+
+ if(add_antag_datum && antag_datum_type)
+ var/datum/antagonist/antag = get_antag_datum_from_member(new_member) // make sure they have the antag datum
+ // If no matching antag datum was found, give them one.
+ if(!antag)
+ return new_member.add_antag_datum(antag_datum_type, src)
/**
* Removes `member` from this team.
@@ -55,7 +59,6 @@ GLOBAL_LIST_EMPTY(antagonist_teams)
/datum/team/proc/remove_member(datum/mind/member)
SHOULD_CALL_PARENT(TRUE)
members -= member
- LAZYREMOVE(member.teams, src)
var/datum/antagonist/antag = get_antag_datum_from_member(member)
if(!QDELETED(antag))
qdel(antag)
@@ -103,9 +106,6 @@ GLOBAL_LIST_EMPTY(antagonist_teams)
if(A.get_team() != src)
continue
return A
- // If no matching antag datum was found, give them one.
- if(antag_datum_type)
- return member.add_antag_datum(antag_datum_type, src)
/**
* Special overrides for teams for target exclusion from objectives.
diff --git a/config/example/config.toml b/config/example/config.toml
index 806902f22aee..366ffba962f3 100644
--- a/config/example/config.toml
+++ b/config/example/config.toml
@@ -35,6 +35,7 @@
# SS220 CONFIGS
# - gateway_configuration
# - tts_configuration
+# - antag_mix_gamemode_configuration
################################################################
@@ -247,6 +248,7 @@ gamemode_probabilities = [
{gamemode = "vampire", probability = 3},
{gamemode = "wizard", probability = 2},
{gamemode = "trifecta", probability = 3},
+ {gamemode = "antag_mix", probability = 3},
]
# Do we want the amount of traitors to scale with population?
traitor_scaling = true
@@ -901,3 +903,142 @@ force_discord_verification = false
species_whitelist_enabled = false
################################################################
+
+
+[antag_mix_gamemode_configuration]
+# This section contains all settings related to antag_mix gamemode
+
+# Budged multiplier of antag_mix gamemode.
+# Which controlls how basic budget is multiplied, values less than 1 result in less antag budget
+budget_multiplier = 1
+# Max fraction of antags relative to ready players. Value between 0 and 1
+max_antag_fraction = 1
+# Configuration of antag scenarios
+# Must be list of objects with next params:
+# - [tag] required param which identifies the antag scenarios
+# - [params] antag scenario specific params
+#
+# Valid scenario params are:
+# - [cost] optional param, which defines how much of `antag_mix` budget will be spent on it
+# - [weight] optional param, which defines how often this scenario will be picked, related to others
+# - [antag_cap] optional param, which defines how many players can be picked for this scenario at most
+# - [candidates_required] optional param, which defines how many players must be picked for this scenario at least
+# - [restricted_roles] optional param, which defines which roles are restricted from becoming candidate for this scenario.
+# Must be list of strings
+# - [protected_roles] optional param, which defines which roles are protected from becoming candidate for this scenario.
+# Must be list of strings
+# - [restricted_species] optional param, which defines which species are restricted from becoming candidate for this scenario.
+# Must be list of strings
+# Team antag specific:
+# - [team_size] optional param, which defines how much of players must be present in one team. Must equal or lower than antag_cap,
+# or otherwise scenario won't be rolled
+[[antag_mix_gamemode_configuration.antag_scenarios_configuration]]
+tag = "traitor"
+
+[antag_mix_gamemode_configuration.antag_scenarios_configuration.params]
+"required_players" = 1
+"cost" = 1
+"weight" = 1
+"antag_cap" = 1
+"candidates_required" = 1
+"restricted_roles" = ["Cyborg"]
+"protected_roles" = [
+ "Security Cadet",
+ "Security Officer",
+ "Warden",
+ "Detective",
+ "Head of Security",
+ "Captain",
+ "Blueshield",
+ "Nanotrasen Representative",
+ "Magistrate",
+ "Internal Affairs Agent",
+ "Nanotrasen Navy Officer",
+ "Special Operations Officer",
+ "Syndicate Officer",
+ "Solar Federation General",
+]
+
+[[antag_mix_gamemode_configuration.antag_scenarios_configuration]]
+tag = "changeling"
+
+[antag_mix_gamemode_configuration.antag_scenarios_configuration.params]
+"required_players" = 1
+"cost" = 1
+"weight" = 1
+"antag_cap" = 1
+"candidates_required" = 1
+"restricted_roles" = ["Cyborg", "AI"]
+"protected_roles" = [
+ "Security Cadet",
+ "Security Officer",
+ "Warden",
+ "Detective",
+ "Head of Security",
+ "Captain",
+ "Blueshield",
+ "Nanotrasen Representative",
+ "Magistrate",
+ "Internal Affairs Agent",
+ "Nanotrasen Navy Officer",
+ "Special Operations Officer",
+ "Solar Federation General",
+]
+"restricted_species" = ["Machine"]
+
+[[antag_mix_gamemode_configuration.antag_scenarios_configuration]]
+tag = "vampire"
+
+[antag_mix_gamemode_configuration.antag_scenarios_configuration.params]
+"required_players" = 1
+"cost" = 1
+"weight" = 1
+"antag_cap" = 1
+"candidates_required" = 1
+"restricted_roles" = ["Cyborg", "AI", "Chaplain"]
+"protected_roles" = [
+ "Security Cadet",
+ "Security Officer",
+ "Warden",
+ "Detective",
+ "Head of Security",
+ "Captain",
+ "Blueshield",
+ "Nanotrasen Representative",
+ "Magistrate",
+ "Internal Affairs Agent",
+ "Nanotrasen Navy Officer",
+ "Special Operations Officer",
+ "Solar Federation General",
+]
+"restricted_species" = ["Machine"]
+
+[[antag_mix_gamemode_configuration.antag_scenarios_configuration]]
+tag = "blood_brothers"
+
+[antag_mix_gamemode_configuration.antag_scenarios_configuration.params]
+"required_players" = 1
+"cost" = 1
+"weight" = 1
+"antag_cap" = 1
+"team_size" = 1
+"candidates_required" = 1
+"restricted_roles" = ["Cyborg", "AI"]
+"protected_roles" = [
+ "Security Cadet",
+ "Security Officer",
+ "Warden",
+ "Detective",
+ "Head of Security",
+ "Captain",
+ "Blueshield",
+ "Nanotrasen Representative",
+ "Magistrate",
+ "Internal Affairs Agent",
+ "Nanotrasen Navy Officer",
+ "Special Operations Officer",
+ "Solar Federation General",
+]
+
+
+################################################################
diff --git a/icons/mob/hud/antaghud.dmi b/icons/mob/hud/antaghud.dmi
index 3b6ba79189e7..5f8ae83975b4 100644
Binary files a/icons/mob/hud/antaghud.dmi and b/icons/mob/hud/antaghud.dmi differ
diff --git a/modular_ss220/_defines220/_defines220.dme b/modular_ss220/_defines220/_defines220.dme
index 898c1b3f08a8..c04f3832953e 100644
--- a/modular_ss220/_defines220/_defines220.dme
+++ b/modular_ss220/_defines220/_defines220.dme
@@ -10,6 +10,9 @@
#include "code/signals_obj.dm"
#include "code/cult_defines_ss220.dm"
#include "code/hud_ss220.dm"
+#include "code/atom_hud.dm"
+#include "code/gamemode.dm"
+#include "code/role_preferences.dm"
#include "code/layers_ss220.dm"
#include "code/signals_atom.dm"
#include "code/jobs_defines.dm"
diff --git a/modular_ss220/_defines220/code/atom_hud.dm b/modular_ss220/_defines220/code/atom_hud.dm
new file mode 100644
index 000000000000..eb191aefd6c6
--- /dev/null
+++ b/modular_ss220/_defines220/code/atom_hud.dm
@@ -0,0 +1 @@
+#define ANTAG_HUD_BLOOD_BROTHER 21
diff --git a/modular_ss220/_defines220/code/gamemode.dm b/modular_ss220/_defines220/code/gamemode.dm
new file mode 100644
index 000000000000..555859244f6c
--- /dev/null
+++ b/modular_ss220/_defines220/code/gamemode.dm
@@ -0,0 +1 @@
+#define SPECIAL_ROLE_BLOOD_BROTHER "Blood Brother"
diff --git a/modular_ss220/_defines220/code/role_preferences.dm b/modular_ss220/_defines220/code/role_preferences.dm
new file mode 100644
index 000000000000..d65f3c910f61
--- /dev/null
+++ b/modular_ss220/_defines220/code/role_preferences.dm
@@ -0,0 +1 @@
+#define ROLE_BLOOD_BROTHER "blood brother"
diff --git a/modular_ss220/antagonists/_antagonists.dm b/modular_ss220/antagonists/_antagonists.dm
new file mode 100644
index 000000000000..ed703929a3bd
--- /dev/null
+++ b/modular_ss220/antagonists/_antagonists.dm
@@ -0,0 +1,8 @@
+/datum/modpack/antagonists
+ name = "Антагонисты и режимы"
+ desc = "Добавляет новые режимы и антагонистов."
+ author = "Gaxeer, dj-34"
+
+/datum/modpack/antagonists/initialize()
+ GLOB.special_roles |= ROLE_BLOOD_BROTHER
+ GLOB.huds += new/datum/atom_hud/antag/hidden()
diff --git a/modular_ss220/antagonists/_antagonists.dme b/modular_ss220/antagonists/_antagonists.dme
new file mode 100644
index 000000000000..c03286e61a90
--- /dev/null
+++ b/modular_ss220/antagonists/_antagonists.dme
@@ -0,0 +1,10 @@
+#include "_antagonists.dm"
+
+#include "code/antag_mix/scenarios/antag_scenario.dm"
+#include "code/antag_mix/scenarios/antag_team_scenario.dm"
+#include "code/antag_mix/scenarios/minor_scenarios.dm"
+#include "code/configuration/antag_mix_configuration.dm"
+#include "code/blood_brothers/blood_brothers_datum.dm"
+#include "code/blood_brothers/blood_brothers_team.dm"
+#include "code/mind/memory_edit.dm"
+#include "code/antag_mix/antag_mix.dm"
diff --git a/modular_ss220/antagonists/code/antag_mix/antag_mix.dm b/modular_ss220/antagonists/code/antag_mix/antag_mix.dm
new file mode 100644
index 000000000000..842375a5842e
--- /dev/null
+++ b/modular_ss220/antagonists/code/antag_mix/antag_mix.dm
@@ -0,0 +1,170 @@
+/datum/game_mode/antag_mix
+ name = "Antag Mix"
+ config_tag = "antag_mix"
+ /// Multiplier of antag credits. Credits will be used to choose antag scenarios
+ var/budget_multiplier = 1
+ /// Max fraction of antags relative to ready players. Value between 0 and 1
+ var/max_antag_fraction = 1
+ /// How much budget has left
+ var/budget = 0
+ /// List of scenarios chosen on `pre_setup` stage, and which will be applied on `post_setup`
+ var/list/datum/antag_scenario/executed_scenarios = list()
+
+
+/datum/game_mode/antag_mix/New()
+ . = ..()
+ apply_configuration()
+
+
+/datum/game_mode/antag_mix/pre_setup()
+ var/list/datum/antag_scenario/possible_scenarios = subtypesof(/datum/antag_scenario)
+
+ var/list/mob/new_player/ready_players = get_ready_players()
+ var/ready_players_amount = length(ready_players)
+ log_antag_mix("Trying to start round with [ready_players_amount] ready players")
+ log_antag_mix("Max antagonist fraction is '[max_antag_fraction]'")
+
+ var/list/datum/antag_scenario/acceptable_scenarios = initialize_acceptable_scenarios(possible_scenarios, ready_players_amount)
+ if(!length(acceptable_scenarios))
+ return FALSE
+
+ budget = calculate_budget(ready_players_amount)
+ log_antag_mix("Roundstart budget: [budget]")
+ return pick_scenarios(draft_scenarios(acceptable_scenarios, ready_players), ready_players_amount)
+
+
+/datum/game_mode/antag_mix/post_setup()
+ for(var/datum/antag_scenario/scenario_to_execute as anything in executed_scenarios)
+ scenario_to_execute.execute()
+
+ return ..()
+
+
+/datum/game_mode/antag_mix/proc/apply_configuration()
+ budget_multiplier = GLOB.configuration.antag_mix_gamemode.budget_multiplier
+ max_antag_fraction = GLOB.configuration.antag_mix_gamemode.max_antag_fraction
+
+
+/**
+ * Calculates amount of `credits` that will spent on antag scenarios, that are available.
+*/
+/datum/game_mode/antag_mix/proc/calculate_budget(ready_players_amount)
+ return ready_players_amount * budget_multiplier
+
+
+/**
+ * Count players that are currently ingame and ready.
+*/
+/datum/game_mode/antag_mix/proc/get_ready_players()
+ var/list/mob/new_player/ready_players = list()
+ for(var/mob/new_player/player in GLOB.player_list)
+ if(!player.client || !player.ready)
+ continue
+
+ ready_players.Add(player)
+
+ return ready_players
+
+/**
+ * Creates actual scenario datums from `inited_scenario_paths`.
+*/
+/datum/game_mode/antag_mix/proc/initialize_acceptable_scenarios(list/datum/antag_scenario/scenarios_to_init, players_ready)
+ var/list/datum/antag_scenario/acceptable_scenarios = list()
+ for(var/datum/antag_scenario/scenario_path as anything in scenarios_to_init)
+ if(!ispath(scenario_path))
+ continue
+
+ if(initial(scenario_path.abstract))
+ continue
+
+ var/datum/antag_scenario/possible_scenario = new scenario_path
+ if(!possible_scenario.acceptable(players_ready))
+ qdel(possible_scenario)
+ continue
+
+ acceptable_scenarios += possible_scenario
+
+ return acceptable_scenarios
+
+
+/datum/game_mode/antag_mix/proc/pick_scenarios(list/datum/antag_scenario/drafted_scenarios, players_ready_amount, current_antag_fraction = 0)
+ if(!length(drafted_scenarios))
+ return FALSE
+
+ var/budget_left = budget
+ var/list/picked_scenarios = list()
+ while(budget_left > 0)
+ if(!length(drafted_scenarios) || (current_antag_fraction >= max_antag_fraction))
+ break
+
+ var/datum/antag_scenario/picked_scenario = pickweight(drafted_scenarios)
+ if(picked_scenario.cost > budget_left)
+ drafted_scenarios.Remove(picked_scenario)
+ continue
+
+ var/added_antag_fraction = min(length(picked_scenario.candidates), picked_scenario.get_antag_cap(players_ready_amount)) / players_ready_amount
+ if(!added_antag_fraction || (added_antag_fraction + current_antag_fraction > max_antag_fraction))
+ drafted_scenarios.Remove(picked_scenario)
+ continue
+
+ budget_left -= picked_scenario.cost
+ current_antag_fraction += added_antag_fraction
+
+ picked_scenarios[picked_scenario] += 1
+ log_antag_mix("Scenario '[picked_scenario.name]' with: cost '[picked_scenario.cost]', weight '[picked_scenario.weight]' was picked [picked_scenarios[picked_scenario]] times")
+ log_antag_mix("Antagonist fraction is '[current_antag_fraction]'")
+
+ if(!length(picked_scenarios))
+ log_antag_mix("No antag scenarios were picked. Let another game mode roll.")
+ return FALSE
+
+ for(var/picked_scenario in picked_scenarios)
+ spend_budget(pre_execute_scenario(picked_scenario, picked_scenarios[picked_scenario] - 1, players_ready_amount))
+
+ if(budget != budget_left && current_antag_fraction < max_antag_fraction && length(drafted_scenarios))
+ log_antag_mix("Some of the picked scenarios failed pre execution, try to pick scenarios from leftovers")
+ return pick_scenarios(drafted_scenarios, players_ready_amount, current_antag_fraction)
+
+ return TRUE
+
+/datum/game_mode/antag_mix/proc/spend_budget(budget_to_spend)
+ budget = max(0, budget - budget_to_spend)
+ log_antag_mix("Budget spent: [budget_to_spend], budget left: [budget]")
+
+
+/datum/game_mode/antag_mix/proc/pre_execute_scenario(datum/antag_scenario/scenario_to_pre_execute, scaled_times, players_ready_amount)
+ if(!scenario_to_pre_execute)
+ return 0
+
+ scenario_to_pre_execute.trim_candidates()
+
+ scenario_to_pre_execute.scaled_times = scaled_times
+ if(!scenario_to_pre_execute.pre_execute(players_ready_amount))
+ log_antag_mix("Scenario '[scenario_to_pre_execute.name]' failed to pre execute")
+ return 0
+
+ executed_scenarios |= scenario_to_pre_execute
+ return scenario_to_pre_execute.cost * (scenario_to_pre_execute.scaled_times + 1)
+
+
+/datum/game_mode/antag_mix/proc/draft_scenarios(list/datum/antag_scenario/scenarios_to_pick_from, list/mob/new_player/ready_players)
+ if(!length(scenarios_to_pick_from) || !length(ready_players))
+ return
+
+ var/drafted_scenarios = list()
+ for(var/datum/antag_scenario/scenario as anything in scenarios_to_pick_from)
+ if(!scenario.weight)
+ continue
+
+ scenario.candidates = ready_players.Copy()
+ scenario.trim_candidates()
+ if(!scenario.ready())
+ continue
+
+ drafted_scenarios[scenario] = scenario.weight
+ log_antag_mix("Scenario: '[scenario.name]' with weight: '[scenario.weight]' was drafted")
+
+ return drafted_scenarios
+
+/datum/game_mode/antag_mix/proc/log_antag_mix(text)
+ log_debug("\[ANTAG MIX\] [text]")
diff --git a/modular_ss220/antagonists/code/antag_mix/readme.md b/modular_ss220/antagonists/code/antag_mix/readme.md
new file mode 100644
index 000000000000..92c227dde813
--- /dev/null
+++ b/modular_ss220/antagonists/code/antag_mix/readme.md
@@ -0,0 +1,179 @@
+# Antag Mix Mode
+
+## Antag Mix Mode
+
+`Antag Mix` is gamemode which simply allows to have multiple roundstart antagonists without creating new gamemode per each antagonists combination.
+The setup order of the gamemode is following:
+
+1. The first stage is `/datum/game_mode/antag_mix` gamemode new instance creation,
+ where `apply_configuration()` is executed, which handles loading of configuration into the instance.
+
+2. Next is `pre_setup()`, where `Antag Scenarios` are picked.
+ If no scenarios were picked, than `pre_setup` will fail and another gamomode will roll.
+ This stage consists of these steps:
+
+ 1. Initializing accepatable scenarios (see `initialize_acceptable_scenarios()`).
+ 2. Round budget calculation (see `calculate_budget()`),
+ which is linearly dependent from amount of ready player, simply `ready_players_amount * budget_multiplier`.
+ So, more players means more budget to spend on `Antag Scenarios`.
+ `budget_multiplier` is simple coefficient used to scale the budget. The higher it is, the greater is the budget and vice versa.
+ 3. Drafting scenarios (see `draft_scenarios()`), which is also divided into following step:
+
+ 1. Trimming ready players, to have only ones, that actually can be picked for the scenario.
+
+ 2. Filtering scenarios which meet following requirement:
+
+ - Scenario has `weight` more than `0`
+ - Scenario is ready to be executed (see `/datum/antag_scenario/proc/ready()`)
+
+ 3. The valid scenario than added to assoc list of `scenario -> it's weight`
+
+ 4. The result assoc list from the previous stage is then passed into pick stage:
+
+ 1. The scenarios are picked until all of the conditions below are true:
+
+ - Round `budget` is higher than `0`
+ - There is at least `1` scenario available to be picked
+ - Antag fraction of already picked scenarios is not higher than `max_antag_fraction`
+
+ 2. When scenario is picked, it's then added to assoc list of `picked_scenarios`, where `picked_scenario -> picked_times` pairs are stored. Scenarios are considered unpickable if:
+
+ - Scenario's cost is higher than budget left
+ - Antag fraction after adding the scenario will be higher than `max_antag_fraction`
+
+ Also tracked `current_antag_fraction` and `budget_left` are updated based on scenario's parameters.
+
+ 3. Picked scenarios will be passed into next stage `pre_execute_scenario`. This stage also has thigs to say:
+
+ - Candidates will be trimmed again, because same player can't be picked for multiple scenarios
+ - Scenarios will be properly scaled and then `pre_execute` will be called on scenario to pick and added to `executed_scenarios`
+ - If scenario `pre_execute` failes - scenario won't be added to `executed_scenarios` which means that it won't be included into round
+ - The resulting cost is calculated, `scaled_times` wise, and then substracted from the budget.
+
+ 4. If `budget` was spent incorrectly, which means that some of the picked scenarios failed to `pre_execute`,
+ then `pick_scenarios` will be recursively called, to try spend `budget` that was left.
+ Any left `budget` that can't be spent unfortunately will burn.
+
+3. Next and last stage is scenarios execution (see `/datum/antag_scenario/proc/execute()`),
+ which will happen on gamemode `post_setup()`. Unfortunetale on this stage the world is already set up
+ and any failures are to leave with.
+
+## Antag Scenarios
+
+The `Antag Scenario` is simply set of rules, based on the antagonist is picked and injected into the round.
+How scenarios are used is better explained in [previous topic](#antag-mix-mode).
+
+### How To Create New Scenario
+
+You came up with idea of adding new antagonist into the game and what it to be included in `Antag Mix`?
+So here is the guide for you how to do it properly:
+
+1. Create a new subtype of `/datum/antag_scenario` for teamless antags or `/datum/antag_scenario/team` for antags,
+ that are divided into teams.
+
+2. Most of the type, all you have to do is to simply set following properties to values you need:
+
+- `name` - just the name of scenario. Currently is not used
+- `config_tag` - unique identifier of scenario in configuration
+- `abstract` - this must be `TRUE` for scenarios, that can be used by `Antag Mix`
+- `antag_role` - value, that uniquely specifies the antagonist. Used to check player eligibility to play the role
+- `antag_special_role` - special role, that is assigned to player chosen for scenario
+- `antag_datum` - `/datum/antagonist` that will be assigned to chosen player in `execute()`
+- `scaled_times` - amount of times, the scenario was chosen to be executed again.
+ Lineary affects `antag_cap`: [`antag_cap * (scaled_times + 1)`]
+- `required_players` (configurable) - amount of players required to start the scenario
+- `cost` (configurable) - cost of the antag scenario
+- `weight` (configurable) - frequency of scenario, related to other scenarios. Higher values - higher frequency
+- `antag_cap` (configurable) - max amount of antagonists, the scenario can inject.
+- `candidates_required` - how many possible candidates are required for this scenario to be executed
+- `restricted_roles` - roles that can't be chosen for the scenario
+- `protected_roles` - roles that can't be chosen for the scenario
+ if 'GLOB.configuration.gamemode.prevent_mindshield_antags' is TRUE
+- `restricted_species` - species that can't be chosen for the scenario
+
+## Configuration
+
+`Antag Scenarios` and `Antag Mix Mode` have plenty of variables, that can be configured.
+This allows to easily enough tweak existing values.
+All of the following variables are present in [configuration file](/config/config.toml) `antag_mix_gamemode_configuration` section.
+
+### Antag Mix Mode
+
+When adding new configurables, add values in configuration file and corresponding value to `/datum/configuration_section/antag_mix_gamemode_configuration`.
+Values from configuration file should be loaded in `/datum/configuration_section/antag_mix_gamemode_configuration/load_data`.
+Values from configuration are assigned to `/datum/game_mode/antag_mix` instance corresponding properties in `/datum/game_mode/antag_mix/proc/apply_configuration()`,
+so also do it here.
+
+Here is complete list of configurable variables of `Antag Mix Mode`:
+
+- `budget_multiplier` - defines how much the rounstart budget is multiplied.
+ Higher values - mean more budget, lower - less.
+- `max_antag_fraction` - defines percentage of roundstart antags injected in relation to roundstart ready players.
+ For example value 0.1 means 10%. If 60 players are ready on roundstart - 6 antagonist players can be added at most.
+
+### Antag Scenarios
+
+`Antag Scenario` can be fine grain tuned in [configuration file](/config/config.toml) `antag_mix_gamemode_configuration` section.
+Long story short - configuration is in `.toml` format, so best way to understand it, is to read [documentation](https://toml.io/en/v1.0.0#array-of-tables).
+Each `Antag Scenario` entry can be uniquely identified by `tag` property in configuration.
+The `/datum/antag_scenario` with `config_tag` property equal to corresponding `tag` will be configured in `/datum/antag_scenario/proc/apply_configuration()`.
+If non existing param will be present in configuration, the error will be logged.
+Here is also example, which will be much easier to understand after familiarizing with syntax and semantics of `.toml`:
+
+```toml
+# Here `antag_mix_gamemode_configuration` is name of whole section, in `toml` it's called `table`.
+# `antag_scenarios_configuration` is name of `key` in `antag_mix_gamemode_configuration` table. Which itself represents array of tables.
+# Each new scenarion configuration entry must start with `[[antag_mix_gamemode_configuration.antag_scenarios_configuration]]`.
+[[antag_mix_gamemode_configuration.antag_scenarios_configuration]]
+# `Key` with name `tag` in table `antag_scenarios_configuration. Tag uniquely identifies each scenario.
+tag = "traitor"
+
+# Here we specify belonging of `params` table to `antag_scenarios_configuration` array of tables.
+# Whole path must be specified in order to correctly be identified by `.toml` parser.
+[antag_mix_gamemode_configuration.antag_scenarios_configuration.params]
+"required_players" = 1
+"cost" = 1
+"weight" = 1
+"antag_cap" = 1
+"candidates_required" = 1
+"restricted_roles" = ["Cyborg"]
+"protected_roles" = [
+ "Security Officer",
+ "Warden",
+ "Detective",
+ "Head of Security",
+ "Captain",
+ "Blueshield",
+ "Nanotrasen Representative",
+ "Magistrate",
+ "Internal Affairs Agent",
+ "Nanotrasen Navy Officer",
+ "Special Operations Officer",
+ "Syndicate Officer",
+ "Solar Federation General",
+]
+```
+
+Below is present complete list of existing configurables for curretly existing antag scenarios.
+Other variables are prohibited to be changed using configuration file, so they are not present here.
+To read more about other variables check [/datum/antag_scenario](/modular_ss220/antagonists/code/antag_mix/scenarios/antag_scenario.dm).
+If new configurable properties will be added, include them below:
+
+### General Properties
+
+- `required_players` - number of ready players, that are required for this scenario to be acceptable
+- `cost` - how much of `Antag Mix` budget is spent on this scenario.
+ `0` means, that scenario is only bounded by `max_antag_fraction`
+- `weight` - how often the scenario is picked relatively to other scenarios
+- `antag_cap` - how many players can be chosen for antags by scenario per `scaled_times`
+- `candidates_required` - how many possible candidates are required for this scenario to be executed
+ The value is used in `/datum/antag_scenario/proc/ready()`
+- `restricted_roles` - list of roles, that can't be chosen for this scenario
+- `protected_roles` - list of roles, that can't be chosen for this scenario if `GLOB.configuration.gamemode.prevent_mindshield_antags` is `TRUE`
+- `restricted_species` - list of species, that can't be chosen for this scenario
+
+Team scenarios are separated into own subtype. Complete list of specific variables is present here:
+
+### `/datum/antag_scenario/team` Specific Properties
+
+- `team_size` - amount of players, that will be added to same team
diff --git a/modular_ss220/antagonists/code/antag_mix/scenarios/antag_scenario.dm b/modular_ss220/antagonists/code/antag_mix/scenarios/antag_scenario.dm
new file mode 100644
index 000000000000..a5fffbd4e512
--- /dev/null
+++ b/modular_ss220/antagonists/code/antag_mix/scenarios/antag_scenario.dm
@@ -0,0 +1,180 @@
+/datum/antag_scenario
+ /// Name of the scenario. Will be used in round end statistics
+ var/name = "Generic Scenario"
+ /// Configuration tag of scenario, which will be used on scenario configuration loading
+ var/config_tag = null
+ /// Whether scenario shouldn't be used in `Antag Mix` and which purpose is to be literally abstract.
+ /// Scenario will crash in `/datum/antag_scenario/New()` if tried to instantiate
+ var/abstract = TRUE
+ /// Role used to check if the player is banned from it. Example: `ROLE_TRAITOR`, `ROLE_CHANGELING`, etc.
+ var/antag_role = null
+ /// Special role, that will be assigned to chosen players. Example: `SPECIAL_ROLE_TRAITOR`, `SPECIAL_ROLE_CHANGELIN`, etc.
+ var/antag_special_role = null
+ /// Antag datum, that will be granted to chosen players on `execute` which will be called on post setup
+ var/datum/antagonist/antag_datum = null
+ /// Amount of times, this scenario has been picked again
+ var/scaled_times = 0
+ /// Amount of players required to start this scenario
+ var/required_players = 1
+ /// Cost of the antag scenario. Antag_mix antag generation is based on this value.
+ /// Scenarios with `0` cost considered free and only limited by `max_antag_fraction` of `/datum/game_mode/antag_mix`
+ var/cost = 1
+ /// Weight of the scenario. Will be taken into consideration by 'antag_mix' gamemode.
+ /// Higher values make scenario more frequent, lower - less. Defaults to '1'
+ var/weight = 1
+ /// Number of player population, based on which the new scenario antag will be scaled.
+ /// Can be specified using just number, which will be treated as hard cap, or in linear equation form:
+ /// list("denominator" = 10, "offset" = 1), where 'denominator' is divider for current players population,
+ /// and 'offset' is guaranteed amount of antag of scenario's type
+ var/antag_cap = 1
+ /// How many possible candidates are required for this scenario to be executed
+ var/candidates_required = 1
+ /// Jobs that can't be chosen for the scenario
+ var/list/restricted_roles = list()
+ /// Jobs that can't be chosen for the scenario if 'GLOB.configuration.gamemode.prevent_mindshield_antags' is TRUE
+ var/list/protected_roles = list()
+ /// Species that can't be chosen for the scenario
+ var/list/restricted_species = list()
+ /// List of available candidates for this scenario
+ var/list/mob/new_player/candidates = list()
+ /// List of players that were drafted to be antagonists of this scenario
+ var/list/datum/mind/assigned = list()
+
+/datum/antag_scenario/New()
+ if(abstract)
+ stack_trace("Instantiation of abstract antag scenarios is prohibited.")
+ qdel(src)
+
+ apply_configuration()
+
+
+/**
+ * Gets configuration params from [GLOB.configuration.antag_mix_gamemode.params_by_scenario],
+ * which are grouped by `config_tag` property of `/datum/antag_scenario`.
+ * If `config_tag` field is `null` - default scenario
+ * and write them into
+*/
+/datum/antag_scenario/proc/apply_configuration()
+ SHOULD_NOT_OVERRIDE(TRUE)
+ if(!config_tag)
+ return
+
+ var/params = GLOB.configuration.antag_mix_gamemode.params_by_scenario[config_tag]
+ if(!islist(params))
+ return
+
+ for(var/param in params)
+ if(!(param in vars))
+ error("Invalid antag scenario configuration param '[param]' in [type]")
+ continue
+
+ vars[param] = params[param]
+
+ if(GLOB.configuration.gamemode.prevent_mindshield_antags)
+ restricted_roles |= protected_roles
+
+
+/**
+ * Performs sanity check for this scenario to be run.
+ *
+ * Returns: 'TRUE' if scenario is matching current situation (game has enough players, enough antags can be spawned).
+ * 'FALSE' returned otherwise.
+*/
+/datum/antag_scenario/proc/acceptable(population)
+ return (population >= required_players) && (get_antag_cap(population) > 0)
+
+
+/**
+ * Checks whether this scenario is ready to be applied.
+ *
+ * Returns: 'TRUE' if all requirements are met for this scenario to be executed, 'FALSE' otherwise
+*/
+/datum/antag_scenario/proc/ready()
+ return length(candidates) >= candidates_required
+
+
+/**
+ * Called in `pre_setup` of [/datum/game_mode/antag_mix] gamemode. Here antags should be chosen.
+ *
+ * Returns: 'TRUE' if successfully executed, for example antags successfully chosen, 'FALSE' otherwise.
+*/
+/datum/antag_scenario/proc/pre_execute(population)
+ var/assigned_before = length(assigned)
+ var/calculated_antag_cap = get_total_antag_cap(population)
+ for(var/i in 1 to calculated_antag_cap)
+ if(!length(candidates))
+ break
+
+ var/mob/new_player/chosen = pick_n_take(candidates)
+
+ // We will check if something bad happened with candidates here.
+ if(!chosen || !chosen.mind)
+ error("Antag scenario 'candidates' were containing 'null' or mindless mob. This should not happen.")
+ calculated_antag_cap++
+ continue
+
+ var/datum/mind/chosen_mind = chosen.mind
+ assigned |= chosen_mind
+ chosen_mind.special_role = antag_special_role
+ chosen_mind.restricted_roles |= restricted_roles
+
+ return length(assigned) - assigned_before > 0
+
+/**
+ * Called in `post_setup`, which means that all players already have jobs. Here antags should receive everything they need.
+ * Can fail here, but there is nothing we can do on this stage - all players already have their jobs.
+*/
+/datum/antag_scenario/proc/execute()
+ for(var/datum/mind/assignee as anything in assigned)
+ assignee.add_antag_datum(antag_datum)
+
+ return TRUE
+
+/**
+ * Gets antag cap per one scenario.
+*/
+/datum/antag_scenario/proc/get_antag_cap(population)
+ if(isnum(antag_cap))
+ return antag_cap
+
+ return FLOOR(population / (antag_cap["denominator"] || 1), 1) + (antag_cap["offset"] || 0)
+
+/**
+ * Gets antag cap per this scenario, but taking `scaled_times` into calculation.
+*/
+/datum/antag_scenario/proc/get_total_antag_cap(population)
+ return get_antag_cap(population) * (scaled_times + 1)
+
+/**
+ * Filter candidates scenario specific requirement vise.
+*/
+/datum/antag_scenario/proc/trim_candidates()
+ for(var/mob/new_player/candidate as anything in candidates)
+ var/client/candidate_client = candidate.client
+ var/datum/mind/candidate_mind = candidate.mind
+ if(!candidate_client || !candidate_mind || !candidate.ready)
+ candidates.Remove(candidate)
+ continue
+
+ if(candidate_client.skip_antag)
+ candidates.Remove(candidate)
+ continue
+
+ if(candidate_mind.special_role)
+ candidates.Remove(candidate)
+ continue
+
+ if(!player_old_enough_antag(candidate_client, antag_role))
+ candidates.Remove(candidate)
+ continue
+
+ if(jobban_isbanned(candidate, ROLE_SYNDICATE) || jobban_isbanned(candidate, antag_role))
+ candidates.Remove(candidate)
+ continue
+
+ if(!(antag_role in candidate.client.prefs.be_special) || (candidate.client.prefs.active_character.species in restricted_species))
+ candidates.Remove(candidate)
+ continue
+
+ if(candidate_mind.assigned_role in restricted_roles)
+ candidates.Remove(candidate)
diff --git a/modular_ss220/antagonists/code/antag_mix/scenarios/antag_team_scenario.dm b/modular_ss220/antagonists/code/antag_mix/scenarios/antag_team_scenario.dm
new file mode 100644
index 000000000000..40bddc27db76
--- /dev/null
+++ b/modular_ss220/antagonists/code/antag_mix/scenarios/antag_team_scenario.dm
@@ -0,0 +1,60 @@
+/datum/antag_scenario/team
+ /// Path of team, that antags will be joined to. This is required property.
+ var/datum/team/antag_team = /datum/team
+ /// Size the team should match.
+ var/team_size = 1
+ /// List of lists of player's minds, picked for teams. Each nested list represents one team.
+ var/list/picked_teams = list()
+
+
+/datum/antag_scenario/team/pre_execute(population)
+ if(!ispath(antag_team))
+ error("'antag_team' in '[type]' team antag scenario is '[antag_team]' which is invalid.")
+
+ var/max_teams = FLOOR((get_total_antag_cap(population) / team_size), 1)
+ message_admins("Max teams: [max_teams]")
+ if(!max_teams)
+ return FALSE
+
+ var/teams_before = length(picked_teams)
+ for(var/i in 1 to max_teams)
+ var/list/datum/mind/members = list()
+ for(var/j in 1 to team_size)
+ if(!length(candidates))
+ break
+
+ var/mob/new_player/team_member = pick_n_take(candidates)
+ if(!team_member || !team_member.mind)
+ error("For some reason 'null' or mindless candidate was present in [type] 'candidates' list")
+ continue
+
+ var/datum/mind/chosen_mind = team_member.mind
+ chosen_mind.special_role = antag_special_role
+ chosen_mind.restricted_roles |= restricted_roles
+
+ members += chosen_mind
+ assigned |= chosen_mind
+
+ message_admins("Members: [json_encode(members)]")
+ // If for some reason, not enough members were found - we will try again
+ if(team_size > length(members))
+ max_teams++
+ continue
+
+ message_admins("Picked team of: [json_encode(members)]")
+ picked_teams += list(members)
+
+ return length(picked_teams) - teams_before > 0
+
+
+/datum/antag_scenario/team/execute()
+ for(var/list/team_members in picked_teams)
+ if(!length(team_members))
+ continue
+
+ message_admins("Creating team of [json_encode(team_members)]")
+ var/datum/team/new_team = new antag_team(team_members, FALSE)
+ for(var/datum/mind/team_member as anything in new_team.members)
+ team_member.add_antag_datum(antag_datum, new_team)
+
+ return TRUE
diff --git a/modular_ss220/antagonists/code/antag_mix/scenarios/minor_scenarios.dm b/modular_ss220/antagonists/code/antag_mix/scenarios/minor_scenarios.dm
new file mode 100644
index 000000000000..f0f2c7ae1aad
--- /dev/null
+++ b/modular_ss220/antagonists/code/antag_mix/scenarios/minor_scenarios.dm
@@ -0,0 +1,119 @@
+/datum/antag_scenario/traitor
+ name = "Traitor"
+ config_tag = "traitor"
+ abstract = FALSE
+ antag_role = ROLE_TRAITOR
+ antag_special_role = SPECIAL_ROLE_TRAITOR
+ antag_datum = /datum/antagonist/traitor
+ required_players = 10
+ cost = 10
+ weight = 1
+ antag_cap = 1
+ candidates_required = 1
+ restricted_roles = list("Cyborg")
+ protected_roles = list(
+ "Security Cadet",
+ "Security Officer",
+ "Warden",
+ "Detective",
+ "Head of Security",
+ "Captain",
+ "Blueshield",
+ "Nanotrasen Representative",
+ "Magistrate",
+ "Internal Affairs Agent",
+ "Nanotrasen Navy Officer",
+ "Special Operations Officer",
+ "Syndicate Officer",
+ "Solar Federation General")
+
+/datum/antag_scenario/changeling
+ name = "Changeling"
+ config_tag = "changeling"
+ abstract = FALSE
+ antag_role = ROLE_CHANGELING
+ antag_special_role = SPECIAL_ROLE_CHANGELING
+ antag_datum = /datum/antagonist/changeling
+ required_players = 10
+ cost = 10
+ weight = 1
+ antag_cap = 1
+ candidates_required = 1
+ restricted_roles = list("Cyborg", "AI")
+ protected_roles = list(
+ "Security Cadet",
+ "Security Officer",
+ "Warden",
+ "Detective",
+ "Head of Security",
+ "Captain",
+ "Blueshield",
+ "Nanotrasen Representative",
+ "Magistrate",
+ "Internal Affairs Agent",
+ "Nanotrasen Navy Officer",
+ "Special Operations Officer",
+ "Syndicate Officer",
+ "Solar Federation General")
+ restricted_species = list("Machine")
+
+/datum/antag_scenario/vampire
+ name = "Vampire"
+ config_tag = "vampire"
+ abstract = FALSE
+ antag_role = ROLE_VAMPIRE
+ antag_special_role = SPECIAL_ROLE_VAMPIRE
+ antag_datum = /datum/antagonist/vampire
+ required_players = 10
+ cost = 10
+ weight = 1
+ antag_cap = 1
+ candidates_required = 1
+ restricted_roles = list("Cyborg", "AI", "Chaplain")
+ protected_roles = list(
+ "Security Cadet",
+ "Security Officer",
+ "Warden",
+ "Detective",
+ "Head of Security",
+ "Captain",
+ "Blueshield",
+ "Nanotrasen Representative",
+ "Magistrate",
+ "Internal Affairs Agent",
+ "Nanotrasen Navy Officer",
+ "Special Operations Officer",
+ "Syndicate Officer",
+ "Solar Federation General")
+ restricted_species = list("Machine")
+
+/datum/antag_scenario/team/blood_brothers
+ name = "Blood Brothers"
+ config_tag = "blood_brothers"
+ abstract = FALSE
+ antag_role = ROLE_BLOOD_BROTHER
+ antag_special_role = SPECIAL_ROLE_BLOOD_BROTHER
+ antag_datum = /datum/antagonist/blood_brother
+ antag_team = /datum/team/blood_brothers_team
+ required_players = 20
+ cost = 20
+ weight = 1
+ antag_cap = 2
+ candidates_required = 2
+ team_size = 2
+ restricted_roles = list("Cyborg", "AI")
+ protected_roles = list(
+ "Security Cadet",
+ "Security Officer",
+ "Warden",
+ "Detective",
+ "Head of Security",
+ "Captain",
+ "Blueshield",
+ "Nanotrasen Representative",
+ "Magistrate",
+ "Internal Affairs Agent",
+ "Nanotrasen Navy Officer",
+ "Special Operations Officer",
+ "Syndicate Officer",
+ "Solar Federation General")
diff --git a/modular_ss220/antagonists/code/blood_brothers/blood_brothers_datum.dm b/modular_ss220/antagonists/code/blood_brothers/blood_brothers_datum.dm
new file mode 100644
index 000000000000..888677a0921a
--- /dev/null
+++ b/modular_ss220/antagonists/code/blood_brothers/blood_brothers_datum.dm
@@ -0,0 +1,147 @@
+/datum/game_mode
+ var/list/datum/mind/blood_brothers = list()
+
+/datum/antagonist/blood_brother
+ name = "Blood Brother"
+ roundend_category = "Blood Brothers"
+ job_rank = ROLE_BLOOD_BROTHER
+ special_role = SPECIAL_ROLE_BLOOD_BROTHER
+ antag_hud_name = "hudbloodbrother"
+ antag_hud_type = ANTAG_HUD_BLOOD_BROTHER
+ clown_gain_text = {"Ты очень много тренировался, чтобы наконец-то вступить в Синдикат, даже твоя клоунская натура не сможет помешать.
+ Ты уверенно владеешь всем оружием."}
+ clown_removal_text = "Все тренировки пошли насмарку - ты как был клоуном, так и остался."
+ wiki_page_name = "Blood_Brothers"
+ var/datum/team/blood_brothers_team/brothers_team = null
+
+/datum/antagonist/blood_brother/add_owner_to_gamemode()
+ SSticker.mode.blood_brothers |= owner
+
+/datum/antagonist/blood_brother/remove_owner_from_gamemode()
+ SSticker.mode.blood_brothers -= owner
+
+/datum/antagonist/blood_brother/greet()
+ . = ..()
+ SEND_SOUND(owner.current, sound('modular_ss220/antagonists/sound/ambience/antag/blood_brothers_intro.ogg'))
+
+ . += {"Вы ненавидите Нанотрейзен, корпорация дала вам достаточно поводов для этого.
+ Лучшую возможность бороться с ней предоставляет Синдикат, так что вы со своим напарником, разделяющим подобные чувства, связались с ними, чтобы вступить в их ряды.
+ Теперь вы кровные братья и вы готовы сделать все ради общей цели."}
+
+ var/brother_names = get_brother_names_text()
+ if(brother_names)
+ . += "Оберегай и кооперируйся с братьями: [brother_names]. Ведь только вместе вы сможете добиться успеха!"
+ antag_memory += "Ваши братья: [brother_names]
"
+
+ var/meeting_area = get_meeting_area()
+ if(meeting_area)
+ . += "Встреть их в назначенном месте: [meeting_area]"
+ antag_memory += "Место встречи: [meeting_area]
"
+
+ . += "Слава Синдикату!"
+
+/datum/antagonist/blood_brother/create_team(datum/team/blood_brothers_team/team)
+ if(!istype(team))
+ error("Wrong team type passed to [type].")
+ return
+
+ brothers_team = team
+ return brothers_team
+
+/datum/antagonist/blood_brother/get_team()
+ return brothers_team
+
+/datum/antagonist/blood_brother/proc/get_brother_names_text()
+ PRIVATE_PROC(TRUE)
+ var/datum/team/blood_brothers_team/team = get_team()
+ if(!istype(team))
+ return ""
+
+ return team.get_brother_names_text(owner)
+
+/datum/antagonist/blood_brother/proc/get_meeting_area()
+ PRIVATE_PROC(TRUE)
+ var/datum/team/blood_brothers_team/team = get_team()
+ if(!istype(team))
+ return ""
+
+ return team.meeting_area
+
+/datum/antagonist/blood_brother/proc/admin_add(admin, datum/mind/new_antag)
+ if(!new_antag)
+ return FALSE
+
+ if(new_antag.has_antag_datum(/datum/antagonist/blood_brother))
+ alert(admin, "Candidate is already blood brother")
+ return FALSE
+
+ if(!can_be_owned(new_antag))
+ alert(admin, "Candidate can't be blood brother.")
+ return FALSE
+
+ switch(alert(admin, "Create new team or add to existing?", "Blood Brothers", "Create", "Add", "Cancel"))
+ if("Create")
+ return create_new_blood_brothers_team(admin, new_antag)
+ if("Add")
+ return add_to_existing_blood_brothers_team(admin, new_antag)
+
+ return FALSE
+
+/datum/antagonist/blood_brother/proc/create_new_blood_brothers_team(admin, datum/mind/first_brother)
+ PRIVATE_PROC(TRUE)
+ var/list/choices = list()
+ for(var/mob/living/alive_living_mob in GLOB.alive_mob_list)
+ var/datum/mind/mind_to_check = alive_living_mob.mind
+ if(!mind_to_check || mind_to_check == first_brother || !can_be_owned(mind_to_check))
+ continue
+
+ choices["[mind_to_check.name]([alive_living_mob.ckey])"] = mind_to_check
+
+ if(!length(choices))
+ alert(admin, "No candidates for second blood brother found.")
+ return FALSE
+
+ sortTim(choices, GLOBAL_PROC_REF(cmp_text_asc))
+ var/choice = tgui_input_list(admin, "Choose the blood brother.", "Brother", choices)
+ if(!choice)
+ return FALSE
+
+ var/datum/mind/second_brother = choices[choice]
+ if(!second_brother)
+ stack_trace("Chosen second blood brother `[choice]` was `null` for some reason")
+
+ var/datum/team/blood_brothers_team/brother_team = new(list(first_brother, second_brother), FALSE)
+ if(isnull(first_brother.add_antag_datum(src, brother_team)))
+ qdel(brother_team)
+ return FALSE
+
+ if(isnull(second_brother.add_antag_datum(/datum/antagonist/blood_brother, brother_team)))
+ error("Antag datum couldn't be granted to second brother in `/datum/antagonist/blood_brother/proc/create_new_blood_brothers_team`")
+ alert(admin, "Second brother wasn't made into `Blood Brother` for some reason. Try again.")
+ return TRUE
+
+ log_admin("[key_name(admin)] made [key_name(first_brother)] and [key_name(second_brother)] into blood brothers.")
+ return TRUE
+
+/datum/antagonist/blood_brother/proc/add_to_existing_blood_brothers_team(admin, datum/mind/brother_to_add)
+ PRIVATE_PROC(TRUE)
+ var/list/choices = list()
+ for(var/datum/team/blood_brothers_team/team in GLOB.antagonist_teams)
+ var/list/member_ckeys = team.get_member_ckeys()
+ choices["[team.name][length(member_ckeys) ? "([member_ckeys.Join(", ")])" : ""]"] = team
+
+ if(!length(choices))
+ alert(admin, "No blood brother teams found. Try creating new one.")
+ return FALSE
+
+ sortTim(choices, GLOBAL_PROC_REF(cmp_text_asc))
+ var/choice = tgui_input_list(admin, "Choose the blood brothers team.", "Blood Brothers Team", choices)
+ if(!choice)
+ return FALSE
+
+ var/datum/team/blood_brothers_team/chosen_team = choices[choice]
+ if(!chosen_team)
+ stack_trace("Chosen blood brothers team `[choice]` was `null` for some reason.")
+
+
+ return !isnull(brother_to_add.add_antag_datum(src, chosen_team))
diff --git a/modular_ss220/antagonists/code/blood_brothers/blood_brothers_team.dm b/modular_ss220/antagonists/code/blood_brothers/blood_brothers_team.dm
new file mode 100644
index 000000000000..5f3640ecd464
--- /dev/null
+++ b/modular_ss220/antagonists/code/blood_brothers/blood_brothers_team.dm
@@ -0,0 +1,105 @@
+/datum/team/proc/get_member_ckeys()
+ var/list/member_ckeys = list()
+ for(var/datum/mind/member as anything in members)
+ if(!member.current)
+ continue
+
+ member_ckeys += member.current.ckey
+
+ return member_ckeys
+
+
+/datum/team/blood_brothers_team
+ name = "Blood Brothers"
+ antag_datum_type = /datum/antagonist/blood_brother
+ /// Amount of objectives to give
+ var/objectives_amount = 2
+ /// Probability of hijack objective in percent
+ var/hijack_probability = 2
+ /// Selected meeting area given to the team members
+ var/meeting_area = "Согласованная локация"
+ /// List of meeting areas that are randomly selected.
+ var/static/meeting_areas = list(
+ "Бар",
+ "Дормы",
+ "Док отбытия",
+ "Док прибытия",
+ "Голодек",
+ "Ассистентская",
+ "Храм",
+ "Библиотека",
+ )
+ /// List of objective_path -> weight
+ var/static/list/available_objectives = list(
+ /datum/objective/maroon = 1,
+ /datum/objective/assassinate = 1,
+ /datum/objective/assassinateonce = 1,
+ /datum/objective/debrain = 1,
+ /datum/objective/steal = 1,
+ /datum/objective/protect = 1
+ )
+
+/datum/team/blood_brothers_team/New(list/starting_members, add_antag_datum)
+ . = ..()
+ pick_meeting_area()
+ forge_objectives()
+
+/datum/team/blood_brothers_team/add_member(datum/mind/new_member, add_antag_datum)
+ . = ..()
+ update_name()
+
+/datum/team/blood_brothers_team/remove_member(datum/mind/member)
+ . = ..()
+ update_name()
+
+/datum/team/blood_brothers_team/proc/get_brother_names_text(datum/mind/brother_to_exclude)
+ var/list/brother_names = list()
+ for(var/datum/mind/brother as anything in members)
+ if(brother == brother_to_exclude)
+ continue
+
+ brother_names += brother.name
+
+ return brother_names.Join(", ")
+
+/datum/team/blood_brothers_team/proc/pick_meeting_area()
+ PRIVATE_PROC(TRUE)
+ var/chosen_meeting_area = pick(meeting_areas)
+ if(istext(chosen_meeting_area))
+ meeting_area = chosen_meeting_area
+
+/datum/team/blood_brothers_team/proc/update_name()
+ PRIVATE_PROC(TRUE)
+ var/new_name = get_brother_names_text()
+ if(!new_name)
+ name = initial(name)
+ return
+
+ name = "[initial(name)] of [new_name]"
+
+/datum/team/blood_brothers_team/proc/forge_objectives()
+ PRIVATE_PROC(TRUE)
+ var/is_hijacker = prob(hijack_probability)
+ for(var/i in 1 to (objectives_amount - is_hijacker))
+ forge_single_objective()
+
+ if(is_hijacker)
+ add_team_objective(new /datum/objective/hijack)
+ else
+ add_team_objective(new /datum/objective/escape(
+ {"Сбегите на шаттле или спасательной капсуле вместе с братьями.
+ Вы должны быть живы и свободны(Не находиться в бриге шаттла и не быть закованными в наручники).
+ Если хотя бы один из вас не удовлетворяет условиям - задание будет провалено для всех!
+ "}))
+
+/datum/team/blood_brothers_team/proc/forge_single_objective()
+ PRIVATE_PROC(TRUE)
+ if(prob(10) && length(active_ais()))
+ add_team_objective(new /datum/objective/destroy)
+ else
+ var/datum/objective/objective_path = pickweight(available_objectives)
+ if(!ispath(objective_path))
+ error("Wrong objective path in 'available_objectives' of '[type]'")
+ return
+
+ add_team_objective(new objective_path())
diff --git a/modular_ss220/antagonists/code/configuration/antag_mix_configuration.dm b/modular_ss220/antagonists/code/configuration/antag_mix_configuration.dm
new file mode 100644
index 000000000000..ac8fddc37435
--- /dev/null
+++ b/modular_ss220/antagonists/code/configuration/antag_mix_configuration.dm
@@ -0,0 +1,29 @@
+/datum/server_configuration
+ var/datum/configuration_section/antag_mix_gamemode_configuration/antag_mix_gamemode
+
+/datum/configuration_section/antag_mix_gamemode_configuration
+ protection_state = PROTECTION_READONLY
+ /// Antag mix budget multiplied. By default it's 1 - so budged is calculated without any modifications
+ var/budget_multiplier = 1
+ /// Max antag fraction defines the percent of antags relatively to ready players. Must be value between 0 and 1.
+ /// 0 means that no antags can be present, 1 - all players can be antags.
+ var/max_antag_fraction = 0.1
+ /// Assoc list of antag scenario config tag -> list of parameters of this scenarios
+ var/list/params_by_scenario = list()
+
+/datum/server_configuration/load_all_sections()
+ . = ..()
+ antag_mix_gamemode = new()
+ safe_load(antag_mix_gamemode, "antag_mix_gamemode_configuration")
+
+/datum/configuration_section/antag_mix_gamemode_configuration/load_data(list/data)
+ CONFIG_LOAD_NUM(budget_multiplier, data["budget_multiplier"])
+ CONFIG_LOAD_NUM(max_antag_fraction, data["max_antag_fraction"])
+
+ for(var/list/scenario_params in data["antag_scenarios_configuration"])
+ var/tag = scenario_params["tag"]
+ if(!tag)
+ error("`tag` missing in `antag_scenarios_configuration`.")
+ continue
+
+ params_by_scenario[tag] = scenario_params["params"]
diff --git a/modular_ss220/antagonists/code/mind/memory_edit.dm b/modular_ss220/antagonists/code/mind/memory_edit.dm
new file mode 100644
index 000000000000..6ae3574b00f8
--- /dev/null
+++ b/modular_ss220/antagonists/code/mind/memory_edit.dm
@@ -0,0 +1,119 @@
+/datum/mind/edit_memory()
+ if(!SSticker || !SSticker.mode)
+ alert("Not before round-start!", "Alert")
+ return
+
+ var/list/out = list("[name][(current && (current.real_name != name))?" (as [current.real_name])" : ""]")
+ out.Add("Mind currently owned by key: [key] [active ? "(synced)" : "(not synced)"]")
+ out.Add("Assigned role: [assigned_role]. Edit")
+ out.Add("Factions and special roles:")
+
+ var/list/sections = list(
+ "implant",
+ "revolution",
+ "cult",
+ "wizard",
+ "changeling",
+ "vampire", // "traitorvamp",
+ "nuclear",
+ "traitor", // "traitorchan",
+ )
+ var/mob/living/carbon/human/H = current
+ if(ishuman(current))
+ /** Impanted**/
+ sections["implant"] = memory_edit_implant(H)
+ /** REVOLUTION ***/
+ sections["revolution"] = memory_edit_revolution(H)
+ /** WIZARD ***/
+ sections["wizard"] = memory_edit_wizard(H)
+ /** CHANGELING ***/
+ sections["changeling"] = memory_edit_changeling(H)
+ /** VAMPIRE ***/
+ sections["vampire"] = memory_edit_vampire(H)
+ /** NUCLEAR ***/
+ sections["nuclear"] = memory_edit_nuclear(H)
+ /** Abductors **/
+ sections["abductor"] = memory_edit_abductor(H)
+ sections["eventmisc"] = memory_edit_eventmisc(H)
+ /** TRAITOR ***/
+ sections["traitor"] = memory_edit_traitor()
+ /** BLOOD BROTHER **/
+ sections["blood_brother"] = memory_edit_blood_brother()
+ if(!issilicon(current))
+ /** CULT ***/
+ sections["cult"] = memory_edit_cult(H)
+ /** SILICON ***/
+ if(issilicon(current))
+ sections["silicon"] = memory_edit_silicon()
+ /*
+ This prioritizes antags relevant to the current round to make them appear at the top of the panel.
+ Traitorchan and traitorvamp are snowflaked in because they have multiple sections.
+ */
+ if(SSticker.mode.config_tag == "traitorchan")
+ if(sections["traitor"])
+ out.Add(sections["traitor"])
+ if(sections["changeling"])
+ out.Add(sections["changeling"])
+ sections -= "traitor"
+ sections -= "changeling"
+ // Elif technically unnecessary but it makes the following else look better
+ else if(SSticker.mode.config_tag == "traitorvamp")
+ if(sections["traitor"])
+ out.Add(sections["traitor"])
+ if(sections["vampire"])
+ out.Add(sections["vampire"])
+ sections -= "traitor"
+ sections -= "vampire"
+ else
+ if(sections[SSticker.mode.config_tag])
+ out.Add(sections[SSticker.mode.config_tag])
+ sections -= SSticker.mode.config_tag
+
+ for(var/i in sections)
+ if(sections[i])
+ out.Add(sections[i])
+
+ out.Add(memory_edit_uplink())
+
+ out.Add("Memory:")
+ out.Add(memory)
+ out.Add("Edit memory
")
+ out.Add("Objectives:")
+ out.Add(gen_objective_text(admin = TRUE))
+ out.Add("Add objective
")
+ out.Add("Announce objectives
")
+ DIRECT_OUTPUT(usr, browse(out.Join("
"), "window=edit_memory[src];size=500x500"))
+
+/datum/mind/proc/memory_edit_blood_brother()
+ . = _memory_edit_header("blood brother")
+ if(has_antag_datum(/datum/antagonist/blood_brother))
+ . += "BLOOD BROTHER|Remove"
+ else
+ . += "Make Blood Brother"
+
+ . += _memory_edit_role_enabled(ROLE_BLOOD_BROTHER)
+
+
+/datum/mind/Topic(href, href_list)
+ if(!check_rights(R_ADMIN))
+ return
+
+ if(href_list["blood_brother"])
+ switch(href_list["blood_brother"])
+ if("clear")
+ clear_antag_datum(/datum/antagonist/blood_brother)
+ if("make")
+ var/datum/antagonist/blood_brother/brother_antag_datum = new
+ if(!brother_antag_datum.admin_add(usr, src))
+ qdel(brother_antag_datum)
+
+ . = ..()
+
+/datum/mind/proc/clear_antag_datum(datum/antagonist/antag_datum_to_clear)
+ if(!has_antag_datum(antag_datum_to_clear))
+ return
+
+ remove_antag_datum(antag_datum_to_clear)
+ var/antag_name = initial(antag_datum_to_clear.name)
+ log_admin("[key_name(usr)] has removed [antag_name] from [key_name(current)]")
+ message_admins("[key_name_admin(usr)] has removed [antag_name] from [key_name_admin(current)]")
diff --git a/modular_ss220/antagonists/sound/ambience/antag/blood_brothers_intro.ogg b/modular_ss220/antagonists/sound/ambience/antag/blood_brothers_intro.ogg
new file mode 100644
index 000000000000..51e1f421563c
Binary files /dev/null and b/modular_ss220/antagonists/sound/ambience/antag/blood_brothers_intro.ogg differ
diff --git a/modular_ss220/modular_ss220.dme b/modular_ss220/modular_ss220.dme
index 9596927f48f8..53a170e02283 100644
--- a/modular_ss220/modular_ss220.dme
+++ b/modular_ss220/modular_ss220.dme
@@ -76,6 +76,7 @@
#include "queue/_queue.dme"
#include "phrases/_phrases.dme"
#include "species_whitelist/_species_whitelist.dme"
+#include "antagonists/_antagonists.dme"
// --- PRIME --- //
// #define MODPACK_MAIN_ONLY