mirror of
https://github.com/CHOMPStation2/CHOMPStation2.git
synced 2025-12-10 18:22:39 +00:00
Cleans Up WIP Event System + New Grid Check For It
Separates the 'count and assess everything' stuff to it's own datum, called the metric datum, which I plan to add on to in the future to make counting and metrics easier. Makes decision process a bit more weight-based, will probably continue tweaking later. Makes the admin debug UI have links to change settings easily. Adds replacement for grid check event, which works similar to the old one, but is now based on a physical machine in the game world, that Engineering can hack to make the event end faster, if so desired. Note that the machine is not mapped in, and won't be mapped in until the event system is ready for launch. Adds grid_check variables to SMESes and APCs to make them stop doing work without draining the battery. Grid checks in the new system are caused by a "power spike" which originates from the engine and will cause bad things, should no grid checker machine be connected to the power-net. These power spikes occur when the GM decides that a grid check is a good event to have. The grid checker can be built and deconstructed using the standard machine construction methods.
This commit is contained in:
@@ -167,3 +167,15 @@
|
||||
#define ANTAG_HIDDEN "Hidden"
|
||||
#define ANTAG_SHARED "Shared"
|
||||
#define ANTAG_KNOWN "Known"
|
||||
|
||||
// Job groups
|
||||
#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"
|
||||
|
||||
66
code/datums/wires/grid_checker.dm
Normal file
66
code/datums/wires/grid_checker.dm
Normal file
@@ -0,0 +1,66 @@
|
||||
/datum/wires/grid_checker
|
||||
holder_type = /obj/machinery/power/grid_checker
|
||||
wire_count = 8
|
||||
|
||||
var/const/GRID_CHECKER_WIRE_REBOOT = 1 // This wire causes the grid-check to end, if pulsed.
|
||||
var/const/GRID_CHECKER_WIRE_LOCKOUT = 2 // If cut or pulsed, locks the user out for half a minute.
|
||||
var/const/GRID_CHECKER_WIRE_ALLOW_MANUAL_1 = 4 // Needs to be cut for REBOOT to be possible.
|
||||
var/const/GRID_CHECKER_WIRE_ALLOW_MANUAL_2 = 8 // Needs to be cut for REBOOT to be possible.
|
||||
var/const/GRID_CHECKER_WIRE_ALLOW_MANUAL_3 = 16 // Needs to be cut for REBOOT to be possible.
|
||||
var/const/GRID_CHECKER_WIRE_SHOCK = 32 // Shocks the user if not wearing gloves.
|
||||
var/const/GRID_CHECKER_WIRE_NOTHING_1 = 64 // Does nothing, but makes it a bit harder.
|
||||
var/const/GRID_CHECKER_WIRE_NOTHING_2 = 128 // Does nothing, but makes it a bit harder.
|
||||
|
||||
|
||||
/datum/wires/grid_checker/CanUse(var/mob/living/L)
|
||||
var/obj/machinery/power/grid_checker/G = holder
|
||||
if(G.opened)
|
||||
return TRUE
|
||||
return FALSE
|
||||
|
||||
|
||||
/datum/wires/grid_checker/GetInteractWindow()
|
||||
var/obj/machinery/power/grid_checker/G = holder
|
||||
. += ..()
|
||||
. += "The green light is [G.power_failing ? "off" : "on"].<br>"
|
||||
. += "The red light is [G.wire_locked_out ? "on" : "off"].<br>"
|
||||
. += "The blue light is [G.wire_allow_manual_1 && G.wire_allow_manual_2 && G.wire_allow_manual_3 ? "on" : "off"]."
|
||||
|
||||
|
||||
/datum/wires/grid_checker/UpdateCut(var/index, var/mended)
|
||||
var/obj/machinery/power/grid_checker/G = holder
|
||||
switch(index)
|
||||
if(GRID_CHECKER_WIRE_LOCKOUT)
|
||||
G.wire_locked_out = !mended
|
||||
if(GRID_CHECKER_WIRE_ALLOW_MANUAL_1)
|
||||
G.wire_allow_manual_1 = !mended
|
||||
if(GRID_CHECKER_WIRE_ALLOW_MANUAL_2)
|
||||
G.wire_allow_manual_2 = !mended
|
||||
if(GRID_CHECKER_WIRE_ALLOW_MANUAL_3)
|
||||
G.wire_allow_manual_3 = !mended
|
||||
if(GRID_CHECKER_WIRE_SHOCK)
|
||||
if(G.wire_locked_out)
|
||||
return
|
||||
G.shock(usr, 70)
|
||||
|
||||
|
||||
/datum/wires/grid_checker/UpdatePulsed(var/index)
|
||||
var/obj/machinery/power/grid_checker/G = holder
|
||||
switch(index)
|
||||
if(GRID_CHECKER_WIRE_REBOOT)
|
||||
if(G.wire_locked_out)
|
||||
return
|
||||
|
||||
if(G.power_failing && G.wire_allow_manual_1 && G.wire_allow_manual_2 && G.wire_allow_manual_3)
|
||||
G.end_power_failure(TRUE)
|
||||
if(GRID_CHECKER_WIRE_LOCKOUT)
|
||||
if(G.wire_locked_out)
|
||||
return
|
||||
|
||||
G.wire_locked_out = TRUE
|
||||
spawn(30 SECONDS)
|
||||
G.wire_locked_out = FALSE
|
||||
if(GRID_CHECKER_WIRE_SHOCK)
|
||||
if(G.wire_locked_out)
|
||||
return
|
||||
G.shock(usr, 70)
|
||||
@@ -22,3 +22,10 @@
|
||||
build_path = /obj/machinery/power/smes/batteryrack/makeshift
|
||||
board_type = new /datum/frame/frame_types/machine
|
||||
req_components = list(/obj/item/weapon/cell = 3)
|
||||
|
||||
/obj/item/weapon/circuitboard/grid_checker
|
||||
name = T_BOARD("power grid checker")
|
||||
build_path = /obj/machinery/power/grid_checker
|
||||
board_type = new /datum/frame/frame_types/machine
|
||||
origin_tech = list(TECH_POWER = 4, TECH_ENGINEERING = 3)
|
||||
req_components = list(/obj/item/weapon/stock_parts/capacitor = 3, /obj/item/stack/cable_coil = 10)
|
||||
|
||||
@@ -119,6 +119,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/datum/metric/metric = new() // Metric datum, used to keep track of the round.
|
||||
|
||||
var/list/awaydestinations = list() // Away missions. A list of landmarks that the warpgate can take you to.
|
||||
|
||||
|
||||
@@ -4,3 +4,6 @@
|
||||
name = "communications blackout"
|
||||
departments = list(ROLE_ENGINEERING, ROLE_EVERYONE)
|
||||
chaotic = 35
|
||||
|
||||
/datum/gm_action/comms_blackout/get_weight()
|
||||
return 50 + (metric.count_people_in_department(ROLE_ENGINEERING) * 40)
|
||||
@@ -1,9 +1,22 @@
|
||||
// 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.
|
||||
// it will reboot itself after a few minutes, just like the old one. Bad things happen if there is no grid checker machine protecting
|
||||
// the powernet when this event fires.
|
||||
|
||||
/datum/gm_action/grid_check
|
||||
name = "grid check"
|
||||
departments = list(ROLE_ENGINEERING, ROLE_EVERYONE)
|
||||
chaotic = 20
|
||||
|
||||
/datum/gm_action/grid_check/get_weight()
|
||||
return 50 + (metric.count_people_in_department(ROLE_ENGINEERING) * 30)
|
||||
|
||||
/datum/gm_action/grid_check/start()
|
||||
// This sets off a chain of events that lead to the actual grid check (or perhaps worse).
|
||||
// First, the Supermatter engine makes a power spike.
|
||||
for(var/obj/machinery/power/generator/engine in machines)
|
||||
engine.power_spike()
|
||||
break // Just one engine, please.
|
||||
// After that, the engine checks if a grid checker exists on the same powernet, and if so, it triggers a blackout.
|
||||
// If not, lots of stuff breaks. See code/modules/power/generator.dm for that piece of code.
|
||||
@@ -4,3 +4,6 @@
|
||||
name = "waste disposal"
|
||||
departments = list(ROLE_CARGO)
|
||||
chaotic = 0
|
||||
|
||||
/datum/gm_action/waste_disposal/get_weight()
|
||||
return metric.count_people_in_department(ROLE_CARGO) * 50
|
||||
@@ -11,30 +11,70 @@
|
||||
|
||||
var/HTML = "<html><head><title>Game Master AI</title></head><body>"
|
||||
|
||||
HTML += "Staleness: [staleness]<br>"
|
||||
HTML += "Danger: [danger]<br><br>"
|
||||
HTML += "<a href='?src=\ref[src];toggle_time_restrictions=1'>\[Toggle Time Restrictions\]</a> | \
|
||||
<a href='?src=\ref[src];suspend=1'>\[Toggle GM\]</a> | \
|
||||
<a href='?src=\ref[src];force_choose_event=1'>\[Force Event Decision\]</a><br>"
|
||||
|
||||
HTML += "Status: [pre_action_checks() ? "Ready" : "Suppressed"]<br><br>"
|
||||
|
||||
HTML += "Staleness: [staleness] <a href='?src=\ref[src];adjust_staleness=1'>\[Adjust\]</a><br>"
|
||||
HTML += "Danger: [danger] <a href='?src=\ref[src];adjust_danger=1'>\[Adjust\]</a><br><br>"
|
||||
|
||||
HTML += "Actions available;<br>"
|
||||
for(var/datum/gm_action/action in available_actions)
|
||||
if(action.enabled == FALSE)
|
||||
continue
|
||||
HTML += "[action.name] ([english_list(action.departments)])<br>"
|
||||
HTML += "[action.name] ([english_list(action.departments)]) (weight: [action.get_weight()])<br>"
|
||||
|
||||
HTML += "<br>"
|
||||
HTML += "All living mobs activity: [assess_all_living_mobs()]<br>"
|
||||
HTML += "All living mobs activity: [metric.assess_all_living_mobs()]%<br>"
|
||||
HTML += "All ghost activity: [metric.assess_all_dead_mobs()]%<br>"
|
||||
|
||||
HTML += "<br>"
|
||||
HTML += "Departmental activity;<br>"
|
||||
for(var/department in departments)
|
||||
var/number_of_people = count_people_in_department(department)
|
||||
HTML += " [department] : [assess_department(department)] / [number_of_people * 100]<br>"
|
||||
for(var/department in metric.departments)
|
||||
HTML += " [department] : [metric.assess_department(department)]%<br>"
|
||||
|
||||
HTML += "<br>"
|
||||
HTML += "Activity of players;<br>"
|
||||
for(var/mob/player in player_list)
|
||||
HTML += " [player] : [assess_player_activity(player)]<br>"
|
||||
HTML += " [player] ([player.key]) : [metric.assess_player_activity(player)]%<br>"
|
||||
|
||||
|
||||
|
||||
HTML +="</body></html>"
|
||||
user << browse(HTML, "window=log;size=400x450;border=1;can_resize=1;can_close=1;can_minimize=1")
|
||||
|
||||
/datum/game_master/Topic(href, href_list)
|
||||
if(..())
|
||||
return
|
||||
|
||||
if(!is_admin(usr))
|
||||
message_admins("[usr] has attempted to modify the Game Master values without being an admin.")
|
||||
return
|
||||
|
||||
if(href_list["toggle_time_restrictions"])
|
||||
ignore_time_restrictions = !ignore_time_restrictions
|
||||
message_admins("GM event time restrictions was [ignore_time_restrictions ? "dis" : "en"]abled by [usr.key].")
|
||||
|
||||
if(href_list["force_choose_event"])
|
||||
start_action()
|
||||
message_admins("[usr.key] forced the Game Master to choose an event immediately.")
|
||||
|
||||
if(href_list["suspend"])
|
||||
suspended = !suspended
|
||||
message_admins("GM was [suspended ? "dis" : "en"]abled by [usr.key].")
|
||||
|
||||
if(href_list["adjust_staleness"])
|
||||
var/amount = input(usr, "How much staleness should be added or subtracted?", "Game Master") as null|num
|
||||
if(amount)
|
||||
adjust_staleness(amount)
|
||||
message_admins("GM staleness was adjusted by [amount] by [usr.key].")
|
||||
|
||||
if(href_list["adjust_danger"])
|
||||
var/amount = input(usr, "How much danger should be added or subtracted?", "Game Master") as null|num
|
||||
if(amount)
|
||||
adjust_danger(amount)
|
||||
message_admins("GM danger was adjusted by [amount] by [usr.key].")
|
||||
|
||||
interact(usr) // To refresh the UI.
|
||||
@@ -1,10 +1 @@
|
||||
#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"
|
||||
#define EVENT_BASELINE_WEIGHT 200
|
||||
@@ -4,7 +4,8 @@
|
||||
// the round.
|
||||
|
||||
/datum/game_master
|
||||
var/suspended = FALSE // If true, it will not do anything.
|
||||
var/suspended = TRUE // If true, it will not do anything.
|
||||
var/ignore_time_restrictions = FALSE// Useful for debugging without needing to wait 20 minutes each time.
|
||||
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.
|
||||
@@ -12,31 +13,20 @@
|
||||
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
|
||||
)
|
||||
var/last_department_used = null // If an event was done for a specific department, it is written here, so it doesn't do it again.
|
||||
|
||||
|
||||
/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)
|
||||
if(ticker && ticker.current_state == GAME_STATE_PLAYING && !suspended)
|
||||
adjust_staleness(1)
|
||||
adjust_danger(-1)
|
||||
ticks_completed++
|
||||
|
||||
var/global_afk = assess_all_living_mobs()
|
||||
var/global_afk = metric.assess_all_living_mobs()
|
||||
global_afk -= 100
|
||||
global_afk = abs(global_afk)
|
||||
global_afk = round(global_afk / 100, 0.1)
|
||||
@@ -46,19 +36,15 @@
|
||||
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
|
||||
if(suspended)
|
||||
return FALSE
|
||||
if(ignore_time_restrictions)
|
||||
return TRUE
|
||||
// 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)
|
||||
@@ -76,43 +62,21 @@
|
||||
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/most_active_departments = metric.assess_all_departments(3, list(last_department_used))
|
||||
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.
|
||||
if(best_actions && best_actions.len)
|
||||
var/list/weighted_actions = list()
|
||||
for(var/datum/gm_action/action in best_actions)
|
||||
weighted_actions[action] = action.get_weight()
|
||||
|
||||
var/datum/gm_action/choice = pickweight(weighted_actions)
|
||||
if(choice)
|
||||
log_debug("[choice.name] was chosen by the Game Master, and is now being ran.")
|
||||
choice.set_up()
|
||||
choice.start()
|
||||
next_action = world.time + rand(15 MINUTES, 30 MINUTES)
|
||||
last_department_used = choice.departments[1]
|
||||
|
||||
|
||||
|
||||
@@ -169,39 +133,4 @@
|
||||
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.
|
||||
|
||||
|
||||
@@ -7,66 +7,3 @@
|
||||
/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
|
||||
82
code/modules/metric/activity.dm
Normal file
82
code/modules/metric/activity.dm
Normal file
@@ -0,0 +1,82 @@
|
||||
// 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/metric/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)
|
||||
if(10 MINUTES to 30 MINUTES)
|
||||
. -= round( (afk / 150), 1)
|
||||
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.
|
||||
|
||||
// This checks a whole department's collective activity.
|
||||
/datum/metric/proc/assess_department(var/department)
|
||||
if(!department)
|
||||
return
|
||||
var/departmental_activity = 0
|
||||
var/departmental_size = 0
|
||||
for(var/mob/M in player_list)
|
||||
if(guess_department(M) != department) // Ignore people outside the department we're assessing.
|
||||
continue
|
||||
departmental_activity += assess_player_activity(M)
|
||||
departmental_size++
|
||||
if(departmental_size)
|
||||
departmental_activity = departmental_activity / departmental_size // Average it out.
|
||||
return departmental_activity
|
||||
|
||||
/datum/metric/proc/assess_all_departments(var/cutoff_number = 3, var/list/department_blacklist = list())
|
||||
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 <= cutoff_number, i++)
|
||||
log_debug("Doing [i]\th round of counting.")
|
||||
for(var/department in activity)
|
||||
if(department in department_blacklist) // Blacklisted?
|
||||
continue
|
||||
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
|
||||
return most_active_departments
|
||||
|
||||
/datum/metric/proc/assess_all_living_mobs() // Living refers to the type, not the stat variable.
|
||||
. = 0
|
||||
var/num = 0
|
||||
for(var/mob/living/L in player_list)
|
||||
. += assess_player_activity(L)
|
||||
num++
|
||||
if(num)
|
||||
. = round(. / num, 0.1)
|
||||
|
||||
/datum/metric/proc/assess_all_dead_mobs() // Ditto.
|
||||
. = 0
|
||||
var/num = 0
|
||||
for(var/mob/observer/dead/O in player_list)
|
||||
. += assess_player_activity(O)
|
||||
num++
|
||||
if(num)
|
||||
. = round(. / num, 0.1)
|
||||
72
code/modules/metric/department.dm
Normal file
72
code/modules/metric/department.dm
Normal file
@@ -0,0 +1,72 @@
|
||||
|
||||
// This proc tries to find the department of an arbitrary mob.
|
||||
/datum/metric/proc/guess_department(var/mob/M)
|
||||
var/list/found_roles = list()
|
||||
. = ROLE_UNKNOWN
|
||||
|
||||
// Records are usually the most reliable way to get what job someone is.
|
||||
var/datum/data/record/R = find_general_record("name", M.real_name)
|
||||
if(R) // We found someone with a record.
|
||||
var/recorded_rank = R.fields["real_rank"]
|
||||
found_roles = role_name_to_department(recorded_rank)
|
||||
. = found_roles[1]
|
||||
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)
|
||||
found_roles = role_name_to_department(M.mind.assigned_role)
|
||||
. = found_roles[1]
|
||||
if(. != ROLE_UNKNOWN)
|
||||
return
|
||||
|
||||
// At this point, they don't have a mind, or for some reason assigned_role didn't work.
|
||||
found_roles = role_name_to_department(M.job)
|
||||
. = found_roles[1]
|
||||
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.
|
||||
// Note that this returns a list, as some jobs are in more than one department, like Command. The 'primary' department is the first
|
||||
// in the list, e.g. a HoS has Security as first, Command as second in the returned list.
|
||||
/datum/metric/proc/role_name_to_department(var/role_name)
|
||||
var/list/result = list()
|
||||
|
||||
if(role_name in security_positions)
|
||||
result += ROLE_SECURITY
|
||||
|
||||
if(role_name in engineering_positions)
|
||||
result += ROLE_ENGINEERING
|
||||
|
||||
if(role_name in medical_positions)
|
||||
result += ROLE_MEDICAL
|
||||
|
||||
if(role_name in science_positions)
|
||||
result += ROLE_RESEARCH
|
||||
|
||||
if(role_name in cargo_positions)
|
||||
result += ROLE_CARGO
|
||||
|
||||
if(role_name in civilian_positions)
|
||||
result += ROLE_CIVILIAN
|
||||
|
||||
if(role_name in nonhuman_positions)
|
||||
result += ROLE_SYNTHETIC
|
||||
|
||||
if(role_name in command_positions) // We do Command last, since we consider command to only be a primary department for hop/admin.
|
||||
result += ROLE_COMMAND
|
||||
|
||||
if(!result.len) // No department was found.
|
||||
result += ROLE_UNKNOWN
|
||||
return result
|
||||
|
||||
/datum/metric/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
|
||||
15
code/modules/metric/metric.dm
Normal file
15
code/modules/metric/metric.dm
Normal file
@@ -0,0 +1,15 @@
|
||||
// This is a global datum used to retrieve certain information about the round, such as activity of a department or a specific
|
||||
// player.
|
||||
|
||||
/datum/metric
|
||||
var/departments = list(
|
||||
ROLE_COMMAND,
|
||||
ROLE_SECURITY,
|
||||
ROLE_ENGINEERING,
|
||||
ROLE_MEDICAL,
|
||||
ROLE_RESEARCH,
|
||||
ROLE_CARGO,
|
||||
ROLE_CIVILIAN,
|
||||
ROLE_SYNTHETIC
|
||||
)
|
||||
|
||||
@@ -68,6 +68,7 @@
|
||||
var/cell_type = /obj/item/weapon/cell/apc
|
||||
var/opened = 0 //0=closed, 1=opened, 2=cover removed
|
||||
var/shorted = 0
|
||||
var/grid_check = FALSE
|
||||
var/lighting = 3
|
||||
var/equipment = 3
|
||||
var/environ = 3
|
||||
@@ -796,7 +797,7 @@
|
||||
return "[area.name] : [equipment]/[lighting]/[environ] ([lastused_equip+lastused_light+lastused_environ]) : [cell? cell.percent() : "N/C"] ([charging])"
|
||||
|
||||
/obj/machinery/power/apc/proc/update()
|
||||
if(operating && !shorted)
|
||||
if(operating && !shorted && !grid_check)
|
||||
area.power_light = (lighting > 1)
|
||||
area.power_equip = (equipment > 1)
|
||||
area.power_environ = (environ > 1)
|
||||
@@ -1001,7 +1002,7 @@
|
||||
if(debug)
|
||||
log_debug("Status: [main_status] - Excess: [excess] - Last Equip: [lastused_equip] - Last Light: [lastused_light] - Longterm: [longtermpower]")
|
||||
|
||||
if(cell && !shorted)
|
||||
if(cell && !shorted && !grid_check)
|
||||
// draw power from cell as before to power the area
|
||||
var/cellused = min(cell.charge, CELLRATE * lastused_total) // clamp deduction to a max, amount left in cell
|
||||
cell.use(cellused)
|
||||
@@ -1196,7 +1197,7 @@ obj/machinery/power/apc/proc/autoset(var/val, var/on)
|
||||
// overload the lights in this APC area
|
||||
|
||||
/obj/machinery/power/apc/proc/overload_lighting(var/chance = 100)
|
||||
if(/* !get_connection() || */ !operating || shorted)
|
||||
if(/* !get_connection() || */ !operating || shorted || grid_check)
|
||||
return
|
||||
if( cell && cell.charge>=20)
|
||||
cell.use(20);
|
||||
@@ -1225,4 +1226,34 @@ obj/machinery/power/apc/proc/autoset(var/val, var/on)
|
||||
update_icon()
|
||||
return 1
|
||||
|
||||
/obj/machinery/power/apc/overload(var/obj/machinery/power/source)
|
||||
if(is_critical)
|
||||
return
|
||||
|
||||
if(prob(30)) // Nothing happens.
|
||||
return
|
||||
|
||||
if(prob(40)) // Lights blow.
|
||||
overload_lighting()
|
||||
|
||||
if(prob(40)) // Spooky flickers.
|
||||
for(var/obj/machinery/light/L in area)
|
||||
L.flicker(20)
|
||||
|
||||
if(prob(25)) // Bluescreens.
|
||||
emagged = 1
|
||||
locked = 0
|
||||
update_icon()
|
||||
|
||||
if(prob(25)) // Cell gets damaged.
|
||||
if(cell)
|
||||
cell.corrupt()
|
||||
|
||||
if(prob(10)) // Computers get broken.
|
||||
for(var/obj/machinery/computer/comp in area)
|
||||
comp.ex_act(3)
|
||||
|
||||
if(prob(5)) // APC completely ruined.
|
||||
set_broken()
|
||||
|
||||
#undef APC_UPDATE_ICON_COOLDOWN
|
||||
|
||||
@@ -234,3 +234,8 @@
|
||||
return
|
||||
|
||||
src.set_dir(turn(src.dir, -90))
|
||||
|
||||
/obj/machinery/power/generator/power_spike()
|
||||
if(effective_gen >= max_power / 2 && powernet) // Don't make a spike if we're not making a whole lot of power.
|
||||
..()
|
||||
|
||||
|
||||
125
code/modules/power/grid_checker.dm
Normal file
125
code/modules/power/grid_checker.dm
Normal file
@@ -0,0 +1,125 @@
|
||||
/obj/machinery/power/grid_checker
|
||||
name = "grid checker"
|
||||
desc = "A machine that reacts to unstable conditions in the powernet, by safely shutting everything down. Probably better \
|
||||
than the alternative."
|
||||
icon_state = "gridchecker_on"
|
||||
circuit = /obj/item/weapon/circuitboard/grid_checker
|
||||
var/power_failing = FALSE // Turns to TRUE when the grid check event is fired by the Game Master, or perhaps a cheeky antag.
|
||||
// Wire stuff below.
|
||||
var/datum/wires/grid_checker/wires
|
||||
var/wire_locked_out = FALSE
|
||||
var/wire_allow_manual_1 = FALSE
|
||||
var/wire_allow_manual_2 = FALSE
|
||||
var/wire_allow_manual_3 = FALSE
|
||||
var/opened = FALSE
|
||||
|
||||
/obj/machinery/power/grid_checker/New()
|
||||
..()
|
||||
connect_to_network()
|
||||
update_icon()
|
||||
wires = new(src)
|
||||
component_parts = list()
|
||||
component_parts += new /obj/item/weapon/stock_parts/capacitor(src)
|
||||
component_parts += new /obj/item/weapon/stock_parts/capacitor(src)
|
||||
component_parts += new /obj/item/weapon/stock_parts/capacitor(src)
|
||||
component_parts += new /obj/item/stack/cable_coil(src, 10)
|
||||
RefreshParts()
|
||||
|
||||
/obj/machinery/power/grid_checker/Destroy()
|
||||
qdel(wires)
|
||||
wires = null
|
||||
..()
|
||||
|
||||
/obj/machinery/power/grid_checker/update_icon()
|
||||
if(power_failing)
|
||||
icon_state = "gridchecker_off"
|
||||
set_light(2, 2, "#F86060")
|
||||
else
|
||||
icon_state = "gridchecker_on"
|
||||
set_light(2, 2, "#A8B0F8")
|
||||
|
||||
/obj/machinery/power/grid_checker/attackby(obj/item/W, mob/user)
|
||||
if(!user)
|
||||
return
|
||||
if(istype(W, /obj/item/weapon/screwdriver))
|
||||
default_deconstruction_screwdriver(user, W)
|
||||
opened = !opened
|
||||
else if(istype(W, /obj/item/weapon/crowbar))
|
||||
default_deconstruction_crowbar(user, W)
|
||||
else if(istype(W, /obj/item/device/multitool) || istype(W, /obj/item/weapon/wirecutters) )
|
||||
attack_hand(user)
|
||||
|
||||
/obj/machinery/power/grid_checker/attack_hand(mob/user)
|
||||
if(!user)
|
||||
return
|
||||
add_fingerprint(user)
|
||||
interact(user)
|
||||
|
||||
/obj/machinery/power/grid_checker/interact(mob/user)
|
||||
if(!user)
|
||||
return
|
||||
|
||||
if(opened)
|
||||
wires.Interact(user)
|
||||
|
||||
return ui_interact(user)
|
||||
|
||||
/obj/machinery/power/grid_checker/proc/power_failure(var/announce = TRUE)
|
||||
if(announce)
|
||||
command_announcement.Announce("Abnormal activity detected in [station_name()]'s powernet. As a precautionary measure, \
|
||||
the colony's power will be shut off for an indeterminate duration while the powernet monitor restarts automatically, or \
|
||||
when Engineering can manually resolve the issue.",
|
||||
"Critical Power Failure",
|
||||
new_sound = 'sound/AI/poweroff.ogg')
|
||||
power_failing = TRUE
|
||||
if(powernet)
|
||||
for(var/obj/machinery/power/terminal/T in powernet.nodes) // SMESes that are "downstream" of the powernet.
|
||||
|
||||
if(istype(T.master, /obj/machinery/power/apc))
|
||||
var/obj/machinery/power/apc/A = T.master
|
||||
if(A.is_critical)
|
||||
continue
|
||||
A.grid_check = TRUE
|
||||
|
||||
for(var/obj/machinery/power/smes/smes in powernet.nodes) // These are "upstream"
|
||||
smes.grid_check = TRUE
|
||||
/*
|
||||
smes.last_charge = smes.charge
|
||||
smes.last_output_attempt = smes.output_attempt
|
||||
smes.last_input_attempt = smes.input_attempt
|
||||
smes.charge = 0
|
||||
smes.inputting(FALSE)
|
||||
smes.outputting(FALSE)
|
||||
smes.update_icon()
|
||||
smes.power_change()
|
||||
*/
|
||||
update_icon()
|
||||
|
||||
spawn(rand(4 MINUTES, 10 MINUTES) )
|
||||
if(power_failing) // Check to see if engineering didn't beat us to it.
|
||||
end_power_failure(TRUE)
|
||||
|
||||
/obj/machinery/power/grid_checker/proc/end_power_failure(var/announce = TRUE)
|
||||
if(announce)
|
||||
command_announcement.Announce("Power has been restored to [station_name()]. We apologize for the inconvenience.",
|
||||
"Power Systems Nominal",
|
||||
new_sound = 'sound/AI/poweron.ogg')
|
||||
power_failing = FALSE
|
||||
update_icon()
|
||||
|
||||
for(var/obj/machinery/power/terminal/T in powernet.nodes)
|
||||
if(istype(T.master, /obj/machinery/power/apc))
|
||||
var/obj/machinery/power/apc/A = T.master
|
||||
if(A.is_critical)
|
||||
continue
|
||||
A.grid_check = FALSE
|
||||
|
||||
for(var/obj/machinery/power/smes/smes in powernet.nodes) // These are "upstream"
|
||||
smes.grid_check = FALSE
|
||||
/*
|
||||
smes.charge = smes.last_charge
|
||||
smes.output_attempt = smes.last_output_attempt
|
||||
smes.input_attempt = smes.last_input_attempt
|
||||
smes.update_icon()
|
||||
smes.power_change()
|
||||
*/
|
||||
@@ -138,6 +138,29 @@
|
||||
..()
|
||||
return
|
||||
|
||||
// Used for power spikes by the engine, has specific effects on different machines.
|
||||
/obj/machinery/power/proc/overload(var/obj/machinery/power/source)
|
||||
return
|
||||
|
||||
/obj/machinery/power/proc/power_spike()
|
||||
var/obj/machinery/power/grid_checker/G = locate() in powernet.nodes
|
||||
if(G) // If we found a grid checker, then all is well.
|
||||
G.power_failure(prob(30))
|
||||
else // Otherwise lets break some stuff.
|
||||
spawn(1)
|
||||
command_announcement.Announce("Dangerous power spike detected in the power network. Please check machinery \
|
||||
for electrical damage.",
|
||||
"Critical Power Overload")
|
||||
var/i = 0
|
||||
var/limit = rand(30, 50)
|
||||
for(var/obj/machinery/power/P in powernet.nodes)
|
||||
P.overload(src)
|
||||
i++
|
||||
if(i % 5)
|
||||
sleep(1)
|
||||
if(i >= limit)
|
||||
break
|
||||
|
||||
///////////////////////////////////////////
|
||||
// Powernet handling helpers
|
||||
//////////////////////////////////////////
|
||||
|
||||
@@ -44,6 +44,7 @@
|
||||
var/building_terminal = 0 //Suggestions about how to avoid clickspam building several terminals accepted!
|
||||
var/obj/machinery/power/terminal/terminal = null
|
||||
var/should_be_mapped = 0 // If this is set to 0 it will send out warning on New()
|
||||
var/grid_check = FALSE // If true, suspends all I/O.
|
||||
|
||||
/obj/machinery/power/smes/drain_power(var/drain_check, var/surge, var/amount = 0)
|
||||
|
||||
@@ -124,7 +125,7 @@
|
||||
var/last_onln = outputting
|
||||
|
||||
//inputting
|
||||
if(input_attempt && (!input_pulsed && !input_cut))
|
||||
if(input_attempt && (!input_pulsed && !input_cut) && !grid_check)
|
||||
var/target_load = min((capacity-charge)/SMESRATE, input_level) // charge at set rate, limited to spare capacity
|
||||
var/actual_load = draw_power(target_load) // add the load to the terminal side network
|
||||
charge += actual_load * SMESRATE // increase the charge
|
||||
@@ -137,7 +138,7 @@
|
||||
inputting = 0
|
||||
|
||||
//outputting
|
||||
if(outputting && (!output_pulsed && !output_cut))
|
||||
if(outputting && (!output_pulsed && !output_cut) && !grid_check)
|
||||
output_used = min( charge/SMESRATE, output_level) //limit output to that stored
|
||||
|
||||
charge -= output_used*SMESRATE // reduce the storage (may be recovered in /restore() if excessive)
|
||||
@@ -420,6 +421,11 @@
|
||||
update_icon()
|
||||
..()
|
||||
|
||||
/obj/machinery/power/smes/overload(var/obj/machinery/power/source) // This propagates the power spike down the powernet.
|
||||
if(istype(source, /obj/machinery/power/smes)) // Prevent infinite loops if two SMESes are hooked up to each other.
|
||||
return
|
||||
power_spike()
|
||||
|
||||
|
||||
/obj/machinery/power/smes/magical
|
||||
name = "magical power storage unit"
|
||||
|
||||
@@ -37,3 +37,7 @@
|
||||
// Powernet rebuilds need this to work properly.
|
||||
/obj/machinery/power/terminal/process()
|
||||
return 1
|
||||
|
||||
/obj/machinery/power/terminal/overload(var/obj/machinery/power/source)
|
||||
if(master)
|
||||
master.overload(source)
|
||||
|
||||
@@ -1038,6 +1038,14 @@ CIRCUITS BELOW
|
||||
build_path = /obj/item/weapon/circuitboard/smes
|
||||
sort_string = "JBABB"
|
||||
|
||||
/datum/design/circuit/grid_checker
|
||||
name = "power grid checker"
|
||||
desc = "Allows for the construction of circuit boards used to build a grid checker."
|
||||
id = "grid_checker"
|
||||
req_tech = list(TECH_POWER = 4, TECH_ENGINEERING = 3)
|
||||
build_path = /obj/item/weapon/circuitboard/grid_checker
|
||||
sort_string = "JBABC"
|
||||
|
||||
/datum/design/circuit/gas_heater
|
||||
name = "gas heating system"
|
||||
id = "gasheater"
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 29 KiB |
@@ -253,6 +253,7 @@
|
||||
#include "code\datums\wires\autolathe.dm"
|
||||
#include "code\datums\wires\camera.dm"
|
||||
#include "code\datums\wires\explosive.dm"
|
||||
#include "code\datums\wires\grid_checker.dm"
|
||||
#include "code\datums\wires\particle_accelerator.dm"
|
||||
#include "code\datums\wires\radio.dm"
|
||||
#include "code\datums\wires\robot.dm"
|
||||
@@ -1409,6 +1410,9 @@
|
||||
#include "code\modules\materials\material_sheets.dm"
|
||||
#include "code\modules\materials\material_synth.dm"
|
||||
#include "code\modules\materials\materials.dm"
|
||||
#include "code\modules\metric\activity.dm"
|
||||
#include "code\modules\metric\department.dm"
|
||||
#include "code\modules\metric\metric.dm"
|
||||
#include "code\modules\mining\abandonedcrates.dm"
|
||||
#include "code\modules\mining\alloys.dm"
|
||||
#include "code\modules\mining\coins.dm"
|
||||
@@ -1762,6 +1766,7 @@
|
||||
#include "code\modules\power\generator.dm"
|
||||
#include "code\modules\power\generator_type2.dm"
|
||||
#include "code\modules\power\gravitygenerator.dm"
|
||||
#include "code\modules\power\grid_checker.dm"
|
||||
#include "code\modules\power\lighting.dm"
|
||||
#include "code\modules\power\port_gen.dm"
|
||||
#include "code\modules\power\power.dm"
|
||||
|
||||
Reference in New Issue
Block a user