From c99f7e3b22ad3ff6fec4e193b5c3eab1e0c6fbe7 Mon Sep 17 00:00:00 2001 From: Samuel Date: Mon, 26 Oct 2020 19:57:02 -0400 Subject: [PATCH] Boo! Rework (#14543) * Boo Refactor * Boo-Refactor Review Items Co-authored-by: SteelSlayer <42044220+SteelSlayer@users.noreply.github.com> * Vending Machines Flicker * Dave's Fire PR Review Mixtape Ft. SteelSlayer Co-authored-by: SteelSlayer <42044220+SteelSlayer@users.noreply.github.com> Co-authored-by: SteelSlayer <42044220+SteelSlayer@users.noreply.github.com> --- code/datums/spell.dm | 21 +++--- code/game/area/areas.dm | 2 + code/game/area/ss13_areas.dm | 4 + code/game/atoms.dm | 9 ++- code/game/machinery/computer/computer.dm | 38 +++++++++- code/game/machinery/doors/airlock.dm | 9 +++ code/game/machinery/machinery.dm | 17 +++++ code/game/machinery/status_display.dm | 12 ++- code/game/machinery/vending.dm | 73 +++++++++++++++---- code/game/turfs/turf.dm | 4 - code/modules/mob/dead/observer/observer.dm | 2 +- code/modules/mob/dead/observer/spells.dm | 28 +++++-- code/modules/mob/living/carbon/human/human.dm | 1 + code/modules/mob/living/living_defines.dm | 1 - code/modules/mob/mob_defines.dm | 2 + code/modules/power/apc.dm | 32 +++++++- code/modules/power/lighting.dm | 35 +++++---- 17 files changed, 229 insertions(+), 61 deletions(-) diff --git a/code/datums/spell.dm b/code/datums/spell.dm index 3e77be7d6e2..eab723fa329 100644 --- a/code/datums/spell.dm +++ b/code/datums/spell.dm @@ -5,7 +5,7 @@ var/panel = "Debug"//What panel the proc holder needs to go on. var/active = FALSE //Used by toggle based abilities. var/ranged_mousepointer - var/mob/living/ranged_ability_user + var/mob/ranged_ability_user /obj/effect/proc_holder/singularity_act() return @@ -15,7 +15,7 @@ GLOBAL_LIST_INIT(spells, typesof(/obj/effect/proc_holder/spell)) -/obj/effect/proc_holder/proc/InterceptClickOn(mob/living/user, params, atom/A) +/obj/effect/proc_holder/proc/InterceptClickOn(mob/user, params, atom/A) if(user.ranged_ability != src) to_chat(user, "[user.ranged_ability.name] has been disabled.") user.ranged_ability.remove_ranged_ability(user) @@ -38,7 +38,7 @@ GLOBAL_LIST_INIT(spells, typesof(/obj/effect/proc_holder/spell)) spell.remove_ranged_ability(spell.ranged_ability_user) return ..() -/obj/effect/proc_holder/proc/add_ranged_ability(mob/living/user, var/msg) +/obj/effect/proc_holder/proc/add_ranged_ability(mob/user, var/msg) if(!user || !user.client) return if(user.ranged_ability && user.ranged_ability != src) @@ -61,7 +61,7 @@ GLOBAL_LIST_INIT(spells, typesof(/obj/effect/proc_holder/spell)) if(C && ranged_mousepointer && C.mouse_pointer_icon == ranged_mousepointer) C.mouse_pointer_icon = initial(C.mouse_pointer_icon) -/obj/effect/proc_holder/proc/remove_ranged_ability(mob/living/user, var/msg) +/obj/effect/proc_holder/proc/remove_ranged_ability(mob/user, var/msg) if(!user || (user.ranged_ability && user.ranged_ability != src)) //To avoid removing the wrong ability return user.ranged_ability = null @@ -135,7 +135,7 @@ GLOBAL_LIST_INIT(spells, typesof(/obj/effect/proc_holder/spell)) * @param start_recharge If the proc should set the cooldown * @param user The caster of the spell */ -/obj/effect/proc_holder/spell/proc/cast_check(charge_check = TRUE, start_recharge = TRUE, mob/living/user = usr) //checks if the spell can be cast based on its settings; skipcharge is used when an additional cast_check is called inside the spell +/obj/effect/proc_holder/spell/proc/cast_check(charge_check = TRUE, start_recharge = TRUE, mob/user = usr) //checks if the spell can be cast based on its settings; skipcharge is used when an additional cast_check is called inside the spell if(!can_cast(user, charge_check, TRUE)) return FALSE @@ -428,9 +428,12 @@ GLOBAL_LIST_INIT(spells, typesof(/obj/effect/proc_holder/spell)) var/selection_deactivated_message = "You choose to not cast this spell." var/allowed_type = /mob/living // Which type the targets have to be var/auto_target_single = TRUE // If the spell should auto select a target if only one is found + /// does this spell generate attack logs? + var/create_logs = TRUE /obj/effect/proc_holder/spell/targeted/click/Click() - var/mob/living/user = usr + // biased goddamn variable types assuming that we're alive. eat shit. + var/mob/user = usr if(!istype(user)) return @@ -455,7 +458,7 @@ GLOBAL_LIST_INIT(spells, typesof(/obj/effect/proc_holder/spell)) if(target && cast_check(TRUE, TRUE, user)) // Singular target found. Cast it instantly to_chat(user, "Only one target found. Casting [src] on [target]!") - perform(list(target), user = user) + perform(list(target), user = user, make_attack_logs = create_logs) return TRUE return FALSE @@ -500,7 +503,7 @@ GLOBAL_LIST_INIT(spells, typesof(/obj/effect/proc_holder/spell)) revert_cast(user) return FALSE - perform(targets, user = user) + perform(targets, user = user, make_attack_logs = create_logs) remove_ranged_ability(user) return TRUE @@ -513,7 +516,7 @@ GLOBAL_LIST_INIT(spells, typesof(/obj/effect/proc_holder/spell)) return istype(target, allowed_type) && (include_user || target != user) && \ (target in view_or_range(range, user, selection_type)) -/obj/effect/proc_holder/spell/targeted/click/choose_targets(mob/living/user, atom/A) // Not used +/obj/effect/proc_holder/spell/targeted/click/choose_targets(mob/user, atom/A) // Not used return /obj/effect/proc_holder/spell/aoe_turf/choose_targets(mob/user = usr) diff --git a/code/game/area/areas.dm b/code/game/area/areas.dm index aec3ea58f01..fa175c94c19 100644 --- a/code/game/area/areas.dm +++ b/code/game/area/areas.dm @@ -68,6 +68,8 @@ var/parallax_movedir = 0 var/moving = FALSE + /// "Haunted" areas such as the morgue and chapel are easier to boo. Because flavor. + var/is_haunted = FALSE /area/Initialize(mapload) GLOB.all_areas += src diff --git a/code/game/area/ss13_areas.dm b/code/game/area/ss13_areas.dm index 8792e710f2b..21cc4c3a4c0 100644 --- a/code/game/area/ss13_areas.dm +++ b/code/game/area/ss13_areas.dm @@ -929,6 +929,7 @@ NOTE: there are two lists of areas in the end of this file: centcom and station /area/chapel icon_state = "chapel" ambientsounds = HOLY_SOUNDS + is_haunted = TRUE /area/chapel/main name = "\improper Chapel" @@ -1299,6 +1300,7 @@ NOTE: there are two lists of areas in the end of this file: centcom and station name = "\improper Morgue" icon_state = "morgue" ambientsounds = SPOOKY_SOUNDS + is_haunted = TRUE /area/medical/chemistry name = "\improper Chemistry" @@ -1777,10 +1779,12 @@ NOTE: there are two lists of areas in the end of this file: centcom and station /area/derelict/medical/morgue name = "\improper Derelict Morgue" icon_state = "morgue" + is_haunted = TRUE /area/derelict/medical/chapel name = "\improper Derelict Chapel" icon_state = "chapel" + is_haunted = TRUE /area/derelict/teleporter name = "\improper Derelict Teleporter" diff --git a/code/game/atoms.dm b/code/game/atoms.dm index 5f1ef3e94c7..3f20a5ea13a 100644 --- a/code/game/atoms.dm +++ b/code/game/atoms.dm @@ -420,8 +420,15 @@ if(AM && isturf(AM.loc)) step(AM, turn(AM.dir, 180)) +/* + * Base proc, terribly named but it's all over the code so who cares I guess right? + * + * Returns FALSE by default, if a child returns TRUE it is implied that the atom has in + * some way done a spooky thing. Current usage is so that Boo knows if it needs to cool + * down or not, but this could be expanded upon if you were a bad enough dude. + */ /atom/proc/get_spooked() - return + return FALSE /** Base proc, intended to be overriden. diff --git a/code/game/machinery/computer/computer.dm b/code/game/machinery/computer/computer.dm index 2ec8611f420..4e2fb78c385 100644 --- a/code/game/machinery/computer/computer.dm +++ b/code/game/machinery/computer/computer.dm @@ -17,6 +17,10 @@ var/light_range_on = 2 var/light_power_on = 1 var/overlay_layer + /// Are we in the middle of a flicker event? + var/flickering = FALSE + /// Are we forcing the icon to be represented in a no-power state? + var/force_no_power_icon_state = FALSE /obj/machinery/computer/New() overlay_layer = layer @@ -36,9 +40,41 @@ set_light(0) visible_message("[src] grows dim, its screen barely readable.") +/* + * Reimp, flash the screen on and off repeatedly. + */ +/obj/machinery/computer/flicker() + if(flickering) + return FALSE + + if(stat & (BROKEN|NOPOWER)) + return FALSE + + flickering = TRUE + INVOKE_ASYNC(src, /obj/machinery/computer/.proc/flicker_event) + + return TRUE + +/* + * Proc to be called by invoke_async in the above flicker() proc. + */ +/obj/machinery/computer/proc/flicker_event() + var/amount = rand(5, 15) + + for(var/i in 1 to amount) + force_no_power_icon_state = TRUE + update_icon() + sleep(rand(1, 3)) + + force_no_power_icon_state = FALSE + update_icon() + sleep(rand(1, 10)) + update_icon() + flickering = FALSE + /obj/machinery/computer/update_icon() overlays.Cut() - if(stat & NOPOWER) + if((stat & NOPOWER) || force_no_power_icon_state) if(icon_keyboard) overlays += image(icon,"[icon_keyboard]_off",overlay_layer) return diff --git a/code/game/machinery/doors/airlock.dm b/code/game/machinery/doors/airlock.dm index a732348ac3c..5189ff1095a 100644 --- a/code/game/machinery/doors/airlock.dm +++ b/code/game/machinery/doors/airlock.dm @@ -123,6 +123,15 @@ About the new airlock wires panel: ..() wires = new(src) +/* + * reimp, imitate an access denied event. + */ +/obj/machinery/door/airlock/flicker() + if(arePowerSystemsOn()) + do_animate("deny") + return TRUE + return FALSE + /obj/machinery/door/airlock/Initialize() . = ..() if(closeOtherId != null) diff --git a/code/game/machinery/machinery.dm b/code/game/machinery/machinery.dm index 9745a01c875..5a8f401ac29 100644 --- a/code/game/machinery/machinery.dm +++ b/code/game/machinery/machinery.dm @@ -93,6 +93,8 @@ Class Procs: Compiled by Aygar */ +#define MACHINE_FLICKER_CHANCE 0.05 // roughly 1/2000 chance of a machine flickering on any given tick. That means in a two hour round each machine will flicker on average a little less than two times. + /obj/machinery name = "machinery" icon = 'icons/obj/stationobjs.dmi' @@ -125,6 +127,19 @@ Class Procs: /// A reference to a `datum/radio_frequency`. Gives the machine the ability to interact with things using radio signals. var/datum/radio_frequency/radio_connection +/* + * reimp, attempts to flicker this machinery if the behavior is supported. + */ +/obj/machinery/get_spooked() + return flicker() + +/* + * Base class, attempt to flicker. Returns TRUE if we complete our 'flicker + * behavior', false otherwise. + */ +/obj/machinery/proc/flicker() + return FALSE + /obj/machinery/Initialize(mapload) if(!armor) armor = list(melee = 25, bullet = 10, laser = 10, energy = 0, bomb = 0, bio = 0, rad = 0, fire = 50, acid = 70) @@ -199,6 +214,8 @@ Class Procs: use_power(idle_power_usage,power_channel, 1) else if(use_power >= ACTIVE_POWER_USE) use_power(active_power_usage,power_channel, 1) + if(prob(MACHINE_FLICKER_CHANCE)) + flicker() return 1 /obj/machinery/proc/multitool_topic(mob/user, list/href_list, obj/O) diff --git a/code/game/machinery/status_display.dm b/code/game/machinery/status_display.dm index b8b9af9ad95..30dfc78fe29 100644 --- a/code/game/machinery/status_display.dm +++ b/code/game/machinery/status_display.dm @@ -78,8 +78,12 @@ set_picture("ai_bsod") ..(severity) -/obj/machinery/status_display/get_spooked() +/obj/machinery/status_display/flicker() + if(stat & (NOPOWER | BROKEN)) + return FALSE + spookymode = TRUE + return TRUE // set what is displayed /obj/machinery/status_display/proc/update() @@ -230,8 +234,12 @@ set_picture("ai_bsod") ..(severity) -/obj/machinery/ai_status_display/get_spooked() +/obj/machinery/ai_status_display/flicker() + if(stat & (NOPOWER | BROKEN)) + return FALSE + spookymode = TRUE + return TRUE /obj/machinery/ai_status_display/proc/update() if(mode==0) //Blank diff --git a/code/game/machinery/vending.dm b/code/game/machinery/vending.dm index 57b75863ae9..fd6c85de9c4 100644 --- a/code/game/machinery/vending.dm +++ b/code/game/machinery/vending.dm @@ -95,6 +95,11 @@ /// the actual item inserted var/obj/item/inserted_item = null + /// blocks further flickering while true + var/flickering = FALSE + /// do I look unpowered, even when powered? + var/force_no_power_icon_state = FALSE + /obj/machinery/vending/Initialize(mapload) var/build_inv = FALSE if(!refill_canister) @@ -138,6 +143,49 @@ for(var/obj/item/vending_refill/VR in component_parts) restock(VR) +/obj/machinery/vending/update_icon() + cut_overlays() + if(panel_open) + add_overlay(image(icon, "[initial(icon_state)]-panel")) + + if(stat & BROKEN) + icon_state = "[initial(icon_state)]-broken" + else if (stat & NOPOWER || force_no_power_icon_state) + icon_state = "[initial(icon_state)]-off" + else + icon_state = initial(icon_state) + +/* + * Reimp, flash the screen on and off repeatedly. + */ +/obj/machinery/vending/flicker() + if(flickering) + return FALSE + + if(stat & (BROKEN|NOPOWER)) + return FALSE + + flickering = TRUE + INVOKE_ASYNC(src, /obj/machinery/vending/.proc/flicker_event) + + return TRUE + +/* + * Proc to be called by invoke_async in the above flicker() proc. + */ +/obj/machinery/vending/proc/flicker_event() + var/amount = rand(5, 15) + + for(var/i in 1 to amount) + force_no_power_icon_state = TRUE + update_icon() + sleep(rand(1, 3)) + + force_no_power_icon_state = FALSE + update_icon() + sleep(rand(1, 10)) + update_icon() + flickering = FALSE /** * Build src.produdct_records from the products lists @@ -285,12 +333,8 @@ return if(anchored) panel_open = !panel_open - if(panel_open) - SCREWDRIVER_OPEN_PANEL_MESSAGE - overlays += image(icon, "[initial(icon_state)]-panel") - else - SCREWDRIVER_CLOSE_PANEL_MESSAGE - overlays.Cut() + panel_open ? SCREWDRIVER_OPEN_PANEL_MESSAGE : SCREWDRIVER_CLOSE_PANEL_MESSAGE + update_icon() SStgui.update_uis(src) /obj/machinery/vending/wirecutter_act(mob/user, obj/item/I) @@ -753,21 +797,18 @@ atom_say(message) /obj/machinery/vending/power_change() - if(stat & BROKEN) - icon_state = "[initial(icon_state)]-broken" + if(powered()) + stat &= ~NOPOWER + update_icon() else - if( powered() ) - icon_state = initial(icon_state) - stat &= ~NOPOWER - else - spawn(rand(0, 15)) - icon_state = "[initial(icon_state)]-off" - stat |= NOPOWER + spawn(rand(0, 15)) + stat |= NOPOWER + update_icon() /obj/machinery/vending/obj_break(damage_flag) if(!(stat & BROKEN)) stat |= BROKEN - icon_state = "[initial(icon_state)]-broken" + update_icon() var/dump_amount = 0 var/found_anything = TRUE diff --git a/code/game/turfs/turf.dm b/code/game/turfs/turf.dm index 8170f9fbe5f..0f49f4e2508 100644 --- a/code/game/turfs/turf.dm +++ b/code/game/turfs/turf.dm @@ -330,10 +330,6 @@ /turf/proc/Bless() flags |= NOJAUNT -/turf/get_spooked() - for(var/atom/movable/AM in contents) - AM.get_spooked() - // Defined here to avoid runtimes /turf/proc/MakeDry(wet_setting = TURF_WET_WATER) return diff --git a/code/modules/mob/dead/observer/observer.dm b/code/modules/mob/dead/observer/observer.dm index 341f84afc7f..6398ce71a8c 100644 --- a/code/modules/mob/dead/observer/observer.dm +++ b/code/modules/mob/dead/observer/observer.dm @@ -41,7 +41,7 @@ GLOBAL_VAR_INIT(observer_default_invisibility, INVISIBILITY_OBSERVER) /mob/dead/observer/proc/open_spawners_menu) // Our new boo spell. - AddSpell(new /obj/effect/proc_holder/spell/aoe_turf/boo(null)) + AddSpell(new /obj/effect/proc_holder/spell/targeted/click/boo(null)) can_reenter_corpse = flags & GHOST_CAN_REENTER started_as_observer = flags & GHOST_IS_OBSERVER diff --git a/code/modules/mob/dead/observer/spells.dm b/code/modules/mob/dead/observer/spells.dm index e890098b29b..6cb5d898082 100644 --- a/code/modules/mob/dead/observer/spells.dm +++ b/code/modules/mob/dead/observer/spells.dm @@ -9,22 +9,38 @@ GLOBAL_LIST_INIT(boo_phrases, list( "It feels like someone's standing behind you.", )) -/obj/effect/proc_holder/spell/aoe_turf/boo +/obj/effect/proc_holder/spell/targeted/click/boo name = "Boo!" desc = "Fuck with the living." + selection_deactivated_message = "Your presence will not be known. For now." + selection_activated_message = "You prepare to reach across the veil. Left-click to influence a target!" + auto_target_single = FALSE + allowed_type = /atom // No subtypes are safe from spookage. ghost = TRUE action_icon_state = "boo" school = "transmutation" - charge_max = 600 + charge_max = 2 MINUTES starts_charged = FALSE clothes_req = 0 stat_allowed = 1 invocation = "" invocation_type = "none" - range = 1 // Or maybe 3? + range = 20 + // no need to spam admins regarding boo casts + create_logs = FALSE -/obj/effect/proc_holder/spell/aoe_turf/boo/cast(list/targets, mob/user = usr) - for(var/turf/T in targets) - T.get_spooked() + +/obj/effect/proc_holder/spell/targeted/click/boo/cast(list/targets, mob/user = usr) + var/atom/target = targets[1] + ASSERT(istype(target)) + + if(target.get_spooked()) + var/area/spook_zone = get_area(target) + if (spook_zone.is_haunted == TRUE) + to_chat(usr, "The veil is weak in [spook_zone], it took less effort to influence [target].") + charge_counter = charge_max / 2 + return + + charge_counter = charge_max * 0.9 // We've targetted a non-spookable object! Try again fast! diff --git a/code/modules/mob/living/carbon/human/human.dm b/code/modules/mob/living/carbon/human/human.dm index 942e4cc9a23..42e861ce45e 100644 --- a/code/modules/mob/living/carbon/human/human.dm +++ b/code/modules/mob/living/carbon/human/human.dm @@ -1962,6 +1962,7 @@ Eyes need to have significantly high darksight to shine unless the mob has the X /mob/living/carbon/human/get_spooked() to_chat(src, "[pick(GLOB.boo_phrases)]") + return TRUE /mob/living/carbon/human/extinguish_light() // Parent function handles stuff the human may be holding diff --git a/code/modules/mob/living/living_defines.dm b/code/modules/mob/living/living_defines.dm index f8a5f08b313..65ef4943059 100644 --- a/code/modules/mob/living/living_defines.dm +++ b/code/modules/mob/living/living_defines.dm @@ -57,7 +57,6 @@ var/gene_stability = DEFAULT_GENE_STABILITY var/ignore_gene_stability = 0 - var/obj/effect/proc_holder/ranged_ability //Any ranged ability the mob has, as a click override var/tesla_ignore = FALSE diff --git a/code/modules/mob/mob_defines.dm b/code/modules/mob/mob_defines.dm index 80404460b50..85017c66617 100644 --- a/code/modules/mob/mob_defines.dm +++ b/code/modules/mob/mob_defines.dm @@ -199,3 +199,5 @@ var/forced_look = null // This can either be a numerical direction or a soft object reference (UID). It makes the mob always face towards the selected thing. var/registered_z + + var/obj/effect/proc_holder/ranged_ability //Any ranged ability the mob has, as a click override diff --git a/code/modules/power/apc.dm b/code/modules/power/apc.dm index df9abed6733..fbad25dff64 100644 --- a/code/modules/power/apc.dm +++ b/code/modules/power/apc.dm @@ -419,18 +419,27 @@ update_icon() updating_icon = 0 -/obj/machinery/power/apc/get_spooked(second_pass = FALSE) +/obj/machinery/power/apc/flicker(second_pass = FALSE) if(opened || panel_open) - return + return FALSE if(stat & (NOPOWER | BROKEN)) - return + return FALSE if(!second_pass) //The first time, we just cut overlays - addtimer(CALLBACK(src, /obj/machinery/power/apc/proc.get_spooked, TRUE), 1) + addtimer(CALLBACK(src, /obj/machinery/power/apc/proc.flicker, TRUE), 1) cut_overlays() + // APC power distruptions have a chance to propogate to other machines on its network + for(var/obj/machinery/M in area) + // Please don't cascade, thanks + if(M == src) + continue + if(prob(10)) + M.flicker() else flick("apcemag", src) //Second time we cause the APC to update its icon, then add a timer to update icon later addtimer(CALLBACK(src, /obj/machinery/power/apc/proc.update_icon, TRUE), 10) + return TRUE + //attack with an item - open/close cover, insert cell, or (un)lock interface /obj/machinery/power/apc/attackby(obj/item/W, mob/living/user, params) @@ -1231,6 +1240,21 @@ else if(last_ch != charging) queue_icon_update() + if(prob(MACHINE_FLICKER_CHANCE)) + flicker() + + // lights don't have their own processing loop, so APCs will be the father they never had. 3x as likely to cause a light flicker in a particular area, pick a light to flicker at random + if(prob(MACHINE_FLICKER_CHANCE) * 3) + var/list/lights = list() + for(var/obj/machinery/light/L in area) + lights += L + + if(lights.len > 0) + var/obj/machinery/light/picked_light = pick(lights) + ASSERT(istype(picked_light)) + picked_light.flicker() + + /obj/machinery/power/apc/proc/autoset(var/val, var/on) if(on==0) if(val==2) // if on, return off diff --git a/code/modules/power/lighting.dm b/code/modules/power/lighting.dm index 2fcba4656a7..d6089a421d0 100644 --- a/code/modules/power/lighting.dm +++ b/code/modules/power/lighting.dm @@ -289,9 +289,6 @@ on = FALSE return -/obj/machinery/light/get_spooked() - flicker() - /** * Updates the light's properties * @@ -519,32 +516,38 @@ var/area/A = get_area(src) return A.lightswitch && A.power_light -/obj/machinery/light/proc/flicker(amount = rand(10, 20)) +/obj/machinery/light/flicker(amount = rand(20, 30)) if(flickering) - return + return FALSE - if(!on) - return + if(!on || status != LIGHT_OK) + return FALSE flickering = TRUE INVOKE_ASYNC(src, /obj/machinery/light/.proc/flicker_event, amount) + return TRUE + /** * Flicker routine for the light. * Called by invoke_async so the parent proc can return immediately. */ -/obj/machinery/light/proc/flicker_event(amount) - for(var/i in 1 to amount) - if(status != LIGHT_OK) - break - on = !on - update() // No param means that ghosts can burn out lights - sleep(rand(5, 15)) - if(status == LIGHT_OK) - on = TRUE +/obj/machinery/light/proc/flicker_event(var/amount) + if(on && status == LIGHT_OK) + for(var/i = 0; i < amount; i++) + if(status != LIGHT_OK) + break + on = FALSE + update(FALSE) + sleep(rand(1, 3)) + on = (status == LIGHT_OK) + update(FALSE) + sleep(rand(1, 10)) + on = (status == LIGHT_OK) update(FALSE) flickering = FALSE + // ai attack - make lights flicker, because why not /obj/machinery/light/attack_ai(mob/user) flicker(1)