diff --git a/code/controllers/Processes/game_master.dm b/code/controllers/Processes/game_master.dm new file mode 100644 index 0000000000..dd4e0d1c23 --- /dev/null +++ b/code/controllers/Processes/game_master.dm @@ -0,0 +1,6 @@ +/datum/controller/process/event/setup() + name = "\improper GM controller" + schedule_interval = 600 // every 60 seconds + +/datum/controller/process/event/doWork() + game_master.process() \ No newline at end of file diff --git a/code/global.dm b/code/global.dm index 503c953a9e..cd27fbc8ec 100644 --- a/code/global.dm +++ b/code/global.dm @@ -118,6 +118,7 @@ var/join_motd = null var/datum/nanomanager/nanomanager = new() // NanoManager, the manager for Nano UIs. var/datum/event_manager/event_manager = new() // Event Manager, the manager for events. +var/datum/game_master/game_master = new() // Game Master, an AI for choosing events. var/list/awaydestinations = list() // Away missions. A list of landmarks that the warpgate can take you to. diff --git a/code/modules/admin/admin_verbs.dm b/code/modules/admin/admin_verbs.dm index 8e1e81c603..3ccfffdf36 100644 --- a/code/modules/admin/admin_verbs.dm +++ b/code/modules/admin/admin_verbs.dm @@ -197,7 +197,8 @@ var/list/admin_verbs_debug = list( /client/proc/dsay, /client/proc/toggle_debug_logs, /client/proc/admin_ghost, //allows us to ghost/reenter body at will, - /datum/admins/proc/view_runtimes + /datum/admins/proc/view_runtimes, + /client/proc/show_gm_status ) var/list/admin_verbs_paranoid_debug = list( diff --git a/code/modules/gamemaster/actions/action.dm b/code/modules/gamemaster/actions/action.dm new file mode 100644 index 0000000000..82ebd860b5 --- /dev/null +++ b/code/modules/gamemaster/actions/action.dm @@ -0,0 +1,29 @@ +/datum/gm_action + var/name = "no name" // Simple name, for organization. + var/enabled = TRUE // If not enabled, this action is never taken. + var/departments = list() // What kinds of departments are affected by this action. Multiple departments can be listed. + var/chaotic = 0 // A number showing how chaotic the action may be. If danger is high, the GM will avoid it. + var/reusable = FALSE // If true, the event does not become disabled upon being used. Should be used sparingly. + var/observers_used = FALSE // Determines if the GM should check if ghosts are available before using this. + var/datum/game_master/gm = null + +/datum/gm_action/New(var/datum/game_master/new_gm) + ..() + gm = new_gm + +/datum/gm_action/proc/set_up() + return + +/datum/gm_action/proc/get_weight() + return + +/datum/gm_action/proc/start() + if(!reusable) + enabled = FALSE + return + +/datum/gm_action/proc/end() + return + +/datum/gm_action/proc/announce() + return \ No newline at end of file diff --git a/code/modules/gamemaster/actions/comms_blackout.dm b/code/modules/gamemaster/actions/comms_blackout.dm new file mode 100644 index 0000000000..77814c3f93 --- /dev/null +++ b/code/modules/gamemaster/actions/comms_blackout.dm @@ -0,0 +1,6 @@ +// Comms blackout is, just like grid check, mostly the same as always, yet engineering has an option to get it back sooner. + +/datum/gm_action/comms_blackout + name = "communications blackout" + departments = list(ROLE_ENGINEERING, ROLE_EVERYONE) + chaotic = 35 \ No newline at end of file diff --git a/code/modules/gamemaster/actions/grid_check.dm b/code/modules/gamemaster/actions/grid_check.dm new file mode 100644 index 0000000000..f46f0b162d --- /dev/null +++ b/code/modules/gamemaster/actions/grid_check.dm @@ -0,0 +1,9 @@ +// New grid check event: +// Very similar to the old one, power goes out in most of the colony, however the new feature is the ability for engineering to +// get power back on sooner, if they are able to reach a special machine and initiate a manual reboot. If no one is able to do so, +// it will reboot itself after a few minutes, just like the old one. + +/datum/gm_action/grid_check + name = "grid check" + departments = list(ROLE_ENGINEERING, ROLE_EVERYONE) + chaotic = 20 \ No newline at end of file diff --git a/code/modules/gamemaster/actions/waste_disposal.dm b/code/modules/gamemaster/actions/waste_disposal.dm new file mode 100644 index 0000000000..4edfba9a1e --- /dev/null +++ b/code/modules/gamemaster/actions/waste_disposal.dm @@ -0,0 +1,6 @@ +// A shuttle full of junk docks, and cargo is tasked with sifting through it all to find valuables, or just dispose of it. + +/datum/gm_action/waste_disposal + name = "waste disposal" + departments = list(ROLE_CARGO) + chaotic = 0 \ No newline at end of file diff --git a/code/modules/gamemaster/controller.dm b/code/modules/gamemaster/controller.dm new file mode 100644 index 0000000000..abff64ea11 --- /dev/null +++ b/code/modules/gamemaster/controller.dm @@ -0,0 +1,40 @@ +/client/proc/show_gm_status() + set category = "Debug" + set name = "Show GM Status" + set desc = "Shows you what the GM is thinking. If only that existed in real life..." + + game_master.interact(usr) + +/datum/game_master/proc/interact(var/client/user) + if(!user) + return + + var/HTML = "Game Master AI" + + HTML += "Staleness: [staleness]
" + HTML += "Danger: [danger]

" + + HTML += "Actions available;
" + for(var/datum/gm_action/action in available_actions) + if(action.enabled == FALSE) + continue + HTML += "[action.name] ([english_list(action.departments)])
" + + HTML += "
" + HTML += "All living mobs activity: [assess_all_living_mobs()]
" + + HTML += "
" + HTML += "Departmental activity;
" + for(var/department in departments) + var/number_of_people = count_people_in_department(department) + HTML += " [department] : [assess_department(department)] / [number_of_people * 100]
" + + HTML += "
" + HTML += "Activity of players;
" + for(var/mob/player in player_list) + HTML += " [player] : [assess_player_activity(player)]
" + + + + HTML +="" + user << browse(HTML, "window=log;size=400x450;border=1;can_resize=1;can_close=1;can_minimize=1") \ No newline at end of file diff --git a/code/modules/gamemaster/defines.dm b/code/modules/gamemaster/defines.dm new file mode 100644 index 0000000000..796eb014f8 --- /dev/null +++ b/code/modules/gamemaster/defines.dm @@ -0,0 +1,10 @@ +#define ROLE_COMMAND "command" +#define ROLE_SECURITY "security" +#define ROLE_ENGINEERING "engineering" +#define ROLE_MEDICAL "medical" +#define ROLE_RESEARCH "research" +#define ROLE_CARGO "cargo" +#define ROLE_CIVILIAN "civilian" +#define ROLE_SYNTHETIC "synthetic" +#define ROLE_UNKNOWN "unknown" +#define ROLE_EVERYONE "everyone" \ No newline at end of file diff --git a/code/modules/gamemaster/game_master.dm b/code/modules/gamemaster/game_master.dm new file mode 100644 index 0000000000..e21b9631bf --- /dev/null +++ b/code/modules/gamemaster/game_master.dm @@ -0,0 +1,207 @@ +// This is a sort of successor to the various event systems created over the years. It is designed to be just a tad smarter than the +// previous ones, checking various things like player count, department size and composition, individual player activity, +// individual player (IC) skill, and such, in order to try to choose the best actions to take in order to add spice or variety to +// the round. + +/datum/game_master + var/suspended = FALSE // If true, it will not do anything. + var/list/available_actions = list() // A list of 'actions' that the GM has access to, to spice up a round, such as events. + var/danger = 0 // The GM's best guess at how chaotic the round is. High danger makes it hold back. + var/staleness = -20 // Determines liklihood of the GM doing something, increases over time. + var/danger_modifier = 1 // Multiplier for how much 'danger' is accumulated. + var/staleness_modifier = 1 // Ditto. Higher numbers generally result in more events occuring in a round. + var/ticks_completed = 0 // Counts amount of ticks completed. Note that this ticks once a minute. + var/next_action = 0 // Minimum amount of time of nothingness until the GM can pick something again. + var/departments = list( // List of departments the GM considers for choosing events for. + ROLE_COMMAND, + ROLE_SECURITY, + ROLE_ENGINEERING, + ROLE_MEDICAL, + ROLE_RESEARCH, + ROLE_CARGO, + ROLE_CIVILIAN, + ROLE_SYNTHETIC + ) + +/datum/game_master/New() + ..() + available_actions = init_subtypes(/datum/gm_action) +// var/actions = typesof(/datum/gm_actions) +// for(var/type in actions) +// available_actions.Add(new type) + +/datum/game_master/proc/process() + if(ticker && ticker.current_state == GAME_STATE_PLAYING) + adjust_staleness(1) + adjust_danger(-1) + ticks_completed++ + + var/global_afk = assess_all_living_mobs() + global_afk -= 100 + global_afk = abs(global_afk) + global_afk = round(global_afk / 100, 0.1) + adjust_staleness(global_afk) // Staleness increases faster if more people are less active. + + if(world.time < next_action && prob(staleness * 2) ) + log_debug("Game Master going to start something.") + start_action() + +/datum/game_master/proc/assess_all_living_mobs() + var/num = 0 + for(var/mob/living/L in player_list) // Ghosts being AFK isn't that much of a concern. + . += assess_player_activity(L) + num++ + if(num) + . = round(. / num, 0.1) + +// This is run before committing to an action/event. +/datum/game_master/proc/pre_action_checks() + if(!ticker || ticker.current_state != GAME_STATE_PLAYING) + log_debug("Game Master unable to start event: Ticker is nonexistant, or the game is not ongoing.") + return FALSE + // Last minute antagging is bad for humans to do, so the GM will respect the start and end of the round. + var/mills = round_duration_in_ticks + var/mins = round((mills % 36000) / 600) + var/hours = round(mills / 36000) + + if(hours < 1 && mins <= 20) // Don't do anything for the first twenty minutes of the round. + log_debug("Game Master unable to start event: It is too early.") + return FALSE + if(hours >= 2 && mins >= 40) // Don't do anything in the last twenty minutes of the round, as well. + log_debug("Game Master unable to start event: It is too late.") + return FALSE + return TRUE + +/datum/game_master/proc/start_action() + if(!pre_action_checks()) // Make sure we're not doing last minute events, or early events. + return + log_debug("Game Master now starting action decision.") + var/list/best_actions = assess_round() // Checks the whole round for active people, and returns a list of the most activie departments. + if(best_actions && best_actions.len) + var/datum/gm_action/choice = pick(best_actions) + if(choice) +// log_debug("[choice.name] was chosen by the Game Master, and is now being ran.") +// choice.set_up() +// choice.start() +// choice.annnounce() + next_action = world.time + rand(15 MINUTES, 30 MINUTES) + +/datum/game_master/proc/assess_round() + var/list/activity = list() + for(var/department in departments) + activity[department] = assess_department(department) + log_debug("Assessing department [department]. They have activity of [activity[department]].") + + var/list/most_active_departments = list() // List of winners. + var/highest_activity = null // Department who is leading in activity, if one exists. + var/highest_number = 0 // Activity score needed to beat to be the most active department. + for(var/i = 1, i <= 3, i++) + log_debug("Doing [i]\th round of counting.") + for(var/department in activity) + if(activity[department] > highest_number && activity[department] > 0) // More active than the current highest department? + highest_activity = department + highest_number = activity[department] + + if(highest_activity) // Someone's a winner. + most_active_departments.Add(highest_activity) // Add to the list of most active. + activity.Remove(highest_activity) // Remove them from the other list so they don't win more than once. + log_debug("[highest_activity] has won the [i]\th round of activity counting.") + highest_activity = null // Now reset for the next round. + highest_number = 0 + //todo: finish + var/list/best_actions = decide_best_action(most_active_departments) + return best_actions + + // By now, we should have a list of departments populated. The GM will prefer events tailored to these departments. + + + + +/datum/game_master/proc/decide_best_action(var/list/most_active_departments) + if(!most_active_departments.len) // Server's empty? + log_debug("Game Master failed to find any active departments.") + return list() + + var/list/best_actions = list() // List of actions which involve the most active departments. + if(most_active_departments.len >= 2) + for(var/datum/gm_action/action in available_actions) + if(!action.enabled) + continue + // Try to incorporate an action with the top two departments first. + if(most_active_departments[1] in action.departments && most_active_departments[2] in action.departments) + best_actions.Add(action) + log_debug("[action.name] is being considered because both most active departments are involved.") + + if(best_actions.len) // We found something for those two, let's do it. + return best_actions + + // Otherwise we probably couldn't find something for the second highest group, so let's ignore them. + for(var/datum/gm_action/action in available_actions) + if(!action.enabled) + continue + if(most_active_departments[1] in action.departments) + best_actions.Add(action) + log_debug("[action.name] is being considered because the most active department is involved.") + + if(best_actions.len) // Found something for the one guy. + return best_actions + + // At this point we should expand our horizons. + for(var/datum/gm_action/action in available_actions) + if(!action.enabled) + continue + if(ROLE_EVERYONE in action.departments) + best_actions.Add(action) + log_debug("[action.name] is being considered because it involves everyone.") + + if(best_actions.len) // Finally, perhaps? + return best_actions + + // Just give a random event if for some reason it still can't make up its mind. + for(var/datum/gm_action/action in available_actions) + if(!action.enabled) + continue + best_actions.Add(action) + log_debug("[action.name] is being considered because everything else failed.") + + if(best_actions.len) // Finally, perhaps? + return best_actions + else + log_debug("Game Master failed to find a suitable event, something very wrong is going on.") + +// This checks a whole department's viability to receive an event. +/datum/game_master/proc/assess_department(var/department) + if(!department) + return + var/departmental_activitiy = 0 + for(var/mob/M in player_list) + if(guess_department(M) != department) // Ignore people outside the department we're assessing. + continue + departmental_activitiy += assess_player_activity(M) + return departmental_activitiy + +// This checks an individual player's activity level. People who have been afk for a few minutes aren't punished as much as those +// who were afk for hours, as they're most likely gone for good. +/datum/game_master/proc/assess_player_activity(var/mob/M) + . = 100 + if(!M) + . = 0 + return + + if(!M.mind || !M.client) // Logged out. They might come back but we can't do any meaningful assessments for now. + . = 0 + return + + var/afk = M.client.is_afk(1 MINUTE) + if(afk) // Deduct points based on length of AFK-ness. + switch(afk) // One minute is equal to 600, for reference. + if(1 MINUTE to 10 MINUTES) // People gone for this emough of time hopefully will come back soon. + . -= round( (afk / 200), 1) + // . -= 30 + if(10 MINUTES to 30 MINUTES) + . -= round( (afk / 150), 1) + // . -= 70 + if(30 MINUTES to INFINITY) // They're probably not coming back if it's been 30 minutes. + . -= 100 + . = max(. , 0) // No negative numbers, or else people could drag other, non-afk players down. + diff --git a/code/modules/gamemaster/helpers.dm b/code/modules/gamemaster/helpers.dm new file mode 100644 index 0000000000..a0972207a3 --- /dev/null +++ b/code/modules/gamemaster/helpers.dm @@ -0,0 +1,72 @@ +// Tell the game master that something dangerous happened, e.g. someone dying. +/datum/game_master/proc/adjust_danger(var/amt) + amt = amt * danger_modifier + danger = round( Clamp(danger + amt, 0, 1000), 0.1) + +// Tell the game master that something interesting happened. +/datum/game_master/proc/adjust_staleness(var/amt) + amt = amt * staleness_modifier + staleness = round( Clamp(staleness + amt, -50, 200), 0.1) + +// This proc tries to find the department of an arbitrary mob. +/datum/game_master/proc/guess_department(var/mob/M) + var/datum/data/record/R = find_general_record("name", M.real_name) + . = ROLE_UNKNOWN + if(R) // We found someone with a record. + var/recorded_rank = R.fields["real_rank"] + . = role_name_to_department(recorded_rank) + if(. != ROLE_UNKNOWN) // We found the correct department, so we can stop now. + return + + // They have a custom title, aren't crew, or someone deleted their record, so we need a fallback method. + // Let's check the mind. + if(M.mind) + . = role_name_to_department(M.mind.assigned_role) + if(. != ROLE_UNKNOWN) + return + + // At this point, they don't have a mind, or for some reason assigned_role didn't work. + if(ishuman(M)) + var/mob/living/carbon/human/H = M + . = role_name_to_department(H.job) + if(. != ROLE_UNKNOWN) + return + + return ROLE_UNKNOWN // Welp. + + +// Feed this proc the name of a job, and it will try to figure out what department they are apart of. +/datum/game_master/proc/role_name_to_department(var/role_name) + if(role_name in security_positions) + return ROLE_SECURITY + + if(role_name in engineering_positions) + return ROLE_ENGINEERING + + if(role_name in medical_positions) + return ROLE_MEDICAL + + if(role_name in science_positions) + return ROLE_RESEARCH + + if(role_name in cargo_positions) + return ROLE_CARGO + + if(role_name in civilian_positions) + return ROLE_CIVILIAN + + if(role_name in nonhuman_positions) + return ROLE_SYNTHETIC + + if(role_name in command_positions) // We do command last, so that only the Captain and command secretaries get caught. + return ROLE_COMMAND + + return ROLE_UNKNOWN + +/datum/game_master/proc/count_people_in_department(var/department) + if(!department) + return + for(var/mob/M in player_list) + if(guess_department(M) != department) // Ignore people outside the department we're counting. + continue + . += 1 \ No newline at end of file diff --git a/polaris.dme b/polaris.dme index 16fd036728..b52b6bc082 100644 --- a/polaris.dme +++ b/polaris.dme @@ -140,6 +140,7 @@ #include "code\controllers\Processes\chemistry.dm" #include "code\controllers\Processes\emergencyShuttle.dm" #include "code\controllers\Processes\event.dm" +#include "code\controllers\Processes\game_master.dm" #include "code\controllers\Processes\garbage.dm" #include "code\controllers\Processes\inactivity.dm" #include "code\controllers\Processes\machinery.dm" @@ -1334,6 +1335,14 @@ #include "code\modules\flufftext\Hallucination.dm" #include "code\modules\flufftext\TextFilters.dm" #include "code\modules\food\recipes_microwave.dm" +#include "code\modules\gamemaster\controller.dm" +#include "code\modules\gamemaster\defines.dm" +#include "code\modules\gamemaster\game_master.dm" +#include "code\modules\gamemaster\helpers.dm" +#include "code\modules\gamemaster\actions\action.dm" +#include "code\modules\gamemaster\actions\comms_blackout.dm" +#include "code\modules\gamemaster\actions\grid_check.dm" +#include "code\modules\gamemaster\actions\waste_disposal.dm" #include "code\modules\games\cah.dm" #include "code\modules\games\cah_black_cards.dm" #include "code\modules\games\cah_white_cards.dm"