From bb8e2a91bd502b288f0f3773a772404164968d54 Mon Sep 17 00:00:00 2001 From: JaySparrow Date: Thu, 24 Sep 2020 23:54:21 -0500 Subject: [PATCH] Dynamic Mode for reals Here we go. --- code/__DEFINES/maths.dm | 7 + code/__HELPERS/roundend.dm | 9 + code/__HELPERS/time.dm | 8 + .../configuration/entries/game_options.dm | 2 + code/controllers/subsystem/events.dm | 2 + code/controllers/subsystem/ticker.dm | 7 +- code/controllers/subsystem/vote.dm | 65 +- code/game/gamemodes/dynamic/dynamic.dm | 842 ++++++++++++++++++ .../gamemodes/dynamic/dynamic_rulesets.dm | 227 +++++ .../dynamic/dynamic_rulesets_events.dm | 676 ++++++++++++++ .../dynamic/dynamic_rulesets_latejoin.dm | 112 +++ .../dynamic/dynamic_rulesets_midround.dm | 599 +++++++++++++ .../dynamic/dynamic_rulesets_roundstart.dm | 762 ++++++++++++++++ code/game/gamemodes/dynamic/readme.md | 57 ++ code/game/gamemodes/game_mode.dm | 4 + code/modules/admin/admin.dm | 57 ++ code/modules/admin/topic.dm | 293 ++++++ hyperstation/code/gamemode/traitor_thief.dm | 146 +++ tgstation.dme | 7 + 19 files changed, 3875 insertions(+), 7 deletions(-) create mode 100644 code/game/gamemodes/dynamic/dynamic.dm create mode 100644 code/game/gamemodes/dynamic/dynamic_rulesets.dm create mode 100644 code/game/gamemodes/dynamic/dynamic_rulesets_events.dm create mode 100644 code/game/gamemodes/dynamic/dynamic_rulesets_latejoin.dm create mode 100644 code/game/gamemodes/dynamic/dynamic_rulesets_midround.dm create mode 100644 code/game/gamemodes/dynamic/dynamic_rulesets_roundstart.dm create mode 100644 code/game/gamemodes/dynamic/readme.md create mode 100644 hyperstation/code/gamemode/traitor_thief.dm diff --git a/code/__DEFINES/maths.dm b/code/__DEFINES/maths.dm index 402c3dc1..b6d1b771 100644 --- a/code/__DEFINES/maths.dm +++ b/code/__DEFINES/maths.dm @@ -209,4 +209,11 @@ return list(region_x1 & region_x2, region_y1 & region_y2) + +#define EXP_DISTRIBUTION(desired_mean) ( -(1/(1/desired_mean)) * log(rand(1, 1000) * 0.001) ) + +#define LORENTZ_DISTRIBUTION(x, s) ( s*TAN(TODEGREES(PI*(rand()-0.5))) + x ) +#define LORENTZ_CUMULATIVE_DISTRIBUTION(x, y, s) ( (1/PI)*TORADIANS(arctan((x-y)/s)) + 1/2 ) + +#define RULE_OF_THREE(a, b, x) ((a*x)/b) // ) diff --git a/code/__HELPERS/roundend.dm b/code/__HELPERS/roundend.dm index 6a80f230..834fbea9 100644 --- a/code/__HELPERS/roundend.dm +++ b/code/__HELPERS/roundend.dm @@ -310,6 +310,15 @@ parts += "[FOURSPACES]Nobody died this shift!" return parts.Join("
") + if(istype(SSticker.mode, /datum/game_mode/dynamic)) + var/datum/game_mode/dynamic/mode = SSticker.mode + //chaos level + parts += "[FOURSPACES]Threat level: [mode.threat_level]" + parts += "[FOURSPACES]Threat left: [mode.threat]" + parts += "[FOURSPACES]Executed rules:" + for(var/datum/dynamic_ruleset/rule in mode.executed_rules) + parts += "[FOURSPACES][FOURSPACES][rule.ruletype] - [rule.name]: -[rule.cost] threat" + /client/proc/roundend_report_file() return "data/roundend_reports/[ckey].html" diff --git a/code/__HELPERS/time.dm b/code/__HELPERS/time.dm index 4362f33a..83381318 100644 --- a/code/__HELPERS/time.dm +++ b/code/__HELPERS/time.dm @@ -78,3 +78,11 @@ GLOBAL_VAR_INIT(rollovercheck_last_timeofday, 0) /proc/daysSince(realtimev) return round((world.realtime - realtimev) / (24 HOURS)) + +/proc/worldtime2text() + return gameTimestamp("hh:mm:ss", world.time) + +/proc/gameTimestamp(format = "hh:mm:ss", wtime=null) + if(!wtime) + wtime = world.time + return time2text(wtime - GLOB.timezoneOffset, format) \ No newline at end of file diff --git a/code/controllers/configuration/entries/game_options.dm b/code/controllers/configuration/entries/game_options.dm index 77976f90..f9bb532d 100644 --- a/code/controllers/configuration/entries/game_options.dm +++ b/code/controllers/configuration/entries/game_options.dm @@ -364,3 +364,5 @@ /datum/config_entry/number/auto_transfer_delay config_entry_value = 72000 min_val = 0 + +/datum/config_entry/flag/dynamic_mode \ No newline at end of file diff --git a/code/controllers/subsystem/events.dm b/code/controllers/subsystem/events.dm index abc46cba..b45af8e2 100644 --- a/code/controllers/subsystem/events.dm +++ b/code/controllers/subsystem/events.dm @@ -52,6 +52,8 @@ SUBSYSTEM_DEF(events) //decides which world.time we should select another random event at. /datum/controller/subsystem/events/proc/reschedule() scheduled = world.time + rand(frequency_lower, max(frequency_lower,frequency_upper)) + if(GLOB.master_mode == "dynamic") + scheduled = INFINITY //HYPER CHANGE - Easy way to bypass the basic event handler. //selects a random event based on whether it can occur and it's 'weight'(probability) /datum/controller/subsystem/events/proc/spawnEvent() diff --git a/code/controllers/subsystem/ticker.dm b/code/controllers/subsystem/ticker.dm index c1b9c203..f07371c0 100644 --- a/code/controllers/subsystem/ticker.dm +++ b/code/controllers/subsystem/ticker.dm @@ -478,7 +478,12 @@ SUBSYSTEM_DEF(ticker) if(SSticker.timeLeft < 900) SSticker.timeLeft = 900 SSticker.modevoted = TRUE - SSvote.initiate_vote("roundtype","server",TRUE) + var/dynamic = CONFIG_GET(flag/dynamic_mode) //WORK DAMN YOU + //var/dynamic = TRUE //FUCK IT + if(!dynamic) + SSvote.initiate_vote("roundtype","server",TRUE) + else + SSvote.initiate_vote("dynamic","server",TRUE) /datum/controller/subsystem/ticker/Recover() current_state = SSticker.current_state diff --git a/code/controllers/subsystem/vote.dm b/code/controllers/subsystem/vote.dm index 1ec5f1d0..fe7f197b 100644 --- a/code/controllers/subsystem/vote.dm +++ b/code/controllers/subsystem/vote.dm @@ -53,11 +53,23 @@ SUBSYSTEM_DEF(vote) //get the highest number of votes var/greatest_votes = 0 var/total_votes = 0 - for(var/option in choices) - var/votes = choices[option] - total_votes += votes - if(votes > greatest_votes) - greatest_votes = votes + + //Catch for dynamic vote. We want to return all the votes. + if(mode == "dynamic") + return choices //Just return everything to handle + /* + . = list() + for(var/option in choices) + . += option + return . + */ + else + for (var/option in choices) + var/votes = choices[option] + total_votes += votes + if(votes > greatest_votes) + greatest_votes = votes + //default-vote for everyone who didn't vote if(!CONFIG_GET(flag/default_no_vote) && choices.len) var/list/non_voters = GLOB.directory.Copy() @@ -107,7 +119,43 @@ SUBSYSTEM_DEF(vote) if(was_roundtype_vote) stored_gamemode_votes[choices[i]] = votes text += "\n[choices[i]]: [obfuscated ? "???" : votes]" //CIT CHANGE - adds obfuscated votes - if(mode != "custom") + + //Dynamic mode + if(mode == "dynamic") + text = "\n Dynamic Chaos Vote: " + var/numbers = list() + var/v = 0 + for (var/option in winners) //Everyone is a winner in aggregate vote + //We returned choices, which is now winners. So syntax and variables gets a little weird here. + switch(option) //If there is a proc for string to digits, I couldn't find it. Might clean this later. + if("0") + v += winners[option] //Add the number votes to the pool + numbers += (winners[option]*0) //Add the value of the vote to numbers + if("1") + v += winners[option] //Add the number votes to the pool + numbers += (winners[option]*1) //Add the value of the vote to numbers + if("2") + v += winners[option] //Add the number votes to the pool + numbers += (winners[option]*2) //Add the value of the vote to numbers + if("3") + v += winners[option] //Add the number votes to the pool + numbers += (winners[option]*3) //Add the value of the vote to numbers + if("4") + v += winners[option] //Add the number votes to the pool + numbers += (winners[option]*4) //Add the value of the vote to numbers + if("5") + v += winners[option] //Add the number votes to the pool + numbers += (winners[option]*5) //Add the value of the vote to numbers + if (v == 0) + . = 2.5 //Default no vote value. Will change to define later + else + var/total = 0 + for (var/i in numbers) + total += i + . = (total / v) + text += "\nChaos level [obfuscated ? "???" : .]" + + if(mode != "custom" && mode != "dynamic") if(winners.len > 1 && !obfuscated) //CIT CHANGE - adds obfuscated votes text = "\nVote Tied Between:" for(var/option in winners) @@ -141,6 +189,9 @@ SUBSYSTEM_DEF(vote) SSticker.save_mode(.) message_admins("The gamemode has been voted for, and has been changed to: [GLOB.master_mode]") log_admin("Gamemode has been voted for and switched to: [GLOB.master_mode].") + if("dynamic") + GLOB.master_mode = "dynamic" + GLOB.dynamic_chaos_level = . if("restart") if(. == "Restart Round") restart = 1 @@ -228,6 +279,8 @@ SUBSYSTEM_DEF(vote) choices.Add("Initiate Crew Transfer","Continue Playing") // austation end if("roundtype") //CIT CHANGE - adds the roundstart secret/extended vote choices.Add("secret", "extended") + if("dynamic") + choices.Add("0","1","2","3","4","5") if("custom") question = stripped_input(usr,"What is the vote for?") if(!question) diff --git a/code/game/gamemodes/dynamic/dynamic.dm b/code/game/gamemodes/dynamic/dynamic.dm new file mode 100644 index 00000000..d2113a69 --- /dev/null +++ b/code/game/gamemodes/dynamic/dynamic.dm @@ -0,0 +1,842 @@ +#define CURRENT_LIVING_PLAYERS 1 +#define CURRENT_LIVING_ANTAGS 2 +#define CURRENT_DEAD_PLAYERS 3 +#define CURRENT_OBSERVERS 4 + +#define ONLY_RULESET 1 +#define HIGHLANDER_RULESET 2 +#define TRAITOR_RULESET 4 +#define MINOR_RULESET 8 + +#define RULESET_STOP_PROCESSING 1 + +// -- Injection delays +GLOBAL_VAR_INIT(dynamic_latejoin_delay_min, (5 MINUTES)) +GLOBAL_VAR_INIT(dynamic_latejoin_delay_max, (25 MINUTES)) + +GLOBAL_VAR_INIT(dynamic_midround_delay_min, (15 MINUTES)) +GLOBAL_VAR_INIT(dynamic_midround_delay_max, (35 MINUTES)) + +GLOBAL_VAR_INIT(dynamic_event_delay_min, (3 MINUTES)) +GLOBAL_VAR_INIT(dynamic_event_delay_max, (10 MINUTES)) + +GLOBAL_VAR_INIT(dynamic_threat_delay, (5 MINUTES)) + +// Are HIGHLANDER_RULESETs allowed to stack? +GLOBAL_VAR_INIT(dynamic_no_stacking, TRUE) +// A number between -5 and +5. +// A negative value will give a more peaceful round and +// a positive value will give a round with higher threat. +GLOBAL_VAR_INIT(dynamic_curve_centre, 0) +// A number between 0.5 and 4. +// Higher value will favour extreme rounds and +// lower value rounds closer to the average. +GLOBAL_VAR_INIT(dynamic_curve_width, 1.8) +// If enabled only picks a single starting rule and executes only autotraitor midround ruleset. +GLOBAL_VAR_INIT(dynamic_classic_secret, FALSE) +// How many roundstart players required for high population override to take effect. +GLOBAL_VAR_INIT(dynamic_high_pop_limit, 55) +// If enabled does not accept or execute any rulesets. +GLOBAL_VAR_INIT(dynamic_forced_extended, FALSE) +// How high threat is required for HIGHLANDER_RULESETs stacking. +// This is independent of dynamic_no_stacking. +GLOBAL_VAR_INIT(dynamic_stacking_limit, 90) +// List of forced roundstart rulesets. +GLOBAL_LIST_EMPTY(dynamic_forced_roundstart_ruleset) +// Forced threat level, setting this to zero or higher forces the roundstart threat to the value. +GLOBAL_VAR_INIT(dynamic_forced_threat_level, -1) +// Chaos level +GLOBAL_VAR_INIT(dynamic_chaos_level, 1.5) + +/datum/game_mode/dynamic + name = "dynamic mode" + config_tag = "dynamic" + + announce_span = "danger" + announce_text = "Dynamic mode!" // This needs to be changed maybe + + reroll_friendly = FALSE; + + // Threat logging vars + /// The "threat cap", threat shouldn't normally go above this and is used in ruleset calculations + ///Changed for Hyper + var/threat_level = 0 + /// Set at the beginning of the round. Spent by the mode to "purchase" rules. + ///Changed for Hyper + var/threat = 0 + /// Running information about the threat. Can store text or datum entries. + var/list/threat_log = list() + /// List of roundstart rules used for selecting the rules. + var/list/roundstart_rules = list() + /// List of latejoin rules used for selecting the rules. + var/list/latejoin_rules = list() + /// List of midround rules used for selecting the rules. + var/list/midround_rules = list() + /// List of event rules for selecting midround events + var/list/event_rules = list() + /** # Pop range per requirement. + * If the value is five the range is: + * 0-4, 5-9, 10-14, 15-19, 20-24, 25-29, 30-34, 35-39, 40-54, 45+ + * If it is six the range is: + * 0-5, 6-11, 12-17, 18-23, 24-29, 30-35, 36-41, 42-47, 48-53, 54+ + * If it is seven the range is: + * 0-6, 7-13, 14-20, 21-27, 28-34, 35-41, 42-48, 49-55, 56-62, 63+ + */ + var/pop_per_requirement = 6 + /// The requirement used for checking if a second rule should be selected. + var/list/second_rule_req = list(100, 100, 80, 70, 60, 50, 30, 20, 10, 0) + /// The requirement used for checking if a third rule should be selected. + var/list/third_rule_req = list(100, 100, 100, 90, 80, 70, 60, 50, 40, 30) + /// Threat requirement for a second ruleset when high pop override is in effect. + var/high_pop_second_rule_req = 40 + /// Threat requirement for a third ruleset when high pop override is in effect. + var/high_pop_third_rule_req = 60 + /// Number of players who were ready on roundstart. + var/roundstart_pop_ready = 0 + /// List of candidates used on roundstart rulesets. + var/list/candidates = list() + /// Rules that are processed, rule_process is called on the rules in this list. + var/list/current_rules = list() + /// List of executed rulesets. + var/list/executed_rules = list() + /// Associative list of current players, in order: living players, living antagonists, dead players and observers. + var/list/list/current_players = list(CURRENT_LIVING_PLAYERS, CURRENT_LIVING_ANTAGS, CURRENT_DEAD_PLAYERS, CURRENT_OBSERVERS) + /// When world.time is over this number the mode tries to inject a latejoin ruleset. + var/latejoin_injection_cooldown = 0 + /// When world.time is over this number the mode tries to inject a midround ruleset. + var/midround_injection_cooldown = 0 + ///When world.time is over this number the mode tries to inject an event + var/event_injection_cooldown = 0 + ///When world.time is over this, increase the threat level + var/threat_injection_cooldown = 0 + /// When TRUE GetInjectionChance returns 100. + var/forced_injection = FALSE + /// Forced ruleset to be executed for the next latejoin. + var/datum/dynamic_ruleset/latejoin/forced_latejoin_rule = null + /// When current_players was updated last time. + var/pop_last_updated = 0 + /// How many percent of the rounds are more peaceful. + var/peaceful_percentage = 50 + /// If a highlander executed. + var/highlander_executed = FALSE + /// If a only ruleset has been executed. + var/only_ruleset_executed = FALSE + +/datum/game_mode/dynamic/admin_panel() + var/list/dat = list("Game Mode Panel

Game Mode Panel

") + dat += "Dynamic Mode \[VV\]
" + dat += "Threat Level: [threat_level]
" + + dat += "Threat to Spend: [threat] \[Adjust\] \[View Log\]
" + dat += "
" + dat += "Parameters: centre = [GLOB.dynamic_curve_centre] ; width = [GLOB.dynamic_curve_width].
" + dat += "On average, [peaceful_percentage]% of the rounds are more peaceful.
" + dat += "Forced extended: [GLOB.dynamic_forced_extended ? "On" : "Off"]
" + dat += "Classic secret (only autotraitor): [GLOB.dynamic_classic_secret ? "On" : "Off"]
" + dat += "No stacking (only one round-ender): [GLOB.dynamic_no_stacking ? "On" : "Off"]
" + dat += "Stacking limit: [GLOB.dynamic_stacking_limit] \[Adjust\]" + dat += "
" + dat += "Executed rulesets: " + if (executed_rules.len > 0) + dat += "
" + for (var/datum/dynamic_ruleset/DR in executed_rules) + dat += "[DR.ruletype] - [DR.name]
" + else + dat += "none.
" + dat += "
Injection Timers: ([get_injection_chance(TRUE)]% chance)
" + dat += "Latejoin: [(latejoin_injection_cooldown-world.time)>60*10 ? "[round((latejoin_injection_cooldown-world.time)/60/10,0.1)] minutes" : "[(latejoin_injection_cooldown-world.time)] seconds"] \[Now!\]
" + dat += "Midround: [(midround_injection_cooldown-world.time)>60*10 ? "[round((midround_injection_cooldown-world.time)/60/10,0.1)] minutes" : "[(midround_injection_cooldown-world.time)] seconds"] \[Now!\]
" + dat += "Event: [(event_injection_cooldown-world.time)>60*10 ? "[round((event_injection_cooldown-world.time)/60/10,0.1)] minutes" : "[(event_injection_cooldown-world.time)] seconds"] \[Now!\]
" + dat += "Threat: [(threat_injection_cooldown-world.time)>60*10 ? "[round((threat_injection_cooldown-world.time)/60/10,0.1)] minutes" : "[(threat_injection_cooldown-world.time)] seconds"] \[Now!\]
" + + + usr << browse(dat.Join(), "window=gamemode_panel;size=500x500") + +/datum/game_mode/dynamic/Topic(href, href_list) + if (..()) // Sanity, maybe ? + return + if(!check_rights(R_ADMIN)) + message_admins("[usr.key] has attempted to override the game mode panel!") + log_admin("[key_name(usr)] tried to use the game mode panel without authorization.") + return + if (href_list["forced_extended"]) + GLOB.dynamic_forced_extended = !GLOB.dynamic_forced_extended + else if (href_list["no_stacking"]) + GLOB.dynamic_no_stacking = !GLOB.dynamic_no_stacking + else if (href_list["classic_secret"]) + GLOB.dynamic_classic_secret = !GLOB.dynamic_classic_secret + else if (href_list["adjustthreat"]) + var/threatadd = input("Specify how much threat to add (negative to subtract). This can inflate the threat level.", "Adjust Threat", 0) as null|num + if(!threatadd) + return + if(threatadd > 0) + create_threat(threatadd) + else + spend_threat(-threatadd) + else if (href_list["injectlate"]) + latejoin_injection_cooldown = 0 + forced_injection = TRUE + message_admins("[key_name(usr)] forced a latejoin injection.", 1) + else if (href_list["injectmid"]) + midround_injection_cooldown = 0 + forced_injection = TRUE + message_admins("[key_name(usr)] forced a midround injection.", 1) + else if (href_list["injectevent"]) + event_injection_cooldown = 0 + message_admins("[key_name(usr)] forced an event injection.", 1) + else if (href_list["injectthreat"]) + threat_injection_cooldown = 0 + message_admins("[key_name(usr)] forced a threat injection.", 1) + else if (href_list["threatlog"]) + show_threatlog(usr) + else if (href_list["stacking_limit"]) + GLOB.dynamic_stacking_limit = input(usr,"Change the threat limit at which round-endings rulesets will start to stack.", "Change stacking limit", null) as num + + admin_panel() // Refreshes the window + +// Checks if there are HIGHLANDER_RULESETs and calls the rule's round_result() proc +/datum/game_mode/dynamic/set_round_result() + for(var/datum/dynamic_ruleset/rule in executed_rules) + if(rule.flags & HIGHLANDER_RULESET) + if(rule.check_finished()) // Only the rule that actually finished the round sets round result. + return rule.round_result() + // If it got to this part, just pick one highlander if it exists + for(var/datum/dynamic_ruleset/rule in executed_rules) + if(rule.flags & HIGHLANDER_RULESET) + return rule.round_result() + return ..() + +/datum/game_mode/dynamic/send_intercept() + . = "Central Command Status Summary
" + var/starting_threat_level = (GLOB.dynamic_chaos_level * 20) + switch(round(starting_threat_level)) + if(0 to 19) + update_playercounts() + if(!current_players[CURRENT_LIVING_ANTAGS].len) + . += "Peaceful Waypoint
" + . += "Your station orbits deep within controlled, core-sector systems and serves as a waypoint for routine traffic through Nanotrasen's trade empire. Due to the combination of high security, interstellar traffic, and low strategic value, it makes any direct threat of violence unlikely. Your primary enemies will be incompetence and bored crewmen: try to organize team-building events to keep staffers interested and productive." + else + . += "Core Territory
" + . += "Your station orbits within reliably mundane, secure space. Although Nanotrasen has a firm grip on security in your region, the valuable resources and strategic position aboard your station make it a potential target for infiltrations. Monitor crew for non-loyal behavior, but expect a relatively tame shift free of large-scale destruction. We expect great things from your station." + if(20 to 39) + . += "Anomalous Exogeology
" + . += "Although your station lies within what is generally considered Nanotrasen-controlled space, the course of its orbit has caused it to cross unusually close to exogeological features with anomalous readings. Although these features offer opportunities for our research department, it is known that these little understood readings are often correlated with increased activity from competing interstellar organizations and individuals, among them the Wizard Federation and Cult of the Geometer of Blood - all known competitors for Anomaly Type B sites. Exercise elevated caution." + if(40 to 49) + . += "Contested System
" + . += "Your station's orbit passes along the edge of Nanotrasen's sphere of influence. While subversive elements remain the most likely threat against your station, hostile organizations are bolder here, where our grip is weaker. Exercise increased caution against elite Syndicate strike forces, or Executives forbid, some kind of ill-conceived unionizing attempt." + if(GLOB.security_level < SEC_LEVEL_BLUE) + set_security_level(SEC_LEVEL_BLUE) + if(50 to 59) + . += "Uncharted Space
" + . += "Congratulations and thank you for participating in the NT 'Frontier' space program! Your station is actively orbiting a high value system far from the nearest support stations. Little is known about your region of space, and the opportunity to encounter the unknown invites greater glory. You are encouraged to elevate security as necessary to protect Nanotrasen assets." + if(GLOB.security_level < SEC_LEVEL_BLUE) + set_security_level(SEC_LEVEL_BLUE) + if(60 to 79) + . += "Black Orbit
" + . += "As part of a mandatory security protocol, we are required to inform you that as a result of your orbital pattern directly behind an astrological body (oriented from our nearest observatory), your station will be under decreased monitoring and support. It is anticipated that your extreme location and decreased surveillance could pose security risks. Avoid unnecessary risks and attempt to keep your station in one piece." + if(GLOB.security_level < SEC_LEVEL_BLUE) + set_security_level(SEC_LEVEL_BLUE) + if(80 to 100) + . += "Impending Doom
" + . += "Your station is somehow in the middle of hostile territory, in clear view of any enemy of the corporation. Your likelihood to survive is low, and station destruction is expected and almost inevitable. Secure any sensitive material and neutralize any enemy you will come across. It is important that you at least try to maintain the station.
" + . += "Good luck." + if(GLOB.security_level < SEC_LEVEL_AMBER) + set_security_level(SEC_LEVEL_AMBER) + if(station_goals.len) + . += "
Special Orders for [station_name()]:" + for(var/datum/station_goal/G in station_goals) + G.on_report() + . += G.get_report() + + print_command_report(., "Central Command Status Summary", announce=FALSE) + if(GLOB.security_level >= SEC_LEVEL_BLUE) + priority_announce("A summary has been copied and printed to all communications consoles.", "Security level elevated.", 'sound/ai/intercept.ogg') + else + priority_announce("A summary has been copied and printed to all communications consoles.") //STOP YELLING AT US COMPUTER LADY + +// Yes, this is copy pasted from game_mode +/datum/game_mode/dynamic/check_finished(force_ending) + if(!SSticker.setup_done || !gamemode_ready) + return FALSE + if(replacementmode && round_converted == 2) + return replacementmode.check_finished() + if(SSshuttle.emergency && (SSshuttle.emergency.mode == SHUTTLE_ENDGAME)) + return TRUE + if(station_was_nuked) + return TRUE + if(force_ending) + return TRUE + for(var/datum/dynamic_ruleset/rule in executed_rules) + if(rule.flags & HIGHLANDER_RULESET) + return rule.check_finished() + +/datum/game_mode/dynamic/proc/show_threatlog(mob/admin) + if(!SSticker.HasRoundStarted()) + alert("The round hasn't started yet!") + return + + if(!check_rights(R_ADMIN)) + return + + var/list/out = list("Threat LogThreat Log
Starting Threat: [threat_level]
") + + for(var/entry in threat_log) + if(istext(entry)) + out += "[entry]
" + + out += "Remaining threat/threat_level: [threat]/[threat_level]" + + usr << browse(out.Join(), "window=threatlog;size=700x500") + +/// Generates the threat level using lorentz distribution and assigns peaceful_percentage. +/datum/game_mode/dynamic/proc/generate_threat() //Generates initial threat. +/* + var/relative_threat = LORENTZ_DISTRIBUTION(GLOB.dynamic_curve_centre, GLOB.dynamic_curve_width) + threat_level = round(lorentz_to_threat(relative_threat), 0.1) + + peaceful_percentage = round(LORENTZ_CUMULATIVE_DISTRIBUTION(relative_threat, GLOB.dynamic_curve_centre, GLOB.dynamic_curve_width), 0.01)*100 + + threat = threat_level +*/ //Hyper Change + + threat_level = GLOB.dynamic_chaos_level * 20 //Initial threat will be based off chaos vote. + threat = threat_level + +/datum/game_mode/dynamic/can_start() + /* Disabled for now, had some changes that need to be tested and this might interfere with that. + if(GLOB.dynamic_curve_centre == 0) + // 10 is when the centre starts to decrease + // 6 is just 1 + 5 (from the maximum value and the one decreased) + // 1 just makes the curve look better, I don't know. + // Limited between 1 and 5 then inverted and rounded + // With this you get a centre curve that stays at -5 until 10 then first rapidly decreases but slows down at the end + GLOB.dynamic_curve_centre = round(-CLAMP((10*6/GLOB.player_list.len)-1, 0, 5), 0.5) + */ + message_admins("Dynamic mode parameters for the round:") + message_admins("Centre is [GLOB.dynamic_curve_centre], Width is [GLOB.dynamic_curve_width], Forced extended is [GLOB.dynamic_forced_extended ? "Enabled" : "Disabled"], No stacking is [GLOB.dynamic_no_stacking ? "Enabled" : "Disabled"].") + message_admins("Stacking limit is [GLOB.dynamic_stacking_limit], Classic secret is [GLOB.dynamic_classic_secret ? "Enabled" : "Disabled"], High population limit is [GLOB.dynamic_high_pop_limit].") + log_game("DYNAMIC: Dynamic mode parameters for the round:") + log_game("DYNAMIC: Centre is [GLOB.dynamic_curve_centre], Width is [GLOB.dynamic_curve_width], Forced extended is [GLOB.dynamic_forced_extended ? "Enabled" : "Disabled"], No stacking is [GLOB.dynamic_no_stacking ? "Enabled" : "Disabled"].") + log_game("DYNAMIC: Stacking limit is [GLOB.dynamic_stacking_limit], Classic secret is [GLOB.dynamic_classic_secret ? "Enabled" : "Disabled"], High population limit is [GLOB.dynamic_high_pop_limit].") + if(GLOB.dynamic_forced_threat_level >= 0) + threat_level = round(GLOB.dynamic_forced_threat_level, 0.1) + threat = threat_level + else + generate_threat() + + var/latejoin_injection_cooldown_middle = 0.5*(GLOB.dynamic_latejoin_delay_max + GLOB.dynamic_latejoin_delay_min) + latejoin_injection_cooldown = round(CLAMP(EXP_DISTRIBUTION(latejoin_injection_cooldown_middle), GLOB.dynamic_latejoin_delay_min, GLOB.dynamic_latejoin_delay_max)) + world.time + + var/midround_injection_cooldown_middle = 0.5*(GLOB.dynamic_midround_delay_max + GLOB.dynamic_midround_delay_min) + midround_injection_cooldown = round(CLAMP(EXP_DISTRIBUTION(midround_injection_cooldown_middle), GLOB.dynamic_midround_delay_min, GLOB.dynamic_midround_delay_max)) + world.time + + event_injection_cooldown = (rand(GLOB.dynamic_event_delay_min, GLOB.dynamic_event_delay_max) + world.time + (10 MINUTES)) //Add a delay to the first event so people get settled in + + threat_injection_cooldown = (GLOB.dynamic_threat_delay + world.time) + + message_admins("Dynamic Mode initialized with a Threat Level of... [threat_level]!") + log_game("DYNAMIC: Dynamic Mode initialized with a Threat Level of... [threat_level]!") + return TRUE + +/datum/game_mode/dynamic/pre_setup() + for (var/rule in subtypesof(/datum/dynamic_ruleset)) + var/datum/dynamic_ruleset/ruleset = new rule() + // Simple check if the ruleset should be added to the lists. + if(ruleset.name == "") + continue + switch(ruleset.ruletype) + if("Roundstart") + roundstart_rules += ruleset + if ("Latejoin") + latejoin_rules += ruleset + if ("Midround") + if (ruleset.weight) + midround_rules += ruleset + if ("Event") + if(ruleset.weight) + event_rules += ruleset + for(var/mob/dead/new_player/player in GLOB.player_list) + if(player.ready == PLAYER_READY_TO_PLAY && player.mind) + roundstart_pop_ready++ + candidates.Add(player) + log_game("DYNAMIC: Listing [roundstart_rules.len] round start rulesets, and [candidates.len] players ready.") + if (candidates.len <= 0) + return TRUE + if (roundstart_rules.len <= 0) + return TRUE + + if(GLOB.dynamic_forced_roundstart_ruleset.len > 0) + rigged_roundstart() + else + roundstart() + + var/starting_rulesets = "" + for (var/datum/dynamic_ruleset/roundstart/DR in executed_rules) + starting_rulesets += "[DR.name], " + candidates.Cut() + return TRUE + +/datum/game_mode/dynamic/post_setup(report) + update_playercounts() + + for(var/datum/dynamic_ruleset/roundstart/rule in executed_rules) + rule.candidates.Cut() // The rule should not use candidates at this point as they all are null. + if(!rule.execute()) + stack_trace("The starting rule \"[rule.name]\" failed to execute.") + ..() + +/// A simple roundstart proc used when dynamic_forced_roundstart_ruleset has rules in it. +/datum/game_mode/dynamic/proc/rigged_roundstart() + message_admins("[GLOB.dynamic_forced_roundstart_ruleset.len] rulesets being forced. Will now attempt to draft players for them.") + log_game("DYNAMIC: [GLOB.dynamic_forced_roundstart_ruleset.len] rulesets being forced. Will now attempt to draft players for them.") + for (var/datum/dynamic_ruleset/roundstart/rule in GLOB.dynamic_forced_roundstart_ruleset) + message_admins("Drafting players for forced ruleset [rule.name].") + log_game("DYNAMIC: Drafting players for forced ruleset [rule.name].") + rule.mode = src + rule.candidates = candidates.Copy() + rule.trim_candidates() + if (rule.ready(TRUE)) + picking_roundstart_rule(list(rule), forced = TRUE) + +/datum/game_mode/dynamic/proc/roundstart() + if (GLOB.dynamic_forced_extended) + log_game("DYNAMIC: Starting a round of forced extended.") + return TRUE + var/list/drafted_rules = list() + for (var/datum/dynamic_ruleset/roundstart/rule in roundstart_rules) + if (rule.acceptable(roundstart_pop_ready, threat_level) && threat >= rule.cost) // If we got the population and threat required + rule.candidates = candidates.Copy() + rule.trim_candidates() + if (rule.ready() && rule.candidates.len > 0) + drafted_rules[rule] = rule.weight + + var/indice_pop = min(10,round(roundstart_pop_ready/pop_per_requirement)+1) + var/extra_rulesets_amount = 0 + if (GLOB.dynamic_classic_secret) + extra_rulesets_amount = 0 + else + if (roundstart_pop_ready > GLOB.dynamic_high_pop_limit) + message_admins("High Population Override is in effect! Threat Level will have more impact on which roles will appear, and player population less.") + log_game("DYNAMIC: High Population Override is in effect! Threat Level will have more impact on which roles will appear, and player population less.") + if (threat_level > high_pop_second_rule_req) + extra_rulesets_amount++ + if (threat_level > high_pop_third_rule_req) + extra_rulesets_amount++ + else + if (threat_level >= second_rule_req[indice_pop]) + extra_rulesets_amount++ + if (threat_level >= third_rule_req[indice_pop]) + extra_rulesets_amount++ + + if (drafted_rules.len > 0 && picking_roundstart_rule(drafted_rules)) + if (extra_rulesets_amount > 0) // We've got enough population and threat for a second rulestart rule + for (var/datum/dynamic_ruleset/roundstart/rule in drafted_rules) + if (rule.cost > threat) + drafted_rules -= rule + if (drafted_rules.len > 0 && picking_roundstart_rule(drafted_rules)) + if (extra_rulesets_amount > 1) // We've got enough population and threat for a third rulestart rule + for (var/datum/dynamic_ruleset/roundstart/rule in drafted_rules) + if (rule.cost > threat) + drafted_rules -= rule + picking_roundstart_rule(drafted_rules) + else + return FALSE + return TRUE + +/// Picks a random roundstart rule from the list given as an argument and executes it. +/datum/game_mode/dynamic/proc/picking_roundstart_rule(list/drafted_rules = list(), forced = FALSE) + var/datum/dynamic_ruleset/roundstart/starting_rule = pickweight(drafted_rules) + if(!starting_rule) + return FALSE + + if(!forced) + if(only_ruleset_executed) + return FALSE + // Check if a blocking ruleset has been executed. + else if(check_blocking(starting_rule.blocking_rules, executed_rules)) + drafted_rules -= starting_rule + if(drafted_rules.len <= 0) + return FALSE + starting_rule = pickweight(drafted_rules) + // Check if the ruleset is highlander and if a highlander ruleset has been executed + else if(starting_rule.flags & HIGHLANDER_RULESET) + if(threat < GLOB.dynamic_stacking_limit && GLOB.dynamic_no_stacking) + if(highlander_executed) + drafted_rules -= starting_rule + if(drafted_rules.len <= 0) + return FALSE + starting_rule = pickweight(drafted_rules) + + message_admins("Picking a [istype(starting_rule, /datum/dynamic_ruleset/roundstart/delayed/) ? " delayed " : ""] ruleset [starting_rule.name]") + log_game("DYNAMIC: Picking a [istype(starting_rule, /datum/dynamic_ruleset/roundstart/delayed/) ? " delayed " : ""] ruleset [starting_rule.name]") + + roundstart_rules -= starting_rule + drafted_rules -= starting_rule + + if (istype(starting_rule, /datum/dynamic_ruleset/roundstart/delayed/)) + var/datum/dynamic_ruleset/roundstart/delayed/rule = starting_rule + addtimer(CALLBACK(src, .proc/execute_delayed, rule), rule.delay) + + starting_rule.trim_candidates() + if (starting_rule.pre_execute()) + var/rule_threat = starting_rule.get_threat(roundstart_pop_ready) + spend_threat(rule_threat) + spend_threat(starting_rule.cost) + threat_log += "[worldtime2text()]: Roundstart [starting_rule.name] spent [starting_rule.cost + rule_threat]" + if(starting_rule.flags & HIGHLANDER_RULESET) + highlander_executed = TRUE + else if(starting_rule.flags & ONLY_RULESET) + only_ruleset_executed = TRUE + executed_rules += starting_rule + if (starting_rule.persistent) + current_rules += starting_rule + for(var/mob/M in starting_rule.assigned) + for (var/datum/dynamic_ruleset/roundstart/rule in roundstart_rules) + if (!rule.ready()) + drafted_rules -= rule // And removing rules that are no longer elligible + return TRUE + else + stack_trace("The starting rule \"[starting_rule.name]\" failed to pre_execute.") + return FALSE + +/// Executes delayed roundstart rules and has a hack in it. +/datum/game_mode/dynamic/proc/execute_delayed(datum/dynamic_ruleset/roundstart/delayed/rule) + update_playercounts() + rule.candidates = current_players[CURRENT_LIVING_PLAYERS].Copy() + rule.trim_candidates() + if(rule.execute()) + executed_rules += rule + if (rule.persistent) + current_rules += rule + return TRUE + else + stack_trace("The delayed roundstart rule \"[rule.name]\" failed to execute.") + return FALSE + +/// Picks a random midround OR latejoin rule from the list given as an argument and executes it. +/// Also this could be named better. +/datum/game_mode/dynamic/proc/picking_midround_latejoin_rule(list/drafted_rules = list(), forced = FALSE) + var/datum/dynamic_ruleset/rule = pickweight(drafted_rules) + if(!rule) + return FALSE + + if(!forced) + if(only_ruleset_executed) + return FALSE + // Check if a blocking ruleset has been executed. + else if(check_blocking(rule.blocking_rules, executed_rules)) + drafted_rules -= rule + if(drafted_rules.len <= 0) + return FALSE + rule = pickweight(drafted_rules) + // Check if the ruleset is highlander and if a highlander ruleset has been executed + else if(rule.flags & HIGHLANDER_RULESET) + if(threat < GLOB.dynamic_stacking_limit && GLOB.dynamic_no_stacking) + if(highlander_executed) + drafted_rules -= rule + if(drafted_rules.len <= 0) + return FALSE + rule = pickweight(drafted_rules) + + if(!rule.repeatable) + if(rule.ruletype == "Latejoin") + latejoin_rules = remove_from_list(latejoin_rules, rule.type) + else if(rule.type == "Midround") + midround_rules = remove_from_list(midround_rules, rule.type) + else if(rule.type == "Event") + event_rules = remove_from_list(event_rules, rule.type) + + if (rule.execute()) + log_game("DYNAMIC: Injected a [rule.ruletype == "latejoin" ? "latejoin" : "midround"] ruleset [rule.name].") + spend_threat(rule.cost) + var/rule_threat = rule.get_threat(current_players[CURRENT_LIVING_PLAYERS].len) + spend_threat(rule_threat) + threat_log += "[worldtime2text()]: [rule.ruletype] [rule.name] spent [rule.cost + rule_threat]" + if(rule.flags & HIGHLANDER_RULESET) + highlander_executed = TRUE + else if(rule.flags & ONLY_RULESET) + only_ruleset_executed = TRUE + if(rule.ruletype == "Latejoin") + var/mob/M = pick(rule.candidates) + message_admins("[key_name(M)] joined the station, and was selected by the [rule.name] ruleset.") + log_game("DYNAMIC: [key_name(M)] joined the station, and was selected by the [rule.name] ruleset.") + executed_rules += rule + rule.candidates.Cut() + if (rule.persistent) + current_rules += rule + return TRUE + else + stack_trace("The [rule.ruletype] rule \"[rule.name]\" failed to execute.") + return FALSE + +/// An experimental proc to allow admins to call rules on the fly or have rules call other rules. +/datum/game_mode/dynamic/proc/picking_specific_rule(ruletype, forced = FALSE) + var/datum/dynamic_ruleset/midround/new_rule + if(ispath(ruletype)) + new_rule = new ruletype() // You should only use it to call midround rules though. + else if(istype(ruletype, /datum/dynamic_ruleset)) + new_rule = ruletype + else + return FALSE + + if(!new_rule) + return FALSE + + if(!forced) + if(only_ruleset_executed) + return FALSE + // Check if a blocking ruleset has been executed. + else if(check_blocking(new_rule.blocking_rules, executed_rules)) + return FALSE + // Check if the ruleset is highlander and if a highlander ruleset has been executed + else if(new_rule.flags & HIGHLANDER_RULESET) + if(threat < GLOB.dynamic_stacking_limit && GLOB.dynamic_no_stacking) + if(highlander_executed) + return FALSE + + update_playercounts() + if ((forced || (new_rule.acceptable(current_players[CURRENT_LIVING_PLAYERS].len, threat_level) && new_rule.cost <= threat))) + new_rule.candidates = current_players.Copy() + new_rule.trim_candidates() + if (new_rule.ready(forced)) + spend_threat(new_rule.cost) + threat_log += "[worldtime2text()]: Forced rule [new_rule.name] spent [new_rule.cost]" + if (new_rule.execute()) // This should never fail since ready() returned 1 + if(new_rule.flags & HIGHLANDER_RULESET) + highlander_executed = TRUE + else if(new_rule.flags & ONLY_RULESET) + only_ruleset_executed = TRUE + log_game("DYNAMIC: Making a call to a specific ruleset...[new_rule.name]!") + executed_rules += new_rule + if (new_rule.persistent) + current_rules += new_rule + return TRUE + else if (forced) + log_game("DYNAMIC: The ruleset [new_rule.name] couldn't be executed due to lack of elligible players.") + return FALSE + +/datum/game_mode/dynamic/process() + if (pop_last_updated < world.time - (60 SECONDS)) + pop_last_updated = world.time + update_playercounts() + + for (var/datum/dynamic_ruleset/rule in current_rules) + if(rule.rule_process() == RULESET_STOP_PROCESSING) // If rule_process() returns 1 (RULESET_STOP_PROCESSING), stop processing. + current_rules -= rule + + if (midround_injection_cooldown < world.time) + if (GLOB.dynamic_forced_extended) + return + + // Somehow it manages to trigger midround multiple times so this was moved here. + // There is no way this should be able to trigger an injection twice now. + var/midround_injection_cooldown_middle = 0.5*(GLOB.dynamic_midround_delay_max + GLOB.dynamic_midround_delay_min) + midround_injection_cooldown = (round(CLAMP(EXP_DISTRIBUTION(midround_injection_cooldown_middle), GLOB.dynamic_midround_delay_min, GLOB.dynamic_midround_delay_max)) + world.time) + + // Time to inject some threat into the round + if(EMERGENCY_ESCAPED_OR_ENDGAMED) // Unless the shuttle is gone + return + + log_game("DYNAMIC: Checking state of the round.") + + update_playercounts() + + if (prob(get_injection_chance())) + var/list/drafted_rules = list() + for (var/datum/dynamic_ruleset/midround/rule in midround_rules) + if (rule.acceptable(current_players[CURRENT_LIVING_PLAYERS].len, threat_level) && threat >= rule.cost) + // Classic secret : only autotraitor/minor roles + if (GLOB.dynamic_classic_secret && !((rule.flags & TRAITOR_RULESET) || (rule.flags & MINOR_RULESET))) + continue + rule.candidates = list() + rule.candidates = current_players.Copy() + rule.trim_candidates() + if (rule.ready() && rule.candidates.len > 0) + drafted_rules[rule] = rule.get_weight() + if (drafted_rules.len > 0) + picking_midround_latejoin_rule(drafted_rules) + + if(event_injection_cooldown < world.time) + //SSblackbox.record_feedback("tally","dynamic",1,"Attempted event injections") + //event_injection_cooldown = storyteller.get_event_cooldown() + world.time + event_injection_cooldown = (rand(GLOB.dynamic_event_delay_min, GLOB.dynamic_event_delay_max) + world.time) + message_admins("DYNAMIC: Doing event injection.") + log_game("DYNAMIC: Doing event injection.") + update_playercounts() + var/list/drafted_rules = list() + for (var/datum/dynamic_ruleset/event/rule in event_rules) + if (rule.acceptable(current_players[CURRENT_LIVING_PLAYERS].len, threat_level) && threat >= rule.cost) + // Classic secret : only autotraitor/minor roles + if (!GLOB.master_mode == "dynamic") + continue + if (world.time < rule.earliest_start) + continue + if(rule.occurances_current >= rule.occurances_max && rule.occurances_max) + continue + rule.candidates = list() + rule.candidates = current_players.Copy() + rule.trim_candidates() + if (rule.ready() && rule.candidates.len > 0) + drafted_rules[rule] = rule.get_weight() + if(drafted_rules.len > 0) + picking_midround_latejoin_rule(drafted_rules) + + if(threat_injection_cooldown < world.time) + threat_injection_cooldown = (GLOB.dynamic_threat_delay + world.time) + var/threat_to_add = GLOB.dynamic_chaos_level//Simple + create_threat(threat_to_add) + +/// Updates current_players. +/datum/game_mode/dynamic/proc/update_playercounts() + current_players[CURRENT_LIVING_PLAYERS] = list() + current_players[CURRENT_LIVING_ANTAGS] = list() + current_players[CURRENT_DEAD_PLAYERS] = list() + current_players[CURRENT_OBSERVERS] = list() + for (var/mob/M in GLOB.player_list) + if (istype(M, /mob/dead/new_player)) + continue + if (M.stat != DEAD) + current_players[CURRENT_LIVING_PLAYERS].Add(M) + if (M.mind && (M.mind.special_role || M.mind.antag_datums?.len > 0)) + current_players[CURRENT_LIVING_ANTAGS].Add(M) + else + if (istype(M,/mob/dead/observer)) + var/mob/dead/observer/O = M + if (O.started_as_observer) // Observers + current_players[CURRENT_OBSERVERS].Add(M) + continue + current_players[CURRENT_DEAD_PLAYERS].Add(M) // Players who actually died (and admins who ghosted, would be nice to avoid counting them somehow) + +/// Gets the chance for latejoin and midround injection, the dry_run argument is only used for forced injection. +/datum/game_mode/dynamic/proc/get_injection_chance(dry_run = FALSE) + if(forced_injection) + forced_injection = !dry_run// + return 100 + var/chance = 0 + // If the high pop override is in effect, we reduce the impact of population on the antag injection chance + /* + var/high_pop_factor = (current_players[CURRENT_LIVING_PLAYERS].len >= GLOB.dynamic_high_pop_limit) + var/max_pop_per_antag = max(5,15 - round(threat_level/10) - round(current_players[CURRENT_LIVING_PLAYERS].len/(high_pop_factor ? 10 : 5))) + if (!current_players[CURRENT_LIVING_ANTAGS].len) + chance += 50 // No antags at all? let's boost those odds! + else + var/current_pop_per_antag = current_players[CURRENT_LIVING_PLAYERS].len / current_players[CURRENT_LIVING_ANTAGS].len + if (current_pop_per_antag > max_pop_per_antag) + chance += min(50, 25+10*(current_pop_per_antag-max_pop_per_antag)) + else + chance += 25-10*(max_pop_per_antag-current_pop_per_antag) + */ + //Hyper change - Base injection chance based on chaos. + chance = (GLOB.dynamic_chaos_level * 20) //Base chance from 0 to 100 + if (current_players[CURRENT_DEAD_PLAYERS].len > current_players[CURRENT_LIVING_PLAYERS].len) + chance -= 30 // More than half the crew died? ew, let's calm down on antags + if (threat > 70) + chance += 15 + if (threat < 30) + chance -= 15 + if (chance > 100) //I don't know what would happen if we returned a probability greater than 100% + return 100 //So I won't + return round(max(0,chance)) + +/// Removes type from the list +/datum/game_mode/dynamic/proc/remove_from_list(list/type_list, type) + for(var/I in type_list) + if(istype(I, type)) + type_list -= I + return type_list + +/// Checks if a type in blocking_list is in rule_list. +/datum/game_mode/dynamic/proc/check_blocking(list/blocking_list, list/rule_list) + if(blocking_list.len > 0) + for(var/blocking in blocking_list) + for(var/datum/executed in rule_list) + if(blocking == executed.type) + return TRUE + return FALSE + +/// Checks if client age is age or older. +/datum/game_mode/dynamic/proc/check_age(client/C, age) + enemy_minimum_age = age + if(get_remaining_days(C) == 0) + enemy_minimum_age = initial(enemy_minimum_age) + return TRUE // Available in 0 days = available right now = player is old enough to play. + enemy_minimum_age = initial(enemy_minimum_age) + return FALSE + +/datum/game_mode/dynamic/make_antag_chance(mob/living/carbon/human/newPlayer) + if (GLOB.dynamic_forced_extended) + return + if(EMERGENCY_ESCAPED_OR_ENDGAMED) // No more rules after the shuttle has left + return + + update_playercounts() + + if (forced_latejoin_rule) + forced_latejoin_rule.candidates = list(newPlayer) + forced_latejoin_rule.trim_candidates() + log_game("DYNAMIC: Forcing ruleset [forced_latejoin_rule]") + if (forced_latejoin_rule.ready(TRUE)) + picking_midround_latejoin_rule(list(forced_latejoin_rule), forced = TRUE) + forced_latejoin_rule = null + + else if (latejoin_injection_cooldown < world.time && prob(get_injection_chance())) + var/list/drafted_rules = list() + for (var/datum/dynamic_ruleset/latejoin/rule in latejoin_rules) + if (rule.acceptable(current_players[CURRENT_LIVING_PLAYERS].len, threat_level) && threat >= rule.cost) + // Classic secret : only autotraitor/minor roles + if (GLOB.dynamic_classic_secret && !((rule.flags & TRAITOR_RULESET) || (rule.flags & MINOR_RULESET))) + continue + // No stacking : only one round-enter, unless > stacking_limit threat. + if (threat < GLOB.dynamic_stacking_limit && GLOB.dynamic_no_stacking) + if(rule.flags & HIGHLANDER_RULESET && highlander_executed) + continue + + rule.candidates = list(newPlayer) + rule.trim_candidates() + if (rule.ready()) + drafted_rules[rule] = rule.get_weight() + + if (drafted_rules.len > 0 && picking_midround_latejoin_rule(drafted_rules)) + var/latejoin_injection_cooldown_middle = 0.5*(GLOB.dynamic_latejoin_delay_max + GLOB.dynamic_latejoin_delay_min) + latejoin_injection_cooldown = round(CLAMP(EXP_DISTRIBUTION(latejoin_injection_cooldown_middle), GLOB.dynamic_latejoin_delay_min, GLOB.dynamic_latejoin_delay_max)) + world.time + +/// Refund threat, but no more than threat_level. +/datum/game_mode/dynamic/proc/refund_threat(regain) + threat = min(threat_level,threat+regain) + +/// Generate threat and increase the threat_level if it goes beyond, capped at 100 +/datum/game_mode/dynamic/proc/create_threat(gain) + threat = min(100, threat+gain) + if(threat > threat_level) + threat_level = threat + +/// Expend threat, can't fall under 0. +/datum/game_mode/dynamic/proc/spend_threat(cost) + threat = max(threat-cost,0) + threat_level = threat + if(threat > 100) + threat = 100 + +/// Turns the value generated by lorentz distribution to threat value between 0 and 100. +/datum/game_mode/dynamic/proc/lorentz_to_threat(x) + switch (x) + if (-INFINITY to -20) + return rand(0, 10) + if (-20 to -10) + return RULE_OF_THREE(-40, -20, x) + 50 + if (-10 to -5) + return RULE_OF_THREE(-30, -10, x) + 50 + if (-5 to -2.5) + return RULE_OF_THREE(-20, -5, x) + 50 + if (-2.5 to -0) + return RULE_OF_THREE(-10, -2.5, x) + 50 + if (0 to 2.5) + return RULE_OF_THREE(10, 2.5, x) + 50 + if (2.5 to 5) + return RULE_OF_THREE(20, 5, x) + 50 + if (5 to 10) + return RULE_OF_THREE(30, 10, x) + 50 + if (10 to 20) + return RULE_OF_THREE(40, 20, x) + 50 + if (20 to INFINITY) + return rand(90, 100) \ No newline at end of file diff --git a/code/game/gamemodes/dynamic/dynamic_rulesets.dm b/code/game/gamemodes/dynamic/dynamic_rulesets.dm new file mode 100644 index 00000000..fa944b17 --- /dev/null +++ b/code/game/gamemodes/dynamic/dynamic_rulesets.dm @@ -0,0 +1,227 @@ +/datum/dynamic_ruleset + /// For admin logging and round end screen. + var/name = "" + /// For admin logging and round end screen, do not change this unless making a new rule type. + var/ruletype = "" + /// If set to TRUE, the rule won't be discarded after being executed, and dynamic will call rule_process() every time it ticks. + var/persistent = FALSE + /// If set to TRUE, dynamic mode will be able to draft this ruleset again later on. (doesn't apply for roundstart rules) + var/repeatable = FALSE + /// If set higher than 0 decreases weight by itself causing the ruleset to appear less often the more it is repeated. + var/repeatable_weight_decrease = 2 + /// List of players that are being drafted for this rule + var/list/mob/candidates = list() + /// List of players that were selected for this rule + var/list/datum/mind/assigned = list() + /// Preferences flag such as ROLE_WIZARD that need to be turned on for players to be antag + var/antag_flag = null + /// The antagonist datum that is assigned to the mobs mind on ruleset execution. + var/datum/antagonist/antag_datum = null + /// The required minimum account age for this ruleset. + var/minimum_required_age = 7 + /// If set, and config flag protect_roles_from_antagonist is false, then the rule will not pick players from these roles. + var/list/protected_roles = list() + /// If set, rule will deny candidates from those roles always. + var/list/restricted_roles = list() + /// If set, rule will only accept candidates from those roles, IMPORTANT: DOES NOT WORK ON ROUNDSTART RULESETS. + var/list/exclusive_roles = list() + /// If set, there needs to be a certain amount of players doing those roles (among the players who won't be drafted) for the rule to be drafted IMPORTANT: DOES NOT WORK ON ROUNDSTART RULESETS. + var/list/enemy_roles = list() + /// If enemy_roles was set, this is the amount of enemy job workers needed per threat_level range (0-10,10-20,etc) IMPORTANT: DOES NOT WORK ON ROUNDSTART RULESETS. + var/required_enemies = list(1,1,0,0,0,0,0,0,0,0) + /// The rule needs this many candidates (post-trimming) to be executed (example: Cult needs 4 players at round start) + var/required_candidates = 0 + /// 1 -> 9, probability for this rule to be picked against other rules + var/weight = 5 + /// Threat cost for this rule, this is decreased from the mode's threat when the rule is executed. + var/cost = 0 + /// A flag that determines how the ruleset is handled + /// HIGHLANDER_RULESET are rulesets can end the round. + /// TRAITOR_RULESET and MINOR_RULESET can't end the round and have no difference right now. + var/flags = 0 + /// Pop range per requirement. If zero defaults to mode's pop_per_requirement. + var/pop_per_requirement = 0 + /// Requirements are the threat level requirements per pop range. + /// With the default values, The rule will never get drafted below 10 threat level (aka: "peaceful extended"), and it requires a higher threat level at lower pops. + var/list/requirements = list(40,30,20,10,10,10,10,10,10,10) + /// An alternative, static requirement used instead when pop is over mode's high_pop_limit. + var/high_population_requirement = 10 + /// Reference to the mode, use this instead of SSticker.mode. + var/datum/game_mode/dynamic/mode = null + /// If a role is to be considered another for the purpose of banning. + var/antag_flag_override = null + /// If a ruleset type which is in this list has been executed, then the ruleset will not be executed. + var/list/blocking_rules = list() + /// The minimum amount of players required for the rule to be considered. + var/minimum_players = 0 + /// The maximum amount of players required for the rule to be considered. + /// Anything below zero or exactly zero is ignored. + var/maximum_players = 0 + /// Hyper Change - Chaos Gating + /// Events require a max or min chaos value to be called. + var/chaos_min = 0 + var/chaos_max = 5 + /// Event requires an admin to be online + var/admin_required = FALSE + + +/datum/dynamic_ruleset/New() + ..() + if(CONFIG_GET(flag/protect_roles_from_antagonist)) + restricted_roles += protected_roles + if(CONFIG_GET(flag/protect_assistant_from_antagonist)) + restricted_roles += "Assistant" + + if (istype(SSticker.mode, /datum/game_mode/dynamic)) + mode = SSticker.mode + else if (GLOB.master_mode != "dynamic") // This is here to make roundstart forced ruleset function. + qdel(src) + +/datum/dynamic_ruleset/roundstart // One or more of those drafted at roundstart + ruletype = "Roundstart" + +/datum/dynamic_ruleset/roundstart/delayed/ // Executed with a 30 seconds delay + var/delay = 30 SECONDS + var/required_type = /mob/living/carbon/human // No ghosts, new players or silicons allowed. + +// Can be drafted when a player joins the server +/datum/dynamic_ruleset/latejoin + ruletype = "Latejoin" + +/// By default, a rule is acceptable if it satisfies the threat level/population requirements. +/// If your rule has extra checks, such as counting security officers, do that in ready() instead +/datum/dynamic_ruleset/proc/acceptable(population = 0, threat_level = 0) + if(minimum_players > population) + return FALSE + if(maximum_players > 0 && population > maximum_players) + return FALSE + if(GLOB.dynamic_chaos_level < chaos_min || GLOB.dynamic_chaos_level > chaos_max) + return FALSE + if(admin_required && !GLOB.admins.len) + return FALSE + if (population >= GLOB.dynamic_high_pop_limit) + return (threat_level >= high_population_requirement) + else + pop_per_requirement = pop_per_requirement > 0 ? pop_per_requirement : mode.pop_per_requirement + var/indice_pop = min(10,round(population/pop_per_requirement)+1) + return (threat_level >= requirements[indice_pop]) + +///Return the current threat requirement for the current population for the current rule +/datum/dynamic_ruleset/proc/get_threat(population = 0) + pop_per_requirement = pop_per_requirement > 0 ? pop_per_requirement : mode.pop_per_requirement + var/indice_pop = min(10,round(population/pop_per_requirement)+1) + return requirements[indice_pop] + +/// This is called if persistent variable is true everytime SSTicker ticks. +/datum/dynamic_ruleset/proc/rule_process() + return + +/// Called on game mode pre_setup, used for non-delayed roundstart rulesets only. +/// Do everything you need to do before job is assigned here. +/// IMPORTANT: ASSIGN special_role HERE +/datum/dynamic_ruleset/proc/pre_execute() + return TRUE + +/// Called on post_setup on roundstart and when the rule executes on midround and latejoin. +/// Give your candidates or assignees equipment and antag datum here. +/datum/dynamic_ruleset/proc/execute() + for(var/datum/mind/M in assigned) + M.add_antag_datum(antag_datum) + return TRUE + +/// Called after delay set in ruleset. +/// Give your candidates or assignees equipment and antag datum here. +/datum/dynamic_ruleset/roundstart/delayed/execute() + if (SSticker && SSticker.current_state < GAME_STATE_PLAYING) + CRASH("The delayed ruleset [name] executed before the round started.") + +/// Here you can perform any additional checks you want. (such as checking the map etc) +/// Remember that on roundstart no one knows what their job is at this point. +/// IMPORTANT: If ready() returns TRUE, that means pre_execute() or execute() should never fail! +/datum/dynamic_ruleset/proc/ready(forced = 0) + if (required_candidates > candidates.len) + return FALSE + return TRUE + +/// Gets weight of the ruleset +/// Note that this decreases weight if repeatable is TRUE and repeatable_weight_decrease is higher than 0 +/// Note: If you don't want repeatable rulesets to decrease their weight use the weight variable directly +/datum/dynamic_ruleset/proc/get_weight() + if(repeatable && weight > 1 && repeatable_weight_decrease > 0) + for(var/datum/dynamic_ruleset/DR in mode.executed_rules) + if(istype(DR, type)) + weight = max(weight-repeatable_weight_decrease,1) + return weight + +/// Here you can remove candidates that do not meet your requirements. +/// This means if their job is not correct or they have disconnected you can remove them from candidates here. +/// Usually this does not need to be changed unless you need some specific requirements from your candidates. +/datum/dynamic_ruleset/proc/trim_candidates() + return + +/// Counts how many players are ready at roundstart. +/// Used only by non-delayed roundstart rulesets. +/datum/dynamic_ruleset/proc/num_players() + . = 0 + for(var/mob/dead/new_player/P in GLOB.player_list) + if(P.client && P.ready == PLAYER_READY_TO_PLAY) + . ++ + +/// Set mode result and news report here. +/// Only called if ruleset is flagged as HIGHLANDER_RULESET +/datum/dynamic_ruleset/proc/round_result() + +/// Checks if round is finished, return true to end the round. +/// Only called if ruleset is flagged as HIGHLANDER_RULESET +/datum/dynamic_ruleset/proc/check_finished() + return FALSE + +////////////////////////////////////////////// +// // +// ROUNDSTART RULESETS // +// // +////////////////////////////////////////////// + +/// Checks if candidates are connected and if they are banned or don't want to be the antagonist. +/datum/dynamic_ruleset/roundstart/trim_candidates() + for(var/mob/dead/new_player/P in candidates) + if (!P.client || !P.mind) // Are they connected? + candidates.Remove(P) + continue + if(!mode.check_age(P.client, minimum_required_age)) + candidates.Remove(P) + continue + if(P.mind.special_role) // We really don't want to give antag to an antag. + candidates.Remove(P) + continue + if (!(antag_flag in P.client.prefs.be_special) || jobban_isbanned(P.ckey, list(antag_flag, ROLE_SYNDICATE)) || (antag_flag_override && jobban_isbanned(P.ckey, list(antag_flag_override, ROLE_SYNDICATE))))//are they willing and not antag-banned? + candidates.Remove(P) + continue + +/// Checks if candidates are required mob type, connected, banned and if the job is exclusive to the role. +/datum/dynamic_ruleset/roundstart/delayed/trim_candidates() + . = ..() + for (var/mob/P in candidates) + if (!istype(P, required_type)) + candidates.Remove(P) // Can be a new_player, etc. + continue + if(!mode.check_age(P.client, minimum_required_age)) + candidates.Remove(P) + continue + if (!P.client || !P.mind || !P.mind.assigned_role) // Are they connected? + candidates.Remove(P) + continue + if(P.mind.special_role || P.mind.antag_datums?.len > 0) // Are they an antag already? + candidates.Remove(P) + continue + if (!(antag_flag in P.client.prefs.be_special) || jobban_isbanned(P.ckey, list(antag_flag, ROLE_SYNDICATE)) || (antag_flag_override && jobban_isbanned(P.ckey, list(antag_flag_override, ROLE_SYNDICATE))))//are they willing and not antag-banned? + candidates.Remove(P) + continue + if ((exclusive_roles.len > 0) && !(P.mind.assigned_role in exclusive_roles)) // Is the rule exclusive to their job? + candidates.Remove(P) + continue + +/// Do your checks if the ruleset is ready to be executed here. +/// Should ignore certain checks if forced is TRUE +/datum/dynamic_ruleset/roundstart/ready(forced = FALSE) + return ..() \ No newline at end of file diff --git a/code/game/gamemodes/dynamic/dynamic_rulesets_events.dm b/code/game/gamemodes/dynamic/dynamic_rulesets_events.dm new file mode 100644 index 00000000..9bdd2503 --- /dev/null +++ b/code/game/gamemodes/dynamic/dynamic_rulesets_events.dm @@ -0,0 +1,676 @@ +/datum/dynamic_ruleset/event + ruletype = "Event" + var/typepath // typepath of the event + var/triggering + var/earliest_start = 20 MINUTES + var/occurances_current = 0 //Don't touch this. Skyrat Change. + var/occurances_max = 0 //Maximum occurances for this event. Set to 0 to allow an infinite amount of this event. Skyrat change. + +/datum/dynamic_ruleset/event/acceptable(population=0, threat=0) + if(GLOB.wizardstart.len == 0) + log_admin("Cannot accept Wizard ruleset. Couldn't find any wizard spawn points.") + message_admins("Cannot accept Wizard ruleset. Couldn't find any wizard spawn points.") + return FALSE + return ..() + +/datum/dynamic_ruleset/event/ready(forced = 0) + if (!forced) + var/job_check = 0 + if (enemy_roles.len > 0) + for (var/mob/M in mode.current_players[CURRENT_LIVING_PLAYERS]) + if (M.stat == DEAD) + continue // Dead players cannot count as opponents + if (M.mind && M.mind.assigned_role && (M.mind.assigned_role in enemy_roles) && (!(M in candidates) || (M.mind.assigned_role in restricted_roles))) + job_check++ // Checking for "enemies" (such as sec officers). To be counters, they must either not be candidates to that rule, or have a job that restricts them from it + + var/threat = round(mode.threat_level/10) + if (job_check < required_enemies[threat]) + return FALSE + return ..() + +/datum/dynamic_ruleset/event/execute() + var/datum/round_event/E = new typepath() + E.current_players = get_active_player_count(alive_check = 1, afk_check = 1, human_check = 1) + //E.control = src + SSblackbox.record_feedback("tally", "event_ran", 1, "[E]") + occurances_current++ + + testing("[time2text(world.time, "hh:mm:ss")] [E.type]") + deadchat_broadcast("[name] has just been randomly triggered!") //STOP ASSUMING IT'S BADMINS! + log_game("Random Event triggering: [name] ([typepath])") + + return E + +////////////////////////////////////////////// +// // +// PIRATES // +// // +////////////////////////////////////////////// + +/datum/dynamic_ruleset/event/pirates + name = "Space Pirates" + //config_tag = "pirates" + typepath = /datum/round_event/pirates + antag_flag = ROLE_TRAITOR + enemy_roles = list("AI","Security Officer","Head of Security","Captain") + required_enemies = list(2,2,1,1,0,0,0,0,0,0) + weight = 5 + cost = 10 + earliest_start = 50 MINUTES + blocking_rules = list(/datum/dynamic_ruleset/roundstart/nuclear,/datum/dynamic_ruleset/midround/from_ghosts/nuclear) + requirements = list(70,60,50,50,40,40,40,30,20,15) + //property_weights = list("story_potential" = 1, "trust" = 1, "chaos" = 1) + high_population_requirement = 15 + occurances_max = 1 + chaos_min = 2.0 + +/datum/dynamic_ruleset/event/pirates/ready(forced = FALSE) + if (!SSmapping.empty_space) + return FALSE + return ..() + +////////////////////////////////////////////// +// // +// SPIDERS // +// // +////////////////////////////////////////////// + +/datum/dynamic_ruleset/event/spiders + name = "Spider Infestation" + //config_tag = "spiders" + typepath = /datum/round_event/spider_infestation + enemy_roles = list("AI","Security Officer","Head of Security","Captain") + required_enemies = list(2,2,1,1,0,0,0,0,0,0) + weight = 5 + cost = 10 + requirements = list(70,60,50,50,40,40,40,30,20,15) + high_population_requirement = 15 + //property_weights = list("chaos" = 1, "valid" = 1) + earliest_start = 30 MINUTES //Skyrat change. + occurances_max = 2 + chaos_min = 1.5 + +////////////////////////////////////////////// +// // +// CLOGGED VENTS // +// // +////////////////////////////////////////////// + +/datum/dynamic_ruleset/event/ventclog + name = "Clogged Vents" + //config_tag = "ventclog" + typepath = /datum/round_event/vent_clog + enemy_roles = list("Chemist","Medical Doctor","Chief Medical Officer") + required_enemies = list(1,1,1,0,0,0,0,0,0,0) + cost = 2 + weight = 4 + repeatable_weight_decrease = 3 + requirements = list(10,10,10,5,5,5,5,5,5,5) // yes, can happen on fake-extended + high_population_requirement = 5 + repeatable = TRUE + //property_weights = list("chaos" = 1, "extended" = 2) + occurances_max = 2 + chaos_min = 1.5 + +/datum/dynamic_ruleset/event/ventclog/ready() + if(mode.threat_level > 30 && mode.threat >= 5 && prob(20)) + name = "Clogged Vents: Threatening" + cost = 10 + required_enemies = list(3,3,3,2,2,2,1,1,1,1) + typepath = /datum/round_event/vent_clog/threatening + else if(mode.threat_level > 15 && mode.threat > 15 && prob(30)) + name = "Clogged Vents: Catastrophic" + cost = 15 + required_enemies = list(2,2,1,1,1,1,0,0,0,0) + typepath = /datum/round_event/vent_clog/catastrophic + else + cost = 5 + name = "Clogged Vents: Normal" + required_enemies = list(1,1,1,0,0,0,0,0,0,0) + typepath = /datum/round_event/vent_clog + return ..() + +////////////////////////////////////////////// +// // +// ION STORM // +// // +////////////////////////////////////////////// + +/datum/dynamic_ruleset/event/ion_storm + name = "Ion Storm" + //config_tag = "ion_storm" + typepath = /datum/round_event/ion_storm + enemy_roles = list("Research Director","Captain","Chief Engineer") + required_enemies = list(1,1,0,0,0,0,0,0,0,0) + weight = 4 + // no repeatable weight decrease. too variable to be unfun multiple times in one round + cost = 1 + requirements = list(5,5,5,5,5,5,5,5,5,5) + high_population_requirement = 5 + repeatable = TRUE + //property_weights = list("story_potential" = 1, "extended" = 1) + //always_max_weight = TRUE + occurances_max = 3 + chaos_min = 1.0 + +////////////////////////////////////////////// +// // +// METEORS // +// // +////////////////////////////////////////////// + +/datum/dynamic_ruleset/event/meteor_wave + name = "Meteor Wave" + //config_tag = "meteor_wave" + typepath = /datum/round_event/meteor_wave + enemy_roles = list("Chief Engineer","Station Engineer","Atmospheric Technician","Captain","Cyborg") + required_enemies = list(2,2,2,2,2,2,2,2,2,2) + cost = 15 + weight = 3 + earliest_start = 45 MINUTES + repeatable_weight_decrease = 2 + requirements = list(60,50,40,30,30,30,30,30,30,30) + high_population_requirement = 30 + //property_weights = list("extended" = -2) + occurances_max = 2 + chaos_min = 1.5 + +/datum/dynamic_ruleset/event/meteor_wave/ready() + if(world.time-SSticker.round_start_time > 35 MINUTES && mode.threat_level > 40 && mode.threat >= 25 && prob(30)) + name = "Meteor Wave: Threatening" + cost = 25 + typepath = /datum/round_event/meteor_wave/threatening + else if(world.time-SSticker.round_start_time > 45 MINUTES && mode.threat_level > 50 && mode.threat >= 40 && prob(30)) + name = "Meteor Wave: Catastrophic" + cost = 40 + typepath = /datum/round_event/meteor_wave/catastrophic + required_enemies = list(3,3,3,3,3,3,3,3,3,3) + else + name = "Meteor Wave: Normal" + cost = 15 + typepath = /datum/round_event/meteor_wave + return ..() + +////////////////////////////////////////////// +// // +// ANOMALIES // +// // +////////////////////////////////////////////// + +/datum/dynamic_ruleset/event/anomaly_bluespace + name = "Anomaly: Bluespace" + //config_tag = "anomaly_bluespace" + typepath = /datum/round_event/anomaly/anomaly_bluespace + enemy_roles = list("Chief Engineer","Station Engineer","Atmospheric Technician","Research Director","Scientist","Captain") + required_enemies = list(1,1,1,0,0,0,0,0,0,0) + weight = 2 + earliest_start = 20 MINUTES + repeatable_weight_decrease = 1 + cost = 3 + requirements = list(5,5,5,5,5,5,5,5,5,5) + high_population_requirement = 5 + repeatable = TRUE + //property_weights = list("extended" = 1) + occurances_max = 3 + chaos_min = 1.5 + +/datum/dynamic_ruleset/event/anomaly_flux + name = "Anomaly: Hyper-Energetic Flux" + //config_tag = "anomaly_flux" + typepath = /datum/round_event/anomaly/anomaly_flux + enemy_roles = list("Chief Engineer","Station Engineer","Atmospheric Technician","Research Director","Scientist","Captain") + required_enemies = list(1,1,1,0,0,0,0,0,0,0) + weight = 2 + earliest_start = 20 MINUTES + repeatable_weight_decrease = 1 + cost = 5 + requirements = list(5,5,5,5,5,5,5,5,5,5) + high_population_requirement = 10 + repeatable = TRUE + //property_weights = list("extended" = 1) + occurances_max = 2 + chaos_min = 1.5 + +/datum/dynamic_ruleset/event/anomaly_gravitational + name = "Anomaly: Gravitational" + //config_tag = "anomaly_gravitational" + typepath = /datum/round_event/anomaly/anomaly_grav + weight = 2 + repeatable_weight_decrease = 1 + cost = 3 + requirements = list(5,5,5,5,5,5,5,5,5,5) + high_population_requirement = 5 + repeatable = TRUE + //property_weights = list("extended" = 1) + occurances_max = 3 + chaos_min = 0.5 + +/datum/dynamic_ruleset/event/anomaly_pyroclastic + name = "Anomaly: Pyroclastic" + //config_tag = "anomaly_pyroclastic" + typepath = /datum/round_event/anomaly/anomaly_pyro + weight = 2 + earliest_start = 20 MINUTES + repeatable_weight_decrease = 1 + cost = 5 + enemy_roles = list("Chief Engineer","Station Engineer","Atmospheric Technician","Research Director","Scientist","Captain","Cyborg") + required_enemies = list(1,1,1,1,1,1,1,1,1,1) + requirements = list(10,10,10,10,10,10,10,10,10,10) + high_population_requirement = 10 + repeatable = TRUE + //property_weights = list("extended" = 1) + occurances_max = 2 + chaos_min = 1.5 + +/datum/dynamic_ruleset/event/anomaly_vortex + name = "Anomaly: Vortex" + //config_tag = "anomaly_vortex" + typepath = /datum/round_event/anomaly/anomaly_vortex + weight = 2 + earliest_start = 30 MINUTES + repeatable_weight_decrease = 1 + cost = 5 + enemy_roles = list("Chief Engineer","Station Engineer","Atmospheric Technician","Research Director","Scientist","Captain","Cyborg") + required_enemies = list(1,1,1,1,1,1,1,1,1,1) + requirements = list(10,10,10,10,10,10,10,10,10,10) + high_population_requirement = 10 + repeatable = TRUE + //property_weights = list("extended" = 1) + occurances_max = 2 + chaos_min = 2.0 + +////////////////////////////////////////////// +// // +// LONE OPERATIVE // +// // +////////////////////////////////////////////// + +/datum/dynamic_ruleset/event/operative + name = "Lone Operative" + typepath = /datum/round_event/ghost_role/operative + weight = 0 //This is changed in nuclearbomb.dm + occurances_max = 1 + requirements = list(20,20,20,20,20,20,20,20,20,20) //SECURE THAT DISK + cost = 50 + +/datum/dynamic_ruleset/event/operative/ready() + var/datum/round_event_control/operative/loneop = locate(/datum/round_event_control/operative) in SSevents.control + if(istype(loneop)) + weight = loneop.weight //Get the weight whenever it's called. + +////////////////////////////////////////////// +// // +// WOW THAT'S A LOT OF EVENTS // +// // +////////////////////////////////////////////// + +/datum/dynamic_ruleset/event/brand_intelligence + name = "Brand Intelligence" + //config_tag = "brand_intelligence" + typepath = /datum/round_event/brand_intelligence + weight = 1 + earliest_start = 30 MINUTES + repeatable_weight_decrease = 1 + cost = 10 + enemy_roles = list("Chief Engineer","Station Engineer","Atmospheric Technician","Research Director","Scientist","Captain","Cyborg") + required_enemies = list(1,1,1,1,0,0,0,0,0,0) + requirements = list(50,40,30,25,20,15,10,10,10,10) + high_population_requirement = 10 + repeatable = TRUE + //property_weights = list("extended" = -1, "chaos" = 1) + occurances_max = 1 + chaos_min = 2.0 + +/datum/dynamic_ruleset/event/carp_migration + name = "Carp Migration" + //config_tag = "carp_migration" + typepath = /datum/round_event/carp_migration + weight = 7 + repeatable_weight_decrease = 3 + cost = 4 + requirements = list(101,20,15,10,10,10,10,10,10,10) + high_population_requirement = 10 + earliest_start = 10 MINUTES + repeatable = TRUE + //property_weights = list("extended" = 1) + occurances_max = 3 + chaos_min = 0.5 + +/datum/dynamic_ruleset/event/communications_blackout + name = "Communications Blackout" + //config_tag = "communications_blackout" + typepath = /datum/round_event/communications_blackout + cost = 4 + weight = 10 + repeatable_weight_decrease = 2 + enemy_roles = list("Chief Engineer","Station Engineer") + required_enemies = list(1,1,1,0,0,0,0,0,0,0) + requirements = list(5,5,5,10,10,10,10,15,15,15) + high_population_requirement = 5 + repeatable = TRUE + //property_weights = list("extended" = 1, "chaos" = 1) + occurances_max = 5 + +/datum/dynamic_ruleset/event/processor_overload + name = "Processor Overload" + //config_tag = "processor_overload" + typepath = /datum/round_event/processor_overload + cost = 4 + weight = 2 + repeatable_weight_decrease = 3 + enemy_roles = list("Chief Engineer","Station Engineer") + required_enemies = list(1,1,1,1,1,1,1,1,1,1) + requirements = list(101,101,10,10,10,15,15,15,15,15) + high_population_requirement = 5 + repeatable = TRUE + //property_weights = list("extended" = 1, "chaos" = 1) + //always_max_weight = TRUE + occurances_max = 2 + +/datum/dynamic_ruleset/event/space_dust + name = "Minor Space Dust" + //config_tag = "space_dust" + typepath = /datum/round_event/space_dust + cost = 2 + weight = 10 + repeatable_weight_decrease = 2 + enemy_roles = list("Chief Engineer","Station Engineer") + required_enemies = list(1,1,1,0,0,0,0,0,0,0) + requirements = list(5,5,5,5,5,5,5,5,5,5) + high_population_requirement = 5 + repeatable = TRUE + earliest_start = 0 MINUTES + //property_weights = list("extended" = 1) + //always_max_weight = TRUE + occurances_max = 0 + +/datum/dynamic_ruleset/event/major_dust + name = "Major Space Dust" + //config_tag = "major_dust" + typepath = /datum/round_event/meteor_wave/major_dust + cost = 5 + weight = 2 + repeatable_weight_decrease = 1 + enemy_roles = list("Chief Engineer","Station Engineer") + required_enemies = list(2,2,2,2,2,2,2,2,2,2) + requirements = list(20,15,15,10,10,10,10,10,10,10) + high_population_requirement = 10 + repeatable = TRUE + //property_weights = list("extended" = 1) + occurances_max = 3 + +/datum/dynamic_ruleset/event/electrical_storm + name = "Electrical Storm" + typepath = /datum/round_event/electrical_storm + cost = 1 + weight = 20 + repeatable_weight_decrease = 2 + enemy_roles = list("Chief Engineer","Station Engineer") + required_enemies = list(1,1,1,0,0,0,0,0,0,0) + requirements = list(5,5,5,5,5,5,5,5,5,5) + high_population_requirement = 5 + repeatable = TRUE + occurances_max = 10 + +/datum/dynamic_ruleset/event/heart_attack + name = "Random Heart Attack" + typepath = /datum/round_event/heart_attack + cost = 15 + weight = 2 + earliest_start = 30 MINUTES + repeatable_weight_decrease = 1 + enemy_roles = list("Medical Doctor","Chief Medical Officer") + required_enemies = list(2,2,2,2,2,2,2,2,2,2) + requirements = list(101,101,101,5,5,5,5,5,5,5) + high_population_requirement = 5 + repeatable = TRUE + occurances_max = 2 + +/datum/dynamic_ruleset/event/radiation_storm + name = "Radiation Storm" + //config_tag = "radiation_storm" + typepath = /datum/round_event/radiation_storm + cost = 3 + weight = 3 + enemy_roles = list("Chemist","Chief Medical Officer","Geneticist","Medical Doctor","AI","Captain") + required_enemies = list(1,1,1,1,1,1,1,1,1,1) + requirements = list(5,5,5,5,5,5,5,5,5,5) + high_population_requirement = 5 + //property_weights = list("extended" = 1,"chaos" = 1) + occurances_max = 2 + +/datum/dynamic_ruleset/event/portal_storm_syndicate + name = "Portal Storm" + //config_tag = "portal_storm" + typepath = /datum/round_event/portal_storm/syndicate_shocktroop + cost = 10 + weight = 1 + enemy_roles = list("Head of Security","Security Officer","AI","Captain","Shaft Miner") + required_enemies = list(2,2,2,2,2,2,2,2,2,2) + requirements = list(101,101,101,30,30,30,30,30,30,30) + high_population_requirement = 30 + earliest_start = 40 MINUTES + //property_weights = list("teamwork" = 1,"chaos" = 1, "extended" = -1) + occurances_max = 1 + chaos_min = 1.0 + + +/datum/dynamic_ruleset/event/wormholes + name = "Wormholes" + //config_tag = "wormhole" + typepath = /datum/round_event/wormholes + cost = 3 + weight = 4 + enemy_roles = list("AI","Medical Doctor","Station Engineer","Head of Personnel","Captain") + required_enemies = list(2,2,2,2,2,2,2,2,2,2) + requirements = list(5,5,5,5,5,5,5,5,5,5) + high_population_requirement = 5 + //property_weights = list("extended" = 1) + occurances_max = 2 + chaos_min = 1.0 + +/datum/dynamic_ruleset/event/swarmers + name = "Swarmers" + //config_tag = "swarmer" + typepath = /datum/round_event/spawn_swarmer + cost = 10 + weight = 1 + earliest_start = 40 MINUTES + enemy_roles = list("AI","Security Officer","Head of Security","Captain","Station Engineer","Atmos Technician","Chief Engineer") + required_enemies = list(4,4,4,4,3,3,3,3,3,3) + requirements = list(101,101,101,101,50,45,40,35,30,30) + high_population_requirement = 5 + //property_weights = list("extended" = -2) + occurances_max = 1 + chaos_min = 2.0 + +/datum/dynamic_ruleset/event/sentient_disease + name = "Sentient Disease" + //config_tag = "sentient_disease" + typepath = /datum/round_event/ghost_role/sentient_disease + enemy_roles = list("Virologist","Chief Medical Officer","Captain","Chemist") + required_enemies = list(2,1,1,1,0,0,0,0,0,0) + required_candidates = 1 + weight = 4 + cost = 5 + requirements = list(30,30,20,20,15,10,10,10,10,5) // yes, it can even happen in "extended"! + //property_weights = list("story_potential" = 1, "extended" = 1, "valid" = -2) + high_population_requirement = 5 + occurances_max = 1 + chaos_min = 1.5 + +/datum/dynamic_ruleset/event/revenant + name = "Revenant" + //config_tag = "revenant" + typepath = /datum/round_event/ghost_role/revenant + enemy_roles = list("Chief Engineer","Station Engineer","Captain","Chaplain","AI") + required_enemies = list(2,1,1,1,0,0,0,0,0,0) + required_candidates = 1 + weight = 3 + cost = 5 + requirements = list(101,40,35,30,20,15,15,15,15,15) + high_population_requirement = 15 + //property_weights = list("story_potential" = -2, "extended" = -1) + occurances_max = 1 //Skyrat change. + chaos_min = 1.5 + +/datum/dynamic_ruleset/event/immovable_rod + name = "Immovable Rod" + typepath = /datum/round_event/immovable_rod + enemy_roles = list("Research Director","Chief Engineer","Station Engineer","Captain","Chaplain","AI") + required_enemies = list(2,2,2,2,2,2,1,1,1,1) + requirements = list(101,101,40,30,30,20,15,15,15,15) + high_population_requirement = 15 + cost = 10 + occurances_max = 2 + weight = 4 + earliest_start = 30 MINUTES + chaos_min = 1.5 + + +////////////////////////////////////////////// +// // +// NEUTRAL EVENTS // +// // +////////////////////////////////////////////// + + +/datum/dynamic_ruleset/event/aurora_caelus //A good omen. Drop chaos a little. + name = "Aurora Caelus" + typepath = /datum/round_event/aurora_caelus + requirements = list(0,0,0,0,0,0,0,0,0,0) + high_population_requirement = 0 + weight = 5 + cost = 10 + repeatable = TRUE + occurances_max = 2 + +/datum/dynamic_ruleset/event/high_priority_bounty + name = "High Priority Bounty" + typepath = /datum/round_event/high_priority_bounty + requirements = list(0,0,0,0,0,0,0,0,0,0) + high_population_requirement = 0 + weight = 10 + repeatable = TRUE + occurances_max = 3 + +/datum/dynamic_ruleset/event/bureaucratic_error + name = "Bureaucratic Error" + requirements = list(0,0,0,0,0,0,0,0,0,0) + high_population_requirement = 0 + typepath = /datum/round_event/bureaucratic_error + occurances_max = 1 + weight = 5 + +/datum/dynamic_ruleset/event/camera_failure + name = "Camera Failure" + typepath = /datum/round_event/camera_failure + requirements = list(0,0,0,0,0,0,0,0,0,0) + high_population_requirement = 0 + weight = 100 + repeatable = TRUE + occurances_max = 20 + +/datum/dynamic_ruleset/event/disease_outbreak + name = "Disease Outbreak" + enemy_roles = list("Virologist","Chief Medical Officer","Captain","Chemist") + required_enemies = list(2,1,1,1,0,0,0,0,0,0) + typepath = /datum/round_event/disease_outbreak + requirements = list(10,10,10,5,5,5,5,5,5,5) + high_population_requirement = 5 + weight = 5 + cost = 5 + repeatable = TRUE + occurances_max = 1 + +/datum/dynamic_ruleset/event/falsealarm + name = "False Alarm" + typepath = /datum/round_event/falsealarm + requirements = list(0,0,0,0,0,0,0,0,0,0) + high_population_requirement = 0 + weight = 20 + repeatable = TRUE + occurances_max = 5 + +/datum/dynamic_ruleset/event/grid_check + name = "Grid Check" + typepath = /datum/round_event/grid_check + requirements = list(5,5,5,5,5,5,0,0,0,0) //Can actually cause problems + high_population_requirement = 0 + weight = 5 + repeatable = TRUE + occurances_max = 2 + +/datum/dynamic_ruleset/event/mass_hallucination + name = "Mass Hallucination" + typepath = /datum/round_event/mass_hallucination + requirements = list(0,0,0,0,0,0,0,0,0,0) + high_population_requirement = 0 + weight = 4 + repeatable = TRUE + occurances_max = 2 + +/datum/dynamic_ruleset/event/mice_migration + name = "Mice Migration" + typepath = /datum/round_event/mice_migration + enemy_roles = list("Chief Engineer","Station Engineer","Captain","Cook","Atmospheric Technician") + required_enemies = list(1,1,1,1,1,0,0,0,0,0) + requirements = list(10,5,5,5,0,0,0,0,0,0) + high_population_requirement = 0 + weight = 10 + repeatable = TRUE + occurances_max = 3 + +/datum/dynamic_ruleset/event/grey_tide + name = "Grey Tide" + typepath = /datum/round_event/grey_tide + enemy_roles = list("Chief Engineer","Station Engineer","Captain","Atmospheric Technician","AI","Cyborg") + required_enemies = list(3,2,2,2,2,2,1,1,1,1) + requirements = list(101,20,15,15,10,10,5,5,5,5) + high_population_requirement = 0 + repeatable = TRUE + weight = 5 + occurances_max = 2 + +/datum/dynamic_ruleset/event/sentience + name = "Random Human-level Intelligence" + typepath = /datum/round_event/ghost_role/sentience + requirements = list(0,0,0,0,0,0,0,0,0,0) + high_population_requirement = 0 + weight = 10 + +/datum/dynamic_ruleset/event/shuttle_loan + name = "Shuttle Loan" + typepath = /datum/round_event/shuttle_loan + enemy_roles = list("Quartermaster","Cargo Technician") + required_enemies = list(1,1,1,1,1,1,1,1,1,1) + requirements = list(10,10,10,5,5,5,0,0,0,0) + high_population_requirement = 0 + weight = 10 + repeatable = TRUE + occurances_max = 2 + +/datum/dynamic_ruleset/event/spacevine + name = "Spacevine" + typepath = /datum/round_event/spacevine + enemy_roles = list("Cook","Botanist","Security Officer","Captain","Station Engineer") + required_enemies = list(2,2,2,1,1,1,1,1,1,1) + requirements = list(101,30,25,20,15,15,10,10,10,10) + high_population_requirement = 0 + weight = 8 + repeatable_weight_decrease = 5 + earliest_start = 20 MINUTES + repeatable = TRUE + occurances_max = 3 + +/datum/dynamic_ruleset/event/spontaneous_appendicitis + name = "Spontaneous Appendicitis" + typepath = /datum/round_event/spontaneous_appendicitis + enemy_roles = list("Medical Doctor","Chief Medical Officer") + required_enemies = list(2,2,1,1,1,1,1,1,1,1) + requirements = list(15,10,10,5,5,5,5,5,5,5) + high_population_requirement = 5 + weight = 12 + repeatable = TRUE + repeatable_weight_decrease = 5 + occurances_max = 4 \ No newline at end of file diff --git a/code/game/gamemodes/dynamic/dynamic_rulesets_latejoin.dm b/code/game/gamemodes/dynamic/dynamic_rulesets_latejoin.dm new file mode 100644 index 00000000..a51cd078 --- /dev/null +++ b/code/game/gamemodes/dynamic/dynamic_rulesets_latejoin.dm @@ -0,0 +1,112 @@ +////////////////////////////////////////////// +// // +// LATEJOIN RULESETS // +// // +////////////////////////////////////////////// + +/datum/dynamic_ruleset/latejoin/trim_candidates() + for(var/mob/P in candidates) + if (!P.client || !P.mind || !P.mind.assigned_role) // Are they connected? + candidates.Remove(P) + continue + if(!mode.check_age(P.client, minimum_required_age)) + candidates.Remove(P) + continue + if (!(antag_flag in P.client.prefs.be_special) || jobban_isbanned(P.ckey, list(antag_flag, ROLE_SYNDICATE)) || (antag_flag_override && jobban_isbanned(P.ckey, list(antag_flag_override))))//are they willing and not antag-banned? + candidates.Remove(P) + continue + if (P.mind.assigned_role in restricted_roles) // Does their job allow for it? + candidates.Remove(P) + continue + if ((exclusive_roles.len > 0) && !(P.mind.assigned_role in exclusive_roles)) // Is the rule exclusive to their job? + candidates.Remove(P) + continue + +/datum/dynamic_ruleset/latejoin/ready(forced = 0) + if (!forced) + var/job_check = 0 + if (enemy_roles.len > 0) + for (var/mob/M in mode.current_players[CURRENT_LIVING_PLAYERS]) + if (M.stat == DEAD) + continue // Dead players cannot count as opponents + if (M.mind && M.mind.assigned_role && (M.mind.assigned_role in enemy_roles) && (!(M in candidates) || (M.mind.assigned_role in restricted_roles))) + job_check++ // Checking for "enemies" (such as sec officers). To be counters, they must either not be candidates to that rule, or have a job that restricts them from it + + var/threat = round(mode.threat_level/10) + if (job_check < required_enemies[threat]) + return FALSE + return ..() + +/datum/dynamic_ruleset/latejoin/execute() + var/mob/M = pick(candidates) + assigned += M.mind + M.mind.special_role = antag_flag + M.mind.add_antag_datum(antag_datum) + return TRUE + +////////////////////////////////////////////// +// // +// SYNDICATE TRAITORS // +// // +////////////////////////////////////////////// + +/datum/dynamic_ruleset/latejoin/infiltrator + name = "Syndicate Infiltrator" + antag_datum = /datum/antagonist/traitor + antag_flag = ROLE_TRAITOR + restricted_roles = list("AI", "Cyborg") + protected_roles = list("Security Officer", "Warden", "Detective", "Head of Security", "Captain", "Head of Personnel", "Chief Engineer", "Chief Medical Officer", "Research Director", "Quartermaster") + required_candidates = 1 + weight = 7 + cost = 5 + requirements = list(40,30,20,10,10,10,10,10,10,10) + high_population_requirement = 10 + repeatable = TRUE + flags = TRAITOR_RULESET + chaos_min = 3.0 + +////////////////////////////////////////////// +// // +// REVOLUTIONARY PROVOCATEUR // +// // +////////////////////////////////////////////// + +/datum/dynamic_ruleset/latejoin/provocateur + name = "Provocateur" + antag_datum = /datum/antagonist/rev/head + antag_flag = ROLE_REV_HEAD + antag_flag_override = ROLE_REV + restricted_roles = list("AI", "Cyborg", "Security Officer", "Warden", "Detective", "Head of Security", "Captain", "Head of Personnel", "Chief Engineer", "Chief Medical Officer", "Research Director", "Quartermaster") + enemy_roles = list("AI", "Cyborg", "Security Officer","Detective","Head of Security", "Captain", "Warden") + required_enemies = list(2,2,1,1,1,1,1,0,0,0) + required_candidates = 1 + weight = 2 + cost = 20 + requirements = list(101,101,70,40,30,20,20,20,20,20) + high_population_requirement = 50 + flags = HIGHLANDER_RULESET + var/required_heads = 3 + chaos_min = 4.0 + +/datum/dynamic_ruleset/latejoin/provocateur/ready(forced=FALSE) + if (forced) + required_heads = 1 + if(!..()) + return FALSE + var/head_check = 0 + for(var/mob/player in mode.current_players[CURRENT_LIVING_PLAYERS]) + if (player.mind.assigned_role in GLOB.command_positions) + head_check++ + return (head_check >= required_heads) + +/datum/dynamic_ruleset/latejoin/provocateur/execute() + var/mob/M = pick(candidates) + assigned += M.mind + M.mind.special_role = antag_flag + var/datum/antagonist/rev/head/new_head = new() + new_head.give_flash = TRUE + new_head.give_hud = TRUE + new_head.remove_clumsy = TRUE + new_head = M.mind.add_antag_datum(new_head) + new_head.rev_team.max_headrevs = 1 // Only one revhead if it is latejoin. + return TRUE \ No newline at end of file diff --git a/code/game/gamemodes/dynamic/dynamic_rulesets_midround.dm b/code/game/gamemodes/dynamic/dynamic_rulesets_midround.dm new file mode 100644 index 00000000..e5452a44 --- /dev/null +++ b/code/game/gamemodes/dynamic/dynamic_rulesets_midround.dm @@ -0,0 +1,599 @@ +#define REVENANT_SPAWN_THRESHOLD 20 +#define ABDUCTOR_MAX_TEAMS 4 // blame TG for not using the defines files + +////////////////////////////////////////////// +// // +// MIDROUND RULESETS // +// // +////////////////////////////////////////////// + +/datum/dynamic_ruleset/midround // Can be drafted once in a while during a round + ruletype = "Midround" + /// If the ruleset should be restricted from ghost roles. + var/restrict_ghost_roles = TRUE + /// What type the ruleset is restricted to. + var/required_type = /mob/living/carbon/human + var/list/living_players = list() + var/list/living_antags = list() + var/list/dead_players = list() + var/list/list_observers = list() + +/datum/dynamic_ruleset/midround/from_ghosts + weight = 0 + /// Whether the ruleset should call generate_ruleset_body or not. + var/makeBody = TRUE + +/datum/dynamic_ruleset/midround/trim_candidates() + // Unlike the previous two types, these rulesets are not meant for /mob/dead/new_player + // And since I want those rulesets to be as flexible as possible, I'm not gonna put much here, + // + // All you need to know is that here, the candidates list contains 4 lists itself, indexed with the following defines: + // Candidates = list(CURRENT_LIVING_PLAYERS, CURRENT_LIVING_ANTAGS, CURRENT_DEAD_PLAYERS, CURRENT_OBSERVERS) + // So for example you can get the list of all current dead players with var/list/dead_players = candidates[CURRENT_DEAD_PLAYERS] + // Make sure to properly typecheck the mobs in those lists, as the dead_players list could contain ghosts, or dead players still in their bodies. + // We're still gonna trim the obvious (mobs without clients, jobbanned players, etc) + living_players = trim_list(mode.current_players[CURRENT_LIVING_PLAYERS]) + living_antags = trim_list(mode.current_players[CURRENT_LIVING_ANTAGS]) + dead_players = trim_list(mode.current_players[CURRENT_DEAD_PLAYERS]) + list_observers = trim_list(mode.current_players[CURRENT_OBSERVERS]) + +/datum/dynamic_ruleset/midround/proc/trim_list(list/L = list()) + var/list/trimmed_list = L.Copy() + var/antag_name = initial(antag_flag) + for(var/mob/living/M in trimmed_list) + if (!istype(M, required_type)) + trimmed_list.Remove(M) + continue + if (!M.client) // Are they connected? + trimmed_list.Remove(M) + continue + if(!mode.check_age(M.client, minimum_required_age)) + trimmed_list.Remove(M) + continue + if (!(antag_name in M.client.prefs.be_special) || jobban_isbanned(M.ckey, list(antag_name, ROLE_SYNDICATE)))//are they willing and not antag-banned? + trimmed_list.Remove(M) + continue + if (M.mind) + if (restrict_ghost_roles && M.mind.assigned_role in GLOB.exp_specialmap[EXP_TYPE_SPECIAL]) // Are they playing a ghost role? + trimmed_list.Remove(M) + continue + if (M.mind.assigned_role in restricted_roles || HAS_TRAIT(M, TRAIT_MINDSHIELD)) // Does their job allow it or are they mindshielded? + trimmed_list.Remove(M) + continue + if ((exclusive_roles.len > 0) && !(M.mind.assigned_role in exclusive_roles)) // Is the rule exclusive to their job? + trimmed_list.Remove(M) + continue + return trimmed_list + +// You can then for example prompt dead players in execute() to join as strike teams or whatever +// Or autotator someone + +// IMPORTANT, since /datum/dynamic_ruleset/midround may accept candidates from both living, dead, and even antag players, you need to manually check whether there are enough candidates +// (see /datum/dynamic_ruleset/midround/autotraitor/ready(var/forced = FALSE) for example) +/datum/dynamic_ruleset/midround/ready(forced = FALSE) + if (!forced) + var/job_check = 0 + if (enemy_roles.len > 0) + for (var/mob/M in living_players) + if (M.stat == DEAD) + continue // Dead players cannot count as opponents + if (M.mind && M.mind.assigned_role && (M.mind.assigned_role in enemy_roles) && (!(M in candidates) || (M.mind.assigned_role in restricted_roles))) + job_check++ // Checking for "enemies" (such as sec officers). To be counters, they must either not be candidates to that rule, or have a job that restricts them from it + + var/threat = round(mode.threat_level/10) + if (job_check < required_enemies[threat]) + return FALSE + return TRUE + +/datum/dynamic_ruleset/midround/from_ghosts/execute() + var/list/possible_candidates = list() + possible_candidates.Add(dead_players) + possible_candidates.Add(list_observers) + send_applications(possible_candidates) + if(assigned.len > 0) + return TRUE + else + return FALSE + +/// This sends a poll to ghosts if they want to be a ghost spawn from a ruleset. +/datum/dynamic_ruleset/midround/from_ghosts/proc/send_applications(list/possible_volunteers = list()) + if (possible_volunteers.len <= 0) // This shouldn't happen, as ready() should return FALSE if there is not a single valid candidate + message_admins("Possible volunteers was 0. This shouldn't appear, because of ready(), unless you forced it!") + return + message_admins("Polling [possible_volunteers.len] players to apply for the [name] ruleset.") + log_game("DYNAMIC: Polling [possible_volunteers.len] players to apply for the [name] ruleset.") + + candidates = pollGhostCandidates("The mode is looking for volunteers to become [antag_flag] for [name]", antag_flag, SSticker.mode, antag_flag, poll_time = 300) + + if(!candidates || candidates.len <= 0) + message_admins("The ruleset [name] received no applications.") + log_game("DYNAMIC: The ruleset [name] received no applications.") + mode.refund_threat(cost) + mode.threat_log += "[worldtime2text()]: Rule [name] refunded [cost] (no applications)" + mode.executed_rules -= src + return + + message_admins("[candidates.len] players volunteered for the ruleset [name].") + log_game("DYNAMIC: [candidates.len] players volunteered for [name].") + review_applications() + +/// Here is where you can check if your ghost applicants are valid for the ruleset. +/// Called by send_applications(). +/datum/dynamic_ruleset/midround/from_ghosts/proc/review_applications() + for (var/i = 1, i <= required_candidates, i++) + if(candidates.len <= 0) + if(i == 1) + // We have found no candidates so far and we are out of applicants. + mode.refund_threat(cost) + mode.threat_log += "[worldtime2text()]: Rule [name] refunded [cost] (all applications invalid)" + mode.executed_rules -= src + break + var/mob/applicant = pick(candidates) + candidates -= applicant + if(!isobserver(applicant)) + if(applicant.stat == DEAD) // Not an observer? If they're dead, make them one. + applicant = applicant.ghostize(FALSE) + else // Not dead? Disregard them, pick a new applicant + i-- + continue + + if(!applicant) + i-- + continue + + var/mob/new_character = applicant + + if (makeBody) + new_character = generate_ruleset_body(applicant) + + finish_setup(new_character, i) + assigned += applicant + notify_ghosts("[new_character] has been picked for the ruleset [name]!", source = new_character, action = NOTIFY_ORBIT) + +/datum/dynamic_ruleset/midround/from_ghosts/proc/generate_ruleset_body(mob/applicant) + var/mob/living/carbon/human/new_character = makeBody(applicant) + new_character.dna.remove_all_mutations() + return new_character + +/datum/dynamic_ruleset/midround/from_ghosts/proc/finish_setup(mob/new_character, index) + var/datum/antagonist/new_role = new antag_datum() + setup_role(new_role) + new_character.mind.add_antag_datum(new_role) + new_character.mind.special_role = antag_flag + +/datum/dynamic_ruleset/midround/from_ghosts/proc/setup_role(datum/antagonist/new_role) + return + +////////////////////////////////////////////// +// // +// SYNDICATE TRAITORS // +// // +////////////////////////////////////////////// + +/datum/dynamic_ruleset/midround/autotraitor + name = "Syndicate Sleeper Agent" + antag_datum = /datum/antagonist/traitor + antag_flag = ROLE_TRAITOR + restricted_roles = list("AI", "Cyborg", "Positronic Brain") + protected_roles = list("Security Officer", "Warden", "Detective", "Head of Security", "Captain", "Head of Personnel", "Chief Engineer", "Chief Medical Officer", "Research Director", "Quartermaster") + required_candidates = 1 + weight = 7 + cost = 10 + requirements = list(101,101,60,55,50,45,40,35,30,25) + repeatable = TRUE + high_population_requirement = 10 + flags = TRAITOR_RULESET + chaos_min = 3.0 + +/datum/dynamic_ruleset/midround/autotraitor/thief + name = "Syndicate Sleeper Agent" + antag_datum = /datum/antagonist/traitor/thief + antag_flag = ROLE_TRAITOR + restricted_roles = list("AI", "Cyborg", "Positronic Brain") + protected_roles = list("Security Officer", "Warden", "Detective", "Head of Security", "Captain", "Head of Personnel", "Chief Engineer", "Chief Medical Officer", "Research Director", "Quartermaster") + required_candidates = 1 + weight = 7 + cost = 10 + requirements = list(50,45,40,35,30,25,20,15,10,5) + repeatable = TRUE + high_population_requirement = 10 + flags = TRAITOR_RULESET + chaos_min = 2.0 + chaos_max = 3.0 + +/datum/dynamic_ruleset/midround/autotraitor/acceptable(population = 0, threat = 0) + var/player_count = mode.current_players[CURRENT_LIVING_PLAYERS].len + var/antag_count = mode.current_players[CURRENT_LIVING_ANTAGS].len + var/max_traitors = round(player_count / 10) + 1 + if ((antag_count < max_traitors) && prob(mode.threat_level))//adding traitors if the antag population is getting low + return ..() + else + return FALSE + +/datum/dynamic_ruleset/midround/autotraitor/trim_candidates() + ..() + for(var/mob/living/player in living_players) + if(issilicon(player)) // Your assigned role doesn't change when you are turned into a silicon. + living_players -= player + continue + if(is_centcom_level(player.z)) + living_players -= player // We don't autotator people in CentCom + continue + if(player.mind && (player.mind.special_role || player.mind.antag_datums?.len > 0)) + living_players -= player // We don't autotator people with roles already + +/datum/dynamic_ruleset/midround/autotraitor/ready(forced = FALSE) + if (required_candidates > living_players.len) + return FALSE + return ..() + +/datum/dynamic_ruleset/midround/autotraitor/execute() + var/mob/M = pick(living_players) + assigned += M + living_players -= M + var/datum/antagonist/traitor/newTraitor = new + M.mind.add_antag_datum(newTraitor) + return TRUE + + +////////////////////////////////////////////// +// // +// Malfunctioning AI // +// // +////////////////////////////////////////////// + +/datum/dynamic_ruleset/midround/malf + name = "Malfunctioning AI" + antag_datum = /datum/antagonist/traitor + antag_flag = ROLE_MALF + enemy_roles = list("Security Officer", "Warden","Detective","Head of Security", "Captain", "Scientist", "Research Director", "Chief Engineer") + exclusive_roles = list("AI") + required_enemies = list(4,4,4,4,4,4,2,2,2,0) + required_candidates = 1 + weight = 3 + cost = 35 + requirements = list(101,101,80,70,60,50,40,30,20,20) + high_population_requirement = 35 + required_type = /mob/living/silicon/ai + var/ion_announce = 33 + var/removeDontImproveChance = 10 + chaos_min = 3.0 + +/datum/dynamic_ruleset/midround/malf/trim_candidates() + ..() + candidates = candidates[CURRENT_LIVING_PLAYERS] + for(var/mob/living/player in candidates) + if(!isAI(player)) + candidates -= player + continue + if(is_centcom_level(player.z)) + candidates -= player + continue + if(player.mind && (player.mind.special_role || player.mind.antag_datums?.len > 0)) + candidates -= player + +/datum/dynamic_ruleset/midround/malf/execute() + if(!candidates || !candidates.len) + return FALSE + var/mob/living/silicon/ai/M = pick(candidates) + candidates -= M + assigned += M.mind + var/datum/antagonist/traitor/AI = new + M.mind.special_role = antag_flag + M.mind.add_antag_datum(AI) + if(prob(ion_announce)) + priority_announce("Ion storm detected near the station. Please check all AI-controlled equipment for errors.", "Anomaly Alert", 'sound/ai/ionstorm.ogg') + if(prob(removeDontImproveChance)) + M.replace_random_law(generate_ion_law(), list(LAW_INHERENT, LAW_SUPPLIED, LAW_ION)) + else + M.add_ion_law(generate_ion_law()) + return TRUE + +////////////////////////////////////////////// +// // +// WIZARD (GHOST) // +// // +////////////////////////////////////////////// + +/datum/dynamic_ruleset/midround/from_ghosts/wizard + name = "Wizard" + antag_datum = /datum/antagonist/wizard + antag_flag = ROLE_WIZARD + enemy_roles = list("Security Officer","Detective","Head of Security", "Captain") + required_enemies = list(2,2,1,1,1,1,1,0,0,0) + required_candidates = 1 + weight = 1 + cost = 20 + requirements = list(101,90,80,70,60,50,40,30,30,30) + high_population_requirement = 50 + repeatable = FALSE //WE DON'T NEED MORE THAN ONE WIZARD + chaos_min = 3.5 + +/datum/dynamic_ruleset/midround/from_ghosts/wizard/ready(forced = FALSE) + if (required_candidates > (dead_players.len + list_observers.len)) + return FALSE + if(GLOB.wizardstart.len == 0) + log_admin("Cannot accept Wizard ruleset. Couldn't find any wizard spawn points.") + message_admins("Cannot accept Wizard ruleset. Couldn't find any wizard spawn points.") + return FALSE + return ..() + +/datum/dynamic_ruleset/midround/from_ghosts/wizard/finish_setup(mob/new_character, index) + ..() + new_character.forceMove(pick(GLOB.wizardstart)) + +////////////////////////////////////////////// +// // +// NUCLEAR OPERATIVES (MIDROUND) // +// // +////////////////////////////////////////////// + +/datum/dynamic_ruleset/midround/from_ghosts/nuclear + name = "Nuclear Assault" + antag_flag = ROLE_OPERATIVE + antag_datum = /datum/antagonist/nukeop + enemy_roles = list("AI", "Cyborg", "Security Officer", "Warden","Detective","Head of Security", "Captain") + required_enemies = list(3,3,3,3,3,2,1,1,0,0) + required_candidates = 5 + weight = 5 + cost = 35 + requirements = list(101,90,90,80,70,60,50,50,50,50) + high_population_requirement = 10 + var/operative_cap = list(2,2,3,3,4,5,5,5,5,5) + var/datum/team/nuclear/nuke_team + flags = HIGHLANDER_RULESET + chaos_min = 4.0 + +/datum/dynamic_ruleset/midround/from_ghosts/nuclear/acceptable(population=0, threat=0) + if (locate(/datum/dynamic_ruleset/roundstart/nuclear) in mode.executed_rules) + return FALSE // Unavailable if nuke ops were already sent at roundstart + var/indice_pop = min(10,round(living_players.len/5)+1) + required_candidates = operative_cap[indice_pop] + return ..() + +/datum/dynamic_ruleset/midround/from_ghosts/nuclear/ready(forced = FALSE) + if (required_candidates > (dead_players.len + list_observers.len)) + return FALSE + return ..() + +/datum/dynamic_ruleset/midround/from_ghosts/nuclear/finish_setup(mob/new_character, index) + new_character.mind.special_role = "Nuclear Operative" + new_character.mind.assigned_role = "Nuclear Operative" + if (index == 1) // Our first guy is the leader + var/datum/antagonist/nukeop/leader/new_role = new + nuke_team = new_role.nuke_team + new_character.mind.add_antag_datum(new_role) + else + return ..() + +////////////////////////////////////////////// +// // +// BLOB (GHOST) // +// // +////////////////////////////////////////////// + +/datum/dynamic_ruleset/midround/from_ghosts/blob + name = "Blob" + antag_datum = /datum/antagonist/blob + antag_flag = ROLE_BLOB + enemy_roles = list("Security Officer", "Detective", "Head of Security", "Captain") + required_enemies = list(2,2,1,1,1,1,1,0,0,0) + required_candidates = 1 + weight = 4 + cost = 10 + requirements = list(101,100,90,80,70,60,50,40,30,20) + high_population_requirement = 50 + repeatable = TRUE + chaos_min = 3.5 + +/datum/dynamic_ruleset/midround/from_ghosts/blob/generate_ruleset_body(mob/applicant) + var/body = applicant.become_overmind() + return body + +////////////////////////////////////////////// +// // +// XENOMORPH (GHOST) // +// // +////////////////////////////////////////////// + +/datum/dynamic_ruleset/midround/from_ghosts/xenomorph + name = "Alien Infestation" + antag_datum = /datum/antagonist/xeno + antag_flag = ROLE_ALIEN + enemy_roles = list("Security Officer", "Detective", "Head of Security", "Captain") + required_enemies = list(2,2,1,1,1,1,1,0,0,0) + required_candidates = 1 + weight = 3 + cost = 10 + requirements = list(101,101,101,80,70,70,60,50,40,30) + high_population_requirement = 50 + repeatable = TRUE + var/list/vents = list() + chaos_min = 3.5 + +/datum/dynamic_ruleset/midround/from_ghosts/xenomorph/execute() + // 50% chance of being incremented by one + required_candidates += prob(50) + for(var/obj/machinery/atmospherics/components/unary/vent_pump/temp_vent in GLOB.machines) + if(QDELETED(temp_vent)) + continue + if(is_station_level(temp_vent.loc.z) && !temp_vent.welded) + var/datum/pipeline/temp_vent_parent = temp_vent.parents[1] + if(!temp_vent_parent) + continue // No parent vent + // Stops Aliens getting stuck in small networks. + // See: Security, Virology + if(temp_vent_parent.other_atmosmch.len > 20) + vents += temp_vent + if(!vents.len) + return FALSE + . = ..() + +/datum/dynamic_ruleset/midround/from_ghosts/xenomorph/generate_ruleset_body(mob/applicant) + var/obj/vent = pick_n_take(vents) + var/mob/living/carbon/alien/larva/new_xeno = new(vent.loc) + new_xeno.key = applicant.key + message_admins("[ADMIN_LOOKUPFLW(new_xeno)] has been made into an alien by the midround ruleset.") + log_game("DYNAMIC: [key_name(new_xeno)] was spawned as an alien by the midround ruleset.") + return new_xeno + +////////////////////////////////////////////// +// // +// NIGHTMARE (GHOST) // +// // +////////////////////////////////////////////// + +/datum/dynamic_ruleset/midround/from_ghosts/nightmare + name = "Nightmare" + antag_datum = /datum/antagonist/nightmare + antag_flag = "Nightmare" + antag_flag_override = ROLE_ALIEN + enemy_roles = list("Security Officer", "Detective", "Head of Security", "Captain") + required_enemies = list(2,2,1,1,1,1,1,0,0,0) + required_candidates = 1 + weight = 3 + cost = 10 + requirements = list(100,90,80,70,50,40,30,20,15,10) + high_population_requirement = 50 + repeatable = TRUE + var/list/spawn_locs = list() + chaos_min = 2.5 + +/datum/dynamic_ruleset/midround/from_ghosts/nightmare/execute() + for(var/X in GLOB.xeno_spawn) + var/turf/T = X + var/light_amount = T.get_lumcount() + if(light_amount < SHADOW_SPECIES_LIGHT_THRESHOLD) + spawn_locs += T + if(!spawn_locs.len) + return FALSE + . = ..() + +/datum/dynamic_ruleset/midround/from_ghosts/nightmare/generate_ruleset_body(mob/applicant) + var/datum/mind/player_mind = new /datum/mind(applicant.key) + player_mind.active = TRUE + + var/mob/living/carbon/human/S = new (pick(spawn_locs)) + player_mind.transfer_to(S) + player_mind.assigned_role = "Nightmare" + player_mind.special_role = "Nightmare" + player_mind.add_antag_datum(/datum/antagonist/nightmare) + S.set_species(/datum/species/shadow/nightmare) + + playsound(S, 'sound/magic/ethereal_exit.ogg', 50, 1, -1) + message_admins("[ADMIN_LOOKUPFLW(S)] has been made into a Nightmare by the midround ruleset.") + log_game("DYNAMIC: [key_name(S)] was spawned as a Nightmare by the midround ruleset.") + return S + +////////////////////////////////////////////// +// // +// ABDUCTORS // +// // +////////////////////////////////////////////// + +/datum/dynamic_ruleset/midround/from_ghosts/abductors + name = "Abductors" + //config_tag = "abductors" + antag_flag = ROLE_ABDUCTOR + // Has two antagonist flags, in fact + enemy_roles = list("AI", "Cyborg", "Security Officer", "Warden","Detective","Head of Security", "Captain") + required_enemies = list(3,3,2,2,1,1,0,0,0,0) + required_candidates = 2 + weight = 4 + cost = 10 + requirements = list(101,101,70,50,40,30,30,30,30,30) + blocking_rules = list(/datum/dynamic_ruleset/roundstart/nuclear,/datum/dynamic_ruleset/midround/from_ghosts/nuclear) + high_population_requirement = 15 + var/datum/team/abductor_team/team + //property_weights = list("extended" = -2, "valid" = 1, "trust" = -1, "chaos" = 2) + repeatable_weight_decrease = 4 + repeatable = TRUE + +/datum/dynamic_ruleset/midround/from_ghosts/abductors/ready(forced = FALSE) + team = new /datum/team/abductor_team + if(team.team_number > ABDUCTOR_MAX_TEAMS) + return FALSE + return ..() + +/datum/dynamic_ruleset/midround/from_ghosts/abductors/finish_setup(mob/new_character, index) + switch(index) + if(1) // yeah this seems like a baffling anti-pattern but it's actually the best way to do this, shit you not + var/mob/living/carbon/human/agent = new_character + agent.mind.add_antag_datum(/datum/antagonist/abductor/agent, team) + log_game("[key_name(agent)] has been selected as [team.name] abductor agent.") + if(2) + var/mob/living/carbon/human/scientist = new_character + scientist.mind.add_antag_datum(/datum/antagonist/abductor/scientist, team) + log_game("[key_name(scientist)] has been selected as [team.name] abductor scientist.") + + +////////////////////////////////////////////// +// // +// SPACE NINJA // +// // +////////////////////////////////////////////// + +/datum/dynamic_ruleset/midround/from_ghosts/ninja + name = "Space Ninja" + //config_tag = "ninja" + antag_flag = ROLE_NINJA + enemy_roles = list("Security Officer","Head of Security","Captain","AI","Cyborg") + required_enemies = list(3,2,2,2,2,1,1,1,1,0) + required_candidates = 1 + weight = 4 + cost = 15 + requirements = list(101,101,101,90,80,70,60,50,40,30) + high_population_requirement = 30 + //property_weights = list("story_potential" = 1, "extended" = -2, "valid" = 2) + var/list/spawn_locs = list() + var/spawn_loc + +/datum/dynamic_ruleset/midround/from_ghosts/ninja/ready(forced = FALSE) + if(!spawn_loc) + var/list/spawn_locs = list() + for(var/obj/effect/landmark/carpspawn/L in GLOB.landmarks_list) + if(isturf(L.loc)) + spawn_locs += L.loc + /* This part breaks. I guess we don't have this landmark. + for(var/obj/effect/landmark/loneopspawn/L in GLOB.landmarks_list) + if(isturf(L.loc)) + spawn_locs += L.loc + */ + if(!spawn_locs.len) + return FALSE + spawn_loc = pick(spawn_locs) + if(!spawn_loc) + return FALSE + return ..() + +/datum/dynamic_ruleset/midround/from_ghosts/ninja/generate_ruleset_body(mob/applicant) + var/key = applicant.key + + //Prepare ninja player mind + var/datum/mind/Mind = new /datum/mind(key) + Mind.assigned_role = ROLE_NINJA + Mind.special_role = ROLE_NINJA + Mind.active = 1 + + //spawn the ninja and assign the candidate + var/mob/living/carbon/human/Ninja = create_space_ninja(spawn_loc) + Mind.transfer_to(Ninja) + var/datum/antagonist/ninja/ninjadatum = new + ninjadatum.helping_station = pick(TRUE,FALSE) + Mind.add_antag_datum(ninjadatum) + + if(Ninja.mind != Mind) //something has gone wrong! + stack_trace("Ninja created with incorrect mind") + + message_admins("[ADMIN_LOOKUPFLW(Ninja)] has been made into a ninja by dynamic.") + log_game("[key_name(Ninja)] was spawned as a ninja by dynamic.") + return Ninja + +/datum/dynamic_ruleset/midround/from_ghosts/ninja/finish_setup(mob/new_character, index) + return + + + + +#undef ABDUCTOR_MAX_TEAMS +#undef REVENANT_SPAWN_THRESHOLD \ No newline at end of file diff --git a/code/game/gamemodes/dynamic/dynamic_rulesets_roundstart.dm b/code/game/gamemodes/dynamic/dynamic_rulesets_roundstart.dm new file mode 100644 index 00000000..66aad39b --- /dev/null +++ b/code/game/gamemodes/dynamic/dynamic_rulesets_roundstart.dm @@ -0,0 +1,762 @@ + +////////////////////////////////////////////// +// // +// SYNDICATE TRAITORS // +// // +////////////////////////////////////////////// + +/datum/dynamic_ruleset/roundstart/traitor + name = "Traitors" + persistent = TRUE + antag_flag = ROLE_TRAITOR + antag_datum = /datum/antagonist/traitor/ + minimum_required_age = 0 + protected_roles = list("Security Officer", "Warden", "Detective", "Head of Security", "Captain", "Head of Personnel", "Chief Engineer", "Chief Medical Officer", "Research Director", "Cyborg") + restricted_roles = list("Cyborg") + required_candidates = 1 + weight = 5 + cost = 10 + requirements = list(101,50,45,40,35,30,25,20,20,20) + high_population_requirement = 10 + var/autotraitor_cooldown = 450 // 15 minutes (ticks once per 2 sec) + chaos_min = 2.5 + +/datum/dynamic_ruleset/roundstart/traitor/thief //The steal only version + name = "Thief Traitors" + persistent = TRUE + antag_flag = ROLE_TRAITOR + antag_datum = /datum/antagonist/traitor/thief + minimum_required_age = 0 + //protected_roles = list("Security Officer", "Warden", "Detective", "Head of Security", "Captain", "Head of Personnel", "Chief Engineer", "Chief Medical Officer", "Research Director", "Cyborg") + restricted_roles = list("Cyborg") + //required_candidates = 1 + //weight = 5 + cost = 0 + requirements = list(40,35,30,25,20,15,15,10,10,5) + high_population_requirement = 10 + //var/autotraitor_cooldown = 450 // 15 minutes (ticks once per 2 sec) + chaos_min = 2.0 + chaos_max = 3.0 //To remove excess tator weight from the spicy rounds + +/datum/dynamic_ruleset/roundstart/traitor/pre_execute() + var/traitor_scaling_coeff = 10 - max(0,round(mode.threat_level/10)-5) // Above 50 threat level, coeff goes down by 1 for every 10 levels + var/num_traitors = min(round(mode.candidates.len / traitor_scaling_coeff) + 1, candidates.len) + for (var/i = 1 to num_traitors) + var/mob/M = pick(candidates) + candidates -= M + assigned += M.mind + M.mind.special_role = ROLE_TRAITOR + M.mind.restricted_roles = restricted_roles + return TRUE + +/* //If things are set up right, this shouldn't be necessary +/datum/dynamic_ruleset/roundstart/traitor/rule_process() + if (autotraitor_cooldown > 0) + autotraitor_cooldown-- + else + autotraitor_cooldown = 450 // 15 minutes + message_admins("Checking if we can turn someone into a traitor.") + log_game("DYNAMIC: Checking if we can turn someone into a traitor.") + mode.picking_specific_rule(/datum/dynamic_ruleset/midround/autotraitor) +*/ + +////////////////////////////////////////// +// // +// BLOOD BROTHERS // +// // +////////////////////////////////////////// + +/datum/dynamic_ruleset/roundstart/traitorbro + name = "Blood Brothers" + antag_flag = ROLE_BROTHER + antag_datum = /datum/antagonist/brother/ + restricted_roles = list("AI", "Cyborg") + protected_roles = list("Security Officer", "Warden", "Detective", "Head of Security", "Captain", "Head of Personnel", "Chief Engineer", "Chief Medical Officer", "Research Director", "Quartermaster") + required_candidates = 2 + weight = 4 + cost = 10 + requirements = list(101,50,50,40,40,20,20,20,20,20) + high_population_requirement = 15 + var/list/datum/team/brother_team/pre_brother_teams = list() + var/const/team_amount = 2 // Hard limit on brother teams if scaling is turned off + var/const/min_team_size = 2 + chaos_min = 2.5 + +/datum/dynamic_ruleset/roundstart/traitorbro/pre_execute() + var/num_teams = team_amount + var/bsc = CONFIG_GET(number/brother_scaling_coeff) + if(bsc) + num_teams = max(1, round(num_players() / bsc)) + + for(var/j = 1 to num_teams) + if(candidates.len < min_team_size || candidates.len < required_candidates) + break + var/datum/team/brother_team/team = new + var/team_size = prob(10) ? min(3, candidates.len) : 2 + for(var/k = 1 to team_size) + var/mob/bro = pick(candidates) + candidates -= bro + assigned += bro.mind + team.add_member(bro.mind) + bro.mind.special_role = "brother" + bro.mind.restricted_roles = restricted_roles + pre_brother_teams += team + return TRUE + +/datum/dynamic_ruleset/roundstart/traitorbro/execute() + for(var/datum/team/brother_team/team in pre_brother_teams) + team.pick_meeting_area() + team.forge_brother_objectives() + for(var/datum/mind/M in team.members) + M.add_antag_datum(/datum/antagonist/brother, team) + team.update_name() + mode.brother_teams += pre_brother_teams + return TRUE + +////////////////////////////////////////////// +// // +// CHANGELINGS // +// // +////////////////////////////////////////////// + +/datum/dynamic_ruleset/roundstart/changeling + name = "Changelings" + antag_flag = ROLE_CHANGELING + antag_datum = /datum/antagonist/changeling + restricted_roles = list("AI", "Cyborg") + protected_roles = list("Security Officer", "Warden", "Detective", "Head of Security", "Captain", "Head of Personnel", "Chief Engineer", "Chief Medical Officer", "Research Director") + required_candidates = 1 + weight = 3 + cost = 30 + requirements = list(101,101,101,50,40,40,30,30,20,10) + high_population_requirement = 10 + var/team_mode_probability = 30 + chaos_min = 3.0 + +/datum/dynamic_ruleset/roundstart/changeling/pre_execute() + var/num_changelings = min(round(mode.candidates.len / 10) + 1, candidates.len) + for (var/i = 1 to num_changelings) + var/mob/M = pick(candidates) + candidates -= M + assigned += M.mind + M.mind.restricted_roles = restricted_roles + M.mind.special_role = ROLE_CHANGELING + return TRUE + +/datum/dynamic_ruleset/roundstart/changeling/execute() + var/team_mode = FALSE + if(prob(team_mode_probability)) + team_mode = TRUE + var/list/team_objectives = subtypesof(/datum/objective/changeling_team_objective) + var/list/possible_team_objectives = list() + for(var/T in team_objectives) + var/datum/objective/changeling_team_objective/CTO = T + if(assigned.len >= initial(CTO.min_lings)) + possible_team_objectives += T + + if(possible_team_objectives.len && prob(20*assigned.len)) + GLOB.changeling_team_objective_type = pick(possible_team_objectives) + for(var/datum/mind/changeling in assigned) + var/datum/antagonist/changeling/new_antag = new antag_datum() + new_antag.team_mode = team_mode + changeling.add_antag_datum(new_antag) + return TRUE + +////////////////////////////////////////////// +// // +// WIZARDS // +// // +////////////////////////////////////////////// + +// Dynamic is a wonderful thing that adds wizards to every round and then adds even more wizards during the round. +/datum/dynamic_ruleset/roundstart/wizard + name = "Wizard" + antag_flag = ROLE_WIZARD + antag_datum = /datum/antagonist/wizard + minimum_required_age = 14 + restricted_roles = list("Head of Security", "Captain") // Just to be sure that a wizard getting picked won't ever imply a Captain or HoS not getting drafted + required_candidates = 1 + weight = 1 + cost = 30 + requirements = list(101,90,70,40,30,20,10,10,10,10) + high_population_requirement = 10 + var/list/roundstart_wizards = list() + chaos_min = 3.5 + +/datum/dynamic_ruleset/roundstart/wizard/acceptable(population=0, threat=0) + if(GLOB.wizardstart.len == 0) + log_admin("Cannot accept Wizard ruleset. Couldn't find any wizard spawn points.") + message_admins("Cannot accept Wizard ruleset. Couldn't find any wizard spawn points.") + return FALSE + return ..() + +/datum/dynamic_ruleset/roundstart/wizard/pre_execute() + if(GLOB.wizardstart.len == 0) + return FALSE + + var/mob/M = pick(candidates) + if (M) + candidates -= M + assigned += M.mind + M.mind.assigned_role = ROLE_WIZARD + M.mind.special_role = ROLE_WIZARD + + return TRUE + +/datum/dynamic_ruleset/roundstart/wizard/execute() + for(var/datum/mind/M in assigned) + M.current.forceMove(pick(GLOB.wizardstart)) + M.add_antag_datum(new antag_datum()) + return TRUE + +////////////////////////////////////////////// +// // +// BLOOD CULT // +// // +////////////////////////////////////////////// + +/datum/dynamic_ruleset/roundstart/bloodcult + name = "Blood Cult" + antag_flag = ROLE_CULTIST + antag_datum = /datum/antagonist/cult + minimum_required_age = 14 + restricted_roles = list("AI", "Cyborg") + protected_roles = list("Security Officer", "Warden", "Detective", "Head of Security", "Captain", "Head of Personnel", "Chief Engineer", "Chief Medical Officer", "Research Director") + required_candidates = 2 + weight = 3 + cost = 30 + requirements = list(101,101,101,95,70,60,60,60,50,50) + high_population_requirement = 10 + pop_per_requirement = 5 + flags = HIGHLANDER_RULESET + var/cultist_cap = list(2,2,2,3,3,4,4,4,4,4) + var/datum/team/cult/main_cult + chaos_min = 4.5 + +/datum/dynamic_ruleset/roundstart/bloodcult/ready(forced = FALSE) + var/indice_pop = min(10,round(mode.roundstart_pop_ready/pop_per_requirement)+1) + required_candidates = cultist_cap[indice_pop] + . = ..() + +/datum/dynamic_ruleset/roundstart/bloodcult/pre_execute() + var/indice_pop = min(10,round(mode.roundstart_pop_ready/pop_per_requirement)+1) + var/cultists = cultist_cap[indice_pop] + for(var/cultists_number = 1 to cultists) + if(candidates.len <= 0) + break + var/mob/M = pick(candidates) + candidates -= M + assigned += M.mind + M.mind.special_role = ROLE_CULTIST + M.mind.restricted_roles = restricted_roles + return TRUE + +/datum/dynamic_ruleset/roundstart/bloodcult/execute() + main_cult = new + for(var/datum/mind/M in assigned) + var/datum/antagonist/cult/new_cultist = new antag_datum() + new_cultist.cult_team = main_cult + new_cultist.give_equipment = TRUE + M.add_antag_datum(new_cultist) + main_cult.setup_objectives() + return TRUE + +/datum/dynamic_ruleset/roundstart/bloodcult/round_result() + ..() + if(main_cult.check_cult_victory()) + SSticker.mode_result = "win - cult win" + SSticker.news_report = CULT_SUMMON + else + SSticker.mode_result = "loss - staff stopped the cult" + SSticker.news_report = CULT_FAILURE + +////////////////////////////////////////////// +// // +// NUCLEAR OPERATIVES // +// // +////////////////////////////////////////////// + +/datum/dynamic_ruleset/roundstart/nuclear + name = "Nuclear Emergency" + antag_flag = ROLE_OPERATIVE + antag_datum = /datum/antagonist/nukeop + var/datum/antagonist/antag_leader_datum = /datum/antagonist/nukeop/leader + minimum_required_age = 14 + restricted_roles = list("Head of Security", "Captain") // Just to be sure that a nukie getting picked won't ever imply a Captain or HoS not getting drafted + required_candidates = 5 + weight = 3 + cost = 40 + requirements = list(101,95,90,85,80,50,40,30,30,20) + high_population_requirement = 10 + pop_per_requirement = 5 + flags = HIGHLANDER_RULESET + var/operative_cap = list(2,2,2,3,3,3,4,4,5,5) + var/datum/team/nuclear/nuke_team + chaos_min = 4.0 + +/datum/dynamic_ruleset/roundstart/nuclear/ready(forced = FALSE) + var/indice_pop = min(10,round(mode.roundstart_pop_ready/pop_per_requirement)+1) + required_candidates = operative_cap[indice_pop] + . = ..() + +/datum/dynamic_ruleset/roundstart/nuclear/pre_execute() + // If ready() did its job, candidates should have 5 or more members in it + + var/indice_pop = min(10,round(mode.roundstart_pop_ready/5)+1) + var/operatives = operative_cap[indice_pop] + for(var/operatives_number = 1 to operatives) + if(candidates.len <= 0) + break + var/mob/M = pick(candidates) + candidates -= M + assigned += M.mind + M.mind.assigned_role = "Nuclear Operative" + M.mind.special_role = "Nuclear Operative" + return TRUE + +/datum/dynamic_ruleset/roundstart/nuclear/execute() + var/leader = TRUE + for(var/datum/mind/M in assigned) + if (leader) + leader = FALSE + var/datum/antagonist/nukeop/leader/new_op = M.add_antag_datum(antag_leader_datum) + nuke_team = new_op.nuke_team + else + var/datum/antagonist/nukeop/new_op = new antag_datum() + M.add_antag_datum(new_op) + return TRUE + +/datum/dynamic_ruleset/roundstart/nuclear/round_result() + var result = nuke_team.get_result() + switch(result) + if(NUKE_RESULT_FLUKE) + SSticker.mode_result = "loss - syndicate nuked - disk secured" + SSticker.news_report = NUKE_SYNDICATE_BASE + if(NUKE_RESULT_NUKE_WIN) + SSticker.mode_result = "win - syndicate nuke" + SSticker.news_report = STATION_NUKED + if(NUKE_RESULT_NOSURVIVORS) + SSticker.mode_result = "halfwin - syndicate nuke - did not evacuate in time" + SSticker.news_report = STATION_NUKED + if(NUKE_RESULT_WRONG_STATION) + SSticker.mode_result = "halfwin - blew wrong station" + SSticker.news_report = NUKE_MISS + if(NUKE_RESULT_WRONG_STATION_DEAD) + SSticker.mode_result = "halfwin - blew wrong station - did not evacuate in time" + SSticker.news_report = NUKE_MISS + if(NUKE_RESULT_CREW_WIN_SYNDIES_DEAD) + SSticker.mode_result = "loss - evacuation - disk secured - syndi team dead" + SSticker.news_report = OPERATIVES_KILLED + if(NUKE_RESULT_CREW_WIN) + SSticker.mode_result = "loss - evacuation - disk secured" + SSticker.news_report = OPERATIVES_KILLED + if(NUKE_RESULT_DISK_LOST) + SSticker.mode_result = "halfwin - evacuation - disk not secured" + SSticker.news_report = OPERATIVE_SKIRMISH + if(NUKE_RESULT_DISK_STOLEN) + SSticker.mode_result = "halfwin - detonation averted" + SSticker.news_report = OPERATIVE_SKIRMISH + else + SSticker.mode_result = "halfwin - interrupted" + SSticker.news_report = OPERATIVE_SKIRMISH + +////////////////////////////////////////////// +// // +// REVS // +// // +////////////////////////////////////////////// + +/datum/dynamic_ruleset/roundstart/delayed/revs + name = "Revolution" + persistent = TRUE + antag_flag = ROLE_REV_HEAD + antag_flag_override = ROLE_REV + antag_datum = /datum/antagonist/rev/head + minimum_required_age = 14 + restricted_roles = list("AI", "Cyborg", "Security Officer", "Warden", "Detective", "Head of Security", "Captain", "Head of Personnel", "Chief Engineer", "Chief Medical Officer", "Research Director") + required_candidates = 3 + weight = 2 + cost = 35 + requirements = list(101,101,101,40,30,20,20,20,20,20) + high_population_requirement = 10 + delay = 5 MINUTES + flags = HIGHLANDER_RULESET + // I give up, just there should be enough heads with 35 players... + minimum_players = 35 + var/datum/team/revolution/revolution + var/finished = 0 + chaos_min = 3.5 + +/datum/dynamic_ruleset/roundstart/delayed/revs/execute() + var/max_canditates = 4 + revolution = new() + for(var/i = 1 to max_canditates) + if(candidates.len <= 0) + break + var/mob/M = pick(candidates) + candidates -= M + assigned += M.mind + M.mind.restricted_roles = restricted_roles + M.mind.special_role = antag_flag + var/datum/antagonist/rev/head/new_head = new antag_datum() + new_head.give_flash = TRUE + new_head.give_hud = TRUE + new_head.remove_clumsy = TRUE + M.mind.add_antag_datum(new_head,revolution) + + revolution.update_objectives() + revolution.update_heads() + SSshuttle.registerHostileEnvironment(src) + + return TRUE + +/datum/dynamic_ruleset/roundstart/delayed/revs/rule_process() + if(check_rev_victory()) + finished = 1 + else if(check_heads_victory()) + finished = 2 + return + +/datum/dynamic_ruleset/roundstart/delayed/revs/check_finished() + if(CONFIG_GET(keyed_list/continuous)["revolution"]) + if(finished) + SSshuttle.clearHostileEnvironment(src) + return ..() + if(finished != 0) + return TRUE + else + return ..() + +/datum/dynamic_ruleset/roundstart/delayed/revs/proc/check_rev_victory() + for(var/datum/objective/mutiny/objective in revolution.objectives) + if(!(objective.check_completion())) + return FALSE + return TRUE + +/datum/dynamic_ruleset/roundstart/delayed/revs/proc/check_heads_victory() + for(var/datum/mind/rev_mind in revolution.head_revolutionaries()) + var/turf/T = get_turf(rev_mind.current) + if(!considered_afk(rev_mind) && considered_alive(rev_mind) && is_station_level(T.z)) + if(ishuman(rev_mind.current) || ismonkey(rev_mind.current)) + return FALSE + return TRUE + +/datum/dynamic_ruleset/roundstart/delayed/revs/round_result() + if(finished == 1) + SSticker.mode_result = "win - heads killed" + SSticker.news_report = REVS_WIN + else if(finished == 2) + SSticker.mode_result = "loss - rev heads killed" + SSticker.news_report = REVS_LOSE + +// Admin only rulesets. The threat requirement is 101 so it is not possible to roll them. + +////////////////////////////////////////////// +// // +// EXTENDED // +// // +////////////////////////////////////////////// + +/datum/dynamic_ruleset/roundstart/extended + name = "Extended" + antag_flag = null + antag_datum = null + restricted_roles = list() + required_candidates = 0 + weight = 3 + cost = 0 + requirements = list(101,101,101,101,101,101,101,101,101,101) + high_population_requirement = 101 + +/datum/dynamic_ruleset/roundstart/extended/pre_execute() + message_admins("Starting a round of extended.") + log_game("Starting a round of extended.") + mode.spend_threat(mode.threat) + return TRUE + +////////////////////////////////////////////// +// // +// CLOCKCULT // +// // +////////////////////////////////////////////// + +/datum/dynamic_ruleset/roundstart/clockcult //Enabled by popular request + name = "Clockcult" + antag_flag = ROLE_SERVANT_OF_RATVAR + antag_datum = /datum/antagonist/clockcult + restricted_roles = list("AI", "Cyborg", "Security Officer", "Warden", "Detective", "Head of Security", "Captain", "Head of Personnel", "Chief Engineer", "Chief Medical Officer", "Research Director") + required_candidates = 4 + weight = 3 + cost = 0 + requirements = list(101,100,90,80,70,60,50,50,40,30) + high_population_requirement = 101 + flags = HIGHLANDER_RULESET + var/ark_time + chaos_min = 4.0 + +/datum/dynamic_ruleset/roundstart/clockcult/pre_execute() + var/list/errorList = list() + var/list/reebes = SSmapping.LoadGroup(errorList, "Reebe", "map_files/generic", "City_of_Cogs.dmm", default_traits = ZTRAITS_REEBE, silent = TRUE) + if(errorList.len) + message_admins("Reebe failed to load!") + log_game("Reebe failed to load!") + return FALSE + for(var/datum/parsed_map/PM in reebes) + PM.initTemplateBounds() + + var/starter_servants = 4 + var/number_players = num_players() + if(number_players > 30) + number_players -= 30 + starter_servants += round(number_players / 10) + starter_servants = min(starter_servants, 8) + for (var/i in 1 to starter_servants) + var/mob/servant = pick(candidates) + candidates -= servant + assigned += servant.mind + servant.mind.assigned_role = ROLE_SERVANT_OF_RATVAR + servant.mind.special_role = ROLE_SERVANT_OF_RATVAR + ark_time = 30 + round((number_players / 5)) + ark_time = min(ark_time, 35) + return TRUE + +/datum/dynamic_ruleset/roundstart/clockcult/execute() + var/list/spread_out_spawns = GLOB.servant_spawns.Copy() + for(var/datum/mind/servant in assigned) + var/mob/S = servant.current + if(!spread_out_spawns.len) + spread_out_spawns = GLOB.servant_spawns.Copy() + log_game("[key_name(servant)] was made an initial servant of Ratvar") + var/turf/T = pick_n_take(spread_out_spawns) + S.forceMove(T) + greet_servant(S) + equip_servant(S) + add_servant_of_ratvar(S, TRUE) + var/obj/structure/destructible/clockwork/massive/celestial_gateway/G = GLOB.ark_of_the_clockwork_justiciar //that's a mouthful + G.final_countdown(ark_time) + return TRUE + +/datum/dynamic_ruleset/roundstart/clockcult/proc/greet_servant(mob/M) //Description of their role + if(!M) + return 0 + to_chat(M, "You are a servant of Ratvar, the Clockwork Justiciar!") + to_chat(M, "You have approximately [ark_time] minutes until the Ark activates.") + to_chat(M, "Unlock Script scripture by converting a new servant.") + to_chat(M, "Application scripture will be unlocked halfway until the Ark's activation.") + M.playsound_local(get_turf(M), 'sound/ambience/antag/clockcultalr.ogg', 100, FALSE, pressure_affected = FALSE) + return 1 + +/datum/dynamic_ruleset/roundstart/clockcult/proc/equip_servant(mob/living/M) //Grants a clockwork slab to the mob, with one of each component + if(!M || !ishuman(M)) + return FALSE + var/mob/living/carbon/human/L = M + L.equipOutfit(/datum/outfit/servant_of_ratvar) + var/obj/item/clockwork/slab/S = new + var/slot = "At your feet" + var/list/slots = list("In your left pocket" = SLOT_L_STORE, "In your right pocket" = SLOT_R_STORE, "In your backpack" = SLOT_IN_BACKPACK, "On your belt" = SLOT_BELT) + if(ishuman(L)) + var/mob/living/carbon/human/H = L + slot = H.equip_in_one_of_slots(S, slots) + if(slot == "In your backpack") + slot = "In your [H.back.name]" + if(slot == "At your feet") + if(!S.forceMove(get_turf(L))) + qdel(S) + if(S && !QDELETED(S)) + to_chat(L, "There is a paper in your backpack! It'll tell you if anything's changed, as well as what to expect.") + to_chat(L, "[slot] is a clockwork slab, a multipurpose tool used to construct machines and invoke ancient words of power. If this is your first time \ + as a servant, you can find a concise tutorial in the Recollection category of its interface.") + to_chat(L, "If you want more information, you can read the wiki page to learn more.") + return TRUE + return FALSE + +/datum/dynamic_ruleset/roundstart/clockcult/round_result() + if(GLOB.clockwork_gateway_activated) + SSticker.news_report = CLOCK_SUMMON + SSticker.mode_result = "win - servants completed their objective (summon ratvar)" + else + SSticker.news_report = CULT_FAILURE + SSticker.mode_result = "loss - servants failed their objective (summon ratvar)" + +////////////////////////////////////////////// +// // +// CLOWN OPS // +// // +////////////////////////////////////////////// + +/datum/dynamic_ruleset/roundstart/nuclear/clown_ops + name = "Clown Ops" + antag_datum = /datum/antagonist/nukeop/clownop + antag_leader_datum = /datum/antagonist/nukeop/leader/clownop + requirements = list(101,101,101,101,101,101,101,101,101,101) + high_population_requirement = 101 + chaos_min = 4.0 + +/datum/dynamic_ruleset/roundstart/nuclear/clown_ops/pre_execute() + . = ..() + if(.) + for(var/obj/machinery/nuclearbomb/syndicate/S in GLOB.nuke_list) + var/turf/T = get_turf(S) + if(T) + qdel(S) + new /obj/machinery/nuclearbomb/syndicate/bananium(T) + for(var/datum/mind/V in assigned) + V.assigned_role = "Clown Operative" + V.special_role = "Clown Operative" + +////////////////////////////////////////////// +// // +// DEVIL // +// // +////////////////////////////////////////////// + +//Best as adminbus +/datum/dynamic_ruleset/roundstart/devil + name = "Devil" + antag_flag = ROLE_DEVIL + antag_datum = /datum/antagonist/devil + restricted_roles = list("Lawyer", "Curator", "Chaplain", "Head of Security", "Captain", "AI") + required_candidates = 1 + weight = 3 + cost = 0 + requirements = list(101,101,101,101,101,101,101,101,101,101) + high_population_requirement = 101 + var/devil_limit = 4 // Hard limit on devils if scaling is turned off + admin_required = TRUE + +/datum/dynamic_ruleset/roundstart/devil/pre_execute() + var/tsc = CONFIG_GET(number/traitor_scaling_coeff) + var/num_devils = 1 + + if(tsc) + num_devils = max(required_candidates, min(round(num_players() / (tsc * 3)) + 2, round(num_players() / (tsc * 1.5)))) + else + num_devils = max(required_candidates, min(num_players(), devil_limit)) + + for(var/j = 0, j < num_devils, j++) + if (!candidates.len) + break + var/mob/devil = pick(candidates) + assigned += devil + candidates -= devil + devil.mind.special_role = ROLE_DEVIL + devil.mind.restricted_roles = restricted_roles + + log_game("[key_name(devil)] has been selected as a devil") + return TRUE + +/datum/dynamic_ruleset/roundstart/devil/execute() + for(var/datum/mind/devil in assigned) + add_devil(devil.current, ascendable = TRUE) + add_devil_objectives(devil,2) + return TRUE + +/datum/dynamic_ruleset/roundstart/devil/proc/add_devil_objectives(datum/mind/devil_mind, quantity) + var/list/validtypes = list(/datum/objective/devil/soulquantity, /datum/objective/devil/soulquality, /datum/objective/devil/sintouch, /datum/objective/devil/buy_target) + var/datum/antagonist/devil/D = devil_mind.has_antag_datum(/datum/antagonist/devil) + for(var/i = 1 to quantity) + var/type = pick(validtypes) + var/datum/objective/devil/objective = new type(null) + objective.owner = devil_mind + D.objectives += objective + if(!istype(objective, /datum/objective/devil/buy_target)) + validtypes -= type + else + objective.find_target() + +////////////////////////////////////////////// +// // +// MONKEY // +// // +////////////////////////////////////////////// + +/datum/dynamic_ruleset/roundstart/monkey + name = "Monkey" + antag_flag = ROLE_MONKEY + antag_datum = /datum/antagonist/monkey/leader + restricted_roles = list("Cyborg", "AI") + required_candidates = 1 + weight = 3 + cost = 0 + requirements = list(101,101,101,101,101,101,101,101,101,101) + high_population_requirement = 101 + var/players_per_carrier = 30 + var/monkeys_to_win = 1 + var/escaped_monkeys = 0 + var/datum/team/monkey/monkey_team + +/datum/dynamic_ruleset/roundstart/monkey/pre_execute() + var/carriers_to_make = max(round(num_players()/players_per_carrier, 1), 1) + + for(var/j = 0, j < carriers_to_make, j++) + if (!candidates.len) + break + var/mob/carrier = pick(candidates) + candidates -= carrier + assigned += carrier.mind + carrier.mind.special_role = "Monkey Leader" + carrier.mind.restricted_roles = restricted_roles + log_game("[key_name(carrier)] has been selected as a Jungle Fever carrier") + return TRUE + +/datum/dynamic_ruleset/roundstart/monkey/execute() + for(var/datum/mind/carrier in assigned) + var/datum/antagonist/monkey/M = add_monkey_leader(carrier) + if(M) + monkey_team = M.monkey_team + return TRUE + +/datum/dynamic_ruleset/roundstart/monkey/proc/check_monkey_victory() + if(SSshuttle.emergency.mode != SHUTTLE_ENDGAME) + return FALSE + var/datum/disease/D = new /datum/disease/transformation/jungle_fever() + for(var/mob/living/carbon/monkey/M in GLOB.alive_mob_list) + if (M.HasDisease(D)) + if(M.onCentCom() || M.onSyndieBase()) + escaped_monkeys++ + if(escaped_monkeys >= monkeys_to_win) + return TRUE + else + return FALSE + +// This does not get called. Look into making it work. +/datum/dynamic_ruleset/roundstart/monkey/round_result() + if(check_monkey_victory()) + SSticker.mode_result = "win - monkey win" + else + SSticker.mode_result = "loss - staff stopped the monkeys" + +////////////////////////////////////////////// +// // +// METEOR // +// // +////////////////////////////////////////////// + +/datum/dynamic_ruleset/roundstart/meteor + name = "Meteor" + persistent = TRUE + required_candidates = 0 + weight = 3 + cost = 0 + requirements = list(101,101,101,101,101,101,101,101,101,101) + high_population_requirement = 101 + var/meteordelay = 2000 + var/nometeors = 0 + var/rampupdelta = 5 + +/datum/dynamic_ruleset/roundstart/meteor/rule_process() + if(nometeors || meteordelay > world.time - SSticker.round_start_time) + return + + var/list/wavetype = GLOB.meteors_normal + var/meteorminutes = (world.time - SSticker.round_start_time - meteordelay) / 10 / 60 + + if (prob(meteorminutes)) + wavetype = GLOB.meteors_threatening + + if (prob(meteorminutes/2)) + wavetype = GLOB.meteors_catastrophic + + var/ramp_up_final = CLAMP(round(meteorminutes/rampupdelta), 1, 10) + + spawn_meteors(ramp_up_final, wavetype) \ No newline at end of file diff --git a/code/game/gamemodes/dynamic/readme.md b/code/game/gamemodes/dynamic/readme.md new file mode 100644 index 00000000..b690e57a --- /dev/null +++ b/code/game/gamemodes/dynamic/readme.md @@ -0,0 +1,57 @@ +# DYNAMIC + +## ROUNDSTART + +Dynamic rolls threat based on a special sauce formula: +"dynamic_curve_width \* tan((3.1416 \* (rand() - 0.5) \* 57.2957795)) + dynamic_curve_centre" + +Latejoin and midround injection cooldowns are set using exponential distribution between +5 minutes and 25 for latejoin +15 minutes and 35 for midround +this value is then added to world.time and assigned to the injection cooldown variables. + +rigged_roundstart() is called instead if there are forced rules (an admin set the mode) + +can_start() -> pre_setup() -> roundstart() OR rigged_roundstart() -> picking_roundstart_rule(drafted_rules) -> post_setup() + +## PROCESS + +Calls rule_process on every rule which is in the current_rules list. +Every sixty seconds, update_playercounts() +Midround injection time is checked against world.time to see if an injection should happen. +If midround injection time is lower than world.time, it updates playercounts again, then tries to inject and generates a new cooldown regardless of whether a rule is picked. + +## LATEJOIN + +make_antag_chance(newPlayer) -> [For each latespawn rule...] +-> acceptable(living players, threat_level) -> trim_candidates() -> ready(forced=FALSE) +**If true, add to drafted rules +**NOTE that acceptable uses threat_level not threat! +**NOTE Latejoin timer is ONLY reset if at least one rule was drafted. +**NOTE the new_player.dm AttemptLateSpawn() calls OnPostSetup for all roles (unless assigned role is MODE) +[After collecting all draftble rules...] +-> picking_latejoin_ruleset(drafted_rules) -> spend threat -> ruleset.execute() +## MIDROUND +process() -> [For each midround rule...] +-> acceptable(living players, threat_level) -> trim_candidates() -> ready(forced=FALSE) +[After collecting all draftble rules...] +-> picking_midround_ruleset(drafted_rules) -> spend threat -> ruleset.execute() +## FORCED +For latejoin, it simply sets forced_latejoin_rule +make_antag_chance(newPlayer) -> trim_candidates() -> ready(forced=TRUE) **NOTE no acceptable() call +For midround, calls the below proc with forced = TRUE +picking_specific_rule(ruletype,forced) -> forced OR acceptable(living_players, threat_level) -> trim_candidates() -> ready(forced) -> spend threat -> execute() +**NOTE specific rule can be called by RS traitor->MR autotraitor w/ forced=FALSE +**NOTE that due to short circuiting acceptable() need not be called if forced. +## RULESET +acceptable(population,threat) just checks if enough threat_level for population indice. +**NOTE that we currently only send threat_level as the second arg, not threat. +ready(forced) checks if enough candidates and calls the map's map_ruleset(dynamic_ruleset) at the parent level +trim_candidates() varies significantly according to the ruleset type +Roundstart: All candidates are new_player mobs. Check them for standard stuff: connected, desire role, not banned, etc. +**NOTE Roundstart deals with both candidates (trimmed list of valid players) and mode.candidates (everyone readied up). Don't confuse them! +Latejoin: Only one candidate, the latejoiner. Standard checks. +Midround: Instead of building a single list candidates, candidates contains four lists: living, dead, observing, and living antags. Standard checks in trim_list(list). +Midround - Rulesets have additional types +/from_ghosts: execute() -> send_applications() -> review_applications() -> finish_setup(mob/newcharacter, index) -> setup_role(role) +**NOTE: execute() here adds dead players and observers to candidates list \ No newline at end of file diff --git a/code/game/gamemodes/game_mode.dm b/code/game/gamemodes/game_mode.dm index b6b5ce3c..8b7c0ed4 100644 --- a/code/game/gamemodes/game_mode.dm +++ b/code/game/gamemodes/game_mode.dm @@ -546,3 +546,7 @@ SSticker.news_report = STATION_EVACUATED if(SSshuttle.emergency.is_hijacked()) SSticker.news_report = SHUTTLE_HIJACK + +/// Mode specific admin panel. +/datum/game_mode/proc/admin_panel() + return \ No newline at end of file diff --git a/code/modules/admin/admin.dm b/code/modules/admin/admin.dm index 5e662993..3d9c9bcd 100644 --- a/code/modules/admin/admin.dm +++ b/code/modules/admin/admin.dm @@ -427,6 +427,25 @@ "} if(GLOB.master_mode == "secret") dat += "(Force Secret Mode)
" + + if(GLOB.master_mode == "dynamic") + if(SSticker.current_state <= GAME_STATE_PREGAME) + dat += "(Force Roundstart Rulesets)
" + if (GLOB.dynamic_forced_roundstart_ruleset.len > 0) + for(var/datum/dynamic_ruleset/roundstart/rule in GLOB.dynamic_forced_roundstart_ruleset) + dat += {"-> [rule.name] <-
"} + dat += "(Clear Rulesets)
" + dat += "(Dynamic mode options)
" + else if (SSticker.IsRoundInProgress()) + dat += "(Force Next Latejoin Ruleset)
" + if (SSticker && SSticker.mode && istype(SSticker.mode,/datum/game_mode/dynamic)) + var/datum/game_mode/dynamic/mode = SSticker.mode + if (mode.forced_latejoin_rule) + dat += {"-> [mode.forced_latejoin_rule.name] <-
"} + dat += "(Execute Midround Ruleset!)
" + dat += "
" + if(SSticker.IsRoundInProgress()) + dat += "(Game Mode Panel)
" dat += {"
@@ -844,6 +863,44 @@ browser.set_content(dat.Join()) browser.open() +/datum/admins/proc/dynamic_mode_options(mob/user) + var/dat = {" +

Dynamic Mode Options


+
+

Common options

+ All these options can be changed midround.
+
+ Force extended: - Option is [GLOB.dynamic_forced_extended ? "ON" : "OFF"]. +
This will force the round to be extended. No rulesets will be drafted.
+
+ No stacking: - Option is [GLOB.dynamic_no_stacking ? "ON" : "OFF"]. +
Unless the threat goes above [GLOB.dynamic_stacking_limit], only one "round-ender" ruleset will be drafted.
+
+ Classic secret mode: - Option is [GLOB.dynamic_classic_secret ? "ON" : "OFF"]. +
Only one roundstart ruleset will be drafted. Only traitors and minor roles will latespawn.
+
+
+ Forced threat level: Current value : [GLOB.dynamic_forced_threat_level]. +
The value threat is set to if it is higher than -1.
+
+ High population limit: Current value : [GLOB.dynamic_high_pop_limit]. +
The threshold at which "high population override" will be in effect.
+
+ Stacking threeshold: Current value : [GLOB.dynamic_stacking_limit]. +
The threshold at which "round-ender" rulesets will stack. A value higher than 100 ensure this never happens.
+

Advanced parameters

+ Curve centre: -> [GLOB.dynamic_curve_centre] <-
+ Curve width: -> [GLOB.dynamic_curve_width] <-
+ Latejoin injection delay:
+ Minimum: -> [GLOB.dynamic_latejoin_delay_min / 60 / 10] <- Minutes
+ Maximum: -> [GLOB.dynamic_latejoin_delay_max / 60 / 10] <- Minutes
+ Midround injection delay:
+ Minimum: -> [GLOB.dynamic_midround_delay_min / 60 / 10] <- Minutes
+ Maximum: -> [GLOB.dynamic_midround_delay_max / 60 / 10] <- Minutes
+ "} + + user << browse(dat, "window=dyn_mode_options;size=900x650") + /datum/admins/proc/create_or_modify_area() set category = "Debug" set name = "Create or modify area" diff --git a/code/modules/admin/topic.dm b/code/modules/admin/topic.dm index 8bcbb306..2ac189e4 100644 --- a/code/modules/admin/topic.dm +++ b/code/modules/admin/topic.dm @@ -299,6 +299,12 @@ else if(href_list["editrights"]) edit_rights_topic(href_list) + else if(href_list["gamemode_panel"]) + if(!check_rights(R_ADMIN)) + return + SSticker.mode.admin_panel() + + else if(href_list["call_shuttle"]) if(!check_rights(R_ADMIN)) return @@ -1350,6 +1356,293 @@ else if(href_list["f_secret"]) return HandleFSecret() + +//Dynamic mode + else if(href_list["f_dynamic_roundstart"]) + if(!check_rights(R_ADMIN)) + return + if(SSticker && SSticker.mode) + return alert(usr, "The game has already started.", null, null, null, null) + if(GLOB.master_mode != "dynamic") + return alert(usr, "The game mode has to be dynamic mode.", null, null, null, null) + var/roundstart_rules = list() + for (var/rule in subtypesof(/datum/dynamic_ruleset/roundstart)) + var/datum/dynamic_ruleset/roundstart/newrule = new rule() + roundstart_rules[newrule.name] = newrule + var/added_rule = input(usr,"What ruleset do you want to force? This will bypass threat level and population restrictions.", "Rigging Roundstart", null) as null|anything in roundstart_rules + if (added_rule) + GLOB.dynamic_forced_roundstart_ruleset += roundstart_rules[added_rule] + log_admin("[key_name(usr)] set [added_rule] to be a forced roundstart ruleset.") + message_admins("[key_name(usr)] set [added_rule] to be a forced roundstart ruleset.", 1) + Game() + + else if(href_list["f_dynamic_roundstart_clear"]) + if(!check_rights(R_ADMIN)) + return + GLOB.dynamic_forced_roundstart_ruleset = list() + Game() + log_admin("[key_name(usr)] cleared the rigged roundstart rulesets. The mode will pick them as normal.") + message_admins("[key_name(usr)] cleared the rigged roundstart rulesets. The mode will pick them as normal.", 1) + + else if(href_list["f_dynamic_roundstart_remove"]) + if(!check_rights(R_ADMIN)) + return + var/datum/dynamic_ruleset/roundstart/rule = locate(href_list["f_dynamic_roundstart_remove"]) + GLOB.dynamic_forced_roundstart_ruleset -= rule + Game() + log_admin("[key_name(usr)] removed [rule] from the forced roundstart rulesets.") + message_admins("[key_name(usr)] removed [rule] from the forced roundstart rulesets.", 1) + + else if(href_list["f_dynamic_latejoin"]) + if(!check_rights(R_ADMIN)) + return + if(!SSticker || !SSticker.mode) + return alert(usr, "The game must start first.", null, null, null, null) + if(GLOB.master_mode != "dynamic") + return alert(usr, "The game mode has to be dynamic mode!", null, null, null, null) + var/latejoin_rules = list() + for (var/rule in subtypesof(/datum/dynamic_ruleset/latejoin)) + var/datum/dynamic_ruleset/latejoin/newrule = new rule() + latejoin_rules[newrule.name] = newrule + var/added_rule = input(usr,"What ruleset do you want to force upon the next latejoiner? This will bypass threat level and population restrictions.", "Rigging Latejoin", null) as null|anything in latejoin_rules + if (added_rule) + var/datum/game_mode/dynamic/mode = SSticker.mode + mode.forced_latejoin_rule = latejoin_rules[added_rule] + log_admin("[key_name(usr)] set [added_rule] to proc on the next latejoin.") + message_admins("[key_name(usr)] set [added_rule] to proc on the next latejoin.", 1) + Game() + + else if(href_list["f_dynamic_latejoin_clear"]) + if(!check_rights(R_ADMIN)) + return + if (SSticker && SSticker.mode && istype(SSticker.mode,/datum/game_mode/dynamic)) + var/datum/game_mode/dynamic/mode = SSticker.mode + mode.forced_latejoin_rule = null + Game() + log_admin("[key_name(usr)] cleared the forced latejoin ruleset.") + message_admins("[key_name(usr)] cleared the forced latejoin ruleset.", 1) + + else if(href_list["f_dynamic_midround"]) + if(!check_rights(R_ADMIN)) + return + if(!SSticker || !SSticker.mode) + return alert(usr, "The game must start first.", null, null, null, null) + if(GLOB.master_mode != "dynamic") + return alert(usr, "The game mode has to be dynamic mode!", null, null, null, null) + var/midround_rules = list() + for (var/rule in subtypesof(/datum/dynamic_ruleset/midround)) + var/datum/dynamic_ruleset/midround/newrule = new rule() + midround_rules[newrule.name] = rule + var/added_rule = input(usr,"What ruleset do you want to force right now? This will bypass threat level and population restrictions.", "Execute Ruleset", null) as null|anything in midround_rules + if (added_rule) + var/datum/game_mode/dynamic/mode = SSticker.mode + log_admin("[key_name(usr)] executed the [added_rule] ruleset.") + message_admins("[key_name(usr)] executed the [added_rule] ruleset.", 1) + mode.picking_specific_rule(midround_rules[added_rule],1) + + else if (href_list["f_dynamic_options"]) + if(!check_rights(R_ADMIN)) + return + + if(SSticker && SSticker.mode) + return alert(usr, "The game has already started.", null, null, null, null) + if(GLOB.master_mode != "dynamic") + return alert(usr, "The game mode has to be dynamic mode!", null, null, null, null) + + dynamic_mode_options(usr) + + else if(href_list["f_dynamic_roundstart_centre"]) + if(!check_rights(R_ADMIN)) + return + if(SSticker && SSticker.mode) + return alert(usr, "The game has already started.", null, null, null, null) + if(GLOB.master_mode != "dynamic") + return alert(usr, "The game mode has to be dynamic mode!", null, null, null, null) + + var/new_centre = input(usr,"Change the centre of the dynamic mode threat curve. A negative value will give a more peaceful round ; a positive value, a round with higher threat. Any number between -5 and +5 is allowed.", "Change curve centre", null) as num + if (new_centre < -5 || new_centre > 5) + return alert(usr, "Only values between -5 and +5 are allowed.", null, null, null, null) + + log_admin("[key_name(usr)] changed the distribution curve center to [new_centre].") + message_admins("[key_name(usr)] changed the distribution curve center to [new_centre]", 1) + GLOB.dynamic_curve_centre = new_centre + dynamic_mode_options(usr) + + else if(href_list["f_dynamic_roundstart_width"]) + if(!check_rights(R_ADMIN)) + return + if(SSticker && SSticker.mode) + return alert(usr, "The game has already started.", null, null, null, null) + if(GLOB.master_mode != "dynamic") + return alert(usr, "The game mode has to be dynamic mode!", null, null, null, null) + + var/new_width = input(usr,"Change the width of the dynamic mode threat curve. A higher value will favour extreme rounds ; a lower value, a round closer to the average. Any Number between 0.5 and 4 are allowed.", "Change curve width", null) as num + if (new_width < 0.5 || new_width > 4) + return alert(usr, "Only values between 0.5 and +2.5 are allowed.", null, null, null, null) + + log_admin("[key_name(usr)] changed the distribution curve width to [new_width].") + message_admins("[key_name(usr)] changed the distribution curve width to [new_width]", 1) + GLOB.dynamic_curve_width = new_width + dynamic_mode_options(usr) + + else if(href_list["f_dynamic_roundstart_latejoin_min"]) + if(!check_rights(R_ADMIN)) + return + if(SSticker && SSticker.mode) + return alert(usr, "The game has already started.", null, null, null, null) + if(GLOB.master_mode != "dynamic") + return alert(usr, "The game mode has to be dynamic mode!", null, null, null, null) + var/new_min = input(usr,"Change the minimum delay of latejoin injection in minutes.", "Change latejoin injection delay minimum", null) as num + if(new_min <= 0) + return alert(usr, "The minimum can't be zero or lower.", null, null, null, null) + if((new_min MINUTES) > GLOB.dynamic_latejoin_delay_max) + return alert(usr, "The minimum must be lower than the maximum.", null, null, null, null) + + log_admin("[key_name(usr)] changed the latejoin injection minimum delay to [new_min] minutes.") + message_admins("[key_name(usr)] changed the latejoin injection minimum delay to [new_min] minutes", 1) + GLOB.dynamic_latejoin_delay_min = (new_min MINUTES) + dynamic_mode_options(usr) + + else if(href_list["f_dynamic_roundstart_latejoin_max"]) + if(!check_rights(R_ADMIN)) + return + if(SSticker && SSticker.mode) + return alert(usr, "The game has already started.", null, null, null, null) + if(GLOB.master_mode != "dynamic") + return alert(usr, "The game mode has to be dynamic mode!", null, null, null, null) + var/new_max = input(usr,"Change the maximum delay of latejoin injection in minutes.", "Change latejoin injection delay maximum", null) as num + if(new_max <= 0) + return alert(usr, "The maximum can't be zero or lower.", null, null, null, null) + if((new_max MINUTES) < GLOB.dynamic_latejoin_delay_min) + return alert(usr, "The maximum must be higher than the minimum.", null, null, null, null) + + log_admin("[key_name(usr)] changed the latejoin injection maximum delay to [new_max] minutes.") + message_admins("[key_name(usr)] changed the latejoin injection maximum delay to [new_max] minutes", 1) + GLOB.dynamic_latejoin_delay_max = (new_max MINUTES) + dynamic_mode_options(usr) + + else if(href_list["f_dynamic_roundstart_midround_min"]) + if(!check_rights(R_ADMIN)) + return + if(SSticker && SSticker.mode) + return alert(usr, "The game has already started.", null, null, null, null) + if(GLOB.master_mode != "dynamic") + return alert(usr, "The game mode has to be dynamic mode!", null, null, null, null) + var/new_min = input(usr,"Change the minimum delay of midround injection in minutes.", "Change midround injection delay minimum", null) as num + if(new_min <= 0) + return alert(usr, "The minimum can't be zero or lower.", null, null, null, null) + if((new_min MINUTES) > GLOB.dynamic_midround_delay_max) + return alert(usr, "The minimum must be lower than the maximum.", null, null, null, null) + + log_admin("[key_name(usr)] changed the midround injection minimum delay to [new_min] minutes.") + message_admins("[key_name(usr)] changed the midround injection minimum delay to [new_min] minutes", 1) + GLOB.dynamic_midround_delay_min = (new_min MINUTES) + dynamic_mode_options(usr) + + else if(href_list["f_dynamic_roundstart_midround_max"]) + if(!check_rights(R_ADMIN)) + return + if(SSticker && SSticker.mode) + return alert(usr, "The game has already started.", null, null, null, null) + if(GLOB.master_mode != "dynamic") + return alert(usr, "The game mode has to be dynamic mode!", null, null, null, null) + var/new_max = input(usr,"Change the maximum delay of midround injection in minutes.", "Change midround injection delay maximum", null) as num + if(new_max <= 0) + return alert(usr, "The maximum can't be zero or lower.", null, null, null, null) + if((new_max MINUTES) > GLOB.dynamic_midround_delay_max) + return alert(usr, "The maximum must be higher than the minimum.", null, null, null, null) + + log_admin("[key_name(usr)] changed the midround injection maximum delay to [new_max] minutes.") + message_admins("[key_name(usr)] changed the midround injection maximum delay to [new_max] minutes", 1) + GLOB.dynamic_midround_delay_max = (new_max MINUTES) + dynamic_mode_options(usr) + + else if(href_list["f_dynamic_force_extended"]) + if(!check_rights(R_ADMIN)) + return + + if(GLOB.master_mode != "dynamic") + return alert(usr, "The game mode has to be dynamic mode!", null, null, null, null) + + GLOB.dynamic_forced_extended = !GLOB.dynamic_forced_extended + log_admin("[key_name(usr)] set 'forced_extended' to [GLOB.dynamic_forced_extended].") + message_admins("[key_name(usr)] set 'forced_extended' to [GLOB.dynamic_forced_extended].") + dynamic_mode_options(usr) + + else if(href_list["f_dynamic_no_stacking"]) + if(!check_rights(R_ADMIN)) + return + + if(GLOB.master_mode != "dynamic") + return alert(usr, "The game mode has to be dynamic mode!", null, null, null, null) + + GLOB.dynamic_no_stacking = !GLOB.dynamic_no_stacking + log_admin("[key_name(usr)] set 'no_stacking' to [GLOB.dynamic_no_stacking].") + message_admins("[key_name(usr)] set 'no_stacking' to [GLOB.dynamic_no_stacking].") + dynamic_mode_options(usr) + + else if(href_list["f_dynamic_classic_secret"]) + if(!check_rights(R_ADMIN)) + return + + if(GLOB.master_mode != "dynamic") + return alert(usr, "The game mode has to be dynamic mode!", null, null, null, null) + + GLOB.dynamic_classic_secret = !GLOB.dynamic_classic_secret + log_admin("[key_name(usr)] set 'classic_secret' to [GLOB.dynamic_classic_secret].") + message_admins("[key_name(usr)] set 'classic_secret' to [GLOB.dynamic_classic_secret].") + dynamic_mode_options(usr) + + else if(href_list["f_dynamic_stacking_limit"]) + if(!check_rights(R_ADMIN)) + return + + if(GLOB.master_mode != "dynamic") + return alert(usr, "The game mode has to be dynamic mode!", null, null, null, null) + + GLOB.dynamic_stacking_limit = input(usr,"Change the threat limit at which round-endings rulesets will start to stack.", "Change stacking limit", null) as num + log_admin("[key_name(usr)] set 'stacking_limit' to [GLOB.dynamic_stacking_limit].") + message_admins("[key_name(usr)] set 'stacking_limit' to [GLOB.dynamic_stacking_limit].") + dynamic_mode_options(usr) + + else if(href_list["f_dynamic_high_pop_limit"]) + if(!check_rights(R_ADMIN)) + return + + if(SSticker && SSticker.mode) + return alert(usr, "The game has already started.", null, null, null, null) + + if(GLOB.master_mode != "dynamic") + return alert(usr, "The game mode has to be dynamic mode!", null, null, null, null) + + var/new_value = input(usr, "Enter the high-pop override threshold for dynamic mode.", "High pop override") as num + if (new_value < 0) + return alert(usr, "Only positive values allowed!", null, null, null, null) + GLOB.dynamic_high_pop_limit = new_value + + log_admin("[key_name(usr)] set 'high_pop_limit' to [GLOB.dynamic_high_pop_limit].") + message_admins("[key_name(usr)] set 'high_pop_limit' to [GLOB.dynamic_high_pop_limit].") + dynamic_mode_options(usr) + + else if(href_list["f_dynamic_forced_threat"]) + if(!check_rights(R_ADMIN)) + return + + if(SSticker && SSticker.mode) + return alert(usr, "The game has already started.", null, null, null, null) + + if(GLOB.master_mode != "dynamic") + return alert(usr, "The game mode has to be dynamic mode!", null, null, null, null) + + var/new_value = input(usr, "Enter the forced threat level for dynamic mode.", "Forced threat level") as num + if (new_value > 100) + return alert(usr, "The value must be be under 100.", null, null, null, null) + GLOB.dynamic_forced_threat_level = new_value + + log_admin("[key_name(usr)] set 'forced_threat_level' to [GLOB.dynamic_forced_threat_level].") + message_admins("[key_name(usr)] set 'forced_threat_level' to [GLOB.dynamic_forced_threat_level].") + dynamic_mode_options(usr) +//End Dynamic mode else if(href_list["c_mode2"]) if(!check_rights(R_ADMIN|R_SERVER)) diff --git a/hyperstation/code/gamemode/traitor_thief.dm b/hyperstation/code/gamemode/traitor_thief.dm new file mode 100644 index 00000000..b54fa714 --- /dev/null +++ b/hyperstation/code/gamemode/traitor_thief.dm @@ -0,0 +1,146 @@ +//Jay Sparrow +//A less murder traitor for low chaos dynamic rounds. + +///datum/game_mode +// var/list/datum/mind/traitor = list() + + + +///datum/mind/ + //var/sexed = FALSE //General flag for completion check + +//TODO - Move this somewhere else +//GLOBAL_LIST_INIT(hyper_special_roles, list( + //ROLE_LEWD_TRAITOR = /datum/game_mode/traitor/lewd +//)) + + +/datum/game_mode/traitor/thief + name = "thief traitor" + config_tag = "thief traitor" + antag_flag = ROLE_TRAITOR + false_report_weight = 20 //Reports of traitors are pretty common. + restricted_jobs = list("AI","Cyborg") + protected_jobs = list("Security Officer", "Warden", "Detective", "Head of Security", "Captain", "Head of Personnel", "Chief Engineer", "Chief Medical Officer", "Research Director", "Quartermaster") + required_players = 0 + required_enemies = 0 + + announce_span = "danger" + announce_text = "There are enemy agents on the station!\n\ + Traitors: Accomplish your objectives.\n\ + Crew: Do not let the traitors succeed!" + + traitors_possible = 3 //hard limit on traitors if scaling is turned off //Let it be high for now. Testing phase. + num_modifier = 0 // Used for gamemodes, that are a child of traitor, that need more than the usual. + antag_datum = /datum/antagonist/traitor/thief //what type of antag to create + +/datum/antagonist/traitor/thief + name = "Thief Traitor" + roundend_category = "traitors" + antagpanel_category = "Traitor" + job_rank = ROLE_TRAITOR + antag_moodlet = /datum/mood_event/focused //No special moodlet yet + special_role = ROLE_TRAITOR + employer = "The Syndicate" + give_objectives = TRUE + should_give_codewords = TRUE + should_equip = TRUE + traitor_kind = TRAITOR_HUMAN //Set on initial assignment + can_hijack = HIJACK_NEUTRAL + + +/datum/game_mode/traitor/thief/generate_report() + return "Although more specific threats are commonplace, you should always remain vigilant for Syndicate agents aboard your station. Syndicate communications have implied that many \ + Nanotrasen employees are Syndicate agents with hidden memories that may be activated at a moment's notice, so it's possible that these agents might not even know their positions." + +/datum/mind/proc/make_ThiefTraitor() + if(!(has_antag_datum(/datum/antagonist/traitor/thief))) + add_antag_datum(/datum/antagonist/traitor/thief) + +/* +/datum/admins/proc/makeLewdtraitors() + to_chat(GLOB.admins, "makeLewd_traitors called") + var/datum/game_mode/traitor/lewd/temp = new + + //if(CONFIG_GET(flag/protect_roles_from_antagonist)) + //continue + + //if(CONFIG_GET(flag/protect_assistant_from_antagonist)) + //continue + + var/list/mob/living/carbon/human/candidates = list() + var/mob/living/carbon/human/H = null + var/list/mob/living/carbon/human/targets = list() + //var/mob/living/carbon/human/T = null + + for(var/mob/living/carbon/human/applicant in GLOB.player_list) + if(isReadytoRumble(applicant, ROLE_LEWD_TRAITOR)) + if(temp.age_check(applicant.client)) + if(!(applicant.job in temp.restricted_jobs)) + candidates += applicant + + for(var/mob/living/carbon/human/target in GLOB.player_list) + if(target.client.prefs.noncon) + if(!(target.job in temp.restricted_jobs)) + targets += target + + if(candidates.len) + var/numTraitors = min(candidates.len, targets.len, 1) //This number affects the maximum number of traitors. We want 1 for right now. + if(numTraitors == 0) + to_chat(GLOB.admins, "No lewd traitors created. Are there any valid targets?") + return 0 + for(var/i = 0, iYou are the [owner.special_role].") + owner.announce_objectives() + if(should_give_codewords) + give_codewords() + +/datum/antagonist/traitor/thief/on_gain() + if(owner.current && isAI(owner.current)) + traitor_kind = TRAITOR_AI + + SSticker.mode.traitors += owner + owner.special_role = special_role + if(give_objectives) + forge_objectives() + finalize_traitor() diff --git a/tgstation.dme b/tgstation.dme index f4fb06be..8fa80ec2 100644 --- a/tgstation.dme +++ b/tgstation.dme @@ -560,6 +560,12 @@ #include "code\game\gamemodes\devil\game_mode.dm" #include "code\game\gamemodes\devil\objectives.dm" #include "code\game\gamemodes\devil\devil agent\devil_agent.dm" +#include "code\game\gamemodes\dynamic\dynamic.dm" +#include "code\game\gamemodes\dynamic\dynamic_rulesets.dm" +#include "code\game\gamemodes\dynamic\dynamic_rulesets_events.dm" +#include "code\game\gamemodes\dynamic\dynamic_rulesets_latejoin.dm" +#include "code\game\gamemodes\dynamic\dynamic_rulesets_midround.dm" +#include "code\game\gamemodes\dynamic\dynamic_rulesets_roundstart.dm" #include "code\game\gamemodes\extended\extended.dm" #include "code\game\gamemodes\meteor\meteor.dm" #include "code\game\gamemodes\meteor\meteors.dm" @@ -2897,6 +2903,7 @@ #include "code\modules\zombie\items.dm" #include "code\modules\zombie\organs.dm" #include "hyperstation\code\gamemode\traitor_lewd.dm" +#include "hyperstation\code\gamemode\traitor_thief.dm" #include "hyperstation\code\gamemode\werewolf\werewolf.dm" #include "hyperstation\code\mobs\hugbot.dm" #include "hyperstation\code\mobs\mimic.dm"