Merge things

This commit is contained in:
Anewbe
2016-10-06 21:37:10 -05:00
22 changed files with 564 additions and 186 deletions

View File

@@ -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"

View 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)

View File

@@ -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)

View File

@@ -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.

View File

@@ -3,4 +3,7 @@
/datum/gm_action/comms_blackout
name = "communications blackout"
departments = list(ROLE_ENGINEERING, ROLE_EVERYONE)
chaotic = 35
chaotic = 35
/datum/gm_action/comms_blackout/get_weight()
return 50 + (metric.count_people_in_department(ROLE_ENGINEERING) * 40)

View File

@@ -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
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.

View File

@@ -3,4 +3,7 @@
/datum/gm_action/waste_disposal
name = "waste disposal"
departments = list(ROLE_CARGO)
chaotic = 0
chaotic = 0
/datum/gm_action/waste_disposal/get_weight()
return metric.count_people_in_department(ROLE_CARGO) * 50

View File

@@ -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")
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.

View File

@@ -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

View File

@@ -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.

View File

@@ -6,67 +6,4 @@
// 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
staleness = round( Clamp(staleness + amt, -50, 200), 0.1)

View 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)

View 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

View 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
)

View File

@@ -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

View File

@@ -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.
..()

View 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()
*/

View File

@@ -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
//////////////////////////////////////////

View File

@@ -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"

View File

@@ -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)

View File

@@ -1047,6 +1047,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"

View File

@@ -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"