The round has ended."))))
log_game("The round has ended.")
diff --git a/code/controllers/master.dm b/code/controllers/master.dm
index a7013f058ed6..f44a5f5161e9 100644
--- a/code/controllers/master.dm
+++ b/code/controllers/master.dm
@@ -282,6 +282,8 @@ GLOBAL_REAL(Master, /datum/controller/master) = new
if(sleep_offline_after_initializations && CONFIG_GET(flag/resume_after_initializations))
world.sleep_offline = FALSE
initializations_finished_with_no_players_logged_in = initialized_tod < REALTIMEOFDAY - 10
+ /// run votes
+ SSvote.initiate_vote(/datum/vote/storyteller, "pick round storyteller", forced = TRUE) // idk where else to run this lol
/**
* Initialize a given subsystem and handle the results.
diff --git a/code/controllers/subsystem/processing/station.dm b/code/controllers/subsystem/processing/station.dm
index 21cd66c11a72..0b5626d68977 100644
--- a/code/controllers/subsystem/processing/station.dm
+++ b/code/controllers/subsystem/processing/station.dm
@@ -76,6 +76,9 @@ PROCESSING_SUBSYSTEM_DEF(station)
for(var/iterator in 1 to amount)
var/datum/station_trait/trait_type = pick_weight(selectable_traits_by_types[trait_sign]) //Rolls from the table for the specific trait type
selectable_traits_by_types[trait_sign] -= trait_type
+ if(istype(trait_type, /datum/station_trait/late_arrivals) && SSmapping.config.map_name == "Oshan Station")
+ amount++
+ continue
setup_trait(trait_type)
///Creates a given trait of a specific type, while also removing any blacklisted ones from the future pool.
diff --git a/code/controllers/subsystem/statpanel.dm b/code/controllers/subsystem/statpanel.dm
index 13df2be77ba9..517c26798a60 100644
--- a/code/controllers/subsystem/statpanel.dm
+++ b/code/controllers/subsystem/statpanel.dm
@@ -26,6 +26,7 @@ SUBSYSTEM_DEF(statpanels)
global_data = list(
"Map: [SSmapping.config?.map_name || "Loading..."]",
cached ? "Next Map: [cached.map_name]" : null,
+ "Storyteller: [SSgamemode.storyteller ? SSgamemode.storyteller.name : "N/A"]", //monkestation addition
"Round ID: [GLOB.round_id ? GLOB.round_id : "NULL"]",
"Server Time: [time2text(world.timeofday, "YYYY-MM-DD hh:mm:ss")]",
"Round Time: [ROUND_TIME()]",
diff --git a/code/controllers/subsystem/ticker.dm b/code/controllers/subsystem/ticker.dm
index aed9754a2a46..c94405a1afa3 100755
--- a/code/controllers/subsystem/ticker.dm
+++ b/code/controllers/subsystem/ticker.dm
@@ -225,10 +225,14 @@ SUBSYSTEM_DEF(ticker)
var/init_start = world.timeofday
mode = new /datum/game_mode/dynamic
-
+ SSgamemode.init_storyteller() //monkestation addition
CHECK_TICK
//Configure mode and assign player to special mode stuff
var/can_continue = 0
+ //monkestation addition start
+ can_continue = SSgamemode.pre_setup()
+ CHECK_TICK
+ //monkestation addition end
can_continue = src.mode.pre_setup() //Choose antagonists
CHECK_TICK
can_continue = can_continue && SSjob.DivideOccupations() //Distribute jobs
@@ -295,6 +299,8 @@ SUBSYSTEM_DEF(ticker)
/datum/controller/subsystem/ticker/proc/PostSetup()
set waitfor = FALSE
+ SSgamemode.storyteller.process(STORYTELLER_WAIT_TIME * 0.1) // we want this asap
+ SSgamemode.storyteller.round_started = TRUE
mode.post_setup()
GLOB.start_state = new /datum/station_state()
GLOB.start_state.count()
diff --git a/code/controllers/subsystem/vote.dm b/code/controllers/subsystem/vote.dm
index 0513c666b774..787748405aaa 100644
--- a/code/controllers/subsystem/vote.dm
+++ b/code/controllers/subsystem/vote.dm
@@ -285,12 +285,16 @@ SUBSYSTEM_DEF(vote)
"message" = vote.message,
)
+ if(vote.has_desc)
+ vote_data += list("desc" = vote.return_desc(vote_name))
+
if(vote == current_vote)
var/list/choices = list()
for(var/key in current_vote.choices)
choices += list(list(
"name" = key,
"votes" = current_vote.choices[key],
+ "desc" = current_vote.return_desc(key)
))
data["currentVote"] = list(
diff --git a/code/datums/components/food/edible.dm b/code/datums/components/food/edible.dm
index ae6bc7f5a599..07d5763c6654 100644
--- a/code/datums/components/food/edible.dm
+++ b/code/datums/components/food/edible.dm
@@ -42,6 +42,9 @@ Behavior that's still missing from this component that original food items had t
var/list/tastes
///The buffs these foods give when eaten
var/food_buffs
+ ///how many bites we can get
+ var/total_bites = 0
+ var/current_mask
/datum/component/edible/Initialize(
list/initial_reagents,
@@ -74,6 +77,7 @@ Behavior that's still missing from this component that original food items had t
src.tastes = string_assoc_list(tastes)
src.check_liked = check_liked
+
setup_initial_reagents(initial_reagents)
/datum/component/edible/RegisterWithParent()
@@ -214,6 +218,8 @@ Behavior that's still missing from this component that original food items had t
else
owner.reagents.add_reagent(rid, amount)
+ total_bites = round(owner.reagents.total_volume / bite_consumption)
+
/datum/component/edible/proc/examine(datum/source, mob/user, list/examine_list)
SIGNAL_HANDLER
@@ -443,6 +449,14 @@ Behavior that's still missing from this component that original food items had t
var/fraction = min(bite_consumption / owner.reagents.total_volume, 1)
owner.reagents.trans_to(eater, bite_consumption, transfered_by = feeder, methods = INGEST)
bitecount++
+ var/desired_mask = (total_bites / bitecount)
+ desired_mask = round(desired_mask)
+ desired_mask = max(1,desired_mask)
+ desired_mask = min(desired_mask, 4)
+
+ if(desired_mask != current_mask)
+ current_mask = desired_mask
+ parent.add_filter("bite", 0, alpha_mask_filter(icon=icon('goon/icons/obj/food.dmi', "eating[desired_mask]")))
checkLiked(fraction, eater)
if(!owner.reagents.total_volume)
On_Consume(eater, feeder)
@@ -570,6 +584,15 @@ Behavior that's still missing from this component that original food items had t
if(bitecount == 0 || prob(50))
L.manual_emote("nibbles away at \the [parent].")
bitecount++
+ var/desired_mask = (total_bites / bitecount)
+ desired_mask = round(desired_mask)
+ desired_mask = max(1,desired_mask)
+ desired_mask = min(desired_mask, 4)
+
+ if(desired_mask != current_mask)
+ current_mask = desired_mask
+ src.add_filter("bite", 0, alpha_mask_filter(icon=icon('goon/icons/obj/food.dmi', "eating[desired_mask]")))
+
. = COMPONENT_CANCEL_ATTACK_CHAIN
L.taste(owner.reagents) // why should carbons get all the fun?
if(bitecount >= 5)
diff --git a/code/game/gamemodes/dynamic/dynamic.dm b/code/game/gamemodes/dynamic/dynamic.dm
index c8c1b2d45064..022de70d54a6 100644
--- a/code/game/gamemodes/dynamic/dynamic.dm
+++ b/code/game/gamemodes/dynamic/dynamic.dm
@@ -135,7 +135,7 @@ GLOBAL_VAR_INIT(dynamic_forced_threat_level, -1)
var/waittime_h = 1800
/// Maximum amount of threat allowed to generate.
- var/max_threat_level = 50
+ var/max_threat_level = 0 //disables dynamic threat PLEASE DONT LET ME MERGE THIS
/// The extra chance multiplier that a heavy impact midround ruleset will run next time.
/// For example, if this is set to 50, then the next heavy roll will be about 50% more likely to happen.
diff --git a/code/game/gamemodes/dynamic/dynamic_rulesets_midround.dm b/code/game/gamemodes/dynamic/dynamic_rulesets_midround.dm
index 5ab88b19956c..38b7062217af 100644
--- a/code/game/gamemodes/dynamic/dynamic_rulesets_midround.dm
+++ b/code/game/gamemodes/dynamic/dynamic_rulesets_midround.dm
@@ -370,7 +370,6 @@
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
var/list/operative_cap = list(2,2,3,3,4,5,5,5,5,5)
diff --git a/code/game/gamemodes/dynamic/dynamic_rulesets_roundstart.dm b/code/game/gamemodes/dynamic/dynamic_rulesets_roundstart.dm
index b0404e7250a4..d016b0429037 100644
--- a/code/game/gamemodes/dynamic/dynamic_rulesets_roundstart.dm
+++ b/code/game/gamemodes/dynamic/dynamic_rulesets_roundstart.dm
@@ -450,7 +450,6 @@
requirements = list(90,90,90,80,60,40,30,20,10,10)
flags = HIGH_IMPACT_RULESET
antag_cap = list("denominator" = 18, "offset" = 1)
- ruleset_lazy_templates = list(LAZY_TEMPLATE_KEY_NUKIEBASE)
var/required_role = ROLE_NUCLEAR_OPERATIVE
var/datum/team/nuclear/nuke_team
diff --git a/code/modules/admin/admin_verbs.dm b/code/modules/admin/admin_verbs.dm
index 86ca881ef3bc..c01ab9e5b21d 100644
--- a/code/modules/admin/admin_verbs.dm
+++ b/code/modules/admin/admin_verbs.dm
@@ -118,6 +118,7 @@ GLOBAL_LIST_INIT(admin_verbs_fun, list(
/client/proc/drop_dynex_bomb,
/client/proc/flip_ghost_spawn,
/client/proc/forceEvent,
+ /client/proc/forceGamemode,
/client/proc/generate_bulk_code, //monkestation addition
/client/proc/generate_code, //monkestation addition
/client/proc/mass_add_metacoins, //monkestation addition
diff --git a/code/modules/admin/force_event.dm b/code/modules/admin/force_event.dm
index 1c97936ac48d..e519d2f4ed8a 100644
--- a/code/modules/admin/force_event.dm
+++ b/code/modules/admin/force_event.dm
@@ -1,4 +1,5 @@
///Allows an admin to force an event
+/*
/client/proc/forceEvent()
set name = "Trigger Event"
set category = "Admin.Events"
@@ -15,7 +16,7 @@
var/datum/force_event/ui = new(usr)
ui.ui_interact(usr)
-
+*/
/// Force Event Panel
/datum/force_event
diff --git a/code/modules/admin/topic.dm b/code/modules/admin/topic.dm
index cc21cc3ebe45..b3600978a9a6 100644
--- a/code/modules/admin/topic.dm
+++ b/code/modules/admin/topic.dm
@@ -70,6 +70,7 @@
if(!check_rights(R_ADMIN))
return
SSticker.mode.admin_panel()
+ SSgamemode.admin_panel(usr) //monkestation addition
else if(href_list["call_shuttle"])
if(!check_rights(R_ADMIN))
diff --git a/code/modules/antagonists/nukeop/nukeop.dm b/code/modules/antagonists/nukeop/nukeop.dm
index b94c33d326ad..c2809d680cc6 100644
--- a/code/modules/antagonists/nukeop/nukeop.dm
+++ b/code/modules/antagonists/nukeop/nukeop.dm
@@ -126,8 +126,6 @@
/// Actually moves our nukie to where they should be
/datum/antagonist/nukeop/proc/move_to_spawnpoint()
- // Ensure that the nukiebase is loaded, and wait for it if required
- SSmapping.lazy_load_template(LAZY_TEMPLATE_KEY_NUKIEBASE)
var/turf/destination = get_spawnpoint()
owner.current.forceMove(destination)
if(!owner.current.onSyndieBase())
@@ -530,13 +528,6 @@
/// Returns whether or not syndicate operatives escaped.
/proc/is_infiltrator_docked_at_syndiebase()
var/obj/docking_port/mobile/infiltrator/infiltrator_port = SSshuttle.getShuttle("syndicate")
+ var/obj/docking_port/stationary/transit/infiltrator_dock = locate() in infiltrator_port.loc
- var/datum/lazy_template/nukie_base/nukie_template = GLOB.lazy_templates[LAZY_TEMPLATE_KEY_NUKIEBASE]
- if(!nukie_template)
- return FALSE // if its not even loaded, cant be docked
-
- for(var/datum/turf_reservation/loaded_area as anything in nukie_template.reservations)
- var/infiltrator_turf = get_turf(infiltrator_port)
- if(infiltrator_turf in loaded_area.reserved_turfs)
- return TRUE
- return FALSE
+ return infiltrator_port && (is_centcom_level(infiltrator_port.z) || infiltrator_dock)
diff --git a/code/modules/antagonists/nukeop/outfits.dm b/code/modules/antagonists/nukeop/outfits.dm
index 28ba70e13ceb..bb8d4126fe6f 100644
--- a/code/modules/antagonists/nukeop/outfits.dm
+++ b/code/modules/antagonists/nukeop/outfits.dm
@@ -44,8 +44,6 @@
if(visualsOnly)
return
- // We don't require the nukiebase be loaded to function, but lets go ahead and kick off loading just in case
- INVOKE_ASYNC(SSmapping, TYPE_PROC_REF(/datum/controller/subsystem/mapping, lazy_load_template), LAZY_TEMPLATE_KEY_NUKIEBASE)
var/obj/item/radio/radio = nukie.ears
radio.set_frequency(FREQ_SYNDICATE)
radio.freqlock = RADIO_FREQENCY_LOCKED
diff --git a/code/modules/antagonists/pirate/pirate_event.dm b/code/modules/antagonists/pirate/pirate_event.dm
index 30dc8c84f31b..505292612241 100644
--- a/code/modules/antagonists/pirate/pirate_event.dm
+++ b/code/modules/antagonists/pirate/pirate_event.dm
@@ -9,11 +9,9 @@
description = "The crew will either pay up, or face a pirate assault."
admin_setup = list(/datum/event_admin_setup/listed_options/pirates)
map_flags = EVENT_SPACE_ONLY
-
-/datum/round_event_control/pirates/preRunEvent()
- if (!SSmapping.is_planetary())
- return EVENT_CANT_RUN
- return ..()
+ track = EVENT_TRACK_MAJOR
+ tags = list(TAG_COMBAT, TAG_COMMUNAL)
+ checks_antag_cap = TRUE
/datum/round_event/pirates
///admin chosen pirate team
diff --git a/code/modules/antagonists/wizard/wizard.dm b/code/modules/antagonists/wizard/wizard.dm
index 5f7ff984dc1f..cced5fe3f251 100644
--- a/code/modules/antagonists/wizard/wizard.dm
+++ b/code/modules/antagonists/wizard/wizard.dm
@@ -115,9 +115,6 @@ GLOBAL_LIST_EMPTY(wizard_spellbook_purchases_by_key)
RegisterSignal(ritual, COMSIG_GRAND_RITUAL_FINAL_COMPLETE, PROC_REF(on_ritual_complete))
/datum/antagonist/wizard/proc/send_to_lair()
- // And now we ensure that its loaded
- SSmapping.lazy_load_template(LAZY_TEMPLATE_KEY_WIZARDDEN)
-
if(!owner.current)
return
if(!GLOB.wizardstart.len)
diff --git a/code/modules/bitrunning/event.dm b/code/modules/bitrunning/event.dm
index 0ac35a2df8f2..daae0d15c09e 100644
--- a/code/modules/bitrunning/event.dm
+++ b/code/modules/bitrunning/event.dm
@@ -18,7 +18,7 @@
ROLE_CYBER_POLICE,
)
-/datum/round_event_control/bitrunning_glitch/can_spawn_event(players_amt, allow_magic = FALSE)
+/datum/round_event_control/bitrunning_glitch/can_spawn_event(players_amt, allow_magic = FALSE, fake_check = FALSE)
. = ..()
if(!.)
return .
diff --git a/code/modules/bitrunning/virtual_domain/domains/gondola_asteroid.dm b/code/modules/bitrunning/virtual_domain/domains/gondola_asteroid.dm
index 4deacb4f9c59..01d58e398038 100644
--- a/code/modules/bitrunning/virtual_domain/domains/gondola_asteroid.dm
+++ b/code/modules/bitrunning/virtual_domain/domains/gondola_asteroid.dm
@@ -28,11 +28,7 @@
/datum/reagent/gondola_mutation_toxin/virtual_domain
name = "Advanced Tranquility"
-
-/datum/reagent/gondola_mutation_toxin/virtual_domain/expose_mob(mob/living/exposed_mob, methods = TOUCH, reac_volume, show_message = TRUE, touch_protection = 0)
- . = ..()
- if((methods & (PATCH|INGEST|INJECT)) || ((methods & VAPOR) && prob(min(reac_volume,100)*(1 - touch_protection))))
- exposed_mob.ForceContractDisease(new /datum/disease/transformation/gondola/virtual_domain(), FALSE, TRUE)
+ gondola_disease = /datum/disease/transformation/gondola/virtual_domain
/datum/disease/transformation/gondola/virtual_domain
stage_prob = 9
diff --git a/code/modules/client/preferences/species.dm b/code/modules/client/preferences/species.dm
index a590a3334b78..1aae0e58dbbc 100644
--- a/code/modules/client/preferences/species.dm
+++ b/code/modules/client/preferences/species.dm
@@ -41,7 +41,6 @@
data[species_id]["desc"] = species.get_species_description()
data[species_id]["icon"] = sanitize_css_class_name(species.name)
data[species_id]["use_skintones"] = species.use_skintones
- data[species_id]["use_fur"] = species.use_fur
data[species_id]["sexes"] = species.sexes
data[species_id]["enabled_features"] = species.get_features()
data[species_id]["perks"] = species.get_species_perks()
diff --git a/code/modules/events/_event.dm b/code/modules/events/_event.dm
index a5a7b699bea4..7a0b3c3133aa 100644
--- a/code/modules/events/_event.dm
+++ b/code/modules/events/_event.dm
@@ -42,6 +42,21 @@
/// Flags dictating whether this event should be run on certain kinds of map
var/map_flags = NONE
+ //monkestation vars starts
+ var/roundstart = FALSE
+ var/cost = 1
+ var/reoccurence_penalty_multiplier = 0.75
+ var/shared_occurence_type
+ var/track = EVENT_TRACK_MODERATE
+ /// Last calculated weight that the storyteller assigned this event
+ var/calculated_weight = 0
+ var/tags = list() /// Tags of the event
+ /// List of the shared occurence types.
+ var/static/list/shared_occurences = list()
+ /// Whether a roundstart event can happen post roundstart. Very important for events which override job assignments.
+ var/can_run_post_roundstart = TRUE
+ //monkestation vars end
+
/datum/round_event_control/New()
if(config && !wizardevent) // Magic is unaffected by configs
earliest_start = CEILING(earliest_start * CONFIG_GET(number/events_min_time_mul), 1)
@@ -71,8 +86,10 @@
// Checks if the event can be spawned. Used by event controller and "false alarm" event.
// Admin-created events override this.
-/datum/round_event_control/proc/can_spawn_event(players_amt, allow_magic = FALSE)
+/datum/round_event_control/proc/can_spawn_event(players_amt, allow_magic = FALSE, fake_check = FALSE)
SHOULD_CALL_PARENT(TRUE)
+ if(roundstart && (world.time-SSticker.round_start_time >= 2 MINUTES || (SSgamemode.ran_roundstart && !fake_check)))
+ return FALSE
if(occurrences >= max_occurrences)
return FALSE
if(earliest_start >= world.time-SSticker.round_start_time)
@@ -88,13 +105,21 @@
if(ispath(typepath, /datum/round_event/ghost_role) && !(GLOB.ghost_role_flags & GHOSTROLE_MIDROUND_EVENT))
return FALSE
+ //monkestation edit start - STORYTELLERS
+ if(checks_antag_cap)
+ if(!roundstart && !SSgamemode.can_inject_antags())
+ return FALSE
+ if(!check_enemies())
+ return FALSE
+ //monkestation edit end - STORYTELLERS
+
var/datum/game_mode/dynamic/dynamic = SSticker.mode
if (istype(dynamic) && dynamic_should_hijack && dynamic.random_event_hijacked != HIJACKED_NOTHING)
return FALSE
return TRUE
-/datum/round_event_control/proc/preRunEvent()
+/datum/round_event_control/proc/preRunEvent(forced = FALSE)
if(!ispath(typepath, /datum/round_event))
return EVENT_CANT_RUN
@@ -106,11 +131,14 @@
// We sleep HERE, in pre-event setup (because there's no sense doing it in runEvent() since the event is already running!) for the given amount of time to make an admin has enough time to cancel an event un-fitting of the present round.
if(alert_observers)
message_admins("Random Event triggering in [DisplayTimeText(RANDOM_EVENT_ADMIN_INTERVENTION_TIME)]: [name]. (CANCEL)")
- sleep(RANDOM_EVENT_ADMIN_INTERVENTION_TIME)
+ if(!roundstart)
+ sleep(RANDOM_EVENT_ADMIN_INTERVENTION_TIME)
var/players_amt = get_active_player_count(alive_check = TRUE, afk_check = TRUE, human_check = TRUE)
- if(!can_spawn_event(players_amt))
+ if(!can_spawn_event(players_amt, fake_check = TRUE) && !forced)
message_admins("Second pre-condition check for [name] failed, skipping...")
return EVENT_INTERRUPTED
+ if(!can_spawn_event(players_amt, fake_check = TRUE) && forced)
+ message_admins("Second pre-condition check for [name] failed, but event forced, running event regardless this may have issues...")
if(!triggering)
return EVENT_CANCELLED //admin cancelled
@@ -203,8 +231,14 @@ Runs the event
var/fakeable = TRUE
/// Whether a admin wants this event to be cancelled
var/cancel_event = FALSE
+ //monkestation vars starts
///canceled on oshan
var/oshan_blocked = FALSE
+ /// Whether the event called its start() yet or not.
+ var/has_started = FALSE
+ ///have we finished setup?
+ var/setup = FALSE
+ //monkestation vars end
//Called first before processing.
//Allows you to setup your event, such as randomly
@@ -215,6 +249,7 @@ Runs the event
//This is really only for setting defaults which can be overridden later when New() finishes.
/datum/round_event/proc/setup()
SHOULD_CALL_PARENT(FALSE)
+ setup = TRUE
return
///Annouces the event name to deadchat, override this if what an event should show to deadchat is different to its event name.
@@ -228,6 +263,76 @@ Runs the event
SHOULD_CALL_PARENT(FALSE)
return
+//monkestation addition starts - STORYTELLERS
+/// This section of event processing is in a proc because roundstart events may get their start invoked.
+/datum/round_event/proc/try_start()
+ if(has_started)
+ return
+ has_started = TRUE
+ processing = FALSE
+ start()
+ processing = TRUE
+
+/datum/round_event_control/roundstart
+ roundstart = TRUE
+ earliest_start = 0
+
+///Adds an occurence. Has to use the setter to properly handle shared occurences
+/datum/round_event_control/proc/add_occurence()
+ if(shared_occurence_type)
+ if(!shared_occurences[shared_occurence_type])
+ shared_occurences[shared_occurence_type] = 0
+ shared_occurences[shared_occurence_type]++
+ occurrences++
+
+///Subtracts an occurence. Has to use the setter to properly handle shared occurences
+/datum/round_event_control/proc/subtract_occurence()
+ if(shared_occurence_type)
+ if(!shared_occurences[shared_occurence_type])
+ shared_occurences[shared_occurence_type] = 0
+ shared_occurences[shared_occurence_type]--
+ occurrences--
+
+///Gets occurences. Has to use the getter to properly handle shared occurences
+/datum/round_event_control/proc/get_occurences()
+ if(shared_occurence_type)
+ if(!shared_occurences[shared_occurence_type])
+ shared_occurences[shared_occurence_type] = 0
+ return shared_occurences[shared_occurence_type]
+ return occurrences
+
+/// Prints the action buttons for this event.
+/datum/round_event_control/proc/get_href_actions()
+ if(SSticker.HasRoundStarted())
+ if(roundstart)
+ if(!can_run_post_roundstart)
+ return "FireSchedule"
+ return "FireSchedule"
+ else
+ return "FireScheduleForce Next"
+ else
+ if(roundstart)
+ return "Add RoundstartForce Roundstart"
+ else
+ return "FireScheduleForce Next"
+
+
+/datum/round_event_control/Topic(href, href_list)
+ . = ..()
+ if(QDELETED(src))
+ return
+ switch(href_list["action"])
+ if("schedule")
+ message_admins("[key_name_admin(usr)] scheduled event [src.name].")
+ log_admin_private("[key_name(usr)] scheduled [src.name].")
+ SSgamemode.storyteller.buy_event(src, src.track)
+ if("force_next")
+ message_admins("[key_name_admin(usr)] force scheduled event [src.name].")
+ log_admin_private("[key_name(usr)] force scheduled event [src.name].")
+ SSgamemode.forced_next_events[src.track] += src
+
+//monkestation addition ends - STORYTELLERS
+
//Called after something followable has been spawned by an event
//Provides ghosts a follow link to an atom if possible
//Only called once.
@@ -265,6 +370,8 @@ Runs the event
//This proc will handle the calls to the appropiate procs.
/datum/round_event/process()
SHOULD_NOT_OVERRIDE(TRUE)
+ if(!setup)
+ return
if(!processing)
return
diff --git a/code/modules/events/anomaly/_anomaly.dm b/code/modules/events/anomaly/_anomaly.dm
index 13fedcde2768..eaa9f4122787 100644
--- a/code/modules/events/anomaly/_anomaly.dm
+++ b/code/modules/events/anomaly/_anomaly.dm
@@ -24,6 +24,7 @@
impact_area = get_area(spawn_location)
else
impact_area = placer.findValidArea()
+ setup = TRUE
/datum/round_event/anomaly/announce(fake)
priority_announce("Energetic flux wave detected on [ANOMALY_ANNOUNCE_DANGEROUS_TEXT] [impact_area.name].", "Anomaly Alert")
diff --git a/code/modules/events/aurora_caelus.dm b/code/modules/events/aurora_caelus.dm
index 8ce6fcdd89a3..83b3612b9b2a 100644
--- a/code/modules/events/aurora_caelus.dm
+++ b/code/modules/events/aurora_caelus.dm
@@ -6,8 +6,10 @@
earliest_start = 5 MINUTES
category = EVENT_CATEGORY_FRIENDLY
description = "A colourful display can be seen through select windows. And the kitchen."
+ track = EVENT_TRACK_MUNDANE
+ tags = list(TAG_COMMUNAL, TAG_POSITIVE, TAG_SPACE)
-/datum/round_event_control/aurora_caelus/can_spawn_event(players, allow_magic = FALSE)
+/datum/round_event_control/aurora_caelus/can_spawn_event(players, allow_magic = FALSE, fake_check = FALSE)
if(!SSmapping.empty_space)
return FALSE
return ..()
diff --git a/code/modules/events/brain_trauma.dm b/code/modules/events/brain_trauma.dm
index 77d17ce5ecbb..40df052b1ecd 100644
--- a/code/modules/events/brain_trauma.dm
+++ b/code/modules/events/brain_trauma.dm
@@ -6,6 +6,8 @@
description = "A crewmember gains a random trauma."
min_wizard_trigger_potency = 2
max_wizard_trigger_potency = 6
+ track = EVENT_TRACK_MUNDANE
+ tags = list(TAG_TARGETED)
/datum/round_event/brain_trauma
fakeable = FALSE
diff --git a/code/modules/events/brand_intelligence.dm b/code/modules/events/brand_intelligence.dm
index 813be9b3d8ba..689878896eeb 100644
--- a/code/modules/events/brand_intelligence.dm
+++ b/code/modules/events/brand_intelligence.dm
@@ -9,6 +9,8 @@
min_wizard_trigger_potency = 2
max_wizard_trigger_potency = 6
admin_setup = list(/datum/event_admin_setup/listed_options/brand_intelligence)
+ track = EVENT_TRACK_MODERATE
+ tags = list(TAG_DESTRUCTIVE, TAG_COMMUNAL)
/datum/round_event/brand_intelligence
announce_when = 21
@@ -46,6 +48,7 @@
kill()
return
origin_machine = pick_n_take(vending_machines)
+ setup = TRUE
/datum/round_event/brand_intelligence/announce(fake)
priority_announce("Rampant brand intelligence has been detected aboard [station_name()]. Please inspect any [origin_machine] brand vendors for aggressive marketing tactics, and reboot them if necessary.", "Machine Learning Alert")
diff --git a/code/modules/events/bureaucratic_error.dm b/code/modules/events/bureaucratic_error.dm
index 5f206134a921..6b9b62efd945 100644
--- a/code/modules/events/bureaucratic_error.dm
+++ b/code/modules/events/bureaucratic_error.dm
@@ -5,6 +5,8 @@
weight = 5
category = EVENT_CATEGORY_BUREAUCRATIC
description = "Randomly opens and closes job slots, along with changing the overflow role."
+ track = EVENT_TRACK_MAJOR // if you've ever dealt with 10 mimes you understand why.
+ tags = list(TAG_COMMUNAL)
/datum/round_event/bureaucratic_error
announce_when = 1
@@ -22,10 +24,11 @@
var/datum/job/current = job
if(!current.allow_bureaucratic_error)
continue
- current.total_positions = 0
+ var/ran = rand(-2,4)
+ current.total_positions = max(current.total_positions + ran, 1)
else // Adds/removes a random amount of job slots from all jobs.
for(var/datum/job/current as anything in jobs)
if(!current.allow_bureaucratic_error)
continue
var/ran = rand(-2,4)
- current.total_positions = max(current.total_positions + ran, 0)
+ current.total_positions = max(current.total_positions + ran, 1)
diff --git a/code/modules/events/camerafailure.dm b/code/modules/events/camerafailure.dm
index 453b919c5b88..7459a575dc05 100644
--- a/code/modules/events/camerafailure.dm
+++ b/code/modules/events/camerafailure.dm
@@ -6,6 +6,8 @@
alert_observers = FALSE
category = EVENT_CATEGORY_ENGINEERING
description = "Turns off a random amount of cameras."
+ track = EVENT_TRACK_MUNDANE
+ tags = list(TAG_COMMUNAL, TAG_SPOOKY)
/datum/round_event/camera_failure
fakeable = FALSE
diff --git a/code/modules/events/carp_migration.dm b/code/modules/events/carp_migration.dm
index e62d26598c23..17f083a97d4d 100644
--- a/code/modules/events/carp_migration.dm
+++ b/code/modules/events/carp_migration.dm
@@ -10,6 +10,8 @@
min_wizard_trigger_potency = 0
max_wizard_trigger_potency = 3
admin_setup = list(/datum/event_admin_setup/carp_migration)
+ track = EVENT_TRACK_MODERATE
+ tags = list(TAG_COMMUNAL)
/datum/round_event_control/carp_migration/New()
. = ..()
@@ -35,6 +37,7 @@
/datum/round_event/carp_migration/setup()
start_when = rand(40, 60)
+ setup = TRUE
/datum/round_event/carp_migration/announce(fake)
priority_announce("[fluff_signal] have been detected near [station_name()], please stand-by.", "Lifesign Alert")
diff --git a/code/modules/events/communications_blackout.dm b/code/modules/events/communications_blackout.dm
index 0747998e6744..f0e7c4694aa0 100644
--- a/code/modules/events/communications_blackout.dm
+++ b/code/modules/events/communications_blackout.dm
@@ -6,6 +6,8 @@
description = "Heavily emps all telecommunication machines, blocking all communication for a while."
min_wizard_trigger_potency = 0
max_wizard_trigger_potency = 3
+ track = EVENT_TRACK_MODERATE
+ tags = list(TAG_COMMUNAL, TAG_SPOOKY)
/datum/round_event/communications_blackout
announce_when = 1
diff --git a/code/modules/events/creep_awakening.dm b/code/modules/events/creep_awakening.dm
index 648c2cc9db04..a519ee9430bd 100644
--- a/code/modules/events/creep_awakening.dm
+++ b/code/modules/events/creep_awakening.dm
@@ -5,6 +5,8 @@
min_players = 20
category = EVENT_CATEGORY_HEALTH
description = "A random crewmember becomes obsessed with another."
+ track = EVENT_TRACK_MODERATE
+ tags = list(TAG_CREW_ANTAG, TAG_TARGETED)
/datum/round_event/obsessed
fakeable = FALSE
diff --git a/code/modules/events/disease_outbreak.dm b/code/modules/events/disease_outbreak.dm
index f57ed1774d4c..7348875be007 100644
--- a/code/modules/events/disease_outbreak.dm
+++ b/code/modules/events/disease_outbreak.dm
@@ -32,8 +32,10 @@
admin_setup = list(/datum/event_admin_setup/minimum_candidate_requirement/disease_outbreak, /datum/event_admin_setup/listed_options/disease_outbreak)
///Disease recipient candidates
var/list/disease_candidates = list()
+ track = EVENT_TRACK_MUNDANE
+ tags = list(TAG_TARGETED)
-/datum/round_event_control/disease_outbreak/can_spawn_event(players_amt, allow_magic = FALSE)
+/datum/round_event_control/disease_outbreak/can_spawn_event(players_amt, allow_magic = FALSE, fake_check = FALSE)
. = ..()
if(!.)
return .
@@ -100,6 +102,7 @@
/datum/round_event/disease_outbreak/setup()
announce_when = ADV_ANNOUNCE_DELAY
+ setup = TRUE
/datum/round_event/disease_outbreak/start()
var/datum/round_event_control/disease_outbreak/disease_event = control
@@ -255,6 +258,7 @@
/datum/round_event/disease_outbreak/advance/setup()
announce_when = ADV_ANNOUNCE_DELAY
+ setup = TRUE
/**
* Generate advanced virus
diff --git a/code/modules/events/dust.dm b/code/modules/events/dust.dm
index c12b86d5803c..71f97a4e2fbc 100644
--- a/code/modules/events/dust.dm
+++ b/code/modules/events/dust.dm
@@ -1,13 +1,15 @@
/datum/round_event_control/space_dust
name = "Space Dust: Minor"
typepath = /datum/round_event/space_dust
- weight = 200
- max_occurrences = 1000
+ weight = 200 // why the hell was this so much
+ max_occurrences = 10
earliest_start = 0 MINUTES
alert_observers = FALSE
category = EVENT_CATEGORY_SPACE
description = "A single space dust is hurled at the station."
map_flags = EVENT_SPACE_ONLY
+ track = EVENT_TRACK_MUNDANE
+ tags = list(TAG_DESTRUCTIVE, TAG_SPACE)
/datum/round_event/space_dust
start_when = 1
diff --git a/code/modules/events/electrical_storm.dm b/code/modules/events/electrical_storm.dm
index 1309887d4b73..8ec4e485bcbc 100644
--- a/code/modules/events/electrical_storm.dm
+++ b/code/modules/events/electrical_storm.dm
@@ -8,6 +8,8 @@
description = "Destroys all lights in a large area."
min_wizard_trigger_potency = 0
max_wizard_trigger_potency = 4
+ track = EVENT_TRACK_MUNDANE
+ tags = list(TAG_SPOOKY)
/datum/round_event/electrical_storm
var/lightsoutAmount = 1
diff --git a/code/modules/events/fake_virus.dm b/code/modules/events/fake_virus.dm
index fb6bfd5be975..36ce68dd61cf 100644
--- a/code/modules/events/fake_virus.dm
+++ b/code/modules/events/fake_virus.dm
@@ -4,6 +4,8 @@
weight = 20
category = EVENT_CATEGORY_HEALTH
description = "Some crewmembers suffer from temporary hypochondria."
+ track = EVENT_TRACK_MUNDANE
+ tags = list(TAG_TARGETED)
/datum/round_event/fake_virus/start()
var/list/fake_virus_victims = list()
diff --git a/code/modules/events/false_alarm.dm b/code/modules/events/false_alarm.dm
index 6e5cfdc61a1f..ff0be53939f2 100644
--- a/code/modules/events/false_alarm.dm
+++ b/code/modules/events/false_alarm.dm
@@ -6,8 +6,10 @@
category = EVENT_CATEGORY_BUREAUCRATIC
description = "Fakes an event announcement."
admin_setup = list(/datum/event_admin_setup/listed_options/false_alarm)
+ track = EVENT_TRACK_MUNDANE
+ tags = list(TAG_COMMUNAL)
-/datum/round_event_control/falsealarm/can_spawn_event(players_amt, allow_magic = FALSE)
+/datum/round_event_control/falsealarm/can_spawn_event(players_amt, allow_magic = FALSE, fake_check = FALSE)
. = ..()
if(!.)
return .
diff --git a/code/modules/events/ghost_role/abductor.dm b/code/modules/events/ghost_role/abductor.dm
index f6928222cfdc..5188f3ef6293 100644
--- a/code/modules/events/ghost_role/abductor.dm
+++ b/code/modules/events/ghost_role/abductor.dm
@@ -7,6 +7,9 @@
dynamic_should_hijack = TRUE
category = EVENT_CATEGORY_INVASION
description = "One or more abductor teams spawns, and they plan to experiment on the crew."
+ track = EVENT_TRACK_MAJOR
+ tags = list(TAG_TARGETED, TAG_SPOOKY)
+ checks_antag_cap = TRUE
/datum/round_event/ghost_role/abductor
minimum_required = 2
diff --git a/code/modules/events/ghost_role/alien_infestation.dm b/code/modules/events/ghost_role/alien_infestation.dm
index 72ef044229b4..e76e8c8ccb11 100644
--- a/code/modules/events/ghost_role/alien_infestation.dm
+++ b/code/modules/events/ghost_role/alien_infestation.dm
@@ -5,12 +5,15 @@
min_players = 35 //monkie edit: 10 to 35 (tg what the fuck)
- earliest_start = 90 MINUTES //monkie edit: 20 to 90
+ earliest_start = 60 MINUTES //monkie edit: 20 to 90
dynamic_should_hijack = TRUE
category = EVENT_CATEGORY_ENTITIES
description = "A xenomorph larva spawns on a random vent."
+ track = EVENT_TRACK_MAJOR
+ tags = list(TAG_COMBAT)
+ checks_antag_cap = TRUE
-/datum/round_event_control/alien_infestation/can_spawn_event(players_amt, allow_magic = FALSE)
+/datum/round_event_control/alien_infestation/can_spawn_event(players_amt, allow_magic = FALSE, fake_check = FALSE)
. = ..()
if(!.)
return .
@@ -34,7 +37,7 @@
announce_when = rand(announce_when, announce_when + 50)
if(prob(50))
spawncount++
-
+ setup = TRUE
/datum/round_event/ghost_role/alien_infestation/announce(fake)
var/living_aliens = FALSE
for(var/mob/living/carbon/alien/A in GLOB.player_list)
diff --git a/code/modules/events/ghost_role/blob.dm b/code/modules/events/ghost_role/blob.dm
index a07d9d1be594..2278ca8cca18 100644
--- a/code/modules/events/ghost_role/blob.dm
+++ b/code/modules/events/ghost_role/blob.dm
@@ -6,12 +6,15 @@
min_players = 35 //monkie edit: 20 to 35
- earliest_start = 90 MINUTES //monkie edit: 20 to 90
+ earliest_start = 60 MINUTES //monkie edit: 20 to 90
dynamic_should_hijack = TRUE
category = EVENT_CATEGORY_ENTITIES
description = "Spawns a new blob overmind."
+ track = EVENT_TRACK_MAJOR
+ tags = list(TAG_DESTRUCTIVE, TAG_COMBAT)
+ checks_antag_cap = TRUE
-/datum/round_event_control/blob/can_spawn_event(players, allow_magic = FALSE)
+/datum/round_event_control/blob/can_spawn_event(players, allow_magic = FALSE, fake_check = FALSE)
if(EMERGENCY_PAST_POINT_OF_NO_RETURN) // no blobs if the shuttle is past the point of no return
return FALSE
diff --git a/code/modules/events/ghost_role/changeling_event.dm b/code/modules/events/ghost_role/changeling_event.dm
index 570a6166093d..3bd3770da4b7 100644
--- a/code/modules/events/ghost_role/changeling_event.dm
+++ b/code/modules/events/ghost_role/changeling_event.dm
@@ -14,6 +14,9 @@
dynamic_should_hijack = TRUE
category = EVENT_CATEGORY_ENTITIES
description = "A meteor containing a changeling is summoned and thrown at the exterior of the station."
+ track = EVENT_TRACK_MAJOR
+ tags = list(TAG_COMBAT)
+ checks_antag_cap = TRUE
/datum/round_event/ghost_role/changeling
minimum_required = 1
diff --git a/code/modules/events/ghost_role/fugitive_event.dm b/code/modules/events/ghost_role/fugitive_event.dm
index 24aa4798c79c..08f1390889aa 100644
--- a/code/modules/events/ghost_role/fugitive_event.dm
+++ b/code/modules/events/ghost_role/fugitive_event.dm
@@ -9,6 +9,9 @@
category = EVENT_CATEGORY_INVASION
description = "Fugitives will hide on the station, followed by hunters."
map_flags = EVENT_SPACE_ONLY
+ track = EVENT_TRACK_MAJOR
+ tags = list(TAG_COMBAT)
+ checks_antag_cap = TRUE
/datum/round_event/ghost_role/fugitives
minimum_required = 1
diff --git a/code/modules/events/ghost_role/morph_event.dm b/code/modules/events/ghost_role/morph_event.dm
index e0b75119c282..8ec6b9a0d72c 100644
--- a/code/modules/events/ghost_role/morph_event.dm
+++ b/code/modules/events/ghost_role/morph_event.dm
@@ -7,6 +7,9 @@
description = "Spawns a hungry shapeshifting blobby creature."
min_wizard_trigger_potency = 4
max_wizard_trigger_potency = 7
+ track = EVENT_TRACK_ROLESET
+ tags = list(TAG_COMBAT, TAG_SPOOKY)
+ checks_antag_cap = TRUE
/datum/round_event/ghost_role/morph
minimum_required = 1
diff --git a/code/modules/events/ghost_role/nightmare.dm b/code/modules/events/ghost_role/nightmare.dm
index 57b942988cdb..20a70e542454 100644
--- a/code/modules/events/ghost_role/nightmare.dm
+++ b/code/modules/events/ghost_role/nightmare.dm
@@ -8,6 +8,9 @@
description = "Spawns a nightmare, aiming to darken the station."
min_wizard_trigger_potency = 6
max_wizard_trigger_potency = 7
+ track = EVENT_TRACK_ROLESET
+ tags = list(TAG_COMBAT, TAG_SPOOKY)
+ checks_antag_cap = TRUE
/datum/round_event/ghost_role/nightmare
minimum_required = 1
diff --git a/code/modules/events/ghost_role/operative.dm b/code/modules/events/ghost_role/operative.dm
index 4cf8da639bb5..4a8d4d668fd9 100644
--- a/code/modules/events/ghost_role/operative.dm
+++ b/code/modules/events/ghost_role/operative.dm
@@ -5,6 +5,9 @@
max_occurrences = 1
category = EVENT_CATEGORY_INVASION
description = "A single nuclear operative assaults the station."
+ track = EVENT_TRACK_MAJOR
+ tags = list(TAG_DESTRUCTIVE, TAG_COMBAT)
+ checks_antag_cap = TRUE
/datum/round_event/ghost_role/operative
minimum_required = 1
diff --git a/code/modules/events/ghost_role/revenant_event.dm b/code/modules/events/ghost_role/revenant_event.dm
index 27f3597a7ad2..b15d6e9aa543 100644
--- a/code/modules/events/ghost_role/revenant_event.dm
+++ b/code/modules/events/ghost_role/revenant_event.dm
@@ -11,7 +11,9 @@
description = "Spawns an angry, soul sucking ghost."
min_wizard_trigger_potency = 4
max_wizard_trigger_potency = 7
-
+ track = EVENT_TRACK_ROLESET
+ tags = list(TAG_DESTRUCTIVE, TAG_SPOOKY)
+ checks_antag_cap = TRUE
/datum/round_event/ghost_role/revenant
var/ignore_mobcheck = FALSE
diff --git a/code/modules/events/ghost_role/sentience.dm b/code/modules/events/ghost_role/sentience.dm
index 646fcbe2e9c7..cc38f7202447 100644
--- a/code/modules/events/ghost_role/sentience.dm
+++ b/code/modules/events/ghost_role/sentience.dm
@@ -22,6 +22,8 @@ GLOBAL_LIST_INIT(high_priority_sentience, typecacheof(list(
description = "An animal or robot becomes sentient!"
min_wizard_trigger_potency = 0
max_wizard_trigger_potency = 7
+ track = EVENT_TRACK_MODERATE
+ tags = list(TAG_COMMUNAL, TAG_SPOOKY)
/datum/round_event/ghost_role/sentience
diff --git a/code/modules/events/ghost_role/sentient_disease.dm b/code/modules/events/ghost_role/sentient_disease.dm
index 662f6de22c8a..b8e769ab27fe 100644
--- a/code/modules/events/ghost_role/sentient_disease.dm
+++ b/code/modules/events/ghost_role/sentient_disease.dm
@@ -9,6 +9,9 @@
description = "Spawns a sentient disease, who wants to infect as many people as possible."
min_wizard_trigger_potency = 4
max_wizard_trigger_potency = 7
+ track = EVENT_TRACK_MAJOR
+ tags = list(TAG_COMBAT, TAG_DESTRUCTIVE)
+ checks_antag_cap = TRUE
/datum/round_event/ghost_role/sentient_disease
role_name = "sentient disease"
diff --git a/code/modules/events/ghost_role/slaughter_event.dm b/code/modules/events/ghost_role/slaughter_event.dm
index 8cb2b729aa4e..b9892fb5cecc 100644
--- a/code/modules/events/ghost_role/slaughter_event.dm
+++ b/code/modules/events/ghost_role/slaughter_event.dm
@@ -10,6 +10,9 @@
description = "Spawns a slaughter demon, to hunt by travelling through pools of blood."
min_wizard_trigger_potency = 6
max_wizard_trigger_potency = 7
+ track = EVENT_TRACK_MAJOR
+ tags = list(TAG_COMBAT, TAG_SPOOKY)
+ checks_antag_cap = TRUE
/datum/round_event/ghost_role/slaughter
minimum_required = 1
diff --git a/code/modules/events/ghost_role/space_dragon.dm b/code/modules/events/ghost_role/space_dragon.dm
index 735d6d1dcb8f..8b579abad17d 100644
--- a/code/modules/events/ghost_role/space_dragon.dm
+++ b/code/modules/events/ghost_role/space_dragon.dm
@@ -10,6 +10,9 @@
description = "Spawns a space dragon, which will try to take over the station."
min_wizard_trigger_potency = 6
max_wizard_trigger_potency = 7
+ track = EVENT_TRACK_ROLESET
+ tags = list(TAG_COMBAT)
+ checks_antag_cap = TRUE
/datum/round_event/ghost_role/space_dragon
minimum_required = 1
diff --git a/code/modules/events/ghost_role/space_ninja.dm b/code/modules/events/ghost_role/space_ninja.dm
index a14511b72779..894e092a1c21 100644
--- a/code/modules/events/ghost_role/space_ninja.dm
+++ b/code/modules/events/ghost_role/space_ninja.dm
@@ -8,6 +8,9 @@
dynamic_should_hijack = TRUE
category = EVENT_CATEGORY_INVASION
description = "A space ninja infiltrates the station."
+ track = EVENT_TRACK_ROLESET
+ tags = list(TAG_COMBAT)
+ checks_antag_cap = TRUE
/datum/round_event/ghost_role/space_ninja
minimum_required = 1
diff --git a/code/modules/events/gravity_generator_blackout.dm b/code/modules/events/gravity_generator_blackout.dm
index 89cc5a43367e..6384c8030656 100644
--- a/code/modules/events/gravity_generator_blackout.dm
+++ b/code/modules/events/gravity_generator_blackout.dm
@@ -6,8 +6,10 @@
description = "Turns off the gravity generator."
min_wizard_trigger_potency = 0
max_wizard_trigger_potency = 4
+ track = EVENT_TRACK_MODERATE
+ tags = list(TAG_COMMUNAL, TAG_SPACE)
-/datum/round_event_control/gravity_generator_blackout/can_spawn_event(players_amt, allow_magic = FALSE)
+/datum/round_event_control/gravity_generator_blackout/can_spawn_event(players_amt, allow_magic = FALSE, fake_check = FALSE)
. = ..()
if(!.)
return .
diff --git a/code/modules/events/grey_tide.dm b/code/modules/events/grey_tide.dm
index 7c7e23950a96..d09f76ebe393 100644
--- a/code/modules/events/grey_tide.dm
+++ b/code/modules/events/grey_tide.dm
@@ -8,6 +8,8 @@
description = "Bolts open all doors in one or more departments."
min_wizard_trigger_potency = 0
max_wizard_trigger_potency = 7
+ track = EVENT_TRACK_MODERATE
+ tags = list(TAG_DESTRUCTIVE, TAG_SPOOKY)
/datum/round_event/grey_tide
announce_when = 50
@@ -21,6 +23,7 @@
announce_when = rand(50, 60)
end_when = rand(20, 30)
severity = rand(1,3)
+ setup = TRUE
var/list/potential_areas = list(/area/station/command,
/area/station/engineering,
diff --git a/code/modules/events/grid_check.dm b/code/modules/events/grid_check.dm
index ecc70df98bcd..2743c4d839d9 100644
--- a/code/modules/events/grid_check.dm
+++ b/code/modules/events/grid_check.dm
@@ -10,6 +10,8 @@
/// Cooldown for the announement associated with this event.
/// Necessary due to the fact that this event is player triggerable.
COOLDOWN_DECLARE(announcement_spam_protection)
+ track = EVENT_TRACK_MODERATE
+ tags = list(TAG_COMMUNAL, TAG_SPOOKY)
/datum/round_event/grid_check
announce_when = 1
diff --git a/code/modules/events/heart_attack.dm b/code/modules/events/heart_attack.dm
index 8a8902d5724c..4980225e7836 100644
--- a/code/modules/events/heart_attack.dm
+++ b/code/modules/events/heart_attack.dm
@@ -11,8 +11,10 @@
admin_setup = list(/datum/event_admin_setup/minimum_candidate_requirement/heart_attack, /datum/event_admin_setup/input_number/heart_attack)
///Candidates for recieving a healthy dose of heart disease
var/list/heart_attack_candidates = list()
+ track = EVENT_TRACK_MODERATE
+ tags = list(TAG_TARGETED)
-/datum/round_event_control/heart_attack/can_spawn_event(players_amt, allow_magic = FALSE)
+/datum/round_event_control/heart_attack/can_spawn_event(players_amt, allow_magic = FALSE, fake_check = FALSE)
. = ..()
if(!.)
return .
diff --git a/code/modules/events/immovable_rod/immovable_rod_event.dm b/code/modules/events/immovable_rod/immovable_rod_event.dm
index 63d2e79b3911..0b8437cf4f9d 100644
--- a/code/modules/events/immovable_rod/immovable_rod_event.dm
+++ b/code/modules/events/immovable_rod/immovable_rod_event.dm
@@ -11,6 +11,8 @@
min_wizard_trigger_potency = 6
max_wizard_trigger_potency = 7
admin_setup = list(/datum/event_admin_setup/set_location/immovable_rod, /datum/event_admin_setup/question/immovable_rod)
+ track = EVENT_TRACK_MODERATE
+ tags = list(TAG_DESTRUCTIVE)
/datum/round_event/immovable_rod
announce_when = 5
diff --git a/code/modules/events/ion_storm.dm b/code/modules/events/ion_storm.dm
index 772d1576cdc0..f2edeac485a6 100644
--- a/code/modules/events/ion_storm.dm
+++ b/code/modules/events/ion_storm.dm
@@ -7,6 +7,8 @@
description = "Gives the AI a new, randomized law."
min_wizard_trigger_potency = 2
max_wizard_trigger_potency = 7
+ track = EVENT_TRACK_MODERATE
+ tags = list(TAG_TARGETED)
/datum/round_event/ion_storm
var/replaceLawsetChance = 25 //chance the AI's lawset is completely replaced with something else per config weights
diff --git a/code/modules/events/mass_hallucination.dm b/code/modules/events/mass_hallucination.dm
index 85b1ef024775..2e2baf9a85f7 100644
--- a/code/modules/events/mass_hallucination.dm
+++ b/code/modules/events/mass_hallucination.dm
@@ -9,6 +9,8 @@
min_wizard_trigger_potency = 0
max_wizard_trigger_potency = 2
admin_setup = list(/datum/event_admin_setup/mass_hallucination)
+ track = EVENT_TRACK_MODERATE
+ tags = list(TAG_COMMUNAL)
/datum/round_event/mass_hallucination
fakeable = FALSE
diff --git a/code/modules/events/meteors/meteor_wave_events.dm b/code/modules/events/meteors/meteor_wave_events.dm
index e45c2b3fda97..475e3265b8c8 100644
--- a/code/modules/events/meteors/meteor_wave_events.dm
+++ b/code/modules/events/meteors/meteor_wave_events.dm
@@ -10,6 +10,8 @@
category = EVENT_CATEGORY_SPACE
description = "A regular meteor wave."
map_flags = EVENT_SPACE_ONLY
+ track = EVENT_TRACK_MAJOR
+ tags = list(TAG_COMMUNAL, TAG_SPACE, TAG_DESTRUCTIVE)
/datum/round_event/meteor_wave
start_when = 6
diff --git a/code/modules/events/meteors/stray_meteor_event.dm b/code/modules/events/meteors/stray_meteor_event.dm
index 53daff1ac8a1..849be38b6a09 100644
--- a/code/modules/events/meteors/stray_meteor_event.dm
+++ b/code/modules/events/meteors/stray_meteor_event.dm
@@ -11,6 +11,8 @@
max_wizard_trigger_potency = 7
admin_setup = list(/datum/event_admin_setup/listed_options/stray_meteor)
map_flags = EVENT_SPACE_ONLY
+ track = EVENT_TRACK_MODERATE
+ tags = list(TAG_DESTRUCTIVE, TAG_SPACE)
/datum/round_event/stray_meteor
announce_when = 1
diff --git a/code/modules/events/mice_migration.dm b/code/modules/events/mice_migration.dm
index e7f31567f4c9..038ee8bcb193 100644
--- a/code/modules/events/mice_migration.dm
+++ b/code/modules/events/mice_migration.dm
@@ -4,6 +4,8 @@
weight = 10
category = EVENT_CATEGORY_ENTITIES
description = "A horde of mice arrives, and perhaps even the Rat King themselves."
+ track = EVENT_TRACK_MUNDANE
+ tags = list(TAG_DESTRUCTIVE)
/datum/round_event/mice_migration
var/minimum_mice = 5
diff --git a/code/modules/events/portal_storm.dm b/code/modules/events/portal_storm.dm
index 5f2bca5071ba..b5771d5075a0 100644
--- a/code/modules/events/portal_storm.dm
+++ b/code/modules/events/portal_storm.dm
@@ -3,9 +3,12 @@
typepath = /datum/round_event/portal_storm/syndicate_shocktroop
weight = 2
min_players = 15
+ max_occurrences = 5
earliest_start = 30 MINUTES
category = EVENT_CATEGORY_ENTITIES
description = "Syndicate troops pour out of portals."
+ track = EVENT_TRACK_MAJOR
+ tags = list(TAG_COMBAT)
/datum/round_event/portal_storm/syndicate_shocktroop
boss_types = list(/mob/living/basic/syndicate/melee/space/stormtrooper = 2)
@@ -65,6 +68,7 @@
hostiles_spawn += get_random_station_turf()
next_boss_spawn = start_when + CEILING(2 * number_of_hostiles / number_of_bosses, 1)
+ setup = TRUE
/datum/round_event/portal_storm/announce(fake)
set waitfor = 0
diff --git a/code/modules/events/processor_overload.dm b/code/modules/events/processor_overload.dm
index ebcbb27f2781..9a18da1bd3c9 100644
--- a/code/modules/events/processor_overload.dm
+++ b/code/modules/events/processor_overload.dm
@@ -5,6 +5,8 @@
min_players = 20
category = EVENT_CATEGORY_ENGINEERING
description = "Emps the telecomm processors, scrambling radio speech. Might blow up a few."
+ track = EVENT_TRACK_MODERATE
+ tags = list(TAG_COMMUNAL)
/datum/round_event/processor_overload
announce_when = 1
diff --git a/code/modules/events/radiation_leak.dm b/code/modules/events/radiation_leak.dm
index 0fbe29927666..a94c8fe03e1a 100644
--- a/code/modules/events/radiation_leak.dm
+++ b/code/modules/events/radiation_leak.dm
@@ -8,6 +8,8 @@
category = EVENT_CATEGORY_ENGINEERING
min_wizard_trigger_potency = 3
max_wizard_trigger_potency = 7
+ track = EVENT_TRACK_MODERATE
+ tags = list(TAG_COMMUNAL)
/datum/round_event/radiation_leak
start_when = 1 // 2 seconds in
@@ -47,6 +49,7 @@
// We found something, we can just return now
picked_machine_ref = WEAKREF(sick_device)
return
+ setup = TRUE
/datum/round_event/radiation_leak/announce(fake)
var/obj/machinery/the_source_of_our_problems = picked_machine_ref?.resolve()
diff --git a/code/modules/events/radiation_storm.dm b/code/modules/events/radiation_storm.dm
index 5b2b6b71ea1d..8d4be6c8299b 100644
--- a/code/modules/events/radiation_storm.dm
+++ b/code/modules/events/radiation_storm.dm
@@ -1,11 +1,13 @@
/datum/round_event_control/radiation_storm
name = "Radiation Storm"
typepath = /datum/round_event/radiation_storm
- max_occurrences = 1
+ max_occurrences = 2 //monkestation edit - STORYTELLERS
category = EVENT_CATEGORY_SPACE
description = "Radiation storm affects the station, forcing the crew to escape to maintenance."
min_wizard_trigger_potency = 3
max_wizard_trigger_potency = 7
+ track = EVENT_TRACK_MODERATE
+ tags = list(TAG_COMMUNAL)
/datum/round_event/radiation_storm
@@ -14,6 +16,7 @@
start_when = 3
end_when = start_when + 1
announce_when = 1
+ setup = TRUE
/datum/round_event/radiation_storm/announce(fake)
priority_announce("High levels of radiation detected near the station. Maintenance is best shielded from radiation.", "Anomaly Alert", ANNOUNCER_RADIATION)
diff --git a/code/modules/events/sandstorm.dm b/code/modules/events/sandstorm.dm
index 0288b32ec207..fc141315bb4b 100644
--- a/code/modules/events/sandstorm.dm
+++ b/code/modules/events/sandstorm.dm
@@ -19,6 +19,8 @@
max_wizard_trigger_potency = 7
admin_setup = list(/datum/event_admin_setup/listed_options/sandstorm)
map_flags = EVENT_SPACE_ONLY
+ track = EVENT_TRACK_MODERATE
+ tags = list(TAG_DESTRUCTIVE)
/datum/round_event/sandstorm
start_when = 60
@@ -30,6 +32,7 @@
/datum/round_event/sandstorm/setup()
start_when = rand(70, 90)
end_when = rand(110, 140)
+ setup = TRUE
/datum/round_event/sandstorm/announce(fake)
if(!start_side)
diff --git a/code/modules/events/scrubber_clog.dm b/code/modules/events/scrubber_clog.dm
index 0bbb0801182a..7a332e67cf5d 100644
--- a/code/modules/events/scrubber_clog.dm
+++ b/code/modules/events/scrubber_clog.dm
@@ -6,6 +6,8 @@
earliest_start = 5 MINUTES
category = EVENT_CATEGORY_JANITORIAL
description = "Harmless mobs climb out of a scrubber."
+ track = EVENT_TRACK_MUNDANE
+ tags = list(TAG_COMMUNAL)
/datum/round_event/scrubber_clog
announce_when = 10
@@ -40,6 +42,7 @@
end_when = rand(300, 600)
maximum_spawns = rand(3, 5)
spawn_delay = rand(10, 15)
+ setup = TRUE
/datum/round_event/scrubber_clog/start() //Sets the scrubber up for unclogging/mob production.
scrubber.clog()
@@ -86,7 +89,7 @@
scrubber_list += scrubber
return pick(scrubber_list)
-/datum/round_event_control/scrubber_clog/can_spawn_event(players_amt, allow_magic = FALSE)
+/datum/round_event_control/scrubber_clog/can_spawn_event(players_amt, allow_magic = FALSE, fake_check = FALSE)
. = ..()
if(!.)
return
diff --git a/code/modules/events/scrubber_overflow.dm b/code/modules/events/scrubber_overflow.dm
index 897d07110317..353293a6e3df 100644
--- a/code/modules/events/scrubber_overflow.dm
+++ b/code/modules/events/scrubber_overflow.dm
@@ -7,6 +7,8 @@
category = EVENT_CATEGORY_JANITORIAL
description = "The scrubbers release a tide of mostly harmless froth."
admin_setup = list(/datum/event_admin_setup/listed_options/scrubber_overflow)
+ track = EVENT_TRACK_MUNDANE
+ tags = list(TAG_COMMUNAL)
/datum/round_event/scrubber_overflow
announce_when = 1
@@ -84,8 +86,9 @@
if(!scrubbers.len)
return kill()
+ setup = TRUE
-/datum/round_event_control/scrubber_overflow/can_spawn_event(players_amt, allow_magic = FALSE)
+/datum/round_event_control/scrubber_overflow/can_spawn_event(players_amt, allow_magic = FALSE, fake_check = FALSE)
. = ..()
if(!.)
return
diff --git a/code/modules/events/shuttle_catastrophe.dm b/code/modules/events/shuttle_catastrophe.dm
index ed64c52a836a..9c8332cadebe 100644
--- a/code/modules/events/shuttle_catastrophe.dm
+++ b/code/modules/events/shuttle_catastrophe.dm
@@ -6,8 +6,10 @@
category = EVENT_CATEGORY_BUREAUCRATIC
description = "Replaces the emergency shuttle with a random one."
admin_setup = list(/datum/event_admin_setup/warn_admin/shuttle_catastrophe, /datum/event_admin_setup/listed_options/shuttle_catastrophe)
+ track = EVENT_TRACK_MODERATE
+ tags = list(TAG_COMMUNAL)
-/datum/round_event_control/shuttle_catastrophe/can_spawn_event(players, allow_magic = FALSE)
+/datum/round_event_control/shuttle_catastrophe/can_spawn_event(players, allow_magic = FALSE, fake_check = FALSE)
. = ..()
if(!.)
return .
@@ -46,6 +48,7 @@
if(!isnull(template.who_can_purchase) && template.credit_cost < INFINITY) //if we could get it from the communications console, it's cool for us to get it here
valid_shuttle_templates += template
new_shuttle = pick(valid_shuttle_templates)
+ setup = TRUE
/datum/round_event/shuttle_catastrophe/start()
if(SSshuttle.shuttle_insurance)
diff --git a/code/modules/events/shuttle_insurance.dm b/code/modules/events/shuttle_insurance.dm
index d1e39125e346..e3709f0308c5 100644
--- a/code/modules/events/shuttle_insurance.dm
+++ b/code/modules/events/shuttle_insurance.dm
@@ -6,8 +6,10 @@
max_occurrences = 1
category = EVENT_CATEGORY_BUREAUCRATIC
description = "A sketchy but legit insurance offer."
+ track = EVENT_TRACK_MODERATE
+ tags = list(TAG_COMMUNAL)
-/datum/round_event_control/shuttle_insurance/can_spawn_event(players, allow_magic = FALSE)
+/datum/round_event_control/shuttle_insurance/can_spawn_event(players, allow_magic = FALSE, fake_check = FALSE)
. = ..()
if(!.)
return .
@@ -39,6 +41,7 @@
break
if(!insurance_evaluation)
insurance_evaluation = 5000 //gee i dunno
+ setup = TRUE
/datum/round_event/shuttle_insurance/start()
insurance_message = new("Shuttle Insurance", "Hey, pal, this is the [ship_name]. Can't help but notice you're rocking a wild and crazy shuttle there with NO INSURANCE! Crazy. What if something happened to it, huh?! We've done a quick evaluation on your rates in this sector and we're offering [insurance_evaluation] to cover for your shuttle in case of any disaster.", list("Purchase Insurance.","Reject Offer."))
diff --git a/code/modules/events/shuttle_loan/shuttle_loan_event.dm b/code/modules/events/shuttle_loan/shuttle_loan_event.dm
index 96db32c044d6..ca593d3b8414 100644
--- a/code/modules/events/shuttle_loan/shuttle_loan_event.dm
+++ b/code/modules/events/shuttle_loan/shuttle_loan_event.dm
@@ -10,7 +10,7 @@
admin_setup = list(/datum/event_admin_setup/listed_options/shuttle_loan)
var/list/run_situations = list()
-/datum/round_event_control/shuttle_loan/can_spawn_event(players_amt, allow_magic = FALSE)
+/datum/round_event_control/shuttle_loan/can_spawn_event(players_amt, allow_magic = FALSE, fake_check = FALSE)
. = ..()
for(var/datum/round_event/running_event in SSevents.running)
if(istype(running_event, /datum/round_event/shuttle_loan)) //Make sure two of these don't happen at once.
@@ -37,6 +37,7 @@
loan_control.run_situations.Add(situation)
situation = new situation()
+ setup = TRUE
/datum/round_event/shuttle_loan/announce(fake)
priority_announce("Cargo: [situation.announcement_text]", situation.sender)
diff --git a/code/modules/events/space_vines/vine_event.dm b/code/modules/events/space_vines/vine_event.dm
index a668f036cac3..825244ad7f3b 100644
--- a/code/modules/events/space_vines/vine_event.dm
+++ b/code/modules/events/space_vines/vine_event.dm
@@ -14,6 +14,9 @@
/datum/event_admin_setup/input_number/spacevine_potency,
/datum/event_admin_setup/input_number/spacevine_production,
)
+ track = EVENT_TRACK_MAJOR
+ tags = list(TAG_COMMUNAL, TAG_COMBAT)
+ checks_antag_cap = TRUE
/datum/round_event/spacevine
fakeable = FALSE
@@ -64,7 +67,7 @@
/datum/event_admin_setup/set_location/spacevine/apply_to_event(datum/round_event/spacevine/event)
event.override_turf = chosen_turf
-
+
/datum/event_admin_setup/multiple_choice/spacevine
input_text = "Select starting mutations."
min_choices = 0
@@ -88,7 +91,7 @@
type_choices += text2path(choice)
event.mutations_overridden = TRUE
event.override_mutations = type_choices
-
+
/datum/event_admin_setup/input_number/spacevine_potency
input_text = "Set vine's potency (effects mutation frequency + max severity)"
max_value = 100
diff --git a/code/modules/events/spider_infestation.dm b/code/modules/events/spider_infestation.dm
index 52ad7474694d..8a600da626ea 100644
--- a/code/modules/events/spider_infestation.dm
+++ b/code/modules/events/spider_infestation.dm
@@ -10,6 +10,8 @@
description = "Spawns spider eggs, ready to hatch."
min_wizard_trigger_potency = 5
max_wizard_trigger_potency = 7
+ track = EVENT_TRACK_ROLESET
+ tags = list(TAG_COMBAT)
/datum/round_event/spider_infestation
announce_when = 400
@@ -17,6 +19,7 @@
/datum/round_event/spider_infestation/setup()
announce_when = rand(announce_when, announce_when + 50)
+ setup = TRUE
/datum/round_event/spider_infestation/announce(fake)
priority_announce("Unidentified lifesigns detected coming aboard [station_name()]. Secure any exterior access, including ducting and ventilation.", "Lifesign Alert", ANNOUNCER_ALIENS)
diff --git a/code/modules/events/stray_cargo.dm b/code/modules/events/stray_cargo.dm
index e783f18ec025..b2ec87736f3b 100644
--- a/code/modules/events/stray_cargo.dm
+++ b/code/modules/events/stray_cargo.dm
@@ -8,6 +8,8 @@
category = EVENT_CATEGORY_BUREAUCRATIC
description = "A pod containing a random supply crate lands on the station."
admin_setup = list(/datum/event_admin_setup/set_location/stray_cargo, /datum/event_admin_setup/listed_options/stray_cargo)
+ track = EVENT_TRACK_MUNDANE
+ tags = list(TAG_COMMUNAL)
/datum/event_admin_setup/set_location/stray_cargo
input_text = "Aim pod at turf we're on?"
@@ -66,6 +68,7 @@
var/datum/supply_pack/pack_type = pack
if(initial(pack_type.special))
stray_spawnable_supply_packs -= pack
+ setup = TRUE
///Spawns a random supply pack, puts it in a pod, and spawns it on a random tile of the selected area
/datum/round_event/stray_cargo/start()
diff --git a/code/modules/events/tram_malfunction.dm b/code/modules/events/tram_malfunction.dm
index b5130a8c6934..698b88171d69 100644
--- a/code/modules/events/tram_malfunction.dm
+++ b/code/modules/events/tram_malfunction.dm
@@ -11,9 +11,11 @@
description = "Tram crossing signals malfunction, tram collision damage is increased."
min_wizard_trigger_potency = 0
max_wizard_trigger_potency = 3
+ track = EVENT_TRACK_MODERATE
+ tags = list(TAG_DESTRUCTIVE)
//Check if there's a tram we can cause to malfunction.
-/datum/round_event_control/tram_malfunction/can_spawn_event(players_amt, allow_magic = FALSE)
+/datum/round_event_control/tram_malfunction/can_spawn_event(players_amt, allow_magic = FALSE, fake_check = FALSE)
. = ..()
if (!.)
return FALSE
@@ -33,6 +35,7 @@
/datum/round_event/tram_malfunction/setup()
end_when = rand(TRAM_MALFUNCTION_TIME_LOWER, TRAM_MALFUNCTION_TIME_UPPER)
+ setup = TRUE
/datum/round_event/tram_malfunction/announce()
priority_announce("Our automated control system has lost contact with the tram's on board computer. Please take extra care while we diagnose and resolve the issue. Signals and emergency braking may not be available during this time.", "CentCom Engineering Division")
diff --git a/code/modules/events/wisdomcow.dm b/code/modules/events/wisdomcow.dm
index 1ecd43797e58..7aa24b45a27f 100644
--- a/code/modules/events/wisdomcow.dm
+++ b/code/modules/events/wisdomcow.dm
@@ -10,6 +10,8 @@
/datum/event_admin_setup/listed_options/wisdom_cow,
/datum/event_admin_setup/input_number/wisdom_cow,
)
+ track = EVENT_TRACK_MUNDANE
+ tags = list(TAG_COMMUNAL, TAG_POSITIVE)
/datum/round_event/wisdomcow
///Location override that, if set causes the cow to spawn in a pre-determined locaction instead of randomly.
@@ -56,5 +58,5 @@
/datum/event_admin_setup/input_number/wisdom_cow/apply_to_event(datum/round_event/wisdomcow/event)
event.selected_experience = chosen_value
-
-
+
+
diff --git a/code/modules/events/wizard/embeddies.dm b/code/modules/events/wizard/embeddies.dm
index ea8c5fd176bc..ee4f803699a6 100644
--- a/code/modules/events/wizard/embeddies.dm
+++ b/code/modules/events/wizard/embeddies.dm
@@ -9,7 +9,7 @@
max_wizard_trigger_potency = 7
///behold... the only reason sticky is a subtype...
-/datum/round_event_control/wizard/embedpocalypse/can_spawn_event(players_amt, allow_magic = FALSE)
+/datum/round_event_control/wizard/embedpocalypse/can_spawn_event(players_amt, allow_magic = FALSE, fake_check = FALSE)
. = ..()
if(!.)
return .
diff --git a/code/modules/events/wizard/identity_spoof.dm b/code/modules/events/wizard/identity_spoof.dm
index dcd923c2776a..bdad88e9ab84 100644
--- a/code/modules/events/wizard/identity_spoof.dm
+++ b/code/modules/events/wizard/identity_spoof.dm
@@ -5,7 +5,7 @@
max_occurrences = 1
description = "Makes everyone dressed up like a wizard."
-/datum/round_event_control/wizard/identity_spoof/can_spawn_event(players_amt, allow_magic = FALSE)
+/datum/round_event_control/wizard/identity_spoof/can_spawn_event(players_amt, allow_magic = FALSE, fake_check = FALSE)
. = ..()
if(!.)
return .
diff --git a/code/modules/events/wormholes.dm b/code/modules/events/wormholes.dm
index f80d432c8920..ee96155468c4 100644
--- a/code/modules/events/wormholes.dm
+++ b/code/modules/events/wormholes.dm
@@ -10,6 +10,8 @@ GLOBAL_LIST_EMPTY(all_wormholes) // So we can pick wormholes to teleport to
description = "Space time anomalies appear on the station, randomly teleporting people who walk into them."
min_wizard_trigger_potency = 3
max_wizard_trigger_potency = 7
+ track = EVENT_TRACK_MODERATE
+ tags = list(TAG_COMMUNAL)
/datum/round_event/wormholes
announce_when = 10
@@ -23,6 +25,7 @@ GLOBAL_LIST_EMPTY(all_wormholes) // So we can pick wormholes to teleport to
/datum/round_event/wormholes/setup()
announce_when = rand(0, 20)
end_when = rand(40, 80)
+ setup = TRUE
/datum/round_event/wormholes/start()
for(var/turf/open/floor/T in world)
diff --git a/code/modules/hydroponics/seeds.dm b/code/modules/hydroponics/seeds.dm
index 88468a7a7bc3..51c7ba004a50 100644
--- a/code/modules/hydroponics/seeds.dm
+++ b/code/modules/hydroponics/seeds.dm
@@ -175,9 +175,23 @@
copy_seed.desc = desc
copy_seed.productdesc = productdesc
- copy_seed.reagents_add = reagents_add.Copy() // Faster than grabbing the list from genes.
+ copy_seed.reagents_add = reagents_add.Copy() // Fastetr than grabbing the list from genes.
copy_seed.harvest_age = harvest_age
+ copy_seed.species = species
+ copy_seed.icon_grow = icon_grow
+ copy_seed.icon_harvest = icon_harvest
+ copy_seed.icon_dead = icon_dead
+ copy_seed.growthstages = growthstages
+ copy_seed.growing_icon = growing_icon
+ copy_seed.seed_offset = seed_offset
+ copy_seed.traits_in_progress = traits_in_progress
+
+ if(istype(src, /obj/item/seeds/spliced))
+ var/obj/item/seeds/spliced/spliced_seed = src
+ var/obj/item/seeds/spliced/new_spliced_seed = copy_seed
+ new_spliced_seed.produce_list += spliced_seed.produce_list
+
return copy_seed
/obj/item/seeds/proc/get_gene(typepath)
diff --git a/code/modules/mob/living/carbon/human/_species.dm b/code/modules/mob/living/carbon/human/_species.dm
index 6506195f1acb..9949cfccbc0a 100644
--- a/code/modules/mob/living/carbon/human/_species.dm
+++ b/code/modules/mob/living/carbon/human/_species.dm
@@ -163,7 +163,8 @@ GLOBAL_LIST_EMPTY(features_by_species)
///What gas does this species breathe? Used by suffocation screen alerts, most of actual gas breathing is handled by mutantlungs. See [life.dm][code/modules/mob/living/carbon/human/life.dm]
var/breathid = "o2"
-
+ ///are we a furry little guy?
+ var/uses_fur = FALSE
///What anim to use for dusting
var/dust_anim = "dust-h"
///What anim to use for gibbing
@@ -241,8 +242,6 @@ GLOBAL_LIST_EMPTY(features_by_species)
///A list containing outfits that will be overridden in the species_equip_outfit proc. [Key = Typepath passed in] [Value = Typepath of outfit you want to equip for this specific species instead].
var/list/outfit_override_registry = list()
- ///are we furry little creatures
- var/use_fur = FALSE
///health mod of a species
var/maxhealthmod = 1
///Path to BODYTYPE_CUSTOM species worn icons. An assoc list of ITEM_SLOT_X => /icon
diff --git a/code/modules/mob/living/carbon/human/examine.dm b/code/modules/mob/living/carbon/human/examine.dm
index 1736ea531837..f893404cab6f 100644
--- a/code/modules/mob/living/carbon/human/examine.dm
+++ b/code/modules/mob/living/carbon/human/examine.dm
@@ -237,7 +237,7 @@
msg += "[t_He] look[p_s()] extremely disgusted.\n"
var/apparent_blood_volume = blood_volume
- if((dna.species.use_skintones || dna.species.use_fur)&& skin_tone == "albino")
+ if((dna.species.use_skintones)&& skin_tone == "albino")
apparent_blood_volume -= 150 // enough to knock you down one tier
switch(apparent_blood_volume)
if(BLOOD_VOLUME_OKAY to BLOOD_VOLUME_SAFE)
diff --git a/code/modules/reagents/chemistry/reagents/other_reagents.dm b/code/modules/reagents/chemistry/reagents/other_reagents.dm
index 3da8ceb7a017..2fddba00e8b2 100644
--- a/code/modules/reagents/chemistry/reagents/other_reagents.dm
+++ b/code/modules/reagents/chemistry/reagents/other_reagents.dm
@@ -2610,11 +2610,12 @@
color = "#9A6750" //RGB: 154, 103, 80
taste_description = "inner peace"
penetrates_skin = NONE
+ var/datum/disease/transformation/gondola_disease = /datum/disease/transformation/gondola
/datum/reagent/gondola_mutation_toxin/expose_mob(mob/living/exposed_mob, methods=TOUCH, reac_volume, show_message = TRUE, touch_protection = 0)
. = ..()
if((methods & (PATCH|INGEST|INJECT)) || ((methods & VAPOR) && prob(min(reac_volume,100)*(1 - touch_protection))))
- exposed_mob.ForceContractDisease(new /datum/disease/transformation/gondola(), FALSE, TRUE)
+ exposed_mob.ForceContractDisease(new gondola_disease, FALSE, TRUE)
/datum/reagent/spider_extract
diff --git a/code/modules/shuttle/emergency.dm b/code/modules/shuttle/emergency.dm
index 70eccdc78828..a9f22d0eb2d9 100644
--- a/code/modules/shuttle/emergency.dm
+++ b/code/modules/shuttle/emergency.dm
@@ -181,9 +181,6 @@
/obj/machinery/computer/emergency_shuttle/proc/increase_hijack_stage()
var/obj/docking_port/mobile/emergency/shuttle = SSshuttle.emergency
- // Begin loading this early, prevents a delay when the shuttle goes to land
- INVOKE_ASYNC(SSmapping, TYPE_PROC_REF(/datum/controller/subsystem/mapping, lazy_load_template), LAZY_TEMPLATE_KEY_NUKIEBASE)
-
shuttle.hijack_status++
if(hijack_announce)
announce_hijack_stage()
@@ -548,7 +545,6 @@
var/destination_dock = "emergency_away"
if(is_hijacked() || elimination_hijack())
// just double check
- SSmapping.lazy_load_template(LAZY_TEMPLATE_KEY_NUKIEBASE)
destination_dock = "emergency_syndicate"
minor_announce("Corruption detected in \
shuttle navigation protocols. Please contact your \
diff --git a/code/modules/surgery/bodyparts/_bodyparts.dm b/code/modules/surgery/bodyparts/_bodyparts.dm
index 2eec7eb2285b..ca2cff40edb5 100644
--- a/code/modules/surgery/bodyparts/_bodyparts.dm
+++ b/code/modules/surgery/bodyparts/_bodyparts.dm
@@ -772,12 +772,12 @@
species_flags_list = owner_species.species_traits
limb_gender = (human_owner.physique == MALE) ? "m" : "f"
- if(owner_species.use_skintones || owner_species.use_fur)
+ if(owner_species.use_skintones)
skin_tone = human_owner.skin_tone
else
skin_tone = ""
- if(((MUTCOLORS in owner_species.species_traits) || (DYNCOLORS in owner_species.species_traits))) //Ethereal code. Motherfuckers.
+ if(((MUTCOLORS in owner_species.species_traits) || (DYNCOLORS in owner_species.species_traits) || (SPECIES_FUR in owner_species.species_traits))) //Ethereal code. Motherfuckers.
if(owner_species.fixed_mut_color)
species_color = owner_species.fixed_mut_color
else
diff --git a/code/modules/unit_tests/breath.dm b/code/modules/unit_tests/breath.dm
index faba1a08e22e..8bd6e3ce9b3d 100644
--- a/code/modules/unit_tests/breath.dm
+++ b/code/modules/unit_tests/breath.dm
@@ -86,6 +86,8 @@
/datum/unit_test/breath/breath_sanity_ashwalker
/datum/unit_test/breath/breath_sanity_ashwalker/Run()
+ if(SSmapping.config.map_name == "Oshan Station")
+ return
var/mob/living/carbon/human/species/lizard/ashwalker/lab_rat = allocate(/mob/living/carbon/human/species/lizard/ashwalker)
lab_rat.forceMove(run_loc_floor_bottom_left)
var/turf/open/to_fill = run_loc_floor_bottom_left
diff --git a/code/modules/unit_tests/screenshots/screenshot_humanoids__datum_species_simian.png b/code/modules/unit_tests/screenshots/screenshot_humanoids__datum_species_simian.png
index 079c292cb9bb..e7bea2e1bb64 100644
Binary files a/code/modules/unit_tests/screenshots/screenshot_humanoids__datum_species_simian.png and b/code/modules/unit_tests/screenshots/screenshot_humanoids__datum_species_simian.png differ
diff --git a/config/events.json b/config/events.json
new file mode 100644
index 000000000000..a30ccaf58ab5
--- /dev/null
+++ b/config/events.json
@@ -0,0 +1,13 @@
+{
+ "/datum/round_event_control":
+ {
+ "weight" : 10,
+ "min_players" : 0,
+ "max_occurrences" : 100,
+ "earliest_start" : 20,
+ "track" : "Moderate",
+ "cost" : 1,
+ "reoccurence_penalty_multiplier" : 1,
+ "shared_occurence_type" : null
+ }
+}
diff --git a/config/game_options.txt b/config/game_options.txt
index ce6d491a57af..94e6203385ca 100644
--- a/config/game_options.txt
+++ b/config/game_options.txt
@@ -540,3 +540,50 @@ MAXFINE 2000
METACURRENCY_NAME Metacoin
TWITCH_KEY mrhouse
+
+## Gamemode configurations
+
+## Multipliers for points gained over time for event tracks.
+MUNDANE_POINT_GAIN_MULTIPLIER 1
+MODERATE_POINT_GAIN_MULTIPLIER 1
+MAJOR_POINT_GAIN_MULTIPLIER 1
+ROLESET_POINT_GAIN_MULTIPLIER 1
+OBJECTIVES_POINT_GAIN_MULTIPLIER 1
+
+## Multipliers for points to spend on roundstart events.
+MUNDANE_ROUNDSTART_POINT_MULTIPLIER 1
+MODERATE_ROUNDSTART_POINT_MULTIPLIER 1
+MAJOR_ROUNDSTART_POINT_MULTIPLIER 1
+ROLESET_ROUNDSTART_POINT_MULTIPLIER 1
+OBJECTIVES_ROUNDSTART_POINT_MULTIPLIER 1
+
+## Minimum population caps for event tracks to run their events.
+MUNDANE_MIN_POP 0
+MODERATE_MIN_POP 0
+MAJOR_MIN_POP 0
+ROLESET_MIN_POP 0
+OBJECTIVES_MIN_POP 0
+
+## Point thresholds for tracks to run events. The lesser the more frequent events will be.
+MUNDANE_POINT_THRESHOLD 25
+MODERATE_POINT_THRESHOLD 50
+MAJOR_POINT_THRESHOLD 90
+ROLESET_POINT_THRESHOLD 120
+OBJECTIVES_POINT_THRESHOLD 130
+
+## Allows the storyteller to scale event frequencies based on population
+ALLOW_STORYTELLER_POP_SCALING
+
+## Thresholds that population frequency scalling penalize up to.
+MUNDANE_POP_SCALE_THRESHOLD 10
+MODERATE_POP_SCALE_THRESHOLD 15
+MAJOR_POP_SCALE_THRESHOLD 40
+ROLESET_POP_SCALE_THRESHOLD 45
+OBJECTIVES_POP_SCALE_THRESHOLD 40
+
+## The maximum penalties population scalling will apply to the tracks for having less pop than POP_SCALE_THRESHOLD. This is treated as percentages
+MUNDANE_POP_SCALE_PENALTY 30
+MODERATE_POP_SCALE_PENALTY 30
+MAJOR_POP_SCALE_PENALTY 30
+ROLESET_POP_SCALE_PENALTY 30
+OBJECTIVES_POP_SCALE_PENALTY 30
diff --git a/goon/icons/obj/food.dmi b/goon/icons/obj/food.dmi
new file mode 100644
index 000000000000..c8ca42d14f49
Binary files /dev/null and b/goon/icons/obj/food.dmi differ
diff --git a/monkestation/code/__HELPERS/mobs.dm b/monkestation/code/__HELPERS/mobs.dm
deleted file mode 100644
index cf7a6bba7065..000000000000
--- a/monkestation/code/__HELPERS/mobs.dm
+++ /dev/null
@@ -1,19 +0,0 @@
-GLOBAL_LIST_INIT(fur_tone_names, list(
- "ffffff" = "Albino",
- "ffb089" = "Chimp",
- "aeafb3" = "Grey",
- "bfd0ca" = "Snow",
- "ce7d54" = "Orange",
- "c47373" = "Red",
- "f4e2d5" = "Cream"
- ))
-
-GLOBAL_LIST_INIT(fur_tones, sort_list(list(
- "ffffff",
- "ffb089",
- "aeafb3",
- "bfd0ca",
- "ce7d54",
- "c47373",
- "f4e2d5",
- )))
diff --git a/monkestation/code/modules/antagonists/florida_man/florida_events.dm b/monkestation/code/modules/antagonists/florida_man/florida_events.dm
index aa63c44d3cda..1055348961ea 100644
--- a/monkestation/code/modules/antagonists/florida_man/florida_events.dm
+++ b/monkestation/code/modules/antagonists/florida_man/florida_events.dm
@@ -3,6 +3,9 @@
typepath = /datum/round_event/ghost_role/florida_man
weight = 14
max_occurrences = 3
+ track = EVENT_TRACK_MUNDANE
+ tags = list(TAG_COMMUNAL, TAG_COMBAT)
+ checks_antag_cap = TRUE
/datum/round_event/ghost_role/florida_man
minimum_required = 1
diff --git a/monkestation/code/modules/client/preferences/species_features/simians.dm b/monkestation/code/modules/client/preferences/species_features/simians.dm
index 2c131cd44835..a4f27e181827 100644
--- a/monkestation/code/modules/client/preferences/species_features/simians.dm
+++ b/monkestation/code/modules/client/preferences/species_features/simians.dm
@@ -1,39 +1,14 @@
-/datum/preference/choiced/fur_color
+/datum/preference/color/fur_color
category = PREFERENCE_CATEGORY_SECONDARY_FEATURES
savefile_identifier = PREFERENCE_CHARACTER
savefile_key = "fur"
+ relevant_species_trait = SPECIES_FUR
-/datum/preference/choiced/fur_color/init_possible_values()
- return GLOB.fur_tones
-
-/datum/preference/choiced/fur_color/compile_constant_data()
- var/list/data = ..()
-
- data[CHOICED_PREFERENCE_DISPLAY_NAMES] = GLOB.fur_tone_names
-
- var/list/to_hex = list()
- for (var/choice in get_choices())
- var/list/hsl = rgb2num("#[choice]", COLORSPACE_HSL)
-
- to_hex[choice] = list(
- "lightness" = hsl[3],
- "value" = "#[choice]",
- )
-
- data["to_hex"] = to_hex
-
- return data
-
-/datum/preference/choiced/fur_color/apply_to_human(mob/living/carbon/human/target, value)
- if(target.dna.species.use_fur)
- target.skin_tone = value
-
-/datum/preference/choiced/fur_color/is_accessible(datum/preferences/preferences)
- if (!..(preferences))
- return FALSE
-
- var/datum/species/species_type = preferences.read_preference(/datum/preference/choiced/species)
- return initial(species_type.use_fur)
+/datum/preference/color/fur_color/apply_to_human(mob/living/carbon/human/target, value)
+ var/mob/user = usr
+ var/datum/species/species_type = user?.client.prefs.read_preference(/datum/preference/choiced/species)
+ if(initial(species_type.uses_fur))
+ target.dna.features["mcolor"] = value
/datum/preference/choiced/simian_tail
savefile_key = "feature_tail_monkey"
diff --git a/monkestation/code/modules/loadouts/items/under/under.dm b/monkestation/code/modules/loadouts/items/under/under.dm
index e3cc7646ab7a..a8b52d2c1f44 100644
--- a/monkestation/code/modules/loadouts/items/under/under.dm
+++ b/monkestation/code/modules/loadouts/items/under/under.dm
@@ -477,3 +477,7 @@ GLOBAL_LIST_INIT(loadout_miscunders, generate_loadout_items(/datum/loadout_item/
/datum/loadout_item/under/donator
donator_only = TRUE
requires_purchase = FALSE
+
+/datum/loadout_item/under/miscellaneous/shrine
+ name = "Shrine Priestess Kimono"
+ item_path = /obj/item/clothing/under/dress/shrine_priestess
diff --git a/monkestation/code/modules/mob/living/carbon/human/species_type/simian.dm b/monkestation/code/modules/mob/living/carbon/human/species_type/simian.dm
index 41a83238d5c8..5f4a06feda33 100644
--- a/monkestation/code/modules/mob/living/carbon/human/species_type/simian.dm
+++ b/monkestation/code/modules/mob/living/carbon/human/species_type/simian.dm
@@ -8,7 +8,8 @@
species_traits = list(
EYECOLOR,
LIPS,
- NO_UNDERWEAR
+ NO_UNDERWEAR,
+ SPECIES_FUR,
)
inherent_traits = list(
TRAIT_VAULTING,
@@ -17,7 +18,6 @@
)
use_skintones = FALSE
- use_fur = TRUE
inherent_biotypes = list(
MOB_ORGANIC,
@@ -30,6 +30,7 @@
skinned_type = /obj/item/stack/sheet/animalhide/monkey
disliked_food = GROSS
liked_food = FRUIT | MEAT
+ uses_fur = TRUE
//deathsound = 'monkestation/sound/voice/simian/deathsound.ogg'
species_language_holder = /datum/language_holder/monkey
maxhealthmod = 0.85 //small = weak
diff --git a/monkestation/code/modules/new_antagonists/slasher/ghost_role.dm b/monkestation/code/modules/new_antagonists/slasher/ghost_role.dm
index 216e9d103eaf..a10fe8c00c5c 100644
--- a/monkestation/code/modules/new_antagonists/slasher/ghost_role.dm
+++ b/monkestation/code/modules/new_antagonists/slasher/ghost_role.dm
@@ -1,8 +1,11 @@
/datum/round_event_control/slasher
name = "Slasher"
typepath = /datum/round_event/ghost_role/slasher
- weight = 0 // for now
- max_occurrences = 3
+ weight = 14 // for now
+ max_occurrences = 2
+ track = EVENT_TRACK_MODERATE
+ tags = list(TAG_SPOOKY, TAG_COMBAT)
+ checks_antag_cap = TRUE
/datum/round_event/ghost_role/slasher
minimum_required = 1
diff --git a/monkestation/code/modules/new_antagonists/slasher/slasher_datum.dm b/monkestation/code/modules/new_antagonists/slasher/slasher_datum.dm
index 103f96f82d2d..66f5d4a77a7d 100644
--- a/monkestation/code/modules/new_antagonists/slasher/slasher_datum.dm
+++ b/monkestation/code/modules/new_antagonists/slasher/slasher_datum.dm
@@ -154,9 +154,6 @@
if(linked_machette)
linked_machette.force += 2.5
linked_machette.throwforce += 2.5
- if(owner.current.team_monitor.tracking[stalked_human.tracking_beacon])
- qdel(owner.current.team_monitor.tracking[stalked_human.tracking_beacon])
- qdel(stalked_human.tracking_beacon)
stalked_human = null
/datum/antagonist/slasher/proc/failed_stalking()
@@ -164,7 +161,4 @@
if(linked_machette)
linked_machette.force -= 5
linked_machette.throwforce -= 5
- if(owner.current.team_monitor.tracking[stalked_human.tracking_beacon])
- qdel(owner.current.team_monitor.tracking[stalked_human.tracking_beacon])
- qdel(stalked_human.tracking_beacon)
stalked_human = null
diff --git a/monkestation/code/modules/storytellers/config.dm b/monkestation/code/modules/storytellers/config.dm
new file mode 100644
index 000000000000..121a512099d1
--- /dev/null
+++ b/monkestation/code/modules/storytellers/config.dm
@@ -0,0 +1,149 @@
+///Gamemode related configs below
+// Point Gain Multipliers
+/datum/config_entry/number/mundane_point_gain_multiplier
+ config_entry_value = 1
+ min_val = 0
+
+/datum/config_entry/number/moderate_point_gain_multiplier
+ config_entry_value = 1
+ min_val = 0
+
+/datum/config_entry/number/major_point_gain_multiplier
+ config_entry_value = 1
+ min_val = 0
+
+/datum/config_entry/number/roleset_point_gain_multiplier
+ config_entry_value = 1
+ min_val = 0
+
+/datum/config_entry/number/objectives_point_gain_multiplier
+ config_entry_value = 1
+ min_val = 0
+
+// Roundstart points Multipliers
+/datum/config_entry/number/mundane_roundstart_point_multiplier
+ config_entry_value = 1
+ min_val = 0
+
+/datum/config_entry/number/moderate_roundstart_point_multiplier
+ config_entry_value = 1
+ min_val = 0
+
+/datum/config_entry/number/major_roundstart_point_multiplier
+ config_entry_value = 1
+ min_val = 0
+
+/datum/config_entry/number/roleset_roundstart_point_multiplier
+ config_entry_value = 1
+ min_val = 0
+
+/datum/config_entry/number/objectives_roundstart_point_multiplier
+ config_entry_value = 1
+ min_val = 0
+
+// Minimum population
+/datum/config_entry/number/mundane_min_pop
+ config_entry_value = MUNDANE_MIN_POP
+ integer = TRUE
+ min_val = 0
+
+/datum/config_entry/number/moderate_min_pop
+ config_entry_value = MODERATE_MIN_POP
+ integer = TRUE
+ min_val = 0
+
+/datum/config_entry/number/major_min_pop
+ config_entry_value = MAJOR_MIN_POP
+ integer = TRUE
+ min_val = 0
+
+/datum/config_entry/number/roleset_min_pop
+ config_entry_value = ROLESET_MIN_POP
+ integer = TRUE
+ min_val = 0
+
+/datum/config_entry/number/objectives_min_pop
+ config_entry_value = OBJECTIVES_MIN_POP
+ integer = TRUE
+ min_val = 0
+
+// Point Thresholds
+/datum/config_entry/number/mundane_point_threshold
+ config_entry_value = MUNDANE_POINT_THRESHOLD
+ integer = TRUE
+ min_val = 0
+
+/datum/config_entry/number/moderate_point_threshold
+ config_entry_value = MODERATE_POINT_THRESHOLD
+ integer = TRUE
+ min_val = 0
+
+/datum/config_entry/number/major_point_threshold
+ config_entry_value = MAJOR_POINT_THRESHOLD
+ integer = TRUE
+ min_val = 0
+
+/datum/config_entry/number/roleset_point_threshold
+ config_entry_value = ROLESET_POINT_THRESHOLD
+ integer = TRUE
+ min_val = 0
+
+/datum/config_entry/number/objectives_point_threshold
+ config_entry_value = OBJECTIVES_POINT_THRESHOLD
+ integer = TRUE
+ min_val = 0
+
+
+/datum/config_entry/flag/allow_storyteller_pop_scaling // Allows storyteller to scale down the event frequency by population
+
+// Pop scalling thresholds
+/datum/config_entry/number/mundane_pop_scale_threshold
+ config_entry_value = MUNDANE_POP_SCALE_THRESHOLD
+ integer = TRUE
+ min_val = 0
+
+/datum/config_entry/number/moderate_pop_scale_threshold
+ config_entry_value = MODERATE_POP_SCALE_THRESHOLD
+ integer = TRUE
+ min_val = 0
+
+/datum/config_entry/number/major_pop_scale_threshold
+ config_entry_value = MAJOR_POP_SCALE_THRESHOLD
+ integer = TRUE
+ min_val = 0
+
+/datum/config_entry/number/roleset_pop_scale_threshold
+ config_entry_value = ROLESET_POP_SCALE_THRESHOLD
+ integer = TRUE
+ min_val = 0
+
+/datum/config_entry/number/objectives_pop_scale_threshold
+ config_entry_value = OBJECTIVES_POP_SCALE_THRESHOLD
+ integer = TRUE
+ min_val = 0
+
+// Pop scalling penalties
+/datum/config_entry/number/mundane_pop_scale_penalty
+ config_entry_value = MUNDANE_POP_SCALE_PENALTY
+ integer = TRUE
+ min_val = 0
+
+/datum/config_entry/number/moderate_pop_scale_penalty
+ config_entry_value = MODERATE_POP_SCALE_PENALTY
+ integer = TRUE
+ min_val = 0
+
+/datum/config_entry/number/major_pop_scale_penalty
+ config_entry_value = MAJOR_POP_SCALE_PENALTY
+ integer = TRUE
+ min_val = 0
+
+/datum/config_entry/number/roleset_pop_scale_penalty
+ config_entry_value = ROLESET_POP_SCALE_PENALTY
+ integer = TRUE
+ min_val = 0
+
+/datum/config_entry/number/objectives_pop_scale_penalty
+ config_entry_value = OBJECTIVES_POP_SCALE_PENALTY
+ integer = TRUE
+ min_val = 0
diff --git a/monkestation/code/modules/storytellers/converted_events/_base_event.dm b/monkestation/code/modules/storytellers/converted_events/_base_event.dm
new file mode 100644
index 000000000000..8f896a2e74cf
--- /dev/null
+++ b/monkestation/code/modules/storytellers/converted_events/_base_event.dm
@@ -0,0 +1,250 @@
+/datum/round_event_control
+ ///do we check against the antag cap before attempting a spawn?
+ var/checks_antag_cap = FALSE
+ /// List of enemy roles, will check if x amount of these exist exist
+ var/list/enemy_roles
+ ///required number of enemies in roles to exist
+ var/required_enemies = 0
+
+/datum/round_event_control/proc/return_failure_string(players_amt)
+ var/string
+ if(roundstart && (world.time-SSticker.round_start_time >= 2 MINUTES))
+ string += "Roundstart"
+ if(occurrences >= max_occurrences)
+ if(string)
+ string += ","
+ string += "Cap Reached"
+ if(earliest_start >= world.time-SSticker.round_start_time)
+ if(string)
+ string += ","
+ string +="Too Soon"
+ if(players_amt < min_players)
+ if(string)
+ string += ","
+ string += "Lack of players"
+ if(holidayID && !check_holidays(holidayID))
+ if(string)
+ string += ","
+ string += "Holiday Event"
+ if(EMERGENCY_ESCAPED_OR_ENDGAMED)
+ if(string)
+ string += ","
+ string += "Round End"
+ if(checks_antag_cap)
+ if(!roundstart && !SSgamemode.can_inject_antags())
+ if(string)
+ string += ","
+ string += "Too Many Antags"
+ return string
+
+/datum/round_event_control/antagonist/return_failure_string(players_amt)
+ . =..()
+ if(!check_enemies())
+ if(.)
+ . += ", "
+ . += "No Enemies"
+ if(!check_required())
+ if(.)
+ . += ", "
+ . += "No Required"
+ return .
+
+/datum/round_event_control/antagonist/solo/return_failure_string(players_amt)
+ . =..()
+
+ var/antag_amt = get_antag_amount()
+ var/list/candidates = get_candidates()
+ if(candidates.len < antag_amt)
+ if(.)
+ . += ", "
+ . += "Not Enough Candidates!"
+
+ return .
+
+/datum/round_event_control/antagonist
+ checks_antag_cap = TRUE
+ track = EVENT_TRACK_ROLESET
+ ///list of required roles, needed for this to form
+ var/list/exclusive_roles
+ /// Protected roles from the antag roll. People will not get those roles if a config is enabled
+ var/list/protected_roles
+ /// Restricted roles from the antag roll
+ var/list/restricted_roles
+
+/datum/round_event_control/antagonist/proc/check_required()
+ if(!length(exclusive_roles))
+ return TRUE
+ for (var/mob/M in GLOB.alive_player_list)
+ if (M.stat == DEAD)
+ continue // Dead players cannot count as passing requirements
+ if(M.mind && (M.mind.assigned_role.title in exclusive_roles))
+ return TRUE
+
+/datum/round_event_control/antagonist/proc/trim_candidates(list/candidates)
+ return candidates
+
+/datum/round_event_control/proc/check_enemies()
+ if(!length(enemy_roles))
+ return TRUE
+ var/job_check = 0
+ for (var/mob/M in GLOB.alive_player_list)
+ if (M.stat == DEAD)
+ continue // Dead players cannot count as opponents
+ if (M.mind && (M.mind.assigned_role.title in enemy_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
+
+ if(job_check >= required_enemies)
+ return TRUE
+ return FALSE
+
+/datum/round_event_control/antagonist/New()
+ . = ..()
+ if(CONFIG_GET(flag/protect_roles_from_antagonist))
+ restricted_roles |= protected_roles
+
+/datum/round_event_control/antagonist/can_spawn_event(players_amt, allow_magic = FALSE, fake_check = FALSE)
+ . = ..()
+ if(!check_required())
+ return FALSE
+
+ if(!.)
+ return
+
+/datum/round_event_control/antagonist/solo
+ typepath = /datum/round_event/antagonist/solo
+ /// How many baseline antags do we spawn
+ var/base_antags = 1
+ /// How many maximum antags can we spawn
+ var/maximum_antags = 3
+ /// For this many players we'll add 1 up to the maximum antag amount
+ var/denominator = 20
+ /// The antag flag to be used
+ var/antag_flag
+ /// The antag datum to be applied
+ var/antag_datum
+ /// Prompt players for consent to turn them into antags before doing so. Dont allow this for roundstart.
+ var/prompted_picking = FALSE
+
+/datum/round_event_control/antagonist/solo/from_ghosts/get_candidates()
+ var/round_started = SSticker.HasRoundStarted()
+ var/midround_antag_pref_arg = round_started ? FALSE : TRUE
+
+ var/list/candidates = SSgamemode.get_candidates(antag_flag, antag_flag, observers = TRUE, midround_antag_pref = midround_antag_pref_arg, restricted_roles = restricted_roles)
+ candidates = trim_candidates(candidates)
+ return candidates
+
+/datum/round_event_control/antagonist/solo/can_spawn_event(players_amt, allow_magic = FALSE, fake_check = FALSE)
+ . = ..()
+ if(!.)
+ return
+ var/antag_amt = get_antag_amount()
+ var/list/candidates = get_candidates()
+ if(candidates.len < antag_amt)
+ return FALSE
+
+/datum/round_event_control/antagonist/solo/proc/get_antag_amount()
+ var/people = SSgamemode.get_correct_popcount()
+ var/amount = base_antags + FLOOR(people / denominator, 1)
+ return min(amount, maximum_antags)
+
+/datum/round_event_control/antagonist/solo/proc/get_candidates()
+ var/round_started = SSticker.HasRoundStarted()
+ var/new_players_arg = round_started ? FALSE : TRUE
+ var/living_players_arg = round_started ? TRUE : FALSE
+ var/midround_antag_pref_arg = round_started ? FALSE : TRUE
+
+ var/list/candidates = SSgamemode.get_candidates(antag_flag, antag_flag, ready_newplayers = new_players_arg, living_players = living_players_arg, midround_antag_pref = midround_antag_pref_arg, restricted_roles = restricted_roles)
+ candidates = trim_candidates(candidates)
+ return candidates
+
+/datum/round_event
+ var/excute_round_end_reports = FALSE
+
+/datum/round_event/proc/round_end_report()
+ return
+
+/datum/round_event/setup()
+ . = ..()
+ if(excute_round_end_reports)
+ SSgamemode.round_end_data |= src
+
+/datum/round_event/antagonist
+ fakeable = FALSE
+ end_when = 60 //This is so prompted picking events have time to run //TODO: refactor events so they can be the masters of themselves, instead of relying on some weirdly timed vars
+
+/datum/round_event/antagonist/solo
+ // ALL of those variables are internal. Check the control event to change them
+ /// The antag flag passed from control
+ var/antag_flag
+ /// The antag datum passed from control
+ var/antag_datum
+ /// The antag count passed from control
+ var/antag_count
+ /// The restricted roles (jobs) passed from control
+ var/list/restricted_roles
+ /// The minds we've setup in setup() and need to finalize in start()
+ var/list/setup_minds = list()
+ /// Whether we prompt the players before picking them.
+ var/prompted_picking = FALSE //TODO: Implement this
+
+/datum/round_event/antagonist/solo/setup()
+ var/datum/round_event_control/antagonist/solo/cast_control = control
+ antag_count = cast_control.get_antag_amount()
+ antag_flag = cast_control.antag_flag
+ antag_datum = cast_control.antag_datum
+ restricted_roles = cast_control.restricted_roles
+ prompted_picking = cast_control.prompted_picking
+ var/list/candidates = cast_control.get_candidates()
+ if(prompted_picking)
+ candidates = poll_candidates("Would you like to be a [cast_control.name]", antag_flag, antag_flag, 20 SECONDS, FALSE, FALSE, candidates)
+
+ for(var/i in 1 to antag_count)
+ if(!candidates.len)
+ break
+ var/mob/candidate = pick_n_take(candidates)
+ if(!candidate.mind)
+ candidate.mind = new /datum/mind(candidate.key)
+
+ setup_minds += candidate.mind
+ candidate.mind.special_role = antag_flag
+ candidate.mind.restricted_roles = restricted_roles
+ setup = TRUE
+
+
+/datum/round_event/antagonist/solo/ghost/setup()
+ var/datum/round_event_control/antagonist/solo/cast_control = control
+ antag_count = cast_control.get_antag_amount()
+ antag_flag = cast_control.antag_flag
+ antag_datum = cast_control.antag_datum
+ restricted_roles = cast_control.restricted_roles
+ prompted_picking = cast_control.prompted_picking
+ var/list/candidates = cast_control.get_candidates()
+ if(prompted_picking)
+ candidates = poll_candidates("Would you like to be a [cast_control.name]", antag_flag, antag_flag, 20 SECONDS, FALSE, FALSE, candidates)
+
+ for(var/i in 1 to antag_count)
+ if(!candidates.len)
+ break
+ var/mob/candidate = pick_n_take(candidates)
+ if(!candidate.mind)
+ candidate.mind = new /datum/mind(candidate.key)
+
+ setup_minds += candidate.mind
+ var/mob/living/carbon/human/new_human = make_body(candidate)
+ candidate.mind.set_current(new_human)
+ candidate.mind.special_role = antag_flag
+ candidate.mind.restricted_roles = restricted_roles
+ setup = TRUE
+
+
+/datum/round_event/antagonist/solo/start()
+ for(var/datum/mind/antag_mind as anything in setup_minds)
+ add_datum_to_mind(antag_mind, antag_mind.current)
+
+/datum/round_event/antagonist/solo/proc/add_datum_to_mind(datum/mind/antag_mind)
+ antag_mind.add_antag_datum(antag_datum)
+
+/datum/round_event/antagonist/solo/ghost/start()
+ for(var/datum/mind/antag_mind as anything in setup_minds)
+ add_datum_to_mind(antag_mind)
+
diff --git a/monkestation/code/modules/storytellers/converted_events/solo/bloodcult.dm b/monkestation/code/modules/storytellers/converted_events/solo/bloodcult.dm
new file mode 100644
index 000000000000..f76463ae9e06
--- /dev/null
+++ b/monkestation/code/modules/storytellers/converted_events/solo/bloodcult.dm
@@ -0,0 +1,75 @@
+/datum/round_event_control/antagonist/solo/bloodcult
+ name = "Blood Cult"
+ tags = list(TAG_SPOOKY, TAG_DESTRUCTIVE, TAG_COMBAT, TAG_TEAM_ANTAG)
+ antag_flag = ROLE_CULTIST
+ antag_datum = /datum/antagonist/cult
+ typepath = /datum/round_event/antagonist/solo/bloodcult
+ restricted_roles = list(
+ JOB_AI,
+ JOB_CAPTAIN,
+ JOB_CHAPLAIN,
+ JOB_CYBORG,
+ JOB_DETECTIVE,
+ JOB_HEAD_OF_PERSONNEL,
+ JOB_HEAD_OF_SECURITY,
+ JOB_PRISONER,
+ JOB_SECURITY_OFFICER,
+ JOB_WARDEN,
+ )
+ maximum_antags = 3
+ enemy_roles = list(
+ JOB_CAPTAIN,
+ JOB_DETECTIVE,
+ JOB_HEAD_OF_SECURITY,
+ JOB_SECURITY_OFFICER,
+ JOB_WARDEN,
+ )
+ required_enemies = 5
+ base_antags = 2
+ // I give up, just there should be enough heads with 35 players...
+ min_players = 30
+ roundstart = TRUE
+ earliest_start = 0 SECONDS
+ weight = 4
+ max_occurrences = 3
+
+/datum/round_event/antagonist/solo/bloodcult
+ excute_round_end_reports = TRUE
+ end_when = 60000
+ var/static/datum/team/cult/main_cult
+
+/datum/round_event/antagonist/solo/bloodcult/setup()
+ . = ..()
+ if(!main_cult)
+ main_cult = new()
+
+/datum/round_event/antagonist/solo/bloodcult/start()
+ . = ..()
+ main_cult.setup_objectives()
+
+/datum/round_event/antagonist/solo/bloodcult/add_datum_to_mind(datum/mind/antag_mind)
+ var/datum/antagonist/cult/new_cultist = new antag_datum()
+ new_cultist.cult_team = main_cult
+ new_cultist.give_equipment = TRUE
+ antag_mind.add_antag_datum(new_cultist)
+
+/datum/round_event/antagonist/solo/bloodcult/round_end_report()
+ if(main_cult.check_cult_victory())
+ SSticker.mode_result = "win - cult win"
+ SSticker.news_report = CULT_SUMMON
+ return
+
+ SSticker.mode_result = "loss - staff stopped the cult"
+
+ if(main_cult.size_at_maximum == 0)
+ CRASH("Cult team existed with a size_at_maximum of 0 at round end!")
+
+ // If more than a certain ratio of our cultists have escaped, give the "cult escape" resport.
+ // Otherwise, give the "cult failure" report.
+ 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.news_report = (escaped_cultists / main_cult.size_at_maximum) >= ratio_to_be_considered_escaped ? CULT_ESCAPE : CULT_FAILURE
diff --git a/monkestation/code/modules/storytellers/converted_events/solo/bloodsuckers.dm b/monkestation/code/modules/storytellers/converted_events/solo/bloodsuckers.dm
new file mode 100644
index 000000000000..b1d19ed354f5
--- /dev/null
+++ b/monkestation/code/modules/storytellers/converted_events/solo/bloodsuckers.dm
@@ -0,0 +1,39 @@
+/datum/round_event_control/antagonist/solo/bloodsucker
+ antag_flag = ROLE_BLOODSUCKER
+ tags = list(TAG_COMBAT)
+ antag_datum = /datum/antagonist/bloodsucker
+ protected_roles = list(
+ JOB_CAPTAIN,
+ JOB_HEAD_OF_PERSONNEL,
+ JOB_CHIEF_ENGINEER,
+ JOB_CHIEF_MEDICAL_OFFICER,
+ JOB_RESEARCH_DIRECTOR,
+ JOB_DETECTIVE,
+ JOB_HEAD_OF_SECURITY,
+ JOB_PRISONER,
+ JOB_SECURITY_OFFICER,
+ JOB_WARDEN,
+ )
+ restricted_roles = list(
+ JOB_AI,
+ JOB_CYBORG,
+ )
+ min_players = 20
+ weight = 5
+ maximum_antags = 2
+
+/datum/round_event_control/antagonist/solo/bloodsucker/roundstart
+ name = "Bloodsuckers"
+ roundstart = TRUE
+ earliest_start = 0 SECONDS
+
+/datum/round_event_control/antagonist/solo/bloodsucker/midround
+ typepath = /datum/round_event/antagonist/solo/bloodsucker
+ antag_flag = ROLE_VAMPIRICACCIDENT
+ name = "Vampiric Accident"
+ prompted_picking = TRUE
+ max_occurrences = 1
+
+/datum/round_event/antagonist/solo/bloodsucker/add_datum_to_mind(datum/mind/antag_mind)
+ var/datum/antagonist/bloodsucker/bloodsuckerdatum = antag_mind.make_bloodsucker()
+ bloodsuckerdatum.bloodsucker_level_unspent = rand(2,3)
diff --git a/monkestation/code/modules/storytellers/converted_events/solo/changeling.dm b/monkestation/code/modules/storytellers/converted_events/solo/changeling.dm
new file mode 100644
index 000000000000..0171f36d42ae
--- /dev/null
+++ b/monkestation/code/modules/storytellers/converted_events/solo/changeling.dm
@@ -0,0 +1,30 @@
+/datum/round_event_control/antagonist/solo/changeling
+ antag_flag = ROLE_CHANGELING
+ tags = list(TAG_COMBAT)
+ antag_datum = /datum/antagonist/changeling
+ protected_roles = list(
+ JOB_CAPTAIN,
+ JOB_HEAD_OF_PERSONNEL,
+ JOB_CHIEF_ENGINEER,
+ JOB_CHIEF_MEDICAL_OFFICER,
+ JOB_RESEARCH_DIRECTOR,
+ JOB_DETECTIVE,
+ JOB_HEAD_OF_SECURITY,
+ JOB_PRISONER,
+ JOB_SECURITY_OFFICER,
+ JOB_WARDEN,
+ )
+ restricted_roles = list(
+ JOB_AI,
+ JOB_CYBORG,
+ )
+ min_players = 20
+
+/datum/round_event_control/antagonist/solo/changeling/roundstart
+ name = "Changelings"
+ roundstart = TRUE
+ earliest_start = 0
+
+/datum/round_event_control/antagonist/solo/changeling/midround
+ name = "Genome Awakening (Changelings)"
+ prompted_picking = TRUE
diff --git a/monkestation/code/modules/storytellers/converted_events/solo/clockwork_cult.dm b/monkestation/code/modules/storytellers/converted_events/solo/clockwork_cult.dm
new file mode 100644
index 000000000000..e19bd8f800f3
--- /dev/null
+++ b/monkestation/code/modules/storytellers/converted_events/solo/clockwork_cult.dm
@@ -0,0 +1,46 @@
+/datum/round_event_control/antagonist/solo/clockcult
+ name = "Clock Cult"
+ tags = list(TAG_SPOOKY, TAG_DESTRUCTIVE, TAG_COMBAT, TAG_TEAM_ANTAG)
+ antag_flag = ROLE_CLOCK_CULTIST
+ antag_datum = /datum/antagonist/clock_cultist
+ typepath = /datum/round_event/antagonist/solo/clockcult
+ restricted_roles = list(
+ JOB_AI,
+ JOB_CAPTAIN,
+ JOB_CHAPLAIN,
+ JOB_CYBORG,
+ JOB_DETECTIVE,
+ JOB_HEAD_OF_PERSONNEL,
+ JOB_HEAD_OF_SECURITY,
+ JOB_PRISONER,
+ JOB_SECURITY_OFFICER,
+ JOB_WARDEN,
+ )
+ maximum_antags = 3
+ enemy_roles = list(
+ JOB_CAPTAIN,
+ JOB_DETECTIVE,
+ JOB_HEAD_OF_SECURITY,
+ JOB_SECURITY_OFFICER,
+ JOB_WARDEN,
+ )
+ required_enemies = 3
+ base_antags = 4
+ maximum_antags = 4
+ // I give up, just there should be enough heads with 35 players...
+ min_players = 30
+ roundstart = TRUE
+ earliest_start = 0 SECONDS
+ weight = 4
+ max_occurrences = 3
+
+/datum/round_event/antagonist/solo/clockcult
+ end_when = 60000
+
+/datum/round_event/antagonist/solo/clockcult/setup()
+ . = ..()
+ INVOKE_ASYNC(GLOBAL_PROC, GLOBAL_PROC_REF(spawn_reebe))
+
+/datum/round_event/antagonist/solo/clockcult/add_datum_to_mind(datum/mind/antag_mind)
+ antag_mind.special_role = ROLE_CLOCK_CULTIST
+ antag_mind.add_antag_datum(antag_datum)
diff --git a/monkestation/code/modules/storytellers/converted_events/solo/clown_operative.dm b/monkestation/code/modules/storytellers/converted_events/solo/clown_operative.dm
new file mode 100644
index 000000000000..03d1866b32f4
--- /dev/null
+++ b/monkestation/code/modules/storytellers/converted_events/solo/clown_operative.dm
@@ -0,0 +1,116 @@
+/datum/round_event_control/antagonist/solo/clown_operative
+ name = "Roundstart Clown Operative"
+ tags = list(TAG_DESTRUCTIVE, TAG_COMBAT, TAG_TEAM_ANTAG)
+ antag_flag = ROLE_CLOWN_OPERATIVE
+ antag_datum = /datum/antagonist/nukeop/clownop
+ typepath = /datum/round_event/antagonist/solo/clown_operative
+ restricted_roles = list(
+ JOB_AI,
+ JOB_CAPTAIN,
+ JOB_CHIEF_ENGINEER,
+ JOB_CHIEF_MEDICAL_OFFICER,
+ JOB_CYBORG,
+ JOB_DETECTIVE,
+ JOB_HEAD_OF_PERSONNEL,
+ JOB_HEAD_OF_SECURITY,
+ JOB_PRISONER,
+ JOB_RESEARCH_DIRECTOR,
+ JOB_SECURITY_OFFICER,
+ JOB_WARDEN,
+ )
+ base_antags = 3
+ maximum_antags = 5
+ enemy_roles = list(
+ JOB_AI,
+ JOB_CYBORG,
+ JOB_CAPTAIN,
+ JOB_DETECTIVE,
+ JOB_HEAD_OF_SECURITY,
+ JOB_SECURITY_OFFICER,
+ JOB_WARDEN,
+ )
+ required_enemies = 5
+ // I give up, just there should be enough heads with 35 players...
+ min_players = 35
+ roundstart = TRUE
+ earliest_start = 0 SECONDS
+ weight = 4
+ max_occurrences = 1
+
+/datum/round_event/antagonist/solo/clown_operative
+ excute_round_end_reports = TRUE
+ end_when = 60000 /// we will end on our own when revs win
+ var/static/datum/team/nuclear/nuke_team
+ var/datum/antagonist/antag_leader_datum = /datum/antagonist/nukeop/leader
+ var/set_leader = FALSE
+ var/required_role = ROLE_CLOWN_OPERATIVE
+
+/datum/round_event/antagonist/solo/clown_operative/setup()
+ . = ..()
+ var/obj/machinery/nuclearbomb/syndicate/syndicate_nuke = locate() in GLOB.nuke_list
+ if(syndicate_nuke)
+ var/turf/nuke_turf = get_turf(syndicate_nuke)
+ if(nuke_turf)
+ new /obj/machinery/nuclearbomb/syndicate/bananium(nuke_turf)
+ qdel(syndicate_nuke)
+
+/datum/round_event/antagonist/solo/clown_operative/add_datum_to_mind(datum/mind/antag_mind)
+ var/mob/living/current_mob = antag_mind.current
+ SSjob.FreeRole(antag_mind.assigned_role.title)
+ var/list/items = current_mob.get_equipped_items(TRUE)
+ current_mob.unequip_everything()
+ for(var/obj/item/item as anything in items)
+ qdel(item)
+
+ antag_mind.set_assigned_role(SSjob.GetJobType(/datum/job/clown_operative))
+ antag_mind.special_role = ROLE_CLOWN_OPERATIVE
+
+ var/datum/mind/most_experienced = get_most_experienced(setup_minds, required_role)
+ if(!most_experienced)
+ most_experienced = antag_mind
+
+ if(!set_leader)
+ set_leader = TRUE
+ var/datum/antagonist/nukeop/leader/leader = most_experienced.add_antag_datum(antag_leader_datum)
+ nuke_team = leader.nuke_team
+
+ if(antag_mind == most_experienced)
+ return
+
+ var/datum/antagonist/nukeop/new_op = new antag_datum()
+ antag_mind.add_antag_datum(new_op)
+
+
+/datum/round_event/antagonist/solo/clown_operative/round_end_report()
+ 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
diff --git a/monkestation/code/modules/storytellers/converted_events/solo/ghosts/nuclear_operative_ghost.dm b/monkestation/code/modules/storytellers/converted_events/solo/ghosts/nuclear_operative_ghost.dm
new file mode 100644
index 000000000000..90dfba4182be
--- /dev/null
+++ b/monkestation/code/modules/storytellers/converted_events/solo/ghosts/nuclear_operative_ghost.dm
@@ -0,0 +1,103 @@
+/datum/round_event_control/antagonist/solo/from_ghosts/nuclear_operative
+ name = "Nuclear Assault"
+ tags = list(TAG_DESTRUCTIVE, TAG_COMBAT, TAG_TEAM_ANTAG)
+ antag_flag = ROLE_OPERATIVE_MIDROUND
+ antag_datum = /datum/antagonist/nukeop
+ typepath = /datum/round_event/antagonist/solo/ghost/nuclear_operative
+ restricted_roles = list(
+ JOB_AI,
+ JOB_CAPTAIN,
+ JOB_CHIEF_ENGINEER,
+ JOB_CHIEF_MEDICAL_OFFICER,
+ JOB_CYBORG,
+ JOB_DETECTIVE,
+ JOB_HEAD_OF_PERSONNEL,
+ JOB_HEAD_OF_SECURITY,
+ JOB_PRISONER,
+ JOB_RESEARCH_DIRECTOR,
+ JOB_SECURITY_OFFICER,
+ JOB_WARDEN,
+ )
+ base_antags = 3
+ maximum_antags = 4
+ enemy_roles = list(
+ JOB_AI,
+ JOB_CYBORG,
+ JOB_CAPTAIN,
+ JOB_DETECTIVE,
+ JOB_HEAD_OF_SECURITY,
+ JOB_SECURITY_OFFICER,
+ JOB_WARDEN,
+ )
+ required_enemies = 5
+ // I give up, just there should be enough heads with 35 players...
+ min_players = 35
+ roundstart = TRUE
+ earliest_start = 45 MINUTES
+ weight = 4
+ max_occurrences = 1
+
+/datum/round_event/antagonist/solo/ghost/nuclear_operative
+ excute_round_end_reports = TRUE
+ end_when = 60000 /// we will end on our own when revs win
+ var/static/datum/team/nuclear/nuke_team
+ var/datum/antagonist/antag_leader_datum = /datum/antagonist/nukeop/leader
+ var/set_leader = FALSE
+ var/required_role = ROLE_NUCLEAR_OPERATIVE
+
+/datum/round_event/antagonist/solo/ghost/nuclear_operative/add_datum_to_mind(datum/mind/antag_mind)
+ var/mob/living/current_mob = antag_mind.current
+ var/list/items = current_mob.get_equipped_items(TRUE)
+ current_mob.unequip_everything()
+ for(var/obj/item/item as anything in items)
+ qdel(item)
+
+ var/datum/mind/most_experienced = get_most_experienced(setup_minds, required_role)
+ if(!most_experienced)
+ most_experienced = antag_mind
+
+ if(!set_leader)
+ set_leader = TRUE
+ var/datum/antagonist/nukeop/leader/leader = most_experienced.add_antag_datum(antag_leader_datum)
+ nuke_team = leader.nuke_team
+
+ if(antag_mind == most_experienced)
+ return
+
+ var/datum/antagonist/nukeop/new_op = new antag_datum()
+ antag_mind.add_antag_datum(new_op)
+
+
+/datum/round_event/antagonist/solo/ghost/nuclear_operative/round_end_report()
+ 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
diff --git a/monkestation/code/modules/storytellers/converted_events/solo/ghosts/wizard.dm b/monkestation/code/modules/storytellers/converted_events/solo/ghosts/wizard.dm
new file mode 100644
index 000000000000..a8720fcf90fa
--- /dev/null
+++ b/monkestation/code/modules/storytellers/converted_events/solo/ghosts/wizard.dm
@@ -0,0 +1,28 @@
+/datum/round_event_control/antagonist/solo/from_ghosts/wizard
+ name = "Ghost Wizard"
+ tags = list(TAG_COMBAT, TAG_DESTRUCTIVE)
+ typepath = /datum/round_event/antagonist/solo/ghost/wizard
+ antag_flag = ROLE_WIZARD
+ antag_datum = /datum/antagonist/wizard
+ restricted_roles = list(
+ JOB_CAPTAIN,
+ JOB_HEAD_OF_SECURITY,
+ ) // Just to be sure that a wizard getting picked won't ever imply a Captain or HoS not getting drafted
+ maximum_antags = 1
+ weight = 2
+ min_players = 35
+ max_occurrences = 1
+ prompted_picking = TRUE
+
+/datum/round_event_control/antagonist/solo/ghost/wizard/can_spawn_event(players_amt, allow_magic = FALSE, fake_check = FALSE)
+ . = ..()
+ if(!.)
+ return
+ if(GLOB.wizardstart.len == 0)
+ return FALSE
+
+/datum/round_event/antagonist/solo/ghost/wizard
+
+/datum/round_event/antagonist/solo/ghost/wizard/add_datum_to_mind(datum/mind/antag_mind)
+ . = ..()
+ antag_mind.current.forceMove(pick(GLOB.wizardstart))
diff --git a/monkestation/code/modules/storytellers/converted_events/solo/heretic.dm b/monkestation/code/modules/storytellers/converted_events/solo/heretic.dm
new file mode 100644
index 000000000000..cb0c244a83d7
--- /dev/null
+++ b/monkestation/code/modules/storytellers/converted_events/solo/heretic.dm
@@ -0,0 +1,32 @@
+/datum/round_event_control/antagonist/solo/heretic
+ antag_flag = ROLE_HERETIC
+ tags = list(TAG_COMBAT, TAG_SPOOKY)
+ antag_datum = /datum/antagonist/heretic
+ protected_roles = list(
+ JOB_CAPTAIN,
+ JOB_HEAD_OF_PERSONNEL,
+ JOB_CHIEF_ENGINEER,
+ JOB_CHIEF_MEDICAL_OFFICER,
+ JOB_RESEARCH_DIRECTOR,
+ JOB_DETECTIVE,
+ JOB_HEAD_OF_PERSONNEL,
+ JOB_HEAD_OF_SECURITY,
+ JOB_PRISONER,
+ JOB_SECURITY_OFFICER,
+ JOB_WARDEN,
+ )
+ restricted_roles = list(
+ JOB_AI,
+ JOB_CYBORG,
+ )
+ weight = 4
+ min_players = 20
+
+/datum/round_event_control/antagonist/solo/heretic/roundstart
+ name = "Heretics"
+ roundstart = TRUE
+ earliest_start = 0
+
+/datum/round_event_control/antagonist/solo/heretic/midround
+ name = "Midround Heretics"
+ prompted_picking = TRUE
diff --git a/monkestation/code/modules/storytellers/converted_events/solo/malf.dm b/monkestation/code/modules/storytellers/converted_events/solo/malf.dm
new file mode 100644
index 000000000000..0c17035a1c46
--- /dev/null
+++ b/monkestation/code/modules/storytellers/converted_events/solo/malf.dm
@@ -0,0 +1,41 @@
+/datum/round_event_control/antagonist/solo/malf
+ antag_datum = /datum/antagonist/malf_ai
+ tags = list(TAG_COMBAT, TAG_DESTRUCTIVE)
+ antag_flag = 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 = 4
+ weight = 4
+ max_occurrences = 1
+
+/datum/round_event_control/antagonist/solo/malf/trim_candidates(list/candidates)
+ for(var/mob/living/player in candidates)
+ if(!isAI(player))
+ candidates -= player
+ continue
+
+ if(is_centcom_level(player.z))
+ candidates -= player
+ continue
+
+ if(player.mind && (player.mind.special_role || player.mind.antag_datums?.len > 0))
+ candidates -= player
+
+ return candidates
+
+/datum/round_event_control/antagonist/solo/malf/midround
+ name = "Malfunctioning AI Midround"
+ antag_flag = ROLE_MALF_MIDROUND
+
+/datum/round_event_control/antagonist/solo/malf/roundstart
+ name = "Roundstart Malf AI"
+ roundstart = TRUE
+ earliest_start = 0
diff --git a/monkestation/code/modules/storytellers/converted_events/solo/nuclear_operative.dm b/monkestation/code/modules/storytellers/converted_events/solo/nuclear_operative.dm
new file mode 100644
index 000000000000..6646c502a4a7
--- /dev/null
+++ b/monkestation/code/modules/storytellers/converted_events/solo/nuclear_operative.dm
@@ -0,0 +1,104 @@
+/datum/round_event_control/antagonist/solo/nuclear_operative
+ name = "Roundstart Nuclear Operative"
+ tags = list(TAG_DESTRUCTIVE, TAG_COMBAT, TAG_TEAM_ANTAG)
+ antag_flag = ROLE_OPERATIVE
+ antag_datum = /datum/antagonist/nukeop
+ typepath = /datum/round_event/antagonist/solo/nuclear_operative
+ restricted_roles = list(
+ JOB_AI,
+ JOB_CAPTAIN,
+ JOB_CHIEF_ENGINEER,
+ JOB_CHIEF_MEDICAL_OFFICER,
+ JOB_CYBORG,
+ JOB_DETECTIVE,
+ JOB_HEAD_OF_PERSONNEL,
+ JOB_HEAD_OF_SECURITY,
+ JOB_PRISONER,
+ JOB_RESEARCH_DIRECTOR,
+ JOB_SECURITY_OFFICER,
+ JOB_WARDEN,
+ )
+ base_antags = 3
+ maximum_antags = 5
+ enemy_roles = list(
+ JOB_AI,
+ JOB_CYBORG,
+ JOB_CAPTAIN,
+ JOB_DETECTIVE,
+ JOB_HEAD_OF_SECURITY,
+ JOB_SECURITY_OFFICER,
+ JOB_WARDEN,
+ )
+ required_enemies = 5
+ // I give up, just there should be enough heads with 35 players...
+ min_players = 35
+ roundstart = TRUE
+ earliest_start = 0 SECONDS
+ weight = 4
+ max_occurrences = 3
+
+/datum/round_event/antagonist/solo/nuclear_operative
+ excute_round_end_reports = TRUE
+ end_when = 60000 /// we will end on our own when revs win
+ var/static/datum/team/nuclear/nuke_team
+ var/datum/antagonist/antag_leader_datum = /datum/antagonist/nukeop/leader
+ var/set_leader = FALSE
+ var/required_role = ROLE_NUCLEAR_OPERATIVE
+
+/datum/round_event/antagonist/solo/nuclear_operative/add_datum_to_mind(datum/mind/antag_mind)
+ var/mob/living/current_mob = antag_mind.current
+ SSjob.FreeRole(antag_mind.assigned_role.title)
+ var/list/items = current_mob.get_equipped_items(TRUE)
+ current_mob.unequip_everything()
+ for(var/obj/item/item as anything in items)
+ qdel(item)
+
+ var/datum/mind/most_experienced = get_most_experienced(setup_minds, required_role)
+ if(!most_experienced)
+ most_experienced = antag_mind
+
+ if(!set_leader)
+ set_leader = TRUE
+ var/datum/antagonist/nukeop/leader/leader = most_experienced.add_antag_datum(antag_leader_datum)
+ nuke_team = leader.nuke_team
+
+ if(antag_mind == most_experienced)
+ return
+
+ var/datum/antagonist/nukeop/new_op = new antag_datum()
+ antag_mind.add_antag_datum(new_op)
+
+
+/datum/round_event/antagonist/solo/nuclear_operative/round_end_report()
+ 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
diff --git a/monkestation/code/modules/storytellers/converted_events/solo/obsessed.dm b/monkestation/code/modules/storytellers/converted_events/solo/obsessed.dm
new file mode 100644
index 000000000000..4509b33a8ab0
--- /dev/null
+++ b/monkestation/code/modules/storytellers/converted_events/solo/obsessed.dm
@@ -0,0 +1,16 @@
+/datum/round_event_control/antagonist/solo/obsessed
+ antag_flag = ROLE_TRAITOR
+ tags = list(TAG_COMBAT)
+ antag_datum = /datum/antagonist/obsessed
+ restricted_roles = list(
+ JOB_AI,
+ JOB_CYBORG,
+ ROLE_POSITRONIC_BRAIN,
+ )
+ weight = 4
+ max_occurrences = 3
+
+/datum/round_event_control/antagonist/solo/obsessed/midround
+ name = "Compulsive Obsession"
+ prompted_picking = TRUE
+ maximum_antags = 4
diff --git a/monkestation/code/modules/storytellers/converted_events/solo/revolutionary.dm b/monkestation/code/modules/storytellers/converted_events/solo/revolutionary.dm
new file mode 100644
index 000000000000..5bf9e1d81fcd
--- /dev/null
+++ b/monkestation/code/modules/storytellers/converted_events/solo/revolutionary.dm
@@ -0,0 +1,65 @@
+/datum/round_event_control/antagonist/solo/revolutionary
+ name = "Roundstart Revolution"
+ tags = list(TAG_COMMUNAL, TAG_DESTRUCTIVE, TAG_COMBAT, TAG_TEAM_ANTAG)
+ antag_flag = ROLE_REV_HEAD
+ antag_datum = /datum/antagonist/rev/head/event_trigger
+ typepath = /datum/round_event/antagonist/solo/revolutionary
+ restricted_roles = list(
+ JOB_AI,
+ JOB_CAPTAIN,
+ JOB_CHIEF_ENGINEER,
+ JOB_CHIEF_MEDICAL_OFFICER,
+ JOB_CYBORG,
+ JOB_DETECTIVE,
+ JOB_HEAD_OF_PERSONNEL,
+ JOB_HEAD_OF_SECURITY,
+ JOB_PRISONER,
+ JOB_RESEARCH_DIRECTOR,
+ JOB_SECURITY_OFFICER,
+ JOB_WARDEN,
+ )
+ base_antags = 2
+ enemy_roles = list(
+ JOB_CAPTAIN,
+ JOB_DETECTIVE,
+ JOB_HEAD_OF_SECURITY,
+ JOB_SECURITY_OFFICER,
+ JOB_WARDEN,
+ )
+ required_enemies = 6
+ // I give up, just there should be enough heads with 35 players...
+ min_players = 35
+ roundstart = TRUE
+ earliest_start = 0 SECONDS
+ weight = 4
+ max_occurrences = 1
+
+/datum/antagonist/rev/head/event_trigger
+ remove_clumsy = TRUE
+ give_flash = TRUE
+
+/datum/round_event/antagonist/solo/revolutionary
+ excute_round_end_reports = TRUE
+ end_when = 60000 /// we will end on our own when revs win
+ var/static/datum/team/revolution/revolution
+ var/static/finished = FALSE
+
+/datum/round_event/antagonist/solo/revolutionary/setup()
+ . = ..()
+ if(!revolution)
+ revolution = new()
+
+/datum/round_event/antagonist/solo/revolutionary/add_datum_to_mind(datum/mind/antag_mind)
+ antag_mind.add_antag_datum(antag_datum, revolution)
+ if(revolution.members.len)
+ revolution.update_objectives()
+ revolution.update_heads()
+ SSshuttle.registerHostileEnvironment(revolution)
+
+
+/datum/round_event/antagonist/solo/revolutionary/round_end_report()
+ var/winner = revolution.process_victory()
+ if (isnull(winner))
+ return
+ finished = TRUE
+ revolution.round_result(finished)
diff --git a/monkestation/code/modules/storytellers/converted_events/solo/traitor.dm b/monkestation/code/modules/storytellers/converted_events/solo/traitor.dm
new file mode 100644
index 000000000000..3100c3a50250
--- /dev/null
+++ b/monkestation/code/modules/storytellers/converted_events/solo/traitor.dm
@@ -0,0 +1,29 @@
+/datum/round_event_control/antagonist/solo/traitor
+ antag_flag = ROLE_TRAITOR
+ tags = list(TAG_COMBAT)
+ antag_datum = /datum/antagonist/traitor
+ protected_roles = list(
+ JOB_CAPTAIN,
+ JOB_HEAD_OF_PERSONNEL,
+ JOB_CHIEF_ENGINEER,
+ JOB_CHIEF_MEDICAL_OFFICER,
+ JOB_RESEARCH_DIRECTOR,
+ JOB_DETECTIVE,
+ JOB_HEAD_OF_SECURITY,
+ JOB_PRISONER,
+ JOB_SECURITY_OFFICER,
+ JOB_WARDEN,
+ )
+ restricted_roles = list(
+ JOB_AI,
+ JOB_CYBORG,
+ )
+
+/datum/round_event_control/antagonist/solo/traitor/roundstart
+ name = "Traitors"
+ roundstart = TRUE
+ earliest_start = 0 SECONDS
+
+/datum/round_event_control/antagonist/solo/traitor/midround
+ name = "Sleeper Agents (Traitors)"
+ prompted_picking = TRUE
diff --git a/monkestation/code/modules/storytellers/converted_events/solo/wizard.dm b/monkestation/code/modules/storytellers/converted_events/solo/wizard.dm
new file mode 100644
index 000000000000..a0069ece6634
--- /dev/null
+++ b/monkestation/code/modules/storytellers/converted_events/solo/wizard.dm
@@ -0,0 +1,36 @@
+/datum/round_event_control/antagonist/solo/wizard
+ name = "Wizard"
+ tags = list(TAG_COMBAT, TAG_DESTRUCTIVE)
+ typepath = /datum/round_event/antagonist/solo/wizard
+ antag_flag = ROLE_WIZARD
+ antag_datum = /datum/antagonist/wizard
+ restricted_roles = list(
+ JOB_CAPTAIN,
+ JOB_HEAD_OF_SECURITY,
+ ) // Just to be sure that a wizard getting picked won't ever imply a Captain or HoS not getting drafted
+ maximum_antags = 1
+ roundstart = TRUE
+ earliest_start = 0 SECONDS
+ weight = 2
+ min_players = 35
+ max_occurrences = 1
+
+/datum/round_event_control/antagonist/solo/wizard/can_spawn_event(players_amt, allow_magic = FALSE, fake_check = FALSE)
+ . = ..()
+ if(!.)
+ return
+ if(GLOB.wizardstart.len == 0)
+ return FALSE
+
+/datum/round_event/antagonist/solo/wizard
+
+/datum/round_event/antagonist/solo/wizard/add_datum_to_mind(datum/mind/antag_mind)
+ . = ..()
+ var/mob/living/current_mob = antag_mind.current
+ SSjob.FreeRole(antag_mind.assigned_role.title)
+ var/list/items = current_mob.get_equipped_items(TRUE)
+ current_mob.unequip_everything()
+ for(var/obj/item/item as anything in items)
+ qdel(item)
+
+ antag_mind.current.forceMove(pick(GLOB.wizardstart))
diff --git a/monkestation/code/modules/storytellers/gamemode_subsystem.dm b/monkestation/code/modules/storytellers/gamemode_subsystem.dm
new file mode 100644
index 000000000000..c49d02243962
--- /dev/null
+++ b/monkestation/code/modules/storytellers/gamemode_subsystem.dm
@@ -0,0 +1,1154 @@
+#define INIT_ORDER_GAMEMODE 70
+
+SUBSYSTEM_DEF(gamemode)
+ name = "Gamemode"
+ init_order = INIT_ORDER_GAMEMODE
+ runlevels = RUNLEVEL_GAME
+ flags = SS_BACKGROUND | SS_KEEP_TIMING
+ wait = 2 SECONDS
+
+ /// List of our event tracks for fast access during for loops.
+ var/list/event_tracks = EVENT_TRACKS
+ /// Our storyteller. He progresses our trackboards and picks out events
+ var/datum/storyteller/storyteller
+ /// Result of the storyteller vote. Defaults to the guide.
+ var/voted_storyteller = /datum/storyteller/guide
+ /// List of all the storytellers. Populated at init. Associative from type
+ var/list/storytellers = list()
+ /// Next process for our storyteller. The wait time is STORYTELLER_WAIT_TIME
+ var/next_storyteller_process = 0
+ /// Associative list of even track points.
+ var/list/event_track_points = list(
+ EVENT_TRACK_MUNDANE = 0,
+ EVENT_TRACK_MODERATE = 0,
+ EVENT_TRACK_MAJOR = 0,
+ EVENT_TRACK_ROLESET = 0,
+ EVENT_TRACK_OBJECTIVES = 0
+ )
+ /// Last point amount gained of each track. Those are recorded for purposes of estimating how long until next event.
+ var/list/last_point_gains = list(
+ EVENT_TRACK_MUNDANE = 0,
+ EVENT_TRACK_MODERATE = 0,
+ EVENT_TRACK_MAJOR = 0,
+ EVENT_TRACK_ROLESET = 0,
+ EVENT_TRACK_OBJECTIVES = 0
+ )
+ /// Point thresholds at which the events are supposed to be rolled, it is also the base cost for events.
+ var/list/point_thresholds = list(
+ EVENT_TRACK_MUNDANE = MUNDANE_POINT_THRESHOLD,
+ EVENT_TRACK_MODERATE = MODERATE_POINT_THRESHOLD,
+ EVENT_TRACK_MAJOR = MAJOR_POINT_THRESHOLD,
+ EVENT_TRACK_ROLESET = ROLESET_POINT_THRESHOLD,
+ EVENT_TRACK_OBJECTIVES = OBJECTIVES_POINT_THRESHOLD
+ )
+
+ /// Minimum population thresholds for the tracks to fire off events.
+ var/list/min_pop_thresholds = list(
+ EVENT_TRACK_MUNDANE = MUNDANE_MIN_POP,
+ EVENT_TRACK_MODERATE = MODERATE_MIN_POP,
+ EVENT_TRACK_MAJOR = MAJOR_MIN_POP,
+ EVENT_TRACK_ROLESET = ROLESET_MIN_POP,
+ EVENT_TRACK_OBJECTIVES = OBJECTIVES_MIN_POP
+ )
+
+ /// Configurable multipliers for point gain over time.
+ var/list/point_gain_multipliers = list(
+ EVENT_TRACK_MUNDANE = 1,
+ EVENT_TRACK_MODERATE = 1,
+ EVENT_TRACK_MAJOR = 1,
+ EVENT_TRACK_ROLESET = 1,
+ EVENT_TRACK_OBJECTIVES = 1
+ )
+ /// Configurable multipliers for roundstart points.
+ var/list/roundstart_point_multipliers = list(
+ EVENT_TRACK_MUNDANE = 1,
+ EVENT_TRACK_MODERATE = 1,
+ EVENT_TRACK_MAJOR = 1,
+ EVENT_TRACK_ROLESET = 1,
+ EVENT_TRACK_OBJECTIVES = 1
+ )
+ /// Whether we allow pop scaling. This is configured by config, or the storyteller UI
+ var/allow_pop_scaling = TRUE
+
+ /// Associative list of pop scale thresholds.
+ var/list/pop_scale_thresholds = list(
+ EVENT_TRACK_MUNDANE = MUNDANE_POP_SCALE_THRESHOLD,
+ EVENT_TRACK_MODERATE = MODERATE_POP_SCALE_THRESHOLD,
+ EVENT_TRACK_MAJOR = MAJOR_POP_SCALE_THRESHOLD,
+ EVENT_TRACK_ROLESET = ROLESET_POP_SCALE_THRESHOLD,
+ EVENT_TRACK_OBJECTIVES = OBJECTIVES_POP_SCALE_THRESHOLD
+ )
+
+ /// Associative list of pop scale penalties.
+ var/list/pop_scale_penalties = list(
+ EVENT_TRACK_MUNDANE = MUNDANE_POP_SCALE_PENALTY,
+ EVENT_TRACK_MODERATE = MODERATE_POP_SCALE_PENALTY,
+ EVENT_TRACK_MAJOR = MAJOR_POP_SCALE_PENALTY,
+ EVENT_TRACK_ROLESET = ROLESET_POP_SCALE_PENALTY,
+ EVENT_TRACK_OBJECTIVES = OBJECTIVES_POP_SCALE_PENALTY
+ )
+
+ /// Associative list of active multipliers from pop scale penalty.
+ var/list/current_pop_scale_multipliers = list(
+ EVENT_TRACK_MUNDANE = 1,
+ EVENT_TRACK_MODERATE = 1,
+ EVENT_TRACK_MAJOR = 1,
+ EVENT_TRACK_ROLESET = 1,
+ EVENT_TRACK_OBJECTIVES = 1,
+ )
+
+
+
+ /// Associative list of control events by their track category. Compiled in Init
+ var/list/event_pools = list()
+
+ /// Events that we have scheduled to run in the nearby future
+ var/list/scheduled_events = list()
+
+ /// Associative list of tracks to forced event controls. For admins to force events (though they can still invoke them freely outside of the track system)
+ var/list/forced_next_events = list()
+
+ var/list/control = list() //list of all datum/round_event_control. Used for selecting events based on weight and occurrences.
+ var/list/running = list() //list of all existing /datum/round_event
+ var/list/round_end_data = list() //list of all reports that need to add round end reports
+ var/list/currentrun = list()
+
+ /// List of all uncategorized events, because they were wizard or holiday events
+ var/list/uncategorized = list()
+
+ var/list/holidays //List of all holidays occuring today or null if no holidays
+
+ /// Event frequency multiplier, it exists because wizard, eugh.
+ var/event_frequency_multiplier = 1
+
+ /// Current preview page for the statistics UI.
+ var/statistics_track_page = EVENT_TRACK_MUNDANE
+ /// Page of the UI panel.
+ var/panel_page = GAMEMODE_PANEL_MAIN
+ /// Whether we are viewing the roundstart events or not
+ var/roundstart_event_view = TRUE
+
+ /// Whether the storyteller has been halted
+ var/halted_storyteller = FALSE
+
+ /// Ready players for roundstart events.
+ var/ready_players = 0
+ var/active_players = 0
+ var/head_crew = 0
+ var/eng_crew = 0
+ var/sec_crew = 0
+ var/med_crew = 0
+
+ var/wizardmode = FALSE
+
+ var/datum/round_event_control/current_roundstart_event
+ var/list/last_round_events = list()
+ var/ran_roundstart = FALSE
+ var/list/triggered_round_events = list()
+
+ var/total_valid_antags = 0
+
+/datum/controller/subsystem/gamemode/Initialize(time, zlevel)
+ // Populate event pools
+ for(var/track in event_tracks)
+ event_pools[track] = list()
+
+ // Populate storytellers
+ for(var/type in subtypesof(/datum/storyteller))
+ storytellers[type] = new type()
+
+ for(var/type in typesof(/datum/round_event_control))
+ var/datum/round_event_control/event = new type()
+ if(!event.typepath || !event.name)
+ continue //don't want this one! leave it for the garbage collector
+ if(!event.valid_for_map())
+ continue // event isn't good for this map no point in trying to add it to the list
+ control += event //add it to the list of all events (controls)
+ getHoliday()
+
+ load_config_vars()
+ load_event_config_vars()
+
+ ///Seeding events into track event pools needs to happen after event config vars are loaded
+ for(var/datum/round_event_control/event as anything in control)
+ if(event.holidayID || event.wizardevent)
+ uncategorized += event
+ continue
+ event_pools[event.track] += event //Add it to the categorized event pools
+
+ load_roundstart_data()
+
+// return ..()
+
+
+/datum/controller/subsystem/gamemode/fire(resumed = FALSE)
+ if(!resumed)
+ src.currentrun = running.Copy()
+
+ ///Handle scheduled events
+ for(var/datum/scheduled_event/sch_event in scheduled_events)
+ if(world.time >= sch_event.start_time)
+ sch_event.try_fire()
+ else if(!sch_event.alerted_admins && world.time >= sch_event.start_time - 1 MINUTES)
+ ///Alert admins 1 minute before running and allow them to cancel or refund the event, once again.
+ sch_event.alerted_admins = TRUE
+ message_admins("Scheduled Event: [sch_event.event] will run in [(sch_event.start_time - world.time) / 10] seconds. (CANCEL) (REFUND)")
+
+ if(!halted_storyteller && next_storyteller_process <= world.time && storyteller)
+ // We update crew information here to adjust population scalling and event thresholds for the storyteller.
+ update_crew_infos()
+ next_storyteller_process = world.time + STORYTELLER_WAIT_TIME
+ storyteller.process(STORYTELLER_WAIT_TIME * 0.1)
+
+ //cache for sanic speed (lists are references anyways)
+ var/list/currentrun = src.currentrun
+
+ while(currentrun.len)
+ var/datum/thing = currentrun[currentrun.len]
+ currentrun.len--
+ if(thing)
+ thing.process(wait * 0.1)
+ else
+ running.Remove(thing)
+ if (MC_TICK_CHECK)
+ return
+
+/// Gets the number of antagonists the antagonist injection events will stop rolling after.
+/datum/controller/subsystem/gamemode/proc/get_antag_cap()
+ var/cap = FLOOR((get_correct_popcount() / ANTAG_CAP_DENOMINATOR), 1) + ANTAG_CAP_FLAT
+ return cap
+
+/// Whether events can inject more antagonists into the round
+/datum/controller/subsystem/gamemode/proc/can_inject_antags()
+ total_valid_antags = 0
+ for(var/mob/checked_mob in GLOB.mob_list)
+ if(!checked_mob.mind)
+ continue
+ if(!checked_mob.mind.special_role)
+ continue
+ if(checked_mob.stat == DEAD)
+ continue
+ total_valid_antags++
+
+
+ return (get_antag_cap() > total_valid_antags)
+
+/// Gets candidates for antagonist roles.
+/datum/controller/subsystem/gamemode/proc/get_candidates(be_special, job_ban, observers, ready_newplayers, living_players, required_time, inherit_required_time = TRUE, midround_antag_pref, no_antags = TRUE, list/restricted_roles)
+ var/list/candidates = list()
+ var/list/candidate_candidates = list() //lol
+
+ for(var/mob/player as anything in GLOB.player_list)
+ if(ready_newplayers && isnewplayer(player))
+ var/mob/dead/new_player/new_player = player
+ if(new_player.ready == PLAYER_READY_TO_PLAY && new_player.mind && new_player.check_preferences())
+ candidate_candidates += player
+ else if (observers && isobserver(player))
+ candidate_candidates += player
+ else if (living_players && isliving(player))
+ if(!ishuman(player))
+ continue
+ if(!(player.z in SSmapping.levels_by_trait(ZTRAIT_STATION)))
+ continue
+ candidate_candidates += player
+
+ for(var/mob/candidate as anything in candidate_candidates)
+ if(QDELETED(candidate) || !candidate.key || !candidate.client || (!observers && !candidate.mind))
+ continue
+ if(!observers)
+ if(no_antags && candidate.mind.special_role)
+ continue
+ if(restricted_roles && (candidate.mind.assigned_role.title in restricted_roles))
+ continue
+ if(be_special)
+ if(!(candidate.client.prefs) || !(be_special in candidate.client.prefs.be_special))
+ continue
+
+ var/time_to_check
+ if(required_time)
+ time_to_check = required_time
+ else if (inherit_required_time)
+ time_to_check = GLOB.special_roles[be_special]
+
+ if(time_to_check && candidate.client.get_remaining_days(time_to_check) > 0)
+ continue
+
+ //if(midround_antag_pref)
+ //continue
+
+ if(job_ban && is_banned_from(candidate.ckey, list(job_ban, ROLE_SYNDICATE)))
+ continue
+ candidates += candidate
+ return candidates
+
+/// Gets the correct popcount, returning READY people if roundstart, and active people if not.
+/datum/controller/subsystem/gamemode/proc/get_correct_popcount()
+ if(SSticker.HasRoundStarted())
+ update_crew_infos()
+ return active_players
+ else
+ calculate_ready_players()
+ return ready_players
+
+/// Refunds and removes a scheduled event.
+/datum/controller/subsystem/gamemode/proc/refund_scheduled_event(datum/scheduled_event/refunded)
+ if(refunded.cost)
+ var/track_type = refunded.event.track
+ event_track_points[track_type] += refunded.cost
+ remove_scheduled_event(refunded)
+
+/// Removes a scheduled event.
+/datum/controller/subsystem/gamemode/proc/remove_scheduled_event(datum/scheduled_event/removed)
+ scheduled_events -= removed
+ qdel(removed)
+
+/// We need to calculate ready players for the sake of roundstart events becoming eligible.
+/datum/controller/subsystem/gamemode/proc/calculate_ready_players()
+ ready_players = 0
+ for(var/mob/dead/new_player/player as anything in GLOB.new_player_list)
+ if(player.ready == PLAYER_READY_TO_PLAY)
+ ready_players++
+
+/// We roll points to be spent for roundstart events, including antagonists.
+/datum/controller/subsystem/gamemode/proc/roll_pre_setup_points()
+ if(storyteller.disable_distribution || halted_storyteller)
+ return
+ /// Distribute points
+ for(var/track in event_track_points)
+ var/base_amt
+ var/gain_amt
+ switch(track)
+ if(EVENT_TRACK_MUNDANE)
+ base_amt = ROUNDSTART_MUNDANE_BASE
+ gain_amt = ROUNDSTART_MUNDANE_GAIN
+ if(EVENT_TRACK_MODERATE)
+ base_amt = ROUNDSTART_MODERATE_BASE
+ gain_amt = ROUNDSTART_MODERATE_GAIN
+ if(EVENT_TRACK_MAJOR)
+ base_amt = ROUNDSTART_MAJOR_BASE
+ gain_amt = ROUNDSTART_MAJOR_GAIN
+ if(EVENT_TRACK_ROLESET)
+ base_amt = ROUNDSTART_ROLESET_BASE
+ gain_amt = ROUNDSTART_ROLESET_GAIN
+ if(EVENT_TRACK_OBJECTIVES)
+ base_amt = ROUNDSTART_OBJECTIVES_BASE
+ gain_amt = ROUNDSTART_OBJECTIVES_GAIN
+ var/calc_value = base_amt + (gain_amt * ready_players)
+ calc_value *= roundstart_point_multipliers[track]
+ calc_value *= storyteller.starting_point_multipliers[track]
+ calc_value *= (rand(100 - storyteller.roundstart_points_variance,100 + storyteller.roundstart_points_variance)/100)
+ event_track_points[track] = round(calc_value)
+
+ /// If the storyteller guarantees an antagonist roll, add points to make it so.
+ if(storyteller.guarantees_roundstart_roleset && event_track_points[EVENT_TRACK_ROLESET] < point_thresholds[EVENT_TRACK_ROLESET])
+ event_track_points[EVENT_TRACK_ROLESET] = point_thresholds[EVENT_TRACK_ROLESET]
+
+ /// If we have any forced events, ensure we get enough points for them
+ for(var/track in event_tracks)
+ if(forced_next_events[track] && event_track_points[track] < point_thresholds[track])
+ event_track_points[track] = point_thresholds[track]
+
+/// At this point we've rolled roundstart events and antags and we handle leftover points here.
+/datum/controller/subsystem/gamemode/proc/handle_post_setup_points()
+ for(var/track in event_track_points) //Just halve the points for now.
+ event_track_points[track] *= 0.5
+
+/// Because roundstart events need 2 steps of firing for purposes of antags, here is the first step handled, happening before occupation division.
+/datum/controller/subsystem/gamemode/proc/handle_pre_setup_roundstart_events()
+ if(storyteller.disable_distribution)
+ return
+ if(halted_storyteller)
+ message_admins("WARNING: Didn't roll roundstart events (including antagonists) due to the storyteller being halted.")
+ return
+ while(TRUE)
+ if(!storyteller.handle_tracks())
+ break
+
+/// Second step of handlind roundstart events, happening after people spawn.
+/datum/controller/subsystem/gamemode/proc/handle_post_setup_roundstart_events()
+ /// Start all roundstart events on post_setup immediately
+ for(var/datum/round_event/event as anything in running)
+ if(!event.control.roundstart)
+ continue
+ ASYNC
+ event.try_start()
+// INVOKE_ASYNC(event, /datum/round_event.proc/try_start)
+
+/// Schedules an event to run later.
+/datum/controller/subsystem/gamemode/proc/schedule_event(datum/round_event_control/passed_event, passed_time, passed_cost, passed_ignore, passed_announce, _forced = FALSE)
+ if(_forced)
+ passed_ignore = TRUE
+ var/datum/scheduled_event/scheduled = new (passed_event, world.time + passed_time, passed_cost, passed_ignore, passed_announce)
+ var/round_started = SSticker.HasRoundStarted()
+ if(round_started)
+ message_admins("Event: [passed_event] has been scheduled to run in [passed_time / 10] seconds. (CANCEL) (REFUND)")
+ else //Only roundstart events can be scheduled before round start
+ message_admins("Event: [passed_event] has been scheduled to run on roundstart. (CANCEL)")
+ scheduled_events += scheduled
+
+/datum/controller/subsystem/gamemode/proc/update_crew_infos()
+ // Very similar logic to `get_active_player_count()`
+ active_players = 0
+ head_crew = 0
+ eng_crew = 0
+ med_crew = 0
+ sec_crew = 0
+ for(var/mob/player_mob as anything in GLOB.player_list)
+ if(!player_mob.client)
+ continue
+ if(player_mob.stat) //If they're alive
+ continue
+ if(player_mob.client.is_afk()) //If afk
+ continue
+ if(!ishuman(player_mob))
+ continue
+ active_players++
+ if(player_mob.mind?.assigned_role)
+ var/datum/job/player_role = player_mob.mind.assigned_role
+ if(player_role.departments_bitflags & DEPARTMENT_BITFLAG_COMMAND)
+ head_crew++
+ if(player_role.departments_bitflags & DEPARTMENT_BITFLAG_ENGINEERING)
+ eng_crew++
+ if(player_role.departments_bitflags & DEPARTMENT_BITFLAG_MEDICAL)
+ med_crew++
+ if(player_role.departments_bitflags & DEPARTMENT_BITFLAG_SECURITY)
+ sec_crew++
+ update_pop_scaling()
+
+/datum/controller/subsystem/gamemode/proc/update_pop_scaling()
+ for(var/track in event_tracks)
+ var/low_pop_bound = min_pop_thresholds[track]
+ var/high_pop_bound = pop_scale_thresholds[track]
+ var/scale_penalty = pop_scale_penalties[track]
+
+ var/perceived_pop = max(low_pop_bound, active_players) // after max pop we start generating even more threat
+
+ var/divisor = high_pop_bound - low_pop_bound
+ /// If the bounds are equal, we'd be dividing by zero or worse, if upper is smaller than lower, we'd be increasing the factor, just make it 1 and continue.
+ /// this is only a problem for bad configs
+ if(divisor <= 0)
+ current_pop_scale_multipliers[track] = 1
+ continue
+ var/scalar = (perceived_pop - low_pop_bound) / divisor
+ var/penalty = scale_penalty - (scale_penalty * scalar)
+ var/calculated_multiplier = 1 - (penalty / 100)
+
+ current_pop_scale_multipliers[track] = calculated_multiplier
+
+/datum/controller/subsystem/gamemode/proc/TriggerEvent(datum/round_event_control/event, forced = FALSE)
+ . = event.preRunEvent(forced)
+ if(. == EVENT_CANT_RUN)//we couldn't run this event for some reason, set its max_occurrences to 0
+ event.max_occurrences = 0
+ else if(. == EVENT_READY)
+ event.runEvent(random = TRUE) // fallback to dynamic
+
+///Resets frequency multiplier.
+/datum/controller/subsystem/gamemode/proc/resetFrequency()
+ event_frequency_multiplier = 1
+
+/client/proc/forceEvent()
+ set name = "Trigger Event"
+ set category = "Admin.Events"
+ if(!holder ||!check_rights(R_FUN))
+ return
+ holder.forceEvent(usr)
+
+/datum/admins/proc/forceEvent(mob/user)
+ SSgamemode.event_panel(user)
+
+/client/proc/forceGamemode()
+ set name = "Open Gamemode Panel"
+ set category = "Admin.Events"
+ if(!holder ||!check_rights(R_FUN))
+ return
+ holder.forceGamemode(usr)
+
+/datum/admins/proc/forceGamemode(mob/user)
+ SSgamemode.admin_panel(user)
+
+
+//////////////
+// HOLIDAYS //
+//////////////
+//Uncommenting ALLOW_HOLIDAYS in config.txt will enable holidays
+
+//It's easy to add stuff. Just add a holiday datum in code/modules/holiday/holidays.dm
+//You can then check if it's a special day in any code in the game by doing if(SSgamemode.holidays["Groundhog Day"])
+
+//You can also make holiday random events easily thanks to Pete/Gia's system.
+//simply make a random event normally, then assign it a holidayID string which matches the holiday's name.
+//Anything with a holidayID, which isn't in the holidays list, will never occur.
+
+//Please, Don't spam stuff up with stupid stuff (key example being april-fools Pooh/ERP/etc),
+//And don't forget: CHECK YOUR CODE!!!! We don't want any zero-day bugs which happen only on holidays and never get found/fixed!
+
+//////////////////////////////////////////////////////////////////////////////////////////////////////////
+//ALSO, MOST IMPORTANTLY: Don't add stupid stuff! Discuss bonus content with Project-Heads first please!//
+//////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+
+//sets up the holidays and holidays list
+/datum/controller/subsystem/gamemode/proc/getHoliday()
+ if(!CONFIG_GET(flag/allow_holidays))
+ return // Holiday stuff was not enabled in the config!
+ for(var/H in subtypesof(/datum/holiday))
+ var/datum/holiday/holiday = new H()
+ var/delete_holiday = TRUE
+ for(var/timezone in holiday.timezones)
+ var/time_in_timezone = world.realtime + timezone HOURS
+
+ var/YYYY = text2num(time2text(time_in_timezone, "YYYY")) // get the current year
+ var/MM = text2num(time2text(time_in_timezone, "MM")) // get the current month
+ var/DD = text2num(time2text(time_in_timezone, "DD")) // get the current day
+ var/DDD = time2text(time_in_timezone, "DDD") // get the current weekday
+
+ if(holiday.shouldCelebrate(DD, MM, YYYY, DDD))
+ holiday.celebrate()
+ LAZYSET(holidays, holiday.name, holiday)
+ delete_holiday = FALSE
+ break
+ if(delete_holiday)
+ qdel(holiday)
+
+ if(holidays)
+ holidays = shuffle(holidays)
+ // regenerate station name because holiday prefixes.
+ set_station_name(new_station_name())
+ world.update_status()
+
+/datum/controller/subsystem/gamemode/proc/toggleWizardmode()
+ wizardmode = !wizardmode //TODO: decide what to do with wiz events
+ message_admins("Summon Events has been [wizardmode ? "enabled, events will occur [SSgamemode.event_frequency_multiplier] times as fast" : "disabled"]!")
+ log_game("Summon Events was [wizardmode ? "enabled" : "disabled"]!")
+
+///Attempts to select players for special roles the mode might have.
+/datum/controller/subsystem/gamemode/proc/pre_setup()
+ calculate_ready_players()
+ roll_pre_setup_points()
+ //handle_pre_setup_roundstart_events()
+ return TRUE
+
+///Everyone should now be on the station and have their normal gear. This is the place to give the special roles extra things
+/datum/controller/subsystem/gamemode/proc/post_setup(report) //Gamemodes can override the intercept report. Passing TRUE as the argument will force a report.
+ if(!report)
+ report = !CONFIG_GET(flag/no_intercept_report)
+ addtimer(CALLBACK(GLOBAL_PROC, .proc/display_roundstart_logout_report), ROUNDSTART_LOGOUT_REPORT_TIME)
+
+ if(CONFIG_GET(flag/reopen_roundstart_suicide_roles))
+ var/delay = CONFIG_GET(number/reopen_roundstart_suicide_roles_delay)
+ if(delay)
+ delay = (delay SECONDS)
+ else
+ delay = (4 MINUTES) //default to 4 minutes if the delay isn't defined.
+ addtimer(CALLBACK(GLOBAL_PROC, .proc/reopen_roundstart_suicide_roles), delay)
+
+ if(SSdbcore.Connect())
+ var/list/to_set = list()
+ var/arguments = list()
+ if(storyteller)
+ to_set += "game_mode = :game_mode"
+ arguments["game_mode"] = storyteller.name
+ if(GLOB.revdata.originmastercommit)
+ to_set += "commit_hash = :commit_hash"
+ arguments["commit_hash"] = GLOB.revdata.originmastercommit
+ if(to_set.len)
+ arguments["round_id"] = GLOB.round_id
+ var/datum/db_query/query_round_game_mode = SSdbcore.NewQuery(
+ "UPDATE [format_table_name("round")] SET [to_set.Join(", ")] WHERE id = :round_id",
+ arguments
+ )
+ query_round_game_mode.Execute()
+ qdel(query_round_game_mode)
+ generate_station_goals()
+ handle_post_setup_roundstart_events()
+ handle_post_setup_points()
+ roundstart_event_view = FALSE
+ return TRUE
+
+
+///Handles late-join antag assignments
+/datum/controller/subsystem/gamemode/proc/make_antag_chance(mob/living/carbon/human/character)
+ return
+
+/datum/controller/subsystem/gamemode/proc/check_finished(force_ending) //to be called by SSticker
+ if(!SSticker.setup_done)
+ return FALSE
+ if(SSshuttle.emergency && (SSshuttle.emergency.mode == SHUTTLE_ENDGAME))
+ return TRUE
+ if(GLOB.station_was_nuked)
+ return TRUE
+ if(force_ending)
+ return TRUE
+
+/*
+ * Generate a list of station goals available to purchase to report to the crew.
+ *
+ * Returns a formatted string all station goals that are available to the station.
+ */
+/datum/controller/subsystem/gamemode/proc/generate_station_goal_report()
+ if(!GLOB.station_goals.len)
+ return
+ . = "
Special Orders for [station_name()]: "
+ for(var/datum/station_goal/station_goal as anything in GLOB.station_goals)
+ station_goal.on_report()
+ . += station_goal.get_report()
+ return
+
+/*
+ * Generate a list of active station traits to report to the crew.
+ *
+ * Returns a formatted string of all station traits (that are shown) affecting the station.
+ */
+/datum/controller/subsystem/gamemode/proc/generate_station_trait_report()
+ if(!SSstation.station_traits.len)
+ return
+ . = "Identified shift divergencies: "
+ for(var/datum/station_trait/station_trait as anything in SSstation.station_traits)
+ if(!station_trait.show_in_report)
+ continue
+ . += "[station_trait.get_report()] "
+ return
+
+/* /proc/reopen_roundstart_suicide_roles()
+ var/include_command = CONFIG_GET(flag/reopen_roundstart_suicide_roles_command_positions)
+ var/list/reopened_jobs = list()
+ for(var/mob/living/quitter in GLOB.suicided_mob_list)
+ var/datum/job/job = SSjob.GetJob(quitter.job)
+ if(!job || !(job.job_flags & JOB_REOPEN_ON_ROUNDSTART_LOSS))
+ continue
+ if(!include_command && job.departments_bitflags & DEPARTMENT_BITFLAG_COMMAND)
+ continue
+ job.current_positions = max(job.current_positions - 1, 0)
+ reopened_jobs += quitter.job
+ if(CONFIG_GET(flag/reopen_roundstart_suicide_roles_command_report))
+ if(reopened_jobs.len)
+ var/reopened_job_report_positions
+ for(var/dead_dudes_job in reopened_jobs)
+ reopened_job_report_positions = "[reopened_job_report_positions ? "[reopened_job_report_positions]\n":""][dead_dudes_job]"
+ var/suicide_command_report = "Central Command Human Resources Board \
+ Notice of Personnel Change\
+ To personnel management staff aboard [station_name()]:
\
+ Our medical staff have detected a series of anomalies in the vital sensors \
+ of some of the staff aboard your station.
\
+ Further investigation into the situation on our end resulted in us discovering \
+ a series of rather... unforturnate decisions that were made on the part of said staff.
\
+ As such, we have taken the liberty to automatically reopen employment opportunities for the positions of the crew members \
+ who have decided not to partake in our research. We will be forwarding their cases to our employment review board \
+ to determine their eligibility for continued service with the company (and of course the \
+ continued storage of cloning records within the central medical backup server.)
\
+ The following positions have been reopened on our behalf:
\
+ [reopened_job_report_positions]"
+ print_command_report(suicide_command_report, "Central Command Personnel Update") */
+
+//////////////////////////
+//Reports player logouts//
+//////////////////////////
+/* /proc/display_roundstart_logout_report()
+ var/list/msg = list("[SPAN_BOLDNOTICE("Roundstart logout report")]\n\n")
+ for(var/i in GLOB.mob_living_list)
+ var/mob/living/L = i
+ var/mob/living/carbon/C = L
+ if (istype(C) && !C.last_mind)
+ continue // never had a client
+ if(L.ckey && !GLOB.directory[L.ckey])
+ msg += "[L.name] ([L.key]), the [L.job] (Disconnected)\n"
+ if(L.ckey && L.client)
+ var/failed = FALSE
+ if(L.client.inactivity >= (ROUNDSTART_LOGOUT_REPORT_TIME / 2)) //Connected, but inactive (alt+tabbed or something)
+ msg += "[L.name] ([L.key]), the [L.job] (Connected, Inactive)\n"
+ failed = TRUE //AFK client
+ if(!failed && L.stat)
+ if(L.suiciding) //Suicider
+ msg += "[L.name] ([L.key]), the [L.job] ([SPAN_BOLDANNOUNCE("Suicide")])\n"
+ failed = TRUE //Disconnected client
+ if(!failed && (L.stat == UNCONSCIOUS || L.stat == HARD_CRIT))
+ msg += "[L.name] ([L.key]), the [L.job] (Dying)\n"
+ failed = TRUE //Unconscious
+ if(!failed && L.stat == DEAD)
+ msg += "[L.name] ([L.key]), the [L.job] (Dead)\n"
+ failed = TRUE //Dead
+ continue //Happy connected client
+ for(var/mob/dead/observer/D in GLOB.dead_mob_list)
+ if(D.mind && D.mind.current == L)
+ if(L.stat == DEAD)
+ if(L.suiciding) //Suicider
+ msg += "[L.name] ([ckey(D.mind.key)]), the [L.job] ([SPAN_BOLDANNOUNCE("Suicide")])\n"
+ continue //Disconnected client
+ else
+ msg += "[L.name] ([ckey(D.mind.key)]), the [L.job] (Dead)\n"
+ continue //Dead mob, ghost abandoned
+ else
+ if(D.can_reenter_corpse)
+ continue //Adminghost, or cult/wizard ghost
+ else
+ msg += "[L.name] ([ckey(D.mind.key)]), the [L.job] ([SPAN_BOLDANNOUNCE("Ghosted")])\n"
+ continue //Ghosted while alive
+ for (var/C in GLOB.admins)
+ to_chat(C, msg.Join()) */
+
+/datum/controller/subsystem/gamemode/proc/generate_station_goals()
+ var/list/possible = subtypesof(/datum/station_goal)
+ var/goal_weights = 0
+ while(possible.len && goal_weights < 1) // station goal budget is 1
+ var/datum/station_goal/picked = pick_n_take(possible)
+ goal_weights += initial(picked.weight)
+ GLOB.station_goals += new picked
+
+//Set result and news report here
+/datum/controller/subsystem/gamemode/proc/set_round_result()
+ SSticker.mode_result = "undefined"
+ if(GLOB.station_was_nuked)
+ SSticker.news_report = STATION_DESTROYED_NUKE
+ if(EMERGENCY_ESCAPED_OR_ENDGAMED)
+ SSticker.news_report = STATION_EVACUATED
+ if(SSshuttle.emergency.is_hijacked())
+ SSticker.news_report = SHUTTLE_HIJACK
+
+/// Loads json event config values from events.txt
+/datum/controller/subsystem/gamemode/proc/load_event_config_vars()
+ var/json_file = file("[global.config.directory]/events.json")
+ if(!fexists(json_file))
+ return
+ var/list/decoded = json_decode(file2text(json_file))
+ for(var/event_text_path in decoded)
+ var/event_path = text2path(event_text_path)
+ var/datum/round_event_control/event
+ for(var/datum/round_event_control/iterated_event as anything in control)
+ if(iterated_event.type == event_path)
+ event = iterated_event
+ break
+ if(!event)
+ continue
+ var/list/var_list = decoded[event_text_path]
+ for(var/variable in var_list)
+ var/value = var_list[variable]
+ switch(variable)
+ if("weight")
+ event.weight = value
+ if("min_players")
+ event.min_players = value
+ if("max_occurrences")
+ event.max_occurrences = value
+ if("earliest_start")
+ event.earliest_start = value * (1 MINUTES)
+ if("track")
+ if(value in event_tracks)
+ event.track = value
+ if("cost")
+ event.cost = value
+ if("reoccurence_penalty_multiplier")
+ event.reoccurence_penalty_multiplier = value
+ if("shared_occurence_type")
+ if(!isnull(value))
+ value = text2path(value)
+ event.shared_occurence_type = value
+
+/// Loads config values from game_options.txt
+/datum/controller/subsystem/gamemode/proc/load_config_vars()
+ point_gain_multipliers[EVENT_TRACK_MUNDANE] = CONFIG_GET(number/mundane_point_gain_multiplier)
+ point_gain_multipliers[EVENT_TRACK_MODERATE] = CONFIG_GET(number/moderate_point_gain_multiplier)
+ point_gain_multipliers[EVENT_TRACK_MAJOR] = CONFIG_GET(number/major_point_gain_multiplier)
+ point_gain_multipliers[EVENT_TRACK_ROLESET] = CONFIG_GET(number/roleset_point_gain_multiplier)
+ point_gain_multipliers[EVENT_TRACK_OBJECTIVES] = CONFIG_GET(number/objectives_point_gain_multiplier)
+
+ roundstart_point_multipliers[EVENT_TRACK_MUNDANE] = CONFIG_GET(number/mundane_roundstart_point_multiplier)
+ roundstart_point_multipliers[EVENT_TRACK_MODERATE] = CONFIG_GET(number/moderate_roundstart_point_multiplier)
+ roundstart_point_multipliers[EVENT_TRACK_MAJOR] = CONFIG_GET(number/major_roundstart_point_multiplier)
+ roundstart_point_multipliers[EVENT_TRACK_ROLESET] = CONFIG_GET(number/roleset_roundstart_point_multiplier)
+ roundstart_point_multipliers[EVENT_TRACK_OBJECTIVES] = CONFIG_GET(number/objectives_roundstart_point_multiplier)
+
+ min_pop_thresholds[EVENT_TRACK_MUNDANE] = CONFIG_GET(number/mundane_min_pop)
+ min_pop_thresholds[EVENT_TRACK_MODERATE] = CONFIG_GET(number/moderate_min_pop)
+ min_pop_thresholds[EVENT_TRACK_MAJOR] = CONFIG_GET(number/major_min_pop)
+ min_pop_thresholds[EVENT_TRACK_ROLESET] = CONFIG_GET(number/roleset_min_pop)
+ min_pop_thresholds[EVENT_TRACK_OBJECTIVES] = CONFIG_GET(number/objectives_min_pop)
+
+ point_thresholds[EVENT_TRACK_MUNDANE] = CONFIG_GET(number/mundane_point_threshold)
+ point_thresholds[EVENT_TRACK_MODERATE] = CONFIG_GET(number/moderate_point_threshold)
+ point_thresholds[EVENT_TRACK_MAJOR] = CONFIG_GET(number/major_point_threshold)
+ point_thresholds[EVENT_TRACK_ROLESET] = CONFIG_GET(number/roleset_point_threshold)
+ point_thresholds[EVENT_TRACK_OBJECTIVES] = CONFIG_GET(number/objectives_point_threshold)
+
+/datum/controller/subsystem/gamemode/proc/storyteller_vote_choices()
+ var/client_amount = GLOB.clients.len
+ var/list/choices = list()
+ for(var/storyteller_type in storytellers)
+ var/datum/storyteller/storyboy = storytellers[storyteller_type]
+ if(!storyboy.votable)
+ continue
+ if((storyboy.population_min && storyboy.population_min > client_amount) || (storyboy.population_max && storyboy.population_max < client_amount))
+ continue
+ choices += storyboy.name
+ choices[storyboy.name] = 0
+ return choices
+
+/datum/controller/subsystem/gamemode/proc/storyteller_desc(storyteller_name)
+ for(var/storyteller_type in storytellers)
+ var/datum/storyteller/storyboy = storytellers[storyteller_type]
+ if(storyboy.name != storyteller_name)
+ continue
+ return storyboy.desc
+
+
+/datum/controller/subsystem/gamemode/proc/storyteller_vote_result(winner_name)
+ for(var/storyteller_type in storytellers)
+ var/datum/storyteller/storyboy = storytellers[storyteller_type]
+ if(storyboy.name == winner_name)
+ voted_storyteller = storyteller_type
+ break
+
+/datum/controller/subsystem/gamemode/proc/init_storyteller()
+ set_storyteller(voted_storyteller)
+
+/datum/controller/subsystem/gamemode/proc/set_storyteller(passed_type)
+ if(!storytellers[passed_type])
+ message_admins("Attempted to set an invalid storyteller type: [passed_type].")
+ CRASH("Attempted to set an invalid storyteller type: [passed_type].")
+ storyteller = storytellers[passed_type]
+ to_chat(world, span_notice("Storyteller is [storyteller.name]!"))
+ to_chat(world, span_notice("[storyteller.welcome_text]"))
+
+/// Panel containing information, variables and controls about the gamemode and scheduled event
+/datum/controller/subsystem/gamemode/proc/admin_panel(mob/user)
+ update_crew_infos()
+ total_valid_antags = 0
+ for(var/mob/checked_mob in GLOB.mob_list)
+ if(!checked_mob.mind)
+ continue
+ if(!checked_mob.mind.special_role)
+ continue
+ if(checked_mob.stat == DEAD)
+ continue
+ total_valid_antags++
+
+ var/round_started = SSticker.HasRoundStarted()
+ var/list/dat = list()
+ dat += "Storyteller: [storyteller ? "[storyteller.name]" : "None"] "
+ dat += " HALT StorytellerEvent PanelSet StorytellerRefresh"
+ dat += " Storyteller determines points gained, event chances, and is the entity responsible for rolling events."
+ dat += " Active Players: [active_players] (Head: [head_crew], Sec: [sec_crew], Eng: [eng_crew], Med: [med_crew])"
+ dat += " Antagonist Count vs Maximum: [total_valid_antags] / [get_antag_cap()]"
+ dat += ""
+ dat += "Main"
+ dat += " Variables"
+ dat += ""
+ switch(panel_page)
+ if(GAMEMODE_PANEL_VARIABLES)
+ dat += "Reload Config VarsConfigs located in game_options.txt."
+ dat += " Point Gains Multipliers (only over time):"
+ dat += " This affects points gained over time towards scheduling new events of the tracks."
+ for(var/track in event_tracks)
+ dat += " [track]: [point_gain_multipliers[track]]"
+ dat += ""
+
+ dat += "Roundstart Points Multipliers:"
+ dat += " This affects points generated for roundstart events and antagonists."
+ for(var/track in event_tracks)
+ dat += " [track]: [roundstart_point_multipliers[track]]"
+ dat += ""
+
+ dat += "Minimum Population for Tracks:"
+ dat += " This are the minimum population caps for events to be able to run."
+ for(var/track in event_tracks)
+ dat += " [track]: [min_pop_thresholds[track]]"
+ dat += ""
+
+ dat += "Point Thresholds:"
+ dat += " Those are thresholds the tracks require to reach with points to make an event."
+ for(var/track in event_tracks)
+ dat += " [track]: [point_thresholds[track]]"
+
+ if(GAMEMODE_PANEL_MAIN)
+ var/even = TRUE
+ dat += "
Event Tracks:
"
+ dat += "Every track represents progression towards scheduling an event of it's severity"
+ dat += "
"
+ even = TRUE
+ for(var/datum/round_event/event as anything in running)
+ even = !even
+ var/background_cl = even ? "#17191C" : "#23273C"
+ dat += "
"
+ dat += "
[event.control.name]
" //Name
+ dat += "
-TBA-
" //Actions
+ dat += "
"
+ dat += "
"
+
+ var/datum/browser/popup = new(user, "gamemode_admin_panel", "Gamemode Panel", 670, 650)
+ popup.set_content(dat.Join())
+ popup.open()
+
+ /// Panel containing information and actions regarding events
+/datum/controller/subsystem/gamemode/proc/event_panel(mob/user)
+ var/list/dat = list()
+ if(storyteller)
+ dat += "Storyteller: [storyteller.name]"
+ dat += " Repetition penalty multiplier: [storyteller.event_repetition_multiplier]"
+ dat += " Cost variance: [storyteller.cost_variance]"
+ if(storyteller.tag_multipliers)
+ dat += " Tag multipliers:"
+ for(var/tag in storyteller.tag_multipliers)
+ dat += "[tag]:[storyteller.tag_multipliers[tag]] | "
+ storyteller.calculate_weights(statistics_track_page)
+ else
+ dat += "Storyteller: None Weight and chance statistics will be inaccurate due to the present lack of a storyteller."
+ dat += " Roundstart Events Forced Roundstart events will use rolled points, and are guaranteed to trigger (even if the used points are not enough)"
+ dat += " Avg. event intervals: "
+ for(var/track in event_tracks)
+ if(last_point_gains[track])
+ var/est_time = round(point_thresholds[track] / last_point_gains[track] / STORYTELLER_WAIT_TIME * 40 / 6) / 10
+ dat += "[track]: ~[est_time] m. | "
+ dat += ""
+ for(var/track in EVENT_PANEL_TRACKS)
+ dat += "[track]"
+ dat += ""
+ /// Create event info and stats table
+ dat += "