/datum/dynamic_ruleset/roundstart // We can pick multiple of a roundstart ruleset to "scale up" (spawn more of the same type of antag) // Set this to FALSE if you DON'T want this ruleset to "scale up" repeatable = TRUE /// If TRUE, the ruleset will be the only one selected for roundstart var/solo = FALSE /datum/dynamic_ruleset/roundstart/is_valid_candidate(mob/candidate, client/candidate_client) if(isnull(candidate.mind)) return FALSE // Checks that any other roundstart ruleset hasn't already picked this guy for(var/datum/dynamic_ruleset/roundstart/ruleset as anything in SSdynamic.queued_rulesets) if(candidate.mind in ruleset.selected_minds) return FALSE return ..() /// Helpful proc - to use if your ruleset forces a job - which ensures a candidate can play the passed job typepath /datum/dynamic_ruleset/roundstart/proc/ruleset_forced_job_check(mob/candidate, client/candidate_client, datum/job/job_typepath) // Malf AI can only go to people who want to be AI if(!candidate_client.prefs.job_preferences[job_typepath::title]) return FALSE // And only to people who can actually be AI this round if(SSjob.check_job_eligibility(candidate, SSjob.get_job_type(job_typepath), "[name] Candidacy") != JOB_AVAILABLE) return FALSE // (Something else forced us to play a job that isn't AI) var/forced_job = LAZYACCESS(SSjob.forced_occupations, candidate) if(forced_job && forced_job != job_typepath) return FALSE // (Something else forced us NOT to play AI) if(job_typepath::title in LAZYACCESS(SSjob.prevented_occupations, candidate)) return FALSE return TRUE /datum/dynamic_ruleset/roundstart/traitor name = "Traitors" config_tag = "Roundstart Traitor" preview_antag_datum = /datum/antagonist/traitor pref_flag = ROLE_TRAITOR weight = 10 min_pop = 3 max_antag_cap = list("denominator" = 24) /datum/dynamic_ruleset/roundstart/traitor/assign_role(datum/mind/candidate) candidate.add_antag_datum(/datum/antagonist/traitor) /datum/dynamic_ruleset/roundstart/malf_ai name = "Malfunctioning AI" config_tag = "Roundstart Malfunctioning AI" pref_flag = ROLE_MALF preview_antag_datum = /datum/antagonist/malf_ai ruleset_flags = RULESET_HIGH_IMPACT weight = alist( DYNAMIC_TIER_LOW = 0, DYNAMIC_TIER_LOWMEDIUM = 1, DYNAMIC_TIER_MEDIUMHIGH = 3, DYNAMIC_TIER_HIGH = 3, ) min_pop = 30 max_antag_cap = 1 repeatable = FALSE /datum/dynamic_ruleset/roundstart/malf_ai/get_always_blacklisted_roles() return list() /datum/dynamic_ruleset/roundstart/malf_ai/is_valid_candidate(mob/candidate, client/candidate_client) return ..() && ruleset_forced_job_check(candidate, candidate_client, /datum/job/ai) /datum/dynamic_ruleset/roundstart/malf_ai/prepare_for_role(datum/mind/candidate) LAZYSET(SSjob.forced_occupations, candidate, /datum/job/ai) /datum/dynamic_ruleset/roundstart/malf_ai/assign_role(datum/mind/candidate) candidate.add_antag_datum(/datum/antagonist/malf_ai) /datum/dynamic_ruleset/roundstart/malf_ai/can_be_selected() return ..() && !HAS_TRAIT(SSstation, STATION_TRAIT_HUMAN_AI) /datum/dynamic_ruleset/roundstart/blood_brother name = "Blood Brothers" config_tag = "Roundstart Blood Brothers" preview_antag_datum = /datum/antagonist/brother pref_flag = ROLE_BROTHER weight = 5 max_antag_cap = list("denominator" = 29) min_pop = 10 /datum/dynamic_ruleset/roundstart/blood_brother/assign_role(datum/mind/candidate) candidate.add_antag_datum(/datum/antagonist/brother) /datum/dynamic_ruleset/roundstart/changeling name = "Changelings" config_tag = "Roundstart Changeling" preview_antag_datum = /datum/antagonist/changeling pref_flag = ROLE_CHANGELING weight = 3 min_pop = 15 max_antag_cap = list("denominator" = 29) /datum/dynamic_ruleset/roundstart/changeling/assign_role(datum/mind/candidate) candidate.add_antag_datum(/datum/antagonist/changeling) // Quick thing about this event, it turns out roundstart blood worms, despite being granted a host, are worse at antaggery in practice. // They have their static, so unlike blood worms who spawn in the middle of the round, they just kinda tend to do nothing. So having at least 2 is ideal. // This way, if one ends up being a fluke, the other can hopefully pick up the slack. It's a semi-rare event, so having a massive chance to just fizzle out isn't ideal. /datum/dynamic_ruleset/roundstart/blood_worm name = "Blood Worms" config_tag = "Roundstart Blood Worm" preview_antag_datum = /datum/antagonist/blood_worm pref_flag = ROLE_BLOOD_WORM weight = 1 min_pop = 10 // Resource limited, spawning on lowpop is fine. max_antag_cap = list("denominator" = 29) // +1 blood worm per 30 pop. repeatable = FALSE // Yeah no. /datum/dynamic_ruleset/roundstart/blood_worm/is_valid_candidate(mob/living/candidate, client/candidate_client) if (!..()) return FALSE var/species_type = candidate_client.prefs.read_preference(/datum/preference/choiced/species) var/datum/species/species = GLOB.species_prototypes[species_type] return !(TRAIT_NOBLOOD in species.inherent_traits) /datum/dynamic_ruleset/roundstart/blood_worm/assign_role(datum/mind/candidate) if (!CAN_HAVE_BLOOD(candidate.current)) CRASH("A roundstart blood worm tried to spawn into a candidate mob with no blood. This shouldn't happen, because we already checked for TRAIT_NOBLOOD in species traits.") var/mob/living/carbon/human/host = candidate.current var/mob/living/basic/blood_worm/hatchling/worm = new(get_turf(host)) candidate.transfer_to(worm) candidate.add_antag_datum(/datum/antagonist/blood_worm/infestation) worm.enter_host(host, silent = TRUE, gain_progress = FALSE) /datum/dynamic_ruleset/roundstart/heretic name = "Heretics" config_tag = "Roundstart Heretics" preview_antag_datum = /datum/antagonist/heretic pref_flag = ROLE_HERETIC weight = 3 max_antag_cap = list("denominator" = 24) min_pop = 30 // Ensures good spread of sacrifice targets /datum/dynamic_ruleset/roundstart/heretic/assign_role(datum/mind/candidate) candidate.add_antag_datum(/datum/antagonist/heretic) /datum/dynamic_ruleset/roundstart/wizard name = "Wizard" config_tag = "Roundstart Wizard" preview_antag_datum = /datum/antagonist/wizard pref_flag = ROLE_WIZARD ruleset_flags = RULESET_INVADER|RULESET_HIGH_IMPACT weight = alist( DYNAMIC_TIER_LOW = 0, DYNAMIC_TIER_LOWMEDIUM = 0, DYNAMIC_TIER_MEDIUMHIGH = 1, DYNAMIC_TIER_HIGH = 2, ) max_antag_cap = 1 min_pop = 30 ruleset_lazy_templates = list(LAZY_TEMPLATE_KEY_WIZARDDEN) repeatable = FALSE /datum/dynamic_ruleset/roundstart/wizard/prepare_for_role(datum/mind/candidate) LAZYSET(SSjob.forced_occupations, candidate, /datum/job/space_wizard) /datum/dynamic_ruleset/roundstart/wizard/assign_role(datum/mind/candidate) candidate.add_antag_datum(/datum/antagonist/wizard) // moves to lair for us /datum/dynamic_ruleset/roundstart/wizard/round_result() for(var/datum/mind/wiz as anything in selected_minds) if(considered_alive(wiz) && !considered_exiled(wiz)) return FALSE SSticker.news_report = WIZARD_KILLED return TRUE /datum/dynamic_ruleset/roundstart/blood_cult name = "Blood Cult" config_tag = "Roundstart Blood Cult" preview_antag_datum = /datum/antagonist/cult pref_flag = ROLE_CULTIST ruleset_flags = RULESET_HIGH_IMPACT weight = alist( DYNAMIC_TIER_LOW = 0, DYNAMIC_TIER_LOWMEDIUM = 1, DYNAMIC_TIER_MEDIUMHIGH = 3, DYNAMIC_TIER_HIGH = 3, ) min_pop = 30 blacklisted_roles = list( JOB_HEAD_OF_PERSONNEL, ) min_antag_cap = list("denominator" = 20, "offset" = 1) repeatable = FALSE /// Ratio of cultists getting on the shuttle to be considered a minor win var/ratio_to_be_considered_escaped = 0.5 /datum/dynamic_ruleset/roundstart/blood_cult/get_always_blacklisted_roles() return ..() | JOB_CHAPLAIN /datum/dynamic_ruleset/roundstart/blood_cult/create_execute_args() return list( new /datum/team/cult(), get_most_experienced(selected_minds, pref_flag), ) /datum/dynamic_ruleset/roundstart/blood_cult/execute() . = ..() // future todo, find a cleaner way to get this from execute args var/datum/team/cult/main_cult = locate() in GLOB.antagonist_teams main_cult.setup_objectives() /datum/dynamic_ruleset/roundstart/blood_cult/assign_role(datum/mind/candidate, datum/team/cult/cult, datum/mind/most_experienced) var/datum/antagonist/cult/cultist = new() cultist.give_equipment = TRUE candidate.add_antag_datum(cultist, cult) if(most_experienced == candidate) cultist.make_cult_leader() /datum/dynamic_ruleset/roundstart/blood_cult/round_result() var/datum/team/cult/main_cult = locate() in GLOB.antagonist_teams if(main_cult.check_cult_victory()) SSticker.mode_result = "win - cult win" SSticker.news_report = CULT_SUMMON return TRUE var/num_cultists = main_cult.size_at_maximum || 100 var/ratio_to_be_considered_escaped = 0.5 var/escaped_cultists = 0 for(var/datum/mind/escapee as anything in main_cult.members) if(considered_escaped(escapee)) escaped_cultists++ SSticker.mode_result = "loss - staff stopped the cult" SSticker.news_report = (escaped_cultists / num_cultists) >= ratio_to_be_considered_escaped ? CULT_ESCAPE : CULT_FAILURE return TRUE /datum/dynamic_ruleset/roundstart/nukies name = "Nuclear Operatives" config_tag = "Roundstart Nukeops" preview_antag_datum = /datum/antagonist/nukeop pref_flag = ROLE_OPERATIVE ruleset_flags = RULESET_INVADER|RULESET_HIGH_IMPACT weight = alist( DYNAMIC_TIER_LOW = 0, DYNAMIC_TIER_LOWMEDIUM = 1, DYNAMIC_TIER_MEDIUMHIGH = 3, DYNAMIC_TIER_HIGH = 3, ) min_pop = 30 min_antag_cap = list("denominator" = 18, "offset" = 1) ruleset_lazy_templates = list(LAZY_TEMPLATE_KEY_NUKIEELEVATOR) repeatable = FALSE /datum/dynamic_ruleset/roundstart/nukies/prepare_for_role(datum/mind/candidate) // they all get the normal operative job, even the leader. (leader's job is updated when they get the antag datum) // all this ultimately matters for is 1. ensuring they *don't* get a normal job and 2. spawning them in the elevator LAZYSET(SSjob.forced_occupations, candidate, /datum/job/nuclear_operative) /datum/dynamic_ruleset/roundstart/nukies/execute() . = ..() addtimer(CALLBACK(src, PROC_REF(load_nukie_base)), 3 SECONDS) #define ELEVATOR_WIDTH 6 #define ELEVATOR_HEIGHT 5 /datum/dynamic_ruleset/roundstart/nukies/proc/load_nukie_base() SSmapping.lazy_load_template(LAZY_TEMPLATE_KEY_NUKIEBASE) addtimer(CALLBACK(src, PROC_REF(teleport_elevator_contents)), 2 SECONDS) addtimer(CALLBACK(src, PROC_REF(open_nukie_elevator)), 3 SECONDS) addtimer(CALLBACK(src, PROC_REF(call_infiltrator)), 5 SECONDS) /datum/dynamic_ruleset/roundstart/nukies/proc/teleport_elevator_contents() var/obj/effect/landmark/nukeop_elevator/interior/interior = locate() in GLOB.landmarks_list var/obj/effect/landmark/nukeop_elevator/exterior/exterior = locate() in GLOB.landmarks_list if(isnull(interior) || isnull(exterior)) stack_trace("Failed to find nukie elevator landmarks during load!") message_admins("Failed to find nukie elevator landmarks during load, you might have some stuck nukies that need help.") return var/turf/top_right_interior = get_turf(interior) var/turf/top_right_exterior = get_turf(exterior) var/list/turf/elevator_turfs = block( top_right_interior.x, top_right_interior.y, top_right_interior.z, top_right_interior.x - ELEVATOR_WIDTH + 1, top_right_interior.y - ELEVATOR_HEIGHT + 1, top_right_interior.z, ) for(var/turf/elevator_turf as anything in elevator_turfs) var/turf/destination_turf = locate( top_right_exterior.x - (top_right_interior.x - elevator_turf.x), top_right_exterior.y - (top_right_interior.y - elevator_turf.y), top_right_exterior.z, ) if(isnull(destination_turf)) stack_trace("Failed to find destination turf for nukie elevator!") message_admins("Failed to find corresponding elevator turf for nuke ops, you might have some stuck nukies that need help.") continue for(var/atom/movable/thing in elevator_turf) if(thing.anchored || thing.orbit_target) continue thing.forceMove(destination_turf) /datum/dynamic_ruleset/roundstart/nukies/proc/open_nukie_elevator() for(var/obj/machinery/door/poddoor/shutter in SSmachines.get_machines_by_type_and_subtypes(/obj/machinery/door/poddoor)) if(shutter.id_tag == "nukiespawnlift") shutter.open() /datum/dynamic_ruleset/roundstart/nukies/proc/call_infiltrator() for(var/datum/mind/leader_mind as anything in selected_minds) var/datum/antagonist/nukeop/leader/nuke_leader = leader_mind.has_antag_datum(/datum/antagonist/nukeop/leader) nuke_leader?.spawn_infiltrator() #undef ELEVATOR_WIDTH #undef ELEVATOR_HEIGHT /datum/dynamic_ruleset/roundstart/nukies/create_execute_args() return list( new /datum/team/nuclear(), get_most_experienced(selected_minds, pref_flag), ) /datum/dynamic_ruleset/roundstart/nukies/assign_role(datum/mind/candidate, datum/team/nuke_team, datum/mind/most_experienced) if(most_experienced == candidate) var/datum/antagonist/nukeop/leader/nuke_leader = new() nuke_leader.send_to_spawnpoint = FALSE // because they get spawned in the elevator nuke_leader.spawn_ship = FALSE // spawned manually later candidate.add_antag_datum(nuke_leader, nuke_team) else var/datum/antagonist/nukeop/nukie = new() nukie.send_to_spawnpoint = FALSE // ditto candidate.add_antag_datum(nukie, nuke_team) /datum/dynamic_ruleset/roundstart/nukies/round_result() var/datum/antagonist/nukeop/nukie = selected_minds[1].has_antag_datum(/datum/antagonist/nukeop) var/datum/team/nuclear/nuke_team = nukie.get_team() 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_DESTROYED_NUKE if(NUKE_RESULT_NOSURVIVORS) SSticker.mode_result = "halfwin - syndicate nuke - did not evacuate in time" SSticker.news_report = STATION_DESTROYED_NUKE 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 /datum/dynamic_ruleset/roundstart/nukies/clown name = "Clown Operatives" config_tag = "Roundstart Clownops" preview_antag_datum = /datum/antagonist/nukeop/clownop pref_flag = ROLE_CLOWN_OPERATIVE weight = 0 /datum/dynamic_ruleset/roundstart/nukies/clown/prepare_for_role(datum/mind/candidate) LAZYSET(SSjob.forced_occupations, candidate, /datum/job/nuclear_operative/clown_operative) /datum/dynamic_ruleset/roundstart/nukies/clown/assign_role(datum/mind/candidate, datum/team/nuke_team, datum/mind/most_experienced) if(most_experienced == candidate) candidate.add_antag_datum(/datum/antagonist/nukeop/leader/clownop) else candidate.add_antag_datum(/datum/antagonist/nukeop/clownop) /datum/dynamic_ruleset/roundstart/revolution name = "Revolution" config_tag = "Roundstart Revolution" preview_antag_datum = /datum/antagonist/rev/head pref_flag = ROLE_REV_HEAD ruleset_flags = RULESET_HIGH_IMPACT weight = alist( DYNAMIC_TIER_LOW = 0, DYNAMIC_TIER_LOWMEDIUM = 1, DYNAMIC_TIER_MEDIUMHIGH = 3, DYNAMIC_TIER_HIGH = 3, ) min_pop = 30 min_antag_cap = 1 max_antag_cap = 3 repeatable = FALSE /// If we have fewer heads of staff than this 7 minutes into the round, we'll cancel the revolution var/heads_necessary = 2 /datum/dynamic_ruleset/roundstart/revolution/get_always_blacklisted_roles() . = ..() for(var/datum/job/job as anything in SSjob.all_occupations) if(job.job_flags & JOB_HEAD_OF_STAFF) . |= job.title /datum/dynamic_ruleset/roundstart/revolution/assign_role(datum/mind/candidate) LAZYADD(candidate.special_roles, "Dormant Head Revolutionary") addtimer(CALLBACK(src, PROC_REF(reveal_head), candidate), 7 MINUTES, TIMER_DELETE_ME) /// Reveals the headrev after a set amount of time /datum/dynamic_ruleset/roundstart/revolution/proc/reveal_head(datum/mind/candidate) LAZYREMOVE(candidate.special_roles, "Dormant Head Revolutionary") var/head_check = 0 for(var/mob/player as anything in get_active_player_list(alive_check = TRUE, afk_check = TRUE)) if(player.mind?.assigned_role.job_flags & JOB_HEAD_OF_STAFF) head_check++ if(head_check < heads_necessary) log_dynamic("[config_tag]: Not enough heads of staff were present to start a revolution.") addtimer(CALLBACK(src, PROC_REF(revs_execution_failed)), 1 MINUTES, TIMER_UNIQUE|TIMER_DELETE_ME) return if(!can_be_headrev(candidate, TRUE)) log_dynamic("[config_tag]: [key_name(candidate)] was not eligible to be a headrev after the timer expired - finding a replacement.") find_another_headrev() return GLOB.revolution_handler ||= new() var/datum/antagonist/rev/head/new_head = new() new_head.give_flash = TRUE new_head.give_hud = TRUE new_head.remove_clumsy = TRUE candidate.add_antag_datum(new_head, GLOB.revolution_handler.revs) GLOB.revolution_handler.start_revolution() /datum/dynamic_ruleset/roundstart/revolution/proc/find_another_headrev() for(var/mob/living/carbon/human/upstanding_citizen in GLOB.player_list) if(!can_be_headrev(upstanding_citizen.mind, TRUE)) continue reveal_head(upstanding_citizen.mind) log_dynamic("[config_tag]: [key_name(upstanding_citizen)] was selected as a replacement headrev.") return log_dynamic("[config_tag]: Failed to find a replacement headrev.") addtimer(CALLBACK(src, PROC_REF(revs_execution_failed)), 1 MINUTES, TIMER_UNIQUE|TIMER_DELETE_ME) /datum/dynamic_ruleset/roundstart/revolution/proc/revs_execution_failed() if(GLOB.revolution_handler) return // Execution is effectively cancelled by this point, but it's not like we can go back and refund it SSdynamic.unreported_rulesets += src name += " (Canceled)" log_dynamic("[config_tag]: All headrevs were ineligible after the timer expired, and no replacements could be found. Ruleset canceled.") message_admins("[config_tag]: All headrevs were ineligible after the timer expired, and no replacements could be found. Ruleset canceled.") /datum/dynamic_ruleset/roundstart/spies name = "Spies" config_tag = "Roundstart Spies" preview_antag_datum = /datum/antagonist/spy pref_flag = ROLE_SPY weight = alist( DYNAMIC_TIER_LOW = 0, DYNAMIC_TIER_LOWMEDIUM = 1, DYNAMIC_TIER_MEDIUMHIGH = 3, DYNAMIC_TIER_HIGH = 3, ) min_pop = 10 min_antag_cap = list("denominator" = 20, "offset" = 1) /datum/dynamic_ruleset/roundstart/spies/assign_role(datum/mind/candidate) candidate.add_antag_datum(/datum/antagonist/spy) /datum/dynamic_ruleset/roundstart/extended name = "Extended" config_tag = "Extended" weight = 0 min_antag_cap = 0 repeatable = FALSE solo = TRUE /datum/dynamic_ruleset/roundstart/extended/execute() // No midrounds no latejoins for(var/category in SSdynamic.rulesets_to_spawn) SSdynamic.rulesets_to_spawn[category] = 0 /datum/dynamic_ruleset/roundstart/meteor name = "Meteor" config_tag = "Meteor" weight = 0 min_antag_cap = 0 repeatable = FALSE /datum/dynamic_ruleset/roundstart/meteor/execute() GLOB.meteor_mode ||= new() GLOB.meteor_mode.start_meteor() /datum/dynamic_ruleset/roundstart/nations name = "Nations" config_tag = "Nations" weight = 0 min_antag_cap = 0 repeatable = FALSE solo = TRUE /datum/dynamic_ruleset/roundstart/nations/execute() // No midrounds no latejoins for(var/category in SSdynamic.rulesets_to_spawn) SSdynamic.rulesets_to_spawn[category] = 0 //notably assistant is not in this list to prevent the round turning into BARBARISM instantly, and silicon is in this list for UN var/list/department_types = list( /datum/job_department/silicon, //united nations /datum/job_department/cargo, /datum/job_department/engineering, /datum/job_department/medical, /datum/job_department/science, /datum/job_department/security, /datum/job_department/service, ) for(var/department_type in department_types) create_separatist_nation(department_type, announcement = FALSE, dangerous = FALSE, message_admins = FALSE) GLOB.round_default_lawset = /datum/ai_laws/united_nations