mirror of
https://github.com/Bubberstation/Bubberstation.git
synced 2026-08-19 19:18:50 +01:00
## About The Previous Pull Request #85308 reverted by #85929  ~~Causes the round to not start when a player isn't eligible for any jobs at a specific priority level due to runtimes trying to `pick()` from an empty list aborting the entire job assignment stack.~~ (Fixed???? by https://github.com/tgstation/tgstation/pull/85947/commits/e0e9f2f430079d4ab7097abe12e75f934131a638) Maybe we should test merge this for a mo just to make sure no more cheeky runtimes pop up before merging. ## About The Pull Request This PR does a couple of minor things: Makes the job debug logging a bit easier to follow. Minorly brings some SSjob code up to code standards, converting proc names to snake_case and doing some otherm is cleanup. Refactored some stuff into different procs, updated some comments. And some major things: Changes the job assignment logic. Old behaviour > Assign dynamic priority roles > Force one Head of Staff (if possible) > Assign all AIs > Assign overflow roles (bugged in 2 ways) > Shuffle the available jobs list once, at the start of the random job assignment loop > Pick and assign random jobs for random players from High prefs down, with a priority on Head of Staff roles > Handle everyone that couldn't be assigned a random job New behaviour > Assign dynamic priority roles > Assign all Head of Staff roles to players with High prefs > If no Head of Staff was made in the above way, force one Head of Staff (if possible) > Assign all AIs > Assign overflow roles (fixed) > Prioritise and fill unfilled head roles at each job priority pref level, from High prefs down. > Build a list of all jobs that each unassigned player could be eligible for at the above pref level. > Pick a job from that list at random and assign it to the player. > Handle everyone that couldn't be assigned a random job. In reality there should be little impact on overall job assignment, the code changes read more as semantics. For example, the priority check for filling Head slots will have the same candidate pool in both old and new versions, but in the new version we're more clearly saying that Heads are important and we want to prioritise filling them for the sake of round progression even though the outcome in new and old is the same. A key change will lead to an increase in assistants - Overflow fixes. Currently the code block to do early assignments to the Overflow role doesn't work - or works but not as you'd expect. The idea was is that because enabling the Overflow role in the prefs menu is an On/Off toggle that sets the job to High priority when enabled and prevents any other High priority pref, players that have the Overflow role enabled will **always** get it. It's their highest priority job with infinite slots. So we do a pass right at the start to give everyone with the Overflow role enabled that role and save us wasting time later on in random job code giving them that same role but with more work. The problem is the code for this only assigns the Overflow role to people with it set to Low priority in their prefs, resulting in log readouts like: ``` [2024-07-27 09:49:43.469] DEBUG-JOB: DO, Running Overflow Check 1 [2024-07-27 09:49:43.469] DEBUG-JOB: Running FOC, Job: /datum/job/assistant, Level: Low Priority [2024-07-27 09:49:43.472] DEBUG-JOB: FOC player job enabled at wrong level, Player: Radioprague, TheirLevel: Medium Priority, ReqLevel: Low Priority [2024-07-27 09:49:43.472] DEBUG-JOB: FOC player job enabled at wrong level, Player: Caluan, TheirLevel: High Priority, ReqLevel: Low Priority [2024-07-27 09:49:43.473] DEBUG-JOB: FOC player job enabled at wrong level, Player: Caractaser, TheirLevel: High Priority, ReqLevel: Low Priority [2024-07-27 09:49:43.473] DEBUG-JOB: FOC player job enabled at wrong level, Player: Apsua, TheirLevel: High Priority, ReqLevel: Low Priority [2024-07-27 09:49:43.475] DEBUG-JOB: FOC player job enabled at wrong level, Player: Bebrus2, TheirLevel: Medium Priority, ReqLevel: Low Priority [2024-07-27 09:49:43.475] DEBUG-JOB: AC1, Candidates: 0 ``` Where nobody gets pre-assigned the overflow role because their prefs are all set to the High priority from being toggled... Except wait a second, some people have it at Medium priority when it should just be a No Role/High Priority Role toggle? And herein we meet a problem. My hypothesis is that traits and stuff that change the overflow have allowed players to set the "ordinary" overflow role of Assistant to Medium and/or Low priority. This still shows as enabled in the prefs menu, but leads to an outcome where a player with assistant enabled is assigned Cook instead. ``` [2024-07-27 09:49:47.775] DEBUG-JOB: DO, Running Overflow Check 1 [2024-07-27 09:49:47.775] DEBUG-JOB: Running FOC, Job: /datum/job/assistant, Level: Low Priority ... [2024-07-27 09:49:43.475] DEBUG-JOB: FOC player job enabled at wrong level, Player: Bebrus2, TheirLevel: Medium Priority, ReqLevel: Low Priority ... [2024-07-27 09:49:47.987] DEBUG-JOB: Running AR, Player: Bebrus2, Job: /datum/job/cook, LateJoin: 0 ``` So players with the Overflow job pref set to Low (an unexpected state, should be disabled or High) would be guaranteed to get that role if none of the higher priority Head of Staff/AI/Dynamic roles took over via the bugged "force overflow for people with the pref enabled" proc. Players with the Overflow job pref set to High would be guaranteed to get that role if none of the higher priority Head of Staff/AI/Dynamic roles took over via the random job assignment code giving them their Highest priority role thanks to the infinite job slots of the Overflow. And players with the Overflow job pref set to Medium (an unexpected state, should be disabled or High) would get Assistant if the shuffle step of the available jobs list put Assisstant before any of the other jobs they had prefs enabled for at Medium that weren't already filled, otherwise they'd get another random job. This code is now changed to ignore the priority the player has set when looking for people to fill the overflow role. As long as it **is** enabled, the player will get it unless they're forced into a dynamic ruleset role (AI when malf rolls) or a Head of Staff role due to their other prefs (they have RD set to med or low, and no other player has a Head of Staff at high so they get randomly picked and miss the overflow role). This will increase the number of assistants in shifts where their pref state has Assisstant in the bugged Medium priority, but doesn't change it for bugged Low and not-bugged High/On priority. On the other side of the coin, we have how the random jobs are picked. They're kinda not random, and I noticed this reading the logs then reading the code. The list of available jobs to pick from is randomly shuffled - but only **once**. All players pull from a list of jobs in the same order. So you end up with a log block like this: ``` [2024-07-27 09:49:47.985] DEBUG-JOB: DO pass, Player: Pierow, Level:3, Job:Botanist [2024-07-27 09:49:47.985] DEBUG-JOB: Running AR, Player: Pierow, Job: /datum/job/botanist, LateJoin: 0 [2024-07-27 09:49:47.985] DEBUG-JOB: Player: Pierow is now Rank: Botanist, JCP:0, JPL:2 [2024-07-27 09:49:47.986] DEBUG-JOB: DO pass, Player: Daddos, Level:3, Job:Botanist [2024-07-27 09:49:47.986] DEBUG-JOB: Running AR, Player: Daddos, Job: /datum/job/botanist, LateJoin: 0 [2024-07-27 09:49:47.986] DEBUG-JOB: Player: Daddos is now Rank: Botanist, JCP:1, JPL:2 [2024-07-27 09:49:47.986] DEBUG-JOB: FOC job filled and not overflow, Player: Bebrus2, Job: /datum/job/botanist, Current: 2, Limit: 2 [2024-07-27 09:49:47.987] DEBUG-JOB: FOC player job not enabled, Player: Bebrus2 [2024-07-27 09:49:47.987] DEBUG-JOB: DO pass, Player: Bebrus2, Level:3, Job:Cook [2024-07-27 09:49:47.987] DEBUG-JOB: Running AR, Player: Bebrus2, Job: /datum/job/cook, LateJoin: 0 [2024-07-27 09:49:47.988] DEBUG-JOB: Player: Bebrus2 is now Rank: Cook, JCP:0, JPL:1 [2024-07-27 09:49:47.988] DEBUG-JOB: FOC player job not enabled, Player: Redwizz [2024-07-27 09:49:47.988] DEBUG-JOB: FOC job filled and not overflow, Player: Redwizz, Job: /datum/job/cook, Current: 1, Limit: 1 ``` The list is shuffled into an order of something like `list("Scientist", "Botanist", "Cook", "Sec Officer", ...)` then iterated over for each player. So every random job selection goes: > "Does Player1 have Scientist enabled and at the right priority? No? Okay, Botanist? Yes? You get botanist." > "Does Player2 have Scientist enabled and at the right priority? No? Okay, Botanist? Yes? You get botanist." > "Does Player3 have Scientist enabled and at the right priority? No? Okay, Botanist has no slots left so we'll remove it from the list. Okay, Cook? Yes? You get cook." > "Does Player4 have Scientist enabled and at the right priority? No? Okay, Cook has no slots left so we'll remove it from the list. Okay, Sec Officer? ..." This can lead to stacked individual departments if it gets randomly rolled to the start of the list in the shuffle, and completely empty departments if they end up at the end. On high pop shifts this is probably less of an issue. Player prefs add noise to this and as departments at the front fill up, those at the back pick up some of the lower pref players. But have you ever had a shift where there's just like... No fucking sec even though there's tons of players? The logging (before I made changes in this PR) was a bit ass, but my hypothesis there is that sec officer was shuffled right at the end of the random job list, so every other department was filled up before sec officers were picked. To mitigate this, I made the list shuffle every single time the game picks a random available job for the player. This should lead to a more balanced selection of available jobs by avoiding situations where the code is biased towards packing some departments by accident. ## Why It's Good For The Game Overflow fixes mean people who go to their prefs and see the Overflow Role is On will all have the same experience - They will be the Overflow role. More random random job selection should prevent individual departments having a jobs be stacked when it would have otherwise been possible for a more balanced selection but the code unintentially biased random departments to be overstaffed and understaffed each shift. ## Changelog 🆑 fix: Having the Overflow Role set to On will properly ensure you get that role at a High priority as intended by the game code. fix: Job selection is now a little bit more random. Fixes an unintentional bias in random job assignment that could lead to feast-or-famine for roles where everyone is assigned one job and nobody is assigned another job. /🆑
1002 lines
37 KiB
Plaintext
1002 lines
37 KiB
Plaintext
/// Probability the AI going malf will be accompanied by an ion storm announcement and some ion laws.
|
|
#define MALF_ION_PROB 33
|
|
/// The probability to replace an existing law with an ion law instead of adding a new ion law.
|
|
#define REPLACE_LAW_WITH_ION_PROB 10
|
|
|
|
/// Midround Rulesets
|
|
/datum/dynamic_ruleset/midround // Can be drafted once in a while during a round
|
|
ruletype = MIDROUND_RULESET
|
|
var/midround_ruleset_style
|
|
/// If the ruleset should be restricted from ghost roles.
|
|
var/restrict_ghost_roles = TRUE
|
|
/// What mob 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()
|
|
|
|
/// The minimum round time before this ruleset will show up
|
|
var/minimum_round_time = 0
|
|
/// Abstract root value
|
|
var/abstract_type = /datum/dynamic_ruleset/midround
|
|
|
|
/datum/dynamic_ruleset/midround/forget_startup()
|
|
living_players = list()
|
|
living_antags = list()
|
|
dead_players = list()
|
|
list_observers = list()
|
|
return ..()
|
|
|
|
/datum/dynamic_ruleset/midround/from_ghosts
|
|
weight = 0
|
|
required_type = /mob/dead/observer
|
|
abstract_type = /datum/dynamic_ruleset/midround/from_ghosts
|
|
/// Whether the ruleset should call generate_ruleset_body or not.
|
|
var/makeBody = TRUE
|
|
/// The rule needs this many applicants to be properly executed.
|
|
var/required_applicants = 1
|
|
///Path of an item to show up in ghost polls for applicants to sign up.
|
|
var/signup_item_path = /obj/structure/sign/poster/contraband/syndicate_recruitment
|
|
|
|
/datum/dynamic_ruleset/midround/from_ghosts/check_candidates()
|
|
var/dead_count = dead_players.len + list_observers.len
|
|
if (required_candidates <= dead_count)
|
|
return TRUE
|
|
|
|
log_dynamic("FAIL: [src], a from_ghosts ruleset, did not have enough dead candidates: [required_candidates] needed, [dead_count] found")
|
|
|
|
return FALSE
|
|
|
|
/datum/dynamic_ruleset/midround/trim_candidates()
|
|
living_players = trim_list(GLOB.alive_player_list)
|
|
living_antags = trim_list(GLOB.current_living_antags)
|
|
dead_players = trim_list(GLOB.dead_player_list)
|
|
list_observers = trim_list(GLOB.current_observers_list)
|
|
|
|
/datum/dynamic_ruleset/midround/proc/trim_list(list/to_trim = list())
|
|
var/list/trimmed_list = to_trim.Copy()
|
|
for(var/mob/creature in trimmed_list)
|
|
if (!istype(creature, required_type))
|
|
trimmed_list.Remove(creature)
|
|
continue
|
|
if (isnull(creature.client)) // Are they connected?
|
|
trimmed_list.Remove(creature)
|
|
continue
|
|
if(creature.client.get_remaining_days(minimum_required_age) > 0)
|
|
trimmed_list.Remove(creature)
|
|
continue
|
|
if (!((antag_preference || antag_flag) in creature.client.prefs.be_special))
|
|
trimmed_list.Remove(creature)
|
|
continue
|
|
if (is_banned_from(creature.ckey, list(antag_flag_override || antag_flag, ROLE_SYNDICATE)))
|
|
trimmed_list.Remove(creature)
|
|
continue
|
|
if (isnull(creature.mind))
|
|
continue
|
|
if (restrict_ghost_roles && !(creature.mind.assigned_role.job_flags & JOB_CREW_MEMBER)) // Are they not playing a station role?
|
|
trimmed_list.Remove(creature)
|
|
continue
|
|
if (creature.mind.assigned_role.title in restricted_roles) // Does their job allow it?
|
|
trimmed_list.Remove(creature)
|
|
continue
|
|
if (length(exclusive_roles) && !(creature.mind.assigned_role.title in exclusive_roles)) // Is the rule exclusive to their job?
|
|
trimmed_list.Remove(creature)
|
|
continue
|
|
if(HAS_TRAIT(creature, TRAIT_MIND_TEMPORARILY_GONE)) // are they out of body?
|
|
trimmed_list.Remove(creature)
|
|
continue
|
|
if(HAS_TRAIT(creature, TRAIT_TEMPORARY_BODY)) // are they an avatar?
|
|
trimmed_list.Remove(creature)
|
|
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
|
|
// subtype your midround with /from_ghosts or /from_living to get candidate checking. Or check yourself by subtyping from neither
|
|
/datum/dynamic_ruleset/midround/ready(forced = FALSE)
|
|
if (forced)
|
|
return TRUE
|
|
|
|
var/job_check = 0
|
|
if (enemy_roles.len > 0)
|
|
for (var/mob/M in GLOB.alive_player_list)
|
|
if (M.stat == DEAD || !M.client)
|
|
continue // Dead/disconnected players cannot count as opponents
|
|
if (M.mind && (M.mind.assigned_role.title in enemy_roles) && (!(M in candidates) || (M.mind.assigned_role.title 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(SSdynamic.threat_level/10)
|
|
var/ruleset_forced = (GLOB.dynamic_forced_rulesets[type] || RULESET_NOT_FORCED) == RULESET_FORCE_ENABLED
|
|
if (!ruleset_forced && job_check < required_enemies[threat])
|
|
log_dynamic("FAIL: [src] is not ready, because there are not enough enemies: [required_enemies[threat]] needed, [job_check] found")
|
|
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
|
|
|
|
SSdynamic.log_dynamic_and_announce("Polling [possible_volunteers.len] players to apply for the [name] ruleset.")
|
|
candidates = SSpolling.poll_ghost_candidates(
|
|
question = "Looking for volunteers to become [span_notice(antag_flag)] for [span_danger(name)]",
|
|
check_jobban = antag_flag_override,
|
|
role = antag_flag || antag_flag_override,
|
|
poll_time = 30 SECONDS,
|
|
alert_pic = signup_item_path,
|
|
role_name_text = antag_flag,
|
|
)
|
|
|
|
if(!candidates || candidates.len <= 0)
|
|
SSdynamic.log_dynamic_and_announce("The ruleset [name] received no applications.")
|
|
SSdynamic.executed_rules -= src
|
|
attempt_replacement()
|
|
return
|
|
|
|
SSdynamic.log_dynamic_and_announce("[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()
|
|
if(candidates.len < required_applicants)
|
|
SSdynamic.executed_rules -= src
|
|
return
|
|
for (var/i = 1, i <= required_candidates, i++)
|
|
if(candidates.len <= 0)
|
|
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
|
|
assigned += applicant
|
|
finish_applications()
|
|
|
|
/// Here the accepted applications get generated bodies and their setup is finished.
|
|
/// Called by review_applications()
|
|
/datum/dynamic_ruleset/midround/from_ghosts/proc/finish_applications()
|
|
var/i = 0
|
|
for(var/mob/applicant as anything in assigned)
|
|
i++
|
|
var/mob/new_character = applicant
|
|
if(makeBody)
|
|
new_character = generate_ruleset_body(applicant)
|
|
SEND_GLOBAL_SIGNAL(COMSIG_RULESET_BODY_GENERATED_FROM_GHOSTS, applicant)
|
|
finish_setup(new_character, i)
|
|
notify_ghosts(
|
|
"[applicant.name] has been picked for the ruleset [name]!",
|
|
source = new_character,
|
|
)
|
|
|
|
/datum/dynamic_ruleset/midround/from_ghosts/proc/generate_ruleset_body(mob/applicant)
|
|
var/mob/living/carbon/human/new_character = make_body(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
|
|
|
|
/// Fired when there are no valid candidates. Will spawn a sleeper agent or latejoin traitor.
|
|
/datum/dynamic_ruleset/midround/from_ghosts/proc/attempt_replacement()
|
|
var/datum/dynamic_ruleset/midround/from_living/autotraitor/sleeper_agent = new
|
|
|
|
SSdynamic.configure_ruleset(sleeper_agent)
|
|
|
|
if (!SSdynamic.picking_specific_rule(sleeper_agent))
|
|
return
|
|
|
|
SSdynamic.picking_specific_rule(/datum/dynamic_ruleset/latejoin/infiltrator)
|
|
|
|
///subtype to handle checking players
|
|
/datum/dynamic_ruleset/midround/from_living
|
|
weight = 0
|
|
abstract_type = /datum/dynamic_ruleset/midround/from_living
|
|
|
|
/datum/dynamic_ruleset/midround/from_living/ready(forced)
|
|
if(!check_candidates())
|
|
return FALSE
|
|
return ..()
|
|
|
|
|
|
/// Midround Traitor Ruleset (From Living)
|
|
/datum/dynamic_ruleset/midround/from_living/autotraitor
|
|
name = "Syndicate Sleeper Agent"
|
|
midround_ruleset_style = MIDROUND_RULESET_STYLE_LIGHT
|
|
antag_datum = /datum/antagonist/traitor/infiltrator/sleeper_agent
|
|
antag_flag = ROLE_SLEEPER_AGENT
|
|
antag_flag_override = ROLE_TRAITOR
|
|
protected_roles = list(
|
|
JOB_CAPTAIN,
|
|
JOB_DETECTIVE,
|
|
JOB_HEAD_OF_PERSONNEL,
|
|
JOB_HEAD_OF_SECURITY,
|
|
JOB_PRISONER,
|
|
JOB_SECURITY_OFFICER,
|
|
JOB_WARDEN,
|
|
)
|
|
restricted_roles = list(
|
|
JOB_AI,
|
|
JOB_CYBORG,
|
|
ROLE_POSITRONIC_BRAIN,
|
|
)
|
|
required_candidates = 1
|
|
weight = 35
|
|
cost = 3
|
|
requirements = list(3,3,3,3,3,3,3,3,3,3)
|
|
repeatable = TRUE
|
|
|
|
/datum/dynamic_ruleset/midround/from_living/autotraitor/trim_candidates()
|
|
..()
|
|
candidates = living_players
|
|
for(var/mob/living/player in candidates)
|
|
if(issilicon(player)) // Your assigned role doesn't change when you are turned into a silicon.
|
|
candidates -= player
|
|
else if(is_centcom_level(player.z))
|
|
candidates -= player // We don't autotator people in CentCom
|
|
else if(player.mind && (player.mind.special_role || player.mind.antag_datums?.len > 0))
|
|
candidates -= player // We don't autotator people with roles already
|
|
|
|
/datum/dynamic_ruleset/midround/from_living/autotraitor/execute()
|
|
var/mob/M = pick(candidates)
|
|
assigned += M
|
|
candidates -= M
|
|
var/datum/antagonist/traitor/infiltrator/sleeper_agent/newTraitor = new
|
|
M.mind.add_antag_datum(newTraitor)
|
|
message_admins("[ADMIN_LOOKUPFLW(M)] was selected by the [name] ruleset and has been made into a midround traitor.")
|
|
log_dynamic("[key_name(M)] was selected by the [name] ruleset and has been made into a midround traitor.")
|
|
return TRUE
|
|
|
|
/// Midround Malf AI Ruleset (From Living)
|
|
/datum/dynamic_ruleset/midround/malf
|
|
name = "Malfunctioning AI"
|
|
midround_ruleset_style = MIDROUND_RULESET_STYLE_HEAVY
|
|
antag_datum = /datum/antagonist/malf_ai
|
|
antag_flag = ROLE_MALF_MIDROUND
|
|
antag_flag_override = ROLE_MALF
|
|
enemy_roles = list(
|
|
JOB_CHEMIST,
|
|
JOB_CHIEF_ENGINEER,
|
|
JOB_HEAD_OF_SECURITY,
|
|
JOB_RESEARCH_DIRECTOR,
|
|
JOB_SCIENTIST,
|
|
JOB_SECURITY_OFFICER,
|
|
JOB_WARDEN,
|
|
)
|
|
exclusive_roles = list(JOB_AI)
|
|
required_enemies = list(4,4,4,4,4,4,2,2,2,0)
|
|
required_candidates = 1
|
|
minimum_players = 25
|
|
weight = 2
|
|
cost = 10
|
|
required_type = /mob/living/silicon/ai
|
|
blocking_rules = list(/datum/dynamic_ruleset/roundstart/malf_ai)
|
|
// AIs are technically considered "Ghost roles" as far as candidate selection are concerned
|
|
// So we need to allow it here. We filter of actual ghost role AIs (charlie) via trim_candidates ourselves
|
|
restrict_ghost_roles = FALSE
|
|
|
|
/datum/dynamic_ruleset/midround/malf/trim_candidates()
|
|
..()
|
|
candidates = list()
|
|
for(var/mob/living/silicon/ai/player in living_players)
|
|
if(!is_station_level(player.z))
|
|
continue
|
|
if(isnull(player.mind))
|
|
continue
|
|
if(player.mind.special_role || length(player.mind.antag_datums))
|
|
continue
|
|
candidates += player
|
|
|
|
/datum/dynamic_ruleset/midround/malf/ready(forced)
|
|
if(!check_candidates())
|
|
log_dynamic("FAIL: No valid AI found for the Malfunctioning AI ruleset.")
|
|
return FALSE
|
|
return ..()
|
|
|
|
/datum/dynamic_ruleset/midround/malf/execute()
|
|
var/mob/living/silicon/ai/new_malf_ai = pick_n_take(candidates)
|
|
assigned += new_malf_ai.mind
|
|
var/datum/antagonist/malf_ai/malf_antag_datum = new
|
|
new_malf_ai.mind.special_role = antag_flag
|
|
new_malf_ai.mind.add_antag_datum(malf_antag_datum)
|
|
if(prob(MALF_ION_PROB))
|
|
priority_announce("Ion storm detected near the station. Please check all AI-controlled equipment for errors.", "Anomaly Alert", ANNOUNCER_IONSTORM)
|
|
if(prob(REPLACE_LAW_WITH_ION_PROB))
|
|
new_malf_ai.replace_random_law(generate_ion_law(), list(LAW_INHERENT, LAW_SUPPLIED, LAW_ION), LAW_ION)
|
|
else
|
|
new_malf_ai.add_ion_law(generate_ion_law())
|
|
return TRUE
|
|
|
|
/// Midround Wizard Ruleset (From Ghosts)
|
|
/datum/dynamic_ruleset/midround/from_ghosts/wizard
|
|
name = "Wizard"
|
|
midround_ruleset_style = MIDROUND_RULESET_STYLE_HEAVY
|
|
antag_datum = /datum/antagonist/wizard
|
|
antag_flag = ROLE_WIZARD_MIDROUND
|
|
antag_flag_override = ROLE_WIZARD
|
|
ruleset_category = parent_type::ruleset_category | RULESET_CATEGORY_NO_WITTING_CREW_ANTAGONISTS
|
|
required_enemies = list(2,2,1,1,1,1,1,0,0,0)
|
|
required_candidates = 1
|
|
weight = 1
|
|
cost = 10
|
|
requirements = REQUIREMENTS_VERY_HIGH_THREAT_NEEDED
|
|
flags = HIGH_IMPACT_RULESET
|
|
ruleset_lazy_templates = list(LAZY_TEMPLATE_KEY_WIZARDDEN)
|
|
signup_item_path = /obj/item/clothing/head/wizard
|
|
|
|
/datum/dynamic_ruleset/midround/from_ghosts/wizard/ready(forced = FALSE)
|
|
if(!check_candidates())
|
|
return FALSE
|
|
if(!length(GLOB.wizardstart))
|
|
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))
|
|
|
|
/// Midround Nuclear Operatives Ruleset (From Ghosts)
|
|
/datum/dynamic_ruleset/midround/from_ghosts/nuclear
|
|
name = "Nuclear Assault"
|
|
midround_ruleset_style = MIDROUND_RULESET_STYLE_HEAVY
|
|
antag_flag = ROLE_OPERATIVE_MIDROUND
|
|
antag_flag_override = ROLE_OPERATIVE
|
|
antag_datum = /datum/antagonist/nukeop
|
|
ruleset_category = parent_type::ruleset_category | RULESET_CATEGORY_NO_WITTING_CREW_ANTAGONISTS
|
|
enemy_roles = list(
|
|
JOB_AI,
|
|
JOB_CYBORG,
|
|
JOB_CAPTAIN,
|
|
JOB_DETECTIVE,
|
|
JOB_HEAD_OF_SECURITY,
|
|
JOB_SECURITY_OFFICER,
|
|
JOB_WARDEN,
|
|
)
|
|
required_enemies = list(3,3,3,3,3,2,1,1,0,0)
|
|
required_candidates = 5
|
|
weight = 5
|
|
cost = 7
|
|
minimum_round_time = 70 MINUTES
|
|
requirements = REQUIREMENTS_VERY_HIGH_THREAT_NEEDED
|
|
ruleset_lazy_templates = list(LAZY_TEMPLATE_KEY_NUKIEBASE)
|
|
flags = HIGH_IMPACT_RULESET
|
|
signup_item_path = /obj/machinery/nuclearbomb
|
|
|
|
var/list/operative_cap = list(2,2,3,3,4,5,5,5,5,5)
|
|
|
|
/datum/dynamic_ruleset/midround/from_ghosts/nuclear/acceptable(population=0, threat_level=0)
|
|
if (locate(/datum/dynamic_ruleset/roundstart/nuclear) in SSdynamic.executed_rules)
|
|
return FALSE // Unavailable if nuke ops were already sent at roundstart
|
|
indice_pop = min(operative_cap.len, round(living_players.len/5)+1)
|
|
required_candidates = operative_cap[indice_pop]
|
|
return ..()
|
|
|
|
/datum/dynamic_ruleset/midround/from_ghosts/nuclear/ready(forced = FALSE)
|
|
if (!check_candidates())
|
|
return FALSE
|
|
return ..()
|
|
|
|
/datum/dynamic_ruleset/midround/from_ghosts/nuclear/finish_applications()
|
|
var/mob/leader = get_most_experienced(assigned, ROLE_NUCLEAR_OPERATIVE)
|
|
if(leader)
|
|
assigned.Remove(leader)
|
|
assigned.Insert(1, leader)
|
|
return ..()
|
|
|
|
/datum/dynamic_ruleset/midround/from_ghosts/nuclear/finish_setup(mob/new_character, index)
|
|
new_character.mind.set_assigned_role(SSjob.get_job_type(/datum/job/nuclear_operative))
|
|
new_character.mind.special_role = ROLE_NUCLEAR_OPERATIVE
|
|
if(index == 1)
|
|
var/datum/antagonist/nukeop/leader/leader_antag_datum = new()
|
|
new_character.mind.add_antag_datum(leader_antag_datum)
|
|
return
|
|
return ..()
|
|
|
|
/// Midround Blob Ruleset (From Ghosts)
|
|
/datum/dynamic_ruleset/midround/from_ghosts/blob
|
|
name = "Blob"
|
|
midround_ruleset_style = MIDROUND_RULESET_STYLE_HEAVY
|
|
antag_datum = /datum/antagonist/blob
|
|
antag_flag = ROLE_BLOB
|
|
ruleset_category = parent_type::ruleset_category | RULESET_CATEGORY_NO_WITTING_CREW_ANTAGONISTS
|
|
required_enemies = list(2,2,1,1,1,1,1,0,0,0)
|
|
required_candidates = 1
|
|
minimum_round_time = 35 MINUTES
|
|
weight = 3
|
|
cost = 8
|
|
minimum_players = 25
|
|
repeatable = TRUE
|
|
signup_item_path = /obj/structure/blob/normal
|
|
|
|
/datum/dynamic_ruleset/midround/from_ghosts/blob/generate_ruleset_body(mob/applicant)
|
|
var/body = applicant.become_overmind()
|
|
return body
|
|
|
|
/// Midround Blob Infection Ruleset (From Living)
|
|
/datum/dynamic_ruleset/midround/from_living/blob_infection
|
|
name = "Blob Infection"
|
|
midround_ruleset_style = MIDROUND_RULESET_STYLE_HEAVY
|
|
antag_datum = /datum/antagonist/blob/infection
|
|
antag_flag = ROLE_BLOB_INFECTION
|
|
antag_flag_override = ROLE_BLOB
|
|
ruleset_category = parent_type::ruleset_category | RULESET_CATEGORY_NO_WITTING_CREW_ANTAGONISTS
|
|
protected_roles = list(
|
|
JOB_CAPTAIN,
|
|
JOB_DETECTIVE,
|
|
JOB_HEAD_OF_SECURITY,
|
|
JOB_PRISONER,
|
|
JOB_SECURITY_OFFICER,
|
|
JOB_WARDEN,
|
|
)
|
|
restricted_roles = list(
|
|
JOB_AI,
|
|
JOB_CYBORG,
|
|
ROLE_POSITRONIC_BRAIN,
|
|
)
|
|
required_enemies = list(2,2,1,1,1,1,1,0,0,0)
|
|
required_candidates = 1
|
|
minimum_round_time = 35 MINUTES
|
|
weight = 3
|
|
cost = 10
|
|
minimum_players = 25
|
|
repeatable = TRUE
|
|
|
|
/datum/dynamic_ruleset/midround/from_living/blob_infection/trim_candidates()
|
|
..()
|
|
candidates = living_players
|
|
for(var/mob/living/player as anything in candidates)
|
|
var/turf/player_turf = get_turf(player)
|
|
if(!player_turf || !is_station_level(player_turf.z))
|
|
candidates -= player
|
|
continue
|
|
|
|
if(player.mind && (player.mind.special_role || length(player.mind.antag_datums) > 0))
|
|
candidates -= player
|
|
|
|
/datum/dynamic_ruleset/midround/from_living/blob_infection/execute()
|
|
if(!candidates || !candidates.len)
|
|
return FALSE
|
|
var/mob/living/carbon/human/blob_antag = pick_n_take(candidates)
|
|
assigned += blob_antag.mind
|
|
blob_antag.mind.special_role = antag_flag
|
|
notify_ghosts(
|
|
"[blob_antag] has become a blob host!",
|
|
source = blob_antag,
|
|
header = "So Bulbous...",
|
|
)
|
|
return ..()
|
|
|
|
/// Midround Xenomorph Ruleset (From Ghosts)
|
|
/datum/dynamic_ruleset/midround/from_ghosts/xenomorph
|
|
name = "Alien Infestation"
|
|
midround_ruleset_style = MIDROUND_RULESET_STYLE_HEAVY
|
|
antag_datum = /datum/antagonist/xeno
|
|
antag_flag = ROLE_ALIEN
|
|
ruleset_category = parent_type::ruleset_category | RULESET_CATEGORY_NO_WITTING_CREW_ANTAGONISTS
|
|
required_enemies = list(2,2,1,1,1,1,1,0,0,0)
|
|
required_candidates = 1
|
|
minimum_round_time = 40 MINUTES
|
|
weight = 5
|
|
cost = 10
|
|
minimum_players = 25
|
|
repeatable = TRUE
|
|
signup_item_path = /mob/living/basic/alien
|
|
var/list/vents = list()
|
|
|
|
/datum/dynamic_ruleset/midround/from_ghosts/xenomorph/forget_startup()
|
|
vents = list()
|
|
return ..()
|
|
|
|
/datum/dynamic_ruleset/midround/from_ghosts/xenomorph/execute()
|
|
// 50% chance of being incremented by one
|
|
required_candidates += prob(50)
|
|
var/list/vent_pumps = SSmachines.get_machines_by_type_and_subtypes(/obj/machinery/atmospherics/components/unary/vent_pump)
|
|
for(var/obj/machinery/atmospherics/components/unary/vent_pump/temp_vent as anything in vent_pumps)
|
|
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_atmos_machines.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
|
|
new_xeno.move_into_vent(vent)
|
|
message_admins("[ADMIN_LOOKUPFLW(new_xeno)] has been made into an alien by the midround ruleset.")
|
|
log_dynamic("[key_name(new_xeno)] was spawned as an alien by the midround ruleset.")
|
|
return new_xeno
|
|
|
|
/// Midround Nightmare Ruleset (From Ghosts)
|
|
/datum/dynamic_ruleset/midround/from_ghosts/nightmare
|
|
name = "Nightmare"
|
|
midround_ruleset_style = MIDROUND_RULESET_STYLE_LIGHT
|
|
antag_datum = /datum/antagonist/nightmare
|
|
antag_flag = ROLE_NIGHTMARE
|
|
antag_flag_override = ROLE_ALIEN
|
|
ruleset_category = parent_type::ruleset_category | RULESET_CATEGORY_NO_WITTING_CREW_ANTAGONISTS
|
|
required_enemies = list(2,2,1,1,1,1,1,0,0,0)
|
|
required_candidates = 1
|
|
weight = 3
|
|
cost = 5
|
|
minimum_players = 15
|
|
repeatable = TRUE
|
|
signup_item_path = /obj/item/light_eater
|
|
|
|
/datum/dynamic_ruleset/midround/from_ghosts/nightmare/acceptable(population = 0, threat_level = 0)
|
|
var/turf/spawn_loc = find_maintenance_spawn(atmos_sensitive = TRUE, require_darkness = TRUE) //Checks if there's a single safe, dark tile on station.
|
|
if(!spawn_loc)
|
|
return FALSE
|
|
return ..()
|
|
|
|
/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/new_nightmare = new (find_maintenance_spawn(atmos_sensitive = TRUE, require_darkness = TRUE))
|
|
player_mind.transfer_to(new_nightmare)
|
|
player_mind.set_assigned_role(SSjob.get_job_type(/datum/job/nightmare))
|
|
player_mind.special_role = ROLE_NIGHTMARE
|
|
player_mind.add_antag_datum(/datum/antagonist/nightmare)
|
|
new_nightmare.set_species(/datum/species/shadow/nightmare)
|
|
|
|
playsound(new_nightmare, 'sound/magic/ethereal_exit.ogg', 50, TRUE, -1)
|
|
message_admins("[ADMIN_LOOKUPFLW(new_nightmare)] has been made into a Nightmare by the midround ruleset.")
|
|
log_dynamic("[key_name(new_nightmare)] was spawned as a Nightmare by the midround ruleset.")
|
|
return new_nightmare
|
|
|
|
/// Midround Space Dragon Ruleset (From Ghosts)
|
|
/datum/dynamic_ruleset/midround/from_ghosts/space_dragon
|
|
name = "Space Dragon"
|
|
midround_ruleset_style = MIDROUND_RULESET_STYLE_HEAVY
|
|
antag_datum = /datum/antagonist/space_dragon
|
|
antag_flag = ROLE_SPACE_DRAGON
|
|
ruleset_category = parent_type::ruleset_category | RULESET_CATEGORY_NO_WITTING_CREW_ANTAGONISTS
|
|
required_enemies = list(2,2,1,1,1,1,1,0,0,0)
|
|
required_candidates = 1
|
|
weight = 4
|
|
cost = 7
|
|
minimum_players = 25
|
|
repeatable = TRUE
|
|
signup_item_path = /mob/living/basic/space_dragon
|
|
var/list/spawn_locs = list()
|
|
|
|
/datum/dynamic_ruleset/midround/from_ghosts/space_dragon/forget_startup()
|
|
spawn_locs = list()
|
|
return ..()
|
|
|
|
/datum/dynamic_ruleset/midround/from_ghosts/space_dragon/execute()
|
|
for(var/obj/effect/landmark/carpspawn/C in GLOB.landmarks_list)
|
|
spawn_locs += (C.loc)
|
|
if(!spawn_locs.len)
|
|
message_admins("No valid spawn locations found, aborting...")
|
|
return MAP_ERROR
|
|
. = ..()
|
|
|
|
/datum/dynamic_ruleset/midround/from_ghosts/space_dragon/generate_ruleset_body(mob/applicant)
|
|
var/datum/mind/player_mind = new /datum/mind(applicant.key)
|
|
player_mind.active = TRUE
|
|
|
|
var/mob/living/basic/space_dragon/S = new (pick(spawn_locs))
|
|
player_mind.transfer_to(S)
|
|
player_mind.add_antag_datum(/datum/antagonist/space_dragon)
|
|
|
|
playsound(S, 'sound/magic/ethereal_exit.ogg', 50, TRUE, -1)
|
|
message_admins("[ADMIN_LOOKUPFLW(S)] has been made into a Space Dragon by the midround ruleset.")
|
|
log_dynamic("[key_name(S)] was spawned as a Space Dragon by the midround ruleset.")
|
|
priority_announce("A large organic energy flux has been recorded near of [station_name()], please stand-by.", "Lifesign Alert")
|
|
return S
|
|
|
|
/datum/dynamic_ruleset/midround/from_ghosts/abductors
|
|
name = "Abductors"
|
|
midround_ruleset_style = MIDROUND_RULESET_STYLE_LIGHT
|
|
antag_datum = /datum/antagonist/abductor
|
|
antag_flag = ROLE_ABDUCTOR
|
|
ruleset_category = parent_type::ruleset_category | RULESET_CATEGORY_NO_WITTING_CREW_ANTAGONISTS
|
|
required_enemies = list(2,2,1,1,1,1,1,0,0,0)
|
|
required_candidates = 2
|
|
required_applicants = 2
|
|
weight = 4
|
|
cost = 7
|
|
minimum_players = 25
|
|
repeatable = TRUE
|
|
ruleset_lazy_templates = list(LAZY_TEMPLATE_KEY_ABDUCTOR_SHIPS)
|
|
|
|
var/datum/team/abductor_team/new_team
|
|
|
|
/datum/dynamic_ruleset/midround/from_ghosts/abductors/forget_startup()
|
|
new_team = null
|
|
return ..()
|
|
|
|
/datum/dynamic_ruleset/midround/from_ghosts/abductors/ready(forced = FALSE)
|
|
if (required_candidates > (dead_players.len + list_observers.len))
|
|
return FALSE
|
|
return ..()
|
|
|
|
/datum/dynamic_ruleset/midround/from_ghosts/abductors/finish_setup(mob/new_character, index)
|
|
if (index == 1) // Our first guy is the scientist. We also initialize the team here as well since this should only happen once per pair of abductors.
|
|
new_team = new
|
|
if(new_team.team_number > ABDUCTOR_MAX_TEAMS)
|
|
return MAP_ERROR
|
|
var/datum/antagonist/abductor/scientist/new_role = new
|
|
new_character.mind.add_antag_datum(new_role, new_team)
|
|
else // Our second guy is the agent, team is already created, don't need to make another one.
|
|
var/datum/antagonist/abductor/agent/new_role = new
|
|
new_character.mind.add_antag_datum(new_role, new_team)
|
|
|
|
/// Midround Space Ninja Ruleset (From Ghosts)
|
|
/datum/dynamic_ruleset/midround/from_ghosts/space_ninja
|
|
name = "Space Ninja"
|
|
midround_ruleset_style = MIDROUND_RULESET_STYLE_HEAVY
|
|
antag_datum = /datum/antagonist/ninja
|
|
antag_flag = ROLE_NINJA
|
|
ruleset_category = parent_type::ruleset_category | RULESET_CATEGORY_NO_WITTING_CREW_ANTAGONISTS
|
|
required_enemies = list(2,2,1,1,1,1,1,0,0,0)
|
|
required_candidates = 1
|
|
weight = 4
|
|
cost = 8
|
|
minimum_players = 30
|
|
repeatable = TRUE
|
|
ruleset_lazy_templates = list(LAZY_TEMPLATE_KEY_NINJA_HOLDING_FACILITY) // I mean, no one uses the nets anymore but whateva
|
|
signup_item_path = /obj/item/energy_katana
|
|
|
|
var/list/spawn_locs = list()
|
|
|
|
/datum/dynamic_ruleset/midround/from_ghosts/space_ninja/forget_startup()
|
|
spawn_locs = list()
|
|
return ..()
|
|
|
|
/datum/dynamic_ruleset/midround/from_ghosts/space_ninja/execute()
|
|
for(var/obj/effect/landmark/carpspawn/carp_spawn in GLOB.landmarks_list)
|
|
if(!isturf(carp_spawn.loc))
|
|
stack_trace("Carp spawn found not on a turf: [carp_spawn.type] on [isnull(carp_spawn.loc) ? "null" : carp_spawn.loc.type]")
|
|
continue
|
|
spawn_locs += carp_spawn.loc
|
|
if(!spawn_locs.len)
|
|
message_admins("No valid spawn locations found, aborting...")
|
|
return MAP_ERROR
|
|
return ..()
|
|
|
|
/datum/dynamic_ruleset/midround/from_ghosts/space_ninja/generate_ruleset_body(mob/applicant)
|
|
var/mob/living/carbon/human/ninja = create_space_ninja(pick(spawn_locs))
|
|
ninja.key = applicant.key
|
|
ninja.mind.add_antag_datum(/datum/antagonist/ninja)
|
|
|
|
message_admins("[ADMIN_LOOKUPFLW(ninja)] has been made into a Space Ninja by the midround ruleset.")
|
|
log_dynamic("[key_name(ninja)] was spawned as a Space Ninja by the midround ruleset.")
|
|
return ninja
|
|
|
|
/// Midround Spiders Ruleset (From Ghosts)
|
|
/datum/dynamic_ruleset/midround/spiders
|
|
name = "Spiders"
|
|
midround_ruleset_style = MIDROUND_RULESET_STYLE_HEAVY
|
|
antag_flag = ROLE_SPIDER
|
|
ruleset_category = parent_type::ruleset_category | RULESET_CATEGORY_NO_WITTING_CREW_ANTAGONISTS
|
|
required_type = /mob/dead/observer
|
|
required_enemies = list(2,2,1,1,1,1,1,0,0,0)
|
|
required_candidates = 0
|
|
weight = 3
|
|
cost = 8
|
|
minimum_players = 27
|
|
repeatable = TRUE
|
|
var/spawncount = 2
|
|
|
|
/datum/dynamic_ruleset/midround/spiders/execute()
|
|
create_midwife_eggs(spawncount)
|
|
return ..()
|
|
|
|
/// Midround Revenant Ruleset (From Ghosts)
|
|
/datum/dynamic_ruleset/midround/from_ghosts/revenant
|
|
name = "Revenant"
|
|
midround_ruleset_style = MIDROUND_RULESET_STYLE_LIGHT
|
|
ruleset_category = parent_type::ruleset_category | RULESET_CATEGORY_NO_WITTING_CREW_ANTAGONISTS
|
|
antag_datum = /datum/antagonist/revenant
|
|
antag_flag = ROLE_REVENANT
|
|
required_enemies = list(2,2,1,1,1,1,1,0,0,0)
|
|
required_candidates = 1
|
|
weight = 4
|
|
cost = 5
|
|
minimum_players = 15
|
|
repeatable = TRUE
|
|
signup_item_path = /mob/living/basic/revenant
|
|
var/dead_mobs_required = 20
|
|
var/need_extra_spawns_value = 15
|
|
var/list/spawn_locs = list()
|
|
|
|
/datum/dynamic_ruleset/midround/from_ghosts/revenant/forget_startup()
|
|
spawn_locs = list()
|
|
return ..()
|
|
|
|
/datum/dynamic_ruleset/midround/from_ghosts/revenant/acceptable(population=0, threat_level=0)
|
|
if(GLOB.dead_mob_list.len < dead_mobs_required)
|
|
return FALSE
|
|
return ..()
|
|
|
|
/datum/dynamic_ruleset/midround/from_ghosts/revenant/execute()
|
|
for(var/mob/living/corpse in GLOB.dead_mob_list) //look for any dead bodies
|
|
var/turf/corpse_turf = get_turf(corpse)
|
|
if(corpse_turf && is_station_level(corpse_turf.z))
|
|
spawn_locs += corpse_turf
|
|
if(!spawn_locs.len || spawn_locs.len < need_extra_spawns_value) //look for any morgue trays, crematoriums, ect if there weren't alot of dead bodies on the station to pick from
|
|
for(var/obj/structure/bodycontainer/corpse_container in GLOB.bodycontainers)
|
|
var/turf/container_turf = get_turf(corpse_container)
|
|
if(container_turf && is_station_level(container_turf.z))
|
|
spawn_locs += container_turf
|
|
if(!spawn_locs.len) //If we can't find any valid spawnpoints, try the carp spawns
|
|
for(var/obj/effect/landmark/carpspawn/carp_spawnpoint in GLOB.landmarks_list)
|
|
if(isturf(carp_spawnpoint.loc))
|
|
spawn_locs += carp_spawnpoint.loc
|
|
if(!spawn_locs.len) //If we can't find THAT, then just give up and cry
|
|
return FALSE
|
|
. = ..()
|
|
|
|
/datum/dynamic_ruleset/midround/from_ghosts/revenant/generate_ruleset_body(mob/applicant)
|
|
var/mob/living/basic/revenant/revenant = new(pick(spawn_locs))
|
|
revenant.key = applicant.key
|
|
message_admins("[ADMIN_LOOKUPFLW(revenant)] has been made into a revenant by the midround ruleset.")
|
|
log_game("[key_name(revenant)] was spawned as a revenant by the midround ruleset.")
|
|
return revenant
|
|
|
|
/// Midround Space Pirates Ruleset (From Ghosts)
|
|
/datum/dynamic_ruleset/midround/pirates
|
|
name = "Space Pirates"
|
|
midround_ruleset_style = MIDROUND_RULESET_STYLE_LIGHT
|
|
ruleset_category = parent_type::ruleset_category | RULESET_CATEGORY_NO_WITTING_CREW_ANTAGONISTS
|
|
antag_flag = "Space Pirates"
|
|
required_type = /mob/dead/observer
|
|
required_enemies = list(2,2,1,1,1,1,1,0,0,0)
|
|
required_candidates = 0
|
|
weight = 3
|
|
cost = 8
|
|
minimum_players = 20
|
|
repeatable = TRUE
|
|
|
|
/datum/dynamic_ruleset/midround/pirates/acceptable(population=0, threat_level=0)
|
|
if (SSmapping.is_planetary() || GLOB.light_pirate_gangs.len == 0)
|
|
return FALSE
|
|
return ..()
|
|
|
|
/datum/dynamic_ruleset/midround/pirates/execute()
|
|
send_pirate_threat(GLOB.light_pirate_gangs)
|
|
return ..()
|
|
|
|
/// Dangerous Space Pirates ruleset
|
|
/datum/dynamic_ruleset/midround/dangerous_pirates
|
|
name = "Dangerous Space Pirates"
|
|
midround_ruleset_style = MIDROUND_RULESET_STYLE_HEAVY
|
|
ruleset_category = parent_type::ruleset_category | RULESET_CATEGORY_NO_WITTING_CREW_ANTAGONISTS
|
|
antag_flag = "Space Pirates"
|
|
required_type = /mob/dead/observer
|
|
required_enemies = list(2,2,1,1,1,1,1,0,0,0)
|
|
required_candidates = 0
|
|
weight = 3
|
|
cost = 8
|
|
minimum_players = 25
|
|
repeatable = TRUE
|
|
|
|
/datum/dynamic_ruleset/midround/dangerous_pirates/acceptable(population=0, threat_level=0)
|
|
if (SSmapping.is_planetary() || GLOB.heavy_pirate_gangs.len == 0)
|
|
return FALSE
|
|
return ..()
|
|
|
|
/datum/dynamic_ruleset/midround/dangerous_pirates/execute()
|
|
send_pirate_threat(GLOB.heavy_pirate_gangs)
|
|
return ..()
|
|
|
|
/// Midround Obsessed Ruleset (From Living)
|
|
/datum/dynamic_ruleset/midround/from_living/obsessed
|
|
name = "Obsessed"
|
|
midround_ruleset_style = MIDROUND_RULESET_STYLE_LIGHT
|
|
antag_datum = /datum/antagonist/obsessed
|
|
antag_flag = ROLE_OBSESSED
|
|
ruleset_category = parent_type::ruleset_category | RULESET_CATEGORY_NO_WITTING_CREW_ANTAGONISTS
|
|
restricted_roles = list(
|
|
JOB_AI,
|
|
JOB_CYBORG,
|
|
ROLE_POSITRONIC_BRAIN,
|
|
)
|
|
required_enemies = list(2,2,1,1,1,1,1,0,0,0)
|
|
required_candidates = 1
|
|
weight = 4
|
|
cost = 3 // Doesn't have the same impact on rounds as revenants, dragons, sentient disease (10) or syndicate infiltrators (5).
|
|
repeatable = TRUE
|
|
|
|
/datum/dynamic_ruleset/midround/from_living/obsessed/trim_candidates()
|
|
..()
|
|
candidates = living_players
|
|
for(var/mob/living/carbon/human/candidate in candidates)
|
|
if( \
|
|
!candidate.get_organ_by_type(/obj/item/organ/internal/brain) \
|
|
|| candidate.mind.has_antag_datum(/datum/antagonist/obsessed) \
|
|
|| candidate.stat == DEAD \
|
|
|| !(ROLE_OBSESSED in candidate.client?.prefs?.be_special) \
|
|
|| !candidate.mind.assigned_role \
|
|
)
|
|
candidates -= candidate
|
|
|
|
/datum/dynamic_ruleset/midround/from_living/obsessed/execute()
|
|
var/mob/living/carbon/human/obsessed = pick_n_take(candidates)
|
|
obsessed.gain_trauma(/datum/brain_trauma/special/obsessed)
|
|
message_admins("[ADMIN_LOOKUPFLW(obsessed)] has been made Obsessed by the midround ruleset.")
|
|
log_game("[key_name(obsessed)] was made Obsessed by the midround ruleset.")
|
|
notify_ghosts(
|
|
"[obsessed] has developed an obsession with someone!",
|
|
source = obsessed,
|
|
header = "Love Can Bloom",
|
|
)
|
|
return TRUE
|
|
|
|
/// Midround Space Changeling Ruleset (From Ghosts)
|
|
/datum/dynamic_ruleset/midround/from_ghosts/changeling_midround
|
|
name = "Space Changeling"
|
|
midround_ruleset_style = MIDROUND_RULESET_STYLE_LIGHT
|
|
antag_datum = /datum/antagonist/changeling/space
|
|
antag_flag = ROLE_CHANGELING_MIDROUND
|
|
antag_flag_override = ROLE_CHANGELING
|
|
ruleset_category = parent_type::ruleset_category | RULESET_CATEGORY_NO_WITTING_CREW_ANTAGONISTS
|
|
required_type = /mob/dead/observer
|
|
required_enemies = list(2,2,1,1,1,1,1,0,0,0)
|
|
required_candidates = 1
|
|
weight = 3
|
|
cost = 7
|
|
minimum_players = 15
|
|
repeatable = TRUE
|
|
signup_item_path = /obj/effect/meteor/meaty/changeling
|
|
|
|
/datum/dynamic_ruleset/midround/from_ghosts/changeling_midround/generate_ruleset_body(mob/applicant)
|
|
var/body = generate_changeling_meteor(applicant)
|
|
message_admins("[ADMIN_LOOKUPFLW(body)] has been made into a space changeling by the midround ruleset.")
|
|
log_dynamic("[key_name(body)] was spawned as a space changeling by the midround ruleset.")
|
|
return body
|
|
|
|
/// Midround Paradox Clone Ruleset (From Ghosts)
|
|
/datum/dynamic_ruleset/midround/from_ghosts/paradox_clone
|
|
name = "Paradox Clone"
|
|
midround_ruleset_style = MIDROUND_RULESET_STYLE_LIGHT
|
|
antag_datum = /datum/antagonist/paradox_clone
|
|
antag_flag = ROLE_PARADOX_CLONE
|
|
ruleset_category = parent_type::ruleset_category | RULESET_CATEGORY_NO_WITTING_CREW_ANTAGONISTS
|
|
enemy_roles = list(
|
|
JOB_CAPTAIN,
|
|
JOB_DETECTIVE,
|
|
JOB_HEAD_OF_SECURITY,
|
|
JOB_SECURITY_OFFICER,
|
|
JOB_WARDEN,
|
|
)
|
|
required_enemies = list(2, 2, 1, 1, 1, 1, 1, 0, 0, 0)
|
|
required_candidates = 1
|
|
weight = 4
|
|
cost = 3
|
|
repeatable = TRUE
|
|
signup_item_path = /obj/effect/bluespace_stream
|
|
var/list/possible_spawns = list() ///places the antag can spawn
|
|
|
|
/datum/dynamic_ruleset/midround/from_ghosts/paradox_clone/forget_startup()
|
|
possible_spawns = list()
|
|
return ..()
|
|
|
|
/datum/dynamic_ruleset/midround/from_ghosts/paradox_clone/execute()
|
|
possible_spawns += find_maintenance_spawn(atmos_sensitive = TRUE, require_darkness = FALSE)
|
|
if(!possible_spawns.len)
|
|
return MAP_ERROR
|
|
return ..()
|
|
|
|
/datum/dynamic_ruleset/midround/from_ghosts/paradox_clone/generate_ruleset_body(mob/applicant)
|
|
var/datum/mind/player_mind = new /datum/mind(applicant.key)
|
|
player_mind.active = TRUE
|
|
|
|
var/mob/living/carbon/human/clone_victim = find_original()
|
|
var/mob/living/carbon/human/clone = clone_victim.make_full_human_copy(pick(possible_spawns))
|
|
player_mind.transfer_to(clone)
|
|
|
|
var/datum/antagonist/paradox_clone/new_datum = player_mind.add_antag_datum(/datum/antagonist/paradox_clone)
|
|
new_datum.original_ref = WEAKREF(clone_victim.mind)
|
|
new_datum.setup_clone()
|
|
|
|
playsound(clone, 'sound/weapons/zapbang.ogg', 30, TRUE)
|
|
new /obj/item/storage/toolbox/mechanical(clone.loc) //so they dont get stuck in maints
|
|
|
|
message_admins("[ADMIN_LOOKUPFLW(clone)] has been made into a Paradox Clone by the midround ruleset.")
|
|
clone.log_message("was spawned as a Paradox Clone of [key_name(clone)] by the midround ruleset.", LOG_GAME)
|
|
|
|
return clone
|
|
|
|
/**
|
|
* Trims through GLOB.player_list and finds a target
|
|
* Returns a single human victim, if none is possible then returns null.
|
|
*/
|
|
/datum/dynamic_ruleset/midround/from_ghosts/paradox_clone/proc/find_original()
|
|
var/list/possible_targets = list()
|
|
|
|
for(var/mob/living/carbon/human/player in GLOB.player_list)
|
|
if(!player.client || !player.mind || player.stat)
|
|
continue
|
|
if(!(player.mind.assigned_role.job_flags & JOB_CREW_MEMBER))
|
|
continue
|
|
possible_targets += player
|
|
|
|
if(possible_targets.len)
|
|
return pick(possible_targets)
|
|
return FALSE
|
|
|
|
#undef MALF_ION_PROB
|
|
#undef REPLACE_LAW_WITH_ION_PROB
|
|
|
|
/// Midround Voidwalker Ruleset (From Ghosts)
|
|
/datum/dynamic_ruleset/midround/from_ghosts/voidwalker
|
|
name = "Voidwalker"
|
|
midround_ruleset_style = MIDROUND_RULESET_STYLE_LIGHT
|
|
antag_datum = /datum/antagonist/voidwalker
|
|
antag_flag = ROLE_VOIDWALKER
|
|
antag_flag_override = ROLE_VOIDWALKER
|
|
ruleset_category = parent_type::ruleset_category | RULESET_CATEGORY_NO_WITTING_CREW_ANTAGONISTS
|
|
required_enemies = list(2,2,1,1,1,1,1,0,0,0)
|
|
required_candidates = 1
|
|
weight = 2
|
|
cost = 5
|
|
minimum_players = 40
|
|
repeatable = TRUE
|
|
signup_item_path = /obj/item/cosmic_skull
|
|
ruleset_lazy_templates = list(LAZY_TEMPLATE_KEY_VOIDWALKER_VOID)
|
|
/// The space turf we find in acceptable(), cached for ease
|
|
var/space_turf
|
|
|
|
/datum/dynamic_ruleset/midround/from_ghosts/voidwalker/acceptable(population = 0, threat_level = 0)
|
|
space_turf = find_space_spawn()
|
|
// Space only antag and will die on planetary gravity.
|
|
if(SSmapping.is_planetary() || !space_turf)
|
|
return FALSE
|
|
return ..()
|
|
|
|
/datum/dynamic_ruleset/midround/from_ghosts/voidwalker/generate_ruleset_body(mob/applicant)
|
|
var/datum/mind/player_mind = new /datum/mind(applicant.key)
|
|
player_mind.active = TRUE
|
|
|
|
var/mob/living/carbon/human/voidwalker = new (space_turf)
|
|
player_mind.transfer_to(voidwalker)
|
|
player_mind.set_assigned_role(SSjob.get_job_type(/datum/job/voidwalker))
|
|
player_mind.special_role = antag_flag
|
|
player_mind.add_antag_datum(antag_datum)
|
|
|
|
playsound(voidwalker, 'sound/magic/ethereal_exit.ogg', 50, TRUE, -1)
|
|
message_admins("[ADMIN_LOOKUPFLW(voidwalker)] has been made into a Voidwalker by the midround ruleset.")
|
|
log_dynamic("[key_name(voidwalker)] was spawned as a Voidwalker by the midround ruleset.")
|
|
return voidwalker
|