Files
Bubberstation/code/controllers/subsystem/dynamic/dynamic_rulesets_midround.dm
T
TimberpoesandGitHub 6808a082eb Assorted changes to job assignment code and logging. Runtime free, guaranteed or your money back. Price: $£0. (#85947)
## About The Previous Pull Request

#85308 reverted by #85929


![image](https://github.com/user-attachments/assets/e7518dcb-a60a-4bf1-a3d4-a5a8966d8633)

~~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.
/🆑
2024-09-13 13:58:35 +02:00

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