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