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