diff --git a/code/__DEFINES/~~bubber_defines/storyteller_defines.dm b/code/__DEFINES/~~bubber_defines/storyteller_defines.dm
new file mode 100644
index 00000000000..6d475b07c6e
--- /dev/null
+++ b/code/__DEFINES/~~bubber_defines/storyteller_defines.dm
@@ -0,0 +1,94 @@
+//Could be bitflags, but that would require a good amount of translations, which eh, either way works for me
+/// When the event is combat oriented (spawning monsters, inherently hostile antags)
+#define TAG_COMBAT "combat"
+/// When the event is spooky (broken lights, some antags)
+#define TAG_SPOOKY "spooky"
+/// When the event is destructive in a decent capacity (meteors, blob)
+#define TAG_DESTRUCTIVE "destructive"
+/// When the event impacts most of the crewmembers in some capacity (comms blackout)
+#define TAG_COMMUNAL "communal"
+/// When the event targets a person for something (appendix, heart attack)
+#define TAG_TARGETED "targeted"
+/// When the event is positive and helps the crew, in some capacity (Shuttle Loan, Supply Pod)
+#define TAG_POSITIVE "positive"
+/// When one of the crewmembers becomes an antagonist
+#define TAG_CREW_ANTAG "crew_antag"
+/// When the antagonist event is focused around team cooperation.
+#define TAG_TEAM_ANTAG "team_antag"
+/// When one of the non-crewmember players becomes an antagonist
+#define TAG_OUTSIDER_ANTAG "away_antag"
+/// When the event impacts the overmap
+#define TAG_OVERMAP "overmap"
+/// When the event requires the station to be in space (meteors, carp)
+#define TAG_SPACE "space"
+/// When the event requires the station to be on planetary.
+#define TAG_PLANETARY "planetary"
+
+#define EVENT_TRACK_MUNDANE "Mundane"
+#define EVENT_TRACK_MODERATE "Moderate"
+#define EVENT_TRACK_MAJOR "Major"
+#define EVENT_TRACK_ROLESET "Roleset"
+#define EVENT_TRACK_OBJECTIVES "Objectives"
+
+#define ALL_EVENTS "All"
+#define UNCATEGORIZED_EVENTS "Uncategorized"
+
+#define STORYTELLER_WAIT_TIME 20 SECONDS
+
+#define EVENT_POINT_GAINED_PER_SECOND 0.05
+
+#define TRACK_FAIL_POINT_PENALTY_MULTIPLIER 0.5
+
+#define GAMEMODE_PANEL_MAIN "Main"
+#define GAMEMODE_PANEL_VARIABLES "Variables"
+
+#define MUNDANE_POINT_THRESHOLD 40
+#define MODERATE_POINT_THRESHOLD 70
+#define MAJOR_POINT_THRESHOLD 130
+#define ROLESET_POINT_THRESHOLD 150
+#define OBJECTIVES_POINT_THRESHOLD 170
+
+#define MUNDANE_MIN_POP 4
+#define MODERATE_MIN_POP 6
+#define MAJOR_MIN_POP 20
+#define ROLESET_MIN_POP 20
+#define OBJECTIVES_MIN_POP 20
+
+/// Defines for how much pop do we need to stop applying a pop scalling penalty to event frequency.
+#define MUNDANE_POP_SCALE_THRESHOLD 25
+#define MODERATE_POP_SCALE_THRESHOLD 32
+#define MAJOR_POP_SCALE_THRESHOLD 45
+#define ROLESET_POP_SCALE_THRESHOLD 45
+#define OBJECTIVES_POP_SCALE_THRESHOLD 45
+
+/// The maximum penalty coming from pop scalling, when we're at the most minimum point, easing into 0 as we reach the SCALE_THRESHOLD. This is treated as a percentage.
+#define MUNDANE_POP_SCALE_PENALTY 35
+#define MODERATE_POP_SCALE_PENALTY 35
+#define MAJOR_POP_SCALE_PENALTY 35
+#define ROLESET_POP_SCALE_PENALTY 35
+#define OBJECTIVES_POP_SCALE_PENALTY 35
+
+#define STORYTELLER_VOTE "storyteller"
+
+#define EVENT_TRACKS list(EVENT_TRACK_MUNDANE, EVENT_TRACK_MODERATE, EVENT_TRACK_MAJOR, EVENT_TRACK_ROLESET, EVENT_TRACK_OBJECTIVES)
+#define EVENT_PANEL_TRACKS list(EVENT_TRACK_MUNDANE, EVENT_TRACK_MODERATE, EVENT_TRACK_MAJOR, EVENT_TRACK_ROLESET, EVENT_TRACK_OBJECTIVES, UNCATEGORIZED_EVENTS, ALL_EVENTS)
+
+/// Defines for the antag cap to prevent midround injections.
+#define ANTAG_CAP_FLAT 1
+#define ANTAG_CAP_DENOMINATOR 9
+
+///Below are defines for roundstart point pool. The GAIN ones are multiplied by ready population
+#define ROUNDSTART_MUNDANE_BASE 20
+#define ROUNDSTART_MUNDANE_GAIN 0.5
+
+#define ROUNDSTART_MODERATE_BASE 35
+#define ROUNDSTART_MODERATE_GAIN 1.2
+
+#define ROUNDSTART_MAJOR_BASE 40
+#define ROUNDSTART_MAJOR_GAIN 2
+
+#define ROUNDSTART_ROLESET_BASE 60
+#define ROUNDSTART_ROLESET_GAIN 4
+
+#define ROUNDSTART_OBJECTIVES_BASE 40
+#define ROUNDSTART_OBJECTIVES_GAIN 2
diff --git a/code/controllers/subsystem/dynamic/dynamic.dm b/code/controllers/subsystem/dynamic/dynamic.dm
index cfe093eb11b..eeded0a2214 100644
--- a/code/controllers/subsystem/dynamic/dynamic.dm
+++ b/code/controllers/subsystem/dynamic/dynamic.dm
@@ -319,7 +319,7 @@ SUBSYSTEM_DEF(dynamic)
SSticker.news_report = SSshuttle.emergency?.is_hijacked() ? SHUTTLE_HIJACK : STATION_EVACUATED
/datum/controller/subsystem/dynamic/proc/send_intercept()
- if(GLOB.communications_controller.block_command_report) //If we don't want the report to be printed just yet, we put it off until it's ready
+ /*if(GLOB.communications_controller.block_command_report) //If we don't want the report to be printed just yet, we put it off until it's ready
addtimer(CALLBACK(src, PROC_REF(send_intercept)), 10 SECONDS)
return
@@ -369,7 +369,7 @@ SUBSYSTEM_DEF(dynamic)
if(SSsecurity_level.get_current_level_as_number() < SEC_LEVEL_BLUE)
SSsecurity_level.set_level(SEC_LEVEL_BLUE, announce = FALSE)
priority_announce("[SSsecurity_level.current_security_level.elevating_to_announcement]\n\nA summary has been copied and printed to all communications consoles.", "Security level elevated.", ANNOUNCER_INTERCEPT, color_override = SSsecurity_level.current_security_level.announcement_color)
-#endif
+#endif*/ // BUBBER EDIT END
return .
diff --git a/code/controllers/subsystem/persistence/_persistence.dm b/code/controllers/subsystem/persistence/_persistence.dm
index eae40a733b0..e61966e8a06 100644
--- a/code/controllers/subsystem/persistence/_persistence.dm
+++ b/code/controllers/subsystem/persistence/_persistence.dm
@@ -50,6 +50,8 @@ SUBSYSTEM_DEF(persistence)
var/tram_hits_this_round = 0
var/tram_hits_last_round = 0
+ var/last_storyteller = "" // BUBBER EDIT ADD: Storyteller votes
+
/datum/controller/subsystem/persistence/Initialize()
load_poly()
load_wall_engravings()
@@ -63,6 +65,7 @@ SUBSYSTEM_DEF(persistence)
load_panic_bunker() //SKYRAT EDIT ADDITION - PANICBUNKER
load_tram_counter()
load_adventures()
+ load_storyteller() //BUBBER EDIT ADD - Storyteller
return SS_INIT_SUCCESS
///Collects all data to persist.
diff --git a/code/controllers/subsystem/statpanel.dm b/code/controllers/subsystem/statpanel.dm
index 733b94d582a..4daf107083f 100644
--- a/code/controllers/subsystem/statpanel.dm
+++ b/code/controllers/subsystem/statpanel.dm
@@ -45,6 +45,7 @@ SUBSYSTEM_DEF(statpanels)
"Time Dilation: [round(SStime_track.time_dilation_current,1)]% AVG:([round(SStime_track.time_dilation_avg_fast,1)]%, [round(SStime_track.time_dilation_avg,1)]%, [round(SStime_track.time_dilation_avg_slow,1)]%)",
"Map: [SSmapping.config?.map_name || "Loading..."]",
cached ? "Next Map: [cached.map_name]" : null,
+ "Storyteller: [SSgamemode.storyteller ? SSgamemode.storyteller.name : "N/A"]", // BUBBER EDIT ADDITION
"Round ID: [GLOB.round_id ? GLOB.round_id : "NULL"]",
"Connected: [GLOB.clients.len] | Active: [active_players]/[CONFIG_GET(number/hard_popcap)] | Observing: [observing_players]", //BUBBER EDIT: ACTIVE AND OBSERVING PLAYERS
" ",
diff --git a/code/controllers/subsystem/ticker.dm b/code/controllers/subsystem/ticker.dm
index c15129d9345..2e2fd930ba1 100644
--- a/code/controllers/subsystem/ticker.dm
+++ b/code/controllers/subsystem/ticker.dm
@@ -169,6 +169,7 @@ SUBSYSTEM_DEF(ticker)
send2chat(new /datum/tgs_message_content("<@&[CONFIG_GET(string/game_alert_role_id)]> Round **[GLOB.round_id]** starting on [SSmapping.config.map_name], [CONFIG_GET(string/servername)]! \nIf you wish to be pinged for game related stuff, go to <#[CONFIG_GET(string/role_assign_channel_id)]> and assign yourself the roles."), CONFIG_GET(string/channel_announce_new_game)) // SKYRAT EDIT - Role ping and round ID in game-alert
// SKYRAT EDIT END
current_state = GAME_STATE_PREGAME
+ SSvote.initiate_vote(/datum/vote/storyteller, "Storyteller Vote", forced = TRUE) // BUBBER EDIT ADDITION
SStitle.change_title_screen() //SKYRAT EDIT ADDITION - Title screen
addtimer(CALLBACK(SStitle, TYPE_PROC_REF(/datum/controller/subsystem/title, change_title_screen)), 1 SECONDS) //SKYRAT EDIT ADDITION - Title screen
//Everyone who wants to be an observer is now spawned
@@ -246,7 +247,12 @@ SUBSYSTEM_DEF(ticker)
CHECK_TICK
//Configure mode and assign player to antagonists
var/can_continue = FALSE
- can_continue = SSdynamic.pre_setup() //Choose antagonists
+// can_continue = SSdynamic.pre_setup() //Choose antagonists // BUBBER EDIT - STORYTELLER (note: maybe disable)
+ //BUBBER EDIT BEGIN - STORYTELLER
+ SSgamemode.init_storyteller()
+ can_continue = SSgamemode.pre_setup()
+ //BUBBER EDIT END - STORYTELLER
+
CHECK_TICK
SEND_GLOBAL_SIGNAL(COMSIG_GLOB_PRE_JOBS_ASSIGNED, src)
can_continue = can_continue && SSjob.DivideOccupations() //Distribute jobs
@@ -315,6 +321,7 @@ SUBSYSTEM_DEF(ticker)
/datum/controller/subsystem/ticker/proc/PostSetup()
set waitfor = FALSE
SSdynamic.post_setup()
+ SSgamemode.post_setup() // BUBBER EDIT - Storyteller
GLOB.start_state = new /datum/station_state()
GLOB.start_state.count()
diff --git a/code/modules/admin/admin.dm b/code/modules/admin/admin.dm
index 5f40de037f4..e0651c9453a 100644
--- a/code/modules/admin/admin.dm
+++ b/code/modules/admin/admin.dm
@@ -21,8 +21,11 @@
var/dat = "
Game Panel
"
if(SSticker.current_state <= GAME_STATE_PREGAME)
- dat += "(Manage Dynamic Rulesets)
"
- dat += "(Force Roundstart Rulesets)
"
+ // BUBBER EDIT START - STORYTELLER
+ //dat += "(Manage Dynamic Rulesets)
"
+ //dat += "(Force Roundstart Rulesets)
"
+ dat += "(Game Mode Panel)
"
+ // BUBBER EDIT END
if (GLOB.dynamic_forced_roundstart_ruleset.len > 0)
for(var/datum/dynamic_ruleset/roundstart/rule in GLOB.dynamic_forced_roundstart_ruleset)
dat += {"-> [rule.name] <-
"}
@@ -31,7 +34,7 @@
dat += "
"
if(SSticker.IsRoundInProgress())
dat += "(Game Mode Panel)
"
- dat += "(Manage Dynamic Rulesets)
"
+ //dat += "(Manage Dynamic Rulesets)
" BUBBER EDIT - STORYTELLER
dat += {"
Create Object
diff --git a/code/modules/admin/topic.dm b/code/modules/admin/topic.dm
index 6fa2140d219..d8547e8afae 100644
--- a/code/modules/admin/topic.dm
+++ b/code/modules/admin/topic.dm
@@ -113,8 +113,8 @@
else if(href_list["gamemode_panel"])
if(!check_rights(R_ADMIN))
return
- SSdynamic.admin_panel()
-
+ //SSdynamic.admin_panel() // BUBBER EDIT - STORYTELLER
+ SSgamemode.admin_panel(usr) // BUBBER EDIT - STORYTELLER
else if(href_list["call_shuttle"])
if(!check_rights(R_ADMIN))
return
diff --git a/code/modules/events/_event.dm b/code/modules/events/_event.dm
index 39865fa956d..c815a23ce56 100644
--- a/code/modules/events/_event.dm
+++ b/code/modules/events/_event.dm
@@ -1,4 +1,4 @@
-#define RANDOM_EVENT_ADMIN_INTERVENTION_TIME (3 MINUTES) //SKYRAT EDIT CHANGE
+#define RANDOM_EVENT_ADMIN_INTERVENTION_TIME (2 MINUTES) //SKYRAT EDIT CHANGE
//this singleton datum is used by the events controller to dictate how it selects events
/datum/round_event_control
@@ -74,7 +74,7 @@
SHOULD_CALL_PARENT(TRUE)
if(occurrences >= max_occurrences)
return FALSE
- if(earliest_start >= world.time-SSticker.round_start_time)
+ if(!roundstart && earliest_start >= world.time-SSticker.round_start_time ) // BUBBER EDIT: Roundstart checks added
return FALSE
if(!allow_magic && wizardevent != SSevents.wizardmode)
return FALSE
@@ -115,19 +115,24 @@
// SKYRAT EDIT REMOVAL END - Event notification
// SKYRAT EDIT ADDITION BEGIN - Event notification
- message_admins("Random Event triggering in [DisplayTimeText(RANDOM_EVENT_ADMIN_INTERVENTION_TIME)]: [name]. (\
- CANCEL | \
- SOMETHING ELSE)")
- for(var/client/staff as anything in GLOB.admins)
- if(staff?.prefs.read_preference(/datum/preference/toggle/comms_notification))
- SEND_SOUND(staff, sound('sound/misc/server-ready.ogg'))
- sleep(RANDOM_EVENT_ADMIN_INTERVENTION_TIME * 0.5)
-
- if(triggering)
- message_admins("Random Event triggering in [DisplayTimeText(RANDOM_EVENT_ADMIN_INTERVENTION_TIME * 0.5)]: [name]. (\
- CANCEL | \
- SOMETHING ELSE)")
+ // BUBBER EDIT START - Only delay on roundstart
+ if(SSticker.HasRoundStarted())
+ message_admins("Random Event triggering in [DisplayTimeText(RANDOM_EVENT_ADMIN_INTERVENTION_TIME)]: [name]. (\
+ CANCEL | \
+ SOMETHING ELSE)")
+ for(var/client/staff as anything in GLOB.admins)
+ if(staff?.prefs.read_preference(/datum/preference/toggle/comms_notification))
+ SEND_SOUND(staff, sound('sound/misc/server-ready.ogg'))
sleep(RANDOM_EVENT_ADMIN_INTERVENTION_TIME * 0.5)
+
+ if(triggering)
+ message_admins("Random Event triggering in [DisplayTimeText(RANDOM_EVENT_ADMIN_INTERVENTION_TIME * 0.5)]: [name]. (\
+ CANCEL | \
+ SOMETHING ELSE)")
+ sleep(RANDOM_EVENT_ADMIN_INTERVENTION_TIME * 0.5)
+ else
+ message_admins(" Roundstart event chosen: [name].")
+ // BUBBER EDIT END
// SKYRAT EDIT ADDITION END - Event notification
if(!triggering)
diff --git a/code/modules/events/bureaucratic_error.dm b/code/modules/events/bureaucratic_error.dm
index 0e44ac391c1..f06e8f6d7f7 100644
--- a/code/modules/events/bureaucratic_error.dm
+++ b/code/modules/events/bureaucratic_error.dm
@@ -14,7 +14,7 @@
/datum/round_event/bureaucratic_error/start()
var/list/jobs = SSjob.get_valid_overflow_jobs()
- /* SKYRAT EDIT REMOVAL START
+ // BUBBER EDIT BEGIN /* SKYRAT EDIT REMOVAL START
if(prob(33)) // Only allows latejoining as a single role.
var/datum/job/overflow = pick_n_take(jobs)
overflow.spawn_positions = -1
@@ -23,7 +23,7 @@
var/datum/job/current = job
current.total_positions = 0
return
- */ // SKYRAT EDIT REMOVAL - no more locking off jobs
+ // BUBBER EDIT END */ // SKYRAT EDIT REMOVAL - no more locking off jobs
// Adds/removes a random amount of job slots from all jobs.
for(var/datum/job/current as anything in jobs)
current.total_positions = max(current.total_positions + rand(-2,4), 1) // SKYRAT EDIT CHANGE - No more locking off jobs - ORIGINAL: current.total_positions = max(current.total_positions + rand(-2,4), 0)
diff --git a/config/events.json b/config/events.json
new file mode 100644
index 00000000000..a30ccaf58ab
--- /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 5c3b55fd881..51355f6fa8b 100644
--- a/config/game_options.txt
+++ b/config/game_options.txt
@@ -141,7 +141,7 @@ ALLOW_RANDOM_EVENTS
## Multiplier for earliest start time of dangerous events.
## Set to 0 to make dangerous events avaliable from round start.
-EVENTS_MIN_TIME_MUL 5
+EVENTS_MIN_TIME_MUL 1
## Multiplier for minimal player count (players = alive non-AFK humans) for dangerous events to start.
## Set to 0 to make dangerous events avaliable for all populations.
@@ -585,3 +585,49 @@ NEGATIVE_STATION_TRAITS 3 1
# If set to 0, then players won't be able to select any positive quirks.
# If commented-out or undefined, the maximum default is 6.
MAX_POSITIVE_QUIRKS 6
+## 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/modular_skyrat/master_files/code/modules/events/_event.dm b/modular_skyrat/master_files/code/modules/events/_event.dm
index 91a5fd72f8d..2091d6ec6f6 100644
--- a/modular_skyrat/master_files/code/modules/events/_event.dm
+++ b/modular_skyrat/master_files/code/modules/events/_event.dm
@@ -1,4 +1,4 @@
-/datum/round_event_control/can_spawn_event(players_amt, allow_magic = FALSE)
+/* /datum/round_event_control/can_spawn_event(players_amt, allow_magic = FALSE) // BUBBER EDIT REMOVAL
SHOULD_CALL_PARENT(TRUE)
. = ..()
if(intensity_restriction && !GLOB.intense_event_credits)
@@ -17,3 +17,4 @@
else
log_game("ICES: [src.name] does not need an intensity credit. Intensity credit count: [GLOB.intense_event_credits] credits")
message_admins("ICES: [src.name] does not need an intensity credit. Intensity credit count: [GLOB.intense_event_credits] credits")
+*/
diff --git a/modular_skyrat/modules/ices_events/code/ICES_event_config.dm b/modular_skyrat/modules/ices_events/code/ICES_event_config.dm
index c65aafe5da5..fdb4d67a06f 100644
--- a/modular_skyrat/modules/ices_events/code/ICES_event_config.dm
+++ b/modular_skyrat/modules/ices_events/code/ICES_event_config.dm
@@ -44,10 +44,10 @@
frequency_upper = CONFIG_GET(number/event_frequency_upper)
intensity_credit_rate = CONFIG_GET(number/intensity_credit_rate)
-/**
+/** // BUBBER EDIT BEGIN
* Abductors
*/
-/datum/round_event_control/abductor
+/* /datum/round_event_control/abductor
max_occurrences = 1
weight = VERY_LOW_EVENT_FREQ
@@ -603,7 +603,7 @@
/datum/round_event_control/wormholes
max_occurrences = 2
weight = MED_EVENT_FREQ
-
+ */ // BUBBER EDIT END
#undef VERY_HIGH_EVENT_FREQ
diff --git a/modular_skyrat/modules/supermatter_surge/code/supermatter_surge.dm b/modular_skyrat/modules/supermatter_surge/code/supermatter_surge.dm
new file mode 100644
index 00000000000..47149390c9e
--- /dev/null
+++ b/modular_skyrat/modules/supermatter_surge/code/supermatter_surge.dm
@@ -0,0 +1,73 @@
+// BUBBER EDIT - REVERT TO TG
+/*#define SUPERMATTER_SURGE_BULLET_ENERGY_FACTOR_UPPER 300
+#define SUPERMATTER_SURGE_BULLET_ENERGY_FACTOR_LOWER 100
+#define SUPERMATTER_SURGE_TIME_UPPER 360 * 0.5
+#define SUPERMATTER_SURGE_TIME_LOWER 180 * 0.5
+#define SUPERMATTER_SURGE_ANNOUNCE_THRESHOLD 25
+#define LOWER_SURGE_LIMIT 100 to 150
+#define MIDDLE_SURGE_LIMIT 151 to 200
+#define UPPER_SURGE_LIMIT 201 to 250
+/**
+ * Supermatter Surge
+ *
+ * A very simple event designed to give engineering a challenge. It simply increases the supermatters power by a set amount, and announces it.
+ *
+ * This should be entirely fine for a powerful setup, but will require intervention on a lower power setup.
+ */
+
+/datum/round_event_control/supermatter_surge
+ name = "Supermatter Surge"
+ typepath = /datum/round_event/supermatter_surge
+ category = EVENT_CATEGORY_ENGINEERING
+ max_occurrences = 4
+ earliest_start = 30 MINUTES
+ description = "The supermatter will increase in power by a random amount, and announce it."
+ weight = 10 // BUBBER EDIT
+
+/datum/round_event/supermatter_surge
+ announce_when = 1
+ end_when = SUPERMATTER_SURGE_TIME_LOWER
+ /// How powerful is the supermatter surge going to be? Set in setup.
+ var/surge_power = SUPERMATTER_SURGE_BULLET_ENERGY_FACTOR_LOWER
+ var/starting_surge_power = 0
+ /// Typecasted reference to the supermatter chosen at the events start. Prevents the engine from going AWOL if it changes for some reason.
+ var/obj/machinery/power/supermatter_crystal/our_main_engine
+
+/datum/round_event/supermatter_surge/setup()
+ our_main_engine = GLOB.main_supermatter_engine
+ surge_power = rand(SUPERMATTER_SURGE_BULLET_ENERGY_FACTOR_LOWER, SUPERMATTER_SURGE_BULLET_ENERGY_FACTOR_UPPER)
+ starting_surge_power = our_main_engine.bullet_energy
+ end_when = rand(SUPERMATTER_SURGE_TIME_LOWER, SUPERMATTER_SURGE_TIME_UPPER)
+
+/datum/round_event/supermatter_surge/announce()
+ if(surge_power > SUPERMATTER_SURGE_ANNOUNCE_THRESHOLD || prob(round(surge_power)))
+ priority_announce("Class [get_surge_level()] supermatter surge detected. Intervention may be required.", "Anomaly Alert", ANNOUNCER_KLAXON)
+
+/datum/round_event/supermatter_surge/proc/get_surge_level()
+ switch(surge_power)
+ if(LOWER_SURGE_LIMIT)
+ return 4
+ if(MIDDLE_SURGE_LIMIT)
+ return 3
+ if(UPPER_SURGE_LIMIT)
+ return 2
+ else
+ return 1
+
+/datum/round_event/supermatter_surge/start()
+ our_main_engine?.bullet_energy *= surge_power
+
+/datum/round_event/supermatter_surge/end()
+ our_main_engine?.bullet_energy = starting_surge_power
+ priority_announce("The supermatter surge has dissipated.", "Anomaly Cleared")
+ our_main_engine = null
+
+#undef SUPERMATTER_SURGE_BULLET_ENERGY_FACTOR_UPPER
+#undef SUPERMATTER_SURGE_BULLET_ENERGY_FACTOR_LOWER
+#undef SUPERMATTER_SURGE_TIME_UPPER
+#undef SUPERMATTER_SURGE_TIME_LOWER
+#undef SUPERMATTER_SURGE_ANNOUNCE_THRESHOLD
+#undef LOWER_SURGE_LIMIT
+#undef MIDDLE_SURGE_LIMIT
+#undef UPPER_SURGE_LIMIT
+*/
diff --git a/modular_zubbers/code/modules/storyteller/config.dm b/modular_zubbers/code/modules/storyteller/config.dm
new file mode 100644
index 00000000000..121a512099d
--- /dev/null
+++ b/modular_zubbers/code/modules/storyteller/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/modular_zubbers/code/modules/storyteller/divergency_report.dm b/modular_zubbers/code/modules/storyteller/divergency_report.dm
new file mode 100644
index 00000000000..947dbaa158f
--- /dev/null
+++ b/modular_zubbers/code/modules/storyteller/divergency_report.dm
@@ -0,0 +1,60 @@
+/datum/controller/subsystem/gamemode/proc/send_trait_report()
+ . = "Central Command Status Summary
"
+ SSstation.generate_station_goals(20)
+
+ var/list/station_goals = SSstation.get_station_goals()
+
+ if(!length(station_goals))
+ . = "
No assigned goals.
"
+ else
+ . += generate_station_goal_report(station_goals)
+ if(!SSstation.station_traits.len)
+ . = "
No identified shift divergencies.
"
+ else
+ . += generate_station_trait_report()
+
+ . += "
This concludes your shift-start evaluation. Have a secure shift!
\
+ This label certifies an Intern has reviewed the above before sending. This document is the property of Nanotrasen Corporation.
"
+
+ print_command_report(., "Central Command Status Summary", announce = FALSE)
+ priority_announce("Hello, crew of [station_name()]. Our intern has finished their shift-start divergency and goals evaluation, which has been sent to your communications console. Have a secure shift!", "Divergency Report", SSstation.announcer.get_rand_report_sound())
+
+
+
+/*
+ * 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(var/list/station_goals)
+ . = "
Special Orders for [station_name()]:
"
+ var/list/goal_reports = list()
+ for(var/datum/station_goal/station_goal as anything in station_goals)
+ station_goal.on_report()
+ goal_reports += station_goal.get_report()
+
+ . += goal_reports.Join("
")
+ 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
+
+/*/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)
+ SSstation.goals_by_type += new picked // does this still work?
+*/
diff --git a/modular_zubbers/code/modules/storyteller/event_defines/_event.dm b/modular_zubbers/code/modules/storyteller/event_defines/_event.dm
new file mode 100644
index 00000000000..232381eddb3
--- /dev/null
+++ b/modular_zubbers/code/modules/storyteller/event_defines/_event.dm
@@ -0,0 +1,77 @@
+/datum/round_event_control
+ 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
+
+/datum/round_event
+ /// Whether the event called its start() yet or not.
+ var/has_started = FALSE
+
+/// 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 "Fire Schedule"
+ return "Fire Schedule"
+ else
+ return "Fire Schedule Force Next"
+ else
+ if(roundstart)
+ return "Force Roundstart"
+ else
+ return "Fire Schedule Force Next"
+
+/datum/round_event_control/Topic(href, href_list)
+ . = ..()
+ switch(href_list["action"])
+ if("force_next")
+ message_admins("[key_name_admin(usr)] has forced scheduled event [src.name].")
+ log_admin_private("[key_name(usr)] has forced scheduled event [src.name].")
+ SSgamemode.force_event(src)
diff --git a/modular_zubbers/code/modules/storyteller/event_defines/disabled_event_overrides.dm b/modular_zubbers/code/modules/storyteller/event_defines/disabled_event_overrides.dm
new file mode 100644
index 00000000000..9a122e270f7
--- /dev/null
+++ b/modular_zubbers/code/modules/storyteller/event_defines/disabled_event_overrides.dm
@@ -0,0 +1,3 @@
+/datum/round_event_control/sentient_disease
+ weight = 0
+ max_occurrences = 0
diff --git a/modular_zubbers/code/modules/storyteller/event_defines/major/major_overrides.dm b/modular_zubbers/code/modules/storyteller/event_defines/major/major_overrides.dm
new file mode 100644
index 00000000000..2caacdbdca1
--- /dev/null
+++ b/modular_zubbers/code/modules/storyteller/event_defines/major/major_overrides.dm
@@ -0,0 +1,54 @@
+/datum/round_event_control/earthquake
+ track = EVENT_TRACK_MAJOR
+ tags = list(TAG_DESTRUCTIVE)
+
+/datum/round_event_control/bureaucratic_error
+ track = EVENT_TRACK_MAJOR // Yes, it's annoying.
+ tags = list(TAG_COMMUNAL)
+ weight = 5
+
+/datum/round_event_control/blob
+ track = EVENT_TRACK_MAJOR
+ tags = list(TAG_DESTRUCTIVE, TAG_COMBAT)
+ weight = 10
+
+/datum/round_event_control/meteor_wave
+ track = EVENT_TRACK_MAJOR
+ tags = list(TAG_COMMUNAL, TAG_SPACE, TAG_DESTRUCTIVE)
+ weight = 10
+
+/datum/round_event_control/meteor_wave/meaty
+ weight = 15
+
+/datum/round_event_control/meteor_wave/ices
+ weight = 0
+
+/datum/round_event_control/radiation_storm
+ track = EVENT_TRACK_MAJOR
+ tags = list(TAG_COMMUNAL)
+
+/datum/round_event_control/wormholes
+ track = EVENT_TRACK_MAJOR
+ tags = list(TAG_COMMUNAL)
+
+/datum/round_event_control/immovable_rod
+ track = EVENT_TRACK_MAJOR
+ tags = list(TAG_DESTRUCTIVE)
+ weight = 20
+
+/datum/round_event_control/stray_meteor
+ track = EVENT_TRACK_MAJOR
+ tags = list(TAG_DESTRUCTIVE, TAG_SPACE)
+ weight = 25
+
+/datum/round_event_control/anomaly/anomaly_vortex
+ track = EVENT_TRACK_MAJOR
+ tags = list(TAG_DESTRUCTIVE)
+
+/datum/round_event_control/anomaly/anomaly_pyro
+ track = EVENT_TRACK_MAJOR
+ tags = list(TAG_DESTRUCTIVE)
+
+/datum/round_event_control/spider_infestation
+ track = EVENT_TRACK_ROLESET
+ tags = list(TAG_COMBAT)
diff --git a/modular_zubbers/code/modules/storyteller/event_defines/moderate/moderate_overrides.dm b/modular_zubbers/code/modules/storyteller/event_defines/moderate/moderate_overrides.dm
new file mode 100644
index 00000000000..18b9711fc06
--- /dev/null
+++ b/modular_zubbers/code/modules/storyteller/event_defines/moderate/moderate_overrides.dm
@@ -0,0 +1,55 @@
+/datum/round_event_control/brand_intelligence
+ track = EVENT_TRACK_MODERATE
+ tags = list(TAG_DESTRUCTIVE, TAG_COMMUNAL)
+
+/datum/round_event_control/carp_migration
+ track = EVENT_TRACK_MODERATE
+ tags = list(TAG_COMMUNAL)
+
+/datum/round_event_control/communications_blackout
+ track = EVENT_TRACK_MODERATE
+ tags = list(TAG_COMMUNAL, TAG_SPOOKY)
+
+/datum/round_event_control/grid_check
+ track = EVENT_TRACK_MODERATE
+ tags = list(TAG_COMMUNAL, TAG_SPOOKY)
+
+/datum/round_event_control/ion_storm
+ track = EVENT_TRACK_MODERATE
+ tags = list(TAG_TARGETED)
+
+/datum/round_event_control/processor_overload
+ track = EVENT_TRACK_MODERATE
+ tags = list(TAG_COMMUNAL)
+
+/datum/round_event_control/radiation_storm
+ max_occurrences = 2
+
+/datum/round_event_control/radiation_leak
+ track = EVENT_TRACK_MODERATE
+ tags = list(TAG_COMMUNAL)
+
+/datum/round_event_control/sandstorm
+ track = EVENT_TRACK_MODERATE
+ tags = list(TAG_DESTRUCTIVE)
+
+/datum/round_event_control/shuttle_catastrophe
+ track = EVENT_TRACK_MODERATE
+ tags = list(TAG_COMMUNAL)
+
+/datum/round_event_control/vent_clog
+ track = EVENT_TRACK_MODERATE
+ tags = list(TAG_COMMUNAL)
+
+/datum/round_event_control/anomaly
+ weight = 10 // Lower from original 15 because it KEEPS SPAWNING THEM
+ track = EVENT_TRACK_MODERATE
+ tags = list(TAG_COMMUNAL, TAG_DESTRUCTIVE)
+
+/datum/round_event_control/spacevine
+ track = EVENT_TRACK_MODERATE
+ tags = list(TAG_COMMUNAL, TAG_COMBAT)
+
+/datum/round_event_control/portal_storm_syndicate
+ track = EVENT_TRACK_MODERATE
+ tags = list(TAG_COMBAT)
diff --git a/modular_zubbers/code/modules/storyteller/event_defines/mundane/mundane_overrides.dm b/modular_zubbers/code/modules/storyteller/event_defines/mundane/mundane_overrides.dm
new file mode 100644
index 00000000000..20269c24a59
--- /dev/null
+++ b/modular_zubbers/code/modules/storyteller/event_defines/mundane/mundane_overrides.dm
@@ -0,0 +1,72 @@
+/datum/round_event_control/aurora_caelus
+ track = EVENT_TRACK_MUNDANE
+ tags = list(TAG_COMMUNAL, TAG_POSITIVE, TAG_SPACE)
+
+/datum/round_event_control/brain_trauma
+ track = EVENT_TRACK_MUNDANE
+ tags = list(TAG_TARGETED)
+
+/datum/round_event_control/camera_failure
+ track = EVENT_TRACK_MUNDANE
+ tags = list(TAG_COMMUNAL, TAG_SPOOKY)
+
+/datum/round_event_control/disease_outbreak
+ track = EVENT_TRACK_MUNDANE
+ tags = list(TAG_TARGETED)
+
+/datum/round_event_control/space_dust
+ track = EVENT_TRACK_MUNDANE
+ tags = list(TAG_DESTRUCTIVE, TAG_SPACE)
+
+/datum/round_event_control/electrical_storm
+ track = EVENT_TRACK_MUNDANE
+ tags = list(TAG_SPOOKY)
+
+/datum/round_event_control/fake_virus
+ track = EVENT_TRACK_MUNDANE
+ tags = list(TAG_TARGETED)
+
+/datum/round_event_control/falsealarm
+ track = EVENT_TRACK_MUNDANE
+ tags = list(TAG_COMMUNAL)
+
+/datum/round_event_control/market_crash
+ track = EVENT_TRACK_MUNDANE
+ tags = list(TAG_COMMUNAL)
+
+/datum/round_event_control/mice_migration
+ track = EVENT_TRACK_MUNDANE
+ tags = list(TAG_DESTRUCTIVE)
+
+/datum/round_event_control/wisdomcow
+ track = EVENT_TRACK_MUNDANE
+ tags = list(TAG_COMMUNAL, TAG_POSITIVE)
+
+/datum/round_event_control/shuttle_loan
+ var/list/run_situations = list()
+ track = EVENT_TRACK_MUNDANE
+ tags = list(TAG_COMMUNAL)
+
+/datum/round_event_control/mass_hallucination
+ track = EVENT_TRACK_MUNDANE
+ tags = list(TAG_COMMUNAL)
+
+/datum/round_event_control/stray_cargo
+ track = EVENT_TRACK_MUNDANE
+ tags = list(TAG_COMMUNAL)
+
+/datum/round_event_control/grey_tide
+ track = EVENT_TRACK_MUNDANE
+ tags = list(TAG_DESTRUCTIVE, TAG_SPOOKY)
+
+/datum/round_event_control/gravity_generator_blackout
+ track = EVENT_TRACK_MUNDANE
+ tags = list(TAG_COMMUNAL, TAG_SPACE)
+
+/datum/round_event_control/shuttle_insurance
+ track = EVENT_TRACK_MUNDANE
+ tags = list(TAG_COMMUNAL)
+
+/datum/round_event_control/tram_malfunction
+ track = EVENT_TRACK_MUNDANE
+ tags = list(TAG_DESTRUCTIVE)
diff --git a/modular_zubbers/code/modules/storyteller/event_defines/mundane/scrubber_overrides.dm b/modular_zubbers/code/modules/storyteller/event_defines/mundane/scrubber_overrides.dm
new file mode 100644
index 00000000000..8796ecb74d0
--- /dev/null
+++ b/modular_zubbers/code/modules/storyteller/event_defines/mundane/scrubber_overrides.dm
@@ -0,0 +1,19 @@
+/datum/round_event_control/scrubber_overflow
+ track = EVENT_TRACK_MUNDANE
+ tags = list(TAG_COMMUNAL)
+
+/datum/round_event_control/scrubber_overflow/threatening
+ weight = 0
+ max_occurrences = 0
+
+/datum/round_event_control/scrubber_overflow/catastrophic
+ weight = 0
+ max_occurrences = 0
+
+/datum/round_event_control/scrubber_overflow/every_vent
+ weight = 0
+ max_occurrences = 0
+
+/datum/round_event_control/scrubber_overflow/ices
+ weight = 0
+ max_occurrences = 0
diff --git a/modular_zubbers/code/modules/storyteller/event_defines/readme.md b/modular_zubbers/code/modules/storyteller/event_defines/readme.md
new file mode 100644
index 00000000000..30f37316fe6
--- /dev/null
+++ b/modular_zubbers/code/modules/storyteller/event_defines/readme.md
@@ -0,0 +1,11 @@
+# Storyteller event overrides/defines
+
+## Existing events
+All tg/skyrat event categorizations and adjustments are to be done in the correct track folder, in a .dm file whose name specifies it is an override.
+
+(An exception applies to antag rolls, which are handled independently from any tg or skyrat code)
+
+Some events might have multiple severities, these are to be put in the "multitrack" folder (If you don't see one that means there are no such events *yet*. Feel free to add it!)
+
+## New events
+So you have decided to create a new event. Depending on how much code you might need to write, you either create a single dm file in the relevant folder, or create a subfolder if you need more code!
diff --git a/modular_zubbers/code/modules/storyteller/event_defines/roleset/_antagonist_event.dm b/modular_zubbers/code/modules/storyteller/event_defines/roleset/_antagonist_event.dm
new file mode 100644
index 00000000000..88c90abc1db
--- /dev/null
+++ b/modular_zubbers/code/modules/storyteller/event_defines/roleset/_antagonist_event.dm
@@ -0,0 +1,158 @@
+/datum/round_event_control/antagonist
+ reoccurence_penalty_multiplier = 0
+ track = EVENT_TRACK_ROLESET
+ /// Protected roles from the antag roll. People will not get those roles if a config is enabled
+ var/protected_roles = list(
+ JOB_CAPTAIN,
+ JOB_BLUESHIELD,
+
+ // Heads of staff
+ JOB_HEAD_OF_PERSONNEL,
+ JOB_HEAD_OF_SECURITY,
+ JOB_CHIEF_ENGINEER,
+ JOB_CHIEF_MEDICAL_OFFICER,
+ JOB_RESEARCH_DIRECTOR,
+ JOB_QUARTERMASTER,
+ JOB_NT_REP,
+
+ // Seccies
+ JOB_DETECTIVE,
+ JOB_SECURITY_OFFICER,
+ JOB_WARDEN,
+ JOB_CORRECTIONS_OFFICER,
+ JOB_PRISONER,
+ JOB_SECURITY_MEDIC,
+ )
+ /// Restricted roles from the antag roll
+ var/restricted_roles = list(JOB_AI, JOB_CYBORG)
+
+ /// How many baseline antags do we spawn
+ var/base_antags = 2
+ /// How many maximum antags can we spawn
+ var/maximum_antags = 6
+ /// 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
+
+ var/minimum_candidate_base = 1
+
+ var/list/ruleset_lazy_templates
+
+/datum/round_event_control/antagonist/New()
+ . = ..()
+ if(CONFIG_GET(flag/protect_roles_from_antagonist))
+ restricted_roles |= protected_roles
+ restricted_roles |= SSstation.antag_restricted_roles
+ restricted_roles |= SSstation.antag_protected_roles
+ if(CONFIG_GET(flag/protect_assistant_from_antagonist))
+ restricted_roles |= JOB_ASSISTANT
+ for(var/datum/job/iterating_job as anything in subtypesof(/datum/job))
+ if(initial(iterating_job.restricted_antagonists))
+ restricted_roles |= initial(iterating_job.title)
+
+/datum/round_event_control/antagonist/can_spawn_event(popchecks = TRUE, allow_magic)
+ . = ..()
+ if(!.)
+ return
+ if(!roundstart && !SSgamemode.can_inject_antags())
+ return FALSE
+ var/list/candidates = get_candidates()
+ if(candidates.len < get_minimum_candidates())
+ return FALSE
+
+/datum/round_event_control/antagonist/proc/get_minimum_candidates()
+ return minimum_candidate_base
+
+/datum/round_event_control/antagonist/proc/get_candidates()
+ var/round_started = SSticker.HasRoundStarted()
+ var/list/candidates = SSgamemode.get_candidates(antag_flag, pick_roundstart_players = !round_started, restricted_roles = restricted_roles)
+ return candidates
+
+/datum/round_event_control/antagonist/solo
+ typepath = /datum/round_event/antagonist/solo
+
+/datum/round_event_control/antagonist/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/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
+ // 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()
+
+/datum/round_event/antagonist/solo
+
+/datum/round_event/antagonist/setup()
+ load_vars(control)
+ candidate_setup(control)
+ template_setup(control)
+
+/datum/round_event/antagonist/proc/load_vars(datum/round_event_control/antagonist/cast_control)
+ // Set up all the different variables on the round_event. God isn't it ugly
+ // Maybe move this to New() if possible? ~Waterpig
+ antag_flag = cast_control.antag_flag
+ antag_datum = cast_control.antag_datum
+ antag_count = cast_control.get_antag_amount()
+ restricted_roles = cast_control.restricted_roles
+
+/datum/round_event/antagonist/proc/candidate_setup(datum/round_event_control/antagonist/cast_control)
+ var/list/candidates = cast_control.get_candidates()
+ for(var/i in 1 to antag_count)
+ if(!candidates.len)
+ break
+ var/mob/candidate = pick_n_take(candidates)
+ setup_minds += candidate.mind
+ candidate_roles_setup(candidate)
+
+/datum/round_event/antagonist/proc/candidate_roles_setup(mob/candidate)
+ SHOULD_CALL_PARENT(FALSE)
+
+ candidate.mind.special_role = antag_flag
+ candidate.mind.restricted_roles = restricted_roles
+
+/datum/round_event/antagonist/proc/template_setup(datum/round_event_control/antagonist/cast_control)
+ for(var/template in cast_control.ruleset_lazy_templates)
+ SSmapping.lazy_load_template(template)
+
+/datum/round_event/antagonist/solo/start()
+ for(var/datum/mind/antag_mind as anything in setup_minds)
+ add_datum_to_mind(antag_mind)
+
+/datum/round_event/antagonist/proc/add_datum_to_mind(datum/mind/antag_mind)
+ antag_mind.add_antag_datum(antag_datum)
+
+/datum/round_event_control/antagonist/team
+ typepath = /datum/round_event/antagonist/team
+ minimum_candidate_base = 1
+
+ var/antag_leader_datum
+
+/datum/round_event_control/antagonist/team/New()
+ . = ..()
+ if(isnull(antag_leader_datum))
+ antag_leader_datum = antag_datum
+
+/datum/round_event/antagonist/team
+ var/antag_leader_datum
+
+/datum/round_event/antagonist/team/start()
+ for(var/datum/mind/antag_mind as anything in setup_minds)
+ add_datum_to_mind(antag_mind)
+
+/datum/round_event/antagonist/team/load_vars(datum/round_event_control/antagonist/team/cast_control)
+ . = ..()
+ antag_leader_datum = cast_control.antag_leader_datum
diff --git a/modular_zubbers/code/modules/storyteller/event_defines/roleset/crew_antagonists/bloodsucker.dm b/modular_zubbers/code/modules/storyteller/event_defines/roleset/crew_antagonists/bloodsucker.dm
new file mode 100644
index 00000000000..4c205671b70
--- /dev/null
+++ b/modular_zubbers/code/modules/storyteller/event_defines/roleset/crew_antagonists/bloodsucker.dm
@@ -0,0 +1,18 @@
+/datum/round_event_control/antagonist/solo/bloodsucker
+ name = "Bloodsuckers"
+ roundstart = TRUE
+
+ antag_flag = ROLE_BLOODSUCKER
+ antag_datum = /datum/antagonist/bloodsucker
+ weight = 2
+ min_players = 20
+
+ base_antags = 2
+ maximum_antags = 3
+
+ tags = list(TAG_COMBAT, TAG_SPOOKY, TAG_CREW_ANTAG)
+
+/datum/round_event_control/antagonist/solo/bloodsucker/midround
+ name = "Vampiric Accident"
+ roundstart = FALSE
+ weight = 6 // Outweight the other ghost roles slightly
diff --git a/modular_zubbers/code/modules/storyteller/event_defines/roleset/crew_antagonists/changeling.dm b/modular_zubbers/code/modules/storyteller/event_defines/roleset/crew_antagonists/changeling.dm
new file mode 100644
index 00000000000..a320c068e49
--- /dev/null
+++ b/modular_zubbers/code/modules/storyteller/event_defines/roleset/crew_antagonists/changeling.dm
@@ -0,0 +1,15 @@
+/datum/round_event_control/antagonist/solo/changeling
+ name = "Changelings"
+ roundstart = TRUE
+
+ antag_flag = ROLE_CHANGELING
+ antag_datum = /datum/antagonist/changeling
+ weight = 4
+ min_players = 20
+
+ tags = list(TAG_COMBAT, TAG_CREW_ANTAG)
+
+/datum/round_event_control/antagonist/solo/changeling/midround
+ name = "Genome Awakening (Changelings)"
+ roundstart = FALSE
+ weight = 6 // Outweight the other ghost roles slightly
diff --git a/modular_zubbers/code/modules/storyteller/event_defines/roleset/crew_antagonists/heretic.dm b/modular_zubbers/code/modules/storyteller/event_defines/roleset/crew_antagonists/heretic.dm
new file mode 100644
index 00000000000..bdb99df31f4
--- /dev/null
+++ b/modular_zubbers/code/modules/storyteller/event_defines/roleset/crew_antagonists/heretic.dm
@@ -0,0 +1,22 @@
+/datum/round_event_control/antagonist/solo/heretic
+ name = "Heretics"
+ roundstart = TRUE
+
+ antag_flag = ROLE_HERETIC
+ antag_datum = /datum/antagonist/heretic
+ weight = 4
+ min_players = 40
+
+ base_antags = 2
+ maximum_antags = 3
+
+ tags = list(TAG_COMBAT, TAG_SPOOKY, TAG_CREW_ANTAG)
+
+/datum/round_event_control/antagonist/solo/heretic/New()
+ protected_roles |= JOB_CHAPLAIN // Would be silly to get chaplain heretics
+ . = ..()
+
+/datum/round_event_control/antagonist/solo/heretic/midround
+ name = "Midround Heretics"
+ roundstart = FALSE
+ weight = 6 // Outweight the other ghost roles slightly
diff --git a/modular_zubbers/code/modules/storyteller/event_defines/roleset/crew_antagonists/malf.dm b/modular_zubbers/code/modules/storyteller/event_defines/roleset/crew_antagonists/malf.dm
new file mode 100644
index 00000000000..d361d9ad8a5
--- /dev/null
+++ b/modular_zubbers/code/modules/storyteller/event_defines/roleset/crew_antagonists/malf.dm
@@ -0,0 +1,46 @@
+
+/datum/round_event_control/antagonist/solo/malf
+ name = "Malfunctioning AI"
+
+ base_antags = 1
+ maximum_antags = 1
+
+ min_players = 20
+
+ antag_datum = /datum/antagonist/malf_ai
+ antag_flag = ROLE_MALF
+ weight = 0
+ tags = list(TAG_CREW_ANTAG, TAG_COMBAT, TAG_DESTRUCTIVE)
+ restricted_roles = list()
+
+/datum/round_event_control/antagonist/solo/malf/roundstart
+ roundstart = TRUE
+ typepath = /datum/round_event/antagonist/solo/malf_ai/roundstart
+ weight = 2
+
+// God has abandoned us
+/datum/round_event_control/antagonist/solo/malf/roundstart/get_candidates()
+ var/list/candidates = ..()
+ . = list()
+ var/datum/job/aijob = SSjob.GetJob(JOB_AI)
+ for(var/mob/candidate as anything in candidates)
+ if(SSjob.check_job_eligibility(candidate, aijob) == JOB_AVAILABLE)
+ . += candidate
+ return .
+
+/datum/round_event_control/antagonist/solo/malf/roundstart/can_spawn_event(popchecks, allow_magic)
+ . = ..()
+ if(!.)
+ return .
+
+ var/datum/job/ai_job = SSjob.GetJobType(/datum/job/ai)
+ if(!(ai_job.total_positions - ai_job.current_positions && ai_job.spawn_positions))
+ return FALSE
+ else
+ return TRUE
+
+/datum/round_event/antagonist/solo/malf_ai/roundstart/setup()
+ . = ..()
+ for(var/datum/mind/new_malf in setup_minds)
+ GLOB.pre_setup_antags += new_malf
+ LAZYADDASSOC(SSjob.dynamic_forced_occupations, new_malf.current, "AI")
diff --git a/modular_zubbers/code/modules/storyteller/event_defines/roleset/crew_antagonists/nuke_ops.dm b/modular_zubbers/code/modules/storyteller/event_defines/roleset/crew_antagonists/nuke_ops.dm
new file mode 100644
index 00000000000..d0de7691cda
--- /dev/null
+++ b/modular_zubbers/code/modules/storyteller/event_defines/roleset/crew_antagonists/nuke_ops.dm
@@ -0,0 +1,42 @@
+/datum/round_event_control/antagonist/team/nuke_ops
+ name = "Nuclear Operatives"
+ roundstart = TRUE
+
+ antag_flag = ROLE_OPERATIVE
+ antag_datum = /datum/antagonist/nukeop
+ antag_leader_datum = /datum/antagonist/nukeop/leader
+
+ weight = 0
+ tags = list(TAG_CREW_ANTAG)
+
+ base_antags = 2
+ maximum_antags = 5
+
+ typepath = /datum/round_event/antagonist/team/nukie
+
+ ruleset_lazy_templates = list(LAZY_TEMPLATE_KEY_NUKIEBASE)
+
+/datum/round_event/antagonist/team/nukie
+ var/datum/job/job_type = /datum/job/nuclear_operative
+ var/required_role = ROLE_NUCLEAR_OPERATIVE
+
+ var/datum/team/nuclear/nuke_team
+
+/datum/round_event/antagonist/team/nukie/candidate_roles_setup(mob/candidate)
+ candidate.mind.set_assigned_role(SSjob.GetJobType(job_type))
+ candidate.mind.special_role = required_role
+
+/datum/round_event/antagonist/team/nukie/start()
+ // Get our nukie leader
+ var/datum/mind/most_experienced = get_most_experienced(setup_minds, required_role)
+ if(!most_experienced)
+ most_experienced = setup_minds[1]
+ var/datum/antagonist/nukeop/leader/leader = most_experienced.add_antag_datum(antag_leader_datum)
+ nuke_team = leader.nuke_team
+
+ // Setup everyone else
+ for(var/datum/mind/assigned_player in setup_minds)
+ if(assigned_player == most_experienced)
+ continue
+ add_datum_to_mind(assigned_player)
+ return TRUE
diff --git a/modular_zubbers/code/modules/storyteller/event_defines/roleset/crew_antagonists/spies.dm b/modular_zubbers/code/modules/storyteller/event_defines/roleset/crew_antagonists/spies.dm
new file mode 100644
index 00000000000..49752994e4c
--- /dev/null
+++ b/modular_zubbers/code/modules/storyteller/event_defines/roleset/crew_antagonists/spies.dm
@@ -0,0 +1,14 @@
+/datum/round_event_control/antagonist/solo/spy
+ name = "Spies"
+ roundstart = TRUE
+
+ antag_flag = ROLE_SPY
+ antag_datum = /datum/antagonist/spy
+ weight = 4
+
+ tags = list(TAG_CREW_ANTAG)
+
+/datum/round_event_control/antagonist/solo/spy/midround
+ name = "Spies (Midround)"
+ roundstart = FALSE
+ weight = 7
diff --git a/modular_zubbers/code/modules/storyteller/event_defines/roleset/crew_antagonists/traitor.dm b/modular_zubbers/code/modules/storyteller/event_defines/roleset/crew_antagonists/traitor.dm
new file mode 100644
index 00000000000..2075027950d
--- /dev/null
+++ b/modular_zubbers/code/modules/storyteller/event_defines/roleset/crew_antagonists/traitor.dm
@@ -0,0 +1,14 @@
+/datum/round_event_control/antagonist/solo/traitor
+ name = "Traitors"
+ roundstart = TRUE
+
+ antag_flag = ROLE_TRAITOR
+ antag_datum = /datum/antagonist/traitor
+ weight = 6
+
+ tags = list(TAG_CREW_ANTAG)
+
+/datum/round_event_control/antagonist/solo/traitor/midround
+ name = "Sleeper Agents (Traitors)"
+ roundstart = FALSE
+ weight = 7
diff --git a/modular_zubbers/code/modules/storyteller/event_defines/roleset/roleset_ghost_overrides.dm b/modular_zubbers/code/modules/storyteller/event_defines/roleset/roleset_ghost_overrides.dm
new file mode 100644
index 00000000000..cbff2e9f15b
--- /dev/null
+++ b/modular_zubbers/code/modules/storyteller/event_defines/roleset/roleset_ghost_overrides.dm
@@ -0,0 +1,32 @@
+/datum/round_event_control/nightmare
+ track = EVENT_TRACK_ROLESET
+ tags = list(TAG_COMBAT, TAG_SPOOKY)
+
+/datum/round_event_control/revenant
+ min_players = 20
+ track = EVENT_TRACK_ROLESET
+ tags = list(TAG_DESTRUCTIVE, TAG_SPOOKY)
+
+/datum/round_event_control/space_dragon
+ track = EVENT_TRACK_ROLESET
+ tags = list(TAG_COMBAT)
+
+/datum/round_event_control/space_ninja
+ track = EVENT_TRACK_ROLESET
+ tags = list(TAG_COMBAT)
+
+/datum/round_event_control/changeling
+ track = EVENT_TRACK_ROLESET
+ tags = list(TAG_COMBAT, TAG_CREW_ANTAG)
+
+/datum/round_event_control/fugitives
+ track = EVENT_TRACK_ROLESET
+ tags = list(TAG_COMBAT)
+
+/datum/round_event_control/alien_infestation
+ track = EVENT_TRACK_ROLESET
+ tags = list(TAG_COMBAT, TAG_SPOOKY)
+
+/datum/round_event_control/abductor
+ track = EVENT_TRACK_ROLESET
+ tags = list(TAG_COMBAT, TAG_SPOOKY)
diff --git a/modular_zubbers/code/modules/storyteller/gamemode.dm b/modular_zubbers/code/modules/storyteller/gamemode.dm
new file mode 100644
index 00000000000..e8711f700cc
--- /dev/null
+++ b/modular_zubbers/code/modules/storyteller/gamemode.dm
@@ -0,0 +1,1042 @@
+#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/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/storyteller_voted = FALSE
+
+/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 || !event.valid_for_map())
+ continue //don't want this one! leave it for the garbage collector
+ 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
+// 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
+
+/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
+
+/// 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() / storyteller?.antag_divisor), 1) + sec_crew ? sec_crew : ANTAG_CAP_FLAT // Population divide by storyeteller's divisor, add one antag per sec.
+ return cap
+
+/// Whether events can inject more antagonists into the round
+/datum/controller/subsystem/gamemode/proc/can_inject_antags()
+ return (get_antag_cap() > length(GLOB.antagonists))
+
+/// Gets candidates for antagonist roles.
+
+/// Todo: Split into get_candidates and post_get_candidates
+/datum/controller/subsystem/gamemode/proc/get_candidates(
+ special_role_flag,
+ pick_observers,
+ pick_roundstart_players,
+ required_time,
+ inherit_required_time = TRUE,
+ no_antags = TRUE,
+ list/restricted_roles,
+ )
+
+
+ var/list/candidates = list()
+ var/list/candidate_candidates = list() //lol
+ if(pick_roundstart_players)
+ for(var/mob/dead/new_player/player in GLOB.new_player_list)
+ if(player.ready == PLAYER_READY_TO_PLAY && player.mind && player.check_preferences())
+ candidate_candidates += player
+ else if(pick_observers)
+ for(var/mob/player as anything in GLOB.dead_mob_list)
+ candidate_candidates += player
+ else
+ for(var/datum/record/locked/manifest_log as anything in GLOB.manifest.locked)
+ var/datum/mind/player_mind = manifest_log.mind_ref.resolve()
+ var/mob/living/player = player_mind.current
+ if(isnull(player))
+ continue
+ candidate_candidates += player
+
+
+ for(var/mob/candidate as anything in candidate_candidates)
+ if(QDELETED(candidate) || !candidate.key || !candidate.client || !candidate.mind)
+ continue
+ if(no_antags && candidate.mind.special_role)
+ continue
+ if(restricted_roles && (candidate.mind.assigned_role.title in restricted_roles))
+ continue
+ if(special_role_flag)
+ if(!(candidate.client.prefs) || !(special_role_flag 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[special_role_flag]
+
+ if(time_to_check && candidate.client.get_remaining_days(time_to_check) > 0)
+ continue
+
+ if(special_role_flag && is_banned_from(candidate.ckey, list(special_role_flag, ROLE_SYNDICATE)))
+ continue
+ if(is_banned_from(candidate.client.ckey, BAN_ANTAGONIST))
+ continue
+ if(!candidate.client?.prefs?.read_preference(/datum/preference/toggle/be_antag))
+ 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)
+
+/// Schedules an event.
+/datum/controller/subsystem/gamemode/proc/force_event(datum/round_event_control/event)
+ forced_next_events[event.track] = event
+
+/// 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)
+ 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 = min(max(low_pop_bound, active_players), high_pop_bound)
+
+ 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)
+ . = event.preRunEvent()
+ 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.run_event(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/forceEvent(mob/user)
+ SSgamemode.event_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()
+ // We need to do this to prevent some niche fuckery... and make dep. orders work. Lol
+ SSjob.ResetOccupations()
+ 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(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)
+ addtimer(CALLBACK(src, PROC_REF(send_trait_report)), rand(1 MINUTES, 5 MINUTES))
+ 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
+
+//////////////////////////
+//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
+ var/mob/living/carbon/human/H = C
+ 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(H.get_dnr()) //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(H.get_dnr()) //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())
+
+//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]
+ /// Prevent repeating storytellers
+ if(storyboy.name == SSpersistence.last_storyteller)
+ continue
+ 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
+ ///Because the vote subsystem is dumb and does not support any descriptions, we dump them into world.
+ to_chat(world, span_notice("[storyboy.name]"))
+ to_chat(world, span_notice("[storyboy.desc]"))
+ return choices
+
+/datum/controller/subsystem/gamemode/proc/storyteller_vote_result(winner_name)
+ /// Find the winner
+ /// Hijacking the proc because we don't have a vote right now..
+/* var/datum/storyteller/storyteller = pick(storytellers)
+ message_admins("We picked [storyteller]") */
+ voted_storyteller = winner_name
+ if(storyteller)
+ return
+ 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()
+ var/datum/storyteller/storyteller_pick
+ if(!voted_storyteller)
+ storyteller_pick = pick(storytellers)
+ message_admins("We picked [storyteller_pick] for this rounds storyteller, randomly.")
+ voted_storyteller = storyteller_pick
+ if(storyteller) // If this is true, then an admin bussed one, don't overwrite it
+ return
+ 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()
+ var/round_started = SSticker.HasRoundStarted()
+ var/list/dat = list()
+ var/active_pop = get_correct_popcount()
+ dat += "Storyteller: [storyteller ? "[storyteller.name]" : "None"] "
+ dat += " HALT Storyteller Event Panel Set Storyteller Refresh"
+ dat += "
Storyteller determines points gained, event chances, and is the entity responsible for rolling events."
+ dat += "
Active Players: [active_pop] (Head: [head_crew], Sec: [sec_crew], Eng: [eng_crew], Med: [med_crew]) - Antag Cap: [get_antag_cap()]"
+ dat += "
"
+ dat += "Main"
+ dat += " Variables"
+ dat += "
"
+ switch(panel_page)
+ if(GAMEMODE_PANEL_VARIABLES)
+ dat += "Reload Config Vars Configs 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 += ""
+ dat += ""
+ dat += "| Track | "
+ dat += "Progress | "
+ dat += "Next | "
+ dat += "Forced | "
+ dat += "Actions | "
+ dat += "
"
+ for(var/track in event_tracks)
+ even = !even
+ var/background_cl = even ? "#17191C" : "#23273C"
+ var/lower = event_track_points[track]
+ var/upper = point_thresholds[track]
+ var/percent = round((lower/upper)*100)
+ var/next = 0
+ var/last_points = last_point_gains[track]
+ if(last_points)
+ next = round((upper - lower) / last_points / STORYTELLER_WAIT_TIME * 40 / 6) / 10
+ dat += ""
+ dat += "| [track] | " //Track
+ dat += "[percent]% ([lower]/[upper]) | " //Progress
+ dat += "~[next] m. | " //Next
+ var/datum/round_event_control/forced_event = forced_next_events[track]
+ var/forced = forced_event ? "[forced_event.name] X" : ""
+ dat += "[forced] | " //Forced
+ dat += "Set Pts. Next Event | " //Actions
+ dat += "
"
+ dat += "
"
+
+ dat += "Scheduled Events:
"
+ dat += ""
+ dat += ""
+ dat += "| Name | "
+ dat += "Severity | "
+ dat += "Time | "
+ dat += "Actions | "
+ dat += "
"
+ var/sorted_scheduled = list()
+ for(var/datum/scheduled_event/scheduled as anything in scheduled_events)
+ sorted_scheduled[scheduled] = scheduled.start_time
+ sortTim(sorted_scheduled, cmp=/proc/cmp_numeric_asc, associative = TRUE)
+ even = TRUE
+ for(var/datum/scheduled_event/scheduled as anything in sorted_scheduled)
+ even = !even
+ var/background_cl = even ? "#17191C" : "#23273C"
+ dat += ""
+ dat += "| [scheduled.event.name] | " //Name
+ dat += "[scheduled.event.track] | " //Severity
+ var/time = (scheduled.event.roundstart && !round_started) ? "ROUNDSTART" : "[(scheduled.start_time - world.time) / (1 SECONDS)] s."
+ dat += "[time] | " //Time
+ dat += "[scheduled.get_href_actions()] | " //Actions
+ dat += "
"
+ dat += "
"
+
+ dat += "Running Events:
"
+ dat += ""
+ dat += ""
+ dat += "| Name | "
+ dat += "Actions | "
+ 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 += ""
+ dat += ""
+ dat += "| Name | "
+ dat += "Tags | "
+ dat += "Occurences | "
+ dat += "M.Pop | "
+ dat += "M.Time | "
+ dat += "Can Occur | "
+ dat += "Weight | "
+ dat += "Actions | "
+ dat += "
"
+ var/even = TRUE
+ var/total_weight = 0
+ var/list/event_lookup
+ switch(statistics_track_page)
+ if(ALL_EVENTS)
+ event_lookup = control
+ if(UNCATEGORIZED_EVENTS)
+ event_lookup = uncategorized
+ else
+ event_lookup = event_pools[statistics_track_page]
+ var/list/assoc_spawn_weight = list()
+ var/active_pop = get_correct_popcount()
+ for(var/datum/round_event_control/event as anything in event_lookup)
+ if(event.roundstart != roundstart_event_view)
+ continue
+ if(event.can_spawn_event(active_pop))
+ total_weight += event.calculated_weight
+ assoc_spawn_weight[event] = event.calculated_weight
+ else
+ assoc_spawn_weight[event] = 0
+ sortTim(assoc_spawn_weight, cmp=/proc/cmp_numeric_dsc, associative = TRUE)
+ for(var/datum/round_event_control/event as anything in assoc_spawn_weight)
+ even = !even
+ var/background_cl = even ? "#17191C" : "#23273C"
+ dat += ""
+ dat += "| [event.name] | " //Name
+ dat += "" //Tags
+ for(var/tag in event.tags)
+ dat += "[tag] "
+ dat += " | "
+ var/occurence_string = "[event.occurrences]"
+ if(event.shared_occurence_type)
+ occurence_string += " (shared: [event.get_occurences()])"
+ dat += "[occurence_string] | " //Occurences
+ dat += "[event.min_players] | " //Minimum pop
+ dat += "[event.earliest_start / (1 MINUTES)] m. | " //Minimum time
+ dat += "[assoc_spawn_weight[event] ? "Yes" : "No"] | " //Can happen?
+ var/weight_string = "([event.calculated_weight] /raw.[event.weight])"
+ if(assoc_spawn_weight[event])
+ var/percent = round((event.calculated_weight / total_weight) * 100)
+ weight_string = "[percent]% - [weight_string]"
+ dat += "[weight_string] | " //Weight
+ dat += "[event.get_href_actions()] | " //Actions
+ dat += "
"
+ dat += "
"
+ var/datum/browser/popup = new(user, "gamemode_event_panel", "Event Panel", 1000, 600)
+ popup.set_content(dat.Join())
+ popup.open()
+
+/datum/controller/subsystem/gamemode/Topic(href, href_list)
+ . = ..()
+ var/mob/user = usr
+ if(!check_rights(R_ADMIN))
+ return
+ switch(href_list["panel"])
+ if("main")
+ switch(href_list["action"])
+ if("set_storyteller")
+ message_admins("[key_name_admin(usr)] is picking a new Storyteller.")
+ var/list/name_list = list()
+ for(var/storyteller_type in storytellers)
+ var/datum/storyteller/storyboy = storytellers[storyteller_type]
+ name_list[storyboy.name] = storyboy.type
+ var/new_storyteller_name = input(usr, "Choose new storyteller (circumvents voted one):", "Storyteller") as null|anything in name_list
+ if(!new_storyteller_name)
+ message_admins("[key_name_admin(usr)] has cancelled picking a Storyteller.")
+ return
+ message_admins("[key_name_admin(usr)] has chosen [new_storyteller_name] as the new Storyteller.")
+ var/new_storyteller_type = name_list[new_storyteller_name]
+ set_storyteller(new_storyteller_type)
+ if("halt_storyteller")
+ halted_storyteller = !halted_storyteller
+ message_admins("[key_name_admin(usr)] has [halted_storyteller ? "HALTED" : "un-halted"] the Storyteller.")
+ if("vars")
+ var/track = href_list["track"]
+ switch(href_list["var"])
+ if("pts_multiplier")
+ var/new_value = input(usr, "New value:", "Set new value") as num|null
+ if(isnull(new_value) || new_value < 0)
+ return
+ message_admins("[key_name_admin(usr)] set point gain multiplier for [track] track to [new_value].")
+ point_gain_multipliers[track] = new_value
+ if("roundstart_pts")
+ var/new_value = input(usr, "New value:", "Set new value") as num|null
+ if(isnull(new_value) || new_value < 0)
+ return
+ message_admins("[key_name_admin(usr)] set roundstart pts multiplier for [track] track to [new_value].")
+ roundstart_point_multipliers[track] = new_value
+ if("min_pop")
+ var/new_value = input(usr, "New value:", "Set new value") as num|null
+ if(isnull(new_value) || new_value < 0)
+ return
+ message_admins("[key_name_admin(usr)] set minimum population for [track] track to [new_value].")
+ min_pop_thresholds[track] = new_value
+ if("pts_threshold")
+ var/new_value = input(usr, "New value:", "Set new value") as num|null
+ if(isnull(new_value) || new_value < 0)
+ return
+ message_admins("[key_name_admin(usr)] set point threshold of [track] track to [new_value].")
+ point_thresholds[track] = new_value
+ if("reload_config_vars")
+ message_admins("[key_name_admin(usr)] reloaded gamemode config vars.")
+ load_config_vars()
+ if("tab")
+ var/tab = href_list["tab"]
+ panel_page = tab
+ if("open_stats")
+ event_panel(user)
+ return
+ if("track_action")
+ var/track = href_list["track"]
+ if(!(track in event_tracks))
+ return
+ switch(href_list["track_action"])
+ if("remove_forced")
+ if(forced_next_events[track])
+ var/datum/round_event_control/event = forced_next_events[track]
+ message_admins("[key_name_admin(usr)] removed forced event [event.name] from track [track].")
+ forced_next_events -= track
+ if("set_pts")
+ var/set_pts = input(usr, "New point amount ([point_thresholds[track]]+ invokes event):", "Set points for [track]") as num|null
+ if(isnull(set_pts))
+ return
+ event_track_points[track] = set_pts
+ message_admins("[key_name_admin(usr)] set points of [track] track to [set_pts].")
+ log_admin_private("[key_name(usr)] set points of [track] track to [set_pts].")
+ if("next_event")
+ message_admins("[key_name_admin(usr)] invoked next event for [track] track.")
+ log_admin_private("[key_name(usr)] invoked next event for [track] track.")
+ event_track_points[track] = point_thresholds[track]
+ if(storyteller)
+ storyteller.handle_tracks()
+ admin_panel(user)
+ if("stats")
+ switch(href_list["action"])
+ if("set_roundstart")
+ roundstart_event_view = !roundstart_event_view
+ if("set_cat")
+ var/new_category = href_list["cat"]
+ if(new_category in EVENT_PANEL_TRACKS)
+ statistics_track_page = new_category
+ event_panel(user)
diff --git a/modular_zubbers/code/modules/storyteller/scheduled_event.dm b/modular_zubbers/code/modules/storyteller/scheduled_event.dm
new file mode 100644
index 00000000000..df501e14e49
--- /dev/null
+++ b/modular_zubbers/code/modules/storyteller/scheduled_event.dm
@@ -0,0 +1,96 @@
+///Scheduled event datum for SSgamemode to put events into.
+/datum/scheduled_event
+ /// What event are scheduling.
+ var/datum/round_event_control/event
+ /// When do we start our event
+ var/start_time = 0
+ /// If we were created by a storyteller, here's a cost to refund in case.
+ var/cost
+ /// Whether we alerted admins about this schedule when it's close to being invoked.
+ var/alerted_admins = FALSE
+ /// Whether we are faking an occurence or not
+ var/fakes_occurence = TRUE
+ /// Whether this ignores event can run checks. If bussed by an admin, you want to ignore checks
+ var/ignores_checks
+ /// Whether the scheduled event will override the announcement change. If null it won't. TRUE = force yes. FALSE = force no.
+ var/announce_change
+
+/datum/scheduled_event/New(datum/round_event_control/passed_event, passed_time, passed_cost, passed_ignore, passed_announce)
+ . = ..()
+ event = passed_event
+ start_time = passed_time
+ cost = passed_cost
+ ignores_checks = passed_ignore
+ announce_change = passed_announce
+ /// Add a fake occurence to make the weightings/checks properly respect the scheduled event.
+ event.add_occurence()
+ fakes_occurence = TRUE
+
+/datum/scheduled_event/proc/remove_occurence()
+ if(fakes_occurence)
+ /// Remove the fake occurence if we still have it
+ event.subtract_occurence()
+ fakes_occurence = FALSE
+
+/// For admins who want to reschedule the event.
+/datum/scheduled_event/proc/reschedule(new_time)
+ start_time = new_time
+ alerted_admins = FALSE
+
+/datum/scheduled_event/proc/get_href_actions()
+ var/round_started = SSticker.HasRoundStarted()
+ if(round_started)
+ return "Fire Reschedule Cancel Refund"
+ else
+ return "Cancel"
+
+/// Try and fire off the scheduled event
+/datum/scheduled_event/proc/try_fire()
+ /// Remove our fake occurence pre-emptively for the checks.
+ remove_occurence()
+ var/players_amt = get_active_player_count(alive_check = TRUE, afk_check = TRUE, human_check = FALSE)
+ ///If we can't spawn the scheduled event, refund it.
+ if(!ignores_checks && !event.can_spawn_event(players_amt)) //FALSE argument to ignore popchecks, to prevent scheduled events from failing from people dying/cryoing etc.
+ message_admins("Scheduled Event: [event] was unable to run and has been refunded.")
+ log_admin("Scheduled Event: [event] was unable to run and has been refunded.")
+
+ SSgamemode.refund_scheduled_event(src)
+ return
+
+ ///Trigger the event and remove the scheduled datum
+ message_admins("Scheduled Event: [event] successfully triggered.")
+ SSgamemode.TriggerEvent(event)
+ SSgamemode.remove_scheduled_event(src)
+
+/datum/scheduled_event/Destroy()
+ remove_occurence()
+ event = null
+ return ..()
+
+/datum/scheduled_event/Topic(href, href_list)
+ . = ..()
+ if(QDELETED(src))
+ return
+ var/round_started = SSticker.HasRoundStarted()
+ switch(href_list["action"])
+ if("cancel")
+ message_admins("[key_name_admin(usr)] cancelled scheduled event [event.name].")
+ log_admin_private("[key_name(usr)] cancelled scheduled event [event.name].")
+ SSgamemode.remove_scheduled_event(src)
+ if("refund")
+ message_admins("[key_name_admin(usr)] refunded scheduled event [event.name].")
+ log_admin_private("[key_name(usr)] refunded scheduled event [event.name].")
+ SSgamemode.refund_scheduled_event(src)
+ if("reschedule")
+ var/new_schedule = input(usr, "New schedule time (in seconds):", "Reschedule Event") as num|null
+ if(isnull(new_schedule) || QDELETED(src))
+ return
+ start_time = world.time + new_schedule * 1 SECONDS
+ message_admins("[key_name_admin(usr)] rescheduled event [event.name] to [new_schedule] seconds.")
+ log_admin_private("[key_name(usr)] rescheduled event [event.name] to [new_schedule] seconds.")
+ if("fire")
+ if(!round_started)
+ return
+ message_admins("[key_name_admin(usr)] has fired scheduled event [event.name].")
+ log_admin_private("[key_name(usr)] has fired scheduled event [event.name].")
+ try_fire()
diff --git a/modular_zubbers/code/modules/storyteller/storyteller_vote.dm b/modular_zubbers/code/modules/storyteller/storyteller_vote.dm
new file mode 100644
index 00000000000..99c3479e4a4
--- /dev/null
+++ b/modular_zubbers/code/modules/storyteller/storyteller_vote.dm
@@ -0,0 +1,68 @@
+/datum/vote/var/has_desc = FALSE
+
+/datum/vote/proc/return_desc(vote_name)
+ return ""
+
+/datum/vote/storyteller
+ name = "Storyteller"
+ default_message = "Vote for the storyteller!"
+ has_desc = TRUE
+ count_method = VOTE_COUNT_METHOD_MULTI
+ winner_method = VOTE_WINNER_METHOD_SIMPLE
+
+/datum/vote/storyteller/New()
+ . = ..()
+ default_choices = list()
+ default_choices = SSgamemode.storyteller_vote_choices()
+
+
+/datum/vote/storyteller/return_desc(vote_name)
+ return SSgamemode.storyteller_desc(vote_name)
+
+/datum/vote/storyteller/create_vote()
+ . = ..()
+ if((length(choices) == 1)) // Only one choice, no need to vote. Let's just auto-rotate it to the only remaining map because it would just happen anyways.
+ var/de_facto_winner = choices[1]
+ SSgamemode.storyteller_vote_result(de_facto_winner)
+ to_chat(world, span_boldannounce("The storyteller vote has been skipped because there is only one storyteller left to vote for. The storyteller has been changed to [de_facto_winner]."))
+ return FALSE
+
+/datum/vote/storyteller/can_be_initiated(mob/by_who, forced = FALSE)
+ . = ..()
+ if(forced)
+ return TRUE
+
+ if(SSgamemode.storyteller_voted)
+ default_message = "The next Storyteller has already been selected."
+ return FALSE
+
+/datum/vote/storyteller/finalize_vote(winning_option)
+ SSgamemode.storyteller_vote_result(winning_option)
+ SSgamemode.storyteller_voted = TRUE
+
+/*
+### PERSISTENCE SUBSYSTEM TRACKING BELOW ###
+Basically, this keeps track of what we voted last time to prevent it being voted on again.
+For this, we use the SSpersistence.last_storyteller variable
+
+We then just check what the last one is in SSgamemode.storyteller_vote_choices()
+*/
+
+#define STORYTELLER_LAST_FILEPATH "data/storyteller_last_round.txt"
+
+/// Extends collect_data
+/datum/controller/subsystem/persistence/collect_data()
+ . = ..()
+ collect_storyteller()
+
+/// Loads last storyteller into last_storyteller
+/datum/controller/subsystem/persistence/proc/load_storyteller()
+ if(!fexists(STORYTELLER_LAST_FILEPATH))
+ return
+ last_storyteller = file2text(STORYTELLER_LAST_FILEPATH)
+
+/// Collects current storyteller and stores it
+/datum/controller/subsystem/persistence/proc/collect_storyteller()
+ rustg_file_write("[SSgamemode.storyteller.name]", STORYTELLER_LAST_FILEPATH)
+
+#undef STORYTELLER_LAST_FILEPATH
diff --git a/modular_zubbers/code/modules/storyteller/storytellers/_storyteller.dm b/modular_zubbers/code/modules/storyteller/storytellers/_storyteller.dm
new file mode 100644
index 00000000000..a42c71d8a48
--- /dev/null
+++ b/modular_zubbers/code/modules/storyteller/storytellers/_storyteller.dm
@@ -0,0 +1,145 @@
+///The storyteller datum. He operates with the SSgamemode data to run events
+/datum/storyteller
+ /// Name of our storyteller.
+ var/name = "Badly coded storyteller"
+ /// Description of our storyteller.
+ var/desc = "Report this to the coders."
+ /// Text that the players will be greeted with when this storyteller is chosen.
+ var/welcome_text = "The storyteller has been selected. Get ready!"
+ /// This is the multiplier for repetition penalty in event weight. The lower the harsher it is
+ var/event_repetition_multiplier = 0.6
+ /// Multipliers for starting points.
+ var/list/starting_point_multipliers = list(
+ EVENT_TRACK_MUNDANE = 1,
+ EVENT_TRACK_MODERATE = 1,
+ EVENT_TRACK_MAJOR = 1,
+ EVENT_TRACK_ROLESET = 1,
+ EVENT_TRACK_OBJECTIVES = 1
+ )
+ /// Multipliers for point gains.
+ var/list/point_gains_multipliers = list(
+ EVENT_TRACK_MUNDANE = 1,
+ EVENT_TRACK_MODERATE = 1,
+ EVENT_TRACK_MAJOR = 1,
+ EVENT_TRACK_ROLESET = 1,
+ EVENT_TRACK_OBJECTIVES = 1
+ )
+ /// Multipliers of weight to apply for each tag of an event.
+ var/list/tag_multipliers
+
+ /// Variance in cost of the purchased events. Effectively affects frequency of events
+ var/cost_variance = 15
+
+ /// Variance in the budget of roundstart points.
+ var/roundstart_points_variance = 15
+
+ /// Whether the storyteller guaranteed a roleset roll (antag) on roundstart. (Still needs to pass pop check)
+ var/guarantees_roundstart_roleset = TRUE
+
+ /// Whether the storyteller has the distributions disabled. Important for ghost storytellers
+ var/disable_distribution = FALSE
+
+ /// Whether people can vote for the storyteller
+ var/votable = TRUE
+ /// If defined, will need a minimum of population to be votable
+ var/population_min
+ /// If defined, it will not be votable if exceeding the population
+ var/population_max
+ /// The antag divisor, the higher it is the lower the antag cap gets.
+ var/antag_divisor = 1
+
+/datum/storyteller/process(delta_time)
+ if(disable_distribution)
+ return
+ add_points(delta_time)
+ handle_tracks()
+
+/// Add points to all tracks while respecting the multipliers.
+/datum/storyteller/proc/add_points(delta_time)
+ var/datum/controller/subsystem/gamemode/mode = SSgamemode
+ var/base_point = EVENT_POINT_GAINED_PER_SECOND * delta_time * mode.event_frequency_multiplier
+ for(var/track in mode.event_track_points)
+ var/point_gain = base_point * point_gains_multipliers[track] * mode.point_gain_multipliers[track]
+ if(mode.allow_pop_scaling)
+ point_gain *= mode.current_pop_scale_multipliers[track]
+ mode.event_track_points[track] += point_gain
+ mode.last_point_gains[track] = point_gain
+
+/// Goes through every track of the gamemode and checks if it passes a threshold to buy an event, if does, buys one.
+/datum/storyteller/proc/handle_tracks()
+ . = FALSE //Has return value for the roundstart loop
+ var/datum/controller/subsystem/gamemode/mode = SSgamemode
+ for(var/track in mode.event_track_points)
+ var/points = mode.event_track_points[track]
+ if(points >= mode.point_thresholds[track] && find_and_buy_event_from_track(track))
+ . = TRUE
+
+/// Find and buy a valid event from a track.
+/datum/storyteller/proc/find_and_buy_event_from_track(track)
+ . = FALSE
+ var/datum/round_event_control/picked_event
+ if(SSgamemode.forced_next_events[track]) //Forced event by admin
+ /// Dont check any prerequisites, it has been forced by an admin
+ picked_event = SSgamemode.forced_next_events[track]
+ SSgamemode.forced_next_events -= track
+ else
+ var/player_pop = SSgamemode.get_correct_popcount()
+ var/pop_required = SSgamemode.min_pop_thresholds[track]
+ if(player_pop < pop_required)
+ message_admins("Storyteller failed to pick an event for track of [track] due to insufficient population. (required: [pop_required] active pop for [track]. Current: [player_pop])")
+ log_admin("Storyteller failed to pick an event for track of [track] due to insufficient population. (required: [pop_required] active pop for [track]. Current: [player_pop])")
+ SSgamemode.event_track_points[track] *= TRACK_FAIL_POINT_PENALTY_MULTIPLIER
+ return
+ calculate_weights(track)
+ var/list/valid_events = list()
+ // Determine which events are valid to pick
+ for(var/datum/round_event_control/event as anything in SSgamemode.event_pools[track])
+ if(event.can_spawn_event(player_pop))
+ valid_events[event] = event.calculated_weight
+ ///If we didn't get any events, remove the points inform admins and dont do anything
+ if(!length(valid_events))
+ message_admins("Storyteller failed to pick an event for track of [track].")
+ log_admin("Storyteller failed to pick an event for track of [track].")
+ SSgamemode.event_track_points[track] *= TRACK_FAIL_POINT_PENALTY_MULTIPLIER
+ return
+ picked_event = pick_weight(valid_events)
+ if(!picked_event)
+ message_admins("WARNING: Storyteller picked a null from event pool. Aborting event roll.")
+ log_admin("WARNING: Storyteller picked a null from event pool. Aborting event roll.")
+ stack_trace("WARNING: Storyteller picked a null from event pool.")
+ return
+ buy_event(picked_event, track)
+ . = TRUE
+
+/// Find and buy a valid event from a track.
+/datum/storyteller/proc/buy_event(datum/round_event_control/bought_event, track)
+ var/datum/controller/subsystem/gamemode/mode = SSgamemode
+ // Perhaps use some bell curve instead of a flat variance?
+ var/total_cost = bought_event.cost * mode.point_thresholds[track]
+ if(!bought_event.roundstart)
+ total_cost *= (1 + (rand(-cost_variance, cost_variance)/100)) //Apply cost variance if not roundstart event
+ mode.event_track_points[track] -= total_cost
+ message_admins("Storyteller purchased and triggered [bought_event] event, on [track] track, for [total_cost] cost.")
+ log_admin("Storyteller purchased and triggered [bought_event] event, on [track] track, for [total_cost] cost.")
+ if(bought_event.roundstart)
+ mode.TriggerEvent(bought_event)
+ else
+ mode.schedule_event(bought_event, (rand(3, 4) MINUTES), total_cost)
+
+/// Calculates the weights of the events from a passed track.
+/datum/storyteller/proc/calculate_weights(track)
+ var/datum/controller/subsystem/gamemode/mode = SSgamemode
+ for(var/datum/round_event_control/event as anything in mode.event_pools[track])
+ var/weight_total = event.weight
+ /// Apply tag multipliers if able
+ if(tag_multipliers)
+ for(var/tag in tag_multipliers)
+ if(tag in event.tags)
+ weight_total *= tag_multipliers[tag]
+ /// Apply occurence multipliers if able
+ var/occurences = event.get_occurences()
+ if(occurences)
+ ///If the event has occured already, apply a penalty multiplier based on amount of occurences
+ weight_total -= event.reoccurence_penalty_multiplier * weight_total * (1 - (event_repetition_multiplier ** occurences))
+ /// Write it
+ event.calculated_weight = weight_total
diff --git a/modular_zubbers/code/modules/storyteller/storytellers/storyteller_fragile.dm b/modular_zubbers/code/modules/storyteller/storytellers/storyteller_fragile.dm
new file mode 100644
index 00000000000..dcfa0d5f976
--- /dev/null
+++ b/modular_zubbers/code/modules/storyteller/storytellers/storyteller_fragile.dm
@@ -0,0 +1,36 @@
+/*
+/datum/storyteller/fragile
+ name = "The Fragile"
+ desc = "The Fragile will limit destructive and combat-focused events, and you'll rarely see midround antagonist roles that usually cause it."
+ welcome_text = "Handle with care!"
+
+ starting_point_multipliers = list(
+ EVENT_TRACK_MUNDANE = 1,
+ EVENT_TRACK_MODERATE = 1,
+ EVENT_TRACK_MAJOR = 1,
+ EVENT_TRACK_ROLESET = 4,
+ EVENT_TRACK_OBJECTIVES = 1
+ )
+
+ point_gains_multipliers = list(
+ EVENT_TRACK_MUNDANE = 1,
+ EVENT_TRACK_MODERATE = 1,
+ EVENT_TRACK_MAJOR = 1,
+ EVENT_TRACK_ROLESET = 0.1,
+ EVENT_TRACK_OBJECTIVES = 1
+ )
+
+ event_repetition_multiplier = 0.5
+
+ tag_multipliers = list(
+ TAG_COMBAT = 0.5,
+ TAG_SPOOKY = 1,
+ TAG_DESTRUCTIVE = 0.1,
+ TAG_COMMUNAL = 0.5,
+ TAG_TARGETED = 0.5,
+ TAG_POSITIVE = 2,
+ TAG_CREW_ANTAG = 2,
+ TAG_TEAM_ANTAG = 0.5,
+ TAG_OUTSIDER_ANTAG = 0.25
+ )
+*/
diff --git a/modular_zubbers/code/modules/storyteller/storytellers/storyteller_predictable.dm b/modular_zubbers/code/modules/storyteller/storytellers/storyteller_predictable.dm
new file mode 100644
index 00000000000..cb8377ff3fe
--- /dev/null
+++ b/modular_zubbers/code/modules/storyteller/storytellers/storyteller_predictable.dm
@@ -0,0 +1,156 @@
+/datum/storyteller/predictable
+ name = "The Predictable Chaos"
+ desc = "The Predictable Chaos will attempt to spawn a lot of antagonists relative to the crew population, while also ensuring events roll every set amount of time. Expect minor events every 10 minutes, moderate events every 30 minutes, and major events every hour and a half."
+ welcome_text = "Waiter, more chaos! That's enough. Thank you, waiter."
+ population_min = 35
+
+ var/crew_per_antag = 20 //Basically this means for every 10 crew, spawn 1 antag. REMEMBER: This is CREW pop, NOT server pop
+
+ tag_multipliers = list(
+
+ TAG_COMBAT = 0.25, //Already got this from the constant antag spawns.
+ TAG_POSITIVE = 1.25, //Increase this even more if there is too much chaos in events. This is basically how you balance this storyteller.
+
+ TAG_TARGETED = 0.25, //Let us not waste event rolls on single people every 5 or so minutes.
+
+ TAG_OUTSIDER_ANTAG = 0.25, //BurgerBB was here.
+
+ )
+
+ starting_point_multipliers = list(
+ EVENT_TRACK_MUNDANE = 2,
+ EVENT_TRACK_MODERATE = 2,
+ EVENT_TRACK_MAJOR = 2,
+ EVENT_TRACK_ROLESET = 2,
+ EVENT_TRACK_OBJECTIVES = 2
+ )
+
+ point_gains_multipliers = list(
+ EVENT_TRACK_MUNDANE = 2,
+ EVENT_TRACK_MODERATE = 2,
+ EVENT_TRACK_MAJOR = 2,
+ EVENT_TRACK_ROLESET = 2,
+ EVENT_TRACK_OBJECTIVES = 2
+ )
+
+ event_repetition_multiplier = 0.25 //Repeat events are boring.
+
+ var/last_crew_score = 0
+ var/last_antag_score = 0
+
+ var/antag_event_delay = 5 MINUTES
+ var/mundane_event_delay = 10 MINUTES
+ var/moderate_event_delay = 30 MINUTES
+ var/major_event_delay = 90 MINUTES
+
+ COOLDOWN_DECLARE(antag_event_cooldown)
+ COOLDOWN_DECLARE(mundane_event_cooldown)
+ COOLDOWN_DECLARE(moderate_event_cooldown)
+ COOLDOWN_DECLARE(major_event_cooldown)
+
+
+/datum/storyteller/predictable/New(...)
+ reset_cooldowns()
+ . = ..()
+
+
+/datum/storyteller/predictable/proc/reset_cooldowns()
+ COOLDOWN_START(src, mundane_event_cooldown, mundane_event_delay)
+ COOLDOWN_START(src, moderate_event_cooldown, moderate_event_delay)
+ COOLDOWN_START(src, major_event_cooldown, major_event_delay)
+
+
+//IF YOU EDIT THIS PROC, REMEMBER TO ALSO EDIT THE UNIT TESTS IN unit_tests/zubbers/predictable_storyteller.dm
+/proc/storyteller_get_antag_to_crew_ratio(do_debug=FALSE,minds_to_use_override)
+
+ var/total_crew_score = 0
+ var/total_antagonist_score = 0
+
+ if(!minds_to_use_override)
+ minds_to_use_override = SSticker.minds
+
+ var/medical_count = 0
+ var/engineering_count = 0
+
+ for(var/datum/mind/mob_mind as anything in minds_to_use_override)
+
+ if(do_debug)
+ if(!mob_mind.current)
+ continue
+ else
+ if(!mob_mind.key || !mob_mind.current)
+ continue
+
+ var/antagonist_score = 0
+ for(var/datum/antagonist/antag as anything in mob_mind.antag_datums)
+ if( !antag.show_in_antagpanel || (antag.antag_flags & FLAG_FAKE_ANTAG)) //For unimportant antags, like ashwalkers or valentines. You're not a real antag.
+ continue
+ antagonist_score += 1
+ //We add to the total antagonist score later.
+
+ if(mob_mind.assigned_role)
+ var/datum/job/current_job = mob_mind.assigned_role
+ if(current_job.faction == FACTION_STATION) //This means you're actually crew.
+ var/crew_score = 1 //You count as 1.
+ if(current_job.auto_deadmin_role_flags & DEADMIN_POSITION_SECURITY)
+ crew_score *= 2
+ if(current_job.auto_deadmin_role_flags & DEADMIN_POSITION_HEAD)
+ crew_score *= 1.25
+ if(antagonist_score > 0)
+ if(crew_score > 1)
+ antagonist_score *= crew_score //If you're an antagonist as an important role, then you're going to cause some chaos.
+ else
+ if(mob_mind.current && mob_mind.current.stat == DEAD)
+ crew_score *= -0.25 //If you're dead, then usually some chaos must be happening and you in fact are a slight burden towards the crew.
+ total_crew_score += crew_score
+ //Hacky nonsense here. Limits based on support roles.
+ //MEDICAL
+ if(current_job.supervisors == SUPERVISOR_CMO)
+ if(current_job.title == JOB_MEDICAL_DOCTOR)
+ medical_count += 1
+ else
+ medical_count += 0.5
+ //ENGINEERING
+ if(current_job.supervisors == SUPERVISOR_CE)
+ if(current_job.title == JOB_STATION_ENGINEER)
+ engineering_count += 1
+ else
+ engineering_count += 0.5
+
+ if(antagonist_score)
+ total_antagonist_score += antagonist_score
+
+ if(total_crew_score <= 0)
+ return INFINITY //Force infinity.
+
+ total_crew_score = total_crew_score*0.75 + min(total_crew_score*0.25,engineering_count*10) //Up to 25% penalty if there are no engineers.
+ total_crew_score = total_crew_score*0.25 + min(total_crew_score*0.75,medical_count*10) //Up to 75% penalty if there are no medical doctors.
+
+ return round(total_antagonist_score/total_crew_score,0.01)
+
+
+/datum/storyteller/predictable/handle_tracks()
+
+ if(!COOLDOWN_FINISHED(src,antag_event_cooldown)) //Don't want to run an antag event then suddenly have meteors.
+ return FALSE
+
+ if(SSshuttle.emergency.mode == SHUTTLE_IDLE) //Only do serious shit if the emergency shuttle is at Central Command and not in transit.
+ if(storyteller_get_antag_to_crew_ratio() < (1/crew_per_antag) && find_and_buy_event_from_track(EVENT_TRACK_ROLESET))
+ COOLDOWN_START(src,antag_event_cooldown,antag_event_delay)
+ return TRUE
+ if(COOLDOWN_FINISHED(src,major_event_cooldown) && find_and_buy_event_from_track(EVENT_TRACK_MAJOR))
+ COOLDOWN_START(src, major_event_cooldown, major_event_delay)
+ COOLDOWN_START(src, moderate_event_cooldown, moderate_event_delay)
+ COOLDOWN_START(src, mundane_event_cooldown, mundane_event_delay)
+ return TRUE
+
+ if(COOLDOWN_FINISHED(src,moderate_event_cooldown) && find_and_buy_event_from_track(EVENT_TRACK_MODERATE))
+ COOLDOWN_START(src, moderate_event_cooldown, moderate_event_delay)
+ COOLDOWN_START(src, mundane_event_cooldown, mundane_event_delay)
+ return TRUE
+
+ if(COOLDOWN_FINISHED(src,mundane_event_cooldown) && find_and_buy_event_from_track(EVENT_TRACK_MUNDANE))
+ COOLDOWN_START(src, mundane_event_cooldown, mundane_event_delay)
+ return TRUE
+
+ return FALSE
diff --git a/modular_zubbers/code/modules/storyteller/storytellers/storyteller_tellers.dm b/modular_zubbers/code/modules/storyteller/storytellers/storyteller_tellers.dm
new file mode 100644
index 00000000000..16373d9e3fa
--- /dev/null
+++ b/modular_zubbers/code/modules/storyteller/storytellers/storyteller_tellers.dm
@@ -0,0 +1,68 @@
+/datum/storyteller/guide
+ name = "The Guide"
+ desc = "The Guide is the default Storyteller, and the comparison point for every other Storyteller. Best for an average, varied experience."
+ antag_divisor = 8
+
+/datum/storyteller/sleeper
+ name = "The Sleeper"
+ desc = "The Sleeper will be light on events compared to the Guide, especially so on ones involving combat or destruction. Best for more chill rounds."
+ point_gains_multipliers = list(
+ EVENT_TRACK_MUNDANE = 1,
+ EVENT_TRACK_MODERATE = 0.7,
+ EVENT_TRACK_MAJOR = 0.7,
+ EVENT_TRACK_ROLESET = 0.7,
+ EVENT_TRACK_OBJECTIVES = 1
+ )
+ guarantees_roundstart_roleset = FALSE
+ tag_multipliers = list(TAG_COMBAT = 0.1, TAG_DESTRUCTIVE = 0.3)
+ antag_divisor = 32
+
+/datum/storyteller/jester
+ name = "The Jester"
+ desc = "The Jester will create the most events overall, with higher chances of repeating. Best for the most hectic rounds."
+ event_repetition_multiplier = 0.8
+ point_gains_multipliers = list(
+ EVENT_TRACK_MUNDANE = 1.2,
+ EVENT_TRACK_MODERATE = 1.4,
+ EVENT_TRACK_MAJOR = 1.4,
+ EVENT_TRACK_ROLESET = 1,
+ EVENT_TRACK_OBJECTIVES = 1
+ )
+ population_min = 35
+ antag_divisor = 8
+
+/datum/storyteller/warrior
+ name = "The Warrior"
+ desc = "The Warrior will create more antag-focused events than the Guide, but will spawn less events overall than the Jester. Best for more hectic rounds with a dash of combat."
+ point_gains_multipliers = list(
+ EVENT_TRACK_MUNDANE = 1,
+ EVENT_TRACK_MODERATE = 1.3,
+ EVENT_TRACK_MAJOR = 1.3,
+ EVENT_TRACK_ROLESET = 1,
+ EVENT_TRACK_OBJECTIVES = 1
+ )
+ tag_multipliers = list(TAG_COMBAT = 1.5)
+ population_min = 35
+ antag_divisor = 5
+
+/datum/storyteller/demoman
+ name = "The DemoMan"
+ desc = "The Demoman will focus on impactful environmental events, best for hectic shifts with relatively normal antagonists."
+ welcome_text = "What makes me a good demoman?"
+ point_gains_multipliers = list(
+ EVENT_TRACK_MUNDANE = 0.5,
+ EVENT_TRACK_MODERATE = 1.4,
+ EVENT_TRACK_MAJOR = 1.5,
+ EVENT_TRACK_ROLESET = 1,
+ EVENT_TRACK_OBJECTIVES = 0.8
+ )
+ tag_multipliers = list(TAG_DESTRUCTIVE = 2.5) // You asked and I delivered. Destructiveness increased
+ population_min = 25
+ antag_divisor = 10
+
+/datum/storyteller/ghost
+ name = "The Ghost"
+ desc = "The Ghost is the absence of a Storyteller. It will not spawn a single event of any sort, or run any Antagonists. Best for rounds where the population is so low that not even the Sleeper is low enough."
+ disable_distribution = TRUE
+ population_max = 35
+ antag_divisor = 32
diff --git a/modular_zubbers/maps/offstation/dauntless/mob_spawns.dm b/modular_zubbers/maps/offstation/dauntless/mob_spawns.dm
index b4068d22b4a..4d243f2544f 100644
--- a/modular_zubbers/maps/offstation/dauntless/mob_spawns.dm
+++ b/modular_zubbers/maps/offstation/dauntless/mob_spawns.dm
@@ -2,6 +2,7 @@
title = ROLE_DAUNTLESS
policy_index = ROLE_DAUNTLESS
akula_outfit = /datum/outfit/akula
+ antagonist_restricted = TRUE
// Dauntless Ghost Spawners (Lava)
diff --git a/tgstation.dme b/tgstation.dme
index 7229984ab1d..523647a56d2 100644
--- a/tgstation.dme
+++ b/tgstation.dme
@@ -495,6 +495,7 @@
#include "code\__DEFINES\~~bubber_defines\signals.dm"
#include "code\__DEFINES\~~bubber_defines\species.dm"
#include "code\__DEFINES\~~bubber_defines\status_indicator_defines.dm"
+#include "code\__DEFINES\~~bubber_defines\storyteller_defines.dm"
#include "code\__DEFINES\~~bubber_defines\traits.dm"
#include "code\__DEFINES\~~bubber_defines\transport.dm"
#include "code\__DEFINES\~~bubber_defines\___HELPERS\global_lists.dm"
@@ -8254,6 +8255,7 @@
#include "modular_skyrat\modules\subsystems\code\ticket_ping\adminhelp.dm"
#include "modular_skyrat\modules\subsystems\code\ticket_ping\preference.dm"
#include "modular_skyrat\modules\subsystems\code\ticket_ping\ticket_ss.dm"
+#include "modular_skyrat\modules\supermatter_surge\code\supermatter_surge.dm"
#include "modular_skyrat\modules\supersoups\code\soup_mixtures.dm"
#include "modular_skyrat\modules\supersoups\code\stove.dm"
#include "modular_skyrat\modules\Syndie_edits\code\area.dm"
@@ -8755,6 +8757,30 @@
#include "modular_zubbers\code\modules\status_effects\buffs\frenzy.dm"
#include "modular_zubbers\code\modules\status_indicators\status_indicator.dm"
#include "modular_zubbers\code\modules\status_indicators\status_indicator_pref.dm"
+#include "modular_zubbers\code\modules\storyteller\config.dm"
+#include "modular_zubbers\code\modules\storyteller\divergency_report.dm"
+#include "modular_zubbers\code\modules\storyteller\gamemode.dm"
+#include "modular_zubbers\code\modules\storyteller\scheduled_event.dm"
+#include "modular_zubbers\code\modules\storyteller\storyteller_vote.dm"
+#include "modular_zubbers\code\modules\storyteller\event_defines\_event.dm"
+#include "modular_zubbers\code\modules\storyteller\event_defines\disabled_event_overrides.dm"
+#include "modular_zubbers\code\modules\storyteller\event_defines\major\major_overrides.dm"
+#include "modular_zubbers\code\modules\storyteller\event_defines\moderate\moderate_overrides.dm"
+#include "modular_zubbers\code\modules\storyteller\event_defines\mundane\mundane_overrides.dm"
+#include "modular_zubbers\code\modules\storyteller\event_defines\mundane\scrubber_overrides.dm"
+#include "modular_zubbers\code\modules\storyteller\event_defines\roleset\_antagonist_event.dm"
+#include "modular_zubbers\code\modules\storyteller\event_defines\roleset\roleset_ghost_overrides.dm"
+#include "modular_zubbers\code\modules\storyteller\event_defines\roleset\crew_antagonists\bloodsucker.dm"
+#include "modular_zubbers\code\modules\storyteller\event_defines\roleset\crew_antagonists\changeling.dm"
+#include "modular_zubbers\code\modules\storyteller\event_defines\roleset\crew_antagonists\heretic.dm"
+#include "modular_zubbers\code\modules\storyteller\event_defines\roleset\crew_antagonists\malf.dm"
+#include "modular_zubbers\code\modules\storyteller\event_defines\roleset\crew_antagonists\nuke_ops.dm"
+#include "modular_zubbers\code\modules\storyteller\event_defines\roleset\crew_antagonists\spies.dm"
+#include "modular_zubbers\code\modules\storyteller\event_defines\roleset\crew_antagonists\traitor.dm"
+#include "modular_zubbers\code\modules\storyteller\storytellers\_storyteller.dm"
+#include "modular_zubbers\code\modules\storyteller\storytellers\storyteller_fragile.dm"
+#include "modular_zubbers\code\modules\storyteller\storytellers\storyteller_predictable.dm"
+#include "modular_zubbers\code\modules\storyteller\storytellers\storyteller_tellers.dm"
#include "modular_zubbers\code\modules\surgery\tools.dm"
#include "modular_zubbers\code\modules\surgery\bodyparts\dismemberment.dm"
#include "modular_zubbers\code\modules\surgery\bodyparts\species_parts\misc_bodyparts.dm"