diff --git a/code/__DEFINES/anomaly.dm b/code/__DEFINES/anomaly.dm index 05a9ff13b009f..9916dd48db6ff 100644 --- a/code/__DEFINES/anomaly.dm +++ b/code/__DEFINES/anomaly.dm @@ -4,7 +4,7 @@ */ ///Time in ticks before the anomaly goes poof/explodes depending on type. -#define ANOMALY_COUNTDOWN_TIMER (120 SECONDS) +#define ANOMALY_COUNTDOWN_TIMER (200 SECONDS) // BANDASTATION EDIT - STORYTELLER: 120 seconds -> 200 seconds /** * Nuisance/funny anomalies diff --git a/code/__DEFINES/antagonists.dm b/code/__DEFINES/antagonists.dm index 3c7ee3a214519..c8ae3a5e38013 100644 --- a/code/__DEFINES/antagonists.dm +++ b/code/__DEFINES/antagonists.dm @@ -392,6 +392,12 @@ GLOBAL_LIST_INIT(human_invader_antagonists, list( // This flag disables certain checks that presume antagonist datums mean 'baddie'. #define FLAG_FAKE_ANTAG (1 << 0) +// BANDASTATION EDIT START - STORYTELLER +// The storyteller will ignore this antag datum as counting against the antag cap. +#define FLAG_ANTAG_CAP_IGNORE (1 << 1) +// The storyteller will count everyone on this antag's team as a singular antag instead. +#define FLAG_ANTAG_CAP_TEAM (1 << 2) +// BANDASTATION EDIT END - STORYTELLER #define HUNTER_PACK_COPS "Spacepol Fugitive Hunters" #define HUNTER_PACK_RUSSIAN "Russian Fugitive Hunters" diff --git a/code/__HELPERS/roundend.dm b/code/__HELPERS/roundend.dm index 9d92766ab8bde..aef42f412f097 100644 --- a/code/__HELPERS/roundend.dm +++ b/code/__HELPERS/roundend.dm @@ -243,6 +243,10 @@ GLOBAL_LIST_INIT(achievements_unlocked, list()) //Set news report and mode result SSdynamic.set_round_result() + // BANDASTATION EDIT START - STORYTELLER + SSgamemode.round_end_report() + SSgamemode.store_roundend_data() // store data on roundend for next round + // BANDASTATION EDIT END - STORYTELLER to_chat(world, span_infoplain(span_big(span_bold("


The round has ended.")))) log_game("The round has ended.") @@ -305,6 +309,9 @@ GLOBAL_LIST_INIT(achievements_unlocked, list()) /datum/controller/subsystem/ticker/proc/build_roundend_report() var/list/parts = list() + //might want to make this a full section + parts += SSgamemode.create_roundend_score() // BANDASTATION EDIT - STORYTELLER + //AI laws parts += law_report() @@ -354,8 +361,11 @@ GLOBAL_LIST_INIT(achievements_unlocked, list()) else parts += "[FOURSPACES]Nobody died this shift!" + // BANDASTATION EDIT START - STORYTELLER + /* parts += "[FOURSPACES]Threat level: [SSdynamic.threat_level]" parts += "[FOURSPACES]Threat left: [SSdynamic.mid_round_budget]" + if(SSdynamic.roundend_threat_log.len) parts += "[FOURSPACES]Threat edits:" for(var/entry as anything in SSdynamic.roundend_threat_log) @@ -363,7 +373,8 @@ GLOBAL_LIST_INIT(achievements_unlocked, list()) parts += "[FOURSPACES]Executed rules:" for(var/datum/dynamic_ruleset/rule in SSdynamic.executed_rules) parts += "[FOURSPACES][FOURSPACES][rule.ruletype] - [rule.name]: -[rule.cost + rule.scaled_times * rule.scaling_cost] threat" - + */ + // BANDASTATION EDIT END - STORYTELLER return parts.Join("
") /client/proc/roundend_report_file() diff --git a/code/_globalvars/lists/objects.dm b/code/_globalvars/lists/objects.dm index 4707ebf0a962e..9c3e93144bc97 100644 --- a/code/_globalvars/lists/objects.dm +++ b/code/_globalvars/lists/objects.dm @@ -104,3 +104,4 @@ GLOBAL_LIST_INIT(prototype_organs, typecacheof(list( /obj/item/organ/eyes/dullahan, ), only_root_path = TRUE)) +GLOBAL_LIST_EMPTY(nuke_list) // BANDASTATION EDIT - STORYTELLER diff --git a/code/controllers/master.dm b/code/controllers/master.dm index 721920111e253..91bc6b052a31b 100644 --- a/code/controllers/master.dm +++ b/code/controllers/master.dm @@ -388,6 +388,7 @@ ADMIN_VERB(cmd_controller_view_ui, R_SERVER|R_DEBUG, "Controller Overview", "Vie if(sleep_offline_after_initializations && CONFIG_GET(flag/resume_after_initializations)) world.sleep_offline = FALSE initializations_finished_with_no_players_logged_in = initialized_tod < REALTIMEOFDAY - 10 + SSgamemode.handle_picking_storyteller() // BANDASTATION EDIT - STORYTELLER /** * Initialize a given subsystem and handle the results. diff --git a/code/controllers/subsystem/ambience.dm b/code/controllers/subsystem/ambience.dm index 80e15349212b7..40043aee344ae 100644 --- a/code/controllers/subsystem/ambience.dm +++ b/code/controllers/subsystem/ambience.dm @@ -113,7 +113,12 @@ SUBSYSTEM_DEF(ambience) return var/area/my_area = get_area(src) - var/sound_to_use = my_area.ambient_buzz + // BANDASTATION EDIT START - STORYTELLERS + var/sound_to_use + if(my_area) + sound_to_use= my_area.ambient_buzz + // BANDASTATION EDIT END - STORYTELLERS + var/volume_modifier = client.prefs.read_preference(/datum/preference/numeric/sound_ship_ambience_volume) if(!sound_to_use || !(client.prefs.read_preference(/datum/preference/numeric/sound_ship_ambience_volume))) diff --git a/code/controllers/subsystem/dynamic/dynamic.dm b/code/controllers/subsystem/dynamic/dynamic.dm index 2cbd451aaf934..54554349faeff 100644 --- a/code/controllers/subsystem/dynamic/dynamic.dm +++ b/code/controllers/subsystem/dynamic/dynamic.dm @@ -143,7 +143,7 @@ SUBSYSTEM_DEF(dynamic) var/waittime_h = 1800 /// A number between 0 and 100. The maximum amount of threat allowed to generate. - var/max_threat_level = 100 + var/max_threat_level = 0 // BANDASTATION EDIT - STORYTELLER /// The extra chance multiplier that a heavy impact midround ruleset will run next time. /// For example, if this is set to 50, then the next heavy roll will be about 50% more likely to happen. @@ -326,7 +326,8 @@ SUBSYSTEM_DEF(dynamic) continue min_threat = min(ruleset.cost, min_threat) - var/greenshift = GLOB.dynamic_forced_extended || (threat_level < min_threat) //if threat is below any ruleset, its extended time + // var/greenshift = GLOB.dynamic_forced_extended || (threat_level < min_threat) //if threat is below any ruleset, its extended time + var/greenshift = SSgamemode.current_storyteller.disable_distribution // BANDASTATION EDIT - STORYTELLER SSstation.generate_station_goals(greenshift ? INFINITY : CONFIG_GET(number/station_goal_budget)) var/list/datum/station_goal/goals = SSstation.get_station_goals() diff --git a/code/controllers/subsystem/dynamic/dynamic_rulesets_midround.dm b/code/controllers/subsystem/dynamic/dynamic_rulesets_midround.dm index c82cc649c645c..e3a167851c2d9 100644 --- a/code/controllers/subsystem/dynamic/dynamic_rulesets_midround.dm +++ b/code/controllers/subsystem/dynamic/dynamic_rulesets_midround.dm @@ -387,7 +387,7 @@ cost = 7 minimum_round_time = 70 MINUTES requirements = REQUIREMENTS_VERY_HIGH_THREAT_NEEDED - ruleset_lazy_templates = list(LAZY_TEMPLATE_KEY_NUKIEBASE) + //ruleset_lazy_templates = list(LAZY_TEMPLATE_KEY_NUKIEBASE) // BANDASTATION EDIT - STORYTELLER flags = HIGH_IMPACT_RULESET signup_item_path = /obj/machinery/nuclearbomb diff --git a/code/controllers/subsystem/dynamic/dynamic_rulesets_roundstart.dm b/code/controllers/subsystem/dynamic/dynamic_rulesets_roundstart.dm index 79eedc0adb8d7..facb56f315bd8 100644 --- a/code/controllers/subsystem/dynamic/dynamic_rulesets_roundstart.dm +++ b/code/controllers/subsystem/dynamic/dynamic_rulesets_roundstart.dm @@ -410,7 +410,7 @@ GLOBAL_VAR_INIT(revolutionary_win, FALSE) requirements = list(90,90,90,80,60,40,30,20,10,10) flags = HIGH_IMPACT_RULESET antag_cap = list("denominator" = 18, "offset" = 1) - ruleset_lazy_templates = list(LAZY_TEMPLATE_KEY_NUKIEBASE) + //ruleset_lazy_templates = list(LAZY_TEMPLATE_KEY_NUKIEBASE) // BANDASTATION EDIT - STORYTELLER var/required_role = ROLE_NUCLEAR_OPERATIVE var/datum/team/nuclear/nuke_team ///The job type to dress up our nuclear operative as. diff --git a/code/controllers/subsystem/dynamic/dynamic_unfavorable_situation.dm b/code/controllers/subsystem/dynamic/dynamic_unfavorable_situation.dm index 994f2e3f5de78..06b5a1d7626f9 100644 --- a/code/controllers/subsystem/dynamic/dynamic_unfavorable_situation.dm +++ b/code/controllers/subsystem/dynamic/dynamic_unfavorable_situation.dm @@ -29,18 +29,25 @@ return list() var/list/possible_heavies = list() - for (var/datum/dynamic_ruleset/midround/ruleset as anything in midround_rules) + // BANDASTATION EDIT START - STORYTELLER + var/list/generated_midround_rules = init_rulesets(/datum/dynamic_ruleset/midround) + for (var/datum/dynamic_ruleset/midround/ruleset as anything in generated_midround_rules) + // BANDASTATION EDIT END - STORYTELLER if (ruleset.midround_ruleset_style != MIDROUND_RULESET_STYLE_HEAVY) continue if (ruleset.weight == 0) continue + // BANDASTATION EDIT START - STORYTELLER + /* if (ruleset.cost > max_threat_level) continue if (!ruleset.acceptable(GLOB.alive_player_list.len, threat_level)) continue + */ + // BANDASTATION EDIT END - STORYTELLER if (ruleset.minimum_round_time > world.time - SSticker.round_start_time) continue diff --git a/code/controllers/subsystem/events.dm b/code/controllers/subsystem/events.dm index 1710d1d9a021d..ab4731412d37f 100644 --- a/code/controllers/subsystem/events.dm +++ b/code/controllers/subsystem/events.dm @@ -73,7 +73,7 @@ SUBSYSTEM_DEF(events) */ /datum/controller/subsystem/events/proc/spawnEvent(datum/round_event_control/excluded_event) set waitfor = FALSE //for the admin prompt - if(!CONFIG_GET(flag/allow_random_events)) + if(!CONFIG_GET(flag/allow_random_events) || !excluded_event) // BANDASTATION EDIT START - STORYTELLER - No SSevents spawn except rerolling return var/players_amt = get_active_player_count(alive_check = TRUE, afk_check = TRUE, human_check = TRUE) @@ -86,8 +86,8 @@ SUBSYSTEM_DEF(events) continue if(!event_to_check.can_spawn_event(players_amt)) continue - if(event_to_check.weight < 0) //for round-start events etc. - var/res = TriggerEvent(event_to_check) + if(event_to_check.roundstart) //for round-start events etc. + var/res = SSgamemode.TriggerEvent(event_to_check) if(res == EVENT_INTERRUPTED) continue //like it never happened if(res == EVENT_CANT_RUN) @@ -97,15 +97,25 @@ SUBSYSTEM_DEF(events) var/datum/round_event_control/event_to_run = pick_weight(event_roster) if(event_to_run) - TriggerEvent(event_to_run) + // BANDASTATION EDIT START - STORYTELLER + /// TriggerEvent(event_to_run) + SSgamemode.TriggerEvent(event_to_run, forced = FALSE) + // BANDASTATION EDIT END - STORYTELLER ///Does the last pre-flight checks for the passed event, and runs it if the event is ready. + /datum/controller/subsystem/events/proc/TriggerEvent(datum/round_event_control/event_to_trigger) . = event_to_trigger.preRunEvent() if(. == EVENT_CANT_RUN)//we couldn't run this event for some reason, set its max_occurrences to 0 event_to_trigger.max_occurrences = 0 else if(. == EVENT_READY) - event_to_trigger.run_event(random = TRUE) + // BANDASTATION EDIT START - STORYTELLER + //event_to_trigger.run_event(random = TRUE) + message_admins("SSevents runs and try to buy a event: [event_to_trigger.name]!") + log_game("SSevents runs and try to buy a event: [event_to_trigger.name]!") + SSgamemode.current_storyteller.try_buy_event(src) + // BANDASTATION EDIT END - STORYTELLER + ///Toggles whether or not wizard events will be in the event pool, and sends a notification to the admins. /datum/controller/subsystem/events/proc/toggleWizardmode() diff --git a/code/controllers/subsystem/job.dm b/code/controllers/subsystem/job.dm index 9af14f226ace5..2915b43bfbdee 100644 --- a/code/controllers/subsystem/job.dm +++ b/code/controllers/subsystem/job.dm @@ -84,6 +84,10 @@ SUBSYSTEM_DEF(job) ## The game will not read any line that is commented out with a '#', as to allow you to defer to codebase defaults.\n## If you want to override the codebase values, add the value and then uncomment that line by removing the # from the job key's name.\n\ ## Ensure that the key is flush, do not introduce any whitespaces when you uncomment a key. For example:\n## \"# Total Positions\" should always be changed to \"Total Positions\", no additional spacing.\n\ ## Best of luck editing!\n" + // BANDASTATIONE EDIT START - STORYTELLER + /// Assoc list of new players keyed to the type of job they will currently get + var/list/assigned_players_by_job = list() + // BANDASTATIONE EDIT END - STORYTELLER /datum/controller/subsystem/job/Initialize() setup_job_lists() @@ -979,3 +983,98 @@ SUBSYSTEM_DEF(job) return TRUE return FALSE + +// BANDASTATION EDIT START - STORYTELLER +/datum/controller/subsystem/job/proc/FreeRole(rank) + if(!rank) + return + job_debug("Freeing role: [rank]") + var/datum/job/job = get_job(rank) + if(!job) + return FALSE + job.current_positions = max(0, job.current_positions - 1) + +/datum/controller/subsystem/job/proc/SetupOccupations() + name_occupations = list() + type_occupations = list() + + var/list/all_jobs = subtypesof(/datum/job) + if(!length(all_jobs)) + all_occupations = list() + joinable_occupations = list() + joinable_departments = list() + joinable_departments_by_type = list() + experience_jobs_map = list() + to_chat(world, span_boldannounce("Error setting up jobs, no job datums found")) + return FALSE + + var/list/new_all_occupations = list() + var/list/new_joinable_occupations = list() + var/list/new_joinable_departments = list() + var/list/new_joinable_departments_by_type = list() + var/list/new_experience_jobs_map = list() + + for(var/job_type in all_jobs) + var/datum/job/job = new job_type() + if(!job.config_check()) + continue + if(!job.map_check()) //Even though we initialize before mapping, this is fine because the config is loaded at new + log_job_debug("Removed [job.title] due to map config") + continue + new_all_occupations += job + name_occupations[job.title] = job + type_occupations[job_type] = job + if(job.job_flags & JOB_NEW_PLAYER_JOINABLE) + new_joinable_occupations += job + if(!LAZYLEN(job.departments_list)) + var/datum/job_department/department = new_joinable_departments_by_type[/datum/job_department/undefined] + if(!department) + department = new /datum/job_department/undefined() + new_joinable_departments_by_type[/datum/job_department/undefined] = department + department.add_job(job) + continue + for(var/department_type in job.departments_list) + var/datum/job_department/department = new_joinable_departments_by_type[department_type] + if(!department) + department = new department_type() + new_joinable_departments_by_type[department_type] = department + department.add_job(job) + + sortTim(new_all_occupations, GLOBAL_PROC_REF(cmp_job_display_asc)) + for(var/datum/job/job as anything in new_all_occupations) + if(!job.exp_granted_type) + continue + new_experience_jobs_map[job.exp_granted_type] += list(job) + + sortTim(new_joinable_departments_by_type, GLOBAL_PROC_REF(cmp_department_display_asc), associative = TRUE) + for(var/department_type in new_joinable_departments_by_type) + var/datum/job_department/department = new_joinable_departments_by_type[department_type] + sortTim(department.department_jobs, GLOBAL_PROC_REF(cmp_job_display_asc)) + new_joinable_departments += department + if(department.department_experience_type) + new_experience_jobs_map[department.department_experience_type] = department.department_jobs.Copy() + + all_occupations = new_all_occupations + joinable_occupations = sortTim(new_joinable_occupations, GLOBAL_PROC_REF(cmp_job_display_asc)) + joinable_departments = new_joinable_departments + joinable_departments_by_type = new_joinable_departments_by_type + experience_jobs_map = new_experience_jobs_map + + return TRUE + +/datum/controller/subsystem/job/proc/SendToLateJoin(mob/M, buckle = TRUE) + var/atom/destination + + if(M.mind && !is_unassigned_job(M.mind.assigned_role) && length(GLOB.jobspawn_overrides[M.mind.assigned_role.title])) //We're doing something special today. + destination = pick(GLOB.jobspawn_overrides[M.mind.assigned_role.title]) + destination.JoinPlayerHere(M, FALSE) + return TRUE + + if(latejoin_trackers.len) + destination = pick(latejoin_trackers) + destination.JoinPlayerHere(M, buckle) + return TRUE + + destination = get_last_resort_spawn_points() + destination.JoinPlayerHere(M, buckle) +// BANDASTATION EDIT END - STORYTELLER diff --git a/code/controllers/subsystem/statpanel.dm b/code/controllers/subsystem/statpanel.dm index d6bd2a95944c7..d0603f52d14fd 100644 --- a/code/controllers/subsystem/statpanel.dm +++ b/code/controllers/subsystem/statpanel.dm @@ -26,6 +26,7 @@ SUBSYSTEM_DEF(statpanels) global_data = list( "Map: [SSmapping.current_map?.map_name || "Loading..."]", cached ? "Next Map: [cached.map_name]" : null, + "Storyteller: [!SSgamemode.secret_storyteller && SSgamemode.current_storyteller ? SSgamemode.current_storyteller.name : "Secret"]", // BANDASTATION ADDITION - STORYTELLER "Round ID: [GLOB.round_id ? GLOB.round_id : "NULL"]", "Players Connected: [LAZYLEN(GLOB.clients)]", // BANDASTATION ADD "Players in Lobby: [LAZYLEN(GLOB.new_player_list)]", // BANDASTATION ADD diff --git a/code/controllers/subsystem/ticker.dm b/code/controllers/subsystem/ticker.dm index 7760bb4430e79..cf6ec92d25442 100644 --- a/code/controllers/subsystem/ticker.dm +++ b/code/controllers/subsystem/ticker.dm @@ -162,11 +162,13 @@ SUBSYSTEM_DEF(ticker) to_chat(world, span_notice("Welcome to [station_name()]!")) send2chat(new /datum/tgs_message_content("New round starting on [SSmapping.current_map.map_name]!"), CONFIG_GET(string/channel_announce_new_game)) current_state = GAME_STATE_PREGAME + //SSvote.initiate_vote(/datum/vote/storyteller, "Storyteller Vote", forced = TRUE) // BANDASTATION EDIT - STORYTELLER SEND_SIGNAL(src, COMSIG_TICKER_ENTER_PREGAME) fire() if(GAME_STATE_PREGAME) //lobby stats for statpanels + SSgamemode.init_storyteller() // BANDASTATION EDIT - STORYTELLER if(isnull(timeLeft)) timeLeft = max(0,start_at - world.time) totalPlayers = LAZYLEN(GLOB.new_player_list) @@ -231,11 +233,14 @@ SUBSYSTEM_DEF(ticker) /datum/controller/subsystem/ticker/proc/setup() to_chat(world, span_boldannounce("Starting game...")) var/init_start = world.timeofday - + // BANDASTATION EDIT START - STORYTELLER CHECK_TICK //Configure mode and assign player to antagonists var/can_continue = FALSE - can_continue = SSdynamic.pre_setup() //Choose antagonists + + CHECK_TICK + can_continue = SSgamemode.pre_setup() + // BANDASTATION EDIT END - STORYTELLER CHECK_TICK SEND_GLOBAL_SIGNAL(COMSIG_GLOB_PRE_JOBS_ASSIGNED, src) can_continue = can_continue && SSjob.divide_occupations() //Distribute jobs @@ -301,7 +306,8 @@ SUBSYSTEM_DEF(ticker) /datum/controller/subsystem/ticker/proc/PostSetup() set waitfor = FALSE - SSdynamic.post_setup() + //SSdynamic.post_setup() // BANDASTATION EDIT - STORYTELLER + SSgamemode.post_setup() // BANDASTATION EDIT - STORYTELLER GLOB.start_state = new /datum/station_state() GLOB.start_state.count() diff --git a/code/controllers/subsystem/vote.dm b/code/controllers/subsystem/vote.dm index d9d796782c2b2..80eff00900a57 100644 --- a/code/controllers/subsystem/vote.dm +++ b/code/controllers/subsystem/vote.dm @@ -134,6 +134,10 @@ SUBSYSTEM_DEF(vote) return if(CONFIG_GET(flag/no_dead_vote) && voter.stat == DEAD && !voter.client?.holder) return + // BANDASTATION EDIT START - STORYTELLER + if(!current_vote.can_vote(voter)) + return + // BANDASTATION EDIT END - STORYTELLER // If user has already voted, remove their specific vote if(voter.ckey in current_vote.choices_by_ckey) @@ -156,6 +160,10 @@ SUBSYSTEM_DEF(vote) return if(!voter?.ckey) return + // BANDASTATION EDIT START - STORYTELLER + if(!current_vote.can_vote(voter)) + return + // BANDASTATION EDIT END - STORYTELLER if(CONFIG_GET(flag/no_dead_vote) && voter.stat == DEAD && !voter.client?.holder) return @@ -330,6 +338,11 @@ SUBSYSTEM_DEF(vote) "message" = can_vote == VOTE_AVAILABLE ? vote.default_message : can_vote, ) + // BANDASTATION EDIT START - STORYTELLER + if(vote.has_desc) + vote_data += list("desc" = vote.return_desc(vote_name)) + // BANDASTATION EDIT END - STORYTELLER + if(vote == current_vote) var/list/choices = list() for(var/key in current_vote.choices) @@ -346,6 +359,7 @@ SUBSYSTEM_DEF(vote) "displayStatistics" = current_vote.display_statistics, "choices" = choices, "vote" = vote_data, + "canVote" = current_vote.can_vote(user), // BANDASTATION EDIT - STORYTELLER ) all_vote_data += list(vote_data) diff --git a/code/datums/mind/_mind.dm b/code/datums/mind/_mind.dm index a9f0d7e501604..0e471c8d3c406 100644 --- a/code/datums/mind/_mind.dm +++ b/code/datums/mind/_mind.dm @@ -102,6 +102,10 @@ var/list/failed_special_equipment /// A list to keep track of which books a person has read (to prevent people from reading the same book again and again for positive mood events) var/list/book_titles_read + // BANDASTATION EDIT START - STORYTELLER + /// Variable that lets the event picker see if someones getting chosen or not + var/picking = FALSE + // BANDASTATION EDIT END - STORYTELLER /datum/mind/New(_key) key = _key diff --git a/code/datums/votes/_vote_datum.dm b/code/datums/votes/_vote_datum.dm index 957160dea7d3e..00da5652c4390 100644 --- a/code/datums/votes/_vote_datum.dm +++ b/code/datums/votes/_vote_datum.dm @@ -80,6 +80,10 @@ */ /datum/vote/proc/can_be_initiated(forced = FALSE) SHOULD_CALL_PARENT(TRUE) + // BANDASTATION EDIT START - STORYTELLER + if(!player_startable && !forced) + return FALSE + // BANDASTATION EDIT END - STORYTELLER if(!forced && !is_config_enabled()) return "This vote is currently disabled by the server configuration." @@ -97,6 +101,8 @@ for(var/key in default_choices) choices[key] = 0 + list_clear_nulls(choices) // monke edit: ensure no nulls end up in a vote + return TRUE /** diff --git a/code/game/machinery/computer/communications.dm b/code/game/machinery/computer/communications.dm index f0dce708276dc..e2c7d2c5878d6 100644 --- a/code/game/machinery/computer/communications.dm +++ b/code/game/machinery/computer/communications.dm @@ -887,7 +887,19 @@ SSdynamic.unfavorable_situation() if(HACK_SLEEPER) // Trigger one or multiple sleeper agents with the crew (or for latejoining crew) - var/datum/dynamic_ruleset/midround/sleeper_agent_type = /datum/dynamic_ruleset/midround/from_living/autotraitor + // BANDASTATION EDIT START - inject storyteller events instead of dynamic rulesets + var/event_to_spawn = pick_weight(list( + /datum/round_event_control/antagonist/solo/traitor/midround = 75, + // hmmm, let's rarely spawn some non-traitor antags just to spice things up a bit + /datum/round_event_control/antagonist/solo/heretic/midround = 15, + /datum/round_event_control/antagonist/solo/from_ghosts/wizard = 1 + )) + force_event_after(event_to_spawn, "[hacker] hacking a communications console", rand(20 SECONDS, 1 MINUTES)) + priority_announce( + "Attention crew, it appears that someone on your station has hijacked your telecommunications and broadcasted an unknown signal.", + "[command_name()] High-Priority Update", + ) + /* var/datum/dynamic_ruleset/midround/sleeper_agent_type = /datum/dynamic_ruleset/midround/from_living/autotraitor var/max_number_of_sleepers = clamp(round(length(GLOB.alive_player_list) / 20), 1, 3) var/num_agents_created = 0 for(var/num_agents in 1 to rand(1, max_number_of_sleepers)) @@ -905,6 +917,7 @@ "Внимание экипажу, похоже, зафиксирован взлом системы телекоммуникаций с последующей передачей неизвестного сигнала.", "[command_name()]: Высокоприоритетное оповещение", ) + */ // BANDASTATION EDIT END #undef HACK_PIRATE #undef HACK_FUGITIVES diff --git a/code/modules/admin/admin.dm b/code/modules/admin/admin.dm index b5a466cac2cda..358212c955ba3 100644 --- a/code/modules/admin/admin.dm +++ b/code/modules/admin/admin.dm @@ -19,7 +19,9 @@ if(!check_rights(0)) return - var/dat + var/dat = "
Game Panel

" + // BANDASTATION EDIT START- STORYTELLER + /* if(SSticker.current_state <= GAME_STATE_PREGAME) dat += "(Manage Dynamic Rulesets)
" dat += "(Force Roundstart Rulesets)
" @@ -30,9 +32,10 @@ dat += "(Dynamic mode options)
" dat += "
" if(SSticker.IsRoundInProgress()) - dat += "(Game Mode Panel)
" - dat += "(Manage Dynamic Rulesets)
" - dat += "
" + dat += "(Manage Dynamic Rulesets)
" + */ + dat += "(Game Mode Panel)
" + // BANDASTATION EDIT END - STORYTELLER dat += {" Create Object
Quick Create Object
diff --git a/code/modules/admin/force_event.dm b/code/modules/admin/force_event.dm index 519adbd8a9e5c..1f1517bc0fbad 100644 --- a/code/modules/admin/force_event.dm +++ b/code/modules/admin/force_event.dm @@ -56,12 +56,12 @@ ADMIN_VERB(force_event, R_FUN, "Trigger Event", "Forces an event to occur.", ADM )) //add event, with one value matching up the category UNTYPED_LIST_ADD(events, list( - "name" = event_control.name, - "description" = event_control.description, - "type" = event_control.type, - "category" = event_control.category, - "has_customization" = !!length(event_control.admin_setup), - )) + "name" = event_control.name, + "description" = event_control.description, + "type" = event_control.type, + "category" = event_control.category, + "has_customization" = !!length(event_control.admin_setup), + )) data["categories"] = categories data["events"] = events return data diff --git a/code/modules/admin/topic.dm b/code/modules/admin/topic.dm index ce640cf0d3f14..d3e2af87605da 100644 --- a/code/modules/admin/topic.dm +++ b/code/modules/admin/topic.dm @@ -69,7 +69,10 @@ else if(href_list["gamemode_panel"]) if(!check_rights(R_ADMIN)) return - SSdynamic.admin_panel() + // BANDASTATION EDIT START - STORYTELLER + //SSdynamic.admin_panel() + SSgamemode.admin_panel(usr) + // BANDASTATION EDIT END - STORYTELLER else if(href_list["call_shuttle"]) if(!check_rights(R_ADMIN)) diff --git a/code/modules/antagonists/_common/antag_team.dm b/code/modules/antagonists/_common/antag_team.dm index 527196c51c3ea..a53ef2ff19071 100644 --- a/code/modules/antagonists/_common/antag_team.dm +++ b/code/modules/antagonists/_common/antag_team.dm @@ -44,6 +44,13 @@ GLOBAL_LIST_EMPTY(antagonist_teams) new_objective.find_target(dupe_search_range = list(src)) new_objective.update_explanation_text() objectives += new_objective + // BANDASTATION EDIT START - STORYTELLER. Я удивлен, почему это в Сторителлере, но Окэээй. + for(var/datum/mind/member as anything in members) + for(var/datum/antagonist/antag in member.antag_datums) + if(antag.get_team() == src) + antag.objectives |= new_objective + antag.update_static_data_for_all_viewers() + // BANDASTATION EDIT END - STORYTELLER //Display members/victory/failure/objectives for the team /datum/team/proc/roundend_report() diff --git a/code/modules/antagonists/brother/brother.dm b/code/modules/antagonists/brother/brother.dm index 80d14724170fe..1b2028ef7977d 100644 --- a/code/modules/antagonists/brother/brother.dm +++ b/code/modules/antagonists/brother/brother.dm @@ -24,6 +24,12 @@ return team /datum/antagonist/brother/on_gain() + // BANDASTATION EDIT START - STORYTELLER - фикс ББ на отсутствие задач и команды + if(!team) + var/datum/team/brother_team/brother_team = new /datum/team/brother_team + brother_team.add_member(owner) + create_team(brother_team) + // BANDASTATION EDIT END - STORYTELLER - фикс ББ на отсутствие задач и команды objectives += team.objectives owner.special_role = special_role finalize_brother() diff --git a/code/modules/antagonists/nukeop/datums/operative.dm b/code/modules/antagonists/nukeop/datums/operative.dm index 60bd5edfaae54..44478b681c262 100644 --- a/code/modules/antagonists/nukeop/datums/operative.dm +++ b/code/modules/antagonists/nukeop/datums/operative.dm @@ -198,7 +198,7 @@ /// Actually moves our nukie to where they should be /datum/antagonist/nukeop/proc/move_to_spawnpoint() // Ensure that the nukiebase is loaded, and wait for it if required - SSmapping.lazy_load_template(LAZY_TEMPLATE_KEY_NUKIEBASE) + //SSmapping.lazy_load_template(LAZY_TEMPLATE_KEY_NUKIEBASE) // BANDASTATION EDIT - STORYTELLER var/turf/destination = get_spawnpoint() owner.current.forceMove(destination) if(!owner.current.onSyndieBase()) diff --git a/code/modules/antagonists/nukeop/equipment/nuclear_bomb/_nuclear_bomb.dm b/code/modules/antagonists/nukeop/equipment/nuclear_bomb/_nuclear_bomb.dm index 6440ba04658a7..17adf59797fc9 100644 --- a/code/modules/antagonists/nukeop/equipment/nuclear_bomb/_nuclear_bomb.dm +++ b/code/modules/antagonists/nukeop/equipment/nuclear_bomb/_nuclear_bomb.dm @@ -57,6 +57,7 @@ GLOBAL_VAR(station_nuke_source) /obj/machinery/nuclearbomb/Initialize(mapload) . = ..() countdown = new(src) + GLOB.nuke_list += src // BANDASTATION EDIT - STORYTELLER core = new /obj/item/nuke_core(src) STOP_PROCESSING(SSobj, core) update_appearance() @@ -68,6 +69,7 @@ GLOBAL_VAR(station_nuke_source) if(!exploding) // If we're not exploding, set the alert level back to normal toggle_nuke_safety() + GLOB.nuke_list -= src // BANDASTATION EDIT - STORYTELLER QDEL_NULL(countdown) QDEL_NULL(core) return ..() diff --git a/code/modules/antagonists/obsessed/obsessed.dm b/code/modules/antagonists/obsessed/obsessed.dm index 6a62f0875ec5f..d52b7acbcb13b 100644 --- a/code/modules/antagonists/obsessed/obsessed.dm +++ b/code/modules/antagonists/obsessed/obsessed.dm @@ -23,6 +23,8 @@ var/objectives_to_generate = 3 /// Brain trauma that causes the obsession var/datum/brain_trauma/special/obsessed/trauma + antag_flags = FLAG_FAKE_ANTAG // BANDASTATION EDIT - STORYTELLER + /// Dummy antag datum that will show the cured obsessed to admins /datum/antagonist/former_obsessed @@ -34,7 +36,7 @@ count_against_dynamic_roll_chance = FALSE silent = TRUE can_elimination_hijack = ELIMINATION_PREVENT - antag_flags = FLAG_FAKE_ANTAG + antag_flags = FLAG_FAKE_ANTAG | FLAG_ANTAG_CAP_IGNORE // BANDASTATION EDIT - STORYTELLER /datum/antagonist/obsessed/admin_add(datum/mind/new_owner,mob/admin) var/mob/living/carbon/C = new_owner.current diff --git a/code/modules/antagonists/pirate/pirate_event.dm b/code/modules/antagonists/pirate/pirate_event.dm index c1d18c77a7d96..326ccce3a46a1 100644 --- a/code/modules/antagonists/pirate/pirate_event.dm +++ b/code/modules/antagonists/pirate/pirate_event.dm @@ -14,7 +14,7 @@ admin_setup = list(/datum/event_admin_setup/listed_options/pirates) map_flags = EVENT_SPACE_ONLY -/datum/round_event_control/pirates/preRunEvent() +/datum/round_event_control/pirates/preRunEvent(scheduled = FALSE) // BANDASTATION EDIT - STORYTELLER if (SSmapping.is_planetary()) return EVENT_CANT_RUN return ..() @@ -27,6 +27,8 @@ send_pirate_threat(gang_list) /proc/send_pirate_threat(list/pirate_selection) + if(!pirate_selection) + pirate_selection = GLOB.light_pirate_gangs + GLOB.heavy_pirate_gangs var/datum/pirate_gang/chosen_gang = pick_n_take(pirate_selection) ///If there was nothing to pull from our requested list, stop here. if(!chosen_gang) @@ -41,7 +43,10 @@ //send message priority_announce("Входящий подпространственный вызов. Защищенный канал открыт на всех коммуникационных консолях.", "Входящее сообщение", SSstation.announcer.get_rand_report_sound()) threat.answer_callback = CALLBACK(GLOBAL_PROC, GLOBAL_PROC_REF(pirates_answered), threat, chosen_gang, payoff, world.time) - addtimer(CALLBACK(GLOBAL_PROC, GLOBAL_PROC_REF(spawn_pirates), threat, chosen_gang), RESPONSE_MAX_TIME) + // BANDASTATION EDIT START - STORYTELLER + //addtimer(CALLBACK(GLOBAL_PROC, GLOBAL_PROC_REF(spawn_pirates), threat, chosen_gang), RESPONSE_MAX_TIME) + spawn_pirates(threat, chosen_gang) + // BANDASTATION EDIT END - STORYTELLER GLOB.communications_controller.send_message(threat, unique = TRUE) /proc/pirates_answered(datum/comm_message/threat, datum/pirate_gang/chosen_gang, payoff, initial_send_time) diff --git a/code/modules/antagonists/space_dragon/space_carp.dm b/code/modules/antagonists/space_dragon/space_carp.dm index 43b5f874e918d..530397b85c7ad 100644 --- a/code/modules/antagonists/space_dragon/space_carp.dm +++ b/code/modules/antagonists/space_dragon/space_carp.dm @@ -5,6 +5,7 @@ show_in_antagpanel = FALSE show_name_in_check_antagonists = TRUE show_to_ghosts = TRUE + antag_flags = FLAG_ANTAG_CAP_IGNORE // BANDASTATION EDIT - STORYTELLER /// The rift to protect var/datum/weakref/rift diff --git a/code/modules/antagonists/wizard/wizard.dm b/code/modules/antagonists/wizard/wizard.dm index 8d10189fc0993..9948dec7067ec 100644 --- a/code/modules/antagonists/wizard/wizard.dm +++ b/code/modules/antagonists/wizard/wizard.dm @@ -122,7 +122,7 @@ GLOBAL_LIST_EMPTY(wizard_spellbook_purchases_by_key) /datum/antagonist/wizard/proc/send_to_lair() // And now we ensure that its loaded - SSmapping.lazy_load_template(LAZY_TEMPLATE_KEY_WIZARDDEN) + // SSmapping.lazy_load_template(LAZY_TEMPLATE_KEY_WIZARDDEN) // BANDASTATION EDIT - STORYTELLER if(!owner.current) return diff --git a/code/modules/bitrunning/antagonists/_parent.dm b/code/modules/bitrunning/antagonists/_parent.dm index 8bd061d72a1df..9e23f46ab14f6 100644 --- a/code/modules/bitrunning/antagonists/_parent.dm +++ b/code/modules/bitrunning/antagonists/_parent.dm @@ -12,6 +12,7 @@ show_to_ghosts = TRUE suicide_cry = "ALT F4!" ui_name = "AntagInfoGlitch" + antag_flags = FLAG_FAKE_ANTAG // BANDASTATION EDIT - STORYTELLER /// Minimum Qserver threat required to spawn this mob. This is subtracted (x/2) from the server thereafter. var/threat = 0 diff --git a/code/modules/bitrunning/antagonists/ghost_role.dm b/code/modules/bitrunning/antagonists/ghost_role.dm index 3bf88e16dfb21..e2cf2ad761b53 100644 --- a/code/modules/bitrunning/antagonists/ghost_role.dm +++ b/code/modules/bitrunning/antagonists/ghost_role.dm @@ -1,6 +1,7 @@ /datum/antagonist/domain_ghost_actor name = "Virtual Domain Actor" antagpanel_category = ANTAG_GROUP_GLITCH + antag_flags = FLAG_FAKE_ANTAG |FLAG_ANTAG_CAP_IGNORE // BANDASTATION EDIT - STORYTELLER job_rank = ROLE_GLITCH show_to_ghosts = TRUE suicide_cry = "FATAL ERROR" diff --git a/code/modules/bitrunning/event.dm b/code/modules/bitrunning/event.dm index 370957e2ebb0f..fa6c83c1bd1a9 100644 --- a/code/modules/bitrunning/event.dm +++ b/code/modules/bitrunning/event.dm @@ -14,7 +14,7 @@ /// List of servers on the station var/list/datum/weakref/active_servers = list() -/datum/round_event_control/bitrunning_glitch/can_spawn_event(players_amt, allow_magic = FALSE) +/datum/round_event_control/bitrunning_glitch/can_spawn_event(players_amt, allow_magic = FALSE, fake_check = FALSE) // BANDASTATION EDIT - STORYTELLER . = ..() if(!.) return . diff --git a/code/modules/events/_event.dm b/code/modules/events/_event.dm index a5f8675b3b9ad..5c94656dfc4e0 100644 --- a/code/modules/events/_event.dm +++ b/code/modules/events/_event.dm @@ -1,4 +1,4 @@ -#define RANDOM_EVENT_ADMIN_INTERVENTION_TIME (10 SECONDS) +#define RANDOM_EVENT_ADMIN_INTERVENTION_TIME (60 SECONDS) // BANDASTATION EDIT CHANGE - STORYTELLER. Original: 10 SECONDS //this singleton datum is used by the events controller to dictate how it selects events /datum/round_event_control @@ -70,11 +70,21 @@ // Checks if the event can be spawned. Used by event controller and "false alarm" event. // Admin-created events override this. -/datum/round_event_control/proc/can_spawn_event(players_amt, allow_magic = FALSE) +/datum/round_event_control/proc/can_spawn_event(players_amt, allow_magic = FALSE, fake_check = FALSE) // BANDASTATION EDIT - STORYTELLER SHOULD_CALL_PARENT(TRUE) + // BANDASTATION EDIT START - STORYTELLER + if(SSgamemode.current_storyteller?.disable_distribution || SSgamemode.halted_storyteller) + return FALSE + if(event_group && !GLOB.event_groups[event_group].can_run()) + return FALSE + if(roundstart && (!SSgamemode.can_run_roundstart || (SSgamemode.ran_roundstart && !fake_check && !SSgamemode.current_storyteller?.ignores_roundstart))) + return FALSE + if(!roundstart && (SSgamemode.can_run_roundstart)) + return FALSE + // BANDASTATION EDIT END - STORYTELLER if(occurrences >= max_occurrences) return FALSE - if(earliest_start >= world.time-SSticker.round_start_time) + if(earliest_start >= (world.time - SSticker.round_start_time)) return FALSE if(!allow_magic && wizardevent != SSevents.wizardmode) return FALSE @@ -86,13 +96,33 @@ return FALSE if(ispath(typepath, /datum/round_event/ghost_role) && !(GLOB.ghost_role_flags & GHOSTROLE_MIDROUND_EVENT)) return FALSE - + // BANDASTATION EDIT START - STORYTELLER + if(checks_antag_cap && !SSgamemode.can_inject_antags()) + return FALSE + if(!weight) + return FALSE + if(!check_enemies()) + return FALSE + if(allowed_storytellers && ((islist(allowed_storytellers) && !is_type_in_list(SSgamemode.current_storyteller, allowed_storytellers)) || SSgamemode.current_storyteller.type != allowed_storytellers)) + return FALSE + if(allowed_storytellers && SSgamemode.current_storyteller && ((islist(allowed_storytellers) && !is_type_in_list(SSgamemode.current_storyteller, allowed_storytellers)) || SSgamemode.current_storyteller.type != allowed_storytellers)) + return FALSE + if(eng_required_power > SSgamemode.current_eng_power) + return FALSE + if(med_required_power > SSgamemode.current_med_power) + return FALSE + if(rnd_required_power > SSgamemode.current_rnd_power) + return FALSE + if(head_required_power > SSgamemode.current_head_power) + return FALSE + if(type in SSgamemode.current_storyteller.exclude_events) + return FALSE + // BANDASTATION EDIT END - STORYTELLER if (dynamic_should_hijack && SSdynamic.random_event_hijacked != HIJACKED_NOTHING) return FALSE - return TRUE -/datum/round_event_control/proc/preRunEvent() +/datum/round_event_control/proc/preRunEvent(forced = FALSE, scheduled = FALSE) // BANDASTATION EDIT - STORYTELLER if(!ispath(typepath, /datum/round_event)) return EVENT_CANT_RUN @@ -101,17 +131,28 @@ triggering = TRUE - // We sleep HERE, in pre-event setup (because there's no sense doing it in run_event() since the event is already running!) for the given amount of time to make an admin has enough time to cancel an event un-fitting of the present round or at least reroll it. - message_admins("Random Event triggering in [DisplayTimeText(RANDOM_EVENT_ADMIN_INTERVENTION_TIME)]: [name]. (CANCEL) (SOMETHING ELSE)") - sleep(RANDOM_EVENT_ADMIN_INTERVENTION_TIME) + // BANDASTATION EDIT START - STORYTELLER + if(!roundstart && !scheduled) + // We sleep HERE, in pre-event setup (because there's no sense doing it in run_event() since the event is already running!) for the given amount of time to make an admin has enough time to cancel an event un-fitting of the present round or at least reroll it. + message_admins("Random Event triggering in [DisplayTimeText(RANDOM_EVENT_ADMIN_INTERVENTION_TIME)]: [name]. (CANCEL) (SOMETHING ELSE)") + sleep(RANDOM_EVENT_ADMIN_INTERVENTION_TIME) + // BANDASTATION EDIT END - STORYTELLER var/players_amt = get_active_player_count(alive_check = TRUE, afk_check = TRUE, human_check = TRUE) - if(!can_spawn_event(players_amt)) + if(!can_spawn_event(players_amt, fake_check = TRUE) && !forced && !roundstart) /// BANDSTATION EDIT - STORYTELLER message_admins("Second pre-condition check for [name] failed, rerolling...") + message_admins("REASON: [return_failure_string(players_amt)]")/// BANDSTATION EDIT - STORYTELLER SSevents.spawnEvent(excluded_event = src) return EVENT_INTERRUPTED + // BANDASTATION EDIT START - STORYTELLER + if(!can_spawn_event(players_amt, fake_check = TRUE) && forced) + message_admins("Second pre-condition check for [name] failed, but event forced, running event regardless this may have issues...") + message_admins("REASON: [return_failure_string(players_amt)]") + // BANDASTATION EDIT END - STORYTELLER + if(!triggering) return EVENT_CANCELLED //admin cancelled + triggering = FALSE return EVENT_READY @@ -129,11 +170,12 @@ if(!triggering) to_chat(usr, span_admin("Too late to change events now!")) return + SSevents.spawnEvent(excluded_event = src) triggering = FALSE message_admins("[key_name_admin(usr)] chose to have event [name] rolled into a different event.") log_admin_private("[key_name(usr)] rerolled event [name].") SSblackbox.record_feedback("tally", "event_admin_rerolled", 1, typepath) - SSevents.spawnEvent(excluded_event = src) + /* Runs the event @@ -155,6 +197,7 @@ Runs the event for(var/datum/event_admin_setup/admin_setup_datum in admin_setup) admin_setup_datum.apply_to_event(round_event) SEND_SIGNAL(src, COMSIG_CREATED_ROUND_EVENT, round_event) + round_event.forced = admin_forced round_event.setup() round_event.current_players = get_active_player_count(alive_check = 1, afk_check = 1, human_check = 1) occurrences++ @@ -173,6 +216,11 @@ Runs the event triggering = FALSE log_game("[random ? "Random" : "Forced"] Event triggering: [name] ([typepath]).") + // BANDASTATION EDIT START - STORYTELLER: event groups + if(event_group) + GLOB.event_groups[event_group].on_run(src) + // BANDASTATION EDIT END + if(alert_observers) round_event.announce_deadchat(random, event_cause) @@ -218,6 +266,7 @@ Runs the event //This is really only for setting defaults which can be overridden later when New() finishes. /datum/round_event/proc/setup() SHOULD_CALL_PARENT(FALSE) + setup = TRUE // BANDASTATION EDIT - STORYTELLER return ///Announces the event name to deadchat, override this if what an event should show to deadchat is different to its event name. @@ -271,6 +320,10 @@ Runs the event //This proc will handle the calls to the appropriate procs. /datum/round_event/process() SHOULD_NOT_OVERRIDE(TRUE) + // BANDASTATION EDIT START - STORYTELLER + if(!setup) + return + // BANDASTATION EDIT END - STORYTELLER if(!processing) return @@ -302,6 +355,7 @@ Runs the event // Everything is done, let's clean up. if(activeFor >= end_when && activeFor >= announce_when && activeFor >= start_when) processing = FALSE + SSgamemode.runned_events += control.name // BANDASTATION EDIT - STORYTELLER kill() activeFor++ diff --git a/code/modules/events/anomaly/_anomaly.dm b/code/modules/events/anomaly/_anomaly.dm index 6d72a90d7e9be..c5526c4cd3f92 100644 --- a/code/modules/events/anomaly/_anomaly.dm +++ b/code/modules/events/anomaly/_anomaly.dm @@ -24,6 +24,7 @@ impact_area = get_area(spawn_location) else impact_area = placer.findValidArea() + setup = TRUE // BANDASTATION EDIT - STORYTELLER /datum/round_event/anomaly/announce(fake) if(isnull(impact_area)) diff --git a/code/modules/events/aurora_caelus.dm b/code/modules/events/aurora_caelus.dm index 297e7e5afa8d9..0e945cbef4ce4 100644 --- a/code/modules/events/aurora_caelus.dm +++ b/code/modules/events/aurora_caelus.dm @@ -7,7 +7,7 @@ category = EVENT_CATEGORY_FRIENDLY description = "A colourful display can be seen through select windows. And the kitchen." -/datum/round_event_control/aurora_caelus/can_spawn_event(players, allow_magic = FALSE) +/datum/round_event_control/aurora_caelus/can_spawn_event(players, allow_magic = FALSE, fake_check = FALSE) // BANDASTATION EDIT - STORYTELLER if(!SSmapping.empty_space) return FALSE return ..() diff --git a/code/modules/events/brand_intelligence.dm b/code/modules/events/brand_intelligence.dm index 554d4bd1b89ce..b801820e65dd8 100644 --- a/code/modules/events/brand_intelligence.dm +++ b/code/modules/events/brand_intelligence.dm @@ -46,6 +46,7 @@ kill() return origin_machine = pick_n_take(vending_machines) + setup = TRUE // BANDASTATION EDIT - STORYTELLER /datum/round_event/brand_intelligence/announce(fake) var/machine_name = initial(origin_machine.name) diff --git a/code/modules/events/bureaucratic_error.dm b/code/modules/events/bureaucratic_error.dm index 11f50930245c8..54fe3a90921c3 100644 --- a/code/modules/events/bureaucratic_error.dm +++ b/code/modules/events/bureaucratic_error.dm @@ -20,8 +20,11 @@ overflow.total_positions = -1 // Ensures infinite slots as this role. Assistant will still be open for those that cant play it. for(var/job in jobs) var/datum/job/current = job - current.total_positions = 0 + // BANDASTATION EDIT START - STORYTELLER + //current.total_positions = 0 + current.total_positions = max(current.total_positions + rand(-2,4), 1) + // BANDASTATION EDIT END - STORYTELLER return // 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), 0) + current.total_positions = max(current.total_positions + rand(-2,4), 1) // BANDASTATION EDIT - STORYTELLER diff --git a/code/modules/events/carp_migration.dm b/code/modules/events/carp_migration.dm index 975077ab35789..4657c2ced2169 100644 --- a/code/modules/events/carp_migration.dm +++ b/code/modules/events/carp_migration.dm @@ -35,6 +35,7 @@ /datum/round_event/carp_migration/setup() start_when = rand(40, 60) + setup = TRUE // BANDASTATION EDIT - STORYTELLER /datum/round_event/carp_migration/announce(fake) priority_announce("[fluff_signal] были обнаружены вблизи [station_name()], будьте наготове.", "Неопознанные формы жизни") diff --git a/code/modules/events/creep_awakening.dm b/code/modules/events/creep_awakening.dm index 0dfa87ddfa333..e11bf773a82bb 100644 --- a/code/modules/events/creep_awakening.dm +++ b/code/modules/events/creep_awakening.dm @@ -1,7 +1,7 @@ /datum/round_event_control/obsessed name = "Obsession Awakening" typepath = /datum/round_event/obsessed - max_occurrences = 1 + max_occurrences = 2 //BANDASTATION EDIT - STORYTELLER min_players = 20 category = EVENT_CATEGORY_HEALTH description = "A random crewmember becomes obsessed with another." @@ -17,6 +17,8 @@ continue if(!(H.mind.assigned_role.job_flags & JOB_CREW_MEMBER)) //only station jobs sans nonhuman roles, prevents ashwalkers trying to stalk with crewmembers they never met continue + if((H.mind.assigned_role in protected_roles)) //BANDASTATION EDIT - STORYTELLER + continue if(H.mind.has_antag_datum(/datum/antagonist/obsessed)) continue if(!H.get_organ_by_type(/obj/item/organ/brain)) diff --git a/code/modules/events/disease_outbreak.dm b/code/modules/events/disease_outbreak.dm index 74e31cc72c8e8..419d237ef6b97 100644 --- a/code/modules/events/disease_outbreak.dm +++ b/code/modules/events/disease_outbreak.dm @@ -37,7 +37,7 @@ ///Disease recipient candidates var/list/disease_candidates = list() -/datum/round_event_control/disease_outbreak/can_spawn_event(players_amt, allow_magic = FALSE) +/datum/round_event_control/disease_outbreak/can_spawn_event(players_amt, allow_magic = FALSE, fake_check = FALSE) // BANDASTATION EDIT - STORYTELLER . = ..() if(!.) return . diff --git a/code/modules/events/earthquake.dm b/code/modules/events/earthquake.dm index d8361bc3c359c..d30cef7ea692a 100644 --- a/code/modules/events/earthquake.dm +++ b/code/modules/events/earthquake.dm @@ -15,7 +15,7 @@ max_wizard_trigger_potency = 7 map_flags = EVENT_PLANETARY_ONLY -/datum/round_event_control/earthquake/can_spawn_event(players_amt, allow_magic) +/datum/round_event_control/earthquake/can_spawn_event(players_amt, allow_magic, fake_check = FALSE) // BANDASTATION EDIT - STORYTELLER . = ..() if(!.) return . diff --git a/code/modules/events/false_alarm.dm b/code/modules/events/false_alarm.dm index 6e5cfdc61a1f3..59898bfaf5df4 100644 --- a/code/modules/events/false_alarm.dm +++ b/code/modules/events/false_alarm.dm @@ -7,7 +7,7 @@ description = "Fakes an event announcement." admin_setup = list(/datum/event_admin_setup/listed_options/false_alarm) -/datum/round_event_control/falsealarm/can_spawn_event(players_amt, allow_magic = FALSE) +/datum/round_event_control/falsealarm/can_spawn_event(players_amt, allow_magic = FALSE, fake_check = FALSE) // BANDASTATION EDIT - STORYTELLER . = ..() if(!.) return . diff --git a/code/modules/events/ghost_role/_ghost_role.dm b/code/modules/events/ghost_role/_ghost_role.dm index 336fb4c03bbda..e9651c1f74ff6 100644 --- a/code/modules/events/ghost_role/_ghost_role.dm +++ b/code/modules/events/ghost_role/_ghost_role.dm @@ -59,6 +59,7 @@ if(NOT_ENOUGH_PLAYERS) message_admins("[role_name] cannot be spawned due to lack of players signing up.") deadchat_broadcast(" did not get enough candidates ([minimum_required]) to spawn.", "[role_name]", message_type=DEADCHAT_ANNOUNCEMENT) + SSgamemode.refill_roleset() // BANDASTATION ADDITION - STORYTELLER kill() return if(SUCCESSFUL_SPAWN) diff --git a/code/modules/events/ghost_role/alien_infestation.dm b/code/modules/events/ghost_role/alien_infestation.dm index fee4a3704d2e4..5022403026157 100644 --- a/code/modules/events/ghost_role/alien_infestation.dm +++ b/code/modules/events/ghost_role/alien_infestation.dm @@ -9,7 +9,7 @@ category = EVENT_CATEGORY_ENTITIES description = "A xenomorph larva spawns on a random vent." -/datum/round_event_control/alien_infestation/can_spawn_event(players_amt, allow_magic = FALSE) +/datum/round_event_control/alien_infestation/can_spawn_event(players_amt, allow_magic = FALSE, fake_check = FALSE) // BANDASTATION EDIT - STORYTELLER . = ..() if(!.) return . @@ -33,6 +33,7 @@ announce_when = rand(announce_when, announce_when + 50) if(prob(50)) spawncount++ + setup = TRUE /datum/round_event/ghost_role/alien_infestation/announce(fake) var/living_aliens = FALSE diff --git a/code/modules/events/ghost_role/blob.dm b/code/modules/events/ghost_role/blob.dm index ed9d0377f994a..a657bdf538049 100644 --- a/code/modules/events/ghost_role/blob.dm +++ b/code/modules/events/ghost_role/blob.dm @@ -10,7 +10,7 @@ category = EVENT_CATEGORY_ENTITIES description = "Spawns a new blob overmind." -/datum/round_event_control/blob/can_spawn_event(players, allow_magic = FALSE) +/datum/round_event_control/blob/can_spawn_event(players, allow_magic = FALSE, fake_check = FALSE) // BANDASTATION EDIT - STORYTELLER if(EMERGENCY_PAST_POINT_OF_NO_RETURN) // no blobs if the shuttle is past the point of no return return FALSE diff --git a/code/modules/events/gravity_generator_blackout.dm b/code/modules/events/gravity_generator_blackout.dm index 9d48f2428d81c..bb8cce642c4d7 100644 --- a/code/modules/events/gravity_generator_blackout.dm +++ b/code/modules/events/gravity_generator_blackout.dm @@ -7,7 +7,7 @@ min_wizard_trigger_potency = 0 max_wizard_trigger_potency = 4 -/datum/round_event_control/gravity_generator_blackout/can_spawn_event(players_amt, allow_magic = FALSE) +/datum/round_event_control/gravity_generator_blackout/can_spawn_event(players_amt, allow_magic = FALSE, fake_check = FALSE) // BANDASTATION EDIT - STORYTELLER . = ..() if(!.) return . diff --git a/code/modules/events/grey_tide.dm b/code/modules/events/grey_tide.dm index c5d5808c28691..2356916e6cfff 100644 --- a/code/modules/events/grey_tide.dm +++ b/code/modules/events/grey_tide.dm @@ -20,6 +20,7 @@ announce_when = rand(50, 60) end_when = rand(20, 30) severity = rand(1,3) + setup = TRUE // BANDASTATION EDIT - STORYTELLER var/list/potential_areas = list(/area/station/command, /area/station/engineering, diff --git a/code/modules/events/heart_attack.dm b/code/modules/events/heart_attack.dm index a0bc06718c08d..54144a3672763 100644 --- a/code/modules/events/heart_attack.dm +++ b/code/modules/events/heart_attack.dm @@ -12,7 +12,7 @@ ///Candidates for receiving a healthy dose of heart disease var/list/heart_attack_candidates = list() -/datum/round_event_control/heart_attack/can_spawn_event(players_amt, allow_magic = FALSE) +/datum/round_event_control/heart_attack/can_spawn_event(players_amt, allow_magic = FALSE, fake_check = FALSE) // BANDASTATION EDIT - STORYTELLER . = ..() if(!.) return . diff --git a/code/modules/events/portal_storm.dm b/code/modules/events/portal_storm.dm index 426972606d340..af7bf5f243961 100644 --- a/code/modules/events/portal_storm.dm +++ b/code/modules/events/portal_storm.dm @@ -3,6 +3,7 @@ typepath = /datum/round_event/portal_storm/syndicate_shocktroop weight = 2 min_players = 15 + max_occurrences = 1 // BANDASTATION EDIT - STORYTELLER earliest_start = 30 MINUTES category = EVENT_CATEGORY_ENTITIES description = "Syndicate troops pour out of portals." @@ -69,6 +70,7 @@ hostiles_spawn += get_random_station_turf() next_boss_spawn = start_when + CEILING(2 * number_of_hostiles / number_of_bosses, 1) + setup = TRUE // BANDASTATION EDIT - STORYTELLER /datum/round_event/portal_storm/announce(fake) set waitfor = 0 diff --git a/code/modules/events/radiation_leak.dm b/code/modules/events/radiation_leak.dm index 2224d8e4161f1..983b5e4f050bd 100644 --- a/code/modules/events/radiation_leak.dm +++ b/code/modules/events/radiation_leak.dm @@ -47,6 +47,7 @@ // We found something, we can just return now picked_machine_ref = WEAKREF(sick_device) return + setup = TRUE // BANDASTATION EDIT - STORYTELLER /datum/round_event/radiation_leak/announce(fake) var/obj/machinery/the_source_of_our_problems = picked_machine_ref?.resolve() diff --git a/code/modules/events/radiation_storm.dm b/code/modules/events/radiation_storm.dm index a6df97858ea90..8f6263eb14dfa 100644 --- a/code/modules/events/radiation_storm.dm +++ b/code/modules/events/radiation_storm.dm @@ -14,6 +14,7 @@ start_when = 3 end_when = start_when + 1 announce_when = 1 + setup = TRUE // BANDASTATION EDIT - STORYTELLER /datum/round_event/radiation_storm/announce(fake) priority_announce("Вблизи станции обнаружено радиационное поле высокой интенсивности. Всему персоналу надлежит проследовать в технические тоннели.", "Обнаружена аномалия", ANNOUNCER_RADIATION) diff --git a/code/modules/events/sandstorm.dm b/code/modules/events/sandstorm.dm index db38265763334..19abad3e8f836 100644 --- a/code/modules/events/sandstorm.dm +++ b/code/modules/events/sandstorm.dm @@ -30,6 +30,7 @@ /datum/round_event/sandstorm/setup() start_when = rand(70, 90) end_when = rand(110, 140) + setup = TRUE // BANDASTATION EDIT - STORYTELLER /datum/round_event/sandstorm/announce(fake) if(!start_side) diff --git a/code/modules/events/scrubber_overflow.dm b/code/modules/events/scrubber_overflow.dm index bcad6d0424f06..d5a24b97bd514 100644 --- a/code/modules/events/scrubber_overflow.dm +++ b/code/modules/events/scrubber_overflow.dm @@ -84,8 +84,9 @@ if(!scrubbers.len) return kill() + setup = TRUE // BANDASTATION EDIT - STORYTELLER -/datum/round_event_control/scrubber_overflow/can_spawn_event(players_amt, allow_magic = FALSE) +/datum/round_event_control/scrubber_overflow/can_spawn_event(players_amt, allow_magic = FALSE, fake_check = FALSE) // BANDASTATION EDIT - STORYTELLER . = ..() if(!.) return diff --git a/code/modules/events/shuttle_catastrophe.dm b/code/modules/events/shuttle_catastrophe.dm index 7c9b1954fd8a0..6589a46e6fe53 100644 --- a/code/modules/events/shuttle_catastrophe.dm +++ b/code/modules/events/shuttle_catastrophe.dm @@ -7,7 +7,7 @@ description = "Replaces the emergency shuttle with a random one." admin_setup = list(/datum/event_admin_setup/warn_admin/shuttle_catastrophe, /datum/event_admin_setup/listed_options/shuttle_catastrophe) -/datum/round_event_control/shuttle_catastrophe/can_spawn_event(players, allow_magic = FALSE) +/datum/round_event_control/shuttle_catastrophe/can_spawn_event(players, allow_magic = FALSE, fake_check = FALSE) // BANDASTATION EDIT - STORYTELLER . = ..() if(!.) return . @@ -46,6 +46,7 @@ if(!isnull(template.who_can_purchase) && template.credit_cost < INFINITY) //if we could get it from the communications console, it's cool for us to get it here valid_shuttle_templates += template new_shuttle = pick(valid_shuttle_templates) + setup = TRUE // BANDASTATION EDIT - STORYTELLER /datum/round_event/shuttle_catastrophe/start() if(SSshuttle.shuttle_insurance) diff --git a/code/modules/events/shuttle_insurance.dm b/code/modules/events/shuttle_insurance.dm index 0e42b9ecfbd9b..e3b8a5f9b81c7 100644 --- a/code/modules/events/shuttle_insurance.dm +++ b/code/modules/events/shuttle_insurance.dm @@ -7,7 +7,7 @@ category = EVENT_CATEGORY_BUREAUCRATIC description = "A sketchy but legit insurance offer." -/datum/round_event_control/shuttle_insurance/can_spawn_event(players, allow_magic = FALSE) +/datum/round_event_control/shuttle_insurance/can_spawn_event(players, allow_magic = FALSE, fake_check = FALSE) // BANDASTATION EDIT - STORYTELLER . = ..() if(!.) return . @@ -39,6 +39,7 @@ break if(!insurance_evaluation) insurance_evaluation = 5000 //gee i dunno + setup = TRUE // BANDASTATION EDIT - STORYTELLER /datum/round_event/shuttle_insurance/start() insurance_message = new("Страховка шаттла", "Привет, приятель, говорит капитан судна [ship_name]. Не могу не заметить, что у тебя там дикий и сумасшедший шаттл БЕЗ СТРАХОВКИ! Безумие! А что, если с ним что-нибудь случится?! Мы провели быструю оценку тарифов в этом секторе и возьмем всего [insurance_evaluation] за страховку вашего шаттла в случае любой катастрофы.", list("Приобрести страховку.","Отклонить предложение.")) diff --git a/code/modules/events/shuttle_loan/shuttle_loan_event.dm b/code/modules/events/shuttle_loan/shuttle_loan_event.dm index 97244230ac126..a9dc733ed5bc7 100644 --- a/code/modules/events/shuttle_loan/shuttle_loan_event.dm +++ b/code/modules/events/shuttle_loan/shuttle_loan_event.dm @@ -11,7 +11,7 @@ ///A list of normally unavailable (or already run) situations datums var/list/unavailable_situations = list(/datum/shuttle_loan_situation/mail_strike) -/datum/round_event_control/shuttle_loan/can_spawn_event(players_amt, allow_magic = FALSE) +/datum/round_event_control/shuttle_loan/can_spawn_event(players_amt, allow_magic = FALSE, fake_check = FALSE) // BANDASTATION EDIT - STORYTELLER . = ..() for(var/datum/round_event/running_event in SSevents.running) if(istype(running_event, /datum/round_event/shuttle_loan)) //Make sure two of these don't happen at once. diff --git a/code/modules/events/space_vines/vine_event.dm b/code/modules/events/space_vines/vine_event.dm index ce9881c990797..04e73d17c663e 100644 --- a/code/modules/events/space_vines/vine_event.dm +++ b/code/modules/events/space_vines/vine_event.dm @@ -3,7 +3,7 @@ typepath = /datum/round_event/spacevine weight = 15 max_occurrences = 3 - min_players = 10 + min_players = 25 // BANDASTATION EDIT - STORYTELLER: 10 ==> 25 category = EVENT_CATEGORY_ENTITIES description = "Kudzu begins to overtake the station. Might spawn man-traps." min_wizard_trigger_potency = 4 diff --git a/code/modules/events/spider_infestation.dm b/code/modules/events/spider_infestation.dm index 6dceccfefa384..219fc925b4ff7 100644 --- a/code/modules/events/spider_infestation.dm +++ b/code/modules/events/spider_infestation.dm @@ -16,6 +16,7 @@ /datum/round_event/spider_infestation/setup() announce_when = rand(announce_when, announce_when + 50) + setup = TRUE // BANDASTATION EDIT - STORYTELLER /datum/round_event/spider_infestation/announce(fake) priority_announce("Обнаружены неопознанные формы жизни на борту [station_name()]. Обезопасьте все наружные входы и выходы, включая вентиляцию и вытяжки.", "Неопознанные формы жизни", ANNOUNCER_ALIENS) diff --git a/code/modules/events/stray_cargo.dm b/code/modules/events/stray_cargo.dm index 7a66828d5e6ce..b636037a07e87 100644 --- a/code/modules/events/stray_cargo.dm +++ b/code/modules/events/stray_cargo.dm @@ -66,6 +66,7 @@ var/datum/supply_pack/pack_type = pack if(initial(pack_type.special)) stray_spawnable_supply_packs -= pack + setup = TRUE // BANDASTATION EDIT - STORYTELLER ///Spawns a random supply pack, puts it in a pod, and spawns it on a random tile of the selected area /datum/round_event/stray_cargo/start() diff --git a/code/modules/events/supermatter_surge.dm b/code/modules/events/supermatter_surge.dm index 018be6fecd244..a7ec100effea6 100644 --- a/code/modules/events/supermatter_surge.dm +++ b/code/modules/events/supermatter_surge.dm @@ -37,7 +37,7 @@ /datum/event_admin_setup/input_number/surge_spiciness, ) -/datum/round_event_control/supermatter_surge/can_spawn_event(players_amt, allow_magic = FALSE) +/datum/round_event_control/supermatter_surge/can_spawn_event(players_amt, allow_magic = FALSE, fake_check = FALSE) // BANDASTATION EDIT - STORYTELLER . = ..() if(!SSjob.has_minimum_jobs(crew_threshold = 3, jobs = JOB_GROUP_ENGINEERS, head_jobs = list(JOB_CHIEF_ENGINEER))) diff --git a/code/modules/events/tram_malfunction.dm b/code/modules/events/tram_malfunction.dm index 088285e4f17e2..cdbe4a14d7d3d 100644 --- a/code/modules/events/tram_malfunction.dm +++ b/code/modules/events/tram_malfunction.dm @@ -13,7 +13,7 @@ max_wizard_trigger_potency = 3 //Check if there's a tram we can cause to malfunction. -/datum/round_event_control/tram_malfunction/can_spawn_event(players_amt, allow_magic = FALSE) +/datum/round_event_control/tram_malfunction/can_spawn_event(players_amt, allow_magic = FALSE, fake_check = FALSE) // BANDASTATION EDIT - STORYTELLER . = ..() if (!.) return FALSE @@ -33,6 +33,7 @@ /datum/round_event/tram_malfunction/setup() end_when = rand(TRAM_MALFUNCTION_TIME_LOWER, TRAM_MALFUNCTION_TIME_UPPER) + setup = TRUE // BANDASTATION EDIT - STORYTELLER /datum/round_event/tram_malfunction/start() for(var/datum/transport_controller/linear/tram/malfunctioning_controller as anything in SStransport.transports_by_type[TRANSPORT_TYPE_TRAM]) diff --git a/code/modules/events/vent_clog.dm b/code/modules/events/vent_clog.dm index 1fa63be169021..548fecfd84c3d 100644 --- a/code/modules/events/vent_clog.dm +++ b/code/modules/events/vent_clog.dm @@ -8,7 +8,7 @@ category = EVENT_CATEGORY_JANITORIAL description = "Harmless mobs climb out of a vent." -/datum/round_event_control/vent_clog/can_spawn_event(players_amt, allow_magic = FALSE) +/datum/round_event_control/vent_clog/can_spawn_event(players_amt, allow_magic = FALSE, fake_check = FALSE) // BANDASTATION EDIT - STORYTELLER . = ..() if(!.) return diff --git a/code/modules/events/wizard/embeddies.dm b/code/modules/events/wizard/embeddies.dm index 49f4fbc5afe38..61577248582ad 100644 --- a/code/modules/events/wizard/embeddies.dm +++ b/code/modules/events/wizard/embeddies.dm @@ -9,7 +9,7 @@ max_wizard_trigger_potency = 7 ///behold... the only reason sticky is a subtype... -/datum/round_event_control/wizard/embedpocalypse/can_spawn_event(players_amt, allow_magic = FALSE) +/datum/round_event_control/wizard/embedpocalypse/can_spawn_event(players_amt, allow_magic = FALSE, fake_check = FALSE) // BANDASTATION EDIT - STORYTELLER . = ..() if(!.) return . diff --git a/code/modules/events/wizard/identity_spoof.dm b/code/modules/events/wizard/identity_spoof.dm index dcd923c2776a4..5aab6e7866e7f 100644 --- a/code/modules/events/wizard/identity_spoof.dm +++ b/code/modules/events/wizard/identity_spoof.dm @@ -5,7 +5,7 @@ max_occurrences = 1 description = "Makes everyone dressed up like a wizard." -/datum/round_event_control/wizard/identity_spoof/can_spawn_event(players_amt, allow_magic = FALSE) +/datum/round_event_control/wizard/identity_spoof/can_spawn_event(players_amt, allow_magic = FALSE, fake_check = FALSE) // BANDASTATION EDIT - STORYTELLER . = ..() if(!.) return . diff --git a/code/modules/events/wizard/petsplosion.dm b/code/modules/events/wizard/petsplosion.dm index 09ee4348f7052..e34769d6f3435 100644 --- a/code/modules/events/wizard/petsplosion.dm +++ b/code/modules/events/wizard/petsplosion.dm @@ -32,7 +32,7 @@ GLOBAL_LIST_INIT(petsplosion_candidates, typecacheof(list( /// Number of mobs we're going to duplicate var/mobs_to_dupe = 0 -/datum/round_event_control/wizard/petsplosion/preRunEvent() +/datum/round_event_control/wizard/petsplosion/preRunEvent(scheduled = FALSE) // BANDASTATION EDIT - STORYTELLER for(var/mob/living/basic/dupe_animal in GLOB.alive_mob_list) count_mob(dupe_animal) for(var/mob/living/simple_animal/dupe_animal in GLOB.alive_mob_list) diff --git a/code/modules/events/wormholes.dm b/code/modules/events/wormholes.dm index 6dba38670f667..deae851c45858 100644 --- a/code/modules/events/wormholes.dm +++ b/code/modules/events/wormholes.dm @@ -23,6 +23,7 @@ GLOBAL_LIST_EMPTY(all_wormholes) // So we can pick wormholes to teleport to /datum/round_event/wormholes/setup() announce_when = rand(0, 20) end_when = rand(40, 80) + setup = TRUE // BANDASTATION ADDITION - STORYTELLER /datum/round_event/wormholes/start() for(var/turf/open/floor/valid in GLOB.station_turfs) diff --git a/code/modules/mob/dead/new_player/login.dm b/code/modules/mob/dead/new_player/login.dm index 3b374971ee5b0..e71d16ed299d3 100644 --- a/code/modules/mob/dead/new_player/login.dm +++ b/code/modules/mob/dead/new_player/login.dm @@ -22,7 +22,7 @@ if (required_living_minutes >= living_minutes) client.interviewee = TRUE - check_whitelist_or_make_interviewee() /// Bandastation Add + check_whitelist_or_make_interviewee() // BANDASTATION Add . = ..() if(!. || !client) return FALSE diff --git a/code/modules/reagents/chemistry/reagents/medicine_reagents.dm b/code/modules/reagents/chemistry/reagents/medicine_reagents.dm index 44677e6682cb6..dafeecb5cebba 100644 --- a/code/modules/reagents/chemistry/reagents/medicine_reagents.dm +++ b/code/modules/reagents/chemistry/reagents/medicine_reagents.dm @@ -1006,6 +1006,7 @@ exposed_mob.visible_message(span_warning("[exposed_mob]'s body does not react...")) return + if(iscarbon(exposed_mob) && !(methods & (INGEST|INHALE))) //simplemobs can still be splashed return ..() diff --git a/config/bandastation/bandastation_config.txt b/config/bandastation/bandastation_config.txt index 51d0906d8418a..0087ff1d08530 100644 --- a/config/bandastation/bandastation_config.txt +++ b/config/bandastation/bandastation_config.txt @@ -22,6 +22,45 @@ ROUNDSTART_RACES vulpkanin #MAX_THREAT_TO_ROUNDSTART_PERCENT 60 #MIN_THREAT_LEVEL 20 +## Gamemode configurations + +## Multipliers for points gained over time for event tracks. +MUNDANE_POINT_GAIN_MULTIPLIER 0.5 +MODERATE_POINT_GAIN_MULTIPLIER 0.5 +MAJOR_POINT_GAIN_MULTIPLIER 0.5 +ROLESET_POINT_GAIN_MULTIPLIER 0.5 +OBJECTIVES_POINT_GAIN_MULTIPLIER 0.5 + +## Minimum population caps for event tracks to run their events. +MUNDANE_MIN_POP 0 +MODERATE_MIN_POP 0 +MAJOR_MIN_POP 20 +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 500 +MODERATE_POINT_THRESHOLD 750 +MAJOR_POINT_THRESHOLD 1950 +ROLESET_POINT_THRESHOLD 1650 +OBJECTIVES_POINT_THRESHOLD 8000 + +## 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 ## Automatic crew transfer @@ -35,7 +74,6 @@ ROUNDSTART_RACES vulpkanin #ENABLE_AUTOMATIC_CREW_TRANSFER # Cryo - ## Time in deciseconds the mob must be clientless for to be despawned by cryopod. 30 minutes by default #CRYO_MIN_SSD_TIME 18000 diff --git a/config/bandastation/events.toml b/config/bandastation/events.toml new file mode 100644 index 0000000000000..fc0490b21ddc5 --- /dev/null +++ b/config/bandastation/events.toml @@ -0,0 +1,9 @@ +["/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 diff --git a/config/game_options.txt b/config/game_options.txt index fba22109dfb0d..e1c3ac843fa91 100644 --- a/config/game_options.txt +++ b/config/game_options.txt @@ -85,7 +85,7 @@ STATION_GOAL_BUDGET 1 ## GAME MODES ### ## Uncomment to not send a roundstart intercept report. Gamemodes may override this. -NO_INTERCEPT_REPORT +## NO_INTERCEPT_REPORT ## Percent weight reductions for three of the most recent modes diff --git a/config/logging.txt b/config/logging.txt index 2bd9e90fcd95c..2938904e55ced 100644 --- a/config/logging.txt +++ b/config/logging.txt @@ -101,3 +101,6 @@ LOG_WHISPER ## log manual target zone switching LOG_ZONE_SWITCH + +## log storyteller +# LOG_STORYTELLER diff --git a/modular_bandastation/game/_game.dm b/modular_bandastation/game/_game.dm new file mode 100644 index 0000000000000..45f5c7951a256 --- /dev/null +++ b/modular_bandastation/game/_game.dm @@ -0,0 +1,4 @@ +/datum/modpack/logging + name = "Logging mode" + desc = "Адаптация кастомного логирования." + author = "KageIIte" diff --git a/modular_bandastation/game/_game.dme b/modular_bandastation/game/_game.dme new file mode 100644 index 0000000000000..e6cc07ae08c90 --- /dev/null +++ b/modular_bandastation/game/_game.dme @@ -0,0 +1,8 @@ +#include "_game.dm" + +#include "code\_defines.dm" +#include "code\_game_options.dm" +#include "code\_procs.dm" +#include "code\log_categories.dm" + +#include "code\votes\_vote_datum.dm" diff --git a/modular_bandastation/game/code/_defines.dm b/modular_bandastation/game/code/_defines.dm new file mode 100644 index 0000000000000..d8bffe73c04c7 --- /dev/null +++ b/modular_bandastation/game/code/_defines.dm @@ -0,0 +1 @@ +#define LOG_CATEGORY_STORYTELLER "storyteller" diff --git a/modular_bandastation/game/code/_game_options.dm b/modular_bandastation/game/code/_game_options.dm new file mode 100644 index 0000000000000..2875224b6bf4b --- /dev/null +++ b/modular_bandastation/game/code/_game_options.dm @@ -0,0 +1,3 @@ +/datum/config_entry/flag/log_storyteller + +/datum/config_entry/flag/disable_storyteller diff --git a/modular_bandastation/game/code/_procs.dm b/modular_bandastation/game/code/_procs.dm new file mode 100644 index 0000000000000..58e0a4d6e69ed --- /dev/null +++ b/modular_bandastation/game/code/_procs.dm @@ -0,0 +1,2 @@ +/proc/log_storyteller(text, list/data) + logger.Log(LOG_CATEGORY_STORYTELLER, text, data) diff --git a/modular_bandastation/game/code/log_categories.dm b/modular_bandastation/game/code/log_categories.dm new file mode 100644 index 0000000000000..4ced1de2423d9 --- /dev/null +++ b/modular_bandastation/game/code/log_categories.dm @@ -0,0 +1,3 @@ +/datum/log_category/storyteller + category = LOG_CATEGORY_STORYTELLER + config_flag = /datum/config_entry/flag/log_storyteller diff --git a/modular_bandastation/game/code/votes/_vote_datum.dm b/modular_bandastation/game/code/votes/_vote_datum.dm new file mode 100644 index 0000000000000..c69fc89bb40ca --- /dev/null +++ b/modular_bandastation/game/code/votes/_vote_datum.dm @@ -0,0 +1,5 @@ +/datum/vote + var/player_startable = TRUE + +/datum/vote/proc/can_vote(mob/voter) + return TRUE diff --git a/modular_bandastation/modular_bandastation.dme b/modular_bandastation/modular_bandastation.dme index 82f77e6eceafa..d7327b23ec017 100644 --- a/modular_bandastation/modular_bandastation.dme +++ b/modular_bandastation/modular_bandastation.dme @@ -54,13 +54,15 @@ #include "turfs/_turfs.dme" #include "whitelist220/_whitelist220.dme" #include "world_topics/_world_topics.dme" +#include "storyteller/_storyteller.dme" +#include "game/_game.dme" // --- MODULES END --- // - // --- PRIME --- // // #define DISABLE_PRIME_MODPACKS // Чтобы отлючить модпаки прайма, нужно раскоментить строку "#define DISABLE_PRIME_MODPACKS" выше. // Модпаки включены по умолчанию для запуска всех тестов и для этого кода тоже. #ifndef DISABLE_PRIME_MODPACKS #include "prime_only/_prime_only.dme" +#include "storyteller/code/storytellers/storyteller_prime.dm" #endif diff --git a/modular_bandastation/storyteller/_storyteller.dm b/modular_bandastation/storyteller/_storyteller.dm new file mode 100644 index 0000000000000..f00ea34cb8163 --- /dev/null +++ b/modular_bandastation/storyteller/_storyteller.dm @@ -0,0 +1,4 @@ +/datum/modpack/storyteller + name = "Storyteller mode" + desc = "Адаптация режима сторителлера для Bandastation." + author = "Bubberstation, KageIIte" diff --git a/modular_bandastation/storyteller/_storyteller.dme b/modular_bandastation/storyteller/_storyteller.dme new file mode 100644 index 0000000000000..df1ab552e9341 --- /dev/null +++ b/modular_bandastation/storyteller/_storyteller.dme @@ -0,0 +1,60 @@ +#include "_storyteller.dm" + +#include "code\_defines\storyteller_defines.dm" +#include "code\_defines\storyteller_config.dm" +#include "code\_defines\_ticker.dm" +#include "code\_helpers.dm" +#include "code\event_defines\_round_event.dm" +#include "code\event_defines\_round_event_control.dm" + +#include "code\events\group\_event_group.dm" +#include "code\events\group\groups.dm" +#include "code\events\object\anomalies_dimensional.dm" +#include "code\events\summon_wizard.dm" + +#include "code\event_defines\crewset\_override.dm" +#include "code\event_defines\crewset\blob_infection.dm" +#include "code\event_defines\crewset\changeling.dm" +#include "code\event_defines\crewset\heretic.dm" +#include "code\event_defines\crewset\malf.dm" +#include "code\event_defines\crewset\nuke_ops.dm" +#include "code\event_defines\crewset\spies.dm" +#include "code\event_defines\crewset\traitors.dm" +#include "code\event_defines\crewset\blood_brothers.dm" +#include "code\event_defines\crewset\cult.dm" +#include "code\event_defines\crewset\clown_ops.dm" +#include "code\event_defines\crewset\revolution.dm" +#include "code\event_defines\crewset\obsessed.dm" +#include "code\event_defines\crewset\wizard.dm" + +#include "code\event_defines\ghostset\override.dm" +#include "code\event_defines\ghostset\voidwalker.dm" +#include "code\event_defines\ghostset\nuke_ops.dm" +#include "code\event_defines\ghostset\wizard.dm" +#include "code\event_defines\ghostset\paradox_clone.dm" +#include "code\event_defines\ghostset\alien_infestation.dm" + +#include "code\event_defines\major\override.dm" +#include "code\event_defines\moderate\override.dm" +#include "code\event_defines\mundane\override.dm" + +#include "code\storytellers\_storyteller.dm" +#include "code\storytellers\storyteller_bomb.dm" +#include "code\storytellers\storyteller_chill.dm" +#include "code\storytellers\storyteller_clown.dm" +#include "code\storytellers\storyteller_default.dm" +#include "code\storytellers\storyteller_extended.dm" +#include "code\storytellers\storyteller_fragile.dm" +#include "code\storytellers\storyteller_gamer.dm" +#include "code\storytellers\storyteller_jester.dm" +#include "code\storytellers\storyteller_mystic.dm" +#include "code\storytellers\storyteller_operative.dm" +#include "code\storytellers\storyteller_black_orbit.dm" +#include "code\storytellers\storyteller_phantom.dm" + +#include "code\dynamic_report.dm" +#include "code\scheduled_event.dm" +#include "code\vote.dm" +#include "code\gamemode.dm" + +#include "code\event_defines\override_events.dm" diff --git a/modular_bandastation/storyteller/code/_defines/_ticker.dm b/modular_bandastation/storyteller/code/_defines/_ticker.dm new file mode 100644 index 0000000000000..161e16844e584 --- /dev/null +++ b/modular_bandastation/storyteller/code/_defines/_ticker.dm @@ -0,0 +1,4 @@ +/datum/controller/subsystem/ticker/PostSetup() + . = .. () + SSgamemode.current_storyteller.process(STORYTELLER_WAIT_TIME * 0.1) // we want this asap + SSgamemode.current_storyteller.round_started = TRUE diff --git a/modular_bandastation/storyteller/code/_defines/storyteller_config.dm b/modular_bandastation/storyteller/code/_defines/storyteller_config.dm new file mode 100644 index 0000000000000..10629b32db087 --- /dev/null +++ b/modular_bandastation/storyteller/code/_defines/storyteller_config.dm @@ -0,0 +1,159 @@ +///Gamemode related configs below +// Point Gain Multipliers +/datum/config_entry/number/mundane_point_gain_multiplier + integer = FALSE + default = 1 + min_val = 0 + +/datum/config_entry/number/moderate_point_gain_multiplier + integer = FALSE + default = 1 + min_val = 0 + +/datum/config_entry/number/major_point_gain_multiplier + integer = FALSE + default = 1 + min_val = 0 + +/datum/config_entry/number/roleset_point_gain_multiplier + integer = FALSE + default = 1 + min_val = 0 + +/datum/config_entry/number/objectives_point_gain_multiplier + integer = FALSE + default = 1 + min_val = 0 + +// Roundstart points Multipliers +/datum/config_entry/number/mundane_roundstart_point_multiplier + integer = FALSE + default = 1 + min_val = 0 + +/datum/config_entry/number/moderate_roundstart_point_multiplier + integer = FALSE + default = 1 + min_val = 0 + +/datum/config_entry/number/major_roundstart_point_multiplier + integer = FALSE + default = 1 + min_val = 0 + +/datum/config_entry/number/roleset_roundstart_point_multiplier + integer = FALSE + default = 1 + min_val = 0 + +/datum/config_entry/number/objectives_roundstart_point_multiplier + integer = FALSE + default = 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_bandastation/storyteller/code/_defines/storyteller_defines.dm b/modular_bandastation/storyteller/code/_defines/storyteller_defines.dm new file mode 100644 index 0000000000000..bc4b8119fa6b3 --- /dev/null +++ b/modular_bandastation/storyteller/code/_defines/storyteller_defines.dm @@ -0,0 +1,115 @@ + +//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 planetary. +#define TAG_PLANETARY "planetary" +/// When the event is an external threat (meteors, nukies). +#define TAG_EXTERNAL "external" +/// When the event is an alien threat (blob, xenos) +#define TAG_ALIEN "alien" +/// When the event is magical in nature +#define TAG_MAGICAL "magical" +/// Сколько раз сторителлер будет пытаться переподбирать сикей для гост-ролей в случае ошибки +#define STORYTELLER_MAXIMUM_RETRIES 10 + +#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 5 SECONDS + +#define EVENT_POINT_GAINED_PER_SECOND 0.08 + +#define TRACK_FAIL_POINT_PENALTY_MULTIPLIER 0.75 + +#define GAMEMODE_PANEL_MAIN "Main" +#define GAMEMODE_PANEL_VARIABLES "Variables" + +#define MUNDANE_POINT_THRESHOLD 500 +#define MODERATE_POINT_THRESHOLD 750 +#define MAJOR_POINT_THRESHOLD 1950 +#define ROLESET_POINT_THRESHOLD 1650 +#define OBJECTIVES_POINT_THRESHOLD 8000 + +#define MUNDANE_MIN_POP 4 +#define MODERATE_MIN_POP 6 +#define MAJOR_MIN_POP 20 +#define ROLESET_MIN_POP 25 +#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_BASIC_MULT 10 +#define STORYTELLER_MIN_ANTAG_POPCOUNT 20 +#define STORYTELLER_SEC_ANTAG_MODIFIER 1 +#define STORYTELLER_BASIC_MODIFIER 1 + +#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 20 + +///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 2 + +#define ROUNDSTART_OBJECTIVES_BASE 40 +#define ROUNDSTART_OBJECTIVES_GAIN 2 + +#define SHARED_HIGH_THREAT "high threat event" +#define SHARED_ANOMALIES "anomalous event" +#define SHARED_SCRUBBERS "scrubber-related event" +#define SHARED_METEORS "meteor event" +#define SHARED_BSOD "tech malfunction event" +#define SHARED_CHANGELING "changelings" diff --git a/modular_bandastation/storyteller/code/_helpers.dm b/modular_bandastation/storyteller/code/_helpers.dm new file mode 100644 index 0000000000000..f485fb29adb80 --- /dev/null +++ b/modular_bandastation/storyteller/code/_helpers.dm @@ -0,0 +1,37 @@ +/proc/return_antag_weight(list/candidates) + . = list() + for(var/anything in candidates) + var/client/client_source + if(ismob(anything)) + var/mob/mob = anything + client_source = mob.client + else if(IS_CLIENT_OR_MOCK(anything)) + client_source = anything + if(QDELETED(client_source) || !client_source.ckey) + continue + .[client_source.ckey] = 10 + +/// Pick a random element from the list and remove it from the list. +/proc/pick_n_take_weighted(list/list_to_pick) + if(length(list_to_pick)) + var/picked = pick_weight(list_to_pick) + list_to_pick -= picked + return picked + +/proc/is_late_arrival(mob/living/player) + var/static/cached_result + if(!isnull(cached_result)) + return cached_result + if(!HAS_TRAIT(SSstation, STATION_TRAIT_LATE_ARRIVALS) || (STATION_TIME_PASSED() > 1 MINUTES)) + return cached_result = FALSE + if(QDELETED(player) || !istype(get_area(player), /area/shuttle/arrival)) + return FALSE + return TRUE + +/mob/living/proc/clear_inventory(include_pockets = TRUE, include_held_items = TRUE) + var/list/items = get_equipped_items(include_pockets) + if(include_held_items) + items |= held_items + for(var/item in items) + dropItemToGround(item, force = TRUE, silent = TRUE, invdrop = FALSE) + qdel(item) diff --git a/modular_bandastation/storyteller/code/dynamic_report.dm b/modular_bandastation/storyteller/code/dynamic_report.dm new file mode 100644 index 0000000000000..a832d7e14ae03 --- /dev/null +++ b/modular_bandastation/storyteller/code/dynamic_report.dm @@ -0,0 +1,25 @@ +/// Generate the advisory level depending on the shown threat level. +/datum/controller/subsystem/dynamic/generate_advisory_level() + var/advisory_string = "" + var/list/green_nebula_storytellers = list(/datum/storyteller/extended, /datum/storyteller/chill) //list for calmer storytellers for a greenshift + var/list/midnight_sun_storytellers = list(/datum/storyteller/jester, /datum/storyteller/clown) + var/list/orange_star_storytellers = list(/datum/storyteller/default, /datum/storyteller/fragile, /datum/storyteller/mystic) + var/list/red_moon_storytellers = list(/datum/storyteller/bomb, /datum/storyteller/gamer, /datum/storyteller/operative) + var/list/black_orbit_storytellers = list(/datum/storyteller/black_orbit, /datum/storyteller/pups) //list for the more chaotic storytellers for black sun + if(SSgamemode.selected_storyteller in green_nebula_storytellers) + advisory_string += "Advisory Level: Green Nebula
" + advisory_string += "Your sector's advisory level is Green Nebula. Surveillance information shows no credible threats to Nanotrasen assets within the Spinward Sector at this time. As always, the Department advises maintaining vigilance against potential threats, regardless of a lack of known threats." + if(SSgamemode.selected_storyteller in midnight_sun_storytellers) + advisory_string += "Advisory Level: Midnight Sun
" + advisory_string += "Your sector's advisory level is Midnight Sun. Credible information passed to us by GDI suggests that the Syndicate is preparing to mount a major concerted offensive on Nanotrasen assets in the Spinward Sector to cripple our foothold there. All stations should remain on high alert and prepared to defend themselves." + if(SSgamemode.selected_storyteller in orange_star_storytellers) + advisory_string += "Advisory Level: Orange Star
" + advisory_string += "Your sector's advisory level is Orange Star. Upon reviewing your sector's intelligence, the Department has determined that the risk of enemy activity is moderate to severe. At this advisory, we recommend maintaining a higher degree of security and alertness, and vigilance against threats that may (or will) arise." + if(SSgamemode.selected_storyteller in red_moon_storytellers) + advisory_string += "Advisory Level: Red Moon
" + advisory_string += "Your sector's advisory level is Red Moon. Upon reviewing your sector's intelligence, the Department has determined that the risk of enemy activity is moderate to severe. At this advisory, we recommend maintaining a higher degree of security and alertness, and vigilance against threats that may (or will) arise." + if(SSgamemode.selected_storyteller in black_orbit_storytellers) + advisory_string += "Advisory Level: Black Orbit
" + advisory_string += "Your sector's advisory level is Black Orbit. Central Command has determined that the risk of enemy activity is high. Central Command abandon you and crew. Now only god can help you." + + return advisory_string diff --git a/modular_bandastation/storyteller/code/event_defines/_round_event.dm b/modular_bandastation/storyteller/code/event_defines/_round_event.dm new file mode 100644 index 0000000000000..600919cff3352 --- /dev/null +++ b/modular_bandastation/storyteller/code/event_defines/_round_event.dm @@ -0,0 +1,233 @@ +/datum/round_event + /// Whether the event called its start() yet or not. + var/has_started = FALSE + ///have we finished setup? + var/setup = FALSE + ///Записывать ли событие в лог СТ для вывода в конце раунда? + var/excute_round_end_reports = FALSE + ///Ивент считается форшеным? + var/forced = 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 + +/datum/round_event/proc/round_end_report() + return + +/datum/round_event/setup() + . = ..() + if(excute_round_end_reports) + SSgamemode.round_end_data |= src + +/datum/round_event/antagonist + fakeable = FALSE + end_when = 6000 //This is so prompted picking events have time to run //TODO: refactor events so they can be the masters of themselves, instead of relying on some weirdly timed vars + +/datum/round_event/antagonist/solo + // ALL of those variables are internal. Check the control event to change them + /// The antag flag passed from control + var/antag_flag + /// The antag datum passed from control + var/antag_datum + /// The antag count passed from control + var/antag_count + /// The restricted roles (jobs) passed from control + var/list/restricted_roles + /// The minds we've setup in setup() and need to finalize in start() + var/list/setup_minds = list() + /// Whether we prompt the players before picking them. + var/prompted_picking = FALSE + /// DO NOT SET THIS MANUALLY, THIS IS INHERITED FROM THE EVENT CONTROLLER ON NEW + var/list/extra_spawned_events + /// Similar to extra_spawned_events however these are only used by roundstart events and will only try and run if we have the points to do so + var/list/preferred_events + +/datum/round_event/antagonist/solo/New(my_processing, datum/round_event_control/event_controller) + . = ..() + if(istype(event_controller, /datum/round_event_control/antagonist/solo)) + var/datum/round_event_control/antagonist/solo/antag_event_controller = event_controller + if(antag_event_controller) + if(antag_event_controller.extra_spawned_events) + extra_spawned_events = fill_with_ones(antag_event_controller.extra_spawned_events) + if(antag_event_controller.preferred_events) + preferred_events = fill_with_ones(antag_event_controller.preferred_events) + +/datum/round_event/antagonist/solo/setup() + var/datum/round_event_control/antagonist/solo/cast_control = control + antag_count = forced && cast_control.forced_antags_count > 0 ? cast_control.forced_antags_count : cast_control.get_antag_count_to_spawn(forced) + if(!antag_count) + return + + antag_flag = cast_control.antag_flag + antag_datum = cast_control.antag_datum + restricted_roles = cast_control.restricted_roles + prompted_picking = cast_control.prompted_picking + var/list/possible_candidates = cast_control.get_candidates() + var/list/candidates = list() + + //guh + var/list/cliented_list = list() + for(var/mob/living/mob as anything in possible_candidates) + cliented_list += mob.client + + var/list/weighted_candidates = return_antag_weight(possible_candidates) + + var/valid_to_spawn = TRUE + var/failed_retries = 0 + while(length(weighted_candidates) && length(candidates) < antag_count && valid_to_spawn && failed_retries < STORYTELLER_MAXIMUM_RETRIES) //both of these pick_n_take from weighted_candidates so this should be fine + if(prompted_picking) + var/picked_ckey = pick_n_take_weighted(weighted_candidates) + var/client/picked_client = GLOB.directory[picked_ckey] + if(QDELETED(picked_client)) + failed_retries++ + continue + var/mob/picked_mob = picked_client.mob + log_storyteller("Prompted antag event mob: [picked_mob], special role: [picked_mob.mind?.special_role ? picked_mob.mind.special_role : "none"]") + if(picked_mob) + candidates |= SSpolling.poll_candidates( + question = "Would you like to be a [cast_control.name]?", + check_jobban = antag_flag, + role = antag_flag, + poll_time = 20 SECONDS, + group = list(picked_mob), + alert_pic = antag_datum, + role_name_text = lowertext(cast_control.name), + chat_text_border_icon = antag_datum, + ) + else + var/picked_ckey = pick_n_take_weighted(weighted_candidates) + var/client/picked_client = GLOB.directory[picked_ckey] + if(QDELETED(picked_client)) + continue + var/mob/picked_mob = picked_client.mob + picked_mob?.mind?.picking = TRUE + log_storyteller("Picked antag event mob: [picked_mob], special role: [picked_mob.mind?.special_role ? picked_mob.mind.special_role : "none"]") + candidates |= picked_mob + + + var/list/picked_mobs = list() + var/spawned_count = 0 + while(spawned_count < antag_count) + if(!length(candidates)) + message_admins("A roleset event got fewer antags then its antag_count and may not function correctly.") + break + + spawned_count++ + if(spawned_count > SSgamemode.get_antag_cap(forced) || spawned_count > SSgamemode.left_antag_count_by_type(cast_control)) + break + + var/mob/candidate = pick_n_take(candidates) + log_storyteller("Antag event spawned mob: [candidate], special role: [candidate.mind?.special_role ? candidate.mind.special_role : "none"]") + + if(!candidate.mind) + candidate.mind = new /datum/mind(candidate.key) + + setup_minds += candidate.mind + candidate.mind.special_role = antag_flag + candidate.mind.restricted_roles = restricted_roles + picked_mobs += WEAKREF(candidate.client) + + setup = TRUE + control.generate_image(picked_mobs) + if(LAZYLEN(extra_spawned_events)) + var/event_type = pick_weight(extra_spawned_events) + if(!event_type) + return + var/datum/round_event_control/triggered_event = locate(event_type) in SSgamemode.control + //wait a second to avoid any potential omnitraitor bs + addtimer(CALLBACK(triggered_event, TYPE_PROC_REF(/datum/round_event_control, run_event), FALSE, null, FALSE, "storyteller"), 1 SECONDS) + +/datum/round_event/antagonist/solo/start() + for(var/datum/mind/antag_mind as anything in setup_minds) + add_datum_to_mind(antag_mind, antag_mind.current) + +/datum/round_event/antagonist/solo/proc/add_datum_to_mind(datum/mind/antag_mind) + antag_mind.add_antag_datum(antag_datum) + GLOB.pre_setup_antags -= antag_mind + +/datum/round_event/antagonist/solo/proc/spawn_extra_events() + if(!LAZYLEN(extra_spawned_events)) + return + var/datum/round_event_control/event = pick_weight(extra_spawned_events) + event?.run_event(random = FALSE, event_cause = "storyteller") + +/datum/round_event/antagonist/solo/proc/create_human_mob_copy(turf/create_at, mob/living/carbon/human/old_mob, qdel_old_mob = TRUE) + if(!old_mob?.client) + return + + var/mob/living/carbon/human/new_character = new(create_at) + if(!create_at) + SSjob.SendToLateJoin(new_character) + + old_mob.client.prefs.safe_transfer_prefs_to(new_character) + new_character.dna.update_dna_identity() + old_mob.mind.transfer_to(new_character) + if(qdel_old_mob) + qdel(old_mob) + return new_character + +/datum/round_event/antagonist/solo/ghost/start() + for(var/datum/mind/antag_mind as anything in setup_minds) + add_datum_to_mind(antag_mind) + +/datum/round_event/antagonist/solo/ghost/setup() + var/datum/round_event_control/antagonist/solo/cast_control = control + antag_count = forced && cast_control.forced_antags_count > 0 ? cast_control.forced_antags_count : cast_control.get_antag_count_to_spawn(forced) + if(!antag_count) + return + + antag_flag = cast_control.antag_flag + antag_datum = cast_control.antag_datum + restricted_roles = cast_control.restricted_roles + prompted_picking = cast_control.prompted_picking + var/list/candidates = cast_control.get_candidates() + + //guh + var/list/cliented_list = list() + for(var/mob/living/mob as anything in candidates) + cliented_list += mob.client + + if(prompted_picking) + //candidates = SSpolling.poll_ghost_candidates(check_jobban = antag_flag, role = antag_flag, alert_pic = /mob/living/carbon/alien/larva, role_name_text = lowertext(cast_control.name)) + candidates = SSpolling.poll_candidates( + question = "Would you like to be a [cast_control.name]?", + check_jobban = antag_flag, + role = antag_flag, + poll_time = 20 SECONDS, + group = candidates, + alert_pic = antag_datum, + role_name_text = lowertext(cast_control.name), + chat_text_border_icon = antag_datum, + ) + + var/list/weighted_candidates = return_antag_weight(candidates) + var/spawned_count = 0 + while(length(weighted_candidates) && spawned_count < antag_count) + var/candidate_ckey = pick_n_take_weighted(weighted_candidates) + var/client/candidate_client = GLOB.directory[candidate_ckey] + if(QDELETED(candidate_client) || QDELETED(candidate_client.mob)) + continue + var/mob/candidate = candidate_client.mob + + spawned_count++ + if(spawned_count > SSgamemode.get_antag_cap(forced) || spawned_count > SSgamemode.left_antag_count_by_type(cast_control)) + break + + if(!candidate.mind) + candidate.mind = new /datum/mind(candidate.key) + var/mob/living/carbon/human/new_human = make_body(candidate) + new_human.mind.special_role = antag_flag + new_human.mind.restricted_roles = restricted_roles + setup_minds += new_human.mind + + setup = TRUE diff --git a/modular_bandastation/storyteller/code/event_defines/_round_event_control.dm b/modular_bandastation/storyteller/code/event_defines/_round_event_control.dm new file mode 100644 index 0000000000000..aa6563be86103 --- /dev/null +++ b/modular_bandastation/storyteller/code/event_defines/_round_event_control.dm @@ -0,0 +1,391 @@ +/datum/round_event_control + /// The typepath to the event group this event is a part of. + var/datum/event_group/event_group = null + 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/calculated_on_track_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 + /// If set then the type or list of types of storytellers we are restricted to being trigged by + var/list/allowed_storytellers + ///do we check against the antag cap before attempting a spawn? + var/checks_antag_cap = FALSE + /// List of enemy roles, will check if x amount of these exist exist + var/list/enemy_roles + ///required number of enemies in roles to exist + var/required_enemies = 0 + ///required power of department to start event + var/eng_required_power = 0 + var/med_required_power = 0 + var/rnd_required_power = 0 + var/head_required_power = 0 + /// Является ли событие эксклюзивным (не допускающим другие) в случае раундстарта + var/exclusive_roundstart_event = FALSE + /// Значение, которое используется при расчете стоимости покупки из раундстарт бюджета. Считается если значение 0. + var/roundstart_cost = 0 + +/datum/round_event_control/proc/get_pre_cost() + return roundstart_cost + +/datum/round_event_control/proc/return_failure_string(players_amt) + var/string + if(SSgamemode.current_storyteller?.disable_distribution || SSgamemode.halted_storyteller) + string += "Storyteller halted" + if(event_group && !GLOB.event_groups[event_group].can_run()) + if(string) + string += "," + string += "Group runing" + if(roundstart && (!SSgamemode.can_run_roundstart)) + if(string) + string += "," + string += "Roundstart only" + if(!roundstart && (SSgamemode.can_run_roundstart)) + if(string) + string += "," + string += "Not roundstart" + // BANDASTATION EDIT END - STORYTELLER + if(occurrences >= max_occurrences) + if(string) + string += "," + string += "Cap Reached" + if(earliest_start >= (world.time - SSticker.round_start_time)) + if(string) + string += "," + string +="Too Soon" + if(players_amt < min_players) + if(string) + string += "," + string += "Lack of players" + if(holidayID && !check_holidays(holidayID)) + if(string) + string += "," + string += "Holiday Event" + if(EMERGENCY_ESCAPED_OR_ENDGAMED) + if(string) + string += "," + string += "Round End" + if(ispath(typepath, /datum/round_event/ghost_role) && !(GLOB.ghost_role_flags & GHOSTROLE_MIDROUND_EVENT)) + if(string) + string += "," + string += "No ghost candidates" + if(checks_antag_cap && !SSgamemode.can_inject_antags()) + if(string) + string += "," + string += "Too Many Antags" + if(allowed_storytellers && SSgamemode.current_storyteller && ((islist(allowed_storytellers) && !is_type_in_list(SSgamemode.current_storyteller, allowed_storytellers)) || SSgamemode.current_storyteller.type != allowed_storytellers)) + if(string) + string += "," + string += "Wrong Storyteller" + if(eng_required_power > SSgamemode.current_eng_power) + if(string) + string += "," + string += "Too low eng power" + if(!weight) + if(string) + string += "," + string += "Can't be selected" + if(med_required_power > SSgamemode.current_med_power) + if(string) + string += "," + string += "Too low med power" + if(rnd_required_power > SSgamemode.current_rnd_power) + if(string) + string += "," + string += "Too low rnd power" + if(head_required_power > SSgamemode.current_head_power) + if(string) + string += "," + string += "Too low head power" + if (dynamic_should_hijack && SSdynamic.random_event_hijacked != HIJACKED_NOTHING) + if(string) + string += "," + string += "Hijack mission" + if (type in SSgamemode.current_storyteller.exclude_events) + if(string) + string += "," + string += "Disabled by choosen storyteller" + return string + +/datum/round_event_control/antagonist/return_failure_string(players_amt) + . =..() + if(!check_enemies()) + if(.) + . += ", " + . += "No Enemies" + if(!check_required()) + if(.) + . += ", " + . += "No Required" + return . + +/datum/round_event_control/antagonist + checks_antag_cap = TRUE + track = EVENT_TRACK_ROLESET + ///list of required roles, needed for this to form + var/list/exclusive_roles + /// Protected roles from the antag roll. People will not get those roles if a config is enabled + var/list/protected_roles + /// Restricted roles from the antag roll + var/list/restricted_roles + var/event_icon_state + //The number of forced antags to spawn with the event if it forced + var/forced_antags_count = 0 + var/can_change_count = FALSE + +/datum/round_event_control/antagonist/solo + typepath = /datum/round_event/antagonist/solo + /// How many baseline antags do we spawn + var/base_antags = 1 + /// How many maximum antags can we spawn + var/maximum_antags = 3 + var/maximum_antags_per_round = 10 + /// For this many players we'll add 1 up to the maximum antag amount + var/denominator = 20 + /// The antag flag to be used + var/antag_flag + /// The antag datum to be applied + var/antag_datum + /// Prompt players for consent to turn them into antags before doing so. Dont allow this for roundstart. + var/prompted_picking = FALSE + /// A list of extra events to force whenever this one is chosen by the storyteller. + /// Can either be normal list or a weighted list. + var/list/extra_spawned_events + /// Similar to extra_spawned_events however these are only used by roundstart events and will only try and run if we have the points to do so + var/list/preferred_events + + var/price_to_buy_adds = 20 + +/datum/round_event_control/antagonist/solo/return_failure_string(players_amt) + . =..() + var/list/candidates = get_candidates() //we should optimize this + var/ghost_event = ispath(typepath, /datum/round_event/antagonist/solo/ghost) || ispath(typepath, /datum/round_event/ghost_role) + if((length(candidates) < base_antags)) + if(.) + . += ", " + . += get_antag_count_to_spawn() ? "Not Enough [ghost_event ? "ghost" : ""] candidates!" : "No empty antag-slots" + + return . + +/datum/round_event_control/proc/generate_image(list/mobs) + return + +/* Для титров +/datum/round_event_control/antagonist/generate_image(list/mobs) + SScredits.generate_major_icon(mobs, event_icon_state) +*/ + +/// Check if our enemy_roles requirement is met, if return_players is set then we will return the list of enemy players instead +/datum/round_event_control/proc/check_enemies(return_players = FALSE) + if(!length(enemy_roles)) + return return_players ? list() : TRUE + + var/job_check = 0 + var/list/enemy_players = list() + if(roundstart) + for(var/enemy in enemy_roles) + var/datum/job/enemy_job = SSjob.get_job(enemy) + if(enemy_job && SSjob.assigned_players_by_job[enemy_job.type]) + job_check += length(SSjob.assigned_players_by_job[enemy_job.type]) + enemy_players += SSjob.assigned_players_by_job[enemy_job.type] + + else + for(var/mob/M in GLOB.alive_player_list) + if (M.stat == DEAD) + continue // Dead players cannot count as opponents + if (M.mind && (M.mind.assigned_role.title in enemy_roles)) + job_check++ // Checking for "enemies" (such as sec officers). To be counters, they must either not be candidates to that + enemy_players += M + + if(job_check >= required_enemies) + return return_players ? enemy_players : TRUE + return return_players ? enemy_players : FALSE + +/datum/round_event_control/antagonist/New() + . = ..() + if(CONFIG_GET(flag/protect_roles_from_antagonist)) + restricted_roles |= protected_roles + +/datum/round_event_control/antagonist/can_spawn_event(players_amt, allow_magic = FALSE, fake_check = FALSE) + . = ..() + if(!check_required()) + return FALSE + + if(!.) + return + +/datum/round_event_control/antagonist/proc/check_required() + if(!length(exclusive_roles)) + return TRUE + for (var/mob/M in GLOB.alive_player_list) + if (M.stat == DEAD) + continue // Dead players cannot count as passing requirements + if(M.mind && (M.mind.assigned_role.title in exclusive_roles)) + return TRUE + +/datum/round_event_control/antagonist/proc/trim_candidates(list/candidates) + if(!roundstart) + for(var/mob/candidate in candidates) + //Если событие призраков, то проверить. что кандидат - призрак + if((ispath(typepath, /datum/round_event/ghost_role) || ispath(typepath, /datum/round_event/antagonist/solo/ghost/)) && candidate.stat != DEAD) + candidates -= candidate + //Если событие живых, то проверить. что кандидат - жив или без сознания (не мертв, не в софт крите и не в хард крите) + if(!(ispath(typepath, /datum/round_event/ghost_role) || ispath(typepath, /datum/round_event/antagonist/solo/ghost/)) && !(candidate.stat == CONSCIOUS || candidate.stat == UNCONSCIOUS)) + candidates -= candidate + return candidates + +/datum/round_event_control/antagonist/New() + . = ..() + if(CONFIG_GET(flag/protect_roles_from_antagonist)) + restricted_roles |= protected_roles + +/datum/round_event_control/antagonist/can_spawn_event(players_amt, allow_magic = FALSE, fake_check = FALSE) + . = ..() + if(!check_required()) + return FALSE + + if(!.) + return + +/datum/round_event_control/antagonist/solo/from_ghosts/get_candidates() + var/round_started = SSticker.HasRoundStarted() + var/midround_antag_pref_arg = round_started ? FALSE : TRUE + + var/list/candidates = SSgamemode.get_candidates(antag_flag, antag_flag, observers = TRUE, midround_antag_pref = midround_antag_pref_arg, restricted_roles = restricted_roles) + candidates = trim_candidates(candidates) + return candidates + +/datum/round_event_control/antagonist/solo/can_spawn_event(players_amt, allow_magic = FALSE, fake_check = FALSE) + . = ..() + if(!.) + return + var/antag_amt = get_antag_count_to_spawn() + var/list/candidates = get_candidates() + if((length(candidates) < antag_amt) || !antag_amt) + return FALSE + +/datum/round_event_control/antagonist/solo/proc/get_antag_count_to_spawn(forced_event = FALSE) + var/decided_count = rand(base_antags, maximum_antags) + var/current_cap = SSgamemode.get_antag_cap(forced_event) + var/gamemode_antags_left = current_cap - SSgamemode.get_antag_count() + var/maximum_to_spawn_on_calculation = min(gamemode_antags_left, maximum_antags) + var/maximum_to_spawn_on_population = FLOOR(SSgamemode.get_correct_popcount() / denominator, 1) + var/maximum_to_spawn = min(maximum_to_spawn_on_calculation, maximum_to_spawn_on_population) + var/clamped_value = clamp(decided_count, 0, maximum_to_spawn) + //Maximum antags per round left to spawn + //Получить уже количество антагов в раунде + var/typed_antags_in_round = SSgamemode.get_antag_count_by_type(antag_datum) + var/left_to_spawn = maximum_antags_per_round - typed_antags_in_round + var/final_value = clamp(clamped_value, 0, left_to_spawn) + //double check + var/predicted_count = final_value + SSgamemode.get_antag_count() + while(predicted_count > current_cap && final_value > 0) + final_value-- + + return final_value + +/datum/round_event_control/antagonist/solo/proc/get_candidates() + var/round_started = SSticker.HasRoundStarted() + var/new_players_arg = round_started ? FALSE : TRUE + var/living_players_arg = round_started ? TRUE : FALSE + var/midround_antag_pref_arg = round_started ? FALSE : TRUE + + var/list/candidates = SSgamemode.get_candidates(antag_flag, antag_flag, FALSE, new_players_arg, living_players_arg, midround_antag_pref = midround_antag_pref_arg, \ + restricted_roles = restricted_roles, required_roles = exclusive_roles) + candidates = trim_candidates(candidates) + return candidates + +///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 "Add Roundstart Force Roundstart" + else + return "Fire Schedule Force Next" + + +/datum/round_event_control/Topic(href, href_list) + . = ..() + if(QDELETED(src)) + return + switch(href_list["action"]) + if("schedule") + var/new_schedule = roundstart ? 0 : input(usr, "New schedule time (in seconds):", "Reschedule Event") as num|null + if(isnull(new_schedule) || QDELETED(src)) + return + message_admins("[key_name_admin(usr)] scheduled event [src.name] in [new_schedule] seconds.") + log_admin_private("[key_name(usr)] scheduled [src.name] in [new_schedule] seconds.") + SSgamemode.schedule_event(src, new_schedule SECONDS, 0, _forced = FALSE) + if("force_next") + if(length(src.admin_setup)) + for(var/datum/event_admin_setup/admin_setup_datum in src.admin_setup) + if(admin_setup_datum.prompt_admins() == ADMIN_CANCEL_EVENT) + return + if(ispath(type, /datum/round_event_control/antagonist)) + var/new_value = input(usr, "How many players affected:", "Set value") as num|null + if(isnull(new_value) || new_value < 1) + return + message_admins("[key_name_admin(usr)] forced scheduled event [src.name] with count [new_value].") + log_admin_private("[key_name(usr)] forced scheduled event [src.name] with count [new_value].") + var/datum/round_event_control/antagonist/forced_antag_event = new type() + forced_antag_event.forced_antags_count = new_value + SSgamemode.forced_next_events[forced_antag_event.track] += forced_antag_event + else + message_admins("[key_name_admin(usr)] forced scheduled event [src.name].") + log_admin_private("[key_name(usr)] forced scheduled event [src.name].") + SSgamemode.forced_next_events[src.track] += src + if("fire") + if(length(src.admin_setup)) + for(var/datum/event_admin_setup/admin_setup_datum in src.admin_setup) + if(admin_setup_datum.prompt_admins() == ADMIN_CANCEL_EVENT) + return + message_admins("[key_name_admin(usr)] fired event [src.name].") + log_admin_private("[key_name(usr)] fired event [src.name].") + if(ispath(type, /datum/round_event_control/antagonist)) + var/datum/round_event_control/antagonist/forced_antag_event = src + if(forced_antag_event.can_change_count) + var/new_value = input(usr, "How many players affected:", "Set value") as num|null + if(isnull(new_value) || new_value < 1) + return + forced_antag_event.forced_antags_count = new_value + forced_antag_event.run_event(random = FALSE, admin_forced = TRUE) + else + run_event(random = FALSE, admin_forced = TRUE) diff --git a/modular_bandastation/storyteller/code/event_defines/crewset/_override.dm b/modular_bandastation/storyteller/code/event_defines/crewset/_override.dm new file mode 100644 index 0000000000000..88edd216b0ff2 --- /dev/null +++ b/modular_bandastation/storyteller/code/event_defines/crewset/_override.dm @@ -0,0 +1,11 @@ +/datum/round_event_control/spider_infestation + track = EVENT_TRACK_ROLESET + tags = list(TAG_COMBAT, TAG_DESTRUCTIVE, TAG_EXTERNAL, TAG_ALIEN) + weight = 2 + +/datum/round_event_control/blob + track = EVENT_TRACK_ROLESET + tags = list(TAG_DESTRUCTIVE, TAG_COMBAT, TAG_EXTERNAL, TAG_ALIEN) + earliest_start = 60 MINUTES + checks_antag_cap = TRUE + event_group = /datum/event_group/blobs diff --git a/modular_bandastation/storyteller/code/event_defines/crewset/blob_infection.dm b/modular_bandastation/storyteller/code/event_defines/crewset/blob_infection.dm new file mode 100644 index 0000000000000..1ff936b5a9aa6 --- /dev/null +++ b/modular_bandastation/storyteller/code/event_defines/crewset/blob_infection.dm @@ -0,0 +1,14 @@ +/datum/round_event_control/antagonist/solo/blob + track = EVENT_TRACK_ROLESET + tags = list(TAG_DESTRUCTIVE, TAG_COMBAT, TAG_EXTERNAL, TAG_ALIEN) + earliest_start = 60 MINUTES + checks_antag_cap = TRUE + antag_flag = ROLE_BLOB_INFECTION + min_players = 20 + antag_datum = /datum/antagonist/blob/infection + maximum_antags = 1 + event_group = /datum/event_group/blobs + +/datum/round_event_control/antagonist/solo/blob/midround + name = "Blob infection (Blob)" + prompted_picking = FALSE diff --git a/modular_bandastation/storyteller/code/event_defines/crewset/blood_brothers.dm b/modular_bandastation/storyteller/code/event_defines/crewset/blood_brothers.dm new file mode 100644 index 0000000000000..5bc1fe671c00c --- /dev/null +++ b/modular_bandastation/storyteller/code/event_defines/crewset/blood_brothers.dm @@ -0,0 +1,65 @@ +/datum/round_event_control/antagonist/solo/brother + antag_flag = ROLE_BROTHER + antag_datum = /datum/antagonist/brother + typepath = /datum/round_event/antagonist/solo/brother + tags = list(TAG_COMBAT, TAG_TEAM_ANTAG) + protected_roles = list( + JOB_CAPTAIN, + JOB_HEAD_OF_PERSONNEL, + JOB_CHIEF_ENGINEER, + JOB_CHIEF_MEDICAL_OFFICER, + JOB_RESEARCH_DIRECTOR, + JOB_DETECTIVE, + JOB_HEAD_OF_SECURITY, + JOB_BLUESHIELD, + JOB_NANOTRASEN_REPRESENTATIVE, + JOB_PRISONER, + JOB_SECURITY_OFFICER, + JOB_MAGISTRATE, + JOB_WARDEN, + ) + restricted_roles = list( + JOB_AI, + JOB_CYBORG + ) + enemy_roles = list( + JOB_CAPTAIN, + JOB_HEAD_OF_SECURITY, + JOB_DETECTIVE, + JOB_WARDEN, + JOB_SECURITY_OFFICER, + ) + required_enemies = 1 + weight = 15 + maximum_antags = 2 + maximum_antags_per_round = 2 + denominator = 30 + cost = 0.45 // so it doesn't eat up threat for a relatively low-threat antag + +/datum/round_event_control/antagonist/solo/brother/roundstart + name = "Antag-mix" + roundstart = TRUE + earliest_start = 0 SECONDS + extra_spawned_events = list( + /datum/round_event_control/antagonist/solo/traitor/adds = 12, + /datum/round_event_control/antagonist/solo/heretic/adds = 10, + /datum/round_event_control/antagonist/solo/changeling/adds = 8, + ) + roundstart_cost = 20 + +/datum/round_event_control/antagonist/solo/brother/midround + name = "Blood Brothers (Admin spawn)" + earliest_start = 0 SECONDS + extra_spawned_events = list( + /datum/round_event_control/antagonist/solo/traitor/adds = 12, + /datum/round_event_control/antagonist/solo/heretic/adds = 10, + /datum/round_event_control/antagonist/solo/changeling/adds = 8, + ) + weight = 0 + +/datum/round_event/antagonist/solo/brother/add_datum_to_mind(datum/mind/antag_mind) + var/datum/team/brother_team/team = new + team.add_member(antag_mind) + team.forge_brother_objectives() + antag_mind.add_antag_datum(/datum/antagonist/brother, team) + GLOB.pre_setup_antags -= antag_mind diff --git a/modular_bandastation/storyteller/code/event_defines/crewset/changeling.dm b/modular_bandastation/storyteller/code/event_defines/crewset/changeling.dm new file mode 100644 index 0000000000000..e858da3e9f690 --- /dev/null +++ b/modular_bandastation/storyteller/code/event_defines/crewset/changeling.dm @@ -0,0 +1,46 @@ +/datum/round_event_control/antagonist/solo/changeling + antag_flag = ROLE_CHANGELING + tags = list(TAG_COMBAT, TAG_ALIEN) + antag_datum = /datum/antagonist/changeling + protected_roles = list( + JOB_CAPTAIN, + JOB_HEAD_OF_PERSONNEL, + JOB_CHIEF_ENGINEER, + JOB_CHIEF_MEDICAL_OFFICER, + JOB_RESEARCH_DIRECTOR, + JOB_DETECTIVE, + JOB_HEAD_OF_SECURITY, + JOB_PRISONER, + JOB_SECURITY_OFFICER, + JOB_WARDEN, + JOB_BLUESHIELD, + JOB_MAGISTRATE, + JOB_NANOTRASEN_REPRESENTATIVE, + ) + restricted_roles = list( + JOB_AI, + JOB_CYBORG, + ) + min_players = 20 + maximum_antags_per_round = 3 + +/datum/round_event_control/antagonist/solo/changeling/adds + name = "Changelings" + roundstart = TRUE + earliest_start = 0 + roundstart_cost = 20 + +/datum/round_event_control/antagonist/solo/changeling/solomode + name = "Changelings" + roundstart = TRUE + earliest_start = 0 + base_antags = 1 + maximum_antags = 1 + roundstart_cost = 24 + exclusive_roundstart_event = TRUE + price_to_buy_adds = 30 + +/datum/round_event_control/antagonist/solo/changeling/midround + name = "Genome Awakening (Changelings)" + prompted_picking = FALSE + can_change_count = TRUE diff --git a/modular_bandastation/storyteller/code/event_defines/crewset/clown_ops.dm b/modular_bandastation/storyteller/code/event_defines/crewset/clown_ops.dm new file mode 100644 index 0000000000000..7e097c15cd070 --- /dev/null +++ b/modular_bandastation/storyteller/code/event_defines/crewset/clown_ops.dm @@ -0,0 +1,15 @@ +/datum/round_event_control/antagonist/solo/nuclear_operative/clown + name = "Roundstart Clown Operative" + tags = list(TAG_DESTRUCTIVE, TAG_COMBAT, TAG_TEAM_ANTAG, TAG_EXTERNAL) + antag_flag = ROLE_CLOWN_OPERATIVE + antag_datum = /datum/antagonist/nukeop/clownop + typepath = /datum/round_event/antagonist/solo/nuclear_operative/clown + weight = 0 //these are meant to be very rare + max_occurrences = 1 + event_icon_state = "flukeops" + +/datum/round_event/antagonist/solo/nuclear_operative/clown + required_role = ROLE_CLOWN_OPERATIVE + job_type = /datum/job/clown_operative + antag_type = /datum/antagonist/nukeop/clownop + leader_antag_type = /datum/antagonist/nukeop/leader/clownop diff --git a/modular_bandastation/storyteller/code/event_defines/crewset/cult.dm b/modular_bandastation/storyteller/code/event_defines/crewset/cult.dm new file mode 100644 index 0000000000000..c5933c8c34f22 --- /dev/null +++ b/modular_bandastation/storyteller/code/event_defines/crewset/cult.dm @@ -0,0 +1,84 @@ +/datum/round_event_control/antagonist/solo/bloodcult + name = "Blood Cult" + tags = list(TAG_SPOOKY, TAG_DESTRUCTIVE, TAG_COMBAT, TAG_TEAM_ANTAG, TAG_MAGICAL) + antag_flag = ROLE_CULTIST + antag_datum = /datum/antagonist/cult + typepath = /datum/round_event/antagonist/solo/bloodcult + restricted_roles = list( + JOB_AI, + JOB_CAPTAIN, + JOB_CHAPLAIN, + JOB_CYBORG, + JOB_DETECTIVE, + JOB_HEAD_OF_PERSONNEL, + JOB_HEAD_OF_SECURITY, + JOB_PRISONER, + JOB_SECURITY_OFFICER, + JOB_WARDEN, + JOB_BLUESHIELD, + JOB_MAGISTRATE, + JOB_NANOTRASEN_REPRESENTATIVE, + ) + maximum_antags = 3 + enemy_roles = list( + JOB_CAPTAIN, + JOB_DETECTIVE, + JOB_HEAD_OF_SECURITY, + JOB_SECURITY_OFFICER, + JOB_WARDEN, + ) + required_enemies = 5 + base_antags = 2 + // I give up, just there should be enough heads with 35 players... + min_players = 30 + roundstart = TRUE + earliest_start = 0 SECONDS + weight = 1 + max_occurrences = 3 + +/datum/round_event_control/antagonist/solo/bloodcult/get_pre_cost() + var/candidates = get_candidates() + roundstart_cost = 20 * length(candidates) + return roundstart_cost + +/datum/round_event/antagonist/solo/bloodcult + excute_round_end_reports = TRUE + end_when = 60000 + var/static/datum/team/cult/main_cult + +/datum/round_event/antagonist/solo/bloodcult/setup() + . = ..() + if(!main_cult) + main_cult = new() + +/datum/round_event/antagonist/solo/bloodcult/start() + . = ..() + main_cult.setup_objectives() + +/datum/round_event/antagonist/solo/bloodcult/add_datum_to_mind(datum/mind/antag_mind) + var/datum/antagonist/cult/new_cultist = new antag_datum() + new_cultist.cult_team = main_cult + new_cultist.give_equipment = TRUE + antag_mind.add_antag_datum(new_cultist) + GLOB.pre_setup_antags -= antag_mind + +/datum/round_event/antagonist/solo/bloodcult/round_end_report() + if(main_cult.check_cult_victory()) + SSticker.mode_result = "win - cult win" + SSticker.news_report = CULT_SUMMON + return + + SSticker.mode_result = "loss - staff stopped the cult" + + if(main_cult.size_at_maximum == 0) + CRASH("Cult team existed with a size_at_maximum of 0 at round end!") + + // If more than a certain ratio of our cultists have escaped, give the "cult escape" resport. + // Otherwise, give the "cult failure" report. + var/ratio_to_be_considered_escaped = 0.5 + var/escaped_cultists = 0 + for(var/datum/mind/escapee as anything in main_cult.members) + if(considered_escaped(escapee)) + escaped_cultists++ + + SSticker.news_report = (escaped_cultists / main_cult.size_at_maximum) >= ratio_to_be_considered_escaped ? CULT_ESCAPE : CULT_FAILURE diff --git a/modular_bandastation/storyteller/code/event_defines/crewset/heretic.dm b/modular_bandastation/storyteller/code/event_defines/crewset/heretic.dm new file mode 100644 index 0000000000000..a305c5e5ee296 --- /dev/null +++ b/modular_bandastation/storyteller/code/event_defines/crewset/heretic.dm @@ -0,0 +1,57 @@ +/datum/round_event_control/antagonist/solo/heretic + antag_flag = ROLE_HERETIC + tags = list(TAG_COMBAT, TAG_SPOOKY, TAG_MAGICAL) + antag_datum = /datum/antagonist/heretic + protected_roles = list( + JOB_CAPTAIN, + JOB_HEAD_OF_PERSONNEL, + JOB_CHIEF_ENGINEER, + JOB_CHIEF_MEDICAL_OFFICER, + JOB_RESEARCH_DIRECTOR, + JOB_BLUESHIELD, + JOB_NANOTRASEN_REPRESENTATIVE, + JOB_DETECTIVE, + JOB_HEAD_OF_PERSONNEL, + JOB_HEAD_OF_SECURITY, + JOB_PRISONER, + JOB_SECURITY_OFFICER, + JOB_MAGISTRATE, + JOB_WARDEN, + JOB_CHAPLAIN, + ) + restricted_roles = list( + JOB_AI, + JOB_CYBORG, + ) + enemy_roles = list( + JOB_CAPTAIN, + JOB_HEAD_OF_SECURITY, + JOB_DETECTIVE, + JOB_WARDEN, + JOB_SECURITY_OFFICER, + ) + weight = 4 + min_players = 20 + maximum_antags_per_round = 3 + +/datum/round_event_control/antagonist/solo/heretic/adds + name = "Heretics" + weight = 0 + earliest_start = 0 + roundstart_cost = 15 + +/datum/round_event_control/antagonist/solo/heretic/solomode + name = "Heretics" + roundstart = TRUE + earliest_start = 0 + roundstart_cost = 15 + exclusive_roundstart_event = TRUE + price_to_buy_adds = 20 + base_antags = 1 + maximum_antags = 1 + +/datum/round_event_control/antagonist/solo/heretic/midround + name = "Midround Heretics" + prompted_picking = FALSE + required_enemies = 3 + can_change_count = TRUE diff --git a/modular_bandastation/storyteller/code/event_defines/crewset/malf.dm b/modular_bandastation/storyteller/code/event_defines/crewset/malf.dm new file mode 100644 index 0000000000000..ae520771673bf --- /dev/null +++ b/modular_bandastation/storyteller/code/event_defines/crewset/malf.dm @@ -0,0 +1,45 @@ +/datum/round_event_control/antagonist/solo/malf + antag_datum = /datum/antagonist/malf_ai + tags = list(TAG_COMBAT, TAG_DESTRUCTIVE, TAG_ALIEN) //not exactly alien but close enough + antag_flag = ROLE_MALF + enemy_roles = list( + JOB_CHEMIST, + JOB_CHIEF_ENGINEER, + JOB_HEAD_OF_SECURITY, + JOB_RESEARCH_DIRECTOR, + JOB_SCIENTIST, + JOB_SECURITY_OFFICER, + JOB_WARDEN, + JOB_BLUESHIELD, + JOB_NANOTRASEN_REPRESENTATIVE, + ) + exclusive_roles = list(JOB_AI) + required_enemies = 4 + weight = 2 + min_players = 35 + roundstart_cost = 35 + max_occurrences = 1 + +/datum/round_event_control/antagonist/solo/malf/trim_candidates(list/candidates) + for(var/mob/living/player in candidates) + if(!isAI(player)) + candidates -= player + continue + + if(is_centcom_level(player.z)) + candidates -= player + continue + + if(player.mind && (player.mind.special_role || player.mind.antag_datums?.len > 0)) + candidates -= player + + return candidates + +/datum/round_event_control/antagonist/solo/malf/midround + name = "Malfunctioning AI Midround" + antag_flag = ROLE_MALF_MIDROUND + +/datum/round_event_control/antagonist/solo/malf/roundstart + name = "Roundstart Malf AI" + roundstart = TRUE + earliest_start = 0 diff --git a/modular_bandastation/storyteller/code/event_defines/crewset/nuke_ops.dm b/modular_bandastation/storyteller/code/event_defines/crewset/nuke_ops.dm new file mode 100644 index 0000000000000..25e14464dc1bf --- /dev/null +++ b/modular_bandastation/storyteller/code/event_defines/crewset/nuke_ops.dm @@ -0,0 +1,113 @@ +/datum/round_event_control/antagonist/solo/nuclear_operative + name = "Roundstart Nuclear Operative" + tags = list(TAG_DESTRUCTIVE, TAG_COMBAT, TAG_TEAM_ANTAG, TAG_EXTERNAL) + antag_flag = ROLE_OPERATIVE + antag_datum = /datum/antagonist/nukeop + typepath = /datum/round_event/antagonist/solo/nuclear_operative + shared_occurence_type = SHARED_HIGH_THREAT + restricted_roles = list( + JOB_AI, + JOB_CAPTAIN, + JOB_CHIEF_ENGINEER, + JOB_CHIEF_MEDICAL_OFFICER, + JOB_CYBORG, + JOB_DETECTIVE, + JOB_HEAD_OF_PERSONNEL, + JOB_HEAD_OF_SECURITY, + JOB_PRISONER, + JOB_RESEARCH_DIRECTOR, + JOB_SECURITY_OFFICER, + JOB_WARDEN, + JOB_BLUESHIELD, + JOB_MAGISTRATE, + JOB_NANOTRASEN_REPRESENTATIVE, + ) + base_antags = 3 + maximum_antags = 5 + enemy_roles = list( + JOB_AI, + JOB_CYBORG, + JOB_CAPTAIN, + JOB_DETECTIVE, + JOB_HEAD_OF_SECURITY, + JOB_SECURITY_OFFICER, + JOB_WARDEN, + ) + required_enemies = 5 + // I give up, just there should be enough heads with 35 players... + min_players = 35 + roundstart = TRUE + earliest_start = 0 SECONDS + weight = 1 + roundstart_cost = 50 + max_occurrences = 3 + base_antags = 2 + maximum_antags = 2 + event_icon_state = "nukeops" + exclusive_roundstart_event = TRUE + can_change_count = TRUE + +/datum/round_event/antagonist/solo/nuclear_operative + excute_round_end_reports = TRUE + var/required_role = ROLE_NUCLEAR_OPERATIVE + var/job_type = /datum/job/nuclear_operative + var/antag_type = /datum/antagonist/nukeop + var/leader_antag_type = /datum/antagonist/nukeop/leader + var/datum/team/nuclear/nuke_team + +/datum/round_event/antagonist/solo/nuclear_operative/start() + var/datum/mind/most_experienced = get_most_experienced(setup_minds, required_role) || setup_minds[1] + prepare(most_experienced) + var/datum/antagonist/nukeop/leader/leader = most_experienced.add_antag_datum(leader_antag_type, nuke_team) + nuke_team = leader.nuke_team + for(var/datum/mind/antag_mind as anything in setup_minds - most_experienced) + prepare(antag_mind) + var/datum/antagonist/nukeop/op = new antag_type + op.nuke_team = nuke_team + antag_mind.add_antag_datum(op) + +/// Frees the target mind's job slot, clears and deletes all their items, creates a fresh body for them, and sets +/datum/round_event/antagonist/solo/nuclear_operative/proc/prepare(datum/mind/antag_mind) + var/mob/living/current_mob = antag_mind.current + SSjob.FreeRole(antag_mind.assigned_role.title) + current_mob.clear_inventory() + create_human_mob_copy(get_turf(current_mob), current_mob) + antag_mind.set_assigned_role(SSjob.get_job_type(job_type)) + antag_mind.special_role = required_role + +/datum/round_event/antagonist/solo/nuclear_operative/add_datum_to_mind(datum/mind/antag_mind) + CRASH("this should not be called") + +/datum/round_event/antagonist/solo/nuclear_operative/round_end_report() + var/result = nuke_team.get_result() + switch(result) + if(NUKE_RESULT_FLUKE) + SSticker.mode_result = "loss - syndicate nuked - disk secured" + SSticker.news_report = NUKE_SYNDICATE_BASE + if(NUKE_RESULT_NUKE_WIN) + SSticker.mode_result = "win - syndicate nuke" + SSticker.news_report = STATION_DESTROYED_NUKE + if(NUKE_RESULT_NOSURVIVORS) + SSticker.mode_result = "halfwin - syndicate nuke - did not evacuate in time" + SSticker.news_report = STATION_DESTROYED_NUKE + if(NUKE_RESULT_WRONG_STATION) + SSticker.mode_result = "halfwin - blew wrong station" + SSticker.news_report = NUKE_MISS + if(NUKE_RESULT_WRONG_STATION_DEAD) + SSticker.mode_result = "halfwin - blew wrong station - did not evacuate in time" + SSticker.news_report = NUKE_MISS + if(NUKE_RESULT_CREW_WIN_SYNDIES_DEAD) + SSticker.mode_result = "loss - evacuation - disk secured - syndi team dead" + SSticker.news_report = OPERATIVES_KILLED + if(NUKE_RESULT_CREW_WIN) + SSticker.mode_result = "loss - evacuation - disk secured" + SSticker.news_report = OPERATIVES_KILLED + if(NUKE_RESULT_DISK_LOST) + SSticker.mode_result = "halfwin - evacuation - disk not secured" + SSticker.news_report = OPERATIVE_SKIRMISH + if(NUKE_RESULT_DISK_STOLEN) + SSticker.mode_result = "halfwin - detonation averted" + SSticker.news_report = OPERATIVE_SKIRMISH + else + SSticker.mode_result = "halfwin - interrupted" + SSticker.news_report = OPERATIVE_SKIRMISH diff --git a/modular_bandastation/storyteller/code/event_defines/crewset/obsessed.dm b/modular_bandastation/storyteller/code/event_defines/crewset/obsessed.dm new file mode 100644 index 0000000000000..8e0d6b520174f --- /dev/null +++ b/modular_bandastation/storyteller/code/event_defines/crewset/obsessed.dm @@ -0,0 +1,35 @@ +/datum/round_event_control/antagonist/solo/obsessed + antag_flag = ROLE_OBSESSED + tags = list(TAG_COMBAT) + antag_datum // потому-что антаг выдается через событие + typepath = /datum/round_event/obsessed + restricted_roles = list( + JOB_AI, + JOB_CYBORG, + ROLE_POSITRONIC_BRAIN, + ) + weight = 4 + max_occurrences = 3 + +/datum/round_event_control/antagonist/solo/obsessed/midround + name = "Compulsive Obsession" + prompted_picking = FALSE + maximum_antags = 4 + can_change_count = TRUE + +/datum/round_event/obsessed + var/protected_roles = list( + JOB_CAPTAIN, + JOB_HEAD_OF_PERSONNEL, + JOB_CHIEF_ENGINEER, + JOB_CHIEF_MEDICAL_OFFICER, + JOB_RESEARCH_DIRECTOR, + JOB_DETECTIVE, + JOB_HEAD_OF_SECURITY, + JOB_PRISONER, + JOB_SECURITY_OFFICER, + JOB_WARDEN, + JOB_BLUESHIELD, + JOB_MAGISTRATE, + JOB_NANOTRASEN_REPRESENTATIVE, + ) diff --git a/modular_bandastation/storyteller/code/event_defines/crewset/revolution.dm b/modular_bandastation/storyteller/code/event_defines/crewset/revolution.dm new file mode 100644 index 0000000000000..55e6fd9cb050f --- /dev/null +++ b/modular_bandastation/storyteller/code/event_defines/crewset/revolution.dm @@ -0,0 +1,70 @@ +/datum/round_event_control/antagonist/solo/revolutionary + name = "Roundstart Revolution" + tags = list(TAG_COMMUNAL, TAG_DESTRUCTIVE, TAG_COMBAT, TAG_TEAM_ANTAG) + antag_flag = ROLE_REV_HEAD + antag_datum = /datum/antagonist/rev/head/event_trigger + typepath = /datum/round_event/antagonist/solo/revolutionary + restricted_roles = list( + JOB_AI, + JOB_CAPTAIN, + JOB_CHIEF_ENGINEER, + JOB_CHIEF_MEDICAL_OFFICER, + JOB_CYBORG, + JOB_DETECTIVE, + JOB_HEAD_OF_PERSONNEL, + JOB_HEAD_OF_SECURITY, + JOB_PRISONER, + JOB_RESEARCH_DIRECTOR, + JOB_SECURITY_OFFICER, + JOB_WARDEN, + JOB_BLUESHIELD, + JOB_MAGISTRATE, + JOB_NANOTRASEN_REPRESENTATIVE, + ) + base_antags = 2 + enemy_roles = list( + JOB_CAPTAIN, + JOB_DETECTIVE, + JOB_HEAD_OF_SECURITY, + JOB_SECURITY_OFFICER, + JOB_WARDEN, + ) + required_enemies = 6 + // I give up, just there should be enough heads with 35 players... + min_players = 35 + roundstart = TRUE + earliest_start = 0 SECONDS + weight = 0 //value was 3, we need to manually test if this works or not before allowing it normally + roundstart_cost = 40 + max_occurrences = 1 + +/datum/antagonist/rev/head/event_trigger + remove_clumsy = TRUE + give_flash = TRUE + +/datum/round_event/antagonist/solo/revolutionary + excute_round_end_reports = TRUE + end_when = 60000 /// we will end on our own when revs win + var/static/datum/team/revolution/revolution + var/static/finished = FALSE + +/datum/round_event/antagonist/solo/revolutionary/setup() + . = ..() + if(!revolution) + revolution = new() + +/datum/round_event/antagonist/solo/revolutionary/add_datum_to_mind(datum/mind/antag_mind) + antag_mind.add_antag_datum(antag_datum, revolution) + if(length(revolution.members)) + revolution.update_objectives() + revolution.update_rev_heads() + SSshuttle.registerHostileEnvironment(revolution) + GLOB.pre_setup_antags -= antag_mind + + +/datum/round_event/antagonist/solo/revolutionary/round_end_report() + var/winner = revolution.process_victory() + if(isnull(winner)) + return + finished = TRUE + end() diff --git a/modular_bandastation/storyteller/code/event_defines/crewset/spies.dm b/modular_bandastation/storyteller/code/event_defines/crewset/spies.dm new file mode 100644 index 0000000000000..418d41e76c3e5 --- /dev/null +++ b/modular_bandastation/storyteller/code/event_defines/crewset/spies.dm @@ -0,0 +1,15 @@ +/datum/round_event_control/antagonist/solo/spy + name = "Spies" + roundstart = TRUE + earliest_start = 0 SECONDS + + antag_flag = ROLE_SPY + antag_datum = /datum/antagonist/spy + weight = 0 + category = EVENT_CATEGORY_INVASION + tags = list(TAG_CREW_ANTAG) + +/datum/round_event_control/antagonist/solo/spy/midround + name = "Spies (Midround)" + roundstart = FALSE + can_change_count = TRUE diff --git a/modular_bandastation/storyteller/code/event_defines/crewset/traitors.dm b/modular_bandastation/storyteller/code/event_defines/crewset/traitors.dm new file mode 100644 index 0000000000000..67d54dc9f0fcd --- /dev/null +++ b/modular_bandastation/storyteller/code/event_defines/crewset/traitors.dm @@ -0,0 +1,50 @@ +/datum/round_event_control/antagonist/solo/traitor + antag_flag = ROLE_SYNDICATE_INFILTRATOR + tags = list(TAG_COMBAT) + antag_datum = /datum/antagonist/traitor/infiltrator + protected_roles = list( + JOB_CAPTAIN, + JOB_HEAD_OF_PERSONNEL, + JOB_CHIEF_ENGINEER, + JOB_CHIEF_MEDICAL_OFFICER, + JOB_RESEARCH_DIRECTOR, + JOB_DETECTIVE, + JOB_HEAD_OF_SECURITY, + JOB_PRISONER, + JOB_SECURITY_OFFICER, + JOB_WARDEN, + JOB_BLUESHIELD, + JOB_MAGISTRATE, + JOB_NANOTRASEN_REPRESENTATIVE, + ) + restricted_roles = list( + JOB_AI, + JOB_CYBORG, + ) + +/datum/round_event_control/antagonist/solo/traitor/adds + name = "Traitors" + weight = 0 + antag_flag = ROLE_TRAITOR + antag_datum = /datum/antagonist/traitor + earliest_start = 0 SECONDS + roundstart_cost = 15 + +/datum/round_event_control/antagonist/solo/traitor/solomode + name = "Traitors" + antag_flag = ROLE_TRAITOR + antag_datum = /datum/antagonist/traitor + roundstart = TRUE + earliest_start = 0 SECONDS + base_antags = 1 + maximum_antags = 1 + roundstart_cost = 15 + exclusive_roundstart_event = TRUE + price_to_buy_adds = 20 + +/datum/round_event_control/antagonist/solo/traitor/midround + name = "Sleeper Agents (Traitors)" + antag_flag = ROLE_SLEEPER_AGENT + antag_datum = /datum/antagonist/traitor/infiltrator/sleeper_agent + prompted_picking = FALSE + can_change_count = TRUE diff --git a/modular_bandastation/storyteller/code/event_defines/crewset/wizard.dm b/modular_bandastation/storyteller/code/event_defines/crewset/wizard.dm new file mode 100644 index 0000000000000..b4ee7d267ae58 --- /dev/null +++ b/modular_bandastation/storyteller/code/event_defines/crewset/wizard.dm @@ -0,0 +1,55 @@ +/datum/round_event_control/antagonist/solo/wizard + name = "Wizard" + tags = list(TAG_COMBAT, TAG_DESTRUCTIVE, TAG_EXTERNAL, TAG_MAGICAL) + typepath = /datum/round_event/antagonist/solo/wizard + antag_flag = ROLE_WIZARD + antag_datum = /datum/antagonist/wizard + shared_occurence_type = SHARED_HIGH_THREAT + restricted_roles = list( + JOB_CAPTAIN, + JOB_HEAD_OF_SECURITY, + ) // Just to be sure that a wizard getting picked won't ever imply a Captain or HoS not getting drafted + maximum_antags = 1 + enemy_roles = list( + JOB_AI, + JOB_CAPTAIN, + JOB_DETECTIVE, + JOB_HEAD_OF_SECURITY, + JOB_SECURITY_OFFICER, + JOB_WARDEN, + JOB_CHAPLAIN, + JOB_BLUESHIELD, + JOB_MAGISTRATE, + JOB_NANOTRASEN_REPRESENTATIVE, + ) + required_enemies = 5 + roundstart = TRUE + roundstart_cost = 30 + earliest_start = 0 SECONDS + weight = 1 + min_players = 35 + max_occurrences = 1 + event_icon_state = "wizard" + +/datum/round_event_control/antagonist/solo/wizard/can_spawn_event(players_amt, allow_magic = FALSE, fake_check = FALSE) + . = ..() + if(!.) + return + if(!length(GLOB.wizardstart)) + return FALSE + +/datum/round_event/antagonist/solo/wizard + +/datum/round_event/antagonist/solo/wizard/add_datum_to_mind(datum/mind/antag_mind) + var/mob/living/current_mob = antag_mind.current + SSjob.FreeRole(antag_mind.assigned_role.title) + var/list/items = current_mob.get_equipped_items(TRUE) + current_mob.unequip_everything() + for(var/obj/item/item as anything in items) + qdel(item) + + var/mob/living/carbon/human/new_player_mob = new //while funny, it would kind of suck to be a blind criple wizard + antag_mind.transfer_to(new_player_mob) + qdel(current_mob) + antag_mind.make_wizard() + GLOB.pre_setup_antags -= antag_mind diff --git a/modular_bandastation/storyteller/code/event_defines/ghostset/alien_infestation.dm b/modular_bandastation/storyteller/code/event_defines/ghostset/alien_infestation.dm new file mode 100644 index 0000000000000..91f5a5ffdd769 --- /dev/null +++ b/modular_bandastation/storyteller/code/event_defines/ghostset/alien_infestation.dm @@ -0,0 +1,110 @@ +/datum/round_event_control/antagonist/solo/from_ghosts/alien_infestation + name = "Alien Infestation" + typepath = /datum/round_event/antagonist/solo/ghost/alien_infestation + weight = 3 + track = EVENT_TRACK_ROLESET + + min_players = 15 + + earliest_start = 60 MINUTES + + category = EVENT_CATEGORY_ENTITIES + description = "A xenomorph larva spawns on a random vent." + + maximum_antags = 1 + antag_flag = ROLE_ALIEN + enemy_roles = list( + JOB_AI, + JOB_CAPTAIN, + JOB_DETECTIVE, + JOB_HEAD_OF_SECURITY, + JOB_SECURITY_OFFICER, + JOB_WARDEN, + ) + required_enemies = 3 + prompted_picking = TRUE + +/datum/round_event/antagonist/solo/ghost/alien_infestation + announce_when = 400 + fakeable = TRUE + +/datum/round_event/antagonist/solo/ghost/alien_infestation/setup() + announce_when = rand(announce_when, announce_when + 50) + var/datum/round_event_control/antagonist/solo/cast_control = control + antag_count = forced && cast_control.forced_antags_count > 0 ? cast_control.forced_antags_count : cast_control.get_antag_count_to_spawn(forced) + if(!antag_count) + return + + if(prob(50)) + antag_count++ + + antag_flag = cast_control.antag_flag + antag_datum = cast_control.antag_datum + restricted_roles = cast_control.restricted_roles + prompted_picking = cast_control.prompted_picking + var/list/candidates = cast_control.get_candidates() + + //guh + var/list/cliented_list = list() + for(var/mob/living/mob as anything in candidates) + cliented_list += mob.client + + candidates = SSpolling.poll_ghost_candidates(check_jobban = antag_flag, role = antag_flag, alert_pic = /mob/living/carbon/alien/larva, role_name_text = lowertext(cast_control.name)) + + if(!length(candidates)) + return NOT_ENOUGH_PLAYERS + + var/list/vents = list() + for(var/obj/machinery/atmospherics/components/unary/vent_pump/temp_vent as anything in SSmachines.get_machines_by_type_and_subtypes(/obj/machinery/atmospherics/components/unary/vent_pump)) + if(QDELETED(temp_vent)) + continue + if(is_station_level(temp_vent.loc.z) && !temp_vent.welded) + var/datum/pipeline/temp_vent_parent = temp_vent.parents[1] + if(!temp_vent_parent) + continue//no parent vent + //Stops Aliens getting stuck in small networks. + //See: Security, Virology + if(temp_vent_parent.other_atmos_machines.len > 20) + vents += temp_vent + + if(!length(vents)) + message_admins("An event attempted to spawn an alien but no suitable vents were found. Shutting down.") + return MAP_ERROR + + var/list/weighted_candidates = return_antag_weight(candidates) + var/spawned_count = 0 + var/failed_retries = 0 + while(length(weighted_candidates) && spawned_count < antag_count && failed_retries <= STORYTELLER_MAXIMUM_RETRIES) + var/client/candidate_ckey = pick_n_take_weighted(weighted_candidates) + var/client/candidate_client = GLOB.directory[candidate_ckey] + if(QDELETED(candidate_client) || QDELETED(candidate_client.mob)) + failed_retries++ + continue + + spawned_count++ + if(spawned_count > SSgamemode.get_antag_cap(forced) || spawned_count > SSgamemode.left_antag_count_by_type(cast_control)) + break + + var/mob/candidate = candidate_client.mob + if(!candidate.mind) + candidate.mind = new /datum/mind(candidate.key) + + var/obj/vent = pick_n_take(vents) + var/mob/living/carbon/alien/larva/new_xeno = new(vent.loc) + new_xeno.ckey = candidate_ckey + new_xeno.move_into_vent(vent) + + + message_admins("[ADMIN_LOOKUPFLW(new_xeno)] has been made into an alien by an event.") + new_xeno.log_message("was spawned as an alien by an event.", LOG_GAME) + + setup = TRUE + +/datum/round_event/antagonist/solo/ghost/alien_infestation/announce(fake) + var/living_aliens = FALSE + for(var/mob/living/carbon/alien/A in GLOB.player_list) + if(A.stat != DEAD) + living_aliens = TRUE + + if(living_aliens || fake) + priority_announce("Unidentified lifesigns detected coming aboard [station_name()]. Secure any exterior access, including ducting and ventilation.", "Lifesign Alert", ANNOUNCER_ALIENS) diff --git a/modular_bandastation/storyteller/code/event_defines/ghostset/nuke_ops.dm b/modular_bandastation/storyteller/code/event_defines/ghostset/nuke_ops.dm new file mode 100644 index 0000000000000..0c0f8b7336688 --- /dev/null +++ b/modular_bandastation/storyteller/code/event_defines/ghostset/nuke_ops.dm @@ -0,0 +1,152 @@ +/datum/round_event_control/antagonist/solo/from_ghosts/nuclear_operative + name = "Nuclear Assault" + tags = list(TAG_DESTRUCTIVE, TAG_COMBAT, TAG_TEAM_ANTAG, TAG_EXTERNAL) + antag_flag = ROLE_OPERATIVE_MIDROUND + antag_datum = /datum/antagonist/nukeop + typepath = /datum/round_event/antagonist/solo/ghost/nuclear_operative + restricted_roles = list( + JOB_AI, + JOB_CAPTAIN, + JOB_CHIEF_ENGINEER, + JOB_CHIEF_MEDICAL_OFFICER, + JOB_CYBORG, + JOB_DETECTIVE, + JOB_HEAD_OF_PERSONNEL, + JOB_HEAD_OF_SECURITY, + JOB_PRISONER, + JOB_RESEARCH_DIRECTOR, + JOB_SECURITY_OFFICER, + JOB_WARDEN, + ) + base_antags = 3 + maximum_antags = 4 + enemy_roles = list( + JOB_AI, + JOB_CYBORG, + JOB_CAPTAIN, + JOB_DETECTIVE, + JOB_HEAD_OF_SECURITY, + JOB_SECURITY_OFFICER, + JOB_WARDEN, + ) + required_enemies = 5 + // I give up, just there should be enough heads with 35 players... + min_players = 35 + earliest_start = 45 MINUTES + weight = 4 + max_occurrences = 1 + prompted_picking = TRUE + can_change_count = TRUE + +/datum/round_event/antagonist/solo/ghost/nuclear_operative + excute_round_end_reports = TRUE + end_when = 60000 /// we will end on our own when revs win + var/static/datum/team/nuclear/nuke_team + var/set_leader = FALSE + var/required_role = ROLE_NUCLEAR_OPERATIVE + var/job_type = /datum/job/nuclear_operative + +/datum/round_event/antagonist/solo/ghost/nuclear_operative/add_datum_to_mind(datum/mind/antag_mind) + var/mob/living/current_mob = antag_mind.current + var/list/items = current_mob.get_equipped_items(TRUE) + current_mob.unequip_everything() + for(var/obj/item/item as anything in items) + qdel(item) + + var/datum/mind/most_experienced = get_most_experienced(setup_minds, required_role) + antag_mind.set_assigned_role(SSjob.get_job_type(/datum/job/nuclear_operative)) + antag_mind.special_role = ROLE_NUCLEAR_OPERATIVE + + if(!most_experienced) + most_experienced = antag_mind + + if(!set_leader) + set_leader = TRUE + var/datum/antagonist/nukeop/leader/leader_antag_datum = new() + nuke_team = leader_antag_datum.nuke_team + most_experienced.add_antag_datum(leader_antag_datum) + + if(antag_mind == most_experienced) + return + + var/datum/antagonist/nukeop/new_op = new antag_datum() + antag_mind.add_antag_datum(new_op) + +//this might be able to be kept as just calling parent +/datum/round_event/antagonist/solo/ghost/nuclear_operative/round_end_report() + var/result = nuke_team.get_result() + switch(result) + if(NUKE_RESULT_FLUKE) + SSticker.mode_result = "loss - syndicate nuked - disk secured" + SSticker.news_report = NUKE_SYNDICATE_BASE + if(NUKE_RESULT_NUKE_WIN) + SSticker.mode_result = "win - syndicate nuke" + SSticker.news_report = STATION_DESTROYED_NUKE + if(NUKE_RESULT_NOSURVIVORS) + SSticker.mode_result = "halfwin - syndicate nuke - did not evacuate in time" + SSticker.news_report = STATION_DESTROYED_NUKE + if(NUKE_RESULT_WRONG_STATION) + SSticker.mode_result = "halfwin - blew wrong station" + SSticker.news_report = NUKE_MISS + if(NUKE_RESULT_WRONG_STATION_DEAD) + SSticker.mode_result = "halfwin - blew wrong station - did not evacuate in time" + SSticker.news_report = NUKE_MISS + if(NUKE_RESULT_CREW_WIN_SYNDIES_DEAD) + SSticker.mode_result = "loss - evacuation - disk secured - syndi team dead" + SSticker.news_report = OPERATIVES_KILLED + if(NUKE_RESULT_CREW_WIN) + SSticker.mode_result = "loss - evacuation - disk secured" + SSticker.news_report = OPERATIVES_KILLED + if(NUKE_RESULT_DISK_LOST) + SSticker.mode_result = "halfwin - evacuation - disk not secured" + SSticker.news_report = OPERATIVE_SKIRMISH + if(NUKE_RESULT_DISK_STOLEN) + SSticker.mode_result = "halfwin - detonation averted" + SSticker.news_report = OPERATIVE_SKIRMISH + else + SSticker.mode_result = "halfwin - interrupted" + SSticker.news_report = OPERATIVE_SKIRMISH + +/datum/round_event/antagonist/solo/ghost/nuclear_operative/setup() + var/datum/round_event_control/antagonist/solo/cast_control = control + antag_count = forced && cast_control.forced_antags_count > 0 ? cast_control.forced_antags_count : cast_control.get_antag_count_to_spawn(forced) + if(!antag_count) + return + + antag_flag = cast_control.antag_flag + antag_datum = cast_control.antag_datum + restricted_roles = cast_control.restricted_roles + prompted_picking = cast_control.prompted_picking + var/list/candidates = cast_control.get_candidates() + + //guh + var/list/cliented_list = list() + for(var/mob/living/mob as anything in candidates) + cliented_list += mob.client + + if(prompted_picking) + candidates = SSpolling.poll_ghost_candidates(check_jobban = antag_flag, role = antag_flag, alert_pic = /obj/structure/sign/poster/contraband/syndicate_recruitment, role_name_text = lowertext(cast_control.name)) + + var/list/weighted_candidates = return_antag_weight(candidates) + var/spawned_count = 0 + var/failed_retries = 0 + while(length(weighted_candidates) && spawned_count < antag_count && failed_retries < STORYTELLER_MAXIMUM_RETRIES) + var/candidate_ckey = pick_n_take_weighted(weighted_candidates) + var/client/candidate_client = GLOB.directory[candidate_ckey] + if(QDELETED(candidate_client) || QDELETED(candidate_client.mob)) + failed_retries++ + continue + var/mob/candidate = candidate_client.mob + + spawned_count++ + if(spawned_count > SSgamemode.get_antag_cap(forced) || spawned_count > SSgamemode.left_antag_count_by_type(cast_control)) + break + + if(!candidate.mind) + candidate.mind = new /datum/mind(candidate.key) + var/mob/living/carbon/human/new_human = make_body(candidate) + new_human.mind.special_role = antag_flag + new_human.mind.restricted_roles = restricted_roles + setup_minds += new_human.mind + + setup = TRUE diff --git a/modular_bandastation/storyteller/code/event_defines/ghostset/override.dm b/modular_bandastation/storyteller/code/event_defines/ghostset/override.dm new file mode 100644 index 0000000000000..0ea28c8207aca --- /dev/null +++ b/modular_bandastation/storyteller/code/event_defines/ghostset/override.dm @@ -0,0 +1,14 @@ +/datum/round_event_control/nightmare + track = EVENT_TRACK_ROLESET + tags = list(TAG_COMBAT, TAG_SPOOKY) + weight = 4 + +/datum/round_event_control/space_dragon + track = EVENT_TRACK_ROLESET + tags = list(TAG_COMBAT, TAG_SPACE, TAG_EXTERNAL, TAG_ALIEN, TAG_MAGICAL) + checks_antag_cap = TRUE + +/datum/round_event_control/space_ninja + track = EVENT_TRACK_ROLESET + tags = list(TAG_COMBAT) + checks_antag_cap = TRUE diff --git a/modular_bandastation/storyteller/code/event_defines/ghostset/paradox_clone.dm b/modular_bandastation/storyteller/code/event_defines/ghostset/paradox_clone.dm new file mode 100644 index 0000000000000..2d36893eb294c --- /dev/null +++ b/modular_bandastation/storyteller/code/event_defines/ghostset/paradox_clone.dm @@ -0,0 +1,113 @@ +/datum/round_event_control/antagonist/solo/from_ghosts/paradox_clone + name = "Paradox Clone" + tags = list(TAG_OUTSIDER_ANTAG, TAG_SPOOKY, TAG_TARGETED) + typepath = /datum/round_event/antagonist/solo/ghost/paradox_clone + antag_flag = ROLE_PARADOX_CLONE + track = EVENT_TRACK_MODERATE + antag_datum = /datum/antagonist/paradox_clone + enemy_roles = list( + JOB_CAPTAIN, + JOB_DETECTIVE, + JOB_HEAD_OF_SECURITY, + JOB_SECURITY_OFFICER, + ) + maximum_antags = 1 + required_enemies = 2 + weight = 6 + max_occurrences = 2 + prompted_picking = TRUE + +/datum/round_event/antagonist/solo/ghost/paradox_clone + var/list/possible_spawns = list() ///places the antag can spawn + var/mob/living/carbon/human/clone_victim + var/mob/living/carbon/human/new_human + +/datum/round_event/antagonist/solo/ghost/paradox_clone/setup() + possible_spawns += find_maintenance_spawn(atmos_sensitive = TRUE, require_darkness = FALSE) + if(!possible_spawns.len) + return + + var/datum/round_event_control/antagonist/solo/cast_control = control + antag_count = forced && cast_control.forced_antags_count > 0 ? cast_control.forced_antags_count : cast_control.get_antag_count_to_spawn(forced) + if(!antag_count) + return + + antag_flag = cast_control.antag_flag + antag_datum = cast_control.antag_datum + restricted_roles = cast_control.restricted_roles + prompted_picking = cast_control.prompted_picking + var/list/candidates = cast_control.get_candidates() + + var/list/cliented_list = list() + for(var/mob/living/mob as anything in candidates) + cliented_list += mob.client + + if(prompted_picking) + candidates = SSpolling.poll_ghost_candidates( + "Would you like to be a paradox clone?", + check_jobban = ROLE_PARADOX_CLONE, + poll_time = 20 SECONDS, + alert_pic = /mob/living/carbon/human, + role_name_text = "paradox clone", + chat_text_border_icon = /datum/antagonist/paradox_clone, + ) + + var/list/weighted_candidates = return_antag_weight(candidates) + var/spawned_count = 0 + var/failed_retries = 0 + while(length(weighted_candidates) && spawned_count < antag_count && failed_retries < STORYTELLER_MAXIMUM_RETRIES) + var/client/candidate_ckey = pick_n_take_weighted(weighted_candidates) + var/client/candidate_client = GLOB.directory[candidate_ckey] + if(QDELETED(candidate_client) || QDELETED(candidate_client.mob)) + failed_retries++ + continue + + spawned_count++ + if(spawned_count > SSgamemode.get_antag_cap(forced) || spawned_count > SSgamemode.left_antag_count_by_type(cast_control)) + break + + var/mob/candidate = candidate_client.mob + if(!candidate.mind) + candidate.mind = new /datum/mind(candidate.key) + + clone_victim = find_original() + new_human = duplicate_object(clone_victim, pick(possible_spawns)) + if(!candidate_ckey) + continue + new_human.ckey = candidate_ckey + new_human.mind.special_role = antag_flag + new_human.mind.restricted_roles = restricted_roles + setup_minds += new_human.mind + + + setup = TRUE + + +/datum/round_event/antagonist/solo/ghost/paradox_clone/add_datum_to_mind(datum/mind/antag_mind) + var/datum/antagonist/paradox_clone/new_datum = antag_mind.add_antag_datum(/datum/antagonist/paradox_clone) + new_datum.original_ref = WEAKREF(clone_victim.mind) + new_datum.setup_clone() + new /obj/item/storage/toolbox/mechanical(new_human.loc) //so they dont get stuck in maints + + message_admins("[ADMIN_LOOKUPFLW(new_human)] has been made into a Paradox Clone by the midround ruleset.") + new_human.log_message("was spawned as a Paradox Clone of [key_name(new_human)] by the midround ruleset.", LOG_GAME) + GLOB.pre_setup_antags -= antag_mind + + +/** + * Trims through GLOB.player_list and finds a target + * Returns a single human victim, if none is possible then returns null. + */ +/datum/round_event/antagonist/solo/ghost/paradox_clone/proc/find_original() + var/list/possible_targets = list() + + for(var/mob/living/carbon/human/player in GLOB.player_list) + if(!player.client || !player.mind || player.stat) + continue + if(!(player.mind.assigned_role.job_flags & JOB_CREW_MEMBER)) + continue + possible_targets += player + + if(possible_targets.len) + return pick(possible_targets) + return FALSE diff --git a/modular_bandastation/storyteller/code/event_defines/ghostset/voidwalker.dm b/modular_bandastation/storyteller/code/event_defines/ghostset/voidwalker.dm new file mode 100644 index 0000000000000..2135da3a58f7f --- /dev/null +++ b/modular_bandastation/storyteller/code/event_defines/ghostset/voidwalker.dm @@ -0,0 +1,43 @@ +// TG did not cook this antag into the event system. So I had to make my own + + +/datum/round_event_control/voidwalker + name = "Spawn Void Walker" + typepath = /datum/round_event/ghost_role/void_walker + max_occurrences = 1 + weight = 3 + earliest_start = 20 MINUTES + min_players = 30 + dynamic_should_hijack = TRUE + category = EVENT_CATEGORY_ENTITIES + description = "A Void Walker that drags people out of the station and into the abyss" + map_flags = EVENT_SPACE_ONLY + track = EVENT_TRACK_MODERATE + tags = list(TAG_COMBAT, TAG_SPOOKY, TAG_SPACE) + +/datum/round_event/ghost_role/void_walker + minimum_required = 30 + fakeable = FALSE + role_name = "Void Walker" + +/datum/round_event/ghost_role/void_walker/spawn_role() + var/spawn_location = find_space_spawn() + if(isnull(spawn_location)) + return MAP_ERROR + + var/mob/chosen_one = SSpolling.poll_ghost_candidates(check_jobban = ROLE_VOIDWALKER, role = ROLE_VOIDWALKER, alert_pic = /obj/item/void_eater, jump_target = spawn_location, role_name_text = "Void Walker", amount_to_pick = 1) + if(isnull(chosen_one)) + return NOT_ENOUGH_PLAYERS + var/datum/mind/player_mind = new /datum/mind(chosen_one.key) + player_mind.active = TRUE + + var/mob/living/carbon/human/walker = new (spawn_location) + player_mind.transfer_to(walker) + player_mind.set_assigned_role(SSjob.get_job_type(/datum/job/voidwalker)) + player_mind.add_antag_datum(/datum/antagonist/voidwalker) + walker.set_species(/datum/species/voidwalker) + playsound(walker, 'sound/effects/magic/ethereal_exit.ogg', 50, TRUE, -1) + message_admins("[ADMIN_LOOKUPFLW(walker)] has been made into a Voidwalker by the midround event.") + walker.log_message("[key_name(walker)] was spawned as a Voidwalker by an event.", LOG_GAME) + spawned_mobs += walker + return SUCCESSFUL_SPAWN diff --git a/modular_bandastation/storyteller/code/event_defines/ghostset/wizard.dm b/modular_bandastation/storyteller/code/event_defines/ghostset/wizard.dm new file mode 100644 index 0000000000000..f5f4dcfc32357 --- /dev/null +++ b/modular_bandastation/storyteller/code/event_defines/ghostset/wizard.dm @@ -0,0 +1,38 @@ +/datum/round_event_control/antagonist/solo/from_ghosts/wizard + name = "Ghost Wizard" + tags = list(TAG_COMBAT, TAG_DESTRUCTIVE, TAG_EXTERNAL, TAG_MAGICAL) + typepath = /datum/round_event/antagonist/solo/ghost/wizard + antag_flag = ROLE_WIZARD + antag_datum = /datum/antagonist/wizard + restricted_roles = list( + JOB_CAPTAIN, + JOB_HEAD_OF_SECURITY, + ) // Just to be sure that a wizard getting picked won't ever imply a Captain or HoS not getting drafted + maximum_antags = 1 + enemy_roles = list( + JOB_AI, + JOB_CAPTAIN, + JOB_DETECTIVE, + JOB_HEAD_OF_SECURITY, + JOB_SECURITY_OFFICER, + JOB_WARDEN, + ) + required_enemies = 5 + weight = 2 + min_players = 35 + max_occurrences = 1 + prompted_picking = FALSE + +/datum/round_event_control/antagonist/solo/ghost/wizard/can_spawn_event(players_amt, allow_magic = FALSE, fake_check = FALSE) + . = ..() + if(!.) + return + if(GLOB.wizardstart.len == 0) + return FALSE + +/datum/round_event/antagonist/solo/ghost/wizard + +/datum/round_event/antagonist/solo/ghost/wizard/add_datum_to_mind(datum/mind/antag_mind) + . = ..() + antag_mind.current.forceMove(pick(GLOB.wizardstart)) + GLOB.pre_setup_antags -= antag_mind diff --git a/modular_bandastation/storyteller/code/event_defines/major/override.dm b/modular_bandastation/storyteller/code/event_defines/major/override.dm new file mode 100644 index 0000000000000..b84845bad652b --- /dev/null +++ b/modular_bandastation/storyteller/code/event_defines/major/override.dm @@ -0,0 +1,43 @@ +/datum/round_event_control/earthquake + track = EVENT_TRACK_MAJOR + tags = list(TAG_DESTRUCTIVE) + eng_required_power = 2 + +/datum/round_event_control/meteor_wave + track = EVENT_TRACK_MAJOR + tags = list(TAG_COMMUNAL, TAG_SPACE, TAG_DESTRUCTIVE) + event_group = /datum/event_group/meteors + eng_required_power = 3 + +/datum/round_event_control/revenant + min_players = 20 + track = EVENT_TRACK_MAJOR + tags = list(TAG_DESTRUCTIVE, TAG_SPOOKY) + +/datum/round_event_control/operative + track = EVENT_TRACK_MAJOR //this is a safe guard and does not trigger normally(technically it can but not really) so no tags + checks_antag_cap = TRUE + +/datum/round_event_control/wizard/round_start + track = EVENT_TRACK_MAJOR + weight = 5 + tags = list(TAG_COMMUNAL, TAG_DESTRUCTIVE) + roundstart = TRUE + +/datum/round_event_control/slaughter + track = EVENT_TRACK_MAJOR + tags = list(TAG_COMBAT, TAG_SPOOKY, TAG_EXTERNAL, TAG_MAGICAL) + checks_antag_cap = TRUE + +/datum/round_event_control/portal_storm_syndicate + track = EVENT_TRACK_MAJOR + tags = list(TAG_COMBAT) + event_group = /datum/event_group/guests + +/datum/round_event_control/morph + track = EVENT_TRACK_MAJOR + tags = list(TAG_COMBAT, TAG_SPOOKY, TAG_EXTERNAL, TAG_ALIEN) + checks_antag_cap = TRUE + +/datum/round_event_control/portal_storm_monkey + track = EVENT_TRACK_MAJOR diff --git a/modular_bandastation/storyteller/code/event_defines/moderate/override.dm b/modular_bandastation/storyteller/code/event_defines/moderate/override.dm new file mode 100644 index 0000000000000..900612ad705dd --- /dev/null +++ b/modular_bandastation/storyteller/code/event_defines/moderate/override.dm @@ -0,0 +1,157 @@ +/datum/round_event_control/brand_intelligence + tags = list(TAG_DESTRUCTIVE, TAG_COMMUNAL) + event_group = /datum/event_group/bsod + head_required_power = 1 + +/datum/round_event_control/carp_migration + tags = list(TAG_DESTRUCTIVE, TAG_COMBAT, TAG_SPACE, TAG_EXTERNAL, TAG_ALIEN) + max_occurrences = 3 + event_group = /datum/event_group/guests + +/datum/round_event_control/communications_blackout + tags = list(TAG_COMMUNAL, TAG_SPOOKY) + eng_required_power = 1 + +/datum/round_event_control/fugitives + track = EVENT_TRACK_MODERATE + tags = list(TAG_COMBAT) + checks_antag_cap = TRUE + +/datum/round_event_control/pirates + track = EVENT_TRACK_MODERATE + tags = list(TAG_COMBAT, TAG_COMMUNAL) + checks_antag_cap = TRUE + +/datum/round_event_control/ion_storm + tags = list(TAG_TARGETED, TAG_ALIEN) + event_group = /datum/event_group/bsod + +/datum/round_event_control/processor_overload + max_occurrences = 2 + tags = list(TAG_COMMUNAL) + event_group = /datum/event_group/comms + +/datum/round_event_control/radiation_leak + tags = list(TAG_COMMUNAL) + max_occurrences = 2 + eng_required_power = 2 + med_required_power = 1 + +/datum/round_event_control/radiation_storm + weight = 5 + max_occurrences = 1 + tags = list(TAG_COMMUNAL) + +/datum/round_event_control/supermatter_surge + tags = list(TAG_DESTRUCTIVE, TAG_COMMUNAL) + event_group = /datum/event_group/error + eng_required_power = 2 + +/datum/round_event_control/stray_meteor + tags = list(TAG_DESTRUCTIVE, TAG_SPACE) + event_group = /datum/event_group/debris + eng_required_power = 2 + +/datum/round_event_control/shuttle_catastrophe + tags = list(TAG_COMMUNAL) + head_required_power = 1 + +/datum/round_event_control/shuttle_insurance + tags = list(TAG_COMMUNAL) + +/datum/round_event_control/vent_clog + tags = list(TAG_COMMUNAL) + +/datum/round_event_control/anomaly + weight = 10 // Lower from original 15 because it KEEPS SPAWNING THEM + tags = list(TAG_DESTRUCTIVE, TAG_MAGICAL) + event_group = /datum/event_group/anomalies + rnd_required_power = 1 + +/datum/round_event_control/portal_storm_narsie + tags = list(TAG_COMBAT) + +/datum/round_event_control/obsessed + weight = 0 // use storyteller variants instead + +/datum/round_event_control/gravity_generator_blackout + tags = list(TAG_COMMUNAL, TAG_SPACE) + event_group = /datum/event_group/bsod + weight = 8 + max_occurrences = 2 + eng_required_power = 1 + +/datum/round_event_control/grey_tide + tags = list(TAG_DESTRUCTIVE, TAG_SPOOKY) + event_group = /datum/event_group/meteors + +/datum/round_event_control/heart_attack + tags = list(TAG_TARGETED, TAG_MAGICAL) + med_required_power = 1 + +/datum/round_event_control/sandstorm + tags = list(TAG_DESTRUCTIVE, TAG_EXTERNAL) + event_group = /datum/event_group/debris + +/datum/round_event_control/wormholes + tags = list(TAG_COMMUNAL, TAG_MAGICAL) + event_group = /datum/event_group/anomalies + +/datum/round_event_control/immovable_rod + tags = list(TAG_DESTRUCTIVE, TAG_EXTERNAL, TAG_MAGICAL) + eng_required_power = 2 + +/datum/round_event_control/changeling + track = EVENT_TRACK_MODERATE + tags = list(TAG_COMBAT, TAG_SPACE, TAG_EXTERNAL, TAG_ALIEN) + event_group = /datum/event_group/comms + checks_antag_cap = TRUE + +/datum/round_event_control/nightmare + track = EVENT_TRACK_MODERATE + tags = list(TAG_COMBAT, TAG_SPOOKY, TAG_EXTERNAL, TAG_ALIEN) + checks_antag_cap = TRUE + +/datum/round_event_control/revenant + track = EVENT_TRACK_MODERATE + tags = list(TAG_DESTRUCTIVE, TAG_SPOOKY, TAG_EXTERNAL, TAG_MAGICAL) + checks_antag_cap = TRUE + +/datum/round_event_control/anomaly/anomaly_vortex + track = EVENT_TRACK_MODERATE + tags = list(TAG_DESTRUCTIVE) + rnd_required_power = 3 + weight = 5 + +/datum/round_event_control/anomaly/anomaly_pyro + track = EVENT_TRACK_MODERATE + tags = list(TAG_DESTRUCTIVE) + rnd_required_power = 3 + weight = 5 + +/datum/round_event_control/spacevine + track = EVENT_TRACK_MODERATE + tags = list(TAG_COMBAT, TAG_DESTRUCTIVE, TAG_ALIEN) + checks_antag_cap = TRUE + event_group = /datum/event_group/guests + med_required_power = 2 + weight = 5 + +/datum/round_event_control/abductor + track = EVENT_TRACK_MODERATE + tags = list(TAG_TARGETED, TAG_SPOOKY, TAG_EXTERNAL, TAG_ALIEN) + checks_antag_cap = TRUE + +/datum/round_event_control/meteor_wave/dust_storm + track = EVENT_TRACK_MODERATE + tags = list(TAG_COMMUNAL, TAG_SPACE, TAG_DESTRUCTIVE) + event_group = /datum/event_group/meteors + eng_required_power = 3 + +/datum/round_event_control/bureaucratic_error + track = EVENT_TRACK_MODERATE // if you've ever dealt with 10 mimes you understand why. + tags = list(TAG_COMMUNAL) + event_group = /datum/event_group/error + weight = 5 + max_occurrences = 1 + head_required_power = 3 diff --git a/modular_bandastation/storyteller/code/event_defines/mundane/override.dm b/modular_bandastation/storyteller/code/event_defines/mundane/override.dm new file mode 100644 index 0000000000000..bd6871e6d17cf --- /dev/null +++ b/modular_bandastation/storyteller/code/event_defines/mundane/override.dm @@ -0,0 +1,135 @@ +/datum/round_event_control/space_dust + track = EVENT_TRACK_MUNDANE + weight = 10 + max_occurrences = 10 + tags = list(TAG_DESTRUCTIVE, TAG_SPACE) + +/datum/round_event_control/camera_failure + track = EVENT_TRACK_MUNDANE + weight = 10 + tags = list(TAG_COMMUNAL, TAG_SPOOKY) + eng_required_power = 1 + +/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, TAG_MAGICAL) //im putting magical on this because I think this can give the magic brain traumas + med_required_power = 1 + +/datum/round_event_control/grid_check + track = EVENT_TRACK_MODERATE + tags = list(TAG_COMMUNAL, TAG_SPOOKY) + +/datum/round_event_control/disease_outbreak + max_occurrences = 2 + track = EVENT_TRACK_MUNDANE + tags = list(TAG_TARGETED, TAG_COMMUNAL, TAG_EXTERNAL, TAG_ALIEN, TAG_MAGICAL) + med_required_power = 1 + +/datum/round_event_control/electrical_storm + track = EVENT_TRACK_MUNDANE + tags = list(TAG_SPOOKY) + event_group = /datum/event_group/error + eng_required_power = 1 + +/datum/round_event_control/fake_virus + track = EVENT_TRACK_MUNDANE + tags = list(TAG_TARGETED) + weight = 0 + +/datum/round_event_control/falsealarm + track = EVENT_TRACK_MUNDANE + tags = list(TAG_COMMUNAL) + event_group = /datum/event_group/error + +/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, TAG_ALIEN) //not really alien but rat lords kind of are + event_group = /datum/event_group/guests + +/datum/round_event_control/wisdomcow + track = EVENT_TRACK_MUNDANE + tags = list(TAG_COMMUNAL, TAG_POSITIVE, TAG_MAGICAL) + event_group = /datum/event_group/guests + weight = 0 + +/datum/round_event_control/shuttle_loan + track = EVENT_TRACK_MUNDANE + tags = list(TAG_COMMUNAL) + +/datum/round_event_control/stray_cargo + track = EVENT_TRACK_MUNDANE + tags = list(TAG_COMMUNAL) + eng_required_power = 1 + +/datum/round_event_control/tram_malfunction + track = EVENT_TRACK_MUNDANE + tags = list(TAG_COMMUNAL) + event_group = /datum/event_group/error + eng_required_power = 1 + +/datum/round_event_control/bitrunning_glitch + track = EVENT_TRACK_MUNDANE + tags = list(TAG_TARGETED) + +/datum/round_event_control/easter + track = EVENT_TRACK_MUNDANE + roundstart = TRUE + weight = 0 + max_occurrences = 0 + tags = list(TAG_COMMUNAL, TAG_POSITIVE) + +/datum/round_event_control/rabbitrelease + track = EVENT_TRACK_MUNDANE + tags = list(TAG_COMMUNAL, TAG_POSITIVE) + +/datum/round_event_control/valentines + track = EVENT_TRACK_MUNDANE + roundstart = TRUE + weight = 0 + max_occurrences = 0 + tags = list(TAG_COMMUNAL, TAG_POSITIVE) + +/datum/round_event_control/santa + track = EVENT_TRACK_MUNDANE + tags = list(TAG_COMMUNAL, TAG_POSITIVE) + +/datum/round_event_control/spooky + track = EVENT_TRACK_MUNDANE + roundstart = TRUE + weight = 0 + max_occurrences = 0 + tags = list(TAG_COMMUNAL, TAG_POSITIVE, TAG_SPOOKY) + +/datum/round_event_control/mass_hallucination + track = EVENT_TRACK_MUNDANE + tags = list(TAG_COMMUNAL, TAG_MAGICAL) + +/datum/round_event_control/sentience + track = EVENT_TRACK_MUNDANE + tags = list(TAG_COMMUNAL, TAG_SPOOKY, TAG_MAGICAL) + +//SCRUBBER OVERRIDES +/datum/round_event_control/scrubber_overflow + tags = list(TAG_COMMUNAL) + event_group = /datum/event_group/scrubber_overflow + +/datum/round_event_control/scrubber_overflow/threatening + weight = 0 + med_required_power = 1 + +/datum/round_event_control/scrubber_overflow/catastrophic + weight = 0 + med_required_power = 1 + eng_required_power = 1 + +/datum/round_event_control/scrubber_overflow/every_vent + weight = 0 + head_required_power = 1 diff --git a/modular_bandastation/storyteller/code/event_defines/override_events.dm b/modular_bandastation/storyteller/code/event_defines/override_events.dm new file mode 100644 index 0000000000000..451e96d5ac9d4 --- /dev/null +++ b/modular_bandastation/storyteller/code/event_defines/override_events.dm @@ -0,0 +1,43 @@ +/datum/round_event/scrubber_overflow + /// Whitelist of reagents we want scrubbers to dispense + safer_chems = list(/datum/reagent/baldium, + /datum/reagent/bluespace, + /datum/reagent/carbon, + /datum/reagent/colorful_reagent, + /datum/reagent/concentrated_barbers_aid, + /datum/reagent/consumable/astrotame, + /datum/reagent/consumable/char, + /datum/reagent/consumable/condensedcapsaicin, + /datum/reagent/consumable/cream, + /datum/reagent/consumable/ethanol/antifreeze, + /datum/reagent/consumable/ethanol/beer, + /datum/reagent/consumable/ethanol/fernet_cola, + /datum/reagent/consumable/ethanol/sugar_rush, + /datum/reagent/consumable/flour, + /datum/reagent/consumable/ice, + /datum/reagent/consumable/laughter, + /datum/reagent/consumable/sugar, + /datum/reagent/consumable/tinlux, + /datum/reagent/cryptobiolin, + /datum/reagent/drug/mushroomhallucinogen, + /datum/reagent/drug/space_drugs, + /datum/reagent/fuel, + /datum/reagent/glitter/blue, + /datum/reagent/glitter/confetti, + /datum/reagent/glitter/pink, + /datum/reagent/glitter/white, + /datum/reagent/gravitum, + /datum/reagent/growthserum, + /datum/reagent/hair_dye, + /datum/reagent/hydrogen_peroxide, + /datum/reagent/lube, + /datum/reagent/lube/superlube, + /datum/reagent/medicine/c2/multiver, + /datum/reagent/metalgen, + /datum/reagent/pax, + /datum/reagent/plastic_polymers, + /datum/reagent/space_cleaner, + /datum/reagent/spraytan, + /datum/reagent/water/salt, + /datum/reagent/yuck, + ) diff --git a/modular_bandastation/storyteller/code/events/group/_event_group.dm b/modular_bandastation/storyteller/code/events/group/_event_group.dm new file mode 100644 index 0000000000000..7872629f42781 --- /dev/null +++ b/modular_bandastation/storyteller/code/events/group/_event_group.dm @@ -0,0 +1,66 @@ +/// An associative list of singleton event groups, in the format of [type] = instance. +GLOBAL_LIST_INIT_TYPED(event_groups, /datum/event_group, initialize_event_groups()) + +/datum/event_group + /// The name of the event group. + var/name + /// If set, this will limit the amount of times events in this group can run. + var/max_occurrences + /// If set, whenever an event in this group runs, no other events in this group + /// will be able to run for the specified amount of time. + /// Can be either a number for a static cooldown, or a list containing 2 numbers, + /// for a random cooldown between the given numbers, which will bias towards the upper bound + /// the more the event group has occured. + var/cooldown_time + /// The amount of time events in this group have ran. + VAR_FINAL/occurrences = 0 + /// If [cooldown_time] is set, this will be set to the minimum world.time where the next event in this group can run. + COOLDOWN_DECLARE(event_cooldown) + +/datum/event_group/Destroy(force) + if(!force && GLOB.event_groups[type] == src) + stack_trace("Something is trying to destroy the event group ([type]), which is a singleton! This is super duper bad!") + return QDEL_HINT_LETMELIVE + return ..() + +/datum/event_group/proc/get_cooldown_time() as num + if(isnum(cooldown_time)) + return cooldown_time + var/min_cooldown = cooldown_time[1] + var/max_cooldown = cooldown_time[2] + + if (max_occurrences) + var/occurrence_ratio = min(1, occurrences / max_occurrences) + return min_cooldown + (max_cooldown - min_cooldown) * occurrence_ratio + else + // If max_occurrences is not set, use a simple exponential increase + return min_cooldown + (max_cooldown - min_cooldown) * (occurrences / (occurrences + 1)) + +/datum/event_group/proc/can_run() as num + . = TRUE + if(cooldown_time && !COOLDOWN_FINISHED(src, event_cooldown)) + return FALSE + if(max_occurrences && occurrences >= max_occurrences) + return FALSE + +/datum/event_group/proc/on_run(datum/round_event_control/running_event) + if(cooldown_time) + var/cooldown = get_cooldown_time() + COOLDOWN_START(src, event_cooldown, cooldown) + for(var/datum/scheduled_event/scheduled_event in SSgamemode.scheduled_events) + if(scheduled_event.event == running_event || scheduled_event.event?.event_group != type || !scheduled_event.start_time || (scheduled_event.start_time > src.event_cooldown) || (scheduled_event.start_time <= world.time)) + continue + var/old_start_time = scheduled_event.start_time + scheduled_event.start_time += cooldown + message_admins("Scheduled event [scheduled_event.event.name] start time pushed back by [DisplayTimeText(cooldown)] ([DisplayTimeText(COOLDOWN_TIMELEFT(scheduled_event, start_time))] from now) due to event group [name] running.") + log_storyteller("Scheduled event [scheduled_event.event.name] start time pushed back by [DisplayTimeText(cooldown)] ([old_start_time] -> [scheduled_event.start_time]) due to event group [name] running.", list("group" = "[name]", "cooldown" = cooldown)) + occurrences++ + SSblackbox.record_feedback("tally", "event_group_ran", 1, "[name]") + +/proc/initialize_event_groups() as /list + RETURN_TYPE(/list) + . = list() + for(var/datum/event_group/event_group as anything in subtypesof(/datum/event_group)) + if(!event_group::name) + continue + .[event_group] = new event_group diff --git a/modular_bandastation/storyteller/code/events/group/groups.dm b/modular_bandastation/storyteller/code/events/group/groups.dm new file mode 100644 index 0000000000000..06d8abba3d1d5 --- /dev/null +++ b/modular_bandastation/storyteller/code/events/group/groups.dm @@ -0,0 +1,49 @@ +/datum/event_group/anomalies + name = "Anomalies" + cooldown_time = list(10 MINUTES, 25 MINUTES) + +/datum/event_group/comms + name = "Communications" + cooldown_time = list(15 MINUTES, 45 MINUTES) + max_occurrences = 3 + +/// Represents small-scale technical difficulties - might annoy some people, but not everyone will notice or be affected, +/// If the event directly affects the entire crew, use [/datum/event_group/bsod]. +/// If the main effect involves telecommunications, use [/datum/event_group/comms]. +/datum/event_group/error + name = "Technical Difficulties (small scale)" + cooldown_time = list(2.5 MINUTES, 7.5 MINUTES) + +/// Represents large-scale technical difficulties - stuff that affects the whole crew. +/// If the main effect involves telecommunications, use [/datum/event_group/comms]. +/datum/event_group/bsod + name = "Technical Difficulties (large scale)" + cooldown_time = list(15 MINUTES, 25 MINUTES) + max_occurrences = 5 + +/datum/event_group/debris + name = "Space Debris" + cooldown_time = list(2 MINUTES, 10 MINUTES) + +/datum/event_group/meteors + name = "Meteors" + cooldown_time = 20 MINUTES + max_occurrences = 3 + +// needs a better name - this is basically for events focused around spawning some sort of NPCs, +// i.e vines, wisdom cow, carp, etc. +/datum/event_group/guests + name = "Guests" + cooldown_time = list(7.5 MINUTES, 20 MINUTES) + +// These event groups are somewhat specific to a single event, +// but we're using event groups to share cooldowns/occurrences between SSevents and storytellers +/datum/event_group/scrubber_overflow + name = "Scrubber Overflows" + cooldown_time = list(25 MINUTES, 45 MINUTES) + max_occurrences = 2 + +/datum/event_group/blobs + name = "Blobs" + max_occurrences = 1 + cooldown_time = list(7.5 MINUTES, 20 MINUTES) diff --git a/modular_bandastation/storyteller/code/events/object/anomalies_dimensional.dm b/modular_bandastation/storyteller/code/events/object/anomalies_dimensional.dm new file mode 100644 index 0000000000000..01cb702613c19 --- /dev/null +++ b/modular_bandastation/storyteller/code/events/object/anomalies_dimensional.dm @@ -0,0 +1,30 @@ +/obj/effect/anomaly/dimensional + /// How many remaining times this anomaly will relocate, before its detonation. + var/relocations_left + +/obj/effect/anomaly/dimensional/Initialize(mapload, new_lifespan, drops_core) + . = ..() + if(!isnum(relocations_left)) + relocations_left = rand(3, 5) + +/obj/effect/anomaly/dimensional/detonate() + . = ..() + if(!theme) + return + visible_message(span_bolddanger("[src] explodes, distorting the space around it in surreal ways!")) + var/detonate_range = range + rand(5, 7) + var/list/turf/target_turfs = spiral_range_turfs(detonate_range, src) + for(var/turf/target in target_turfs) + if(prob(15) || QDELING(target)) // the prob is so it looks more erratic + continue + theme.apply_theme(target) + +/obj/effect/anomaly/dimensional/relocate() + if(relocations_left == -1) + return ..() + if(relocations_left < 1) + detonate() + qdel(src) + return + . = ..() + relocations_left -= 1 diff --git a/modular_bandastation/storyteller/code/events/summon_wizard.dm b/modular_bandastation/storyteller/code/events/summon_wizard.dm new file mode 100644 index 0000000000000..23686f45a312c --- /dev/null +++ b/modular_bandastation/storyteller/code/events/summon_wizard.dm @@ -0,0 +1,31 @@ +/datum/round_event_control/summon_wizard_event + name = "Summon Wizard Event" + typepath = /datum/round_event/summon_wizard_event + weight = 0 + category = EVENT_CATEGORY_WIZARD + description = "Trigger a random wizard event that meets its normal conditions." + track = EVENT_TRACK_MAJOR + tags = list(TAG_SPOOKY, TAG_MAGICAL) + allowed_storytellers = /datum/storyteller/mystic + +/datum/round_event/summon_wizard_event + ///the event we have actually chosen to run + var/datum/round_event_control/triggered_event + +/datum/round_event/summon_wizard_event/setup() + var/list/possible_events = list() + var/player_count = get_active_player_count(alive_check = TRUE, afk_check = TRUE, human_check = TRUE) + for(var/datum/round_event_control/possible_event as anything in SSevents.control) + if(!possible_event.wizardevent || !possible_event.can_spawn_event(player_count, allow_magic = TRUE)) + continue + possible_events[possible_event] = possible_event.weight + + if(!length(possible_events)) + kill() + return + + triggered_event = pick_weight(possible_events) + setup = TRUE + +/datum/round_event/summon_wizard_event/start() + triggered_event.run_event() diff --git a/modular_bandastation/storyteller/code/gamemode.dm b/modular_bandastation/storyteller/code/gamemode.dm new file mode 100644 index 0000000000000..86863b41b0593 --- /dev/null +++ b/modular_bandastation/storyteller/code/gamemode.dm @@ -0,0 +1,1536 @@ +#define INIT_ORDER_GAMEMODE 70 +///how many storytellers can be voted for along with always_votable ones +#define DEFAULT_STORYTELLER_VOTE_OPTIONS 4 +///amount of players we can have before no longer running votes for storyteller +#define MAX_POP_FOR_STORYTELLER_VOTE 25 +///the duration into the round for which roundstart events are still valid to run +#define ROUNDSTART_VALID_TIMEFRAME 3 MINUTES + +SUBSYSTEM_DEF(gamemode) + name = "Gamemode" + init_order = INIT_ORDER_GAMEMODE + runlevels = RUNLEVEL_GAME + flags = SS_BACKGROUND | SS_KEEP_TIMING + priority = 20 + wait = 2 SECONDS + + /// List of our event tracks for fast access during for loops. + var/list/event_tracks = EVENT_TRACKS + /// Our storyteller. They progresses our trackboards and picks out events + var/datum/storyteller/current_storyteller + /// Result of the storyteller vote/pick. Defaults to the guide. + var/selected_storyteller = /datum/storyteller/default + /// 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 + ) + /// 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( + EVENT_TRACK_MUNDANE = list(), + EVENT_TRACK_MODERATE = list(), + EVENT_TRACK_MAJOR = list(), + EVENT_TRACK_ROLESET = list(), + EVENT_TRACK_OBJECTIVES = list(), + ) + + var/list/control = list() //list of all datum/round_event_control. Used for selecting events based on weight and occurrences. + var/list/running = list() //list of all existing /datum/round_event + var/list/round_end_data = list() //list of all reports that need to add round end reports + + /// List of all uncategorized events, because they were wizard or holiday events + var/list/uncategorized = list() + + /// 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/full_sec_crew = 0 + var/sec_crew = 0 + var/head_crew = 0 + var/eng_crew = 0 + var/med_crew = 0 + var/rnd_crew = 0 + var/current_head_power = 0 + var/current_eng_power = 0 + var/current_med_power = 0 + var/current_rnd_power = 0 + + /// Is storyteller secret or not + var/secret_storyteller = FALSE + + /// List of new player minds we currently want to give our roundstart antag to + var/list/roundstart_antag_minds = list() + + var/wizardmode = FALSE //refactor this into just being a unique storyteller + + var/list/last_round_events = list() + /// Has a roundstart event been run + var/ran_roundstart = FALSE + /// Are we able to run roundstart events + var/can_run_roundstart = TRUE + var/list/triggered_round_events = list() + var/runned_events = list() + /// Список ролей перед их сбросом в начале раунда + var/list/presetuped_ocupations = list() + var/empty_event_chance = 0 + var/roundstart_budget = 0 + var/roundstart_budget_set = -1 + +/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/datum/round_event_control/event_type as anything in typesof(/datum/round_event_control)) + if(!event_type::typepath || !event_type::name) + continue + + var/datum/round_event_control/event = new event_type + if(!event.valid_for_map()) + qdel(event) + continue // event isn't good for this map no point in trying to add it to the list + control += event //add it to the list of all events (controls) + + load_config_vars() + load_event_config_vars() + + ///Seeding events into track event pools needs to happen after event config vars are loaded + for(var/datum/round_event_control/event as anything in control) + if(event.holidayID || event.wizardevent) + uncategorized += event + continue + event_pools[event.track] += event //Add it to the categorized event pools + + load_roundstart_data() + if(CONFIG_GET(flag/disable_storyteller)) // we're just gonna disable firing but still initialize, so we don't have any weird runtimes + flags |= SS_NO_FIRE + return SS_INIT_NO_NEED + + return SS_INIT_SUCCESS + +/datum/controller/subsystem/gamemode/fire(resumed = FALSE) + if(SSticker.round_start_time && (world.time - SSticker.round_start_time) >= ROUNDSTART_VALID_TIMEFRAME) + can_run_roundstart = FALSE + roundstart_event_view = FALSE + + ///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 && current_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 + current_storyteller.process(STORYTELLER_WAIT_TIME * 0.1) + +/// Gets the number of antagonists the antagonist injection events will stop rolling after. +/datum/controller/subsystem/gamemode/proc/get_antag_cap(forced = FALSE) + var/pop_count = get_correct_popcount() + if(pop_count < current_storyteller.min_antag_popcount && !forced) + return 0 + var/total_number = pop_count + (sec_crew * current_storyteller.sec_antag_modifier) + var/cap = FLOOR((total_number / current_storyteller.antag_denominator) * current_storyteller.roundstart_cap_multiplier, 1) + current_storyteller.antag_flat_cap + return cap + +/datum/controller/subsystem/gamemode/proc/get_antag_count() + var/count = 0 + var/list/already_counted = list() // Never count the same mind twice + if(SSticker.HasRoundStarted()) + for(var/datum/antagonist/antag as anything in GLOB.antagonists) + if(QDELETED(antag) || QDELETED(antag.owner) || already_counted[antag.owner]) + continue + if(!antag.count_against_dynamic_roll_chance || (antag.antag_flags & (FLAG_FAKE_ANTAG | FLAG_ANTAG_CAP_IGNORE))) + continue + if(antag.antag_flags & FLAG_ANTAG_CAP_TEAM) + var/datum/team/antag_team = antag.get_team() + if(antag_team) + if(already_counted[antag_team]) + continue + already_counted[antag_team] = TRUE + var/mob/antag_mob = antag.owner.current + if(QDELETED(antag_mob) || !antag_mob.key || antag_mob.stat == DEAD || antag_mob.client?.is_afk()) + continue + already_counted[antag.owner] = TRUE + count++ + else + for(var/mob/dead/new_player/player as anything in GLOB.new_player_list) + if(player.ready != PLAYER_READY_TO_PLAY) + continue + var/client/client_source = player.client + if(QDELETED(client_source) || !client_source.ckey) + continue + if(player.mind.picking) + if(!(player.mind in GLOB.pre_setup_antags)) + GLOB.pre_setup_antags += player.mind + count++ + return count + +/// Whether events can inject more antagonists into the round +/datum/controller/subsystem/gamemode/proc/can_inject_antags() + return (get_antag_cap() > get_antag_count()) + +/// Gets candidates for antagonist roles. +/datum/controller/subsystem/gamemode/proc/get_candidates(be_special, job_ban, observers, ready_newplayers, living_players, required_time, inherit_required_time = TRUE, midround_antag_pref, no_antags = TRUE, list/restricted_roles, list/required_roles) + var/list/candidates = list() + var/list/candidate_candidates = list() //lol + + for(var/mob/player as anything in GLOB.player_list) + if(QDELETED(player) || player.mind?.picking) + continue + if(ready_newplayers && isnewplayer(player)) + var/mob/dead/new_player/new_player = player + if(new_player.ready == PLAYER_READY_TO_PLAY && new_player.mind && new_player.check_preferences()) + candidate_candidates += player + else if(observers && isobserver(player)) + candidate_candidates += player + else if(living_players && isliving(player)) + if(!ishuman(player) && !isAI(player)) + continue + // I split these checks up to make the code more readable + var/is_on_station = is_station_level(player.z) + if(!is_on_station && !is_late_arrival(player)) + continue + candidate_candidates += player + + for(var/mob/candidate as anything in candidate_candidates) + if(QDELETED(candidate) || !candidate.key || !candidate.client || (!observers && !candidate.mind)) + continue + if(!observers) + if(!ready_players && !isliving(candidate)) + continue + if(no_antags && !isnull(candidate.mind.antag_datums)) + var/real = FALSE + for(var/datum/antagonist/antag_datum as anything in candidate.mind.antag_datums) + if(antag_datum.count_against_dynamic_roll_chance && !(antag_datum.antag_flags & FLAG_FAKE_ANTAG)) + real = TRUE + break + if(real) + continue + if(restricted_roles && (candidate.mind.assigned_role.title in restricted_roles)) + continue + if(length(required_roles) && !(candidate.mind.assigned_role.title in required_roles)) + continue + + if(be_special) + if(!(candidate.client.prefs) || !(be_special in candidate.client.prefs.be_special)) + continue + + var/time_to_check + if(required_time) + time_to_check = required_time + else if(inherit_required_time) + time_to_check = GLOB.special_roles[be_special] + + if(time_to_check && candidate.client.get_remaining_days(time_to_check) > 0) + continue + + if(job_ban && is_banned_from(candidate.ckey, list(job_ban, ROLE_SYNDICATE))) + continue + candidates += candidate + + return candidates + +/// Gets the correct popcount, returning READY people if roundstart, and active people if not. +/datum/controller/subsystem/gamemode/proc/get_correct_popcount() + if(SSticker.HasRoundStarted()) + update_crew_infos() + return active_players + else + recalculate_ready_pop() + return ready_players + +/// Refunds and removes a scheduled event. +/datum/controller/subsystem/gamemode/proc/refund_scheduled_event(datum/scheduled_event/refunded) + if(refunded.cost) + var/track_type = refunded.event.track + event_track_points[track_type] += refunded.cost + remove_scheduled_event(refunded) + +/// Removes a scheduled event. +/datum/controller/subsystem/gamemode/proc/remove_scheduled_event(datum/scheduled_event/removed) + scheduled_events -= removed + qdel(removed) + +/// We roll points to be spent for roundstart events, including antagonists. +/datum/controller/subsystem/gamemode/proc/roll_pre_setup_points() + if(current_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 *= current_storyteller.roundstart_point_multipliers[track] + calc_value *= current_storyteller.starting_point_multipliers[track] + calc_value *= (rand(100 - current_storyteller.roundstart_points_variance,100 + current_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(current_storyteller.guarantees_roundstart_roleset && event_track_points[EVENT_TRACK_ROLESET] < point_thresholds[EVENT_TRACK_ROLESET] && current_storyteller.min_antag_popcount <= get_correct_popcount()) + 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(length(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 TESTING HOW THINGS GO WITHOUT THIS HALVING OF POINTS + return + +/// 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(current_storyteller.disable_distribution) + return + if(halted_storyteller) + message_admins("WARNING: Didn't roll roundstart events (including antagonists) due to the storyteller being halted.") + return + handle_pre_setup_occupations() + SSjob.reset_occupations() + while(TRUE) + if(!current_storyteller.handle_tracks()) + break + +/// Прок который сохраняет список ролей перед их сбросом. Важно, так позволяет более тонко настраивать количество СБ/Антагов +/datum/controller/subsystem/gamemode/proc/handle_pre_setup_occupations() + for(var/mob/dead/new_player/player as anything in GLOB.new_player_list) + if(!player?.mind) + continue + var/datum/job/job_datum = get_preferenced_job(player) + var/client/client_source = player.client + presetuped_ocupations[client_source.ckey] = job_datum + +/// Прок, который пытается получить предпочтительную роль для игрока (и назначить в дальнейшем вес для нее) +/datum/controller/subsystem/gamemode/proc/get_preferenced_job(mob/dead/new_player/check_player) + var/list/available_occupations = SSjob.joinable_occupations + for(var/level in SSjob.level_order) + for(var/datum/job/job in available_occupations) + // Filter any job that doesn't fit the current level. + var/player_job_level = check_player.client?.prefs.job_preferences[job.title] + if(isnull(player_job_level)) + continue + if(player_job_level != level) + continue + return job + +/// 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, TYPE_PROC_REF(/datum/round_event, try_start)) + +/// Для восполнения очков ролсета +/datum/controller/subsystem/gamemode/proc/refill_roleset() + event_track_points[EVENT_TRACK_ROLESET] = point_thresholds[EVENT_TRACK_ROLESET] + +/// Schedules an event to run later. +/datum/controller/subsystem/gamemode/proc/schedule_event(datum/round_event_control/passed_event, passed_time, passed_cost, passed_ignore, passed_announce, _forced = FALSE) + if(_forced) + passed_ignore = TRUE + var/datum/scheduled_event/scheduled = new (passed_event, world.time + passed_time, passed_cost, passed_ignore, passed_announce) + var/round_started = SSticker.HasRoundStarted() + if(round_started) + message_admins("Event: [passed_event] has been scheduled to run in [passed_time / 10] seconds. (CANCEL) (REFUND)") + else //Only roundstart events can be scheduled before round start + message_admins("Event: [passed_event] has been scheduled to run on roundstart. (CANCEL)") + scheduled_events += scheduled + +/datum/controller/subsystem/gamemode/proc/update_crew_infos() + // Very similar logic to `get_active_player_count()` + active_players = 0 + head_crew = 0 + current_head_power = 0 + eng_crew = 0 + current_eng_power = 0 + med_crew = 0 + current_med_power = 0 + rnd_crew = 0 + current_rnd_power = 0 + sec_crew = 0 + var/intern_threshold = (CONFIG_GET(number/use_low_living_hour_intern_hours) * 60) || (CONFIG_GET(number/use_exp_restrictions_heads_hours) * 60) + var/is_intern = FALSE + + 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/playtime = player_mob.client.get_exp_living(pure_numeric = TRUE) + is_intern = (intern_threshold >= playtime) && (player_mob.mind?.assigned_role.job_flags & JOB_CAN_BE_INTERN) + var/datum/job/player_role = player_mob.mind.assigned_role + if(player_role.departments_bitflags & DEPARTMENT_BITFLAG_COMMAND) + head_crew++ + current_head_power += is_intern ? 0 : (playtime / 100) + if(player_role.departments_bitflags & DEPARTMENT_BITFLAG_ENGINEERING) + eng_crew++ + current_eng_power += is_intern ? 0 : (playtime / 100) + if(player_role.departments_bitflags & DEPARTMENT_BITFLAG_MEDICAL) + med_crew++ + current_med_power += is_intern ? 0 : (playtime / 100) + if(player_role.departments_bitflags & DEPARTMENT_BITFLAG_SCIENCE) + rnd_crew++ + current_rnd_power += is_intern ? 0 : (playtime / 100) + 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, forced = FALSE, from_schedule = FALSE) + . = event.preRunEvent(forced, scheduled = from_schedule) + 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, admin_forced = forced) // fallback to dynamic + triggered_round_events |= event.name + +///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() + if(!check_rights(R_FUN)) + return + + SSgamemode.event_panel(usr) + +/client/proc/forceGamemode() + set name = "Open Gamemode Panel" + set category = "Admin.Events" + if(!holder ||!check_rights(R_FUN)) + return + holder.forceGamemode(usr) + +/datum/admins/proc/forceGamemode(mob/user) + SSgamemode.admin_panel(user) + +/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() + recalculate_ready_pop() + 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) + + if (!CONFIG_GET(flag/no_intercept_report)) + addtimer(CALLBACK(GLOBAL_PROC, GLOBAL_PROC_REF(display_roundstart_logout_report)), ROUNDSTART_LOGOUT_REPORT_TIME) + + if(CONFIG_GET(flag/reopen_roundstart_suicide_roles)) + var/delay = CONFIG_GET(number/reopen_roundstart_suicide_roles_delay) + if(delay) + delay = (delay SECONDS) + else + delay = (4 MINUTES) //default to 4 minutes if the delay isn't defined. + addtimer(CALLBACK(GLOBAL_PROC, GLOBAL_PROC_REF(reopen_roundstart_suicide_roles)), delay) + + if(SSdbcore.Connect()) + var/list/to_set = list() + var/arguments = list() + if(current_storyteller) + to_set += "game_mode = :game_mode" + arguments["game_mode"] = current_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) + SSstation.generate_station_goals(INFINITY) + if(report) + generate_station_goal_report() + handle_post_setup_roundstart_events() + handle_post_setup_points() + return TRUE + + +///Handles late-join antag assignments +/datum/controller/subsystem/gamemode/proc/make_antag_chance(mob/living/carbon/human/character) + return + +/datum/controller/subsystem/gamemode/proc/check_finished(force_ending) //to be called by SSticker + if(!SSticker.setup_done) + return FALSE + if(SSshuttle.emergency && (SSshuttle.emergency.mode == SHUTTLE_ENDGAME)) + return TRUE + if(GLOB.station_was_nuked) + return TRUE + if(force_ending) + return TRUE + +/* + * Generate a list of station goals available to purchase to report to the crew. + * + * Returns a formatted string all station goals that are available to the station. + */ +/datum/controller/subsystem/gamemode/proc/generate_station_goal_report() + if(GLOB.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(generate_station_goal_report)), 10 SECONDS) + return + + . = "Департамент разведки и оценки угроз Nanotrasen, Текущий сектор, Дата и время: [time2text(world.realtime, "DDD, MMM DD")], [CURRENT_STATION_YEAR]:
" + //. += SSdynamic.generate_advisory_level() - генерация псевдо-орбит + + var/list/datum/station_goal/goals = SSstation.get_station_goals() + if(length(goals)) + var/list/texts = list("
Особые заказы для станции: [station_name()]:
") + for(var/datum/station_goal/station_goal as anything in goals) + station_goal.on_report() + texts += station_goal.get_report() + . += texts.Join("
") + + var/list/trait_list_strings = list() + for(var/datum/station_trait/station_trait as anything in SSstation.station_traits) + if(!station_trait.show_in_report) + continue + trait_list_strings += "[station_trait.get_report()]
" + if(trait_list_strings.len > 0) + . += "
Отчет отдела учета отклонений:
" + trait_list_strings.Join() + + if(length(GLOB.communications_controller.command_report_footnotes)) + var/footnote_pile = "" + + for(var/datum/command_footnote/footnote in GLOB.communications_controller.command_report_footnotes) + footnote_pile += "[footnote.message]
" + footnote_pile += "[footnote.signature]
" + footnote_pile += "
" + + . += "
Дополнительная информация:

" + footnote_pile + +#ifndef MAP_TEST + print_command_report(., "[command_name()] Status Summary", announce=FALSE) + priority_announce("Отчет был скопирован и распечатан на всех консолях связи.", "Отчет о безопасности", SSstation.announcer.get_rand_report_sound()) +#endif + + return . + +/datum/controller/subsystem/gamemode/proc/recalculate_ready_pop() + ready_players = 0 + sec_crew = 0 + for(var/mob/dead/new_player/player as anything in GLOB.new_player_list) + if(player.ready == PLAYER_READY_TO_PLAY) + ready_players++ + var/client/client_source = player.client + if(QDELETED(client_source) || !client_source.ckey) + continue + var/datum/job/player_role = presetuped_ocupations[client_source.ckey] + if(player_role?.departments_bitflags & DEPARTMENT_BITFLAG_SECURITY && !player.mind.special_role) + sec_crew++ + +/datum/controller/subsystem/gamemode/proc/round_start_handle() + recalculate_ready_pop() + recalculate_roundstart_budget() + message_admins("Storyteller begin to get roundstart events with budget [roundstart_budget].") + + var/track = EVENT_TRACK_ROLESET + if(forced_next_events[track]) + for(var/datum/round_event_control/forced_event in forced_next_events[track]) + if(forced_event.exclusive_roundstart_event) + forced_event = exclusives_add_antags(forced_event, forced = TRUE) + message_admins("Storyteller purchased and triggered forced roundstart event [forced_event].") + TriggerEvent(forced_event, forced = TRUE) + runned_events += "Runned forced roundstart: [forced_event.name]" + forced_next_events[track] = list() + + + event_track_points[track] = 0 + var/pop_count = ready_players + (sec_crew * current_storyteller.sec_antag_modifier) + if(pop_count < current_storyteller.min_antag_popcount) + message_admins("Not enough ready players to run general and scheduled roundstart events.") + message_admins("Storyteller finished to get roundstart events.") + return + + var/list/valid_events = recalculate_roundstart_costs(track) + + valid_events = rountstart_scheduled_events_run(valid_events) + + rountstart_general_events_run(valid_events, track) + + message_admins("Storyteller finished to get roundstart events with points left - [roundstart_budget].") + current_storyteller.roundstart_cap_multiplier = 1 + +/datum/controller/subsystem/gamemode/proc/recalculate_roundstart_budget() + var/pop_count = ready_players + (sec_crew * current_storyteller.sec_antag_modifier) + var/calculated_roundstart_budget = pop_count + roundstart_budget = roundstart_budget_set == -1 ? calculated_roundstart_budget : roundstart_budget_set + +/datum/controller/subsystem/gamemode/proc/rountstart_general_events_run(valid_events, track) + + if(!length(valid_events)) + message_admins("Storyteller failed to pick an events for roundstart.") + event_track_points[track] *= TRACK_FAIL_POINT_PENALTY_MULTIPLIER + return + + while(length(valid_events)) + recalculate_ready_pop() + recalculate_roundstart_costs(track) + + var/datum/round_event_control/picked_event = pick_weight(valid_events) + if(picked_event.can_spawn_event(ready_players) && (roundstart_budget >= picked_event.get_pre_cost())) + roundstart_budget -= picked_event.get_pre_cost() + message_admins("Storyteller purchased and triggered [picked_event] event for [picked_event.get_pre_cost()]. Left balance: [roundstart_budget].") + if(picked_event.exclusive_roundstart_event) + valid_events = list() + picked_event = exclusives_add_antags(picked_event, forced = FALSE) + else + // Если первое событие не-эксклюзивное, то удаляем из списка все эксклюзивные + for(var/datum/round_event_control/exclude_event as anything in valid_events) + if(exclude_event.exclusive_roundstart_event) + valid_events -= exclude_event + TriggerEvent(picked_event, forced = FALSE) + runned_events += "Runned roundstart: [picked_event.name]" + // Если первое событие эксклюзивное, то отчищаем список + + else + valid_events -= picked_event + +/datum/controller/subsystem/gamemode/proc/rountstart_scheduled_events_run(valid_events) + var/list/scheduled_events_roleset = list() + for(var/datum/scheduled_event/scheduled_pre_event in scheduled_events) + scheduled_events_roleset += scheduled_pre_event.event + scheduled_events_roleset[scheduled_pre_event.event] = scheduled_pre_event.event.weight + scheduled_events -= scheduled_pre_event + + var/list/dynamic_roundstart_rules = SSdynamic.init_rulesets(/datum/dynamic_ruleset/roundstart) + if(length(scheduled_events_roleset)) + var/datum/round_event_control/antagonist/solo/scheduled_event = pick_weight(scheduled_events_roleset) + for(var/datum/dynamic_ruleset/ruleset as anything in dynamic_roundstart_rules) + if(ruleset.antag_datum == scheduled_event.antag_datum) + scheduled_event.roundstart_cost = scheduled_event.get_pre_cost() ? scheduled_event.get_pre_cost() : ruleset.cost + break + + while(length(scheduled_events_roleset)) + var/datum/round_event_control/scheduled_event = pick_weight(scheduled_events_roleset) + if(scheduled_event.can_spawn_event(ready_players) && (roundstart_budget >= scheduled_event.get_pre_cost())) + roundstart_budget -= scheduled_event.get_pre_cost() + message_admins("Storyteller purchased and triggered scheduled event [scheduled_event] for [scheduled_event.get_pre_cost()]. Left balance: [roundstart_budget].") + if(scheduled_event.exclusive_roundstart_event) + valid_events = list() + scheduled_event = exclusives_add_antags(scheduled_event, forced = FALSE) + else + // Если первое событие не-эксклюзивное, то удаляем из списка все эксклюзивные + for(var/datum/round_event_control/exclude_event as anything in scheduled_events_roleset) + if(exclude_event.exclusive_roundstart_event) + scheduled_events_roleset -= exclude_event + for(var/datum/round_event_control/exclude_event as anything in valid_events) + if(exclude_event.exclusive_roundstart_event) + valid_events -= exclude_event + TriggerEvent(scheduled_event, forced = FALSE) + runned_events += "Runned scheduled roundstart: [scheduled_event.name]" + scheduled_events_roleset -= scheduled_event + + else + message_admins("Storyteller failed to purchase scheduled event [scheduled_event] for [scheduled_event.roundstart_cost]. Left balance: [roundstart_budget].") + scheduled_events_roleset -= scheduled_event + + return valid_events + +/datum/controller/subsystem/gamemode/proc/exclusives_add_antags(datum/round_event_control/antagonist/solo/exclusive_event, forced = FALSE) + //Получить бюджет + var/addition_antags = 0 + if(forced) + addition_antags = exclusive_event.forced_antags_count + else + while(exclusive_event.price_to_buy_adds <= roundstart_budget) + roundstart_budget -= exclusive_event.price_to_buy_adds + addition_antags++ + //Расчитать новый максимум и минимум антагов + exclusive_event.base_antags += addition_antags + exclusive_event.maximum_antags += addition_antags + roundstart_budget = 0 + return exclusive_event + +/datum/controller/subsystem/gamemode/proc/recalculate_roundstart_costs(track) + full_sec_crew = 0 + for(var/datum/job/job as anything in SSjob.all_occupations) + if(job.departments_bitflags & DEPARTMENT_BITFLAG_SECURITY) + full_sec_crew += job.total_positions + + /// Получить количество доступных раундстартом событий + current_storyteller.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 event_pools[track]) + if(event.can_spawn_event(ready_players)) + if(QDELETED(event)) + message_admins("[event.name] was deleted!") + continue + valid_events[event] = round(event.calculated_weight * 10) + if(!length(valid_events)) + return + + var/list/dynamic_roundstart_rules = SSdynamic.init_rulesets(/datum/dynamic_ruleset/roundstart) + for(var/datum/round_event_control/antagonist/solo/event as anything in valid_events) + for(var/datum/dynamic_ruleset/ruleset as anything in dynamic_roundstart_rules) + if(ruleset.antag_datum == event.antag_datum) + event.roundstart_cost = event.get_pre_cost() ? event.get_pre_cost() : ruleset.cost + break + + return valid_events + +/* + * 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 + +////////////////////////// +//Reports player logouts// +////////////////////////// +/proc/display_roundstart_logout_report() + var/list/msg = list("[span_boldnotice("Roundstart logout report")]\n\n") + for(var/i in GLOB.mob_living_list) + var/mob/living/L = i + var/mob/living/carbon/C = L + if (istype(C) && !C.last_mind) + continue // never had a client + + if(L.ckey && !GLOB.directory[L.ckey]) + msg += "[L.name] ([L.key]), the [L.job] (Disconnected)\n" + + + if(L.ckey && L.client) + var/failed = FALSE + if(L.client.inactivity >= ROUNDSTART_LOGOUT_AFK_THRESHOLD) //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(HAS_TRAIT(L, TRAIT_SUICIDED)) //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(HAS_TRAIT(L, TRAIT_SUICIDED)) //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 + + var/concatenated_message = msg.Join() + log_admin(concatenated_message) + to_chat(GLOB.admins, concatenated_message) + +//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 = "[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) + + 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/handle_picking_storyteller() + if(CONFIG_GET(flag/disable_storyteller)) + return + if(length(GLOB.clients) > MAX_POP_FOR_STORYTELLER_VOTE) + secret_storyteller = FALSE + //selected_storyteller = pick_weight(get_valid_storytellers(TRUE)) + return + //SSvote.initiate_vote(/datum/vote/storyteller, "pick round storyteller", forced = TRUE) + +/datum/controller/subsystem/gamemode/proc/storyteller_vote_choices() + var/list/final_choices = list() + var/list/pick_from = list() + for(var/datum/storyteller/storyboy in get_valid_storytellers()) + if(storyboy.always_votable) + final_choices[storyboy.name] = 0 + else + pick_from[storyboy.name] = storyboy.weight //might be able to refactor this to be slightly better due to get_valid_storytellers returning a weighted list + + var/added_storytellers = 0 + while(added_storytellers < DEFAULT_STORYTELLER_VOTE_OPTIONS && length(pick_from)) + added_storytellers++ + var/picked_storyteller = pick_weight(pick_from) + final_choices[picked_storyteller] = 0 + pick_from -= picked_storyteller + return final_choices + +/datum/controller/subsystem/gamemode/proc/storyteller_desc(storyteller_name) + for(var/storyteller_type in storytellers) + var/datum/storyteller/storyboy = storytellers[storyteller_type] + if(storyboy.name != storyteller_name) + continue + return storyboy.desc + + +/datum/controller/subsystem/gamemode/proc/storyteller_vote_result(winner_name) + for(var/storyteller_type in storytellers) + var/datum/storyteller/storyboy = storytellers[storyteller_type] + if(storyboy.name == winner_name) + selected_storyteller = storyteller_type + break + +///return a weighted list of all storytellers that are currently valid to roll, if return_types is set then we will return types instead of instances +/datum/controller/subsystem/gamemode/proc/get_valid_storytellers(return_types = FALSE) + var/client_amount = length(GLOB.clients) + var/list/valid_storytellers = list() + for(var/storyteller_type in storytellers) + var/datum/storyteller/storyboy = storytellers[storyteller_type] + if(storyboy.restricted || (storyboy.population_min && storyboy.population_min > client_amount) || (storyboy.population_max && storyboy.population_max < client_amount)) + continue + + valid_storytellers[return_types ? storyboy.type : storyboy] = storyboy.weight + return valid_storytellers + +/datum/controller/subsystem/gamemode/proc/init_storyteller() + if(!current_storyteller) + set_storyteller(selected_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], force setting to guide instead.") + current_storyteller = /datum/storyteller/default //if we dont have any then we brick, lets not do that + CRASH("Attempted to set an invalid storyteller type: [passed_type].") + var/passed_state = FALSE + var/passed_multiplayer + if (current_storyteller) + passed_state = current_storyteller.round_started + passed_multiplayer = current_storyteller.roundstart_cap_multiplier + current_storyteller.round_started = FALSE + current_storyteller = storytellers[passed_type] + current_storyteller.round_started = passed_state + current_storyteller.round_started = passed_multiplayer + if(!secret_storyteller) + send_to_playing_players(span_notice("Storyteller is [current_storyteller.name]!")) + send_to_playing_players(span_notice("[current_storyteller.welcome_text]")) + else + send_to_observers(span_boldbig("Storyteller is [current_storyteller.name]!")) //observers still get to know + +/// Panel containing information, variables and controls about the gamemode and scheduled event +/datum/controller/subsystem/gamemode/proc/admin_panel(mob/user) + if(!current_storyteller) + set_storyteller(selected_storyteller) + update_crew_infos() + var/round_started = SSticker.HasRoundStarted() + var/list/dat = list() + dat += "Storyteller: [current_storyteller ? "[current_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_players] (Head: [head_crew]/[current_head_power], Sec: [sec_crew], Eng: [eng_crew]/[current_eng_power], Med: [med_crew]/[current_med_power], RnD: [rnd_crew]/[current_rnd_power])" + dat += "
Antagonist Count vs Maximum: [get_antag_count()] / [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 += "
" + + dat += "
Storyteller Basic Variables:" + dat += "
Storyteller Antag Low pop:[current_storyteller.min_antag_popcount]" + dat += "
Это значение устанавливает то, сколько игроков считается минимально-необходимым для спавна антагонистов" + dat += "
Guarantees Roundstart Roleset: [current_storyteller.guarantees_roundstart_roleset ? "TRUE" : "FALSE" ]" + dat += "
Storyteller Antag Cap Formula: floor((pop_count + secs * sec_antag_modifier) / denominator)[!SSticker.HasRoundStarted() ? " * roundstart cap multiplier" : ""] + addiction" + dat += "
Storyteller Antag Cap Result: floor(([get_correct_popcount()] + [sec_crew] * [current_storyteller.sec_antag_modifier]) / [current_storyteller.antag_denominator])[!SSticker.HasRoundStarted() ? " * [current_storyteller.roundstart_cap_multiplier]" : ""] + [current_storyteller.antag_flat_cap]" + dat += "
Sec antag modifier: [current_storyteller.sec_antag_modifier]" + dat += "
Antag addiction: [current_storyteller.antag_flat_cap]" + if(!SSticker.HasRoundStarted()) + dat += "
Antag roundstart cap multiplier: [current_storyteller.roundstart_cap_multiplier]" + dat += "
Antag denominator: [current_storyteller.antag_denominator]" + dat += "
Эта настройка влияет на то, сколько антагонистов может заспавниться максимум на 1 члена экипажа." + dat += "
" + + if(!SSticker.HasRoundStarted()) + handle_pre_setup_occupations() + recalculate_ready_pop() + recalculate_roundstart_costs(EVENT_TRACK_ROLESET) + recalculate_roundstart_budget() + dat += "
Storyteller Roundstart Values:" + + dat += "
Sec info: Full sec crew = [full_sec_crew], Players with High Sec = [sec_crew]" + dat += "
Отображает максимальное количество ролей с пометкой СБ, у скольких игроков эти должности в высоком приоритете и сколько нехватка." + dat += "
Roundstart info: Roundstart budget = ready_players([ready_players]) + (sec_crew([sec_crew]) * current_storyteller.sec_antag_modifier([current_storyteller.sec_antag_modifier])) = [roundstart_budget]" + dat += "
Раундстартовый бюджет для событий, расчитанный с помощью формулы выше." + dat += "
Roundstart info: Forced Roundstart budget = [roundstart_budget_set]" + dat += "
Зафоршенный андминами раундстарт бюджет. Установите -1 для автоматического расчета." + dat += "
" + + dat += "
Point Gains Multipliers (only over time):" + dat += "
Basic all tracks multiplayer: [current_storyteller.point_gain_base_mult]" + 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]: [current_storyteller.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 += "" + dat += "" + dat += "" + dat += "" + dat += "" + 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] != 0 ? last_point_gains[track] : 1 + if(last_points) + next = round(((upper - lower) / last_points / STORYTELLER_WAIT_TIME)) + dat += "" + dat += "" //Track + dat += "" //Progress + dat += "" //Next + var/list/forced_events = forced_next_events[track] + var/forced = "" + for(var/datum/round_event_control/forced_event in forced_events) + var/event_data = "[forced_event.name] X
" + if(ispath(forced_event.type, /datum/round_event_control/antagonist)) + var/datum/round_event_control/antagonist/forced_antag_event = forced_event + event_data = "[forced_antag_event.name][forced_antag_event?.forced_antags_count > 0 ? "([forced_antag_event?.forced_antags_count])": ""] X
" + forced = forced + event_data + dat += "" //Forced + dat += "" //Actions + dat += "" + dat += "
TrackProgressNextForcedActions
[track] - [last_points] per process.[percent]% ([lower]/[upper])~[next] seconds[forced]Set Pts. Next Event
" + + dat += "

Scheduled Events:

" + dat += "" + dat += "" + dat += "" + dat += "" + dat += "" + dat += "" + 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 += "" //Name + dat += "" //Severity + var/time = (scheduled.event.roundstart && !round_started) ? "ROUNDSTART" : "[(scheduled.start_time - world.time) / (1 SECONDS)] s." + dat += "" //Time + dat += "" //Actions + dat += "" + dat += "
NameSeverityTimeActions
[scheduled.event.name][scheduled.event.track][time][scheduled.get_href_actions()]
" + + dat += "

Running Events:

" + dat += "" + dat += "" + dat += "" + dat += "" + dat += "" + even = TRUE + for(var/datum/round_event/event as anything in running) + even = !even + var/background_cl = even ? "#17191C" : "#23273C" + dat += "" + dat += "" //Name + dat += "" //Actions + dat += "" + dat += "
NameActions
[event.control.name]-TBA-
" + + dat += "

Runned Events:

" + dat += "" + dat += "" + dat += "" + dat += "" + even = TRUE + for(var/event as anything in runned_events) + even = !even + var/background_cl = even ? "#17191C" : "#23273C" + dat += "" + dat += "" //Name + dat += "" + dat += "
Name
[event]
" + + 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(!SSticker.HasRoundStarted()) + handle_pre_setup_occupations() + recalculate_ready_pop() + recalculate_roundstart_costs(EVENT_TRACK_ROLESET) + recalculate_roundstart_budget() + + if(current_storyteller) + dat += "Storyteller: [current_storyteller.name]" + dat += "
Repetition penalty multiplier: [current_storyteller.event_repetition_multiplier]" + dat += "
Cost variance: [current_storyteller.cost_variance][SSticker.HasRoundStarted() ? "" : ", roundstart_budget = [roundstart_budget]"]" + if(current_storyteller.tag_multipliers) + dat += "
Tag multipliers:" + for(var/tag in current_storyteller.tag_multipliers) + dat += "[tag]:[current_storyteller.tag_multipliers[tag]] | " + current_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/lower = event_track_points[track] + var/upper = point_thresholds[track] + var/last_points = last_point_gains[track] != 0 ? last_point_gains[track] : 1 + var/est_time = round(((upper - lower) / last_points / STORYTELLER_WAIT_TIME)) + dat += "[track]: ~[est_time] secs. | " + dat += "
" + for(var/track in EVENT_PANEL_TRACKS) + dat += "[track]" + dat += "
" + /// Create event info and stats table + dat += "" + dat += "" + dat += "" + dat += "" + dat += "" + dat += "" + dat += "" + dat += "" + dat += "" + dat += "" + dat += "" + dat += "" + 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() + for(var/datum/round_event_control/event as anything in event_lookup) + var/players_amt = get_active_player_count(alive_check = 1, afk_check = 1, human_check = 1) + if(event.roundstart != roundstart_event_view) + continue + if(event.can_spawn_event(players_amt)) + 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 += "" //Name + dat += "" + var/occurence_string = "[event.occurrences]" + if(event.shared_occurence_type) + occurence_string += " (shared: [event.get_occurences()])" + var/max_occurence_string = "[event.max_occurrences]" + dat += "" //Occurences + dat += "" //Max Occurences + dat += "" //Minimum pop + dat += "" //Minimum time + dat += "" //Can happen? + dat += "" //Why can't happen? + var/weight_string = "(new.[event.calculated_weight] /raw.[event.weight])" + if(assoc_spawn_weight[event]) + var/percent = round((event.calculated_weight / total_weight) * 100) + weight_string = "[event.calculated_on_track_weight]% - [percent]% - [weight_string]" + dat += "" //Weight + dat += "" //Actions + dat += "" + dat += "
NameTagsOccurencesMax OccurencesM.PopM.TimeCan OccurFailure Reason[!SSticker.HasRoundStarted() ? "Cost/" : ""]WeightActions
[event.name]" //Tags + for(var/tag in event.tags) + dat += "[tag] " + dat += "[occurence_string][max_occurence_string][event.min_players][event.earliest_start / (1 MINUTES)] m.[assoc_spawn_weight[event] ? "Yes" : "No"][event.return_failure_string(active_players)][SSticker.HasRoundStarted() && !event.get_pre_cost() ? weight_string : "[event.get_pre_cost()]/[weight_string]"][event.get_href_actions()]
" + var/datum/browser/popup = new(user, "gamemode_event_panel", "Event Panel", 1100, 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].") + current_storyteller.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("vars_addiction") + 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 addictive antags to [new_value].") + current_storyteller.antag_flat_cap = new_value + if("vars_sec_antag") + 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 sec antag modifier to [new_value].") + current_storyteller.sec_antag_modifier = new_value + if("vars_denominator") + 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 antags denominator to [new_value].") + current_storyteller.antag_denominator = new_value + if("vars_lowpop") + 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 low pop count to [new_value].") + current_storyteller.min_antag_popcount = new_value + if("vars_basemult") + 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 basic storyteller multiplier to [new_value].") + current_storyteller.point_gain_base_mult = new_value + if("vars_cap_mult") + 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 basic storyteller cap multiplier to [new_value].") + current_storyteller.roundstart_cap_multiplier = new_value + if("vars_guarante_roundstart") + var/new_value = !current_storyteller.guarantees_roundstart_roleset + message_admins("[key_name_admin(usr)] set basic storyteller multiplier to [new_value].") + current_storyteller.guarantees_roundstart_roleset = new_value + if("vars_roundstart_budget") + 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 forced roundstart budget to [new_value].") + roundstart_budget_set = new_value + if("vars_storyteller_basic_modifier") + 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 storyteller basic cost modifier to [new_value].") + current_storyteller.storyteller_basic_modifier = 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/forced_event_to_remove + for(var/datum/round_event_control/forced_event_to_check in forced_next_events[track]) + if(forced_event_to_check.name == href_list["forced_event_to_remove"]) + forced_event_to_remove = forced_event_to_check + if(!forced_event_to_remove) + return + message_admins("[key_name_admin(usr)] removed forced event [forced_event_to_remove.name] from track [forced_event_to_remove.track].") + forced_next_events[track] -= forced_event_to_remove + 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(current_storyteller) + current_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) + +/datum/controller/subsystem/gamemode/proc/round_end_report() + if(!length(round_end_data)) + return + for(var/datum/round_event/event as anything in round_end_data) + if(!istype(event)) + continue + event.round_end_report() + +/datum/controller/subsystem/gamemode/proc/store_roundend_data() + var/congealed_string = "" + for(var/event_name as anything in triggered_round_events) + congealed_string += event_name + congealed_string += "," + text2file(congealed_string, "data/last_round_events.txt") + +/datum/controller/subsystem/gamemode/proc/load_roundstart_data() + INVOKE_ASYNC(SSmapping, TYPE_PROC_REF(/datum/controller/subsystem/mapping, lazy_load_template), LAZY_TEMPLATE_KEY_NUKIEBASE) + INVOKE_ASYNC(SSmapping, TYPE_PROC_REF(/datum/controller/subsystem/mapping, lazy_load_template), LAZY_TEMPLATE_KEY_WIZARDDEN) + + var/massive_string = trim(file2text("data/last_round_events.txt")) + if(fexists("data/last_round_events.txt")) + fdel("data/last_round_events.txt") + if(!massive_string) + return + last_round_events = splittext(massive_string, ",") + + if(!length(last_round_events)) + return + for(var/event_name as anything in last_round_events) + for(var/datum/round_event_control/listed as anything in control) + if(listed.name != event_name) + continue + listed.occurrences++ + listed.occurrences++ + +/datum/controller/subsystem/gamemode/proc/get_antag_count_by_type(type) + var/count = 0 + if(!type) + return count + + for(var/datum/antagonist/antag_datum_element in GLOB.antagonists) + if(antag_datum_element.type == type) + count++ + + return count + +/datum/controller/subsystem/gamemode/proc/left_antag_count_by_type(control) + var/datum/round_event_control/antagonist/solo/cast_control = control + var/left = cast_control.maximum_antags_per_round - get_antag_count_by_type(cast_control.antag_datum) + return left + +/datum/controller/subsystem/gamemode/proc/create_roundend_score() + var/list/parts = list() + parts += "
[("Storyteller: [SSgamemode.current_storyteller ? SSgamemode.current_storyteller.name : "N/A"]")]

" + parts += "Spawned durning this round events:
" + for(var/i in SSgamemode.runned_events) + parts += "[i]
" + + parts += "
" + return parts + +#undef DEFAULT_STORYTELLER_VOTE_OPTIONS +#undef MAX_POP_FOR_STORYTELLER_VOTE +#undef ROUNDSTART_VALID_TIMEFRAME diff --git a/modular_bandastation/storyteller/code/readme.md b/modular_bandastation/storyteller/code/readme.md new file mode 100644 index 0000000000000..d8f40256bd553 --- /dev/null +++ b/modular_bandastation/storyteller/code/readme.md @@ -0,0 +1,44 @@ +## Title: + + +MODULE ID: STORYTELLERS + +### Description: + +This PR adds adds on to the current dynamic system by having events be guided by storytellers, this also caches the events ran last round and depending on severity cuts their weights by x % to make rounds not repeat as often. + + + + +### TG Proc/File Changes: + + + - N/A + +### Defines: + + + - code\__DEFINES\~monkestation\storytellers.dm + +### Master file additions + +- code\modules\events\_event.dm +- code\modules\admin\topic.dm +- code\controllers\subsystem\ticker.dm +- code\controllers\subsystem\statpanel.dm +- all event files + + + +### Included files that are not contained in this module: + +- N/A + + +### Credits: + + + +Made by Unknown Coders on Horizon (Horizon's Repo atleast as of 10/14/2023 no longer exists if this changes please let me know on discord #Borbop) + +Ported by KageIIte diff --git a/modular_bandastation/storyteller/code/scheduled_event.dm b/modular_bandastation/storyteller/code/scheduled_event.dm new file mode 100644 index 0000000000000..f79130b9cffc2 --- /dev/null +++ b/modular_bandastation/storyteller/code/scheduled_event.dm @@ -0,0 +1,94 @@ +///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() + + ///If we can't spawn the scheduled event, refund it. + if(!ignores_checks && !event.can_spawn_event(1000)) //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.") + SSgamemode.refund_scheduled_event(src) + return + + ///Trigger the event and remove the scheduled datum + message_admins("Scheduled Event: [event] successfully triggered.") + SSgamemode.TriggerEvent(event, ignores_checks, from_schedule = TRUE) + 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_bandastation/storyteller/code/storytellers/_storyteller.dm b/modular_bandastation/storyteller/code/storytellers/_storyteller.dm new file mode 100644 index 0000000000000..e814d17395ff9 --- /dev/null +++ b/modular_bandastation/storyteller/code/storytellers/_storyteller.dm @@ -0,0 +1,238 @@ + +///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 = "Set your eyes on the horizon." + /// 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 + ) + /// 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 + ) + /// 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 a storyteller is pickable/can be voted for + var/restricted = FALSE + /// 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 + /// has the round gotten to the point where jobs are pre-created? + var/round_started = FALSE + ///have we done roundstart checks? + var/roundstart_checks = FALSE + ///prob of roundstart antag + var/roundstart_prob = 25 + ///do we ignore ran_roundstart + var/ignores_roundstart = FALSE + ///is a storyteller always able to be voted for(also does not count for the amount of storytellers to pick from) + var/always_votable = FALSE + ///weight this has of being picked for random storyteller/showing up in the vote if not always_votable + var/weight = 0 + ///Количество игроков на сервере, чтобы сторителлер начинал расчеты максимального количества антагов + var/min_antag_popcount = STORYTELLER_MIN_ANTAG_POPCOUNT + ///Количество игроков на сервере, которое требуется чтобы появился хотя бы один антаг (по умолчанию 20) + var/antag_denominator = ANTAG_CAP_DENOMINATOR + ///Количество антагов, которое СТ может добавить сверх расчетов + var/antag_flat_cap = ANTAG_CAP_FLAT + ///Общий множитель всех треков сторителлера (для коректировок) + var/point_gain_base_mult = STORYTELLER_BASIC_MULT + ///Множитель силы СБ + var/sec_antag_modifier = STORYTELLER_SEC_ANTAG_MODIFIER + ///Множитель цен антагов + var/storyteller_basic_modifier = STORYTELLER_BASIC_MODIFIER + ///Список событий на исключение + var/list/exclude_events = list() + ///Финальный множитель антаг-капа (до ручного добавления) + var/roundstart_cap_multiplier = 1 + +/datum/storyteller/process(seconds_per_tick) + if(!round_started || disable_distribution) // we are differing roundstarted ones until base roundstart so we can get cooler stuff + return + + if(!guarantees_roundstart_roleset && prob(roundstart_prob) && !roundstart_checks) + roundstart_checks = TRUE + + add_points(seconds_per_tick) + handle_tracks() + +/// Add points to all tracks while respecting the multipliers. +/datum/storyteller/proc/add_points(seconds_per_tick) + var/datum/controller/subsystem/gamemode/mode = SSgamemode + var/base_point = EVENT_POINT_GAINED_PER_SECOND * seconds_per_tick * mode.event_frequency_multiplier //w = 0.08*y*1 = 0.4 => y = 5 + for(var/track in mode.event_track_points) + var/point_gain = base_point * point_gains_multipliers[track] * mode.point_gain_multipliers[track] * point_gain_base_mult // p = w*1*1*10=4 => w = 0.4 + if(mode.allow_pop_scaling) + point_gain *= mode.current_pop_scale_multipliers[track] //p*1 = 4 + 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(SSgamemode.can_run_roundstart && track == EVENT_TRACK_ROLESET) + SSgamemode.round_start_handle() + SSgamemode.can_run_roundstart = FALSE + SSgamemode.roundstart_event_view = FALSE + + if(points >= mode.point_thresholds[track]) + if(prob(SSgamemode.empty_event_chance) && track == EVENT_TRACK_ROLESET) + calculate_empty_event(TRUE) + mode.event_track_points[track] = 0 + else if(find_and_buy_event_from_track(track)) + calculate_empty_event(FALSE) + . = TRUE + +/datum/storyteller/proc/calculate_empty_event(reset = FALSE) + if(reset) + SSgamemode.empty_event_chance = 5 + else + SSgamemode.empty_event_chance += SSgamemode.get_antag_count() + +/// Find and buy a valid event from a track. +/datum/storyteller/proc/find_and_buy_event_from_track(track) + . = FALSE + + var/are_forced = FALSE + var/datum/controller/subsystem/gamemode/mode = SSgamemode + var/datum/round_event_control/picked_event + if(length(SSgamemode.forced_next_events[track])) + picked_event = pick(SSgamemode.forced_next_events[track]) + SSgamemode.forced_next_events[track] -= picked_event + are_forced = TRUE + else + mode.update_crew_infos() + var/pop_required = mode.min_pop_thresholds[track] + if(mode.active_players < 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: [mode.active_players])") + mode.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 mode.event_pools[track]) + var/players_amt = get_active_player_count(alive_check = TRUE, afk_check = TRUE, human_check = TRUE) + if(event.can_spawn_event(players_amt)) + if(QDELETED(event)) + message_admins("[event.name] was deleted!") + continue + valid_events[event] = round(event.calculated_weight * 10) //multiply weight by 10 to get first decimal value + ///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].") + mode.event_track_points[track] *= TRACK_FAIL_POINT_PENALTY_MULTIPLIER + return + picked_event = pick_weight(valid_events) + if(!picked_event) + if(length(valid_events)) + var/added_string = "" + for(var/datum/round_event_control/item as anything in valid_events) + added_string += "[item.name]:[valid_events[item]]; " + stack_trace("WARNING: Storyteller picked a null from event pool, defaulting to option 1, look at weights:[added_string]") + shuffle_inplace(valid_events) + picked_event = valid_events[1] + else + message_admins("WARNING: Storyteller picked a null from event pool. Aborting event roll.") + stack_trace("WARNING: Storyteller picked a null from event pool.") + SSgamemode.event_track_points[track] = 0 + return + + if(picked_event?.can_spawn_event(get_active_player_count(alive_check = TRUE, afk_check = TRUE, human_check = TRUE)) && track && !are_forced) + buy_event(picked_event, track, are_forced) + else if(are_forced) + buy_event(picked_event, track, are_forced) + . = TRUE + +///Attempt to buy a specific event if we can afford it, otherwise returns FALSE, note this does NOT take cost variance into account +/datum/storyteller/proc/try_buy_event(datum/round_event_control/bought_event) + if(ispath(bought_event)) + bought_event = locate(bought_event) in SSevents.control //might be able to make this slightly cheaper by searching in the track sorted list + var/track = bought_event.track + if(!track || (bought_event in SSgamemode.uncategorized)) + return FALSE //trackless events cant be bought + + var/datum/controller/subsystem/gamemode/mode = SSgamemode + if(mode.event_track_points[track] - (bought_event.cost * mode.point_thresholds[track]) < 0) + return FALSE + + buy_event(bought_event, track) + return TRUE + +/// Find and buy a valid event from a track. +/datum/storyteller/proc/buy_event(datum/round_event_control/bought_event, track, forced = FALSE) + if(!track) + track = 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] = max(mode.event_track_points[track] - total_cost, 0) + message_admins("Storyteller purchased and triggered [bought_event] event, on [track] track, for [total_cost] cost.") + if(bought_event.roundstart) + mode.TriggerEvent(bought_event, forced) + else + mode.schedule_event(bought_event, 3 MINUTES, total_cost, _forced = forced) + +/// Calculates the weights of the events from a passed track. +/datum/storyteller/proc/calculate_weights(track) + var/datum/controller/subsystem/gamemode/mode = SSgamemode + var/track_weight = 0 + for(var/datum/round_event_control/event as anything in mode.event_pools[track]) + track_weight += event.weight + 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 + event.calculated_on_track_weight = weight_total/track_weight //Получить все веса всего трека и разделить на количество событий в треке diff --git a/modular_bandastation/storyteller/code/storytellers/storyteller_black_orbit.dm b/modular_bandastation/storyteller/code/storytellers/storyteller_black_orbit.dm new file mode 100644 index 0000000000000..0e4a491553e5d --- /dev/null +++ b/modular_bandastation/storyteller/code/storytellers/storyteller_black_orbit.dm @@ -0,0 +1,8 @@ +/datum/storyteller/black_orbit + name = "Black Orbit" + desc = "Black Orbit exist only for one purpose. To make you suffer." + welcome_text = "Suffer, b*tch!" + weight = 0 + disable_distribution = TRUE + population_max = 25 + tag_multipliers = list(TAG_COMBAT = 5, TAG_DESTRUCTIVE = 5, TAG_TARGETED = 5) diff --git a/modular_bandastation/storyteller/code/storytellers/storyteller_bomb.dm b/modular_bandastation/storyteller/code/storytellers/storyteller_bomb.dm new file mode 100644 index 0000000000000..643b88dbff583 --- /dev/null +++ b/modular_bandastation/storyteller/code/storytellers/storyteller_bomb.dm @@ -0,0 +1,14 @@ +/datum/storyteller/bomb + name = "The Bomb" + desc = "The Bomb enjoys a good fight but abhors senseless destruction. Prefers heavy hits on single targets." + welcome_text = "GLA! GLA! GLA!" + point_gains_multipliers = list( + EVENT_TRACK_MUNDANE = 1, + EVENT_TRACK_MODERATE = 1.2, + EVENT_TRACK_MAJOR = 1.15, + EVENT_TRACK_ROLESET = 1, + EVENT_TRACK_OBJECTIVES = 1 + ) + tag_multipliers = list(TAG_COMBAT = 1.4, TAG_DESTRUCTIVE = 2, TAG_TARGETED = 1.2) + population_min = 25 //combat based so we should have some kind of min pop(even if low) + weight = 3 diff --git a/modular_bandastation/storyteller/code/storytellers/storyteller_chill.dm b/modular_bandastation/storyteller/code/storytellers/storyteller_chill.dm new file mode 100644 index 0000000000000..073a566df487c --- /dev/null +++ b/modular_bandastation/storyteller/code/storytellers/storyteller_chill.dm @@ -0,0 +1,16 @@ +/datum/storyteller/chill + name = "The Chill" + desc = "The Chill will be light on events compared to other storytellers, especially so on ones involving combat, destruction, or chaos. Best for more chill rounds." + welcome_text = "The day is going slowly." + 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.6, TAG_DESTRUCTIVE = 0.7) + always_votable = TRUE //good for low pop + population_max = 45 + weight = 2 diff --git a/modular_bandastation/storyteller/code/storytellers/storyteller_clown.dm b/modular_bandastation/storyteller/code/storytellers/storyteller_clown.dm new file mode 100644 index 0000000000000..0ba6750acbd39 --- /dev/null +++ b/modular_bandastation/storyteller/code/storytellers/storyteller_clown.dm @@ -0,0 +1,17 @@ +/datum/storyteller/clown + name = "The Clown" + desc = "The clown creates only harmless events(citation needed), its all fun and games with this one!" + welcome_text = "HONKHONKHONKHONKHONK!" + event_repetition_multiplier = 1 //can repeat things freely + point_gains_multipliers = list( + EVENT_TRACK_MUNDANE = 5, //admin only, welcome to hell + EVENT_TRACK_MODERATE = 4, + EVENT_TRACK_MAJOR = 0.3, + EVENT_TRACK_ROLESET = 1, + EVENT_TRACK_OBJECTIVES = 1, + ) + tag_multipliers = list(TAG_COMMUNAL = 1.1, TAG_SPOOKY = 1.2) + guarantees_roundstart_roleset = FALSE + restricted = TRUE //admins can still use this if they want the crew to really suffer, for that reason im going all in + roundstart_prob = 75 + ignores_roundstart = TRUE diff --git a/modular_bandastation/storyteller/code/storytellers/storyteller_default.dm b/modular_bandastation/storyteller/code/storytellers/storyteller_default.dm new file mode 100644 index 0000000000000..93036b1bbfe80 --- /dev/null +++ b/modular_bandastation/storyteller/code/storytellers/storyteller_default.dm @@ -0,0 +1,7 @@ +/datum/storyteller/default + name = "Default Andy" + desc = "Default Andy is the default Storyteller, and the comparison point for every other Storyteller. Best for an average, varied experience." + always_votable = TRUE + welcome_text = "I am not Randy!" + weight = 6 + roundstart_cap_multiplier = 0.7 diff --git a/modular_bandastation/storyteller/code/storytellers/storyteller_extended.dm b/modular_bandastation/storyteller/code/storytellers/storyteller_extended.dm new file mode 100644 index 0000000000000..a2645ed0a8907 --- /dev/null +++ b/modular_bandastation/storyteller/code/storytellers/storyteller_extended.dm @@ -0,0 +1,8 @@ +/datum/storyteller/extended + name = "Extended" + desc = "Extended 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 peaceful storytellers are low enough." + welcome_text = "The station feels invisible to outside influence." + weight = 1 + disable_distribution = TRUE + population_max = 25 + diff --git a/modular_bandastation/storyteller/code/storytellers/storyteller_fragile.dm b/modular_bandastation/storyteller/code/storytellers/storyteller_fragile.dm new file mode 100644 index 0000000000000..50e25a6dcbf72 --- /dev/null +++ b/modular_bandastation/storyteller/code/storytellers/storyteller_fragile.dm @@ -0,0 +1,13 @@ +/datum/storyteller/fragile + name = "The Fragile" + desc = "The Fragile will create mostly internal conflict around the station, and rarely any external threats." + event_repetition_multiplier = 0.7 //Hermit has a smaller event pool, let it repeat a bit more + point_gains_multipliers = list( + EVENT_TRACK_MUNDANE = 1.2, + EVENT_TRACK_MODERATE = 1.1, + EVENT_TRACK_MAJOR = 0.9, + EVENT_TRACK_ROLESET = 0.9, + EVENT_TRACK_OBJECTIVES = 1 + ) + tag_multipliers = list(TAG_EXTERNAL = 0.2) + weight = 3 diff --git a/modular_bandastation/storyteller/code/storytellers/storyteller_gamer.dm b/modular_bandastation/storyteller/code/storytellers/storyteller_gamer.dm new file mode 100644 index 0000000000000..7ea1addb20f7a --- /dev/null +++ b/modular_bandastation/storyteller/code/storytellers/storyteller_gamer.dm @@ -0,0 +1,14 @@ +/datum/storyteller/gamer + name = "The Gamer" + desc = "The Gamer will try to create the most combat focused events, while trying to avoid purely destructive ones." + welcome_text = "You feel like a fight is brewing." + weight = 1 + 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 = 40 diff --git a/modular_bandastation/storyteller/code/storytellers/storyteller_jester.dm b/modular_bandastation/storyteller/code/storytellers/storyteller_jester.dm new file mode 100644 index 0000000000000..f330d68b91418 --- /dev/null +++ b/modular_bandastation/storyteller/code/storytellers/storyteller_jester.dm @@ -0,0 +1,14 @@ +/datum/storyteller/jester + name = "The Jester" + desc = "The Jester will create much more events, with higher possibilities of them repeating." + event_repetition_multiplier = 0.8 + point_gains_multipliers = list( + EVENT_TRACK_MUNDANE = 1.2, + EVENT_TRACK_MODERATE = 1.3, + EVENT_TRACK_MAJOR = 1.3, + EVENT_TRACK_ROLESET = 1, + EVENT_TRACK_OBJECTIVES = 1 + ) + population_min = 40 + ignores_roundstart = TRUE + weight = 2 diff --git a/modular_bandastation/storyteller/code/storytellers/storyteller_mystic.dm b/modular_bandastation/storyteller/code/storytellers/storyteller_mystic.dm new file mode 100644 index 0000000000000..cf42c6bce029a --- /dev/null +++ b/modular_bandastation/storyteller/code/storytellers/storyteller_mystic.dm @@ -0,0 +1,6 @@ +/datum/storyteller/mystic + name = "The Mystic" + desc = "The Mystic gives events from beyond the veil, some of which may even be magic in nature." + tag_multipliers = list(TAG_SPOOKY = 1.2, TAG_MAGICAL = 1.5, TAG_SPACE = 1.1) + weight = 2 + population_min = 40 //all current magic antags are very murder and/or pop-based (eg: cult, wizard, heretic) change if we get less murdery magic antags diff --git a/modular_bandastation/storyteller/code/storytellers/storyteller_operative.dm b/modular_bandastation/storyteller/code/storytellers/storyteller_operative.dm new file mode 100644 index 0000000000000..4a1a10d8287c1 --- /dev/null +++ b/modular_bandastation/storyteller/code/storytellers/storyteller_operative.dm @@ -0,0 +1,23 @@ +/datum/storyteller/operative + name = "The Operative" + desc = "The Operative tries to create more direct confrontation with human threats." + welcome_text = "The eyes of multiple organizations have been set on the station." + starting_point_multipliers = list( + EVENT_TRACK_MUNDANE = 1, + EVENT_TRACK_MODERATE = 1, + EVENT_TRACK_MAJOR = 1, + EVENT_TRACK_ROLESET = 1.1, + EVENT_TRACK_OBJECTIVES = 1 + ) + point_gains_multipliers = list( + EVENT_TRACK_MUNDANE = 1, + EVENT_TRACK_MODERATE = 0.8, + EVENT_TRACK_MAJOR = 1, + EVENT_TRACK_ROLESET = 1.2, + EVENT_TRACK_OBJECTIVES = 1 + ) + tag_multipliers = list(TAG_ALIEN = 0.4, TAG_CREW_ANTAG = 1.1) + population_min = 45 + ignores_roundstart = TRUE + weight = 1 + restricted = TRUE diff --git a/modular_bandastation/storyteller/code/storytellers/storyteller_phantom.dm b/modular_bandastation/storyteller/code/storytellers/storyteller_phantom.dm new file mode 100644 index 0000000000000..34cb9865e3568 --- /dev/null +++ b/modular_bandastation/storyteller/code/storytellers/storyteller_phantom.dm @@ -0,0 +1,27 @@ +/datum/storyteller/pups + name = "Poops x10" + desc = "It's super friendly storyteller, to make your game more fun." + welcome_text = "Friendship is a magic!" + + event_repetition_multiplier = 0.1 + + starting_point_multipliers = list( + EVENT_TRACK_MUNDANE = 10, + EVENT_TRACK_MODERATE = 10, + EVENT_TRACK_MAJOR = 10, + EVENT_TRACK_ROLESET = 10, + EVENT_TRACK_OBJECTIVES = 10 + ) + point_gains_multipliers = list( + EVENT_TRACK_MUNDANE = 10, + EVENT_TRACK_MODERATE = 10, + EVENT_TRACK_MAJOR = 10, + EVENT_TRACK_ROLESET = 10, + EVENT_TRACK_OBJECTIVES = 10 + ) + tag_multipliers = list(TAG_DESTRUCTIVE = 2, TAG_COMMUNAL = 2, TAG_POSITIVE = 2, TAG_MAGICAL = 2) + population_min = 15 + ignores_roundstart = TRUE + weight = 1 + restricted = TRUE + guarantees_roundstart_roleset = TRUE diff --git a/modular_bandastation/storyteller/code/storytellers/storyteller_prime.dm b/modular_bandastation/storyteller/code/storytellers/storyteller_prime.dm new file mode 100644 index 0000000000000..9ca170d9462ad --- /dev/null +++ b/modular_bandastation/storyteller/code/storytellers/storyteller_prime.dm @@ -0,0 +1,22 @@ +/datum/controller/subsystem/gamemode/Initialize(time, zlevel) + . = ..() + selected_storyteller = /datum/storyteller/prime + +/datum/storyteller/prime + name = "Andy Prime" + desc = "Andy Prime is the default Storyteller, and the comparison point for every other Storyteller. Best for an average, varied experience." + always_votable = TRUE + welcome_text = "I am not your Andy!" + weight = 6 + exclude_events = list( + /datum/round_event_control/stray_meteor, + /datum/round_event_control/meteor_wave, + /datum/round_event_control/earthquake, + /datum/round_event_control/stray_cargo, + /datum/round_event_control/meteor_wave/threatening, + /datum/round_event_control/meteor_wave/catastrophic, + /datum/round_event_control/meteor_wave/meaty, + /datum/round_event_control/stray_cargo/syndicate, + ) + antag_denominator = 15 + roundstart_cap_multiplier = 0.7 diff --git a/modular_bandastation/storyteller/code/vote.dm b/modular_bandastation/storyteller/code/vote.dm new file mode 100644 index 0000000000000..4eddcab85642f --- /dev/null +++ b/modular_bandastation/storyteller/code/vote.dm @@ -0,0 +1,33 @@ +/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 + player_startable = FALSE + + +/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 storyteller 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/finalize_vote(winning_option) + SSgamemode.storyteller_vote_result(winning_option)