diff --git a/code/_helpers/events.dm b/code/_helpers/events.dm
index e31d24783e..2d15bb3aa9 100644
--- a/code/_helpers/events.dm
+++ b/code/_helpers/events.dm
@@ -24,4 +24,16 @@
if(A == myarea) //The loc of a turf is the area it is in.
return 1
return 0
-
\ No newline at end of file
+
+// Returns a list of area instances, or a subtypes of them, that are mapped in somewhere.
+// Avoid feeding it `/area`, as it will likely cause a lot of lag as it evaluates every single area coded in.
+/proc/get_all_existing_areas_of_types(list/area_types)
+ . = list()
+ for(var/area_type in area_types)
+ var/list/types = typesof(area_type)
+ for(var/T in types)
+ // Test for existance.
+ var/area/A = locate(T)
+ if(!istype(A) || !A.contents.len) // Empty contents list means it's not on the map.
+ continue
+ . += A
\ No newline at end of file
diff --git a/code/controllers/subsystems/game_master.dm b/code/controllers/subsystems/game_master.dm
index 7c402e14b5..b27b6e578e 100644
--- a/code/controllers/subsystems/game_master.dm
+++ b/code/controllers/subsystems/game_master.dm
@@ -36,9 +36,6 @@ SUBSYSTEM_DEF(game_master)
if(config && !config.enable_game_master)
can_fire = FALSE
- // Remove when finished.
- debug_gm()
-
return ..()
/datum/controller/subsystem/game_master/fire(resumed)
diff --git a/code/game/area/Space Station 13 areas.dm b/code/game/area/Space Station 13 areas.dm
index 81c74f3ba3..2a4b933edf 100644
--- a/code/game/area/Space Station 13 areas.dm
+++ b/code/game/area/Space Station 13 areas.dm
@@ -71,6 +71,7 @@ NOTE: there are two lists of areas in the end of this file: centcom and station
flags = RAD_SHIELDED
sound_env = SMALL_ENCLOSED
base_turf = /turf/space
+ forbid_events = TRUE
/area/shuttle/arrival
name = "\improper Arrival Shuttle"
@@ -952,6 +953,9 @@ NOTE: there are two lists of areas in the end of this file: centcom and station
name = "\improper Construction Area"
icon_state = "construction"
+/area/hallway/secondary/entry
+ forbid_events = TRUE
+
/area/hallway/secondary/entry/fore
name = "\improper Shuttle Dock Hallway - Mid"
icon_state = "entry_1"
@@ -1126,6 +1130,7 @@ NOTE: there are two lists of areas in the end of this file: centcom and station
icon_state = "Sleep"
flags = RAD_SHIELDED
ambience = AMBIENCE_GENERIC
+ forbid_events = TRUE
/area/crew_quarters/toilet
name = "\improper Dormitory Toilets"
@@ -1425,6 +1430,7 @@ NOTE: there are two lists of areas in the end of this file: centcom and station
icon_state = "Holodeck"
dynamic_lighting = 0
sound_env = LARGE_ENCLOSED
+ forbid_events = TRUE
/area/holodeck/alphadeck
name = "\improper Holodeck Alpha"
@@ -1524,6 +1530,7 @@ NOTE: there are two lists of areas in the end of this file: centcom and station
name = "\improper Engine Room"
icon_state = "engine"
sound_env = LARGE_ENCLOSED
+ forbid_events = TRUE
/area/engineering/engine_airlock
name = "\improper Engine Room Airlock"
diff --git a/code/game/area/areas.dm b/code/game/area/areas.dm
index 0f9b208d77..ef6e13f711 100644
--- a/code/game/area/areas.dm
+++ b/code/game/area/areas.dm
@@ -41,6 +41,7 @@
var/turf/base_turf //The base turf type of the area, which can be used to override the z-level's base turf
var/global/global_uid = 0
var/uid
+ var/forbid_events = FALSE // If true, random events will not start inside this area.
/area/New()
icon_state = ""
@@ -335,6 +336,8 @@ var/list/mob/living/forced_ambiance_list = new
temp_airlock.prison_open()
for(var/obj/machinery/door/window/temp_windoor in src)
temp_windoor.open()
+ for(var/obj/machinery/door/blast/temp_blast in src)
+ temp_blast.open()
/area/has_gravity()
return has_gravity
diff --git a/code/game/objects/effects/spiders.dm b/code/game/objects/effects/spiders.dm
index 3bf7284214..8b74a26246 100644
--- a/code/game/objects/effects/spiders.dm
+++ b/code/game/objects/effects/spiders.dm
@@ -278,6 +278,9 @@
grow_as = list(/mob/living/simple_mob/animal/giant_spider, /mob/living/simple_mob/animal/giant_spider/hunter)
+/obj/effect/spider/spiderling/non_growing
+ amount_grown = -1
+
/obj/effect/decal/cleanable/spiderling_remains
name = "spiderling remains"
desc = "Green squishy mess."
diff --git a/code/game/objects/items/devices/PDA/PDA.dm b/code/game/objects/items/devices/PDA/PDA.dm
index e5acb5c4e7..a8b3d8f430 100644
--- a/code/game/objects/items/devices/PDA/PDA.dm
+++ b/code/game/objects/items/devices/PDA/PDA.dm
@@ -65,6 +65,8 @@ var/global/list/obj/item/device/pda/PDAs = list()
var/obj/item/device/paicard/pai = null // A slot for a personal AI device
+ var/spam_proof = FALSE // If true, it can't be spammed by random events.
+
/obj/item/device/pda/examine(mob/user)
if(..(user, 1))
to_chat(user, "The time [stationtime2text()] is displayed in the corner of the screen.")
@@ -334,6 +336,8 @@ var/global/list/obj/item/device/pda/PDAs = list()
/obj/item/device/pda/ai/pai
ttone = "assist"
+/obj/item/device/pda/ai/shell
+ spam_proof = TRUE // Since empty shells get a functional PDA.
// Used for the PDA multicaster, which mirrors messages sent to it to a specific department,
/obj/item/device/pda/multicaster
@@ -342,6 +346,7 @@ var/global/list/obj/item/device/pda/PDAs = list()
ttone = "data"
detonate = 0
news_silent = 1
+ spam_proof = TRUE // Spam messages don't actually work and its difficult to disable these.
var/list/cartridges_to_send_to = list()
// This is what actually mirrors the message,
@@ -1194,6 +1199,15 @@ var/global/list/obj/item/device/pda/PDAs = list()
log_pda("(PDA: [sending_unit]) sent \"[message]\" to [name]",usr)
new_message = 1
+/obj/item/device/pda/proc/spam_message(sender, message)
+ var/reception_message = "\icon[src] Message from [sender] (Unknown / spam?), \"[message]\" (Unable to Reply)"
+ new_info(message_silent, ttone, reception_message)
+
+ if(prob(50)) // Give the AI an increased chance to intercept the message
+ for(var/mob/living/silicon/ai/ai in mob_list)
+ if(ai.aiPDA != src)
+ ai.show_message("Intercepted message from [sender] (Unknown / spam?) to [owner]: [message]")
+
/obj/item/device/pda/verb/verb_reset_pda()
set category = "Object"
set name = "Reset PDA"
diff --git a/code/game/objects/structures/window.dm b/code/game/objects/structures/window.dm
index 344428c1f2..208c8473ab 100644
--- a/code/game/objects/structures/window.dm
+++ b/code/game/objects/structures/window.dm
@@ -441,6 +441,13 @@
//this way it will only update full-tile ones
overlays.Cut()
if(!is_fulltile())
+ // Rotate the sprite somewhat so non-fulltiled windows can be seen as needing repair.
+ var/full_tilt_degrees = 15
+ var/tilt_to_apply = abs((health / maxhealth) - 1)
+ if(tilt_to_apply && prob(50))
+ tilt_to_apply = -tilt_to_apply
+ adjust_rotation(LERP(0, full_tilt_degrees, tilt_to_apply))
+
icon_state = "[basestate]"
return
var/list/dirs = list()
diff --git a/code/game/turfs/simulated/walls.dm b/code/game/turfs/simulated/walls.dm
index bf5f711b20..3c8ab70ede 100644
--- a/code/game/turfs/simulated/walls.dm
+++ b/code/game/turfs/simulated/walls.dm
@@ -231,10 +231,25 @@
// Wall-rot effect, a nasty fungus that destroys walls.
/turf/simulated/wall/proc/rot()
if(locate(/obj/effect/overlay/wallrot) in src)
- return
+ return FALSE
+
+ // Wall-rot can't go onto walls that are surrounded in all four cardinal directions.
+ // Because of spores, or something. It's actually to avoid the pain that is removing wallrot surrounded by
+ // four r-walls.
+ var/at_least_one_open_turf = FALSE
+ for(var/direction in GLOB.cardinal)
+ var/turf/T = get_step(src, direction)
+ if(!T.check_density())
+ at_least_one_open_turf = TRUE
+ break
+
+ if(!at_least_one_open_turf)
+ return FALSE
+
var/number_rots = rand(2,3)
for(var/i=0, i", "", "", 2)
+
+ // Nobody reads the requests consoles so lets use the radio as well.
+ global_announcer.autosay(message, my_department, DEPARTMENT_COMMAND)
+
+/datum/event2/event/money_hacker/end()
+ var/message = null
+ if(targeted_account && !targeted_account.suspended) // Hacker wins.
+ message = "The hack attempt has succeeded."
+ hack_account(targeted_account)
+ log_debug("Money hacker event managed to hack the targeted account.")
+
+ else // Crew wins.
+ message = "The attack has ceased, the affected accounts can now be brought online."
+ log_debug("Money hacker event failed to hack the targeted account due to intervention by the crew.")
+
+ var/my_department = "[location_name()] Firewall Subroutines"
+
+ for(var/obj/machinery/message_server/MS in machines)
+ if(!MS.active) continue
+ MS.send_rc_message("Head of Personnel's Desk", my_department, message, "", "", 2)
+
+ global_announcer.autosay(message, my_department, DEPARTMENT_COMMAND)
+
+/datum/event2/event/money_hacker/proc/hack_account(datum/money_account/A)
+ // Subtract the money.
+ var/lost = A.money * 0.8 + (rand(2,4) - 2) / 10
+ A.money -= lost
+
+ // Create a taunting log entry.
+ var/datum/transaction/T = new()
+ T.target_name = pick(list(
+ "",
+ "yo brotha from anotha motha",
+ "el Presidente",
+ "chieF smackDowN",
+ "Nobody"
+ ))
+
+ T.purpose = pick(list(
+ "Ne$ ---ount fu%ds init*&lisat@*n",
+ "PAY BACK YOUR MUM",
+ "Funds withdrawal",
+ "pWnAgE",
+ "l33t hax",
+ "liberationez",
+ "Hit",
+ "Nothing"
+ ))
+
+ T.amount = pick(list(
+ "",
+ "([rand(0,99999)])",
+ "alla money",
+ "9001$",
+ "HOLLA HOLLA GET DOLLA",
+ "([lost])",
+ "69,420t"
+ ))
+
+ var/date1 = "31 December, 1999"
+ var/date2 = "[num2text(rand(1,31))] [pick("January","February","March","April","May","June","July","August","September","October","November","December")], [rand(1000,3000)]"
+ T.date = pick("", current_date_string, date1, date2,"Nowhen")
+
+ var/time1 = rand(0, 99999999)
+ var/time2 = "[round(time1 / 36000)+12]:[(time1 / 600 % 60) < 10 ? add_zero(time1 / 600 % 60, 1) : time1 / 600 % 60]"
+ T.time = pick("", stationtime2text(), time2, "Never")
+
+ T.source_terminal = pick("","[pick("Biesel","New Gibson")] GalaxyNet Terminal #[rand(111,999)]","your mums place","nantrasen high CommanD","Angessa's Pearl","Nowhere")
+
+ A.transaction_log.Add(T)
diff --git a/code/modules/gamemaster/event2/events/command/raise_funds.dm b/code/modules/gamemaster/event2/events/command/raise_funds.dm
new file mode 100644
index 0000000000..33053c0996
--- /dev/null
+++ b/code/modules/gamemaster/event2/events/command/raise_funds.dm
@@ -0,0 +1,102 @@
+/datum/event2/meta/raise_funds
+ name = "local funding drive"
+ enabled = FALSE // There isn't really any suitable way for the crew to generate thalers right now, if that gets fixed feel free to turn this event on.
+ departments = list(DEPARTMENT_COMMAND, DEPARTMENT_CARGO)
+ chaotic_threshold = EVENT_CHAOS_THRESHOLD_LOW_IMPACT
+ event_type = /datum/event2/event/raise_funds
+
+/datum/event2/meta/raise_funds/get_weight()
+ var/command = metric.count_people_in_department(DEPARTMENT_COMMAND)
+ if(!command) // Need someone to read the centcom message.
+ return 0
+
+ var/cargo = metric.count_people_in_department(DEPARTMENT_CARGO)
+ return (command * 20) + (cargo * 20)
+
+
+
+/datum/event2/event/raise_funds
+ length_lower_bound = 30 MINUTES
+ length_upper_bound = 45 MINUTES
+ var/money_at_start = 0
+
+/datum/event2/event/raise_funds/announce()
+ var/message = "Due to [pick("recent", "unfortunate", "possible future")] budget \
+ [pick("changes", "issues")], in-system stations are now advised to increase funding income."
+
+ send_command_report("Budget Advisement", message)
+
+/datum/event2/event/raise_funds/start()
+ // Note that the event remembers the amount of money when it started. If an issue develops where people try to scam centcom by
+ // taking out loads of money before the event, then depositing it back in after the event fires, feel free to make this check for
+ // roundstart money instead.
+ money_at_start = count_money()
+ log_debug("Funding Drive event logged a sum of [money_at_start] thalers in all station accounts at the start of the event.")
+
+/datum/event2/event/raise_funds/end()
+ var/money_at_end = count_money()
+ log_debug("Funding Drive event logged a sum of [money_at_end] thalers in all station accounts at the end of the event, compared \
+ to [money_at_start] thalers. A difference of [money_at_end / money_at_start] was calculated.")
+
+ // A number above 1 indicates money was made, while below 1 does the opposite.
+ var/budget_shift = money_at_end / money_at_start
+
+ // Centcom will say different things based on if they gained or lost money.
+ var/message = null
+ switch(budget_shift)
+ if(0 to 0.02) // Abyssmal response.
+ message = "We are very interested in learning where [round(money_at_start, 1000)] thaler went in \
+ just half an hour. We highly recommend rectifying this issue before the end of the shift, otherwise a \
+ discussion regarding your future employment prospects will occur.
\
+ Your facility's current balance of requisition tokens has been revoked, and the rate of further \
+ tokens will be severely slowed."
+ supply_controller.points = 0
+ supply_controller.points_per_process = max(supply_controller.points_per_process - 1, 0.5)
+ log_debug("Funding Drive event ended with an abyssmal response, and the loss of all cargo points.")
+
+ if(0.02 to 0.98) // Bad response.
+ message = "We're very disappointed that \the [location_name()] has ran a deficit since our request. \
+ As such, we will be taking away some requisition tokens to cover the cost of operating your facility, \
+ as well as decreasing the amount of tokens your facility will receive over time.."
+ var/points_lost = round(supply_controller.points * rand(0.5, 0.8))
+ supply_controller.points -= points_lost
+ supply_controller.points_per_process = max(supply_controller.points_per_process - 0.5, 0.5)
+ log_debug("Funding Drive event ended with a bad response, and [points_lost] cargo points was taken away.")
+
+ if(0.98 to 1.02) // Neutral response.
+ message = "It is unfortunate that \the [location_name()]'s finances remain at a standstill, however \
+ that is still preferred over having a decicit. We hope that in the future, your facility will be able to be \
+ more profitable."
+ log_debug("Funding Drive event ended with a neutral response.")
+
+ if(1.02 to INFINITY) // Good response.
+ message = "We appreciate the efforts made by \the [location_name()] to run at a surplus. \
+ Together, along with the other facilities present in the [using_map.starsys_name] system, \
+ the company is expected to meet the quota.
\
+ We will allocate additional requisition tokens for the cargo department as a reward. The rate \
+ of further tokens has also been increased."
+
+ // If cargo is ever made to use station funds instead of cargo points, then a new kind of reward will be needed.
+ // Otherwise it would be weird for centcom to go 'thanks for not spending money, your reward is money to spend'.
+ var/point_reward = rand(100, 200)
+ supply_controller.points += point_reward
+ supply_controller.points_per_process += 0.5
+ log_debug("Funding Drive event ended with a good response and a bonus of [point_reward] cargo points.")
+
+ send_command_report("Budget Followup", message)
+
+
+
+// Returns the sum of the station account and all the departmental accounts.
+/datum/event2/event/raise_funds/proc/count_money()
+ . = 0
+ . += station_account.money
+ for(var/i = 1 to SSjob.department_datums.len)
+ var/datum/money_account/account = LAZYACCESS(department_accounts, SSjob.department_datums[i])
+ if(istype(account))
+ . += account.money
+
+/datum/event2/event/raise_funds/proc/send_command_report(title, message)
+ post_comm_message(title, message)
+ to_world(span("danger", "New [using_map.company_name] Update available at all communication consoles."))
+ SEND_SOUND(world, 'sound/AI/commandreport.ogg')
diff --git a/code/modules/gamemaster/event2/events/engineering/airlock_failure.dm b/code/modules/gamemaster/event2/events/engineering/airlock_failure.dm
new file mode 100644
index 0000000000..42297ca56c
--- /dev/null
+++ b/code/modules/gamemaster/event2/events/engineering/airlock_failure.dm
@@ -0,0 +1,181 @@
+/datum/event2/meta/airlock_failure
+ event_class = "airlock failure"
+ departments = list(DEPARTMENT_ENGINEERING, DEPARTMENT_MEDICAL)
+ chaos = 15
+ chaotic_threshold = EVENT_CHAOS_THRESHOLD_MEDIUM_IMPACT
+ event_type = /datum/event2/event/airlock_failure
+
+/datum/event2/meta/airlock_failure/emag
+ name = "airlock failure - emag"
+ event_type = /datum/event2/event/airlock_failure/emag
+
+/datum/event2/meta/airlock_failure/door_crush
+ name = "airlock failure - crushing"
+ event_type = /datum/event2/event/airlock_failure/door_crush
+
+/datum/event2/meta/airlock_failure/shock
+ name = "airlock failure - shock"
+ chaos = 30
+ event_type = /datum/event2/event/airlock_failure/shock
+
+
+/datum/event2/meta/airlock_failure/get_weight()
+ var/engineering = metric.count_people_in_department(DEPARTMENT_ENGINEERING)
+
+ // Synths are good both for fixing the doors and getting blamed for the doors zapping people.
+ var/synths = metric.count_people_in_department(DEPARTMENT_SYNTHETIC)
+ if(!engineering && !synths) // Nobody's around to fix the door.
+ return 0
+
+ // Medical might be needed for some of the more violent airlock failures.
+ var/medical = metric.count_people_in_department(DEPARTMENT_MEDICAL)
+
+ return (engineering * 20) + (medical * 20) + (synths * 20)
+
+
+
+/datum/event2/event/airlock_failure
+ announce_delay_lower_bound = 20 SECONDS
+ announce_delay_upper_bound = 40 SECONDS
+ var/announce_odds = 0
+ var/doors_to_break = 1
+ var/list/affected_areas = list()
+
+/datum/event2/event/airlock_failure/emag
+ announce_odds = 10 // To make people wonder if the emagged door was from a baddie or from this event.
+ doors_to_break = 2 // Replacing emagged doors really sucks for engineering so don't overdo it.
+
+/datum/event2/event/airlock_failure/door_crush
+ announce_odds = 30
+ doors_to_break = 5
+
+/datum/event2/event/airlock_failure/shock
+ announce_odds = 70
+
+/datum/event2/event/airlock_failure/start()
+ var/list/areas = find_random_areas()
+ if(!LAZYLEN(areas))
+ log_debug("Airlock Failure event could not find any areas. Aborting.")
+ abort()
+ return
+
+ while(areas.len)
+ var/area/area = pick(areas)
+ areas -= area
+
+ for(var/obj/machinery/door/airlock/door in area.contents)
+ if(can_break_door(door))
+ addtimer(CALLBACK(src, .proc/break_door, door), 1) // Emagging proc is actually a blocking proc and that's bad for the ticker.
+ door.visible_message(span("danger", "\The [door]'s panel sparks!"))
+ playsound(door, "sparks", 50, 1)
+ log_debug("Airlock Failure event has broken \the [door] airlock in [area].")
+ affected_areas |= area
+ doors_to_break--
+
+ if(doors_to_break <= 0)
+ return
+
+/datum/event2/event/airlock_failure/announce()
+ if(prob(announce_odds))
+ command_announcement.Announce("An electrical issue has been detected in the airlock grid at [english_list(affected_areas)]. \
+ Some airlocks may require servicing by a qualified technician.", "Electrical Alert")
+
+
+/datum/event2/event/airlock_failure/proc/can_break_door(obj/machinery/door/airlock/door)
+ if(istype(door, /obj/machinery/door/airlock/lift))
+ return FALSE
+ return door.arePowerSystemsOn()
+
+// Override this for door busting.
+/datum/event2/event/airlock_failure/proc/break_door(obj/machinery/door/airlock/door)
+
+/datum/event2/event/airlock_failure/emag/break_door(obj/machinery/door/airlock/door)
+ door.emag_act(1)
+
+/datum/event2/event/airlock_failure/door_crush/break_door(obj/machinery/door/airlock/door)
+ door.normalspeed = FALSE
+ door.safe = FALSE
+
+/datum/event2/event/airlock_failure/shock/break_door(obj/machinery/door/airlock/door)
+ door.electrify(-1)
+
+
+
+/*
+/datum/gm_action/electrified_door
+ name = "airlock short-circuit"
+ departments = list(DEPARTMENT_ENGINEERING, DEPARTMENT_MEDICAL)
+ chaotic = 10
+ var/obj/machinery/door/airlock/chosen_door
+ var/area/target_area
+ var/list/area/excluded = list(
+ /area/submap,
+ /area/shuttle,
+ /area/crew_quarters
+ )
+
+/datum/gm_action/electrified_door/set_up()
+ var/list/area/grand_list_of_areas = get_station_areas(excluded)
+
+ severity = pickweight(EVENT_LEVEL_MUNDANE = 10,
+ EVENT_LEVEL_MODERATE = 5,
+ EVENT_LEVEL_MAJOR = 1
+ )
+
+ //try 10 times
+ for(var/i in 1 to 10)
+ target_area = pick(grand_list_of_areas)
+ var/list/obj/machinery/door/airlock/target_doors = list()
+ for(var/obj/machinery/door/airlock/target_door in target_area.contents)
+ target_doors += target_door
+ target_doors = shuffle(target_doors)
+
+ for(var/obj/machinery/door/airlock/target_door in target_doors)
+ if(!target_door.isElectrified() && target_door.arePowerSystemsOn() && target_door.maxhealth == target_door.health)
+ chosen_door = target_door
+ return
+
+/datum/gm_action/electrified_door/start()
+ ..()
+ if(!chosen_door)
+ return
+ command_announcement.Announce("An electrical issue has been detected in your area, please repair potential electronic overloads.", "Electrical Alert")
+ chosen_door.visible_message("\The [chosen_door]'s panel sparks!")
+ chosen_door.set_safeties(0)
+ playsound(get_turf(chosen_door), 'sound/machines/buzz-sigh.ogg', 50, 1)
+ if(severity >= EVENT_LEVEL_MODERATE)
+ chosen_door.electrify(-1)
+ spawn(rand(10 SECONDS, 2 MINUTES))
+ if(chosen_door && chosen_door.arePowerSystemsOn() && prob(25 + 25 * severity))
+ command_announcement.Announce("Overload has been localized to \the [target_area].", "Electrical Alert")
+
+ if(severity >= EVENT_LEVEL_MAJOR) // New Major effect. Hydraulic boom.
+ spawn()
+ chosen_door.visible_message("\The [chosen_door] buzzes.")
+ playsound(get_turf(chosen_door), 'sound/machines/buzz-sigh.ogg', 50, 1)
+ sleep(rand(10 SECONDS, 3 MINUTES))
+ if(!chosen_door || !chosen_door.arePowerSystemsOn())
+ return
+ chosen_door.visible_message("\The [chosen_door]'s hydraulics creak.")
+ playsound(get_turf(chosen_door), 'sound/machines/airlock_creaking.ogg', 50, 1)
+ sleep(rand(30 SECONDS, 10 MINUTES))
+ if(!chosen_door || !chosen_door.arePowerSystemsOn())
+ return
+ chosen_door.visible_message("\The [chosen_door] emits a hydraulic shriek!")
+ playsound(get_turf(chosen_door), 'sound/machines/airlock.ogg', 80, 1)
+ spawn(rand(5 SECONDS, 30 SECONDS))
+ if(!chosen_door || !chosen_door.arePowerSystemsOn())
+ return
+ chosen_door.visible_message("\The [chosen_door]'s hydraulics detonate!")
+ chosen_door.fragmentate(get_turf(chosen_door), rand(5, 10), rand(3, 5), list(/obj/item/projectile/bullet/pellet/fragment/tank/small))
+ explosion(get_turf(chosen_door),-1,-1,2,3)
+
+ chosen_door.lock()
+ chosen_door.health = chosen_door.maxhealth / 6
+ chosen_door.aiControlDisabled = 1
+ chosen_door.update_icon()
+
+/datum/gm_action/electrified_door/get_weight()
+ return 10 + (metric.count_people_in_department(DEPARTMENT_ENGINEERING) * 5 + metric.count_people_in_department(DEPARTMENT_MEDICAL) * 10)
+
+*/
\ No newline at end of file
diff --git a/code/modules/gamemaster/event2/events/engineering/blob.dm b/code/modules/gamemaster/event2/events/engineering/blob.dm
index badb260114..a62cd6742f 100644
--- a/code/modules/gamemaster/event2/events/engineering/blob.dm
+++ b/code/modules/gamemaster/event2/events/engineering/blob.dm
@@ -106,7 +106,7 @@
continue
var/list/turfs = list()
for(var/turf/simulated/floor/F in A)
- if(turf_clear(F))
+ if(!F.check_density())
turfs += F
if(turfs.len < 5 + number_of_blobs)
log_debug("Blob infestation event: Rejected [A] because it has too little clear turfs.")
diff --git a/code/modules/gamemaster/event2/events/engineering/brand_intelligence.dm b/code/modules/gamemaster/event2/events/engineering/brand_intelligence.dm
new file mode 100644
index 0000000000..bf1a8dfed6
--- /dev/null
+++ b/code/modules/gamemaster/event2/events/engineering/brand_intelligence.dm
@@ -0,0 +1,90 @@
+/datum/event2/meta/brand_intelligence
+ name = "vending machine malware"
+ departments = list(DEPARTMENT_EVERYONE, DEPARTMENT_EVERYONE)
+ chaos = 10
+ chaotic_threshold = EVENT_CHAOS_THRESHOLD_LOW_IMPACT
+ event_type = /datum/event2/event/brand_intelligence
+
+/datum/event2/meta/brand_intelligence/get_weight()
+ return 10 + (metric.count_people_in_department(DEPARTMENT_ENGINEERING) * 20)
+
+
+
+/datum/event2/event/brand_intelligence
+ var/malware_spread_cooldown = 30 SECONDS
+
+ var/list/vending_machines = list() // List of venders that can potentially be infected.
+ var/list/infected_vending_machines = list() // List of venders that have been infected.
+ var/obj/machinery/vending/vender_zero = null // The first vending machine infected. If that one gets fixed, all other infected machines will be cured.
+ var/last_malware_spread_time = null
+
+/datum/event2/event/brand_intelligence/set_up()
+ for(var/obj/machinery/vending/V in machines)
+ if(!(V.z in using_map.station_levels))
+ continue
+ vending_machines += V
+
+ if(!vending_machines.len)
+ log_debug("Could not find any vending machines on station Z levels. Aborting.")
+ abort()
+ return
+
+ vender_zero = pick(vending_machines)
+
+/datum/event2/event/brand_intelligence/announce()
+ if(prob(90))
+ command_announcement.Announce("An ongoing mass upload of malware for venders has been detected onboard \the [location_name()], \
+ which appears to transmit to nearby vendors. The original infected machine is believed to be \a [vender_zero].", "Vender Service Alert")
+
+/datum/event2/event/brand_intelligence/start()
+ infect_vender(vender_zero)
+
+/datum/event2/event/brand_intelligence/event_tick()
+ if(last_malware_spread_time + malware_spread_cooldown > world.time)
+ return // Still on cooldown.
+ last_malware_spread_time = world.time
+
+ if(vending_machines.len)
+ var/next_victim = pick(vending_machines)
+ infect_vender(next_victim)
+
+ // Every time Vender Zero infects, it says something.
+ vender_zero.speak(pick("Try our aggressive new marketing strategies!", \
+ "You should buy products to feed your lifestyle obsession!", \
+ "Consume!", \
+ "Your money can buy happiness!", \
+ "Engage direct marketing!", \
+ "Advertising is legalized lying! But don't let that put you off our great deals!", \
+ "You don't want to buy anything? Yeah, well I didn't want to buy your mom either."))
+
+
+/datum/event2/event/brand_intelligence/should_end()
+ if(!vending_machines.len)
+ return TRUE
+ if(!can_propagate(vender_zero))
+ return TRUE
+ return FALSE
+
+/datum/event2/event/brand_intelligence/end()
+ if(can_propagate(vender_zero)) // The crew failed and all the machines are infected!
+ return
+ // Otherwise Vender Zero was taken out in some form.
+ if(vender_zero)
+ vender_zero.visible_message(span("notice", "\The [vender_zero]'s network activity light flickers wildly \
+ for a few seconds as a small screen reads: 'Rolling out firmware reset to networked machines'."))
+ for(var/obj/machinery/vending/vender in infected_vending_machines)
+ cure_vender(vender)
+
+/datum/event2/event/brand_intelligence/proc/infect_vender(obj/machinery/vending/V)
+ vending_machines -= V
+ infected_vending_machines += V
+ V.shut_up = FALSE
+ V.shoot_inventory = TRUE
+
+/datum/event2/event/brand_intelligence/proc/cure_vender(obj/machinery/vending/V)
+ infected_vending_machines -= V
+ V.shut_up = TRUE
+ V.shoot_inventory = FALSE
+
+/datum/event2/event/brand_intelligence/proc/can_propagate(obj/machinery/vending/V)
+ return V && V.shut_up == FALSE
diff --git a/code/modules/gamemaster/event2/events/engineering/carp_migration.dm b/code/modules/gamemaster/event2/events/engineering/carp_migration.dm
deleted file mode 100644
index 4a4ceee6ce..0000000000
--- a/code/modules/gamemaster/event2/events/engineering/carp_migration.dm
+++ /dev/null
@@ -1,142 +0,0 @@
-/datum/event2/meta/carp_migration
- name = "carp migration"
- event_class = "carp"
- departments = list(DEPARTMENT_SECURITY, DEPARTMENT_EVERYONE)
- chaos = 30
- chaotic_threshold = EVENT_CHAOS_THRESHOLD_MEDIUM_IMPACT
- event_type = /datum/event2/event/carp_migration
-
-/datum/event2/meta/carp_migration/get_weight()
- return 10 + (metric.count_people_in_department(DEPARTMENT_SECURITY) * 20) + (metric.count_all_space_mobs() * 40)
-
-
-/datum/event2/event/carp_migration
- announce_delay_lower_bound = 1 MINUTE
- announce_delay_upper_bound = 2 MINUTES
- length_lower_bound = 30 SECONDS
- length_upper_bound = 1 MINUTE
- var/use_map_edge_with_landmarks = TRUE // Use both landmarks and spawning from the "edge" of the map. Otherise uses landmarks over map edge.
- var/carp_cap = 30 // No more than this many (living) carp can exist from this event.
- var/carp_smallest_group = 3
- var/carp_largest_group = 5
- var/carp_wave_cooldown = 10 SECONDS
-
- var/list/spawned_carp = list()
- var/last_carp_wave_time = null // Last world.time we spawned a carp wave.
- var/list/valid_z_levels = null
-
-/datum/event2/event/carp_migration/set_up()
- valid_z_levels = get_location_z_levels()
- valid_z_levels -= using_map.sealed_levels // Space levels only please!
-
-/datum/event2/event/carp_migration/announce()
- var/announcement = "Unknown biological entities been detected near \the [location_name()], please stand-by."
- command_announcement.Announce(announcement, "Lifesign Alert")
-
-
-/datum/event2/event/carp_migration/event_tick()
- if(last_carp_wave_time + carp_wave_cooldown > world.time)
- return
- last_carp_wave_time = world.time
-
- if(count_spawned_carps() < carp_cap)
- spawn_fish(number_of_groups = rand(1, 4), min_size_of_group = carp_smallest_group, max_size_of_group = carp_largest_group)
-
-/datum/event2/event/carp_migration/end()
- // Clean up carp that died in space for some reason.
- for(var/mob/living/simple_mob/SM in spawned_carp)
- if(SM.stat == DEAD)
- var/turf/T = get_turf(SM)
- if(istype(T, /turf/space))
- if(prob(75))
- qdel(SM)
-
-// Makes carp spawn, no landmarks required, but they're still used if they exist.
-/datum/event2/event/carp_migration/proc/spawn_fish(number_of_groups, min_size_of_group, max_size_of_group, dir)
- if(isnull(dir))
- dir = pick(GLOB.cardinal)
-
- // Check if any landmarks exist!
- var/list/spawn_locations = list()
- for(var/obj/effect/landmark/C in landmarks_list)
- if(C.name == "carpspawn" && (C.z in valid_z_levels))
- spawn_locations.Add(C.loc)
-
- var/prioritize_landmarks = TRUE
- if(use_map_edge_with_landmarks && prob(50))
- prioritize_landmarks = FALSE // One in two chance to come from the edge instead.
-
- if(spawn_locations.len && prioritize_landmarks) // Okay we've got landmarks, lets use those!
- shuffle_inplace(spawn_locations)
- number_of_groups = min(number_of_groups, spawn_locations.len)
- var/i = 1
- while (i <= number_of_groups)
- var/group_size = rand(min_size_of_group, max_size_of_group)
- for (var/j = 0, j < group_size, j++)
- spawn_one_carp(spawn_locations[i])
- i++
- return
-
- // Okay we did *not* have any landmarks, or we're being told to do both, so lets do our best!
- var/i = 1
- while(i <= number_of_groups)
- var/z_level = pick(valid_z_levels)
- var/group_size = rand(min_size_of_group, max_size_of_group)
- var/turf/map_center = locate(round(world.maxx/2), round(world.maxy/2), z_level)
- var/turf/group_center = pick_random_edge_turf(dir, z_level, TRANSITIONEDGE + 2)
- var/list/turfs = getcircle(group_center, 2)
- for(var/j = 0, j < group_size, j++)
- // On larger maps, BYOND gets in the way of letting simple_mobs path to the closest edge of the station.
- // So instead we need to simulate the carp's travel, then spawn them somewhere still hopefully off screen.
-
- // Find a turf to be the edge of the map.
- var/turf/edge_of_map = turfs[(i % turfs.len) + 1]
-
- // Now walk a straight line towards the center of the map, until we find a non-space tile.
- var/turf/edge_of_station = null
-
- var/list/space_line = list() // This holds all space tiles on the line. Will be used a bit later.
- for(var/turf/T in getline(edge_of_map, map_center))
- if(!T.is_space())
- break // We found the station!
- space_line += T
- edge_of_station = T
-
- // Now put the carp somewhere on the line, hopefully off screen.
- // I wish this was higher than 8 but the BYOND internal A* algorithm gives up sometimes when using
- // 16 or more.
- // In the future, a new AI stance that handles long distance travel using getline() could work.
- var/max_distance = 8
- var/turf/spawn_turf = null
- for(var/P in space_line)
- var/turf/point = P
- if(get_dist(point, edge_of_station) <= max_distance)
- spawn_turf = P
- break
-
- if(spawn_turf)
- // Finally, make the carp go towards the edge of the station.
- var/mob/living/simple_mob/animal/M = spawn_one_carp(spawn_turf)
- if(edge_of_station)
- M.ai_holder?.give_destination(edge_of_station) // Ask carp to swim towards the edge of the station.
- i++
-
-/datum/event2/event/carp_migration/proc/spawn_one_carp(new_loc)
- var/mob/living/simple_mob/animal/M = new /mob/living/simple_mob/animal/space/carp/event(new_loc)
- GLOB.destroyed_event.register(M, src, .proc/on_carp_destruction)
- spawned_carp += M
- return M
-
-
-// Counts living carp spawned by this event.
-/datum/event2/event/carp_migration/proc/count_spawned_carps()
- . = 0
- for(var/I in spawned_carp)
- var/mob/living/simple_mob/animal/M = I
- if(!QDELETED(M) && M.stat != DEAD)
- . += 1
-
-// If carp is bomphed, remove it from the list.
-/datum/event2/event/carp_migration/proc/on_carp_destruction(mob/M)
- spawned_carp -= M
- GLOB.destroyed_event.unregister(M, src, .proc/on_carp_destruction)
diff --git a/code/modules/gamemaster/event2/events/engineering/wallrot.dm b/code/modules/gamemaster/event2/events/engineering/wallrot.dm
new file mode 100644
index 0000000000..29d12c8524
--- /dev/null
+++ b/code/modules/gamemaster/event2/events/engineering/wallrot.dm
@@ -0,0 +1,46 @@
+/datum/event2/meta/wallrot
+ name = "wall-rot"
+ departments = list(DEPARTMENT_ENGINEERING)
+ reusable = TRUE
+ event_type = /datum/event2/event/wallrot
+
+/datum/event2/meta/wallrot/get_weight()
+ return (10 + metric.count_people_in_department(DEPARTMENT_ENGINEERING) * 10) / (times_ran + 1)
+
+
+
+
+/datum/event2/event/wallrot
+ var/turf/simulated/wall/origin = null
+
+/datum/event2/event/wallrot/set_up()
+ for(var/i = 1 to 100)
+ var/turf/candidate = locate(rand(1, world.maxx), rand(1, world.maxy), pick(get_location_z_levels()) )
+ if(istype(candidate, /turf/simulated/wall))
+ origin = candidate
+ log_debug("Wall-rot event has chosen \the [origin] ([origin.loc]) as the origin for the wallrot infestation.")
+ return
+
+ log_debug("Wall-rot event failed to find a valid wall after one hundred tries. Aborting.")
+ abort()
+
+/datum/event2/event/wallrot/announce()
+ if(origin && prob(80))
+ command_announcement.Announce("Harmful fungi detected on \the [location_name()], near \the [origin.loc]. \
+ Station structural integrity may be compromised.", "Biohazard Alert")
+
+/datum/event2/event/wallrot/start()
+ if(origin)
+ origin.rot()
+
+ var/rot_count = 0
+ var/target_rot = rand(5, 20)
+ for(var/turf/simulated/wall/W in range(7, origin))
+ if(prob(50))
+ if(W.rot())
+ rot_count++
+ if(rot_count >= target_rot)
+ break
+
+
+
diff --git a/code/modules/gamemaster/event2/events/engineering/window_break.dm b/code/modules/gamemaster/event2/events/engineering/window_break.dm
new file mode 100644
index 0000000000..0b02e78b3d
--- /dev/null
+++ b/code/modules/gamemaster/event2/events/engineering/window_break.dm
@@ -0,0 +1,148 @@
+// This event causes a random window near space to become damaged.
+// If that window is not fixed in a certain amount of time,
+// that window and nearby windows will shatter, causing a breach.
+
+/datum/event2/meta/window_break
+ name = "window break"
+ departments = list(DEPARTMENT_ENGINEERING)
+ chaos = 10
+ reusable = TRUE
+ chaotic_threshold = EVENT_CHAOS_THRESHOLD_MEDIUM_IMPACT
+ event_type = /datum/event2/event/window_break
+
+/datum/event2/meta/window_break/get_weight()
+ return metric.count_people_in_department(DEPARTMENT_ENGINEERING) * 20
+
+
+
+/datum/event2/event/window_break
+ announce_delay_lower_bound = 10 SECONDS
+ announce_delay_upper_bound = 20 SECONDS
+ length_lower_bound = 8 MINUTES
+ length_upper_bound = 12 MINUTES
+ var/turf/chosen_turf_with_windows = null
+ var/obj/structure/window/chosen_window = null
+ var/list/collateral_windows = list()
+
+/datum/event2/event/window_break/set_up()
+ var/list/areas = find_random_areas()
+ if(!LAZYLEN(areas))
+ log_debug("Window Break event could not find any areas. Aborting.")
+ abort()
+ return
+
+ while(areas.len)
+ var/area/area = pick(areas)
+ areas -= area
+
+ for(var/obj/structure/window/W in area.contents)
+ if(!is_window_to_space(W))
+ continue
+ chosen_turf_with_windows = get_turf(W)
+ collateral_windows = gather_collateral_windows(W)
+ break // Break out of the inner loop.
+
+ if(chosen_turf_with_windows)
+ log_debug("Window Break event has chosen turf '[chosen_turf_with_windows.name]' in [chosen_turf_with_windows.loc].")
+ break // Then the outer loop.
+
+ if(!chosen_turf_with_windows)
+ log_debug("Window Break event could not find a turf with valid windows to break. Aborting.")
+ abort()
+ return
+
+/datum/event2/event/window_break/announce()
+ if(chosen_window)
+ command_announcement.Announce("Structural integrity of space-facing windows at \the [get_area(chosen_turf_with_windows)] are failing. \
+ Repair of the damaged window is advised. Personnel without EVA suits in the area should leave until repairs are complete.", "Structural Alert")
+
+/datum/event2/event/window_break/start()
+ if(!chosen_turf_with_windows)
+ return
+
+ for(var/obj/structure/window/W in chosen_turf_with_windows.contents)
+ if(W.is_fulltile()) // Full tile windows are simple and can always be used.
+ chosen_window = W
+ break
+ else // Otherwise we only want the window that is on the inside side of the station.
+ var/turf/T = get_step(W, W.dir)
+ if(T.is_space())
+ continue
+ if(T.check_density())
+ continue
+ chosen_window = W
+ break
+
+ if(!chosen_window)
+ return
+
+ chosen_window.take_damage(chosen_window.maxhealth * 0.8)
+ playsound(chosen_window, 'sound/effects/Glasshit.ogg', 100, 1)
+ chosen_window.visible_message(span("danger", "\The [chosen_window] suddenly begins to crack!"))
+
+/datum/event2/event/window_break/should_end()
+ . = ..()
+ if(!.) // If the timer didn't expire, we can still end it early if someone messes up.
+ if(!chosen_window || !chosen_window.anchored || chosen_window.health == chosen_window.maxhealth)
+ // If the window got deconstructed/moved/etc, immediately end and make the breach happen.
+ // Also end early if it was repaired.
+ return TRUE
+
+/datum/event2/event/window_break/end()
+ // If someone fixed the window, then everything is fine.
+ if(chosen_window && chosen_window.anchored && chosen_window.health == chosen_window.maxhealth)
+ log_debug("Window Break event ended with window repaired.")
+ return
+
+ // Otherwise a bunch of windows shatter.
+ chosen_window?.shatter()
+
+ var/windows_to_shatter = min(rand(4, 10), collateral_windows.len)
+ for(var/i = 1 to windows_to_shatter)
+ var/obj/structure/window/W = collateral_windows[i]
+ W?.shatter()
+
+ log_debug("Window Break event ended with [windows_to_shatter] shattered windows and a breach.")
+
+// Checks if a window is adjacent to a space tile, and also that the opposite direction is open.
+// This is done to avoid getting caught in corner parts of windows.
+/datum/event2/event/window_break/proc/is_window_to_space(obj/structure/window/W)
+ for(var/direction in GLOB.cardinal)
+ var/turf/T = get_step(W, direction)
+ if(T.is_space())
+ var/turf/opposite_T = get_step(W, GLOB.reverse_dir[direction])
+ if(!opposite_T.check_density())
+ return TRUE
+ return FALSE
+
+//TL;DR: breadth first search for all connected turfs with windows
+/datum/event2/event/window_break/proc/gather_collateral_windows(var/obj/structure/window/target_window)
+ var/list/turf/frontier_set = list(target_window.loc)
+ var/list/obj/structure/window/result_set = list()
+ var/list/turf/explored_set = list()
+
+ while(frontier_set.len > 0)
+ var/turf/current = frontier_set[1]
+ frontier_set -= current
+ explored_set += current
+
+ var/contains_windows = 0
+ for(var/obj/structure/window/to_add in current.contents)
+ contains_windows = 1
+ result_set += to_add
+
+ if(contains_windows)
+ //add adjacent turfs to be checked for windows as well
+ var/turf/neighbor = locate(current.x + 1, current.y, current.z)
+ if(!(neighbor in frontier_set) && !(neighbor in explored_set))
+ frontier_set += neighbor
+ neighbor = locate(current.x - 1, current.y, current.z)
+ if(!(neighbor in frontier_set) && !(neighbor in explored_set))
+ frontier_set += neighbor
+ neighbor = locate(current.x, current.y + 1, current.z)
+ if(!(neighbor in frontier_set) && !(neighbor in explored_set))
+ frontier_set += neighbor
+ neighbor = locate(current.x, current.y - 1, current.z)
+ if(!(neighbor in frontier_set) && !(neighbor in explored_set))
+ frontier_set += neighbor
+ return result_set
diff --git a/code/modules/gamemaster/event2/events/everyone/comms_blackout.dm b/code/modules/gamemaster/event2/events/everyone/comms_blackout.dm
index 69e91d6942..6da19c32cc 100644
--- a/code/modules/gamemaster/event2/events/everyone/comms_blackout.dm
+++ b/code/modules/gamemaster/event2/events/everyone/comms_blackout.dm
@@ -12,13 +12,18 @@
/datum/event2/event/comms_blackout/announce()
+ var/alert = pick("Ionospheric anomalies detected. Temporary telecommunication failure imminent. Please contact you*%fj00)`5vc-BZZT", \
+ "Ionospheric anomalies detected. Temporary telecommunication failu*3mga;b4;'1v¬-BZZZT", \
+ "Ionospheric anomalies detected. Temporary telec#MCi46:5.;@63-BZZZZT", \
+ "Ionospheric anomalies dete'fZ\\kg5_0-BZZZZZT", \
+ "Ionospheri:%£ MCayj^j<.3-BZZZZZZT", \
+ "#4nd%;f4y6,>£%-BZZZZZZZT")
if(prob(33))
- command_announcement.Announce("Ionospheric anomalies detected. \
- Temporary telecommunication failure imminent. Please contact you-BZZT", new_sound = 'sound/misc/interference.ogg')
+ command_announcement.Announce(alert, new_sound = 'sound/misc/interference.ogg')
// AIs will always know if there's a comm blackout, rogue AIs could then lie about comm blackouts in the future while they shutdown comms
for(var/mob/living/silicon/ai/A in player_list)
to_chat(A, "
")
- to_chat(A, "Ionospheric anomalies detected. Temporary telecommunication failure imminent. Please contact you-BZZT")
+ to_chat(A, "[alert]")
to_chat(A, "
")
/datum/event2/event/comms_blackout/start()
@@ -32,3 +37,7 @@
log_debug("Doing complete outage of telecomms.")
for(var/obj/machinery/telecomms/T in telecomms_list)
T.emp_act(1)
+
+ // Communicators go down no matter what.
+ for(var/obj/machinery/exonet_node/N in machines)
+ N.emp_act(1)
diff --git a/code/modules/gamemaster/event2/events/everyone/electrical_fault.dm b/code/modules/gamemaster/event2/events/everyone/electrical_fault.dm
index e43d02fd21..6467646f1b 100644
--- a/code/modules/gamemaster/event2/events/everyone/electrical_fault.dm
+++ b/code/modules/gamemaster/event2/events/everyone/electrical_fault.dm
@@ -1,5 +1,6 @@
// Makes a spooky electrical thing happen, that can blow the lights or make the APCs turn off for a short period of time.
// Doesn't do any permanent damage beyond the small chance to emag an APC, which just unlocks it forever. As such, this is free to occur even with no engineers.
+// Since this is an 'external' thing, the Grid Checker can't stop it.
/datum/event2/meta/electrical_fault
name = "electrical fault"
@@ -17,16 +18,20 @@
start_delay_upper_bound = 1 MINUTE
length_lower_bound = 20 SECONDS
length_upper_bound = 40 SECONDS
- var/max_apcs_per_tick = 2
+ var/max_apcs_per_tick = 6
var/list/valid_apcs = null
var/list/valid_z_levels = null
+ var/apcs_disabled = 0
+ var/apcs_overloaded = 0
+ var/apcs_emagged = 0
+
/datum/event2/event/electrical_fault/announce()
// Trying to be vague to avoid 'space lightning storms'.
// This could be re-flavored to be a solar flare or something and have robots outside be sad.
- command_announce.Announce("External conditions near \the [location_name()] are likely \
- to cause voltage spikes and other electrical issues very soon. Please secure sensitive electrical equipment until conditions improve.", "[location_name()] Sensor Array")
+ command_announcement.Announce("External conditions near \the [location_name()] are likely \
+ to cause voltage spikes and other electrical issues very soon. Please secure sensitive electrical equipment until the situation passes.", "[location_name()] Sensor Array")
/datum/event2/event/electrical_fault/set_up()
valid_z_levels = get_location_z_levels()
@@ -37,9 +42,13 @@
if(A.z in valid_z_levels)
valid_apcs += A
+/datum/event2/event/electrical_fault/start()
+ command_announcement.Announce("Irregularities detected in \the [location_name()] power grid.", "[location_name()] Power Grid Monitoring")
+
/datum/event2/event/electrical_fault/event_tick()
if(!valid_apcs.len)
- log_debug("No valid APCs found for electrical fault event.")
+ log_debug("ELECTRICAL EVENT: No valid APCs found for electrical fault event. Aborting.")
+ abort()
return
var/list/picked_apcs = list()
@@ -50,62 +59,41 @@
affect_apc(A)
/datum/event2/event/electrical_fault/end()
- command_announce.Announce("External conditions have returned to normal around \the [location_name()].")
+ command_announcement.Announce("The irregular electrical conditions inside \the [location_name()] power grid has ceased.", "[location_name()] Power Grid Monitoring")
+ log_debug("Electrical Fault event caused [apcs_disabled] APC\s to shut off, \
+ [apcs_overloaded] APC\s to overload lighting, and [apcs_emagged] APC\s to be emagged.")
/datum/event2/event/electrical_fault/proc/affect_apc(obj/machinery/power/apc/A)
// Main breaker is turned off or is Special(tm). Consider it protected.
- if((!A.operating || failure_timer > 0) || A.is_critical)
+ // Important APCs like the AI or the engine core shouldn't get shut off by this event.
+ if((!A.operating || A.failure_timer > 0) || A.is_critical)
return
- playsound(
+ // In reality this would probably make the lights get brighter but oh well.
+ for(var/obj/machinery/light/L in get_area(A))
+ L.flicker(rand(10, 20))
+
+ // Chance to make the APC turn off for awhile.
+ // This will actually protect it from further damage.
+ if(prob(25))
+ A.energy_fail(rand(60, 120))
+ log_debug("ELECTRICAL EVENT: Disabled \the [A]'s power for a temporary amount of time.")
+ playsound(A, 'sound/machines/defib_success.ogg', 50, 1)
+ apcs_disabled++
+ return
// Decent chance to overload lighting circuit.
- if(prob(6))
+ if(prob(30))
A.overload_lighting()
-
- // Small chance to make the APC turn off for awhile.
- // This will actually protect it from further damage.
- if(prob(3))
- A.energy_fail(rand(20, 40))
+ log_debug("ELECTRICAL EVENT: Overloaded \the [A]'s lighting.")
+ playsound(A, 'sound/effects/lightningshock.ogg', 50, 1)
+ apcs_overloaded++
// Relatively small chance to emag the apc as apc_damage event does.
- if(prob(1))
+ if(prob(5))
A.emagged = TRUE
A.update_icon()
+ log_debug("ELECTRICAL EVENT: Emagged \the [A].")
+ playsound(A, 'sound/machines/chime.ogg', 50, 1)
+ apcs_emagged++
-/*
-/datum/event/electrical_storm/start()
- ..()
- valid_apcs = list()
- for(var/obj/machinery/power/apc/A in global.machines)
- if(A.z in affecting_z)
- valid_apcs.Add(A)
- endWhen = (severity * 60) + startWhen
-
-/datum/event/electrical_storm/tick()
- ..()
- // See if shields can stop it first (It would be nice to port baystation's cooler shield gens perhaps)
- // TODO - We need a better shield generator system to handle this properly.
- if(!valid_apcs.len)
- log_debug("No valid APCs found for electrical storm event ship=[victim]!")
- return
- var/list/picked_apcs = list()
- for(var/i=0, i< severity * 2, i++) // up to 2/4/6 APCs per tick depending on severity
- picked_apcs |= pick(valid_apcs)
- for(var/obj/machinery/power/apc/T in picked_apcs)
- affect_apc(T)
-
-/datum/event/electrical_storm/proc/affect_apc(var/obj/machinery/power/apc/T)
- // Main breaker is turned off. Consider this APC protected.
- if(!T.operating)
- return
-
- // Decent chance to overload lighting circuit.
- if(prob(3 * severity))
- T.overload_lighting()
-
- // Relatively small chance to emag the apc as apc_damage event does.
- if(prob(0.2 * severity))
- T.emagged = 1
- T.update_icon()
-*/
diff --git a/code/modules/gamemaster/event2/events/everyone/gravity.dm b/code/modules/gamemaster/event2/events/everyone/gravity.dm
new file mode 100644
index 0000000000..abfa22ec12
--- /dev/null
+++ b/code/modules/gamemaster/event2/events/everyone/gravity.dm
@@ -0,0 +1,34 @@
+/datum/event2/meta/gravity
+ name = "gravity failure"
+ departments = list(DEPARTMENT_EVERYONE)
+ chaos = 20
+ chaotic_threshold = EVENT_CHAOS_THRESHOLD_LOW_IMPACT
+ reusable = TRUE
+ event_type = /datum/event2/event/gravity
+
+/datum/event2/meta/gravity/get_weight()
+ return (20 + (metric.count_people_in_department(DEPARTMENT_EVERYONE) * 20)) / (times_ran + 1)
+
+
+
+
+/datum/event2/event/gravity
+ length_lower_bound = 5 MINUTES
+ length_upper_bound = 10 MINUTES
+
+/datum/event2/event/gravity/announce()
+ command_announcement.Announce("Feedback surge detected in mass-distributions systems. \
+ Artificial gravity has been disabled whilst the system reinitializes. \
+ Please stand by while the gravity system reinitializes.", "Gravity Failure")
+
+/datum/event2/event/gravity/start()
+ for(var/area/A in all_areas)
+ if(A.z in get_location_z_levels())
+ A.gravitychange(FALSE, A)
+
+/datum/event2/event/gravity/end()
+ for(var/area/A in all_areas)
+ if(A.z in get_location_z_levels())
+ A.gravitychange(TRUE, A)
+
+ command_announcement.Announce("Gravity generators are again functioning within normal parameters. Sorry for any inconvenience.", "Gravity Restored")
\ No newline at end of file
diff --git a/code/modules/gamemaster/event2/events/everyone/infestation.dm b/code/modules/gamemaster/event2/events/everyone/infestation.dm
new file mode 100644
index 0000000000..f51456f362
--- /dev/null
+++ b/code/modules/gamemaster/event2/events/everyone/infestation.dm
@@ -0,0 +1,72 @@
+/datum/event2/meta/infestation
+ event_class = "infestation"
+ departments = list(DEPARTMENT_EVERYONE)
+
+/datum/event2/meta/infestation/get_weight()
+ return metric.count_people_in_department(DEPARTMENT_EVERYONE) * 10
+
+/datum/event2/meta/infestation/rodents
+ name = "infestation - rodents"
+ event_type = /datum/event2/event/infestation/rodents
+
+/datum/event2/meta/infestation/lizards
+ name = "infestation - lizards"
+ event_type = /datum/event2/event/infestation/lizards
+
+/datum/event2/meta/infestation/spiderlings
+ name = "infestation - spiders"
+ event_type = /datum/event2/event/infestation/spiderlings
+
+
+/datum/event2/event/infestation
+ var/vermin_string = null
+ var/max_vermin = 0
+ var/list/things_to_spawn = list()
+
+ var/list/turfs = list()
+
+/datum/event2/event/infestation/rodents
+ vermin_string = "rodents"
+ max_vermin = 12
+ things_to_spawn = list(
+ /mob/living/simple_mob/animal/passive/mouse/gray,
+ /mob/living/simple_mob/animal/passive/mouse/brown,
+ /mob/living/simple_mob/animal/passive/mouse/white,
+ /mob/living/simple_mob/animal/passive/mouse/rat
+ )
+
+/datum/event2/event/infestation/lizards
+ vermin_string = "lizards"
+ max_vermin = 6
+ things_to_spawn = list(
+ /mob/living/simple_mob/animal/passive/lizard,
+ /mob/living/simple_mob/animal/passive/lizard/large,
+ /mob/living/simple_mob/animal/passive/lizard/large/defensive
+ )
+
+/datum/event2/event/infestation/spiderlings
+ vermin_string = "spiders"
+ max_vermin = 3
+ things_to_spawn = list(/obj/effect/spider/spiderling/non_growing)
+
+
+/datum/event2/event/infestation/set_up()
+ turfs = find_random_turfs(max_vermin)
+ if(!turfs.len)
+ log_debug("Infestation event failed to find any valid turfs. Aborting.")
+ abort()
+ return
+
+/datum/event2/event/infestation/announce()
+ var/turf/T = turfs[1]
+ command_announcement.Announce("Bioscans indicate that [vermin_string] have been breeding \
+ in \the [T.loc]. Clear them out, before this starts to affect productivity.", "Vermin infestation")
+
+
+/datum/event2/event/infestation/start()
+ var/vermin_to_spawn = rand(2, max_vermin)
+ for(var/i = 1 to vermin_to_spawn)
+ var/turf/T = pick(turfs)
+ turfs -= T
+ var/spawn_type = pick(things_to_spawn)
+ new spawn_type(T)
diff --git a/code/modules/gamemaster/event2/events/everyone/pda_spam.dm b/code/modules/gamemaster/event2/events/everyone/pda_spam.dm
new file mode 100644
index 0000000000..d8eceffe06
--- /dev/null
+++ b/code/modules/gamemaster/event2/events/everyone/pda_spam.dm
@@ -0,0 +1,268 @@
+/datum/event2/meta/pda_spam
+ name = "pda spam"
+ departments = list(DEPARTMENT_EVERYONE)
+ event_type = /datum/event2/event/pda_spam
+
+/datum/event2/meta/pda_spam/get_weight()
+ return metric.count_people_in_department(DEPARTMENT_EVERYONE) * 2
+
+
+/datum/event2/event/pda_spam
+ length_lower_bound = 30 MINUTES
+ length_upper_bound = 1 HOUR
+ var/spam_debug = FALSE // If true, notices of the event sending spam go to `log_debug()`.
+ var/last_spam_time = null // world.time of most recent spam.
+ var/next_spam_attempt_time = 0 // world.time of next attempt to try to spam.
+ var/give_up_after = 5 MINUTES
+ var/obj/machinery/message_server/MS = null
+ var/obj/machinery/exonet_node/node = null
+
+/datum/event2/event/pda_spam/set_up()
+ last_spam_time = world.time // So it won't immediately give up.
+ MS = pick_message_server()
+ node = get_exonet_node()
+
+/datum/event2/event/pda_spam/event_tick()
+ if(!can_spam())
+ return
+
+ if(world.time < next_spam_attempt_time)
+ return
+
+ next_spam_attempt_time = world.time + rand(30 SECONDS, 2 MINUTES)
+
+ var/obj/item/device/pda/P = null
+ var/list/viables = list()
+
+ for(var/obj/item/device/pda/check_pda in sortAtom(PDAs))
+ if(!check_pda.owner || check_pda.toff || check_pda.hidden || check_pda.spam_proof)
+ continue
+ viables += check_pda
+
+ if(!viables.len)
+ return
+
+ P = pick(viables)
+ var/list/spam = generate_spam()
+
+ if(MS.send_pda_message("[P.owner]", spam[1], spam[2])) // Message been filtered by spam filter.
+ return
+
+ send_spam(P, spam[1], spam[2])
+
+
+/datum/event2/event/pda_spam/should_end()
+ . = ..()
+ if(!.)
+ // Give up if nobody was reachable for five minutes.
+ if(last_spam_time + give_up_after < world.time)
+ log_debug("PDA Spam event giving up after not being able to spam for awhile.")
+ return TRUE
+
+/datum/event2/event/pda_spam/proc/can_spam()
+ if(!node || !node.on || !node.allow_external_PDAs)
+ node = get_exonet_node()
+ return FALSE
+
+ if(!MS || !MS.active)
+ MS = pick_message_server()
+ return FALSE
+
+ return TRUE
+
+// Returns a list containing two items, the sender and message.
+/datum/event2/event/pda_spam/proc/generate_spam()
+ var/sender = null
+ var/message = null
+ switch(rand(1, 7))
+ if(1)
+ sender = pick("MaxBet","MaxBet Online Casino","There is no better time to register","I'm excited for you to join us")
+ message = pick("Triple deposits are waiting for you at MaxBet Online when you register to play with us.",\
+ "You can qualify for a 200% Welcome Bonus at MaxBet Online when you sign up today.",\
+ "Once you are a player with MaxBet, you will also receive lucrative weekly and monthly promotions.",\
+ "You will be able to enjoy over 450 top-flight casino games at MaxBet.")
+ if(2)
+ sender = pick(300;"QuickDatingSystem",200;"Find your russian bride",50;"Tajaran beauties are waiting",50;"Find your secret skrell crush",50;"Beautiful unathi brides")
+ message = pick("Your profile caught my attention and I wanted to write and say hello (QuickDating).",\
+ "If you will write to me on my email [pick(first_names_female)]@[pick(last_names)].[pick("ru","ck","tj","ur","nt")] I shall necessarily send you a photo (QuickDating).",\
+ "I want that we write each other and I hope, that you will like my profile and you will answer me (QuickDating).",\
+ "You have (1) new message!",\
+ "You have (2) new profile views!")
+ if(3)
+ sender = pick("Galactic Payments Association","Better Business Bureau","[using_map.starsys_name] E-Payments","NAnoTransen Finance Deparmtent","Luxury Replicas")
+ message = pick("Luxury watches for Blowout sale prices!",\
+ "Watches, Jewelry & Accessories, Bags & Wallets !",\
+ "Deposit 100$ and get 300$ totally free!",\
+ " 100K NT.|WOWGOLD õnly $89 ",\
+ "We have been filed with a complaint from one of your customers in respect of their business relations with you.",\
+ "We kindly ask you to open the COMPLAINT REPORT (attached) to reply on this complaint..")
+ if(4)
+ sender = pick("Buy Dr. Maxman","Having dysfuctional troubles?")
+ message = pick("DR MAXMAN: REAL Doctors, REAL Science, REAL Results!",\
+ "Dr. Maxman was created by George Acuilar, M.D, a [using_map.boss_short] Certified Urologist who has treated over 70,000 patients sector wide with 'male problems'.",\
+ "After seven years of research, Dr Acuilar and his team came up with this simple breakthrough male enhancement formula.",\
+ "Men of all species report AMAZING increases in length, width and stamina.")
+ if(5)
+ sender = pick("Dr","Crown prince","King Regent","Professor","Captain")
+ sender += " " + pick("Robert","Alfred","Josephat","Kingsley","Sehi","Zbahi")
+ sender += " " + pick("Mugawe","Nkem","Gbatokwia","Nchekwube","Ndim","Ndubisi")
+ message = pick("YOUR FUND HAS BEEN MOVED TO [uppertext(pick("Salusa","Segunda","Cepheus","Andromeda","Gruis","Corona","Aquila","ARES","Asellus"))] DEVELOPMENTARY BANK FOR ONWARD REMITTANCE.",\
+ "We are happy to inform you that due to the delay, we have been instructed to IMMEDIATELY deposit all funds into your account",\
+ "Dear fund beneficiary, We have please to inform you that overdue funds payment has finally been approved and released for payment",\
+ "Due to my lack of agents I require an off-world financial account to immediately deposit the sum of 1 POINT FIVE MILLION credits.",\
+ "Greetings sir, I regretfully to inform you that as I lay dying here due to my lack ofheirs I have chosen you to recieve the full sum of my lifetime savings of 1.5 billion credits")
+ if(6)
+ sender = pick("[using_map.company_name] Morale Divison","Feeling Lonely?","Bored?","www.wetskrell.nt")
+ message = pick("The [using_map.company_name] Morale Division wishes to provide you with quality entertainment sites.",\
+ "WetSkrell.nt is a xenophillic website endorsed by NT for the use of male crewmembers among it's many stations and outposts.",\
+ "Wetskrell.nt only provides the higest quality of male entertaiment to [using_map.company_name] Employees.",\
+ "Simply enter your [using_map.company_name] Bank account system number and pin. With three easy steps this service could be yours!")
+ if(7)
+ sender = pick("You have won free tickets!","Click here to claim your prize!","You are the 1000th vistor!","You are our lucky grand prize winner!")
+ message = pick("You have won tickets to the newest ACTION JAXSON MOVIE!",\
+ "You have won tickets to the newest crime drama DETECTIVE MYSTERY IN THE CLAMITY CAPER!",\
+ "You have won tickets to the newest romantic comedy 16 RULES OF LOVE!",\
+ "You have won tickets to the newest thriller THE CULT OF THE SLEEPING ONE!")
+ return list(sender, message)
+
+/datum/event2/event/pda_spam/proc/send_spam(obj/item/device/pda/P, sender, message)
+ last_spam_time = world.time
+ P.spam_message(sender, message)
+ if(spam_debug)
+ log_debug("PDA Spam event sent spam to \the [P].")
+
+
+/datum/event2/event/pda_spam/proc/pick_message_server()
+ if(LAZYLEN(message_servers))
+ return pick(message_servers)
+
+
+/*
+/datum/event/pda_spam
+ endWhen = 36000
+ var/last_spam_time = 0
+ var/obj/machinery/message_server/useMS
+ var/obj/machinery/exonet_node/node
+
+/datum/event/pda_spam/setup()
+ last_spam_time = world.time
+ pick_message_server()
+
+/datum/event/pda_spam/proc/pick_message_server()
+ if(message_servers)
+ for (var/obj/machinery/message_server/MS in message_servers)
+ if(MS.active)
+ useMS = MS
+ break
+
+/datum/event/pda_spam/tick()
+ if(world.time > last_spam_time + 3000)
+ //if there's no spam managed to get to receiver for five minutes, give up
+ kill()
+ return
+ if(!node)
+ node = get_exonet_node()
+
+ if(!node || !node.on || !node.allow_external_PDAs)
+ return
+
+ if(!useMS || !useMS.active)
+ useMS = null
+ pick_message_server()
+
+ if(useMS)
+ if(prob(5))
+ // /obj/machinery/message_server/proc/send_pda_message(var/recipient = "",var/sender = "",var/message = "")
+ var/obj/item/device/pda/P
+ var/list/viables = list()
+ for(var/obj/item/device/pda/check_pda in sortAtom(PDAs))
+ if (!check_pda.owner||check_pda.toff||check_pda == src||check_pda.hidden)
+ continue
+ viables.Add(check_pda)
+
+ if(!viables.len)
+ return
+ P = pick(viables)
+
+ var/sender
+ var/message
+ switch(pick(1,2,3,4,5,6,7))
+ if(1)
+ sender = pick("MaxBet","MaxBet Online Casino","There is no better time to register","I'm excited for you to join us")
+ message = pick("Triple deposits are waiting for you at MaxBet Online when you register to play with us.",\
+ "You can qualify for a 200% Welcome Bonus at MaxBet Online when you sign up today.",\
+ "Once you are a player with MaxBet, you will also receive lucrative weekly and monthly promotions.",\
+ "You will be able to enjoy over 450 top-flight casino games at MaxBet.")
+ if(2)
+ sender = pick(300;"QuickDatingSystem",200;"Find your russian bride",50;"Tajaran beauties are waiting",50;"Find your secret skrell crush",50;"Beautiful unathi brides")
+ message = pick("Your profile caught my attention and I wanted to write and say hello (QuickDating).",\
+ "If you will write to me on my email [pick(first_names_female)]@[pick(last_names)].[pick("ru","ck","tj","ur","nt")] I shall necessarily send you a photo (QuickDating).",\
+ "I want that we write each other and I hope, that you will like my profile and you will answer me (QuickDating).",\
+ "You have (1) new message!",\
+ "You have (2) new profile views!")
+ if(3)
+ sender = pick("Galactic Payments Association","Better Business Bureau","[using_map.starsys_name] E-Payments","NAnoTransen Finance Deparmtent","Luxury Replicas")
+ message = pick("Luxury watches for Blowout sale prices!",\
+ "Watches, Jewelry & Accessories, Bags & Wallets !",\
+ "Deposit 100$ and get 300$ totally free!",\
+ " 100K NT.|WOWGOLD õnly $89 ",\
+ "We have been filed with a complaint from one of your customers in respect of their business relations with you.",\
+ "We kindly ask you to open the COMPLAINT REPORT (attached) to reply on this complaint..")
+ if(4)
+ sender = pick("Buy Dr. Maxman","Having dysfuctional troubles?")
+ message = pick("DR MAXMAN: REAL Doctors, REAL Science, REAL Results!",\
+ "Dr. Maxman was created by George Acuilar, M.D, a [using_map.boss_short] Certified Urologist who has treated over 70,000 patients sector wide with 'male problems'.",\
+ "After seven years of research, Dr Acuilar and his team came up with this simple breakthrough male enhancement formula.",\
+ "Men of all species report AMAZING increases in length, width and stamina.")
+ if(5)
+ sender = pick("Dr","Crown prince","King Regent","Professor","Captain")
+ sender += " " + pick("Robert","Alfred","Josephat","Kingsley","Sehi","Zbahi")
+ sender += " " + pick("Mugawe","Nkem","Gbatokwia","Nchekwube","Ndim","Ndubisi")
+ message = pick("YOUR FUND HAS BEEN MOVED TO [pick("Salusa","Segunda","Cepheus","Andromeda","Gruis","Corona","Aquila","ARES","Asellus")] DEVELOPMENTARY BANK FOR ONWARD REMITTANCE.",\
+ "We are happy to inform you that due to the delay, we have been instructed to IMMEDIATELY deposit all funds into your account",\
+ "Dear fund beneficiary, We have please to inform you that overdue funds payment has finally been approved and released for payment",\
+ "Due to my lack of agents I require an off-world financial account to immediately deposit the sum of 1 POINT FIVE MILLION credits.",\
+ "Greetings sir, I regretfully to inform you that as I lay dying here due to my lack ofheirs I have chosen you to recieve the full sum of my lifetime savings of 1.5 billion credits")
+ if(6)
+ sender = pick("[using_map.company_name] Morale Divison","Feeling Lonely?","Bored?","www.wetskrell.nt")
+ message = pick("The [using_map.company_name] Morale Division wishes to provide you with quality entertainment sites.",\
+ "WetSkrell.nt is a xenophillic website endorsed by NT for the use of male crewmembers among it's many stations and outposts.",\
+ "Wetskrell.nt only provides the higest quality of male entertaiment to [using_map.company_name] Employees.",\
+ "Simply enter your [using_map.company_name] Bank account system number and pin. With three easy steps this service could be yours!")
+ if(7)
+ sender = pick("You have won free tickets!","Click here to claim your prize!","You are the 1000th vistor!","You are our lucky grand prize winner!")
+ message = pick("You have won tickets to the newest ACTION JAXSON MOVIE!",\
+ "You have won tickets to the newest crime drama DETECTIVE MYSTERY IN THE CLAMITY CAPER!",\
+ "You have won tickets to the newest romantic comedy 16 RULES OF LOVE!",\
+ "You have won tickets to the newest thriller THE CULT OF THE SLEEPING ONE!")
+
+ if (useMS.send_pda_message("[P.owner]", sender, message)) //Message been filtered by spam filter.
+ return
+
+ last_spam_time = world.time
+
+ if (prob(50)) //Give the AI an increased chance to intercept the message
+ for(var/mob/living/silicon/ai/ai in mob_list)
+ // Allows other AIs to intercept the message but the AI won't intercept their own message.
+ if(ai.aiPDA != P && ai.aiPDA != src)
+ ai.show_message("Intercepted message from [sender] (Unknown / spam?) to [P:owner]: [message]")
+
+ //Commented out because we don't send messages like this anymore. Instead it will just popup in their chat window.
+ //P.tnote += "← From [sender] (Unknown / spam?):
[message]
"
+
+ if (!P.message_silent)
+ playsound(P.loc, 'sound/machines/twobeep.ogg', 50, 1)
+ for (var/mob/O in hearers(3, P.loc))
+ if(!P.message_silent) O.show_message(text("\icon[P] *[P.ttone]*"))
+ //Search for holder of the PDA.
+ var/mob/living/L = null
+ if(P.loc && isliving(P.loc))
+ L = P.loc
+ //Maybe they are a pAI!
+ else
+ L = get(P, /mob/living/silicon)
+
+ if(L)
+ to_chat(L, "\icon[P] Message from [sender] (Unknown / spam?), \"[message]\" (Unable to Reply)")
+
+*/
\ No newline at end of file
diff --git a/code/modules/gamemaster/event2/events/everyone/radiation_storm.dm b/code/modules/gamemaster/event2/events/everyone/radiation_storm.dm
new file mode 100644
index 0000000000..ebd137b404
--- /dev/null
+++ b/code/modules/gamemaster/event2/events/everyone/radiation_storm.dm
@@ -0,0 +1,49 @@
+/datum/event2/meta/radiation_storm
+ name = "radiation storm"
+ departments = list(DEPARTMENT_EVERYONE)
+ chaos = 20
+ chaotic_threshold = EVENT_CHAOS_THRESHOLD_MEDIUM_IMPACT
+ event_type = /datum/event2/event/radiation_storm
+
+/datum/event2/meta/radiation_storm/get_weight()
+ var/medical_factor = metric.count_people_in_department(DEPARTMENT_MEDICAL) * 10
+ var/population_factor = metric.count_people_in_department(DEPARTMENT_EVERYONE) * 5 // Note medical people will get counted twice at 25 weight.
+ return 20 + medical_factor + population_factor
+
+
+
+/datum/event2/event/radiation_storm
+ start_delay_lower_bound = 1 MINUTE
+ length_lower_bound = 1 MINUTE
+
+/datum/event2/event/radiation_storm/announce()
+ command_announcement.Announce("High levels of radiation detected near \the [location_name()]. \
+ Please evacuate into one of the shielded maintenance tunnels.", "Anomaly Alert", new_sound = 'sound/AI/radiation.ogg')
+ make_maint_all_access()
+
+/datum/event2/event/radiation_storm/start()
+ command_announcement.Announce("The station has entered the radiation belt. \
+ Please remain in a sheltered area until we have passed the radiation belt.", "Anomaly Alert")
+
+/datum/event2/event/radiation_storm/event_tick()
+ radiate()
+
+/datum/event2/event/radiation_storm/proc/radiate()
+ var/radiation_level = rand(15, 35)
+ for(var/z in using_map.station_levels)
+ SSradiation.z_radiate(locate(1, 1, z), radiation_level, 1)
+
+/datum/event2/event/radiation_storm/end()
+ command_announcement.Announce("The station has passed the radiation belt. \
+ Please allow for up to one minute while radiation levels dissipate, and report to \
+ medbay if you experience any unusual symptoms. Maintenance will lose all \
+ access again shortly.", "Anomaly Alert")
+ addtimer(CALLBACK(src, .proc/maint_callback), 2 MINUTES)
+
+/datum/event2/event/radiation_storm/proc/maint_callback()
+ revoke_maint_all_access()
+
+
+// There is no actual radiation during a fake storm.
+/datum/event2/event/radiation_storm/fake/radiate()
+ return
diff --git a/code/modules/gamemaster/event2/events/everyone/random_antag.dm b/code/modules/gamemaster/event2/events/everyone/random_antag.dm
new file mode 100644
index 0000000000..fe3b58be8c
--- /dev/null
+++ b/code/modules/gamemaster/event2/events/everyone/random_antag.dm
@@ -0,0 +1,31 @@
+// No idea if this is needed for autotraitor or not.
+// If it is, it shouldn't depend on the event system, but fixing that would be it's own project.
+// If not, it can stay off until an admin wants to play with it.
+
+/datum/event2/meta/random_antagonist
+ name = "random antagonist"
+ enabled = FALSE
+ reusable = TRUE
+ chaos = 0 // This is zero due to the event system not being able to know if an antag actually got spawned or not.
+ departments = list(DEPARTMENT_EVERYONE)
+ chaotic_threshold = EVENT_CHAOS_THRESHOLD_MEDIUM_IMPACT
+ event_type = /datum/event2/event/random_antagonist
+
+// This has an abnormally high weight due to antags being very important for the round,
+// however the weight will decay with more antags, and more attempts to add antags.
+/datum/event2/meta/random_antagonist/get_weight()
+ var/antags = metric.count_all_antags()
+ return 200 / (antags + times_ran + 1)
+
+
+
+// The random spawn proc on the antag datum will handle announcing the spawn and whatnot, in theory.
+/datum/event2/event/random_antagonist/start()
+ var/list/valid_types = list()
+ for(var/antag_type in all_antag_types)
+ var/datum/antagonist/antag = all_antag_types[antag_type]
+ if(antag.flags & ANTAG_RANDSPAWN)
+ valid_types |= antag
+ if(valid_types.len)
+ var/datum/antagonist/antag = pick(valid_types)
+ antag.attempt_random_spawn()
diff --git a/code/modules/gamemaster/event2/events/everyone/sudden_weather_shift.dm b/code/modules/gamemaster/event2/events/everyone/sudden_weather_shift.dm
new file mode 100644
index 0000000000..484d226bb8
--- /dev/null
+++ b/code/modules/gamemaster/event2/events/everyone/sudden_weather_shift.dm
@@ -0,0 +1,82 @@
+/datum/event2/meta/sudden_weather_shift
+ name = "sudden weather shift"
+ departments = list(DEPARTMENT_EVERYONE)
+ reusable = TRUE
+ event_type = /datum/event2/event/sudden_weather_shift
+
+/datum/event2/meta/sudden_weather_shift/get_weight()
+ // The proc name is a bit misleading, it only counts players outside, not all mobs.
+ return (metric.count_all_outdoor_mobs() * 20) / (times_ran + 1)
+
+/datum/event2/event/sudden_weather_shift
+ start_delay_lower_bound = 30 SECONDS
+ start_delay_upper_bound = 1 MINUTE
+ var/datum/planet/chosen_planet = null
+
+/datum/event2/event/sudden_weather_shift/set_up()
+ if(!LAZYLEN(SSplanets.planets))
+ log_debug("Weather shift event was ran when no planets exist. Aborting.")
+ abort()
+ return
+
+ chosen_planet = pick(SSplanets.planets)
+
+/datum/event2/event/sudden_weather_shift/announce()
+ if(!chosen_planet)
+ return
+ command_announcement.Announce("Local weather patterns on [chosen_planet.name] suggest that a \
+ sudden atmospheric fluctuation has occurred. All groundside personnel should be wary of \
+ rapidly deteriorating conditions.", "Weather Alert")
+
+/datum/event2/event/sudden_weather_shift/start()
+ // Using the roundstart weather list is handy, because it avoids the chance of choosing a bus-only weather.
+ // It also makes this event generic and suitable for other planets besides the main one, with no additional code needed.
+ // Only flaw is that roundstart weathers are -usually- safe ones, but we can fix that by tweaking a copy of it.
+ var/list/weather_choices = chosen_planet.weather_holder.roundstart_weather_chances.Copy()
+ var/list/new_weather_weights = list()
+
+ // A lazy way of inverting the odds is to use some division.
+ for(var/weather in weather_choices)
+ new_weather_weights[weather] = 100 / weather_choices[weather]
+
+ // Now choose a new weather.
+ var/new_weather = pickweight(new_weather_weights)
+ log_debug("Sudden weather shift event is now changing [chosen_planet.name]'s weather to [new_weather].")
+ chosen_planet.weather_holder.change_weather(new_weather)
+
+/*
+/datum/gm_action/planet_weather_shift
+ name = "sudden weather shift"
+ enabled = TRUE
+ departments = list(DEPARTMENT_EVERYONE)
+ reusable = TRUE
+ var/datum/planet/target_planet
+
+ var/list/banned_weathers = list(
+ /datum/weather/sif/ash_storm,
+ /datum/weather/sif/emberfall,
+ /datum/weather/sif/blood_moon,
+ /datum/weather/sif/fallout)
+ var/list/possible_weathers = list()
+
+/datum/gm_action/planet_weather_shift/set_up()
+ if(!target_planet || isnull(target_planet))
+ target_planet = pick(SSplanets.planets)
+ possible_weathers |= target_planet.weather_holder.allowed_weather_types
+ possible_weathers -= banned_weathers
+ return
+
+/datum/gm_action/planet_weather_shift/get_weight()
+ return max(0, -15 + (metric.count_all_outdoor_mobs() * 20))
+
+/datum/gm_action/planet_weather_shift/start()
+ ..()
+ var/new_weather = pick(possible_weathers)
+ target_planet.weather_holder.change_weather(new_weather)
+
+/datum/gm_action/planet_weather_shift/announce()
+ spawn(rand(3 SECONDS, 2 MINUTES))
+ command_announcement.Announce("Local weather patterns on [target_planet.name] suggest that a sudden atmospheric fluctuation has occurred. All groundside personnel should be wary of rapidly deteriorating conditions.", "Weather Alert")
+ return
+
+*/
\ No newline at end of file
diff --git a/code/modules/gamemaster/event2/events/ghost_pod_spawner.dm b/code/modules/gamemaster/event2/events/ghost_pod_spawner.dm
new file mode 100644
index 0000000000..4c462d06d6
--- /dev/null
+++ b/code/modules/gamemaster/event2/events/ghost_pod_spawner.dm
@@ -0,0 +1,21 @@
+// Generic subtype for events that make ghost pods.
+
+/datum/event2/event/ghost_pod_spawner
+ var/pod_type = null
+ var/list/desired_turf_areas = list() // If this is left empty, it will default to a global list of 'station' turfs.
+ var/list/free_turfs = list()
+
+/datum/event2/event/ghost_pod_spawner/set_up()
+ free_turfs = find_random_turfs(5, desired_turf_areas)
+
+ if(!free_turfs.len)
+ log_debug("Ghost Pod Spawning event failed to find a place to spawn. Aborting.")
+ abort()
+ return
+
+/datum/event2/event/ghost_pod_spawner/start()
+ var/obj/structure/ghost_pod/pod = new pod_type(pick(free_turfs))
+ post_pod_creation(pod)
+
+// Override to do things to the pod after it's spawned.
+/datum/event2/event/ghost_pod_spawner/proc/post_pod_creation(obj/structure/ghost_pod/pod)
\ No newline at end of file
diff --git a/code/modules/gamemaster/event2/events/legacy.dm b/code/modules/gamemaster/event2/events/legacy.dm
deleted file mode 100644
index f1663c546a..0000000000
--- a/code/modules/gamemaster/event2/events/legacy.dm
+++ /dev/null
@@ -1,54 +0,0 @@
-// This is a somewhat special type of event, that bridges to the old event datum and makes it work with the new system.
-// This is possible because the new datum is mostly superset of the old one.
-/datum/event2/event/legacy
- var/datum/event/legacy_event = null
- var/tick_count = 0 // Used to emulate legacy's `activeFor` tick counter.
-
-/datum/event2/meta/legacy/get_weight()
- return 50
-
-/datum/event2/event/legacy/process()
- ..()
- tick_count++
-
-/datum/event2/event/legacy/set_up()
- legacy_event = new legacy_event(null, external_use = TRUE)
- legacy_event.setup()
-
-/datum/event2/event/legacy/should_announce()
- return tick_count >= legacy_event.announceWhen
-
-/datum/event2/event/legacy/announce()
- legacy_event.announce()
-
-
-// Legacy events don't tick before they start, so we don't need to do `wait_tick()`.
-
-/datum/event2/event/legacy/should_start()
- return tick_count >= legacy_event.startWhen
-
-/datum/event2/event/legacy/start()
- legacy_event.start()
-
-/datum/event2/event/legacy/event_tick()
- legacy_event.tick()
-
-
-/datum/event2/event/legacy/should_end()
- return tick_count >= legacy_event.endWhen
-
-/datum/event2/event/legacy/end()
- legacy_event.end()
-
-/datum/event2/event/legacy/finish()
- legacy_event.kill(external_use = TRUE)
- ..()
-
-// Testing.
-/datum/event2/meta/legacy_gravity
- name = "gravity (legacy)"
- reusable = TRUE
- event_type = /datum/event2/event/legacy/gravity
-
-/datum/event2/event/legacy/gravity
- legacy_event = /datum/event/gravity
\ No newline at end of file
diff --git a/code/modules/gamemaster/event2/events/legacy/legacy.dm b/code/modules/gamemaster/event2/events/legacy/legacy.dm
index 3b69ab8d7a..6f70cafa85 100644
--- a/code/modules/gamemaster/event2/events/legacy/legacy.dm
+++ b/code/modules/gamemaster/event2/events/legacy/legacy.dm
@@ -51,11 +51,13 @@
legacy_event.kill(external_use = TRUE)
..()
-// Testing.
+// Proof of concept.
+/*
/datum/event2/meta/legacy_gravity
name = "gravity (legacy)"
reusable = TRUE
event_type = /datum/event2/event/legacy/gravity
/datum/event2/event/legacy/gravity
- legacy_event = /datum/event/gravity
\ No newline at end of file
+ legacy_event = /datum/event/gravity
+*/
\ No newline at end of file
diff --git a/code/modules/gamemaster/event2/events/medical/appendicitis.dm b/code/modules/gamemaster/event2/events/medical/appendicitis.dm
new file mode 100644
index 0000000000..27f5284020
--- /dev/null
+++ b/code/modules/gamemaster/event2/events/medical/appendicitis.dm
@@ -0,0 +1,36 @@
+/datum/event2/meta/appendicitis
+ name = "appendicitis"
+ departments = list(DEPARTMENT_MEDICAL)
+ chaos = 40
+ chaotic_threshold = EVENT_CHAOS_THRESHOLD_MEDIUM_IMPACT
+ event_type = /datum/event2/event/appendicitis
+
+/datum/event2/meta/appendicitis/get_weight()
+ var/list/doctors = metric.get_people_with_job(/datum/job/doctor)
+
+ doctors -= metric.get_people_with_alt_title(/datum/job/doctor, /datum/alt_title/nurse)
+ doctors -= metric.get_people_with_alt_title(/datum/job/doctor, /datum/alt_title/virologist)
+ doctors += metric.get_people_with_job(/datum/job/cmo)
+
+ return doctors.len * 10
+
+
+
+/datum/event2/event/appendicitis/start()
+ for(var/mob/living/carbon/human/H in shuffle(living_mob_list))
+ // Don't do it to SSD people.
+ if(!H.client)
+ continue
+
+ // Or antags.
+ if(player_is_antag(H.mind))
+ continue
+
+ // Or doctors (otherwise it could be possible for the only surgeon to need surgery).
+ if(H in metric.get_people_with_job(/datum/job/doctor) )
+ continue
+
+ if(H.appendicitis())
+ log_debug("Appendicitis event gave appendicitis to \the [H].")
+ return
+ log_debug("Appendicitis event could not find a valid victim.")
diff --git a/code/modules/gamemaster/event2/events/medical/virus.dm b/code/modules/gamemaster/event2/events/medical/virus.dm
new file mode 100644
index 0000000000..58791d2ad1
--- /dev/null
+++ b/code/modules/gamemaster/event2/events/medical/virus.dm
@@ -0,0 +1,69 @@
+/datum/event2/meta/virus
+ name = "viral infection"
+ event_class = "virus"
+ departments = list(DEPARTMENT_MEDICAL, DEPARTMENT_EVERYONE)
+ chaos = 40
+ chaotic_threshold = EVENT_CHAOS_THRESHOLD_HIGH_IMPACT
+ event_type = /datum/event2/event/virus
+
+/datum/event2/meta/virus/superbug
+ name = "viral superbug"
+ chaos = 60
+ event_type = /datum/event2/event/virus/superbug
+
+/datum/event2/meta/virus/outbreak
+ name = "viral outbreak"
+ chaos = 60
+ event_type = /datum/event2/event/virus/outbreak
+
+/datum/event2/meta/virus/get_weight()
+ var/list/virologists = metric.get_people_with_alt_title(/datum/job/doctor, /datum/alt_title/virologist)
+ virologists += metric.get_people_with_job(/datum/job/cmo)
+
+ return virologists.len * 25
+
+
+
+/datum/event2/event/virus
+ announce_delay_lower_bound = 1 MINUTE
+ announce_delay_upper_bound = 3 MINUTES
+ var/number_of_viruses = 1
+ var/virus_power = 2 // Ranges from 1 to 3, with 1 being the weakest.
+ var/list/candidates = list()
+
+// A single powerful virus.
+/datum/event2/event/virus/superbug
+ virus_power = 3
+
+// A lot of weaker viruses.
+/datum/event2/event/virus/outbreak
+ virus_power = 1
+ number_of_viruses = 3
+
+
+
+/datum/event2/event/virus/set_up()
+ for(var/mob/living/carbon/human/H in player_list)
+ if(H.client && !H.isSynthetic() && H.stat != DEAD && !player_is_antag(H.mind))
+ candidates += H
+ candidates = shuffle(candidates)
+
+/datum/event2/event/virus/announce()
+ command_announcement.Announce("Confirmed outbreak of level 7 biohazard aboard \the [location_name()]. \
+ All personnel must contain the outbreak.", "Biohazard Alert", new_sound = 'sound/AI/outbreak7.ogg')
+
+/datum/event2/event/virus/start()
+ if(!candidates.len)
+ log_debug("Virus event could not find any valid targets to infect. Aborting.")
+ abort()
+ return
+
+ for(var/i = 1 to number_of_viruses)
+ var/mob/living/carbon/human/H = LAZYACCESS(candidates, 1)
+ if(!H)
+ return
+ var/datum/disease2/disease/D = new()
+ D.makerandom(virus_power)
+ log_debug("Virus event is now infecting \the [H] with a new random virus.")
+ infect_mob(H, D)
+ candidates -= H
diff --git a/code/modules/gamemaster/event2/events/mob_spawning.dm b/code/modules/gamemaster/event2/events/mob_spawning.dm
new file mode 100644
index 0000000000..78b1eac69c
--- /dev/null
+++ b/code/modules/gamemaster/event2/events/mob_spawning.dm
@@ -0,0 +1,99 @@
+// A subtype that involves spawning mobs like carp, rogue drones, spiders, etc.
+
+/datum/event2/event/mob_spawning
+ var/list/spawned_mobs = list()
+ var/use_map_edge_with_landmarks = TRUE // Use both landmarks and spawning from the "edge" of the map. Otherise uses landmarks over map edge.
+ var/landmark_name = "carpspawn" // Which landmark to use for spawning.
+
+// Spawns a specific mob from the "edge" of the map, and makes them go towards the station.
+// Can also use landmarks, if desired.
+/datum/event2/event/mob_spawning/proc/spawn_mobs_in_space(mob_type, number_of_groups, min_size_of_group, max_size_of_group, dir)
+ if(isnull(dir))
+ dir = pick(GLOB.cardinal)
+
+ var/list/valid_z_levels = get_location_z_levels()
+ valid_z_levels -= using_map.sealed_levels // Space levels only please!
+
+ // Check if any landmarks exist!
+ var/list/spawn_locations = list()
+ for(var/obj/effect/landmark/C in landmarks_list)
+ if(C.name == landmark_name && (C.z in valid_z_levels))
+ spawn_locations.Add(C.loc)
+
+ var/prioritize_landmarks = TRUE
+ if(use_map_edge_with_landmarks && prob(50))
+ prioritize_landmarks = FALSE // One in two chance to come from the edge instead.
+
+ if(spawn_locations.len && prioritize_landmarks) // Okay we've got landmarks, lets use those!
+ shuffle_inplace(spawn_locations)
+ number_of_groups = min(number_of_groups, spawn_locations.len)
+ var/i = 1
+ while (i <= number_of_groups)
+ var/group_size = rand(min_size_of_group, max_size_of_group)
+ for (var/j = 0, j < group_size, j++)
+ spawn_one_mob(spawn_locations[i], mob_type)
+ i++
+ return
+
+ // Okay we did *not* have any landmarks, or we're being told to do both, so lets do our best!
+ var/i = 1
+ while(i <= number_of_groups)
+ var/z_level = pick(valid_z_levels)
+ var/group_size = rand(min_size_of_group, max_size_of_group)
+ var/turf/map_center = locate(round(world.maxx/2), round(world.maxy/2), z_level)
+ var/turf/group_center = pick_random_edge_turf(dir, z_level, TRANSITIONEDGE + 2)
+ var/list/turfs = getcircle(group_center, 2)
+ for(var/j = 0, j < group_size, j++)
+ // On larger maps, BYOND gets in the way of letting simple_mobs path to the closest edge of the station.
+ // So instead we need to simulate the mob's travel, then spawn them somewhere still hopefully off screen.
+
+ // Find a turf to be the edge of the map.
+ var/turf/edge_of_map = turfs[(i % turfs.len) + 1]
+
+ // Now walk a straight line towards the center of the map, until we find a non-space tile.
+ var/turf/edge_of_station = null
+
+ var/list/space_line = list() // This holds all space tiles on the line. Will be used a bit later.
+ for(var/turf/T in getline(edge_of_map, map_center))
+ if(!T.is_space())
+ break // We found the station!
+ space_line += T
+ edge_of_station = T
+
+ // Now put the mob somewhere on the line, hopefully off screen.
+ // I wish this was higher than 8 but the BYOND internal A* algorithm gives up sometimes when using
+ // 16 or more.
+ // In the future, a new AI stance that handles long distance travel using getline() could work.
+ var/max_distance = 8
+ var/turf/spawn_turf = null
+ for(var/P in space_line)
+ var/turf/point = P
+ if(get_dist(point, edge_of_station) <= max_distance)
+ spawn_turf = P
+ break
+
+ if(spawn_turf)
+ // Finally, make the simple_mob go towards the edge of the station.
+ var/mob/living/simple_mob/M = spawn_one_mob(spawn_turf, mob_type)
+ if(edge_of_station)
+ M.ai_holder?.give_destination(edge_of_station) // Ask simple_mobs to fly towards the edge of the station.
+ i++
+
+/datum/event2/event/mob_spawning/proc/spawn_one_mob(new_loc, mob_type)
+ var/mob/living/simple_mob/M = new mob_type(new_loc)
+ GLOB.destroyed_event.register(M, src, .proc/on_mob_destruction)
+ spawned_mobs += M
+ return M
+
+// Counts living simple_mobs spawned by this event.
+/datum/event2/event/mob_spawning/proc/count_spawned_mobs()
+ . = 0
+ for(var/I in spawned_mobs)
+ var/mob/living/simple_mob/M = I
+ if(!QDELETED(M) && M.stat != DEAD)
+ . += 1
+
+// If simple_mob is bomphed, remove it from the list.
+/datum/event2/event/mob_spawning/proc/on_mob_destruction(mob/M)
+ spawned_mobs -= M
+ GLOB.destroyed_event.unregister(M, src, .proc/on_mob_destruction)
\ No newline at end of file
diff --git a/code/modules/gamemaster/event2/events/security/carp_migration.dm b/code/modules/gamemaster/event2/events/security/carp_migration.dm
new file mode 100644
index 0000000000..12f08f3edd
--- /dev/null
+++ b/code/modules/gamemaster/event2/events/security/carp_migration.dm
@@ -0,0 +1,49 @@
+/datum/event2/meta/carp_migration
+ name = "carp migration"
+ event_class = "carp"
+ departments = list(DEPARTMENT_SECURITY, DEPARTMENT_EVERYONE)
+ chaos = 30
+ chaotic_threshold = EVENT_CHAOS_THRESHOLD_MEDIUM_IMPACT
+ event_type = /datum/event2/event/mob_spawning/carp_migration
+
+/datum/event2/meta/carp_migration/get_weight()
+ return 10 + (metric.count_people_in_department(DEPARTMENT_SECURITY) * 20) + (metric.count_all_space_mobs() * 40)
+
+
+/datum/event2/event/mob_spawning/carp_migration
+ announce_delay_lower_bound = 1 MINUTE
+ announce_delay_upper_bound = 2 MINUTES
+ length_lower_bound = 30 SECONDS
+ length_upper_bound = 1 MINUTE
+ var/carp_cap = 30 // No more than this many (living) carp can exist from this event.
+ var/carp_smallest_group = 3
+ var/carp_largest_group = 5
+ var/carp_wave_cooldown = 10 SECONDS
+
+ var/last_carp_wave_time = null // Last world.time we spawned a carp wave.
+
+/datum/event2/event/mob_spawning/carp_migration/announce()
+ var/announcement = "Unknown biological entities been detected near \the [location_name()], please stand-by."
+ command_announcement.Announce(announcement, "Lifesign Alert")
+
+/datum/event2/event/mob_spawning/carp_migration/event_tick()
+ if(last_carp_wave_time + carp_wave_cooldown > world.time)
+ return
+ last_carp_wave_time = world.time
+
+ if(count_spawned_mobs() < carp_cap)
+ spawn_mobs_in_space(
+ mob_type = /mob/living/simple_mob/animal/space/carp/event,
+ number_of_groups = rand(1, 4),
+ min_size_of_group = carp_smallest_group,
+ max_size_of_group = carp_largest_group
+ )
+
+/datum/event2/event/mob_spawning/carp_migration/end()
+ // Clean up carp that died in space for some reason.
+ for(var/mob/living/simple_mob/SM in spawned_mobs)
+ if(SM.stat == DEAD)
+ var/turf/T = get_turf(SM)
+ if(istype(T, /turf/space))
+ if(prob(75))
+ qdel(SM)
diff --git a/code/modules/gamemaster/event2/events/security/prison_break.dm b/code/modules/gamemaster/event2/events/security/prison_break.dm
new file mode 100644
index 0000000000..7c5127c26c
--- /dev/null
+++ b/code/modules/gamemaster/event2/events/security/prison_break.dm
@@ -0,0 +1,227 @@
+
+// Type for inheritence.
+// It has a null name, so it won't be ran.
+/datum/event2/meta/prison_break
+ chaos = 10
+ chaotic_threshold = EVENT_CHAOS_THRESHOLD_LOW_IMPACT
+ // The weight system can check if people are in these areas.
+ // This isn't the same list as what the event itself will break, as the event will also
+ // break open areas inbetween the holding area and the public hallway, like the brig area verses
+ // the prison area.
+ var/list/relevant_areas = list()
+ var/list/irrelevant_areas = list()
+
+/datum/event2/meta/prison_break/get_weight()
+ // First, don't do this if nobody can fix the doors.
+ var/door_fixers = metric.count_people_in_department(DEPARTMENT_ENGINEERING) + metric.count_people_in_department(DEPARTMENT_SYNTHETIC)
+ if(!door_fixers)
+ return 0
+ var/list/afflicted_departments = departments.Copy()
+ var/afflicted_crew = 0
+
+ afflicted_departments -= DEPARTMENT_SYNTHETIC
+ for(var/D in afflicted_departments)
+ afflicted_crew += metric.count_people_in_department(D)
+
+ // Don't do it if nobody is around to ""appreciate"" it.
+ if(!afflicted_crew)
+ return 0
+
+ var/trapped = get_odds_from_trapped_mobs()
+
+ return 10 + (door_fixers * 20) + (afflicted_crew * 10) + trapped
+
+// This is overriden to have specific events trigger more often based on who is trapped in where, if applicable.
+/datum/event2/meta/prison_break/proc/get_odds_from_trapped_mobs()
+ return 0
+
+/datum/event2/meta/prison_break/proc/is_mob_in_relevant_area(mob/living/L)
+ var/area/A = get_area(L)
+ if(!A)
+ return FALSE
+ if(is_type_in_list(A, relevant_areas) && !is_type_in_list(A, irrelevant_areas))
+ return TRUE
+ return FALSE
+
+/datum/event2/meta/prison_break/brig
+ name = "prison break - brig"
+ departments = list(DEPARTMENT_SECURITY, DEPARTMENT_SYNTHETIC)
+ event_type = /datum/event2/event/prison_break/brig
+ relevant_areas = list(
+ /area/security/prison,
+ /area/security/security_cell_hallway,
+ /area/security/security_processing,
+ /area/security/interrogation
+ )
+
+/datum/event2/meta/prison_break/brig/get_odds_from_trapped_mobs()
+ . = 0
+ for(var/mob/living/L in player_list)
+ if(is_mob_in_relevant_area(L))
+ // Don't count them if they're in security.
+ if(!(L in metric.count_people_in_department(DEPARTMENT_SECURITY)))
+ . += 40
+
+
+/datum/event2/meta/prison_break/armory
+ name = "prison break - armory"
+ departments = list(DEPARTMENT_SECURITY, DEPARTMENT_SYNTHETIC)
+ chaos = 40 // Potentially free guns.
+ chaotic_threshold = EVENT_CHAOS_THRESHOLD_MEDIUM_IMPACT
+ event_type = /datum/event2/event/prison_break/armory
+
+/datum/event2/meta/prison_break/bridge
+ name = "prison break - bridge"
+ departments = list(DEPARTMENT_COMMAND, DEPARTMENT_SYNTHETIC)
+ chaos = 40 // Potentially free spare ID.
+ chaotic_threshold = EVENT_CHAOS_THRESHOLD_MEDIUM_IMPACT
+ event_type = /datum/event2/event/prison_break/bridge
+
+/datum/event2/meta/prison_break/xenobio
+ name = "prison break - xenobio"
+ departments = list(DEPARTMENT_RESEARCH, DEPARTMENT_SYNTHETIC)
+ chaos = 20 // This one is more likely to actually kill someone.
+ chaotic_threshold = EVENT_CHAOS_THRESHOLD_MEDIUM_IMPACT
+ event_type = /datum/event2/event/prison_break/xenobio
+ relevant_areas = list(/area/rnd/xenobiology)
+ irrelevant_areas = list(
+ /area/rnd/xenobiology/xenoflora,
+ /area/rnd/xenobiology/xenoflora_storage
+ )
+
+/datum/event2/meta/prison_break/xenobio/get_odds_from_trapped_mobs()
+ . = 0
+ for(var/mob/living/simple_mob/slime/xenobio/X in living_mob_list)
+ if(is_mob_in_relevant_area(X))
+ . += 5
+
+
+/datum/event2/meta/prison_break/virology
+ name = "prison break - virology"
+ departments = list(DEPARTMENT_MEDICAL, DEPARTMENT_SYNTHETIC)
+ event_type = /datum/event2/event/prison_break/virology
+ relevant_areas = list(
+ /area/medical/virology,
+ /area/medical/virologyaccess
+ )
+
+/datum/event2/meta/prison_break/virology/get_odds_from_trapped_mobs()
+ . = 0
+ for(var/mob/living/L in player_list)
+ if(is_mob_in_relevant_area(L))
+ // Don't count them if they're in medical.
+ if(!(L in metric.count_people_in_department(DEPARTMENT_MEDICAL)))
+ . += 40
+
+
+
+
+/datum/event2/event/prison_break
+ start_delay_lower_bound = 3 MINUTES
+ start_delay_upper_bound = 4 MINUTES
+ length_lower_bound = 40 SECONDS
+ length_upper_bound = 1 MINUTE
+ var/area_display_name = null // A string used to describe the area being messed with.
+ var/containment_display_desc = null
+ var/list/areas_to_break = list()
+ var/list/area_types_to_break = null // Area types to include.
+ var/list/area_types_to_ignore = null // Area types to exclude, usually due to undesired inclusion from inheritence.
+
+/datum/event2/event/prison_break/brig
+ area_display_name = "Brig"
+ containment_display_desc = "imprisonment"
+ area_types_to_break = list(
+ /area/security/prison,
+ /area/security/brig,
+ /area/security/security_cell_hallway,
+ /area/security/security_processing,
+ /area/security/interrogation
+ )
+
+/datum/event2/event/prison_break/armory
+ area_display_name = "Armory"
+ containment_display_desc = "protection"
+ area_types_to_break = list(
+ /area/security/brig,
+ /area/security/warden,
+ /area/security/evidence_storage,
+ /area/security/security_equiptment_storage,
+ /area/security/armoury,
+ /area/security/tactical
+ )
+
+/datum/event2/event/prison_break/bridge
+ area_display_name = "Bridge"
+ containment_display_desc = "isolation"
+ area_types_to_break = list(
+ /area/bridge,
+ /area/bridge_hallway
+ )
+
+/datum/event2/event/prison_break/xenobio
+ area_display_name = "Xenobiology"
+ containment_display_desc = "containment"
+ area_types_to_break = list(/area/rnd/xenobiology)
+ area_types_to_ignore = list(
+ /area/rnd/xenobiology/xenoflora,
+ /area/rnd/xenobiology/xenoflora_storage
+ )
+
+/datum/event2/event/prison_break/virology
+ area_display_name = "Virology"
+ containment_display_desc = "quarantine"
+ area_types_to_break = list(
+ /area/medical/virology,
+ /area/medical/virologyaccess
+ )
+
+
+/datum/event2/event/prison_break/set_up()
+ for(var/area/A in all_areas)
+ if(is_type_in_list(A, area_types_to_break) && !is_type_in_list(A, area_types_to_ignore))
+ areas_to_break += A
+
+ if(!areas_to_break.len)
+ log_debug("Prison Break event failed to find any areas to break. Aborting.")
+ abort()
+ return
+
+/datum/event2/event/prison_break/announce()
+ var/my_department = "[location_name()] Firewall Subroutines"
+ var/message = "An unknown malicious program has been detected in the [area_display_name] \
+ lighting and airlock control systems at [stationtime2text()]. Systems will be fully compromised \
+ within approximately three minutes. Direct intervention is required immediately. Disabling the \
+ main breaker in the APCs will protect the APC's room from being compromised."
+
+ for(var/obj/machinery/message_server/MS in machines)
+ MS.send_rc_message(DEPARTMENT_ENGINEERING, my_department, "[message]
", "", "", 2)
+
+ // Nobody reads the requests consoles so lets use the radio as well.
+ global_announcer.autosay(message, my_department, DEPARTMENT_ENGINEERING)
+
+ for(var/mob/living/silicon/ai/A in player_list)
+ to_chat(A, span("danger", "Malicious program detected in the [area_display_name] lighting and airlock control systems by [my_department]. \
+ Disabling the main breaker in the APCs will protect the APC's room from being compromised."))
+
+ var/time_to_flicker = start_delay - 10 SECONDS
+ addtimer(CALLBACK(src, .proc/flicker_area), time_to_flicker)
+
+
+/datum/event2/event/prison_break/proc/flicker_area()
+ for(var/area/A in areas_to_break)
+ var/obj/machinery/power/apc/apc = A.get_apc()
+ if(apc.operating) //If the apc's off, it's a little hard to overload the lights.
+ for(var/obj/machinery/light/L in A)
+ L.flicker(10)
+
+/datum/event2/event/prison_break/start()
+ for(var/area/A in areas_to_break)
+ spawn(0) // So we don't block the ticker.
+ A.prison_break()
+
+// There's between 40 seconds and one minute before the whole station knows.
+// If there's a baddie engineer, they can choose to keep their early announcement to themselves and get a minute to exploit it.
+/datum/event2/event/prison_break/end()
+ command_announcement.Announce("[pick("Gr3y.T1d3 virus","Malignant trojan")] was detected \
+ in \the [location_name()] [area_display_name] [containment_display_desc] subroutines. Secure any compromised \
+ areas immediately. AI involvement is recommended.", "[capitalize(containment_display_desc)] Alert")
diff --git a/code/modules/gamemaster/event2/events/security/rogue_drones.dm b/code/modules/gamemaster/event2/events/security/rogue_drones.dm
new file mode 100644
index 0000000000..da40b3262f
--- /dev/null
+++ b/code/modules/gamemaster/event2/events/security/rogue_drones.dm
@@ -0,0 +1,70 @@
+/datum/event2/meta/rogue_drones
+ name = "rogue drones"
+ departments = list(DEPARTMENT_SECURITY, DEPARTMENT_EVERYONE)
+ chaos = 40
+ chaotic_threshold = EVENT_CHAOS_THRESHOLD_MEDIUM_IMPACT
+ event_type = /datum/event2/event/mob_spawning/rogue_drones
+
+/datum/event2/meta/rogue_drones/get_weight()
+ . = 10 // Start with a base weight, since this event does provide some value even if no sec is around.
+ . += metric.count_people_in_department(DEPARTMENT_SECURITY) * 20
+ . += metric.count_all_space_mobs() * 40
+
+
+/datum/event2/event/mob_spawning/rogue_drones
+ length_lower_bound = 15 MINUTES
+ length_upper_bound = 20 MINUTES
+ var/drones_to_spawn = 6
+
+/datum/event2/event/mob_spawning/rogue_drones/set_up()
+ if(prob(10)) // Small chance for a false alarm.
+ drones_to_spawn = 0
+
+/datum/event2/event/mob_spawning/rogue_drones/announce()
+ var/msg = null
+ var/rng = rand(1,5)
+ switch(rng)
+ if(1)
+ msg = "A combat drone wing operating in close orbit above Sif has failed to return from a anti-piracy sweep. \
+ If any are sighted, approach with caution."
+ if(2)
+ msg = "Contact has been lost with a combat drone wing in Sif orbit. \
+ If any are sighted in the area, approach with caution."
+ if(3)
+ msg = "Unidentified hackers have targeted a combat drone wing deployed around Sif. \
+ If any are sighted in the area, approach with caution."
+ if(4)
+ msg = "A passing derelict ship's drone defense systems have just activated. \
+ If any are sighted in the area, use caution."
+ if(5)
+ msg = "We're detecting a swarm of small objects approaching your station. \
+ Most likely a bunch of drones. Please exercise caution if you see any."
+
+ command_announcement.Announce(msg, "Rogue drone alert")
+
+/datum/event2/event/mob_spawning/rogue_drones/start()
+ for(var/i = 1 to drones_to_spawn)
+ spawn_mobs_in_space(
+ mob_type = /mob/living/simple_mob/mechanical/combat_drone/event,
+ number_of_groups = 1,
+ min_size_of_group = 1,
+ max_size_of_group = 1
+ )
+
+/datum/event2/event/mob_spawning/rogue_drones/end()
+ if(drones_to_spawn)
+ var/number_recovered = 0
+ for(var/mob/living/simple_mob/mechanical/combat_drone/D in spawned_mobs)
+ var/datum/effect/effect/system/spark_spread/sparks = new /datum/effect/effect/system/spark_spread()
+ sparks.set_up(3, 0, D.loc)
+ sparks.start()
+ D.z = using_map.admin_levels[1]
+ D.loot_list = list()
+
+ qdel(D)
+ number_recovered++
+
+ if(number_recovered > spawned_mobs.len * 0.75)
+ command_announcement.Announce("The drones that were malfunctioning have been recovered safely.", "Rogue drone alert")
+ else
+ command_announcement.Announce("We're disappointed at the loss of the drones, but the survivors have been recovered.", "Rogue drone alert")
diff --git a/code/modules/gamemaster/event2/events/security/spider_infestation.dm b/code/modules/gamemaster/event2/events/security/spider_infestation.dm
new file mode 100644
index 0000000000..23dc61c673
--- /dev/null
+++ b/code/modules/gamemaster/event2/events/security/spider_infestation.dm
@@ -0,0 +1,49 @@
+/datum/event2/meta/spider_infestation
+ name = "spider infestation"
+ event_class = "spiders"
+ departments = list(DEPARTMENT_SECURITY, DEPARTMENT_MEDICAL, DEPARTMENT_EVERYONE)
+ chaos = 30
+ chaotic_threshold = EVENT_CHAOS_THRESHOLD_MEDIUM_IMPACT
+ event_type = /datum/event2/event/spider_infestation
+
+/datum/event2/meta/spider_infestation/weak
+ name = "weak spider infestation"
+ chaos = 20
+ event_type = /datum/event2/event/spider_infestation/weak
+
+
+/datum/event2/meta/spider_infestation/get_weight()
+ . = 10
+ . += metric.count_people_in_department(DEPARTMENT_SECURITY) * 20
+ . += metric.count_people_in_department(DEPARTMENT_MEDICAL) * 10
+
+
+// This isn't a /mob_spawning subtype since spiderlings aren't actually mobs.
+
+/datum/event2/event/spider_infestation
+ var/spiders_to_spawn = 8
+ var/spiderling_to_spawn = /obj/effect/spider/spiderling
+
+/datum/event2/event/spider_infestation/weak
+ spiders_to_spawn = 5
+ spiderling_to_spawn = /obj/effect/spider/spiderling/stunted
+
+
+
+/datum/event2/event/spider_infestation/announce()
+ command_announcement.Announce("Unidentified lifesigns detected coming aboard \the [location_name()]. \
+ Secure any exterior access, including ducting and ventilation.", "Lifesign Alert", new_sound = 'sound/AI/aliens.ogg')
+
+/datum/event2/event/spider_infestation/start()
+ var/list/vents = list()
+ for(var/obj/machinery/atmospherics/unary/vent_pump/temp_vent in machines)
+ if(!temp_vent.welded && temp_vent.network && temp_vent.loc.z in get_location_z_levels())
+ if(temp_vent.network.normal_members.len > 50)
+ vents += temp_vent
+
+ while((spiders_to_spawn >= 1) && vents.len)
+ var/obj/vent = pick(vents)
+ new spiderling_to_spawn(vent.loc)
+ log_debug("Spider infestation event spawned a spiderling at [get_area(vent)].")
+ vents -= vent
+ spiders_to_spawn--
diff --git a/code/modules/gamemaster/event2/events/security/stowaway.dm b/code/modules/gamemaster/event2/events/security/stowaway.dm
new file mode 100644
index 0000000000..d04d350234
--- /dev/null
+++ b/code/modules/gamemaster/event2/events/security/stowaway.dm
@@ -0,0 +1,64 @@
+// Base type used for inheritence.
+/datum/event2/meta/stowaway
+ event_class = "stowaway"
+ departments = list(DEPARTMENT_SECURITY, DEPARTMENT_EVERYONE)
+ chaos = 10
+ chaotic_threshold = EVENT_CHAOS_THRESHOLD_LOW_IMPACT
+ event_type = /datum/event2/event/ghost_pod_spawner/stowaway
+ var/safe_for_extended = FALSE
+
+/datum/event2/meta/stowaway/normal
+ name = "stowaway - normal"
+ safe_for_extended = TRUE
+
+/datum/event2/meta/stowaway/renegade
+ name = "stowaway - renegade"
+ chaos = 30
+ event_type = /datum/event2/event/ghost_pod_spawner/stowaway/renegade
+
+/datum/event2/meta/stowaway/infiltrator
+ name = "stowaway - infiltrator"
+ chaos = 60
+ chaotic_threshold = EVENT_CHAOS_THRESHOLD_MEDIUM_IMPACT
+ event_type = /datum/event2/event/ghost_pod_spawner/stowaway/infiltrator
+
+/datum/event2/meta/stowaway/get_weight()
+ if(istype(ticker.mode, /datum/game_mode/extended) && !safe_for_extended)
+ return 0
+
+ var/security = metric.count_people_in_department(DEPARTMENT_SECURITY)
+ var/everyone = metric.count_people_in_department(DEPARTMENT_EVERYONE) - security
+ var/ghost_activity = metric.assess_all_dead_mobs() / 100
+
+ return ( (security * 20) + (everyone * 2) ) * ghost_activity
+
+
+/datum/event2/event/ghost_pod_spawner/stowaway
+ pod_type = /obj/structure/ghost_pod/ghost_activated/human
+ desired_turf_areas = list(/area/maintenance)
+ announce_delay_lower_bound = 15 MINUTES
+ announce_delay_upper_bound = 30 MINUTES
+ var/antag_type = MODE_STOWAWAY
+ var/announce_odds = 20
+
+/datum/event2/event/ghost_pod_spawner/stowaway/renegade
+ antag_type = MODE_RENEGADE
+ announce_odds = 33
+
+/datum/event2/event/ghost_pod_spawner/stowaway/infiltrator
+ antag_type = MODE_INFILTRATOR
+ announce_odds = 50
+
+/datum/event2/event/ghost_pod_spawner/stowaway/post_pod_creation(obj/structure/ghost_pod/ghost_activated/human/pod)
+ pod.make_antag = antag_type
+ pod.occupant_type = "[pod.make_antag] [pod.occupant_type]"
+
+ say_dead_object("[span("notice", pod.occupant_type)] pod is now available in \the [get_area(pod)].", pod)
+
+/datum/event2/event/ghost_pod_spawner/stowaway/announce()
+ if(prob(announce_odds))
+ if(atc?.squelched)
+ return
+ atc.msg("Attention civilian vessels in [using_map.starsys_name] shipping lanes, caution is advised as \
+ [pick("an unidentified vessel", "a known criminal's vessel", "a derelict vessel")] \
+ has been detected passing multiple local stations.")
diff --git a/code/modules/gamemaster/event2/events/security/surprise_carp.dm b/code/modules/gamemaster/event2/events/security/surprise_carp.dm
new file mode 100644
index 0000000000..39be6d8412
--- /dev/null
+++ b/code/modules/gamemaster/event2/events/security/surprise_carp.dm
@@ -0,0 +1,71 @@
+// This event sends a few carp after someone hanging around in space, unannounced.
+
+/datum/event2/meta/surprise_carp
+ name = "surprise carp"
+ departments = list(DEPARTMENT_EVERYONE)
+ chaos = 20
+ chaotic_threshold = EVENT_CHAOS_THRESHOLD_MEDIUM_IMPACT
+ event_type = /datum/event2/event/surprise_carp
+
+/datum/event2/meta/surprise_carp/get_weight()
+ return metric.count_all_space_mobs() * 50
+
+
+/datum/event2/event/surprise_carp
+ var/mob/living/victim = null
+
+/datum/event2/event/surprise_carp/set_up()
+ var/list/potential_victims = list()
+ for(var/mob/living/L in player_list)
+ if(!(L.z in get_location_z_levels()))
+ continue // Not on the right z-level.
+ if(L.stat)
+ continue // Don't want dead people.
+ if(istype(get_turf(L), /turf/space) && istype(get_area(L),/area/space))
+ potential_victims += L
+
+ if(potential_victims.len)
+ victim = pick(potential_victims)
+
+/datum/event2/event/surprise_carp/start()
+ if(!victim)
+ log_debug("Failed to find a target for surprise carp attack. Aborting.")
+ abort()
+ return
+
+ var/number_of_carp = rand(1, 2)
+ log_debug("Sending [number_of_carp] carp\s after \the [victim].")
+ // Getting off screen tiles is kind of tricky due to potential edge cases that could arise.
+ // The method we're gonna do is make a big square around the victim, then
+ // subtract a smaller square in the middle for the default vision range.
+ var/list/outer_square = get_safe_square(victim, world.view + 3)
+ var/list/inner_square = get_safe_square(victim, world.view)
+
+ var/list/donut = outer_square - inner_square
+ for(var/T in donut)
+ if(!istype(T, /turf/space))
+ donut -= T
+
+ for(var/i = 1 to number_of_carp)
+ var/turf/spawning_turf = pick(donut)
+
+ if(spawning_turf)
+ var/mob/living/simple_mob/animal/space/carp/C = new(spawning_turf)
+ // Ask carp to swim onto the victim's screen. The AI will then switch to hostile and try to eat them.
+ C.ai_holder?.give_destination(get_turf(victim))
+ else
+ log_debug("Surprise carp attack failed to find any space turfs offscreen to the victim.")
+
+// Gets suitable spots for carp to spawn, without risk of going off the edge of the map.
+// If there is demand for this proc, then it can easily be made independant and moved into one of the helper files.
+/datum/event2/event/surprise_carp/proc/get_safe_square(atom/center, radius)
+ var/lower_left_x = max(center.x - radius, 1 + TRANSITIONEDGE)
+ var/lower_left_y = max(center.y - radius, 1 + TRANSITIONEDGE)
+
+ var/upper_right_x = min(center.x + radius, world.maxx - TRANSITIONEDGE)
+ var/upper_right_y = min(center.y + radius, world.maxy - TRANSITIONEDGE)
+
+ var/turf/lower_left = locate(lower_left_x, lower_left_y, victim.z)
+ var/turf/upper_right = locate(upper_right_x, upper_right_y, victim.z)
+
+ return block(lower_left, upper_right)
diff --git a/code/modules/gamemaster/event2/events/security/swarm_boarder.dm b/code/modules/gamemaster/event2/events/security/swarm_boarder.dm
new file mode 100644
index 0000000000..283333c679
--- /dev/null
+++ b/code/modules/gamemaster/event2/events/security/swarm_boarder.dm
@@ -0,0 +1,55 @@
+// This is just porting the event to the new new event system, it's not been balanced in any way
+// so don't @ me if these things are grossly OP.
+/datum/event2/meta/swarm_boarder
+ event_class = "swarm boarder"
+ departments = list(DEPARTMENT_EVERYONE, DEPARTMENT_SECURITY, DEPARTMENT_ENGINEERING)
+ chaos = 60
+ chaotic_threshold = EVENT_CHAOS_THRESHOLD_HIGH_IMPACT
+ var/safe_for_extended = FALSE
+
+/datum/event2/meta/swarm_boarder/get_weight()
+ if(istype(ticker.mode, /datum/game_mode/extended) && !safe_for_extended)
+ return 0
+
+ var/security = metric.count_people_in_department(DEPARTMENT_SECURITY)
+ var/engineering = metric.count_people_in_department(DEPARTMENT_ENGINEERING)
+ var/everyone = metric.count_people_in_department(DEPARTMENT_EVERYONE) - (engineering + security)
+
+ var/ghost_activity = metric.assess_all_dead_mobs() / 100
+
+ return ( (security * 20) + (engineering * 10) + (everyone * 2) ) * ghost_activity
+
+/datum/event2/meta/swarm_boarder/normal
+ name = "swarmer shell - normal"
+ event_type = /datum/event2/event/ghost_pod_spawner/swarm_boarder
+
+/datum/event2/meta/swarm_boarder/melee
+ name = "swarmer shell - melee"
+ event_type = /datum/event2/event/ghost_pod_spawner/swarm_boarder/melee
+
+/datum/event2/meta/swarm_boarder/gunner
+ name = "swarmer shell - gunner"
+ event_type = /datum/event2/event/ghost_pod_spawner/swarm_boarder/gunner
+
+
+
+
+/datum/event2/event/ghost_pod_spawner/swarm_boarder
+ announce_delay_lower_bound = 5 MINUTES
+ announce_delay_upper_bound = 15 MINUTES
+ pod_type = /obj/structure/ghost_pod/ghost_activated/swarm_drone/event
+ desired_turf_areas = list(/area/maintenance)
+ var/announce_odds = 80
+
+/datum/event2/event/ghost_pod_spawner/swarm_boarder/melee
+ pod_type = /obj/structure/ghost_pod/ghost_activated/swarm_drone/event/melee
+
+/datum/event2/event/ghost_pod_spawner/swarm_boarder/gunner
+ pod_type = /obj/structure/ghost_pod/ghost_activated/swarm_drone/event/gunner
+
+/datum/event2/event/ghost_pod_spawner/swarm_boarder/announce()
+ if(prob(announce_odds))
+ if(atc?.squelched)
+ atc.msg("Attention civilian vessels in [using_map.starsys_name] shipping lanes, caution \
+ is advised as [pick("an unidentified vessel", "a known criminal's vessel", "a derelict vessel")] \
+ has been detected passing multiple local stations.")
diff --git a/code/modules/gamemaster/event2/events/synthetic/ion_storm.dm b/code/modules/gamemaster/event2/events/synthetic/ion_storm.dm
index e84108d608..afcf20ebc8 100644
--- a/code/modules/gamemaster/event2/events/synthetic/ion_storm.dm
+++ b/code/modules/gamemaster/event2/events/synthetic/ion_storm.dm
@@ -15,6 +15,7 @@
announce_delay_lower_bound = 7 MINUTES
announce_delay_upper_bound = 15 MINUTES
var/bot_emag_chance = 30 // This is rolled once, instead of once a second for a minute like the old version.
+ var/announce_odds = 50 // Probability of an announcement actually happening after the delay.
/datum/event2/event/ion_storm/start()
// Ion laws.
@@ -55,6 +56,16 @@
"director", "Hello", "Hi!"," ","nuke","crate","taj","xeno")
/datum/event2/event/ion_storm/announce()
- if(prob(50))
- command_announcement.Announce("An ion storm was detected within proximity to \the [location_name()]. \
+ if(prob(announce_odds))
+ command_announcement.Announce("An ion storm was detected within proximity to \the [location_name()] recently. \
Check all AI controlled equipment for corruption.", "Anomaly Alert", new_sound = 'sound/AI/ionstorm.ogg')
+
+// Fake variant used by traitors.
+/datum/event2/event/ion_storm/fake
+ // Fake ion storms announce instantly, so the traitor can time it to make the AI look suspicious.
+ announce_delay_lower_bound = 0
+ announce_delay_upper_bound = 0
+ announce_odds = 100
+
+/datum/event2/event/ion_storm/fake/start()
+ return
\ No newline at end of file
diff --git a/code/modules/hydroponics/spreading/spreading.dm b/code/modules/hydroponics/spreading/spreading.dm
index 53e4e87627..e3ac11ed76 100644
--- a/code/modules/hydroponics/spreading/spreading.dm
+++ b/code/modules/hydroponics/spreading/spreading.dm
@@ -7,7 +7,7 @@
for(var/areapath in typesof(/area/hallway))
var/area/A = locate(areapath)
for(var/turf/simulated/floor/F in A.contents)
- if(turf_clear(F))
+ if(!F.check_density())
turfs += F
if(turfs.len) //Pick a turf to spawn at if we can
diff --git a/code/modules/metric/count.dm b/code/modules/metric/count.dm
index 55565016ad..ba3678295f 100644
--- a/code/modules/metric/count.dm
+++ b/code/modules/metric/count.dm
@@ -49,8 +49,19 @@
return num
// Like above, but for all FBPs.
-/datum/metric/proc/count_all_FBPs(desired_FBP_class, cutoff = 75, respect_z = TRUE)
+/datum/metric/proc/count_all_FBPs(cutoff = 75, respect_z = TRUE)
var/num = count_all_FBPs_of_kind(FBP_CYBORG, cutoff, respect_z)
num += count_all_FBPs_of_kind(FBP_POSI, cutoff, respect_z)
num += count_all_FBPs_of_kind(FBP_DRONE, cutoff, respect_z)
- return num
\ No newline at end of file
+ return num
+
+
+/datum/metric/proc/get_all_antags(cutoff = 75)
+ . = list()
+ for(var/mob/living/L in player_list)
+ if(L.mind && player_is_antag(L.mind) && assess_player_activity(L) >= cutoff)
+ . += L
+
+/datum/metric/proc/count_all_antags(cutoff = 75)
+ var/list/L = get_all_antags(cutoff)
+ return L.len
\ No newline at end of file
diff --git a/code/modules/metric/department.dm b/code/modules/metric/department.dm
index e97e0cb17c..89a0f78ed9 100644
--- a/code/modules/metric/department.dm
+++ b/code/modules/metric/department.dm
@@ -70,7 +70,7 @@
if(!department)
return
for(var/mob/M in player_list)
- if(guess_department(M) != department) // Ignore people outside the department we're counting.
+ if(department != DEPARTMENT_EVERYONE && guess_department(M) != department) // Ignore people outside the department we're counting.
continue
if(assess_player_activity(M) < cutoff)
continue
@@ -94,7 +94,23 @@
continue
. += M
-
/datum/metric/proc/count_people_with_job(job_type, cutoff = 75)
var/list/L = get_people_with_job(job_type, cutoff)
+ return L.len
+
+
+
+/datum/metric/proc/get_people_with_alt_title(job_type, alt_title_type, cutoff = 75)
+ . = list()
+
+ var/list/people_with_jobs = get_people_with_job(job_type, cutoff)
+ var/datum/job/J = SSjob.get_job_type(job_type)
+ var/datum/alt_title/A = new alt_title_type()
+
+ for(var/M in people_with_jobs)
+ if(J.has_alt_title(M, null, A.title))
+ . += M
+
+/datum/metric/proc/count_people_with_alt_title(job_type, alt_title_type, cutoff = 75)
+ var/list/L = get_people_with_alt_title(job_type, alt_title_type, cutoff)
return L.len
\ No newline at end of file
diff --git a/code/modules/mob/living/silicon/robot/robot_remote_control.dm b/code/modules/mob/living/silicon/robot/robot_remote_control.dm
index 90d74b2638..2f4ca0e56b 100644
--- a/code/modules/mob/living/silicon/robot/robot_remote_control.dm
+++ b/code/modules/mob/living/silicon/robot/robot_remote_control.dm
@@ -26,6 +26,8 @@ GLOBAL_LIST_EMPTY(available_ai_shells)
shell = TRUE
braintype = "AI Shell"
SetName("[modtype] AI Shell [num2text(ident)]")
+ rbPDA = new /obj/item/device/pda/ai/shell(src)
+ setup_PDA()
GLOB.available_ai_shells |= src
if(!QDELETED(camera))
camera.c_tag = real_name //update the camera name too
diff --git a/code/modules/power/lighting.dm b/code/modules/power/lighting.dm
index ab91a82510..6d01a2e445 100644
--- a/code/modules/power/lighting.dm
+++ b/code/modules/power/lighting.dm
@@ -520,6 +520,8 @@ var/global/list/light_type_cache = list()
if(status != LIGHT_OK) break
on = !on
update(0)
+ if(!on) // Only play when the light turns off.
+ playsound(src, 'sound/effects/light_flicker.ogg', 50, 1)
sleep(rand(5, 15))
on = (status == LIGHT_OK)
update(0)
diff --git a/maps/southern_cross/southern_cross.dm b/maps/southern_cross/southern_cross.dm
index 29c4f295f2..741d64d58e 100644
--- a/maps/southern_cross/southern_cross.dm
+++ b/maps/southern_cross/southern_cross.dm
@@ -6,6 +6,7 @@
#include "southern_cross_defines.dm"
#include "southern_cross_jobs.dm"
#include "southern_cross_elevator.dm"
+ #include "southern_cross_events.dm"
#include "southern_cross_presets.dm"
#include "southern_cross_shuttles.dm"
diff --git a/maps/southern_cross/southern_cross_events.dm b/maps/southern_cross/southern_cross_events.dm
new file mode 100644
index 0000000000..ab7ac253f6
--- /dev/null
+++ b/maps/southern_cross/southern_cross_events.dm
@@ -0,0 +1,7 @@
+// The Station Director's office is specific to the Southern Cross, so this needs to go here.
+/datum/event2/event/prison_break/bridge
+ area_types_to_break = list(
+ /area/bridge,
+ /area/bridge_hallway,
+ /area/crew_quarters/heads/sc/sd
+ )
\ No newline at end of file
diff --git a/maps/submaps/surface_submaps/wilderness/wilderness_areas.dm b/maps/submaps/surface_submaps/wilderness/wilderness_areas.dm
index f7ee7f2542..1607781d91 100644
--- a/maps/submaps/surface_submaps/wilderness/wilderness_areas.dm
+++ b/maps/submaps/surface_submaps/wilderness/wilderness_areas.dm
@@ -3,6 +3,7 @@
icon_state = "submap"
flags = RAD_SHIELDED
ambience = AMBIENCE_RUINS
+ forbid_events = TRUE
/area/submap/event //To be used for Events not for regular PoIs
name = "Unknown"
diff --git a/polaris.dme b/polaris.dme
index 7bfbbeb7b2..9c11e26383 100644
--- a/polaris.dme
+++ b/polaris.dme
@@ -1730,21 +1730,45 @@
#include "code\modules\gamemaster\defines.dm"
#include "code\modules\gamemaster\event2\event.dm"
#include "code\modules\gamemaster\event2\meta.dm"
+#include "code\modules\gamemaster\event2\events\ghost_pod_spawner.dm"
+#include "code\modules\gamemaster\event2\events\mob_spawning.dm"
#include "code\modules\gamemaster\event2\events\cargo\shipping_error.dm"
+#include "code\modules\gamemaster\event2\events\command\manifest_malfunction.dm"
+#include "code\modules\gamemaster\event2\events\command\money_hacker.dm"
+#include "code\modules\gamemaster\event2\events\command\raise_funds.dm"
+#include "code\modules\gamemaster\event2\events\engineering\airlock_failure.dm"
#include "code\modules\gamemaster\event2\events\engineering\blob.dm"
+#include "code\modules\gamemaster\event2\events\engineering\brand_intelligence.dm"
#include "code\modules\gamemaster\event2\events\engineering\camera_damage.dm"
#include "code\modules\gamemaster\event2\events\engineering\canister_leak.dm"
-#include "code\modules\gamemaster\event2\events\engineering\carp_migration.dm"
#include "code\modules\gamemaster\event2\events\engineering\dust.dm"
#include "code\modules\gamemaster\event2\events\engineering\gas_leak.dm"
#include "code\modules\gamemaster\event2\events\engineering\grid_check.dm"
#include "code\modules\gamemaster\event2\events\engineering\meteor_defense.dm"
#include "code\modules\gamemaster\event2\events\engineering\spacevine.dm"
+#include "code\modules\gamemaster\event2\events\engineering\wallrot.dm"
+#include "code\modules\gamemaster\event2\events\engineering\window_break.dm"
#include "code\modules\gamemaster\event2\events\everyone\comms_blackout.dm"
+#include "code\modules\gamemaster\event2\events\everyone\electrical_fault.dm"
+#include "code\modules\gamemaster\event2\events\everyone\gravity.dm"
+#include "code\modules\gamemaster\event2\events\everyone\infestation.dm"
+#include "code\modules\gamemaster\event2\events\everyone\pda_spam.dm"
+#include "code\modules\gamemaster\event2\events\everyone\radiation_storm.dm"
+#include "code\modules\gamemaster\event2\events\everyone\random_antag.dm"
#include "code\modules\gamemaster\event2\events\everyone\solar_storm.dm"
+#include "code\modules\gamemaster\event2\events\everyone\sudden_weather_shift.dm"
#include "code\modules\gamemaster\event2\events\legacy\legacy.dm"
+#include "code\modules\gamemaster\event2\events\medical\appendicitis.dm"
+#include "code\modules\gamemaster\event2\events\medical\virus.dm"
+#include "code\modules\gamemaster\event2\events\security\carp_migration.dm"
#include "code\modules\gamemaster\event2\events\security\drill_announcement.dm"
+#include "code\modules\gamemaster\event2\events\security\prison_break.dm"
+#include "code\modules\gamemaster\event2\events\security\rogue_drones.dm"
#include "code\modules\gamemaster\event2\events\security\security_advisement.dm"
+#include "code\modules\gamemaster\event2\events\security\spider_infestation.dm"
+#include "code\modules\gamemaster\event2\events\security\stowaway.dm"
+#include "code\modules\gamemaster\event2\events\security\surprise_carp.dm"
+#include "code\modules\gamemaster\event2\events\security\swarm_boarder.dm"
#include "code\modules\gamemaster\event2\events\synthetic\ion_storm.dm"
#include "code\modules\games\cah.dm"
#include "code\modules\games\cah_black_cards.dm"