diff --git a/code/__DEFINES/dcs/datum_signals.dm b/code/__DEFINES/dcs/datum_signals.dm index 19682ccd80d9..fa475cfd2a2a 100644 --- a/code/__DEFINES/dcs/datum_signals.dm +++ b/code/__DEFINES/dcs/datum_signals.dm @@ -129,3 +129,8 @@ /// /datum/component/label /// Called when a handlabeler is used on an item when off #define COMSIG_LABEL_REMOVE "label_remove" + +// /datum/ruleset + +/// from base of /datum/ruleset/proc/can_apply() +#define COMSIG_RULESET_FAILED_SPECIES "failed_species" diff --git a/code/__DEFINES/directions.dm b/code/__DEFINES/directions.dm index b78423b7bcc2..718ebeedf14f 100644 --- a/code/__DEFINES/directions.dm +++ b/code/__DEFINES/directions.dm @@ -35,3 +35,5 @@ #define DIR_JUST_HORIZONTAL(dir) ((dir == EAST) || (dir == WEST)) /// returns TRUE if the direction is NORTH or SOUTH #define DIR_JUST_VERTICAL(dir) ((dir == NORTH) || (dir == SOUTH)) + +#define EXCLUSIVE_OR(thing_one, thing_two) ((thing_one)^(thing_two)) diff --git a/code/__DEFINES/gamemode.dm b/code/__DEFINES/gamemode.dm index 2c9ef8563d63..76332f7fb738 100644 --- a/code/__DEFINES/gamemode.dm +++ b/code/__DEFINES/gamemode.dm @@ -71,3 +71,14 @@ #define NUKE_SITE_OFF_STATION_ZLEVEL 2 /// The bomb's location cannot be found. #define NUKE_SITE_INVALID 3 + +/** + * Dynamic Gamemode Defines + */ +#define DYNAMIC_RULESET_NORMAL "Normal" +#define DYNAMIC_RULESET_FORCED "Forced" +#define DYNAMIC_RULESET_BANNED "Banned" + +#define RULESET_FAILURE_BUDGET "Not enough budget" +#define RULESET_FAILURE_NO_PLAYERS "No drafted players" +#define RULESET_FAILURE_CHANGELING_SECONDARY_RULESET "Needs a secondary ruleset in rotation" diff --git a/code/game/gamemodes/autotraitor/autotraitor.dm b/code/game/gamemodes/autotraitor/autotraitor.dm index dbcf5e38f9ab..cb4e4048bca2 100644 --- a/code/game/gamemodes/autotraitor/autotraitor.dm +++ b/code/game/gamemodes/autotraitor/autotraitor.dm @@ -144,3 +144,6 @@ message_admins("New traitor roll passed. Making a new Traitor.") log_game("New traitor roll passed. Making a new Traitor.") character.mind.make_Traitor() + +/datum/game_mode/traitor/autotraitor/on_mob_cryo(mob/sleepy_mob, obj/machinery/cryopod/cryopod) + possible_traitors.Remove(sleepy_mob) diff --git a/code/game/gamemodes/dynamic/antag_rulesets.dm b/code/game/gamemodes/dynamic/antag_rulesets.dm new file mode 100644 index 000000000000..3e56784bdbb2 --- /dev/null +++ b/code/game/gamemodes/dynamic/antag_rulesets.dm @@ -0,0 +1,192 @@ +/** + * These are gamemode rulesets for the dynamic gamemode type. They determine what antagonists spawn during a round. + */ +/datum/ruleset + /// What this ruleset is called + var/name = "BASE RULESET" + /// The cost to roll this ruleset + var/ruleset_cost = 1 + /// The weight to roll this ruleset + var/ruleset_weight = 1 + /// The cost to roll an antagonist of this ruleset + var/antag_cost = 1 + /// The weight to roll an antagonist of this ruleset + var/antag_weight = 1 + /// Antagonist datum to apply to users + var/datum/antagonist/antagonist_type + /// A ruleset to be added when this ruleset is selected by the gamemode + var/datum/ruleset/implied/implied_ruleset_type + + /// These roles 100% cannot be this antagonist + var/list/banned_jobs = list("Cyborg") + /// These roles can't be antagonists because mindshielding (this can be disabled via config) + var/list/protected_jobs = list( + "Security Officer", + "Warden", + "Detective", + "Head of Security", + "Captain", + "Blueshield", + "Nanotrasen Representative", + "Magistrate", + "Internal Affairs Agent", + "Nanotrasen Career Trainer", + "Nanotrasen Navy Officer", + "Special Operations Officer", + "Trans-Solar Federation General" + ) + /// Applies the mind roll to assigned_role, preventing them from rolling a normal job. Good for wizards and nuclear operatives. + var/assign_job_role = FALSE + /// A blacklist of species names that cannot play this antagonist + var/list/banned_species = list() + /// If true, the species blacklist is now a species whitelist + var/banned_species_only = FALSE + + // var/list/banned_mutual_rulesets = list() // UNIMPLEMENTED: could be used to prevent nukies rolling while theres cultists, or wizards, etc + + /* This stuff changes, all stuff above is static */ + /// How many antagonists to spawn + var/antag_amount = 0 + /// All of the minds that we will make into our antagonist type + var/list/datum/mind/pre_antags = list() + +/datum/ruleset/Destroy(force, ...) + stack_trace("[src] ([type]) was destroyed.") + return ..() + +/datum/ruleset/proc/ruleset_possible(ruleset_budget, rulesets) + if(ruleset_budget < ruleset_cost) + return RULESET_FAILURE_BUDGET + if(!length(SSticker.mode.get_players_for_role(antagonist_type::job_rank))) // this specifically needs to be job_rank not special_rank + return RULESET_FAILURE_NO_PLAYERS + +/datum/ruleset/proc/antagonist_possible(budget) + return budget >= antag_cost + +/datum/ruleset/proc/pre_setup() + if(antag_amount == 0) + return + if(antag_amount < 0) + stack_trace("/datum/ruleset/proc/pre_setup() for [type] somehow had a negative antagonist amount") + return + var/list/datum/mind/possible_antags = SSticker.mode.get_players_for_role(antagonist_type::job_rank) // this specifically needs to be job_rank not special_rank + if(!length(possible_antags)) + refund("No possible players for [src] ruleset.") // we allocate antag budget before we allocate players, and previous rulesets can steal our players + return antag_cost * antag_amount // shitty refund for now + + if(GLOB.configuration.gamemode.prevent_mindshield_antags) + banned_jobs += protected_jobs + + shuffle_inplace(possible_antags) + for(var/datum/mind/antag as anything in possible_antags) + if(antag_amount <= 0) + break + if(!can_apply(antag)) + continue + pre_antags += antag + if(assign_job_role) + antag.assigned_role = antagonist_type::special_role + antag.special_role = antagonist_type::special_role + antag.restricted_roles = banned_jobs + antag_amount -= 1 + + if(antag_amount > 0) + refund("Missing [antag_amount] antagonists for [src] ruleset.") + return antag_cost * antag_amount // shitty refund for now + +/datum/ruleset/proc/can_apply(datum/mind/antag) + if(EXCLUSIVE_OR(antag.current.client.prefs.active_character.species in banned_species, banned_species_only)) + SEND_SIGNAL(src, COMSIG_RULESET_FAILED_SPECIES) + return FALSE + if(antag.special_role) // You can only have 1 antag roll at a time, sorry + return FALSE + return TRUE + +/datum/ruleset/proc/post_setup(datum/game_mode/dynamic) + for(var/datum/mind/antag as anything in pre_antags) + antag.add_antag_datum(antagonist_type) + +/datum/ruleset/proc/refund(info) + // not enough antagonists signed up!!! idk what to do. The only real solution is to procedurally allocate budget, which will result in 1000x more get_players_for_role() calls. Which is not cheap. + // OR we cache get_players_for_role() and then just check if they have a special_role. May be unreliable. + // log_dynamic("[info] Refunding [antag_cost * antag_amount] budget.") + // Currently unimplemented. Will be useful for a possible future PR where latejoin antagonists are factored in. + return + +/datum/ruleset/traitor + name = "Traitor" + ruleset_weight = 11 + antag_cost = 5 + antag_weight = 2 + antagonist_type = /datum/antagonist/traitor + +/datum/ruleset/traitor/post_setup(datum/game_mode/dynamic) + var/random_time = rand(5 MINUTES, 15 MINUTES) + for(var/datum/mind/antag as anything in pre_antags) + var/datum/antagonist/traitor/traitor_datum = new antagonist_type() + if(ishuman(antag.current)) + traitor_datum.delayed_objectives = TRUE + traitor_datum.addtimer(CALLBACK(traitor_datum, TYPE_PROC_REF(/datum/antagonist/traitor, reveal_delayed_objectives)), random_time, TIMER_DELETE_ME) + antag.add_antag_datum(traitor_datum) + addtimer(CALLBACK(dynamic, TYPE_PROC_REF(/datum/game_mode, fill_antag_slots)), random_time) + +/datum/ruleset/vampire + name = "Vampire" + ruleset_weight = 12 + antag_cost = 10 + antagonist_type = /datum/antagonist/vampire + + banned_jobs = list("Cyborg", "AI", "Chaplain") + banned_species = list("Machine") + implied_ruleset_type = /datum/ruleset/implied/mindflayer + +/datum/ruleset/changeling + name = "Changeling" + ruleset_weight = 9 + antag_cost = 10 + antagonist_type = /datum/antagonist/changeling + + banned_jobs = list("Cyborg", "AI") + banned_species = list("Machine") + implied_ruleset_type = /datum/ruleset/implied/mindflayer + +/datum/ruleset/changeling/ruleset_possible(ruleset_budget, rulesets) + // Theres already a ruleset, we're good to go + if(length(rulesets)) + return ..() + // We're the first ruleset, but we can afford another ruleset + if((ruleset_budget >= /datum/ruleset/traitor::ruleset_cost) || (ruleset_budget >= /datum/ruleset/vampire::ruleset_cost)) + return ..() + return RULESET_FAILURE_CHANGELING_SECONDARY_RULESET + +// This is the fucking worst, but its required to not change functionality with mindflayers. Cannot be rolled normally, this is applied by other methods. +/datum/ruleset/implied + // These 3 variables should never change + ruleset_cost = 0 + ruleset_weight = 0 + antag_weight = 0 + // antag_cost is allowed to be edited to help with refunding antagonists + antag_cost = 0 + /// This signal is registered on whatever (multiple) rulesets implied us. This will call on_implied. + var/target_signal + /// Set this to true if this implied ruleset was activated + var/was_triggered = FALSE + +/datum/ruleset/implied/proc/on_implied(datum/antagonist/implier) + stack_trace("[type]/on_implied() not implemented!") + +/datum/ruleset/implied/mindflayer + name = "Mindflayer" + antagonist_type = /datum/antagonist/mindflayer + antag_cost = 10 + target_signal = COMSIG_RULESET_FAILED_SPECIES + + banned_jobs = list("Cyborg", "AI") + banned_species = list("Machine") + banned_species_only = TRUE + +/datum/ruleset/implied/mindflayer/on_implied(datum/ruleset/implier) + // log_dynamic("Rolled implied [name]: +1 [name], -1 [implier.name].") + implier.antag_amount -= 1 + antag_amount += 1 + was_triggered = TRUE diff --git a/code/game/gamemodes/dynamic/dynamic.dm b/code/game/gamemodes/dynamic/dynamic.dm new file mode 100644 index 000000000000..1e0e1ec4ad2e --- /dev/null +++ b/code/game/gamemodes/dynamic/dynamic.dm @@ -0,0 +1,173 @@ +GLOBAL_LIST_EMPTY(dynamic_forced_rulesets) + +/datum/game_mode/dynamic + name = "Dynamic" + config_tag = "dynamic" + secondary_restricted_jobs = list("AI") + required_players = 10 + /// Non-implied rulesets in play + var/list/datum/ruleset/rulesets = list() + /// Implied rulesets that are in play + var/list/datum/ruleset/implied_rulesets = list() + + /// How much budget is left after roundstart antagonists roll + var/budget_overflow = 0 + +/datum/game_mode/dynamic/announce() + to_chat(world, "The current game mode is - Dynamic") + var/list/possible_rulesets = list() + for(var/datum/ruleset/ruleset as anything in subtypesof(/datum/ruleset)) + if(ruleset.ruleset_weight <= 0) + continue + possible_rulesets |= ruleset.name + if(ruleset.implied_ruleset_type) + possible_rulesets |= ruleset.implied_ruleset_type.name + to_chat(world, "Possible Rulesets: [english_list(possible_rulesets)]") + +/datum/game_mode/dynamic/proc/allocate_ruleset_budget() + var/ruleset_budget = text2num(GLOB.dynamic_forced_rulesets["budget"] || pickweight(list("0" = 3, "1" = 5, "2" = 12, "3" = 3))) // more likely to or 2 + // log_dynamic("Allocated gamemode budget: [ruleset_budget]") + var/list/possible_rulesets = list() + for(var/datum/ruleset/ruleset as anything in subtypesof(/datum/ruleset)) + if(ruleset.ruleset_weight <= 0) + continue + if(GLOB.dynamic_forced_rulesets[ruleset] == DYNAMIC_RULESET_BANNED) + continue + var/datum/ruleset/new_ruleset = new ruleset() + possible_rulesets[new_ruleset] = new_ruleset.ruleset_weight + + // log_dynamic("Available rulesets: [english_list(possible_rulesets)]") + + for(var/datum/ruleset/ruleset as anything in GLOB.dynamic_forced_rulesets) + if(ruleset == "budget") + continue + if(GLOB.dynamic_forced_rulesets[ruleset] != DYNAMIC_RULESET_FORCED) + continue + if(!ispath(ruleset, /datum/ruleset)) + stack_trace("Non-ruleset in GLOB.dynamic_forced_rulesets: \"[ruleset]\" ([ruleset?.type])") + continue + // log_dynamic("Forcing ruleset: [ruleset.name]") + ruleset_budget -= pick_ruleset(new ruleset, ruleset_budget, force = TRUE) + for(var/datum/ruleset/old_ruleset in possible_rulesets) + if(old_ruleset.type == ruleset) + possible_rulesets -= old_ruleset + qdel(old_ruleset) + + while(ruleset_budget >= 0) + var/datum/ruleset/ruleset = pickweight(possible_rulesets) + if(!ruleset) + // log_dynamic("No more available rulesets") + return + ruleset_budget -= pick_ruleset(ruleset, ruleset_budget) + possible_rulesets -= ruleset + // log_dynamic("No more ruleset budget") + +/datum/game_mode/dynamic/proc/pick_ruleset(datum/ruleset/ruleset, ruleset_budget, force) + if(!ruleset) + return + if(!force) + var/failure_reason = ruleset.ruleset_possible(ruleset_budget, rulesets) + if(failure_reason) + // log_dynamic("Failed [ruleset.name] ruleset: [failure_reason]") + return + // log_dynamic("Rolled ruleset: [ruleset.name]") + rulesets[ruleset] = ruleset.antag_weight + . = ruleset.ruleset_cost // return the ruleset cost to be subtracted from the gamemode budget + if(!ruleset.implied_ruleset_type) + return + + var/datum/ruleset/implied/implied = locate(ruleset.implied_ruleset_type) in implied_rulesets + if(!implied) + // log_dynamic("Adding implied ruleset: [ruleset.implied_ruleset_type.name]") + implied = new ruleset.implied_ruleset_type + implied_rulesets += implied + implied.RegisterSignal(ruleset, implied.target_signal, TYPE_PROC_REF(/datum/ruleset/implied, on_implied)) + +/datum/game_mode/dynamic/proc/allocate_antagonist_budget() + if(!length(rulesets)) + // log_dynamic("No rulesets in play.") + return + var/budget = num_players() + // log_dynamic("Allocated antagonist budget: [budget].") + + for(var/datum/ruleset/ruleset in rulesets) + ruleset.antag_amount = 1 + budget -= ruleset.antag_cost + // log_dynamic("Automatic deduction: +1 [ruleset.name]. Remaining budget: [budget].") + + // log_dynamic("Rulesets in play: [english_list((rulesets + implied_rulesets))]") + + apply_antag_budget(budget) + +/datum/game_mode/dynamic/proc/apply_antag_budget(budget) // todo, can be called later in the game to apply more budget. That also means there has to be shit done for latejoins. + var/list/temp_rulesets = rulesets.Copy() + while(budget >= 0) + var/datum/ruleset/ruleset = pickweight(temp_rulesets) + if(!ruleset) + // log_dynamic("No rulesets remaining. Remaining budget: [budget].") + budget_overflow = budget + return + if(!ruleset.antagonist_possible(budget)) + // log_dynamic("Rolled [ruleset.name]: failed, removing [ruleset.name] ruleset.") + temp_rulesets -= ruleset + continue + ruleset.antag_amount++ + budget -= ruleset.antag_cost + // log_dynamic("Rolled [ruleset.name]: success, +1 [ruleset.name]. Remaining budget: [budget].") + // log_dynamic("No more antagonist budget remaining.") + +/datum/game_mode/dynamic/pre_setup() + // var/watch = start_watch() + // log_dynamic("Starting dynamic setup.") + allocate_ruleset_budget() + // log_dynamic("-=-=-=-=-=-=-=-=-=-=-=-=-") + allocate_antagonist_budget() + // log_dynamic("=-=-=-=-=-=-=-=-=-=-=-=-=") + + for(var/datum/ruleset/ruleset in (rulesets + implied_rulesets)) // rulesets first, then implied rulesets + // log_dynamic("Applying [ruleset.antag_amount] [ruleset.name]\s.") + budget_overflow += ruleset.pre_setup() + + // log_dynamic("Budget overflow: [budget_overflow].") + // for the future, maybe try readding antagonists with apply_antag_budget(budget_overflow) + // log_dynamic("Finished dynamic setup in [stop_watch(watch)]s") + return TRUE + +/datum/game_mode/dynamic/post_setup() + for(var/datum/ruleset/ruleset in (rulesets + implied_rulesets)) + // if(length(ruleset.pre_antags)) + // log_dynamic("Making antag datums for [ruleset.name] ruleset.") + ruleset.post_setup(src) + ..() + +/datum/game_mode/dynamic/traitors_to_add() + . = floor(budget_overflow / /datum/ruleset/traitor::antag_cost) + budget_overflow -= (. * /datum/ruleset/traitor::antag_cost) + +/datum/game_mode/dynamic/latespawn(mob) + . = ..() + budget_overflow++ + +/datum/game_mode/dynamic/on_mob_cryo(mob/sleepy_mob, obj/machinery/cryopod/cryopod) + var/turf/T = get_turf(cryopod) + if(!T || is_admin_level(T.z)) + return + budget_overflow-- + if(!sleepy_mob.mind || !length(sleepy_mob.mind.antag_datums)) + return + for(var/datum/antagonist/antag in sleepy_mob.mind.antag_datums) + for(var/datum/ruleset/possible_ruleset as anything in subtypesof(/datum/ruleset)) + if(istype(antag, possible_ruleset.antagonist_type)) + budget_overflow += possible_ruleset.antag_cost + +/datum/game_mode/dynamic/get_webhook_name() + var/list/implied_and_used = list() + for(var/datum/ruleset/implied/implied as anything in implied_rulesets) + if(implied.was_triggered) + implied_and_used += implied + return "[name] ([english_list(rulesets + implied_and_used, nothing_text = "Extended")])" + +// /proc/log_dynamic(text) +// for(var/client/C in GLOB.admins) +// if(check_rights(R_DEBUG, FALSE, C.mob) && (C.prefs.toggles & PREFTOGGLE_CHAT_DEBUGLOGS)) +// to_chat(C, "DYNAMIC: [text]", MESSAGE_TYPE_DEBUG, confidential = TRUE) diff --git a/code/game/gamemodes/game_mode.dm b/code/game/gamemodes/game_mode.dm index 39c7f0c0d560..fe838e735d09 100644 --- a/code/game/gamemodes/game_mode.dm +++ b/code/game/gamemodes/game_mode.dm @@ -249,7 +249,7 @@ for(var/tech_id in SSeconomy.tech_levels) SSblackbox.record_feedback("tally", "cargo max tech level sold", SSeconomy.tech_levels[tech_id], tech_id) - GLOB.discord_manager.send2discord_simple(DISCORD_WEBHOOK_PRIMARY, "A round of [name] has ended - [surviving_total] survivors, [ghosts] ghosts.") + GLOB.discord_manager.send2discord_simple(DISCORD_WEBHOOK_PRIMARY, "A round of [get_webhook_name()] has ended - [surviving_total] survivors, [ghosts] ghosts.") if(SSredis.connected) // Send our presence to required channels var/list/presence_data = list() @@ -653,7 +653,7 @@ if(length(traitors) < traitors_to_add()) traitors_to_add += (traitors_to_add() - length(traitors)) - if(!traitors_to_add) + if(traitors_to_add <= 0) return var/list/potential_recruits = get_alive_players_for_role(ROLE_TRAITOR) @@ -671,3 +671,9 @@ traitor.special_role = SPECIAL_ROLE_TRAITOR traitor.restricted_roles = restricted_jobs traitor.add_antag_datum(/datum/antagonist/traitor) // They immediately get a new objective + +/datum/game_mode/proc/get_webhook_name() + return name + +/datum/game_mode/proc/on_mob_cryo(mob/sleepy_mob, obj/machinery/cryopod/cryopod) + return diff --git a/code/game/machinery/cryopod.dm b/code/game/machinery/cryopod.dm index 327d6885e6a7..fdcfc2518104 100644 --- a/code/game/machinery/cryopod.dm +++ b/code/game/machinery/cryopod.dm @@ -339,7 +339,7 @@ //Delete all items not on the preservation list. var/list/items = contents - items -= occupant // Don't delete the occupant + items -= occupant // Don't delete the occupant // this fucking nullspaces the occupant btw, i fuckin hate old code items -= announce // or the autosay radio. ADD_TRAIT(occupant, TRAIT_CRYO_DESPAWNING, TRAIT_GENERIC) @@ -367,6 +367,8 @@ if(IS_SACRIFICE_TARGET(occupant.mind)) SSticker.mode.cult_team.find_new_sacrifice_target() + SSticker.mode.on_mob_cryo(occupant, src) + //Update any existing objectives involving this mob. if(occupant.mind) if(occupant.mind.initial_account) @@ -385,10 +387,6 @@ if(occupant.mind.objective_holder.clear()) occupant.mind.special_role = null - else - if(SSticker.mode.name == "AutoTraitor") - var/datum/game_mode/traitor/autotraitor/current_mode = SSticker.mode - current_mode.possible_traitors.Remove(occupant) // Delete them from datacore. diff --git a/code/modules/admin/misc_admin_procs.dm b/code/modules/admin/misc_admin_procs.dm index e3349394eba6..bf1c706cac86 100644 --- a/code/modules/admin/misc_admin_procs.dm +++ b/code/modules/admin/misc_admin_procs.dm @@ -327,6 +327,8 @@ GLOBAL_VAR_INIT(disable_explosions, FALSE) dat += "
" if(GLOB.master_mode == "secret") dat += "" + if(GLOB.master_mode == "dynamic" || (GLOB.master_mode == "secret" && GLOB.secret_force_mode == "dynamic")) + dat += "" dat += "