diff --git a/code/game/gamemodes/malfunction/Malf_Modules.dm b/code/game/gamemodes/malfunction/Malf_Modules.dm
index 9be9e59cab..1c17893a28 100644
--- a/code/game/gamemodes/malfunction/Malf_Modules.dm
+++ b/code/game/gamemodes/malfunction/Malf_Modules.dm
@@ -734,6 +734,33 @@ GLOBAL_LIST_INIT(blacklisted_malf_machines, typecacheof(list(
owner.playsound_local(owner, "sparks", 50, 0)
+//Disable Emergency Lights
+/datum/AI_Module/small/emergency_lights
+ module_name = "Disable Emergency Lights"
+ mod_pick_name = "disable_emergency_lights"
+ description = "Cuts emergency lights across the entire station. If power is lost to light fixtures, they will not attempt to fall back on emergency power reserves."
+ cost = 10
+ one_purchase = TRUE
+ power_type = /datum/action/innate/ai/emergency_lights
+ unlock_text = "You hook into the powernet and locate the connections between light fixtures and their fallbacks."
+ unlock_sound = "sparks"
+
+/datum/action/innate/ai/emergency_lights
+ name = "Disable Emergency Lights"
+ desc = "Disables all emergency lighting. Note that emergency lights can be restored through reboot at an APC."
+ button_icon_state = "emergency_lights"
+ uses = 1
+
+/datum/action/innate/ai/emergency_lights/Activate()
+ for(var/obj/machinery/light/L in GLOB.machines)
+ if(L.z in GLOB.station_z_levels)
+ L.no_emergency = TRUE
+ INVOKE_ASYNC(L, /obj/machinery/light/.proc/update, FALSE)
+ CHECK_TICK
+ to_chat(owner, "Emergency light connections severed.")
+ owner.playsound_local(owner, 'sound/effects/light_flicker.ogg', 50, FALSE)
+
+
//Reactivate Camera Network: Reactivates up to 30 cameras across the station.
/datum/AI_Module/small/reactivate_cameras
module_name = "Reactivate Camera Network"
diff --git a/code/modules/power/apc.dm b/code/modules/power/apc.dm
index 815da3fbdb..7f3b428978 100644
--- a/code/modules/power/apc.dm
+++ b/code/modules/power/apc.dm
@@ -89,6 +89,7 @@
var/auto_name = 0
var/failure_timer = 0
var/force_update = 0
+ var/emergency_lights = FALSE
var/update_state = -1
var/update_overlay = -1
var/icon_update_needed = FALSE
@@ -760,6 +761,7 @@
"coverLocked" = coverlocked,
"siliconUser" = user.has_unlimited_silicon_privilege || user.using_power_flow_console(),
"malfStatus" = get_malf_status(user),
+ "emergencyLights" = !emergency_lights,
"powerChannels" = list(
list(
@@ -899,6 +901,14 @@
failure_timer = 0
update_icon()
update()
+ if("emergency_lighting")
+ emergency_lights = !emergency_lights
+ for(var/area/A in area.related)
+ for(var/obj/machinery/light/L in A)
+ if(!initial(L.no_emergency)) //If there was an override set on creation, keep that override
+ L.no_emergency = emergency_lights
+ INVOKE_ASYNC(L, /obj/machinery/light/.proc/update, FALSE)
+ CHECK_TICK
return 1
/obj/machinery/power/apc/proc/toggle_breaker()
diff --git a/code/modules/power/cell.dm b/code/modules/power/cell.dm
index c8e58791e2..acbebea458 100644
--- a/code/modules/power/cell.dm
+++ b/code/modules/power/cell.dm
@@ -343,3 +343,17 @@
/obj/item/stock_parts/cell/beam_rifle/emp_act(severity)
charge = Clamp((charge-(10000/severity)),0,maxcharge)
+
+/obj/item/stock_parts/cell/emergency_light
+ name = "miniature power cell"
+ desc = "A tiny power cell with a very low power capacity. Used in light fixtures to power them in the event of an outage."
+ maxcharge = 120 //Emergency lights use 0.2 W per tick, meaning ~10 minutes of emergency power from a cell
+ materials = list(MAT_GLASS = 20)
+ rating = 1
+ w_class = WEIGHT_CLASS_TINY
+
+/obj/item/stock_parts/cell/emergency_light/Initialize()
+ . = ..()
+ var/area/A = get_area(src)
+ if(!A.lightswitch || !A.light_power)
+ charge = 0 //For naturally depowered areas, we start with no power
diff --git a/code/modules/power/lighting.dm b/code/modules/power/lighting.dm
index 98fb2ed233..635a17056d 100644
--- a/code/modules/power/lighting.dm
+++ b/code/modules/power/lighting.dm
@@ -49,12 +49,22 @@
var/fixture_type = "tube"
var/sheets_refunded = 2
var/obj/machinery/light/newlight = null
+ var/obj/item/stock_parts/cell/cell
-/obj/structure/light_construct/New(loc, ndir, building)
- ..()
+ var/cell_connectors = TRUE
+
+/obj/structure/light_construct/Initialize(mapload, ndir, building)
+ . = ..()
if(building)
setDir(ndir)
+/obj/structure/light_construct/Destroy()
+ QDEL_NULL(cell)
+ return ..()
+
+/obj/structure/light_construct/get_cell()
+ return cell
+
/obj/structure/light_construct/examine(mob/user)
..()
switch(src.stage)
@@ -64,9 +74,38 @@
to_chat(user, "It's wired.")
if(3)
to_chat(user, "The casing is closed.")
+ if(cell_connectors)
+ if(cell)
+ to_chat(user, "You see [cell] inside the casing.")
+ else
+ to_chat(user, "The casing has no power cell for backup power.")
+ else
+ to_chat(user, "This casing doesn't support power cells for backup power.")
+ return
/obj/structure/light_construct/attackby(obj/item/W, mob/user, params)
add_fingerprint(user)
+ if(istype(W, /obj/item/stock_parts/cell))
+ if(!cell_connectors)
+ to_chat(user, "This [name] can't support a power cell!")
+ return
+ if(W.flags_1 & NODROP_1)
+ to_chat(user, "[W] is stuck to your hand!")
+ return
+ user.dropItemToGround(W)
+ if(cell)
+ user.visible_message("[user] swaps [W] out for [src]'s cell.", \
+ "You swap [src]'s power cells.")
+ cell.forceMove(drop_location())
+ user.put_in_hands(cell)
+ else
+ user.visible_message("[user] hooks up [W] to [src].", \
+ "You add [W] to [src].")
+ playsound(src, 'sound/machines/click.ogg', 50, TRUE)
+ W.forceMove(src)
+ cell = W
+ add_fingerprint(user)
+ return
switch(stage)
if(1)
if(istype(W, /obj/item/wrench))
@@ -124,6 +163,10 @@
newlight = new /obj/machinery/light/small/built(loc)
newlight.setDir(dir)
transfer_fingerprints_to(newlight)
+ if(cell)
+ newlight.cell = cell
+ cell.forceMove(newlight)
+ cell = null
qdel(src)
return
return ..()
@@ -173,6 +216,11 @@
var/rigged = 0 // true if rigged to explode
+ var/obj/item/stock_parts/cell/cell
+ var/start_with_cell = TRUE // if true, this fixture generates a very weak cell at roundstart
+ var/emergency_mode = FALSE // if true, the light is in emergency mode
+ var/no_emergency = FALSE // if true, this light cannot ever have an emergency mode
+
// the smaller bulb light fixture
/obj/machinery/light/small
@@ -192,8 +240,14 @@
/obj/machinery/light/built
icon_state = "tube-empty"
+ start_with_cell = FALSE
+<<<<<<< HEAD
/obj/machinery/light/built/New()
+=======
+/obj/machinery/light/built/Initialize()
+ . = ..()
+>>>>>>> fe816da... Adds emergency lights, built into light fixtures (#33213)
status = LIGHT_EMPTY
update(0)
..()
@@ -208,8 +262,15 @@
// create a new lighting fixture
+<<<<<<< HEAD
/obj/machinery/light/New()
..()
+=======
+/obj/machinery/light/Initialize()
+ . = ..()
+ if(start_with_cell && !no_emergency)
+ cell = new/obj/item/stock_parts/cell/emergency_light(src)
+>>>>>>> fe816da... Adds emergency lights, built into light fixtures (#33213)
spawn(2)
switch(fitting)
if("tube")
@@ -228,13 +289,17 @@
if(A)
on = FALSE
// A.update_lights()
+ QDEL_NULL(cell)
return ..()
/obj/machinery/light/update_icon()
switch(status) // set icon_states
if(LIGHT_OK)
- icon_state = "[base_state][on]"
+ if(emergency_mode)
+ icon_state = "[base_state]_emergency"
+ else
+ icon_state = "[base_state][on]"
if(LIGHT_EMPTY)
icon_state = "[base_state]-empty"
on = FALSE
@@ -249,7 +314,7 @@
// update the icon_state and luminosity of the light depending on its state
/obj/machinery/light/proc/update(trigger = 1)
- update_icon()
+ emergency_mode = FALSE
if(on)
if(!light || light.light_range != brightness)
switchcount++
@@ -261,10 +326,15 @@
burn_out()
else
use_power = ACTIVE_POWER_USE
- set_light(brightness)
+ set_light(brightness, 1, "#FFFFFF")
+ else if(has_emergency_power() && !turned_off())
+ use_power = IDLE_POWER_USE
+ emergency_mode = TRUE
+ process() //Force a process tick to update the light immediately
else
use_power = IDLE_POWER_USE
set_light(0)
+ update_icon()
active_power_usage = (brightness * 10)
if(on != on_gs)
@@ -276,6 +346,12 @@
removeStaticPower(static_power_used, STATIC_LIGHT)
+/obj/machinery/light/process()
+ if(has_power() && cell)
+ cell.charge = min(cell.maxcharge, cell.charge + 0.2) //Recharge emergency power automatically while not using it
+ if(emergency_mode && !use_emergency_power(0.2))
+ update(FALSE) //Disables emergency mode and sets the color to normal
+
/obj/machinery/light/proc/burn_out()
if(status == LIGHT_OK)
status = LIGHT_BURNED
@@ -289,6 +365,9 @@
on = (s && status == LIGHT_OK)
update()
+/obj/machinery/light/get_cell()
+ return cell
+
// examine verb
/obj/machinery/light/examine(mob/user)
..()
@@ -301,6 +380,8 @@
to_chat(user, "The [fitting] is burnt out.")
if(LIGHT_BROKEN)
to_chat(user, "The [fitting] has been smashed.")
+ if(cell)
+ to_chat(user, "Its backup power charge meter reads [(cell.charge / cell.maxcharge) * 100]%.")
@@ -384,6 +465,10 @@
drop_light_tube()
new /obj/item/stack/cable_coil(loc, 1, "red")
transfer_fingerprints_to(newlight)
+ if(cell)
+ newlight.cell = cell
+ cell.forceMove(newlight)
+ cell = null
qdel(src)
/obj/machinery/light/attacked_by(obj/item/I, mob/living/user)
@@ -415,6 +500,11 @@
if(BURN)
playsound(src.loc, 'sound/items/welder.ogg', 100, 1)
+// returns if the light has power /but/ is manually turned off
+// if a light is turned off, it won't activate emergency power
+/obj/machinery/light/proc/turned_off()
+ var/area/A = get_area(src)
+ return !A.lightswitch && A.power_light
// returns whether this light has power
// true if area has power and lightswitch is on
@@ -422,6 +512,27 @@
var/area/A = get_area(src)
return A.lightswitch && A.power_light
+// returns whether this light has emergency power
+// can also return if it has access to a certain amount of that power
+/obj/machinery/light/proc/has_emergency_power(pwr)
+ if(no_emergency || !cell)
+ return FALSE
+ if(pwr ? cell.charge >= pwr : cell.charge)
+ return status == LIGHT_OK
+
+// attempts to use power from the installed emergency cell, returns true if it does and false if it doesn't
+/obj/machinery/light/proc/use_emergency_power(pwr = 0.2)
+ if(!has_emergency_power(pwr))
+ return FALSE
+ if(cell.charge > 300) //it's meant to handle 120 W, ya doofus
+ visible_message("[src] short-circuits from too powerful of a power cell!")
+ burn_out()
+ return FALSE
+ cell.use(pwr)
+ set_light(brightness * 0.25, max(0.5, 0.75 * (cell.charge / cell.maxcharge)), "#FF3232") //RGB: 255, 50, 50
+ return TRUE
+
+
/obj/machinery/light/proc/flicker(var/amount = rand(10, 20))
set waitfor = 0
if(flickering)
@@ -441,7 +552,9 @@
// ai attack - make lights flicker, because why not
/obj/machinery/light/attack_ai(mob/user)
- src.flicker(1)
+ no_emergency = !no_emergency
+ to_chat(user, "Emergency lights for this fixture have been [no_emergency ? "disabled" : "enabled"].")
+ update(FALSE)
return
// attack with hand - remove tube/bulb
diff --git a/code/modules/research/designs/autolathe_designs.dm b/code/modules/research/designs/autolathe_designs.dm
index 03424346e8..77150a44d8 100644
--- a/code/modules/research/designs/autolathe_designs.dm
+++ b/code/modules/research/designs/autolathe_designs.dm
@@ -799,3 +799,11 @@
materials = list(MAT_METAL = 300, MAT_GLASS = 200)
build_path = /obj/item/device/slime_scanner
category = list("initial", "Misc")
+
+/datum/design/miniature_power_cell
+ name = "Light Fixture Battery"
+ id = "miniature_power_cell"
+ build_type = AUTOLATHE
+ materials = list(MAT_GLASS = 20)
+ build_path = /obj/item/stock_parts/cell/emergency_light
+ category = list("initial", "Electronics")
diff --git a/icons/mob/actions/actions_AI.dmi b/icons/mob/actions/actions_AI.dmi
index c6e4e1dae7..2ddc7923cc 100644
Binary files a/icons/mob/actions/actions_AI.dmi and b/icons/mob/actions/actions_AI.dmi differ
diff --git a/tgui/src/interfaces/apc.ract b/tgui/src/interfaces/apc.ract
index 686d20755a..fb1d7d2799 100644
--- a/tgui/src/interfaces/apc.ract
+++ b/tgui/src/interfaces/apc.ract
@@ -121,6 +121,15 @@
{{/if}}
{{/if}}
+
+
+ {{#if data.locked && !data.siliconUser}}
+ {{data.emergencyLights ? "Enabled" : "Disabled"}}
+ {{else}}
+ {{data.emergencyLights ? "Enabled" : "Disabled"}}
+ {{/if}}
+
+
{{#if data.locked && !data.siliconUser}}