Storyteller port from horizon [DNM] (#456)

<!-- Write **BELOW** The Headers and **ABOVE** The comments else it may
not be viewable. -->
<!-- You can view Contributing.MD for a detailed description of the pull
request process. -->

## About The Pull Request
Ports storyteller from horizon, who deleted their repo. 

### How it works

_Written by Majkl-J_

Basically, the storyteller runs several "Tracks" that slowly fill up
with points. Upon reaching their set limit, an event is chosen and
spawned, and the storyteller takes a somewhat-random amount of points
from that track.


![image](https://github.com/Bubberstation/Bubberstation/assets/49160555/23b6f7ba-4d5a-45dc-9ed6-e88ed02ee019)

In this example, the moderate track will soon spawn an event.

Each track has its own sets of events it can spawn, sorted by intensity
(Roleset is midround and roundstart antags). Every event also has little
tags that the storyteller can be made to prioritize.

As with dynamic, events have a certain weight that determines the chance
they roll. Prioritized events get a multiplier to this weight.


![image](https://github.com/Bubberstation/Bubberstation/assets/49160555/136374ac-feb0-408f-89bf-c862167e2d47)

In this example, we can see some of the tags. Some storytellers can be
made to increase/decrease the weight of these events.

#### Extra stuff it does

Keeps track of med, engineering, and sec players. Whilst this still has
no actual usage, it is good to know it is capable of this.

## Why It's Good For The Game

Storyteller is an alternate event controller, in my opinion, superior to
dynamic.

## Changelog

<!-- If your PR modifies aspects of the game that can be concretely
observed by players or admins you should add a changelog. If your change
does NOT meet this description, remove this section. Be sure to properly
mark your PRs to prevent unnecessary GBP loss. You can read up on GBP
and it's effects on PRs in the tgstation guides for contributors. Please
note that maintainers freely reserve the right to remove and add tags
should they deem it appropriate. You can attempt to finagle the system
all you want, but it's best to shoot for clear communication right off
the bat. -->

🆑 Nevimer, Majkl-J, BurgerBB, Azarak
add: Ported the storyteller event system from horizon
/🆑

<!-- Both 🆑's are required for the changelog to work! You can put
your name to the right of the first 🆑 if you want to overwrite your
GitHub username as author ingame. -->
<!-- You can use multiple of the same prefix (they're only used for the
icon ingame) and delete the unneeded ones. Despite some of the tags,
changelogs should generally represent how a player might be affected by
the changes rather than a summary of the PR's contents. -->

---------

Co-authored-by: The Sharkening <95130227+StrangeWeirdKitten@users.noreply.github.com>
Co-authored-by: iero <>
Co-authored-by: Waterpig <wtryoutube@seznam.cz>
Co-authored-by: Waterpig <49160555+Majkl-J@users.noreply.github.com>
Co-authored-by: Return <donwest947@gmail.com>
Co-authored-by: BurgerLUA <8602857+BurgerLUA@users.noreply.github.com>
This commit is contained in:
nevimer
2024-06-03 11:24:04 -04:00
committed by GitHub
parent ed2c342893
commit ba2b366c7d
41 changed files with 2774 additions and 29 deletions
@@ -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
@@ -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 .
@@ -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.
+1
View File
@@ -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
" ",
+8 -1
View File
@@ -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()
+6 -3
View File
@@ -21,8 +21,11 @@
var/dat = "<center><B>Game Panel</B></center><hr>"
if(SSticker.current_state <= GAME_STATE_PREGAME)
dat += "<A href='?src=[REF(src)];[HrefToken()];f_dynamic_ruleset_manage=1'>(Manage Dynamic Rulesets)</A><br>"
dat += "<A href='?src=[REF(src)];[HrefToken()];f_dynamic_roundstart=1'>(Force Roundstart Rulesets)</A><br>"
// BUBBER EDIT START - STORYTELLER
//dat += "<A href='?src=[REF(src)];[HrefToken()];f_dynamic_ruleset_manage=1'>(Manage Dynamic Rulesets)</A><br>"
//dat += "<A href='?src=[REF(src)];[HrefToken()];f_dynamic_roundstart=1'>(Force Roundstart Rulesets)</A><br>"
dat += "<a href='?src=[REF(src)];[HrefToken()];gamemode_panel=1'>(Game Mode Panel)</a><BR>"
// 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 += {"<A href='?src=[REF(src)];[HrefToken()];f_dynamic_roundstart_remove=[text_ref(rule)]'>-> [rule.name] <-</A><br>"}
@@ -31,7 +34,7 @@
dat += "<hr/>"
if(SSticker.IsRoundInProgress())
dat += "<a href='?src=[REF(src)];[HrefToken()];gamemode_panel=1'>(Game Mode Panel)</a><BR>"
dat += "<A href='?src=[REF(src)];[HrefToken()];f_dynamic_ruleset_manage=1'>(Manage Dynamic Rulesets)</A><br>"
//dat += "<A href='?src=[REF(src)];[HrefToken()];f_dynamic_ruleset_manage=1'>(Manage Dynamic Rulesets)</A><br>" BUBBER EDIT - STORYTELLER
dat += {"
<BR>
<A href='?src=[REF(src)];[HrefToken()];create_object=1'>Create Object</A><br>
+2 -2
View File
@@ -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
+19 -14
View File
@@ -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("<font color='[COLOR_ADMIN_PINK]'>Random Event triggering in [DisplayTimeText(RANDOM_EVENT_ADMIN_INTERVENTION_TIME)]: [name]. (\
<a href='?src=[REF(src)];cancel=1'>CANCEL</a> | \
<a href='?src=[REF(src)];something_else=1'>SOMETHING ELSE</a>)</font>")
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("<font color='[COLOR_ADMIN_PINK]'>Random Event triggering in [DisplayTimeText(RANDOM_EVENT_ADMIN_INTERVENTION_TIME * 0.5)]: [name]. (\
<a href='?src=[REF(src)];cancel=1'>CANCEL</a> | \
<a href='?src=[REF(src)];something_else=1'>SOMETHING ELSE</a>)</font>")
// BUBBER EDIT START - Only delay on roundstart
if(SSticker.HasRoundStarted())
message_admins("<font color='[COLOR_ADMIN_PINK]'>Random Event triggering in [DisplayTimeText(RANDOM_EVENT_ADMIN_INTERVENTION_TIME)]: [name]. (\
<a href='?src=[REF(src)];cancel=1'>CANCEL</a> | \
<a href='?src=[REF(src)];something_else=1'>SOMETHING ELSE</a>)</font>")
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("<font color='[COLOR_ADMIN_PINK]'>Random Event triggering in [DisplayTimeText(RANDOM_EVENT_ADMIN_INTERVENTION_TIME * 0.5)]: [name]. (\
<a href='?src=[REF(src)];cancel=1'>CANCEL</a> | \
<a href='?src=[REF(src)];something_else=1'>SOMETHING ELSE</a>)</font>")
sleep(RANDOM_EVENT_ADMIN_INTERVENTION_TIME * 0.5)
else
message_admins("<font color='[COLOR_ADMIN_PINK]'> Roundstart event chosen: [name].")
// BUBBER EDIT END
// SKYRAT EDIT ADDITION END - Event notification
if(!triggering)
+2 -2
View File
@@ -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)
+13
View File
@@ -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
}
}
+47 -1
View File
@@ -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
@@ -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")
*/
@@ -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
@@ -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
*/
@@ -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
@@ -0,0 +1,60 @@
/datum/controller/subsystem/gamemode/proc/send_trait_report()
. = "<b><i>Central Command Status Summary</i></b><hr>"
SSstation.generate_station_goals(20)
var/list/station_goals = SSstation.get_station_goals()
if(!length(station_goals))
. = "<hr><b>No assigned goals.</b><BR>"
else
. += generate_station_goal_report(station_goals)
if(!SSstation.station_traits.len)
. = "<hr><b>No identified shift divergencies.</b><BR>"
else
. += generate_station_trait_report()
. += "<hr>This concludes your shift-start evaluation. Have a secure shift!<hr>\
<p style=\"color: grey; text-align: justify;\">This label certifies an Intern has reviewed the above before sending. This document is the property of Nanotrasen Corporation.</p>"
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)
. = "<hr><b>Special Orders for [station_name()]:</b><BR>"
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("<hr>")
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
. = "<hr><b>Identified shift divergencies:</b><BR>"
for(var/datum/station_trait/station_trait as anything in SSstation.station_traits)
if(!station_trait.show_in_report)
continue
. += "[station_trait.get_report()]<BR>"
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?
*/
@@ -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 "<a class='linkOff'>Fire</a> <a class='linkOff'>Schedule</a>"
return "<a href='?src=[REF(src)];action=fire'>Fire</a> <a href='?src=[REF(src)];action=schedule'>Schedule</a>"
else
return "<a href='?src=[REF(src)];action=fire'>Fire</a> <a href='?src=[REF(src)];action=schedule'>Schedule</a> <a href='?src=[REF(src)];action=force_next'>Force Next</a>"
else
if(roundstart)
return "<a href='?src=[REF(src)];action=force_next'>Force Roundstart</a>"
else
return "<a class='linkOff'>Fire</a> <a class='linkOff'>Schedule</a> <a class='linkOff'>Force Next</a>"
/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)
@@ -0,0 +1,3 @@
/datum/round_event_control/sentient_disease
weight = 0
max_occurrences = 0
@@ -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)
@@ -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)
@@ -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)
@@ -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
@@ -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!
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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")
@@ -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
@@ -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
@@ -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
@@ -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)
File diff suppressed because it is too large Load Diff
@@ -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 "<a href='?src=[REF(src)];action=fire'>Fire</a> <a href='?src=[REF(src)];action=reschedule'>Reschedule</a> <a href='?src=[REF(src)];action=cancel'>Cancel</a> <a href='?src=[REF(src)];action=refund'>Refund</a></td>"
else
return "<a href='?src=[REF(src)];action=cancel'>Cancel</a>"
/// 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()
@@ -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
@@ -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
@@ -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
)
*/
@@ -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
@@ -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
@@ -2,6 +2,7 @@
title = ROLE_DAUNTLESS
policy_index = ROLE_DAUNTLESS
akula_outfit = /datum/outfit/akula
antagonist_restricted = TRUE
// Dauntless Ghost Spawners (Lava)
+26
View File
@@ -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"