Files
RikuTheKillerandGitHub c7e4e90004 Adds a new antag, the Blood Worm (#93787)
## LTS Document

Check this document before making any significant future changes to
blood worms, please.

https://hackmd.io/@RikuTheKiller/H1AHQSKNZx

## About The Pull Request

THIS PR SHOULD ABSOLUTELY BE TM'D FIRST

Blood worms are a new progression antag. When the event runs, 2
candidates are picked from ghosts and spawned in as blood worm
hatchlings, which then have to grow up, do a couple objectives and take
over the station.

Hatchlings are weak outside of a host, while juveniles can stand their
own reasonably well. Adults have high offensive power and can only be
dealt with using the right gear or a lot of luck and robustness. They're
meant to be a moment of glory for achieving maximum progression and they
can bootstrap the next hatchlings by gathering corpses before cocooning.

Each growth stage requires 30 seconds in a cocoon, which can only be
created after consuming a lot of blood. There's a falloff curve on a
per-blood-type basis, meaning you can't drain the same person over and
over again to reach adulthood. The medbay freezer is a priority target
for the blood worms and can get one of them to the juvenile stage if
fully ransacked.

It takes 500 blood to mature from hatchling to juvenile, and 1500 blood
to mature from juvenile to adult. You can only get up to 1000 blood from
synthetic sources like monkeys, and consuming synthetic blood is 30%
less efficient. Blood worms can also examine living targets to see how
much blood a target has, and how much growth the blood worm would gain
for consuming that blood.

Blood worms spawn in vents and have night vision for maneuvering in
maintenance. Hatchlings can ventcrawl, while juveniles can move around
by breaking things. Optionally, you can take over a host with a lot of
access like the Captain to go basically anywhere, especially if nobody
knows you killed the captain.

Behind the scenes, host-taking kicks the host's original mind to a
backseat mob. This needs the most testing in practice, but it's
confirmed that it returns the host's mind back to their body, at least
in testing.

All mob, ability and action sprites are made by INFRARED_BARON. Legal
rights were transferred to me after I paid for the commission.

Note, I've been working on this massive PR for quite a while, so
documenting every small change is really hard! Apologies for anything
I've missed. There's a lot.

Final note, admins can spawn these by either:
A. Trigger the midround event via the dynamic-panel verb, under the
Rulesets tab.
B. Giving someone the Blood Worm antag datum via the Traitor Panel in
the Player Panel for the target player. This will transform their mob
into a valid Blood Worm, with all of the associated objectives and such.

### Active Abilities
1. Leech Blood (No Host) - Lets the blood worm drain blood from living
targets and reagent containers. Uses an aggressive grab to restrain
living targets until leeching is over, which takes around a second to
initiate. Causes oxyloss during the leeching. NPC monkeys can't escape
from this and it floors targets as well.
2. Spit Blood (Both) - Multi-function ability, lets the blood worm fire
ranged corrosive blood spit at targets, melt restraints on their hosts
by right-clicking, and as an adult, shoot a burst of blood spit at a
target by right-clicking. Note of the right-click abilities, shooting
bursts can't be done while in a host. (to avoid unfair stealth kills)
Shooting a burst has a much longer cooldown than shooting normally. All
spit types cost blood to use.
3. Invade Corpse (No Host) - Lets the blood worm take a host for
themselves, consuming all of the host's blood and in essence, "becoming"
the host. Any bloodloss inflicted on the host is taken as damage to the
blood worm, and the blood worm retains its weakness to fire even in this
state. Burn damage itself no longer has any extra damage, though.
4. Leave Host (Host) - Title, literally just leaves the host after a
delay. Notably works even while the host is moving, dead, incapacitated
or otherwise fucked up in any way, shape or form.
5. Inject Blood (Host) - Lets the blood worm heal its host. The potency
of this increases as the worm grows up, but so does the cooldown and
blood consumption. This works on organ damage, injuries, etc.
6. Mature (No Host) - Makes the blood worm enter a cocoon for 30
seconds, emerging as the next growth stage. Requires an increasing
amount of consumed blood / growth as the blood worm uses it.
7. Reproduce (No Host, Adult Only) - Makes the blood worm enter a cocoon
for 30 seconds, with 4 hatchlings emerging out of it, including the
original blood worm, now reverted back into a hatchling as well.
8. Revive Host (Host) - If the host is in a viable state to be revived,
revives them after an animation sequence plays out.

### Passive Abilities
1. Space Immunity - Blood worms are immune to the cold, low pressures
and a lack of oxygen. Only the immunity to a lack of oxygen carries on
to hosts from this.
2. Organ Insertion - Blood worms can insert organs into their hosts by
right-clicking on them with the organ in-hand. This mainly exists to
deal with hosts that lack organs, and avoids the gotcha where an adult
blood worm ends up gutting their host by hitting them too hard, as they
can simply fix it on the spot.
3. Life Support - Blood worm hosts don't need a heart, lungs or a liver
to survive. Lungs are useful for speaking, and a liver is necessary to
process reagents.
4. Regeneration - Blood worms slowly heal over time. This is nowhere
near enough to overcome bleeding or heat damage, since it's 0.3 hp/s for
a hatchling, 0.4 hp/s for a juvenile and 0.5 hp/s for an adult.
5. Night Vision - Blood worms can see in the dark. This doesn't extend
to hosts.
6. Ventcrawling - Hatchling blood worms can ventcrawl.
7. Doorcrawling - Hatchling and juvenile blood worms can slide under
doors. Doing so takes 3 seconds for a hatchling and 5 seconds for a
juvenile.
8. HUD - Blood worms can tell how much blood targets have at a glance,
via a blood HUD bar exclusive to them. They can also tell apart other
blood worm hosts from normal people via an antag HUD. There's also an
examine message they can use on living targets for even more info.

### Weaknesses
1. Heat and Fire - Blood worms quickly die to heat, their bodies are
flammable and their blood will burn up if their host's core temperature
is too high. The main counter to this is getting a host with
flame-resistant gear.
2. Bleeding - While in a host, bleeding wounds will directly damage the
blood worm itself. How much a host needs to bleed before the worm dies
depends on their growth stage. Blood worm hosts keep bleeding even while
dead, so just keep hitting them and they'll die. Blood worms
automatically leave their hosts when they hit 10% health or lower, and
their hosts bleed 50% faster than normal people.
3. Stuns - Blood worms have no way of dealing with a stunned host other
than getting out. They can deal with any restraints by melting them,
though.
4. Testing - Security can order a blood worm testing crate from cargo,
either for a 20 minute cooldown via the security cargo interface
console, or for 10000 credits via the supply console. It contains 4
single-use testers that hurt a bit when applied, but are instant to use
and 100% accurate. The stopgap is that they're really fucking expensive
and only work once per item.

### Screenshot
<img width="280" height="132" alt="image"
src="https://github.com/user-attachments/assets/00d22361-997e-4347-a0bf-aa240de40727"
/>

## Why It's Good For The Game

Antagonist variety, mainly. This is basically Cortical Borers 2:
Electric Boogaloo.

Currently, we lack any antagonists with mind control abilities. That
really sucks!

I've also gotten a lot of positive feedback about the antagonist while
working on it.

This antagonist also has great potential for roleplay, as they can take
over hosts, surprise attack people by getting out of a dead corpse, talk
to each other using Wormspeak, etc.

I think we're also itching for variety on "pest" antagonists. Right now
we just have spiders and xenos. Everybody knows these two, so why not
mix it up a bit?

And as for balance? Blood worms are relatively easy to dispatch when you
know their weaknesses, which are extremely clear. Bleeding for hosts,
fire for either one, lasers for the worms themselves. As long as you get
the host in crit and keep hitting, you've pretty much won, and they
can't keep spamming Inject Blood forever since they'll quickly run out
of blood to use.
## Changelog
🆑
add: Added a new heavy roundstart/midround antagonist, the Blood Worm.
Credit to INFRARED_BARON for the sprites!
fix: Removing traits based on a source no longer causes issues with
trait signals.
fix: High-priority effects no longer double-trigger due to subsystem
issues.
fix: Weighted averaging in reagent merging code has been band-aid fixed.
It's not the best, but it works.
/🆑
2026-01-14 23:30:35 -06:00

468 lines
17 KiB
Plaintext

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